'use client' import { useCallback, useEffect, useState, type ReactElement } from 'react' import type { LogEvent, MetaTag, ParsedRunLog } from '@/lib/parse-worker-log' import { cn, formatDuration } from '@/lib/utils' async function fetchDetail(fileName: string): Promise { const res = await fetch(`/api/worker-logs/${encodeURIComponent(fileName)}`, { cache: 'no-store' }) const body = await res.json().catch(() => ({})) if (!res.ok) throw new Error(body?.error ?? `request failed (${res.status})`) return body as ParsedRunLog } const META_TAG_STYLES: Record = { claim: 'text-muted-foreground', auth: 'text-muted-foreground', quota: 'text-muted-foreground', 'no-job': 'text-muted-foreground', claimed: 'text-blue-600 dark:text-blue-400', worktree: 'text-muted-foreground', config: 'text-blue-600 dark:text-blue-400', payload: 'text-muted-foreground', spawn: 'text-blue-600 dark:text-blue-400', 'claude-done': 'text-blue-600 dark:text-blue-400', cleanup: 'text-muted-foreground', exit: 'text-muted-foreground', error: 'text-destructive', 'token-expired': 'text-destructive', timeout: 'text-muted-foreground', other: 'text-muted-foreground', } function timeOnly(ts: string | null): string { if (!ts) return '' const d = new Date(ts) return isNaN(d.getTime()) ? '' : d.toLocaleTimeString() } function inputPreview(input: string): string { const oneLine = input.replace(/\s+/g, ' ').trim() return oneLine.length > 100 ? `${oneLine.slice(0, 100)}…` : oneLine } function TruncNote({ chars }: { chars?: number }) { return (
— afgekapt{chars != null ? ` (${chars} chars totaal)` : ''}
) } function EventBlock({ event }: { event: LogEvent }): ReactElement { switch (event.kind) { case 'meta': return (
{timeOnly(event.ts)} {event.tag} {event.text}
) case 'system-init': return (
Sessie gestart
model {event.model}
permission {event.permissionMode}
claude v{event.version || '?'}
{event.cwd && (
cwd: {event.cwd}
)} {(event.tools.length > 0 || event.mcpServers.length > 0) && (
{event.tools.length} tools · {event.mcpServers.length} MCP-server(s)
{event.mcpServers.length > 0 &&
mcp: {event.mcpServers.join(', ')}
}
{event.tools.join(', ')}
)}
) case 'assistant-text': return (
{event.text}
{event.truncated && }
) case 'thinking': return (
thinking…
{event.text} {event.truncated && }
) case 'tool-call': return (
▸ {event.name} {inputPreview(event.input)}
            {event.input}
          
{event.truncated && }
) case 'tool-result': return (
{event.isError ? '✕ result (error)' : '◂ result'} {event.fullLength} chars
            {event.body || '(body weggelaten — timeline ingekort)'}
          
{event.truncated && }
) case 'rate-limit': return (
rate limit: {event.status}
) case 'result': return (
Resultaat: {event.subtype} {event.durationMs != null && ( {formatDuration(event.durationMs)} )} {event.numTurns != null && ( {event.numTurns} turns )} {event.totalCostUsd != null && ( ${event.totalCostUsd.toFixed(2)} )}
{event.resultText && (
{event.resultText}
)} {event.resultTruncated && }
) case 'raw': return (
{event.text}
) } } export default function RunLogDetail({ fileName }: { fileName: string }) { const [data, setData] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) const load = useCallback(async () => { try { const d = await fetchDetail(fileName) setData(d) setError(null) } catch (err) { setError(err instanceof Error ? err.message : 'kon log niet laden') } finally { setLoading(false) } }, [fileName]) useEffect(() => { setLoading(true) setData(null) setError(null) load() }, [load]) // Keep refreshing while the run is still in progress. useEffect(() => { if (!data?.inProgress) return const id = setInterval(load, 5000) return () => clearInterval(id) }, [data?.inProgress, load]) if (loading) { return
log laden…
} if (error) { return (
{error}
) } if (!data) return null const { summary, events } = data return (
{summary.fileName} {summary.jobId && job {summary.jobId}} {summary.model && {summary.model}} {summary.permissionMode && {summary.permissionMode}} {summary.durationMs != null && {formatDuration(summary.durationMs)}} {data.inProgress && ( ● running… )} {data.responseTruncated && ( timeline ingekort (zeer grote log) )}
{summary.errorSummary && (
{summary.errorSummary}
)}
{events.length === 0 ? (
geen events
) : ( events.map((event, i) => ) )}
) }