Files
quote-judge-studio-v1/frontend/dist/index.html
2026-07-18 03:22:30 +00:00

1 line
10 KiB
HTML

<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>QuoteJudge Studio</title><style>body{margin:0;font-family:Inter,system-ui,sans-serif;background:#f5f2ea;color:#17201b}.wrap{width:min(1120px,calc(100% - 32px));margin:auto;padding:30px 0;display:grid;gap:20px}.hero,.grid{display:grid;grid-template-columns:1fr 340px;gap:20px}.card{background:#fffffff0;border:1px solid #d8e0da;border-radius:22px;padding:22px;box-shadow:0 18px 60px #1c2c2412}.eyebrow{color:#0b7f68;text-transform:uppercase;font-weight:800;font-size:12px;letter-spacing:.08em}h1{font-size:clamp(38px,7vw,68px);line-height:.94;letter-spacing:-.05em;margin:.1em 0}h2{margin:0 0 12px}.muted{color:#596860;line-height:1.5}.session{display:grid;gap:8px}.session a,button{border:0;border-radius:999px;background:#0b7f68;color:white;padding:10px 14px;text-decoration:none;font:inherit;cursor:pointer}button.secondary{background:#17201b}button.light{background:#e5efe9;color:#163d32}button:disabled{opacity:.55;cursor:not-allowed}label{display:grid;gap:7px;font-weight:700;color:#35443d}input{border:1px solid #cfd9d3;border-radius:12px;padding:10px;font:inherit}.quotes{display:grid;gap:8px;overflow:auto}.row{min-width:820px;display:grid;grid-template-columns:1.4fr .8fr .6fr .8fr .9fr auto;gap:8px;align-items:center}.head{font-size:12px;text-transform:uppercase;font-weight:800;color:#627068}.weights{position:sticky;top:16px;display:grid;gap:16px}.slider{grid-template-columns:80px 1fr 36px;align-items:center}.actions{display:flex;gap:10px;flex-wrap:wrap}.err{background:#fff0ed;color:#9d2412;border-radius:14px;padding:12px}.ok{background:linear-gradient(135deg,#0b7f68,#12241e);color:white;border-radius:18px;padding:18px}.bar{display:grid;grid-template-columns:170px 1fr 50px;gap:10px;align-items:center}.bar div{height:12px;background:#e3ebe6;border-radius:99px;overflow:hidden}.bar i{display:block;height:100%;background:#0b7f68}table{width:100%;border-collapse:collapse;background:white;border-radius:14px;overflow:hidden}td,th{padding:11px;border-bottom:1px solid #edf1ee;text-align:left}@media(max-width:850px){.hero,.grid{grid-template-columns:1fr}.weights{position:static}}</style></head><body><main id="app" class="wrap">Loading QuoteJudge...</main><script type="module">let cfg,session={authenticated:false},quotes=[{vendor:'Acme Bearings',unit_price:10.75,quantity:500,delivery_days:12,warranty_months:12},{vendor:'Beta Industrial',unit_price:9.1,quantity:500,delivery_days:8,warranty_months:18}],weights={price:50,delivery:30,warranty:20},result=null,error='',status='',uploadFile=null;const app=document.getElementById('app'),num=v=>{const n=Number(v);return Number.isFinite(n)?n:0};async function req(u,o={}){const r=await fetch(u,{credentials:'same-origin',...o});const t=await r.text(),d=t?JSON.parse(t):null;if(!r.ok)throw Error(d?.detail||d?.message||`HTTP ${r.status}`);return d}function signin(){const t=cfg?.auth?.authorizeUrl||cfg?.auth?.loginUrl;if(!t)return null;return `${t}${t.includes('?')?'&':'?'}next=${encodeURIComponent(location.pathname+location.search+location.hash)}`}async function call(name,args){if(cfg.auth?.invokeRequiresSession&&!session.authenticated)throw Error('sign in required');return req(`${cfg.endpoints.invoke}/${encodeURIComponent(name)}`,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({arguments:args})})}function cleanQuotes(){return quotes.map(q=>({vendor:String(q.vendor||'').trim(),unit_price:num(q.unit_price),quantity:Math.trunc(num(q.quantity)),delivery_days:Math.trunc(num(q.delivery_days)),warranty_months:Math.trunc(num(q.warranty_months))}))}function validate(id,rows){if(!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(id))return'Comparison ID must be 1-80 safe characters.';if(rows.length<2)return'Add at least two quotes.';const s=new Set;for(const q of rows){if(!q.vendor)return'Every quote needs a vendor.';if(s.has(q.vendor.toLowerCase()))return'Vendors must be unique.';s.add(q.vendor.toLowerCase());if(q.unit_price<=0||q.quantity<=0)return'Prices and quantities must be positive.';if(q.delivery_days<0||q.warranty_months<0)return'Delivery and warranty cannot be negative.'}if(num(weights.price)+num(weights.delivery)+num(weights.warranty)<=0)return'At least one weight must be greater than zero.';return''}async function b64(f){const b=new Uint8Array(await f.arrayBuffer());let s='';for(const x of b)s+=String.fromCharCode(x);return btoa(s)}function csv(text){const lines=text.split(/\r?\n/).filter(Boolean),h=lines[0].split(',').map(x=>x.trim());return lines.slice(1).map(line=>{const c=line.split(',').map(x=>x.trim()),r=Object.fromEntries(h.map((k,i)=>[k,c[i]||'']));return{vendor:r.vendor,unit_price:num(r.unit_price),quantity:num(r.quantity),delivery_days:num(r.delivery_days),warranty_months:num(r.warranty_months)}})}async function onUpload(f){uploadFile=f;error='';if(!f){render();return}if(f.size>65536){error='Upload must be at most 64 KiB.';render();return}try{const text=await f.text();const p=(f.type||'').includes('json')||f.name.endsWith('.json')?(JSON.parse(text).quotes||JSON.parse(text)):csv(text);if(Array.isArray(p))quotes=p}catch(e){error='Preview parse failed: '+e.message}render()}async function compare(e){e.preventDefault();const id=document.getElementById('cid').value.trim(),rows=cleanQuotes(),v=validate(id,rows);if(v){error=v;render();return}status='Saving comparison...';error='';render();try{const w={price:num(weights.price),delivery:num(weights.delivery),warranty:num(weights.warranty)};if(uploadFile){const media_type=uploadFile.type||(uploadFile.name.endsWith('.json')?'application/json':'text/csv');result=await call('compare_quotes_upload',{comparison_id:id,upload:{filename:uploadFile.name,media_type,data_base64:await b64(uploadFile)},weights:w})}else result=await call('compare_quotes',{comparison_id:id,quotes:rows,weights:w});if(!result.ok)error=result.message||result.code}catch(e){error=/sign in/i.test(e.message)?'Sign in to run, then retry.':e.message}status='';render()}async function reload(){status='Reopening saved comparison...';error='';render();try{result=await call('get_comparison',{comparison_id:document.getElementById('cid').value.trim()});if(!result.ok)error=result.message||result.code}catch(e){error=e.message}status='';render()}function render(){const need=cfg?.auth?.invokeRequiresSession&&session&&!session.authenticated,href=signin(),win=result?.ok?result.recommendation:null;app.innerHTML=`<section class="hero"><div><p class="eyebrow">QuoteJudge</p><h1>Pick the best vendor quote with transparent weighted scoring.</h1><p class="muted">Paste or upload vendor quotes, weight price/delivery/warranty, compare them, persist the recommendation, and reopen it later under your A2A Cloud user account.</p></div><aside class="card session"><span>${session.authenticated?'Signed in':'Signed out'}</span><b>${session?.user?.email||session?.email||'A2A Cloud user'}</b>${need&&href?`<a href="${href}">Sign in to run</a>`:''}<small>${cfg.agent.name} v${cfg.agent.version} · MCP /mcp</small></aside></section><form class="grid" id="form"><section class="card"><h2>Quotes</h2><label>Comparison ID<input id="cid" value="studio-quote-v1"></label><p><input id="file" type="file" accept=".csv,.json,text/csv,text/plain,application/json"> ${uploadFile?'<button class="light" type="button" id="clear">Use pasted rows</button>':''}</p><div class="quotes"><div class="row head"><span>Vendor</span><span>Unit price</span><span>Qty</span><span>Delivery</span><span>Warranty</span><span></span></div>${quotes.map((q,i)=>`<div class="row"><input data-i="${i}" data-f="vendor" value="${String(q.vendor).replaceAll('"','&quot;')}"><input data-i="${i}" data-f="unit_price" type="number" step="0.01" value="${q.unit_price}"><input data-i="${i}" data-f="quantity" type="number" value="${q.quantity}"><input data-i="${i}" data-f="delivery_days" type="number" value="${q.delivery_days}"><input data-i="${i}" data-f="warranty_months" type="number" value="${q.warranty_months}"><button class="light" type="button" data-r="${i}">Remove</button></div>`).join('')}</div><p><button class="light" type="button" id="add">Add quote</button></p></section><section class="card weights"><h2>Weights</h2>${['price','delivery','warranty'].map(k=>`<label class="slider">${k}<input type="range" min="0" max="100" data-w="${k}" value="${weights[k]}"><output>${weights[k]}</output></label>`).join('')}<div class="actions"><button ${need?'disabled':''}>Compare & save</button><button class="secondary" ${need?'disabled':''} type="button" id="reload">Reopen saved</button></div>${status?`<p class="muted">${status}</p>`:''}${error?`<p class="err">${error}</p>`:''}</section></form><section class="card"><p class="eyebrow">Recommendation</p><h2>${win?win.vendor:'No comparison yet'}</h2>${result?.reloaded?'<b>Reloaded from Postgres</b>':''}${result?.ok?`<div class="ok"><h2>${win.vendor}</h2><p>${win.weighted_score.toFixed(1)} weighted score</p><p>${win.rationale}</p></div><p></p>${result.quotes.map(q=>`<div class="bar"><span>${q.vendor}</span><div><i style="width:${Math.min(100,Math.max(0,q.weighted_score))}%"></i></div><b>${q.weighted_score.toFixed(1)}</b></div>`).join('')}<p></p><table><tr><th>Vendor</th><th>Total price</th><th>Delivery</th><th>Warranty</th><th>Score</th></tr>${result.quotes.map(q=>`<tr><td>${q.vendor}</td><td>${q.total_price.toLocaleString(undefined,{style:'currency',currency:'BRL'})}</td><td>${q.delivery_days} days</td><td>${q.warranty_months} mo</td><td>${q.weighted_score.toFixed(1)}</td></tr>`).join('')}</table><p class="muted">Receipt persisted: ${String(result.receipt?.persisted)} · comparison_id=${result.comparison_id}</p>`:'<p class="muted">Run the success fixture or upload your own CSV/JSON.</p>'}</section>`;document.getElementById('form').onsubmit=compare;document.getElementById('reload').onclick=reload;document.getElementById('add').onclick=()=>{quotes.push({vendor:`Vendor ${quotes.length+1}`,unit_price:0,quantity:1,delivery_days:0,warranty_months:0});render()};document.getElementById('file').onchange=e=>onUpload(e.target.files?.[0]);document.getElementById('clear')?.addEventListener('click',()=>{uploadFile=null;render()});document.querySelectorAll('[data-i]').forEach(el=>el.oninput=e=>{quotes[+e.target.dataset.i][e.target.dataset.f]=e.target.value});document.querySelectorAll('[data-r]').forEach(el=>el.onclick=e=>{quotes=quotes.filter((_,i)=>i!==+e.target.dataset.r);render()});document.querySelectorAll('[data-w]').forEach(el=>el.oninput=e=>{weights[e.target.dataset.w]=+e.target.value;render()})}try{cfg=await req('./config.json');try{session=await req(cfg.endpoints.session)}catch{}render()}catch(e){app.textContent=e.message}</script></body></html>