/* ============================================================ FitCheck — frontend logic (UI brick) Gathers inputs, calls the /api/advise connector, renders results. The backend currently returns input-aware PLACEHOLDER data; the real engine plugs into the same /api/advise contract later. ============================================================ */ // ---- Inline icon helpers (icons.js loads first) -------------------------- function ic(name) { return (window.ICONS && window.ICONS[name]) || ""; } function hydrate(root) { (root || document).querySelectorAll("[data-ic]").forEach(el => { if (!el.dataset.done) { el.innerHTML = ic(el.dataset.ic); el.dataset.done = "1"; } }); } // ---- Use-case taxonomy: the full realm, not just LLMs -------------------- const USE_CASES = [ { icon: "cat-text", name: "Text & chat", items: [ { id: "chat", icon: "chat", label: "Chatbot / assistant" }, { id: "writing", icon: "writing", label: "Writing & summarising" }, { id: "coding", icon: "coding", label: "Coding help" }, { id: "agents", icon: "agents", label: "Agents & tool use" }, { id: "rag", icon: "rag", label: "Chat with my documents" }, { id: "translate", icon: "translate", label: "Translation" }, ]}, { icon: "cat-vision", name: "See & understand images", items: [ { id: "vlm", icon: "classify", label: "Chat about images (VLM)" }, { id: "detect", icon: "detect", label: "Object detection (YOLO)" }, { id: "segment", icon: "segment", label: "Image segmentation" }, { id: "pose", icon: "pose", label: "Pose / 6-DoF (FoundationPose)" }, { id: "classify",icon: "classify", label: "Image classification" }, { id: "depth", icon: "depth", label: "Depth estimation" }, { id: "ocr", icon: "ocr", label: "Read text from images (OCR)" }, ]}, { icon: "cat-gen", name: "Create images & video", items: [ { id: "imagegen", icon: "imagegen", label: "Generate images (SD / Flux)" }, { id: "inpaint", icon: "inpaint", label: "Edit / inpaint images" }, { id: "upscale", icon: "upscale", label: "Upscale / restore" }, { id: "videogen", icon: "videogen", label: "Generate video" }, { id: "bgremove", icon: "bgremove", label: "Remove backgrounds" }, ]}, { icon: "cat-audio", name: "Audio & speech", items: [ { id: "stt", icon: "stt", label: "Speech to text (Whisper)" }, { id: "tts", icon: "tts", label: "Text to speech / voice" }, { id: "music", icon: "music", label: "Generate music" }, ]}, { icon: "cat-data", name: "Data & search", items: [ { id: "embed", icon: "embed", label: "Semantic search / embeddings" }, { id: "forecast", icon: "forecast", label: "Time-series forecasting" }, { id: "tabular", icon: "tabular", label: "Predict from spreadsheets" }, ]}, { icon: "cat-train", name: "Train your own", items: [ { id: "finetune", icon: "finetune", label: "Fine-tune an LLM (LoRA)" }, { id: "train-vision", icon: "train-vision", label: "Train a vision model" }, ]}, { icon: "cat-custom", name: "Something else", items: [ { id: "custom", icon: "custom", label: "Custom: describe it" }, ]}, ]; // ---- GPU lists per provider ---------------------------------------------- const GPUS = { none: [], unsure: [], nvidia: [ "RTX 5090 (32 GB)","RTX 5080 (16 GB)","RTX 5070 Ti (16 GB)","RTX 5070 (12 GB)","RTX 5060 (8 GB)", "RTX 4090 (24 GB)","RTX 4080 (16 GB)","RTX 4070 (12 GB)","RTX 4060 (8 GB)", "RTX 3090 (24 GB)","RTX 3080 (10 GB)","RTX 3070 (8 GB)","RTX 3060 (12 GB)","RTX 3050 (8 GB)", "GTX 1660 (6 GB)","GTX 1650 (4 GB)","Laptop RTX (not sure of model)", ], amd: ["RX 7900 XTX (24 GB)","RX 7800 XT (16 GB)","RX 7600 (8 GB)","RX 6700 XT (12 GB)","Built-in Radeon"], apple: ["M-series base (8 GB)","M-series Pro (16 GB)","M-series Max (32 GB)","M-series Ultra (64 GB)"], intel: ["Arc A770 (16 GB)","Arc A750 (8 GB)","Arc A380 (6 GB)","Built-in Intel graphics"], }; const $ = (s) => document.querySelector(s); const state = { mode: "have", computer: "Windows laptop", provider: "none", priority: "balanced", usecases: ["chat"], checked: false }; let lastAdvice = null; // the most recent /api/advise result — facts the model explains let multiCache = null; // {ucs, results} when several goals are checked at once // ---- Buy-vs-check mode ----------------------------------------------------- function applyMode() { const buy = state.mode === "buy"; ["#machine-step", "#priority-step", "#find-specs"].forEach(s => { const el = $(s); if (el) el.style.display = buy ? "none" : ""; }); const repo = $("#repo-field"); if (repo) repo.style.display = buy ? "none" : ""; $("#check-btn").innerHTML = (buy ? "What should I get? " : "Check my setup ") + ''; hydrate($("#check-btn")); } // ---- Best-effort hardware hints from the browser (honest: vendor + floor) -- async function detectHardware() { const bits = []; try { if (navigator.gpu) { const ad = await navigator.gpu.requestAdapter(); const v = ((ad && ad.info && (ad.info.vendor || ad.info.description)) || "").toLowerCase(); for (const vendor of ["nvidia", "amd", "apple", "intel"]) { if (v.includes(vendor)) { state.provider = vendor; setActive("#provider-seg", vendor); fillGpu(); bits.push(`a ${vendor.toUpperCase().replace("APPLE","Apple")} GPU`); break; } } } } catch (e) { /* detection is best-effort only */ } if (navigator.deviceMemory) bits.push(`at least ${navigator.deviceMemory} GB of RAM`); if (bits.length) { const h = $("#detect-hint"); h.style.display = ""; h.textContent = `Your browser reports ${bits.join(" and ")} — browsers can't see exact specs, so please confirm below.`; } } // ---- Build the use-case picker ------------------------------------------- function buildPicker() { const wrap = $("#usecase-picker"); wrap.innerHTML = USE_CASES.map(g => `
${g.name}
${g.items.map(it => ` `).join("")}
`).join(""); hydrate(wrap); // Pills toggle: pick one goal or several (several = checked together). wrap.querySelectorAll(".uc-pill").forEach(p => p.addEventListener("click", () => { const uc = p.dataset.uc; const i = state.usecases.indexOf(uc); if (i >= 0) { if (state.usecases.length > 1) { state.usecases.splice(i, 1); p.classList.remove("active"); } } else { state.usecases.push(uc); p.classList.add("active"); } $("#custom-uc-field").style.display = state.usecases.includes("custom") ? "block" : "none"; maybeLiveUpdate(); })); } // ---- Segmented controls --------------------------------------------------- function wireSegmented(id, key, after) { $(id).querySelectorAll(".seg-btn").forEach(b => b.addEventListener("click", () => { $(id).querySelectorAll(".seg-btn").forEach(x => x.classList.remove("active")); b.classList.add("active"); state[key] = b.dataset.val; if (after) after(); maybeLiveUpdate(); })); } function setActive(id, val) { $(id).querySelectorAll(".seg-btn").forEach(b => b.classList.toggle("active", b.dataset.val === val)); } // ---- GPU select depends on provider -------------------------------------- function fillGpu() { const list = GPUS[state.provider] || []; const sel = $("#gpu"); if (!list.length) { sel.style.display = "none"; sel.innerHTML = ""; } else { sel.style.display = "block"; sel.innerHTML = list.map(g => ``).join(""); } } // Real machines only: a Windows laptop can't have Apple Silicon, a Pi can't // take a desktop card. Impossible provider buttons get disabled per computer. const PROVIDER_ALLOWED = { "Windows laptop": ["none", "nvidia", "amd", "intel", "unsure"], "Windows desktop": ["none", "nvidia", "amd", "intel", "unsure"], "Linux PC": ["none", "nvidia", "amd", "intel", "unsure"], "Mac": ["apple"], "Mini PC / Raspberry Pi": ["none", "nvidia", "unsure"], // nvidia = Jetson boards }; function syncProviderForComputer() { const allowed = PROVIDER_ALLOWED[state.computer] || ["none", "nvidia", "amd", "intel", "apple", "unsure"]; $("#provider-seg").querySelectorAll(".seg-btn").forEach(b => { const ok = allowed.includes(b.dataset.val); b.classList.toggle("disabled", !ok); b.disabled = !ok; }); if (!allowed.includes(state.provider)) { state.provider = allowed[0]; setActive("#provider-seg", state.provider); } fillGpu(); } // ---- Find-my-specs help text --------------------------------------------- function findSpecsText() { const c = state.computer; if (c === "Mac") return ` `; if (c === "Linux PC" || c.includes("Mini PC")) return ` `; return ` `; } // ---- Gather inputs & call the connector ---------------------------------- function gather() { const sel = $("#gpu"); return { computer: state.computer, ram_gb: parseFloat($("#ram").value), provider: state.provider, gpu: sel.style.display === "none" ? "" : sel.value, vram_gb: $("#vram").value ? parseFloat($("#vram").value) : null, paste: $("#paste").value.trim(), usecase: state.usecases[0], usecases: state.usecases.slice(), custom: $("#custom-uc").value.trim(), priority: state.priority, repo: $("#repo-check") ? $("#repo-check").value.trim() : "", }; } let debounce; function maybeLiveUpdate() { if (!state.checked) return; clearTimeout(debounce); debounce = setTimeout(check, 200); } async function check() { state.checked = true; const payload = gather(); try { if (state.mode === "buy") { const res = await fetch("/api/minspecs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ usecases: state.usecases }), }); renderBuy(await res.json()); return; } if (state.usecases.length > 1) { const results = await Promise.all(state.usecases.map(u => fetch("/api/advise", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...payload, usecase: u }), }).then(r => r.json()))); multiCache = { ucs: state.usecases.slice(), results }; renderMulti(results); if (payload.repo) lookupRepo(payload); return; } multiCache = null; const res = await fetch("/api/advise", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); render(await res.json()); if (payload.repo) lookupRepo(payload); // optional live check, appended on top } catch (e) { $("#results").innerHTML = `

