@wallavi/widget 1.5.1 → 1.6.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 +84 -2
- package/dist/index.d.ts +84 -2
- package/dist/index.js +481 -109
- package/dist/index.mjs +482 -112
- 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,6 +248,7 @@ 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();
|
|
@@ -261,8 +263,10 @@ function useChat({
|
|
|
261
263
|
const assistantMsgId = newId();
|
|
262
264
|
streamingMsgIdRef.current = assistantMsgId;
|
|
263
265
|
setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
|
|
266
|
+
const attachments = pendingAttachmentsRef.current.length > 0 ? [...pendingAttachmentsRef.current] : void 0;
|
|
267
|
+
pendingAttachmentsRef.current = [];
|
|
264
268
|
try {
|
|
265
|
-
await fetchAndStream({ input: userInput, msgId: assistantMsgId });
|
|
269
|
+
await fetchAndStream({ input: userInput, msgId: assistantMsgId, attachments });
|
|
266
270
|
} catch {
|
|
267
271
|
setMessages((prev) => {
|
|
268
272
|
const idx = prev.findIndex((m) => m.id === assistantMsgId);
|
|
@@ -312,8 +316,11 @@ function useChat({
|
|
|
312
316
|
setStreaming(false);
|
|
313
317
|
streamingMsgIdRef.current = null;
|
|
314
318
|
},
|
|
315
|
-
[streaming, fetchAndStream]
|
|
319
|
+
[input, streaming, fetchAndStream]
|
|
316
320
|
);
|
|
321
|
+
const queueAttachments = react.useCallback((payloads) => {
|
|
322
|
+
pendingAttachmentsRef.current = payloads;
|
|
323
|
+
}, []);
|
|
317
324
|
const regenerate = react.useCallback(async () => {
|
|
318
325
|
if (streaming) return;
|
|
319
326
|
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
@@ -327,7 +334,176 @@ function useChat({
|
|
|
327
334
|
});
|
|
328
335
|
await send(lastText);
|
|
329
336
|
}, [streaming, messages, send]);
|
|
330
|
-
return { messages, input, setInput, streaming, threadId, send, regenerate, reset, selectPickerOption };
|
|
337
|
+
return { messages, input, setInput, streaming, threadId, send, queueAttachments, regenerate, reset, selectPickerOption };
|
|
338
|
+
}
|
|
339
|
+
function getPreferredMimeType() {
|
|
340
|
+
if (typeof MediaRecorder === "undefined") return "";
|
|
341
|
+
const candidates = [
|
|
342
|
+
"audio/webm;codecs=opus",
|
|
343
|
+
"audio/webm",
|
|
344
|
+
"audio/ogg;codecs=opus",
|
|
345
|
+
"audio/ogg",
|
|
346
|
+
"audio/mp4"
|
|
347
|
+
];
|
|
348
|
+
return candidates.find((t) => MediaRecorder.isTypeSupported(t)) ?? "";
|
|
349
|
+
}
|
|
350
|
+
function mimeTypeToExtension(mimeType) {
|
|
351
|
+
if (mimeType.includes("ogg")) return "ogg";
|
|
352
|
+
if (mimeType.includes("mp4")) return "mp4";
|
|
353
|
+
return "webm";
|
|
354
|
+
}
|
|
355
|
+
var DEFAULT_API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
|
|
356
|
+
function useVoice({ agentId, apiUrl, onTranscript, onError }) {
|
|
357
|
+
const [voiceState, setVoiceState] = react.useState("idle");
|
|
358
|
+
const recorderRef = react.useRef(null);
|
|
359
|
+
const chunksRef = react.useRef([]);
|
|
360
|
+
const streamRef = react.useRef(null);
|
|
361
|
+
const errorTimerRef = react.useRef(null);
|
|
362
|
+
const isSupported = typeof window !== "undefined" && typeof MediaRecorder !== "undefined" && !!navigator?.mediaDevices?.getUserMedia;
|
|
363
|
+
const base = apiUrl ?? DEFAULT_API_URL;
|
|
364
|
+
const transcribeBlob = react.useCallback(
|
|
365
|
+
async (blob, mimeType) => {
|
|
366
|
+
setVoiceState("transcribing");
|
|
367
|
+
try {
|
|
368
|
+
const ext = mimeTypeToExtension(mimeType);
|
|
369
|
+
const form = new FormData();
|
|
370
|
+
form.append("audio", blob, `recording.${ext}`);
|
|
371
|
+
form.append("agentId", agentId);
|
|
372
|
+
const res = await fetch(`${base}/api/chat/transcribe`, { method: "POST", body: form });
|
|
373
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
374
|
+
const data = await res.json();
|
|
375
|
+
if (!data.text?.trim()) throw new Error(data.error ?? "Empty transcript");
|
|
376
|
+
onTranscript(data.text.trim());
|
|
377
|
+
setVoiceState("idle");
|
|
378
|
+
} catch (err) {
|
|
379
|
+
const msg = err instanceof Error ? err.message : "Transcription failed";
|
|
380
|
+
onError?.(msg);
|
|
381
|
+
setVoiceState("error");
|
|
382
|
+
errorTimerRef.current = setTimeout(() => setVoiceState("idle"), 2500);
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
[agentId, base, onTranscript, onError]
|
|
386
|
+
);
|
|
387
|
+
const start = react.useCallback(async () => {
|
|
388
|
+
if (!isSupported || voiceState !== "idle") return;
|
|
389
|
+
try {
|
|
390
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
391
|
+
streamRef.current = stream;
|
|
392
|
+
const mimeType = getPreferredMimeType();
|
|
393
|
+
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : void 0);
|
|
394
|
+
chunksRef.current = [];
|
|
395
|
+
recorder.ondataavailable = (e) => {
|
|
396
|
+
if (e.data.size > 0) chunksRef.current.push(e.data);
|
|
397
|
+
};
|
|
398
|
+
recorder.onstop = async () => {
|
|
399
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
400
|
+
const blob = new Blob(chunksRef.current, { type: mimeType || "audio/webm" });
|
|
401
|
+
if (blob.size === 0) {
|
|
402
|
+
onError?.("Recording was empty \u2014 please try again.");
|
|
403
|
+
setVoiceState("idle");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
await transcribeBlob(blob, mimeType || "audio/webm");
|
|
407
|
+
};
|
|
408
|
+
recorder.start(250);
|
|
409
|
+
recorderRef.current = recorder;
|
|
410
|
+
setVoiceState("recording");
|
|
411
|
+
} catch (err) {
|
|
412
|
+
const msg = err instanceof Error ? err.message : "Microphone access denied";
|
|
413
|
+
onError?.(msg);
|
|
414
|
+
setVoiceState("idle");
|
|
415
|
+
}
|
|
416
|
+
}, [isSupported, voiceState, transcribeBlob, onError]);
|
|
417
|
+
const stop = react.useCallback(() => {
|
|
418
|
+
if (recorderRef.current?.state === "recording") {
|
|
419
|
+
recorderRef.current.stop();
|
|
420
|
+
recorderRef.current = null;
|
|
421
|
+
}
|
|
422
|
+
}, []);
|
|
423
|
+
react.useEffect(() => {
|
|
424
|
+
return () => {
|
|
425
|
+
if (errorTimerRef.current) clearTimeout(errorTimerRef.current);
|
|
426
|
+
if (recorderRef.current?.state === "recording") recorderRef.current.stop();
|
|
427
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
428
|
+
};
|
|
429
|
+
}, []);
|
|
430
|
+
return { voiceState, isSupported, start, stop };
|
|
431
|
+
}
|
|
432
|
+
var DEFAULT_API_URL2 = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
|
|
433
|
+
function makeId() {
|
|
434
|
+
return `att_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
435
|
+
}
|
|
436
|
+
function useAttachments({
|
|
437
|
+
agentId,
|
|
438
|
+
apiUrl,
|
|
439
|
+
maxFiles = 5
|
|
440
|
+
}) {
|
|
441
|
+
const [attachments, setAttachments] = react.useState([]);
|
|
442
|
+
const base = apiUrl ?? DEFAULT_API_URL2;
|
|
443
|
+
const uploadOne = react.useCallback(
|
|
444
|
+
async (file, id) => {
|
|
445
|
+
const form = new FormData();
|
|
446
|
+
form.append("file", file);
|
|
447
|
+
form.append("agentId", agentId);
|
|
448
|
+
try {
|
|
449
|
+
const res = await fetch(`${base}/api/chat/upload`, { method: "POST", body: form });
|
|
450
|
+
if (!res.ok) {
|
|
451
|
+
const data = await res.json().catch(() => ({}));
|
|
452
|
+
setAttachments(
|
|
453
|
+
(prev) => prev.map(
|
|
454
|
+
(a) => a.id === id ? {
|
|
455
|
+
...a,
|
|
456
|
+
status: "error",
|
|
457
|
+
errorMessage: data.error ?? `Upload failed (${res.status})`
|
|
458
|
+
} : a
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const payload = await res.json();
|
|
464
|
+
setAttachments(
|
|
465
|
+
(prev) => prev.map((a) => a.id === id ? { ...a, status: "ready", payload } : a)
|
|
466
|
+
);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
const msg = err instanceof Error ? err.message : "Upload failed";
|
|
469
|
+
setAttachments(
|
|
470
|
+
(prev) => prev.map(
|
|
471
|
+
(a) => a.id === id ? { ...a, status: "error", errorMessage: msg } : a
|
|
472
|
+
)
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
[agentId, base]
|
|
477
|
+
);
|
|
478
|
+
const attach = react.useCallback(
|
|
479
|
+
(files) => {
|
|
480
|
+
const list = Array.from(files);
|
|
481
|
+
setAttachments((prev) => {
|
|
482
|
+
const remaining = maxFiles - prev.filter((a) => a.status !== "error").length;
|
|
483
|
+
if (remaining <= 0) return prev;
|
|
484
|
+
const toAdd = list.slice(0, remaining).map((f) => {
|
|
485
|
+
const id = makeId();
|
|
486
|
+
void uploadOne(f, id);
|
|
487
|
+
return {
|
|
488
|
+
id,
|
|
489
|
+
name: f.name,
|
|
490
|
+
mimeType: f.type,
|
|
491
|
+
sizeBytes: f.size,
|
|
492
|
+
status: "uploading"
|
|
493
|
+
};
|
|
494
|
+
});
|
|
495
|
+
return [...prev, ...toAdd];
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
[maxFiles, uploadOne]
|
|
499
|
+
);
|
|
500
|
+
const remove = react.useCallback((id) => {
|
|
501
|
+
setAttachments((prev) => prev.filter((a) => a.id !== id));
|
|
502
|
+
}, []);
|
|
503
|
+
const clear = react.useCallback(() => setAttachments([]), []);
|
|
504
|
+
const isUploading = attachments.some((a) => a.status === "uploading");
|
|
505
|
+
const readyPayloads = attachments.filter((a) => a.status === "ready" && a.payload).map((a) => a.payload);
|
|
506
|
+
return { attachments, attach, remove, clear, isUploading, readyPayloads };
|
|
331
507
|
}
|
|
332
508
|
function DefaultIcon() {
|
|
333
509
|
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", style: { width: 26, height: 26 }, children: [
|
|
@@ -501,83 +677,112 @@ function ReasoningBlock({ text }) {
|
|
|
501
677
|
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
678
|
] });
|
|
503
679
|
}
|
|
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
680
|
function PickerSelector({
|
|
543
681
|
part,
|
|
544
682
|
disabled,
|
|
545
683
|
onSelect
|
|
546
684
|
}) {
|
|
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
|
-
|
|
685
|
+
const count = part.options.length;
|
|
686
|
+
const mode = count <= 6 ? "pills" : count <= 20 ? "grid" : "list";
|
|
687
|
+
const [query, setQuery] = react.useState("");
|
|
688
|
+
const filtered = react.useMemo(() => {
|
|
689
|
+
if (!query.trim()) return part.options;
|
|
690
|
+
const q = query.toLowerCase();
|
|
691
|
+
return part.options.filter((o) => o.label.toLowerCase().includes(q));
|
|
692
|
+
}, [part.options, query]);
|
|
693
|
+
const isConsumed = !!part.selectedValue;
|
|
694
|
+
const handleClick = (opt) => {
|
|
695
|
+
if (!disabled && !isConsumed) onSelect(part.pickerId, part.paramName, opt.value, opt.label);
|
|
696
|
+
};
|
|
697
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(
|
|
698
|
+
"flex flex-col gap-2.5 rounded-xl rounded-tl-none border bg-muted/60 p-3",
|
|
699
|
+
"border-border/50"
|
|
700
|
+
), children: [
|
|
701
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10.5px] font-semibold uppercase tracking-widest text-muted-foreground/70 leading-none", children: part.label }),
|
|
702
|
+
mode === "list" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
703
|
+
/* @__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" }),
|
|
704
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
705
|
+
"input",
|
|
706
|
+
{
|
|
707
|
+
type: "text",
|
|
708
|
+
value: query,
|
|
709
|
+
onChange: (e) => setQuery(e.target.value),
|
|
710
|
+
placeholder: "Search\u2026",
|
|
711
|
+
disabled: disabled || isConsumed,
|
|
712
|
+
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"
|
|
713
|
+
}
|
|
714
|
+
)
|
|
715
|
+
] }),
|
|
716
|
+
mode === "pills" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: part.options.map((opt) => {
|
|
717
|
+
const sel = part.selectedValue === opt.value;
|
|
718
|
+
const faded = isConsumed && !sel;
|
|
719
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
720
|
+
"button",
|
|
721
|
+
{
|
|
722
|
+
disabled: disabled || isConsumed,
|
|
723
|
+
onClick: () => handleClick(opt),
|
|
724
|
+
className: cn(
|
|
725
|
+
"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",
|
|
726
|
+
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",
|
|
727
|
+
disabled && !isConsumed && "opacity-60 pointer-events-none"
|
|
728
|
+
),
|
|
729
|
+
children: [
|
|
730
|
+
sel && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3 w-3 shrink-0" }),
|
|
731
|
+
opt.label
|
|
732
|
+
]
|
|
733
|
+
},
|
|
734
|
+
opt.value
|
|
735
|
+
);
|
|
736
|
+
}) }),
|
|
737
|
+
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) => {
|
|
738
|
+
const sel = part.selectedValue === opt.value;
|
|
739
|
+
const faded = isConsumed && !sel;
|
|
740
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
741
|
+
"button",
|
|
742
|
+
{
|
|
743
|
+
disabled: disabled || isConsumed,
|
|
744
|
+
onClick: () => handleClick(opt),
|
|
745
|
+
className: cn(
|
|
746
|
+
"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",
|
|
747
|
+
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",
|
|
748
|
+
disabled && !isConsumed && "opacity-60 pointer-events-none"
|
|
749
|
+
),
|
|
750
|
+
children: [
|
|
751
|
+
sel ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-3 w-3 shrink-0" }),
|
|
752
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: opt.label })
|
|
753
|
+
]
|
|
754
|
+
},
|
|
755
|
+
opt.value
|
|
756
|
+
);
|
|
757
|
+
}) }),
|
|
758
|
+
mode === "list" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-0.5 max-h-[180px] overflow-y-auto pr-0.5", children: [
|
|
759
|
+
filtered.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-3 text-center text-xs text-muted-foreground/50", children: "No results" }),
|
|
760
|
+
filtered.map((opt) => {
|
|
761
|
+
const sel = part.selectedValue === opt.value;
|
|
762
|
+
const faded = isConsumed && !sel;
|
|
763
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
764
|
+
"button",
|
|
765
|
+
{
|
|
766
|
+
disabled: disabled || isConsumed,
|
|
767
|
+
onClick: () => handleClick(opt),
|
|
768
|
+
className: cn(
|
|
769
|
+
"flex items-center gap-2 rounded-lg px-2.5 py-2 text-[12.5px] text-left transition-all duration-100 select-none",
|
|
770
|
+
sel ? "bg-foreground text-background font-medium" : faded ? "opacity-30 cursor-default" : "hover:bg-background text-foreground cursor-pointer",
|
|
771
|
+
disabled && !isConsumed && "opacity-60 pointer-events-none"
|
|
772
|
+
),
|
|
773
|
+
children: [
|
|
774
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
|
|
775
|
+
"flex h-4 w-4 shrink-0 items-center justify-center rounded-full border text-[10px]",
|
|
776
|
+
sel ? "border-background bg-background/20" : "border-border/50"
|
|
777
|
+
), children: sel && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-2.5 w-2.5" }) }),
|
|
778
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: opt.label })
|
|
779
|
+
]
|
|
780
|
+
},
|
|
781
|
+
opt.value
|
|
782
|
+
);
|
|
783
|
+
})
|
|
784
|
+
] })
|
|
785
|
+
] });
|
|
581
786
|
}
|
|
582
787
|
function MessageBubble({
|
|
583
788
|
message,
|
|
@@ -706,6 +911,44 @@ function ChatMessages({
|
|
|
706
911
|
] });
|
|
707
912
|
}
|
|
708
913
|
var cn2 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
914
|
+
function formatBytes(bytes) {
|
|
915
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
916
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
917
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
918
|
+
}
|
|
919
|
+
function FileIcon({ mimeType }) {
|
|
920
|
+
if (mimeType.startsWith("image/")) return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImageIcon, { className: "h-3 w-3 shrink-0" });
|
|
921
|
+
return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "h-3 w-3 shrink-0" });
|
|
922
|
+
}
|
|
923
|
+
function AttachmentChips({ attachments, onRemove }) {
|
|
924
|
+
if (attachments.length === 0) return null;
|
|
925
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pb-1.5", children: attachments.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
926
|
+
"div",
|
|
927
|
+
{
|
|
928
|
+
className: cn2(
|
|
929
|
+
"flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[11px] max-w-[180px]",
|
|
930
|
+
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"
|
|
931
|
+
),
|
|
932
|
+
children: [
|
|
933
|
+
a.status === "uploading" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-3 w-3 shrink-0 animate-spin" }) : a.status === "error" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsxRuntime.jsx(FileIcon, { mimeType: a.mimeType }),
|
|
934
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate leading-tight", children: a.status === "error" ? a.errorMessage ?? "Upload failed" : a.name }),
|
|
935
|
+
a.status === "ready" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-muted-foreground/60", children: formatBytes(a.sizeBytes) }),
|
|
936
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
937
|
+
"button",
|
|
938
|
+
{
|
|
939
|
+
type: "button",
|
|
940
|
+
onClick: () => onRemove(a.id),
|
|
941
|
+
className: "shrink-0 rounded-full p-0.5 hover:bg-foreground/10 transition-colors",
|
|
942
|
+
title: "Remove attachment",
|
|
943
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-2.5 w-2.5" })
|
|
944
|
+
}
|
|
945
|
+
)
|
|
946
|
+
]
|
|
947
|
+
},
|
|
948
|
+
a.id
|
|
949
|
+
)) });
|
|
950
|
+
}
|
|
951
|
+
var cn3 = (...inputs) => tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
709
952
|
function ChatInput({
|
|
710
953
|
input,
|
|
711
954
|
setInput,
|
|
@@ -714,8 +957,18 @@ function ChatInput({
|
|
|
714
957
|
placeholder,
|
|
715
958
|
accentColor,
|
|
716
959
|
canRegenerate = false,
|
|
717
|
-
onRegenerate
|
|
960
|
+
onRegenerate,
|
|
961
|
+
voiceState,
|
|
962
|
+
onVoiceStart,
|
|
963
|
+
onVoiceStop,
|
|
964
|
+
attachments,
|
|
965
|
+
onAttach,
|
|
966
|
+
onRemoveAttachment,
|
|
967
|
+
isUploading = false
|
|
718
968
|
}) {
|
|
969
|
+
const hasVoice = onVoiceStart !== void 0;
|
|
970
|
+
const hasAttachments = onAttach !== void 0;
|
|
971
|
+
const fileInputRef = react.useRef(null);
|
|
719
972
|
const textareaRef = react.useRef(null);
|
|
720
973
|
react.useEffect(() => {
|
|
721
974
|
if (!input && textareaRef.current) {
|
|
@@ -747,39 +1000,90 @@ function ChatInput({
|
|
|
747
1000
|
]
|
|
748
1001
|
}
|
|
749
1002
|
),
|
|
750
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex
|
|
751
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
752
|
-
|
|
1003
|
+
/* @__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: [
|
|
1004
|
+
hasAttachments && attachments && attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-2 pt-2", children: /* @__PURE__ */ jsxRuntime.jsx(AttachmentChips, { attachments, onRemove: (id) => onRemoveAttachment?.(id) }) }),
|
|
1005
|
+
hasAttachments && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1006
|
+
"input",
|
|
753
1007
|
{
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
value: input,
|
|
1008
|
+
ref: fileInputRef,
|
|
1009
|
+
type: "file",
|
|
1010
|
+
multiple: true,
|
|
1011
|
+
accept: ".csv,.txt,.tsv,.pdf,text/plain,text/csv,application/pdf,image/jpeg,image/png,image/webp,image/gif",
|
|
1012
|
+
className: "hidden",
|
|
760
1013
|
onChange: (e) => {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
disabled: streaming,
|
|
767
|
-
autoFocus: true
|
|
1014
|
+
if (e.target.files?.length) {
|
|
1015
|
+
onAttach(e.target.files);
|
|
1016
|
+
e.target.value = "";
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
768
1019
|
}
|
|
769
1020
|
),
|
|
770
|
-
/* @__PURE__ */ jsxRuntime.
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1021
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-end gap-2 px-3 py-2", children: [
|
|
1022
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1023
|
+
"textarea",
|
|
1024
|
+
{
|
|
1025
|
+
id: "wallavi-chat-input",
|
|
1026
|
+
ref: textareaRef,
|
|
1027
|
+
rows: 1,
|
|
1028
|
+
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",
|
|
1029
|
+
placeholder: placeholder ?? "Send a message\u2026",
|
|
1030
|
+
value: input,
|
|
1031
|
+
onChange: (e) => {
|
|
1032
|
+
setInput(e.target.value);
|
|
1033
|
+
e.target.style.height = "auto";
|
|
1034
|
+
e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
|
|
1035
|
+
},
|
|
1036
|
+
onKeyDown: handleKeyDown,
|
|
1037
|
+
disabled: streaming || voiceState === "recording" || voiceState === "transcribing",
|
|
1038
|
+
autoFocus: true
|
|
1039
|
+
}
|
|
1040
|
+
),
|
|
1041
|
+
hasVoice && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1042
|
+
"button",
|
|
1043
|
+
{
|
|
1044
|
+
type: "button",
|
|
1045
|
+
onClick: voiceState === "recording" ? onVoiceStop : onVoiceStart,
|
|
1046
|
+
disabled: streaming || voiceState === "transcribing",
|
|
1047
|
+
title: voiceState === "recording" ? "Stop recording" : "Record voice message",
|
|
1048
|
+
className: cn3(
|
|
1049
|
+
"h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
|
|
1050
|
+
voiceState === "recording" && "animate-pulse",
|
|
1051
|
+
voiceState === "error" ? "text-red-500 opacity-80" : "text-muted-foreground hover:text-foreground",
|
|
1052
|
+
(streaming || voiceState === "transcribing") && "opacity-40 pointer-events-none"
|
|
1053
|
+
),
|
|
1054
|
+
style: voiceState === "recording" ? { color: accentColor } : void 0,
|
|
1055
|
+
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" })
|
|
1056
|
+
}
|
|
1057
|
+
),
|
|
1058
|
+
hasAttachments && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1059
|
+
"button",
|
|
1060
|
+
{
|
|
1061
|
+
type: "button",
|
|
1062
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1063
|
+
disabled: streaming || isUploading,
|
|
1064
|
+
title: "Attach file (CSV, image\u2026)",
|
|
1065
|
+
className: cn3(
|
|
1066
|
+
"h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
|
|
1067
|
+
"text-muted-foreground hover:text-foreground",
|
|
1068
|
+
(streaming || isUploading) && "opacity-40 pointer-events-none"
|
|
1069
|
+
),
|
|
1070
|
+
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" })
|
|
1071
|
+
}
|
|
1072
|
+
),
|
|
1073
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1074
|
+
"button",
|
|
1075
|
+
{
|
|
1076
|
+
onClick: onSend,
|
|
1077
|
+
disabled: streaming || !hasText || voiceState === "recording" || voiceState === "transcribing",
|
|
1078
|
+
className: cn3(
|
|
1079
|
+
"h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
|
|
1080
|
+
hasText || streaming ? "opacity-100 shadow-sm" : "opacity-30"
|
|
1081
|
+
),
|
|
1082
|
+
style: hasText || streaming ? { backgroundColor: accentColor, color: getContrastColor(accentColor) } : { backgroundColor: "transparent", color: "currentColor" },
|
|
1083
|
+
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" })
|
|
1084
|
+
}
|
|
1085
|
+
)
|
|
1086
|
+
] })
|
|
783
1087
|
] })
|
|
784
1088
|
] });
|
|
785
1089
|
}
|
|
@@ -805,6 +1109,9 @@ function ChatWidget({
|
|
|
805
1109
|
source = "playground",
|
|
806
1110
|
userContext,
|
|
807
1111
|
playgroundOverrides,
|
|
1112
|
+
enableVoice = false,
|
|
1113
|
+
voiceAutoSend = false,
|
|
1114
|
+
enableAttachments = false,
|
|
808
1115
|
className,
|
|
809
1116
|
onClose,
|
|
810
1117
|
onReset,
|
|
@@ -812,6 +1119,48 @@ function ChatWidget({
|
|
|
812
1119
|
expanded
|
|
813
1120
|
}) {
|
|
814
1121
|
const chat = useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides });
|
|
1122
|
+
const voice = useVoice({
|
|
1123
|
+
agentId,
|
|
1124
|
+
onTranscript: (text) => {
|
|
1125
|
+
if (voiceAutoSend) {
|
|
1126
|
+
void chat.send(text);
|
|
1127
|
+
} else {
|
|
1128
|
+
chat.setInput(text);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
const attachmentHook = useAttachments({ agentId });
|
|
1133
|
+
const [isDragOver, setIsDragOver] = react.useState(false);
|
|
1134
|
+
const handleDragOver = react.useCallback((e) => {
|
|
1135
|
+
if (!enableAttachments) return;
|
|
1136
|
+
e.preventDefault();
|
|
1137
|
+
e.stopPropagation();
|
|
1138
|
+
if (e.dataTransfer.types.includes("Files")) setIsDragOver(true);
|
|
1139
|
+
}, [enableAttachments]);
|
|
1140
|
+
const handleDragLeave = react.useCallback((e) => {
|
|
1141
|
+
if (!enableAttachments) return;
|
|
1142
|
+
e.preventDefault();
|
|
1143
|
+
e.stopPropagation();
|
|
1144
|
+
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
1145
|
+
setIsDragOver(false);
|
|
1146
|
+
}
|
|
1147
|
+
}, [enableAttachments]);
|
|
1148
|
+
const handleDrop = react.useCallback((e) => {
|
|
1149
|
+
if (!enableAttachments) return;
|
|
1150
|
+
e.preventDefault();
|
|
1151
|
+
e.stopPropagation();
|
|
1152
|
+
setIsDragOver(false);
|
|
1153
|
+
const files = e.dataTransfer.files;
|
|
1154
|
+
if (files.length > 0) attachmentHook.attach(files);
|
|
1155
|
+
}, [enableAttachments, attachmentHook]);
|
|
1156
|
+
const handleSend = () => {
|
|
1157
|
+
const payloads = attachmentHook.readyPayloads;
|
|
1158
|
+
if (payloads.length > 0) {
|
|
1159
|
+
chat.queueAttachments(payloads);
|
|
1160
|
+
attachmentHook.clear();
|
|
1161
|
+
}
|
|
1162
|
+
void chat.send();
|
|
1163
|
+
};
|
|
815
1164
|
const canRegenerate = regenerateMessage && chat.messages.length > 0 && chat.messages.at(-1)?.role === "assistant" && !chat.streaming;
|
|
816
1165
|
const title = displayName || agentName;
|
|
817
1166
|
const headerBg = userMessageColor;
|
|
@@ -824,11 +1173,21 @@ function ChatWidget({
|
|
|
824
1173
|
"div",
|
|
825
1174
|
{
|
|
826
1175
|
className: cn(
|
|
827
|
-
"flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background h-full",
|
|
1176
|
+
"flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background h-full relative",
|
|
1177
|
+
isDragOver && "ring-2 ring-inset ring-primary/60",
|
|
828
1178
|
className
|
|
829
1179
|
),
|
|
830
1180
|
style: { colorScheme: theme },
|
|
1181
|
+
onDragOver: handleDragOver,
|
|
1182
|
+
onDragEnter: handleDragOver,
|
|
1183
|
+
onDragLeave: handleDragLeave,
|
|
1184
|
+
onDrop: handleDrop,
|
|
831
1185
|
children: [
|
|
1186
|
+
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: [
|
|
1187
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.UploadCloud, { className: "h-8 w-8 text-primary/70" }),
|
|
1188
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-foreground/70", children: "Drop files to attach" }),
|
|
1189
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: "CSV, TXT, PDF, JPG, PNG, WebP" })
|
|
1190
|
+
] }),
|
|
832
1191
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
833
1192
|
ChatHeader,
|
|
834
1193
|
{
|
|
@@ -862,12 +1221,23 @@ function ChatWidget({
|
|
|
862
1221
|
{
|
|
863
1222
|
input: chat.input,
|
|
864
1223
|
setInput: chat.setInput,
|
|
865
|
-
onSend:
|
|
1224
|
+
onSend: handleSend,
|
|
866
1225
|
streaming: chat.streaming,
|
|
867
1226
|
placeholder: messagePlaceholder,
|
|
868
1227
|
accentColor: userMessageColor,
|
|
869
1228
|
canRegenerate: !!canRegenerate,
|
|
870
|
-
onRegenerate: () => void chat.regenerate()
|
|
1229
|
+
onRegenerate: () => void chat.regenerate(),
|
|
1230
|
+
...enableVoice && voice.isSupported ? {
|
|
1231
|
+
voiceState: voice.voiceState,
|
|
1232
|
+
onVoiceStart: () => void voice.start(),
|
|
1233
|
+
onVoiceStop: voice.stop
|
|
1234
|
+
} : {},
|
|
1235
|
+
...enableAttachments ? {
|
|
1236
|
+
attachments: attachmentHook.attachments,
|
|
1237
|
+
onAttach: attachmentHook.attach,
|
|
1238
|
+
onRemoveAttachment: attachmentHook.remove,
|
|
1239
|
+
isUploading: attachmentHook.isUploading
|
|
1240
|
+
} : {}
|
|
871
1241
|
}
|
|
872
1242
|
),
|
|
873
1243
|
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 +1507,6 @@ exports.BubbleWidget = BubbleWidget;
|
|
|
1137
1507
|
exports.ChatWidget = ChatWidget;
|
|
1138
1508
|
exports.formatToolName = formatToolName;
|
|
1139
1509
|
exports.getContrastColor = getContrastColor;
|
|
1510
|
+
exports.useAttachments = useAttachments;
|
|
1140
1511
|
exports.useChat = useChat;
|
|
1512
|
+
exports.useVoice = useVoice;
|