@useatlas/react 0.0.1 → 0.0.3

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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -2
  3. package/dist/{chunk-5SEVKHS5.cjs → chunk-35SCTKSW.js} +100 -7
  4. package/dist/chunk-35SCTKSW.js.map +1 -0
  5. package/dist/{chunk-UIRB6L36.cjs → chunk-DZFSZSQB.cjs} +46 -54
  6. package/dist/chunk-DZFSZSQB.cjs.map +1 -0
  7. package/dist/{chunk-2WFDP7G5.js → chunk-FMSGREKS.js} +46 -54
  8. package/dist/chunk-FMSGREKS.js.map +1 -0
  9. package/dist/{chunk-44HBZYKP.js → chunk-IDXGFWFS.cjs} +109 -3
  10. package/dist/chunk-IDXGFWFS.cjs.map +1 -0
  11. package/dist/global.d.ts +36 -0
  12. package/dist/hooks.cjs +10 -10
  13. package/dist/hooks.cjs.map +1 -1
  14. package/dist/hooks.d.cts +2 -2
  15. package/dist/hooks.d.ts +2 -2
  16. package/dist/hooks.js +3 -3
  17. package/dist/hooks.js.map +1 -1
  18. package/dist/index.cjs +385 -265
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +224 -4
  21. package/dist/index.d.ts +224 -4
  22. package/dist/index.js +328 -208
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/widget-types.d.ts +232 -0
  25. package/dist/{result-chart-YLCKBNV4.cjs → result-chart-ANZOT6FL.cjs} +24 -34
  26. package/dist/result-chart-ANZOT6FL.cjs.map +1 -0
  27. package/dist/{result-chart-NFAJ4IQ5.js → result-chart-C3EJTN5G.js} +22 -32
  28. package/dist/result-chart-C3EJTN5G.js.map +1 -0
  29. package/dist/widget.css +2 -2
  30. package/dist/widget.js +215 -246
  31. package/package.json +27 -17
  32. package/src/components/__tests__/data-table.test.tsx +125 -0
  33. package/src/components/actions/action-approval-card.tsx +26 -19
  34. package/src/components/actions/action-status-badge.tsx +3 -3
  35. package/src/components/atlas-chat.tsx +97 -37
  36. package/src/components/chart/result-chart.tsx +13 -37
  37. package/src/components/chat/api-key-bar.tsx +4 -4
  38. package/src/components/chat/data-table.tsx +42 -3
  39. package/src/components/chat/error-banner.tsx +108 -5
  40. package/src/components/chat/follow-up-chips.tsx +1 -1
  41. package/src/components/chat/managed-auth-card.tsx +6 -6
  42. package/src/components/conversations/conversation-item.tsx +19 -14
  43. package/src/components/conversations/conversation-list.tsx +3 -3
  44. package/src/components/conversations/conversation-sidebar.tsx +15 -4
  45. package/src/components/conversations/delete-confirmation.tsx +2 -2
  46. package/src/components/error-boundary.tsx +66 -0
  47. package/src/components/schema-explorer/schema-explorer.tsx +4 -0
  48. package/src/env.d.ts +9 -7
  49. package/src/global.d.ts +36 -0
  50. package/src/hooks/__tests__/use-atlas-conversations.test.tsx +4 -6
  51. package/src/hooks/use-atlas-chat.ts +1 -1
  52. package/src/hooks/use-atlas-conversations.ts +2 -2
  53. package/src/hooks/use-conversations.ts +60 -68
  54. package/src/index.ts +8 -0
  55. package/src/lib/action-types.ts +2 -2
  56. package/src/lib/helpers.ts +16 -16
  57. package/src/lib/types.ts +3 -2
  58. package/src/lib/widget-types.ts +232 -0
  59. package/src/test-setup.ts +2 -2
  60. package/dist/chunk-2WFDP7G5.js.map +0 -1
  61. package/dist/chunk-44HBZYKP.js.map +0 -1
  62. package/dist/chunk-5SEVKHS5.cjs.map +0 -1
  63. package/dist/chunk-UIRB6L36.cjs.map +0 -1
  64. package/dist/result-chart-NFAJ4IQ5.js.map +0 -1
  65. package/dist/result-chart-YLCKBNV4.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,19 +1,17 @@
1
- import { DarkModeContext, setTheme, useDarkMode, useConversations, AUTH_MODES, OKLCH_RE, applyBrandColor, useThemeMode, parseChatError } from './chunk-2WFDP7G5.js';
2
- export { AUTH_MODES, THEME_STORAGE_KEY, buildThemeInitScript, parseChatError, setTheme, useConversations } from './chunk-2WFDP7G5.js';
3
- import { detectCharts } from './chunk-44HBZYKP.js';
1
+ import { DarkModeContext, setTheme, useDarkMode, useConversations, AUTH_MODES, OKLCH_RE, applyBrandColor, useThemeMode, parseChatError } from './chunk-FMSGREKS.js';
2
+ export { AUTH_MODES, THEME_STORAGE_KEY, buildThemeInitScript, parseChatError, setTheme, useConversations } from './chunk-FMSGREKS.js';
3
+ import { detectCharts, ErrorBoundary, Button, cn } from './chunk-35SCTKSW.js';
4
4
  import { useChat } from '@ai-sdk/react';
5
5
  import { getToolName, DefaultChatTransport, isToolUIPart } from 'ai';
6
6
  import * as React from 'react';
7
7
  import { createContext, lazy, memo, useContext, Component, useState, useMemo, Suspense, useEffect, useRef, useCallback } from 'react';
8
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
+ import { FileDown, FileSpreadsheet, TableProperties, Star, Moon, Sun, Monitor, ArrowLeft, AlertTriangle, ServerCrash, Clock, ShieldAlert, WifiOff, XIcon, Columns3, Link2, Sparkles, Search, Eye, ArrowRight, Trash2 } from 'lucide-react';
9
10
  import { isActionToolResult, RESOLVED_STATUSES } from '@useatlas/types/action';
10
- import { FileDown, FileSpreadsheet, TableProperties, Star, Moon, Sun, Monitor, ArrowLeft, XIcon, Columns3, Link2, Sparkles, Search, Eye, ArrowRight, Trash2 } from 'lucide-react';
11
11
  import ReactMarkdown from 'react-markdown';
12
12
  import remarkGfm from 'remark-gfm';
13
+ import { ScrollArea as ScrollArea$1, ToggleGroup as ToggleGroup$1, Slot, DropdownMenu as DropdownMenu$1, Dialog, Separator as Separator$1, AlertDialog as AlertDialog$1 } from 'radix-ui';
13
14
  import { cva } from 'class-variance-authority';
14
- import { Slot, ScrollArea as ScrollArea$1, ToggleGroup as ToggleGroup$1, DropdownMenu as DropdownMenu$1, Dialog, Separator as Separator$1, AlertDialog as AlertDialog$1 } from 'radix-ui';
15
- import { clsx } from 'clsx';
16
- import { twMerge } from 'tailwind-merge';
17
15
 
18
16
  var AtlasUIContext = createContext(null);
