@townco/debugger 0.1.22 → 0.1.24
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 +10 -8
- package/src/App.tsx +13 -0
- package/src/comparison-db.test.ts +113 -0
- package/src/comparison-db.ts +332 -0
- package/src/components/DebuggerHeader.tsx +62 -2
- package/src/components/SessionTimelineView.tsx +173 -0
- package/src/components/SpanTimeline.tsx +6 -4
- package/src/components/UnifiedTimeline.tsx +691 -0
- package/src/db.ts +71 -0
- package/src/index.ts +2 -0
- package/src/lib/metrics.test.ts +51 -0
- package/src/lib/metrics.ts +136 -0
- package/src/lib/pricing.ts +23 -0
- package/src/lib/turnExtractor.ts +64 -23
- package/src/pages/ComparisonView.tsx +685 -0
- package/src/pages/SessionList.tsx +77 -56
- package/src/pages/SessionView.tsx +3 -64
- package/src/pages/TownHall.tsx +406 -0
- package/src/schemas.ts +15 -0
- package/src/server.ts +345 -13
- package/src/types.ts +87 -0
- package/tsconfig.json +14 -0
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
import { Button } from "@/components/ui/button";
|
|
3
|
-
import {
|
|
4
|
-
Card,
|
|
5
|
-
CardContent,
|
|
6
|
-
CardDescription,
|
|
7
|
-
CardHeader,
|
|
8
|
-
CardTitle,
|
|
9
|
-
} from "@/components/ui/card";
|
|
10
3
|
import { DebuggerLayout } from "../components/DebuggerLayout";
|
|
11
4
|
import type { Session } from "../types";
|
|
12
5
|
|
|
13
6
|
function formatDuration(startNano: number, endNano: number): string {
|
|
14
7
|
const ms = (endNano - startNano) / 1_000_000;
|
|
15
|
-
if (ms < 1000) return `${ms.toFixed(
|
|
16
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(
|
|
17
|
-
return `${(ms / 60000).toFixed(
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function formatTimestamp(nanoseconds: number): string {
|
|
21
|
-
return new Date(nanoseconds / 1_000_000).toLocaleString();
|
|
8
|
+
if (ms < 1000) return `${ms.toFixed(0)}ms`;
|
|
9
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
10
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
22
11
|
}
|
|
23
12
|
|
|
24
13
|
function formatRelativeTime(nanoseconds: number): string {
|
|
@@ -35,21 +24,59 @@ function formatRelativeTime(nanoseconds: number): string {
|
|
|
35
24
|
return "just now";
|
|
36
25
|
}
|
|
37
26
|
|
|
27
|
+
interface SessionWithMessage extends Session {
|
|
28
|
+
firstMessage?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
38
31
|
export function SessionList() {
|
|
39
|
-
const [sessions, setSessions] = useState<
|
|
32
|
+
const [sessions, setSessions] = useState<SessionWithMessage[]>([]);
|
|
33
|
+
const [comparisonSessionIds, setComparisonSessionIds] = useState<Set<string>>(
|
|
34
|
+
new Set(),
|
|
35
|
+
);
|
|
40
36
|
const [loading, setLoading] = useState(true);
|
|
41
37
|
const [error, setError] = useState<string | null>(null);
|
|
42
38
|
|
|
43
|
-
// Fetch sessions
|
|
39
|
+
// Fetch sessions, comparison session IDs, and first messages
|
|
44
40
|
const fetchSessions = useCallback(() => {
|
|
45
41
|
setLoading(true);
|
|
46
|
-
|
|
47
|
-
.then((res) => {
|
|
42
|
+
Promise.all([
|
|
43
|
+
fetch("/api/sessions").then((res) => {
|
|
48
44
|
if (!res.ok) throw new Error("Failed to fetch sessions");
|
|
49
45
|
return res.json();
|
|
50
|
-
})
|
|
51
|
-
.then((
|
|
52
|
-
|
|
46
|
+
}),
|
|
47
|
+
fetch("/api/comparison-session-ids").then((res) => {
|
|
48
|
+
if (!res.ok) return { sessionIds: [] };
|
|
49
|
+
return res.json();
|
|
50
|
+
}),
|
|
51
|
+
])
|
|
52
|
+
.then(async ([sessionsData, comparisonData]) => {
|
|
53
|
+
const comparisonIds = new Set<string>(comparisonData.sessionIds || []);
|
|
54
|
+
setComparisonSessionIds(comparisonIds);
|
|
55
|
+
|
|
56
|
+
// Filter sessions first, then fetch first messages only for filtered sessions
|
|
57
|
+
const filtered = (sessionsData as Session[]).filter(
|
|
58
|
+
(s) => !comparisonIds.has(s.session_id),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Fetch first messages for all filtered sessions in parallel
|
|
62
|
+
const sessionsWithMessages = await Promise.all(
|
|
63
|
+
filtered.map(async (session) => {
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(
|
|
66
|
+
`/api/session-first-message/${session.session_id}`,
|
|
67
|
+
);
|
|
68
|
+
if (res.ok) {
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
return { ...session, firstMessage: data.message };
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Ignore errors fetching first message
|
|
74
|
+
}
|
|
75
|
+
return session;
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
setSessions(sessionsWithMessages);
|
|
53
80
|
setLoading(false);
|
|
54
81
|
})
|
|
55
82
|
.catch((err) => {
|
|
@@ -63,6 +90,11 @@ export function SessionList() {
|
|
|
63
90
|
fetchSessions();
|
|
64
91
|
}, [fetchSessions]);
|
|
65
92
|
|
|
93
|
+
// Filter out comparison sessions (already filtered during fetch, but keep for safety)
|
|
94
|
+
const filteredSessions = sessions.filter(
|
|
95
|
+
(session) => !comparisonSessionIds.has(session.session_id),
|
|
96
|
+
);
|
|
97
|
+
|
|
66
98
|
if (loading) {
|
|
67
99
|
return (
|
|
68
100
|
<DebuggerLayout title="Sessions" showNav>
|
|
@@ -85,53 +117,42 @@ export function SessionList() {
|
|
|
85
117
|
|
|
86
118
|
return (
|
|
87
119
|
<DebuggerLayout title="Sessions" showNav>
|
|
88
|
-
<div className="
|
|
89
|
-
<div className="flex gap-2 mb-4">
|
|
120
|
+
<div className="h-[calc(100vh-4rem)] flex flex-col p-8">
|
|
121
|
+
<div className="flex gap-2 mb-4 shrink-0">
|
|
90
122
|
<Button variant="outline" onClick={fetchSessions} disabled={loading}>
|
|
91
123
|
Refresh
|
|
92
124
|
</Button>
|
|
93
125
|
</div>
|
|
94
126
|
|
|
95
|
-
{
|
|
127
|
+
{filteredSessions.length === 0 ? (
|
|
96
128
|
<div className="text-muted-foreground">No sessions found</div>
|
|
97
129
|
) : (
|
|
98
|
-
<div className="space-y-
|
|
99
|
-
{
|
|
130
|
+
<div className="space-y-1 overflow-y-auto flex-1">
|
|
131
|
+
{filteredSessions.map((session) => (
|
|
100
132
|
<a
|
|
101
133
|
key={session.session_id}
|
|
102
134
|
href={`/sessions/${session.session_id}`}
|
|
103
135
|
className="block"
|
|
104
136
|
>
|
|
105
|
-
<
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
</CardDescription>
|
|
125
|
-
</CardHeader>
|
|
126
|
-
<CardContent className="py-2 pt-0">
|
|
127
|
-
<div className="text-xs text-muted-foreground">
|
|
128
|
-
Started: {formatTimestamp(session.first_trace_time)}
|
|
129
|
-
</div>
|
|
130
|
-
<div className="text-xs text-muted-foreground">
|
|
131
|
-
Last activity: {formatTimestamp(session.last_trace_time)}
|
|
132
|
-
</div>
|
|
133
|
-
</CardContent>
|
|
134
|
-
</Card>
|
|
137
|
+
<div className="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-muted/50 transition-colors cursor-pointer border">
|
|
138
|
+
<code className="text-xs text-muted-foreground shrink-0">
|
|
139
|
+
{session.session_id.slice(0, 8)}
|
|
140
|
+
</code>
|
|
141
|
+
<span className="text-sm truncate flex-1">
|
|
142
|
+
{session.firstMessage || "No message"}
|
|
143
|
+
</span>
|
|
144
|
+
<span className="text-xs text-muted-foreground shrink-0">
|
|
145
|
+
{session.trace_count}{" "}
|
|
146
|
+
{session.trace_count === 1 ? "trace" : "traces"} ·{" "}
|
|
147
|
+
{formatDuration(
|
|
148
|
+
session.first_trace_time,
|
|
149
|
+
session.last_trace_time,
|
|
150
|
+
)}
|
|
151
|
+
</span>
|
|
152
|
+
<span className="text-xs text-muted-foreground shrink-0">
|
|
153
|
+
{formatRelativeTime(session.last_trace_time)}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
135
156
|
</a>
|
|
136
157
|
))}
|
|
137
158
|
</div>
|
|
@@ -1,76 +1,15 @@
|
|
|
1
|
-
import { useCallback, useState } from "react";
|
|
2
1
|
import { DebuggerLayout } from "../components/DebuggerLayout";
|
|
3
|
-
import {
|
|
4
|
-
import { TraceDetailContent } from "../components/TraceDetailContent";
|
|
5
|
-
|
|
6
|
-
function getInitialTraceId(): string | null {
|
|
7
|
-
const params = new URLSearchParams(window.location.search);
|
|
8
|
-
return params.get("traceId");
|
|
9
|
-
}
|
|
2
|
+
import { SessionTimelineView } from "../components/SessionTimelineView";
|
|
10
3
|
|
|
11
4
|
interface SessionViewProps {
|
|
12
5
|
sessionId: string;
|
|
13
6
|
}
|
|
14
7
|
|
|
15
8
|
export function SessionView({ sessionId }: SessionViewProps) {
|
|
16
|
-
const [selectedTraceId, setSelectedTraceId] = useState<string | null>(
|
|
17
|
-
getInitialTraceId,
|
|
18
|
-
);
|
|
19
|
-
const [autoSelectDone, setAutoSelectDone] = useState(
|
|
20
|
-
() => getInitialTraceId() !== null,
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const handleSelectTrace = (traceId: string) => {
|
|
24
|
-
setSelectedTraceId(traceId);
|
|
25
|
-
|
|
26
|
-
// Update URL without navigation
|
|
27
|
-
const url = new URL(window.location.href);
|
|
28
|
-
url.searchParams.set("traceId", traceId);
|
|
29
|
-
window.history.replaceState({}, "", url);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const handleTracesLoaded = useCallback(
|
|
33
|
-
(traces: { trace_id: string }[]) => {
|
|
34
|
-
// Auto-select most recent trace if none selected yet
|
|
35
|
-
// Most recent is now last in array due to ASC ordering
|
|
36
|
-
const mostRecent = traces[traces.length - 1];
|
|
37
|
-
if (!autoSelectDone && mostRecent) {
|
|
38
|
-
setSelectedTraceId(mostRecent.trace_id);
|
|
39
|
-
setAutoSelectDone(true);
|
|
40
|
-
|
|
41
|
-
// Update URL
|
|
42
|
-
const url = new URL(window.location.href);
|
|
43
|
-
url.searchParams.set("traceId", mostRecent.trace_id);
|
|
44
|
-
window.history.replaceState({}, "", url);
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
[autoSelectDone],
|
|
48
|
-
);
|
|
49
|
-
|
|
50
9
|
return (
|
|
51
10
|
<DebuggerLayout title={`Session: ${sessionId}`} showBackButton backHref="/">
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{/* Left pane - trace list */}
|
|
55
|
-
<div className="w-[480px] border-r overflow-y-auto shrink-0">
|
|
56
|
-
<SessionTraceList
|
|
57
|
-
sessionId={sessionId}
|
|
58
|
-
selectedTraceId={selectedTraceId}
|
|
59
|
-
onSelectTrace={handleSelectTrace}
|
|
60
|
-
onTracesLoaded={handleTracesLoaded}
|
|
61
|
-
/>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
{/* Right pane - trace detail */}
|
|
65
|
-
<div className="flex-1 overflow-y-auto">
|
|
66
|
-
{selectedTraceId ? (
|
|
67
|
-
<TraceDetailContent traceId={selectedTraceId} compact />
|
|
68
|
-
) : (
|
|
69
|
-
<div className="p-8 text-muted-foreground">
|
|
70
|
-
Select a trace to view details
|
|
71
|
-
</div>
|
|
72
|
-
)}
|
|
73
|
-
</div>
|
|
11
|
+
<div className="flex-1 overflow-y-auto">
|
|
12
|
+
<SessionTimelineView sessionId={sessionId} />
|
|
74
13
|
</div>
|
|
75
14
|
</DebuggerLayout>
|
|
76
15
|
);
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from "@/components/ui/card";
|
|
10
|
+
import { Label } from "@/components/ui/label";
|
|
11
|
+
import {
|
|
12
|
+
Select,
|
|
13
|
+
SelectContent,
|
|
14
|
+
SelectItem,
|
|
15
|
+
SelectTrigger,
|
|
16
|
+
SelectValue,
|
|
17
|
+
} from "@/components/ui/select";
|
|
18
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
19
|
+
import { DebuggerLayout } from "../components/DebuggerLayout";
|
|
20
|
+
import type {
|
|
21
|
+
AgentConfig,
|
|
22
|
+
ComparisonConfig,
|
|
23
|
+
ComparisonDimension,
|
|
24
|
+
Session,
|
|
25
|
+
} from "../types";
|
|
26
|
+
|
|
27
|
+
function formatDuration(startNano: number, endNano: number): string {
|
|
28
|
+
const ms = (endNano - startNano) / 1_000_000;
|
|
29
|
+
if (ms < 1000) return `${ms.toFixed(2)}ms`;
|
|
30
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
|
|
31
|
+
return `${(ms / 60000).toFixed(2)}m`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatRelativeTime(nanoseconds: number): string {
|
|
35
|
+
const ms = Date.now() - nanoseconds / 1_000_000;
|
|
36
|
+
const seconds = Math.floor(ms / 1000);
|
|
37
|
+
const minutes = Math.floor(seconds / 60);
|
|
38
|
+
const hours = Math.floor(minutes / 60);
|
|
39
|
+
const days = Math.floor(hours / 24);
|
|
40
|
+
|
|
41
|
+
if (days > 0) return `${days}d ago`;
|
|
42
|
+
if (hours > 0) return `${hours}h ago`;
|
|
43
|
+
if (minutes > 0) return `${minutes}m ago`;
|
|
44
|
+
if (seconds > 5) return `${seconds}s ago`;
|
|
45
|
+
return "just now";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function TownHall() {
|
|
49
|
+
const [sessions, setSessions] = useState<Session[]>([]);
|
|
50
|
+
const [loading, setLoading] = useState(true);
|
|
51
|
+
const [error, setError] = useState<string | null>(null);
|
|
52
|
+
|
|
53
|
+
// Comparison config state
|
|
54
|
+
const [dimension, setDimension] = useState<ComparisonDimension | null>(null);
|
|
55
|
+
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
|
56
|
+
const [agentConfig, setAgentConfig] = useState<AgentConfig | null>(null);
|
|
57
|
+
|
|
58
|
+
// Variant values
|
|
59
|
+
const [variantModel, setVariantModel] = useState<string | null>(null);
|
|
60
|
+
const [variantSystemPrompt, setVariantSystemPrompt] = useState<string>("");
|
|
61
|
+
const [variantTools, setVariantTools] = useState<string[]>([]);
|
|
62
|
+
|
|
63
|
+
// Running comparison state
|
|
64
|
+
const [runningComparison, setRunningComparison] = useState<string | null>(
|
|
65
|
+
null,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Fetch sessions
|
|
69
|
+
const fetchSessions = useCallback(() => {
|
|
70
|
+
setLoading(true);
|
|
71
|
+
fetch("/api/sessions")
|
|
72
|
+
.then((res) => {
|
|
73
|
+
if (!res.ok) throw new Error("Failed to fetch sessions");
|
|
74
|
+
return res.json();
|
|
75
|
+
})
|
|
76
|
+
.then((data) => {
|
|
77
|
+
setSessions(data);
|
|
78
|
+
setLoading(false);
|
|
79
|
+
})
|
|
80
|
+
.catch((err) => {
|
|
81
|
+
setError(err.message);
|
|
82
|
+
setLoading(false);
|
|
83
|
+
});
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
// Fetch available models and agent config
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
fetch("/api/available-models")
|
|
89
|
+
.then((res) => res.json())
|
|
90
|
+
.then((data) => setAvailableModels(data.models || []))
|
|
91
|
+
.catch(console.error);
|
|
92
|
+
|
|
93
|
+
fetch("/api/agent-config")
|
|
94
|
+
.then((res) => res.json())
|
|
95
|
+
.then((data) => {
|
|
96
|
+
setAgentConfig(data);
|
|
97
|
+
// Initialize variant system prompt with current
|
|
98
|
+
if (data.systemPrompt) {
|
|
99
|
+
setVariantSystemPrompt(data.systemPrompt);
|
|
100
|
+
}
|
|
101
|
+
// Initialize variant tools with current
|
|
102
|
+
if (data.tools) {
|
|
103
|
+
setVariantTools(data.tools.map((t: { name: string }) => t.name));
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
.catch(console.error);
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
// Fetch sessions on mount
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
fetchSessions();
|
|
112
|
+
}, [fetchSessions]);
|
|
113
|
+
|
|
114
|
+
// Check if comparison is ready
|
|
115
|
+
const isComparisonReady = () => {
|
|
116
|
+
if (!dimension) return false;
|
|
117
|
+
|
|
118
|
+
switch (dimension) {
|
|
119
|
+
case "model":
|
|
120
|
+
return !!variantModel && variantModel !== agentConfig?.model;
|
|
121
|
+
case "system_prompt":
|
|
122
|
+
return (
|
|
123
|
+
variantSystemPrompt !== "" &&
|
|
124
|
+
variantSystemPrompt !== agentConfig?.systemPrompt
|
|
125
|
+
);
|
|
126
|
+
case "tools":
|
|
127
|
+
const currentToolNames =
|
|
128
|
+
agentConfig?.tools?.map((t) => t.name).sort() || [];
|
|
129
|
+
const variantToolNames = [...variantTools].sort();
|
|
130
|
+
return (
|
|
131
|
+
JSON.stringify(currentToolNames) !== JSON.stringify(variantToolNames)
|
|
132
|
+
);
|
|
133
|
+
default:
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Start comparison
|
|
139
|
+
const startComparison = async (sessionId: string) => {
|
|
140
|
+
if (!dimension || !isComparisonReady()) return;
|
|
141
|
+
|
|
142
|
+
setRunningComparison(sessionId);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Create comparison config
|
|
146
|
+
const config: Partial<ComparisonConfig> = {
|
|
147
|
+
dimension,
|
|
148
|
+
...(dimension === "model" && { controlModel: agentConfig?.model }),
|
|
149
|
+
...(dimension === "model" && variantModel && { variantModel }),
|
|
150
|
+
...(dimension === "system_prompt" &&
|
|
151
|
+
variantSystemPrompt && {
|
|
152
|
+
variantSystemPrompt,
|
|
153
|
+
}),
|
|
154
|
+
...(dimension === "tools" &&
|
|
155
|
+
variantTools.length > 0 && { variantTools }),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Save config
|
|
159
|
+
const configRes = await fetch("/api/comparison-config", {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: { "Content-Type": "application/json" },
|
|
162
|
+
body: JSON.stringify(config),
|
|
163
|
+
});
|
|
164
|
+
const { id: configId } = await configRes.json();
|
|
165
|
+
|
|
166
|
+
// Start comparison run
|
|
167
|
+
const runRes = await fetch("/api/run-comparison", {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { "Content-Type": "application/json" },
|
|
170
|
+
body: JSON.stringify({ sessionId, configId }),
|
|
171
|
+
});
|
|
172
|
+
const { runId } = await runRes.json();
|
|
173
|
+
|
|
174
|
+
// Navigate to comparison view
|
|
175
|
+
window.location.href = `/town-hall/compare/${runId}`;
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error("Failed to start comparison:", err);
|
|
178
|
+
setRunningComparison(null);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (loading) {
|
|
183
|
+
return (
|
|
184
|
+
<DebuggerLayout title="Town Hall" showNav>
|
|
185
|
+
<div className="container mx-auto p-8">
|
|
186
|
+
<div className="text-muted-foreground">Loading sessions...</div>
|
|
187
|
+
</div>
|
|
188
|
+
</DebuggerLayout>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (error) {
|
|
193
|
+
return (
|
|
194
|
+
<DebuggerLayout title="Town Hall" showNav>
|
|
195
|
+
<div className="container mx-auto p-8">
|
|
196
|
+
<div className="text-red-500">Error: {error}</div>
|
|
197
|
+
</div>
|
|
198
|
+
</DebuggerLayout>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<DebuggerLayout title="Town Hall" showNav>
|
|
204
|
+
<div className="container mx-auto p-8">
|
|
205
|
+
{/* Comparison Config Bar */}
|
|
206
|
+
<Card className="mb-6">
|
|
207
|
+
<CardHeader className="py-4">
|
|
208
|
+
<CardTitle className="text-lg">Comparison Configuration</CardTitle>
|
|
209
|
+
<CardDescription>
|
|
210
|
+
Select what dimension to compare and configure the variant
|
|
211
|
+
</CardDescription>
|
|
212
|
+
</CardHeader>
|
|
213
|
+
<CardContent className="space-y-4">
|
|
214
|
+
{/* Dimension selector */}
|
|
215
|
+
<div className="space-y-2">
|
|
216
|
+
<Label>Compare by</Label>
|
|
217
|
+
<Select
|
|
218
|
+
value={dimension || ""}
|
|
219
|
+
onValueChange={(v) =>
|
|
220
|
+
setDimension(v as ComparisonDimension | null)
|
|
221
|
+
}
|
|
222
|
+
>
|
|
223
|
+
<SelectTrigger className="w-[200px]">
|
|
224
|
+
<SelectValue placeholder="Select dimension" />
|
|
225
|
+
</SelectTrigger>
|
|
226
|
+
<SelectContent>
|
|
227
|
+
<SelectItem value="model">Model</SelectItem>
|
|
228
|
+
<SelectItem value="system_prompt">System Prompt</SelectItem>
|
|
229
|
+
<SelectItem value="tools">Tools</SelectItem>
|
|
230
|
+
</SelectContent>
|
|
231
|
+
</Select>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{/* Variant configuration */}
|
|
235
|
+
{dimension === "model" && (
|
|
236
|
+
<div className="space-y-2">
|
|
237
|
+
<Label>
|
|
238
|
+
Variant Model{" "}
|
|
239
|
+
<span className="text-muted-foreground text-xs">
|
|
240
|
+
(Control: {agentConfig?.model || "unknown"})
|
|
241
|
+
</span>
|
|
242
|
+
</Label>
|
|
243
|
+
<Select
|
|
244
|
+
value={variantModel || ""}
|
|
245
|
+
onValueChange={setVariantModel}
|
|
246
|
+
>
|
|
247
|
+
<SelectTrigger className="w-[300px]">
|
|
248
|
+
<SelectValue placeholder="Select variant model" />
|
|
249
|
+
</SelectTrigger>
|
|
250
|
+
<SelectContent>
|
|
251
|
+
{availableModels.map((model) => (
|
|
252
|
+
<SelectItem
|
|
253
|
+
key={model}
|
|
254
|
+
value={model}
|
|
255
|
+
disabled={model === agentConfig?.model}
|
|
256
|
+
>
|
|
257
|
+
{model}
|
|
258
|
+
{model === agentConfig?.model && " (current)"}
|
|
259
|
+
</SelectItem>
|
|
260
|
+
))}
|
|
261
|
+
</SelectContent>
|
|
262
|
+
</Select>
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{dimension === "system_prompt" && (
|
|
267
|
+
<div className="space-y-2">
|
|
268
|
+
<Label>
|
|
269
|
+
Variant System Prompt{" "}
|
|
270
|
+
<span className="text-muted-foreground text-xs">
|
|
271
|
+
(Edit to create variant)
|
|
272
|
+
</span>
|
|
273
|
+
</Label>
|
|
274
|
+
<Textarea
|
|
275
|
+
value={variantSystemPrompt}
|
|
276
|
+
onChange={(e) => setVariantSystemPrompt(e.target.value)}
|
|
277
|
+
className="min-h-[150px] font-mono text-sm"
|
|
278
|
+
placeholder="Enter variant system prompt..."
|
|
279
|
+
/>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{dimension === "tools" && agentConfig?.tools && (
|
|
284
|
+
<div className="space-y-2">
|
|
285
|
+
<Label>
|
|
286
|
+
Variant Tools{" "}
|
|
287
|
+
<span className="text-muted-foreground text-xs">
|
|
288
|
+
(Select tools to enable)
|
|
289
|
+
</span>
|
|
290
|
+
</Label>
|
|
291
|
+
<div className="flex flex-wrap gap-2">
|
|
292
|
+
{agentConfig.tools.map((tool) => (
|
|
293
|
+
<Button
|
|
294
|
+
key={tool.name}
|
|
295
|
+
variant={
|
|
296
|
+
variantTools.includes(tool.name) ? "default" : "outline"
|
|
297
|
+
}
|
|
298
|
+
size="sm"
|
|
299
|
+
onClick={() => {
|
|
300
|
+
if (variantTools.includes(tool.name)) {
|
|
301
|
+
setVariantTools(
|
|
302
|
+
variantTools.filter((t) => t !== tool.name),
|
|
303
|
+
);
|
|
304
|
+
} else {
|
|
305
|
+
setVariantTools([...variantTools, tool.name]);
|
|
306
|
+
}
|
|
307
|
+
}}
|
|
308
|
+
>
|
|
309
|
+
{tool.name}
|
|
310
|
+
</Button>
|
|
311
|
+
))}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{/* Status indicator */}
|
|
317
|
+
{dimension && (
|
|
318
|
+
<div className="text-sm">
|
|
319
|
+
{isComparisonReady() ? (
|
|
320
|
+
<span className="text-green-600 dark:text-green-400">
|
|
321
|
+
Ready to compare. Click "Compare" on a session below.
|
|
322
|
+
</span>
|
|
323
|
+
) : (
|
|
324
|
+
<span className="text-yellow-600 dark:text-yellow-400">
|
|
325
|
+
Configure a different variant to enable comparison.
|
|
326
|
+
</span>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
</CardContent>
|
|
331
|
+
</Card>
|
|
332
|
+
|
|
333
|
+
{/* Session list */}
|
|
334
|
+
<div className="flex gap-2 mb-4">
|
|
335
|
+
<Button variant="outline" onClick={fetchSessions} disabled={loading}>
|
|
336
|
+
Refresh
|
|
337
|
+
</Button>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
{sessions.length === 0 ? (
|
|
341
|
+
<div className="text-muted-foreground">No sessions found</div>
|
|
342
|
+
) : (
|
|
343
|
+
<div className="space-y-2">
|
|
344
|
+
{sessions.map((session) => (
|
|
345
|
+
<Card
|
|
346
|
+
key={session.session_id}
|
|
347
|
+
className="hover:bg-muted/50 transition-colors"
|
|
348
|
+
>
|
|
349
|
+
<CardHeader className="py-3">
|
|
350
|
+
<div className="flex items-center justify-between">
|
|
351
|
+
<div className="flex-1">
|
|
352
|
+
<CardTitle className="text-base font-medium font-mono">
|
|
353
|
+
{session.session_id}
|
|
354
|
+
</CardTitle>
|
|
355
|
+
<CardDescription className="flex items-center gap-4 mt-1">
|
|
356
|
+
<span>{session.trace_count} traces</span>
|
|
357
|
+
<span>
|
|
358
|
+
Duration:{" "}
|
|
359
|
+
{formatDuration(
|
|
360
|
+
session.first_trace_time,
|
|
361
|
+
session.last_trace_time,
|
|
362
|
+
)}
|
|
363
|
+
</span>
|
|
364
|
+
<span>
|
|
365
|
+
{formatRelativeTime(session.last_trace_time)}
|
|
366
|
+
</span>
|
|
367
|
+
</CardDescription>
|
|
368
|
+
</div>
|
|
369
|
+
<div className="flex items-center gap-2">
|
|
370
|
+
<Button
|
|
371
|
+
variant="outline"
|
|
372
|
+
size="sm"
|
|
373
|
+
onClick={() =>
|
|
374
|
+
(window.location.href = `/sessions/${session.session_id}`)
|
|
375
|
+
}
|
|
376
|
+
>
|
|
377
|
+
View
|
|
378
|
+
</Button>
|
|
379
|
+
<Button
|
|
380
|
+
size="sm"
|
|
381
|
+
disabled={
|
|
382
|
+
!isComparisonReady() ||
|
|
383
|
+
runningComparison === session.session_id
|
|
384
|
+
}
|
|
385
|
+
onClick={() => startComparison(session.session_id)}
|
|
386
|
+
title={
|
|
387
|
+
!isComparisonReady()
|
|
388
|
+
? "Configure comparison first"
|
|
389
|
+
: "Start comparison"
|
|
390
|
+
}
|
|
391
|
+
>
|
|
392
|
+
{runningComparison === session.session_id
|
|
393
|
+
? "Starting..."
|
|
394
|
+
: "Compare"}
|
|
395
|
+
</Button>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</CardHeader>
|
|
399
|
+
</Card>
|
|
400
|
+
))}
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
</DebuggerLayout>
|
|
405
|
+
);
|
|
406
|
+
}
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const VariantToolsSchema = z.array(z.string());
|
|
4
|
+
|
|
5
|
+
export const SessionMetricsSchema = z.object({
|
|
6
|
+
durationMs: z.number(),
|
|
7
|
+
inputTokens: z.number(),
|
|
8
|
+
outputTokens: z.number(),
|
|
9
|
+
totalTokens: z.number(),
|
|
10
|
+
estimatedCost: z.number(),
|
|
11
|
+
toolCallCount: z.number(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type VariantTools = z.infer<typeof VariantToolsSchema>;
|
|
15
|
+
export type SessionMetrics = z.infer<typeof SessionMetricsSchema>;
|