@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,740 @@
|
|
|
1
|
+
import type { ChatMessageContentItem, LLMCallLogEntry, LLMCallMessage } from '@roj-ai/sdk'
|
|
2
|
+
import { estimateTokens } from '../../lib/domain-utils.js'
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { api, getApiBaseUrl, unwrap } from '@roj-ai/client'
|
|
5
|
+
|
|
6
|
+
function isLLMCallLogEntry(data: unknown): data is LLMCallLogEntry {
|
|
7
|
+
return typeof data === 'object' && data !== null && 'id' in data && 'status' in data && 'request' in data
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface LLMCallDetailProps {
|
|
11
|
+
sessionId: string
|
|
12
|
+
callId: string
|
|
13
|
+
onClose?: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function LLMCallDetail({ sessionId, callId, onClose }: LLMCallDetailProps) {
|
|
17
|
+
const [call, setCall] = useState<LLMCallLogEntry | null>(null)
|
|
18
|
+
const [loading, setLoading] = useState(true)
|
|
19
|
+
const [error, setError] = useState<string | null>(null)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
let cancelled = false
|
|
23
|
+
|
|
24
|
+
const load = async () => {
|
|
25
|
+
try {
|
|
26
|
+
setLoading(true)
|
|
27
|
+
setError(null)
|
|
28
|
+
const data = unwrap(await api.call('llm.getCall', { sessionId, callId }))
|
|
29
|
+
if (!cancelled && isLLMCallLogEntry(data)) {
|
|
30
|
+
setCall(data)
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (!cancelled) {
|
|
34
|
+
setError(err instanceof Error ? err.message : 'Failed to load LLM call')
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
if (!cancelled) {
|
|
38
|
+
setLoading(false)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
load()
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
cancelled = true
|
|
47
|
+
}
|
|
48
|
+
}, [sessionId, callId])
|
|
49
|
+
|
|
50
|
+
if (loading) {
|
|
51
|
+
return <div className="text-slate-500 text-sm p-4">Loading...</div>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
return <div className="text-red-500 text-sm p-4">{error}</div>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!call) {
|
|
59
|
+
return <div className="text-slate-500 text-sm p-4">No data</div>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="space-y-6 text-sm">
|
|
64
|
+
{/* Header */}
|
|
65
|
+
<div className="flex items-center justify-between border-b pb-4">
|
|
66
|
+
<div className="flex items-center gap-3">
|
|
67
|
+
<StatusBadge status={call.status} />
|
|
68
|
+
<span className="font-mono font-medium text-lg">{call.request.model}</span>
|
|
69
|
+
</div>
|
|
70
|
+
{onClose && (
|
|
71
|
+
<button
|
|
72
|
+
onClick={onClose}
|
|
73
|
+
className="text-slate-400 hover:text-slate-600 text-xl"
|
|
74
|
+
>
|
|
75
|
+
×
|
|
76
|
+
</button>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Metrics Grid (4 tiles) */}
|
|
81
|
+
<MetricsGrid call={call} />
|
|
82
|
+
|
|
83
|
+
{/* Error */}
|
|
84
|
+
{call.error && (
|
|
85
|
+
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
86
|
+
<h3 className="font-semibold text-red-800 mb-2">Error</h3>
|
|
87
|
+
<div className="font-medium text-red-700">{call.error.type}</div>
|
|
88
|
+
<div className="text-red-600">{call.error.message}</div>
|
|
89
|
+
{call.error.retryAfterMs !== undefined && (
|
|
90
|
+
<div className="text-red-500 text-xs mt-1">
|
|
91
|
+
Retry after: {call.error.retryAfterMs}ms
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{/* Main Content - Two Column Layout */}
|
|
98
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
99
|
+
{/* Left: Messages */}
|
|
100
|
+
<div className="lg:col-span-2 space-y-4">
|
|
101
|
+
{/* System Prompt */}
|
|
102
|
+
<CollapsibleSection title="System Prompt" defaultOpen={false}>
|
|
103
|
+
<CollapsibleContent maxLines={10}>
|
|
104
|
+
{call.request.systemPrompt}
|
|
105
|
+
</CollapsibleContent>
|
|
106
|
+
</CollapsibleSection>
|
|
107
|
+
|
|
108
|
+
{/* Messages */}
|
|
109
|
+
<CollapsibleSection
|
|
110
|
+
title={`Messages (${call.request.messages.length})`}
|
|
111
|
+
defaultOpen={true}
|
|
112
|
+
>
|
|
113
|
+
<div className="space-y-3">
|
|
114
|
+
{call.request.messages.map((msg, idx) => (
|
|
115
|
+
<div key={idx}>
|
|
116
|
+
<MessageBlock message={msg} index={idx} sessionId={sessionId} />
|
|
117
|
+
{msg.cacheControl && <CacheBreakpointMarker />}
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
|
|
121
|
+
{/* Response */}
|
|
122
|
+
{call.response && (
|
|
123
|
+
<div className="border-t-2 border-green-300 pt-3">
|
|
124
|
+
<div className="text-xs text-green-600 font-medium mb-2">Response</div>
|
|
125
|
+
|
|
126
|
+
{/* Reasoning (orange box) */}
|
|
127
|
+
{call.response.reasoning && (
|
|
128
|
+
<div className="bg-orange-50 border border-orange-200 rounded-md p-3 mb-3">
|
|
129
|
+
<div className="text-sm font-semibold text-orange-800 mb-2">Reasoning:</div>
|
|
130
|
+
<CollapsibleContent maxLines={20}>
|
|
131
|
+
{call.response.reasoning}
|
|
132
|
+
</CollapsibleContent>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{/* Content */}
|
|
137
|
+
{call.response.content && (
|
|
138
|
+
<div className="bg-green-50 border border-green-200 rounded-md p-3 mb-3">
|
|
139
|
+
<CollapsibleContent maxLines={15}>
|
|
140
|
+
{call.response.content}
|
|
141
|
+
</CollapsibleContent>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
{/* Tool Calls */}
|
|
146
|
+
{call.response.toolCalls.length > 0 && (
|
|
147
|
+
<div className="space-y-2">
|
|
148
|
+
<div className="text-xs font-medium text-slate-600">Tool Calls:</div>
|
|
149
|
+
{call.response.toolCalls.map((tc) => (
|
|
150
|
+
<div
|
|
151
|
+
key={tc.id}
|
|
152
|
+
className="bg-cyan-50 border border-cyan-200 rounded-md p-3"
|
|
153
|
+
>
|
|
154
|
+
<div className="flex items-center gap-2 mb-2">
|
|
155
|
+
<span className="font-mono font-medium text-cyan-800">{tc.name}</span>
|
|
156
|
+
<span className="text-xs text-cyan-600 font-mono bg-cyan-100 px-2 py-0.5 rounded">
|
|
157
|
+
{tc.id.slice(0, 8)}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
<CollapsibleContent maxLines={10}>
|
|
161
|
+
{JSON.stringify(tc.input, null, 2)}
|
|
162
|
+
</CollapsibleContent>
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{/* Finish Reason */}
|
|
169
|
+
<div className="mt-2 flex items-center gap-2">
|
|
170
|
+
<span className="text-xs text-slate-500">Finish reason:</span>
|
|
171
|
+
<FinishReasonBadge reason={call.response.finishReason} />
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
</CollapsibleSection>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Right: Sidebar */}
|
|
180
|
+
<div className="space-y-4">
|
|
181
|
+
{/* Token Usage Details */}
|
|
182
|
+
{call.metrics && (
|
|
183
|
+
<SidebarPanel title="Token Details">
|
|
184
|
+
<div className="space-y-2">
|
|
185
|
+
<MetricRow label="Prompt" value={call.metrics.promptTokens.toLocaleString()} color="text-green-600" />
|
|
186
|
+
<MetricRow label="Completion" value={call.metrics.completionTokens.toLocaleString()} color="text-violet-600" />
|
|
187
|
+
{call.metrics.reasoningTokens !== undefined && (
|
|
188
|
+
<MetricRow label="Reasoning" value={call.metrics.reasoningTokens.toLocaleString()} color="text-purple-600" />
|
|
189
|
+
)}
|
|
190
|
+
{call.metrics.cachedTokens !== undefined && (
|
|
191
|
+
<MetricRow label="Cached" value={call.metrics.cachedTokens.toLocaleString()} color="text-slate-500" />
|
|
192
|
+
)}
|
|
193
|
+
<div className="border-t pt-2">
|
|
194
|
+
<MetricRow label="Total" value={call.metrics.totalTokens.toLocaleString()} color="text-slate-900" bold />
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</SidebarPanel>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{/* Tools */}
|
|
201
|
+
{call.request.tools && call.request.tools.length > 0 && (
|
|
202
|
+
<SidebarPanel title={`Tools (${call.request.toolsCount})`}>
|
|
203
|
+
<div className="space-y-2 max-h-96 overflow-y-auto">
|
|
204
|
+
{call.request.tools.map((tool) => (
|
|
205
|
+
<details key={tool.name} className="bg-slate-50 rounded border">
|
|
206
|
+
<summary className="p-2 cursor-pointer hover:bg-slate-100 text-xs">
|
|
207
|
+
<span className="font-mono font-medium">{tool.name}</span>
|
|
208
|
+
</summary>
|
|
209
|
+
<div className="p-2 border-t text-xs space-y-2">
|
|
210
|
+
<div className="text-slate-600">{tool.description}</div>
|
|
211
|
+
{tool.parameters && (
|
|
212
|
+
<details className="bg-white rounded border">
|
|
213
|
+
<summary className="p-2 cursor-pointer hover:bg-slate-50 text-xs font-medium text-violet-600">
|
|
214
|
+
JSON Schema
|
|
215
|
+
</summary>
|
|
216
|
+
<div className="p-2 border-t">
|
|
217
|
+
<pre className="text-xs font-mono text-slate-700 whitespace-pre-wrap overflow-x-auto max-h-64 overflow-y-auto">
|
|
218
|
+
{JSON.stringify(tool.parameters, null, 2)}
|
|
219
|
+
</pre>
|
|
220
|
+
</div>
|
|
221
|
+
</details>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
</details>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
</SidebarPanel>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{/* Request Info */}
|
|
231
|
+
<SidebarPanel title="Request Info">
|
|
232
|
+
<div className="space-y-2 text-xs">
|
|
233
|
+
<div>
|
|
234
|
+
<span className="text-slate-500">Created:</span> <span className="font-mono">{new Date(call.createdAt).toLocaleString()}</span>
|
|
235
|
+
</div>
|
|
236
|
+
{call.completedAt && (
|
|
237
|
+
<div>
|
|
238
|
+
<span className="text-slate-500">Completed:</span> <span className="font-mono">{new Date(call.completedAt).toLocaleString()}</span>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
<div>
|
|
242
|
+
<span className="text-slate-500">Agent:</span> <span className="font-mono">{call.agentId.slice(0, 12)}...</span>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</SidebarPanel>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// Collapsible Content Component
|
|
254
|
+
// ============================================================================
|
|
255
|
+
|
|
256
|
+
function CollapsibleContent({ children, maxLines = 10 }: { children: string; maxLines?: number }) {
|
|
257
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
258
|
+
|
|
259
|
+
const { lines, shouldCollapse, tokenCount } = useMemo(() => {
|
|
260
|
+
const content = children
|
|
261
|
+
const lines = content.split('\n')
|
|
262
|
+
return {
|
|
263
|
+
lines,
|
|
264
|
+
shouldCollapse: lines.length > maxLines,
|
|
265
|
+
tokenCount: estimateTokens(content),
|
|
266
|
+
}
|
|
267
|
+
}, [children, maxLines])
|
|
268
|
+
|
|
269
|
+
if (!shouldCollapse) {
|
|
270
|
+
return (
|
|
271
|
+
<div className="space-y-2">
|
|
272
|
+
<pre className="font-mono text-xs whitespace-pre-wrap break-words">{children}</pre>
|
|
273
|
+
<div className="flex justify-end">
|
|
274
|
+
<TokenCountBadge count={tokenCount} />
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div className="space-y-2">
|
|
282
|
+
<div className={`relative ${!isExpanded ? 'max-h-64 overflow-hidden' : ''}`}>
|
|
283
|
+
<pre className="font-mono text-xs whitespace-pre-wrap break-words">
|
|
284
|
+
{isExpanded ? children : lines.slice(0, maxLines).join('\n')}
|
|
285
|
+
</pre>
|
|
286
|
+
{!isExpanded && <div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-white to-transparent pointer-events-none" />}
|
|
287
|
+
</div>
|
|
288
|
+
<div className="flex items-center justify-between">
|
|
289
|
+
<button
|
|
290
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
291
|
+
className="px-3 py-1 text-xs bg-violet-100 hover:bg-violet-200 text-violet-800 rounded transition-colors"
|
|
292
|
+
>
|
|
293
|
+
{isExpanded ? 'Show less' : `Show all (${lines.length} lines)`}
|
|
294
|
+
</button>
|
|
295
|
+
<TokenCountBadge count={tokenCount} />
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ============================================================================
|
|
302
|
+
// Helper Components
|
|
303
|
+
// ============================================================================
|
|
304
|
+
|
|
305
|
+
function CollapsibleSection({
|
|
306
|
+
title,
|
|
307
|
+
defaultOpen,
|
|
308
|
+
children,
|
|
309
|
+
}: {
|
|
310
|
+
title: string
|
|
311
|
+
defaultOpen: boolean
|
|
312
|
+
children: React.ReactNode
|
|
313
|
+
}) {
|
|
314
|
+
const [open, setOpen] = useState(defaultOpen)
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<div className="border border-slate-200 rounded-md overflow-hidden">
|
|
318
|
+
<button
|
|
319
|
+
onClick={() => setOpen(!open)}
|
|
320
|
+
className="w-full px-4 py-3 text-left flex items-center justify-between bg-slate-50 hover:bg-slate-100 transition-colors"
|
|
321
|
+
>
|
|
322
|
+
<span className="font-medium text-slate-700">{title}</span>
|
|
323
|
+
<span className="text-slate-400 text-lg">{open ? '−' : '+'}</span>
|
|
324
|
+
</button>
|
|
325
|
+
{open && <div className="p-4 border-t border-slate-200">{children}</div>}
|
|
326
|
+
</div>
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Convert a file:// URL to the session file proxy endpoint.
|
|
332
|
+
* Handles both real paths (file:///.../.roj/data/sessions/{id}/foo.png)
|
|
333
|
+
* and virtual/sandboxed paths (file:///home/user/session/foo.png).
|
|
334
|
+
*/
|
|
335
|
+
function fileUrlToProxyUrl(fileUrl: string, sessionId: string): string {
|
|
336
|
+
const baseUrl = getApiBaseUrl()
|
|
337
|
+
|
|
338
|
+
// Try real path: extract after /sessions/{sessionId}/
|
|
339
|
+
const sessionMarker = `/sessions/${sessionId}/`
|
|
340
|
+
const idx = fileUrl.indexOf(sessionMarker)
|
|
341
|
+
if (idx !== -1) {
|
|
342
|
+
const relativePath = fileUrl.slice(idx + sessionMarker.length)
|
|
343
|
+
return `${baseUrl}/sessions/${sessionId}/files/${relativePath}`
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Try virtual/sandboxed path: /home/user/session/...
|
|
347
|
+
const virtualPrefix = 'file:///home/user/session/'
|
|
348
|
+
if (fileUrl.startsWith(virtualPrefix)) {
|
|
349
|
+
const relativePath = fileUrl.slice(virtualPrefix.length)
|
|
350
|
+
return `${baseUrl}/sessions/${sessionId}/files/${relativePath}`
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return fileUrl
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function MessageContentItems({ items, sessionId }: { items: ChatMessageContentItem[]; sessionId: string }) {
|
|
357
|
+
const textContent = items.filter((it): it is ChatMessageContentItem & { type: 'text' } => it.type === 'text').map((it) => it.text).join('\n')
|
|
358
|
+
const images = items.filter((it): it is ChatMessageContentItem & { type: 'image_url' } => it.type === 'image_url')
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<>
|
|
362
|
+
{textContent && <CollapsibleContent maxLines={10}>{textContent}</CollapsibleContent>}
|
|
363
|
+
{images.length > 0 && (
|
|
364
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
365
|
+
{images.map((img, i) => (
|
|
366
|
+
<a key={i} href={fileUrlToProxyUrl(img.imageUrl.url, sessionId)} target="_blank" rel="noopener noreferrer">
|
|
367
|
+
<img
|
|
368
|
+
src={fileUrlToProxyUrl(img.imageUrl.url, sessionId)}
|
|
369
|
+
alt="Tool result image"
|
|
370
|
+
className="max-w-64 max-h-48 rounded border border-slate-300 object-contain"
|
|
371
|
+
/>
|
|
372
|
+
</a>
|
|
373
|
+
))}
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
</>
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function MessageBlock({ message, index, sessionId }: { message: LLMCallMessage; index: number; sessionId: string }) {
|
|
381
|
+
const roleColors: Record<string, string> = {
|
|
382
|
+
user: 'bg-blue-50 border-blue-200',
|
|
383
|
+
assistant: 'bg-green-50 border-green-200',
|
|
384
|
+
tool: 'bg-purple-50 border-purple-200',
|
|
385
|
+
system: 'bg-yellow-50 border-yellow-200',
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<div className={`p-3 rounded-md border ${roleColors[message.role] || 'bg-slate-50 border-slate-200'}`}>
|
|
390
|
+
<div className="flex items-center gap-2 mb-2">
|
|
391
|
+
<RoleBadge role={message.role} />
|
|
392
|
+
<span className="text-xs text-slate-500 font-mono">#{index + 1}</span>
|
|
393
|
+
{message.toolCallId && (
|
|
394
|
+
<span className="text-xs text-slate-500 font-mono bg-slate-100 px-1 rounded">
|
|
395
|
+
{message.toolCallId.slice(0, 8)}
|
|
396
|
+
</span>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
{/* Reasoning for assistant messages */}
|
|
401
|
+
{message.reasoning && (
|
|
402
|
+
<div className="bg-orange-50 border border-orange-200 rounded p-2 mb-2">
|
|
403
|
+
<div className="text-xs font-semibold text-orange-800 mb-1">Reasoning:</div>
|
|
404
|
+
<CollapsibleContent maxLines={10}>{message.reasoning}</CollapsibleContent>
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
|
|
408
|
+
{/* Content */}
|
|
409
|
+
{typeof message.content === 'string'
|
|
410
|
+
? <CollapsibleContent maxLines={10}>{message.content}</CollapsibleContent>
|
|
411
|
+
: <MessageContentItems items={message.content} sessionId={sessionId} />}
|
|
412
|
+
|
|
413
|
+
{/* Tool Calls */}
|
|
414
|
+
{message.toolCalls && message.toolCalls.length > 0 && (
|
|
415
|
+
<div className="mt-3 space-y-2">
|
|
416
|
+
<div className="text-xs font-medium text-slate-600">Tool Calls:</div>
|
|
417
|
+
{message.toolCalls.map((tc) => (
|
|
418
|
+
<div key={tc.id} className="bg-white rounded border p-2">
|
|
419
|
+
<div className="flex items-center gap-2 mb-1">
|
|
420
|
+
<span className="font-mono text-xs font-medium">{tc.name}</span>
|
|
421
|
+
<span className="text-xs text-slate-500 font-mono">{tc.id.slice(0, 8)}</span>
|
|
422
|
+
</div>
|
|
423
|
+
<pre className="text-xs text-slate-600 overflow-x-auto">
|
|
424
|
+
{JSON.stringify(tc.input, null, 2)}
|
|
425
|
+
</pre>
|
|
426
|
+
</div>
|
|
427
|
+
))}
|
|
428
|
+
</div>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function MetricTile({
|
|
435
|
+
icon,
|
|
436
|
+
iconBg,
|
|
437
|
+
iconColor,
|
|
438
|
+
title,
|
|
439
|
+
children,
|
|
440
|
+
}: {
|
|
441
|
+
icon: React.ReactNode
|
|
442
|
+
iconBg: string
|
|
443
|
+
iconColor: string
|
|
444
|
+
title: string
|
|
445
|
+
children: React.ReactNode
|
|
446
|
+
}) {
|
|
447
|
+
return (
|
|
448
|
+
<div className="bg-white rounded-md border border-slate-200 p-4">
|
|
449
|
+
<div className="flex items-center gap-2 mb-3">
|
|
450
|
+
<div className={`w-8 h-8 ${iconBg} rounded-full flex items-center justify-center`}>
|
|
451
|
+
<span className={iconColor}>{icon}</span>
|
|
452
|
+
</div>
|
|
453
|
+
<h3 className="text-xs font-medium text-slate-500">{title}</h3>
|
|
454
|
+
</div>
|
|
455
|
+
{children}
|
|
456
|
+
</div>
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function SidebarPanel({ title, children }: { title: string; children: React.ReactNode }) {
|
|
461
|
+
return (
|
|
462
|
+
<div className="bg-white rounded-md border border-slate-200">
|
|
463
|
+
<div className="px-4 py-2 border-b border-slate-200 bg-slate-50">
|
|
464
|
+
<h3 className="text-xs font-semibold text-slate-700">{title}</h3>
|
|
465
|
+
</div>
|
|
466
|
+
<div className="px-4 py-3">{children}</div>
|
|
467
|
+
</div>
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function MetricRow({
|
|
472
|
+
label,
|
|
473
|
+
value,
|
|
474
|
+
color,
|
|
475
|
+
bold,
|
|
476
|
+
}: {
|
|
477
|
+
label: string
|
|
478
|
+
value: string
|
|
479
|
+
color?: string
|
|
480
|
+
bold?: boolean
|
|
481
|
+
}) {
|
|
482
|
+
return (
|
|
483
|
+
<div className="flex justify-between text-xs">
|
|
484
|
+
<span className="text-slate-500">{label}</span>
|
|
485
|
+
<span className={`font-mono ${color || ''} ${bold ? 'font-bold' : 'font-semibold'}`}>{value}</span>
|
|
486
|
+
</div>
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function TokenCountBadge({ count }: { count: number }) {
|
|
491
|
+
return (
|
|
492
|
+
<span className="text-xs text-violet-500 bg-violet-50 px-2 py-1 rounded-full font-mono">
|
|
493
|
+
~{count.toLocaleString()} tokens
|
|
494
|
+
</span>
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function StatusBadge({ status }: { status: 'running' | 'success' | 'error' }) {
|
|
499
|
+
const colors: Record<string, string> = {
|
|
500
|
+
running: 'bg-yellow-100 text-yellow-700',
|
|
501
|
+
success: 'bg-green-100 text-green-700',
|
|
502
|
+
error: 'bg-red-100 text-red-700',
|
|
503
|
+
}
|
|
504
|
+
return (
|
|
505
|
+
<span className={`text-xs px-2 py-0.5 rounded font-medium ${colors[status]}`}>
|
|
506
|
+
{status}
|
|
507
|
+
</span>
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function CacheBreakpointMarker() {
|
|
512
|
+
return (
|
|
513
|
+
<div className="flex items-center gap-2 my-2" title="Prompt cache breakpoint — everything above is eligible for caching">
|
|
514
|
+
<div className="flex-1 border-t border-dashed border-amber-400" />
|
|
515
|
+
<span className="text-[10px] font-mono font-semibold text-amber-700 bg-amber-50 border border-amber-300 px-2 py-0.5 rounded uppercase tracking-wider">
|
|
516
|
+
Cache Breakpoint
|
|
517
|
+
</span>
|
|
518
|
+
<div className="flex-1 border-t border-dashed border-amber-400" />
|
|
519
|
+
</div>
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function CacheBadge({ status }: { status: 'hit' | 'miss' | 'none' }) {
|
|
524
|
+
const colors: Record<string, string> = {
|
|
525
|
+
hit: 'text-green-600',
|
|
526
|
+
miss: 'text-orange-600',
|
|
527
|
+
none: 'text-slate-500',
|
|
528
|
+
}
|
|
529
|
+
const labels: Record<string, string> = {
|
|
530
|
+
hit: 'CACHE HIT',
|
|
531
|
+
miss: 'CACHE MISS',
|
|
532
|
+
none: 'NO CACHE',
|
|
533
|
+
}
|
|
534
|
+
return (
|
|
535
|
+
<span className={`text-xs font-mono font-semibold ${colors[status]}`}>
|
|
536
|
+
{labels[status]}
|
|
537
|
+
</span>
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function RoleBadge({ role }: { role: string }) {
|
|
542
|
+
const colors: Record<string, string> = {
|
|
543
|
+
user: 'bg-blue-100 text-blue-700',
|
|
544
|
+
assistant: 'bg-green-100 text-green-700',
|
|
545
|
+
tool: 'bg-purple-100 text-purple-700',
|
|
546
|
+
system: 'bg-yellow-100 text-yellow-700',
|
|
547
|
+
}
|
|
548
|
+
return (
|
|
549
|
+
<span className={`text-xs px-2 py-0.5 rounded font-medium uppercase ${colors[role] || 'bg-slate-100 text-slate-700'}`}>
|
|
550
|
+
{role}
|
|
551
|
+
</span>
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function FinishReasonBadge({ reason }: { reason: string }) {
|
|
556
|
+
const colors: Record<string, string> = {
|
|
557
|
+
stop: 'bg-green-100 text-green-700',
|
|
558
|
+
tool_calls: 'bg-cyan-100 text-cyan-700',
|
|
559
|
+
length: 'bg-yellow-100 text-yellow-700',
|
|
560
|
+
error: 'bg-red-100 text-red-700',
|
|
561
|
+
}
|
|
562
|
+
return (
|
|
563
|
+
<span className={`text-xs px-2 py-0.5 rounded font-medium ${colors[reason] || 'bg-slate-100 text-slate-700'}`}>
|
|
564
|
+
{reason}
|
|
565
|
+
</span>
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ============================================================================
|
|
570
|
+
// Icons (simple SVG)
|
|
571
|
+
// ============================================================================
|
|
572
|
+
|
|
573
|
+
function DollarIcon() {
|
|
574
|
+
return (
|
|
575
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
576
|
+
<path
|
|
577
|
+
strokeLinecap="round"
|
|
578
|
+
strokeLinejoin="round"
|
|
579
|
+
strokeWidth={2}
|
|
580
|
+
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
581
|
+
/>
|
|
582
|
+
</svg>
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function TokenIcon() {
|
|
587
|
+
return (
|
|
588
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
589
|
+
<path
|
|
590
|
+
strokeLinecap="round"
|
|
591
|
+
strokeLinejoin="round"
|
|
592
|
+
strokeWidth={2}
|
|
593
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
594
|
+
/>
|
|
595
|
+
</svg>
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function PerformanceIcon() {
|
|
600
|
+
return (
|
|
601
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
602
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
603
|
+
</svg>
|
|
604
|
+
)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function ModelIcon() {
|
|
608
|
+
return (
|
|
609
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
610
|
+
<path
|
|
611
|
+
strokeLinecap="round"
|
|
612
|
+
strokeLinejoin="round"
|
|
613
|
+
strokeWidth={2}
|
|
614
|
+
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
615
|
+
/>
|
|
616
|
+
</svg>
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ============================================================================
|
|
621
|
+
// Metrics Grid Component
|
|
622
|
+
// ============================================================================
|
|
623
|
+
|
|
624
|
+
function MetricsGrid({ call }: { call: LLMCallLogEntry }) {
|
|
625
|
+
const metrics = call.metrics
|
|
626
|
+
|
|
627
|
+
const totalCost = metrics?.cost
|
|
628
|
+
const promptTokens = metrics?.promptTokens
|
|
629
|
+
const completionTokens = metrics?.completionTokens
|
|
630
|
+
const reasoningTokens = metrics?.reasoningTokens
|
|
631
|
+
const cachedTokens = metrics?.cachedTokens
|
|
632
|
+
const latency = metrics?.latencyMs
|
|
633
|
+
const generationTime = metrics?.generationTimeMs
|
|
634
|
+
const provider = metrics?.provider
|
|
635
|
+
|
|
636
|
+
// Calculate total tokens
|
|
637
|
+
const totalTokens = (promptTokens ?? 0) + (completionTokens ?? 0)
|
|
638
|
+
|
|
639
|
+
// Calculate effective rate (tokens/sec)
|
|
640
|
+
const tokensPerSecond = generationTime && completionTokens
|
|
641
|
+
? ((completionTokens / generationTime) * 1000).toFixed(1)
|
|
642
|
+
: null
|
|
643
|
+
|
|
644
|
+
return (
|
|
645
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
646
|
+
{/* Cost Tile */}
|
|
647
|
+
<MetricTile
|
|
648
|
+
icon={<DollarIcon />}
|
|
649
|
+
iconBg="bg-green-100"
|
|
650
|
+
iconColor="text-green-600"
|
|
651
|
+
title="Cost"
|
|
652
|
+
>
|
|
653
|
+
<div className="text-2xl font-bold text-slate-900">
|
|
654
|
+
{totalCost !== undefined ? `$${totalCost.toFixed(6)}` : '—'}
|
|
655
|
+
</div>
|
|
656
|
+
</MetricTile>
|
|
657
|
+
|
|
658
|
+
{/* Tokens Tile */}
|
|
659
|
+
<MetricTile
|
|
660
|
+
icon={<TokenIcon />}
|
|
661
|
+
iconBg="bg-violet-100"
|
|
662
|
+
iconColor="text-violet-600"
|
|
663
|
+
title="Tokens"
|
|
664
|
+
>
|
|
665
|
+
<div className="text-2xl font-bold text-slate-900">
|
|
666
|
+
{totalTokens.toLocaleString()}
|
|
667
|
+
</div>
|
|
668
|
+
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs mt-1">
|
|
669
|
+
<span className="text-green-600">
|
|
670
|
+
<span className="text-slate-400">In:</span> {(promptTokens ?? 0).toLocaleString()}
|
|
671
|
+
</span>
|
|
672
|
+
<span className="text-violet-600">
|
|
673
|
+
<span className="text-slate-400">Out:</span> {(completionTokens ?? 0).toLocaleString()}
|
|
674
|
+
</span>
|
|
675
|
+
{reasoningTokens !== null && reasoningTokens !== undefined && reasoningTokens > 0 && (
|
|
676
|
+
<span className="text-purple-600">
|
|
677
|
+
<span className="text-slate-400">Reason:</span> {reasoningTokens.toLocaleString()}
|
|
678
|
+
</span>
|
|
679
|
+
)}
|
|
680
|
+
</div>
|
|
681
|
+
{cachedTokens !== null && cachedTokens !== undefined && (
|
|
682
|
+
<div className="mt-2">
|
|
683
|
+
<CacheBadge status={cachedTokens > 0 ? 'hit' : 'miss'} />
|
|
684
|
+
{cachedTokens > 0 && (
|
|
685
|
+
<span className="text-xs text-slate-500 ml-2">
|
|
686
|
+
({cachedTokens.toLocaleString()} cached)
|
|
687
|
+
</span>
|
|
688
|
+
)}
|
|
689
|
+
</div>
|
|
690
|
+
)}
|
|
691
|
+
</MetricTile>
|
|
692
|
+
|
|
693
|
+
{/* Performance Tile */}
|
|
694
|
+
<MetricTile
|
|
695
|
+
icon={<PerformanceIcon />}
|
|
696
|
+
iconBg="bg-amber-100"
|
|
697
|
+
iconColor="text-amber-600"
|
|
698
|
+
title="Performance"
|
|
699
|
+
>
|
|
700
|
+
<div className="text-2xl font-bold text-slate-900">
|
|
701
|
+
{latency !== undefined && latency !== null ? `${(latency / 1000).toFixed(2)}s` : '—'}
|
|
702
|
+
</div>
|
|
703
|
+
<div className="space-y-1 text-xs mt-1">
|
|
704
|
+
{generationTime !== null && generationTime !== undefined && (
|
|
705
|
+
<div className="text-slate-600">
|
|
706
|
+
<span className="text-slate-400">Generation:</span> {(generationTime / 1000).toFixed(2)}s
|
|
707
|
+
</div>
|
|
708
|
+
)}
|
|
709
|
+
{tokensPerSecond && (
|
|
710
|
+
<div className="text-purple-600 font-medium">
|
|
711
|
+
{tokensPerSecond} tok/s
|
|
712
|
+
</div>
|
|
713
|
+
)}
|
|
714
|
+
</div>
|
|
715
|
+
</MetricTile>
|
|
716
|
+
|
|
717
|
+
{/* Model & Provider Tile */}
|
|
718
|
+
<MetricTile
|
|
719
|
+
icon={<ModelIcon />}
|
|
720
|
+
iconBg="bg-purple-100"
|
|
721
|
+
iconColor="text-purple-600"
|
|
722
|
+
title="Model"
|
|
723
|
+
>
|
|
724
|
+
<div className="font-mono text-sm font-medium text-slate-900 truncate" title={call.request.model}>
|
|
725
|
+
{call.request.model.split('/').pop()}
|
|
726
|
+
</div>
|
|
727
|
+
{provider && (
|
|
728
|
+
<div className="text-xs text-slate-500 mt-1">
|
|
729
|
+
<span className="text-slate-400">via</span> {provider}
|
|
730
|
+
</div>
|
|
731
|
+
)}
|
|
732
|
+
{call.response?.finishReason && (
|
|
733
|
+
<div className="mt-2">
|
|
734
|
+
<FinishReasonBadge reason={call.response.finishReason} />
|
|
735
|
+
</div>
|
|
736
|
+
)}
|
|
737
|
+
</MetricTile>
|
|
738
|
+
</div>
|
|
739
|
+
)
|
|
740
|
+
}
|