@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.js CHANGED
@@ -45,7 +45,7 @@ var defaultChatConfig = {
45
45
  voiceSendNow: "Send now",
46
46
  voiceCancel: "Cancel",
47
47
  voiceDiscard: "Delete recording",
48
- voiceRecordAgain: "Record again",
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
- state === "error" && 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 })
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: setVoiceDurationMs,
3757
- onTranscriptChange: setVoiceTranscript,
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
- setVoiceDraft(segment);
3760
- setVoiceTranscript(segment.transcript ?? clearVoiceTranscript());
3761
- setVoiceDurationMs(segment.attachment.durationMs ?? 0);
3762
- setVoiceAudioLevel(0);
3763
- setVoiceCountdownMs(voiceAutoSendDelayMs);
3764
- setIsVoiceAutoSendActive(voiceAutoSendDelayMs > 0);
3765
- setVoiceError(null);
3766
- setVoiceState("review");
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
- setVoiceError(resolveVoiceErrorMessage(error, config));
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: () => {