diff --git a/src/components/sessions/EventBlock.tsx b/src/components/sessions/EventBlock.tsx new file mode 100644 index 0000000..0d3b467 --- /dev/null +++ b/src/components/sessions/EventBlock.tsx @@ -0,0 +1,87 @@ +import { useState } from 'react' +import type { SessionEvent } from '../../../api/types/session' + +function truncate(s: string, n: number): string { + return s.length > n ? s.slice(0, n) + '…' : s +} + +interface Props { + event: SessionEvent + defaultCollapsed: boolean +} + +export function EventBlock({ event, defaultCollapsed }: Props) { + const [collapsed, setCollapsed] = useState(defaultCollapsed) + + const base = 'rounded border px-3 py-2 my-1 text-sm' + switch (event.type) { + case 'user': + if (event.tool_result !== null && event.tool_result !== undefined) { + return ( +
+ + {!collapsed && ( +
+                {typeof event.tool_result === 'string' ? event.tool_result : JSON.stringify(event.tool_result, null, 2)}
+              
+ )} +
+ ) + } + return ( +
+
user
+
{event.text ?? '—'}
+
+ ) + case 'assistant': + if (event.tool_name) { + return ( +
+ + {!collapsed && event.tool_input && ( +
+                {JSON.stringify(event.tool_input, null, 2)}
+              
+ )} +
+ ) + } + return ( +
+
assistant
+
{truncate(event.text ?? '—', collapsed ? 300 : Number.MAX_SAFE_INTEGER)}
+ {(event.text?.length ?? 0) > 300 && ( + + )} +
+ ) + case 'system': + return ( +
+ + {!collapsed &&
{event.text ?? JSON.stringify(event.raw)}
} +
+ ) + case 'attachment': + return ( +
+ 📎 attachment +
+ ) + default: + return ( +
+ {event.type} +
+ ) + } +} diff --git a/src/pages/SessionDetail.tsx b/src/pages/SessionDetail.tsx index f92514b..adfb23a 100644 --- a/src/pages/SessionDetail.tsx +++ b/src/pages/SessionDetail.tsx @@ -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
Em construção — Task 7.
+ const { id } = useParams<{ id: string }>() + const [meta, setMeta] = useState(null) + const [events, setEvents] = useState([]) + const [mode, setMode] = useState('all') + const [loading, setLoading] = useState(true) + const [error, setError] = useState(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
A carregar…
+ if (error) return
{error}
+ if (!meta) return
Sessão não encontrada.
+ + return ( +
+ + ← Voltar à lista + + +
+

{meta.first_prompt?.slice(0, 120) ?? meta.session_id}

+
+ {new Date(meta.started_at).toLocaleString('pt-PT')} + Projecto: {meta.project_slug} + Duração: {meta.duration_sec ?? 0}s + Eventos: {meta.event_count} + Tool calls: {meta.tool_calls} + Skills: {meta.skills_invoked.join(', ') || '—'} + Resultado: {meta.outcome} +
+
JSONL: {meta.jsonl_path}
+
+ +
+ {(['all', 'no-system', 'tools-only', 'prompts-only'] as FilterMode[]).map((m) => ( + + ))} +
+ +
+ A mostrar {visible.length} de {events.length} eventos. +
+ +
+ {visible.map((e) => ( + + ))} +
+
+ ) }