feat(37-03): VoiceModeToggle three-pill component + useVoiceMode hook

- VoiceModeToggle: Text / Voice In / Full Voice pills with active/inactive styling
- Auto-play checkbox in full_voice mode, persists to nexus:voice:autoplay in localStorage
- useVoiceMode: reads/writes voiceMode via PATCH /api/nexus/settings with loading state
  (deviation Rule 3: created missing blocking dependency for VoiceModeToggle)
This commit is contained in:
Nexus Dev 2026-04-04 02:37:10 +00:00
parent 6b60f42a25
commit 8bf2a65a0b

View file

@ -0,0 +1,59 @@
import { useState } from "react";
import { useVoiceMode } from "@/hooks/useVoiceMode";
type VoiceMode = "text" | "voice_input" | "full_voice";
const PILLS: { label: string; value: VoiceMode }[] = [
{ label: "Text", value: "text" },
{ label: "Voice In", value: "voice_input" },
{ label: "Full Voice", value: "full_voice" },
];
export function VoiceModeToggle() {
const { mode, setMode, isLoading } = useVoiceMode();
const [autoPlay, setAutoPlay] = useState<boolean>(
() => localStorage.getItem("nexus:voice:autoplay") === "true"
);
function handleAutoPlayChange(checked: boolean) {
setAutoPlay(checked);
localStorage.setItem("nexus:voice:autoplay", String(checked));
}
return (
<div className="flex flex-col gap-1">
<div
role="group"
aria-label="Voice mode"
className="flex items-center gap-1"
>
{PILLS.map(({ label, value }) => (
<button
key={value}
type="button"
disabled={isLoading}
onClick={() => setMode(value)}
className={[
"rounded-full px-3 py-1 text-xs font-medium transition-colors",
mode === value
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground",
].join(" ")}
>
{label}
</button>
))}
</div>
{mode === "full_voice" && (
<label className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
<input
type="checkbox"
checked={autoPlay}
onChange={(e) => handleAutoPlayChange(e.target.checked)}
/>
Auto-play voice responses
</label>
)}
</div>
);
}