@copilotz/chat-ui 0.1.34 → 0.1.36

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
@@ -96,6 +96,8 @@ var defaultChatConfig = {
96
96
  voiceFinishing: "Finishing capture...",
97
97
  voiceReview: "Ready to send",
98
98
  voiceSending: "Sending...",
99
+ voiceReviewArmedHint: "Still listening. Speak to add more before it sends.",
100
+ voiceReviewPausedHint: "Tap the mic to keep adding to this message.",
99
101
  voiceStart: "Start recording",
100
102
  voiceStop: "Stop recording",
101
103
  voiceSendNow: "Send now",
@@ -171,6 +173,7 @@ var defaultChatConfig = {
171
173
  voiceCompose: {
172
174
  enabled: false,
173
175
  defaultMode: "text",
176
+ reviewMode: "manual",
174
177
  autoSendDelayMs: 5e3,
175
178
  persistComposer: true,
176
179
  showTranscriptPreview: true,
@@ -3256,11 +3259,13 @@ var VoiceComposer = ({
3256
3259
  countdownMs,
3257
3260
  autoSendDelayMs,
3258
3261
  isAutoSendActive,
3262
+ reviewMode,
3259
3263
  errorMessage,
3260
3264
  disabled = false,
3261
3265
  labels,
3262
3266
  onStart,
3263
3267
  onStop,
3268
+ onPauseReview,
3264
3269
  onCancelAutoSend,
3265
3270
  onDiscard,
3266
3271
  onRecordAgain,
@@ -3269,17 +3274,34 @@ var VoiceComposer = ({
3269
3274
  }) => {
3270
3275
  const transcriptText = resolveTranscriptText(transcript, transcriptMode);
3271
3276
  const countdownSeconds = Math.max(1, Math.ceil(countdownMs / 1e3));
3272
- const countdownValue = autoSendDelayMs > 0 ? Math.min(100, Math.max(0, (autoSendDelayMs - countdownMs) / autoSendDelayMs * 100)) : 100;
3273
3277
  const isBusy = state === "preparing" || state === "finishing" || state === "sending";
3274
3278
  const isCapturing = state === "waiting_for_speech" || state === "listening";
3275
- const isReviewing = state === "review";
3279
+ const hasDraft = Boolean(attachment);
3280
+ const isDraftLayout = hasDraft;
3281
+ const isArmedDraft = isDraftLayout && reviewMode === "armed" && (state === "waiting_for_speech" || state === "listening");
3282
+ const draftStatusLabel = state === "listening" ? labels?.voiceListening || "Listening..." : state === "waiting_for_speech" ? labels?.voiceWaiting || "Waiting for speech..." : state === "finishing" ? labels?.voiceFinishing || "Finishing capture..." : state === "sending" ? labels?.voiceSending || "Sending..." : labels?.voiceReview || "Ready to send";
3276
3283
  const levelValue = isCapturing || state === "preparing" || state === "finishing" ? Math.max(8, Math.round(audioLevel * 100)) : 0;
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: [
3284
+ const headerLabel = hasDraft && state !== "sending" && state !== "error" ? draftStatusLabel : state === "error" ? labels?.voiceCaptureError || "Unable to capture audio." : resolveStateLabel(state, labels, errorMessage);
3285
+ const reviewHelperText = isArmedDraft ? labels?.voiceReviewArmedHint || "Speak to add more before it sends." : labels?.voiceReviewPausedHint || labels?.voiceRecordAgain || "Tap the mic to continue this message.";
3286
+ const orbIsListening = state === "listening";
3287
+ const orbCanStop = !isDraftLayout && (state === "waiting_for_speech" || state === "listening");
3288
+ const orbIsReviewBusy = state === "preparing" || state === "finishing" || state === "sending";
3289
+ const handleReviewOrbClick = () => {
3290
+ if (state === "listening") {
3291
+ onStop();
3292
+ return;
3293
+ }
3294
+ if (isArmedDraft) {
3295
+ onPauseReview();
3296
+ return;
3297
+ }
3298
+ onRecordAgain();
3299
+ };
3300
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "w-full max-w-3xl rounded-2xl border bg-background p-3 shadow-sm sm:p-4 md:min-w-3xl", children: [
3279
3301
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center justify-between gap-2 sm:gap-3", children: [
3280
3302
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
3281
3303
  /* @__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 })
3304
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "truncate rounded-full bg-muted px-2.5 py-1 text-[11px] sm:text-xs text-muted-foreground", children: headerLabel })
3283
3305
  ] }),
3284
3306
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
3285
3307
  Button,
@@ -3297,7 +3319,7 @@ var VoiceComposer = ({
3297
3319
  }
3298
3320
  )
3299
3321
  ] }),
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: [
3322
+ !isDraftLayout ? /* @__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
3323
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3302
3324
  Button,
3303
3325
  {
@@ -3319,11 +3341,8 @@ var VoiceComposer = ({
3319
3341
  ] }),
3320
3342
  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
3343
  ] }) }) : /* @__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) })
3326
- ] }),
3344
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center justify-between gap-2 text-xs text-muted-foreground", children: [
3345
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: formatDuration(durationMs) }),
3327
3346
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3328
3347
  Button,
3329
3348
  {
@@ -3339,31 +3358,39 @@ var VoiceComposer = ({
3339
3358
  }
3340
3359
  )
3341
3360
  ] }),
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: [
3350
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.X, { className: "h-4 w-4" }),
3351
- labels?.voiceCancel || "Cancel"
3352
- ] }),
3353
- !isAutoSendActive && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3361
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-4 flex flex-col items-center gap-4 text-center", children: [
3362
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3354
3363
  Button,
3355
3364
  {
3356
3365
  type: "button",
3357
- variant: "outline",
3358
3366
  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" })
3367
+ variant: orbCanStop ? "destructive" : "outline",
3368
+ className: `h-20 w-20 rounded-full sm:h-24 sm:w-24 ${orbIsListening ? "border-red-500 bg-red-500 text-white hover:bg-red-600" : isArmedDraft ? "border-red-200 bg-red-50 text-red-600 shadow-[0_0_0_10px_rgba(239,68,68,0.08)] hover:bg-red-100 hover:text-red-700" : "border-red-200 bg-red-50 text-red-600 hover:bg-red-100 hover:text-red-700"}`,
3369
+ onClick: handleReviewOrbClick,
3370
+ disabled: disabled || orbIsReviewBusy,
3371
+ children: orbIsReviewBusy ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Loader2, { className: "h-7 w-7 animate-spin" }) : orbIsListening ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Square, { className: "h-7 w-7" }) : isArmedDraft ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Mic, { className: "h-7 w-7 animate-pulse" }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Mic, { className: "h-7 w-7" })
3364
3372
  }
3365
3373
  ),
3366
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", size: "sm", onClick: onSendNow, disabled, children: [
3374
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "max-w-sm space-y-1 px-2", children: [
3375
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "text-sm text-foreground", children: reviewHelperText }),
3376
+ isCapturing && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "mx-auto h-1.5 w-32 overflow-hidden rounded-full bg-red-100", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3377
+ "div",
3378
+ {
3379
+ className: "h-full rounded-full bg-red-500 transition-[width] duration-150",
3380
+ style: { width: `${levelValue}%` }
3381
+ }
3382
+ ) })
3383
+ ] })
3384
+ ] }),
3385
+ attachment && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "mt-4 rounded-lg border bg-background/90 p-2 shadow-sm", 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 }) }) }),
3386
+ 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 }),
3387
+ isAutoSendActive && autoSendDelayMs > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "mt-3 flex justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "inline-flex items-center rounded-full border bg-background px-3 py-1 text-xs text-muted-foreground", children: interpolateSeconds(labels?.voiceAutoSendIn, countdownSeconds) }) }),
3388
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-4 grid grid-cols-1 gap-2 sm:flex sm:items-center sm:justify-end", children: [
3389
+ isAutoSendActive && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", variant: "ghost", size: "sm", onClick: onCancelAutoSend, disabled, className: "w-full sm:w-auto", children: [
3390
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.X, { className: "h-4 w-4" }),
3391
+ labels?.voiceCancel || "Cancel"
3392
+ ] }),
3393
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", size: "sm", onClick: onSendNow, disabled, className: "w-full sm:w-auto", children: [
3367
3394
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Send, { className: "h-4 w-4" }),
3368
3395
  labels?.voiceSendNow || "Send now"
3369
3396
  ] })
@@ -3640,6 +3667,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3640
3667
  }) {
3641
3668
  const voiceComposeEnabled = config?.voiceCompose?.enabled === true;
3642
3669
  const voiceDefaultMode = config?.voiceCompose?.defaultMode ?? "text";
3670
+ const voiceReviewMode = config?.voiceCompose?.reviewMode ?? "manual";
3643
3671
  const voiceAutoSendDelayMs = config?.voiceCompose?.autoSendDelayMs ?? 5e3;
3644
3672
  const voicePersistComposer = config?.voiceCompose?.persistComposer ?? true;
3645
3673
  const voiceShowTranscriptPreview = config?.voiceCompose?.showTranscriptPreview ?? true;
@@ -3874,13 +3902,30 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3874
3902
  setIsVoiceAutoSendActive(false);
3875
3903
  setVoiceError(null);
3876
3904
  }, []);
3905
+ const armVoiceDraftForAppend = (0, import_react5.useCallback)((segment) => {
3906
+ voiceAppendBaseRef.current = segment;
3907
+ voiceAppendBaseDurationRef.current = segment ? resolveVoiceSegmentDuration(segment) : 0;
3908
+ }, []);
3909
+ const handleVoiceProviderStateChange = (0, import_react5.useCallback)((nextState) => {
3910
+ if (voiceReviewMode === "armed" && (nextState === "waiting_for_speech" || nextState === "listening")) {
3911
+ const currentDraft = voiceDraftRef.current;
3912
+ if (currentDraft) {
3913
+ armVoiceDraftForAppend(currentDraft);
3914
+ }
3915
+ }
3916
+ if (voiceReviewMode === "armed" && nextState === "listening" && voiceDraftRef.current) {
3917
+ setVoiceCountdownMs(voiceAutoSendDelayMs);
3918
+ setIsVoiceAutoSendActive(false);
3919
+ }
3920
+ setVoiceState(nextState);
3921
+ }, [armVoiceDraftForAppend, voiceAutoSendDelayMs, voiceReviewMode]);
3877
3922
  const ensureVoiceProvider = (0, import_react5.useCallback)(async () => {
3878
3923
  if (voiceProviderRef.current) {
3879
3924
  return voiceProviderRef.current;
3880
3925
  }
3881
3926
  const createProvider = resolveVoiceProviderFactory(config?.voiceCompose?.createProvider);
3882
3927
  const provider = await createProvider({
3883
- onStateChange: setVoiceState,
3928
+ onStateChange: handleVoiceProviderStateChange,
3884
3929
  onAudioLevelChange: setVoiceAudioLevel,
3885
3930
  onDurationChange: (durationMs) => {
3886
3931
  setVoiceDurationMs(voiceAppendBaseDurationRef.current + durationMs);
@@ -3896,8 +3941,6 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3896
3941
  const previousSegment = voiceAppendBaseRef.current;
3897
3942
  try {
3898
3943
  const nextSegment = previousSegment ? await appendVoiceSegments(previousSegment, segment) : segment;
3899
- voiceAppendBaseRef.current = null;
3900
- voiceAppendBaseDurationRef.current = 0;
3901
3944
  voiceDraftRef.current = nextSegment;
3902
3945
  setVoiceDraft(nextSegment);
3903
3946
  setVoiceTranscript(nextSegment.transcript ?? clearVoiceTranscript());
@@ -3906,11 +3949,15 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3906
3949
  setVoiceCountdownMs(voiceAutoSendDelayMs);
3907
3950
  setIsVoiceAutoSendActive(voiceAutoSendDelayMs > 0);
3908
3951
  setVoiceError(null);
3909
- setVoiceState("review");
3952
+ if (voiceReviewMode === "armed") {
3953
+ armVoiceDraftForAppend(nextSegment);
3954
+ } else {
3955
+ armVoiceDraftForAppend(null);
3956
+ }
3957
+ setVoiceState((currentState) => voiceReviewMode === "armed" && (currentState === "waiting_for_speech" || currentState === "listening") ? currentState : "review");
3910
3958
  } catch (error) {
3911
3959
  const resolvedError = resolveVoiceErrorMessage(error, config);
3912
- voiceAppendBaseRef.current = null;
3913
- voiceAppendBaseDurationRef.current = 0;
3960
+ armVoiceDraftForAppend(null);
3914
3961
  setVoiceAudioLevel(0);
3915
3962
  setVoiceCountdownMs(0);
3916
3963
  setIsVoiceAutoSendActive(false);
@@ -3934,8 +3981,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3934
3981
  },
3935
3982
  onError: (error) => {
3936
3983
  const previousSegment = voiceAppendBaseRef.current;
3937
- voiceAppendBaseRef.current = null;
3938
- voiceAppendBaseDurationRef.current = 0;
3984
+ armVoiceDraftForAppend(null);
3939
3985
  setVoiceError(resolveVoiceErrorMessage(error, config));
3940
3986
  setVoiceAudioLevel(0);
3941
3987
  setVoiceCountdownMs(0);
@@ -3959,7 +4005,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3959
4005
  });
3960
4006
  voiceProviderRef.current = provider;
3961
4007
  return provider;
3962
- }, [config, voiceAutoSendDelayMs, voiceMaxRecordingMs]);
4008
+ }, [armVoiceDraftForAppend, config, handleVoiceProviderStateChange, voiceAutoSendDelayMs, voiceMaxRecordingMs, voiceReviewMode]);
3963
4009
  const closeVoiceComposer = (0, import_react5.useCallback)(async () => {
3964
4010
  voiceAppendBaseRef.current = null;
3965
4011
  voiceAppendBaseDurationRef.current = 0;
@@ -4051,16 +4097,21 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
4051
4097
  void closeVoiceComposer();
4052
4098
  }, [voicePersistComposer, resetVoiceComposerState, closeVoiceComposer]);
4053
4099
  const sendVoiceDraft = (0, import_react5.useCallback)(() => {
4054
- if (!voiceDraft || disabled || isGenerating) {
4055
- return;
4056
- }
4057
- setVoiceState("sending");
4058
- setVoiceCountdownMs(0);
4059
- setIsVoiceAutoSendActive(false);
4060
- onSubmit("", [...attachments, voiceDraft.attachment]);
4061
- onChange("");
4062
- onAttachmentsChange([]);
4063
- finalizeVoiceComposerAfterSend();
4100
+ void (async () => {
4101
+ if (!voiceDraft || disabled || isGenerating) {
4102
+ return;
4103
+ }
4104
+ setVoiceState("sending");
4105
+ setVoiceCountdownMs(0);
4106
+ setIsVoiceAutoSendActive(false);
4107
+ if (voiceProviderRef.current) {
4108
+ await voiceProviderRef.current.cancel();
4109
+ }
4110
+ onSubmit("", [...attachments, voiceDraft.attachment]);
4111
+ onChange("");
4112
+ onAttachmentsChange([]);
4113
+ finalizeVoiceComposerAfterSend();
4114
+ })();
4064
4115
  }, [
4065
4116
  voiceDraft,
4066
4117
  disabled,
@@ -4072,25 +4123,51 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
4072
4123
  finalizeVoiceComposerAfterSend
4073
4124
  ]);
4074
4125
  const cancelVoiceAutoSend = (0, import_react5.useCallback)(() => {
4126
+ void (async () => {
4127
+ if (voiceReviewMode === "armed" && voiceProviderRef.current) {
4128
+ await voiceProviderRef.current.cancel();
4129
+ }
4130
+ armVoiceDraftForAppend(null);
4131
+ setVoiceAudioLevel(0);
4132
+ setVoiceState("review");
4133
+ })();
4075
4134
  setVoiceCountdownMs(0);
4076
4135
  setIsVoiceAutoSendActive(false);
4077
- }, []);
4136
+ }, [armVoiceDraftForAppend, voiceReviewMode]);
4137
+ const pauseVoiceReview = (0, import_react5.useCallback)(async () => {
4138
+ if (voiceState === "listening") {
4139
+ await stopVoiceCapture();
4140
+ return;
4141
+ }
4142
+ if (voiceReviewMode === "armed" && voiceProviderRef.current) {
4143
+ await voiceProviderRef.current.cancel();
4144
+ }
4145
+ armVoiceDraftForAppend(null);
4146
+ setVoiceAudioLevel(0);
4147
+ setVoiceState("review");
4148
+ }, [armVoiceDraftForAppend, stopVoiceCapture, voiceReviewMode, voiceState]);
4078
4149
  (0, import_react5.useEffect)(() => {
4079
- if (voiceState !== "review" || !voiceDraft || voiceAutoSendDelayMs <= 0 || !isVoiceAutoSendActive) {
4150
+ if (!voiceDraft || voiceAutoSendDelayMs <= 0 || !isVoiceAutoSendActive) {
4151
+ return;
4152
+ }
4153
+ const canContinueCounting = voiceState === "review" || voiceReviewMode === "armed" && voiceState === "waiting_for_speech";
4154
+ if (!canContinueCounting) {
4080
4155
  return;
4081
4156
  }
4082
- const startedAt = Date.now();
4083
- setVoiceCountdownMs(voiceAutoSendDelayMs);
4084
4157
  const timer = setInterval(() => {
4085
- const remaining = Math.max(0, voiceAutoSendDelayMs - (Date.now() - startedAt));
4086
- setVoiceCountdownMs(remaining);
4087
- if (remaining <= 0) {
4088
- clearInterval(timer);
4089
- sendVoiceDraft();
4090
- }
4158
+ setVoiceCountdownMs((previous) => {
4159
+ const remaining = Math.max(0, previous - 100);
4160
+ if (remaining <= 0) {
4161
+ clearInterval(timer);
4162
+ queueMicrotask(() => {
4163
+ sendVoiceDraft();
4164
+ });
4165
+ }
4166
+ return remaining;
4167
+ });
4091
4168
  }, 100);
4092
4169
  return () => clearInterval(timer);
4093
- }, [voiceState, voiceDraft, voiceAutoSendDelayMs, isVoiceAutoSendActive, sendVoiceDraft]);
4170
+ }, [voiceState, voiceDraft, voiceReviewMode, voiceAutoSendDelayMs, isVoiceAutoSendActive, sendVoiceDraft]);
4094
4171
  const removeAttachment = (index) => {
4095
4172
  const newAttachments = attachments.filter((_, i) => i !== index);
4096
4173
  onAttachmentsChange(newAttachments);
@@ -4145,6 +4222,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
4145
4222
  countdownMs: voiceCountdownMs,
4146
4223
  autoSendDelayMs: voiceAutoSendDelayMs,
4147
4224
  isAutoSendActive: isVoiceAutoSendActive,
4225
+ reviewMode: voiceReviewMode,
4148
4226
  errorMessage: voiceError,
4149
4227
  disabled: disabled || isGenerating,
4150
4228
  labels: config?.labels,
@@ -4154,6 +4232,9 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
4154
4232
  onStop: () => {
4155
4233
  void stopVoiceCapture();
4156
4234
  },
4235
+ onPauseReview: () => {
4236
+ void pauseVoiceReview();
4237
+ },
4157
4238
  onCancelAutoSend: () => {
4158
4239
  cancelVoiceAutoSend();
4159
4240
  },