Enhance your writing with grammar checking, Hebrew-English translation, and contextual text expansion with Formal-Persuasive and Casual-Everyday tones
Size
54.3 KB
Version
1.1.3
Created
Oct 15, 2025
Updated
about 1 month ago
1// ==UserScript==
2// @name AI Writing Assistant - Grammar, Translation & Expansion
3// @description Enhance your writing with grammar checking, Hebrew-English translation, and contextual text expansion with Formal-Persuasive and Casual-Everyday tones
4// @version 1.1.3
5// @match https://*/*
6// @match http://*/*
7// @icon https://cdn.oaistatic.com/assets/favicon-l4nq08hd.svg
8// ==/UserScript==
9(function() {
10 'use strict';
11
12 // ============================================
13 // CONFIGURATION & CONSTANTS
14 // ============================================
15
16 const CONFIG = {
17 tones: {
18 FORMAL: 'Formal-Persuasive',
19 CASUAL: 'Casual-Everyday'
20 },
21 colors: {
22 background: '#FDFDFD',
23 backgroundAlt: '#F7F8FB',
24 primary: '#3B82F6',
25 textPrimary: '#111827',
26 textSecondary: '#374151',
27 error: '#EF4444',
28 success: '#10B981'
29 },
30 shortcuts: {
31 commandPalette: 'k',
32 grammar: 'g',
33 translate: 't',
34 expand: 'e'
35 }
36 };
37
38 // ============================================
39 // STATE MANAGEMENT
40 // ============================================
41
42 let state = {
43 selectedText: '',
44 selectedElement: null,
45 currentTone: CONFIG.tones.FORMAL,
46 showExplanations: true,
47 history: [],
48 glossary: []
49 };
50
51 // ============================================
52 // UTILITY FUNCTIONS
53 // ============================================
54
55 function detectLanguage(text) {
56 const hebrewPattern = /[\u0590-\u05FF]/;
57 return hebrewPattern.test(text) ? 'he' : 'en';
58 }
59
60 function getDefaultToneForPlatform() {
61 const url = window.location.href;
62 if (url.includes('mail.google.com') || url.includes('linkedin.com')) {
63 return CONFIG.tones.FORMAL;
64 } else if (url.includes('web.whatsapp.com')) {
65 return CONFIG.tones.CASUAL;
66 } else if (url.includes('docs.google.com')) {
67 return CONFIG.tones.FORMAL;
68 }
69 return CONFIG.tones.FORMAL;
70 }
71
72 function debounce(func, wait) {
73 let timeout;
74 return function executedFunction(...args) {
75 const later = () => {
76 clearTimeout(timeout);
77 func(...args);
78 };
79 clearTimeout(timeout);
80 timeout = setTimeout(later, wait);
81 };
82 }
83
84 async function saveToHistory(action, input, output) {
85 const historyItem = {
86 timestamp: Date.now(),
87 action,
88 input,
89 output,
90 tone: state.currentTone
91 };
92
93 state.history.unshift(historyItem);
94 if (state.history.length > 50) {
95 state.history = state.history.slice(0, 50);
96 }
97
98 await GM.setValue('writing_assistant_history', JSON.stringify(state.history));
99 console.log('Saved to history:', action);
100 }
101
102 async function loadState() {
103 try {
104 const savedTone = await GM.getValue('writing_assistant_tone', CONFIG.tones.FORMAL);
105 const savedExplanations = await GM.getValue('writing_assistant_explanations', true);
106 const savedHistory = await GM.getValue('writing_assistant_history', '[]');
107 const savedGlossary = await GM.getValue('writing_assistant_glossary', '[]');
108
109 state.currentTone = savedTone;
110 state.showExplanations = savedExplanations;
111 state.history = JSON.parse(savedHistory);
112 state.glossary = JSON.parse(savedGlossary);
113
114 console.log('State loaded:', state);
115 } catch (error) {
116 console.error('Error loading state:', error);
117 }
118 }
119
120 // ============================================
121 // AI FUNCTIONS
122 // ============================================
123
124 async function checkGrammar(text) {
125 console.log('Checking grammar for:', text);
126
127 const prompt = `You are an expert English grammar checker. Analyze the following text and identify ALL grammar, spelling, punctuation, syntax, and word choice errors.
128
129Text to check: "${text}"
130
131For each error found, provide:
1321. The exact error text
1332. The suggested correction
1343. A brief explanation (10-20 words)
1354. The error category (Subject-Verb Agreement, Tense Consistency, Comma Splices, Capitalization, Articles, Spelling, Punctuation, Word Choice, etc.)
136
137IMPORTANT RULES:
138- Never change the meaning of sentences
139- Preserve proper nouns, brand names, and technical terms
140- Only suggest corrections for actual errors
141- Be precise and specific`;
142
143 try {
144 const result = await RM.aiCall(prompt, {
145 type: 'json_schema',
146 json_schema: {
147 name: 'grammar_check',
148 schema: {
149 type: 'object',
150 properties: {
151 errors: {
152 type: 'array',
153 items: {
154 type: 'object',
155 properties: {
156 errorText: { type: 'string' },
157 correction: { type: 'string' },
158 explanation: { type: 'string' },
159 category: { type: 'string' }
160 },
161 required: ['errorText', 'correction', 'explanation', 'category']
162 }
163 },
164 correctedText: { type: 'string' }
165 },
166 required: ['errors', 'correctedText']
167 }
168 }
169 });
170
171 await saveToHistory('grammar', text, result.correctedText);
172 return result;
173 } catch (error) {
174 console.error('Grammar check error:', error);
175 throw error;
176 }
177 }
178
179 async function translateText(text, targetLang = null) {
180 const sourceLang = detectLanguage(text);
181 const target = targetLang || (sourceLang === 'he' ? 'en' : 'he');
182
183 console.log(`Translating from ${sourceLang} to ${target}`);
184
185 const langNames = {
186 he: 'Hebrew',
187 en: 'English'
188 };
189
190 const prompt = `Translate the following text from ${langNames[sourceLang]} to ${langNames[target]}.
191
192Text: "${text}"
193
194Tone: ${state.currentTone}
195
196IMPORTANT RULES:
197- Maintain the ${state.currentTone} tone in the translation
198- Preserve formatting: paragraphs, lists, emojis
199- Keep proper nouns, brand names unchanged
200- Provide an accurate, natural translation
201- Do NOT expand or rewrite beyond translation`;
202
203 try {
204 const result = await RM.aiCall(prompt, {
205 type: 'json_schema',
206 json_schema: {
207 name: 'translation',
208 schema: {
209 type: 'object',
210 properties: {
211 translatedText: { type: 'string' },
212 sourceLang: { type: 'string' },
213 targetLang: { type: 'string' }
214 },
215 required: ['translatedText', 'sourceLang', 'targetLang']
216 }
217 }
218 });
219
220 await saveToHistory('translate', text, result.translatedText);
221 return result;
222 } catch (error) {
223 console.error('Translation error:', error);
224 throw error;
225 }
226 }
227
228 async function expandText(text, mode = 'auto') {
229 console.log('Expanding text:', text, 'Mode:', mode);
230
231 // Determine expansion mode
232 const wordCount = text.split(/\s+/).length;
233 const isSentence = wordCount < 20 || mode === 'sentence';
234
235 let prompt;
236 if (isSentence) {
237 prompt = `Expand the following sentence by adding 1-2 complementary sentences that add value, clarity, or a call-to-action.
238
239Original sentence: "${text}"
240
241Tone: ${state.currentTone}
242
243Guidelines for ${state.currentTone}:
244${state.currentTone === CONFIG.tones.FORMAL ?
245 '- Use measured, professional language\n- Active verbs\n- Clear call-to-action\n- Avoid slang' :
246 '- Conversational, friendly style\n- Short and direct\n- Casual but clear'}
247
248Provide 2 different variations.`;
249 } else {
250 prompt = `Expand the following paragraph to 80-140 words using this structure:
2511. Intent/Purpose
2522. Supporting argument
2533. Example/Value proposition
2544. ${state.currentTone === CONFIG.tones.FORMAL ? 'Professional call-to-action' : 'Direct, friendly message'}
255
256Original text: "${text}"
257
258Tone: ${state.currentTone}
259
260Guidelines for ${state.currentTone}:
261${state.currentTone === CONFIG.tones.FORMAL ?
262 '- Professional, persuasive language\n- Structured arguments\n- Clear business value\n- Action-oriented conclusion' :
263 '- Conversational, approachable style\n- Natural flow\n- Relatable examples\n- Friendly closing'}
264
265IMPORTANT:
266- Preserve all facts and key information
267- Do NOT add false or sensitive data
268- Maintain key terms and phrases
269- Provide 2 different variations`;
270 }
271
272 try {
273 const result = await RM.aiCall(prompt, {
274 type: 'json_schema',
275 json_schema: {
276 name: 'text_expansion',
277 schema: {
278 type: 'object',
279 properties: {
280 variations: {
281 type: 'array',
282 items: { type: 'string' },
283 minItems: 2,
284 maxItems: 2
285 },
286 mode: { type: 'string', enum: ['sentence', 'paragraph'] },
287 wordCount: { type: 'number' }
288 },
289 required: ['variations', 'mode']
290 }
291 }
292 });
293
294 await saveToHistory('expand', text, result.variations[0]);
295 return result;
296 } catch (error) {
297 console.error('Expansion error:', error);
298 throw error;
299 }
300 }
301
302 // ============================================
303 // UI COMPONENTS
304 // ============================================
305
306 function createFloatingToolbar() {
307 const toolbar = document.createElement('div');
308 toolbar.id = 'writing-assistant-toolbar';
309 toolbar.style.cssText = `
310 position: absolute;
311 display: none;
312 background: ${CONFIG.colors.background};
313 border: 1px solid #E5E7EB;
314 border-radius: 8px;
315 padding: 8px;
316 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
317 z-index: 999999;
318 font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
319 font-size: 14px;
320 gap: 4px;
321 display: flex;
322 align-items: center;
323 `;
324
325 const buttons = [
326 { id: 'grammar', label: 'Grammar', icon: '✓', action: handleGrammarCheck },
327 { id: 'translate', label: 'Translate', icon: '⇄', action: handleTranslate },
328 { id: 'expand', label: 'Expand', icon: '↗', action: handleExpand },
329 { id: 'tone', label: state.currentTone === CONFIG.tones.FORMAL ? 'Formal' : 'Casual', icon: '◐', action: toggleTone }
330 ];
331
332 buttons.forEach(btn => {
333 const button = document.createElement('button');
334 button.id = `wa-btn-${btn.id}`;
335 button.innerHTML = `<span style="margin-right: 4px;">${btn.icon}</span>${btn.label}`;
336 button.style.cssText = `
337 background: white;
338 border: 1px solid #E5E7EB;
339 border-radius: 6px;
340 padding: 6px 12px;
341 cursor: pointer;
342 font-size: 13px;
343 color: ${CONFIG.colors.textPrimary};
344 transition: all 0.2s;
345 white-space: nowrap;
346 `;
347
348 button.addEventListener('mouseenter', () => {
349 button.style.background = CONFIG.colors.backgroundAlt;
350 button.style.borderColor = CONFIG.colors.primary;
351 });
352
353 button.addEventListener('mouseleave', () => {
354 button.style.background = 'white';
355 button.style.borderColor = '#E5E7EB';
356 });
357
358 button.addEventListener('click', async (e) => {
359 e.preventDefault();
360 e.stopPropagation();
361 await btn.action();
362 });
363
364 toolbar.appendChild(button);
365 });
366
367 // More menu button
368 const moreBtn = document.createElement('button');
369 moreBtn.innerHTML = '⋯';
370 moreBtn.style.cssText = `
371 background: white;
372 border: 1px solid #E5E7EB;
373 border-radius: 6px;
374 padding: 6px 12px;
375 cursor: pointer;
376 font-size: 16px;
377 color: ${CONFIG.colors.textPrimary};
378 `;
379 moreBtn.addEventListener('click', (e) => {
380 e.preventDefault();
381 e.stopPropagation();
382 showCommandPalette();
383 });
384 toolbar.appendChild(moreBtn);
385
386 document.body.appendChild(toolbar);
387 return toolbar;
388 }
389
390 function showToolbar(x, y) {
391 const toolbar = document.getElementById('writing-assistant-toolbar');
392 if (!toolbar) return;
393
394 toolbar.style.display = 'flex';
395 toolbar.style.left = `${x}px`;
396 toolbar.style.top = `${y + 10}px`;
397
398 // Update tone button label
399 const toneBtn = document.getElementById('wa-btn-tone');
400 if (toneBtn) {
401 toneBtn.innerHTML = `<span style="margin-right: 4px;">◐</span>${state.currentTone === CONFIG.tones.FORMAL ? 'Formal' : 'Casual'}`;
402 }
403 }
404
405 function hideToolbar() {
406 const toolbar = document.getElementById('writing-assistant-toolbar');
407 if (toolbar) {
408 toolbar.style.display = 'none';
409 }
410 }
411
412 function createResultModal(title, content, variations = null) {
413 // Remove existing modal
414 const existing = document.getElementById('writing-assistant-modal');
415 if (existing) existing.remove();
416
417 const modal = document.createElement('div');
418 modal.id = 'writing-assistant-modal';
419 modal.style.cssText = `
420 position: fixed;
421 top: 50%;
422 left: 50%;
423 transform: translate(-50%, -50%);
424 background: ${CONFIG.colors.background};
425 border-radius: 12px;
426 padding: 24px;
427 box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
428 z-index: 1000000;
429 max-width: 600px;
430 width: 90%;
431 max-height: 80vh;
432 overflow-y: auto;
433 font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
434 `;
435
436 const titleEl = document.createElement('h3');
437 titleEl.textContent = title;
438 titleEl.style.cssText = `
439 margin: 0 0 16px 0;
440 font-size: 18px;
441 font-weight: 600;
442 color: ${CONFIG.colors.textPrimary};
443 `;
444 modal.appendChild(titleEl);
445
446 if (variations && Array.isArray(variations)) {
447 variations.forEach((variation, index) => {
448 const varContainer = document.createElement('div');
449 varContainer.style.cssText = `
450 background: white;
451 border: 1px solid #E5E7EB;
452 border-radius: 8px;
453 padding: 16px;
454 margin-bottom: 12px;
455 `;
456
457 const varLabel = document.createElement('div');
458 varLabel.textContent = `Variation ${index + 1}`;
459 varLabel.style.cssText = `
460 font-size: 12px;
461 font-weight: 600;
462 color: ${CONFIG.colors.textSecondary};
463 margin-bottom: 8px;
464 text-transform: uppercase;
465 letter-spacing: 0.5px;
466 `;
467 varContainer.appendChild(varLabel);
468
469 const varText = document.createElement('div');
470 varText.textContent = variation;
471 varText.style.cssText = `
472 color: ${CONFIG.colors.textPrimary};
473 line-height: 1.6;
474 margin-bottom: 12px;
475 `;
476 varContainer.appendChild(varText);
477
478 const btnContainer = document.createElement('div');
479 btnContainer.style.cssText = 'display: flex; gap: 8px;';
480
481 const applyBtn = document.createElement('button');
482 applyBtn.textContent = 'Apply';
483 applyBtn.style.cssText = `
484 background: ${CONFIG.colors.primary};
485 color: white;
486 border: none;
487 border-radius: 6px;
488 padding: 8px 16px;
489 cursor: pointer;
490 font-size: 13px;
491 font-weight: 500;
492 `;
493 applyBtn.addEventListener('click', () => {
494 applyText(variation);
495 modal.remove();
496 removeOverlay();
497 });
498 btnContainer.appendChild(applyBtn);
499
500 const copyBtn = document.createElement('button');
501 copyBtn.textContent = 'Copy';
502 copyBtn.style.cssText = `
503 background: white;
504 color: ${CONFIG.colors.textPrimary};
505 border: 1px solid #E5E7EB;
506 border-radius: 6px;
507 padding: 8px 16px;
508 cursor: pointer;
509 font-size: 13px;
510 `;
511 copyBtn.addEventListener('click', async () => {
512 await GM.setClipboard(variation);
513 copyBtn.textContent = 'Copied!';
514 setTimeout(() => copyBtn.textContent = 'Copy', 2000);
515 });
516 btnContainer.appendChild(copyBtn);
517
518 varContainer.appendChild(btnContainer);
519 modal.appendChild(varContainer);
520 });
521 } else {
522 const contentEl = document.createElement('div');
523 contentEl.style.cssText = `
524 background: white;
525 border: 1px solid #E5E7EB;
526 border-radius: 8px;
527 padding: 16px;
528 color: ${CONFIG.colors.textPrimary};
529 line-height: 1.6;
530 margin-bottom: 16px;
531 `;
532 contentEl.textContent = content;
533 modal.appendChild(contentEl);
534
535 const btnContainer = document.createElement('div');
536 btnContainer.style.cssText = 'display: flex; gap: 8px; justify-content: flex-end;';
537
538 const applyBtn = document.createElement('button');
539 applyBtn.textContent = 'Apply';
540 applyBtn.style.cssText = `
541 background: ${CONFIG.colors.primary};
542 color: white;
543 border: none;
544 border-radius: 6px;
545 padding: 8px 16px;
546 cursor: pointer;
547 font-size: 13px;
548 font-weight: 500;
549 `;
550 applyBtn.addEventListener('click', () => {
551 applyText(content);
552 modal.remove();
553 removeOverlay();
554 });
555 btnContainer.appendChild(applyBtn);
556
557 const copyBtn = document.createElement('button');
558 copyBtn.textContent = 'Copy';
559 copyBtn.style.cssText = `
560 background: white;
561 color: ${CONFIG.colors.textPrimary};
562 border: 1px solid #E5E7EB;
563 border-radius: 6px;
564 padding: 8px 16px;
565 cursor: pointer;
566 font-size: 13px;
567 `;
568 copyBtn.addEventListener('click', async () => {
569 await GM.setClipboard(content);
570 copyBtn.textContent = 'Copied!';
571 setTimeout(() => copyBtn.textContent = 'Copy', 2000);
572 });
573 btnContainer.appendChild(copyBtn);
574
575 modal.appendChild(btnContainer);
576 }
577
578 const closeBtn = document.createElement('button');
579 closeBtn.textContent = '×';
580 closeBtn.style.cssText = `
581 position: absolute;
582 top: 16px;
583 right: 16px;
584 background: none;
585 border: none;
586 font-size: 24px;
587 cursor: pointer;
588 color: ${CONFIG.colors.textSecondary};
589 width: 32px;
590 height: 32px;
591 display: flex;
592 align-items: center;
593 justify-content: center;
594 border-radius: 6px;
595 `;
596 closeBtn.addEventListener('click', () => {
597 modal.remove();
598 removeOverlay();
599 });
600 modal.appendChild(closeBtn);
601
602 createOverlay();
603 document.body.appendChild(modal);
604 }
605
606 function createOverlay() {
607 const existing = document.getElementById('writing-assistant-overlay');
608 if (existing) return;
609
610 const overlay = document.createElement('div');
611 overlay.id = 'writing-assistant-overlay';
612 overlay.style.cssText = `
613 position: fixed;
614 top: 0;
615 left: 0;
616 right: 0;
617 bottom: 0;
618 background: rgba(0, 0, 0, 0.5);
619 z-index: 999999;
620 `;
621 overlay.addEventListener('click', () => {
622 const modal = document.getElementById('writing-assistant-modal');
623 if (modal) modal.remove();
624 overlay.remove();
625 });
626 document.body.appendChild(overlay);
627 }
628
629 function removeOverlay() {
630 const overlay = document.getElementById('writing-assistant-overlay');
631 if (overlay) overlay.remove();
632 }
633
634 function showLoadingIndicator(message = 'Processing...') {
635 const existing = document.getElementById('writing-assistant-loading');
636 if (existing) existing.remove();
637
638 const loader = document.createElement('div');
639 loader.id = 'writing-assistant-loading';
640 loader.style.cssText = `
641 position: fixed;
642 top: 20px;
643 right: 20px;
644 background: ${CONFIG.colors.primary};
645 color: white;
646 padding: 12px 20px;
647 border-radius: 8px;
648 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
649 z-index: 1000001;
650 font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
651 font-size: 14px;
652 display: flex;
653 align-items: center;
654 gap: 10px;
655 `;
656
657 const spinner = document.createElement('div');
658 spinner.style.cssText = `
659 width: 16px;
660 height: 16px;
661 border: 2px solid rgba(255, 255, 255, 0.3);
662 border-top-color: white;
663 border-radius: 50%;
664 animation: spin 0.8s linear infinite;
665 `;
666
667 const text = document.createElement('span');
668 text.textContent = message;
669
670 loader.appendChild(spinner);
671 loader.appendChild(text);
672 document.body.appendChild(loader);
673
674 // Add animation
675 if (!document.getElementById('writing-assistant-spin-style')) {
676 const style = document.createElement('style');
677 style.id = 'writing-assistant-spin-style';
678 style.textContent = `
679 @keyframes spin {
680 to { transform: rotate(360deg); }
681 }
682 `;
683 document.head.appendChild(style);
684 }
685 }
686
687 function hideLoadingIndicator() {
688 const loader = document.getElementById('writing-assistant-loading');
689 if (loader) loader.remove();
690 }
691
692 function showCommandPalette() {
693 const existing = document.getElementById('writing-assistant-command-palette');
694 if (existing) {
695 existing.remove();
696 removeOverlay();
697 return;
698 }
699
700 const palette = document.createElement('div');
701 palette.id = 'writing-assistant-command-palette';
702 palette.style.cssText = `
703 position: fixed;
704 top: 20%;
705 left: 50%;
706 transform: translateX(-50%);
707 background: ${CONFIG.colors.background};
708 border-radius: 12px;
709 padding: 16px;
710 box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
711 z-index: 1000000;
712 width: 500px;
713 max-width: 90%;
714 font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
715 `;
716
717 const searchInput = document.createElement('input');
718 searchInput.type = 'text';
719 searchInput.placeholder = 'Type a command...';
720 searchInput.style.cssText = `
721 width: 100%;
722 padding: 12px;
723 border: 1px solid #E5E7EB;
724 border-radius: 8px;
725 font-size: 14px;
726 margin-bottom: 12px;
727 box-sizing: border-box;
728 `;
729 palette.appendChild(searchInput);
730
731 const commands = [
732 { label: 'Check Grammar', shortcut: 'Ctrl+G', action: handleGrammarCheck },
733 { label: 'Translate Text', shortcut: 'Ctrl+T', action: handleTranslate },
734 { label: 'Expand Text', shortcut: 'Ctrl+E', action: handleExpand },
735 { label: 'Toggle Tone (Formal ⇄ Casual)', shortcut: '', action: toggleTone },
736 { label: 'Toggle Explanations', shortcut: '', action: toggleExplanations },
737 { label: 'View History', shortcut: '', action: showHistory }
738 ];
739
740 const commandList = document.createElement('div');
741 commandList.style.cssText = 'max-height: 400px; overflow-y: auto;';
742
743 function renderCommands(filter = '') {
744 commandList.innerHTML = '';
745 const filtered = commands.filter(cmd =>
746 cmd.label.toLowerCase().includes(filter.toLowerCase())
747 );
748
749 filtered.forEach(cmd => {
750 const cmdEl = document.createElement('div');
751 cmdEl.style.cssText = `
752 padding: 12px;
753 border-radius: 6px;
754 cursor: pointer;
755 display: flex;
756 justify-content: space-between;
757 align-items: center;
758 margin-bottom: 4px;
759 `;
760
761 cmdEl.addEventListener('mouseenter', () => {
762 cmdEl.style.background = CONFIG.colors.backgroundAlt;
763 });
764
765 cmdEl.addEventListener('mouseleave', () => {
766 cmdEl.style.background = 'transparent';
767 });
768
769 cmdEl.addEventListener('click', async () => {
770 palette.remove();
771 removeOverlay();
772 await cmd.action();
773 });
774
775 const label = document.createElement('span');
776 label.textContent = cmd.label;
777 label.style.color = CONFIG.colors.textPrimary;
778 cmdEl.appendChild(label);
779
780 if (cmd.shortcut) {
781 const shortcut = document.createElement('span');
782 shortcut.textContent = cmd.shortcut;
783 shortcut.style.cssText = `
784 color: ${CONFIG.colors.textSecondary};
785 font-size: 12px;
786 background: ${CONFIG.colors.backgroundAlt};
787 padding: 4px 8px;
788 border-radius: 4px;
789 `;
790 cmdEl.appendChild(shortcut);
791 }
792
793 commandList.appendChild(cmdEl);
794 });
795 }
796
797 renderCommands();
798
799 searchInput.addEventListener('input', (e) => {
800 renderCommands(e.target.value);
801 });
802
803 palette.appendChild(commandList);
804
805 createOverlay();
806 document.body.appendChild(palette);
807 searchInput.focus();
808 }
809
810 // ============================================
811 // ACTION HANDLERS
812 // ============================================
813
814 async function handleGrammarCheck() {
815 if (!state.selectedText) {
816 console.log('No text selected for grammar check');
817 return;
818 }
819
820 const lang = detectLanguage(state.selectedText);
821 if (lang !== 'en') {
822 createResultModal('Grammar Check', 'Grammar checking is only available for English text.');
823 return;
824 }
825
826 try {
827 showLoadingIndicator('Checking grammar...');
828 const result = await checkGrammar(state.selectedText);
829 hideLoadingIndicator();
830
831 if (result.errors.length === 0) {
832 createResultModal('Grammar Check', 'No errors found! Your text looks great.');
833 } else {
834 // Apply inline highlighting
835 applyInlineGrammarHighlighting(result);
836 }
837 } catch (error) {
838 hideLoadingIndicator();
839 console.error('Grammar check failed:', error);
840 createResultModal('Error', 'Failed to check grammar. Please try again.');
841 }
842 }
843
844 function applyInlineGrammarHighlighting(result) {
845 if (!state.selectedElement) {
846 console.log('No element to apply highlighting to');
847 createGrammarResultModal(result);
848 return;
849 }
850
851 const element = state.selectedElement;
852 let originalText = state.selectedText;
853
854 // Check if element supports rich content
855 const supportsRichContent = element.isContentEditable ||
856 element.tagName === 'TEXTAREA' ||
857 element.tagName === 'INPUT';
858
859 if (!supportsRichContent) {
860 // Fallback to modal view
861 createGrammarResultModal(result);
862 return;
863 }
864
865 // For contenteditable elements, we can add inline highlighting
866 if (element.isContentEditable) {
867 applyContentEditableHighlighting(element, result);
868 } else {
869 // For textarea/input, show modal with inline preview
870 createGrammarResultModal(result);
871 }
872 }
873
874 function applyContentEditableHighlighting(element, result) {
875 // Create a wrapper to show highlighted text
876 const selection = window.getSelection();
877 if (selection.rangeCount === 0) {
878 createGrammarResultModal(result);
879 return;
880 }
881
882 const range = selection.getRangeAt(0);
883 const originalText = range.toString();
884
885 // Build highlighted HTML
886 let highlightedHTML = originalText;
887 let offset = 0;
888
889 // Sort errors by position in text
890 const sortedErrors = result.errors.sort((a, b) => {
891 const posA = originalText.indexOf(a.errorText);
892 const posB = originalText.indexOf(b.errorText);
893 return posA - posB;
894 });
895
896 sortedErrors.forEach((error, index) => {
897 const errorPos = highlightedHTML.indexOf(error.errorText, offset);
898 if (errorPos !== -1) {
899 const errorId = `grammar-error-${Date.now()}-${index}`;
900 const before = highlightedHTML.substring(0, errorPos);
901 const after = highlightedHTML.substring(errorPos + error.errorText.length);
902
903 const errorSpan = `<span class="grammar-error" data-error-id="${errorId}" style="text-decoration: underline wavy ${CONFIG.colors.error}; cursor: pointer; background-color: rgba(239, 68, 68, 0.1);" title="${error.category}: ${error.explanation}">${error.errorText}</span>`;
904
905 highlightedHTML = before + errorSpan + after;
906 offset = errorPos + errorSpan.length;
907
908 // Store error data for click handling
909 setTimeout(() => {
910 const errorElement = document.querySelector(`[data-error-id="${errorId}"]`);
911 if (errorElement) {
912 errorElement.addEventListener('click', (e) => {
913 e.preventDefault();
914 e.stopPropagation();
915 showGrammarErrorPopup(e.target, error, result.correctedText);
916 });
917 }
918 }, 100);
919 }
920 });
921
922 // Replace selected text with highlighted version
923 range.deleteContents();
924 const tempDiv = document.createElement('div');
925 tempDiv.innerHTML = highlightedHTML;
926
927 while (tempDiv.firstChild) {
928 range.insertNode(tempDiv.lastChild);
929 }
930
931 // Show summary notification
932 showNotification(`Found ${result.errors.length} grammar issue${result.errors.length !== 1 ? 's' : ''}. Click on underlined text to fix.`);
933
934 // Also show the full modal for reference
935 setTimeout(() => {
936 createGrammarResultModal(result);
937 }, 500);
938 }
939
940 function showGrammarErrorPopup(targetElement, error, correctedFullText) {
941 // Remove existing popup
942 const existingPopup = document.getElementById('grammar-error-popup');
943 if (existingPopup) existingPopup.remove();
944
945 const popup = document.createElement('div');
946 popup.id = 'grammar-error-popup';
947
948 const rect = targetElement.getBoundingClientRect();
949 popup.style.cssText = `
950 position: fixed;
951 left: ${rect.left}px;
952 top: ${rect.bottom + 5}px;
953 background: white;
954 border: 1px solid ${CONFIG.colors.error};
955 border-radius: 8px;
956 padding: 12px;
957 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
958 z-index: 1000000;
959 max-width: 300px;
960 font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
961 `;
962
963 const category = document.createElement('div');
964 category.textContent = error.category;
965 category.style.cssText = `
966 font-size: 10px;
967 font-weight: 600;
968 color: ${CONFIG.colors.error};
969 text-transform: uppercase;
970 margin-bottom: 6px;
971 letter-spacing: 0.5px;
972 `;
973 popup.appendChild(category);
974
975 const errorText = document.createElement('div');
976 errorText.innerHTML = `<span style="text-decoration: line-through; color: ${CONFIG.colors.error};">${error.errorText}</span> → <span style="color: ${CONFIG.colors.success}; font-weight: 500;">${error.correction}</span>`;
977 errorText.style.cssText = `
978 margin-bottom: 8px;
979 font-size: 13px;
980 `;
981 popup.appendChild(errorText);
982
983 if (state.showExplanations) {
984 const explanation = document.createElement('div');
985 explanation.textContent = error.explanation;
986 explanation.style.cssText = `
987 font-size: 12px;
988 color: ${CONFIG.colors.textSecondary};
989 margin-bottom: 10px;
990 font-style: italic;
991 `;
992 popup.appendChild(explanation);
993 }
994
995 const btnContainer = document.createElement('div');
996 btnContainer.style.cssText = 'display: flex; gap: 6px;';
997
998 const fixBtn = document.createElement('button');
999 fixBtn.textContent = 'Fix';
1000 fixBtn.style.cssText = `
1001 background: ${CONFIG.colors.primary};
1002 color: white;
1003 border: none;
1004 border-radius: 4px;
1005 padding: 6px 12px;
1006 cursor: pointer;
1007 font-size: 12px;
1008 font-weight: 500;
1009 `;
1010 fixBtn.addEventListener('click', () => {
1011 targetElement.textContent = error.correction;
1012 targetElement.style.textDecoration = 'none';
1013 targetElement.style.backgroundColor = 'transparent';
1014 targetElement.classList.remove('grammar-error');
1015 popup.remove();
1016 showNotification('Error fixed!');
1017 });
1018 btnContainer.appendChild(fixBtn);
1019
1020 const ignoreBtn = document.createElement('button');
1021 ignoreBtn.textContent = 'Ignore';
1022 ignoreBtn.style.cssText = `
1023 background: white;
1024 color: ${CONFIG.colors.textPrimary};
1025 border: 1px solid #E5E7EB;
1026 border-radius: 4px;
1027 padding: 6px 12px;
1028 cursor: pointer;
1029 font-size: 12px;
1030 `;
1031 ignoreBtn.addEventListener('click', () => {
1032 targetElement.style.textDecoration = 'none';
1033 targetElement.style.backgroundColor = 'transparent';
1034 targetElement.classList.remove('grammar-error');
1035 popup.remove();
1036 });
1037 btnContainer.appendChild(ignoreBtn);
1038
1039 popup.appendChild(btnContainer);
1040
1041 document.body.appendChild(popup);
1042
1043 // Close popup when clicking outside
1044 setTimeout(() => {
1045 document.addEventListener('click', function closePopup(e) {
1046 if (!popup.contains(e.target) && e.target !== targetElement) {
1047 popup.remove();
1048 document.removeEventListener('click', closePopup);
1049 }
1050 });
1051 }, 100);
1052 }
1053
1054 function createGrammarResultModal(result) {
1055 const existing = document.getElementById('writing-assistant-modal');
1056 if (existing) existing.remove();
1057
1058 const modal = document.createElement('div');
1059 modal.id = 'writing-assistant-modal';
1060 modal.style.cssText = `
1061 position: fixed;
1062 top: 50%;
1063 left: 50%;
1064 transform: translate(-50%, -50%);
1065 background: ${CONFIG.colors.background};
1066 border-radius: 12px;
1067 padding: 24px;
1068 box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
1069 z-index: 1000000;
1070 max-width: 700px;
1071 width: 90%;
1072 max-height: 80vh;
1073 overflow-y: auto;
1074 font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1075 `;
1076
1077 const title = document.createElement('h3');
1078 title.textContent = `Grammar Check - ${result.errors.length} issue${result.errors.length !== 1 ? 's' : ''} found`;
1079 title.style.cssText = `
1080 margin: 0 0 16px 0;
1081 font-size: 18px;
1082 font-weight: 600;
1083 color: ${CONFIG.colors.textPrimary};
1084 `;
1085 modal.appendChild(title);
1086
1087 result.errors.forEach((error, index) => {
1088 const errorEl = document.createElement('div');
1089 errorEl.style.cssText = `
1090 background: white;
1091 border: 1px solid #E5E7EB;
1092 border-left: 3px solid ${CONFIG.colors.error};
1093 border-radius: 8px;
1094 padding: 16px;
1095 margin-bottom: 12px;
1096 `;
1097
1098 const category = document.createElement('div');
1099 category.textContent = error.category;
1100 category.style.cssText = `
1101 font-size: 11px;
1102 font-weight: 600;
1103 color: ${CONFIG.colors.error};
1104 margin-bottom: 8px;
1105 text-transform: uppercase;
1106 letter-spacing: 0.5px;
1107 `;
1108 errorEl.appendChild(category);
1109
1110 const errorText = document.createElement('div');
1111 errorText.innerHTML = `<span style="text-decoration: line-through; color: ${CONFIG.colors.error};">${error.errorText}</span> → <span style="color: ${CONFIG.colors.success}; font-weight: 500;">${error.correction}</span>`;
1112 errorText.style.cssText = `
1113 margin-bottom: 8px;
1114 font-size: 14px;
1115 `;
1116 errorEl.appendChild(errorText);
1117
1118 if (state.showExplanations) {
1119 const explanation = document.createElement('div');
1120 explanation.textContent = error.explanation;
1121 explanation.style.cssText = `
1122 font-size: 13px;
1123 color: ${CONFIG.colors.textSecondary};
1124 font-style: italic;
1125 `;
1126 errorEl.appendChild(explanation);
1127 }
1128
1129 modal.appendChild(errorEl);
1130 });
1131
1132 const btnContainer = document.createElement('div');
1133 btnContainer.style.cssText = 'display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;';
1134
1135 const applyAllBtn = document.createElement('button');
1136 applyAllBtn.textContent = 'Apply All';
1137 applyAllBtn.style.cssText = `
1138 background: ${CONFIG.colors.primary};
1139 color: white;
1140 border: none;
1141 border-radius: 6px;
1142 padding: 10px 20px;
1143 cursor: pointer;
1144 font-size: 14px;
1145 font-weight: 500;
1146 `;
1147 applyAllBtn.addEventListener('click', () => {
1148 applyText(result.correctedText);
1149 modal.remove();
1150 removeOverlay();
1151 });
1152 btnContainer.appendChild(applyAllBtn);
1153
1154 const copyBtn = document.createElement('button');
1155 copyBtn.textContent = 'Copy Corrected';
1156 copyBtn.style.cssText = `
1157 background: white;
1158 color: ${CONFIG.colors.textPrimary};
1159 border: 1px solid #E5E7EB;
1160 border-radius: 6px;
1161 padding: 10px 20px;
1162 cursor: pointer;
1163 font-size: 14px;
1164 `;
1165 copyBtn.addEventListener('click', async () => {
1166 await GM.setClipboard(result.correctedText);
1167 copyBtn.textContent = 'Copied!';
1168 setTimeout(() => copyBtn.textContent = 'Copy Corrected', 2000);
1169 });
1170 btnContainer.appendChild(copyBtn);
1171
1172 modal.appendChild(btnContainer);
1173
1174 const closeBtn = document.createElement('button');
1175 closeBtn.textContent = '×';
1176 closeBtn.style.cssText = `
1177 position: absolute;
1178 top: 16px;
1179 right: 16px;
1180 background: none;
1181 border: none;
1182 font-size: 24px;
1183 cursor: pointer;
1184 color: ${CONFIG.colors.textSecondary};
1185 `;
1186 closeBtn.addEventListener('click', () => {
1187 modal.remove();
1188 removeOverlay();
1189 });
1190 modal.appendChild(closeBtn);
1191
1192 createOverlay();
1193 document.body.appendChild(modal);
1194 }
1195
1196 async function handleTranslate() {
1197 if (!state.selectedText) {
1198 console.log('No text selected for translation');
1199 return;
1200 }
1201
1202 try {
1203 showLoadingIndicator('Translating...');
1204 const result = await translateText(state.selectedText);
1205 hideLoadingIndicator();
1206
1207 createResultModal('Translation', result.translatedText);
1208 } catch (error) {
1209 hideLoadingIndicator();
1210 console.error('Translation failed:', error);
1211 createResultModal('Error', 'Failed to translate. Please try again.');
1212 }
1213 }
1214
1215 async function handleExpand() {
1216 if (!state.selectedText) {
1217 console.log('No text selected for expansion');
1218 return;
1219 }
1220
1221 try {
1222 showLoadingIndicator('Expanding text...');
1223 const result = await expandText(state.selectedText);
1224 hideLoadingIndicator();
1225
1226 createResultModal('Expanded Text', null, result.variations);
1227 } catch (error) {
1228 hideLoadingIndicator();
1229 console.error('Expansion failed:', error);
1230 createResultModal('Error', 'Failed to expand text. Please try again.');
1231 }
1232 }
1233
1234 async function toggleTone() {
1235 state.currentTone = state.currentTone === CONFIG.tones.FORMAL ? CONFIG.tones.CASUAL : CONFIG.tones.FORMAL;
1236 await GM.setValue('writing_assistant_tone', state.currentTone);
1237
1238 console.log('Tone switched to:', state.currentTone);
1239
1240 // Update toolbar button
1241 const toneBtn = document.getElementById('wa-btn-tone');
1242 if (toneBtn) {
1243 toneBtn.innerHTML = `<span style="margin-right: 4px;">◐</span>${state.currentTone === CONFIG.tones.FORMAL ? 'Formal' : 'Casual'}`;
1244 }
1245
1246 // Show notification
1247 showNotification(`Tone: ${state.currentTone}`);
1248 }
1249
1250 async function toggleExplanations() {
1251 state.showExplanations = !state.showExplanations;
1252 await GM.setValue('writing_assistant_explanations', state.showExplanations);
1253
1254 showNotification(`Explanations: ${state.showExplanations ? 'On' : 'Off'}`);
1255 }
1256
1257 function showHistory() {
1258 if (state.history.length === 0) {
1259 createResultModal('History', 'No history yet. Start using the assistant!');
1260 return;
1261 }
1262
1263 const existing = document.getElementById('writing-assistant-modal');
1264 if (existing) existing.remove();
1265
1266 const modal = document.createElement('div');
1267 modal.id = 'writing-assistant-modal';
1268 modal.style.cssText = `
1269 position: fixed;
1270 top: 50%;
1271 left: 50%;
1272 transform: translate(-50%, -50%);
1273 background: ${CONFIG.colors.background};
1274 border-radius: 12px;
1275 padding: 24px;
1276 box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
1277 z-index: 1000000;
1278 max-width: 700px;
1279 width: 90%;
1280 max-height: 80vh;
1281 overflow-y: auto;
1282 font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1283 `;
1284
1285 const title = document.createElement('h3');
1286 title.textContent = 'History';
1287 title.style.cssText = `
1288 margin: 0 0 16px 0;
1289 font-size: 18px;
1290 font-weight: 600;
1291 color: ${CONFIG.colors.textPrimary};
1292 `;
1293 modal.appendChild(title);
1294
1295 state.history.slice(0, 20).forEach(item => {
1296 const historyItem = document.createElement('div');
1297 historyItem.style.cssText = `
1298 background: white;
1299 border: 1px solid #E5E7EB;
1300 border-radius: 8px;
1301 padding: 12px;
1302 margin-bottom: 8px;
1303 `;
1304
1305 const header = document.createElement('div');
1306 header.style.cssText = 'display: flex; justify-content: space-between; margin-bottom: 8px;';
1307
1308 const action = document.createElement('span');
1309 action.textContent = item.action.toUpperCase();
1310 action.style.cssText = `
1311 font-size: 11px;
1312 font-weight: 600;
1313 color: ${CONFIG.colors.primary};
1314 `;
1315 header.appendChild(action);
1316
1317 const time = document.createElement('span');
1318 time.textContent = new Date(item.timestamp).toLocaleString();
1319 time.style.cssText = `
1320 font-size: 11px;
1321 color: ${CONFIG.colors.textSecondary};
1322 `;
1323 header.appendChild(time);
1324
1325 historyItem.appendChild(header);
1326
1327 const output = document.createElement('div');
1328 output.textContent = item.output.substring(0, 100) + (item.output.length > 100 ? '...' : '');
1329 output.style.cssText = `
1330 font-size: 13px;
1331 color: ${CONFIG.colors.textPrimary};
1332 `;
1333 historyItem.appendChild(output);
1334
1335 modal.appendChild(historyItem);
1336 });
1337
1338 const closeBtn = document.createElement('button');
1339 closeBtn.textContent = '×';
1340 closeBtn.style.cssText = `
1341 position: absolute;
1342 top: 16px;
1343 right: 16px;
1344 background: none;
1345 border: none;
1346 font-size: 24px;
1347 cursor: pointer;
1348 color: ${CONFIG.colors.textSecondary};
1349 `;
1350 closeBtn.addEventListener('click', () => {
1351 modal.remove();
1352 removeOverlay();
1353 });
1354 modal.appendChild(closeBtn);
1355
1356 createOverlay();
1357 document.body.appendChild(modal);
1358 }
1359
1360 function showNotification(message) {
1361 const notification = document.createElement('div');
1362 notification.style.cssText = `
1363 position: fixed;
1364 bottom: 20px;
1365 right: 20px;
1366 background: ${CONFIG.colors.textPrimary};
1367 color: white;
1368 padding: 12px 20px;
1369 border-radius: 8px;
1370 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
1371 z-index: 1000001;
1372 font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1373 font-size: 14px;
1374 `;
1375 notification.textContent = message;
1376 document.body.appendChild(notification);
1377
1378 setTimeout(() => {
1379 notification.remove();
1380 }, 2000);
1381 }
1382
1383 function applyText(text) {
1384 if (!state.selectedElement) {
1385 console.log('No element to apply text to');
1386 return;
1387 }
1388
1389 const element = state.selectedElement;
1390
1391 // Handle different input types
1392 if (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT') {
1393 element.value = text;
1394 element.dispatchEvent(new Event('input', { bubbles: true }));
1395 } else if (element.isContentEditable) {
1396 // For contenteditable elements
1397 const selection = window.getSelection();
1398 if (selection.rangeCount > 0) {
1399 const range = selection.getRangeAt(0);
1400 range.deleteContents();
1401 range.insertNode(document.createTextNode(text));
1402 } else {
1403 element.textContent = text;
1404 }
1405 element.dispatchEvent(new Event('input', { bubbles: true }));
1406 }
1407
1408 console.log('Text applied successfully');
1409 showNotification('Text applied!');
1410 }
1411
1412 // ============================================
1413 // EVENT LISTENERS
1414 // ============================================
1415
1416 function setupEventListeners() {
1417 // Text selection handler
1418 document.addEventListener('mouseup', debounce((e) => {
1419 const selection = window.getSelection();
1420 const selectedText = selection.toString().trim();
1421
1422 if (selectedText.length > 0) {
1423 state.selectedText = selectedText;
1424 state.selectedElement = e.target;
1425
1426 const range = selection.getRangeAt(0);
1427 const rect = range.getBoundingClientRect();
1428
1429 showToolbar(rect.left + rect.width / 2 - 150, rect.bottom + window.scrollY);
1430 console.log('Text selected:', selectedText);
1431 } else {
1432 hideToolbar();
1433 }
1434 }, 200));
1435
1436 // Click outside to hide toolbar
1437 document.addEventListener('mousedown', (e) => {
1438 const toolbar = document.getElementById('writing-assistant-toolbar');
1439 if (toolbar && !toolbar.contains(e.target)) {
1440 const selection = window.getSelection();
1441 if (!selection.toString().trim()) {
1442 hideToolbar();
1443 }
1444 }
1445 });
1446
1447 // Keyboard shortcuts
1448 document.addEventListener('keydown', async (e) => {
1449 // Command Palette: Ctrl/Cmd + K
1450 if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
1451 e.preventDefault();
1452 showCommandPalette();
1453 return;
1454 }
1455
1456 // Only process other shortcuts if text is selected
1457 if (!state.selectedText) return;
1458
1459 // Grammar: Ctrl/Cmd + G
1460 if ((e.ctrlKey || e.metaKey) && e.key === 'g') {
1461 e.preventDefault();
1462 await handleGrammarCheck();
1463 }
1464
1465 // Translate: Ctrl/Cmd + T
1466 if ((e.ctrlKey || e.metaKey) && e.key === 't') {
1467 e.preventDefault();
1468 await handleTranslate();
1469 }
1470
1471 // Expand: Ctrl/Cmd + E
1472 if ((e.ctrlKey || e.metaKey) && e.key === 'e') {
1473 e.preventDefault();
1474 await handleExpand();
1475 }
1476 });
1477
1478 console.log('Event listeners setup complete');
1479 }
1480
1481 // ============================================
1482 // INITIALIZATION
1483 // ============================================
1484
1485 async function init() {
1486 console.log('AI Writing Assistant initializing...');
1487
1488 // Load saved state
1489 await loadState();
1490
1491 // Set default tone based on platform
1492 if (!await GM.getValue('writing_assistant_tone')) {
1493 state.currentTone = getDefaultToneForPlatform();
1494 await GM.setValue('writing_assistant_tone', state.currentTone);
1495 }
1496
1497 // Create UI components
1498 createFloatingToolbar();
1499
1500 // Setup event listeners
1501 setupEventListeners();
1502
1503 console.log('AI Writing Assistant ready!');
1504 console.log('Current tone:', state.currentTone);
1505 console.log('Shortcuts: Ctrl/Cmd+K (Command Palette), Ctrl/Cmd+G (Grammar), Ctrl/Cmd+T (Translate), Ctrl/Cmd+E (Expand)');
1506 }
1507
1508 // Start the extension
1509 if (document.readyState === 'loading') {
1510 document.addEventListener('DOMContentLoaded', init);
1511 } else {
1512 init();
1513 }
1514
1515})();