beacon / frontend /src /components /AnnotatorPanel.jsx
kiyer's picture
feat: streaming annotation generation and annotator panel
25e1306
Raw
History Blame Contribute Delete
3.83 kB
import React from 'react';
// Annotator settings panel — ported from prototype/annotator.jsx.
// Changes from prototype:
// - "Built-in (Claude)" provider chip DROPPED; three chips: Anthropic / OpenAI / Gemini.
// - Default provider is 'anthropic' (no built-in server-funded model).
// - API key hint: "stored only in this browser · sent per-request to {provider} via your local backend".
// - Model defaults updated per task spec.
const AP_ANNOTATOR_KEY = 'astroparse_annotator_v1';
export const AP_PROVIDERS = {
anthropic: {
label: 'Anthropic',
needsKey: true,
defaultModel: 'claude-haiku-4-5',
keyPlaceholder: 'sk-ant-…',
hint: 'console.anthropic.com → API keys',
},
openai: {
label: 'OpenAI',
needsKey: true,
defaultModel: 'gpt-4o-mini',
keyPlaceholder: 'sk-…',
hint: 'platform.openai.com → API keys',
},
gemini: {
label: 'Gemini',
needsKey: true,
defaultModel: 'gemini-2.0-flash',
keyPlaceholder: 'AIza…',
hint: 'aistudio.google.com → Get API key',
},
};
export 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) { /* ignore */ }
return { provider: 'anthropic', model: AP_PROVIDERS.anthropic.defaultModel, key: '' };
}
export function apSaveAnnotator(cfg) {
try { localStorage.setItem(AP_ANNOTATOR_KEY, JSON.stringify(cfg)); } catch (e) { /* ignore */ }
}
export default function AnnotatorPanel({ 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} &middot; stored only in this browser &middot; sent per-request to {p.label} via your local backend
</div>
</div>
)}
<div className="ap-settings-foot">
<button className="ap-textbtn" onClick={onClose}>done</button>
</div>
</div>
</div>
);
}