Couldn't reach the advisor: ${e && e.message ? e.message : e}

`; hydrate($("#results")); } } // ---- Live single-model lookup (the one online feature, labelled as such) --- async function lookupRepo(payload) { const holder = $("#lookup-result"); if (!holder) return; holder.innerHTML = `
Looking up ${payload.repo} on Hugging Face…
`; try { const res = await fetch("/api/lookup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const d = await res.json(); if (d.error) { holder.innerHTML = `

Couldn't check that model

${d.error}

`; return; } const o = d.option || {}; const v = VMAP[o.verdict] || VMAP.tight; holder.innerHTML = `
${v.word} Live Hugging Face lookup

${d.explain || ""}

${o.memory && o.memory !== "Too big" ? `${o.memory} needed (${o.setting})` : `Too big for this machine`} ${o.url ? `View on Hugging Face` : ""}
${o.run && o.run.ollama ? `
Run it
${o.run.ollama}
` : ""}
`; holder.querySelectorAll(".copy-btn").forEach(b => b.addEventListener("click", () => { navigator.clipboard.writeText(decodeURIComponent(b.dataset.code)); b.textContent = "Copied ✓"; setTimeout(() => { b.textContent = "Copy"; }, 1500); })); } catch (e) { holder.innerHTML = `

Lookup failed

