@wallavi/widget 1.4.4 → 1.5.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
@@ -9,13 +9,25 @@ type ToolPart = {
9
9
  output?: unknown;
10
10
  errorText?: string;
11
11
  };
12
+ type PickerPart = {
13
+ type: "picker";
14
+ pickerId: string;
15
+ paramName: string;
16
+ toolName: string;
17
+ label: string;
18
+ options: Array<{
19
+ value: string;
20
+ label: string;
21
+ }>;
22
+ selectedValue?: string;
23
+ };
12
24
  type MessagePart = {
13
25
  type: "text";
14
26
  text: string;
15
27
  } | {
16
28
  type: "reasoning";
17
29
  text: string;
18
- } | ToolPart;
30
+ } | ToolPart | PickerPart;
19
31
  type Message = {
20
32
  id: string;
21
33
  role: "user" | "assistant";
@@ -170,6 +182,7 @@ interface UseChatReturn {
170
182
  send: (text?: string) => Promise<void>;
171
183
  regenerate: () => Promise<void>;
172
184
  reset: () => void;
185
+ selectPickerOption: (pickerId: string, paramName: string, value: string, label: string) => Promise<void>;
173
186
  }
174
187
  declare function useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides, }: UseChatOptions): UseChatReturn;
175
188
 
package/dist/index.d.ts CHANGED
@@ -9,13 +9,25 @@ type ToolPart = {
9
9
  output?: unknown;
10
10
  errorText?: string;
11
11
  };
12
+ type PickerPart = {
13
+ type: "picker";
14
+ pickerId: string;
15
+ paramName: string;
16
+ toolName: string;
17
+ label: string;
18
+ options: Array<{
19
+ value: string;
20
+ label: string;
21
+ }>;
22
+ selectedValue?: string;
23
+ };
12
24
  type MessagePart = {
13
25
  type: "text";
14
26
  text: string;
15
27
  } | {
16
28
  type: "reasoning";
17
29
  text: string;
18
- } | ToolPart;
30
+ } | ToolPart | PickerPart;
19
31
  type Message = {
20
32
  id: string;
21
33
  role: "user" | "assistant";
@@ -170,6 +182,7 @@ interface UseChatReturn {
170
182
  send: (text?: string) => Promise<void>;
171
183
  regenerate: () => Promise<void>;
172
184
  reset: () => void;
185
+ selectPickerOption: (pickerId: string, paramName: string, value: string, label: string) => Promise<void>;
173
186
  }
174
187
  declare function useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides, }: UseChatOptions): UseChatReturn;
175
188
 
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ var ReactMarkdownLib__default = /*#__PURE__*/_interopDefault(ReactMarkdownLib);
34
34
  var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
35
35
 
36
36
  // src/bubble-widget.tsx
37
+ var cn = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
37
38
 
38
39
  // src/types.ts
