@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
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import { useState, memo, useMemo
|
|
1
|
+
import { useState, memo, useMemo } from "react";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
4
4
|
import { extractStopReason } from "../../lib/stopReason";
|
|
5
|
-
import { cn } from "../../lib/utils";
|
|
6
5
|
import {
|
|
7
6
|
ConversationHeader,
|
|
8
|
-
getConversationId,
|
|
9
7
|
getGroupApiFormat,
|
|
10
8
|
hasMixedApiFormat,
|
|
11
9
|
type ConversationGroupData,
|
|
12
|
-
type ViewMode,
|
|
13
10
|
} from "./ConversationHeader";
|
|
14
|
-
import {
|
|
15
|
-
import { ThreadConnector } from "./ThreadConnector";
|
|
11
|
+
import { TurnGroup } from "./TurnGroup";
|
|
16
12
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
17
13
|
|
|
18
14
|
export type ConversationGroupProps = {
|
|
@@ -27,8 +23,8 @@ export type ConversationGroupProps = {
|
|
|
27
23
|
cacheTrends?: Map<number, CacheTrendEntry>;
|
|
28
24
|
/** Callback to open CompareDrawer with a log and its immediate predecessor. */
|
|
29
25
|
onCompareWithPrevious: (log: CapturedLog) => void;
|
|
30
|
-
/**
|
|
31
|
-
|
|
26
|
+
/** When true, skip the group header and render content directly. */
|
|
27
|
+
standalone?: boolean;
|
|
32
28
|
};
|
|
33
29
|
|
|
34
30
|
function computeStats(logs: CapturedLog[]): {
|
|
@@ -50,23 +46,16 @@ export const ConversationGroup = memo(function ({
|
|
|
50
46
|
strip,
|
|
51
47
|
cacheTrends,
|
|
52
48
|
onCompareWithPrevious,
|
|
53
|
-
|
|
49
|
+
standalone = false,
|
|
54
50
|
}: ConversationGroupProps): JSX.Element {
|
|
55
|
-
const [expanded, setExpanded] = useState(
|
|
56
|
-
const [groupViewMode, setGroupViewMode] = useState<ViewMode>(defaultGroupViewMode);
|
|
57
|
-
|
|
58
|
-
// Sync local view mode when global toggle changes (it only acts as default on mount otherwise)
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
setGroupViewMode(defaultGroupViewMode);
|
|
61
|
-
}, [defaultGroupViewMode]);
|
|
62
|
-
|
|
51
|
+
const [expanded, setExpanded] = useState(standalone);
|
|
63
52
|
const stats = computeStats(group.logs);
|
|
64
53
|
const startTime = group.logs[0]?.timestamp ?? new Date().toISOString();
|
|
65
54
|
const endTime = group.logs[group.logs.length - 1]?.timestamp ?? new Date().toISOString();
|
|
66
55
|
const mixed = hasMixedApiFormat(group.logs);
|
|
67
56
|
const isLoading = group.logs.some((log) => log.responseStatus === null);
|
|
68
57
|
|
|
69
|
-
// Pre-compute stop reasons for each log — used by
|
|
58
|
+
// Pre-compute stop reasons for each log — used by turnIndices
|
|
70
59
|
const stopReasons = useMemo(() => group.logs.map((log) => extractStopReason(log)), [group.logs]);
|
|
71
60
|
|
|
72
61
|
// Compute turn indices for alternating background colours
|
|
@@ -82,6 +71,31 @@ export const ConversationGroup = memo(function ({
|
|
|
82
71
|
return indices;
|
|
83
72
|
}, [stopReasons]);
|
|
84
73
|
|
|
74
|
+
// Group logs into turns for collapsible turn display
|
|
75
|
+
const turnGroups = useMemo(() => {
|
|
76
|
+
const groups: { logs: CapturedLog[]; turnIndex: number }[] = [];
|
|
77
|
+
let currentGroup: CapturedLog[] = [];
|
|
78
|
+
let currentTurn = 0;
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < group.logs.length; i++) {
|
|
81
|
+
const turnIdx = turnIndices[i] ?? 0;
|
|
82
|
+
const log = group.logs[i];
|
|
83
|
+
if (!log) continue;
|
|
84
|
+
if (turnIdx !== currentTurn) {
|
|
85
|
+
if (currentGroup.length > 0) {
|
|
86
|
+
groups.push({ logs: currentGroup, turnIndex: currentTurn });
|
|
87
|
+
}
|
|
88
|
+
currentGroup = [log];
|
|
89
|
+
currentTurn = turnIdx;
|
|
90
|
+
} else {
|
|
91
|
+
currentGroup.push(log);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (currentGroup.length > 0) {
|
|
95
|
+
groups.push({ logs: currentGroup, turnIndex: currentTurn });
|
|
96
|
+
}
|
|
97
|
+
return groups;
|
|
98
|
+
}, [group.logs, turnIndices]);
|
|
85
99
|
const displayId =
|
|
86
100
|
group.conversationId.startsWith("PID:") || group.conversationId.includes("|")
|
|
87
101
|
? group.conversationId
|
|
@@ -91,71 +105,38 @@ export const ConversationGroup = memo(function ({
|
|
|
91
105
|
|
|
92
106
|
return (
|
|
93
107
|
<div className="mb-4">
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
{!standalone && (
|
|
109
|
+
<ConversationHeader
|
|
110
|
+
conversationId={displayId}
|
|
111
|
+
startTime={startTime}
|
|
112
|
+
endTime={endTime}
|
|
113
|
+
totalCalls={group.logs.length}
|
|
114
|
+
totalInputTokens={stats.totalInputTokens}
|
|
115
|
+
totalOutputTokens={stats.totalOutputTokens}
|
|
116
|
+
apiFormat={getGroupApiFormat(group.logs)}
|
|
117
|
+
expanded={expanded}
|
|
118
|
+
onToggle={() => setExpanded(!expanded)}
|
|
119
|
+
hideApiFormat={mixed}
|
|
120
|
+
isLoading={isLoading}
|
|
121
|
+
userAgent={group.logs[0]?.userAgent ?? null}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
110
124
|
|
|
111
|
-
{expanded &&
|
|
112
|
-
<div
|
|
113
|
-
{
|
|
114
|
-
<
|
|
115
|
-
key={
|
|
116
|
-
|
|
125
|
+
{expanded && (
|
|
126
|
+
<div>
|
|
127
|
+
{turnGroups.map((tg) => (
|
|
128
|
+
<TurnGroup
|
|
129
|
+
key={tg.turnIndex}
|
|
130
|
+
logs={tg.logs}
|
|
117
131
|
viewMode={viewMode}
|
|
118
132
|
strip={strip}
|
|
119
|
-
|
|
120
|
-
onCompareWithPrevious={
|
|
133
|
+
cacheTrends={cacheTrends}
|
|
134
|
+
onCompareWithPrevious={onCompareWithPrevious}
|
|
135
|
+
turnIndex={tg.turnIndex}
|
|
121
136
|
/>
|
|
122
137
|
))}
|
|
123
138
|
</div>
|
|
124
139
|
)}
|
|
125
|
-
|
|
126
|
-
{expanded && groupViewMode === "thread" && (
|
|
127
|
-
<div className="ml-3">
|
|
128
|
-
{group.logs.map((log, idx) => {
|
|
129
|
-
const isTurnStart =
|
|
130
|
-
idx === 0 || stopReasons[idx - 1] === "end_turn" || stopReasons[idx - 1] === "stop";
|
|
131
|
-
return (
|
|
132
|
-
<div key={log.id} className="flex items-stretch">
|
|
133
|
-
<ThreadConnector
|
|
134
|
-
stopReason={stopReasons[idx] ?? null}
|
|
135
|
-
isPending={log.responseStatus === null}
|
|
136
|
-
isFirst={idx === 0}
|
|
137
|
-
isLast={idx === group.logs.length - 1}
|
|
138
|
-
isTurnStart={isTurnStart}
|
|
139
|
-
/>
|
|
140
|
-
<div
|
|
141
|
-
className={cn(
|
|
142
|
-
"flex-1 min-w-0 mb-2 rounded-lg",
|
|
143
|
-
(turnIndices[idx] ?? 0) % 2 === 0 ? "bg-muted/10" : "bg-muted/25",
|
|
144
|
-
)}
|
|
145
|
-
>
|
|
146
|
-
<LogEntry
|
|
147
|
-
log={log}
|
|
148
|
-
viewMode={viewMode}
|
|
149
|
-
strip={strip}
|
|
150
|
-
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
151
|
-
onCompareWithPrevious={() => onCompareWithPrevious(log)}
|
|
152
|
-
/>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
);
|
|
156
|
-
})}
|
|
157
|
-
</div>
|
|
158
|
-
)}
|
|
159
140
|
</div>
|
|
160
141
|
);
|
|
161
142
|
});
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChevronDown,
|
|
3
|
-
ChevronRight,
|
|
4
|
-
Clock,
|
|
5
|
-
GitBranch,
|
|
6
|
-
Loader2,
|
|
7
|
-
MessageSquare,
|
|
8
|
-
User,
|
|
9
|
-
Zap,
|
|
10
|
-
} from "lucide-react";
|
|
1
|
+
import { ChevronDown, ChevronRight, Clock, Loader2, MessageSquare, User, Zap } from "lucide-react";
|
|
11
2
|
import type { JSX } from "react";
|
|
12
3
|
import { cn, formatTokens } from "../../lib/utils";
|
|
13
4
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
@@ -37,10 +28,6 @@ export type ConversationHeaderProps = {
|
|
|
37
28
|
/** When true and the group is collapsed, show a spinner instead of the
|
|
38
29
|
* expand chevron to indicate an in-flight request inside the group. */
|
|
39
30
|
isLoading?: boolean;
|
|
40
|
-
/** Current display mode for this group (flat cards or threaded timeline). */
|
|
41
|
-
viewMode?: ViewMode;
|
|
42
|
-
/** Toggle between flat and thread display modes for this group. */
|
|
43
|
-
onToggleViewMode?: () => void;
|
|
44
31
|
/** User-Agent string from the first log in the group. */
|
|
45
32
|
userAgent?: string | null;
|
|
46
33
|
};
|
|
@@ -62,8 +49,6 @@ export function ConversationHeader({
|
|
|
62
49
|
onToggle,
|
|
63
50
|
hideApiFormat = false,
|
|
64
51
|
isLoading = false,
|
|
65
|
-
viewMode,
|
|
66
|
-
onToggleViewMode,
|
|
67
52
|
userAgent,
|
|
68
53
|
}: ConversationHeaderProps): JSX.Element {
|
|
69
54
|
return (
|
|
@@ -93,30 +78,6 @@ export function ConversationHeader({
|
|
|
93
78
|
<ChevronRight className="size-4 text-muted-foreground shrink-0" />
|
|
94
79
|
)}
|
|
95
80
|
|
|
96
|
-
{/* Thread/flat view toggle — only shown when expanded */}
|
|
97
|
-
{expanded && onToggleViewMode !== undefined && (
|
|
98
|
-
<button
|
|
99
|
-
type="button"
|
|
100
|
-
onClick={(e) => {
|
|
101
|
-
e.stopPropagation();
|
|
102
|
-
onToggleViewMode();
|
|
103
|
-
}}
|
|
104
|
-
className={cn(
|
|
105
|
-
"px-1.5 py-0.5 rounded text-[10px] font-mono transition-colors shrink-0 cursor-pointer",
|
|
106
|
-
viewMode === "thread"
|
|
107
|
-
? "bg-amber-500/15 text-amber-400 border border-amber-500/30"
|
|
108
|
-
: "bg-muted text-muted-foreground border border-border hover:text-foreground",
|
|
109
|
-
)}
|
|
110
|
-
title={
|
|
111
|
-
viewMode === "thread"
|
|
112
|
-
? "Thread view — click for flat view"
|
|
113
|
-
: "Flat view — click for thread view"
|
|
114
|
-
}
|
|
115
|
-
>
|
|
116
|
-
<GitBranch className="size-3" />
|
|
117
|
-
</button>
|
|
118
|
-
)}
|
|
119
|
-
|
|
120
81
|
{/* Conversation ID */}
|
|
121
82
|
<span
|
|
122
83
|
className="text-purple-400/90 font-mono text-xs font-semibold shrink-0"
|
|
@@ -155,7 +155,7 @@ export const LogEntryHeader = memo(function ({
|
|
|
155
155
|
|
|
156
156
|
{/* Elapsed time */}
|
|
157
157
|
{log.elapsedMs !== null && (
|
|
158
|
-
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
158
|
+
<span className="hidden xl:flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
159
159
|
<Clock className="size-3" />
|
|
160
160
|
<span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
|
|
161
161
|
</span>
|
|
@@ -260,7 +260,7 @@ export const LogEntryHeader = memo(function ({
|
|
|
260
260
|
{/* Origin */}
|
|
261
261
|
{log.origin !== null && (
|
|
262
262
|
<span
|
|
263
|
-
className="flex items-center gap-1 text-muted-foreground text-xs shrink-0"
|
|
263
|
+
className="hidden xl:flex items-center gap-1 text-muted-foreground text-xs shrink-0"
|
|
264
264
|
title={`Origin: ${log.origin}`}
|
|
265
265
|
>
|
|
266
266
|
<Globe className="size-3" />
|
|
@@ -275,7 +275,7 @@ export const LogEntryHeader = memo(function ({
|
|
|
275
275
|
<TooltipProvider>
|
|
276
276
|
<Tooltip>
|
|
277
277
|
<TooltipTrigger asChild>
|
|
278
|
-
<span className="flex items-center gap-1 text-purple-400/80 text-xs shrink-0">
|
|
278
|
+
<span className="hidden xl:flex items-center gap-1 text-purple-400/80 text-xs shrink-0">
|
|
279
279
|
<FileTerminal className="size-3" />
|
|
280
280
|
{log.clientProjectFolder !== null ? (
|
|
281
281
|
<span className="font-mono tabular-nums">{log.clientProjectFolder}</span>
|
|
@@ -1,79 +1,103 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type JSX, useMemo } from "react";
|
|
2
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
2
3
|
import { cn } from "../../lib/utils";
|
|
3
4
|
import type { StopReason } from "../../lib/stopReason";
|
|
5
|
+
import { getCrabVariant } from "../ui/crab-variants";
|
|
4
6
|
|
|
5
7
|
export type ThreadConnectorProps = {
|
|
6
8
|
stopReason: StopReason;
|
|
7
9
|
isPending: boolean;
|
|
8
10
|
isFirst: boolean;
|
|
9
|
-
isLast: boolean;
|
|
10
11
|
/** True when this entry starts a new turn (first overall, or after end_turn/stop). */
|
|
11
12
|
isTurnStart: boolean;
|
|
13
|
+
/** Seed for crab variant selection (0-11). */
|
|
14
|
+
crabIndex?: number;
|
|
15
|
+
/** When true, the boundary marker is clickable to collapse/expand the turn. */
|
|
16
|
+
collapsible?: boolean;
|
|
17
|
+
collapsed?: boolean;
|
|
18
|
+
onToggle?: () => void;
|
|
12
19
|
};
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
22
|
* Vertical timeline connector for thread view. Uses flexbox layout (no
|
|
16
23
|
* absolute positioning) so the connector naturally tracks its sibling
|
|
17
24
|
* LogEntry height — no scroll jitter.
|
|
25
|
+
*
|
|
26
|
+
* Markers use the CrabLogo. At turn starts the crab is static (amber),
|
|
27
|
+
* during processing it "crawls" downward with a bounce animation, and
|
|
28
|
+
* at turn boundaries it stops with a glow.
|
|
18
29
|
*/
|
|
19
30
|
export function ThreadConnector({
|
|
20
31
|
stopReason,
|
|
21
32
|
isPending,
|
|
22
33
|
isFirst,
|
|
23
|
-
isLast: _isLast,
|
|
24
34
|
isTurnStart,
|
|
35
|
+
crabIndex = 0,
|
|
36
|
+
collapsible = false,
|
|
37
|
+
collapsed = false,
|
|
38
|
+
onToggle,
|
|
25
39
|
}: ThreadConnectorProps): JSX.Element {
|
|
26
40
|
const isBoundary = stopReason === "end_turn" || stopReason === "stop";
|
|
27
|
-
const
|
|
41
|
+
const isRunning = isPending && !isBoundary;
|
|
42
|
+
const Crab = useMemo(() => getCrabVariant(crabIndex), [crabIndex]);
|
|
28
43
|
|
|
29
44
|
return (
|
|
30
45
|
<div className="flex flex-col items-center w-6 shrink-0">
|
|
31
|
-
{/* Top: incoming line
|
|
32
|
-
|
|
33
|
-
<div className="flex justify-center h-4">
|
|
46
|
+
{/* Top: incoming line */}
|
|
47
|
+
<div className="flex justify-center h-2.5">
|
|
34
48
|
{!isFirst && <div className="w-0.5 bg-muted-foreground/30" />}
|
|
35
49
|
</div>
|
|
36
50
|
|
|
37
|
-
{/*
|
|
51
|
+
{/* Collapse toggle — sits above the crab, no overlap */}
|
|
52
|
+
{collapsible && (
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={onToggle}
|
|
56
|
+
title={collapsed ? "Expand turn" : "Collapse turn"}
|
|
57
|
+
className="cursor-pointer flex justify-center py-0.5"
|
|
58
|
+
>
|
|
59
|
+
{collapsed ? (
|
|
60
|
+
<ChevronRight className="size-3 text-muted-foreground hover:text-foreground transition-colors" />
|
|
61
|
+
) : (
|
|
62
|
+
<ChevronDown className="size-3 text-muted-foreground hover:text-foreground transition-colors" />
|
|
63
|
+
)}
|
|
64
|
+
</button>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{/* Center marker — CrabLogo */}
|
|
38
68
|
<div className="flex items-center justify-center py-0.5">
|
|
39
69
|
{isBoundary ? (
|
|
40
|
-
<
|
|
41
|
-
className={cn(
|
|
42
|
-
"size-2.5 rounded-full border-2",
|
|
43
|
-
"bg-background border-amber-400",
|
|
44
|
-
"shadow-[0_0_6px_rgba(251,191,36,0.4)]",
|
|
45
|
-
)}
|
|
70
|
+
<span
|
|
46
71
|
title={stopReason === "end_turn" ? "End of Turn (Anthropic)" : "End of Turn (OpenAI)"}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
>
|
|
73
|
+
<Crab
|
|
74
|
+
className={cn(
|
|
75
|
+
"size-3.5 text-amber-400",
|
|
76
|
+
"animate-crab-settle",
|
|
77
|
+
"drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]",
|
|
78
|
+
)}
|
|
79
|
+
/>
|
|
80
|
+
</span>
|
|
81
|
+
) : isTurnStart ? (
|
|
82
|
+
<span title="Start of turn">
|
|
83
|
+
<Crab
|
|
84
|
+
className={cn(
|
|
85
|
+
"size-3.5 text-emerald-400",
|
|
86
|
+
"animate-crab-appear",
|
|
87
|
+
"drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
|
|
88
|
+
)}
|
|
89
|
+
/>
|
|
90
|
+
</span>
|
|
91
|
+
) : isRunning ? (
|
|
92
|
+
<span title="Processing…">
|
|
93
|
+
<Crab className={cn("size-3.5 text-amber-300/80", "animate-crab-crawl")} />
|
|
94
|
+
</span>
|
|
63
95
|
) : (
|
|
64
|
-
<
|
|
65
|
-
className={cn(
|
|
66
|
-
"size-1.5 rounded-full",
|
|
67
|
-
isTurnStart
|
|
68
|
-
? "bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.5)]"
|
|
69
|
-
: "bg-muted-foreground/30",
|
|
70
|
-
)}
|
|
71
|
-
/>
|
|
96
|
+
<Crab className="size-3.5 text-muted-foreground/40" />
|
|
72
97
|
)}
|
|
73
98
|
</div>
|
|
74
99
|
|
|
75
|
-
{/* Bottom: outgoing line
|
|
76
|
-
flex-1 fills the remaining height of the LogEntry card. */}
|
|
100
|
+
{/* Bottom: outgoing line */}
|
|
77
101
|
<div className="flex-1 flex justify-center min-h-1">
|
|
78
102
|
{isBoundary ? (
|
|
79
103
|
<div className="w-0.5 bg-muted-foreground/10 h-4" />
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useState, useMemo, type JSX, useCallback } from "react";
|
|
2
|
+
import type { CapturedLog } from "../../proxy/schemas";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import { extractStopReason, isTurnBoundary } from "../../lib/stopReason";
|
|
5
|
+
import { LogEntry } from "./LogEntry";
|
|
6
|
+
import { ThreadConnector } from "./ThreadConnector";
|
|
7
|
+
import type { CacheTrendEntry } from "./cacheTrend";
|
|
8
|
+
|
|
9
|
+
type TurnGroupProps = {
|
|
10
|
+
logs: CapturedLog[];
|
|
11
|
+
viewMode: "simple" | "full";
|
|
12
|
+
strip: boolean;
|
|
13
|
+
cacheTrends?: Map<number, CacheTrendEntry>;
|
|
14
|
+
onCompareWithPrevious: (log: CapturedLog) => void;
|
|
15
|
+
/** Turn index for alternating background colours. */
|
|
16
|
+
turnIndex?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function TurnGroup({
|
|
20
|
+
logs,
|
|
21
|
+
viewMode,
|
|
22
|
+
strip,
|
|
23
|
+
cacheTrends,
|
|
24
|
+
onCompareWithPrevious,
|
|
25
|
+
turnIndex = 0,
|
|
26
|
+
}: TurnGroupProps): JSX.Element {
|
|
27
|
+
const stopReasons = useMemo(() => logs.map((l) => extractStopReason(l)), [logs]);
|
|
28
|
+
const lastIdx = logs.length - 1;
|
|
29
|
+
const lastStop = stopReasons[lastIdx] ?? null;
|
|
30
|
+
const isComplete = lastStop !== null ? isTurnBoundary(lastStop) : false;
|
|
31
|
+
const isPending = logs[lastIdx]?.responseStatus === null;
|
|
32
|
+
const collapsible = isComplete && !isPending;
|
|
33
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
34
|
+
|
|
35
|
+
const toggleCollapse = useCallback(() => {
|
|
36
|
+
if (collapsible) setCollapsed((prev) => !prev);
|
|
37
|
+
}, [collapsible]);
|
|
38
|
+
|
|
39
|
+
// When collapsed, only show the boundary entry
|
|
40
|
+
const visibleLogs = useMemo(() => {
|
|
41
|
+
if (!collapsed) return logs;
|
|
42
|
+
const last = logs[lastIdx];
|
|
43
|
+
return last !== undefined ? [last] : [];
|
|
44
|
+
}, [logs, collapsed, lastIdx]);
|
|
45
|
+
|
|
46
|
+
const bgClass = turnIndex % 2 === 0 ? "bg-muted/10" : "bg-muted/25";
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className={cn("border rounded-lg", isPending ? "border-amber-500/10" : "border-transparent")}
|
|
51
|
+
>
|
|
52
|
+
{visibleLogs.map((log, visibleIdx) => {
|
|
53
|
+
const originalIdx = collapsed ? lastIdx : visibleIdx;
|
|
54
|
+
const reason = stopReasons[originalIdx] ?? null;
|
|
55
|
+
const isBoundary = reason === "end_turn" || reason === "stop";
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div key={log.id} className="flex items-stretch">
|
|
59
|
+
<ThreadConnector
|
|
60
|
+
stopReason={reason}
|
|
61
|
+
isPending={log.responseStatus === null}
|
|
62
|
+
isFirst={originalIdx === 0}
|
|
63
|
+
isTurnStart={originalIdx === 0}
|
|
64
|
+
crabIndex={log.id % 12}
|
|
65
|
+
collapsible={collapsible && isBoundary && logs.length > 1}
|
|
66
|
+
collapsed={collapsed}
|
|
67
|
+
onToggle={toggleCollapse}
|
|
68
|
+
/>
|
|
69
|
+
<div className={cn("flex-1 min-w-0 mb-2 rounded-lg", bgClass)}>
|
|
70
|
+
<LogEntry
|
|
71
|
+
log={log}
|
|
72
|
+
viewMode={viewMode}
|
|
73
|
+
strip={strip}
|
|
74
|
+
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
75
|
+
onCompareWithPrevious={() => onCompareWithPrevious(log)}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|