@townco/debugger 0.1.27 → 0.1.29

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.
@@ -0,0 +1,238 @@
1
+ import { Database } from "bun:sqlite";
2
+ import type { Connection, Table } from "@lancedb/lancedb";
3
+ import { connect } from "@lancedb/lancedb";
4
+ import type { SessionAnalysis } from "./analysis/types";
5
+
6
+ export class AnalysisDb {
7
+ private db: Database;
8
+ private lanceDb: Connection | null = null;
9
+ private embeddingTable: Table | null = null;
10
+ private lanceDbInitialized: Promise<void>;
11
+
12
+ constructor(dbPath: string) {
13
+ // Open the main debugger database (not readonly, since we need to write)
14
+ this.db = new Database(dbPath);
15
+ console.log("AnalysisDb: Opening database at", dbPath);
16
+
17
+ this.initTables();
18
+ this.lanceDbInitialized = this.initLanceDb(dbPath);
19
+ }
20
+
21
+ private async initLanceDb(dbPath: string): Promise<void> {
22
+ try {
23
+ // LanceDB stores in a directory, use same location as SQLite db
24
+ const lanceDbPath = dbPath.replace(/\.db$/, ".lancedb");
25
+ console.log("AnalysisDb: Initializing LanceDB at", lanceDbPath);
26
+
27
+ this.lanceDb = await connect(lanceDbPath);
28
+
29
+ // Check if table exists, create if not
30
+ const tableNames = await this.lanceDb.tableNames();
31
+ if (tableNames.includes("session_embeddings")) {
32
+ this.embeddingTable =
33
+ await this.lanceDb.openTable("session_embeddings");
34
+ } else {
35
+ // Create table with schema
36
+ this.embeddingTable = await this.lanceDb.createTable(
37
+ "session_embeddings",
38
+ [{ session_id: "placeholder", embedding: new Array(1536).fill(0) }],
39
+ );
40
+ }
41
+
42
+ console.log("AnalysisDb: LanceDB initialized successfully");
43
+ } catch (error) {
44
+ console.error("Failed to initialize LanceDB:", error);
45
+ console.warn("Embedding functionality will be disabled");
46
+ }
47
+ }
48
+
49
+ private initTables() {
50
+ console.log("AnalysisDb: Initializing SQLite tables...");
51
+
52
+ // Create session_analyses table
53
+ this.db.run(`
54
+ CREATE TABLE IF NOT EXISTS session_analyses (
55
+ session_id TEXT PRIMARY KEY,
56
+ analysis_json TEXT NOT NULL,
57
+ analyzed_at TEXT NOT NULL,
58
+ updated_at TEXT NOT NULL
59
+ )
60
+ `);
61
+
62
+ // Create index on analyzed_at for sorting
63
+ this.db.run(`
64
+ CREATE INDEX IF NOT EXISTS idx_session_analyses_analyzed_at
65
+ ON session_analyses(analyzed_at)
66
+ `);
67
+
68
+ console.log("AnalysisDb: SQLite tables initialized");
69
+ }
70
+
71
+ saveAnalysis(analysis: SessionAnalysis): void {
72
+ const now = new Date().toISOString();
73
+
74
+ this.db.run(
75
+ `INSERT INTO session_analyses (
76
+ session_id, analysis_json, analyzed_at, updated_at
77
+ ) VALUES (?, ?, ?, ?)
78
+ ON CONFLICT(session_id) DO UPDATE SET
79
+ analysis_json = excluded.analysis_json,
80
+ updated_at = excluded.updated_at
81
+ `,
82
+ [analysis.session_id, JSON.stringify(analysis), now, now],
83
+ );
84
+ }
85
+
86
+ getAnalysis(sessionId: string): SessionAnalysis | null {
87
+ const row = this.db
88
+ .query<{ analysis_json: string }, [string]>(
89
+ `SELECT analysis_json FROM session_analyses WHERE session_id = ?`,
90
+ )
91
+ .get(sessionId);
92
+
93
+ if (!row) return null;
94
+
95
+ return JSON.parse(row.analysis_json) as SessionAnalysis;
96
+ }
97
+
98
+ listAnalyses(limit = 50, offset = 0): SessionAnalysis[] {
99
+ const rows = this.db
100
+ .query<{ analysis_json: string }, [number, number]>(
101
+ `SELECT analysis_json FROM session_analyses
102
+ ORDER BY analyzed_at DESC
103
+ LIMIT ? OFFSET ?`,
104
+ )
105
+ .all(limit, offset);
106
+
107
+ return rows.map((row) => JSON.parse(row.analysis_json) as SessionAnalysis);
108
+ }
109
+
110
+ /**
111
+ * Saves an embedding for a session analysis.
112
+ * If an embedding already exists for this session, it will be replaced.
113
+ */
114
+ async saveEmbedding(
115
+ sessionId: string,
116
+ embedding: Float32Array,
117
+ ): Promise<void> {
118
+ // Wait for LanceDB to initialize
119
+ await this.lanceDbInitialized;
120
+
121
+ if (!this.embeddingTable) {
122
+ console.warn("LanceDB not initialized, skipping embedding save");
123
+ return;
124
+ }
125
+
126
+ try {
127
+ // LanceDB uses add() to insert/upsert records
128
+ // Convert Float32Array to regular array for LanceDB
129
+ const embeddingArray = Array.from(embedding);
130
+
131
+ await this.embeddingTable.add([
132
+ {
133
+ session_id: sessionId,
134
+ embedding: embeddingArray,
135
+ },
136
+ ]);
137
+
138
+ console.log(`AnalysisDb: Saved embedding for session ${sessionId}`);
139
+ } catch (error) {
140
+ console.error(`Failed to save embedding for ${sessionId}:`, error);
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Retrieves the embedding for a session.
147
+ * Returns null if no embedding exists.
148
+ */
149
+ async getEmbedding(sessionId: string): Promise<Float32Array | null> {
150
+ // Wait for LanceDB to initialize
151
+ await this.lanceDbInitialized;
152
+
153
+ if (!this.embeddingTable) {
154
+ return null;
155
+ }
156
+
157
+ try {
158
+ // We need to do a vector search to query LanceDB
159
+ // Use a dummy vector and large limit to get all records, then filter
160
+ const dummyVector = new Array(1536).fill(0);
161
+ const allRecords = await this.embeddingTable
162
+ .search(dummyVector)
163
+ .limit(1000) // Should be enough for most use cases
164
+ .toArray();
165
+
166
+ const record = allRecords.find((r: any) => r.session_id === sessionId);
167
+
168
+ if (!record) {
169
+ return null;
170
+ }
171
+
172
+ const embedding = record.embedding;
173
+ return new Float32Array(embedding);
174
+ } catch (error) {
175
+ console.error("Error retrieving embedding:", error);
176
+ return null;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Searches for sessions with similar embeddings using KNN search.
182
+ * Returns session IDs with their distance scores (lower = more similar).
183
+ */
184
+ async searchSimilarSessions(
185
+ embedding: Float32Array,
186
+ limit = 10,
187
+ ): Promise<Array<{ session_id: string; distance: number }>> {
188
+ // Wait for LanceDB to initialize
189
+ await this.lanceDbInitialized;
190
+
191
+ if (!this.embeddingTable) {
192
+ return [];
193
+ }
194
+
195
+ try {
196
+ // LanceDB vector search
197
+ const embeddingArray = Array.from(embedding);
198
+ const results = await this.embeddingTable
199
+ .search(embeddingArray)
200
+ .limit(limit)
201
+ .toArray();
202
+
203
+ return results.map((result: any) => ({
204
+ session_id: result.session_id,
205
+ distance: result._distance,
206
+ }));
207
+ } catch (error) {
208
+ console.error("Error searching similar sessions:", error);
209
+ return [];
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Checks if an embedding exists for a session.
215
+ */
216
+ async hasEmbedding(sessionId: string): Promise<boolean> {
217
+ // Wait for LanceDB to initialize
218
+ await this.lanceDbInitialized;
219
+
220
+ if (!this.embeddingTable) {
221
+ return false;
222
+ }
223
+
224
+ try {
225
+ // Use a dummy vector search to get all records, then check if session exists
226
+ const dummyVector = new Array(1536).fill(0);
227
+ const allRecords = await this.embeddingTable
228
+ .search(dummyVector)
229
+ .limit(1000)
230
+ .toArray();
231
+
232
+ return allRecords.some((r: any) => r.session_id === sessionId);
233
+ } catch (error) {
234
+ console.error("Error checking embedding existence:", error);
235
+ return false;
236
+ }
237
+ }
238
+ }
@@ -1,6 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
3
  import { ComparisonDb } from "./comparison-db";
4
+ import type { ComparisonDimension } from "./types";
4
5
 
5
6
  const TEST_DB = "test-comparison.db";
6
7
 
@@ -27,7 +28,7 @@ describe("ComparisonDb", () => {
27
28
  const now = new Date().toISOString();
28
29
  const config = {
29
30
  id: "test-config",
30
- dimension: "model" as const,
31
+ dimensions: ["model"] as ComparisonDimension[],
31
32
  controlModel: "model-a",
32
33
  variantModel: "model-b",
33
34
  createdAt: now,
@@ -39,7 +40,7 @@ describe("ComparisonDb", () => {
39
40
 
40
41
  // Compare non-timestamp fields and verify timestamps exist
41
42
  expect(saved?.id).toBe(config.id);
42
- expect(saved?.dimension).toBe(config.dimension);
43
+ expect(saved?.dimensions).toEqual(config.dimensions);
43
44
  expect(saved?.controlModel).toBe(config.controlModel);
44
45
  expect(saved?.variantModel).toBe(config.variantModel);
45
46
  expect(saved?.createdAt).toBeDefined();
@@ -52,7 +53,7 @@ describe("ComparisonDb", () => {
52
53
  test("saves and retrieves config with tools", () => {
53
54
  const config = {
54
55
  id: "test-config-tools",
55
- dimension: "tools" as const,
56
+ dimensions: ["tools"] as ComparisonDimension[],
56
57
  variantTools: ["tool-a", "tool-b"],
57
58
  createdAt: new Date().toISOString(),
58
59
  updatedAt: new Date().toISOString(),
@@ -64,11 +65,32 @@ describe("ComparisonDb", () => {
64
65
  expect(saved?.variantTools).toEqual(["tool-a", "tool-b"]);
65
66
  });
66
67
 
68
+ test("saves and retrieves config with multiple dimensions", () => {
69
+ const config = {
70
+ id: "test-config-multi",
71
+ dimensions: ["model", "system_prompt", "tools"] as ComparisonDimension[],
72
+ controlModel: "model-a",
73
+ variantModel: "model-b",
74
+ variantSystemPrompt: "You are a test assistant",
75
+ variantTools: ["tool-a"],
76
+ createdAt: new Date().toISOString(),
77
+ updatedAt: new Date().toISOString(),
78
+ };
79
+
80
+ db.saveConfig(config);
81
+ const saved = db.getConfig("test-config-multi");
82
+
83
+ expect(saved?.dimensions).toEqual(["model", "system_prompt", "tools"]);
84
+ expect(saved?.variantModel).toBe("model-b");
85
+ expect(saved?.variantSystemPrompt).toBe("You are a test assistant");
86
+ expect(saved?.variantTools).toEqual(["tool-a"]);
87
+ });
88
+
67
89
  test("creates and lists runs", () => {
68
90
  const configId = "config-1";
69
91
  db.saveConfig({
70
92
  id: configId,
71
- dimension: "model",
93
+ dimensions: ["model"] as ComparisonDimension[],
72
94
  createdAt: new Date().toISOString(),
73
95
  updatedAt: new Date().toISOString(),
74
96
  });
@@ -86,7 +108,7 @@ describe("ComparisonDb", () => {
86
108
  const configId = "config-1";
87
109
  db.saveConfig({
88
110
  id: configId,
89
- dimension: "model",
111
+ dimensions: ["model"] as ComparisonDimension[],
90
112
  createdAt: new Date().toISOString(),
91
113
  updatedAt: new Date().toISOString(),
92
114
  });
@@ -100,6 +122,7 @@ describe("ComparisonDb", () => {
100
122
  totalTokens: 30,
101
123
  estimatedCost: 0.01,
102
124
  toolCallCount: 1,
125
+ toolCalls: [],
103
126
  };
104
127
 
105
128
  db.updateRunStatus(run.id, "completed", {
@@ -8,7 +8,7 @@ import type {
8
8
  SessionMetrics,
9
9
  } from "./types";
10
10
 
11
- const SCHEMA_VERSION = 1;
11
+ const SCHEMA_VERSION = 2;
12
12
 
13
13
  export class ComparisonDb {
14
14
  private db: Database;
@@ -34,7 +34,7 @@ export class ComparisonDb {
34
34
  this.db.run(`
35
35
  CREATE TABLE IF NOT EXISTS comparison_configs (
36
36
  id TEXT PRIMARY KEY,
37
- dimension TEXT NOT NULL,
37
+ dimensions TEXT NOT NULL,
38
38
  control_model TEXT,
39
39
  variant_model TEXT,
40
40
  variant_system_prompt TEXT,
@@ -71,8 +71,32 @@ export class ComparisonDb {
71
71
  `);
72
72
  }
73
73
 
74
- // Future migrations would go here:
75
- // if (currentVersion < 2) { ... }
74
+ // Migration: rename 'dimension' column to 'dimensions' and convert to JSON array
75
+ if (currentVersion < 2) {
76
+ // Check if old 'dimension' column exists
77
+ const tableInfo = this.db
78
+ .query<{ name: string }, []>("PRAGMA table_info(comparison_configs)")
79
+ .all();
80
+ const hasDimensionColumn = tableInfo.some(
81
+ (col) => col.name === "dimension",
82
+ );
83
+ const hasDimensionsColumn = tableInfo.some(
84
+ (col) => col.name === "dimensions",
85
+ );
86
+
87
+ if (hasDimensionColumn && !hasDimensionsColumn) {
88
+ // Rename column and migrate data
89
+ this.db.run(
90
+ `ALTER TABLE comparison_configs RENAME COLUMN dimension TO dimensions`,
91
+ );
92
+ // Convert single dimension values to JSON arrays
93
+ this.db.run(`
94
+ UPDATE comparison_configs
95
+ SET dimensions = '["' || dimensions || '"]'
96
+ WHERE dimensions NOT LIKE '[%'
97
+ `);
98
+ }
99
+ }
76
100
 
77
101
  // Update schema version
78
102
  this.db.run(`PRAGMA user_version = ${SCHEMA_VERSION}`);
@@ -84,10 +108,10 @@ export class ComparisonDb {
84
108
  const now = new Date().toISOString();
85
109
  this.db.run(
86
110
  `
87
- INSERT INTO comparison_configs (id, dimension, control_model, variant_model, variant_system_prompt, variant_tools, created_at, updated_at)
111
+ INSERT INTO comparison_configs (id, dimensions, control_model, variant_model, variant_system_prompt, variant_tools, created_at, updated_at)
88
112
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
89
113
  ON CONFLICT(id) DO UPDATE SET
90
- dimension = excluded.dimension,
114
+ dimensions = excluded.dimensions,
91
115
  control_model = excluded.control_model,
92
116
  variant_model = excluded.variant_model,
93
117
  variant_system_prompt = excluded.variant_system_prompt,
@@ -96,7 +120,7 @@ export class ComparisonDb {
96
120
  `,
97
121
  [
98
122
  config.id,
99
- config.dimension,
123
+ JSON.stringify(config.dimensions),
100
124
  config.controlModel ?? null,
101
125
  config.variantModel ?? null,
102
126
  config.variantSystemPrompt ?? null,
@@ -145,9 +169,32 @@ export class ComparisonDb {
145
169
  }
146
170
  }
147
171
 
172
+ // Parse dimensions from JSON array
173
+ let dimensions: ComparisonConfig["dimensions"] = [];
174
+ if (row.dimensions) {
175
+ try {
176
+ const parsed = JSON.parse(row.dimensions);
177
+ if (Array.isArray(parsed)) {
178
+ dimensions = parsed as ComparisonConfig["dimensions"];
179
+ } else if (typeof parsed === "string") {
180
+ // Handle case where it's a single dimension string (legacy format)
181
+ dimensions = [parsed as ComparisonConfig["dimensions"][number]];
182
+ }
183
+ } catch (e) {
184
+ // If JSON parsing fails, it might be a plain string (legacy format before migration)
185
+ if (typeof row.dimensions === "string" && row.dimensions.length > 0) {
186
+ dimensions = [
187
+ row.dimensions as ComparisonConfig["dimensions"][number],
188
+ ];
189
+ } else {
190
+ console.error("Failed to parse dimensions JSON:", e);
191
+ }
192
+ }
193
+ }
194
+
148
195
  return {
149
196
  id: row.id,
150
- dimension: row.dimension as ComparisonConfig["dimension"],
197
+ dimensions,
151
198
  controlModel: row.control_model ?? undefined,
152
199
  variantModel: row.variant_model ?? undefined,
153
200
  variantSystemPrompt: row.variant_system_prompt ?? undefined,
@@ -303,7 +350,8 @@ export class ComparisonDb {
303
350
  return result.data;
304
351
  }
305
352
  console.error("Invalid metrics schema:", result.error);
306
- return null;
353
+ const fallback = SessionMetricsSchema.safeParse({});
354
+ return fallback.success ? fallback.data : null;
307
355
  } catch (e) {
308
356
  console.error("Failed to parse metrics JSON:", e);
309
357
  return null;
@@ -0,0 +1,81 @@
1
+ import { Sparkles } from "lucide-react";
2
+ import { useState } from "react";
3
+ import { Button } from "./ui/button";
4
+
5
+ interface Props {
6
+ sessionIds: string[];
7
+ onComplete?: () => void;
8
+ }
9
+
10
+ export function AnalyzeAllButton({ sessionIds, onComplete }: Props) {
11
+ const [loading, setLoading] = useState(false);
12
+ const [progress, setProgress] = useState<{
13
+ completed: number;
14
+ total: number;
15
+ errors: number;
16
+ } | null>(null);
17
+ const [error, setError] = useState<string | null>(null);
18
+
19
+ const handleAnalyzeAll = async () => {
20
+ setLoading(true);
21
+ setError(null);
22
+ setProgress({ completed: 0, total: sessionIds.length, errors: 0 });
23
+
24
+ try {
25
+ const response = await fetch("/api/analyze-all-sessions", {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({ sessionIds }),
29
+ });
30
+
31
+ if (!response.ok) {
32
+ const errorData = await response.json();
33
+ throw new Error(errorData.error || response.statusText);
34
+ }
35
+
36
+ const { results } = await response.json();
37
+
38
+ // Count successes and errors
39
+ const completed = results.filter((r: any) => r.success).length;
40
+ const errors = results.filter((r: any) => !r.success).length;
41
+
42
+ setProgress({ completed, total: sessionIds.length, errors });
43
+
44
+ if (onComplete) {
45
+ onComplete();
46
+ }
47
+ } catch (err) {
48
+ const message = err instanceof Error ? err.message : "Analysis failed";
49
+ setError(message);
50
+ console.error("Batch analysis error:", err);
51
+ } finally {
52
+ setLoading(false);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <div className="flex flex-col gap-2">
58
+ <Button
59
+ variant="default"
60
+ onClick={handleAnalyzeAll}
61
+ disabled={loading || sessionIds.length === 0}
62
+ >
63
+ <Sparkles className="size-4 mr-2" />
64
+ {loading
65
+ ? `Analyzing ${progress?.completed}/${progress?.total}...`
66
+ : `Analyze All (${sessionIds.length})`}
67
+ </Button>
68
+
69
+ {progress && !loading && (
70
+ <div className="text-sm text-muted-foreground">
71
+ Completed: {progress.completed}/{progress.total}
72
+ {progress.errors > 0 && ` (${progress.errors} errors)`}
73
+ </div>
74
+ )}
75
+
76
+ {error && (
77
+ <div className="text-sm text-destructive max-w-md">{error}</div>
78
+ )}
79
+ </div>
80
+ );
81
+ }
@@ -29,6 +29,7 @@ export function DebuggerHeader({
29
29
  typeof window !== "undefined" ? window.location.pathname : "/";
30
30
  const isSessionsActive = pathname === "/";
31
31
  const isTracesActive = pathname === "/traces";
32
+ const isFindSessionsActive = pathname === "/find-sessions";
32
33
  const isTownHallActive = pathname.startsWith("/town-hall");
33
34
  const [agentName, setAgentName] = useState<string>("Agent");
34
35
 
@@ -107,6 +108,17 @@ export function DebuggerHeader({
107
108
  >
108
109
  Traces
109
110
  </a>
111
+ <a
112
+ href="/find-sessions"
113
+ className={cn(
114
+ "px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
115
+ isFindSessionsActive
116
+ ? "bg-muted text-foreground"
117
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/50",
118
+ )}
119
+ >
120
+ Find Sessions
121
+ </a>
110
122
  <a
111
123
  href="/town-hall"
112
124
  className={cn(
@@ -0,0 +1,109 @@
1
+ import { Eye, Sparkles } from "lucide-react";
2
+ import { useState } from "react";
3
+ import type { SessionAnalysis } from "../analysis/types";
4
+ import { SessionAnalysisDialog } from "./SessionAnalysisDialog";
5
+ import { Button } from "./ui/button";
6
+
7
+ interface Props {
8
+ sessionId: string;
9
+ existingAnalysis?: SessionAnalysis | null;
10
+ onAnalysisComplete?: (analysis: SessionAnalysis) => void;
11
+ }
12
+
13
+ export function SessionAnalysisButton({
14
+ sessionId,
15
+ existingAnalysis,
16
+ onAnalysisComplete,
17
+ }: Props) {
18
+ const [showDialog, setShowDialog] = useState(false);
19
+ const [loading, setLoading] = useState(false);
20
+ const [error, setError] = useState<string | null>(null);
21
+ const [analysis, setAnalysis] = useState<SessionAnalysis | null>(
22
+ existingAnalysis || null,
23
+ );
24
+
25
+ // Show existing analysis without re-analyzing
26
+ const handleShowAnalysis = () => {
27
+ if (existingAnalysis) {
28
+ setAnalysis(existingAnalysis);
29
+ setShowDialog(true);
30
+ }
31
+ };
32
+
33
+ // Analyze (or re-analyze) the session
34
+ const handleAnalyze = async () => {
35
+ setLoading(true);
36
+ setError(null);
37
+
38
+ try {
39
+ const response = await fetch(`/api/analyze-session/${sessionId}`, {
40
+ method: "POST",
41
+ });
42
+
43
+ if (!response.ok) {
44
+ const errorData = await response.json();
45
+ throw new Error(errorData.error || response.statusText);
46
+ }
47
+
48
+ const result = await response.json();
49
+ setAnalysis(result);
50
+ setShowDialog(true);
51
+
52
+ // Notify parent component
53
+ if (onAnalysisComplete) {
54
+ onAnalysisComplete(result);
55
+ }
56
+ } catch (err) {
57
+ const message = err instanceof Error ? err.message : "Analysis failed";
58
+ setError(message);
59
+ console.error("Session analysis error:", err);
60
+ } finally {
61
+ setLoading(false);
62
+ }
63
+ };
64
+
65
+ return (
66
+ <>
67
+ <div className="flex gap-2">
68
+ {existingAnalysis ? (
69
+ // Show both buttons when analysis exists
70
+ <>
71
+ <Button size="sm" onClick={handleShowAnalysis}>
72
+ <Eye className="size-4 mr-2" />
73
+ Show Analysis
74
+ </Button>
75
+ <Button
76
+ size="sm"
77
+ variant="outline"
78
+ onClick={handleAnalyze}
79
+ disabled={loading}
80
+ >
81
+ <Sparkles className="size-4 mr-2" />
82
+ {loading ? "Re-analyzing..." : "Re-analyze"}
83
+ </Button>
84
+ </>
85
+ ) : (
86
+ // Show only "Analyze Session" button if no analysis exists
87
+ <Button size="sm" onClick={handleAnalyze} disabled={loading}>
88
+ <Sparkles className="size-4 mr-2" />
89
+ {loading ? "Analyzing..." : "Analyze Session"}
90
+ </Button>
91
+ )}
92
+ </div>
93
+
94
+ {showDialog && analysis && (
95
+ <SessionAnalysisDialog
96
+ open={showDialog}
97
+ onClose={() => setShowDialog(false)}
98
+ analysis={analysis}
99
+ />
100
+ )}
101
+
102
+ {error && (
103
+ <div className="absolute top-14 right-4 bg-destructive text-white text-sm px-3 py-2 rounded shadow-lg max-w-md">
104
+ {error}
105
+ </div>
106
+ )}
107
+ </>
108
+ );
109
+ }