@tonyclaw/llm-inspector 1.14.7 → 1.14.8
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-CdnotuLh.js +105 -0
- package/.output/public/assets/index-vP91146S.css +1 -0
- package/.output/public/assets/{main-BV7uNIIz.js → main-CJ4MreBr.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +87 -79
- package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
- package/.output/server/_ssr/{index-BvHLASu8.mjs → index-9uTJ4xYR.mjs} +744 -581
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-lUOA8pi6.mjs → router-BKnjB_zi.mjs} +2 -2
- package/.output/server/{_tanstack-start-manifest_v-XNH7fVPN.mjs → _tanstack-start-manifest_v-IsglLVKy.mjs} +1 -1
- package/.output/server/index.mjs +28 -28
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +114 -146
- package/src/components/providers/ProviderCard.tsx +79 -26
- package/src/components/providers/ProviderForm.tsx +37 -22
- package/src/components/providers/ProvidersPanel.tsx +79 -47
- package/src/components/providers/SettingsDialog.tsx +25 -15
- package/src/components/proxy-viewer/ConversationGroup.tsx +50 -10
- package/src/components/proxy-viewer/ConversationHeader.tsx +48 -2
- package/src/components/proxy-viewer/LogEntry.tsx +116 -45
- package/src/components/proxy-viewer/LogEntryHeader.tsx +89 -71
- package/src/components/proxy-viewer/ReplayDialog.tsx +16 -6
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +24 -16
- package/src/components/proxy-viewer/ThreadConnector.tsx +104 -0
- package/src/components/proxy-viewer/index.ts +2 -1
- package/src/lib/stopReason.ts +57 -0
- package/.output/public/assets/index-Cmi8TfeU.js +0 -105
- package/.output/public/assets/index-DXUNTCVh.css +0 -1
|
@@ -5,6 +5,7 @@ import { cn } from "../../lib/utils";
|
|
|
5
5
|
import { type CapturedLog, parseRequest } from "../../proxy/schemas";
|
|
6
6
|
import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
|
|
7
7
|
import { Button } from "../ui/button";
|
|
8
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
8
9
|
import { JsonViewerFromString } from "../ui/json-viewer";
|
|
9
10
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
|
10
11
|
import { computeHeadersDiff, computeRequestDiff, DiffView } from "./diff";
|
|
@@ -31,10 +32,8 @@ export type LogEntryProps = {
|
|
|
31
32
|
* `null` (or absent) means the header should render with no arrows.
|
|
32
33
|
*/
|
|
33
34
|
cacheTrend?: CacheTrendEntry | null;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
/** Toggle this log in/out of the comparison selection. */
|
|
37
|
-
onToggleSelect?: (logId: number) => void;
|
|
35
|
+
/** Callback to open CompareDrawer with this log and its immediate predecessor. */
|
|
36
|
+
onCompareWithPrevious?: () => void;
|
|
38
37
|
};
|
|
39
38
|
|
|
40
39
|
/**
|
|
@@ -118,21 +117,29 @@ function DiffToggleButton({
|
|
|
118
117
|
onClick: (e: React.MouseEvent) => void;
|
|
119
118
|
}): JSX.Element {
|
|
120
119
|
return (
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
120
|
+
<TooltipProvider>
|
|
121
|
+
<Tooltip>
|
|
122
|
+
<TooltipTrigger asChild>
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={onClick}
|
|
126
|
+
aria-pressed={active}
|
|
127
|
+
className={cn(
|
|
128
|
+
"flex items-center gap-1.5 text-xs px-2 py-1 rounded transition-colors",
|
|
129
|
+
active
|
|
130
|
+
? "bg-primary/10 text-primary"
|
|
131
|
+
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
<GitCompareArrows className="size-3" />
|
|
135
|
+
{active ? "Showing diff" : "Diff with Raw"}
|
|
136
|
+
</button>
|
|
137
|
+
</TooltipTrigger>
|
|
138
|
+
<TooltipContent>
|
|
139
|
+
{active ? "Hide diff view" : "Compare proxy output against the original raw version"}
|
|
140
|
+
</TooltipContent>
|
|
141
|
+
</Tooltip>
|
|
142
|
+
</TooltipProvider>
|
|
136
143
|
);
|
|
137
144
|
}
|
|
138
145
|
|
|
@@ -142,8 +149,7 @@ export const LogEntry = memo(function ({
|
|
|
142
149
|
suppressApiFormatBadge = false,
|
|
143
150
|
strip,
|
|
144
151
|
cacheTrend = null,
|
|
145
|
-
|
|
146
|
-
onToggleSelect,
|
|
152
|
+
onCompareWithPrevious,
|
|
147
153
|
}: LogEntryProps): JSX.Element {
|
|
148
154
|
const [expanded, setExpanded] = useState<boolean>(false);
|
|
149
155
|
const [requestCopied, setRequestCopied] = useState<boolean>(false);
|
|
@@ -199,12 +205,7 @@ export const LogEntry = memo(function ({
|
|
|
199
205
|
|
|
200
206
|
return (
|
|
201
207
|
<>
|
|
202
|
-
<div
|
|
203
|
-
className={cn(
|
|
204
|
-
"border border-border rounded-lg mb-3 overflow-hidden",
|
|
205
|
-
isSelected && "border-l-2 border-l-amber-400",
|
|
206
|
-
)}
|
|
207
|
-
>
|
|
208
|
+
<div className="border border-border rounded-lg mb-3 overflow-hidden">
|
|
208
209
|
<LogEntryHeader
|
|
209
210
|
log={log}
|
|
210
211
|
parsedRequest={parsedRequest}
|
|
@@ -212,21 +213,61 @@ export const LogEntry = memo(function ({
|
|
|
212
213
|
onToggle={() => setExpanded(!expanded)}
|
|
213
214
|
suppressApiFormatBadge={suppressApiFormatBadge}
|
|
214
215
|
cacheTrend={cacheTrend}
|
|
215
|
-
isSelected={isSelected}
|
|
216
|
-
onToggleSelect={onToggleSelect}
|
|
217
216
|
/>
|
|
218
217
|
|
|
219
218
|
{expanded && (
|
|
220
219
|
<div onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
|
|
221
220
|
<Tabs defaultValue="request">
|
|
222
221
|
<TabsList className="mx-4 mt-2">
|
|
223
|
-
{viewMode === "full" &&
|
|
224
|
-
|
|
222
|
+
{viewMode === "full" && (
|
|
223
|
+
<TooltipProvider>
|
|
224
|
+
<Tooltip>
|
|
225
|
+
<TooltipTrigger asChild>
|
|
226
|
+
<TabsTrigger value="raw-headers">Raw Headers</TabsTrigger>
|
|
227
|
+
</TooltipTrigger>
|
|
228
|
+
<TooltipContent>
|
|
229
|
+
HTTP headers received from the upstream provider
|
|
230
|
+
</TooltipContent>
|
|
231
|
+
</Tooltip>
|
|
232
|
+
</TooltipProvider>
|
|
233
|
+
)}
|
|
234
|
+
{viewMode === "full" && (
|
|
235
|
+
<TooltipProvider>
|
|
236
|
+
<Tooltip>
|
|
237
|
+
<TooltipTrigger asChild>
|
|
238
|
+
<TabsTrigger value="headers">Headers</TabsTrigger>
|
|
239
|
+
</TooltipTrigger>
|
|
240
|
+
<TooltipContent>
|
|
241
|
+
Request and response headers sent and received
|
|
242
|
+
</TooltipContent>
|
|
243
|
+
</Tooltip>
|
|
244
|
+
</TooltipProvider>
|
|
245
|
+
)}
|
|
225
246
|
{shouldShowRawRequestTab(log.apiFormat, viewMode, strip) && (
|
|
226
|
-
<
|
|
247
|
+
<TooltipProvider>
|
|
248
|
+
<Tooltip>
|
|
249
|
+
<TooltipTrigger asChild>
|
|
250
|
+
<TabsTrigger value="raw-request">Raw Request</TabsTrigger>
|
|
251
|
+
</TooltipTrigger>
|
|
252
|
+
<TooltipContent>
|
|
253
|
+
Exact HTTP request sent to the upstream provider
|
|
254
|
+
</TooltipContent>
|
|
255
|
+
</Tooltip>
|
|
256
|
+
</TooltipProvider>
|
|
227
257
|
)}
|
|
228
258
|
<TabsTrigger value="request">Request</TabsTrigger>
|
|
229
|
-
{viewMode === "full" &&
|
|
259
|
+
{viewMode === "full" && (
|
|
260
|
+
<TooltipProvider>
|
|
261
|
+
<Tooltip>
|
|
262
|
+
<TooltipTrigger asChild>
|
|
263
|
+
<TabsTrigger value="raw">Raw Response</TabsTrigger>
|
|
264
|
+
</TooltipTrigger>
|
|
265
|
+
<TooltipContent>
|
|
266
|
+
Exact HTTP response from the upstream provider
|
|
267
|
+
</TooltipContent>
|
|
268
|
+
</Tooltip>
|
|
269
|
+
</TooltipProvider>
|
|
270
|
+
)}
|
|
230
271
|
<TabsTrigger value="parsed">Response</TabsTrigger>
|
|
231
272
|
</TabsList>
|
|
232
273
|
|
|
@@ -267,18 +308,48 @@ export const LogEntry = memo(function ({
|
|
|
267
308
|
}}
|
|
268
309
|
/>
|
|
269
310
|
)}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
311
|
+
{onCompareWithPrevious !== undefined && (
|
|
312
|
+
<TooltipProvider>
|
|
313
|
+
<Tooltip>
|
|
314
|
+
<TooltipTrigger asChild>
|
|
315
|
+
<Button
|
|
316
|
+
variant="outline"
|
|
317
|
+
size="sm"
|
|
318
|
+
className="h-7 text-xs"
|
|
319
|
+
onClick={(e) => {
|
|
320
|
+
e.stopPropagation();
|
|
321
|
+
onCompareWithPrevious();
|
|
322
|
+
}}
|
|
323
|
+
>
|
|
324
|
+
<GitCompareArrows className="size-3 mr-1" />
|
|
325
|
+
Diff with Previous
|
|
326
|
+
</Button>
|
|
327
|
+
</TooltipTrigger>
|
|
328
|
+
<TooltipContent>
|
|
329
|
+
Compare this request with the immediately preceding one
|
|
330
|
+
</TooltipContent>
|
|
331
|
+
</Tooltip>
|
|
332
|
+
</TooltipProvider>
|
|
333
|
+
)}
|
|
334
|
+
<TooltipProvider>
|
|
335
|
+
<Tooltip>
|
|
336
|
+
<TooltipTrigger asChild>
|
|
337
|
+
<Button
|
|
338
|
+
variant="outline"
|
|
339
|
+
size="sm"
|
|
340
|
+
className="h-7 text-xs"
|
|
341
|
+
onClick={(e) => {
|
|
342
|
+
e.stopPropagation();
|
|
343
|
+
setReplayOpen(true);
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
<RotateCcw className="size-3 mr-1" />
|
|
347
|
+
Replay
|
|
348
|
+
</Button>
|
|
349
|
+
</TooltipTrigger>
|
|
350
|
+
<TooltipContent>Re-send this request to the provider</TooltipContent>
|
|
351
|
+
</Tooltip>
|
|
352
|
+
</TooltipProvider>
|
|
282
353
|
<CopyButton
|
|
283
354
|
text={displayedRequestBody}
|
|
284
355
|
label="Copy Request"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ArrowDown,
|
|
3
3
|
ArrowUp,
|
|
4
|
-
Check,
|
|
5
4
|
ChevronDown,
|
|
6
5
|
ChevronRight,
|
|
7
6
|
Clock,
|
|
@@ -20,6 +19,7 @@ import { cn, formatTokens, getStatusCategory, type StatusCategory } from "../../
|
|
|
20
19
|
import type { CapturedLog, InspectorRequest } from "../../proxy/schemas";
|
|
21
20
|
import { Badge } from "../ui/badge";
|
|
22
21
|
import { ProviderLogo, detectProvider } from "../providers/ProviderLogo";
|
|
22
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
23
23
|
import type { CacheTrend } from "./cacheTrend";
|
|
24
24
|
|
|
25
25
|
function formatElapsed(ms: number): string {
|
|
@@ -67,10 +67,6 @@ export type LogEntryHeaderProps = {
|
|
|
67
67
|
* the corresponding cache span renders as it did before — no arrow.
|
|
68
68
|
*/
|
|
69
69
|
cacheTrend?: { creation: CacheTrend | null; read: CacheTrend | null } | null;
|
|
70
|
-
/** Whether this log is currently marked for comparison. */
|
|
71
|
-
isSelected?: boolean;
|
|
72
|
-
/** Toggle this log in/out of the comparison selection. */
|
|
73
|
-
onToggleSelect?: (logId: number) => void;
|
|
74
70
|
};
|
|
75
71
|
|
|
76
72
|
export const LogEntryHeader = memo(function ({
|
|
@@ -80,8 +76,6 @@ export const LogEntryHeader = memo(function ({
|
|
|
80
76
|
onToggle,
|
|
81
77
|
suppressApiFormatBadge = false,
|
|
82
78
|
cacheTrend = null,
|
|
83
|
-
isSelected = false,
|
|
84
|
-
onToggleSelect,
|
|
85
79
|
}: LogEntryHeaderProps): JSX.Element {
|
|
86
80
|
const statusCategory = getStatusCategory(log.responseStatus);
|
|
87
81
|
|
|
@@ -111,40 +105,23 @@ export const LogEntryHeader = memo(function ({
|
|
|
111
105
|
}
|
|
112
106
|
}}
|
|
113
107
|
>
|
|
114
|
-
{/* Selection checkbox (for log-request comparison) */}
|
|
115
|
-
{onToggleSelect !== undefined && (
|
|
116
|
-
<button
|
|
117
|
-
type="button"
|
|
118
|
-
onClick={(e) => {
|
|
119
|
-
e.stopPropagation();
|
|
120
|
-
onToggleSelect(log.id);
|
|
121
|
-
}}
|
|
122
|
-
aria-label={isSelected ? "Deselect for comparison" : "Select for comparison"}
|
|
123
|
-
aria-pressed={isSelected}
|
|
124
|
-
className={cn(
|
|
125
|
-
"shrink-0 size-4 rounded-sm border flex items-center justify-center transition-colors cursor-pointer",
|
|
126
|
-
isSelected
|
|
127
|
-
? "bg-amber-400 border-amber-400 text-amber-950"
|
|
128
|
-
: "border-muted-foreground/40 hover:border-amber-400 hover:bg-amber-400/10",
|
|
129
|
-
)}
|
|
130
|
-
>
|
|
131
|
-
{isSelected && <Check className="size-3" strokeWidth={3} />}
|
|
132
|
-
</button>
|
|
133
|
-
)}
|
|
134
|
-
|
|
135
108
|
{/* Request ID */}
|
|
136
109
|
<span className="text-blue-400/80 font-mono text-xs font-semibold tabular-nums shrink-0">
|
|
137
110
|
#{log.id}
|
|
138
111
|
</span>
|
|
139
112
|
|
|
140
|
-
{/* Model */}
|
|
113
|
+
{/* Model — logo icon only, model name in tooltip */}
|
|
141
114
|
{log.model !== null && (
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
115
|
+
<TooltipProvider>
|
|
116
|
+
<Tooltip>
|
|
117
|
+
<TooltipTrigger asChild>
|
|
118
|
+
<span className="shrink-0">
|
|
119
|
+
<ProviderLogo provider={detectProvider(log.model)} className="size-4" />
|
|
120
|
+
</span>
|
|
121
|
+
</TooltipTrigger>
|
|
122
|
+
<TooltipContent>{log.model}</TooltipContent>
|
|
123
|
+
</Tooltip>
|
|
124
|
+
</TooltipProvider>
|
|
148
125
|
)}
|
|
149
126
|
|
|
150
127
|
{/* API Format Badge */}
|
|
@@ -220,36 +197,64 @@ export const LogEntryHeader = memo(function ({
|
|
|
220
197
|
)}
|
|
221
198
|
{/* Cache tokens */}
|
|
222
199
|
{log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
|
|
223
|
-
<
|
|
224
|
-
<
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
200
|
+
<TooltipProvider>
|
|
201
|
+
<Tooltip>
|
|
202
|
+
<TooltipTrigger asChild>
|
|
203
|
+
<span className="flex items-center gap-1 text-xs shrink-0">
|
|
204
|
+
<CacheTrendIndicator trend={cacheTrend?.creation ?? null} />
|
|
205
|
+
<span className="font-mono tabular-nums text-emerald-400">
|
|
206
|
+
Cache +{formatTokens(log.cacheCreationInputTokens)}
|
|
207
|
+
</span>
|
|
208
|
+
</span>
|
|
209
|
+
</TooltipTrigger>
|
|
210
|
+
<TooltipContent>Tokens cached for reuse, reducing future API cost</TooltipContent>
|
|
211
|
+
</Tooltip>
|
|
212
|
+
</TooltipProvider>
|
|
229
213
|
)}
|
|
230
214
|
{log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
|
|
231
|
-
<
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
215
|
+
<TooltipProvider>
|
|
216
|
+
<Tooltip>
|
|
217
|
+
<TooltipTrigger asChild>
|
|
218
|
+
<span className="flex items-center gap-1 text-xs shrink-0">
|
|
219
|
+
<CacheTrendIndicator trend={cacheTrend?.read ?? null} />
|
|
220
|
+
<span className="font-mono tabular-nums text-purple-400">
|
|
221
|
+
Cache ~{formatTokens(log.cacheReadInputTokens)}
|
|
222
|
+
</span>
|
|
223
|
+
</span>
|
|
224
|
+
</TooltipTrigger>
|
|
225
|
+
<TooltipContent>Tokens served from cache, reducing API cost</TooltipContent>
|
|
226
|
+
</Tooltip>
|
|
227
|
+
</TooltipProvider>
|
|
237
228
|
)}
|
|
238
229
|
|
|
239
230
|
{/* Message count */}
|
|
240
231
|
{messageCount !== null && (
|
|
241
|
-
<
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
232
|
+
<TooltipProvider>
|
|
233
|
+
<Tooltip>
|
|
234
|
+
<TooltipTrigger asChild>
|
|
235
|
+
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
236
|
+
<MessageSquare className="size-3" />
|
|
237
|
+
<span className="font-mono tabular-nums">{messageCount}</span>
|
|
238
|
+
</span>
|
|
239
|
+
</TooltipTrigger>
|
|
240
|
+
<TooltipContent>Number of messages in the conversation</TooltipContent>
|
|
241
|
+
</Tooltip>
|
|
242
|
+
</TooltipProvider>
|
|
245
243
|
)}
|
|
246
244
|
|
|
247
245
|
{/* Tool count */}
|
|
248
246
|
{toolCount !== null && (
|
|
249
|
-
<
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
247
|
+
<TooltipProvider>
|
|
248
|
+
<Tooltip>
|
|
249
|
+
<TooltipTrigger asChild>
|
|
250
|
+
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
251
|
+
<Wrench className="size-3" />
|
|
252
|
+
<span className="font-mono tabular-nums">{toolCount}</span>
|
|
253
|
+
</span>
|
|
254
|
+
</TooltipTrigger>
|
|
255
|
+
<TooltipContent>Number of tools defined in the request</TooltipContent>
|
|
256
|
+
</Tooltip>
|
|
257
|
+
</TooltipProvider>
|
|
253
258
|
)}
|
|
254
259
|
|
|
255
260
|
{/* Origin */}
|
|
@@ -280,25 +285,38 @@ export const LogEntryHeader = memo(function ({
|
|
|
280
285
|
|
|
281
286
|
{/* Client info (PID + project folder) */}
|
|
282
287
|
{(log.clientPid !== null || log.clientProjectFolder !== null) && (
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
288
|
+
<TooltipProvider>
|
|
289
|
+
<Tooltip>
|
|
290
|
+
<TooltipTrigger asChild>
|
|
291
|
+
<span className="flex items-center gap-1 text-purple-400/80 text-xs shrink-0">
|
|
292
|
+
<FileTerminal className="size-3" />
|
|
293
|
+
{log.clientProjectFolder !== null ? (
|
|
294
|
+
<span className="font-mono tabular-nums">{log.clientProjectFolder}</span>
|
|
295
|
+
) : (
|
|
296
|
+
<span className="font-mono tabular-nums">PID {log.clientPid}</span>
|
|
297
|
+
)}
|
|
298
|
+
</span>
|
|
299
|
+
</TooltipTrigger>
|
|
300
|
+
<TooltipContent>
|
|
301
|
+
{log.clientCwd !== null
|
|
302
|
+
? `PID: ${log.clientPid ?? "?"} CWD: ${log.clientCwd}`
|
|
303
|
+
: `Process ID: ${log.clientPid ?? "?"}`}
|
|
304
|
+
</TooltipContent>
|
|
305
|
+
</Tooltip>
|
|
306
|
+
</TooltipProvider>
|
|
298
307
|
)}
|
|
299
308
|
|
|
300
309
|
{/* Streaming indicator */}
|
|
301
|
-
{log.streaming &&
|
|
310
|
+
{log.streaming && (
|
|
311
|
+
<TooltipProvider>
|
|
312
|
+
<Tooltip>
|
|
313
|
+
<TooltipTrigger asChild>
|
|
314
|
+
<Radio className="size-3 text-muted-foreground/60 shrink-0" />
|
|
315
|
+
</TooltipTrigger>
|
|
316
|
+
<TooltipContent>Request used SSE streaming</TooltipContent>
|
|
317
|
+
</Tooltip>
|
|
318
|
+
</TooltipProvider>
|
|
319
|
+
)}
|
|
302
320
|
|
|
303
321
|
{/* Spacer */}
|
|
304
322
|
<span className="flex-1 min-w-0" />
|
|
@@ -3,6 +3,7 @@ import type { JSX } from "react";
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { Button } from "../ui/button";
|
|
6
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
6
7
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
|
|
7
8
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
|
8
9
|
import { ResponseView } from "./ResponseView";
|
|
@@ -92,12 +93,21 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
92
93
|
<TabsContent value="modified" className="space-y-4">
|
|
93
94
|
<div>
|
|
94
95
|
<label className="text-sm font-medium mb-2 block">Request Body (JSON)</label>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
<TooltipProvider>
|
|
97
|
+
<Tooltip>
|
|
98
|
+
<TooltipTrigger asChild>
|
|
99
|
+
<textarea
|
|
100
|
+
className="w-full h-64 p-3 font-mono text-xs bg-muted rounded-md border border-input resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
|
101
|
+
value={modifiedBody}
|
|
102
|
+
onChange={(e) => setModifiedBody(e.target.value)}
|
|
103
|
+
spellCheck={false}
|
|
104
|
+
/>
|
|
105
|
+
</TooltipTrigger>
|
|
106
|
+
<TooltipContent>
|
|
107
|
+
Edit the request body before re-sending to the provider
|
|
108
|
+
</TooltipContent>
|
|
109
|
+
</Tooltip>
|
|
110
|
+
</TooltipProvider>
|
|
101
111
|
</div>
|
|
102
112
|
|
|
103
113
|
{error !== null && error !== "" && (
|
|
@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, type JSX } from "react";
|
|
|
2
2
|
import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
|
|
3
3
|
import { Badge } from "../ui/badge";
|
|
4
4
|
import { JsonViewer } from "../ui/json-viewer";
|
|
5
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
5
6
|
import type { StreamingChunk } from "../../proxy/schemas";
|
|
6
7
|
|
|
7
8
|
export type StreamingChunkSequenceProps = {
|
|
@@ -153,22 +154,29 @@ export function StreamingChunkSequence({
|
|
|
153
154
|
|
|
154
155
|
return (
|
|
155
156
|
<div className="space-y-1">
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
157
|
+
<TooltipProvider>
|
|
158
|
+
<Tooltip>
|
|
159
|
+
<TooltipTrigger asChild>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
|
163
|
+
onClick={() => setContainerExpanded((v) => !v)}
|
|
164
|
+
>
|
|
165
|
+
{containerExpanded ? (
|
|
166
|
+
<ChevronDown className="size-3" />
|
|
167
|
+
) : (
|
|
168
|
+
<ChevronRight className="size-3" />
|
|
169
|
+
)}
|
|
170
|
+
<span>Raw SSE Events</span>
|
|
171
|
+
<Badge variant="outline" className="text-[9px] px-1 py-0 h-4 font-mono ml-1">
|
|
172
|
+
{logId}
|
|
173
|
+
{truncated === true ? "+" : ""}
|
|
174
|
+
</Badge>
|
|
175
|
+
</button>
|
|
176
|
+
</TooltipTrigger>
|
|
177
|
+
<TooltipContent>Server-Sent Events streaming chunks from the provider</TooltipContent>
|
|
178
|
+
</Tooltip>
|
|
179
|
+
</TooltipProvider>
|
|
172
180
|
|
|
173
181
|
{containerExpanded === true ? (
|
|
174
182
|
<div className="rounded-md border border-border bg-muted/20 overflow-auto max-h-64">
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
import type { StopReason } from "../../lib/stopReason";
|
|
4
|
+
|
|
5
|
+
export type ThreadConnectorProps = {
|
|
6
|
+
/** The stop reason extracted from this log entry's response. */
|
|
7
|
+
stopReason: StopReason;
|
|
8
|
+
/** True when the response is still in-flight (responseStatus === null). */
|
|
9
|
+
isPending: boolean;
|
|
10
|
+
/** True when this is the first entry in the group. */
|
|
11
|
+
isFirst: boolean;
|
|
12
|
+
/** True when this is the last entry in the group. */
|
|
13
|
+
isLast: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Renders the vertical timeline connector on the left side of a log entry
|
|
18
|
+
* in thread view. A continuous line runs through intermediate entries, with:
|
|
19
|
+
* - Line starts from the top for the first entry
|
|
20
|
+
* - Line continues through intermediate entries
|
|
21
|
+
* - Line ends at a turn-boundary dot/cap when stop_reason is end_turn/stop
|
|
22
|
+
* - Dimmed/dashed line for pending (in-flight) entries
|
|
23
|
+
* - A solid dot marks turn boundaries
|
|
24
|
+
*/
|
|
25
|
+
export function ThreadConnector({
|
|
26
|
+
stopReason,
|
|
27
|
+
isPending,
|
|
28
|
+
isFirst,
|
|
29
|
+
isLast,
|
|
30
|
+
}: ThreadConnectorProps): JSX.Element {
|
|
31
|
+
// Turn boundary: "end_turn" for Anthropic, "stop" for OpenAI
|
|
32
|
+
const isBoundary = stopReason === "end_turn" || stopReason === "stop";
|
|
33
|
+
// Tool use: the turn continues
|
|
34
|
+
const isToolUse = stopReason === "tool_use";
|
|
35
|
+
|
|
36
|
+
// Compute classes for each segment of the connector:
|
|
37
|
+
// - top half line (from previous entry down to this entry's top)
|
|
38
|
+
// - center dot/circle (this entry's response marker)
|
|
39
|
+
// - bottom half line (from this entry's bottom down to the next entry)
|
|
40
|
+
|
|
41
|
+
const lineClass = cn(
|
|
42
|
+
"w-0.5 bg-muted-foreground/30",
|
|
43
|
+
isPending && "border-dashed bg-transparent border-l-2 border-muted-foreground/20",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex items-stretch h-full w-6 shrink-0 relative">
|
|
48
|
+
{/* Top line segment */}
|
|
49
|
+
<div className="absolute left-1/2 -translate-x-1/2 w-0.5 top-0 flex flex-col items-center">
|
|
50
|
+
{!isFirst && (
|
|
51
|
+
<div
|
|
52
|
+
className={cn("w-0.5 grow", "bg-muted-foreground/30", "h-4")}
|
|
53
|
+
style={{ minHeight: "0.5rem" }}
|
|
54
|
+
/>
|
|
55
|
+
)}
|
|
56
|
+
{isFirst && <div className="h-4" />}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Center marker */}
|
|
60
|
+
<div className="absolute left-1/2 -translate-x-1/2 top-4 flex items-center justify-center z-10">
|
|
61
|
+
{isBoundary ? (
|
|
62
|
+
<div
|
|
63
|
+
className={cn(
|
|
64
|
+
"size-2.5 rounded-full border-2",
|
|
65
|
+
"bg-background border-amber-400",
|
|
66
|
+
"shadow-[0_0_6px_rgba(251,191,36,0.4)]",
|
|
67
|
+
)}
|
|
68
|
+
title={stopReason === "end_turn" ? "End of Turn (Anthropic)" : "End of Turn (OpenAI)"}
|
|
69
|
+
/>
|
|
70
|
+
) : isToolUse ? (
|
|
71
|
+
<div
|
|
72
|
+
className="size-2 rounded-full bg-muted-foreground/25"
|
|
73
|
+
title="Tool Use — turn continues"
|
|
74
|
+
/>
|
|
75
|
+
) : isPending ? (
|
|
76
|
+
<div
|
|
77
|
+
className="size-2.5 rounded-full border-2 border-dashed border-muted-foreground/30 animate-pulse"
|
|
78
|
+
title="Response pending"
|
|
79
|
+
/>
|
|
80
|
+
) : (
|
|
81
|
+
<div className="size-1.5 rounded-full bg-muted-foreground/30" />
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Bottom line segment */}
|
|
86
|
+
<div
|
|
87
|
+
className="absolute left-1/2 -translate-x-1/2 w-0.5 top-4 flex flex-col items-center"
|
|
88
|
+
style={{ bottom: 0 }}
|
|
89
|
+
>
|
|
90
|
+
{!isBoundary && (
|
|
91
|
+
<div
|
|
92
|
+
className={cn(
|
|
93
|
+
"w-0.5 flex-1",
|
|
94
|
+
isPending
|
|
95
|
+
? "border-dashed bg-transparent border-l-2 border-muted-foreground/20"
|
|
96
|
+
: "bg-muted-foreground/30",
|
|
97
|
+
)}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
{isBoundary && <div className="w-0.5 h-4 bg-muted-foreground/10" />}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -4,5 +4,6 @@ export {
|
|
|
4
4
|
getConversationId,
|
|
5
5
|
groupLogsByConversation,
|
|
6
6
|
} from "./ConversationHeader";
|
|
7
|
-
export type { ConversationGroupData } from "./ConversationHeader";
|
|
7
|
+
export type { ConversationGroupData, ViewMode } from "./ConversationHeader";
|
|
8
8
|
export { LogEntry } from "./LogEntry";
|
|
9
|
+
export { ThreadConnector } from "./ThreadConnector";
|