@wallavi/widget 1.4.5 → 1.5.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
@@ -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
@@ -135,6 +135,118 @@ function useChat({
135
135
  }
136
136
  }
137
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]);
138
250
  const send = react.useCallback(
139
251
  async (text) => {
140
252
  const userInput = (text ?? input).trim();
@@ -148,144 +260,59 @@ function useChat({
148
260
  setStreaming(true);
149
261
  const assistantMsgId = newId();
150
262
  streamingMsgIdRef.current = assistantMsgId;
151
- setMessages((prev) => [
152
- ...prev,
153
- { id: assistantMsgId, role: "assistant", parts: [] }
154
- ]);
263
+ setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
155
264
  try {
156
- const isPrivate = Boolean(workspaceId);
157
- const token = isPrivate && typeof window !== "undefined" ? await window.Clerk?.session?.getToken() : null;
158
- const url = isPrivate ? `${API_URL}/api/threads/${threadId}/stream` : `${API_URL}/api/chat/stream`;
159
- const res = await fetch(url, {
160
- method: "POST",
161
- headers: {
162
- "Content-Type": "application/json",
163
- ...token ? { Authorization: `Bearer ${token}` } : {}
164
- },
165
- body: JSON.stringify({
166
- input: userInput,
167
- agentId,
168
- // Private endpoint: workspaceId + threadId in URL, playgroundOverrides allowed.
169
- // Public endpoint: threadId in body (no workspaceId, no playgroundOverrides).
170
- ...isPrivate ? { workspaceId, ...playgroundOverrides ? { playgroundOverrides } : {} } : { threadId },
171
- source,
172
- ...userContext?.userName ? { userName: userContext.userName } : {},
173
- ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
174
- userMetadata: {
175
- ...userContext?.metadata ?? {},
176
- ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
177
- // Forward client auth headers to the pipeline (for action/tool execution).
178
- // On the private path, also include the Wallavi Clerk token.
179
- headers: {
180
- ...token ? { Authorization: `Bearer ${token}` } : {},
181
- ...userContext?.headers ?? {}
182
- }
183
- }
184
- })
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 } }
185
301
  });
186
- if (!res.ok) {
187
- const errText = await res.text().catch(() => "");
188
- throw new Error(errText || `API error ${res.status}`);
189
- }
190
- if (!res.body) throw new Error("No stream body");
191
- const applyEvent = (proto) => {
192
- if (proto.type === "navigate") {
193
- if (proto.path.startsWith("/")) onNavigateRef.current?.(proto.path);
194
- return;
195
- }
196
- setMessages((prev) => {
197
- const idx = prev.findIndex((m) => m.id === assistantMsgId);
198
- if (idx === -1) return prev;
199
- const existing = prev[idx];
200
- const msg = {
201
- id: existing.id,
202
- role: existing.role,
203
- parts: [...existing.parts]
204
- };
205
- switch (proto.type) {
206
- case "text-delta": {
207
- const textIdx = msg.parts.findIndex((p) => p.type === "text");
208
- if (textIdx === -1) {
209
- msg.parts.push({ type: "text", text: proto.delta });
210
- } else {
211
- const p = msg.parts[textIdx];
212
- msg.parts[textIdx] = { type: "text", text: p.text + proto.delta };
213
- }
214
- break;
215
- }
216
- case "reasoning-delta": {
217
- const rIdx = msg.parts.findIndex((p) => p.type === "reasoning");
218
- if (rIdx === -1) {
219
- msg.parts.unshift({ type: "reasoning", text: proto.delta });
220
- } else {
221
- const p = msg.parts[rIdx];
222
- msg.parts[rIdx] = { type: "reasoning", text: p.text + proto.delta };
223
- }
224
- break;
225
- }
226
- case "tool-input-available": {
227
- msg.parts.push({
228
- type: "tool",
229
- toolCallId: proto.toolCallId,
230
- toolName: proto.toolName,
231
- input: proto.input ?? {},
232
- status: "running"
233
- });
234
- break;
235
- }
236
- case "tool-output-available": {
237
- const tIdx = msg.parts.findIndex(
238
- (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
239
- );
240
- if (tIdx !== -1) {
241
- msg.parts[tIdx] = {
242
- ...msg.parts[tIdx],
243
- status: "done",
244
- output: proto.output
245
- };
246
- }
247
- break;
248
- }
249
- case "tool-output-error": {
250
- const tIdx = msg.parts.findIndex(
251
- (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
252
- );
253
- if (tIdx !== -1) {
254
- msg.parts[tIdx] = {
255
- ...msg.parts[tIdx],
256
- status: "error",
257
- errorText: proto.errorText
258
- };
259
- }
260
- break;
261
- }
262
- default:
263
- break;
264
- }
265
- const copy = [...prev];
266
- copy[idx] = msg;
267
- return copy;
268
- });
269
- };
270
- await consumeStream(res.body, applyEvent);
271
302
  } catch {
272
303
  setMessages((prev) => {
273
304
  const idx = prev.findIndex((m) => m.id === assistantMsgId);
274
305
  if (idx === -1) return prev;
275
306
  const copy = [...prev];
276
307
  const err = copy[idx];
277
- copy[idx] = {
278
- id: err.id,
279
- role: err.role,
280
- parts: [{ type: "text", text: "Sorry, something went wrong. Please try again." }]
281
- };
308
+ copy[idx] = { id: err.id, role: err.role, parts: [{ type: "text", text: "Sorry, something went wrong. Please try again." }] };
282
309
  return copy;
283
310
  });
284
311
  }
285
312
  setStreaming(false);
286
313
  streamingMsgIdRef.current = null;
287
314
  },
288
- [input, streaming, agentId, workspaceId, source, threadId, userContext]
315
+ [streaming, fetchAndStream]
289
316
  );
290
317
  const regenerate = react.useCallback(async () => {
291
318
  if (streaming) return;
@@ -300,7 +327,7 @@ function useChat({
300
327
  });
301
328
  await send(lastText);
302
329
  }, [streaming, messages, send]);
303
- return { messages, input, setInput, streaming, threadId, send, regenerate, reset };
330
+ return { messages, input, setInput, streaming, threadId, send, regenerate, reset, selectPickerOption };
304
331
  }
305
332
  function DefaultIcon() {
306
333
  return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", style: { width: 26, height: 26 }, children: [
@@ -474,18 +501,98 @@ function ReasoningBlock({ text }) {
474
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 })
475
502
  ] });
