Spaces:
Running
Running
| <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("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """) | |
| .replaceAll("'", "'"); | |
| } | |
| </script> | |
| </body> | |
| </html> | |