@copilotz/chat-ui 0.1.32 → 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 +347 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +362 -109
- package/dist/index.js.map +1 -1
- package/dist/styles.css +86 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -88,17 +88,20 @@ var defaultChatConfig = {
|
|
|
88
88
|
recordAudioTooltip: "Record audio",
|
|
89
89
|
voiceEnter: "Voice input",
|
|
90
90
|
voiceExit: "Use keyboard",
|
|
91
|
-
voiceTitle: "Voice
|
|
91
|
+
voiceTitle: "Voice",
|
|
92
|
+
voiceIdle: "Tap the mic to record",
|
|
92
93
|
voicePreparing: "Preparing microphone...",
|
|
93
94
|
voiceWaiting: "Waiting for speech...",
|
|
94
95
|
voiceListening: "Listening...",
|
|
95
96
|
voiceFinishing: "Finishing capture...",
|
|
96
97
|
voiceReview: "Ready to send",
|
|
98
|
+
voiceSending: "Sending...",
|
|
97
99
|
voiceStart: "Start recording",
|
|
98
100
|
voiceStop: "Stop recording",
|
|
99
101
|
voiceSendNow: "Send now",
|
|
100
102
|
voiceCancel: "Cancel",
|
|
101
|
-
|
|
103
|
+
voiceDiscard: "Delete recording",
|
|
104
|
+
voiceRecordAgain: "Continue recording",
|
|
102
105
|
voiceAutoSendIn: "Auto-sends in {{seconds}}s",
|
|
103
106
|
voiceTranscriptPending: "Transcript unavailable",
|
|
104
107
|
voicePermissionDenied: "Microphone access was denied.",
|
|
@@ -167,6 +170,7 @@ var defaultChatConfig = {
|
|
|
167
170
|
},
|
|
168
171
|
voiceCompose: {
|
|
169
172
|
enabled: false,
|
|
173
|
+
defaultMode: "text",
|
|
170
174
|
autoSendDelayMs: 5e3,
|
|
171
175
|
persistComposer: true,
|
|
172
176
|
showTranscriptPreview: true,
|
|
@@ -2862,6 +2866,121 @@ var blobToDataUrl = (blob) => new Promise((resolve, reject) => {
|
|
|
2862
2866
|
reader.onerror = () => reject(reader.error ?? new Error("Failed to read recorded audio"));
|
|
2863
2867
|
reader.readAsDataURL(blob);
|
|
2864
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
|
+
};
|
|
2865
2984
|
var stopStream = (stream) => {
|
|
2866
2985
|
if (!stream) return;
|
|
2867
2986
|
stream.getTracks().forEach((track) => track.stop());
|
|
@@ -2983,7 +3102,7 @@ var createManualVoiceProvider = async (handlers, options = {}) => {
|
|
|
2983
3102
|
fileName: `voice-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.webm`,
|
|
2984
3103
|
size: blob.size
|
|
2985
3104
|
},
|
|
2986
|
-
metadata: { source: "manual" }
|
|
3105
|
+
metadata: { source: "manual", segmentCount: 1 }
|
|
2987
3106
|
});
|
|
2988
3107
|
} else {
|
|
2989
3108
|
handlers.onStateChange?.("idle");
|
|
@@ -3109,12 +3228,12 @@ var resolveStateLabel = (state, labels, errorMessage) => {
|
|
|
3109
3228
|
case "review":
|
|
3110
3229
|
return labels?.voiceReview || "Ready to send";
|
|
3111
3230
|
case "sending":
|
|
3112
|
-
return "Sending...";
|
|
3231
|
+
return labels?.voiceSending || "Sending...";
|
|
3113
3232
|
case "error":
|
|
3114
3233
|
return errorMessage || labels?.voiceCaptureError || "Unable to capture audio.";
|
|
3115
3234
|
case "idle":
|
|
3116
3235
|
default:
|
|
3117
|
-
return labels?.
|
|
3236
|
+
return labels?.voiceIdle || "Tap the mic to record";
|
|
3118
3237
|
}
|
|
3119
3238
|
};
|
|
3120
3239
|
var resolveTranscriptText = (transcript, transcriptMode) => {
|
|
@@ -3131,18 +3250,21 @@ var VoiceComposer = ({
|
|
|
3131
3250
|
transcript,
|
|
3132
3251
|
transcriptMode,
|
|
3133
3252
|
showTranscriptPreview,
|
|
3253
|
+
attachment,
|
|
3134
3254
|
durationMs,
|
|
3135
3255
|
audioLevel,
|
|
3136
3256
|
countdownMs,
|
|
3137
3257
|
autoSendDelayMs,
|
|
3258
|
+
isAutoSendActive,
|
|
3138
3259
|
errorMessage,
|
|
3139
3260
|
disabled = false,
|
|
3140
3261
|
labels,
|
|
3141
3262
|
onStart,
|
|
3142
3263
|
onStop,
|
|
3143
|
-
|
|
3144
|
-
|
|
3264
|
+
onCancelAutoSend,
|
|
3265
|
+
onDiscard,
|
|
3145
3266
|
onRecordAgain,
|
|
3267
|
+
onSendNow,
|
|
3146
3268
|
onExit
|
|
3147
3269
|
}) => {
|
|
3148
3270
|
const transcriptText = resolveTranscriptText(transcript, transcriptMode);
|
|
@@ -3150,12 +3272,14 @@ var VoiceComposer = ({
|
|
|
3150
3272
|
const countdownValue = autoSendDelayMs > 0 ? Math.min(100, Math.max(0, (autoSendDelayMs - countdownMs) / autoSendDelayMs * 100)) : 100;
|
|
3151
3273
|
const isBusy = state === "preparing" || state === "finishing" || state === "sending";
|
|
3152
3274
|
const isCapturing = state === "waiting_for_speech" || state === "listening";
|
|
3275
|
+
const isReviewing = state === "review";
|
|
3153
3276
|
const levelValue = isCapturing || state === "preparing" || state === "finishing" ? Math.max(8, Math.round(audioLevel * 100)) : 0;
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3277
|
+
const headerLabel = state === "error" ? labels?.voiceCaptureError || "Unable to capture audio." : resolveStateLabel(state, labels, errorMessage);
|
|
3278
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "w-full max-w-3xl rounded-xl border bg-background p-3 shadow-sm sm:p-4 md:min-w-3xl", children: [
|
|
3279
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center justify-between gap-2 sm:gap-3", children: [
|
|
3280
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
|
|
3281
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: "outline", children: labels?.voiceTitle || "Voice" }),
|
|
3282
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "truncate text-xs sm:text-sm text-muted-foreground", children: headerLabel })
|
|
3159
3283
|
] }),
|
|
3160
3284
|
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
|
|
3161
3285
|
Button,
|
|
@@ -3163,71 +3287,89 @@ var VoiceComposer = ({
|
|
|
3163
3287
|
type: "button",
|
|
3164
3288
|
variant: "ghost",
|
|
3165
3289
|
size: "sm",
|
|
3290
|
+
className: "shrink-0 px-2 sm:px-3",
|
|
3166
3291
|
onClick: onExit,
|
|
3167
3292
|
disabled: disabled || isBusy,
|
|
3168
3293
|
children: [
|
|
3169
3294
|
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Keyboard, { className: "h-4 w-4" }),
|
|
3170
|
-
labels?.voiceExit || "Use keyboard"
|
|
3295
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "hidden sm:inline", children: labels?.voiceExit || "Use keyboard" })
|
|
3171
3296
|
]
|
|
3172
3297
|
}
|
|
3173
3298
|
)
|
|
3174
3299
|
] }),
|
|
3175
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.
|
|
3176
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3177
|
-
|
|
3300
|
+
!isReviewing ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("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__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mx-auto flex w-full max-w-sm flex-col items-center gap-3", children: [
|
|
3301
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3302
|
+
Button,
|
|
3303
|
+
{
|
|
3304
|
+
type: "button",
|
|
3305
|
+
size: "icon",
|
|
3306
|
+
variant: isCapturing ? "destructive" : "outline",
|
|
3307
|
+
className: `h-16 w-16 rounded-full sm:h-20 sm:w-20 ${isCapturing ? "bg-red-500 hover:bg-red-600 text-white border-red-500" : "border-red-200 bg-red-50 text-red-600 hover:bg-red-100 hover:text-red-700"}`,
|
|
3308
|
+
onClick: isCapturing ? onStop : onStart,
|
|
3309
|
+
disabled: disabled || isBusy,
|
|
3310
|
+
children: isBusy ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Loader2, { className: "h-7 w-7 animate-spin" }) : isCapturing ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Square, { className: "h-7 w-7" }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Mic, { className: "h-7 w-7" })
|
|
3311
|
+
}
|
|
3312
|
+
),
|
|
3313
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "w-full space-y-2", children: [
|
|
3178
3314
|
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Progress, { value: levelValue, className: "h-2" }),
|
|
3179
3315
|
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [
|
|
3180
3316
|
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: formatDuration(durationMs) }),
|
|
3181
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children:
|
|
3317
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: isCapturing ? labels?.voiceStop || "Stop recording" : labels?.voiceStart || "Start recording" })
|
|
3182
3318
|
] })
|
|
3183
3319
|
] }),
|
|
3184
|
-
showTranscriptPreview && transcriptMode !== "none" && transcriptText && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "w-full
|
|
3185
|
-
] }),
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
state === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "mt-4 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive", children: errorMessage }),
|
|
3191
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-4 flex flex-wrap items-center justify-center gap-2", children: [
|
|
3192
|
-
state === "idle" && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", onClick: onStart, disabled, children: [
|
|
3193
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Mic, { className: "h-4 w-4" }),
|
|
3194
|
-
labels?.voiceStart || "Start recording"
|
|
3195
|
-
] }),
|
|
3196
|
-
isCapturing && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
|
|
3197
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", onClick: onStop, disabled, children: [
|
|
3198
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Square, { className: "h-4 w-4" }),
|
|
3199
|
-
labels?.voiceStop || "Stop recording"
|
|
3320
|
+
showTranscriptPreview && transcriptMode !== "none" && transcriptText && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "w-full rounded-lg border bg-background px-3 py-2 text-left text-sm", children: transcriptText })
|
|
3321
|
+
] }) }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-3 rounded-xl border bg-muted/20 p-3 sm:p-4", children: [
|
|
3322
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [
|
|
3323
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "min-w-0", children: [
|
|
3324
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-sm font-medium text-foreground", children: labels?.voiceReview || "Ready to send" }),
|
|
3325
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-xs text-muted-foreground", children: formatDuration(durationMs) })
|
|
3200
3326
|
] }),
|
|
3201
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3327
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3328
|
+
Button,
|
|
3329
|
+
{
|
|
3330
|
+
type: "button",
|
|
3331
|
+
variant: "ghost",
|
|
3332
|
+
size: "icon",
|
|
3333
|
+
className: "h-8 w-8 shrink-0 text-muted-foreground hover:text-destructive",
|
|
3334
|
+
onClick: onDiscard,
|
|
3335
|
+
disabled,
|
|
3336
|
+
"aria-label": labels?.voiceDiscard || "Delete recording",
|
|
3337
|
+
title: labels?.voiceDiscard || "Delete recording",
|
|
3338
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Trash2, { className: "h-4 w-4" })
|
|
3339
|
+
}
|
|
3340
|
+
)
|
|
3205
3341
|
] }),
|
|
3206
|
-
|
|
3207
|
-
|
|
3342
|
+
attachment && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "mt-3 rounded-lg bg-background p-2", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("audio", { controls: true, preload: "metadata", className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("source", { src: attachment.dataUrl, type: attachment.mimeType }) }) }),
|
|
3343
|
+
showTranscriptPreview && transcriptMode !== "none" && transcriptText && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "mt-3 rounded-lg border bg-background px-3 py-2 text-left text-sm", children: transcriptText }),
|
|
3344
|
+
isAutoSendActive && autoSendDelayMs > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-3 space-y-2", children: [
|
|
3345
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Progress, { value: countdownValue, className: "h-2" }),
|
|
3346
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-center text-xs text-muted-foreground", children: interpolateSeconds(labels?.voiceAutoSendIn, countdownSeconds) })
|
|
3347
|
+
] }),
|
|
3348
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "mt-3 flex items-center justify-end gap-2", children: [
|
|
3349
|
+
isAutoSendActive && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", variant: "ghost", size: "sm", onClick: onCancelAutoSend, disabled, children: [
|
|
3208
3350
|
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.X, { className: "h-4 w-4" }),
|
|
3209
3351
|
labels?.voiceCancel || "Cancel"
|
|
3210
3352
|
] }),
|
|
3211
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3353
|
+
!isAutoSendActive && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3354
|
+
Button,
|
|
3355
|
+
{
|
|
3356
|
+
type: "button",
|
|
3357
|
+
variant: "outline",
|
|
3358
|
+
size: "icon",
|
|
3359
|
+
onClick: onRecordAgain,
|
|
3360
|
+
disabled,
|
|
3361
|
+
"aria-label": labels?.voiceRecordAgain || "Record again",
|
|
3362
|
+
title: labels?.voiceRecordAgain || "Record again",
|
|
3363
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Mic, { className: "h-4 w-4" })
|
|
3364
|
+
}
|
|
3365
|
+
),
|
|
3366
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", size: "sm", onClick: onSendNow, disabled, children: [
|
|
3216
3367
|
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.Send, { className: "h-4 w-4" }),
|
|
3217
3368
|
labels?.voiceSendNow || "Send now"
|
|
3218
3369
|
] })
|
|
3219
|
-
] }),
|
|
3220
|
-
state === "error" && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
|
|
3221
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", variant: "outline", onClick: onCancel, disabled, children: [
|
|
3222
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.X, { className: "h-4 w-4" }),
|
|
3223
|
-
labels?.voiceCancel || "Cancel"
|
|
3224
|
-
] }),
|
|
3225
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { type: "button", onClick: onRecordAgain, disabled, children: [
|
|
3226
|
-
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react9.RotateCcw, { className: "h-4 w-4" }),
|
|
3227
|
-
labels?.voiceRecordAgain || "Record again"
|
|
3228
|
-
] })
|
|
3229
3370
|
] })
|
|
3230
|
-
] })
|
|
3371
|
+
] }),
|
|
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 })
|
|
3231
3373
|
] });
|
|
3232
3374
|
};
|
|
3233
3375
|
|
|
@@ -3334,7 +3476,7 @@ var AttachmentPreview = (0, import_react5.memo)(function AttachmentPreview2({ at
|
|
|
3334
3476
|
"img",
|
|
3335
3477
|
{
|
|
3336
3478
|
src: attachment.dataUrl,
|
|
3337
|
-
alt: attachment.fileName || "
|
|
3479
|
+
alt: attachment.fileName || "Attachment",
|
|
3338
3480
|
className: "w-full h-20 object-cover rounded"
|
|
3339
3481
|
}
|
|
3340
3482
|
),
|
|
@@ -3383,7 +3525,7 @@ var AttachmentPreview = (0, import_react5.memo)(function AttachmentPreview2({ at
|
|
|
3383
3525
|
}
|
|
3384
3526
|
),
|
|
3385
3527
|
/* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex-1", children: [
|
|
3386
|
-
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "text-xs font-medium", children: attachment.fileName || "
|
|
3528
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "text-xs font-medium", children: attachment.fileName || "Audio" }),
|
|
3387
3529
|
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "text-xs text-muted-foreground", children: formatDuration2(attachment.durationMs) })
|
|
3388
3530
|
] }),
|
|
3389
3531
|
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
@@ -3435,7 +3577,7 @@ var AudioRecorder = (0, import_react5.memo)(function AudioRecorder2({ isRecordin
|
|
|
3435
3577
|
return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Card, { className: "border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CardContent, { className: "p-3", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-3", children: [
|
|
3436
3578
|
/* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
3437
3579
|
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "h-3 w-3 bg-red-500 rounded-full animate-pulse" }),
|
|
3438
|
-
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-sm font-medium text-red-700 dark:text-red-300", children: "
|
|
3580
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-sm font-medium text-red-700 dark:text-red-300", children: config?.labels?.voiceListening || "Recording" })
|
|
3439
3581
|
] }),
|
|
3440
3582
|
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: "outline", className: "text-xs", children: formatTime(recordingDuration) }),
|
|
3441
3583
|
/* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex gap-1 ml-auto", children: [
|
|
@@ -3447,7 +3589,7 @@ var AudioRecorder = (0, import_react5.memo)(function AudioRecorder2({ isRecordin
|
|
|
3447
3589
|
onClick: onCancel,
|
|
3448
3590
|
children: [
|
|
3449
3591
|
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react10.X, { className: "h-3 w-3 mr-1" }),
|
|
3450
|
-
"
|
|
3592
|
+
config?.labels?.cancel || "Cancel"
|
|
3451
3593
|
]
|
|
3452
3594
|
}
|
|
3453
3595
|
),
|
|
@@ -3459,7 +3601,7 @@ var AudioRecorder = (0, import_react5.memo)(function AudioRecorder2({ isRecordin
|
|
|
3459
3601
|
onClick: onStopRecording,
|
|
3460
3602
|
children: [
|
|
3461
3603
|
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react10.Square, { className: "h-3 w-3 mr-1" }),
|
|
3462
|
-
"
|
|
3604
|
+
config?.labels?.voiceStop || "Stop"
|
|
3463
3605
|
]
|
|
3464
3606
|
}
|
|
3465
3607
|
)
|
|
@@ -3476,13 +3618,14 @@ var resolveVoiceErrorMessage = (error, config) => {
|
|
|
3476
3618
|
return config?.labels?.voiceCaptureError || "Unable to capture audio.";
|
|
3477
3619
|
};
|
|
3478
3620
|
var clearVoiceTranscript = () => ({});
|
|
3621
|
+
var resolveVoiceSegmentDuration = (segment) => segment.attachment.durationMs ?? 0;
|
|
3479
3622
|
var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
3480
3623
|
value,
|
|
3481
3624
|
onChange,
|
|
3482
3625
|
onSubmit,
|
|
3483
3626
|
attachments,
|
|
3484
3627
|
onAttachmentsChange,
|
|
3485
|
-
placeholder = "
|
|
3628
|
+
placeholder = "Type your message...",
|
|
3486
3629
|
disabled = false,
|
|
3487
3630
|
isGenerating = false,
|
|
3488
3631
|
onStopGeneration,
|
|
@@ -3495,17 +3638,27 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3495
3638
|
className = "",
|
|
3496
3639
|
config
|
|
3497
3640
|
}) {
|
|
3641
|
+
const voiceComposeEnabled = config?.voiceCompose?.enabled === true;
|
|
3642
|
+
const voiceDefaultMode = config?.voiceCompose?.defaultMode ?? "text";
|
|
3643
|
+
const voiceAutoSendDelayMs = config?.voiceCompose?.autoSendDelayMs ?? 5e3;
|
|
3644
|
+
const voicePersistComposer = config?.voiceCompose?.persistComposer ?? true;
|
|
3645
|
+
const voiceShowTranscriptPreview = config?.voiceCompose?.showTranscriptPreview ?? true;
|
|
3646
|
+
const voiceTranscriptMode = config?.voiceCompose?.transcriptMode ?? "final-only";
|
|
3647
|
+
const voiceMaxRecordingMs = config?.voiceCompose?.maxRecordingMs;
|
|
3498
3648
|
const [isRecording, setIsRecording] = (0, import_react5.useState)(false);
|
|
3499
3649
|
const { setContext } = useChatUserContext();
|
|
3500
3650
|
const [recordingDuration, setRecordingDuration] = (0, import_react5.useState)(0);
|
|
3501
3651
|
const [uploadProgress, setUploadProgress] = (0, import_react5.useState)(/* @__PURE__ */ new Map());
|
|
3502
|
-
const [isVoiceComposerOpen, setIsVoiceComposerOpen] = (0, import_react5.useState)(
|
|
3652
|
+
const [isVoiceComposerOpen, setIsVoiceComposerOpen] = (0, import_react5.useState)(
|
|
3653
|
+
() => voiceComposeEnabled && voiceDefaultMode === "voice"
|
|
3654
|
+
);
|
|
3503
3655
|
const [voiceState, setVoiceState] = (0, import_react5.useState)("idle");
|
|
3504
3656
|
const [voiceDraft, setVoiceDraft] = (0, import_react5.useState)(null);
|
|
3505
3657
|
const [voiceTranscript, setVoiceTranscript] = (0, import_react5.useState)(clearVoiceTranscript);
|
|
3506
3658
|
const [voiceDurationMs, setVoiceDurationMs] = (0, import_react5.useState)(0);
|
|
3507
3659
|
const [voiceAudioLevel, setVoiceAudioLevel] = (0, import_react5.useState)(0);
|
|
3508
3660
|
const [voiceCountdownMs, setVoiceCountdownMs] = (0, import_react5.useState)(0);
|
|
3661
|
+
const [isVoiceAutoSendActive, setIsVoiceAutoSendActive] = (0, import_react5.useState)(false);
|
|
3509
3662
|
const [voiceError, setVoiceError] = (0, import_react5.useState)(null);
|
|
3510
3663
|
const textareaRef = (0, import_react5.useRef)(null);
|
|
3511
3664
|
const fileInputRef = (0, import_react5.useRef)(null);
|
|
@@ -3514,12 +3667,9 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3514
3667
|
const recordingInterval = (0, import_react5.useRef)(null);
|
|
3515
3668
|
const mediaStreamRef = (0, import_react5.useRef)(null);
|
|
3516
3669
|
const voiceProviderRef = (0, import_react5.useRef)(null);
|
|
3517
|
-
const
|
|
3518
|
-
const
|
|
3519
|
-
const
|
|
3520
|
-
const voiceShowTranscriptPreview = config?.voiceCompose?.showTranscriptPreview ?? true;
|
|
3521
|
-
const voiceTranscriptMode = config?.voiceCompose?.transcriptMode ?? "final-only";
|
|
3522
|
-
const voiceMaxRecordingMs = config?.voiceCompose?.maxRecordingMs;
|
|
3670
|
+
const voiceDraftRef = (0, import_react5.useRef)(null);
|
|
3671
|
+
const voiceAppendBaseRef = (0, import_react5.useRef)(null);
|
|
3672
|
+
const voiceAppendBaseDurationRef = (0, import_react5.useRef)(0);
|
|
3523
3673
|
(0, import_react5.useEffect)(() => {
|
|
3524
3674
|
return () => {
|
|
3525
3675
|
if (mediaStreamRef.current) {
|
|
@@ -3534,6 +3684,9 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3534
3684
|
}
|
|
3535
3685
|
};
|
|
3536
3686
|
}, []);
|
|
3687
|
+
(0, import_react5.useEffect)(() => {
|
|
3688
|
+
voiceDraftRef.current = voiceDraft;
|
|
3689
|
+
}, [voiceDraft]);
|
|
3537
3690
|
const handleSubmit = (e) => {
|
|
3538
3691
|
e.preventDefault();
|
|
3539
3692
|
if (!value.trim() && attachments.length === 0 || disabled || isGenerating) return;
|
|
@@ -3549,7 +3702,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3549
3702
|
};
|
|
3550
3703
|
const processFile = async (file) => {
|
|
3551
3704
|
if (file.size > maxFileSize) {
|
|
3552
|
-
alert(`
|
|
3705
|
+
alert(`File too large. Max allowed: ${Math.round(maxFileSize / 1024 / 1024)}MB`);
|
|
3553
3706
|
return null;
|
|
3554
3707
|
}
|
|
3555
3708
|
const fileId = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
@@ -3608,7 +3761,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3608
3761
|
newMap.delete(fileId);
|
|
3609
3762
|
return newMap;
|
|
3610
3763
|
});
|
|
3611
|
-
alert("
|
|
3764
|
+
alert("Failed to process file");
|
|
3612
3765
|
return null;
|
|
3613
3766
|
}
|
|
3614
3767
|
};
|
|
@@ -3683,7 +3836,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3683
3836
|
}, 1e3);
|
|
3684
3837
|
} catch (error) {
|
|
3685
3838
|
console.error("Error starting recording:", error);
|
|
3686
|
-
alert(
|
|
3839
|
+
alert(config?.labels?.voicePermissionDenied || "Microphone access was denied.");
|
|
3687
3840
|
}
|
|
3688
3841
|
};
|
|
3689
3842
|
const stopRecording = () => {
|
|
@@ -3711,10 +3864,14 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3711
3864
|
const resetVoiceComposerState = (0, import_react5.useCallback)((nextState = "idle") => {
|
|
3712
3865
|
setVoiceState(nextState);
|
|
3713
3866
|
setVoiceDraft(null);
|
|
3867
|
+
voiceDraftRef.current = null;
|
|
3868
|
+
voiceAppendBaseRef.current = null;
|
|
3869
|
+
voiceAppendBaseDurationRef.current = 0;
|
|
3714
3870
|
setVoiceTranscript(clearVoiceTranscript());
|
|
3715
3871
|
setVoiceDurationMs(0);
|
|
3716
3872
|
setVoiceAudioLevel(0);
|
|
3717
3873
|
setVoiceCountdownMs(0);
|
|
3874
|
+
setIsVoiceAutoSendActive(false);
|
|
3718
3875
|
setVoiceError(null);
|
|
3719
3876
|
}, []);
|
|
3720
3877
|
const ensureVoiceProvider = (0, import_react5.useCallback)(async () => {
|
|
@@ -3725,21 +3882,76 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3725
3882
|
const provider = await createProvider({
|
|
3726
3883
|
onStateChange: setVoiceState,
|
|
3727
3884
|
onAudioLevelChange: setVoiceAudioLevel,
|
|
3728
|
-
onDurationChange:
|
|
3729
|
-
|
|
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
|
+
},
|
|
3730
3894
|
onSegmentReady: (segment) => {
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
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
|
+
})();
|
|
3738
3934
|
},
|
|
3739
3935
|
onError: (error) => {
|
|
3936
|
+
const previousSegment = voiceAppendBaseRef.current;
|
|
3937
|
+
voiceAppendBaseRef.current = null;
|
|
3938
|
+
voiceAppendBaseDurationRef.current = 0;
|
|
3740
3939
|
setVoiceError(resolveVoiceErrorMessage(error, config));
|
|
3741
3940
|
setVoiceAudioLevel(0);
|
|
3742
3941
|
setVoiceCountdownMs(0);
|
|
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);
|
|
3743
3955
|
setVoiceState("error");
|
|
3744
3956
|
}
|
|
3745
3957
|
}, {
|
|
@@ -3749,34 +3961,67 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3749
3961
|
return provider;
|
|
3750
3962
|
}, [config, voiceAutoSendDelayMs, voiceMaxRecordingMs]);
|
|
3751
3963
|
const closeVoiceComposer = (0, import_react5.useCallback)(async () => {
|
|
3964
|
+
voiceAppendBaseRef.current = null;
|
|
3965
|
+
voiceAppendBaseDurationRef.current = 0;
|
|
3752
3966
|
setIsVoiceComposerOpen(false);
|
|
3753
3967
|
setVoiceError(null);
|
|
3754
3968
|
setVoiceCountdownMs(0);
|
|
3755
3969
|
setVoiceAudioLevel(0);
|
|
3756
3970
|
setVoiceTranscript(clearVoiceTranscript());
|
|
3757
3971
|
setVoiceDraft(null);
|
|
3972
|
+
voiceDraftRef.current = null;
|
|
3758
3973
|
setVoiceDurationMs(0);
|
|
3759
3974
|
setVoiceState("idle");
|
|
3760
3975
|
if (voiceProviderRef.current) {
|
|
3761
3976
|
await voiceProviderRef.current.cancel();
|
|
3762
3977
|
}
|
|
3763
3978
|
}, []);
|
|
3764
|
-
const startVoiceCapture = (0, import_react5.useCallback)(async () => {
|
|
3979
|
+
const startVoiceCapture = (0, import_react5.useCallback)(async (appendToDraft = false) => {
|
|
3765
3980
|
if (disabled || isGenerating) {
|
|
3766
3981
|
return;
|
|
3767
3982
|
}
|
|
3983
|
+
const previousDraft = appendToDraft ? voiceDraftRef.current : null;
|
|
3984
|
+
const previousDurationMs = previousDraft ? resolveVoiceSegmentDuration(previousDraft) : 0;
|
|
3768
3985
|
setIsVoiceComposerOpen(true);
|
|
3769
3986
|
setVoiceError(null);
|
|
3770
|
-
setVoiceDraft(null);
|
|
3771
3987
|
setVoiceCountdownMs(0);
|
|
3772
|
-
setVoiceTranscript(clearVoiceTranscript());
|
|
3773
3988
|
setVoiceAudioLevel(0);
|
|
3774
|
-
|
|
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
|
+
}
|
|
3775
4001
|
try {
|
|
3776
4002
|
const provider = await ensureVoiceProvider();
|
|
3777
4003
|
await provider.start();
|
|
3778
4004
|
} catch (error) {
|
|
3779
|
-
|
|
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);
|
|
3780
4025
|
setVoiceState("error");
|
|
3781
4026
|
}
|
|
3782
4027
|
}, [disabled, isGenerating, ensureVoiceProvider, config]);
|
|
@@ -3790,6 +4035,8 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3790
4035
|
}
|
|
3791
4036
|
}, [config]);
|
|
3792
4037
|
const cancelVoiceCapture = (0, import_react5.useCallback)(async () => {
|
|
4038
|
+
voiceAppendBaseRef.current = null;
|
|
4039
|
+
voiceAppendBaseDurationRef.current = 0;
|
|
3793
4040
|
if (voiceProviderRef.current) {
|
|
3794
4041
|
await voiceProviderRef.current.cancel();
|
|
3795
4042
|
}
|
|
@@ -3809,6 +4056,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3809
4056
|
}
|
|
3810
4057
|
setVoiceState("sending");
|
|
3811
4058
|
setVoiceCountdownMs(0);
|
|
4059
|
+
setIsVoiceAutoSendActive(false);
|
|
3812
4060
|
onSubmit("", [...attachments, voiceDraft.attachment]);
|
|
3813
4061
|
onChange("");
|
|
3814
4062
|
onAttachmentsChange([]);
|
|
@@ -3823,12 +4071,12 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3823
4071
|
onAttachmentsChange,
|
|
3824
4072
|
finalizeVoiceComposerAfterSend
|
|
3825
4073
|
]);
|
|
3826
|
-
const
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
}, [
|
|
4074
|
+
const cancelVoiceAutoSend = (0, import_react5.useCallback)(() => {
|
|
4075
|
+
setVoiceCountdownMs(0);
|
|
4076
|
+
setIsVoiceAutoSendActive(false);
|
|
4077
|
+
}, []);
|
|
3830
4078
|
(0, import_react5.useEffect)(() => {
|
|
3831
|
-
if (voiceState !== "review" || !voiceDraft || voiceAutoSendDelayMs <= 0) {
|
|
4079
|
+
if (voiceState !== "review" || !voiceDraft || voiceAutoSendDelayMs <= 0 || !isVoiceAutoSendActive) {
|
|
3832
4080
|
return;
|
|
3833
4081
|
}
|
|
3834
4082
|
const startedAt = Date.now();
|
|
@@ -3842,7 +4090,7 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3842
4090
|
}
|
|
3843
4091
|
}, 100);
|
|
3844
4092
|
return () => clearInterval(timer);
|
|
3845
|
-
}, [voiceState, voiceDraft, voiceAutoSendDelayMs, sendVoiceDraft]);
|
|
4093
|
+
}, [voiceState, voiceDraft, voiceAutoSendDelayMs, isVoiceAutoSendActive, sendVoiceDraft]);
|
|
3846
4094
|
const removeAttachment = (index) => {
|
|
3847
4095
|
const newAttachments = attachments.filter((_, i) => i !== index);
|
|
3848
4096
|
onAttachmentsChange(newAttachments);
|
|
@@ -3891,10 +4139,12 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3891
4139
|
transcript: voiceTranscript,
|
|
3892
4140
|
transcriptMode: voiceTranscriptMode,
|
|
3893
4141
|
showTranscriptPreview: voiceShowTranscriptPreview,
|
|
4142
|
+
attachment: voiceDraft?.attachment ?? null,
|
|
3894
4143
|
durationMs: voiceDurationMs,
|
|
3895
4144
|
audioLevel: voiceAudioLevel,
|
|
3896
4145
|
countdownMs: voiceCountdownMs,
|
|
3897
4146
|
autoSendDelayMs: voiceAutoSendDelayMs,
|
|
4147
|
+
isAutoSendActive: isVoiceAutoSendActive,
|
|
3898
4148
|
errorMessage: voiceError,
|
|
3899
4149
|
disabled: disabled || isGenerating,
|
|
3900
4150
|
labels: config?.labels,
|
|
@@ -3904,13 +4154,16 @@ var ChatInput = (0, import_react5.memo)(function ChatInput2({
|
|
|
3904
4154
|
onStop: () => {
|
|
3905
4155
|
void stopVoiceCapture();
|
|
3906
4156
|
},
|
|
3907
|
-
|
|
4157
|
+
onCancelAutoSend: () => {
|
|
4158
|
+
cancelVoiceAutoSend();
|
|
4159
|
+
},
|
|
4160
|
+
onDiscard: () => {
|
|
3908
4161
|
void cancelVoiceCapture();
|
|
3909
4162
|
},
|
|
3910
|
-
onSendNow: sendVoiceDraft,
|
|
3911
4163
|
onRecordAgain: () => {
|
|
3912
|
-
void
|
|
4164
|
+
void startVoiceCapture(true);
|
|
3913
4165
|
},
|
|
4166
|
+
onSendNow: sendVoiceDraft,
|
|
3914
4167
|
onExit: () => {
|
|
3915
4168
|
void closeVoiceComposer();
|
|
3916
4169
|
}
|