From 5da973d361ce1ac880954ca4e19f90505ca4af91 Mon Sep 17 00:00:00 2001 From: Mohammad-Hassan027 Date: Tue, 7 Jul 2026 20:15:40 +0530 Subject: [PATCH] fix(ai-voice): resolve speech recognition typescript and closure errors Creates a global ambient declaration file for the Web Speech API to resolve TS2717 conflicts across multiple pages. Removes duplicate module-scoped declarations from ai-voice and ai-tutor pages. Fixes a stale closure bug in the AI Voice page's onend handler by introducing a ref for state. Closes #76 --- app/(dashboard)/ai-tutor/old-page.tsx | 37 --------------- app/(dashboard)/ai-tutor/page.tsx | 37 --------------- app/(dashboard)/ai-voice/page.tsx | 39 ++++++++-------- types/speech-recognition.d.ts | 65 +++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 93 deletions(-) create mode 100644 types/speech-recognition.d.ts diff --git a/app/(dashboard)/ai-tutor/old-page.tsx b/app/(dashboard)/ai-tutor/old-page.tsx index 5b021f5..bec4bd3 100644 --- a/app/(dashboard)/ai-tutor/old-page.tsx +++ b/app/(dashboard)/ai-tutor/old-page.tsx @@ -150,44 +150,7 @@ function getAudioExtension(mimeType: string) { return "webm" } -declare global { - interface Window { - SpeechRecognition?: { - new (): SpeechRecognition - } - webkitSpeechRecognition?: { - new (): SpeechRecognition - } - } -} - -interface SpeechRecognition extends EventTarget { - continuous: boolean - interimResults: boolean - lang: string - onresult: ((event: SpeechRecognitionEvent) => void) | null - onerror: ((event: SpeechRecognitionErrorEvent) => void) | null - onend: (() => void) | null - start(): void - stop(): void -} -interface SpeechRecognitionEvent { - resultIndex: number - results: { - [index: number]: { - isFinal: boolean - 0: { - transcript: string - } - } - length: number - } -} - -interface SpeechRecognitionErrorEvent { - error: string -} function AITutorContent() { const searchParams = useSearchParams() diff --git a/app/(dashboard)/ai-tutor/page.tsx b/app/(dashboard)/ai-tutor/page.tsx index d4f9935..86fdeda 100644 --- a/app/(dashboard)/ai-tutor/page.tsx +++ b/app/(dashboard)/ai-tutor/page.tsx @@ -189,44 +189,7 @@ function getAudioExtension(mimeType: string) { return "webm" } -declare global { - interface Window { - SpeechRecognition?: { - new (): SpeechRecognition - } - webkitSpeechRecognition?: { - new (): SpeechRecognition - } - } -} - -interface SpeechRecognition extends EventTarget { - continuous: boolean - interimResults: boolean - lang: string - onresult: ((event: SpeechRecognitionEvent) => void) | null - onerror: ((event: SpeechRecognitionErrorEvent) => void) | null - onend: (() => void) | null - start(): void - stop(): void -} -interface SpeechRecognitionEvent { - resultIndex: number - results: { - [index: number]: { - isFinal: boolean - 0: { - transcript: string - } - } - length: number - } -} - -interface SpeechRecognitionErrorEvent { - error: string -} function AITutorContent() { const searchParams = useSearchParams() diff --git a/app/(dashboard)/ai-voice/page.tsx b/app/(dashboard)/ai-voice/page.tsx index 5a8c469..95b70e0 100644 --- a/app/(dashboard)/ai-voice/page.tsx +++ b/app/(dashboard)/ai-voice/page.tsx @@ -25,12 +25,7 @@ import { useUser } from "@/hooks/use-user" import { canAccessFeature } from "@/lib/plans" import { MarkdownRenderer } from "@/components/markdown-renderer" -declare global { - interface Window { - SpeechRecognition?: new () => SpeechRecognition - webkitSpeechRecognition?: new () => SpeechRecognition - } -} + type VoiceState = "idle" | "listening" | "processing" | "speaking" @@ -52,6 +47,11 @@ export default function AIVoicePage() { const { subscription, isLoading, error: userError, email } = useUser() const [voiceState, setVoiceState] = useState("idle") + // Keep a ref in sync so event callbacks (e.g. onend) see the live value + const setVoiceStateAndRef = (s: VoiceState) => { + voiceStateRef.current = s + setVoiceState(s) + } const [messages, setMessages] = useState([]) const [transcript, setTranscript] = useState("") const [error, setError] = useState("") @@ -62,6 +62,7 @@ export default function AIVoicePage() { const [currentHistoryId, setCurrentHistoryId] = useState(null) const recognitionRef = useRef(null) + const voiceStateRef = useRef("idle") const synthRef = useRef(null) const scrollRef = useRef(null) const isSubmittingRef = useRef(false) @@ -132,7 +133,7 @@ export default function AIVoicePage() { isSubmittingRef.current = false finalTranscriptRef.current = "" - setVoiceState("idle") + setVoiceStateAndRef("idle") } const speak = (text: string) => { @@ -150,10 +151,10 @@ export default function AIVoicePage() { utter.rate = 0.95 utter.pitch = 1 - utter.onend = () => setVoiceState("idle") - utter.onerror = () => setVoiceState("idle") + utter.onend = () => setVoiceStateAndRef("idle") + utter.onerror = () => setVoiceStateAndRef("idle") - setVoiceState("speaking") + setVoiceStateAndRef("speaking") synthRef.current.speak(utter) } @@ -190,7 +191,7 @@ export default function AIVoicePage() { setTranscript(item.prompt) setCurrentHistoryId(item.id) setError("") - setVoiceState("idle") + setVoiceStateAndRef("idle") if (typeof window !== "undefined") { const url = new URL(window.location.href) @@ -221,7 +222,7 @@ export default function AIVoicePage() { isSubmittingRef.current = true setError("") setTranscript(cleanText) - setVoiceState("processing") + setVoiceStateAndRef("processing") const userMessageId = `user-${Date.now()}` const assistantMessageId = `assistant-${Date.now()}` @@ -246,14 +247,14 @@ export default function AIVoicePage() { if (!res.ok) { if (data.requiresLogin) { setShowLoginModal(true) - setVoiceState("idle") + setVoiceStateAndRef("idle") isSubmittingRef.current = false return } if (data.requiresUpgrade) { window.location.href = "/pricing?plan=pro&feature=ai-voice" - setVoiceState("idle") + setVoiceStateAndRef("idle") isSubmittingRef.current = false return } @@ -292,7 +293,7 @@ export default function AIVoicePage() { } catch (e) { const errText = e instanceof Error ? e.message : "Something went wrong" setError(errText) - setVoiceState("idle") + setVoiceStateAndRef("idle") } finally { isSubmittingRef.current = false } @@ -339,7 +340,7 @@ export default function AIVoicePage() { recognition.maxAlternatives = 1 recognition.onstart = () => { - setVoiceState("listening") + setVoiceStateAndRef("listening") } recognition.onresult = (event) => { @@ -393,11 +394,11 @@ export default function AIVoicePage() { setError(`Voice error: ${e.error}`) } - setVoiceState("idle") + setVoiceStateAndRef("idle") } recognition.onend = async () => { - if (voiceState === "listening") { + if (voiceStateRef.current === "listening") { const leftover = finalTranscriptRef.current.trim() if (leftover && !isSubmittingRef.current) { @@ -406,7 +407,7 @@ export default function AIVoicePage() { return } - setVoiceState("idle") + setVoiceStateAndRef("idle") } } diff --git a/types/speech-recognition.d.ts b/types/speech-recognition.d.ts new file mode 100644 index 0000000..6563ae1 --- /dev/null +++ b/types/speech-recognition.d.ts @@ -0,0 +1,65 @@ +// Global ambient declarations for the Web Speech API. +// Placed here (not inside a module file) so they are global and shared +// across all files in the project without any import needed. +// +// The TypeScript DOM lib (lib.dom.d.ts) shipped with older TS versions may be +// missing some properties (maxAlternatives, onstart) or omit the webkit prefix +// entirely. This file fills those gaps without conflicting with any existing +// declarations. + +interface SpeechRecognitionEvent extends Event { + readonly resultIndex: number + readonly results: SpeechRecognitionResultList +} + +interface SpeechRecognitionResultList { + readonly length: number + item(index: number): SpeechRecognitionResult + [index: number]: SpeechRecognitionResult +} + +interface SpeechRecognitionResult { + readonly isFinal: boolean + readonly length: number + item(index: number): SpeechRecognitionAlternative + [index: number]: SpeechRecognitionAlternative +} + +interface SpeechRecognitionAlternative { + readonly transcript: string + readonly confidence: number +} + +interface SpeechRecognitionErrorEvent extends Event { + readonly error: string + readonly message: string +} + +interface SpeechRecognition extends EventTarget { + // Configuration + lang: string + continuous: boolean + interimResults: boolean + maxAlternatives: number + + // Event handlers + onstart: ((this: SpeechRecognition, ev: Event) => void) | null + onend: ((this: SpeechRecognition, ev: Event) => void) | null + onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null + onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null + + // Methods + start(): void + stop(): void + abort(): void +} + +declare var SpeechRecognition: { + prototype: SpeechRecognition + new (): SpeechRecognition +} + +interface Window { + SpeechRecognition?: typeof SpeechRecognition + webkitSpeechRecognition?: typeof SpeechRecognition +}