answerwise-demo / static /index.html
openfree's picture
Answerwise demo: multi-tenant clinic AI consult + RAG + safety guardrail + lead CRM (JGOS Darwin-398B)
18af949 verified
Raw
History Blame Contribute Delete
12.5 kB
<!doctype html><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,'&lt;').replace(/>/g,'&gt;');}
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>