523 lines
29 KiB
TypeScript
523 lines
29 KiB
TypeScript
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<Meeting | null>(meetings.find((m) => m.id === id) ?? null)
|
|
const [tab, setTab] = useState<Tab>('transcript')
|
|
const [minutes, setMinutes] = useState<Minutes | null>(null)
|
|
const [sentiment, setSentiment] = useState<SentimentData | null>(null)
|
|
const [psychological, setPsychological] = useState<PsychologicalData | null>(null)
|
|
const [communication, setCommunication] = useState<CommunicationData | null>(null)
|
|
const [summary, setSummary] = useState<SummaryData | null>(null)
|
|
const [loadingTab, setLoadingTab] = useState(false)
|
|
const [editingLabel, setEditingLabel] = useState<string | null>(null)
|
|
const [editName, setEditName] = useState('')
|
|
const [audioUrl, setAudioUrl] = useState<string | null>(null)
|
|
const [audioLoading, setAudioLoading] = useState(false)
|
|
const [isPlaying, setIsPlaying] = useState(false)
|
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
|
|
|
const [enrollNames, setEnrollNames] = useState<Record<string, string>>({})
|
|
const [enrollingLabel, setEnrollingLabel] = useState<string | null>(null)
|
|
const [enrollErrors, setEnrollErrors] = useState<Record<string, string>>({})
|
|
const [enrollSuccess, setEnrollSuccess] = useState<Record<string, boolean>>({})
|
|
|
|
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 (
|
|
<Layout>
|
|
<div className="p-4 max-w-2xl mx-auto space-y-4 pb-8">
|
|
<div className="flex items-center gap-3">
|
|
<button onClick={() => navigate(-1)} className="text-slate-400 hover:text-white"><ArrowLeft size={20} /></button>
|
|
<div className="flex-1 min-w-0">
|
|
<h1 className="text-lg font-bold text-white truncate">{meeting?.title || 'Reuniao sem titulo'}</h1>
|
|
{meeting && <StatusBadge status={meeting.status} />}
|
|
</div>
|
|
</div>
|
|
|
|
{meeting?.status === 'completed' && (
|
|
<Card>
|
|
<CardContent className="flex items-center gap-3 py-3">
|
|
<Volume2 size={16} className="text-slate-400 shrink-0" />
|
|
{!audioUrl && !audioLoading && (
|
|
<button onClick={loadAudio} className="text-sm text-green-400 hover:text-green-300">
|
|
Carregar audio da reuniao
|
|
</button>
|
|
)}
|
|
{audioLoading && <span className="text-slate-400 text-sm flex items-center gap-2"><Loader2 size={14} className="animate-spin" /> Carregando...</span>}
|
|
{audioUrl && (
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<button onClick={togglePlay} className="w-8 h-8 rounded-full bg-green-600 hover:bg-green-500 flex items-center justify-center shrink-0">
|
|
{isPlaying ? <Pause size={14} /> : <Play size={14} />}
|
|
</button>
|
|
<audio
|
|
ref={audioRef}
|
|
src={audioUrl}
|
|
onEnded={() => setIsPlaying(false)}
|
|
onPause={() => setIsPlaying(false)}
|
|
onPlay={() => setIsPlaying(true)}
|
|
controls
|
|
className="flex-1 h-8"
|
|
style={{ accentColor: '#4ade80' }}
|
|
/>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{!isTerminal && meeting?.status !== undefined && (
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center py-8 gap-3">
|
|
<Loader2 size={32} className="animate-spin text-yellow-400" />
|
|
<p className="text-slate-300 text-sm font-medium">
|
|
{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...'}
|
|
</p>
|
|
<p className="text-slate-500 text-xs">A pagina atualiza automaticamente</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{meeting?.status === 'completed' && (
|
|
<>
|
|
{unidentifiedSpeakers.length > 0 && (
|
|
<Card>
|
|
<CardContent className="py-3">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<span className="text-lg">🎤</span>
|
|
<div>
|
|
<p className="text-white text-sm font-semibold">Identificar participantes</p>
|
|
<p className="text-slate-400 text-xs">Para reconhecimento automático em reuniões futuras</p>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{unidentifiedSpeakers.map(label => (
|
|
<div key={label} className="flex items-center gap-2">
|
|
<span className="text-xs font-mono text-slate-400 w-24 shrink-0">{label}</span>
|
|
<Input
|
|
value={enrollNames[label] || ''}
|
|
onChange={e => 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]}
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
className="h-7 px-2 text-xs shrink-0"
|
|
disabled={enrollingLabel === label || !!enrollSuccess[label]}
|
|
onClick={() => handleEnroll(label)}
|
|
>
|
|
{enrollingLabel === label ? <Loader2 size={12} className="animate-spin" /> : enrollSuccess[label] ? <Check size={12} /> : 'Salvar'}
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{Object.entries(enrollErrors).filter(([, v]) => v).map(([k, v]) => (
|
|
<p key={k} className="text-red-400 text-xs mt-1">{k}: {v}</p>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
<div className="flex gap-1 overflow-x-auto pb-1">
|
|
{tabs.map((t) => (
|
|
<button key={t.key} onClick={() => loadTab(t.key)}
|
|
className={`px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors ${tab === t.key ? 'bg-green-700 text-white' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-700'}`}>
|
|
{t.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{loadingTab && <div className="flex justify-center py-8"><Loader2 size={20} className="animate-spin text-slate-400" /></div>}
|
|
|
|
{tab === 'transcript' && !loadingTab && (
|
|
<div className="space-y-2">
|
|
{segments.length > 0 ? segments.map((seg, i) => (
|
|
<div key={i} className="flex gap-3">
|
|
<div className="flex flex-col items-center">
|
|
<div className="w-2 h-2 rounded-full mt-2 shrink-0" style={{ background: getSpeakerColor(seg.speaker) }} />
|
|
<div className="w-px flex-1 bg-slate-700 mt-1" />
|
|
</div>
|
|
<div className="flex-1 pb-3">
|
|
<div className="flex items-center gap-2 mb-0.5">
|
|
<span className="text-xs font-semibold" style={{ color: getSpeakerColor(seg.speaker) }}>{seg.speaker}</span>
|
|
<span className="text-slate-500 text-xs">{Math.floor(seg.start / 60)}:{String(Math.floor(seg.start % 60)).padStart(2, '0')}</span>
|
|
{editingLabel === seg.speaker ? (
|
|
<div className="flex items-center gap-1 ml-1">
|
|
<Input value={editName} onChange={(e) => setEditName(e.target.value)} className="h-5 px-1 text-xs w-24" />
|
|
<button onClick={() => saveLabel(seg.speaker)} className="text-green-400"><Check size={12} /></button>
|
|
<button onClick={() => setEditingLabel(null)} className="text-red-400"><X size={12} /></button>
|
|
</div>
|
|
) : (
|
|
<button onClick={() => { setEditingLabel(seg.speaker); setEditName(seg.speaker) }} className="text-slate-500 hover:text-slate-300"><Edit2 size={10} /></button>
|
|
)}
|
|
</div>
|
|
<p className="text-slate-200 text-sm">{seg.text}</p>
|
|
</div>
|
|
</div>
|
|
)) : <p className="text-slate-400 text-sm text-center py-8">Transcricao nao disponivel.</p>}
|
|
</div>
|
|
)}
|
|
|
|
{tab === 'summary' && !loadingTab && (
|
|
<div className="space-y-3">
|
|
{summary ? (
|
|
<>
|
|
{summary.summary && <Card><CardContent><h3 className="text-green-400 text-sm font-semibold mb-2">Resumo</h3><p className="text-slate-200 text-sm leading-relaxed">{summary.summary}</p></CardContent></Card>}
|
|
{summary.key_points && summary.key_points.length > 0 && (
|
|
<Card><CardContent>
|
|
<h3 className="text-green-400 text-sm font-semibold mb-2">Pontos-chave</h3>
|
|
<ul className="space-y-1">{summary.key_points.map((p, i) => <li key={i} className="text-slate-200 text-sm flex gap-2"><span className="text-green-400">•</span>{p}</li>)}</ul>
|
|
</CardContent></Card>
|
|
)}
|
|
<div className="flex gap-3">
|
|
{summary.duration_minutes != null && <Card className="flex-1"><CardContent className="text-center py-3"><p className="text-2xl font-bold text-white">{summary.duration_minutes}'</p><p className="text-slate-400 text-xs">duracao</p></CardContent></Card>}
|
|
{summary.total_participants != null && <Card className="flex-1"><CardContent className="text-center py-3"><p className="text-2xl font-bold text-white">{summary.total_participants}</p><p className="text-slate-400 text-xs">participantes</p></CardContent></Card>}
|
|
</div>
|
|
</>
|
|
) : <p className="text-slate-400 text-sm text-center py-8">Resumo nao disponivel.</p>}
|
|
</div>
|
|
)}
|
|
|
|
{tab === 'minutes' && !loadingTab && (
|
|
<div className="space-y-3">
|
|
{minutes ? (
|
|
<>
|
|
{minutes.agenda && minutes.agenda.length > 0 && (
|
|
<Card><CardContent><h3 className="text-green-400 text-sm font-semibold mb-2">Pauta</h3><ul className="space-y-1">{minutes.agenda.map((a, i) => <li key={i} className="text-slate-200 text-sm flex gap-2"><span className="text-slate-400">{i+1}.</span>{a}</li>)}</ul></CardContent></Card>
|
|
)}
|
|
{minutes.decisions && minutes.decisions.length > 0 && (
|
|
<Card><CardContent><h3 className="text-green-400 text-sm font-semibold mb-2">Decisoes</h3><ul className="space-y-1">{minutes.decisions.map((d, i) => <li key={i} className="text-slate-200 text-sm flex gap-2"><span>•</span>{d}</li>)}</ul></CardContent></Card>
|
|
)}
|
|
{minutes.action_items && minutes.action_items.length > 0 && (
|
|
<Card><CardContent><h3 className="text-green-400 text-sm font-semibold mb-2">Acoes</h3>
|
|
<div className="space-y-2">{minutes.action_items.map((a, i) => (
|
|
<div key={i} className="text-sm border-l-2 border-slate-700 pl-3">
|
|
<p className="text-slate-200">{a.description}</p>
|
|
{a.responsible && <p className="text-slate-400 text-xs">→ {a.responsible}</p>}
|
|
{a.deadline && a.deadline !== 'A definir' && <p className="text-slate-500 text-xs">{a.deadline}</p>}
|
|
</div>
|
|
))}</div>
|
|
</CardContent></Card>
|
|
)}
|
|
{minutes.next_steps && minutes.next_steps.length > 0 && (
|
|
<Card><CardContent><h3 className="text-blue-400 text-sm font-semibold mb-2">Proximos passos</h3><ul className="space-y-1">{minutes.next_steps.map((s, i) => <li key={i} className="text-slate-200 text-sm flex gap-2"><span>→</span>{s}</li>)}</ul></CardContent></Card>
|
|
)}
|
|
{minutes.open_points && minutes.open_points.length > 0 && (
|
|
<Card><CardContent><h3 className="text-yellow-400 text-sm font-semibold mb-2">Pontos em aberto</h3><ul className="space-y-1">{minutes.open_points.map((s, i) => <li key={i} className="text-slate-200 text-sm flex gap-2"><span>•</span>{s}</li>)}</ul></CardContent></Card>
|
|
)}
|
|
</>
|
|
) : <p className="text-slate-400 text-sm text-center py-8">Ata nao disponivel.</p>}
|
|
</div>
|
|
)}
|
|
|
|
{tab === 'sentiment' && !loadingTab && (
|
|
<div className="space-y-3">
|
|
{sentiment ? (
|
|
<>
|
|
{sentiment.overall_sentiment && (
|
|
<Card><CardContent className="flex items-center gap-3">
|
|
<div className="w-3 h-3 rounded-full" style={{ background: sentimentColor(sentiment.overall_sentiment) }} />
|
|
<div>
|
|
<p className="text-white text-sm font-medium">Sentimento geral</p>
|
|
<p className="text-slate-400 text-xs capitalize">{sentiment.overall_sentiment}</p>
|
|
</div>
|
|
</CardContent></Card>
|
|
)}
|
|
{sentiment.participation_metrics && sentiment.participation_metrics.length > 0 && (
|
|
<Card><CardContent>
|
|
<h3 className="text-green-400 text-sm font-semibold mb-3">Participacao</h3>
|
|
<ResponsiveContainer width="100%" height={140}>
|
|
<BarChart data={sentiment.participation_metrics}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
|
<XAxis dataKey="speaker" tick={{ fill: '#94a3b8', fontSize: 10 }} />
|
|
<YAxis tick={{ fill: '#94a3b8', fontSize: 10 }} unit="%" />
|
|
<Tooltip contentStyle={{ background: '#1e293b', border: 'none', borderRadius: 8 }} formatter={(v) => [`${v}%`, 'Participacao']} />
|
|
<Bar dataKey="talk_percentage" fill="#4ade80" radius={[4,4,0,0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</CardContent></Card>
|
|
)}
|
|
{sentiment.participants_sentiment && sentiment.participants_sentiment.length > 0 && (
|
|
<Card><CardContent>
|
|
<h3 className="text-green-400 text-sm font-semibold mb-2">Por participante</h3>
|
|
<div className="space-y-3">{sentiment.participants_sentiment.map((p, i) => (
|
|
<div key={i}>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<div className="w-2 h-2 rounded-full" style={{ background: sentimentColor(p.sentiment) }} />
|
|
<span className="text-slate-300 text-sm font-medium">{p.speaker}</span>
|
|
<span className="text-slate-500 text-xs capitalize">({p.sentiment})</span>
|
|
</div>
|
|
<p className="text-slate-400 text-xs leading-relaxed pl-4">{p.details}</p>
|
|
</div>
|
|
))}</div>
|
|
</CardContent></Card>
|
|
)}
|
|
{sentiment.timeline && sentiment.timeline.length > 0 && (
|
|
<Card><CardContent>
|
|
<h3 className="text-green-400 text-sm font-semibold mb-2">Linha do tempo</h3>
|
|
<div className="space-y-2">{sentiment.timeline.map((t, i) => (
|
|
<div key={i} className="flex gap-3 text-sm">
|
|
<span className="text-slate-500 text-xs whitespace-nowrap shrink-0">{t.time_range}</span>
|
|
<div>
|
|
<span className="font-medium capitalize" style={{ color: sentimentColor(t.sentiment) }}>{t.sentiment}</span>
|
|
{t.notes && <p className="text-slate-400 text-xs mt-0.5">{t.notes}</p>}
|
|
</div>
|
|
</div>
|
|
))}</div>
|
|
</CardContent></Card>
|
|
)}
|
|
{sentiment.tension_moments && sentiment.tension_moments.length > 0 && (
|
|
<Card><CardContent><h3 className="text-red-400 text-sm font-semibold mb-2">Momentos de tensao</h3><ul className="space-y-1">{sentiment.tension_moments.map((t, i) => <li key={i} className="text-slate-300 text-sm flex gap-2"><span>•</span>{t}</li>)}</ul></CardContent></Card>
|
|
)}
|
|
{sentiment.consensus_moments && sentiment.consensus_moments.length > 0 && (
|
|
<Card><CardContent><h3 className="text-green-400 text-sm font-semibold mb-2">Momentos de consenso</h3><ul className="space-y-1">{sentiment.consensus_moments.map((t, i) => <li key={i} className="text-slate-300 text-sm flex gap-2"><span>•</span>{t}</li>)}</ul></CardContent></Card>
|
|
)}
|
|
</>
|
|
) : <p className="text-slate-400 text-sm text-center py-8">Analise de sentimento nao disponivel.</p>}
|
|
</div>
|
|
)}
|
|
|
|
{tab === 'psychological' && !loadingTab && (
|
|
<div className="space-y-3">
|
|
{psychological ? (
|
|
<>
|
|
{psychological.group_dynamics && (
|
|
<Card><CardContent><h3 className="text-purple-400 text-sm font-semibold mb-2">Dinamica do grupo</h3><p className="text-slate-200 text-sm leading-relaxed">{psychological.group_dynamics}</p></CardContent></Card>
|
|
)}
|
|
{psychological.participants_profiles && psychological.participants_profiles.length > 0 && psychological.participants_profiles.map((p, i) => (
|
|
<Card key={i}><CardContent>
|
|
<h3 className="text-purple-400 text-sm font-semibold mb-2">{p.name || p.speaker}</h3>
|
|
<div className="space-y-2 text-sm">
|
|
{p.communication_style && <div><span className="text-slate-400 text-xs">Estilo de comunicacao:</span><p className="text-slate-200">{p.communication_style}</p></div>}
|
|
{p.leadership_tendencies && <div><span className="text-slate-400 text-xs">Lideranca:</span><p className="text-slate-200">{p.leadership_tendencies}</p></div>}
|
|
{p.power_dynamics_role && <div><span className="text-slate-400 text-xs">Papel na dinamica:</span><p className="text-slate-200">{p.power_dynamics_role}</p></div>}
|
|
</div>
|
|
</CardContent></Card>
|
|
))}
|
|
{psychological.key_observations && psychological.key_observations.length > 0 && (
|
|
<Card><CardContent><h3 className="text-purple-400 text-sm font-semibold mb-2">Observacoes</h3><ul className="space-y-1">{psychological.key_observations.map((o, i) => <li key={i} className="text-slate-200 text-sm flex gap-2"><span>•</span>{o}</li>)}</ul></CardContent></Card>
|
|
)}
|
|
</>
|
|
) : <p className="text-slate-400 text-sm text-center py-8">Perfil psicologico nao disponivel.</p>}
|
|
</div>
|
|
)}
|
|
|
|
{tab === 'communication' && !loadingTab && (
|
|
<div className="space-y-3">
|
|
{communication ? (
|
|
<>
|
|
{communication.overall_score != null && (
|
|
<Card><CardContent className="text-center py-4">
|
|
<div className="text-5xl font-bold text-white mb-1">{communication.overall_score}<span className="text-xl text-slate-400">/10</span></div>
|
|
<p className="text-slate-400 text-sm">Score de comunicacao</p>
|
|
</CardContent></Card>
|
|
)}
|
|
{communication.general_feedback && (
|
|
<Card><CardContent><h3 className="text-green-400 text-sm font-semibold mb-2">Feedback geral</h3><p className="text-slate-200 text-sm leading-relaxed">{communication.general_feedback}</p></CardContent></Card>
|
|
)}
|
|
{communication.dimensions && (
|
|
<Card><CardContent>
|
|
<h3 className="text-green-400 text-sm font-semibold mb-3">Dimensoes</h3>
|
|
<div className="space-y-3">{Object.entries(communication.dimensions).map(([dim, val]) => (
|
|
<div key={dim}>
|
|
<div className="flex justify-between text-xs text-slate-300 mb-1">
|
|
<span className="capitalize">{dim.replace(/_/g, ' ')}</span>
|
|
<span>{val.score}/10</span>
|
|
</div>
|
|
<div className="h-1.5 bg-slate-700 rounded-full"><div className="h-full bg-green-500 rounded-full transition-all" style={{ width: `${val.score * 10}%` }} /></div>
|
|
{val.notes && <p className="text-slate-500 text-xs mt-1">{val.notes}</p>}
|
|
</div>
|
|
))}</div>
|
|
</CardContent></Card>
|
|
)}
|
|
{communication.strengths && communication.strengths.length > 0 && (
|
|
<Card><CardContent><h3 className="text-green-400 text-sm font-semibold mb-2">Pontos fortes</h3>
|
|
<div className="space-y-2">{communication.strengths.map((s, i) => (
|
|
<div key={i}><p className="text-slate-200 text-sm">{s.description}</p>{s.example && <p className="text-slate-500 text-xs mt-0.5 pl-4">"{s.example}"</p>}</div>
|
|
))}</div>
|
|
</CardContent></Card>
|
|
)}
|
|
{communication.improvement_areas && communication.improvement_areas.length > 0 && (
|
|
<Card><CardContent><h3 className="text-yellow-400 text-sm font-semibold mb-2">Melhorias</h3>
|
|
<div className="space-y-2">{communication.improvement_areas.map((a, i) => (
|
|
<div key={i}><p className="text-slate-200 text-sm">{a.description}</p>{a.suggestion && <p className="text-slate-500 text-xs mt-0.5 pl-4">→ {a.suggestion}</p>}</div>
|
|
))}</div>
|
|
</CardContent></Card>
|
|
)}
|
|
</>
|
|
) : <p className="text-slate-400 text-sm text-center py-8">Analise de comunicacao nao disponivel.</p>}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{meeting?.status === 'error' && (
|
|
<Card>
|
|
<CardContent className="text-center py-8">
|
|
<p className="text-red-400 font-medium">Erro ao processar reuniao</p>
|
|
{meeting.error_message && <p className="text-slate-400 text-sm mt-1">{meeting.error_message}</p>}
|
|
<Button variant="outline" onClick={() => navigate(-1)} className="mt-4">Voltar</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</Layout>
|
|
)
|
|
}
|