@copilotz/chat-ui 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -88,17 +88,20 @@ var defaultChatConfig = {
88
88
  recordAudioTooltip: "Record audio",
89
89
  voiceEnter: "Voice input",
90
90
  voiceExit: "Use keyboard",
91
- voiceTitle: "Voice input",
91
+ voiceTitle: "Voice",
92
+ voiceIdle: "Tap the mic to record",
92
93
  voicePreparing: "Preparing microphone...",
93
94
  voiceWaiting: "Waiting for speech...",
94
95
  voiceListening: "Listening...",
95
96
  voiceFinishing: "Finishing capture...",
96
97
  voiceReview: "Ready to send",
98
+ voiceSending: "Sending...",
97
99
  voiceStart: "Start recording",
98
100
  voiceStop: "Stop recording",
99
101
  voiceSendNow: "Send now",
100
102
  voiceCancel: "Cancel",
101
- voiceRecordAgain: "Record again",
103
+ voiceDiscard: "Delete recording",
104
+ voiceRecordAgain: "Continue recording",
102
105
  voiceAutoSendIn: "Auto-sends in {{seconds}}s",
103
106
  voiceTranscriptPending: "Transcript unavailable",
104
107
  voicePermissionDenied: "Microphone access was denied.",
@@ -167,6 +170,7 @@ var defaultChatConfig = {
167
170
  },
168
171
  voiceCompose: {
169
172
  enabled: false,
173
+ defaultMode: "text",
170
174
  autoSendDelayMs: 5e3,
171
175
  persistComposer: true,
172
176
  showTranscriptPreview: true,
@@ -2862,6 +2866,121 @@ var blobToDataUrl = (blob) => new Promise((resolve, reject) => {
2862
2866
  reader.onerror = () => reject(reader.error ?? new Error("Failed to read recorded audio"));
2863
2867
  reader.readAsDataURL(blob);
2864
2868
  });
2869
+ var joinTranscriptParts = (...parts) => {
2870
+ const value = parts.map((part) => part?.trim()).filter((part) => Boolean(part && part.length > 0)).join(" ").trim();
2871
+ return value.length > 0 ? value : void 0;
2872
+ };
2873
+ var getAudioContextCtor = () => globalThis.AudioContext || globalThis.webkitAudioContext;
2874
+ var getOfflineAudioContextCtor = () => globalThis.OfflineAudioContext || globalThis.webkitOfflineAudioContext;
2875
+ var attachmentToArrayBuffer = async (attachment) => {
2876
+ const response = await fetch(attachment.dataUrl);
2877
+ return response.arrayBuffer();
2878
+ };
2879
+ var decodeAudioAttachment = async (attachment) => {
2880
+ const AudioContextCtor = getAudioContextCtor();
2881
+ if (!AudioContextCtor) {
2882
+ throw new Error("Audio decoding is not supported in this browser");
2883
+ }
2884
+ const audioContext = new AudioContextCtor();
2885
+ try {
2886
+ const arrayBuffer = await attachmentToArrayBuffer(attachment);
2887
+ return await audioContext.decodeAudioData(arrayBuffer.slice(0));
2888
+ } finally {
2889
+ await closeAudioContext(audioContext);
2890
+ }
2891
+ };
2892
+ var renderMergedBuffer = async (buffers) => {
2893
+ const OfflineAudioContextCtor = getOfflineAudioContextCtor();
2894
+ if (!OfflineAudioContextCtor) {
2895
+ throw new Error("Offline audio rendering is not supported in this browser");
2896
+ }
2897
+ const numberOfChannels = Math.max(...buffers.map((buffer) => buffer.numberOfChannels));
2898
+ const sampleRate = Math.max(...buffers.map((buffer) => buffer.sampleRate));
2899
+ const totalFrames = Math.max(1, Math.ceil(buffers.reduce((sum, buffer) => sum + buffer.duration * sampleRate, 0)));
2900
+ const offlineContext = new OfflineAudioContextCtor(numberOfChannels, totalFrames, sampleRate);
2901
+ let offsetSeconds = 0;
2902
+ for (const buffer of buffers) {
2903
+ const source = offlineContext.createBufferSource();
2904
+ source.buffer = buffer;
2905
+ source.connect(offlineContext.destination);
2906
+ source.start(offsetSeconds);
2907
+ offsetSeconds += buffer.duration;
2908
+ }
2909
+ return offlineContext.startRendering();
2910
+ };
2911
+ var encodeWav = (audioBuffer) => {
2912
+ const numberOfChannels = audioBuffer.numberOfChannels;
2913
+ const sampleRate = audioBuffer.sampleRate;
2914
+ const bitsPerSample = 16;
2915
+ const bytesPerSample = bitsPerSample / 8;
2916
+ const dataLength = audioBuffer.length * numberOfChannels * bytesPerSample;
2917
+ const buffer = new ArrayBuffer(44 + dataLength);
2918
+ const view = new DataView(buffer);
2919
+ const writeString = (offset2, value) => {
2920
+ for (let index = 0; index < value.length; index += 1) {
2921
+ view.setUint8(offset2 + index, value.charCodeAt(index));
2922
+ }
2923
+ };
2924
+ writeString(0, "RIFF");
2925
+ view.setUint32(4, 36 + dataLength, true);
2926
+ writeString(8, "WAVE");
2927
+ writeString(12, "fmt ");
2928
+ view.setUint32(16, 16, true);
2929
+ view.setUint16(20, 1, true);
2930
+ view.setUint16(22, numberOfChannels, true);
2931
+ view.setUint32(24, sampleRate, true);
2932
+ view.setUint32(28, sampleRate * numberOfChannels * bytesPerSample, true);
2933
+ view.setUint16(32, numberOfChannels * bytesPerSample, true);
2934
+ view.setUint16(34, bitsPerSample, true);
2935
+ writeString(36, "data");
2936
+ view.setUint32(40, dataLength, true);
2937
+ let offset = 44;
2938
+ const channelData = Array.from({ length: numberOfChannels }, (_, index) => audioBuffer.getChannelData(index));
2939
+ for (let sampleIndex = 0; sampleIndex < audioBuffer.length; sampleIndex += 1) {
2940
+ for (let channelIndex = 0; channelIndex < numberOfChannels; channelIndex += 1) {
2941
+ const sample = Math.max(-1, Math.min(1, channelData[channelIndex][sampleIndex]));
2942
+ const pcmValue = sample < 0 ? sample * 32768 : sample * 32767;
2943
+ view.setInt16(offset, pcmValue, true);
2944
+ offset += 2;
2945
+ }
2946
+ }
2947
+ return new Blob([buffer], { type: "audio/wav" });
2948
+ };
2949
+ var resolveSegmentCount = (segment) => {
2950
+ const candidate = segment?.metadata?.segmentCount;
2951
+ return typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0 ? candidate : segment ? 1 : 0;
2952
+ };
2953
+ var mergeVoiceTranscripts = (previous, incoming) => ({
2954
+ final: joinTranscriptParts(previous?.final, incoming?.final),
2955
+ partial: joinTranscriptParts(previous?.final, incoming?.partial)
2956
+ });
2957
+ var appendVoiceSegments = async (previous, incoming) => {
2958
+ const [previousBuffer, incomingBuffer] = await Promise.all([
2959
+ decodeAudioAttachment(previous.attachment),
2960
+ decodeAudioAttachment(incoming.attachment)
2961
+ ]);
2962
+ const mergedBuffer = await renderMergedBuffer([previousBuffer, incomingBuffer]);
2963
+ const mergedBlob = encodeWav(mergedBuffer);
2964
+ const dataUrl = await blobToDataUrl(mergedBlob);
2965
+ const segmentCount = resolveSegmentCount(previous) + resolveSegmentCount(incoming);
2966
+ return {
2967
+ attachment: {
2968
+ kind: "audio",
2969
+ dataUrl,
2970
+ mimeType: mergedBlob.type,
2971
+ durationMs: Math.round(mergedBuffer.duration * 1e3),
2972
+ fileName: `voice-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.wav`,
2973
+ size: mergedBlob.size
2974
+ },
2975
+ transcript: mergeVoiceTranscripts(previous.transcript, incoming.transcript),
2976
+ metadata: {
2977
+ ...previous.metadata,
2978
+ ...incoming.metadata,
2979
+ segmentCount,
2980
+ source: segmentCount > 1 ? "merged" : incoming.metadata?.source ?? previous.metadata?.source
2981
+ }
2982
+ };
2983
+ };
2865
2984
  var stopStream = (stream) => {
2866
2985
  if (!stream) return;
2867
2986
  stream.getTracks().forEach((track) => track.stop());
@@ -2983,7 +3102,7 @@ var createManualVoiceProvider = async (handlers, options = {}) => {
2983
3102
  fileName: `voice-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.webm`,
2984
3103
  size: blob.size
2985
3104
  },
2986
- metadata: { source: "manual" }
3105
+ metadata: { source: "manual", segmentCount: 1 }
2987
3106
  });
2988
3107
  } else {
2989
3108
  handlers.onStateChange?.("idle");
@@ -3109,12 +3228,12 @@ var resolveStateLabel = (state, labels, errorMessage) => {
3109
3228
  case "review":
3110
3229
  return labels?.voiceReview || "Ready to send";
3111
3230
  case "sending":
3112
- return "Sending...";
3231
+ return labels?.voiceSending || "Sending...";
3113
3232
  case "error":
3114
3233
  return errorMessage || labels?.voiceCaptureError || "Unable to capture audio.";
3115
3234
  case "idle":
3116
3235
  default:
3117
- return labels?.voiceTitle || "Voice input";
3236
+ return labels?.voiceIdle || "Tap the mic to record";
3118
3237
  }
3119
3238
  };
