@adatechnology/conversations-ui 0.9.0 → 0.10.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.
Files changed (90) hide show
  1. package/README.md +106 -0
  2. package/dist/{ConversationSimulatorPanel-q-wVgBCN.d.ts → ConversationSimulatorPanel-P6eqTSAO.d.ts} +1 -16
  3. package/dist/{chunk-O6457XHK.js → chunk-WCZNIJ3M.js} +16 -1
  4. package/dist/{chunk-L3OFAQPS.js → chunk-Y326LLEP.js} +4 -14
  5. package/dist/index.d.ts +10 -3
  6. package/dist/index.js +3 -3
  7. package/dist/participant/index.d.ts +51 -3
  8. package/dist/participant/index.js +1363 -319
  9. package/dist/preview/index.d.ts +2 -2
  10. package/dist/preview/index.js +2 -2
  11. package/dist/styles.css +182 -0
  12. package/package.json +2 -2
  13. package/src/AudioRecorderButton.tsx +4 -28
  14. package/src/audioRecorderFormat.ts +27 -0
  15. package/src/participant/ConversationThread.tsx +4 -0
  16. package/src/participant/ParticipantAttachmentItem.tsx +7 -10
  17. package/src/participant/ParticipantAudioAttachment.tsx +34 -0
  18. package/src/participant/ParticipantAudioPlayer.tsx +68 -0
  19. package/src/participant/ParticipantAudioPlayerControls.tsx +68 -0
  20. package/src/participant/ParticipantAudioRecorderView.tsx +55 -0
  21. package/src/participant/ParticipantAudioTranscription.tsx +90 -0
  22. package/src/participant/ParticipantComposer.tsx +50 -15
  23. package/src/participant/ParticipantComposerAudio.tsx +95 -0
  24. package/src/participant/ParticipantConversations.tsx +3 -0
  25. package/src/participant/ParticipantRecorderParts.tsx +118 -0
  26. package/src/participant/ParticipantThread.tsx +7 -0
  27. package/src/participant/ParticipantThreadScreen.tsx +1 -0
  28. package/src/participant/index.ts +3 -0
  29. package/src/participant/participantAudio.types.ts +10 -0
  30. package/src/participant/participantAudioCapture.test.ts +15 -0
  31. package/src/participant/participantAudioCapture.ts +85 -0
  32. package/src/participant/participantAudioCoordinator.ts +13 -0
  33. package/src/participant/participantAudioFormat.test.ts +93 -0
  34. package/src/participant/participantAudioFormat.ts +72 -0
  35. package/src/participant/participantAudioGolden.test.tsx +168 -0
  36. package/src/participant/participantAudioLabels.test.ts +22 -0
  37. package/src/participant/participantAudioLabels.ts +76 -0
  38. package/src/participant/participantAudioPlayerControlsDom.test.tsx +123 -0
  39. package/src/participant/participantAudioPlayerCss.test.ts +63 -0
  40. package/src/participant/participantAudioPlayerDom.test.tsx +154 -0
  41. package/src/participant/participantAudioPlayerDomKit.test-helper.tsx +84 -0
  42. package/src/participant/participantAudioPlayerState.ts +35 -0
  43. package/src/participant/participantAudioRecorderDom.test.tsx +127 -0
  44. package/src/participant/participantAudioRecorderInterruptionDom.test.tsx +135 -0
  45. package/src/participant/participantAudioRecorderSpecDom.test.tsx +110 -0
  46. package/src/participant/participantAudioRecorderState.test.ts +144 -0
  47. package/src/participant/participantAudioRecorderState.ts +93 -0
  48. package/src/participant/participantAudioRecorderView.test.tsx +98 -0
  49. package/src/participant/participantAudioSupport.ts +36 -0
  50. package/src/participant/participantAudioTranscriptionDom.test.tsx +198 -0
  51. package/src/participant/participantComposerAudioDom.test.tsx +185 -0
  52. package/src/participant/participantComposerAudioGolden.test.tsx +68 -0
  53. package/src/participant/participantComposerAudioSsr.test.tsx +74 -0
  54. package/src/participant/participantFileValidation.ts +1 -1
  55. package/src/participant/participantGolden.test.tsx +2 -0
  56. package/src/participant/participantGoldenScenarios.test-helper.tsx +14 -1
  57. package/src/participant/participantLabels.ts +4 -1
  58. package/src/participant/participantMediaFakes.test-helper.ts +82 -0
  59. package/src/participant/participantNoMicrophoneDom.test.tsx +44 -0
  60. package/src/participant/participantPublicTypes.test.ts +11 -0
  61. package/src/participant/participantRecorderDomKit.test-helper.tsx +131 -0
  62. package/src/participant/participantRecorderFakes.test-helper.ts +47 -0
  63. package/src/participant/participantSpecRecorderFake.test-helper.ts +52 -0
  64. package/src/participant/participantThemeContrast.test.ts +9 -0
  65. package/src/participant/test-fixtures/composer-audio-denied.golden.html +1 -0
  66. package/src/participant/test-fixtures/composer-audio-idle.golden.html +1 -0
  67. package/src/participant/test-fixtures/composer-audio-recording.golden.html +1 -0
  68. package/src/participant/test-fixtures/composer-audio-review.golden.html +1 -0
  69. package/src/participant/test-fixtures/thread-audio-native.golden.html +1 -0
  70. package/src/participant/test-fixtures/thread-audio-player.golden.html +1 -0
  71. package/src/participant/test-fixtures/thread-audio-transcription-empty.golden.html +1 -0
  72. package/src/participant/test-fixtures/thread-audio-transcription-pending.golden.html +1 -0
  73. package/src/participant/test-fixtures/thread-audio-transcription-ready.golden.html +1 -0
  74. package/src/participant/test-fixtures/thread-audio-transcription-too_long.golden.html +1 -0
  75. package/src/participant/test-fixtures/thread-composer-microphone-idle.golden.html +1 -0
  76. package/src/participant/test-fixtures/thread-empty-draft.golden.html +1 -0
  77. package/src/participant/useParticipantAudioMediaEvents.ts +46 -0
  78. package/src/participant/useParticipantAudioPlayer.ts +52 -0
  79. package/src/participant/useParticipantAudioPlayerActions.ts +47 -0
  80. package/src/participant/useParticipantAudioRecorder.ts +153 -0
  81. package/src/participant/useParticipantAudioRecorderCore.ts +55 -0
  82. package/src/participant/useParticipantAudioRecorderEffects.ts +70 -0
  83. package/src/participant/useParticipantAudioSourceRetry.ts +42 -0
  84. package/src/participant/useParticipantSendAudio.test.tsx +85 -0
  85. package/src/participant/useParticipantSendAudio.ts +28 -0
  86. package/src/participant/useParticipantThreadController.ts +20 -7
  87. package/src/participant/useStableAttachmentUrl.ts +71 -0
  88. package/src/participant/useTranscriptionOverflow.ts +25 -0
  89. package/src/participant/useTranscriptionReadyAnnouncement.ts +24 -0
  90. package/src/styles.css +29 -0
@@ -2,15 +2,17 @@ import {
2
2
  TooltipLayer
3
3
  } from "../chunk-NSOAUYNE.js";
4
4
  import {
5
+ DEFAULT_MAX_RECORDING_MILLISECONDS,
5
6
  StatusTicks,
6
7
  channelCapabilityFor,
7
8
  formatFileSize,
8
9
  formatTimestamp,
9
- isSameDay
10
- } from "../chunk-O6457XHK.js";
10
+ isSameDay,
11
+ resolveRecordingFormat
12
+ } from "../chunk-WCZNIJ3M.js";
11
13
 
12
14
  // src/participant/ParticipantConversationsScreen.tsx
13
- import { useMemo as useMemo5, useReducer as useReducer6, useRef as useRef12, useState as useState9 } from "react";
15
+ import { useMemo as useMemo6, useReducer as useReducer6, useRef as useRef21, useState as useState14 } from "react";
14
16
 
15
17
  // src/participant/participantLoadView.ts
16
18
  function resolveLoadView({ status, hasItems }) {
@@ -21,6 +23,45 @@ function resolveLoadView({ status, hasItems }) {
21
23
  };
22
24
  }
23
25
 
26
+ // src/participant/participantAudioLabels.ts
27
+ var DEFAULT_PARTICIPANT_AUDIO_LABELS = {
28
+ recordAudio: "Record audio",
29
+ recordingInProgress: "Recording",
30
+ stopRecording: "Stop recording",
31
+ recordingElapsed: "{elapsed} of {limit}",
32
+ reviewAudio: "Listen before sending",
33
+ sendAudio: "Send audio",
34
+ discardAudio: "Discard audio",
35
+ microphoneDenied: "No permission to use the microphone.",
36
+ recordingUnsupported: "This device cannot record audio.",
37
+ audioEmpty: "Nothing was captured by the microphone.",
38
+ audioTooLarge: "Audio too large to send.",
39
+ audioPlayer: "Audio",
40
+ playAudio: "Play audio",
41
+ pauseAudio: "Pause audio",
42
+ audioPosition: "Audio position",
43
+ audioPositionValue: "{position} of {duration}",
44
+ playbackRate: "Playback speed {rate}",
45
+ audioUnavailable: "Audio unavailable",
46
+ transcribing: "Transcribing\u2026",
47
+ transcriptionReady: "Transcription ready",
48
+ machineTranscription: "Transcription generated by machine",
49
+ emptyTranscription: "Nothing was said in the audio",
50
+ transcriptionTooLong: "Transcription only for audios shorter than {minutes} minutes",
51
+ showFullTranscription: "Show all",
52
+ collapseTranscription: "Show less"
53
+ };
54
+ function resolveParticipantAudioLabels(labels) {
55
+ const resolved = { ...DEFAULT_PARTICIPANT_AUDIO_LABELS };
56
+ for (const key of Object.keys(resolved)) {
57
+ resolved[key] = labels?.[key] ?? DEFAULT_PARTICIPANT_AUDIO_LABELS[key];
58
+ }
59
+ return resolved;
60
+ }
61
+ function formatRecordingElapsedLabel(template, elapsed, limit) {
62
+ return template.replace("{elapsed}", elapsed).replace("{limit}", limit);
63
+ }
64
+
24
65
  // src/participant/participantLabels.ts
25
66
  var DEFAULT_MESSAGE_OPTIONS_LABEL = "Message options";
26
67
  var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
@@ -80,7 +121,8 @@ var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
80
121
  channelWebchat: "Web chat",
81
122
  externalLinkHint: "opens in a new tab",
82
123
  copyValue: "Copy",
83
- valueCopied: "Copied"
124
+ valueCopied: "Copied",
125
+ ...DEFAULT_PARTICIPANT_AUDIO_LABELS
84
126
  };