${e && e.message ? e.message : e}

`; } } // ---- Multi-goal overview (several goals checked at once) ------------------- function renderMulti(results) { const ok = results.filter(d => d.verdict === "great").length; const cards = results.map((d, i) => { const v = VMAP[d.verdict] || VMAP.tight; const need = (d.gauge || {}).need_gb || ""; return `
${d.verdict_word || v.word}
${d.use_case || ""}
${d.headline_model ? `→ ${d.headline_model}` : "Nothing realistic on this machine"}
${need}
See full breakdown
`; }).join(""); $("#results").innerHTML = `
${results.length} goals checked

${ok} of ${results.length} run great on this machine.

Each goal is checked independently with the same conservative engine. Click any card for the full honest breakdown, links and commands.

${cards}
`; hydrate($("#results")); $("#results").querySelectorAll(".goal-card").forEach(c => c.addEventListener("click", () => { render(multiCache.results[parseInt(c.dataset.i, 10)]); })); $("#cat-version").textContent = (results[0] || {}).catalogue_version || "—"; } // ---- Buy-advice render ------------------------------------------------------ function renderBuy(d) { const goalLines = (t) => (t.goals && t.goals.length > 1) ? `` : `
Runs: ${t.runs}${t.goals && t.goals[0] && t.goals[0].verdict === "tight" ? " (with trade-offs)" : ""}
`; const lane = (title, icon, lanes) => { const tier = (t, kind) => t ? `
${kind === "min" ? "Minimum" : "Comfortable"} ${t.price}
${t.label}
${goalLines(t)}
` : `
No tier on this ladder handles ${d.goals && d.goals.length > 1 ? "all of these together" : "it"} comfortably — this combination wants workstation hardware.
`; return `
${title}
${tier(lanes.minimum, "min")}${tier(lanes.comfortable, "comfy")}
`; }; $("#results").innerHTML = `
Buying advice

