@syncagent/react 0.1.8 → 0.1.9

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/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, CSSProperties } from 'react';
3
- import { SyncAgentConfig, SyncAgentClient, Message } from '@syncagent/js';
3
+ import { SyncAgentConfig, SyncAgentClient, ToolData, Message } from '@syncagent/js';
4
4
  export { ChatOptions, Message, SyncAgentClient, SyncAgentConfig, ToolDefinition, ToolParameter } from '@syncagent/js';
5
5
 
6
6
  declare function SyncAgentProvider({ config, children, }: {
@@ -11,6 +11,10 @@ declare function useSyncAgentClient(): SyncAgentClient;
11
11
 
12
12
  interface UseSyncAgentOptions {
13
13
  client?: SyncAgentClient;
14
+ /** Extra context injected into every message */
15
+ context?: Record<string, any>;
16
+ /** Called when a DB tool returns structured data */
17
+ onData?: (data: ToolData) => void;
14
18
  }
15
19
  declare function useSyncAgent(options?: UseSyncAgentOptions): {
16
20
  messages: Message[];
@@ -20,6 +24,7 @@ declare function useSyncAgent(options?: UseSyncAgentOptions): {
20
24
  step: string;
21
25
  label: string;
22
26
  } | null;
27
+ lastData: ToolData | null;
23
28
  sendMessage: (content: string) => Promise<void>;
24
29
  stop: () => void;
25
30
  reset: () => void;
@@ -38,10 +43,14 @@ interface SyncAgentChatProps {
38
43
  className?: string;
39
44
  style?: CSSProperties;
40
45
  suggestions?: string[];
41
- /** Unique key for localStorage persistence — use project ID */
46
+ /** Unique key for localStorage persistence */
42
47
  persistKey?: string;
48
+ /** Extra context injected into every message */
49
+ context?: Record<string, any>;
43
50
  /** Called when user reacts to a message */
44
51
  onReaction?: (messageIndex: number, reaction: "up" | "down", content: string) => void;
52
+ /** Called when a DB tool returns structured data */
53
+ onData?: (data: ToolData) => void;
45
54
  }
46
55
  declare function SyncAgentChat({ config, ...props }: SyncAgentChatProps): react_jsx_runtime.JSX.Element;
47
56
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, CSSProperties } from 'react';
3
- import { SyncAgentConfig, SyncAgentClient, Message } from '@syncagent/js';
3
+ import { SyncAgentConfig, SyncAgentClient, ToolData, Message } from '@syncagent/js';
4
4
  export { ChatOptions, Message, SyncAgentClient, SyncAgentConfig, ToolDefinition, ToolParameter } from '@syncagent/js';
5
5
 
6
6
  declare function SyncAgentProvider({ config, children, }: {
@@ -11,6 +11,10 @@ declare function useSyncAgentClient(): SyncAgentClient;
11
11
 
12
12
  interface UseSyncAgentOptions {
13
13
  client?: SyncAgentClient;
14
+ /** Extra context injected into every message */
15
+ context?: Record<string, any>;
16
+ /** Called when a DB tool returns structured data */
17
+ onData?: (data: ToolData) => void;
14
18
  }
15
19
  declare function useSyncAgent(options?: UseSyncAgentOptions): {
16
20
  messages: Message[];
@@ -20,6 +24,7 @@ declare function useSyncAgent(options?: UseSyncAgentOptions): {
20
24
  step: string;
21
25
  label: string;
22
26
  } | null;
27
+ lastData: ToolData | null;
23
28
  sendMessage: (content: string) => Promise<void>;
24
29
  stop: () => void;
25
30
  reset: () => void;
@@ -38,10 +43,14 @@ interface SyncAgentChatProps {
38
43
  className?: string;
39
44
  style?: CSSProperties;
40
45
  suggestions?: string[];
41
- /** Unique key for localStorage persistence — use project ID */
46
+ /** Unique key for localStorage persistence */
42
47
  persistKey?: string;
48
+ /** Extra context injected into every message */
49
+ context?: Record<string, any>;
43
50
  /** Called when user reacts to a message */
44
51
  onReaction?: (messageIndex: number, reaction: "up" | "down", content: string) => void;
52
+ /** Called when a DB tool returns structured data */
53
+ onData?: (data: ToolData) => void;
45
54
  }
46
55
  declare function SyncAgentChat({ config, ...props }: SyncAgentChatProps): react_jsx_runtime.JSX.Element;
47
56
 
package/dist/index.js CHANGED
@@ -64,6 +64,7 @@ function useSyncAgent(options = {}) {
64
64
  const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
65
65
  const [error, setError] = (0, import_react2.useState)(null);
66
66
  const [status, setStatus] = (0, import_react2.useState)(null);
67
+ const [lastData, setLastData] = (0, import_react2.useState)(null);
67
68
  const abortRef = (0, import_react2.useRef)(null);
68
69
  const sendMessage = (0, import_react2.useCallback)(
69
70
  async (content) => {
@@ -74,13 +75,19 @@ function useSyncAgent(options = {}) {
74
75
  setIsLoading(true);
75
76
  setError(null);
76
77
  setStatus(null);
78
+ setLastData(null);
77
79
  const placeholder = { role: "assistant", content: "" };
78
80
  setMessages([...updated, placeholder]);
79
81
  abortRef.current = new AbortController();
80
82
  try {
81
83
  await client.chat(updated, {
82
84
  signal: abortRef.current.signal,
85
+ context: options.context,
83
86
  onStatus: (step, label) => setStatus({ step, label }),
87
+ onData: (data) => {
88
+ setLastData(data);
89
+ options.onData?.(data);
90
+ },
84
91
  onToken: (token) => {
85
92
  placeholder.content += token;
86
93
  setMessages((prev) => {
@@ -113,7 +120,7 @@ function useSyncAgent(options = {}) {
113
120
  abortRef.current = null;
114
121
  }
115
122
  },
116
- [client, messages, isLoading]
123
+ [client, messages, isLoading, options.context]
117
124
  );
118
125
  const stop = (0, import_react2.useCallback)(() => {
119
126
  abortRef.current?.abort();
@@ -124,41 +131,70 @@ function useSyncAgent(options = {}) {
124
131
  setError(null);
125
132
  setIsLoading(false);
126
133
  setStatus(null);
134
+ setLastData(null);
127
135
  }, []);
128
- return { messages, isLoading, error, status, sendMessage, stop, reset };
136
+ return { messages, isLoading, error, status, lastData, sendMessage, stop, reset };
129
137
  }
130
138
 
131
139
  // src/chat.tsx
132
140
  var import_react3 = require("react");
133
141
  var import_jsx_runtime2 = require("react/jsx-runtime");
134
- function loadHistory(key) {
135
- try {
136
- const raw = localStorage.getItem(`sa_chat_${key}`);
137
- if (!raw) return null;
138
- const parsed = JSON.parse(raw);
139
- if (parsed.timestamps) {
140
- parsed.timestamps = parsed.timestamps.map((t) => t ? new Date(t) : null);
142
+ var LS = {
143
+ load: (key) => {
144
+ try {
145
+ const r = localStorage.getItem(`sa_${key}`);
146
+ return r ? JSON.parse(r) : null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ },
151
+ save: (key, v) => {
152
+ try {
153
+ localStorage.setItem(`sa_${key}`, JSON.stringify(v));
154
+ } catch {
155
+ }
156
+ },
157
+ del: (key) => {
158
+ try {
159
+ localStorage.removeItem(`sa_${key}`);
160
+ } catch {
141
161
  }
142
- return parsed;
143
- } catch {
144
- return null;
145
162
  }
163
+ };
164
+ function mdTableToCSV(text) {
165
+ const match = text.match(/\|(.+)\|\r?\n\|[-| :]+\|\r?\n((?:\|.+\|\r?\n?)+)/);
166
+ if (!match) return null;
167
+ const headers = match[1].split("|").map((c) => c.trim()).filter(Boolean);
168
+ const rows = match[2].trim().split("\n").map(
169
+ (row) => row.split("|").map((c) => c.trim()).filter(Boolean)
170
+ );
171
+ const escape = (v) => `"${v.replace(/"/g, '""')}"`;
172
+ return [headers.map(escape).join(","), ...rows.map((r) => r.map(escape).join(","))].join("\n");
146
173
  }
147
- function saveHistory(key, messages, timestamps) {
148
- try {
149
- localStorage.setItem(`sa_chat_${key}`, JSON.stringify({
150
- messages,
151
- timestamps: timestamps.map((t) => t?.toISOString() ?? null),
152
- savedAt: (/* @__PURE__ */ new Date()).toISOString()
153
- }));
154
- } catch {
155
- }
174
+ function downloadCSV(csv, filename = "syncagent-export.csv") {
175
+ const blob = new Blob([csv], { type: "text/csv" });
176
+ const url = URL.createObjectURL(blob);
177
+ const a = document.createElement("a");
178
+ a.href = url;
179
+ a.download = filename;
180
+ a.click();
181
+ URL.revokeObjectURL(url);
156
182
  }
157
- function clearHistory(key) {
158
- try {
159
- localStorage.removeItem(`sa_chat_${key}`);
160
- } catch {
161
- }
183
+ function BarChart({ data, accent }) {
184
+ const max = Math.max(...data.map((d) => d.value), 1);
185
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { margin: "10px 0", padding: "12px", background: "#f8fafc", borderRadius: 10, border: "1px solid #e2e8f0" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", alignItems: "flex-end", gap: 6, height: 80 }, children: data.slice(0, 12).map((d, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 3, minWidth: 0 }, children: [
186
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: "100%", background: `${accent}cc`, borderRadius: "3px 3px 0 0", height: `${d.value / max * 68}px`, minHeight: 2, transition: "height .3s" }, title: `${d.label}: ${d.value}` }),
187
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 9, color: "#94a3b8", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", width: "100%", textAlign: "center" }, children: d.label })
188
+ ] }, i)) }) });
189
+ }
190
+ function detectChartData(data) {
191
+ if (!data?.data?.length) return null;
192
+ const rows = data.data;
193
+ const keys = Object.keys(rows[0]);
194
+ const labelKey = keys.find((k) => typeof rows[0][k] === "string" || k === "_id");
195
+ const valueKey = keys.find((k) => typeof rows[0][k] === "number" && k !== labelKey);
196
+ if (!labelKey || !valueKey) return null;
197
+ return rows.map((r) => ({ label: String(r[labelKey] ?? ""), value: Number(r[valueKey] ?? 0) }));
162
198
  }
163
199
  var CSS = (accent) => `
164
200
  @keyframes sa-bounce { 0%,80%,100%{transform:translateY(0);opacity:.35} 40%{transform:translateY(-5px);opacity:1} }
@@ -175,13 +211,10 @@ var CSS = (accent) => `
175
211
  .sa-send:disabled { opacity: .4; cursor: not-allowed }
176
212
  .sa-act:hover { opacity: 1 !important; background: rgba(0,0,0,.06) !important }
177
213
  .sa-react:hover { transform: scale(1.2) }
178
- .sa-react.active { transform: scale(1.15) }
179
214
  .sa-scroll::-webkit-scrollbar { width: 4px }
180
215
  .sa-scroll::-webkit-scrollbar-track { background: transparent }
181
216
  .sa-scroll::-webkit-scrollbar-thumb { background: rgba(0,0,0,.12); border-radius: 4px }
182
217
  .sa-cursor { display:inline-block; width:2px; height:1em; background:currentColor; margin-left:1px; vertical-align:text-bottom; animation: sa-cursor .7s infinite }
183
- .sa-resize { cursor: ns-resize; user-select: none }
184
- .sa-resize:hover::after { opacity: 1 }
185
218
  .sa-md table { width:100%; border-collapse:collapse; font-size:12.5px; margin:10px 0 }
186
219
  .sa-md th { padding:7px 10px; text-align:left; font-weight:600; background:rgba(0,0,0,.04); border-bottom:2px solid rgba(0,0,0,.1); white-space:nowrap }
187
220
  .sa-md td { padding:6px 10px; border-bottom:1px solid rgba(0,0,0,.06); vertical-align:top }
@@ -235,35 +268,29 @@ function CopyBtn({ text }) {
235
268
  "Copy"
236
269
  ] }) });
237
270
  }
238
- function ReactionBtns({ idx, reactions, onReact, accent }) {
239
- const cur = reactions[idx];
240
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", gap: 2 }, children: ["up", "down"].map((r) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: `sa-react${cur === r ? " active" : ""}`, onClick: () => onReact(idx, r), title: r === "up" ? "Helpful" : "Not helpful", style: {
241
- background: cur === r ? r === "up" ? `${accent}18` : "#fef2f2" : "none",
242
- border: cur === r ? `1px solid ${r === "up" ? accent + "44" : "#fecaca"}` : "none",
243
- cursor: "pointer",
244
- padding: "3px 6px",
245
- borderRadius: 5,
246
- fontSize: 13,
247
- transition: "all .15s",
248
- opacity: cur && cur !== r ? 0.3 : 0.7
249
- }, children: r === "up" ? "\u{1F44D}" : "\u{1F44E}" }, r)) });
250
- }
251
- function Bubble({ role, content, streaming, accent, time, idx, reactions, onReact, onRetry, hasError }) {
271
+ function Bubble({ role, content, streaming, accent, time, idx, reactions, onReact, onRetry, chartData, hasTable }) {
252
272
  const isUser = role === "user";
253
273
  const t = time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
274
+ const cur = reactions[idx];
275
+ const csv = hasTable ? mdTableToCSV(content) : null;
254
276
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "sa-msg", style: { display: "flex", flexDirection: "column", alignItems: isUser ? "flex-end" : "flex-start", gap: 4, maxWidth: "90%", alignSelf: isUser ? "flex-end" : "flex-start" }, children: [
255
277
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6, flexDirection: isUser ? "row-reverse" : "row" }, children: [
256
278
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: 26, height: 26, borderRadius: "50%", flexShrink: 0, background: isUser ? `linear-gradient(135deg,${accent},${adj(accent, -25)})` : "linear-gradient(135deg,#6366f1,#8b5cf6)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 11, color: "white", fontWeight: 700, boxShadow: isUser ? `0 2px 8px ${accent}44` : "0 2px 8px #6366f144" }, children: isUser ? "U" : "\u2726" }),
257
279
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 11.5, fontWeight: 600, color: isUser ? "#475569" : "#6366f1" }, children: isUser ? "You" : "SyncAgent" }),
258
280
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 10, color: "#cbd5e1" }, children: t })
259
281
  ] }),
