@townco/debugger 0.1.66 → 0.1.68
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/package.json +3 -3
- package/src/App.tsx +9 -2
- package/src/components/ConversationPanel.tsx +392 -0
- package/src/components/DebuggerHeader.tsx +1 -1
- package/src/components/DiagnosticDetailsPanel.tsx +1095 -0
- package/src/components/DiagnosticPanel.tsx +111 -0
- package/src/components/NavBar.tsx +130 -0
- package/src/components/SpanDetailsPanel.tsx +2 -2
- package/src/components/SpanIcon.tsx +75 -24
- package/src/components/SpanTimeline.tsx +2 -3
- package/src/components/SpansLogsList.tsx +477 -0
- package/src/components/UnifiedTimeline.tsx +1 -2
- package/src/components/ViewControlBar.tsx +79 -0
- package/src/lib/segmentation.ts +391 -0
- package/src/lib/toolRegistry.ts +424 -0
- package/src/pages/SessionLogsView.tsx +585 -0
- package/src/pages/SessionView.tsx +24 -13
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { ConversationPanel } from "../components/ConversationPanel";
|
|
3
|
+
import { DiagnosticDetailsPanel } from "../components/DiagnosticDetailsPanel";
|
|
4
|
+
import {
|
|
5
|
+
DiagnosticPanel,
|
|
6
|
+
type DiagnosticViewMode,
|
|
7
|
+
} from "../components/DiagnosticPanel";
|
|
8
|
+
import { NavBar } from "../components/NavBar";
|
|
9
|
+
import { buildSpanTree } from "../components/SpanTimeline";
|
|
10
|
+
import { segmentByTurn, type TraceData } from "../lib/segmentation";
|
|
11
|
+
import type { ConversationTrace, Log, Span, SpanNode } from "../types";
|
|
12
|
+
|
|
13
|
+
interface SessionLogsViewProps {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build a hierarchical span tree from flat spans (duplicated from SpansLogsList for navigation)
|
|
19
|
+
*/
|
|
20
|
+
function buildSpanTreeLocal(spans: Span[]): SpanNode[] {
|
|
21
|
+
const spanMap = new Map<string, SpanNode>();
|
|
22
|
+
const roots: SpanNode[] = [];
|
|
23
|
+
|
|
24
|
+
// First pass: create SpanNode objects
|
|
25
|
+
for (const span of spans) {
|
|
26
|
+
spanMap.set(span.span_id, {
|
|
27
|
+
...span,
|
|
28
|
+
children: [],
|
|
29
|
+
depth: 0,
|
|
30
|
+
durationMs:
|
|
31
|
+
(span.end_time_unix_nano - span.start_time_unix_nano) / 1_000_000,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Second pass: build the tree
|
|
36
|
+
for (const span of spans) {
|
|
37
|
+
const node = spanMap.get(span.span_id);
|
|
38
|
+
if (!node) continue;
|
|
39
|
+
|
|
40
|
+
if (span.parent_span_id && spanMap.has(span.parent_span_id)) {
|
|
41
|
+
const parent = spanMap.get(span.parent_span_id);
|
|
42
|
+
if (parent) {
|
|
43
|
+
parent.children.push(node);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
roots.push(node);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Third pass: set depths
|
|
51
|
+
const setDepth = (nodes: SpanNode[], depth: number) => {
|
|
52
|
+
for (const node of nodes) {
|
|
53
|
+
node.depth = depth;
|
|
54
|
+
setDepth(node.children, depth + 1);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
setDepth(roots, 0);
|
|
58
|
+
|
|
59
|
+
return roots;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Flatten span tree for navigation (same order as rendered)
|
|
64
|
+
*/
|
|
65
|
+
function flattenSpanTree(nodes: SpanNode[]): SpanNode[] {
|
|
66
|
+
const result: SpanNode[] = [];
|
|
67
|
+
for (const node of nodes) {
|
|
68
|
+
result.push(node);
|
|
69
|
+
if (node.children.length > 0) {
|
|
70
|
+
result.push(...flattenSpanTree(node.children));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function SessionLogsView({ sessionId }: SessionLogsViewProps) {
|
|
77
|
+
// Data state
|
|
78
|
+
const [traces, setTraces] = useState<ConversationTrace[]>([]);
|
|
79
|
+
const [traceDataMap, setTraceDataMap] = useState<Map<string, TraceData>>(
|
|
80
|
+
new Map(),
|
|
81
|
+
);
|
|
82
|
+
const [loading, setLoading] = useState(true);
|
|
83
|
+
const [error, setError] = useState<string | null>(null);
|
|
84
|
+
|
|
85
|
+
// UI state
|
|
86
|
+
const [viewMode, setViewMode] = useState<DiagnosticViewMode>("spans");
|
|
87
|
+
const [highlightedItemId, setHighlightedItemId] = useState<string | null>(
|
|
88
|
+
null,
|
|
89
|
+
);
|
|
90
|
+
const [highlightedSpanId, setHighlightedSpanId] = useState<string | null>(
|
|
91
|
+
null,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Selected span/log for the details panel
|
|
95
|
+
const [selectedSpan, setSelectedSpan] = useState<SpanNode | null>(null);
|
|
96
|
+
const [selectedLog, setSelectedLog] = useState<Log | null>(null);
|
|
97
|
+
|
|
98
|
+
// State for conversation item <-> span linking
|
|
99
|
+
const [selectedConversationItemId, setSelectedConversationItemId] = useState<
|
|
100
|
+
string | null
|
|
101
|
+
>(null);
|
|
102
|
+
const [selectedConversationSpanId, setSelectedConversationSpanId] = useState<
|
|
103
|
+
string | null
|
|
104
|
+
>(null);
|
|
105
|
+
|
|
106
|
+
// Scroll targets - only set when selection comes from ConversationPanel
|
|
107
|
+
const [scrollToSpanId, setScrollToSpanId] = useState<string | null>(null);
|
|
108
|
+
const [scrollToLogId, setScrollToLogId] = useState<number | null>(null);
|
|
109
|
+
|
|
110
|
+
// Refs for scroll sync
|
|
111
|
+
const conversationRef = useRef<HTMLDivElement>(null);
|
|
112
|
+
|
|
113
|
+
// Fetch conversation traces
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const params = new URLSearchParams();
|
|
116
|
+
params.set("sessionId", sessionId);
|
|
117
|
+
fetch(`/api/session-conversation?${params}`)
|
|
118
|
+
.then((res) => {
|
|
119
|
+
if (!res.ok) throw new Error("Failed to fetch conversation");
|
|
120
|
+
return res.json();
|
|
121
|
+
})
|
|
122
|
+
.then((data: ConversationTrace[]) => {
|
|
123
|
+
setTraces(data);
|
|
124
|
+
setLoading(false);
|
|
125
|
+
})
|
|
126
|
+
.catch((err) => {
|
|
127
|
+
setError(err.message);
|
|
128
|
+
setLoading(false);
|
|
129
|
+
});
|
|
130
|
+
}, [sessionId]);
|
|
131
|
+
|
|
132
|
+
// Fetch detailed trace data
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (traces.length === 0) return;
|
|
135
|
+
|
|
136
|
+
const fetchAllTraceData = async () => {
|
|
137
|
+
const dataMap = new Map<string, TraceData>();
|
|
138
|
+
|
|
139
|
+
await Promise.all(
|
|
140
|
+
traces.map(async (trace) => {
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch(`/api/traces/${trace.trace_id}`);
|
|
143
|
+
if (!res.ok) return;
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
const updatedTrace: ConversationTrace = {
|
|
146
|
+
trace_id: trace.trace_id,
|
|
147
|
+
start_time_unix_nano: trace.start_time_unix_nano,
|
|
148
|
+
userInput: data.messages?.userInput ?? trace.userInput,
|
|
149
|
+
llmOutput: data.messages?.llmOutput ?? trace.llmOutput,
|
|
150
|
+
agentMessages:
|
|
151
|
+
data.messages?.agentMessages ?? trace.agentMessages,
|
|
152
|
+
};
|
|
153
|
+
dataMap.set(trace.trace_id, {
|
|
154
|
+
trace: updatedTrace,
|
|
155
|
+
spans: data.spans || [],
|
|
156
|
+
logs: data.logs || [],
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error(`Failed to fetch trace ${trace.trace_id}:`, err);
|
|
160
|
+
}
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
setTraceDataMap(dataMap);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
fetchAllTraceData();
|
|
168
|
+
}, [traces]);
|
|
169
|
+
|
|
170
|
+
// Compute segments using per-turn mode
|
|
171
|
+
const segments = useMemo(() => {
|
|
172
|
+
if (traces.length === 0 || traceDataMap.size === 0) return [];
|
|
173
|
+
return segmentByTurn(traces, traceDataMap);
|
|
174
|
+
}, [traces, traceDataMap]);
|
|
175
|
+
|
|
176
|
+
// Combine all spans for timeline view and span tree
|
|
177
|
+
const allSpans = useMemo(() => {
|
|
178
|
+
const spans: Array<Span & { traceId: string; turnIndex: number }> = [];
|
|
179
|
+
traces.forEach((trace, index) => {
|
|
180
|
+
const data = traceDataMap.get(trace.trace_id);
|
|
181
|
+
if (data) {
|
|
182
|
+
data.spans.forEach((span) => {
|
|
183
|
+
spans.push({
|
|
184
|
+
...span,
|
|
185
|
+
traceId: trace.trace_id,
|
|
186
|
+
turnIndex: index,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return spans;
|
|
192
|
+
}, [traces, traceDataMap]);
|
|
193
|
+
|
|
194
|
+
// All logs combined
|
|
195
|
+
const allLogs = useMemo(() => {
|
|
196
|
+
const logs: Log[] = [];
|
|
197
|
+
for (const segment of segments) {
|
|
198
|
+
logs.push(...segment.logs);
|
|
199
|
+
}
|
|
200
|
+
return logs.sort((a, b) => a.timestamp_unix_nano - b.timestamp_unix_nano);
|
|
201
|
+
}, [segments]);
|
|
202
|
+
|
|
203
|
+
// Build span tree for the details panel
|
|
204
|
+
const spanTree = useMemo(() => {
|
|
205
|
+
return buildSpanTree(allSpans);
|
|
206
|
+
}, [allSpans]);
|
|
207
|
+
|
|
208
|
+
// Flattened span list for navigation (in render order)
|
|
209
|
+
const flattenedSpans = useMemo(() => {
|
|
210
|
+
const result: SpanNode[] = [];
|
|
211
|
+
for (const segment of segments) {
|
|
212
|
+
const segmentTree = buildSpanTreeLocal(segment.spans);
|
|
213
|
+
result.push(...flattenSpanTree(segmentTree));
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}, [segments]);
|
|
217
|
+
|
|
218
|
+
// Compute the set of span IDs to highlight (selected span + all its children)
|
|
219
|
+
const selectedSpanIds = useMemo(() => {
|
|
220
|
+
const ids = new Set<string>();
|
|
221
|
+
if (!selectedConversationSpanId) return ids;
|
|
222
|
+
|
|
223
|
+
// Build a map of parent -> children
|
|
224
|
+
const childrenMap = new Map<string, string[]>();
|
|
225
|
+
for (const span of allSpans) {
|
|
226
|
+
if (span.parent_span_id) {
|
|
227
|
+
const children = childrenMap.get(span.parent_span_id) || [];
|
|
228
|
+
children.push(span.span_id);
|
|
229
|
+
childrenMap.set(span.parent_span_id, children);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Add the target span
|
|
234
|
+
ids.add(selectedConversationSpanId);
|
|
235
|
+
|
|
236
|
+
// Recursively add all children
|
|
237
|
+
const addChildren = (spanId: string) => {
|
|
238
|
+
const children = childrenMap.get(spanId) || [];
|
|
239
|
+
for (const childId of children) {
|
|
240
|
+
ids.add(childId);
|
|
241
|
+
addChildren(childId);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
addChildren(selectedConversationSpanId);
|
|
245
|
+
|
|
246
|
+
return ids;
|
|
247
|
+
}, [allSpans, selectedConversationSpanId]);
|
|
248
|
+
|
|
249
|
+
// Compute the set of log IDs that correspond to the selected conversation item
|
|
250
|
+
const selectedLogIds = useMemo(() => {
|
|
251
|
+
const ids = new Set<number>();
|
|
252
|
+
if (selectedSpanIds.size === 0) return ids;
|
|
253
|
+
|
|
254
|
+
// Find logs that belong to any of the selected spans
|
|
255
|
+
for (const log of allLogs) {
|
|
256
|
+
if (log.span_id && selectedSpanIds.has(log.span_id)) {
|
|
257
|
+
ids.add(log.id);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return ids;
|
|
262
|
+
}, [allLogs, selectedSpanIds]);
|
|
263
|
+
|
|
264
|
+
// Handle conversation item selection
|
|
265
|
+
const handleConversationItemSelect = useCallback(
|
|
266
|
+
(itemId: string | null, spanId: string | null) => {
|
|
267
|
+
if (spanId === null) {
|
|
268
|
+
// Deselecting
|
|
269
|
+
setSelectedConversationItemId(null);
|
|
270
|
+
setSelectedConversationSpanId(null);
|
|
271
|
+
setSelectedSpan(null);
|
|
272
|
+
setSelectedLog(null);
|
|
273
|
+
setScrollToSpanId(null);
|
|
274
|
+
setScrollToLogId(null);
|
|
275
|
+
} else {
|
|
276
|
+
setSelectedConversationItemId(itemId);
|
|
277
|
+
setSelectedConversationSpanId(spanId);
|
|
278
|
+
|
|
279
|
+
// Auto-open the first span in the details panel and scroll to it
|
|
280
|
+
const span = flattenedSpans.find((s) => s.span_id === spanId);
|
|
281
|
+
if (span) {
|
|
282
|
+
setSelectedSpan(span);
|
|
283
|
+
setSelectedLog(null);
|
|
284
|
+
// Only scroll when selection comes from ConversationPanel
|
|
285
|
+
setScrollToSpanId(spanId);
|
|
286
|
+
setScrollToLogId(null);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
[flattenedSpans],
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Handle span click from DiagnosticPanel
|
|
294
|
+
const handleSpanClick = useCallback(
|
|
295
|
+
(span: SpanNode) => {
|
|
296
|
+
setSelectedSpan(span);
|
|
297
|
+
setSelectedLog(null);
|
|
298
|
+
// Clear scroll targets - no auto-scroll for direct clicks
|
|
299
|
+
setScrollToSpanId(null);
|
|
300
|
+
setScrollToLogId(null);
|
|
301
|
+
|
|
302
|
+
// Check if this span is in the highlighted set from conversation selection
|
|
303
|
+
if (selectedSpanIds.size > 0 && !selectedSpanIds.has(span.span_id)) {
|
|
304
|
+
// Clear conversation selection since user selected a different span
|
|
305
|
+
setSelectedConversationItemId(null);
|
|
306
|
+
setSelectedConversationSpanId(null);
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
[selectedSpanIds],
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// Handle log click from DiagnosticPanel
|
|
313
|
+
const handleLogClick = useCallback(
|
|
314
|
+
(log: Log) => {
|
|
315
|
+
setSelectedLog(log);
|
|
316
|
+
setSelectedSpan(null);
|
|
317
|
+
// Clear scroll targets - no auto-scroll for direct clicks
|
|
318
|
+
setScrollToSpanId(null);
|
|
319
|
+
setScrollToLogId(null);
|
|
320
|
+
|
|
321
|
+
// Check if this log is in the highlighted set from conversation selection
|
|
322
|
+
if (selectedLogIds.size > 0 && !selectedLogIds.has(log.id)) {
|
|
323
|
+
// Clear conversation selection since user selected a different log
|
|
324
|
+
setSelectedConversationItemId(null);
|
|
325
|
+
setSelectedConversationSpanId(null);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
[selectedLogIds],
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Handle closing the details panel
|
|
332
|
+
const handleCloseDetails = useCallback(() => {
|
|
333
|
+
setSelectedSpan(null);
|
|
334
|
+
setSelectedLog(null);
|
|
335
|
+
setScrollToSpanId(null);
|
|
336
|
+
setScrollToLogId(null);
|
|
337
|
+
}, []);
|
|
338
|
+
|
|
339
|
+
// Handle view mode change with selection sync
|
|
340
|
+
const handleViewModeChange = useCallback(
|
|
341
|
+
(newMode: DiagnosticViewMode) => {
|
|
342
|
+
setViewMode(newMode);
|
|
343
|
+
|
|
344
|
+
// Sync selection when switching views
|
|
345
|
+
if (newMode === "logs" && selectedSpan) {
|
|
346
|
+
// Switching to logs view - find a corresponding log
|
|
347
|
+
const correspondingLog = allLogs.find(
|
|
348
|
+
(log) =>
|
|
349
|
+
log.span_id === selectedSpan.span_id ||
|
|
350
|
+
(selectedSpanIds.size > 0 &&
|
|
351
|
+
log.span_id &&
|
|
352
|
+
selectedSpanIds.has(log.span_id)),
|
|
353
|
+
);
|
|
354
|
+
if (correspondingLog) {
|
|
355
|
+
setSelectedLog(correspondingLog);
|
|
356
|
+
setSelectedSpan(null);
|
|
357
|
+
// Scroll to the corresponding log in the new view
|
|
358
|
+
setScrollToLogId(correspondingLog.id);
|
|
359
|
+
setScrollToSpanId(null);
|
|
360
|
+
}
|
|
361
|
+
} else if (newMode === "spans" && selectedLog) {
|
|
362
|
+
// Switching to spans view - find a corresponding span
|
|
363
|
+
if (selectedLog.span_id) {
|
|
364
|
+
const correspondingSpan = flattenedSpans.find(
|
|
365
|
+
(s) => s.span_id === selectedLog.span_id,
|
|
366
|
+
);
|
|
367
|
+
if (correspondingSpan) {
|
|
368
|
+
setSelectedSpan(correspondingSpan);
|
|
369
|
+
setSelectedLog(null);
|
|
370
|
+
// Scroll to the corresponding span in the new view
|
|
371
|
+
setScrollToSpanId(correspondingSpan.span_id);
|
|
372
|
+
setScrollToLogId(null);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
[selectedSpan, selectedLog, allLogs, flattenedSpans, selectedSpanIds],
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Handle keyboard navigation
|
|
381
|
+
const handleNavigate = useCallback(
|
|
382
|
+
(direction: "up" | "down") => {
|
|
383
|
+
if (viewMode === "spans") {
|
|
384
|
+
// Navigate spans
|
|
385
|
+
if (!selectedSpan) {
|
|
386
|
+
// No span selected, select first or last
|
|
387
|
+
if (flattenedSpans.length > 0) {
|
|
388
|
+
const span =
|
|
389
|
+
direction === "down"
|
|
390
|
+
? flattenedSpans[0]
|
|
391
|
+
: flattenedSpans[flattenedSpans.length - 1];
|
|
392
|
+
if (span) {
|
|
393
|
+
setSelectedSpan(span);
|
|
394
|
+
setSelectedLog(null);
|
|
395
|
+
// Scroll to the newly selected span
|
|
396
|
+
setScrollToSpanId(span.span_id);
|
|
397
|
+
setScrollToLogId(null);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const currentIndex = flattenedSpans.findIndex(
|
|
404
|
+
(s) => s.span_id === selectedSpan.span_id,
|
|
405
|
+
);
|
|
406
|
+
if (currentIndex === -1) return;
|
|
407
|
+
|
|
408
|
+
const newIndex =
|
|
409
|
+
direction === "down"
|
|
410
|
+
? Math.min(currentIndex + 1, flattenedSpans.length - 1)
|
|
411
|
+
: Math.max(currentIndex - 1, 0);
|
|
412
|
+
|
|
413
|
+
if (newIndex !== currentIndex) {
|
|
414
|
+
const newSpan = flattenedSpans[newIndex];
|
|
415
|
+
if (newSpan) {
|
|
416
|
+
setSelectedSpan(newSpan);
|
|
417
|
+
setSelectedLog(null);
|
|
418
|
+
// Scroll to the newly selected span
|
|
419
|
+
setScrollToSpanId(newSpan.span_id);
|
|
420
|
+
setScrollToLogId(null);
|
|
421
|
+
|
|
422
|
+
// Check if leaving the highlighted set
|
|
423
|
+
if (
|
|
424
|
+
selectedSpanIds.size > 0 &&
|
|
425
|
+
!selectedSpanIds.has(newSpan.span_id)
|
|
426
|
+
) {
|
|
427
|
+
setSelectedConversationItemId(null);
|
|
428
|
+
setSelectedConversationSpanId(null);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
// Navigate logs
|
|
434
|
+
if (!selectedLog) {
|
|
435
|
+
// No log selected, select first or last
|
|
436
|
+
if (allLogs.length > 0) {
|
|
437
|
+
const log =
|
|
438
|
+
direction === "down" ? allLogs[0] : allLogs[allLogs.length - 1];
|
|
439
|
+
if (log) {
|
|
440
|
+
setSelectedLog(log);
|
|
441
|
+
setSelectedSpan(null);
|
|
442
|
+
// Scroll to the newly selected log
|
|
443
|
+
setScrollToLogId(log.id);
|
|
444
|
+
setScrollToSpanId(null);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const currentIndex = allLogs.findIndex((l) => l.id === selectedLog.id);
|
|
451
|
+
if (currentIndex === -1) return;
|
|
452
|
+
|
|
453
|
+
const newIndex =
|
|
454
|
+
direction === "down"
|
|
455
|
+
? Math.min(currentIndex + 1, allLogs.length - 1)
|
|
456
|
+
: Math.max(currentIndex - 1, 0);
|
|
457
|
+
|
|
458
|
+
if (newIndex !== currentIndex) {
|
|
459
|
+
const newLog = allLogs[newIndex];
|
|
460
|
+
if (newLog) {
|
|
461
|
+
setSelectedLog(newLog);
|
|
462
|
+
setSelectedSpan(null);
|
|
463
|
+
// Scroll to the newly selected log
|
|
464
|
+
setScrollToLogId(newLog.id);
|
|
465
|
+
setScrollToSpanId(null);
|
|
466
|
+
|
|
467
|
+
// Check if leaving the highlighted set
|
|
468
|
+
if (selectedLogIds.size > 0 && !selectedLogIds.has(newLog.id)) {
|
|
469
|
+
setSelectedConversationItemId(null);
|
|
470
|
+
setSelectedConversationSpanId(null);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
[
|
|
477
|
+
viewMode,
|
|
478
|
+
selectedSpan,
|
|
479
|
+
selectedLog,
|
|
480
|
+
flattenedSpans,
|
|
481
|
+
allLogs,
|
|
482
|
+
selectedSpanIds,
|
|
483
|
+
selectedLogIds,
|
|
484
|
+
],
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// Handle Enter key to confirm selection (open details panel)
|
|
488
|
+
const handleEnterSelect = useCallback(() => {
|
|
489
|
+
// The current selection is already shown in the details panel
|
|
490
|
+
// This callback is here in case we need additional behavior on Enter
|
|
491
|
+
// Currently, selection already opens the details panel, so this is a no-op
|
|
492
|
+
}, []);
|
|
493
|
+
|
|
494
|
+
// Determine if details panel is open
|
|
495
|
+
const isDetailsPanelOpen = selectedSpan !== null || selectedLog !== null;
|
|
496
|
+
|
|
497
|
+
// Get the selected item ID for highlighting in the list
|
|
498
|
+
const selectedDiagnosticItemId =
|
|
499
|
+
selectedSpan?.span_id ?? (selectedLog ? String(selectedLog.id) : null);
|
|
500
|
+
|
|
501
|
+
if (loading) {
|
|
502
|
+
return (
|
|
503
|
+
<div className="h-screen flex flex-col bg-background">
|
|
504
|
+
<NavBar sessionId={sessionId} />
|
|
505
|
+
<div className="flex-1 flex items-center justify-center">
|
|
506
|
+
<div className="text-muted-foreground">Loading session...</div>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (error) {
|
|
513
|
+
return (
|
|
514
|
+
<div className="h-screen flex flex-col bg-background">
|
|
515
|
+
<NavBar sessionId={sessionId} />
|
|
516
|
+
<div className="flex-1 flex items-center justify-center">
|
|
517
|
+
<div className="text-red-500">Error: {error}</div>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (traces.length === 0) {
|
|
524
|
+
return (
|
|
525
|
+
<div className="h-screen flex flex-col bg-background">
|
|
526
|
+
<NavBar sessionId={sessionId} />
|
|
527
|
+
<div className="flex-1 flex items-center justify-center">
|
|
528
|
+
<div className="text-muted-foreground">No data for this session</div>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<div className="h-screen flex flex-col bg-background">
|
|
536
|
+
<NavBar sessionId={sessionId} />
|
|
537
|
+
|
|
538
|
+
{/* Main content - 2 or 3 panel layout */}
|
|
539
|
+
<div className="flex-1 flex min-h-0">
|
|
540
|
+
{/* Left panel - Conversation */}
|
|
541
|
+
<div ref={conversationRef} className="flex-1 overflow-y-auto">
|
|
542
|
+
<ConversationPanel
|
|
543
|
+
segments={segments}
|
|
544
|
+
highlightedItemId={highlightedItemId}
|
|
545
|
+
onItemHover={setHighlightedItemId}
|
|
546
|
+
selectedItemId={selectedConversationItemId}
|
|
547
|
+
onItemSelect={handleConversationItemSelect}
|
|
548
|
+
/>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
{/* Middle panel - Diagnostic (Spans/Logs) */}
|
|
552
|
+
<div className="flex-1 min-h-0">
|
|
553
|
+
<DiagnosticPanel
|
|
554
|
+
segments={segments}
|
|
555
|
+
viewMode={viewMode}
|
|
556
|
+
onViewModeChange={handleViewModeChange}
|
|
557
|
+
highlightedSpanId={highlightedSpanId}
|
|
558
|
+
selectedSpanIds={selectedSpanIds}
|
|
559
|
+
selectedLogIds={selectedLogIds}
|
|
560
|
+
onSpanHover={setHighlightedSpanId}
|
|
561
|
+
onSpanClick={handleSpanClick}
|
|
562
|
+
onLogClick={handleLogClick}
|
|
563
|
+
scrollToSpanId={scrollToSpanId}
|
|
564
|
+
scrollToLogId={scrollToLogId}
|
|
565
|
+
selectedItemId={selectedDiagnosticItemId}
|
|
566
|
+
onNavigate={handleNavigate}
|
|
567
|
+
onEnterSelect={handleEnterSelect}
|
|
568
|
+
/>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
{/* Right panel - Details (conditional) */}
|
|
572
|
+
{isDetailsPanelOpen && (
|
|
573
|
+
<div className="flex-1 min-h-0">
|
|
574
|
+
<DiagnosticDetailsPanel
|
|
575
|
+
span={selectedSpan}
|
|
576
|
+
log={selectedLog}
|
|
577
|
+
onClose={handleCloseDetails}
|
|
578
|
+
allSpans={spanTree}
|
|
579
|
+
/>
|
|
580
|
+
</div>
|
|
581
|
+
)}
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { List } from "lucide-react";
|
|
1
2
|
import { useEffect, useState } from "react";
|
|
2
3
|
import type { SessionAnalysis } from "../analysis/types";
|
|
3
|
-
import {
|
|
4
|
+
import { NavBar } from "../components/NavBar";
|
|
4
5
|
import { SessionAnalysisButton } from "../components/SessionAnalysisButton";
|
|
5
6
|
import { SessionTimelineView } from "../components/SessionTimelineView";
|
|
7
|
+
import { Button } from "../components/ui/button";
|
|
6
8
|
|
|
7
9
|
interface SessionViewProps {
|
|
8
10
|
sessionId: string;
|
|
@@ -30,19 +32,28 @@ export function SessionView({ sessionId }: SessionViewProps) {
|
|
|
30
32
|
}, [sessionId]);
|
|
31
33
|
|
|
32
34
|
return (
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
<div className="h-screen flex flex-col">
|
|
36
|
+
<NavBar sessionId={sessionId} />
|
|
37
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
38
|
+
<div className="relative flex-1 flex flex-col overflow-hidden">
|
|
39
|
+
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
|
40
|
+
<Button variant="outline" size="sm" asChild>
|
|
41
|
+
<a href={`/sessions/${sessionId}/logs`}>
|
|
42
|
+
<List className="size-4 mr-2" />
|
|
43
|
+
Logs View
|
|
44
|
+
</a>
|
|
45
|
+
</Button>
|
|
46
|
+
{!loadingAnalysis && (
|
|
47
|
+
<SessionAnalysisButton
|
|
48
|
+
sessionId={sessionId}
|
|
49
|
+
existingAnalysis={existingAnalysis}
|
|
50
|
+
onAnalysisComplete={(analysis) => setExistingAnalysis(analysis)}
|
|
51
|
+
/>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
<SessionTimelineView sessionId={sessionId} />
|
|
43
55
|
</div>
|
|
44
|
-
<SessionTimelineView sessionId={sessionId} />
|
|
45
56
|
</div>
|
|
46
|
-
</
|
|
57
|
+
</div>
|
|
47
58
|
);
|
|
48
59
|
}
|