@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 CHANGED
@@ -101,7 +101,7 @@ var defaultChatConfig = {
101
101
  voiceSendNow: "Send now",
102
102
  voiceCancel: "Cancel",
103
103
  voiceDiscard: "Delete recording",
104
- voiceRecordAgain: "Record again",
104
+ voiceRecordAgain: "Continue recording",
105
105
  voiceAutoSendIn: "Auto-sends in {{seconds}}s",
106
106
  voiceTranscriptPending: "Transcript unavailable",
107
107
  voicePermissionDenied: "Microphone access was denied.",
@@ -2866,6 +2866,121 @@ var blobToDataUrl = (blob) => new Promise((resolve, reject) => {
2866
2866
  reader.onerror = () => reject(reader.error ?? new Error("Failed to read recorded audio"));
2867
2867
  reader.readAsDataURL(blob);
2868
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
+ };
2869
2984
  var stopStream = (stream) => {
2870
2985
  if (!stream) return;
2871
2986
  stream.getTracks().forEach((track) => track.stop());
@@ -2987,7 +3102,7 @@ var createManualVoiceProvider = async (handlers, options = {}) => {
2987
3102
  fileName: `voice-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.webm`,
2988
3103
  size: blob.size
2989
3104
  },
2990
- metadata: { source: "manual" }
3105
+ metadata: { source: "manual", segmentCount: 1 }
2991
3106
  });
2992
3107
  } else {
2993
3108
  handlers.onStateChange?.("idle");
@@ -3254,7 +3369,7 @@ var VoiceComposer = ({
3254
3369
  ] })
3255
3370
  ] })
3256
3371
  ] }),
3257
- state === "error" && 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 })
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 })
3258
3373
  ] });
3259
3374
  };
3260
3375
 
@@ -3503,6 +3618,7 @@ var resolveVoiceErrorMessage = (error, config) => {
3503
3618
  return config?.labels?.voiceCaptureError || "Unable to capture audio.";
3504
3619
  };
3505
3620
  var clearVoiceTranscript = () => ({});
