@tonyclaw/llm-inspector 1.16.3 → 1.16.5
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/CompareDrawer-C1w4KUGZ.js +1 -0
- package/.output/public/assets/ReplayDialog-DR2Sgq_g.js +1 -0
- package/.output/public/assets/RequestAnatomy-DAre35kj.js +1 -0
- package/.output/public/assets/ResponseView-ackes7_g.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-GrXwIGKA.js +1 -0
- package/.output/public/assets/index-BGzHFOEX.css +1 -0
- package/.output/public/assets/index-DX88k9br.js +101 -0
- package/.output/public/assets/json-viewer-C_QUhGeu.js +14 -0
- package/.output/public/assets/{main-Cpts3Ifr.js → main-CDMdNDY_.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +96 -76
- package/.output/server/_ssr/CompareDrawer-ftkJxyk6.mjs +1040 -0
- package/.output/server/_ssr/ReplayDialog-DcmE3lj5.mjs +321 -0
- package/.output/server/_ssr/RequestAnatomy-rK_LNMdG.mjs +351 -0
- package/.output/server/_ssr/ResponseView-CbQ4n-aJ.mjs +601 -0
- package/.output/server/_ssr/StreamingChunkSequence-84FZkIzv.mjs +301 -0
- package/.output/server/_ssr/{index-CjvQZBI0.mjs → index-CDjLoMsk.mjs} +1036 -2352
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/json-viewer-B-qpM5xC.mjs +510 -0
- package/.output/server/_ssr/{router-CO9_4CVh.mjs → router-BrdjOUEW.mjs} +24 -14
- package/.output/server/{_tanstack-start-manifest_v-D-9SW7K3.mjs → _tanstack-start-manifest_v-DmOZEcJ3.mjs} +1 -1
- package/.output/server/index.mjs +72 -30
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +2 -2
- package/src/components/ProxyViewer.tsx +38 -26
- package/src/components/ProxyViewerContainer.tsx +3 -24
- package/src/components/proxy-viewer/CompareDrawer.tsx +6 -6
- package/src/components/proxy-viewer/ConversationGroup.tsx +1 -1
- package/src/components/proxy-viewer/ConversationHeader.tsx +4 -1
- package/src/components/proxy-viewer/LogEntry.tsx +230 -163
- package/src/components/proxy-viewer/LogEntryHeader.tsx +134 -36
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +1 -1
- package/src/components/proxy-viewer/ThreadConnector.tsx +17 -2
- package/src/components/proxy-viewer/TurnGroup.tsx +94 -71
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +98 -0
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +196 -0
- package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +53 -0
- package/src/components/proxy-viewer/anatomy/types.ts +39 -0
- package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +114 -0
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +4 -24
- package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +21 -0
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +6 -4
- package/src/components/proxy-viewer/lazy.ts +37 -0
- package/src/components/proxy-viewer/log-formats/anthropic.ts +146 -0
- package/src/components/proxy-viewer/log-formats/openai.ts +127 -0
- package/src/components/proxy-viewer/log-formats/types.ts +7 -0
- package/src/components/proxy-viewer/log-formats/unknown.ts +4 -0
- package/src/components/proxy-viewer/logEntryVisibility.ts +39 -0
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +190 -0
- package/src/components/proxy-viewer/viewerState.ts +8 -0
- package/src/components/ui/crab-variants.tsx +11 -0
- package/src/components/ui/json-expansion-button.tsx +56 -0
- package/src/components/ui/json-viewer-bulk.ts +97 -0
- package/src/components/ui/json-viewer.tsx +129 -118
- package/src/lib/utils.ts +2 -3
- package/src/routes/api/logs.stream.ts +26 -16
- package/.output/public/assets/index-DRRCmu5p.css +0 -1
- package/.output/public/assets/index-DfjhkDNi.js +0 -107
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState, useTransition } from "react";
|
|
2
|
+
|
|
3
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
4
|
+
export type JsonValue =
|
|
5
|
+
| JsonPrimitive
|
|
6
|
+
| ReadonlyArray<JsonValue>
|
|
7
|
+
| Readonly<{ [key: string]: JsonValue }>;
|
|
8
|
+
|
|
9
|
+
export type JsonExpansionPolicy = {
|
|
10
|
+
depth: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function getJsonExpansionPolicy(_value: JsonValue): JsonExpansionPolicy {
|
|
14
|
+
return { depth: Number.POSITIVE_INFINITY };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type JsonBulkExpansion = {
|
|
18
|
+
parsedData: JsonValue | null;
|
|
19
|
+
policy: JsonExpansionPolicy | null;
|
|
20
|
+
isExpanded: boolean;
|
|
21
|
+
toggle: () => void;
|
|
22
|
+
isPending: boolean;
|
|
23
|
+
bulkDepth: number;
|
|
24
|
+
bulkRevision: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ParsedJsonText =
|
|
28
|
+
| {
|
|
29
|
+
kind: "json";
|
|
30
|
+
data: JsonValue;
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
kind: "text";
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function parseJsonText(text: string): ParsedJsonText {
|
|
37
|
+
try {
|
|
38
|
+
const parsed: unknown = JSON.parse(text);
|
|
39
|
+
return { kind: "json", data: safeJsonValue(parsed) };
|
|
40
|
+
} catch {
|
|
41
|
+
return { kind: "text" };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useJsonBulkExpansion(text: string | null): JsonBulkExpansion {
|
|
46
|
+
const parsed = useMemo(() => (text === null ? null : parseJsonText(text)), [text]);
|
|
47
|
+
const parsedData = parsed?.kind === "json" ? parsed.data : null;
|
|
48
|
+
const policy = useMemo(
|
|
49
|
+
() => (parsedData === null ? null : getJsonExpansionPolicy(parsedData)),
|
|
50
|
+
[parsedData],
|
|
51
|
+
);
|
|
52
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
53
|
+
const [bulkDepth, setBulkDepth] = useState(0);
|
|
54
|
+
const [bulkRevision, setBulkRevision] = useState(0);
|
|
55
|
+
const [isPending, startTransition] = useTransition();
|
|
56
|
+
|
|
57
|
+
const toggle = useCallback(() => {
|
|
58
|
+
const nextExpanded = !isExpanded;
|
|
59
|
+
const targetDepth = nextExpanded && policy !== null ? policy.depth : 0;
|
|
60
|
+
startTransition(() => {
|
|
61
|
+
setIsExpanded(nextExpanded);
|
|
62
|
+
setBulkDepth(targetDepth);
|
|
63
|
+
setBulkRevision((current) => current + 1);
|
|
64
|
+
});
|
|
65
|
+
}, [isExpanded, policy]);
|
|
66
|
+
|
|
67
|
+
return { parsedData, policy, isExpanded, toggle, isPending, bulkDepth, bulkRevision };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function safeJsonValue(value: unknown): JsonValue {
|
|
71
|
+
if (value === null || value === undefined) return null;
|
|
72
|
+
switch (typeof value) {
|
|
73
|
+
case "string":
|
|
74
|
+
return value;
|
|
75
|
+
case "number":
|
|
76
|
+
return value;
|
|
77
|
+
case "boolean":
|
|
78
|
+
return value;
|
|
79
|
+
case "object": {
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
return value.map((item: unknown) => safeJsonValue(item));
|
|
82
|
+
}
|
|
83
|
+
const result: Record<string, JsonValue> = {};
|
|
84
|
+
for (const key of Object.keys(value)) {
|
|
85
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
86
|
+
result[key] = safeJsonValue(descriptor?.value);
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
case "bigint":
|
|
91
|
+
case "symbol":
|
|
92
|
+
case "function":
|
|
93
|
+
case "undefined":
|
|
94
|
+
return String(value);
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Check, ChevronDown, ChevronRight, ChevronsDown,
|
|
2
|
-
import { type JSX, memo, useState } from "react";
|
|
1
|
+
import { Check, ChevronDown, ChevronRight, ChevronsDown, Copy } from "lucide-react";
|
|
2
|
+
import { type JSX, memo, useEffect, useMemo, useState } 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
|
-
|
|
8
|
-
type JsonValue = JsonPrimitive | ReadonlyArray<JsonValue> | Readonly<{ [key: string]: JsonValue }>;
|
|
7
|
+
import type { JsonValue } from "./json-viewer-bulk";
|
|
8
|
+
import { parseJsonText, safeJsonValue } from "./json-viewer-bulk";
|
|
9
9
|
|
|
10
10
|
type DataType = "string" | "number" | "boolean" | "null" | "array" | "object";
|
|
11
11
|
|
|
@@ -33,31 +33,16 @@ function isExpandable(value: JsonValue): boolean {
|
|
|
33
33
|
return value !== null && (Array.isArray(value) || typeof value === "object");
|
|
34
34
|
}
|
|
35
35
|
|
|
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
36
|
function getEntries(value: JsonValue): ReadonlyArray<readonly [string, JsonValue]> {
|
|
43
37
|
if (Array.isArray(value)) {
|
|
44
|
-
return value.map((item, index) => [String(index), item]
|
|
38
|
+
return value.map((item, index): readonly [string, JsonValue] => [String(index), item]);
|
|
45
39
|
}
|
|
46
40
|
if (typeof value === "object" && value !== null) {
|
|
47
|
-
return Object.
|
|
48
|
-
const raw = getPropertyValue(value, key);
|
|
49
|
-
return [key, safeJsonValue(raw)] as const;
|
|
50
|
-
});
|
|
41
|
+
return Object.entries(value);
|
|
51
42
|
}
|
|
52
43
|
return [];
|
|
53
44
|
}
|
|
54
45
|
|
|
55
|
-
function getItemCount(value: JsonValue): number {
|
|
56
|
-
if (Array.isArray(value)) return value.length;
|
|
57
|
-
if (typeof value === "object" && value !== null) return Object.keys(value).length;
|
|
58
|
-
return 0;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
46
|
const STRING_TRUNCATE_LIMIT = 120;
|
|
62
47
|
|
|
63
48
|
function StringValue({ text }: { text: string }): JSX.Element {
|
|
@@ -204,18 +189,17 @@ type JsonNodeProps = {
|
|
|
204
189
|
level: number;
|
|
205
190
|
defaultExpandDepth: number;
|
|
206
191
|
isArrayItem: boolean;
|
|
192
|
+
/** Full JSON-pointer-style path to this node (e.g. "/messages/3"). */
|
|
193
|
+
path: string;
|
|
194
|
+
/** When set, ancestors of this path auto-expand for one render. */
|
|
195
|
+
expandTargetPath: string | null;
|
|
196
|
+
/** Set of paths that should expose `data-anatomy-path` on their row. */
|
|
197
|
+
anatomyPaths: Set<string> | null;
|
|
207
198
|
};
|
|
208
199
|
|
|
209
|
-
function hasExpandableDescendant(value: JsonValue): boolean {
|
|
210
|
-
if (!isExpandable(value)) return false;
|
|
211
|
-
return getEntries(value).some(([, v]) => isExpandable(v));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
200
|
function ExpandCollapseButton({
|
|
215
|
-
allExpanded,
|
|
216
201
|
onClick,
|
|
217
202
|
}: {
|
|
218
|
-
allExpanded: boolean;
|
|
219
203
|
onClick: (e: React.MouseEvent) => void;
|
|
220
204
|
}): JSX.Element {
|
|
221
205
|
return (
|
|
@@ -223,13 +207,9 @@ function ExpandCollapseButton({
|
|
|
223
207
|
type="button"
|
|
224
208
|
onClick={onClick}
|
|
225
209
|
className="opacity-0 group-hover/row:opacity-100 hover:bg-muted p-0.5 rounded transition-opacity shrink-0"
|
|
226
|
-
title=
|
|
210
|
+
title="Expand all descendants"
|
|
227
211
|
>
|
|
228
|
-
|
|
229
|
-
<ChevronsUp className="size-3.5 text-muted-foreground" />
|
|
230
|
-
) : (
|
|
231
|
-
<ChevronsDown className="size-3.5 text-muted-foreground" />
|
|
232
|
-
)}
|
|
212
|
+
<ChevronsDown className="size-3.5 text-muted-foreground" />
|
|
233
213
|
</button>
|
|
234
214
|
);
|
|
235
215
|
}
|
|
@@ -240,34 +220,70 @@ const JsonNode = memo(function JsonNode({
|
|
|
240
220
|
level,
|
|
241
221
|
defaultExpandDepth,
|
|
242
222
|
isArrayItem,
|
|
223
|
+
path,
|
|
224
|
+
expandTargetPath,
|
|
225
|
+
anatomyPaths,
|
|
243
226
|
}: JsonNodeProps): JSX.Element {
|
|
244
|
-
|
|
227
|
+
// A node is an ancestor of the target if the target starts with this
|
|
228
|
+
// node's path followed by a path separator. The root path is "" and
|
|
229
|
+
// is an ancestor of every non-empty path.
|
|
230
|
+
const isAncestorOfTarget = useMemo(() => {
|
|
231
|
+
if (expandTargetPath === null) return false;
|
|
232
|
+
if (path === "") return expandTargetPath.length > 0;
|
|
233
|
+
return expandTargetPath === path || expandTargetPath.startsWith(`${path}/`);
|
|
234
|
+
}, [expandTargetPath, path]);
|
|
235
|
+
|
|
236
|
+
const initialExpanded = level < defaultExpandDepth || (isAncestorOfTarget && isExpandable(value));
|
|
237
|
+
|
|
238
|
+
const [expanded, setExpanded] = useState(initialExpanded);
|
|
239
|
+
// Re-evaluate on every render so a freshly-changed expandTargetPath
|
|
240
|
+
// immediately auto-expands a previously-collapsed ancestor.
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
if (isAncestorOfTarget && isExpandable(value) && !expanded) {
|
|
243
|
+
setExpanded(true);
|
|
244
|
+
}
|
|
245
|
+
// We deliberately only react to expandTargetPath changes here.
|
|
246
|
+
}, [expandTargetPath]);
|
|
247
|
+
|
|
245
248
|
const [childResetKey, setChildResetKey] = useState(0);
|
|
246
249
|
const [childDepthOverride, setChildDepthOverride] = useState<number | null>(null);
|
|
247
|
-
const [allExpanded, setAllExpanded] = useState(false);
|
|
248
250
|
const expandable = isExpandable(value);
|
|
251
|
+
const fullyExpanded = childDepthOverride === Number.POSITIVE_INFINITY;
|
|
252
|
+
const entries = useMemo(() => getEntries(value), [value]);
|
|
253
|
+
const hasExpandableChild = useMemo(
|
|
254
|
+
() => entries.some(([, child]) => isExpandable(child)),
|
|
255
|
+
[entries],
|
|
256
|
+
);
|
|
249
257
|
|
|
250
258
|
const dataType = classifyValue(value);
|
|
251
259
|
const openBracket = dataType === "array" ? "[" : "{";
|
|
252
260
|
const closeBracket = dataType === "array" ? "]" : "}";
|
|
253
261
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
262
|
+
const hasAnatomyPath = anatomyPaths !== null && anatomyPaths.has(path);
|
|
263
|
+
|
|
264
|
+
function expandAllDeep(): void {
|
|
265
|
+
setExpanded(true);
|
|
266
|
+
setChildDepthOverride(Number.POSITIVE_INFINITY);
|
|
267
|
+
setChildResetKey((k) => k + 1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function collapseToDefault(): void {
|
|
271
|
+
setExpanded(false);
|
|
272
|
+
setChildDepthOverride(0);
|
|
273
|
+
setChildResetKey((k) => k + 1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function handleRowToggle(): void {
|
|
277
|
+
if (fullyExpanded) {
|
|
278
|
+
collapseToDefault();
|
|
260
279
|
} else {
|
|
261
|
-
setExpanded(
|
|
262
|
-
setChildDepthOverride(Number.POSITIVE_INFINITY);
|
|
263
|
-
setChildResetKey((k) => k + 1);
|
|
264
|
-
setAllExpanded(true);
|
|
280
|
+
setExpanded(!expanded);
|
|
265
281
|
}
|
|
266
282
|
}
|
|
267
283
|
|
|
268
284
|
function handleExpandAll(e: React.MouseEvent): void {
|
|
269
285
|
e.stopPropagation();
|
|
270
|
-
|
|
286
|
+
expandAllDeep();
|
|
271
287
|
}
|
|
272
288
|
|
|
273
289
|
const effectiveChildDepth = childDepthOverride ?? defaultExpandDepth;
|
|
@@ -279,29 +295,19 @@ const JsonNode = memo(function JsonNode({
|
|
|
279
295
|
"flex items-start gap-1 py-0.5 px-1 -ml-1 rounded-sm group/row",
|
|
280
296
|
expandable && "cursor-pointer hover:bg-muted/50",
|
|
281
297
|
)}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
? () => {
|
|
285
|
-
if (expanded) {
|
|
286
|
-
setAllExpanded(false);
|
|
287
|
-
}
|
|
288
|
-
setExpanded(!expanded);
|
|
289
|
-
}
|
|
290
|
-
: undefined
|
|
291
|
-
}
|
|
298
|
+
data-anatomy-path={hasAnatomyPath ? path : undefined}
|
|
299
|
+
onClick={expandable ? handleRowToggle : undefined}
|
|
292
300
|
onKeyDown={
|
|
293
301
|
expandable
|
|
294
302
|
? (e) => {
|
|
295
303
|
if (e.key === "Enter" || e.key === " ") {
|
|
296
304
|
e.preventDefault();
|
|
297
|
-
|
|
305
|
+
handleRowToggle();
|
|
298
306
|
}
|
|
299
307
|
}
|
|
300
308
|
: undefined
|
|
301
309
|
}
|
|
302
|
-
onDoubleClick={
|
|
303
|
-
expandable && hasExpandableDescendant(value) ? () => toggleDeepExpansion() : undefined
|
|
304
|
-
}
|
|
310
|
+
onDoubleClick={expandable && hasExpandableChild ? () => expandAllDeep() : undefined}
|
|
305
311
|
role={expandable ? "button" : undefined}
|
|
306
312
|
tabIndex={expandable ? 0 : undefined}
|
|
307
313
|
>
|
|
@@ -328,7 +334,7 @@ const JsonNode = memo(function JsonNode({
|
|
|
328
334
|
{openBracket}
|
|
329
335
|
<span className="text-muted-foreground/60 text-xs">
|
|
330
336
|
{" "}
|
|
331
|
-
{
|
|
337
|
+
{entries.length} {entries.length === 1 ? "item" : "items"} {closeBracket}
|
|
332
338
|
</span>
|
|
333
339
|
</span>
|
|
334
340
|
) : (
|
|
@@ -337,15 +343,15 @@ const JsonNode = memo(function JsonNode({
|
|
|
337
343
|
</span>
|
|
338
344
|
)}
|
|
339
345
|
|
|
340
|
-
{expandable &&
|
|
341
|
-
<ExpandCollapseButton
|
|
346
|
+
{expandable && hasExpandableChild && !fullyExpanded && (
|
|
347
|
+
<ExpandCollapseButton onClick={handleExpandAll} />
|
|
342
348
|
)}
|
|
343
349
|
<CopyButton value={value} />
|
|
344
350
|
</div>
|
|
345
351
|
|
|
346
352
|
{expandable && expanded && (
|
|
347
353
|
<div className="pl-4" key={childResetKey}>
|
|
348
|
-
{
|
|
354
|
+
{entries.map(([key, childValue]) => (
|
|
349
355
|
<JsonNode
|
|
350
356
|
key={key}
|
|
351
357
|
name={key}
|
|
@@ -353,6 +359,9 @@ const JsonNode = memo(function JsonNode({
|
|
|
353
359
|
level={level + 1}
|
|
354
360
|
defaultExpandDepth={effectiveChildDepth}
|
|
355
361
|
isArrayItem={dataType === "array"}
|
|
362
|
+
path={path === "" ? `/${key}` : `${path}/${key}`}
|
|
363
|
+
expandTargetPath={expandTargetPath}
|
|
364
|
+
anatomyPaths={anatomyPaths}
|
|
356
365
|
/>
|
|
357
366
|
))}
|
|
358
367
|
<div className="text-muted-foreground py-0.5 px-1">{closeBracket}</div>
|
|
@@ -367,15 +376,38 @@ export type JsonViewerProps = {
|
|
|
367
376
|
defaultExpandDepth?: number;
|
|
368
377
|
className?: string;
|
|
369
378
|
showCopy?: boolean;
|
|
379
|
+
bulkDepth?: number;
|
|
380
|
+
bulkRevision?: number;
|
|
381
|
+
/**
|
|
382
|
+
* Set of JSON-pointer-style paths whose row should expose a
|
|
383
|
+
* `data-anatomy-path` attribute. Used by the Anatomy view to
|
|
384
|
+
* locate rows for click-to-jump. When `null` or `undefined`,
|
|
385
|
+
* no `data-anatomy-path` attribute is emitted (zero cost).
|
|
386
|
+
*/
|
|
387
|
+
anatomyPaths?: Set<string> | null;
|
|
388
|
+
/**
|
|
389
|
+
* When set, every ancestor of this path in the tree auto-expands
|
|
390
|
+
* for one render so the target row becomes visible. The hook
|
|
391
|
+
* flips this back to `null` after the scroll lands.
|
|
392
|
+
*/
|
|
393
|
+
expandToPath?: string | null;
|
|
370
394
|
};
|
|
371
395
|
|
|
372
396
|
export function JsonViewer({
|
|
373
397
|
data,
|
|
374
|
-
defaultExpandDepth =
|
|
398
|
+
defaultExpandDepth = 0,
|
|
375
399
|
className,
|
|
376
400
|
showCopy = false,
|
|
401
|
+
bulkDepth: controlledBulkDepth,
|
|
402
|
+
bulkRevision: controlledBulkRevision,
|
|
403
|
+
anatomyPaths = null,
|
|
404
|
+
expandToPath = null,
|
|
377
405
|
}: JsonViewerProps): JSX.Element {
|
|
378
406
|
const expandable = isExpandable(data);
|
|
407
|
+
const entries = useMemo(() => getEntries(data), [data]);
|
|
408
|
+
|
|
409
|
+
const bulkDepth = controlledBulkDepth ?? defaultExpandDepth;
|
|
410
|
+
const bulkRevision = controlledBulkRevision ?? 0;
|
|
379
411
|
|
|
380
412
|
if (!expandable) {
|
|
381
413
|
return (
|
|
@@ -396,17 +428,26 @@ export function JsonViewer({
|
|
|
396
428
|
return (
|
|
397
429
|
<TooltipProvider>
|
|
398
430
|
<div className={cn("font-mono text-xs leading-relaxed", className)}>
|
|
399
|
-
{showCopy &&
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
431
|
+
{showCopy && (
|
|
432
|
+
<div className="mb-2 flex items-center justify-end gap-2">
|
|
433
|
+
<CopyValueButton value={data} />
|
|
434
|
+
</div>
|
|
435
|
+
)}
|
|
436
|
+
<div key={bulkRevision}>
|
|
437
|
+
{entries.map(([key, childValue]) => (
|
|
438
|
+
<JsonNode
|
|
439
|
+
key={key}
|
|
440
|
+
name={key}
|
|
441
|
+
value={childValue}
|
|
442
|
+
level={0}
|
|
443
|
+
defaultExpandDepth={bulkDepth}
|
|
444
|
+
isArrayItem={isArray}
|
|
445
|
+
path={`/${key}`}
|
|
446
|
+
expandTargetPath={expandToPath}
|
|
447
|
+
anatomyPaths={anatomyPaths}
|
|
448
|
+
/>
|
|
449
|
+
))}
|
|
450
|
+
</div>
|
|
410
451
|
</div>
|
|
411
452
|
</TooltipProvider>
|
|
412
453
|
);
|
|
@@ -418,54 +459,24 @@ export type JsonViewerFromStringProps = {
|
|
|
418
459
|
className?: string;
|
|
419
460
|
};
|
|
420
461
|
|
|
421
|
-
export function JsonViewerFromString({
|
|
462
|
+
export const JsonViewerFromString = memo(function JsonViewerFromString({
|
|
422
463
|
text,
|
|
423
|
-
defaultExpandDepth =
|
|
464
|
+
defaultExpandDepth = 0,
|
|
424
465
|
className,
|
|
425
466
|
}: JsonViewerFromStringProps): JSX.Element {
|
|
426
|
-
|
|
427
|
-
|
|
467
|
+
const parsed = useMemo(() => parseJsonText(text), [text]);
|
|
468
|
+
|
|
469
|
+
if (parsed.kind === "json") {
|
|
428
470
|
return (
|
|
429
471
|
<JsonViewer
|
|
430
|
-
data={
|
|
472
|
+
data={parsed.data}
|
|
431
473
|
defaultExpandDepth={defaultExpandDepth}
|
|
432
474
|
className={className}
|
|
433
475
|
/>
|
|
434
476
|
);
|
|
435
|
-
} catch {
|
|
436
|
-
return (
|
|
437
|
-
<pre className={cn("font-mono text-xs whitespace-pre-wrap break-words", className)}>
|
|
438
|
-
{text}
|
|
439
|
-
</pre>
|
|
440
|
-
);
|
|
441
477
|
}
|
|
442
|
-
}
|
|
443
478
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return value;
|
|
449
|
-
case "number":
|
|
450
|
-
return value;
|
|
451
|
-
case "boolean":
|
|
452
|
-
return value;
|
|
453
|
-
case "object": {
|
|
454
|
-
if (Array.isArray(value)) {
|
|
455
|
-
return value.map((item: unknown) => safeJsonValue(item));
|
|
456
|
-
}
|
|
457
|
-
const result: Record<string, JsonValue> = {};
|
|
458
|
-
for (const key of Object.keys(value)) {
|
|
459
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
460
|
-
result[key] = safeJsonValue(descriptor?.value);
|
|
461
|
-
}
|
|
462
|
-
return result;
|
|
463
|
-
}
|
|
464
|
-
case "bigint":
|
|
465
|
-
case "symbol":
|
|
466
|
-
case "function":
|
|
467
|
-
case "undefined":
|
|
468
|
-
return String(value);
|
|
469
|
-
}
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
479
|
+
return (
|
|
480
|
+
<pre className={cn("font-mono text-xs whitespace-pre-wrap break-words", className)}>{text}</pre>
|
|
481
|
+
);
|
|
482
|
+
});
|
package/src/lib/utils.ts
CHANGED
|
@@ -6,9 +6,8 @@ export function cn(...inputs: ClassValue[]): string {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function formatTokens(count: number): string {
|
|
9
|
-
if (count >=
|
|
10
|
-
|
|
11
|
-
return count.toString();
|
|
9
|
+
if (count >= 1_000_000) return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
10
|
+
return (count / 1000).toFixed(1).replace(/\.0$/, "") + "K";
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
export type StatusCategory = "success" | "client_error" | "server_error" | "pending";
|
|
@@ -11,20 +11,10 @@ export const Route = createFileRoute("/api/logs/stream")({
|
|
|
11
11
|
const model = url.searchParams.get("model") ?? undefined;
|
|
12
12
|
|
|
13
13
|
let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
14
|
-
|
|
15
|
-
// Send heartbeat comment every 30s to keep connection alive
|
|
16
|
-
const heartbeat = setInterval(() => {
|
|
17
|
-
if (controllerRef) {
|
|
18
|
-
try {
|
|
19
|
-
controllerRef.enqueue(new TextEncoder().encode(": heartbeat\n\n"));
|
|
20
|
-
} catch {
|
|
21
|
-
// Stream closed
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}, 30000);
|
|
14
|
+
let cleanedUp = false;
|
|
25
15
|
|
|
26
16
|
const unsubscribe = onLogUpdate((log: CapturedLog) => {
|
|
27
|
-
if (
|
|
17
|
+
if (controllerRef === null) return;
|
|
28
18
|
// Filter by session/model if specified
|
|
29
19
|
if (sessionId !== undefined && log.sessionId !== sessionId) return;
|
|
30
20
|
if (model !== undefined && log.model !== model) return;
|
|
@@ -32,10 +22,32 @@ export const Route = createFileRoute("/api/logs/stream")({
|
|
|
32
22
|
const data = `data: ${JSON.stringify({ type: "update", log })}\n\n`;
|
|
33
23
|
controllerRef.enqueue(new TextEncoder().encode(data));
|
|
34
24
|
} catch {
|
|
35
|
-
|
|
25
|
+
cleanup();
|
|
36
26
|
}
|
|
37
27
|
});
|
|
38
28
|
|
|
29
|
+
const cleanup = (): void => {
|
|
30
|
+
if (cleanedUp) return;
|
|
31
|
+
cleanedUp = true;
|
|
32
|
+
clearInterval(heartbeat);
|
|
33
|
+
unsubscribe();
|
|
34
|
+
controllerRef = null;
|
|
35
|
+
request.signal.removeEventListener("abort", cleanup);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Send heartbeat comment every 30s to keep connection alive
|
|
39
|
+
const heartbeat = setInterval(() => {
|
|
40
|
+
if (controllerRef !== null) {
|
|
41
|
+
try {
|
|
42
|
+
controllerRef.enqueue(new TextEncoder().encode(": heartbeat\n\n"));
|
|
43
|
+
} catch {
|
|
44
|
+
cleanup();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}, 30000);
|
|
48
|
+
|
|
49
|
+
request.signal.addEventListener("abort", cleanup, { once: true });
|
|
50
|
+
|
|
39
51
|
const stream = new ReadableStream<Uint8Array>({
|
|
40
52
|
start(controller) {
|
|
41
53
|
controllerRef = controller;
|
|
@@ -45,9 +57,7 @@ export const Route = createFileRoute("/api/logs/stream")({
|
|
|
45
57
|
controller.enqueue(new TextEncoder().encode(initData));
|
|
46
58
|
},
|
|
47
59
|
cancel() {
|
|
48
|
-
|
|
49
|
-
unsubscribe();
|
|
50
|
-
controllerRef = null;
|
|
60
|
+
cleanup();
|
|
51
61
|
},
|
|
52
62
|
});
|
|
53
63
|
|