/* PlaigLab front-of-house: upload -> live investigation log -> dossier report */ "use strict"; const $ = (id) => document.getElementById(id); const views = { upload: $("view-upload"), progress: $("view-progress"), report: $("view-report") }; const caseNo = Math.random().toString(36).slice(2, 8).toUpperCase(); $("caseNo").textContent = `CASE Nº ${caseNo}`; function show(name) { Object.entries(views).forEach(([k, el]) => { el.hidden = k !== name; }); window.scrollTo({ top: 0, behavior: "smooth" }); } /* ---------------- upload wiring ---------------- */ const dz = $("dropzone"); const fileInput = $("fileInput"); dz.addEventListener("click", () => fileInput.click()); dz.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") fileInput.click(); }); ["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) submitFile(f); }); fileInput.addEventListener("change", () => { if (fileInput.files[0]) submitFile(fileInput.files[0]); }); const depthParam = () => ($("maxToggle") && $("maxToggle").checked ? "max" : $("deepToggle").checked ? "deep" : "standard"); $("sampleBtn").addEventListener("click", async () => { const r = await fetch(`/api/sample?depth=${depthParam()}`, { method: "POST" }); const j = await r.json(); beginInvestigation(j.job_id, "specimen_case.txt"); }); $("newCaseBtn").addEventListener("click", () => { fileInput.value = ""; show("upload"); }); /* ---------------- past-case history ---------------- */ $("historyBtn").addEventListener("click", async () => { const box = $("caseHistory"); if (!box.hidden) { box.hidden = true; return; } const r = await fetch("/api/cases"); const j = await r.json(); const cases = j.cases || []; box.hidden = false; box.innerHTML = cases.length ? "
no past cases yet
"; box.querySelectorAll(".hist-row").forEach(row => { row.addEventListener("click", async () => { const id = row.getAttribute("data-case"); const rep = await fetch(`/api/cases/${id}`); if (rep.ok) { currentJobId = id; renderReport(await rep.json()); } }); }); }); async function submitFile(file) { const ext = file.name.toLowerCase().split(".").pop(); if (!["pdf", "txt", "docx"].includes(ext)) { alert("Only PDF, DOCX or TXT evidence is admissible."); return; } const fd = new FormData(); fd.append("file", file); const r = await fetch(`/api/analyze?depth=${depthParam()}`, { method: "POST", body: fd }); if (!r.ok) { alert("Upload failed: " + (await r.text())); return; } const j = await r.json(); beginInvestigation(j.job_id, file.name); } /* ---------------- progress polling ---------------- */ const STAGE_KEYS = ["extract", "understood", "searching", "graph", "scraping", "comparison", "ai-writing", "report"]; const STAGE_MATCH = { extract: /extracting/, understood: /document understood/, searching: /searching|search wave|unique candidates/, graph: /citation graph|historical canon|literature space/, scraping: /deep-scraping|full text acquired/, comparison: /deep comparison/, "ai-writing": /ai-writing/, report: /report ready/, }; let pollTimer = null; let currentJobId = null; function beginInvestigation(jobId, filename) { currentJobId = jobId; $("investFile").textContent = `evidence: ${filename} · case ${caseNo}`; $("console").innerHTML = ""; document.querySelectorAll("#custody li").forEach(li => li.className = ""); show("progress"); let seen = 0; pollTimer = setInterval(async () => { const r = await fetch(`/api/jobs/${jobId}`); if (!r.ok) return; const j = await r.json(); const log = j.log || []; for (; seen < log.length; seen++) appendLog(log[seen]); markStages(log); if (j.status === "done") { clearInterval(pollTimer); const rep = await fetch(`/api/jobs/${jobId}/report`); renderReport(await rep.json()); } else if (j.status === "error") { clearInterval(pollTimer); appendLog("INVESTIGATION FAILED: " + j.error); } }, 900); } function appendLog(msg) { const div = document.createElement("div"); div.textContent = msg; const con = $("console"); con.appendChild(div); con.scrollTop = con.scrollHeight; } function markStages(log) { const joined = log.join("\n"); let lastHit = -1; STAGE_KEYS.forEach((k, i) => { if (STAGE_MATCH[k].test(joined)) lastHit = i; }); document.querySelectorAll("#custody li").forEach((li, i) => { li.className = i < lastHit ? "done" : i === lastHit ? "active" : ""; }); } /* ---------------- report rendering ---------------- */ const ARC_LEN = 251.3; function setDial(arcId, needleId, valId, value, dur = 1200) { const v = Math.max(0, Math.min(100, value)); $(arcId).style.strokeDashoffset = ARC_LEN * (1 - v / 100); $(needleId).style.transform = `rotate(${-90 + 1.8 * v}deg)`; const el = $(valId); const t0 = performance.now(); (function tick(t) { const p = Math.min(1, (t - t0) / dur); el.textContent = (v * (1 - Math.pow(1 - p, 3))).toFixed(1); if (p < 1) requestAnimationFrame(tick); })(t0); } function esc(s) { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } function renderReport(r) { show("report"); $("rptCase").textContent = `CASE ${caseNo}`; $("rptTitle").textContent = r.title; const yrs = r.literature_years ? ` · literature ${r.literature_years[0]}–${r.literature_years[1]}` : ""; $("rptMeta").textContent = `${r.filename} · ${r.words.toLocaleString()} words · ${r.sentences} sentences · ` + `${(r.depth || "standard").toUpperCase()} sweep · ${r.candidates_screened} works screened${yrs} · ${r.elapsed_seconds}s`; const stamp = $("stamp"); // verdict now comes from the recall-first engine (R1): borderline = REVIEW const verdictCls = { PLAGIARIZED: "", SUSPICIOUS: "warn", REVIEW: "warn", CLEAN: "ok" }; const stampText = r.verdict || "REVIEW"; $("stampText").textContent = stampText; stamp.className = "stamp " + (verdictCls[stampText] ?? "warn"); stamp.style.animation = "none"; void stamp.offsetWidth; // restart slam animation stamp.style.animation = ""; $("verdictBasis").innerHTML = (r.verdict_reasons || []).length ? "VERDICT BASIS — " + r.verdict_reasons.map(esc).join(" · ") : ""; /* obfuscation / evasion evidence */ const ob = r.obfuscation || {}; const evp = $("evasionPanel"); if ((ob.homoglyph_count || 0) > 0 || (ob.zero_width_count || 0) > 0) { evp.hidden = false; const ex = (ob.homoglyph_examples || []).slice(0, 6) .map(e => `${esc(e.codepoint)} ${esc(e.name)} → ${esc(e.folded_to)}`).join(" ");
evp.innerHTML =
`${ob.spoof_suspected ? "⚠ OBFUSCATION EVIDENCE — evasion attempt suspected"
: "obfuscation artifacts (minor)"}
${ob.homoglyph_count} homoglyph(s) · ${ob.zero_width_count} hidden character(s)
· ${ob.invisible_per_10k_chars}/10k chars invisible ${ex}`;
} else {
evp.hidden = true;
}
setDial("simArc", "simNeedle", "simVal", r.similarity_index);
setDial("aiArc", "aiNeedle", "aiVal", r.ai_score);
$("simSub").textContent = `${r.plagiarism_risk} RISK`;
$("aiSub").textContent = r.ai_band.replace(/_/g, " ").toUpperCase();
$("statStack").innerHTML = [
["sources matched", r.sources.length],
["full PDFs read", r.full_texts_read ?? r.sources.filter(s => s.deep_scraped).length],
["works screened", r.candidates_screened],
["via citation graph", r.graph_expanded ?? 0],
["via topic canon", r.topic_canon ?? 0],
["matched sentences", r.heatmap.filter(h => h.sim >= 0.55).length + " / " + r.sentences],
["semantic engine", r.semantic_enabled ? "ON (MiniLM)" : "OFF"],
["vault passages", (r.coverage && r.coverage.vault_passages) || 0],
].map(([k, v]) => `No matching sources surfaced from the open indexes.
"; r.sources.forEach((s, i) => { const card = document.createElement("div"); card.className = "source-card"; card.style.animationDelay = `${0.08 * i}s`; const authors = (s.authors || []).filter(Boolean).join(", "); const pairs = (s.top_pairs || []).map(p => `language-model detectors unavailable — stylometry only
"; const lm = r.ai_lm_metrics; $("lmMetrics").textContent = lm ? `binoculars ratio ${lm.binoculars} · perplexity ${lm.ppl} · ` + `top-10 token rate ${lm.gltr_top10} · ${lm.tokens_scored} tokens scored` : ""; const cf = r.ai_conformal; const fusionLabel = { meta_hc3: "HC3-calibrated meta-classifier", hand_lm: "hand-weighted (uncalibrated)", stylometry_only: "stylometry only" }; $("fusionNote").innerHTML = `fusion: ${esc(fusionLabel[r.ai_fusion] || r.ai_fusion || "—")}` + (cf ? ` · P(AI)=${cf.p_ai} · conformal band [${cf.abstain_lo}, ${cf.abstain_hi}]` + (cf.inconclusive ? ` · INCONCLUSIVE — abstaining` : "") : ""); /* optional LLM executive summary */ const ls = $("llmSummary"); if (r.llm_summary) { ls.hidden = false; ls.innerHTML = `LLM REVIEWER SUMMARY ${esc(r.llm_summary)}`; } else { ls.hidden = true; } /* data fraud panel */ const df = r.data_fraud; const fp = $("fraudPanel"); if (df && (df.suspicious || df.benford)) { fp.hidden = false; const b = df.benford; const bLine = b ? `Benford: ${b.n_numbers} numbers, conformity ${esc(b.conformity)} ` + `(MAD ${b.mad}, χ² ${b.chi_square}/${b.chi2_critical_p05})` : ""; const gLine = df.grim ? ` · GRIM: ${df.grim.count} impossible mean(s)` : ""; fp.className = "fraud-panel" + (df.suspicious ? " hot" : ""); fp.innerHTML = `${df.suspicious ? "⚠ DATA FABRICATION SIGNALS" : "data integrity check"}` + `${bLine}${gLine}` + (df.flags && df.flags.length ? df.flags.map(f => `no open-access full texts were retrievable for this case
"; const cands = cov.candidates || []; const ACCESS = { read_full_text: ["✓ full text read", "acc-read"], oa_available: ["open access (not read)", "acc-oa"], shadow_text: ["shadow text", "acc-oa"], metadata_only: ["metadata only · paywalled?", "acc-meta"], }; $("covTable").innerHTML = "