// Puck's settings — the prototype's "Tweaks" reborn as product UI, // styled with the app's own theme tokens. import * as React from "react"; import { requestNotifyPermission } from "../lib/notify"; import { ACCENTS, type Settings } from "../lib/settings"; import { type Pos, useDrag } from "../lib/useDrag"; import { KOKORO_VOICES, listVoices, previewVoice, sayVoices, type VoiceEngine } from "../lib/voice"; function Row({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } function Pills({ value, options, onChange, }: { value: T; options: { value: T; label: string }[]; onChange: (v: T) => void; }) { return (
{options.map((o) => ( ))}
); } function Slider({ value, onChange }: { value: number; onChange: (v: number) => void }) { return (
onChange(Number(e.target.value))} /> {value}
); } export function SettingsPanel({ open, pos, onPos, onClose, settings, onChange, overlay = false, onPatrol, onSleep, }: { open: boolean; pos: Pos; onPos: (p: Pos) => void; onClose: () => void; settings: Settings; onChange: (key: K, value: Settings[K]) => void; /** Overlay (WKWebView) forces the Mac `say` engine — browser audio is dead there. */ overlay?: boolean; onPatrol: () => void; onSleep: () => void; }) { const drag = useDrag(pos, onPos, { marginR: 120, marginB: 80 }); // the overlay can only use the Mac voice; elsewhere honor the chosen engine const engine: VoiceEngine = overlay ? "say" : settings.voiceEngine; const [sayList, setSayList] = React.useState<{ id: string; lang: string }[]>([]); React.useEffect(() => { if (open && engine === "say") void sayVoices().then(setSayList); }, [open, engine]); if (!open) return null; return (
{ if ((e.target as HTMLElement).closest(".comp-x")) return; drag(e); }} >
Settings how Puck looks, sounds, behaves
Look
onChange("theme", v)} />
{ACCENTS.map((c) => (
Creature
onChange("form", v)} /> onChange("presence", v)} /> onChange("mischief", v)} />
Voice & behaviour
onChange("voiceSound", v)} /> {settings.voiceSound !== "off" && ( <> {!overlay && ( onChange("voiceEngine", v)} /> )}
{engine === "say" ? ( ) : engine === "kokoro" ? ( ) : ( )}
{engine === "say" && (
Mac's built-in voice — works everywhere, including the overlay. For much better voices, install Premium/Siri voices in System Settings → Accessibility → Spoken Content → System Voice.
)} {engine === "kokoro" && (
Neural voice runs locally in your browser. First use downloads the ~80MB model once (cached after); the system voice covers the gap while it loads.
)} onChange("speakShouts", v === "on")} /> )} onChange("voice", v)} /> onChange("notify", v)} /> { // permission request must ride a user gesture; only persist on if granted if (v === "on") onChange("osNotify", await requestNotifyPermission()); else onChange("osNotify", false); }} />
Eyes (vision)
onChange("visionMode", v)} />
{settings.visionMode === "off" ? "Puck's eyes are shut." : settings.visionMode === "ondemand" ? "Looks only when you pick “Look around”. No cost unattended." : "Looks every ~45s while you're active; pauses when idle so the cloud GPU scales to zero."}
); }