@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.
- package/LICENSE +21 -0
- package/README.md +79 -2
- package/dist/{chunk-5SEVKHS5.cjs → chunk-35SCTKSW.js} +100 -7
- package/dist/chunk-35SCTKSW.js.map +1 -0
- package/dist/{chunk-UIRB6L36.cjs → chunk-DZFSZSQB.cjs} +46 -54
- package/dist/chunk-DZFSZSQB.cjs.map +1 -0
- package/dist/{chunk-2WFDP7G5.js → chunk-FMSGREKS.js} +46 -54
- package/dist/chunk-FMSGREKS.js.map +1 -0
- package/dist/{chunk-44HBZYKP.js → chunk-IDXGFWFS.cjs} +109 -3
- package/dist/chunk-IDXGFWFS.cjs.map +1 -0
- package/dist/global.d.ts +36 -0
- package/dist/hooks.cjs +10 -10
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +2 -2
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +3 -3
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +385 -265
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +224 -4
- package/dist/index.d.ts +224 -4
- package/dist/index.js +328 -208
- package/dist/index.js.map +1 -1
- package/dist/lib/widget-types.d.ts +232 -0
- package/dist/{result-chart-YLCKBNV4.cjs → result-chart-ANZOT6FL.cjs} +24 -34
- package/dist/result-chart-ANZOT6FL.cjs.map +1 -0
- package/dist/{result-chart-NFAJ4IQ5.js → result-chart-C3EJTN5G.js} +22 -32
- package/dist/result-chart-C3EJTN5G.js.map +1 -0
- package/dist/widget.css +2 -2
- package/dist/widget.js +215 -246
- package/package.json +26 -16
- package/src/components/__tests__/data-table.test.tsx +125 -0
- package/src/components/actions/action-approval-card.tsx +26 -19
- package/src/components/actions/action-status-badge.tsx +3 -3
- package/src/components/atlas-chat.tsx +97 -37
- package/src/components/chart/result-chart.tsx +13 -37
- package/src/components/chat/api-key-bar.tsx +4 -4
- package/src/components/chat/data-table.tsx +42 -3
- package/src/components/chat/error-banner.tsx +108 -5
- package/src/components/chat/follow-up-chips.tsx +1 -1
- package/src/components/chat/managed-auth-card.tsx +6 -6
- package/src/components/conversations/conversation-item.tsx +19 -14
- package/src/components/conversations/conversation-list.tsx +3 -3
- package/src/components/conversations/conversation-sidebar.tsx +15 -4
- package/src/components/conversations/delete-confirmation.tsx +2 -2
- package/src/components/error-boundary.tsx +66 -0
- package/src/components/schema-explorer/schema-explorer.tsx +4 -0
- package/src/env.d.ts +9 -7
- package/src/global.d.ts +36 -0
- package/src/hooks/__tests__/use-atlas-conversations.test.tsx +4 -6
- package/src/hooks/use-atlas-chat.ts +1 -1
- package/src/hooks/use-atlas-conversations.ts +2 -2
- package/src/hooks/use-conversations.ts +60 -68
- package/src/index.ts +8 -0
- package/src/lib/action-types.ts +2 -2
- package/src/lib/helpers.ts +16 -16
- package/src/lib/types.ts +3 -2
- package/src/lib/widget-types.ts +232 -0
- package/src/test-setup.ts +2 -2
- package/dist/chunk-2WFDP7G5.js.map +0 -1
- package/dist/chunk-44HBZYKP.js.map +0 -1
- package/dist/chunk-5SEVKHS5.cjs.map +0 -1
- package/dist/chunk-UIRB6L36.cjs.map +0 -1
- package/dist/result-chart-NFAJ4IQ5.js.map +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
192
|
+
rolledBack = true;
|
|
193
|
+
throw new Error(`Failed to update star (HTTP ${res.status})`);
|
|
203
194
|
}
|
|
204
|
-
return true;
|
|
205
195
|
} catch (err) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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-
|
|
249
|
-
//# sourceMappingURL=chunk-
|
|
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"]}
|