@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.
@@ -0,0 +1,691 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ ChevronRight,
4
+ Copy,
5
+ Check,
6
+ Wrench,
7
+ MessageSquare,
8
+ Brain,
9
+ Settings,
10
+ BarChart3,
11
+ } from 'lucide-react';
12
+ import {
13
+ Drawer,
14
+ DrawerContent,
15
+ DrawerHeader,
16
+ DrawerTitle,
17
+ DrawerTrigger,
18
+ } from '@/components/ui/drawer';
19
+ import type {
20
+ InputTokenBreakdown,
21
+ OutputTokenBreakdown,
22
+ ParsedInput,
23
+ ParsedUsage,
24
+ ToolDefinition,
25
+ } from '../types';
26
+ import {
27
+ safeParseJson,
28
+ formatToolParams,
29
+ getInputTokenBreakdown,
30
+ getOutputTokenBreakdown,
31
+ } from '../utils';
32
+
33
+ export function JsonBlock({
34
+ data,
35
+ compact = false,
36
+ size = 'sm',
37
+ }: {
38
+ data: unknown;
39
+ compact?: boolean;
40
+ size?: 'sm' | 'base' | 'lg';
41
+ }) {
42
+ const [copied, setCopied] = useState(false);
43
+
44
+ const jsonString = JSON.stringify(data, null, 2);
45
+ const displayString =
46
+ compact && jsonString.length > 200 ? JSON.stringify(data) : jsonString;
47
+
48
+ const sizeClasses = {
49
+ sm: 'text-xs',
50
+ base: 'text-sm',
51
+ lg: 'text-base',
52
+ };
53
+
54
+ const handleCopy = async () => {
55
+ await navigator.clipboard.writeText(jsonString);
56
+ setCopied(true);
57
+ setTimeout(() => setCopied(false), 2000);
58
+ };
59
+
60
+ return (
61
+ <div className="relative group">
62
+ <button
63
+ onClick={handleCopy}
64
+ 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"
65
+ title="Copy to clipboard"
66
+ >
67
+ {copied ? (
68
+ <Check className="size-3 text-success" />
69
+ ) : (
70
+ <Copy className="size-3 text-muted-foreground" />
71
+ )}
72
+ </button>
73
+ <pre
74
+ className={`font-mono ${sizeClasses[size]} text-muted-foreground whitespace-pre-wrap wrap-break-words bg-background rounded p-2 ${
75
+ compact ? 'overflow-hidden max-h-20' : ''
76
+ }`}
77
+ >
78
+ {displayString}
79
+ </pre>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ export function RawDataSection({
85
+ rawRequest,
86
+ rawResponse,
87
+ rawChunks,
88
+ isStream,
89
+ }: {
90
+ rawRequest: string | null;
91
+ rawResponse: string | null;
92
+ rawChunks: string | null;
93
+ isStream: boolean;
94
+ }) {
95
+ const [expanded, setExpanded] = useState(false);
96
+ const [responseView, setResponseView] = useState<'parsed' | 'raw'>('parsed');
97
+
98
+ if (!rawRequest && !rawResponse) return null;
99
+
100
+ const hasRawChunks = isStream && rawChunks;
101
+
102
+ return (
103
+ <div className="border-t border-border">
104
+ <button
105
+ 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"
106
+ onClick={() => setExpanded(!expanded)}
107
+ >
108
+ <ChevronRight
109
+ className={`size-3 transition-transform ${expanded ? 'rotate-90' : ''}`}
110
+ />
111
+ <span className="font-medium tracking-wider uppercase">
112
+ Request / Response
113
+ </span>
114
+ </button>
115
+
116
+ {expanded && (
117
+ <div className="grid grid-cols-2 gap-4 px-4 pb-4">
118
+ {rawRequest && (
119
+ <div className="flex flex-col">
120
+ <div className="flex items-end mb-2 h-7">
121
+ <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
122
+ Request
123
+ </span>
124
+ </div>
125
+ <div className="max-h-[400px] overflow-auto rounded-md border border-border">
126
+ <JsonBlock data={safeParseJson(rawRequest)} />
127
+ </div>
128
+ </div>
129
+ )}
130
+ {(rawResponse || rawChunks) && (
131
+ <div className="flex flex-col">
132
+ <div className="flex justify-between items-end mb-2 h-7">
133
+ <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
134
+ {isStream ? 'Stream' : 'Response'}
135
+ </span>
136
+ {hasRawChunks && (
137
+ <div className="inline-flex rounded-md border border-border bg-muted/30 p-0.5">
138
+ <button
139
+ onClick={() => setResponseView('parsed')}
140
+ className={`px-2 py-0.5 text-[10px] font-medium rounded transition-colors ${
141
+ responseView === 'parsed'
142
+ ? 'bg-background text-foreground shadow-sm'
143
+ : 'text-muted-foreground hover:text-foreground'
144
+ }`}
145
+ >
146
+ AI SDK
147
+ </button>
148
+ <button
149
+ onClick={() => setResponseView('raw')}
150
+ className={`px-2 py-0.5 text-[10px] font-medium rounded transition-colors ${
151
+ responseView === 'raw'
152
+ ? 'bg-background text-foreground shadow-sm'
153
+ : 'text-muted-foreground hover:text-foreground'
154
+ }`}
155
+ >
156
+ Provider
157
+ </button>
158
+ </div>
159
+ )}
160
+ </div>
161
+ <div className="max-h-[400px] overflow-auto rounded-md border border-border">
162
+ <JsonBlock
163
+ data={safeParseJson(
164
+ hasRawChunks && responseView === 'raw'
165
+ ? rawChunks
166
+ : rawResponse,
167
+ )}
168
+ />
169
+ </div>
170
+ </div>
171
+ )}
172
+ </div>
173
+ )}
174
+ </div>
175
+ );
176
+ }
177
+
178
+ export function UsageDetails({ usage }: { usage: ParsedUsage }) {
179
+ const inputBreakdown = getInputTokenBreakdown(usage?.inputTokens);
180
+ const outputBreakdown = getOutputTokenBreakdown(usage?.outputTokens);
181
+
182
+ return (
183
+ <div className="space-y-6">
184
+ <div className="grid grid-cols-2 gap-4">
185
+ <div className="p-4 rounded-lg border border-border bg-card">
186
+ <div className="mb-2 text-xs font-medium tracking-wider uppercase text-muted-foreground">
187
+ Input Tokens
188
+ </div>
189
+ <div className="text-2xl font-semibold">{inputBreakdown.total}</div>
190
+ {(inputBreakdown.cacheRead !== undefined ||
191
+ inputBreakdown.cacheWrite !== undefined) && (
192
+ <div className="mt-3 space-y-1 text-sm text-muted-foreground">
193
+ {inputBreakdown.cacheRead !== undefined && (
194
+ <div className="flex justify-between">
195
+ <span>Cache read</span>
196
+ <span className="font-mono">{inputBreakdown.cacheRead}</span>
197
+ </div>
198
+ )}
199
+ {inputBreakdown.cacheWrite !== undefined && (
200
+ <div className="flex justify-between">
201
+ <span>Cache write</span>
202
+ <span className="font-mono">{inputBreakdown.cacheWrite}</span>
203
+ </div>
204
+ )}
205
+ {inputBreakdown.noCache !== undefined && (
206
+ <div className="flex justify-between">
207
+ <span>No cache</span>
208
+ <span className="font-mono">{inputBreakdown.noCache}</span>
209
+ </div>
210
+ )}
211
+ </div>
212
+ )}
213
+ </div>
214
+
215
+ <div className="p-4 rounded-lg border border-border bg-card">
216
+ <div className="mb-2 text-xs font-medium tracking-wider uppercase text-muted-foreground">
217
+ Output Tokens
218
+ </div>
219
+ <div className="text-2xl font-semibold">{outputBreakdown.total}</div>
220
+ {(outputBreakdown.text !== undefined ||
221
+ outputBreakdown.reasoning !== undefined) && (
222
+ <div className="mt-3 space-y-1 text-sm text-muted-foreground">
223
+ {outputBreakdown.text !== undefined && (
224
+ <div className="flex justify-between">
225
+ <span>Text</span>
226
+ <span className="font-mono">{outputBreakdown.text}</span>
227
+ </div>
228
+ )}
229
+ {outputBreakdown.reasoning !== undefined && (
230
+ <div className="flex justify-between">
231
+ <span>Reasoning</span>
232
+ <span className="font-mono">{outputBreakdown.reasoning}</span>
233
+ </div>
234
+ )}
235
+ </div>
236
+ )}
237
+ </div>
238
+ </div>
239
+
240
+ {usage?.raw != null && (
241
+ <div>
242
+ <div className="mb-2 text-xs font-medium tracking-wider uppercase text-muted-foreground">
243
+ Raw Provider Usage
244
+ </div>
245
+ <JsonBlock data={usage.raw} size="sm" />
246
+ </div>
247
+ )}
248
+
249
+ <div>
250
+ <div className="mb-2 text-xs font-medium tracking-wider uppercase text-muted-foreground">
251
+ Full Usage Object
252
+ </div>
253
+ <JsonBlock data={usage} size="sm" />
254
+ </div>
255
+ </div>
256
+ );
257
+ }
258
+
259
+ export function TokenBreakdownTooltip({
260
+ input,
261
+ output,
262
+ raw,
263
+ }: {
264
+ input: InputTokenBreakdown;
265
+ output: OutputTokenBreakdown;
266
+ raw?: unknown;
267
+ }) {
268
+ const hasInputBreakdown =
269
+ input.noCache !== undefined ||
270
+ input.cacheRead !== undefined ||
271
+ input.cacheWrite !== undefined;
272
+ const hasOutputBreakdown =
273
+ output.text !== undefined || output.reasoning !== undefined;
274
+ const hasBreakdown = hasInputBreakdown || hasOutputBreakdown;
275
+
276
+ if (!hasBreakdown) {
277
+ return (
278
+ <div className="text-xs">
279
+ <div>Input: {input.total}</div>
280
+ <div>Output: {output.total}</div>
281
+ </div>
282
+ );
283
+ }
284
+
285
+ return (
286
+ <div className="space-y-2 text-xs">
287
+ <div>
288
+ <div className="mb-1 font-medium">Input: {input.total}</div>
289
+ {input.cacheRead !== undefined && (
290
+ <div className="ml-2 text-muted-foreground">
291
+ Cache read: {input.cacheRead}
292
+ </div>
293
+ )}
294
+ {input.cacheWrite !== undefined && (
295
+ <div className="ml-2 text-muted-foreground">
296
+ Cache write: {input.cacheWrite}
297
+ </div>
298
+ )}
299
+ </div>
300
+ <div>
301
+ <div className="mb-1 font-medium">Output: {output.total}</div>
302
+ {output.text !== undefined && (
303
+ <div className="ml-2 text-muted-foreground">Text: {output.text}</div>
304
+ )}
305
+ {output.reasoning !== undefined && (
306
+ <div className="ml-2 text-muted-foreground">
307
+ Reasoning: {output.reasoning}
308
+ </div>
309
+ )}
310
+ </div>
311
+ {raw !== undefined && (
312
+ <div className="pt-1 mt-1 border-t border-border">
313
+ <div className="text-muted-foreground/70 font-mono text-[10px] max-w-[200px] truncate">
314
+ Raw: {JSON.stringify(raw)}
315
+ </div>
316
+ </div>
317
+ )}
318
+ </div>
319
+ );
320
+ }
321
+
322
+ export function ReasoningBlock({ content }: { content: string }) {
323
+ const [expanded, setExpanded] = useState(false);
324
+
325
+ const previewContent =
326
+ content.length > 200 ? content.slice(0, 200) + '…' : content;
327
+
328
+ return (
329
+ <div className="overflow-hidden rounded-md border border-amber-500/30">
330
+ <button
331
+ className="flex gap-2 items-center px-3 py-2 w-full transition-colors bg-amber-500/10 hover:bg-amber-500/20"
332
+ onClick={() => setExpanded(!expanded)}
333
+ >
334
+ <ChevronRight
335
+ className={`size-3 text-amber-500 transition-transform shrink-0 ${
336
+ expanded ? 'rotate-90' : ''
337
+ }`}
338
+ />
339
+ <Brain className="text-amber-500 size-3 shrink-0" />
340
+ <span className="text-xs font-medium text-amber-500">Thinking</span>
341
+ {!expanded && (
342
+ <span className="text-[11px] text-amber-500/70 truncate ml-1">
343
+ {previewContent}
344
+ </span>
345
+ )}
346
+ </button>
347
+
348
+ {expanded && (
349
+ <div className="p-3 border-t bg-card/50 border-amber-500/30">
350
+ <div className="text-xs leading-relaxed whitespace-pre-wrap text-foreground/80">
351
+ {content}
352
+ </div>
353
+ </div>
354
+ )}
355
+ </div>
356
+ );
357
+ }
358
+
359
+ export function TextBlock({
360
+ content,
361
+ defaultExpanded = false,
362
+ isSystem = false,
363
+ }: {
364
+ content: string;
365
+ defaultExpanded?: boolean;
366
+ isSystem?: boolean;
367
+ }) {
368
+ const [expanded, setExpanded] = useState(defaultExpanded);
369
+ const [copied, setCopied] = useState(false);
370
+
371
+ const previewContent =
372
+ content.length > 200 ? content.slice(0, 200) + '…' : content;
373
+
374
+ const borderColor = isSystem ? 'border-blue-500/30' : 'border-border';
375
+ const bgColor = isSystem ? 'bg-blue-500/10' : 'bg-muted/30';
376
+ const hoverBgColor = isSystem ? 'hover:bg-blue-500/20' : 'hover:bg-muted/50';
377
+ const iconColor = isSystem ? 'text-blue-400' : 'text-muted-foreground';
378
+ const labelColor = isSystem ? 'text-blue-400' : 'text-foreground';
379
+
380
+ const handleCopy = async (e: React.MouseEvent) => {
381
+ e.stopPropagation();
382
+ await navigator.clipboard.writeText(content);
383
+ setCopied(true);
384
+ setTimeout(() => setCopied(false), 2000);
385
+ };
386
+
387
+ return (
388
+ <div className={`overflow-hidden rounded-md border ${borderColor}`}>
389
+ <button
390
+ className={`flex gap-2 items-center px-3 py-2 w-full transition-colors ${bgColor} ${hoverBgColor}`}
391
+ onClick={() => setExpanded(!expanded)}
392
+ >
393
+ <ChevronRight
394
+ className={`size-3 ${iconColor} transition-transform shrink-0 ${
395
+ expanded ? 'rotate-90' : ''
396
+ }`}
397
+ />
398
+ <MessageSquare className={`size-3 ${iconColor} shrink-0`} />
399
+ <span className={`text-xs font-medium ${labelColor}`}>Text</span>
400
+ {!expanded && (
401
+ <span
402
+ className={`text-[11px] ${isSystem ? 'text-blue-400/70' : 'text-muted-foreground'} truncate ml-1`}
403
+ >
404
+ {previewContent}
405
+ </span>
406
+ )}
407
+ </button>
408
+
409
+ {expanded && (
410
+ <div
411
+ className={`relative p-3 border-t bg-card/50 ${borderColor} group`}
412
+ >
413
+ <button
414
+ onClick={handleCopy}
415
+ 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"
416
+ title="Copy to clipboard"
417
+ >
418
+ {copied ? (
419
+ <Check className="size-3 text-success" />
420
+ ) : (
421
+ <Copy className="size-3 text-muted-foreground" />
422
+ )}
423
+ </button>
424
+ <div className="overflow-y-auto max-h-60 text-xs leading-relaxed whitespace-pre-wrap text-foreground">
425
+ {content}
426
+ </div>
427
+ </div>
428
+ )}
429
+ </div>
430
+ );
431
+ }
432
+
433
+ export function ToolItem({ tool }: { tool: ToolDefinition }) {
434
+ const [expanded, setExpanded] = useState(false);
435
+
436
+ return (
437
+ <div className="overflow-hidden rounded-md border border-border bg-background">
438
+ <button
439
+ className="w-full flex items-center justify-between p-2.5 hover:bg-accent/50 transition-colors"
440
+ onClick={() => setExpanded(!expanded)}
441
+ >
442
+ <span className="font-mono text-sm text-purple">{tool.name}</span>
443
+ {tool.parameters && (
444
+ <ChevronRight
445
+ className={`size-3 text-muted-foreground transition-transform ${
446
+ expanded ? 'rotate-90' : ''
447
+ }`}
448
+ />
449
+ )}
450
+ </button>
451
+ {expanded && tool.parameters && (
452
+ <div className="px-2.5 pb-2.5 border-t border-border">
453
+ {tool.description && (
454
+ <p className="pt-2 mb-2 text-xs text-muted-foreground">
455
+ {tool.description}
456
+ </p>
457
+ )}
458
+ <JsonBlock data={tool.parameters} compact />
459
+ </div>
460
+ )}
461
+ {!expanded && tool.description && (
462
+ <div className="px-2.5 pb-2 -mt-1">
463
+ <p className="text-[11px] text-muted-foreground truncate">
464
+ {tool.description}
465
+ </p>
466
+ </div>
467
+ )}
468
+ </div>
469
+ );
470
+ }
471
+
472
+ export function CollapsibleToolCall({
473
+ toolName,
474
+ toolCallId,
475
+ data,
476
+ }: {
477
+ toolName: string;
478
+ toolCallId?: string;
479
+ data: unknown;
480
+ }) {
481
+ const [expanded, setExpanded] = useState(false);
482
+ const parsedData = typeof data === 'string' ? safeParseJson(data) : data;
483
+
484
+ return (
485
+ <div className="overflow-hidden rounded-md border border-purple/30">
486
+ <button
487
+ className="flex gap-2 items-center px-3 py-2 w-full transition-colors bg-purple/10 hover:bg-purple/20"
488
+ onClick={() => setExpanded(!expanded)}
489
+ >
490
+ <ChevronRight
491
+ className={`size-3 text-purple transition-transform shrink-0 ${
492
+ expanded ? 'rotate-90' : ''
493
+ }`}
494
+ />
495
+ <Wrench className="size-3 text-purple shrink-0" />
496
+ <span className="font-mono text-xs font-medium text-purple">
497
+ {toolName}
498
+ </span>
499
+ {!expanded &&
500
+ parsedData != null &&
501
+ typeof parsedData === 'object' &&
502
+ !Array.isArray(parsedData) && (
503
+ <span className="text-[11px] font-mono text-purple/70 truncate">
504
+ {formatToolParams(parsedData as Record<string, unknown>)}
505
+ </span>
506
+ )}
507
+ {toolCallId && (
508
+ <span className="text-[10px] font-mono text-muted-foreground/60 ml-auto shrink-0">
509
+ {toolCallId}
510
+ </span>
511
+ )}
512
+ </button>
513
+ {expanded && (
514
+ <div className="p-3 border-t bg-card/50 border-purple/30">
515
+ <JsonBlock data={parsedData} />
516
+ </div>
517
+ )}
518
+ </div>
519
+ );
520
+ }
521
+
522
+ export function CollapsibleToolResult({
523
+ toolName,
524
+ toolCallId,
525
+ data,
526
+ }: {
527
+ toolName?: string;
528
+ toolCallId?: string;
529
+ data: unknown;
530
+ }) {
531
+ const [expanded, setExpanded] = useState(false);
532
+
533
+ return (
534
+ <div className="overflow-hidden rounded-md border border-success/30">
535
+ <button
536
+ className="flex gap-2 items-center px-3 py-2 w-full transition-colors bg-success/10 hover:bg-success/20"
537
+ onClick={() => setExpanded(!expanded)}
538
+ >
539
+ <ChevronRight
540
+ className={`size-3 text-success transition-transform shrink-0 ${
541
+ expanded ? 'rotate-90' : ''
542
+ }`}
543
+ />
544
+ <span className="text-xs font-medium text-success">Result</span>
545
+ {toolName && (
546
+ <span className="text-[11px] font-mono text-muted-foreground">
547
+ {toolName}
548
+ </span>
549
+ )}
550
+ {toolCallId && (
551
+ <span className="text-[10px] font-mono text-muted-foreground/60 ml-auto shrink-0">
552
+ {toolCallId}
553
+ </span>
554
+ )}
555
+ </button>
556
+ {expanded && (
557
+ <div className="p-3 border-t bg-card/50 border-success/30">
558
+ <JsonBlock data={data} />
559
+ </div>
560
+ )}
561
+ </div>
562
+ );
563
+ }
564
+
565
+ export function StepConfigBar({
566
+ modelId,
567
+ provider,
568
+ input,
569
+ providerOptions,
570
+ usage,
571
+ }: {
572
+ modelId?: string;
573
+ provider?: string | null;
574
+ input: ParsedInput | null;
575
+ providerOptions?: Record<string, unknown> | null;
576
+ usage?: ParsedUsage | null;
577
+ }) {
578
+ const toolCount = input?.tools?.length ?? 0;
579
+
580
+ const params: { label: string; value: string }[] = [];
581
+ if (input?.temperature != null)
582
+ params.push({ label: 'temp', value: String(input.temperature) });
583
+ if (input?.maxOutputTokens != null)
584
+ params.push({ label: 'max tokens', value: String(input.maxOutputTokens) });
585
+ if (input?.topP != null)
586
+ params.push({ label: 'topP', value: String(input.topP) });
587
+ if (input?.topK != null)
588
+ params.push({ label: 'topK', value: String(input.topK) });
589
+ if (input?.toolChoice != null) {
590
+ const choice =
591
+ typeof input.toolChoice === 'string'
592
+ ? input.toolChoice
593
+ : input.toolChoice.type;
594
+ params.push({ label: 'tool choice', value: choice });
595
+ }
596
+
597
+ return (
598
+ <div className="flex items-center gap-2 px-4 py-2 bg-muted/20 border-b border-border text-[11px] text-muted-foreground">
599
+ {provider && (
600
+ <span className="px-1.5 py-0.5 rounded bg-sidebar-primary/10 text-sidebar-primary text-[10px] font-medium">
601
+ {provider}
602
+ </span>
603
+ )}
604
+ <span className="font-mono">{modelId}</span>
605
+
606
+ {params.length > 0 && (
607
+ <>
608
+ <span className="text-muted-foreground/30">·</span>
609
+ {params.map((p, i) => (
610
+ <span key={i}>
611
+ {p.label}: <span className="text-foreground">{p.value}</span>
612
+ {i < params.length - 1 && (
613
+ <span className="mx-1 text-muted-foreground/30">·</span>
614
+ )}
615
+ </span>
616
+ ))}
617
+ </>
618
+ )}
619
+
620
+ {toolCount > 0 && (
621
+ <>
622
+ <span className="text-muted-foreground/30">·</span>
623
+ <Drawer direction="right">
624
+ <DrawerTrigger asChild>
625
+ <button className="inline-flex gap-1 items-center transition-colors cursor-pointer hover:text-foreground">
626
+ <Wrench className="size-3" />
627
+ {toolCount} available {toolCount === 1 ? 'tool' : 'tools'}
628
+ </button>
629
+ </DrawerTrigger>
630
+ <DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
631
+ <DrawerHeader className="border-b border-border shrink-0">
632
+ <DrawerTitle>Available Tools ({toolCount})</DrawerTitle>
633
+ </DrawerHeader>
634
+ <div className="overflow-y-auto flex-1 p-4">
635
+ <div className="space-y-3">
636
+ {input?.tools?.map((tool: ToolDefinition, i: number) => (
637
+ <ToolItem key={i} tool={tool} />
638
+ ))}
639
+ </div>
640
+ </div>
641
+ </DrawerContent>
642
+ </Drawer>
643
+ </>
644
+ )}
645
+
646
+ {providerOptions && Object.keys(providerOptions).length > 0 && (
647
+ <>
648
+ <span className="text-muted-foreground/30">·</span>
649
+ <Drawer direction="right">
650
+ <DrawerTrigger asChild>
651
+ <button className="inline-flex gap-1 items-center transition-colors cursor-pointer hover:text-foreground">
652
+ <Settings className="size-3" />
653
+ Provider options
654
+ </button>
655
+ </DrawerTrigger>
656
+ <DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
657
+ <DrawerHeader className="border-b border-border shrink-0">
658
+ <DrawerTitle>Provider Options</DrawerTitle>
659
+ </DrawerHeader>
660
+ <div className="overflow-y-auto flex-1 p-4">
661
+ <JsonBlock data={providerOptions} size="lg" />
662
+ </div>
663
+ </DrawerContent>
664
+ </Drawer>
665
+ </>
666
+ )}
667
+
668
+ {usage && (
669
+ <>
670
+ <span className="text-muted-foreground/30">·</span>
671
+ <Drawer direction="right">
672
+ <DrawerTrigger asChild>
673
+ <button className="inline-flex gap-1 items-center transition-colors cursor-pointer hover:text-foreground">
674
+ <BarChart3 className="size-3" />
675
+ Usage
676
+ </button>
677
+ </DrawerTrigger>
678
+ <DrawerContent className="h-full w-[800px] sm:max-w-[800px] overflow-hidden">
679
+ <DrawerHeader className="border-b border-border shrink-0">
680
+ <DrawerTitle>Token Usage</DrawerTitle>
681
+ </DrawerHeader>
682
+ <div className="overflow-y-auto flex-1 p-4">
683
+ <UsageDetails usage={usage} />
684
+ </div>
685
+ </DrawerContent>
686
+ </Drawer>
687
+ </>
688
+ )}
689
+ </div>
690
+ );
691
+ }