3120
3239
  var resolveTranscriptText = (transcript, transcriptMode) => {
@@ -3131,18 +3250,21 @@ var VoiceComposer = ({
3131
3250
  transcript,
3132
3251
  transcriptMode,
3133
3252
  showTranscriptPreview,
3253
+ attachment,
3134
3254
  durationMs,
3135
3255
  audioLevel,
3136
3256
  countdownMs,
3137
3257
  autoSendDelayMs,
3258
+ isAutoSendActive,
3138
3259
  errorMessage,
3139
3260
  disabled = false,
3140
3261
  labels,
3141
3262
  onStart,
3142
3263
  onStop,
3143
- onCancel,
3144
- onSendNow,
3264
+ onCancelAutoSend,
3265
+ onDiscard,
3145
3266
  onRecordAgain,
3267
+ onSendNow,
3146
3268
  onExit
3147
3269
  }) => {
3148
3270
  const transcriptText = resolveTranscriptText(transcript, transcriptMode);
@@ -3150,12 +3272,14 @@ var VoiceComposer = ({
3150
3272
  const countdownValue = autoSendDelayMs > 0 ? Math.min(100, Math.max(0, (autoSendDelayMs - countdownMs) / autoSendDelayMs * 100)) : 100;
3151
3273
  const isBusy = state === "preparing" || state === "finishing" || state === "sending";
3152
3274
  const isCapturing = state === "waiting_for_speech" || state === "listening";
3275
+ const isReviewing = state === "review";
3153
3276
  const levelValue = isCapturing || state === "preparing" || state === "finishing" ? Math.max(8, Math.round(audioLevel * 100)) : 0;
3154
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "w-full md:min-w-3xl max-w-3xl rounded-xl border bg-background p-4 shadow-sm", children: [
3155
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center justify-between gap-3", children: [
3156
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-2", children: [
3157
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: "outline", children: labels?.voiceTitle || "Voice input" }),
3158
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-sm text-muted-foreground", children: resolveStateLabel(state, labels, errorMessage) })
3277
+ const headerLabel = state === "error" ? labels?.voiceCaptureError || "Unable to capture audio." : resolveStateLabel(state, labels, errorMessage);
3278
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "w-full max-w-3xl rounded-xl border bg-background p-3 shadow-sm sm:p-4 md:min-w-3xl", children: [
3279
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center justify-between gap-2 sm:gap-3", children: [
3280
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
3281
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: "outline", children: labels?.voiceTitle || "Voice" }),
3282
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "truncate text-xs sm:text-sm text-muted-foreground", children: headerLabel })
3159
3283
  ] }),
