'use client'; import { useMemo, useState } from 'react'; import type { EvalTrace } from '@/services/evals/evalsStore'; type Props = { traces: EvalTrace[]; scenarioList: Array<{ id: string; name: string; description?: string; tools?: string[]; enabled?: boolean; notes?: string; }>; }; function formatPreview(text: string | null, max = 80) { if (!text) return ''; const compact = text.replace(/\s+/g, ' ').trim(); return compact.length > max ? `${compact.slice(0, max)}...` : compact; } function statusLabel(success: number | null) { if (success === null) return 'n/a'; return success ? 'success' : 'fail'; } function badgeStyle(kind: 'success' | 'fail' | 'neutral') { const base = { display: 'inline-block', padding: '2px 8px', borderRadius: 999, fontSize: 12 }; if (kind === 'success') return { ...base, background: '#e6f4ea', color: '#146c2e' }; if (kind === 'fail') return { ...base, background: '#fdecea', color: '#b42318' }; return { ...base, background: '#f2f2f2', color: '#333' }; } function prettyJson(value: string | null) { if (!value) return ''; try { return JSON.stringify(JSON.parse(value), null, 2); } catch { return value; } } function parseJsonArray(value: string | null): string[] { if (!value) return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : []; } catch { return []; } } export default function EvalsClient({ traces, scenarioList }: Props) { const [openTraceId, setOpenTraceId] = useState(traces[0]?.chat.trace_id || null); const [comments, setComments] = useState>(() => { const initial: Record = {}; traces.forEach((trace) => { if (trace.comment) { initial[trace.chat.trace_id] = trace.comment; } }); return initial; }); const [sourceFilter, setSourceFilter] = useState('all'); // 'all' | 'live' | 'scenario' const [scenarioFilter, setScenarioFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); const [searchFilter, setSearchFilter] = useState(''); const rows = useMemo(() => { return traces.map((trace) => { const { chat, toolCalls } = trace; const status = statusLabel(chat.success); const isLive = !chat.scenario_id; return { trace, id: chat.trace_id, source: isLive ? 'live' : 'scenario', scenario: chat.scenario_id || '—', model: chat.model || 'n/a', latency: chat.latency_ms ?? 'n/a', tokens: `${chat.input_tokens ?? 0}/${chat.output_tokens ?? 0} (${chat.total_tokens ?? 0})`, cost: chat.estimated_cost_usd ?? null, cache: chat.cache_hit == null ? 'n/a' : chat.cache_hit ? 'hit' : 'miss', cacheTokens: `${chat.cache_read_tokens ?? 0}/${chat.cache_write_tokens ?? 0}`, toolCount: chat.tool_calls_count ?? toolCalls.length, status, userPreview: formatPreview(chat.user_message), timestamp: chat.ts, mode: chat.mode || 'n/a', workflow: chat.workflow_key || '—', }; }); }, [traces]); const filteredRows = useMemo(() => { return rows.filter((row) => { if (sourceFilter !== 'all' && row.source !== sourceFilter) return false; if (scenarioFilter !== 'all' && row.scenario !== scenarioFilter) return false; if (statusFilter !== 'all' && row.status !== statusFilter) return false; if (searchFilter.trim()) { const needle = searchFilter.toLowerCase(); if (!row.userPreview.toLowerCase().includes(needle)) return false; } return true; }); }, [rows, sourceFilter, scenarioFilter, statusFilter, searchFilter]); const openTrace = traces.find((trace) => trace.chat.trace_id === openTraceId) || traces[0]; return (
Scenario Set
{scenarioList.map((scenario) => ( ))}
ID Name Description Tools Enabled Notes
{scenario.id} {scenario.name} {scenario.description || '—'} {scenario.tools?.join(', ') || '—'} {scenario.enabled === false ? 'no' : 'yes'} {scenario.notes || '—'}
{filteredRows.map((row) => { const isOpen = row.id === openTraceId; const statusKind = row.status === 'success' ? 'success' : row.status === 'fail' ? 'fail' : 'neutral'; const commentValue = comments[row.id] || ''; return ( setOpenTraceId(row.id)} style={{ cursor: 'pointer', background: isOpen ? '#eef4ff' : '#fff', borderBottom: '1px solid #eee', }} >
Source Scenario Status Latency Tokens Cost Cache Tools Model Mode Workflow User Input Time Comment
{row.source === 'live' ? '🟢 Live' : '🔬 Scenario'} {row.scenario} {row.status} {row.latency} ms {row.tokens} {row.cost == null ? 'n/a' : `$${row.cost.toFixed(4)}`} {row.cache} ({row.cacheTokens}) {row.toolCount} {row.model} {row.mode} {row.workflow} {row.userPreview} {row.timestamp}