FarmerHelp / frontend /src /context /VoiceContext.jsx
Gankit12's picture
All Code
b64de39
Raw
History Blame Contribute Delete
8.46 kB
/* eslint-disable react-refresh/only-export-components */
/**
* VoiceContext - Voice recognition state management.
*
* Manages:
* - Web Speech API integration (SpeechRecognition)
* - Listening state and current transcript
* - Command history
* - Voice settings (language, continuous mode)
*
* Gracefully degrades when the Web Speech API is unavailable.
*/
import {
createContext,
useReducer,
useCallback,
useRef,
useEffect,
} from "react";
import { LANGUAGES, FEATURES } from "@utils/constants";
// ---------------------------------------------------------------------------
// Browser Feature Detection
// ---------------------------------------------------------------------------
const SpeechRecognitionAPI =
typeof window !== "undefined"
? window.SpeechRecognition || window.webkitSpeechRecognition
: null;
const SPEECH_API_AVAILABLE =
Boolean(SpeechRecognitionAPI) && FEATURES.VOICE_ENABLED;
// Language code mapping for Web Speech API
const VOICE_LANG_MAP = {
[LANGUAGES.EN]: "en-IN",
[LANGUAGES.HI]: "hi-IN",
};
// ---------------------------------------------------------------------------
// Helpers (pure, declared before any usage)
// ---------------------------------------------------------------------------
const SPEECH_ERROR_MAP = {
"not-allowed":
"Microphone access was denied. Please allow microphone permissions.",
"no-speech": "No speech was detected. Please try again.",
"audio-capture": "No microphone was found. Please check your device.",
network: "A network error occurred during speech recognition.",
aborted: "Speech recognition was aborted.",
"service-not-allowed": "Speech recognition service is not allowed.",
};
function mapSpeechError(errorCode) {
return (
SPEECH_ERROR_MAP[errorCode] || `Speech recognition error: ${errorCode}`
);
}
// ---------------------------------------------------------------------------
// Initial State
// ---------------------------------------------------------------------------
const MAX_HISTORY_SIZE = 50;
const INITIAL_STATE = {
isSupported: SPEECH_API_AVAILABLE,
isListening: false,
transcript: "",
interimTranscript: "",
commandHistory: [],
error: null,
settings: {
language: LANGUAGES.EN,
continuous: false,
interimResults: true,
},
};
// ---------------------------------------------------------------------------
// Action Types
// ---------------------------------------------------------------------------
const ACTION_TYPES = {
START_LISTENING: "START_LISTENING",
STOP_LISTENING: "STOP_LISTENING",
SET_TRANSCRIPT: "SET_TRANSCRIPT",
SET_INTERIM: "SET_INTERIM",
ADD_TO_HISTORY: "ADD_TO_HISTORY",
CLEAR_HISTORY: "CLEAR_HISTORY",
SET_ERROR: "SET_ERROR",
CLEAR_ERROR: "CLEAR_ERROR",
UPDATE_SETTINGS: "UPDATE_SETTINGS",
RESET: "RESET",
};
// ---------------------------------------------------------------------------
// Reducer
// ---------------------------------------------------------------------------
function voiceReducer(state, action) {
switch (action.type) {
case ACTION_TYPES.START_LISTENING:
return {
...state,
isListening: true,
transcript: "",
interimTranscript: "",
error: null,
};
case ACTION_TYPES.STOP_LISTENING:
return { ...state, isListening: false, interimTranscript: "" };
case ACTION_TYPES.SET_TRANSCRIPT:
return { ...state, transcript: action.payload };
case ACTION_TYPES.SET_INTERIM:
return { ...state, interimTranscript: action.payload };
case ACTION_TYPES.ADD_TO_HISTORY: {
const entry = {
text: action.payload,
timestamp: new Date().toISOString(),
};
const updatedHistory = [entry, ...state.commandHistory].slice(
0,
MAX_HISTORY_SIZE,
);
return { ...state, commandHistory: updatedHistory };
}
case ACTION_TYPES.CLEAR_HISTORY:
return { ...state, commandHistory: [] };
case ACTION_TYPES.SET_ERROR:
return { ...state, error: action.payload, isListening: false };
case ACTION_TYPES.CLEAR_ERROR:
return { ...state, error: null };
case ACTION_TYPES.UPDATE_SETTINGS:
return {
...state,
settings: { ...state.settings, ...action.payload },
};
case ACTION_TYPES.RESET:
return { ...INITIAL_STATE };
default:
return state;
}
}
// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------
export const VoiceContext = createContext(null);
// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------
export function VoiceProvider({ children }) {
const [state, dispatch] = useReducer(voiceReducer, INITIAL_STATE);
const recognitionRef = useRef(null);
// Create / re-create the SpeechRecognition instance when settings change
useEffect(() => {
if (!SPEECH_API_AVAILABLE) {
return;
}
const recognition = new SpeechRecognitionAPI();
recognition.lang = VOICE_LANG_MAP[state.settings.language] || "en-IN";
recognition.continuous = state.settings.continuous;
recognition.interimResults = state.settings.interimResults;
recognition.onresult = (event) => {
let interim = "";
let finalText = "";
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
if (result.isFinal) {
finalText += result[0].transcript;
} else {
interim += result[0].transcript;
}
}
if (finalText) {
dispatch({
type: ACTION_TYPES.SET_TRANSCRIPT,
payload: finalText.trim(),
});
dispatch({
type: ACTION_TYPES.ADD_TO_HISTORY,
payload: finalText.trim(),
});
}
if (interim) {
dispatch({ type: ACTION_TYPES.SET_INTERIM, payload: interim });
}
};
recognition.onerror = (event) => {
const message = mapSpeechError(event.error);
dispatch({ type: ACTION_TYPES.SET_ERROR, payload: message });
};
recognition.onend = () => {
dispatch({ type: ACTION_TYPES.STOP_LISTENING });
};
recognitionRef.current = recognition;
return () => {
recognition.onresult = null;
recognition.onerror = null;
recognition.onend = null;
try {
recognition.abort();
} catch {
// Ignore abort errors during cleanup
}
};
}, [
state.settings.language,
state.settings.continuous,
state.settings.interimResults,
]);
// --- Actions ---------------------------------------------------------------
const startListening = useCallback(() => {
if (!SPEECH_API_AVAILABLE) {
dispatch({
type: ACTION_TYPES.SET_ERROR,
payload: "Voice recognition is not supported in this browser.",
});
return;
}
const recognition = recognitionRef.current;
if (!recognition) {
return;
}
try {
recognition.start();
dispatch({ type: ACTION_TYPES.START_LISTENING });
} catch {
// Already started -- restart
try {
recognition.stop();
setTimeout(() => {
recognition.start();
dispatch({ type: ACTION_TYPES.START_LISTENING });
}, 100);
} catch {
dispatch({
type: ACTION_TYPES.SET_ERROR,
payload: "Failed to start voice recognition.",
});
}
}
}, []);
const stopListening = useCallback(() => {
const recognition = recognitionRef.current;
if (recognition) {
try {
recognition.stop();
} catch {
// Ignore stop errors
}
}
dispatch({ type: ACTION_TYPES.STOP_LISTENING });
}, []);
const clearHistory = useCallback(() => {
dispatch({ type: ACTION_TYPES.CLEAR_HISTORY });
}, []);
const clearError = useCallback(() => {
dispatch({ type: ACTION_TYPES.CLEAR_ERROR });
}, []);
const updateSettings = useCallback((newSettings) => {
dispatch({ type: ACTION_TYPES.UPDATE_SETTINGS, payload: newSettings });
}, []);
const value = {
...state,
startListening,
stopListening,
clearHistory,
clearError,
updateSettings,
};
return (
<VoiceContext.Provider value={value}>{children}</VoiceContext.Provider>
);
}