476
503
  }
504
+ function PickerOption({
505
+ opt,
506
+ isSelected,
507
+ isConsumed,
508
+ actionDisabled,
509
+ pickerId,
510
+ paramName,
511
+ onSelect
512
+ }) {
513
+ const [hovered, setHovered] = react.useState(false);
514
+ const inactive = actionDisabled || isConsumed;
515
+ return /* @__PURE__ */ jsxRuntime.jsx(
516
+ "button",
517
+ {
518
+ disabled: inactive,
519
+ onMouseEnter: () => setHovered(true),
520
+ onMouseLeave: () => setHovered(false),
521
+ onClick: () => {
522
+ if (!inactive) onSelect(pickerId, paramName, opt.value, opt.label);
523
+ },
524
+ style: {
525
+ fontSize: 13,
526
+ lineHeight: "1.4",
527
+ borderRadius: 8,
528
+ padding: "6px 13px",
529
+ border: isSelected ? "1.5px solid var(--primary, #19191c)" : hovered && !inactive ? "1.5px solid rgba(0,0,0,0.28)" : "1.5px solid rgba(0,0,0,0.13)",
530
+ backgroundColor: isSelected ? "var(--primary, #19191c)" : hovered && !inactive ? "rgba(0,0,0,0.04)" : "#fff",
531
+ color: isSelected ? "var(--primary-foreground, #fff)" : "var(--foreground, #09090b)",
532
+ cursor: inactive ? "default" : "pointer",
533
+ opacity: isConsumed ? 0.28 : 1,
534
+ transition: "border-color 0.12s ease, background-color 0.12s ease, opacity 0.12s ease",
535
+ fontWeight: isSelected ? 600 : 400,
536
+ boxShadow: isSelected || inactive ? "none" : "0 1px 2px rgba(0,0,0,0.07)"
537
+ },
538
+ children: opt.label
539
+ }
540
+ );
541
+ }
542
+ function PickerSelector({
543
+ part,
544
+ disabled,
545
+ onSelect
546
+ }) {
547
+ return /* @__PURE__ */ jsxRuntime.jsxs(
548
+ "div",
549
+ {
550
+ style: {
551
+ display: "flex",
552
+ flexDirection: "column",
553
+ gap: 10,
554
+ padding: "12px 14px",
555
+ borderRadius: "0 14px 14px 14px",
556
+ backgroundColor: "var(--muted, #f4f4f5)",
557
+ border: "1px solid rgba(0,0,0,0.06)"
558
+ },
559
+ children: [
560
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0, fontSize: 11.5, color: "var(--muted-foreground, #71717a)", fontWeight: 600, letterSpacing: "0.04em", textTransform: "uppercase" }, children: part.label }),
561
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 7 }, children: part.options.map((opt) => {
562
+ const isSelected = part.selectedValue === opt.value;
563
+ const isConsumed = !!part.selectedValue && !isSelected;
564
+ return /* @__PURE__ */ jsxRuntime.jsx(
565
+ PickerOption,
566
+ {
567
+ opt,
568
+ isSelected,
569
+ isConsumed,
570
+ actionDisabled: disabled,
571
+ pickerId: part.pickerId,
572
+ paramName: part.paramName,
573
+ onSelect
574
+ },
575
+ opt.value
576
+ );
577
+ }) })
578
+ ]
579
+ }
580
+ );
581
+ }
477
582
  function MessageBubble({
478
583
  message,
479
584
  userColor,
480
585
  agentName,
481
586
  profilePicture,
482
587
  isStreaming,
483
- showThinking = true
588
+ showThinking = true,
589
+ onPickerSelect
484
590
  }) {
485
591
  const isUser = message.role === "user";
486
592
  const textPart = message.parts.find((p) => p.type === "text");
487
593
  const reasoningPart = message.parts.find((p) => p.type === "reasoning");
488
594
  const toolParts = message.parts.filter((p) => p.type === "tool");
595
+ const pickerParts = message.parts.filter((p) => p.type === "picker");
489
596
  const contrastColor = getContrastColor(userColor);
490
597
  if (isUser) {
491
598
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -498,12 +605,22 @@ function MessageBubble({
498
605
  ) });
499
606
  }
