Archived
feat: mobile book search and discovery with voice search, suggestions, and responsive layout
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user