@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.
- 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 +27 -17
- 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
|
@@ -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<
|
|
39
|
-
onStar: (starred: boolean) => Promise<
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
90
|
-
{
|
|
91
|
-
|
|
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
|
|
104
|
-
|
|
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<
|
|
19
|
-
onStar: (id: string, starred: boolean) => Promise<
|
|
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-
|
|
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<
|
|
28
|
-
onStar: (id: string, starred: boolean) => Promise<
|
|
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 "
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
}
|
package/src/global.d.ts
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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<
|
|
23
|
-
starConversation: (id: string, starred: boolean) => Promise<
|
|
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[]
|
|
23
|
-
deleteConversation: (id: string) => Promise<
|
|
24
|
-
starConversation: (id: string, starred: boolean) => Promise<
|
|
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
|
|
35
|
+
const parts: UIMessage["parts"] = Array.isArray(m.content)
|
|
33
36
|
? m.content
|
|
34
|
-
|
|
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
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
125
|
+
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
126
|
+
setTotal((prev) => Math.max(0, prev - 1));
|
|
131
127
|
|
|
132
|
-
|
|
133
|
-
|
|
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<
|
|
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}
|
|
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
|
-
|
|
150
|
+
rolledBack = true;
|
|
151
|
+
throw new Error(`Failed to update star (HTTP ${res.status})`);
|
|
159
152
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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";
|
package/src/lib/action-types.ts
CHANGED