500
607
  const visibleToolParts = showThinking ? toolParts : [];
501
- const isEmpty = !textPart?.text && visibleToolParts.length === 0;
608
+ const isEmpty = !textPart?.text && visibleToolParts.length === 0 && pickerParts.length === 0;
502
609
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2.5 items-start", children: [
503
610
  /* @__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() }) }),
504
611
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5 min-w-0 max-w-[82%]", children: [
505
612
  showThinking && reasoningPart && /* @__PURE__ */ jsxRuntime.jsx(ReasoningBlock, { text: reasoningPart.text }),
506
613
  visibleToolParts.map((t) => /* @__PURE__ */ jsxRuntime.jsx(ToolCallBadge, { part: t }, t.toolCallId)),
614
+ pickerParts.map((p) => /* @__PURE__ */ jsxRuntime.jsx(
615
+ PickerSelector,
616
+ {
617
+ part: p,
618
+ disabled: isStreaming ?? false,
619
+ onSelect: onPickerSelect ?? (() => {
620
+ })
621
+ },
622
+ p.pickerId
623
+ )),
507
624
  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: [
508
625
  /* @__PURE__ */ jsxRuntime.jsx(
509
626
  ReactMarkdown,
@@ -548,7 +665,8 @@ function ChatMessages({
548
665
  initialMessages = [],
549
666
  suggestedMessages = [],
550
667
  showThinking = true,
551
- onSuggest
668
+ onSuggest,
669
+ onPickerSelect
552
670
  }) {
553
671
  const bottomRef = react.useRef(null);
554
672
  const showGreeting = messages.length === 0;
@@ -569,7 +687,8 @@ function ChatMessages({
569
687
  agentName,
570
688
  profilePicture,
571
689
  isStreaming: streaming && i === messages.length - 1 && msg.role === "assistant",
572
- showThinking
690
+ showThinking,
691
+ onPickerSelect
573
692
  },
574
693
  msg.id
575
694
  )) }),
