Grow the companion, fix idle fallback, add update banner

Companion: bump the corner mascot ~12% (128→144) and make animation
resolution robust — fall back to the idle clip when a mood has no clip of
its own, and pin the always-asleep cat to one pose so its Lottie never
reloads. This stops a bare emoji from replacing the cute companion on idle.

Update notifications: serve a build id at /api/version (a hash of the
embedded dist/index.html, which changes on every frontend deploy). The
client records its load-time id, polls every 90s (and on tab focus), and
floats a Mandarin-first "new version available" banner offering a refresh
when the id changes.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
prosolis
2026-06-25 22:45:23 -07:00
parent a634994d25
commit 95123e8c49
7 changed files with 180 additions and 7 deletions

View File

@@ -0,0 +1,52 @@
import { useEffect, useRef, useState } from 'react'
import { api } from '../api/client'
// How often to ask the server whether a newer frontend has shipped. Gentle —
// a deploy is rare, and the check is a tiny no-store GET.
const POLL_MS = 90_000
// useVersionWatch records the build id the app loaded with, then quietly polls
// /api/version. When the server reports a different id, a new version has been
// deployed and `updateAvailable` flips true (and stays true) so the UI can
// offer a refresh. Network blips are ignored — it only ever reacts to a real,
// confirmed change. Returns false until the baseline is established.
export function useVersionWatch(): boolean {
const [updateAvailable, setUpdateAvailable] = useState(false)
// The version we're currently running. Null until the first successful fetch.
const baseline = useRef<string | null>(null)
useEffect(() => {
let active = true
const check = async () => {
try {
const { version } = await api.version()
if (!active || !version) return
if (baseline.current === null) {
baseline.current = version // first read: this is "us"
} else if (version !== baseline.current) {
setUpdateAvailable(true)
}
} catch {
/* offline / server bounce — try again next tick, never alarm the user */
}
}
check()
const id = setInterval(check, POLL_MS)
// Re-check the moment she returns to the tab, so a deploy that happened while
// she was away surfaces right away instead of up to POLL_MS later.
const onVisible = () => {
if (document.visibilityState === 'visible') check()
}
document.addEventListener('visibilitychange', onVisible)
return () => {
active = false
clearInterval(id)
document.removeEventListener('visibilitychange', onVisible)
}
}, [])
return updateAvailable
}