39
40
  function getContrastColor(hex) {
@@ -83,6 +84,10 @@ function useChat({
83
84
  playgroundOverrides
84
85
  }) {
85
86
  const persistKey = persist ? `wallavi_${agentId}` : null;
87
+ const onNavigateRef = react.useRef(onNavigate);
88
+ react.useEffect(() => {
89
+ onNavigateRef.current = onNavigate;
90
+ });
86
91
  const [messages, setMessages] = react.useState(() => {
87
92
  if (!persistKey || typeof window === "undefined") return [];
88
93
  try {
@@ -130,6 +135,118 @@ function useChat({
130
135
  }
131
136
  }
132
137
  }, [persistKey]);
138
+ const applyStreamEvent = react.useCallback((proto, msgId) => {
139
+ if (proto.type === "navigate") {
140
+ if (proto.path.startsWith("/")) onNavigateRef.current?.(proto.path);
141
+ return;
142
+ }
143
+ setMessages((prev) => {
144
+ const idx = prev.findIndex((m) => m.id === msgId);
145
+ if (idx === -1) return prev;
146
+ const existing = prev[idx];
147
+ const msg = { id: existing.id, role: existing.role, parts: [...existing.parts] };
148
+ switch (proto.type) {
149
+ case "text-delta": {
150
+ const textIdx = msg.parts.findIndex((p) => p.type === "text");
151
+ if (textIdx === -1) {
152
+ msg.parts.push({ type: "text", text: proto.delta });
153
+ } else {
154
+ const p = msg.parts[textIdx];
155
+ msg.parts[textIdx] = { type: "text", text: p.text + proto.delta };
156
+ }
157
+ break;
158
+ }
159
+ case "reasoning-delta": {
160
+ const rIdx = msg.parts.findIndex((p) => p.type === "reasoning");
161
+ if (rIdx === -1) {
162
+ msg.parts.unshift({ type: "reasoning", text: proto.delta });
163
+ } else {
164
+ const p = msg.parts[rIdx];
165
+ msg.parts[rIdx] = { type: "reasoning", text: p.text + proto.delta };
166
+ }
167
+ break;
168
+ }
169
+ case "tool-input-available": {
170
+ msg.parts.push({
171
+ type: "tool",
172
+ toolCallId: proto.toolCallId,
173
+ toolName: proto.toolName,
174
+ input: proto.input ?? {},
175
+ status: "running"
176
+ });
177
+ break;
178
+ }
179
+ case "tool-output-available": {
180
+ const tIdx = msg.parts.findIndex(
181
+ (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
182
+ );
183
+ if (tIdx !== -1) {
184
+ msg.parts[tIdx] = { ...msg.parts[tIdx], status: "done", output: proto.output };
185
+ }
186
+ break;
187
+ }
188
+ case "tool-output-error": {
189
+ const tIdx = msg.parts.findIndex(
190
+ (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
191
+ );
192
+ if (tIdx !== -1) {
193
+ msg.parts[tIdx] = { ...msg.parts[tIdx], status: "error", errorText: proto.errorText };
194
+ }
195
+ break;
196
+ }
197
+ case "picker": {
198
+ msg.parts.push({
199
+ type: "picker",
200
+ pickerId: proto.pickerId,
201
+ paramName: proto.paramName,
202
+ toolName: proto.toolName,
203
+ label: proto.label,
204
+ options: proto.options
205
+ });
206
+ break;
207
+ }
208
+ }
209
+ const copy = [...prev];
210
+ copy[idx] = msg;
211
+ return copy;
212
+ });
213
+ }, []);
214
+ const fetchAndStream = react.useCallback(async (opts) => {
215
+ const { input: userInput, msgId, extraMetadata } = opts;
216
+ const isPrivate = Boolean(workspaceId);
217
+ const token = isPrivate && typeof window !== "undefined" ? await window.Clerk?.session?.getToken() : null;
218
+ const url = isPrivate ? `${API_URL}/api/threads/${threadId}/stream` : `${API_URL}/api/chat/stream`;
219
+ const res = await fetch(url, {
220
+ method: "POST",
221
+ headers: {
222
+ "Content-Type": "application/json",
223
+ ...token ? { Authorization: `Bearer ${token}` } : {}
224
+ },
225
+ body: JSON.stringify({
226
+ input: userInput,
227
+ agentId,
228
+ ...isPrivate ? { workspaceId, ...playgroundOverrides ? { playgroundOverrides } : {} } : { threadId },
229
+ source,
230
+ ...userContext?.userName ? { userName: userContext.userName } : {},
231
+ ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
232
+ userMetadata: {
233
+ ...userContext?.metadata ?? {},
234
+ ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
235
+ headers: {
236
+ ...token ? { Authorization: `Bearer ${token}` } : {},
237
+ ...userContext?.headers ?? {}
238
+ },
239
+ ...extraMetadata ?? {}
240
+ }
241
+ })
242
+ });
243
+ if (!res.ok) {
244
+ const errText = await res.text().catch(() => "");
245
+ throw new Error(errText || `API error ${res.status}`);
246
+ }
247
+ if (!res.body) throw new Error("No stream body");
248
+ await consumeStream(res.body, (proto) => applyStreamEvent(proto, msgId));
249
+ }, [agentId, workspaceId, source, threadId, userContext, playgroundOverrides, applyStreamEvent]);
133
250
  const send = react.useCallback(
134
251
  async (text) => {
135
252
  const userInput = (text ?? input).trim();
@@ -143,144 +260,59 @@ function useChat({
143
260
  setStreaming(true);
144
261
  const assistantMsgId = newId();
145
262
  streamingMsgIdRef.current = assistantMsgId;
146
- setMessages((prev) => [
147
- ...prev,
148
- { id: assistantMsgId, role: "assistant", parts: [] }
149
- ]);
263
+ setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
150
264
  try {
151
- const isPrivate = Boolean(workspaceId);
152
- const token = isPrivate && typeof window !== "undefined" ? await window.Clerk?.session?.getToken() : null;
153
- const url = isPrivate ? `${API_URL}/api/threads/${threadId}/stream` : `${API_URL}/api/chat/stream`;
154
- const res = await fetch(url, {
155
- method: "POST",
156
- headers: {
157
- "Content-Type": "application/json",
158
- ...token ? { Authorization: `Bearer ${token}` } : {}
159
- },
160
- body: JSON.stringify({
161
- input: userInput,
162
- agentId,
163
- // Private endpoint: workspaceId + threadId in URL, playgroundOverrides allowed.
164
- // Public endpoint: threadId in body (no workspaceId, no playgroundOverrides).
165
- ...isPrivate ? { workspaceId, ...playgroundOverrides ? { playgroundOverrides } : {} } : { threadId },
166
- source,
167
- ...userContext?.userName ? { userName: userContext.userName } : {},
168
- ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
169
- userMetadata: {
170
- ...userContext?.metadata ?? {},
171
- ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
172
- // Forward client auth headers to the pipeline (for action/tool execution).
173
- // On the private path, also include the Wallavi Clerk token.
174
- headers: {
175
- ...token ? { Authorization: `Bearer ${token}` } : {},
176
- ...userContext?.headers ?? {}
177
- }
178
- }
179
- })
265
+ await fetchAndStream({ input: userInput, msgId: assistantMsgId });
266
+ } catch {
267
+ setMessages((prev) => {
268
+ const idx = prev.findIndex((m) => m.id === assistantMsgId);
269
+ if (idx === -1) return prev;
270
+ const copy = [...prev];
271
+ const err = copy[idx];
272
+ copy[idx] = { id: err.id, role: err.role, parts: [{ type: "text", text: "Sorry, something went wrong. Please try again." }] };
273
+ return copy;
274
+ });
275
+ }
276
+ setStreaming(false);
277
+ streamingMsgIdRef.current = null;
278
+ },
279
+ [input, streaming, fetchAndStream]
280
+ );
281
+ const selectPickerOption = react.useCallback(
282
+ async (pickerId, paramName, value, label) => {
283
+ if (streaming) return;
284
+ setMessages(
285
+ (prev) => prev.map((msg) => ({
286
+ ...msg,
287
+ parts: msg.parts.map(
288
+ (part) => part.type === "picker" && part.pickerId === pickerId ? { ...part, selectedValue: value } : part
289
+ )
290
+ }))
291
+ );
292
+ setStreaming(true);
293
+ const assistantMsgId = newId();
294
+ streamingMsgIdRef.current = assistantMsgId;
295
+ setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
296
+ try {
297
+ await fetchAndStream({
298
+ input: label,
299
+ msgId: assistantMsgId,
300
+ extraMetadata: { __pickerSelection: { pickerId, paramName, value, label } }
180
301
  });
181
- if (!res.ok) {
182
- const errText = await res.text().catch(() => "");
183
- throw new Error(errText || `API error ${res.status}`);
184
- }
185
- if (!res.body) throw new Error("No stream body");
186
- const applyEvent = (proto) => {
187
- if (proto.type === "navigate") {
188
- if (proto.path.startsWith("/")) onNavigate?.(proto.path);
189
- return;
190
- }
191
- setMessages((prev) => {
192
- const idx = prev.findIndex((m) => m.id === assistantMsgId);
193
- if (idx === -1) return prev;
194
- const existing = prev[idx];
195
- const msg = {
196
- id: existing.id,
197
- role: existing.role,
198
- parts: [...existing.parts]
199
- };
200
- switch (proto.type) {
201
- case "text-delta": {
202
- const textIdx = msg.parts.findIndex((p) => p.type === "text");
203
- if (textIdx === -1) {
204
- msg.parts.push({ type: "text", text: proto.delta });
205
- } else {
206
- const p = msg.parts[textIdx];
207
- msg.parts[textIdx] = { type: "text", text: p.text + proto.delta };
208
- }
209
- break;
210
- }
211
- case "reasoning-delta": {
212
- const rIdx = msg.parts.findIndex((p) => p.type === "reasoning");
213
- if (rIdx === -1) {
214
- msg.parts.unshift({ type: "reasoning", text: proto.delta });
215
- } else {
216
- const p = msg.parts[rIdx];
217
- msg.parts[rIdx] = { type: "reasoning", text: p.text + proto.delta };
218
- }
219
- break;
220
- }
221
- case "tool-input-available": {
222
- msg.parts.push({
223
- type: "tool",
224
- toolCallId: proto.toolCallId,
225
- toolName: proto.toolName,
226
- input: proto.input ?? {},
227
- status: "running"
228
- });
229
- break;
230
- }
231
- case "tool-output-available": {
232
- const tIdx = msg.parts.findIndex(
233
- (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
234
- );
235
- if (tIdx !== -1) {
236
- msg.parts[tIdx] = {
237
- ...msg.parts[tIdx],
238
- status: "done",
239
- output: proto.output
240
- };
241
- }
242
- break;
243
- }
244
- case "tool-output-error": {
245
- const tIdx = msg.parts.findIndex(
246
- (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
247
- );
248
- if (tIdx !== -1) {
249
- msg.parts[tIdx] = {
250
- ...msg.parts[tIdx],
251
- status: "error",
252
- errorText: proto.errorText
253
- };
254
- }
255
- break;
256
- }
257
- default:
258
- break;
259
- }
260
- const copy = [...prev];
261
- copy[idx] = msg;
262
- return copy;
263
- });
264
- };
265
- await consumeStream(res.body, applyEvent);
266
302
  } catch {
267
303
  setMessages((prev) => {
268
304
  const idx = prev.findIndex((m) => m.id === assistantMsgId);
269
305
  if (idx === -1) return prev;
270
306
  const copy = [...prev];
271
307
  const err = copy[idx];
272
- copy[idx] = {
273
- id: err.id,
274
- role: err.role,
275
- parts: [{ type: "text", text: "Sorry, something went wrong. Please try again." }]
276
- };
308
+ copy[idx] = { id: err.id, role: err.role, parts: [{ type: "text", text: "Sorry, something went wrong. Please try again." }] };
277
309
  return copy;
278
310
  });
279
311
  }
280
312
  setStreaming(false);
281
313
  streamingMsgIdRef.current = null;
282
314
  },
283
- [input, streaming, agentId, workspaceId, source, threadId, userContext]
315
+ [streaming, fetchAndStream]
284
316
  );
285
317
  const regenerate = react.useCallback(async () => {
286
318
  if (streaming) return;
@@ -295,7 +327,7 @@ function useChat({
295
327
  });
296
328
  await send(lastText);
297
329
  }, [streaming, messages, send]);
298
- return { messages, input, setInput, streaming, threadId, send, regenerate, reset };
330
+ return { messages, input, setInput, streaming, threadId, send, regenerate, reset, selectPickerOption };
299
331
  }
300
332
  function DefaultIcon() {
301
333
  return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", style: { width: 26, height: 26 }, children: [
@@ -401,7 +433,6 @@ function ChatHeader({
401
433
  var Avatar2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
402
434
  var AvatarImage2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
403
435
  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 });
404
- var cn = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
405
436
  var ReactMarkdown = ReactMarkdownLib__default.default;
406
437
  function ThinkingDots() {
407
438
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -470,18 +501,71 @@ function ReasoningBlock({ text }) {
470
501
  open && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs text-muted-foreground/80 whitespace-pre-wrap border-l-2 border-muted pl-2 leading-relaxed", children: text })
471
502
  ] });
472
503
  }
504
+ function PickerSelector({
505
+ part,
506
+ disabled,
507
+ onSelect
508
+ }) {
509
+ return /* @__PURE__ */ jsxRuntime.jsxs(
510
+ "div",
511
+ {
512
+ style: {
513
+ display: "flex",
514
+ flexDirection: "column",
515
+ gap: 8,
516
+ padding: "12px 16px",
517
+ borderRadius: "0 16px 16px 16px",
518
+ backgroundColor: "var(--muted, #f4f4f5)"
519
+ },
520
+ children: [
521
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0, fontSize: 13, color: "var(--muted-foreground, #71717a)", fontWeight: 500 }, children: part.label }),
522
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: part.options.map((opt) => {
523
+ const isSelected = part.selectedValue === opt.value;
524
+ const isConsumed = !!part.selectedValue && !isSelected;
525
+ return /* @__PURE__ */ jsxRuntime.jsx(
526
+ "button",
527
+ {
528
+ onClick: () => {
529
+ if (!part.selectedValue && !disabled) {
530
+ onSelect(part.pickerId, part.paramName, opt.value, opt.label);
531
+ }
532
+ },
533
+ disabled: disabled || !!part.selectedValue,
534
+ style: {
535
+ fontSize: 13,
536
+ borderRadius: 20,
537
+ padding: "6px 14px",
538
+ border: isSelected ? "1.5px solid var(--primary, #19191c)" : "1.5px solid var(--border, #e4e4e7)",
539
+ backgroundColor: isSelected ? "var(--primary, #19191c)" : "transparent",
540
+ color: isSelected ? "var(--primary-foreground, #fff)" : "var(--foreground, #09090b)",
541
+ cursor: disabled || !!part.selectedValue ? "default" : "pointer",
542
+ opacity: isConsumed ? 0.35 : 1,
543
+ transition: "all 0.15s ease",
544
+ fontWeight: isSelected ? 500 : 400
545
+ },
546
+ children: opt.label
547
+ },
548
+ opt.value
549
+ );
550
+ }) })
551
+ ]
552
+ }
553
+ );
554
+ }
473
555
  function MessageBubble({
474
556
  message,
475
557
  userColor,
476
558
  agentName,
477
559
  profilePicture,
478
560
  isStreaming,
479
- showThinking = true
561
+ showThinking = true,
562
+ onPickerSelect
480
563
  }) {
481
564
  const isUser = message.role === "user";
482
565
  const textPart = message.parts.find((p) => p.type === "text");
483
566
  const reasoningPart = message.parts.find((p) => p.type === "reasoning");
484
567
  const toolParts = message.parts.filter((p) => p.type === "tool");
568
+ const pickerParts = message.parts.filter((p) => p.type === "picker");
485
569
  const contrastColor = getContrastColor(userColor);
486
570
  if (isUser) {
487
571
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -494,12 +578,22 @@ function MessageBubble({
494
578
  ) });
495
579
  }
496
580
  const visibleToolParts = showThinking ? toolParts : [];
497
- const isEmpty = !textPart?.text && visibleToolParts.length === 0;
581
+ const isEmpty = !textPart?.text && visibleToolParts.length === 0 && pickerParts.length === 0;
498
582
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2.5 items-start", children: [
499
583
  /* @__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() }) }),
500
584
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5 min-w-0 max-w-[82%]", children: [
501
585
  showThinking && reasoningPart && /* @__PURE__ */ jsxRuntime.jsx(ReasoningBlock, { text: reasoningPart.text }),
502
586
  visibleToolParts.map((t) => /* @__PURE__ */ jsxRuntime.jsx(ToolCallBadge, { part: t }, t.toolCallId)),
587
+ pickerParts.map((p) => /* @__PURE__ */ jsxRuntime.jsx(
588
+ PickerSelector,
589
+ {
590
+ part: p,
591
+ disabled: isStreaming ?? false,
592
+ onSelect: onPickerSelect ?? (() => {
593
+ })
594
+ },
595
+ p.pickerId
596
+ )),
503
597
  isEmpty && isStreaming ? /* @__PURE__ */ jsxRuntime.jsx(ThinkingDots, {}) : textPart?.text ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl rounded-tl-sm bg-muted px-4 py-2.5 text-sm leading-relaxed overflow-hidden prose prose-sm max-w-none prose-p:my-1 prose-headings:font-semibold prose-headings:my-1.5 prose-ul:my-1 prose-li:my-0.5 prose-strong:font-semibold", children: [
504
598
  /* @__PURE__ */ jsxRuntime.jsx(
505
599
  ReactMarkdown,
@@ -544,7 +638,8 @@ function ChatMessages({
544
638
  initialMessages = [],
545
639
  suggestedMessages = [],
546
640
  showThinking = true,
547
- onSuggest
641
+ onSuggest,
642
+ onPickerSelect
548
643
  }) {
549
644
  const bottomRef = react.useRef(null);
550
645
  const showGreeting = messages.length === 0;
@@ -565,7 +660,8 @@ function ChatMessages({
565
660
  agentName,
566
661
  profilePicture,
567
662
  isStreaming: streaming && i === messages.length - 1 && msg.role === "assistant",
568
- showThinking
663
+ showThinking,
664
+ onPickerSelect
569
665
  },
570
666
  msg.id
571
667
  )) }),
@@ -599,6 +695,11 @@ function ChatInput({
599
695
  textareaRef.current.style.height = "auto";
600
696
  }
601
697
  }, [input]);
698
+ react.useEffect(() => {
699
+ if (!streaming) {
700
+ textareaRef.current?.focus();
701
+ }
702
+ }, [streaming]);
602
703
  const handleKeyDown = (e) => {
603
704
  if (e.key === "Enter" && !e.shiftKey) {
604
705
  e.preventDefault();
@@ -655,7 +756,6 @@ function ChatInput({
655
756
  ] })
656
757
  ] });
657
758
  }
658
- var cn3 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
659
759
  function ChatWidget({
660
760
  agentId,
661
761
  workspaceId,
@@ -696,7 +796,7 @@ function ChatWidget({
696
796
  return /* @__PURE__ */ jsxRuntime.jsxs(
697
797
  "div",
698
798
  {
699
- className: cn3(
799
+ className: cn(
700
800
  "flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background h-full",
701
801
  className
702
802
  ),
@@ -726,7 +826,8 @@ function ChatWidget({
726
826
  initialMessages,
727
827
  suggestedMessages,
728
828
  showThinking,
729
- onSuggest: (msg) => chat.send(msg)
829
+ onSuggest: (msg) => chat.send(msg),
830
+ onPickerSelect: chat.selectPickerOption
730
831
  }
731
832
  ),
732
833
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -825,7 +926,6 @@ function useAutoConfig(agentId, enabled) {
825
926
  }, [agentId, enabled]);
826
927
  return result;
827
928
  }
828
- var cn4 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
829
929
  var KEY_EXPANDED = "wallavi_bubble_expanded";
830
930
  var KEY_DISMISSED = "wallavi_bubble_dismissed";
831
931
  function BubbleWidget({
@@ -873,35 +973,13 @@ function BubbleWidget({
873
973
  if (localStorage.getItem(KEY_EXPANDED) === "true") setExpanded(true);
874
974
  }, []);
875
975
  const remote = useAutoConfig(chatProps.agentId, autoConfig);
876
- const [resolvedBubbleIcon, setResolvedBubbleIcon] = react.useState(bubbleIconUrlProp);
877
- const [resolvedAutoOpen, setResolvedAutoOpen] = react.useState(autoOpenProp);
878
- const [resolvedKeyboardShortcut, setResolvedKeyboardShortcut] = react.useState(keyboardShortcutProp);
879
- const [resolvedPosition, setResolvedPosition] = react.useState(positionProp ?? "bottom-right");
880
- const [resolvedBubbleSize, setResolvedBubbleSize] = react.useState(bubbleSizeProp ?? 52);
881
- const [resolvedWidth, setResolvedWidth] = react.useState(widthProp ?? 360);
882
- const [resolvedHeight, setResolvedHeight] = react.useState(heightProp ?? 580);
883
- react.useEffect(() => {
884
- if (remote.loading) return;
885
- if (!bubbleIconUrlProp) setResolvedBubbleIcon(remote.bubbleIconUrl);
886
- if (!autoOpenProp) setResolvedAutoOpen(remote.autoOpen);
887
- if (!keyboardShortcutProp) setResolvedKeyboardShortcut(remote.keyboardShortcut);
888
- if (!positionProp) setResolvedPosition(remote.position);
889
- if (bubbleSizeProp == null) setResolvedBubbleSize(remote.bubbleSize);
890
- if (widthProp == null) setResolvedWidth(remote.panelWidth);
891
- if (heightProp == null) setResolvedHeight(remote.panelHeight);
892
- }, [remote.loading]);
893
- react.useEffect(() => {
894
- if (autoOpenProp) setResolvedAutoOpen(true);
895
- }, [autoOpenProp]);
896
- react.useEffect(() => {
897
- if (keyboardShortcutProp) setResolvedKeyboardShortcut(true);
898
- }, [keyboardShortcutProp]);
899
- react.useEffect(() => {
900
- if (bubbleIconUrlProp) setResolvedBubbleIcon(bubbleIconUrlProp);
901
- }, [bubbleIconUrlProp]);
902
- react.useEffect(() => {
903
- if (positionProp) setResolvedPosition(positionProp);
904
- }, [positionProp]);
976
+ const resolvedPosition = positionProp ?? remote.position;
977
+ const resolvedBubbleIcon = bubbleIconUrlProp ?? remote.bubbleIconUrl;
978
+ const resolvedAutoOpen = autoOpenProp || remote.autoOpen;
979
+ const resolvedKeyboardShortcut = keyboardShortcutProp || remote.keyboardShortcut;
980
+ const resolvedBubbleSize = bubbleSizeProp ?? remote.bubbleSize;
981
+ const resolvedWidth = widthProp ?? remote.panelWidth;
982
+ const resolvedHeight = heightProp ?? remote.panelHeight;
905
983
  const definedChatProps = Object.fromEntries(
906
984
  Object.entries(chatProps).filter(([, v]) => v !== void 0)
907
985
  );
@@ -911,18 +989,18 @@ function BubbleWidget({
911
989
  agentId: chatProps.agentId,
912
990
  agentName: chatProps.agentName
913
991
  };
992
+ const setOpenRef = react.useRef(setOpen);
993
+ react.useEffect(() => {
994
+ setOpenRef.current = setOpen;
995
+ });
914
996
  react.useEffect(() => {
915
997
  if (!resolvedAutoOpen || autoOpenedRef.current) return;
916
998
  const dismissedUntil = Number(localStorage.getItem(KEY_DISMISSED) ?? 0);
917
999
  if (dismissedUntil < Date.now()) {
918
1000
  autoOpenedRef.current = true;
919
- setOpen(true);
1001
+ setOpenRef.current(true);
920
1002
  }
921
1003
  }, [resolvedAutoOpen]);
922
- const setOpenRef = react.useRef(setOpen);
923
- react.useEffect(() => {
924
- setOpenRef.current = setOpen;
925
- });
926
1004
  react.useEffect(() => {
927
1005
  if (!resolvedKeyboardShortcut) return;
928
1006
  const onKey = (e) => {
@@ -988,7 +1066,7 @@ function BubbleWidget({
988
1066
  onClose: handleClose,
989
1067
  onExpand: toggleExpanded,
990
1068
  expanded,
991
- className: cn4("shadow-2xl h-full", panelClassName)
1069
+ className: cn("shadow-2xl h-full", panelClassName)
992
1070
  }
993
1071
  )
994
1072
  }
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useRef, useEffect, useCallback } from 'react';
1
+ import { useRef, useEffect, useState, useCallback } from 'react';
2
2
  import { X, RotateCcw, Loader2, ArrowUp, Zap, ChevronDown, CheckCircle2, AlertCircle } from 'lucide-react';
3
3
  import { clsx } from 'clsx';
4
4
  import { twMerge } from 'tailwind-merge';
@@ -8,6 +8,7 @@ import ReactMarkdownLib from 'react-markdown';
8
8
  import remarkGfm from 'remark-gfm';
9
9
 
10
10
  // src/bubble-widget.tsx
11
+ var cn = (...inputs) => twMerge(clsx(inputs));
11
12
 
12
13
  // src/types.ts
13
14
  function getContrastColor(hex) {
@@ -57,6 +58,10 @@ function useChat({
57
58
  playgroundOverrides
58
59
  }) {
59
60
  const persistKey = persist ? `wallavi_${agentId}` : null;
61
+ const onNavigateRef = useRef(onNavigate);
62
+ useEffect(() => {
63
+ onNavigateRef.current = onNavigate;
64
+ });
60
65
  const [messages, setMessages] = useState(() => {
61
66
  if (!persistKey || typeof window === "undefined") return [];
62
67
  try {
@@ -104,6 +109,118 @@ function useChat({
104
109
  }
105
110
  }
106
111
  }, [persistKey]);
112
+ const applyStreamEvent = useCallback((proto, msgId) => {
113
+ if (proto.type === "navigate") {
114
+ if (proto.path.startsWith("/")) onNavigateRef.current?.(proto.path);
115
+ return;
116
+ }
117
+ setMessages((prev) => {
118
+ const idx = prev.findIndex((m) => m.id === msgId);
119
+ if (idx === -1) return prev;
120
+ const existing = prev[idx];
121
+ const msg = { id: existing.id, role: existing.role, parts: [...existing.parts] };
122
+ switch (proto.type) {
123
+ case "text-delta": {
124
+ const textIdx = msg.parts.findIndex((p) => p.type === "text");
125
+ if (textIdx === -1) {
126
+ msg.parts.push({ type: "text", text: proto.delta });
127
+ } else {
128
+ const p = msg.parts[textIdx];
129
+ msg.parts[textIdx] = { type: "text", text: p.text + proto.delta };
130
+ }
131
+ break;
132
+ }
133
+ case "reasoning-delta": {
134
+ const rIdx = msg.parts.findIndex((p) => p.type === "reasoning");
135
+ if (rIdx === -1) {
136
+ msg.parts.unshift({ type: "reasoning", text: proto.delta });
137
+ } else {
138
+ const p = msg.parts[rIdx];
139
+ msg.parts[rIdx] = { type: "reasoning", text: p.text + proto.delta };
140
+ }
141
+ break;
142
+ }
143
+ case "tool-input-available": {
144
+ msg.parts.push({
145
+ type: "tool",
146
+ toolCallId: proto.toolCallId,
147
+ toolName: proto.toolName,
148
+ input: proto.input ?? {},
149
+ status: "running"
150
+ });
151
+ break;
152
+ }
153
+ case "tool-output-available": {
154
+ const tIdx = msg.parts.findIndex(
155
+ (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
156
+ );
157
+ if (tIdx !== -1) {
158
+ msg.parts[tIdx] = { ...msg.parts[tIdx], status: "done", output: proto.output };
159
+ }
160
+ break;
161
+ }
162
+ case "tool-output-error": {
163
+ const tIdx = msg.parts.findIndex(
164
+ (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
165
+ );
166
+ if (tIdx !== -1) {
167
+ msg.parts[tIdx] = { ...msg.parts[tIdx], status: "error", errorText: proto.errorText };
168
+ }
169
+ break;
170
+ }
171
+ case "picker": {
172
+ msg.parts.push({
173
+ type: "picker",
174
+ pickerId: proto.pickerId,
175
+ paramName: proto.paramName,
176
+ toolName: proto.toolName,
177
+ label: proto.label,
178
+ options: proto.options
179
+ });
180
+ break;
181
+ }
182
+ }
183
+ const copy = [...prev];
184
+ copy[idx] = msg;
185
+ return copy;
186
+ });
187
+ }, []);
188
+ const fetchAndStream = useCallback(async (opts) => {
189
+ const { input: userInput, msgId, extraMetadata } = opts;
190
+ const isPrivate = Boolean(workspaceId);
191
+ const token = isPrivate && typeof window !== "undefined" ? await window.Clerk?.session?.getToken() : null;
192
+ const url = isPrivate ? `${API_URL}/api/threads/${threadId}/stream` : `${API_URL}/api/chat/stream`;
193
+ const res = await fetch(url, {
194
+ method: "POST",
195
+ headers: {
196
+ "Content-Type": "application/json",
197
+ ...token ? { Authorization: `Bearer ${token}` } : {}
198
+ },
199
+ body: JSON.stringify({
200
+ input: userInput,
201
+ agentId,
202
+ ...isPrivate ? { workspaceId, ...playgroundOverrides ? { playgroundOverrides } : {} } : { threadId },
203
+ source,
204
+ ...userContext?.userName ? { userName: userContext.userName } : {},
205
+ ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
206
+ userMetadata: {
207
+ ...userContext?.metadata ?? {},
208
+ ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
209
+ headers: {
210
+ ...token ? { Authorization: `Bearer ${token}` } : {},
211
+ ...userContext?.headers ?? {}
212
+ },
213
+ ...extraMetadata ?? {}
214
+ }
215
+ })
216
+ });
217
+ if (!res.ok) {
218
+ const errText = await res.text().catch(() => "");
219
+ throw new Error(errText || `API error ${res.status}`);
220
+ }
221
+ if (!res.body) throw new Error("No stream body");
222
+ await consumeStream(res.body, (proto) => applyStreamEvent(proto, msgId));
223
+ }, [agentId, workspaceId, source, threadId, userContext, playgroundOverrides, applyStreamEvent]);
107
224
  const send = useCallback(
108
225
  async (text) => {
109
226
  const userInput = (text ?? input).trim();
@@ -117,144 +234,59 @@ function useChat({
117
234
  setStreaming(true);
118
235
  const assistantMsgId = newId();
119
236
  streamingMsgIdRef.current = assistantMsgId;
120
- setMessages((prev) => [
121
- ...prev,
122
- { id: assistantMsgId, role: "assistant", parts: [] }
123
- ]);
237
+ setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
124
238
  try {
125
- const isPrivate = Boolean(workspaceId);
126
- const token = isPrivate && typeof window !== "undefined" ? await window.Clerk?.session?.getToken() : null;
127
- const url = isPrivate ? `${API_URL}/api/threads/${threadId}/stream` : `${API_URL}/api/chat/stream`;
128
- const res = await fetch(url, {
129
- method: "POST",
130
- headers: {
131
- "Content-Type": "application/json",
132
- ...token ? { Authorization: `Bearer ${token}` } : {}
133
- },
134
- body: JSON.stringify({
135
- input: userInput,
136
- agentId,
137
- // Private endpoint: workspaceId + threadId in URL, playgroundOverrides allowed.
138
- // Public endpoint: threadId in body (no workspaceId, no playgroundOverrides).
139
- ...isPrivate ? { workspaceId, ...playgroundOverrides ? { playgroundOverrides } : {} } : { threadId },
140
- source,
141
- ...userContext?.userName ? { userName: userContext.userName } : {},
142
- ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
143
- userMetadata: {
144
- ...userContext?.metadata ?? {},
145
- ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
146
- // Forward client auth headers to the pipeline (for action/tool execution).
147
- // On the private path, also include the Wallavi Clerk token.
148
- headers: {
149
- ...token ? { Authorization: `Bearer ${token}` } : {},
150
- ...userContext?.headers ?? {}
151
- }
152
- }
153
- })
239
+ await fetchAndStream({ input: userInput, msgId: assistantMsgId });
240
+ } catch {
241
+ setMessages((prev) => {
242
+ const idx = prev.findIndex((m) => m.id === assistantMsgId);
243
+ if (idx === -1) return prev;
244
+ const copy = [...prev];
245
+ const err = copy[idx];
246
+ copy[idx] = { id: err.id, role: err.role, parts: [{ type: "text", text: "Sorry, something went wrong. Please try again." }] };
247
+ return copy;
248
+ });
249
+ }
250
+ setStreaming(false);
251
+ streamingMsgIdRef.current = null;
252
+ },
253
+ [input, streaming, fetchAndStream]
254
+ );
255
+ const selectPickerOption = useCallback(
256
+ async (pickerId, paramName, value, label) => {
257
+ if (streaming) return;
258
+ setMessages(
259
+ (prev) => prev.map((msg) => ({
260
+ ...msg,
261
+ parts: msg.parts.map(
262
+ (part) => part.type === "picker" && part.pickerId === pickerId ? { ...part, selectedValue: value } : part
263
+ )
264
+ }))
265
+ );
266
+ setStreaming(true);
267
+ const assistantMsgId = newId();
268
+ streamingMsgIdRef.current = assistantMsgId;
269
+ setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
270
+ try {
271
+ await fetchAndStream({
272
+ input: label,
273
+ msgId: assistantMsgId,
274
+ extraMetadata: { __pickerSelection: { pickerId, paramName, value, label } }
154
275
  });
155
- if (!res.ok) {
156
- const errText = await res.text().catch(() => "");
157
- throw new Error(errText || `API error ${res.status}`);
158
- }
159
- if (!res.body) throw new Error("No stream body");
160
- const applyEvent = (proto) => {
161
- if (proto.type === "navigate") {
162
- if (proto.path.startsWith("/")) onNavigate?.(proto.path);
163
- return;
164
- }
165
- setMessages((prev) => {
166
- const idx = prev.findIndex((m) => m.id === assistantMsgId);
167
- if (idx === -1) return prev;
168
- const existing = prev[idx];
169
- const msg = {
170
- id: existing.id,
171
- role: existing.role,
172
- parts: [...existing.parts]
173
- };
174
- switch (proto.type) {
175
- case "text-delta": {
176
- const textIdx = msg.parts.findIndex((p) => p.type === "text");
177
- if (textIdx === -1) {
178
- msg.parts.push({ type: "text", text: proto.delta });
179
- } else {
180
- const p = msg.parts[textIdx];
181
- msg.parts[textIdx] = { type: "text", text: p.text + proto.delta };
182
- }
183
- break;
184
- }
185
- case "reasoning-delta": {
186
- const rIdx = msg.parts.findIndex((p) => p.type === "reasoning");
187
- if (rIdx === -1) {
188
- msg.parts.unshift({ type: "reasoning", text: proto.delta });
189
- } else {
190
- const p = msg.parts[rIdx];
191
- msg.parts[rIdx] = { type: "reasoning", text: p.text + proto.delta };
192
- }
193
- break;
194
- }
195
- case "tool-input-available": {
196
- msg.parts.push({
197
- type: "tool",
198
- toolCallId: proto.toolCallId,
199
- toolName: proto.toolName,
200
- input: proto.input ?? {},
201
- status: "running"
202
- });
203
- break;
204
- }
205
- case "tool-output-available": {
206
- const tIdx = msg.parts.findIndex(
207
- (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
208
- );
209
- if (tIdx !== -1) {
210
- msg.parts[tIdx] = {
211
- ...msg.parts[tIdx],
212
- status: "done",
213
- output: proto.output
214
- };
215
- }
216
- break;
217
- }
218
- case "tool-output-error": {
219
- const tIdx = msg.parts.findIndex(
220
- (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
221
- );
222
- if (tIdx !== -1) {
223
- msg.parts[tIdx] = {
224
- ...msg.parts[tIdx],
225
- status: "error",
226
- errorText: proto.errorText
227
- };
228
- }
229
- break;
230
- }
231
- default:
232
- break;
233
- }
234
- const copy = [...prev];
235
- copy[idx] = msg;
236
- return copy;
237
- });
238
- };
239
- await consumeStream(res.body, applyEvent);
240
276
  } catch {
241
277
  setMessages((prev) => {
242
278
  const idx = prev.findIndex((m) => m.id === assistantMsgId);
243
279
  if (idx === -1) return prev;
244
280
  const copy = [...prev];
245
281
  const err = copy[idx];
246
- copy[idx] = {
247
- id: err.id,
248
- role: err.role,
249
- parts: [{ type: "text", text: "Sorry, something went wrong. Please try again." }]
250
- };
282
+ copy[idx] = { id: err.id, role: err.role, parts: [{ type: "text", text: "Sorry, something went wrong. Please try again." }] };
251
283
  return copy;
252
284
  });
253
285
  }
254
286
  setStreaming(false);
255
287
  streamingMsgIdRef.current = null;
256
288
  },
257
- [input, streaming, agentId, workspaceId, source, threadId, userContext]
289
+ [streaming, fetchAndStream]
258
290
  );
259
291
  const regenerate = useCallback(async () => {
260
292
  if (streaming) return;
@@ -269,7 +301,7 @@ function useChat({
269
301
  });
270
302
  await send(lastText);
271
303
  }, [streaming, messages, send]);
272
- return { messages, input, setInput, streaming, threadId, send, regenerate, reset };
304
+ return { messages, input, setInput, streaming, threadId, send, regenerate, reset, selectPickerOption };
273
305
  }
274
306
  function DefaultIcon() {
275
307
  return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", style: { width: 26, height: 26 }, children: [
@@ -375,7 +407,6 @@ function ChatHeader({
375
407
  var Avatar2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
376
408
  var AvatarImage2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
377
409
  var AvatarFallback2 = ({ style, ...p }) => /* @__PURE__ */ jsx(AvatarPrimitive.Fallback, { style: { display: "flex", width: "100%", height: "100%", alignItems: "center", justifyContent: "center", borderRadius: "9999px", ...style }, ...p });
378
- var cn = (...inputs) => twMerge(clsx(inputs));
379
410
  var ReactMarkdown = ReactMarkdownLib;
380
411
  function ThinkingDots() {
381
412
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -444,18 +475,71 @@ function ReasoningBlock({ text }) {
444
475
  open && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground/80 whitespace-pre-wrap border-l-2 border-muted pl-2 leading-relaxed", children: text })
445
476
  ] });
446
477
  }
478
+ function PickerSelector({
479
+ part,
480
+ disabled,
481
+ onSelect
482
+ }) {
483
+ return /* @__PURE__ */ jsxs(
484
+ "div",
485
+ {
486
+ style: {
487
+ display: "flex",
488
+ flexDirection: "column",
489
+ gap: 8,
490
+ padding: "12px 16px",
491
+ borderRadius: "0 16px 16px 16px",
492
+ backgroundColor: "var(--muted, #f4f4f5)"
493
+ },
494
+ children: [
495
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 13, color: "var(--muted-foreground, #71717a)", fontWeight: 500 }, children: part.label }),
496
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: part.options.map((opt) => {
497
+ const isSelected = part.selectedValue === opt.value;
498
+ const isConsumed = !!part.selectedValue && !isSelected;
499
+ return /* @__PURE__ */ jsx(
500
+ "button",
501
+ {
502
+ onClick: () => {
503
+ if (!part.selectedValue && !disabled) {
504
+ onSelect(part.pickerId, part.paramName, opt.value, opt.label);
505
+ }
506
+ },
507
+ disabled: disabled || !!part.selectedValue,
508
+ style: {
509
+ fontSize: 13,
510
+ borderRadius: 20,
511
+ padding: "6px 14px",
512
+ border: isSelected ? "1.5px solid var(--primary, #19191c)" : "1.5px solid var(--border, #e4e4e7)",
513
+ backgroundColor: isSelected ? "var(--primary, #19191c)" : "transparent",
514
+ color: isSelected ? "var(--primary-foreground, #fff)" : "var(--foreground, #09090b)",
515
+ cursor: disabled || !!part.selectedValue ? "default" : "pointer",
516
+ opacity: isConsumed ? 0.35 : 1,
517
+ transition: "all 0.15s ease",
518
+ fontWeight: isSelected ? 500 : 400
519
+ },
520
+ children: opt.label
521
+ },
522
+ opt.value
523
+ );
524
+ }) })
525
+ ]
526
+ }
527
+ );
528
+ }
447
529
  function MessageBubble({
448
530
  message,
449
531
  userColor,
450
532
  agentName,
451
533
  profilePicture,
452
534
  isStreaming,
453
- showThinking = true
535
+ showThinking = true,
536
+ onPickerSelect
454
537
  }) {
455
538
  const isUser = message.role === "user";
456
539
  const textPart = message.parts.find((p) => p.type === "text");
457
540
  const reasoningPart = message.parts.find((p) => p.type === "reasoning");
458
541
  const toolParts = message.parts.filter((p) => p.type === "tool");
542
+ const pickerParts = message.parts.filter((p) => p.type === "picker");
459
543
  const contrastColor = getContrastColor(userColor);
460
544
  if (isUser) {
461
545
  return /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
@@ -468,12 +552,22 @@ function MessageBubble({
468
552
  ) });
469
553
  }
470
554
  const visibleToolParts = showThinking ? toolParts : [];
471
- const isEmpty = !textPart?.text && visibleToolParts.length === 0;
555
+ const isEmpty = !textPart?.text && visibleToolParts.length === 0 && pickerParts.length === 0;
472
556
  return /* @__PURE__ */ jsxs("div", { className: "flex gap-2.5 items-start", children: [
473
557
  /* @__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() }) }),
474
558
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 min-w-0 max-w-[82%]", children: [
475
559
  showThinking && reasoningPart && /* @__PURE__ */ jsx(ReasoningBlock, { text: reasoningPart.text }),
476
560
  visibleToolParts.map((t) => /* @__PURE__ */ jsx(ToolCallBadge, { part: t }, t.toolCallId)),
561
+ pickerParts.map((p) => /* @__PURE__ */ jsx(
562
+ PickerSelector,
563
+ {
564
+ part: p,
565
+ disabled: isStreaming ?? false,
566
+ onSelect: onPickerSelect ?? (() => {
567
+ })
568
+ },
569
+ p.pickerId
570
+ )),
477
571
  isEmpty && isStreaming ? /* @__PURE__ */ jsx(ThinkingDots, {}) : textPart?.text ? /* @__PURE__ */ jsxs("div", { className: "rounded-2xl rounded-tl-sm bg-muted px-4 py-2.5 text-sm leading-relaxed overflow-hidden prose prose-sm max-w-none prose-p:my-1 prose-headings:font-semibold prose-headings:my-1.5 prose-ul:my-1 prose-li:my-0.5 prose-strong:font-semibold", children: [
478
572
  /* @__PURE__ */ jsx(
479
573
  ReactMarkdown,
@@ -518,7 +612,8 @@ function ChatMessages({
518
612
  initialMessages = [],
519
613
  suggestedMessages = [],
520
614
  showThinking = true,
521
- onSuggest
615
+ onSuggest,
616
+ onPickerSelect
522
617
  }) {
523
618
  const bottomRef = useRef(null);
524
619
  const showGreeting = messages.length === 0;
@@ -539,7 +634,8 @@ function ChatMessages({
539
634
  agentName,
540
635
  profilePicture,
541
636
  isStreaming: streaming && i === messages.length - 1 && msg.role === "assistant",
542
- showThinking
637
+ showThinking,
638
+ onPickerSelect
543
639
  },
544
640
  msg.id
545
641
  )) }),
@@ -573,6 +669,11 @@ function ChatInput({
573
669
  textareaRef.current.style.height = "auto";
574
670
  }
575
671
  }, [input]);
672
+ useEffect(() => {
673
+ if (!streaming) {
674
+ textareaRef.current?.focus();
675
+ }
676
+ }, [streaming]);
576
677
  const handleKeyDown = (e) => {
577
678
  if (e.key === "Enter" && !e.shiftKey) {
578
679
  e.preventDefault();
@@ -629,7 +730,6 @@ function ChatInput({
629
730
  ] })
630
731
  ] });
631
732
  }
632
- var cn3 = (...inputs) => twMerge(clsx(inputs));
633
733
  function ChatWidget({
634
734
  agentId,
635
735
  workspaceId,
@@ -670,7 +770,7 @@ function ChatWidget({
670
770
  return /* @__PURE__ */ jsxs(
671
771
  "div",
672
772
  {
673
- className: cn3(
773
+ className: cn(
674
774
  "flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background h-full",
675
775
  className
676
776
  ),
@@ -700,7 +800,8 @@ function ChatWidget({
700
800
  initialMessages,
701
801
  suggestedMessages,
702
802
  showThinking,
703
- onSuggest: (msg) => chat.send(msg)
803
+ onSuggest: (msg) => chat.send(msg),
804
+ onPickerSelect: chat.selectPickerOption
704
805
  }
705
806
  ),
706
807
  /* @__PURE__ */ jsx(
@@ -799,7 +900,6 @@ function useAutoConfig(agentId, enabled) {
799
900
  }, [agentId, enabled]);
800
901
  return result;
801
902
  }
802
- var cn4 = (...inputs) => twMerge(clsx(inputs));
803
903
  var KEY_EXPANDED = "wallavi_bubble_expanded";
804
904
  var KEY_DISMISSED = "wallavi_bubble_dismissed";
805
905
  function BubbleWidget({
@@ -847,35 +947,13 @@ function BubbleWidget({
847
947
  if (localStorage.getItem(KEY_EXPANDED) === "true") setExpanded(true);
848
948
  }, []);
849
949
  const remote = useAutoConfig(chatProps.agentId, autoConfig);
850
- const [resolvedBubbleIcon, setResolvedBubbleIcon] = useState(bubbleIconUrlProp);
851
- const [resolvedAutoOpen, setResolvedAutoOpen] = useState(autoOpenProp);
852
- const [resolvedKeyboardShortcut, setResolvedKeyboardShortcut] = useState(keyboardShortcutProp);
853
- const [resolvedPosition, setResolvedPosition] = useState(positionProp ?? "bottom-right");
854
- const [resolvedBubbleSize, setResolvedBubbleSize] = useState(bubbleSizeProp ?? 52);
855
- const [resolvedWidth, setResolvedWidth] = useState(widthProp ?? 360);
856
- const [resolvedHeight, setResolvedHeight] = useState(heightProp ?? 580);
857
- useEffect(() => {
858
- if (remote.loading) return;
859
- if (!bubbleIconUrlProp) setResolvedBubbleIcon(remote.bubbleIconUrl);
860
- if (!autoOpenProp) setResolvedAutoOpen(remote.autoOpen);
861
- if (!keyboardShortcutProp) setResolvedKeyboardShortcut(remote.keyboardShortcut);
862
- if (!positionProp) setResolvedPosition(remote.position);
863
- if (bubbleSizeProp == null) setResolvedBubbleSize(remote.bubbleSize);
864
- if (widthProp == null) setResolvedWidth(remote.panelWidth);
865
- if (heightProp == null) setResolvedHeight(remote.panelHeight);
866
- }, [remote.loading]);
867
- useEffect(() => {
868
- if (autoOpenProp) setResolvedAutoOpen(true);
869
- }, [autoOpenProp]);
870
- useEffect(() => {
871
- if (keyboardShortcutProp) setResolvedKeyboardShortcut(true);
872
- }, [keyboardShortcutProp]);
873
- useEffect(() => {
874
- if (bubbleIconUrlProp) setResolvedBubbleIcon(bubbleIconUrlProp);
875
- }, [bubbleIconUrlProp]);
876
- useEffect(() => {
877
- if (positionProp) setResolvedPosition(positionProp);
878
- }, [positionProp]);
950
+ const resolvedPosition = positionProp ?? remote.position;
951
+ const resolvedBubbleIcon = bubbleIconUrlProp ?? remote.bubbleIconUrl;
952
+ const resolvedAutoOpen = autoOpenProp || remote.autoOpen;
953
+ const resolvedKeyboardShortcut = keyboardShortcutProp || remote.keyboardShortcut;
954
+ const resolvedBubbleSize = bubbleSizeProp ?? remote.bubbleSize;
955
+ const resolvedWidth = widthProp ?? remote.panelWidth;
956
+ const resolvedHeight = heightProp ?? remote.panelHeight;
879
957
  const definedChatProps = Object.fromEntries(
880
958
  Object.entries(chatProps).filter(([, v]) => v !== void 0)
881
959
  );
@@ -885,18 +963,18 @@ function BubbleWidget({
885
963
  agentId: chatProps.agentId,
886
964
  agentName: chatProps.agentName
887
965
  };
966
+ const setOpenRef = useRef(setOpen);
967
+ useEffect(() => {
968
+ setOpenRef.current = setOpen;
969
+ });
888
970
  useEffect(() => {
889
971
  if (!resolvedAutoOpen || autoOpenedRef.current) return;
890
972
  const dismissedUntil = Number(localStorage.getItem(KEY_DISMISSED) ?? 0);
891
973
  if (dismissedUntil < Date.now()) {
892
974
  autoOpenedRef.current = true;
893
- setOpen(true);
975
+ setOpenRef.current(true);
894
976
  }
895
977
  }, [resolvedAutoOpen]);
896
- const setOpenRef = useRef(setOpen);
897
- useEffect(() => {
898
- setOpenRef.current = setOpen;
899
- });
900
978
  useEffect(() => {
901
979
  if (!resolvedKeyboardShortcut) return;
902
980
  const onKey = (e) => {
@@ -962,7 +1040,7 @@ function BubbleWidget({
962
1040
  onClose: handleClose,
963
1041
  onExpand: toggleExpanded,
964
1042
  expanded,
965
- className: cn4("shadow-2xl h-full", panelClassName)
1043
+ className: cn("shadow-2xl h-full", panelClassName)
966
1044
  }
967
1045
  )
968
1046
  }
package/package.json CHANGED
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "private": false,
35
35
  "types": "./dist/index.d.ts",
36
- "version": "1.4.4",
36
+ "version": "1.5.0",
37
37
  "scripts": {
38
38
  "build": "tsup",
39
39
  "typecheck": "tsc --noEmit"