Archived
105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
export interface UseVoiceSearchResult {
|
|
isListening: boolean;
|
|
transcript: string;
|
|
isSupported: boolean;
|
|
hasError: boolean;
|
|
errorMessage: string | null;
|
|
startListening: () => void;
|
|
stopListening: () => void;
|
|
}
|
|
|
|
/**
|
|
* Hook for voice search using the Web Speech API.
|
|
* Returns a microphone control interface.
|
|
* Gracefully degrades when SpeechRecognition is unavailable.
|
|
*/
|
|
export function useVoiceSearch(): UseVoiceSearchResult {
|
|
const [isListening, setIsListening] = useState(false);
|
|
const [transcript, setTranscript] = useState("");
|
|
const [isSupported, setIsSupported] = useState(false);
|
|
const [hasError, setHasError] = useState(false);
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
|
|
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
|
const mountedRef = useRef(true);
|
|
|
|
useEffect(() => {
|
|
mountedRef.current = true;
|
|
// Check for SpeechRecognition support (standard + webkit prefix)
|
|
const SpeechRecognitionCtor =
|
|
(window as unknown as Record<string, unknown>).SpeechRecognition ??
|
|
(window as unknown as Record<string, unknown>).webkitSpeechRecognition;
|
|
|
|
if (typeof SpeechRecognitionCtor === "function") {
|
|
setIsSupported(true);
|
|
const recognition = new (SpeechRecognitionCtor as new () => SpeechRecognition)();
|
|
recognition.continuous = false;
|
|
recognition.interimResults = false;
|
|
recognition.lang = "en-US";
|
|
|
|
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
|
const resultText = event.results[0]?.[0]?.transcript ?? "";
|
|
if (mountedRef.current) {
|
|
setTranscript(resultText);
|
|
setHasError(false);
|
|
setErrorMessage(null);
|
|
}
|
|
};
|
|
|
|
recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
|
|
if (mountedRef.current) {
|
|
setHasError(true);
|
|
setErrorMessage(event.error);
|
|
setIsListening(false);
|
|
}
|
|
};
|
|
|
|
recognition.onend = () => {
|
|
if (mountedRef.current) {
|
|
setIsListening(false);
|
|
}
|
|
};
|
|
|
|
recognitionRef.current = recognition;
|
|
}
|
|
|
|
return () => {
|
|
mountedRef.current = false;
|
|
if (recognitionRef.current) {
|
|
recognitionRef.current.abort();
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const startListening = useCallback(() => {
|
|
if (!recognitionRef.current) return;
|
|
setTranscript("");
|
|
setHasError(false);
|
|
setErrorMessage(null);
|
|
try {
|
|
recognitionRef.current.start();
|
|
setIsListening(true);
|
|
} catch {
|
|
// May throw if already started
|
|
setIsListening(false);
|
|
}
|
|
}, []);
|
|
|
|
const stopListening = useCallback(() => {
|
|
if (!recognitionRef.current) return;
|
|
recognitionRef.current.stop();
|
|
setIsListening(false);
|
|
}, []);
|
|
|
|
return {
|
|
isListening,
|
|
transcript,
|
|
isSupported,
|
|
hasError,
|
|
errorMessage,
|
|
startListening,
|
|
stopListening,
|
|
};
|
|
} |