@copilotz/chat-ui 0.1.34 → 0.1.35

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.d.cts CHANGED
@@ -29,6 +29,7 @@ type AudioAttachment = Extract<MediaAttachment, {
29
29
  kind: 'audio';
30
30
  }>;
31
31
  type VoiceComposerState = 'idle' | 'preparing' | 'waiting_for_speech' | 'listening' | 'finishing' | 'review' | 'sending' | 'error';
32
+ type VoiceReviewMode = 'manual' | 'armed';
32
33
  type VoiceTranscriptMode = 'none' | 'final-only' | 'partial-and-final';
33
34
  interface VoiceTranscript {
34
35
  partial?: string;
@@ -130,6 +131,8 @@ interface ChatConfig {
130
131
  voiceFinishing?: string;
131
132
  voiceReview?: string;
132
133
  voiceSending?: string;
134
+ voiceReviewArmedHint?: string;
135
+ voiceReviewPausedHint?: string;
133
136
  voiceStart?: string;
134
137
  voiceStop?: string;
135
138
  voiceSendNow?: string;
@@ -202,6 +205,7 @@ interface ChatConfig {
202
205
  voiceCompose?: {
203
206
  enabled?: boolean;
204
207
  defaultMode?: 'text' | 'voice';
208
+ reviewMode?: VoiceReviewMode;
205
209
  autoSendDelayMs?: number;
206
210
  persistComposer?: boolean;
207
211
  showTranscriptPreview?: boolean;
@@ -669,4 +673,4 @@ declare const chatUtils: {
669
673
  generateThreadTitle: (firstMessage: string) => string;
670
674
  };
671
675
 
672
- export { type AgentOption, type AudioAttachment, type ChatCallbacks, type ChatConfig, ChatHeader, type ChatHeaderConfig, type ChatHeaderProps, ChatInput, type ChatMessage, type ChatState, type ChatThread, ChatUI, type ChatUserContext, ChatUserContextProvider, type ChatV2Props, type CreateVoiceProvider, type CustomField, type FileUploadProgress, type MediaAttachment, type MemoryItem, Message, type MessageAction, type MessageActionEvent, Sidebar, type SidebarConfig, type SidebarProps, type StateCallback, type StreamingUpdate, ThreadManager, type ToolCall, type UserCustomField, UserMenu, type UserMenuCallbacks, type UserMenuConfig, type UserMenuProps, type UserMenuUser, UserProfile, type UserProfileConfig, type UserProfileProps, type UserProfileUser, type VoiceComposerState, type VoiceProvider, type VoiceProviderHandlers, type VoiceProviderOptions, type VoiceSegment, type VoiceTranscript, type VoiceTranscriptMode, chatConfigPresets, chatUtils, cn, configUtils, createObjectUrlFromDataUrl, defaultChatConfig, featureFlags, formatDate, mergeConfig, themeUtils, useChatUserContext, validateConfig };
676
+ export { type AgentOption, type AudioAttachment, type ChatCallbacks, type ChatConfig, ChatHeader, type ChatHeaderConfig, type ChatHeaderProps, ChatInput, type ChatMessage, type ChatState, type ChatThread, ChatUI, type ChatUserContext, ChatUserContextProvider, type ChatV2Props, type CreateVoiceProvider, type CustomField, type FileUploadProgress, type MediaAttachment, type MemoryItem, Message, type MessageAction, type MessageActionEvent, Sidebar, type SidebarConfig, type SidebarProps, type StateCallback, type StreamingUpdate, ThreadManager, type ToolCall, type UserCustomField, UserMenu, type UserMenuCallbacks, type UserMenuConfig, type UserMenuProps, type UserMenuUser, UserProfile, type UserProfileConfig, type UserProfileProps, type UserProfileUser, type VoiceComposerState, type VoiceProvider, type VoiceProviderHandlers, type VoiceProviderOptions, type VoiceReviewMode, type VoiceSegment, type VoiceTranscript, type VoiceTranscriptMode, chatConfigPresets, chatUtils, cn, configUtils, createObjectUrlFromDataUrl, defaultChatConfig, featureFlags, formatDate, mergeConfig, themeUtils, useChatUserContext, validateConfig };
package/dist/index.d.ts CHANGED
@@ -29,6 +29,7 @@ type AudioAttachment = Extract<MediaAttachment, {
29
29
  kind: 'audio';
30
30
  }>;
31
31
  type VoiceComposerState = 'idle' | 'preparing' | 'waiting_for_speech' | 'listening' | 'finishing' | 'review' | 'sending' | 'error';
32
+ type VoiceReviewMode = 'manual' | 'armed';
32
33
  type VoiceTranscriptMode = 'none' | 'final-only' | 'partial-and-final';
33
34
  interface VoiceTranscript {
34
35
  partial?: string;
@@ -130,6 +131,8 @@ interface ChatConfig {
130
131
  voiceFinishing?: string;
131
132
  voiceReview?: string;
132
133
  voiceSending?: string;
134
+ voiceReviewArmedHint?: string;
135
+ voiceReviewPausedHint?: string;
133
136
  voiceStart?: string;
134
137
  voiceStop?: string;
135
138
  voiceSendNow?: string;
@@ -202,6 +205,7 @@ interface ChatConfig {
202
205
  voiceCompose?: {
203
206
  enabled?: boolean;
204
207
  defaultMode?: 'text' | 'voice';
208
+ reviewMode?: VoiceReviewMode;
205
209
  autoSendDelayMs?: number;
206
210
  persistComposer?: boolean;
207
211
  showTranscriptPreview?: boolean;
@@ -669,4 +673,4 @@ declare const chatUtils: {
669
673
  generateThreadTitle: (firstMessage: string) => string;
670
674
  };
671
675
 
672
- export { type AgentOption, type AudioAttachment, type ChatCallbacks, type ChatConfig, ChatHeader, type ChatHeaderConfig, type ChatHeaderProps, ChatInput, type ChatMessage, type ChatState, type ChatThread, ChatUI, type ChatUserContext, ChatUserContextProvider, type ChatV2Props, type CreateVoiceProvider, type CustomField, type FileUploadProgress, type MediaAttachment, type MemoryItem, Message, type MessageAction, type MessageActionEvent, Sidebar, type SidebarConfig, type SidebarProps, type StateCallback, type StreamingUpdate, ThreadManager, type ToolCall, type UserCustomField, UserMenu, type UserMenuCallbacks, type UserMenuConfig, type UserMenuProps, type UserMenuUser, UserProfile, type UserProfileConfig, type UserProfileProps, type UserProfileUser, type VoiceComposerState, type VoiceProvider, type VoiceProviderHandlers, type VoiceProviderOptions, type VoiceSegment, type VoiceTranscript, type VoiceTranscriptMode, chatConfigPresets, chatUtils, cn, configUtils, createObjectUrlFromDataUrl, defaultChatConfig, featureFlags, formatDate, mergeConfig, themeUtils, useChatUserContext, validateConfig };
676
+ export { type AgentOption, type AudioAttachment, type ChatCallbacks, type ChatConfig, ChatHeader, type ChatHeaderConfig, type ChatHeaderProps, ChatInput, type ChatMessage, type ChatState, type ChatThread, ChatUI, type ChatUserContext, ChatUserContextProvider, type ChatV2Props, type CreateVoiceProvider, type CustomField, type FileUploadProgress, type MediaAttachment, type MemoryItem, Message, type MessageAction, type MessageActionEvent, Sidebar, type SidebarConfig, type SidebarProps, type StateCallback, type StreamingUpdate, ThreadManager, type ToolCall, type UserCustomField, UserMenu, type UserMenuCallbacks, type UserMenuConfig, type UserMenuProps, type UserMenuUser, UserProfile, type UserProfileConfig, type UserProfileProps, type UserProfileUser, type VoiceComposerState, type VoiceProvider, type VoiceProviderHandlers, type VoiceProviderOptions, type VoiceReviewMode, type VoiceSegment, type VoiceTranscript, type VoiceTranscriptMode, chatConfigPresets, chatUtils, cn, configUtils, createObjectUrlFromDataUrl, defaultChatConfig, featureFlags, formatDate, mergeConfig, themeUtils, useChatUserContext, validateConfig };
package/dist/index.js CHANGED
@@ -40,6 +40,8 @@ var defaultChatConfig = {
40
40
  voiceFinishing: "Finishing capture...",
41
41
  voiceReview: "Ready to send",
42
42
  voiceSending: "Sending...",
43
+ voiceReviewArmedHint: "Still listening. Speak to add more before it sends.",
44
+ voiceReviewPausedHint: "Tap the mic to keep adding to this message.",
43
45
  voiceStart: "Start recording",
44
46
  voiceStop: "Stop recording",
45
47
  voiceSendNow: "Send now",
@@ -115,6 +117,7 @@ var defaultChatConfig = {
115
117
  voiceCompose: {
116
118
  enabled: false,
117
119
  defaultMode: "text",
120
+ reviewMode: "manual",
118
121
  autoSendDelayMs: 5e3,
119
122
  persistComposer: true,
120
123
  showTranscriptPreview: true,
@@ -3240,11 +3243,13 @@ var VoiceComposer = ({
3240
3243
  countdownMs,
3241
3244
  autoSendDelayMs,
3242
3245
  isAutoSendActive,
3246
+ reviewMode,
3243
3247
  errorMessage,
3244
3248
  disabled = false,
3245
3249
  labels,
3246
3250
  onStart,
3247
3251
  onStop,
3252
+ onPauseReview,
3248
3253
  onCancelAutoSend,
3249
3254
  onDiscard,
3250
3255
  onRecordAgain,
@@ -3256,9 +3261,26 @@ var VoiceComposer = ({
3256
3261
  const countdownValue = autoSendDelayMs > 0 ? Math.min(100, Math.max(0, (autoSendDelayMs - countdownMs) / autoSendDelayMs * 100)) : 100;
3257
3262
  const isBusy = state === "preparing" || state === "finishing" || state === "sending";
3258
3263
  const isCapturing = state === "waiting_for_speech" || state === "listening";
3259
- const isReviewing = state === "review";
3264
+ const hasDraft = Boolean(attachment);
3265
+ const isDraftLayout = hasDraft;
3266
+ const isArmedDraft = isDraftLayout && reviewMode === "armed" && (state === "waiting_for_speech" || state === "listening");
3260
3267
  const levelValue = isCapturing || state === "preparing" || state === "finishing" ? Math.max(8, Math.round(audioLevel * 100)) : 0;
3261
- const headerLabel = state === "error" ? labels?.voiceCaptureError || "Unable to capture audio." : resolveStateLabel(state, labels, errorMessage);
3268
+ const headerLabel = hasDraft && state !== "sending" && state !== "error" ? labels?.voiceReview || "Ready to send" : state === "error" ? labels?.voiceCaptureError || "Unable to capture audio." : resolveStateLabel(state, labels, errorMessage);
3269
+ const reviewHelperText = isArmedDraft ? labels?.voiceReviewArmedHint || "Speak to add more before it sends." : labels?.voiceReviewPausedHint || labels?.voiceRecordAgain || "Tap the mic to continue this message.";
3270
+ const orbIsListening = state === "listening";
3271
+ const orbCanStop = !isDraftLayout && (state === "waiting_for_speech" || state === "listening");
3272
+ const orbIsReviewBusy = state === "preparing" || state === "finishing" || state === "sending";
3273
+ const handleReviewOrbClick = () => {
3274
+ if (state === "listening") {
3275
+ onStop();
3276
+ return;
3277
+ }
3278
+ if (isArmedDraft) {
3279
+ onPauseReview();
3280
+ return;
3281
+ }
3282
+ onRecordAgain();
3283
+ };
3262
3284
  return /* @__PURE__ */ jsxs11("div", { className: "w-full max-w-3xl rounded-xl border bg-background p-3 shadow-sm sm:p-4 md:min-w-3xl", children: [
3263
3285
  /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between gap-2 sm:gap-3", children: [
3264
3286
  /* @__PURE__ */ jsxs11("div", { className: "flex min-w-0 items-center gap-2", children: [
@@ -3281,7 +3303,7 @@ var VoiceComposer = ({
3281
3303
  }
3282
3304
  )
3283
3305
  ] }),
3284
- !isReviewing ? /* @__PURE__ */ jsx21("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__ */ jsxs11("div", { className: "mx-auto flex w-full max-w-sm flex-col items-center gap-3", children: [
3306
+ !isDraftLayout ? /* @__PURE__ */ jsx21("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__ */ jsxs11("div", { className: "mx-auto flex w-full max-w-sm flex-col items-center gap-3", children: [
3285
3307
  /* @__PURE__ */ jsx21(
3286
3308
  Button,
3287
3309
  {
@@ -3323,6 +3345,27 @@ var VoiceComposer = ({
3323
3345
  }
3324
3346
  )
3325
3347
  ] }),
3348
+ /* @__PURE__ */ jsxs11("div", { className: "mt-4 flex flex-col items-center gap-3 text-center", children: [
3349
+ /* @__PURE__ */ jsx21(
3350
+ Button,
3351
+ {
3352
+ type: "button",
3353
+ size: "icon",
3354
+ variant: orbCanStop ? "destructive" : "outline",
3355
+ className: `h-16 w-16 rounded-full sm:h-20 sm:w-20 ${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"}`,
3356
+ onClick: handleReviewOrbClick,
3357
+ disabled: disabled || orbIsReviewBusy,
3358
+ children: orbIsReviewBusy ? /* @__PURE__ */ jsx21(Loader2, { className: "h-7 w-7 animate-spin" }) : orbIsListening ? /* @__PURE__ */ jsx21(Square, { className: "h-7 w-7" }) : isArmedDraft ? /* @__PURE__ */ jsx21(Mic, { className: "h-7 w-7 animate-pulse" }) : /* @__PURE__ */ jsx21(Mic, { className: "h-7 w-7" })
3359
+ }
3360
+ ),
3361
+ /* @__PURE__ */ jsxs11("div", { className: "w-full max-w-sm space-y-2", children: [
3362
+ /* @__PURE__ */ jsx21(Progress, { value: levelValue, className: "h-2" }),
3363
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [
3364
+ /* @__PURE__ */ jsx21("span", { children: formatDuration(durationMs) }),
3365
+ /* @__PURE__ */ jsx21("span", { className: "max-w-[15rem] text-right", children: reviewHelperText })
3366
+ ] })
3367
+ ] })
3368
+ ] }),
3326
3369
  attachment && /* @__PURE__ */ jsx21("div", { className: "mt-3 rounded-lg bg-background p-2", children: /* @__PURE__ */ jsx21("audio", { controls: true, preload: "metadata", className: "w-full", children: /* @__PURE__ */ jsx21("source", { src: attachment.dataUrl, type: attachment.mimeType }) }) }),
3327
3370
  showTranscriptPreview && transcriptMode !== "none" && transcriptText && /* @__PURE__ */ jsx21("div", { className: "mt-3 rounded-lg border bg-background px-3 py-2 text-left text-sm", children: transcriptText }),
3328
3371
  isAutoSendActive && autoSendDelayMs > 0 && /* @__PURE__ */ jsxs11("div", { className: "mt-3 space-y-2", children: [
@@ -3334,19 +3377,6 @@ var VoiceComposer = ({
3334
3377
  /* @__PURE__ */ jsx21(X2, { className: "h-4 w-4" }),
3335
3378
  labels?.voiceCancel || "Cancel"
3336
3379
  ] }),
3337
- !isAutoSendActive && /* @__PURE__ */ jsx21(
3338
- Button,
3339
- {
3340
- type: "button",
3341
- variant: "outline",
3342
- size: "icon",
3343
- onClick: onRecordAgain,
3344
- disabled,
3345
- "aria-label": labels?.voiceRecordAgain || "Record again",
3346
- title: labels?.voiceRecordAgain || "Record again",
3347
- children: /* @__PURE__ */ jsx21(Mic, { className: "h-4 w-4" })
3348
- }
3349
- ),
3350
3380
  /* @__PURE__ */ jsxs11(Button, { type: "button", size: "sm", onClick: onSendNow, disabled, children: [
3351
3381
  /* @__PURE__ */ jsx21(Send, { className: "h-4 w-4" }),
3352
3382
  labels?.voiceSendNow || "Send now"
@@ -3636,6 +3666,7 @@ var ChatInput = memo2(function ChatInput2({
3636
3666
  }) {
3637
3667
  const voiceComposeEnabled = config?.voiceCompose?.enabled === true;
3638
3668
  const voiceDefaultMode = config?.voiceCompose?.defaultMode ?? "text";
3669
+ const voiceReviewMode = config?.voiceCompose?.reviewMode ?? "manual";
3639
3670
  const voiceAutoSendDelayMs = config?.voiceCompose?.autoSendDelayMs ?? 5e3;
3640
3671
  const voicePersistComposer = config?.voiceCompose?.persistComposer ?? true;
3641
3672
  const voiceShowTranscriptPreview = config?.voiceCompose?.showTranscriptPreview ?? true;
@@ -3870,13 +3901,30 @@ var ChatInput = memo2(function ChatInput2({
3870
3901
  setIsVoiceAutoSendActive(false);
3871
3902
  setVoiceError(null);
3872
3903
  }, []);
3904
+ const armVoiceDraftForAppend = useCallback3((segment) => {
3905
+ voiceAppendBaseRef.current = segment;
3906
+ voiceAppendBaseDurationRef.current = segment ? resolveVoiceSegmentDuration(segment) : 0;
3907
+ }, []);
3908
+ const handleVoiceProviderStateChange = useCallback3((nextState) => {
3909
+ if (voiceReviewMode === "armed" && (nextState === "waiting_for_speech" || nextState === "listening")) {
3910
+ const currentDraft = voiceDraftRef.current;
3911
+ if (currentDraft) {
3912
+ armVoiceDraftForAppend(currentDraft);
3913
+ }
3914
+ }
3915
+ if (voiceReviewMode === "armed" && nextState === "listening" && voiceDraftRef.current) {
3916
+ setVoiceCountdownMs(voiceAutoSendDelayMs);
3917
+ setIsVoiceAutoSendActive(false);
3918
+ }
3919
+ setVoiceState(nextState);
3920
+ }, [armVoiceDraftForAppend, voiceAutoSendDelayMs, voiceReviewMode]);
3873
3921
  const ensureVoiceProvider = useCallback3(async () => {
3874
3922
  if (voiceProviderRef.current) {
3875
3923
  return voiceProviderRef.current;
3876
3924
  }
3877
3925
  const createProvider = resolveVoiceProviderFactory(config?.voiceCompose?.createProvider);
3878
3926
  const provider = await createProvider({
3879
- onStateChange: setVoiceState,
3927
+ onStateChange: handleVoiceProviderStateChange,
3880
3928
  onAudioLevelChange: setVoiceAudioLevel,
3881
3929
  onDurationChange: (durationMs) => {
3882
3930
  setVoiceDurationMs(voiceAppendBaseDurationRef.current + durationMs);
@@ -3892,8 +3940,6 @@ var ChatInput = memo2(function ChatInput2({
3892
3940
  const previousSegment = voiceAppendBaseRef.current;
3893
3941
  try {
3894
3942
  const nextSegment = previousSegment ? await appendVoiceSegments(previousSegment, segment) : segment;
3895
- voiceAppendBaseRef.current = null;
3896
- voiceAppendBaseDurationRef.current = 0;
3897
3943
  voiceDraftRef.current = nextSegment;
3898
3944
  setVoiceDraft(nextSegment);
3899
3945
  setVoiceTranscript(nextSegment.transcript ?? clearVoiceTranscript());
@@ -3902,11 +3948,15 @@ var ChatInput = memo2(function ChatInput2({
3902
3948
  setVoiceCountdownMs(voiceAutoSendDelayMs);
3903
3949
  setIsVoiceAutoSendActive(voiceAutoSendDelayMs > 0);
3904
3950
  setVoiceError(null);
3905
- setVoiceState("review");
3951
+ if (voiceReviewMode === "armed") {
3952
+ armVoiceDraftForAppend(nextSegment);
3953
+ } else {
3954
+ armVoiceDraftForAppend(null);
3955
+ }
3956
+ setVoiceState((currentState) => voiceReviewMode === "armed" && (currentState === "waiting_for_speech" || currentState === "listening") ? currentState : "review");
3906
3957
  } catch (error) {
3907
3958
  const resolvedError = resolveVoiceErrorMessage(error, config);
3908
- voiceAppendBaseRef.current = null;
3909
- voiceAppendBaseDurationRef.current = 0;
3959
+ armVoiceDraftForAppend(null);
3910
3960
  setVoiceAudioLevel(0);
3911
3961
  setVoiceCountdownMs(0);
3912
3962
  setIsVoiceAutoSendActive(false);
@@ -3930,8 +3980,7 @@ var ChatInput = memo2(function ChatInput2({
3930
3980
  },
3931
3981
  onError: (error) => {
3932
3982
  const previousSegment = voiceAppendBaseRef.current;
3933
- voiceAppendBaseRef.current = null;
3934
- voiceAppendBaseDurationRef.current = 0;
3983
+ armVoiceDraftForAppend(null);
3935
3984
  setVoiceError(resolveVoiceErrorMessage(error, config));
3936
3985
  setVoiceAudioLevel(0);
3937
3986
  setVoiceCountdownMs(0);
@@ -3955,7 +4004,7 @@ var ChatInput = memo2(function ChatInput2({
3955
4004
  });
3956
4005
  voiceProviderRef.current = provider;
3957
4006
  return provider;
3958
- }, [config, voiceAutoSendDelayMs, voiceMaxRecordingMs]);
4007
+ }, [armVoiceDraftForAppend, config, handleVoiceProviderStateChange, voiceAutoSendDelayMs, voiceMaxRecordingMs, voiceReviewMode]);
3959
4008
  const closeVoiceComposer = useCallback3(async () => {
3960
4009
  voiceAppendBaseRef.current = null;
3961
4010
  voiceAppendBaseDurationRef.current = 0;
@@ -4047,16 +4096,21 @@ var ChatInput = memo2(function ChatInput2({
4047
4096
  void closeVoiceComposer();
4048
4097
  }, [voicePersistComposer, resetVoiceComposerState, closeVoiceComposer]);
4049
4098
  const sendVoiceDraft = useCallback3(() => {
4050
- if (!voiceDraft || disabled || isGenerating) {
4051
- return;
4052
- }
4053
- setVoiceState("sending");
4054
- setVoiceCountdownMs(0);
4055
- setIsVoiceAutoSendActive(false);
4056
- onSubmit("", [...attachments, voiceDraft.attachment]);
4057
- onChange("");
4058
- onAttachmentsChange([]);
4059
- finalizeVoiceComposerAfterSend();
4099
+ void (async () => {
4100
+ if (!voiceDraft || disabled || isGenerating) {
4101
+ return;
4102
+ }
4103
+ setVoiceState("sending");
4104
+ setVoiceCountdownMs(0);
4105
+ setIsVoiceAutoSendActive(false);
4106
+ if (voiceProviderRef.current) {
4107
+ await voiceProviderRef.current.cancel();
4108
+ }
4109
+ onSubmit("", [...attachments, voiceDraft.attachment]);
4110
+ onChange("");
4111
+ onAttachmentsChange([]);
4112
+ finalizeVoiceComposerAfterSend();
4113
+ })();
4060
4114
  }, [
4061
4115
  voiceDraft,
4062
4116
  disabled,
@@ -4068,25 +4122,51 @@ var ChatInput = memo2(function ChatInput2({
4068
4122
  finalizeVoiceComposerAfterSend
4069
4123
  ]);
4070
4124
  const cancelVoiceAutoSend = useCallback3(() => {
4125
+ void (async () => {
4126
+ if (voiceReviewMode === "armed" && voiceProviderRef.current) {
4127
+ await voiceProviderRef.current.cancel();
4128
+ }
4129
+ armVoiceDraftForAppend(null);
4130
+ setVoiceAudioLevel(0);
4131
+ setVoiceState("review");
4132
+ })();
4071
4133
  setVoiceCountdownMs(0);
4072
4134
  setIsVoiceAutoSendActive(false);
4073
- }, []);
4135
+ }, [armVoiceDraftForAppend, voiceReviewMode]);
4136
+ const pauseVoiceReview = useCallback3(async () => {
4137
+ if (voiceState === "listening") {
4138
+ await stopVoiceCapture();
4139
+ return;
4140
+ }
4141
+ if (voiceReviewMode === "armed" && voiceProviderRef.current) {
4142
+ await voiceProviderRef.current.cancel();
4143
+ }
4144
+ armVoiceDraftForAppend(null);
4145
+ setVoiceAudioLevel(0);
4146
+ setVoiceState("review");
4147
+ }, [armVoiceDraftForAppend, stopVoiceCapture, voiceReviewMode, voiceState]);
4074
4148
  useEffect9(() => {
4075
- if (voiceState !== "review" || !voiceDraft || voiceAutoSendDelayMs <= 0 || !isVoiceAutoSendActive) {
4149
+ if (!voiceDraft || voiceAutoSendDelayMs <= 0 || !isVoiceAutoSendActive) {
4150
+ return;
4151
+ }
4152
+ const canContinueCounting = voiceState === "review" || voiceReviewMode === "armed" && voiceState === "waiting_for_speech";
4153
+ if (!canContinueCounting) {
4076
4154
  return;
4077
4155
  }
4078
- const startedAt = Date.now();
4079
- setVoiceCountdownMs(voiceAutoSendDelayMs);
4080
4156
  const timer = setInterval(() => {
4081
- const remaining = Math.max(0, voiceAutoSendDelayMs - (Date.now() - startedAt));
4082
- setVoiceCountdownMs(remaining);
4083
- if (remaining <= 0) {
4084
- clearInterval(timer);
4085
- sendVoiceDraft();
4086
- }
4157
+ setVoiceCountdownMs((previous) => {
4158
+ const remaining = Math.max(0, previous - 100);
4159
+ if (remaining <= 0) {
4160
+ clearInterval(timer);
4161
+ queueMicrotask(() => {
4162
+ sendVoiceDraft();
4163
+ });
4164
+ }
4165
+ return remaining;
4166
+ });
4087
4167
  }, 100);
4088
4168
  return () => clearInterval(timer);
4089
- }, [voiceState, voiceDraft, voiceAutoSendDelayMs, isVoiceAutoSendActive, sendVoiceDraft]);
4169
+ }, [voiceState, voiceDraft, voiceReviewMode, voiceAutoSendDelayMs, isVoiceAutoSendActive, sendVoiceDraft]);
4090
4170
  const removeAttachment = (index) => {
4091
4171
  const newAttachments = attachments.filter((_, i) => i !== index);
4092
4172
  onAttachmentsChange(newAttachments);
@@ -4141,6 +4221,7 @@ var ChatInput = memo2(function ChatInput2({
4141
4221
  countdownMs: voiceCountdownMs,
4142
4222
  autoSendDelayMs: voiceAutoSendDelayMs,
4143
4223
  isAutoSendActive: isVoiceAutoSendActive,
4224
+ reviewMode: voiceReviewMode,
4144
4225
  errorMessage: voiceError,
4145
4226
  disabled: disabled || isGenerating,
4146
4227
  labels: config?.labels,
@@ -4150,6 +4231,9 @@ var ChatInput = memo2(function ChatInput2({
4150
4231
  onStop: () => {
4151
4232
  void stopVoiceCapture();
4152
4233
  },
4234
+ onPauseReview: () => {
4235
+ void pauseVoiceReview();
4236
+ },
4153
4237
  onCancelAutoSend: () => {
4154
4238
  cancelVoiceAutoSend();
4155
4239
  },