@@ -603,6 +722,11 @@ function ChatInput({
603
722
  textareaRef.current.style.height = "auto";
604
723
  }
605
724
  }, [input]);
725
+ react.useEffect(() => {
726
+ if (!streaming) {
727
+ textareaRef.current?.focus();
728
+ }
729
+ }, [streaming]);
606
730
  const handleKeyDown = (e) => {
607
731
  if (e.key === "Enter" && !e.shiftKey) {
608
732
  e.preventDefault();
@@ -729,7 +853,8 @@ function ChatWidget({
729
853
  initialMessages,
730
854
  suggestedMessages,
731
855
  showThinking,
732
- onSuggest: (msg) => chat.send(msg)
856
+ onSuggest: (msg) => chat.send(msg),
857
+ onPickerSelect: chat.selectPickerOption
733
858
  }
734
859
  ),
735
860
  /* @__PURE__ */ jsxRuntime.jsx(
package/dist/index.mjs CHANGED
@@ -109,6 +109,118 @@ function useChat({
109
109
  }
110
110
  }
111
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]);
112
224
  const send = useCallback(
113
225
  async (text) => {
114
226
  const userInput = (text ?? input).trim();
@@ -122,144 +234,59 @@ function useChat({
122
234
  setStreaming(true);
123
235
  const assistantMsgId = newId();
124
236
  streamingMsgIdRef.current = assistantMsgId;
125
- setMessages((prev) => [
126
- ...prev,
127
- { id: assistantMsgId, role: "assistant", parts: [] }
128
- ]);
237
+ setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
129
238
  try {
130
- const isPrivate = Boolean(workspaceId);
131
- const token = isPrivate && typeof window !== "undefined" ? await window.Clerk?.session?.getToken() : null;
132
- const url = isPrivate ? `${API_URL}/api/threads/${threadId}/stream` : `${API_URL}/api/chat/stream`;
133
- const res = await fetch(url, {
134
- method: "POST",
135
- headers: {
136
- "Content-Type": "application/json",
137
- ...token ? { Authorization: `Bearer ${token}` } : {}
138
- },
139
- body: JSON.stringify({
140
- input: userInput,
141
- agentId,
142
- // Private endpoint: workspaceId + threadId in URL, playgroundOverrides allowed.
143
- // Public endpoint: threadId in body (no workspaceId, no playgroundOverrides).
144
- ...isPrivate ? { workspaceId, ...playgroundOverrides ? { playgroundOverrides } : {} } : { threadId },
145
- source,
146
- ...userContext?.userName ? { userName: userContext.userName } : {},
147
- ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
148
- userMetadata: {
149
- ...userContext?.metadata ?? {},
150
- ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
151
- // Forward client auth headers to the pipeline (for action/tool execution).
152
- // On the private path, also include the Wallavi Clerk token.
153
- headers: {
154
- ...token ? { Authorization: `Bearer ${token}` } : {},
155
- ...userContext?.headers ?? {}
156
- }
157
- }
158
- })
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 } }
159
275
  });
160
- if (!res.ok) {
161
- const errText = await res.text().catch(() => "");
162
- throw new Error(errText || `API error ${res.status}`);
163
- }
164
- if (!res.body) throw new Error("No stream body");
165
- const applyEvent = (proto) => {
166
- if (proto.type === "navigate") {
167
- if (proto.path.startsWith("/")) onNavigateRef.current?.(proto.path);
168
- return;
169
- }
170
- setMessages((prev) => {
171
- const idx = prev.findIndex((m) => m.id === assistantMsgId);
172
- if (idx === -1) return prev;
173
- const existing = prev[idx];
174
- const msg = {
175
- id: existing.id,
176
- role: existing.role,
177
- parts: [...existing.parts]
178
- };
179
- switch (proto.type) {
180
- case "text-delta": {
181
- const textIdx = msg.parts.findIndex((p) => p.type === "text");
182
- if (textIdx === -1) {
183
- msg.parts.push({ type: "text", text: proto.delta });
184
- } else {
185
- const p = msg.parts[textIdx];
186
- msg.parts[textIdx] = { type: "text", text: p.text + proto.delta };
187
- }
188
- break;
189
- }
190
- case "reasoning-delta": {
191
- const rIdx = msg.parts.findIndex((p) => p.type === "reasoning");
192
- if (rIdx === -1) {
193
- msg.parts.unshift({ type: "reasoning", text: proto.delta });
194
- } else {
195
- const p = msg.parts[rIdx];
196
- msg.parts[rIdx] = { type: "reasoning", text: p.text + proto.delta };
197
- }
198
- break;
199
- }
200
- case "tool-input-available": {
201
- msg.parts.push({
202
- type: "tool",
203
- toolCallId: proto.toolCallId,
204
- toolName: proto.toolName,
205
- input: proto.input ?? {},
206
- status: "running"
207
- });
208
- break;
209
- }
210
- case "tool-output-available": {
211
- const tIdx = msg.parts.findIndex(
212
- (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
213
- );
214
- if (tIdx !== -1) {
215
- msg.parts[tIdx] = {
216
- ...msg.parts[tIdx],
217
- status: "done",
218
- output: proto.output
219
- };
220
- }
221
- break;
222
- }
223
- case "tool-output-error": {
224
- const tIdx = msg.parts.findIndex(
225
- (p) => p.type === "tool" && p.toolCallId === proto.toolCallId
226
- );
227
- if (tIdx !== -1) {
228
- msg.parts[tIdx] = {
229
- ...msg.parts[tIdx],
230
- status: "error",
231
- errorText: proto.errorText
232
- };
233
- }
234
- break;
235
- }
236
- default:
237
- break;
238
- }
239
- const copy = [...prev];
240
- copy[idx] = msg;
241
- return copy;
242
- });
243
- };
244
- await consumeStream(res.body, applyEvent);
245
276
  } catch {
246
277
  setMessages((prev) => {
247
278
  const idx = prev.findIndex((m) => m.id === assistantMsgId);
248
279
  if (idx === -1) return prev;
249
280
  const copy = [...prev];
250
281
  const err = copy[idx];
251
- copy[idx] = {
252
- id: err.id,
253
- role: err.role,
254
- parts: [{ type: "text", text: "Sorry, something went wrong. Please try again." }]
255
- };
282
+ copy[idx] = { id: err.id, role: err.role, parts: [{ type: "text", text: "Sorry, something went wrong. Please try again." }] };
256
283
  return copy;
257
284
  });
258
285
  }
259
286
  setStreaming(false);
260
287
  streamingMsgIdRef.current = null;
261
288
  },
262
- [input, streaming, agentId, workspaceId, source, threadId, userContext]
289
+ [streaming, fetchAndStream]
263
290
  );
264
291
  const regenerate = useCallback(async () => {
265
292
  if (streaming) return;
@@ -274,7 +301,7 @@ function useChat({
274
301
  });
275
302
  await send(lastText);
276
303
  }, [streaming, messages, send]);
277
- return { messages, input, setInput, streaming, threadId, send, regenerate, reset };
304
+ return { messages, input, setInput, streaming, threadId, send, regenerate, reset, selectPickerOption };
278
305
  }
279
306
  function DefaultIcon() {
280
307
  return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", style: { width: 26, height: 26 }, children: [
@@ -448,18 +475,98 @@ function ReasoningBlock({ text }) {
448
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 })
449
476
  ] });
