@wallavi/widget 1.0.1 → 1.1.1

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,27 @@ 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;
65
+ /**
66
+ * Hide the close (X) button in the widget header.
67
+ * Useful when the integrator controls open/close from an outer bubble wrapper
68
+ * and wants to avoid showing two close buttons.
69
+ */
70
+ hideCloseButton?: boolean;
46
71
  source?: string;
47
72
  userContext?: UserContext;
73
+ /** Per-session tool overrides used by Playground (not persisted) */
74
+ playgroundOverrides?: {
75
+ ragIds?: string[];
76
+ mcpIds?: string[];
77
+ actionIds?: string[];
78
+ };
48
79
  }
49
80
  interface ChatWidgetProps extends ChatWidgetConfig {
50
81
  className?: string;
@@ -54,13 +85,20 @@ interface ChatWidgetProps extends ChatWidgetConfig {
54
85
  declare function getContrastColor(hex: string): string;
55
86
  declare function formatToolName(name: string): string;
56
87
 
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;
88
+ declare function ChatWidget({ agentId, workspaceId, agentName, displayName, profilePicture, userMessageColor, initialMessages, suggestedMessages, messagePlaceholder, watermark, watermarkLogoUrl, footer, theme, showThinking, regenerateMessage, persist, onNavigate, hideCloseButton, source, userContext, playgroundOverrides, className, onClose, onReset, }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
58
89
 
59
90
  interface UseChatOptions {
60
91
  agentId: string;
61
92
  workspaceId?: string;
62
93
  source?: string;
63
94
  userContext?: UserContext;
95
+ persist?: boolean;
96
+ onNavigate?: (path: string) => void;
97
+ playgroundOverrides?: {
98
+ ragIds?: string[];
99
+ mcpIds?: string[];
100
+ actionIds?: string[];
101
+ };
64
102
  }
65
103
  interface UseChatReturn {
66
104
  messages: Message[];
@@ -69,8 +107,9 @@ interface UseChatReturn {
69
107
  streaming: boolean;
70
108
  threadId: string;
71
109
  send: (text?: string) => Promise<void>;
110
+ regenerate: () => Promise<void>;
72
111
  reset: () => void;
73
112
  }
74
- declare function useChat({ agentId, workspaceId, source, userContext, }: UseChatOptions): UseChatReturn;
113
+ declare function useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides, }: UseChatOptions): UseChatReturn;
75
114
 
76
115
  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,27 @@ 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;
65
+ /**
66
+ * Hide the close (X) button in the widget header.
67
+ * Useful when the integrator controls open/close from an outer bubble wrapper
68
+ * and wants to avoid showing two close buttons.
69
+ */
70
+ hideCloseButton?: boolean;
46
71
  source?: string;
47
72
  userContext?: UserContext;
73
+ /** Per-session tool overrides used by Playground (not persisted) */
74
+ playgroundOverrides?: {
75
+ ragIds?: string[];
76
+ mcpIds?: string[];
77
+ actionIds?: string[];
78
+ };
48
79
  }
49
80
  interface ChatWidgetProps extends ChatWidgetConfig {
50
81
  className?: string;
@@ -54,13 +85,20 @@ interface ChatWidgetProps extends ChatWidgetConfig {
54
85
  declare function getContrastColor(hex: string): string;
55
86
  declare function formatToolName(name: string): string;
56
87
 
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;
88
+ declare function ChatWidget({ agentId, workspaceId, agentName, displayName, profilePicture, userMessageColor, initialMessages, suggestedMessages, messagePlaceholder, watermark, watermarkLogoUrl, footer, theme, showThinking, regenerateMessage, persist, onNavigate, hideCloseButton, source, userContext, playgroundOverrides, className, onClose, onReset, }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
58
89
 
59
90
  interface UseChatOptions {
60
91
  agentId: string;
61
92
  workspaceId?: string;
62
93
  source?: string;
63
94
  userContext?: UserContext;
95
+ persist?: boolean;
96
+ onNavigate?: (path: string) => void;
97
+ playgroundOverrides?: {
98
+ ragIds?: string[];
99
+ mcpIds?: string[];
100
+ actionIds?: string[];
101
+ };
64
102
  }
65
103
  interface UseChatReturn {
66
104
  messages: Message[];
@@ -69,8 +107,9 @@ interface UseChatReturn {
69
107
  streaming: boolean;
70
108
  threadId: string;
71
109
  send: (text?: string) => Promise<void>;
110
+ regenerate: () => Promise<void>;
72
111
  reset: () => void;
73
112
  }
74
- declare function useChat({ agentId, workspaceId, source, userContext, }: UseChatOptions): UseChatReturn;
113
+ declare function useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides, }: UseChatOptions): UseChatReturn;
75
114
 
76
115
  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,7 +277,20 @@ 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
295
  var Avatar = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
235
296
  var AvatarImage = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
@@ -476,7 +537,9 @@ function ChatInput({
476
537
  onSend,
477
538
  streaming,
478
539
  placeholder,
479
- accentColor
540
+ accentColor,
541
+ canRegenerate = false,
542
+ onRegenerate
480
543
  }) {
481
544
  const textareaRef = react.useRef(null);
482
545
  react.useEffect(() => {
@@ -491,39 +554,54 @@ function ChatInput({
491
554
  }
492
555
  };
493
556
  const hasText = input.trim().length > 0;
494
- 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: [
495
- /* @__PURE__ */ jsxRuntime.jsx(
496
- "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",
497
560
  {
498
- ref: textareaRef,
499
- rows: 1,
500
- 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",
501
- placeholder: placeholder ?? "Send a message\u2026",
502
- value: input,
503
- onChange: (e) => {
504
- setInput(e.target.value);
505
- e.target.style.height = "auto";
506
- e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
507
- },
508
- onKeyDown: handleKeyDown,
561
+ onClick: onRegenerate,
509
562
  disabled: streaming,
510
- 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
+ ]
511
568
  }
512
569
  ),
513
- /* @__PURE__ */ jsxRuntime.jsx(
514
- "button",
515
- {
516
- onClick: onSend,
517
- disabled: streaming || !hasText,
518
- className: cn2(
519
- "h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
520
- hasText || streaming ? "opacity-100 shadow-sm" : "opacity-30"
521
- ),
522
- style: hasText || streaming ? { backgroundColor: accentColor, color: getContrastColor(accentColor) } : { backgroundColor: "transparent", color: "currentColor" },
523
- 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" })
524
- }
525
- )
526
- ] }) });
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
+ ] });
527
605
  }
528
606
  var cn3 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
529
607
  function ChatWidget({
@@ -537,16 +615,23 @@ function ChatWidget({
537
615
  suggestedMessages = [],
538
616
  messagePlaceholder,
539
617
  watermark = true,
540
- watermarkLogoUrl = "https://wallavi.com/wallavi.svg",
618
+ watermarkLogoUrl = "https://app.wallavi.com/wallavi.svg",
541
619
  footer,
542
620
  theme,
543
621
  showThinking = false,
622
+ regenerateMessage = false,
623
+ persist = false,
624
+ onNavigate,
625
+ hideCloseButton = false,
544
626
  source = "playground",
627
+ userContext,
628
+ playgroundOverrides,
545
629
  className,
546
630
  onClose,
547
631
  onReset
548
632
  }) {
549
- const chat = useChat({ agentId, workspaceId, source });
633
+ const chat = useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides });
634
+ const canRegenerate = regenerateMessage && chat.messages.length > 0 && chat.messages.at(-1)?.role === "assistant" && !chat.streaming;
550
635
  const title = displayName || agentName;
551
636
  const headerBg = userMessageColor;
552
637
  const headerText = getContrastColor(userMessageColor);
@@ -558,7 +643,7 @@ function ChatWidget({
558
643
  "div",
559
644
  {
560
645
  className: cn3(
561
- "flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background",
646
+ "flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background h-full",
562
647
  className
563
648
  ),
564
649
  style: { colorScheme: theme },
@@ -571,7 +656,7 @@ function ChatWidget({
571
656
  headerBg,
572
657
  headerText,
573
658
  onReset: handleReset,
574
- onClose
659
+ onClose: hideCloseButton ? void 0 : onClose
575
660
  }
576
661
  ),
577
662
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -596,7 +681,9 @@ function ChatWidget({
596
681
  onSend: () => chat.send(),
597
682
  streaming: chat.streaming,
598
683
  placeholder: messagePlaceholder,
599
- accentColor: userMessageColor
684
+ accentColor: userMessageColor,
685
+ canRegenerate: !!canRegenerate,
686
+ onRegenerate: () => void chat.regenerate()
600
687
  }
601
688
  ),
602
689
  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,7 +251,20 @@ 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
269
  var Avatar = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
209
270
  var AvatarImage = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
@@ -450,7 +511,9 @@ function ChatInput({
450
511
  onSend,
451
512
  streaming,
452
513
  placeholder,
453
- accentColor
514
+ accentColor,
515
+ canRegenerate = false,
516
+ onRegenerate
454
517
  }) {
455
518
  const textareaRef = useRef(null);
456
519
  useEffect(() => {
@@ -465,39 +528,54 @@ function ChatInput({
465
528
  }
466
529
  };
467
530
  const hasText = input.trim().length > 0;
468
- 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: [
469
- /* @__PURE__ */ jsx(
470
- "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",
471
534
  {
472
- ref: textareaRef,
473
- rows: 1,
474
- 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",
475
- placeholder: placeholder ?? "Send a message\u2026",
476
- value: input,
477
- onChange: (e) => {
478
- setInput(e.target.value);
479
- e.target.style.height = "auto";
480
- e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
481
- },
482
- onKeyDown: handleKeyDown,
535
+ onClick: onRegenerate,
483
536
  disabled: streaming,
484
- 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
+ ]
485
542
  }
486
543
  ),
487
- /* @__PURE__ */ jsx(
488
- "button",
489
- {
490
- onClick: onSend,
491
- disabled: streaming || !hasText,
492
- className: cn2(
493
- "h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
494
- hasText || streaming ? "opacity-100 shadow-sm" : "opacity-30"
495
- ),
496
- style: hasText || streaming ? { backgroundColor: accentColor, color: getContrastColor(accentColor) } : { backgroundColor: "transparent", color: "currentColor" },
497
- children: streaming ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(ArrowUp, { className: "h-3.5 w-3.5" })
498
- }
499
- )
500
- ] }) });
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
+ ] });
501
579
  }