3160
3284
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
3161
3285
  Button,
@@ -3163,71 +3287,89 @@ var VoiceComposer = ({
3163
3287
  type: "button",
3164
3288
  variant: "ghost",
3165
3289
  size: "sm",
3290
+ className: "shrink-0 px-2 sm:px-3",
3166
3291
  onClick: onExit,
3167
3292
  disabled: disabled || isBusy,
3168
3293
  children: [
3169
3294
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Keyboard, { className: "h-4 w-4" }),
3170
- labels?.voiceExit || "Use keyboard"
3295
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "hidden sm:inline", children: labels?.voiceExit || "Use keyboard" })
3171
3296
  ]
3172
3297
  }
3173
3298
  )
3174
3299
  ] }),
3175
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-4 flex flex-col items-center gap-4 rounded-xl border border-dashed border-primary/30 bg-primary/5 px-4 py-6 text-center", children: [
3176
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "flex h-20 w-20 items-center justify-center rounded-full bg-primary/10", children: isBusy ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Loader2, { className: "h-8 w-8 animate-spin text-primary" }) : isCapturing ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Square, { className: "h-8 w-8 text-primary" }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Mic, { className: "h-8 w-8 text-primary" }) }),
3177
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "w-full max-w-md space-y-2", children: [
3300
+ !isReviewing ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "mt-3 rounded-xl border border-dashed border-primary/30 bg-primary/5 px-3 py-3 text-center sm:px-4 sm:py-4", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mx-auto flex w-full max-w-sm flex-col items-center gap-3", children: [
3301
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3302
+ Button,
3303
+ {
3304
+ type: "button",
3305
+ size: "icon",
3306
+ variant: isCapturing ? "destructive" : "outline",
3307
+ className: `h-16 w-16 rounded-full sm:h-20 sm:w-20 ${isCapturing ? "bg-red-500 hover:bg-red-600 text-white border-red-500" : "border-red-200 bg-red-50 text-red-600 hover:bg-red-100 hover:text-red-700"}`,
3308
+ onClick: isCapturing ? onStop : onStart,
3309
+ disabled: disabled || isBusy,
3310
+ children: isBusy ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Loader2, { className: "h-7 w-7 animate-spin" }) : isCapturing ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Square, { className: "h-7 w-7" }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Mic, { className: "h-7 w-7" })
3311
+ }
3312
+ ),
3313
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "w-full space-y-2", children: [
3178
3314
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Progress, { value: levelValue, className: "h-2" }),
3179
3315
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [
3180
3316
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: formatDuration(durationMs) }),
3181
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: resolveStateLabel(state, labels, errorMessage) })
3317
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: isCapturing ? labels?.voiceStop || "Stop recording" : labels?.voiceStart || "Start recording" })
3182
3318
  ] })
3183
3319
  ] }),
3184
- showTranscriptPreview && transcriptMode !== "none" && transcriptText && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "w-full max-w-md rounded-lg border bg-background px-3 py-2 text-left text-sm", children: transcriptText })
3185
- ] }),
3186
- state === "review" && autoSendDelayMs > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-4 space-y-2", children: [
3187
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Progress, { value: countdownValue, className: "h-2" }),
3188
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-center text-xs text-muted-foreground", children: interpolateSeconds(labels?.voiceAutoSendIn, countdownSeconds) })
3189
- ] }),
3190
- state === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "mt-4 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive", children: errorMessage }),
3191
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-4 flex flex-wrap items-center justify-center gap-2", children: [
3192
- state === "idle" && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", onClick: onStart, disabled, children: [
3193
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Mic, { className: "h-4 w-4" }),
3194
- labels?.voiceStart || "Start recording"
3195
- ] }),
3196
- isCapturing && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
3197
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", onClick: onStop, disabled, children: [
3198
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Square, { className: "h-4 w-4" }),
3199
- labels?.voiceStop || "Stop recording"
3320
+ showTranscriptPreview && transcriptMode !== "none" && transcriptText && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "w-full rounded-lg border bg-background px-3 py-2 text-left text-sm", children: transcriptText })
3321
+ ] }) }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-3 rounded-xl border bg-muted/20 p-3 sm:p-4", children: [
3322
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [
3323
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "min-w-0", children: [
3324
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-sm font-medium text-foreground", children: labels?.voiceReview || "Ready to send" }),
3325
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-xs text-muted-foreground", children: formatDuration(durationMs) })
3200
3326
  ] }),