3621
+ var resolveVoiceSegmentDuration = (segment) => segment.attachment.durationMs ?? 0;
3506
3622
  var ChatInput = (0, import_react5.memo)(function ChatInput2({
3507
3623
  value,
3508
3624
  onChange,
@@ -3551,6 +3667,9 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3551
3667
  const recordingInterval = (0, import_react5.useRef)(null);
3552
3668
  const mediaStreamRef = (0, import_react5.useRef)(null);
3553
3669
  const voiceProviderRef = (0, import_react5.useRef)(null);
3670
+ const voiceDraftRef = (0, import_react5.useRef)(null);
3671
+ const voiceAppendBaseRef = (0, import_react5.useRef)(null);
3672
+ const voiceAppendBaseDurationRef = (0, import_react5.useRef)(0);
3554
3673
  (0, import_react5.useEffect)(() => {
3555
3674
  return () => {
3556
3675
  if (mediaStreamRef.current) {
@@ -3565,6 +3684,9 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3565
3684
  }
3566
3685
  };
3567
3686
  }, []);
3687
+ (0, import_react5.useEffect)(() => {
3688
+ voiceDraftRef.current = voiceDraft;
3689
+ }, [voiceDraft]);
3568
3690
  const handleSubmit = (e) => {
3569
3691
  e.preventDefault();
3570
3692
  if (!value.trim() && attachments.length === 0 || disabled || isGenerating) return;
@@ -3742,6 +3864,9 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3742
3864
  const resetVoiceComposerState = (0, import_react5.useCallback)((nextState = "idle") => {
3743
3865
  setVoiceState(nextState);
3744
3866
  setVoiceDraft(null);
3867
+ voiceDraftRef.current = null;
3868
+ voiceAppendBaseRef.current = null;
3869
+ voiceAppendBaseDurationRef.current = 0;
3745
3870
  setVoiceTranscript(clearVoiceTranscript());
3746
3871
  setVoiceDurationMs(0);
3747
3872
  setVoiceAudioLevel(0);
@@ -3757,23 +3882,76 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3757
3882
  const provider = await createProvider({
3758
3883
  onStateChange: setVoiceState,
3759
3884
  onAudioLevelChange: setVoiceAudioLevel,
3760
- onDurationChange: setVoiceDurationMs,
3761
- 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
+ },
3762
3894
  onSegmentReady: (segment) => {
3763
- setVoiceDraft(segment);
3764
- setVoiceTranscript(segment.transcript ?? clearVoiceTranscript());
3765
- setVoiceDurationMs(segment.attachment.durationMs ?? 0);
3766
- setVoiceAudioLevel(0);
3767
- setVoiceCountdownMs(voiceAutoSendDelayMs);
3768
- setIsVoiceAutoSendActive(voiceAutoSendDelayMs > 0);
3769
- setVoiceError(null);
3770
- 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
+ })();
3771
3934
  },
3772
3935
  onError: (error) => {
3936
+ const previousSegment = voiceAppendBaseRef.current;
3937
+ voiceAppendBaseRef.current = null;
3938
+ voiceAppendBaseDurationRef.current = 0;
3773
3939
  setVoiceError(resolveVoiceErrorMessage(error, config));
3774
3940
  setVoiceAudioLevel(0);
3775
3941
  setVoiceCountdownMs(0);
3776
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);
3777
3955
  setVoiceState("error");
3778
3956
  }
3779
3957
  }, {
@@ -3783,35 +3961,67 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3783
3961
  return provider;
3784
3962
  }, [config, voiceAutoSendDelayMs, voiceMaxRecordingMs]);
3785
3963
  const closeVoiceComposer = (0, import_react5.useCallback)(async () => {
3964
+ voiceAppendBaseRef.current = null;
3965
+ voiceAppendBaseDurationRef.current = 0;
3786
3966
  setIsVoiceComposerOpen(false);
3787
3967
  setVoiceError(null);
3788
3968
  setVoiceCountdownMs(0);
3789
3969
  setVoiceAudioLevel(0);
3790
3970
  setVoiceTranscript(clearVoiceTranscript());
3791
3971
  setVoiceDraft(null);
3972
+ voiceDraftRef.current = null;
3792
3973
  setVoiceDurationMs(0);
3793
3974
  setVoiceState("idle");
3794
3975
  if (voiceProviderRef.current) {
3795
3976
  await voiceProviderRef.current.cancel();
3796
3977
  }
3797
3978
  }, []);
3798
- const startVoiceCapture = (0, import_react5.useCallback)(async () => {
3979
+ const startVoiceCapture = (0, import_react5.useCallback)(async (appendToDraft = false) => {
3799
3980
  if (disabled || isGenerating) {
3800
3981
  return;
3801
3982
  }
3983
+ const previousDraft = appendToDraft ? voiceDraftRef.current : null;
3984
+ const previousDurationMs = previousDraft ? resolveVoiceSegmentDuration(previousDraft) : 0;
3802
3985
  setIsVoiceComposerOpen(true);
3803
3986
  setVoiceError(null);
3804
- setVoiceDraft(null);
3805
3987
  setVoiceCountdownMs(0);
3806
- setVoiceTranscript(clearVoiceTranscript());
3807
3988
  setVoiceAudioLevel(0);
3808
- setVoiceDurationMs(0);
3809
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
+ }
3810
4001
  try {
3811
4002
  const provider = await ensureVoiceProvider();
3812
4003
  await provider.start();
3813
4004
  } catch (error) {
3814
- 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);
3815
4025
  setVoiceState("error");
3816
4026
  }
3817
4027
  }, [disabled, isGenerating, ensureVoiceProvider, config]);
@@ -3825,6 +4035,8 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3825
4035
  }
3826
4036
  }, [config]);
3827
4037
  const cancelVoiceCapture = (0, import_react5.useCallback)(async () => {
4038
+ voiceAppendBaseRef.current = null;
4039
+ voiceAppendBaseDurationRef.current = 0;
3828
4040
  if (voiceProviderRef.current) {
3829
4041
  await voiceProviderRef.current.cancel();
3830
4042
  }
@@ -3949,7 +4161,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
3949
4161
  void cancelVoiceCapture();
3950
4162
  },
3951
4163
  onRecordAgain: () => {
3952
- void startVoiceCapture();
4164
+ void startVoiceCapture(true);
3953
4165
  },
3954
4166
  onSendNow: sendVoiceDraft,
3955
4167
  onExit: () => {