@tonyclaw/llm-inspector 1.16.4 → 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-DbWwVQFh.js → main-CDMdNDY_.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +104 -84
- 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-C-z-fZtq.mjs → index-CDjLoMsk.mjs} +1026 -2455
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/json-viewer-B-qpM5xC.mjs +510 -0
- package/.output/server/_ssr/{router-CNM9Kbi0.mjs → router-BrdjOUEW.mjs} +24 -14
- package/.output/server/{_tanstack-start-manifest_v-BWfLeIsC.mjs → _tanstack-start-manifest_v-DmOZEcJ3.mjs} +1 -1
- package/.output/server/index.mjs +68 -26
- 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/ConversationGroup.tsx +1 -1
- package/src/components/proxy-viewer/ConversationHeader.tsx +4 -1
- package/src/components/proxy-viewer/LogEntry.tsx +213 -181
- package/src/components/proxy-viewer/LogEntryHeader.tsx +134 -36
- 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 +3 -23
- package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +21 -0
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +5 -3
- 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 +58 -183
- 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-X7CHS7fS.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,15 +1,11 @@
|
|
|
1
|
-
import { Check, ChevronDown, ChevronRight, ChevronsDown,
|
|
2
|
-
import { type JSX, memo,
|
|
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
5
|
import { Button } from "./button";
|
|
6
6
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export type JsonValue =
|
|
10
|
-
| JsonPrimitive
|
|
11
|
-
| ReadonlyArray<JsonValue>
|
|
12
|
-
| Readonly<{ [key: string]: JsonValue }>;
|
|
7
|
+
import type { JsonValue } from "./json-viewer-bulk";
|
|
8
|
+
import { parseJsonText, safeJsonValue } from "./json-viewer-bulk";
|
|
13
9
|
|
|
14
10
|
type DataType = "string" | "number" | "boolean" | "null" | "array" | "object";
|
|
15
11
|
|
|
@@ -47,133 +43,6 @@ function getEntries(value: JsonValue): ReadonlyArray<readonly [string, JsonValue
|
|
|
47
43
|
return [];
|
|
48
44
|
}
|
|
49
45
|
|
|
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
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
46
|
const STRING_TRUNCATE_LIMIT = 120;
|
|
178
47
|
|
|
179
48
|
function StringValue({ text }: { text: string }): JSX.Element {
|
|
@@ -320,6 +189,12 @@ type JsonNodeProps = {
|
|
|
320
189
|
level: number;
|
|
321
190
|
defaultExpandDepth: number;
|
|
322
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;
|
|
323
198
|
};
|
|
324
199
|
|
|
325
200
|
function ExpandCollapseButton({
|
|
@@ -345,8 +220,31 @@ const JsonNode = memo(function JsonNode({
|
|
|
345
220
|
level,
|
|
346
221
|
defaultExpandDepth,
|
|
347
222
|
isArrayItem,
|
|
223
|
+
path,
|
|
224
|
+
expandTargetPath,
|
|
225
|
+
anatomyPaths,
|
|
348
226
|
}: JsonNodeProps): JSX.Element {
|
|
349
|
-
|
|
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
|
+
|
|
350
248
|
const [childResetKey, setChildResetKey] = useState(0);
|
|
351
249
|
const [childDepthOverride, setChildDepthOverride] = useState<number | null>(null);
|
|
352
250
|
const expandable = isExpandable(value);
|
|
@@ -361,6 +259,8 @@ const JsonNode = memo(function JsonNode({
|
|
|
361
259
|
const openBracket = dataType === "array" ? "[" : "{";
|
|
362
260
|
const closeBracket = dataType === "array" ? "]" : "}";
|
|
363
261
|
|
|
262
|
+
const hasAnatomyPath = anatomyPaths !== null && anatomyPaths.has(path);
|
|
263
|
+
|
|
364
264
|
function expandAllDeep(): void {
|
|
365
265
|
setExpanded(true);
|
|
366
266
|
setChildDepthOverride(Number.POSITIVE_INFINITY);
|
|
@@ -395,6 +295,7 @@ const JsonNode = memo(function JsonNode({
|
|
|
395
295
|
"flex items-start gap-1 py-0.5 px-1 -ml-1 rounded-sm group/row",
|
|
396
296
|
expandable && "cursor-pointer hover:bg-muted/50",
|
|
397
297
|
)}
|
|
298
|
+
data-anatomy-path={hasAnatomyPath ? path : undefined}
|
|
398
299
|
onClick={expandable ? handleRowToggle : undefined}
|
|
399
300
|
onKeyDown={
|
|
400
301
|
expandable
|
|
@@ -458,6 +359,9 @@ const JsonNode = memo(function JsonNode({
|
|
|
458
359
|
level={level + 1}
|
|
459
360
|
defaultExpandDepth={effectiveChildDepth}
|
|
460
361
|
isArrayItem={dataType === "array"}
|
|
362
|
+
path={path === "" ? `/${key}` : `${path}/${key}`}
|
|
363
|
+
expandTargetPath={expandTargetPath}
|
|
364
|
+
anatomyPaths={anatomyPaths}
|
|
461
365
|
/>
|
|
462
366
|
))}
|
|
463
367
|
<div className="text-muted-foreground py-0.5 px-1">{closeBracket}</div>
|
|
@@ -474,6 +378,19 @@ export type JsonViewerProps = {
|
|
|
474
378
|
showCopy?: boolean;
|
|
475
379
|
bulkDepth?: number;
|
|
476
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;
|
|
477
394
|
};
|
|
478
395
|
|
|
479
396
|
export function JsonViewer({
|
|
@@ -483,6 +400,8 @@ export function JsonViewer({
|
|
|
483
400
|
showCopy = false,
|
|
484
401
|
bulkDepth: controlledBulkDepth,
|
|
485
402
|
bulkRevision: controlledBulkRevision,
|
|
403
|
+
anatomyPaths = null,
|
|
404
|
+
expandToPath = null,
|
|
486
405
|
}: JsonViewerProps): JSX.Element {
|
|
487
406
|
const expandable = isExpandable(data);
|
|
488
407
|
const entries = useMemo(() => getEntries(data), [data]);
|
|
@@ -523,6 +442,9 @@ export function JsonViewer({
|
|
|
523
442
|
level={0}
|
|
524
443
|
defaultExpandDepth={bulkDepth}
|
|
525
444
|
isArrayItem={isArray}
|
|
445
|
+
path={`/${key}`}
|
|
446
|
+
expandTargetPath={expandToPath}
|
|
447
|
+
anatomyPaths={anatomyPaths}
|
|
526
448
|
/>
|
|
527
449
|
))}
|
|
528
450
|
</div>
|
|
@@ -537,24 +459,6 @@ export type JsonViewerFromStringProps = {
|
|
|
537
459
|
className?: string;
|
|
538
460
|
};
|
|
539
461
|
|
|
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
462
|
export const JsonViewerFromString = memo(function JsonViewerFromString({
|
|
559
463
|
text,
|
|
560
464
|
defaultExpandDepth = 0,
|
|
@@ -576,32 +480,3 @@ export const JsonViewerFromString = memo(function JsonViewerFromString({
|
|
|
576
480
|
<pre className={cn("font-mono text-xs whitespace-pre-wrap break-words", className)}>{text}</pre>
|
|
577
481
|
);
|
|
578
482
|
});
|
|
579
|
-
|
|
580
|
-
export function safeJsonValue(value: unknown): JsonValue {
|
|
581
|
-
if (value === null || value === undefined) return null;
|
|
582
|
-
switch (typeof value) {
|
|
583
|
-
case "string":
|
|
584
|
-
return value;
|
|
585
|
-
case "number":
|
|
586
|
-
return value;
|
|
587
|
-
case "boolean":
|
|
588
|
-
return value;
|
|
589
|
-
case "object": {
|
|
590
|
-
if (Array.isArray(value)) {
|
|
591
|
-
return value.map((item: unknown) => safeJsonValue(item));
|
|
592
|
-
}
|
|
593
|
-
const result: Record<string, JsonValue> = {};
|
|
594
|
-
for (const key of Object.keys(value)) {
|
|
595
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
596
|
-
result[key] = safeJsonValue(descriptor?.value);
|
|
597
|
-
}
|
|
598
|
-
return result;
|
|
599
|
-
}
|
|
600
|
-
case "bigint":
|
|
601
|
-
case "symbol":
|
|
602
|
-
case "function":
|
|
603
|
-
case "undefined":
|
|
604
|
-
return String(value);
|
|
605
|
-
}
|
|
606
|
-
return null;
|
|
607
|
-
}
|
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
|
|