19
17
  function useAtlasConfig() {
@@ -43,9 +41,30 @@ function ActionAuthProvider({
43
41
  );
44
42
  return /* @__PURE__ */ jsx(ActionAuthContext.Provider, { value, children });
45
43
  }
46
- function ErrorBanner({ error, authMode }) {
44
+ function ErrorIcon({ clientCode }) {
45
+ switch (clientCode) {
46
+ case "offline":
47
+ return /* @__PURE__ */ jsx(WifiOff, { className: "size-4 shrink-0" });
48
+ case "api_unreachable":
49
+ return /* @__PURE__ */ jsx(ServerCrash, { className: "size-4 shrink-0" });
50
+ case "auth_failure":
51
+ return /* @__PURE__ */ jsx(ShieldAlert, { className: "size-4 shrink-0" });
52
+ case "rate_limited_http":
53
+ return /* @__PURE__ */ jsx(Clock, { className: "size-4 shrink-0" });
54
+ case "server_error":
55
+ return /* @__PURE__ */ jsx(ServerCrash, { className: "size-4 shrink-0" });
56
+ default:
57
+ return /* @__PURE__ */ jsx(AlertTriangle, { className: "size-4 shrink-0" });
58
+ }
59
+ }
60
+ function ErrorBanner({
61
+ error,
62
+ authMode,
63
+ onRetry
64
+ }) {
47
65
  const info = useMemo(() => parseChatError(error, authMode), [error, authMode]);
48
66
  const [countdown, setCountdown] = useState(info.retryAfterSeconds ?? 0);
67
+ const [restoredOnline, setRestoredOnline] = useState(false);
49
68
  useEffect(() => {
50
69
  if (!info.retryAfterSeconds) return;
51
70
  setCountdown(info.retryAfterSeconds);
@@ -60,11 +79,74 @@ function ErrorBanner({ error, authMode }) {
60
79
  }, 1e3);
61
80
  return () => clearInterval(interval);
62
81
  }, [info.retryAfterSeconds]);
82
+ useEffect(() => {
83
+ if (info.retryAfterSeconds && countdown === 0 && onRetry) {
84
+ onRetry();
85
+ }
86
+ }, [countdown, info.retryAfterSeconds, onRetry]);
87
+ useEffect(() => {
88
+ if (info.clientCode !== "offline") return;
89
+ function handleOnline() {
90
+ setRestoredOnline(true);
91
+ onRetry?.();
92
+ }
93
+ window.addEventListener("online", handleOnline);
94
+ return () => {
95
+ window.removeEventListener("online", handleOnline);
96
+ };
97
+ }, [info.clientCode, onRetry]);
98
+ useEffect(() => {
99
+ try {
100
+ if (typeof window !== "undefined" && window.parent !== window) {
101
+ window.parent.postMessage(
102
+ {
103
+ type: "atlas:error",
104
+ error: {
105
+ code: info.clientCode ?? info.code ?? "unknown",
106
+ message: info.title,
107
+ detail: info.detail,
108
+ retryable: info.retryable
109
+ }
110
+ },
111
+ "*"
112
+ );
113
+ }
114
+ } catch {
115
+ }
116
+ }, [info.clientCode, info.code, info.title, info.detail, info.retryable]);
117
+ if (info.clientCode === "offline" && restoredOnline) {
118
+ return null;
119
+ }
63
120
  const detail = info.retryAfterSeconds && countdown > 0 ? `Try again in ${countdown} second${countdown !== 1 ? "s" : ""}.` : info.detail;
64
- return /* @__PURE__ */ jsxs("div", { className: "mb-2 rounded-lg border border-red-300 bg-red-50 text-red-700 dark:border-red-900/50 dark:bg-red-950/20 dark:text-red-400 px-4 py-3 text-sm", children: [
65
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: info.title }),
66
- detail && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs opacity-80", children: detail })
67
- ] });
121
+ const showRetry = info.retryable && onRetry && countdown === 0 && info.clientCode !== "offline";
122
+ return /* @__PURE__ */ jsx(
123
+ "div",
124
+ {
125
+ className: "mb-2 rounded-lg border border-red-300 bg-red-50 text-red-700 dark:border-red-900/50 dark:bg-red-950/20 dark:text-red-400 px-4 py-3 text-sm",
126
+ role: "alert",
127
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
128
+ /* @__PURE__ */ jsx(ErrorIcon, { clientCode: info.clientCode }),
129
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
130
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: info.title }),
131
+ detail && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs opacity-80", children: detail }),
132
+ info.requestId && /* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs opacity-60", children: [
133
+ "Request ID: ",
134
+ info.requestId
135
+ ] }),
136
+ showRetry && /* @__PURE__ */ jsx(
137
+ Button,
138
+ {
139
+ variant: "link",
140
+ size: "sm",
141
+ onClick: onRetry,
142
+ className: "mt-2 h-auto p-0 text-xs font-medium text-red-700 dark:text-red-400",
143
+ children: "Try again"
144
+ }
145
+ )
146
+ ] })
147
+ ] })
148
+ }
149
+ );
68
150
  }
69
151
  function ApiKeyBar({
70
152
  apiKey,
@@ -82,7 +164,7 @@ function ApiKeyBar({
82
164
  setDraft(apiKey);
83
165
  setEditing(true);
84
166
  },
85
- className: "rounded border border-zinc-200 px-2 py-0.5 text-zinc-500 transition-colors hover:border-zinc-400 hover:text-zinc-800 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200",
167
+ className: "rounded border border-zinc-200 px-2 py-0.5 text-zinc-500 transition-colors hover:border-zinc-400 hover:text-zinc-800 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200",
86
168
  children: "Change"
87
169
  }
88
170
  )
@@ -107,7 +189,7 @@ function ApiKeyBar({
107
189
  value: draft,
108
190
  onChange: (e) => setDraft(e.target.value),
109
191
  placeholder: "Enter your API key...",
110
- className: "flex-1 bg-transparent text-xs text-zinc-900 placeholder-zinc-400 outline-none dark:text-zinc-100 dark:placeholder-zinc-600",
192
+ className: "flex-1 bg-transparent text-xs text-zinc-900 placeholder-zinc-400 outline-none focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:text-zinc-100 dark:placeholder-zinc-600",
111
193
  autoFocus: true
112
194
  }
113
195
  ),
@@ -116,7 +198,7 @@ function ApiKeyBar({
116
198
  {
117
199
  type: "submit",
118
200
  disabled: !draft.trim(),
119
- className: "rounded border border-zinc-200 px-2 py-0.5 text-xs text-zinc-500 transition-colors hover:border-zinc-400 hover:text-zinc-800 disabled:opacity-40 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200",
201
+ className: "rounded border border-zinc-200 px-2 py-0.5 text-xs text-zinc-500 transition-colors hover:border-zinc-400 hover:text-zinc-800 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-40 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200",
120
202
  children: "Save"
121
203
  }
122
204
  ),
@@ -125,7 +207,7 @@ function ApiKeyBar({
125
207
  {
126
208
  type: "button",
127
209
  onClick: () => setEditing(false),
128
- className: "text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300",
210
+ className: "rounded text-xs text-zinc-400 hover:text-zinc-600 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:hover:text-zinc-300",
129
211
  children: "Cancel"
130
212
  }
131
213
  )
@@ -182,7 +264,7 @@ function ManagedAuthCard() {
182
264
  value: name,
183
265
  onChange: (e) => setName(e.target.value),
184
266
  placeholder: "Name (optional)",
185
- className: "w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
267
+ className: "w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus-visible:border-blue-500 focus-visible:ring-[3px] focus-visible:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
186
268
  }
187
269
  ),
188
270
  /* @__PURE__ */ jsx(
@@ -193,7 +275,7 @@ function ManagedAuthCard() {
193
275
  onChange: (e) => setEmail(e.target.value),
194
276
  placeholder: "Email",
195
277
  required: true,
196
- className: "w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
278
+ className: "w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus-visible:border-blue-500 focus-visible:ring-[3px] focus-visible:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
197
279
  }
198
280
  ),
199
281
  /* @__PURE__ */ jsx(
@@ -205,7 +287,7 @@ function ManagedAuthCard() {
205
287
  placeholder: "Password",
206
288
  required: true,
207
289
  minLength: 8,
208
- className: "w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
290
+ className: "w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus-visible:border-blue-500 focus-visible:ring-[3px] focus-visible:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
209
291
  }
210
292
  ),
211
293
  error && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-600 dark:text-red-400", children: error }),
@@ -214,7 +296,7 @@ function ManagedAuthCard() {
214
296
  {
215
297
  type: "submit",
216
298
  disabled: loading,
217
- className: "w-full rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500 disabled:opacity-40",
299
+ className: "w-full rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-blue-500/50 disabled:opacity-40",
218
300
  children: loading ? "..." : view === "login" ? "Sign in" : "Create account"
219
301
  }
220
302
  )
@@ -225,14 +307,14 @@ function ManagedAuthCard() {
225
307
  /* @__PURE__ */ jsx("button", { onClick: () => {
226
308
  setView("signup");
227
309
  setError("");
228
- }, className: "text-blue-600 hover:underline dark:text-blue-400", children: "Create one" })
310
+ }, className: "rounded text-blue-600 hover:underline focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-blue-500/50 dark:text-blue-400", children: "Create one" })
229
311
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
230
312
  "Already have an account?",
