Spaces:
Running
Running
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55 | // Det. Hale — your partner on the wire. Contextual, spoiler-safe hints from the server. | |
| import { useEffect, useState } from 'preact/hooks' | |
| import { getHint } from '../api' | |
| import { useTypewriter } from '../engine/pixel' | |
| import { useGame } from '../store' | |
| import { Portrait } from './components' | |
| const HIDDEN = new Set(['title', 'verdict', 'share', 'boot']) | |
| const FALLBACK = 'Work the evidence, detective. Find where a statement and a fact disagree, and press there.' | |
| export function Assistant() { | |
| const g = useGame() | |
| const [open, setOpen] = useState(false) | |
| const [hint, setHint] = useState('') | |
| const screen = g.state.screen | |
| useEffect(() => { | |
| const t = () => setOpen((o) => !o) | |
| const c = () => setOpen(false) | |
| window.addEventListener('toggle-hint', t) | |
| window.addEventListener('close-hint', c) | |
| return () => { | |
| window.removeEventListener('toggle-hint', t) | |
| window.removeEventListener('close-hint', c) | |
| } | |
| }, []) | |
| useEffect(() => { | |
| setOpen(false) | |
| }, [screen]) | |
| useEffect(() => { | |
| if (!open) return | |
| let alive = true | |
| setHint('') | |
| getHint(g.runId, screen) | |
| .then((r) => { | |
| if (alive) setHint(r.hint || FALLBACK) | |
| }) | |
| .catch(() => { | |
| if (alive) setHint(FALLBACK) | |
| }) | |
| return () => { | |
| alive = false | |
| } | |
| }, [open, screen, g.runId]) | |
| const [typed, done] = useTypewriter(open ? hint : '', g.state.tweaks.typeSpeed || 18, open) | |
| if (HIDDEN.has(screen) || !open) return null | |
| return ( | |
| <div class="assistant"> | |
| <div class="assistant__panel"> | |
| <div class="between" style={{ marginBottom: 8 }}> | |
| <div class="row" style={{ gap: 8, alignItems: 'center' }}> | |
| <div style={{ background: 'var(--ink-1)', boxShadow: 'inset 0 0 0 2px var(--ink-0)', padding: 2 }}> | |
| <Portrait id="detective" px={3} /> | |
| </div> | |
| <div class="col" style={{ gap: 1 }}> | |
| <span class="t-display amber" style={{ fontSize: 9 }}>DET. HALE</span> | |
| <span class="t-label" style={{ fontSize: 7 }}>YOUR PARTNER · ON THE WIRE</span> | |
| </div> | |
| </div> | |
| <button class="assistant__x" onClick={() => setOpen(false)}>✕</button> | |
| </div> | |
| <p class="t-body" style={{ fontSize: 14, lineHeight: 1.5, color: 'var(--bone-2)' }}> | |
| “{typed} | |
| {!done && <span class="cursor" />}” | |
| </p> | |
| </div> | |
| </div> | |
| ) | |
| } | |