@wallavi/widget 1.5.3 → 1.6.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 +88 -2
- package/dist/index.d.ts +88 -2
- package/dist/index.js +570 -114
- package/dist/index.mjs +571 -117
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -212,7 +212,7 @@ function useChat({
|
|
|
212
212
|
});
|
|
213
213
|
}, []);
|
|
214
214
|
const fetchAndStream = react.useCallback(async (opts) => {
|
|
215
|
-
const { input: userInput, msgId, extraMetadata } = opts;
|
|
215
|
+
const { input: userInput, msgId, extraMetadata, attachments } = opts;
|
|
216
216
|
const isPrivate = Boolean(workspaceId);
|
|
217
217
|
const token = isPrivate && typeof window !== "undefined" ? await window.Clerk?.session?.getToken() : null;
|
|
218
218
|
const url = isPrivate ? `${API_URL}/api/threads/${threadId}/stream` : `${API_URL}/api/chat/stream`;
|
|
@@ -227,6 +227,7 @@ function useChat({
|
|
|
227
227
|
agentId,
|
|
228
228
|
...isPrivate ? { workspaceId, ...playgroundOverrides ? { playgroundOverrides } : {} } : { threadId },
|
|
229
229
|
source,
|
|
230
|
+
...attachments?.length ? { attachments } : {},
|
|
230
231
|
...userContext?.userName ? { userName: userContext.userName } : {},
|
|
231
232
|
...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
|
|
232
233
|
userMetadata: {
|
|
@@ -247,22 +248,30 @@ function useChat({
|
|
|
247
248
|
if (!res.body) throw new Error("No stream body");
|
|
248
249
|
await consumeStream(res.body, (proto) => applyStreamEvent(proto, msgId));
|
|
249
250
|
}, [agentId, workspaceId, source, threadId, userContext, playgroundOverrides, applyStreamEvent]);
|
|
251
|
+
const pendingAttachmentsRef = react.useRef([]);
|
|
250
252
|
const send = react.useCallback(
|
|
251
253
|
async (text) => {
|
|
252
254
|
const userInput = (text ?? input).trim();
|
|
253
255
|
if (!userInput || streaming) return;
|
|
254
256
|
setInput("");
|
|
257
|
+
const attachments = pendingAttachmentsRef.current.length > 0 ? [...pendingAttachmentsRef.current] : void 0;
|
|
258
|
+
pendingAttachmentsRef.current = [];
|
|
255
259
|
const userMsgId = newId();
|
|
256
260
|
setMessages((prev) => [
|
|
257
261
|
...prev,
|
|
258
|
-
{
|
|
262
|
+
{
|
|
263
|
+
id: userMsgId,
|
|
264
|
+
role: "user",
|
|
265
|
+
parts: [{ type: "text", text: userInput }],
|
|
266
|
+
...attachments ? { attachments } : {}
|
|
267
|
+
}
|
|
259
268
|
]);
|
|
260
269
|
setStreaming(true);
|
|
261
270
|
const assistantMsgId = newId();
|
|
262
271
|
streamingMsgIdRef.current = assistantMsgId;
|
|
263
272
|
setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
|
|
264
273
|
try {
|
|
265
|
-
await fetchAndStream({ input: userInput, msgId: assistantMsgId });
|
|
274
|
+
await fetchAndStream({ input: userInput, msgId: assistantMsgId, attachments });
|
|
266
275
|
} catch {
|
|
267
276
|
setMessages((prev) => {
|
|
268
277
|
const idx = prev.findIndex((m) => m.id === assistantMsgId);
|
|
@@ -312,8 +321,11 @@ function useChat({
|
|
|
312
321
|
setStreaming(false);
|
|
313
322
|
streamingMsgIdRef.current = null;
|
|
314
323
|
},
|
|
315
|
-
[streaming, fetchAndStream]
|
|
324
|
+
[input, streaming, fetchAndStream]
|
|
316
325
|
);
|
|
326
|
+
const queueAttachments = react.useCallback((payloads) => {
|
|
327
|
+
pendingAttachmentsRef.current = payloads;
|
|
328
|
+
}, []);
|
|
317
329
|
const regenerate = react.useCallback(async () => {
|
|
318
330
|
if (streaming) return;
|
|
319
331
|
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
@@ -327,7 +339,176 @@ function useChat({
|
|
|
327
339
|
});
|
|
328
340
|
await send(lastText);
|
|
329
341
|
}, [streaming, messages, send]);
|
|
330
|
-
return { messages, input, setInput, streaming, threadId, send, regenerate, reset, selectPickerOption };
|
|
342
|
+
return { messages, input, setInput, streaming, threadId, send, queueAttachments, regenerate, reset, selectPickerOption };
|
|
343
|
+
}
|
|
344
|
+
function getPreferredMimeType() {
|
|
345
|
+
if (typeof MediaRecorder === "undefined") return "";
|
|
346
|
+
const candidates = [
|
|
347
|
+
"audio/webm;codecs=opus",
|
|
348
|
+
"audio/webm",
|
|
349
|
+
"audio/ogg;codecs=opus",
|
|
350
|
+
"audio/ogg",
|
|
351
|
+
"audio/mp4"
|
|
352
|
+
];
|
|
353
|
+
return candidates.find((t) => MediaRecorder.isTypeSupported(t)) ?? "";
|
|
354
|
+
}
|
|
355
|
+
function mimeTypeToExtension(mimeType) {
|
|
356
|
+
if (mimeType.includes("ogg")) return "ogg";
|
|
357
|
+
if (mimeType.includes("mp4")) return "mp4";
|
|
358
|
+
return "webm";
|
|
359
|
+
}
|
|
360
|
+
var DEFAULT_API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
|
|
361
|
+
function useVoice({ agentId, apiUrl, onTranscript, onError }) {
|
|
362
|
+
const [voiceState, setVoiceState] = react.useState("idle");
|
|
363
|
+
const recorderRef = react.useRef(null);
|
|
364
|
+
const chunksRef = react.useRef([]);
|
|
365
|
+
const streamRef = react.useRef(null);
|
|
366
|
+
const errorTimerRef = react.useRef(null);
|
|
367
|
+
const isSupported = typeof window !== "undefined" && typeof MediaRecorder !== "undefined" && !!navigator?.mediaDevices?.getUserMedia;
|
|
368
|
+
const base = apiUrl ?? DEFAULT_API_URL;
|
|
369
|
+
const transcribeBlob = react.useCallback(
|
|
370
|
+
async (blob, mimeType) => {
|
|
371
|
+
setVoiceState("transcribing");
|
|
372
|
+
try {
|
|
373
|
+
const ext = mimeTypeToExtension(mimeType);
|
|
374
|
+
const form = new FormData();
|
|
375
|
+
form.append("audio", blob, `recording.${ext}`);
|
|
376
|
+
form.append("agentId", agentId);
|
|
377
|
+
const res = await fetch(`${base}/api/chat/transcribe`, { method: "POST", body: form });
|
|
378
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
379
|
+
const data = await res.json();
|
|
380
|
+
if (!data.text?.trim()) throw new Error(data.error ?? "Empty transcript");
|
|
381
|
+
onTranscript(data.text.trim());
|
|
382
|
+
setVoiceState("idle");
|
|
383
|
+
} catch (err) {
|
|
384
|
+
const msg = err instanceof Error ? err.message : "Transcription failed";
|
|
385
|
+
onError?.(msg);
|
|
386
|
+
setVoiceState("error");
|
|
387
|
+
errorTimerRef.current = setTimeout(() => setVoiceState("idle"), 2500);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
[agentId, base, onTranscript, onError]
|
|
391
|
+
);
|
|
392
|
+
const start = react.useCallback(async () => {
|
|
393
|
+
if (!isSupported || voiceState !== "idle") return;
|
|
394
|
+
try {
|
|
395
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
396
|
+
streamRef.current = stream;
|
|
397
|
+
const mimeType = getPreferredMimeType();
|
|
398
|
+
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : void 0);
|
|
399
|
+
chunksRef.current = [];
|
|
400
|
+
recorder.ondataavailable = (e) => {
|
|
401
|
+
if (e.data.size > 0) chunksRef.current.push(e.data);
|
|
402
|
+
};
|
|
403
|
+
recorder.onstop = async () => {
|
|
404
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
405
|
+
const blob = new Blob(chunksRef.current, { type: mimeType || "audio/webm" });
|
|
406
|
+
if (blob.size === 0) {
|
|
407
|
+
onError?.("Recording was empty \u2014 please try again.");
|
|
408
|
+
setVoiceState("idle");
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
await transcribeBlob(blob, mimeType || "audio/webm");
|
|
412
|
+
};
|
|
413
|
+
recorder.start(250);
|
|
414
|
+
recorderRef.current = recorder;
|
|
415
|
+
setVoiceState("recording");
|
|
416
|
+
} catch (err) {
|
|
417
|
+
const msg = err instanceof Error ? err.message : "Microphone access denied";
|
|
418
|
+
onError?.(msg);
|
|
419
|
+
setVoiceState("idle");
|
|
420
|
+
}
|
|
421
|
+
}, [isSupported, voiceState, transcribeBlob, onError]);
|
|
422
|
+
const stop = react.useCallback(() => {
|
|
423
|
+
if (recorderRef.current?.state === "recording") {
|
|
424
|
+
recorderRef.current.stop();
|
|
425
|
+
recorderRef.current = null;
|
|
426
|
+
}
|
|
427
|
+
}, []);
|
|
428
|
+
react.useEffect(() => {
|
|
429
|
+
return () => {
|
|
430
|
+
if (errorTimerRef.current) clearTimeout(errorTimerRef.current);
|
|
431
|
+
if (recorderRef.current?.state === "recording") recorderRef.current.stop();
|
|
432
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
433
|
+
};
|
|
434
|
+
}, []);
|
|
435
|
+
return { voiceState, isSupported, start, stop };
|
|
436
|
+
}
|
|
437
|
+
var DEFAULT_API_URL2 = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
|
|
438
|
+
function makeId() {
|
|
439
|
+
return `att_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
440
|
+
}
|
|
441
|
+
function useAttachments({
|
|
442
|
+
agentId,
|
|
443
|
+
apiUrl,
|
|
444
|
+
maxFiles = 5
|
|
445
|
+
}) {
|
|
446
|
+
const [attachments, setAttachments] = react.useState([]);
|
|
447
|
+
const base = apiUrl ?? DEFAULT_API_URL2;
|
|
448
|
+
const uploadOne = react.useCallback(
|
|
449
|
+
async (file, id) => {
|
|
450
|
+
const form = new FormData();
|
|
451
|
+
form.append("file", file);
|
|
452
|
+
form.append("agentId", agentId);
|
|
453
|
+
try {
|
|
454
|
+
const res = await fetch(`${base}/api/chat/upload`, { method: "POST", body: form });
|
|
455
|
+
if (!res.ok) {
|
|
456
|
+
const data = await res.json().catch(() => ({}));
|
|
457
|
+
setAttachments(
|
|
458
|
+
(prev) => prev.map(
|
|
459
|
+
(a) => a.id === id ? {
|
|
460
|
+
...a,
|
|
461
|
+
status: "error",
|
|
462
|
+
errorMessage: data.error ?? `Upload failed (${res.status})`
|
|
463
|
+
} : a
|
|
464
|
+
)
|
|
465
|
+
);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const payload = await res.json();
|
|
469
|
+
setAttachments(
|
|
470
|
+
(prev) => prev.map((a) => a.id === id ? { ...a, status: "ready", payload } : a)
|
|
471
|
+
);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
const msg = err instanceof Error ? err.message : "Upload failed";
|
|
474
|
+
setAttachments(
|
|
475
|
+
(prev) => prev.map(
|
|
476
|
+
(a) => a.id === id ? { ...a, status: "error", errorMessage: msg } : a
|
|
477
|
+
)
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
[agentId, base]
|
|
482
|
+
);
|
|
483
|
+
const attach = react.useCallback(
|
|
484
|
+
(files) => {
|
|
485
|
+
const list = Array.from(files);
|
|
486
|
+
setAttachments((prev) => {
|
|
487
|
+
const remaining = maxFiles - prev.filter((a) => a.status !== "error").length;
|
|
488
|
+
if (remaining <= 0) return prev;
|
|
489
|
+
const toAdd = list.slice(0, remaining).map((f) => {
|
|
490
|
+
const id = makeId();
|
|
491
|
+
void uploadOne(f, id);
|
|
492
|
+
return {
|
|
493
|
+
id,
|
|
494
|
+
name: f.name,
|
|
495
|
+
mimeType: f.type,
|
|
496
|
+
sizeBytes: f.size,
|
|
497
|
+
status: "uploading"
|
|
498
|
+
};
|
|
499
|
+
});
|
|
500
|
+
return [...prev, ...toAdd];
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
[maxFiles, uploadOne]
|
|
504
|
+
);
|
|
505
|
+
const remove = react.useCallback((id) => {
|
|
506
|
+
setAttachments((prev) => prev.filter((a) => a.id !== id));
|
|
507
|
+
}, []);
|
|
508
|
+
const clear = react.useCallback(() => setAttachments([]), []);
|
|
509
|
+
const isUploading = attachments.some((a) => a.status === "uploading");
|
|
510
|
+
const readyPayloads = attachments.filter((a) => a.status === "ready" && a.payload).map((a) => a.payload);
|
|
511
|
+
return { attachments, attach, remove, clear, isUploading, readyPayloads };
|
|
331
512
|
}
|
|
332
513
|
function DefaultIcon() {
|
|
333
514
|
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", style: { width: 26, height: 26 }, children: [
|
|
@@ -430,10 +611,112 @@ function ChatHeader({
|
|
|
430
611
|
}
|
|
431
612
|
);
|
|
432
613
|
}
|
|
614
|
+
var cn2 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
615
|
+
function formatBytes(bytes) {
|
|
616
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
617
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
618
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
619
|
+
}
|
|
620
|
+
function ChipLeading({ a }) {
|
|
621
|
+
if (a.status === "uploading") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3 w-3 shrink-0 animate-spin" });
|
|
622
|
+
if (a.status === "error") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-3 w-3 shrink-0" });
|
|
623
|
+
const thumbSrc = a.payload?.url ?? a.payload?.base64;
|
|
624
|
+
if (a.mimeType.startsWith("image/") && thumbSrc) {
|
|
625
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
626
|
+
"img",
|
|
627
|
+
{
|
|
628
|
+
src: thumbSrc,
|
|
629
|
+
alt: a.name,
|
|
630
|
+
className: "h-6 w-6 rounded object-cover shrink-0 border border-border/40"
|
|
631
|
+
}
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (a.mimeType.startsWith("image/")) return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-6 w-6 rounded bg-muted-foreground/10 border border-border/30 shrink-0 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[8px] font-semibold text-muted-foreground/50", children: "IMG" }) });
|
|
635
|
+
if (a.mimeType === "application/pdf") return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "h-3 w-3 shrink-0 text-red-400" });
|
|
636
|
+
if (a.mimeType.includes("csv") || a.mimeType.includes("spreadsheet") || a.name.endsWith(".csv")) {
|
|
637
|
+
return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileSpreadsheet, { className: "h-3 w-3 shrink-0 text-emerald-500" });
|
|
638
|
+
}
|
|
639
|
+
return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "h-3 w-3 shrink-0" });
|
|
640
|
+
}
|
|
641
|
+
function AttachmentChips({ attachments, onRemove }) {
|
|
642
|
+
if (attachments.length === 0) return null;
|
|
643
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pb-1.5", children: attachments.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
644
|
+
"div",
|
|
645
|
+
{
|
|
646
|
+
className: cn2(
|
|
647
|
+
"flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[11px] max-w-[200px]",
|
|
648
|
+
a.status === "error" ? "border-red-200 bg-red-50 text-red-600 dark:border-red-800 dark:bg-red-950 dark:text-red-400" : "border-border bg-muted/60 text-muted-foreground"
|
|
649
|
+
),
|
|
650
|
+
children: [
|
|
651
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChipLeading, { a }),
|
|
652
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate leading-tight", children: a.status === "error" ? a.errorMessage ?? "Upload failed" : a.name }),
|
|
653
|
+
a.status === "ready" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-muted-foreground/50", children: formatBytes(a.sizeBytes) }),
|
|
654
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
655
|
+
"button",
|
|
656
|
+
{
|
|
657
|
+
type: "button",
|
|
658
|
+
onClick: () => onRemove(a.id),
|
|
659
|
+
className: "shrink-0 rounded-full p-0.5 hover:bg-foreground/10 transition-colors ml-0.5",
|
|
660
|
+
title: "Remove",
|
|
661
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-2.5 w-2.5" })
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
]
|
|
665
|
+
},
|
|
666
|
+
a.id
|
|
667
|
+
)) });
|
|
668
|
+
}
|
|
433
669
|
var Avatar2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Root, { style: { position: "relative", display: "flex", flexShrink: 0, overflow: "hidden", borderRadius: "9999px", ...style }, ...p });
|
|
434
670
|
var AvatarImage2 = ({ style, ...p }) => /* @__PURE__ */ jsxRuntime.jsx(AvatarPrimitive__namespace.Image, { style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", ...style }, ...p });
|
|
435
671
|
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 });
|
|
436
672
|
var ReactMarkdown = ReactMarkdownLib__default.default;
|
|
673
|
+
function SentAttachments({ attachments, contrastColor }) {
|
|
674
|
+
const images = attachments.filter((a) => a.contentType === "image");
|
|
675
|
+
const files = attachments.filter((a) => a.contentType !== "image");
|
|
676
|
+
const isDark = contrastColor === "#ffffff" || contrastColor === "#fff";
|
|
677
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5 w-full", children: [
|
|
678
|
+
images.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex gap-1.5 flex-wrap", images.length === 1 && ""), children: images.map((img) => {
|
|
679
|
+
const src = img.url ?? img.base64;
|
|
680
|
+
return src ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
681
|
+
"img",
|
|
682
|
+
{
|
|
683
|
+
src,
|
|
684
|
+
alt: img.name,
|
|
685
|
+
className: cn(
|
|
686
|
+
"rounded-xl object-cover border",
|
|
687
|
+
images.length === 1 ? "w-full max-h-48" : "h-20 w-20",
|
|
688
|
+
isDark ? "border-white/20" : "border-black/10"
|
|
689
|
+
)
|
|
690
|
+
},
|
|
691
|
+
img.id
|
|
692
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
693
|
+
"div",
|
|
694
|
+
{
|
|
695
|
+
className: cn(
|
|
696
|
+
"h-20 w-20 rounded-xl flex items-center justify-center text-[10px] font-medium",
|
|
697
|
+
isDark ? "bg-white/10 text-white/60" : "bg-black/10 text-black/40"
|
|
698
|
+
),
|
|
699
|
+
children: "IMG"
|
|
700
|
+
},
|
|
701
|
+
img.id
|
|
702
|
+
);
|
|
703
|
+
}) }),
|
|
704
|
+
files.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: files.map((f) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
705
|
+
"div",
|
|
706
|
+
{
|
|
707
|
+
className: cn(
|
|
708
|
+
"flex items-center gap-1 rounded-lg px-2 py-1 text-[11px]",
|
|
709
|
+
isDark ? "bg-white/15 text-white/80" : "bg-black/10 text-black/60"
|
|
710
|
+
),
|
|
711
|
+
children: [
|
|
712
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate max-w-[120px]", children: f.name }),
|
|
713
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-60 shrink-0", children: formatBytes(f.sizeBytes) })
|
|
714
|
+
]
|
|
715
|
+
},
|
|
716
|
+
f.id
|
|
717
|
+
)) })
|
|
718
|
+
] });
|
|
719
|
+
}
|
|
437
720
|
function ThinkingDots() {
|
|
438
721
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
439
722
|
/* @__PURE__ */ jsxRuntime.jsx("style", { children: `
|
|
@@ -501,83 +784,112 @@ function ReasoningBlock({ text }) {
|
|
|
501
784
|
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 })
|
|
502
785
|
] });
|
|
503
786
|
}
|
|
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: 100,
|
|
528
|
-
padding: "6px 14px",
|
|
529
|
-
border: "none",
|
|
530
|
-
backgroundColor: isSelected ? "var(--primary, #19191c)" : hovered && !inactive ? "rgba(0,0,0,0.14)" : "rgba(0,0,0,0.08)",
|
|
531
|
-
color: isSelected ? "var(--primary-foreground, #fff)" : "var(--foreground, #09090b)",
|
|
532
|
-
cursor: inactive ? "default" : "pointer",
|
|
533
|
-
opacity: isConsumed ? 0.3 : 1,
|
|
534
|
-
transition: "background-color 0.12s ease, opacity 0.12s ease",
|
|
535
|
-
fontWeight: isSelected ? 600 : 500,
|
|
536
|
-
boxShadow: "none"
|
|
537
|
-
},
|
|
538
|
-
children: opt.label
|
|
539
|
-
}
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
787
|
function PickerSelector({
|
|
543
788
|
part,
|
|
544
789
|
disabled,
|
|
545
790
|
onSelect
|
|
546
791
|
}) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
792
|
+
const count = part.options.length;
|
|
793
|
+
const mode = count <= 6 ? "pills" : count <= 20 ? "grid" : "list";
|
|
794
|
+
const [query, setQuery] = react.useState("");
|
|
795
|
+
const filtered = react.useMemo(() => {
|
|
796
|
+
if (!query.trim()) return part.options;
|
|
797
|
+
const q = query.toLowerCase();
|
|
798
|
+
return part.options.filter((o) => o.label.toLowerCase().includes(q));
|
|
799
|
+
}, [part.options, query]);
|
|
800
|
+
const isConsumed = !!part.selectedValue;
|
|
801
|
+
const handleClick = (opt) => {
|
|
802
|
+
if (!disabled && !isConsumed) onSelect(part.pickerId, part.paramName, opt.value, opt.label);
|
|
803
|
+
};
|
|
804
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
|
|
805
|
+
"flex flex-col gap-2.5 rounded-xl rounded-tl-none border bg-muted/60 p-3",
|
|
806
|
+
"border-border/50"
|
|
807
|
+
), children: [
|
|
808
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10.5px] font-semibold uppercase tracking-widest text-muted-foreground/70 leading-none", children: part.label }),
|
|
809
|
+
mode === "list" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
810
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground/50 pointer-events-none" }),
|
|
811
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
812
|
+
"input",
|
|
813
|
+
{
|
|
814
|
+
type: "text",
|
|
815
|
+
value: query,
|
|
816
|
+
onChange: (e) => setQuery(e.target.value),
|
|
817
|
+
placeholder: "Search\u2026",
|
|
818
|
+
disabled: disabled || isConsumed,
|
|
819
|
+
className: "w-full rounded-lg border bg-background pl-7 pr-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-ring/40 placeholder:text-muted-foreground/40 disabled:opacity-50"
|
|
820
|
+
}
|
|
821
|
+
)
|
|
822
|
+
] }),
|
|
823
|
+
mode === "pills" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: part.options.map((opt) => {
|
|
824
|
+
const sel = part.selectedValue === opt.value;
|
|
825
|
+
const faded = isConsumed && !sel;
|
|
826
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
827
|
+
"button",
|
|
828
|
+
{
|
|
829
|
+
disabled: disabled || isConsumed,
|
|
830
|
+
onClick: () => handleClick(opt),
|
|
831
|
+
className: cn(
|
|
832
|
+
"inline-flex items-center gap-1 rounded-full px-3.5 py-1.5 text-[12.5px] font-medium transition-all duration-150 select-none",
|
|
833
|
+
sel ? "bg-foreground text-background shadow-sm" : faded ? "bg-background/50 text-muted-foreground opacity-35 cursor-default" : "bg-background border border-border/70 text-foreground hover:border-foreground/30 hover:bg-background cursor-pointer",
|
|
834
|
+
disabled && !isConsumed && "opacity-60 pointer-events-none"
|
|
835
|
+
),
|
|
836
|
+
children: [
|
|
837
|
+
sel && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3 w-3 shrink-0" }),
|
|
838
|
+
opt.label
|
|
839
|
+
]
|
|
840
|
+
},
|
|
841
|
+
opt.value
|
|
842
|
+
);
|
|
843
|
+
}) }),
|
|
844
|
+
mode === "grid" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-1.5 max-h-[168px] overflow-y-auto pr-0.5 scrollbar-thin", children: part.options.map((opt) => {
|
|
845
|
+
const sel = part.selectedValue === opt.value;
|
|
846
|
+
const faded = isConsumed && !sel;
|
|
847
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
848
|
+
"button",
|
|
849
|
+
{
|
|
850
|
+
disabled: disabled || isConsumed,
|
|
851
|
+
onClick: () => handleClick(opt),
|
|
852
|
+
className: cn(
|
|
853
|
+
"flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[12px] font-medium text-left transition-all duration-150 select-none truncate",
|
|
854
|
+
sel ? "bg-foreground text-background" : faded ? "bg-background/40 text-muted-foreground opacity-35 cursor-default" : "bg-background border border-border/60 text-foreground hover:border-foreground/25 cursor-pointer",
|
|
855
|
+
disabled && !isConsumed && "opacity-60 pointer-events-none"
|
|
856
|
+
),
|
|
857
|
+
children: [
|
|
858
|
+
sel ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-3 w-3 shrink-0" }),
|
|
859
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: opt.label })
|
|
860
|
+
]
|
|
861
|
+
},
|
|
862
|
+
opt.value
|
|
863
|
+
);
|
|
864
|
+
}) }),
|
|
865
|
+
mode === "list" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-0.5 max-h-[180px] overflow-y-auto pr-0.5", children: [
|
|
866
|
+
filtered.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-3 text-center text-xs text-muted-foreground/50", children: "No results" }),
|
|
867
|
+
filtered.map((opt) => {
|
|
868
|
+
const sel = part.selectedValue === opt.value;
|
|
869
|
+
const faded = isConsumed && !sel;
|
|
870
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
871
|
+
"button",
|
|
872
|
+
{
|
|
873
|
+
disabled: disabled || isConsumed,
|
|
874
|
+
onClick: () => handleClick(opt),
|
|
875
|
+
className: cn(
|
|
876
|
+
"flex items-center gap-2 rounded-lg px-2.5 py-2 text-[12.5px] text-left transition-all duration-100 select-none",
|
|
877
|
+
sel ? "bg-foreground text-background font-medium" : faded ? "opacity-30 cursor-default" : "hover:bg-background text-foreground cursor-pointer",
|
|
878
|
+
disabled && !isConsumed && "opacity-60 pointer-events-none"
|
|
879
|
+
),
|
|
880
|
+
children: [
|
|
881
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
|
|
882
|
+
"flex h-4 w-4 shrink-0 items-center justify-center rounded-full border text-[10px]",
|
|
883
|
+
sel ? "border-background bg-background/20" : "border-border/50"
|
|
884
|
+
), children: sel && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-2.5 w-2.5" }) }),
|
|
885
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: opt.label })
|
|
886
|
+
]
|
|
887
|
+
},
|
|
888
|
+
opt.value
|
|
889
|
+
);
|
|
890
|
+
})
|
|
891
|
+
] })
|
|
892
|
+
] });
|
|
581
893
|
}
|
|
582
894
|
function MessageBubble({
|
|
583
895
|
message,
|
|
@@ -595,12 +907,15 @@ function MessageBubble({
|
|
|
595
907
|
const pickerParts = message.parts.filter((p) => p.type === "picker");
|
|
596
908
|
const contrastColor = getContrastColor(userColor);
|
|
597
909
|
if (isUser) {
|
|
598
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.
|
|
910
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
599
911
|
"div",
|
|
600
912
|
{
|
|
601
|
-
className: "max-w-[78%] rounded-2xl rounded-tr-sm px-4 py-2.5 text-sm leading-relaxed",
|
|
913
|
+
className: "max-w-[78%] rounded-2xl rounded-tr-sm px-4 py-2.5 text-sm leading-relaxed flex flex-col gap-2",
|
|
602
914
|
style: { backgroundColor: userColor, color: contrastColor },
|
|
603
|
-
children:
|
|
915
|
+
children: [
|
|
916
|
+
message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(SentAttachments, { attachments: message.attachments, contrastColor }),
|
|
917
|
+
textPart?.text && /* @__PURE__ */ jsxRuntime.jsx("span", { children: textPart.text })
|
|
918
|
+
]
|
|
604
919
|
}
|
|
605
920
|
) });
|
|
606
921
|
}
|
|
@@ -705,7 +1020,7 @@ function ChatMessages({
|
|
|
705
1020
|
/* @__PURE__ */ jsxRuntime.jsx("div", { ref: bottomRef })
|
|
706
1021
|
] });
|
|
707
1022
|
}
|
|
708
|
-
var
|
|
1023
|
+
var cn3 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
709
1024
|
function ChatInput({
|
|
710
1025
|
input,
|
|
711
1026
|
setInput,
|
|
@@ -714,8 +1029,18 @@ function ChatInput({
|
|
|
714
1029
|
placeholder,
|
|
715
1030
|
accentColor,
|
|
716
1031
|
canRegenerate = false,
|
|
717
|
-
onRegenerate
|
|
1032
|
+
onRegenerate,
|
|
1033
|
+
voiceState,
|
|
1034
|
+
onVoiceStart,
|
|
1035
|
+
onVoiceStop,
|
|
1036
|
+
attachments,
|
|
1037
|
+
onAttach,
|
|
1038
|
+
onRemoveAttachment,
|
|
1039
|
+
isUploading = false
|
|
718
1040
|
}) {
|
|
1041
|
+
const hasVoice = onVoiceStart !== void 0;
|
|
1042
|
+
const hasAttachments = onAttach !== void 0;
|
|
1043
|
+
const fileInputRef = react.useRef(null);
|
|
719
1044
|
const textareaRef = react.useRef(null);
|
|
720
1045
|
react.useEffect(() => {
|
|
721
1046
|
if (!input && textareaRef.current) {
|
|
@@ -747,39 +1072,102 @@ function ChatInput({
|
|
|
747
1072
|
]
|
|
748
1073
|
}
|
|
749
1074
|
),
|
|
750
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex
|
|
751
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
752
|
-
|
|
1075
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col rounded-2xl border bg-background shadow-sm focus-within:ring-1 focus-within:ring-ring/40 transition-shadow", children: [
|
|
1076
|
+
hasAttachments && attachments && attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-2 pt-2", children: /* @__PURE__ */ jsxRuntime.jsx(AttachmentChips, { attachments, onRemove: (id) => onRemoveAttachment?.(id) }) }),
|
|
1077
|
+
hasAttachments && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1078
|
+
"input",
|
|
753
1079
|
{
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
value: input,
|
|
1080
|
+
ref: fileInputRef,
|
|
1081
|
+
type: "file",
|
|
1082
|
+
multiple: true,
|
|
1083
|
+
accept: ".csv,.txt,.tsv,.pdf,text/plain,text/csv,application/pdf,image/jpeg,image/png,image/webp,image/gif",
|
|
1084
|
+
className: "hidden",
|
|
760
1085
|
onChange: (e) => {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
disabled: streaming,
|
|
767
|
-
autoFocus: true
|
|
1086
|
+
if (e.target.files?.length) {
|
|
1087
|
+
onAttach(e.target.files);
|
|
1088
|
+
e.target.value = "";
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
768
1091
|
}
|
|
769
1092
|
),
|
|
770
|
-
/* @__PURE__ */ jsxRuntime.
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1093
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-end gap-2 px-3 py-2", children: [
|
|
1094
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1095
|
+
"textarea",
|
|
1096
|
+
{
|
|
1097
|
+
id: "wallavi-chat-input",
|
|
1098
|
+
ref: textareaRef,
|
|
1099
|
+
rows: 1,
|
|
1100
|
+
className: "flex-1 resize-none bg-transparent text-sm outline-none placeholder:text-muted-foreground/50 leading-relaxed max-h-32 overflow-y-auto py-0.5",
|
|
1101
|
+
placeholder: placeholder ?? "Send a message\u2026",
|
|
1102
|
+
value: input,
|
|
1103
|
+
onChange: (e) => {
|
|
1104
|
+
setInput(e.target.value);
|
|
1105
|
+
e.target.style.height = "auto";
|
|
1106
|
+
e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
|
|
1107
|
+
},
|
|
1108
|
+
onKeyDown: handleKeyDown,
|
|
1109
|
+
onPaste: (e) => {
|
|
1110
|
+
if (!hasAttachments || !onAttach) return;
|
|
1111
|
+
const files = Array.from(e.clipboardData?.files ?? []).filter(
|
|
1112
|
+
(f) => f.type.startsWith("image/")
|
|
1113
|
+
);
|
|
1114
|
+
if (files.length > 0) {
|
|
1115
|
+
e.preventDefault();
|
|
1116
|
+
const dt = new DataTransfer();
|
|
1117
|
+
files.forEach((f) => dt.items.add(f));
|
|
1118
|
+
onAttach(dt.files);
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
disabled: streaming || voiceState === "recording" || voiceState === "transcribing",
|
|
1122
|
+
autoFocus: true
|
|
1123
|
+
}
|
|
1124
|
+
),
|
|
1125
|
+
hasVoice && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1126
|
+
"button",
|
|
1127
|
+
{
|
|
1128
|
+
type: "button",
|
|
1129
|
+
onClick: voiceState === "recording" ? onVoiceStop : onVoiceStart,
|
|
1130
|
+
disabled: streaming || voiceState === "transcribing",
|
|
1131
|
+
title: voiceState === "recording" ? "Stop recording" : "Record voice message",
|
|
1132
|
+
className: cn3(
|
|
1133
|
+
"h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
|
|
1134
|
+
voiceState === "recording" && "animate-pulse",
|
|
1135
|
+
voiceState === "error" ? "text-red-500 opacity-80" : "text-muted-foreground hover:text-foreground",
|
|
1136
|
+
(streaming || voiceState === "transcribing") && "opacity-40 pointer-events-none"
|
|
1137
|
+
),
|
|
1138
|
+
style: voiceState === "recording" ? { color: accentColor } : void 0,
|
|
1139
|
+
children: voiceState === "transcribing" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : voiceState === "recording" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Square, { className: "h-3.5 w-3.5 fill-current" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { className: "h-3.5 w-3.5" })
|
|
1140
|
+
}
|
|
1141
|
+
),
|
|
1142
|
+
hasAttachments && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1143
|
+
"button",
|
|
1144
|
+
{
|
|
1145
|
+
type: "button",
|
|
1146
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1147
|
+
disabled: streaming || isUploading,
|
|
1148
|
+
title: "Attach file (CSV, image\u2026)",
|
|
1149
|
+
className: cn3(
|
|
1150
|
+
"h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
|
|
1151
|
+
"text-muted-foreground hover:text-foreground",
|
|
1152
|
+
(streaming || isUploading) && "opacity-40 pointer-events-none"
|
|
1153
|
+
),
|
|
1154
|
+
children: isUploading ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Paperclip, { className: "h-3.5 w-3.5" })
|
|
1155
|
+
}
|
|
1156
|
+
),
|
|
1157
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1158
|
+
"button",
|
|
1159
|
+
{
|
|
1160
|
+
onClick: onSend,
|
|
1161
|
+
disabled: streaming || !hasText || voiceState === "recording" || voiceState === "transcribing",
|
|
1162
|
+
className: cn3(
|
|
1163
|
+
"h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
|
|
1164
|
+
hasText || streaming ? "opacity-100 shadow-sm" : "opacity-30"
|
|
1165
|
+
),
|
|
1166
|
+
style: hasText || streaming ? { backgroundColor: accentColor, color: getContrastColor(accentColor) } : { backgroundColor: "transparent", color: "currentColor" },
|
|
1167
|
+
children: streaming ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUp, { className: "h-3.5 w-3.5" })
|
|
1168
|
+
}
|
|
1169
|
+
)
|
|
1170
|
+
] })
|
|
783
1171
|
] })
|
|
784
1172
|
] });
|
|
785
1173
|
}
|
|
@@ -805,6 +1193,9 @@ function ChatWidget({
|
|
|
805
1193
|
source = "playground",
|
|
806
1194
|
userContext,
|
|
807
1195
|
playgroundOverrides,
|
|
1196
|
+
enableVoice = false,
|
|
1197
|
+
voiceAutoSend = false,
|
|
1198
|
+
enableAttachments = false,
|
|
808
1199
|
className,
|
|
809
1200
|
onClose,
|
|
810
1201
|
onReset,
|
|
@@ -812,6 +1203,48 @@ function ChatWidget({
|
|
|
812
1203
|
expanded
|
|
813
1204
|
}) {
|
|
814
1205
|
const chat = useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides });
|
|
1206
|
+
const voice = useVoice({
|
|
1207
|
+
agentId,
|
|
1208
|
+
onTranscript: (text) => {
|
|
1209
|
+
if (voiceAutoSend) {
|
|
1210
|
+
void chat.send(text);
|
|
1211
|
+
} else {
|
|
1212
|
+
chat.setInput(text);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
const attachmentHook = useAttachments({ agentId });
|
|
1217
|
+
const [isDragOver, setIsDragOver] = react.useState(false);
|
|
1218
|
+
const handleDragOver = react.useCallback((e) => {
|
|
1219
|
+
if (!enableAttachments) return;
|
|
1220
|
+
e.preventDefault();
|
|
1221
|
+
e.stopPropagation();
|
|
1222
|
+
if (e.dataTransfer.types.includes("Files")) setIsDragOver(true);
|
|
1223
|
+
}, [enableAttachments]);
|
|
1224
|
+
const handleDragLeave = react.useCallback((e) => {
|
|
1225
|
+
if (!enableAttachments) return;
|
|
1226
|
+
e.preventDefault();
|
|
1227
|
+
e.stopPropagation();
|
|
1228
|
+
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
1229
|
+
setIsDragOver(false);
|
|
1230
|
+
}
|
|
1231
|
+
}, [enableAttachments]);
|
|
1232
|
+
const handleDrop = react.useCallback((e) => {
|
|
1233
|
+
if (!enableAttachments) return;
|
|
1234
|
+
e.preventDefault();
|
|
1235
|
+
e.stopPropagation();
|
|
1236
|
+
setIsDragOver(false);
|
|
1237
|
+
const files = e.dataTransfer.files;
|
|
1238
|
+
if (files.length > 0) attachmentHook.attach(files);
|
|
1239
|
+
}, [enableAttachments, attachmentHook]);
|
|
1240
|
+
const handleSend = () => {
|
|
1241
|
+
const payloads = attachmentHook.readyPayloads;
|
|
1242
|
+
if (payloads.length > 0) {
|
|
1243
|
+
chat.queueAttachments(payloads);
|
|
1244
|
+
attachmentHook.clear();
|
|
1245
|
+
}
|
|
1246
|
+
void chat.send();
|
|
1247
|
+
};
|
|
815
1248
|
const canRegenerate = regenerateMessage && chat.messages.length > 0 && chat.messages.at(-1)?.role === "assistant" && !chat.streaming;
|
|
816
1249
|
const title = displayName || agentName;
|
|
817
1250
|
const headerBg = userMessageColor;
|
|
@@ -824,11 +1257,21 @@ function ChatWidget({
|
|
|
824
1257
|
"div",
|
|
825
1258
|
{
|
|
826
1259
|
className: cn(
|
|
827
|
-
"flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background h-full",
|
|
1260
|
+
"flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background h-full relative",
|
|
1261
|
+
isDragOver && "ring-2 ring-inset ring-primary/60",
|
|
828
1262
|
className
|
|
829
1263
|
),
|
|
830
1264
|
style: { colorScheme: theme },
|
|
1265
|
+
onDragOver: handleDragOver,
|
|
1266
|
+
onDragEnter: handleDragOver,
|
|
1267
|
+
onDragLeave: handleDragLeave,
|
|
1268
|
+
onDrop: handleDrop,
|
|
831
1269
|
children: [
|
|
1270
|
+
isDragOver && enableAttachments && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 z-50 flex flex-col items-center justify-center gap-2 rounded-2xl bg-background/90 backdrop-blur-sm pointer-events-none", children: [
|
|
1271
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.UploadCloud, { className: "h-8 w-8 text-primary/70" }),
|
|
1272
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-foreground/70", children: "Drop files to attach" }),
|
|
1273
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: "CSV, TXT, PDF, JPG, PNG, WebP" })
|
|
1274
|
+
] }),
|
|
832
1275
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
833
1276
|
ChatHeader,
|
|
834
1277
|
{
|
|
@@ -862,12 +1305,23 @@ function ChatWidget({
|
|
|
862
1305
|
{
|
|
863
1306
|
input: chat.input,
|
|
864
1307
|
setInput: chat.setInput,
|
|
865
|
-
onSend:
|
|
1308
|
+
onSend: handleSend,
|
|
866
1309
|
streaming: chat.streaming,
|
|
867
1310
|
placeholder: messagePlaceholder,
|
|
868
1311
|
accentColor: userMessageColor,
|
|
869
1312
|
canRegenerate: !!canRegenerate,
|
|
870
|
-
onRegenerate: () => void chat.regenerate()
|
|
1313
|
+
onRegenerate: () => void chat.regenerate(),
|
|
1314
|
+
...enableVoice && voice.isSupported ? {
|
|
1315
|
+
voiceState: voice.voiceState,
|
|
1316
|
+
onVoiceStart: () => void voice.start(),
|
|
1317
|
+
onVoiceStop: voice.stop
|
|
1318
|
+
} : {},
|
|
1319
|
+
...enableAttachments ? {
|
|
1320
|
+
attachments: attachmentHook.attachments,
|
|
1321
|
+
onAttach: attachmentHook.attach,
|
|
1322
|
+
onRemoveAttachment: attachmentHook.remove,
|
|
1323
|
+
isUploading: attachmentHook.isUploading
|
|
1324
|
+
} : {}
|
|
871
1325
|
}
|
|
872
1326
|
),
|
|
873
1327
|
watermark && /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "shrink-0 flex items-center justify-center gap-1.5 bg-muted/50 py-1.5 border-t", children: [
|
|
@@ -1137,4 +1591,6 @@ exports.BubbleWidget = BubbleWidget;
|
|
|
1137
1591
|
exports.ChatWidget = ChatWidget;
|
|
1138
1592
|
exports.formatToolName = formatToolName;
|
|
1139
1593
|
exports.getContrastColor = getContrastColor;
|
|
1594
|
+
exports.useAttachments = useAttachments;
|
|
1140
1595
|
exports.useChat = useChat;
|
|
1596
|
+
exports.useVoice = useVoice;
|