@syncagent/react 0.1.0

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/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @syncagent/react
2
+
3
+ React SDK for SyncAgent — drop-in AI database chat widget & hooks.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @syncagent/react @syncagent/js
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```tsx
14
+ import { SyncAgentChat } from "@syncagent/react";
15
+
16
+ export default function App() {
17
+ return (
18
+ <SyncAgentChat
19
+ config={{
20
+ apiKey: "sa_your_api_key",
21
+ baseUrl: "https://your-syncagent-instance.com",
22
+ connectionString: process.env.DATABASE_URL,
23
+ }}
24
+ />
25
+ );
26
+ }
27
+ ```
28
+
29
+ ## Widget Props
30
+
31
+ | Prop | Type | Default |
32
+ | ---------------- | --------------------------------- | ------------------ |
33
+ | `config` | `{ apiKey, baseUrl, connectionString }` | Required |
34
+ | `mode` | `"floating" \| "inline"` | `"floating"` |
35
+ | `position` | `"bottom-right" \| "bottom-left"` | `"bottom-right"` |
36
+ | `defaultOpen` | `boolean` | `false` |
37
+ | `title` | `string` | `"✨ AI Assistant"` |
38
+ | `placeholder` | `string` | `"Ask about..."` |
39
+ | `accentColor` | `string` | `"#10b981"` |
40
+
41
+ ## Custom UI with Hooks
42
+
43
+ ```tsx
44
+ import { SyncAgentProvider, useSyncAgent } from "@syncagent/react";
45
+
46
+ function App() {
47
+ return (
48
+ <SyncAgentProvider config={{ apiKey: "...", baseUrl: "...", connectionString: "..." }}>
49
+ <MyChat />
50
+ </SyncAgentProvider>
51
+ );
52
+ }
53
+
54
+ function MyChat() {
55
+ const { messages, isLoading, sendMessage, stop, reset } = useSyncAgent();
56
+ // Build your own UI
57
+ }
58
+ ```
59
+
60
+ ## Hook Returns
61
+
62
+ | Return | Type | Description |
63
+ | ------------ | --------------------------- | -------------------- |
64
+ | `messages` | `Message[]` | Chat history |
65
+ | `isLoading` | `boolean` | Currently streaming |
66
+ | `error` | `Error \| null` | Last error |
67
+ | `sendMessage`| `(content: string) => void` | Send a message |
68
+ | `stop` | `() => void` | Abort current stream |
69
+ | `reset` | `() => void` | Clear messages |
@@ -0,0 +1,49 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode, CSSProperties } from 'react';
3
+ import { SyncAgentConfig, SyncAgentClient, Message } from '@syncagent/js';
4
+ export { ChatOptions, Message, SyncAgentClient, SyncAgentConfig, ToolDefinition, ToolParameter } from '@syncagent/js';
5
+
6
+ declare function SyncAgentProvider({ config, children, }: {
7
+ config: SyncAgentConfig;
8
+ children: ReactNode;
9
+ }): react_jsx_runtime.JSX.Element;
10
+ declare function useSyncAgentClient(): SyncAgentClient;
11
+
12
+ interface UseSyncAgentOptions {
13
+ /** Pass a client directly instead of using the provider */
14
+ client?: SyncAgentClient;
15
+ }
16
+ declare function useSyncAgent(options?: UseSyncAgentOptions): {
17
+ messages: Message[];
18
+ isLoading: boolean;
19
+ error: Error | null;
20
+ sendMessage: (content: string) => Promise<void>;
21
+ stop: () => void;
22
+ reset: () => void;
23
+ };
24
+
25
+ interface SyncAgentChatProps {
26
+ /** Config — required if not wrapped in SyncAgentProvider */
27
+ config?: SyncAgentConfig;
28
+ /** Show as floating widget (FAB + panel) or inline */
29
+ mode?: "floating" | "inline";
30
+ /** FAB position */
31
+ position?: "bottom-right" | "bottom-left";
32
+ /** Start open */
33
+ defaultOpen?: boolean;
34
+ /** Widget title */
35
+ title?: string;
36
+ /** Placeholder text */
37
+ placeholder?: string;
38
+ /** Welcome message */
39
+ welcomeMessage?: string;
40
+ /** Accent color */
41
+ accentColor?: string;
42
+ /** Custom class for the container */
43
+ className?: string;
44
+ /** Custom styles for the container */
45
+ style?: CSSProperties;
46
+ }
47
+ declare function SyncAgentChat({ config, ...props }: SyncAgentChatProps): react_jsx_runtime.JSX.Element;
48
+
49
+ export { SyncAgentChat, type SyncAgentChatProps, SyncAgentProvider, useSyncAgent, useSyncAgentClient };
@@ -0,0 +1,49 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode, CSSProperties } from 'react';
3
+ import { SyncAgentConfig, SyncAgentClient, Message } from '@syncagent/js';
4
+ export { ChatOptions, Message, SyncAgentClient, SyncAgentConfig, ToolDefinition, ToolParameter } from '@syncagent/js';
5
+
6
+ declare function SyncAgentProvider({ config, children, }: {
7
+ config: SyncAgentConfig;
8
+ children: ReactNode;
9
+ }): react_jsx_runtime.JSX.Element;
10
+ declare function useSyncAgentClient(): SyncAgentClient;
11
+
12
+ interface UseSyncAgentOptions {
13
+ /** Pass a client directly instead of using the provider */
14
+ client?: SyncAgentClient;
15
+ }
16
+ declare function useSyncAgent(options?: UseSyncAgentOptions): {
17
+ messages: Message[];
18
+ isLoading: boolean;
19
+ error: Error | null;
20
+ sendMessage: (content: string) => Promise<void>;
21
+ stop: () => void;
22
+ reset: () => void;
23
+ };
24
+
25
+ interface SyncAgentChatProps {
26
+ /** Config — required if not wrapped in SyncAgentProvider */
27
+ config?: SyncAgentConfig;
28
+ /** Show as floating widget (FAB + panel) or inline */
29
+ mode?: "floating" | "inline";
30
+ /** FAB position */
31
+ position?: "bottom-right" | "bottom-left";
32
+ /** Start open */
33
+ defaultOpen?: boolean;
34
+ /** Widget title */
35
+ title?: string;
36
+ /** Placeholder text */
37
+ placeholder?: string;
38
+ /** Welcome message */
39
+ welcomeMessage?: string;
40
+ /** Accent color */
41
+ accentColor?: string;
42
+ /** Custom class for the container */
43
+ className?: string;
44
+ /** Custom styles for the container */
45
+ style?: CSSProperties;
46
+ }
47
+ declare function SyncAgentChat({ config, ...props }: SyncAgentChatProps): react_jsx_runtime.JSX.Element;
48
+
49
+ export { SyncAgentChat, type SyncAgentChatProps, SyncAgentProvider, useSyncAgent, useSyncAgentClient };
package/dist/index.js ADDED
@@ -0,0 +1,346 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ SyncAgentChat: () => SyncAgentChat,
25
+ SyncAgentClient: () => import_js2.SyncAgentClient,
26
+ SyncAgentProvider: () => SyncAgentProvider,
27
+ useSyncAgent: () => useSyncAgent,
28
+ useSyncAgentClient: () => useSyncAgentClient
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/provider.tsx
33
+ var import_react = require("react");
34
+ var import_js = require("@syncagent/js");
35
+ var import_jsx_runtime = require("react/jsx-runtime");
36
+ var SyncAgentContext = (0, import_react.createContext)(null);
37
+ function SyncAgentProvider({
38
+ config,
39
+ children
40
+ }) {
41
+ const client = (0, import_react.useMemo)(
42
+ () => new import_js.SyncAgentClient(config),
43
+ [config.apiKey, config.connectionString]
44
+ );
45
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SyncAgentContext.Provider, { value: client, children });
46
+ }
47
+ function useSyncAgentClient() {
48
+ const client = (0, import_react.useContext)(SyncAgentContext);
49
+ if (!client) throw new Error("useSyncAgentClient must be used within <SyncAgentProvider>");
50
+ return client;
51
+ }
52
+
53
+ // src/use-sync-agent.ts
54
+ var import_react2 = require("react");
55
+ function useSyncAgent(options = {}) {
56
+ let client;
57
+ try {
58
+ client = options.client || useSyncAgentClient();
59
+ } catch {
60
+ if (!options.client) throw new Error("Provide a client via options or wrap in <SyncAgentProvider>");
61
+ client = options.client;
62
+ }
63
+ const [messages, setMessages] = (0, import_react2.useState)([]);
64
+ const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
65
+ const [error, setError] = (0, import_react2.useState)(null);
66
+ const abortRef = (0, import_react2.useRef)(null);
67
+ const sendMessage = (0, import_react2.useCallback)(
68
+ async (content) => {
69
+ if (!content.trim() || isLoading) return;
70
+ const userMsg = { role: "user", content };
71
+ const updated = [...messages, userMsg];
72
+ setMessages(updated);
73
+ setIsLoading(true);
74
+ setError(null);
75
+ const placeholder = { role: "assistant", content: "" };
76
+ setMessages([...updated, placeholder]);
77
+ abortRef.current = new AbortController();
78
+ try {
79
+ await client.chat(updated, {
80
+ signal: abortRef.current.signal,
81
+ onToken: (token) => {
82
+ placeholder.content += token;
83
+ setMessages((prev) => {
84
+ const next = [...prev];
85
+ next[next.length - 1] = { ...placeholder };
86
+ return next;
87
+ });
88
+ },
89
+ onComplete: (text) => {
90
+ setMessages((prev) => {
91
+ const next = [...prev];
92
+ next[next.length - 1] = { role: "assistant", content: text };
93
+ return next;
94
+ });
95
+ }
96
+ });
97
+ } catch (e) {
98
+ if (e.name !== "AbortError") {
99
+ const err = e instanceof Error ? e : new Error(String(e));
100
+ setError(err);
101
+ setMessages((prev) => {
102
+ const last = prev[prev.length - 1];
103
+ return last?.content === "" ? prev.slice(0, -1) : prev;
104
+ });
105
+ }
106
+ } finally {
107
+ setIsLoading(false);
108
+ abortRef.current = null;
109
+ }
110
+ },
111
+ [client, messages, isLoading]
112
+ );
113
+ const stop = (0, import_react2.useCallback)(() => {
114
+ abortRef.current?.abort();
115
+ }, []);
116
+ const reset = (0, import_react2.useCallback)(() => {
117
+ abortRef.current?.abort();
118
+ setMessages([]);
119
+ setError(null);
120
+ setIsLoading(false);
121
+ }, []);
122
+ return { messages, isLoading, error, sendMessage, stop, reset };
123
+ }
124
+
125
+ // src/chat.tsx
126
+ var import_react3 = require("react");
127
+ var import_jsx_runtime2 = require("react/jsx-runtime");
128
+ function ChatInner({
129
+ mode = "floating",
130
+ position = "bottom-right",
131
+ defaultOpen = false,
132
+ title = "\u2728 AI Assistant",
133
+ placeholder = "Ask about your data...",
134
+ welcomeMessage = "Ask me anything about your data. I can query, create, update, and analyze your database.",
135
+ accentColor = "#10b981",
136
+ className,
137
+ style: customStyle
138
+ }) {
139
+ const { messages, isLoading, sendMessage } = useSyncAgent();
140
+ const [isOpen, setIsOpen] = (0, import_react3.useState)(defaultOpen);
141
+ const [input, setInput] = (0, import_react3.useState)("");
142
+ const messagesEndRef = (0, import_react3.useRef)(null);
143
+ (0, import_react3.useEffect)(() => {
144
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
145
+ }, [messages]);
146
+ const handleSubmit = (e) => {
147
+ e.preventDefault();
148
+ if (!input.trim()) return;
149
+ sendMessage(input);
150
+ setInput("");
151
+ };
152
+ const panel = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { ...styles.panel, ...mode === "inline" ? styles.panelInline : {}, ...customStyle }, className, children: [
153
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { ...styles.header, background: `linear-gradient(135deg, ${accentColor}, ${adjustColor(accentColor, -20)})` }, children: [
154
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: title }),
155
+ mode === "floating" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: styles.closeBtn, onClick: () => setIsOpen(false), children: "\xD7" })
156
+ ] }),
157
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.messages, children: [
158
+ messages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles.welcome, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { fontSize: 14, color: "#9ca3af" }, children: welcomeMessage }) }),
159
+ messages.map((msg, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
160
+ "div",
161
+ {
162
+ style: {
163
+ ...styles.msg,
164
+ ...msg.role === "user" ? { ...styles.msgUser, background: accentColor } : styles.msgAi
165
+ },
166
+ children: msg.content || (isLoading && i === messages.length - 1 ? "..." : "")
167
+ },
168
+ i
169
+ )),
170
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: messagesEndRef })
171
+ ] }),
172
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { style: styles.form, onSubmit: handleSubmit, children: [
173
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
174
+ "input",
175
+ {
176
+ style: styles.input,
177
+ value: input,
178
+ onChange: (e) => setInput(e.target.value),
179
+ placeholder,
180
+ disabled: isLoading
181
+ }
182
+ ),
183
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
184
+ "button",
185
+ {
186
+ type: "submit",
187
+ disabled: isLoading || !input.trim(),
188
+ style: { ...styles.sendBtn, background: accentColor },
189
+ children: "Send"
190
+ }
191
+ )
192
+ ] })
193
+ ] });
194
+ if (mode === "inline") return panel;
195
+ const isLeft = position === "bottom-left";
196
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
197
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...styles.floatingPanel, ...isLeft ? { left: 24, right: "auto" } : {} }, children: panel }),
198
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
199
+ "button",
200
+ {
201
+ onClick: () => setIsOpen(!isOpen),
202
+ style: {
203
+ ...styles.fab,
204
+ background: `linear-gradient(135deg, ${accentColor}, ${adjustColor(accentColor, -20)})`,
205
+ ...isLeft ? { left: 24, right: "auto" } : {}
206
+ },
207
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "white", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z" }) })
208
+ }
209
+ )
210
+ ] });
211
+ }
212
+ function SyncAgentChat({ config, ...props }) {
213
+ if (config) {
214
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SyncAgentProvider, { config, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ChatInner, { ...props }) });
215
+ }
216
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ChatInner, { ...props });
217
+ }
218
+ function adjustColor(hex, amount) {
219
+ const num = parseInt(hex.replace("#", ""), 16);
220
+ const r = Math.min(255, Math.max(0, (num >> 16 & 255) + amount));
221
+ const g = Math.min(255, Math.max(0, (num >> 8 & 255) + amount));
222
+ const b = Math.min(255, Math.max(0, (num & 255) + amount));
223
+ return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, "0")}`;
224
+ }
225
+ var styles = {
226
+ fab: {
227
+ position: "fixed",
228
+ bottom: 24,
229
+ right: 24,
230
+ zIndex: 99999,
231
+ width: 56,
232
+ height: 56,
233
+ borderRadius: "50%",
234
+ border: "none",
235
+ cursor: "pointer",
236
+ boxShadow: "0 4px 20px rgba(0,0,0,0.2)",
237
+ display: "flex",
238
+ alignItems: "center",
239
+ justifyContent: "center"
240
+ },
241
+ floatingPanel: {
242
+ position: "fixed",
243
+ bottom: 96,
244
+ right: 24,
245
+ zIndex: 99998,
246
+ width: 400,
247
+ maxWidth: "calc(100vw - 48px)"
248
+ },
249
+ panel: {
250
+ height: 560,
251
+ maxHeight: "calc(100vh - 120px)",
252
+ background: "#fff",
253
+ borderRadius: 16,
254
+ boxShadow: "0 8px 40px rgba(0,0,0,0.15)",
255
+ display: "flex",
256
+ flexDirection: "column",
257
+ overflow: "hidden",
258
+ fontFamily: "system-ui, -apple-system, sans-serif"
259
+ },
260
+ panelInline: {
261
+ height: "100%",
262
+ maxHeight: "none",
263
+ borderRadius: 12,
264
+ boxShadow: "0 2px 12px rgba(0,0,0,0.1)"
265
+ },
266
+ header: {
267
+ padding: "16px",
268
+ color: "white",
269
+ fontWeight: 600,
270
+ fontSize: 15,
271
+ display: "flex",
272
+ alignItems: "center",
273
+ justifyContent: "space-between"
274
+ },
275
+ closeBtn: {
276
+ background: "none",
277
+ border: "none",
278
+ color: "white",
279
+ cursor: "pointer",
280
+ padding: 4,
281
+ fontSize: 20
282
+ },
283
+ messages: {
284
+ flex: 1,
285
+ overflowY: "auto",
286
+ padding: 16,
287
+ display: "flex",
288
+ flexDirection: "column",
289
+ gap: 12
290
+ },
291
+ welcome: { textAlign: "center", padding: "40px 20px" },
292
+ msg: {
293
+ maxWidth: "85%",
294
+ padding: "10px 14px",
295
+ borderRadius: 12,
296
+ fontSize: 14,
297
+ lineHeight: 1.5,
298
+ wordBreak: "break-word",
299
+ whiteSpace: "pre-wrap"
300
+ },
301
+ msgUser: {
302
+ alignSelf: "flex-end",
303
+ color: "white",
304
+ borderBottomRightRadius: 4
305
+ },
306
+ msgAi: {
307
+ alignSelf: "flex-start",
308
+ background: "#f3f4f6",
309
+ color: "#1f2937",
310
+ borderBottomLeftRadius: 4
311
+ },
312
+ form: {
313
+ padding: "12px 16px",
314
+ borderTop: "1px solid #e5e7eb",
315
+ display: "flex",
316
+ gap: 8
317
+ },
318
+ input: {
319
+ flex: 1,
320
+ padding: "10px 14px",
321
+ border: "1px solid #d1d5db",
322
+ borderRadius: 10,
323
+ fontSize: 14,
324
+ outline: "none"
325
+ },
326
+ sendBtn: {
327
+ padding: "10px 16px",
328
+ color: "white",
329
+ border: "none",
330
+ borderRadius: 10,
331
+ cursor: "pointer",
332
+ fontWeight: 600,
333
+ fontSize: 14
334
+ }
335
+ };
336
+
337
+ // src/index.ts
338
+ var import_js2 = require("@syncagent/js");
339
+ // Annotate the CommonJS export names for ESM import in node:
340
+ 0 && (module.exports = {
341
+ SyncAgentChat,
342
+ SyncAgentClient,
343
+ SyncAgentProvider,
344
+ useSyncAgent,
345
+ useSyncAgentClient
346
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,316 @@
1
+ "use client";
2
+
3
+ // src/provider.tsx
4
+ import { createContext, useContext, useMemo } from "react";
5
+ import { SyncAgentClient } from "@syncagent/js";
6
+ import { jsx } from "react/jsx-runtime";
7
+ var SyncAgentContext = createContext(null);
8
+ function SyncAgentProvider({
9
+ config,
10
+ children
11
+ }) {
12
+ const client = useMemo(
13
+ () => new SyncAgentClient(config),
14
+ [config.apiKey, config.connectionString]
15
+ );
16
+ return /* @__PURE__ */ jsx(SyncAgentContext.Provider, { value: client, children });
17
+ }
18
+ function useSyncAgentClient() {
19
+ const client = useContext(SyncAgentContext);
20
+ if (!client) throw new Error("useSyncAgentClient must be used within <SyncAgentProvider>");
21
+ return client;
22
+ }
23
+
24
+ // src/use-sync-agent.ts
25
+ import { useState, useCallback, useRef } from "react";
26
+ function useSyncAgent(options = {}) {
27
+ let client;
28
+ try {
29
+ client = options.client || useSyncAgentClient();
30
+ } catch {
31
+ if (!options.client) throw new Error("Provide a client via options or wrap in <SyncAgentProvider>");
32
+ client = options.client;
33
+ }
34
+ const [messages, setMessages] = useState([]);
35
+ const [isLoading, setIsLoading] = useState(false);
36
+ const [error, setError] = useState(null);
37
+ const abortRef = useRef(null);
38
+ const sendMessage = useCallback(
39
+ async (content) => {
40
+ if (!content.trim() || isLoading) return;
41
+ const userMsg = { role: "user", content };
42
+ const updated = [...messages, userMsg];
43
+ setMessages(updated);
44
+ setIsLoading(true);
45
+ setError(null);
46
+ const placeholder = { role: "assistant", content: "" };
47
+ setMessages([...updated, placeholder]);
48
+ abortRef.current = new AbortController();
49
+ try {
50
+ await client.chat(updated, {
51
+ signal: abortRef.current.signal,
52
+ onToken: (token) => {
53
+ placeholder.content += token;
54
+ setMessages((prev) => {
55
+ const next = [...prev];
56
+ next[next.length - 1] = { ...placeholder };
57
+ return next;
58
+ });
59
+ },
60
+ onComplete: (text) => {
61
+ setMessages((prev) => {
62
+ const next = [...prev];
63
+ next[next.length - 1] = { role: "assistant", content: text };
64
+ return next;
65
+ });
66
+ }
67
+ });
68
+ } catch (e) {
69
+ if (e.name !== "AbortError") {
70
+ const err = e instanceof Error ? e : new Error(String(e));
71
+ setError(err);
72
+ setMessages((prev) => {
73
+ const last = prev[prev.length - 1];
74
+ return last?.content === "" ? prev.slice(0, -1) : prev;
75
+ });
76
+ }
77
+ } finally {
78
+ setIsLoading(false);
79
+ abortRef.current = null;
80
+ }
81
+ },
82
+ [client, messages, isLoading]
83
+ );
84
+ const stop = useCallback(() => {
85
+ abortRef.current?.abort();
86
+ }, []);
87
+ const reset = useCallback(() => {
88
+ abortRef.current?.abort();
89
+ setMessages([]);
90
+ setError(null);
91
+ setIsLoading(false);
92
+ }, []);
93
+ return { messages, isLoading, error, sendMessage, stop, reset };
94
+ }
95
+
96
+ // src/chat.tsx
97
+ import { useState as useState2, useRef as useRef2, useEffect } from "react";
98
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
99
+ function ChatInner({
100
+ mode = "floating",
101
+ position = "bottom-right",
102
+ defaultOpen = false,
103
+ title = "\u2728 AI Assistant",
104
+ placeholder = "Ask about your data...",
105
+ welcomeMessage = "Ask me anything about your data. I can query, create, update, and analyze your database.",
106
+ accentColor = "#10b981",
107
+ className,
108
+ style: customStyle
109
+ }) {
110
+ const { messages, isLoading, sendMessage } = useSyncAgent();
111
+ const [isOpen, setIsOpen] = useState2(defaultOpen);
112
+ const [input, setInput] = useState2("");
113
+ const messagesEndRef = useRef2(null);
114
+ useEffect(() => {
115
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
116
+ }, [messages]);
117
+ const handleSubmit = (e) => {
118
+ e.preventDefault();
119
+ if (!input.trim()) return;
120
+ sendMessage(input);
121
+ setInput("");
122
+ };
123
+ const panel = /* @__PURE__ */ jsxs("div", { style: { ...styles.panel, ...mode === "inline" ? styles.panelInline : {}, ...customStyle }, className, children: [
124
+ /* @__PURE__ */ jsxs("div", { style: { ...styles.header, background: `linear-gradient(135deg, ${accentColor}, ${adjustColor(accentColor, -20)})` }, children: [
125
+ /* @__PURE__ */ jsx2("span", { children: title }),
126
+ mode === "floating" && /* @__PURE__ */ jsx2("button", { style: styles.closeBtn, onClick: () => setIsOpen(false), children: "\xD7" })
127
+ ] }),
128
+ /* @__PURE__ */ jsxs("div", { style: styles.messages, children: [
129
+ messages.length === 0 && /* @__PURE__ */ jsx2("div", { style: styles.welcome, children: /* @__PURE__ */ jsx2("p", { style: { fontSize: 14, color: "#9ca3af" }, children: welcomeMessage }) }),
130
+ messages.map((msg, i) => /* @__PURE__ */ jsx2(
131
+ "div",
132
+ {
133
+ style: {
134
+ ...styles.msg,
135
+ ...msg.role === "user" ? { ...styles.msgUser, background: accentColor } : styles.msgAi
136
+ },
137
+ children: msg.content || (isLoading && i === messages.length - 1 ? "..." : "")
138
+ },
139
+ i
140
+ )),
141
+ /* @__PURE__ */ jsx2("div", { ref: messagesEndRef })
142
+ ] }),
143
+ /* @__PURE__ */ jsxs("form", { style: styles.form, onSubmit: handleSubmit, children: [
144
+ /* @__PURE__ */ jsx2(
145
+ "input",
146
+ {
147
+ style: styles.input,
148
+ value: input,
149
+ onChange: (e) => setInput(e.target.value),
150
+ placeholder,
151
+ disabled: isLoading
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsx2(
155
+ "button",
156
+ {
157
+ type: "submit",
158
+ disabled: isLoading || !input.trim(),
159
+ style: { ...styles.sendBtn, background: accentColor },
160
+ children: "Send"
161
+ }
162
+ )
163
+ ] })
164
+ ] });
165
+ if (mode === "inline") return panel;
166
+ const isLeft = position === "bottom-left";
167
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
168
+ isOpen && /* @__PURE__ */ jsx2("div", { style: { ...styles.floatingPanel, ...isLeft ? { left: 24, right: "auto" } : {} }, children: panel }),
169
+ /* @__PURE__ */ jsx2(
170
+ "button",
171
+ {
172
+ onClick: () => setIsOpen(!isOpen),
173
+ style: {
174
+ ...styles.fab,
175
+ background: `linear-gradient(135deg, ${accentColor}, ${adjustColor(accentColor, -20)})`,
176
+ ...isLeft ? { left: 24, right: "auto" } : {}
177
+ },
178
+ children: /* @__PURE__ */ jsx2("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "white", children: /* @__PURE__ */ jsx2("path", { d: "M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z" }) })
179
+ }
180
+ )
181
+ ] });
182
+ }
183
+ function SyncAgentChat({ config, ...props }) {
184
+ if (config) {
185
+ return /* @__PURE__ */ jsx2(SyncAgentProvider, { config, children: /* @__PURE__ */ jsx2(ChatInner, { ...props }) });
186
+ }
187
+ return /* @__PURE__ */ jsx2(ChatInner, { ...props });
188
+ }
189
+ function adjustColor(hex, amount) {
190
+ const num = parseInt(hex.replace("#", ""), 16);
191
+ const r = Math.min(255, Math.max(0, (num >> 16 & 255) + amount));
192
+ const g = Math.min(255, Math.max(0, (num >> 8 & 255) + amount));
193
+ const b = Math.min(255, Math.max(0, (num & 255) + amount));
194
+ return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, "0")}`;
195
+ }
196
+ var styles = {
197
+ fab: {
198
+ position: "fixed",
199
+ bottom: 24,
200
+ right: 24,
201
+ zIndex: 99999,
202
+ width: 56,
203
+ height: 56,
204
+ borderRadius: "50%",
205
+ border: "none",
206
+ cursor: "pointer",
207
+ boxShadow: "0 4px 20px rgba(0,0,0,0.2)",
208
+ display: "flex",
209
+ alignItems: "center",
210
+ justifyContent: "center"
211
+ },
212
+ floatingPanel: {
213
+ position: "fixed",
214
+ bottom: 96,
215
+ right: 24,
216
+ zIndex: 99998,
217
+ width: 400,
218
+ maxWidth: "calc(100vw - 48px)"
219
+ },
220
+ panel: {
221
+ height: 560,
222
+ maxHeight: "calc(100vh - 120px)",
223
+ background: "#fff",
224
+ borderRadius: 16,
225
+ boxShadow: "0 8px 40px rgba(0,0,0,0.15)",
226
+ display: "flex",
227
+ flexDirection: "column",
228
+ overflow: "hidden",
229
+ fontFamily: "system-ui, -apple-system, sans-serif"
230
+ },
231
+ panelInline: {
232
+ height: "100%",
233
+ maxHeight: "none",
234
+ borderRadius: 12,
235
+ boxShadow: "0 2px 12px rgba(0,0,0,0.1)"
236
+ },
237
+ header: {
238
+ padding: "16px",
239
+ color: "white",
240
+ fontWeight: 600,
241
+ fontSize: 15,
242
+ display: "flex",
243
+ alignItems: "center",
244
+ justifyContent: "space-between"
245
+ },
246
+ closeBtn: {
247
+ background: "none",
248
+ border: "none",
249
+ color: "white",
250
+ cursor: "pointer",
251
+ padding: 4,
252
+ fontSize: 20
253
+ },
254
+ messages: {
255
+ flex: 1,
256
+ overflowY: "auto",
257
+ padding: 16,
258
+ display: "flex",
259
+ flexDirection: "column",
260
+ gap: 12
261
+ },
262
+ welcome: { textAlign: "center", padding: "40px 20px" },
263
+ msg: {
264
+ maxWidth: "85%",
265
+ padding: "10px 14px",
266
+ borderRadius: 12,
267
+ fontSize: 14,
268
+ lineHeight: 1.5,
269
+ wordBreak: "break-word",
270
+ whiteSpace: "pre-wrap"
271
+ },
272
+ msgUser: {
273
+ alignSelf: "flex-end",
274
+ color: "white",
275
+ borderBottomRightRadius: 4
276
+ },
277
+ msgAi: {
278
+ alignSelf: "flex-start",
279
+ background: "#f3f4f6",
280
+ color: "#1f2937",
281
+ borderBottomLeftRadius: 4
282
+ },
283
+ form: {
284
+ padding: "12px 16px",
285
+ borderTop: "1px solid #e5e7eb",
286
+ display: "flex",
287
+ gap: 8
288
+ },
289
+ input: {
290
+ flex: 1,
291
+ padding: "10px 14px",
292
+ border: "1px solid #d1d5db",
293
+ borderRadius: 10,
294
+ fontSize: 14,
295
+ outline: "none"
296
+ },
297
+ sendBtn: {
298
+ padding: "10px 16px",
299
+ color: "white",
300
+ border: "none",
301
+ borderRadius: 10,
302
+ cursor: "pointer",
303
+ fontWeight: 600,
304
+ fontSize: 14
305
+ }
306
+ };
307
+
308
+ // src/index.ts
309
+ import { SyncAgentClient as SyncAgentClient2 } from "@syncagent/js";
310
+ export {
311
+ SyncAgentChat,
312
+ SyncAgentClient2 as SyncAgentClient,
313
+ SyncAgentProvider,
314
+ useSyncAgent,
315
+ useSyncAgentClient
316
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@syncagent/react",
3
+ "version": "0.1.0",
4
+ "description": "SyncAgent React SDK — AI database chat widget & hooks",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": ["dist"],
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "dev": "tsup --watch"
19
+ },
20
+ "peerDependencies": {
21
+ "react": ">=18.0.0",
22
+ "react-dom": ">=18.0.0",
23
+ "@syncagent/js": ">=0.1.0"
24
+ },
25
+ "devDependencies": {
26
+ "@syncagent/js": "file:../js",
27
+ "@types/react": "^19.1.4",
28
+ "@types/react-dom": "^19.1.5",
29
+ "react": "^19.1.0",
30
+ "react-dom": "^19.1.0",
31
+ "tsup": "^8.4.0",
32
+ "typescript": "^5.8.3"
33
+ },
34
+ "license": "MIT",
35
+ "keywords": ["syncagent", "ai", "database", "agent", "react", "chat", "widget"]
36
+ }