Falsify / static /app.js
Aaryan Kumar
deploy to hugging face
1605cbb
Raw
History Blame Contribute Delete
17.4 kB
/* ============================================================================
FALSIFY live UI — force-graph rendering, SSE stream, and the four signature
animations that make "revised, not forgotten" visible:
• strike — a refuted node flashes red and gains a dashed-red border
• sweep — invalidation greys downstream nodes with a staggered delay
• dissolve — a forgotten node shrinks to nothing, then is removed
• rise — a promoted / newly-added node pulses green and settles
State is never carried by color alone (green↔red are close under deuteranopia):
every node draws its truth-state color AND a text label AND — when refuted — a
dashed border, and every change is narrated in the revision log.
========================================================================== */
const API = window.__API_BASE__ || ""; // "" = same-origin (HF monolith)
const $ = (id) => document.getElementById(id);
const STATE = {
alive: { color: "#22c55e", tag: "alive" },
refuted: { color: "#ef4444", tag: "refuted" },
invalidated: { color: "#9ca3af", tag: "invalidated" },
superseded: { color: "#f59e0b", tag: "superseded" },
forgotten: { color: "#4b5563", tag: "forgotten" },
};
const colorOf = (s) => (STATE[s] || STATE.alive).color;
/* ------------------------------------------------------------------ graph state */
let Graph; // the force-graph instance
let nodes = []; // {id,label,type,state,color, _flash,_flashColor,_shrink,_born}
let links = []; // {source,target,relation, _pulse}
const byId = () => Object.fromEntries(nodes.map((n) => [n.id, n]));
/* animation timers tick every frame; the renderer reads them */
function tickAnimations() {
let alive = false;
for (const n of nodes) {
if (n._flash > 0) { n._flash = Math.max(0, n._flash - 0.018); alive = true; }
if (n._shrink > 0) { n._shrink = Math.min(1, n._shrink + 0.04); alive = true; }
if (n._born > 0) { n._born = Math.max(0, n._born - 0.02); alive = true; }
}
for (const l of links) {
if (l._pulse > 0) { l._pulse = Math.max(0, l._pulse - 0.03); alive = true; }
}
// drop fully-dissolved nodes
const gone = nodes.filter((n) => n._shrink >= 1).map((n) => n.id);
if (gone.length) removeNodes(gone);
if (alive && Graph) Graph.nodeRelSize(Graph.nodeRelSize()); // nudge a redraw
requestAnimationFrame(tickAnimations);
}
/* ------------------------------------------------------------------ rendering */
function drawNode(node, ctx, scale) {
const base = 6;
const grow = node._born > 0 ? 1 + node._born * 0.4 : 1; // "rise" scale-in
const shrink = node._shrink > 0 ? 1 - node._shrink : 1; // "dissolve" scale-out
const r = base * grow * shrink;
if (r <= 0.2) return;
const isRefuted = node.state === "refuted";
const alpha = shrink;
ctx.globalAlpha = alpha;
// soft state glow so the canvas feels alive
ctx.beginPath();
ctx.arc(node.x, node.y, r + 5, 0, 2 * Math.PI);
ctx.fillStyle = hexA(node.color, 0.12);
ctx.fill();
// node body
ctx.beginPath();
ctx.arc(node.x, node.y, r, 0, 2 * Math.PI);
ctx.fillStyle = node.color;
ctx.fill();
// refuted = dashed red ring (the second channel, so it's not color-only)
if (isRefuted) {
ctx.setLineDash([3, 2]);
ctx.strokeStyle = "#ef4444";
ctx.lineWidth = 1.6 / scale;
ctx.stroke();
ctx.setLineDash([]);
} else {
ctx.strokeStyle = "#0b1020";
ctx.lineWidth = 1.4 / scale;
ctx.stroke();
}
// flash ring (strike / state-change / rise)
if (node._flash > 0) {
ctx.beginPath();
ctx.arc(node.x, node.y, r + 4 + (1 - node._flash) * 8, 0, 2 * Math.PI);
ctx.strokeStyle = hexA(node._flashColor || node.color, node._flash);
ctx.lineWidth = 2.4 / scale;
ctx.stroke();
}
// label — always present (identity never color-alone)
const label = node.label.length > 34 ? node.label.slice(0, 31) + "…" : node.label;
const fs = Math.max(3.2, 11 / scale);
ctx.font = `${fs}px Inter, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillStyle = "#cbd5e1";
ctx.globalAlpha = alpha * 0.9;
ctx.fillText(label, node.x, node.y + r + 2);
ctx.globalAlpha = 1;
}
function drawLink(link, ctx, scale) {
const s = link.source, t = link.target;
if (!s || !t || typeof s !== "object") return;
ctx.beginPath();
ctx.moveTo(s.x, s.y);
ctx.lineTo(t.x, t.y);
if (link._pulse > 0) {
ctx.strokeStyle = hexA("#34d399", link._pulse);
ctx.lineWidth = 2.4 / scale;
} else {
ctx.strokeStyle = "rgba(148,163,184,.28)";
ctx.lineWidth = 1 / scale;
}
ctx.stroke();
}
function initGraph() {
Graph = ForceGraph()($("graph"))
.backgroundColor("rgba(0,0,0,0)")
.nodeRelSize(6)
.nodeCanvasObject(drawNode)
.nodePointerAreaPaint((node, color, ctx) => {
ctx.fillStyle = color; ctx.beginPath();
ctx.arc(node.x, node.y, 9, 0, 2 * Math.PI); ctx.fill();
})
.linkCanvasObject(drawLink)
.linkDirectionalArrowLength(2.5)
.onNodeHover((n) => ($("graph").style.cursor = n ? "pointer" : "default"))
.onNodeClick((n) => addMessage("system", nodeCard(n)))
.cooldownTicks(120)
.d3VelocityDecay(0.28);
sizeGraph();
window.addEventListener("resize", sizeGraph);
}
function sizeGraph() {
const el = $("graph-panel");
Graph.width(el.clientWidth).height(el.clientHeight);
}
/* ------------------------------------------------------------------ graph data ops */
function setGraph(g) {
const prev = byId();
nodes = g.nodes.map((n) => ({
...n,
_flash: prev[n.id] ? 0 : 0.9, // new nodes "rise"
_flashColor: "#22c55e",
_born: prev[n.id] ? 0 : 1,
_shrink: 0,
x: prev[n.id]?.x, y: prev[n.id]?.y, // keep positions stable across refetches
}));
const idset = new Set(nodes.map((n) => n.id));
links = g.edges
.filter((e) => idset.has(e.source) && idset.has(e.target))
.map((e) => ({ source: e.source, target: e.target, relation: e.relation, _pulse: 0 }));
Graph.graphData({ nodes, links });
updateBackendUI(g.backend, g.scenario);
}
function removeNodes(ids) {
const set = new Set(ids);
nodes = nodes.filter((n) => !set.has(n.id));
links = links.filter((l) => {
const s = typeof l.source === "object" ? l.source.id : l.source;
const t = typeof l.target === "object" ? l.target.id : l.target;
return !set.has(s) && !set.has(t);
});
Graph.graphData({ nodes, links });
}
async function refetchGraph() {
const g = await fetch(`${API}/api/graph`).then((r) => r.json());
setGraph(g);
}
/* ------------------------------------------------------------------ SSE events */
function connectSSE() {
const es = new EventSource(`${API}/api/events`);
es.onopen = () => setConn("live", "live");
es.onerror = () => { setConn("reconnecting", "reconnecting…"); };
es.onmessage = (e) => {
let ev; try { ev = JSON.parse(e.data); } catch { return; }
handleEvent(ev);
};
}
function handleEvent(ev) {
const map = byId();
if (ev.type === "graph_reset") {
refetchGraph();
} else if (ev.type === "node_state_changed") {
const n = map[ev.id];
if (!n) return;
n.state = ev.state;
n.color = colorOf(ev.state);
n._flash = 1.0;
n._flashColor = colorOf(ev.state);
// light up edges INTO this node so the cascade visibly travels
for (const l of links) {
const t = typeof l.target === "object" ? l.target.id : l.target;
if (t === ev.id) l._pulse = 1.0;
}
logEntry(ev.state, n.label, ev.id, whyFor(ev.state));
} else if (ev.type === "node_forgotten") {
const n = map[ev.id];
if (n) { n._shrink = 0.01; logEntry("forgotten", n.label, ev.id, whyFor("forgotten")); }
} else if (ev.type === "pipeline_step") {
addMessage("system", `<div class="msg-title">${escapeHtml(ev.step)}</div>${escapeHtml(ev.detail || "")}`);
}
}
const WHY = {
refuted: "contradicted by the incoming fact",
invalidated: "its critical support died",
superseded: "its evidence collapsed; a rival now leads",
forgotten: "orphaned — no live consumer (provenance kept)",
alive: "promoted — still standing",
};
const whyFor = (s) => WHY[s] || "";
/* ------------------------------------------------------------------ panels */
function addMessage(kind, html, warn) {
const div = document.createElement("div");
div.className = `msg ${kind}${warn ? " warn" : ""}`;
div.innerHTML = html;
$("messages").appendChild(div);
$("messages").scrollTop = $("messages").scrollHeight;
$("msg-count").textContent = $("messages").querySelectorAll(".msg").length + " msgs";
clearCoach();
}
function clearCoach() { const c = document.querySelector(".coach"); if (c) c.remove(); }
function nodeCard(n) {
return `<div class="msg-title">${escapeHtml(n.label)}</div>
<div class="msg-meta">${n.type} · <span style="color:${n.color}">${STATE[n.state]?.tag || n.state}</span> · ${n.id.slice(0,8)}</div>`;
}
function logEntry(state, label, id, why) {
const wrap = $("log");
const row = document.createElement("div");
row.className = "log-entry";
row.innerHTML = `<span class="log-dot" style="background:${colorOf(state)}"></span>
<div class="log-body">
<div class="lg-title">${escapeHtml(shortLabel(label))}
<span class="id">${state}</span></div>
<div class="lg-why">${escapeHtml(why)}</div>
</div>`;
wrap.prepend(row);
$("log-count").textContent = wrap.querySelectorAll(".log-entry").length + " events";
}
function renderScoreboard(sb) {
if (!sb) return;
$("sb-q").textContent = sb.question || "";
$("falsify-answer").textContent = sb.falsify_answer || "—";
$("falsify-support").textContent = (sb.falsify_support || []).length
? "supported by: " + sb.falsify_support.join(", ") : "";
$("rag-answer").textContent = sb.rag_answer || "—";
const ragCard = $("rag-card");
if (sb.stale) {
$("rag-note").textContent = "⚠ still cites a node FALSIFY refuted — a stale answer.";
ragCard.classList.add("stale");
ragCard.classList.remove("stale-flash"); void ragCard.offsetWidth;
ragCard.classList.add("stale-flash");
} else {
$("rag-note").textContent = sb.rag_citations?.length ? "cites: " + sb.rag_citations[0] : "";
ragCard.classList.remove("stale");
}
}
/* ------------------------------------------------------------------ actions */
async function sendMessage() {
const input = $("msg-input");
const msg = input.value.trim();
if (!msg) return;
addMessage("user", escapeHtml(msg));
input.value = ""; autoGrow();
$("send-btn").disabled = true;
try {
const res = await fetch(`${API}/api/chat`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: msg, demo: true }),
}).then((r) => r.json());
if (res.type === "answer") { renderScoreboard(res.data); addMessage("system", "Answered from the alive belief graph — see the scoreboard."); }
else if (res.type === "revision") { summarizeRevision(res.data); await afterRevision(); }
} catch (e) { addMessage("system", "Request failed: " + escapeHtml(String(e)), true); }
$("send-btn").disabled = false;
}
function summarizeRevision(d) {
if (!d.revised) { addMessage("system", "No contradiction found — the graph is unchanged."); return; }
const pill = (t, c) => `<span class="pill" style="background:${c}22;color:${c}">${t}</span>`;
addMessage("system",
`<div class="msg-title">Belief revised</div>
${pill(d.refuted.length + " refuted", "#ef4444")}
${pill(d.invalidated.length + " invalidated", "#9ca3af")}
${pill(d.forgotten.length + " forgotten", "#4b5563")}
<div class="msg-meta">epoch ${d.epoch} · provenance retained: ${d.retained_provenance.length}</div>`);
}
async function afterRevision() {
const sb = await fetch(`${API}/api/scoreboard`).then((r) => r.json());
renderScoreboard(sb);
}
async function runDemo() {
clearCoach();
addMessage("system", `<div class="msg-title">Running investigation…</div>watch the graph revise in real time.`);
const res = await fetch(`${API}/api/demo`, { method: "POST" }).then((r) => r.json());
summarizeRevision(res.report);
renderScoreboard(res.scoreboard);
}
async function runDiamond() {
clearCoach();
addMessage("system", `<div class="msg-title">Diamond scenario</div>K2 rests on two supports — watch it survive one blow, then fall to the second.`);
const res = await fetch(`${API}/api/diamond`, { method: "POST" }).then((r) => r.json());
addMessage("system", "Phase 1 done — K died, K2 survived. Phase 2 done — K2 collapsed.");
renderScoreboard(res.scoreboard);
}
async function setScenario(kind) {
const g = await fetch(`${API}/api/scenario`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kind }),
}).then((r) => r.json());
setGraph(g);
addMessage("system", `Loaded the ${kind} investigation — ${g.nodes.length} beliefs.`);
}
async function resetGraph() { const g = await fetch(`${API}/api/reset`, { method: "POST" }).then((r) => r.json()); setGraph(g); addMessage("system", "Graph reset."); }
async function verifyPersistence() {
addMessage("system", "Re-reading belief state from the on-disk graph store…");
const res = await fetch(`${API}/api/verify`).then((r) => r.json());
if (!res.persisted) { addMessage("system", "Could not read persisted state: " + escapeHtml(res.error || ""), true); return; }
const lines = Object.entries(res.summary || {})
.map(([t, c]) => `${t}: ${Object.entries(c).map(([s, n]) => `${n} ${s}`).join(", ")}`).join("<br>");
addMessage("system", `<div class="msg-title">✓ Beliefs are persisted on the graph</div>${lines}<div class="msg-meta">truth-state lives in storage, not in process memory.</div>`);
}
async function uploadFile(file) {
addMessage("user", `📄 ${escapeHtml(file.name)}`);
addMessage("system", "Ingesting into memory (add + cognify)…");
const fd = new FormData(); fd.append("file", file);
const res = await fetch(`${API}/api/upload`, { method: "POST", body: fd }).then((r) => r.json());
if (res.ok) { setGraph(res.graph); addMessage("system", "Ingested — new evidence is now in the graph."); }
else { addMessage("system", (res.error || "upload failed") + (res.hint ? `<div class="msg-meta">${res.hint}</div>` : ""), true); }
}
async function setBackend(mode) {
const res = await fetch(`${API}/api/mode`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode }),
}).then((r) => r.json());
updateBackendUI(res.backend);
addMessage("system", res.backend === "cloud"
? "Backend → Cognee Cloud. All memory ops now run on the tenant."
: "Backend → self-hosted (open source).");
}
/* ------------------------------------------------------------------ ui helpers */
function updateBackendUI(backend, scenario) {
if (backend) {
document.querySelectorAll("#backend-toggle button").forEach((b) =>
b.classList.toggle("on", b.dataset.mode === backend));
}
}
function setConn(cls, label) {
const el = $("conn"); el.className = "status-dot " + cls;
$("conn-label").textContent = label;
}
function autoGrow() { const t = $("msg-input"); t.style.height = "auto"; t.style.height = Math.min(120, t.scrollHeight) + "px"; }
function shortLabel(s) { return s.length > 40 ? s.slice(0, 37) + "…" : s; }
function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c])); }
function hexA(hex, a) {
const h = hex.replace("#", "");
const n = parseInt(h.length === 3 ? h.split("").map((x) => x + x).join("") : h, 16);
return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`;
}
/* ------------------------------------------------------------------ wire up */
function wire() {
$("send-btn").onclick = sendMessage;
$("msg-input").addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
$("msg-input").addEventListener("input", autoGrow);
$("btn-run").onclick = runDemo;
$("btn-simple").onclick = () => setScenario("simple");
$("btn-diamond").onclick = runDiamond;
$("btn-reset").onclick = resetGraph;
$("btn-verify").onclick = verifyPersistence;
document.querySelectorAll("#backend-toggle button").forEach((b) =>
(b.onclick = () => setBackend(b.dataset.mode)));
const dz = $("dropzone"), fi = $("file-input");
fi.onchange = () => fi.files[0] && uploadFile(fi.files[0]);
["dragover", "dragenter"].forEach((ev) => dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.add("drag"); }));
["dragleave", "drop"].forEach((ev) => dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.remove("drag"); }));
dz.addEventListener("drop", (e) => { const f = e.dataTransfer.files[0]; if (f) uploadFile(f); });
// coach mark
$("messages").innerHTML = `<div class="coach">Hit <b>▶ Run investigation</b> to watch a belief die — or drop a contradicting fact of your own.</div>`;
}
async function boot() {
wire();
initGraph();
requestAnimationFrame(tickAnimations);
connectSSE();
try { await refetchGraph(); } catch { addMessage("system", "Backend not reachable yet — retrying via events.", true); }
}
boot();