const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => [...document.querySelectorAll(selector)];
const verdictClass = {
"SUPPORTED BY PROVIDED LABEL": "supported",
"CONTRADICTED BY PROVIDED LABEL": "contradicted",
"TECHNICALLY TRUE, CONTEXT MISSING": "context",
"CANNOT VERIFY": "unknown",
};
let currentAudit = null;
let feedbackVerdict = "";
function setMode(mode) {
$$(".mode-switch button").forEach((button) => button.classList.toggle("active", button.dataset.mode === mode));
$$(".mode-panel").forEach((panel) => panel.classList.toggle("active", panel.id === `mode-${mode}`));
}
$$(".mode-switch button").forEach((button) => button.addEventListener("click", () => setMode(button.dataset.mode)));
$$("[data-scroll]").forEach((button) => button.addEventListener("click", () => $(`#${button.dataset.scroll}`).scrollIntoView()));
const photoSets = { front: [], back: [] };
function preview(input, target, side) {
input.addEventListener("change", () => {
const incoming = [...input.files];
if (!incoming.length) return;
const available = Math.max(0, 6 - photoSets[side].length);
photoSets[side].push(...incoming.slice(0, available));
const file = photoSets[side][photoSets[side].length - 1];
target.style.backgroundImage = `url(${URL.createObjectURL(file)})`;
target.innerHTML = "";
$(`#${side}-count`).textContent = `${photoSets[side].length} photo${photoSets[side].length === 1 ? "" : "s"} · add more`;
input.value = "";
});
}
preview($("#front-file"), $("#front-preview"), "front");
preview($("#back-file"), $("#back-preview"), "back");
$("#clear-photos").addEventListener("click", () => {
photoSets.front.length = 0;
photoSets.back.length = 0;
[["front", "F"], ["back", "B"]].forEach(([side, label]) => {
$(`#${side}-preview`).style.backgroundImage = "";
$(`#${side}-preview`).innerHTML = `${label}`;
$(`#${side}-count`).textContent = "Add photos";
});
$("#ocr-status").textContent = "Selected photos cleared.";
});
async function runAudit(frontText, backText) {
const response = await fetch("api/audit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ front_text: frontText, back_text: backText }),
});
if (!response.ok) throw new Error("The evidence engine could not open this case.");
render(await response.json());
}
function evidenceHtml(item) {
return `
${escapeHtml(item.source)}${escapeHtml(item.text)}
`;
}
function escapeHtml(value = "") {
return value.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
}
function render(data) {
currentAudit = data;
$("#claim-count").textContent = data.claims.length;
$("#router-model").textContent = data.investigation.router_model;
$("#agent-steps").innerHTML = data.investigation.steps.map((step, index) => `
${String(index + 1).padStart(2, "0")}
${escapeHtml(step.tool.replaceAll("_", " "))}${escapeHtml(step.reason)}
${escapeHtml(step.source)} · ${escapeHtml(step.status)}
`).join("");
$("#stop-reason").textContent = data.investigation.stop_reason;
$("#missing-evidence").textContent = data.investigation.missing_evidence.length
? data.investigation.missing_evidence.join(" · ")
: "None. The required evidence path completed.";
const review = data.agent_review;
$("#nemotron-review").innerHTML = review.status === "NOT_REQUESTED"
? ""
: `NVIDIA NEMOTRON INDEPENDENT REVIEW · ${escapeHtml(review.status)}
${escapeHtml(review.priority || "No additional action requested.")}
${escapeHtml(review.evidence_request || review.rationale)}`;
$("#claim-grid").innerHTML = data.claims.length
? data.claims.map((claim) => `
${escapeHtml(claim.claim)}${escapeHtml(claim.verdict)}
Evidence confidence · ${escapeHtml(claim.confidence)}
${escapeHtml(claim.summary)}
${claim.evidence.map(evidenceHtml).join("")}
${claim.caveat ? `${escapeHtml(claim.caveat)}
` : ""}
`).join("")
: `No supported claim detectedTry High Protein, No Added Sugar, Multigrain, 100% Natural, FSSAI Approved, or No Preservatives.
`;
$("#gap-grid").innerHTML = data.persuasion_gap.length
? data.persuasion_gap.map((finding) => `
${escapeHtml(finding.severity)} context gap
${escapeHtml(finding.headline)}
FRONT IMPRESSION${escapeHtml(finding.front_impression)}
QUIET CONTEXT${escapeHtml(finding.quiet_context)}
${finding.evidence.map(evidenceHtml).join("")}
`).join("")
: `No persuasion gap calculatedMore complete nutrition, ingredient, or package-size evidence may be required.`;
const packet = data.whole_packet;
const facts = [
["Declared basis", data.nutrition.basis],
["Packet size", data.nutrition.package_size_g == null ? "Not found" : `${data.nutrition.package_size_g}g`],
["Whole-packet protein", packet.protein_g == null ? "Not calculable" : `${packet.protein_g}g`],
["Whole-packet total sugar", packet.total_sugar_g == null ? "Not calculable" : `${packet.total_sugar_g}g`],
["Sugar equivalent", packet.sugar_teaspoons == null ? "Not calculable" : `≈ ${packet.sugar_teaspoons} tsp`],
["Whole-packet sodium", packet.sodium_mg == null ? "Not calculable" : `${packet.sodium_mg}mg`],
];
$("#nutrition-grid").innerHTML = facts.map(([key, value]) => `${key}${value}
`).join("");
$("#nutrition-explanation").textContent = packet.explanation;
$("#expiry-status").textContent = data.expiry.status;
$("#opening-status").textContent = data.expiry.after_opening_instruction
? `After opening: ${data.expiry.after_opening_instruction}`
: "No after-opening deadline found.";
$("#raw-json").textContent = JSON.stringify(data, null, 2);
$("#results").classList.remove("hidden");
$("#results").scrollIntoView({ behavior: "smooth" });
}
$$("[data-feedback]").forEach((button) => button.addEventListener("click", () => {
feedbackVerdict = button.dataset.feedback;
$$("[data-feedback]").forEach((choice) => choice.classList.toggle("active", choice === button));
}));
$("#submit-feedback").addEventListener("click", async () => {
if (!currentAudit || !feedbackVerdict) {
$("#feedback-status").textContent = "Run an audit and choose a review outcome first.";
return;
}
$("#feedback-status").textContent = "Packaging evidence review...";
const response = await fetch("api/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
verdict: feedbackVerdict,
correction: $("#feedback-correction").value,
audit: currentAudit,
}),
});
const result = await response.json();
$("#feedback-status").textContent = result.message;
});
$("#audit-text").addEventListener("click", async () => {
$("#audit-text").textContent = "Examining evidence...";
try { await runAudit($("#front-text").value, $("#back-text").value); }
catch (error) { alert(error.message); }
finally { $("#audit-text").innerHTML = "Put this packet on trial →"; }
});
$("#read-photos").addEventListener("click", async () => {
const form = new FormData();
photoSets.front.forEach((file) => form.append("fronts", file));
photoSets.back.forEach((file) => form.append("backs", file));
$("#ocr-status").textContent = `Reading ${photoSets.front.length + photoSets.back.length} packet photos...`;
try {
const response = await fetch("api/ocr", { method: "POST", body: form });
const result = await response.json();
$("#front-text").value = result.front.text;
$("#back-text").value = result.back.text;
$("#ocr-status").textContent = `${result.front.status} ${result.back.status} Review merged evidence before trial.`;
setMode("text");
} catch (error) { $("#ocr-status").textContent = "OCR failed. Paste the label text to continue."; }
});
async function loadSamples() {
const samples = await fetch("api/samples").then((response) => response.json());
$("#sample-grid").innerHTML = Object.entries(samples).map(([name, value]) => `
`).join("");
$$(".sample-card").forEach((button) => button.addEventListener("click", async () => {
const sample = samples[button.dataset.name];
$("#front-text").value = sample.front;
$("#back-text").value = sample.back;
await runAudit(sample.front, sample.back);
}));
$("#hero-sample").addEventListener("click", () => { setMode("samples"); $("#workspace").scrollIntoView(); });
}
loadSamples();
fetch("api/model").then((response) => response.json()).then((status) => {
$("#model-mode").textContent = status.enabled ? "MiniCPM-V evidence engine online" : "Deterministic evidence engine online";
}).catch(() => {});