85
127
  function formatParticipantLabel(template, count) {
86
128
  return template.replace("{count}", String(count));
@@ -382,7 +424,7 @@ function ParticipantInbox(props) {
382
424
  }
383
425
 
384
426
  // src/participant/ParticipantThreadScreen.tsx
385
- import { useEffect as useEffect13, useState as useState8 } from "react";
427
+ import { useEffect as useEffect20, useState as useState13 } from "react";
386
428
 
387
429
  // src/participant/ParticipantBackButton.tsx
388
430
  import { ArrowLeft } from "lucide-react";
@@ -392,14 +434,818 @@ function ParticipantBackButton({ label, onBack }) {
392
434
  }
393
435
 
394
436
  // src/participant/ParticipantThread.tsx
395
- import { useRef as useRef5 } from "react";
437
+ import { useRef as useRef13 } from "react";
396
438
 
397
439
  // src/participant/ParticipantComposer.tsx
398
- import { useState } from "react";
440
+ import { useState as useState3 } from "react";
441
+
442
+ // src/participant/ParticipantComposerAudio.tsx
443
+ import { useEffect as useEffect5, useRef as useRef6 } from "react";
444
+
445
+ // src/participant/participantFileValidation.ts
446
+ function matchesAcceptedType(mimeType, acceptedTypes) {
447
+ return acceptedTypes.some(
448
+ (accepted) => accepted.endsWith("/*") ? mimeType.startsWith(accepted.slice(0, -1)) : mimeType === accepted
449
+ );
450
+ }
451
+ function rejectionOf(file, params) {
452
+ if (params.acceptedTypes && !matchesAcceptedType(file.type, params.acceptedTypes)) return "typeNotAccepted";
453
+ if (file.size > params.maxBytes) return "tooLarge";
454
+ return void 0;
455
+ }
456
+ function reduceFilesSelection(params) {
457
+ const files = [...params.current];
458
+ const rejected = [];
459
+ for (const file of params.incoming) {
460
+ const reason = rejectionOf(file, params);
461
+ if (reason === void 0) files.push(file);
462
+ else rejected.push({ file, reason });
463
+ }
464
+ return { files, rejected };
465
+ }
466
+ function maxBytesForChannel(channel = "app") {
467
+ return channelCapabilityFor(channel).attachments.maxBytes;
468
+ }
469
+
470
+ // src/participant/participantAudio.types.ts
471
+ var DEFAULT_MAX_RECORDING_BYTES = 16 * 1024 * 1024;
472
+
473
+ // src/participant/participantAudioRecorderState.ts
474
+ var MINIMUM_RECORDING_MILLISECONDS = 500;
475
+ var MUTE_INTERRUPTION_DELAY_MS = 3e3;
476
+ var RETRYABLE = ["idle", "denied", "unsupported", "empty", "tooLarge"];
477
+ var TRANSITIONS = {
478
+ request: [[RETRYABLE, "requesting"]],
479
+ granted: [[["requesting"], "recording"]],
480
+ microphoneDenied: [[["requesting"], "denied"]],
481
+ microphoneUnavailable: [[["requesting"], "unsupported"]],
482
+ capturedReview: [[["recording"], "review"]],
483
+ capturedEmpty: [[["recording"], "empty"]],
484
+ capturedTooLarge: [[["recording"], "tooLarge"]],
485
+ cancel: [[["requesting", "recording", "review"], "idle"]]
486
+ };
487
+ function canStartRecording(status) {
488
+ return RETRYABLE.includes(status);
489
+ }
490
+ function reduceRecorderStatus(status, event) {
491
+ const match = TRANSITIONS[event].find(([from]) => from.includes(status));
492
+ return match ? match[1] : status;
493
+ }
494
+ function resolveInterruptionAction(status) {
495
+ if (status === "recording") return "stop";
496
+ if (status === "review") return "pause";
497
+ if (status === "requesting") return "cancel";
498
+ return "ignore";
499
+ }
500
+ function lowerOnly(requested, ceiling) {
501
+ if (requested === void 0 || !Number.isFinite(requested) || requested <= 0) return ceiling;
502
+ return Math.min(requested, ceiling);
503
+ }
504
+ function resolveRecordingLimits({ options, channelMaxBytes }) {
505
+ const sizeCeiling = lowerOnly(channelMaxBytes, DEFAULT_MAX_RECORDING_BYTES);
506
+ return {
507
+ maxDurationMs: lowerOnly(options?.maxDurationMs, DEFAULT_MAX_RECORDING_MILLISECONDS),
508
+ maxSizeBytes: lowerOnly(options?.maxSizeBytes, sizeCeiling)
509
+ };
510
+ }
511
+ function classifyCapture({ sizeBytes, durationMs }, limits) {
512
+ if (sizeBytes === 0 || durationMs < MINIMUM_RECORDING_MILLISECONDS) return "capturedEmpty";
513
+ if (sizeBytes > limits.maxSizeBytes) return "capturedTooLarge";
514
+ return "capturedReview";
515
+ }
516
+ function classifyMicrophoneError(error) {
517
+ const name = typeof error === "object" && error !== null && "name" in error ? error.name : void 0;
518
+ return name === "NotAllowedError" || name === "SecurityError" ? "microphoneDenied" : "microphoneUnavailable";
519
+ }
520
+ function formatRecordingClock(milliseconds) {
521
+ const totalSeconds = Math.floor(Math.max(0, milliseconds) / 1e3);
522
+ const seconds = String(totalSeconds % 60).padStart(2, "0");
523
+ return `${Math.floor(totalSeconds / 60)}:${seconds}`;
524
+ }
525
+
526
+ // src/participant/ParticipantRecorderParts.tsx
527
+ import { Mic, Send, Square, Trash2 } from "lucide-react";
528
+
529
+ // src/participant/participantAudioFormat.ts
530
+ var PLAYBACK_RATES = [1, 1.5, 2];
531
+ var SEEK_STEP_SECONDS = 5;
532
+ var UNKNOWN_CLOCK = "--:--";
533
+ function formatAudioClock(seconds) {
534
+ if (seconds === void 0 || !Number.isFinite(seconds) || seconds < 0) return UNKNOWN_CLOCK;
535
+ const wholeSeconds = Math.floor(seconds);
536
+ const remainder = String(wholeSeconds % 60).padStart(2, "0");
537
+ return `${Math.floor(wholeSeconds / 60)}:${remainder}`;
538
+ }
539
+ function nextRateIndex(index) {
540
+ return (index + 1) % PLAYBACK_RATES.length;
541
+ }
542
+ function formatPlaybackRate(rate) {
543
+ return `${rate}\xD7`;
544
+ }
545
+ function resolveServerDurationSeconds(durationMs) {
546
+ if (durationMs === void 0 || !Number.isFinite(durationMs) || durationMs <= 0) return void 0;
547
+ return durationMs / 1e3;
548
+ }
549
+ function resolveMediaDurationSeconds(duration) {
550
+ return Number.isFinite(duration) && duration > 0 ? duration : void 0;
551
+ }
552
+ function clampPosition(position, duration) {
553
+ const floored = Math.max(0, position);
554
+ return duration === void 0 ? floored : Math.min(duration, floored);
555
+ }
556
+ function resolveSeekKeyTarget({ key, position, duration }) {
557
+ if (key === "ArrowRight" || key === "ArrowUp") return clampPosition(position + SEEK_STEP_SECONDS, duration);
558
+ if (key === "ArrowLeft" || key === "ArrowDown") return clampPosition(position - SEEK_STEP_SECONDS, duration);
559
+ if (key === "Home") return 0;
560
+ if (key === "End") return duration;
561
+ return void 0;
562
+ }
563
+ function resolveSeekMax({ duration, maxSeen, position }) {
564
+ if (duration !== void 0) return Math.ceil(duration);
565
+ return Math.ceil(Math.max(maxSeen, position));
566
+ }
567
+ function formatPositionValueText({ template, position, duration }) {
568
+ const positionText = formatAudioClock(position);
569
+ if (duration === void 0) return positionText;
570
+ return template.replace("{position}", positionText).replace("{duration}", formatAudioClock(duration));
571
+ }
572
+
573
+ // src/participant/ParticipantAudioPlayerControls.tsx
574
+ import { Pause, Play } from "lucide-react";
575
+ import { jsx as jsx7 } from "react/jsx-runtime";
576
+ function AudioToggleButton({ label, isPlaying, isPending, isDisabled, onToggle }) {
577
+ return /* @__PURE__ */ jsx7(
578
+ "button",
579
+ {
580
+ type: "button",
581
+ className: "cv-p-audio__toggle",
582
+ "aria-label": label,
583
+ "data-cv-tooltip": label,
584
+ "aria-busy": isPending ? true : void 0,
585
+ disabled: isDisabled,
586
+ onClick: onToggle,
587
+ children: isPlaying ? /* @__PURE__ */ jsx7(Pause, { size: 20, "aria-hidden": "true", focusable: "false" }) : /* @__PURE__ */ jsx7(Play, { size: 20, "aria-hidden": "true", focusable: "false" })
588
+ }
589
+ );
590
+ }
591
+ function AudioSeekRange({ label, max, position, valueText, isDisabled, onSeek, onKeyDown }) {
592
+ return /* @__PURE__ */ jsx7(
593
+ "input",
594
+ {
595
+ type: "range",
596
+ className: "cv-p-audio__seek",
597
+ "aria-label": label,
598
+ min: 0,
599
+ max,
600
+ step: 1,
601
+ value: Math.min(position, max),
602
+ "aria-valuetext": valueText,
603
+ disabled: isDisabled,
604
+ onChange: (event) => onSeek(Number(event.currentTarget.value)),
605
+ onKeyDown
606
+ }
607
+ );
608
+ }
609
+ function AudioRateButton({ label, text, onClick }) {
610
+ return /* @__PURE__ */ jsx7("button", { type: "button", className: "cv-p-audio__rate", "aria-label": label, onClick, children: text });
611
+ }
612
+
613
+ // src/participant/useParticipantAudioPlayer.ts
614
+ import { useCallback, useEffect as useEffect3, useRef as useRef3, useState } from "react";
615
+
616
+ // src/participant/participantAudioCoordinator.ts
617
+ var activeHandle;
618
+ function claimAudioPlayback(handle) {
619
+ if (activeHandle && activeHandle !== handle) activeHandle.pause();
620
+ activeHandle = handle;
621
+ }
622
+ function releaseAudioPlayback(handle) {
623
+ if (activeHandle === handle) activeHandle = void 0;
624
+ }
625
+
626
+ // src/participant/participantAudioPlayerState.ts
627
+ var INITIAL_PLAYER_STATE = {
628
+ isPlaying: false,
629
+ position: 0,
630
+ mediaDuration: void 0,
631
+ maxSeen: 0,
632
+ rateIndex: 0,
633
+ hasFailed: false
634
+ };
635
+ function applyPlaybackRate(audio, rateIndex) {
636
+ const rate = PLAYBACK_RATES[rateIndex] ?? 1;
637
+ audio.defaultPlaybackRate = rate;
638
+ audio.playbackRate = rate;
639
+ }
640
+
641
+ // src/participant/useParticipantAudioSourceRetry.ts
642
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
643
+ function useParticipantAudioSourceRetry({ context, url, onSourceError }) {
644
+ const { audioRef, update } = context;
645
+ const canRetryRef = useRef2(true);
646
+ const resumeAtRef = useRef2(void 0);
647
+ useEffect2(() => {
648
+ const audio = audioRef.current;
649
+ const resumeAt = resumeAtRef.current;
650
+ if (!audio || resumeAt === void 0 || !url) return;
651
+ resumeAtRef.current = void 0;
652
+ audio.currentTime = resumeAt;
653
+ Promise.resolve(audio.play()).catch(() => update({ isPlaying: false }));
654
+ }, [url, audioRef, update]);
655
+ function allowRetry() {
656
+ canRetryRef.current = true;
657
+ }
658
+ function handleError(event) {
659
+ if (!onSourceError || !canRetryRef.current) return update({ hasFailed: true, isPlaying: false });
660
+ canRetryRef.current = false;
661
+ resumeAtRef.current = event.currentTarget.currentTime;
662
+ onSourceError().then((fresh) => {
663
+ if (fresh && fresh !== url) return;
664
+ resumeAtRef.current = void 0;
665
+ update({ hasFailed: true, isPlaying: false });
666
+ });
667
+ }
668
+ return { handleError, allowRetry };
669
+ }
670
+
671
+ // src/participant/useParticipantAudioMediaEvents.ts
672
+ function useParticipantAudioMediaEvents({ context, url, onSourceError }) {
673
+ const { handleRef, state, update } = context;
674
+ const { handleError, allowRetry } = useParticipantAudioSourceRetry({ context, url, onSourceError });
675
+ function handleStop() {
676
+ releaseAudioPlayback(handleRef.current);
677
+ update({ isPlaying: false });
678
+ }
679
+ function handleDuration(event) {
680
+ update({ mediaDuration: resolveMediaDurationSeconds(event.currentTarget.duration) });
681
+ }
682
+ return {
683
+ onPlay: () => {
684
+ claimAudioPlayback(handleRef.current);
685
+ update({ isPlaying: true });
686
+ },
687
+ onPause: handleStop,
688
+ onEnded: handleStop,
689
+ onTimeUpdate: (event) => {
690
+ const position = event.currentTarget.currentTime;
691
+ update({ position, maxSeen: Math.max(state.maxSeen, position) });
692
+ },
693
+ onLoadedMetadata: (event) => {
694
+ allowRetry();
695
+ handleDuration(event);
696
+ },
697
+ onDurationChange: handleDuration,
698
+ onError: handleError
699
+ };
700
+ }
701
+
702
+ // src/participant/useParticipantAudioPlayerActions.ts
703
+ function useParticipantAudioPlayerActions({ context, url, duration }) {
704
+ const { audioRef, handleRef, state, update } = context;
705
+ function handleToggle() {
706
+ const audio = audioRef.current;
707
+ if (!audio || !url) return;
708
+ if (!audio.paused) return audio.pause();
709
+ claimAudioPlayback(handleRef.current);
710
+ applyPlaybackRate(audio, state.rateIndex);
711
+ Promise.resolve(audio.play()).catch(() => update({ isPlaying: false }));
712
+ }
713
+ function seekTo(seconds) {
714
+ const audio = audioRef.current;
715
+ if (!audio || !url) return;
716
+ audio.currentTime = seconds;
717
+ update({ position: seconds, maxSeen: Math.max(state.maxSeen, seconds) });
718
+ }
719
+ function handleKeyDown(event) {
720
+ const position = audioRef.current?.currentTime ?? state.position;
721
+ const target = resolveSeekKeyTarget({ key: event.key, position, duration });
722
+ if (target === void 0) return;
723
+ event.preventDefault();
724
+ seekTo(target);
725
+ }
726
+ function handleRateClick() {
727
+ const rateIndex = nextRateIndex(state.rateIndex);
728
+ if (audioRef.current) applyPlaybackRate(audioRef.current, rateIndex);
729
+ update({ rateIndex });
730
+ }
731
+ return { handleToggle, seekTo, handleKeyDown, handleRateClick };
732
+ }
733
+
734
+ // src/participant/useParticipantAudioPlayer.ts
735
+ function assignRef(target, element) {
736
+ if (typeof target === "function") target(element);
737
+ else if (target) target.current = element;
738
+ }
739
+ function useParticipantAudioPlayer({ url, durationMs, onSourceError, mediaRef }) {
740
+ const audioRef = useRef3(null);
741
+ const handleRef = useRef3({ pause: () => audioRef.current?.pause() });
742
+ const [state, setState2] = useState(INITIAL_PLAYER_STATE);
743
+ const update = useCallback((patch) => setState2((previous) => ({ ...previous, ...patch })), []);
744
+ const mediaEvents = useParticipantAudioMediaEvents({ context: { audioRef, handleRef, state, update }, url, onSourceError });
745
+ const duration = resolveServerDurationSeconds(durationMs) ?? state.mediaDuration;
746
+ const setMedia = useCallback(
747
+ (element) => {
748
+ audioRef.current = element;
749
+ assignRef(mediaRef, element);
750
+ },
751
+ [mediaRef]
752
+ );
753
+ useEffect3(() => {
754
+ const handle = handleRef.current;
755
+ return () => releaseAudioPlayback(handle);
756
+ }, []);
757
+ const actions = useParticipantAudioPlayerActions({ context: { audioRef, handleRef, state, update }, url, duration });
758
+ return {
759
+ state,
760
+ duration,
761
+ rate: PLAYBACK_RATES[state.rateIndex] ?? 1,
762
+ mediaProps: { ...mediaEvents, ref: setMedia },
763
+ ...actions
764
+ };
765
+ }
766
+
767
+ // src/participant/ParticipantAudioPlayer.tsx
768
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
769
+ function fillRate(template, rate) {
770
+ return template.replace("{rate}", formatPlaybackRate(rate));
771
+ }
772
+ function ParticipantAudioPlayer({
773
+ labels,
774
+ url,
775
+ durationMs,
776
+ filename,
777
+ isPending = false,
778
+ isUnavailable = false,
779
+ onSourceError,
780
+ mediaRef
781
+ }) {
782
+ const player = useParticipantAudioPlayer({ url, durationMs, onSourceError, mediaRef });
783
+ const { state, duration, rate } = player;
784
+ const position = Math.floor(state.position);
785
+ const hasFailed = isUnavailable || state.hasFailed;
786
+ return /* @__PURE__ */ jsxs6("div", { className: "cv-p-audio", role: "group", "aria-label": filename ? `${labels.audioPlayer}: ${filename}` : labels.audioPlayer, children: [
787
+ /* @__PURE__ */ jsx8(
788
+ AudioToggleButton,
789
+ {
790
+ label: state.isPlaying ? labels.pauseAudio : labels.playAudio,
791
+ isPlaying: state.isPlaying,
792
+ isPending,
793
+ isDisabled: hasFailed,
794
+ onToggle: player.handleToggle
795
+ }
796
+ ),
797
+ /* @__PURE__ */ jsx8(
798
+ AudioSeekRange,
799
+ {
800
+ label: labels.audioPosition,
801
+ max: resolveSeekMax({ duration, maxSeen: state.maxSeen, position }),
802
+ position,
803
+ valueText: formatPositionValueText({ template: labels.audioPositionValue, position, duration }),
804
+ isDisabled: hasFailed,
805
+ onSeek: player.seekTo,
806
+ onKeyDown: player.handleKeyDown
807
+ }
808
+ ),
809
+ /* @__PURE__ */ jsx8("span", { className: "cv-p-audio__time", children: `${formatAudioClock(position)} / ${formatAudioClock(duration)}` }),
810
+ /* @__PURE__ */ jsx8(AudioRateButton, { label: fillRate(labels.playbackRate, rate), text: formatPlaybackRate(rate), onClick: player.handleRateClick }),
811
+ /* @__PURE__ */ jsx8("audio", { className: "cv-p-audio__media", preload: "none", src: url, ...player.mediaProps }),
812
+ hasFailed ? /* @__PURE__ */ jsx8("span", { className: "cv-p-audio__error", children: labels.audioUnavailable }) : null
813
+ ] });
814
+ }
815
+
816
+ // src/participant/ParticipantRecorderParts.tsx
817
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
818
+ function RecordAudioButton({ label, isBusy, disabled, onClick, buttonRef }) {
819
+ return /* @__PURE__ */ jsx9(
820
+ "button",
821
+ {
822
+ ref: buttonRef,
823
+ type: "button",
824
+ className: "cv-p-button cv-p-button--primary cv-p-composer__record",
825
+ "aria-label": label,
826
+ "data-cv-tooltip": label,
827
+ "aria-busy": isBusy ? true : void 0,
828
+ disabled: disabled || isBusy,
829
+ onClick,
830
+ children: /* @__PURE__ */ jsx9(Mic, { size: 20, "aria-hidden": "true", focusable: "false" })
831
+ }
832
+ );
833
+ }
834
+ function RecorderError({ message }) {
835
+ return /* @__PURE__ */ jsx9("p", { className: "cv-p-composer__error", role: "alert", children: message });
836
+ }
837
+ function focusOnMount(element) {
838
+ element?.focus();
839
+ }
840
+ function DiscardButton({ labels, onDiscard }) {
841
+ return /* @__PURE__ */ jsx9(
842
+ "button",
843
+ {
844
+ type: "button",
845
+ className: "cv-p-button cv-p-recorder__discard",
846
+ "aria-label": labels.discardAudio,
847
+ "data-cv-tooltip": labels.discardAudio,
848
+ onClick: onDiscard,
849
+ children: /* @__PURE__ */ jsx9(Trash2, { size: 20, "aria-hidden": "true", focusable: "false" })
850
+ }
851
+ );
852
+ }
853
+ function RecordingGroup({ labels, clockText, onStop, onDiscard }) {
854
+ return /* @__PURE__ */ jsxs7("div", { className: "cv-p-recorder", role: "group", "aria-label": labels.recordingInProgress, children: [
855
+ /* @__PURE__ */ jsx9(DiscardButton, { labels, onDiscard }),
856
+ /* @__PURE__ */ jsx9("span", { className: "cv-p-recorder__dot", "aria-hidden": "true" }),
857
+ /* @__PURE__ */ jsx9("span", { className: "cv-p-recorder__elapsed", role: "timer", children: clockText }),
858
+ /* @__PURE__ */ jsx9(
859
+ "button",
860
+ {
861
+ type: "button",
862
+ ref: focusOnMount,
863
+ className: "cv-p-button cv-p-button--primary cv-p-recorder__stop",
864
+ "aria-label": labels.stopRecording,
865
+ "data-cv-tooltip": labels.stopRecording,
866
+ onClick: onStop,
867
+ children: /* @__PURE__ */ jsx9(Square, { size: 20, "aria-hidden": "true", focusable: "false" })
868
+ }
869
+ )
870
+ ] });
871
+ }
872
+ function ReviewGroup({ labels, url, audioRef, durationMs, onSend, onDiscard, disabled = false }) {
873
+ return /* @__PURE__ */ jsxs7("div", { className: "cv-p-recorder cv-p-recorder--review", role: "group", "aria-label": labels.reviewAudio, children: [
874
+ /* @__PURE__ */ jsx9("p", { className: "cv-p-recorder__hint", "aria-hidden": "true", children: labels.reviewAudio }),
875
+ /* @__PURE__ */ jsx9(DiscardButton, { labels, onDiscard }),
876
+ /* @__PURE__ */ jsx9(ParticipantAudioPlayer, { labels, url, durationMs, mediaRef: audioRef }),
877
+ /* @__PURE__ */ jsx9(
878
+ "button",
879
+ {
880
+ type: "button",
881
+ ref: focusOnMount,
882
+ className: "cv-p-button cv-p-button--primary cv-p-recorder__send",
883
+ "aria-label": labels.sendAudio,
884
+ "data-cv-tooltip": labels.sendAudio,
885
+ disabled,
886
+ onClick: onSend,
887
+ children: /* @__PURE__ */ jsx9(Send, { size: 20, "aria-hidden": "true", focusable: "false" })
888
+ }
889
+ )
890
+ ] });
891
+ }
892
+
893
+ // src/participant/ParticipantAudioRecorderView.tsx
894
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
895
+ var ERROR_LABEL_BY_STATE = {
896
+ denied: "microphoneDenied",
897
+ unsupported: "recordingUnsupported",
898
+ empty: "audioEmpty",
899
+ tooLarge: "audioTooLarge"
900
+ };
901
+ function resolveRecorderErrorMessage(state, labels) {
902
+ return state in ERROR_LABEL_BY_STATE ? labels[ERROR_LABEL_BY_STATE[state]] : void 0;
903
+ }
904
+ function ParticipantAudioRecorderView(props) {
905
+ const { state, elapsedMs, maxDurationMs = DEFAULT_MAX_RECORDING_MILLISECONDS, review, actions, disabled = false } = props;
906
+ const labels = resolveParticipantAudioLabels(props.labels);
907
+ if (state === "recording") {
908
+ const clockText = formatRecordingElapsedLabel(labels.recordingElapsed, formatRecordingClock(elapsedMs), formatRecordingClock(maxDurationMs));
909
+ return /* @__PURE__ */ jsx10(RecordingGroup, { labels, clockText, onStop: actions?.onStop, onDiscard: actions?.onDiscard });
910
+ }
911
+ if (state === "review") {
912
+ return /* @__PURE__ */ jsx10(ReviewGroup, { labels, url: review?.url, audioRef: review?.audioRef, durationMs: review?.durationMs, onSend: actions?.onSend, onDiscard: actions?.onDiscard, disabled });
913
+ }
914
+ const errorMessage = resolveRecorderErrorMessage(state, labels);
915
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
916
+ errorMessage ? /* @__PURE__ */ jsx10(RecorderError, { message: errorMessage }) : null,
917
+ /* @__PURE__ */ jsx10(RecordAudioButton, { label: labels.recordAudio, isBusy: state === "requesting", disabled, onClick: actions?.onStart })
918
+ ] });
919
+ }
920
+
921
+ // src/participant/useParticipantAudioRecorder.ts
922
+ import { useCallback as useCallback3, useMemo } from "react";
923
+
924
+ // src/participant/participantAudioCapture.ts
925
+ function stopStream(stream) {
926
+ for (const track of stream.getTracks()) track.stop();
927
+ }
928
+ function formatRecordedFileName(moment, extension) {
929
+ const iso = moment.toISOString();
930
+ const day = iso.slice(0, 10).replace(/-/g, "");
931
+ const time = iso.slice(11, 19).replace(/:/g, "");
932
+ return `audio-${day}-${time}.${extension}`;
933
+ }
934
+ function buildRecordedFile(blob, format, moment = /* @__PURE__ */ new Date()) {
935
+ return new File([blob], formatRecordedFileName(moment, format.extension), { type: format.uploadMimeType });
936
+ }
937
+ function createMuteWatch(onPersistentMute) {
938
+ let timer;
939
+ const clear = () => {
940
+ if (timer !== void 0) clearTimeout(timer);
941
+ timer = void 0;
942
+ };
943
+ const arm = () => {
944
+ clear();
945
+ timer = setTimeout(onPersistentMute, MUTE_INTERRUPTION_DELAY_MS);
946
+ };
947
+ return { arm, clear };
948
+ }
949
+ function openCaptureSession({ stream, format, onInterrupt, onStopped }) {
950
+ const recorder = new MediaRecorder(stream, { mimeType: format.mimeType });
951
+ const chunks = [];
952
+ const startedAt = Date.now();
953
+ const muteWatch = createMuteWatch(onInterrupt);
954
+ for (const track of stream.getTracks()) {
955
+ track.onended = onInterrupt;
956
+ track.onmute = muteWatch.arm;
957
+ track.onunmute = muteWatch.clear;
958
+ }
959
+ recorder.ondataavailable = (event) => {
960
+ if (event.data.size > 0) chunks.push(event.data);
961
+ };
962
+ recorder.onerror = onInterrupt;
963
+ recorder.onstop = () => onStopped(new Blob(chunks, { type: format.uploadMimeType }), Date.now() - startedAt);
964
+ recorder.start();
965
+ let isStopRequested = false;
966
+ function requestStop() {
967
+ if (isStopRequested || recorder.state === "inactive") return;
968
+ isStopRequested = true;
969
+ recorder.stop();
970
+ }
971
+ function release() {
972
+ muteWatch.clear();
973
+ recorder.ondataavailable = null;
974
+ recorder.onerror = null;
975
+ recorder.onstop = null;
976
+ for (const track of stream.getTracks()) {
977
+ track.onended = null;
978
+ track.onmute = null;
979
+ track.onunmute = null;
980
+ }
981
+ stopStream(stream);
982
+ }
983
+ return { format, startedAt, requestStop, release };
984
+ }
985
+
986
+ // src/participant/useParticipantAudioRecorderCore.ts
987
+ import { useCallback as useCallback2, useRef as useRef4, useState as useState2 } from "react";
988
+ function useRecorderCore(limits) {
989
+ const [status, setStatus] = useState2("idle");
990
+ const [elapsedMs, setElapsedMs] = useState2(0);
991
+ const [recorded, setRecorded] = useState2();
992
+ const statusRef = useRef4("idle");
993
+ const limitsRef = useRef4(limits);
994
+ const tokenRef = useRef4(0);
995
+ const sessionRef = useRef4(void 0);
996
+ const recordedRef = useRef4(void 0);
997
+ const audioRef = useRef4(null);
998
+ limitsRef.current = limits;
999
+ const moveTo = useCallback2((event) => {
1000
+ statusRef.current = reduceRecorderStatus(statusRef.current, event);
1001
+ setStatus(statusRef.current);
1002
+ }, []);
1003
+ const releaseSession = useCallback2(() => {
1004
+ sessionRef.current?.release();
1005
+ sessionRef.current = void 0;
1006
+ }, []);
1007
+ const releaseRecorded = useCallback2(() => {
1008
+ if (recordedRef.current) URL.revokeObjectURL(recordedRef.current.objectUrl);
1009
+ recordedRef.current = void 0;
1010
+ setRecorded(void 0);
1011
+ }, []);
1012
+ return {
1013
+ status,
1014
+ elapsedMs,
1015
+ setElapsedMs,
1016
+ recorded,
1017
+ setRecorded,
1018
+ limitsRef,
1019
+ statusRef,
1020
+ tokenRef,
1021
+ sessionRef,
1022
+ recordedRef,
1023
+ audioRef,
1024
+ moveTo,
1025
+ releaseSession,
1026
+ releaseRecorded
1027
+ };
1028
+ }
1029
+
1030
+ // src/participant/useParticipantAudioRecorderEffects.ts
1031
+ import { useEffect as useEffect4, useRef as useRef5 } from "react";
1032
+ var ELAPSED_TICK_MS = 250;
1033
+ function useRecorderInterruptionListeners(onInterruption) {
1034
+ useEffect4(() => {
1035
+ const handleVisibilityChange = () => {
1036
+ if (document.visibilityState === "hidden") onInterruption();
1037
+ };
1038
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1039
+ window.addEventListener("pagehide", onInterruption);
1040
+ return () => {
1041
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1042
+ window.removeEventListener("pagehide", onInterruption);
1043
+ };
1044
+ }, [onInterruption]);
1045
+ }
1046
+ function useRecordingClock({ status, maxDurationMs, sessionRef, setElapsedMs, stop }) {
1047
+ useEffect4(() => {
1048
+ if (status !== "recording") return void 0;
1049
+ const startedAt = sessionRef.current?.startedAt ?? Date.now();
1050
+ const tick = setInterval(() => setElapsedMs(Date.now() - startedAt), ELAPSED_TICK_MS);
1051
+ const ceiling = setTimeout(stop, maxDurationMs);
1052
+ return () => {
1053
+ clearInterval(tick);
1054
+ clearTimeout(ceiling);
1055
+ };
1056
+ }, [status, maxDurationMs, sessionRef, setElapsedMs, stop]);
1057
+ }
1058
+ function useRecorderLifecycle({ status, onCaptureChange, teardown }) {
1059
+ const isCapturing = status === "requesting" || status === "recording" || status === "review";
1060
+ const reportedRef = useRef5(false);
1061
+ const onCaptureChangeRef = useRef5(onCaptureChange);
1062
+ onCaptureChangeRef.current = onCaptureChange;
1063
+ useEffect4(() => {
1064
+ if (reportedRef.current === isCapturing) return;
1065
+ reportedRef.current = isCapturing;
1066
+ onCaptureChangeRef.current?.(isCapturing);
1067
+ }, [isCapturing]);
1068
+ useEffect4(
1069
+ () => () => {
1070
+ teardown();
1071
+ if (!reportedRef.current) return;
1072
+ reportedRef.current = false;
1073
+ onCaptureChangeRef.current?.(false);
1074
+ },
1075
+ [teardown]
1076
+ );
1077
+ }
1078
+
1079
+ // src/participant/useParticipantAudioRecorder.ts
1080
+ function useRecorderFinalize(core) {
1081
+ const { statusRef, sessionRef, recordedRef, limitsRef, setRecorded, moveTo, releaseSession } = core;
1082
+ return useCallback3(
1083
+ (blob, durationMs) => {
1084
+ const format = sessionRef.current?.format;
1085
+ if (statusRef.current !== "recording" || !format) return;
1086
+ releaseSession();
1087
+ const event = classifyCapture({ sizeBytes: blob.size, durationMs }, limitsRef.current);
1088
+ if (event === "capturedReview") {
1089
+ const file = buildRecordedFile(blob, format);
1090
+ recordedRef.current = { file, objectUrl: URL.createObjectURL(file), durationMs };
1091
+ setRecorded(recordedRef.current);
1092
+ }
1093
+ moveTo(event);
1094
+ },
1095
+ [statusRef, sessionRef, recordedRef, limitsRef, setRecorded, moveTo, releaseSession]
1096
+ );
1097
+ }
1098
+ function useRecorderInterruption(core) {
1099
+ const { statusRef, sessionRef, tokenRef, audioRef, moveTo } = core;
1100
+ return useCallback3(() => {
1101
+ const action = resolveInterruptionAction(statusRef.current);
1102
+ if (action === "stop") sessionRef.current?.requestStop();
1103
+ if (action === "pause") audioRef.current?.pause();
1104
+ if (action === "cancel") {
1105
+ tokenRef.current += 1;
1106
+ moveTo("cancel");
1107
+ }
1108
+ }, [statusRef, sessionRef, tokenRef, audioRef, moveTo]);
1109
+ }
1110
+ function useRecorderStart(core, { onInterrupt, onStopped }) {
1111
+ const { statusRef, tokenRef, sessionRef, setElapsedMs, moveTo } = core;
1112
+ const beginRecording = useCallback3(
1113
+ (stream, format) => {
1114
+ try {
1115
+ sessionRef.current = openCaptureSession({ stream, format, onInterrupt, onStopped });
1116
+ } catch {
1117
+ stopStream(stream);
1118
+ moveTo("microphoneUnavailable");
1119
+ return;
1120
+ }
1121
+ setElapsedMs(0);
1122
+ moveTo("granted");
1123
+ },
1124
+ [sessionRef, setElapsedMs, onInterrupt, onStopped, moveTo]
1125
+ );
1126
+ return useCallback3(async () => {
1127
+ if (!canStartRecording(statusRef.current)) return;
1128
+ const token = tokenRef.current += 1;
1129
+ moveTo("request");
1130
+ const format = resolveRecordingFormat();
1131
+ if (!format || !navigator.mediaDevices?.getUserMedia) return moveTo("microphoneUnavailable");
1132
+ let stream;
1133
+ try {
1134
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1135
+ } catch (error) {
1136
+ if (token === tokenRef.current) moveTo(classifyMicrophoneError(error));
1137
+ return;
1138
+ }
1139
+ if (token !== tokenRef.current) return stopStream(stream);
1140
+ beginRecording(stream, format);
1141
+ }, [statusRef, tokenRef, moveTo, beginRecording]);
1142
+ }
1143
+ function useRecorderActions(core) {
1144
+ const { statusRef, tokenRef, sessionRef, recordedRef, moveTo, releaseSession, releaseRecorded } = core;
1145
+ const stop = useCallback3(() => sessionRef.current?.requestStop(), [sessionRef]);
1146
+ const discard = useCallback3(() => {
1147
+ tokenRef.current += 1;
1148
+ releaseSession();
1149
+ releaseRecorded();
1150
+ moveTo("cancel");
1151
+ }, [tokenRef, moveTo, releaseRecorded, releaseSession]);
1152
+ const confirm = useCallback3(() => {
1153
+ if (statusRef.current !== "review") return void 0;
1154
+ const file = recordedRef.current?.file;
1155
+ releaseRecorded();
1156
+ moveTo("cancel");
1157
+ return file;
1158
+ }, [statusRef, recordedRef, moveTo, releaseRecorded]);
1159
+ const teardown = useCallback3(() => {
1160
+ tokenRef.current += 1;
1161
+ releaseSession();
1162
+ if (recordedRef.current) URL.revokeObjectURL(recordedRef.current.objectUrl);
1163
+ recordedRef.current = void 0;
1164
+ }, [tokenRef, recordedRef, releaseSession]);
1165
+ return { stop, discard, confirm, teardown };
1166
+ }
1167
+ function useParticipantAudioRecorder({ options, channelMaxBytes } = {}) {
1168
+ const limits = useMemo(
1169
+ () => resolveRecordingLimits({ options, channelMaxBytes }),
1170
+ [options?.maxDurationMs, options?.maxSizeBytes, channelMaxBytes]
1171
+ );
1172
+ const core = useRecorderCore(limits);
1173
+ const onInterrupt = useRecorderInterruption(core);
1174
+ const onStopped = useRecorderFinalize(core);
1175
+ const start = useRecorderStart(core, { onInterrupt, onStopped });
1176
+ const { stop, discard, confirm, teardown } = useRecorderActions(core);
1177
+ useRecorderInterruptionListeners(onInterrupt);
1178
+ useRecordingClock({ status: core.status, maxDurationMs: limits.maxDurationMs, sessionRef: core.sessionRef, setElapsedMs: core.setElapsedMs, stop });
1179
+ useRecorderLifecycle({ status: core.status, onCaptureChange: options?.onCaptureChange, teardown });
1180
+ const { status, elapsedMs, recorded, audioRef } = core;
1181
+ return { status, elapsedMs, limits, recorded, audioRef, start, stop, discard, confirm };
1182
+ }
1183
+
1184
+ // src/participant/ParticipantComposerAudio.tsx
1185
+ import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1186
+ function isCapturingStatus(status) {
1187
+ return status === "recording" || status === "review";
1188
+ }
1189
+ function useRestoreMicrophoneFocus(status) {
1190
+ const microphoneRef = useRef6(null);
1191
+ const wasCapturingRef = useRef6(false);
1192
+ useEffect5(() => {
1193
+ const isCapturing = isCapturingStatus(status);
1194
+ if (wasCapturingRef.current && !isCapturing) microphoneRef.current?.focus();
1195
+ wasCapturingRef.current = isCapturing;
1196
+ }, [status]);
1197
+ return microphoneRef;
1198
+ }
1199
+ function RecorderLine({ recorder, labels, onSendAudio, disabled }) {
1200
+ function handleSendAudio() {
1201
+ const file = recorder.confirm();
1202
+ if (file) onSendAudio(file);
1203
+ }
1204
+ return /* @__PURE__ */ jsx11(
1205
+ ParticipantAudioRecorderView,
1206
+ {
1207
+ state: recorder.status,
1208
+ elapsedMs: recorder.elapsedMs,
1209
+ labels,
1210
+ disabled,
1211
+ maxDurationMs: recorder.limits.maxDurationMs,
1212
+ review: { url: recorder.recorded?.objectUrl, audioRef: recorder.audioRef, durationMs: recorder.recorded?.durationMs },
1213
+ actions: { onStop: recorder.stop, onSend: handleSendAudio, onDiscard: recorder.discard }
1214
+ }
1215
+ );
1216
+ }
1217
+ function ComposerAudioRow({ settings, labels, disabled, isDraftEmpty, sendButton, children }) {
1218
+ const recorder = useParticipantAudioRecorder({
1219
+ options: settings.audioRecording,
1220
+ channelMaxBytes: maxBytesForChannel(settings.channel)
1221
+ });
1222
+ const microphoneRef = useRestoreMicrophoneFocus(recorder.status);
1223
+ const audioLabels = resolveParticipantAudioLabels(labels);
1224
+ const errorMessage = resolveRecorderErrorMessage(recorder.status, audioLabels);
1225
+ if (isCapturingStatus(recorder.status)) {
1226
+ return /* @__PURE__ */ jsx11(RecorderLine, { recorder, labels, onSendAudio: settings.onSendAudio, disabled });
1227
+ }
1228
+ return /* @__PURE__ */ jsxs9(Fragment4, { children: [
1229
+ errorMessage ? /* @__PURE__ */ jsx11(RecorderError, { message: errorMessage }) : null,
1230
+ /* @__PURE__ */ jsxs9("div", { className: "cv-p-composer__row", children: [
1231
+ children,
1232
+ isDraftEmpty ? /* @__PURE__ */ jsx11(
1233
+ RecordAudioButton,
1234
+ {
1235
+ label: audioLabels.recordAudio,
1236
+ isBusy: recorder.status === "requesting",
1237
+ disabled,
1238
+ onClick: () => void recorder.start(),
1239
+ buttonRef: microphoneRef
1240
+ }
1241
+ ) : sendButton
1242
+ ] })
1243
+ ] });
1244
+ }
399
1245
 
