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>
This commit is contained in:
87
memento-note/hooks/use-voice-transcription.ts
Normal file
87
memento-note/hooks/use-voice-transcription.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/// <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 }
|
||||
}
|
||||
Reference in New Issue
Block a user