@opengeni/react 0.44.5 → 0.46.1
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/README.md +3 -0
- package/dist/{chunk-L3EWO3UK.js → chunk-3PB7MIT6.js} +243 -22
- package/dist/chunk-3PB7MIT6.js.map +1 -0
- package/dist/{chunk-EHSYZ4KP.js → chunk-FGZUCXZF.js} +2 -2
- package/dist/{chunk-EHSYZ4KP.js.map → chunk-FGZUCXZF.js.map} +1 -1
- package/dist/{chunk-LYRJUU5V.js → chunk-LGVMUIRV.js} +543 -200
- package/dist/chunk-LGVMUIRV.js.map +1 -0
- package/dist/components/user-message-body.d.ts +36 -0
- package/dist/composer.js +2 -2
- package/dist/hooks/use-voice-input.d.ts +8 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/realtime.js +4 -4
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +2 -0
- package/dist/session-ui.js +7 -3
- package/package.json +2 -2
- package/src/components/composer.tsx +4 -4
- package/src/components/copy-button.tsx +1 -1
- package/src/components/markdown.tsx +1 -1
- package/src/components/message-timeline.tsx +134 -25
- package/src/components/model-picker.tsx +1 -1
- package/src/components/model-policy-picker.tsx +1 -1
- package/src/components/user-message-body.tsx +341 -0
- package/src/hooks/use-codex-accounts.ts +1 -1
- package/src/hooks/use-voice-input.ts +320 -19
- package/src/index.ts +2 -0
- package/src/realtime/realtime-control.tsx +3 -3
- package/src/session-ui.ts +2 -0
- package/src/timeline/activity-rail.tsx +1 -1
- package/src/timeline/shared.tsx +3 -3
- package/src/timeline/turn-summary.tsx +4 -4
- package/dist/chunk-L3EWO3UK.js.map +0 -1
- package/dist/chunk-LYRJUU5V.js.map +0 -1
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
VoiceRecordingOwnedError,
|
|
6
6
|
VoiceRecordingStorageUnavailableError,
|
|
7
7
|
createVoiceRecordingManifest,
|
|
8
|
+
type VoiceRecordingChunk,
|
|
8
9
|
type VoiceRecordingManifest,
|
|
9
10
|
type VoiceRecordingStore,
|
|
10
11
|
} from "../voice-recording-store";
|
|
@@ -25,7 +26,7 @@ export type VoiceInputStatus =
|
|
|
25
26
|
| "error";
|
|
26
27
|
|
|
27
28
|
export type UseVoiceInputOptions = {
|
|
28
|
-
client:
|
|
29
|
+
client: VoiceInputClient | null;
|
|
29
30
|
workspaceId: string;
|
|
30
31
|
capability: ClientVoiceInputConfig | null;
|
|
31
32
|
enabled: boolean;
|
|
@@ -40,6 +41,19 @@ export type UseVoiceInputOptions = {
|
|
|
40
41
|
now?: (() => Date) | undefined;
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
type ResumableVoiceInputClient = Pick<
|
|
45
|
+
OpenGeniClient,
|
|
46
|
+
| "createTranscriptionRecording"
|
|
47
|
+
| "getTranscriptionRecording"
|
|
48
|
+
| "uploadTranscriptionRecordingChunk"
|
|
49
|
+
| "finalizeTranscriptionRecording"
|
|
50
|
+
| "processNextTranscriptionRecordingSegment"
|
|
51
|
+
| "discardTranscriptionRecording"
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
type VoiceInputClient = Pick<OpenGeniClient, "transcribeAudio"> &
|
|
55
|
+
Partial<ResumableVoiceInputClient>;
|
|
56
|
+
|
|
43
57
|
export type UseVoiceInputResult = {
|
|
44
58
|
status: VoiceInputStatus;
|
|
45
59
|
error: string | null;
|
|
@@ -68,11 +82,43 @@ export const VOICE_RECORDING_TIMESLICE_MILLISECONDS = 5_000;
|
|
|
68
82
|
export const VOICE_RECORDING_OWNER_HEARTBEAT_MILLISECONDS = 5_000;
|
|
69
83
|
export const VOICE_RECORDING_OWNER_STALE_MILLISECONDS = 30_000;
|
|
70
84
|
export const VOICE_RECORDING_CLIENT_MAX_DURATION_SECONDS = 600;
|
|
85
|
+
export const VOICE_RECORDING_RESUMABLE_CLIENT_MAX_DURATION_SECONDS = 8 * 60 * 60;
|
|
86
|
+
export const VOICE_RECORDING_RECOVERY_STATUS_POLL_MILLISECONDS = 2_000;
|
|
87
|
+
export const VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS = 30_000;
|
|
88
|
+
const TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS = 5_000;
|
|
71
89
|
|
|
72
90
|
const MIME_PREFERENCES = ["audio/webm;codecs=opus", "audio/mp4", "audio/ogg;codecs=opus"];
|
|
73
91
|
const createDefaultVoiceRecordingId = () => crypto.randomUUID();
|
|
74
92
|
const currentDate = () => new Date();
|
|
75
93
|
|
|
94
|
+
export function transcriptionRecoveryMutationDelayMilliseconds(
|
|
95
|
+
retryAfterMilliseconds: number | undefined,
|
|
96
|
+
attempt: number,
|
|
97
|
+
random: () => number = Math.random,
|
|
98
|
+
): number {
|
|
99
|
+
const hint =
|
|
100
|
+
typeof retryAfterMilliseconds === "number" &&
|
|
101
|
+
Number.isInteger(retryAfterMilliseconds) &&
|
|
102
|
+
retryAfterMilliseconds > 0
|
|
103
|
+
? retryAfterMilliseconds
|
|
104
|
+
: TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS;
|
|
105
|
+
const boundedHint = Math.max(
|
|
106
|
+
500,
|
|
107
|
+
Math.min(hint, VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS),
|
|
108
|
+
);
|
|
109
|
+
const exponent = Math.max(0, Math.min(Math.floor(attempt), 6));
|
|
110
|
+
const exponential = Math.min(
|
|
111
|
+
VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS,
|
|
112
|
+
boundedHint * 2 ** exponent,
|
|
113
|
+
);
|
|
114
|
+
const sampledJitter = random();
|
|
115
|
+
const jitterRatio = Number.isFinite(sampledJitter) ? Math.max(0, Math.min(1, sampledJitter)) : 0;
|
|
116
|
+
return Math.min(
|
|
117
|
+
VOICE_RECORDING_RECOVERY_MAX_MUTATION_DELAY_MILLISECONDS,
|
|
118
|
+
Math.ceil(exponential * (1 + jitterRatio * 0.2)),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
76
122
|
export function useVoiceInput({
|
|
77
123
|
client,
|
|
78
124
|
workspaceId,
|
|
@@ -322,8 +368,17 @@ export function useVoiceInput({
|
|
|
322
368
|
const finalizePersistedRecording = useCallback(
|
|
323
369
|
async (generation: number): Promise<void> => {
|
|
324
370
|
const manifest = manifestRef.current;
|
|
325
|
-
const
|
|
326
|
-
|
|
371
|
+
const resumable =
|
|
372
|
+
capability?.resumable && isResumableVoiceInputClient(client) ? capability.resumable : null;
|
|
373
|
+
const maxSizeBytes = resumable?.maxSizeBytes ?? capability?.maxSizeBytes;
|
|
374
|
+
if (
|
|
375
|
+
!manifest ||
|
|
376
|
+
!client ||
|
|
377
|
+
!capability ||
|
|
378
|
+
!maxSizeBytes ||
|
|
379
|
+
manifest.workspaceId !== workspaceId
|
|
380
|
+
)
|
|
381
|
+
return;
|
|
327
382
|
const controller = new AbortController();
|
|
328
383
|
controllerRef.current?.abort();
|
|
329
384
|
controllerRef.current = controller;
|
|
@@ -368,18 +423,12 @@ export function useVoiceInput({
|
|
|
368
423
|
await preserveForRetry(retained, "invalid_audio", generation);
|
|
369
424
|
return;
|
|
370
425
|
}
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (!active()) return;
|
|
378
|
-
if (audio.size > maxSizeBytes) throw { code: "too_large" };
|
|
379
|
-
const response = await client.transcribeAudio(workspaceId, {
|
|
380
|
-
audio,
|
|
381
|
-
mimeType: audio.type,
|
|
382
|
-
durationSeconds: transcribing.totalDurationMilliseconds / 1_000,
|
|
426
|
+
const response = await transcribePersistedRecording({
|
|
427
|
+
client,
|
|
428
|
+
workspaceId,
|
|
429
|
+
capability,
|
|
430
|
+
manifest: transcribing,
|
|
431
|
+
chunks,
|
|
383
432
|
signal: controller.signal,
|
|
384
433
|
});
|
|
385
434
|
if (!active()) return;
|
|
@@ -422,6 +471,14 @@ export function useVoiceInput({
|
|
|
422
471
|
}
|
|
423
472
|
if (!active()) return;
|
|
424
473
|
rememberManifest(handedOff);
|
|
474
|
+
if (resumable && isResumableVoiceInputClient(client)) {
|
|
475
|
+
await client
|
|
476
|
+
.discardTranscriptionRecording(workspaceId, handedOff.recordingId, {
|
|
477
|
+
signal: controller.signal,
|
|
478
|
+
})
|
|
479
|
+
.catch(() => undefined);
|
|
480
|
+
}
|
|
481
|
+
if (!active()) return;
|
|
425
482
|
await store
|
|
426
483
|
.discard(handedOff.recordingId, ownerIdRef.current ?? undefined)
|
|
427
484
|
.catch(() => undefined);
|
|
@@ -448,7 +505,7 @@ export function useVoiceInput({
|
|
|
448
505
|
}
|
|
449
506
|
},
|
|
450
507
|
[
|
|
451
|
-
capability
|
|
508
|
+
capability,
|
|
452
509
|
clearVisibleRecording,
|
|
453
510
|
client,
|
|
454
511
|
ensureStore,
|
|
@@ -602,7 +659,11 @@ export function useVoiceInput({
|
|
|
602
659
|
});
|
|
603
660
|
if (!startAttemptIsCurrent()) return;
|
|
604
661
|
rememberManifest(result.manifest);
|
|
605
|
-
|
|
662
|
+
const maxSizeBytes =
|
|
663
|
+
capability.resumable && isResumableVoiceInputClient(client)
|
|
664
|
+
? capability.resumable.maxSizeBytes
|
|
665
|
+
: capability.maxSizeBytes;
|
|
666
|
+
if (result.manifest.totalBytes > maxSizeBytes) {
|
|
606
667
|
attemptCaptureLimitError = "too_large";
|
|
607
668
|
if (attemptOwnsSharedCapture()) captureLimitErrorRef.current = "too_large";
|
|
608
669
|
if (recorder.state !== "inactive") recorder.stop();
|
|
@@ -675,8 +736,14 @@ export function useVoiceInput({
|
|
|
675
736
|
setStatus("recording");
|
|
676
737
|
timerRef.current = setTimeout(
|
|
677
738
|
stop,
|
|
678
|
-
Math.min(
|
|
679
|
-
|
|
739
|
+
Math.min(
|
|
740
|
+
capability.resumable && isResumableVoiceInputClient(client)
|
|
741
|
+
? capability.resumable.maxDurationSeconds
|
|
742
|
+
: capability.maxDurationSeconds,
|
|
743
|
+
capability.resumable && isResumableVoiceInputClient(client)
|
|
744
|
+
? VOICE_RECORDING_RESUMABLE_CLIENT_MAX_DURATION_SECONDS
|
|
745
|
+
: VOICE_RECORDING_CLIENT_MAX_DURATION_SECONDS,
|
|
746
|
+
) * 1_000,
|
|
680
747
|
);
|
|
681
748
|
return true;
|
|
682
749
|
} catch (reason) {
|
|
@@ -800,6 +867,12 @@ export function useVoiceInput({
|
|
|
800
867
|
return;
|
|
801
868
|
}
|
|
802
869
|
if (generation !== generationRef.current) return;
|
|
870
|
+
if (capability?.resumable && isResumableVoiceInputClient(client)) {
|
|
871
|
+
await client
|
|
872
|
+
.discardTranscriptionRecording(workspaceId, handedOff.recordingId)
|
|
873
|
+
.catch(() => undefined);
|
|
874
|
+
}
|
|
875
|
+
if (generation !== generationRef.current) return;
|
|
803
876
|
await store
|
|
804
877
|
.discard(handedOff.recordingId, ownerIdRef.current ?? undefined)
|
|
805
878
|
.catch(() => undefined);
|
|
@@ -811,6 +884,8 @@ export function useVoiceInput({
|
|
|
811
884
|
await loadNextRecoverable(generation);
|
|
812
885
|
}, [
|
|
813
886
|
clearVisibleRecording,
|
|
887
|
+
capability?.resumable,
|
|
888
|
+
client,
|
|
814
889
|
ensureStore,
|
|
815
890
|
focusInput,
|
|
816
891
|
loadNextRecoverable,
|
|
@@ -835,6 +910,12 @@ export function useVoiceInput({
|
|
|
835
910
|
await captureSettled.catch(() => undefined);
|
|
836
911
|
if (generation !== generationRef.current) return;
|
|
837
912
|
if (manifest) {
|
|
913
|
+
if (capability?.resumable && isResumableVoiceInputClient(client)) {
|
|
914
|
+
await client
|
|
915
|
+
.discardTranscriptionRecording(workspaceId, manifest.recordingId)
|
|
916
|
+
.catch(() => undefined);
|
|
917
|
+
}
|
|
918
|
+
if (generation !== generationRef.current) return;
|
|
838
919
|
try {
|
|
839
920
|
await (await ensureStore()).discard(manifest.recordingId, ownerIdRef.current ?? undefined);
|
|
840
921
|
} catch {
|
|
@@ -856,10 +937,13 @@ export function useVoiceInput({
|
|
|
856
937
|
}, [
|
|
857
938
|
clearCaptureRuntime,
|
|
858
939
|
clearVisibleRecording,
|
|
940
|
+
capability?.resumable,
|
|
941
|
+
client,
|
|
859
942
|
ensureStore,
|
|
860
943
|
focusInput,
|
|
861
944
|
loadNextRecoverable,
|
|
862
945
|
rememberManifest,
|
|
946
|
+
workspaceId,
|
|
863
947
|
]);
|
|
864
948
|
|
|
865
949
|
const cancel = useCallback(() => {
|
|
@@ -1032,6 +1116,223 @@ export function useVoiceInput({
|
|
|
1032
1116
|
};
|
|
1033
1117
|
}
|
|
1034
1118
|
|
|
1119
|
+
async function transcribePersistedRecording(input: {
|
|
1120
|
+
client: VoiceInputClient;
|
|
1121
|
+
workspaceId: string;
|
|
1122
|
+
capability: ClientVoiceInputConfig;
|
|
1123
|
+
manifest: VoiceRecordingManifest;
|
|
1124
|
+
chunks: VoiceRecordingChunk[];
|
|
1125
|
+
signal: AbortSignal;
|
|
1126
|
+
}): Promise<{ text: string; languages: string[] }> {
|
|
1127
|
+
const client = input.client;
|
|
1128
|
+
const resumable = input.capability.resumable;
|
|
1129
|
+
const ordered = [...input.chunks].sort((left, right) => left.chunkNumber - right.chunkNumber);
|
|
1130
|
+
if (!resumable || !isResumableVoiceInputClient(client)) {
|
|
1131
|
+
const audio = new Blob(
|
|
1132
|
+
ordered.map((chunk) => chunk.audio),
|
|
1133
|
+
{ type: input.manifest.mimeType },
|
|
1134
|
+
);
|
|
1135
|
+
if (audio.size > input.capability.maxSizeBytes) throw { code: "too_large" };
|
|
1136
|
+
return await client.transcribeAudio(input.workspaceId, {
|
|
1137
|
+
audio,
|
|
1138
|
+
mimeType: audio.type,
|
|
1139
|
+
durationSeconds: input.manifest.totalDurationMilliseconds / 1_000,
|
|
1140
|
+
signal: input.signal,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (
|
|
1145
|
+
input.manifest.totalBytes > resumable.maxSizeBytes ||
|
|
1146
|
+
input.manifest.totalDurationMilliseconds > resumable.maxDurationSeconds * 1_000
|
|
1147
|
+
) {
|
|
1148
|
+
throw { code: "too_large" };
|
|
1149
|
+
}
|
|
1150
|
+
if (
|
|
1151
|
+
ordered.some((chunk, index) => chunk.chunkNumber !== index) ||
|
|
1152
|
+
ordered.length !== input.manifest.chunkCount
|
|
1153
|
+
) {
|
|
1154
|
+
throw { code: "invalid_audio" };
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const finalizeInput = {
|
|
1158
|
+
chunkCount: input.manifest.chunkCount,
|
|
1159
|
+
totalBytes: input.manifest.totalBytes,
|
|
1160
|
+
totalDurationMilliseconds: input.manifest.totalDurationMilliseconds,
|
|
1161
|
+
signal: input.signal,
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
let remote = await client.createTranscriptionRecording(input.workspaceId, {
|
|
1165
|
+
recordingId: input.manifest.recordingId,
|
|
1166
|
+
mimeType: input.manifest.mimeType,
|
|
1167
|
+
signal: input.signal,
|
|
1168
|
+
});
|
|
1169
|
+
for (const chunk of ordered) {
|
|
1170
|
+
if (chunk.chunkNumber < remote.recording.nextChunkNumber) continue;
|
|
1171
|
+
if (chunk.chunkNumber !== remote.recording.nextChunkNumber) {
|
|
1172
|
+
throw { code: "invalid_audio" };
|
|
1173
|
+
}
|
|
1174
|
+
if (chunk.byteLength > resumable.maxChunkSizeBytes) throw { code: "too_large" };
|
|
1175
|
+
const uploaded = await client.uploadTranscriptionRecordingChunk(
|
|
1176
|
+
input.workspaceId,
|
|
1177
|
+
input.manifest.recordingId,
|
|
1178
|
+
chunk.chunkNumber,
|
|
1179
|
+
{
|
|
1180
|
+
audio: chunk.audio,
|
|
1181
|
+
mimeType: input.manifest.mimeType,
|
|
1182
|
+
sha256: chunk.sha256,
|
|
1183
|
+
startMilliseconds: chunk.startMilliseconds,
|
|
1184
|
+
durationMilliseconds: chunk.durationMilliseconds,
|
|
1185
|
+
signal: input.signal,
|
|
1186
|
+
},
|
|
1187
|
+
);
|
|
1188
|
+
remote = { recording: uploaded.recording, segments: remote.segments };
|
|
1189
|
+
}
|
|
1190
|
+
if (remote.recording.nextChunkNumber !== ordered.length) {
|
|
1191
|
+
throw { code: "invalid_audio" };
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
remote = await client.finalizeTranscriptionRecording(
|
|
1195
|
+
input.workspaceId,
|
|
1196
|
+
input.manifest.recordingId,
|
|
1197
|
+
finalizeInput,
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1200
|
+
let recoveryMutationAttempt = 0;
|
|
1201
|
+
let recoveryMutationDueAt = 0;
|
|
1202
|
+
const scheduleRecoveryMutation = (response: typeof remote): void => {
|
|
1203
|
+
if (response.recording.state !== "segmenting" && response.recording.state !== "transcribing") {
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
recoveryMutationDueAt =
|
|
1207
|
+
Date.now() +
|
|
1208
|
+
transcriptionRecoveryMutationDelayMilliseconds(
|
|
1209
|
+
response.retryAfterMilliseconds,
|
|
1210
|
+
recoveryMutationAttempt,
|
|
1211
|
+
);
|
|
1212
|
+
recoveryMutationAttempt += 1;
|
|
1213
|
+
};
|
|
1214
|
+
scheduleRecoveryMutation(remote);
|
|
1215
|
+
|
|
1216
|
+
for (;;) {
|
|
1217
|
+
if (input.signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
1218
|
+
switch (remote.recording.state) {
|
|
1219
|
+
case "complete":
|
|
1220
|
+
if (remote.recording.transcriptText === null) throw { code: "invalid_audio" };
|
|
1221
|
+
return {
|
|
1222
|
+
text: remote.recording.transcriptText,
|
|
1223
|
+
languages: remote.recording.languages,
|
|
1224
|
+
};
|
|
1225
|
+
case "ready":
|
|
1226
|
+
remote = await client.processNextTranscriptionRecordingSegment(
|
|
1227
|
+
input.workspaceId,
|
|
1228
|
+
input.manifest.recordingId,
|
|
1229
|
+
{ signal: input.signal },
|
|
1230
|
+
);
|
|
1231
|
+
scheduleRecoveryMutation(remote);
|
|
1232
|
+
break;
|
|
1233
|
+
case "failed": {
|
|
1234
|
+
if (!remote.recording.retryable) {
|
|
1235
|
+
throw { code: remote.recording.errorCode ?? "unknown" };
|
|
1236
|
+
}
|
|
1237
|
+
const retried =
|
|
1238
|
+
remote.recording.segmentCount === 0
|
|
1239
|
+
? await client.finalizeTranscriptionRecording(
|
|
1240
|
+
input.workspaceId,
|
|
1241
|
+
input.manifest.recordingId,
|
|
1242
|
+
finalizeInput,
|
|
1243
|
+
)
|
|
1244
|
+
: await client.processNextTranscriptionRecordingSegment(
|
|
1245
|
+
input.workspaceId,
|
|
1246
|
+
input.manifest.recordingId,
|
|
1247
|
+
{ signal: input.signal },
|
|
1248
|
+
);
|
|
1249
|
+
if (retried.recording.state === "failed") {
|
|
1250
|
+
throw { code: retried.recording.errorCode ?? "unknown" };
|
|
1251
|
+
}
|
|
1252
|
+
remote = retried;
|
|
1253
|
+
scheduleRecoveryMutation(remote);
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
case "discarded":
|
|
1257
|
+
throw { code: "invalid_audio" };
|
|
1258
|
+
case "uploading":
|
|
1259
|
+
remote = await client.finalizeTranscriptionRecording(
|
|
1260
|
+
input.workspaceId,
|
|
1261
|
+
input.manifest.recordingId,
|
|
1262
|
+
finalizeInput,
|
|
1263
|
+
);
|
|
1264
|
+
scheduleRecoveryMutation(remote);
|
|
1265
|
+
break;
|
|
1266
|
+
case "segmenting":
|
|
1267
|
+
case "transcribing":
|
|
1268
|
+
if (Date.now() < recoveryMutationDueAt) {
|
|
1269
|
+
await abortableDelay(
|
|
1270
|
+
Math.min(
|
|
1271
|
+
recoveryMutationDueAt - Date.now(),
|
|
1272
|
+
VOICE_RECORDING_RECOVERY_STATUS_POLL_MILLISECONDS,
|
|
1273
|
+
),
|
|
1274
|
+
input.signal,
|
|
1275
|
+
);
|
|
1276
|
+
remote = await client.getTranscriptionRecording(
|
|
1277
|
+
input.workspaceId,
|
|
1278
|
+
input.manifest.recordingId,
|
|
1279
|
+
{ signal: input.signal },
|
|
1280
|
+
);
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
if (remote.recording.state === "segmenting") {
|
|
1284
|
+
// Re-enter the server-authoritative assembly claim only when its
|
|
1285
|
+
// durable recovery schedule is due.
|
|
1286
|
+
remote = await client.finalizeTranscriptionRecording(
|
|
1287
|
+
input.workspaceId,
|
|
1288
|
+
input.manifest.recordingId,
|
|
1289
|
+
finalizeInput,
|
|
1290
|
+
);
|
|
1291
|
+
} else {
|
|
1292
|
+
// Re-enter the server-authoritative segment claim only when its
|
|
1293
|
+
// durable recovery schedule is due.
|
|
1294
|
+
remote = await client.processNextTranscriptionRecordingSegment(
|
|
1295
|
+
input.workspaceId,
|
|
1296
|
+
input.manifest.recordingId,
|
|
1297
|
+
{ signal: input.signal },
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
scheduleRecoveryMutation(remote);
|
|
1301
|
+
break;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function isResumableVoiceInputClient(
|
|
1307
|
+
client: VoiceInputClient | null,
|
|
1308
|
+
): client is VoiceInputClient & ResumableVoiceInputClient {
|
|
1309
|
+
return Boolean(
|
|
1310
|
+
client &&
|
|
1311
|
+
client.createTranscriptionRecording &&
|
|
1312
|
+
client.getTranscriptionRecording &&
|
|
1313
|
+
client.uploadTranscriptionRecordingChunk &&
|
|
1314
|
+
client.finalizeTranscriptionRecording &&
|
|
1315
|
+
client.processNextTranscriptionRecordingSegment &&
|
|
1316
|
+
client.discardTranscriptionRecording,
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
async function abortableDelay(milliseconds: number, signal: AbortSignal): Promise<void> {
|
|
1321
|
+
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
1322
|
+
await new Promise<void>((resolve, reject) => {
|
|
1323
|
+
const timer = setTimeout(() => {
|
|
1324
|
+
signal.removeEventListener("abort", onAbort);
|
|
1325
|
+
resolve();
|
|
1326
|
+
}, milliseconds);
|
|
1327
|
+
const onAbort = () => {
|
|
1328
|
+
clearTimeout(timer);
|
|
1329
|
+
signal.removeEventListener("abort", onAbort);
|
|
1330
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
1331
|
+
};
|
|
1332
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1035
1336
|
function chooseMimeType(accepted: string[]): string | undefined {
|
|
1036
1337
|
const normalized = accepted.map((value) => value.trim().toLowerCase());
|
|
1037
1338
|
return MIME_PREFERENCES.find((mimeType) => {
|
package/src/index.ts
CHANGED
|
@@ -460,6 +460,8 @@ export {
|
|
|
460
460
|
export type { LatencyModeId, PickerBillingClass, PickerModelRow } from "./model-policy";
|
|
461
461
|
export { MessageTimeline, TimelineRow } from "./components/message-timeline";
|
|
462
462
|
export type { MessageTimelineProps } from "./components/message-timeline";
|
|
463
|
+
export { UserMessageBody, userMessageLikelyNeedsDisclosure } from "./components/user-message-body";
|
|
464
|
+
export type { UserMessageBodyProps } from "./components/user-message-body";
|
|
463
465
|
export { Markdown } from "./components/markdown";
|
|
464
466
|
export { CopyButton, CopyHoverFrame } from "./components/copy-button";
|
|
465
467
|
export { copyTextToClipboard, tableElementToTsv } from "./lib/clipboard";
|
|
@@ -746,11 +746,11 @@ export function RealtimeVoiceControl(props: {
|
|
|
746
746
|
title={`Voice model: ${selectedModel.label}`}
|
|
747
747
|
disabled={props.selectionDisabled}
|
|
748
748
|
className={cn(
|
|
749
|
-
//
|
|
750
|
-
|
|
749
|
+
// Keep the split trigger compact on desktop while preserving a
|
|
750
|
+
// full 44px mobile target independent of pointer-media support.
|
|
751
|
+
"inline-flex h-11 w-11 items-center justify-center rounded-r-og-md outline-none sm:h-8 sm:w-6",
|
|
751
752
|
"transition-[background-color,border-color,color] duration-200 ease-og-out",
|
|
752
753
|
"focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-og-accent/45",
|
|
753
|
-
"pointer-coarse:h-11 pointer-coarse:w-7",
|
|
754
754
|
desktopOnlyModelMenu && "hidden sm:inline-flex",
|
|
755
755
|
voiceChevronTone(status.phase),
|
|
756
756
|
)}
|
package/src/session-ui.ts
CHANGED
|
@@ -11,6 +11,8 @@ export { HumanInputSurface } from "./components/human-input-surface";
|
|
|
11
11
|
export type { HumanInputSurfaceProps } from "./components/human-input-surface";
|
|
12
12
|
export { MessageTimeline, TimelineRow } from "./components/message-timeline";
|
|
13
13
|
export type { MessageTimelineProps } from "./components/message-timeline";
|
|
14
|
+
export { UserMessageBody, userMessageLikelyNeedsDisclosure } from "./components/user-message-body";
|
|
15
|
+
export type { UserMessageBodyProps } from "./components/user-message-body";
|
|
14
16
|
export { BUILT_IN_TURN_SUMMARY_FACET_IDS } from "./timeline/turn-summary";
|
|
15
17
|
export type {
|
|
16
18
|
BuiltInTurnSummaryFacetId,
|
|
@@ -419,7 +419,7 @@ function WorkerRow({
|
|
|
419
419
|
className={cn(
|
|
420
420
|
"group/worker -mx-1.5 flex w-full items-start gap-2 rounded-og-sm px-1.5 py-1.5 text-left",
|
|
421
421
|
"outline-none transition-colors duration-150 hover:bg-og-surface-1 focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
422
|
-
"pointer-coarse:py-2.5",
|
|
422
|
+
"pointer-coarse:min-h-11 pointer-coarse:py-2.5",
|
|
423
423
|
)}
|
|
424
424
|
>
|
|
425
425
|
{inner}
|
package/src/timeline/shared.tsx
CHANGED
|
@@ -154,9 +154,9 @@ export function ActivityDisclosure({
|
|
|
154
154
|
const rowClass = cn(
|
|
155
155
|
"group/disclosure flex w-full min-w-0 items-center gap-2 rounded-og-sm px-1.5 py-1.5 text-left text-og-base",
|
|
156
156
|
"text-og-fg-muted transition-colors duration-150",
|
|
157
|
-
// A tool row is a touch target on coarse pointers: grow its
|
|
158
|
-
//
|
|
159
|
-
"pointer-coarse:py-2.5",
|
|
157
|
+
// A tool row is a touch target on coarse pointers: grow its hit area to the
|
|
158
|
+
// 44px mobile minimum without loosening the dense desktop rail.
|
|
159
|
+
"pointer-coarse:min-h-11 pointer-coarse:py-2.5",
|
|
160
160
|
);
|
|
161
161
|
// The chevron rotates to point down when open; it tracks `data-state` on this
|
|
162
162
|
// same row (the Trigger), so the affordance never freezes.
|
|
@@ -416,8 +416,8 @@ export function TurnSummary({
|
|
|
416
416
|
// as a turn landmark above any nested cluster rows it groups.
|
|
417
417
|
"group flex w-full items-center rounded-og-sm text-left transition-colors",
|
|
418
418
|
// A folded turn is a touch target on coarse pointers: grow the row so it
|
|
419
|
-
// clears the
|
|
420
|
-
"pointer-coarse:py-2.5",
|
|
419
|
+
// clears the 44px minimum without disturbing the calm desktop rhythm.
|
|
420
|
+
"pointer-coarse:min-h-11 pointer-coarse:py-2.5",
|
|
421
421
|
bare
|
|
422
422
|
? "gap-2 px-1.5 py-1.5 text-og-sm text-og-fg-muted"
|
|
423
423
|
: "-mx-2 gap-2.5 px-2 py-1.5 text-og-base text-og-fg-muted",
|
|
@@ -646,8 +646,8 @@ const BUILT_IN_TURN_SUMMARY_FACETS: readonly TurnSummaryFacet[] = Object.freeze(
|
|
|
646
646
|
contextCompactionCount === 1 ? "compacted" : `${contextCompactionCount} compacts`,
|
|
647
647
|
ariaLabel:
|
|
648
648
|
contextCompactionCount === 1
|
|
649
|
-
? "Conversation
|
|
650
|
-
: `${contextCompactionCount} conversation
|
|
649
|
+
? "Conversation history compacted"
|
|
650
|
+
: `${contextCompactionCount} conversation history compactions`,
|
|
651
651
|
}
|
|
652
652
|
: null,
|
|
653
653
|
},
|