@wallavi/widget 1.0.0 → 1.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/dist/index.d.mts CHANGED
@@ -21,11 +21,23 @@ type Message = {
21
21
  role: "user" | "assistant";
22
22
  parts: MessagePart[];
23
23
  };
24
+ interface PageContext {
25
+ /** Current URL or path (e.g. "/dashboard/agent/abc/agent-studio") */
26
+ url?: string;
27
+ /** Human-readable page name (e.g. "Agent Studio") */
28
+ title?: string;
29
+ /** Route parameters relevant to the page (e.g. { agentId: "abc123" }) */
30
+ params?: Record<string, string>;
31
+ /** Any custom key-value pairs the developer wants the agent to know about */
32
+ vars?: Record<string, unknown>;
33
+ }
24
34
  interface UserContext {
25
35
  headers?: Record<string, string>;
26
36
  userId?: string;
27
37
  userName?: string;
28
38
  userEmail?: string;
39
+ /** Contextual info about where the user is and what they're doing */
40
+ pageContext?: PageContext;
29
41
  metadata?: Record<string, unknown>;
30
42
  }
31
43
  interface ChatWidgetConfig {
@@ -43,8 +55,21 @@ interface ChatWidgetConfig {
43
55
  footer?: string | null;
44
56
  theme?: "light" | "dark";
45
57
  showThinking?: boolean;
58
+ regenerateMessage?: boolean;
59
+ /**
60
+ * Persist messages and threadId in sessionStorage so the conversation
61
+ * survives page reloads and navigations. Uses agentId as the storage key.
62
+ */
63
+ persist?: boolean;
64
+ onNavigate?: (path: string) => void;
46
65
  source?: string;
47
66
  userContext?: UserContext;
67
+ /** Per-session tool overrides used by Playground (not persisted) */
68
+ playgroundOverrides?: {
69
+ ragIds?: string[];
70
+ mcpIds?: string[];
71
+ actionIds?: string[];
72
+ };
48
73
  }
49
74
  interface ChatWidgetProps extends ChatWidgetConfig {
50
75
  className?: string;
@@ -54,13 +79,20 @@ interface ChatWidgetProps extends ChatWidgetConfig {
54
79
  declare function getContrastColor(hex: string): string;
55
80
  declare function formatToolName(name: string): string;
56
81
 
57
- declare function ChatWidget({ agentId, workspaceId, agentName, displayName, profilePicture, userMessageColor, initialMessages, suggestedMessages, messagePlaceholder, watermark, watermarkLogoUrl, footer, theme, showThinking, source, className, onClose, onReset, }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
82
+ declare function ChatWidget({ agentId, workspaceId, agentName, displayName, profilePicture, userMessageColor, initialMessages, suggestedMessages, messagePlaceholder, watermark, watermarkLogoUrl, footer, theme, showThinking, regenerateMessage, persist, onNavigate, source, userContext, playgroundOverrides, className, onClose, onReset, }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
58
83
 
59
84
  interface UseChatOptions {
60
85
  agentId: string;
61
86
  workspaceId?: string;
62
87
  source?: string;
63
88
  userContext?: UserContext;
89
+ persist?: boolean;
90
+ onNavigate?: (path: string) => void;
91
+ playgroundOverrides?: {
92
+ ragIds?: string[];
93
+ mcpIds?: string[];
94
+ actionIds?: string[];
95
+ };
64
96
  }
65
97
  interface UseChatReturn {
66
98
  messages: Message[];
@@ -69,8 +101,9 @@ interface UseChatReturn {
69
101
  streaming: boolean;
70
102
  threadId: string;
71
103
  send: (text?: string) => Promise<void>;
104
+ regenerate: () => Promise<void>;
72
105
  reset: () => void;
73
106
  }
74
- declare function useChat({ agentId, workspaceId, source, userContext, }: UseChatOptions): UseChatReturn;
107
+ declare function useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides, }: UseChatOptions): UseChatReturn;
75
108
 
76
109
  export { ChatWidget, type ChatWidgetConfig, type ChatWidgetProps, type Message, type MessagePart, type ToolPart, type UserContext, formatToolName, getContrastColor, useChat };
package/dist/index.d.ts CHANGED
@@ -21,11 +21,23 @@ type Message = {
21
21
  role: "user" | "assistant";
22
22
  parts: MessagePart[];
23
23
  };
24
+ interface PageContext {
25
+ /** Current URL or path (e.g. "/dashboard/agent/abc/agent-studio") */
26
+ url?: string;
27
+ /** Human-readable page name (e.g. "Agent Studio") */
28
+ title?: string;
29
+ /** Route parameters relevant to the page (e.g. { agentId: "abc123" }) */
30
+ params?: Record<string, string>;
31
+ /** Any custom key-value pairs the developer wants the agent to know about */
32
+ vars?: Record<string, unknown>;
33
+ }
24
34
  interface UserContext {
25
35
  headers?: Record<string, string>;
26
36
  userId?: string;
27
37
  userName?: string;
28
38
  userEmail?: string;
39
+ /** Contextual info about where the user is and what they're doing */
40
+ pageContext?: PageContext;
29
41
  metadata?: Record<string, unknown>;
30
42
  }
31
43
  interface ChatWidgetConfig {
@@ -43,8 +55,21 @@ interface ChatWidgetConfig {
43
55
  footer?: string | null;
44
56
  theme?: "light" | "dark";
45
57
  showThinking?: boolean;
58
+ regenerateMessage?: boolean;
59
+ /**
60
+ * Persist messages and threadId in sessionStorage so the conversation
61
+ * survives page reloads and navigations. Uses agentId as the storage key.
62
+ */
63
+ persist?: boolean;
64
+ onNavigate?: (path: string) => void;
46
65
  source?: string;
47
66
  userContext?: UserContext;
67
+ /** Per-session tool overrides used by Playground (not persisted) */
68
+ playgroundOverrides?: {
69
+ ragIds?: string[];
70
+ mcpIds?: string[];
71
+ actionIds?: string[];
72
+ };
48
73
  }
49
74
  interface ChatWidgetProps extends ChatWidgetConfig {
50
75
  className?: string;
@@ -54,13 +79,20 @@ interface ChatWidgetProps extends ChatWidgetConfig {
54
79
  declare function getContrastColor(hex: string): string;
55
80
  declare function formatToolName(name: string): string;
56
81
 
57
- declare function ChatWidget({ agentId, workspaceId, agentName, displayName, profilePicture, userMessageColor, initialMessages, suggestedMessages, messagePlaceholder, watermark, watermarkLogoUrl, footer, theme, showThinking, source, className, onClose, onReset, }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
82
+ declare function ChatWidget({ agentId, workspaceId, agentName, displayName, profilePicture, userMessageColor, initialMessages, suggestedMessages, messagePlaceholder, watermark, watermarkLogoUrl, footer, theme, showThinking, regenerateMessage, persist, onNavigate, source, userContext, playgroundOverrides, className, onClose, onReset, }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
58
83
 
59
84
  interface UseChatOptions {
60
85
  agentId: string;
61
86
  workspaceId?: string;
62
87
  source?: string;
63
88
  userContext?: UserContext;
89
+ persist?: boolean;
90
+ onNavigate?: (path: string) => void;
91
+ playgroundOverrides?: {
92
+ ragIds?: string[];
93
+ mcpIds?: string[];
94
+ actionIds?: string[];
95
+ };
64
96
  }
65
97
  interface UseChatReturn {
66
98
  messages: Message[];
@@ -69,8 +101,9 @@ interface UseChatReturn {
69
101
  streaming: boolean;
70
102
  threadId: string;
71
103
  send: (text?: string) => Promise<void>;
104
+ regenerate: () => Promise<void>;
72
105
  reset: () => void;
73
106
  }
74
- declare function useChat({ agentId, workspaceId, source, userContext, }: UseChatOptions): UseChatReturn;
107
+ declare function useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides, }: UseChatOptions): UseChatReturn;
75
108
 
76
109
  export { ChatWidget, type ChatWidgetConfig, type ChatWidgetProps, type Message, type MessagePart, type ToolPart, type UserContext, formatToolName, getContrastColor, useChat };
package/dist/index.js CHANGED
@@ -77,20 +77,59 @@ function useChat({
77
77
  agentId,
78
78
  workspaceId = "",
79
79
  source = "playground",
80
- userContext
80
+ userContext,
81
+ persist = false,
82
+ onNavigate,
83
+ playgroundOverrides
81
84
  }) {
82
- const [messages, setMessages] = react.useState([]);
85
+ const persistKey = persist ? `wallavi_${agentId}` : null;
86
+ const [messages, setMessages] = react.useState(() => {
87
+ if (!persistKey || typeof window === "undefined") return [];
88
+ try {
89
+ const saved = sessionStorage.getItem(`${persistKey}_msgs`);
90
+ return saved ? JSON.parse(saved) : [];
91
+ } catch {
92
+ return [];
93
+ }
94
+ });
83
95
  const [input, setInput] = react.useState("");
84
96
  const [streaming, setStreaming] = react.useState(false);
85
- const [threadId, setThreadId] = react.useState(() => crypto.randomUUID());
97
+ const [threadId, setThreadId] = react.useState(() => {
98
+ if (persistKey && typeof window !== "undefined") {
99
+ const saved = sessionStorage.getItem(`${persistKey}_tid`);
100
+ if (saved) return saved;
101
+ }
102
+ return crypto.randomUUID();
103
+ });
86
104
  const streamingMsgIdRef = react.useRef(null);
105
+ react.useEffect(() => {
106
+ if (!persistKey) return;
107
+ try {
108
+ sessionStorage.setItem(`${persistKey}_msgs`, JSON.stringify(messages));
109
+ } catch {
110
+ }
111
+ }, [persistKey, messages]);
112
+ react.useEffect(() => {
113
+ if (!persistKey) return;
114
+ try {
115
+ sessionStorage.setItem(`${persistKey}_tid`, threadId);
116
+ } catch {
117
+ }
118
+ }, [persistKey, threadId]);
87
119
  const reset = react.useCallback(() => {
88
120
  setMessages([]);
89
121
  setInput("");
90
122
  setStreaming(false);
91
123
  setThreadId(crypto.randomUUID());
92
124
  streamingMsgIdRef.current = null;
93
- }, []);
125
+ if (persistKey) {
126
+ try {
127
+ sessionStorage.removeItem(`${persistKey}_msgs`);
128
+ sessionStorage.removeItem(`${persistKey}_tid`);
129
+ } catch {
130
+ }
131
+ }
132
+ }, [persistKey]);
94
133
  const send = react.useCallback(
95
134
  async (text) => {
96
135
  const userInput = (text ?? input).trim();
@@ -121,11 +160,16 @@ function useChat({
121
160
  agentId,
122
161
  workspaceId,
123
162
  source,
163
+ ...playgroundOverrides ? { playgroundOverrides } : {},
124
164
  ...userContext?.userName ? { userName: userContext.userName } : {},
125
165
  ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
126
166
  userMetadata: {
127
167
  ...userContext?.metadata ?? {},
128
- ...userContext?.headers ? { headers: userContext.headers } : {}
168
+ ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
169
+ headers: {
170
+ ...token ? { Authorization: `Bearer ${token}` } : {},
171
+ ...userContext?.headers ?? {}
172
+ }
129
173
  }
130
174
  })
131
175
  });
@@ -135,6 +179,10 @@ function useChat({
135
179
  }
136
180
  if (!res.body) throw new Error("No stream body");
137
181
  const applyEvent = (proto) => {
182
+ if (proto.type === "navigate") {
183
+ if (proto.path.startsWith("/")) onNavigate?.(proto.path);
184
+ return;
185
+ }
138
186
  setMessages((prev) => {
139
187
  const idx = prev.findIndex((m) => m.id === assistantMsgId);
140
188
  if (idx === -1) return prev;
@@ -229,11 +277,24 @@ function useChat({
229
277
  },
230
278
  [input, streaming, agentId, workspaceId, source, threadId, userContext]
231
279
  );
232
- return { messages, input, setInput, streaming, threadId, send, reset };
280
+ const regenerate = react.useCallback(async () => {
281
+ if (streaming) return;
282
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
283
+ const lastText = lastUser?.parts.find((p) => p.type === "text")?.text;
284
+ if (!lastText) return;
285
+ setMessages((prev) => {
286
+ const copy = [...prev];
287
+ if (copy.at(-1)?.role === "assistant") copy.pop();
288
+ if (copy.at(-1)?.role === "user") copy.pop();
289
+ return copy;
290
+ });
291
+ await send(lastText);
292
+ }, [streaming, messages, send]);
293
+ return { messages, input, setInput, streaming, threadId, send, regenerate, reset };
233
294
  }
234
- var Avatar = ({ className, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Root, { className: ["relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className].filter(Boolean).join(" "), ...p });
235
- var AvatarImage = AvatarPrimitive__namespace.Image;
236
- var AvatarFallback = ({ className, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Fallback, { className: ["flex h-full w-full items-center justify-center rounded-full bg-muted", className].filter(Boolean).join(" "), ...p });
295
+ var Avatar = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
296
+ var AvatarImage = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
297
+ var AvatarFallback = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Fallback, { style: { display: "flex", width: "100%", height: "100%", alignItems: "center", justifyContent: "center", borderRadius: "9999px", ...style }, ...p });
237
298
  function ChatHeader({
238
299
  title,
239
300
  profilePicture,
@@ -249,11 +310,10 @@ function ChatHeader({
249
310
  style: { backgroundColor: headerBg, color: headerText },
250
311
  children: [
251
312
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 min-w-0", children: [
252
- /* @__PURE__ */ jsxRuntime.jsx(Avatar, { className: "h-8 w-8 shrink-0 border-2", style: { borderColor: `${headerText}30` }, children: profilePicture ? /* @__PURE__ */ jsxRuntime.jsx(AvatarImage, { src: profilePicture, alt: title }) : /* @__PURE__ */ jsxRuntime.jsx(
313
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { style: { width: 32, height: 32, border: `2px solid ${headerText}30` }, children: profilePicture ? /* @__PURE__ */ jsxRuntime.jsx(AvatarImage, { src: profilePicture, alt: title }) : /* @__PURE__ */ jsxRuntime.jsx(
253
314
  AvatarFallback,
254
315
  {
255
- className: "text-xs font-bold",
256
- style: { backgroundColor: `${headerText}20`, color: headerText },
316
+ style: { backgroundColor: `${headerText}20`, color: headerText, fontSize: 11, fontWeight: 700 },
257
317
  children: title.slice(0, 2).toUpperCase()
258
318
  }
259
319
  ) }),
@@ -286,9 +346,9 @@ function ChatHeader({
286
346
  }
287
347
  );
288
348
  }
289
- var Avatar2 = ({ className, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Root, { className: ["relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className].filter(Boolean).join(" "), ...p });
290
- var AvatarImage2 = AvatarPrimitive__namespace.Image;
291
- var AvatarFallback2 = ({ className, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Fallback, { className: ["flex h-full w-full items-center justify-center rounded-full bg-muted", className].filter(Boolean).join(" "), ...p });
349
+ var Avatar2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
350
+ var AvatarImage2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
351
+ var AvatarFallback2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Fallback, { style: { display: "flex", width: "100%", height: "100%", alignItems: "center", justifyContent: "center", borderRadius: "9999px", ...style }, ...p });
292
352
  var cn = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
293
353
  var ReactMarkdown = ReactMarkdownLib__default.default;
294
354
  function ThinkingDots() {
@@ -384,7 +444,7 @@ function MessageBubble({
384
444
  const visibleToolParts = showThinking ? toolParts : [];
385
445
  const isEmpty = !textPart?.text && visibleToolParts.length === 0;
386
446
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2.5 items-start", children: [
387
- /* @__PURE__ */ jsxRuntime.jsx(Avatar2, { className: "h-7 w-7 shrink-0 mt-0.5 border", children: profilePicture ? /* @__PURE__ */ jsxRuntime.jsx(AvatarImage2, { src: profilePicture, alt: agentName }) : /* @__PURE__ */ jsxRuntime.jsx(AvatarFallback2, { className: "text-[10px] font-semibold bg-primary text-primary-foreground", children: agentName.slice(0, 2).toUpperCase() }) }),
447
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar2, { style: { width: 28, height: 28, marginTop: 2, border: "1px solid rgba(0,0,0,0.08)" }, children: profilePicture ? /* @__PURE__ */ jsxRuntime.jsx(AvatarImage2, { src: profilePicture, alt: agentName }) : /* @__PURE__ */ jsxRuntime.jsx(AvatarFallback2, { style: { fontSize: 10, fontWeight: 600, backgroundColor: "var(--primary, #19191c)", color: "var(--primary-foreground, #fff)" }, children: agentName.slice(0, 2).toUpperCase() }) }),
388
448
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5 min-w-0 max-w-[82%]", children: [
389
449
  showThinking && reasoningPart && /* @__PURE__ */ jsxRuntime.jsx(ReasoningBlock, { text: reasoningPart.text }),
390
450
  visibleToolParts.map((t) => /* @__PURE__ */ jsxRuntime.jsx(ToolCallBadge, { part: t }, t.toolCallId)),
@@ -477,7 +537,9 @@ function ChatInput({
477
537
  onSend,
478
538
  streaming,
479
539
  placeholder,
480
- accentColor
540
+ accentColor,
541
+ canRegenerate = false,
542
+ onRegenerate
481
543
  }) {
482
544
  const textareaRef = react.useRef(null);
483
545
  react.useEffect(() => {
@@ -492,39 +554,54 @@ function ChatInput({
492
554
  }
493
555
  };
494
556
  const hasText = input.trim().length > 0;
495
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0 border-t bg-background px-3 py-2.5", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-end gap-2 rounded-2xl border bg-background px-3 py-2 shadow-sm focus-within:ring-1 focus-within:ring-ring/40 transition-shadow", children: [
496
- /* @__PURE__ */ jsxRuntime.jsx(
497
- "textarea",
557
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 border-t bg-background px-3 py-2.5", children: [
558
+ canRegenerate && onRegenerate && /* @__PURE__ */ jsxRuntime.jsxs(
559
+ "button",
498
560
  {
499
- ref: textareaRef,
500
- rows: 1,
501
- className: "flex-1 resize-none bg-transparent text-sm outline-none placeholder:text-muted-foreground/50 leading-relaxed max-h-32 overflow-y-auto py-0.5",
502
- placeholder: placeholder ?? "Send a message\u2026",
503
- value: input,
504
- onChange: (e) => {
505
- setInput(e.target.value);
506
- e.target.style.height = "auto";
507
- e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
508
- },
509
- onKeyDown: handleKeyDown,
561
+ onClick: onRegenerate,
510
562
  disabled: streaming,
511
- autoFocus: true
563
+ className: "flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors mb-2 disabled:opacity-40",
564
+ children: [
565
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { className: "h-3 w-3" }),
566
+ "Regenerate response"
567
+ ]
512
568
  }
513
569
  ),
514
- /* @__PURE__ */ jsxRuntime.jsx(
515
- "button",
516
- {
517
- onClick: onSend,
518
- disabled: streaming || !hasText,
519
- className: cn2(
520
- "h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
521
- hasText || streaming ? "opacity-100 shadow-sm" : "opacity-30"
522
- ),
523
- style: hasText || streaming ? { backgroundColor: accentColor, color: getContrastColor(accentColor) } : { backgroundColor: "transparent", color: "currentColor" },
524
- children: streaming ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUp, { className: "h-3.5 w-3.5" })
525
- }
526
- )
527
- ] }) });
570
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-end gap-2 rounded-2xl border bg-background px-3 py-2 shadow-sm focus-within:ring-1 focus-within:ring-ring/40 transition-shadow", children: [
571
+ /* @__PURE__ */ jsxRuntime.jsx(
572
+ "textarea",
573
+ {
574
+ id: "wallavi-chat-input",
575
+ ref: textareaRef,
576
+ rows: 1,
577
+ className: "flex-1 resize-none bg-transparent text-sm outline-none placeholder:text-muted-foreground/50 leading-relaxed max-h-32 overflow-y-auto py-0.5",
578
+ placeholder: placeholder ?? "Send a message\u2026",
579
+ value: input,
580
+ onChange: (e) => {
581
+ setInput(e.target.value);
582
+ e.target.style.height = "auto";
583
+ e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
584
+ },
585
+ onKeyDown: handleKeyDown,
586
+ disabled: streaming,
587
+ autoFocus: true
588
+ }
589
+ ),
590
+ /* @__PURE__ */ jsxRuntime.jsx(
591
+ "button",
592
+ {
593
+ onClick: onSend,
594
+ disabled: streaming || !hasText,
595
+ className: cn2(
596
+ "h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
597
+ hasText || streaming ? "opacity-100 shadow-sm" : "opacity-30"
598
+ ),
599
+ style: hasText || streaming ? { backgroundColor: accentColor, color: getContrastColor(accentColor) } : { backgroundColor: "transparent", color: "currentColor" },
600
+ children: streaming ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUp, { className: "h-3.5 w-3.5" })
601
+ }
602
+ )
603
+ ] })
604
+ ] });
528
605
  }
529
606
  var cn3 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
530
607
  function ChatWidget({
@@ -542,12 +619,18 @@ function ChatWidget({
542
619
  footer,
543
620
  theme,
544
621
  showThinking = false,
622
+ regenerateMessage = false,
623
+ persist = false,
624
+ onNavigate,
545
625
  source = "playground",
626
+ userContext,
627
+ playgroundOverrides,
546
628
  className,
547
629
  onClose,
548
630
  onReset
549
631
  }) {
550
- const chat = useChat({ agentId, workspaceId, source });
632
+ const chat = useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides });
633
+ const canRegenerate = regenerateMessage && chat.messages.length > 0 && chat.messages.at(-1)?.role === "assistant" && !chat.streaming;
551
634
  const title = displayName || agentName;
552
635
  const headerBg = userMessageColor;
553
636
  const headerText = getContrastColor(userMessageColor);
@@ -597,7 +680,9 @@ function ChatWidget({
597
680
  onSend: () => chat.send(),
598
681
  streaming: chat.streaming,
599
682
  placeholder: messagePlaceholder,
600
- accentColor: userMessageColor
683
+ accentColor: userMessageColor,
684
+ canRegenerate: !!canRegenerate,
685
+ onRegenerate: () => void chat.regenerate()
601
686
  }
602
687
  ),
603
688
  watermark && /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "shrink-0 flex items-center justify-center gap-1.5 bg-muted/50 py-1.5 border-t", children: [
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { clsx } from 'clsx';
2
2
  import { twMerge } from 'tailwind-merge';
3
- import { useState, useRef, useCallback, useEffect } from 'react';
3
+ import { useState, useRef, useEffect, useCallback } from 'react';
4
4
  import { RotateCcw, X, Loader2, ArrowUp, Zap, ChevronDown, CheckCircle2, AlertCircle } from 'lucide-react';
5
5
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
6
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
@@ -51,20 +51,59 @@ function useChat({
51
51
  agentId,
52
52
  workspaceId = "",
53
53
  source = "playground",
54
- userContext
54
+ userContext,
55
+ persist = false,
56
+ onNavigate,
57
+ playgroundOverrides
55
58
  }) {
56
- const [messages, setMessages] = useState([]);
59
+ const persistKey = persist ? `wallavi_${agentId}` : null;
60
+ const [messages, setMessages] = useState(() => {
61
+ if (!persistKey || typeof window === "undefined") return [];
62
+ try {
63
+ const saved = sessionStorage.getItem(`${persistKey}_msgs`);
64
+ return saved ? JSON.parse(saved) : [];
65
+ } catch {
66
+ return [];
67
+ }
68
+ });
57
69
  const [input, setInput] = useState("");
58
70
  const [streaming, setStreaming] = useState(false);
59
- const [threadId, setThreadId] = useState(() => crypto.randomUUID());
71
+ const [threadId, setThreadId] = useState(() => {
72
+ if (persistKey && typeof window !== "undefined") {
73
+ const saved = sessionStorage.getItem(`${persistKey}_tid`);
74
+ if (saved) return saved;
75
+ }
76
+ return crypto.randomUUID();
77
+ });
60
78
  const streamingMsgIdRef = useRef(null);
79
+ useEffect(() => {
80
+ if (!persistKey) return;
81
+ try {
82
+ sessionStorage.setItem(`${persistKey}_msgs`, JSON.stringify(messages));
83
+ } catch {
84
+ }
85
+ }, [persistKey, messages]);
86
+ useEffect(() => {
87
+ if (!persistKey) return;
88
+ try {
89
+ sessionStorage.setItem(`${persistKey}_tid`, threadId);
90
+ } catch {
91
+ }
92
+ }, [persistKey, threadId]);
61
93
  const reset = useCallback(() => {
62
94
  setMessages([]);
63
95
  setInput("");
64
96
  setStreaming(false);
65
97
  setThreadId(crypto.randomUUID());
66
98
  streamingMsgIdRef.current = null;
67
- }, []);
99
+ if (persistKey) {
100
+ try {
101
+ sessionStorage.removeItem(`${persistKey}_msgs`);
102
+ sessionStorage.removeItem(`${persistKey}_tid`);
103
+ } catch {
104
+ }
105
+ }
106
+ }, [persistKey]);
68
107
  const send = useCallback(
69
108
  async (text) => {
70
109
  const userInput = (text ?? input).trim();
@@ -95,11 +134,16 @@ function useChat({
95
134
  agentId,
96
135
  workspaceId,
97
136
  source,
137
+ ...playgroundOverrides ? { playgroundOverrides } : {},
98
138
  ...userContext?.userName ? { userName: userContext.userName } : {},
99
139
  ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
100
140
  userMetadata: {
101
141
  ...userContext?.metadata ?? {},
102
- ...userContext?.headers ? { headers: userContext.headers } : {}
142
+ ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
143
+ headers: {
144
+ ...token ? { Authorization: `Bearer ${token}` } : {},
145
+ ...userContext?.headers ?? {}
146
+ }
103
147
  }
104
148
  })
105
149
  });
@@ -109,6 +153,10 @@ function useChat({
109
153
  }
110
154
  if (!res.body) throw new Error("No stream body");
111
155
  const applyEvent = (proto) => {
156
+ if (proto.type === "navigate") {
157
+ if (proto.path.startsWith("/")) onNavigate?.(proto.path);
158
+ return;
159
+ }
112
160
  setMessages((prev) => {
113
161
  const idx = prev.findIndex((m) => m.id === assistantMsgId);
114
162
  if (idx === -1) return prev;
@@ -203,11 +251,24 @@ function useChat({
203
251
  },
204
252
  [input, streaming, agentId, workspaceId, source, threadId, userContext]
205
253
  );
206
- return { messages, input, setInput, streaming, threadId, send, reset };
254
+ const regenerate = useCallback(async () => {
255
+ if (streaming) return;
256
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
257
+ const lastText = lastUser?.parts.find((p) => p.type === "text")?.text;
258
+ if (!lastText) return;
259
+ setMessages((prev) => {
260
+ const copy = [...prev];
261
+ if (copy.at(-1)?.role === "assistant") copy.pop();
262
+ if (copy.at(-1)?.role === "user") copy.pop();
263
+ return copy;
264
+ });
265
+ await send(lastText);
266
+ }, [streaming, messages, send]);
267
+ return { messages, input, setInput, streaming, threadId, send, regenerate, reset };
207
268
  }
208
- var Avatar = ({ className, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Root, { className: ["relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className].filter(Boolean).join(" "), ...p });
209
- var AvatarImage = AvatarPrimitive.Image;
210
- var AvatarFallback = ({ className, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Fallback, { className: ["flex h-full w-full items-center justify-center rounded-full bg-muted", className].filter(Boolean).join(" "), ...p });
269
+ var Avatar = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
270
+ var AvatarImage = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
271
+ var AvatarFallback = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Fallback, { style: { display: "flex", width: "100%", height: "100%", alignItems: "center", justifyContent: "center", borderRadius: "9999px", ...style }, ...p });
211
272
  function ChatHeader({
212
273
  title,
213
274
  profilePicture,
@@ -223,11 +284,10 @@ function ChatHeader({
223
284
  style: { backgroundColor: headerBg, color: headerText },
224
285
  children: [
225
286
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 min-w-0", children: [
226
- /* @__PURE__ */ jsx(Avatar, { className: "h-8 w-8 shrink-0 border-2", style: { borderColor: `${headerText}30` }, children: profilePicture ? /* @__PURE__ */ jsx(AvatarImage, { src: profilePicture, alt: title }) : /* @__PURE__ */ jsx(
287
+ /* @__PURE__ */ jsx(Avatar, { style: { width: 32, height: 32, border: `2px solid ${headerText}30` }, children: profilePicture ? /* @__PURE__ */ jsx(AvatarImage, { src: profilePicture, alt: title }) : /* @__PURE__ */ jsx(
227
288
  AvatarFallback,
228
289
  {
229
- className: "text-xs font-bold",
230
- style: { backgroundColor: `${headerText}20`, color: headerText },
290
+ style: { backgroundColor: `${headerText}20`, color: headerText, fontSize: 11, fontWeight: 700 },
231
291
  children: title.slice(0, 2).toUpperCase()
232
292
  }
233
293
  ) }),
@@ -260,9 +320,9 @@ function ChatHeader({
260
320
  }
261
321
  );
262
322
  }
263
- var Avatar2 = ({ className, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Root, { className: ["relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className].filter(Boolean).join(" "), ...p });
264
- var AvatarImage2 = AvatarPrimitive.Image;
265
- var AvatarFallback2 = ({ className, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Fallback, { className: ["flex h-full w-full items-center justify-center rounded-full bg-muted", className].filter(Boolean).join(" "), ...p });
323
+ var Avatar2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
324
+ var AvatarImage2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
325
+ var AvatarFallback2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Fallback, { style: { display: "flex", width: "100%", height: "100%", alignItems: "center", justifyContent: "center", borderRadius: "9999px", ...style }, ...p });
266
326
  var cn = (...inputs) => twMerge(clsx(inputs));
267
327
  var ReactMarkdown = ReactMarkdownLib;
268
328
  function ThinkingDots() {
@@ -358,7 +418,7 @@ function MessageBubble({
358
418
  const visibleToolParts = showThinking ? toolParts : [];
359
419
  const isEmpty = !textPart?.text && visibleToolParts.length === 0;
360
420
  return /* @__PURE__ */ jsxs("div", { className: "flex gap-2.5 items-start", children: [
361
- /* @__PURE__ */ jsx(Avatar2, { className: "h-7 w-7 shrink-0 mt-0.5 border", children: profilePicture ? /* @__PURE__ */ jsx(AvatarImage2, { src: profilePicture, alt: agentName }) : /* @__PURE__ */ jsx(AvatarFallback2, { className: "text-[10px] font-semibold bg-primary text-primary-foreground", children: agentName.slice(0, 2).toUpperCase() }) }),
421
+ /* @__PURE__ */ jsx(Avatar2, { style: { width: 28, height: 28, marginTop: 2, border: "1px solid rgba(0,0,0,0.08)" }, children: profilePicture ? /* @__PURE__ */ jsx(AvatarImage2, { src: profilePicture, alt: agentName }) : /* @__PURE__ */ jsx(AvatarFallback2, { style: { fontSize: 10, fontWeight: 600, backgroundColor: "var(--primary, #19191c)", color: "var(--primary-foreground, #fff)" }, children: agentName.slice(0, 2).toUpperCase() }) }),
362
422
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 min-w-0 max-w-[82%]", children: [
363
423
  showThinking && reasoningPart && /* @__PURE__ */ jsx(ReasoningBlock, { text: reasoningPart.text }),
364
424
  visibleToolParts.map((t) => /* @__PURE__ */ jsx(ToolCallBadge, { part: t }, t.toolCallId)),
@@ -451,7 +511,9 @@ function ChatInput({
451
511
  onSend,
452
512
  streaming,
453
513
  placeholder,
454
- accentColor
514
+ accentColor,
515
+ canRegenerate = false,
516
+ onRegenerate
455
517
  }) {
456
518
  const textareaRef = useRef(null);
457
519
  useEffect(() => {
@@ -466,39 +528,54 @@ function ChatInput({
466
528
  }
467
529
  };
468
530
  const hasText = input.trim().length > 0;
469
- return /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t bg-background px-3 py-2.5", children: /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2 rounded-2xl border bg-background px-3 py-2 shadow-sm focus-within:ring-1 focus-within:ring-ring/40 transition-shadow", children: [
470
- /* @__PURE__ */ jsx(
471
- "textarea",
531
+ return /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-t bg-background px-3 py-2.5", children: [
532
+ canRegenerate && onRegenerate && /* @__PURE__ */ jsxs(
533
+ "button",
472
534
  {
473
- ref: textareaRef,
474
- rows: 1,
475
- className: "flex-1 resize-none bg-transparent text-sm outline-none placeholder:text-muted-foreground/50 leading-relaxed max-h-32 overflow-y-auto py-0.5",
476
- placeholder: placeholder ?? "Send a message\u2026",
477
- value: input,
478
- onChange: (e) => {
479
- setInput(e.target.value);
480
- e.target.style.height = "auto";
481
- e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
482
- },
483
- onKeyDown: handleKeyDown,
535
+ onClick: onRegenerate,
484
536
  disabled: streaming,
485
- autoFocus: true
537
+ className: "flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors mb-2 disabled:opacity-40",
538
+ children: [
539
+ /* @__PURE__ */ jsx(RotateCcw, { className: "h-3 w-3" }),
540
+ "Regenerate response"
541
+ ]
486
542
  }
487
543
  ),
488
- /* @__PURE__ */ jsx(
489
- "button",
490
- {
491
- onClick: onSend,
492
- disabled: streaming || !hasText,
493
- className: cn2(
494
- "h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
495
- hasText || streaming ? "opacity-100 shadow-sm" : "opacity-30"
496
- ),
497
- style: hasText || streaming ? { backgroundColor: accentColor, color: getContrastColor(accentColor) } : { backgroundColor: "transparent", color: "currentColor" },
498
- children: streaming ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(ArrowUp, { className: "h-3.5 w-3.5" })
499
- }
500
- )
501
- ] }) });
544
+ /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2 rounded-2xl border bg-background px-3 py-2 shadow-sm focus-within:ring-1 focus-within:ring-ring/40 transition-shadow", children: [
545
+ /* @__PURE__ */ jsx(
546
+ "textarea",
547
+ {
548
+ id: "wallavi-chat-input",
549
+ ref: textareaRef,
550
+ rows: 1,
551
+ className: "flex-1 resize-none bg-transparent text-sm outline-none placeholder:text-muted-foreground/50 leading-relaxed max-h-32 overflow-y-auto py-0.5",
552
+ placeholder: placeholder ?? "Send a message\u2026",
553
+ value: input,
554
+ onChange: (e) => {
555
+ setInput(e.target.value);
556
+ e.target.style.height = "auto";
557
+ e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
558
+ },
559
+ onKeyDown: handleKeyDown,
560
+ disabled: streaming,
561
+ autoFocus: true
562
+ }
563
+ ),
564
+ /* @__PURE__ */ jsx(
565
+ "button",
566
+ {
567
+ onClick: onSend,
568
+ disabled: streaming || !hasText,
569
+ className: cn2(
570
+ "h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
571
+ hasText || streaming ? "opacity-100 shadow-sm" : "opacity-30"
572
+ ),
573
+ style: hasText || streaming ? { backgroundColor: accentColor, color: getContrastColor(accentColor) } : { backgroundColor: "transparent", color: "currentColor" },
574
+ children: streaming ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(ArrowUp, { className: "h-3.5 w-3.5" })
575
+ }
576
+ )
577
+ ] })
578
+ ] });
502
579
  }
503
580
  var cn3 = (...inputs) => twMerge(clsx(inputs));
504
581
  function ChatWidget({
@@ -516,12 +593,18 @@ function ChatWidget({
516
593
  footer,
517
594
  theme,
518
595
  showThinking = false,
596
+ regenerateMessage = false,
597
+ persist = false,
598
+ onNavigate,
519
599
  source = "playground",
600
+ userContext,
601
+ playgroundOverrides,
520
602
  className,
521
603
  onClose,
522
604
  onReset
523
605
  }) {
524
- const chat = useChat({ agentId, workspaceId, source });
606
+ const chat = useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides });
607
+ const canRegenerate = regenerateMessage && chat.messages.length > 0 && chat.messages.at(-1)?.role === "assistant" && !chat.streaming;
525
608
  const title = displayName || agentName;
526
609
  const headerBg = userMessageColor;
527
610
  const headerText = getContrastColor(userMessageColor);
@@ -571,7 +654,9 @@ function ChatWidget({
571
654
  onSend: () => chat.send(),
572
655
  streaming: chat.streaming,
573
656
  placeholder: messagePlaceholder,
574
- accentColor: userMessageColor
657
+ accentColor: userMessageColor,
658
+ canRegenerate: !!canRegenerate,
659
+ onRegenerate: () => void chat.regenerate()
575
660
  }
576
661
  ),
577
662
  watermark && /* @__PURE__ */ jsxs("footer", { className: "shrink-0 flex items-center justify-center gap-1.5 bg-muted/50 py-1.5 border-t", children: [
package/package.json CHANGED
@@ -48,5 +48,5 @@
48
48
  "typecheck": "tsc --noEmit"
49
49
  },
50
50
  "types": "./src/index.ts",
51
- "version": "1.0.0"
51
+ "version": "1.1.0"
52
52
  }