nexus/ui/src/hooks/useVoiceMode.ts
Nexus Dev 0d0b17c8a0 feat(37-02): encodeWav utility, useVadRecorder + useVoiceMode hooks
- encodeWav: 44-byte WAV header encoder (RIFF/WAVE/fmt/data), PCM mono 16-bit
- useVadRecorder: wraps useMicVAD with startOnLoad:false, auto-stop on speech end, POSTs to /api/transcribe
- useVoiceMode: reads/writes voiceMode from GET/PATCH /api/nexus/settings with optimistic update
2026-04-04 03:55:50 +00:00

71 lines
1.8 KiB
TypeScript

import { useState, useEffect } from "react";
type VoiceMode = "text" | "voice_input" | "full_voice";
interface UseVoiceModeReturn {
mode: VoiceMode;
setMode: (next: VoiceMode) => Promise<void>;
isLoading: boolean;
}
export function useVoiceMode(): UseVoiceModeReturn {
const [mode, setModeState] = useState<VoiceMode>("text");
const [isLoading, setIsLoading] = useState(true);
// Load current voiceMode from nexus-settings on mount
useEffect(() => {
let cancelled = false;
const load = async () => {
try {
const res = await fetch("/api/nexus/settings", {
credentials: "include",
});
if (res.ok && !cancelled) {
const data = (await res.json()) as { voiceMode?: string };
const raw = data.voiceMode;
if (raw === "voice_input" || raw === "full_voice" || raw === "text") {
setModeState(raw as VoiceMode);
}
}
} catch (err) {
console.error("[useVoiceMode] Failed to load settings:", err);
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
};
load();
return () => {
cancelled = true;
};
}, []);
const setMode = async (next: VoiceMode): Promise<void> => {
const previous = mode;
// Optimistic update
setModeState(next);
try {
const res = await fetch("/api/nexus/settings", {
method: "PATCH",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ voiceMode: next }),
});
if (!res.ok) {
throw new Error(`PATCH /api/nexus/settings returned ${res.status}`);
}
} catch (err) {
console.error("[useVoiceMode] Failed to update voiceMode:", err);
// Revert on error
setModeState(previous);
}
};
return { mode, setMode, isLoading };
}