Spaces:
Runtime error
Runtime error
| <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> | |