260
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: isUser ? "10px 14px" : "12px 16px", borderRadius: isUser ? "18px 4px 18px 18px" : "4px 18px 18px 18px", background: isUser ? `linear-gradient(135deg,${accent},${adj(accent, -20)})` : "#ffffff", color: isUser ? "white" : "#1e293b", fontSize: 13.5, lineHeight: 1.65, wordBreak: "break-word", border: isUser ? "none" : "1px solid #e8edf3", boxShadow: isUser ? `0 4px 16px ${accent}33` : "0 2px 12px rgba(0,0,0,0.07)", maxWidth: "100%", animation: hasError ? "sa-shake .3s ease" : "none" }, children: [
282
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: isUser ? "10px 14px" : "12px 16px", borderRadius: isUser ? "18px 4px 18px 18px" : "4px 18px 18px 18px", background: isUser ? `linear-gradient(135deg,${accent},${adj(accent, -20)})` : "#ffffff", color: isUser ? "white" : "#1e293b", fontSize: 13.5, lineHeight: 1.65, wordBreak: "break-word", border: isUser ? "none" : "1px solid #e8edf3", boxShadow: isUser ? `0 4px 16px ${accent}33` : "0 2px 12px rgba(0,0,0,0.07)", maxWidth: "100%" }, children: [
261
283
  streaming && !content ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dots, { color: accent }) : isUser ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { whiteSpace: "pre-wrap" }, children: content }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "sa-md", dangerouslySetInnerHTML: { __html: md(content) } }),
