Synthesis

Ask a question — get curated stories back. Powered by the public /v1/search API.

⚙ connection
` sequence smuggled inside a rendered string. Corpus text is scraped from the // open web — an attacker controls titles/summaries — so this is the primary XSS defence; the CSP header on // this page (deploy/nginx) is the second layer. Every corpus field reaches innerHTML through here. const esc = (s) => String(s ?? "").replace(/[&<>"'/]/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'","/":"/" }[c])); // esc() neutralizes HTML metacharacters; it says nothing about a URL's SCHEME. Article urls come from // SCRAPED pages, so a `javascript:` (or `data:text/html`) url in the corpus becomes a one-click script // execution on this page — which keeps your API key in localStorage. Allow the two schemes a link can // legitimately have; anything else renders as plain text instead of a link. const safeUrl = (u) => (/^https?:\/\//i.test(String(u ?? "").trim()) ? String(u).trim() : ""); const pill = (t, cls = "") => t ? `${esc(t)}` : ""; const span = (v) => (typeof v === "string" ? v : (v && typeof v === "object" && v.from) ? `${v.from} – ${v.to || "now"}` : ""); const art = (a) => { const title = a.title || a.summary || a.url || "(untitled)"; const href = safeUrl(a.url); const link = href ? `${esc(title)}` : esc(title); const menu = [a.source, a.published_at].filter(Boolean).map(esc).join(" · "); return `
  • ${link}${menu ? ` — ${menu}` : ""}
  • `; }; // The RAW response, always rendered alongside the cards — so "10 results" is never a black box and an // EMPTY page still shows you exactly what came back (the whole point of a debug console). // `config_version` names the published cockpit config that produced this response (null = none was ever // published, so the frozen server defaults ran). Surfaced in the summary because "why did I get this?" // is the question a debug console exists to answer. const cfgLabel = (json) => { if (!json || !("config_version" in json)) return ""; return json.config_version ? ` · config ${esc(String(json.config_version).slice(11, 16))}` : ` · config: server defaults (none published)`; }; const rawBlock = (json) => `
    ⚙ raw JSON — exactly what /v1/search returned${cfgLabel(json)}
    ${esc(JSON.stringify(json, null, 2))}
    `; const render = (results, raw) => { const dbg = raw ? rawBlock(raw) : ""; if (!results.length) { els.results.innerHTML = dbg + `

    No stories matched. Try a broader question or a longer period.

    `; return; } const cards = results.map((r) => { const isStory = Array.isArray(r.items); // With enrichment OFF there is no llm_title, and a story whose cluster has no title falls back in // enrichedOutput to the raw cluster_id ("clu_00QBYK") — not a headline. Prefer the best article's title. const raw = r.llm_title || r.title; const heading = (raw && !/^clu_/.test(raw)) ? raw : ((isStory && r.items && r.items[0] && r.items[0].title) || raw || (isStory ? "(story)" : "(article)")); const url = isStory ? "" : safeUrl(r.url); const h2 = url ? `${esc(heading)}` : esc(heading); const tags = isStory ? [ pill(r.type), pill(r.subtype), pill(r.verdict, r.verdict === "incoherent" ? "verdict-incoherent" : ""), ].join("") : ""; const summary = r.llm_summary || r.summary || ""; const metaBits = isStory ? [ r.source_count != null ? `${r.source_count} source${r.source_count === 1 ? "" : "s"}` : "", span(r.story_span) ] : [ r.source, r.published_at ]; const meta = metaBits.filter(Boolean).map(esc).join(" · "); const articles = isStory && r.items.length ? `` : ""; return `

    ${h2}

    ${tags ? `
    ${tags}
    ` : ""} ${summary ? `

    ${esc(summary)}

    ` : ""} ${meta ? `
    ${meta}
    ` : ""} ${articles}
    `; }).join(""); els.results.innerHTML = dbg + `
    ${countLabel(results)}
    ${cards}`; }; // THE COUNT IS ARTICLES. Every article, whether it came back on its own or inside a cluster — one `uid`, // one article, one unit of the count. A CLUSTER IS NOT A RESULT and is never the headline number: the old // "30 results" counted cards (a 25-article story counted once), which is why it sat above 55 `uid`s in the // raw JSON with nothing explaining the gap. Same unit as lib/labBatch.articlesIn — the app's own definition // (`a lead = an article`) and the unit the rerank page budget `maxArticles` caps. The grouping is still // WORTH knowing, so it rides as a secondary breakdown; it is never what "N" means. const countLabel = (results) => { const n = (k, one, many) => `${k.toLocaleString()} ${k === 1 ? one : many}`; const stories = results.filter((r) => Array.isArray(r.items)).length; const singles = results.length - stories; const articles = results.reduce((k, r) => k + (Array.isArray(r.items) ? r.items.length : 1), 0); const grouping = [ stories ? n(stories, "story", "stories") : "", singles ? n(singles, "individual", "individual") : "", ].filter(Boolean).join(" + "); return `${n(articles, "article", "articles")}${grouping ? ` · ${grouping}` : ""}`; }; const showError = (msg, rid, raw) => { els.results.innerHTML = `
    ${esc(msg)}
    ${rid ? `
    request id: ${esc(rid)}
    ` : ""}${raw ? rawBlock(raw) : ""}`; }; // ── run ───────────────────────────────────────────────────────────────── let running = false, ctrl = null; async function run() { if (running) { if (ctrl) ctrl.abort(); return; } // a second click STOPS waiting — never trapped behind a long run const question = els.q.value.trim(); if (!question) { els.q.focus(); return; } const key = els.key.value.trim(); if (!key) { els.conn.open = true; els.key.focus(); showError("Add your API key under ⚙ connection first."); return; } const body = { question }; if (els.task.value.trim()) body.task = els.task.value.trim(); if (els.context.value.trim()) body.context = els.context.value.trim(); if (els.requirements.value.trim()) body.requirements = els.requirements.value.trim(); if (els.period.value.trim()) body.period = els.period.value.trim(); if (els.max.value) body.max_results = Number(els.max.value); running = true; ctrl = new AbortController(); els.run.textContent = "Stop"; els.run.classList.add("stop"); els.results.innerHTML = ""; const t0 = Date.now(); const secs = () => Math.round((Date.now() - t0) / 1000); const tick = setInterval(() => { els.status.innerHTML = `searching… ${secs()}s — click Stop to stop waiting`; }, 250); let statusMsg = ""; try { const res = await fetch("/v1/search", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` }, body: JSON.stringify(body), signal: ctrl.signal, }); const json = await res.json().catch(() => ({})); if (!res.ok) { showError(json?.error?.message || `Request failed (HTTP ${res.status})`, json?.error?.request_id, json); return; } render(json.results || [], json); statusMsg = `done in ${secs()}s`; } catch (e) { // Aborting stops US waiting; the server finishes (or hits its own deadline) on its own. if (e && e.name === "AbortError") statusMsg = `stopped after ${secs()}s`; else showError(`Could not reach the API — is the app running? (${e && e.message ? e.message : e})`); } finally { clearInterval(tick); els.status.textContent = statusMsg; running = false; ctrl = null; els.run.textContent = "Search"; els.run.classList.remove("stop"); els.run.disabled = false; } } els.run.addEventListener("click", run); els.q.addEventListener("keydown", (e) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") run(); }); })();