@tonyclaw/llm-inspector 1.14.9 → 1.15.0
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-CMuJQyt1.js +105 -0
- package/.output/public/assets/index-DciyfYBk.css +1 -0
- package/.output/public/assets/{main-C8OUJKbz.js → main-BLYgekFx.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +85 -111
- package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
- package/.output/server/_ssr/{index-_9xcAkkw.mjs → index-P66uoVEU.mjs} +570 -287
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-CmanwZJc.mjs → router-DpLCKk51.mjs} +2 -2
- package/.output/server/{_tanstack-start-manifest_v-BVIiyDeJ.mjs → _tanstack-start-manifest_v-C9Wq6YdJ.mjs} +1 -1
- package/.output/server/index.mjs +27 -27
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +99 -203
- package/src/components/proxy-viewer/ConversationGroup.tsx +57 -76
- package/src/components/proxy-viewer/ConversationHeader.tsx +1 -40
- package/src/components/proxy-viewer/LogEntryHeader.tsx +3 -3
- package/src/components/proxy-viewer/ThreadConnector.tsx +64 -40
- package/src/components/proxy-viewer/TurnGroup.tsx +83 -0
- package/src/components/ui/crab-variants.tsx +456 -0
- package/src/lib/stopReason.ts +7 -6
- package/styles/globals.css +38 -0
- package/.output/public/assets/index-Dv-dj1xH.js +0 -105
- package/.output/public/assets/index-bqeypwJB.css +0 -1
|
@@ -198,7 +198,7 @@ function getResponse() {
|
|
|
198
198
|
return event.res;
|
|
199
199
|
}
|
|
200
200
|
async function getStartManifest(matchedRoutes) {
|
|
201
|
-
const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-
|
|
201
|
+
const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-C9Wq6YdJ.mjs");
|
|
202
202
|
const startManifest = tsrStartManifest();
|
|
203
203
|
const rootRoute = startManifest.routes[rootRouteId] = startManifest.routes[rootRouteId] || {};
|
|
204
204
|
rootRoute.assets = rootRoute.assets || [];
|
|
@@ -767,7 +767,7 @@ let entriesPromise;
|
|
|
767
767
|
let baseManifestPromise;
|
|
768
768
|
let cachedFinalManifestPromise;
|
|
769
769
|
async function loadEntries() {
|
|
770
|
-
const routerEntry = await import("./router-
|
|
770
|
+
const routerEntry = await import("./router-DpLCKk51.mjs").then((n) => n.r);
|
|
771
771
|
const startEntry = await import("./start-HYkvq4Ni.mjs");
|
|
772
772
|
return { startEntry, routerEntry };
|
|
773
773
|
}
|
|
@@ -45,7 +45,7 @@ import "../_libs/debounce-fn.mjs";
|
|
|
45
45
|
import "../_libs/mimic-function.mjs";
|
|
46
46
|
import "../_libs/semver.mjs";
|
|
47
47
|
import "../_libs/uint8array-extras.mjs";
|
|
48
|
-
const appCss = "/assets/index-
|
|
48
|
+
const appCss = "/assets/index-DciyfYBk.css";
|
|
49
49
|
const Route$k = createRootRoute({
|
|
50
50
|
head: () => ({
|
|
51
51
|
meta: [
|
|
@@ -69,7 +69,7 @@ function RootDocument({ children }) {
|
|
|
69
69
|
] })
|
|
70
70
|
] });
|
|
71
71
|
}
|
|
72
|
-
const $$splitComponentImporter = () => import("./index-
|
|
72
|
+
const $$splitComponentImporter = () => import("./index-P66uoVEU.mjs");
|
|
73
73
|
const Route$j = createFileRoute("/")({
|
|
74
74
|
component: lazyRouteComponent($$splitComponentImporter, "component")
|
|
75
75
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/config", "/api/health", "/api/logs", "/api/mcp", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-
|
|
1
|
+
const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/config", "/api/health", "/api/logs", "/api/mcp", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-BLYgekFx.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-CMuJQyt1.js"] }, "/api/config": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.ts", "children": ["/api/config/paths"] }, "/api/health": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/health.ts" }, "/api/logs": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.ts", "children": ["/api/logs/$id", "/api/logs/stream"] }, "/api/mcp": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/mcp.ts" }, "/api/models": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/models.ts" }, "/api/providers": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.ts", "children": ["/api/providers/$providerId", "/api/providers/export", "/api/providers/import", "/api/providers/scan"] }, "/api/sessions": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/sessions.ts" }, "/proxy/$": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/proxy/$.ts" }, "/api/config/paths": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.paths.ts" }, "/api/logs/$id": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.ts", "children": ["/api/logs/$id/chunks", "/api/logs/$id/replay"] }, "/api/logs/stream": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.stream.ts" }, "/api/providers/$providerId": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.ts", "children": ["/api/providers/$providerId/test"] }, "/api/providers/export": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.export.ts" }, "/api/providers/import": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.import.ts" }, "/api/providers/scan": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.scan.ts" }, "/api/logs/$id/chunks": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.chunks.ts" }, "/api/logs/$id/replay": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.replay.ts" }, "/api/providers/$providerId/test": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.ts", "children": ["/api/providers/$providerId/test/log"] }, "/api/providers/$providerId/test/log": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.log.ts" } }, "clientEntry": "/assets/main-BLYgekFx.js" });
|
|
2
2
|
export {
|
|
3
3
|
tsrStartManifest
|
|
4
4
|
};
|
package/.output/server/index.mjs
CHANGED
|
@@ -35,54 +35,54 @@ const headers = ((m) => function headersRouteRule(event) {
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
const assets = {
|
|
38
|
+
"/assets/index-DciyfYBk.css": {
|
|
39
|
+
"type": "text/css; charset=utf-8",
|
|
40
|
+
"etag": '"15867-TOX1Gddns0p3jKt+7co1NuZS+d8"',
|
|
41
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
42
|
+
"size": 88167,
|
|
43
|
+
"path": "../public/assets/index-DciyfYBk.css"
|
|
44
|
+
},
|
|
38
45
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
39
46
|
"type": "image/svg+xml",
|
|
40
47
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
41
|
-
"mtime": "2026-06-
|
|
48
|
+
"mtime": "2026-06-12T07:17:56.428Z",
|
|
42
49
|
"size": 5915,
|
|
43
50
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
44
51
|
},
|
|
45
|
-
"/assets/
|
|
46
|
-
"type": "text/
|
|
47
|
-
"etag": '"
|
|
48
|
-
"mtime": "2026-06-
|
|
49
|
-
"size":
|
|
50
|
-
"path": "../public/assets/
|
|
51
|
-
},
|
|
52
|
-
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
53
|
-
"type": "image/jpeg",
|
|
54
|
-
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
55
|
-
"mtime": "2026-06-11T13:44:56.125Z",
|
|
56
|
-
"size": 6918,
|
|
57
|
-
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
52
|
+
"/assets/main-BLYgekFx.js": {
|
|
53
|
+
"type": "text/javascript; charset=utf-8",
|
|
54
|
+
"etag": '"50599-tks6aTeb/Edw4laR3cdpiWA48mc"',
|
|
55
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
56
|
+
"size": 329113,
|
|
57
|
+
"path": "../public/assets/main-BLYgekFx.js"
|
|
58
58
|
},
|
|
59
59
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
60
60
|
"type": "image/svg+xml",
|
|
61
61
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
62
|
-
"mtime": "2026-06-
|
|
62
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
63
63
|
"size": 11256,
|
|
64
64
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
65
65
|
},
|
|
66
|
-
"/assets/
|
|
67
|
-
"type": "
|
|
68
|
-
"etag": '"
|
|
69
|
-
"mtime": "2026-06-
|
|
70
|
-
"size":
|
|
71
|
-
"path": "../public/assets/
|
|
66
|
+
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
67
|
+
"type": "image/jpeg",
|
|
68
|
+
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
69
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
70
|
+
"size": 6918,
|
|
71
|
+
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
72
72
|
},
|
|
73
73
|
"/assets/qwen-CONDcHqt.png": {
|
|
74
74
|
"type": "image/png",
|
|
75
75
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
76
|
-
"mtime": "2026-06-
|
|
76
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
77
77
|
"size": 357059,
|
|
78
78
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
79
79
|
},
|
|
80
|
-
"/assets/index-
|
|
80
|
+
"/assets/index-CMuJQyt1.js": {
|
|
81
81
|
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"
|
|
83
|
-
"mtime": "2026-06-
|
|
84
|
-
"size":
|
|
85
|
-
"path": "../public/assets/index-
|
|
82
|
+
"etag": '"97f56-eCDTvraNzTc2Un6pdqimYNiZVhw"',
|
|
83
|
+
"mtime": "2026-06-12T07:17:56.431Z",
|
|
84
|
+
"size": 622422,
|
|
85
|
+
"path": "../public/assets/index-CMuJQyt1.js"
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
88
|
function readAsset(id) {
|
package/package.json
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import { type JSX, useCallback, useEffect, useMemo, useState, useRef } from "react";
|
|
2
2
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
3
|
-
import { Download
|
|
4
|
-
|
|
3
|
+
import { Download } from "lucide-react";
|
|
4
|
+
|
|
5
5
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip";
|
|
6
6
|
import type { CapturedLog } from "../proxy/schemas";
|
|
7
7
|
import { exportLogsAsZip } from "../lib/export-logs";
|
|
8
8
|
import packageJson from "../../package.json";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
groupLogsByConversation,
|
|
12
|
-
LogEntry,
|
|
13
|
-
ThreadConnector,
|
|
14
|
-
type ConversationGroupData,
|
|
15
|
-
type ViewMode,
|
|
16
|
-
} from "./proxy-viewer";
|
|
17
|
-
import { extractStopReason } from "../lib/stopReason";
|
|
9
|
+
import { ConversationGroup, groupLogsByConversation } from "./proxy-viewer";
|
|
10
|
+
|
|
18
11
|
import { CrabLogo } from "./ui/crab-logo";
|
|
12
|
+
import { crabVariants } from "./ui/crab-variants";
|
|
19
13
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
20
14
|
import { SettingsDialog } from "./providers/SettingsDialog";
|
|
21
15
|
import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
@@ -117,8 +111,6 @@ export function ProxyViewer({
|
|
|
117
111
|
strip,
|
|
118
112
|
}: ProxyViewerProps): JSX.Element {
|
|
119
113
|
const { totalIn, totalOut } = computeTokenSummary(logs);
|
|
120
|
-
const [groupedView, setGroupedView] = useState(true);
|
|
121
|
-
const [groupViewMode, setGroupViewMode] = useState<ViewMode>("thread");
|
|
122
114
|
const [exporting, setExporting] = useState(false);
|
|
123
115
|
const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
|
|
124
116
|
|
|
@@ -155,25 +147,9 @@ export function ProxyViewer({
|
|
|
155
147
|
|
|
156
148
|
const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
|
|
157
149
|
const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
|
|
158
|
-
const stopReasons = useMemo(() => logs.map((log) => extractStopReason(log)), [logs]);
|
|
159
|
-
|
|
160
|
-
const turnIndices = useMemo(() => {
|
|
161
|
-
const indices: number[] = [];
|
|
162
|
-
let turn = 0;
|
|
163
|
-
for (let i = 0; i < stopReasons.length; i++) {
|
|
164
|
-
if (i > 0 && (stopReasons[i - 1] === "end_turn" || stopReasons[i - 1] === "stop")) {
|
|
165
|
-
turn++;
|
|
166
|
-
}
|
|
167
|
-
indices.push(turn);
|
|
168
|
-
}
|
|
169
|
-
return indices;
|
|
170
|
-
}, [stopReasons]);
|
|
171
|
-
|
|
172
|
-
// Determine what items to render (groups or individual logs)
|
|
173
|
-
const renderGroups = logs.length > 0 && groupedView && groups.length > 1;
|
|
174
150
|
|
|
175
151
|
const rowVirtualizer = useVirtualizer({
|
|
176
|
-
count:
|
|
152
|
+
count: groups.length,
|
|
177
153
|
getScrollElement: () => parentRef.current,
|
|
178
154
|
estimateSize: () => 150,
|
|
179
155
|
measureElement:
|
|
@@ -185,15 +161,75 @@ export function ProxyViewer({
|
|
|
185
161
|
|
|
186
162
|
return (
|
|
187
163
|
<div className="max-w-[1200px] mx-auto flex flex-col h-screen" style={{ maxHeight: "100vh" }}>
|
|
188
|
-
{/*
|
|
189
|
-
<div className="flex items-
|
|
190
|
-
<h1 className="text-lg font-bold flex
|
|
191
|
-
|
|
164
|
+
{/* Brand row */}
|
|
165
|
+
<div className="flex items-end px-6 pt-6 pb-3 relative">
|
|
166
|
+
<h1 className="text-lg font-bold flex items-end gap-2 absolute left-1/2 -translate-x-1/2 whitespace-nowrap">
|
|
167
|
+
{/* Crab family — hover to animate together */}
|
|
168
|
+
<span className="flex items-end gap-1 group cursor-default" aria-hidden="true">
|
|
169
|
+
<CrabLogo className="size-10 text-amber-500 transition-all duration-300 group-hover:scale-125 group-hover:-translate-y-1.5" />
|
|
170
|
+
<span className="flex items-end gap-0.5">
|
|
171
|
+
{crabVariants.map((Crab, i) => (
|
|
172
|
+
<Crab
|
|
173
|
+
key={i}
|
|
174
|
+
className={`size-5 ${
|
|
175
|
+
[
|
|
176
|
+
"text-amber-500",
|
|
177
|
+
"text-rose-500",
|
|
178
|
+
"text-sky-500",
|
|
179
|
+
"text-emerald-500",
|
|
180
|
+
"text-violet-500",
|
|
181
|
+
"text-orange-500",
|
|
182
|
+
"text-cyan-500",
|
|
183
|
+
"text-pink-500",
|
|
184
|
+
"text-lime-500",
|
|
185
|
+
"text-blue-500",
|
|
186
|
+
"text-yellow-500",
|
|
187
|
+
"text-fuchsia-500",
|
|
188
|
+
][i]
|
|
189
|
+
} transition-all duration-300 ease-out group-hover:scale-125 group-hover:-translate-y-1`}
|
|
190
|
+
style={{ transitionDelay: `${i * 50}ms` }}
|
|
191
|
+
/>
|
|
192
|
+
))}
|
|
193
|
+
</span>
|
|
194
|
+
</span>
|
|
192
195
|
<span className="flex items-baseline gap-2">
|
|
193
196
|
LLM Inspector
|
|
194
197
|
<span className="text-xs text-muted-foreground font-mono">v{packageJson.version}</span>
|
|
195
198
|
</span>
|
|
196
199
|
</h1>
|
|
200
|
+
<div className="ml-auto">
|
|
201
|
+
<SettingsDialog />
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{/* Controls + Filters */}
|
|
206
|
+
<div className="flex items-center gap-3 px-6 mb-4">
|
|
207
|
+
<Select value={selectedSession} onValueChange={onSessionChange}>
|
|
208
|
+
<SelectTrigger className="flex-1 max-w-[350px] text-xs">
|
|
209
|
+
<SelectValue placeholder="All sessions" />
|
|
210
|
+
</SelectTrigger>
|
|
211
|
+
<SelectContent>
|
|
212
|
+
<SelectItem value="__all__">All sessions</SelectItem>
|
|
213
|
+
{sessions.map((s) => (
|
|
214
|
+
<SelectItem key={s} value={s}>
|
|
215
|
+
{truncateSessionId(s)}
|
|
216
|
+
</SelectItem>
|
|
217
|
+
))}
|
|
218
|
+
</SelectContent>
|
|
219
|
+
</Select>
|
|
220
|
+
<Select value={selectedModel} onValueChange={onModelChange}>
|
|
221
|
+
<SelectTrigger className="flex-1 max-w-[250px] text-xs">
|
|
222
|
+
<SelectValue placeholder="All models" />
|
|
223
|
+
</SelectTrigger>
|
|
224
|
+
<SelectContent>
|
|
225
|
+
<SelectItem value="__all__">All models</SelectItem>
|
|
226
|
+
{models.map((m) => (
|
|
227
|
+
<SelectItem key={m} value={m}>
|
|
228
|
+
{m}
|
|
229
|
+
</SelectItem>
|
|
230
|
+
))}
|
|
231
|
+
</SelectContent>
|
|
232
|
+
</Select>
|
|
197
233
|
<TooltipProvider>
|
|
198
234
|
<Tooltip>
|
|
199
235
|
<TooltipTrigger asChild>
|
|
@@ -227,7 +263,7 @@ export function ProxyViewer({
|
|
|
227
263
|
</TooltipContent>
|
|
228
264
|
</Tooltip>
|
|
229
265
|
</TooltipProvider>
|
|
230
|
-
<
|
|
266
|
+
<div className="flex-1" />
|
|
231
267
|
<span className="text-muted-foreground text-xs font-mono">
|
|
232
268
|
{logs.length} request{logs.length !== 1 ? "s" : ""}
|
|
233
269
|
{totalIn > 0 || totalOut > 0
|
|
@@ -249,7 +285,7 @@ export function ProxyViewer({
|
|
|
249
285
|
) : (
|
|
250
286
|
<>
|
|
251
287
|
<Download className="size-3" />
|
|
252
|
-
<span>Export
|
|
288
|
+
<span>Export</span>
|
|
253
289
|
</>
|
|
254
290
|
)}
|
|
255
291
|
</button>
|
|
@@ -260,97 +296,15 @@ export function ProxyViewer({
|
|
|
260
296
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
|
261
297
|
title="Clear all logs"
|
|
262
298
|
>
|
|
263
|
-
Clear
|
|
299
|
+
Clear
|
|
264
300
|
</button>
|
|
265
301
|
</div>
|
|
266
302
|
|
|
267
|
-
{/* Filters */}
|
|
268
|
-
<div className="flex gap-3 px-6 mb-4">
|
|
269
|
-
<Select value={selectedSession} onValueChange={onSessionChange}>
|
|
270
|
-
<SelectTrigger className="flex-1 max-w-[400px] text-xs">
|
|
271
|
-
<SelectValue placeholder="All sessions" />
|
|
272
|
-
</SelectTrigger>
|
|
273
|
-
<SelectContent>
|
|
274
|
-
<SelectItem value="__all__">All sessions</SelectItem>
|
|
275
|
-
{sessions.map((s) => (
|
|
276
|
-
<SelectItem key={s} value={s}>
|
|
277
|
-
{truncateSessionId(s)}
|
|
278
|
-
</SelectItem>
|
|
279
|
-
))}
|
|
280
|
-
</SelectContent>
|
|
281
|
-
</Select>
|
|
282
|
-
|
|
283
|
-
<Select value={selectedModel} onValueChange={onModelChange}>
|
|
284
|
-
<SelectTrigger className="text-xs">
|
|
285
|
-
<SelectValue placeholder="All models" />
|
|
286
|
-
</SelectTrigger>
|
|
287
|
-
<SelectContent>
|
|
288
|
-
<SelectItem value="__all__">All models</SelectItem>
|
|
289
|
-
{models.map((m) => (
|
|
290
|
-
<SelectItem key={m} value={m}>
|
|
291
|
-
{m}
|
|
292
|
-
</SelectItem>
|
|
293
|
-
))}
|
|
294
|
-
</SelectContent>
|
|
295
|
-
</Select>
|
|
296
|
-
|
|
297
|
-
{/* View toggle */}
|
|
298
|
-
<div className="flex items-center border border-border rounded-md overflow-hidden">
|
|
299
|
-
<button
|
|
300
|
-
type="button"
|
|
301
|
-
onClick={() => setGroupedView(true)}
|
|
302
|
-
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
303
|
-
groupedView ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50"
|
|
304
|
-
}`}
|
|
305
|
-
title="Grouped view"
|
|
306
|
-
>
|
|
307
|
-
<LayoutGrid className="size-4" />
|
|
308
|
-
</button>
|
|
309
|
-
<button
|
|
310
|
-
type="button"
|
|
311
|
-
onClick={() => setGroupedView(false)}
|
|
312
|
-
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
313
|
-
!groupedView ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50"
|
|
314
|
-
}`}
|
|
315
|
-
title="Flat view"
|
|
316
|
-
>
|
|
317
|
-
<List className="size-4" />
|
|
318
|
-
</button>
|
|
319
|
-
</div>
|
|
320
|
-
|
|
321
|
-
{/* Thread/flat mode toggle */}
|
|
322
|
-
<div className="flex items-center border border-border rounded-md overflow-hidden">
|
|
323
|
-
<button
|
|
324
|
-
type="button"
|
|
325
|
-
onClick={() => setGroupViewMode("thread")}
|
|
326
|
-
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
327
|
-
groupViewMode === "thread"
|
|
328
|
-
? "bg-amber-500/15 text-amber-400 border-r border-amber-500/30"
|
|
329
|
-
: "text-muted-foreground hover:bg-muted/50"
|
|
330
|
-
}`}
|
|
331
|
-
title="Thread view (connected timeline)"
|
|
332
|
-
>
|
|
333
|
-
<GitBranch className="size-4" />
|
|
334
|
-
</button>
|
|
335
|
-
<button
|
|
336
|
-
type="button"
|
|
337
|
-
onClick={() => setGroupViewMode("flat")}
|
|
338
|
-
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
339
|
-
groupViewMode === "flat"
|
|
340
|
-
? "bg-muted text-foreground"
|
|
341
|
-
: "text-muted-foreground hover:bg-muted/50"
|
|
342
|
-
}`}
|
|
343
|
-
title="Flat view (card list)"
|
|
344
|
-
>
|
|
345
|
-
<List className="size-4" />
|
|
346
|
-
</button>
|
|
347
|
-
</div>
|
|
348
|
-
</div>
|
|
349
|
-
|
|
350
303
|
{/* Log list */}
|
|
351
304
|
<div className="flex-1 min-h-0 px-6 pb-6">
|
|
352
305
|
{logs.length === 0 ? (
|
|
353
306
|
<div className="text-center text-muted-foreground py-16 space-y-4">
|
|
307
|
+
<CrabLogo className="size-12 text-muted-foreground/20 mx-auto" />
|
|
354
308
|
<p className="text-sm">No requests captured yet.</p>
|
|
355
309
|
<p className="text-xs">Route AI coding tools through the proxy:</p>
|
|
356
310
|
<CopyableCommand command="LLM_BASE_URL=http://localhost:25947/proxy <your-tool>" />
|
|
@@ -365,89 +319,31 @@ export function ProxyViewer({
|
|
|
365
319
|
}}
|
|
366
320
|
>
|
|
367
321
|
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
);
|
|
394
|
-
} else {
|
|
395
|
-
const log = logs[virtualRow.index];
|
|
396
|
-
if (log === undefined) return null;
|
|
397
|
-
const idx = virtualRow.index;
|
|
398
|
-
return (
|
|
399
|
-
<div
|
|
400
|
-
key={log.id}
|
|
401
|
-
data-index={virtualRow.index}
|
|
402
|
-
ref={rowVirtualizer.measureElement}
|
|
403
|
-
style={{
|
|
404
|
-
position: "absolute",
|
|
405
|
-
top: 0,
|
|
406
|
-
left: 0,
|
|
407
|
-
width: "100%",
|
|
408
|
-
transform: `translateY(${virtualRow.start}px)`,
|
|
409
|
-
}}
|
|
410
|
-
>
|
|
411
|
-
{groupViewMode === "thread" ? (
|
|
412
|
-
<div className="flex items-stretch ml-3">
|
|
413
|
-
<ThreadConnector
|
|
414
|
-
stopReason={stopReasons[idx] ?? null}
|
|
415
|
-
isPending={log.responseStatus === null}
|
|
416
|
-
isFirst={idx === 0}
|
|
417
|
-
isLast={idx === logs.length - 1}
|
|
418
|
-
isTurnStart={
|
|
419
|
-
idx === 0 ||
|
|
420
|
-
stopReasons[idx - 1] === "end_turn" ||
|
|
421
|
-
stopReasons[idx - 1] === "stop"
|
|
422
|
-
}
|
|
423
|
-
/>
|
|
424
|
-
<div
|
|
425
|
-
className={cn(
|
|
426
|
-
"flex-1 min-w-0 mb-2 rounded-lg",
|
|
427
|
-
(turnIndices[idx] ?? 0) % 2 === 0 ? "bg-muted/10" : "bg-muted/25",
|
|
428
|
-
)}
|
|
429
|
-
>
|
|
430
|
-
<LogEntry
|
|
431
|
-
log={log}
|
|
432
|
-
viewMode={viewMode}
|
|
433
|
-
strip={strip}
|
|
434
|
-
cacheTrend={cacheTrends.get(log.id) ?? null}
|
|
435
|
-
onCompareWithPrevious={() => handleCompareWithPrevious(log)}
|
|
436
|
-
/>
|
|
437
|
-
</div>
|
|
438
|
-
</div>
|
|
439
|
-
) : (
|
|
440
|
-
<LogEntry
|
|
441
|
-
log={log}
|
|
442
|
-
viewMode={viewMode}
|
|
443
|
-
strip={strip}
|
|
444
|
-
cacheTrend={cacheTrends.get(log.id) ?? null}
|
|
445
|
-
onCompareWithPrevious={() => handleCompareWithPrevious(log)}
|
|
446
|
-
/>
|
|
447
|
-
)}
|
|
448
|
-
</div>
|
|
449
|
-
);
|
|
450
|
-
}
|
|
322
|
+
const group = groups[virtualRow.index];
|
|
323
|
+
if (group === undefined) return null;
|
|
324
|
+
return (
|
|
325
|
+
<div
|
|
326
|
+
key={group.id}
|
|
327
|
+
data-index={virtualRow.index}
|
|
328
|
+
ref={rowVirtualizer.measureElement}
|
|
329
|
+
style={{
|
|
330
|
+
position: "absolute",
|
|
331
|
+
top: 0,
|
|
332
|
+
left: 0,
|
|
333
|
+
width: "100%",
|
|
334
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
335
|
+
}}
|
|
336
|
+
>
|
|
337
|
+
<ConversationGroup
|
|
338
|
+
group={group}
|
|
339
|
+
viewMode={viewMode}
|
|
340
|
+
strip={strip}
|
|
341
|
+
cacheTrends={cacheTrends}
|
|
342
|
+
onCompareWithPrevious={handleCompareWithPrevious}
|
|
343
|
+
standalone={groups.length === 1}
|
|
344
|
+
/>
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
451
347
|
})}
|
|
452
348
|
</div>
|
|
453
349
|
</div>
|