wpl-discovery / index.html
GameTheory's picture
Sync from GitHub
24c5932 verified
Raw
History Blame Contribute Delete
7.15 kB
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Worcestershire Libraries - Live Assistant</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
<style>
:root{
--teal:#0a7d78; --teal-dark:#075a56; --cream:#f6f3ec; --ink:#1f2a2a;
--line:#e2ded3; --soft:#eef5f4;
}
*{box-sizing:border-box}
body{
margin:0; background:var(--cream); color:var(--ink);
font-family:'Inter',system-ui,sans-serif; line-height:1.5;
}
.wrap{max-width:760px; margin:0 auto; min-height:100vh; display:flex;
flex-direction:column;}
header{
background:linear-gradient(135deg,var(--teal),var(--teal-dark)); color:#fff;
padding:26px 28px 22px; border-radius:0 0 22px 22px;
box-shadow:0 8px 26px rgba(7,90,86,.28);
}
.live{display:inline-block; background:#fff; color:var(--teal-dark);
font-size:.66rem; font-weight:600; letter-spacing:.08em; text-transform:uppercase;
padding:3px 10px; border-radius:999px; margin-bottom:10px;}
header h1{font-family:'Fraunces',Georgia,serif; font-weight:600; margin:0;
font-size:1.7rem; letter-spacing:-.01em;}
header p{margin:.45rem 0 0; opacity:.92; font-size:.92rem; max-width:60ch;}
main{flex:1; padding:18px 20px 0; overflow-y:auto;}
.msg{margin:14px 0; display:flex; gap:10px; animation:rise.25s ease;}
@keyframes rise{from{opacity:0; transform:translateY(6px)} to{opacity:1}}
.msg.av{flex:0 0 30px; height:30px; border-radius:50%; display:grid;
place-items:center; font-size:15px;}
.msg.user{flex-direction:row-reverse;}
.msg.user.av{background:var(--teal); color:#fff;}
.msg.bot.av{background:var(--soft); border:1px solid var(--line);}
.bubble{padding:12px 15px; border-radius:14px; max-width:84%; font-size:.95rem;}
.msg.user.bubble{background:var(--teal); color:#fff; border-bottom-right-radius:4px;}
.msg.bot.bubble{background:#fff; border:1px solid var(--line);
border-bottom-left-radius:4px;}
.bubble:is(h1,h2,h3){font-family:'Fraunces',serif; font-size:1.05rem; margin:.4em 0;}
.bubble a{color:var(--teal-dark); font-weight:600;}
.bubble ul{padding-left:1.1em; margin:.4em 0;}
.bubble code{background:var(--soft); padding:1px 5px; border-radius:5px; font-size:.85em;}
.bubble details{margin-top:10px; background:var(--soft); border-radius:10px;
padding:8px 12px; font-size:.85rem;}
.bubble summary{cursor:pointer; font-weight:600;}
.chips{display:flex; flex-wrap:wrap; gap:8px; padding:6px 20px 0;}
.chip{background:#fff; border:1px solid var(--line); border-radius:999px;
padding:7px 13px; font-size:.83rem; cursor:pointer; color:var(--teal-dark);
transition:.15s;}
.chip:hover{background:var(--teal); color:#fff; border-color:var(--teal);}
form{position:sticky; bottom:0; background:var(--cream); padding:14px 20px 20px;
display:flex; gap:10px;}
input{flex:1; padding:13px 16px; border:1px solid var(--line); border-radius:12px;
font-size:1rem; font-family:inherit; background:#fff;}
input:focus{outline:2px solid var(--teal); border-color:var(--teal);}
button{background:var(--teal); color:#fff; border:0; border-radius:12px;
padding:0 20px; font-weight:600; cursor:pointer; font-size:.95rem;}
button:hover{background:var(--teal-dark);}
button:disabled{opacity:.5; cursor:default;}
footer{text-align:center; font-size:.74rem; color:#8a8676; padding:0 20px 16px;}
.dot{display:inline-block; width:6px; height:6px; border-radius:50%;
background:var(--teal); margin:0 2px; animation:blink 1.2s infinite both;}
.dot:nth-child(2){animation-delay:.2s}.dot:nth-child(3){animation-delay:.4s}
@keyframes blink{0%,80%,100%{opacity:.2} 40%{opacity:1}}
</style>
</head>
<body>
<div class="wrap">
<header>
<span class="live">● live data</span>
<h1>Worcestershire Libraries</h1>
<p>Exactly where to get the book you want, everything The Hive offers,
the mobile library, events, printing and what's free online - from the
council site, catalogue &amp; every Hive page, with exactly what you
need to sign up.</p>
</header>
<main id="log">
<div class="msg bot"><div class="av"></div><div class="bubble" id="welcome">
Hello! Try one of these, or ask your own question.
</div></div>
</main>
<div class="chips" id="chips">
<div class="chip">How do I get Wolf Hall?</div>
<div class="chip">What can I do at The Hive?</div>
<div class="chip">Is Malvern library open now?</div>
<div class="chip">A late-opening library with a café and meeting rooms</div>
<div class="chip">Can I read newspapers for free?</div>
</div>
<form id="form" autocomplete="off">
<input id="box" placeholder="Ask anything about your local library…" autofocus />
<button id="send" type="submit">Ask</button>
</form>
<footer>Official sources only - council site &amp; catalogue checked live,
plus every page of the Hive's own site · runs on a ≤32B model</footer>
</div>
<script type="module">
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
const log = document.getElementById("log");
const form = document.getElementById("form");
const box = document.getElementById("box");
const send = document.getElementById("send");
let client = null;
async function getClient(){
if(!client) client = await Client.connect(window.location.origin);
return client;
}
function addMsg(role, html){
const wrap = document.createElement("div");
wrap.className = "msg " + role;
wrap.innerHTML = `<div class="av">${role==="user"?"":""}</div>`+
`<div class="bubble"></div>`;
wrap.querySelector(".bubble").innerHTML = html;
log.appendChild(wrap);
log.scrollTop = log.scrollHeight;
return wrap.querySelector(".bubble");
}
async function ask(message){
addMsg("user", message.replace(/</g,"&lt;"));
const bubble = addMsg("bot", '<span class="dot"></span><span class="dot"></span><span class="dot"></span>');
send.disabled = true;
try{
const c = await getClient();
const job = c.submit("/ask", [message]);
let latest = "";
for await (const ev of job){
if(ev.type === "data" && ev.data && ev.data[0]!= null){
latest = ev.data[0];
bubble.innerHTML = marked.parse(latest);
log.scrollTop = log.scrollHeight;
}
}
if(!latest) bubble.textContent = "Sorry - no response. Please try again.";
}catch(err){
bubble.textContent = "Couldn't reach the assistant. Please try again in a moment.";
console.error(err);
}finally{
send.disabled = false;
box.focus();
}
}
form.addEventListener("submit", e=>{
e.preventDefault();
const v = box.value.trim();
if(!v) return;
box.value = "";
ask(v);
});
document.getElementById("chips").addEventListener("click", e=>{
if(e.target.classList.contains("chip")) ask(e.target.textContent.trim());
});
</script>
</body>
</html>