@cntyclub/agent-react 0.10.2 → 0.12.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.js CHANGED
@@ -1,8 +1,9 @@
1
- import { Button, cn, Tooltip, TooltipTrigger, TooltipContent, ChatMessage, ChatMessageBubble, Markdown, Spinner, Checkbox, Collapsible, CollapsibleTrigger, ChatToolChip, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Group, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, ChatHeaderActions, Chat, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion, useMediaQuery, TooltipProvider, DataTablePaged } from '@cntyclub/ui-react';
2
- import { FileTextIcon, XIcon, PaperclipIcon, ArrowUpIcon, CheckIcon, CopyIcon, Maximize2Icon, ListChecksIcon, ChevronDownIcon, ShieldAlertIcon, CheckCheckIcon, UploadCloudIcon, HistoryIcon, SquarePenIcon, Minimize2Icon, Trash2Icon, SparklesIcon, DownloadIcon, CodeIcon } from 'lucide-react';
3
- import * as React8 from 'react';
1
+ import { cn, Button, Tooltip, TooltipTrigger, TooltipContent, ChatMessage, ChatMessageBubble, Markdown, Spinner, Checkbox, Collapsible, CollapsibleTrigger, ChatToolChip, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Group, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, ChatHeaderActions, Chat, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion, useMediaQuery, TooltipProvider, DataTablePaged } from '@cntyclub/ui-react';
2
+ import { XIcon, Trash2Icon, SquareIcon, PauseIcon, PlayIcon, CheckIcon, Loader2Icon, FileTextIcon, PaperclipIcon, MicIcon, ArrowUpIcon, CopyIcon, Maximize2Icon, ListChecksIcon, ChevronDownIcon, ShieldAlertIcon, CheckCheckIcon, UploadCloudIcon, HistoryIcon, SquarePenIcon, Minimize2Icon, SparklesIcon, DownloadIcon, CodeIcon } from 'lucide-react';
3
+ import * as React11 from 'react';
4
+ import { AnimatePresence, motion } from 'motion/react';
5
+ import { createPortal } from 'react-dom';
4
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
- import { motion, AnimatePresence } from 'motion/react';
6
7
 
7
8
  // src/api-client.ts
