Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 0 additions & 37 deletions app/(dashboard)/ai-tutor/old-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
37 changes: 0 additions & 37 deletions app/(dashboard)/ai-tutor/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
39 changes: 20 additions & 19 deletions app/(dashboard)/ai-voice/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -52,6 +47,11 @@ export default function AIVoicePage() {
const { subscription, isLoading, error: userError, email } = useUser()

const [voiceState, setVoiceState] = useState<VoiceState>("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<Message[]>([])
const [transcript, setTranscript] = useState("")
const [error, setError] = useState("")
Expand All @@ -62,6 +62,7 @@ export default function AIVoicePage() {
const [currentHistoryId, setCurrentHistoryId] = useState<string | null>(null)

const recognitionRef = useRef<SpeechRecognition | null>(null)
const voiceStateRef = useRef<VoiceState>("idle")
const synthRef = useRef<SpeechSynthesis | null>(null)
const scrollRef = useRef<HTMLDivElement>(null)
const isSubmittingRef = useRef(false)
Expand Down Expand Up @@ -132,7 +133,7 @@ export default function AIVoicePage() {

isSubmittingRef.current = false
finalTranscriptRef.current = ""
setVoiceState("idle")
setVoiceStateAndRef("idle")
}

const speak = (text: string) => {
Expand All @@ -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)
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()}`
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -339,7 +340,7 @@ export default function AIVoicePage() {
recognition.maxAlternatives = 1

recognition.onstart = () => {
setVoiceState("listening")
setVoiceStateAndRef("listening")
}

recognition.onresult = (event) => {
Expand Down Expand Up @@ -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) {
Expand All @@ -406,7 +407,7 @@ export default function AIVoicePage() {
return
}

setVoiceState("idle")
setVoiceStateAndRef("idle")
}
}

Expand Down
65 changes: 65 additions & 0 deletions types/speech-recognition.d.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading