@useatlas/react 0.0.1 → 0.0.2

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 +26 -16
  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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Sywulak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,7 +5,7 @@ Embeddable Atlas chat UI for React applications.
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @useatlas/react
8
+ bun add @useatlas/react
9
9
  ```
10
10
 
11
11
  ## Usage
@@ -83,6 +83,83 @@ Every renderer receives `ToolRendererProps<T>`:
83
83
  | `result` | `T` | Tool output. Built-in tool types include `\| null` for the loading state |
84
84
  | `isLoading` | `boolean` | Whether the tool invocation is in progress |
85
85
 
86
+ ## Error Handling
87
+
88
+ The widget handles errors at three levels — render errors (error boundary), API/network errors (error banner), and widget-level errors (postMessage). Each is automatic; you only need to listen if you want to react in your host application.
89
+
90
+ ### Error States
91
+
92
+ | Error | What the user sees | Auto-recovery |
93
+ |-------|-------------------|---------------|
94
+ | API unreachable | "Unable to connect to Atlas." with a retry button | No — user must retry |
95
+ | Auth failure | Auth-mode-specific message (e.g. "Your session has expired.") | No — requires re-auth |
96
+ | Offline | "You appear to be offline." | Yes — auto-retries when `navigator.onLine` restores |
97
+ | Rate limited | "Too many requests." with countdown timer | Yes — auto-retries after countdown |
98
+ | Server error (5xx) | "Something went wrong on our end." with retry button | No — user must retry |
99
+ | Render crash | "Something went wrong." with a try-again button (error boundary) | No — user must click retry |
100
+
101
+ ### Listening for Errors via postMessage
102
+
103
+ When embedded as an iframe, the widget emits `atlas:error` messages to the parent window on every error:
104
+
105
+ ```javascript
106
+ window.addEventListener("message", (event) => {
107
+ if (event.origin !== "https://your-atlas-api.example.com") return;
108
+
109
+ if (event.data?.type === "atlas:error") {
110
+ const { code, message, detail, retryable } = event.data.error;
111
+ // code: "api_unreachable" | "auth_failure" | "rate_limited_http" | "offline" | "server_error" | ChatErrorCode
112
+ // retryable: true for transient errors, false for permanent ones
113
+ console.error(`[Atlas] ${code}: ${message}`);
114
+ }
115
+ });
116
+ ```
117
+
118
+ ### Listening for Errors via the Programmatic API
119
+
120
+ When using the script tag loader, use `Atlas.on("error", ...)`:
121
+
122
+ ```javascript
123
+ Atlas.on("error", (detail) => {
124
+ // detail: { code?: string, message?: string }
125
+ console.error("Widget error:", detail.code, detail.message);
126
+ });
127
+ ```
128
+
129
+ ### Auth Token Refresh
130
+
131
+ When a managed auth session expires mid-conversation, the widget shows "Your session has expired. Please sign in again." For iframe embeds using external tokens (BYOT mode), refresh the token via postMessage:
132
+
133
+ ```javascript
134
+ // When your app refreshes a token, push it to the widget
135
+ function onTokenRefresh(newToken) {
136
+ const iframe = document.querySelector("iframe");
137
+ iframe.contentWindow.postMessage(
138
+ { type: "auth", token: newToken },
139
+ "https://your-atlas-api.example.com",
140
+ );
141
+ }
142
+ ```
143
+
144
+ For the script tag loader:
145
+
146
+ ```javascript
147
+ Atlas.setAuthToken(newToken);
148
+ ```
149
+
150
+ ### CSP Configuration
151
+
152
+ If your site uses a Content Security Policy, add the Atlas API domain to these directives:
153
+
154
+ ```
155
+ Content-Security-Policy:
156
+ script-src 'self' https://your-atlas-api.example.com;
157
+ frame-src 'self' https://your-atlas-api.example.com;
158
+ connect-src 'self' https://your-atlas-api.example.com;
159
+ ```
160
+
161
+ See the [Embedding Widget guide](https://docs.useatlas.dev/guides/embedding-widget#content-security-policy-csp) for full CSP details.
162
+
86
163
  ## Headless Hooks
87
164
 
88
165
  For fully custom UIs, use the hooks entry point. Tool renderer types are also available here:
@@ -92,4 +169,4 @@ import { AtlasProvider, useAtlasChat } from "@useatlas/react/hooks";
92
169
  import type { ToolRendererProps, SQLToolResult } from "@useatlas/react/hooks";
93
170
  ```
94
171
 
95
- See the [hooks documentation](https://docs.useatlas.dev) for details.
172
+ See the [hooks documentation](https://docs.useatlas.dev/reference/react) for details.
@@ -1,4 +1,9 @@
1
- 'use strict';
1
+ import { Component } from 'react';
2
+ import { cva } from 'class-variance-authority';
3
+ import { Slot } from 'radix-ui';
4
+ import { clsx } from 'clsx';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
7
 
3
8
  // src/components/chart/chart-detection.ts
4
9
  var CHART_COLORS_LIGHT = [
@@ -220,10 +225,98 @@ function transformData(rows, recommendation) {
220
225
  return record;
221
226
  });
222
227
  }
228
+ function cn(...inputs) {
229
+ return twMerge(clsx(inputs));
230
+ }
231
+ var buttonVariants = cva(
232
+ "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",
233
+ {
234
+ variants: {
235
+ variant: {
236
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
237
+ destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
238
+ 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",
239
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
240
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
241
+ link: "text-primary underline-offset-4 hover:underline"
242
+ },
243
+ size: {
244
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
245
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
246
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
247
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
248
+ icon: "size-9",
249
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
250
+ "icon-sm": "size-8",
251
+ "icon-lg": "size-10"
252
+ }
253
+ },
254
+ defaultVariants: {
255
+ variant: "default",
256
+ size: "default"
257
+ }
258
+ }
259
+ );
260
+ function Button({
261
+ className,
262
+ variant = "default",
263
+ size = "default",
264
+ asChild = false,
265
+ ...props
266
+ }) {
267
+ const Comp = asChild ? Slot.Root : "button";
268
+ return /* @__PURE__ */ jsx(
269
+ Comp,
270
+ {
271
+ "data-slot": "button",
272
+ "data-variant": variant,
273
+ "data-size": size,
274
+ className: cn(buttonVariants({ variant, size, className })),
275
+ ...props
276
+ }
277
+ );
278
+ }
279
+ var ErrorBoundary = class extends Component {
280
+ constructor(props) {
281
+ super(props);
282
+ this.state = { error: null };
283
+ }
284
+ static getDerivedStateFromError(error) {
285
+ return { error };
286
+ }
287
+ componentDidCatch(error, info) {
288
+ console.error("[ErrorBoundary]", error, info.componentStack);
289
+ this.props.onError?.(error, info);
290
+ }
291
+ resetErrorBoundary = () => {
292
+ this.setState({ error: null });
293
+ };
294
+ render() {
295
+ const { error } = this.state;
296
+ if (!error) return this.props.children;
297
+ if (this.props.fallbackRender) {
298
+ return this.props.fallbackRender(error, this.resetErrorBoundary);
299
+ }
300
+ if (this.props.fallback) {
301
+ return this.props.fallback;
302
+ }
303
+ return /* @__PURE__ */ jsxs(
304
+ "div",
305
+ {
306
+ role: "alert",
307
+ className: cn(
308
+ "flex flex-col items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-4 text-center",
309
+ "dark:border-red-900/50 dark:bg-red-950/20"
310
+ ),
311
+ children: [
312
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-red-700 dark:text-red-400", children: "Something went wrong." }),
313
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: this.resetErrorBoundary, children: "Try again" })
314
+ ]
315
+ }
316
+ );
317
+ }
318
+ };
223
319
 
