Bitcheck-image / test-client.html
BitCheck Codex
feat: add browser test console for image verification
cfbe21c
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>BitCheck Test Console</title>
<style>
:root {
color-scheme: light;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f5f7fb;
color: #161b26;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
}
main {
width: min(1180px, calc(100% - 32px));
margin: 0 auto;
padding: 28px 0;
}
header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: end;
margin-bottom: 18px;
}
h1,
h2 {
margin: 0;
line-height: 1.2;
}
h1 {
font-size: 28px;
}
h2 {
font-size: 16px;
}
p {
margin: 6px 0 0;
color: #5f6b7a;
}
.layout {
display: grid;
grid-template-columns: 390px 1fr;
gap: 18px;
align-items: start;
}
.panel {
background: #ffffff;
border: 1px solid #dce3ee;
border-radius: 8px;
padding: 18px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}
label {
display: block;
margin: 14px 0 6px;
font-size: 13px;
font-weight: 750;
color: #263246;
}
input,
button {
width: 100%;
min-height: 42px;
border-radius: 6px;
font: inherit;
}
input {
border: 1px solid #c8d1df;
padding: 9px 11px;
color: #161b26;
background: #ffffff;
}
input[type="file"] {
padding: 8px;
}
button {
border: 0;
background: #165dff;
color: #ffffff;
font-weight: 800;
cursor: pointer;
}
button.secondary {
background: #edf2f7;
color: #253044;
border: 1px solid #cfd8e5;
}
button:disabled {
cursor: not-allowed;
background: #9aa8bd;
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 16px;
}
.toggles {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-top: 10px;
}
.toggle {
display: flex;
align-items: center;
gap: 8px;
min-height: 38px;
padding: 8px 10px;
border: 1px solid #d7deea;
border-radius: 6px;
background: #fbfcfe;
font-size: 13px;
font-weight: 700;
}
.toggle input {
width: 16px;
min-height: 16px;
margin: 0;
}
.preview {
width: 100%;
aspect-ratio: 4 / 3;
margin-top: 14px;
border: 1px dashed #aeb8c8;
border-radius: 8px;
display: grid;
place-items: center;
overflow: hidden;
color: #677386;
background: #f9fbfe;
}
.preview img {
width: 100%;
height: 100%;
object-fit: contain;
}
.status {
min-height: 22px;
margin-top: 14px;
font-size: 14px;
font-weight: 800;
}
.status.ok {
color: #0f7a4b;
}
.status.error {
color: #bd271e;
}
.summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin: 14px 0;
}
.metric {
border: 1px solid #e1e7f0;
border-radius: 8px;
padding: 12px;
background: #fbfcfe;
}
.metric span {
display: block;
color: #637083;
font-size: 12px;
font-weight: 800;
}
.metric strong {
display: block;
margin-top: 4px;
font-size: 17px;
line-height: 1.25;
overflow-wrap: anywhere;
}
.links {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.links a {
display: block;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid #ccd6e4;
color: #1247a7;
text-decoration: none;
font-weight: 800;
overflow-wrap: anywhere;
background: #f8fbff;
}
pre {
margin: 0;
max-height: 620px;
overflow: auto;
padding: 14px;
border-radius: 8px;
background: #101828;
color: #d1fadf;
font-size: 13px;
line-height: 1.45;
}
@media (max-width: 900px) {
main {
width: min(100% - 24px, 680px);
padding-top: 20px;
}
header,
.layout {
display: block;
}
.panel + .panel {
margin-top: 14px;
}
.summary,
.links {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
</head>
<body>
<main>
<header>
<div>
<h1>BitCheck Test Console</h1>
<p>Upload an image, run the verification API, and inspect the report.</p>
</div>
<button class="secondary" id="healthButton" type="button">Check Health</button>
</header>
<div class="layout">
<section class="panel">
<h2>Request</h2>
<form id="verifyForm">
<label for="apiBase">API base URL</label>
<input id="apiBase" value="http://127.0.0.1:7860" />
<label for="userEmail">User email</label>
<input id="userEmail" type="email" placeholder="user@example.com" required />
<label for="imageFile">Image file</label>
<input id="imageFile" type="file" accept="image/jpeg,image/png,image/webp" required />
<label>Modules</label>
<div class="toggles">
<span class="toggle"><input id="runExplainability" type="checkbox" checked /> Grad-CAM</span>
<span class="toggle"><input id="runOcr" type="checkbox" checked /> OCR</span>
<span class="toggle"><input id="runForensics" type="checkbox" checked /> Forensics</span>
<span class="toggle"><input id="runC2pa" type="checkbox" checked /> C2PA</span>
</div>
<label for="threshold">Classifier threshold</label>
<input id="threshold" type="number" min="0" max="1" step="0.01" placeholder="optional" />
<div class="preview" id="preview">No image selected</div>
<div class="actions">
<button id="submitButton" type="submit">Verify</button>
<button class="secondary" id="reportsButton" type="button">User Reports</button>
</div>
<div class="status" id="status"></div>
</form>
</section>
<section class="panel">
<h2>Response</h2>
<div class="summary" id="summary"></div>
<div class="links" id="outputLinks"></div>
<pre id="output">{}</pre>
</section>
</div>
</main>
<script>
const form = document.getElementById("verifyForm");
const apiBase = document.getElementById("apiBase");
const userEmail = document.getElementById("userEmail");
const imageFile = document.getElementById("imageFile");
const preview = document.getElementById("preview");
const statusEl = document.getElementById("status");
const output = document.getElementById("output");
const outputLinks = document.getElementById("outputLinks");
const summary = document.getElementById("summary");
const submitButton = document.getElementById("submitButton");
const healthButton = document.getElementById("healthButton");
const reportsButton = document.getElementById("reportsButton");
const toggles = {
run_explainability: document.getElementById("runExplainability"),
run_ocr: document.getElementById("runOcr"),
run_forensics: document.getElementById("runForensics"),
run_c2pa: document.getElementById("runC2pa"),
};
renderSummary();
imageFile.addEventListener("change", () => {
const file = imageFile.files[0];
if (!file) {
preview.textContent = "No image selected";
return;
}
const img = document.createElement("img");
img.alt = file.name;
img.src = URL.createObjectURL(file);
img.onload = () => URL.revokeObjectURL(img.src);
preview.replaceChildren(img);
});
form.addEventListener("submit", async (event) => {
event.preventDefault();
const file = imageFile.files[0];
if (!file) {
setStatus("Choose an image first.", "error");
return;
}
const body = new FormData();
body.append("user_email", userEmail.value.trim());
body.append("file", file);
for (const [name, input] of Object.entries(toggles)) {
body.append(name, input.checked ? "true" : "false");
}
if (document.getElementById("threshold").value.trim()) {
body.append("threshold", document.getElementById("threshold").value.trim());
}
await requestJson("/verify/image", {
method: "POST",
body,
busyLabel: "Verifying image...",
button: submitButton,
});
});
healthButton.addEventListener("click", async () => {
await requestJson("/health", { method: "GET", busyLabel: "Checking health...", button: healthButton });
});
reportsButton.addEventListener("click", async () => {
const email = userEmail.value.trim();
if (!email) {
setStatus("Enter a user email first.", "error");
return;
}
await requestJson(`/reports?user_email=${encodeURIComponent(email)}`, {
method: "GET",
busyLabel: "Loading user reports...",
button: reportsButton,
});
});
async function requestJson(path, options) {
setStatus(options.busyLabel, "");
options.button.disabled = true;
output.textContent = "{}";
outputLinks.replaceChildren();
try {
const base = apiBase.value.trim().replace(/\/$/, "");
const response = await fetch(`${base}${path}`, { method: options.method, body: options.body });
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
renderSummary(data);
renderLinks(data, base);
if (!response.ok) {
setStatus(`Request failed: HTTP ${response.status}`, "error");
return;
}
setStatus("Done.", "ok");
} catch (error) {
setStatus(error.message || "Request failed.", "error");
output.textContent = JSON.stringify({ error: String(error) }, null, 2);
renderSummary();
} finally {
options.button.disabled = false;
}
}
function renderSummary(data = null) {
const trust = data?.trust;
const classifier = data?.classifier;
const values = [
["Status", data?.status || data?.status === "ok" ? data.status : "-"],
["Trust", trust ? `${trust.trust_score}/100` : "-"],
["Risk", trust?.risk_level || "-"],
["Decision", trust?.decision || "-"],
["Classifier", classifier?.predicted_label || "-"],
["AI prob", numberOrDash(classifier?.ai_generated_probability)],
["OCR", data?.visible_watermark_ocr?.ocr_available === false ? "unavailable" : data?.visible_watermark_ocr?.found ? "found" : "-"],
["Owner", data?.user_email || "-"],
];
summary.replaceChildren(
...values.map(([label, value]) => {
const card = document.createElement("div");
card.className = "metric";
card.innerHTML = `<span>${label}</span><strong>${escapeHtml(String(value))}</strong>`;
return card;
}),
);
}
function renderLinks(data, base) {
const links = [];
const explainability = data?.explainability || {};
const forensics = data?.forensics || {};
addLink(links, "Grad-CAM overlay", explainability.overlay_url);
addLink(links, "Grad-CAM boxes", explainability.boxed_image_url);
addLink(links, "Forensic image", forensics.annotated_image_url);
if (data?.verification_id) {
addLink(links, "Saved report", `/reports/${data.verification_id}`);
}
outputLinks.replaceChildren(
...links.map(({ label, url }) => {
const a = document.createElement("a");
a.href = `${base}${url}`;
a.target = "_blank";
a.rel = "noreferrer";
a.textContent = label;
return a;
}),
);
}
function addLink(links, label, url) {
if (url) links.push({ label, url });
}
function numberOrDash(value) {
return typeof value === "number" ? value.toFixed(3) : "-";
}
function setStatus(message, type) {
statusEl.textContent = message;
statusEl.className = `status ${type || ""}`;
}
function escapeHtml(value) {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
</script>
</body>
</html>