case0 / web /src /ui /assistant.tsx
HusseinEid's picture
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>
)
}