@tonyclaw/llm-inspector 1.16.3 → 1.16.4
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/.output/nitro.json +1 -1
- package/.output/public/assets/{index-DfjhkDNi.js → index-X7CHS7fS.js} +40 -40
- package/.output/public/assets/{main-Cpts3Ifr.js → main-DbWwVQFh.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +12 -12
- package/.output/server/_ssr/{index-CjvQZBI0.mjs → index-C-z-fZtq.mjs} +195 -82
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-CO9_4CVh.mjs → router-CNM9Kbi0.mjs} +1 -1
- package/.output/server/{_tanstack-start-manifest_v-D-9SW7K3.mjs → _tanstack-start-manifest_v-BWfLeIsC.mjs} +1 -1
- package/.output/server/index.mjs +28 -28
- package/package.json +1 -1
- package/src/components/proxy-viewer/CompareDrawer.tsx +6 -6
- package/src/components/proxy-viewer/LogEntry.tsx +47 -12
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +1 -1
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +1 -1
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +1 -1
- package/src/components/ui/json-viewer.tsx +220 -84
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { Check, ChevronDown, ChevronRight, ChevronsDown, ChevronsUp, Copy } from "lucide-react";
|
|
2
|
-
import { type JSX, memo, useState } from "react";
|
|
2
|
+
import { type JSX, memo, useCallback, useMemo, useState, useTransition } from "react";
|
|
3
3
|
import ReactMarkdown from "react-markdown";
|
|
4
4
|
import { cn } from "../../lib/utils";
|
|
5
|
+
import { Button } from "./button";
|
|
5
6
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
|
|
6
7
|
|
|
7
8
|
type JsonPrimitive = string | number | boolean | null;
|
|
8
|
-
type JsonValue =
|
|
9
|
+
export type JsonValue =
|
|
10
|
+
| JsonPrimitive
|
|
11
|
+
| ReadonlyArray<JsonValue>
|
|
12
|
+
| Readonly<{ [key: string]: JsonValue }>;
|
|
9
13
|
|
|
10
14
|
type DataType = "string" | "number" | "boolean" | "null" | "array" | "object";
|
|
11
15
|
|
|
@@ -33,29 +37,141 @@ function isExpandable(value: JsonValue): boolean {
|
|
|
33
37
|
return value !== null && (Array.isArray(value) || typeof value === "object");
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
function getPropertyValue(obj: object, key: string): unknown {
|
|
37
|
-
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
|
|
38
|
-
if (descriptor === undefined) return undefined;
|
|
39
|
-
return descriptor.value;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
40
|
function getEntries(value: JsonValue): ReadonlyArray<readonly [string, JsonValue]> {
|
|
43
41
|
if (Array.isArray(value)) {
|
|
44
|
-
return value.map((item, index) => [String(index), item]
|
|
42
|
+
return value.map((item, index): readonly [string, JsonValue] => [String(index), item]);
|
|
45
43
|
}
|
|
46
44
|
if (typeof value === "object" && value !== null) {
|
|
47
|
-
return Object.
|
|
48
|
-
const raw = getPropertyValue(value, key);
|
|
49
|
-
return [key, safeJsonValue(raw)] as const;
|
|
50
|
-
});
|
|
45
|
+
return Object.entries(value);
|
|
51
46
|
}
|
|
52
47
|
return [];
|
|
53
48
|
}
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
export const JSON_FULL_EXPANSION_NODE_LIMIT = 1000;
|
|
51
|
+
export const JSON_LARGE_EXPANSION_DEPTH = 2;
|
|
52
|
+
|
|
53
|
+
export type JsonExpansionPolicy = {
|
|
54
|
+
depth: number;
|
|
55
|
+
limited: boolean;
|
|
56
|
+
countedNodes: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function countJsonNodes(value: JsonValue, limit = Number.POSITIVE_INFINITY): number {
|
|
60
|
+
let count = 0;
|
|
61
|
+
const pending: JsonValue[] = [value];
|
|
62
|
+
|
|
63
|
+
while (pending.length > 0) {
|
|
64
|
+
const current = pending.pop();
|
|
65
|
+
if (current === undefined) continue;
|
|
66
|
+
count += 1;
|
|
67
|
+
if (count > limit) return count;
|
|
68
|
+
if (!isExpandable(current)) continue;
|
|
69
|
+
for (const [, child] of getEntries(current)) {
|
|
70
|
+
pending.push(child);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return count;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getJsonExpansionPolicy(value: JsonValue): JsonExpansionPolicy {
|
|
78
|
+
const countedNodes = countJsonNodes(value, JSON_FULL_EXPANSION_NODE_LIMIT);
|
|
79
|
+
const limited = countedNodes > JSON_FULL_EXPANSION_NODE_LIMIT;
|
|
80
|
+
return {
|
|
81
|
+
depth: limited ? JSON_LARGE_EXPANSION_DEPTH : Number.POSITIVE_INFINITY,
|
|
82
|
+
limited,
|
|
83
|
+
countedNodes,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type JsonBulkExpansion = {
|
|
88
|
+
parsedData: JsonValue | null;
|
|
89
|
+
policy: JsonExpansionPolicy | null;
|
|
90
|
+
isExpanded: boolean;
|
|
91
|
+
toggle: () => void;
|
|
92
|
+
isPending: boolean;
|
|
93
|
+
bulkDepth: number;
|
|
94
|
+
bulkRevision: number;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export function useJsonBulkExpansion(text: string | null): JsonBulkExpansion {
|
|
98
|
+
const parsed = useMemo(() => (text === null ? null : parseJsonText(text)), [text]);
|
|
99
|
+
const parsedData = parsed?.kind === "json" ? parsed.data : null;
|
|
100
|
+
const policy = useMemo(
|
|
101
|
+
() => (parsedData === null ? null : getJsonExpansionPolicy(parsedData)),
|
|
102
|
+
[parsedData],
|
|
103
|
+
);
|
|
104
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
105
|
+
const [bulkDepth, setBulkDepth] = useState(0);
|
|
106
|
+
const [bulkRevision, setBulkRevision] = useState(0);
|
|
107
|
+
const [isPending, startTransition] = useTransition();
|
|
108
|
+
|
|
109
|
+
const toggle = useCallback(() => {
|
|
110
|
+
const nextExpanded = !isExpanded;
|
|
111
|
+
const targetDepth = nextExpanded && policy !== null ? policy.depth : 0;
|
|
112
|
+
startTransition(() => {
|
|
113
|
+
setIsExpanded(nextExpanded);
|
|
114
|
+
setBulkDepth(targetDepth);
|
|
115
|
+
setBulkRevision((current) => current + 1);
|
|
116
|
+
});
|
|
117
|
+
}, [isExpanded, policy]);
|
|
118
|
+
|
|
119
|
+
return { parsedData, policy, isExpanded, toggle, isPending, bulkDepth, bulkRevision };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function JsonExpansionButton({
|
|
123
|
+
policy,
|
|
124
|
+
isExpanded,
|
|
125
|
+
isPending,
|
|
126
|
+
onToggle,
|
|
127
|
+
}: {
|
|
128
|
+
policy: JsonExpansionPolicy | null;
|
|
129
|
+
isExpanded: boolean;
|
|
130
|
+
isPending: boolean;
|
|
131
|
+
onToggle: () => void;
|
|
132
|
+
}): JSX.Element | null {
|
|
133
|
+
if (policy === null) return null;
|
|
134
|
+
|
|
135
|
+
const label = isPending
|
|
136
|
+
? "Updating..."
|
|
137
|
+
: isExpanded
|
|
138
|
+
? "Collapse all"
|
|
139
|
+
: policy.limited
|
|
140
|
+
? `Expand ${JSON_LARGE_EXPANSION_DEPTH} levels`
|
|
141
|
+
: "Expand all";
|
|
142
|
+
|
|
143
|
+
const tooltip =
|
|
144
|
+
policy.limited && !isExpanded
|
|
145
|
+
? `Large JSON detected; expand ${JSON_LARGE_EXPANSION_DEPTH} levels to protect rendering performance`
|
|
146
|
+
: isExpanded
|
|
147
|
+
? "Collapse all JSON nodes"
|
|
148
|
+
: "Expand all JSON nodes";
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Tooltip>
|
|
152
|
+
<TooltipTrigger asChild>
|
|
153
|
+
<Button
|
|
154
|
+
variant="outline"
|
|
155
|
+
size="sm"
|
|
156
|
+
className="h-7 text-xs"
|
|
157
|
+
onClick={(e) => {
|
|
158
|
+
e.stopPropagation();
|
|
159
|
+
onToggle();
|
|
160
|
+
}}
|
|
161
|
+
disabled={isPending}
|
|
162
|
+
aria-pressed={isExpanded}
|
|
163
|
+
>
|
|
164
|
+
{isExpanded ? (
|
|
165
|
+
<ChevronsUp className="size-3 mr-1" />
|
|
166
|
+
) : (
|
|
167
|
+
<ChevronsDown className="size-3 mr-1" />
|
|
168
|
+
)}
|
|
169
|
+
{label}
|
|
170
|
+
</Button>
|
|
171
|
+
</TooltipTrigger>
|
|
172
|
+
<TooltipContent>{tooltip}</TooltipContent>
|
|
173
|
+
</Tooltip>
|
|
174
|
+
);
|
|
59
175
|
}
|
|
60
176
|
|
|
61
177
|
const STRING_TRUNCATE_LIMIT = 120;
|
|
@@ -206,16 +322,9 @@ type JsonNodeProps = {
|
|
|
206
322
|
isArrayItem: boolean;
|
|
207
323
|
};
|
|
208
324
|
|
|
209
|
-
function hasExpandableDescendant(value: JsonValue): boolean {
|
|
210
|
-
if (!isExpandable(value)) return false;
|
|
211
|
-
return getEntries(value).some(([, v]) => isExpandable(v));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
325
|
function ExpandCollapseButton({
|
|
215
|
-
allExpanded,
|
|
216
326
|
onClick,
|
|
217
327
|
}: {
|
|
218
|
-
allExpanded: boolean;
|
|
219
328
|
onClick: (e: React.MouseEvent) => void;
|
|
220
329
|
}): JSX.Element {
|
|
221
330
|
return (
|
|
@@ -223,13 +332,9 @@ function ExpandCollapseButton({
|
|
|
223
332
|
type="button"
|
|
224
333
|
onClick={onClick}
|
|
225
334
|
className="opacity-0 group-hover/row:opacity-100 hover:bg-muted p-0.5 rounded transition-opacity shrink-0"
|
|
226
|
-
title=
|
|
335
|
+
title="Expand all descendants"
|
|
227
336
|
>
|
|
228
|
-
|
|
229
|
-
<ChevronsUp className="size-3.5 text-muted-foreground" />
|
|
230
|
-
) : (
|
|
231
|
-
<ChevronsDown className="size-3.5 text-muted-foreground" />
|
|
232
|
-
)}
|
|
337
|
+
<ChevronsDown className="size-3.5 text-muted-foreground" />
|
|
233
338
|
</button>
|
|
234
339
|
);
|
|
235
340
|
}
|
|
@@ -244,30 +349,41 @@ const JsonNode = memo(function JsonNode({
|
|
|
244
349
|
const [expanded, setExpanded] = useState(level < defaultExpandDepth);
|
|
245
350
|
const [childResetKey, setChildResetKey] = useState(0);
|
|
246
351
|
const [childDepthOverride, setChildDepthOverride] = useState<number | null>(null);
|
|
247
|
-
const [allExpanded, setAllExpanded] = useState(false);
|
|
248
352
|
const expandable = isExpandable(value);
|
|
353
|
+
const fullyExpanded = childDepthOverride === Number.POSITIVE_INFINITY;
|
|
354
|
+
const entries = useMemo(() => getEntries(value), [value]);
|
|
355
|
+
const hasExpandableChild = useMemo(
|
|
356
|
+
() => entries.some(([, child]) => isExpandable(child)),
|
|
357
|
+
[entries],
|
|
358
|
+
);
|
|
249
359
|
|
|
250
360
|
const dataType = classifyValue(value);
|
|
251
361
|
const openBracket = dataType === "array" ? "[" : "{";
|
|
252
362
|
const closeBracket = dataType === "array" ? "]" : "}";
|
|
253
363
|
|
|
254
|
-
function
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
364
|
+
function expandAllDeep(): void {
|
|
365
|
+
setExpanded(true);
|
|
366
|
+
setChildDepthOverride(Number.POSITIVE_INFINITY);
|
|
367
|
+
setChildResetKey((k) => k + 1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function collapseToDefault(): void {
|
|
371
|
+
setExpanded(false);
|
|
372
|
+
setChildDepthOverride(0);
|
|
373
|
+
setChildResetKey((k) => k + 1);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function handleRowToggle(): void {
|
|
377
|
+
if (fullyExpanded) {
|
|
378
|
+
collapseToDefault();
|
|
260
379
|
} else {
|
|
261
|
-
setExpanded(
|
|
262
|
-
setChildDepthOverride(Number.POSITIVE_INFINITY);
|
|
263
|
-
setChildResetKey((k) => k + 1);
|
|
264
|
-
setAllExpanded(true);
|
|
380
|
+
setExpanded(!expanded);
|
|
265
381
|
}
|
|
266
382
|
}
|
|
267
383
|
|
|
268
384
|
function handleExpandAll(e: React.MouseEvent): void {
|
|
269
385
|
e.stopPropagation();
|
|
270
|
-
|
|
386
|
+
expandAllDeep();
|
|
271
387
|
}
|
|
272
388
|
|
|
273
389
|
const effectiveChildDepth = childDepthOverride ?? defaultExpandDepth;
|
|
@@ -279,29 +395,18 @@ const JsonNode = memo(function JsonNode({
|
|
|
279
395
|
"flex items-start gap-1 py-0.5 px-1 -ml-1 rounded-sm group/row",
|
|
280
396
|
expandable && "cursor-pointer hover:bg-muted/50",
|
|
281
397
|
)}
|
|
282
|
-
onClick={
|
|
283
|
-
expandable
|
|
284
|
-
? () => {
|
|
285
|
-
if (expanded) {
|
|
286
|
-
setAllExpanded(false);
|
|
287
|
-
}
|
|
288
|
-
setExpanded(!expanded);
|
|
289
|
-
}
|
|
290
|
-
: undefined
|
|
291
|
-
}
|
|
398
|
+
onClick={expandable ? handleRowToggle : undefined}
|
|
292
399
|
onKeyDown={
|
|
293
400
|
expandable
|
|
294
401
|
? (e) => {
|
|
295
402
|
if (e.key === "Enter" || e.key === " ") {
|
|
296
403
|
e.preventDefault();
|
|
297
|
-
|
|
404
|
+
handleRowToggle();
|
|
298
405
|
}
|
|
299
406
|
}
|
|
300
407
|
: undefined
|
|
301
408
|
}
|
|
302
|
-
onDoubleClick={
|
|
303
|
-
expandable && hasExpandableDescendant(value) ? () => toggleDeepExpansion() : undefined
|
|
304
|
-
}
|
|
409
|
+
onDoubleClick={expandable && hasExpandableChild ? () => expandAllDeep() : undefined}
|
|
305
410
|
role={expandable ? "button" : undefined}
|
|
306
411
|
tabIndex={expandable ? 0 : undefined}
|
|
307
412
|
>
|
|
@@ -328,7 +433,7 @@ const JsonNode = memo(function JsonNode({
|
|
|
328
433
|
{openBracket}
|
|
329
434
|
<span className="text-muted-foreground/60 text-xs">
|
|
330
435
|
{" "}
|
|
331
|
-
{
|
|
436
|
+
{entries.length} {entries.length === 1 ? "item" : "items"} {closeBracket}
|
|
332
437
|
</span>
|
|
333
438
|
</span>
|
|
334
439
|
) : (
|
|
@@ -337,15 +442,15 @@ const JsonNode = memo(function JsonNode({
|
|
|
337
442
|
</span>
|
|
338
443
|
)}
|
|
339
444
|
|
|
340
|
-
{expandable &&
|
|
341
|
-
<ExpandCollapseButton
|
|
445
|
+
{expandable && hasExpandableChild && !fullyExpanded && (
|
|
446
|
+
<ExpandCollapseButton onClick={handleExpandAll} />
|
|
342
447
|
)}
|
|
343
448
|
<CopyButton value={value} />
|
|
344
449
|
</div>
|
|
345
450
|
|
|
346
451
|
{expandable && expanded && (
|
|
347
452
|
<div className="pl-4" key={childResetKey}>
|
|
348
|
-
{
|
|
453
|
+
{entries.map(([key, childValue]) => (
|
|
349
454
|
<JsonNode
|
|
350
455
|
key={key}
|
|
351
456
|
name={key}
|
|
@@ -367,15 +472,23 @@ export type JsonViewerProps = {
|
|
|
367
472
|
defaultExpandDepth?: number;
|
|
368
473
|
className?: string;
|
|
369
474
|
showCopy?: boolean;
|
|
475
|
+
bulkDepth?: number;
|
|
476
|
+
bulkRevision?: number;
|
|
370
477
|
};
|
|
371
478
|
|
|
372
479
|
export function JsonViewer({
|
|
373
480
|
data,
|
|
374
|
-
defaultExpandDepth =
|
|
481
|
+
defaultExpandDepth = 0,
|
|
375
482
|
className,
|
|
376
483
|
showCopy = false,
|
|
484
|
+
bulkDepth: controlledBulkDepth,
|
|
485
|
+
bulkRevision: controlledBulkRevision,
|
|
377
486
|
}: JsonViewerProps): JSX.Element {
|
|
378
487
|
const expandable = isExpandable(data);
|
|
488
|
+
const entries = useMemo(() => getEntries(data), [data]);
|
|
489
|
+
|
|
490
|
+
const bulkDepth = controlledBulkDepth ?? defaultExpandDepth;
|
|
491
|
+
const bulkRevision = controlledBulkRevision ?? 0;
|
|
379
492
|
|
|
380
493
|
if (!expandable) {
|
|
381
494
|
return (
|
|
@@ -396,17 +509,23 @@ export function JsonViewer({
|
|
|
396
509
|
return (
|
|
397
510
|
<TooltipProvider>
|
|
398
511
|
<div className={cn("font-mono text-xs leading-relaxed", className)}>
|
|
399
|
-
{showCopy &&
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
512
|
+
{showCopy && (
|
|
513
|
+
<div className="mb-2 flex items-center justify-end gap-2">
|
|
514
|
+
<CopyValueButton value={data} />
|
|
515
|
+
</div>
|
|
516
|
+
)}
|
|
517
|
+
<div key={bulkRevision}>
|
|
518
|
+
{entries.map(([key, childValue]) => (
|
|
519
|
+
<JsonNode
|
|
520
|
+
key={key}
|
|
521
|
+
name={key}
|
|
522
|
+
value={childValue}
|
|
523
|
+
level={0}
|
|
524
|
+
defaultExpandDepth={bulkDepth}
|
|
525
|
+
isArrayItem={isArray}
|
|
526
|
+
/>
|
|
527
|
+
))}
|
|
528
|
+
</div>
|
|
410
529
|
</div>
|
|
411
530
|
</TooltipProvider>
|
|
412
531
|
);
|
|
@@ -418,28 +537,45 @@ export type JsonViewerFromStringProps = {
|
|
|
418
537
|
className?: string;
|
|
419
538
|
};
|
|
420
539
|
|
|
421
|
-
|
|
540
|
+
type ParsedJsonText =
|
|
541
|
+
| {
|
|
542
|
+
kind: "json";
|
|
543
|
+
data: JsonValue;
|
|
544
|
+
}
|
|
545
|
+
| {
|
|
546
|
+
kind: "text";
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
function parseJsonText(text: string): ParsedJsonText {
|
|
550
|
+
try {
|
|
551
|
+
const parsed: unknown = JSON.parse(text);
|
|
552
|
+
return { kind: "json", data: safeJsonValue(parsed) };
|
|
553
|
+
} catch {
|
|
554
|
+
return { kind: "text" };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export const JsonViewerFromString = memo(function JsonViewerFromString({
|
|
422
559
|
text,
|
|
423
|
-
defaultExpandDepth =
|
|
560
|
+
defaultExpandDepth = 0,
|
|
424
561
|
className,
|
|
425
562
|
}: JsonViewerFromStringProps): JSX.Element {
|
|
426
|
-
|
|
427
|
-
|
|
563
|
+
const parsed = useMemo(() => parseJsonText(text), [text]);
|
|
564
|
+
|
|
565
|
+
if (parsed.kind === "json") {
|
|
428
566
|
return (
|
|
429
567
|
<JsonViewer
|
|
430
|
-
data={
|
|
568
|
+
data={parsed.data}
|
|
431
569
|
defaultExpandDepth={defaultExpandDepth}
|
|
432
570
|
className={className}
|
|
433
571
|
/>
|
|
434
572
|
);
|
|
435
|
-
} catch {
|
|
436
|
-
return (
|
|
437
|
-
<pre className={cn("font-mono text-xs whitespace-pre-wrap break-words", className)}>
|
|
438
|
-
{text}
|
|
439
|
-
</pre>
|
|
440
|
-
);
|
|
441
573
|
}
|
|
442
|
-
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
<pre className={cn("font-mono text-xs whitespace-pre-wrap break-words", className)}>{text}</pre>
|
|
577
|
+
);
|
|
578
|
+
});
|
|
443
579
|
|
|
444
580
|
export function safeJsonValue(value: unknown): JsonValue {
|
|
445
581
|
if (value === null || value === undefined) return null;
|