(() => { "use strict"; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; /* ---------------- nav ---------------- */ const nav = document.querySelector(".nav"); if (nav) { window.addEventListener("scroll", () => { nav.classList.toggle("scrolled", window.scrollY > 8); }, { passive: true }); } const navToggle = document.querySelector(".nav-toggle"); const navLinks = document.querySelector(".nav-links"); if (navToggle) { navToggle.addEventListener("click", () => { const open = navLinks.style.display === "flex"; navLinks.style.display = open ? "none" : "flex"; navLinks.style.cssText += open ? "" : "position:fixed;top:62px;left:0;right:0;background:#0b0d12;flex-direction:column;padding:18px 28px;border-bottom:1px solid var(--border);gap:16px;"; }); } /* ---------------- scroll reveal ---------------- */ const revealEls = document.querySelectorAll("[data-reveal]"); if (reduceMotion) { revealEls.forEach(el => el.classList.add("in-view")); } else { const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add("in-view"); io.unobserve(e.target); } }); }, { threshold: 0.15 }); revealEls.forEach(el => io.observe(el)); } /* ---------------- pipeline sequential activation ---------------- */ const pipeStages = document.querySelectorAll(".pipe-stage"); if (pipeStages.length) { const pipeIO = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { pipeStages.forEach((stage, i) => { setTimeout(() => stage.classList.add("active"), reduceMotion ? 0 : i * 260); }); pipeIO.disconnect(); } }); }, { threshold: 0.4 }); pipeIO.observe(document.querySelector(".pipeline")); } /* ---------------- diff console (signature element) ---------------- */ function buildConsoleLines(diff) { const lines = []; diff.added_entities.forEach(e => lines.push({ type: "add", tag: "+", body: `entity ${e}` })); diff.removed_entities.forEach(e => lines.push({ type: "rem", tag: "-", body: `entity ${e}` })); diff.added_relations.forEach(([s, p, o]) => lines.push({ type: "add", tag: "+", body: `relation ${s} --${p}--> ${o}` })); diff.removed_relations.forEach(([s, p, o]) => lines.push({ type: "rem", tag: "-", body: `relation ${s} --${p}--> ${o}` })); lines.push({ type: "info", tag: "\u2713", body: `diff complete \u2014 ${diff.added_entities.length + diff.added_relations.length} added \u00b7 ${diff.removed_entities.length + diff.removed_relations.length} removed \u00b7 ${diff.unchanged_entities.length + diff.unchanged_relations.length} unchanged` }); return lines; } function playConsole(el, lines, { loop = false, lineDelay = 110, restartDelay = 4200 } = {}) { let timers = []; function clearTimers() { timers.forEach(t => clearTimeout(t)); timers = []; } function run() { clearTimers(); el.innerHTML = ""; lines.forEach((ln, i) => { const t = setTimeout(() => { const row = document.createElement("div"); row.className = `console-line ${ln.type}`; row.innerHTML = `${ln.tag}`; row.querySelector(".body").textContent = ln.body; el.appendChild(row); }, reduceMotion ? 0 : i * lineDelay); timers.push(t); }); const cursor = document.createElement("span"); cursor.className = "cursor"; const t2 = setTimeout(() => el.appendChild(cursor), reduceMotion ? 0 : lines.length * lineDelay + 100); timers.push(t2); if (loop && !reduceMotion) { const t3 = setTimeout(run, lines.length * lineDelay + restartDelay); timers.push(t3); } } run(); return clearTimers; } const heroConsole = document.getElementById("hero-console"); if (heroConsole) { playConsole(heroConsole, buildConsoleLines(DEMO_FIXTURE.diff), { loop: true, lineDelay: 95, restartDelay: 3600 }); } /* ---------------- D3 force graph ---------------- */ function colorForNode(id, diff) { if (diff.added_entities.includes(id)) return "var(--add)"; if (diff.removed_entities.includes(id)) return "var(--rem)"; return "var(--neu)"; } function tripleKey(s, p, o) { return `${s}\u0001${p}\u0001${o}`; } function colorForEdge(link, diff) { const k = tripleKey(link.source.id || link.source, link.label, link.target.id || link.target); const addedSet = new Set(diff.added_relations.map(([s, p, o]) => tripleKey(s, p, o))); const removedSet = new Set(diff.removed_relations.map(([s, p, o]) => tripleKey(s, p, o))); if (addedSet.has(k)) return "var(--add)"; if (removedSet.has(k)) return "var(--rem)"; return "var(--edge)"; } function renderGraph(svgEl, graph, diff) { const svg = d3.select(svgEl); svg.selectAll("*").remove(); const bbox = svgEl.getBoundingClientRect(); const width = bbox.width || 460; const height = bbox.height || 300; svg.attr("viewBox", `0 0 ${width} ${height}`); if (!graph.nodes.length) { svg.append("text") .attr("x", width / 2).attr("y", height / 2) .attr("text-anchor", "middle") .attr("fill", "var(--text-faint)") .attr("font-family", "var(--mono)") .attr("font-size", 12) .text("no entities extracted"); return; } const nodes = graph.nodes.map(d => ({ ...d })); const links = graph.links.map(d => ({ ...d })); const defs = svg.append("defs"); ["add", "rem", "edge"].forEach(kind => { defs.append("marker") .attr("id", `arrow-${kind}-${svgEl.id}`) .attr("viewBox", "0 -5 10 10") .attr("refX", 19).attr("refY", 0) .attr("markerWidth", 6).attr("markerHeight", 6) .attr("orient", "auto") .append("path") .attr("d", "M0,-5L10,0L0,5") .attr("fill", `var(--${kind === "edge" ? "edge" : kind})`); }); const sim = d3.forceSimulation(nodes) .force("link", d3.forceLink(links).id(d => d.id).distance(78).strength(0.7)) .force("charge", d3.forceManyBody().strength(-220)) .force("center", d3.forceCenter(width / 2, height / 2)) .force("collide", d3.forceCollide(26)); const link = svg.append("g").selectAll("line") .data(links).join("line") .attr("stroke", d => colorForEdge(d, diff)) .attr("stroke-width", d => { const k = tripleKey(d.source.id || d.source, d.label, d.target.id || d.target); const changed = diff.added_relations.some(([s, p, o]) => tripleKey(s, p, o) === k) || diff.removed_relations.some(([s, p, o]) => tripleKey(s, p, o) === k); return changed ? 2.2 : 1.1; }) .attr("marker-end", d => { const k = tripleKey(d.source.id || d.source, d.label, d.target.id || d.target); if (diff.added_relations.some(([s, p, o]) => tripleKey(s, p, o) === k)) return `url(#arrow-add-${svgEl.id})`; if (diff.removed_relations.some(([s, p, o]) => tripleKey(s, p, o) === k)) return `url(#arrow-rem-${svgEl.id})`; return `url(#arrow-edge-${svgEl.id})`; }) .attr("opacity", 0.85); link.append("title").text(d => d.label); const node = svg.append("g").selectAll("circle") .data(nodes).join("circle") .attr("r", d => (diff.added_entities.includes(d.id) || diff.removed_entities.includes(d.id)) ? 9 : 7) .attr("fill", d => colorForNode(d.id, diff)) .attr("stroke", "var(--bg-card)") .attr("stroke-width", 2) .style("cursor", "grab") .call(d3.drag() .on("start", (event, d) => { if (!event.active) sim.alphaTarget(0.25).restart(); d.fx = d.x; d.fy = d.y; }) .on("drag", (event, d) => { d.fx = event.x; d.fy = event.y; }) .on("end", (event, d) => { if (!event.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })); node.append("title").text(d => d.id); const label = svg.append("g").selectAll("text") .data(nodes).join("text") .attr("class", "graph-node-label") .attr("text-anchor", "middle") .attr("dy", -12) .text(d => d.id.length > 16 ? d.id.slice(0, 15) + "\u2026" : d.id); sim.on("tick", () => { link .attr("x1", d => d.source.x).attr("y1", d => d.source.y) .attr("x2", d => d.target.x).attr("y2", d => d.target.y); node.attr("cx", d => d.x).attr("cy", d => d.y); label.attr("x", d => d.x).attr("y", d => d.y); }); if (reduceMotion) { for (let i = 0; i < 200; i++) sim.tick(); sim.stop(); } } /* ---------------- stat count-up ---------------- */ function countUp(el, target, duration = 700) { if (reduceMotion) { el.textContent = target; return; } const start = performance.now(); function frame(now) { const p = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - p, 3); el.textContent = Math.round(eased * target); if (p < 1) requestAnimationFrame(frame); } requestAnimationFrame(frame); } /* ---------------- summary typewriter ---------------- */ function typewrite(el, text, speed = 14) { el.textContent = ""; const cursor = document.createElement("span"); cursor.className = "typecursor"; if (reduceMotion) { el.textContent = text; return; } let i = 0; el.appendChild(cursor); function step() { if (i <= text.length) { el.textContent = text.slice(0, i); el.appendChild(cursor); i += 2; requestAnimationFrame(() => setTimeout(step, speed)); } else { cursor.remove(); } } step(); } /* ---------------- JSON pretty print with diff highlighting ---------------- */ function renderJson(diff) { const pretty = JSON.stringify(diff, null, 2); return pretty .replace(/"(added_\w+|removed_\w+|unchanged_\w+)":/g, '"$1":'); } /* ---------------- demo orchestration ---------------- */ const els = { v1: document.getElementById("input-v1"), v2: document.getElementById("input-v2"), apiKey: document.getElementById("input-apikey"), advToggle: document.getElementById("adv-toggle"), advPanel: document.getElementById("adv-panel"), runBtn: document.getElementById("run-diff-btn"), resetBtn: document.getElementById("reset-sample-btn"), status: document.getElementById("demo-status"), results: document.getElementById("demo-results"), console: document.getElementById("demo-console"), g1: document.getElementById("graph-v1"), g2: document.getElementById("graph-v2"), statAdd: document.getElementById("stat-add"), statRem: document.getElementById("stat-rem"), statAddRel: document.getElementById("stat-add-rel"), statRemRel: document.getElementById("stat-rem-rel"), summary: document.getElementById("summary-text"), json: document.getElementById("json-output"), }; if (els.v1) { els.v1.value = DEMO_FIXTURE.doc_v1; els.v2.value = DEMO_FIXTURE.doc_v2; } // Never let the browser silently restore a previously typed API key on // reload or back/forward navigation — it's never stored, but make that // visibly true too. if (els.apiKey) { els.apiKey.value = ""; window.addEventListener("pageshow", () => { els.apiKey.value = ""; }); } if (els.advToggle) { els.advToggle.addEventListener("click", () => { els.advPanel.classList.toggle("open"); els.advToggle.textContent = els.advPanel.classList.contains("open") ? "\u2212 use your own Groq API key" : "+ use your own Groq API key"; }); } if (els.resetBtn) { els.resetBtn.addEventListener("click", () => { els.v1.value = DEMO_FIXTURE.doc_v1; els.v2.value = DEMO_FIXTURE.doc_v2; }); } function setStatus(text, live) { els.status.querySelector(".status-text").textContent = text; els.status.querySelector(".status-dot").classList.toggle("live", !!live); } function renderResults(data) { els.results.classList.remove("hidden"); playConsole(els.console, buildConsoleLines(data.diff), { loop: false, lineDelay: 70 }); renderGraph(els.g1, data.graph_json.v1, data.diff); renderGraph(els.g2, data.graph_json.v2, data.diff); countUp(els.statAdd, data.diff.added_entities.length); countUp(els.statRem, data.diff.removed_entities.length); countUp(els.statAddRel, data.diff.added_relations.length); countUp(els.statRemRel, data.diff.removed_relations.length); typewrite(els.summary, data.summary); els.json.innerHTML = renderJson(data.diff); els.results.scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "nearest" }); } async function runDiff() { const v1 = els.v1.value.trim(); const v2 = els.v2.value.trim(); const key = els.apiKey ? els.apiKey.value.trim() : ""; els.runBtn.disabled = true; const originalLabel = els.runBtn.textContent; els.runBtn.textContent = "computing diff\u2026"; if (!key) { setStatus("bundled sample \u2014 add a Groq key above to run on your own text", false); renderResults(DEMO_FIXTURE); els.runBtn.disabled = false; els.runBtn.textContent = originalLabel; return; } try { setStatus("calling live backend\u2026", true); const res = await fetch("/api/diff", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ v1, v2, api_key: key }), }); const data = await res.json().catch(() => ({})); if (!res.ok || data.error) throw new Error(data.error || `backend responded ${res.status}`); setStatus("live result from your Flask backend + Groq", true); renderResults(data); } catch (err) { setStatus(`couldn't reach the live backend (${err.message}) \u2014 showing bundled sample instead`, false); renderResults(DEMO_FIXTURE); } finally { els.runBtn.disabled = false; els.runBtn.textContent = originalLabel; } } if (els.runBtn) els.runBtn.addEventListener("click", runDiff); // auto-run the bundled demo once, the first time the section scrolls into view const demoSection = document.getElementById("demo"); if (demoSection) { const demoIO = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { setStatus("bundled sample diff \u2014 computed from this repo's own data/doc_v1.txt & doc_v2.txt", false); renderResults(DEMO_FIXTURE); demoIO.disconnect(); } }); }, { threshold: 0.3 }); demoIO.observe(demoSection); } // re-render graphs cleanly on resize (debounced) let resizeT; window.addEventListener("resize", () => { clearTimeout(resizeT); resizeT = setTimeout(() => { if (els.results && !els.results.classList.contains("hidden")) { renderGraph(els.g1, DEMO_FIXTURE.graph_json.v1, DEMO_FIXTURE.diff); renderGraph(els.g2, DEMO_FIXTURE.graph_json.v2, DEMO_FIXTURE.diff); } }, 250); }); })();