Spaces:
Running on Zero
Running on Zero
Download static/app.js from RajTheBuilder/ReturnGuard: direct link, hf CLI and curl.
- Browser
- Download file 49.4 kB
-
https://huggingface.co/spaces/RajTheBuilder/ReturnGuard/resolve/main/static/app.js
- Command line
-
hf download hf://spaces/RajTheBuilder/ReturnGuard/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/RajTheBuilder/ReturnGuard/resolve/main/static/app.js
49.4 kB
| /* ReturnGuard UI: hash router, forms, live pipeline view, dashboard, evaluation, hero animation. */ | |
| ; | |
| const $ = (s, el = document) => el.querySelector(s); | |
| const $$ = (s, el = document) => [...el.querySelectorAll(s)]; | |
| const ANGLES = ['front', 'back', 'side']; | |
| const REDUCED_MOTION = window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); | |
| const pct = (x, d = 1) => (x == null || isNaN(x)) ? 'β' : (x * 100).toFixed(d) + '%'; | |
| const store = { | |
| get(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }, | |
| set(k, v) { try { localStorage.setItem(k, v); } catch (e) { /* storage unavailable */ } }, | |
| }; | |
| const VERDICT = { | |
| ACCEPT: 'Accepted', HUMAN_REVIEW: 'Human review', REJECT: 'Rejected', | |
| RETAKE_REQUIRED: 'Retake photos', ERROR: 'Error', PROCESSING: 'Processing', | |
| }; | |
| const FRAUD_LABEL = { | |
| substitution: 'Product substitution', label_removed: 'Label removed / unit swap', wardrobing: 'Wardrobing (worn and returned)', | |
| false_damage_claim: 'False damage claim', damage_after_delivery: 'Damaged after delivery', | |
| }; | |
| const NODES = [ | |
| ['intake', 'Intake', 'Background removal, quality gate, features'], | |
| ['policy', 'Policy agent', 'Return window and category rules'], | |
| ['history', 'Customer history agent', 'Prior returns set the cut-offs'], | |
| ['screen', 'Visual screen', 'Object-only DINOv2 + colour, no LLM'], | |
| ['triage', 'Triage router', 'Fast path, or call an agent?'], | |
| ['identity', 'Identity agent', 'Blind label read, compared in code', true], | |
| ['inspector', 'Visual inspector', 'Six photos, independent verdict', true], | |
| ['decide', 'Decision agent', 'Evidence-weighted risk score'], | |
| ]; | |
| /* ββ API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function api(url, opts) { | |
| const res = await fetch(url, opts); | |
| let data = {}; | |
| try { data = await res.json(); } catch (e) { /* non-JSON error page */ } | |
| if (!res.ok || data.success === false) throw new Error(data.error || `Request failed (${res.status})`); | |
| return data; | |
| } | |
| /* ββ toast / busy ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| let toastTimer; | |
| function toast(msg, type = '') { | |
| const t = $('#toast'); | |
| t.textContent = msg; | |
| t.className = `toast show ${type}`; | |
| clearTimeout(toastTimer); | |
| toastTimer = setTimeout(() => t.classList.remove('show'), 4500); | |
| } | |
| function busy(on, title = '', sub = '', steps = null) { | |
| $('#busy').classList.toggle('show', on); | |
| $('#busy-spinner').classList.toggle('hidden', on && !!steps); | |
| $('#busy-steps').classList.toggle('hidden', !(on && steps)); | |
| if (on) { $('#busy-title').textContent = title; $('#busy-sub').textContent = sub; } | |
| if (on && steps) { | |
| $('#busy-steps').innerHTML = steps.map(s => `<li data-step="${s.key}"><span class="ls-icon"></span><span class="ls-text">${esc(s.label)}<small class="ls-detail"></small></span></li>`).join(''); | |
| busyStep(steps[0].key); | |
| } | |
| } | |
| // Mark every step before `key` done and `key` itself active (with an optional detail like "front view (1/3)"). | |
| function busyStep(key, detail = '') { | |
| const items = $$('#busy-steps li'); | |
| const at = items.findIndex(li => li.dataset.step === key); | |
| if (at < 0) return; | |
| items.forEach((li, i) => { | |
| li.className = i < at ? 'done' : i === at ? 'active' : ''; | |
| $('.ls-detail', li).textContent = i === at && detail ? ` Β· ${detail}` : ''; | |
| }); | |
| } | |
| const DELIVERY_STEPS = [ | |
| { key: 'upload', label: 'Uploading photos' }, | |
| { key: 'background', label: 'Removing backgrounds' }, | |
| { key: 'quality', label: 'Checking photo quality' }, | |
| { key: 'embeddings', label: 'Computing DINOv2 embeddings and colour' }, | |
| { key: 'save', label: 'Saving the order' }, | |
| { key: 'labels', label: 'Gemini reading labels and serials' }, | |
| ]; | |
| const newJobId = () => (crypto.randomUUID ? crypto.randomUUID() : `job-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`); | |
| /* ββ theme βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function initTheme() { | |
| const apply = t => { | |
| document.documentElement.setAttribute('data-theme', t); | |
| $('.i-sun').classList.toggle('hidden', t === 'light'); | |
| $('.i-moon').classList.toggle('hidden', t !== 'light'); | |
| }; | |
| apply(document.documentElement.getAttribute('data-theme') || 'dark'); | |
| $('#theme-toggle').addEventListener('click', () => { | |
| const next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light'; | |
| apply(next); | |
| store.set('rg-theme', next); | |
| }); | |
| } | |
| /* ββ router ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const PAGES = ['home', 'deliver', 'return', 'result', 'dashboard', 'evaluation', 'architecture', 'about', 'account']; | |
| let pollTimer = null; | |
| let lastPage = null; | |
| function route() { | |
| const [page, arg] = location.hash.replace(/^#/, '').split('/'); | |
| const name = PAGES.includes(page) ? page : 'home'; | |
| // after signing in, go back to the page that asked for it | |
| if (name === 'account' && ['deliver', 'return', 'dashboard'].includes(lastPage)) store.set('rg-after-signin', `#${lastPage}`); | |
| lastPage = name; | |
| $$('.page').forEach(p => p.classList.toggle('active', p.id === `page-${name}`)); | |
| $$('[data-nav]').forEach(a => a.classList.toggle('active', a.dataset.nav === (name === 'result' ? 'return' : name))); | |
| $('#mobile-nav').classList.remove('open'); | |
| window.scrollTo(0, 0); | |
| clearTimeout(pollTimer); | |
| scene.setVisible(name === 'home'); | |
| if (name === 'dashboard') loadDashboard(); | |
| if (name === 'evaluation') loadEvaluation(); | |
| if (name === 'return') prefillReturn(); | |
| if (name === 'deliver' || name === 'return') refreshMe(); | |
| if (name === 'account') loadAccount(); | |
| if (name === 'result' && /^\d+$/.test(arg || '')) openResult(Number(arg)); | |
| } | |
| /* ββ uploads & samples βββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function setPreview(zone, file) { | |
| if (!file || !file.type.startsWith('image/')) return; | |
| zone.querySelector('img.preview')?.remove(); | |
| zone.querySelector('.u-tag')?.remove(); | |
| const img = document.createElement('img'); | |
| img.className = 'preview'; | |
| img.alt = ''; | |
| img.src = URL.createObjectURL(file); | |
| zone.prepend(img); | |
| const tag = document.createElement('span'); | |
| tag.className = 'u-tag'; | |
| tag.textContent = zone.querySelector('.u-label').textContent; | |
| zone.append(tag); | |
| zone.classList.add('has-file'); | |
| } | |
| function clearPreviews(form) { | |
| $$('.upload', form).forEach(z => { z.classList.remove('has-file'); z.querySelector('img.preview')?.remove(); z.querySelector('.u-tag')?.remove(); }); | |
| } | |
| function initUploads() { | |
| $$('.upload').forEach(zone => { | |
| const input = $('input', zone); | |
| input.addEventListener('change', () => setPreview(zone, input.files[0])); | |
| zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); }); | |
| zone.addEventListener('dragleave', () => zone.classList.remove('drag')); | |
| zone.addEventListener('drop', e => { | |
| e.preventDefault(); | |
| zone.classList.remove('drag'); | |
| const f = e.dataTransfer.files[0]; | |
| if (!f) return; | |
| const dt = new DataTransfer(); | |
| dt.items.add(f); | |
| input.files = dt.files; | |
| setPreview(zone, f); | |
| }); | |
| }); | |
| } | |
| async function loadSample(set, formId) { | |
| const form = document.getElementById(formId); | |
| try { | |
| for (const a of ANGLES) { | |
| const blob = await (await fetch(`/static/samples/${set}_${a}.jpg`)).blob(); | |
| const file = new File([blob], `${set}_${a}.jpg`, { type: 'image/jpeg' }); | |
| const input = $(`input[name="${a}_image"]`, form); | |
| const dt = new DataTransfer(); | |
| dt.items.add(file); | |
| input.files = dt.files; | |
| setPreview(input.closest('.upload'), file); | |
| } | |
| if (formId === 'delivery-form') { | |
| $('#d-product').value = 'Portronics SoundDrum 1 speaker (blue)'; | |
| $('#d-category').value = 'electronics'; | |
| } | |
| toast('Sample photos loaded. Submit when ready.', 'success'); | |
| } catch (e) { | |
| toast('Could not load the sample photos.', 'error'); | |
| } | |
| } | |
| function initSamples() { | |
| $$('[data-load]').forEach(b => b.addEventListener('click', e => { e.preventDefault(); loadSample(b.dataset.load, b.dataset.form); })); | |
| $$('[data-sample]').forEach(a => a.addEventListener('click', () => setTimeout(() => loadSample(a.dataset.sample, 'delivery-form'), 60))); | |
| } | |
| /* ββ delivery ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function initDelivery() { | |
| const form = $('#delivery-form'); | |
| form.addEventListener('submit', async e => { | |
| e.preventDefault(); | |
| if (ANGLES.some(a => !$(`input[name="${a}_image"]`, form).files.length)) return toast('Add all three photos (front, back, side).', 'error'); | |
| busy(true, 'Registering delivery', 'Live progress from the server:', DELIVERY_STEPS); | |
| const jobId = newJobId(); | |
| const body = new FormData(form); | |
| body.append('job_id', jobId); | |
| let polling = true; | |
| (async function poll() { | |
| while (polling) { | |
| try { | |
| const p = await api(`/api/deliveries/progress/${jobId}`); | |
| if (polling && p.stage) busyStep(p.stage, p.detail); | |
| } catch (e) { /* progress is best effort */ } | |
| await new Promise(r => setTimeout(r, 450)); | |
| } | |
| })(); | |
| try { | |
| const data = await api('/api/deliveries', { method: 'POST', body }); | |
| polling = false; | |
| $$('#busy-steps li').forEach(li => { li.className = 'done'; }); | |
| store.set('rg-last-order', data.order.order_id); | |
| renderDelivery(data.order, data.images); | |
| clearPreviews(form); | |
| form.reset(); | |
| refreshMe(); | |
| toast(`Delivery registered as order #${data.order.order_id}`, 'success'); | |
| } catch (err) { | |
| toast(err.message, 'error'); | |
| } finally { | |
| polling = false; | |
| busy(false); | |
| } | |
| }); | |
| } | |
| function renderDelivery(o, images) { | |
| const fp = o.fingerprint; | |
| const ids = fp?.identifiers?.length | |
| ? fp.identifiers.map(i => `<span class="chip ${['serial_number', 'batch_number', 'barcode'].includes(i.kind) ? 'unit' : ''}" title="${esc(i.kind)} Β· ${esc(i.location)}">${esc(i.text)}</span>`).join('') | |
| : '<span class="dim">None visible</span>'; | |
| const list = arr => arr?.length ? arr.map(x => esc(typeof x === 'string' ? x : `${x.description} (${x.view})`)).join('; ') : '<span class="dim">None</span>'; | |
| $('#delivery-result').innerHTML = ` | |
| <div class="card-title">Delivery fingerprint saved</div> | |
| <div class="order-badge"><span class="num mono">#${o.order_id}</span><span class="muted">order ID Β· ${esc(o.category)}</span></div> | |
| ${fp ? `<p>${esc(fp.product_description)}</p>` : `<p class="muted">Gemini could not read the labels for this delivery, so the label check will be skipped for this order. Reason: ${esc(o.fingerprint_error || 'unavailable')}</p>`} | |
| <dl class="kv"> | |
| <dt>Identifiers</dt><dd><div class="chips">${ids}</div></dd> | |
| <dt>Marks</dt><dd>${list(fp?.distinctive_marks)}</dd> | |
| <dt>Accessories</dt><dd>${list(fp?.accessories)}</dd> | |
| <dt>Tags</dt><dd>${list(fp?.tags_attached)}</dd> | |
| </dl> | |
| <div class="thumbs">${ANGLES.map(a => `<div class="thumb"><span>${a}</span><img src="${images[a].cutout}" alt="${a} cut-out"></div>`).join('')}</div> | |
| <a class="btn btn-primary" style="width:100%;margin-top:16px" href="#return">Process a return for #${o.order_id} β</a>`; | |
| } | |
| /* ββ return ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function prefillReturn() { | |
| const last = store.get('rg-last-order'); | |
| if (last && !$('#r-order').value) $('#r-order').value = last; | |
| } | |
| function initReturn() { | |
| const form = $('#return-form'); | |
| form.addEventListener('submit', async e => { | |
| e.preventDefault(); | |
| if (!/^#?\d+$/.test($('#r-order').value.trim())) return toast('Enter the numeric order ID from the delivery.', 'error'); | |
| if (ANGLES.some(a => !$(`input[name="${a}_image"]`, form).files.length)) return toast('Add all three photos (front, back, side).', 'error'); | |
| busy(true, 'Uploading return photos', ''); | |
| try { | |
| const data = await api('/api/returns', { method: 'POST', body: new FormData(form) }); | |
| clearPreviews(form); | |
| $$('input[type=file]', form).forEach(i => { i.value = ''; }); | |
| location.hash = `#result/${data.return_id}`; | |
| } catch (err) { | |
| toast(err.message, 'error'); | |
| } finally { | |
| busy(false); | |
| } | |
| }); | |
| } | |
| /* ββ result (live) βββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const LEVEL_BADGE = { good: 'b-ok', ok: 'b-ok', warn: 'b-warn', bad: 'b-bad', info: 'b-dim' }; | |
| const LEVEL_ICON = { good: 'β', ok: 'β', warn: '!', bad: 'β', info: 'i' }; | |
| async function openResult(id) { | |
| ['#res-verdict', '#res-identity', '#res-inspection', '#res-llm', '#res-views'].forEach(s => { $(s).innerHTML = ''; }); | |
| $('#res-checks').innerHTML = '<p class="dim">Waiting for the agentsβ¦</p>'; | |
| $('#res-gemini').innerHTML = '<p class="dim">Waiting for the agentsβ¦</p>'; | |
| $('#res-evidence').innerHTML = ''; | |
| $('#res-report').classList.add('hidden'); | |
| $('#res-title').textContent = 'Verifying returnβ¦'; | |
| const tick = async () => { | |
| try { | |
| const data = await api(`/api/returns/${id}`); | |
| renderResult(data); | |
| if (data.ret.status === 'PROCESSING' && location.hash === `#result/${id}`) pollTimer = setTimeout(tick, 1100); | |
| } catch (err) { | |
| $('#res-title').textContent = 'Return not found'; | |
| $('#res-sub').textContent = err.message; | |
| } | |
| }; | |
| tick(); | |
| } | |
| function renderPipeline(ret) { | |
| const done = new Set(ret.status === 'PROCESSING' ? ret.progress : (ret.result?.route || ret.progress)); | |
| const times = Object.fromEntries((ret.result?.trace || []).map(t => [t.node, t.ms])); | |
| const finished = ret.status !== 'PROCESSING'; | |
| const firstPending = NODES.find(([k]) => !done.has(k) && !['identity', 'inspector', 'decide'].includes(k)); | |
| $('#res-pipeline').innerHTML = NODES.map(([k, name, desc, llm]) => { | |
| let cls = '', st = 'waiting'; | |
| if (done.has(k)) { cls = 'done'; st = times[k] != null ? `${(times[k] / 1000).toFixed(1)} s` : 'done'; } | |
| else if (finished) { cls = 'skipped'; st = 'not needed'; } | |
| else if ((firstPending && firstPending[0] === k) || (!firstPending && done.has('triage') && k === 'decide')) { cls = 'running'; st = 'running'; } | |
| return `<div class="pnode ${cls} ${llm ? 'llm' : ''}"><span class="ic"></span><span class="nm">${name}<small>${desc}</small></span><span class="st">${st}</span></div>`; | |
| }).join(''); | |
| } | |
| function gauge(risk, band) { | |
| const r = Math.max(0, Math.min(100, risk ?? 0)) / 100; | |
| const a = Math.PI * (1 - r); | |
| const x = 100 + 80 * Math.cos(a), y = 100 - 80 * Math.sin(a); | |
| const color = risk == null ? 'var(--text-3)' : risk < 50 ? 'var(--ok)' : risk < 90 ? 'var(--warn)' : 'var(--bad)'; | |
| return `<div class="gauge"><svg viewBox="0 0 200 110" aria-hidden="true"> | |
| <path d="M20 100 A80 80 0 0 1 180 100" fill="none" stroke="var(--surface-3)" stroke-width="14" stroke-linecap="round"/> | |
| ${r > 0 ? `<path d="M20 100 A80 80 0 0 1 ${x.toFixed(1)} ${y.toFixed(1)}" fill="none" stroke="${color}" stroke-width="14" stroke-linecap="round"/>` : ''} | |
| </svg><div class="gv mono" style="color:${color}">${risk ?? 'β'}</div><div class="gk">${band ? esc(band.label) + ' Β· ' : ''}risk score out of 100</div></div>`; | |
| } | |
| function checkRow(c) { | |
| return `<div class="check"><span class="ck ${LEVEL_BADGE[c.level] || 'b-dim'}">${LEVEL_ICON[c.level] || 'i'}</span> | |
| <div><div class="ck-top"><b>${esc(c.name)}</b>${c.value ? `<span class="ck-val mono">${esc(c.value)}</span>` : ''}</div> | |
| <div class="ck-verdict">${esc(c.verdict)}</div>${c.detail ? `<div class="ck-detail">${esc(c.detail)}</div>` : ''}</div></div>`; | |
| } | |
| function geminiCard(g, returnId) { | |
| const status = { ran: 'Ran', unavailable: 'Unavailable', not_needed: 'Not needed' }[g.status] || g.status; | |
| return `<div class="check"><span class="ck ${LEVEL_BADGE[g.level] || 'b-dim'}">${g.status === 'ran' ? (LEVEL_ICON[g.level] || 'i') : 'β'}</span> | |
| <div><div class="ck-top"><b>${esc(g.agent)}</b><span class="badge ${g.status === 'ran' ? 'b-ok' : g.status === 'unavailable' ? 'b-warn' : 'b-dim'}">${status}</span></div> | |
| ${g.status === 'ran' ? `<div class="ck-verdict">${esc(g.verdict)}</div>` : ''} | |
| ${g.detail ? `<div class="ck-detail">${esc(g.detail)}</div>` : ''} | |
| ${g.bullets?.length ? `<ul class="list" style="margin-top:6px">${g.bullets.map(b => `<li>${esc(b)}</li>`).join('')}</ul>` : ''} | |
| ${g.note ? `<div class="ck-detail"><em>${esc(g.note)}</em></div>` : ''} | |
| ${g.can_request ? `<button class="btn btn-ghost btn-sm" style="margin-top:10px" data-second-opinion="${returnId}">Ask Gemini for a second opinion</button>` : ''} | |
| </div></div>`; | |
| } | |
| function renderResult({ ret, order, explained: x, delivery_images: dImg, return_images: rImg, report_url }) { | |
| $('#res-eyebrow').textContent = `Return #${ret.return_id} Β· Order #${ret.order_id}`; | |
| $('#res-sub').textContent = `${order.product_name || order.category} Β· customer ${order.customer_id} Β· reason: ${ret.reason.replace(/_/g, ' ')}`; | |
| renderPipeline(ret); | |
| if (ret.status === 'PROCESSING') { | |
| $('#res-title').textContent = 'Verifying returnβ¦'; | |
| $('#res-verdict').innerHTML = `<div class="verdict"><div><div class="vlabel" style="color:var(--accent)">Analysing</div><div class="vhead">Agents are working through the evidence. This page updates live.</div></div><div class="spinner" style="margin:0"></div></div>`; | |
| return; | |
| } | |
| if (ret.status === 'ERROR') { | |
| $('#res-title').textContent = 'Pipeline error'; | |
| $('#res-verdict').innerHTML = `<div class="verdict ERROR"><div><div class="vlabel">Error</div><div class="vhead">${esc(ret.error)}</div><div class="vmeta">The return was not decided. Resubmit it, or review manually.</div></div></div>`; | |
| return; | |
| } | |
| const r = ret.result, d = r.decision; | |
| $('#res-title').textContent = d.headline; | |
| $('#res-report').href = report_url || `/report/${ret.return_id}`; | |
| $('#res-report').classList.remove('hidden'); | |
| $('#res-verdict').innerHTML = ` | |
| <div class="verdict ${d.decision}"> | |
| <div> | |
| <div class="vlabel">${VERDICT[d.decision]}</div> | |
| <div class="vhead">${esc(x.decision.sentence)}</div> | |
| ${x.decision.reasons.length ? `<p class="muted" style="margin-top:8px">Main reasons: ${x.decision.reasons.map(esc).join(' Β· ')}</p>` : ''} | |
| ${d.fraud_type ? `<span class="ftype">${esc(FRAUD_LABEL[d.fraud_type] || d.fraud_type)}</span>` : ''} | |
| <ul class="list" style="margin-top:12px">${d.actions.map(a => `<li>${esc(a)}</li>`).join('')}</ul> | |
| <div class="vmeta">${r.llm_call_count ? `${r.llm_call_count} Gemini call${r.llm_call_count === 1 ? '' : 's'}` : 'Decided by the free visual check, no Gemini call'}${d.thresholds ? ` Β· auto-approved below ${d.thresholds.accept_below}, rejected from ${d.thresholds.reject_from}` : ''}</div> | |
| </div> | |
| ${d.risk_score != null ? gauge(d.risk_score, x.decision.risk_band) : ''} | |
| </div>`; | |
| $('#res-checks').innerHTML = x.checks.map(checkRow).join('') || '<p class="muted">No checks ran.</p>'; | |
| $('#res-gemini').innerHTML = x.gemini.map(g => geminiCard(g, ret.return_id)).join(''); | |
| $$('[data-second-opinion]').forEach(b => b.addEventListener('click', () => requestSecondOpinion(Number(b.dataset.secondOpinion), b))); | |
| // technical details | |
| $('#res-evidence').innerHTML = x.evidence.length ? x.evidence.map(e => { | |
| const w = Math.min(Math.abs(e.weight) / 6, 1) * 50; | |
| const bar = e.weight >= 0 ? `left:50%;width:${w}%;background:var(--bad)` : `left:${50 - w}%;width:${w}%;background:var(--ok)`; | |
| return `<div class="ev-row"><div><div class="ev-src">${esc(e.source.replace(/_/g, ' '))} Β· ${esc(e.effect)}</div>${esc(e.finding)}</div> | |
| <div class="ev-bar"><i style="${bar}"></i></div><div class="ev-w ${e.weight > 0 ? 'pos' : e.weight < 0 ? 'neg' : ''}">${e.weight > 0 ? '+' : ''}${e.weight.toFixed(1)}</div></div>`; | |
| }).join('') + '<p class="dim" style="font-size:12px;margin-top:10px">Each finding adds (red, towards fraud) or subtracts (green, towards genuine) log-odds; the total goes through a sigmoid to give the 0β100 score.</p>' | |
| : `<p class="muted">${esc(d.summary)}</p>`; | |
| const idn = r.identity; | |
| $('#res-identity').innerHTML = idn?.locations ? `<div class="card-title" style="margin-top:22px">Identity agent Β· raw label readout</div> | |
| <table class="t"><tr><th>Location</th><th>Recorded at delivery</th><th>Read at return</th><th>Result</th></tr> | |
| ${idn.locations.map(l => `<tr><td>${esc(l.location)}<div class="dim" style="font-size:12px">${esc(l.view)} view</div></td> | |
| <td class="mono" style="font-size:12px">${(l.expected || (order.fingerprint?.identifiers || []) | |
| .filter(i => i.location === l.location && i.view === l.view).map(i => i.text)).map(esc).join('<br>') || 'β'}</td> | |
| <td class="mono" style="font-size:12px">${esc(l.observed) || 'β'}</td> | |
| <td><span class="badge ${['unit_match', 'model_match'].includes(l.status) ? 'b-ok' : ['missing', 'mismatch'].includes(l.status) ? 'b-bad' : 'b-dim'}">${esc(l.status.replace(/_/g, ' '))}</span></td></tr>`).join('')} | |
| </table>` : ''; | |
| const ins = r.inspection; | |
| $('#res-inspection').innerHTML = ins ? `<div class="card-title" style="margin-top:22px">Visual inspector Β· raw output per view</div> | |
| <table class="t"><tr><th>View</th><th>Verdict</th><th>Differences</th></tr> | |
| ${ins.findings.map(f => `<tr><td>${esc(f.view)}</td><td>${esc(f.verdict.replace(/_/g, ' '))}</td><td>${esc(f.differences.join('; ')) || 'β'}</td></tr>`).join('')}</table>` : ''; | |
| $('#res-llm').innerHTML = `<div class="card-title" style="margin-top:22px">LLM calls (${r.llm_call_count})</div> | |
| ${r.llm_calls.length ? `<table class="t"><tr><th>Agent</th><th>Time</th><th>Tokens</th><th>Tries</th></tr>${r.llm_calls.map(c => | |
| `<tr><td>${esc(c.agent.replace(/_/g, ' '))}${c.ok ? '' : ' <span class="badge b-bad">failed</span>'}</td><td class="mono">${c.latency_s}s</td><td class="mono">${c.input_tokens + c.output_tokens}</td><td class="mono">${c.attempts}</td></tr>`).join('')}</table>` | |
| : '<p class="muted">None. The free visual check settled this return on its own.</p>'}`; | |
| const q = r.quality || {}; | |
| $('#res-views').innerHTML = `<div class="card-title" style="margin-top:4px">Delivered vs returned, view by view</div><div class="views">${ANGLES.map(a => { | |
| const v = x.views[a]; | |
| const retImg = d.decision === 'RETAKE_REQUIRED' ? rImg[a].photo : rImg[a].cutout; | |
| return `<div class="view"><div class="pair"><div><span>DELIVERED</span><img src="${dImg[a].cutout}" alt="Delivered ${a}"></div> | |
| <div><span>RETURNED</span><img src="${retImg}" alt="Returned ${a}" onerror="this.src='${rImg[a].photo}'"></div></div> | |
| <div class="vfoot"><b>${a}</b>${q[a] && !q[a].ok ? `<span class="badge b-warn">${esc(q[a].problem)}</span>` : v ? `<span> | |
| <span class="badge ${LEVEL_BADGE[v.shape_level]}" title="${esc(v.shape_verdict)}">shape ${esc(v.shape)}</span> | |
| <span class="badge ${LEVEL_BADGE[v.colour_level]}">${esc(v.colour_verdict)}</span></span>` : ''}</div></div>`; | |
| }).join('')}</div>`; | |
| } | |
| async function requestSecondOpinion(returnId, btn) { | |
| btn.disabled = true; | |
| btn.textContent = 'Asking Geminiβ¦'; | |
| try { | |
| const { opinion } = await api(`/api/returns/${returnId}/second-opinion`, { method: 'POST' }); | |
| toast(opinion.ok ? 'Gemini second opinion added.' : 'Gemini could not run right now; the reason is shown below.', opinion.ok ? 'success' : 'error'); | |
| } catch (err) { | |
| toast(err.message, 'error'); | |
| } | |
| openResult(returnId); | |
| } | |
| /* ββ account βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| let me = null; | |
| async function refreshMe() { | |
| try { | |
| me = await api('/api/auth/me'); | |
| } catch (e) { | |
| me = { customer: null }; | |
| } | |
| const c = me.customer; | |
| $('#nav-account').textContent = c ? c.customer_id : 'Sign in'; | |
| $('#nav-account').classList.toggle('signed-in', !!c); | |
| $$('[data-who]').forEach(el => { | |
| el.classList.toggle('out', !c); | |
| el.innerHTML = c | |
| ? `<span class="badge b-ok">Signed in</span><span class="who-text">Customer <b class="mono">${esc(c.customer_id)}</b> Β· ${esc(c.email)}</span>` | |
| : `<span class="who-text">Sign in so this is linked to your customer ID and return history.</span><a class="btn btn-dark btn-sm" href="#account">Sign in</a>`; | |
| }); | |
| const chips = $('#r-my-orders'); | |
| if (chips) { | |
| chips.innerHTML = c && me.orders?.length | |
| ? '<span class="dim" style="font-size:12px">Your orders:</span>' + me.orders.map(o => `<button type="button" class="chip" data-pick="${o.order_id}">#${o.order_id} ${esc((o.product_name || o.category).slice(0, 24))}</button>`).join('') | |
| : ''; | |
| $$('[data-pick]', chips).forEach(b => b.addEventListener('click', () => { $('#r-order').value = b.dataset.pick; })); | |
| } | |
| return me; | |
| } | |
| async function loadAccount() { | |
| await refreshMe(); | |
| const c = me.customer; | |
| $('#acct-out').classList.toggle('hidden', !!c); | |
| $('#acct-in').classList.toggle('hidden', !c); | |
| if (!c) return; | |
| $('#acct-id').textContent = c.customer_id; | |
| $('#acct-details').innerHTML = `<dt>Email</dt><dd>${esc(c.email)}</dd><dt>Mobile</dt><dd class="mono">${esc(c.phone)}</dd><dt>Member since</dt><dd>${esc(c.created_at.slice(0, 10))}</dd>`; | |
| const p = me.profile, px = me.profile_explained; | |
| $('#acct-profile').innerHTML = historyCard(me.profile, me.profile_explained); | |
| } | |
| function historyCard(p, px) { | |
| return checkRow({ name: 'Return history', ...px }) + | |
| `<p class="dim" style="font-size:13px;margin-top:10px">${p.prior_returns} earlier return(s), ${p.prior_flagged} flagged. Your next return is auto-approved below risk ${p.accept_below} (standard: 50) and rejected from ${p.reject_from}.</p>`; | |
| } | |
| const AUTH_COPY = { | |
| login: ['Welcome back', 'Sign in with the email and mobile number you registered with.'], | |
| signup: ['Create your account', 'Get a customer ID that links your deliveries and returns.'], | |
| }; | |
| const DEMO_ACCOUNT = { email: 'demo@returnguard.app', phone: '9000000001' }; | |
| function showAuthTab(tab) { | |
| $$('[data-auth-tab]').forEach(b => { | |
| const on = b.dataset.authTab === tab; | |
| b.classList.toggle('on', on); | |
| b.setAttribute('aria-selected', on); | |
| }); | |
| $('#login-form').classList.toggle('hidden', tab !== 'login'); | |
| $('#signup-form').classList.toggle('hidden', tab !== 'signup'); | |
| [$('#auth-title').textContent, $('#auth-sub').textContent] = AUTH_COPY[tab]; | |
| } | |
| async function authRequest(url, email, phone) { | |
| return api(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, phone }) }); | |
| } | |
| function afterSignIn(customer, verb) { | |
| toast(`${verb} Β· ${customer.customer_id}`, 'success'); | |
| const next = store.get('rg-after-signin'); | |
| if (next) { store.set('rg-after-signin', ''); location.hash = next; } else { loadAccount(); } | |
| } | |
| function initAccount() { | |
| $$('[data-auth-tab]').forEach(b => b.addEventListener('click', () => showAuthTab(b.dataset.authTab))); | |
| const submit = (formId, url, verb) => $(formId).addEventListener('submit', async e => { | |
| e.preventDefault(); | |
| const f = new FormData(e.target); | |
| try { | |
| const { customer } = await authRequest(url, f.get('email'), f.get('phone')); | |
| e.target.reset(); | |
| afterSignIn(customer, verb); | |
| } catch (err) { | |
| toast(err.message, 'error'); | |
| } | |
| }); | |
| submit('#signup-form', '/api/auth/signup', 'Account created'); | |
| submit('#login-form', '/api/auth/login', 'Signed in'); | |
| $('#demo-login').addEventListener('click', async () => { | |
| try { | |
| let customer; | |
| try { | |
| ({ customer } = await authRequest('/api/auth/login', DEMO_ACCOUNT.email, DEMO_ACCOUNT.phone)); | |
| } catch (e) { | |
| ({ customer } = await authRequest('/api/auth/signup', DEMO_ACCOUNT.email, DEMO_ACCOUNT.phone)); | |
| } | |
| afterSignIn(customer, 'Signed in to the demo account'); | |
| } catch (err) { | |
| toast(err.message, 'error'); | |
| } | |
| }); | |
| $('#logout-btn').addEventListener('click', async () => { | |
| await api('/api/auth/logout', { method: 'POST' }); | |
| toast('Signed out.', 'success'); | |
| loadAccount(); | |
| }); | |
| } | |
| /* ββ dashboard βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function loadDashboard() { | |
| await refreshMe(); | |
| const signedIn = !!me.customer; | |
| $('#dash-signed-out').classList.toggle('hidden', signedIn); | |
| $('#dash-body').classList.toggle('hidden', !signedIn); | |
| $('#dash-eyebrow').textContent = signedIn ? `Customer ${me.customer.customer_id}` : 'Your activity'; | |
| if (!signedIn) return; | |
| try { | |
| const [{ stats: s, profile, profile_explained: px }, { returns }, { orders }] = | |
| await Promise.all([api('/api/stats'), api('/api/returns'), api('/api/orders')]); | |
| const by = s.by_status; | |
| const flagged = (by.HUMAN_REVIEW || 0) + (by.REJECT || 0); | |
| $('#dash-kpis').innerHTML = [ | |
| [s.orders, 'My deliveries'], [s.returns, 'My returns'], [by.ACCEPT || 0, 'Auto-accepted'], | |
| [flagged, 'Flagged (review + reject)'], [s.decided_without_llm, 'Decided with no LLM call'], | |
| ].map(([v, k]) => `<div class="kpi"><div class="v">${v}</div><div class="k">${k}</div></div>`).join(''); | |
| $('#dash-history').innerHTML = historyCard(profile, px); | |
| $('#dash-returns').innerHTML = returns.length ? `<table class="t"><tr><th>Return</th><th>Order</th><th>Product</th><th>Reason</th><th>Risk</th><th>Verdict</th></tr> | |
| ${returns.map(r => `<tr class="click" data-href="#result/${r.return_id}"><td class="mono">#${r.return_id}</td><td class="mono">#${r.order_id}</td> | |
| <td>${esc(r.product_name || r.category)}</td><td>${esc(r.reason.replace(/_/g, ' '))}</td><td class="mono">${r.risk_score ?? 'β'}</td> | |
| <td><span class="pill ${r.status}">${VERDICT[r.status] || r.status}</span></td></tr>`).join('')}</table>` | |
| : '<div class="empty">No returns yet. Register a delivery, then process a return.</div>'; | |
| $('#dash-orders').innerHTML = orders.length ? `<table class="t"><tr><th>Order</th><th>Delivered</th><th>Product</th><th>Category</th><th>Fingerprint</th><th>Last return</th></tr> | |
| ${orders.map(o => `<tr><td class="mono">#${o.order_id}</td><td>${esc(o.delivered_at.slice(0, 10))}</td><td>${esc(o.product_name || 'β')}</td><td>${esc(o.category)}</td> | |
| <td>${o.has_fingerprint ? '<span class="badge b-ok">saved</span>' : '<span class="badge b-dim">visual only</span>'}</td> | |
| <td>${o.last_return_status ? `<span class="pill ${o.last_return_status}">${VERDICT[o.last_return_status] || o.last_return_status}</span>` : 'β'}</td></tr>`).join('')}</table>` | |
| : '<div class="empty">No deliveries yet.</div>'; | |
| $$('#dash-returns tr.click').forEach(tr => tr.addEventListener('click', () => { location.hash = tr.dataset.href; })); | |
| } catch (err) { | |
| toast(err.message, 'error'); | |
| } | |
| } | |
| /* ββ evaluation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| let evalCache = null; | |
| async function getEval(pairs = false) { | |
| if (evalCache && (!pairs || evalCache.per_pair)) return evalCache; | |
| evalCache = (await api(`/api/metrics${pairs ? '?pairs=1' : ''}`)).results; | |
| return evalCache; | |
| } | |
| let stressCache; | |
| async function getStress() { | |
| if (stressCache === undefined) { | |
| try { stressCache = await api('/api/stress'); } catch (e) { stressCache = null; } | |
| } | |
| return stressCache; | |
| } | |
| const num = n => Number(n).toLocaleString('en-US'); | |
| async function loadHomeMetrics() { | |
| try { | |
| const [r, s] = await Promise.all([getEval(), getStress()]); | |
| const m = r.metrics, a = r.agentic; | |
| const p = s?.pooled; | |
| const vals = { f0_5: m.f0_5.toFixed(3), precision: pct(p ? p.precision : m.precision), recall: pct(m.recall), | |
| no_llm: a.decided_without_llm_pct + '%', calls: a.llm_calls_per_return }; | |
| $$('#home-metrics [data-m]').forEach(el => { el.textContent = vals[el.dataset.m]; }); | |
| const prec = p | |
| ? ` Precision ${pct(p.precision)}: ${num(p.tp)} of ${num(p.flags)} flags were fraud across ${p.conditions} photo conditions (95% interval ${pct(p.precision_ci[0])}β${pct(p.precision_ci[1])}); ${m.tp}/${m.tp + m.fp} on the main set.` | |
| : (m.precision_ci ? ` Precision 95% interval ${pct(m.precision_ci[0], 0)}β${pct(m.precision_ci[1], 0)}.` : ''); | |
| const held = r.held_out ? ` On items added after tuning was frozen: F0.5 ${r.held_out.f0_5.toFixed(3)} (${r.held_out.pairs} pairs).` : ''; | |
| $('#home-metrics-note').innerHTML = `F0.5, recall and LLM use measured on ${r.dataset.pairs} labelled delivery/return pairs (${r.dataset.genuine} genuine, ${r.dataset.fraud} fraud, ${r.dataset.items} physical items) with <span class="mono">${esc(r.model)}</span>.${prec}${held} <a href="#evaluation">How these are measured β</a>`; | |
| } catch (e) { | |
| $('#home-metrics-note').textContent = 'No evaluation results yet. Run: python scripts/evaluate.py'; | |
| } | |
| } | |
| async function loadEvaluation() { | |
| const body = $('#eval-body'); | |
| let r; | |
| try { r = await getEval(true); } catch (e) { | |
| body.innerHTML = `<div class="note">No evaluation results yet. Run <span class="code">python scripts/evaluate.py</span>.</div>`; | |
| return; | |
| } | |
| const s = await getStress(); | |
| const sp = s?.pooled; | |
| const m = r.metrics, a = r.agentic; | |
| const kpi = (v, k, d, hero) => `<div class="kpi ${hero ? 'hero-kpi' : ''}"><div class="v">${v}</div><div class="k">${k}</div>${d ? `<div class="d">${d}</div>` : ''}</div>`; | |
| const cell = (lab, dec) => { | |
| const n = (r.confusion[lab] || {})[dec] || 0; | |
| const good = (lab === 'genuine' && dec === 'ACCEPT') || (lab === 'fraud' && dec !== 'ACCEPT'); | |
| return `<div class="c ${n ? (good ? 'good' : 'badc') : ''}">${n}<small>${good ? 'correct' : 'error'}</small></div>`; | |
| }; | |
| const misses = (r.per_pair || []).filter(p => (p.decision !== 'ACCEPT') !== p.is_fraud); | |
| const maxRoute = Math.max(...Object.values(a.routes)); | |
| const ci = c => c ? `95% interval ${pct(c[0], 0)}β${pct(c[1], 0)}` : ''; | |
| const DECISIONS = ['ACCEPT', 'HUMAN_REVIEW', 'REJECT', 'RETAKE_REQUIRED']; | |
| body.innerHTML = ` | |
| <div class="kpis"> | |
| ${kpi(m.f0_5.toFixed(3), 'F0.5 score', 'precision-weighted (Ξ² = 0.5)', true)} | |
| ${sp ? kpi(pct(sp.precision), 'Precision, stress-tested', `${num(sp.tp)} of ${num(sp.flags)} flags were fraud Β· 95% interval ${pct(sp.precision_ci[0])}β${pct(sp.precision_ci[1])} Β· main set ${m.tp}/${m.tp + m.fp}`) | |
| : kpi(pct(m.precision), 'Precision', `${m.tp} of ${m.tp + m.fp} flags were fraud Β· ${ci(m.precision_ci)}`)} | |
| ${kpi(pct(m.recall), 'Recall', `${m.tp} of ${m.tp + m.fn} frauds flagged Β· ${ci(m.recall_ci)}`)} | |
| ${kpi(m.f1.toFixed(3), 'F1 score')} | |
| ${kpi(pct(m.false_rejection_rate), 'Genuine returns flagged', `${m.fp} of ${m.fp + m.tn} Β· ${ci(m.false_rejection_rate_ci)}`)} | |
| </div> | |
| ${r.held_out ? `<div class="section-head" style="margin:8px 0 14px"><span class="eyebrow">Held-out test Β· ${r.held_out.items.map(esc).join(' + ')}</span></div> | |
| <div class="kpis"> | |
| ${kpi(r.held_out.f0_5.toFixed(3), 'F0.5 on held-out items', `${r.held_out.pairs} pairs (${r.held_out.genuine} genuine, ${r.held_out.fraud} fraud)`, true)} | |
| ${kpi(pct(r.held_out.precision), 'Precision', `${r.held_out.tp}/${r.held_out.tp + r.held_out.fp} Β· ${ci(r.held_out.precision_ci)}`)} | |
| ${kpi(pct(r.held_out.recall), 'Recall', `${r.held_out.tp}/${r.held_out.tp + r.held_out.fn} Β· ${ci(r.held_out.recall_ci)}`)} | |
| ${kpi(pct(r.held_out.false_rejection_rate), 'Genuine flagged', `${r.held_out.fp} of ${r.held_out.fp + r.held_out.tn}`)} | |
| ${kpi('frozen', 'Thresholds & weights', 'not changed after these items were added')} | |
| </div>` : ''} | |
| <div class="note" style="margin-bottom:20px"><b>Positive class = fraud.</b> A return is "flagged" when it is not auto-accepted (human review, reject${r.retakes?.count ? `, or one of the ${r.retakes.count} retake requests` : ''}). F0.5 weights precision twice as much as recall, because wrongly blocking a genuine customer costs trust. The 95% intervals show how much a small test set can say: a perfect score on a few dozen pairs is still compatible with a noticeably lower true rate.</div> | |
| <div class="result-grid"> | |
| <div class="card"><div class="card-title">Ablation Β· what each stage adds (F0.5)</div><div class="bars"> | |
| ${r.ablations.map(x => `<div class="bar-row"><span>${esc(x.name)}<div class="dim" style="font-size:12px">${esc(x.detail)}</div></span> | |
| <div class="track"><div class="fill" style="width:${x.f0_5 * 100}%"></div></div><b class="mono">${x.f0_5.toFixed(3)}</b></div>`).join('')} | |
| </div> | |
| <table class="t" style="margin-top:12px"><tr><th>System</th><th>Precision</th><th>Recall</th><th>LLM calls</th></tr> | |
| ${r.ablations.map(x => `<tr><td>${esc(x.name)}</td><td class="mono">${pct(x.precision)}</td><td class="mono">${pct(x.recall)}</td><td class="mono">${x.llm_calls_per_return ?? 'β'}</td></tr>`).join('')}</table> | |
| </div> | |
| <div class="card"><div class="card-title">Confusion matrix Β· full pipeline</div> | |
| <div class="confusion"><div></div><div class="h">Accept</div><div class="h">Human review</div><div class="h">Reject</div><div class="h">Retake</div> | |
| <div class="h">Genuine (${r.dataset.genuine})</div>${DECISIONS.map(d => cell('genuine', d)).join('')} | |
| <div class="h">Fraud (${r.dataset.fraud})</div>${DECISIONS.map(d => cell('fraud', d)).join('')} | |
| </div> | |
| <table class="t" style="margin-top:16px"><tr><th>Pair type</th><th>Accept</th><th>Review</th><th>Reject</th><th>Retake</th></tr> | |
| ${Object.entries(r.by_pair_type).map(([k, v]) => `<tr><td>${esc(k.replace(/_/g, ' '))}</td>${DECISIONS.map(d => `<td class="mono">${v[d] || 0}</td>`).join('')}</tr>`).join('')}</table> | |
| </div> | |
| </div> | |
| ${sp ? robustness(s, kpi) : ''} | |
| <div class="section-head" style="margin:28px 0 16px"><span class="eyebrow">Agentic metrics</span></div> | |
| <div class="kpis"> | |
| ${kpi(a.llm_calls_per_return, 'LLM calls per return', `v1 made up to ${a.v1_llm_calls_per_return_upper_bound}`, true)} | |
| ${kpi(a.decided_without_llm_pct + '%', 'Decided with no LLM call')} | |
| ${kpi(a.schema_valid_first_try_pct + '%', 'Valid structured output, 1st try', `${a.total_retries} retries`)} | |
| ${kpi(`${a.identity_agent_accuracy.correct}/${a.identity_agent_accuracy.runs}`, 'Identity agent correct')} | |
| ${kpi(`${a.inspector_accuracy.correct}/${a.inspector_accuracy.runs}`, 'Visual inspector correct')} | |
| </div> | |
| <div class="result-grid"> | |
| <div class="card"><div class="card-title">Routes through the graph</div><div class="bars"> | |
| ${Object.entries(a.routes).map(([k, v]) => `<div class="bar-row"><span class="mono" style="font-size:12.5px">${esc(k)}</span><div class="track"><div class="fill" style="width:${v / maxRoute * 100}%"></div></div><b class="mono">${v}</b></div>`).join('')} | |
| </div></div> | |
| <div class="card"><div class="card-title">Cost & latency</div><dl class="kv"> | |
| <dt>Tokens / return</dt><dd class="mono">${a.tokens_per_return}</dd> | |
| <dt>Cost / 1,000 returns</dt><dd class="mono">${a.est_cost_usd_per_1000_returns != null ? '$' + a.est_cost_usd_per_1000_returns : 'n/a'} <span class="dim">(Gemini list price)</span></dd> | |
| <dt>Latency / return</dt><dd class="mono">mean ${a.latency_s_per_return.mean}s Β· max ${a.latency_s_per_return.p_max}s</dd> | |
| ${Object.entries(a.agents).map(([k, v]) => `<dt>${esc(k.replace(/_/g, ' '))}</dt><dd class="mono">${v.calls} calls Β· ${v.mean_latency_s}s Β· ${v.mean_tokens} tok</dd>`).join('')} | |
| </dl></div> | |
| </div> | |
| <div class="section-head" style="margin:28px 0 16px"><span class="eyebrow">Error analysis & honest caveats</span></div> | |
| <div class="card" style="margin-bottom:20px"><div class="card-title">Misclassified pairs (${misses.length})</div> | |
| ${misses.length ? `<table class="t"><tr><th>Pair</th><th>Truth</th><th>Decision</th><th>Strongest evidence</th></tr>${misses.map(p => `<tr><td>${esc(p.delivery_item)} β ${esc(p.return_item)}<div class="dim" style="font-size:12px">${esc(p.pair_type.replace(/_/g, ' '))}</div></td> | |
| <td>${p.is_fraud ? 'fraud' : 'genuine'}</td><td><span class="pill ${p.decision}">${VERDICT[p.decision]}</span></td> | |
| <td style="font-size:13px">${p.evidence.slice().sort((x, y) => Math.abs(y.weight) - Math.abs(x.weight)).slice(0, 2).map(e => `${esc(e.finding)} (${e.weight > 0 ? '+' : ''}${e.weight.toFixed(1)})`).join('<br>')}</td></tr>`).join('')}</table> | |
| <p class="muted" style="margin-top:12px;font-size:14px">Each row shows the two strongest pieces of evidence behind the wrong decision. Fixes suggested by these errors are | |
| <b>not</b> applied before this run: they come from the same data, so they need new items to be validated honestly. Full per-pair log: <span class="code">results/eval_pairs.csv</span>.</p>` | |
| : '<p class="muted">None.</p>'} | |
| </div> | |
| ${r.real_recapture ? `<div class="card" style="margin-bottom:20px"><div class="card-title">Real re-photographs vs simulated</div> | |
| <table class="t"><tr><th>Genuine pairs</th><th>Pairs</th><th>Auto-accepted</th><th>Flagged</th></tr> | |
| ${r.real_recapture.map(x => `<tr><td>${esc(x.kind)}</td><td class="mono">${x.pairs}</td><td class="mono">${x.accepted}</td><td class="mono">${x.flagged}</td></tr>`).join('')}</table> | |
| <p class="muted" style="margin-top:10px;font-size:14px">Simulated re-captures re-use the delivery photo, so they are easy. Real ones are separate photo sessions (new place, angle and lighting) and are the honest test.</p></div>` : ''} | |
| <div class="note"><b>Read these numbers with care.</b> ${esc(r.dataset.note)} ${r.dataset.pairs} pairs is small, and the thresholds and weights were chosen by looking at the first 6 items${r.held_out ? ' (the held-out items above were added only after that)' : ''}. | |
| Reproduce with <span class="code">python scripts/evaluate.py</span>. Generated ${esc(r.generated_at)} with <span class="mono">${esc(r.model)}</span>.</div>`; | |
| } | |
| function robustness(s, kpi) { | |
| const p = s.pooled, sum = s.summary; | |
| const byLabel = {}; | |
| s.conditions.forEach(c => { (byLabel[c.label] = byLabel[c.label] || {})[c.level] = c; }); | |
| const lvl = c => c ? `<td class="mono ${c.fp >= c.genuine / 2 ? 'bad-t' : ''}">${c.fp}/${c.genuine}</td><td class="mono">${pct(c.precision)}</td>` : '<td>β</td><td>β</td>'; | |
| return ` | |
| <div class="section-head" style="margin:28px 0 16px"><span class="eyebrow">Robustness Β· precision under bad photos</span></div> | |
| <div class="kpis kpis-3"> | |
| ${kpi(pct(p.precision), 'Pooled precision', `${num(p.tp)} of ${num(p.flags)} flags were fraud Β· 95% interval ${pct(p.precision_ci[0])}β${pct(p.precision_ci[1])}`, true)} | |
| ${kpi(pct(p.recall), 'Recall', `every fraud flagged in all ${p.conditions} conditions`)} | |
| ${kpi(`${sum.genuine_flagged_by_local_stage}/${sum.genuine_returns_tested}`, 'Genuine returns flagged', `${sum.hard_false_rejections} hard rejections Β· ${sum.sent_to_gemini} sent to Gemini Β· ${sum.retakes} retakes`)} | |
| </div> | |
| <div class="card" style="margin-bottom:20px"><div class="card-title">Each return photo distorted ${p.conditions / 2} ways at two strengths (${num(p.pairs)} pairs)</div> | |
| <table class="t"><tr><th>Condition</th><th>Mild Β· genuine flagged</th><th>Precision</th><th>Strong Β· genuine flagged</th><th>Precision</th></tr> | |
| ${Object.entries(byLabel).map(([k, v]) => `<tr><td>${esc(k)}</td>${lvl(v.mild)}${lvl(v.strong)}</tr>`).join('')}</table> | |
| <p class="muted" style="margin-top:12px;font-size:14px">Only the local stage (DINOv2 + colour) was run, so a genuine return that would be sent to Gemini counts as a false flag here. That makes these figures a worst case. | |
| The weak spots are strong warm or cool light, very heavy JPEG compression and photos taken from far away. Colour-cast correction and a compression quality gate are the planned fixes. | |
| Precision also depends on how common fraud is: this test has 7 fraud pairs per genuine one, while a real store might see around 15% fraud, which would give a much lower precision for the local stage alone. | |
| Reproduce with <span class="code">python scripts/stress_test_precision.py</span>.</p> | |
| </div>`; | |
| } | |
| /* ββ hero animation ββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const scene = (() => { | |
| const el = $('#scene'); | |
| if (!el) return { setVisible() {} }; | |
| const steps = [ | |
| { t: 700, cap: 'A delivery agent brings a new speaker to the customer.', day: 'Day 0 Β· Delivery' }, | |
| { t: 1900, cap: 'At the door, before handing it overβ¦' }, | |
| { t: 3400, cap: 'the agent photographs it in ReturnGuard: 3 views, label read, fingerprint saved.' }, | |
| { t: 2400, cap: 'The customer receives the blue speaker.' }, | |
| { t: 2600, cap: 'The customer asks for a return and hands back⦠a speaker.', day: 'Day 5 · Return pickup', fade: true }, | |
| { t: 3600, cap: 'The agent scans it: same model, but the colour is off and the batch label is gone.' }, | |
| { t: 4400, cap: 'Fraud caught at the doorstep. The refund is held and the seller gets the evidence.' }, | |
| ]; | |
| let i = 0, timer = null, playing = !REDUCED_MOTION, visible = true; | |
| const dots = $('#scene-dots'); | |
| dots.innerHTML = steps.map((_, k) => `<button aria-label="Show step ${k + 1}"></button>`).join(''); | |
| function show(n) { | |
| i = n; | |
| el.dataset.step = n; | |
| $('#scene-agent').classList.toggle('walking', n <= 1); | |
| $('#scene-caption').textContent = steps[n].cap; | |
| const day = steps.slice(0, n + 1).reverse().find(s => s.day); | |
| $('#scene-day').textContent = day.day; | |
| $$('button', dots).forEach((b, k) => b.classList.toggle('on', k === n)); | |
| } | |
| function schedule() { | |
| clearTimeout(timer); | |
| if (playing && visible && !document.hidden) timer = setTimeout(next, steps[i].t); | |
| } | |
| function next() { | |
| const n = (i + 1) % steps.length; | |
| if (steps[n].fade) { | |
| el.classList.add('transition'); | |
| timer = setTimeout(() => { show(n); el.classList.remove('transition'); schedule(); }, 1400); | |
| } else { | |
| show(n); | |
| schedule(); | |
| } | |
| } | |
| $$('button', dots).forEach((b, k) => b.addEventListener('click', () => { show(k); schedule(); })); | |
| $('#scene-toggle').addEventListener('click', () => { | |
| playing = !playing; | |
| $('#scene-toggle').textContent = playing ? 'Pause' : 'Play'; | |
| schedule(); | |
| }); | |
| document.addEventListener('visibilitychange', schedule); | |
| show(REDUCED_MOTION ? 6 : 0); | |
| if (REDUCED_MOTION) $('#scene-toggle').textContent = 'Play'; | |
| schedule(); | |
| return { setVisible(v) { visible = v; schedule(); } }; | |
| })(); | |
| /* ββ init ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initTheme(); | |
| initUploads(); | |
| initSamples(); | |
| initDelivery(); | |
| initReturn(); | |
| initAccount(); | |
| refreshMe(); | |
| $('#menu-btn').addEventListener('click', () => $('#mobile-nav').classList.toggle('open')); | |
| $('#dash-refresh').addEventListener('click', loadDashboard); | |
| window.addEventListener('hashchange', route); | |
| route(); | |
| loadHomeMetrics(); | |
| }); | |