- 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)
59 lines
1.7 KiB
TypeScript
59 lines
1.7 KiB
TypeScript
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>
|
|
);
|
|
}
|