450
477
  }
478
+ function PickerOption({
479
+ opt,
480
+ isSelected,
481
+ isConsumed,
482
+ actionDisabled,
483
+ pickerId,
484
+ paramName,
485
+ onSelect
486
+ }) {
487
+ const [hovered, setHovered] = useState(false);
488
+ const inactive = actionDisabled || isConsumed;
489
+ return /* @__PURE__ */ jsx(
490
+ "button",
491
+ {
492
+ disabled: inactive,
493
+ onMouseEnter: () => setHovered(true),
494
+ onMouseLeave: () => setHovered(false),
495
+ onClick: () => {
496
+ if (!inactive) onSelect(pickerId, paramName, opt.value, opt.label);
497
+ },
498
+ style: {
499
+ fontSize: 13,
500
+ lineHeight: "1.4",
501
+ borderRadius: 8,
502
+ padding: "6px 13px",
503
+ border: isSelected ? "1.5px solid var(--primary, #19191c)" : hovered && !inactive ? "1.5px solid rgba(0,0,0,0.28)" : "1.5px solid rgba(0,0,0,0.13)",
504
+ backgroundColor: isSelected ? "var(--primary, #19191c)" : hovered && !inactive ? "rgba(0,0,0,0.04)" : "#fff",
505
+ color: isSelected ? "var(--primary-foreground, #fff)" : "var(--foreground, #09090b)",
506
+ cursor: inactive ? "default" : "pointer",
507
+ opacity: isConsumed ? 0.28 : 1,
508
+ transition: "border-color 0.12s ease, background-color 0.12s ease, opacity 0.12s ease",
509
+ fontWeight: isSelected ? 600 : 400,
510
+ boxShadow: isSelected || inactive ? "none" : "0 1px 2px rgba(0,0,0,0.07)"
511
+ },
512
+ children: opt.label
513
+ }
514
+ );
515
+ }
516
+ function PickerSelector({
517
+ part,
518
+ disabled,
519
+ onSelect
520
+ }) {
521
+ return /* @__PURE__ */ jsxs(
522
+ "div",
523
+ {
524
+ style: {
525
+ display: "flex",
526
+ flexDirection: "column",
527
+ gap: 10,
528
+ padding: "12px 14px",
529
+ borderRadius: "0 14px 14px 14px",
530
+ backgroundColor: "var(--muted, #f4f4f5)",
531
+ border: "1px solid rgba(0,0,0,0.06)"
532
+ },
533
+ children: [
534
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 11.5, color: "var(--muted-foreground, #71717a)", fontWeight: 600, letterSpacing: "0.04em", textTransform: "uppercase" }, children: part.label }),
535
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 7 }, children: part.options.map((opt) => {
536
+ const isSelected = part.selectedValue === opt.value;
537
+ const isConsumed = !!part.selectedValue && !isSelected;
538
+ return /* @__PURE__ */ jsx(
539
+ PickerOption,
540
+ {
541
+ opt,
542
+ isSelected,
543
+ isConsumed,
544
+ actionDisabled: disabled,
545
+ pickerId: part.pickerId,
546
+ paramName: part.paramName,
547
+ onSelect
548
+ },
549
+ opt.value
550
+ );
551
+ }) })
552
+ ]
553
+ }
554
+ );
555
+ }
451
556
  function MessageBubble({
452
557
  message,
453
558
  userColor,
454
559
  agentName,
455
560
  profilePicture,
456
561
  isStreaming,
457
- showThinking = true
562
+ showThinking = true,
563
+ onPickerSelect
458
564
  }) {
459
565
  const isUser = message.role === "user";
460
566
  const textPart = message.parts.find((p) => p.type === "text");
461
567
  const reasoningPart = message.parts.find((p) => p.type === "reasoning");
462
568
  const toolParts = message.parts.filter((p) => p.type === "tool");
569
+ const pickerParts = message.parts.filter((p) => p.type === "picker");
463
570
  const contrastColor = getContrastColor(userColor);
464
571
  if (isUser) {
465
572
  return /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
@@ -472,12 +579,22 @@ function MessageBubble({
472
579
  ) });
473
580
  }
