@copilotz/chat-ui 0.1.33 → 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 +231 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +231 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -45,7 +45,7 @@ var defaultChatConfig = {
|
|
|
45
45
|
voiceSendNow: "Send now",
|
|
46
46
|
voiceCancel: "Cancel",
|
|
47
47
|
voiceDiscard: "Delete recording",
|
|
48
|
-
voiceRecordAgain: "
|
|
48
|
+
voiceRecordAgain: "Continue recording",
|
|
49
49
|
voiceAutoSendIn: "Auto-sends in {{seconds}}s",
|
|
50
50
|
voiceTranscriptPending: "Transcript unavailable",
|
|
51
51
|
voicePermissionDenied: "Microphone access was denied.",
|
|
@@ -2850,6 +2850,121 @@ var blobToDataUrl = (blob) => new Promise((resolve, reject) => {
|
|
|
2850
2850
|
reader.onerror = () => reject(reader.error ?? new Error("Failed to read recorded audio"));
|
|
2851
2851
|
reader.readAsDataURL(blob);
|
|
2852
2852
|
});
|
|
2853
|
+
var joinTranscriptParts = (...parts) => {
|
|
2854
|
+
const value = parts.map((part) => part?.trim()).filter((part) => Boolean(part && part.length > 0)).join(" ").trim();
|
|
2855
|
+
return value.length > 0 ? value : void 0;
|
|
2856
|
+
};
|
|
2857
|
+
var getAudioContextCtor = () => globalThis.AudioContext || globalThis.webkitAudioContext;
|
|
2858
|
+
var getOfflineAudioContextCtor = () => globalThis.OfflineAudioContext || globalThis.webkitOfflineAudioContext;
|
|
2859
|
+
var attachmentToArrayBuffer = async (attachment) => {
|
|
2860
|
+
const response = await fetch(attachment.dataUrl);
|
|
2861
|
+
return response.arrayBuffer();
|
|
2862
|
+
};
|
|
2863
|
+
var decodeAudioAttachment = async (attachment) => {
|
|
2864
|
+
const AudioContextCtor = getAudioContextCtor();
|
|
2865
|
+
if (!AudioContextCtor) {
|
|
2866
|
+
throw new Error("Audio decoding is not supported in this browser");
|
|
2867
|
+
}
|
|
2868
|
+
const audioContext = new AudioContextCtor();
|
|
2869
|
+
try {
|
|
2870
|
+
const arrayBuffer = await attachmentToArrayBuffer(attachment);
|
|
2871
|
+
return await audioContext.decodeAudioData(arrayBuffer.slice(0));
|
|
2872
|
+
} finally {
|
|
2873
|
+
await closeAudioContext(audioContext);
|
|
2874
|
+
}
|
|
2875
|
+
};
|
|
2876
|
+
var renderMergedBuffer = async (buffers) => {
|
|
2877
|
+
const OfflineAudioContextCtor = getOfflineAudioContextCtor();
|
|
2878
|
+
if (!OfflineAudioContextCtor) {
|
|
2879
|
+
throw new Error("Offline audio rendering is not supported in this browser");
|
|
2880
|
+
}
|
|
2881
|
+
const numberOfChannels = Math.max(...buffers.map((buffer) => buffer.numberOfChannels));
|
|
2882
|
+
const sampleRate = Math.max(...buffers.map((buffer) => buffer.sampleRate));
|
|
2883
|
+
const totalFrames = Math.max(1, Math.ceil(buffers.reduce((sum, buffer) => sum + buffer.duration * sampleRate, 0)));
|
|
2884
|
+
const offlineContext = new OfflineAudioContextCtor(numberOfChannels, totalFrames, sampleRate);
|
|
2885
|
+
let offsetSeconds = 0;
|
|
2886
|
+
for (const buffer of buffers) {
|
|
2887
|
+
const source = offlineContext.createBufferSource();
|
|
2888
|
+
source.buffer = buffer;
|
|
2889
|
+
source.connect(offlineContext.destination);
|
|
2890
|
+
source.start(offsetSeconds);
|
|
2891
|
+
offsetSeconds += buffer.duration;
|
|
2892
|
+
}
|
|
2893
|
+
return offlineContext.startRendering();
|
|
2894
|
+
};
|
|
2895
|
+
var encodeWav = (audioBuffer) => {
|
|
2896
|
+
const numberOfChannels = audioBuffer.numberOfChannels;
|
|
2897
|
+
const sampleRate = audioBuffer.sampleRate;
|
|
2898
|
+
const bitsPerSample = 16;
|
|
2899
|
+
const bytesPerSample = bitsPerSample / 8;
|
|
2900
|
+
const dataLength = audioBuffer.length * numberOfChannels * bytesPerSample;
|
|
2901
|
+
const buffer = new ArrayBuffer(44 + dataLength);
|
|
2902
|
+
const view = new DataView(buffer);
|
|
2903
|
+
const writeString = (offset2, value) => {
|
|
2904
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
2905
|
+
view.setUint8(offset2 + index, value.charCodeAt(index));
|
|
2906
|
+
}
|
|
2907
|
+
};
|
|
2908
|
+
writeString(0, "RIFF");
|
|
2909
|
+
view.setUint32(4, 36 + dataLength, true);
|
|
2910
|
+
writeString(8, "WAVE");
|
|
2911
|
+
writeString(12, "fmt ");
|
|
2912
|
+
view.setUint32(16, 16, true);
|
|
2913
|
+
view.setUint16(20, 1, true);
|
|
2914
|
+
view.setUint16(22, numberOfChannels, true);
|
|
2915
|
+
view.setUint32(24, sampleRate, true);
|
|
2916
|
+
view.setUint32(28, sampleRate * numberOfChannels * bytesPerSample, true);
|
|
2917
|
+
view.setUint16(32, numberOfChannels * bytesPerSample, true);
|
|
2918
|
+
view.setUint16(34, bitsPerSample, true);
|
|
2919
|
+
writeString(36, "data");
|
|
2920
|
+
view.setUint32(40, dataLength, true);
|
|
2921
|
+
let offset = 44;
|
|
2922
|
+
const channelData = Array.from({ length: numberOfChannels }, (_, index) => audioBuffer.getChannelData(index));
|
|
2923
|
+
for (let sampleIndex = 0; sampleIndex < audioBuffer.length; sampleIndex += 1) {
|
|
2924
|
+
for (let channelIndex = 0; channelIndex < numberOfChannels; channelIndex += 1) {
|
|
2925
|
+
const sample = Math.max(-1, Math.min(1, channelData[channelIndex][sampleIndex]));
|
|
2926
|
+
const pcmValue = sample < 0 ? sample * 32768 : sample * 32767;
|
|
2927
|
+
view.setInt16(offset, pcmValue, true);
|
|
2928
|
+
offset += 2;
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
return new Blob([buffer], { type: "audio/wav" });
|
|
2932
|
+
};
|
|
2933
|
+
var resolveSegmentCount = (segment) => {
|
|
2934
|
+
const candidate = segment?.metadata?.segmentCount;
|
|
2935
|
+
return typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0 ? candidate : segment ? 1 : 0;
|
|
2936
|
+
};
|
|
2937
|
+
var mergeVoiceTranscripts = (previous, incoming) => ({
|
|
2938
|
+
final: joinTranscriptParts(previous?.final, incoming?.final),
|
|
2939
|
+
partial: joinTranscriptParts(previous?.final, incoming?.partial)
|
|
2940
|
+
});
|
|
2941
|
+
var appendVoiceSegments = async (previous, incoming) => {
|
|
2942
|
+
const [previousBuffer, incomingBuffer] = await Promise.all([
|
|
2943
|
+
decodeAudioAttachment(previous.attachment),
|
|
2944
|
+
decodeAudioAttachment(incoming.attachment)
|
|
2945
|
+
]);
|
|
2946
|
+
const mergedBuffer = await renderMergedBuffer([previousBuffer, incomingBuffer]);
|
|
2947
|
+
const mergedBlob = encodeWav(mergedBuffer);
|
|
2948
|
+
const dataUrl = await blobToDataUrl(mergedBlob);
|
|
2949
|
+
const segmentCount = resolveSegmentCount(previous) + resolveSegmentCount(incoming);
|
|
2950
|
+
return {
|
|
2951
|
+
attachment: {
|
|
2952
|
+
kind: "audio",
|
|
2953
|
+
dataUrl,
|
|
2954
|
+
mimeType: mergedBlob.type,
|
|
2955
|
+
durationMs: Math.round(mergedBuffer.duration * 1e3),
|
|
2956
|
+
fileName: `voice-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.wav`,
|
|
2957
|
+
size: mergedBlob.size
|
|
2958
|
+
},
|
|
2959
|
+
transcript: mergeVoiceTranscripts(previous.transcript, incoming.transcript),
|
|
2960
|
+
metadata: {
|
|
2961
|
+
...previous.metadata,
|
|
2962
|
+
...incoming.metadata,
|
|
2963
|
+
segmentCount,
|
|
2964
|
+
source: segmentCount > 1 ? "merged" : incoming.metadata?.source ?? previous.metadata?.source
|
|
2965
|
+
}
|
|
2966
|
+
};
|
|
2967
|
+
};
|
|
2853
2968
|
var stopStream = (stream) => {
|
|
2854
2969
|
if (!stream) return;
|
|
2855
2970
|
stream.getTracks().forEach((track) => track.stop());
|
|
@@ -2971,7 +3086,7 @@ var createManualVoiceProvider = async (handlers, options = {}) => {
|
|
|
2971
3086
|
fileName: `voice-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.webm`,
|
|
2972
3087
|
size: blob.size
|
|
2973
3088
|
},
|
|
2974
|
-
metadata: { source: "manual" }
|
|
3089
|
+
metadata: { source: "manual", segmentCount: 1 }
|
|
2975
3090
|
});
|
|
2976
3091
|
} else {
|
|
2977
3092
|
handlers.onStateChange?.("idle");
|
|
@@ -3238,7 +3353,7 @@ var VoiceComposer = ({
|
|
|
3238
3353
|
] })
|
|
3239
3354
|
] })
|
|
3240
3355
|
] }),
|
|
3241
|
-
|
|
3356
|
+
errorMessage && /* @__PURE__ */ jsx21("div", { className: "mt-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive", children: errorMessage })
|
|
3242
3357
|
] });
|
|
3243
3358
|
};
|
|
3244
3359
|
|
|
@@ -3499,6 +3614,7 @@ var resolveVoiceErrorMessage = (error, config) => {
|
|
|
3499
3614
|
return config?.labels?.voiceCaptureError || "Unable to capture audio.";
|
|
3500
3615
|
};
|
|
3501
3616
|
var clearVoiceTranscript = () => ({});
|
|
3617
|
+
var resolveVoiceSegmentDuration = (segment) => segment.attachment.durationMs ?? 0;
|
|
3502
3618
|
var ChatInput = memo2(function ChatInput2({
|
|
3503
3619
|
value,
|
|
3504
3620
|
onChange,
|
|
@@ -3547,6 +3663,9 @@ var ChatInput = memo2(function ChatInput2({
|
|
|
3547
3663
|
const recordingInterval = useRef5(null);
|
|
3548
3664
|
const mediaStreamRef = useRef5(null);
|
|
3549
3665
|
const voiceProviderRef = useRef5(null);
|
|
3666
|
+
const voiceDraftRef = useRef5(null);
|
|
3667
|
+
const voiceAppendBaseRef = useRef5(null);
|
|
3668
|
+
const voiceAppendBaseDurationRef = useRef5(0);
|
|
3550
3669
|
useEffect9(() => {
|
|
3551
3670
|
return () => {
|
|
3552
3671
|
if (mediaStreamRef.current) {
|
|
@@ -3561,6 +3680,9 @@ var ChatInput = memo2(function ChatInput2({
|
|
|
3561
3680
|
}
|
|
3562
3681
|
};
|
|
3563
3682
|
}, []);
|
|
3683
|
+
useEffect9(() => {
|
|
3684
|
+
voiceDraftRef.current = voiceDraft;
|
|
3685
|
+
}, [voiceDraft]);
|
|
3564
3686
|
const handleSubmit = (e) => {
|
|
3565
3687
|
e.preventDefault();
|
|
3566
3688
|
if (!value.trim() && attachments.length === 0 || disabled || isGenerating) return;
|
|
@@ -3738,6 +3860,9 @@ var ChatInput = memo2(function ChatInput2({
|
|
|
3738
3860
|
const resetVoiceComposerState = useCallback3((nextState = "idle") => {
|
|
3739
3861
|
setVoiceState(nextState);
|
|
3740
3862
|
setVoiceDraft(null);
|
|
3863
|
+
voiceDraftRef.current = null;
|
|
3864
|
+
voiceAppendBaseRef.current = null;
|
|
3865
|
+
voiceAppendBaseDurationRef.current = 0;
|
|
3741
3866
|
setVoiceTranscript(clearVoiceTranscript());
|
|
3742
3867
|
setVoiceDurationMs(0);
|
|
3743
3868
|
setVoiceAudioLevel(0);
|
|
@@ -3753,23 +3878,76 @@ var ChatInput = memo2(function ChatInput2({
|
|
|
3753
3878
|
const provider = await createProvider({
|
|
3754
3879
|
onStateChange: setVoiceState,
|
|
3755
3880
|
onAudioLevelChange: setVoiceAudioLevel,
|
|
3756
|
-
onDurationChange:
|
|
3757
|
-
|
|
3881
|
+
onDurationChange: (durationMs) => {
|
|
3882
|
+
setVoiceDurationMs(voiceAppendBaseDurationRef.current + durationMs);
|
|
3883
|
+
},
|
|
3884
|
+
onTranscriptChange: (transcript) => {
|
|
3885
|
+
const baseTranscript = voiceAppendBaseRef.current?.transcript;
|
|
3886
|
+
setVoiceTranscript(
|
|
3887
|
+
baseTranscript ? mergeVoiceTranscripts(baseTranscript, transcript) : transcript
|
|
3888
|
+
);
|
|
3889
|
+
},
|
|
3758
3890
|
onSegmentReady: (segment) => {
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3891
|
+
void (async () => {
|
|
3892
|
+
const previousSegment = voiceAppendBaseRef.current;
|
|
3893
|
+
try {
|
|
3894
|
+
const nextSegment = previousSegment ? await appendVoiceSegments(previousSegment, segment) : segment;
|
|
3895
|
+
voiceAppendBaseRef.current = null;
|
|
3896
|
+
voiceAppendBaseDurationRef.current = 0;
|
|
3897
|
+
voiceDraftRef.current = nextSegment;
|
|
3898
|
+
setVoiceDraft(nextSegment);
|
|
3899
|
+
setVoiceTranscript(nextSegment.transcript ?? clearVoiceTranscript());
|
|
3900
|
+
setVoiceDurationMs(resolveVoiceSegmentDuration(nextSegment));
|
|
3901
|
+
setVoiceAudioLevel(0);
|
|
3902
|
+
setVoiceCountdownMs(voiceAutoSendDelayMs);
|
|
3903
|
+
setIsVoiceAutoSendActive(voiceAutoSendDelayMs > 0);
|
|
3904
|
+
setVoiceError(null);
|
|
3905
|
+
setVoiceState("review");
|
|
3906
|
+
} catch (error) {
|
|
3907
|
+
const resolvedError = resolveVoiceErrorMessage(error, config);
|
|
3908
|
+
voiceAppendBaseRef.current = null;
|
|
3909
|
+
voiceAppendBaseDurationRef.current = 0;
|
|
3910
|
+
setVoiceAudioLevel(0);
|
|
3911
|
+
setVoiceCountdownMs(0);
|
|
3912
|
+
setIsVoiceAutoSendActive(false);
|
|
3913
|
+
if (previousSegment) {
|
|
3914
|
+
voiceDraftRef.current = previousSegment;
|
|
3915
|
+
setVoiceDraft(previousSegment);
|
|
3916
|
+
setVoiceTranscript(previousSegment.transcript ?? clearVoiceTranscript());
|
|
3917
|
+
setVoiceDurationMs(resolveVoiceSegmentDuration(previousSegment));
|
|
3918
|
+
setVoiceError(resolvedError);
|
|
3919
|
+
setVoiceState("review");
|
|
3920
|
+
return;
|
|
3921
|
+
}
|
|
3922
|
+
voiceDraftRef.current = null;
|
|
3923
|
+
setVoiceDraft(null);
|
|
3924
|
+
setVoiceTranscript(clearVoiceTranscript());
|
|
3925
|
+
setVoiceDurationMs(0);
|
|
3926
|
+
setVoiceError(resolvedError);
|
|
3927
|
+
setVoiceState("error");
|
|
3928
|
+
}
|
|
3929
|
+
})();
|
|
3767
3930
|
},
|
|
3768
3931
|
onError: (error) => {
|
|
3932
|
+
const previousSegment = voiceAppendBaseRef.current;
|
|
3933
|
+
voiceAppendBaseRef.current = null;
|
|
3934
|
+
voiceAppendBaseDurationRef.current = 0;
|
|
3769
3935
|
setVoiceError(resolveVoiceErrorMessage(error, config));
|
|
3770
3936
|
setVoiceAudioLevel(0);
|
|
3771
3937
|
setVoiceCountdownMs(0);
|
|
3772
3938
|
setIsVoiceAutoSendActive(false);
|
|
3939
|
+
if (previousSegment) {
|
|
3940
|
+
voiceDraftRef.current = previousSegment;
|
|
3941
|
+
setVoiceDraft(previousSegment);
|
|
3942
|
+
setVoiceTranscript(previousSegment.transcript ?? clearVoiceTranscript());
|
|
3943
|
+
setVoiceDurationMs(resolveVoiceSegmentDuration(previousSegment));
|
|
3944
|
+
setVoiceState("review");
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
voiceDraftRef.current = null;
|
|
3948
|
+
setVoiceDraft(null);
|
|
3949
|
+
setVoiceTranscript(clearVoiceTranscript());
|
|
3950
|
+
setVoiceDurationMs(0);
|
|
3773
3951
|
setVoiceState("error");
|
|
3774
3952
|
}
|
|
3775
3953
|
}, {
|
|
@@ -3779,35 +3957,67 @@ var ChatInput = memo2(function ChatInput2({
|
|
|
3779
3957
|
return provider;
|
|
3780
3958
|
}, [config, voiceAutoSendDelayMs, voiceMaxRecordingMs]);
|
|
3781
3959
|
const closeVoiceComposer = useCallback3(async () => {
|
|
3960
|
+
voiceAppendBaseRef.current = null;
|
|
3961
|
+
voiceAppendBaseDurationRef.current = 0;
|
|
3782
3962
|
setIsVoiceComposerOpen(false);
|
|
3783
3963
|
setVoiceError(null);
|
|
3784
3964
|
setVoiceCountdownMs(0);
|
|
3785
3965
|
setVoiceAudioLevel(0);
|
|
3786
3966
|
setVoiceTranscript(clearVoiceTranscript());
|
|
3787
3967
|
setVoiceDraft(null);
|
|
3968
|
+
voiceDraftRef.current = null;
|
|
3788
3969
|
setVoiceDurationMs(0);
|
|
3789
3970
|
setVoiceState("idle");
|
|
3790
3971
|
if (voiceProviderRef.current) {
|
|
3791
3972
|
await voiceProviderRef.current.cancel();
|
|
3792
3973
|
}
|
|
3793
3974
|
}, []);
|
|
3794
|
-
const startVoiceCapture = useCallback3(async () => {
|
|
3975
|
+
const startVoiceCapture = useCallback3(async (appendToDraft = false) => {
|
|
3795
3976
|
if (disabled || isGenerating) {
|
|
3796
3977
|
return;
|
|
3797
3978
|
}
|
|
3979
|
+
const previousDraft = appendToDraft ? voiceDraftRef.current : null;
|
|
3980
|
+
const previousDurationMs = previousDraft ? resolveVoiceSegmentDuration(previousDraft) : 0;
|
|
3798
3981
|
setIsVoiceComposerOpen(true);
|
|
3799
3982
|
setVoiceError(null);
|
|
3800
|
-
setVoiceDraft(null);
|
|
3801
3983
|
setVoiceCountdownMs(0);
|
|
3802
|
-
setVoiceTranscript(clearVoiceTranscript());
|
|
3803
3984
|
setVoiceAudioLevel(0);
|
|
3804
|
-
setVoiceDurationMs(0);
|
|
3805
3985
|
setIsVoiceAutoSendActive(false);
|
|
3986
|
+
voiceAppendBaseRef.current = previousDraft;
|
|
3987
|
+
voiceAppendBaseDurationRef.current = previousDurationMs;
|
|
3988
|
+
if (!previousDraft) {
|
|
3989
|
+
setVoiceDraft(null);
|
|
3990
|
+
voiceDraftRef.current = null;
|
|
3991
|
+
setVoiceTranscript(clearVoiceTranscript());
|
|
3992
|
+
setVoiceDurationMs(0);
|
|
3993
|
+
} else {
|
|
3994
|
+
setVoiceTranscript(previousDraft.transcript ?? clearVoiceTranscript());
|
|
3995
|
+
setVoiceDurationMs(previousDurationMs);
|
|
3996
|
+
}
|
|
3806
3997
|
try {
|
|
3807
3998
|
const provider = await ensureVoiceProvider();
|
|
3808
3999
|
await provider.start();
|
|
3809
4000
|
} catch (error) {
|
|
3810
|
-
|
|
4001
|
+
const resolvedError = resolveVoiceErrorMessage(error, config);
|
|
4002
|
+
voiceAppendBaseRef.current = null;
|
|
4003
|
+
voiceAppendBaseDurationRef.current = 0;
|
|
4004
|
+
setVoiceAudioLevel(0);
|
|
4005
|
+
setVoiceCountdownMs(0);
|
|
4006
|
+
setIsVoiceAutoSendActive(false);
|
|
4007
|
+
if (previousDraft) {
|
|
4008
|
+
voiceDraftRef.current = previousDraft;
|
|
4009
|
+
setVoiceDraft(previousDraft);
|
|
4010
|
+
setVoiceTranscript(previousDraft.transcript ?? clearVoiceTranscript());
|
|
4011
|
+
setVoiceDurationMs(previousDurationMs);
|
|
4012
|
+
setVoiceError(resolvedError);
|
|
4013
|
+
setVoiceState("review");
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
voiceDraftRef.current = null;
|
|
4017
|
+
setVoiceDraft(null);
|
|
4018
|
+
setVoiceTranscript(clearVoiceTranscript());
|
|
4019
|
+
setVoiceDurationMs(0);
|
|
4020
|
+
setVoiceError(resolvedError);
|
|
3811
4021
|
setVoiceState("error");
|
|
3812
4022
|
}
|
|
3813
4023
|
}, [disabled, isGenerating, ensureVoiceProvider, config]);
|
|
@@ -3821,6 +4031,8 @@ var ChatInput = memo2(function ChatInput2({
|
|
|
3821
4031
|
}
|
|
3822
4032
|
}, [config]);
|
|
3823
4033
|
const cancelVoiceCapture = useCallback3(async () => {
|
|
4034
|
+
voiceAppendBaseRef.current = null;
|
|
4035
|
+
voiceAppendBaseDurationRef.current = 0;
|
|
3824
4036
|
if (voiceProviderRef.current) {
|
|
3825
4037
|
await voiceProviderRef.current.cancel();
|
|
3826
4038
|
}
|
|
@@ -3945,7 +4157,7 @@ var ChatInput = memo2(function ChatInput2({
|
|
|
3945
4157
|
void cancelVoiceCapture();
|
|
3946
4158
|
},
|
|
3947
4159
|
onRecordAgain: () => {
|
|
3948
|
-
void startVoiceCapture();
|
|
4160
|
+
void startVoiceCapture(true);
|
|
3949
4161
|
},
|
|
3950
4162
|
onSendNow: sendVoiceDraft,
|
|
3951
4163
|
onExit: () => {
|