/** * UI wiring: model/mode selection, token handling, and the live execution trace. * * The trace is rendered in the exact `[Turn N] -> tool(args) <- result` notation * the assignment brief specifies, and it is written as the run happens rather * than reconstructed afterwards. */ import { runAgent } from "./agent.js"; /* ------------------------------------------------------------------ * * model catalogue * * `nativeTools` reflects what is actually in each model's chat_template, * verified against tokenizer_config.json — not guesswork. * ------------------------------------------------------------------ */ const MODELS = [ { id: "Qwen/Qwen3.5-2B", provider: "featherless-ai", label: "Qwen3.5-2B", nativeTools: true, note: "2B. Has native tool support, but is small enough to fail the prompted protocol sometimes — the honest demo case.", }, { id: "Qwen/Qwen3.5-4B", provider: "featherless-ai", label: "Qwen3.5-4B", nativeTools: true, note: "4B. Verified working end-to-end in prompted mode.", }, { id: "google/gemma-2-2b-it", provider: "featherless-ai", label: "Gemma-2-2B-it", nativeTools: false, note: "No native tool support at all, and its template rejects a system role. Prompted mode is the only way it can call a tool.", }, { id: "microsoft/Phi-3-mini-4k-instruct", provider: "featherless-ai", label: "Phi-3-mini (3.8B)", nativeTools: false, note: "No native tool support. Prompted mode only.", }, { id: "mistralai/Mistral-7B-Instruct-v0.2", provider: "featherless-ai", label: "Mistral-7B-Instruct-v0.2", nativeTools: false, note: "No native tool support. Prompted mode only.", }, { id: "Qwen/Qwen2.5-3B-Instruct", provider: "featherless-ai", label: "Qwen2.5-3B-Instruct", nativeTools: true, note: "Native tool support — useful as the control group.", }, ]; const EXAMPLES = [ "What is Bitcoin trading at right now?", "Is Ethereum pricier than Solana, and what is Ethereum worth in Turkish lira?", "How has Cardano done over the last 30 days?", "What is Dogecoin, and what is its market cap in euros?", ]; /* ------------------------------------------------------------------ * * dom * ------------------------------------------------------------------ */ const $ = (id) => document.getElementById(id); const els = { model: $("model"), modelHint: $("model-hint"), modeGroup: $("mode-group"), modeHint: $("mode-hint"), token: $("token"), tokenHint: $("token-hint"), signin: $("signin"), chat: $("chat"), examples: $("examples"), q: $("q"), send: $("send"), banner: $("banner-slot"), }; let mode = "auto"; let history = []; let busy = false; /* ------------------------------------------------------------------ * * setup * ------------------------------------------------------------------ */ function initModels() { const groups = [ { name: "No native tool support — prompted mode only", want: false }, { name: "Native tool support — control group", want: true }, ]; for (const g of groups) { const og = document.createElement("optgroup"); og.label = g.name; for (const m of MODELS.filter((m) => m.nativeTools === g.want)) { const o = document.createElement("option"); o.value = m.id; o.textContent = m.label; og.appendChild(o); } els.model.appendChild(og); } els.model.value = "Qwen/Qwen3.5-2B"; syncModelHint(); els.model.addEventListener("change", syncModelHint); } const currentModel = () => MODELS.find((m) => m.id === els.model.value) || MODELS[0]; function syncModelHint() { const m = currentModel(); els.modelHint.textContent = m.note; syncModeHint(); } function syncModeHint() { const m = currentModel(); if (mode === "native" && !m.nativeTools) { els.modeHint.innerHTML = "This model has no native tool support — expect it to ignore the tools and answer from memory. That is the point of the comparison."; } else if (mode === "auto") { els.modeHint.textContent = "Auto tries native first, then falls back to the prompted protocol."; } else if (mode === "prompted") { els.modeHint.textContent = "Schemas are injected into the system prompt; calls are parsed out of plain text."; } else { els.modeHint.textContent = "Uses the provider's built-in tool-calling API."; } } function initModes() { els.modeGroup.addEventListener("click", (e) => { const btn = e.target.closest("button[data-mode]"); if (!btn) return; mode = btn.dataset.mode; for (const b of els.modeGroup.querySelectorAll("button")) { b.setAttribute("aria-pressed", String(b === btn)); } syncModeHint(); }); } function initExamples() { for (const ex of EXAMPLES) { const b = document.createElement("button"); b.type = "button"; b.textContent = ex; b.addEventListener("click", () => { els.q.value = ex; els.q.focus(); }); els.examples.appendChild(b); } } /* ------------------------------------------------------------------ * * token: OAuth when hosted on a Space, pasted token otherwise * ------------------------------------------------------------------ */ const TOKEN_KEY = "ftas_token"; async function initAuth() { const saved = sessionStorage.getItem(TOKEN_KEY); if (saved) els.token.value = saved; els.token.addEventListener("change", () => sessionStorage.setItem(TOKEN_KEY, els.token.value.trim()) ); // window.huggingface is injected only inside a Space. No Space, no OAuth. if (!window.huggingface?.variables?.OAUTH_CLIENT_ID) return; try { const { oauthLoginUrl, oauthHandleRedirectIfPresent } = await import( "https://esm.sh/@huggingface/hub@1" ); const result = await oauthHandleRedirectIfPresent(); if (result?.accessToken) { els.token.value = result.accessToken; sessionStorage.setItem(TOKEN_KEY, result.accessToken); els.tokenHint.innerHTML = `Signed in as ${escapeHTML( result.userInfo?.name || "your account" )}. Inference uses your own quota.`; return; } els.signin.hidden = false; els.signin.addEventListener("click", async () => { window.location.href = await oauthLoginUrl({ scopes: "openid profile inference-api" }); }); } catch { // CDN blocked or OAuth misconfigured: the pasted-token path still works. els.signin.hidden = true; } } /* ------------------------------------------------------------------ * * rendering * ------------------------------------------------------------------ */ const escapeHTML = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); function addMessage(who, text) { const wrap = document.createElement("div"); wrap.className = `msg ${who}`; wrap.innerHTML = `