@ai-sdk/devtools 1.0.0-beta.9 → 1.0.0-canary.21
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
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Drawer,
|
|
4
|
+
DrawerContent,
|
|
5
|
+
DrawerHeader,
|
|
6
|
+
DrawerTitle,
|
|
7
|
+
DrawerTrigger,
|
|
8
|
+
} from '@/components/ui/drawer';
|
|
9
|
+
import type {
|
|
10
|
+
ParsedInput,
|
|
11
|
+
PromptMessage,
|
|
12
|
+
ContentPart,
|
|
13
|
+
ToolCallContentPart,
|
|
14
|
+
ToolResultContentPart,
|
|
15
|
+
} from '../types';
|
|
16
|
+
import {
|
|
17
|
+
safeParseJson,
|
|
18
|
+
formatToolParamsInline,
|
|
19
|
+
formatResultPreview,
|
|
20
|
+
} from '../utils';
|
|
21
|
+
import {
|
|
22
|
+
CollapsibleToolCall,
|
|
23
|
+
CollapsibleToolResult,
|
|
24
|
+
ReasoningBlock,
|
|
25
|
+
TextBlock,
|
|
26
|
+
} from './shared-components';
|
|
27
|
+
|
|
28
|
+
export function InputPanel({ input }: { input: ParsedInput | null }) {
|
|
29
|
+
const messages: PromptMessage[] = input?.prompt ?? [];
|
|
30
|
+
const messageCount = messages.length;
|
|
31
|
+
|
|
32
|
+
const lastTwoMessages = messages.slice(-2);
|
|
33
|
+
const previousMessageCount = Math.max(0, messageCount - 2);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Drawer direction="right">
|
|
37
|
+
<DrawerTrigger asChild>
|
|
38
|
+
<button className="w-full h-full text-left p-4 hover:bg-accent/30 transition-colors cursor-pointer flex flex-col justify-start">
|
|
39
|
+
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-3">
|
|
40
|
+
Input
|
|
41
|
+
</h3>
|
|
42
|
+
|
|
43
|
+
<div className="space-y-3">
|
|
44
|
+
{previousMessageCount > 0 && (
|
|
45
|
+
<div className="text-[11px] text-muted-foreground/60 text-center py-1.5 rounded-md bg-muted/30">
|
|
46
|
+
+ {previousMessageCount} previous{' '}
|
|
47
|
+
{previousMessageCount === 1 ? 'message' : 'messages'}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
{lastTwoMessages.map((msg, i) => (
|
|
52
|
+
<InputMessagePreview
|
|
53
|
+
key={i}
|
|
54
|
+
message={msg}
|
|
55
|
+
index={previousMessageCount + i + 1}
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
|
|
59
|
+
{messageCount === 0 && (
|
|
60
|
+
<p className="text-sm text-muted-foreground">No messages</p>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
</button>
|
|
64
|
+
</DrawerTrigger>
|
|
65
|
+
<DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
|
|
66
|
+
<DrawerHeader className="border-b border-border shrink-0">
|
|
67
|
+
<DrawerTitle>All Messages ({messageCount})</DrawerTitle>
|
|
68
|
+
</DrawerHeader>
|
|
69
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
70
|
+
<div className="space-y-3">
|
|
71
|
+
{messages.map((msg, i) => (
|
|
72
|
+
<MessageBubble key={i} message={msg} index={i + 1} />
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</DrawerContent>
|
|
77
|
+
</Drawer>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getTextContent(content: string | ContentPart[]): string {
|
|
82
|
+
if (typeof content === 'string') return content;
|
|
83
|
+
if (Array.isArray(content)) {
|
|
84
|
+
return content
|
|
85
|
+
.filter((p): p is { type: 'text'; text: string } => p.type === 'text')
|
|
86
|
+
.map(p => p.text)
|
|
87
|
+
.join('');
|
|
88
|
+
}
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getToolCalls(content: string | ContentPart[]): ToolCallContentPart[] {
|
|
93
|
+
if (Array.isArray(content)) {
|
|
94
|
+
return content.filter(
|
|
95
|
+
(p): p is ToolCallContentPart => p.type === 'tool-call',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getToolResults(
|
|
102
|
+
content: string | ContentPart[],
|
|
103
|
+
): ToolResultContentPart[] {
|
|
104
|
+
if (Array.isArray(content)) {
|
|
105
|
+
return content.filter(
|
|
106
|
+
(p): p is ToolResultContentPart => p.type === 'tool-result',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getReasoningContent(content: string | ContentPart[]): string {
|
|
113
|
+
if (Array.isArray(content)) {
|
|
114
|
+
return content
|
|
115
|
+
.filter(
|
|
116
|
+
(
|
|
117
|
+
p,
|
|
118
|
+
): p is {
|
|
119
|
+
type: 'thinking' | 'reasoning';
|
|
120
|
+
text?: string;
|
|
121
|
+
thinking?: string;
|
|
122
|
+
reasoning?: string;
|
|
123
|
+
} => p.type === 'thinking' || p.type === 'reasoning',
|
|
124
|
+
)
|
|
125
|
+
.map(p => p.thinking || p.text || p.reasoning || '')
|
|
126
|
+
.join('');
|
|
127
|
+
}
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function InputMessagePreview({
|
|
132
|
+
message,
|
|
133
|
+
index,
|
|
134
|
+
}: {
|
|
135
|
+
message: PromptMessage;
|
|
136
|
+
index?: number;
|
|
137
|
+
}) {
|
|
138
|
+
const { role, content } = message;
|
|
139
|
+
|
|
140
|
+
const roleLabels: Record<string, string> = {
|
|
141
|
+
user: 'User',
|
|
142
|
+
assistant: 'Assistant',
|
|
143
|
+
system: 'System',
|
|
144
|
+
tool: 'Tool',
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const textContent = getTextContent(content);
|
|
148
|
+
const toolCalls = getToolCalls(content);
|
|
149
|
+
const toolResults = getToolResults(content);
|
|
150
|
+
const reasoningContent = getReasoningContent(content);
|
|
151
|
+
|
|
152
|
+
const partCount =
|
|
153
|
+
(textContent ? 1 : 0) +
|
|
154
|
+
(reasoningContent ? 1 : 0) +
|
|
155
|
+
toolCalls.length +
|
|
156
|
+
toolResults.length;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div className="rounded-md border border-border/50 bg-background/50 p-2.5 space-y-2">
|
|
160
|
+
<div className="flex items-center gap-2">
|
|
161
|
+
{index && (
|
|
162
|
+
<span className="text-[10px] font-mono text-muted-foreground/50">
|
|
163
|
+
{index}
|
|
164
|
+
</span>
|
|
165
|
+
)}
|
|
166
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
167
|
+
{roleLabels[role] || role}
|
|
168
|
+
</span>
|
|
169
|
+
{partCount > 1 && (
|
|
170
|
+
<span className="text-[10px] text-muted-foreground/60">
|
|
171
|
+
{partCount} parts
|
|
172
|
+
</span>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{reasoningContent && (
|
|
177
|
+
<div className="text-xs text-amber-500/60">[thinking]</div>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{textContent && (
|
|
181
|
+
<div className="text-xs text-foreground/90 line-clamp-3">
|
|
182
|
+
{textContent}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{toolCalls.length > 0 && (
|
|
187
|
+
<div className="space-y-1">
|
|
188
|
+
{toolCalls.slice(0, 3).map((call, i) => {
|
|
189
|
+
const args = call.args ?? call.input;
|
|
190
|
+
const parsedArgs =
|
|
191
|
+
typeof args === 'string'
|
|
192
|
+
? (safeParseJson(args) as Record<string, unknown>)
|
|
193
|
+
: ((args as Record<string, unknown>) ?? {});
|
|
194
|
+
return (
|
|
195
|
+
<div
|
|
196
|
+
key={i}
|
|
197
|
+
className="text-[11px] font-mono text-muted-foreground truncate"
|
|
198
|
+
>
|
|
199
|
+
{call.toolName}({formatToolParamsInline(parsedArgs)})
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
})}
|
|
203
|
+
{toolCalls.length > 3 && (
|
|
204
|
+
<div className="text-[11px] text-muted-foreground/60">
|
|
205
|
+
+{toolCalls.length - 3} more tool{' '}
|
|
206
|
+
{toolCalls.length - 3 === 1 ? 'call' : 'calls'}
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{toolResults.length > 0 && (
|
|
213
|
+
<div className="space-y-1">
|
|
214
|
+
{toolResults.slice(0, 3).map((result, i) => {
|
|
215
|
+
const resultContent = result.result ?? result.output ?? result;
|
|
216
|
+
const resultPreview = formatResultPreview(resultContent);
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
key={i}
|
|
220
|
+
className="text-[11px] font-mono text-muted-foreground truncate"
|
|
221
|
+
>
|
|
222
|
+
{result.toolName || 'tool'}(…) => {resultPreview}
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
})}
|
|
226
|
+
{toolResults.length > 3 && (
|
|
227
|
+
<div className="text-[11px] text-muted-foreground/60">
|
|
228
|
+
+{toolResults.length - 3} more tool{' '}
|
|
229
|
+
{toolResults.length - 3 === 1 ? 'result' : 'results'}
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
{!textContent &&
|
|
236
|
+
!reasoningContent &&
|
|
237
|
+
toolCalls.length === 0 &&
|
|
238
|
+
toolResults.length === 0 && (
|
|
239
|
+
<div className="text-[11px] text-muted-foreground italic">
|
|
240
|
+
Empty message
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function MessageBubble({
|
|
248
|
+
message,
|
|
249
|
+
index,
|
|
250
|
+
}: {
|
|
251
|
+
message: PromptMessage;
|
|
252
|
+
index?: number;
|
|
253
|
+
}) {
|
|
254
|
+
const { role, content } = message;
|
|
255
|
+
|
|
256
|
+
const roleLabels: Record<string, string> = {
|
|
257
|
+
user: 'User',
|
|
258
|
+
assistant: 'Assistant',
|
|
259
|
+
system: 'System',
|
|
260
|
+
tool: 'Tool',
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const textContent = getTextContent(content);
|
|
264
|
+
const toolCalls = getToolCalls(content);
|
|
265
|
+
const toolResults = getToolResults(content);
|
|
266
|
+
const reasoningContent = getReasoningContent(content);
|
|
267
|
+
|
|
268
|
+
const partCount =
|
|
269
|
+
(textContent ? 1 : 0) +
|
|
270
|
+
(reasoningContent ? 1 : 0) +
|
|
271
|
+
toolCalls.length +
|
|
272
|
+
toolResults.length;
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<div className="rounded-md border border-border/50 bg-background/50 p-3 space-y-2">
|
|
276
|
+
<div className="flex items-center gap-2">
|
|
277
|
+
{index && (
|
|
278
|
+
<span className="text-[10px] font-mono text-muted-foreground/50">
|
|
279
|
+
{index}
|
|
280
|
+
</span>
|
|
281
|
+
)}
|
|
282
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
283
|
+
{roleLabels[role] || role}
|
|
284
|
+
</span>
|
|
285
|
+
{partCount > 1 && (
|
|
286
|
+
<span className="text-[10px] text-muted-foreground/60">
|
|
287
|
+
{partCount} parts
|
|
288
|
+
</span>
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
{reasoningContent && <ReasoningBlock content={reasoningContent} />}
|
|
293
|
+
|
|
294
|
+
{textContent && (
|
|
295
|
+
<TextBlock
|
|
296
|
+
content={textContent}
|
|
297
|
+
defaultExpanded={
|
|
298
|
+
!reasoningContent &&
|
|
299
|
+
toolCalls.length === 0 &&
|
|
300
|
+
toolResults.length === 0
|
|
301
|
+
}
|
|
302
|
+
isSystem={role === 'system'}
|
|
303
|
+
/>
|
|
304
|
+
)}
|
|
305
|
+
|
|
306
|
+
{toolCalls.length > 0 && (
|
|
307
|
+
<div className="space-y-2">
|
|
308
|
+
{toolCalls.map((call, i) => (
|
|
309
|
+
<CollapsibleToolCall
|
|
310
|
+
key={i}
|
|
311
|
+
toolName={call.toolName}
|
|
312
|
+
toolCallId={call.toolCallId}
|
|
313
|
+
data={call.args ?? call.input}
|
|
314
|
+
/>
|
|
315
|
+
))}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{toolResults.length > 0 && (
|
|
320
|
+
<div className="space-y-2">
|
|
321
|
+
{toolResults.map((result, i) => (
|
|
322
|
+
<CollapsibleToolResult
|
|
323
|
+
key={i}
|
|
324
|
+
toolName={result.toolName}
|
|
325
|
+
toolCallId={result.toolCallId}
|
|
326
|
+
data={result.result ?? result.output ?? result}
|
|
327
|
+
/>
|
|
328
|
+
))}
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{!textContent &&
|
|
333
|
+
!reasoningContent &&
|
|
334
|
+
toolCalls.length === 0 &&
|
|
335
|
+
toolResults.length === 0 && (
|
|
336
|
+
<div className="text-[11px] text-muted-foreground italic">
|
|
337
|
+
Empty message
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { ChevronRight, Wrench } from 'lucide-react';
|
|
3
|
+
import type {
|
|
4
|
+
ParsedOutput,
|
|
5
|
+
ContentPart,
|
|
6
|
+
ToolCallContentPart,
|
|
7
|
+
ToolResultContentPart,
|
|
8
|
+
TextContentPart,
|
|
9
|
+
ReasoningContentPart,
|
|
10
|
+
} from '../types';
|
|
11
|
+
import { safeParseJson, formatToolParams } from '../utils';
|
|
12
|
+
import { JsonBlock, ReasoningBlock, TextBlock } from './shared-components';
|
|
13
|
+
|
|
14
|
+
export function OutputDisplay({
|
|
15
|
+
output,
|
|
16
|
+
toolResults = [],
|
|
17
|
+
}: {
|
|
18
|
+
output: ParsedOutput;
|
|
19
|
+
toolResults?: ContentPart[];
|
|
20
|
+
}) {
|
|
21
|
+
const getToolResult = (toolCallId: string) => {
|
|
22
|
+
return toolResults.find(
|
|
23
|
+
(r): r is ToolResultContentPart =>
|
|
24
|
+
r.type === 'tool-result' &&
|
|
25
|
+
'toolCallId' in r &&
|
|
26
|
+
r.toolCallId === toolCallId,
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const toolCalls: ToolCallContentPart[] =
|
|
31
|
+
output?.toolCalls ||
|
|
32
|
+
(output?.content?.filter(
|
|
33
|
+
(p): p is ToolCallContentPart => p.type === 'tool-call',
|
|
34
|
+
) ??
|
|
35
|
+
[]);
|
|
36
|
+
|
|
37
|
+
const textParts: TextContentPart[] =
|
|
38
|
+
output?.textParts ||
|
|
39
|
+
(output?.content?.filter((p): p is TextContentPart => p.type === 'text') ??
|
|
40
|
+
[]);
|
|
41
|
+
|
|
42
|
+
const reasoningParts: ReasoningContentPart[] =
|
|
43
|
+
output?.reasoningParts ||
|
|
44
|
+
(output?.content?.filter(
|
|
45
|
+
(p): p is ReasoningContentPart =>
|
|
46
|
+
p.type === 'thinking' || p.type === 'reasoning',
|
|
47
|
+
) ??
|
|
48
|
+
[]);
|
|
49
|
+
|
|
50
|
+
const textContent = textParts.map(p => p.text).join('');
|
|
51
|
+
const reasoningContent = reasoningParts
|
|
52
|
+
.map(p => p.text || p.thinking || p.reasoning || '')
|
|
53
|
+
.join('');
|
|
54
|
+
|
|
55
|
+
const isTextOnly = textContent && !reasoningContent && toolCalls.length === 0;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="space-y-3">
|
|
59
|
+
{reasoningContent && <ReasoningBlock content={reasoningContent} />}
|
|
60
|
+
|
|
61
|
+
{textContent && (
|
|
62
|
+
<TextBlock content={textContent} defaultExpanded={!!isTextOnly} />
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{toolCalls.map((call, i) => {
|
|
66
|
+
const result = call.toolCallId
|
|
67
|
+
? getToolResult(call.toolCallId)
|
|
68
|
+
: undefined;
|
|
69
|
+
return (
|
|
70
|
+
<ToolCallCard
|
|
71
|
+
key={i}
|
|
72
|
+
toolName={call.toolName}
|
|
73
|
+
args={call.args ?? call.input}
|
|
74
|
+
result={result?.output ?? result?.result}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ToolCallCard({
|
|
83
|
+
toolName,
|
|
84
|
+
args,
|
|
85
|
+
result,
|
|
86
|
+
}: {
|
|
87
|
+
toolName: string;
|
|
88
|
+
args: Record<string, unknown> | string | undefined;
|
|
89
|
+
result?: unknown;
|
|
90
|
+
}) {
|
|
91
|
+
const [expanded, setExpanded] = useState(false);
|
|
92
|
+
const parsedArgs =
|
|
93
|
+
typeof args === 'string'
|
|
94
|
+
? (safeParseJson(args) as Record<string, unknown>)
|
|
95
|
+
: args;
|
|
96
|
+
const parsedResult =
|
|
97
|
+
typeof result === 'string' ? safeParseJson(result) : result;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="rounded-md border border-purple/30 overflow-hidden">
|
|
101
|
+
<button
|
|
102
|
+
className="w-full flex items-center gap-2 px-3 py-2 bg-purple/10 hover:bg-purple/20 transition-colors"
|
|
103
|
+
onClick={() => setExpanded(!expanded)}
|
|
104
|
+
>
|
|
105
|
+
<ChevronRight
|
|
106
|
+
className={`size-3 text-purple transition-transform shrink-0 ${
|
|
107
|
+
expanded ? 'rotate-90' : ''
|
|
108
|
+
}`}
|
|
109
|
+
/>
|
|
110
|
+
<Wrench className="size-3 text-purple shrink-0" />
|
|
111
|
+
<span className="text-xs font-mono font-medium text-purple">
|
|
112
|
+
{toolName}
|
|
113
|
+
</span>
|
|
114
|
+
{!expanded &&
|
|
115
|
+
parsedArgs &&
|
|
116
|
+
typeof parsedArgs === 'object' &&
|
|
117
|
+
!Array.isArray(parsedArgs) && (
|
|
118
|
+
<span className="text-[11px] font-mono text-purple/70 truncate">
|
|
119
|
+
{formatToolParams(parsedArgs)}
|
|
120
|
+
</span>
|
|
121
|
+
)}
|
|
122
|
+
</button>
|
|
123
|
+
|
|
124
|
+
{expanded && (
|
|
125
|
+
<>
|
|
126
|
+
<div className="p-3 bg-card/50 border-t border-purple/30">
|
|
127
|
+
<div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mb-2">
|
|
128
|
+
Input
|
|
129
|
+
</div>
|
|
130
|
+
<JsonBlock data={parsedArgs} />
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{parsedResult != null && (
|
|
134
|
+
<div className="p-3 border-t border-border bg-success/5">
|
|
135
|
+
<div className="text-[10px] font-medium uppercase tracking-wider text-success mb-2">
|
|
136
|
+
Output
|
|
137
|
+
</div>
|
|
138
|
+
<JsonBlock data={parsedResult} />
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|