// A searchable model picker: frontier models first, each row with provider, price, speed and context. import { esc } from "./util.js"; import { icon } from "./icons.js"; const short = (id) => id.split("/")[1] || id; const org = (id) => id.split("/")[0]; const ctx = (n) => (n ? (n >= 1e6 ? `${(n / 1e6).toFixed(n % 1e6 ? 1 : 0)}M` : `${Math.round(n / 1000)}k`) : ""); export function picker(host, { models, value, onChange, note, groupLabel }) { let current = models.find((m) => m.id === value) || models[0]; let open = false, q = "", active = 0; host.classList.add("picker"); const render = () => { const list = filtered(); host.innerHTML = ` ${open ? `
${list.length ? list.map((m, i) => `${i === 0 || m.featured !== list[i - 1].featured ? `
${groupLabel || (m.featured ? "Frontier" : "All with tool calling")}
` : ""} `).join("") : `

No model matches “${esc(q)}”.

`}
${note ? `

${note}

` : ""}
` : ""}`; if (open) { const inp = host.querySelector(".pk-q"); inp.focus(); inp.setSelectionRange(q.length, q.length); host.querySelector(".pk-row.act")?.scrollIntoView({ block: "nearest" }); } }; const filtered = () => { const t = q.trim().toLowerCase(); return t ? models.filter((m) => (m.id + " " + m.provider).toLowerCase().includes(t)) : models; }; const choose = (id) => { current = models.find((m) => m.id === id) || current; open = false; q = ""; render(); onChange(current); }; host.addEventListener("click", (e) => { if (e.target.closest(".pk-btn")) { open = !open; active = 0; render(); return; } const row = e.target.closest(".pk-row"); if (row) choose(row.dataset.id); }); host.addEventListener("input", (e) => { if (e.target.classList.contains("pk-q")) { q = e.target.value; active = 0; render(); } }); host.addEventListener("keydown", (e) => { if (!open) return; const list = filtered(); if (e.key === "ArrowDown") { active = Math.min(list.length - 1, active + 1); render(); e.preventDefault(); } else if (e.key === "ArrowUp") { active = Math.max(0, active - 1); render(); e.preventDefault(); } else if (e.key === "Enter" && list[active]) { choose(list[active].id); e.preventDefault(); } else if (e.key === "Escape") { open = false; render(); e.stopPropagation(); } }); // composedPath is fixed when the click happens; host.contains(target) is not, because render() replaces the target document.addEventListener("click", (e) => { if (open && !e.composedPath().includes(host)) { open = false; render(); } }); render(); return { get value() { return current.id; }, get model() { return current; } }; }