// timeline.jsx — LLM Timeline experimental UX
// 시간 + 종류 아이콘 + 펼치는 본문, 도구 호출은 args/result, 명령은 입력/출력
const { useState: useTLState, useMemo: useTLMemo } = React;
function timeBetween(prev, cur) {
if (!prev) return null;
const ms = +cur - +prev;
if (ms < 1000) return `+${ms}ms`;
return `+${(ms / 1000).toFixed(1)}s`;
}
function TimelineRow({ ev, prev, idx, total, filter, openMap, onToggle, live }) {
if (filter === 'tool' && ev.kind !== 'tool' && ev.kind !== 'cmd') return null;
if (filter === 'message' && ev.kind === 'tool') return null;
if (filter === 'thought' && ev.kind !== 'thought') return null;
const open = !!openMap[ev.id];
const isLive = live && idx === total - 1;
const glyph = {
message: '💬',
tool: '⚙',
cmd: '$_',
thought: '·',
decision: '✓',
}[ev.kind];
const tagLabel = {
message: 'MSG',
tool: 'TOOL',
cmd: 'CMD',
thought: 'THINK',
decision: 'FINAL',
}[ev.kind];
const delta = timeBetween(prev?.t, ev.t);
return (
{fmtTimeFull(ev.t)}
{delta && {delta}}
onToggle(ev.id)}
>
{tagLabel}
{ev.kind === 'tool' &&
{ev.tool}}
{ev.kind === 'decision' &&
cycle_complete}
{ev.summary}
{(ev.durationMs != null) && (
{ev.durationMs}ms
)}
{open && (ev.kind === 'tool' || ev.kind === 'cmd') && (
{ev.kind === 'tool' && (
)}
{ev.kind === 'cmd' && (
)}
)}
);
}
function LLMTimeline({ events, live = false }) {
const [filter, setFilter] = useTLState('all');
const [openMap, setOpenMap] = useTLState({});
const [expandAll, setExpandAll] = useTLState(false);
const filtered = events;
const counts = useTLMemo(() => {
const c = { all: events.length, tool: 0, message: 0, thought: 0 };
for (const e of events) {
if (e.kind === 'tool' || e.kind === 'cmd') c.tool++;
else if (e.kind === 'message' || e.kind === 'decision') c.message++;
else if (e.kind === 'thought') c.thought++;
}
return c;
}, [events]);
const onToggle = (id) => setOpenMap(m => ({ ...m, [id]: !m[id] }));
const onToggleAll = () => {
const next = !expandAll;
setExpandAll(next);
if (next) {
const m = {};
for (const e of events) if (e.kind === 'tool' || e.kind === 'cmd') m[e.id] = true;
setOpenMap(m);
} else {
setOpenMap({});
}
};
const totalDuration = events.reduce((a, e) => a + (e.durationMs || 0), 0);
const toolCount = events.filter(e => e.kind === 'tool' || e.kind === 'cmd').length;
return (
{events.length}개 이벤트
·
{toolCount}회 도구
·
{(totalDuration / 1000).toFixed(1)}s 실행
{filtered.map((ev, i) => {
if (filter === 'tool' && ev.kind !== 'tool' && ev.kind !== 'cmd') return null;
if (filter === 'message' && ev.kind !== 'message' && ev.kind !== 'decision') return null;
if (filter === 'thought' && ev.kind !== 'thought') return null;
return (
);
})}
);
}
Object.assign(window, { LLMTimeline, TimelineRow });