import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Mic, Loader2 } from 'lucide-react' import type { LogEventPayload } from '../utils/logEntryPayload.js' import { parseLiveVoiceRemark } from '../utils/liveEventCodes.js' import { formatEventSummary } from '../utils/formatEventSummary.js' import VoiceMemoPlayer, { type PreloadedVoiceMemo } from './VoiceMemoPlayer.tsx' import { useDialog } from './ModalDialog.tsx' import { updateVoiceMemoTranscript } from '../services/voiceAttachments.js' import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js' import { getAiAuthorized } from '../services/userPreferences.js' interface EventRemarksCellProps { event: LogEventPayload logbookId: string voiceMemoLookup?: Map readOnly?: boolean } export default function EventRemarksCell({ event, logbookId, voiceMemoLookup, readOnly = false }: EventRemarksCellProps) { const { t } = useTranslation() const { showAlert } = useDialog() const voiceId = parseLiveVoiceRemark(event.remarks.trim()) const preloaded = voiceId ? voiceMemoLookup?.get(voiceId) : undefined const [transcribing, setTranscribing] = useState(false) const [isOnline, setIsOnline] = useState(navigator.onLine) useEffect(() => { const handleOnline = () => setIsOnline(true) const handleOffline = () => setIsOnline(false) window.addEventListener('online', handleOnline) window.addEventListener('offline', handleOffline) return () => { window.removeEventListener('online', handleOnline) window.removeEventListener('offline', handleOffline) } }, []) const handleTranscribe = async (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() if (transcribing || !preloaded?.audio || !voiceId) return if (!getAiAuthorized()) { void showAlert( t('profile.ai_unauthorized_alert_desc'), t('profile.ai_unauthorized_alert_title') ) return } setTranscribing(true) const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 15000) try { const res = await fetch('/api/ai/transcribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ audioDataUrl: preloaded.audio }), signal: controller.signal }) clearTimeout(timeoutId) if (!res.ok) { throw new Error(`Server returned status ${res.status}`) } const data = await res.json() const text = (data.text || '').trim() if (!text) { throw new Error('Transcription returned empty text') } await updateVoiceMemoTranscript(logbookId, voiceId, text) trackPlausibleEvent(PlausibleEvents.VOICE_MEMO_TRANSCRIBED, { status: 'success', mode: 'manual' }) } catch (err) { clearTimeout(timeoutId) console.error('[EventRemarksCell] Transcription failed:', err) trackPlausibleEvent(PlausibleEvents.VOICE_MEMO_TRANSCRIBED, { status: 'failed', mode: 'manual' }) void showAlert(t('logs.live_voice_transcribe_failed'), t('logs.live_voice_btn')) } finally { setTranscribing(false) } } let summary = formatEventSummary(event, t) if (voiceId && preloaded?.caption) { summary = t('logs.live_voice_entry', { caption: preloaded.caption }) } return (
{summary} {voiceId && (
{!readOnly && preloaded && preloaded.transcribed === false && isOnline && ( )}
)}
) }