400
1246
  // src/participant/ParticipantComposerControls.tsx
401
- import { useLayoutEffect, useRef as useRef2 } from "react";
402
- import { Paperclip, Send } from "lucide-react";
1247
+ import { useLayoutEffect, useRef as useRef7 } from "react";
1248
+ import { Paperclip, Send as Send2 } from "lucide-react";
403
1249
 
404
1250
  // src/participant/participantAutoGrow.ts
405
1251
  function fitTextareaHeight(target) {
@@ -408,11 +1254,11 @@ function fitTextareaHeight(target) {
408
1254
  }
409
1255
 
410
1256
  // src/participant/ParticipantComposerControls.tsx
411
- import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1257
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
412
1258
  function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
413
- const fileInputRef = useRef2(null);
414
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
415
- /* @__PURE__ */ jsx7(
1259
+ const fileInputRef = useRef7(null);
1260
+ return /* @__PURE__ */ jsxs10(Fragment5, { children: [
1261
+ /* @__PURE__ */ jsx12(
416
1262
  "input",
417
1263
  {
418
1264
  ref: fileInputRef,
@@ -424,7 +1270,7 @@ function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
424
1270
  onChange: onChosen
425
1271
  }
426
1272
  ),
427
- /* @__PURE__ */ jsx7(
1273
+ /* @__PURE__ */ jsx12(
428
1274
  "button",
429
1275
  {
430
1276
  type: "button",
@@ -433,13 +1279,13 @@ function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
433
1279
  "data-cv-tooltip": labels.attach,
434
1280
  disabled,
435
1281
  onClick: () => fileInputRef.current?.click(),
436
- children: /* @__PURE__ */ jsx7(Paperclip, { size: 24, "aria-hidden": "true", focusable: "false" })
1282
+ children: /* @__PURE__ */ jsx12(Paperclip, { size: 24, "aria-hidden": "true", focusable: "false" })
437
1283
  }
438
1284
  )
439
1285
  ] });
440
1286
  }
441
1287
  function SendButton({ label, disabled }) {
442
- return /* @__PURE__ */ jsx7(
1288
+ return /* @__PURE__ */ jsx12(
443
1289
  "button",
444
1290
  {
445
1291
  type: "submit",
@@ -447,16 +1293,16 @@ function SendButton({ label, disabled }) {
447
1293
  "aria-label": label,
448
1294
  "data-cv-tooltip": label,
449
1295
  disabled,
450
- children: /* @__PURE__ */ jsx7(Send, { size: 20, "aria-hidden": "true", focusable: "false" })
1296
+ children: /* @__PURE__ */ jsx12(Send2, { size: 20, "aria-hidden": "true", focusable: "false" })
451
1297
  }
452
1298
  );
453
1299
  }
454
1300
  function MessageField({ value, label, placeholder, disabled, maxLength, onChange }) {
455
- const fieldRef = useRef2(null);
1301
+ const fieldRef = useRef7(null);
456
1302
  useLayoutEffect(() => {
457
1303
  if (fieldRef.current) fitTextareaHeight(fieldRef.current);
458
1304
  }, [value]);
459
- return /* @__PURE__ */ jsx7(
1305
+ return /* @__PURE__ */ jsx12(
460
1306
  "textarea",
461
1307
  {
462
1308
  ref: fieldRef,
@@ -472,29 +1318,20 @@ function MessageField({ value, label, placeholder, disabled, maxLength, onChange
472
1318
  );
473
1319
  }
474
1320
 
475
- // src/participant/participantFileValidation.ts
476
- function matchesAcceptedType(mimeType, acceptedTypes) {
477
- return acceptedTypes.some(
478
- (accepted) => accepted.endsWith("/*") ? mimeType.startsWith(accepted.slice(0, -1)) : mimeType === accepted
479
- );
480
- }
481
- function rejectionOf(file, params) {
482
- if (params.acceptedTypes && !matchesAcceptedType(file.type, params.acceptedTypes)) return "typeNotAccepted";
483
- if (file.size > params.maxBytes) return "tooLarge";
484
- return void 0;
485
- }
486
- function reduceFilesSelection(params) {
487
- const files = [...params.current];
488
- const rejected = [];
489
- for (const file of params.incoming) {
490
- const reason = rejectionOf(file, params);
491
- if (reason === void 0) files.push(file);
492
- else rejected.push({ file, reason });
493
- }
494
- return { files, rejected };
1321
+ // src/participant/participantAudioSupport.ts
1322
+ function resolveBrowserRecordingFormat() {
1323
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) return void 0;
1324
+ return resolveRecordingFormat();
495
1325
  }
496
- function maxBytesForChannel(channel = "app") {
497
- return channelCapabilityFor(channel).attachments.maxBytes;
1326
+ function resolveComposerAudio(params) {
1327
+ const { audioRecording, onSendAudio, channel = "app", acceptedTypes } = params;
1328
+ if (!audioRecording || !onSendAudio) return void 0;
1329
+ const capability = channelCapabilityFor(channel);
1330
+ if (!capability.audio.records || !capability.attachments.accepted) return void 0;
1331
+ const format = resolveBrowserRecordingFormat();
1332
+ if (!format) return void 0;
1333
+ if (acceptedTypes && !matchesAcceptedType(format.uploadMimeType, acceptedTypes)) return void 0;
1334
+ return { audioRecording, channel, onSendAudio };
498
1335
  }
499
1336
 
500
1337
  // src/participant/participantQuickReply.ts
@@ -505,18 +1342,18 @@ function applyQuickReply(currentText, replyText) {
505
1342
  }
506
1343
 
507
1344
  // src/participant/ParticipantComposer.tsx
508
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1345
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
509
1346
  function QuickReplyStrip({ quickReplies, labels, onPick }) {
510
- return /* @__PURE__ */ jsx8("div", { className: "cv-p-quick", role: "group", "aria-label": labels.quickRepliesGroup, children: quickReplies.map((quickReply) => /* @__PURE__ */ jsx8("button", { type: "button", className: "cv-p-chip", onClick: () => onPick(quickReply.body), children: quickReply.title }, quickReply.id)) });
1347
+ return /* @__PURE__ */ jsx13("div", { className: "cv-p-quick", role: "group", "aria-label": labels.quickRepliesGroup, children: quickReplies.map((quickReply) => /* @__PURE__ */ jsx13("button", { type: "button", className: "cv-p-chip", onClick: () => onPick(quickReply.body), children: quickReply.title }, quickReply.id)) });
511
1348
  }
512
1349
  function FileList({ files, labels, onRemove }) {
513
1350
  if (files.length === 0) return null;
514
- return /* @__PURE__ */ jsx8("ul", { className: "cv-p-files", "aria-label": labels.attachmentsList, children: files.map((file, index) => /* @__PURE__ */ jsxs7("li", { className: "cv-p-files__item", children: [
515
- /* @__PURE__ */ jsx8("span", { className: "cv-p-files__name", children: file.name }),
516
- /* @__PURE__ */ jsx8("span", { className: "cv-p-files__size", children: formatFileSize(file.size) }),
517
- /* @__PURE__ */ jsxs7("button", { type: "button", className: "cv-p-files__remove", "aria-label": `${labels.removeAttachment}: ${file.name}`, "data-cv-tooltip": labels.removeAttachment, onClick: () => onRemove(file), children: [
518
- /* @__PURE__ */ jsx8("span", { "aria-hidden": "true", children: "\xD7" }),
519
- /* @__PURE__ */ jsx8("span", { className: "cv-p-sr-only", children: labels.removeAttachment })
1351
+ return /* @__PURE__ */ jsx13("ul", { className: "cv-p-files", "aria-label": labels.attachmentsList, children: files.map((file, index) => /* @__PURE__ */ jsxs11("li", { className: "cv-p-files__item", children: [
1352
+ /* @__PURE__ */ jsx13("span", { className: "cv-p-files__name", children: file.name }),
1353
+ /* @__PURE__ */ jsx13("span", { className: "cv-p-files__size", children: formatFileSize(file.size) }),
1354
+ /* @__PURE__ */ jsxs11("button", { type: "button", className: "cv-p-files__remove", "aria-label": `${labels.removeAttachment}: ${file.name}`, "data-cv-tooltip": labels.removeAttachment, onClick: () => onRemove(file), children: [
1355
+ /* @__PURE__ */ jsx13("span", { "aria-hidden": "true", children: "\xD7" }),
1356
+ /* @__PURE__ */ jsx13("span", { className: "cv-p-sr-only", children: labels.removeAttachment })
520
1357
  ] })
521
1358
  ] }, `${file.name}-${file.size}-${index}`)) });
522
1359
  }
@@ -524,7 +1361,7 @@ function rejectionText(rejection, labels) {
524
1361
  return rejection === "tooLarge" ? labels.attachmentTooLarge : labels.attachmentTypeNotAccepted;
525
1362
  }
526
1363
  function useFileSelection(props) {
527
- const [rejection, setRejection] = useState(void 0);
1364
+ const [rejection, setRejection] = useState3(void 0);
528
1365
  const { files, onFilesChange, channel } = props;
529
1366
  const maxBytes = maxBytesForChannel(channel);
530
1367
  function handleFilesChosen(event) {
@@ -540,40 +1377,55 @@ function useFileSelection(props) {
540
1377
  }
541
1378
  return { rejection, handleFilesChosen, handleRemoveFile };
542
1379
  }
1380
+ function ComposerRow({ composer, canSend, onFilesChosen }) {
1381
+ const { value, onChange, files, labels, channel = "app", disabled = false } = composer;
1382
+ const attach = channelCapabilityFor(channel).attachments.accepted ? /* @__PURE__ */ jsx13(AttachControl, { labels, disabled, acceptedTypes: composer.acceptedTypes, onChosen: onFilesChosen }) : null;
1383
+ const field = /* @__PURE__ */ jsx13(
1384
+ MessageField,
1385
+ {
1386
+ value,
1387
+ label: labels.messageInputLabel,
1388
+ placeholder: labels.messageInputPlaceholder,
1389
+ disabled,
1390
+ maxLength: composer.maxLength,
1391
+ onChange
1392
+ }
1393
+ );
1394
+ const sendButton = /* @__PURE__ */ jsx13(SendButton, { label: labels.send, disabled: !canSend });
1395
+ const audio = resolveComposerAudio(composer);
1396
+ if (!audio) {
1397
+ return /* @__PURE__ */ jsxs11("div", { className: "cv-p-composer__row", children: [
1398
+ attach,
1399
+ field,
1400
+ sendButton
1401
+ ] });
1402
+ }
1403
+ const isDraftEmpty = value.trim().length === 0 && files.length === 0;
1404
+ return /* @__PURE__ */ jsxs11(ComposerAudioRow, { settings: audio, labels, disabled, isDraftEmpty, sendButton, children: [
1405
+ attach,
1406
+ field
1407
+ ] });
1408
+ }
543
1409
  function ParticipantComposer(props) {
544
- const { value, onChange, files, onSend, labels, channel = "app", disabled = false, isSending = false } = props;
1410
+ const { value, onChange, files, onSend, labels, disabled = false, isSending = false } = props;
545
1411
  const { rejection, handleFilesChosen, handleRemoveFile } = useFileSelection(props);
546
1412
  const canSend = !disabled && !isSending && (value.trim().length > 0 || files.length > 0);
547
1413
  function handleSubmit(event) {
548
1414
  event.preventDefault();
549
1415
  if (canSend) onSend();
550
1416
  }
551
- return /* @__PURE__ */ jsxs7("form", { className: "cv-p-composer", onSubmit: handleSubmit, children: [
552
- props.quickReplies && props.quickReplies.length > 0 ? /* @__PURE__ */ jsx8(QuickReplyStrip, { quickReplies: props.quickReplies, labels, onPick: (body) => onChange(applyQuickReply(value, body)) }) : null,
553
- /* @__PURE__ */ jsx8(FileList, { files, labels, onRemove: handleRemoveFile }),
554
- rejection ? /* @__PURE__ */ jsx8("p", { className: "cv-p-composer__error", role: "alert", children: rejectionText(rejection, labels) }) : null,
555
- /* @__PURE__ */ jsxs7("div", { className: "cv-p-composer__row", children: [
556
- channelCapabilityFor(channel).attachments.accepted ? /* @__PURE__ */ jsx8(AttachControl, { labels, disabled, acceptedTypes: props.acceptedTypes, onChosen: handleFilesChosen }) : null,
557
- /* @__PURE__ */ jsx8(
558
- MessageField,
559
- {
560
- value,
561
- label: labels.messageInputLabel,
562
- placeholder: labels.messageInputPlaceholder,
563
- disabled,
564
- maxLength: props.maxLength,
565
- onChange
566
- }
567
- ),
568
- /* @__PURE__ */ jsx8(SendButton, { label: labels.send, disabled: !canSend })
569
- ] })
1417
+ return /* @__PURE__ */ jsxs11("form", { className: "cv-p-composer", onSubmit: handleSubmit, children: [
1418
+ props.quickReplies && props.quickReplies.length > 0 ? /* @__PURE__ */ jsx13(QuickReplyStrip, { quickReplies: props.quickReplies, labels, onPick: (body) => onChange(applyQuickReply(value, body)) }) : null,
1419
+ /* @__PURE__ */ jsx13(FileList, { files, labels, onRemove: handleRemoveFile }),
1420
+ rejection ? /* @__PURE__ */ jsx13("p", { className: "cv-p-composer__error", role: "alert", children: rejectionText(rejection, labels) }) : null,
1421
+ /* @__PURE__ */ jsx13(ComposerRow, { composer: props, canSend, onFilesChosen: handleFilesChosen })
570
1422
  ] });
571
1423
  }
572
1424
 
573
1425
  // src/participant/useSuppressTouchTooltips.ts
574
- import { useEffect as useEffect2 } from "react";
1426
+ import { useEffect as useEffect6 } from "react";
575
1427
  function useSuppressTouchTooltips(rootRef) {
576
- useEffect2(() => {
1428
+ useEffect6(() => {
577
1429
  const root = rootRef.current;
578
1430
  if (!root) return void 0;
579
1431
  let isTouching = false;
@@ -603,7 +1455,7 @@ function useSuppressTouchTooltips(rootRef) {
603
1455
  }
604
1456
 
605
1457
  // src/participant/ParticipantCopyAnnouncer.tsx
606
- import { createContext, useCallback, useContext, useEffect as useEffect3, useReducer, useRef as useRef3 } from "react";
1458
+ import { createContext, useCallback as useCallback4, useContext, useEffect as useEffect7, useReducer, useRef as useRef8 } from "react";
607
1459
 
608
1460
  // src/participant/participantCopyAnnouncement.ts
609
1461
  var INITIAL_COPY_ANNOUNCEMENT = { text: "", tick: 0 };
@@ -623,7 +1475,7 @@ function resolveLiveText(params) {
623
1475
  }
624
1476
 
625
1477
  // src/participant/ParticipantCopyAnnouncer.tsx
626
- import { jsx as jsx9 } from "react/jsx-runtime";
1478
+ import { jsx as jsx14 } from "react/jsx-runtime";
627
1479
  var ANNOUNCEMENT_MS = 3e3;
628
1480
  var AnnounceContext = createContext(() => void 0);
629
1481
  var AnnouncementContext = createContext(INITIAL_COPY_ANNOUNCEMENT);
@@ -632,20 +1484,20 @@ function useAnnounceCopy() {
632
1484
  }
633
1485
  function ParticipantCopyAnnouncer({ children }) {
634
1486
  const [announcement, dispatch] = useReducer(copyAnnouncementReducer, INITIAL_COPY_ANNOUNCEMENT);
635
- const announce = useCallback((text) => dispatch({ type: "announce", text }), []);
636
- useEffect3(() => {
1487
+ const announce = useCallback4((text) => dispatch({ type: "announce", text }), []);
1488
+ useEffect7(() => {
637
1489
  if (announcement.text === "") return void 0;
638
1490
  const timer = setTimeout(() => dispatch({ type: "expired" }), ANNOUNCEMENT_MS);
639
1491
  return () => clearTimeout(timer);
640
1492
  }, [announcement.tick, announcement.text]);
641
- return /* @__PURE__ */ jsx9(AnnounceContext.Provider, { value: announce, children: /* @__PURE__ */ jsx9(AnnouncementContext.Provider, { value: announcement, children }) });
1493
+ return /* @__PURE__ */ jsx14(AnnounceContext.Provider, { value: announce, children: /* @__PURE__ */ jsx14(AnnouncementContext.Provider, { value: announcement, children }) });
642
1494
  }
643
1495
  function ParticipantThreadLive({ notice }) {
644
1496
  const copy = useContext(AnnouncementContext);
645
- const baselineNotice = useRef3(void 0);
1497
+ const baselineNotice = useRef8(void 0);
646
1498
  const resolution = resolveLiveText({ copy, notice, baselineNotice: baselineNotice.current });
647
1499
  baselineNotice.current = resolution.baselineNotice;
648
- return /* @__PURE__ */ jsx9("div", { className: "cv-p-thread__live", role: "status", "aria-live": "polite", children: resolution.text });
1500
+ return /* @__PURE__ */ jsx14("div", { className: "cv-p-thread__live", role: "status", "aria-live": "polite", children: resolution.text });
649
1501
  }
650
1502
 
651
1503
  // src/participant/ParticipantScrollToLatest.tsx
@@ -658,7 +1510,7 @@ function focusScrollerOf(button) {
658
1510
  }
659
1511
 
660
1512
  // src/participant/ParticipantScrollToLatest.tsx
661
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1513
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
662
1514
  function ParticipantScrollToLatest({
663
1515
  label,
664
1516
  isAwayFromBottom,
@@ -668,7 +1520,7 @@ function ParticipantScrollToLatest({
668
1520
  }) {
669
1521
  const { isVisible, badge } = resolveScrollToLatestView({ isAwayFromBottom, newMessagesCount, hasMessages });
670
1522
  if (!isVisible || onClick === void 0) return null;
671
- return /* @__PURE__ */ jsx10("div", { className: "cv-p-thread__latest", children: /* @__PURE__ */ jsxs8(
1523
+ return /* @__PURE__ */ jsx15("div", { className: "cv-p-thread__latest", children: /* @__PURE__ */ jsxs12(
672
1524
  "button",
673
1525
  {
674
1526
  type: "button",
@@ -680,8 +1532,8 @@ function ParticipantScrollToLatest({
680
1532
  onClick();
681
1533
  },
682
1534
  children: [
683
- /* @__PURE__ */ jsx10(ChevronDown, { size: 24, "aria-hidden": "true", focusable: "false" }),
684
- badge ? /* @__PURE__ */ jsx10("span", { className: "cv-p-thread__latest-badge", "aria-hidden": "true", children: badge }) : null
1535
+ /* @__PURE__ */ jsx15(ChevronDown, { size: 24, "aria-hidden": "true", focusable: "false" }),
1536
+ badge ? /* @__PURE__ */ jsx15("span", { className: "cv-p-thread__latest-badge", "aria-hidden": "true", children: badge }) : null
685
1537
  ]
686
1538
  }
687
1539
  ) });
@@ -727,13 +1579,13 @@ function shouldShowAvatar({ previousAuthor, author, isMine }) {
727
1579
  }
728
1580
 
729
1581
  // src/participant/ParticipantAuthorAvatar.tsx
730
- import { jsx as jsx11 } from "react/jsx-runtime";
1582
+ import { jsx as jsx16 } from "react/jsx-runtime";
731
1583
  function ParticipantAuthorAvatar({ name, render }) {
732
1584
  const fromHost = render?.({ name });
733
1585
  const hasHostContent = !(fromHost == null || fromHost === false || fromHost === "");
734
1586
  const initials = authorInitials(name);
735
- const fallback = initials ?? /* @__PURE__ */ jsx11(User, { size: 16, "aria-hidden": true });
736
- return /* @__PURE__ */ jsx11("span", { className: "cv-p-avatar", "aria-hidden": "true", children: hasHostContent ? fromHost : fallback });
1587
+ const fallback = initials ?? /* @__PURE__ */ jsx16(User, { size: 16, "aria-hidden": true });
1588
+ return /* @__PURE__ */ jsx16("span", { className: "cv-p-avatar", "aria-hidden": "true", children: hasHostContent ? fromHost : fallback });
737
1589
  }
738
1590
 
739
1591
  // src/participant/participantDay.ts
@@ -745,17 +1597,112 @@ function toDateTimeAttribute(date) {
745
1597
  }
746
1598
 
747
1599
  // src/participant/ParticipantDayDivider.tsx
748
- import { jsx as jsx12 } from "react/jsx-runtime";
1600
+ import { jsx as jsx17 } from "react/jsx-runtime";
749
1601
  function ParticipantDayDivider({ day, locale }) {
750
1602
  const label = day.toLocaleDateString(locale, { weekday: "short", day: "2-digit", month: "2-digit" });
751
- return /* @__PURE__ */ jsx12("div", { className: "cv-p-day", role: "separator", "aria-label": label, children: /* @__PURE__ */ jsx12("time", { className: "cv-p-day__label", dateTime: toDateTimeAttribute(day), children: label }) });
1603
+ return /* @__PURE__ */ jsx17("div", { className: "cv-p-day", role: "separator", "aria-label": label, children: /* @__PURE__ */ jsx17("time", { className: "cv-p-day__label", dateTime: toDateTimeAttribute(day), children: label }) });
752
1604
  }
753
1605
 
754
1606
  // src/participant/ParticipantMessageBubble.tsx
755
1607
  import { useId as useId3 } from "react";
756
1608
 
757
1609
  // src/participant/ParticipantAttachmentItem.tsx
758
- import { useEffect as useEffect4, useState as useState2 } from "react";
1610
+ import { useEffect as useEffect11, useState as useState7 } from "react";
1611
+
1612
+ // src/participant/ParticipantAudioTranscription.tsx
1613
+ import { useState as useState5 } from "react";
1614
+
1615
+ // src/participant/useTranscriptionOverflow.ts
1616
+ import { useEffect as useEffect8, useRef as useRef9, useState as useState4 } from "react";
1617
+ function useTranscriptionOverflow(text, isExpanded) {
1618
+ const textRef = useRef9(null);
1619
+ const [isOverflowing, setIsOverflowing] = useState4(false);
1620
+ useEffect8(() => {
1621
+ const element = textRef.current;
1622
+ if (!element || isExpanded) return void 0;
1623
+ const measure = () => setIsOverflowing(element.scrollHeight > element.clientHeight);
1624
+ measure();
1625
+ if (typeof ResizeObserver === "undefined") return void 0;
1626
+ const observer = new ResizeObserver(measure);
1627
+ observer.observe(element);
1628
+ return () => observer.disconnect();
1629
+ }, [text, isExpanded]);
1630
+ return { textRef, isOverflowing };
1631
+ }
1632
+
1633
+ // src/participant/useTranscriptionReadyAnnouncement.ts
1634
+ import { useEffect as useEffect9, useRef as useRef10 } from "react";
1635
+ function useTranscriptionReadyAnnouncement({ attachmentId, status, text }) {
1636
+ const announce = useAnnounceCopy();
1637
+ const previous = useRef10({ attachmentId, status });
1638
+ useEffect9(() => {
1639
+ const before = previous.current;
1640
+ previous.current = { attachmentId, status };
1641
+ if (before.attachmentId !== attachmentId) return;
1642
+ if (before.status === "pending" && status === "ready") announce(text);
1643
+ }, [announce, attachmentId, status, text]);
1644
+ }
1645
+
1646
+ // src/participant/ParticipantAudioTranscription.tsx
1647
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1648
+ var MILLISECONDS_PER_MINUTE = 6e4;
1649
+ function resolveTooLongMinutes(transcription) {
1650
+ if (transcription?.status !== "failed" || transcription.reason !== "too_long") return void 0;
1651
+ if (typeof transcription.maxDurationMs !== "number") return void 0;
1652
+ const minutes = Math.floor(transcription.maxDurationMs / MILLISECONDS_PER_MINUTE);
1653
+ return minutes >= 1 ? minutes : void 0;
1654
+ }
1655
+ function dropUnusableTranscription(transcription) {
1656
+ if (transcription?.status === "ready" && typeof transcription.text !== "string") return void 0;
1657
+ return transcription;
1658
+ }
1659
+ function ParticipantAudioTranscription({ attachmentId, transcription: received, labels }) {
1660
+ const transcription = dropUnusableTranscription(received);
1661
+ useTranscriptionReadyAnnouncement({
1662
+ attachmentId,
1663
+ status: transcription?.status,
1664
+ text: labels.transcriptionReady
1665
+ });
1666
+ if (transcription?.status === "pending") {
1667
+ return /* @__PURE__ */ jsx18("p", { className: "cv-p-transcription cv-p-transcription--pending", children: labels.transcribing });
1668
+ }
1669
+ if (transcription?.status === "ready") return /* @__PURE__ */ jsx18(ReadyTranscriptionBlock, { transcription, labels });
1670
+ const minutes = resolveTooLongMinutes(transcription);
1671
+ if (minutes === void 0) return null;
1672
+ return /* @__PURE__ */ jsx18("p", { className: "cv-p-transcription cv-p-transcription--too-long", children: labels.transcriptionTooLong.replace("{minutes}", String(minutes)) });
1673
+ }
1674
+ function ReadyTranscriptionBlock({ transcription, labels }) {
1675
+ const [isExpanded, setIsExpanded] = useState5(false);
1676
+ const { textRef, isOverflowing } = useTranscriptionOverflow(transcription.text, isExpanded);
1677
+ const hasText = transcription.text !== "";
1678
+ const showsToggle = hasText && (isOverflowing || isExpanded);
1679
+ return /* @__PURE__ */ jsxs13("div", { className: "cv-p-transcription", children: [
1680
+ /* @__PURE__ */ jsx18("p", { className: "cv-p-transcription__notice", children: labels.machineTranscription }),
1681
+ hasText ? /* @__PURE__ */ jsx18(
1682
+ "p",
1683
+ {
1684
+ ref: textRef,
1685
+ className: "cv-p-transcription__text",
1686
+ lang: transcription.language,
1687
+ "data-expanded": isExpanded ? "true" : void 0,
1688
+ children: transcription.text
1689
+ }
1690
+ ) : /* @__PURE__ */ jsx18("p", { className: "cv-p-transcription__empty", children: labels.emptyTranscription }),
1691
+ showsToggle ? /* @__PURE__ */ jsx18(
1692
+ "button",
1693
+ {
1694
+ type: "button",
1695
+ className: "cv-p-transcription__toggle",
1696
+ "aria-expanded": isExpanded,
1697
+ onClick: () => setIsExpanded((current) => !current),
1698
+ children: isExpanded ? labels.collapseTranscription : labels.showFullTranscription
1699
+ }
1700
+ ) : null
1701
+ ] });
1702
+ }
1703
+
1704
+ // src/participant/useStableAttachmentUrl.ts
1705
+ import { useCallback as useCallback5, useEffect as useEffect10, useRef as useRef11, useState as useState6 } from "react";
759
1706
 
760
1707
  // src/participant/participantAttachmentUrl.ts
761
1708
  var SAFE_ATTACHMENT_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:", "blob:"]);
@@ -767,12 +1714,79 @@ function isSafeAttachmentUrl(url) {
767
1714
  }
768
1715
  }
769
1716
 
1717
+ // src/participant/useStableAttachmentUrl.ts
1718
+ var ATTACHMENT_URL_RETRY_DELAY_MS = 1e3;
1719
+ var PENDING_STATE = { status: "pending", url: void 0 };
1720
+ async function resolveSafely({ attachment, resolveAttachmentUrl }) {
1721
+ try {
1722
+ const resolved = await resolveAttachmentUrl(attachment, "inline");
1723
+ return isSafeAttachmentUrl(resolved) ? resolved.trim() : void 0;
1724
+ } catch {
1725
+ return void 0;
1726
+ }
1727
+ }
1728
+ function useStableAttachmentUrl(params) {
1729
+ const latest = useRef11(params);
1730
+ const currentId = useRef11(params.attachment.id);
1731
+ const [state, setState2] = useState6(PENDING_STATE);
1732
+ const { id } = params.attachment;
1733
+ useEffect10(() => {
1734
+ latest.current = params;
1735
+ });
1736
+ useEffect10(() => {
1737
+ currentId.current = id;
1738
+ setState2(PENDING_STATE);
1739
+ let retryTimer;
1740
+ const settle = (url) => {
1741
+ if (currentId.current === id) setState2(url ? { status: "ready", url } : { status: "failed", url: void 0 });
1742
+ };
1743
+ resolveSafely(latest.current).then((url) => {
1744
+ if (url || currentId.current !== id) return settle(url);
1745
+ retryTimer = setTimeout(() => void resolveSafely(latest.current).then(settle), ATTACHMENT_URL_RETRY_DELAY_MS);
1746
+ });
1747
+ return () => {
1748
+ currentId.current = "";
1749
+ clearTimeout(retryTimer);
1750
+ };
1751
+ }, [id]);
1752
+ const resolveAgain = useCallback5(async () => {
1753
+ const requestedId = currentId.current;
1754
+ const url = await resolveSafely(latest.current);
1755
+ if (url && currentId.current === requestedId) setState2({ status: "ready", url });
1756
+ return url;
1757
+ }, []);
1758
+ return { ...state, resolveAgain };
1759
+ }
1760
+
1761
+ // src/participant/ParticipantAudioAttachment.tsx
1762
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
1763
+ function ParticipantAudioAttachment({ attachment, resolveAttachmentUrl, labels }) {
1764
+ const source = useStableAttachmentUrl({ attachment, resolveAttachmentUrl });
1765
+ const audioLabels = resolveParticipantAudioLabels(labels);
1766
+ return /* @__PURE__ */ jsxs14("div", { className: "cv-p-attachment cv-p-attachment--audio", children: [
1767
+ /* @__PURE__ */ jsx19(
1768
+ ParticipantAudioPlayer,
1769
+ {
1770
+ labels: audioLabels,
1771
+ url: source.url,
1772
+ durationMs: attachment.durationMs,
1773
+ filename: attachment.filename,
1774
+ isPending: source.status === "pending",
1775
+ isUnavailable: source.status === "failed",
1776
+ onSourceError: source.resolveAgain
1777
+ }
1778
+ ),
1779
+ /* @__PURE__ */ jsx19("span", { className: "cv-p-attachment__name", children: attachment.filename }),
1780
+ /* @__PURE__ */ jsx19(ParticipantAudioTranscription, { attachmentId: attachment.id, transcription: attachment.transcription, labels: audioLabels })
1781
+ ] });
1782
+ }
1783
+
770
1784
  // src/participant/ParticipantAttachmentItem.tsx
771
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1785
+ import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
772
1786
  function useAttachmentUrl(attachment, resolveAttachmentUrl) {
773
- const [url, setUrl] = useState2(void 0);
1787
+ const [url, setUrl] = useState7(void 0);
774
1788
  const disposition = attachment.kind === "document" ? "attachment" : "inline";
775
- useEffect4(() => {
1789
+ useEffect11(() => {
776
1790
  let isCurrent = true;
777
1791
  resolveAttachmentUrl(attachment, disposition).then(
778
1792
  (resolved) => {
@@ -788,21 +1802,19 @@ function useAttachmentUrl(attachment, resolveAttachmentUrl) {
788
1802
  }, [attachment, disposition, resolveAttachmentUrl]);
789
1803
  return url;
790
1804
  }
791
- function ParticipantAttachmentItem({ attachment, resolveAttachmentUrl, labels }) {
1805
+ function ParticipantAttachmentItem(props) {
1806
+ if (props.attachment.kind === "audio") return /* @__PURE__ */ jsx20(ParticipantAudioAttachment, { ...props });
1807
+ return /* @__PURE__ */ jsx20(ParticipantFileAttachment, { ...props });
1808
+ }
1809
+ function ParticipantFileAttachment({ attachment, resolveAttachmentUrl, labels }) {
792
1810
  const url = useAttachmentUrl(attachment, resolveAttachmentUrl);
793
1811
  if (attachment.kind === "image") {
794
- return /* @__PURE__ */ jsxs9("figure", { className: "cv-p-attachment cv-p-attachment--image", children: [
795
- url ? /* @__PURE__ */ jsx13("img", { className: "cv-p-attachment__image", src: url, alt: attachment.filename }) : null,
796
- /* @__PURE__ */ jsx13("figcaption", { className: "cv-p-attachment__name", children: attachment.filename })
1812
+ return /* @__PURE__ */ jsxs15("figure", { className: "cv-p-attachment cv-p-attachment--image", children: [
1813
+ url ? /* @__PURE__ */ jsx20("img", { className: "cv-p-attachment__image", src: url, alt: attachment.filename }) : null,
1814
+ /* @__PURE__ */ jsx20("figcaption", { className: "cv-p-attachment__name", children: attachment.filename })
797
1815
  ] });
798
1816
  }
799
- if (attachment.kind === "audio") {
800
- return /* @__PURE__ */ jsxs9("div", { className: "cv-p-attachment cv-p-attachment--audio", children: [
801
- /* @__PURE__ */ jsx13("span", { className: "cv-p-attachment__name", children: attachment.filename }),
802
- /* @__PURE__ */ jsx13("audio", { className: "cv-p-attachment__audio", controls: true, preload: "none", src: url })
803
- ] });
804
- }
805
- return /* @__PURE__ */ jsxs9(
1817
+ return /* @__PURE__ */ jsxs15(
806
1818
  "a",
807
1819
  {
808
1820
  className: "cv-p-attachment cv-p-attachment--document",
@@ -810,15 +1822,15 @@ function ParticipantAttachmentItem({ attachment, resolveAttachmentUrl, labels })
810
1822
  download: attachment.filename,
811
1823
  "aria-label": `${labels.downloadAttachment}: ${attachment.filename}`,
812
1824
  children: [
813
- /* @__PURE__ */ jsx13("span", { className: "cv-p-attachment__name", children: attachment.filename }),
814
- /* @__PURE__ */ jsx13("span", { className: "cv-p-attachment__size", children: formatFileSize(attachment.sizeBytes) })
1825
+ /* @__PURE__ */ jsx20("span", { className: "cv-p-attachment__name", children: attachment.filename }),
1826
+ /* @__PURE__ */ jsx20("span", { className: "cv-p-attachment__size", children: formatFileSize(attachment.sizeBytes) })
815
1827
  ]
816
1828
  }
817
1829
  );
818
1830
  }
819
1831
 
820
1832
  // src/participant/ParticipantBubbleStatus.tsx
821
- import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1833
+ import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
822
1834
  function statusText(status, labels) {
823
1835
  const byStatus = {
824
1836
  sending: labels.statusSending,
@@ -835,13 +1847,13 @@ function OwnStatus({ status, labels, onRetry }) {
835
1847
  const isVisibleText = status === "sending" || status === "queued" || status === "failed";
836
1848
  const text = status === "failed" ? labels.statusFailedShort : statusText(status, labels);
837
1849
  const isTextVisible = isVisibleText && !(status === "failed" && onRetry);
838
- return /* @__PURE__ */ jsxs10("span", { className: "cv-p-bubble__status", children: [
839
- /* @__PURE__ */ jsx14(StatusTicks, { status: ticksStatus, appearance: "stylesheet", title: isTextVisible ? void 0 : text }),
840
- /* @__PURE__ */ jsx14("span", { className: isTextVisible ? "cv-p-bubble__status-text" : "cv-p-sr-only", children: text })
1850
+ return /* @__PURE__ */ jsxs16("span", { className: "cv-p-bubble__status", children: [
1851
+ /* @__PURE__ */ jsx21(StatusTicks, { status: ticksStatus, appearance: "stylesheet", title: isTextVisible ? void 0 : text }),
1852
+ /* @__PURE__ */ jsx21("span", { className: isTextVisible ? "cv-p-bubble__status-text" : "cv-p-sr-only", children: text })
841
1853
  ] });
842
1854
  }
843
1855
  function RetryButton({ labels, onRetry, describedBy }) {
844
- return /* @__PURE__ */ jsx14(
1856
+ return /* @__PURE__ */ jsx21(
845
1857
  "button",
846
1858
  {
847
1859
  type: "button",
@@ -850,7 +1862,7 @@ function RetryButton({ labels, onRetry, describedBy }) {
850
1862
  "aria-describedby": describedBy,
851
1863
  "data-cv-tooltip": labels.statusFailed,
852
1864
  onClick: onRetry,
853
- children: /* @__PURE__ */ jsx14("span", { className: "cv-p-failed-retry__icon", children: /* @__PURE__ */ jsx14("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx14("path", { d: "M13 8a5 5 0 1 1-1.6-3.7M13 2.5v3h-3", fill: "none", stroke: "currentColor", strokeWidth: "1.5" }) }) })
1865
+ children: /* @__PURE__ */ jsx21("span", { className: "cv-p-failed-retry__icon", children: /* @__PURE__ */ jsx21("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx21("path", { d: "M13 8a5 5 0 1 1-1.6-3.7M13 2.5v3h-3", fill: "none", stroke: "currentColor", strokeWidth: "1.5" }) }) })
854
1866
  }
855
1867
  );
856
1868
  }
@@ -859,7 +1871,7 @@ function RetryButton({ labels, onRetry, describedBy }) {
859
1871
  import { useId as useId2 } from "react";
860
1872
 
861
1873
  // src/participant/ParticipantFailedMenuList.tsx
862
- import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
1874
+ import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
863
1875
  function ParticipantFailedMenuList({ menu, labelledBy, labels, onEdit, onDiscard }) {
864
1876
  function runAndClose(action) {
865
1877
  return () => {
@@ -868,9 +1880,9 @@ function ParticipantFailedMenuList({ menu, labelledBy, labels, onEdit, onDiscard
868
1880
  };
869
1881
  }
870
1882
  const className = `cv-p-failed-menu__list${menu.placement === "below" ? " cv-p-failed-menu__list--below" : ""}`;
871
- return /* @__PURE__ */ jsxs11("div", { role: "menu", "aria-labelledby": labelledBy, ref: menu.menuRef, className, onKeyDown: menu.handleMenuKeyDown, children: [
872
- onEdit ? /* @__PURE__ */ jsx15("button", { type: "button", role: "menuitem", tabIndex: -1, className: "cv-p-failed-menu__item", onClick: runAndClose(onEdit), children: labels.edit }) : null,
873
- onDiscard ? /* @__PURE__ */ jsx15(
1883
+ return /* @__PURE__ */ jsxs17("div", { role: "menu", "aria-labelledby": labelledBy, ref: menu.menuRef, className, onKeyDown: menu.handleMenuKeyDown, children: [
1884
+ onEdit ? /* @__PURE__ */ jsx22("button", { type: "button", role: "menuitem", tabIndex: -1, className: "cv-p-failed-menu__item", onClick: runAndClose(onEdit), children: labels.edit }) : null,
1885
+ onDiscard ? /* @__PURE__ */ jsx22(
874
1886
  "button",
875
1887
  {
876
1888
  type: "button",
@@ -885,7 +1897,7 @@ function ParticipantFailedMenuList({ menu, labelledBy, labels, onEdit, onDiscard
885
1897
  }
886
1898
 
887
1899
  // src/participant/useParticipantFailedMenu.ts
888
- import { useReducer as useReducer2, useRef as useRef4 } from "react";
1900
+ import { useReducer as useReducer2, useRef as useRef12 } from "react";
889
1901
 
890
1902
  // src/participant/participantFailedMenuState.ts
891
1903
  var CLOSED_FAILED_MENU = { isOpen: false, initialFocus: "first" };
@@ -933,10 +1945,10 @@ function handleFailedMenuKeyDown({ event, menuRef, triggerRef, dispatch }) {
933
1945
  }
934
1946
 
935
1947
  // src/participant/useFailedMenuEffects.ts
936
- import { useEffect as useEffect5, useLayoutEffect as useLayoutEffect2, useState as useState3 } from "react";
1948
+ import { useEffect as useEffect12, useLayoutEffect as useLayoutEffect2, useState as useState8 } from "react";
937
1949
  var SCROLLER_SELECTOR2 = ".cv-p-thread__scroll";
938
1950
  function useFailedMenuInitialFocus(state, menuRef) {
939
- useEffect5(() => {
1951
+ useEffect12(() => {
940
1952
  if (!state.isOpen) return;
941
1953
  const items = Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []);
942
1954
  const target = state.initialFocus === "last" ? items[items.length - 1] : items[0];
@@ -944,7 +1956,7 @@ function useFailedMenuInitialFocus(state, menuRef) {
944
1956
  }, [state, menuRef]);
945
1957
  }
946
1958
  function useFailedMenuOutsideClose(isOpen, containerRef, dispatch) {
947
- useEffect5(() => {
1959
+ useEffect12(() => {
948
1960
  if (!isOpen) return;
949
1961
  function handlePointerDown(event) {
950
1962
  if (event.target instanceof Node && containerRef.current?.contains(event.target)) return;
@@ -955,7 +1967,7 @@ function useFailedMenuOutsideClose(isOpen, containerRef, dispatch) {
955
1967
  }, [isOpen, containerRef, dispatch]);
956
1968
  }
957
1969
  function useFailedMenuPlacement(isOpen, { triggerRef, menuRef }) {
958
- const [placement, setPlacement] = useState3("above");
1970
+ const [placement, setPlacement] = useState8("above");
959
1971
  useLayoutEffect2(() => {
960
1972
  const trigger = triggerRef.current;
961
1973
  const menu = menuRef.current;
@@ -976,9 +1988,9 @@ function useFailedMenuPlacement(isOpen, { triggerRef, menuRef }) {
976
1988
  // src/participant/useParticipantFailedMenu.ts
977
1989
  function useParticipantFailedMenu() {
978
1990
  const [state, dispatch] = useReducer2(failedMenuReducer, CLOSED_FAILED_MENU);
979
- const containerRef = useRef4(null);
980
- const triggerRef = useRef4(null);
981
- const menuRef = useRef4(null);
1991
+ const containerRef = useRef12(null);
1992
+ const triggerRef = useRef12(null);
1993
+ const menuRef = useRef12(null);
982
1994
  useFailedMenuInitialFocus(state, menuRef);
983
1995
  useFailedMenuOutsideClose(state.isOpen, containerRef, dispatch);
984
1996
  const placement = useFailedMenuPlacement(state.isOpen, { triggerRef, menuRef });
@@ -1007,14 +2019,14 @@ function useParticipantFailedMenu() {
1007
2019
  }
1008
2020
 
1009
2021
  // src/participant/ParticipantFailedMenu.tsx
1010
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
2022
+ import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
1011
2023
  function ParticipantFailedMenu({ labels, onEdit, onDiscard }) {
1012
2024
  const menu = useParticipantFailedMenu();
1013
2025
  const triggerId = useId2();
1014
2026
  if (!onEdit && !onDiscard) return null;
1015
2027
  const label = labels.messageOptions ?? DEFAULT_MESSAGE_OPTIONS_LABEL;
1016
- return /* @__PURE__ */ jsxs12("span", { className: "cv-p-failed-menu", ref: menu.containerRef, onBlur: menu.handleBlur, children: [
1017
- /* @__PURE__ */ jsx16(
2028
+ return /* @__PURE__ */ jsxs18("span", { className: "cv-p-failed-menu", ref: menu.containerRef, onBlur: menu.handleBlur, children: [
2029
+ /* @__PURE__ */ jsx23(
1018
2030
  "button",
1019
2031
  {
1020
2032
  type: "button",
@@ -1027,21 +2039,21 @@ function ParticipantFailedMenu({ labels, onEdit, onDiscard }) {
1027
2039
  "data-cv-tooltip": menu.state.isOpen ? void 0 : label,
1028
2040
  onClick: menu.handleTriggerClick,
1029
2041
  onKeyDown: menu.handleTriggerKeyDown,
1030
- children: /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", children: "\u22EF" })
2042
+ children: /* @__PURE__ */ jsx23("span", { "aria-hidden": "true", children: "\u22EF" })
1031
2043
  }
1032
2044
  ),
1033
- menu.state.isOpen ? /* @__PURE__ */ jsx16(ParticipantFailedMenuList, { menu, labelledBy: triggerId, labels, onEdit, onDiscard }) : null
2045
+ menu.state.isOpen ? /* @__PURE__ */ jsx23(ParticipantFailedMenuList, { menu, labelledBy: triggerId, labels, onEdit, onDiscard }) : null
1034
2046
  ] });
1035
2047
  }
1036
2048
 
1037
2049
  // src/participant/ParticipantMessageText.tsx
1038
- import { useMemo } from "react";
2050
+ import { useMemo as useMemo2 } from "react";
1039
2051
 
1040
2052
  // src/participant/ParticipantCopyableToken.tsx
1041
2053
  import { Check, Copy } from "lucide-react";
1042
2054
 
1043
2055
  // src/participant/useParticipantCopyFeedback.ts
1044
- import { useEffect as useEffect6, useReducer as useReducer3 } from "react";
2056
+ import { useEffect as useEffect13, useReducer as useReducer3 } from "react";
1045
2057
 
1046
2058
  // src/participant/participantProtocolCopy.ts
1047
2059
  async function copyProtocolToClipboard(protocol, clipboard) {
@@ -1065,7 +2077,7 @@ function useParticipantCopyFeedback() {
1065
2077
  const [state, dispatch] = useReducer3(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
1066
2078
  const announce = useAnnounceCopy();
1067
2079
  const { isCopied, copyCount } = state;
1068
- useEffect6(() => {
2080
+ useEffect13(() => {
1069
2081
  if (!isCopied) return void 0;
1070
2082
  const timer = setTimeout(() => dispatch({ type: "expired" }), COPIED_FEEDBACK_MS);
1071
2083
  return () => clearTimeout(timer);
@@ -1080,10 +2092,10 @@ function useParticipantCopyFeedback() {
1080
2092
  }
1081
2093
 
1082
2094
  // src/participant/ParticipantCopyableToken.tsx
1083
- import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
2095
+ import { jsx as jsx24, jsxs as jsxs19 } from "react/jsx-runtime";
1084
2096
  function ParticipantCopyableToken({ copyKind, value, labels }) {
1085
2097
  const { isCopied, copy } = useParticipantCopyFeedback();
1086
- return /* @__PURE__ */ jsxs13(
2098
+ return /* @__PURE__ */ jsxs19(
1087
2099
  "button",
1088
2100
  {
1089
2101
  type: "button",
@@ -1092,8 +2104,8 @@ function ParticipantCopyableToken({ copyKind, value, labels }) {
1092
2104
  onClick: () => copy(value, labels.valueCopied),
1093
2105
  children: [
1094
2106
  value,
1095
- isCopied ? /* @__PURE__ */ jsxs13("span", { className: "cv-p-copyable__done", "aria-hidden": "true", children: [
1096
- /* @__PURE__ */ jsx17(Check, { size: 12, "aria-hidden": true }),
2107
+ isCopied ? /* @__PURE__ */ jsxs19("span", { className: "cv-p-copyable__done", "aria-hidden": "true", children: [
2108
+ /* @__PURE__ */ jsx24(Check, { size: 12, "aria-hidden": true }),
1097
2109
  " ",
1098
2110
  labels.valueCopied
1099
2111
  ] }) : null
@@ -1103,7 +2115,7 @@ function ParticipantCopyableToken({ copyKind, value, labels }) {
1103
2115
  }
1104
2116
  function ParticipantLinkCopyButton({ value, labels }) {
1105
2117
  const { isCopied, copy } = useParticipantCopyFeedback();
1106
- return /* @__PURE__ */ jsx17(
2118
+ return /* @__PURE__ */ jsx24(
1107
2119
  "button",
1108
2120
  {
1109
2121
  type: "button",
@@ -1111,26 +2123,26 @@ function ParticipantLinkCopyButton({ value, labels }) {
1111
2123
  "aria-label": `${labels.copyValue} ${value}`,
1112
2124
  "data-cv-tooltip": labels.copyValue,
1113
2125
  onClick: () => copy(value, labels.valueCopied),
1114
- children: isCopied ? /* @__PURE__ */ jsx17(Check, { size: 14, "aria-hidden": true }) : /* @__PURE__ */ jsx17(Copy, { size: 14, "aria-hidden": true })
2126
+ children: isCopied ? /* @__PURE__ */ jsx24(Check, { size: 14, "aria-hidden": true }) : /* @__PURE__ */ jsx24(Copy, { size: 14, "aria-hidden": true })
1115
2127
  }
1116
2128
  );
1117
2129
  }
1118
2130
 
1119
2131
  // src/participant/ParticipantMessageNodes.tsx
1120
- import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
2132
+ import { jsx as jsx25, jsxs as jsxs20 } from "react/jsx-runtime";
1121
2133
  var LINK_REL = "noopener noreferrer nofollow ugc";
1122
2134
  function renderLink(node, labels, key) {
1123
2135
  const isExternal = /^https?:/i.test(node.href);
1124
- return /* @__PURE__ */ jsxs14("span", { className: "cv-p-text__link-group", children: [
1125
- /* @__PURE__ */ jsxs14("a", { className: "cv-p-text__link", href: node.href, rel: LINK_REL, target: isExternal ? "_blank" : void 0, children: [
2136
+ return /* @__PURE__ */ jsxs20("span", { className: "cv-p-text__link-group", children: [
2137
+ /* @__PURE__ */ jsxs20("a", { className: "cv-p-text__link", href: node.href, rel: LINK_REL, target: isExternal ? "_blank" : void 0, children: [
1126
2138
  node.label,
1127
- isExternal ? /* @__PURE__ */ jsxs14("span", { className: "cv-p-sr-only", children: [
2139
+ isExternal ? /* @__PURE__ */ jsxs20("span", { className: "cv-p-sr-only", children: [
1128
2140
  " (",
1129
2141
  labels.externalLinkHint,
1130
2142
  ")"
1131
2143
  ] }) : null
1132
2144
  ] }),
1133
- /* @__PURE__ */ jsx18(ParticipantLinkCopyButton, { value: node.href, labels })
2145
+ /* @__PURE__ */ jsx25(ParticipantLinkCopyButton, { value: node.href, labels })
1134
2146
  ] }, key);
1135
2147
  }
1136
2148
  function renderInlineNodes(nodes, labels) {
@@ -1139,17 +2151,17 @@ function renderInlineNodes(nodes, labels) {
1139
2151
  case "text":
1140
2152
  return node.value;
1141
2153
  case "strong":
1142
- return /* @__PURE__ */ jsx18("strong", { children: renderInlineNodes(node.children, labels) }, key);
2154
+ return /* @__PURE__ */ jsx25("strong", { children: renderInlineNodes(node.children, labels) }, key);
1143
2155
  case "em":
1144
- return /* @__PURE__ */ jsx18("em", { children: renderInlineNodes(node.children, labels) }, key);
2156
+ return /* @__PURE__ */ jsx25("em", { children: renderInlineNodes(node.children, labels) }, key);
1145
2157
  case "del":
1146
- return /* @__PURE__ */ jsx18("del", { children: renderInlineNodes(node.children, labels) }, key);
2158
+ return /* @__PURE__ */ jsx25("del", { children: renderInlineNodes(node.children, labels) }, key);
1147
2159
  case "code":
1148
- return /* @__PURE__ */ jsx18("code", { className: "cv-p-text__code", children: node.value }, key);
2160
+ return /* @__PURE__ */ jsx25("code", { className: "cv-p-text__code", children: node.value }, key);
1149
2161
  case "link":
1150
2162
  return renderLink(node, labels, key);
1151
2163
  case "copyable":
1152
- return /* @__PURE__ */ jsx18(ParticipantCopyableToken, { copyKind: node.copyKind, value: node.value, labels }, key);
2164
+ return /* @__PURE__ */ jsx25(ParticipantCopyableToken, { copyKind: node.copyKind, value: node.value, labels }, key);
1153
2165
  }
1154
2166
  });
1155
2167
  }
@@ -1535,41 +2547,41 @@ function startsFenceLine(line) {
1535
2547
  }
1536
2548
 
1537
2549
  // src/participant/ParticipantMessageText.tsx
1538
- import { jsx as jsx19 } from "react/jsx-runtime";
2550
+ import { jsx as jsx26 } from "react/jsx-runtime";
1539
2551
  function renderBlock(block, labels, key) {
1540
- if (block.kind === "paragraph") return /* @__PURE__ */ jsx19("span", { children: renderInlineNodes(block.children, labels) }, key);
2552
+ if (block.kind === "paragraph") return /* @__PURE__ */ jsx26("span", { children: renderInlineNodes(block.children, labels) }, key);
1541
2553
  if (block.kind === "pre") {
1542
- return /* @__PURE__ */ jsx19("pre", { className: "cv-p-text__pre", children: /* @__PURE__ */ jsx19("code", { children: block.value }) }, key);
2554
+ return /* @__PURE__ */ jsx26("pre", { className: "cv-p-text__pre", children: /* @__PURE__ */ jsx26("code", { children: block.value }) }, key);
1543
2555
  }
1544
2556
  if (block.kind === "quote") {
1545
- return /* @__PURE__ */ jsx19("blockquote", { className: "cv-p-text__quote", children: renderInlineNodes(block.children, labels) }, key);
2557
+ return /* @__PURE__ */ jsx26("blockquote", { className: "cv-p-text__quote", children: renderInlineNodes(block.children, labels) }, key);
1546
2558
  }
1547
- const items = block.items.map((item, index) => /* @__PURE__ */ jsx19("li", { children: renderInlineNodes(item, labels) }, index));
2559
+ const items = block.items.map((item, index) => /* @__PURE__ */ jsx26("li", { children: renderInlineNodes(item, labels) }, index));
1548
2560
  if (block.ordered) {
1549
- return /* @__PURE__ */ jsx19("ol", { className: "cv-p-text__list", start: block.start === 1 ? void 0 : block.start, children: items }, key);
2561
+ return /* @__PURE__ */ jsx26("ol", { className: "cv-p-text__list", start: block.start === 1 ? void 0 : block.start, children: items }, key);
1550
2562
  }
1551
- return /* @__PURE__ */ jsx19("ul", { className: "cv-p-text__list", children: items }, key);
2563
+ return /* @__PURE__ */ jsx26("ul", { className: "cv-p-text__list", children: items }, key);
1552
2564
  }
1553
2565
  function ParticipantMessageText({ text, labels, id }) {
1554
- const blocks = useMemo(() => parseParticipantMessage(text), [text]);
2566
+ const blocks = useMemo2(() => parseParticipantMessage(text), [text]);
1555
2567
  if (blocks.length === 0) return null;
1556
2568
  const [only] = blocks;
1557
2569
  const children = blocks.length === 1 && only?.kind === "paragraph" ? renderInlineNodes(only.children, labels) : blocks.map((block, key) => renderBlock(block, labels, key));
1558
- return /* @__PURE__ */ jsx19("div", { className: "cv-p-text", id, children });
2570
+ return /* @__PURE__ */ jsx26("div", { className: "cv-p-text", id, children });
1559
2571
  }
1560
2572
 
1561
2573
  // src/participant/ParticipantBubbleBody.tsx
1562
- import { Fragment as Fragment4, jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
2574
+ import { Fragment as Fragment6, jsx as jsx27, jsxs as jsxs21 } from "react/jsx-runtime";
1563
2575
  function BubbleHeading({ content, labels }) {
1564
- if (content.isMine) return /* @__PURE__ */ jsx20("span", { className: "cv-p-sr-only", children: labels.me });
2576
+ if (content.isMine) return /* @__PURE__ */ jsx27("span", { className: "cv-p-sr-only", children: labels.me });
1565
2577
  if (!content.authorName) return null;
1566
- return /* @__PURE__ */ jsx20("span", { className: "cv-p-bubble__author", children: content.authorName });
2578
+ return /* @__PURE__ */ jsx27("span", { className: "cv-p-bubble__author", children: content.authorName });
1567
2579
  }
1568
2580
  function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl, onRetry, onDiscard, onEdit }) {
1569
- return /* @__PURE__ */ jsxs15(Fragment4, { children: [
1570
- /* @__PURE__ */ jsx20(BubbleHeading, { content, labels }),
1571
- content.text ? /* @__PURE__ */ jsx20(ParticipantMessageText, { text: content.text, labels, id: textId }) : null,
1572
- content.attachments.map((attachment) => /* @__PURE__ */ jsx20(
2581
+ return /* @__PURE__ */ jsxs21(Fragment6, { children: [
2582
+ /* @__PURE__ */ jsx27(BubbleHeading, { content, labels }),
2583
+ content.text ? /* @__PURE__ */ jsx27(ParticipantMessageText, { text: content.text, labels, id: textId }) : null,
2584
+ content.attachments.map((attachment) => /* @__PURE__ */ jsx27(
1573
2585
  ParticipantAttachmentItem,
1574
2586
  {
1575
2587
  attachment,
@@ -1578,11 +2590,11 @@ function BubbleBody({ content, labels, ownStatus, textId, resolveAttachmentUrl,
1578
2590
  },
1579
2591
  attachment.id
1580
2592
  )),
1581
- content.pendingFilenames.map((filename, index) => /* @__PURE__ */ jsx20("span", { className: "cv-p-attachment cv-p-attachment--pending", children: filename }, `${filename}-${index}`)),
1582
- /* @__PURE__ */ jsxs15("span", { className: "cv-p-bubble__meta", children: [
1583
- /* @__PURE__ */ jsx20("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
1584
- ownStatus ? /* @__PURE__ */ jsx20(OwnStatus, { status: ownStatus, labels, onRetry }) : null,
1585
- ownStatus === "failed" ? /* @__PURE__ */ jsx20(ParticipantFailedMenu, { labels, onEdit, onDiscard }) : null
2593
+ content.pendingFilenames.map((filename, index) => /* @__PURE__ */ jsx27("span", { className: "cv-p-attachment cv-p-attachment--pending", children: filename }, `${filename}-${index}`)),
2594
+ /* @__PURE__ */ jsxs21("span", { className: "cv-p-bubble__meta", children: [
2595
+ /* @__PURE__ */ jsx27("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
2596
+ ownStatus ? /* @__PURE__ */ jsx27(OwnStatus, { status: ownStatus, labels, onRetry }) : null,
2597
+ ownStatus === "failed" ? /* @__PURE__ */ jsx27(ParticipantFailedMenu, { labels, onEdit, onDiscard }) : null
1586
2598
  ] })
1587
2599
  ] });
1588
2600
  }
@@ -1649,7 +2661,7 @@ function resolveOwnMessageStatus(params) {
1649
2661
  }
1650
2662
 
1651
2663
  // src/participant/ParticipantMessageBubble.tsx
1652
- import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
2664
+ import { jsx as jsx28, jsxs as jsxs22 } from "react/jsx-runtime";
1653
2665
  function describeItem(item, perspective) {
1654
2666
  if (item.kind === "server") {
1655
2667
  const { message } = item;
@@ -1686,13 +2698,13 @@ function ParticipantMessageBubble(props) {
1686
2698
  const ownStatus = content.isMine ? resolveOwnMessageStatus({ displayState: content.displayState, serverStatus: content.serverStatus, confirmsRead }) : void 0;
1687
2699
  const isFailed = ownStatus === "failed";
1688
2700
  const canRetry = isFailed && onRetry !== void 0;
1689
- const bubble = /* @__PURE__ */ jsxs16("div", { className: bubbleClassName(content.isMine, tail, isFailed), children: [
1690
- canRetry ? /* @__PURE__ */ jsx21(RetryButton, { labels, onRetry, describedBy: textId }) : null,
1691
- /* @__PURE__ */ jsx21(BubbleBody, { ...props, content, ownStatus, textId: canRetry ? textId : void 0 })
2701
+ const bubble = /* @__PURE__ */ jsxs22("div", { className: bubbleClassName(content.isMine, tail, isFailed), children: [
2702
+ canRetry ? /* @__PURE__ */ jsx28(RetryButton, { labels, onRetry, describedBy: textId }) : null,
2703
+ /* @__PURE__ */ jsx28(BubbleBody, { ...props, content, ownStatus, textId: canRetry ? textId : void 0 })
1692
2704
  ] });
1693
2705
  if (avatar === void 0 || content.isMine) return bubble;
1694
- return /* @__PURE__ */ jsxs16("div", { className: "cv-p-bubble-row", children: [
1695
- /* @__PURE__ */ jsx21("span", { className: "cv-p-bubble-row__avatar", children: avatar }),
2706
+ return /* @__PURE__ */ jsxs22("div", { className: "cv-p-bubble-row", children: [
2707
+ /* @__PURE__ */ jsx28("span", { className: "cv-p-bubble-row__avatar", children: avatar }),
1696
2708
  bubble
1697
2709
  ] });
1698
2710
  }
@@ -1714,7 +2726,7 @@ function resolveParticipantBubbleActions(item, handlers) {
1714
2726
  }
1715
2727
 
1716
2728
  // src/participant/ParticipantTimeline.tsx
1717
- import { jsx as jsx22 } from "react/jsx-runtime";
2729
+ import { jsx as jsx29 } from "react/jsx-runtime";
1718
2730
  function createdAtOf2(item) {
1719
2731
  return item.kind === "server" ? item.message.createdAt : item.pending.createdAt;
1720
2732
  }
@@ -1731,7 +2743,7 @@ function resolveActions(item, pendingActions) {
1731
2743
  }
1732
2744
  function resolveAvatarSlot({ isEnabled, isMine, showAvatar, author, render }) {
1733
2745
  if (!isEnabled || isMine) return void 0;
1734
- return showAvatar ? /* @__PURE__ */ jsx22(ParticipantAuthorAvatar, { name: author, render }) : null;
2746
+ return showAvatar ? /* @__PURE__ */ jsx29(ParticipantAuthorAvatar, { name: author, render }) : null;
1735
2747
  }
1736
2748
  function ParticipantTimeline(props) {
1737
2749
  const { items, labels, locale, resolveAttachmentUrl, pendingActions } = props;
@@ -1743,7 +2755,7 @@ function ParticipantTimeline(props) {
1743
2755
  for (const item of items) {
1744
2756
  const day = new Date(createdAtOf2(item));
1745
2757
  if (previousDay === void 0 || !isSameDay(previousDay, day)) {
1746
- nodes.push(/* @__PURE__ */ jsx22(ParticipantDayDivider, { day, locale }, `day:${keyOf(item)}`));
2758
+ nodes.push(/* @__PURE__ */ jsx29(ParticipantDayDivider, { day, locale }, `day:${keyOf(item)}`));
1747
2759
  previousAuthor = void 0;
1748
2760
  }
1749
2761
  previousDay = day;
@@ -1758,7 +2770,7 @@ function ParticipantTimeline(props) {
1758
2770
  render: props.renderAuthorAvatar
1759
2771
  });
1760
2772
  nodes.push(
1761
- /* @__PURE__ */ jsx22(
2773
+ /* @__PURE__ */ jsx29(
1762
2774
  ParticipantMessageBubble,
1763
2775
  {
1764
2776
  item,
@@ -1774,19 +2786,19 @@ function ParticipantTimeline(props) {
1774
2786
  )
1775
2787
  );
1776
2788
  }
1777
- return /* @__PURE__ */ jsx22("div", { className: "cv-p-thread__messages", children: nodes });
2789
+ return /* @__PURE__ */ jsx29("div", { className: "cv-p-thread__messages", children: nodes });
1778
2790
  }
1779
2791
 
1780
2792
  // src/participant/ParticipantConversationAvatar.tsx
1781
- import { jsx as jsx23 } from "react/jsx-runtime";
2793
+ import { jsx as jsx30 } from "react/jsx-runtime";
1782
2794
  function ParticipantConversationAvatar({ icon }) {
1783
- return /* @__PURE__ */ jsx23("span", { className: "cv-p-thread__avatar", "aria-hidden": "true", children: icon });
2795
+ return /* @__PURE__ */ jsx30("span", { className: "cv-p-thread__avatar", "aria-hidden": "true", children: icon });
1784
2796
  }
1785
2797
 
1786
2798
  // src/participant/ParticipantProtocolBadge.tsx
1787
2799
  import { Check as Check2, Copy as Copy2 } from "lucide-react";
1788
- import { useEffect as useEffect7, useReducer as useReducer4 } from "react";
1789
- import { Fragment as Fragment5, jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
2800
+ import { useEffect as useEffect14, useReducer as useReducer4 } from "react";
2801
+ import { Fragment as Fragment7, jsx as jsx31, jsxs as jsxs23 } from "react/jsx-runtime";
1790
2802
  var COPIED_ANNOUNCEMENT_MS = 3e3;
1791
2803
  function ParticipantProtocolBadgeView({
1792
2804
  protocol,
@@ -1795,16 +2807,16 @@ function ParticipantProtocolBadgeView({
1795
2807
  onCopy
1796
2808
  }) {
1797
2809
  const { isCopied, copyCount } = copyState;
1798
- return /* @__PURE__ */ jsxs17(Fragment5, { children: [
1799
- /* @__PURE__ */ jsxs17("span", { className: "cv-p-protocol cv-p-protocol--header", children: [
1800
- /* @__PURE__ */ jsxs17("span", { className: "cv-p-protocol__code", children: [
1801
- /* @__PURE__ */ jsxs17("span", { className: "cv-p-sr-only", children: [
2810
+ return /* @__PURE__ */ jsxs23(Fragment7, { children: [
2811
+ /* @__PURE__ */ jsxs23("span", { className: "cv-p-protocol cv-p-protocol--header", children: [
2812
+ /* @__PURE__ */ jsxs23("span", { className: "cv-p-protocol__code", children: [
2813
+ /* @__PURE__ */ jsxs23("span", { className: "cv-p-sr-only", children: [
1802
2814
  labels.protocolPrefix,
1803
2815
  " "
1804
2816
  ] }),
1805
2817
  protocol
1806
2818
  ] }),
1807
- /* @__PURE__ */ jsxs17(
2819
+ /* @__PURE__ */ jsxs23(
1808
2820
  "button",
1809
2821
  {
1810
2822
  type: "button",
@@ -1813,19 +2825,19 @@ function ParticipantProtocolBadgeView({
1813
2825
  "data-cv-tooltip": labels.copyProtocol,
1814
2826
  onClick: onCopy,
1815
2827
  children: [
1816
- isCopied ? /* @__PURE__ */ jsx24(Check2, { size: 18, "aria-hidden": true }) : /* @__PURE__ */ jsx24(Copy2, { size: 18, "aria-hidden": true }),
1817
- isCopied ? /* @__PURE__ */ jsx24("span", { className: "cv-p-protocol__copied", "aria-hidden": "true", children: labels.protocolCopied }) : null
2828
+ isCopied ? /* @__PURE__ */ jsx31(Check2, { size: 18, "aria-hidden": true }) : /* @__PURE__ */ jsx31(Copy2, { size: 18, "aria-hidden": true }),
2829
+ isCopied ? /* @__PURE__ */ jsx31("span", { className: "cv-p-protocol__copied", "aria-hidden": "true", children: labels.protocolCopied }) : null
1818
2830
  ]
1819
2831
  }
1820
2832
  )
1821
2833
  ] }),
1822
- /* @__PURE__ */ jsx24("div", { className: "cv-p-sr-only", role: "status", "aria-live": "polite", children: isCopied ? /* @__PURE__ */ jsx24("span", { children: labels.protocolCopied }, copyCount) : null })
2834
+ /* @__PURE__ */ jsx31("div", { className: "cv-p-sr-only", role: "status", "aria-live": "polite", children: isCopied ? /* @__PURE__ */ jsx31("span", { children: labels.protocolCopied }, copyCount) : null })
1823
2835
  ] });
1824
2836
  }
1825
2837
  function ParticipantProtocolBadge({ protocol, labels }) {
1826
2838
  const [copyState, dispatch] = useReducer4(protocolCopyReducer, INITIAL_PROTOCOL_COPY_STATE);
1827
2839
  const { isCopied, copyCount } = copyState;
1828
- useEffect7(() => {
2840
+ useEffect14(() => {
1829
2841
  if (!isCopied) return void 0;
1830
2842
  const timer = setTimeout(() => dispatch({ type: "expired" }), COPIED_ANNOUNCEMENT_MS);
1831
2843
  return () => clearTimeout(timer);
@@ -1834,7 +2846,7 @@ function ParticipantProtocolBadge({ protocol, labels }) {
1834
2846
  const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
1835
2847
  if (await copyProtocolToClipboard(protocol, clipboard)) dispatch({ type: "copied" });
1836
2848
  }
1837
- return /* @__PURE__ */ jsx24(
2849
+ return /* @__PURE__ */ jsx31(
1838
2850
  ParticipantProtocolBadgeView,
1839
2851
  {
1840
2852
  protocol,
@@ -1846,19 +2858,19 @@ function ParticipantProtocolBadge({ protocol, labels }) {
1846
2858
  }
1847
2859
 
1848
2860
  // src/participant/ParticipantThreadHeader.tsx
1849
- import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
2861
+ import { jsx as jsx32, jsxs as jsxs24 } from "react/jsx-runtime";
1850
2862
  function Title({ conversation, labels, onOpenSubject }) {
1851
2863
  const { subjectType, subjectId, subjectLabel } = conversation;
1852
- return /* @__PURE__ */ jsx25("h2", { className: "cv-p-thread__title", children: onOpenSubject ? /* @__PURE__ */ jsx25(
2864
+ return /* @__PURE__ */ jsx32("h2", { className: "cv-p-thread__title", children: onOpenSubject ? /* @__PURE__ */ jsx32(
1853
2865
  "button",
1854
2866
  {
1855
2867
  type: "button",
1856
2868
  className: "cv-p-thread__title-button",
1857
2869
  "aria-label": `${labels.openSubject}: ${subjectLabel}`,
1858
2870
  onClick: () => onOpenSubject({ subjectType, subjectId }),
1859
- children: /* @__PURE__ */ jsx25("span", { className: "cv-p-thread__title-text", children: subjectLabel })
2871
+ children: /* @__PURE__ */ jsx32("span", { className: "cv-p-thread__title-text", children: subjectLabel })
1860
2872
  }
1861
- ) : /* @__PURE__ */ jsx25("span", { className: "cv-p-thread__title-text", children: subjectLabel }) });
2873
+ ) : /* @__PURE__ */ jsx32("span", { className: "cv-p-thread__title-text", children: subjectLabel }) });
1862
2874
  }
1863
2875
  function ParticipantThreadHeader({
1864
2876
  conversation,
@@ -1873,30 +2885,30 @@ function ParticipantThreadHeader({
1873
2885
  const group = subjectGroups?.find((candidate) => candidate.subjectType === conversation.subjectType);
1874
2886
  const icon = resolveConversationIcon({ conversation, group, renderSubjectIcon });
1875
2887
  const hasChannels = resolveParticipantChannels(conversation.channels).length > 0;
1876
- return /* @__PURE__ */ jsxs18("header", { className: "cv-p-thread__header", children: [
1877
- onBack ? /* @__PURE__ */ jsx25(ParticipantBackButton, { label: labels.back, onBack }) : null,
1878
- icon !== void 0 ? /* @__PURE__ */ jsx25(ParticipantConversationAvatar, { icon }) : null,
1879
- /* @__PURE__ */ jsxs18("div", { className: "cv-p-thread__heading", children: [
1880
- icon === void 0 && group ? /* @__PURE__ */ jsx25("p", { className: "cv-p-thread__eyebrow", children: group.label }) : null,
1881
- /* @__PURE__ */ jsx25(Title, { conversation, labels, onOpenSubject }),
1882
- counterpartLabel ? /* @__PURE__ */ jsx25("p", { className: "cv-p-thread__counterpart", children: counterpartLabel }) : null,
1883
- conversation.protocol || hasChannels ? /* @__PURE__ */ jsxs18("div", { className: "cv-p-thread__meta", children: [
1884
- conversation.protocol ? /* @__PURE__ */ jsx25(ParticipantProtocolBadge, { protocol: conversation.protocol, labels }) : null,
1885
- /* @__PURE__ */ jsx25(ParticipantChannelBadges, { channels: conversation.channels, labels, variant: "inline", iconSize: 16 })
2888
+ return /* @__PURE__ */ jsxs24("header", { className: "cv-p-thread__header", children: [
2889
+ onBack ? /* @__PURE__ */ jsx32(ParticipantBackButton, { label: labels.back, onBack }) : null,
2890
+ icon !== void 0 ? /* @__PURE__ */ jsx32(ParticipantConversationAvatar, { icon }) : null,
2891
+ /* @__PURE__ */ jsxs24("div", { className: "cv-p-thread__heading", children: [
2892
+ icon === void 0 && group ? /* @__PURE__ */ jsx32("p", { className: "cv-p-thread__eyebrow", children: group.label }) : null,
2893
+ /* @__PURE__ */ jsx32(Title, { conversation, labels, onOpenSubject }),
2894
+ counterpartLabel ? /* @__PURE__ */ jsx32("p", { className: "cv-p-thread__counterpart", children: counterpartLabel }) : null,
2895
+ conversation.protocol || hasChannels ? /* @__PURE__ */ jsxs24("div", { className: "cv-p-thread__meta", children: [
2896
+ conversation.protocol ? /* @__PURE__ */ jsx32(ParticipantProtocolBadge, { protocol: conversation.protocol, labels }) : null,
2897
+ /* @__PURE__ */ jsx32(ParticipantChannelBadges, { channels: conversation.channels, labels, variant: "inline", iconSize: 16 })
1886
2898
  ] }) : null
1887
2899
  ] }),
1888
- actions ? /* @__PURE__ */ jsx25("div", { className: "cv-p-thread__actions", children: actions }) : null
2900
+ actions ? /* @__PURE__ */ jsx32("div", { className: "cv-p-thread__actions", children: actions }) : null
1889
2901
  ] });
1890
2902
  }
1891
2903
 
1892
2904
  // src/participant/ParticipantThread.tsx
1893
- import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
2905
+ import { jsx as jsx33, jsxs as jsxs25 } from "react/jsx-runtime";
1894
2906
  function Footer({ props }) {
1895
2907
  const { conversation, labels, draft } = props;
1896
2908
  if (conversation.status === "closed") {
1897
- return /* @__PURE__ */ jsx26("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
2909
+ return /* @__PURE__ */ jsx33("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
1898
2910
  }
1899
- return /* @__PURE__ */ jsx26(
2911
+ return /* @__PURE__ */ jsx33(
1900
2912
  ParticipantComposer,
1901
2913
  {
1902
2914
  value: draft.value,
@@ -1909,12 +2921,14 @@ function Footer({ props }) {
1909
2921
  isSending: props.isSending,
1910
2922
  maxLength: props.maxLength,
1911
2923
  acceptedTypes: props.acceptedTypes,
1912
- quickReplies: props.quickReplies
2924
+ quickReplies: props.quickReplies,
2925
+ audioRecording: props.audioRecording,
2926
+ onSendAudio: props.onSendAudio
1913
2927
  }
1914
2928
  );
1915
2929
  }
1916
2930
  function Header({ props }) {
1917
- return /* @__PURE__ */ jsx26(
2931
+ return /* @__PURE__ */ jsx33(
1918
2932
  ParticipantThreadHeader,
1919
2933
  {
1920
2934
  conversation: props.conversation,
@@ -1932,17 +2946,17 @@ function ThreadBody(props) {
1932
2946
  const { conversation, labels, hasMore, onLoadOlder, renderSubjectCard } = props;
1933
2947
  const subjectCard = renderSubjectCard?.(conversation);
1934
2948
  const loadView = resolveLoadView({ status: props.status, hasItems: props.items.length > 0 });
1935
- const rootRef = useRef5(null);
2949
+ const rootRef = useRef13(null);
1936
2950
  useSuppressTouchTooltips(rootRef);
1937
- return /* @__PURE__ */ jsxs19("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, ref: rootRef, children: [
1938
- /* @__PURE__ */ jsx26(Header, { props }),
1939
- subjectCard ? /* @__PURE__ */ jsx26("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
1940
- /* @__PURE__ */ jsxs19("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, tabIndex: -1, children: [
1941
- loadView.isLoading ? /* @__PURE__ */ jsx26(ParticipantLoading, { labels }) : null,
1942
- loadView.hasError ? /* @__PURE__ */ jsx26(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
1943
- hasMore && onLoadOlder ? /* @__PURE__ */ jsx26("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
1944
- /* @__PURE__ */ jsx26(ParticipantTimeline, { ...props }),
1945
- /* @__PURE__ */ jsx26(
2951
+ return /* @__PURE__ */ jsxs25("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, ref: rootRef, children: [
2952
+ /* @__PURE__ */ jsx33(Header, { props }),
2953
+ subjectCard ? /* @__PURE__ */ jsx33("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
2954
+ /* @__PURE__ */ jsxs25("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, tabIndex: -1, children: [
2955
+ loadView.isLoading ? /* @__PURE__ */ jsx33(ParticipantLoading, { labels }) : null,
2956
+ loadView.hasError ? /* @__PURE__ */ jsx33(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
2957
+ hasMore && onLoadOlder ? /* @__PURE__ */ jsx33("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
2958
+ /* @__PURE__ */ jsx33(ParticipantTimeline, { ...props }),
2959
+ /* @__PURE__ */ jsx33(
1946
2960
  ParticipantScrollToLatest,
1947
2961
  {
1948
2962
  label: labels.scrollToLatest,
@@ -1953,19 +2967,19 @@ function ThreadBody(props) {
1953
2967
  }
1954
2968
  )
1955
2969
  ] }),
1956
- /* @__PURE__ */ jsx26(ParticipantThreadLive, { notice: props.newMessagesCount > 0 ? labels.newMessages : "" }),
1957
- /* @__PURE__ */ jsx26(Footer, { props })
2970
+ /* @__PURE__ */ jsx33(ParticipantThreadLive, { notice: props.newMessagesCount > 0 ? labels.newMessages : "" }),
2971
+ /* @__PURE__ */ jsx33(Footer, { props })
1958
2972
  ] });
1959
2973
  }
1960
2974
  function ParticipantThread(props) {
1961
- return /* @__PURE__ */ jsxs19(ParticipantCopyAnnouncer, { children: [
1962
- /* @__PURE__ */ jsx26(ThreadBody, { ...props }),
1963
- /* @__PURE__ */ jsx26(TooltipLayer, {})
2975
+ return /* @__PURE__ */ jsxs25(ParticipantCopyAnnouncer, { children: [
2976
+ /* @__PURE__ */ jsx33(ThreadBody, { ...props }),
2977
+ /* @__PURE__ */ jsx33(TooltipLayer, {})
1964
2978
  ] });
1965
2979
  }
1966
2980
 
1967
2981
  // src/participant/useParticipantThreadController.ts
1968
- import { useMemo as useMemo4 } from "react";
2982
+ import { useMemo as useMemo5 } from "react";
1969
2983
 
1970
2984
  // src/participant/bindAttachmentUrlResolver.ts
1971
2985
  function bindAttachmentUrlResolver(api) {
@@ -2049,7 +3063,7 @@ function appendEntryToDraft(entry, current) {
2049
3063
  }
2050
3064
 
2051
3065
  // src/participant/useParticipantConversationView.ts
2052
- import { useCallback as useCallback5, useMemo as useMemo3 } from "react";
3066
+ import { useCallback as useCallback9, useMemo as useMemo4 } from "react";
2053
3067
 
2054
3068
  // src/participant/participantDrafts.ts
2055
3069
  var EMPTY_PARTICIPANT_DRAFT = { text: "", files: [] };
@@ -2093,7 +3107,7 @@ function participantSendStatesReducer(states, { subject, action }) {
2093
3107
  }
2094
3108
 
2095
3109
  // src/participant/useParticipantThread.ts
2096
- import { useCallback as useCallback4, useEffect as useEffect11, useMemo as useMemo2, useReducer as useReducer5, useRef as useRef9 } from "react";
3110
+ import { useCallback as useCallback8, useEffect as useEffect18, useMemo as useMemo3, useReducer as useReducer5, useRef as useRef17 } from "react";
2097
3111
 
2098
3112
  // src/participant/participantThreadState.ts
2099
3113
  var INITIAL_PARTICIPANT_THREAD_STATE = {
@@ -2129,7 +3143,7 @@ function participantThreadReducer(state, action) {
2129
3143
  }
2130
3144
 
2131
3145
  // src/participant/useParticipantMarkRead.ts
2132
- import { useEffect as useEffect9 } from "react";
3146
+ import { useEffect as useEffect16 } from "react";
2133
3147
 
2134
3148
  // src/participant/shouldMarkRead.ts
2135
3149
  function shouldMarkParticipantRead(params) {
@@ -2141,12 +3155,12 @@ function shouldMarkParticipantRead(params) {
2141
3155
  }
2142
3156
 
2143
3157
  // src/participant/useDocumentVisibility.ts
2144
- import { useEffect as useEffect8, useState as useState4 } from "react";
3158
+ import { useEffect as useEffect15, useState as useState9 } from "react";
2145
3159
  function useDocumentVisibility() {
2146
- const [visibilityState, setVisibilityState] = useState4(
3160
+ const [visibilityState, setVisibilityState] = useState9(
2147
3161
  () => typeof document === "undefined" ? "visible" : document.visibilityState
2148
3162
  );
2149
- useEffect8(() => {
3163
+ useEffect15(() => {
2150
3164
  function handleVisibilityChange() {
2151
3165
  setVisibilityState(document.visibilityState);
2152
3166
  }
@@ -2161,18 +3175,18 @@ function useDocumentVisibility() {
2161
3175
  function useParticipantMarkRead(params) {
2162
3176
  const { apiRef, subject, unreadCount, messageCount, onMarkedRead } = params;
2163
3177
  const visibilityState = useDocumentVisibility();
2164
- useEffect9(() => {
3178
+ useEffect16(() => {
2165
3179
  if (!shouldMarkParticipantRead({ selected: subject, subject, visibilityState, unreadCount })) return;
2166
3180
  void apiRef.current.markRead(subject).then(() => onMarkedRead?.(subject), () => void 0);
2167
3181
  }, [apiRef, subject, visibilityState, unreadCount, messageCount, onMarkedRead]);
2168
3182
  }
2169
3183
 
2170
3184
  // src/participant/useParticipantRevalidation.ts
2171
- import { useEffect as useEffect10, useRef as useRef6 } from "react";
3185
+ import { useEffect as useEffect17, useRef as useRef14 } from "react";
2172
3186
  function useParticipantRevalidation(api, onRevalidate) {
2173
- const handlerRef = useRef6(onRevalidate);
3187
+ const handlerRef = useRef14(onRevalidate);
2174
3188
  handlerRef.current = onRevalidate;
2175
- useEffect10(() => {
3189
+ useEffect17(() => {
2176
3190
  function handleFocus() {
2177
3191
  handlerRef.current();
2178
3192
  }
@@ -2199,12 +3213,12 @@ function useParticipantRevalidation(api, onRevalidate) {
2199
3213
  }
2200
3214
 
2201
3215
  // src/participant/useParticipantSend.ts
2202
- import { useCallback as useCallback2, useRef as useRef7 } from "react";
3216
+ import { useCallback as useCallback6, useRef as useRef15 } from "react";
2203
3217
  function useParticipantSend(params) {
2204
3218
  const { apiRef, subject, dispatchSend, onSentMessage } = params;
2205
- const entriesRef = useRef7(params.entries);
3219
+ const entriesRef = useRef15(params.entries);
2206
3220
  entriesRef.current = params.entries;
2207
- const deliver = useCallback2(
3221
+ const deliver = useCallback6(
2208
3222
  async (entry) => {
2209
3223
  const { clientMessageId } = entry;
2210
3224
  try {
@@ -2228,7 +3242,7 @@ function useParticipantSend(params) {
2228
3242
  },
2229
3243
  [apiRef, dispatchSend, onSentMessage]
2230
3244
  );
2231
- const send = useCallback2(
3245
+ const send = useCallback6(
2232
3246
  async (content) => {
2233
3247
  const entry = createParticipantSendEntry({
2234
3248
  clientMessageId: crypto.randomUUID(),
@@ -2241,7 +3255,7 @@ function useParticipantSend(params) {
2241
3255
  },
2242
3256
  [deliver, dispatchSend, subject]
2243
3257
  );
2244
- const retry = useCallback2(
3258
+ const retry = useCallback6(
2245
3259
  async (clientMessageId) => {
2246
3260
  const entry = findFailedEntry(entriesRef.current, clientMessageId);
2247
3261
  if (!entry) return;
@@ -2254,13 +3268,13 @@ function useParticipantSend(params) {
2254
3268
  }
2255
3269
 
2256
3270
  // src/participant/useParticipantUnread.ts
2257
- import { useCallback as useCallback3, useRef as useRef8, useState as useState5 } from "react";
3271
+ import { useCallback as useCallback7, useRef as useRef16, useState as useState10 } from "react";
2258
3272
  function useParticipantUnread(params) {
2259
3273
  const { messages, perspective, reportedUnreadCount, onMarkedRead } = params;
2260
- const [acknowledgedCount, setAcknowledgedCount] = useState5(0);
2261
- const messagesRef = useRef8(messages);
3274
+ const [acknowledgedCount, setAcknowledgedCount] = useState10(0);
3275
+ const messagesRef = useRef16(messages);
2262
3276
  messagesRef.current = messages;
2263
- const handleMarkedRead = useCallback3(
3277
+ const handleMarkedRead = useCallback7(
2264
3278
  (subject) => {
2265
3279
  if (perspective) setAcknowledgedCount(countOtherSideMessages(messagesRef.current, perspective));
2266
3280
  onMarkedRead?.(subject);
@@ -2283,13 +3297,13 @@ function isForSubject(event, subject) {
2283
3297
  function useParticipantThread(params) {
2284
3298
  const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [], perspective } = params;
2285
3299
  const { subjectType, subjectId } = subject;
2286
- const stableSubject = useMemo2(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
3300
+ const stableSubject = useMemo3(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
2287
3301
  const [state, dispatch] = useReducer5(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE);
2288
- const apiRef = useRef9(api);
3302
+ const apiRef = useRef17(api);
2289
3303
  apiRef.current = api;
2290
- const activeSubjectKey = useRef9("");
3304
+ const activeSubjectKey = useRef17("");
2291
3305
  const confirmsRead = channelCapabilityFor(channel).confirmsRead;
2292
- const refresh = useCallback4(async () => {
3306
+ const refresh = useCallback8(async () => {
2293
3307
  const requestKey = `${subjectType}:${subjectId}`;
2294
3308
  dispatch({ type: "started" });
2295
3309
  try {
@@ -2302,7 +3316,7 @@ function useParticipantThread(params) {
2302
3316
  }
2303
3317
  }, [stableSubject, subjectType, subjectId]);
2304
3318
  const oldestMessageId = state.messages[0]?.id;
2305
- const loadOlder = useCallback4(async () => {
3319
+ const loadOlder = useCallback8(async () => {
2306
3320
  if (!oldestMessageId) return;
2307
3321
  const requestKey = `${subjectType}:${subjectId}`;
2308
3322
  try {
@@ -2314,9 +3328,9 @@ function useParticipantThread(params) {
2314
3328
  if (activeSubjectKey.current === requestKey) dispatch({ type: "failed", error: describeError(error) });
2315
3329
  }
2316
3330
  }, [stableSubject, subjectType, subjectId, oldestMessageId]);
2317
- const handleSentMessage = useCallback4((message) => dispatch({ type: "sendConfirmed", message }), []);
3331
+ const handleSentMessage = useCallback8((message) => dispatch({ type: "sendConfirmed", message }), []);
2318
3332
  const sending = useParticipantSend({ apiRef, subject: stableSubject, entries: sendEntries, dispatchSend, onSentMessage: handleSentMessage });
2319
- useEffect11(() => {
3333
+ useEffect18(() => {
2320
3334
  activeSubjectKey.current = `${subjectType}:${subjectId}`;
2321
3335
  dispatch({ type: "reset" });
2322
3336
  void refresh();
@@ -2324,7 +3338,7 @@ function useParticipantThread(params) {
2324
3338
  useParticipantRevalidation(api, (event) => {
2325
3339
  if (isForSubject(event, stableSubject)) void refresh();
2326
3340
  });
2327
- useEffect11(() => {
3341
+ useEffect18(() => {
2328
3342
  if (sendEntries.length === 0) return;
2329
3343
  dispatchSend({ type: "reflected", knownClientMessageIds: collectKnownClientMessageIds({ messages: state.messages, hostPending }) });
2330
3344
  }, [sendEntries, state.messages, hostPending, dispatchSend]);
@@ -2336,7 +3350,7 @@ function useParticipantThread(params) {
2336
3350
  messageCount: state.messages.length,
2337
3351
  onMarkedRead: unread.handleMarkedRead
2338
3352
  });
2339
- const localPending = useMemo2(() => toLocalPending(sendEntries), [sendEntries]);
3353
+ const localPending = useMemo3(() => toLocalPending(sendEntries), [sendEntries]);
2340
3354
  return { ...state, localPending, confirmsRead, refresh, loadOlder, ...sending };
2341
3355
  }
2342
3356
 
@@ -2345,7 +3359,7 @@ function useParticipantConversationView(params) {
2345
3359
  const { api, subject, conversation, pendingMessages, sendStates, dispatchSendStates } = params;
2346
3360
  const { subjectType, subjectId } = subject;
2347
3361
  const sendEntries = selectSendEntries(sendStates, subject);
2348
- const dispatchSend = useCallback5(
3362
+ const dispatchSend = useCallback9(
2349
3363
  (action) => dispatchSendStates({ subject: { subjectType, subjectId }, action }),
2350
3364
  [dispatchSendStates, subjectType, subjectId]
2351
3365
  );
@@ -2360,7 +3374,7 @@ function useParticipantConversationView(params) {
2360
3374
  hostPending: pendingMessages,
2361
3375
  perspective: params.perspective
2362
3376
  });
2363
- const items = useMemo3(
3377
+ const items = useMemo4(
2364
3378
  () => mergeParticipantMessages({
2365
3379
  serverMessages: thread.messages,
2366
3380
  hostPending: pendingMessages ?? [],
@@ -2373,9 +3387,9 @@ function useParticipantConversationView(params) {
2373
3387
  }
2374
3388
 
2375
3389
  // src/participant/useParticipantDraft.ts
2376
- import { useState as useState6 } from "react";
3390
+ import { useState as useState11 } from "react";
2377
3391
  function useParticipantDraft(draftsRef, subject) {
2378
- const [draft, setDraftState] = useState6(() => getDraft(draftsRef.current, subject));
3392
+ const [draft, setDraftState] = useState11(() => getDraft(draftsRef.current, subject));
2379
3393
  function replaceDrafts(next) {
2380
3394
  draftsRef.current = next;
2381
3395
  setDraftState(getDraft(next, subject));
@@ -2394,8 +3408,29 @@ function useParticipantDraft(draftsRef, subject) {
2394
3408
  return { draft, setText: (text) => update({ text }), setFiles: (files) => update({ files }), takeForSend, restoreFromEntry };
2395
3409
  }
2396
3410
 
3411
+ // src/participant/useParticipantSendAudio.ts
3412
+ import { useCallback as useCallback10, useRef as useRef18 } from "react";
3413
+ function useParticipantSendAudio(send) {
3414
+ const sendRef = useRef18(send);
3415
+ sendRef.current = send;
3416
+ const inFlightRef = useRef18(/* @__PURE__ */ new Set());
3417
+ return useCallback10((file) => {
3418
+ const inFlight = inFlightRef.current;
3419
+ if (inFlight.has(file)) return;
3420
+ inFlight.add(file);
3421
+ void (async () => {
3422
+ try {
3423
+ await sendRef.current({ files: [file] });
3424
+ } catch {
3425
+ } finally {
3426
+ inFlight.delete(file);
3427
+ }
3428
+ })();
3429
+ }, []);
3430
+ }
3431
+
2397
3432
  // src/participant/useParticipantThreadScroll.ts
2398
- import { useEffect as useEffect12, useRef as useRef11 } from "react";
3433
+ import { useEffect as useEffect19, useRef as useRef20 } from "react";
2399
3434
 
2400
3435
  // src/participant/participantNewMessages.ts
2401
3436
  function collectServerMessageIds(items) {
@@ -2420,9 +3455,9 @@ function countNewIncomingMessages(previousIds, currentItems, perspective) {
2420
3455
  }
2421
3456
 
2422
3457
  // src/participant/useScrollAwayState.ts
2423
- import { useCallback as useCallback6 } from "react";
3458
+ import { useCallback as useCallback11 } from "react";
2424
3459
  function useScrollAwayState(ref, isNearBottom) {
2425
- const scrollToLatest = useCallback6(() => {
3460
+ const scrollToLatest = useCallback11(() => {
2426
3461
  const element = ref.current;
2427
3462
  if (!element) return;
2428
3463
  element.scrollTo(
@@ -2433,7 +3468,7 @@ function useScrollAwayState(ref, isNearBottom) {
2433
3468
  }
2434
3469
 
2435
3470
  // src/participant/useStickToBottom.ts
2436
- import { useCallback as useCallback7, useLayoutEffect as useLayoutEffect3, useRef as useRef10, useState as useState7 } from "react";
3471
+ import { useCallback as useCallback12, useLayoutEffect as useLayoutEffect3, useRef as useRef19, useState as useState12 } from "react";
2437
3472
 
2438
3473
  // src/participant/participantScroll.ts
2439
3474
  var NEAR_BOTTOM_THRESHOLD_PX = 80;
@@ -2455,17 +3490,17 @@ function resolveScrollAction(params) {
2455
3490
 
2456
3491
  // src/participant/useStickToBottom.ts
2457
3492
  function useStickToBottom({ firstKey, lastKey }) {
2458
- const ref = useRef10(null);
2459
- const snapshot = useRef10({
3493
+ const ref = useRef19(null);
3494
+ const snapshot = useRef19({
2460
3495
  hasPositioned: false,
2461
3496
  firstKey: void 0,
2462
3497
  lastKey: void 0,
2463
3498
  scrollTop: 0,
2464
3499
  scrollHeight: 0
2465
3500
  });
2466
- const [isNearBottom, setIsNearBottom] = useState7(true);
2467
- const isNearBottomRef = useRef10(true);
2468
- const onScroll = useCallback7((event) => {
3501
+ const [isNearBottom, setIsNearBottom] = useState12(true);
3502
+ const isNearBottomRef = useRef19(true);
3503
+ const onScroll = useCallback12((event) => {
2469
3504
  const { scrollTop, clientHeight, scrollHeight } = event.currentTarget;
2470
3505
  const isNear = shouldStickToBottom({ scrollTop, clientHeight, scrollHeight, threshold: NEAR_BOTTOM_THRESHOLD_PX });
2471
3506
  snapshot.current.scrollTop = scrollTop;
@@ -2520,8 +3555,8 @@ function useParticipantThreadScroll(items, perspective) {
2520
3555
  });
2521
3556
  const { isAwayFromBottom, scrollToLatest } = useScrollAwayState(base.ref, isNearBottom);
2522
3557
  const scroll = { ...base, isAwayFromBottom, scrollToLatest };
2523
- const seenIds = useRef11(/* @__PURE__ */ new Set());
2524
- useEffect12(() => {
3558
+ const seenIds = useRef20(/* @__PURE__ */ new Set());
3559
+ useEffect19(() => {
2525
3560
  if (isNearBottom) seenIds.current = collectServerMessageIds(items);
2526
3561
  }, [isNearBottom, items]);
2527
3562
  return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items, perspective) };
@@ -2536,22 +3571,26 @@ function buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending
2536
3571
  ...onRetryPending ? { retryHost: onRetryPending } : {}
2537
3572
  };
2538
3573
  }
3574
+ function buildEditHandler({ sendEntries, dispatchSend, restoreFromEntry }) {
3575
+ return (clientMessageId) => {
3576
+ const entry = findFailedEntry(sendEntries, clientMessageId);
3577
+ if (!entry) return;
3578
+ restoreFromEntry(entry);
3579
+ dispatchSend({ type: "discarded", clientMessageId });
3580
+ };
3581
+ }
2539
3582
  function useParticipantThreadController(params) {
2540
3583
  const { api, subject, conversation, draftsRef } = params;
2541
3584
  const { draft, setText, setFiles, takeForSend, restoreFromEntry } = useParticipantDraft(draftsRef, subject);
2542
3585
  const { thread, items, sendEntries, dispatchSend } = useParticipantConversationView(params);
2543
- const resolveAttachmentUrl = useMemo4(() => bindAttachmentUrlResolver(api), [api]);
3586
+ const resolveAttachmentUrl = useMemo5(() => bindAttachmentUrlResolver(api), [api]);
2544
3587
  const { scroll, newMessagesCount } = useParticipantThreadScroll(items, params.perspective);
3588
+ const handleSendAudio = useParticipantSendAudio(thread.send);
3589
+ const handleEdit = buildEditHandler({ sendEntries, dispatchSend, restoreFromEntry });
2545
3590
  function handleSend() {
2546
3591
  const content = takeForSend();
2547
3592
  if (content) void thread.send(content);
2548
3593
  }
2549
- function handleEdit(clientMessageId) {
2550
- const entry = findFailedEntry(sendEntries, clientMessageId);
2551
- if (!entry) return;
2552
- restoreFromEntry(entry);
2553
- dispatchSend({ type: "discarded", clientMessageId });
2554
- }
2555
3594
  return {
2556
3595
  conversation,
2557
3596
  items,
@@ -2569,17 +3608,18 @@ function useParticipantThreadController(params) {
2569
3608
  locale: params.locale,
2570
3609
  onLoadOlder: () => void thread.loadOlder(),
2571
3610
  pendingActions: buildPendingActions({ thread, dispatchSend, handleEdit, onRetryPending: params.onRetryPending }),
2572
- perspective: params.perspective
3611
+ perspective: params.perspective,
3612
+ onSendAudio: handleSendAudio
2573
3613
  };
2574
3614
  }
2575
3615
 
2576
3616
  // src/participant/ParticipantThreadScreen.tsx
2577
- import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
3617
+ import { jsx as jsx34, jsxs as jsxs26 } from "react/jsx-runtime";
2578
3618
  function useOpenedConversation(api, subject, isNeeded) {
2579
- const [opened, setOpened] = useState8({ status: "idle" });
3619
+ const [opened, setOpened] = useState13({ status: "idle" });
2580
3620
  const { subjectType, subjectId } = subject;
2581
3621
  const canOpen = api.openConversation !== void 0;
2582
- useEffect13(() => {
3622
+ useEffect20(() => {
2583
3623
  if (!isNeeded || !canOpen) return;
2584
3624
  let isCurrent = true;
2585
3625
  setOpened({ status: "loading" });
@@ -2597,9 +3637,9 @@ function NotFound({
2597
3637
  labels,
2598
3638
  onBack
2599
3639
  }) {
2600
- return /* @__PURE__ */ jsxs20("div", { className: "cv-p cv-p-thread", children: [
2601
- /* @__PURE__ */ jsx27("header", { className: "cv-p-thread__header", children: onBack ? /* @__PURE__ */ jsx27(ParticipantBackButton, { label: labels.back, onBack }) : null }),
2602
- /* @__PURE__ */ jsx27("p", { className: "cv-p-empty", children: labels.notFound })
3640
+ return /* @__PURE__ */ jsxs26("div", { className: "cv-p cv-p-thread", children: [
3641
+ /* @__PURE__ */ jsx34("header", { className: "cv-p-thread__header", children: onBack ? /* @__PURE__ */ jsx34(ParticipantBackButton, { label: labels.back, onBack }) : null }),
3642
+ /* @__PURE__ */ jsx34("p", { className: "cv-p-empty", children: labels.notFound })
2603
3643
  ] });
2604
3644
  }
2605
3645
  function ParticipantThreadScreen(props) {
@@ -2609,12 +3649,12 @@ function ParticipantThreadScreen(props) {
2609
3649
  );
2610
3650
  const opened = useOpenedConversation(api, selected, listed === void 0);
2611
3651
  const conversation = listed ?? (opened.status === "found" ? opened.conversation : void 0);
2612
- if (conversation) return /* @__PURE__ */ jsx27(LoadedThread, { ...props, conversation });
3652
+ if (conversation) return /* @__PURE__ */ jsx34(LoadedThread, { ...props, conversation });
2613
3653
  const isResolving = opened.status === "loading" || api.openConversation !== void 0 && opened.status === "idle";
2614
3654
  const isListLoading = inbox.status === "idle" || inbox.status === "loading";
2615
3655
  if (isResolving || api.openConversation === void 0 && isListLoading)
2616
- return /* @__PURE__ */ jsx27("div", { className: "cv-p cv-p-thread", "aria-busy": "true" });
2617
- return /* @__PURE__ */ jsx27(NotFound, { labels, onBack: props.onBack });
3656
+ return /* @__PURE__ */ jsx34("div", { className: "cv-p cv-p-thread", "aria-busy": "true" });
3657
+ return /* @__PURE__ */ jsx34(NotFound, { labels, onBack: props.onBack });
2618
3658
  }
2619
3659
  function LoadedThread(props) {
2620
3660
  const core = useParticipantThreadController({
@@ -2622,7 +3662,7 @@ function LoadedThread(props) {
2622
3662
  subject: props.selected,
2623
3663
  onMarkedRead: props.inbox.markSubjectRead
2624
3664
  });
2625
- return /* @__PURE__ */ jsx27(
3665
+ return /* @__PURE__ */ jsx34(
2626
3666
  ParticipantThread,
2627
3667
  {
2628
3668
  ...core,
@@ -2634,7 +3674,8 @@ function LoadedThread(props) {
2634
3674
  renderSubjectIcon: props.renderSubjectIcon,
2635
3675
  avatars: props.avatars,
2636
3676
  renderAuthorAvatar: props.renderAuthorAvatar,
2637
- tail: props.tail
3677
+ tail: props.tail,
3678
+ audioRecording: props.audioRecording
2638
3679
  }
2639
3680
  );
2640
3681
  }
@@ -2760,16 +3801,16 @@ function buildParticipantThemeStyle(theme) {
2760
3801
  }
2761
3802
 
2762
3803
  // src/participant/ParticipantConversationsScreen.tsx
2763
- import { jsx as jsx28 } from "react/jsx-runtime";
3804
+ import { jsx as jsx35 } from "react/jsx-runtime";
2764
3805
  var ALL_FILTER3 = "all";
2765
3806
  function ParticipantConversationsScreen(props) {
2766
3807
  const { selected, inbox, subjectGroups, classNames } = props;
2767
- const [filter, setFilter] = useState9(ALL_FILTER3);
2768
- const [searchQuery, setSearchQuery] = useState9("");
2769
- const draftsRef = useRef12(/* @__PURE__ */ new Map());
3808
+ const [filter, setFilter] = useState14(ALL_FILTER3);
3809
+ const [searchQuery, setSearchQuery] = useState14("");
3810
+ const draftsRef = useRef21(/* @__PURE__ */ new Map());
2770
3811
  const [sendStates, dispatchSendStates] = useReducer6(participantSendStatesReducer, /* @__PURE__ */ new Map());
2771
- const labels = useMemo5(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2772
- const view = useMemo5(
3812
+ const labels = useMemo6(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
3813
+ const view = useMemo6(
2773
3814
  () => groupParticipantConversations({
2774
3815
  conversations: inbox.conversations,
2775
3816
  subjectGroups,
@@ -2780,7 +3821,7 @@ function ParticipantConversationsScreen(props) {
2780
3821
  );
2781
3822
  const isSearchVisible = isInboxSearchVisible({ conversations: inbox.conversations, searchQuery });
2782
3823
  const rootClassName = ["cv-p-root", props.className, classNames?.root].filter(Boolean).join(" ");
2783
- return /* @__PURE__ */ jsx28("div", { className: rootClassName, style: buildParticipantThemeStyle(props.theme), children: selected === void 0 ? /* @__PURE__ */ jsx28("div", { className: ["cv-p-root__screen", classNames?.inbox].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx28(
3824
+ return /* @__PURE__ */ jsx35("div", { className: rootClassName, style: buildParticipantThemeStyle(props.theme), children: selected === void 0 ? /* @__PURE__ */ jsx35("div", { className: ["cv-p-root__screen", classNames?.inbox].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx35(
2784
3825
  ParticipantInbox,
2785
3826
  {
2786
3827
  view,
@@ -2797,7 +3838,7 @@ function ParticipantConversationsScreen(props) {
2797
3838
  renderSubjectIcon: props.renderSubjectIcon,
2798
3839
  search: { value: searchQuery, onChange: setSearchQuery, isVisible: isSearchVisible }
2799
3840
  }
2800
- ) }) : /* @__PURE__ */ jsx28("div", { className: ["cv-p-root__screen", classNames?.thread].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx28(
3841
+ ) }) : /* @__PURE__ */ jsx35("div", { className: ["cv-p-root__screen", classNames?.thread].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx35(
2801
3842
  ParticipantThreadScreen,
2802
3843
  {
2803
3844
  ...props,
@@ -2812,7 +3853,7 @@ function ParticipantConversationsScreen(props) {
2812
3853
  }
2813
3854
 
2814
3855
  // src/participant/useParticipantInbox.ts
2815
- import { useCallback as useCallback8, useEffect as useEffect14, useReducer as useReducer7 } from "react";
3856
+ import { useCallback as useCallback13, useEffect as useEffect21, useReducer as useReducer7 } from "react";
2816
3857
 
2817
3858
  // src/participant/participantInboxState.ts
2818
3859
  var INITIAL_PARTICIPANT_INBOX_STATE = { status: "idle", conversations: [] };
@@ -2863,7 +3904,7 @@ function describeError2(error) {
2863
3904
  }
2864
3905
  function useParticipantInbox(api) {
2865
3906
  const [state, dispatch] = useReducer7(participantInboxReducer, INITIAL_PARTICIPANT_INBOX_STATE);
2866
- const refresh = useCallback8(async () => {
3907
+ const refresh = useCallback13(async () => {
2867
3908
  dispatch({ type: "started" });
2868
3909
  try {
2869
3910
  const page = await api.listConversations();
@@ -2873,7 +3914,7 @@ function useParticipantInbox(api) {
2873
3914
  }
2874
3915
  }, [api]);
2875
3916
  const { nextCursor } = state;
2876
- const loadMore = useCallback8(async () => {
3917
+ const loadMore = useCallback13(async () => {
2877
3918
  if (!nextCursor) return;
2878
3919
  try {
2879
3920
  const page = await api.listConversations({ cursor: nextCursor });
@@ -2882,10 +3923,10 @@ function useParticipantInbox(api) {
2882
3923
  dispatch({ type: "failed", error: describeError2(error) });
2883
3924
  }
2884
3925
  }, [api, nextCursor]);
2885
- const markSubjectRead = useCallback8((subject) => {
3926
+ const markSubjectRead = useCallback13((subject) => {
2886
3927
  dispatch({ type: "markedRead", subject });
2887
3928
  }, []);
2888
- useEffect14(() => {
3929
+ useEffect21(() => {
2889
3930
  void refresh();
2890
3931
  }, [refresh]);
2891
3932
  useParticipantRevalidation(api, () => {
@@ -2895,11 +3936,11 @@ function useParticipantInbox(api) {
2895
3936
  }
2896
3937
 
2897
3938
  // src/participant/ParticipantConversations.tsx
2898
- import { jsx as jsx29 } from "react/jsx-runtime";
3939
+ import { jsx as jsx36 } from "react/jsx-runtime";
2899
3940
  function ParticipantConversations(props) {
2900
3941
  const inbox = useParticipantInbox(props.api);
2901
3942
  const { status, conversations, hasMore, refresh, loadMore, markSubjectRead } = inbox;
2902
- return /* @__PURE__ */ jsx29(
3943
+ return /* @__PURE__ */ jsx36(
2903
3944
  ParticipantConversationsScreen,
2904
3945
  {
2905
3946
  ...props,
@@ -2909,7 +3950,7 @@ function ParticipantConversations(props) {
2909
3950
  }
2910
3951
 
2911
3952
  // src/participant/ConversationThread.tsx
2912
- import { useCallback as useCallback9, useMemo as useMemo6, useReducer as useReducer8, useRef as useRef13 } from "react";
3953
+ import { useCallback as useCallback14, useMemo as useMemo7, useReducer as useReducer8, useRef as useRef22 } from "react";
2913
3954
 
2914
3955
  // src/participant/conversationThreadApi.ts
2915
3956
  function toParticipantApi(api) {
@@ -2949,20 +3990,20 @@ function buildThreadSummary({
2949
3990
  }
2950
3991
 
2951
3992
  // src/participant/ConversationThread.tsx
2952
- import { jsx as jsx30 } from "react/jsx-runtime";
3993
+ import { jsx as jsx37 } from "react/jsx-runtime";
2953
3994
  function useConversationThreadCore(props) {
2954
3995
  const { perspective = "participant" } = props;
2955
3996
  const { subjectType, subjectId } = props.subject;
2956
- const subject = useMemo6(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
2957
- const onMarkedReadRef = useRef13(props.onMarkedRead);
3997
+ const subject = useMemo7(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
3998
+ const onMarkedReadRef = useRef22(props.onMarkedRead);
2958
3999
  onMarkedReadRef.current = props.onMarkedRead;
2959
- const handleMarkedRead = useCallback9((marked) => onMarkedReadRef.current?.(marked), []);
2960
- const api = useMemo6(() => toParticipantApi(props.api), [props.api]);
2961
- const draftsRef = useRef13(/* @__PURE__ */ new Map());
4000
+ const handleMarkedRead = useCallback14((marked) => onMarkedReadRef.current?.(marked), []);
4001
+ const api = useMemo7(() => toParticipantApi(props.api), [props.api]);
4002
+ const draftsRef = useRef22(/* @__PURE__ */ new Map());
2962
4003
  const [sendStates, dispatchSendStates] = useReducer8(participantSendStatesReducer, /* @__PURE__ */ new Map());
2963
- const labels = useMemo6(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
4004
+ const labels = useMemo7(() => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }), [props.labels]);
2964
4005
  const { title, protocol, channels, status } = props;
2965
- const conversation = useMemo6(
4006
+ const conversation = useMemo7(
2966
4007
  () => buildThreadSummary({ subject, title, protocol, channels, status }),
2967
4008
  [subject, title, protocol, channels, status]
2968
4009
  );
@@ -2984,12 +4025,12 @@ function useConversationThreadCore(props) {
2984
4025
  }
2985
4026
  function ConversationThreadBody(props) {
2986
4027
  const core = useConversationThreadCore(props);
2987
- return /* @__PURE__ */ jsx30(
4028
+ return /* @__PURE__ */ jsx37(
2988
4029
  "div",
2989
4030
  {
2990
4031
  className: ["cv-p-root", props.className].filter(Boolean).join(" "),
2991
4032
  style: buildParticipantThemeStyle(props.theme),
2992
- children: /* @__PURE__ */ jsx30("div", { className: "cv-p-root__screen", children: /* @__PURE__ */ jsx30(
4033
+ children: /* @__PURE__ */ jsx37("div", { className: "cv-p-root__screen", children: /* @__PURE__ */ jsx37(
2993
4034
  ParticipantThread,
2994
4035
  {
2995
4036
  ...core,
@@ -3001,17 +4042,20 @@ function ConversationThreadBody(props) {
3001
4042
  renderAuthorAvatar: props.renderAuthorAvatar,
3002
4043
  tail: props.tail,
3003
4044
  counterpartLabel: props.counterpartLabel,
3004
- headerActions: props.headerActions
4045
+ headerActions: props.headerActions,
4046
+ audioRecording: props.audioRecording
3005
4047
  }
3006
4048
  ) })
3007
4049
  }
3008
4050
  );
3009
4051
  }
3010
4052
  function ConversationThread(props) {
3011
- return /* @__PURE__ */ jsx30(ConversationThreadBody, { ...props }, draftKey(props.subject));
4053
+ return /* @__PURE__ */ jsx37(ConversationThreadBody, { ...props }, draftKey(props.subject));
3012
4054
  }
3013
4055
  export {
3014
4056
  ConversationThread,
4057
+ DEFAULT_MAX_RECORDING_BYTES,
4058
+ DEFAULT_PARTICIPANT_AUDIO_LABELS,
3015
4059
  DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS,
3016
4060
  ParticipantConversations,
3017
4061
  groupParticipantConversations