What you need for ${(d.use_case || "this").toLowerCase()}

Two honest tiers per platform: the cheapest setup that genuinely works, and the one that feels good daily.

${d.note ? `
${d.note}
` : ""}
${lane("Windows / Linux PC", "brand-windows", d.pc || {})} ${lane("Mac (Apple Silicon)", "brand-apple", d.mac || {})}

${d.disclaimer || ""}

`; hydrate($("#results")); $("#cat-version").textContent = d.catalogue_version || "—"; } // ---- Render results ------------------------------------------------------- const VMAP = { great: { cls: "var(--ok)", soft: "var(--ok-soft)", word: "Runs great", em: "✓" }, tight: { cls: "var(--warn)", soft: "var(--warn-soft)", word: "Tight, but works", em: "≈" }, no: { cls: "var(--no)", soft: "var(--no-soft)", word: "Won't fit", em: "✕" }, }; function render(d) { lastAdvice = d; const v = VMAP[d.verdict] || VMAP.tight; const g = d.gauge || {}; $("#cat-version").textContent = d.catalogue_version || "—"; const licChip = (o) => { if (!o.license) return ""; const note = (o.license_note || "").toLowerCase(); const warn = note.includes("non-commercial") || note.includes("agpl") || note.includes("research"); const label = o.license.replace("apache-2.0", "Apache 2.0").replace("mit", "MIT") .replace("agpl-3.0", "AGPL").replace("cc-by-nc-4.0", "CC-NC"); return `${label}` + (o.gated ? `gated` : ""); }; const opts = (d.options || []).map(o => { const ov = VMAP[o.verdict] || VMAP.tight; const name = o.url ? `${o.model}` : o.model; return `
${ov.em}
${name}${licChip(o)}
${o.desc}
${o.memory}
${o.setting}${o.feel && o.feel !== "—" ? " · " + o.feel : ""}
`; }).join(""); const tools = (d.tools || []).map((t, i) => `
${t.name} ${i===0?"Start here":t.tag}
${t.what}
${t.install}
`).join(""); const cmds = (d.commands?.items || []).map((c) => `
${c.label}
${c.code}
`).join(""); $("#results").innerHTML = `
${d.verdict_word || v.word}

