import { useEffect, useState, useCallback, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { ArrowLeft, Loader2, Edit2, Check, X, Play, Pause, Volume2 } from 'lucide-react' import { meetingsApi } from '@/api/meetings' import api from '@/api/axios' import { useMeetingsStore } from '@/store/meetingsStore' import { useWebSocket } from '@/hooks/useWebSocket' import { Layout } from '@/components/Layout' import { StatusBadge } from '@/components/StatusBadge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent } from '@/components/ui/card' import type { Meeting, TranscriptSegment, Minutes, SentimentData, PsychologicalData, CommunicationData, SummaryData } from '@/types' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts' type Tab = 'transcript' | 'minutes' | 'sentiment' | 'psychological' | 'communication' | 'summary' const SPEAKER_COLORS = ['#4ade80', '#60a5fa', '#f472b6', '#fb923c', '#a78bfa', '#34d399'] const TERMINAL = ['completed', 'error'] export function MeetingDetail() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const { meetings, updateMeeting } = useMeetingsStore() const [meeting, setMeeting] = useState(meetings.find((m) => m.id === id) ?? null) const [tab, setTab] = useState('transcript') const [minutes, setMinutes] = useState(null) const [sentiment, setSentiment] = useState(null) const [psychological, setPsychological] = useState(null) const [communication, setCommunication] = useState(null) const [summary, setSummary] = useState(null) const [loadingTab, setLoadingTab] = useState(false) const [editingLabel, setEditingLabel] = useState(null) const [editName, setEditName] = useState('') const [audioUrl, setAudioUrl] = useState(null) const [audioLoading, setAudioLoading] = useState(false) const [isPlaying, setIsPlaying] = useState(false) const audioRef = useRef(null) const [enrollNames, setEnrollNames] = useState>({}) const [enrollingLabel, setEnrollingLabel] = useState(null) const [enrollErrors, setEnrollErrors] = useState>({}) const [enrollSuccess, setEnrollSuccess] = useState>({}) const isTerminal = TERMINAL.includes(meeting?.status ?? '') useEffect(() => { if (!id) return meetingsApi.get(id).then(({ data }) => { setMeeting(data) updateMeeting(id, data) if (data.status === 'completed') setTab('transcript') }) }, [id, updateMeeting]) const onWsMessage = useCallback((data: unknown) => { const msg = data as { type: string; status?: string; progress?: number } if (msg.type === 'status_update' && msg.status) { setMeeting((m) => m ? { ...m, status: msg.status as Meeting['status'], progress: msg.progress } : m) } }, []) useWebSocket(meeting?.status === 'processing' ? (id ?? null) : null, onWsMessage) useEffect(() => { if (!id || isTerminal) return const iv = setInterval(async () => { try { const { data } = await meetingsApi.get(id) setMeeting(data) updateMeeting(id, data) if (TERMINAL.includes(data.status)) clearInterval(iv) } catch { /* ignore */ } }, 4000) return () => clearInterval(iv) }, [id, isTerminal, updateMeeting]) async function loadTab(t: Tab) { setTab(t) setLoadingTab(true) try { if (t === 'minutes' && !minutes) { const { data } = await meetingsApi.getAnalysis(id!, 'minutes') setMinutes(data.content as Minutes) } else if (t === 'sentiment' && !sentiment) { const { data } = await meetingsApi.getAnalysis(id!, 'sentiment') setSentiment(data.content as SentimentData) } else if (t === 'psychological' && !psychological) { const { data } = await meetingsApi.getAnalysis(id!, 'psychological') setPsychological(data.content as PsychologicalData) } else if (t === 'communication' && !communication) { const { data } = await meetingsApi.getAnalysis(id!, 'communication') setCommunication(data.content as CommunicationData) } else if (t === 'summary' && !summary) { const { data } = await meetingsApi.getAnalysis(id!, 'summary') setSummary(data.content as SummaryData) } } catch { /* analysis not ready */ } finally { setLoadingTab(false) } } async function loadAudio() { if (audioUrl) return setAudioLoading(true) try { const resp = await api.get(`/meetings/${id}/audio`, { responseType: 'blob' }) const url = URL.createObjectURL(resp.data) setAudioUrl(url) if (audioRef.current) audioRef.current.src = url } catch { /* no audio */ } finally { setAudioLoading(false) } } function togglePlay() { if (!audioRef.current) return if (isPlaying) { audioRef.current.pause(); setIsPlaying(false) } else { audioRef.current.play(); setIsPlaying(true) } } async function saveLabel(label: string) { if (!id || !editName.trim()) return await meetingsApi.relabelParticipant(id, label, editName.trim()) setMeeting((m) => m ? { ...m } : m) setEditingLabel(null) } function getSpeakerColor(speaker: string): string { const segs = meeting?.transcription?.segments ?? [] const speakers = [...new Set(segs.map((s) => s.speaker))] return SPEAKER_COLORS[speakers.indexOf(speaker) % SPEAKER_COLORS.length] } const sentimentColor = (s?: string) => s === 'positive' ? '#4ade80' : s === 'negative' ? '#f87171' : '#94a3b8' const tabs: { key: Tab; label: string }[] = [ { key: 'transcript', label: 'Transcricao' }, { key: 'summary', label: 'Resumo' }, { key: 'minutes', label: 'Ata' }, { key: 'sentiment', label: 'Sentimento' }, { key: 'psychological', label: 'Perfil' }, { key: 'communication', label: 'Comunicacao' }, ] const segments: TranscriptSegment[] = meeting?.transcription?.segments ?? [] const unidentifiedSpeakers = [...new Set(segments.filter(s => /^SPEAKER_\d+$/.test(s.speaker)).map(s => s.speaker))].sort() async function handleEnroll(label: string) { const name = (enrollNames[label] || '').trim() if (!name || !id) return setEnrollingLabel(label) setEnrollErrors(e => ({ ...e, [label]: '' })) try { await meetingsApi.enrollSpeaker(id, label, name) setEnrollSuccess(s => ({ ...s, [label]: true })) setMeeting(m => { if (!m?.transcription?.segments) return m return { ...m, transcription: { ...m.transcription, segments: m.transcription.segments.map((seg: TranscriptSegment) => seg.speaker === label ? { ...seg, speaker: name } : seg ), }, } }) } catch { setEnrollErrors(e => ({ ...e, [label]: 'Erro ao salvar. Tente novamente.' })) } finally { setEnrollingLabel(null) } } return (

{meeting?.title || 'Reuniao sem titulo'}

{meeting && }
{meeting?.status === 'completed' && ( {!audioUrl && !audioLoading && ( )} {audioLoading && Carregando...} {audioUrl && (
)}
)} {!isTerminal && meeting?.status !== undefined && (

{meeting.status === 'receiving_chunks' && 'Recebendo audio...'} {meeting.status === 'uploading' && 'Enviando para processamento...'} {meeting.status === 'processing' && 'Iniciando processamento...'} {meeting.status === 'transcribing' && 'Transcrevendo com Whisper...'} {meeting.status === 'analyzing' && 'Analisando com IA...'}

A pagina atualiza automaticamente

)} {meeting?.status === 'completed' && ( <> {unidentifiedSpeakers.length > 0 && (
🎤

Identificar participantes

Para reconhecimento automático em reuniões futuras

{unidentifiedSpeakers.map(label => (
{label} setEnrollNames(n => ({ ...n, [label]: e.target.value }))} onKeyDown={e => e.key === 'Enter' && handleEnroll(label)} placeholder="Nome da pessoa" className="h-7 text-xs flex-1" disabled={!!enrollSuccess[label]} />
))}
{Object.entries(enrollErrors).filter(([, v]) => v).map(([k, v]) => (

{k}: {v}

))}
)}
{tabs.map((t) => ( ))}
{loadingTab &&
} {tab === 'transcript' && !loadingTab && (
{segments.length > 0 ? segments.map((seg, i) => (
{seg.speaker} {Math.floor(seg.start / 60)}:{String(Math.floor(seg.start % 60)).padStart(2, '0')} {editingLabel === seg.speaker ? (
setEditName(e.target.value)} className="h-5 px-1 text-xs w-24" />
) : ( )}

{seg.text}

)) :

Transcricao nao disponivel.

}
)} {tab === 'summary' && !loadingTab && (
{summary ? ( <> {summary.summary &&

Resumo

{summary.summary}

} {summary.key_points && summary.key_points.length > 0 && (

Pontos-chave

    {summary.key_points.map((p, i) =>
  • {p}
  • )}
)}
{summary.duration_minutes != null &&

{summary.duration_minutes}'

duracao

} {summary.total_participants != null &&

{summary.total_participants}

participantes

}
) :

Resumo nao disponivel.

}
)} {tab === 'minutes' && !loadingTab && (
{minutes ? ( <> {minutes.agenda && minutes.agenda.length > 0 && (

Pauta

    {minutes.agenda.map((a, i) =>
  • {i+1}.{a}
  • )}
)} {minutes.decisions && minutes.decisions.length > 0 && (

Decisoes

    {minutes.decisions.map((d, i) =>
  • {d}
  • )}
)} {minutes.action_items && minutes.action_items.length > 0 && (

Acoes

{minutes.action_items.map((a, i) => (

{a.description}

{a.responsible &&

→ {a.responsible}

} {a.deadline && a.deadline !== 'A definir' &&

{a.deadline}

}
))}
)} {minutes.next_steps && minutes.next_steps.length > 0 && (

Proximos passos

    {minutes.next_steps.map((s, i) =>
  • {s}
  • )}
)} {minutes.open_points && minutes.open_points.length > 0 && (

Pontos em aberto

    {minutes.open_points.map((s, i) =>
  • {s}
  • )}
)} ) :

Ata nao disponivel.

}
)} {tab === 'sentiment' && !loadingTab && (
{sentiment ? ( <> {sentiment.overall_sentiment && (

Sentimento geral

{sentiment.overall_sentiment}

)} {sentiment.participation_metrics && sentiment.participation_metrics.length > 0 && (

Participacao

[`${v}%`, 'Participacao']} />
)} {sentiment.participants_sentiment && sentiment.participants_sentiment.length > 0 && (

Por participante

{sentiment.participants_sentiment.map((p, i) => (
{p.speaker} ({p.sentiment})

{p.details}

))}
)} {sentiment.timeline && sentiment.timeline.length > 0 && (

Linha do tempo

{sentiment.timeline.map((t, i) => (
{t.time_range}
{t.sentiment} {t.notes &&

{t.notes}

}
))}
)} {sentiment.tension_moments && sentiment.tension_moments.length > 0 && (

Momentos de tensao

    {sentiment.tension_moments.map((t, i) =>
  • {t}
  • )}
)} {sentiment.consensus_moments && sentiment.consensus_moments.length > 0 && (

Momentos de consenso

    {sentiment.consensus_moments.map((t, i) =>
  • {t}
  • )}
)} ) :

Analise de sentimento nao disponivel.

}
)} {tab === 'psychological' && !loadingTab && (
{psychological ? ( <> {psychological.group_dynamics && (

Dinamica do grupo

{psychological.group_dynamics}

)} {psychological.participants_profiles && psychological.participants_profiles.length > 0 && psychological.participants_profiles.map((p, i) => (

{p.name || p.speaker}

{p.communication_style &&
Estilo de comunicacao:

{p.communication_style}

} {p.leadership_tendencies &&
Lideranca:

{p.leadership_tendencies}

} {p.power_dynamics_role &&
Papel na dinamica:

{p.power_dynamics_role}

}
))} {psychological.key_observations && psychological.key_observations.length > 0 && (

Observacoes

    {psychological.key_observations.map((o, i) =>
  • {o}
  • )}
)} ) :

Perfil psicologico nao disponivel.

}
)} {tab === 'communication' && !loadingTab && (
{communication ? ( <> {communication.overall_score != null && (
{communication.overall_score}/10

Score de comunicacao

)} {communication.general_feedback && (

Feedback geral

{communication.general_feedback}

)} {communication.dimensions && (

Dimensoes

{Object.entries(communication.dimensions).map(([dim, val]) => (
{dim.replace(/_/g, ' ')} {val.score}/10
{val.notes &&

{val.notes}

}
))}
)} {communication.strengths && communication.strengths.length > 0 && (

Pontos fortes

{communication.strengths.map((s, i) => (

{s.description}

{s.example &&

"{s.example}"

}
))}
)} {communication.improvement_areas && communication.improvement_areas.length > 0 && (

Melhorias

{communication.improvement_areas.map((a, i) => (

{a.description}

{a.suggestion &&

→ {a.suggestion}

}
))}
)} ) :

Analise de comunicacao nao disponivel.

}
)} )} {meeting?.status === 'error' && (

Erro ao processar reuniao

{meeting.error_message &&

{meeting.error_message}

}
)}
) }