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} &middot; 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 });