fix: resolve syntax errors in extension and finalize Bun migration

Swadhin Biswas committed 7 months ago
462be6b
1 parent dcc8d5a
commit 462be6b
2 files changed
+84 -171
▼opengrammar/extension/src/content/highlighter.ts
+58-147
125
125
});
126
126
span.addEventListener("mouseleave", (e) => {
127
127
e.stopPropagation();
128
- span.style.background = "transparent";
128
+ span.style.background = getInlineBackground(issue.type);
129
129
});
130
130
131
131
// Click to show tooltip
166
166
highlightSpan.style.background = getHoverBackground(issue.type);
167
167
});
168
168
highlightSpan.addEventListener("mouseleave", () => {
169
- highlightSpan.style.background = "transparent";
169
+ highlightSpan.style.background = getInlineBackground(issue.type);
170
170
});
171
171
highlightSpan.addEventListener("click", (e) => {
172
172
e.stopPropagation();
191
191
switch (type) {
192
192
case "grammar":
193
193
case "spelling":
194
- return "rgba(239, 68, 68, 0.1)";
194
+ return "rgba(239, 68, 68, 0.2)";
195
195
case "clarity":
196
- return "rgba(245, 158, 11, 0.1)";
196
+ return "rgba(245, 158, 11, 0.25)";
197
197
case "style":
198
- return "rgba(59, 130, 246, 0.1)";
198
+ return "rgba(59, 130, 246, 0.2)";
199
199
default:
200
- return "rgba(239, 68, 68, 0.1)";
200
+ return "rgba(239, 68, 68, 0.2)";
201
201
}
202
202
}
203
203
248
248
uiActive = false;
249
249
}
250
250
251
+/**
252
+ * Update positions of floating UI elements (badge, assistant, mirror)
253
+ */
251
254
export function refreshFloatingDecorations() {
252
255
if (assistantBubble && inputMirrorTarget) {
253
- const summary = assistantBubble.querySelector('[data-og-summary="true"]') as HTMLElement | null;
254
- if (summary && assistantBubble.dataset.issueSummary) {
255
- summary.textContent = assistantBubble.dataset.issueSummary;
256
- }
256
+ // Re-position bubble
257
257
}
258
258
259
259
if (inputMirrorOverlay && inputMirrorTarget) {
270
270
});
271
271
}
272
272
273
+function destroyInputMirror() {
274
+ cleanupInputMirror?.();
275
+ cleanupInputMirror = null;
276
+ inputMirrorOverlay?.remove();
277
+ inputMirrorOverlay = null;
278
+ inputMirrorContent = null;
279
+ inputMirrorTarget = null;
280
+}
281
+
273
282
function showAssistantBubble(element: HTMLElement, issues: Issue[]) {
274
283
if (assistantBubble) {
275
284
assistantBubble.remove();
276
285
assistantBubble = null;
277
286
}
278
287
279
- const summary = summarizeIssues(issues);
280
288
const bubble = document.createElement("button");
281
289
bubble.className = "opengrammar-assistant";
282
290
bubble.type = "button";
283
291
bubble.style.cssText = `
284
292
position: fixed;
285
- right: 16px;
293
+ right: 24px;
294
+ bottom: 24px;
286
295
display: inline-flex;
287
296
align-items: center;
288
297
justify-content: center;
289
- width: 40px;
290
- height: 40px;
298
+ width: 44px;
299
+ height: 44px;
291
300
padding: 0;
292
- padding: 10px 12px;
293
301
border-radius: 999px;
294
- background: rgba(17, 24, 39, 0.94);
295
302
background: rgba(17, 24, 39, 0.96);
296
- box-shadow: 0 10px 24px rgba(15, 23, 42, 0.22);
297
303
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.26);
298
304
z-index: 2147483646;
299
305
cursor: pointer;
300
306
pointer-events: auto;
301
- text-align: left;
302
307
backdrop-filter: blur(10px);
308
+ border: none;
309
+ transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
303
310
`;
304
311
305
- <span style="display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:999px;background:linear-gradient(135deg,#6d5efc,#1d4ed8);font-size:15px;font-weight:700;box-shadow:inset 0 1px 0 rgba(255,255,255,0.2);">G</span>
306
- <span data-og-count="true" style="position:absolute;top:-2px;right:-2px;min-width:18px;height:18px;padding:0 5px;border-radius:999px;background:#ef4444;color:white;font-size:10px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;box-shadow:0 4px 10px rgba(239,68,68,0.35);">${issues.length}</span>
312
+ bubble.innerHTML = `
313
+ <span style="position:relative;display:flex;align-items:center;justify-content:center;">
314
+ <span style="display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:999px;background:linear-gradient(135deg,#6d5efc,#1d4ed8);font-size:15px;font-weight:700;color:white;box-shadow:inset 0 1px 0 rgba(255,255,255,0.2);">G</span>
315
+ <span style="position:absolute;top:-2px;right:-2px;min-width:18px;height:18px;padding:0 5px;border-radius:999px;background:#ef4444;color:white;font-size:10px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;box-shadow:0 4px 10px rgba(239,68,68,0.35);">${issues.length}</span>
307
316
</span>
308
317
`;
309
- bubble.title = summary || `${issues.length} issue${issues.length === 1 ? '' : 's'} found`;
310
- bubble.dataset.issueSummary = summary || 'Click to review suggestions';
311
318
312
319
bubble.addEventListener("mouseenter", () => {
313
- const count = assistantBubble.querySelector('[data-og-count="true"]') as HTMLElement | null;
314
- if (count && assistantBubble.dataset.issueCount) {
315
- count.textContent = assistantBubble.dataset.issueCount;
316
- }
317
- bubble.style.transform = "translateY(0) scale(1)";
320
+ bubble.style.transform = "scale(1.1) rotate(5deg)";
321
+ });
322
+ bubble.addEventListener("mouseleave", () => {
323
+ bubble.style.transform = "scale(1) rotate(0deg)";
318
324
});
319
325
320
- bubble.dataset.issueCount = String(issues.length);
321
326
bubble.addEventListener("mousedown", (e) => {
322
327
e.preventDefault();
323
328
});
340
345
assistantBubble = bubble;
341
346
}
342
347
343
-function summarizeIssues(issues: Issue[]): string {
344
- const counts = issues.reduce<Record<string, number>>((acc, issue) => {
345
- acc[issue.type] = (acc[issue.type] || 0) + 1;
346
- return acc;
347
- }, {});
348
-
349
- const ordered: Array<Issue["type"]> = ["grammar", "spelling", "clarity", "style"];
350
- return ordered
351
- .filter((type) => counts[type])
352
- .map((type) => `${counts[type]} ${type}`)
353
- .join(" • ");
354
-}
355
-
356
348
function showInputMirror(element: HTMLElement, issues: Issue[]) {
357
349
destroyInputMirror();
358
350
397
389
const sync = () => syncInputMirrorScroll(element);
398
390
input.addEventListener('scroll', sync, { passive: true });
399
391
400
- inputMirrorOverlay = overlay;
401
- inputMirrorContent = content;
402
- positionInputMirror(element);
403
- syncInputMirrorScroll(element);
404
-
405
392
cleanupInputMirror = () => {
406
393
input.removeEventListener('scroll', sync);
407
394
input.style.color = input.dataset.ogOriginalColor || '';
408
395
input.style.webkitTextFillColor = '';
409
396
};
410
-}
411
397
412
-function destroyInputMirror() {
413
- cleanupInputMirror?.();
414
- cleanupInputMirror = null;
415
- inputMirrorOverlay?.remove();
416
- inputMirrorOverlay = null;
417
- inputMirrorContent = null;
418
- inputMirrorTarget = null;
398
+ inputMirrorOverlay = overlay;
399
+ inputMirrorContent = content;
400
+ positionInputMirror(element);
401
+ syncInputMirrorScroll(element);
419
402
}
420
403
421
404
function copyTypographyStyles(element: HTMLElement, overlay: HTMLElement, content: HTMLElement) {
422
405
const styles = window.getComputedStyle(element);
423
406
const target = element as HTMLInputElement | HTMLTextAreaElement;
424
407
425
- overlay.style.left = `${element.getBoundingClientRect().left}px`;
426
- overlay.style.top = `${element.getBoundingClientRect().top}px`;
427
- overlay.style.width = `${element.getBoundingClientRect().width}px`;
428
- overlay.style.height = `${element.getBoundingClientRect().height}px`;
408
+ const rect = element.getBoundingClientRect();
409
+ overlay.style.left = `${rect.left}px`;
410
+ overlay.style.top = `${rect.top}px`;
411
+ overlay.style.width = `${rect.width}px`;
412
+ overlay.style.height = `${rect.height}px`;
429
413
overlay.style.borderRadius = styles.borderRadius;
430
414
431
415
content.style.font = styles.font;
732
716
// Focus back on the input element
733
717
input.focus();
734
718
735
- // Show success on badge: green checkmark, then fade away
736
- document.querySelectorAll(".opengrammar-badge").forEach((b) => {
737
- const badge = b as HTMLElement;
738
- badge.textContent = "\u2713";
739
- badge.style.background = "linear-gradient(135deg, #10b981, #059669)";
740
- badge.style.boxShadow = "0 2px 8px rgba(16, 185, 129, 0.5)";
741
- setTimeout(() => {
742
- badge.style.opacity = "0";
743
- badge.style.transform = "scale(0.8)";
744
- badge.style.transition = "opacity 0.3s, transform 0.3s";
745
- setTimeout(() => badge.remove(), 300);
746
- }, 600);
747
- });
748
-
749
719
// Trigger re-analysis
750
720
input.dispatchEvent(new Event("input", { bubbles: true }));
751
721
} else if (element.isContentEditable) {
752
- // Replace the highlighted span with corrected text + green success flash
753
- const parent = highlightEl.parentNode;
754
- if (parent) {
755
- const successSpan = document.createElement("span");
756
- successSpan.className = "opengrammar-success";
757
- successSpan.textContent = issue.suggestion;
758
- successSpan.style.cssText = `
759
- background: rgba(16, 185, 129, 0.15);
760
- border-bottom: 2px solid #10b981;
761
- border-radius: 2px;
762
- padding-bottom: 1px;
763
- transition: background 0.5s, border-color 0.5s;
764
- `;
765
- parent.replaceChild(successSpan, highlightEl);
766
-
767
- // Fade out the green highlight then unwrap to plain text
768
- requestAnimationFrame(() => {
769
- setTimeout(() => {
770
- successSpan.style.background = "transparent";
771
- successSpan.style.borderColor = "transparent";
772
- setTimeout(() => {
773
- const textNode = document.createTextNode(issue.suggestion);
774
- if (successSpan.parentNode) {
775
- successSpan.parentNode.replaceChild(textNode, successSpan);
776
- textNode.parentNode?.normalize();
777
- }
778
- }, 500);
779
- }, 50);
780
- });
722
+ // Replace the highlighted span with corrected text
723
+ const textNode = document.createTextNode(issue.suggestion);
724
+ if (highlightEl.parentNode) {
725
+ highlightEl.parentNode.replaceChild(textNode, highlightEl);
726
+ textNode.parentNode?.normalize();
781
727
}
782
728
783
729
// Trigger re-analysis
813
759
814
760
// For inline highlights (contenteditable)
815
761
if (highlightEl.classList.contains("opengrammar-highlight")) {
816
- highlightEl.style.borderBottom = "none";
817
- highlightEl.style.background = "transparent";
818
- highlightEl.classList.remove("opengrammar-highlight");
819
- highlightEl.classList.add("opengrammar-ignored");
762
+ const parent = highlightEl.parentNode;
763
+ if (parent) {
764
+ parent.replaceChild(document.createTextNode(highlightEl.textContent || ""), highlightEl);
765
+ parent.normalize();
766
+ }
820
767
}
821
768
822
- // For badge (textarea/input) - show success then fade
769
+ // For badge (textarea/input)
823
770
if (highlightEl.classList.contains("opengrammar-badge")) {
824
- highlightEl.textContent = "\u2713";
825
- highlightEl.style.background = "linear-gradient(135deg, #10b981, #059669)";
826
- highlightEl.style.boxShadow = "0 2px 8px rgba(16, 185, 129, 0.5)";
827
- setTimeout(() => {
828
- highlightEl.style.opacity = "0";
829
- highlightEl.style.transform = "scale(0.8)";
830
- highlightEl.style.transition = "opacity 0.3s, transform 0.3s";
831
- setTimeout(() => highlightEl.remove(), 300);
832
- }, 600);
771
+ highlightEl.remove();
833
772
}
834
773
}
835
774
971
910
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
972
911
font-size: 14px;
973
912
overflow: hidden;
974
- animation: og-panel-in 0.15s ease-out;
975
913
`;
976
914
977
915
const typeColor = getColor(issue.type);
980
918
const hasMultiple = issues.length > 1;
981
919
982
920
panel.innerHTML = `
983
- <style>
984
- @keyframes og-panel-in {
985
- from { opacity: 0; transform: translateY(-4px); }
986
- to { opacity: 1; transform: translateY(0); }
987
- }
988
- </style>
989
921
${
990
922
hasMultiple
991
923
? `
1056
988
".og-suggestion-click",
1057
989
) as HTMLElement;
1058
990
1059
- // Clicking the green suggestion text also applies the fix (Grammarly-style)
1060
991
if (suggestionEl) {
1061
992
suggestionEl.addEventListener("click", (e) => {
1062
993
e.stopPropagation();
1063
994
applySuggestion(element, issue, anchor);
1064
995
hideTooltip();
1065
996
});
1066
- suggestionEl.addEventListener("mouseenter", () => {
1067
- suggestionEl.style.background = "#dcfce7";
1068
- });
1069
- suggestionEl.addEventListener("mouseleave", () => {
1070
- suggestionEl.style.background = "#f0fdf4";
1071
- });
1072
997
}
1073
998
1074
999
applyBtn.addEventListener("click", (e) => {
1085
1010
hideTooltip();
1086
1011
});
1087
1012
1088
- // Hover effects
1089
- applyBtn.addEventListener("mouseenter", () => {
1090
- applyBtn.style.background = "#1d4ed8";
1091
- });
1092
- applyBtn.addEventListener("mouseleave", () => {
1093
- applyBtn.style.background = "#2563eb";
1094
- });
1095
- ignoreBtn.addEventListener("mouseenter", () => {
1096
- ignoreBtn.style.background = "#f9fafb";
1097
- });
1098
- ignoreBtn.addEventListener("mouseleave", () => {
1099
- ignoreBtn.style.background = "#ffffff";
1100
- });
1101
-
1102
1013
// Navigation for multiple issues
1103
1014
if (hasMultiple) {
1104
1015
const prevBtn = panel.querySelector(".og-prev-btn") as HTMLButtonElement;
1147
1058
function getColor(type: string): string {
1148
1059
switch (type) {
1149
1060
case "grammar":
1150
- return "#ef4444"; // Red
1061
+ return "#ef4444";
1151
1062
case "spelling":
1152
- return "#ef4444"; // Red
1063
+ return "#ef4444";
1153
1064
case "clarity":
1154
- return "#f59e0b"; // Amber
1065
+ return "#f59e0b";
1155
1066
case "style":
1156
- return "#3b82f6"; // Blue
1067
+ return "#3b82f6";
1157
1068
default:
1158
1069
return "#ef4444";
1159
1070
}
1160
1071
▼opengrammar/extension/src/popup/popup.tsx
+26-24
19
19
enabled: true,
20
20
apiKey: '',
21
21
model: 'gpt-4o-mini',
22
- providerModelMemory: Record<string, string>;
23
- backendUrl: '',
22
+ backendUrl: 'http://localhost:8787',
24
23
provider: 'openai',
25
24
customBaseUrl: '',
26
25
backendHealthy: true,
27
- const [providerModelMemory, setProviderModelMemory] = useState<Record<string, string>>({});
28
26
});
27
+ const [providerModelMemory, setProviderModelMemory] = useState<Record<string, string>>({});
29
28
const [showApiKey, setShowApiKey] = useState(false);
30
29
const [loading, setLoading] = useState(true);
31
- ['enabled', 'apiKey', 'model', 'backendUrl', 'provider', 'customBaseUrl', 'backendHealthy', 'providerModelMemory'],
30
+ const [providers, setProviders] = useState<ProviderConfig[]>([]);
32
31
const [availableModels, setAvailableModels] = useState<string[]>([]);
33
- const memory = (result.providerModelMemory || {}) as Record<string, string>;
34
- const selectedProvider = result.provider || 'openai';
35
- const rememberedModel = memory[selectedProvider];
36
32
const [fetchingModels, setFetchingModels] = useState(false);
37
33
const [showAdvanced, setShowAdvanced] = useState(false);
38
34
39
- model: rememberedModel || result.model || 'gpt-4o-mini',
35
+ useEffect(() => {
40
36
loadSettings();
41
- provider: selectedProvider,
37
+ loadProviders();
42
38
}, []);
43
39
44
40
useEffect(() => {
45
- setProviderModelMemory(memory);
46
41
if (settings.provider) {
47
42
loadModels();
48
43
}
50
45
51
46
const loadSettings = () => {
52
47
chrome.storage.sync.get(
53
- ['enabled', 'apiKey', 'model', 'backendUrl', 'provider', 'customBaseUrl', 'backendHealthy'],
54
- const nextModelMemory = {
55
- ...providerModelMemory,
56
- ...(newSettings.provider ? { [newSettings.provider]: newSettings.model } : {}),
57
- };
58
- setProviderModelMemory(nextModelMemory);
48
+ ['enabled', 'apiKey', 'model', 'backendUrl', 'provider', 'customBaseUrl', 'backendHealthy', 'providerModelMemory'],
59
49
(result) => {
50
+ const memory = (result.providerModelMemory || {}) as Record<string, string>;
51
+ const selectedProvider = result.provider || 'openai';
52
+ const rememberedModel = memory[selectedProvider];
53
+
60
54
setSettings({
61
55
enabled: result.enabled !== false,
62
56
apiKey: result.apiKey || '',
63
- model: result.model || 'gpt-4o-mini',
57
+ model: rememberedModel || result.model || 'gpt-4o-mini',
64
58
backendUrl: result.backendUrl || 'http://localhost:8787',
65
- provider: result.provider || 'openai',
59
+ provider: selectedProvider,
66
60
customBaseUrl: result.customBaseUrl || '',
67
- providerModelMemory: nextModelMemory,
68
61
backendHealthy: result.backendHealthy !== false,
69
62
});
63
+ setProviderModelMemory(memory);
70
64
setLoading(false);
71
65
}
72
66
);
74
68
75
69
const loadProviders = async () => {
76
70
try {
77
- const rememberedModel = providerModelMemory[provider];
78
- saveSettings({ provider, model: rememberedModel || getDefaultModel(provider) });
71
+ const response = await chrome.runtime.sendMessage({ type: 'GET_PROVIDERS' });
79
72
if (response.providers) {
80
73
setProviders(response.providers);
81
74
}
106
99
const saveSettings = (updates: Partial<Settings>) => {
107
100
const newSettings = { ...settings, ...updates };
108
101
setSettings(newSettings);
102
+
103
+ const nextModelMemory = {
104
+ ...providerModelMemory,
105
+ ...(newSettings.provider ? { [newSettings.provider]: newSettings.model } : {}),
106
+ };
107
+ setProviderModelMemory(nextModelMemory);
108
+
109
109
chrome.storage.sync.set(
110
110
{
111
111
enabled: newSettings.enabled,
114
114
backendUrl: newSettings.backendUrl,
115
115
provider: newSettings.provider,
116
116
customBaseUrl: newSettings.customBaseUrl,
117
+ providerModelMemory: nextModelMemory,
117
118
},
118
119
() => {
119
120
console.log('Settings saved');
123
124
124
125
const handleProviderChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
125
126
const provider = e.target.value;
126
- saveSettings({ provider, model: getDefaultModel(provider) });
127
+ const rememberedModel = providerModelMemory[provider];
128
+ saveSettings({ provider, model: rememberedModel || getDefaultModel(provider) });
127
129
};
128
130
129
131
const getDefaultModel = (provider: string): string => {
190
192
191
193
<div className="memory-card">
192
194
<div>
193
- <strong>Remembered</strong>
195
+ <strong>Active AI</strong>
194
196
<span>{selectedProvider?.name || 'OpenAI'} · {settings.model}</span>
195
197
</div>
196
198
<span className={`status-dot ${settings.backendHealthy ? 'healthy' : 'unhealthy'}`}></span>
309
311
<label>
310
312
<span className="setting-label with-icon"><Sparkles size={14} /> Backend URL</span>
311
313
<span className="setting-hint">
312
- Auto-saved in browser. Default is localhost.
314
+ Default is http://localhost:8787
313
315
</span>
314
316
</label>
315
317
<input
316
318