@wallavi/widget 1.4.5 → 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
@@ -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,71 @@ 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 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
+ }
477
555
  function MessageBubble({
478
556
  message,
479
557
  userColor,
480
558
  agentName,
481
559
  profilePicture,
482
560
  isStreaming,
483
- showThinking = true
561
+ showThinking = true,
562
+ onPickerSelect
484
563
  }) {
485
564
  const isUser = message.role === "user";
486
565
  const textPart = message.parts.find((p) => p.type === "text");
487
566
  const reasoningPart = message.parts.find((p) => p.type === "reasoning");
488
567
  const toolParts = message.parts.filter((p) => p.type === "tool");
568
+ const pickerParts = message.parts.filter((p) => p.type === "picker");
489
569
  const contrastColor = getContrastColor(userColor);
490
570
  if (isUser) {
491
571
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -498,12 +578,22 @@ function MessageBubble({
498
578
  ) });
499
579
  }
500
580
  const visibleToolParts = showThinking ? toolParts : [];
501
- const isEmpty = !textPart?.text && visibleToolParts.length === 0;
581
+ const isEmpty = !textPart?.text && visibleToolParts.length === 0 && pickerParts.length === 0;
502
582
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2.5 items-start", children: [
503
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() }) }),
504
584
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5 min-w-0 max-w-[82%]", children: [
505
585
  showThinking && reasoningPart && /* @__PURE__ */ jsxRuntime.jsx(ReasoningBlock, { text: reasoningPart.text }),
506
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
+ )),
507
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: [
508
598
  /* @__PURE__ */ jsxRuntime.jsx(
509
599
  ReactMarkdown,
@@ -548,7 +638,8 @@ function ChatMessages({
548
638
  initialMessages = [],
549
639
  suggestedMessages = [],
550
640
  showThinking = true,
551
- onSuggest
641
+ onSuggest,
642
+ onPickerSelect
552
643
  }) {
553
644
  const bottomRef = react.useRef(null);
554
645
  const showGreeting = messages.length === 0;
@@ -569,7 +660,8 @@ function ChatMessages({
569
660
  agentName,
570
661
  profilePicture,
571
662
  isStreaming: streaming && i === messages.length - 1 && msg.role === "assistant",
572
- showThinking
663
+ showThinking,
664
+ onPickerSelect
573
665
  },
574
666
  msg.id
575
667
  )) }),
@@ -603,6 +695,11 @@ function ChatInput({
603
695
  textareaRef.current.style.height = "auto";
604
696
  }
605
697
  }, [input]);
698
+ react.useEffect(() => {
699
+ if (!streaming) {
700
+ textareaRef.current?.focus();
701
+ }
702
+ }, [streaming]);
606
703
  const handleKeyDown = (e) => {
607
704
  if (e.key === "Enter" && !e.shiftKey) {
608
705
  e.preventDefault();
@@ -729,7 +826,8 @@ function ChatWidget({
729
826
  initialMessages,
730
827
  suggestedMessages,
731
828
  showThinking,
732
- onSuggest: (msg) => chat.send(msg)
829
+ onSuggest: (msg) => chat.send(msg),
830
+ onPickerSelect: chat.selectPickerOption
733
831
  }
734
832
  ),
735
833
  /* @__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,71 @@ 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 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
+ }
451
529
  function MessageBubble({
452
530
  message,
453
531
  userColor,
454
532
  agentName,
455
533
  profilePicture,
456
534
  isStreaming,
457
- showThinking = true
535
+ showThinking = true,
536
+ onPickerSelect
458
537
  }) {
459
538
  const isUser = message.role === "user";
460
539
  const textPart = message.parts.find((p) => p.type === "text");
461
540
  const reasoningPart = message.parts.find((p) => p.type === "reasoning");
462
541
  const toolParts = message.parts.filter((p) => p.type === "tool");
542
+ const pickerParts = message.parts.filter((p) => p.type === "picker");
463
543
  const contrastColor = getContrastColor(userColor);
464
544
  if (isUser) {
465
545
  return /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
@@ -472,12 +552,22 @@ function MessageBubble({
472
552
  ) });
473
553
  }
474
554
  const visibleToolParts = showThinking ? toolParts : [];
475
- const isEmpty = !textPart?.text && visibleToolParts.length === 0;
555
+ const isEmpty = !textPart?.text && visibleToolParts.length === 0 && pickerParts.length === 0;
476
556
  return /* @__PURE__ */ jsxs("div", { className: "flex gap-2.5 items-start", children: [
477
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() }) }),
478
558
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 min-w-0 max-w-[82%]", children: [
479
559
  showThinking && reasoningPart && /* @__PURE__ */ jsx(ReasoningBlock, { text: reasoningPart.text }),
480
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
+ )),
481
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: [
482
572
  /* @__PURE__ */ jsx(
483
573
  ReactMarkdown,
@@ -522,7 +612,8 @@ function ChatMessages({
522
612
  initialMessages = [],
523
613
  suggestedMessages = [],
524
614
  showThinking = true,
525
- onSuggest
615
+ onSuggest,
616
+ onPickerSelect
526
617
  }) {
527
618
  const bottomRef = useRef(null);
528
619
  const showGreeting = messages.length === 0;
@@ -543,7 +634,8 @@ function ChatMessages({
543
634
  agentName,
544
635
  profilePicture,
545
636
  isStreaming: streaming && i === messages.length - 1 && msg.role === "assistant",
546
- showThinking
637
+ showThinking,
638
+ onPickerSelect
547
639
  },
548
640
  msg.id
549
641
  )) }),
@@ -577,6 +669,11 @@ function ChatInput({
577
669
  textareaRef.current.style.height = "auto";
578
670
  }
579
671
  }, [input]);
672
+ useEffect(() => {
673
+ if (!streaming) {
674
+ textareaRef.current?.focus();
675
+ }
676
+ }, [streaming]);
580
677
  const handleKeyDown = (e) => {
581
678
  if (e.key === "Enter" && !e.shiftKey) {
582
679
  e.preventDefault();
@@ -703,7 +800,8 @@ function ChatWidget({
703
800
  initialMessages,
704
801
  suggestedMessages,
705
802
  showThinking,
706
- onSuggest: (msg) => chat.send(msg)
803
+ onSuggest: (msg) => chat.send(msg),
804
+ onPickerSelect: chat.selectPickerOption
707
805
  }
708
806
  ),
709
807
  /* @__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.0",
37
37
  "scripts": {
38
38
  "build": "tsup",
39
39
  "typecheck": "tsc --noEmit"