8
9
  var AgentApiError = class extends Error {
@@ -52,6 +53,18 @@ var AgentApiClient = class {
52
53
  getConfig() {
53
54
  return this.request("/agent-mode/config/");
54
55
  }
56
+ /** Upload a recorded voice clip and get back the transcribed text. */
57
+ transcribe(audio) {
58
+ const type = audio.type || "audio/webm";
59
+ const ext = type.includes("mp4") ? "mp4" : type.includes("ogg") ? "ogg" : type.includes("wav") ? "wav" : "webm";
60
+ const form = new FormData();
61
+ form.append("client_id", this.config.clientId);
62
+ form.append("audio", audio, `recording.${ext}`);
63
+ return this.request("/agent-mode/transcribe/", {
64
+ body: form,
65
+ method: "POST"
66
+ });
67
+ }
55
68
  sendMessage(message, conversationId, files) {
56
69
  if (files && files.length > 0) {
57
70
  const form = new FormData();
@@ -183,28 +196,55 @@ var AgentApiClient = class {
183
196
 
184
197
  // src/file-support.ts
185
198
  var DEFAULT_ACCEPT = [".txt", ".md", ".markdown", ".csv", ".tsv", ".log", ".docx"];
199
+ var IMAGE_ACCEPT = [".png", ".jpg", ".jpeg", ".webp", ".gif"];
186
200
  var DEFAULT_MAX_SIZE_MB = 5;
201
+ var DEFAULT_IMAGE_MAX_SIZE_MB = 10;
187
202
  function fileUploadEnabled(config) {
188
203
  return config.fileUpload?.enabled !== false;
189
204
  }
190
- function acceptedExtensions(config) {
205
+ function textExtensions(config) {
206
+ if (!fileUploadEnabled(config)) return [];
191
207
  const accept = config.fileUpload?.accept;
192
208
  return accept && accept.length > 0 ? accept.map((ext) => ext.toLowerCase()) : DEFAULT_ACCEPT;
193
209
  }
210
+ function acceptedExtensions(config, caps) {
211
+ const extensions = [...textExtensions(config)];
212
+ if (caps?.visionEnabled) extensions.push(...IMAGE_ACCEPT);
213
+ return extensions;
214
+ }
194
215
  function maxSizeMb(config) {
195
216
  return config.fileUpload?.maxSizeMb ?? DEFAULT_MAX_SIZE_MB;
196
217
  }
197
- function acceptAttribute(config) {
198
- return acceptedExtensions(config).join(",");
218
+ function attachmentsEnabled(config, caps) {
219
+ return fileUploadEnabled(config) || Boolean(caps?.visionEnabled);
220
+ }
221
+ function acceptAttribute(config, caps) {
222
+ return acceptedExtensions(config, caps).join(",");
199
223
  }
200
224
  function extensionOf(filename) {
201
225
  const dot = filename.lastIndexOf(".");
202
226
  return dot >= 0 ? filename.slice(dot).toLowerCase() : "";
203
227
  }
204
- function validateFile(config, file) {
205
- const extensions = acceptedExtensions(config);
228
+ function isImageFile(file) {
229
+ return file.type.startsWith("image/") || IMAGE_ACCEPT.includes(extensionOf(file.name));
230
+ }
231
+ function validateFile(config, file, caps) {
232
+ if (isImageFile(file)) {
233
+ if (!caps?.visionEnabled) {
234
+ return `\u201C${file.name}\u201D is an image \u2014 this assistant can\u2019t accept images.`;
235
+ }
236
+ const limit2 = caps.visionMaxMb ?? DEFAULT_IMAGE_MAX_SIZE_MB;
237
+ if (file.size > limit2 * 1024 * 1024) {
238
+ return `\u201C${file.name}\u201D is too large (max ${limit2} MB).`;
239
+ }
240
+ return null;
241
+ }
242
+ const extensions = textExtensions(config);
243
+ if (!fileUploadEnabled(config)) {
244
+ return `\u201C${file.name}\u201D can\u2019t be attached \u2014 file upload is disabled for this assistant.`;
245
+ }
206
246
  if (!extensions.includes(extensionOf(file.name))) {
207
- return `\u201C${file.name}\u201D isn\u2019t a supported text file. Allowed: ${extensions.join(", ")}.`;
247
+ return `\u201C${file.name}\u201D isn\u2019t a supported file. Allowed: ${extensions.join(", ")}.`;
208
248
  }
209
249
  const limit = maxSizeMb(config);
210
250
  if (file.size > limit * 1024 * 1024) {
@@ -218,42 +258,386 @@ function formatBytes(bytes) {
218
258
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
219
259
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
220
260
  }
261
+ function AgentImageLightbox({ src, alt, onClose }) {
262
+ React11.useEffect(() => {
263
+ if (!src) return;
264
+ const onKey = (event) => {
265
+ if (event.key === "Escape") onClose();
266
+ };
267
+ document.addEventListener("keydown", onKey);
268
+ return () => document.removeEventListener("keydown", onKey);
269
+ }, [src, onClose]);
270
+ if (typeof document === "undefined") return null;
271
+ return createPortal(
272
+ /* @__PURE__ */ jsx(AnimatePresence, { children: src ? /* @__PURE__ */ jsxs(
273
+ motion.div,
274
+ {
275
+ animate: { opacity: 1 },
276
+ "aria-modal": "true",
277
+ className: cn(
278
+ "fixed inset-0 z-[2147483000] flex items-center justify-center bg-black/80 p-6 backdrop-blur-sm"
279
+ ),
280
+ exit: { opacity: 0 },
281
+ initial: { opacity: 0 },
282
+ onClick: onClose,
283
+ role: "dialog",
284
+ transition: { duration: 0.15 },
285
+ children: [
286
+ /* @__PURE__ */ jsx(
287
+ "button",
288
+ {
289
+ "aria-label": "Close image",
290
+ className: "absolute top-4 right-4 flex size-10 cursor-pointer items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20 [&_svg]:size-5",
291
+ onClick: onClose,
292
+ type: "button",
293
+ children: /* @__PURE__ */ jsx(XIcon, {})
294
+ }
295
+ ),
296
+ /* @__PURE__ */ jsx(
297
+ motion.img,
298
+ {
299
+ alt: alt ?? "Attached image",
300
+ animate: { scale: 1, opacity: 1 },
301
+ className: "max-h-full max-w-full rounded-lg object-contain shadow-2xl",
302
+ exit: { scale: 0.96, opacity: 0 },
303
+ initial: { scale: 0.96, opacity: 0 },
304
+ onClick: (event) => event.stopPropagation(),
305
+ src,
306
+ transition: { duration: 0.15 }
307
+ }
308
+ )
309
+ ]
310
+ }
311
+ ) : null }),
312
+ document.body
313
+ );
314
+ }
315
+ var BAR_COUNT = 40;
316
+ function formatClock(totalSeconds) {
317
+ const minutes = Math.floor(totalSeconds / 60);
318
+ const seconds = totalSeconds % 60;
319
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
320
+ }
321
+ function pickMimeType() {
322
+ if (typeof MediaRecorder === "undefined") return "";
323
+ for (const type of ["audio/webm", "audio/mp4", "audio/ogg"]) {
324
+ if (MediaRecorder.isTypeSupported(type)) return type;
325
+ }
326
+ return "";
327
+ }
328
+ function AgentVoiceRecorder({ maxSeconds, transcribe, onDone, onCancel, onError }) {
329
+ const [phase, setPhase] = React11.useState("recording");
330
+ const [elapsed, setElapsed] = React11.useState(0);
331
+ const [playing, setPlaying] = React11.useState(false);
332
+ const recorderRef = React11.useRef(null);
333
+ const streamRef = React11.useRef(null);
334
+ const chunksRef = React11.useRef([]);
335
+ const blobRef = React11.useRef(null);
336
+ const audioUrlRef = React11.useRef(null);
337
+ const audioRef = React11.useRef(null);
338
+ const timerRef = React11.useRef(null);
339
+ const canvasRef = React11.useRef(null);
340
+ const audioContextRef = React11.useRef(null);
341
+ const analyserRef = React11.useRef(null);
342
+ const rafRef = React11.useRef(null);
343
+ const levelsRef = React11.useRef(new Array(BAR_COUNT).fill(0));
344
+ const onErrorRef = React11.useRef(onError);
345
+ onErrorRef.current = onError;
346
+ const stopMeter = React11.useCallback(() => {
347
+ if (rafRef.current != null) {
348
+ cancelAnimationFrame(rafRef.current);
349
+ rafRef.current = null;
350
+ }
351
+ analyserRef.current = null;
352
+ const ctx = audioContextRef.current;
353
+ audioContextRef.current = null;
354
+ if (ctx && ctx.state !== "closed") void ctx.close().catch(() => {
355
+ });
356
+ }, []);
357
+ const stopTracks = React11.useCallback(() => {
358
+ streamRef.current?.getTracks().forEach((track) => track.stop());
359
+ streamRef.current = null;
360
+ if (timerRef.current) {
361
+ clearInterval(timerRef.current);
362
+ timerRef.current = null;
363
+ }
364
+ stopMeter();
365
+ }, [stopMeter]);
366
+ const startMeter = React11.useCallback((stream) => {
367
+ const Ctor = typeof window === "undefined" ? void 0 : window.AudioContext ?? window.webkitAudioContext;
368
+ if (!Ctor) return;
369
+ let context;
370
+ try {
371
+ context = new Ctor();
372
+ } catch {
373
+ return;
374
+ }
375
+ audioContextRef.current = context;
376
+ const source = context.createMediaStreamSource(stream);
377
+ const analyser = context.createAnalyser();
378
+ analyser.fftSize = 1024;
379
+ analyser.smoothingTimeConstant = 0.75;
380
+ source.connect(analyser);
381
+ analyserRef.current = analyser;
382
+ levelsRef.current = new Array(BAR_COUNT).fill(0);
383
+ const buffer = new Uint8Array(analyser.fftSize);
384
+ const draw = () => {
385
+ const canvas = canvasRef.current;
386
+ const activeAnalyser = analyserRef.current;
387
+ if (!canvas || !activeAnalyser) return;
388
+ const ctx2d = canvas.getContext("2d");
389
+ if (!ctx2d) return;
390
+ activeAnalyser.getByteTimeDomainData(buffer);
391
+ let sumSquares = 0;
392
+ for (let i = 0; i < buffer.length; i += 1) {
393
+ const centered = (buffer[i] - 128) / 128;
394
+ sumSquares += centered * centered;
395
+ }
396
+ const rms = Math.sqrt(sumSquares / buffer.length);
397
+ const level = Math.min(1, rms * 3.2);
398
+ const levels = levelsRef.current;
399
+ levels.push(level);
400
+ levels.shift();
401
+ const dpr = Math.min(2, typeof window === "undefined" ? 1 : window.devicePixelRatio || 1);
402
+ const cssWidth = canvas.clientWidth || 1;
403
+ const cssHeight = canvas.clientHeight || 1;
404
+ const pxWidth = Math.round(cssWidth * dpr);
405
+ const pxHeight = Math.round(cssHeight * dpr);
406
+ if (canvas.width !== pxWidth) canvas.width = pxWidth;
407
+ if (canvas.height !== pxHeight) canvas.height = pxHeight;
408
+ ctx2d.clearRect(0, 0, pxWidth, pxHeight);
409
+ ctx2d.fillStyle = getComputedStyle(canvas).color || "currentColor";
410
+ const gap = Math.max(1, pxWidth / BAR_COUNT / 3);
411
+ const barWidth = Math.max(1, (pxWidth - gap * (BAR_COUNT - 1)) / BAR_COUNT);
412
+ const midY = pxHeight / 2;
413
+ const minBar = Math.max(1, dpr);
414
+ const radius = barWidth / 2;
415
+ for (let i = 0; i < BAR_COUNT; i += 1) {
416
+ const value = levels[i];
417
+ const barHeight = Math.max(minBar, value * pxHeight);
418
+ const x = i * (barWidth + gap);
419
+ const y = midY - barHeight / 2;
420
+ if (typeof ctx2d.roundRect === "function") {
421
+ ctx2d.beginPath();
422
+ ctx2d.roundRect(x, y, barWidth, barHeight, Math.min(radius, barHeight / 2));
423
+ ctx2d.fill();
424
+ } else {
425
+ ctx2d.fillRect(x, y, barWidth, barHeight);
426
+ }
427
+ }
428
+ rafRef.current = requestAnimationFrame(draw);
429
+ };
430
+ rafRef.current = requestAnimationFrame(draw);
431
+ }, []);
432
+ React11.useEffect(() => {
433
+ let cancelled = false;
434
+ void (async () => {
435
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
436
+ onErrorRef.current("Voice recording isn't supported in this browser.");
437
+ onCancel();
438
+ return;
439
+ }
440
+ try {
441
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
442
+ if (cancelled) {
443
+ stream.getTracks().forEach((track) => track.stop());
444
+ return;
445
+ }
446
+ streamRef.current = stream;
447
+ startMeter(stream);
448
+ const mimeType = pickMimeType();
449
+ const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : void 0);
450
+ recorderRef.current = recorder;
451
+ chunksRef.current = [];
452
+ recorder.ondataavailable = (event) => {
453
+ if (event.data.size > 0) chunksRef.current.push(event.data);
454
+ };
455
+ recorder.onstop = () => {
456
+ const blob = new Blob(chunksRef.current, { type: mimeType || "audio/webm" });
457
+ blobRef.current = blob;
458
+ if (audioUrlRef.current) URL.revokeObjectURL(audioUrlRef.current);
459
+ audioUrlRef.current = URL.createObjectURL(blob);
460
+ setPhase("review");
461
+ };
462
+ recorder.start();
463
+ timerRef.current = setInterval(() => {
464
+ setElapsed((current) => {
465
+ const next = current + 1;
466
+ if (next >= maxSeconds) stopRecording();
467
+ return next;
468
+ });
469
+ }, 1e3);
470
+ } catch {
471
+ onErrorRef.current("Microphone access was blocked. Enable it to record.");
472
+ onCancel();
473
+ }
474
+ })();
475
+ return () => {
476
+ cancelled = true;
477
+ stopTracks();
478
+ if (recorderRef.current && recorderRef.current.state !== "inactive") {
479
+ try {
480
+ recorderRef.current.stop();
481
+ } catch {
482
+ }
483
+ }
484
+ if (audioUrlRef.current) URL.revokeObjectURL(audioUrlRef.current);
485
+ };
486
+ }, []);
487
+ function stopRecording() {
488
+ if (timerRef.current) {
489
+ clearInterval(timerRef.current);
490
+ timerRef.current = null;
491
+ }
492
+ const recorder = recorderRef.current;
493
+ if (recorder && recorder.state !== "inactive") recorder.stop();
494
+ stopTracks();
495
+ }
496
+ const togglePlayback = () => {
497
+ const audio = audioRef.current;
498
+ if (!audio) return;
499
+ if (audio.paused) {
500
+ void audio.play();
501
+ setPlaying(true);
502
+ } else {
503
+ audio.pause();
504
+ setPlaying(false);
505
+ }
506
+ };
507
+ const confirm = async () => {
508
+ const blob = blobRef.current;
509
+ if (!blob || blob.size === 0) {
510
+ onError("The recording was empty.");
511
+ onCancel();
512
+ return;
513
+ }
514
+ setPhase("transcribing");
515
+ try {
516
+ const text = await transcribe(blob);
517
+ onDone(text.trim());
518
+ } catch {
519
+ onError("Transcription failed. Please try again.");
520
+ setPhase("review");
521
+ }
522
+ };
523
+ return /* @__PURE__ */ jsxs(
524
+ motion.div,
525
+ {
526
+ animate: { opacity: 1, y: 0 },
527
+ className: "flex min-h-8 flex-1 items-center gap-2 px-1",
528
+ initial: { opacity: 0, y: 4 },
529
+ transition: { duration: 0.15 },
530
+ children: [
531
+ phase === "recording" ? /* @__PURE__ */ jsxs(Fragment, { children: [
532
+ /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-2 text-sm", children: [
533
+ /* @__PURE__ */ jsx(
534
+ motion.span,
535
+ {
536
+ animate: { opacity: [1, 0.35, 1] },
537
+ className: "size-2.5 rounded-full bg-destructive",
538
+ transition: { duration: 1.2, repeat: Infinity }
539
+ }
540
+ ),
541
+ /* @__PURE__ */ jsx("span", { className: "font-medium tabular-nums", children: formatClock(elapsed) })
542
+ ] }),
543
+ /* @__PURE__ */ jsx("canvas", { "aria-hidden": true, className: "h-6 min-w-0 flex-1 text-foreground/80", ref: canvasRef }),
544
+ /* @__PURE__ */ jsx(Button, { "aria-label": "Discard recording", onClick: onCancel, size: "icon-sm", type: "button", variant: "ghost", children: /* @__PURE__ */ jsx(Trash2Icon, {}) }),
545
+ /* @__PURE__ */ jsx(Button, { "aria-label": "Stop recording", onClick: stopRecording, size: "icon-sm", type: "button", children: /* @__PURE__ */ jsx(SquareIcon, {}) })
546
+ ] }) : null,
547
+ phase === "review" ? /* @__PURE__ */ jsxs(Fragment, { children: [
548
+ /* @__PURE__ */ jsx("audio", { onEnded: () => setPlaying(false), ref: audioRef, src: audioUrlRef.current ?? void 0 }),
549
+ /* @__PURE__ */ jsx(Button, { "aria-label": playing ? "Pause" : "Play recording", onClick: togglePlayback, size: "icon-sm", type: "button", variant: "ghost", children: playing ? /* @__PURE__ */ jsx(PauseIcon, {}) : /* @__PURE__ */ jsx(PlayIcon, {}) }),
550
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm tabular-nums", children: formatClock(elapsed) }),
551
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
552
+ /* @__PURE__ */ jsx(Button, { "aria-label": "Discard recording", onClick: onCancel, size: "icon-sm", type: "button", variant: "ghost", children: /* @__PURE__ */ jsx(Trash2Icon, {}) }),
553
+ /* @__PURE__ */ jsx(Button, { "aria-label": "Use recording", className: "rounded-full before:rounded-full", onClick: () => void confirm(), size: "icon-sm", type: "button", children: /* @__PURE__ */ jsx(CheckIcon, {}) })
554
+ ] }) : null,
555
+ phase === "transcribing" ? /* @__PURE__ */ jsxs("span", { className: cn("flex flex-1 items-center gap-2 text-muted-foreground text-sm"), children: [
556
+ /* @__PURE__ */ jsx(Loader2Icon, { className: "size-4 animate-spin" }),
557
+ "Transcribing\u2026"
558
+ ] }) : null
559
+ ]
560
+ }
561
+ );
562
+ }
563
+ function appendText(current, addition) {
564
+ if (!addition) return current;
565
+ if (!current) return addition;
566
+ return /\s$/.test(current) ? current + addition : `${current} ${addition}`;
567
+ }
221
568
  function AgentComposer({
222
569
  acceptAttribute: acceptAttribute2,
223
570
  busy,
224
571
  className,
225
572
  disabled,
226
573
  files,
574
+ onError,
227
575
  onPickFiles,
228
576
  onRemoveFile,
229
577
  onSubmit,
578
+ onTranscribe,
230
579
  onValueChange,
231
580
  placeholder = "Ask anything\u2026",
232
- value
581
+ value,
582
+ voiceEnabled,
583
+ voiceMaxSeconds = 600
233
584
  }) {
234
- const textareaRef = React8.useRef(null);
235
- const inputRef = React8.useRef(null);
585
+ const textareaRef = React11.useRef(null);
586
+ const inputRef = React11.useRef(null);
587
+ const [recording, setRecording] = React11.useState(false);
588
+ const [lightboxSrc, setLightboxSrc] = React11.useState(null);
236
589
  const canSubmit = !disabled && !busy && (value.trim().length > 0 || files.length > 0);
237
- const submit = React8.useCallback(() => {
590
+ const voiceAvailable = Boolean(voiceEnabled && onTranscribe);
591
+ const submit = React11.useCallback(() => {
238
592
  if (!canSubmit) return;
239
593
  onSubmit(value.trim());
240
594
  textareaRef.current?.focus();
241
595
  }, [canSubmit, onSubmit, value]);
242
- React8.useEffect(() => {
596
+ React11.useEffect(() => {
243
597
  if (!disabled) textareaRef.current?.focus();
244
598
  }, [disabled]);
245
- React8.useEffect(() => {
599
+ React11.useEffect(() => {
246
600
  const textarea = textareaRef.current;
247
601
  if (!textarea) return;
248
602
  textarea.style.height = "auto";
249
603
  textarea.style.height = `${Math.min(textarea.scrollHeight, 144)}px`;
250
604
  }, [value]);
605
+ const previews = React11.useMemo(
606
+ () => files.map((file, originalIndex) => {
607
+ const image = isImageFile(file);
608
+ return { file, image, originalIndex, url: image ? URL.createObjectURL(file) : null };
609
+ }),
610
+ [files]
611
+ );
612
+ React11.useEffect(
613
+ () => () => {
614
+ previews.forEach((preview) => preview.url && URL.revokeObjectURL(preview.url));
615
+ },
616
+ [previews]
617
+ );
618
+ const ordered = React11.useMemo(
619
+ () => [...previews].sort((a, b) => Number(b.image) - Number(a.image)),
620
+ [previews]
621
+ );
251
622
  const handlePick = (event) => {
252
623
  const picked = Array.from(event.target.files ?? []);
253
624
  if (picked.length) onPickFiles?.(picked);
254
625
  event.target.value = "";
255
626
  };
256
- return /* @__PURE__ */ jsx(
627
+ const handlePaste = (event) => {
628
+ if (!onPickFiles) return;
629
+ const images = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
630
+ if (images.length) {
631
+ event.preventDefault();
632
+ onPickFiles(images);
633
+ }
634
+ };
635
+ const finishRecording = (text) => {
636
+ setRecording(false);
637
+ if (text) onValueChange(appendText(value, text));
638
+ setTimeout(() => textareaRef.current?.focus(), 0);
639
+ };
640
+ return /* @__PURE__ */ jsxs(
257
641
  "form",
258
642
  {
259
643
  className: cn("shrink-0 bg-background p-3 pt-1.5", className),
@@ -262,96 +646,148 @@ function AgentComposer({
262
646
  event.preventDefault();
263
647
  submit();
264
648
  },
265
- children: /* @__PURE__ */ jsxs(
266
- "div",
267
- {
268
- className: cn(
269
- "flex flex-col gap-1.5 rounded-2xl border border-input bg-popover p-1.5 shadow-xs transition-[border-color,box-shadow] duration-150 focus-within:border-ring/64 focus-within:ring-2 focus-within:ring-ring/24 dark:bg-input/32",
270
- disabled && "opacity-64"
271
- ),
272
- children: [
273
- files.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pt-1", children: files.map((file, index) => /* @__PURE__ */ jsxs(
274
- "span",
275
- {
276
- className: "flex max-w-52 items-center gap-1.5 rounded-lg border border-border/72 bg-muted/48 py-1 pr-1 pl-2 text-xs",
277
- children: [
278
- /* @__PURE__ */ jsx(FileTextIcon, { className: "size-3.5 shrink-0 text-muted-foreground" }),
279
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: file.name }),
280
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", children: formatBytes(file.size) }),
649
+ children: [
650
+ /* @__PURE__ */ jsxs(
651
+ "div",
652
+ {
653
+ className: cn(
654
+ "flex flex-col gap-1.5 rounded-2xl border border-input bg-popover p-1.5 shadow-xs transition-[border-color,box-shadow] duration-150 focus-within:border-ring/64 focus-within:ring-2 focus-within:ring-ring/24 dark:bg-input/32",
655
+ disabled && "opacity-64"
656
+ ),
657
+ children: [
658
+ files.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pt-1", children: ordered.map(
659
+ (preview) => preview.image && preview.url ? /* @__PURE__ */ jsxs("span", { className: "group relative", children: [
660
+ /* @__PURE__ */ jsx(
661
+ "button",
662
+ {
663
+ "aria-label": `Preview ${preview.file.name}`,
664
+ className: "block size-14 cursor-zoom-in overflow-hidden rounded-lg border border-border/72",
665
+ onClick: () => setLightboxSrc(preview.url),
666
+ type: "button",
667
+ children: /* @__PURE__ */ jsx("img", { alt: preview.file.name, className: "size-full object-cover", src: preview.url })
668
+ }
669
+ ),
281
670
  onRemoveFile ? /* @__PURE__ */ jsx(
282
671
  "button",
283
672
  {
284
- "aria-label": `Remove ${file.name}`,
285
- className: "flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground hover:bg-background hover:text-foreground [&_svg]:size-3",
286
- onClick: () => onRemoveFile(index),
673
+ "aria-label": `Remove ${preview.file.name}`,
674
+ className: "-right-1.5 -top-1.5 absolute flex size-5 cursor-pointer items-center justify-center rounded-full border border-border bg-background text-muted-foreground shadow-sm hover:text-foreground [&_svg]:size-3",
675
+ onClick: () => onRemoveFile(preview.originalIndex),
287
676
  type: "button",
288
677
  children: /* @__PURE__ */ jsx(XIcon, {})
289
678
  }
290
679
  ) : null
291
- ]
292
- },
293
- `${file.name}-${index}`
294
- )) }) : null,
295
- /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-1.5", children: [
296
- onPickFiles ? /* @__PURE__ */ jsxs(Fragment, { children: [
680
+ ] }, `${preview.file.name}-${preview.originalIndex}`) : /* @__PURE__ */ jsxs(
681
+ "span",
682
+ {
683
+ className: "flex max-w-52 items-center gap-1.5 rounded-lg border border-border/72 bg-muted/48 py-1 pr-1 pl-2 text-xs",
684
+ children: [
685
+ /* @__PURE__ */ jsx(FileTextIcon, { className: "size-3.5 shrink-0 text-muted-foreground" }),
686
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: preview.file.name }),
687
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", children: formatBytes(preview.file.size) }),
688
+ onRemoveFile ? /* @__PURE__ */ jsx(
689
+ "button",
690
+ {
691
+ "aria-label": `Remove ${preview.file.name}`,
692
+ className: "flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground hover:bg-background hover:text-foreground [&_svg]:size-3",
693
+ onClick: () => onRemoveFile(preview.originalIndex),
694
+ type: "button",
695
+ children: /* @__PURE__ */ jsx(XIcon, {})
696
+ }
697
+ ) : null
698
+ ]
699
+ },
700
+ `${preview.file.name}-${preview.originalIndex}`
701
+ )
702
+ ) }) : null,
703
+ recording && onTranscribe ? /* @__PURE__ */ jsx(
704
+ AgentVoiceRecorder,
705
+ {
706
+ maxSeconds: voiceMaxSeconds,
707
+ onCancel: () => {
708
+ setRecording(false);
709
+ setTimeout(() => textareaRef.current?.focus(), 0);
710
+ },
711
+ onDone: finishRecording,
712
+ onError: (message) => onError?.(message),
713
+ transcribe: onTranscribe
714
+ }
715
+ ) : /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-1.5", children: [
716
+ onPickFiles ? /* @__PURE__ */ jsxs(Fragment, { children: [
717
+ /* @__PURE__ */ jsx(
718
+ "input",
719
+ {
720
+ accept: acceptAttribute2,
721
+ className: "hidden",
722
+ multiple: true,
723
+ onChange: handlePick,
724
+ ref: inputRef,
725
+ type: "file"
726
+ }
727
+ ),
728
+ /* @__PURE__ */ jsx(
729
+ Button,
730
+ {
731
+ "aria-label": "Attach files",
732
+ className: "shrink-0 self-center",
733
+ disabled,
734
+ onClick: () => inputRef.current?.click(),
735
+ size: "icon-sm",
736
+ type: "button",
737
+ variant: "ghost",
738
+ children: /* @__PURE__ */ jsx(PaperclipIcon, {})
739
+ }
740
+ )
741
+ ] }) : null,
297
742
  /* @__PURE__ */ jsx(
298
- "input",
743
+ "textarea",
299
744
  {
300
- accept: acceptAttribute2,
301
- className: "hidden",
302
- multiple: true,
303
- onChange: handlePick,
304
- ref: inputRef,
305
- type: "file"
745
+ className: "max-h-36 min-h-8 flex-1 resize-none self-center bg-transparent py-1.5 pl-1.5 text-sm outline-none placeholder:text-muted-foreground",
746
+ disabled,
747
+ onChange: (event) => onValueChange(event.target.value),
748
+ onKeyDown: (event) => {
749
+ if (event.key === "Enter" && !event.shiftKey) {
750
+ event.preventDefault();
751
+ submit();
752
+ }
753
+ },
754
+ onPaste: onPickFiles ? handlePaste : void 0,
755
+ placeholder,
756
+ ref: textareaRef,
757
+ rows: 1,
758
+ value
306
759
  }
307
760
  ),
308
- /* @__PURE__ */ jsx(
761
+ voiceAvailable ? /* @__PURE__ */ jsx(
309
762
  Button,
310
763
  {
311
- "aria-label": "Attach files",
764
+ "aria-label": "Record voice",
312
765
  className: "shrink-0 self-center",
313
766
  disabled,
314
- onClick: () => inputRef.current?.click(),
767
+ onClick: () => setRecording(true),
315
768
  size: "icon-sm",
316
769
  type: "button",
317
770
  variant: "ghost",
318
- children: /* @__PURE__ */ jsx(PaperclipIcon, {})
771
+ children: /* @__PURE__ */ jsx(MicIcon, {})
772
+ }
773
+ ) : null,
774
+ /* @__PURE__ */ jsx(
775
+ Button,
776
+ {
777
+ "aria-label": "Send message",
778
+ className: "shrink-0 rounded-full before:rounded-full",
779
+ disabled: !canSubmit,
780
+ size: "icon-sm",
781
+ type: "submit",
782
+ children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
319
783
  }
320
784
  )
321
- ] }) : null,
322
- /* @__PURE__ */ jsx(
323
- "textarea",
324
- {
325
- className: "max-h-36 min-h-8 flex-1 resize-none self-center bg-transparent py-1.5 pl-1.5 text-sm outline-none placeholder:text-muted-foreground",
326
- disabled,
327
- onChange: (event) => onValueChange(event.target.value),
328
- onKeyDown: (event) => {
329
- if (event.key === "Enter" && !event.shiftKey) {
330
- event.preventDefault();
331
- submit();
332
- }
333
- },
334
- placeholder,
335
- ref: textareaRef,
336
- rows: 1,
337
- value
338
- }
339
- ),
340
- /* @__PURE__ */ jsx(
341
- Button,
342
- {
343
- "aria-label": "Send message",
344
- className: "shrink-0 rounded-full before:rounded-full",
345
- disabled: !canSubmit,
346
- size: "icon-sm",
347
- type: "submit",
348
- children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
349
- }
350
- )
351
- ] })
352
- ]
353
- }
354
- )
785
+ ] })
786
+ ]
787
+ }
788
+ ),
789
+ /* @__PURE__ */ jsx(AgentImageLightbox, { onClose: () => setLightboxSrc(null), src: lightboxSrc })
790
+ ]
355
791
  }
