@cntyclub/agent-react 0.10.1 → 0.11.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,305 @@ 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
+ function formatClock(totalSeconds) {
316
+ const minutes = Math.floor(totalSeconds / 60);
317
+ const seconds = totalSeconds % 60;
318
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
319
+ }
320
+ function pickMimeType() {
321
+ if (typeof MediaRecorder === "undefined") return "";
322
+ for (const type of ["audio/webm", "audio/mp4", "audio/ogg"]) {
323
+ if (MediaRecorder.isTypeSupported(type)) return type;
324
+ }
325
+ return "";
326
+ }
327
+ function AgentVoiceRecorder({ maxSeconds, transcribe, onDone, onCancel, onError }) {
328
+ const [phase, setPhase] = React11.useState("recording");
329
+ const [elapsed, setElapsed] = React11.useState(0);
330
+ const [playing, setPlaying] = React11.useState(false);
331
+ const recorderRef = React11.useRef(null);
332
+ const streamRef = React11.useRef(null);
333
+ const chunksRef = React11.useRef([]);
334
+ const blobRef = React11.useRef(null);
335
+ const audioUrlRef = React11.useRef(null);
336
+ const audioRef = React11.useRef(null);
337
+ const timerRef = React11.useRef(null);
338
+ const onErrorRef = React11.useRef(onError);
339
+ onErrorRef.current = onError;
340
+ const stopTracks = React11.useCallback(() => {
341
+ streamRef.current?.getTracks().forEach((track) => track.stop());
342
+ streamRef.current = null;
343
+ if (timerRef.current) {
344
+ clearInterval(timerRef.current);
345
+ timerRef.current = null;
346
+ }
347
+ }, []);
348
+ React11.useEffect(() => {
349
+ let cancelled = false;
350
+ void (async () => {
351
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
352
+ onErrorRef.current("Voice recording isn't supported in this browser.");
353
+ onCancel();
354
+ return;
355
+ }
356
+ try {
357
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
358
+ if (cancelled) {
359
+ stream.getTracks().forEach((track) => track.stop());
360
+ return;
361
+ }
362
+ streamRef.current = stream;
363
+ const mimeType = pickMimeType();
364
+ const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : void 0);
365
+ recorderRef.current = recorder;
366
+ chunksRef.current = [];
367
+ recorder.ondataavailable = (event) => {
368
+ if (event.data.size > 0) chunksRef.current.push(event.data);
369
+ };
370
+ recorder.onstop = () => {
371
+ const blob = new Blob(chunksRef.current, { type: mimeType || "audio/webm" });
372
+ blobRef.current = blob;
373
+ if (audioUrlRef.current) URL.revokeObjectURL(audioUrlRef.current);
374
+ audioUrlRef.current = URL.createObjectURL(blob);
375
+ setPhase("review");
376
+ };
377
+ recorder.start();
378
+ timerRef.current = setInterval(() => {
379
+ setElapsed((current) => {
380
+ const next = current + 1;
381
+ if (next >= maxSeconds) stopRecording();
382
+ return next;
383
+ });
384
+ }, 1e3);
385
+ } catch {
386
+ onErrorRef.current("Microphone access was blocked. Enable it to record.");
387
+ onCancel();
388
+ }
389
+ })();
390
+ return () => {
391
+ cancelled = true;
392
+ stopTracks();
393
+ if (recorderRef.current && recorderRef.current.state !== "inactive") {
394
+ try {
395
+ recorderRef.current.stop();
396
+ } catch {
397
+ }
398
+ }
399
+ if (audioUrlRef.current) URL.revokeObjectURL(audioUrlRef.current);
400
+ };
401
+ }, []);
402
+ function stopRecording() {
403
+ if (timerRef.current) {
404
+ clearInterval(timerRef.current);
405
+ timerRef.current = null;
406
+ }
407
+ const recorder = recorderRef.current;
408
+ if (recorder && recorder.state !== "inactive") recorder.stop();
409
+ stopTracks();
410
+ }
411
+ const togglePlayback = () => {
412
+ const audio = audioRef.current;
413
+ if (!audio) return;
414
+ if (audio.paused) {
415
+ void audio.play();
416
+ setPlaying(true);
417
+ } else {
418
+ audio.pause();
419
+ setPlaying(false);
420
+ }
421
+ };
422
+ const confirm = async () => {
423
+ const blob = blobRef.current;
424
+ if (!blob || blob.size === 0) {
425
+ onError("The recording was empty.");
426
+ onCancel();
427
+ return;
428
+ }
429
+ setPhase("transcribing");
430
+ try {
431
+ const text = await transcribe(blob);
432
+ onDone(text.trim());
433
+ } catch {
434
+ onError("Transcription failed. Please try again.");
435
+ setPhase("review");
436
+ }
437
+ };
438
+ return /* @__PURE__ */ jsxs(
439
+ motion.div,
440
+ {
441
+ animate: { opacity: 1, y: 0 },
442
+ className: "flex min-h-8 flex-1 items-center gap-2 px-1",
443
+ initial: { opacity: 0, y: 4 },
444
+ transition: { duration: 0.15 },
445
+ children: [
446
+ phase === "recording" ? /* @__PURE__ */ jsxs(Fragment, { children: [
447
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 text-sm", children: [
448
+ /* @__PURE__ */ jsx(
449
+ motion.span,
450
+ {
451
+ animate: { opacity: [1, 0.35, 1] },
452
+ className: "size-2.5 rounded-full bg-destructive",
453
+ transition: { duration: 1.2, repeat: Infinity }
454
+ }
455
+ ),
456
+ /* @__PURE__ */ jsx("span", { className: "font-medium tabular-nums", children: formatClock(elapsed) }),
457
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
458
+ "/ ",
459
+ formatClock(maxSeconds)
460
+ ] })
461
+ ] }),
462
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
463
+ /* @__PURE__ */ jsx(Button, { "aria-label": "Discard recording", onClick: onCancel, size: "icon-sm", type: "button", variant: "ghost", children: /* @__PURE__ */ jsx(Trash2Icon, {}) }),
464
+ /* @__PURE__ */ jsx(Button, { "aria-label": "Stop recording", onClick: stopRecording, size: "icon-sm", type: "button", children: /* @__PURE__ */ jsx(SquareIcon, {}) })
465
+ ] }) : null,
466
+ phase === "review" ? /* @__PURE__ */ jsxs(Fragment, { children: [
467
+ /* @__PURE__ */ jsx("audio", { onEnded: () => setPlaying(false), ref: audioRef, src: audioUrlRef.current ?? void 0 }),
468
+ /* @__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, {}) }),
469
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm tabular-nums", children: formatClock(elapsed) }),
470
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
471
+ /* @__PURE__ */ jsx(Button, { "aria-label": "Discard recording", onClick: onCancel, size: "icon-sm", type: "button", variant: "ghost", children: /* @__PURE__ */ jsx(Trash2Icon, {}) }),
472
+ /* @__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, {}) })
473
+ ] }) : null,
474
+ phase === "transcribing" ? /* @__PURE__ */ jsxs("span", { className: cn("flex flex-1 items-center gap-2 text-muted-foreground text-sm"), children: [
475
+ /* @__PURE__ */ jsx(Loader2Icon, { className: "size-4 animate-spin" }),
476
+ "Transcribing\u2026"
477
+ ] }) : null
478
+ ]
479
+ }
480
+ );
481
+ }
482
+ function appendText(current, addition) {
483
+ if (!addition) return current;
484
+ if (!current) return addition;
485
+ return /\s$/.test(current) ? current + addition : `${current} ${addition}`;
486
+ }
221
487
  function AgentComposer({
222
488
  acceptAttribute: acceptAttribute2,
223
489
  busy,
224
490
  className,
225
491
  disabled,
226
492
  files,
493
+ onError,
227
494
  onPickFiles,
228
495
  onRemoveFile,
229
496
  onSubmit,
497
+ onTranscribe,
230
498
  onValueChange,
231
499
  placeholder = "Ask anything\u2026",
232
- value
500
+ value,
501
+ voiceEnabled,
502
+ voiceMaxSeconds = 600
233
503
  }) {
234
- const textareaRef = React8.useRef(null);
235
- const inputRef = React8.useRef(null);
504
+ const textareaRef = React11.useRef(null);
505
+ const inputRef = React11.useRef(null);
506
+ const [recording, setRecording] = React11.useState(false);
507
+ const [lightboxSrc, setLightboxSrc] = React11.useState(null);
236
508
  const canSubmit = !disabled && !busy && (value.trim().length > 0 || files.length > 0);
237
- const submit = React8.useCallback(() => {
509
+ const voiceAvailable = Boolean(voiceEnabled && onTranscribe);
510
+ const submit = React11.useCallback(() => {
238
511
  if (!canSubmit) return;
239
512
  onSubmit(value.trim());
240
513
  textareaRef.current?.focus();
241
514
  }, [canSubmit, onSubmit, value]);
242
- React8.useEffect(() => {
515
+ React11.useEffect(() => {
243
516
  if (!disabled) textareaRef.current?.focus();
244
517
  }, [disabled]);
245
- React8.useEffect(() => {
518
+ React11.useEffect(() => {
246
519
  const textarea = textareaRef.current;
247
520
  if (!textarea) return;
248
521
  textarea.style.height = "auto";
249
522
  textarea.style.height = `${Math.min(textarea.scrollHeight, 144)}px`;
250
523
  }, [value]);
524
+ const previews = React11.useMemo(
525
+ () => files.map((file, originalIndex) => {
526
+ const image = isImageFile(file);
527
+ return { file, image, originalIndex, url: image ? URL.createObjectURL(file) : null };
528
+ }),
529
+ [files]
530
+ );
531
+ React11.useEffect(
532
+ () => () => {
533
+ previews.forEach((preview) => preview.url && URL.revokeObjectURL(preview.url));
534
+ },
535
+ [previews]
536
+ );
537
+ const ordered = React11.useMemo(
538
+ () => [...previews].sort((a, b) => Number(b.image) - Number(a.image)),
539
+ [previews]
540
+ );
251
541
  const handlePick = (event) => {
252
542
  const picked = Array.from(event.target.files ?? []);
253
543
  if (picked.length) onPickFiles?.(picked);
254
544
  event.target.value = "";
255
545
  };
256
- return /* @__PURE__ */ jsx(
546
+ const handlePaste = (event) => {
547
+ if (!onPickFiles) return;
548
+ const images = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
549
+ if (images.length) {
550
+ event.preventDefault();
551
+ onPickFiles(images);
552
+ }
553
+ };
554
+ const finishRecording = (text) => {
555
+ setRecording(false);
556
+ if (text) onValueChange(appendText(value, text));
557
+ setTimeout(() => textareaRef.current?.focus(), 0);
558
+ };
559
+ return /* @__PURE__ */ jsxs(
257
560
  "form",
258
561
  {
259
562
  className: cn("shrink-0 bg-background p-3 pt-1.5", className),
@@ -262,96 +565,148 @@ function AgentComposer({
262
565
  event.preventDefault();
263
566
  submit();
264
567
  },
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) }),
568
+ children: [
569
+ /* @__PURE__ */ jsxs(
570
+ "div",
571
+ {
572
+ className: cn(
573
+ "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",
574
+ disabled && "opacity-64"
575
+ ),
576
+ children: [
577
+ files.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5 px-1 pt-1", children: ordered.map(
578
+ (preview) => preview.image && preview.url ? /* @__PURE__ */ jsxs("span", { className: "group relative", children: [
579
+ /* @__PURE__ */ jsx(
580
+ "button",
581
+ {
582
+ "aria-label": `Preview ${preview.file.name}`,
583
+ className: "block size-14 cursor-zoom-in overflow-hidden rounded-lg border border-border/72",
584
+ onClick: () => setLightboxSrc(preview.url),
585
+ type: "button",
586
+ children: /* @__PURE__ */ jsx("img", { alt: preview.file.name, className: "size-full object-cover", src: preview.url })
587
+ }
588
+ ),
281
589
  onRemoveFile ? /* @__PURE__ */ jsx(
282
590
  "button",
283
591
  {
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),
592
+ "aria-label": `Remove ${preview.file.name}`,
593
+ 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",
594
+ onClick: () => onRemoveFile(preview.originalIndex),
287
595
  type: "button",
288
596
  children: /* @__PURE__ */ jsx(XIcon, {})
289
597
  }
290
598
  ) : 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: [
599
+ ] }, `${preview.file.name}-${preview.originalIndex}`) : /* @__PURE__ */ jsxs(
600
+ "span",
601
+ {
602
+ 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",
603
+ children: [
604
+ /* @__PURE__ */ jsx(FileTextIcon, { className: "size-3.5 shrink-0 text-muted-foreground" }),
605
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: preview.file.name }),
606
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", children: formatBytes(preview.file.size) }),
607
+ onRemoveFile ? /* @__PURE__ */ jsx(
608
+ "button",
609
+ {
610
+ "aria-label": `Remove ${preview.file.name}`,
611
+ 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",
612
+ onClick: () => onRemoveFile(preview.originalIndex),
613
+ type: "button",
614
+ children: /* @__PURE__ */ jsx(XIcon, {})
615
+ }
616
+ ) : null
617
+ ]
618
+ },
619
+ `${preview.file.name}-${preview.originalIndex}`
620
+ )
621
+ ) }) : null,
622
+ recording && onTranscribe ? /* @__PURE__ */ jsx(
623
+ AgentVoiceRecorder,
624
+ {
625
+ maxSeconds: voiceMaxSeconds,
626
+ onCancel: () => {
627
+ setRecording(false);
628
+ setTimeout(() => textareaRef.current?.focus(), 0);
629
+ },
630
+ onDone: finishRecording,
631
+ onError: (message) => onError?.(message),
632
+ transcribe: onTranscribe
633
+ }
634
+ ) : /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-1.5", children: [
635
+ onPickFiles ? /* @__PURE__ */ jsxs(Fragment, { children: [
636
+ /* @__PURE__ */ jsx(
637
+ "input",
638
+ {
639
+ accept: acceptAttribute2,
640
+ className: "hidden",
641
+ multiple: true,
642
+ onChange: handlePick,
643
+ ref: inputRef,
644
+ type: "file"
645
+ }
646
+ ),
647
+ /* @__PURE__ */ jsx(
648
+ Button,
649
+ {
650
+ "aria-label": "Attach files",
651
+ className: "shrink-0 self-center",
652
+ disabled,
653
+ onClick: () => inputRef.current?.click(),
654
+ size: "icon-sm",
655
+ type: "button",
656
+ variant: "ghost",
657
+ children: /* @__PURE__ */ jsx(PaperclipIcon, {})
658
+ }
659
+ )
660
+ ] }) : null,
297
661
  /* @__PURE__ */ jsx(
298
- "input",
662
+ "textarea",
299
663
  {
300
- accept: acceptAttribute2,
301
- className: "hidden",
302
- multiple: true,
303
- onChange: handlePick,
304
- ref: inputRef,
305
- type: "file"
664
+ 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",
665
+ disabled,
666
+ onChange: (event) => onValueChange(event.target.value),
667
+ onKeyDown: (event) => {
668
+ if (event.key === "Enter" && !event.shiftKey) {
669
+ event.preventDefault();
670
+ submit();
671
+ }
672
+ },
673
+ onPaste: onPickFiles ? handlePaste : void 0,
674
+ placeholder,
675
+ ref: textareaRef,
676
+ rows: 1,
677
+ value
306
678
  }
307
679
  ),
308
- /* @__PURE__ */ jsx(
680
+ voiceAvailable ? /* @__PURE__ */ jsx(
309
681
  Button,
310
682
  {
311
- "aria-label": "Attach files",
683
+ "aria-label": "Record voice",
312
684
  className: "shrink-0 self-center",
313
685
  disabled,
314
- onClick: () => inputRef.current?.click(),
686
+ onClick: () => setRecording(true),
315
687
  size: "icon-sm",
316
688
  type: "button",
317
689
  variant: "ghost",
318
- children: /* @__PURE__ */ jsx(PaperclipIcon, {})
690
+ children: /* @__PURE__ */ jsx(MicIcon, {})
691
+ }
692
+ ) : null,
693
+ /* @__PURE__ */ jsx(
694
+ Button,
695
+ {
696
+ "aria-label": "Send message",
697
+ className: "shrink-0 rounded-full before:rounded-full",
698
+ disabled: !canSubmit,
699
+ size: "icon-sm",
700
+ type: "submit",
701
+ children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
319
702
  }
320
703
  )
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
- )
704
+ ] })
705
+ ]
706
+ }
707
+ ),
708
+ /* @__PURE__ */ jsx(AgentImageLightbox, { onClose: () => setLightboxSrc(null), src: lightboxSrc })
709
+ ]
355
710
  }
