Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>PriceOye AI Phone Advisor</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet"/> | |
| <style> | |
| :root{--red:#E8192C;--dark:#0D0D0F;--card:#161618;--border:#2a2a2e;--muted:#6b6b75;--text:#f0f0f2;--green:#22C55E;--amber:#F59E0B;--blue:#3B82F6;--r:14px;} | |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;} | |
| html,body{height:100%;font-family:'Sora',sans-serif;background:var(--dark);color:var(--text);overflow:hidden;} | |
| body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse 80% 60% at 10% 0%,rgba(232,25,44,.10) 0%,transparent 60%),radial-gradient(ellipse 60% 50% at 90% 100%,rgba(59,130,246,.07) 0%,transparent 60%);pointer-events:none;z-index:0;} | |
| #app{display:flex;flex-direction:column;height:100vh;position:relative;z-index:1;max-width:860px;margin:0 auto;} | |
| /* HEADER */ | |
| header{display:flex;align-items:center;gap:12px;padding:14px 20px;border-bottom:1px solid var(--border);background:rgba(13,13,15,.9);backdrop-filter:blur(16px);flex-shrink:0;} | |
| .logo{display:flex;align-items:center;gap:10px;} | |
| .logo-icon{width:36px;height:36px;background:var(--red);border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:17px;} | |
| .logo-text{font-weight:800;font-size:17px;letter-spacing:-.5px;}.logo-text span{color:var(--red);} | |
| .hdr-actions{margin-left:auto;display:flex;align-items:center;gap:8px;} | |
| .hdr-btn{background:transparent;border:1px solid var(--border);color:var(--muted);padding:6px 12px;border-radius:8px;font-size:12px;font-family:'Sora',sans-serif;cursor:pointer;transition:.2s;} | |
| .hdr-btn:hover{border-color:var(--red);color:var(--text);} | |
| .status-dot{width:7px;height:7px;background:var(--green);border-radius:50%;animation:pulse 2s infinite;} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}} | |
| /* MESSAGES */ | |
| #messages{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:14px;scroll-behavior:smooth;} | |
| #messages::-webkit-scrollbar{width:3px;} | |
| #messages::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px;} | |
| .msg{display:flex;gap:10px;animation:fadeUp .3s ease;max-width:100%;} | |
| @keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}} | |
| .msg.user{flex-direction:row-reverse;} | |
| .avatar{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0;margin-top:2px;} | |
| .bot-avatar{background:linear-gradient(135deg,var(--red),#ff4f64);} | |
| .user-avatar{background:linear-gradient(135deg,#3B82F6,#6366f1);} | |
| .bubble{max-width:80%;padding:11px 15px;border-radius:var(--r);font-size:14px;line-height:1.65;} | |
| .msg.bot .bubble{background:var(--card);border:1px solid var(--border);border-top-left-radius:3px;} | |
| .msg.user .bubble{background:linear-gradient(135deg,var(--red),#c0102a);border-top-right-radius:3px;color:#fff;} | |
| /* QUICK REPLIES */ | |
| .qrs{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px;} | |
| .qr{background:transparent;border:1px solid var(--border);color:var(--text);padding:6px 13px;border-radius:999px;font-size:12.5px;font-family:'Sora',sans-serif;cursor:pointer;transition:.2s;white-space:nowrap;} | |
| .qr:hover{background:var(--red);border-color:var(--red);color:#fff;transform:translateY(-1px);} | |
| /* TYPING */ | |
| .typing{display:flex;gap:5px;align-items:center;padding:3px 0;} | |
| .typing span{width:7px;height:7px;background:var(--muted);border-radius:50%;animation:bounce 1.2s infinite;} | |
| .typing span:nth-child(2){animation-delay:.2s;}.typing span:nth-child(3){animation-delay:.4s;} | |
| @keyframes bounce{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-6px)}} | |
| /* PHONE CARD */ | |
| .phone-card{background:var(--card);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;margin-top:8px;max-width:440px;} | |
| .pc-header{background:linear-gradient(135deg,rgba(232,25,44,.12),rgba(59,130,246,.06));padding:14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:11px;} | |
| .pc-emoji{font-size:34px;}.pc-meta h3{font-size:15px;font-weight:700;letter-spacing:-.3px;}.pc-meta .price{font-size:13px;color:var(--red);font-weight:600;margin-top:2px;} | |
| .ai-badge{margin-left:auto;border:2px solid;border-radius:10px;padding:5px 10px;text-align:center;min-width:58px;} | |
| .ai-badge .score-n{font-size:19px;font-weight:800;font-family:'JetBrains Mono',monospace;} | |
| .ai-badge .score-l{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;} | |
| .pc-highlights{padding:10px 14px;display:flex;flex-wrap:wrap;gap:5px;border-bottom:1px solid var(--border);} | |
| .hi-chip{background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:7px;padding:3px 7px;font-size:10.5px;} | |
| .hi-key{color:var(--muted);margin-right:4px;text-transform:capitalize;}.hi-val{color:var(--text);font-weight:500;} | |
| .sec-lbl{padding:7px 14px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);background:rgba(255,255,255,.02);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:6px;user-select:none;} | |
| .sec-lbl.clickable{cursor:pointer;}.toggle-arrow{margin-left:auto;font-size:9px;} | |
| .cat-scores{padding:10px 14px;display:flex;flex-direction:column;gap:5px;} | |
| .cat-row{display:flex;align-items:center;gap:7px;} | |
| .cat-icon{font-size:12px;width:18px;text-align:center;}.cat-name{font-size:11px;color:var(--muted);width:76px;flex-shrink:0;} | |
| .cat-bar{flex:1;height:4px;background:var(--border);border-radius:4px;overflow:hidden;} | |
| .cat-bar-f{height:100%;border-radius:4px;transition:width .8s ease;} | |
| .cat-score{font-size:11px;font-weight:700;font-family:'JetBrains Mono',monospace;width:26px;text-align:right;} | |
| .deep-dive{display:none;padding:10px 14px;flex-direction:column;gap:7px;border-bottom:1px solid var(--border);} | |
| .sub-row{display:flex;flex-direction:column;gap:2px;} | |
| .sub-label{font-size:11px;font-weight:600;}.sub-note{font-size:10.5px;color:var(--muted);line-height:1.4;} | |
| .sub-sw{display:flex;align-items:center;gap:7px;margin-top:2px;} | |
| .sub-bar{flex:1;height:3px;background:var(--border);border-radius:3px;overflow:hidden;} | |
| .sub-bar-f{height:100%;border-radius:3px;transition:width .8s ease;} | |
| .sub-num{font-size:10px;font-weight:700;font-family:'JetBrains Mono',monospace;width:16px;} | |
| .pc-footer{padding:10px 14px;border-top:1px solid var(--border);display:flex;gap:8px;} | |
| .btn-po{flex:1;background:var(--red);color:#fff;border:none;border-radius:9px;padding:9px;font-family:'Sora',sans-serif;font-size:12px;font-weight:600;cursor:pointer;transition:.2s;} | |
| .btn-po:hover{background:#c0102a;transform:translateY(-1px);} | |
| .btn-wm{background:transparent;border:1px solid var(--border);color:var(--text);border-radius:9px;padding:9px 12px;font-family:'Sora',sans-serif;font-size:12px;cursor:pointer;transition:.2s;} | |
| .btn-wm:hover{border-color:var(--muted);} | |
| .btn-cmp{background:rgba(59,130,246,.12);border:1px solid rgba(59,130,246,.3);color:#93c5fd;border-radius:9px;padding:9px 12px;font-family:'Sora',sans-serif;font-size:12px;cursor:pointer;transition:.2s;} | |
| .btn-cmp:hover{background:rgba(59,130,246,.2);} | |
| /* COMPARISON MODAL */ | |
| #cmp-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:100;align-items:center;justify-content:center;padding:20px;} | |
| #cmp-modal.open{display:flex;} | |
| .cmp-box{background:var(--card);border:1px solid var(--border);border-radius:18px;width:100%;max-width:800px;max-height:90vh;overflow-y:auto;animation:fadeUp .3s ease;} | |
| .cmp-head{padding:18px 22px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;} | |
| .cmp-head h2{font-size:16px;font-weight:700;} | |
| .cmp-close{background:transparent;border:1px solid var(--border);color:var(--text);width:32px;height:32px;border-radius:8px;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;} | |
| .cmp-body{padding:18px 22px;} | |
| .cmp-phones{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:18px;} | |
| .cmp-phone-col{text-align:center;} | |
| .cmp-phone-name{font-size:14px;font-weight:700;margin-bottom:4px;} | |
| .cmp-phone-price{font-size:12px;color:var(--red);font-weight:600;} | |
| .cmp-phone-score{font-size:28px;font-weight:800;font-family:'JetBrains Mono',monospace;margin:8px 0;} | |
| .cmp-winner-tag{font-size:11px;padding:3px 10px;border-radius:999px;background:rgba(34,197,94,.15);color:var(--green);font-weight:600;} | |
| .cmp-winner-tag.red{background:rgba(232,25,44,.15);color:var(--red);} | |
| .cmp-cats{display:flex;flex-direction:column;gap:8px;} | |
| .cmp-cat-row{display:grid;grid-template-columns:70px 1fr 60px 1fr 70px;align-items:center;gap:8px;} | |
| .cmp-cat-name{font-size:11px;color:var(--muted);text-align:center;} | |
| .cmp-bar-wrap{display:flex;align-items:center;gap:6px;} | |
| .cmp-bar-wrap.right{flex-direction:row-reverse;} | |
| .cmp-bar-bg{flex:1;height:5px;background:var(--border);border-radius:4px;overflow:hidden;} | |
| .cmp-bar-fill{height:100%;border-radius:4px;transition:width .8s ease;} | |
| .cmp-score-val{font-size:11px;font-weight:700;font-family:'JetBrains Mono',monospace;width:28px;} | |
| .cmp-score-val.right{text-align:right;} | |
| .cmp-winner-indicator{width:8px;height:8px;border-radius:50%;flex-shrink:0;} | |
| .cmp-footer{padding:14px 22px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px;} | |
| .cmp-priority-sel{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px;} | |
| .cmp-pri-btn{background:transparent;border:1px solid var(--border);color:var(--muted);padding:5px 12px;border-radius:999px;font-size:12px;font-family:'Sora',sans-serif;cursor:pointer;transition:.2s;} | |
| .cmp-pri-btn.active{background:rgba(232,25,44,.15);border-color:var(--red);color:var(--red);} | |
| .cmp-verdict{background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.2);border-radius:10px;padding:12px;margin-top:14px;font-size:13px;line-height:1.6;} | |
| /* IMAGE DETECT */ | |
| .detect-result{background:rgba(59,130,246,.08);border:1px solid rgba(59,130,246,.2);border-radius:10px;padding:12px;margin-top:8px;font-size:13px;} | |
| .detect-result .detected-name{font-size:16px;font-weight:700;margin-bottom:4px;} | |
| .detect-links{display:flex;gap:8px;margin-top:8px;} | |
| .detect-link{background:transparent;border:1px solid var(--border);color:var(--text);padding:6px 12px;border-radius:8px;font-size:12px;cursor:pointer;text-decoration:none;transition:.2s;font-family:'Sora',sans-serif;} | |
| .detect-link:hover{border-color:var(--blue);color:#93c5fd;} | |
| /* INPUT AREA */ | |
| #input-area{padding:12px 20px 16px;border-top:1px solid var(--border);background:rgba(13,13,15,.9);backdrop-filter:blur(16px);flex-shrink:0;} | |
| .input-row{display:flex;gap:8px;align-items:flex-end;} | |
| .input-actions{display:flex;gap:6px;flex-shrink:0;align-items:flex-end;} | |
| .act-btn{width:42px;height:42px;border-radius:11px;border:1px solid var(--border);background:transparent;color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:17px;transition:.2s;flex-shrink:0;} | |
| .act-btn:hover{border-color:var(--muted);color:var(--text);} | |
| #user-input{flex:1;background:var(--card);border:1px solid var(--border);border-radius:12px;padding:11px 14px;color:var(--text);font-family:'Sora',sans-serif;font-size:13.5px;resize:none;outline:none;transition:border-color .2s;min-height:44px;max-height:110px;line-height:1.5;} | |
| #user-input:focus{border-color:var(--red);} | |
| #user-input::placeholder{color:var(--muted);} | |
| #send-btn{width:44px;height:44px;background:var(--red);border:none;border-radius:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:.2s;flex-shrink:0;} | |
| #send-btn:hover{background:#c0102a;transform:scale(1.04);} | |
| #send-btn svg{width:18px;height:18px;fill:#fff;} | |
| #file-input{display:none;} | |
| .hint{font-size:10.5px;color:var(--muted);margin-top:7px;text-align:center;} | |
| /* MEDAL */ | |
| .medal{font-size:11px;font-weight:700;color:var(--amber);margin-bottom:6px;} | |
| @media(max-width:500px){.bubble{max-width:92%;}.cmp-phones{grid-template-columns:1fr 1fr;}.cmp-cat-row{grid-template-columns:55px 1fr 44px 1fr 55px;}} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <header> | |
| <div class="logo"> | |
| <div class="logo-icon">π±</div> | |
| <div class="logo-text"><span>Price</span>Oye AI</div> | |
| </div> | |
| <div class="hdr-actions"> | |
| <button class="hdr-btn" onclick="openCompareModal()">βοΈ Compare</button> | |
| <button class="hdr-btn" onclick="resetChat()">π Reset</button> | |
| <div class="status-dot" title="AI Online"></div> | |
| </div> | |
| </header> | |
| <div id="messages"></div> | |
| <div id="input-area"> | |
| <div class="input-row"> | |
| <div class="input-actions"> | |
| <button class="act-btn" title="Upload phone image to detect" onclick="document.getElementById('file-input').click()">π·</button> | |
| <input type="file" id="file-input" accept="image/*" onchange="handleImageUpload(event)"/> | |
| </div> | |
| <textarea id="user-input" rows="1" placeholder="Budget, OS, use case batayeinβ¦ ya phone image upload karein π·"></textarea> | |
| <button id="send-btn" onclick="send()"> | |
| <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> | |
| </button> | |
| </div> | |
| <div class="hint">Data: WhatMobile.com.pk Β· PriceOye.pk Β· Budget: 9k β 5 lakh PKR</div> | |
| </div> | |
| </div> | |
| <!-- COMPARISON MODAL --> | |
| <div id="cmp-modal"> | |
| <div class="cmp-box"> | |
| <div class="cmp-head"> | |
| <h2>βοΈ Phone Comparison</h2> | |
| <button class="cmp-close" onclick="closeCompare()">β</button> | |
| </div> | |
| <div class="cmp-body" id="cmp-body"> | |
| <div style="color:var(--muted);font-size:13px;margin-bottom:14px">Do phones ka naam likhein compare karne ke liye:</div> | |
| <div style="display:flex;gap:10px;margin-bottom:12px"> | |
| <input id="cmp-a" placeholder="e.g. Samsung A55" style="flex:1;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 14px;color:var(--text);font-family:Sora,sans-serif;font-size:13px;outline:none"/> | |
| <div style="display:flex;align-items:center;color:var(--muted);font-weight:700">VS</div> | |
| <input id="cmp-b" placeholder="e.g. Nothing Phone 3a" style="flex:1;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 14px;color:var(--text);font-family:Sora,sans-serif;font-size:13px;outline:none"/> | |
| </div> | |
| <div id="cmp-results"></div> | |
| </div> | |
| <div class="cmp-footer"> | |
| <button class="btn-wm" onclick="closeCompare()">BΩΨ― Ϊ©Ψ±ΫΪΊ</button> | |
| <button class="btn-po" onclick="runCompare()">Compare Karein βοΈ</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ββ CONFIG ββββββββββββββββββββββββββββββββββββββββββββββ | |
| const API = ''; // empty = same origin (FastAPI backend) | |
| let SESSION_ID = null; | |
| // ββ PHONE DB cache (loaded from /phones) ββββββββββββββββββ | |
| let PHONE_DB_CACHE = []; | |
| async function loadPhones() { | |
| try { | |
| const r = await fetch(`${API}/phones`); | |
| PHONE_DB_CACHE = await r.json(); | |
| } catch(e) { console.warn('Could not load phone DB:', e); } | |
| } | |
| // ββ SCORE COLOR ββββββββββββββββββββββββββββββββββββββββββ | |
| function sc(s) { | |
| if (s >= 8.5) return '#22C55E'; | |
| if (s >= 7) return '#F59E0B'; | |
| if (s >= 5.5) return '#3B82F6'; | |
| return '#EF4444'; | |
| } | |
| // ββ UI HELPERS ββββββββββββββββββββββββββββββββββββββββββββ | |
| function appendMsg(type, html, qrs = []) { | |
| const msgs = document.getElementById('messages'); | |
| const wrap = document.createElement('div'); | |
| wrap.className = `msg ${type}`; | |
| const av = document.createElement('div'); | |
| av.className = `avatar ${type}-avatar`; | |
| av.textContent = type === 'bot' ? 'π€' : 'π€'; | |
| const b = document.createElement('div'); | |
| b.className = 'bubble'; | |
| b.innerHTML = html; | |
| if (qrs.length) { | |
| const qDiv = document.createElement('div'); | |
| qDiv.className = 'qrs'; | |
| qrs.forEach(label => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'qr'; btn.textContent = label; | |
| btn.onclick = () => send(label); | |
| qDiv.appendChild(btn); | |
| }); | |
| b.appendChild(qDiv); | |
| } | |
| wrap.appendChild(av); wrap.appendChild(b); | |
| msgs.appendChild(wrap); | |
| msgs.scrollTop = msgs.scrollHeight; | |
| } | |
| function showTyping() { | |
| const msgs = document.getElementById('messages'); | |
| const d = document.createElement('div'); | |
| d.className = 'msg bot'; d.id = 'typing-ind'; | |
| const av = document.createElement('div'); | |
| av.className = 'avatar bot-avatar'; av.textContent = 'π€'; | |
| const b = document.createElement('div'); | |
| b.className = 'bubble'; | |
| b.innerHTML = '<div class="typing"><span></span><span></span><span></span></div>'; | |
| d.appendChild(av); d.appendChild(b); | |
| msgs.appendChild(d); | |
| msgs.scrollTop = msgs.scrollHeight; | |
| } | |
| function removeTyping() { const e = document.getElementById('typing-ind'); if(e) e.remove(); } | |
| // ββ PHONE CARD BUILDER ββββββββββββββββββββββββββββββββββββ | |
| function buildCard(p, medal) { | |
| const score = p.ai_score ?? p.priority_score ?? 0; | |
| const color = sc(score); | |
| const cats = p.category_scores || {}; | |
| const CAT_META = {camera:'πΈ Camera',performance:'β‘ Perf',display:'π₯οΈ Display',battery:'π Battery',charging:'π Charging',ram:'π§ RAM',storage:'πΎ Storage',build:'ποΈ Build',software:'π² Software',audio:'π Audio'}; | |
| const hiHtml = Object.entries(p.highlights||{}).map(([k,v]) => | |
| `<div class="hi-chip"><span class="hi-key">${k}</span><span class="hi-val">${v}</span></div>`).join(''); | |
| const catHtml = Object.entries(CAT_META).map(([k,label]) => { | |
| const s = cats[k] ?? 5; | |
| const c = sc(s); | |
| return `<div class="cat-row"> | |
| <span class="cat-icon">${label.split(' ')[0]}</span> | |
| <span class="cat-name">${label.split(' ').slice(1).join(' ')}</span> | |
| <div class="cat-bar"><div class="cat-bar-f" style="width:${s*10}%;background:${c}"></div></div> | |
| <span class="cat-score" style="color:${c}">${s.toFixed(1)}</span> | |
| </div>`; | |
| }).join(''); | |
| // Deep dive | |
| const deepCat = p.deep_dive_category || 'camera'; | |
| const deepScores = p.deep_dive_scores || []; | |
| const deepHtml = deepScores.map(d => { | |
| const c = sc(d.score); | |
| return `<div class="sub-row"> | |
| <div class="sub-label">${d.label}</div> | |
| <div class="sub-note">${d.note}</div> | |
| <div class="sub-sw"> | |
| <div class="sub-bar"><div class="sub-bar-f" style="width:${d.score*10}%;background:${c}"></div></div> | |
| <span class="sub-num" style="color:${c}">${d.score}</span> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| const card = document.createElement('div'); | |
| card.className = 'phone-card'; | |
| card.dataset.phoneId = p.id; | |
| card.innerHTML = ` | |
| ${medal ? `<div class="medal" style="padding:8px 14px 0">${medal}</div>` : ''} | |
| <div class="pc-header"> | |
| <div class="pc-emoji">${p.emoji}</div> | |
| <div class="pc-meta"><h3>${p.name}</h3><div class="price">${p.price_label}</div></div> | |
| <div class="ai-badge" style="border-color:${color}"> | |
| <div class="score-n" style="color:${color}">${score.toFixed(1)}</div> | |
| <div class="score-l">AI Score</div> | |
| </div> | |
| </div> | |
| <div class="pc-highlights">${hiHtml}</div> | |
| <div class="sec-lbl">π Category Scores</div> | |
| <div class="cat-scores">${catHtml}</div> | |
| ${deepHtml ? ` | |
| <div class="sec-lbl clickable" onclick="toggleDeep(this)"> | |
| π ${deepCat.charAt(0).toUpperCase()+deepCat.slice(1)} Deep Dive | |
| <span class="toggle-arrow">βΌ</span> | |
| </div> | |
| <div class="deep-dive">${deepHtml}</div>` : ''} | |
| <div class="pc-footer"> | |
| <button class="btn-po" onclick="window.open('${p.priceoye_url}','_blank')">PriceOye π</button> | |
| <button class="btn-wm" onclick="window.open('${p.whatmobile_url}','_blank')">WhatMobile π</button> | |
| <button class="btn-cmp" onclick="compareWithThis('${p.id}','${p.name}')">βοΈ Compare</button> | |
| </div>`; | |
| return card; | |
| } | |
| function appendPhoneCard(p, medal) { | |
| const msgs = document.getElementById('messages'); | |
| const wrap = document.createElement('div'); | |
| wrap.className = 'msg bot'; | |
| const av = document.createElement('div'); | |
| av.className = 'avatar bot-avatar'; av.textContent = 'π€'; | |
| const b = document.createElement('div'); | |
| b.className = 'bubble'; | |
| b.appendChild(buildCard(p, medal)); | |
| wrap.appendChild(av); wrap.appendChild(b); | |
| msgs.appendChild(wrap); | |
| msgs.scrollTop = msgs.scrollHeight; | |
| } | |
| function toggleDeep(el) { | |
| const d = el.nextElementSibling; | |
| const a = el.querySelector('.toggle-arrow'); | |
| const open = d.style.display === 'flex'; | |
| d.style.display = open ? 'none' : 'flex'; | |
| d.style.flexDirection = 'column'; | |
| d.style.gap = '7px'; | |
| a.textContent = open ? 'βΌ' : 'β²'; | |
| } | |
| // ββ CHAT βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function send(overrideText) { | |
| const inp = document.getElementById('user-input'); | |
| const text = overrideText ?? inp.value.trim(); | |
| if (!text) return; | |
| if (!overrideText) { inp.value = ''; inp.style.height = 'auto'; } | |
| appendMsg('user', text); | |
| showTyping(); | |
| try { | |
| const r = await fetch(`${API}/chat`, { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ session_id: SESSION_ID, message: text }) | |
| }); | |
| const data = await r.json(); | |
| SESSION_ID = data.session_id; | |
| removeTyping(); | |
| appendMsg('bot', data.text || '', data.quick_replies || []); | |
| if (data.phones && data.phones.length) { | |
| const medals = ['π₯ Behtar in Sab Mein','π₯ Doosra Best','π₯ Aur Ek Option','4οΈβ£ Yeh Bhi']; | |
| data.phones.forEach((p, i) => { | |
| setTimeout(() => appendPhoneCard(p, p.medal || medals[i] || ''), i * 400); | |
| }); | |
| if (data.followup) { | |
| setTimeout(() => appendMsg('bot', data.followup, data.quick_replies || []), | |
| data.phones.length * 400 + 500); | |
| } | |
| } | |
| } catch(e) { | |
| removeTyping(); | |
| appendMsg('bot', 'Maafi, server se connection nahi ho raha. Please thodi der baad try karein.', ['Dobara Try Karein']); | |
| } | |
| } | |
| // ββ IMAGE UPLOAD & PHONE DETECTION ββββββββββββββββββββββββ | |
| async function handleImageUpload(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| event.target.value = ''; | |
| appendMsg('user', `π· Image upload ho rahi hai: <b>${file.name}</b>`); | |
| showTyping(); | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const r = await fetch(`${API}/detect`, { method: 'POST', body: formData }); | |
| const data = await r.json(); | |
| removeTyping(); | |
| if (!data.brand && !data.model) { | |
| appendMsg('bot', | |
| 'Is image mein koi phone nahi dikha ya detect nahi ho saka. Please ek clear phone photo upload karein.', | |
| ['Camera phone 1 lakh mein', 'Gaming phone 2 lakh mein'] | |
| ); | |
| return; | |
| } | |
| const conf = data.confidence === 'high' ? 'β High Confidence' : | |
| data.confidence === 'medium' ? 'β οΈ Medium Confidence' : 'β Low Confidence'; | |
| const msgs = document.getElementById('messages'); | |
| const wrap = document.createElement('div'); | |
| wrap.className = 'msg bot'; | |
| const av = document.createElement('div'); | |
| av.className = 'avatar bot-avatar'; av.textContent = 'π€'; | |
| const b = document.createElement('div'); | |
| b.className = 'bubble'; | |
| let html = `Image mein yeh phone detect hua:<br>`; | |
| html += `<div class="detect-result">`; | |
| html += `<div class="detected-name">${data.brand || ''} ${data.model || ''}</div>`; | |
| html += `<div style="font-size:11px;color:var(--muted)">${conf}</div>`; | |
| if (data.matched_in_db) { | |
| const m = data.matched_in_db; | |
| html += `<div style="margin-top:8px;font-size:12px">π Hamare database mein hai: <b>${m.name}</b> β ${m.price_label}</div>`; | |
| } else { | |
| html += `<div style="margin-top:8px;font-size:12px;color:var(--muted)">Yeh phone hamare database mein nahi hai β links check karein:</div>`; | |
| } | |
| html += `<div class="detect-links"> | |
| <a class="detect-link" href="${data.search_url || '#'}" target="_blank">PriceOye pe Dekho π</a> | |
| <a class="detect-link" href="${data.whatmobile_url || '#'}" target="_blank">WhatMobile π</a> | |
| </div></div>`; | |
| html += `<br><small style="color:var(--muted)">Kya aap is phone aur ek recommended phone ka comparison dekhna chahte hain?</small>`; | |
| b.innerHTML = html; | |
| // Add quick replies | |
| const qDiv = document.createElement('div'); | |
| qDiv.className = 'qrs'; | |
| const qrs = []; | |
| if (data.matched_in_db) { | |
| qrs.push(`${data.matched_in_db.name} ko Compare Karein`); | |
| } | |
| qrs.push('Is se Behtar Phone Batao', 'Naya Search'); | |
| qrs.forEach(label => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'qr'; btn.textContent = label; | |
| btn.onclick = () => { | |
| if (label.includes('Compare')) { | |
| compareWithThis(data.matched_in_db?.id, data.matched_in_db?.name); | |
| } else { | |
| send(label); | |
| } | |
| }; | |
| qDiv.appendChild(btn); | |
| }); | |
| b.appendChild(qDiv); | |
| wrap.appendChild(av); wrap.appendChild(b); | |
| msgs.appendChild(wrap); | |
| msgs.scrollTop = msgs.scrollHeight; | |
| } catch(e) { | |
| removeTyping(); | |
| appendMsg('bot', 'Image detect karne mein masla aaya. ANTHROPIC_API_KEY set karein HF Secrets mein.', []); | |
| } | |
| } | |
| // ββ COMPARISON ββββββββββββββββββββββββββββββββββββββββββββ | |
| let comparePreFill = null; | |
| function openCompareModal() { | |
| document.getElementById('cmp-modal').classList.add('open'); | |
| document.getElementById('cmp-results').innerHTML = ''; | |
| if (comparePreFill) { | |
| document.getElementById('cmp-a').value = comparePreFill; | |
| comparePreFill = null; | |
| } | |
| } | |
| function closeCompare() { | |
| document.getElementById('cmp-modal').classList.remove('open'); | |
| } | |
| function compareWithThis(phoneId, phoneName) { | |
| comparePreFill = phoneName || phoneId; | |
| openCompareModal(); | |
| document.getElementById('cmp-b').focus(); | |
| } | |
| async function runCompare() { | |
| const nameA = document.getElementById('cmp-a').value.trim(); | |
| const nameB = document.getElementById('cmp-b').value.trim(); | |
| if (!nameA || !nameB) { | |
| document.getElementById('cmp-results').innerHTML = | |
| '<div style="color:var(--red);font-size:13px">Dono phone ka naam zaroor likhein.</div>'; | |
| return; | |
| } | |
| // Find IDs from cache by fuzzy name match | |
| function findId(name) { | |
| const n = name.toLowerCase(); | |
| const exact = PHONE_DB_CACHE.find(p => p.name.toLowerCase() === n); | |
| if (exact) return exact.id; | |
| const partial = PHONE_DB_CACHE.find(p => | |
| p.name.toLowerCase().includes(n) || n.includes(p.name.toLowerCase().split(' ')[1] || '') | |
| ); | |
| return partial?.id; | |
| } | |
| const idA = findId(nameA); | |
| const idB = findId(nameB); | |
| if (!idA) { | |
| document.getElementById('cmp-results').innerHTML = | |
| `<div style="color:var(--red);font-size:13px">"${nameA}" hamare database mein nahi mila.</div>`; | |
| return; | |
| } | |
| if (!idB) { | |
| document.getElementById('cmp-results').innerHTML = | |
| `<div style="color:var(--red);font-size:13px">"${nameB}" hamare database mein nahi mila.</div>`; | |
| return; | |
| } | |
| document.getElementById('cmp-results').innerHTML = | |
| '<div class="typing" style="justify-content:center"><span></span><span></span><span></span></div>'; | |
| try { | |
| const r = await fetch(`${API}/compare`, { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ phone_id_a: idA, phone_id_b: idB, priority: 'balanced' }) | |
| }); | |
| const data = await r.json(); | |
| renderComparison(data); | |
| } catch(e) { | |
| document.getElementById('cmp-results').innerHTML = | |
| '<div style="color:var(--red);font-size:13px">Comparison mein error. Dobara try karein.</div>'; | |
| } | |
| } | |
| function renderComparison(data) { | |
| const { phone_a: a, phone_b: b, winners, overall_winner, priority_winner } = data; | |
| const aWin = overall_winner === 'a'; | |
| const cats = Object.keys(a.category_scores); | |
| const catLabels = {camera:'πΈ Camera',performance:'β‘ Performance',display:'π₯οΈ Display', | |
| battery:'π Battery',charging:'π Charging',ram:'π§ RAM', | |
| storage:'πΎ Storage',build:'ποΈ Build',software:'π² Software',audio:'π Audio'}; | |
| const catRows = cats.map(cat => { | |
| const sa = a.category_scores[cat]; | |
| const sb = b.category_scores[cat]; | |
| const w = winners[cat]; | |
| const ca = sc(sa), cb = sc(sb); | |
| return `<div class="cmp-cat-row"> | |
| <div class="cmp-bar-wrap right"> | |
| <div class="cmp-bar-bg"><div class="cmp-bar-fill" style="width:${sa*10}%;background:${ca}"></div></div> | |
| <span class="cmp-score-val right" style="color:${ca}">${sa.toFixed(1)}</span> | |
| <div class="cmp-winner-indicator" style="background:${w==='a'?'#22C55E':w==='tie'?'#F59E0B':'transparent'}"></div> | |
| </div> | |
| <div class="cmp-cat-name">${catLabels[cat]||cat}</div> | |
| <div class="cmp-bar-wrap"> | |
| <div class="cmp-winner-indicator" style="background:${w==='b'?'#22C55E':w==='tie'?'#F59E0B':'transparent'}"></div> | |
| <span class="cmp-score-val" style="color:${cb}">${sb.toFixed(1)}</span> | |
| <div class="cmp-bar-bg"><div class="cmp-bar-fill" style="width:${sb*10}%;background:${cb}"></div></div> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| // Count wins | |
| const aWins = Object.values(winners).filter(v=>v==='a').length; | |
| const bWins = Object.values(winners).filter(v=>v==='b').length; | |
| const ties = Object.values(winners).filter(v=>v==='tie').length; | |
| const verdict = aWin | |
| ? `<b>${a.name}</b> overall better hai (${a.overall.toFixed(1)} vs ${b.overall.toFixed(1)}) β ${aWins} categories mein jeet gaya, ${ties} ties.` | |
| : overall_winner === 'b' | |
| ? `<b>${b.name}</b> overall better hai (${b.overall.toFixed(1)} vs ${a.overall.toFixed(1)}) β ${bWins} categories mein jeet gaya, ${ties} ties.` | |
| : `Dono phones kaafi close hain! (${a.overall.toFixed(1)} vs ${b.overall.toFixed(1)}) β Use case ke mutabiq choose karein.`; | |
| document.getElementById('cmp-results').innerHTML = ` | |
| <div class="cmp-phones"> | |
| <div class="cmp-phone-col"> | |
| <div style="font-size:28px">${a.emoji||'π±'}</div> | |
| <div class="cmp-phone-name">${a.name}</div> | |
| <div class="cmp-phone-price">${a.price_label}</div> | |
| <div class="cmp-phone-score" style="color:${sc(a.overall)}">${a.overall.toFixed(1)}</div> | |
| ${aWin ? '<div class="cmp-winner-tag">π Winner</div>' : ''} | |
| </div> | |
| <div class="cmp-phone-col"> | |
| <div style="font-size:28px">${b.emoji||'π±'}</div> | |
| <div class="cmp-phone-name">${b.name}</div> | |
| <div class="cmp-phone-price">${b.price_label}</div> | |
| <div class="cmp-phone-score" style="color:${sc(b.overall)}">${b.overall.toFixed(1)}</div> | |
| ${!aWin && overall_winner!=='tie' ? '<div class="cmp-winner-tag">π Winner</div>' : ''} | |
| </div> | |
| </div> | |
| <div class="cmp-cats">${catRows}</div> | |
| <div class="cmp-verdict">π ${verdict}<br><br> | |
| <a href="${a.priceoye_url}" target="_blank" style="color:var(--red);font-size:12px">${a.name} PriceOye pe dekho</a> Β· | |
| <a href="${b.priceoye_url}" target="_blank" style="color:var(--red);font-size:12px">${b.name} PriceOye pe dekho</a> | |
| </div>`; | |
| } | |
| // ββ INIT ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function resetChat() { | |
| SESSION_ID = null; | |
| document.getElementById('messages').innerHTML = ''; | |
| init(); | |
| } | |
| async function init() { | |
| await loadPhones(); | |
| showTyping(); | |
| try { | |
| const r = await fetch(`${API}/chat`, { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ session_id: null, message: '' }) | |
| }); | |
| const data = await r.json(); | |
| SESSION_ID = data.session_id; | |
| removeTyping(); | |
| appendMsg('bot', data.text, data.quick_replies || []); | |
| } catch(e) { | |
| removeTyping(); | |
| appendMsg('bot', 'β οΈ Backend se connect nahi ho raha. Check karein ke server chal raha hai.', []); | |
| } | |
| } | |
| // Input handlers | |
| document.getElementById('user-input').addEventListener('keydown', e => { | |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } | |
| }); | |
| document.getElementById('user-input').addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = Math.min(this.scrollHeight, 110) + 'px'; | |
| }); | |
| document.getElementById('cmp-modal').addEventListener('click', e => { | |
| if (e.target === e.currentTarget) closeCompare(); | |
| }); | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |