| 1 | // Model picker. The list of models is the backend's to decide — this only |
| 2 | // remembers which of the offered ones the user prefers, and sends its id along |
| 3 | // with each request. An unrecognised id is refused server-side, so a stale |
| 4 | // choice is cleared here as soon as we learn what's actually on offer. |
| 5 | import { getConfig } from './config.js'; |
| 6 | import { bindPanel } from './panel.js'; |
| 7 | |
| 8 | const KEY = 'strudel-claude-model'; |
| 9 | |
| 10 | const stored = () => { |
| 11 | try { |
| 12 | return localStorage.getItem(KEY) || ''; |
| 13 | } catch { |
| 14 | return ''; |
| 15 | } |
| 16 | }; |
| 17 | |
| 18 | const store = (id) => { |
| 19 | try { |
| 20 | if (id) localStorage.setItem(KEY, id); |
| 21 | else localStorage.removeItem(KEY); |
| 22 | } catch { |
| 23 | /* storage blocked — the choice just won't survive a reload */ |
| 24 | } |
| 25 | }; |
| 26 | |
| 27 | // What goes in the request body. Empty until the backend has answered, which |
| 28 | // means "use whatever you consider the default" — the same thing an old client |
| 29 | // that doesn't know about models sends. |
| 30 | let chosen = ''; |
| 31 | let models = []; |
| 32 | |
| 33 | export const getModel = () => chosen; |
| 34 | |
| 35 | export function initModelUI() { |
| 36 | const button = document.getElementById('model-btn'); |
| 37 | const label = document.getElementById('model-state'); |
| 38 | const panel = document.getElementById('model-panel'); |
| 39 | const select = document.getElementById('model-select'); |
| 40 | const note = document.getElementById('model-note'); |
| 41 | |
| 42 | const current = () => models.find((m) => m.id === chosen); |
| 43 | |
| 44 | const render = () => { |
| 45 | const model = current(); |
| 46 | label.textContent = model ? model.label : 'Model'; |
| 47 | note.textContent = model?.blurb || ''; |
| 48 | select.value = chosen; |
| 49 | // Nothing to choose between until the backend answers, or if it only |
| 50 | // offers one model. |
| 51 | button.disabled = models.length < 2; |
| 52 | }; |
| 53 | |
| 54 | bindPanel(button, panel, () => select.focus()); |
| 55 | |
| 56 | select.addEventListener('change', () => { |
| 57 | chosen = select.value; |
| 58 | store(chosen); |
| 59 | render(); |
| 60 | openPanel(false); |
| 61 | }); |
| 62 | |
| 63 | render(); |
| 64 | |
| 65 | getConfig().then((cfg) => { |
| 66 | if (!Array.isArray(cfg?.models) || !cfg.models.length) return; |
| 67 | models = cfg.models; |
| 68 | |
| 69 | // Honour the saved choice only if this backend still offers it; otherwise |
| 70 | // fall back to its default rather than sending an id it will refuse. |
| 71 | const saved = stored(); |
| 72 | chosen = models.some((m) => m.id === saved) ? saved : cfg.defaultModel || models[0].id; |
| 73 | if (chosen !== saved) store(chosen); |
| 74 | |
| 75 | select.replaceChildren( |
| 76 | ...models.map((m) => { |
| 77 | const opt = document.createElement('option'); |
| 78 | opt.value = m.id; |
| 79 | opt.textContent = m.label; |
| 80 | return opt; |
| 81 | }), |
| 82 | ); |
| 83 | render(); |
| 84 | }); |
| 85 | } |