← back to Dw Photo Capture

public/learn.html

117 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>What the scanner has learned · DW</title>
<style>
  :root{ --bg:#0f0e0c; --card:#1b1916; --ink:#f3efe7; --muted:#9a9184; --line:#2e2a25;
         --gold:#c8a24a; --green:#3fa06a; --red:#c0563f; --cols:3; }
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.45 -apple-system,BlinkMacSystemFont,"SF Pro Display",sans-serif}
  header{position:sticky;top:0;z-index:5;background:rgba(15,14,12,.96);backdrop-filter:blur(8px);border-bottom:1px solid var(--line);padding:14px 18px}
  .h1{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
  .h1 b{font:700 18px/1 "SF Pro Display";letter-spacing:.02em}.h1 b i{color:var(--gold);font-style:normal}
  .sub{color:var(--muted);font-size:12.5px}
  .ver{font:600 9px/1 ui-monospace,Menlo,monospace;background:#1b1407;color:var(--gold);border-radius:99px;padding:3px 6px}
  .controls{display:flex;gap:10px;align-items:center;margin-top:11px;flex-wrap:wrap}
  input,select{background:#16140f;border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:8px 11px;font-size:13.5px}
  input#q{flex:1;min-width:180px}
  input[type=range]{accent-color:var(--gold);padding:0}
  .lbl{color:var(--muted);font-size:11.5px}
  .wrap{padding:16px 18px 60px}
  .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px}
  .card{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:13px 14px;display:flex;flex-direction:column;gap:8px}
  .card .top{display:flex;align-items:center;gap:8px}
  .logo{max-height:30px;max-width:130px;object-fit:contain;background:#fff;border-radius:5px;padding:3px 5px}
  .vn{font:700 15px/1.2 "SF Pro Display";flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .src{font:600 9.5px/1 ui-monospace,Menlo,monospace;padding:3px 7px;border-radius:99px;border:1px solid var(--line);color:var(--muted)}
  .src.scan{color:var(--green);border-color:#274c39}.src.catalog{color:var(--gold);border-color:#3a2e10}
  .pl{font:700 9.5px/1 sans-serif;color:#000;background:var(--red);padding:3px 7px;border-radius:99px}
  .stats{display:flex;gap:14px;color:var(--muted);font-size:12px}
  .stats b{color:var(--ink);font:600 13px/1 ui-monospace,Menlo,monospace}
  .chips{display:flex;gap:6px;flex-wrap:wrap}
  .chip{font:600 11.5px/1 ui-monospace,Menlo,monospace;background:#221d12;color:var(--gold);border:1px solid #3a2e10;border-radius:7px;padding:4px 7px}
  .chip .n{color:var(--muted);font-weight:400}
  .ex{font:400 11px/1.3 ui-monospace,Menlo,monospace;color:var(--muted);word-break:break-all}
  .when{font:500 10.5px/1 ui-monospace,Menlo,monospace;color:var(--muted);display:inline-flex;gap:5px;align-items:center}
  .lfp{font:500 11px/1.3 ui-monospace,Menlo,monospace;color:var(--green)}
  .lfp.bad{color:var(--red)}
  .empty{color:var(--muted);padding:40px 0;text-align:center}
  a{color:var(--gold)}
</style>
</head>
<body>
  <header>
    <div class="h1"><b><i>DW</i> Scanner</b> · what it's learned about each vendor's samples <span class="ver">__VER__</span></div>
    <div class="sub" id="sub">loading…</div>
    <div class="controls">
      <input id="q" placeholder="🔎 filter vendor / prefix…">
      <select id="sort">
        <option value="learned">Most scan-learned</option>
        <option value="n">Most catalog codes</option>
        <option value="az">Vendor A→Z</option>
        <option value="updated">Recently updated</option>
      </select>
      <label class="lbl">density</label>
      <input type="range" id="dens" min="2" max="6" value="3" title="cards per row">
    </div>
  </header>
  <div class="wrap"><div class="grid" id="grid"></div><div class="empty" id="empty" hidden></div></div>
<script>
const $=s=>document.querySelector(s);
let ALL=[];
const LS=(k,d)=>{try{return localStorage.getItem(k)??d}catch(e){return d}};
function fmtWhen(iso){ if(!iso) return ''; const d=new Date(iso);
  return d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
function card(v){
  const prefs=Object.entries(v.prefixes||{}).sort((a,b)=>b[1]-a[1])
    .map(([p,n])=>`<span class="chip">${p}<span class="n"> ·${n}</span></span>`).join('')
    || (v.numeric_share>=0.6?'<span class="chip">###<span class="n"> numeric</span></span>':'<span class="chip" style="opacity:.5">—</span>');
  const ex=(v.examples||[]).slice(0,5).join('  ');
  const src=(v.source==='scan'||v.learned_scans>0)?'scan':'catalog';
  return `<div class="card">
    ${v.logo_url?`<img class="logo" src="${v.logo_url}" alt="${v.name} logo" loading="lazy">`:''}
    <div class="top"><div class="vn" title="${v.name}">${v.name}</div>
      ${v.private_label?'<span class="pl" title="private-label upstream — never customer-facing">PRIVATE</span>':''}
      <span class="src ${src}">${src}</span></div>
    <div class="stats"><span>codes <b>${v.n||0}</b></span><span>scans <b>${v.learned_scans||0}</b></span>
      ${v.common_len?`<span>len <b>${v.common_len}</b></span>`:''}</div>
    <div class="chips">${prefs}</div>
    ${v.logo_valid===false
      ? `<div class="lfp bad" title="logo file matched this vendor by name but its mark reads as another brand — rejected, not trusted">⚠ logo rejected — read as \u2018${v.logo_reads||'?'}\u2019</div>`
      : (v.logo_reads ? `<div class="lfp" title="how the VLM read this vendor's reference logo">🏷 reads \u2018${v.logo_reads}\u2019${v.logo_typeface?` · ${v.logo_typeface}`:''}</div>` : '')}
    ${ex?`<div class="ex">${ex}</div>`:''}
    <div class="when" title="${v.updated_at||''}">🕓 ${fmtWhen(v.updated_at)||'—'}</div>
  </div>`;
}
function render(){
  const q=$('#q').value.trim().toUpperCase(), sort=$('#sort').value;
  let rows=ALL.filter(v=>!q || v.name.toUpperCase().includes(q) || Object.keys(v.prefixes||{}).some(p=>p.includes(q)));
  const cmp={ learned:(a,b)=>(b.learned_scans||0)-(a.learned_scans||0)||(b.n||0)-(a.n||0),
              n:(a,b)=>(b.n||0)-(a.n||0), az:(a,b)=>a.name.localeCompare(b.name),
              updated:(a,b)=>String(b.updated_at||'').localeCompare(String(a.updated_at||'')) };
  rows.sort(cmp[sort]||cmp.learned);
  $('#grid').innerHTML=rows.map(card).join('');
  $('#empty').hidden=rows.length>0; $('#empty').textContent=q?'No vendor matches that filter.':'Nothing learned yet — run build_vendor_profiles.py or scan a few samples.';
}
async function load(){
  try{
    const d=await (await fetch('/api/vendor-profiles')).json();
    ALL=d.vendors||[];
    const scans=ALL.reduce((s,v)=>s+(v.learned_scans||0),0);
    $('#sub').textContent=`${d.total} vendors learned · ${d.seeded.length} curated seed · ${scans} live scan refinements`;
    render();
  }catch(e){ $('#sub').textContent='Could not load profiles.'; }
}
$('#q').addEventListener('input',render);
$('#sort').addEventListener('change',()=>{ localStorage.setItem('learn_sort',$('#sort').value); render(); });
$('#dens').addEventListener('input',e=>{ document.documentElement.style.setProperty('--cols',e.target.value); localStorage.setItem('learn_dens',e.target.value); });
// restore persisted controls
$('#sort').value=LS('learn_sort','learned'); $('#dens').value=LS('learn_dens','3');
document.documentElement.style.setProperty('--cols',$('#dens').value);
load(); setInterval(load,15000);
</script>
</body>
</html>