262
284
  streaming && content && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "sa-cursor", style: { color: accent } })
263
285
  ] }),
264
- !isUser && content && !streaming && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { paddingLeft: 32, display: "flex", gap: 2, alignItems: "center" }, children: [
286
+ !isUser && !streaming && chartData && chartData.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { paddingLeft: 32, width: "calc(100% - 32px)" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BarChart, { data: chartData, accent }) }),
287
+ !isUser && content && !streaming && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { paddingLeft: 32, display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }, children: [
265
288
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CopyBtn, { text: content }),
266
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ReactionBtns, { idx, reactions, onReact, accent }),
289
+ csv && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: "sa-act", onClick: () => downloadCSV(csv), style: { background: "none", border: "none", cursor: "pointer", padding: "3px 7px", borderRadius: 5, fontSize: 11, color: "#94a3b8", opacity: 0.65, transition: "all .15s", display: "flex", alignItems: "center", gap: 3 }, children: [
290
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" }) }),
291
+ "CSV"
292
+ ] }),
293
+ ["up", "down"].map((r) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "sa-react", onClick: () => onReact(idx, r), style: { background: cur === r ? r === "up" ? `${accent}18` : "#fef2f2" : "none", border: cur === r ? `1px solid ${r === "up" ? accent + "44" : "#fecaca"}` : "none", cursor: "pointer", padding: "3px 6px", borderRadius: 5, fontSize: 13, transition: "all .15s", opacity: cur && cur !== r ? 0.3 : 0.7 }, children: r === "up" ? "\u{1F44D}" : "\u{1F44E}" }, r)),
267
294
  onRetry && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: "sa-act", onClick: onRetry, style: { background: "none", border: "none", cursor: "pointer", padding: "3px 7px", borderRadius: 5, fontSize: 11, color: "#94a3b8", opacity: 0.65, transition: "all .15s", display: "flex", alignItems: "center", gap: 3 }, children: [
268
295
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" }) }),
269
296
  "Retry"
@@ -271,8 +298,11 @@ function Bubble({ role, content, streaming, accent, time, idx, reactions, onReac
271
298
  ] })
272
299
  ] });
273
300
  }
274
- function Chips({ items, onPick, accent }) {
275
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, padding: "0 14px 10px" }, children: items.map((s) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "sa-chip", onClick: () => onPick(s), style: { padding: "5px 13px", borderRadius: 20, fontSize: 12, cursor: "pointer", border: `1px solid ${accent}33`, background: `${accent}0a`, color: accent, fontWeight: 500, transition: "all .15s", whiteSpace: "nowrap" }, children: s }, s)) });
301
+ function Chips({ items, onPick, accent, onPin, pinned }) {
302
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, padding: "0 14px 10px" }, children: items.map((s) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 0 }, children: [
303
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "sa-chip", onClick: () => onPick(s), style: { padding: "5px 10px 5px 13px", borderRadius: onPin ? "20px 0 0 20px" : "20px", fontSize: 12, cursor: "pointer", border: `1px solid ${accent}33`, borderRight: onPin ? "none" : "", background: `${accent}0a`, color: accent, fontWeight: 500, transition: "all .15s", whiteSpace: "nowrap" }, children: s }),
304
+ onPin && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => onPin(s), title: pinned?.includes(s) ? "Unpin" : "Pin", style: { padding: "5px 7px", borderRadius: "0 20px 20px 0", fontSize: 10, cursor: "pointer", border: `1px solid ${accent}33`, borderLeft: "none", background: pinned?.includes(s) ? `${accent}18` : `${accent}0a`, color: accent, transition: "all .15s" }, children: pinned?.includes(s) ? "\u{1F4CC}" : "\u{1F4CD}" })
305
+ ] }, s)) });
276
306
  }