224
- exports.CHART_COLORS_DARK = CHART_COLORS_DARK;
225
- exports.CHART_COLORS_LIGHT = CHART_COLORS_LIGHT;
226
- exports.detectCharts = detectCharts;
227
- exports.transformData = transformData;
228
- //# sourceMappingURL=chunk-5SEVKHS5.cjs.map
229
- //# sourceMappingURL=chunk-5SEVKHS5.cjs.map
320
+ export { Button, CHART_COLORS_DARK, CHART_COLORS_LIGHT, ErrorBoundary, cn, detectCharts, transformData };
321
+ //# sourceMappingURL=chunk-35SCTKSW.js.map
322
+ //# sourceMappingURL=chunk-35SCTKSW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/chart/chart-detection.ts","../src/lib/utils.ts","../src/components/ui/button.tsx","../src/components/error-boundary.tsx"],"names":["jsx"],"mappings":"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","file":"chunk-35SCTKSW.js","sourcesContent":["/* Chart detection — pure functions, zero React deps. Kept framework-agnostic for direct unit testing. */\n\nexport type ColumnType = \"numeric\" | \"date\" | \"categorical\" | \"unknown\";\n\nexport type ClassifiedColumn = {\n index: number;\n header: string;\n type: ColumnType;\n uniqueCount: number;\n};\n\nexport type ChartType = \"bar\" | \"line\" | \"pie\" | \"area\" | \"stacked-bar\" | \"scatter\";\n\nexport type ChartRecommendation = {\n type: ChartType;\n categoryColumn: ClassifiedColumn;\n valueColumns: [ClassifiedColumn, ...ClassifiedColumn[]];\n reason: string;\n};\n\nexport type RechartsRow = Record<string, string | number>;\n\ntype NonChartableResult = {\n chartable: false;\n columns: ClassifiedColumn[];\n};\n\ntype ChartableResult = {\n chartable: true;\n columns: ClassifiedColumn[];\n recommendations: [ChartRecommendation, ...ChartRecommendation[]];\n data: RechartsRow[];\n};\n\nexport type ChartDetectionResult = NonChartableResult | ChartableResult;\n\n/* ------------------------------------------------------------------ */\n/* Color palettes (Tailwind weights) */\n/* ------------------------------------------------------------------ */\n\nexport const CHART_COLORS_LIGHT = [\n \"#3b82f6\", // blue-500\n \"#10b981\", // emerald-500\n \"#f59e0b\", // amber-500\n \"#ef4444\", // red-500\n \"#8b5cf6\", // violet-500\n \"#06b6d4\", // cyan-500\n \"#f97316\", // orange-500\n \"#ec4899\", // pink-500\n];\n\nexport const CHART_COLORS_DARK = [\n \"#60a5fa\", // blue-400\n \"#34d399\", // emerald-400\n \"#fbbf24\", // amber-400\n \"#f87171\", // red-400\n \"#a78bfa\", // violet-400\n \"#22d3ee\", // cyan-400\n \"#fb923c\", // orange-400\n \"#f472b6\", // pink-400\n];\n\n/* ------------------------------------------------------------------ */\n/* Column classification */\n/* ------------------------------------------------------------------ */\n\nconst DATE_HEADER_HINTS = /^(date|month|year|quarter|week|day|period|time|timestamp)$/i;\nconst CATEGORICAL_HEADER_HINTS = /^(name|type|category|status|region|country|industry|department|plan|tier|segment|group|label|source|channel)$/i;\nconst SKIP_HEADER_HINTS = /^(id|uuid|_id|pk|key)$/i;\n\nconst ISO_DATE_RE = /^\\d{4}-\\d{2}/;\nconst MONTH_NAME_RE = /^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i;\nconst YEAR_ONLY_RE = /^(19|20)\\d{2}$/;\nconst QUARTER_RE = /^Q[1-4]\\s*\\d{4}$/i;\n\nexport function classifyColumn(header: string, values: string[]): ColumnType {\n const nonEmpty = values.filter((v) => v !== \"\" && v != null);\n if (nonEmpty.length === 0) return \"unknown\";\n\n // Header hint: skip ID-like columns\n if (SKIP_HEADER_HINTS.test(header)) return \"unknown\";\n\n // Numeric check: >80% parse as finite numbers (date check takes priority for overlapping values)\n const numericCount = nonEmpty.filter((v) => {\n const n = Number(v.replace(/,/g, \"\"));\n return isFinite(n);\n }).length;\n const numericRatio = numericCount / nonEmpty.length;\n\n // Date check: >70% match date patterns (>30% when header hints match)\n const dateCount = nonEmpty.filter(\n (v) => ISO_DATE_RE.test(v) || MONTH_NAME_RE.test(v) || YEAR_ONLY_RE.test(v) || QUARTER_RE.test(v),\n ).length;\n const dateRatio = dateCount / nonEmpty.length;\n\n // Header hint tiebreaker: if header matches date keywords...\n // (a) ...and at least some values look date-like, trust the header\n // (b) ...and values aren't overwhelmingly numeric (catches year-only values)\n if (DATE_HEADER_HINTS.test(header) && dateRatio > 0.3) return \"date\";\n if (DATE_HEADER_HINTS.test(header) && numericRatio < 0.9) return \"date\";\n\n if (dateRatio > 0.7) return \"date\";\n if (numericRatio > 0.8) return \"numeric\";\n\n // Categorical header hint\n if (CATEGORICAL_HEADER_HINTS.test(header)) return \"categorical\";\n\n // Categorical fallback: text values with <50 unique entries (higher cardinality suggests free-text or IDs)\n const unique = new Set(nonEmpty);\n if (unique.size < 50) return \"categorical\";\n\n return \"unknown\";\n}\n\n/* ------------------------------------------------------------------ */\n/* Chart recommendation engine */\n/* ------------------------------------------------------------------ */\n\nexport function detectCharts(headers: string[], rows: string[][]): ChartDetectionResult {\n if (headers.length === 0 || rows.length < 2) {\n return { chartable: false, columns: [] };\n }\n\n // Deduplicate headers so chart dataKey matches transformed data keys\n const seen = new Map<string, number>();\n const dedupedHeaders = headers.map((h) => {\n const count = seen.get(h) ?? 0;\n seen.set(h, count + 1);\n return count > 0 ? `${h}_${count + 1}` : h;\n });\n\n const columns: ClassifiedColumn[] = dedupedHeaders.map((header, index) => {\n const values = rows.map((r) => r[index] ?? \"\");\n const type = classifyColumn(header, values);\n const uniqueCount = new Set(values.filter((v) => v !== \"\")).size;\n return { index, header, type, uniqueCount };\n });\n\n const dateColumns = columns.filter((c) => c.type === \"date\");\n const numericColumns = columns.filter((c) => c.type === \"numeric\");\n const categoricalColumns = columns.filter((c) => c.type === \"categorical\");\n\n if (numericColumns.length === 0) {\n return { chartable: false, columns };\n }\n\n const recommendations: ChartRecommendation[] = [];\n\n // Line: date + numeric (time-series, highest priority)\n if (dateColumns.length >= 1 && numericColumns.length >= 1) {\n recommendations.push({\n type: \"line\",\n categoryColumn: dateColumns[0],\n valueColumns: numericColumns as [ClassifiedColumn, ...ClassifiedColumn[]],\n reason: `Time-series: ${dateColumns[0].header} vs ${numericColumns.map((c) => c.header).join(\", \")}`,\n });\n }\n\n // Area: alternative to line for date + numeric (volume/magnitude over time)\n if (dateColumns.length >= 1 && numericColumns.length >= 1) {\n recommendations.push({\n type: \"area\",\n categoryColumn: dateColumns[0],\n valueColumns: numericColumns as [ClassifiedColumn, ...ClassifiedColumn[]],\n reason: `Volume over time: ${numericColumns.map((c) => c.header).join(\", \")} by ${dateColumns[0].header}`,\n });\n }\n\n // Stacked bar: categorical + multiple numeric columns (part-to-whole comparison)\n if (categoricalColumns.length >= 1 && numericColumns.length >= 2) {\n recommendations.push({\n type: \"stacked-bar\",\n categoryColumn: categoricalColumns[0],\n valueColumns: numericColumns as [ClassifiedColumn, ...ClassifiedColumn[]],\n reason: `Stacked: ${numericColumns.map((c) => c.header).join(\", \")} by ${categoricalColumns[0].header}`,\n });\n }\n\n // Bar: categorical + numeric\n if (categoricalColumns.length >= 1 && numericColumns.length >= 1) {\n recommendations.push({\n type: \"bar\",\n categoryColumn: categoricalColumns[0],\n valueColumns: numericColumns as [ClassifiedColumn, ...ClassifiedColumn[]],\n reason: `Comparison: ${numericColumns.map((c) => c.header).join(\", \")} by ${categoricalColumns[0].header}`,\n });\n }\n\n // Pie: first categorical column (2-7 unique values) + first numeric column\n if (categoricalColumns.length >= 1 && numericColumns.length >= 1) {\n const cat = categoricalColumns[0];\n if (cat.uniqueCount >= 2 && cat.uniqueCount <= 7) {\n recommendations.push({\n type: \"pie\",\n categoryColumn: cat,\n valueColumns: [numericColumns[0]],\n reason: `Distribution: ${numericColumns[0].header} by ${cat.header}`,\n });\n }\n }\n\n // Scatter: 2+ numeric columns (correlation analysis)\n if (numericColumns.length >= 2) {\n const [xCol, yCol, ...rest] = numericColumns;\n const scatterValues = rest.length > 0\n ? [yCol, ...rest] as [ClassifiedColumn, ...ClassifiedColumn[]]\n : [yCol] as [ClassifiedColumn, ...ClassifiedColumn[]];\n recommendations.push({\n type: \"scatter\",\n categoryColumn: xCol,\n valueColumns: scatterValues,\n reason: `Correlation: ${xCol.header} vs ${yCol.header}${rest.length > 0 ? ` (size: ${rest[0].header})` : \"\"}`,\n });\n }\n\n // Fallback: when all columns are numeric, treat first as category axis (often an index or bucket label)\n if (!recommendations.some((r) => r.type === \"bar\") && numericColumns.length >= 2) {\n const first = columns[0];\n const rest = numericColumns.filter((c) => c.index !== first.index);\n if (rest.length >= 1) {\n recommendations.push({\n type: \"bar\",\n categoryColumn: first,\n valueColumns: rest as [ClassifiedColumn, ...ClassifiedColumn[]],\n reason: `Fallback: ${rest.map((c) => c.header).join(\", \")} by ${first.header}`,\n });\n }\n }\n\n // Also allow bar for date columns (as a secondary option after line)\n if (dateColumns.length >= 1 && numericColumns.length >= 1 && !recommendations.some((r) => r.type === \"bar\")) {\n recommendations.push({\n type: \"bar\",\n categoryColumn: dateColumns[0],\n valueColumns: numericColumns as [ClassifiedColumn, ...ClassifiedColumn[]],\n reason: `Comparison: ${numericColumns.map((c) => c.header).join(\", \")} by ${dateColumns[0].header}`,\n });\n }\n\n if (recommendations.length === 0) {\n return { chartable: false, columns };\n }\n\n const data = transformData(rows, recommendations[0]);\n\n return {\n chartable: true,\n columns,\n recommendations: recommendations as [ChartRecommendation, ...ChartRecommendation[]],\n data,\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Data transform */\n/* ------------------------------------------------------------------ */\n\nfunction parseNumericValue(raw: string): number {\n const cleaned = raw.replace(/[$%,\\s]/g, \"\");\n if (cleaned === \"\" || cleaned === \"-\") return 0;\n const num = Number(cleaned);\n return isFinite(num) ? num : 0;\n}\n\nfunction isFiniteNumeric(raw: string): boolean {\n const cleaned = raw.replace(/[$%,\\s]/g, \"\");\n if (cleaned === \"\" || cleaned === \"-\") return false;\n return isFinite(Number(cleaned));\n}\n\nexport function transformData(\n rows: string[][],\n recommendation: ChartRecommendation,\n): RechartsRow[] {\n const catIdx = recommendation.categoryColumn.index;\n const catHeader = recommendation.categoryColumn.header;\n const valIdxs = recommendation.valueColumns.map((c) => c.index);\n\n // Scatter: both axes are numeric — categoryColumn is x, first valueColumn is y, optional z for size\n // Filter out rows where x or y are non-numeric to avoid misleading zero-origin clusters\n if (recommendation.type === \"scatter\") {\n const yIdx = recommendation.valueColumns[0].index;\n return rows.flatMap((row) => {\n const rawX = row[catIdx] ?? \"\";\n const rawY = row[yIdx] ?? \"\";\n if (!isFiniteNumeric(rawX) || !isFiniteNumeric(rawY)) return [];\n const record: RechartsRow = {};\n record[catHeader] = parseNumericValue(rawX);\n for (const vc of recommendation.valueColumns) {\n record[vc.header] = parseNumericValue(row[vc.index] ?? \"0\");\n }\n return [record];\n });\n }\n\n // Cap rows for bar/stacked-bar charts with many categories\n let effectiveRows = rows;\n if ((recommendation.type === \"bar\" || recommendation.type === \"stacked-bar\") && rows.length > 30) {\n // Sort by first value column descending, take top 20\n const valIdx = valIdxs[0];\n effectiveRows = [...rows]\n .sort((a, b) => {\n const av = parseNumericValue(a[valIdx] ?? \"0\");\n const bv = parseNumericValue(b[valIdx] ?? \"0\");\n return bv - av;\n })\n .slice(0, 20);\n }\n\n return effectiveRows.map((row) => {\n const record: RechartsRow = {};\n record[catHeader] = row[catIdx] ?? \"\";\n for (const vc of recommendation.valueColumns) {\n record[vc.header] = parseNumericValue(row[vc.index] ?? \"0\");\n }\n return record;\n });\n}\n","import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n","import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"../../lib/utils\"\n\nconst buttonVariants = cva(\n \"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\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nfunction Button({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean\n }) {\n const Comp = asChild ? Slot.Root : \"button\"\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n />\n )\n}\n\nexport { Button, buttonVariants }\n","\"use client\";\n\nimport { Component, type ReactNode, type ErrorInfo } from \"react\";\nimport { Button } from \"./ui/button\";\nimport { cn } from \"../lib/utils\";\n\ninterface ErrorBoundaryProps {\n children: ReactNode;\n fallback?: ReactNode;\n fallbackRender?: (error: Error, reset: () => void) => ReactNode;\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n}\n\ninterface ErrorBoundaryState {\n error: Error | null;\n}\n\nexport class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {\n constructor(props: ErrorBoundaryProps) {\n super(props);\n this.state = { error: null };\n }\n\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n return { error };\n }\n\n componentDidCatch(error: Error, info: ErrorInfo) {\n console.error(\"[ErrorBoundary]\", error, info.componentStack);\n this.props.onError?.(error, info);\n }\n\n resetErrorBoundary = () => {\n this.setState({ error: null });\n };\n\n render() {\n const { error } = this.state;\n if (!error) return this.props.children;\n\n if (this.props.fallbackRender) {\n return this.props.fallbackRender(error, this.resetErrorBoundary);\n }\n\n if (this.props.fallback) {\n return this.props.fallback;\n }\n\n return (\n <div\n role=\"alert\"\n className={cn(\n \"flex flex-col items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-4 text-center\",\n \"dark:border-red-900/50 dark:bg-red-950/20\",\n )}\n >\n <p className=\"text-sm text-red-700 dark:text-red-400\">\n Something went wrong.\n </p>\n <Button variant=\"outline\" size=\"sm\" onClick={this.resetErrorBoundary}>\n Try again\n </Button>\n </div>\n );\n }\n}\n"]}
@@ -95,11 +95,11 @@ function useThemeMode() {
95
95
  }
