@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.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,
@@ -3253,17 +3258,34 @@ var VoiceComposer = ({
3253
3258
  }) => {
3254
3259
  const transcriptText = resolveTranscriptText(transcript, transcriptMode);
3255
3260
  const countdownSeconds = Math.max(1, Math.ceil(countdownMs / 1e3));
3256
- const countdownValue = autoSendDelayMs > 0 ? Math.min(100, Math.max(0, (autoSendDelayMs - countdownMs) / autoSendDelayMs * 100)) : 100;
3257
3261
  const isBusy = state === "preparing" || state === "finishing" || state === "sending";
3258
3262
  const isCapturing = state === "waiting_for_speech" || state === "listening";
3259
- const isReviewing = state === "review";
3263
+ const hasDraft = Boolean(attachment);
3264
+ const isDraftLayout = hasDraft;
3265
+ const isArmedDraft = isDraftLayout && reviewMode === "armed" && (state === "waiting_for_speech" || state === "listening");
3266
+ 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";
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);
3262
- 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: [
3268
+ const headerLabel = hasDraft && state !== "sending" && state !== "error" ? draftStatusLabel : 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
+ };
3284
+ return /* @__PURE__ */ jsxs11("div", { className: "w-full max-w-3xl rounded-2xl 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: [
3265
3287
  /* @__PURE__ */ jsx21(Badge, { variant: "outline", children: labels?.voiceTitle || "Voice" }),
3266
- /* @__PURE__ */ jsx21("span", { className: "truncate text-xs sm:text-sm text-muted-foreground", children: headerLabel })
3288
+ /* @__PURE__ */ jsx21("span", { className: "truncate rounded-full bg-muted px-2.5 py-1 text-[11px] sm:text-xs text-muted-foreground", children: headerLabel })
3267
3289
  ] }),