231
313
  " ",
232
314
  /* @__PURE__ */ jsx("button", { onClick: () => {
233
315
  setView("login");
234
316
  setError("");
235
- }, className: "text-blue-600 hover:underline dark:text-blue-400", children: "Sign in" })
317
+ }, className: "rounded text-blue-600 hover:underline focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-blue-500/50 dark:text-blue-400", children: "Sign in" })
236
318
  ] }) })
237
319
  ] }) });
238
320
  }
@@ -292,7 +374,7 @@ function downloadCSV(csv, filename = "atlas-results.csv") {
292
374
  a.download = filename;
293
375
  a.click();
294
376
  } catch (err) {
295
- console.error("CSV download failed:", err);
377
+ console.error("CSV download failed:", err instanceof Error ? err.message : String(err));
296
378
  window.alert("CSV download failed");
297
379
  } finally {
298
380
  if (url) {
@@ -311,31 +393,31 @@ function coerceExcelCell(v) {
311
393
  return String(v);
312
394
  }
313
395
  async function downloadExcel(columns, rows, filename = "atlas-results.xlsx") {
314
- let XLSX;
396
+ let ExcelJS;
315
397
  try {
316
- XLSX = await import(
317
- 'xlsx'
398
+ ExcelJS = await import(
399
+ 'exceljs'
318
400
  /* webpackIgnore: true */
319
401
  );
320
402
  } catch (err) {
321
- console.error("Failed to load xlsx library:", err);
403
+ console.error("Failed to load exceljs library:", err instanceof Error ? err.message : String(err));
322
404
  window.alert("Excel export is unavailable. The spreadsheet library failed to load.");
323
405
  return;
324
406
  }
325
407
  let url = null;
326
408
  try {
327
- const data = rows.map((row) => {
409
+ const wb = new ExcelJS.Workbook();
410
+ const ws = wb.addWorksheet("Results");
411
+ ws.columns = columns.map((col) => ({ header: col, key: col }));
412
+ for (const row of rows) {
328
413
  const obj = {};
329
414
  for (const col of columns) {
330
415
  obj[col] = coerceExcelCell(row[col]);
331
416
  }
332
- return obj;
333
- });
334
- const ws = XLSX.utils.json_to_sheet(data, { header: columns });
335
- const wb = XLSX.utils.book_new();
336
- XLSX.utils.book_append_sheet(wb, ws, "Results");
337
- const wbOut = XLSX.write(wb, { bookType: "xlsx", type: "array" });
338
- const blob = new Blob([wbOut], {
417
+ ws.addRow(obj);
418
+ }
419
+ const buffer = await wb.xlsx.writeBuffer();
420
+ const blob = new Blob([buffer], {
339
421
  type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
340
422
  });
341
423
  url = URL.createObjectURL(blob);
@@ -344,8 +426,8 @@ async function downloadExcel(columns, rows, filename = "atlas-results.xlsx") {
344
426
  a.download = filename;
345
427
  a.click();
346
428
  } catch (err) {
347
- console.error("Excel download failed:", err);
348
- const detail = err instanceof Error ? err.message : "Unknown error";
429
+ const detail = err instanceof Error ? err.message : String(err);
430
+ console.error("Excel download failed:", detail);
349
431
  window.alert(`Excel download failed: ${detail}
350
432
 
351
433
  You can try the CSV download as an alternative.`);
@@ -409,13 +491,19 @@ function LoadingCard({ label }) {
409
491
  label
410
492
  ] });
411
493
  }
412
- function DataTable({
494
+ function DataTableInner({
413
495
  columns,
414
496
  rows,
415
497
  maxRows = 10
416
498
  }) {
417
499
  const [sortCol, setSortCol] = useState(null);
418
500
  const [sortDir, setSortDir] = useState("asc");
501
+ if (rows.length === 0) {
502
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-zinc-200 px-4 py-8 text-center dark:border-zinc-700", children: [
503
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-zinc-500 dark:text-zinc-400", children: "Query returned no results" }),
504
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-zinc-400 dark:text-zinc-500", children: "Try adjusting your query filters or criteria" })
505
+ ] });
506
+ }
419
507
  const hasMore = rows.length > maxRows;
420
508
  const cell = (row, colIdx) => {
421
509
  if (Array.isArray(row)) return row[colIdx];
@@ -458,8 +546,17 @@ function DataTable({
458
546
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { className: "border-b border-zinc-200 bg-zinc-100/80 dark:border-zinc-700 dark:bg-zinc-800/80", children: columns.map((col, i) => /* @__PURE__ */ jsxs(
459
547
  "th",
460
548
  {
549
+ tabIndex: 0,
550
+ role: "columnheader",
551
+ "aria-sort": sortCol === i ? sortDir === "asc" ? "ascending" : "descending" : "none",
461
552
  onClick: () => handleSort(i),
462
- className: "group cursor-pointer select-none whitespace-nowrap px-3 py-2 text-left font-medium text-zinc-500 transition-colors hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200",
553
+ onKeyDown: (e) => {
554
+ if (e.key === "Enter" || e.key === " ") {
555
+ e.preventDefault();
556
+ handleSort(i);
557
+ }
558
+ },
559
+ className: "group cursor-pointer select-none whitespace-nowrap px-3 py-2 text-left font-medium text-zinc-500 transition-colors hover:text-zinc-800 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:text-zinc-400 dark:hover:text-zinc-200",
463
560
  children: [
464
561
  col,
465
562
  sortCol === i ? sortDir === "asc" ? " \u25B2" : " \u25BC" : ""
@@ -485,6 +582,18 @@ function DataTable({
485
582
  ] })
486
583
  ] });
487
584
  }
585
+ function DataTable(props) {
586
+ return /* @__PURE__ */ jsx(
587
+ ErrorBoundary,
588
+ {
589
+ fallbackRender: (_error, reset) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-900/50 dark:bg-red-950/20 dark:text-red-400", children: [
590
+ /* @__PURE__ */ jsx("span", { children: "Unable to render results." }),
591
+ /* @__PURE__ */ jsx(Button, { variant: "link", onClick: reset, children: "Retry" })
592
+ ] }),
593
+ children: /* @__PURE__ */ jsx(DataTableInner, { ...props })
594
+ }
595
+ );
596
+ }
488
597
  function CopyButton({ text, label = "Copy" }) {
489
598
  const [state, setState] = useState("idle");
490
599
  return /* @__PURE__ */ jsx(
@@ -539,7 +648,7 @@ function SQLBlock({ sql }) {
539
648
  /* @__PURE__ */ jsx("div", { className: "absolute right-2 top-2", children: /* @__PURE__ */ jsx(CopyButton, { text: sql, label: "Copy SQL" }) })
540
649
  ] });
541
650
  }
542
- var ResultChart = lazy(() => import('./result-chart-NFAJ4IQ5.js').then((m) => ({ default: m.ResultChart })));
651
+ var ResultChart = lazy(() => import('./result-chart-C3EJTN5G.js').then((m) => ({ default: m.ResultChart })));
543
652
  var ChartFallback = /* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800" });
544
653
  function toStringRows(columns, rows) {
545
654
  return rows.map((row) => columns.map((col) => row[col] == null ? "" : String(row[col])));
@@ -710,7 +819,8 @@ function borderColor(status) {
710
819
  case "denied":
711
820
  case "failed":
712
821
  return "border-red-300 dark:border-red-900/50";
713
- default:
822
+ case "rolled_back":
823
+ case "timed_out":
714
824
  return "border-zinc-200 dark:border-zinc-700";
715
825
  }
716
826
  }
@@ -732,7 +842,7 @@ function ActionApprovalCard({ part }) {
732
842
  const effectiveStatus = cardState.phase === "resolved" ? cardState.status : toolResult.status;
733
843
  const isPending = effectiveStatus === "pending_approval" && cardState.phase !== "submitting";
734
844
  const isSubmitting = cardState.phase === "submitting";
735
- const resolvedResult = cardState.phase === "resolved" ? cardState.result : toolResult.result;
845
+ const resolvedResult = cardState.phase === "resolved" ? cardState.result : toolResult.status === "approved" || toolResult.status === "executed" || toolResult.status === "auto_approved" ? toolResult.result : void 0;
736
846
  async function callAction(endpoint, body) {
737
847
  if (!actionAuth) {
738
848
  console.warn("ActionApprovalCard: No ActionAuthProvider found. API calls will be sent without authentication.");
@@ -758,8 +868,10 @@ function ActionApprovalCard({ part }) {
758
868
  } catch {
759
869
  throw new Error("Action was already resolved, but the response could not be read. Refresh the page.");
760
870
  }
761
- const status = typeof data2.status === "string" ? data2.status : "failed";
762
- const label = status.replace(/_/g, " ");
871
+ if (typeof data2.status !== "string" || !RESOLVED_STATUSES.has(data2.status)) {
872
+ throw new Error("Action was already resolved with an unrecognized status. Refresh the page.");
873
+ }
874
+ const label = data2.status.replace(/_/g, " ");
763
875
  throw new Error(`This action was already ${label} by another user or policy.`);
764
876
  }
765
877
  if (!res.ok) {
@@ -772,8 +884,8 @@ function ActionApprovalCard({ part }) {
772
884
  } catch {
773
885
  throw new Error("Action succeeded, but the response could not be read. Refresh the page.");
774
886
  }
775
- if (typeof data.status !== "string") {
776
- throw new Error("Action succeeded, but the server returned an invalid status. Refresh the page.");
887
+ if (typeof data.status !== "string" || !RESOLVED_STATUSES.has(data.status)) {
888
+ throw new Error("Action succeeded, but the server returned an unrecognized status. Refresh the page.");
777
889
  }
778
890
  setCardState({ phase: "resolved", status: data.status, result: data.result });
779
891
  }
@@ -816,11 +928,11 @@ function ActionApprovalCard({ part }) {
816
928
  /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Result: " }),
817
929
  typeof resolvedResult === "string" ? resolvedResult : safeStringify(resolvedResult)
818
930
  ] }),
819
- toolResult.error && /* @__PURE__ */ jsxs("div", { className: "mb-2 rounded bg-red-50 p-2 text-xs text-red-700 dark:bg-red-900/20 dark:text-red-400", children: [
931
+ toolResult.status === "failed" && /* @__PURE__ */ jsxs("div", { className: "mb-2 rounded bg-red-50 p-2 text-xs text-red-700 dark:bg-red-900/20 dark:text-red-400", children: [
820
932
  /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Error: " }),
821
933
  toolResult.error
822
934
  ] }),
823
- toolResult.reason && RESOLVED_STATUSES.has(effectiveStatus) && /* @__PURE__ */ jsxs("div", { className: "mb-2 text-xs text-zinc-500 dark:text-zinc-400", children: [
935
+ toolResult.status === "denied" && RESOLVED_STATUSES.has(effectiveStatus) && /* @__PURE__ */ jsxs("div", { className: "mb-2 text-xs text-zinc-500 dark:text-zinc-400", children: [
824
936
  /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Reason: " }),
825
937
  toolResult.reason
826
938
  ] })
@@ -833,7 +945,7 @@ function ActionApprovalCard({ part }) {
833
945
  {
834
946
  onClick: handleApprove,
835
947
  disabled: isSubmitting,
836
- className: "inline-flex items-center gap-1.5 rounded bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-500 disabled:opacity-40",
948
+ className: "inline-flex items-center gap-1.5 rounded bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-blue-500/50 disabled:opacity-40",
837
949
  children: [
838
950
  isSubmitting && cardState.action === "approve" && /* @__PURE__ */ jsx("span", { className: "inline-block h-3 w-3 animate-spin rounded-full border-2 border-white/30 border-t-white" }),
839
951
  "Approve"
@@ -845,7 +957,7 @@ function ActionApprovalCard({ part }) {
845
957
  {
846
958
  onClick: () => setShowDenyInput(true),
847
959
  disabled: isSubmitting,
848
- className: "rounded border border-zinc-300 px-3 py-1.5 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-400 hover:text-zinc-800 disabled:opacity-40 dark:border-zinc-600 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200",
960
+ className: "rounded border border-zinc-300 px-3 py-1.5 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-400 hover:text-zinc-800 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-40 dark:border-zinc-600 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200",
849
961
  children: "Deny"
850
962
  }
851
963
  ) : /* @__PURE__ */ jsxs("div", { className: "flex flex-1 items-center gap-2", children: [
@@ -855,7 +967,7 @@ function ActionApprovalCard({ part }) {
855
967
  value: denyReason,
856
968
  onChange: (e) => setDenyReason(e.target.value),
857
969
  placeholder: "Reason (optional)",
858
- className: "flex-1 rounded border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-900 placeholder-zinc-400 outline-none focus:border-red-400 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600",
970
+ className: "flex-1 rounded border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-900 placeholder-zinc-400 outline-none focus-visible:border-red-400 focus-visible:ring-[3px] focus-visible:ring-red-400/30 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600",
859
971
  disabled: isSubmitting
860
972
  }
861
973
  ),
@@ -864,7 +976,7 @@ function ActionApprovalCard({ part }) {
864
976
  {
865
977
  onClick: handleDeny,
866
978
  disabled: isSubmitting,
867
- className: "inline-flex items-center gap-1.5 rounded bg-red-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-red-500 disabled:opacity-40",
979
+ className: "inline-flex items-center gap-1.5 rounded bg-red-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-red-500 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-red-500/50 disabled:opacity-40",
868
980
  children: [
869
981
  isSubmitting && cardState.action === "deny" && /* @__PURE__ */ jsx("span", { className: "inline-block h-3 w-3 animate-spin rounded-full border-2 border-white/30 border-t-white" }),
870
982
  "Confirm Deny"
@@ -879,7 +991,7 @@ function ActionApprovalCard({ part }) {
879
991
  setDenyReason("");
880
992
  },
881
993
  disabled: isSubmitting,
882
- className: "text-xs text-zinc-400 hover:text-zinc-600 disabled:opacity-40 dark:hover:text-zinc-300",
994
+ className: "rounded text-xs text-zinc-400 hover:text-zinc-600 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-40 dark:hover:text-zinc-300",
883
995
  children: "Cancel"
884
996
  }
885
997
  )
@@ -888,7 +1000,7 @@ function ActionApprovalCard({ part }) {
888
1000
  ] })
889
1001
  ] });
890
1002
  }
891
- var ResultChart2 = lazy(() => import('./result-chart-NFAJ4IQ5.js').then((m) => ({ default: m.ResultChart })));
1003
+ var ResultChart2 = lazy(() => import('./result-chart-C3EJTN5G.js').then((m) => ({ default: m.ResultChart })));
892
1004
  var ALLOWED_IMAGE_MIME = /* @__PURE__ */ new Set(["image/png", "image/jpeg"]);
893
1005
  var PythonErrorBoundary = class extends Component {
894
1006
  constructor(props) {
@@ -1172,63 +1284,12 @@ var STARTER_PROMPTS = [
1172
1284
  "What is the headcount breakdown by department?",
1173
1285
  "What is total MRR by plan type?"
1174
1286
  ];
1175
- function cn(...inputs) {
1176
- return twMerge(clsx(inputs));
1177
- }
1178
- var buttonVariants = cva(
1179
- "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
1180
- {
1181
- variants: {
1182
- variant: {
1183
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
1184
- destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
1185
- outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
1186
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
1187
- ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
1188
- link: "text-primary underline-offset-4 hover:underline"
1189
- },
1190
- size: {
1191
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
1192
- xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
1193
- sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
1194
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
1195
- icon: "size-9",
1196
- "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
1197
- "icon-sm": "size-8",
1198
- "icon-lg": "size-10"
1199
- }
1200
- },
1201
- defaultVariants: {
1202
- variant: "default",
1203
- size: "default"
1204
- }
1205
- }
1206
- );
1207
- function Button({
1208
- className,
1209
- variant = "default",
1210
- size = "default",
1211
- asChild = false,
1212
- ...props
1213
- }) {
1214
- const Comp = asChild ? Slot.Root : "button";
1215
- return /* @__PURE__ */ jsx(
1216
- Comp,
1217
- {
1218
- "data-slot": "button",
1219
- "data-variant": variant,
1220
- "data-size": size,
1221
- className: cn(buttonVariants({ variant, size, className })),
1222
- ...props
1223
- }
1224
- );
1225
- }
1226
1287
  function FollowUpChips({
1227
1288
  suggestions,
1228
1289
  onSelect
1229
1290
  }) {
1230
1291
  if (suggestions.length === 0) return null;
1231
- return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 pt-1", children: suggestions.map((s, i) => /* @__PURE__ */ jsx(
1292
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 pt-1", role: "group", "aria-label": "Suggested follow-up questions", children: suggestions.map((s, i) => /* @__PURE__ */ jsx(
1232
1293
  Button,
1233
1294
  {
1234
1295
  variant: "outline",
@@ -1364,7 +1425,7 @@ function DeleteConfirmation({
1364
1425
  "button",
1365
1426
  {
1366
1427
  onClick: onCancel,
1367
- className: "rounded px-2 py-0.5 text-zinc-500 transition-colors hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200",
1428
+ className: "rounded px-2 py-0.5 text-zinc-500 transition-colors hover:text-zinc-800 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:text-zinc-400 dark:hover:text-zinc-200",
1368
1429
  children: "Cancel"
1369
1430
  }
1370
1431
  ),
@@ -1372,7 +1433,7 @@ function DeleteConfirmation({
1372
1433
  "button",
1373
1434
  {
1374
1435
  onClick: onConfirm,
1375
- className: "rounded bg-red-600 px-2 py-0.5 text-white transition-colors hover:bg-red-500",
1436
+ className: "rounded bg-red-600 px-2 py-0.5 text-white transition-colors hover:bg-red-500 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-red-500/50 ",
1376
1437
  children: "Delete"
1377
1438
  }
1378
1439
  )
@@ -1402,6 +1463,7 @@ function ConversationItem({
1402
1463
  const [confirmDelete, setConfirmDelete] = useState(false);
1403
1464
  const [deleting, setDeleting] = useState(false);
1404
1465
  const [starPending, setStarPending] = useState(false);
1466
+ const [error, setError] = useState(null);
1405
1467
  if (confirmDelete) {
1406
1468
  return /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-red-200 bg-red-50/50 dark:border-red-900/30 dark:bg-red-950/10", children: /* @__PURE__ */ jsx(
1407
1469
  DeleteConfirmation,
@@ -1410,12 +1472,11 @@ function ConversationItem({
1410
1472
  onConfirm: async () => {
1411
1473
  setDeleting(true);
1412
1474
  try {
1413
- const success = await onDelete();
1414
- if (success) {
1415
- setConfirmDelete(false);
1416
- }
1417
- } catch (err) {
1418
- console.warn("Failed to delete conversation:", err);
1475
+ await onDelete();
1476
+ setConfirmDelete(false);
1477
+ } catch {
1478
+ setError("Failed to delete");
1479
+ setTimeout(() => setError(null), 3e3);
1419
1480
  } finally {
1420
1481
  setDeleting(false);
1421
1482
  }
@@ -1435,11 +1496,11 @@ function ConversationItem({
1435
1496
  onSelect();
1436
1497
  }
1437
1498
  },
1438
- className: `group flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 text-left text-sm transition-colors ${isActive ? "bg-blue-50 text-blue-700 dark:bg-blue-600/10 dark:text-blue-400" : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"}`,
1499
+ className: `group flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 ${isActive ? "bg-blue-50 text-blue-700 dark:bg-blue-600/10 dark:text-blue-400" : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"}`,
1439
1500
  children: [
1440
1501
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1441
1502
  /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: conversation.title || "New conversation" }),
1442
- /* @__PURE__ */ jsx("p", { className: "text-xs text-zinc-400 dark:text-zinc-500", children: relativeTime(conversation.updatedAt) })
1503
+ error ? /* @__PURE__ */ jsx("p", { className: "text-xs text-red-500 dark:text-red-400", children: error }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-zinc-400 dark:text-zinc-500", children: relativeTime(conversation.updatedAt) })
1443
1504
  ] }),
1444
1505
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [
1445
1506
  /* @__PURE__ */ jsx(
@@ -1453,8 +1514,9 @@ function ConversationItem({
1453
1514
  setStarPending(true);
1454
1515
  try {
1455
1516
  await onStar(!conversation.starred);
1456
- } catch (err) {
1457
- console.warn("Failed to update star:", err);
1517
+ } catch {
1518
+ setError("Failed to update");
1519
+ setTimeout(() => setError(null), 3e3);
1458
1520
  } finally {
1459
1521
  setStarPending(false);
1460
1522
  }
@@ -1495,7 +1557,7 @@ function ConversationList({
1495
1557
  emptyMessage
1496
1558
  }) {
1497
1559
  if (conversations.length === 0) {
1498
- return /* @__PURE__ */ jsx("div", { className: "px-3 py-6 text-center text-xs text-zinc-400 dark:text-zinc-500", children: emptyMessage ?? "No conversations yet" });
1560
+ return /* @__PURE__ */ jsx("div", { className: "px-3 py-6 text-center text-xs text-zinc-500 dark:text-zinc-400", children: emptyMessage ?? "No conversations yet" });
1499
1561
  }
1500
1562
  function renderItems(items) {
1501
1563
  return items.map((c) => /* @__PURE__ */ jsx(
@@ -1536,6 +1598,14 @@ function ConversationSidebar({
1536
1598
  onMobileClose
1537
1599
  }) {
1538
1600
  const [filter, setFilter] = useState("all");
1601
+ useEffect(() => {
1602
+ if (!mobileOpen) return;
1603
+ function handleKeyDown(e) {
1604
+ if (e.key === "Escape") onMobileClose();
1605
+ }
1606
+ document.addEventListener("keydown", handleKeyDown);
1607
+ return () => document.removeEventListener("keydown", handleKeyDown);
1608
+ }, [mobileOpen, onMobileClose]);
1539
1609
  const starredConversations = conversations.filter((c) => c.starred);
1540
1610
  const filteredConversations = filter === "saved" ? starredConversations : conversations;
1541
1611
  const sidebar = /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col border-r border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-950", children: [
@@ -1545,7 +1615,7 @@ function ConversationSidebar({
1545
1615
  "button",
1546
1616
  {
1547
1617
  onClick: onNewChat,
1548
- className: "rounded-lg border border-zinc-200 px-3 py-1.5 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-400 hover:text-zinc-900 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200",
1618
+ className: "rounded-lg border border-zinc-200 px-3 py-1.5 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-400 hover:text-zinc-900 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200",
1549
1619
  children: "+ New"
1550
1620
  }
1551
1621
  )
@@ -1897,6 +1967,19 @@ function SheetTitle({
1897
1967
  }
1898
1968
  );
1899
1969
  }
1970
+ function SheetDescription({
1971
+ className,
1972
+ ...props
1973
+ }) {
1974
+ return /* @__PURE__ */ jsx(
1975
+ Dialog.Description,
1976
+ {
1977
+ "data-slot": "sheet-description",
1978
+ className: cn("text-sm text-muted-foreground", className),
1979
+ ...props
1980
+ }
1981
+ );
1982
+ }
1900
1983
  function ScrollArea({
1901
1984
  className,
1902
1985
  children,
@@ -2348,10 +2431,13 @@ function SchemaExplorer({
2348
2431
  onOpenChange(false);
2349
2432
  }
2350
2433
  return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, children: /* @__PURE__ */ jsxs(SheetContent, { side: "right", className: "flex w-full flex-col p-0 sm:max-w-xl", children: [
2351
- /* @__PURE__ */ jsx(SheetHeader, { className: "px-4 pt-4", children: /* @__PURE__ */ jsxs(SheetTitle, { className: "flex items-center gap-2 text-base", children: [
2352
- /* @__PURE__ */ jsx(TableProperties, { className: "size-4" }),
2353
- "Schema Explorer"
2354
- ] }) }),
2434
+ /* @__PURE__ */ jsxs(SheetHeader, { className: "px-4 pt-4", children: [
2435
+ /* @__PURE__ */ jsxs(SheetTitle, { className: "flex items-center gap-2 text-base", children: [
2436
+ /* @__PURE__ */ jsx(TableProperties, { className: "size-4" }),
2437
+ "Schema Explorer"
2438
+ ] }),
2439
+ /* @__PURE__ */ jsx(SheetDescription, { className: "sr-only", children: "Browse tables, columns, joins, and query patterns from the semantic layer" })
2440
+ ] }),
2355
2441
  /* @__PURE__ */ jsx(Separator, { className: "mt-3" }),
2356
2442
  /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-hidden pt-3", children: loading ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx("p", { className: "text-xs text-zinc-400", children: "Loading schema..." }) }) : error ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center px-4", children: /* @__PURE__ */ jsx("p", { className: "text-center text-xs text-red-500", children: error }) }) : selectedName ? detailError ? /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center gap-2 px-4", children: [
2357
2443
  /* @__PURE__ */ jsx("p", { className: "text-center text-xs text-red-500", children: detailError }),
@@ -2519,7 +2605,9 @@ function AtlasChat(props) {
2519
2605
  sidebar = false,
2520
2606
  schemaExplorer: schemaExplorerEnabled = false,
2521
2607
  authClient = noopAuthClient,
2522
- toolRenderers
2608
+ toolRenderers,
2609
+ chatEndpoint = "/api/v1/chat",
2610
+ conversationsEndpoint = "/api/v1/conversations"
2523
2611
  } = props;
2524
2612
  useEffect(() => {
2525
2613
  setTheme(propTheme);
@@ -2530,7 +2618,9 @@ function AtlasChat(props) {
2530
2618
  propApiKey,
2531
2619
  sidebar,
2532
2620
  schemaExplorerEnabled,
2533
- toolRenderers
2621
+ toolRenderers,
2622
+ chatEndpoint,
2623
+ conversationsEndpoint
2534
2624
  }
2535
2625
  ) });
2536
2626
  }
@@ -2538,7 +2628,9 @@ function AtlasChatInner({
2538
2628
  propApiKey,
2539
2629
  sidebar,
2540
2630
  schemaExplorerEnabled,
2541
- toolRenderers
2631
+ toolRenderers,
2632
+ chatEndpoint,
2633
+ conversationsEndpoint
2542
2634
  }) {
2543
2635
  const { apiUrl, isCrossOrigin, authClient } = useAtlasConfig();
2544
2636
  const dark = useDarkMode();
@@ -2572,7 +2664,8 @@ function AtlasChatInner({
2572
2664
  apiUrl,
2573
2665
  enabled: sidebar,
2574
2666
  getHeaders,
2575
- getCredentials
2667
+ getCredentials,
2668
+ conversationsEndpoint
2576
2669
  });
2577
2670
  const refreshConvosRef = useRef(convos.refresh);
2578
2671
  refreshConvosRef.current = convos.refresh;
@@ -2664,7 +2757,7 @@ function AtlasChatInner({
2664
2757
  headers["Authorization"] = `Bearer ${apiKey}`;
2665
2758
  }
2666
2759
  return new DefaultChatTransport({
2667
- api: `${apiUrl}/api/chat`,
2760
+ api: `${apiUrl}${chatEndpoint}`,
2668
2761
  headers,
2669
2762
  credentials: isCrossOrigin ? "include" : void 0,
2670
2763
  body: () => conversationIdRef.current ? { conversationId: conversationIdRef.current } : {},
@@ -2682,7 +2775,7 @@ function AtlasChatInner({
2682
2775
  return response;
2683
2776
  })
2684
2777
  });
2685
- }, [apiKey, apiUrl, isCrossOrigin]);
2778
+ }, [apiKey, apiUrl, isCrossOrigin, chatEndpoint]);
2686
2779
  const { messages, setMessages, sendMessage, status, error } = useChat({ transport });
2687
2780
  const isLoading = status === "streaming" || status === "submitted";
2688
2781
  useEffect(() => {
@@ -2707,17 +2800,12 @@ function AtlasChatInner({
2707
2800
  setLoadingConversation(true);
2708
2801
  try {
2709
2802
  const uiMessages = await convos.loadConversation(id);
2710
- if (uiMessages) {
2711
- setMessages(uiMessages);
2712
- setConversationId(id);
2713
- convos.setSelectedId(id);
2714
- setMobileMenuOpen(false);
2715
- } else {
2716
- setHealthWarning("Could not load conversation. It may have been deleted.");
2717
- setTimeout(() => setHealthWarning(""), 5e3);
2718
- }
2803
+ setMessages(uiMessages);
2804
+ setConversationId(id);
2805
+ convos.setSelectedId(id);
2806
+ setMobileMenuOpen(false);
2719
2807
  } catch (err) {
2720
- console.warn("Failed to load conversation:", err);
2808
+ console.warn("Failed to load conversation:", err instanceof Error ? err.message : String(err));
2721
2809
  setHealthWarning("Failed to load conversation. Please try again.");
2722
2810
  setTimeout(() => setHealthWarning(""), 5e3);
2723
2811
  } finally {
@@ -2751,14 +2839,16 @@ function AtlasChatInner({
2751
2839
  onMobileClose: () => setMobileMenuOpen(false)
2752
2840
  }
2753
2841
  ),
2754
- /* @__PURE__ */ jsx("main", { className: "flex flex-1 flex-col overflow-hidden", children: /* @__PURE__ */ jsxs("div", { className: "mx-auto flex w-full max-w-4xl flex-1 flex-col overflow-hidden p-4", children: [
2842
+ /* @__PURE__ */ jsx("main", { id: "main", tabIndex: -1, className: "flex flex-1 flex-col overflow-hidden", children: /* @__PURE__ */ jsxs("div", { className: "mx-auto flex w-full max-w-4xl flex-1 flex-col overflow-hidden p-4", children: [
2755
2843
  /* @__PURE__ */ jsx("header", { className: "mb-4 flex-none border-b border-zinc-100 pb-3 dark:border-zinc-800", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2756
2844
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2757
2845
  showSidebar && /* @__PURE__ */ jsx(
2758
- "button",
2846
+ Button,
2759
2847
  {
2848
+ variant: "ghost",
2849
+ size: "icon",
2760
2850
  onClick: () => setMobileMenuOpen(true),
2761
- className: "flex size-11 items-center justify-center rounded text-zinc-400 hover:text-zinc-700 md:hidden dark:hover:text-zinc-200",
2851
+ className: "size-11 text-zinc-400 hover:text-zinc-700 md:hidden dark:hover:text-zinc-200",
2762
2852
  "aria-label": "Open conversation history",
2763
2853
  children: MenuIcon
2764
2854
  }
@@ -2787,8 +2877,10 @@ function AtlasChatInner({
2787
2877
  isSignedIn && /* @__PURE__ */ jsxs(Fragment, { children: [
2788
2878
  /* @__PURE__ */ jsx("span", { className: "hidden text-xs text-zinc-500 sm:inline dark:text-zinc-400", children: managedSession.data?.user?.email }),
2789
2879
  /* @__PURE__ */ jsx(
2790
- "button",
2880
+ Button,
2791
2881
  {
2882
+ variant: "outline",
2883
+ size: "sm",
2792
2884
  onClick: () => {
2793
2885
  authClient.signOut().catch((err) => {
2794
2886
  console.error("Sign out failed:", err);
@@ -2796,75 +2888,101 @@ function AtlasChatInner({
2796
2888
  setTimeout(() => setHealthWarning(""), 5e3);
2797
2889
  });
2798
2890
  },
2799
- className: "rounded border border-zinc-200 px-3 py-2 text-xs text-zinc-500 transition-colors hover:border-zinc-400 hover:text-zinc-800 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200",
2891
+ className: "text-xs text-zinc-500 dark:text-zinc-400",
2800
2892
  children: "Sign out"
2801
2893
  }
2802
2894
  )
2803
2895
  ] })
2804
2896
  ] })
2805
2897
  ] }) }),
2806
- healthWarning && /* @__PURE__ */ jsx("p", { className: "mb-2 text-xs text-zinc-400 dark:text-zinc-500", children: healthWarning }),
2898
+ (healthWarning || convos.fetchError) && /* @__PURE__ */ jsx("p", { className: "mb-2 text-xs text-zinc-400 dark:text-zinc-500", children: healthWarning || convos.fetchError }),
2807
2899
  isManaged && !isSignedIn ? /* @__PURE__ */ jsx(ManagedAuthCard, {}) : /* @__PURE__ */ jsxs(ActionAuthProvider, { getHeaders, getCredentials, children: [
2808
2900
  authMode === "simple-key" && !propApiKey && /* @__PURE__ */ jsx("div", { className: "mb-3 flex-none", children: /* @__PURE__ */ jsx(ApiKeyBar, { apiKey, onSave: handleSaveApiKey }) }),
2809
- /* @__PURE__ */ jsx(ScrollArea, { viewportRef: scrollRef, className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxs("div", { "data-atlas-messages": true, className: "space-y-4 pb-4 pr-3", children: [
2810
- messages.length === 0 && !error && /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center gap-6", children: [
2811
- /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
2812
- /* @__PURE__ */ jsx("p", { className: "text-lg font-medium text-zinc-500 dark:text-zinc-400", children: "What would you like to know?" }),
2813
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-zinc-400 dark:text-zinc-600", children: "Ask a question about your data to get started" })
2901
+ /* @__PURE__ */ jsx(ScrollArea, { viewportRef: scrollRef, className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx(
2902
+ ErrorBoundary,
2903
+ {
2904
+ fallbackRender: (_error, reset) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-2 p-6 text-sm text-red-600 dark:text-red-400", children: [
2905
+ /* @__PURE__ */ jsx("p", { children: "Failed to render messages." }),
2906
+ /* @__PURE__ */ jsx(Button, { variant: "link", size: "sm", onClick: reset, className: "text-xs", children: "Try again" })
2814
2907
  ] }),
2815
- /* @__PURE__ */ jsx("div", { className: "grid w-full max-w-lg grid-cols-1 gap-2 sm:grid-cols-2", children: STARTER_PROMPTS.map((prompt) => /* @__PURE__ */ jsx(
2816
- "button",
2817
- {
2818
- onClick: () => handleSend(prompt),
2819
- className: "rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2.5 text-left text-sm text-zinc-500 transition-colors hover:border-zinc-400 hover:bg-zinc-100 hover:text-zinc-800 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400 dark:hover:border-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-200",
2820
- children: prompt
2821
- },
2822
- prompt
2823
- )) })
2824
- ] }),
2825
- messages.map((m, msgIndex) => {
2826
- if (m.role === "user") {
2827
- return /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx("div", { className: "max-w-[85%] rounded-xl bg-blue-600 px-4 py-3 text-sm text-white", children: m.parts?.map(
2828
- (part, i) => part.type === "text" ? /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap", children: part.text }, i) : null
2829
- ) }) }, m.id);
2830
- }
2831
- const isLastAssistant = m.role === "assistant" && msgIndex === messages.length - 1;
2832
- const lastTextWithSuggestions = m.parts?.filter((p) => p.type === "text" && !!p.text.trim()).findLast((p) => parseSuggestions(p.text).suggestions.length > 0);
2833
- const suggestions = lastTextWithSuggestions ? parseSuggestions(lastTextWithSuggestions.text).suggestions : [];
2834
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2835
- m.parts?.map((part, i) => {
2836
- if (part.type === "text" && part.text.trim()) {
2837
- const displayText = parseSuggestions(part.text).text;
2838
- if (!displayText.trim()) return null;
2839
- return /* @__PURE__ */ jsx("div", { className: "max-w-[90%]", children: /* @__PURE__ */ jsx("div", { className: "rounded-xl bg-zinc-100 px-4 py-3 text-sm text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200", children: /* @__PURE__ */ jsx(Markdown, { content: displayText }) }) }, i);
2840
- }
2841
- if (isToolUIPart(part)) {
2842
- return /* @__PURE__ */ jsx("div", { className: "max-w-[95%]", children: /* @__PURE__ */ jsx(ToolPart, { part, toolRenderers }) }, i);
2908
+ children: /* @__PURE__ */ jsxs("div", { "data-atlas-messages": true, className: "space-y-4 pb-4 pr-3", children: [
2909
+ messages.length === 0 && !error && /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center gap-6", children: [
2910
+ /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
2911
+ /* @__PURE__ */ jsx("p", { className: "text-lg font-medium text-zinc-500 dark:text-zinc-400", children: "What would you like to know?" }),
2912
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-zinc-500 dark:text-zinc-500", children: "Ask a question about your data to get started" })
2913
+ ] }),
2914
+ /* @__PURE__ */ jsx("div", { className: "grid w-full max-w-lg grid-cols-1 gap-2 sm:grid-cols-2", children: STARTER_PROMPTS.map((prompt) => /* @__PURE__ */ jsx(
2915
+ Button,
2916
+ {
2917
+ variant: "outline",
2918
+ onClick: () => handleSend(prompt),
2919
+ className: "h-auto whitespace-normal justify-start rounded-lg bg-zinc-50 px-3 py-2.5 text-left text-sm text-zinc-500 hover:text-zinc-800 dark:bg-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200",
2920
+ children: prompt
2921
+ },
2922
+ prompt
2923
+ )) })
2924
+ ] }),
2925
+ messages.map((m, msgIndex) => {
2926
+ if (m.role === "user") {
2927
+ return /* @__PURE__ */ jsx("div", { className: "flex justify-end", role: "article", "aria-label": "Message from you", children: /* @__PURE__ */ jsx("div", { className: "max-w-[85%] rounded-xl bg-blue-600 px-4 py-3 text-sm text-white", children: m.parts?.map(
2928
+ (part, i) => part.type === "text" ? /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap", children: part.text }, i) : null
2929
+ ) }) }, m.id);
2843
2930
  }
2844
- return null;
2931
+ const isLastAssistant = m.role === "assistant" && msgIndex === messages.length - 1;
2932
+ const hasVisibleParts = m.parts?.some(
2933
+ (p) => p.type === "text" && p.text.trim() || isToolUIPart(p)
2934
+ );
2935
+ if (!hasVisibleParts && !isLastAssistant) return null;
2936
+ const lastTextWithSuggestions = m.parts?.filter((p) => p.type === "text" && !!p.text.trim()).findLast((p) => parseSuggestions(p.text).suggestions.length > 0);
2937
+ const suggestions = lastTextWithSuggestions ? parseSuggestions(lastTextWithSuggestions.text).suggestions : [];
2938
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", role: "article", "aria-label": "Message from Atlas", children: [
2939
+ m.parts?.map((part, i) => {
2940
+ if (part.type === "text" && part.text.trim()) {
2941
+ const displayText = parseSuggestions(part.text).text;
2942
+ if (!displayText.trim()) return null;
2943
+ return /* @__PURE__ */ jsx("div", { className: "max-w-[90%]", children: /* @__PURE__ */ jsx("div", { className: "rounded-xl bg-zinc-100 px-4 py-3 text-sm text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200", children: /* @__PURE__ */ jsx(Markdown, { content: displayText }) }) }, i);
2944
+ }
2945
+ if (isToolUIPart(part)) {
2946
+ return /* @__PURE__ */ jsx("div", { className: "max-w-[95%]", children: /* @__PURE__ */ jsx(ToolPart, { part, toolRenderers }) }, i);
2947
+ }
2948
+ return null;
2949
+ }),
2950
+ isLastAssistant && !hasVisibleParts && !isLoading && error && /* @__PURE__ */ jsx("div", { className: "max-w-[90%]", children: /* @__PURE__ */ jsx("div", { className: "rounded-xl bg-red-50 px-4 py-3 text-sm text-red-700 dark:bg-red-950/30 dark:text-red-400", children: error.message ? `Something went wrong generating a response: ${error.message}. Try sending your message again.` : "Something went wrong generating a response. Try sending your message again." }) }),
2951
+ isLastAssistant && !isLoading && hasVisibleParts && /* @__PURE__ */ jsxs(Fragment, { children: [
2952
+ /* @__PURE__ */ jsx(
2953
+ FollowUpChips,
2954
+ {
2955
+ suggestions,
2956
+ onSelect: handleSend
2957
+ }
2958
+ ),
2959
+ conversationId && sidebar && convos.available && /* @__PURE__ */ jsx(
2960
+ SaveButton,
2961
+ {
2962
+ conversationId,
2963
+ conversations: convos.conversations,
2964
+ onStar: convos.starConversation
2965
+ }
2966
+ )
2967
+ ] })
2968
+ ] }, m.id);
2845
2969
  }),
2846
- isLastAssistant && !isLoading && /* @__PURE__ */ jsxs(Fragment, { children: [
2847
- /* @__PURE__ */ jsx(
2848
- FollowUpChips,
2849
- {
2850
- suggestions,
2851
- onSelect: handleSend
2852
- }
2853
- ),
2854
- conversationId && sidebar && convos.available && /* @__PURE__ */ jsx(
2855
- SaveButton,
2856
- {
2857
- conversationId,
2858
- conversations: convos.conversations,
2859
- onStar: convos.starConversation
2860
- }
2861
- )
2862
- ] })
2863
- ] }, m.id);
2864
- }),
2865
- isLoading && messages.length > 0 && /* @__PURE__ */ jsx(TypingIndicator, {})
2866
- ] }) }),
2867
- error && /* @__PURE__ */ jsx(ErrorBanner, { error, authMode }),
2970
+ isLoading && messages.length > 0 && /* @__PURE__ */ jsx(TypingIndicator, {})
2971
+ ] })
2972
+ }
2973
+ ) }),
2974
+ error && /* @__PURE__ */ jsx(
2975
+ ErrorBanner,
2976
+ {
2977
+ error,
2978
+ authMode: authMode ?? "none",
2979
+ onRetry: messages.some((m) => m.role === "user") ? () => {
2980
+ const lastUserMsg = messages.toReversed().find((m) => m.role === "user");
2981
+ const text = lastUserMsg?.parts?.filter((p) => p.type === "text").map((p) => p.text).join(" ");
2982
+ if (text) handleSend(text);
2983
+ } : void 0
2984
+ }
2985
+ ),
2868
2986
  /* @__PURE__ */ jsxs(
2869
2987
  "form",
2870
2988
  {
@@ -2876,22 +2994,24 @@ function AtlasChatInner({
2876
2994
  className: "flex flex-none gap-2 border-t border-zinc-100 pt-4 dark:border-zinc-800",
2877
2995
  children: [
2878
2996
  /* @__PURE__ */ jsx(
2879
- "input",
2997
+ Input,
2880
2998
  {
2881
2999
  "data-atlas-input": true,
2882
3000
  value: input,
2883
3001
  onChange: (e) => setInput(e.target.value),
2884
3002
  placeholder: "Ask a question about your data...",
2885
- className: "min-w-0 flex-1 rounded-lg border border-zinc-200 bg-zinc-50 px-4 py-3 text-base text-zinc-900 placeholder-zinc-400 outline-none focus:border-blue-500 sm:text-sm dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:placeholder-zinc-600",
2886
- disabled: isLoading || healthFailed
3003
+ className: "min-w-0 flex-1 py-3 text-base sm:text-sm",
3004
+ disabled: isLoading || healthFailed,
3005
+ "aria-label": "Chat message"
2887
3006
  }
2888
3007
  ),
2889
3008
  /* @__PURE__ */ jsx(
2890
- "button",
3009
+ Button,
2891
3010
  {
2892
3011
  type: "submit",
2893
- disabled: isLoading || healthFailed || !input.trim(),
2894
- className: "shrink-0 rounded-lg bg-blue-600 px-5 py-3 text-sm font-medium text-white transition-colors hover:bg-blue-500 disabled:opacity-40",
3012
+ disabled: isLoading || healthFailed,
3013
+ "aria-disabled": !(isLoading || healthFailed) && !input.trim() ? true : void 0,
3014
+ className: "shrink-0 px-5",
2895
3015
  children: "Ask"
2896
3016
  }
2897
3017
  )