96
96
  function transformMessages(messages) {
97
97
  return messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => {
98
- const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
98
+ const parts = Array.isArray(m.content) ? m.content.filter((p) => p.type === "text").map((p) => ({ type: "text", text: p.text ?? "" })) : [{ type: "text", text: String(m.content) }];
99
99
  return {
100
100
  id: m.id,
101
101
  role: m.role,
102
- parts: [{ type: "text", text: content }]
102
+ parts
103
103
  };
104
104
  });
105
105
  }
@@ -108,14 +108,16 @@ function useConversations(opts) {
108
108
  const [total, setTotal] = react.useState(0);
109
109
  const [loading, setLoading] = react.useState(false);
110
110
  const [available, setAvailable] = react.useState(true);
111
+ const [fetchError, setFetchError] = react.useState(null);
111
112
  const [selectedId, setSelectedId] = react.useState(null);
112
113
  const fetchedRef = react.useRef(false);
113
- const networkFailRef = react.useRef(0);
114
+ const baseEndpoint = opts.conversationsEndpoint ?? "/api/v1/conversations";
114
115
  const fetchList = react.useCallback(async () => {
115
116
  if (!opts.enabled || !available) return;
116
117
  setLoading(true);
118
+ setFetchError(null);
117
119
  try {
118
- const res = await fetch(`${opts.apiUrl}/api/v1/conversations?limit=50`, {
120
+ const res = await fetch(`${opts.apiUrl}${baseEndpoint}?limit=50`, {
119
121
  headers: opts.getHeaders(),
120
122
  credentials: opts.getCredentials()
121
123
  });
@@ -130,6 +132,7 @@ function useConversations(opts) {
130
132
  return;
131
133
  }
132
134
  console.warn(`fetchList: HTTP ${res.status}`, errorBody);
135
+ setFetchError("Failed to load conversations. Please reload the page to try again.");
133
136
  return;
134
137
  }
135
138
  const data = await res.json();
@@ -137,58 +140,45 @@ function useConversations(opts) {
137
140
  setTotal(data.total ?? 0);
138
141
  fetchedRef.current = true;
139
142
  } catch (err) {
140
- console.warn("fetchList error:", err);
141
- if (!fetchedRef.current) {
142
- networkFailRef.current += 1;
143
- if (networkFailRef.current >= 3) setAvailable(false);
144
- }
143
+ console.warn("fetchList error:", err instanceof Error ? err.message : String(err));
144
+ setFetchError("Failed to load conversations. Please reload the page to try again.");
145
145
  } finally {
146
146
  setLoading(false);
147
147
  }
148
- }, [opts.apiUrl, opts.enabled, opts.getHeaders, opts.getCredentials, available]);
148
+ }, [opts.apiUrl, opts.enabled, opts.getHeaders, opts.getCredentials, available, baseEndpoint]);
149
149
  const loadConversation = react.useCallback(async (id) => {
150
- try {
151
- const res = await fetch(`${opts.apiUrl}/api/v1/conversations/${id}`, {
152
- headers: opts.getHeaders(),
153
- credentials: opts.getCredentials()
154
- });
155
- if (!res.ok) {
156
- console.warn(`loadConversation: HTTP ${res.status} for ${id}`);
157
- return null;
158
- }
159
- const data = await res.json();
160
- return transformMessages(data.messages);
161
- } catch (err) {
162
- console.warn("loadConversation error:", err);
163
- return null;
150
+ const res = await fetch(`${opts.apiUrl}${baseEndpoint}/${id}`, {
151
+ headers: opts.getHeaders(),
152
+ credentials: opts.getCredentials()
153
+ });
154
+ if (!res.ok) {
155
+ console.warn(`loadConversation: HTTP ${res.status} for ${id}`);
156
+ throw new Error(`Failed to load conversation (HTTP ${res.status})`);
164
157
  }
165
- }, [opts.apiUrl, opts.getHeaders, opts.getCredentials]);
158
+ const data = await res.json();
159
+ return transformMessages(data.messages);
160
+ }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, baseEndpoint]);
166
161
  const deleteConversation = react.useCallback(async (id) => {
167
- try {
168
- const res = await fetch(`${opts.apiUrl}/api/v1/conversations/${id}`, {
169
- method: "DELETE",
170
- headers: opts.getHeaders(),
171
- credentials: opts.getCredentials()
172
- });
173
- if (!res.ok) {
174
- console.warn(`deleteConversation: HTTP ${res.status} for ${id}`);
175
- return false;
176
- }
177
- setConversations((prev) => prev.filter((c) => c.id !== id));
178
- setTotal((prev) => Math.max(0, prev - 1));
179
- if (selectedId === id) setSelectedId(null);
180
- return true;
181
- } catch (err) {
182
- console.warn("deleteConversation error:", err);
183
- return false;
162
+ const res = await fetch(`${opts.apiUrl}${baseEndpoint}/${id}`, {
163
+ method: "DELETE",
164
+ headers: opts.getHeaders(),
165
+ credentials: opts.getCredentials()
166
+ });
167
+ if (!res.ok) {
168
+ console.warn(`deleteConversation: HTTP ${res.status} for ${id}`);
169
+ throw new Error(`Failed to delete conversation (HTTP ${res.status})`);
184
170
  }
185
- }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, selectedId]);
171
+ setConversations((prev) => prev.filter((c) => c.id !== id));
172
+ setTotal((prev) => Math.max(0, prev - 1));
173
+ if (selectedId === id) setSelectedId(null);
174
+ }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, selectedId, baseEndpoint]);
186
175
  const starConversation = react.useCallback(async (id, starred) => {
187
176
  setConversations(
188
177
  (prev) => prev.map((c) => c.id === id ? { ...c, starred } : c)
189
178
  );
179
+ let rolledBack = false;
190
180
  try {
191
- const res = await fetch(`${opts.apiUrl}/api/v1/conversations/${id}/star`, {
181
+ const res = await fetch(`${opts.apiUrl}${baseEndpoint}/${id}/star`, {
192
182
  method: "PATCH",
193
183
  headers: { ...opts.getHeaders(), "Content-Type": "application/json" },
194
184
  credentials: opts.getCredentials(),
@@ -199,17 +189,18 @@ function useConversations(opts) {
199
189
  setConversations(
200
190
  (prev) => prev.map((c) => c.id === id ? { ...c, starred: !starred } : c)
201
191
  );
202
- return false;
192
+ rolledBack = true;
193
+ throw new Error(`Failed to update star (HTTP ${res.status})`);
203
194
  }
204
- return true;
205
195
  } catch (err) {
206
- console.warn("starConversation error:", err);
207
- setConversations(
208
- (prev) => prev.map((c) => c.id === id ? { ...c, starred: !starred } : c)
209
- );
210
- return false;
196
+ if (!rolledBack) {
197
+ setConversations(
198
+ (prev) => prev.map((c) => c.id === id ? { ...c, starred: !starred } : c)
199
+ );
200
+ }
201
+ throw err;
211
202
  }
212
- }, [opts.apiUrl, opts.getHeaders, opts.getCredentials]);
203
+ }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, baseEndpoint]);
213
204
  const refresh = react.useCallback(async () => {
214
205
  await fetchList();
215
206
  }, [fetchList]);
@@ -218,6 +209,7 @@ function useConversations(opts) {
218
209
  total,
219
210
  loading,
220
211
  available,
212
+ fetchError,
221
213
  selectedId,
222
214
  setSelectedId,
223
215
  fetchList,
@@ -245,5 +237,5 @@ exports.setTheme = setTheme;
245
237
  exports.useConversations = useConversations;
246
238
  exports.useDarkMode = useDarkMode;
247
239
  exports.useThemeMode = useThemeMode;
248
- //# sourceMappingURL=chunk-UIRB6L36.cjs.map
249
- //# sourceMappingURL=chunk-UIRB6L36.cjs.map
240
+ //# sourceMappingURL=chunk-DZFSZSQB.cjs.map
241
+ //# sourceMappingURL=chunk-DZFSZSQB.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/theme-init-script.ts","../src/hooks/use-dark-mode.ts","../src/hooks/use-conversations.ts"],"names":["createContext","useSyncExternalStore","useState","useRef","useCallback"],"mappings":"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","file":"chunk-DZFSZSQB.cjs","sourcesContent":["/**\n * Blocking inline script for layout.tsx — prevents dark-mode flash on load.\n *\n * This file intentionally has NO \"use client\" directive so it can be imported\n * by server components (layout.tsx). The storage key must stay in sync with\n * use-dark-mode.ts.\n */\n\nexport const THEME_STORAGE_KEY = \"atlas-theme\";\n\n/**\n * Returns the inline script string for the blocking `<script>` in layout.tsx.\n * Reads atlas-theme from localStorage and sets the `dark` class before first paint.\n */\nexport function buildThemeInitScript(): string {\n return `try{var t=localStorage.getItem(\"${THEME_STORAGE_KEY}\");var d=t===\"dark\"||(t!==\"light\"&&window.matchMedia(\"(prefers-color-scheme:dark)\").matches);if(d)document.documentElement.classList.add(\"dark\")}catch(e){}`;\n}\n","\"use client\";\n\nimport { createContext, useSyncExternalStore } from \"react\";\n\n// ---------------------------------------------------------------------------\n// Theme types & constants\n// ---------------------------------------------------------------------------\n\nimport { THEME_STORAGE_KEY } from \"./theme-init-script\";\n\nexport type ThemeMode = \"light\" | \"dark\" | \"system\";\n\nexport { THEME_STORAGE_KEY };\n\n/**\n * Default brand color — must match `brand.css` `:root { --atlas-brand }` and\n * the `ATLAS_BRAND_COLOR` default in `packages/api/src/lib/settings.ts`.\n */\nexport const DEFAULT_BRAND_COLOR = \"oklch(0.759 0.148 167.71)\";\n\n/** Basic oklch format check — prevents obviously invalid values from breaking the theme. */\nexport const OKLCH_RE = /^oklch\\(\\s*[\\d.]+\\s+[\\d.]+\\s+[\\d.]+\\s*(?:\\/\\s*[\\d.%]+\\s*)?\\)$/;\n\n// ---------------------------------------------------------------------------\n// Shared state — single source of truth for the chosen mode.\n// Listeners are notified on change so useSyncExternalStore re-renders.\n// ---------------------------------------------------------------------------\n\nlet _mode: ThemeMode = \"system\";\nconst _listeners = new Set<() => void>();\n\nfunction notify() {\n for (const fn of _listeners) fn();\n}\n\n/** Read stored preference (called once on module load in the browser). */\nfunction init() {\n try {\n const stored = localStorage.getItem(THEME_STORAGE_KEY);\n if (stored === \"light\" || stored === \"dark\" || stored === \"system\") {\n _mode = stored;\n }\n } catch (err) {\n console.warn(\"Could not read theme preference from localStorage:\", err);\n }\n}\n\nif (typeof window !== \"undefined\") init();\n\n// ---------------------------------------------------------------------------\n// Derived boolean: is the effective theme dark?\n// ---------------------------------------------------------------------------\n\nfunction systemPrefersDark(): boolean {\n return typeof window !== \"undefined\" && window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n}\n\nfunction resolveIsDark(mode: ThemeMode): boolean {\n if (mode === \"dark\") return true;\n if (mode === \"light\") return false;\n return systemPrefersDark();\n}\n\nfunction applyClass(isDark: boolean) {\n if (typeof document === \"undefined\") return;\n document.documentElement.classList.toggle(\"dark\", isDark);\n}\n\n// ---------------------------------------------------------------------------\n// External store for isDark (reacts to both mode changes AND system changes)\n// ---------------------------------------------------------------------------\n\nfunction subscribeIsDark(onChange: () => void) {\n _listeners.add(onChange);\n\n // Also listen for system preference changes (relevant when mode === \"system\").\n // Apply dark class immediately on OS change so the DOM stays in sync before React re-renders.\n const mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const handler = () => {\n applyClass(resolveIsDark(_mode));\n onChange();\n };\n mq.addEventListener(\"change\", handler);\n\n return () => {\n _listeners.delete(onChange);\n mq.removeEventListener(\"change\", handler);\n };\n}\n\nfunction getSnapshotIsDark() {\n return resolveIsDark(_mode);\n}\n\nfunction getServerSnapshotIsDark() {\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// External store for mode\n// ---------------------------------------------------------------------------\n\nfunction subscribeMode(onChange: () => void) {\n _listeners.add(onChange);\n return () => {\n _listeners.delete(onChange);\n };\n}\n\nfunction getSnapshotMode() {\n return _mode;\n}\n\nfunction getServerSnapshotMode(): ThemeMode {\n return \"system\";\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport function setTheme(mode: ThemeMode) {\n _mode = mode;\n const isDark = resolveIsDark(mode);\n applyClass(isDark);\n try {\n localStorage.setItem(THEME_STORAGE_KEY, mode);\n } catch (err) {\n console.warn(\"Could not persist theme preference to localStorage:\", err);\n }\n notify();\n}\n\n/** Apply --atlas-brand on :root so theme tokens update without reload. */\nexport function applyBrandColor(color: string) {\n if (typeof document === \"undefined\") return;\n document.documentElement.style.setProperty(\"--atlas-brand\", color);\n}\n\nexport const DarkModeContext = createContext(false);\n\n/** Returns whether the effective theme is dark. */\nexport function useDarkMode(): boolean {\n return useSyncExternalStore(subscribeIsDark, getSnapshotIsDark, getServerSnapshotIsDark);\n}\n\n/** Returns the current ThemeMode (\"light\" | \"dark\" | \"system\"). */\nexport function useThemeMode(): ThemeMode {\n return useSyncExternalStore(subscribeMode, getSnapshotMode, getServerSnapshotMode);\n}\n","\"use client\";\n\nimport { useState, useCallback, useRef } from \"react\";\nimport type { Conversation, ConversationWithMessages, Message } from \"../lib/types\";\nimport type { UIMessage } from \"@ai-sdk/react\";\n\nexport interface UseConversationsOptions {\n apiUrl: string;\n enabled: boolean;\n getHeaders: () => Record<string, string>;\n getCredentials: () => RequestCredentials;\n /** Custom conversations API endpoint path. Defaults to \"/api/v1/conversations\". */\n conversationsEndpoint?: string;\n}\n\nexport interface UseConversationsReturn {\n conversations: Conversation[];\n total: number;\n loading: boolean;\n available: boolean;\n fetchError: string | null;\n selectedId: string | null;\n setSelectedId: (id: string | null) => void;\n fetchList: () => Promise<void>;\n loadConversation: (id: string) => Promise<UIMessage[]>;\n deleteConversation: (id: string) => Promise<void>;\n starConversation: (id: string, starred: boolean) => Promise<void>;\n refresh: () => Promise<void>;\n}\n\nexport function transformMessages(messages: Message[]): UIMessage[] {\n return messages\n .filter((m) => m.role === \"user\" || m.role === \"assistant\")\n .map((m) => {\n const parts: UIMessage[\"parts\"] = Array.isArray(m.content)\n ? m.content\n .filter((p: { type?: string }) => p.type === \"text\")\n .map((p: { text?: string }) => ({ type: \"text\" as const, text: p.text ?? \"\" }))\n : [{ type: \"text\" as const, text: String(m.content) }];\n\n return {\n id: m.id,\n role: m.role as \"user\" | \"assistant\",\n parts,\n };\n });\n}\n\nexport function useConversations(opts: UseConversationsOptions): UseConversationsReturn {\n const [conversations, setConversations] = useState<Conversation[]>([]);\n const [total, setTotal] = useState(0);\n const [loading, setLoading] = useState(false);\n const [available, setAvailable] = useState(true);\n const [fetchError, setFetchError] = useState<string | null>(null);\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const fetchedRef = useRef(false);\n const baseEndpoint = opts.conversationsEndpoint ?? \"/api/v1/conversations\";\n\n const fetchList = useCallback(async () => {\n if (!opts.enabled || !available) return;\n setLoading(true);\n setFetchError(null);\n try {\n const res = await fetch(`${opts.apiUrl}${baseEndpoint}?limit=50`, {\n headers: opts.getHeaders(),\n credentials: opts.getCredentials(),\n });\n\n if (res.status === 404) {\n setAvailable(false);\n return;\n }\n\n if (!res.ok) {\n // intentionally ignored: response may not be JSON\n const errorBody = await res.json().catch(() => null);\n if (errorBody?.code === \"not_available\") {\n setAvailable(false);\n return;\n }\n console.warn(`fetchList: HTTP ${res.status}`, errorBody);\n setFetchError(\"Failed to load conversations. Please reload the page to try again.\");\n return;\n }\n\n const data = await res.json();\n setConversations(data.conversations ?? []);\n setTotal(data.total ?? 0);\n fetchedRef.current = true;\n } catch (err: unknown) {\n console.warn(\"fetchList error:\", err instanceof Error ? err.message : String(err));\n setFetchError(\"Failed to load conversations. Please reload the page to try again.\");\n } finally {\n setLoading(false);\n }\n }, [opts.apiUrl, opts.enabled, opts.getHeaders, opts.getCredentials, available, baseEndpoint]);\n\n const loadConversation = useCallback(async (id: string): Promise<UIMessage[]> => {\n const res = await fetch(`${opts.apiUrl}${baseEndpoint}/${id}`, {\n headers: opts.getHeaders(),\n credentials: opts.getCredentials(),\n });\n\n if (!res.ok) {\n console.warn(`loadConversation: HTTP ${res.status} for ${id}`);\n throw new Error(`Failed to load conversation (HTTP ${res.status})`);\n }\n\n const data: ConversationWithMessages = await res.json();\n return transformMessages(data.messages);\n }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, baseEndpoint]);\n\n const deleteConversation = useCallback(async (id: string): Promise<void> => {\n const res = await fetch(`${opts.apiUrl}${baseEndpoint}/${id}`, {\n method: \"DELETE\",\n headers: opts.getHeaders(),\n credentials: opts.getCredentials(),\n });\n\n if (!res.ok) {\n console.warn(`deleteConversation: HTTP ${res.status} for ${id}`);\n throw new Error(`Failed to delete conversation (HTTP ${res.status})`);\n }\n\n setConversations((prev) => prev.filter((c) => c.id !== id));\n setTotal((prev) => Math.max(0, prev - 1));\n\n if (selectedId === id) setSelectedId(null);\n }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, selectedId, baseEndpoint]);\n\n const starConversation = useCallback(async (id: string, starred: boolean): Promise<void> => {\n // Optimistic update\n setConversations((prev) =>\n prev.map((c) => (c.id === id ? { ...c, starred } : c)),\n );\n let rolledBack = false;\n try {\n const res = await fetch(`${opts.apiUrl}${baseEndpoint}/${id}/star`, {\n method: \"PATCH\",\n headers: { ...opts.getHeaders(), \"Content-Type\": \"application/json\" },\n credentials: opts.getCredentials(),\n body: JSON.stringify({ starred }),\n });\n\n if (!res.ok) {\n console.warn(`starConversation: HTTP ${res.status} for ${id}`);\n setConversations((prev) =>\n prev.map((c) => (c.id === id ? { ...c, starred: !starred } : c)),\n );\n rolledBack = true;\n throw new Error(`Failed to update star (HTTP ${res.status})`);\n }\n } catch (err: unknown) {\n if (!rolledBack) {\n setConversations((prev) =>\n prev.map((c) => (c.id === id ? { ...c, starred: !starred } : c)),\n );\n }\n throw err;\n }\n }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, baseEndpoint]);\n\n const refresh = useCallback(async () => {\n await fetchList();\n }, [fetchList]);\n\n return {\n conversations,\n total,\n loading,\n available,\n fetchError,\n selectedId,\n setSelectedId,\n fetchList,\n loadConversation,\n deleteConversation,\n starConversation,\n refresh,\n };\n}\n"]}