277
307
  function ChatInner({
278
308
  mode = "floating",
@@ -287,40 +317,48 @@ function ChatInner({
287
317
  style: customStyle,
288
318
  suggestions = ["Show all records", "Count total entries", "Show recent activity"],
289
319
  persistKey,
290
- onReaction
320
+ context,
321
+ onReaction,
322
+ onData
291
323
  }) {
292
- const { messages, isLoading, error, status, sendMessage, stop, reset } = useSyncAgent();
324
+ const { messages, isLoading, error, status, lastData, sendMessage, stop, reset } = useSyncAgent({ context, onData });
293
325
  const [open, setOpen] = (0, import_react3.useState)(defaultOpen);
294
326
  const [input, setInput] = (0, import_react3.useState)("");
295
327
  const [ts, setTs] = (0, import_react3.useState)([]);
296
328
  const [reactions, setReactions] = (0, import_react3.useState)({});
297
329
  const [panelH, setPanelH] = (0, import_react3.useState)(600);
298
330
  const [lastUserMsg, setLastUserMsg] = (0, import_react3.useState)("");
331
+ const [pinnedQueries, setPinnedQueries] = (0, import_react3.useState)(() => persistKey ? LS.load(`pins_${persistKey}`) || [] : []);
332
+ const [historyIdx, setHistoryIdx] = (0, import_react3.useState)(-1);
333
+ const [chartDataMap, setChartDataMap] = (0, import_react3.useState)({});
299
334
  const endRef = (0, import_react3.useRef)(null);
300
335
  const inputRef = (0, import_react3.useRef)(null);
301
336
  const resizeRef = (0, import_react3.useRef)(null);
302
- const STORAGE_KEY = persistKey || "default";
303
- const loaded = (0, import_react3.useRef)(false);
337
+ const PKEY = persistKey || "default";
304
338
  (0, import_react3.useEffect)(() => {
305
- if (loaded.current || !persistKey) return;
306
- loaded.current = true;
307
- const saved = loadHistory(STORAGE_KEY);
308
- if (saved?.messages?.length) {
339
+ if (messages.length > ts.length) {
340
+ setTs((prev) => {
341
+ const n = [...prev];
342
+ while (n.length < messages.length) n.push(/* @__PURE__ */ new Date());
343
+ return n;
344
+ });
309
345
  }
310
- }, []);
346
+ }, [messages.length]);
311
347
  (0, import_react3.useEffect)(() => {
312
348
  if (!persistKey || messages.length === 0) return;
313
- saveHistory(STORAGE_KEY, messages, ts);
349
+ LS.save(`chat_${PKEY}`, { messages, ts: ts.map((t) => t?.toISOString() ?? null) });
314
350
  }, [messages, ts, persistKey]);
315
351
  (0, import_react3.useEffect)(() => {
316
- if (messages.length > ts.length) {
317
- setTs((prev) => {
318
- const next = [...prev];
319
- while (next.length < messages.length) next.push(/* @__PURE__ */ new Date());
320
- return next;
321
- });
352
+ if (!lastData) return;
353
+ const chart = detectChartData(lastData);
354
+ if (chart) {
355
+ const lastAiIdx = [...messages].reverse().findIndex((m) => m.role === "assistant");
356
+ if (lastAiIdx >= 0) {
357
+ const idx = messages.length - 1 - lastAiIdx;
358
+ setChartDataMap((prev) => ({ ...prev, [idx]: chart }));
359
+ }
322
360
  }
323
- }, [messages.length]);
361
+ }, [lastData]);
324
362
  (0, import_react3.useEffect)(() => {
325
363
  endRef.current?.scrollIntoView({ behavior: "smooth" });
326
364
  }, [messages, isLoading]);
@@ -331,24 +369,48 @@ function ChatInner({
331
369
  const t = (text || input).trim();
332
370
  if (!t || isLoading) return;
333
371
  setLastUserMsg(t);
372
+ setHistoryIdx(-1);
334
373
  setInput("");
335
374
  sendMessage(t);
336
375
  }, [input, isLoading, sendMessage]);
376
+ const userMessages = messages.filter((m) => m.role === "user").map((m) => m.content);
337
377
  const onKey = (e) => {
338
378
  if (e.key === "Enter" && !e.shiftKey) {
339
379
  e.preventDefault();
340
380
  send();
381
+ return;
382
+ }
383
+ if (e.key === "ArrowUp" && !input.trim()) {
384
+ e.preventDefault();
385
+ const next = Math.min(historyIdx + 1, userMessages.length - 1);
386
+ setHistoryIdx(next);
387
+ setInput(userMessages[userMessages.length - 1 - next] || "");
388
+ }
389
+ if (e.key === "ArrowDown" && historyIdx >= 0) {
390
+ e.preventDefault();
391
+ const next = historyIdx - 1;
392
+ setHistoryIdx(next);
393
+ setInput(next < 0 ? "" : userMessages[userMessages.length - 1 - next] || "");
341
394
  }
342
395
  };
343
396
  const handleReact = (0, import_react3.useCallback)((idx, r) => {
344
397
  setReactions((prev) => ({ ...prev, [idx]: prev[idx] === r ? void 0 : r }));
345
398
  onReaction?.(idx, r, messages[idx]?.content || "");
346
399
  }, [messages, onReaction]);
400
+ const togglePin = (0, import_react3.useCallback)((q) => {
401
+ setPinnedQueries((prev) => {
402
+ const next = prev.includes(q) ? prev.filter((p) => p !== q) : [...prev, q];
403
+ if (persistKey) LS.save(`pins_${PKEY}`, next);
404
+ return next;
405
+ });
406
+ }, [persistKey]);
347
407
  const newConversation = (0, import_react3.useCallback)(() => {
348
- if (persistKey) clearHistory(STORAGE_KEY);
408
+ if (persistKey) LS.del(`chat_${PKEY}`);
349
409
  setTs([]);
350
410
  setReactions({});
351
411
  setLastUserMsg("");
412
+ setHistoryIdx(-1);
413
+ setChartDataMap({});
352
414
  reset();
353
415
  }, [reset, persistKey]);
354
416
  const onResizeStart = (0, import_react3.useCallback)((e) => {
@@ -356,8 +418,7 @@ function ChatInner({
356
418
  resizeRef.current = { startY: e.clientY, startH: panelH };
357
419
  const onMove = (ev) => {
358
420
  if (!resizeRef.current) return;
359
- const delta = resizeRef.current.startY - ev.clientY;
360
- setPanelH(Math.min(Math.max(resizeRef.current.startH + delta, 320), window.innerHeight - 120));
421
+ setPanelH(Math.min(Math.max(resizeRef.current.startH + (resizeRef.current.startY - ev.clientY), 320), window.innerHeight - 120));
361
422
  };
362
423
  const onUp = () => {
363
424
  resizeRef.current = null;
@@ -368,30 +429,10 @@ function ChatInner({
368
429
  window.addEventListener("mouseup", onUp);
369
430
  }, [panelH]);
370
431
  const noMsgs = messages.length === 0;
371
- const panel = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `sa-panel ${className || ""}`, style: {
372
- height: mode === "inline" ? "100%" : panelH,
373
- maxHeight: mode === "inline" ? "none" : "calc(100vh - 110px)",
374
- background: "#f8fafc",
375
- borderRadius: mode === "inline" ? 14 : 20,
376
- boxShadow: mode === "inline" ? "0 2px 20px rgba(0,0,0,.08)" : "0 24px 64px rgba(0,0,0,.18),0 4px 24px rgba(0,0,0,.08)",
377
- display: "flex",
378
- flexDirection: "column",
379
- overflow: "hidden",
380
- fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif",
381
- border: "1px solid rgba(0,0,0,.07)",
382
- ...customStyle
383
- }, children: [
432
+ const allChips = [.../* @__PURE__ */ new Set([...pinnedQueries, ...noMsgs ? suggestions : []])];
433
+ const panel = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `sa-panel ${className || ""}`, style: { height: mode === "inline" ? "100%" : panelH, maxHeight: mode === "inline" ? "none" : "calc(100vh - 110px)", background: "#f8fafc", borderRadius: mode === "inline" ? 14 : 20, boxShadow: mode === "inline" ? "0 2px 20px rgba(0,0,0,.08)" : "0 24px 64px rgba(0,0,0,.18),0 4px 24px rgba(0,0,0,.08)", display: "flex", flexDirection: "column", overflow: "hidden", fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif", border: "1px solid rgba(0,0,0,.07)", ...customStyle }, children: [
384
434
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: CSS(accentColor) }),
385
- mode === "floating" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "sa-resize", onMouseDown: onResizeStart, style: {
386
- height: 6,
387
- flexShrink: 0,
388
- cursor: "ns-resize",
389
- background: "transparent",
390
- position: "relative",
391
- display: "flex",
392
- alignItems: "center",
393
- justifyContent: "center"
394
- }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: 32, height: 3, borderRadius: 2, background: "rgba(0,0,0,.12)" } }) }),
435
+ mode === "floating" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { onMouseDown: onResizeStart, style: { height: 8, flexShrink: 0, cursor: "ns-resize", background: "transparent", display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: 32, height: 3, borderRadius: 2, background: "rgba(0,0,0,.1)" } }) }),
395
436
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: "11px 16px", background: `linear-gradient(135deg,${accentColor},${adj(accentColor, -28)})`, display: "flex", alignItems: "center", justifyContent: "space-between", flexShrink: 0, boxShadow: "0 2px 12px rgba(0,0,0,.12)" }, children: [
396
437
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
397
438
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: 36, height: 36, borderRadius: "50%", background: "rgba(255,255,255,.18)", backdropFilter: "blur(8px)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 17, boxShadow: "0 2px 8px rgba(0,0,0,.15)" }, children: "\u2726" }),
@@ -404,7 +445,7 @@ function ChatInner({
404
445
  ] })
405
446
  ] }),
406
447
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 4 }, children: [
407
- messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { onClick: newConversation, title: "New conversation", style: { background: "rgba(255,255,255,.15)", border: "none", color: "white", cursor: "pointer", borderRadius: 7, padding: "4px 9px", fontSize: 11, fontWeight: 500, backdropFilter: "blur(4px)", display: "flex", alignItems: "center", gap: 4 }, children: [
448
+ messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { onClick: newConversation, style: { background: "rgba(255,255,255,.15)", border: "none", color: "white", cursor: "pointer", borderRadius: 7, padding: "4px 9px", fontSize: 11, fontWeight: 500, backdropFilter: "blur(4px)", display: "flex", alignItems: "center", gap: 4 }, children: [
408
449
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" }) }),
409
450
  "New"
410
451
  ] }),
@@ -427,8 +468,9 @@ function ChatInner({
427
468
  time: ts[i] ?? /* @__PURE__ */ new Date(),
428
469
  reactions,
429
470
  onReact: handleReact,
430
- onRetry: !isLoading && i === messages.length - 1 && msg.role === "assistant" && !!error ? () => send(lastUserMsg) : void 0,
431
- hasError: !!error && i === messages.length - 1 && msg.role === "assistant"
471
+ chartData: chartDataMap[i],
472
+ hasTable: msg.content.includes("|"),
473
+ onRetry: !isLoading && i === messages.length - 1 && msg.role === "assistant" && !!error ? () => send(lastUserMsg) : void 0
432
474
  },
433
475
  i
434
476
  )),
@@ -442,10 +484,19 @@ function ChatInner({
442
484
  ] }),
443
485
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: endRef })
444
486
  ] }),
