-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
234 lines (200 loc) · 8.63 KB
/
background.js
File metadata and controls
234 lines (200 loc) · 8.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
// LingoContext - Background Service Worker
// Handles Gemini API requests and TTS playback
import { CONFIG, getConfig } from './config.js';
import { saveWord } from './db-hook.js';
// FORCE RESET CONFIG ON STARTUP if it's localhost (migration fix)
chrome.runtime.onInstalled.addListener(async () => {
const stored = await chrome.storage.local.get('BACKEND_URL');
if (stored.BACKEND_URL && stored.BACKEND_URL.includes('localhost')) {
console.log('Detected localhost config, clearing to force default...');
await chrome.storage.local.remove('BACKEND_URL');
}
});
// System instruction for Gemini to return JSON
const SYSTEM_INSTRUCTION = `You are a language learning assistant. Analyze text selections and return ONLY valid JSON in this exact format:
{
"meaning": "Definition or translation of the text",
"grammar": "Grammar breakdown or explanation",
"furigana": "For Japanese text, provide HTML with ruby tags like <ruby>漢字<rt>かんじ</rt></ruby>. For English, return the original text.",
"audio_text": "Clean text for TTS (no HTML tags, just the pronunciation)",
"language": "ja or en"
}
Do not include any text outside the JSON object.`;
// Handle Gemini API request (via Backend Streaming)
// Replaced classic handleGeminiRequest with streaming logic inside onConnect listener.
// Play TTS using chrome.tts API
function playFreeTTS(text, lang = 'en') {
return new Promise((resolve, reject) => {
// Stop any currently playing speech
chrome.tts.stop();
const options = {
lang: lang === 'ja' ? 'ja-JP' : 'en-US',
rate: CONFIG.TTS_RATE,
pitch: CONFIG.TTS_PITCH,
onEvent: (event) => {
if (event.type === 'end') {
resolve();
} else if (event.type === 'error') {
reject(new Error(event.errorMessage));
}
}
};
// Try to find a high-quality voice
chrome.tts.getVoices((voices) => {
const targetLang = lang === 'ja' ? 'ja' : 'en';
const preferredVoice = voices.find(v =>
v.lang?.startsWith(targetLang) &&
(v.voiceName?.includes('Google') || v.remote)
);
if (preferredVoice) {
options.voiceName = preferredVoice.voiceName;
}
chrome.tts.speak(text, options);
});
});
}
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'analyze-stream') {
port.onMessage.addListener(async (msg) => {
if (msg.type === 'START_ANALYZE_STREAM') {
try {
const backendUrl = await getConfig('BACKEND_URL');
if (!backendUrl) {
port.postMessage({ error: true, message: 'Please start the backend server or configure the BACKEND_URL in settings.' });
port.disconnect();
return;
}
const response = await fetch(`${backendUrl}/analyze/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: msg.text,
context: msg.context,
mode: msg.mode,
targetLanguage: msg.targetLanguage
}),
credentials: 'include'
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
port.postMessage({ error: true, message: error.message || `Backend Error: ${response.status}` });
port.disconnect();
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep the incomplete line for the next chunk
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.replace('data: ', '').trim();
if (!dataStr) continue;
if (dataStr === '[DONE]') {
port.postMessage({ type: 'DONE' });
} else {
try {
const parsed = JSON.parse(dataStr);
if (parsed.error) {
port.postMessage({ error: true, message: parsed.message });
} else if (parsed.text) {
port.postMessage({ type: 'CHUNK', text: parsed.text });
}
} catch (e) {
// Ignore incomplete JSON chunks from split
}
}
}
}
}
} catch (error) {
port.postMessage({ error: true, message: error.message || 'Stream connection failed' });
} finally {
port.disconnect();
}
}
});
}
});
// Message listener for content script communication
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'PLAY_TTS') {
playFreeTTS(message.text, message.lang)
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ error: true, message: error.message }));
return true;
}
if (message.type === 'STOP_TTS') {
chrome.tts.stop();
sendResponse({ success: true });
}
if (message.type === 'SAVE_WORD') {
saveWord(message.data)
.then(result => sendResponse(result))
.catch(error => sendResponse({ error: true, message: error.message }));
return true;
}
if (message.type === 'GET_CONFIG') {
getConfig(message.key)
.then(value => sendResponse({ value }))
.catch(error => sendResponse({ error: true, message: error.message }));
return true;
}
if (message.type === 'OPEN_LOGIN') {
getConfig('BACKEND_URL').then(backendUrl => {
if (backendUrl) {
const rootUrl = backendUrl.replace('/api', '');
chrome.tabs.create({ url: `${rootUrl}/auth/google` });
} else {
chrome.tabs.create({ url: 'dashboard.html' });
}
sendResponse({ success: true });
});
return true;
}
if (message.type === 'OPEN_DASHBOARD') {
chrome.tabs.create({ url: 'dashboard.html' });
sendResponse({ success: true });
return true;
}
if (message.type === 'OPEN_DASHBOARD_IN_CURRENT_TAB') {
if (sender.tab?.id) {
chrome.tabs.update(sender.tab.id, { url: chrome.runtime.getURL('dashboard.html') });
sendResponse({ success: true });
return true;
}
chrome.tabs.create({ url: 'dashboard.html' });
sendResponse({ success: true });
return true;
}
});
// Hot reload for development
if (CONFIG.DEV_MODE) {
const HOT_RELOAD_URL = 'http://localhost:35729/events';
function connectHotReload() {
try {
const es = new EventSource(HOT_RELOAD_URL);
es.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'reload') {
console.log('🔄 Hot reload triggered, reloading extension...');
chrome.runtime.reload();
}
};
es.onerror = () => {
es.close();
// Retry connection after 5 seconds
setTimeout(connectHotReload, 5000);
};
console.log('🔌 Connected to hot reload server');
} catch (e) {
console.log('Hot reload server not available');
}
}
connectHotReload();
}
console.log('LingoContext background service worker loaded');