-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpopup.js
More file actions
288 lines (257 loc) · 11.2 KB
/
popup.js
File metadata and controls
288 lines (257 loc) · 11.2 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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
/* Lens v4 — popup.js */
const PROVIDER_INFO = {
groq: { name:'Groq', badge:'Free', hint:'gsk_…' },
openai: { name:'OpenAI', badge:'Paid', hint:'sk-…' },
anthropic: { name:'Anthropic', badge:'Paid', hint:'sk-ant-…' },
gemini: { name:'Gemini', badge:'Free', hint:'AIza…' },
mistral: { name:'Mistral', badge:'Paid', hint:'32-char key' },
together: { name:'Together AI', badge:'Paid', hint:'64-char hex' },
};
function detectLocal(key) {
if (!key) return null;
const k = key.trim();
if (k.startsWith('gsk_')) return 'groq';
if (k.startsWith('sk-ant-')) return 'anthropic';
if (k.startsWith('AIza')) return 'gemini';
if (k.startsWith('sk-')) return 'openai';
if (/^[a-f0-9]{64}$/i.test(k)) return 'together';
if (/^[A-Za-z0-9]{32}$/.test(k)) return 'mistral';
return null;
}
/* ── DOM refs ── */
const keyInput = document.getElementById('api-key');
const verifyBtn = document.getElementById('verify-btn');
const verifyLbl = document.getElementById('verify-label');
const disableBtn = document.getElementById('disable-btn');
const statusCard = document.getElementById('status-card');
const statusTitle = document.getElementById('status-title');
const statusSub = document.getElementById('status-sub');
const providerPill = document.getElementById('provider-pill');
const providerName = document.getElementById('provider-name');
const providerBadge= document.getElementById('provider-badge');
const openBtn = document.getElementById('open-btn');
/* ── State ── */
let savedKey = '';
let isVerified = false;
let isEnabled = false;
function setStatus(state, title, sub) {
statusCard.className = 'status-card ' + (state || '');
statusTitle.textContent = title;
statusSub.textContent = sub;
}
function updateProviderPill(key) {
const p = detectLocal(key);
if (p && PROVIDER_INFO[p]) {
const info = PROVIDER_INFO[p];
providerPill.style.display = 'flex';
providerName.textContent = info.name;
providerBadge.textContent = info.badge;
providerBadge.style.background = info.badge === 'Free' ? '#dcfce7' : '#fee2e2';
providerBadge.style.color = info.badge === 'Free' ? '#15803d' : '#be123c';
} else if (key && key.length > 5) {
providerPill.style.display = 'flex';
providerName.textContent = 'Unknown provider';
providerBadge.textContent = '?';
providerBadge.style.background = '#f4f4f8';
providerBadge.style.color = '#9999b0';
} else {
providerPill.style.display = 'none';
}
}
/* ── Render button states based on current state ── */
function renderButtons() {
const currentVal = keyInput.value.trim();
const keyChanged = currentVal !== savedKey;
if (!savedKey) {
// No key saved — show only Verify & Save
verifyBtn.style.display = 'flex';
verifyBtn.disabled = false;
verifyLbl.textContent = 'Verify & Save Key';
disableBtn.style.display = 'none';
} else if (keyChanged) {
// Key changed — show Verify & Save (enabled), hide disable
verifyBtn.style.display = 'flex';
verifyBtn.disabled = false;
verifyLbl.textContent = 'Verify & Save Key';
disableBtn.style.display = 'none';
keyInput.classList.remove('valid');
} else if (isEnabled && isVerified) {
// Key saved + enabled — show Disable AI button
verifyBtn.style.display = 'none';
disableBtn.style.display = 'flex';
disableBtn.textContent = 'Disable AI';
disableBtn.className = 'disable-btn';
} else if (!isEnabled && savedKey) {
// Key saved but disabled — show Enable AI button
verifyBtn.style.display = 'none';
disableBtn.style.display = 'flex';
disableBtn.textContent = 'Enable AI';
disableBtn.className = 'disable-btn enable';
}
}
/* ── Load saved state on open ── */
chrome.storage.local.get(['pn_api_key','pn_api_valid','pn_ai_enabled','pn_provider'], d => {
savedKey = d.pn_api_key || '';
isVerified = !!d.pn_api_valid;
isEnabled = d.pn_ai_enabled !== false && !!d.pn_api_valid; // default enabled if verified
if (savedKey) {
keyInput.value = savedKey;
updateProviderPill(savedKey);
if (isVerified && isEnabled) {
keyInput.classList.add('valid');
const pname = d.pn_provider ? (PROVIDER_INFO[d.pn_provider]?.name || d.pn_provider) : 'AI';
setStatus('connected', '✓ AI Assist is enabled', pname + ' · Ready to analyze');
} else if (isVerified && !isEnabled) {
keyInput.classList.add('valid');
setStatus('disabled', '⏸ AI Assist is disabled', 'Click "Enable AI" to turn it back on');
} else {
setStatus('error', 'Key saved but not verified', 'Click "Verify & Save Key" to test it');
}
} else {
setStatus('', 'No API key saved', 'AI Assist is disabled');
}
renderButtons();
});
/* ── Live provider detection as user types ── */
keyInput.addEventListener('input', e => {
updateProviderPill(e.target.value);
renderButtons();
});
/* ── Eye toggle ── */
document.getElementById('eye-btn').addEventListener('click', () => {
keyInput.type = keyInput.type === 'password' ? 'text' : 'password';
});
/* ── Verify & Save ── */
function setLoading(on) {
verifyBtn.disabled = on;
const existing = verifyBtn.querySelector('.mini-spinner');
if (on && !existing) {
const s = document.createElement('span');
s.className = 'mini-spinner';
verifyBtn.insertBefore(s, verifyLbl);
verifyLbl.textContent = 'Verifying…';
} else if (!on) {
if (existing) existing.remove();
verifyLbl.textContent = 'Verify & Save Key';
}
}
verifyBtn.addEventListener('click', async () => {
const key = keyInput.value.trim();
if (!key) {
keyInput.classList.add('bad');
setStatus('error', 'Enter your API key first', 'Paste the key in the field above');
setTimeout(() => keyInput.classList.remove('bad'), 600);
return;
}
const providerKey = detectLocal(key);
if (!providerKey) {
keyInput.classList.add('bad');
setStatus('error', 'Provider not recognised', 'Supported: Groq, OpenAI, Anthropic, Gemini, Mistral, Together AI');
setTimeout(() => keyInput.classList.remove('bad'), 600);
return;
}
const info = PROVIDER_INFO[providerKey];
setLoading(true);
setStatus('checking', 'Verifying ' + info.name + ' key…', 'Making a test call');
keyInput.classList.remove('valid', 'bad');
try {
let res;
if (providerKey === 'gemini') {
res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${key}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contents: [{ parts: [{ text: 'Hi' }] }], generationConfig: { maxOutputTokens: 5 } })
});
if (res.status === 400) throw new Error('INVALID');
} else if (providerKey === 'anthropic') {
res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true' },
body: JSON.stringify({ model: 'claude-haiku-4-5-20251001', max_tokens: 5, messages: [{ role: 'user', content: 'Hi' }] })
});
if (res.status === 401) throw new Error('INVALID');
} else {
const endpoints = { groq: 'https://api.groq.com/openai/v1/chat/completions', openai: 'https://api.openai.com/v1/chat/completions', mistral: 'https://api.mistral.ai/v1/chat/completions', together: 'https://api.together.xyz/v1/chat/completions' };
const models = { groq: 'llama-3.3-70b-versatile', openai: 'gpt-4o-mini', mistral: 'mistral-small-latest', together: 'meta-llama/Llama-3-70b-chat-hf' };
res = await fetch(endpoints[providerKey], {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` },
body: JSON.stringify({ model: models[providerKey], max_tokens: 5, messages: [{ role: 'user', content: 'Hi' }] })
});
if (res.status === 401) throw new Error('INVALID');
}
if (res.status === 429) {
// Rate limited = key is valid
await chrome.storage.local.set({ pn_api_key: key, pn_api_valid: true, pn_ai_enabled: true, pn_provider: providerKey });
savedKey = key; isVerified = true; isEnabled = true;
keyInput.classList.add('valid');
setStatus('connected', '✓ Key saved — ' + info.name + ' busy', 'Rate limited but key is valid · AI enabled');
notifyTab(key);
renderButtons();
return;
}
if (!res.ok) throw new Error('API_' + res.status);
await chrome.storage.local.set({ pn_api_key: key, pn_api_valid: true, pn_ai_enabled: true, pn_provider: providerKey });
savedKey = key; isVerified = true; isEnabled = true;
keyInput.classList.add('valid');
keyInput.classList.remove('bad');
setStatus('connected', '✓ AI Assist enabled — ' + info.name + '!', info.name + ' · Ready to analyze');
notifyTab(key);
renderButtons();
} catch (err) {
keyInput.classList.add('bad');
keyInput.classList.remove('valid');
await chrome.storage.local.set({ pn_api_valid: false, pn_ai_enabled: false });
isVerified = false; isEnabled = false;
setTimeout(() => keyInput.classList.remove('bad'), 600);
if (err.message === 'INVALID') {
setStatus('error', 'Invalid ' + info.name + ' key', 'Double-check your key and try again');
} else if (err.message.startsWith('API_')) {
setStatus('error', info.name + ' error ' + err.message.slice(4), 'Try again in a moment');
} else {
setStatus('error', 'Could not reach ' + info.name, 'Check your internet connection');
}
renderButtons();
} finally {
setLoading(false);
}
});
/* ── Disable / Enable toggle ── */
disableBtn.addEventListener('click', async () => {
if (isEnabled) {
// Disable
await chrome.storage.local.set({ pn_ai_enabled: false });
isEnabled = false;
keyInput.classList.remove('valid');
keyInput.classList.add('dimmed');
setStatus('disabled', '⏸ AI Assist is disabled', 'Your key is saved · Click "Enable AI" to use it');
notifyTab(null); // tell content.js AI is off
} else {
// Enable
await chrome.storage.local.set({ pn_ai_enabled: true });
isEnabled = true;
keyInput.classList.add('valid');
keyInput.classList.remove('dimmed');
const d = await chrome.storage.local.get('pn_provider');
const pname = d.pn_provider ? (PROVIDER_INFO[d.pn_provider]?.name || d.pn_provider) : 'AI';
setStatus('connected', '✓ AI Assist enabled', pname + ' · Ready to analyze');
notifyTab(savedKey);
}
renderButtons();
});
/* ── Notify active tab of key change ── */
function notifyTab(key) {
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
if (tabs[0]) {
chrome.tabs.sendMessage(tabs[0].id, {
type: 'SET_API_KEY',
key: key || '',
enabled: !!key
}).catch(() => {});
}
});
}
/* ── Open navigator ── */
openBtn.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab) { chrome.tabs.sendMessage(tab.id, { type: 'TOGGLE_PANEL' }).catch(() => {}); window.close(); }
});