@useatlas/react 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -2
  3. package/dist/{chunk-5SEVKHS5.cjs → chunk-35SCTKSW.js} +100 -7
  4. package/dist/chunk-35SCTKSW.js.map +1 -0
  5. package/dist/{chunk-UIRB6L36.cjs → chunk-DZFSZSQB.cjs} +46 -54
  6. package/dist/chunk-DZFSZSQB.cjs.map +1 -0
  7. package/dist/{chunk-2WFDP7G5.js → chunk-FMSGREKS.js} +46 -54
  8. package/dist/chunk-FMSGREKS.js.map +1 -0
  9. package/dist/{chunk-44HBZYKP.js → chunk-IDXGFWFS.cjs} +109 -3
  10. package/dist/chunk-IDXGFWFS.cjs.map +1 -0
  11. package/dist/global.d.ts +36 -0
  12. package/dist/hooks.cjs +10 -10
  13. package/dist/hooks.cjs.map +1 -1
  14. package/dist/hooks.d.cts +2 -2
  15. package/dist/hooks.d.ts +2 -2
  16. package/dist/hooks.js +3 -3
  17. package/dist/hooks.js.map +1 -1
  18. package/dist/index.cjs +385 -265
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +224 -4
  21. package/dist/index.d.ts +224 -4
  22. package/dist/index.js +328 -208
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/widget-types.d.ts +232 -0
  25. package/dist/{result-chart-YLCKBNV4.cjs → result-chart-ANZOT6FL.cjs} +24 -34
  26. package/dist/result-chart-ANZOT6FL.cjs.map +1 -0
  27. package/dist/{result-chart-NFAJ4IQ5.js → result-chart-C3EJTN5G.js} +22 -32
  28. package/dist/result-chart-C3EJTN5G.js.map +1 -0
  29. package/dist/widget.css +2 -2
  30. package/dist/widget.js +215 -246
  31. package/package.json +27 -17
  32. package/src/components/__tests__/data-table.test.tsx +125 -0
  33. package/src/components/actions/action-approval-card.tsx +26 -19
  34. package/src/components/actions/action-status-badge.tsx +3 -3
  35. package/src/components/atlas-chat.tsx +97 -37
  36. package/src/components/chart/result-chart.tsx +13 -37
  37. package/src/components/chat/api-key-bar.tsx +4 -4
  38. package/src/components/chat/data-table.tsx +42 -3
  39. package/src/components/chat/error-banner.tsx +108 -5
  40. package/src/components/chat/follow-up-chips.tsx +1 -1
  41. package/src/components/chat/managed-auth-card.tsx +6 -6
  42. package/src/components/conversations/conversation-item.tsx +19 -14
  43. package/src/components/conversations/conversation-list.tsx +3 -3
  44. package/src/components/conversations/conversation-sidebar.tsx +15 -4
  45. package/src/components/conversations/delete-confirmation.tsx +2 -2
  46. package/src/components/error-boundary.tsx +66 -0
  47. package/src/components/schema-explorer/schema-explorer.tsx +4 -0
  48. package/src/env.d.ts +9 -7
  49. package/src/global.d.ts +36 -0
  50. package/src/hooks/__tests__/use-atlas-conversations.test.tsx +4 -6
  51. package/src/hooks/use-atlas-chat.ts +1 -1
  52. package/src/hooks/use-atlas-conversations.ts +2 -2
  53. package/src/hooks/use-conversations.ts +60 -68
  54. package/src/index.ts +8 -0
  55. package/src/lib/action-types.ts +2 -2
  56. package/src/lib/helpers.ts +16 -16
  57. package/src/lib/types.ts +3 -2
  58. package/src/lib/widget-types.ts +232 -0
  59. package/src/test-setup.ts +2 -2
  60. package/dist/chunk-2WFDP7G5.js.map +0 -1
  61. package/dist/chunk-44HBZYKP.js.map +0 -1
  62. package/dist/chunk-5SEVKHS5.cjs.map +0 -1
  63. package/dist/chunk-UIRB6L36.cjs.map +0 -1
  64. package/dist/result-chart-NFAJ4IQ5.js.map +0 -1
  65. package/dist/result-chart-YLCKBNV4.cjs.map +0 -1
