@brainpilot/web 0.0.4 → 0.0.6
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/dist/assets/index-Br55rkHb.css +1 -0
- package/dist/assets/index-CeUzk-ej.js +445 -0
- package/dist/index.html +2 -2
- package/index.html +13 -0
- package/package.json +12 -3
- package/src/App.tsx +10 -0
- package/src/__tests__/agentsReducer.test.ts +67 -0
- package/src/__tests__/api.test.ts +221 -0
- package/src/__tests__/chatScrollMemory.test.ts +49 -0
- package/src/__tests__/demoConversation.test.ts +73 -0
- package/src/__tests__/demoReset.test.ts +24 -0
- package/src/__tests__/messageGroups.test.ts +80 -0
- package/src/__tests__/newUiComponents.test.tsx +101 -0
- package/src/__tests__/newUiEvents.test.ts +236 -0
- package/src/__tests__/runningToast.test.ts +29 -0
- package/src/__tests__/tokenUsage.test.ts +48 -0
- package/src/__tests__/toolDisplay.test.ts +55 -0
- package/src/__tests__/traceReducer.test.ts +62 -0
- package/src/components/chat/AskUserCard.tsx +123 -0
- package/src/components/chat/AutoRetryIndicator.tsx +71 -0
- package/src/components/chat/ComposerInput.tsx +73 -0
- package/src/components/chat/ComposerSendButton.tsx +26 -0
- package/src/components/chat/MarkdownMessage.tsx +24 -0
- package/src/components/chat/MessageStream.tsx +505 -0
- package/src/components/chat/PromptComposer.tsx +489 -0
- package/src/components/chat/SystemMessageBubble.tsx +46 -0
- package/src/components/chat/chatScrollMemory.ts +49 -0
- package/src/components/demo/DemoFileTree.tsx +146 -0
- package/src/components/demo/DemoView.tsx +730 -0
- package/src/components/demo/TraceNodeModal.tsx +80 -0
- package/src/components/demo/demoBundle.ts +223 -0
- package/src/components/demo/demoCache.ts +42 -0
- package/src/components/demo/demoReset.ts +16 -0
- package/src/components/files/FilePreviewView.tsx +153 -0
- package/src/components/files/FileSidebar.tsx +664 -0
- package/src/components/files/filePreview.ts +113 -0
- package/src/components/primitives/CustomSelect.tsx +200 -0
- package/src/components/primitives/IconButton.tsx +27 -0
- package/src/components/quota/DiskQuotaCriticalDialog.tsx +56 -0
- package/src/components/quota/DiskQuotaWarningDialog.tsx +65 -0
- package/src/components/quota/QuotaFileManager.tsx +197 -0
- package/src/components/search/SearchDialog.tsx +101 -0
- package/src/components/session/AgentNetwork.tsx +1233 -0
- package/src/components/session/AgentTraceViews.tsx +346 -0
- package/src/components/session/AnalyticsTab.tsx +220 -0
- package/src/components/session/GlobalOverview.tsx +108 -0
- package/src/components/session/NodeTooltip.tsx +127 -0
- package/src/components/session/TimelineTab.tsx +320 -0
- package/src/components/session/TraceGraphView.tsx +307 -0
- package/src/components/session/TraceNodeDetail.tsx +179 -0
- package/src/components/session/agentAnalytics.ts +397 -0
- package/src/components/session/agentNetworkShared.ts +339 -0
- package/src/components/session/traceLayout.ts +182 -0
- package/src/components/settings/SettingsDialog.tsx +737 -0
- package/src/components/shell/DesktopShell.tsx +261 -0
- package/src/components/shell/SandboxBuildingOverlay.tsx +73 -0
- package/src/components/shell/SandboxStatus.tsx +287 -0
- package/src/components/shell/TerminalDrawer.tsx +387 -0
- package/src/components/sidebar/Sidebar.tsx +191 -0
- package/src/config.ts +10 -0
- package/src/contexts/AppProviders.tsx +20 -0
- package/src/contexts/AuthContext.tsx +61 -0
- package/src/contexts/PreferencesContext.tsx +125 -0
- package/src/contexts/SSEContext.tsx +264 -0
- package/src/contexts/SandboxContext.tsx +310 -0
- package/src/contexts/SessionContext.tsx +919 -0
- package/src/contexts/agentsReducer.ts +49 -0
- package/src/contexts/draftStore.ts +103 -0
- package/src/contexts/messageFilters.ts +29 -0
- package/src/contexts/messageGroups.ts +77 -0
- package/src/contexts/messageReducer.ts +401 -0
- package/src/contexts/newUiEvents.ts +190 -0
- package/src/contexts/runningToast.ts +33 -0
- package/src/contexts/traceReducer.ts +62 -0
- package/src/contexts/turnTimer.test.ts +97 -0
- package/src/contexts/turnTimer.ts +108 -0
- package/src/contexts/useTurnTimer.ts +104 -0
- package/src/contracts/backend.ts +897 -0
- package/src/contracts/demoBundle.ts +83 -0
- package/src/i18n/messages/analytics.ts +106 -0
- package/src/i18n/messages/chat.ts +130 -0
- package/src/i18n/messages/contexts.ts +42 -0
- package/src/i18n/messages/demo.ts +80 -0
- package/src/i18n/messages/files.ts +82 -0
- package/src/i18n/messages/network.ts +190 -0
- package/src/i18n/messages/profile.ts +44 -0
- package/src/i18n/messages/quota.ts +36 -0
- package/src/i18n/messages/sandbox.ts +116 -0
- package/src/i18n/messages/search.ts +16 -0
- package/src/i18n/messages/settings.ts +188 -0
- package/src/i18n/messages/shell.ts +38 -0
- package/src/i18n/messages/sidebar.ts +52 -0
- package/src/i18n/messages/terminal.ts +22 -0
- package/src/i18n/messages/trace.ts +136 -0
- package/src/i18n/messages.ts +32 -0
- package/src/i18n/translate.ts +46 -0
- package/src/i18n/types.ts +15 -0
- package/src/i18n/useT.ts +15 -0
- package/src/main.tsx +13 -0
- package/src/mocks/backend.ts +729 -0
- package/src/styles/global.css +7578 -0
- package/src/styles/tokens.css +161 -0
- package/src/utils/api.ts +724 -0
- package/src/utils/download.ts +18 -0
- package/src/utils/format.ts +7 -0
- package/src/utils/toolDisplay.ts +74 -0
- package/src/utils/zip.ts +119 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +22 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +13 -0
- package/vite.config.ts +13 -0
- package/dist/assets/index-Cd0Mi_WU.css +0 -1
- package/dist/assets/index-FGg-DeYR.js +0 -448
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { Minus, Plus, RotateCcw } from "lucide-react";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { TraceNode } from "../../contracts/backend";
|
|
4
|
+
import {
|
|
5
|
+
TraceLayoutDirection,
|
|
6
|
+
buildTraceLayout,
|
|
7
|
+
getNodeKind,
|
|
8
|
+
normalizeStatus,
|
|
9
|
+
relationLabels,
|
|
10
|
+
truncateNodeTitle,
|
|
11
|
+
} from "./traceLayout";
|
|
12
|
+
|
|
13
|
+
interface TraceGraphViewProps {
|
|
14
|
+
/** Nodes already filtered / sliced / edge-pruned by the host. */
|
|
15
|
+
nodes: TraceNode[];
|
|
16
|
+
direction: TraceLayoutDirection;
|
|
17
|
+
selectedNodeId: string | null;
|
|
18
|
+
onSelectNode: (id: string) => void;
|
|
19
|
+
zoom: number;
|
|
20
|
+
onZoomChange: (zoom: number) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Bumping this re-fits and recenters the viewport on the current content.
|
|
23
|
+
* The host bumps it when the graph advances (e.g. playback auto-advance) so
|
|
24
|
+
* newly revealed nodes scroll into view; manual scrubbing leaves it alone.
|
|
25
|
+
*/
|
|
26
|
+
fitToken?: number | string;
|
|
27
|
+
/** Shown when there are no nodes to display. */
|
|
28
|
+
emptyLabel?: string;
|
|
29
|
+
formatKind?: (kind: string) => string;
|
|
30
|
+
zoomLabels?: {
|
|
31
|
+
controls?: string;
|
|
32
|
+
zoomIn?: string;
|
|
33
|
+
zoomOut?: string;
|
|
34
|
+
reset?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Presentational reasoning-trace graph: the SVG DAG, zoom controls, viewport
|
|
40
|
+
* pan/drag, wheel-zoom, and per-node drag offsets. Owns only view-local state
|
|
41
|
+
* (node offsets, drag). Extracted from TracePanel so the live trace view and
|
|
42
|
+
* the demo replay render the same graph.
|
|
43
|
+
*/
|
|
44
|
+
export function TraceGraphView({
|
|
45
|
+
nodes,
|
|
46
|
+
direction,
|
|
47
|
+
selectedNodeId,
|
|
48
|
+
onSelectNode,
|
|
49
|
+
zoom,
|
|
50
|
+
onZoomChange,
|
|
51
|
+
fitToken,
|
|
52
|
+
emptyLabel,
|
|
53
|
+
formatKind,
|
|
54
|
+
zoomLabels,
|
|
55
|
+
}: TraceGraphViewProps) {
|
|
56
|
+
const [nodeOffsets, setNodeOffsets] = useState<Map<string, { dx: number; dy: number }>>(new Map());
|
|
57
|
+
const viewportRef = useRef<HTMLDivElement | null>(null);
|
|
58
|
+
const layoutRef = useRef<ReturnType<typeof buildTraceLayout> | null>(null);
|
|
59
|
+
const draggingNodeRef = useRef<{
|
|
60
|
+
nodeId: string;
|
|
61
|
+
startClientX: number;
|
|
62
|
+
startClientY: number;
|
|
63
|
+
startDx: number;
|
|
64
|
+
startDy: number;
|
|
65
|
+
} | null>(null);
|
|
66
|
+
const dragRef = useRef<{
|
|
67
|
+
isDragging: boolean;
|
|
68
|
+
startX: number;
|
|
69
|
+
startY: number;
|
|
70
|
+
scrollLeft: number;
|
|
71
|
+
scrollTop: number;
|
|
72
|
+
hasPanned: boolean;
|
|
73
|
+
}>({ isDragging: false, startX: 0, startY: 0, scrollLeft: 0, scrollTop: 0, hasPanned: false });
|
|
74
|
+
|
|
75
|
+
const layout = useMemo(() => buildTraceLayout(nodes, direction), [direction, nodes]);
|
|
76
|
+
const adjustedLayout = useMemo(() => {
|
|
77
|
+
if (nodeOffsets.size === 0) return layout;
|
|
78
|
+
const adjustedPositioned = layout.positioned.map((p) => {
|
|
79
|
+
const offset = nodeOffsets.get(p.node.id);
|
|
80
|
+
if (!offset) return p;
|
|
81
|
+
return { ...p, x: p.x + offset.dx, y: p.y + offset.dy };
|
|
82
|
+
});
|
|
83
|
+
const adjustedById = new Map(adjustedPositioned.map((p) => [p.node.id, p]));
|
|
84
|
+
return { ...layout, positioned: adjustedPositioned, byId: adjustedById };
|
|
85
|
+
}, [layout, nodeOffsets]);
|
|
86
|
+
layoutRef.current = adjustedLayout;
|
|
87
|
+
const zoomPercent = Math.round(zoom * 100);
|
|
88
|
+
|
|
89
|
+
const zoomIn = () => onZoomChange(Math.min(2, Number((zoom + 0.1).toFixed(2))));
|
|
90
|
+
const zoomOut = () => onZoomChange(Math.max(0.05, Number((zoom - 0.1).toFixed(2))));
|
|
91
|
+
const resetZoom = () => onZoomChange(1);
|
|
92
|
+
|
|
93
|
+
// Re-fit + recenter on fitToken change (host signals an advance).
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (fitToken === undefined || !viewportRef.current) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const l = layoutRef.current;
|
|
99
|
+
if (!l || l.positioned.length === 0) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const viewport = viewportRef.current;
|
|
103
|
+
const padding = 80;
|
|
104
|
+
const xs = l.positioned.map((p) => p.x);
|
|
105
|
+
const ys = l.positioned.map((p) => p.y);
|
|
106
|
+
const minX = Math.min(...xs) - padding;
|
|
107
|
+
const minY = Math.min(...ys) - padding;
|
|
108
|
+
const maxX = Math.max(...xs.map((x) => x + l.nodeWidth)) + padding;
|
|
109
|
+
const maxY = Math.max(...ys.map((y) => y + l.nodeHeight)) + padding;
|
|
110
|
+
const contentWidth = maxX - minX;
|
|
111
|
+
const contentHeight = maxY - minY;
|
|
112
|
+
const viewportWidth = viewport.clientWidth;
|
|
113
|
+
const viewportHeight = viewport.clientHeight;
|
|
114
|
+
const fitZoom = Math.min(viewportWidth / contentWidth, viewportHeight / contentHeight, 1.5);
|
|
115
|
+
const nextZoom = Math.max(0.05, Number(fitZoom.toFixed(2)));
|
|
116
|
+
// Avoid micro-rescales on every reveal — only re-zoom on a meaningful change,
|
|
117
|
+
// so the graph doesn't visibly "breathe" as nodes stream in.
|
|
118
|
+
if (Math.abs(nextZoom - zoom) > 0.06) {
|
|
119
|
+
onZoomChange(nextZoom);
|
|
120
|
+
}
|
|
121
|
+
const effectiveZoom = Math.abs(nextZoom - zoom) > 0.06 ? nextZoom : zoom;
|
|
122
|
+
requestAnimationFrame(() => {
|
|
123
|
+
if (!viewportRef.current) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const vp = viewportRef.current;
|
|
127
|
+
const centerScrollLeft = ((minX + maxX) / 2) * effectiveZoom - viewportWidth / 2;
|
|
128
|
+
const centerScrollTop = ((minY + maxY) / 2) * effectiveZoom - viewportHeight / 2;
|
|
129
|
+
vp.scrollTo({
|
|
130
|
+
left: Math.max(0, centerScrollLeft),
|
|
131
|
+
top: Math.max(0, centerScrollTop),
|
|
132
|
+
behavior: "smooth",
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
136
|
+
}, [fitToken]);
|
|
137
|
+
|
|
138
|
+
// Per-node drag.
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
141
|
+
if (!draggingNodeRef.current) return;
|
|
142
|
+
const { nodeId, startClientX, startClientY, startDx, startDy } = draggingNodeRef.current;
|
|
143
|
+
const dx = startDx + (e.clientX - startClientX) / zoom;
|
|
144
|
+
const dy = startDy + (e.clientY - startClientY) / zoom;
|
|
145
|
+
setNodeOffsets((prev) => {
|
|
146
|
+
const next = new Map(prev);
|
|
147
|
+
next.set(nodeId, { dx, dy });
|
|
148
|
+
return next;
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
const handleMouseUp = () => {
|
|
152
|
+
draggingNodeRef.current = null;
|
|
153
|
+
};
|
|
154
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
155
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
156
|
+
return () => {
|
|
157
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
158
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
159
|
+
};
|
|
160
|
+
}, [zoom]);
|
|
161
|
+
|
|
162
|
+
if (nodes.length === 0) {
|
|
163
|
+
return <p className="trace-empty">{emptyLabel ?? ""}</p>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<>
|
|
168
|
+
<div className="trace-zoom-controls" aria-label={zoomLabels?.controls}>
|
|
169
|
+
<button disabled={zoom <= 0.05} onClick={zoomOut} type="button" aria-label={zoomLabels?.zoomOut}>
|
|
170
|
+
<Minus size={13} />
|
|
171
|
+
</button>
|
|
172
|
+
<span>{zoomPercent}%</span>
|
|
173
|
+
<button disabled={zoom >= 2} onClick={zoomIn} type="button" aria-label={zoomLabels?.zoomIn}>
|
|
174
|
+
<Plus size={13} />
|
|
175
|
+
</button>
|
|
176
|
+
<button disabled={zoom === 1} onClick={resetZoom} type="button" aria-label={zoomLabels?.reset}>
|
|
177
|
+
<RotateCcw size={11} />
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
<div
|
|
181
|
+
ref={viewportRef}
|
|
182
|
+
className="trace-map__viewport"
|
|
183
|
+
onMouseDown={(event) => {
|
|
184
|
+
const viewport = event.currentTarget;
|
|
185
|
+
dragRef.current = {
|
|
186
|
+
isDragging: true,
|
|
187
|
+
startX: event.clientX,
|
|
188
|
+
startY: event.clientY,
|
|
189
|
+
scrollLeft: viewport.scrollLeft,
|
|
190
|
+
scrollTop: viewport.scrollTop,
|
|
191
|
+
hasPanned: false,
|
|
192
|
+
};
|
|
193
|
+
}}
|
|
194
|
+
onMouseMove={(event) => {
|
|
195
|
+
if (!dragRef.current.isDragging) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const dx = event.clientX - dragRef.current.startX;
|
|
199
|
+
const dy = event.clientY - dragRef.current.startY;
|
|
200
|
+
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
|
|
201
|
+
dragRef.current.hasPanned = true;
|
|
202
|
+
}
|
|
203
|
+
const viewport = event.currentTarget;
|
|
204
|
+
viewport.scrollLeft = dragRef.current.scrollLeft - dx;
|
|
205
|
+
viewport.scrollTop = dragRef.current.scrollTop - dy;
|
|
206
|
+
}}
|
|
207
|
+
onMouseUp={() => {
|
|
208
|
+
dragRef.current.isDragging = false;
|
|
209
|
+
}}
|
|
210
|
+
onMouseLeave={() => {
|
|
211
|
+
dragRef.current.isDragging = false;
|
|
212
|
+
}}
|
|
213
|
+
onWheel={(event) => {
|
|
214
|
+
event.preventDefault();
|
|
215
|
+
if (event.deltaY === 0) return;
|
|
216
|
+
const step = event.deltaY > 0 ? -0.1 : 0.1;
|
|
217
|
+
onZoomChange(Math.max(0.05, Math.min(2, Number((zoom + step).toFixed(2)))));
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<svg
|
|
221
|
+
height={adjustedLayout.height * zoom}
|
|
222
|
+
viewBox={`0 0 ${adjustedLayout.width} ${adjustedLayout.height}`}
|
|
223
|
+
width={adjustedLayout.width * zoom}
|
|
224
|
+
role="img"
|
|
225
|
+
>
|
|
226
|
+
<defs>
|
|
227
|
+
<marker id="trace-arrow" markerHeight="8" markerWidth="8" orient="auto" refX="7" refY="4">
|
|
228
|
+
<path d="M 0 0 L 8 4 L 0 8 z" />
|
|
229
|
+
</marker>
|
|
230
|
+
</defs>
|
|
231
|
+
{adjustedLayout.positioned.flatMap(({ node }) =>
|
|
232
|
+
node.parents.map((parentRef) => {
|
|
233
|
+
const parent = adjustedLayout.byId.get(parentRef.id);
|
|
234
|
+
const child = adjustedLayout.byId.get(node.id);
|
|
235
|
+
if (!parent || !child) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const isHorizontal = direction === "LR";
|
|
239
|
+
const startX = parent.x + (isHorizontal ? adjustedLayout.nodeWidth : adjustedLayout.nodeWidth / 2);
|
|
240
|
+
const startY = parent.y + (isHorizontal ? adjustedLayout.nodeHeight / 2 : adjustedLayout.nodeHeight);
|
|
241
|
+
const endX = child.x + (isHorizontal ? 0 : adjustedLayout.nodeWidth / 2);
|
|
242
|
+
const endY = child.y + (isHorizontal ? adjustedLayout.nodeHeight / 2 : 0);
|
|
243
|
+
const path = isHorizontal
|
|
244
|
+
? `M ${startX} ${startY} C ${startX + 54} ${startY}, ${endX - 54} ${endY}, ${endX} ${endY}`
|
|
245
|
+
: `M ${startX} ${startY} C ${startX} ${startY + 42}, ${endX} ${endY - 42}, ${endX} ${endY}`;
|
|
246
|
+
const midX = (startX + endX) / 2;
|
|
247
|
+
const midY = (startY + endY) / 2;
|
|
248
|
+
const labelText = parentRef.relation ? (relationLabels[parentRef.relation] || parentRef.relation) : "";
|
|
249
|
+
const labelWidth = Math.max(48, labelText.length * 5.5 + 10);
|
|
250
|
+
return (
|
|
251
|
+
<g className={`trace-edge trace-edge--${parentRef.relation || "necessitated_by"}`} key={`${parentRef.id}-${node.id}`}>
|
|
252
|
+
<path d={path} />
|
|
253
|
+
{parentRef.relation ? (
|
|
254
|
+
<g className="trace-edge-label">
|
|
255
|
+
<rect
|
|
256
|
+
x={midX - labelWidth / 2}
|
|
257
|
+
y={midY - 14}
|
|
258
|
+
width={labelWidth}
|
|
259
|
+
height={13}
|
|
260
|
+
rx={4}
|
|
261
|
+
/>
|
|
262
|
+
<text x={midX} y={midY - 7}>{labelText}</text>
|
|
263
|
+
</g>
|
|
264
|
+
) : null}
|
|
265
|
+
</g>
|
|
266
|
+
);
|
|
267
|
+
}),
|
|
268
|
+
)}
|
|
269
|
+
{adjustedLayout.positioned.map(({ node, x, y }) => {
|
|
270
|
+
const kind = getNodeKind(node);
|
|
271
|
+
const kindLabel = formatKind?.(kind) ?? kind;
|
|
272
|
+
return (
|
|
273
|
+
<g
|
|
274
|
+
className={`trace-map-node trace-map-node--${kind} trace-map-node--${normalizeStatus(node.status)} ${selectedNodeId === node.id ? "is-selected" : ""} ${draggingNodeRef.current?.nodeId === node.id ? "is-dragging" : ""}`}
|
|
275
|
+
key={node.id}
|
|
276
|
+
onMouseDown={(e) => {
|
|
277
|
+
e.stopPropagation();
|
|
278
|
+
const offset = nodeOffsets.get(node.id) || { dx: 0, dy: 0 };
|
|
279
|
+
draggingNodeRef.current = {
|
|
280
|
+
nodeId: node.id,
|
|
281
|
+
startClientX: e.clientX,
|
|
282
|
+
startClientY: e.clientY,
|
|
283
|
+
startDx: offset.dx,
|
|
284
|
+
startDy: offset.dy,
|
|
285
|
+
};
|
|
286
|
+
}}
|
|
287
|
+
onClick={() => {
|
|
288
|
+
if (dragRef.current.hasPanned || draggingNodeRef.current) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
onSelectNode(node.id);
|
|
292
|
+
}}
|
|
293
|
+
style={{ transform: `translate(${x}px, ${y}px)` }}
|
|
294
|
+
>
|
|
295
|
+
<rect height={adjustedLayout.nodeHeight} rx="8" width={adjustedLayout.nodeWidth} />
|
|
296
|
+
<circle className={`trace-node__dot--${normalizeStatus(node.status)}`} cx="16" cy="24" r="4" />
|
|
297
|
+
<text className="trace-map-node__title" x="28" y="26">{truncateNodeTitle(node.title)}</text>
|
|
298
|
+
<text className="trace-map-node__meta" x="28" y="44">{node.agent || kindLabel}</text>
|
|
299
|
+
<text className="trace-map-node__kind" x="28" y="58">{kindLabel}</text>
|
|
300
|
+
</g>
|
|
301
|
+
);
|
|
302
|
+
})}
|
|
303
|
+
</svg>
|
|
304
|
+
</div>
|
|
305
|
+
</>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { AlertTriangle, ArrowRight, Box, Clock3, FileText, GitBranch, Timer, Wrench } from "lucide-react";
|
|
2
|
+
import { TraceNode } from "../../contracts/backend";
|
|
3
|
+
import { TranslateVars } from "../../i18n/translate";
|
|
4
|
+
import { formatToolName } from "../../utils/toolDisplay";
|
|
5
|
+
import {
|
|
6
|
+
artifactLabels,
|
|
7
|
+
formatDuration,
|
|
8
|
+
formatTime,
|
|
9
|
+
getNodeKind,
|
|
10
|
+
getStatusLabelKey,
|
|
11
|
+
normalizeStatus,
|
|
12
|
+
relationLabels,
|
|
13
|
+
} from "./traceLayout";
|
|
14
|
+
|
|
15
|
+
interface TraceNodeDetailProps {
|
|
16
|
+
node: TraceNode | null;
|
|
17
|
+
nodes?: TraceNode[];
|
|
18
|
+
onSelectNode: (id: string) => void;
|
|
19
|
+
/** When provided, artifact rows become buttons that focus that file. */
|
|
20
|
+
onSelectArtifact?: (path: string) => void;
|
|
21
|
+
/** Currently focused artifact path (for highlight). */
|
|
22
|
+
activeArtifactPath?: string | null;
|
|
23
|
+
formatKind?: (kind: string) => string;
|
|
24
|
+
t: (key: string, vars?: TranslateVars) => string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Presentational detail pane for a single reasoning-trace node. Extracted from
|
|
29
|
+
* TracePanel so the live trace view and the demo replay share it. In the demo
|
|
30
|
+
* an `onSelectArtifact` handler wires artifact rows to the file preview.
|
|
31
|
+
*/
|
|
32
|
+
export function TraceNodeDetail({ node, nodes, onSelectNode, onSelectArtifact, activeArtifactPath, formatKind, t }: TraceNodeDetailProps) {
|
|
33
|
+
if (!node) {
|
|
34
|
+
return <p>{t("trace.node.noneSelected")}</p>;
|
|
35
|
+
}
|
|
36
|
+
const statusKey = getStatusLabelKey(node.status);
|
|
37
|
+
const nodeById = new Map((nodes ?? []).map((item) => [item.id, item]));
|
|
38
|
+
const kind = getNodeKind(node);
|
|
39
|
+
const kindLabel = formatKind?.(kind) ?? kind;
|
|
40
|
+
const parentLabel = (id: string) =>
|
|
41
|
+
nodeById.get(id)?.title || t("trace.node.parentFallback");
|
|
42
|
+
const childNodes = node.childIds
|
|
43
|
+
.map((id) => ({ id, title: nodeById.get(id)?.title }))
|
|
44
|
+
.filter((item) => item.title);
|
|
45
|
+
const metrics = [
|
|
46
|
+
node.durationMs !== undefined
|
|
47
|
+
? { key: "duration", icon: <Timer size={13} />, label: formatDuration(node.durationMs) }
|
|
48
|
+
: null,
|
|
49
|
+
node.toolCalls.length > 0
|
|
50
|
+
? { key: "tools", icon: <Wrench size={13} />, label: t("trace.node.tools", { count: node.toolCalls.length }) }
|
|
51
|
+
: null,
|
|
52
|
+
node.artifacts.length > 0
|
|
53
|
+
? { key: "artifacts", icon: <Box size={13} />, label: t("trace.node.artifacts", { count: node.artifacts.length }) }
|
|
54
|
+
: null,
|
|
55
|
+
].filter((item): item is { key: string; icon: JSX.Element; label: string } => item !== null);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<div className="trace-detail__title">
|
|
60
|
+
<GitBranch size={17} />
|
|
61
|
+
<h3>{node.title}</h3>
|
|
62
|
+
<span className={`trace-detail__status trace-detail__status--${normalizeStatus(node.status)}`}>
|
|
63
|
+
{statusKey ? t(statusKey) : node.status}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="trace-detail__badges">
|
|
67
|
+
<span title={kind}>{kindLabel}</span>
|
|
68
|
+
{node.agent ? <span>{node.agent}</span> : null}
|
|
69
|
+
{node.metadata?.auto ? (
|
|
70
|
+
<span className="trace-detail__badge--auto" title={t("trace.node.autoTitle")}>
|
|
71
|
+
{t("trace.node.auto")}
|
|
72
|
+
</span>
|
|
73
|
+
) : null}
|
|
74
|
+
</div>
|
|
75
|
+
<p>{node.summary || node.description || node.content || "No summary recorded."}</p>
|
|
76
|
+
{node.reason ? (
|
|
77
|
+
<section className="trace-detail__section">
|
|
78
|
+
<h4><ArrowRight size={13} /> Reason</h4>
|
|
79
|
+
<p>{node.reason}</p>
|
|
80
|
+
</section>
|
|
81
|
+
) : null}
|
|
82
|
+
{node.context ? (
|
|
83
|
+
<section className="trace-detail__section">
|
|
84
|
+
<h4><FileText size={13} /> Context</h4>
|
|
85
|
+
<p>{node.context}</p>
|
|
86
|
+
</section>
|
|
87
|
+
) : null}
|
|
88
|
+
{metrics.length > 0 ? (
|
|
89
|
+
<div className="trace-detail__metrics">
|
|
90
|
+
{metrics.map((metric) => (
|
|
91
|
+
<span key={metric.key}>{metric.icon} {metric.label}</span>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
) : null}
|
|
95
|
+
{node.parents.length > 0 ? (
|
|
96
|
+
<section className="trace-detail__section">
|
|
97
|
+
<h4><GitBranch size={13} /> {t("trace.node.dependencies")}</h4>
|
|
98
|
+
<div className="trace-relation-list">
|
|
99
|
+
{node.parents.map((parent) => (
|
|
100
|
+
<button key={parent.id} onClick={() => onSelectNode(parent.id)} title={parent.id} type="button">
|
|
101
|
+
<strong>{parentLabel(parent.id)}</strong>
|
|
102
|
+
<span>{relationLabels[parent.relation || ""] || parent.relation || "parent"}</span>
|
|
103
|
+
{parent.explanation ? <small>{parent.explanation}</small> : null}
|
|
104
|
+
</button>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
</section>
|
|
108
|
+
) : null}
|
|
109
|
+
{node.toolCalls.length > 0 ? (
|
|
110
|
+
<section className="trace-detail__section">
|
|
111
|
+
<h4><Wrench size={13} /> {t("trace.node.toolCalls")}</h4>
|
|
112
|
+
<div className="trace-chip-list">
|
|
113
|
+
{node.toolCalls.map((tool) => <span key={tool} title={tool}>{formatToolName(tool)}</span>)}
|
|
114
|
+
</div>
|
|
115
|
+
</section>
|
|
116
|
+
) : null}
|
|
117
|
+
{node.errorMessage ? (
|
|
118
|
+
<section className="trace-detail__section trace-detail__section--error">
|
|
119
|
+
<h4><AlertTriangle size={13} /> {t("trace.node.error")}</h4>
|
|
120
|
+
<p>{node.errorMessage}</p>
|
|
121
|
+
</section>
|
|
122
|
+
) : null}
|
|
123
|
+
{node.artifacts.length > 0 ? (
|
|
124
|
+
<section className="trace-detail__section">
|
|
125
|
+
<h4><Box size={13} /> {t("trace.node.artifactsTitle")}</h4>
|
|
126
|
+
<div className="trace-artifact-list">
|
|
127
|
+
{node.artifacts.map((artifact) => {
|
|
128
|
+
const label = artifactLabels[artifact.type || ""] || artifact.type || "file";
|
|
129
|
+
const name = artifact.path.split("/").pop() || artifact.path;
|
|
130
|
+
if (onSelectArtifact) {
|
|
131
|
+
return (
|
|
132
|
+
<button
|
|
133
|
+
key={`${artifact.path}-${artifact.type || ""}`}
|
|
134
|
+
type="button"
|
|
135
|
+
className={`trace-artifact-row ${activeArtifactPath === artifact.path ? "is-active" : ""}`}
|
|
136
|
+
title={artifact.path}
|
|
137
|
+
onClick={() => onSelectArtifact(artifact.path)}
|
|
138
|
+
>
|
|
139
|
+
<FileText size={13} />
|
|
140
|
+
<span>{name}</span>
|
|
141
|
+
<small>{label}</small>
|
|
142
|
+
</button>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return (
|
|
146
|
+
<div key={`${artifact.path}-${artifact.type || ""}`} title={artifact.path}>
|
|
147
|
+
<FileText size={13} />
|
|
148
|
+
<span>{name}</span>
|
|
149
|
+
<small>{label}</small>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
153
|
+
</div>
|
|
154
|
+
</section>
|
|
155
|
+
) : null}
|
|
156
|
+
<section className="trace-detail__section">
|
|
157
|
+
<h4><Clock3 size={13} /> {t("trace.node.timeline")}</h4>
|
|
158
|
+
<dl>
|
|
159
|
+
<div>
|
|
160
|
+
<dt>{t("trace.node.created")}</dt>
|
|
161
|
+
<dd>{formatTime(node.timestamp?.createdAt || node.createdAt)}</dd>
|
|
162
|
+
</div>
|
|
163
|
+
{childNodes.length > 0 ? (
|
|
164
|
+
<div>
|
|
165
|
+
<dt>{t("trace.node.children")}</dt>
|
|
166
|
+
<dd className="trace-detail__children">
|
|
167
|
+
{childNodes.map((child) => (
|
|
168
|
+
<button key={child.id} onClick={() => onSelectNode(child.id)} title={child.id} type="button">
|
|
169
|
+
{child.title}
|
|
170
|
+
</button>
|
|
171
|
+
))}
|
|
172
|
+
</dd>
|
|
173
|
+
</div>
|
|
174
|
+
) : null}
|
|
175
|
+
</dl>
|
|
176
|
+
</section>
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
}
|