File size: 5,803 Bytes
04b8892 | 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 | // 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 (
<div>
<div className="ap-pop-backdrop" onClick={onClose}></div>
<div className="ap-settings" data-screen-label="Annotator settings">
<div className="ap-settings-title">The Annotator</div>
<div className="ap-settings-sub">which mind writes in your margins</div>
<div className="ap-field">
<label className="ap-field-label">Provider</label>
<div className="ap-provider-row">
{Object.entries(AP_PROVIDERS).map(([id, pr]) => (
<button
key={id}
className={'ap-provider' + (cfg.provider === id ? ' is-active' : '')}
onClick={() => set({ provider: id, model: pr.defaultModel })}
>{pr.label}</button>
))}
</div>
</div>
<div className="ap-field">
<label className="ap-field-label">Model</label>
<input
className="ap-input" type="text" value={cfg.model}
onChange={(e) => set({ model: e.target.value })}
spellCheck="false"
/>
</div>
{p.needsKey ? (
<div className="ap-field">
<label className="ap-field-label">API key</label>
<input
className="ap-input" type="password" value={cfg.key}
placeholder={p.keyPlaceholder}
onChange={(e) => set({ key: e.target.value })}
spellCheck="false" autoComplete="off"
/>
<div className="ap-field-hint">{p.hint} · stored only in this browser, sent only to {p.label}</div>
</div>
) : (
<div className="ap-field-hint">{p.hint}</div>
)}
<div className="ap-settings-foot">
<button className="ap-textbtn" onClick={onClose}>done</button>
</div>
</div>
</div>
);
}
Object.assign(window, { AnnotatorSettings, apComplete, apLoadAnnotator, apSaveAnnotator, AP_PROVIDERS });
|