@@ -61,7 +61,7 @@ export function ManagedAuthCard() {
61
61
  value={name}
62
62
  onChange={(e) => setName(e.target.value)}
63
63
  placeholder="Name (optional)"
64
- className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
64
+ className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus-visible:border-blue-500 focus-visible:ring-[3px] focus-visible:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
65
65
  />
66
66
  )}
67
67
  <input
@@ -70,7 +70,7 @@ export function ManagedAuthCard() {
70
70
  onChange={(e) => setEmail(e.target.value)}
71
71
  placeholder="Email"
72
72
  required
73
- className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
73
+ className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus-visible:border-blue-500 focus-visible:ring-[3px] focus-visible:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
74
74
  />
75
75
  <input
76
76
  type="password"
@@ -79,7 +79,7 @@ export function ManagedAuthCard() {
79
79
  placeholder="Password"
80
80
  required
81
81
  minLength={8}
82
- className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus:border-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
82
+ className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus-visible:border-blue-500 focus-visible:ring-[3px] focus-visible:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-600"
83
83
  />
84
84
  {error && (
85
85
  <p className="text-xs text-red-600 dark:text-red-400">{error}</p>
@@ -87,7 +87,7 @@ export function ManagedAuthCard() {
87
87
  <button
88
88
  type="submit"
89
89
  disabled={loading}
90
- className="w-full rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500 disabled:opacity-40"
90
+ className="w-full rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-blue-500/50 disabled:opacity-40"
91
91
  >
92
92
  {loading ? "..." : view === "login" ? "Sign in" : "Create account"}
93
93
  </button>
@@ -97,14 +97,14 @@ export function ManagedAuthCard() {
97
97
  {view === "login" ? (
98
98
  <>
99
99
  No account?{" "}
100
- <button onClick={() => { setView("signup"); setError(""); }} className="text-blue-600 hover:underline dark:text-blue-400">
100
+ <button onClick={() => { setView("signup"); setError(""); }} className="rounded text-blue-600 hover:underline focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-blue-500/50 dark:text-blue-400">
101
101
  Create one
102
102
  </button>
103
103
  </>
104
104
  ) : (
105
105
  <>
106
106
  Already have an account?{" "}
107
- <button onClick={() => { setView("login"); setError(""); }} className="text-blue-600 hover:underline dark:text-blue-400">
107
+ <button onClick={() => { setView("login"); setError(""); }} className="rounded text-blue-600 hover:underline focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-blue-500/50 dark:text-blue-400">
108
108
  Sign in
109
109
  </button>
110
110
  </>
@@ -35,12 +35,13 @@ export function ConversationItem({
35
35
  conversation: Conversation;
36
36
  isActive: boolean;
37
37
  onSelect: () => void;
38
- onDelete: () => Promise<boolean>;
39
- onStar: (starred: boolean) => Promise<boolean>;
38
+ onDelete: () => Promise<void>;
39
+ onStar: (starred: boolean) => Promise<void>;
40
40
  }) {
41
41
  const [confirmDelete, setConfirmDelete] = useState(false);
42
42
  const [deleting, setDeleting] = useState(false);
43
43
  const [starPending, setStarPending] = useState(false);
44
+ const [error, setError] = useState<string | null>(null);
44
45
 
45
46
  if (confirmDelete) {
46
47
  return (
@@ -50,12 +51,11 @@ export function ConversationItem({
50
51
  onConfirm={async () => {
51
52
  setDeleting(true);
52
53
  try {
53
- const success = await onDelete();
54
- if (success) {
55
- setConfirmDelete(false);
56
- }
57
- } catch (err) {
58
- console.warn("Failed to delete conversation:", err);
54
+ await onDelete();
55
+ setConfirmDelete(false);
56
+ } catch {
57
+ setError("Failed to delete");
58
+ setTimeout(() => setError(null), 3000);
59
59
  } finally {
60
60
  setDeleting(false);
61
61
  }
@@ -76,7 +76,7 @@ export function ConversationItem({
76
76
  onSelect();
77
77
  }
78
78
  }}
79
- className={`group flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 text-left text-sm transition-colors ${
79
+ className={`group flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 ${
80
80
  isActive
81
81
  ? "bg-blue-50 text-blue-700 dark:bg-blue-600/10 dark:text-blue-400"
82
82
  : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@@ -86,9 +86,13 @@ export function ConversationItem({
86
86
  <p className="truncate text-sm font-medium">
87
87
  {conversation.title || "New conversation"}
88
88
  </p>
89
- <p className="text-xs text-zinc-400 dark:text-zinc-500">
90
- {relativeTime(conversation.updatedAt)}
91
- </p>
89
+ {error ? (
90
+ <p className="text-xs text-red-500 dark:text-red-400">{error}</p>
91
+ ) : (
92
+ <p className="text-xs text-zinc-400 dark:text-zinc-500">
93
+ {relativeTime(conversation.updatedAt)}
94
+ </p>
95
+ )}
92
96
  </div>
93
97
  <div className="flex shrink-0 items-center gap-0.5">
94
98
  <Button
@@ -100,8 +104,9 @@ export function ConversationItem({
100
104
  setStarPending(true);
101
105
  try {
102
106
  await onStar(!conversation.starred);
103
- } catch (err) {
104
- console.warn("Failed to update star:", err);
107
+ } catch {
108
+ setError("Failed to update");
109
+ setTimeout(() => setError(null), 3000);
105
110
  } finally {
106
111
  setStarPending(false);
107
112
  }
@@ -15,14 +15,14 @@ export function ConversationList({
15
15
  conversations: Conversation[];
16
16
  selectedId: string | null;
17
17
  onSelect: (id: string) => void;
18
- onDelete: (id: string) => Promise<boolean>;
19
- onStar: (id: string, starred: boolean) => Promise<boolean>;
18
+ onDelete: (id: string) => Promise<void>;
19
+ onStar: (id: string, starred: boolean) => Promise<void>;
20
20
  showSections?: boolean;
21
21
  emptyMessage?: string;
22
22
  }) {
23
23
  if (conversations.length === 0) {
24
24
  return (
25
- <div className="px-3 py-6 text-center text-xs text-zinc-400 dark:text-zinc-500">
25
+ <div className="px-3 py-6 text-center text-xs text-zinc-500 dark:text-zinc-400">
26
26
  {emptyMessage ?? "No conversations yet"}
27
27
  </div>
28
28
  );
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState } from "react";
3
+ import { useState, useEffect } from "react";
4
4
  import { Star } from "lucide-react";
5
5
  import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group";
6
6
  import { Badge } from "../ui/badge";
@@ -24,13 +24,24 @@ export function ConversationSidebar({
24
24
  selectedId: string | null;
25
25
  loading: boolean;
26
26
  onSelect: (id: string) => void;
27
- onDelete: (id: string) => Promise<boolean>;
28
- onStar: (id: string, starred: boolean) => Promise<boolean>;
27
+ onDelete: (id: string) => Promise<void>;
28
+ onStar: (id: string, starred: boolean) => Promise<void>;
29
29
  onNewChat: () => void;
30
30
  mobileOpen: boolean;
31
31
  onMobileClose: () => void;
32
32
  }) {
33
33
  const [filter, setFilter] = useState<SidebarFilter>("all");
34
+
35
+ // Close mobile sidebar on Escape
36
+ useEffect(() => {
37
+ if (!mobileOpen) return;
38
+ function handleKeyDown(e: KeyboardEvent) {
39
+ if (e.key === "Escape") onMobileClose();
40
+ }
41
+ document.addEventListener("keydown", handleKeyDown);
42
+ return () => document.removeEventListener("keydown", handleKeyDown);
43
+ }, [mobileOpen, onMobileClose]);
44
+
34
45
  const starredConversations = conversations.filter((c) => c.starred);
35
46
  const filteredConversations = filter === "saved" ? starredConversations : conversations;
36
47
 
@@ -40,7 +51,7 @@ export function ConversationSidebar({
40
51
  <span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">History</span>
41
52
  <button
42
53
  onClick={onNewChat}
43
- className="rounded-lg border border-zinc-200 px-3 py-1.5 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-400 hover:text-zinc-900 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200"
54
+ className="rounded-lg border border-zinc-200 px-3 py-1.5 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-400 hover:text-zinc-900 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:border-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200"
44
55
  >
45
56
  + New
46
57
  </button>
@@ -12,13 +12,13 @@ export function DeleteConfirmation({
12
12
  <span className="text-zinc-500 dark:text-zinc-400">Delete?</span>
13
13
  <button
14
14
  onClick={onCancel}
15
- className="rounded px-2 py-0.5 text-zinc-500 transition-colors hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200"
15
+ className="rounded px-2 py-0.5 text-zinc-500 transition-colors hover:text-zinc-800 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:text-zinc-400 dark:hover:text-zinc-200"
16
16
  >
17
17
  Cancel
18
18
  </button>
19
19
  <button
20
20
  onClick={onConfirm}
21
- className="rounded bg-red-600 px-2 py-0.5 text-white transition-colors hover:bg-red-500"
21
+ className="rounded bg-red-600 px-2 py-0.5 text-white transition-colors hover:bg-red-500 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-red-500/50 "
22
22
  >
23
23
  Delete
24
24
  </button>
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import { Component, type ReactNode, type ErrorInfo } from "react";
4
+ import { Button } from "./ui/button";
5
+ import { cn } from "../lib/utils";
6
+
7
+ interface ErrorBoundaryProps {
8
+ children: ReactNode;
9
+ fallback?: ReactNode;
10
+ fallbackRender?: (error: Error, reset: () => void) => ReactNode;
11
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
12
+ }
13
+
14
+ interface ErrorBoundaryState {
15
+ error: Error | null;
16
+ }
17
+
18
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
19
+ constructor(props: ErrorBoundaryProps) {
20
+ super(props);
21
+ this.state = { error: null };
22
+ }
23
+
24
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
25
+ return { error };
26
+ }
27
+
28
+ componentDidCatch(error: Error, info: ErrorInfo) {
29
+ console.error("[ErrorBoundary]", error, info.componentStack);
30
+ this.props.onError?.(error, info);
31
+ }
32
+
33
+ resetErrorBoundary = () => {
34
+ this.setState({ error: null });
35
+ };
36
+
37
+ render() {
38
+ const { error } = this.state;
39
+ if (!error) return this.props.children;
40
+
41
+ if (this.props.fallbackRender) {
42
+ return this.props.fallbackRender(error, this.resetErrorBoundary);
43
+ }
44
+
45
+ if (this.props.fallback) {
46
+ return this.props.fallback;
47
+ }
48
+
49
+ return (
50
+ <div
51
+ role="alert"
52
+ className={cn(
53
+ "flex flex-col items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-4 text-center",
54
+ "dark:border-red-900/50 dark:bg-red-950/20",
55
+ )}
56
+ >
57
+ <p className="text-sm text-red-700 dark:text-red-400">
58
+ Something went wrong.
59
+ </p>
60
+ <Button variant="outline" size="sm" onClick={this.resetErrorBoundary}>
61
+ Try again
62
+ </Button>
63
+ </div>
64
+ );
65
+ }
66
+ }
@@ -5,6 +5,7 @@ import { useAtlasConfig } from "../../context";
5
5
  import {
6
6
  Sheet,
7
7
  SheetContent,
8
+ SheetDescription,
8
9
  SheetHeader,
9
10
  SheetTitle,
10
11
  } from "../ui/sheet";
@@ -467,6 +468,9 @@ export function SchemaExplorer({
467
468
  <TableProperties className="size-4" />
468
469
  Schema Explorer
469
470
  </SheetTitle>
471
+ <SheetDescription className="sr-only">
472
+ Browse tables, columns, joins, and query patterns from the semantic layer
473
+ </SheetDescription>
470
474
  </SheetHeader>
471
475
 
472
476
  <Separator className="mt-3" />
package/src/env.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /** Optional runtime dependency — dynamically imported for Excel export. */
2
- declare module "xlsx" {
3
- const utils: {
4
- json_to_sheet: (data: unknown[], opts?: { header?: string[] }) => unknown;
5
- book_new: () => unknown;
6
- book_append_sheet: (wb: unknown, ws: unknown, name: string) => void;
7
- };
8
- function write(wb: unknown, opts: { bookType: string; type: string }): ArrayBuffer;
2
+ declare module "exceljs" {
3
+ export class Workbook {
4
+ addWorksheet(name: string): Worksheet;
5
+ xlsx: { writeBuffer(): Promise<ArrayBuffer> };
6
+ }
7
+ interface Worksheet {
8
+ columns: Array<{ header: string; key: string }>;
9
+ addRow(data: Record<string, unknown>): void;
10
+ }
9
11
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Ambient type declarations for the Atlas widget script-tag API.
3
+ *
4
+ * These augment `window.Atlas` and the `Atlas` global so that embedders
5
+ * get full IDE autocomplete and type-checking.
6
+ *
7
+ * **Usage:** Add a triple-slash reference at the top of your script:
8
+ *
9
+ * ```ts
10
+ * /// <reference types="@useatlas/react/widget" />
11
+ *
12
+ * window.Atlas?.open();
13
+ * Atlas?.ask("How many users signed up today?");
14
+ * ```
15
+ */
16
+
17
+ import type { AtlasWidget, AtlasWidgetCommand } from "./lib/widget-types";
18
+
19
+ declare global {
20
+ interface Window {
21
+ /**
22
+ * Atlas widget API — available after the widget `<script>` tag loads.
23
+ *
24
+ * Before the script loads, this may be an array of queued commands
25
+ * (`AtlasWidgetCommand[]`) that are replayed once the widget initializes.
26
+ */
27
+ Atlas?: AtlasWidget | AtlasWidgetCommand[];
28
+ }
29
+
30
+ /**
31
+ * Atlas widget API — shorthand for `window.Atlas`.
32
+ *
33
+ * May be `undefined` if the widget script has not loaded yet.
34
+ */
35
+ var Atlas: AtlasWidget | AtlasWidgetCommand[] | undefined;
36
+ }
@@ -123,11 +123,9 @@ describe("useAtlasConversations", () => {
123
123
  Promise.resolve(new Response("", { status: 200 })),
124
124
  );
125
125
 
126
- let deleted = false;
127
126
  await act(async () => {
128
- deleted = await result.current.deleteConversation("conv-1");
127
+ await result.current.deleteConversation("conv-1");
129
128
  });
130
- expect(deleted).toBe(true);
131
129
  expect(result.current.conversations).toHaveLength(0);
132
130
  expect(result.current.total).toBe(0);
133
131
  });
@@ -148,11 +146,11 @@ describe("useAtlasConversations", () => {
148
146
  Promise.resolve(new Response("", { status: 500 })),
149
147
  );
150
148
 
151
- let starred = true;
152
149
  await act(async () => {
153
- starred = await result.current.starConversation("conv-1", true);
150
+ await expect(
151
+ result.current.starConversation("conv-1", true),
152
+ ).rejects.toThrow("Failed to update star (HTTP 500)");
154
153
  });
155
- expect(starred).toBe(false);
156
154
  // Should roll back to unstarred
157
155
  expect(result.current.conversations[0].starred).toBe(false);
158
156
  });
@@ -52,7 +52,7 @@ export function useAtlasChat(options: UseAtlasChatOptions = {}): UseAtlasChatRet
52
52
  if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
53
53
 
54
54
  return new DefaultChatTransport({
55
- api: `${apiUrl}/api/chat`,
55
+ api: `${apiUrl}/api/v1/chat`,
56
56
  headers,
57
57
  credentials: isCrossOrigin ? "include" : undefined,
58
58
  body: () =>
@@ -19,8 +19,8 @@ export interface UseAtlasConversationsReturn {
19
19
  setSelectedId: (id: string | null) => void;
20
20
  refresh: () => Promise<void>;
21
21
  loadConversation: (id: string) => Promise<UIMessage[] | null>;
22
- deleteConversation: (id: string) => Promise<boolean>;
23
- starConversation: (id: string, starred: boolean) => Promise<boolean>;
22
+ deleteConversation: (id: string) => Promise<void>;
23
+ starConversation: (id: string, starred: boolean) => Promise<void>;
24
24
  }
25
25
 
26
26
  /**
@@ -9,6 +9,8 @@ export interface UseConversationsOptions {
9
9
  enabled: boolean;
10
10
  getHeaders: () => Record<string, string>;
11
11
  getCredentials: () => RequestCredentials;
12
+ /** Custom conversations API endpoint path. Defaults to "/api/v1/conversations". */
13
+ conversationsEndpoint?: string;
12
14
  }
13
15
 
14
16
  export interface UseConversationsReturn {
@@ -16,12 +18,13 @@ export interface UseConversationsReturn {
16
18
  total: number;
17
19
  loading: boolean;
18
20
  available: boolean;
21
+ fetchError: string | null;
19
22
  selectedId: string | null;
20
23
  setSelectedId: (id: string | null) => void;
21
24
  fetchList: () => Promise<void>;
22
- loadConversation: (id: string) => Promise<UIMessage[] | null>;
23
- deleteConversation: (id: string) => Promise<boolean>;
24
- starConversation: (id: string, starred: boolean) => Promise<boolean>;
25
+ loadConversation: (id: string) => Promise<UIMessage[]>;
26
+ deleteConversation: (id: string) => Promise<void>;
27
+ starConversation: (id: string, starred: boolean) => Promise<void>;
25
28
  refresh: () => Promise<void>;
26
29
  }
27
30
 
@@ -29,14 +32,16 @@ export function transformMessages(messages: Message[]): UIMessage[] {
29
32
  return messages
30
33
  .filter((m) => m.role === "user" || m.role === "assistant")
31
34
  .map((m) => {
32
- const content = typeof m.content === "string"
35
+ const parts: UIMessage["parts"] = Array.isArray(m.content)
33
36
  ? m.content
34
- : JSON.stringify(m.content);
37
+ .filter((p: { type?: string }) => p.type === "text")
38
+ .map((p: { text?: string }) => ({ type: "text" as const, text: p.text ?? "" }))
39
+ : [{ type: "text" as const, text: String(m.content) }];
35
40
 
36
41
  return {
37
42
  id: m.id,
38
43
  role: m.role as "user" | "assistant",
39
- parts: [{ type: "text" as const, text: content }],
44
+ parts,
40
45
  };
41
46
  });
42
47
  }
@@ -46,15 +51,17 @@ export function useConversations(opts: UseConversationsOptions): UseConversation
46
51
  const [total, setTotal] = useState(0);
47
52
  const [loading, setLoading] = useState(false);
48
53
  const [available, setAvailable] = useState(true);
54
+ const [fetchError, setFetchError] = useState<string | null>(null);
49
55
  const [selectedId, setSelectedId] = useState<string | null>(null);
50
56
  const fetchedRef = useRef(false);
51
- const networkFailRef = useRef(0);
57
+ const baseEndpoint = opts.conversationsEndpoint ?? "/api/v1/conversations";
52
58
 
53
59
  const fetchList = useCallback(async () => {
54
60
  if (!opts.enabled || !available) return;
55
61
  setLoading(true);
62
+ setFetchError(null);
56
63
  try {
57
- const res = await fetch(`${opts.apiUrl}/api/v1/conversations?limit=50`, {
64
+ const res = await fetch(`${opts.apiUrl}${baseEndpoint}?limit=50`, {
58
65
  headers: opts.getHeaders(),
59
66
  credentials: opts.getCredentials(),
60
67
  });
@@ -65,12 +72,14 @@ export function useConversations(opts: UseConversationsOptions): UseConversation
65
72
  }
66
73
 
67
74
  if (!res.ok) {
75
+ // intentionally ignored: response may not be JSON
68
76
  const errorBody = await res.json().catch(() => null);
69
77
  if (errorBody?.code === "not_available") {
70
78
  setAvailable(false);
71
79
  return;
72
80
  }
73
81
  console.warn(`fetchList: HTTP ${res.status}`, errorBody);
82
+ setFetchError("Failed to load conversations. Please reload the page to try again.");
74
83
  return;
75
84
  }
76
85
 
@@ -78,71 +87,55 @@ export function useConversations(opts: UseConversationsOptions): UseConversation
78
87
  setConversations(data.conversations ?? []);
79
88
  setTotal(data.total ?? 0);
80
89
  fetchedRef.current = true;
81
- } catch (err) {
82
- console.warn("fetchList error:", err);
83
- // Network error before any successful fetch disable temporarily.
84
- // A subsequent explicit fetchList() call can retry.
85
- if (!fetchedRef.current) {
86
- networkFailRef.current += 1;
87
- if (networkFailRef.current >= 3) setAvailable(false);
88
- }
90
+ } catch (err: unknown) {
91
+ console.warn("fetchList error:", err instanceof Error ? err.message : String(err));
92
+ setFetchError("Failed to load conversations. Please reload the page to try again.");
89
93
  } finally {
90
94
  setLoading(false);
91
95
  }
92
- }, [opts.apiUrl, opts.enabled, opts.getHeaders, opts.getCredentials, available]);
93
-
94
- const loadConversation = useCallback(async (id: string): Promise<UIMessage[] | null> => {
95
- try {
96
- const res = await fetch(`${opts.apiUrl}/api/v1/conversations/${id}`, {
97
- headers: opts.getHeaders(),
98
- credentials: opts.getCredentials(),
99
- });
96
+ }, [opts.apiUrl, opts.enabled, opts.getHeaders, opts.getCredentials, available, baseEndpoint]);
100
97
 
101
- if (!res.ok) {
102
- console.warn(`loadConversation: HTTP ${res.status} for ${id}`);
103
- return null;
104
- }
98
+ const loadConversation = useCallback(async (id: string): Promise<UIMessage[]> => {
99
+ const res = await fetch(`${opts.apiUrl}${baseEndpoint}/${id}`, {
100
+ headers: opts.getHeaders(),
101
+ credentials: opts.getCredentials(),
102
+ });
105
103
 
106
- const data: ConversationWithMessages = await res.json();
107
- return transformMessages(data.messages);
108
- } catch (err) {
109
- console.warn("loadConversation error:", err);
110
- return null;
104
+ if (!res.ok) {
105
+ console.warn(`loadConversation: HTTP ${res.status} for ${id}`);
106
+ throw new Error(`Failed to load conversation (HTTP ${res.status})`);
111
107
  }
112
- }, [opts.apiUrl, opts.getHeaders, opts.getCredentials]);
113
108
 
114
- const deleteConversation = useCallback(async (id: string): Promise<boolean> => {
115
- try {
116
- const res = await fetch(`${opts.apiUrl}/api/v1/conversations/${id}`, {
117
- method: "DELETE",
118
- headers: opts.getHeaders(),
119
- credentials: opts.getCredentials(),
120
- });
109
+ const data: ConversationWithMessages = await res.json();
110
+ return transformMessages(data.messages);
111
+ }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, baseEndpoint]);
121
112
 
122
- if (!res.ok) {
123
- console.warn(`deleteConversation: HTTP ${res.status} for ${id}`);
124
- return false;
125
- }
113
+ const deleteConversation = useCallback(async (id: string): Promise<void> => {
114
+ const res = await fetch(`${opts.apiUrl}${baseEndpoint}/${id}`, {
115
+ method: "DELETE",
116
+ headers: opts.getHeaders(),
117
+ credentials: opts.getCredentials(),
118
+ });
126
119
 
127
- setConversations((prev) => prev.filter((c) => c.id !== id));
128
- setTotal((prev) => Math.max(0, prev - 1));
120
+ if (!res.ok) {
121
+ console.warn(`deleteConversation: HTTP ${res.status} for ${id}`);
122
+ throw new Error(`Failed to delete conversation (HTTP ${res.status})`);
123
+ }
129
124
 
130
- if (selectedId === id) setSelectedId(null);
125
+ setConversations((prev) => prev.filter((c) => c.id !== id));
126
+ setTotal((prev) => Math.max(0, prev - 1));
131
127
 
132
- return true;
133
- } catch (err) {
134
- console.warn("deleteConversation error:", err);
135
- return false;
136
- }
137
- }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, selectedId]);
128
+ if (selectedId === id) setSelectedId(null);
129
+ }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, selectedId, baseEndpoint]);
138
130
 
139
- const starConversation = useCallback(async (id: string, starred: boolean): Promise<boolean> => {
131
+ const starConversation = useCallback(async (id: string, starred: boolean): Promise<void> => {
140
132
  // Optimistic update
141
133
  setConversations((prev) =>
142
134
  prev.map((c) => (c.id === id ? { ...c, starred } : c)),
143
135
  );
136
+ let rolledBack = false;
144
137
  try {
145
- const res = await fetch(`${opts.apiUrl}/api/v1/conversations/${id}/star`, {
138
+ const res = await fetch(`${opts.apiUrl}${baseEndpoint}/${id}/star`, {
146
139
  method: "PATCH",
147
140
  headers: { ...opts.getHeaders(), "Content-Type": "application/json" },
148
141
  credentials: opts.getCredentials(),
@@ -151,23 +144,21 @@ export function useConversations(opts: UseConversationsOptions): UseConversation
151
144
 
152
145
  if (!res.ok) {
153
146
  console.warn(`starConversation: HTTP ${res.status} for ${id}`);
154
- // Rollback
155
147
  setConversations((prev) =>
156
148
  prev.map((c) => (c.id === id ? { ...c, starred: !starred } : c)),
157
149
  );
158
- return false;
150
+ rolledBack = true;
151
+ throw new Error(`Failed to update star (HTTP ${res.status})`);
159
152
  }
160
-
161
- return true;
162
- } catch (err) {
163
- console.warn("starConversation error:", err);
164
- // Rollback
165
- setConversations((prev) =>
166
- prev.map((c) => (c.id === id ? { ...c, starred: !starred } : c)),
167
- );
168
- return false;
153
+ } catch (err: unknown) {
154
+ if (!rolledBack) {
155
+ setConversations((prev) =>
156
+ prev.map((c) => (c.id === id ? { ...c, starred: !starred } : c)),
157
+ );
158
+ }
159
+ throw err;
169
160
  }
170
- }, [opts.apiUrl, opts.getHeaders, opts.getCredentials]);
161
+ }, [opts.apiUrl, opts.getHeaders, opts.getCredentials, baseEndpoint]);
171
162
 
172
163
  const refresh = useCallback(async () => {
173
164
  await fetchList();
@@ -178,6 +169,7 @@ export function useConversations(opts: UseConversationsOptions): UseConversation
178
169
  total,
179
170
  loading,
180
171
  available,
172
+ fetchError,
181
173
  selectedId,
182
174
  setSelectedId,
183
175
  fetchList,
package/src/index.ts CHANGED
@@ -34,3 +34,11 @@ export type {
34
34
  // Hooks
35
35
  export { useConversations } from "./hooks/use-conversations";
36
36
  export type { UseConversationsOptions, UseConversationsReturn } from "./hooks/use-conversations";
37
+
38
+ // Widget types (for script-tag embedders)
39
+ export type {
40
+ AtlasWidget,
41
+ AtlasWidgetEventMap,
42
+ AtlasWidgetConfig,
43
+ AtlasWidgetCommand,
44
+ } from "./lib/widget-types";
@@ -4,8 +4,8 @@ export {
4
4
  isActionToolResult,
5
5
  } from "@useatlas/types/action";
6
6
  export type {
7
- ActionStatus,
8
- ResolvedStatus,
7
+ ActionDisplayStatus,
8
+ ResolvedDisplayStatus,
9
9
  ActionToolResultShape,
10
10
  ActionApprovalResponse,
11
11
  } from "@useatlas/types/action";