@roj-ai/debug 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/debug/DebugContext.d.ts +10 -0
- package/dist/components/debug/DebugNavigation.d.ts +29 -0
- package/dist/components/debug/DebugShell.d.ts +18 -0
- package/dist/components/debug/LLMCallDetail.d.ts +7 -0
- package/dist/components/debug/TimelineDetailInspector.d.ts +6 -0
- package/dist/components/debug/communication/CommunicationDiagram.d.ts +9 -0
- package/dist/components/debug/communication/DiagramHeader.d.ts +7 -0
- package/dist/components/debug/communication/ParticipantLane.d.ts +7 -0
- package/dist/components/debug/communication/TimeAxis.d.ts +9 -0
- package/dist/components/debug/communication/elements/IdleGap.d.ts +9 -0
- package/dist/components/debug/communication/elements/LLMBlock.d.ts +9 -0
- package/dist/components/debug/communication/elements/MessageArrow.d.ts +10 -0
- package/dist/components/debug/communication/elements/ToolBlock.d.ts +9 -0
- package/dist/components/debug/communication/hooks/useDiagramData.d.ts +12 -0
- package/dist/components/debug/communication/hooks/useTimeCompression.d.ts +7 -0
- package/dist/components/debug/communication/hooks/useZoomPan.d.ts +11 -0
- package/dist/components/debug/communication/popovers/ElementPopover.d.ts +8 -0
- package/dist/components/debug/communication/types.d.ts +136 -0
- package/dist/components/debug/index.d.ts +11 -0
- package/dist/components/debug/pages/AgentDetailPage.d.ts +3 -0
- package/dist/components/debug/pages/AgentsPage.d.ts +1 -0
- package/dist/components/debug/pages/CommunicationPage.d.ts +1 -0
- package/dist/components/debug/pages/DashboardPage.d.ts +1 -0
- package/dist/components/debug/pages/EventsPage.d.ts +1 -0
- package/dist/components/debug/pages/FilesPage.d.ts +1 -0
- package/dist/components/debug/pages/LLMCallPage.d.ts +1 -0
- package/dist/components/debug/pages/LLMCallsPage.d.ts +1 -0
- package/dist/components/debug/pages/LogsPage.d.ts +1 -0
- package/dist/components/debug/pages/MailboxPage.d.ts +1 -0
- package/dist/components/debug/pages/ServicesPage.d.ts +1 -0
- package/dist/components/debug/pages/TimelinePage.d.ts +1 -0
- package/dist/components/debug/pages/UserChatPage.d.ts +1 -0
- package/dist/components/debug/pages/index.d.ts +13 -0
- package/dist/index.d.ts +9 -0
- package/dist/lib/domain-utils.d.ts +7 -0
- package/dist/providers/EventPollingProvider.d.ts +27 -0
- package/dist/stores/event-store.d.ts +93 -0
- package/dist/utils/format.d.ts +1 -0
- package/package.json +43 -0
- package/src/components/debug/DebugContext.tsx +18 -0
- package/src/components/debug/DebugNavigation.tsx +55 -0
- package/src/components/debug/DebugShell.tsx +321 -0
- package/src/components/debug/LLMCallDetail.tsx +740 -0
- package/src/components/debug/TimelineDetailInspector.tsx +204 -0
- package/src/components/debug/communication/CommunicationDiagram.tsx +260 -0
- package/src/components/debug/communication/DiagramHeader.tsx +113 -0
- package/src/components/debug/communication/ParticipantLane.tsx +60 -0
- package/src/components/debug/communication/TimeAxis.tsx +106 -0
- package/src/components/debug/communication/elements/IdleGap.tsx +90 -0
- package/src/components/debug/communication/elements/LLMBlock.tsx +107 -0
- package/src/components/debug/communication/elements/MessageArrow.tsx +119 -0
- package/src/components/debug/communication/elements/ToolBlock.tsx +99 -0
- package/src/components/debug/communication/hooks/useDiagramData.ts +294 -0
- package/src/components/debug/communication/hooks/useTimeCompression.ts +140 -0
- package/src/components/debug/communication/hooks/useZoomPan.ts +87 -0
- package/src/components/debug/communication/popovers/ElementPopover.tsx +158 -0
- package/src/components/debug/communication/types.ts +180 -0
- package/src/components/debug/index.ts +37 -0
- package/src/components/debug/pages/AgentDetailPage.tsx +1295 -0
- package/src/components/debug/pages/AgentsPage.tsx +297 -0
- package/src/components/debug/pages/CommunicationPage.tsx +89 -0
- package/src/components/debug/pages/DashboardPage.tsx +1504 -0
- package/src/components/debug/pages/EventsPage.tsx +276 -0
- package/src/components/debug/pages/FilesPage.tsx +366 -0
- package/src/components/debug/pages/LLMCallPage.tsx +32 -0
- package/src/components/debug/pages/LLMCallsPage.tsx +473 -0
- package/src/components/debug/pages/LogsPage.tsx +199 -0
- package/src/components/debug/pages/MailboxPage.tsx +232 -0
- package/src/components/debug/pages/ServicesPage.tsx +193 -0
- package/src/components/debug/pages/TimelinePage.tsx +569 -0
- package/src/components/debug/pages/UserChatPage.tsx +250 -0
- package/src/components/debug/pages/index.ts +13 -0
- package/src/index.ts +55 -0
- package/src/lib/domain-utils.ts +12 -0
- package/src/providers/EventPollingProvider.tsx +60 -0
- package/src/stores/event-store.ts +497 -0
- package/src/utils/format.ts +8 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { TimelineItem } from '@roj-ai/shared'
|
|
2
|
+
import { LLMCallDetail } from './LLMCallDetail'
|
|
3
|
+
|
|
4
|
+
export function TimelineDetailInspector({
|
|
5
|
+
sessionId,
|
|
6
|
+
item,
|
|
7
|
+
onNavigate,
|
|
8
|
+
}: {
|
|
9
|
+
sessionId: string
|
|
10
|
+
item: TimelineItem
|
|
11
|
+
onNavigate?: (path: string) => void
|
|
12
|
+
}) {
|
|
13
|
+
const agentLink = (agentId: string, name: string) =>
|
|
14
|
+
onNavigate
|
|
15
|
+
? (
|
|
16
|
+
<button type="button" onClick={() => onNavigate(`agents/${agentId}`)} className="text-violet-600 hover:underline cursor-pointer">
|
|
17
|
+
{name}
|
|
18
|
+
</button>
|
|
19
|
+
)
|
|
20
|
+
: <span>{name}</span>
|
|
21
|
+
|
|
22
|
+
// LLM with detailed call log
|
|
23
|
+
if (item.type === 'llm' && item.llmCallId) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="space-y-4">
|
|
26
|
+
<div className="flex items-center gap-3">
|
|
27
|
+
<TypeBadge type={item.type} />
|
|
28
|
+
<span className="font-medium">{item.model?.split('/').pop() ?? 'LLM Call'}</span>
|
|
29
|
+
<StatusBadge status={item.status} />
|
|
30
|
+
</div>
|
|
31
|
+
<LLMCallDetail sessionId={sessionId} callId={item.llmCallId} />
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// LLM without detailed log
|
|
37
|
+
if (item.type === 'llm') {
|
|
38
|
+
return (
|
|
39
|
+
<div className="space-y-4">
|
|
40
|
+
<div className="flex items-center gap-3">
|
|
41
|
+
<TypeBadge type={item.type} />
|
|
42
|
+
<span className="font-medium">{item.model?.split('/').pop() ?? 'LLM Call'}</span>
|
|
43
|
+
<StatusBadge status={item.status} />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
|
47
|
+
<MetricCard label="Agent">{agentLink(item.agentId, item.agentName)}</MetricCard>
|
|
48
|
+
{item.model && <MetricCard label="Model"><span className="font-mono text-xs">{item.model}</span></MetricCard>}
|
|
49
|
+
{item.durationMs !== undefined && <MetricCard label="Duration">{(item.durationMs / 1000).toFixed(2)}s</MetricCard>}
|
|
50
|
+
{item.cost !== undefined && <MetricCard label="Cost"><span className="text-green-600">${item.cost.toFixed(6)}</span></MetricCard>}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{(item.promptTokens !== undefined || item.completionTokens !== undefined) && (
|
|
54
|
+
<div className="bg-violet-50 border border-violet-200 rounded-md p-4">
|
|
55
|
+
<h3 className="font-medium text-violet-800 mb-3">Token Usage</h3>
|
|
56
|
+
<div className="grid grid-cols-3 gap-4 text-sm">
|
|
57
|
+
<div>
|
|
58
|
+
<div className="text-xs text-violet-600 mb-1">Prompt</div>
|
|
59
|
+
<div className="font-mono font-medium text-green-600">{(item.promptTokens ?? 0).toLocaleString()}</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<div className="text-xs text-violet-600 mb-1">Completion</div>
|
|
63
|
+
<div className="font-mono font-medium text-violet-600">{(item.completionTokens ?? 0).toLocaleString()}</div>
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<div className="text-xs text-violet-600 mb-1">Total</div>
|
|
67
|
+
<div className="font-mono font-medium">{((item.promptTokens ?? 0) + (item.completionTokens ?? 0)).toLocaleString()}</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<div className="text-xs text-slate-500 space-y-1">
|
|
74
|
+
<div>Started: {new Date(item.startedAt).toLocaleString()}</div>
|
|
75
|
+
{item.completedAt && <div>Completed: {new Date(item.completedAt).toLocaleString()}</div>}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{item.error && (
|
|
79
|
+
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
80
|
+
<h3 className="font-semibold text-red-800 mb-2">Error</h3>
|
|
81
|
+
<div className="text-red-700 font-mono text-sm whitespace-pre-wrap">{item.error}</div>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Tool
|
|
89
|
+
if (item.type === 'tool') {
|
|
90
|
+
return (
|
|
91
|
+
<div className="space-y-4">
|
|
92
|
+
<div className="flex items-center gap-3">
|
|
93
|
+
<TypeBadge type={item.type} />
|
|
94
|
+
<span className="font-medium font-mono">{item.toolName ?? 'Tool'}</span>
|
|
95
|
+
<StatusBadge status={item.status} />
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
|
99
|
+
<MetricCard label="Agent">{agentLink(item.agentId, item.agentName)}</MetricCard>
|
|
100
|
+
{item.durationMs !== undefined && <MetricCard label="Duration">{(item.durationMs / 1000).toFixed(2)}s</MetricCard>}
|
|
101
|
+
<MetricCard label="Started">{new Date(item.startedAt).toLocaleTimeString()}</MetricCard>
|
|
102
|
+
{item.completedAt && <MetricCard label="Completed">{new Date(item.completedAt).toLocaleTimeString()}</MetricCard>}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{item.toolInput !== undefined && (
|
|
106
|
+
<div className="border border-slate-200 rounded-md overflow-hidden">
|
|
107
|
+
<div className="px-4 py-2 bg-slate-50 border-b border-slate-200">
|
|
108
|
+
<h3 className="text-sm font-medium text-slate-700">Input</h3>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="p-4 max-h-64 overflow-auto">
|
|
111
|
+
<pre className="font-mono text-xs whitespace-pre-wrap break-words">{JSON.stringify(item.toolInput, null, 2)}</pre>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{item.toolResult !== undefined && (
|
|
117
|
+
<div className="border border-emerald-200 rounded-md overflow-hidden">
|
|
118
|
+
<div className="px-4 py-2 bg-emerald-50 border-b border-emerald-200">
|
|
119
|
+
<h3 className="text-sm font-medium text-emerald-700">Result</h3>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="p-4 max-h-64 overflow-auto">
|
|
122
|
+
<pre className="font-mono text-xs whitespace-pre-wrap break-words">{JSON.stringify(item.toolResult, null, 2)}</pre>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{item.error && (
|
|
128
|
+
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
129
|
+
<h3 className="font-semibold text-red-800 mb-2">Error</h3>
|
|
130
|
+
<div className="text-red-700 font-mono text-sm whitespace-pre-wrap">{item.error}</div>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Compaction
|
|
138
|
+
if (item.type === 'compaction') {
|
|
139
|
+
return (
|
|
140
|
+
<div className="space-y-4">
|
|
141
|
+
<div className="flex items-center gap-3">
|
|
142
|
+
<TypeBadge type={item.type} />
|
|
143
|
+
<span className="font-medium">Context Compaction</span>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
|
147
|
+
<MetricCard label="Agent">{agentLink(item.agentId, item.agentName)}</MetricCard>
|
|
148
|
+
{item.originalTokens !== undefined && <MetricCard label="Original Tokens">{item.originalTokens.toLocaleString()}</MetricCard>}
|
|
149
|
+
{item.compactedTokens !== undefined && <MetricCard label="Compacted Tokens">{item.compactedTokens.toLocaleString()}</MetricCard>}
|
|
150
|
+
{item.messagesRemoved !== undefined && <MetricCard label="Messages Removed">{item.messagesRemoved}</MetricCard>}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{item.originalTokens !== undefined && item.compactedTokens !== undefined && (
|
|
154
|
+
<div className="bg-amber-50 border border-amber-200 rounded-md p-4">
|
|
155
|
+
<h3 className="font-medium text-amber-800 mb-3">Compression</h3>
|
|
156
|
+
<div className="flex items-center gap-4">
|
|
157
|
+
<div className="text-2xl font-bold text-amber-600">
|
|
158
|
+
{Math.round((1 - item.compactedTokens / item.originalTokens) * 100)}%
|
|
159
|
+
</div>
|
|
160
|
+
<div className="text-sm text-amber-700">
|
|
161
|
+
reduced ({item.originalTokens.toLocaleString()} → {item.compactedTokens.toLocaleString()} tokens)
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
<div className="text-xs text-slate-500">
|
|
168
|
+
Compacted at: {new Date(item.startedAt).toLocaleString()}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return <div className="text-slate-500 text-sm">Unknown item type</div>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function StatusBadge({ status }: { status: string }) {
|
|
178
|
+
if (status === 'error') return <span className="text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded-full font-medium">Error</span>
|
|
179
|
+
if (status === 'running') return <span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-medium animate-pulse">Running</span>
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function TypeBadge({ type }: { type: TimelineItem['type'] }) {
|
|
184
|
+
const config: Record<TimelineItem['type'], string> = {
|
|
185
|
+
llm: 'bg-violet-100 text-violet-700',
|
|
186
|
+
tool: 'bg-emerald-100 text-emerald-700',
|
|
187
|
+
compaction: 'bg-amber-100 text-amber-700',
|
|
188
|
+
}
|
|
189
|
+
const labels: Record<TimelineItem['type'], string> = {
|
|
190
|
+
llm: 'LLM',
|
|
191
|
+
tool: 'Tool',
|
|
192
|
+
compaction: 'Compact',
|
|
193
|
+
}
|
|
194
|
+
return <span className={`text-xs px-1.5 py-0.5 rounded-full font-medium ${config[type]}`}>{labels[type]}</span>
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function MetricCard({ label, children }: { label: string; children: React.ReactNode }) {
|
|
198
|
+
return (
|
|
199
|
+
<div className="bg-slate-50 rounded-md p-3">
|
|
200
|
+
<div className="text-xs text-slate-500 mb-1">{label}</div>
|
|
201
|
+
<div className="font-medium text-slate-900">{children}</div>
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { DiagramHeader } from './DiagramHeader'
|
|
3
|
+
import { IdleGap } from './elements/IdleGap'
|
|
4
|
+
import { LLMBlock } from './elements/LLMBlock'
|
|
5
|
+
import { MessageArrow } from './elements/MessageArrow'
|
|
6
|
+
import { ToolBlock } from './elements/ToolBlock'
|
|
7
|
+
import { useZoomPan } from './hooks/useZoomPan'
|
|
8
|
+
import { ParticipantLane } from './ParticipantLane'
|
|
9
|
+
import { ElementPopover } from './popovers/ElementPopover'
|
|
10
|
+
import { TimeAxis } from './TimeAxis'
|
|
11
|
+
import type { DiagramData, DiagramLLMBlock, DiagramMessage, DiagramToolBlock, PopoverState, TimeSegment } from './types'
|
|
12
|
+
import { LAYOUT } from './types'
|
|
13
|
+
|
|
14
|
+
interface CommunicationDiagramProps {
|
|
15
|
+
data: DiagramData & {
|
|
16
|
+
timestampToY: (timestamp: number) => number
|
|
17
|
+
formatIdleDuration: (ms: number) => string
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function CommunicationDiagram({ data }: CommunicationDiagramProps) {
|
|
22
|
+
const [popover, setPopover] = useState<PopoverState>({ element: null, x: 0, y: 0 })
|
|
23
|
+
const [showLabels, setShowLabels] = useState(false)
|
|
24
|
+
|
|
25
|
+
const { state: zoomPan, containerRef, zoomIn, zoomOut, resetZoom, toggleAutoScroll, handleScroll, handleWheel } = useZoomPan(data.totalHeight)
|
|
26
|
+
|
|
27
|
+
// Calculate dimensions
|
|
28
|
+
const totalWidth = LAYOUT.timeAxisWidth + data.participants.length * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.padding
|
|
29
|
+
const scaledHeight = data.totalHeight * zoomPan.zoom
|
|
30
|
+
const scaledWidth = totalWidth * zoomPan.zoom
|
|
31
|
+
|
|
32
|
+
// Calculate cumulative Y positions for idle gaps
|
|
33
|
+
const idleGapPositions: Array<{ segment: TimeSegment; yPosition: number }> = []
|
|
34
|
+
let cumulativeY = 0
|
|
35
|
+
for (const segment of data.timeSegments) {
|
|
36
|
+
if (segment.type === 'idle') {
|
|
37
|
+
idleGapPositions.push({ segment, yPosition: cumulativeY })
|
|
38
|
+
}
|
|
39
|
+
cumulativeY += segment.displayHeight
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handleMessageHover = (message: DiagramMessage | null, x: number, y: number) => {
|
|
43
|
+
if (message) {
|
|
44
|
+
setPopover({ element: { type: 'message', data: message }, x, y })
|
|
45
|
+
} else {
|
|
46
|
+
setPopover({ element: null, x: 0, y: 0 })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handleLLMHover = (block: DiagramLLMBlock | null, x: number, y: number) => {
|
|
51
|
+
if (block) {
|
|
52
|
+
setPopover({ element: { type: 'llm', data: block }, x, y })
|
|
53
|
+
} else {
|
|
54
|
+
setPopover({ element: null, x: 0, y: 0 })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const handleToolHover = (block: DiagramToolBlock | null, x: number, y: number) => {
|
|
59
|
+
if (block) {
|
|
60
|
+
setPopover({ element: { type: 'tool', data: block }, x, y })
|
|
61
|
+
} else {
|
|
62
|
+
setPopover({ element: null, x: 0, y: 0 })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="h-full flex flex-col">
|
|
68
|
+
{/* Toolbar */}
|
|
69
|
+
<div className="flex items-center gap-3 px-3 py-2 bg-slate-50/80 border-b border-slate-100 shrink-0">
|
|
70
|
+
{/* Zoom controls */}
|
|
71
|
+
<div className="flex items-center gap-1 bg-white rounded-md border border-slate-200 p-0.5">
|
|
72
|
+
<button
|
|
73
|
+
onClick={zoomOut}
|
|
74
|
+
className="w-7 h-7 flex items-center justify-center text-slate-500 hover:bg-slate-100 rounded-md transition-colors"
|
|
75
|
+
title="Zoom Out (Ctrl+-)"
|
|
76
|
+
>
|
|
77
|
+
<MinusIcon />
|
|
78
|
+
</button>
|
|
79
|
+
<span className="text-xs text-slate-600 w-12 text-center font-medium">
|
|
80
|
+
{Math.round(zoomPan.zoom * 100)}%
|
|
81
|
+
</span>
|
|
82
|
+
<button
|
|
83
|
+
onClick={zoomIn}
|
|
84
|
+
className="w-7 h-7 flex items-center justify-center text-slate-500 hover:bg-slate-100 rounded-md transition-colors"
|
|
85
|
+
title="Zoom In (Ctrl++)"
|
|
86
|
+
>
|
|
87
|
+
<PlusIcon />
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<button
|
|
92
|
+
onClick={resetZoom}
|
|
93
|
+
className="px-2 py-1 text-xs text-slate-500 hover:text-slate-700 hover:bg-white rounded border border-transparent hover:border-slate-200 transition-colors"
|
|
94
|
+
>
|
|
95
|
+
Reset
|
|
96
|
+
</button>
|
|
97
|
+
|
|
98
|
+
<div className="h-4 w-px bg-slate-200" />
|
|
99
|
+
|
|
100
|
+
<button
|
|
101
|
+
onClick={toggleAutoScroll}
|
|
102
|
+
className={`flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md transition-colors ${
|
|
103
|
+
zoomPan.autoScroll
|
|
104
|
+
? 'bg-violet-50 text-violet-600 border border-violet-200'
|
|
105
|
+
: 'bg-white text-slate-500 border border-slate-200 hover:border-slate-300'
|
|
106
|
+
}`}
|
|
107
|
+
>
|
|
108
|
+
<AutoScrollIcon active={zoomPan.autoScroll} />
|
|
109
|
+
Auto-scroll
|
|
110
|
+
</button>
|
|
111
|
+
|
|
112
|
+
<div className="h-4 w-px bg-slate-200" />
|
|
113
|
+
|
|
114
|
+
{/* Show labels toggle */}
|
|
115
|
+
<label className="flex items-center gap-2 cursor-pointer select-none">
|
|
116
|
+
<input
|
|
117
|
+
type="checkbox"
|
|
118
|
+
checked={showLabels}
|
|
119
|
+
onChange={(e) => setShowLabels(e.target.checked)}
|
|
120
|
+
className="w-3.5 h-3.5 rounded border-slate-300 text-violet-500 focus:ring-violet-500 focus:ring-offset-0"
|
|
121
|
+
/>
|
|
122
|
+
<span className="text-xs text-slate-600">Show message text</span>
|
|
123
|
+
</label>
|
|
124
|
+
|
|
125
|
+
<div className="flex-1" />
|
|
126
|
+
|
|
127
|
+
{/* Legend */}
|
|
128
|
+
<div className="flex items-center gap-4 text-[10px] text-slate-400">
|
|
129
|
+
<span className="flex items-center gap-1.5">
|
|
130
|
+
<span className="w-4 h-px bg-blue-300" />
|
|
131
|
+
Messages
|
|
132
|
+
</span>
|
|
133
|
+
<span className="flex items-center gap-1.5">
|
|
134
|
+
<span className="w-3 h-3 bg-violet-50 border border-violet-200 rounded" />
|
|
135
|
+
LLM
|
|
136
|
+
</span>
|
|
137
|
+
<span className="flex items-center gap-1.5">
|
|
138
|
+
<span className="w-3 h-3 bg-teal-50 border border-teal-200 rounded" />
|
|
139
|
+
Tools
|
|
140
|
+
</span>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Diagram container */}
|
|
145
|
+
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
|
146
|
+
{/* Header (sticky) - scales with zoom */}
|
|
147
|
+
<DiagramHeader participants={data.participants} zoom={zoomPan.zoom} />
|
|
148
|
+
|
|
149
|
+
{/* Scrollable SVG area */}
|
|
150
|
+
<div
|
|
151
|
+
ref={containerRef}
|
|
152
|
+
className="flex-1 overflow-auto bg-white"
|
|
153
|
+
onScroll={handleScroll}
|
|
154
|
+
onWheel={handleWheel}
|
|
155
|
+
>
|
|
156
|
+
<svg
|
|
157
|
+
width={scaledWidth}
|
|
158
|
+
height={scaledHeight + LAYOUT.padding * 2}
|
|
159
|
+
>
|
|
160
|
+
<g transform={`scale(${zoomPan.zoom})`}>
|
|
161
|
+
<g transform={`translate(0, ${LAYOUT.padding})`}>
|
|
162
|
+
{/* Background grid lines for participant lanes */}
|
|
163
|
+
{data.participants.map((participant) => (
|
|
164
|
+
<ParticipantLane
|
|
165
|
+
key={participant.id}
|
|
166
|
+
participant={participant}
|
|
167
|
+
totalHeight={data.totalHeight}
|
|
168
|
+
/>
|
|
169
|
+
))}
|
|
170
|
+
|
|
171
|
+
{/* Time axis */}
|
|
172
|
+
<TimeAxis
|
|
173
|
+
segments={data.timeSegments}
|
|
174
|
+
sessionStartTime={data.sessionStartTime}
|
|
175
|
+
timestampToY={data.timestampToY}
|
|
176
|
+
totalHeight={data.totalHeight}
|
|
177
|
+
/>
|
|
178
|
+
|
|
179
|
+
{/* Idle gaps */}
|
|
180
|
+
{idleGapPositions.map((item, idx) => (
|
|
181
|
+
<IdleGap
|
|
182
|
+
key={idx}
|
|
183
|
+
segment={item.segment}
|
|
184
|
+
participants={data.participants}
|
|
185
|
+
yPosition={item.yPosition}
|
|
186
|
+
formatDuration={data.formatIdleDuration}
|
|
187
|
+
/>
|
|
188
|
+
))}
|
|
189
|
+
|
|
190
|
+
{/* LLM blocks */}
|
|
191
|
+
{data.llmBlocks.map((block) => (
|
|
192
|
+
<LLMBlock
|
|
193
|
+
key={block.id}
|
|
194
|
+
block={block}
|
|
195
|
+
participants={data.participants}
|
|
196
|
+
onHover={handleLLMHover}
|
|
197
|
+
/>
|
|
198
|
+
))}
|
|
199
|
+
|
|
200
|
+
{/* Tool blocks */}
|
|
201
|
+
{data.toolBlocks.map((block) => (
|
|
202
|
+
<ToolBlock
|
|
203
|
+
key={block.id}
|
|
204
|
+
block={block}
|
|
205
|
+
participants={data.participants}
|
|
206
|
+
onHover={handleToolHover}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
|
|
210
|
+
{/* Messages (on top) */}
|
|
211
|
+
{data.messages.map((message) => (
|
|
212
|
+
<MessageArrow
|
|
213
|
+
key={message.id}
|
|
214
|
+
message={message}
|
|
215
|
+
participants={data.participants}
|
|
216
|
+
showLabel={showLabels}
|
|
217
|
+
onHover={handleMessageHover}
|
|
218
|
+
/>
|
|
219
|
+
))}
|
|
220
|
+
</g>
|
|
221
|
+
</g>
|
|
222
|
+
</svg>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* Popover */}
|
|
227
|
+
{popover.element && (
|
|
228
|
+
<ElementPopover
|
|
229
|
+
element={popover.element}
|
|
230
|
+
x={popover.x}
|
|
231
|
+
y={popover.y}
|
|
232
|
+
/>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function MinusIcon() {
|
|
239
|
+
return (
|
|
240
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
241
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
|
242
|
+
</svg>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function PlusIcon() {
|
|
247
|
+
return (
|
|
248
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
249
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
250
|
+
</svg>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function AutoScrollIcon({ active }: { active: boolean }) {
|
|
255
|
+
return (
|
|
256
|
+
<svg className={`w-3.5 h-3.5 ${active ? 'text-violet-500' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
257
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
|
258
|
+
</svg>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { DebugLink } from '../DebugNavigation'
|
|
2
|
+
import type { DiagramParticipant } from './types'
|
|
3
|
+
import { COLORS, LAYOUT } from './types'
|
|
4
|
+
|
|
5
|
+
interface DiagramHeaderProps {
|
|
6
|
+
participants: DiagramParticipant[]
|
|
7
|
+
zoom: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DiagramHeader({ participants, zoom }: DiagramHeaderProps) {
|
|
11
|
+
const scaledTimeAxisWidth = LAYOUT.timeAxisWidth * zoom
|
|
12
|
+
const scaledParticipantWidth = LAYOUT.participantWidth * zoom
|
|
13
|
+
const scaledParticipantGap = LAYOUT.participantGap * zoom
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className="sticky top-0 z-10 bg-white border-b border-slate-200 flex shrink-0 overflow-hidden"
|
|
18
|
+
style={{ height: LAYOUT.headerHeight }}
|
|
19
|
+
>
|
|
20
|
+
{/* Time axis header */}
|
|
21
|
+
<div
|
|
22
|
+
className="flex items-center justify-center text-[10px] font-medium text-slate-400 uppercase tracking-wider border-r border-slate-200 shrink-0 bg-slate-50"
|
|
23
|
+
style={{ width: scaledTimeAxisWidth }}
|
|
24
|
+
>
|
|
25
|
+
Time
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{/* Participant headers */}
|
|
29
|
+
{participants.map((participant, idx) => {
|
|
30
|
+
const isOdd = idx % 2 === 1
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
key={participant.id}
|
|
34
|
+
className={`flex flex-col items-center justify-center shrink-0 py-2 border-l border-slate-200 ${isOdd ? 'bg-slate-50/80' : 'bg-white'}`}
|
|
35
|
+
style={{
|
|
36
|
+
width: scaledParticipantWidth + scaledParticipantGap,
|
|
37
|
+
paddingLeft: scaledParticipantGap / 2,
|
|
38
|
+
paddingRight: scaledParticipantGap / 2,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
{participant.id === 'user'
|
|
42
|
+
? (
|
|
43
|
+
<div className="flex items-center gap-1.5">
|
|
44
|
+
<UserIcon />
|
|
45
|
+
<span className={`text-xs font-semibold ${COLORS.participant.user}`}>
|
|
46
|
+
{participant.name}
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
: (
|
|
51
|
+
<DebugLink
|
|
52
|
+
to={`agents/${participant.id}`}
|
|
53
|
+
className={`text-xs font-semibold hover:underline truncate max-w-full ${COLORS.participant[participant.role]}`}
|
|
54
|
+
>
|
|
55
|
+
{participant.name}
|
|
56
|
+
</DebugLink>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
<div className="flex items-center gap-1.5 mt-1">
|
|
60
|
+
<RoleBadge role={participant.role} />
|
|
61
|
+
<StatusIndicator status={participant.status} />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
})}
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function UserIcon() {
|
|
71
|
+
return (
|
|
72
|
+
<svg className="w-3.5 h-3.5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
73
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
74
|
+
</svg>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function RoleBadge({ role }: { role: DiagramParticipant['role'] }) {
|
|
79
|
+
const config: Record<DiagramParticipant['role'], { bg: string; text: string; label: string }> = {
|
|
80
|
+
user: { bg: 'bg-blue-100', text: 'text-blue-700', label: 'User' },
|
|
81
|
+
communicator: { bg: 'bg-emerald-100', text: 'text-emerald-700', label: 'Comm' },
|
|
82
|
+
orchestrator: { bg: 'bg-violet-100', text: 'text-violet-700', label: 'Orch' },
|
|
83
|
+
worker: { bg: 'bg-slate-100', text: 'text-slate-700', label: 'Worker' },
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { bg, text, label } = config[role]
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<span className={`text-[9px] px-1.5 py-0.5 rounded-full ${bg} ${text} font-medium`}>
|
|
90
|
+
{label}
|
|
91
|
+
</span>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function StatusIndicator({ status }: { status: DiagramParticipant['status'] }) {
|
|
96
|
+
const config: Record<DiagramParticipant['status'], { bg: string; ring?: string; animate?: boolean }> = {
|
|
97
|
+
idle: { bg: 'bg-slate-300' },
|
|
98
|
+
thinking: { bg: 'bg-amber-400', ring: 'ring-amber-200', animate: true },
|
|
99
|
+
responding: { bg: 'bg-blue-400', ring: 'ring-blue-200' },
|
|
100
|
+
waiting_for_user: { bg: 'bg-purple-400', ring: 'ring-purple-200' },
|
|
101
|
+
error: { bg: 'bg-red-400', ring: 'ring-red-200' },
|
|
102
|
+
paused: { bg: 'bg-amber-400', ring: 'ring-amber-200' },
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { bg, ring, animate } = config[status]
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<span
|
|
109
|
+
className={`w-1.5 h-1.5 rounded-full ${bg} ${ring ? `ring-2 ${ring}` : ''} ${animate ? 'animate-pulse' : ''}`}
|
|
110
|
+
title={status.replace(/_/g, ' ')}
|
|
111
|
+
/>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { DiagramParticipant } from './types'
|
|
2
|
+
import { LAYOUT } from './types'
|
|
3
|
+
|
|
4
|
+
interface ParticipantLaneProps {
|
|
5
|
+
participant: DiagramParticipant
|
|
6
|
+
totalHeight: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ParticipantLane({ participant, totalHeight }: ParticipantLaneProps) {
|
|
10
|
+
const x = LAYOUT.timeAxisWidth + participant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap)
|
|
11
|
+
const centerX = x + LAYOUT.participantWidth / 2
|
|
12
|
+
const isOdd = participant.columnIndex % 2 === 1
|
|
13
|
+
const columnWidth = LAYOUT.participantWidth + LAYOUT.participantGap
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<g>
|
|
17
|
+
{/* Column background - alternating */}
|
|
18
|
+
<rect
|
|
19
|
+
x={x - LAYOUT.participantGap / 2}
|
|
20
|
+
y={0}
|
|
21
|
+
width={columnWidth}
|
|
22
|
+
height={totalHeight}
|
|
23
|
+
className={isOdd ? 'fill-slate-50/80' : 'fill-white'}
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
{/* Column border - left side */}
|
|
27
|
+
<line
|
|
28
|
+
x1={x - LAYOUT.participantGap / 2}
|
|
29
|
+
y1={0}
|
|
30
|
+
x2={x - LAYOUT.participantGap / 2}
|
|
31
|
+
y2={totalHeight}
|
|
32
|
+
className="stroke-slate-200"
|
|
33
|
+
strokeWidth={1}
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
{/* Column border - right side (only for last column) */}
|
|
37
|
+
{participant.columnIndex === 0 && (
|
|
38
|
+
<line
|
|
39
|
+
x1={x - LAYOUT.participantGap / 2 + columnWidth}
|
|
40
|
+
y1={0}
|
|
41
|
+
x2={x - LAYOUT.participantGap / 2 + columnWidth}
|
|
42
|
+
y2={totalHeight}
|
|
43
|
+
className="stroke-slate-200"
|
|
44
|
+
strokeWidth={1}
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{/* Center line - dashed, subtle */}
|
|
49
|
+
<line
|
|
50
|
+
x1={centerX}
|
|
51
|
+
y1={0}
|
|
52
|
+
x2={centerX}
|
|
53
|
+
y2={totalHeight}
|
|
54
|
+
className="stroke-slate-300"
|
|
55
|
+
strokeWidth={1}
|
|
56
|
+
strokeDasharray="2 8"
|
|
57
|
+
/>
|
|
58
|
+
</g>
|
|
59
|
+
)
|
|
60
|
+
}
|