Enhance your writing with AI-powered tools: improve, expand, summarize, and rewrite text
Size
13.6 KB
Version
1.0.1
Created
Jan 23, 2026
Updated
11 days ago
1// ==UserScript==
2// @name AI Writing Assistant for Lovable
3// @description Enhance your writing with AI-powered tools: improve, expand, summarize, and rewrite text
4// @version 1.0.1
5// @match https://*.lovable.dev/*
6// @match https://*.lovable.app/*
7// @icon https://lovable.dev/favicon.svg
8// ==/UserScript==
9(function() {
10 'use strict';
11
12 console.log('AI Writing Assistant for Lovable initialized');
13
14 // Debounce utility function
15 function debounce(func, wait) {
16 let timeout;
17 return function executedFunction(...args) {
18 const later = () => {
19 clearTimeout(timeout);
20 func(...args);
21 };
22 clearTimeout(timeout);
23 timeout = setTimeout(later, wait);
24 };
25 }
26
27 // State management
28 let currentActiveElement = null;
29 let toolbar = null;
30 let isProcessing = false;
31
32 // Create floating toolbar
33 function createToolbar() {
34 const toolbarContainer = document.createElement('div');
35 toolbarContainer.id = 'ai-writing-toolbar';
36 toolbarContainer.style.cssText = `
37 position: absolute;
38 display: none;
39 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
40 border-radius: 12px;
41 padding: 8px;
42 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
43 z-index: 999999;
44 gap: 6px;
45 flex-wrap: wrap;
46 max-width: 400px;
47 backdrop-filter: blur(10px);
48 border: 1px solid rgba(255, 255, 255, 0.2);
49 `;
50
51 const buttons = [
52 { id: 'improve', label: '✨ Improve', title: 'Improve writing quality (Ctrl+Shift+I)' },
53 { id: 'expand', label: '📝 Expand', title: 'Expand and add more details (Ctrl+Shift+E)' },
54 { id: 'summarize', label: '📋 Summarize', title: 'Make it shorter and concise (Ctrl+Shift+S)' },
55 { id: 'rewrite', label: '🔄 Rewrite', title: 'Rewrite in different style (Ctrl+Shift+R)' }
56 ];
57
58 buttons.forEach(btn => {
59 const button = document.createElement('button');
60 button.id = `ai-${btn.id}`;
61 button.textContent = btn.label;
62 button.title = btn.title;
63 button.style.cssText = `
64 background: rgba(255, 255, 255, 0.95);
65 border: none;
66 border-radius: 8px;
67 padding: 8px 14px;
68 font-size: 13px;
69 font-weight: 600;
70 cursor: pointer;
71 transition: all 0.2s ease;
72 color: #4a5568;
73 white-space: nowrap;
74 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
75 `;
76 button.addEventListener('mouseenter', () => {
77 button.style.transform = 'translateY(-2px)';
78 button.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.2)';
79 button.style.background = 'rgba(255, 255, 255, 1)';
80 });
81 button.addEventListener('mouseleave', () => {
82 button.style.transform = 'translateY(0)';
83 button.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
84 button.style.background = 'rgba(255, 255, 255, 0.95)';
85 });
86 button.addEventListener('click', () => handleAIAction(btn.id));
87 toolbarContainer.appendChild(button);
88 });
89
90 // Add loading indicator
91 const loadingIndicator = document.createElement('div');
92 loadingIndicator.id = 'ai-loading';
93 loadingIndicator.style.cssText = `
94 display: none;
95 background: rgba(255, 255, 255, 0.95);
96 border-radius: 8px;
97 padding: 8px 14px;
98 font-size: 13px;
99 font-weight: 600;
100 color: #4a5568;
101 align-items: center;
102 gap: 8px;
103 `;
104 loadingIndicator.innerHTML = `
105 <div style="width: 16px; height: 16px; border: 2px solid #667eea; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite;"></div>
106 <span>Processing...</span>
107 `;
108 toolbarContainer.appendChild(loadingIndicator);
109
110 document.body.appendChild(toolbarContainer);
111 return toolbarContainer;
112 }
113
114 // Add CSS animations
115 const style = document.createElement('style');
116 style.textContent = `
117 @keyframes spin {
118 to { transform: rotate(360deg); }
119 }
120 @keyframes fadeIn {
121 from { opacity: 0; transform: translateY(-10px); }
122 to { opacity: 1; transform: translateY(0); }
123 }
124 #ai-writing-toolbar {
125 animation: fadeIn 0.3s ease;
126 }
127 .ai-highlight {
128 outline: 2px solid #667eea !important;
129 outline-offset: 2px;
130 }
131 `;
132 document.head.appendChild(style);
133
134 // Position toolbar near the active element
135 function positionToolbar(element) {
136 if (!toolbar || !element) return;
137
138 const rect = element.getBoundingClientRect();
139 const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
140 const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
141
142 let top = rect.bottom + scrollTop + 8;
143 let left = rect.left + scrollLeft;
144
145 // Adjust if toolbar would go off-screen
146 const toolbarRect = toolbar.getBoundingClientRect();
147 if (left + toolbarRect.width > window.innerWidth) {
148 left = window.innerWidth - toolbarRect.width - 20;
149 }
150 if (top + toolbarRect.height > window.innerHeight + scrollTop) {
151 top = rect.top + scrollTop - toolbarRect.height - 8;
152 }
153
154 toolbar.style.top = `${top}px`;
155 toolbar.style.left = `${left}px`;
156 toolbar.style.display = 'flex';
157 }
158
159 // Show toolbar for text inputs and textareas
160 function showToolbarForElement(element) {
161 if (!element || isProcessing) return;
162
163 // Check if element is a text input or textarea
164 const isTextInput = element.tagName === 'TEXTAREA' ||
165 (element.tagName === 'INPUT' && ['text', 'email', 'search', 'url'].includes(element.type)) ||
166 element.contentEditable === 'true';
167
168 if (!isTextInput) return;
169
170 // Check if element has text content
171 const text = element.value || element.textContent || '';
172 if (text.trim().length < 3) return;
173
174 currentActiveElement = element;
175 element.classList.add('ai-highlight');
176 positionToolbar(element);
177 }
178
179 // Hide toolbar
180 function hideToolbar() {
181 if (toolbar) {
182 toolbar.style.display = 'none';
183 }
184 if (currentActiveElement) {
185 currentActiveElement.classList.remove('ai-highlight');
186 currentActiveElement = null;
187 }
188 }
189
190 // Handle AI actions
191 async function handleAIAction(action) {
192 if (!currentActiveElement || isProcessing) return;
193
194 const element = currentActiveElement;
195 const originalText = element.value || element.textContent || '';
196
197 if (originalText.trim().length < 3) {
198 showNotification('Please enter some text first', 'warning');
199 return;
200 }
201
202 isProcessing = true;
203 showLoading(true);
204
205 try {
206 console.log(`AI action: ${action} on text:`, originalText.substring(0, 50) + '...');
207
208 let prompt = '';
209 switch (action) {
210 case 'improve':
211 prompt = `Improve the following text by fixing grammar, enhancing clarity, and making it more professional while keeping the same meaning and tone:\n\n${originalText}`;
212 break;
213 case 'expand':
214 prompt = `Expand the following text by adding more details, examples, and context while maintaining the original message:\n\n${originalText}`;
215 break;
216 case 'summarize':
217 prompt = `Summarize the following text into a shorter, more concise version while keeping the key points:\n\n${originalText}`;
218 break;
219 case 'rewrite':
220 prompt = `Rewrite the following text in a different style while keeping the same meaning. Make it more engaging and creative:\n\n${originalText}`;
221 break;
222 }
223
224 const result = await RM.aiCall(prompt);
225 console.log('AI result received:', result.substring(0, 100) + '...');
226
227 // Update the element with the result
228 if (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT') {
229 element.value = result;
230 element.dispatchEvent(new Event('input', { bubbles: true }));
231 element.dispatchEvent(new Event('change', { bubbles: true }));
232 } else if (element.contentEditable === 'true') {
233 element.textContent = result;
234 element.dispatchEvent(new Event('input', { bubbles: true }));
235 }
236
237 showNotification(`Text ${action}ed successfully!`, 'success');
238
239 // Keep toolbar visible for next action
240 positionToolbar(element);
241
242 } catch (error) {
243 console.error('AI processing error:', error);
244 showNotification('Failed to process text. Please try again.', 'error');
245 } finally {
246 isProcessing = false;
247 showLoading(false);
248 }
249 }
250
251 // Show/hide loading indicator
252 function showLoading(show) {
253 const loadingEl = document.getElementById('ai-loading');
254 const buttons = toolbar.querySelectorAll('button');
255
256 if (show) {
257 loadingEl.style.display = 'flex';
258 buttons.forEach(btn => btn.style.display = 'none');
259 } else {
260 loadingEl.style.display = 'none';
261 buttons.forEach(btn => btn.style.display = 'block');
262 }
263 }
264
265 // Show notification
266 function showNotification(message, type = 'info') {
267 const notification = document.createElement('div');
268 notification.style.cssText = `
269 position: fixed;
270 top: 20px;
271 right: 20px;
272 background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
273 color: white;
274 padding: 16px 24px;
275 border-radius: 12px;
276 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
277 z-index: 1000000;
278 font-size: 14px;
279 font-weight: 600;
280 animation: fadeIn 0.3s ease;
281 max-width: 300px;
282 `;
283 notification.textContent = message;
284 document.body.appendChild(notification);
285
286 setTimeout(() => {
287 notification.style.opacity = '0';
288 notification.style.transform = 'translateY(-20px)';
289 notification.style.transition = 'all 0.3s ease';
290 setTimeout(() => notification.remove(), 300);
291 }, 3000);
292 }
293
294 // Handle keyboard shortcuts
295 function handleKeyboardShortcuts(e) {
296 if (!e.ctrlKey || !e.shiftKey) return;
297
298 const activeEl = document.activeElement;
299 const isTextInput = activeEl.tagName === 'TEXTAREA' ||
300 (activeEl.tagName === 'INPUT' && ['text', 'email', 'search', 'url'].includes(activeEl.type)) ||
301 activeEl.contentEditable === 'true';
302
303 if (!isTextInput) return;
304
305 let action = null;
306 switch (e.key.toLowerCase()) {
307 case 'i':
308 action = 'improve';
309 break;
310 case 'e':
311 action = 'expand';
312 break;
313 case 's':
314 action = 'summarize';
315 break;
316 case 'r':
317 action = 'rewrite';
318 break;
319 }
320
321 if (action) {
322 e.preventDefault();
323 currentActiveElement = activeEl;
324 showToolbarForElement(activeEl);
325 handleAIAction(action);
326 }
327 }
328
329 // Event listeners
330 const debouncedShowToolbar = debounce((e) => {
331 showToolbarForElement(e.target);
332 }, 300);
333
334 document.addEventListener('focusin', (e) => {
335 debouncedShowToolbar(e);
336 });
337
338 document.addEventListener('click', (e) => {
339 if (!toolbar) return;
340
341 // Hide toolbar if clicking outside
342 if (!toolbar.contains(e.target) && e.target !== currentActiveElement) {
343 hideToolbar();
344 }
345 });
346
347 document.addEventListener('keydown', handleKeyboardShortcuts);
348
349 // Handle scroll to reposition toolbar
350 window.addEventListener('scroll', debounce(() => {
351 if (currentActiveElement && toolbar.style.display === 'flex') {
352 positionToolbar(currentActiveElement);
353 }
354 }, 100));
355
356 // Observe DOM changes for dynamically added elements
357 const observer = new MutationObserver(debounce((mutations) => {
358 // Toolbar might need repositioning if DOM changes
359 if (currentActiveElement && toolbar.style.display === 'flex') {
360 positionToolbar(currentActiveElement);
361 }
362 }, 200));
363
364 observer.observe(document.body, {
365 childList: true,
366 subtree: true
367 });
368
369 // Initialize
370 function init() {
371 console.log('Initializing AI Writing Assistant...');
372 toolbar = createToolbar();
373 console.log('AI Writing Assistant ready! Focus on any text field to see the toolbar.');
374 console.log('Keyboard shortcuts: Ctrl+Shift+I (Improve), Ctrl+Shift+E (Expand), Ctrl+Shift+S (Summarize), Ctrl+Shift+R (Rewrite)');
375 }
376
377 // Start when DOM is ready
378 if (document.readyState === 'loading') {
379 document.addEventListener('DOMContentLoaded', init);
380 } else {
381 init();
382 }
383
384})();