356
711
  );
357
712
  }
@@ -424,15 +779,15 @@ function groupMessages(messages) {
424
779
  return items;
425
780
  }
426
781
  function ChatScroller({ children, className, ...props }) {
427
- const viewportRef = React8.useRef(null);
428
- const pinnedRef = React8.useRef(true);
429
- const handleScroll = React8.useCallback(() => {
782
+ const viewportRef = React11.useRef(null);
783
+ const pinnedRef = React11.useRef(true);
784
+ const handleScroll = React11.useCallback(() => {
430
785
  const viewport = viewportRef.current;
431
786
  if (!viewport) return;
432
787
  const distanceFromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
433
788
  pinnedRef.current = distanceFromBottom < 64;
434
789
  }, []);
435
- React8.useEffect(() => {
790
+ React11.useEffect(() => {
436
791
  const viewport = viewportRef.current;
437
792
  if (!viewport || !pinnedRef.current) return;
438
793
  viewport.scrollTop = viewport.scrollHeight;
@@ -450,14 +805,14 @@ function ChatScroller({ children, className, ...props }) {
450
805
  );
451
806
  }
452
807
  function CopyButton({ className, text }) {
453
- const [copied, setCopied] = React8.useState(false);
454
- const timerRef = React8.useRef(null);
455
- React8.useEffect(() => {
808
+ const [copied, setCopied] = React11.useState(false);
809
+ const timerRef = React11.useRef(null);
810
+ React11.useEffect(() => {
456
811
  return () => {
457
812
  if (timerRef.current) clearTimeout(timerRef.current);
458
813
  };
459
814
  }, []);
460
- const copy = React8.useCallback(async () => {
815
+ const copy = React11.useCallback(async () => {
461
816
  try {
462
817
  await navigator.clipboard.writeText(text);
463
818
  } catch {
@@ -488,11 +843,26 @@ function CopyButton({ className, text }) {
488
843
  /* @__PURE__ */ jsx(TooltipContent, { children: copied ? "Copied!" : "Copy" })
489
844
  ] });
490
845
  }
846
+ var isImageAttachment = (format) => IMAGE_ACCEPT.includes(`.${format.toLowerCase()}`);
491
847
  function MessageItem({ message }) {
848
+ const [lightboxSrc, setLightboxSrc] = React11.useState(null);
492
849
  if (message.role === "user") {
493
850
  const attachments = message.attachments ?? [];
851
+ const images = attachments.filter((a) => isImageAttachment(a.format) && a.url);
852
+ const others = attachments.filter((a) => !isImageAttachment(a.format) || !a.url);
494
853
  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(
854
+ images.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-end gap-1.5", children: images.map((attachment) => /* @__PURE__ */ jsx(
855
+ "button",
856
+ {
857
+ "aria-label": `Preview ${attachment.filename}`,
858
+ className: "block size-20 cursor-zoom-in overflow-hidden rounded-lg border border-border/72",
859
+ onClick: () => setLightboxSrc(attachment.url ?? null),
860
+ type: "button",
861
+ children: /* @__PURE__ */ jsx("img", { alt: attachment.filename, className: "size-full object-cover", src: attachment.url ?? void 0 })
862
+ },
863
+ attachment.id
864
+ )) }) : null,
865
+ others.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-end gap-1.5", children: others.map((attachment) => /* @__PURE__ */ jsxs(
496
866
  "span",
497
867
  {
498
868
  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 +873,8 @@ function MessageItem({ message }) {
503
873
  },
504
874
  attachment.id
505
875
  )) }) : null,
506
- message.content ? /* @__PURE__ */ jsx(ChatMessageBubble, { from: "user", children: message.content }) : null
876
+ message.content ? /* @__PURE__ */ jsx(ChatMessageBubble, { from: "user", children: message.content }) : null,
877
+ /* @__PURE__ */ jsx(AgentImageLightbox, { onClose: () => setLightboxSrc(null), src: lightboxSrc })
507
878
  ] });
508
879
  }
509
880
  if (message.role === "assistant") {
@@ -534,8 +905,8 @@ var DEFAULT_PHRASES = [
534
905
  ];
535
906
  function ThinkingIndicator({ className, intervalMs = 1800, phrases }) {
536
907
  const words = phrases && phrases.length ? phrases : DEFAULT_PHRASES;
537
- const [index, setIndex] = React8.useState(0);
538
- React8.useEffect(() => {
908
+ const [index, setIndex] = React11.useState(0);
909
+ React11.useEffect(() => {
539
910
  setIndex(0);
540
911
  const timer = setInterval(() => {
541
912
  setIndex((current) => (current + 1) % words.length);
@@ -797,7 +1168,7 @@ function DownloadButton({ block, size = "sm" }) {
797
1168
  );
798
1169
  }
799
1170
  function DocumentViewer({ block, onClose }) {
800
- React8.useEffect(() => {
1171
+ React11.useEffect(() => {
801
1172
  const onKey = (event) => event.key === "Escape" && onClose();
802
1173
  window.addEventListener("keydown", onKey);
803
1174
  return () => window.removeEventListener("keydown", onKey);
@@ -822,7 +1193,7 @@ function DocumentViewer({ block, onClose }) {
822
1193
  ] });
823
1194
  }
824
1195
  function DocumentCard({ block }) {
825
- const [open, setOpen] = React8.useState(false);
1196
+ const [open, setOpen] = React11.useState(false);
826
1197
  const previewLines = (block.preview || "").split("\n").slice(0, 6).join("\n");
827
1198
  return /* @__PURE__ */ jsxs("div", { className: "w-full max-w-full overflow-hidden rounded-xl border border-border bg-card shadow-xs", children: [
828
1199
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-border/72 border-b px-3 py-2", children: [
@@ -944,7 +1315,7 @@ function StepDetail({
944
1315
  ] });
945
1316
  }
946
1317
  function ToolActivityGroup({ describeToolCall, steps }) {
947
- const [open, setOpen] = React8.useState(false);
1318
+ const [open, setOpen] = React11.useState(false);
948
1319
  if (steps.length === 0) return null;
949
1320
  const last = steps[steps.length - 1];
950
1321
  const anyRunning = steps.some((step) => step.status === "running");
@@ -1048,24 +1419,28 @@ function AgentPanel({
1048
1419
  onClose,
1049
1420
  onToggleAgentMode
1050
1421
  }) {
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);
1422
+ const [draft, setDraft] = React11.useState("");
1423
+ const [view, setView] = React11.useState("chat");
1424
+ const [files, setFiles] = React11.useState([]);
1425
+ const [uploadError, setUploadError] = React11.useState(null);
1426
+ const [dragging, setDragging] = React11.useState(false);
1427
+ const dragDepth = React11.useRef(0);
1057
1428
  const agentName = config.agentName ?? chat.agentInfo?.name ?? "Assistant";
1058
- const uploadEnabled = fileUploadEnabled(config);
1429
+ const caps = {
1430
+ visionEnabled: chat.agentInfo?.vision_enabled,
1431
+ visionMaxMb: chat.agentInfo?.vision_max_file_mb
1432
+ };
1433
+ const uploadEnabled = attachmentsEnabled(config, caps);
1059
1434
  const suggestions = config.suggestions ?? [
1060
1435
  "What can you help me with?",
1061
1436
  "Give me a quick summary of my data."
1062
1437
  ];
1063
- const addFiles = React8.useCallback(
1438
+ const addFiles = React11.useCallback(
1064
1439
  (incoming) => {
1065
1440
  const accepted = [];
1066
1441
  let rejection = null;
1067
1442
  for (const file of incoming) {
1068
- const error = validateFile(config, file);
1443
+ const error = validateFile(config, file, caps);
1069
1444
  if (error) {
1070
1445
  rejection = error;
1071
1446
  continue;
@@ -1075,12 +1450,12 @@ function AgentPanel({
1075
1450
  setUploadError(rejection);
1076
1451
  if (accepted.length) setFiles((current) => [...current, ...accepted].slice(0, 10));
1077
1452
  },
1078
- [config]
1453
+ [config, caps.visionEnabled, caps.visionMaxMb]
1079
1454
  );
1080
- const removeFile = React8.useCallback((index) => {
1455
+ const removeFile = React11.useCallback((index) => {
1081
1456
  setFiles((current) => current.filter((_, i) => i !== index));
1082
1457
  }, []);
1083
- const handleSubmit = React8.useCallback(
1458
+ const handleSubmit = React11.useCallback(
1084
1459
  (text) => {
1085
1460
  const staged = files;
1086
1461
  setDraft("");
@@ -1090,7 +1465,7 @@ function AgentPanel({
1090
1465
  },
1091
1466
  [chat, files]
1092
1467
  );
1093
- const onDragEnter = React8.useCallback(
1468
+ const onDragEnter = React11.useCallback(
1094
1469
  (event) => {
1095
1470
  if (!uploadEnabled || !event.dataTransfer?.types?.includes("Files")) return;
1096
1471
  dragDepth.current += 1;
@@ -1098,12 +1473,12 @@ function AgentPanel({
1098
1473
  },
1099
1474
  [uploadEnabled]
1100
1475
  );
1101
- const onDragLeave = React8.useCallback((event) => {
1476
+ const onDragLeave = React11.useCallback((event) => {
1102
1477
  if (!event.dataTransfer?.types?.includes("Files")) return;
1103
1478
  dragDepth.current = Math.max(0, dragDepth.current - 1);
1104
1479
  if (dragDepth.current === 0) setDragging(false);
1105
1480
  }, []);
1106
- const onDrop = React8.useCallback(
1481
+ const onDrop = React11.useCallback(
1107
1482
  (event) => {
1108
1483
  if (!uploadEnabled) return;
1109
1484
  event.preventDefault();
@@ -1114,7 +1489,7 @@ function AgentPanel({
1114
1489
  },
1115
1490
  [addFiles, uploadEnabled]
1116
1491
  );
1117
- const openHistory = React8.useCallback(() => {
1492
+ const openHistory = React11.useCallback(() => {
1118
1493
  setView("history");
1119
1494
  void chat.refreshConversations();
1120
1495
  }, [chat]);
@@ -1137,8 +1512,8 @@ function AgentPanel({
1137
1512
  children: [
1138
1513
  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
1514
  /* @__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 ") })
1515
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-sm", children: "Drop files to attach" }),
1516
+ /* @__PURE__ */ jsx("div", { className: "text-muted-foreground text-xs", children: acceptAttribute(config, caps).replaceAll(",", " \xB7 ") })
1142
1517
  ] }) }) : null,
1143
1518
  /* @__PURE__ */ jsxs(ChatHeader, { children: [
1144
1519
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2.5", children: [
@@ -1269,7 +1644,7 @@ function AgentPanel({
1269
1644
  const anchorKey = newMessageAnchorId ? grouped.find(
1270
1645
  (it) => it.kind !== "tool-activity" && it.message.id === newMessageAnchorId
1271
1646
  )?.key : void 0;
1272
- return grouped.map((item) => /* @__PURE__ */ jsxs(React8.Fragment, { children: [
1647
+ return grouped.map((item) => /* @__PURE__ */ jsxs(React11.Fragment, { children: [
1273
1648
  item.key === anchorKey ? /* @__PURE__ */ jsx(NewMessagesDivider, {}) : null,
1274
1649
  /* @__PURE__ */ jsx(
1275
1650
  motion.div,
@@ -1319,17 +1694,21 @@ function AgentPanel({
1319
1694
  /* @__PURE__ */ jsx(
1320
1695
  AgentComposer,
1321
1696
  {
1322
- acceptAttribute: acceptAttribute(config),
1697
+ acceptAttribute: acceptAttribute(config, caps),
1323
1698
  busy,
1324
1699
  className: cn(agentMode && "mx-auto w-full max-w-3xl"),
1325
1700
  disabled: chat.pendingApprovals.length > 0,
1326
1701
  files,
1702
+ onError: setUploadError,
1327
1703
  onPickFiles: uploadEnabled ? addFiles : void 0,
1328
1704
  onRemoveFile: removeFile,
1329
1705
  onSubmit: handleSubmit,
1706
+ onTranscribe: chat.transcribe,
1330
1707
  onValueChange: setDraft,
1331
1708
  placeholder: chat.pendingApprovals.length > 0 ? "Approve or decline the pending action first\u2026" : `Ask ${agentName}\u2026`,
1332
- value: draft
1709
+ value: draft,
1710
+ voiceEnabled: chat.agentInfo?.voice_enabled,
1711
+ voiceMaxSeconds: chat.agentInfo?.voice_max_seconds
1333
1712
  }
1334
1713
  )
1335
1714
  ] })
@@ -1365,16 +1744,16 @@ function buildOptimisticUserMessage(text, files) {
1365
1744
  };
1366
1745
  }
1367
1746
  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(() => {
1747
+ const client = React11.useMemo(() => new AgentApiClient(config), [config]);
1748
+ const [agentInfo, setAgentInfo] = React11.useState(null);
1749
+ const [conversationId, setConversationId] = React11.useState(null);
1750
+ const [conversations, setConversations] = React11.useState([]);
1751
+ const [messages, setMessages] = React11.useState([]);
1752
+ const [pendingApprovals, setPendingApprovals] = React11.useState([]);
1753
+ const [sending, setSending] = React11.useState(false);
1754
+ const [resolvingApprovalId, setResolvingApprovalId] = React11.useState(null);
1755
+ const [error, setError] = React11.useState(null);
1756
+ React11.useEffect(() => {
1378
1757
  let cancelled = false;
1379
1758
  client.getConfig().then((info) => {
1380
1759
  if (!cancelled) setAgentInfo(info);
@@ -1384,11 +1763,11 @@ function useAgentChat(config) {
1384
1763
  cancelled = true;
1385
1764
  };
1386
1765
  }, [client]);
1387
- const handleFailure = React8.useCallback((err) => {
1766
+ const handleFailure = React11.useCallback((err) => {
1388
1767
  const message = err instanceof AgentApiError ? err.message : "Something went wrong. Please try again.";
1389
1768
  setError(message);
1390
1769
  }, []);
1391
- const applyResponse = React8.useCallback(
1770
+ const applyResponse = React11.useCallback(
1392
1771
  (response) => {
1393
1772
  if (response.status === "error" && !response.messages?.length) {
1394
1773
  setError(response.message ?? "The assistant could not answer.");
@@ -1410,7 +1789,7 @@ function useAgentChat(config) {
1410
1789
  },
1411
1790
  []
1412
1791
  );
1413
- const handleStreamEvent = React8.useCallback(
1792
+ const handleStreamEvent = React11.useCallback(
1414
1793
  (event, ctx) => {
1415
1794
  switch (event.type) {
1416
1795
  case "conversation":
@@ -1475,7 +1854,7 @@ function useAgentChat(config) {
1475
1854
  []
1476
1855
  );
1477
1856
  const streamingEnabled = config.streaming !== false && typeof ReadableStream !== "undefined";
1478
- const send = React8.useCallback(
1857
+ const send = React11.useCallback(
1479
1858
  async (text, files) => {
1480
1859
  if (sending) return;
1481
1860
  if (!text.trim() && !(files && files.length)) return;
@@ -1506,7 +1885,14 @@ function useAgentChat(config) {
1506
1885
  },
1507
1886
  [applyResponse, client, conversationId, handleFailure, handleStreamEvent, sending, streamingEnabled]
1508
1887
  );
1509
- const resolveApproval = React8.useCallback(
1888
+ const transcribe = React11.useCallback(
1889
+ async (audio) => {
1890
+ const { text } = await client.transcribe(audio);
1891
+ return text;
1892
+ },
1893
+ [client]
1894
+ );
1895
+ const resolveApproval = React11.useCallback(
1510
1896
  async (callId, approve) => {
1511
1897
  if (resolvingApprovalId) return;
1512
1898
  setResolvingApprovalId(callId);
@@ -1535,13 +1921,13 @@ function useAgentChat(config) {
1535
1921
  },
1536
1922
  [applyResponse, client, handleFailure, handleStreamEvent, resolvingApprovalId, streamingEnabled]
1537
1923
  );
1538
- const newChat = React8.useCallback(() => {
1924
+ const newChat = React11.useCallback(() => {
1539
1925
  setConversationId(null);
1540
1926
  setMessages([]);
1541
1927
  setPendingApprovals([]);
1542
1928
  setError(null);
1543
1929
  }, []);
1544
- const refreshConversations = React8.useCallback(async () => {
1930
+ const refreshConversations = React11.useCallback(async () => {
1545
1931
  try {
1546
1932
  const { conversations: list } = await client.listConversations();
1547
1933
  setConversations(list);
@@ -1551,7 +1937,7 @@ function useAgentChat(config) {
1551
1937
  return [];
1552
1938
  }
1553
1939
  }, [client, handleFailure]);
1554
- const openConversation = React8.useCallback(
1940
+ const openConversation = React11.useCallback(
1555
1941
  async (id) => {
1556
1942
  setError(null);
1557
1943
  try {
@@ -1565,7 +1951,7 @@ function useAgentChat(config) {
1565
1951
  },
1566
1952
  [client, handleFailure]
1567
1953
  );
1568
- const deleteConversation = React8.useCallback(
1954
+ const deleteConversation = React11.useCallback(
1569
1955
  async (id) => {
1570
1956
  try {
1571
1957
  await client.deleteConversation(id);
@@ -1577,7 +1963,7 @@ function useAgentChat(config) {
1577
1963
  },
1578
1964
  [client, conversationId, handleFailure, newChat]
1579
1965
  );
1580
- const clearError = React8.useCallback(() => setError(null), []);
1966
+ const clearError = React11.useCallback(() => setError(null), []);
1581
1967
  return {
1582
1968
  agentInfo,
1583
1969
  clearError,
@@ -1593,7 +1979,8 @@ function useAgentChat(config) {
1593
1979
  resolveApproval,
1594
1980
  resolvingApprovalId,
1595
1981
  send,
1596
- sending
1982
+ sending,
1983
+ transcribe
1597
1984
  };
1598
1985
  }
1599
1986
  var STORAGE_PREFIX = "cc-agent-always-approve:";
@@ -1612,11 +1999,11 @@ function readStored(clientId) {
1612
1999
  }
1613
2000
  }
1614
2001
  function useAlwaysApprove(clientId) {
1615
- const [approved, setApproved] = React8.useState(() => /* @__PURE__ */ new Set());
1616
- React8.useEffect(() => {
2002
+ const [approved, setApproved] = React11.useState(() => /* @__PURE__ */ new Set());
2003
+ React11.useEffect(() => {
1617
2004
  setApproved(readStored(clientId));
1618
2005
  }, [clientId]);
1619
- const persist = React8.useCallback(
2006
+ const persist = React11.useCallback(
1620
2007
  (next) => {
1621
2008
  setApproved(next);
1622
2009
  if (typeof window === "undefined") return;
@@ -1627,8 +2014,8 @@ function useAlwaysApprove(clientId) {
1627
2014
  },
1628
2015
  [clientId]
1629
2016
  );
1630
- const isAlwaysApproved = React8.useCallback((toolName) => approved.has(toolName), [approved]);
1631
- const setAlwaysApprove = React8.useCallback(
2017
+ const isAlwaysApproved = React11.useCallback((toolName) => approved.has(toolName), [approved]);
2018
+ const setAlwaysApprove = React11.useCallback(
1632
2019
  (toolName) => {
1633
2020
  if (approved.has(toolName)) return;
1634
2021
  const next = new Set(approved);
@@ -1637,7 +2024,7 @@ function useAlwaysApprove(clientId) {
1637
2024
  },
1638
2025
  [approved, persist]
1639
2026
  );
1640
- const clearAlwaysApprove = React8.useCallback(
2027
+ const clearAlwaysApprove = React11.useCallback(
1641
2028
  (toolName) => {
1642
2029
  if (!approved.has(toolName)) return;
1643
2030
  const next = new Set(approved);
@@ -1657,9 +2044,10 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1657
2044
  const position = config.appearance?.position ?? "bottom-right";
1658
2045
  const isControlled = controlledOpen !== void 0;
1659
2046
  const storageKey2 = `cc-agent-open:${config.clientId}`;
1660
- const [uncontrolledOpen, setUncontrolledOpen] = React8.useState(false);
2047
+ const [uncontrolledOpen, setUncontrolledOpen] = React11.useState(false);
1661
2048
  const open = controlledOpen ?? uncontrolledOpen;
1662
- const persist = React8.useCallback(
2049
+ const [bootstrapDone, setBootstrapDone] = React11.useState(isControlled);
2050
+ const persist = React11.useCallback(
1663
2051
  (next) => {
1664
2052
  if (isControlled || !behavior.persistOpenState) return;
1665
2053
  try {
@@ -1669,7 +2057,7 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1669
2057
  },
1670
2058
  [behavior.persistOpenState, isControlled, storageKey2]
1671
2059
  );
1672
- const setOpen = React8.useCallback(
2060
+ const setOpen = React11.useCallback(
1673
2061
  (next) => {
1674
2062
  setUncontrolledOpen(next);
1675
2063
  persist(next);
@@ -1677,14 +2065,14 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1677
2065
  },
1678
2066
  [onOpenChange, persist]
1679
2067
  );
1680
- const [agentMode, setAgentMode] = React8.useState(false);
2068
+ const [agentMode, setAgentMode] = React11.useState(false);
1681
2069
  const isDesktop = useMediaQuery("(min-width: 640px)", { defaultSSRValue: true, ssr: true });
1682
2070
  const fullscreen = !isDesktop || agentMode;
1683
2071
  const chat = useAgentChat(config);
1684
2072
  const alwaysApprove = useAlwaysApprove(config.clientId);
1685
2073
  const { messages, pendingApprovals, resolveApproval, resolvingApprovalId, sending } = chat;
1686
- const bootRef = React8.useRef(false);
1687
- React8.useEffect(() => {
2074
+ const bootRef = React11.useRef(false);
2075
+ React11.useEffect(() => {
1688
2076
  if (isControlled || bootRef.current) return;
1689
2077
  bootRef.current = true;
1690
2078
  let cancelled = false;
@@ -1707,14 +2095,21 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1707
2095
  setUncontrolledOpen(true);
1708
2096
  onOpenChange?.(true);
1709
2097
  }
2098
+ setBootstrapDone(true);
1710
2099
  })();
1711
2100
  return () => {
1712
2101
  cancelled = true;
1713
2102
  };
1714
2103
  }, []);
1715
- const seenCountRef = React8.useRef(0);
1716
- const [anchorId, setAnchorId] = React8.useState(null);
1717
- React8.useEffect(() => {
2104
+ const seenCountRef = React11.useRef(0);
2105
+ const [baselined, setBaselined] = React11.useState(false);
2106
+ const [anchorId, setAnchorId] = React11.useState(null);
2107
+ React11.useEffect(() => {
2108
+ if (baselined || !bootstrapDone) return;
2109
+ seenCountRef.current = messages.length;
2110
+ setBaselined(true);
2111
+ }, [baselined, bootstrapDone, messages.length]);
2112
+ React11.useEffect(() => {
1718
2113
  if (open) {
1719
2114
  if (messages.length > seenCountRef.current) {
1720
2115
  const firstUnread = messages[seenCountRef.current];
@@ -1723,8 +2118,8 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1723
2118
  seenCountRef.current = messages.length;
1724
2119
  }
1725
2120
  }, [open, messages]);
1726
- const unreadCount = open ? 0 : messages.slice(seenCountRef.current).filter(isUnreadable).length;
1727
- const workLabel = React8.useMemo(() => {
2121
+ const unreadCount = open || !baselined ? 0 : messages.slice(seenCountRef.current).filter(isUnreadable).length;
2122
+ const workLabel = React11.useMemo(() => {
1728
2123
  if (!sending) return null;
1729
2124
  const last = messages[messages.length - 1];
1730
2125
  if (last?.role === "tool") return "Calling tools\u2026";
@@ -1733,14 +2128,14 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1733
2128
  }, [sending, messages]);
1734
2129
  const approvalWaiting = pendingApprovals.length > 0;
1735
2130
  const { isAlwaysApproved } = alwaysApprove;
1736
- React8.useEffect(() => {
2131
+ React11.useEffect(() => {
1737
2132
  if (resolvingApprovalId) return;
1738
2133
  const target = pendingApprovals.find((approval) => isAlwaysApproved(approval.tool_name));
1739
2134
  if (target) void resolveApproval(target.id, true);
1740
2135
  }, [pendingApprovals, resolvingApprovalId, isAlwaysApproved, resolveApproval]);
1741
- const firedWriteIds = React8.useRef(/* @__PURE__ */ new Set());
2136
+ const firedWriteIds = React11.useRef(/* @__PURE__ */ new Set());
1742
2137
  const { onWriteSuccess } = config;
1743
- React8.useEffect(() => {
2138
+ React11.useEffect(() => {
1744
2139
  if (!onWriteSuccess) return;
1745
2140
  for (const message of messages) {
1746
2141
  if (message.role !== "tool" || !message.is_write || message.tool_status !== "success") continue;
@@ -1750,8 +2145,8 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1750
2145
  onWriteSuccess({ arguments: args, toolName: message.tool_name ?? "" });
1751
2146
  }
1752
2147
  }, [messages, onWriteSuccess]);
1753
- const lastNavigatedMessageId = React8.useRef(null);
1754
- React8.useEffect(() => {
2148
+ const lastNavigatedMessageId = React11.useRef(null);
2149
+ React11.useEffect(() => {
1755
2150
  if (!open || fullscreen || !config.navigate || !config.pages?.length) return;
1756
2151
  const toolMessages = messages.filter((message) => message.role === "tool");
1757
2152
  const latest = toolMessages[toolMessages.length - 1];
@@ -1763,7 +2158,7 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1763
2158
  const path = mapping.buildRoute ? mapping.buildRoute(args) : mapping.route;
1764
2159
  if (path) config.navigate(path);
1765
2160
  }, [config, fullscreen, messages, open]);
1766
- const closePanel = React8.useCallback(() => {
2161
+ const closePanel = React11.useCallback(() => {
1767
2162
  setOpen(false);
1768
2163
  setAgentMode(false);
1769
2164
  }, [setOpen]);
@@ -1802,7 +2197,7 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1802
2197
  animate: { scale: 1 },
1803
2198
  exit: { scale: 0 },
1804
2199
  transition: { type: "spring", stiffness: 500, damping: 20 },
1805
- className: "absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-destructive px-1 text-xs font-semibold text-destructive-foreground shadow",
2200
+ className: "absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-xs font-semibold text-white shadow-md ring-2 ring-background",
1806
2201
  "aria-label": `${unreadCount} new message${unreadCount === 1 ? "" : "s"}`,
1807
2202
  children: unreadCount > 9 ? "9+" : unreadCount
1808
2203
  },
@@ -1910,6 +2305,6 @@ function defineAgentConfig(config) {
1910
2305
  return config;
1911
2306
  }
1912
2307
 
1913
- 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 };
2308
+ 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 };
1914
2309
  //# sourceMappingURL=index.js.map
1915
2310
  //# sourceMappingURL=index.js.map