3268
3290
  /* @__PURE__ */ jsxs11(
3269
3291
  Button,
@@ -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
  {
@@ -3303,11 +3325,8 @@ var VoiceComposer = ({
3303
3325
  ] }),
3304
3326
  showTranscriptPreview && transcriptMode !== "none" && transcriptText && /* @__PURE__ */ jsx21("div", { className: "w-full rounded-lg border bg-background px-3 py-2 text-left text-sm", children: transcriptText })
3305
3327
  ] }) }) : /* @__PURE__ */ jsxs11("div", { className: "mt-3 rounded-xl border bg-muted/20 p-3 sm:p-4", children: [
3306
- /* @__PURE__ */ jsxs11("div", { className: "flex items-start justify-between gap-2", children: [
3307
- /* @__PURE__ */ jsxs11("div", { className: "min-w-0", children: [
3308
- /* @__PURE__ */ jsx21("div", { className: "text-sm font-medium text-foreground", children: labels?.voiceReview || "Ready to send" }),
3309
- /* @__PURE__ */ jsx21("div", { className: "text-xs text-muted-foreground", children: formatDuration(durationMs) })
3310
- ] }),
3328
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between gap-2 text-xs text-muted-foreground", children: [
3329
+ /* @__PURE__ */ jsx21("span", { children: formatDuration(durationMs) }),
3311
3330
  /* @__PURE__ */ jsx21(
3312
3331
  Button,
3313
3332
  {
@@ -3323,31 +3342,39 @@ var VoiceComposer = ({
3323
3342
  }
3324
3343
  )
3325
3344
  ] }),
3326
- 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
- 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
- isAutoSendActive && autoSendDelayMs > 0 && /* @__PURE__ */ jsxs11("div", { className: "mt-3 space-y-2", children: [
3329
- /* @__PURE__ */ jsx21(Progress, { value: countdownValue, className: "h-2" }),
3330
- /* @__PURE__ */ jsx21("div", { className: "text-center text-xs text-muted-foreground", children: interpolateSeconds(labels?.voiceAutoSendIn, countdownSeconds) })
3331
- ] }),
3332
- /* @__PURE__ */ jsxs11("div", { className: "mt-3 flex items-center justify-end gap-2", children: [
3333
- isAutoSendActive && /* @__PURE__ */ jsxs11(Button, { type: "button", variant: "ghost", size: "sm", onClick: onCancelAutoSend, disabled, children: [
3334
- /* @__PURE__ */ jsx21(X2, { className: "h-4 w-4" }),
3335
- labels?.voiceCancel || "Cancel"
3336
- ] }),
3337
- !isAutoSendActive && /* @__PURE__ */ jsx21(
3345
+ /* @__PURE__ */ jsxs11("div", { className: "mt-4 flex flex-col items-center gap-4 text-center", children: [
3346
+ /* @__PURE__ */ jsx21(
3338
3347
  Button,
3339
3348
  {
3340
3349
  type: "button",
3341
- variant: "outline",
3342
3350
  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" })
3351
+ variant: orbCanStop ? "destructive" : "outline",
3352
+ 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"}`,
3353
+ onClick: handleReviewOrbClick,
3354
+ disabled: disabled || orbIsReviewBusy,
3355
+ 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" })
3348
3356
  }
3349
3357
  ),
3350
- /* @__PURE__ */ jsxs11(Button, { type: "button", size: "sm", onClick: onSendNow, disabled, children: [
3358
+ /* @__PURE__ */ jsxs11("div", { className: "max-w-sm space-y-1 px-2", children: [
3359
+ /* @__PURE__ */ jsx21("p", { className: "text-sm text-foreground", children: reviewHelperText }),
3360
+ isCapturing && /* @__PURE__ */ jsx21("div", { className: "mx-auto h-1.5 w-32 overflow-hidden rounded-full bg-red-100", children: /* @__PURE__ */ jsx21(
3361
+ "div",
3362
+ {
3363
+ className: "h-full rounded-full bg-red-500 transition-[width] duration-150",
3364
+ style: { width: `${levelValue}%` }
3365
+ }
3366
+ ) })
3367
+ ] })
3368
+ ] }),
3369
+ attachment && /* @__PURE__ */ jsx21("div", { className: "mt-4 rounded-lg border bg-background/90 p-2 shadow-sm", children: /* @__PURE__ */ jsx21("audio", { controls: true, preload: "metadata", className: "w-full", children: /* @__PURE__ */ jsx21("source", { src: attachment.dataUrl, type: attachment.mimeType }) }) }),
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 }),
3371
+ isAutoSendActive && autoSendDelayMs > 0 && /* @__PURE__ */ jsx21("div", { className: "mt-3 flex justify-center", children: /* @__PURE__ */ jsx21("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) }) }),
3372
+ /* @__PURE__ */ jsxs11("div", { className: "mt-4 grid grid-cols-1 gap-2 sm:flex sm:items-center sm:justify-end", children: [
3373
+ isAutoSendActive && /* @__PURE__ */ jsxs11(Button, { type: "button", variant: "ghost", size: "sm", onClick: onCancelAutoSend, disabled, className: "w-full sm:w-auto", children: [
3374
+ /* @__PURE__ */ jsx21(X2, { className: "h-4 w-4" }),
3375
+ labels?.voiceCancel || "Cancel"
3376
+ ] }),
3377
+ /* @__PURE__ */ jsxs11(Button, { type: "button", size: "sm", onClick: onSendNow, disabled, className: "w-full sm:w-auto", children: [
3351
3378
  /* @__PURE__ */ jsx21(Send, { className: "h-4 w-4" }),
3352
3379
  labels?.voiceSendNow || "Send now"
3353
3380
  ] })
@@ -3636,6 +3663,7 @@ var ChatInput = memo2(function ChatInput2({
3636
3663
  }) {
3637
3664
  const voiceComposeEnabled = config?.voiceCompose?.enabled === true;
3638
3665
  const voiceDefaultMode = config?.voiceCompose?.defaultMode ?? "text";
3666
+ const voiceReviewMode = config?.voiceCompose?.reviewMode ?? "manual";
3639
3667
  const voiceAutoSendDelayMs = config?.voiceCompose?.autoSendDelayMs ?? 5e3;
3640
3668
  const voicePersistComposer = config?.voiceCompose?.persistComposer ?? true;
3641
3669
  const voiceShowTranscriptPreview = config?.voiceCompose?.showTranscriptPreview ?? true;
@@ -3870,13 +3898,30 @@ var ChatInput = memo2(function ChatInput2({
3870
3898
  setIsVoiceAutoSendActive(false);
3871
3899
  setVoiceError(null);
3872
3900
  }, []);
3901
+ const armVoiceDraftForAppend = useCallback3((segment) => {
3902
+ voiceAppendBaseRef.current = segment;
3903
+ voiceAppendBaseDurationRef.current = segment ? resolveVoiceSegmentDuration(segment) : 0;
3904
+ }, []);
3905
+ const handleVoiceProviderStateChange = useCallback3((nextState) => {
3906
+ if (voiceReviewMode === "armed" && (nextState === "waiting_for_speech" || nextState === "listening")) {
3907
+ const currentDraft = voiceDraftRef.current;
3908
+ if (currentDraft) {
3909
+ armVoiceDraftForAppend(currentDraft);
3910
+ }
3911
+ }
3912
+ if (voiceReviewMode === "armed" && nextState === "listening" && voiceDraftRef.current) {
3913
+ setVoiceCountdownMs(voiceAutoSendDelayMs);
3914
+ setIsVoiceAutoSendActive(false);
3915
+ }
3916
+ setVoiceState(nextState);
3917
+ }, [armVoiceDraftForAppend, voiceAutoSendDelayMs, voiceReviewMode]);
3873
3918
  const ensureVoiceProvider = useCallback3(async () => {
3874
3919
  if (voiceProviderRef.current) {
3875
3920
  return voiceProviderRef.current;
3876
3921
  }
3877
3922
  const createProvider = resolveVoiceProviderFactory(config?.voiceCompose?.createProvider);
3878
3923
  const provider = await createProvider({
3879
- onStateChange: setVoiceState,
3924
+ onStateChange: handleVoiceProviderStateChange,
3880
3925
  onAudioLevelChange: setVoiceAudioLevel,
3881
3926
  onDurationChange: (durationMs) => {
3882
3927
  setVoiceDurationMs(voiceAppendBaseDurationRef.current + durationMs);
@@ -3892,8 +3937,6 @@ var ChatInput = memo2(function ChatInput2({
3892
3937
  const previousSegment = voiceAppendBaseRef.current;
3893
3938
  try {
3894
3939
  const nextSegment = previousSegment ? await appendVoiceSegments(previousSegment, segment) : segment;
3895
- voiceAppendBaseRef.current = null;
3896
- voiceAppendBaseDurationRef.current = 0;
3897
3940
  voiceDraftRef.current = nextSegment;
3898
3941
  setVoiceDraft(nextSegment);
3899
3942
  setVoiceTranscript(nextSegment.transcript ?? clearVoiceTranscript());
@@ -3902,11 +3945,15 @@ var ChatInput = memo2(function ChatInput2({
3902
3945
  setVoiceCountdownMs(voiceAutoSendDelayMs);
3903
3946
  setIsVoiceAutoSendActive(voiceAutoSendDelayMs > 0);
3904
3947
  setVoiceError(null);
3905
- setVoiceState("review");
3948
+ if (voiceReviewMode === "armed") {
3949
+ armVoiceDraftForAppend(nextSegment);
3950
+ } else {
3951
+ armVoiceDraftForAppend(null);
3952
+ }
3953
+ setVoiceState((currentState) => voiceReviewMode === "armed" && (currentState === "waiting_for_speech" || currentState === "listening") ? currentState : "review");
3906
3954
  } catch (error) {
3907
3955
  const resolvedError = resolveVoiceErrorMessage(error, config);
3908
- voiceAppendBaseRef.current = null;
3909
- voiceAppendBaseDurationRef.current = 0;
3956
+ armVoiceDraftForAppend(null);
3910
3957
  setVoiceAudioLevel(0);
3911
3958
  setVoiceCountdownMs(0);
3912
3959
  setIsVoiceAutoSendActive(false);
@@ -3930,8 +3977,7 @@ var ChatInput = memo2(function ChatInput2({
3930
3977
  },
3931
3978
  onError: (error) => {
3932
3979
  const previousSegment = voiceAppendBaseRef.current;
3933
- voiceAppendBaseRef.current = null;
3934
- voiceAppendBaseDurationRef.current = 0;
3980
+ armVoiceDraftForAppend(null);
3935
3981
  setVoiceError(resolveVoiceErrorMessage(error, config));
3936
3982
  setVoiceAudioLevel(0);
3937
3983
  setVoiceCountdownMs(0);
@@ -3955,7 +4001,7 @@ var ChatInput = memo2(function ChatInput2({
3955
4001
  });
3956
4002
  voiceProviderRef.current = provider;
3957
4003
  return provider;
3958
- }, [config, voiceAutoSendDelayMs, voiceMaxRecordingMs]);
4004
+ }, [armVoiceDraftForAppend, config, handleVoiceProviderStateChange, voiceAutoSendDelayMs, voiceMaxRecordingMs, voiceReviewMode]);
3959
4005
  const closeVoiceComposer = useCallback3(async () => {
3960
4006
  voiceAppendBaseRef.current = null;
3961
4007
  voiceAppendBaseDurationRef.current = 0;
@@ -4047,16 +4093,21 @@ var ChatInput = memo2(function ChatInput2({
4047
4093
  void closeVoiceComposer();
4048
4094
  }, [voicePersistComposer, resetVoiceComposerState, closeVoiceComposer]);
4049
4095
  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();
4096
+ void (async () => {
4097
+ if (!voiceDraft || disabled || isGenerating) {
4098
+ return;
4099
+ }
4100
+ setVoiceState("sending");
4101
+ setVoiceCountdownMs(0);
4102
+ setIsVoiceAutoSendActive(false);
4103
+ if (voiceProviderRef.current) {
4104
+ await voiceProviderRef.current.cancel();
4105
+ }
4106
+ onSubmit("", [...attachments, voiceDraft.attachment]);
4107
+ onChange("");
4108
+ onAttachmentsChange([]);
4109
+ finalizeVoiceComposerAfterSend();
4110
+ })();
4060
4111
  }, [
4061
4112
  voiceDraft,
4062
4113
  disabled,
@@ -4068,25 +4119,51 @@ var ChatInput = memo2(function ChatInput2({
4068
4119
  finalizeVoiceComposerAfterSend
4069
4120
  ]);
4070
4121
  const cancelVoiceAutoSend = useCallback3(() => {
4122
+ void (async () => {
4123
+ if (voiceReviewMode === "armed" && voiceProviderRef.current) {
4124
+ await voiceProviderRef.current.cancel();
4125
+ }
4126
+ armVoiceDraftForAppend(null);
4127
+ setVoiceAudioLevel(0);
4128
+ setVoiceState("review");
4129
+ })();
4071
4130
  setVoiceCountdownMs(0);
4072
4131
  setIsVoiceAutoSendActive(false);
4073
- }, []);
4132
+ }, [armVoiceDraftForAppend, voiceReviewMode]);
4133
+ const pauseVoiceReview = useCallback3(async () => {
4134
+ if (voiceState === "listening") {
4135
+ await stopVoiceCapture();
4136
+ return;
4137
+ }
4138
+ if (voiceReviewMode === "armed" && voiceProviderRef.current) {
4139
+ await voiceProviderRef.current.cancel();
4140
+ }
4141
+ armVoiceDraftForAppend(null);
4142
+ setVoiceAudioLevel(0);
4143
+ setVoiceState("review");
4144
+ }, [armVoiceDraftForAppend, stopVoiceCapture, voiceReviewMode, voiceState]);
4074
4145
  useEffect9(() => {
4075
- if (voiceState !== "review" || !voiceDraft || voiceAutoSendDelayMs <= 0 || !isVoiceAutoSendActive) {
4146
+ if (!voiceDraft || voiceAutoSendDelayMs <= 0 || !isVoiceAutoSendActive) {
4147
+ return;
4148
+ }
4149
+ const canContinueCounting = voiceState === "review" || voiceReviewMode === "armed" && voiceState === "waiting_for_speech";
4150
+ if (!canContinueCounting) {
4076
4151
  return;
4077
4152
  }
4078
- const startedAt = Date.now();
4079
- setVoiceCountdownMs(voiceAutoSendDelayMs);
4080
4153
  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
- }
4154
+ setVoiceCountdownMs((previous) => {
4155
+ const remaining = Math.max(0, previous - 100);
4156
+ if (remaining <= 0) {
4157
+ clearInterval(timer);
4158
+ queueMicrotask(() => {
4159
+ sendVoiceDraft();
4160
+ });
4161
+ }
4162
+ return remaining;
4163
+ });
4087
4164
  }, 100);
4088
4165
  return () => clearInterval(timer);
4089
- }, [voiceState, voiceDraft, voiceAutoSendDelayMs, isVoiceAutoSendActive, sendVoiceDraft]);
4166
+ }, [voiceState, voiceDraft, voiceReviewMode, voiceAutoSendDelayMs, isVoiceAutoSendActive, sendVoiceDraft]);
4090
4167
  const removeAttachment = (index) => {
4091
4168
  const newAttachments = attachments.filter((_, i) => i !== index);
4092
4169
  onAttachmentsChange(newAttachments);
@@ -4141,6 +4218,7 @@ var ChatInput = memo2(function ChatInput2({
4141
4218
  countdownMs: voiceCountdownMs,
4142
4219
  autoSendDelayMs: voiceAutoSendDelayMs,
4143
4220
  isAutoSendActive: isVoiceAutoSendActive,
4221
+ reviewMode: voiceReviewMode,
4144
4222
  errorMessage: voiceError,
4145
4223
  disabled: disabled || isGenerating,
4146
4224
  labels: config?.labels,
@@ -4150,6 +4228,9 @@ var ChatInput = memo2(function ChatInput2({
4150
4228
  onStop: () => {
4151
4229
  void stopVoiceCapture();
4152
4230
  },
4231
+ onPauseReview: () => {
4232
+ void pauseVoiceReview();
4233
+ },
4153
4234
  onCancelAutoSend: () => {
4154
4235
  cancelVoiceAutoSend();
4155
4236
  },