Files
Momento/memento-note/hooks/use-voice-transcription.ts
Antigravity c415d93945
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m7s
CI / Deploy production (on server) (push) Has been skipped
feat: Tier 1 & 2 — Daily Note, Voice, Flashcard quota, Readwise, Calendar, Agent Gallery
Tier 1:
- BASIC tier: chat (10/mo) + reformulate (10/mo) désormais accessibles
- Nouveaux quotas: ai_flashcard + voice_transcribe dans tous les tiers
- /api/notes/daily : note du jour auto-créée (find or create)
- Bouton Note du Jour dans la sidebar (CalendarDays)
- Voice-to-Text dans l'éditeur (Web Speech API, bouton Mic toolbar)
- Flashcard generation → quota ai_flashcard (au lieu de reformulate)

Tier 2:
- Intégration Readwise: GET/POST/DELETE /api/integrations/readwise
- Intégration Google Calendar: OAuth flow + today's events + meeting notes
- /api/integrations/calendar + /callback
- Page /settings/integrations avec cards Calendar + Readwise
- SettingsNav: onglet Intégrations
- AgentTemplates: catégories + 4 nouveaux templates (Digest/Recap/AutoTagger/Synthesis)

Schema:
- UserAISettings.integrationTokens Json? (migration 20260529160000)
- prisma generate + migrate deploy appliqués

Fix:
- SpeechRecognition types (triple-slash @types/dom-speech-recognition)
- Notebook.create: suppression champ 'description' inexistant

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 15:14:01 +00:00

88 lines
2.5 KiB
TypeScript

/// <reference types="@types/dom-speech-recognition" />
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
type VoiceState = 'idle' | 'listening' | 'processing' | 'error'
interface UseVoiceTranscriptionOptions {
onTranscript: (text: string) => void
onError?: (message: string) => void
lang?: string
}
export function useVoiceTranscription({ onTranscript, onError, lang = 'fr-FR' }: UseVoiceTranscriptionOptions) {
const [state, setState] = useState<VoiceState>('idle')
const recognitionRef = useRef<SpeechRecognition | null>(null)
const accumulatedRef = useRef<string>('')
const isSupported =
typeof window !== 'undefined' &&
('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)
const start = useCallback(() => {
if (!isSupported) {
onError?.('La reconnaissance vocale n\'est pas disponible sur ce navigateur.')
setState('error')
return
}
const SR = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
const rec: SpeechRecognition = new SR()
rec.lang = lang
rec.continuous = true
rec.interimResults = false
accumulatedRef.current = ''
rec.onstart = () => setState('listening')
rec.onresult = (event: SpeechRecognitionEvent) => {
let transcript = ''
for (let i = event.resultIndex; i < event.results.length; i++) {
if (event.results[i].isFinal) {
transcript += event.results[i][0].transcript
}
}
if (transcript) accumulatedRef.current += (accumulatedRef.current ? ' ' : '') + transcript
}
rec.onerror = (event: SpeechRecognitionErrorEvent) => {
console.error('[voice] SpeechRecognition error:', event.error)
onError?.(event.error === 'not-allowed' ? 'Microphone non autorisé.' : `Erreur: ${event.error}`)
setState('error')
}
rec.onend = () => {
setState('idle')
if (accumulatedRef.current.trim()) {
onTranscript(accumulatedRef.current.trim())
accumulatedRef.current = ''
}
}
recognitionRef.current = rec
rec.start()
}, [isSupported, lang, onTranscript, onError])
const stop = useCallback(() => {
recognitionRef.current?.stop()
recognitionRef.current = null
}, [])
const toggle = useCallback(() => {
if (state === 'listening') {
stop()
} else {
start()
}
}, [state, start, stop])
useEffect(() => {
return () => {
recognitionRef.current?.stop()
}
}, [])
return { state, toggle, start, stop, isSupported }
}