feat(core): Update opengrammar/extension/src/background/index.ts (Commit #9)

Swadhin Biswas committed 7 months ago
164aa5c
1 parent 6b0eb30
commit 164aa5c
1 file changed
+383 -18
▼opengrammar/extension/src/background/index.ts
+383-18
1
-import type { AnalyzeRequest, AnalyzeResponse, RewriteRequest, RewriteResponse, LLMProvider } from '../types';
1
+import type {
2
+ AnalyzeRequest,
3
+ AnalyzeResponse,
4
+ AnalyticsEventType,
5
+ AnalyticsSummary,
6
+ AutocompleteRequest,
7
+ AutocompleteResponse,
8
+ AnalysisContext,
9
+ EditorContext,
10
+ LLMProvider,
11
+ RewriteContext,
12
+ RewriteRequest,
13
+ RewriteResponse,
14
+} from '../types';
2
15
3
16
// Default backend URL
4
17
const DEFAULT_BACKEND_URL = 'http://localhost:8787';
18
+const REWRITE_PAGE_PATH = 'src/rewrite/index.html';
19
+const STATS_PAGE_PATH = 'src/stats/index.html';
20
+const DEFAULT_PROVIDER = 'openai';
21
+const DEFAULT_MODEL = 'gpt-4o-mini';
22
+const DEFAULT_ANALYTICS: AnalyticsSummary = {
23
+ totals: {
24
+ analysis_runs: 0,
25
+ issues_found: 0,
26
+ suggestions_applied: 0,
27
+ suggestions_ignored: 0,
28
+ autocomplete_shown: 0,
29
+ autocomplete_accepted: 0,
30
+ rewrite_opened: 0,
31
+ rewrite_applied: 0,
32
+ },
33
+ domains: {},
34
+ providers: {},
35
+};
5
36
6
37
// Initialize context menus
7
38
chrome.runtime.onInstalled.addListener(() => {
39
+ void initializeStoredDefaults();
8
40
chrome.contextMenus.create({
9
41
id: 'opengrammar-rewrite',
10
42
title: 'Rewrite with OpenGrammar',
12
44
});
13
45
});
14
46
47
+void initializeStoredDefaults();
48
+
15
49
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
16
50
if (request.type === 'CHECK_GRAMMAR') {
17
- handleGrammarCheck(request.text, sendResponse);
51
+ handleGrammarCheck(request.text, request.context, sendResponse);
18
52
return true;
19
53
}
20
54
37
71
});
38
72
return true;
39
73
}
74
+
75
+ if (request.type === 'SYNC_ACTIVE_CONTEXT') {
76
+ const tabId = sender.tab?.id;
77
+ if (!tabId || typeof request.text !== 'string') {
78
+ sendResponse({ success: false });
79
+ return false;
80
+ }
81
+
82
+ void storeActiveContext(tabId, request.text, request.issues || []).then(() => {
83
+ sendResponse({ success: true });
84
+ });
85
+ return true;
86
+ }
87
+
88
+ if (request.type === 'GET_ACTIVE_TEXT') {
89
+ void getActiveEditorContext().then((context) => {
90
+ sendResponse(context || { text: '', issues: [] });
91
+ });
92
+ return true;
93
+ }
94
+
95
+ if (request.type === 'AUTOCOMPLETE_TEXT') {
96
+ void handleAutocomplete(request, sendResponse);
97
+ return true;
98
+ }
99
+
100
+ if (request.type === 'OPEN_REWRITE_PAGE') {
101
+ void openRewritePage().then(() => sendResponse({ success: true }));
102
+ return true;
103
+ }
104
+
105
+ if (request.type === 'GET_REWRITE_CONTEXT') {
106
+ void getRewriteContext().then((context) => {
107
+ sendResponse(context || { selectedText: '' });
108
+ });
109
+ return true;
110
+ }
111
+
112
+ if (request.type === 'APPLY_REWRITE_TO_SOURCE') {
113
+ void applyRewriteToSource(request.original, request.rewritten).then((result) => {
114
+ sendResponse(result);
115
+ });
116
+ return true;
117
+ }
118
+
119
+ if (request.type === 'OPEN_STATS_PAGE') {
120
+ void openStatsPage().then(() => sendResponse({ success: true }));
121
+ return true;
122
+ }
123
+
124
+ if (request.type === 'TRACK_ANALYTICS_EVENT') {
125
+ void trackAnalyticsEvent(request.eventType, request.payload).then(() => {
126
+ sendResponse({ success: true });
127
+ });
128
+ return true;
129
+ }
130
+
131
+ if (request.type === 'GET_ANALYTICS_SUMMARY') {
132
+ void getAnalyticsSummary().then((summary) => sendResponse(summary));
133
+ return true;
134
+ }
135
+
136
+ if (request.type === 'CLEAR_ANALYTICS') {
137
+ void chrome.storage.sync.set({ analyticsSummary: DEFAULT_ANALYTICS }).then(() => {
138
+ sendResponse({ success: true });
139
+ });
140
+ return true;
141
+ }
40
142
41
143
if (request.type === 'GET_PROVIDERS') {
42
144
getProviders().then(providers => sendResponse({ providers }));
64
166
// Handle context menu clicks
65
167
chrome.contextMenus.onClicked.addListener((info, tab) => {
66
168
if (info.menuItemId === 'opengrammar-rewrite' && info.selectionText) {
67
- // Open rewrite popup with selected text
68
- if (tab?.id) {
69
- chrome.tabs.sendMessage(tab.id, {
70
- type: 'OPEN_REWRITE',
71
- text: info.selectionText,
72
- });
73
- }
169
+ void storeRewriteContext(tab?.id, info.selectionText).then(() => {
170
+ chrome.tabs.create({ url: chrome.runtime.getURL(REWRITE_PAGE_PATH) });
171
+ });
74
172
}
75
173
});
76
174
77
175
// Handle keyboard shortcuts
78
176
chrome.commands.onCommand.addListener((command) => {
79
177
if (command === 'rewrite-text') {
80
- chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
81
- const tab = tabs[0];
82
- if (tab?.id) {
83
- chrome.tabs.sendMessage(tab.id, { type: 'OPEN_REWRITE' });
84
- }
85
- });
178
+ void openRewritePage();
179
+ return;
180
+ }
181
+
182
+ if (command === 'open-stats') {
183
+ void openStatsPage();
86
184
}
87
185
});
88
186
89
-async function handleGrammarCheck(text: string, sendResponse: (response: any) => void) {
187
+async function handleGrammarCheck(
188
+ text: string,
189
+ context: AnalysisContext | undefined,
190
+ sendResponse: (response: any) => void,
191
+) {
90
192
try {
91
193
const {
92
194
apiKey,
125
227
provider: provider as LLMProvider,
126
228
baseUrl: customBaseUrl,
127
229
ignoredIssues: ignoredIssues || [],
230
+ dictionary: dictionary || [],
231
+ context,
128
232
};
129
233
130
234
const response = await fetch(`${baseUrl}/analyze`, {
143
247
const data: AnalyzeResponse = await response.json();
144
248
145
249
// Filter out ignored issues
146
- if (data.issues && ignoredIssues) {
250
+ const normalizedIgnored = normalizeIgnoredIssues(ignoredIssues);
251
+
252
+ if (data.issues && normalizedIgnored.length > 0) {
147
253
data.issues = data.issues.filter(issue => {
148
254
const issueId = issue.id || `${issue.type}-${issue.offset}-${issue.original}`;
149
- return !ignoredIssues.includes(issueId);
255
+ return !normalizedIgnored.includes(issueId);
150
256
});
151
257
}
152
258
158
264
return !dictionary.includes(word);
159
265
});
160
266
}
267
+
268
+ await trackAnalyticsEvent('analysis_runs', {
269
+ count: 1,
270
+ domain: context?.domain,
271
+ provider: provider || 'rule-only',
272
+ });
273
+
274
+ if (data.issues.length > 0) {
275
+ await trackAnalyticsEvent('issues_found', {
276
+ count: data.issues.length,
277
+ domain: context?.domain,
278
+ provider: provider || 'rule-only',
279
+ });
280
+ }
161
281
162
282
sendResponse(data);
163
283
} catch (error) {
170
290
}
171
291
}
172
292
293
+async function handleAutocomplete(
294
+ request: AutocompleteRequest & { type: string },
295
+ sendResponse: (response: AutocompleteResponse) => void,
296
+) {
297
+ try {
298
+ const {
299
+ apiKey,
300
+ model,
301
+ backendUrl,
302
+ provider,
303
+ customBaseUrl,
304
+ autocompleteEnabled,
305
+ } = await chrome.storage.sync.get([
306
+ 'apiKey',
307
+ 'model',
308
+ 'backendUrl',
309
+ 'provider',
310
+ 'customBaseUrl',
311
+ 'autocompleteEnabled',
312
+ ]);
313
+
314
+ if (autocompleteEnabled === false) {
315
+ sendResponse({ suggestion: '', confidence: 0, replaceStart: request.cursor, replaceEnd: request.cursor, source: 'heuristic' });
316
+ return;
317
+ }
318
+
319
+ const baseUrl = backendUrl || DEFAULT_BACKEND_URL;
320
+ const requestBody: AutocompleteRequest = {
321
+ text: request.text,
322
+ cursor: request.cursor,
323
+ apiKey,
324
+ model,
325
+ provider: (provider || 'openai') as LLMProvider,
326
+ baseUrl: customBaseUrl,
327
+ context: request.context,
328
+ };
329
+
330
+ const response = await fetch(`${baseUrl}/autocomplete`, {
331
+ method: 'POST',
332
+ headers: { 'Content-Type': 'application/json' },
333
+ body: JSON.stringify(requestBody),
334
+ });
335
+
336
+ const data: AutocompleteResponse = await response.json();
337
+
338
+ if (data.suggestion) {
339
+ await trackAnalyticsEvent('autocomplete_shown', {
340
+ count: 1,
341
+ domain: request.context?.domain,
342
+ provider: provider || 'heuristic',
343
+ });
344
+ }
345
+
346
+ sendResponse(data);
347
+ } catch (error) {
348
+ sendResponse({
349
+ suggestion: '',
350
+ confidence: 0,
351
+ replaceStart: request.cursor,
352
+ replaceEnd: request.cursor,
353
+ source: 'heuristic',
354
+ error: error instanceof Error ? error.message : 'Autocomplete failed',
355
+ });
356
+ }
357
+}
358
+
173
359
async function handleRewrite(
174
360
text: string,
175
361
tone: string,
260
446
return result.backendUrl || DEFAULT_BACKEND_URL;
261
447
}
262
448
449
+async function initializeStoredDefaults(): Promise<void> {
450
+ const existing = await chrome.storage.sync.get([
451
+ 'enabled',
452
+ 'provider',
453
+ 'model',
454
+ 'backendUrl',
455
+ 'backendAutoDetected',
456
+ ]);
457
+
458
+ await chrome.storage.sync.set({
459
+ enabled: existing.enabled !== false,
460
+ provider: existing.provider || DEFAULT_PROVIDER,
461
+ model: existing.model || DEFAULT_MODEL,
462
+ backendUrl: existing.backendUrl || DEFAULT_BACKEND_URL,
463
+ backendAutoDetected: existing.backendAutoDetected !== false,
464
+ });
465
+}
466
+
467
+function normalizeIgnoredIssues(value: unknown): string[] {
468
+ if (!Array.isArray(value)) {
469
+ return [];
470
+ }
471
+
472
+ return value
473
+ .map((entry) => {
474
+ if (typeof entry === 'string') {
475
+ return entry;
476
+ }
477
+
478
+ if (entry && typeof entry === 'object' && 'id' in entry && typeof entry.id === 'string') {
479
+ return entry.id;
480
+ }
481
+
482
+ return null;
483
+ })
484
+ .filter((entry): entry is string => Boolean(entry));
485
+}
486
+
487
+async function getActiveTab(): Promise<chrome.tabs.Tab | undefined> {
488
+ const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
489
+ return tabs[0];
490
+}
491
+
492
+async function requestSelectedText(tabId: number): Promise<string> {
493
+ try {
494
+ const response = await chrome.tabs.sendMessage(tabId, { type: 'GET_SELECTED_TEXT' });
495
+ return response?.text || '';
496
+ } catch {
497
+ return '';
498
+ }
499
+}
500
+
501
+async function storeRewriteContext(sourceTabId?: number, selectedText: string = ''): Promise<void> {
502
+ const payload: RewriteContext = {
503
+ selectedText,
504
+ sourceTabId,
505
+ capturedAt: Date.now(),
506
+ };
507
+
508
+ await chrome.storage.local.set({ rewriteContext: payload });
509
+}
510
+
511
+async function getRewriteContext(): Promise<RewriteContext | null> {
512
+ const result = await chrome.storage.local.get('rewriteContext');
513
+ return (result.rewriteContext as RewriteContext | undefined) || null;
514
+}
515
+
516
+async function openRewritePage(): Promise<void> {
517
+ const activeTab = await getActiveTab();
518
+ const selectedText = activeTab?.id ? await requestSelectedText(activeTab.id) : '';
519
+ await storeRewriteContext(activeTab?.id, selectedText);
520
+ await trackAnalyticsEvent('rewrite_opened', {
521
+ count: 1,
522
+ domain: getDomainFromUrl(activeTab?.url),
523
+ });
524
+ await chrome.tabs.create({ url: chrome.runtime.getURL(REWRITE_PAGE_PATH) });
525
+}
526
+
527
+async function openStatsPage(): Promise<void> {
528
+ await chrome.tabs.create({ url: chrome.runtime.getURL(STATS_PAGE_PATH) });
529
+}
530
+
531
+async function applyRewriteToSource(original: string, rewritten: string): Promise<{ success: boolean; error?: string }> {
532
+ const context = await getRewriteContext();
533
+ if (!context?.sourceTabId) {
534
+ return { success: false, error: 'No source tab available for rewrite.' };
535
+ }
536
+
537
+ try {
538
+ const response = await chrome.tabs.sendMessage(context.sourceTabId, {
539
+ type: 'APPLY_REWRITE',
540
+ original,
541
+ rewritten,
542
+ });
543
+
544
+ return response?.success
545
+ ? await trackRewriteApplyAndReturn(context.sourceTabId)
546
+ : { success: false, error: response?.error || 'Failed to apply rewrite.' };
547
+ } catch (error) {
548
+ return {
549
+ success: false,
550
+ error: error instanceof Error ? error.message : 'Failed to contact source tab.',
551
+ };
552
+ }
553
+}
554
+
555
+async function trackRewriteApplyAndReturn(sourceTabId?: number) {
556
+ let domain: string | undefined;
557
+ if (sourceTabId) {
558
+ const tab = await chrome.tabs.get(sourceTabId).catch(() => undefined);
559
+ domain = getDomainFromUrl(tab?.url);
560
+ }
561
+ await trackAnalyticsEvent('rewrite_applied', { count: 1, domain });
562
+ return { success: true };
563
+}
564
+
565
+async function storeActiveContext(sourceTabId: number, text: string, issues: AnalyzeResponse['issues'] = []): Promise<void> {
566
+ const payload: EditorContext = {
567
+ text,
568
+ issues,
569
+ sourceTabId,
570
+ capturedAt: Date.now(),
571
+ };
572
+
573
+ await chrome.storage.local.set({ lastEditorContext: payload });
574
+}
575
+
576
+async function getActiveEditorContext(): Promise<EditorContext | null> {
577
+ const result = await chrome.storage.local.get('lastEditorContext');
578
+ return (result.lastEditorContext as EditorContext | undefined) || null;
579
+}
580
+
581
+async function getAnalyticsSummary(): Promise<AnalyticsSummary> {
582
+ const result = await chrome.storage.sync.get('analyticsSummary');
583
+ return {
584
+ ...DEFAULT_ANALYTICS,
585
+ ...(result.analyticsSummary as AnalyticsSummary | undefined),
586
+ totals: {
587
+ ...DEFAULT_ANALYTICS.totals,
588
+ ...((result.analyticsSummary as AnalyticsSummary | undefined)?.totals || {}),
589
+ },
590
+ domains: {
591
+ ...((result.analyticsSummary as AnalyticsSummary | undefined)?.domains || {}),
592
+ },
593
+ providers: {
594
+ ...((result.analyticsSummary as AnalyticsSummary | undefined)?.providers || {}),
595
+ },
596
+ };
597
+}
598
+
599
+async function trackAnalyticsEvent(
600
+ eventType: AnalyticsEventType,
601
+ payload?: { count?: number; domain?: string; provider?: string },
602
+): Promise<void> {
603
+ const summary = await getAnalyticsSummary();
604
+ const count = payload?.count || 1;
605
+ summary.totals[eventType] = (summary.totals[eventType] || 0) + count;
606
+
607
+ if (payload?.domain) {
608
+ summary.domains[payload.domain] = (summary.domains[payload.domain] || 0) + count;
609
+ }
610
+
611
+ if (payload?.provider) {
612
+ summary.providers[payload.provider] = (summary.providers[payload.provider] || 0) + count;
613
+ }
614
+
615
+ summary.lastUpdatedAt = Date.now();
616
+ await chrome.storage.sync.set({ analyticsSummary: summary });
617
+}
618
+
619
+function getDomainFromUrl(url?: string): string | undefined {
620
+ if (!url) return undefined;
621
+ try {
622
+ return new URL(url).hostname;
623
+ } catch {
624
+ return undefined;
625
+ }
626
+}
627
+
263
628
// Health check on extension load
264
629
async function checkBackendHealth() {
265
630
try {
266
631