3201
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", variant: "outline", onClick: onCancel, disabled, children: [
3202
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.X, { className: "h-4 w-4" }),
3203
- labels?.voiceCancel || "Cancel"
3204
- ] })
3327
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3328
+ Button,
3329
+ {
3330
+ type: "button",
3331
+ variant: "ghost",
3332
+ size: "icon",
3333
+ className: "h-8 w-8 shrink-0 text-muted-foreground hover:text-destructive",
3334
+ onClick: onDiscard,
3335
+ disabled,
3336
+ "aria-label": labels?.voiceDiscard || "Delete recording",
3337
+ title: labels?.voiceDiscard || "Delete recording",
3338
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Trash2, { className: "h-4 w-4" })
3339
+ }
3340
+ )
3205
3341
  ] }),
3206
- state === "review" && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
3207
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", variant: "outline", onClick: onCancel, disabled, children: [
3342
+ attachment && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "mt-3 rounded-lg bg-background p-2", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("audio", { controls: true, preload: "metadata", className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("source", { src: attachment.dataUrl, type: attachment.mimeType }) }) }),
3343
+ showTranscriptPreview && transcriptMode !== "none" && transcriptText && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "mt-3 rounded-lg border bg-background px-3 py-2 text-left text-sm", children: transcriptText }),
3344
+ isAutoSendActive && autoSendDelayMs > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-3 space-y-2", children: [
3345
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Progress, { value: countdownValue, className: "h-2" }),
3346
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-center text-xs text-muted-foreground", children: interpolateSeconds(labels?.voiceAutoSendIn, countdownSeconds) })
3347
+ ] }),
3348
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-3 flex items-center justify-end gap-2", children: [
3349
+ isAutoSendActive && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", variant: "ghost", size: "sm", onClick: onCancelAutoSend, disabled, children: [
3208
3350
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.X, { className: "h-4 w-4" }),
3209
3351
  labels?.voiceCancel || "Cancel"
3210
3352
  ] }),
3211
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", variant: "outline", onClick: onRecordAgain, disabled, children: [
3212
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.RotateCcw, { className: "h-4 w-4" }),
3213
- labels?.voiceRecordAgain || "Record again"
3214
- ] }),
3215
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", onClick: onSendNow, disabled, children: [
3353
+ !isAutoSendActive && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3354
+ Button,
3355
+ {
3356
+ type: "button",
3357
+ variant: "outline",
3358
+ size: "icon",
3359
+ onClick: onRecordAgain,
3360
+ disabled,
3361
+ "aria-label": labels?.voiceRecordAgain || "Record again",
3362
+ title: labels?.voiceRecordAgain || "Record again",
3363
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Mic, { className: "h-4 w-4" })
3364
+ }
3365
+ ),
3366
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", size: "sm", onClick: onSendNow, disabled, children: [
3216
3367
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Send, { className: "h-4 w-4" }),
3217
3368
  labels?.voiceSendNow || "Send now"
3218
3369
  ] })
3219
- ] }),
3220
- state === "error" && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
3221
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", variant: "outline", onClick: onCancel, disabled, children: [
3222
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.X, { className: "h-4 w-4" }),
3223
- labels?.voiceCancel || "Cancel"
3224
- ] }),
3225
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", onClick: onRecordAgain, disabled, children: [
3226
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.RotateCcw, { className: "h-4 w-4" }),
3227
- labels?.voiceRecordAgain || "Record again"
3228
- ] })
3229
3370
  ] })
3230
- ] })
3371
+ ] }),
3372
+ errorMessage && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "mt-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive", children: errorMessage })
3231
3373
  ] });
3232
3374
  };
3233
3375
 
@@ -3334,7 +3476,7 @@ var AttachmentPreview = (0, import_react5.memo)(function AttachmentPreview2({ at
3334
3476
  "img",
3335
3477
  {
3336
3478
  src: attachment.dataUrl,
3337
- alt: attachment.fileName || "Anexo",
3479
+ alt: attachment.fileName || "Attachment",
3338
3480
  className: "w-full h-20 object-cover rounded"
3339
3481
  }
3340
3482
  ),
@@ -3383,7 +3525,7 @@ var AttachmentPreview = (0, import_react5.memo)(function AttachmentPreview2({ at
3383
3525
  }
3384
3526
  ),
3385
3527
  /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex-1", children: [
3386
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "text-xs font-medium", children: attachment.fileName || "\xC1udio" }),
3528
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "text-xs font-medium", children: attachment.fileName || "Audio" }),
3387
3529
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "text-xs text-muted-foreground", children: formatDuration2(attachment.durationMs) })
3388
3530
  ] }),
