AI Writing Assistant for Lovable

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})();
AI Writing Assistant for Lovable | Robomonkey