${d.headline || ""}

${d.detail || ""}

${d.note ? `
${d.note}
` : ""}
${g.fill_pct != null ? `
Memory needed vs. what you have ${g.need_gb}
${(g.breakdown||[]).map(b=>`${b.label}`).join("")} Fast: ${g.fast_gb} Total: ${g.total_gb}
${d.provenance ? `
${d.provenance}
` : ""}
` : ""} ${d.speed ? `
Why this speed? real benchmark runs, and where you land

Every grey dot is a real benchmark run from the LocalScore community database. Generation speed tracks memory bandwidth — that's the one number that matters most for local AI (why). The dashed line is the theoretical ceiling for ${d.speed.model} at your setting; your machine is the marked dot at ≈${d.speed.tps} tok/s (${d.speed.method === "measured-model" ? `predicted by a model trained on these measurements, following IBM's LLM-Pilot methodology` : `an analytical estimate; a learned predictor trained on these runs — LLM-Pilot methodology — takes over once trained`}).

` : ""} ${opts ? `
What you can run real models, biggest to smallest — names link to Hugging Face
${opts}
` : ""} ${tools ? `
How to actually run it
${tools}
` : ""} ${cmds ? `
Copy-paste to get started

${d.commands.intro || ""}

${cmds}
` : ""}
Ask a follow-up explained in plain words, from the numbers above
`; if (multiCache) { const back = document.createElement("button"); back.className = "back-link"; back.textContent = "← All goals"; back.addEventListener("click", () => renderMulti(multiCache.results)); $("#results").firstElementChild.prepend(back); } hydrate($("#results")); if (d.speed) drawRoofline(d.speed); $("#results").querySelectorAll(".copy-btn").forEach(b => b.addEventListener("click", () => { navigator.clipboard.writeText(decodeURIComponent(b.dataset.code)); b.textContent = "Copied ✓"; b.classList.add("done"); setTimeout(() => { b.textContent = "Copy"; b.classList.remove("done"); }, 1500); })); wireAsk(); } // ---- "Why this speed?" roofline scatter (real LocalScore runs) ------------ let _rooflinePts = null; async function getRooflinePoints() { if (_rooflinePts) return _rooflinePts; try { const r = await fetch("/static/roofline.json"); _rooflinePts = await r.json(); } catch (e) { _rooflinePts = { points: [] }; } return _rooflinePts; } async function drawRoofline(speed) { const host = $("#roofline-chart"); if (!host) return; const data = await getRooflinePoints(); const pts = (data.points || []).filter(p => p.bw > 0 && p.tps > 0.5); if (!pts.length && !speed) { host.innerHTML = ""; return; } const W = 720, H = 320, L = 52, R = 16, T = 14, B = 40; const xmin = 40, xmax = 2100, ymin = 0.8, ymax = 400; const lx = v => L + (Math.log10(v) - Math.log10(xmin)) / (Math.log10(xmax) - Math.log10(xmin)) * (W - L - R); const ly = v => H - B - (Math.log10(v) - Math.log10(ymin)) / (Math.log10(ymax) - Math.log10(ymin)) * (H - T - B); let s = ``; // gridlines + labels for (const gx of [50, 100, 200, 400, 800, 1600]) { s += `` + `${gx}`; } for (const gy of [1, 3, 10, 30, 100, 300]) { s += `` + `${gy}`; } s += `memory bandwidth (GB/s, log)`; s += `decode tok/s (log)`; // real measurement dots, shaded by model size const shade = p => p.params_b <= 2 ? "rl-p1" : (p.params_b <= 9 ? "rl-p8" : "rl-p14"); for (const p of pts) { if (p.bw < xmin || p.tps < ymin) continue; s += `${p.accel} — ${p.model}: ${p.tps} tok/s`; } if (speed) { // theoretical ceiling for the recommended model: tps = 0.6 * bw / bytes const bytes = speed.bytes_gb || 5; const x1 = xmin, x2 = xmax; const f = bw => Math.min(Math.max(0.6 * bw / bytes, ymin), ymax); s += ``; // your machine const ux = lx(Math.min(Math.max(speed.eff_bw || speed.bw, xmin), xmax)); const uy = ly(Math.min(Math.max(speed.tps, ymin), ymax)); s += ``; // flip the label to the left when the dot sits near the right edge const flip = ux > W * 0.72; s += `` + `you ≈${speed.tps} tok/s`; } s += `
~1B model runs ~8B runs ~14B runs theoretical ceiling (your pick) your machine
`; host.innerHTML = s; } // ---- Live progress ticker (gradio-style elapsed seconds) ------------------- function startTicker(el, base) { const t0 = Date.now(); el.dataset.ticking = "1"; const tick = () => { if (el.dataset.ticking !== "1") return; const s = Math.round((Date.now() - t0) / 1000); el.innerHTML = ` ${base} — ${s}s` + (s > 8 ? " (cold start: the model is waking, up to ~1 min)" : ""); setTimeout(tick, 500); }; tick(); return () => { el.dataset.ticking = "0"; }; } // ---- Paste box: the fine-tuned spec parser fills the form ----------------- async function parsePaste() { const text = $("#paste").value.trim(); const hint = $("#parse-hint"); const btn = $("#parse-btn"); if (!text) { hint.textContent = "Type or paste something first."; return; } if (btn.disabled) return; // one in-flight call, ever — GPU time is real money btn.disabled = true; const stop = startTicker(hint, "Reading your description"); try { const client = await getClient(); const r = await client.predict("/parse", { text }); const d = Array.isArray(r.data) ? r.data[0] : r.data; stop(); if (d.error) { hint.textContent = `Failed: ${d.error}`; return; } applyParsed(d, hint); } catch (e) { stop(); hint.textContent = `Failed: ${e && e.message ? e.message : e}`; } finally { btn.disabled = false; } } function applyParsed(d, hint) { const got = []; if (d.computer) { state.computer = d.computer; setActive("#computer-seg", d.computer); syncProviderForComputer(); got.push(d.computer); } if (d.provider) { state.provider = d.provider; setActive("#provider-seg", d.provider); fillGpu(); got.push(d.provider.toUpperCase()); } if (d.gpu) { const sel = $("#gpu"); const want = String(d.gpu).toLowerCase().replace(/\b(nvidia|geforce|amd|radeon|intel)\b/g, "").trim(); const match = [...sel.options].find(o => o.value.toLowerCase().includes(want)); if (match) { sel.value = match.value; got.push(match.value); } else if (d.vram_gb) { $("#vram").value = d.vram_gb; got.push(`${d.gpu} (${d.vram_gb} GB)`); } else got.push(d.gpu); } else if (d.vram_gb) { $("#vram").value = d.vram_gb; got.push(`${d.vram_gb} GB VRAM`); } if (d.ram_gb) { const ram = $("#ram"); const opt = [...ram.options].map(o => parseFloat(o.value)) .reduce((a, b) => Math.abs(b - d.ram_gb) < Math.abs(a - d.ram_gb) ? b : a); ram.value = `${opt} GB`; got.push(`${d.ram_gb} GB RAM`); } hint.textContent = got.length ? `Understood: ${got.join(", ")}. Anything you didn't mention stayed blank — confirm and press Check.` : "Couldn't find any specs in that text — nothing was filled in (the parser doesn't guess)."; maybeLiveUpdate(); } // ---- Follow-up: the model brick (grounded explainer) --------------------- function wireAsk() { const input = $("#ask-input"), send = $("#ask-send"); if (!input || !send) return; const go = () => askQuestion(input.value); send.addEventListener("click", go); input.addEventListener("keydown", e => { if (e.key === "Enter") go(); }); $("#results").querySelectorAll(".ask-chip").forEach(c => c.addEventListener("click", () => { input.value = c.textContent; askQuestion(c.textContent); })); } let askBusy = false; async function askQuestion(question) { question = (question || "").trim(); const box = $("#ask-answer"); if (!question || !box || askBusy) return; askBusy = true; box.hidden = false; box.innerHTML = `
`; const stop = startTicker($("#ask-tick"), "Thinking it through"); try { const a = await callAsk(question, JSON.stringify(lastAdvice || {})); stop(); renderAnswer(box, a); } catch (e) { stop(); // Surface the real error, never a generic stand-in. box.innerHTML = `

The explainer hit an error

${e && e.message ? e.message : e}

`; } finally { askBusy = false; } } function renderAnswer(box, a) { a = a || {}; if (a.error) { // surface the real error from the model brick, not filler box.innerHTML = `

The explainer hit an error

${a.error}

`; return; } box.innerHTML = `
${a.headline ? `

${a.headline}

` : ""} ${a.why ? `

${a.why}

` : ""} ${a.next_step ? `
${a.next_step}
` : ""}
`; hydrate(box); } // On a ZeroGPU Space the JS client is REQUIRED (it forwards the HF iframe auth // headers ZeroGPU needs). Locally / non-ZeroGPU we fall back to the raw // two-step call so the chat still works with no internet to a CDN. let _gradioClient = null; async function getClient() { if (_gradioClient) return _gradioClient; const mod = await import("https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"); const Client = mod.Client || mod.client; _gradioClient = await Client.connect(window.location.origin); return _gradioClient; } async function callAsk(question, facts) { try { const client = await getClient(); const r = await client.predict("/ask", { question, facts }); return Array.isArray(r.data) ? r.data[0] : r.data; } catch (e) { return await callAskRaw(question, facts); } } async function callAskRaw(question, facts) { const post = await fetch("/gradio_api/call/ask", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: [question, facts] }), }); const { event_id } = await post.json(); const res = await fetch(`/gradio_api/call/ask/${event_id}`); const text = await res.text(); const lines = [...text.matchAll(/data:\s*(.+)/g)]; // SSE data frames if (!lines.length) throw new Error("no data in stream"); const arr = JSON.parse(lines[lines.length - 1][1]); // last frame = result return Array.isArray(arr) ? arr[0] : arr; } // ---- Init ----------------------------------------------------------------- function init() { hydrate(document); buildPicker(); wireSegmented("#mode-seg", "mode", applyMode); wireSegmented("#computer-seg", "computer", () => { syncProviderForComputer(); $("#find-specs-body").innerHTML = findSpecsText(); }); wireSegmented("#provider-seg", "provider", fillGpu); wireSegmented("#priority-seg", "priority"); ["#ram","#gpu","#vram","#custom-uc","#repo-check"].forEach(s => { const el = $(s); if (el) el.addEventListener("change", maybeLiveUpdate); }); $("#paste").addEventListener("input", maybeLiveUpdate); $("#check-btn").addEventListener("click", check); const pb = $("#parse-btn"); if (pb) pb.addEventListener("click", parsePaste); syncProviderForComputer(); $("#find-specs-body").innerHTML = findSpecsText(); detectHardware(); // Pre-filled share/preview links: ?go renders immediately; optional // ?gpu=NVIDIA|RTX 3060 (12 GB)&ram=16&uc=chat pre-select a profile. const q = new URLSearchParams(location.search); if (q.has("gpu")) { const [vendor, label] = (q.get("gpu") || "").split("|"); if (vendor) { state.provider = vendor.toLowerCase(); setActive("#provider-seg", state.provider); fillGpu(); } if (label) { const sel = $("#gpu"); [...sel.options].forEach(o => { if (o.value === label) sel.value = label; }); } } if (q.has("ram")) $("#ram").value = `${q.get("ram")} GB`; if (q.has("uc")) { state.usecases = [q.get("uc")]; document.querySelectorAll(".uc-pill").forEach(p => p.classList.toggle("active", p.dataset.uc === q.get("uc"))); } if (q.has("go")) check(); } init();