@ai-sdk/devtools 1.0.0-beta.9 → 1.0.0-canary.20
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/client/assets/index-B1quREL_.css +1 -0
- package/dist/client/assets/index-DYruJ8X0.js +181 -0
- package/dist/client/index.html +2 -2
- package/dist/index.d.ts +19 -1
- package/dist/index.js +342 -3
- package/dist/viewer/server.js +42 -5
- package/package.json +28 -4
- package/src/db.ts +32 -5
- package/src/index.ts +1 -0
- package/src/integration.ts +509 -0
- package/src/middleware.ts +5 -5
- package/src/viewer/client/app.tsx +97 -1878
- package/src/viewer/client/components/message-components.tsx +342 -0
- package/src/viewer/client/components/output-components.tsx +145 -0
- package/src/viewer/client/components/shared-components.tsx +691 -0
- package/src/viewer/client/components/step-card.tsx +472 -0
- package/src/viewer/client/components/trace-timeline.tsx +529 -0
- package/src/viewer/client/components/ui/badge.tsx +0 -1
- package/src/viewer/client/components/ui/button.tsx +0 -1
- package/src/viewer/client/types.ts +183 -0
- package/src/viewer/client/utils.ts +708 -0
- package/src/viewer/server.ts +49 -5
- package/dist/client/assets/index-BkyOIjbt.css +0 -1
- package/dist/client/assets/index-BwqXGbSV.js +0 -176
|
@@ -1,287 +1,48 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
ChevronRight,
|
|
4
3
|
RefreshCw,
|
|
5
4
|
Trash2,
|
|
6
|
-
Copy,
|
|
7
|
-
Check,
|
|
8
5
|
Zap,
|
|
9
|
-
Wrench,
|
|
10
6
|
MessageSquare,
|
|
11
7
|
AlertCircle,
|
|
12
|
-
ChevronDown,
|
|
13
|
-
Settings,
|
|
14
|
-
Brain,
|
|
15
8
|
Loader2,
|
|
16
|
-
|
|
9
|
+
GanttChart,
|
|
17
10
|
} from 'lucide-react';
|
|
18
11
|
import { AISDKLogo } from '@/components/icons';
|
|
19
12
|
import { Button } from '@/components/ui/button';
|
|
20
13
|
import { Badge } from '@/components/ui/badge';
|
|
21
|
-
import { Card } from '@/components/ui/card';
|
|
22
14
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
23
|
-
import {
|
|
24
|
-
Collapsible,
|
|
25
|
-
CollapsibleContent,
|
|
26
|
-
CollapsibleTrigger,
|
|
27
|
-
} from '@/components/ui/collapsible';
|
|
28
|
-
import {
|
|
29
|
-
Drawer,
|
|
30
|
-
DrawerContent,
|
|
31
|
-
DrawerHeader,
|
|
32
|
-
DrawerTitle,
|
|
33
|
-
DrawerTrigger,
|
|
34
|
-
} from '@/components/ui/drawer';
|
|
35
15
|
import {
|
|
36
16
|
Tooltip,
|
|
37
17
|
TooltipContent,
|
|
38
18
|
TooltipTrigger,
|
|
39
19
|
} from '@/components/ui/tooltip';
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
duration_ms: number | null;
|
|
60
|
-
input: string;
|
|
61
|
-
output: string | null;
|
|
62
|
-
usage: string | null;
|
|
63
|
-
error: string | null;
|
|
64
|
-
raw_request: string | null;
|
|
65
|
-
raw_response: string | null;
|
|
66
|
-
raw_chunks: string | null;
|
|
67
|
-
provider_options: string | null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface RunDetail {
|
|
71
|
-
run: { id: string; started_at: string; isInProgress?: boolean };
|
|
72
|
-
steps: Step[];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
type StepType = 'tool-calls' | 'response' | 'error';
|
|
76
|
-
|
|
77
|
-
interface StepSummary {
|
|
78
|
-
type: StepType;
|
|
79
|
-
icon: 'wrench' | 'message' | 'alert';
|
|
80
|
-
label: string;
|
|
81
|
-
toolDetails?: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function summarizeToolCalls(toolCalls: any[]): {
|
|
85
|
-
label: string;
|
|
86
|
-
details: string;
|
|
87
|
-
} {
|
|
88
|
-
// Count occurrences of each tool
|
|
89
|
-
const counts = toolCalls.reduce((acc: Record<string, number>, call: any) => {
|
|
90
|
-
acc[call.toolName] = (acc[call.toolName] || 0) + 1;
|
|
91
|
-
return acc;
|
|
92
|
-
}, {});
|
|
93
|
-
|
|
94
|
-
const uniqueTools = Object.keys(counts);
|
|
95
|
-
|
|
96
|
-
// Format tool name with count if > 1
|
|
97
|
-
const formatTool = (name: string) => {
|
|
98
|
-
const count = counts[name] ?? 0;
|
|
99
|
-
return count > 1 ? `${name} (x${count})` : name;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const allToolsFormatted = uniqueTools.map(formatTool);
|
|
103
|
-
|
|
104
|
-
// Single tool type
|
|
105
|
-
if (uniqueTools.length === 1 && uniqueTools[0]) {
|
|
106
|
-
return {
|
|
107
|
-
label: formatTool(uniqueTools[0]),
|
|
108
|
-
details: '', // No tooltip needed
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Two tools - show both
|
|
113
|
-
if (uniqueTools.length === 2) {
|
|
114
|
-
return {
|
|
115
|
-
label: `${formatTool(uniqueTools[0])}, ${formatTool(uniqueTools[1])}`,
|
|
116
|
-
details: allToolsFormatted.join(', '),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 3+ tools: show first two + ellipsis
|
|
121
|
-
return {
|
|
122
|
-
label: `${formatTool(uniqueTools[0])}, ${formatTool(uniqueTools[1])}, ...`,
|
|
123
|
-
details: allToolsFormatted.join(', '),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
interface StepInputSummary {
|
|
128
|
-
type: 'user' | 'tool';
|
|
129
|
-
label: string;
|
|
130
|
-
fullText?: string; // For tooltip on user messages
|
|
131
|
-
toolDetails?: string; // For tooltip on tool results
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function getStepInputSummary(
|
|
135
|
-
input: any,
|
|
136
|
-
isFirstStep: boolean,
|
|
137
|
-
): StepInputSummary | null {
|
|
138
|
-
const prompt = input?.prompt;
|
|
139
|
-
if (!Array.isArray(prompt)) return null;
|
|
140
|
-
|
|
141
|
-
// For first step: always show the last user message (what kicked off this run)
|
|
142
|
-
if (isFirstStep) {
|
|
143
|
-
const userMessages = prompt.filter((msg: any) => msg.role === 'user');
|
|
144
|
-
const lastUserMessage = userMessages[userMessages.length - 1];
|
|
145
|
-
|
|
146
|
-
if (lastUserMessage) {
|
|
147
|
-
const content = lastUserMessage.content;
|
|
148
|
-
let text: string | null = null;
|
|
149
|
-
|
|
150
|
-
if (typeof content === 'string') {
|
|
151
|
-
text = content;
|
|
152
|
-
} else if (Array.isArray(content)) {
|
|
153
|
-
const textPart = content.find((part: any) => part.type === 'text');
|
|
154
|
-
if (textPart?.text) {
|
|
155
|
-
text = textPart.text;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (text) {
|
|
160
|
-
return {
|
|
161
|
-
type: 'user',
|
|
162
|
-
label: `"${truncateText(text)}"`,
|
|
163
|
-
fullText: text,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// For subsequent steps: show the last tool results
|
|
171
|
-
const toolMessages = prompt.filter((msg: any) => msg.role === 'tool');
|
|
172
|
-
if (toolMessages.length > 0) {
|
|
173
|
-
// Only get tool names from the last tool message (the most recent tool results)
|
|
174
|
-
const lastToolMessage = toolMessages[toolMessages.length - 1];
|
|
175
|
-
const toolCounts: Record<string, number> = {};
|
|
176
|
-
|
|
177
|
-
const content = lastToolMessage.content;
|
|
178
|
-
if (Array.isArray(content)) {
|
|
179
|
-
for (const part of content) {
|
|
180
|
-
if (part.type === 'tool-result' && part.toolName) {
|
|
181
|
-
toolCounts[part.toolName] = (toolCounts[part.toolName] || 0) + 1;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const uniqueTools = Object.keys(toolCounts);
|
|
187
|
-
if (uniqueTools.length === 0) {
|
|
188
|
-
return { type: 'tool', label: 'tool result' };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Format tool name with count if > 1
|
|
192
|
-
const formatTool = (name: string) => {
|
|
193
|
-
const count = toolCounts[name];
|
|
194
|
-
return count > 1 ? `${name} (x${count})` : name;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const allToolsFormatted = uniqueTools.map(formatTool);
|
|
198
|
-
|
|
199
|
-
if (uniqueTools.length === 1) {
|
|
200
|
-
return { type: 'tool', label: formatTool(uniqueTools[0]) };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (uniqueTools.length === 2) {
|
|
204
|
-
return {
|
|
205
|
-
type: 'tool',
|
|
206
|
-
label: `${formatTool(uniqueTools[0])}, ${formatTool(uniqueTools[1])}`,
|
|
207
|
-
toolDetails: allToolsFormatted.join(', '),
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// 3+ tools: show first two + ellipsis
|
|
212
|
-
return {
|
|
213
|
-
type: 'tool',
|
|
214
|
-
label: `${formatTool(uniqueTools[0])}, ${formatTool(uniqueTools[1])}, ...`,
|
|
215
|
-
toolDetails: allToolsFormatted.join(', '),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Fall back to last user message
|
|
220
|
-
const userMessages = prompt.filter((msg: any) => msg.role === 'user');
|
|
221
|
-
const lastUserMessage = userMessages[userMessages.length - 1];
|
|
222
|
-
|
|
223
|
-
if (!lastUserMessage) return null;
|
|
224
|
-
|
|
225
|
-
// Extract text content from the message
|
|
226
|
-
const content = lastUserMessage.content;
|
|
227
|
-
let text: string | null = null;
|
|
228
|
-
|
|
229
|
-
if (typeof content === 'string') {
|
|
230
|
-
text = content;
|
|
231
|
-
} else if (Array.isArray(content)) {
|
|
232
|
-
const textPart = content.find((part: any) => part.type === 'text');
|
|
233
|
-
if (textPart?.text) {
|
|
234
|
-
text = textPart.text;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (!text) return null;
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
type: 'user',
|
|
242
|
-
label: `"${truncateText(text)}"`,
|
|
243
|
-
fullText: text,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function truncateText(text: string, maxLength: number = 30): string {
|
|
248
|
-
if (text.length <= maxLength) return text;
|
|
249
|
-
return text.slice(0, maxLength).trim() + '…';
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function getStepSummary(output: any, error: string | null): StepSummary {
|
|
253
|
-
if (error) {
|
|
254
|
-
return { type: 'error', icon: 'alert', label: 'Error' };
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// finishReason can be a string or an object with {unified, raw} properties
|
|
258
|
-
const finishReason =
|
|
259
|
-
typeof output?.finishReason === 'string'
|
|
260
|
-
? output.finishReason
|
|
261
|
-
: output?.finishReason?.unified;
|
|
262
|
-
|
|
263
|
-
if (finishReason === 'tool-calls') {
|
|
264
|
-
const toolCalls =
|
|
265
|
-
output?.toolCalls ||
|
|
266
|
-
output?.content?.filter((p: any) => p.type === 'tool-call') ||
|
|
267
|
-
[];
|
|
268
|
-
const { label, details } = summarizeToolCalls(toolCalls);
|
|
269
|
-
return {
|
|
270
|
-
type: 'tool-calls',
|
|
271
|
-
icon: 'wrench',
|
|
272
|
-
label,
|
|
273
|
-
toolDetails: details || undefined,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return { type: 'response', icon: 'message', label: 'Response' };
|
|
278
|
-
}
|
|
20
|
+
import type {
|
|
21
|
+
Run,
|
|
22
|
+
RunDetail,
|
|
23
|
+
Step,
|
|
24
|
+
ParsedInput,
|
|
25
|
+
ParsedUsage,
|
|
26
|
+
PromptMessage,
|
|
27
|
+
InputTokenBreakdown,
|
|
28
|
+
OutputTokenBreakdown,
|
|
29
|
+
} from './types';
|
|
30
|
+
import {
|
|
31
|
+
getInputTokenBreakdown,
|
|
32
|
+
getOutputTokenBreakdown,
|
|
33
|
+
formatInputTokens,
|
|
34
|
+
formatOutputTokens,
|
|
35
|
+
} from './utils';
|
|
36
|
+
import { TokenBreakdownTooltip } from './components/shared-components';
|
|
37
|
+
import { StepCard } from './components/step-card';
|
|
38
|
+
import { TraceTimeline } from './components/trace-timeline';
|
|
279
39
|
|
|
280
40
|
function App() {
|
|
281
41
|
const [runs, setRuns] = useState<Run[]>([]);
|
|
282
42
|
const [selectedRun, setSelectedRun] = useState<RunDetail | null>(null);
|
|
283
43
|
const [expandedSteps, setExpandedSteps] = useState<Set<string>>(new Set());
|
|
284
44
|
const [loading, setLoading] = useState(true);
|
|
45
|
+
const [showTimeline, setShowTimeline] = useState(false);
|
|
285
46
|
|
|
286
47
|
const fetchRuns = async () => {
|
|
287
48
|
try {
|
|
@@ -301,12 +62,10 @@ function App() {
|
|
|
301
62
|
setSelectedRun(null);
|
|
302
63
|
};
|
|
303
64
|
|
|
304
|
-
// Initial fetch
|
|
305
65
|
useEffect(() => {
|
|
306
66
|
fetchRuns();
|
|
307
67
|
}, []);
|
|
308
68
|
|
|
309
|
-
// SSE connection for real-time updates
|
|
310
69
|
useEffect(() => {
|
|
311
70
|
let eventSource: EventSource | null = null;
|
|
312
71
|
let reconnectTimeout: NodeJS.Timeout | null = null;
|
|
@@ -319,9 +78,7 @@ function App() {
|
|
|
319
78
|
});
|
|
320
79
|
|
|
321
80
|
eventSource.addEventListener('update', () => {
|
|
322
|
-
// Refresh the runs list when data changes
|
|
323
81
|
fetchRuns();
|
|
324
|
-
// Also refresh selected run if one is selected
|
|
325
82
|
if (selectedRun) {
|
|
326
83
|
fetch(`/api/runs/${selectedRun.run.id}`)
|
|
327
84
|
.then(res => res.json())
|
|
@@ -334,14 +91,11 @@ function App() {
|
|
|
334
91
|
}
|
|
335
92
|
});
|
|
336
93
|
|
|
337
|
-
eventSource.addEventListener('heartbeat', () => {
|
|
338
|
-
// Connection is alive
|
|
339
|
-
});
|
|
94
|
+
eventSource.addEventListener('heartbeat', () => {});
|
|
340
95
|
|
|
341
96
|
eventSource.onerror = () => {
|
|
342
97
|
console.log('[DevTools] SSE connection lost, reconnecting...');
|
|
343
98
|
eventSource?.close();
|
|
344
|
-
// Reconnect after 2 seconds
|
|
345
99
|
reconnectTimeout = setTimeout(connect, 2000);
|
|
346
100
|
};
|
|
347
101
|
};
|
|
@@ -360,7 +114,6 @@ function App() {
|
|
|
360
114
|
const res = await fetch(`/api/runs/${runId}`);
|
|
361
115
|
const data = await res.json();
|
|
362
116
|
setSelectedRun(data);
|
|
363
|
-
// Start with all steps collapsed
|
|
364
117
|
setExpandedSteps(new Set());
|
|
365
118
|
};
|
|
366
119
|
|
|
@@ -382,7 +135,7 @@ function App() {
|
|
|
382
135
|
return `${(ms / 1000).toFixed(2)}s`;
|
|
383
136
|
};
|
|
384
137
|
|
|
385
|
-
const parseJson = (str: string | null) => {
|
|
138
|
+
const parseJson = (str: string | null): unknown => {
|
|
386
139
|
if (!str) return null;
|
|
387
140
|
try {
|
|
388
141
|
return JSON.parse(str);
|
|
@@ -394,13 +147,19 @@ function App() {
|
|
|
394
147
|
const getFirstUserMessage = (steps: Step[]): string => {
|
|
395
148
|
const firstStep = steps[0];
|
|
396
149
|
if (!firstStep) return 'Empty run';
|
|
397
|
-
const input = parseJson(firstStep.input);
|
|
398
|
-
const userMsg = input?.prompt?.find(
|
|
150
|
+
const input = parseJson(firstStep.input) as ParsedInput | null;
|
|
151
|
+
const userMsg = input?.prompt?.find(
|
|
152
|
+
(m: PromptMessage) => m.role === 'user',
|
|
153
|
+
);
|
|
399
154
|
if (userMsg) {
|
|
400
155
|
const content =
|
|
401
156
|
typeof userMsg.content === 'string'
|
|
402
157
|
? userMsg.content
|
|
403
|
-
: userMsg.content
|
|
158
|
+
: Array.isArray(userMsg.content)
|
|
159
|
+
? userMsg.content.find(
|
|
160
|
+
(p): p is { type: 'text'; text: string } => p.type === 'text',
|
|
161
|
+
)?.text || ''
|
|
162
|
+
: '';
|
|
404
163
|
return content.slice(0, 50) + (content.length > 50 ? '...' : '');
|
|
405
164
|
}
|
|
406
165
|
return 'No user message';
|
|
@@ -419,7 +178,7 @@ function App() {
|
|
|
419
178
|
): { input: InputTokenBreakdown; output: OutputTokenBreakdown } => {
|
|
420
179
|
return steps.reduce(
|
|
421
180
|
(acc, s) => {
|
|
422
|
-
const usage = parseJson(s.usage);
|
|
181
|
+
const usage = parseJson(s.usage) as ParsedUsage | null;
|
|
423
182
|
const inputBreakdown = getInputTokenBreakdown(usage?.inputTokens);
|
|
424
183
|
const outputBreakdown = getOutputTokenBreakdown(usage?.outputTokens);
|
|
425
184
|
|
|
@@ -427,7 +186,6 @@ function App() {
|
|
|
427
186
|
total: acc.input.total + inputBreakdown.total,
|
|
428
187
|
};
|
|
429
188
|
|
|
430
|
-
// Only add breakdown properties if they exist in either accumulator or current breakdown
|
|
431
189
|
if (
|
|
432
190
|
acc.input.noCache !== undefined ||
|
|
433
191
|
inputBreakdown.noCache !== undefined
|
|
@@ -454,7 +212,6 @@ function App() {
|
|
|
454
212
|
total: acc.output.total + outputBreakdown.total,
|
|
455
213
|
};
|
|
456
214
|
|
|
457
|
-
// Only add breakdown properties if they exist in either accumulator or current breakdown
|
|
458
215
|
if (
|
|
459
216
|
acc.output.text !== undefined ||
|
|
460
217
|
outputBreakdown.text !== undefined
|
|
@@ -542,9 +299,16 @@ function App() {
|
|
|
542
299
|
) : (
|
|
543
300
|
<MessageSquare className="size-3.5 text-muted-foreground mt-0.5 shrink-0" />
|
|
544
301
|
)}
|
|
545
|
-
<
|
|
546
|
-
{run.
|
|
547
|
-
|
|
302
|
+
<div className="min-w-0">
|
|
303
|
+
{run.function_id && (
|
|
304
|
+
<span className="text-[11px] font-mono text-sidebar-primary block truncate">
|
|
305
|
+
{run.function_id}
|
|
306
|
+
</span>
|
|
307
|
+
)}
|
|
308
|
+
<span className="text-[13px] text-foreground leading-tight line-clamp-1 break-all">
|
|
309
|
+
{run.firstMessage || 'Loading...'}
|
|
310
|
+
</span>
|
|
311
|
+
</div>
|
|
548
312
|
</div>
|
|
549
313
|
<div className="flex items-center gap-2 ml-5.5 text-[11px] text-muted-foreground">
|
|
550
314
|
{run.type && (
|
|
@@ -643,246 +407,63 @@ function App() {
|
|
|
643
407
|
<span>
|
|
644
408
|
{new Date(selectedRun.run.started_at).toLocaleString()}
|
|
645
409
|
</span>
|
|
410
|
+
<span className="px-3 text-muted-foreground/30">·</span>
|
|
411
|
+
<Tooltip>
|
|
412
|
+
<TooltipTrigger asChild>
|
|
413
|
+
<button
|
|
414
|
+
onClick={() => setShowTimeline(prev => !prev)}
|
|
415
|
+
className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-[11px] font-medium transition-colors ${
|
|
416
|
+
showTimeline
|
|
417
|
+
? 'bg-accent text-foreground'
|
|
418
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
|
|
419
|
+
}`}
|
|
420
|
+
>
|
|
421
|
+
<GanttChart className="size-3.5" />
|
|
422
|
+
Timeline
|
|
423
|
+
</button>
|
|
424
|
+
</TooltipTrigger>
|
|
425
|
+
<TooltipContent>
|
|
426
|
+
{showTimeline
|
|
427
|
+
? 'Switch to steps view'
|
|
428
|
+
: 'Switch to timeline view'}
|
|
429
|
+
</TooltipContent>
|
|
430
|
+
</Tooltip>
|
|
646
431
|
</div>
|
|
647
432
|
</div>
|
|
648
433
|
|
|
649
|
-
{
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
>
|
|
683
|
-
<Card
|
|
684
|
-
className={`overflow-hidden py-0 gap-0 ${isActiveStep ? 'ring-2 ring-blue-500/50 ring-offset-1 ring-offset-background' : ''}`}
|
|
685
|
-
>
|
|
686
|
-
{/* Step Header */}
|
|
687
|
-
<CollapsibleTrigger asChild>
|
|
688
|
-
<button
|
|
689
|
-
className={`w-full flex items-center justify-between px-4 py-3 text-left transition-colors hover:bg-accent/50 ${
|
|
690
|
-
isExpanded ? 'border-b border-border' : ''
|
|
691
|
-
}`}
|
|
692
|
-
>
|
|
693
|
-
<div className="flex items-center gap-3">
|
|
694
|
-
<span className="text-xs text-muted-foreground font-mono w-4">
|
|
695
|
-
{step.step_number}
|
|
696
|
-
</span>
|
|
697
|
-
<div className="flex items-center gap-2">
|
|
698
|
-
{/* Input side */}
|
|
699
|
-
{inputSummary && (
|
|
700
|
-
<>
|
|
701
|
-
{inputSummary.type === 'user' ? (
|
|
702
|
-
<MessageSquare className="size-3.5 text-muted-foreground" />
|
|
703
|
-
) : (
|
|
704
|
-
<Wrench className="size-3.5 text-muted-foreground" />
|
|
705
|
-
)}
|
|
706
|
-
{inputSummary.fullText ||
|
|
707
|
-
inputSummary.toolDetails ? (
|
|
708
|
-
<Tooltip>
|
|
709
|
-
<TooltipTrigger asChild>
|
|
710
|
-
<span
|
|
711
|
-
className={`text-sm text-muted-foreground ${inputSummary.type === 'tool' ? 'font-mono' : ''}`}
|
|
712
|
-
>
|
|
713
|
-
{inputSummary.label}
|
|
714
|
-
</span>
|
|
715
|
-
</TooltipTrigger>
|
|
716
|
-
<TooltipContent
|
|
717
|
-
side="bottom"
|
|
718
|
-
className="max-w-sm max-h-40 overflow-hidden"
|
|
719
|
-
>
|
|
720
|
-
<span className="line-clamp-6">
|
|
721
|
-
{inputSummary.fullText ||
|
|
722
|
-
inputSummary.toolDetails}
|
|
723
|
-
</span>
|
|
724
|
-
</TooltipContent>
|
|
725
|
-
</Tooltip>
|
|
726
|
-
) : (
|
|
727
|
-
<span
|
|
728
|
-
className={`text-sm text-muted-foreground ${inputSummary.type === 'tool' ? 'font-mono' : ''}`}
|
|
729
|
-
>
|
|
730
|
-
{inputSummary.label}
|
|
731
|
-
</span>
|
|
732
|
-
)}
|
|
733
|
-
<span className="text-muted-foreground/50 mx-1">
|
|
734
|
-
→
|
|
735
|
-
</span>
|
|
736
|
-
</>
|
|
737
|
-
)}
|
|
738
|
-
|
|
739
|
-
{/* Output side */}
|
|
740
|
-
{summary.icon === 'wrench' && (
|
|
741
|
-
<Wrench className="size-3.5 text-muted-foreground" />
|
|
742
|
-
)}
|
|
743
|
-
{summary.icon === 'message' && (
|
|
744
|
-
<MessageSquare className="size-3.5 text-muted-foreground" />
|
|
745
|
-
)}
|
|
746
|
-
{summary.icon === 'alert' && (
|
|
747
|
-
<AlertCircle className="size-3.5 text-destructive" />
|
|
748
|
-
)}
|
|
749
|
-
|
|
750
|
-
{summary.toolDetails ? (
|
|
751
|
-
<Tooltip>
|
|
752
|
-
<TooltipTrigger asChild>
|
|
753
|
-
<span className="text-sm font-medium font-mono">
|
|
754
|
-
{summary.label}
|
|
755
|
-
</span>
|
|
756
|
-
</TooltipTrigger>
|
|
757
|
-
<TooltipContent>
|
|
758
|
-
{summary.toolDetails}
|
|
759
|
-
</TooltipContent>
|
|
760
|
-
</Tooltip>
|
|
761
|
-
) : (
|
|
762
|
-
<span
|
|
763
|
-
className={`text-sm font-medium ${
|
|
764
|
-
summary.icon === 'wrench'
|
|
765
|
-
? 'font-mono'
|
|
766
|
-
: ''
|
|
767
|
-
}`}
|
|
768
|
-
>
|
|
769
|
-
{summary.label}
|
|
770
|
-
</span>
|
|
771
|
-
)}
|
|
772
|
-
</div>
|
|
773
|
-
</div>
|
|
774
|
-
|
|
775
|
-
<div className="flex items-center gap-4">
|
|
776
|
-
{isActiveStep ? (
|
|
777
|
-
<span className="text-[11px] text-blue-400 font-medium flex items-center gap-1.5">
|
|
778
|
-
<Loader2 className="size-3 animate-spin" />
|
|
779
|
-
streaming
|
|
780
|
-
</span>
|
|
781
|
-
) : (
|
|
782
|
-
<span className="text-[11px] text-muted-foreground font-mono">
|
|
783
|
-
{formatDuration(step.duration_ms)}
|
|
784
|
-
</span>
|
|
785
|
-
)}
|
|
786
|
-
{usage && (
|
|
787
|
-
<Tooltip>
|
|
788
|
-
<TooltipTrigger asChild>
|
|
789
|
-
<span className="text-[11px] font-mono text-muted-foreground cursor-help">
|
|
790
|
-
{formatInputTokens(
|
|
791
|
-
getInputTokenBreakdown(
|
|
792
|
-
usage.inputTokens,
|
|
793
|
-
),
|
|
794
|
-
)}{' '}
|
|
795
|
-
<span className="text-muted-foreground/50">
|
|
796
|
-
→
|
|
797
|
-
</span>{' '}
|
|
798
|
-
{formatOutputTokens(
|
|
799
|
-
getOutputTokenBreakdown(
|
|
800
|
-
usage.outputTokens,
|
|
801
|
-
),
|
|
802
|
-
)}
|
|
803
|
-
</span>
|
|
804
|
-
</TooltipTrigger>
|
|
805
|
-
<TooltipContent>
|
|
806
|
-
<TokenBreakdownTooltip
|
|
807
|
-
input={getInputTokenBreakdown(
|
|
808
|
-
usage.inputTokens,
|
|
809
|
-
)}
|
|
810
|
-
output={getOutputTokenBreakdown(
|
|
811
|
-
usage.outputTokens,
|
|
812
|
-
)}
|
|
813
|
-
raw={usage.raw}
|
|
814
|
-
/>
|
|
815
|
-
</TooltipContent>
|
|
816
|
-
</Tooltip>
|
|
817
|
-
)}
|
|
818
|
-
<ChevronDown
|
|
819
|
-
className={`size-4 text-muted-foreground transition-transform ${
|
|
820
|
-
isExpanded ? 'rotate-180' : ''
|
|
821
|
-
}`}
|
|
822
|
-
/>
|
|
823
|
-
</div>
|
|
824
|
-
</button>
|
|
825
|
-
</CollapsibleTrigger>
|
|
826
|
-
|
|
827
|
-
{/* Step Content */}
|
|
828
|
-
<CollapsibleContent>
|
|
829
|
-
{/* Config Bar */}
|
|
830
|
-
<StepConfigBar
|
|
831
|
-
modelId={step.model_id}
|
|
832
|
-
provider={step.provider}
|
|
833
|
-
input={input}
|
|
834
|
-
providerOptions={parseJson(step.provider_options)}
|
|
835
|
-
usage={usage}
|
|
836
|
-
/>
|
|
837
|
-
|
|
838
|
-
{/* Two Panel Layout */}
|
|
839
|
-
<div className="grid grid-cols-2 divide-x divide-border">
|
|
840
|
-
{/* INPUT Panel */}
|
|
841
|
-
<div className="bg-card/50">
|
|
842
|
-
<InputPanel input={input} />
|
|
843
|
-
</div>
|
|
844
|
-
|
|
845
|
-
{/* OUTPUT Panel */}
|
|
846
|
-
<div className="p-4 bg-background min-h-full">
|
|
847
|
-
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-3">
|
|
848
|
-
Output
|
|
849
|
-
</h3>
|
|
850
|
-
|
|
851
|
-
{step.error ? (
|
|
852
|
-
<div className="p-3 rounded-md bg-destructive/10 border border-destructive/30 text-sm text-destructive-foreground font-mono">
|
|
853
|
-
{step.error}
|
|
854
|
-
</div>
|
|
855
|
-
) : output ? (
|
|
856
|
-
<OutputDisplay
|
|
857
|
-
output={output}
|
|
858
|
-
toolResults={toolResults}
|
|
859
|
-
/>
|
|
860
|
-
) : isActiveStep ? (
|
|
861
|
-
<div className="flex items-center gap-2 text-sm text-blue-400">
|
|
862
|
-
<Loader2 className="size-4 animate-spin" />
|
|
863
|
-
<span>Waiting for response...</span>
|
|
864
|
-
</div>
|
|
865
|
-
) : (
|
|
866
|
-
<p className="text-sm text-muted-foreground">
|
|
867
|
-
No output
|
|
868
|
-
</p>
|
|
869
|
-
)}
|
|
870
|
-
</div>
|
|
871
|
-
</div>
|
|
872
|
-
|
|
873
|
-
{/* Raw Data Toggle */}
|
|
874
|
-
<RawDataSection
|
|
875
|
-
rawRequest={step.raw_request}
|
|
876
|
-
rawResponse={step.raw_response}
|
|
877
|
-
rawChunks={step.raw_chunks}
|
|
878
|
-
isStream={step.type === 'stream'}
|
|
879
|
-
/>
|
|
880
|
-
</CollapsibleContent>
|
|
881
|
-
</Card>
|
|
882
|
-
</Collapsible>
|
|
883
|
-
);
|
|
884
|
-
})}
|
|
885
|
-
</div>
|
|
434
|
+
{showTimeline ? (
|
|
435
|
+
<TraceTimeline
|
|
436
|
+
runDetail={selectedRun}
|
|
437
|
+
parseJson={parseJson}
|
|
438
|
+
formatDuration={formatDuration}
|
|
439
|
+
/>
|
|
440
|
+
) : (
|
|
441
|
+
<div className="flex flex-col gap-3">
|
|
442
|
+
{selectedRun.steps.map((step, index) => {
|
|
443
|
+
const childRunsForStep = (
|
|
444
|
+
selectedRun.childRuns ?? []
|
|
445
|
+
).filter(cr => cr.run.parent_step_id === step.id);
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<StepCard
|
|
449
|
+
key={step.id}
|
|
450
|
+
step={step}
|
|
451
|
+
index={index}
|
|
452
|
+
steps={selectedRun.steps}
|
|
453
|
+
isRunInProgress={
|
|
454
|
+
selectedRun.run.isInProgress ?? false
|
|
455
|
+
}
|
|
456
|
+
expandedSteps={expandedSteps}
|
|
457
|
+
toggleStep={toggleStep}
|
|
458
|
+
parseJson={parseJson}
|
|
459
|
+
formatDuration={formatDuration}
|
|
460
|
+
childRuns={childRunsForStep}
|
|
461
|
+
depth={0}
|
|
462
|
+
/>
|
|
463
|
+
);
|
|
464
|
+
})}
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
886
467
|
</div>
|
|
887
468
|
)}
|
|
888
469
|
</ScrollArea>
|
|
@@ -892,1366 +473,4 @@ function App() {
|
|
|
892
473
|
);
|
|
893
474
|
}
|
|
894
475
|
|
|
895
|
-
function StepConfigBar({
|
|
896
|
-
modelId,
|
|
897
|
-
provider,
|
|
898
|
-
input,
|
|
899
|
-
providerOptions,
|
|
900
|
-
usage,
|
|
901
|
-
}: {
|
|
902
|
-
modelId?: string;
|
|
903
|
-
provider?: string | null;
|
|
904
|
-
input: any;
|
|
905
|
-
providerOptions?: any;
|
|
906
|
-
usage?: any;
|
|
907
|
-
}) {
|
|
908
|
-
const toolCount = input?.tools?.length ?? 0;
|
|
909
|
-
|
|
910
|
-
// Collect parameters
|
|
911
|
-
const params: { label: string; value: string }[] = [];
|
|
912
|
-
if (input?.temperature != null)
|
|
913
|
-
params.push({ label: 'temp', value: String(input.temperature) });
|
|
914
|
-
if (input?.maxOutputTokens != null)
|
|
915
|
-
params.push({ label: 'max tokens', value: String(input.maxOutputTokens) });
|
|
916
|
-
if (input?.topP != null)
|
|
917
|
-
params.push({ label: 'topP', value: String(input.topP) });
|
|
918
|
-
if (input?.topK != null)
|
|
919
|
-
params.push({ label: 'topK', value: String(input.topK) });
|
|
920
|
-
if (input?.toolChoice != null) {
|
|
921
|
-
const choice =
|
|
922
|
-
typeof input.toolChoice === 'string'
|
|
923
|
-
? input.toolChoice
|
|
924
|
-
: input.toolChoice.type;
|
|
925
|
-
params.push({ label: 'tool choice', value: choice });
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
return (
|
|
929
|
-
<div className="flex items-center gap-2 px-4 py-2 bg-muted/20 border-b border-border text-[11px] text-muted-foreground">
|
|
930
|
-
{provider && (
|
|
931
|
-
<span className="px-1.5 py-0.5 rounded bg-sidebar-primary/10 text-sidebar-primary text-[10px] font-medium">
|
|
932
|
-
{provider}
|
|
933
|
-
</span>
|
|
934
|
-
)}
|
|
935
|
-
<span className="font-mono">{modelId}</span>
|
|
936
|
-
|
|
937
|
-
{params.length > 0 && (
|
|
938
|
-
<>
|
|
939
|
-
<span className="text-muted-foreground/30">·</span>
|
|
940
|
-
{params.map((p, i) => (
|
|
941
|
-
<span key={i}>
|
|
942
|
-
{p.label}: <span className="text-foreground">{p.value}</span>
|
|
943
|
-
{i < params.length - 1 && (
|
|
944
|
-
<span className="text-muted-foreground/30 mx-1">·</span>
|
|
945
|
-
)}
|
|
946
|
-
</span>
|
|
947
|
-
))}
|
|
948
|
-
</>
|
|
949
|
-
)}
|
|
950
|
-
|
|
951
|
-
{toolCount > 0 && (
|
|
952
|
-
<>
|
|
953
|
-
<span className="text-muted-foreground/30">·</span>
|
|
954
|
-
<Drawer direction="right">
|
|
955
|
-
<DrawerTrigger asChild>
|
|
956
|
-
<button className="inline-flex items-center gap-1 hover:text-foreground transition-colors cursor-pointer">
|
|
957
|
-
<Wrench className="size-3" />
|
|
958
|
-
{toolCount} available {toolCount === 1 ? 'tool' : 'tools'}
|
|
959
|
-
</button>
|
|
960
|
-
</DrawerTrigger>
|
|
961
|
-
<DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
|
|
962
|
-
<DrawerHeader className="border-b border-border shrink-0">
|
|
963
|
-
<DrawerTitle>Available Tools ({toolCount})</DrawerTitle>
|
|
964
|
-
</DrawerHeader>
|
|
965
|
-
<div className="flex-1 overflow-y-auto p-4">
|
|
966
|
-
<div className="space-y-3">
|
|
967
|
-
{input?.tools?.map((tool: any, i: number) => (
|
|
968
|
-
<ToolItem key={i} tool={tool} />
|
|
969
|
-
))}
|
|
970
|
-
</div>
|
|
971
|
-
</div>
|
|
972
|
-
</DrawerContent>
|
|
973
|
-
</Drawer>
|
|
974
|
-
</>
|
|
975
|
-
)}
|
|
976
|
-
|
|
977
|
-
{providerOptions && Object.keys(providerOptions).length > 0 && (
|
|
978
|
-
<>
|
|
979
|
-
<span className="text-muted-foreground/30">·</span>
|
|
980
|
-
<Drawer direction="right">
|
|
981
|
-
<DrawerTrigger asChild>
|
|
982
|
-
<button className="inline-flex items-center gap-1 hover:text-foreground transition-colors cursor-pointer">
|
|
983
|
-
<Settings className="size-3" />
|
|
984
|
-
Provider options
|
|
985
|
-
</button>
|
|
986
|
-
</DrawerTrigger>
|
|
987
|
-
<DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
|
|
988
|
-
<DrawerHeader className="border-b border-border shrink-0">
|
|
989
|
-
<DrawerTitle>Provider Options</DrawerTitle>
|
|
990
|
-
</DrawerHeader>
|
|
991
|
-
<div className="flex-1 overflow-y-auto p-4">
|
|
992
|
-
<JsonBlock data={providerOptions} size="lg" />
|
|
993
|
-
</div>
|
|
994
|
-
</DrawerContent>
|
|
995
|
-
</Drawer>
|
|
996
|
-
</>
|
|
997
|
-
)}
|
|
998
|
-
|
|
999
|
-
{usage && (
|
|
1000
|
-
<>
|
|
1001
|
-
<span className="text-muted-foreground/30">·</span>
|
|
1002
|
-
<Drawer direction="right">
|
|
1003
|
-
<DrawerTrigger asChild>
|
|
1004
|
-
<button className="inline-flex items-center gap-1 hover:text-foreground transition-colors cursor-pointer">
|
|
1005
|
-
<BarChart3 className="size-3" />
|
|
1006
|
-
Usage
|
|
1007
|
-
</button>
|
|
1008
|
-
</DrawerTrigger>
|
|
1009
|
-
<DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
|
|
1010
|
-
<DrawerHeader className="border-b border-border shrink-0">
|
|
1011
|
-
<DrawerTitle>Token Usage</DrawerTitle>
|
|
1012
|
-
</DrawerHeader>
|
|
1013
|
-
<div className="flex-1 overflow-y-auto p-4">
|
|
1014
|
-
<UsageDetails usage={usage} />
|
|
1015
|
-
</div>
|
|
1016
|
-
</DrawerContent>
|
|
1017
|
-
</Drawer>
|
|
1018
|
-
</>
|
|
1019
|
-
)}
|
|
1020
|
-
</div>
|
|
1021
|
-
);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function InputPanel({ input }: { input: any }) {
|
|
1025
|
-
const messages = input?.prompt ?? [];
|
|
1026
|
-
const messageCount = messages.length;
|
|
1027
|
-
|
|
1028
|
-
// Get last two messages to display
|
|
1029
|
-
const lastTwoMessages = messages.slice(-2);
|
|
1030
|
-
const previousMessageCount = Math.max(0, messageCount - 2);
|
|
1031
|
-
|
|
1032
|
-
return (
|
|
1033
|
-
<Drawer direction="right">
|
|
1034
|
-
<DrawerTrigger asChild>
|
|
1035
|
-
<button className="w-full h-full text-left p-4 hover:bg-accent/30 transition-colors cursor-pointer flex flex-col justify-start">
|
|
1036
|
-
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-3">
|
|
1037
|
-
Input
|
|
1038
|
-
</h3>
|
|
1039
|
-
|
|
1040
|
-
<div className="space-y-3">
|
|
1041
|
-
{/* Previous messages indicator */}
|
|
1042
|
-
{previousMessageCount > 0 && (
|
|
1043
|
-
<div className="text-[11px] text-muted-foreground/60 text-center py-1.5 rounded-md bg-muted/30">
|
|
1044
|
-
+ {previousMessageCount} previous{' '}
|
|
1045
|
-
{previousMessageCount === 1 ? 'message' : 'messages'}
|
|
1046
|
-
</div>
|
|
1047
|
-
)}
|
|
1048
|
-
|
|
1049
|
-
{/* Last two messages displayed inline */}
|
|
1050
|
-
{lastTwoMessages.map((msg: any, i: number) => (
|
|
1051
|
-
<InputMessagePreview
|
|
1052
|
-
key={i}
|
|
1053
|
-
message={msg}
|
|
1054
|
-
index={previousMessageCount + i + 1}
|
|
1055
|
-
/>
|
|
1056
|
-
))}
|
|
1057
|
-
|
|
1058
|
-
{/* Empty state */}
|
|
1059
|
-
{messageCount === 0 && (
|
|
1060
|
-
<p className="text-sm text-muted-foreground">No messages</p>
|
|
1061
|
-
)}
|
|
1062
|
-
</div>
|
|
1063
|
-
</button>
|
|
1064
|
-
</DrawerTrigger>
|
|
1065
|
-
<DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
|
|
1066
|
-
<DrawerHeader className="border-b border-border shrink-0">
|
|
1067
|
-
<DrawerTitle>All Messages ({messageCount})</DrawerTitle>
|
|
1068
|
-
</DrawerHeader>
|
|
1069
|
-
<div className="flex-1 overflow-y-auto p-4">
|
|
1070
|
-
<div className="space-y-3">
|
|
1071
|
-
{messages.map((msg: any, i: number) => (
|
|
1072
|
-
<MessageBubble key={i} message={msg} index={i + 1} />
|
|
1073
|
-
))}
|
|
1074
|
-
</div>
|
|
1075
|
-
</div>
|
|
1076
|
-
</DrawerContent>
|
|
1077
|
-
</Drawer>
|
|
1078
|
-
);
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
function InputMessagePreview({
|
|
1082
|
-
message,
|
|
1083
|
-
index,
|
|
1084
|
-
}: {
|
|
1085
|
-
message: any;
|
|
1086
|
-
index?: number;
|
|
1087
|
-
}) {
|
|
1088
|
-
const role = message.role;
|
|
1089
|
-
const content = message.content;
|
|
1090
|
-
|
|
1091
|
-
const roleLabels: Record<string, string> = {
|
|
1092
|
-
user: 'User',
|
|
1093
|
-
assistant: 'Assistant',
|
|
1094
|
-
system: 'System',
|
|
1095
|
-
tool: 'Tool',
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1098
|
-
// Get text content
|
|
1099
|
-
const getTextContent = (content: any): string => {
|
|
1100
|
-
if (typeof content === 'string') return content;
|
|
1101
|
-
if (Array.isArray(content)) {
|
|
1102
|
-
return content
|
|
1103
|
-
.filter(p => p.type === 'text')
|
|
1104
|
-
.map(p => p.text)
|
|
1105
|
-
.join('');
|
|
1106
|
-
}
|
|
1107
|
-
return '';
|
|
1108
|
-
};
|
|
1109
|
-
|
|
1110
|
-
// Get tool calls from assistant message
|
|
1111
|
-
const getToolCalls = (content: any): any[] => {
|
|
1112
|
-
if (Array.isArray(content)) {
|
|
1113
|
-
return content.filter(p => p.type === 'tool-call');
|
|
1114
|
-
}
|
|
1115
|
-
return [];
|
|
1116
|
-
};
|
|
1117
|
-
|
|
1118
|
-
// Get tool results from tool message
|
|
1119
|
-
const getToolResults = (content: any): any[] => {
|
|
1120
|
-
if (Array.isArray(content)) {
|
|
1121
|
-
return content.filter(p => p.type === 'tool-result');
|
|
1122
|
-
}
|
|
1123
|
-
return [];
|
|
1124
|
-
};
|
|
1125
|
-
|
|
1126
|
-
// Get reasoning/thinking content
|
|
1127
|
-
const getReasoningContent = (content: any): string => {
|
|
1128
|
-
if (Array.isArray(content)) {
|
|
1129
|
-
return content
|
|
1130
|
-
.filter(p => p.type === 'thinking' || p.type === 'reasoning')
|
|
1131
|
-
.map(p => p.thinking || p.text || p.reasoning)
|
|
1132
|
-
.join('');
|
|
1133
|
-
}
|
|
1134
|
-
return '';
|
|
1135
|
-
};
|
|
1136
|
-
|
|
1137
|
-
const textContent = getTextContent(content);
|
|
1138
|
-
const toolCalls = getToolCalls(content);
|
|
1139
|
-
const toolResults = getToolResults(content);
|
|
1140
|
-
const reasoningContent = getReasoningContent(content);
|
|
1141
|
-
|
|
1142
|
-
// Count total parts
|
|
1143
|
-
const partCount =
|
|
1144
|
-
(textContent ? 1 : 0) +
|
|
1145
|
-
(reasoningContent ? 1 : 0) +
|
|
1146
|
-
toolCalls.length +
|
|
1147
|
-
toolResults.length;
|
|
1148
|
-
|
|
1149
|
-
return (
|
|
1150
|
-
<div className="rounded-md border border-border/50 bg-background/50 p-2.5 space-y-2">
|
|
1151
|
-
<div className="flex items-center gap-2">
|
|
1152
|
-
{index && (
|
|
1153
|
-
<span className="text-[10px] font-mono text-muted-foreground/50">
|
|
1154
|
-
{index}
|
|
1155
|
-
</span>
|
|
1156
|
-
)}
|
|
1157
|
-
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
1158
|
-
{roleLabels[role] || role}
|
|
1159
|
-
</span>
|
|
1160
|
-
{partCount > 1 && (
|
|
1161
|
-
<span className="text-[10px] text-muted-foreground/60">
|
|
1162
|
-
{partCount} parts
|
|
1163
|
-
</span>
|
|
1164
|
-
)}
|
|
1165
|
-
</div>
|
|
1166
|
-
|
|
1167
|
-
{/* Reasoning/Thinking content */}
|
|
1168
|
-
{reasoningContent && (
|
|
1169
|
-
<div className="text-xs text-amber-500/60">[thinking]</div>
|
|
1170
|
-
)}
|
|
1171
|
-
|
|
1172
|
-
{/* Text content */}
|
|
1173
|
-
{textContent && (
|
|
1174
|
-
<div className="text-xs text-foreground/90 line-clamp-3">
|
|
1175
|
-
{textContent}
|
|
1176
|
-
</div>
|
|
1177
|
-
)}
|
|
1178
|
-
|
|
1179
|
-
{/* Tool calls (from assistant) */}
|
|
1180
|
-
{toolCalls.length > 0 && (
|
|
1181
|
-
<div className="space-y-1">
|
|
1182
|
-
{toolCalls.slice(0, 3).map((call: any, i: number) => {
|
|
1183
|
-
const args = call.args ?? call.input;
|
|
1184
|
-
const parsedArgs =
|
|
1185
|
-
typeof args === 'string' ? safeParseJson(args) : args;
|
|
1186
|
-
return (
|
|
1187
|
-
<div
|
|
1188
|
-
key={i}
|
|
1189
|
-
className="text-[11px] font-mono text-muted-foreground truncate"
|
|
1190
|
-
>
|
|
1191
|
-
{call.toolName}({formatToolParamsInline(parsedArgs)})
|
|
1192
|
-
</div>
|
|
1193
|
-
);
|
|
1194
|
-
})}
|
|
1195
|
-
{toolCalls.length > 3 && (
|
|
1196
|
-
<div className="text-[11px] text-muted-foreground/60">
|
|
1197
|
-
+{toolCalls.length - 3} more tool{' '}
|
|
1198
|
-
{toolCalls.length - 3 === 1 ? 'call' : 'calls'}
|
|
1199
|
-
</div>
|
|
1200
|
-
)}
|
|
1201
|
-
</div>
|
|
1202
|
-
)}
|
|
1203
|
-
|
|
1204
|
-
{/* Tool results */}
|
|
1205
|
-
{toolResults.length > 0 && (
|
|
1206
|
-
<div className="space-y-1">
|
|
1207
|
-
{toolResults.slice(0, 3).map((result: any, i: number) => {
|
|
1208
|
-
const resultContent = result.result ?? result.output ?? result;
|
|
1209
|
-
const resultPreview = formatResultPreview(resultContent);
|
|
1210
|
-
return (
|
|
1211
|
-
<div
|
|
1212
|
-
key={i}
|
|
1213
|
-
className="text-[11px] font-mono text-muted-foreground truncate"
|
|
1214
|
-
>
|
|
1215
|
-
{result.toolName || 'tool'}(…) => {resultPreview}
|
|
1216
|
-
</div>
|
|
1217
|
-
);
|
|
1218
|
-
})}
|
|
1219
|
-
{toolResults.length > 3 && (
|
|
1220
|
-
<div className="text-[11px] text-muted-foreground/60">
|
|
1221
|
-
+{toolResults.length - 3} more tool{' '}
|
|
1222
|
-
{toolResults.length - 3 === 1 ? 'result' : 'results'}
|
|
1223
|
-
</div>
|
|
1224
|
-
)}
|
|
1225
|
-
</div>
|
|
1226
|
-
)}
|
|
1227
|
-
|
|
1228
|
-
{/* Empty content fallback */}
|
|
1229
|
-
{!textContent &&
|
|
1230
|
-
!reasoningContent &&
|
|
1231
|
-
toolCalls.length === 0 &&
|
|
1232
|
-
toolResults.length === 0 && (
|
|
1233
|
-
<div className="text-[11px] text-muted-foreground italic">
|
|
1234
|
-
Empty message
|
|
1235
|
-
</div>
|
|
1236
|
-
)}
|
|
1237
|
-
</div>
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
function ToolItem({ tool }: { tool: any }) {
|
|
1242
|
-
const [expanded, setExpanded] = useState(false);
|
|
1243
|
-
|
|
1244
|
-
return (
|
|
1245
|
-
<div className="rounded-md border border-border bg-background overflow-hidden">
|
|
1246
|
-
<button
|
|
1247
|
-
className="w-full flex items-center justify-between p-2.5 hover:bg-accent/50 transition-colors"
|
|
1248
|
-
onClick={() => setExpanded(!expanded)}
|
|
1249
|
-
>
|
|
1250
|
-
<span className="text-sm font-mono text-purple">{tool.name}</span>
|
|
1251
|
-
{tool.parameters && (
|
|
1252
|
-
<ChevronRight
|
|
1253
|
-
className={`size-3 text-muted-foreground transition-transform ${
|
|
1254
|
-
expanded ? 'rotate-90' : ''
|
|
1255
|
-
}`}
|
|
1256
|
-
/>
|
|
1257
|
-
)}
|
|
1258
|
-
</button>
|
|
1259
|
-
{expanded && tool.parameters && (
|
|
1260
|
-
<div className="px-2.5 pb-2.5 border-t border-border">
|
|
1261
|
-
{tool.description && (
|
|
1262
|
-
<p className="text-xs text-muted-foreground mb-2 pt-2">
|
|
1263
|
-
{tool.description}
|
|
1264
|
-
</p>
|
|
1265
|
-
)}
|
|
1266
|
-
<JsonBlock data={tool.parameters} compact />
|
|
1267
|
-
</div>
|
|
1268
|
-
)}
|
|
1269
|
-
{!expanded && tool.description && (
|
|
1270
|
-
<div className="px-2.5 pb-2 -mt-1">
|
|
1271
|
-
<p className="text-[11px] text-muted-foreground truncate">
|
|
1272
|
-
{tool.description}
|
|
1273
|
-
</p>
|
|
1274
|
-
</div>
|
|
1275
|
-
)}
|
|
1276
|
-
</div>
|
|
1277
|
-
);
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
function CollapsibleToolCall({
|
|
1281
|
-
toolName,
|
|
1282
|
-
toolCallId,
|
|
1283
|
-
data,
|
|
1284
|
-
}: {
|
|
1285
|
-
toolName: string;
|
|
1286
|
-
toolCallId?: string;
|
|
1287
|
-
data: any;
|
|
1288
|
-
}) {
|
|
1289
|
-
const [expanded, setExpanded] = useState(false);
|
|
1290
|
-
const parsedData = typeof data === 'string' ? safeParseJson(data) : data;
|
|
1291
|
-
|
|
1292
|
-
return (
|
|
1293
|
-
<div className="rounded-md border border-purple/30 overflow-hidden">
|
|
1294
|
-
<button
|
|
1295
|
-
className="w-full flex items-center gap-2 px-3 py-2 bg-purple/10 hover:bg-purple/20 transition-colors"
|
|
1296
|
-
onClick={() => setExpanded(!expanded)}
|
|
1297
|
-
>
|
|
1298
|
-
<ChevronRight
|
|
1299
|
-
className={`size-3 text-purple transition-transform shrink-0 ${
|
|
1300
|
-
expanded ? 'rotate-90' : ''
|
|
1301
|
-
}`}
|
|
1302
|
-
/>
|
|
1303
|
-
<Wrench className="size-3 text-purple shrink-0" />
|
|
1304
|
-
<span className="text-xs font-mono font-medium text-purple">
|
|
1305
|
-
{toolName}
|
|
1306
|
-
</span>
|
|
1307
|
-
{!expanded && parsedData && (
|
|
1308
|
-
<span className="text-[11px] font-mono text-purple/70 truncate">
|
|
1309
|
-
{formatToolParams(parsedData)}
|
|
1310
|
-
</span>
|
|
1311
|
-
)}
|
|
1312
|
-
{toolCallId && (
|
|
1313
|
-
<span className="text-[10px] font-mono text-muted-foreground/60 ml-auto shrink-0">
|
|
1314
|
-
{toolCallId}
|
|
1315
|
-
</span>
|
|
1316
|
-
)}
|
|
1317
|
-
</button>
|
|
1318
|
-
{expanded && (
|
|
1319
|
-
<div className="p-3 bg-card/50 border-t border-purple/30">
|
|
1320
|
-
<JsonBlock data={parsedData} />
|
|
1321
|
-
</div>
|
|
1322
|
-
)}
|
|
1323
|
-
</div>
|
|
1324
|
-
);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
function CollapsibleToolResult({
|
|
1328
|
-
toolName,
|
|
1329
|
-
toolCallId,
|
|
1330
|
-
data,
|
|
1331
|
-
}: {
|
|
1332
|
-
toolName?: string;
|
|
1333
|
-
toolCallId?: string;
|
|
1334
|
-
data: any;
|
|
1335
|
-
}) {
|
|
1336
|
-
const [expanded, setExpanded] = useState(false);
|
|
1337
|
-
|
|
1338
|
-
return (
|
|
1339
|
-
<div className="rounded-md border border-success/30 overflow-hidden">
|
|
1340
|
-
<button
|
|
1341
|
-
className="w-full flex items-center gap-2 px-3 py-2 bg-success/10 hover:bg-success/20 transition-colors"
|
|
1342
|
-
onClick={() => setExpanded(!expanded)}
|
|
1343
|
-
>
|
|
1344
|
-
<ChevronRight
|
|
1345
|
-
className={`size-3 text-success transition-transform shrink-0 ${
|
|
1346
|
-
expanded ? 'rotate-90' : ''
|
|
1347
|
-
}`}
|
|
1348
|
-
/>
|
|
1349
|
-
<span className="text-xs font-medium text-success">Result</span>
|
|
1350
|
-
{toolName && (
|
|
1351
|
-
<span className="text-[11px] font-mono text-muted-foreground">
|
|
1352
|
-
{toolName}
|
|
1353
|
-
</span>
|
|
1354
|
-
)}
|
|
1355
|
-
{toolCallId && (
|
|
1356
|
-
<span className="text-[10px] font-mono text-muted-foreground/60 ml-auto shrink-0">
|
|
1357
|
-
{toolCallId}
|
|
1358
|
-
</span>
|
|
1359
|
-
)}
|
|
1360
|
-
</button>
|
|
1361
|
-
{expanded && (
|
|
1362
|
-
<div className="p-3 bg-card/50 border-t border-success/30">
|
|
1363
|
-
<JsonBlock data={data} />
|
|
1364
|
-
</div>
|
|
1365
|
-
)}
|
|
1366
|
-
</div>
|
|
1367
|
-
);
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
function MessageBubble({ message, index }: { message: any; index?: number }) {
|
|
1371
|
-
const role = message.role;
|
|
1372
|
-
const content = message.content;
|
|
1373
|
-
|
|
1374
|
-
const roleLabels: Record<string, string> = {
|
|
1375
|
-
user: 'User',
|
|
1376
|
-
assistant: 'Assistant',
|
|
1377
|
-
system: 'System',
|
|
1378
|
-
tool: 'Tool',
|
|
1379
|
-
};
|
|
1380
|
-
|
|
1381
|
-
const getTextContent = (content: any): string => {
|
|
1382
|
-
if (typeof content === 'string') return content;
|
|
1383
|
-
if (Array.isArray(content)) {
|
|
1384
|
-
return content
|
|
1385
|
-
.filter(p => p.type === 'text')
|
|
1386
|
-
.map(p => p.text)
|
|
1387
|
-
.join('');
|
|
1388
|
-
}
|
|
1389
|
-
return '';
|
|
1390
|
-
};
|
|
1391
|
-
|
|
1392
|
-
const getToolCalls = (content: any): any[] => {
|
|
1393
|
-
if (Array.isArray(content)) {
|
|
1394
|
-
return content.filter(p => p.type === 'tool-call');
|
|
1395
|
-
}
|
|
1396
|
-
return [];
|
|
1397
|
-
};
|
|
1398
|
-
|
|
1399
|
-
const getToolResults = (content: any): any[] => {
|
|
1400
|
-
if (Array.isArray(content)) {
|
|
1401
|
-
return content.filter(p => p.type === 'tool-result');
|
|
1402
|
-
}
|
|
1403
|
-
return [];
|
|
1404
|
-
};
|
|
1405
|
-
|
|
1406
|
-
const getReasoningContent = (content: any): string => {
|
|
1407
|
-
if (Array.isArray(content)) {
|
|
1408
|
-
return content
|
|
1409
|
-
.filter(p => p.type === 'thinking' || p.type === 'reasoning')
|
|
1410
|
-
.map(p => p.thinking || p.text || p.reasoning)
|
|
1411
|
-
.join('');
|
|
1412
|
-
}
|
|
1413
|
-
return '';
|
|
1414
|
-
};
|
|
1415
|
-
|
|
1416
|
-
const textContent = getTextContent(content);
|
|
1417
|
-
const toolCalls = getToolCalls(content);
|
|
1418
|
-
const toolResults = getToolResults(content);
|
|
1419
|
-
const reasoningContent = getReasoningContent(content);
|
|
1420
|
-
|
|
1421
|
-
// Count total parts
|
|
1422
|
-
const partCount =
|
|
1423
|
-
(textContent ? 1 : 0) +
|
|
1424
|
-
(reasoningContent ? 1 : 0) +
|
|
1425
|
-
toolCalls.length +
|
|
1426
|
-
toolResults.length;
|
|
1427
|
-
|
|
1428
|
-
return (
|
|
1429
|
-
<div className="rounded-md border border-border/50 bg-background/50 p-3 space-y-2">
|
|
1430
|
-
<div className="flex items-center gap-2">
|
|
1431
|
-
{index && (
|
|
1432
|
-
<span className="text-[10px] font-mono text-muted-foreground/50">
|
|
1433
|
-
{index}
|
|
1434
|
-
</span>
|
|
1435
|
-
)}
|
|
1436
|
-
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
1437
|
-
{roleLabels[role] || role}
|
|
1438
|
-
</span>
|
|
1439
|
-
{partCount > 1 && (
|
|
1440
|
-
<span className="text-[10px] text-muted-foreground/60">
|
|
1441
|
-
{partCount} parts
|
|
1442
|
-
</span>
|
|
1443
|
-
)}
|
|
1444
|
-
</div>
|
|
1445
|
-
|
|
1446
|
-
{/* Reasoning/Thinking content - full display */}
|
|
1447
|
-
{reasoningContent && <ReasoningBlock content={reasoningContent} />}
|
|
1448
|
-
|
|
1449
|
-
{/* Full text content */}
|
|
1450
|
-
{textContent && (
|
|
1451
|
-
<TextBlock
|
|
1452
|
-
content={textContent}
|
|
1453
|
-
defaultExpanded={
|
|
1454
|
-
!reasoningContent &&
|
|
1455
|
-
toolCalls.length === 0 &&
|
|
1456
|
-
toolResults.length === 0
|
|
1457
|
-
}
|
|
1458
|
-
isSystem={role === 'system'}
|
|
1459
|
-
/>
|
|
1460
|
-
)}
|
|
1461
|
-
|
|
1462
|
-
{/* Tool calls */}
|
|
1463
|
-
{toolCalls.length > 0 && (
|
|
1464
|
-
<div className="space-y-2">
|
|
1465
|
-
{toolCalls.map((call: any, i: number) => (
|
|
1466
|
-
<CollapsibleToolCall
|
|
1467
|
-
key={i}
|
|
1468
|
-
toolName={call.toolName}
|
|
1469
|
-
toolCallId={call.toolCallId}
|
|
1470
|
-
data={call.args ?? call.input}
|
|
1471
|
-
/>
|
|
1472
|
-
))}
|
|
1473
|
-
</div>
|
|
1474
|
-
)}
|
|
1475
|
-
|
|
1476
|
-
{/* Tool results */}
|
|
1477
|
-
{toolResults.length > 0 && (
|
|
1478
|
-
<div className="space-y-2">
|
|
1479
|
-
{toolResults.map((result: any, i: number) => (
|
|
1480
|
-
<CollapsibleToolResult
|
|
1481
|
-
key={i}
|
|
1482
|
-
toolName={result.toolName}
|
|
1483
|
-
toolCallId={result.toolCallId}
|
|
1484
|
-
data={result.result ?? result.output ?? result}
|
|
1485
|
-
/>
|
|
1486
|
-
))}
|
|
1487
|
-
</div>
|
|
1488
|
-
)}
|
|
1489
|
-
|
|
1490
|
-
{/* Empty content fallback */}
|
|
1491
|
-
{!textContent &&
|
|
1492
|
-
!reasoningContent &&
|
|
1493
|
-
toolCalls.length === 0 &&
|
|
1494
|
-
toolResults.length === 0 && (
|
|
1495
|
-
<div className="text-[11px] text-muted-foreground italic">
|
|
1496
|
-
Empty message
|
|
1497
|
-
</div>
|
|
1498
|
-
)}
|
|
1499
|
-
</div>
|
|
1500
|
-
);
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
function OutputDisplay({
|
|
1504
|
-
output,
|
|
1505
|
-
toolResults = [],
|
|
1506
|
-
}: {
|
|
1507
|
-
output: any;
|
|
1508
|
-
toolResults?: any[];
|
|
1509
|
-
}) {
|
|
1510
|
-
const getToolResult = (toolCallId: string) => {
|
|
1511
|
-
return toolResults.find((r: any) => r.toolCallId === toolCallId);
|
|
1512
|
-
};
|
|
1513
|
-
|
|
1514
|
-
const toolCalls =
|
|
1515
|
-
output?.toolCalls ||
|
|
1516
|
-
output?.content?.filter((p: any) => p.type === 'tool-call') ||
|
|
1517
|
-
[];
|
|
1518
|
-
|
|
1519
|
-
const textParts =
|
|
1520
|
-
output?.textParts ||
|
|
1521
|
-
output?.content?.filter((p: any) => p.type === 'text') ||
|
|
1522
|
-
[];
|
|
1523
|
-
|
|
1524
|
-
// Get reasoning/thinking parts
|
|
1525
|
-
const reasoningParts =
|
|
1526
|
-
output?.reasoningParts ||
|
|
1527
|
-
output?.content?.filter(
|
|
1528
|
-
(p: any) => p.type === 'thinking' || p.type === 'reasoning',
|
|
1529
|
-
) ||
|
|
1530
|
-
[];
|
|
1531
|
-
|
|
1532
|
-
const textContent = textParts.map((p: any) => p.text).join('');
|
|
1533
|
-
const reasoningContent = reasoningParts.map((p: any) => p.text).join('');
|
|
1534
|
-
|
|
1535
|
-
// Check if text is the only content
|
|
1536
|
-
const isTextOnly = textContent && !reasoningContent && toolCalls.length === 0;
|
|
1537
|
-
|
|
1538
|
-
return (
|
|
1539
|
-
<div className="space-y-3">
|
|
1540
|
-
{/* Reasoning/Thinking */}
|
|
1541
|
-
{reasoningContent && <ReasoningBlock content={reasoningContent} />}
|
|
1542
|
-
|
|
1543
|
-
{/* Text Response */}
|
|
1544
|
-
{textContent && (
|
|
1545
|
-
<TextBlock content={textContent} defaultExpanded={isTextOnly} />
|
|
1546
|
-
)}
|
|
1547
|
-
|
|
1548
|
-
{/* Tool Calls with Results */}
|
|
1549
|
-
{toolCalls.map((call: any, i: number) => {
|
|
1550
|
-
const result = getToolResult(call.toolCallId);
|
|
1551
|
-
return (
|
|
1552
|
-
<ToolCallCard
|
|
1553
|
-
key={i}
|
|
1554
|
-
toolName={call.toolName}
|
|
1555
|
-
args={call.args ?? call.input}
|
|
1556
|
-
result={result?.output ?? result?.result}
|
|
1557
|
-
/>
|
|
1558
|
-
);
|
|
1559
|
-
})}
|
|
1560
|
-
</div>
|
|
1561
|
-
);
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
function ToolCallCard({
|
|
1565
|
-
toolName,
|
|
1566
|
-
args,
|
|
1567
|
-
result,
|
|
1568
|
-
}: {
|
|
1569
|
-
toolName: string;
|
|
1570
|
-
args: any;
|
|
1571
|
-
result?: any;
|
|
1572
|
-
}) {
|
|
1573
|
-
const [expanded, setExpanded] = useState(false);
|
|
1574
|
-
const parsedArgs = typeof args === 'string' ? safeParseJson(args) : args;
|
|
1575
|
-
const parsedResult =
|
|
1576
|
-
typeof result === 'string' ? safeParseJson(result) : result;
|
|
1577
|
-
|
|
1578
|
-
return (
|
|
1579
|
-
<div className="rounded-md border border-purple/30 overflow-hidden">
|
|
1580
|
-
{/* Tool Call Header */}
|
|
1581
|
-
<button
|
|
1582
|
-
className="w-full flex items-center gap-2 px-3 py-2 bg-purple/10 hover:bg-purple/20 transition-colors"
|
|
1583
|
-
onClick={() => setExpanded(!expanded)}
|
|
1584
|
-
>
|
|
1585
|
-
<ChevronRight
|
|
1586
|
-
className={`size-3 text-purple transition-transform shrink-0 ${
|
|
1587
|
-
expanded ? 'rotate-90' : ''
|
|
1588
|
-
}`}
|
|
1589
|
-
/>
|
|
1590
|
-
<Wrench className="size-3 text-purple shrink-0" />
|
|
1591
|
-
<span className="text-xs font-mono font-medium text-purple">
|
|
1592
|
-
{toolName}
|
|
1593
|
-
</span>
|
|
1594
|
-
{!expanded && parsedArgs && (
|
|
1595
|
-
<span className="text-[11px] font-mono text-purple/70 truncate">
|
|
1596
|
-
{formatToolParams(parsedArgs)}
|
|
1597
|
-
</span>
|
|
1598
|
-
)}
|
|
1599
|
-
</button>
|
|
1600
|
-
|
|
1601
|
-
{expanded && (
|
|
1602
|
-
<>
|
|
1603
|
-
{/* Arguments */}
|
|
1604
|
-
<div className="p-3 bg-card/50 border-t border-purple/30">
|
|
1605
|
-
<div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mb-2">
|
|
1606
|
-
Input
|
|
1607
|
-
</div>
|
|
1608
|
-
<JsonBlock data={parsedArgs} />
|
|
1609
|
-
</div>
|
|
1610
|
-
|
|
1611
|
-
{/* Result */}
|
|
1612
|
-
{parsedResult && (
|
|
1613
|
-
<div className="p-3 border-t border-border bg-success/5">
|
|
1614
|
-
<div className="text-[10px] font-medium uppercase tracking-wider text-success mb-2">
|
|
1615
|
-
Output
|
|
1616
|
-
</div>
|
|
1617
|
-
<JsonBlock data={parsedResult} />
|
|
1618
|
-
</div>
|
|
1619
|
-
)}
|
|
1620
|
-
</>
|
|
1621
|
-
)}
|
|
1622
|
-
</div>
|
|
1623
|
-
);
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
function ReasoningBlock({ content }: { content: string }) {
|
|
1627
|
-
const [expanded, setExpanded] = useState(false);
|
|
1628
|
-
|
|
1629
|
-
// Truncate preview to first 200 characters
|
|
1630
|
-
const previewContent =
|
|
1631
|
-
content.length > 200 ? content.slice(0, 200) + '…' : content;
|
|
1632
|
-
|
|
1633
|
-
return (
|
|
1634
|
-
<div className="rounded-md border border-amber-500/30 overflow-hidden">
|
|
1635
|
-
<button
|
|
1636
|
-
className="w-full flex items-center gap-2 px-3 py-2 bg-amber-500/10 hover:bg-amber-500/20 transition-colors"
|
|
1637
|
-
onClick={() => setExpanded(!expanded)}
|
|
1638
|
-
>
|
|
1639
|
-
<ChevronRight
|
|
1640
|
-
className={`size-3 text-amber-500 transition-transform shrink-0 ${
|
|
1641
|
-
expanded ? 'rotate-90' : ''
|
|
1642
|
-
}`}
|
|
1643
|
-
/>
|
|
1644
|
-
<Brain className="size-3 text-amber-500 shrink-0" />
|
|
1645
|
-
<span className="text-xs font-medium text-amber-500">Thinking</span>
|
|
1646
|
-
{!expanded && (
|
|
1647
|
-
<span className="text-[11px] text-amber-500/70 truncate ml-1">
|
|
1648
|
-
{previewContent}
|
|
1649
|
-
</span>
|
|
1650
|
-
)}
|
|
1651
|
-
</button>
|
|
1652
|
-
|
|
1653
|
-
{expanded && (
|
|
1654
|
-
<div className="p-3 bg-card/50 border-t border-amber-500/30">
|
|
1655
|
-
<div className="text-xs text-foreground/80 leading-relaxed whitespace-pre-wrap">
|
|
1656
|
-
{content}
|
|
1657
|
-
</div>
|
|
1658
|
-
</div>
|
|
1659
|
-
)}
|
|
1660
|
-
</div>
|
|
1661
|
-
);
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
function TextBlock({
|
|
1665
|
-
content,
|
|
1666
|
-
defaultExpanded = false,
|
|
1667
|
-
isSystem = false,
|
|
1668
|
-
}: {
|
|
1669
|
-
content: string;
|
|
1670
|
-
defaultExpanded?: boolean;
|
|
1671
|
-
isSystem?: boolean;
|
|
1672
|
-
}) {
|
|
1673
|
-
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
1674
|
-
const [copied, setCopied] = useState(false);
|
|
1675
|
-
|
|
1676
|
-
// Truncate preview to first 200 characters
|
|
1677
|
-
const previewContent =
|
|
1678
|
-
content.length > 200 ? content.slice(0, 200) + '…' : content;
|
|
1679
|
-
|
|
1680
|
-
const borderColor = isSystem ? 'border-blue-500/30' : 'border-border';
|
|
1681
|
-
const bgColor = isSystem ? 'bg-blue-500/10' : 'bg-muted/30';
|
|
1682
|
-
const hoverBgColor = isSystem ? 'hover:bg-blue-500/20' : 'hover:bg-muted/50';
|
|
1683
|
-
const iconColor = isSystem ? 'text-blue-400' : 'text-muted-foreground';
|
|
1684
|
-
const labelColor = isSystem ? 'text-blue-400' : 'text-foreground';
|
|
1685
|
-
|
|
1686
|
-
const handleCopy = async (e: React.MouseEvent) => {
|
|
1687
|
-
e.stopPropagation();
|
|
1688
|
-
await navigator.clipboard.writeText(content);
|
|
1689
|
-
setCopied(true);
|
|
1690
|
-
setTimeout(() => setCopied(false), 2000);
|
|
1691
|
-
};
|
|
1692
|
-
|
|
1693
|
-
return (
|
|
1694
|
-
<div className={`rounded-md border ${borderColor} overflow-hidden`}>
|
|
1695
|
-
<button
|
|
1696
|
-
className={`w-full flex items-center gap-2 px-3 py-2 ${bgColor} ${hoverBgColor} transition-colors`}
|
|
1697
|
-
onClick={() => setExpanded(!expanded)}
|
|
1698
|
-
>
|
|
1699
|
-
<ChevronRight
|
|
1700
|
-
className={`size-3 ${iconColor} transition-transform shrink-0 ${
|
|
1701
|
-
expanded ? 'rotate-90' : ''
|
|
1702
|
-
}`}
|
|
1703
|
-
/>
|
|
1704
|
-
<MessageSquare className={`size-3 ${iconColor} shrink-0`} />
|
|
1705
|
-
<span className={`text-xs font-medium ${labelColor}`}>Text</span>
|
|
1706
|
-
{!expanded && (
|
|
1707
|
-
<span
|
|
1708
|
-
className={`text-[11px] ${isSystem ? 'text-blue-400/70' : 'text-muted-foreground'} truncate ml-1`}
|
|
1709
|
-
>
|
|
1710
|
-
{previewContent}
|
|
1711
|
-
</span>
|
|
1712
|
-
)}
|
|
1713
|
-
</button>
|
|
1714
|
-
|
|
1715
|
-
{expanded && (
|
|
1716
|
-
<div
|
|
1717
|
-
className={`p-3 bg-card/50 border-t ${borderColor} group relative`}
|
|
1718
|
-
>
|
|
1719
|
-
<button
|
|
1720
|
-
onClick={handleCopy}
|
|
1721
|
-
className="absolute top-1.5 right-1.5 p-1.5 rounded-md border border-border bg-background opacity-0 group-hover:opacity-100 transition-opacity z-10"
|
|
1722
|
-
title="Copy to clipboard"
|
|
1723
|
-
>
|
|
1724
|
-
{copied ? (
|
|
1725
|
-
<Check className="size-3 text-success" />
|
|
1726
|
-
) : (
|
|
1727
|
-
<Copy className="size-3 text-muted-foreground" />
|
|
1728
|
-
)}
|
|
1729
|
-
</button>
|
|
1730
|
-
<div className="text-xs text-foreground leading-relaxed whitespace-pre-wrap max-h-60 overflow-y-auto">
|
|
1731
|
-
{content}
|
|
1732
|
-
</div>
|
|
1733
|
-
</div>
|
|
1734
|
-
)}
|
|
1735
|
-
</div>
|
|
1736
|
-
);
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
function JsonBlock({
|
|
1740
|
-
data,
|
|
1741
|
-
compact = false,
|
|
1742
|
-
size = 'sm',
|
|
1743
|
-
}: {
|
|
1744
|
-
data: any;
|
|
1745
|
-
compact?: boolean;
|
|
1746
|
-
size?: 'sm' | 'base' | 'lg';
|
|
1747
|
-
}) {
|
|
1748
|
-
const [copied, setCopied] = useState(false);
|
|
1749
|
-
|
|
1750
|
-
const jsonString = JSON.stringify(data, null, 2);
|
|
1751
|
-
const displayString =
|
|
1752
|
-
compact && jsonString.length > 200 ? JSON.stringify(data) : jsonString;
|
|
1753
|
-
|
|
1754
|
-
const sizeClasses = {
|
|
1755
|
-
sm: 'text-xs',
|
|
1756
|
-
base: 'text-sm',
|
|
1757
|
-
lg: 'text-base',
|
|
1758
|
-
};
|
|
1759
|
-
|
|
1760
|
-
const handleCopy = async () => {
|
|
1761
|
-
await navigator.clipboard.writeText(jsonString);
|
|
1762
|
-
setCopied(true);
|
|
1763
|
-
setTimeout(() => setCopied(false), 2000);
|
|
1764
|
-
};
|
|
1765
|
-
|
|
1766
|
-
return (
|
|
1767
|
-
<div className="relative group">
|
|
1768
|
-
<button
|
|
1769
|
-
onClick={handleCopy}
|
|
1770
|
-
className="absolute top-1.5 right-1.5 p-1.5 rounded-md border border-border bg-background opacity-0 group-hover:opacity-100 transition-opacity z-10"
|
|
1771
|
-
title="Copy to clipboard"
|
|
1772
|
-
>
|
|
1773
|
-
{copied ? (
|
|
1774
|
-
<Check className="size-3 text-success" />
|
|
1775
|
-
) : (
|
|
1776
|
-
<Copy className="size-3 text-muted-foreground" />
|
|
1777
|
-
)}
|
|
1778
|
-
</button>
|
|
1779
|
-
<pre
|
|
1780
|
-
className={`font-mono ${sizeClasses[size]} text-muted-foreground whitespace-pre-wrap wrap-break-words bg-background rounded p-2 ${
|
|
1781
|
-
compact ? 'max-h-20 overflow-hidden' : ''
|
|
1782
|
-
}`}
|
|
1783
|
-
>
|
|
1784
|
-
{displayString}
|
|
1785
|
-
</pre>
|
|
1786
|
-
</div>
|
|
1787
|
-
);
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
function RawDataSection({
|
|
1791
|
-
rawRequest,
|
|
1792
|
-
rawResponse,
|
|
1793
|
-
rawChunks,
|
|
1794
|
-
isStream,
|
|
1795
|
-
}: {
|
|
1796
|
-
rawRequest: string | null;
|
|
1797
|
-
rawResponse: string | null;
|
|
1798
|
-
rawChunks: string | null;
|
|
1799
|
-
isStream: boolean;
|
|
1800
|
-
}) {
|
|
1801
|
-
const [expanded, setExpanded] = useState(false);
|
|
1802
|
-
const [responseView, setResponseView] = useState<'parsed' | 'raw'>('parsed');
|
|
1803
|
-
|
|
1804
|
-
if (!rawRequest && !rawResponse) return null;
|
|
1805
|
-
|
|
1806
|
-
const hasRawChunks = isStream && rawChunks;
|
|
1807
|
-
|
|
1808
|
-
return (
|
|
1809
|
-
<div className="border-t border-border">
|
|
1810
|
-
<button
|
|
1811
|
-
className="w-full flex items-center gap-2 px-4 py-2.5 text-[11px] text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
|
|
1812
|
-
onClick={() => setExpanded(!expanded)}
|
|
1813
|
-
>
|
|
1814
|
-
<ChevronRight
|
|
1815
|
-
className={`size-3 transition-transform ${expanded ? 'rotate-90' : ''}`}
|
|
1816
|
-
/>
|
|
1817
|
-
<span className="font-medium uppercase tracking-wider">
|
|
1818
|
-
Request / Response
|
|
1819
|
-
</span>
|
|
1820
|
-
</button>
|
|
1821
|
-
|
|
1822
|
-
{expanded && (
|
|
1823
|
-
<div className="px-4 pb-4 grid grid-cols-2 gap-4">
|
|
1824
|
-
{rawRequest && (
|
|
1825
|
-
<div className="flex flex-col">
|
|
1826
|
-
<div className="h-7 flex items-end mb-2">
|
|
1827
|
-
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
1828
|
-
Request
|
|
1829
|
-
</span>
|
|
1830
|
-
</div>
|
|
1831
|
-
<div className="max-h-[400px] overflow-auto rounded-md border border-border">
|
|
1832
|
-
<JsonBlock data={safeParseJson(rawRequest)} />
|
|
1833
|
-
</div>
|
|
1834
|
-
</div>
|
|
1835
|
-
)}
|
|
1836
|
-
{(rawResponse || rawChunks) && (
|
|
1837
|
-
<div className="flex flex-col">
|
|
1838
|
-
<div className="h-7 flex items-end justify-between mb-2">
|
|
1839
|
-
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
1840
|
-
{isStream ? 'Stream' : 'Response'}
|
|
1841
|
-
</span>
|
|
1842
|
-
{hasRawChunks && (
|
|
1843
|
-
<div className="inline-flex rounded-md border border-border bg-muted/30 p-0.5">
|
|
1844
|
-
<button
|
|
1845
|
-
onClick={() => setResponseView('parsed')}
|
|
1846
|
-
className={`px-2 py-0.5 text-[10px] font-medium rounded transition-colors ${
|
|
1847
|
-
responseView === 'parsed'
|
|
1848
|
-
? 'bg-background text-foreground shadow-sm'
|
|
1849
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
1850
|
-
}`}
|
|
1851
|
-
>
|
|
1852
|
-
AI SDK
|
|
1853
|
-
</button>
|
|
1854
|
-
<button
|
|
1855
|
-
onClick={() => setResponseView('raw')}
|
|
1856
|
-
className={`px-2 py-0.5 text-[10px] font-medium rounded transition-colors ${
|
|
1857
|
-
responseView === 'raw'
|
|
1858
|
-
? 'bg-background text-foreground shadow-sm'
|
|
1859
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
1860
|
-
}`}
|
|
1861
|
-
>
|
|
1862
|
-
Provider
|
|
1863
|
-
</button>
|
|
1864
|
-
</div>
|
|
1865
|
-
)}
|
|
1866
|
-
</div>
|
|
1867
|
-
<div className="max-h-[400px] overflow-auto rounded-md border border-border">
|
|
1868
|
-
<JsonBlock
|
|
1869
|
-
data={safeParseJson(
|
|
1870
|
-
hasRawChunks && responseView === 'raw'
|
|
1871
|
-
? rawChunks
|
|
1872
|
-
: rawResponse,
|
|
1873
|
-
)}
|
|
1874
|
-
/>
|
|
1875
|
-
</div>
|
|
1876
|
-
</div>
|
|
1877
|
-
)}
|
|
1878
|
-
</div>
|
|
1879
|
-
)}
|
|
1880
|
-
</div>
|
|
1881
|
-
);
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
function UsageDetails({ usage }: { usage: any }) {
|
|
1885
|
-
const inputBreakdown = getInputTokenBreakdown(usage?.inputTokens);
|
|
1886
|
-
const outputBreakdown = getOutputTokenBreakdown(usage?.outputTokens);
|
|
1887
|
-
|
|
1888
|
-
return (
|
|
1889
|
-
<div className="space-y-6">
|
|
1890
|
-
{/* Summary Cards */}
|
|
1891
|
-
<div className="grid grid-cols-2 gap-4">
|
|
1892
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
1893
|
-
<div className="text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2">
|
|
1894
|
-
Input Tokens
|
|
1895
|
-
</div>
|
|
1896
|
-
<div className="text-2xl font-semibold">{inputBreakdown.total}</div>
|
|
1897
|
-
{(inputBreakdown.cacheRead !== undefined ||
|
|
1898
|
-
inputBreakdown.cacheWrite !== undefined) && (
|
|
1899
|
-
<div className="mt-3 space-y-1 text-sm text-muted-foreground">
|
|
1900
|
-
{inputBreakdown.cacheRead !== undefined && (
|
|
1901
|
-
<div className="flex justify-between">
|
|
1902
|
-
<span>Cache read</span>
|
|
1903
|
-
<span className="font-mono">{inputBreakdown.cacheRead}</span>
|
|
1904
|
-
</div>
|
|
1905
|
-
)}
|
|
1906
|
-
{inputBreakdown.cacheWrite !== undefined && (
|
|
1907
|
-
<div className="flex justify-between">
|
|
1908
|
-
<span>Cache write</span>
|
|
1909
|
-
<span className="font-mono">{inputBreakdown.cacheWrite}</span>
|
|
1910
|
-
</div>
|
|
1911
|
-
)}
|
|
1912
|
-
{inputBreakdown.noCache !== undefined && (
|
|
1913
|
-
<div className="flex justify-between">
|
|
1914
|
-
<span>No cache</span>
|
|
1915
|
-
<span className="font-mono">{inputBreakdown.noCache}</span>
|
|
1916
|
-
</div>
|
|
1917
|
-
)}
|
|
1918
|
-
</div>
|
|
1919
|
-
)}
|
|
1920
|
-
</div>
|
|
1921
|
-
|
|
1922
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
1923
|
-
<div className="text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2">
|
|
1924
|
-
Output Tokens
|
|
1925
|
-
</div>
|
|
1926
|
-
<div className="text-2xl font-semibold">{outputBreakdown.total}</div>
|
|
1927
|
-
{(outputBreakdown.text !== undefined ||
|
|
1928
|
-
outputBreakdown.reasoning !== undefined) && (
|
|
1929
|
-
<div className="mt-3 space-y-1 text-sm text-muted-foreground">
|
|
1930
|
-
{outputBreakdown.text !== undefined && (
|
|
1931
|
-
<div className="flex justify-between">
|
|
1932
|
-
<span>Text</span>
|
|
1933
|
-
<span className="font-mono">{outputBreakdown.text}</span>
|
|
1934
|
-
</div>
|
|
1935
|
-
)}
|
|
1936
|
-
{outputBreakdown.reasoning !== undefined && (
|
|
1937
|
-
<div className="flex justify-between">
|
|
1938
|
-
<span>Reasoning</span>
|
|
1939
|
-
<span className="font-mono">{outputBreakdown.reasoning}</span>
|
|
1940
|
-
</div>
|
|
1941
|
-
)}
|
|
1942
|
-
</div>
|
|
1943
|
-
)}
|
|
1944
|
-
</div>
|
|
1945
|
-
</div>
|
|
1946
|
-
|
|
1947
|
-
{/* Raw Usage Data */}
|
|
1948
|
-
{usage?.raw && (
|
|
1949
|
-
<div>
|
|
1950
|
-
<div className="text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2">
|
|
1951
|
-
Raw Provider Usage
|
|
1952
|
-
</div>
|
|
1953
|
-
<JsonBlock data={usage.raw} size="sm" />
|
|
1954
|
-
</div>
|
|
1955
|
-
)}
|
|
1956
|
-
|
|
1957
|
-
{/* Full Usage Object */}
|
|
1958
|
-
<div>
|
|
1959
|
-
<div className="text-xs font-medium uppercase tracking-wider text-muted-foreground mb-2">
|
|
1960
|
-
Full Usage Object
|
|
1961
|
-
</div>
|
|
1962
|
-
<JsonBlock data={usage} size="sm" />
|
|
1963
|
-
</div>
|
|
1964
|
-
</div>
|
|
1965
|
-
);
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
function TokenBreakdownTooltip({
|
|
1969
|
-
input,
|
|
1970
|
-
output,
|
|
1971
|
-
raw,
|
|
1972
|
-
}: {
|
|
1973
|
-
input: InputTokenBreakdown;
|
|
1974
|
-
output: OutputTokenBreakdown;
|
|
1975
|
-
raw?: unknown;
|
|
1976
|
-
}) {
|
|
1977
|
-
// Check if we have any breakdown metadata (even if values are 0)
|
|
1978
|
-
const hasInputBreakdown =
|
|
1979
|
-
input.noCache !== undefined ||
|
|
1980
|
-
input.cacheRead !== undefined ||
|
|
1981
|
-
input.cacheWrite !== undefined;
|
|
1982
|
-
const hasOutputBreakdown =
|
|
1983
|
-
output.text !== undefined || output.reasoning !== undefined;
|
|
1984
|
-
const hasBreakdown = hasInputBreakdown || hasOutputBreakdown;
|
|
1985
|
-
|
|
1986
|
-
if (!hasBreakdown) {
|
|
1987
|
-
return (
|
|
1988
|
-
<div className="text-xs">
|
|
1989
|
-
<div>Input: {input.total}</div>
|
|
1990
|
-
<div>Output: {output.total}</div>
|
|
1991
|
-
</div>
|
|
1992
|
-
);
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
return (
|
|
1996
|
-
<div className="text-xs space-y-2">
|
|
1997
|
-
<div>
|
|
1998
|
-
<div className="font-medium mb-1">Input: {input.total}</div>
|
|
1999
|
-
{input.cacheRead !== undefined && (
|
|
2000
|
-
<div className="text-muted-foreground ml-2">
|
|
2001
|
-
Cache read: {input.cacheRead}
|
|
2002
|
-
</div>
|
|
2003
|
-
)}
|
|
2004
|
-
{input.cacheWrite !== undefined && (
|
|
2005
|
-
<div className="text-muted-foreground ml-2">
|
|
2006
|
-
Cache write: {input.cacheWrite}
|
|
2007
|
-
</div>
|
|
2008
|
-
)}
|
|
2009
|
-
</div>
|
|
2010
|
-
<div>
|
|
2011
|
-
<div className="font-medium mb-1">Output: {output.total}</div>
|
|
2012
|
-
{output.text !== undefined && (
|
|
2013
|
-
<div className="text-muted-foreground ml-2">Text: {output.text}</div>
|
|
2014
|
-
)}
|
|
2015
|
-
{output.reasoning !== undefined && (
|
|
2016
|
-
<div className="text-muted-foreground ml-2">
|
|
2017
|
-
Reasoning: {output.reasoning}
|
|
2018
|
-
</div>
|
|
2019
|
-
)}
|
|
2020
|
-
</div>
|
|
2021
|
-
{raw !== undefined && (
|
|
2022
|
-
<div className="pt-1 border-t border-border mt-1">
|
|
2023
|
-
<div className="text-muted-foreground/70 font-mono text-[10px] max-w-[200px] truncate">
|
|
2024
|
-
Raw: {JSON.stringify(raw)}
|
|
2025
|
-
</div>
|
|
2026
|
-
</div>
|
|
2027
|
-
)}
|
|
2028
|
-
</div>
|
|
2029
|
-
);
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
function safeParseJson(value: any): any {
|
|
2033
|
-
if (typeof value === 'string') {
|
|
2034
|
-
try {
|
|
2035
|
-
return JSON.parse(value);
|
|
2036
|
-
} catch {
|
|
2037
|
-
return value;
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
return value;
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
/**
|
|
2044
|
-
* Token usage can be either a number (old format) or an object with breakdown (ai@6.0.0-beta.139+).
|
|
2045
|
-
* Old format: inputTokens: 4456
|
|
2046
|
-
* New format for input: { total: 4456, noCache: 9, cacheRead: 4100, cacheWrite: 347 }
|
|
2047
|
-
* New format for output: { total: 1262, text: 1262, reasoning: 0 }
|
|
2048
|
-
*/
|
|
2049
|
-
interface InputTokenBreakdown {
|
|
2050
|
-
total: number;
|
|
2051
|
-
noCache?: number;
|
|
2052
|
-
cacheRead?: number;
|
|
2053
|
-
cacheWrite?: number;
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
interface OutputTokenBreakdown {
|
|
2057
|
-
total: number;
|
|
2058
|
-
text?: number;
|
|
2059
|
-
reasoning?: number;
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
function getInputTokenBreakdown(
|
|
2063
|
-
tokens: number | InputTokenBreakdown | null | undefined,
|
|
2064
|
-
): InputTokenBreakdown {
|
|
2065
|
-
if (tokens == null) return { total: 0 };
|
|
2066
|
-
if (typeof tokens === 'number') return { total: tokens };
|
|
2067
|
-
if (typeof tokens === 'object') {
|
|
2068
|
-
// Handle case where total might be missing or not a number
|
|
2069
|
-
const total =
|
|
2070
|
-
'total' in tokens && typeof tokens.total === 'number' ? tokens.total : 0;
|
|
2071
|
-
return {
|
|
2072
|
-
total,
|
|
2073
|
-
// Only include cache fields if they are actual numbers
|
|
2074
|
-
...(typeof tokens.noCache === 'number' && { noCache: tokens.noCache }),
|
|
2075
|
-
...(typeof tokens.cacheRead === 'number' && {
|
|
2076
|
-
cacheRead: tokens.cacheRead,
|
|
2077
|
-
}),
|
|
2078
|
-
...(typeof tokens.cacheWrite === 'number' && {
|
|
2079
|
-
cacheWrite: tokens.cacheWrite,
|
|
2080
|
-
}),
|
|
2081
|
-
};
|
|
2082
|
-
}
|
|
2083
|
-
return { total: 0 };
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
function getOutputTokenBreakdown(
|
|
2087
|
-
tokens: number | OutputTokenBreakdown | null | undefined,
|
|
2088
|
-
): OutputTokenBreakdown {
|
|
2089
|
-
if (tokens == null) return { total: 0 };
|
|
2090
|
-
if (typeof tokens === 'number') return { total: tokens };
|
|
2091
|
-
if (typeof tokens === 'object') {
|
|
2092
|
-
// Handle case where total might be missing or not a number
|
|
2093
|
-
const total =
|
|
2094
|
-
'total' in tokens && typeof tokens.total === 'number' ? tokens.total : 0;
|
|
2095
|
-
return {
|
|
2096
|
-
total,
|
|
2097
|
-
// Only include text/reasoning if they are actual numbers
|
|
2098
|
-
...(typeof tokens.text === 'number' && { text: tokens.text }),
|
|
2099
|
-
...(typeof tokens.reasoning === 'number' && {
|
|
2100
|
-
reasoning: tokens.reasoning,
|
|
2101
|
-
}),
|
|
2102
|
-
};
|
|
2103
|
-
}
|
|
2104
|
-
return { total: 0 };
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
/**
|
|
2108
|
-
* Formats input token count with cache info in parentheses if available.
|
|
2109
|
-
* e.g., "4456" or "4456 (4100 cached)"
|
|
2110
|
-
*/
|
|
2111
|
-
function formatInputTokens(breakdown: InputTokenBreakdown): string {
|
|
2112
|
-
const { total, cacheRead } = breakdown;
|
|
2113
|
-
if (cacheRead && cacheRead > 0) {
|
|
2114
|
-
return `${total} (${cacheRead} cached)`;
|
|
2115
|
-
}
|
|
2116
|
-
return String(total);
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
/**
|
|
2120
|
-
* Formats output token count with reasoning info if available.
|
|
2121
|
-
* e.g., "1054" or "1054 (500 reasoning)"
|
|
2122
|
-
*/
|
|
2123
|
-
function formatOutputTokens(breakdown: OutputTokenBreakdown): string {
|
|
2124
|
-
const { total, reasoning } = breakdown;
|
|
2125
|
-
if (reasoning && reasoning > 0) {
|
|
2126
|
-
return `${total} (${reasoning} reasoning)`;
|
|
2127
|
-
}
|
|
2128
|
-
return String(total);
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
|
-
/**
|
|
2132
|
-
* Truncates tool call parameters for preview display.
|
|
2133
|
-
*/
|
|
2134
|
-
function formatToolParams(args: any, maxLength = 40): string {
|
|
2135
|
-
if (!args || typeof args !== 'object') return '';
|
|
2136
|
-
|
|
2137
|
-
const entries = Object.entries(args);
|
|
2138
|
-
if (entries.length === 0) return '';
|
|
2139
|
-
|
|
2140
|
-
const formatValue = (value: any, maxLen = 20): string => {
|
|
2141
|
-
if (value === null) return 'null';
|
|
2142
|
-
if (value === undefined) return 'undefined';
|
|
2143
|
-
if (typeof value === 'object') {
|
|
2144
|
-
if (Array.isArray(value)) {
|
|
2145
|
-
return `[${value.length}]`;
|
|
2146
|
-
}
|
|
2147
|
-
return '{…}';
|
|
2148
|
-
}
|
|
2149
|
-
if (typeof value === 'string') {
|
|
2150
|
-
if (value.length > maxLen) {
|
|
2151
|
-
return `"${value.slice(0, maxLen)}…"`;
|
|
2152
|
-
}
|
|
2153
|
-
return `"${value}"`;
|
|
2154
|
-
}
|
|
2155
|
-
return String(value);
|
|
2156
|
-
};
|
|
2157
|
-
|
|
2158
|
-
const firstEntry = entries[0];
|
|
2159
|
-
if (!firstEntry) return '';
|
|
2160
|
-
|
|
2161
|
-
const [firstKey, firstValue] = firstEntry;
|
|
2162
|
-
const firstFormatted = `${firstKey}: ${formatValue(firstValue)}`;
|
|
2163
|
-
|
|
2164
|
-
if (entries.length === 1) {
|
|
2165
|
-
return `{ ${firstFormatted} }`;
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
return `{ ${firstFormatted}, … }`;
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
/**
|
|
2172
|
-
* Formats tool params inline for JS-like syntax: { key: "value", … }
|
|
2173
|
-
*/
|
|
2174
|
-
function formatToolParamsInline(args: any): string {
|
|
2175
|
-
if (!args || typeof args !== 'object') return '';
|
|
2176
|
-
|
|
2177
|
-
const entries = Object.entries(args);
|
|
2178
|
-
if (entries.length === 0) return '';
|
|
2179
|
-
|
|
2180
|
-
const formatValue = (value: any, maxLen = 20): string => {
|
|
2181
|
-
if (value === null) return 'null';
|
|
2182
|
-
if (value === undefined) return 'undefined';
|
|
2183
|
-
if (typeof value === 'object') {
|
|
2184
|
-
if (Array.isArray(value)) {
|
|
2185
|
-
return `[${value.length}]`;
|
|
2186
|
-
}
|
|
2187
|
-
return '{…}';
|
|
2188
|
-
}
|
|
2189
|
-
if (typeof value === 'string') {
|
|
2190
|
-
if (value.length > maxLen) {
|
|
2191
|
-
return `"${value.slice(0, maxLen)}…"`;
|
|
2192
|
-
}
|
|
2193
|
-
return `"${value}"`;
|
|
2194
|
-
}
|
|
2195
|
-
return String(value);
|
|
2196
|
-
};
|
|
2197
|
-
|
|
2198
|
-
const firstEntry = entries[0];
|
|
2199
|
-
if (!firstEntry) return '';
|
|
2200
|
-
|
|
2201
|
-
const [firstKey, firstValue] = firstEntry;
|
|
2202
|
-
const firstFormatted = `${firstKey}: ${formatValue(firstValue)}`;
|
|
2203
|
-
|
|
2204
|
-
if (entries.length === 1) {
|
|
2205
|
-
return `{ ${firstFormatted} }`;
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
return `{ ${firstFormatted}, … }`;
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
/**
|
|
2212
|
-
* Formats tool result for preview display in JS-like syntax
|
|
2213
|
-
*/
|
|
2214
|
-
function formatResultPreview(result: any): string {
|
|
2215
|
-
if (result === null) return 'null';
|
|
2216
|
-
if (result === undefined) return 'undefined';
|
|
2217
|
-
|
|
2218
|
-
if (typeof result === 'string') {
|
|
2219
|
-
if (result.length > 30) {
|
|
2220
|
-
return `"${result.slice(0, 30)}…"`;
|
|
2221
|
-
}
|
|
2222
|
-
return `"${result}"`;
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
if (Array.isArray(result)) {
|
|
2226
|
-
return `[${result.length}]`;
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
if (typeof result === 'object') {
|
|
2230
|
-
const entries = Object.entries(result);
|
|
2231
|
-
if (entries.length === 0) return '{}';
|
|
2232
|
-
|
|
2233
|
-
const firstEntry = entries[0];
|
|
2234
|
-
if (!firstEntry) return '{…}';
|
|
2235
|
-
|
|
2236
|
-
const [firstKey, firstValue] = firstEntry;
|
|
2237
|
-
let valueStr: string;
|
|
2238
|
-
if (typeof firstValue === 'string' && firstValue.length > 15) {
|
|
2239
|
-
valueStr = `"${firstValue.slice(0, 15)}…"`;
|
|
2240
|
-
} else if (typeof firstValue === 'string') {
|
|
2241
|
-
valueStr = `"${firstValue}"`;
|
|
2242
|
-
} else if (typeof firstValue === 'object') {
|
|
2243
|
-
valueStr = Array.isArray(firstValue) ? `[${firstValue.length}]` : '{…}';
|
|
2244
|
-
} else {
|
|
2245
|
-
valueStr = String(firstValue);
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
if (entries.length === 1) {
|
|
2249
|
-
return `{ ${firstKey}: ${valueStr} }`;
|
|
2250
|
-
}
|
|
2251
|
-
return `{ ${firstKey}: ${valueStr}, … }`;
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
return String(result);
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
476
|
export default App;
|