feat(observabilidade): UI timeline por sessão com filtros

This commit is contained in:
2026-04-23 01:14:59 +01:00
parent eb781a87ce
commit 8ca6b7e166
2 changed files with 168 additions and 1 deletions
+81 -1
View File
@@ -1,3 +1,83 @@
import { useEffect, useState } from 'react'
import { useParams, Link } from 'react-router-dom'
import { getSession } from '../lib/api/sessions'
import { EventBlock } from '../components/sessions/EventBlock'
import type { SessionMeta, SessionEvent } from '../../api/types/session'
type FilterMode = 'all' | 'no-system' | 'tools-only' | 'prompts-only'
export default function SessionDetail() {
return <div className="p-6 text-slate-400">Em construção Task 7.</div>
const { id } = useParams<{ id: string }>()
const [meta, setMeta] = useState<SessionMeta | null>(null)
const [events, setEvents] = useState<SessionEvent[]>([])
const [mode, setMode] = useState<FilterMode>('all')
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!id) return
setLoading(true)
getSession(id)
.then((r) => {
setMeta(r.meta)
setEvents(r.events)
})
.catch((e: Error) => setError(e.message))
.finally(() => setLoading(false))
}, [id])
const visible = events.filter((e) => {
if (mode === 'no-system') return e.type !== 'system'
if (mode === 'tools-only') return e.tool_name !== null || e.tool_result !== null
if (mode === 'prompts-only') return e.type === 'user' && e.tool_result === null
return true
})
if (loading) return <div className="p-6 text-slate-400">A carregar</div>
if (error) return <div className="p-6 text-red-300">{error}</div>
if (!meta) return <div className="p-6 text-slate-400">Sessão não encontrada.</div>
return (
<div className="p-6 space-y-4">
<Link to="/sessions" className="text-sm text-slate-400 hover:underline">
Voltar à lista
</Link>
<header className="space-y-1">
<h1 className="text-xl font-semibold text-white">{meta.first_prompt?.slice(0, 120) ?? meta.session_id}</h1>
<div className="text-sm text-slate-400 flex flex-wrap gap-4">
<span>{new Date(meta.started_at).toLocaleString('pt-PT')}</span>
<span>Projecto: {meta.project_slug}</span>
<span>Duração: {meta.duration_sec ?? 0}s</span>
<span>Eventos: {meta.event_count}</span>
<span>Tool calls: {meta.tool_calls}</span>
<span>Skills: {meta.skills_invoked.join(', ') || '—'}</span>
<span>Resultado: {meta.outcome}</span>
</div>
<div className="text-xs text-slate-500">JSONL: {meta.jsonl_path}</div>
</header>
<div className="flex gap-2 text-sm">
{(['all', 'no-system', 'tools-only', 'prompts-only'] as FilterMode[]).map((m) => (
<button
key={m}
onClick={() => setMode(m)}
className={`px-3 py-1 rounded ${mode === m ? 'bg-indigo-500/30 text-indigo-200' : 'bg-white/5 text-slate-400'}`}
>
{m === 'all' ? 'Tudo' : m === 'no-system' ? 'Esconder system' : m === 'tools-only' ? 'Só tools' : 'Só prompts'}
</button>
))}
</div>
<div className="text-xs text-slate-500">
A mostrar {visible.length} de {events.length} eventos.
</div>
<div>
{visible.map((e) => (
<EventBlock key={e.index} event={e} defaultCollapsed={e.type === 'system' || (e.tool_result !== null && e.tool_result !== undefined)} />
))}
</div>
</div>
)
}