474
581
  const visibleToolParts = showThinking ? toolParts : [];
475
- const isEmpty = !textPart?.text && visibleToolParts.length === 0;
582
+ const isEmpty = !textPart?.text && visibleToolParts.length === 0 && pickerParts.length === 0;
476
583
  return /* @__PURE__ */ jsxs("div", { className: "flex gap-2.5 items-start", children: [
477
584
  /* @__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() }) }),
478
585
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 min-w-0 max-w-[82%]", children: [
479
586
  showThinking && reasoningPart && /* @__PURE__ */ jsx(ReasoningBlock, { text: reasoningPart.text }),
480
587
  visibleToolParts.map((t) => /* @__PURE__ */ jsx(ToolCallBadge, { part: t }, t.toolCallId)),
588
+ pickerParts.map((p) => /* @__PURE__ */ jsx(
589
+ PickerSelector,
590
+ {
591
+ part: p,
592
+ disabled: isStreaming ?? false,
593
+ onSelect: onPickerSelect ?? (() => {
594
+ })
595
+ },
596
+ p.pickerId
597
+ )),
481
598
  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: [
482
599
  /* @__PURE__ */ jsx(
483
600
  ReactMarkdown,
@@ -522,7 +639,8 @@ function ChatMessages({
522
639
  initialMessages = [],
523
640
  suggestedMessages = [],
524
641
  showThinking = true,
525
- onSuggest
642
+ onSuggest,
643
+ onPickerSelect
526
644
  }) {
527
645
  const bottomRef = useRef(null);
528
646
  const showGreeting = messages.length === 0;
@@ -543,7 +661,8 @@ function ChatMessages({
543
661
  agentName,
544
662
  profilePicture,
545
663
  isStreaming: streaming && i === messages.length - 1 && msg.role === "assistant",
546
- showThinking
664
+ showThinking,
665
+ onPickerSelect
547
666
  },
548
667
  msg.id
549
668
  )) }),
@@ -577,6 +696,11 @@ function ChatInput({
577
696
  textareaRef.current.style.height = "auto";
578
697
  }
579
698
  }, [input]);
699
+ useEffect(() => {
700
+ if (!streaming) {
701
+ textareaRef.current?.focus();
702
+ }
703
+ }, [streaming]);
580
704
  const handleKeyDown = (e) => {
581
705
  if (e.key === "Enter" && !e.shiftKey) {
582
706
  e.preventDefault();
@@ -703,7 +827,8 @@ function ChatWidget({
703
827
  initialMessages,
704
828
  suggestedMessages,
705
829
  showThinking,
706
- onSuggest: (msg) => chat.send(msg)
830
+ onSuggest: (msg) => chat.send(msg),
831
+ onPickerSelect: chat.selectPickerOption
707
832
  }
708
833
  ),
709
834
  /* @__PURE__ */ jsx(
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.5",
36
+ "version": "1.5.1",
37
37
  "scripts": {
38
38
  "build": "tsup",
39
39
  "typecheck": "tsc --noEmit"