445
- noMsgs && suggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Chips, { items: suggestions, onPick: (s) => {
446
- setInput(s);
447
- inputRef.current?.focus();
448
- }, accent: accentColor }),
487
+ allChips.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
488
+ Chips,
489
+ {
490
+ items: allChips,
491
+ onPick: (s) => {
492
+ setInput(s);
493
+ inputRef.current?.focus();
494
+ },
495
+ accent: accentColor,
496
+ onPin: togglePin,
497
+ pinned: pinnedQueries
498
+ }
499
+ ),
449
500
  isLoading && status && status.step !== "done" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: "7px 14px", borderTop: "1px solid #f0f4f8", background: "#fafbfc", display: "flex", alignItems: "center", gap: 8, flexShrink: 0 }, children: [
450
501
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", gap: 3 }, children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: 5, height: 5, borderRadius: "50%", background: accentColor, animation: "sa-bounce 1.2s infinite", animationDelay: `${i * 0.15}s` } }, i)) }),
451
502
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { fontSize: 11.5, color: "#64748b", fontWeight: 500 }, children: [
@@ -502,7 +553,7 @@ function ChatInner({
502
553
  "Powered by ",
503
554
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: accentColor, fontWeight: 600 }, children: "SyncAgent" }),
504
555
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { margin: "0 5px", opacity: 0.5 }, children: "\xB7" }),
505
- "Enter to send \xB7 Shift+Enter for new line"
556
+ "Enter to send \xB7 \u2191\u2193 history \xB7 Shift+Enter new line"
506
557
  ] })
507
558
  ] })
508
559
  ] });
