Spaces:
Runtime error
Runtime error
File size: 7,148 Bytes
ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 21279b2 24c5932 21279b2 ecf42b5 24c5932 ecf42b5 21279b2 ecf42b5 24c5932 21279b2 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 24c5932 ecf42b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | <!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 & 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 & 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,"<"));
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>
|