502
580
  var cn3 = (...inputs) => twMerge(clsx(inputs));
503
581
  function ChatWidget({
@@ -511,16 +589,23 @@ function ChatWidget({
511
589
  suggestedMessages = [],
512
590
  messagePlaceholder,
513
591
  watermark = true,
514
- watermarkLogoUrl = "https://wallavi.com/wallavi.svg",
592
+ watermarkLogoUrl = "https://app.wallavi.com/wallavi.svg",
515
593
  footer,
516
594
  theme,
517
595
  showThinking = false,
596
+ regenerateMessage = false,
597
+ persist = false,
598
+ onNavigate,
599
+ hideCloseButton = false,
518
600
  source = "playground",
601
+ userContext,
602
+ playgroundOverrides,
519
603
  className,
520
604
  onClose,
521
605
  onReset
522
606
  }) {
523
- const chat = useChat({ agentId, workspaceId, source });
607
+ const chat = useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides });
608
+ const canRegenerate = regenerateMessage && chat.messages.length > 0 && chat.messages.at(-1)?.role === "assistant" && !chat.streaming;
524
609
  const title = displayName || agentName;
525
610
  const headerBg = userMessageColor;
526
611
  const headerText = getContrastColor(userMessageColor);
@@ -532,7 +617,7 @@ function ChatWidget({
532
617
  "div",
533
618
  {
534
619
  className: cn3(
535
- "flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background",
620
+ "flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background h-full",
536
621
  className
537
622
  ),
538
623
  style: { colorScheme: theme },
@@ -545,7 +630,7 @@ function ChatWidget({
545
630
  headerBg,
546
631
  headerText,
547
632
  onReset: handleReset,
548
- onClose
633
+ onClose: hideCloseButton ? void 0 : onClose
549
634
  }
550
635
  ),
551
636
  /* @__PURE__ */ jsx(
@@ -570,7 +655,9 @@ function ChatWidget({
570
655
  onSend: () => chat.send(),
571
656
  streaming: chat.streaming,
572
657
  placeholder: messagePlaceholder,
573
- accentColor: userMessageColor
658
+ accentColor: userMessageColor,
659
+ canRegenerate: !!canRegenerate,
660
+ onRegenerate: () => void chat.regenerate()
574
661
  }
575
662
  ),
576
663
  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
@@ -11,42 +11,32 @@
11
11
  "@types/node": "^22.10.7",
12
12
  "@types/react": "^19.1.0",
13
13
  "@types/react-dom": "^19.1.0",
14
- "@wallavi/typescript-config": "workspace:*",
15
14
  "tsup": "^8.3.5",
16
- "typescript": "^5.7.3"
15
+ "typescript": "^5.7.3",
16
+ "@wallavi/typescript-config": "0.0.1"
17
17
  },
18
18
  "exports": {
19
19
  ".": {
20
- "default": "./src/index.ts",
21
- "types": "./src/index.ts"
20
+ "import": "./dist/index.mjs",
21
+ "require": "./dist/index.js",
22
+ "types": "./dist/index.d.ts"
22
23
  }
23
24
  },
24
25
  "files": [
25
26
  "dist"
26
27
  ],
27
- "main": "./src/index.ts",
28
+ "main": "./dist/index.js",
28
29
  "name": "@wallavi/widget",
29
30
  "peerDependencies": {
30
31
  "react": "^18.0.0 || ^19.0.0",
31
32
  "react-dom": "^18.0.0 || ^19.0.0"
32
33
  },
33
34
  "private": false,
34
- "publishConfig": {
35
- "exports": {
36
- ".": {
37
- "import": "./dist/index.mjs",
38
- "require": "./dist/index.js",
39
- "types": "./dist/index.d.ts"
40
- }
41
- },
42
- "main": "./dist/index.js",
43
- "module": "./dist/index.mjs",
44
- "types": "./dist/index.d.ts"
45
- },
35
+ "types": "./dist/index.d.ts",
36
+ "version": "1.1.1",
46
37
  "scripts": {
47
38
  "build": "tsup",
48
39
  "typecheck": "tsc --noEmit"
49
40
  },
50
- "types": "./src/index.ts",
51
- "version": "1.0.1"
52
- }
41
+ "module": "./dist/index.mjs"
42
+ }