@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useRef, useEffect, useState, useCallback } from 'react';
|
|
2
|
-
import { X, RotateCcw, Loader2, ArrowUp, Zap, ChevronDown, CheckCircle2,
|
|
1
|
+
import { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { UploadCloud, X, RotateCcw, Loader2, Square, Mic, Paperclip, ArrowUp, AlertCircle, Zap, ChevronDown, CheckCircle2, Search, Check, ImageIcon, FileText } from 'lucide-react';
|
|
3
3
|
import { clsx } from 'clsx';
|
|
4
4
|
import { twMerge } from 'tailwind-merge';
|
|
5
5
|
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
|
@@ -186,7 +186,7 @@ function useChat({
|
|
|
186
186
|
});
|
|
187
187
|
}, []);
|
|
188
188
|
const fetchAndStream = useCallback(async (opts) => {
|
|
189
|
-
const { input: userInput, msgId, extraMetadata } = opts;
|
|
189
|
+
const { input: userInput, msgId, extraMetadata, attachments } = opts;
|
|
190
190
|
const isPrivate = Boolean(workspaceId);
|
|
191
191
|
const token = isPrivate && typeof window !== "undefined" ? await window.Clerk?.session?.getToken() : null;
|
|
192
192
|
const url = isPrivate ? `${API_URL}/api/threads/${threadId}/stream` : `${API_URL}/api/chat/stream`;
|
|
@@ -201,6 +201,7 @@ function useChat({
|
|
|
201
201
|
agentId,
|
|
202
202
|
...isPrivate ? { workspaceId, ...playgroundOverrides ? { playgroundOverrides } : {} } : { threadId },
|
|
203
203
|
source,
|
|
204
|
+
...attachments?.length ? { attachments } : {},
|
|
204
205
|
...userContext?.userName ? { userName: userContext.userName } : {},
|
|
205
206
|
...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
|
|
206
207
|
userMetadata: {
|
|
@@ -221,6 +222,7 @@ function useChat({
|
|
|
221
222
|
if (!res.body) throw new Error("No stream body");
|
|
222
223
|
await consumeStream(res.body, (proto) => applyStreamEvent(proto, msgId));
|
|
223
224
|
}, [agentId, workspaceId, source, threadId, userContext, playgroundOverrides, applyStreamEvent]);
|
|
225
|
+
const pendingAttachmentsRef = useRef([]);
|
|
224
226
|
const send = useCallback(
|
|
225
227
|
async (text) => {
|
|
226
228
|
const userInput = (text ?? input).trim();
|
|
@@ -235,8 +237,10 @@ function useChat({
|
|
|
235
237
|
const assistantMsgId = newId();
|
|
236
238
|
streamingMsgIdRef.current = assistantMsgId;
|
|
237
239
|
setMessages((prev) => [...prev, { id: assistantMsgId, role: "assistant", parts: [] }]);
|
|
240
|
+
const attachments = pendingAttachmentsRef.current.length > 0 ? [...pendingAttachmentsRef.current] : void 0;
|
|
241
|
+
pendingAttachmentsRef.current = [];
|
|
238
242
|
try {
|
|
239
|
-
await fetchAndStream({ input: userInput, msgId: assistantMsgId });
|
|
243
|
+
await fetchAndStream({ input: userInput, msgId: assistantMsgId, attachments });
|
|
240
244
|
} catch {
|
|
241
245
|
setMessages((prev) => {
|
|
242
246
|
const idx = prev.findIndex((m) => m.id === assistantMsgId);
|
|
@@ -286,8 +290,11 @@ function useChat({
|
|
|
286
290
|
setStreaming(false);
|
|
287
291
|
streamingMsgIdRef.current = null;
|
|
288
292
|
},
|
|
289
|
-
[streaming, fetchAndStream]
|
|
293
|
+
[input, streaming, fetchAndStream]
|
|
290
294
|
);
|
|
295
|
+
const queueAttachments = useCallback((payloads) => {
|
|
296
|
+
pendingAttachmentsRef.current = payloads;
|
|
297
|
+
}, []);
|
|
291
298
|
const regenerate = useCallback(async () => {
|
|
292
299
|
if (streaming) return;
|
|
293
300
|
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
@@ -301,7 +308,176 @@ function useChat({
|
|
|
301
308
|
});
|
|
302
309
|
await send(lastText);
|
|
303
310
|
}, [streaming, messages, send]);
|
|
304
|
-
return { messages, input, setInput, streaming, threadId, send, regenerate, reset, selectPickerOption };
|
|
311
|
+
return { messages, input, setInput, streaming, threadId, send, queueAttachments, regenerate, reset, selectPickerOption };
|
|
312
|
+
}
|
|
313
|
+
function getPreferredMimeType() {
|
|
314
|
+
if (typeof MediaRecorder === "undefined") return "";
|
|
315
|
+
const candidates = [
|
|
316
|
+
"audio/webm;codecs=opus",
|
|
317
|
+
"audio/webm",
|
|
318
|
+
"audio/ogg;codecs=opus",
|
|
319
|
+
"audio/ogg",
|
|
320
|
+
"audio/mp4"
|
|
321
|
+
];
|
|
322
|
+
return candidates.find((t) => MediaRecorder.isTypeSupported(t)) ?? "";
|
|
323
|
+
}
|
|
324
|
+
function mimeTypeToExtension(mimeType) {
|
|
325
|
+
if (mimeType.includes("ogg")) return "ogg";
|
|
326
|
+
if (mimeType.includes("mp4")) return "mp4";
|
|
327
|
+
return "webm";
|
|
328
|
+
}
|
|
329
|
+
var DEFAULT_API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
|
|
330
|
+
function useVoice({ agentId, apiUrl, onTranscript, onError }) {
|
|
331
|
+
const [voiceState, setVoiceState] = useState("idle");
|
|
332
|
+
const recorderRef = useRef(null);
|
|
333
|
+
const chunksRef = useRef([]);
|
|
334
|
+
const streamRef = useRef(null);
|
|
335
|
+
const errorTimerRef = useRef(null);
|
|
336
|
+
const isSupported = typeof window !== "undefined" && typeof MediaRecorder !== "undefined" && !!navigator?.mediaDevices?.getUserMedia;
|
|
337
|
+
const base = apiUrl ?? DEFAULT_API_URL;
|
|
338
|
+
const transcribeBlob = useCallback(
|
|
339
|
+
async (blob, mimeType) => {
|
|
340
|
+
setVoiceState("transcribing");
|
|
341
|
+
try {
|
|
342
|
+
const ext = mimeTypeToExtension(mimeType);
|
|
343
|
+
const form = new FormData();
|
|
344
|
+
form.append("audio", blob, `recording.${ext}`);
|
|
345
|
+
form.append("agentId", agentId);
|
|
346
|
+
const res = await fetch(`${base}/api/chat/transcribe`, { method: "POST", body: form });
|
|
347
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
348
|
+
const data = await res.json();
|
|
349
|
+
if (!data.text?.trim()) throw new Error(data.error ?? "Empty transcript");
|
|
350
|
+
onTranscript(data.text.trim());
|
|
351
|
+
setVoiceState("idle");
|
|
352
|
+
} catch (err) {
|
|
353
|
+
const msg = err instanceof Error ? err.message : "Transcription failed";
|
|
354
|
+
onError?.(msg);
|
|
355
|
+
setVoiceState("error");
|
|
356
|
+
errorTimerRef.current = setTimeout(() => setVoiceState("idle"), 2500);
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
[agentId, base, onTranscript, onError]
|
|
360
|
+
);
|
|
361
|
+
const start = useCallback(async () => {
|
|
362
|
+
if (!isSupported || voiceState !== "idle") return;
|
|
363
|
+
try {
|
|
364
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
365
|
+
streamRef.current = stream;
|
|
366
|
+
const mimeType = getPreferredMimeType();
|
|
367
|
+
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : void 0);
|
|
368
|
+
chunksRef.current = [];
|
|
369
|
+
recorder.ondataavailable = (e) => {
|
|
370
|
+
if (e.data.size > 0) chunksRef.current.push(e.data);
|
|
371
|
+
};
|
|
372
|
+
recorder.onstop = async () => {
|
|
373
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
374
|
+
const blob = new Blob(chunksRef.current, { type: mimeType || "audio/webm" });
|
|
375
|
+
if (blob.size === 0) {
|
|
376
|
+
onError?.("Recording was empty \u2014 please try again.");
|
|
377
|
+
setVoiceState("idle");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
await transcribeBlob(blob, mimeType || "audio/webm");
|
|
381
|
+
};
|
|
382
|
+
recorder.start(250);
|
|
383
|
+
recorderRef.current = recorder;
|
|
384
|
+
setVoiceState("recording");
|
|
385
|
+
} catch (err) {
|
|
386
|
+
const msg = err instanceof Error ? err.message : "Microphone access denied";
|
|
387
|
+
onError?.(msg);
|
|
388
|
+
setVoiceState("idle");
|
|
389
|
+
}
|
|
390
|
+
}, [isSupported, voiceState, transcribeBlob, onError]);
|
|
391
|
+
const stop = useCallback(() => {
|
|
392
|
+
if (recorderRef.current?.state === "recording") {
|
|
393
|
+
recorderRef.current.stop();
|
|
394
|
+
recorderRef.current = null;
|
|
395
|
+
}
|
|
396
|
+
}, []);
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
return () => {
|
|
399
|
+
if (errorTimerRef.current) clearTimeout(errorTimerRef.current);
|
|
400
|
+
if (recorderRef.current?.state === "recording") recorderRef.current.stop();
|
|
401
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
402
|
+
};
|
|
403
|
+
}, []);
|
|
404
|
+
return { voiceState, isSupported, start, stop };
|
|
405
|
+
}
|
|
406
|
+
var DEFAULT_API_URL2 = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
|
|
407
|
+
function makeId() {
|
|
408
|
+
return `att_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
409
|
+
}
|
|
410
|
+
function useAttachments({
|
|
411
|
+
agentId,
|
|
412
|
+
apiUrl,
|
|
413
|
+
maxFiles = 5
|
|
414
|
+
}) {
|
|
415
|
+
const [attachments, setAttachments] = useState([]);
|
|
416
|
+
const base = apiUrl ?? DEFAULT_API_URL2;
|
|
417
|
+
const uploadOne = useCallback(
|
|
418
|
+
async (file, id) => {
|
|
419
|
+
const form = new FormData();
|
|
420
|
+
form.append("file", file);
|
|
421
|
+
form.append("agentId", agentId);
|
|
422
|
+
try {
|
|
423
|
+
const res = await fetch(`${base}/api/chat/upload`, { method: "POST", body: form });
|
|
424
|
+
if (!res.ok) {
|
|
425
|
+
const data = await res.json().catch(() => ({}));
|
|
426
|
+
setAttachments(
|
|
427
|
+
(prev) => prev.map(
|
|
428
|
+
(a) => a.id === id ? {
|
|
429
|
+
...a,
|
|
430
|
+
status: "error",
|
|
431
|
+
errorMessage: data.error ?? `Upload failed (${res.status})`
|
|
432
|
+
} : a
|
|
433
|
+
)
|
|
434
|
+
);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const payload = await res.json();
|
|
438
|
+
setAttachments(
|
|
439
|
+
(prev) => prev.map((a) => a.id === id ? { ...a, status: "ready", payload } : a)
|
|
440
|
+
);
|
|
441
|
+
} catch (err) {
|
|
442
|
+
const msg = err instanceof Error ? err.message : "Upload failed";
|
|
443
|
+
setAttachments(
|
|
444
|
+
(prev) => prev.map(
|
|
445
|
+
(a) => a.id === id ? { ...a, status: "error", errorMessage: msg } : a
|
|
446
|
+
)
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
[agentId, base]
|
|
451
|
+
);
|
|
452
|
+
const attach = useCallback(
|
|
453
|
+
(files) => {
|
|
454
|
+
const list = Array.from(files);
|
|
455
|
+
setAttachments((prev) => {
|
|
456
|
+
const remaining = maxFiles - prev.filter((a) => a.status !== "error").length;
|
|
457
|
+
if (remaining <= 0) return prev;
|
|
458
|
+
const toAdd = list.slice(0, remaining).map((f) => {
|
|
459
|
+
const id = makeId();
|
|
460
|
+
void uploadOne(f, id);
|
|
461
|
+
return {
|
|
462
|
+
id,
|
|
463
|
+
name: f.name,
|
|
464
|
+
mimeType: f.type,
|
|
465
|
+
sizeBytes: f.size,
|
|
466
|
+
status: "uploading"
|
|
467
|
+
};
|
|
468
|
+
});
|
|
469
|
+
return [...prev, ...toAdd];
|
|
470
|
+
});
|
|
471
|
+
},
|
|
472
|
+
[maxFiles, uploadOne]
|
|
473
|
+
);
|
|
474
|
+
const remove = useCallback((id) => {
|
|
475
|
+
setAttachments((prev) => prev.filter((a) => a.id !== id));
|
|
476
|
+
}, []);
|
|
477
|
+
const clear = useCallback(() => setAttachments([]), []);
|
|
478
|
+
const isUploading = attachments.some((a) => a.status === "uploading");
|
|
479
|
+
const readyPayloads = attachments.filter((a) => a.status === "ready" && a.payload).map((a) => a.payload);
|
|
480
|
+
return { attachments, attach, remove, clear, isUploading, readyPayloads };
|
|
305
481
|
}
|
|
306
482
|
function DefaultIcon() {
|
|
307
483
|
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", style: { width: 26, height: 26 }, children: [
|
|
@@ -475,83 +651,112 @@ function ReasoningBlock({ text }) {
|
|
|
475
651
|
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 })
|
|
476
652
|
] });
|
|
477
653
|
}
|
|
478
|
-
function PickerOption({
|
|
479
|
-
opt,
|
|
480
|
-
isSelected,
|
|
481
|
-
isConsumed,
|
|
482
|
-
actionDisabled,
|
|
483
|
-
pickerId,
|
|
484
|
-
paramName,
|
|
485
|
-
onSelect
|
|
486
|
-
}) {
|
|
487
|
-
const [hovered, setHovered] = useState(false);
|
|
488
|
-
const inactive = actionDisabled || isConsumed;
|
|
489
|
-
return /* @__PURE__ */ jsx(
|
|
490
|
-
"button",
|
|
491
|
-
{
|
|
492
|
-
disabled: inactive,
|
|
493
|
-
onMouseEnter: () => setHovered(true),
|
|
494
|
-
onMouseLeave: () => setHovered(false),
|
|
495
|
-
onClick: () => {
|
|
496
|
-
if (!inactive) onSelect(pickerId, paramName, opt.value, opt.label);
|
|
497
|
-
},
|
|
498
|
-
style: {
|
|
499
|
-
fontSize: 13,
|
|
500
|
-
lineHeight: "1.4",
|
|
501
|
-
borderRadius: 8,
|
|
502
|
-
padding: "6px 13px",
|
|
503
|
-
border: isSelected ? "1.5px solid var(--primary, #19191c)" : hovered && !inactive ? "1.5px solid rgba(0,0,0,0.28)" : "1.5px solid rgba(0,0,0,0.13)",
|
|
504
|
-
backgroundColor: isSelected ? "var(--primary, #19191c)" : hovered && !inactive ? "rgba(0,0,0,0.04)" : "#fff",
|
|
505
|
-
color: isSelected ? "var(--primary-foreground, #fff)" : "var(--foreground, #09090b)",
|
|
506
|
-
cursor: inactive ? "default" : "pointer",
|
|
507
|
-
opacity: isConsumed ? 0.28 : 1,
|
|
508
|
-
transition: "border-color 0.12s ease, background-color 0.12s ease, opacity 0.12s ease",
|
|
509
|
-
fontWeight: isSelected ? 600 : 400,
|
|
510
|
-
boxShadow: isSelected || inactive ? "none" : "0 1px 2px rgba(0,0,0,0.07)"
|
|
511
|
-
},
|
|
512
|
-
children: opt.label
|
|
513
|
-
}
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
654
|
function PickerSelector({
|
|
517
655
|
part,
|
|
518
656
|
disabled,
|
|
519
657
|
onSelect
|
|
520
658
|
}) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
659
|
+
const count = part.options.length;
|
|
660
|
+
const mode = count <= 6 ? "pills" : count <= 20 ? "grid" : "list";
|
|
661
|
+
const [query, setQuery] = useState("");
|
|
662
|
+
const filtered = useMemo(() => {
|
|
663
|
+
if (!query.trim()) return part.options;
|
|
664
|
+
const q = query.toLowerCase();
|
|
665
|
+
return part.options.filter((o) => o.label.toLowerCase().includes(q));
|
|
666
|
+
}, [part.options, query]);
|
|
667
|
+
const isConsumed = !!part.selectedValue;
|
|
668
|
+
const handleClick = (opt) => {
|
|
669
|
+
if (!disabled && !isConsumed) onSelect(part.pickerId, part.paramName, opt.value, opt.label);
|
|
670
|
+
};
|
|
671
|
+
return /* @__PURE__ */ jsxs("div", { className: cn(
|
|
672
|
+
"flex flex-col gap-2.5 rounded-xl rounded-tl-none border bg-muted/60 p-3",
|
|
673
|
+
"border-border/50"
|
|
674
|
+
), children: [
|
|
675
|
+
/* @__PURE__ */ jsx("p", { className: "text-[10.5px] font-semibold uppercase tracking-widest text-muted-foreground/70 leading-none", children: part.label }),
|
|
676
|
+
mode === "list" && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
677
|
+
/* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground/50 pointer-events-none" }),
|
|
678
|
+
/* @__PURE__ */ jsx(
|
|
679
|
+
"input",
|
|
680
|
+
{
|
|
681
|
+
type: "text",
|
|
682
|
+
value: query,
|
|
683
|
+
onChange: (e) => setQuery(e.target.value),
|
|
684
|
+
placeholder: "Search\u2026",
|
|
685
|
+
disabled: disabled || isConsumed,
|
|
686
|
+
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"
|
|
687
|
+
}
|
|
688
|
+
)
|
|
689
|
+
] }),
|
|
690
|
+
mode === "pills" && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: part.options.map((opt) => {
|
|
691
|
+
const sel = part.selectedValue === opt.value;
|
|
692
|
+
const faded = isConsumed && !sel;
|
|
693
|
+
return /* @__PURE__ */ jsxs(
|
|
694
|
+
"button",
|
|
695
|
+
{
|
|
696
|
+
disabled: disabled || isConsumed,
|
|
697
|
+
onClick: () => handleClick(opt),
|
|
698
|
+
className: cn(
|
|
699
|
+
"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",
|
|
700
|
+
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",
|
|
701
|
+
disabled && !isConsumed && "opacity-60 pointer-events-none"
|
|
702
|
+
),
|
|
703
|
+
children: [
|
|
704
|
+
sel && /* @__PURE__ */ jsx(Check, { className: "h-3 w-3 shrink-0" }),
|
|
705
|
+
opt.label
|
|
706
|
+
]
|
|
707
|
+
},
|
|
708
|
+
opt.value
|
|
709
|
+
);
|
|
710
|
+
}) }),
|
|
711
|
+
mode === "grid" && /* @__PURE__ */ 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) => {
|
|
712
|
+
const sel = part.selectedValue === opt.value;
|
|
713
|
+
const faded = isConsumed && !sel;
|
|
714
|
+
return /* @__PURE__ */ jsxs(
|
|
715
|
+
"button",
|
|
716
|
+
{
|
|
717
|
+
disabled: disabled || isConsumed,
|
|
718
|
+
onClick: () => handleClick(opt),
|
|
719
|
+
className: cn(
|
|
720
|
+
"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",
|
|
721
|
+
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",
|
|
722
|
+
disabled && !isConsumed && "opacity-60 pointer-events-none"
|
|
723
|
+
),
|
|
724
|
+
children: [
|
|
725
|
+
sel ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx("span", { className: "h-3 w-3 shrink-0" }),
|
|
726
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: opt.label })
|
|
727
|
+
]
|
|
728
|
+
},
|
|
729
|
+
opt.value
|
|
730
|
+
);
|
|
731
|
+
}) }),
|
|
732
|
+
mode === "list" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5 max-h-[180px] overflow-y-auto pr-0.5", children: [
|
|
733
|
+
filtered.length === 0 && /* @__PURE__ */ jsx("p", { className: "py-3 text-center text-xs text-muted-foreground/50", children: "No results" }),
|
|
734
|
+
filtered.map((opt) => {
|
|
735
|
+
const sel = part.selectedValue === opt.value;
|
|
736
|
+
const faded = isConsumed && !sel;
|
|
737
|
+
return /* @__PURE__ */ jsxs(
|
|
738
|
+
"button",
|
|
739
|
+
{
|
|
740
|
+
disabled: disabled || isConsumed,
|
|
741
|
+
onClick: () => handleClick(opt),
|
|
742
|
+
className: cn(
|
|
743
|
+
"flex items-center gap-2 rounded-lg px-2.5 py-2 text-[12.5px] text-left transition-all duration-100 select-none",
|
|
744
|
+
sel ? "bg-foreground text-background font-medium" : faded ? "opacity-30 cursor-default" : "hover:bg-background text-foreground cursor-pointer",
|
|
745
|
+
disabled && !isConsumed && "opacity-60 pointer-events-none"
|
|
746
|
+
),
|
|
747
|
+
children: [
|
|
748
|
+
/* @__PURE__ */ jsx("span", { className: cn(
|
|
749
|
+
"flex h-4 w-4 shrink-0 items-center justify-center rounded-full border text-[10px]",
|
|
750
|
+
sel ? "border-background bg-background/20" : "border-border/50"
|
|
751
|
+
), children: sel && /* @__PURE__ */ jsx(Check, { className: "h-2.5 w-2.5" }) }),
|
|
752
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: opt.label })
|
|
753
|
+
]
|
|
754
|
+
},
|
|
755
|
+
opt.value
|
|
756
|
+
);
|
|
757
|
+
})
|
|
758
|
+
] })
|
|
759
|
+
] });
|
|
555
760
|
}
|
|
556
761
|
function MessageBubble({
|
|
557
762
|
message,
|
|
@@ -680,6 +885,44 @@ function ChatMessages({
|
|
|
680
885
|
] });
|
|
681
886
|
}
|
|
682
887
|
var cn2 = (...inputs) => twMerge(clsx(inputs));
|
|
888
|
+
function formatBytes(bytes) {
|
|
889
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
890
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
891
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
892
|
+
}
|
|
893
|
+
function FileIcon({ mimeType }) {
|
|
894
|
+
if (mimeType.startsWith("image/")) return /* @__PURE__ */ jsx(ImageIcon, { className: "h-3 w-3 shrink-0" });
|
|
895
|
+
return /* @__PURE__ */ jsx(FileText, { className: "h-3 w-3 shrink-0" });
|
|
896
|
+
}
|
|
897
|
+
function AttachmentChips({ attachments, onRemove }) {
|
|
898
|
+
if (attachments.length === 0) return null;
|
|
899
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pb-1.5", children: attachments.map((a) => /* @__PURE__ */ jsxs(
|
|
900
|
+
"div",
|
|
901
|
+
{
|
|
902
|
+
className: cn2(
|
|
903
|
+
"flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[11px] max-w-[180px]",
|
|
904
|
+
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"
|
|
905
|
+
),
|
|
906
|
+
children: [
|
|
907
|
+
a.status === "uploading" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 shrink-0 animate-spin" }) : a.status === "error" ? /* @__PURE__ */ jsx(AlertCircle, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(FileIcon, { mimeType: a.mimeType }),
|
|
908
|
+
/* @__PURE__ */ jsx("span", { className: "truncate leading-tight", children: a.status === "error" ? a.errorMessage ?? "Upload failed" : a.name }),
|
|
909
|
+
a.status === "ready" && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground/60", children: formatBytes(a.sizeBytes) }),
|
|
910
|
+
/* @__PURE__ */ jsx(
|
|
911
|
+
"button",
|
|
912
|
+
{
|
|
913
|
+
type: "button",
|
|
914
|
+
onClick: () => onRemove(a.id),
|
|
915
|
+
className: "shrink-0 rounded-full p-0.5 hover:bg-foreground/10 transition-colors",
|
|
916
|
+
title: "Remove attachment",
|
|
917
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
|
|
918
|
+
}
|
|
919
|
+
)
|
|
920
|
+
]
|
|
921
|
+
},
|
|
922
|
+
a.id
|
|
923
|
+
)) });
|
|
924
|
+
}
|
|
925
|
+
var cn3 = (...inputs) => twMerge(clsx(inputs));
|
|
683
926
|
function ChatInput({
|
|
684
927
|
input,
|
|
685
928
|
setInput,
|
|
@@ -688,8 +931,18 @@ function ChatInput({
|
|
|
688
931
|
placeholder,
|
|
689
932
|
accentColor,
|
|
690
933
|
canRegenerate = false,
|
|
691
|
-
onRegenerate
|
|
934
|
+
onRegenerate,
|
|
935
|
+
voiceState,
|
|
936
|
+
onVoiceStart,
|
|
937
|
+
onVoiceStop,
|
|
938
|
+
attachments,
|
|
939
|
+
onAttach,
|
|
940
|
+
onRemoveAttachment,
|
|
941
|
+
isUploading = false
|
|
692
942
|
}) {
|
|
943
|
+
const hasVoice = onVoiceStart !== void 0;
|
|
944
|
+
const hasAttachments = onAttach !== void 0;
|
|
945
|
+
const fileInputRef = useRef(null);
|
|
693
946
|
const textareaRef = useRef(null);
|
|
694
947
|
useEffect(() => {
|
|
695
948
|
if (!input && textareaRef.current) {
|
|
@@ -721,39 +974,90 @@ function ChatInput({
|
|
|
721
974
|
]
|
|
722
975
|
}
|
|
723
976
|
),
|
|
724
|
-
/* @__PURE__ */ jsxs("div", { className: "flex
|
|
725
|
-
/* @__PURE__ */ jsx(
|
|
726
|
-
|
|
977
|
+
/* @__PURE__ */ 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: [
|
|
978
|
+
hasAttachments && attachments && attachments.length > 0 && /* @__PURE__ */ jsx("div", { className: "px-2 pt-2", children: /* @__PURE__ */ jsx(AttachmentChips, { attachments, onRemove: (id) => onRemoveAttachment?.(id) }) }),
|
|
979
|
+
hasAttachments && /* @__PURE__ */ jsx(
|
|
980
|
+
"input",
|
|
727
981
|
{
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
value: input,
|
|
982
|
+
ref: fileInputRef,
|
|
983
|
+
type: "file",
|
|
984
|
+
multiple: true,
|
|
985
|
+
accept: ".csv,.txt,.tsv,.pdf,text/plain,text/csv,application/pdf,image/jpeg,image/png,image/webp,image/gif",
|
|
986
|
+
className: "hidden",
|
|
734
987
|
onChange: (e) => {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
disabled: streaming,
|
|
741
|
-
autoFocus: true
|
|
988
|
+
if (e.target.files?.length) {
|
|
989
|
+
onAttach(e.target.files);
|
|
990
|
+
e.target.value = "";
|
|
991
|
+
}
|
|
992
|
+
}
|
|
742
993
|
}
|
|
743
994
|
),
|
|
744
|
-
/* @__PURE__ */
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
995
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2 px-3 py-2", children: [
|
|
996
|
+
/* @__PURE__ */ jsx(
|
|
997
|
+
"textarea",
|
|
998
|
+
{
|
|
999
|
+
id: "wallavi-chat-input",
|
|
1000
|
+
ref: textareaRef,
|
|
1001
|
+
rows: 1,
|
|
1002
|
+
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",
|
|
1003
|
+
placeholder: placeholder ?? "Send a message\u2026",
|
|
1004
|
+
value: input,
|
|
1005
|
+
onChange: (e) => {
|
|
1006
|
+
setInput(e.target.value);
|
|
1007
|
+
e.target.style.height = "auto";
|
|
1008
|
+
e.target.style.height = `${Math.min(e.target.scrollHeight, 128)}px`;
|
|
1009
|
+
},
|
|
1010
|
+
onKeyDown: handleKeyDown,
|
|
1011
|
+
disabled: streaming || voiceState === "recording" || voiceState === "transcribing",
|
|
1012
|
+
autoFocus: true
|
|
1013
|
+
}
|
|
1014
|
+
),
|
|
1015
|
+
hasVoice && /* @__PURE__ */ jsx(
|
|
1016
|
+
"button",
|
|
1017
|
+
{
|
|
1018
|
+
type: "button",
|
|
1019
|
+
onClick: voiceState === "recording" ? onVoiceStop : onVoiceStart,
|
|
1020
|
+
disabled: streaming || voiceState === "transcribing",
|
|
1021
|
+
title: voiceState === "recording" ? "Stop recording" : "Record voice message",
|
|
1022
|
+
className: cn3(
|
|
1023
|
+
"h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
|
|
1024
|
+
voiceState === "recording" && "animate-pulse",
|
|
1025
|
+
voiceState === "error" ? "text-red-500 opacity-80" : "text-muted-foreground hover:text-foreground",
|
|
1026
|
+
(streaming || voiceState === "transcribing") && "opacity-40 pointer-events-none"
|
|
1027
|
+
),
|
|
1028
|
+
style: voiceState === "recording" ? { color: accentColor } : void 0,
|
|
1029
|
+
children: voiceState === "transcribing" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : voiceState === "recording" ? /* @__PURE__ */ jsx(Square, { className: "h-3.5 w-3.5 fill-current" }) : /* @__PURE__ */ jsx(Mic, { className: "h-3.5 w-3.5" })
|
|
1030
|
+
}
|
|
1031
|
+
),
|
|
1032
|
+
hasAttachments && /* @__PURE__ */ jsx(
|
|
1033
|
+
"button",
|
|
1034
|
+
{
|
|
1035
|
+
type: "button",
|
|
1036
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1037
|
+
disabled: streaming || isUploading,
|
|
1038
|
+
title: "Attach file (CSV, image\u2026)",
|
|
1039
|
+
className: cn3(
|
|
1040
|
+
"h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
|
|
1041
|
+
"text-muted-foreground hover:text-foreground",
|
|
1042
|
+
(streaming || isUploading) && "opacity-40 pointer-events-none"
|
|
1043
|
+
),
|
|
1044
|
+
children: isUploading ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Paperclip, { className: "h-3.5 w-3.5" })
|
|
1045
|
+
}
|
|
1046
|
+
),
|
|
1047
|
+
/* @__PURE__ */ jsx(
|
|
1048
|
+
"button",
|
|
1049
|
+
{
|
|
1050
|
+
onClick: onSend,
|
|
1051
|
+
disabled: streaming || !hasText || voiceState === "recording" || voiceState === "transcribing",
|
|
1052
|
+
className: cn3(
|
|
1053
|
+
"h-7 w-7 shrink-0 rounded-xl flex items-center justify-center transition-all duration-200",
|
|
1054
|
+
hasText || streaming ? "opacity-100 shadow-sm" : "opacity-30"
|
|
1055
|
+
),
|
|
1056
|
+
style: hasText || streaming ? { backgroundColor: accentColor, color: getContrastColor(accentColor) } : { backgroundColor: "transparent", color: "currentColor" },
|
|
1057
|
+
children: streaming ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(ArrowUp, { className: "h-3.5 w-3.5" })
|
|
1058
|
+
}
|
|
1059
|
+
)
|
|
1060
|
+
] })
|
|
757
1061
|
] })
|
|
758
1062
|
] });
|
|
759
1063
|
}
|
|
@@ -779,6 +1083,9 @@ function ChatWidget({
|
|
|
779
1083
|
source = "playground",
|
|
780
1084
|
userContext,
|
|
781
1085
|
playgroundOverrides,
|
|
1086
|
+
enableVoice = false,
|
|
1087
|
+
voiceAutoSend = false,
|
|
1088
|
+
enableAttachments = false,
|
|
782
1089
|
className,
|
|
783
1090
|
onClose,
|
|
784
1091
|
onReset,
|
|
@@ -786,6 +1093,48 @@ function ChatWidget({
|
|
|
786
1093
|
expanded
|
|
787
1094
|
}) {
|
|
788
1095
|
const chat = useChat({ agentId, workspaceId, source, userContext, persist, onNavigate, playgroundOverrides });
|
|
1096
|
+
const voice = useVoice({
|
|
1097
|
+
agentId,
|
|
1098
|
+
onTranscript: (text) => {
|
|
1099
|
+
if (voiceAutoSend) {
|
|
1100
|
+
void chat.send(text);
|
|
1101
|
+
} else {
|
|
1102
|
+
chat.setInput(text);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
const attachmentHook = useAttachments({ agentId });
|
|
1107
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
1108
|
+
const handleDragOver = useCallback((e) => {
|
|
1109
|
+
if (!enableAttachments) return;
|
|
1110
|
+
e.preventDefault();
|
|
1111
|
+
e.stopPropagation();
|
|
1112
|
+
if (e.dataTransfer.types.includes("Files")) setIsDragOver(true);
|
|
1113
|
+
}, [enableAttachments]);
|
|
1114
|
+
const handleDragLeave = useCallback((e) => {
|
|
1115
|
+
if (!enableAttachments) return;
|
|
1116
|
+
e.preventDefault();
|
|
1117
|
+
e.stopPropagation();
|
|
1118
|
+
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
1119
|
+
setIsDragOver(false);
|
|
1120
|
+
}
|
|
1121
|
+
}, [enableAttachments]);
|
|
1122
|
+
const handleDrop = useCallback((e) => {
|
|
1123
|
+
if (!enableAttachments) return;
|
|
1124
|
+
e.preventDefault();
|
|
1125
|
+
e.stopPropagation();
|
|
1126
|
+
setIsDragOver(false);
|
|
1127
|
+
const files = e.dataTransfer.files;
|
|
1128
|
+
if (files.length > 0) attachmentHook.attach(files);
|
|
1129
|
+
}, [enableAttachments, attachmentHook]);
|
|
1130
|
+
const handleSend = () => {
|
|
1131
|
+
const payloads = attachmentHook.readyPayloads;
|
|
1132
|
+
if (payloads.length > 0) {
|
|
1133
|
+
chat.queueAttachments(payloads);
|
|
1134
|
+
attachmentHook.clear();
|
|
1135
|
+
}
|
|
1136
|
+
void chat.send();
|
|
1137
|
+
};
|
|
789
1138
|
const canRegenerate = regenerateMessage && chat.messages.length > 0 && chat.messages.at(-1)?.role === "assistant" && !chat.streaming;
|
|
790
1139
|
const title = displayName || agentName;
|
|
791
1140
|
const headerBg = userMessageColor;
|
|
@@ -798,11 +1147,21 @@ function ChatWidget({
|
|
|
798
1147
|
"div",
|
|
799
1148
|
{
|
|
800
1149
|
className: cn(
|
|
801
|
-
"flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background h-full",
|
|
1150
|
+
"flex flex-col overflow-hidden rounded-2xl border shadow-xl bg-background h-full relative",
|
|
1151
|
+
isDragOver && "ring-2 ring-inset ring-primary/60",
|
|
802
1152
|
className
|
|
803
1153
|
),
|
|
804
1154
|
style: { colorScheme: theme },
|
|
1155
|
+
onDragOver: handleDragOver,
|
|
1156
|
+
onDragEnter: handleDragOver,
|
|
1157
|
+
onDragLeave: handleDragLeave,
|
|
1158
|
+
onDrop: handleDrop,
|
|
805
1159
|
children: [
|
|
1160
|
+
isDragOver && enableAttachments && /* @__PURE__ */ 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: [
|
|
1161
|
+
/* @__PURE__ */ jsx(UploadCloud, { className: "h-8 w-8 text-primary/70" }),
|
|
1162
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground/70", children: "Drop files to attach" }),
|
|
1163
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "CSV, TXT, PDF, JPG, PNG, WebP" })
|
|
1164
|
+
] }),
|
|
806
1165
|
/* @__PURE__ */ jsx(
|
|
807
1166
|
ChatHeader,
|
|
808
1167
|
{
|
|
@@ -836,12 +1195,23 @@ function ChatWidget({
|
|
|
836
1195
|
{
|
|
837
1196
|
input: chat.input,
|
|
838
1197
|
setInput: chat.setInput,
|
|
839
|
-
onSend:
|
|
1198
|
+
onSend: handleSend,
|
|
840
1199
|
streaming: chat.streaming,
|
|
841
1200
|
placeholder: messagePlaceholder,
|
|
842
1201
|
accentColor: userMessageColor,
|
|
843
1202
|
canRegenerate: !!canRegenerate,
|
|
844
|
-
onRegenerate: () => void chat.regenerate()
|
|
1203
|
+
onRegenerate: () => void chat.regenerate(),
|
|
1204
|
+
...enableVoice && voice.isSupported ? {
|
|
1205
|
+
voiceState: voice.voiceState,
|
|
1206
|
+
onVoiceStart: () => void voice.start(),
|
|
1207
|
+
onVoiceStop: voice.stop
|
|
1208
|
+
} : {},
|
|
1209
|
+
...enableAttachments ? {
|
|
1210
|
+
attachments: attachmentHook.attachments,
|
|
1211
|
+
onAttach: attachmentHook.attach,
|
|
1212
|
+
onRemoveAttachment: attachmentHook.remove,
|
|
1213
|
+
isUploading: attachmentHook.isUploading
|
|
1214
|
+
} : {}
|
|
845
1215
|
}
|
|
846
1216
|
),
|
|
847
1217
|
watermark && /* @__PURE__ */ jsxs("footer", { className: "shrink-0 flex items-center justify-center gap-1.5 bg-muted/50 py-1.5 border-t", children: [
|
|
@@ -1107,4 +1477,4 @@ function BubbleWidget({
|
|
|
1107
1477
|
);
|
|
1108
1478
|
}
|
|
1109
1479
|
|
|
1110
|
-
export { BubbleWidget, ChatWidget, formatToolName, getContrastColor, useChat };
|
|
1480
|
+
export { BubbleWidget, ChatWidget, formatToolName, getContrastColor, useAttachments, useChat, useVoice };
|