3389
3531
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
@@ -3435,7 +3577,7 @@ var AudioRecorder = (0, import_react5.memo)(function AudioRecorder2({ isRecordin
3435
3577
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Card, { className: "border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CardContent, { className: "p-3", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-3", children: [
3436
3578
  /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-2", children: [
3437
3579
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "h-3 w-3 bg-red-500 rounded-full animate-pulse" }),
3438
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-sm font-medium text-red-700 dark:text-red-300", children: "Gravando" })
3580
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-sm font-medium text-red-700 dark:text-red-300", children: config?.labels?.voiceListening || "Recording" })
3439
3581
  ] }),
3440
3582
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: "outline", className: "text-xs", children: formatTime(recordingDuration) }),
3441
3583
  /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex gap-1 ml-auto", children: [
@@ -3447,7 +3589,7 @@ var AudioRecorder = (0, import_react5.memo)(function AudioRecorder2({ isRecordin
3447
3589
  onClick: onCancel,
3448
3590
  children: [
3449
3591
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react10.X, { className: "h-3 w-3 mr-1" }),
3450
- "Cancelar"
3592
+ config?.labels?.cancel || "Cancel"
3451
3593
  ]
3452
3594
  }
3453
3595
  ),
@@ -3459,7 +3601,7 @@ var AudioRecorder = (0, import_react5.memo)(function AudioRecorder2({ isRecordin
3459
3601
  onClick: onStopRecording,
3460
3602
  children: [
3461
3603
  /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react10.Square, { className: "h-3 w-3 mr-1" }),
3462
- "Parar"
3604
+ config?.labels?.voiceStop || "Stop"
3463
3605
  ]
3464
3606
  }
3465
3607
  )
@@ -3476,13 +3618,14 @@ var resolveVoiceErrorMessage = (error, config) => {
3476
3618
  return config?.labels?.voiceCaptureError || "Unable to capture audio.";
3477
3619
  };
3478
3620
  var clearVoiceTranscript = () => ({});
3621
+ var resolveVoiceSegmentDuration = (segment) => segment.attachment.durationMs ?? 0;
3479
3622
  var ChatInput = (0, import_react5.memo)(function ChatInput2({
3480
3623
  value,
3481
3624
  onChange,
3482
3625
  onSubmit,
3483
3626
  attachments,
3484
3627
  onAttachmentsChange,
3485
- placeholder = "Digite sua mensagem...",
3628
+ placeholder = "Type your message...",
3486
3629
  disabled = false,
3487
3630
  isGenerating = false,
3488
3631
  onStopGeneration,
@@ -3495,17 +3638,27 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3495
3638
  className = "",
3496
3639
  config
3497
3640
  }) {
3641
+ const voiceComposeEnabled = config?.voiceCompose?.enabled === true;
3642
+ const voiceDefaultMode = config?.voiceCompose?.defaultMode ?? "text";
3643
+ const voiceAutoSendDelayMs = config?.voiceCompose?.autoSendDelayMs ?? 5e3;
3644
+ const voicePersistComposer = config?.voiceCompose?.persistComposer ?? true;
3645
+ const voiceShowTranscriptPreview = config?.voiceCompose?.showTranscriptPreview ?? true;
3646
+ const voiceTranscriptMode = config?.voiceCompose?.transcriptMode ?? "final-only";
3647
+ const voiceMaxRecordingMs = config?.voiceCompose?.maxRecordingMs;
3498
3648
  const [isRecording, setIsRecording] = (0, import_react5.useState)(false);
3499
3649
  const { setContext } = useChatUserContext();
3500
3650
  const [recordingDuration, setRecordingDuration] = (0, import_react5.useState)(0);
3501
3651
  const [uploadProgress, setUploadProgress] = (0, import_react5.useState)(/* @__PURE__ */ new Map());
3502
- const [isVoiceComposerOpen, setIsVoiceComposerOpen] = (0, import_react5.useState)(false);
3652
+ const [isVoiceComposerOpen, setIsVoiceComposerOpen] = (0, import_react5.useState)(
3653
+ () => voiceComposeEnabled && voiceDefaultMode === "voice"
3654
+ );
3503
3655
  const [voiceState, setVoiceState] = (0, import_react5.useState)("idle");
3504
3656
  const [voiceDraft, setVoiceDraft] = (0, import_react5.useState)(null);
3505
3657
  const [voiceTranscript, setVoiceTranscript] = (0, import_react5.useState)(clearVoiceTranscript);
3506
3658
  const [voiceDurationMs, setVoiceDurationMs] = (0, import_react5.useState)(0);
3507
3659
  const [voiceAudioLevel, setVoiceAudioLevel] = (0, import_react5.useState)(0);
3508
3660
  const [voiceCountdownMs, setVoiceCountdownMs] = (0, import_react5.useState)(0);
3661
+ const [isVoiceAutoSendActive, setIsVoiceAutoSendActive] = (0, import_react5.useState)(false);
3509
3662
  const [voiceError, setVoiceError] = (0, import_react5.useState)(null);
3510
3663
  const textareaRef = (0, import_react5.useRef)(null);
3511
3664
  const fileInputRef = (0, import_react5.useRef)(null);
@@ -3514,12 +3667,9 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3514
3667
  const recordingInterval = (0, import_react5.useRef)(null);
3515
3668
  const mediaStreamRef = (0, import_react5.useRef)(null);
3516
3669
  const voiceProviderRef = (0, import_react5.useRef)(null);
3517
- const voiceComposeEnabled = config?.voiceCompose?.enabled === true;
3518
- const voiceAutoSendDelayMs = config?.voiceCompose?.autoSendDelayMs ?? 5e3;
3519
- const voicePersistComposer = config?.voiceCompose?.persistComposer ?? true;
3520
- const voiceShowTranscriptPreview = config?.voiceCompose?.showTranscriptPreview ?? true;
3521
- const voiceTranscriptMode = config?.voiceCompose?.transcriptMode ?? "final-only";
3522
- const voiceMaxRecordingMs = config?.voiceCompose?.maxRecordingMs;
3670
+ const voiceDraftRef = (0, import_react5.useRef)(null);
3671
+ const voiceAppendBaseRef = (0, import_react5.useRef)(null);
3672
+ const voiceAppendBaseDurationRef = (0, import_react5.useRef)(0);
3523
3673
  (0, import_react5.useEffect)(() => {
3524
3674
  return () => {
3525
3675
  if (mediaStreamRef.current) {
@@ -3534,6 +3684,9 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3534
3684
  }
3535
3685
  };
3536
3686
  }, []);
3687
+ (0, import_react5.useEffect)(() => {
3688
+ voiceDraftRef.current = voiceDraft;
3689
+ }, [voiceDraft]);
3537
3690
  const handleSubmit = (e) => {
3538
3691
  e.preventDefault();
3539
3692
  if (!value.trim() && attachments.length === 0 || disabled || isGenerating) return;
@@ -3549,7 +3702,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3549
3702
  };
3550
3703
  const processFile = async (file) => {
3551
3704
  if (file.size > maxFileSize) {
3552
- alert(`Arquivo muito grande. M\xE1ximo permitido: ${Math.round(maxFileSize / 1024 / 1024)}MB`);
3705
+ alert(`File too large. Max allowed: ${Math.round(maxFileSize / 1024 / 1024)}MB`);
3553
3706
  return null;
3554
3707
  }
3555
3708
  const fileId = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
@@ -3608,7 +3761,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3608
3761
  newMap.delete(fileId);
3609
3762
  return newMap;
3610
3763
  });
3611
- alert("Erro ao processar arquivo");
3764
+ alert("Failed to process file");
3612
3765
  return null;
3613
3766
  }
