Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>The Oracle</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@600;700;900&family=Spectral:ital@0;1&display=swap" rel="stylesheet" /> | |
| <style> | |
| :root { | |
| --gold:#f0c75e; --gold-deep:#c8902f; --violet:#b388ff; --ink:#efe7fb; | |
| --panel:#3a2766cc; --panel-edge:#7b5fc7; --green:#46b34a; --red:#d8442f; | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { margin: 0; min-height: 100%; } | |
| body { | |
| font-family: 'Spectral', Georgia, serif; color: var(--ink); text-align: center; | |
| background: | |
| linear-gradient(rgba(20,11,42,.55), rgba(20,11,42,.78)), | |
| url('https://huggingface.co/buckets/build-small-hackathon/oracle-storage/resolve/background.png') center top / cover no-repeat fixed, | |
| radial-gradient(120% 100% at 50% -10%, #3a2a63 0%, #241546 45%, #140b2a 100%); | |
| overflow-x: hidden; min-height: 100vh; | |
| } | |
| .stars { position: fixed; inset: 0; pointer-events: none; z-index: 0; } | |
| .star { position: absolute; width: 3px; height: 3px; border-radius: 50%; background:#fff; opacity:0; animation: tw 3s ease-in-out infinite; } | |
| @keyframes tw { 0%,100%{opacity:.1;} 50%{opacity:.85;} } | |
| /* top bar */ | |
| .topbar { position: relative; z-index: 2; display: flex; justify-content: space-between; | |
| align-items: center; padding: 14px 22px; } | |
| .logo { font-family:'Cinzel',serif; font-weight:900; color: var(--gold); letter-spacing:1px; | |
| font-size: 1.1rem; text-shadow: 0 2px 6px rgba(0,0,0,.6); } | |
| .logo b { font-size: 1.3rem; } | |
| .built-with { margin-top: 14px; font-size: .78rem; opacity: .65; letter-spacing: .3px; } | |
| .top-controls { display: flex; gap: 10px; } | |
| .chip { font-family:'Cinzel',serif; font-weight:700; cursor: pointer; color: var(--ink); | |
| background: linear-gradient(180deg,#5a3fa0,#3a2766); border: 1px solid var(--panel-edge); | |
| border-radius: 999px; padding: 7px 14px; font-size: .85rem; } | |
| .chip:hover { filter: brightness(1.12); } | |
| .iconbtn { background: none; border: none; padding: 0; cursor: pointer; line-height: 0; } | |
| .iconbtn img { height: 40px; display: block; filter: drop-shadow(0 3px 6px rgba(0,0,0,.5)); } | |
| .iconbtn:hover img { filter: brightness(1.12) drop-shadow(0 3px 6px rgba(0,0,0,.5)); } | |
| #mute.muted img { opacity: .4; } | |
| .imgchip { position: relative; background: none; border: none; padding: 0; cursor: pointer; line-height: 0; } | |
| .imgchip img { height: 40px; display: block; filter: drop-shadow(0 3px 6px rgba(0,0,0,.5)); } | |
| .imgchip span { | |
| position: absolute; | |
| inset: 0; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-family: sans-serif; | |
| font-weight: 800; | |
| font-size: 17px; | |
| color: #c99dfe; | |
| letter-spacing: .5px; | |
| text-shadow: 0 1px 2px rgba(0, 0, 0, .5); | |
| pointer-events: none; | |
| } | |
| .imgchip:hover img { filter: brightness(1.12) drop-shadow(0 3px 6px rgba(0,0,0,.5)); } | |
| .howimg { display: block; width: 100%; height: auto; margin-bottom: 10px; } | |
| /* layout */ | |
| .grid { position: relative; z-index: 1; display: grid; gap: 18px; align-items: start; | |
| grid-template-columns: 230px 1fr 250px; max-width: 1040px; margin: 0 auto; padding: 4px 18px 50px; } | |
| @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } .side { max-width: 460px; margin: 0 auto; } } | |
| .side { background: var(--panel); border: 1px solid var(--panel-edge); border-radius: 16px; | |
| padding: 16px; text-align: left; box-shadow: 0 10px 30px rgba(0,0,0,.35); } | |
| .side h3 { font-family:'Cinzel',serif; font-size: .8rem; letter-spacing: 2px; | |
| text-transform: uppercase; color: var(--gold); margin: 0 0 10px; } | |
| .howto p { margin: 0; line-height: 1.6; color: #d9cdf5; font-size: .98rem; } | |
| /* answers panel framed by top/bottom images, stretchable middle */ | |
| .answers { width: 100%; max-width: 460px; margin: 0 auto; display: flex; flex-direction: column; | |
| filter: drop-shadow(0 10px 30px rgba(0,0,0,.35)); } | |
| .ans-top, .ans-bottom { display: block; width: 100%; height: auto; } | |
| .ans-mid { flex: 1; text-align: left; margin: -1px 0; padding: 2px 18px 8px; | |
| background: url('./images/answer_center.png') center / 100% 100% no-repeat; } | |
| .center { min-width: 0; } | |
| .title-img { display: block; width: min(560px, 92%); height: auto; margin: 6px auto 2px; | |
| filter: drop-shadow(0 4px 10px rgba(0,0,0,.5)); } | |
| .stage { margin: 10px auto 2px; width: 260px; max-width: 78%; position: relative; } | |
| .globe { display: block; width: 100%; height: auto; | |
| filter: drop-shadow(0 10px 18px rgba(0,0,0,.5)) drop-shadow(0 0 26px rgba(150,110,255,.45)); | |
| animation: float 4s ease-in-out infinite; } | |
| @keyframes float { 0%,100%{transform:translateY(0);} 50%{transform:translateY(-7px);} } | |
| .globe.think { animation: float 4s ease-in-out infinite, glow 1.1s ease-in-out infinite; } | |
| @keyframes glow { | |
| 0%,100%{ filter: drop-shadow(0 10px 18px rgba(0,0,0,.5)) drop-shadow(0 0 26px rgba(150,110,255,.45)); } | |
| 50%{ filter: drop-shadow(0 10px 18px rgba(0,0,0,.5)) drop-shadow(0 0 46px rgba(190,150,255,.95)); } } | |
| .panel { display: none; animation: fade .35s ease; } | |
| .panel.active { display: block; } | |
| @keyframes fade { from{opacity:0; transform: translateY(8px);} to{opacity:1; transform:none;} } | |
| .stepper { display:flex; gap:8px; justify-content:center; align-items:center; margin: 4px 0 10px; } | |
| .dot { width: 10px; height: 10px; border-radius: 50%; background: #5a4a86; box-shadow: inset 0 0 0 2px #7b5fc7; } | |
| .dot.on { background: var(--gold); box-shadow: 0 0 10px rgba(240,199,94,.7); } | |
| .status { font-size: .72rem; letter-spacing: 2px; text-transform: uppercase; color: var(--violet); margin-bottom: 4px; } | |
| .question { font-family:'Cinzel',serif; font-weight:700; font-size: 1.5rem; line-height: 1.3; | |
| margin: 4px auto 18px; min-height: 1.6em; max-width: 540px; | |
| color: var(--gold); text-shadow: 0 2px 8px rgba(0,0,0,.5); } | |
| .guess-big { font-family:'Cinzel',serif; font-size: 2.1rem; color: var(--gold); margin: 8px 0 22px; } | |
| .teach-input { font-family:'Cinzel',serif; font-size: 1.1rem; text-align:center; width: 80%; max-width: 340px; | |
| padding: 12px 16px; margin: 0 auto 18px; display:block; border-radius: 12px; border: 1px solid #6a4fa3; | |
| background: rgba(20,10,45,.7); color: #f6f0ff; outline: none; } | |
| .teach-input:focus { border-color: var(--gold); box-shadow: 0 0 0 2px rgba(212,175,55,.3); } | |
| .reveal { font-style: italic; color: #e7d9ff; opacity: .9; max-width: 460px; margin: 6px auto 10px; | |
| min-height: 1.2em; font-size: 1.05rem; text-shadow: 0 1px 6px rgba(0,0,0,.5); } | |
| #teachCats button.sel { outline: 2px solid var(--gold); box-shadow: 0 0 0 3px rgba(212,175,55,.3); } | |
| .reasons { margin: 18px auto 0; max-width: 480px; text-align: left; font-family: system-ui, sans-serif; } | |
| .reasons-h { font-family:'Cinzel',serif; color: var(--gold); font-size: .95rem; margin-bottom: 8px; text-align:center; } | |
| .reason-row { font-size: .9rem; line-height: 1.5; color: #e8def7; background: rgba(120,40,60,.22); | |
| border: 1px solid rgba(200,80,100,.35); border-radius: 10px; padding: 8px 12px; margin: 6px 0; } | |
| .reason-x { color: #ff7a90; font-weight: 700; margin-right: 6px; } | |
| .btns { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; } | |
| button { font-family:'Cinzel',serif; cursor: pointer; font-size: 1rem; font-weight: 700; | |
| padding: 13px 22px; border-radius: 14px; border: 1px solid #6a4fa3; color: #f6f0ff; | |
| background: linear-gradient(180deg, #5a3fa0, #38236a); box-shadow: 0 6px 0 #2a1850, 0 8px 16px rgba(0,0,0,.4); | |
| transition: transform .1s, filter .12s; } | |
| button:hover { filter: brightness(1.1); } | |
| button:active { transform: translateY(3px); box-shadow: 0 3px 0 #2a1850, 0 4px 10px rgba(0,0,0,.4); } | |
| .btn-yes { background: linear-gradient(180deg,#5ec763,#36953b); border-color:#2f7d33; box-shadow:0 6px 0 #235c26,0 8px 16px rgba(0,0,0,.4); color:#fff; } | |
| .btn-no { background: linear-gradient(180deg,#e85b44,#be3422); border-color:#9c2a1b; box-shadow:0 6px 0 #7e2317,0 8px 16px rgba(0,0,0,.4); color:#fff; } | |
| .btn-unsure { background: linear-gradient(180deg,#f0c75e,#cc922f); border-color:#a9741f; box-shadow:0 6px 0 #875b16,0 8px 16px rgba(0,0,0,.4); color:#3a2600; } | |
| /* image buttons (yes / no / not sure) — full painted buttons with text baked in */ | |
| .imgbtn { background:none; border:none; box-shadow:none; padding:0; border-radius:0; } | |
| .imgbtn img { height: 60px; width:auto; display:block; } | |
| .imgbtn:hover { filter: brightness(1.08); } | |
| .imgbtn:active { transform: translateY(2px); box-shadow:none; } | |
| @media (max-width: 900px) { .imgbtn img { height: 52px; } } | |
| /* blank button images with our own text on top */ | |
| .imgtext { background:none; border:none; box-shadow:none; border-radius:0; | |
| background-repeat:no-repeat; background-position:center; background-size:100% 100%; | |
| font-family:'Cinzel',serif; font-weight:700; font-size:1rem; | |
| min-width:150px; height:56px; padding:0 26px; } | |
| .imgtext.green { background-image:url(./images/blank_green_button.png); color:#eafff0; text-shadow:0 1px 2px rgba(0,0,0,.4); } | |
| .imgtext.red { background-image:url(./images/blank_red_button.png); color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.4); } | |
| .imgtext.yellow { background-image:url(./images/empty_yellow_btn.png); color:#3a2600; } | |
| .imgtext:hover { filter:brightness(1.08); } | |
| .imgtext:active { transform:translateY(2px); } | |
| @media (max-width: 900px) { .imgtext { height:50px; min-width:130px; font-size:.92rem; } } | |
| .cat { font-size: 1.05rem; padding: 15px 22px; } | |
| .start { background:none; border:none; box-shadow:none; color:#b9a7e6; font-weight:600; | |
| font-size:.85rem; letter-spacing:1px; text-transform:uppercase; margin-top: 16px; } | |
| .start:hover { color: var(--gold); filter:none; } .start:active{ transform:none; box-shadow:none; } | |
| .loading { color:#cdbdf0; font-style: italic; margin-top: 12px; min-height: 1.2em; } | |
| .end-msg { font-family:'Cinzel',serif; font-size: 1.5rem; line-height: 1.4; margin: 12px 0 22px; } | |
| .confetti { font-size: 2.6rem; animation: pop .5s ease; } | |
| @keyframes pop { 0%{transform:scale(0);} 70%{transform:scale(1.3);} 100%{transform:scale(1);} } | |
| /* answers panel */ | |
| #log { display: none; } | |
| .logrow { display: flex; justify-content: space-between; gap: 10px; padding: 7px 0; | |
| border-top: 1px solid rgba(255,255,255,.1); font-size: 10px; | |
| font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; } | |
| .logrow:first-of-type { border-top: none; } | |
| .logrow .lq { color: #ddd2f6; } | |
| .logrow .la { font-weight: 700; white-space: nowrap; } | |
| .la.yes { color: #6fe074; } .la.no { color: #ff7a63; } .la.maybe { color: var(--gold); } | |
| .empty-hint { color:#9d8fc9; font-style:italic; font-size:.9rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="stars" id="stars"></div> | |
| <div class="topbar"> | |
| <div class="logo">🔮 <b>THE ORACLE</b></div> | |
| <div class="top-controls"> | |
| <button class="imgchip" id="learnBtn" title="Teach the Oracle a new thing"><img src="./images/demo_blank_btn.png" alt="" /><span>+ Teach</span></button> | |
| <button class="iconbtn" id="mute" title="Sound on/off"><img src="./images/volume.png" alt="Sound" /></button> | |
| </div> | |
| </div> | |
| <div class="grid"> | |
| <!-- left: how it works --> | |
| <img class="howimg" src="https://huggingface.co/buckets/build-small-hackathon/oracle-storage/resolve/how_it_work.png" alt="How it works — answer the questions in your mind and I will reveal your answer." /> | |
| <!-- center stage --> | |
| <main class="center"> | |
| <img class="title-img" src="./images/orcale-title.png" alt="The Oracle — think of something, I shall pluck it from your mind." /> | |
| <div class="stage"> | |
| <img class="globe" id="ball" src="https://huggingface.co/buckets/build-small-hackathon/oracle-storage/resolve/center_globe.png" alt="Crystal ball" /> | |
| </div> | |
| <div class="panel active" id="intro"> | |
| <div class="question">What are you thinking of?</div> | |
| <div class="btns"> | |
| <button class="imgtext green" data-cat="animal">🐾 Animal</button> | |
| <button class="imgtext yellow" data-cat="fruit">🍎 Fruit</button> | |
| <button class="imgtext green" data-cat="vegetable">🥕 Vegetable</button> | |
| </div> | |
| </div> | |
| <div class="panel" id="game"> | |
| <div class="stepper" id="stepper"></div> | |
| <div class="status" id="status"></div> | |
| <div class="question" id="question"></div> | |
| <div class="btns" id="options"></div> | |
| <div class="loading" id="loading"></div> | |
| <div><button class="start" id="restart1">✦ Start over</button></div> | |
| </div> | |
| <div class="panel" id="guessView"> | |
| <div class="status">The Oracle senses…</div> | |
| <div class="reveal" id="revealLine"></div> | |
| <div class="guess-big" id="guessText"></div> | |
| <div class="btns"> | |
| <button class="imgbtn" id="guessYes"><img src="./images/yes_button.png" alt="Yes" /></button> | |
| <button class="imgbtn" id="guessNo"><img src="./images/no_button.png" alt="No" /></button> | |
| </div> | |
| </div> | |
| <div class="panel" id="teachView"> | |
| <div class="status" id="teachStatus">🌫️ The Oracle is stumped…</div> | |
| <div class="guess-big" id="teachPrompt">What were you thinking of?</div> | |
| <div class="btns" id="teachCats" style="display:none"> | |
| <button class="imgtext green" data-tcat="animal">🐾 Animal</button> | |
| <button class="imgtext yellow" data-tcat="fruit">🍎 Fruit</button> | |
| <button class="imgtext green" data-tcat="vegetable">🥕 Vegetable</button> | |
| </div> | |
| <input id="teachInput" class="teach-input" type="text" placeholder="Type its name…" autocomplete="off" /> | |
| <div class="btns"> | |
| <button class="imgtext yellow" id="teachBtn">✦ Teach the Oracle</button> | |
| <button class="imgtext green" id="teachSkip">Skip</button> | |
| </div> | |
| <div class="loading" id="teachMsg"></div> | |
| </div> | |
| <div class="panel" id="endView"> | |
| <div class="confetti" id="endIcon">🔮</div> | |
| <div class="end-msg" id="endMsg"></div> | |
| <div class="btns"><button class="imgtext yellow" id="restart2">Play again</button></div> | |
| </div> | |
| </main> | |
| <!-- right: your answers --> | |
| <aside class="answers" id="logCard"> | |
| <img class="ans-top" src="./images/answer_top.png" alt="Your answers" /> | |
| <div class="ans-mid"> | |
| <div id="log"></div> | |
| <div class="empty-hint" id="logEmpty">No answers yet…</div> | |
| </div> | |
| <img class="ans-bottom" src="./images/answer_bottom.png" alt="" /> | |
| </aside> | |
| </div> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| const stars = document.getElementById("stars"); | |
| for (let i = 0; i < 70; i++) { const s = document.createElement("div"); s.className = "star"; | |
| s.style.left = Math.random()*100+"vw"; s.style.top = Math.random()*100+"vh"; | |
| s.style.animationDelay = (-Math.random()*3)+"s"; s.style.transform = `scale(${0.5+Math.random()})`; stars.appendChild(s); } | |
| let _client = null; | |
| const getClient = async () => (_client ??= await Client.connect(window.location.origin)); | |
| const byId = (id) => document.getElementById(id); | |
| const ball = byId("ball"); | |
| const panels = { intro: byId("intro"), game: byId("game"), guess: byId("guessView"), teach: byId("teachView"), end: byId("endView") }; | |
| const show = (name) => { for (const k in panels) panels[k].classList.toggle("active", k === name); }; | |
| let muted = false; | |
| let actx = null; | |
| function chime(){ if (muted) return; try { actx ??= new (window.AudioContext||window.webkitAudioContext)(); | |
| [0,4,7,12].forEach((semi,i)=>{ const o=actx.createOscillator(),g=actx.createGain(); o.type="sine"; | |
| o.frequency.value=440*Math.pow(2,semi/12); g.gain.setValueAtTime(0.0001,actx.currentTime+i*0.08); | |
| g.gain.exponentialRampToValueAtTime(0.2,actx.currentTime+i*0.08+0.02); | |
| g.gain.exponentialRampToValueAtTime(0.0001,actx.currentTime+i*0.08+0.5); | |
| o.connect(g); g.connect(actx.destination); o.start(actx.currentTime+i*0.08); o.stop(actx.currentTime+i*0.08+0.55); }); } catch(e){} } | |
| // state | |
| let category="animal", history=[], asked=0, currentQ="", currentAttr="", currentGuess=""; | |
| document.querySelectorAll("[data-cat]").forEach(b => b.onclick = () => { | |
| category = b.dataset.cat; history = []; asked = 0; | |
| askModel(); | |
| }); | |
| // map an option to one of the 3 button images (else a styled text button) | |
| function optImage(text) { | |
| const t = String(text).toLowerCase(); | |
| if (/^yes\b/.test(t)) return "./images/yes_button.png"; | |
| if (/^no\b/.test(t)) return "./images/no_button.png"; | |
| if (/not sure|don'?t know|can'?t say/.test(t)) return "./images/i_am_not_sure_btn.png"; | |
| return null; | |
| } | |
| function renderOptions(opts, onPick) { | |
| const box = byId("options"); box.innerHTML = ""; | |
| (opts || []).forEach(o => { | |
| const b = document.createElement("button"); | |
| const img = optImage(o); | |
| if (img) { b.className = "imgbtn"; b.innerHTML = `<img src="${img}" alt="${esc(o)}" />`; } | |
| else { b.className = "imgtext yellow"; b.textContent = o; } // colors/sizes etc. on a blank button | |
| b.onclick = () => onPick(o); box.appendChild(b); | |
| }); | |
| } | |
| const esc = (s) => (s == null ? "" : String(s)).replace(/[&<>]/g, c => ({ "&":"&","<":"<",">":">" }[c])); | |
| function ansClass(a){ const t=String(a).toLowerCase(); | |
| if(/^yes\b/.test(t)) return "yes"; if(/^no\b/.test(t)) return "no"; return "maybe"; } | |
| const ansLabel = (a) => a === "yes" ? "Yes" : a === "no" ? "No" : "Not sure"; | |
| function renderLog() { | |
| const log = byId("log"); | |
| byId("logEmpty").style.display = history.length ? "none" : "block"; | |
| log.style.display = history.length ? "block" : "none"; | |
| log.innerHTML = history.map((h, i) => | |
| `<div class="logrow"><span class="lq">${i + 1}. ${esc(h.question)}</span>` + | |
| `<span class="la ${ansClass(h.answer)}">${esc(ansLabel(h.answer))}</span></div>` | |
| ).join(""); | |
| } | |
| function renderStepper(total, remaining) { | |
| const n = Math.min(8, asked + 1); | |
| byId("stepper").innerHTML = Array.from({length: n}, (_, i) => | |
| `<span class="dot ${i <= asked ? "on" : ""}"></span>`).join(""); | |
| } | |
| async function askModel() { | |
| renderLog(); | |
| show("game"); | |
| byId("options").innerHTML = ""; | |
| byId("status").textContent = ""; | |
| byId("question").textContent = ""; | |
| ball.classList.add("think"); | |
| byId("loading").textContent = "The Oracle gazes into the glass…"; | |
| try { | |
| const c = await getClient(); | |
| const res = await c.predict("/next", { | |
| category, history_json: JSON.stringify(history), asked, | |
| }); | |
| const step = res.data[0]; | |
| ball.classList.remove("think"); byId("loading").textContent = ""; | |
| if (step.action === "giveup") { | |
| showTeach(step.text || "What were you thinking of?"); | |
| } else if (step.action === "guess") { | |
| currentGuess = step.text; | |
| byId("revealLine").textContent = step.reveal || ""; | |
| byId("guessText").textContent = "Is it… " + step.text + "?"; | |
| show("guess"); | |
| } else { | |
| currentQ = step.text; | |
| currentAttr = step.attribute || ""; | |
| renderStepper(step.max, step.remaining); | |
| byId("status").textContent = `Question ${asked + 1}` + (step.remaining ? ` · ${step.remaining} left` : ""); | |
| byId("question").textContent = step.text; | |
| let opts = (step.options && step.options.length ? step.options : ["Yes", "No"]).slice(0, 3); | |
| if (!opts.some(o => /not sure|can'?t say|don'?t know/i.test(o))) opts.push("I am not sure"); | |
| renderOptions(opts, (opt) => { | |
| const answer = /^yes/i.test(opt) ? "yes" : /^no/i.test(opt) ? "no" : "unknown"; | |
| history.push({ attribute: currentAttr, question: currentQ, answer }); | |
| if (answer !== "unknown") asked++; | |
| askModel(); | |
| }); | |
| show("game"); | |
| } | |
| } catch (e) { | |
| ball.classList.remove("think"); | |
| byId("loading").textContent = "The spirits are unreachable…"; | |
| console.error(e); | |
| } | |
| } | |
| byId("guessYes").onclick = () => { | |
| chime(); byId("endIcon").textContent = "🎉"; | |
| byId("endMsg").innerHTML = `The Oracle has read your mind.<br><span style="color:var(--gold)">${currentGuess}</span>!`; | |
| show("end"); | |
| }; | |
| byId("guessNo").onclick = () => { | |
| showTeach("I was wrong! What were you really thinking of?"); | |
| }; | |
| // --- discovery / teach mode --- | |
| function renderReasons(list, name) { | |
| if (!list || !list.length) return ""; | |
| const rows = list.map(c => | |
| `<div class="reason-row"><span class="reason-x">✗</span> You answered ` + | |
| `<b>${esc(c.your_answer)}</b> to “${esc(c.question)}”, but ${esc(c.reason)} ` + | |
| `— it should have been <b style="color:var(--gold)">${esc(c.correct_answer)}</b>.</div>` | |
| ).join(""); | |
| return `<div class="reasons"><div class="reasons-h">That's where you threw me off:</div>${rows}</div>`; | |
| } | |
| let teachMode = "game"; // "game" (after a wrong/giveup) or "learn" (standalone) | |
| let teachCategory = "animal"; // chosen category in standalone mode | |
| function showTeach(prompt) { // called after a wrong guess / giveup | |
| teachMode = "game"; | |
| byId("teachStatus").textContent = "🌫️ The Oracle is stumped…"; | |
| byId("teachCats").style.display = "none"; | |
| byId("teachPrompt").textContent = prompt; | |
| byId("teachInput").value = ""; | |
| byId("teachMsg").textContent = ""; | |
| show("teach"); | |
| setTimeout(() => byId("teachInput").focus(), 50); | |
| } | |
| function openLearn() { // standalone "+ Teach" button | |
| teachMode = "learn"; | |
| teachCategory = "animal"; | |
| byId("teachStatus").textContent = "📖 Teach the Oracle"; | |
| byId("teachPrompt").textContent = "Pick a type, then name something new for me to learn."; | |
| byId("teachCats").style.display = "flex"; | |
| document.querySelectorAll("[data-tcat]").forEach(b => | |
| b.classList.toggle("sel", b.dataset.tcat === teachCategory)); | |
| byId("teachInput").value = ""; | |
| byId("teachMsg").textContent = ""; | |
| show("teach"); | |
| setTimeout(() => byId("teachInput").focus(), 50); | |
| } | |
| document.querySelectorAll("[data-tcat]").forEach(b => b.onclick = () => { | |
| teachCategory = b.dataset.tcat; | |
| document.querySelectorAll("[data-tcat]").forEach(x => x.classList.toggle("sel", x === b)); | |
| byId("teachInput").focus(); | |
| }); | |
| async function teach() { | |
| const name = byId("teachInput").value.trim(); | |
| if (!name) { byId("teachInput").focus(); return; } | |
| const cat = teachMode === "learn" ? teachCategory : category; | |
| const hist = teachMode === "learn" ? [] : history; | |
| byId("teachMsg").textContent = "The Oracle studies it closely…"; | |
| try { | |
| const c = await getClient(); | |
| const res = await c.predict("/learn", { category: cat, name, history_json: JSON.stringify(hist) }); | |
| const r = res.data[0] || {}; | |
| chime(); | |
| byId("endIcon").textContent = "📖"; | |
| let msg; | |
| if (r.status === "known") { | |
| msg = `The Oracle already knew <span style="color:var(--gold)">${esc(r.name)}</span>.`; | |
| } else if (r.status === "learned") { | |
| msg = `The Oracle has learned <span style="color:var(--gold)">${esc(r.name)}</span>!<br>It now knows ${r.learned} thing${r.learned === 1 ? "" : "s"} about it.`; | |
| } else { | |
| msg = `The Oracle could not grasp it this time.`; | |
| } | |
| msg += renderReasons(r.contradictions, r.name); | |
| byId("endMsg").innerHTML = msg; | |
| show("end"); | |
| } catch (e) { | |
| console.error(e); | |
| byId("teachMsg").textContent = "The spirits are unreachable…"; | |
| } | |
| } | |
| byId("teachBtn").onclick = teach; | |
| byId("teachInput").addEventListener("keydown", (e) => { if (e.key === "Enter") teach(); }); | |
| byId("teachSkip").onclick = () => { | |
| if (teachMode === "learn") { show("intro"); return; } | |
| byId("endIcon").textContent = "🙇"; | |
| byId("endMsg").innerHTML = `You have bested the Oracle!<br>Its vision was too cloudy this time.`; | |
| show("end"); | |
| }; | |
| byId("learnBtn").onclick = openLearn; | |
| const restart = () => { history = []; asked = 0; currentAttr = ""; renderLog(); show("intro"); }; | |
| byId("restart1").onclick = restart; | |
| byId("restart2").onclick = restart; | |
| byId("mute").onclick = () => { muted = !muted; byId("mute").classList.toggle("muted", muted); }; | |
| renderLog(); | |
| </script> | |
| </body> | |
| </html> | |