// astroparse — annotator settings: model provider + API key, and the completion router const AP_ANNOTATOR_KEY = 'astroparse_annotator_v1'; const AP_PROVIDERS = { builtin: { label: 'Built-in (Claude)', needsKey: false, defaultModel: 'claude-haiku-4-5', hint: 'no key needed \u2014 runs on the prototype\u2019s built-in Claude' }, anthropic: { label: 'Anthropic', needsKey: true, defaultModel: 'claude-haiku-4-5', keyPlaceholder: 'sk-ant-\u2026', hint: 'console.anthropic.com \u2192 API keys' }, openai: { label: 'OpenAI', needsKey: true, defaultModel: 'gpt-4o-mini', keyPlaceholder: 'sk-\u2026', hint: 'platform.openai.com \u2192 API keys' }, gemini: { label: 'Gemini', needsKey: true, defaultModel: 'gemini-2.0-flash', keyPlaceholder: 'AIza\u2026', hint: 'aistudio.google.com \u2192 Get API key' }, }; function apLoadAnnotator() { try { const raw = localStorage.getItem(AP_ANNOTATOR_KEY); if (raw) { const v = JSON.parse(raw); if (v && AP_PROVIDERS[v.provider]) return v; } } catch (e) {} return { provider: 'builtin', model: AP_PROVIDERS.builtin.defaultModel, key: '' }; } function apSaveAnnotator(cfg) { try { localStorage.setItem(AP_ANNOTATOR_KEY, JSON.stringify(cfg)); } catch (e) {} } async function apComplete(cfg, prompt) { const provider = cfg.provider || 'builtin'; const model = (cfg.model || AP_PROVIDERS[provider].defaultModel).trim(); if (provider !== 'builtin' && !(cfg.key || '').trim()) { throw new Error('no API key set \u2014 open the annotator settings'); } if (provider === 'builtin') { return await window.claude.complete(prompt); } if (provider === 'anthropic') { const res = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': cfg.key.trim(), 'anthropic-version': '2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true', }, body: JSON.stringify({ model: model, max_tokens: 400, messages: [{ role: 'user', content: prompt }] }), }); if (!res.ok) throw new Error('Anthropic ' + res.status + ': ' + (await res.text()).slice(0, 140)); const data = await res.json(); return data.content[0].text; } if (provider === 'openai') { const res = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'content-type': 'application/json', authorization: 'Bearer ' + cfg.key.trim() }, body: JSON.stringify({ model: model, max_tokens: 400, messages: [{ role: 'user', content: prompt }] }), }); if (!res.ok) throw new Error('OpenAI ' + res.status + ': ' + (await res.text()).slice(0, 140)); const data = await res.json(); return data.choices[0].message.content; } if (provider === 'gemini') { const url = 'https://generativelanguage.googleapis.com/v1beta/models/' + encodeURIComponent(model) + ':generateContent?key=' + encodeURIComponent(cfg.key.trim()); const res = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }), }); if (!res.ok) throw new Error('Gemini ' + res.status + ': ' + (await res.text()).slice(0, 140)); const data = await res.json(); return data.candidates[0].content.parts[0].text; } throw new Error('unknown provider'); } function AnnotatorSettings({ cfg, onChange, onClose }) { const p = AP_PROVIDERS[cfg.provider]; const set = (patch) => onChange({ ...cfg, ...patch }); React.useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); return (