import { pipeline, cos_sim } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0"; const form = document.getElementById("ask-form"); const input = document.getElementById("question"); const askBtn = document.getElementById("ask-btn"); const btnLabel = askBtn.querySelector(".btn-label"); const spinner = askBtn.querySelector(".spinner"); const result = document.getElementById("result"); const sourcesList = document.getElementById("sources-list"); const errorEl = document.getElementById("error"); const statusBadge = document.getElementById("status-badge"); let embedder = null; let index = []; // [{page, modality, text, image, embedding}] function setLoading(isLoading) { askBtn.disabled = isLoading; spinner.hidden = !isLoading; btnLabel.textContent = isLoading ? "Searching" : "Search"; } function enableUI() { input.disabled = false; askBtn.disabled = false; document.querySelectorAll(".chip").forEach((c) => (c.disabled = false)); } async function init() { try { const [model, data] = await Promise.all([ pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2"), fetch("data/index.json").then((r) => { if (!r.ok) throw new Error("index.json not found"); return r.json(); }), ]); embedder = model; index = data; statusBadge.textContent = `index ready · ${index.length} chunks`; statusBadge.classList.add("ok"); enableUI(); } catch (err) { statusBadge.textContent = "failed to load index"; statusBadge.classList.add("bad"); errorEl.textContent = "Could not load the model or data/index.json. Make sure data/index.json and images/ were copied in (see README.md)."; errorEl.hidden = false; } } async function embedQuery(text) { const output = await embedder(text, { pooling: "mean", normalize: true }); return Array.from(output.data); } function renderResults(hits) { sourcesList.innerHTML = ""; hits.forEach((h) => { const item = document.createElement("div"); item.className = "source-item"; if (h.image) { const img = document.createElement("img"); img.src = `images/${h.image}`; img.alt = `Page ${h.page} visual`; item.appendChild(img); } const body = document.createElement("div"); const meta = document.createElement("div"); meta.className = "source-meta"; meta.innerHTML = `PAGE ${h.page}${h.modality}${(h.score * 100).toFixed(0)}% match`; const snippet = document.createElement("p"); snippet.className = "source-snippet"; snippet.textContent = h.text.length > 400 ? h.text.slice(0, 400) + "…" : h.text; body.appendChild(meta); body.appendChild(snippet); item.appendChild(body); sourcesList.appendChild(item); }); } async function ask(question) { errorEl.hidden = true; setLoading(true); try { const qEmb = await embedQuery(question); const scored = index.map((chunk) => ({ ...chunk, score: cos_sim(qEmb, chunk.embedding), })); scored.sort((a, b) => b.score - a.score); renderResults(scored.slice(0, 5)); result.hidden = false; } catch (err) { errorEl.textContent = "Search failed — check the console for details."; errorEl.hidden = false; console.error(err); } finally { setLoading(false); } } form.addEventListener("submit", (e) => { e.preventDefault(); const q = input.value.trim(); if (!q) return; ask(q); }); document.querySelectorAll(".chip").forEach((chip) => { chip.addEventListener("click", () => { const q = chip.dataset.q; input.value = q; ask(q); }); }); init();