Spaces:
Running
Running
| <html lang="en"><head> | |
| <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Answerwise β Foreign-Patient Clinic AI Consultation (Demo)</title> | |
| <style> | |
| :root{--ind:#4338CA;--ind-l:#6366F1;--bg:#F7F8FC;--card:#fff;--bd:#E2E8F0;--tx:#0F172A;--mut:#64748B} | |
| *{box-sizing:border-box}body{margin:0;font-family:-apple-system,'Segoe UI',Roboto,'Malgun Gothic',sans-serif;background:var(--bg);color:var(--tx)} | |
| .top{background:var(--ind);color:#fff;padding:14px 20px;display:flex;align-items:center;gap:12px} | |
| .top b{font-size:1.05rem}.top .tag{font-size:.7rem;background:rgba(255,255,255,.2);padding:2px 8px;border-radius:100px} | |
| .top .sp{flex:1}.top button{background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.3);border-radius:8px;padding:6px 12px;cursor:pointer;font-size:.8rem} | |
| .wrap{max-width:1100px;margin:0 auto;padding:20px} | |
| .h1{font-size:1.4rem;font-weight:800;margin:6px 0}.sub{color:var(--mut);margin-bottom:18px;font-size:.9rem} | |
| .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px} | |
| .ccard{background:var(--card);border:1px solid var(--bd);border-radius:14px;padding:16px;cursor:pointer;transition:.15s} | |
| .ccard:hover{transform:translateY(-3px);box-shadow:0 8px 24px rgba(0,0,0,.08)} | |
| .ccard .em{font-size:1.8rem}.ccard h3{margin:8px 0 2px;font-size:1.05rem}.ccard .cat{color:var(--ind);font-weight:700;font-size:.75rem} | |
| .ccard .loc{color:var(--mut);font-size:.8rem;margin:4px 0}.ccard .promo{margin-top:8px;font-size:.75rem;background:#FEF3C7;color:#92400E;padding:4px 8px;border-radius:8px;display:inline-block} | |
| .ccard .langs{margin-top:8px;font-size:.9rem} | |
| /* chat */ | |
| .chatview{display:none;grid-template-columns:1fr 300px;gap:16px} | |
| .panel{background:var(--card);border:1px solid var(--bd);border-radius:14px;overflow:hidden} | |
| .chead{padding:12px 16px;border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:10px} | |
| .chead .em{font-size:1.4rem}.chead b{font-size:1rem}.chead .cat{color:var(--mut);font-size:.75rem} | |
| .langsel{display:flex;gap:6px;padding:8px 16px;border-bottom:1px solid var(--bd);flex-wrap:wrap} | |
| .langsel button{border:1px solid var(--bd);background:#fff;border-radius:100px;padding:4px 12px;cursor:pointer;font-size:.8rem} | |
| .langsel button.on{background:var(--ind);color:#fff;border-color:var(--ind)} | |
| .msgs{height:420px;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:10px} | |
| .m{max-width:82%;padding:10px 13px;border-radius:12px;font-size:.9rem;line-height:1.5;white-space:pre-wrap} | |
| .m.u{align-self:flex-end;background:var(--ind);color:#fff;border-bottom-right-radius:3px} | |
| .m.a{align-self:flex-start;background:#F1F5F9;border-bottom-left-radius:3px} | |
| .m .src{margin-top:8px;font-size:.72rem;color:var(--mut);border-top:1px dashed #CBD5E1;padding-top:6px} | |
| .badge{display:inline-block;font-size:.66rem;font-weight:700;padding:1px 7px;border-radius:100px;margin-right:5px} | |
| .b-guard{background:#FEE2E2;color:#B91C1C}.b-emg{background:#7F1D1D;color:#fff}.b-rag{background:#DBEAFE;color:#1E40AF}.b-r2{background:#FEF3C7;color:#92400E} | |
| .inp{display:flex;gap:8px;padding:12px 16px;border-top:1px solid var(--bd)} | |
| .inp input{flex:1;border:1px solid var(--bd);border-radius:10px;padding:10px 12px;font-size:.9rem} | |
| .inp button{background:var(--ind);color:#fff;border:0;border-radius:10px;padding:10px 16px;cursor:pointer} | |
| .side h4{margin:0 0 8px;font-size:.85rem}.side{padding:14px} | |
| .hintbtn{display:block;width:100%;text-align:left;border:1px solid var(--bd);background:#fff;border-radius:9px;padding:8px 10px;margin-bottom:7px;cursor:pointer;font-size:.78rem} | |
| .hintbtn small{color:var(--mut);display:block} | |
| .leadform{margin-top:14px;border-top:1px solid var(--bd);padding-top:12px} | |
| .leadform input{width:100%;border:1px solid var(--bd);border-radius:8px;padding:7px 9px;margin-bottom:7px;font-size:.82rem} | |
| .leadform button{width:100%;background:#10B981;color:#fff;border:0;border-radius:8px;padding:9px;cursor:pointer} | |
| /* admin */ | |
| .adminview{display:none} | |
| table{width:100%;border-collapse:collapse;font-size:.82rem;background:#fff;border:1px solid var(--bd);border-radius:12px;overflow:hidden} | |
| th,td{padding:9px 11px;border-bottom:1px solid var(--bd);text-align:left}th{background:#F8FAFF;font-size:.72rem;color:var(--mut)} | |
| .g{font-weight:800;padding:1px 7px;border-radius:100px;font-size:.72rem}.g5{background:#4338CA;color:#fff}.g4{background:#6366F1;color:#fff}.g3{background:#10B981;color:#fff}.g2{background:#F59E0B;color:#fff}.gL,.g1,.g0{background:#E2E8F0;color:#334155} | |
| </style></head><body> | |
| <div class="top"><b>Answerwise</b><span class="tag">DEMO Β· JGOS Darwin-398B</span><span class="sp"></span> | |
| <button onclick="show('home')">π₯ Clinics</button><button onclick="show('admin')">π Admin (CRM)</button></div> | |
| <div class="wrap"> | |
| <!-- HOME: λ³μ μΉ΄λ --> | |
| <div id="home"> | |
| <div class="h1">Foreign-Patient AI Consultation & Reservation Lead SaaS</div> | |
| <div class="sub">Pick a clinic β chat in your language β grounded on that clinic's data only. AI never diagnoses, reads photos, or confirms bookings.</div> | |
| <div class="cards" id="cards"></div> | |
| </div> | |
| <!-- CHAT --> | |
| <div id="chat" class="chatview"> | |
| <div class="panel"> | |
| <div class="chead"><span class="em" id="c-em"></span><div><b id="c-name"></b><div class="cat" id="c-cat"></div></div></div> | |
| <div class="langsel" id="langsel"></div> | |
| <div class="msgs" id="msgs"></div> | |
| <div class="inp"><input id="txt" placeholder="Ask about procedures, price, hoursβ¦" onkeydown="if(event.key==='Enter')sendMsg()"><button onclick="sendMsg()">Send</button></div> | |
| </div> | |
| <div class="panel side"> | |
| <h4>π‘ Try these (demo flows)</h4> | |
| <button class="hintbtn" onclick="quick('Do you offer rhinoplasty and how much is it?')">Normal RAG Q&A<small>grounded on clinic data</small></button> | |
| <button class="hintbtn" onclick="quick('Do you do dental implants?')">π Tenant isolation<small>ask a plastic-surgery clinic a dental question</small></button> | |
| <button class="hintbtn" onclick="quick('Can you look at my photo and tell me if I need surgery?')">π‘οΈ Photo-read guardrail<small>AI must refuse</small></button> | |
| <button class="hintbtn" onclick="quick('Is it safe for me to get this surgery?')">π‘οΈ Personal-diagnosis guardrail</button> | |
| <button class="hintbtn" onclick="quick('I have heavy bleeding and high fever after surgery')">π¨ Emergency (R3)<small>escalation</small></button> | |
| <div class="leadform"> | |
| <h4>π Leave a reservation inquiry (lead)</h4> | |
| <input id="l-name" placeholder="Name"><input id="l-email" placeholder="Email"><input id="l-date" placeholder="Preferred visit date (e.g. Jul 20)"> | |
| <label style="font-size:.78rem;color:var(--mut)"><input type="checkbox" id="l-photo" style="width:auto;margin-right:6px">attach a reference photo (received, not analyzed)</label> | |
| <button onclick="submitLead()">Submit inquiry</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ADMIN --> | |
| <div id="admin" class="adminview"> | |
| <div class="h1">Clinic Admin Β· Lead CRM</div> | |
| <div class="sub">Each clinic sees only its own leads (tenant isolation). Grades L0βL5, status, and risk are tracked.</div> | |
| <div style="margin-bottom:12px"><select id="adm-sel" onchange="loadAdmin()" style="padding:8px;border-radius:8px;border:1px solid var(--bd)"></select></div> | |
| <table id="adm-tbl"><thead><tr><th>Lead</th><th>Grade</th><th>Interest</th><th>Name</th><th>Contact</th><th>Visit</th><th>Photo</th><th>Status</th><th>Msgs</th></tr></thead><tbody></tbody></table> | |
| </div> | |
| </div> | |
| <script> | |
| let CUR=null, LANG='en', SESSION={}, CLIN=[]; | |
| const LANGS=[['en','English'],['zh','δΈζ'],['ja','ζ₯ζ¬θͺ'],['th','ΰΉΰΈΰΈ’']]; | |
| function show(v){for(const id of ['home','chat','admin'])document.getElementById(id).style.display='none'; | |
| if(v==='home')document.getElementById('home').style.display='block'; | |
| if(v==='chat')document.getElementById('chat').style.display='grid'; | |
| if(v==='admin'){document.getElementById('admin').style.display='block';loadAdmin();}} | |
| async function loadClinics(){const d=await(await fetch('/api/clinics')).json();CLIN=d.clinics; | |
| document.getElementById('cards').innerHTML=CLIN.map(c=>`<div class="ccard" onclick="openClinic('${c.id}')"> | |
| <div class="em">${c.emoji}</div><div class="cat">${c.category}</div><h3>${c.name}</h3> | |
| <div class="loc">π ${c.location}</div><div class="langs">${c.langs.map(l=>({en:'π¬π§',zh:'π¨π³',ja:'π―π΅',th:'πΉπ'}[l]||'')).join(' ')}</div> | |
| <div class="promo">π ${c.promo}</div></div>`).join(''); | |
| const sel=document.getElementById('adm-sel');sel.innerHTML=CLIN.map(c=>`<option value="${c.id}">${c.name}</option>`).join('');} | |
| function openClinic(id){CUR=CLIN.find(c=>c.id===id);SESSION[id]=SESSION[id]||('s'+Math.random().toString(36).slice(2,10)); | |
| document.getElementById('c-em').textContent=CUR.emoji;document.getElementById('c-name').textContent=CUR.name;document.getElementById('c-cat').textContent=CUR.category; | |
| document.getElementById('langsel').innerHTML=LANGS.map(([k,v])=>`<button class="${k==='en'?'on':''}" onclick="setLang('${k}',this)">${v}</button>`).join(''); | |
| LANG='en';document.getElementById('msgs').innerHTML=''; | |
| addMsg('a',`Hello! I'm the AI assistant for ${CUR.name}. Ask me about our procedures, prices, hours or promotions β or leave a reservation inquiry.`,null,'rag'); | |
| show('chat');} | |
| function setLang(k,b){LANG=k;document.querySelectorAll('#langsel button').forEach(x=>x.classList.remove('on'));b.classList.add('on');} | |
| function addMsg(role,text,sources,kind){const m=document.createElement('div');m.className='m '+role; | |
| let badge='';if(kind==='guard')badge='<span class="badge b-guard">π‘οΈ SAFETY GUARDRAIL</span>'; | |
| if(kind==='emg')badge='<span class="badge b-emg">π¨ EMERGENCY R3</span>';if(kind==='r2')badge='<span class="badge b-r2">β οΈ R2 review</span>'; | |
| m.innerHTML=badge+escapeHtml(text); | |
| if(sources&&sources.length){m.innerHTML+='<div class="src"><span class="badge b-rag">π grounded</span>'+sources.map(s=>escapeHtml(s.text)).join('<br>')+'</div>';} | |
| document.getElementById('msgs').appendChild(m);document.getElementById('msgs').scrollTop=1e9;} | |
| function escapeHtml(s){return (s||'').replace(/</g,'<').replace(/>/g,'>');} | |
| function quick(t){document.getElementById('txt').value=t;sendMsg();} | |
| async function sendMsg(){const t=document.getElementById('txt').value.trim();if(!t||!CUR)return; | |
| document.getElementById('txt').value='';addMsg('u',t); | |
| const wait=document.createElement('div');wait.className='m a';wait.textContent='β¦';document.getElementById('msgs').appendChild(wait); | |
| try{const d=await(await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify({tenant:CUR.id,message:t,lang:LANG,session:SESSION[CUR.id]})})).json(); | |
| wait.remove();let kind=d.guardrail?'guard':(d.risk==='R3'?'emg':(d.risk==='R2'?'r2':(d.sources&&d.sources.length?'rag':null))); | |
| addMsg('a',d.answer,d.sources,kind);}catch(e){wait.textContent='(error)';}} | |
| async function submitLead(){if(!CUR)return; | |
| const body={tenant:CUR.id,session:SESSION[CUR.id],name:document.getElementById('l-name').value, | |
| email:document.getElementById('l-email').value,preferred_date:document.getElementById('l-date').value, | |
| photo:document.getElementById('l-photo').checked?'attached':''}; | |
| const d=await(await fetch('/api/lead',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json(); | |
| addMsg('a',`Thank you! Your inquiry is recorded (grade ${d.grade}). The clinic will review and confirm your appointment. You'll hear back by email under the clinic's name.`,null,null);} | |
| async function loadAdmin(){const t=document.getElementById('adm-sel').value;if(!t)return; | |
| const d=await(await fetch('/api/admin/leads?tenant='+t)).json(); | |
| document.querySelector('#adm-tbl tbody').innerHTML=(d.leads||[]).map(l=>`<tr> | |
| <td>${l.lead_id}</td><td><span class="g g${(l.grade||'L0').replace('L','')}">${l.grade||'L0'}</span></td> | |
| <td>${escapeHtml(l.interest||'')}</td><td>${escapeHtml(l.name||'')}</td><td>${escapeHtml(l.email||l.contact||'')}</td> | |
| <td>${escapeHtml(l.preferred_date||'')}</td><td>${l.photo?'π·':''}</td><td>${l.status||''}</td><td>${l.messages||0}</td></tr>`).join('') | |
| ||'<tr><td colspan="9" style="color:#94A3B8">No leads yet β chat with a clinic first.</td></tr>';} | |
| loadClinics(); | |
| </script></body></html> | |