pillpal / index.html
3v324v23's picture
update
f886da4
Raw
History Blame Contribute Delete
14.7 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>PillPal</title>
<style>
:root{
--bg:#f8fafc; --panel:#ffffff; --ink:#0f172a; --muted:#64748b; --line:#e5e7eb;
--blue:#2563eb; --blue-soft:#eff4ff; --blue-ink:#1d4ed8; --green:#16a34a; --green-soft:#dcfce7;
--amber-bg:#fffbeb; --amber-line:#fde68a; --amber-ink:#92660e; --red:#dc2626; --shadow:0 1px 3px rgba(15,23,42,.06),0 1px 2px rgba(15,23,42,.04);
}
body.dark{ --bg:#0b1220; --panel:#111a2e; --ink:#e7edf7; --muted:#90a0bb; --line:#22304d;
--blue-soft:#13243f; --green-soft:#0e2a1c; --amber-bg:#241d09; --amber-line:#4a3a12; --shadow:0 1px 3px rgba(0,0,0,.4);}
*{box-sizing:border-box;} html,body{margin:0;background:var(--bg);color:var(--ink);font-family:'Inter',system-ui,Segoe UI,Roboto,sans-serif;}
a{color:var(--blue);text-decoration:none;}
.topbar{display:flex;align-items:center;gap:18px;background:var(--panel);border-bottom:1px solid var(--line);padding:14px 26px;}
.logo{display:flex;align-items:center;gap:10px;font-weight:800;font-size:1.4rem;}
.logo .mark{width:34px;height:34px;border-radius:9px;background:linear-gradient(135deg,#3b82f6,#2563eb);display:grid;place-items:center;color:#fff;font-size:1.1rem;}
.tagline{color:var(--muted);font-size:.92rem;flex:1;}
.theme{border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:9px;padding:8px 12px;cursor:pointer;font-weight:600;font-size:.85rem;}
.shell{display:flex;align-items:flex-start;}
.sidebar{width:230px;flex:none;padding:18px 14px;display:flex;flex-direction:column;min-height:calc(100vh - 64px);gap:4px;}
.nav{display:flex;align-items:center;gap:11px;padding:10px 12px;border-radius:10px;color:var(--muted);font-weight:600;cursor:pointer;font-size:.95rem;}
.nav:hover{background:var(--blue-soft);} .nav.active{background:var(--blue-soft);color:var(--blue-ink);}
.nav .i{width:18px;text-align:center;}
.track{margin-top:auto;background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px;box-shadow:var(--shadow);}
.track h4{margin:0 0 4px;display:flex;align-items:center;gap:6px;} .track p{margin:0 0 10px;color:var(--muted);font-size:.82rem;line-height:1.4;}
.track button{width:100%;border:1px solid var(--line);background:var(--panel);color:var(--blue);border-radius:9px;padding:8px;font-weight:700;cursor:pointer;}
.content{flex:1;padding:20px 26px 40px;max-width:1180px;margin:0 auto;}
.banner{display:flex;gap:10px;align-items:center;background:var(--amber-bg);border:1px solid var(--amber-line);color:var(--amber-ink);border-radius:12px;padding:12px 16px;font-size:.9rem;margin-bottom:18px;}
.grid{display:grid;grid-template-columns:1fr 320px;gap:18px;align-items:start;}
@media(max-width:1100px){.grid{grid-template-columns:1fr;}}
.card{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:18px 20px;box-shadow:var(--shadow);margin-bottom:18px;}
.stitle{display:flex;align-items:center;gap:10px;font-weight:800;font-size:1.05rem;margin:0 0 16px;}
.stitle::before{content:"";width:4px;height:18px;border-radius:3px;background:var(--blue);}
.two{display:grid;grid-template-columns:1fr;gap:16px;}
.drop{border:2px dashed var(--line);border-radius:12px;background:var(--bg);min-height:210px;display:grid;place-items:center;text-align:center;color:var(--muted);cursor:pointer;padding:16px;}
.drop.hover{border-color:var(--blue);background:var(--blue-soft);} .drop b{color:var(--ink);display:block;margin:8px 0 4px;}
.drop img{max-height:180px;border-radius:8px;}
.dicons{display:flex;justify-content:center;gap:26px;color:var(--muted);margin-top:10px;font-size:1.1rem;}
.btn{border:none;border-radius:10px;padding:11px 14px;font-weight:700;cursor:pointer;font-size:.92rem;width:100%;}
.btn.primary{background:var(--blue);color:#fff;} .btn.primary:hover{filter:brightness(1.05);}
.btn.ghost{background:var(--panel);border:1px solid var(--line);color:var(--ink);} .btn.danger{background:var(--panel);border:1px solid #fecaca;color:var(--red);}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
.med{border:1px solid var(--line);border-radius:12px;padding:14px;margin-bottom:12px;}
.med .top{display:flex;align-items:center;gap:8px;}
.med .name{font-weight:800;font-size:1.05rem;} .badge{background:var(--blue-soft);color:var(--blue-ink);border-radius:7px;padding:2px 8px;font-size:.78rem;font-weight:700;}
.ok{margin-left:auto;background:var(--green-soft);color:var(--green);border-radius:999px;padding:3px 10px;font-size:.78rem;font-weight:700;}
.warn{margin-left:auto;background:var(--amber-bg);color:var(--amber-ink);border:1px solid var(--amber-line);border-radius:999px;padding:3px 10px;font-size:.78rem;font-weight:700;}
.chips{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0;} .chip{background:var(--blue-soft);color:var(--blue-ink);border-radius:7px;padding:4px 10px;font-size:.8rem;font-weight:700;}
.meta{color:var(--muted);font-size:.85rem;display:flex;gap:18px;flex-wrap:wrap;}
.selbox{border:1px solid var(--line);border-radius:12px;padding:14px;margin-top:4px;}
.selbox label{font-size:.85rem;color:var(--muted);} select,input{width:100%;border:1px solid var(--line);border-radius:9px;padding:10px 12px;background:var(--panel);color:var(--ink);font-size:.92rem;margin-top:6px;outline:none;}
input:focus,select:focus{border-color:var(--blue);}
.field{margin-bottom:12px;} .field>label{font-size:.85rem;color:var(--muted);font-weight:600;}
.subtle{color:var(--muted);font-size:.85rem;margin:-8px 0 12px;}
.dose{background:var(--blue-soft);border-radius:10px;padding:9px 12px;margin-bottom:8px;}
.dose .t{color:var(--blue-ink);font-weight:800;font-size:.85rem;} .dose .d{font-weight:700;font-size:.85rem;margin-top:1px;} .dose .q{color:var(--muted);font-size:.75rem;margin-top:1px;}
.tip{display:flex;gap:10px;align-items:flex-start;color:var(--muted);font-size:.9rem;margin:10px 0;}
.head-ico{display:flex;align-items:center;gap:8px;font-weight:800;font-size:.98rem;margin:0 0 4px;}
footer{border-top:1px solid var(--line);margin-top:10px;padding:18px;text-align:center;color:var(--muted);font-size:.85rem;}
footer span{margin:0 10px;}
.toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:#0f172a;color:#fff;padding:10px 16px;border-radius:10px;opacity:0;transition:.3s;z-index:9;}
.toast.show{opacity:1;}
</style>
</head>
<body>
<header class="topbar">
<div class="logo"><span class="mark">💊</span> PillPal</div>
<div class="tagline">Photograph a bottle. Get a clear daily schedule — and a heads-up before you run out.</div>
<button class="theme" id="theme">Theme ▾</button>
</header>
<div class="shell">
<main class="content">
<div class="grid">
<div>
<section class="card">
<h3 class="stitle">1. Read a label</h3>
<div class="two">
<div>
<div class="drop" id="drop">
<div>
<b>Upload pill bottle photo</b>
<span>Drag and drop an image here<br/>or click to browse</span>
</div>
</div>
<input type="file" id="file" accept="image/*" style="display:none"/>
<button class="btn primary" id="btnRead" style="margin-top:14px">Read label</button>
</div>
<div>
<div id="medCards"></div>
<div class="selbox">
<label>Select a medication to edit or delete</label>
<select id="medSelect"><option value="">Choose a medication…</option></select>
<div class="row2" style="margin-top:12px">
<button class="btn ghost" id="btnLoad">Load for editing</button>
<button class="btn danger" id="btnDelete">Delete</button>
</div>
<button class="btn ghost" id="btnClear" style="margin-top:10px">Clear all</button>
</div>
</div>
</div>
</section>
<section class="card">
<h3 class="stitle">2. Confirm or correct</h3>
<div class="two">
<div>
<div class="field"><label>Medication</label><input id="f_name" placeholder="e.g., Metformin"/></div>
<div class="field"><label>Dose</label><input id="f_dose" placeholder="e.g., 500 mg"/></div>
<div class="field"><label>Instruction</label><input id="f_instruction" placeholder="e.g., take 1 tablet twice daily"/></div>
</div>
<div>
<div class="row2">
<div class="field"><label>Quantity in bottle</label><input id="f_qty" placeholder="e.g., 60"/></div>
<div class="field"><label>Refill / expiry (optional)</label><input id="f_expiry" placeholder="YYYY-MM-DD"/></div>
</div>
<div class="row2" style="margin-top:30px">
<button class="btn primary" id="btnAdd">Add new</button>
<button class="btn ghost" id="btnUpdate">Update selected</button>
</div>
</div>
</div>
</section>
</div>
<div>
<section class="card">
<h3 class="head-ico">Today's schedule</h3>
<div class="subtle" id="today"></div>
<div id="scheduleList"></div>
</section>
<section class="card">
<h3 class="head-ico">Quick tips</h3>
<div class="tip">Take photos in good lighting</div>
<div class="tip">Check the label for dose &amp; instructions</div>
<div class="tip">Keep your medication list up to date</div>
</section>
</div>
</div>
</main>
</div>
<div class="toast" id="toast"></div>
<script type="module">
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
const byId=(i)=>document.getElementById(i), esc=(s)=>(s==null?"":String(s)).replace(/[&<>]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;"}[c]));
let _client=null; const C=async()=>(_client??=await Client.connect(window.location.origin));
let selected=-1, imageData="";
const toast=(m)=>{const t=byId("toast");t.textContent=m;t.classList.add("show");clearTimeout(t._h);t._h=setTimeout(()=>t.classList.remove("show"),2400);};
function renderMedCard(m){
const status=m.on_track?`<span class="ok">On track</span>`:`<span class="warn">${esc(m.warning||"Refill soon")}</span>`;
const chips=(m.times||[]).map(t=>`<span class="chip">${esc(t)}</span>`).join("");
return `<div class="med"><div class="top"><span class="name">${esc(m.name||"Medication")}</span>
${m.dose?`<span class="badge">${esc(m.dose)}</span>`:""}${status}</div>
${chips?`<div class="chips">${chips}</div>`:""}
<div class="meta">${m.quantity!=null?`<span>Qty <b>${esc(m.quantity)}</b></span>`:""}
${m.run_out?`<span>Run-out <b>${esc(m.run_out)}</b></span>`:""}</div></div>`;
}
function render(s){
byId("today").textContent=s.today||"";
byId("medCards").innerHTML=(s.meds||[]).map(renderMedCard).join("")||`<div class="meta" style="padding:8px 0">No medications yet — add one below.</div>`;
const sel=byId("medSelect"); sel.innerHTML=`<option value="">Choose a medication…</option>`+
(s.meds||[]).map(m=>`<option value="${m.i}">${esc(m.name||"Medication")}${m.dose?" ("+esc(m.dose)+")":""}</option>`).join("");
byId("scheduleList").innerHTML=(s.schedule||[]).map(b=>`<div class="dose"><div class="t">${esc(b.time)}</div>`+
b.items.map(it=>`<div class="d">${esc(it)}</div><div class="q">1 dose</div>`).join("")+`</div>`).join("")
|| `<div class="subtle">Nothing scheduled today.</div>`;
window._meds=s.meds||[];
}
async function refresh(){ const c=await C(); render((await c.predict("/state",{})).data[0]); }
const drop=byId("drop"), file=byId("file");
drop.onclick=()=>file.click();
file.onchange=e=>{const f=e.target.files[0]; if(f)readFile(f);};
["dragover","dragenter"].forEach(ev=>drop.addEventListener(ev,e=>{e.preventDefault();drop.classList.add("hover");}));
["dragleave","drop"].forEach(ev=>drop.addEventListener(ev,e=>{e.preventDefault();drop.classList.remove("hover");}));
drop.addEventListener("drop",e=>{const f=e.dataTransfer.files[0]; if(f)readFile(f);});
function readFile(f){const r=new FileReader();r.onload=()=>{imageData=r.result; drop.innerHTML=`<img src="${imageData}"/>`;};r.readAsDataURL(f);}
byId("btnRead").onclick=async()=>{ if(!imageData){toast("Upload a photo first.");return;}
toast("Reading label…"); const c=await C(); const r=(await c.predict("/read_label",{image_data:imageData})).data[0];
if(!r.ok){toast(r.message);return;} const f=r.fields;
byId("f_name").value=f.drug_name; byId("f_dose").value=f.dose; byId("f_instruction").value=f.frequency_text;
byId("f_qty").value=f.quantity; byId("f_expiry").value=f.refill_or_expiry_date; toast("Label read — confirm the details."); };
function formArgs(index){ return {drug_name:byId("f_name").value, dose:byId("f_dose").value,
frequency_text:byId("f_instruction").value, quantity:byId("f_qty").value, refill_or_expiry_date:byId("f_expiry").value, index}; }
function clearForm(){ ["f_name","f_dose","f_instruction","f_qty","f_expiry"].forEach(i=>byId(i).value=""); selected=-1; }
async function save(index){ const c=await C(); const r=(await c.predict("/save_med",formArgs(index))).data[0];
if(!r.ok){toast(r.message);return;} render(r); toast(index>=0?"Updated.":"Added."); clearForm(); }
byId("btnAdd").onclick=()=>save(-1);
byId("btnUpdate").onclick=()=>{ if(selected<0){toast("Pick a medication to update (Load for editing).");return;} save(selected); };
byId("btnLoad").onclick=()=>{ const i=byId("medSelect").value; if(i===""){toast("Choose a medication first.");return;}
const m=window._meds[+i]; selected=+i; byId("f_name").value=m.name||""; byId("f_dose").value=m.dose||"";
byId("f_instruction").value=m.instruction||""; byId("f_qty").value=m.quantity??""; byId("f_expiry").value=m.refill_or_expiry||""; toast("Loaded — edit then Update selected."); };
byId("btnDelete").onclick=async()=>{ const i=byId("medSelect").value; if(i===""){toast("Choose a medication first.");return;}
const c=await C(); render((await c.predict("/delete_med",{index:+i})).data[0]); toast("Deleted."); clearForm(); };
byId("btnClear").onclick=async()=>{ const c=await C(); render((await c.predict("/clear_all",{})).data[0]); toast("Cleared all."); clearForm(); };
byId("theme").onclick=()=>{ document.body.classList.toggle("dark");
byId("theme").textContent=(document.body.classList.contains("dark")?"Light ▾":"Dark ▾")+""; };
refresh();
</script>
</body>
</html>