3614
3767
  };
@@ -3683,7 +3836,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3683
3836
  }, 1e3);
3684
3837
  } catch (error) {
3685
3838
  console.error("Error starting recording:", error);
3686
- alert("N\xE3o foi poss\xEDvel acessar o microfone");
3839
+ alert(config?.labels?.voicePermissionDenied || "Microphone access was denied.");
3687
3840
  }
3688
3841
  };
3689
3842
  const stopRecording = () => {
@@ -3711,10 +3864,14 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3711
3864
  const resetVoiceComposerState = (0, import_react5.useCallback)((nextState = "idle") => {
3712
3865
  setVoiceState(nextState);
3713
3866
  setVoiceDraft(null);
3867
+ voiceDraftRef.current = null;
3868
+ voiceAppendBaseRef.current = null;
3869
+ voiceAppendBaseDurationRef.current = 0;
3714
3870
  setVoiceTranscript(clearVoiceTranscript());
3715
3871
  setVoiceDurationMs(0);
3716
3872
  setVoiceAudioLevel(0);
3717
3873
  setVoiceCountdownMs(0);
3874
+ setIsVoiceAutoSendActive(false);
3718
3875
  setVoiceError(null);
3719
3876
  }, []);
3720
3877
  const ensureVoiceProvider = (0, import_react5.useCallback)(async () => {
@@ -3725,21 +3882,76 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3725
3882
  const provider = await createProvider({
3726
3883
  onStateChange: setVoiceState,
3727
3884
  onAudioLevelChange: setVoiceAudioLevel,
3728
- onDurationChange: setVoiceDurationMs,
3729
- onTranscriptChange: setVoiceTranscript,
3885
+ onDurationChange: (durationMs) => {
3886
+ setVoiceDurationMs(voiceAppendBaseDurationRef.current + durationMs);
3887
+ },
3888
+ onTranscriptChange: (transcript) => {
3889
+ const baseTranscript = voiceAppendBaseRef.current?.transcript;
3890
+ setVoiceTranscript(
3891
+ baseTranscript ? mergeVoiceTranscripts(baseTranscript, transcript) : transcript
3892
+ );
3893
+ },
3730
3894
  onSegmentReady: (segment) => {
3731
- setVoiceDraft(segment);
3732
- setVoiceTranscript(segment.transcript ?? clearVoiceTranscript());
3733
- setVoiceDurationMs(segment.attachment.durationMs ?? 0);
3734
- setVoiceAudioLevel(0);
3735
- setVoiceCountdownMs(voiceAutoSendDelayMs);
3736
- setVoiceError(null);
3737
- setVoiceState("review");
3895
+ void (async () => {
3896
+ const previousSegment = voiceAppendBaseRef.current;
3897
+ try {
3898
+ const nextSegment = previousSegment ? await appendVoiceSegments(previousSegment, segment) : segment;
3899
+ voiceAppendBaseRef.current = null;
3900
+ voiceAppendBaseDurationRef.current = 0;
3901
+ voiceDraftRef.current = nextSegment;
3902
+ setVoiceDraft(nextSegment);
3903
+ setVoiceTranscript(nextSegment.transcript ?? clearVoiceTranscript());
3904
+ setVoiceDurationMs(resolveVoiceSegmentDuration(nextSegment));
3905
+ setVoiceAudioLevel(0);
3906
+ setVoiceCountdownMs(voiceAutoSendDelayMs);
3907
+ setIsVoiceAutoSendActive(voiceAutoSendDelayMs > 0);
3908
+ setVoiceError(null);
3909
+ setVoiceState("review");
3910
+ } catch (error) {
3911
+ const resolvedError = resolveVoiceErrorMessage(error, config);
3912
+ voiceAppendBaseRef.current = null;
3913
+ voiceAppendBaseDurationRef.current = 0;
3914
+ setVoiceAudioLevel(0);
3915
+ setVoiceCountdownMs(0);
3916
+ setIsVoiceAutoSendActive(false);
3917
+ if (previousSegment) {
3918
+ voiceDraftRef.current = previousSegment;
3919
+ setVoiceDraft(previousSegment);
3920
+ setVoiceTranscript(previousSegment.transcript ?? clearVoiceTranscript());
3921
+ setVoiceDurationMs(resolveVoiceSegmentDuration(previousSegment));
3922
+ setVoiceError(resolvedError);
3923
+ setVoiceState("review");
3924
+ return;
3925
+ }
3926
+ voiceDraftRef.current = null;
3927
+ setVoiceDraft(null);
3928
+ setVoiceTranscript(clearVoiceTranscript());
3929
+ setVoiceDurationMs(0);
3930
+ setVoiceError(resolvedError);
3931
+ setVoiceState("error");
3932
+ }
3933
+ })();
3738
3934
  },
3739
3935
  onError: (error) => {
3936
+ const previousSegment = voiceAppendBaseRef.current;
3937
+ voiceAppendBaseRef.current = null;
3938
+ voiceAppendBaseDurationRef.current = 0;
3740
3939
  setVoiceError(resolveVoiceErrorMessage(error, config));
3741
3940
  setVoiceAudioLevel(0);
3742
3941
  setVoiceCountdownMs(0);
3942
+ setIsVoiceAutoSendActive(false);
3943
+ if (previousSegment) {
3944
+ voiceDraftRef.current = previousSegment;
3945
+ setVoiceDraft(previousSegment);
3946
+ setVoiceTranscript(previousSegment.transcript ?? clearVoiceTranscript());
3947
+ setVoiceDurationMs(resolveVoiceSegmentDuration(previousSegment));
3948
+ setVoiceState("review");
3949
+ return;
3950
+ }
3951
+ voiceDraftRef.current = null;
3952
+ setVoiceDraft(null);
3953
+ setVoiceTranscript(clearVoiceTranscript());
3954
+ setVoiceDurationMs(0);
3743
3955
  setVoiceState("error");
3744
3956
  }
3745
3957
  }, {
@@ -3749,34 +3961,67 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3749
3961
  return provider;
3750
3962
  }, [config, voiceAutoSendDelayMs, voiceMaxRecordingMs]);
3751
3963
  const closeVoiceComposer = (0, import_react5.useCallback)(async () => {
3964
+ voiceAppendBaseRef.current = null;
3965
+ voiceAppendBaseDurationRef.current = 0;
3752
3966
  setIsVoiceComposerOpen(false);
3753
3967
  setVoiceError(null);
3754
3968
  setVoiceCountdownMs(0);
3755
3969
  setVoiceAudioLevel(0);
3756
3970
  setVoiceTranscript(clearVoiceTranscript());
3757
3971
  setVoiceDraft(null);
3972
+ voiceDraftRef.current = null;
3758
3973
  setVoiceDurationMs(0);
3759
3974
  setVoiceState("idle");
3760
3975
  if (voiceProviderRef.current) {
3761
3976
  await voiceProviderRef.current.cancel();
3762
3977
  }
3763
3978
  }, []);
3764
- const startVoiceCapture = (0, import_react5.useCallback)(async () => {
3979
+ const startVoiceCapture = (0, import_react5.useCallback)(async (appendToDraft = false) => {
3765
3980
  if (disabled || isGenerating) {
3766
3981
  return;
3767
3982
  }
3983
+ const previousDraft = appendToDraft ? voiceDraftRef.current : null;
3984
+ const previousDurationMs = previousDraft ? resolveVoiceSegmentDuration(previousDraft) : 0;
3768
3985
  setIsVoiceComposerOpen(true);
3769
3986
  setVoiceError(null);
3770
- setVoiceDraft(null);
3771
3987
  setVoiceCountdownMs(0);
3772
- setVoiceTranscript(clearVoiceTranscript());
3773
3988
  setVoiceAudioLevel(0);
3774
- setVoiceDurationMs(0);
3989
+ setIsVoiceAutoSendActive(false);
3990
+ voiceAppendBaseRef.current = previousDraft;
3991
+ voiceAppendBaseDurationRef.current = previousDurationMs;
3992
+ if (!previousDraft) {
3993
+ setVoiceDraft(null);
3994
+ voiceDraftRef.current = null;
3995
+ setVoiceTranscript(clearVoiceTranscript());
3996
+ setVoiceDurationMs(0);
3997
+ } else {
3998
+ setVoiceTranscript(previousDraft.transcript ?? clearVoiceTranscript());
3999
+ setVoiceDurationMs(previousDurationMs);
4000
+ }
3775
4001
  try {
3776
4002
  const provider = await ensureVoiceProvider();
3777
4003
  await provider.start();
3778
4004
  } catch (error) {
3779
- setVoiceError(resolveVoiceErrorMessage(error, config));
4005
+ const resolvedError = resolveVoiceErrorMessage(error, config);
4006
+ voiceAppendBaseRef.current = null;
4007
+ voiceAppendBaseDurationRef.current = 0;
4008
+ setVoiceAudioLevel(0);
4009
+ setVoiceCountdownMs(0);
4010
+ setIsVoiceAutoSendActive(false);
4011
+ if (previousDraft) {
4012
+ voiceDraftRef.current = previousDraft;
4013
+ setVoiceDraft(previousDraft);
4014
+ setVoiceTranscript(previousDraft.transcript ?? clearVoiceTranscript());
4015
+ setVoiceDurationMs(previousDurationMs);
4016
+ setVoiceError(resolvedError);
4017
+ setVoiceState("review");
4018
+ return;
4019
+ }
4020
+ voiceDraftRef.current = null;
4021
+ setVoiceDraft(null);
4022
+ setVoiceTranscript(clearVoiceTranscript());
4023
+ setVoiceDurationMs(0);
4024
+ setVoiceError(resolvedError);
3780
4025
  setVoiceState("error");
3781
4026
  }
3782
4027
  }, [disabled, isGenerating, ensureVoiceProvider, config]);
@@ -3790,6 +4035,8 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3790
4035
  }
3791
4036
  }, [config]);
3792
4037
  const cancelVoiceCapture = (0, import_react5.useCallback)(async () => {
4038
+ voiceAppendBaseRef.current = null;
4039
+ voiceAppendBaseDurationRef.current = 0;
3793
4040
  if (voiceProviderRef.current) {
3794
4041
  await voiceProviderRef.current.cancel();
3795
4042
  }
@@ -3809,6 +4056,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3809
4056
  }
3810
4057
  setVoiceState("sending");
3811
4058
  setVoiceCountdownMs(0);
4059
+ setIsVoiceAutoSendActive(false);
3812
4060
  onSubmit("", [...attachments, voiceDraft.attachment]);
3813
4061
  onChange("");
3814
4062
  onAttachmentsChange([]);
@@ -3823,12 +4071,12 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3823
4071
  onAttachmentsChange,
3824
4072
  finalizeVoiceComposerAfterSend
3825
4073
  ]);
3826
- const recordVoiceAgain = (0, import_react5.useCallback)(async () => {
3827
- resetVoiceComposerState("idle");
3828
- await startVoiceCapture();
3829
- }, [resetVoiceComposerState, startVoiceCapture]);
4074
+ const cancelVoiceAutoSend = (0, import_react5.useCallback)(() => {
4075
+ setVoiceCountdownMs(0);
4076
+ setIsVoiceAutoSendActive(false);
4077
+ }, []);
3830
4078
  (0, import_react5.useEffect)(() => {
3831
- if (voiceState !== "review" || !voiceDraft || voiceAutoSendDelayMs <= 0) {
4079
+ if (voiceState !== "review" || !voiceDraft || voiceAutoSendDelayMs <= 0 || !isVoiceAutoSendActive) {
3832
4080
  return;
3833
4081
  }
3834
4082
  const startedAt = Date.now();
@@ -3842,7 +4090,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3842
4090
  }
3843
4091
  }, 100);
3844
4092
  return () => clearInterval(timer);
3845
- }, [voiceState, voiceDraft, voiceAutoSendDelayMs, sendVoiceDraft]);
4093
+ }, [voiceState, voiceDraft, voiceAutoSendDelayMs, isVoiceAutoSendActive, sendVoiceDraft]);
3846
4094
  const removeAttachment = (index) => {
3847
4095
  const newAttachments = attachments.filter((_, i) => i !== index);
3848
4096
  onAttachmentsChange(newAttachments);
@@ -3891,10 +4139,12 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3891
4139
  transcript: voiceTranscript,
3892
4140
  transcriptMode: voiceTranscriptMode,
3893
4141
  showTranscriptPreview: voiceShowTranscriptPreview,
4142
+ attachment: voiceDraft?.attachment ?? null,
3894
4143
  durationMs: voiceDurationMs,
3895
4144
  audioLevel: voiceAudioLevel,
3896
4145
  countdownMs: voiceCountdownMs,
3897
4146
  autoSendDelayMs: voiceAutoSendDelayMs,
4147
+ isAutoSendActive: isVoiceAutoSendActive,
3898
4148
  errorMessage: voiceError,
3899
4149
  disabled: disabled || isGenerating,
3900
4150
  labels: config?.labels,
@@ -3904,13 +4154,16 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3904
4154
  onStop: () => {
3905
4155
  void stopVoiceCapture();
3906
4156
  },
3907
- onCancel: () => {
4157
+ onCancelAutoSend: () => {
4158
+ cancelVoiceAutoSend();
4159
+ },
4160
+ onDiscard: () => {
3908
4161
  void cancelVoiceCapture();
3909
4162
  },
3910
- onSendNow: sendVoiceDraft,
3911
4163
  onRecordAgain: () => {
3912
- void recordVoiceAgain();
4164
+ void startVoiceCapture(true);
3913
4165
  },
4166
+ onSendNow: sendVoiceDraft,
3914
4167
  onExit: () => {
3915
4168
  void closeVoiceComposer();
3916
4169
  }