@ai-sdk/devtools 1.0.8 → 1.0.10
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-CltCAda7.css +1 -0
- package/dist/client/assets/index-DgZG5_rI.js +220 -0
- package/dist/client/index.html +2 -2
- package/dist/index.js +91 -9
- package/package.json +3 -3
- package/src/integration.ts +4 -3
- package/src/middleware.ts +13 -6
- package/src/serialize.ts +111 -0
- package/src/viewer/client/components/media-components.tsx +127 -0
- package/src/viewer/client/components/message-components.tsx +34 -4
- package/src/viewer/client/components/output-components.tsx +26 -6
- package/src/viewer/client/components/shared-components.tsx +25 -4
- package/src/viewer/client/components/step-card.tsx +5 -2
- package/src/viewer/client/components/trace-timeline.tsx +16 -4
- package/src/viewer/client/media.ts +440 -0
- package/src/viewer/client/types.ts +19 -1
- package/src/viewer/client/utils.ts +44 -0
- package/dist/client/assets/index-Beo4T2zg.js +0 -200
- package/dist/client/assets/index-CIZI8r26.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600;700&display=swap"
|
|
11
11
|
rel="stylesheet"
|
|
12
12
|
/>
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-DgZG5_rI.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CltCAda7.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/dist/index.js
CHANGED
|
@@ -122,6 +122,83 @@ var getStepsForRun = async (runId) => {
|
|
|
122
122
|
return db.steps.filter((s) => s.run_id === runId).sort((a, b) => a.step_number - b.step_number);
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
+
// src/serialize.ts
|
|
126
|
+
function normalizeBinaryData(value) {
|
|
127
|
+
if (value instanceof ArrayBuffer) {
|
|
128
|
+
return Buffer.from(value).toString("base64");
|
|
129
|
+
}
|
|
130
|
+
if (ArrayBuffer.isView(value)) {
|
|
131
|
+
return Buffer.from(
|
|
132
|
+
value.buffer,
|
|
133
|
+
value.byteOffset,
|
|
134
|
+
value.byteLength
|
|
135
|
+
).toString("base64");
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
function isRecord(value) {
|
|
140
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
141
|
+
}
|
|
142
|
+
function collectMediaBinaryValues(value) {
|
|
143
|
+
const mediaBinaryValues = /* @__PURE__ */ new WeakSet();
|
|
144
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
145
|
+
const markBinary = (candidate) => {
|
|
146
|
+
if (candidate instanceof ArrayBuffer || ArrayBuffer.isView(candidate)) {
|
|
147
|
+
mediaBinaryValues.add(candidate);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const markGeneratedFile = (candidate) => {
|
|
151
|
+
if (!isRecord(candidate)) return;
|
|
152
|
+
if (Object.hasOwn(candidate, "uint8Array")) {
|
|
153
|
+
markBinary(candidate.uint8Array);
|
|
154
|
+
}
|
|
155
|
+
markBinary(candidate.uint8ArrayData);
|
|
156
|
+
};
|
|
157
|
+
const markDataContent = (candidate) => {
|
|
158
|
+
markBinary(candidate);
|
|
159
|
+
if (!isRecord(candidate)) return;
|
|
160
|
+
if (candidate.type === "data") {
|
|
161
|
+
markBinary(candidate.data);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const visit = (candidate) => {
|
|
165
|
+
if (candidate == null || typeof candidate !== "object" || candidate instanceof ArrayBuffer || ArrayBuffer.isView(candidate) || visited.has(candidate)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
visited.add(candidate);
|
|
169
|
+
if (isRecord(candidate) && typeof candidate.type === "string") {
|
|
170
|
+
switch (candidate.type) {
|
|
171
|
+
case "file":
|
|
172
|
+
case "reasoning-file":
|
|
173
|
+
markDataContent(candidate.data);
|
|
174
|
+
markGeneratedFile(candidate);
|
|
175
|
+
markGeneratedFile(candidate.file);
|
|
176
|
+
break;
|
|
177
|
+
case "image":
|
|
178
|
+
markDataContent(candidate.image);
|
|
179
|
+
break;
|
|
180
|
+
case "media":
|
|
181
|
+
case "file-data":
|
|
182
|
+
case "image-data":
|
|
183
|
+
markBinary(candidate.data);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const child of Array.isArray(candidate) ? candidate : Object.values(candidate)) {
|
|
188
|
+
visit(child);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
visit(value);
|
|
192
|
+
return mediaBinaryValues;
|
|
193
|
+
}
|
|
194
|
+
function serializeForDevTools(value) {
|
|
195
|
+
const mediaBinaryValues = collectMediaBinaryValues(value);
|
|
196
|
+
return JSON.stringify(value, function(key, serializedValue) {
|
|
197
|
+
const originalValue = this[key];
|
|
198
|
+
return originalValue != null && typeof originalValue === "object" && mediaBinaryValues.has(originalValue) ? normalizeBinaryData(originalValue) : serializedValue;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
125
202
|
// src/middleware.ts
|
|
126
203
|
var generateId = () => crypto.randomUUID();
|
|
127
204
|
var activeSteps = /* @__PURE__ */ new Map();
|
|
@@ -136,7 +213,7 @@ var registerSignalHandlers = () => {
|
|
|
136
213
|
const durationMs = Date.now() - data.startTime;
|
|
137
214
|
await updateStepResult(stepId, {
|
|
138
215
|
duration_ms: durationMs,
|
|
139
|
-
output:
|
|
216
|
+
output: serializeForDevTools(data.collectedOutput),
|
|
140
217
|
usage: null,
|
|
141
218
|
error: "Request aborted",
|
|
142
219
|
raw_request: data.request && typeof data.request === "object" && "body" in data.request ? JSON.stringify(data.request.body) : null,
|
|
@@ -197,7 +274,7 @@ var devToolsMiddleware = () => {
|
|
|
197
274
|
// @ts-expect-error broken type
|
|
198
275
|
provider: model.config?.provider,
|
|
199
276
|
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
200
|
-
input:
|
|
277
|
+
input: serializeForDevTools({
|
|
201
278
|
prompt: params.prompt,
|
|
202
279
|
tools: params.tools,
|
|
203
280
|
toolChoice: params.toolChoice,
|
|
@@ -217,7 +294,7 @@ var devToolsMiddleware = () => {
|
|
|
217
294
|
const durationMs = Date.now() - startTime;
|
|
218
295
|
await updateStepResult(stepId, {
|
|
219
296
|
duration_ms: durationMs,
|
|
220
|
-
output:
|
|
297
|
+
output: serializeForDevTools({
|
|
221
298
|
content: result.content,
|
|
222
299
|
finishReason: result.finishReason,
|
|
223
300
|
response: result.response
|
|
@@ -257,7 +334,7 @@ var devToolsMiddleware = () => {
|
|
|
257
334
|
// @ts-expect-error broken type
|
|
258
335
|
provider: model.config?.provider,
|
|
259
336
|
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
260
|
-
input:
|
|
337
|
+
input: serializeForDevTools({
|
|
261
338
|
prompt: params.prompt,
|
|
262
339
|
tools: params.tools,
|
|
263
340
|
toolChoice: params.toolChoice,
|
|
@@ -334,6 +411,11 @@ var devToolsMiddleware = () => {
|
|
|
334
411
|
case "tool-call":
|
|
335
412
|
collectedOutput.toolCalls.push(chunk);
|
|
336
413
|
break;
|
|
414
|
+
case "file":
|
|
415
|
+
case "reasoning-file":
|
|
416
|
+
case "tool-result":
|
|
417
|
+
(collectedOutput.content ??= []).push(chunk);
|
|
418
|
+
break;
|
|
337
419
|
case "finish":
|
|
338
420
|
collectedOutput.finishReason = chunk.finishReason;
|
|
339
421
|
collectedOutput.usage = chunk.usage;
|
|
@@ -346,7 +428,7 @@ var devToolsMiddleware = () => {
|
|
|
346
428
|
const durationMs = Date.now() - startTime;
|
|
347
429
|
await updateStepResult(stepId, {
|
|
348
430
|
duration_ms: durationMs,
|
|
349
|
-
output:
|
|
431
|
+
output: serializeForDevTools(collectedOutput),
|
|
350
432
|
usage: collectedOutput.usage ? JSON.stringify(collectedOutput.usage) : null,
|
|
351
433
|
error: null,
|
|
352
434
|
raw_request: request?.body ? JSON.stringify(request.body) : null,
|
|
@@ -360,7 +442,7 @@ var devToolsMiddleware = () => {
|
|
|
360
442
|
const durationMs = Date.now() - startTime;
|
|
361
443
|
await updateStepResult(stepId, {
|
|
362
444
|
duration_ms: durationMs,
|
|
363
|
-
output:
|
|
445
|
+
output: serializeForDevTools(collectedOutput),
|
|
364
446
|
usage: collectedOutput.usage ? JSON.stringify(collectedOutput.usage) : null,
|
|
365
447
|
error: "Request aborted",
|
|
366
448
|
raw_request: request?.body ? JSON.stringify(request.body) : null,
|
|
@@ -527,7 +609,7 @@ function DevToolsTelemetry(options = {}) {
|
|
|
527
609
|
model_id: stepStartEvent.modelId,
|
|
528
610
|
provider: stepStartEvent.provider ?? null,
|
|
529
611
|
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
530
|
-
input:
|
|
612
|
+
input: serializeForDevTools({
|
|
531
613
|
prompt,
|
|
532
614
|
tools: stepStartEvent.tools ? Object.entries(stepStartEvent.tools).map(([name, tool]) => ({
|
|
533
615
|
name,
|
|
@@ -566,7 +648,7 @@ function DevToolsTelemetry(options = {}) {
|
|
|
566
648
|
model_id: stepStartEvent.modelId,
|
|
567
649
|
provider: stepStartEvent.provider ?? null,
|
|
568
650
|
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
569
|
-
input:
|
|
651
|
+
input: serializeForDevTools({
|
|
570
652
|
prompt: stepStartEvent.promptMessages,
|
|
571
653
|
maxOutputTokens: state.settings.maxOutputTokens,
|
|
572
654
|
temperature: state.settings.temperature,
|
|
@@ -599,7 +681,7 @@ function DevToolsTelemetry(options = {}) {
|
|
|
599
681
|
};
|
|
600
682
|
await updateStepResult(stepState.stepId, {
|
|
601
683
|
duration_ms: durationMs,
|
|
602
|
-
output:
|
|
684
|
+
output: serializeForDevTools(output),
|
|
603
685
|
usage: stepResult.usage ? JSON.stringify(stepResult.usage) : null,
|
|
604
686
|
error: null,
|
|
605
687
|
raw_request: stepResult.request?.body ? JSON.stringify(stepResult.request.body) : null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/devtools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@hono/node-server": "^1.19.13",
|
|
28
28
|
"hono": "^4.12.25",
|
|
29
|
-
"@ai-sdk/provider": "4.0.
|
|
29
|
+
"@ai-sdk/provider": "4.0.5"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"vite": "^6.4.3",
|
|
56
56
|
"vitest": "^4.1.6",
|
|
57
57
|
"zod": "3.25.76",
|
|
58
|
-
"ai": "7.0.
|
|
58
|
+
"ai": "7.0.50"
|
|
59
59
|
},
|
|
60
60
|
"publishConfig": {
|
|
61
61
|
"access": "public",
|
package/src/integration.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
updateStepResult,
|
|
16
16
|
notifyServerAsync,
|
|
17
17
|
} from './db.js';
|
|
18
|
+
import { serializeForDevTools } from './serialize.js';
|
|
18
19
|
|
|
19
20
|
type OperationType = 'generate' | 'stream';
|
|
20
21
|
|
|
@@ -261,7 +262,7 @@ export function DevToolsTelemetry(
|
|
|
261
262
|
model_id: stepStartEvent.modelId,
|
|
262
263
|
provider: stepStartEvent.provider ?? null,
|
|
263
264
|
started_at: new Date().toISOString(),
|
|
264
|
-
input:
|
|
265
|
+
input: serializeForDevTools({
|
|
265
266
|
prompt,
|
|
266
267
|
tools: stepStartEvent.tools
|
|
267
268
|
? Object.entries(stepStartEvent.tools).map(([name, tool]) => ({
|
|
@@ -311,7 +312,7 @@ export function DevToolsTelemetry(
|
|
|
311
312
|
model_id: stepStartEvent.modelId,
|
|
312
313
|
provider: stepStartEvent.provider ?? null,
|
|
313
314
|
started_at: new Date().toISOString(),
|
|
314
|
-
input:
|
|
315
|
+
input: serializeForDevTools({
|
|
315
316
|
prompt: stepStartEvent.promptMessages,
|
|
316
317
|
maxOutputTokens: state.settings.maxOutputTokens,
|
|
317
318
|
temperature: state.settings.temperature,
|
|
@@ -353,7 +354,7 @@ export function DevToolsTelemetry(
|
|
|
353
354
|
|
|
354
355
|
await updateStepResult(stepState.stepId, {
|
|
355
356
|
duration_ms: durationMs,
|
|
356
|
-
output:
|
|
357
|
+
output: serializeForDevTools(output),
|
|
357
358
|
usage: stepResult.usage ? JSON.stringify(stepResult.usage) : null,
|
|
358
359
|
error: null,
|
|
359
360
|
raw_request: stepResult.request?.body
|
package/src/middleware.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
updateStepResult,
|
|
11
11
|
notifyServerAsync,
|
|
12
12
|
} from './db.js';
|
|
13
|
+
import { serializeForDevTools } from './serialize.js';
|
|
13
14
|
|
|
14
15
|
const generateId = () => crypto.randomUUID();
|
|
15
16
|
|
|
@@ -39,7 +40,7 @@ const registerSignalHandlers = () => {
|
|
|
39
40
|
const durationMs = Date.now() - data.startTime;
|
|
40
41
|
await updateStepResult(stepId, {
|
|
41
42
|
duration_ms: durationMs,
|
|
42
|
-
output:
|
|
43
|
+
output: serializeForDevTools(data.collectedOutput),
|
|
43
44
|
usage: null,
|
|
44
45
|
error: 'Request aborted',
|
|
45
46
|
raw_request:
|
|
@@ -143,7 +144,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
|
|
|
143
144
|
// @ts-expect-error broken type
|
|
144
145
|
provider: model.config?.provider,
|
|
145
146
|
started_at: new Date().toISOString(),
|
|
146
|
-
input:
|
|
147
|
+
input: serializeForDevTools({
|
|
147
148
|
prompt: params.prompt,
|
|
148
149
|
tools: params.tools,
|
|
149
150
|
toolChoice: params.toolChoice,
|
|
@@ -167,7 +168,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
|
|
|
167
168
|
|
|
168
169
|
await updateStepResult(stepId, {
|
|
169
170
|
duration_ms: durationMs,
|
|
170
|
-
output:
|
|
171
|
+
output: serializeForDevTools({
|
|
171
172
|
content: result.content,
|
|
172
173
|
finishReason: result.finishReason,
|
|
173
174
|
response: result.response,
|
|
@@ -217,7 +218,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
|
|
|
217
218
|
// @ts-expect-error broken type
|
|
218
219
|
provider: model.config?.provider,
|
|
219
220
|
started_at: new Date().toISOString(),
|
|
220
|
-
input:
|
|
221
|
+
input: serializeForDevTools({
|
|
221
222
|
prompt: params.prompt,
|
|
222
223
|
tools: params.tools,
|
|
223
224
|
toolChoice: params.toolChoice,
|
|
@@ -243,6 +244,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
|
|
|
243
244
|
textParts: Array<{ id: string; text: string }>;
|
|
244
245
|
reasoningParts: Array<{ id: string; text: string }>;
|
|
245
246
|
toolCalls: LanguageModelV4StreamPart[];
|
|
247
|
+
content?: LanguageModelV4StreamPart[];
|
|
246
248
|
finishReason?: LanguageModelV4FinishReason;
|
|
247
249
|
usage?: LanguageModelV4Usage;
|
|
248
250
|
} = {
|
|
@@ -319,6 +321,11 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
|
|
|
319
321
|
case 'tool-call':
|
|
320
322
|
collectedOutput.toolCalls.push(chunk);
|
|
321
323
|
break;
|
|
324
|
+
case 'file':
|
|
325
|
+
case 'reasoning-file':
|
|
326
|
+
case 'tool-result':
|
|
327
|
+
(collectedOutput.content ??= []).push(chunk);
|
|
328
|
+
break;
|
|
322
329
|
case 'finish':
|
|
323
330
|
collectedOutput.finishReason = chunk.finishReason;
|
|
324
331
|
collectedOutput.usage = chunk.usage;
|
|
@@ -335,7 +342,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
|
|
|
335
342
|
const durationMs = Date.now() - startTime;
|
|
336
343
|
await updateStepResult(stepId, {
|
|
337
344
|
duration_ms: durationMs,
|
|
338
|
-
output:
|
|
345
|
+
output: serializeForDevTools(collectedOutput),
|
|
339
346
|
usage: collectedOutput.usage
|
|
340
347
|
? JSON.stringify(collectedOutput.usage)
|
|
341
348
|
: null,
|
|
@@ -354,7 +361,7 @@ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
|
|
|
354
361
|
const durationMs = Date.now() - startTime;
|
|
355
362
|
await updateStepResult(stepId, {
|
|
356
363
|
duration_ms: durationMs,
|
|
357
|
-
output:
|
|
364
|
+
output: serializeForDevTools(collectedOutput),
|
|
358
365
|
usage: collectedOutput.usage
|
|
359
366
|
? JSON.stringify(collectedOutput.usage)
|
|
360
367
|
: null,
|
package/src/serialize.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
function normalizeBinaryData(value: unknown): unknown {
|
|
2
|
+
if (value instanceof ArrayBuffer) {
|
|
3
|
+
return Buffer.from(value).toString('base64');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (ArrayBuffer.isView(value)) {
|
|
7
|
+
return Buffer.from(
|
|
8
|
+
value.buffer,
|
|
9
|
+
value.byteOffset,
|
|
10
|
+
value.byteLength,
|
|
11
|
+
).toString('base64');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectMediaBinaryValues(value: unknown): WeakSet<object> {
|
|
22
|
+
const mediaBinaryValues = new WeakSet<object>();
|
|
23
|
+
const visited = new WeakSet<object>();
|
|
24
|
+
|
|
25
|
+
const markBinary = (candidate: unknown) => {
|
|
26
|
+
if (candidate instanceof ArrayBuffer || ArrayBuffer.isView(candidate)) {
|
|
27
|
+
mediaBinaryValues.add(candidate);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const markGeneratedFile = (candidate: unknown) => {
|
|
32
|
+
if (!isRecord(candidate)) return;
|
|
33
|
+
|
|
34
|
+
if (Object.hasOwn(candidate, 'uint8Array')) {
|
|
35
|
+
markBinary(candidate.uint8Array);
|
|
36
|
+
}
|
|
37
|
+
markBinary(candidate.uint8ArrayData);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const markDataContent = (candidate: unknown) => {
|
|
41
|
+
markBinary(candidate);
|
|
42
|
+
|
|
43
|
+
if (!isRecord(candidate)) return;
|
|
44
|
+
|
|
45
|
+
if (candidate.type === 'data') {
|
|
46
|
+
markBinary(candidate.data);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const visit = (candidate: unknown) => {
|
|
51
|
+
if (
|
|
52
|
+
candidate == null ||
|
|
53
|
+
typeof candidate !== 'object' ||
|
|
54
|
+
candidate instanceof ArrayBuffer ||
|
|
55
|
+
ArrayBuffer.isView(candidate) ||
|
|
56
|
+
visited.has(candidate)
|
|
57
|
+
) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
visited.add(candidate);
|
|
61
|
+
|
|
62
|
+
if (isRecord(candidate) && typeof candidate.type === 'string') {
|
|
63
|
+
switch (candidate.type) {
|
|
64
|
+
case 'file':
|
|
65
|
+
case 'reasoning-file':
|
|
66
|
+
markDataContent(candidate.data);
|
|
67
|
+
markGeneratedFile(candidate);
|
|
68
|
+
markGeneratedFile(candidate.file);
|
|
69
|
+
break;
|
|
70
|
+
case 'image':
|
|
71
|
+
markDataContent(candidate.image);
|
|
72
|
+
break;
|
|
73
|
+
case 'media':
|
|
74
|
+
case 'file-data':
|
|
75
|
+
case 'image-data':
|
|
76
|
+
markBinary(candidate.data);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const child of Array.isArray(candidate)
|
|
82
|
+
? candidate
|
|
83
|
+
: Object.values(candidate)) {
|
|
84
|
+
visit(child);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
visit(value);
|
|
89
|
+
return mediaBinaryValues;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Serializes captured DevTools values while preserving binary values in
|
|
94
|
+
* recognized media-bearing fields as base64. Unrelated binary values retain
|
|
95
|
+
* their normal JSON.stringify representation.
|
|
96
|
+
*/
|
|
97
|
+
export function serializeForDevTools(value: unknown): string {
|
|
98
|
+
const mediaBinaryValues = collectMediaBinaryValues(value);
|
|
99
|
+
|
|
100
|
+
return JSON.stringify(value, function (key, serializedValue) {
|
|
101
|
+
// JSON.stringify invokes toJSON before the replacer. Read the original
|
|
102
|
+
// property from the holder so binary values can still be normalized while
|
|
103
|
+
// preserving custom toJSON behavior for every other object.
|
|
104
|
+
const originalValue = (this as Record<string, unknown>)[key];
|
|
105
|
+
return originalValue != null &&
|
|
106
|
+
typeof originalValue === 'object' &&
|
|
107
|
+
mediaBinaryValues.has(originalValue)
|
|
108
|
+
? normalizeBinaryData(originalValue)
|
|
109
|
+
: serializedValue;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Download, ExternalLink, File, Image } from 'lucide-react';
|
|
3
|
+
import { findMediaPreviews, type MediaPreviewData } from '../media';
|
|
4
|
+
|
|
5
|
+
const anonymousMediaProps = {
|
|
6
|
+
crossOrigin: 'anonymous' as const,
|
|
7
|
+
referrerPolicy: 'no-referrer' as const,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function MediaPreviewList({ data }: { data: unknown }) {
|
|
11
|
+
const previews = findMediaPreviews(data);
|
|
12
|
+
|
|
13
|
+
if (previews.length === 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
19
|
+
{previews.map((preview, index) => (
|
|
20
|
+
<MediaPreviewCard key={index} preview={preview} />
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function MediaPreviewCard({ preview }: { preview: MediaPreviewData }) {
|
|
27
|
+
const [loadRemotePreview, setLoadRemotePreview] = useState(false);
|
|
28
|
+
const canRender =
|
|
29
|
+
preview.source != null &&
|
|
30
|
+
(preview.kind === 'image' ||
|
|
31
|
+
preview.kind === 'audio' ||
|
|
32
|
+
preview.kind === 'video');
|
|
33
|
+
const shouldRender =
|
|
34
|
+
canRender && (preview.sourceType === 'inline' || loadRemotePreview);
|
|
35
|
+
const label = preview.filename ?? `${preview.kind} preview`;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="overflow-hidden rounded-md border border-border bg-background">
|
|
39
|
+
<div className="flex items-center gap-2 border-b border-border px-2.5 py-2">
|
|
40
|
+
{preview.kind === 'image' ? (
|
|
41
|
+
<Image className="size-3.5 text-info" aria-hidden="true" />
|
|
42
|
+
) : (
|
|
43
|
+
<File className="size-3.5 text-info" aria-hidden="true" />
|
|
44
|
+
)}
|
|
45
|
+
<span className="min-w-0 flex-1 truncate text-xs font-medium">
|
|
46
|
+
{label}
|
|
47
|
+
</span>
|
|
48
|
+
<span className="text-[10px] font-mono text-muted-foreground">
|
|
49
|
+
{preview.mediaType}
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{shouldRender && preview.kind === 'image' && (
|
|
54
|
+
<img
|
|
55
|
+
alt={label}
|
|
56
|
+
className="max-h-72 w-full bg-muted/30 object-contain"
|
|
57
|
+
decoding="async"
|
|
58
|
+
loading="lazy"
|
|
59
|
+
src={preview.source}
|
|
60
|
+
{...anonymousMediaProps}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{shouldRender && preview.kind === 'audio' && (
|
|
65
|
+
// Captured media parts do not include caption tracks.
|
|
66
|
+
// oxlint-disable-next-line jsx-a11y/media-has-caption
|
|
67
|
+
<audio
|
|
68
|
+
className="w-full p-2"
|
|
69
|
+
controls
|
|
70
|
+
preload="none"
|
|
71
|
+
src={preview.source}
|
|
72
|
+
{...anonymousMediaProps}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{shouldRender && preview.kind === 'video' && (
|
|
77
|
+
// Captured media parts do not include caption tracks.
|
|
78
|
+
// oxlint-disable-next-line jsx-a11y/media-has-caption
|
|
79
|
+
<video
|
|
80
|
+
className="max-h-72 w-full bg-muted/30"
|
|
81
|
+
controls
|
|
82
|
+
preload="metadata"
|
|
83
|
+
src={preview.source}
|
|
84
|
+
{...anonymousMediaProps}
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
<div className="flex items-center gap-2 px-2.5 py-2">
|
|
89
|
+
{canRender && preview.sourceType === 'remote' && !loadRemotePreview && (
|
|
90
|
+
<button
|
|
91
|
+
className="text-xs font-medium text-info hover:underline"
|
|
92
|
+
onClick={() => setLoadRemotePreview(true)}
|
|
93
|
+
type="button"
|
|
94
|
+
>
|
|
95
|
+
Load preview
|
|
96
|
+
</button>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{preview.source != null && (
|
|
100
|
+
<a
|
|
101
|
+
className="ml-auto inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
|
102
|
+
download={
|
|
103
|
+
preview.sourceType === 'inline' ? preview.filename : undefined
|
|
104
|
+
}
|
|
105
|
+
href={preview.source}
|
|
106
|
+
rel="noreferrer"
|
|
107
|
+
target="_blank"
|
|
108
|
+
>
|
|
109
|
+
{preview.sourceType === 'inline' ? (
|
|
110
|
+
<Download className="size-3" aria-hidden="true" />
|
|
111
|
+
) : (
|
|
112
|
+
<ExternalLink className="size-3" aria-hidden="true" />
|
|
113
|
+
)}
|
|
114
|
+
{preview.sourceType === 'inline' ? 'Download' : 'Open'}
|
|
115
|
+
</a>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{preview.source == null && (
|
|
119
|
+
<span className="text-xs text-muted-foreground">
|
|
120
|
+
{preview.unavailableReason ??
|
|
121
|
+
'Preview unavailable; inspect the JSON metadata below.'}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -21,9 +21,11 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
CollapsibleToolCall,
|
|
23
23
|
CollapsibleToolResult,
|
|
24
|
+
MediaAwareValue,
|
|
24
25
|
ReasoningBlock,
|
|
25
26
|
TextBlock,
|
|
26
27
|
} from './shared-components';
|
|
28
|
+
import { findMediaPreviews } from '../media';
|
|
27
29
|
|
|
28
30
|
export function InputPanel({ input }: { input: ParsedInput | null }) {
|
|
29
31
|
const messages: PromptMessage[] = input?.prompt ?? [];
|
|
@@ -128,6 +130,17 @@ function getReasoningContent(content: string | ContentPart[]): string {
|
|
|
128
130
|
return '';
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
function getDirectMediaContent(content: string | ContentPart[]): ContentPart[] {
|
|
134
|
+
return Array.isArray(content)
|
|
135
|
+
? content.filter(
|
|
136
|
+
part =>
|
|
137
|
+
part.type !== 'tool-call' &&
|
|
138
|
+
part.type !== 'tool-result' &&
|
|
139
|
+
findMediaPreviews(part).length > 0,
|
|
140
|
+
)
|
|
141
|
+
: [];
|
|
142
|
+
}
|
|
143
|
+
|
|
131
144
|
function InputMessagePreview({
|
|
132
145
|
message,
|
|
133
146
|
index,
|
|
@@ -148,12 +161,14 @@ function InputMessagePreview({
|
|
|
148
161
|
const toolCalls = getToolCalls(content);
|
|
149
162
|
const toolResults = getToolResults(content);
|
|
150
163
|
const reasoningContent = getReasoningContent(content);
|
|
164
|
+
const mediaCount = getDirectMediaContent(content).length;
|
|
151
165
|
|
|
152
166
|
const partCount =
|
|
153
167
|
(textContent ? 1 : 0) +
|
|
154
168
|
(reasoningContent ? 1 : 0) +
|
|
155
169
|
toolCalls.length +
|
|
156
|
-
toolResults.length
|
|
170
|
+
toolResults.length +
|
|
171
|
+
mediaCount;
|
|
157
172
|
|
|
158
173
|
return (
|
|
159
174
|
<div className="rounded-md border border-border/50 bg-background/50 p-2.5 space-y-2">
|
|
@@ -183,6 +198,12 @@ function InputMessagePreview({
|
|
|
183
198
|
</div>
|
|
184
199
|
)}
|
|
185
200
|
|
|
201
|
+
{mediaCount > 0 && (
|
|
202
|
+
<div className="text-[11px] text-muted-foreground">
|
|
203
|
+
{mediaCount} media {mediaCount === 1 ? 'part' : 'parts'}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
|
|
186
207
|
{toolCalls.length > 0 && (
|
|
187
208
|
<div className="space-y-1">
|
|
188
209
|
{toolCalls.slice(0, 3).map((call, i) => {
|
|
@@ -235,7 +256,8 @@ function InputMessagePreview({
|
|
|
235
256
|
{!textContent &&
|
|
236
257
|
!reasoningContent &&
|
|
237
258
|
toolCalls.length === 0 &&
|
|
238
|
-
toolResults.length === 0 &&
|
|
259
|
+
toolResults.length === 0 &&
|
|
260
|
+
mediaCount === 0 && (
|
|
239
261
|
<div className="text-[11px] text-muted-foreground italic">
|
|
240
262
|
Empty message
|
|
241
263
|
</div>
|
|
@@ -264,12 +286,15 @@ export function MessageBubble({
|
|
|
264
286
|
const toolCalls = getToolCalls(content);
|
|
265
287
|
const toolResults = getToolResults(content);
|
|
266
288
|
const reasoningContent = getReasoningContent(content);
|
|
289
|
+
const directMediaContent = getDirectMediaContent(content);
|
|
290
|
+
const mediaCount = directMediaContent.length;
|
|
267
291
|
|
|
268
292
|
const partCount =
|
|
269
293
|
(textContent ? 1 : 0) +
|
|
270
294
|
(reasoningContent ? 1 : 0) +
|
|
271
295
|
toolCalls.length +
|
|
272
|
-
toolResults.length
|
|
296
|
+
toolResults.length +
|
|
297
|
+
mediaCount;
|
|
273
298
|
|
|
274
299
|
return (
|
|
275
300
|
<div className="rounded-md border border-border/50 bg-background/50 p-3 space-y-2">
|
|
@@ -303,6 +328,10 @@ export function MessageBubble({
|
|
|
303
328
|
/>
|
|
304
329
|
)}
|
|
305
330
|
|
|
331
|
+
{directMediaContent.length > 0 && (
|
|
332
|
+
<MediaAwareValue data={directMediaContent} />
|
|
333
|
+
)}
|
|
334
|
+
|
|
306
335
|
{toolCalls.length > 0 && (
|
|
307
336
|
<div className="space-y-2">
|
|
308
337
|
{toolCalls.map((call, i) => (
|
|
@@ -332,7 +361,8 @@ export function MessageBubble({
|
|
|
332
361
|
{!textContent &&
|
|
333
362
|
!reasoningContent &&
|
|
334
363
|
toolCalls.length === 0 &&
|
|
335
|
-
toolResults.length === 0 &&
|
|
364
|
+
toolResults.length === 0 &&
|
|
365
|
+
mediaCount === 0 && (
|
|
336
366
|
<div className="text-[11px] text-muted-foreground italic">
|
|
337
367
|
Empty message
|
|
338
368
|
</div>
|