File size: 3,711 Bytes
82ece55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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 = `<span class="source-page">PAGE ${h.page}</span><span class="source-modality">${h.modality}</span><span class="source-score">${(h.score * 100).toFixed(0)}% match</span>`;

    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();