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:
parent
6b60f42a25
commit
8bf2a65a0b
1 changed files with 59 additions and 0 deletions
59
ui/src/components/VoiceModeToggle.tsx
Normal file
59
ui/src/components/VoiceModeToggle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue