AI-powered grammar and spelling checker for text inputs and textareas
Size
12.1 KB
Version
1.1.3
Created
Oct 25, 2025
Updated
about 1 month ago
1// ==UserScript==
2// @name Grammar Checker Assistant
3// @description AI-powered grammar and spelling checker for text inputs and textareas
4// @version 1.1.3
5// @match https://*.robomonkey.io/*
6// @icon https://robomonkey.io/icon.png?adc3438f5fbb5315
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 // Debounce function to avoid excessive API calls
12 function debounce(func, wait) {
13 let timeout;
14 return function executedFunction(...args) {
15 const later = () => {
16 clearTimeout(timeout);
17 func(...args);
18 };
19 clearTimeout(timeout);
20 timeout = setTimeout(later, wait);
21 };
22 }
23
24 // Create grammar check UI
25 function createGrammarUI(element) {
26 // Check if UI already exists for this element
27 if (element.dataset.grammarCheckerEnabled) {
28 return;
29 }
30 element.dataset.grammarCheckerEnabled = 'true';
31
32 // Create container for grammar suggestions
33 const container = document.createElement('div');
34 container.className = 'grammar-checker-container';
35 container.style.cssText = `
36 position: absolute;
37 background: white;
38 border: 1px solid #e0e0e0;
39 border-radius: 8px;
40 padding: 12px;
41 box-shadow: 0 4px 12px rgba(0,0,0,0.15);
42 z-index: 10000;
43 max-width: 350px;
44 display: none;
45 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
46 font-size: 14px;
47 `;
48
49 // Create loading indicator
50 const loadingIndicator = document.createElement('div');
51 loadingIndicator.className = 'grammar-loading';
52 loadingIndicator.style.cssText = `
53 position: absolute;
54 top: -30px;
55 right: 0;
56 background: #4CAF50;
57 color: white;
58 padding: 6px 12px;
59 border-radius: 4px;
60 font-size: 12px;
61 display: none;
62 z-index: 10001;
63 `;
64 loadingIndicator.textContent = '✓ Checking grammar...';
65
66 // Position container relative to element
67 element.style.position = 'relative';
68 element.parentElement.style.position = 'relative';
69 element.parentElement.appendChild(container);
70 element.parentElement.appendChild(loadingIndicator);
71
72 // Check grammar function
73 const checkGrammar = debounce(async (text) => {
74 if (!text || text.trim().length < 3) {
75 container.style.display = 'none';
76 return;
77 }
78
79 loadingIndicator.style.display = 'block';
80
81 try {
82 const result = await RM.aiCall(
83 `Check the following text for grammar, spelling, and style issues. Provide specific corrections and improvements: "${text}"`,
84 {
85 type: 'json_schema',
86 json_schema: {
87 name: 'grammar_check',
88 schema: {
89 type: 'object',
90 properties: {
91 hasIssues: { type: 'boolean' },
92 issues: {
93 type: 'array',
94 items: {
95 type: 'object',
96 properties: {
97 type: { type: 'string', enum: ['grammar', 'spelling', 'style', 'punctuation'] },
98 original: { type: 'string' },
99 suggestion: { type: 'string' },
100 explanation: { type: 'string' }
101 },
102 required: ['type', 'original', 'suggestion', 'explanation']
103 }
104 },
105 correctedText: { type: 'string' }
106 },
107 required: ['hasIssues', 'issues']
108 }
109 }
110 }
111 );
112
113 loadingIndicator.style.display = 'none';
114
115 if (result.hasIssues && result.issues.length > 0) {
116 displaySuggestions(result, element, container);
117 } else {
118 container.innerHTML = '<div style="color: #4CAF50; font-weight: 500;">✓ No issues found!</div>';
119 container.style.display = 'block';
120 setTimeout(() => {
121 container.style.display = 'none';
122 }, 2000);
123 }
124
125 } catch (error) {
126 console.error('Grammar check failed:', error);
127 loadingIndicator.style.display = 'none';
128 container.innerHTML = '<div style="color: #f44336;">Error checking grammar. Please try again.</div>';
129 container.style.display = 'block';
130 }
131 }, 1500);
132
133 // Display suggestions
134 function displaySuggestions(result, element, container) {
135 container.innerHTML = '';
136
137 const title = document.createElement('div');
138 title.style.cssText = 'font-weight: 600; margin-bottom: 10px; color: #333; font-size: 15px;';
139 title.textContent = `Found ${result.issues.length} issue${result.issues.length > 1 ? 's' : ''}`;
140 container.appendChild(title);
141
142 result.issues.forEach((issue, index) => {
143 const issueDiv = document.createElement('div');
144 issueDiv.style.cssText = `
145 margin-bottom: 12px;
146 padding: 10px;
147 background: #f5f5f5;
148 border-radius: 6px;
149 border-left: 3px solid ${getIssueColor(issue.type)};
150 `;
151
152 const typeLabel = document.createElement('div');
153 typeLabel.style.cssText = `
154 font-size: 11px;
155 text-transform: uppercase;
156 color: ${getIssueColor(issue.type)};
157 font-weight: 600;
158 margin-bottom: 4px;
159 `;
160 typeLabel.textContent = issue.type;
161
162 const originalText = document.createElement('div');
163 originalText.style.cssText = 'text-decoration: line-through; color: #d32f2f; margin-bottom: 4px;';
164 originalText.textContent = issue.original;
165
166 const suggestionText = document.createElement('div');
167 suggestionText.style.cssText = 'color: #2e7d32; font-weight: 500; margin-bottom: 6px;';
168 suggestionText.textContent = '→ ' + issue.suggestion;
169
170 const explanation = document.createElement('div');
171 explanation.style.cssText = 'font-size: 12px; color: #666; margin-bottom: 8px;';
172 explanation.textContent = issue.explanation;
173
174 const applyBtn = document.createElement('button');
175 applyBtn.style.cssText = `
176 background: #4CAF50;
177 color: white;
178 border: none;
179 padding: 6px 12px;
180 border-radius: 4px;
181 cursor: pointer;
182 font-size: 12px;
183 font-weight: 500;
184 `;
185 applyBtn.textContent = 'Apply Fix';
186 applyBtn.onclick = () => {
187 const currentText = element.value;
188 element.value = currentText.replace(issue.original, issue.suggestion);
189 element.dispatchEvent(new Event('input', { bubbles: true }));
190 issueDiv.style.opacity = '0.5';
191 applyBtn.disabled = true;
192 applyBtn.textContent = 'Applied ✓';
193 };
194
195 issueDiv.appendChild(typeLabel);
196 issueDiv.appendChild(originalText);
197 issueDiv.appendChild(suggestionText);
198 issueDiv.appendChild(explanation);
199 issueDiv.appendChild(applyBtn);
200 container.appendChild(issueDiv);
201 });
202
203 // Add "Fix All" button if there are multiple issues
204 if (result.issues.length > 1 && result.correctedText) {
205 const fixAllBtn = document.createElement('button');
206 fixAllBtn.style.cssText = `
207 background: #2196F3;
208 color: white;
209 border: none;
210 padding: 8px 16px;
211 border-radius: 4px;
212 cursor: pointer;
213 font-size: 13px;
214 font-weight: 500;
215 width: 100%;
216 margin-top: 8px;
217 `;
218 fixAllBtn.textContent = 'Fix All Issues';
219 fixAllBtn.onclick = () => {
220 element.value = result.correctedText;
221 element.dispatchEvent(new Event('input', { bubbles: true }));
222 container.innerHTML = '<div style="color: #4CAF50; font-weight: 500; text-align: center;">✓ All issues fixed!</div>';
223 setTimeout(() => {
224 container.style.display = 'none';
225 }, 2000);
226 };
227 container.appendChild(fixAllBtn);
228 }
229
230 container.style.display = 'block';
231 positionContainer(element, container);
232 }
233
234 function getIssueColor(type) {
235 const colors = {
236 grammar: '#f44336',
237 spelling: '#ff9800',
238 style: '#2196F3',
239 punctuation: '#9c27b0'
240 };
241 return colors[type] || '#757575';
242 }
243
244 function positionContainer(element, container) {
245 const rect = element.getBoundingClientRect();
246 container.style.top = (rect.height + 5) + 'px';
247 container.style.left = '0px';
248 }
249
250 // Listen to input events
251 element.addEventListener('input', (e) => {
252 checkGrammar(e.target.value);
253 });
254
255 // Close container when clicking outside
256 document.addEventListener('click', (e) => {
257 if (!container.contains(e.target) && e.target !== element) {
258 container.style.display = 'none';
259 }
260 });
261
262 console.log('Grammar checker enabled for element:', element);
263 }
264
265 // Initialize grammar checker on text inputs and textareas
266 function init() {
267 console.log('Grammar Checker Assistant initialized');
268
269 // Find all text inputs and textareas
270 const textElements = document.querySelectorAll('textarea, input[type="text"], [contenteditable="true"]');
271
272 textElements.forEach(element => {
273 createGrammarUI(element);
274 });
275
276 // Watch for dynamically added elements
277 const observer = new MutationObserver((mutations) => {
278 mutations.forEach((mutation) => {
279 mutation.addedNodes.forEach((node) => {
280 if (node.nodeType === 1) {
281 if (node.matches('textarea, input[type="text"], [contenteditable="true"]')) {
282 createGrammarUI(node);
283 }
284 const textElements = node.querySelectorAll('textarea, input[type="text"], [contenteditable="true"]');
285 textElements.forEach(element => {
286 createGrammarUI(element);
287 });
288 }
289 });
290 });
291 });
292
293 observer.observe(document.body, {
294 childList: true,
295 subtree: true
296 });
297 }
298
299 // Start when page is ready
300 if (document.readyState === 'loading') {
301 document.addEventListener('DOMContentLoaded', init);
302 } else {
303 init();
304 }
305
306})();