356
792
  );
357
793
  }
@@ -424,15 +860,15 @@ function groupMessages(messages) {
424
860
  return items;
425
861
  }
426
862
  function ChatScroller({ children, className, ...props }) {
427
- const viewportRef = React8.useRef(null);
428
- const pinnedRef = React8.useRef(true);
429
- const handleScroll = React8.useCallback(() => {
863
+ const viewportRef = React11.useRef(null);
864
+ const pinnedRef = React11.useRef(true);
865
+ const handleScroll = React11.useCallback(() => {
430
866
  const viewport = viewportRef.current;
431
867
  if (!viewport) return;
432
868
  const distanceFromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
433
869
  pinnedRef.current = distanceFromBottom < 64;
434
870
  }, []);
435
- React8.useEffect(() => {
871
+ React11.useEffect(() => {
436
872
  const viewport = viewportRef.current;
437
873
  if (!viewport || !pinnedRef.current) return;
438
874
  viewport.scrollTop = viewport.scrollHeight;
@@ -450,14 +886,14 @@ function ChatScroller({ children, className, ...props }) {
450
886
  );
451
887
  }
452
888
  function CopyButton({ className, text }) {
453
- const [copied, setCopied] = React8.useState(false);
454
- const timerRef = React8.useRef(null);
455
- React8.useEffect(() => {
889
+ const [copied, setCopied] = React11.useState(false);
890
+ const timerRef = React11.useRef(null);
891
+ React11.useEffect(() => {
456
892
  return () => {
457
893
  if (timerRef.current) clearTimeout(timerRef.current);
458
894
  };
459
895
  }, []);
460
- const copy = React8.useCallback(async () => {
896
+ const copy = React11.useCallback(async () => {
461
897
  try {
462
898
  await navigator.clipboard.writeText(text);
463
899
  } catch {
@@ -488,11 +924,26 @@ function CopyButton({ className, text }) {
488
924
  /* @__PURE__ */ jsx(TooltipContent, { children: copied ? "Copied!" : "Copy" })
489
925
  ] });
490
926
  }
927
+ var isImageAttachment = (format) => IMAGE_ACCEPT.includes(`.${format.toLowerCase()}`);
491
928
  function MessageItem({ message }) {
929
+ const [lightboxSrc, setLightboxSrc] = React11.useState(null);
492
930
  if (message.role === "user") {
493
931
  const attachments = message.attachments ?? [];
932
+ const images = attachments.filter((a) => isImageAttachment(a.format) && a.url);
933
+ const others = attachments.filter((a) => !isImageAttachment(a.format) || !a.url);
494
934
  return /* @__PURE__ */ jsxs(ChatMessage, { from: "user", children: [
495
- attachments.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-end gap-1.5", children: attachments.map((attachment) => /* @__PURE__ */ jsxs(
935
+ images.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-end gap-1.5", children: images.map((attachment) => /* @__PURE__ */ jsx(
936
+ "button",
937
+ {
938
+ "aria-label": `Preview ${attachment.filename}`,
939
+ className: "block size-20 cursor-zoom-in overflow-hidden rounded-lg border border-border/72",
940
+ onClick: () => setLightboxSrc(attachment.url ?? null),
941
+ type: "button",
942
+ children: /* @__PURE__ */ jsx("img", { alt: attachment.filename, className: "size-full object-cover", src: attachment.url ?? void 0 })
943
+ },
944
+ attachment.id
945
+ )) }) : null,
946
+ others.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-end gap-1.5", children: others.map((attachment) => /* @__PURE__ */ jsxs(
496
947
  "span",
497
948
  {
498
949
  className: "flex max-w-52 items-center gap-1.5 rounded-lg border border-border/72 bg-muted/48 px-2 py-1 text-muted-foreground text-xs",
@@ -503,7 +954,8 @@ function MessageItem({ message }) {
503
954
  },
504
955
  attachment.id
505
956
  )) }) : null,
506
- message.content ? /* @__PURE__ */ jsx(ChatMessageBubble, { from: "user", children: message.content }) : null
957
+ message.content ? /* @__PURE__ */ jsx(ChatMessageBubble, { from: "user", children: message.content }) : null,
958
+ /* @__PURE__ */ jsx(AgentImageLightbox, { onClose: () => setLightboxSrc(null), src: lightboxSrc })
507
959
  ] });
508
960
  }
509
961
  if (message.role === "assistant") {
@@ -534,8 +986,8 @@ var DEFAULT_PHRASES = [
534
986
  ];
535
987
  function ThinkingIndicator({ className, intervalMs = 1800, phrases }) {
536
988
  const words = phrases && phrases.length ? phrases : DEFAULT_PHRASES;
537
- const [index, setIndex] = React8.useState(0);
538
- React8.useEffect(() => {
989
+ const [index, setIndex] = React11.useState(0);
990
+ React11.useEffect(() => {
539
991
  setIndex(0);
540
992
  const timer = setInterval(() => {
541
993
  setIndex((current) => (current + 1) % words.length);
@@ -797,7 +1249,7 @@ function DownloadButton({ block, size = "sm" }) {
797
1249
  );
798
1250
  }
799
1251
  function DocumentViewer({ block, onClose }) {
800
- React8.useEffect(() => {
1252
+ React11.useEffect(() => {
801
1253
  const onKey = (event) => event.key === "Escape" && onClose();
802
1254
  window.addEventListener("keydown", onKey);
803
1255
  return () => window.removeEventListener("keydown", onKey);
@@ -822,7 +1274,7 @@ function DocumentViewer({ block, onClose }) {
822
1274
  ] });
823
1275
  }
824
1276
  function DocumentCard({ block }) {
825
- const [open, setOpen] = React8.useState(false);
1277
+ const [open, setOpen] = React11.useState(false);
826
1278
  const previewLines = (block.preview || "").split("\n").slice(0, 6).join("\n");
827
1279
  return /* @__PURE__ */ jsxs("div", { className: "w-full max-w-full overflow-hidden rounded-xl border border-border bg-card shadow-xs", children: [
828
1280
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-border/72 border-b px-3 py-2", children: [
@@ -944,7 +1396,7 @@ function StepDetail({
944
1396
  ] });
945
1397
  }
946
1398
  function ToolActivityGroup({ describeToolCall, steps }) {
947
- const [open, setOpen] = React8.useState(false);
1399
+ const [open, setOpen] = React11.useState(false);
948
1400
  if (steps.length === 0) return null;
949
1401
  const last = steps[steps.length - 1];
950
1402
  const anyRunning = steps.some((step) => step.status === "running");
@@ -1048,24 +1500,28 @@ function AgentPanel({
1048
1500
  onClose,
1049
1501
  onToggleAgentMode
1050
1502
  }) {
1051
- const [draft, setDraft] = React8.useState("");
1052
- const [view, setView] = React8.useState("chat");
1053
- const [files, setFiles] = React8.useState([]);
1054
- const [uploadError, setUploadError] = React8.useState(null);
1055
- const [dragging, setDragging] = React8.useState(false);
1056
- const dragDepth = React8.useRef(0);
1503
+ const [draft, setDraft] = React11.useState("");
1504
+ const [view, setView] = React11.useState("chat");
1505
+ const [files, setFiles] = React11.useState([]);
1506
+ const [uploadError, setUploadError] = React11.useState(null);
1507
+ const [dragging, setDragging] = React11.useState(false);
1508
+ const dragDepth = React11.useRef(0);
1057
1509
  const agentName = config.agentName ?? chat.agentInfo?.name ?? "Assistant";
1058
- const uploadEnabled = fileUploadEnabled(config);
1510
+ const caps = {
1511
+ visionEnabled: chat.agentInfo?.vision_enabled,
1512
+ visionMaxMb: chat.agentInfo?.vision_max_file_mb
1513
+ };
1514
+ const uploadEnabled = attachmentsEnabled(config, caps);
1059
1515
  const suggestions = config.suggestions ?? [
1060
1516
  "What can you help me with?",
1061
1517
  "Give me a quick summary of my data."
1062
1518
  ];
1063
- const addFiles = React8.useCallback(
1519
+ const addFiles = React11.useCallback(
1064
1520
  (incoming) => {
1065
1521
  const accepted = [];
1066
1522
  let rejection = null;
1067
1523
  for (const file of incoming) {
1068
- const error = validateFile(config, file);
1524
+ const error = validateFile(config, file, caps);
1069
1525
  if (error) {
1070
1526
  rejection = error;
1071
1527
  continue;
@@ -1075,12 +1531,12 @@ function AgentPanel({
1075
1531
  setUploadError(rejection);
1076
1532
  if (accepted.length) setFiles((current) => [...current, ...accepted].slice(0, 10));
1077
1533
  },
1078
- [config]
1534
+ [config, caps.visionEnabled, caps.visionMaxMb]
1079
1535
  );
1080
- const removeFile = React8.useCallback((index) => {
1536
+ const removeFile = React11.useCallback((index) => {
1081
1537
  setFiles((current) => current.filter((_, i) => i !== index));
1082
1538
  }, []);
1083
- const handleSubmit = React8.useCallback(
1539
+ const handleSubmit = React11.useCallback(
1084
1540
  (text) => {
1085
1541
  const staged = files;
1086
1542
  setDraft("");
@@ -1090,7 +1546,7 @@ function AgentPanel({
1090
1546
  },
1091
1547
  [chat, files]
1092
1548
  );
1093
- const onDragEnter = React8.useCallback(
1549
+ const onDragEnter = React11.useCallback(
1094
1550
  (event) => {
1095
1551
  if (!uploadEnabled || !event.dataTransfer?.types?.includes("Files")) return;
1096
1552
  dragDepth.current += 1;
@@ -1098,12 +1554,12 @@ function AgentPanel({
1098
1554
  },
1099
1555
  [uploadEnabled]
1100
1556
  );
1101
- const onDragLeave = React8.useCallback((event) => {
1557
+ const onDragLeave = React11.useCallback((event) => {
1102
1558
  if (!event.dataTransfer?.types?.includes("Files")) return;
1103
1559
  dragDepth.current = Math.max(0, dragDepth.current - 1);
1104
1560
  if (dragDepth.current === 0) setDragging(false);
1105
1561
  }, []);
1106
- const onDrop = React8.useCallback(
1562
+ const onDrop = React11.useCallback(
1107
1563
  (event) => {
1108
1564
  if (!uploadEnabled) return;
1109
1565
  event.preventDefault();
@@ -1114,7 +1570,7 @@ function AgentPanel({
1114
1570
  },
1115
1571
  [addFiles, uploadEnabled]
1116
1572
  );
1117
- const openHistory = React8.useCallback(() => {
1573
+ const openHistory = React11.useCallback(() => {
1118
1574
  setView("history");
1119
1575
  void chat.refreshConversations();
1120
1576
  }, [chat]);
@@ -1137,8 +1593,8 @@ function AgentPanel({
1137
1593
  children: [
1138
1594
  dragging && view === "chat" ? /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 z-40 flex flex-col items-center justify-center gap-2 bg-background/85 backdrop-blur-sm", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 rounded-2xl border-2 border-primary/40 border-dashed px-8 py-6 text-center", children: [
1139
1595
  /* @__PURE__ */ jsx(UploadCloudIcon, { className: "size-8 text-primary" }),
1140
- /* @__PURE__ */ jsx("div", { className: "font-medium text-sm", children: "Drop text files to attach" }),
1141
- /* @__PURE__ */ jsx("div", { className: "text-muted-foreground text-xs", children: acceptAttribute(config).replaceAll(",", " \xB7 ") })
1596
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-sm", children: "Drop files to attach" }),
1597
+ /* @__PURE__ */ jsx("div", { className: "text-muted-foreground text-xs", children: acceptAttribute(config, caps).replaceAll(",", " \xB7 ") })
1142
1598
  ] }) }) : null,
1143
1599
  /* @__PURE__ */ jsxs(ChatHeader, { children: [
1144
1600
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2.5", children: [
@@ -1269,7 +1725,7 @@ function AgentPanel({
1269
1725
  const anchorKey = newMessageAnchorId ? grouped.find(
1270
1726
  (it) => it.kind !== "tool-activity" && it.message.id === newMessageAnchorId
1271
1727
  )?.key : void 0;
1272
- return grouped.map((item) => /* @__PURE__ */ jsxs(React8.Fragment, { children: [
1728
+ return grouped.map((item) => /* @__PURE__ */ jsxs(React11.Fragment, { children: [
1273
1729
  item.key === anchorKey ? /* @__PURE__ */ jsx(NewMessagesDivider, {}) : null,
1274
1730
  /* @__PURE__ */ jsx(
1275
1731
  motion.div,
@@ -1319,17 +1775,21 @@ function AgentPanel({
1319
1775
  /* @__PURE__ */ jsx(
1320
1776
  AgentComposer,
1321
1777
  {
1322
- acceptAttribute: acceptAttribute(config),
1778
+ acceptAttribute: acceptAttribute(config, caps),
1323
1779
  busy,
1324
1780
  className: cn(agentMode && "mx-auto w-full max-w-3xl"),
1325
1781
  disabled: chat.pendingApprovals.length > 0,
1326
1782
  files,
1783
+ onError: setUploadError,
1327
1784
  onPickFiles: uploadEnabled ? addFiles : void 0,
1328
1785
  onRemoveFile: removeFile,
1329
1786
  onSubmit: handleSubmit,
1787
+ onTranscribe: chat.transcribe,
1330
1788
  onValueChange: setDraft,
1331
1789
  placeholder: chat.pendingApprovals.length > 0 ? "Approve or decline the pending action first\u2026" : `Ask ${agentName}\u2026`,
1332
- value: draft
1790
+ value: draft,
1791
+ voiceEnabled: chat.agentInfo?.voice_enabled,
1792
+ voiceMaxSeconds: chat.agentInfo?.voice_max_seconds
1333
1793
  }
1334
1794
  )
1335
1795
  ] })
@@ -1365,16 +1825,16 @@ function buildOptimisticUserMessage(text, files) {
1365
1825
  };
1366
1826
  }
1367
1827
  function useAgentChat(config) {
1368
- const client = React8.useMemo(() => new AgentApiClient(config), [config]);
1369
- const [agentInfo, setAgentInfo] = React8.useState(null);
1370
- const [conversationId, setConversationId] = React8.useState(null);
1371
- const [conversations, setConversations] = React8.useState([]);
1372
- const [messages, setMessages] = React8.useState([]);
1373
- const [pendingApprovals, setPendingApprovals] = React8.useState([]);
1374
- const [sending, setSending] = React8.useState(false);
1375
- const [resolvingApprovalId, setResolvingApprovalId] = React8.useState(null);
1376
- const [error, setError] = React8.useState(null);
1377
- React8.useEffect(() => {
1828
+ const client = React11.useMemo(() => new AgentApiClient(config), [config]);
1829
+ const [agentInfo, setAgentInfo] = React11.useState(null);
1830
+ const [conversationId, setConversationId] = React11.useState(null);
1831
+ const [conversations, setConversations] = React11.useState([]);
1832
+ const [messages, setMessages] = React11.useState([]);
1833
+ const [pendingApprovals, setPendingApprovals] = React11.useState([]);
1834
+ const [sending, setSending] = React11.useState(false);
1835
+ const [resolvingApprovalId, setResolvingApprovalId] = React11.useState(null);
1836
+ const [error, setError] = React11.useState(null);
1837
+ React11.useEffect(() => {
1378
1838
  let cancelled = false;
1379
1839
  client.getConfig().then((info) => {
1380
1840
  if (!cancelled) setAgentInfo(info);
@@ -1384,11 +1844,11 @@ function useAgentChat(config) {
1384
1844
  cancelled = true;
1385
1845
  };
1386
1846
  }, [client]);
1387
- const handleFailure = React8.useCallback((err) => {
1847
+ const handleFailure = React11.useCallback((err) => {
1388
1848
  const message = err instanceof AgentApiError ? err.message : "Something went wrong. Please try again.";
1389
1849
  setError(message);
1390
1850
  }, []);
1391
- const applyResponse = React8.useCallback(
1851
+ const applyResponse = React11.useCallback(
1392
1852
  (response) => {
1393
1853
  if (response.status === "error" && !response.messages?.length) {
1394
1854
  setError(response.message ?? "The assistant could not answer.");
@@ -1410,7 +1870,7 @@ function useAgentChat(config) {
1410
1870
  },
1411
1871
  []
1412
1872
  );
1413
- const handleStreamEvent = React8.useCallback(
1873
+ const handleStreamEvent = React11.useCallback(
1414
1874
  (event, ctx) => {
1415
1875
  switch (event.type) {
1416
1876
  case "conversation":
@@ -1475,7 +1935,7 @@ function useAgentChat(config) {
1475
1935
  []
1476
1936
  );
1477
1937
  const streamingEnabled = config.streaming !== false && typeof ReadableStream !== "undefined";
1478
- const send = React8.useCallback(
1938
+ const send = React11.useCallback(
1479
1939
  async (text, files) => {
1480
1940
  if (sending) return;
1481
1941
  if (!text.trim() && !(files && files.length)) return;
@@ -1506,7 +1966,14 @@ function useAgentChat(config) {
1506
1966
  },
1507
1967
  [applyResponse, client, conversationId, handleFailure, handleStreamEvent, sending, streamingEnabled]
1508
1968
  );
1509
- const resolveApproval = React8.useCallback(
1969
+ const transcribe = React11.useCallback(
1970
+ async (audio) => {
1971
+ const { text } = await client.transcribe(audio);
1972
+ return text;
1973
+ },
1974
+ [client]
1975
+ );
1976
+ const resolveApproval = React11.useCallback(
1510
1977
  async (callId, approve) => {
1511
1978
  if (resolvingApprovalId) return;
1512
1979
  setResolvingApprovalId(callId);
@@ -1535,13 +2002,13 @@ function useAgentChat(config) {
1535
2002
  },
1536
2003
  [applyResponse, client, handleFailure, handleStreamEvent, resolvingApprovalId, streamingEnabled]
1537
2004
  );
1538
- const newChat = React8.useCallback(() => {
2005
+ const newChat = React11.useCallback(() => {
1539
2006
  setConversationId(null);
1540
2007
  setMessages([]);
1541
2008
  setPendingApprovals([]);
1542
2009
  setError(null);
1543
2010
  }, []);
1544
- const refreshConversations = React8.useCallback(async () => {
2011
+ const refreshConversations = React11.useCallback(async () => {
1545
2012
  try {
1546
2013
  const { conversations: list } = await client.listConversations();
1547
2014
  setConversations(list);
@@ -1551,7 +2018,7 @@ function useAgentChat(config) {
1551
2018
  return [];
1552
2019
  }
1553
2020
  }, [client, handleFailure]);
1554
- const openConversation = React8.useCallback(
2021
+ const openConversation = React11.useCallback(
1555
2022
  async (id) => {
1556
2023
  setError(null);
1557
2024
  try {
@@ -1565,7 +2032,7 @@ function useAgentChat(config) {
1565
2032
  },
1566
2033
  [client, handleFailure]
1567
2034
  );
1568
- const deleteConversation = React8.useCallback(
2035
+ const deleteConversation = React11.useCallback(
1569
2036
  async (id) => {
1570
2037
  try {
1571
2038
  await client.deleteConversation(id);
@@ -1577,7 +2044,7 @@ function useAgentChat(config) {
1577
2044
  },
1578
2045
  [client, conversationId, handleFailure, newChat]
1579
2046
  );
1580
- const clearError = React8.useCallback(() => setError(null), []);
2047
+ const clearError = React11.useCallback(() => setError(null), []);
1581
2048
  return {
1582
2049
  agentInfo,
1583
2050
  clearError,
@@ -1593,7 +2060,8 @@ function useAgentChat(config) {
1593
2060
  resolveApproval,
1594
2061
  resolvingApprovalId,
1595
2062
  send,
1596
- sending
2063
+ sending,
2064
+ transcribe
1597
2065
  };
1598
2066
  }
1599
2067
  var STORAGE_PREFIX = "cc-agent-always-approve:";
@@ -1612,11 +2080,11 @@ function readStored(clientId) {
1612
2080
  }
1613
2081
  }
1614
2082
  function useAlwaysApprove(clientId) {
1615
- const [approved, setApproved] = React8.useState(() => /* @__PURE__ */ new Set());
1616
- React8.useEffect(() => {
2083
+ const [approved, setApproved] = React11.useState(() => /* @__PURE__ */ new Set());
2084
+ React11.useEffect(() => {
1617
2085
  setApproved(readStored(clientId));
1618
2086
  }, [clientId]);
1619
- const persist = React8.useCallback(
2087
+ const persist = React11.useCallback(
1620
2088
  (next) => {
1621
2089
  setApproved(next);
1622
2090
  if (typeof window === "undefined") return;
@@ -1627,8 +2095,8 @@ function useAlwaysApprove(clientId) {
1627
2095
  },
1628
2096
  [clientId]
1629
2097
  );
1630
- const isAlwaysApproved = React8.useCallback((toolName) => approved.has(toolName), [approved]);
1631
- const setAlwaysApprove = React8.useCallback(
2098
+ const isAlwaysApproved = React11.useCallback((toolName) => approved.has(toolName), [approved]);
2099
+ const setAlwaysApprove = React11.useCallback(
1632
2100
  (toolName) => {
1633
2101
  if (approved.has(toolName)) return;
1634
2102
  const next = new Set(approved);
@@ -1637,7 +2105,7 @@ function useAlwaysApprove(clientId) {
1637
2105
  },
1638
2106
  [approved, persist]
1639
2107
  );
1640
- const clearAlwaysApprove = React8.useCallback(
2108
+ const clearAlwaysApprove = React11.useCallback(
1641
2109
  (toolName) => {
1642
2110
  if (!approved.has(toolName)) return;
1643
2111
  const next = new Set(approved);
@@ -1657,10 +2125,10 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1657
2125
  const position = config.appearance?.position ?? "bottom-right";
1658
2126
  const isControlled = controlledOpen !== void 0;
1659
2127
  const storageKey2 = `cc-agent-open:${config.clientId}`;
1660
- const [uncontrolledOpen, setUncontrolledOpen] = React8.useState(false);
2128
+ const [uncontrolledOpen, setUncontrolledOpen] = React11.useState(false);
1661
2129
  const open = controlledOpen ?? uncontrolledOpen;
1662
- const [bootstrapDone, setBootstrapDone] = React8.useState(isControlled);
1663
- const persist = React8.useCallback(
2130
+ const [bootstrapDone, setBootstrapDone] = React11.useState(isControlled);
2131
+ const persist = React11.useCallback(
1664
2132
  (next) => {
1665
2133
  if (isControlled || !behavior.persistOpenState) return;
1666
2134
  try {
@@ -1670,7 +2138,7 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1670
2138
  },
1671
2139
  [behavior.persistOpenState, isControlled, storageKey2]
1672
2140
  );
1673
- const setOpen = React8.useCallback(
2141
+ const setOpen = React11.useCallback(
1674
2142
  (next) => {
1675
2143
  setUncontrolledOpen(next);
1676
2144
  persist(next);
@@ -1678,14 +2146,14 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1678
2146
  },
1679
2147
  [onOpenChange, persist]
1680
2148
  );
1681
- const [agentMode, setAgentMode] = React8.useState(false);
2149
+ const [agentMode, setAgentMode] = React11.useState(false);
1682
2150
  const isDesktop = useMediaQuery("(min-width: 640px)", { defaultSSRValue: true, ssr: true });
1683
2151
  const fullscreen = !isDesktop || agentMode;
1684
2152
  const chat = useAgentChat(config);
1685
2153
  const alwaysApprove = useAlwaysApprove(config.clientId);
1686
2154
  const { messages, pendingApprovals, resolveApproval, resolvingApprovalId, sending } = chat;
1687
- const bootRef = React8.useRef(false);
1688
- React8.useEffect(() => {
2155
+ const bootRef = React11.useRef(false);
2156
+ React11.useEffect(() => {
1689
2157
  if (isControlled || bootRef.current) return;
1690
2158
  bootRef.current = true;
1691
2159
  let cancelled = false;
@@ -1714,15 +2182,15 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1714
2182
  cancelled = true;
1715
2183
  };
1716
2184
  }, []);
1717
- const seenCountRef = React8.useRef(0);
1718
- const [baselined, setBaselined] = React8.useState(false);
1719
- const [anchorId, setAnchorId] = React8.useState(null);
1720
- React8.useEffect(() => {
2185
+ const seenCountRef = React11.useRef(0);
2186
+ const [baselined, setBaselined] = React11.useState(false);
2187
+ const [anchorId, setAnchorId] = React11.useState(null);
2188
+ React11.useEffect(() => {
1721
2189
  if (baselined || !bootstrapDone) return;
1722
2190
  seenCountRef.current = messages.length;
1723
2191
  setBaselined(true);
1724
2192
  }, [baselined, bootstrapDone, messages.length]);
1725
- React8.useEffect(() => {
2193
+ React11.useEffect(() => {
1726
2194
  if (open) {
1727
2195
  if (messages.length > seenCountRef.current) {
1728
2196
  const firstUnread = messages[seenCountRef.current];
@@ -1732,7 +2200,7 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1732
2200
  }
1733
2201
  }, [open, messages]);
1734
2202
  const unreadCount = open || !baselined ? 0 : messages.slice(seenCountRef.current).filter(isUnreadable).length;
1735
- const workLabel = React8.useMemo(() => {
2203
+ const workLabel = React11.useMemo(() => {
1736
2204
  if (!sending) return null;
1737
2205
  const last = messages[messages.length - 1];
1738
2206
  if (last?.role === "tool") return "Calling tools\u2026";
@@ -1741,14 +2209,14 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1741
2209
  }, [sending, messages]);
1742
2210
  const approvalWaiting = pendingApprovals.length > 0;
1743
2211
  const { isAlwaysApproved } = alwaysApprove;
1744
- React8.useEffect(() => {
2212
+ React11.useEffect(() => {
1745
2213
  if (resolvingApprovalId) return;
1746
2214
  const target = pendingApprovals.find((approval) => isAlwaysApproved(approval.tool_name));
1747
2215
  if (target) void resolveApproval(target.id, true);
1748
2216
  }, [pendingApprovals, resolvingApprovalId, isAlwaysApproved, resolveApproval]);
1749
- const firedWriteIds = React8.useRef(/* @__PURE__ */ new Set());
2217
+ const firedWriteIds = React11.useRef(/* @__PURE__ */ new Set());
1750
2218
  const { onWriteSuccess } = config;
1751
- React8.useEffect(() => {
2219
+ React11.useEffect(() => {
1752
2220
  if (!onWriteSuccess) return;
1753
2221
  for (const message of messages) {
1754
2222
  if (message.role !== "tool" || !message.is_write || message.tool_status !== "success") continue;
@@ -1758,8 +2226,8 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1758
2226
  onWriteSuccess({ arguments: args, toolName: message.tool_name ?? "" });
1759
2227
  }
1760
2228
  }, [messages, onWriteSuccess]);
1761
- const lastNavigatedMessageId = React8.useRef(null);
1762
- React8.useEffect(() => {
2229
+ const lastNavigatedMessageId = React11.useRef(null);
2230
+ React11.useEffect(() => {
1763
2231
  if (!open || fullscreen || !config.navigate || !config.pages?.length) return;
1764
2232
  const toolMessages = messages.filter((message) => message.role === "tool");
1765
2233
  const latest = toolMessages[toolMessages.length - 1];
@@ -1771,7 +2239,7 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1771
2239
  const path = mapping.buildRoute ? mapping.buildRoute(args) : mapping.route;
1772
2240
  if (path) config.navigate(path);
1773
2241
  }, [config, fullscreen, messages, open]);
1774
- const closePanel = React8.useCallback(() => {
2242
+ const closePanel = React11.useCallback(() => {
1775
2243
  setOpen(false);
1776
2244
  setAgentMode(false);
1777
2245
  }, [setOpen]);
@@ -1918,6 +2386,6 @@ function defineAgentConfig(config) {
1918
2386
  return config;
1919
2387
  }
1920
2388
 
1921
- export { AgentApiClient, AgentApiError, AgentComposer, AgentPanel, AgentWidget, ChatScroller, CopyButton, DEFAULT_ACCEPT, DocumentCard, MessageItem, TaskChecklist, ThinkingIndicator, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, resolveToolPresentation, useAgentChat, useAlwaysApprove, validateFile };
2389
+ export { AgentApiClient, AgentApiError, AgentComposer, AgentImageLightbox, AgentPanel, AgentVoiceRecorder, AgentWidget, ChatScroller, CopyButton, DEFAULT_ACCEPT, DocumentCard, IMAGE_ACCEPT, MessageItem, TaskChecklist, ThinkingIndicator, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, attachmentsEnabled, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, isImageFile, resolveToolPresentation, textExtensions, useAgentChat, useAlwaysApprove, validateFile };
1922
2390
  //# sourceMappingURL=index.js.map
1923
2391
  //# sourceMappingURL=index.js.map