package/dist/index.mjs CHANGED
@@ -35,6 +35,7 @@ function useSyncAgent(options = {}) {
35
35
  const [isLoading, setIsLoading] = useState(false);
36
36
  const [error, setError] = useState(null);
37
37
  const [status, setStatus] = useState(null);
38
+ const [lastData, setLastData] = useState(null);
38
39
  const abortRef = useRef(null);
39
40
  const sendMessage = useCallback(
40
41
  async (content) => {
@@ -45,13 +46,19 @@ function useSyncAgent(options = {}) {
45
46
  setIsLoading(true);
46
47
  setError(null);
47
48
  setStatus(null);
49
+ setLastData(null);
48
50
  const placeholder = { role: "assistant", content: "" };
49
51
  setMessages([...updated, placeholder]);
50
52
  abortRef.current = new AbortController();
51
53
  try {
52
54
  await client.chat(updated, {
53
55
  signal: abortRef.current.signal,
56
+ context: options.context,
54
57
  onStatus: (step, label) => setStatus({ step, label }),
58
+ onData: (data) => {
59
+ setLastData(data);
60
+ options.onData?.(data);
61
+ },
55
62
  onToken: (token) => {
56
63
  placeholder.content += token;
57
64
  setMessages((prev) => {
@@ -84,7 +91,7 @@ function useSyncAgent(options = {}) {
84
91
  abortRef.current = null;
85
92
  }
86
93
  },
87
- [client, messages, isLoading]
94
+ [client, messages, isLoading, options.context]
88
95
  );
89
96
  const stop = useCallback(() => {
90
97
  abortRef.current?.abort();
@@ -95,8 +102,9 @@ function useSyncAgent(options = {}) {
95
102
  setError(null);
96
103
  setIsLoading(false);
97
104
  setStatus(null);
105
+ setLastData(null);
98
106
  }, []);
99
- return { messages, isLoading, error, status, sendMessage, stop, reset };
107
+ return { messages, isLoading, error, status, lastData, sendMessage, stop, reset };
100
108
  }
101
109
 
102
110
  // src/chat.tsx
@@ -107,34 +115,62 @@ import {
107
115
  useCallback as useCallback2
108
116
  } from "react";
109
117
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
110
- function loadHistory(key) {
111
- try {
112
- const raw = localStorage.getItem(`sa_chat_${key}`);
113
- if (!raw) return null;
114
- const parsed = JSON.parse(raw);
115
- if (parsed.timestamps) {
116
- parsed.timestamps = parsed.timestamps.map((t) => t ? new Date(t) : null);
118
+ var LS = {
119
+ load: (key) => {
120
+ try {
121
+ const r = localStorage.getItem(`sa_${key}`);
122
+ return r ? JSON.parse(r) : null;
123
+ } catch {
124
+ return null;
125
+ }
126
+ },
127
+ save: (key, v) => {
128
+ try {
129
+ localStorage.setItem(`sa_${key}`, JSON.stringify(v));
130
+ } catch {
131
+ }
132
+ },
133
+ del: (key) => {
134
+ try {
135
+ localStorage.removeItem(`sa_${key}`);
136
+ } catch {
117
137
  }
118
- return parsed;
119
- } catch {
120
- return null;
121
138
  }
139
+ };
140
+ function mdTableToCSV(text) {
141
+ const match = text.match(/\|(.+)\|\r?\n\|[-| :]+\|\r?\n((?:\|.+\|\r?\n?)+)/);
142
+ if (!match) return null;
143
+ const headers = match[1].split("|").map((c) => c.trim()).filter(Boolean);
144
+ const rows = match[2].trim().split("\n").map(
145
+ (row) => row.split("|").map((c) => c.trim()).filter(Boolean)
146
+ );
147
+ const escape = (v) => `"${v.replace(/"/g, '""')}"`;
148
+ return [headers.map(escape).join(","), ...rows.map((r) => r.map(escape).join(","))].join("\n");
122
149
  }
123
- function saveHistory(key, messages, timestamps) {
124
- try {
125
- localStorage.setItem(`sa_chat_${key}`, JSON.stringify({
126
- messages,
127
- timestamps: timestamps.map((t) => t?.toISOString() ?? null),
128
- savedAt: (/* @__PURE__ */ new Date()).toISOString()
129
- }));
130
- } catch {
131
- }
150
+ function downloadCSV(csv, filename = "syncagent-export.csv") {
151
+ const blob = new Blob([csv], { type: "text/csv" });
152
+ const url = URL.createObjectURL(blob);
153
+ const a = document.createElement("a");
154
+ a.href = url;
155
+ a.download = filename;
156
+ a.click();
157
+ URL.revokeObjectURL(url);
132
158
  }
133
- function clearHistory(key) {
134
- try {
135
- localStorage.removeItem(`sa_chat_${key}`);
136
- } catch {
137
- }
159
+ function BarChart({ data, accent }) {
160
+ const max = Math.max(...data.map((d) => d.value), 1);
161
+ return /* @__PURE__ */ jsx2("div", { style: { margin: "10px 0", padding: "12px", background: "#f8fafc", borderRadius: 10, border: "1px solid #e2e8f0" }, children: /* @__PURE__ */ jsx2("div", { style: { display: "flex", alignItems: "flex-end", gap: 6, height: 80 }, children: data.slice(0, 12).map((d, i) => /* @__PURE__ */ jsxs("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 3, minWidth: 0 }, children: [
162
+ /* @__PURE__ */ jsx2("div", { style: { width: "100%", background: `${accent}cc`, borderRadius: "3px 3px 0 0", height: `${d.value / max * 68}px`, minHeight: 2, transition: "height .3s" }, title: `${d.label}: ${d.value}` }),
163
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 9, color: "#94a3b8", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", width: "100%", textAlign: "center" }, children: d.label })
164
+ ] }, i)) }) });
165
+ }
166
+ function detectChartData(data) {
167
+ if (!data?.data?.length) return null;
168
+ const rows = data.data;
169
+ const keys = Object.keys(rows[0]);
170
+ const labelKey = keys.find((k) => typeof rows[0][k] === "string" || k === "_id");
171
+ const valueKey = keys.find((k) => typeof rows[0][k] === "number" && k !== labelKey);
172
+ if (!labelKey || !valueKey) return null;
173
+ return rows.map((r) => ({ label: String(r[labelKey] ?? ""), value: Number(r[valueKey] ?? 0) }));
138
174
  }
139
175
  var CSS = (accent) => `
140
176
  @keyframes sa-bounce { 0%,80%,100%{transform:translateY(0);opacity:.35} 40%{transform:translateY(-5px);opacity:1} }
@@ -151,13 +187,10 @@ var CSS = (accent) => `
151
187
  .sa-send:disabled { opacity: .4; cursor: not-allowed }
152
188
  .sa-act:hover { opacity: 1 !important; background: rgba(0,0,0,.06) !important }
153
189
  .sa-react:hover { transform: scale(1.2) }
154
- .sa-react.active { transform: scale(1.15) }
155
190
  .sa-scroll::-webkit-scrollbar { width: 4px }
156
191
  .sa-scroll::-webkit-scrollbar-track { background: transparent }
157
192
  .sa-scroll::-webkit-scrollbar-thumb { background: rgba(0,0,0,.12); border-radius: 4px }
158
193
  .sa-cursor { display:inline-block; width:2px; height:1em; background:currentColor; margin-left:1px; vertical-align:text-bottom; animation: sa-cursor .7s infinite }
159
- .sa-resize { cursor: ns-resize; user-select: none }
160
- .sa-resize:hover::after { opacity: 1 }
161
194
  .sa-md table { width:100%; border-collapse:collapse; font-size:12.5px; margin:10px 0 }
162
195
  .sa-md th { padding:7px 10px; text-align:left; font-weight:600; background:rgba(0,0,0,.04); border-bottom:2px solid rgba(0,0,0,.1); white-space:nowrap }
163
196
  .sa-md td { padding:6px 10px; border-bottom:1px solid rgba(0,0,0,.06); vertical-align:top }
@@ -211,35 +244,29 @@ function CopyBtn({ text }) {
211
244
  "Copy"
212
245
  ] }) });
213
246
  }
214
- function ReactionBtns({ idx, reactions, onReact, accent }) {
215
- const cur = reactions[idx];
216
- return /* @__PURE__ */ jsx2("div", { style: { display: "flex", gap: 2 }, children: ["up", "down"].map((r) => /* @__PURE__ */ jsx2("button", { className: `sa-react${cur === r ? " active" : ""}`, onClick: () => onReact(idx, r), title: r === "up" ? "Helpful" : "Not helpful", style: {
217
- background: cur === r ? r === "up" ? `${accent}18` : "#fef2f2" : "none",
218
- border: cur === r ? `1px solid ${r === "up" ? accent + "44" : "#fecaca"}` : "none",
219
- cursor: "pointer",
220
- padding: "3px 6px",
221
- borderRadius: 5,
222
- fontSize: 13,
223
- transition: "all .15s",
224
- opacity: cur && cur !== r ? 0.3 : 0.7
225
- }, children: r === "up" ? "\u{1F44D}" : "\u{1F44E}" }, r)) });
226
- }
227
- function Bubble({ role, content, streaming, accent, time, idx, reactions, onReact, onRetry, hasError }) {
247
+ function Bubble({ role, content, streaming, accent, time, idx, reactions, onReact, onRetry, chartData, hasTable }) {
228
248
  const isUser = role === "user";
229
249
  const t = time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
250
+ const cur = reactions[idx];
251
+ const csv = hasTable ? mdTableToCSV(content) : null;
230
252
  return /* @__PURE__ */ jsxs("div", { className: "sa-msg", style: { display: "flex", flexDirection: "column", alignItems: isUser ? "flex-end" : "flex-start", gap: 4, maxWidth: "90%", alignSelf: isUser ? "flex-end" : "flex-start" }, children: [
231
253
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, flexDirection: isUser ? "row-reverse" : "row" }, children: [
232
254
  /* @__PURE__ */ jsx2("div", { style: { width: 26, height: 26, borderRadius: "50%", flexShrink: 0, background: isUser ? `linear-gradient(135deg,${accent},${adj(accent, -25)})` : "linear-gradient(135deg,#6366f1,#8b5cf6)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 11, color: "white", fontWeight: 700, boxShadow: isUser ? `0 2px 8px ${accent}44` : "0 2px 8px #6366f144" }, children: isUser ? "U" : "\u2726" }),
233
255
  /* @__PURE__ */ jsx2("span", { style: { fontSize: 11.5, fontWeight: 600, color: isUser ? "#475569" : "#6366f1" }, children: isUser ? "You" : "SyncAgent" }),
234
256
  /* @__PURE__ */ jsx2("span", { style: { fontSize: 10, color: "#cbd5e1" }, children: t })
235
257
  ] }),
236
- /* @__PURE__ */ jsxs("div", { style: { padding: isUser ? "10px 14px" : "12px 16px", borderRadius: isUser ? "18px 4px 18px 18px" : "4px 18px 18px 18px", background: isUser ? `linear-gradient(135deg,${accent},${adj(accent, -20)})` : "#ffffff", color: isUser ? "white" : "#1e293b", fontSize: 13.5, lineHeight: 1.65, wordBreak: "break-word", border: isUser ? "none" : "1px solid #e8edf3", boxShadow: isUser ? `0 4px 16px ${accent}33` : "0 2px 12px rgba(0,0,0,0.07)", maxWidth: "100%", animation: hasError ? "sa-shake .3s ease" : "none" }, children: [
258
+ /* @__PURE__ */ jsxs("div", { style: { padding: isUser ? "10px 14px" : "12px 16px", borderRadius: isUser ? "18px 4px 18px 18px" : "4px 18px 18px 18px", background: isUser ? `linear-gradient(135deg,${accent},${adj(accent, -20)})` : "#ffffff", color: isUser ? "white" : "#1e293b", fontSize: 13.5, lineHeight: 1.65, wordBreak: "break-word", border: isUser ? "none" : "1px solid #e8edf3", boxShadow: isUser ? `0 4px 16px ${accent}33` : "0 2px 12px rgba(0,0,0,0.07)", maxWidth: "100%" }, children: [
237
259
  streaming && !content ? /* @__PURE__ */ jsx2(Dots, { color: accent }) : isUser ? /* @__PURE__ */ jsx2("span", { style: { whiteSpace: "pre-wrap" }, children: content }) : /* @__PURE__ */ jsx2("div", { className: "sa-md", dangerouslySetInnerHTML: { __html: md(content) } }),
238
260
  streaming && content && /* @__PURE__ */ jsx2("span", { className: "sa-cursor", style: { color: accent } })
239
261
  ] }),
240
- !isUser && content && !streaming && /* @__PURE__ */ jsxs("div", { style: { paddingLeft: 32, display: "flex", gap: 2, alignItems: "center" }, children: [
262
+ !isUser && !streaming && chartData && chartData.length > 1 && /* @__PURE__ */ jsx2("div", { style: { paddingLeft: 32, width: "calc(100% - 32px)" }, children: /* @__PURE__ */ jsx2(BarChart, { data: chartData, accent }) }),
263
+ !isUser && content && !streaming && /* @__PURE__ */ jsxs("div", { style: { paddingLeft: 32, display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }, children: [
241
264
  /* @__PURE__ */ jsx2(CopyBtn, { text: content }),
242
- /* @__PURE__ */ jsx2(ReactionBtns, { idx, reactions, onReact, accent }),
265
+ csv && /* @__PURE__ */ jsxs("button", { className: "sa-act", onClick: () => downloadCSV(csv), style: { background: "none", border: "none", cursor: "pointer", padding: "3px 7px", borderRadius: 5, fontSize: 11, color: "#94a3b8", opacity: 0.65, transition: "all .15s", display: "flex", alignItems: "center", gap: 3 }, children: [
266
+ /* @__PURE__ */ jsx2("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" }) }),
267
+ "CSV"
268
+ ] }),
269
+ ["up", "down"].map((r) => /* @__PURE__ */ jsx2("button", { className: "sa-react", onClick: () => onReact(idx, r), style: { background: cur === r ? r === "up" ? `${accent}18` : "#fef2f2" : "none", border: cur === r ? `1px solid ${r === "up" ? accent + "44" : "#fecaca"}` : "none", cursor: "pointer", padding: "3px 6px", borderRadius: 5, fontSize: 13, transition: "all .15s", opacity: cur && cur !== r ? 0.3 : 0.7 }, children: r === "up" ? "\u{1F44D}" : "\u{1F44E}" }, r)),
243
270
  onRetry && /* @__PURE__ */ jsxs("button", { className: "sa-act", onClick: onRetry, style: { background: "none", border: "none", cursor: "pointer", padding: "3px 7px", borderRadius: 5, fontSize: 11, color: "#94a3b8", opacity: 0.65, transition: "all .15s", display: "flex", alignItems: "center", gap: 3 }, children: [
244
271
  /* @__PURE__ */ jsx2("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" }) }),
245
272
  "Retry"
@@ -247,8 +274,11 @@ function Bubble({ role, content, streaming, accent, time, idx, reactions, onReac
247
274
  ] })
248
275
  ] });
249
276
  }
250
- function Chips({ items, onPick, accent }) {
251
- return /* @__PURE__ */ jsx2("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, padding: "0 14px 10px" }, children: items.map((s) => /* @__PURE__ */ jsx2("button", { className: "sa-chip", onClick: () => onPick(s), style: { padding: "5px 13px", borderRadius: 20, fontSize: 12, cursor: "pointer", border: `1px solid ${accent}33`, background: `${accent}0a`, color: accent, fontWeight: 500, transition: "all .15s", whiteSpace: "nowrap" }, children: s }, s)) });
277
+ function Chips({ items, onPick, accent, onPin, pinned }) {
278
+ return /* @__PURE__ */ jsx2("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, padding: "0 14px 10px" }, children: items.map((s) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 0 }, children: [
279
+ /* @__PURE__ */ jsx2("button", { className: "sa-chip", onClick: () => onPick(s), style: { padding: "5px 10px 5px 13px", borderRadius: onPin ? "20px 0 0 20px" : "20px", fontSize: 12, cursor: "pointer", border: `1px solid ${accent}33`, borderRight: onPin ? "none" : "", background: `${accent}0a`, color: accent, fontWeight: 500, transition: "all .15s", whiteSpace: "nowrap" }, children: s }),
280
+ onPin && /* @__PURE__ */ jsx2("button", { onClick: () => onPin(s), title: pinned?.includes(s) ? "Unpin" : "Pin", style: { padding: "5px 7px", borderRadius: "0 20px 20px 0", fontSize: 10, cursor: "pointer", border: `1px solid ${accent}33`, borderLeft: "none", background: pinned?.includes(s) ? `${accent}18` : `${accent}0a`, color: accent, transition: "all .15s" }, children: pinned?.includes(s) ? "\u{1F4CC}" : "\u{1F4CD}" })
281
+ ] }, s)) });
252
282
  }
253
283
  function ChatInner({
254
284
  mode = "floating",
@@ -263,40 +293,48 @@ function ChatInner({
263
293
  style: customStyle,
264
294
  suggestions = ["Show all records", "Count total entries", "Show recent activity"],
265
295
  persistKey,
266
- onReaction
296
+ context,
297
+ onReaction,
298
+ onData
267
299
  }) {
268
- const { messages, isLoading, error, status, sendMessage, stop, reset } = useSyncAgent();
300
+ const { messages, isLoading, error, status, lastData, sendMessage, stop, reset } = useSyncAgent({ context, onData });
269
301
  const [open, setOpen] = useState2(defaultOpen);
270
302
  const [input, setInput] = useState2("");
271
303
  const [ts, setTs] = useState2([]);
272
304
  const [reactions, setReactions] = useState2({});
273
305
  const [panelH, setPanelH] = useState2(600);
274
306
  const [lastUserMsg, setLastUserMsg] = useState2("");
307
+ const [pinnedQueries, setPinnedQueries] = useState2(() => persistKey ? LS.load(`pins_${persistKey}`) || [] : []);
308
+ const [historyIdx, setHistoryIdx] = useState2(-1);
309
+ const [chartDataMap, setChartDataMap] = useState2({});
275
310
  const endRef = useRef2(null);
276
311
  const inputRef = useRef2(null);
277
312
  const resizeRef = useRef2(null);
278
- const STORAGE_KEY = persistKey || "default";
279
- const loaded = useRef2(false);
313
+ const PKEY = persistKey || "default";
280
314
  useEffect(() => {
281
- if (loaded.current || !persistKey) return;
282
- loaded.current = true;
283
- const saved = loadHistory(STORAGE_KEY);
284
- if (saved?.messages?.length) {
315
+ if (messages.length > ts.length) {
316
+ setTs((prev) => {
317
+ const n = [...prev];
318
+ while (n.length < messages.length) n.push(/* @__PURE__ */ new Date());
319
+ return n;
320
+ });
285
321
  }
286
- }, []);
322
+ }, [messages.length]);
287
323
  useEffect(() => {
288
324
  if (!persistKey || messages.length === 0) return;
289
- saveHistory(STORAGE_KEY, messages, ts);
325
+ LS.save(`chat_${PKEY}`, { messages, ts: ts.map((t) => t?.toISOString() ?? null) });
290
326
  }, [messages, ts, persistKey]);
291
327
  useEffect(() => {
292
- if (messages.length > ts.length) {
293
- setTs((prev) => {
294
- const next = [...prev];
295
- while (next.length < messages.length) next.push(/* @__PURE__ */ new Date());
296
- return next;
297
- });
328
+ if (!lastData) return;
329
+ const chart = detectChartData(lastData);
330
+ if (chart) {
331
+ const lastAiIdx = [...messages].reverse().findIndex((m) => m.role === "assistant");
332
+ if (lastAiIdx >= 0) {
333
+ const idx = messages.length - 1 - lastAiIdx;
334
+ setChartDataMap((prev) => ({ ...prev, [idx]: chart }));
335
+ }
298
336
  }
299
- }, [messages.length]);
337
+ }, [lastData]);
300
338
  useEffect(() => {
301
339
  endRef.current?.scrollIntoView({ behavior: "smooth" });
302
340
  }, [messages, isLoading]);
@@ -307,24 +345,48 @@ function ChatInner({
307
345
  const t = (text || input).trim();
308
346
  if (!t || isLoading) return;
309
347
  setLastUserMsg(t);
348
+ setHistoryIdx(-1);
310
349
  setInput("");
311
350
  sendMessage(t);
312
351
  }, [input, isLoading, sendMessage]);
352
+ const userMessages = messages.filter((m) => m.role === "user").map((m) => m.content);
313
353
  const onKey = (e) => {
314
354
  if (e.key === "Enter" && !e.shiftKey) {
315
355
  e.preventDefault();
316
356
  send();
357
+ return;
358
+ }
359
+ if (e.key === "ArrowUp" && !input.trim()) {
360
+ e.preventDefault();
361
+ const next = Math.min(historyIdx + 1, userMessages.length - 1);
362
+ setHistoryIdx(next);
363
+ setInput(userMessages[userMessages.length - 1 - next] || "");
364
+ }
365
+ if (e.key === "ArrowDown" && historyIdx >= 0) {
366
+ e.preventDefault();
367
+ const next = historyIdx - 1;
368
+ setHistoryIdx(next);
369
+ setInput(next < 0 ? "" : userMessages[userMessages.length - 1 - next] || "");
317
370
  }
318
371
  };
319
372
  const handleReact = useCallback2((idx, r) => {
320
373
  setReactions((prev) => ({ ...prev, [idx]: prev[idx] === r ? void 0 : r }));
321
374
  onReaction?.(idx, r, messages[idx]?.content || "");
322
375
  }, [messages, onReaction]);
376
+ const togglePin = useCallback2((q) => {
377
+ setPinnedQueries((prev) => {
378
+ const next = prev.includes(q) ? prev.filter((p) => p !== q) : [...prev, q];
379
+ if (persistKey) LS.save(`pins_${PKEY}`, next);
380
+ return next;
381
+ });
382
+ }, [persistKey]);
323
383
  const newConversation = useCallback2(() => {
324
- if (persistKey) clearHistory(STORAGE_KEY);
384
+ if (persistKey) LS.del(`chat_${PKEY}`);
325
385
  setTs([]);
326
386
  setReactions({});
327
387
  setLastUserMsg("");
388
+ setHistoryIdx(-1);
389
+ setChartDataMap({});
328
390
  reset();
329
391
  }, [reset, persistKey]);
330
392
  const onResizeStart = useCallback2((e) => {
@@ -332,8 +394,7 @@ function ChatInner({
332
394
  resizeRef.current = { startY: e.clientY, startH: panelH };
333
395
  const onMove = (ev) => {
334
396
  if (!resizeRef.current) return;
335
- const delta = resizeRef.current.startY - ev.clientY;
336
- setPanelH(Math.min(Math.max(resizeRef.current.startH + delta, 320), window.innerHeight - 120));
397
+ setPanelH(Math.min(Math.max(resizeRef.current.startH + (resizeRef.current.startY - ev.clientY), 320), window.innerHeight - 120));
337
398
  };
338
399
  const onUp = () => {
339
400
  resizeRef.current = null;
@@ -344,30 +405,10 @@ function ChatInner({
344
405
  window.addEventListener("mouseup", onUp);
345
406
  }, [panelH]);
346
407
  const noMsgs = messages.length === 0;
347
- const panel = /* @__PURE__ */ jsxs("div", { className: `sa-panel ${className || ""}`, style: {
348
- height: mode === "inline" ? "100%" : panelH,
349
- maxHeight: mode === "inline" ? "none" : "calc(100vh - 110px)",
350
- background: "#f8fafc",
351
- borderRadius: mode === "inline" ? 14 : 20,
352
- boxShadow: mode === "inline" ? "0 2px 20px rgba(0,0,0,.08)" : "0 24px 64px rgba(0,0,0,.18),0 4px 24px rgba(0,0,0,.08)",
353
- display: "flex",
354
- flexDirection: "column",
355
- overflow: "hidden",
356
- fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif",
357
- border: "1px solid rgba(0,0,0,.07)",
358
- ...customStyle
359
- }, children: [
408
+ const allChips = [.../* @__PURE__ */ new Set([...pinnedQueries, ...noMsgs ? suggestions : []])];
409
+ const panel = /* @__PURE__ */ jsxs("div", { className: `sa-panel ${className || ""}`, style: { height: mode === "inline" ? "100%" : panelH, maxHeight: mode === "inline" ? "none" : "calc(100vh - 110px)", background: "#f8fafc", borderRadius: mode === "inline" ? 14 : 20, boxShadow: mode === "inline" ? "0 2px 20px rgba(0,0,0,.08)" : "0 24px 64px rgba(0,0,0,.18),0 4px 24px rgba(0,0,0,.08)", display: "flex", flexDirection: "column", overflow: "hidden", fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif", border: "1px solid rgba(0,0,0,.07)", ...customStyle }, children: [
360
410
  /* @__PURE__ */ jsx2("style", { children: CSS(accentColor) }),
361
- mode === "floating" && /* @__PURE__ */ jsx2("div", { className: "sa-resize", onMouseDown: onResizeStart, style: {
362
- height: 6,
363
- flexShrink: 0,
364
- cursor: "ns-resize",
365
- background: "transparent",
366
- position: "relative",
367
- display: "flex",
368
- alignItems: "center",
369
- justifyContent: "center"
370
- }, children: /* @__PURE__ */ jsx2("div", { style: { width: 32, height: 3, borderRadius: 2, background: "rgba(0,0,0,.12)" } }) }),
411
+ mode === "floating" && /* @__PURE__ */ jsx2("div", { onMouseDown: onResizeStart, style: { height: 8, flexShrink: 0, cursor: "ns-resize", background: "transparent", display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx2("div", { style: { width: 32, height: 3, borderRadius: 2, background: "rgba(0,0,0,.1)" } }) }),
371
412
  /* @__PURE__ */ jsxs("div", { style: { padding: "11px 16px", background: `linear-gradient(135deg,${accentColor},${adj(accentColor, -28)})`, display: "flex", alignItems: "center", justifyContent: "space-between", flexShrink: 0, boxShadow: "0 2px 12px rgba(0,0,0,.12)" }, children: [
372
413
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
373
414
  /* @__PURE__ */ jsx2("div", { style: { width: 36, height: 36, borderRadius: "50%", background: "rgba(255,255,255,.18)", backdropFilter: "blur(8px)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 17, boxShadow: "0 2px 8px rgba(0,0,0,.15)" }, children: "\u2726" }),
@@ -380,7 +421,7 @@ function ChatInner({
380
421
  ] })
381
422
  ] }),
382
423
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 4 }, children: [
383
- messages.length > 0 && /* @__PURE__ */ jsxs("button", { onClick: newConversation, title: "New conversation", style: { background: "rgba(255,255,255,.15)", border: "none", color: "white", cursor: "pointer", borderRadius: 7, padding: "4px 9px", fontSize: 11, fontWeight: 500, backdropFilter: "blur(4px)", display: "flex", alignItems: "center", gap: 4 }, children: [
424
+ messages.length > 0 && /* @__PURE__ */ jsxs("button", { onClick: newConversation, style: { background: "rgba(255,255,255,.15)", border: "none", color: "white", cursor: "pointer", borderRadius: 7, padding: "4px 9px", fontSize: 11, fontWeight: 500, backdropFilter: "blur(4px)", display: "flex", alignItems: "center", gap: 4 }, children: [
384
425
  /* @__PURE__ */ jsx2("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" }) }),
385
426
  "New"
386
427
  ] }),
@@ -403,8 +444,9 @@ function ChatInner({
403
444
  time: ts[i] ?? /* @__PURE__ */ new Date(),
404
445
  reactions,
405
446
  onReact: handleReact,
406
- onRetry: !isLoading && i === messages.length - 1 && msg.role === "assistant" && !!error ? () => send(lastUserMsg) : void 0,
407
- hasError: !!error && i === messages.length - 1 && msg.role === "assistant"
447
+ chartData: chartDataMap[i],
448
+ hasTable: msg.content.includes("|"),
449
+ onRetry: !isLoading && i === messages.length - 1 && msg.role === "assistant" && !!error ? () => send(lastUserMsg) : void 0
408
450
  },
409
451
  i
410
452
  )),
@@ -418,10 +460,19 @@ function ChatInner({
418
460
  ] }),
419
461
  /* @__PURE__ */ jsx2("div", { ref: endRef })
420
462
  ] }),
421
- noMsgs && suggestions.length > 0 && /* @__PURE__ */ jsx2(Chips, { items: suggestions, onPick: (s) => {
422
- setInput(s);
423
- inputRef.current?.focus();
424
- }, accent: accentColor }),
463
+ allChips.length > 0 && /* @__PURE__ */ jsx2(
464
+ Chips,
465
+ {
466
+ items: allChips,
467
+ onPick: (s) => {
468
+ setInput(s);
469
+ inputRef.current?.focus();
470
+ },
471
+ accent: accentColor,
472
+ onPin: togglePin,
473
+ pinned: pinnedQueries
474
+ }
475
+ ),
425
476
  isLoading && status && status.step !== "done" && /* @__PURE__ */ jsxs("div", { style: { padding: "7px 14px", borderTop: "1px solid #f0f4f8", background: "#fafbfc", display: "flex", alignItems: "center", gap: 8, flexShrink: 0 }, children: [
426
477
  /* @__PURE__ */ jsx2("div", { style: { display: "flex", gap: 3 }, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx2("div", { style: { width: 5, height: 5, borderRadius: "50%", background: accentColor, animation: "sa-bounce 1.2s infinite", animationDelay: `${i * 0.15}s` } }, i)) }),
427
478
  /* @__PURE__ */ jsxs("span", { style: { fontSize: 11.5, color: "#64748b", fontWeight: 500 }, children: [
@@ -478,7 +529,7 @@ function ChatInner({
478
529
  "Powered by ",
479
530
  /* @__PURE__ */ jsx2("span", { style: { color: accentColor, fontWeight: 600 }, children: "SyncAgent" }),
480
531
  /* @__PURE__ */ jsx2("span", { style: { margin: "0 5px", opacity: 0.5 }, children: "\xB7" }),
481
- "Enter to send \xB7 Shift+Enter for new line"
532
+ "Enter to send \xB7 \u2191\u2193 history \xB7 Shift+Enter new line"
482
533
  ] })
483
534
  ] })
484
535
  ] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncagent/react",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "SyncAgent React SDK — AI database chat widget & hooks",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",