@opengeni/react 0.40.0 → 0.42.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/dist/{chunk-OMCFRWHL.js → chunk-UWYTCQWW.js} +1353 -82
- package/dist/chunk-UWYTCQWW.js.map +1 -0
- package/dist/components/composer-transcription-control.d.ts +14 -1
- package/dist/composer.d.ts +3 -1
- package/dist/composer.js +27 -1
- package/dist/hooks/use-voice-input.d.ts +25 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +27 -1
- package/dist/index.js.map +1 -1
- package/dist/voice-recording-owner.d.ts +12 -0
- package/dist/voice-recording-store.d.ts +124 -0
- package/package.json +2 -2
- package/src/components/composer-transcription-control.tsx +130 -18
- package/src/composer.ts +30 -1
- package/src/hooks/use-voice-input.ts +893 -67
- package/src/index.ts +30 -1
- package/src/voice-recording-owner.ts +251 -0
- package/src/voice-recording-store.ts +528 -0
- package/dist/chunk-OMCFRWHL.js.map +0 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const VOICE_RECORDING_OWNER_SESSION_KEY = "opengeni.voice-recording-owner.v1";
|
|
2
|
+
export type VoiceRecordingOwnerLease = {
|
|
3
|
+
ownerId: string;
|
|
4
|
+
release: () => void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Acquire one document-scoped owner identity shared by every voice hook in the
|
|
8
|
+
* current document. The session-stored candidate survives reload, while the
|
|
9
|
+
* held lock/handshake prevents opener-created or duplicated tabs from reusing
|
|
10
|
+
* that identity concurrently.
|
|
11
|
+
*/
|
|
12
|
+
export declare function acquireDefaultVoiceRecordingOwnerLease(): Promise<VoiceRecordingOwnerLease>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export type VoiceRecordingCaptureState = "capturing" | "stopped" | "discarded";
|
|
2
|
+
export type VoiceRecordingUploadState = "pending" | "syncing" | "retrying" | "complete";
|
|
3
|
+
export type VoiceRecordingTranscriptionState = "pending" | "transcribing" | "retrying" | "complete";
|
|
4
|
+
export type VoiceRecordingFinalizationState = "pending" | "transcript-ready" | "handed-off";
|
|
5
|
+
export type VoiceRecordingChunkUploadState = "pending" | "syncing" | "complete";
|
|
6
|
+
export type VoiceRecordingManifest = {
|
|
7
|
+
version: 1;
|
|
8
|
+
recordingId: string;
|
|
9
|
+
workspaceId: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
mimeType: string;
|
|
13
|
+
codec: string | null;
|
|
14
|
+
captureState: VoiceRecordingCaptureState;
|
|
15
|
+
uploadState: VoiceRecordingUploadState;
|
|
16
|
+
transcriptionState: VoiceRecordingTranscriptionState;
|
|
17
|
+
finalizationState: VoiceRecordingFinalizationState;
|
|
18
|
+
/** Tab/process lease. A different owner may take over only after this heartbeat is stale. */
|
|
19
|
+
ownerId: string | null;
|
|
20
|
+
ownerHeartbeatAt: string | null;
|
|
21
|
+
/** Authoritative provider result, persisted before any composer draft mutation. */
|
|
22
|
+
transcriptText: string | null;
|
|
23
|
+
nextChunkNumber: number;
|
|
24
|
+
chunkCount: number;
|
|
25
|
+
totalBytes: number;
|
|
26
|
+
totalDurationMilliseconds: number;
|
|
27
|
+
};
|
|
28
|
+
export type VoiceRecordingChunk = {
|
|
29
|
+
recordingId: string;
|
|
30
|
+
chunkNumber: number;
|
|
31
|
+
capturedAt: string;
|
|
32
|
+
startMilliseconds: number;
|
|
33
|
+
durationMilliseconds: number;
|
|
34
|
+
mimeType: string;
|
|
35
|
+
codec: string | null;
|
|
36
|
+
byteLength: number;
|
|
37
|
+
sha256: string;
|
|
38
|
+
uploadState: VoiceRecordingChunkUploadState;
|
|
39
|
+
audio: Blob;
|
|
40
|
+
};
|
|
41
|
+
export type PersistVoiceRecordingChunkInput = {
|
|
42
|
+
recordingId: string;
|
|
43
|
+
ownerId?: string | undefined;
|
|
44
|
+
chunkNumber: number;
|
|
45
|
+
capturedAt: string;
|
|
46
|
+
startMilliseconds: number;
|
|
47
|
+
durationMilliseconds: number;
|
|
48
|
+
mimeType: string;
|
|
49
|
+
audio: Blob;
|
|
50
|
+
};
|
|
51
|
+
export type PersistVoiceRecordingChunkResult = {
|
|
52
|
+
manifest: VoiceRecordingManifest;
|
|
53
|
+
chunk: VoiceRecordingChunk;
|
|
54
|
+
deduplicated: boolean;
|
|
55
|
+
};
|
|
56
|
+
export interface VoiceRecordingStore {
|
|
57
|
+
createManifest(manifest: VoiceRecordingManifest): Promise<void>;
|
|
58
|
+
getManifest(recordingId: string): Promise<VoiceRecordingManifest | null>;
|
|
59
|
+
listRecoverableManifests(workspaceId: string, ownership?: {
|
|
60
|
+
ownerId: string;
|
|
61
|
+
staleBefore: string;
|
|
62
|
+
}): Promise<VoiceRecordingManifest[]>;
|
|
63
|
+
claimManifest(recordingId: string, ownerId: string, claimedAt: string, staleBefore: string): Promise<VoiceRecordingManifest>;
|
|
64
|
+
listChunks(recordingId: string): Promise<VoiceRecordingChunk[]>;
|
|
65
|
+
persistChunk(input: PersistVoiceRecordingChunkInput): Promise<PersistVoiceRecordingChunkResult>;
|
|
66
|
+
updateManifest(recordingId: string, update: Partial<Pick<VoiceRecordingManifest, "captureState" | "uploadState" | "transcriptionState" | "finalizationState" | "ownerId" | "ownerHeartbeatAt" | "transcriptText">>, updatedAt: string, ownerId?: string | undefined): Promise<VoiceRecordingManifest>;
|
|
67
|
+
discard(recordingId: string, ownerId?: string | undefined): Promise<void>;
|
|
68
|
+
cleanupHandedOffManifests(ownership: {
|
|
69
|
+
ownerId: string;
|
|
70
|
+
staleBefore: string;
|
|
71
|
+
}): Promise<number>;
|
|
72
|
+
close(): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
export declare class VoiceRecordingStorageUnavailableError extends Error {
|
|
75
|
+
constructor();
|
|
76
|
+
}
|
|
77
|
+
export declare class VoiceRecordingNotFoundError extends Error {
|
|
78
|
+
constructor(recordingId: string);
|
|
79
|
+
}
|
|
80
|
+
export declare class VoiceRecordingChunkConflictError extends Error {
|
|
81
|
+
constructor(recordingId: string, chunkNumber: number);
|
|
82
|
+
}
|
|
83
|
+
export declare class VoiceRecordingChunkSequenceError extends Error {
|
|
84
|
+
constructor(expected: number, received: number);
|
|
85
|
+
}
|
|
86
|
+
export declare class VoiceRecordingOwnedError extends Error {
|
|
87
|
+
constructor(recordingId: string);
|
|
88
|
+
}
|
|
89
|
+
export declare function createVoiceRecordingManifest(input: {
|
|
90
|
+
recordingId: string;
|
|
91
|
+
workspaceId: string;
|
|
92
|
+
mimeType: string;
|
|
93
|
+
createdAt: string;
|
|
94
|
+
ownerId?: string | null | undefined;
|
|
95
|
+
}): VoiceRecordingManifest;
|
|
96
|
+
export declare function prepareVoiceRecordingChunk(input: PersistVoiceRecordingChunkInput): Promise<VoiceRecordingChunk>;
|
|
97
|
+
export declare function planVoiceRecordingChunkCommit(input: {
|
|
98
|
+
manifest: VoiceRecordingManifest;
|
|
99
|
+
chunk: VoiceRecordingChunk;
|
|
100
|
+
existingChunk: VoiceRecordingChunk | null;
|
|
101
|
+
}): PersistVoiceRecordingChunkResult;
|
|
102
|
+
export declare class IndexedDbVoiceRecordingStore implements VoiceRecordingStore {
|
|
103
|
+
private readonly database;
|
|
104
|
+
constructor(options?: {
|
|
105
|
+
indexedDB?: IDBFactory | null;
|
|
106
|
+
databaseName?: string;
|
|
107
|
+
});
|
|
108
|
+
createManifest(manifest: VoiceRecordingManifest): Promise<void>;
|
|
109
|
+
getManifest(recordingId: string): Promise<VoiceRecordingManifest | null>;
|
|
110
|
+
listRecoverableManifests(workspaceId: string, ownership?: {
|
|
111
|
+
ownerId: string;
|
|
112
|
+
staleBefore: string;
|
|
113
|
+
}): Promise<VoiceRecordingManifest[]>;
|
|
114
|
+
claimManifest(recordingId: string, ownerId: string, claimedAt: string, staleBefore: string): Promise<VoiceRecordingManifest>;
|
|
115
|
+
listChunks(recordingId: string): Promise<VoiceRecordingChunk[]>;
|
|
116
|
+
persistChunk(input: PersistVoiceRecordingChunkInput): Promise<PersistVoiceRecordingChunkResult>;
|
|
117
|
+
updateManifest(recordingId: string, update: Partial<Pick<VoiceRecordingManifest, "captureState" | "uploadState" | "transcriptionState" | "finalizationState" | "ownerId" | "ownerHeartbeatAt" | "transcriptText">>, updatedAt: string, ownerId?: string | undefined): Promise<VoiceRecordingManifest>;
|
|
118
|
+
discard(recordingId: string, ownerId?: string | undefined): Promise<void>;
|
|
119
|
+
cleanupHandedOffManifests(ownership: {
|
|
120
|
+
ownerId: string;
|
|
121
|
+
staleBefore: string;
|
|
122
|
+
}): Promise<number>;
|
|
123
|
+
close(): Promise<void>;
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.1",
|
|
4
4
|
"description": "React hooks and styled components for OpenGeni: live session streaming, chat composer, message timeline, session status, and fleet views — token-themed (CSS variables), dark-first, built on Tailwind v4 + Radix + Motion.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@dnd-kit/utilities": "3.2.2",
|
|
76
76
|
"@fontsource-variable/noto-sans-arabic": "5.2.10",
|
|
77
77
|
"@fontsource-variable/noto-sans-jp": "5.2.10",
|
|
78
|
-
"@opengeni/sdk": "^0.
|
|
78
|
+
"@opengeni/sdk": "^0.42.1",
|
|
79
79
|
"clsx": "^2.1.1",
|
|
80
80
|
"lucide-react": "^1.8.0",
|
|
81
81
|
"motion": "^12.0.0",
|
|
@@ -4,11 +4,20 @@ import type {
|
|
|
4
4
|
TranscriptionAdapter,
|
|
5
5
|
WorkspaceTranscriptionPolicy,
|
|
6
6
|
} from "@opengeni/sdk";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
ClipboardPasteIcon,
|
|
9
|
+
LoaderCircleIcon,
|
|
10
|
+
MicIcon,
|
|
11
|
+
RefreshCwIcon,
|
|
12
|
+
SquareIcon,
|
|
13
|
+
Trash2Icon,
|
|
14
|
+
XIcon,
|
|
15
|
+
} from "lucide-react";
|
|
8
16
|
import { AnimatePresence, motion } from "motion/react";
|
|
9
17
|
import { useEffect, useState, type MouseEvent, type ReactElement } from "react";
|
|
10
18
|
import { cn } from "../lib/cn";
|
|
11
19
|
import { useVoiceInput } from "../hooks/use-voice-input";
|
|
20
|
+
import type { VoiceRecordingStore } from "../voice-recording-store";
|
|
12
21
|
import { useChatComposer } from "./composer";
|
|
13
22
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
|
|
14
23
|
|
|
@@ -28,7 +37,12 @@ export type ComposerTranscriptionMessages = {
|
|
|
28
37
|
retry: string;
|
|
29
38
|
requestingPermission: string;
|
|
30
39
|
recording: string;
|
|
40
|
+
saving: string;
|
|
31
41
|
transcribing: string;
|
|
42
|
+
recovered: string;
|
|
43
|
+
recoveredTranscript: string;
|
|
44
|
+
insertRecoveredTranscript: string;
|
|
45
|
+
discardRecovered: string;
|
|
32
46
|
unavailableDisabled: string;
|
|
33
47
|
unavailable: string;
|
|
34
48
|
errorPermissionDenied: string;
|
|
@@ -36,6 +50,9 @@ export type ComposerTranscriptionMessages = {
|
|
|
36
50
|
errorUnavailable: string;
|
|
37
51
|
errorTooLarge: string;
|
|
38
52
|
errorInvalidAudio: string;
|
|
53
|
+
errorStorageUnavailable: string;
|
|
54
|
+
errorRetryable: string;
|
|
55
|
+
errorHandoffUncertain: string;
|
|
39
56
|
errorUnknown: string;
|
|
40
57
|
};
|
|
41
58
|
|
|
@@ -46,7 +63,12 @@ const defaultMessages: ComposerTranscriptionMessages = {
|
|
|
46
63
|
retry: "Retry voice input",
|
|
47
64
|
requestingPermission: "Requesting microphone…",
|
|
48
65
|
recording: "Recording. Press Escape to cancel.",
|
|
66
|
+
saving: "Saving audio locally…",
|
|
49
67
|
transcribing: "Transcribing…",
|
|
68
|
+
recovered: "Recording recovered and saved locally.",
|
|
69
|
+
recoveredTranscript: "Transcript saved locally. Check your draft before inserting.",
|
|
70
|
+
insertRecoveredTranscript: "Insert saved transcript",
|
|
71
|
+
discardRecovered: "Discard saved recording",
|
|
50
72
|
unavailableDisabled: "Voice input is unavailable while the composer is disabled.",
|
|
51
73
|
unavailable: "Voice input is unavailable for this workspace.",
|
|
52
74
|
errorPermissionDenied: "Microphone permission was denied. Your draft was not changed.",
|
|
@@ -54,6 +76,9 @@ const defaultMessages: ComposerTranscriptionMessages = {
|
|
|
54
76
|
errorUnavailable: "Voice input is not configured.",
|
|
55
77
|
errorTooLarge: "Recording is too large. Try a shorter message.",
|
|
56
78
|
errorInvalidAudio: "The recording could not be read. Try again.",
|
|
79
|
+
errorStorageUnavailable: "Voice input stopped because audio could not be saved safely.",
|
|
80
|
+
errorRetryable: "Recording is saved locally. Retry transcription when ready.",
|
|
81
|
+
errorHandoffUncertain: "Transcript is saved. Check your draft before inserting it again.",
|
|
57
82
|
errorUnknown: "Voice input could not start. Try again.",
|
|
58
83
|
};
|
|
59
84
|
|
|
@@ -72,6 +97,10 @@ export type ComposerTranscriptionControlProps = {
|
|
|
72
97
|
onDiagnostic?: unknown;
|
|
73
98
|
messages?: Partial<ComposerTranscriptionMessages> | undefined;
|
|
74
99
|
className?: string | undefined;
|
|
100
|
+
/** Test/embed seam. Production defaults to origin-scoped IndexedDB. */
|
|
101
|
+
createRecordingStore?: (() => VoiceRecordingStore) | undefined;
|
|
102
|
+
/** Test/embed seam. Production acquires a coordinated browser-document owner lease. */
|
|
103
|
+
createOwnerId?: (() => string) | undefined;
|
|
75
104
|
};
|
|
76
105
|
|
|
77
106
|
const WAVEFORM_BARS = 18;
|
|
@@ -88,6 +117,8 @@ export function ComposerTranscriptionControl({
|
|
|
88
117
|
workspaceEnabled = false,
|
|
89
118
|
messages: overrides,
|
|
90
119
|
className,
|
|
120
|
+
createRecordingStore,
|
|
121
|
+
createOwnerId,
|
|
91
122
|
}: ComposerTranscriptionControlProps) {
|
|
92
123
|
const composer = useChatComposer();
|
|
93
124
|
const messages = { ...defaultMessages, ...overrides };
|
|
@@ -100,15 +131,26 @@ export function ComposerTranscriptionControl({
|
|
|
100
131
|
setValue: composer.setValue,
|
|
101
132
|
focusInput: composer.focusInput,
|
|
102
133
|
disabled: composer.disabled,
|
|
134
|
+
createRecordingStore,
|
|
135
|
+
createOwnerId,
|
|
103
136
|
});
|
|
104
137
|
const { status } = transcription;
|
|
105
138
|
const active =
|
|
106
|
-
status === "requesting-permission" ||
|
|
139
|
+
status === "requesting-permission" ||
|
|
140
|
+
status === "recording" ||
|
|
141
|
+
status === "saving" ||
|
|
142
|
+
status === "transcribing";
|
|
143
|
+
const recoverable =
|
|
144
|
+
transcription.hasRecoverableRecording &&
|
|
145
|
+
(status === "recovered" || status === "transcript-ready" || status === "error");
|
|
146
|
+
const savedTranscript = status === "transcript-ready" && transcription.savedTranscript !== null;
|
|
107
147
|
const unavailableMessage = composer.disabled
|
|
108
148
|
? messages.unavailableDisabled
|
|
109
149
|
: !capability?.available || !workspaceEnabled
|
|
110
150
|
? messages.unavailable
|
|
111
|
-
:
|
|
151
|
+
: !transcription.available
|
|
152
|
+
? messages.errorStorageUnavailable
|
|
153
|
+
: null;
|
|
112
154
|
const idleLabel = unavailableMessage ?? (status === "error" ? messages.retry : messages.start);
|
|
113
155
|
const errorMessage = transcription.error
|
|
114
156
|
? transcriptionErrorMessage(transcription.error, messages)
|
|
@@ -118,11 +160,17 @@ export function ComposerTranscriptionControl({
|
|
|
118
160
|
? messages.requestingPermission
|
|
119
161
|
: status === "recording"
|
|
120
162
|
? messages.recording
|
|
121
|
-
: status === "
|
|
122
|
-
? messages.
|
|
123
|
-
: status === "
|
|
124
|
-
?
|
|
125
|
-
:
|
|
163
|
+
: status === "saving"
|
|
164
|
+
? messages.saving
|
|
165
|
+
: status === "transcribing"
|
|
166
|
+
? messages.transcribing
|
|
167
|
+
: status === "recovered"
|
|
168
|
+
? messages.recovered
|
|
169
|
+
: status === "transcript-ready"
|
|
170
|
+
? messages.recoveredTranscript
|
|
171
|
+
: status === "error"
|
|
172
|
+
? (errorMessage ?? messages.errorUnknown)
|
|
173
|
+
: unavailableMessage;
|
|
126
174
|
|
|
127
175
|
function start(event: MouseEvent<HTMLButtonElement>) {
|
|
128
176
|
if (unavailableMessage) {
|
|
@@ -138,16 +186,72 @@ export function ComposerTranscriptionControl({
|
|
|
138
186
|
data-transcription-status={status}
|
|
139
187
|
>
|
|
140
188
|
<AnimatePresence mode="popLayout" initial={false}>
|
|
141
|
-
{
|
|
189
|
+
{recoverable ? (
|
|
142
190
|
<motion.span
|
|
143
|
-
key=
|
|
191
|
+
key="recovered"
|
|
192
|
+
initial={{ opacity: 0, scale: 0.96 }}
|
|
193
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
194
|
+
exit={{ opacity: 0, scale: 0.96 }}
|
|
195
|
+
transition={{ duration: 0.16, ease: [0.22, 1, 0.36, 1] }}
|
|
196
|
+
className={cn(
|
|
197
|
+
"inline-flex h-8 min-w-0 items-center gap-1 rounded-og-md border border-og-border/80",
|
|
198
|
+
"bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11",
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
201
|
+
<span className="max-w-44 truncate text-og-xs text-og-fg-muted max-sm:max-w-28">
|
|
202
|
+
{savedTranscript
|
|
203
|
+
? (errorMessage ?? messages.recoveredTranscript)
|
|
204
|
+
: status === "error"
|
|
205
|
+
? (errorMessage ?? messages.errorRetryable)
|
|
206
|
+
: messages.recovered}
|
|
207
|
+
</span>
|
|
208
|
+
<Tip tip={savedTranscript ? messages.insertRecoveredTranscript : messages.retry}>
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
onClick={() =>
|
|
212
|
+
savedTranscript
|
|
213
|
+
? void transcription.insertSavedTranscript()
|
|
214
|
+
: transcription.retry()
|
|
215
|
+
}
|
|
216
|
+
aria-label={savedTranscript ? messages.insertRecoveredTranscript : messages.retry}
|
|
217
|
+
className={cn(
|
|
218
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
219
|
+
"bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
|
|
220
|
+
"hover:bg-og-fg-muted pointer-coarse:size-11",
|
|
221
|
+
)}
|
|
222
|
+
>
|
|
223
|
+
{savedTranscript ? (
|
|
224
|
+
<ClipboardPasteIcon className="size-3.5" />
|
|
225
|
+
) : (
|
|
226
|
+
<RefreshCwIcon className="size-3.5" />
|
|
227
|
+
)}
|
|
228
|
+
</button>
|
|
229
|
+
</Tip>
|
|
230
|
+
<Tip tip={messages.discardRecovered}>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={() => void transcription.discard()}
|
|
234
|
+
aria-label={messages.discardRecovered}
|
|
235
|
+
className={cn(
|
|
236
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
237
|
+
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
238
|
+
"hover:bg-og-surface-3 hover:text-og-status-failed pointer-coarse:size-11",
|
|
239
|
+
)}
|
|
240
|
+
>
|
|
241
|
+
<Trash2Icon className="size-3.5" />
|
|
242
|
+
</button>
|
|
243
|
+
</Tip>
|
|
244
|
+
</motion.span>
|
|
245
|
+
) : active ? (
|
|
246
|
+
<motion.span
|
|
247
|
+
key={status === "transcribing" || status === "saving" ? "processing" : "capture"}
|
|
144
248
|
initial={{ opacity: 0, scale: 0.96 }}
|
|
145
249
|
animate={{ opacity: 1, scale: 1 }}
|
|
146
250
|
exit={{ opacity: 0, scale: 0.96 }}
|
|
147
251
|
transition={{ duration: 0.16, ease: [0.22, 1, 0.36, 1] }}
|
|
148
252
|
className={cn(
|
|
149
253
|
"inline-flex h-8 items-center gap-1 rounded-og-md border border-og-border/80",
|
|
150
|
-
"bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-
|
|
254
|
+
"bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11",
|
|
151
255
|
)}
|
|
152
256
|
>
|
|
153
257
|
{status === "requesting-permission" ? (
|
|
@@ -165,7 +269,7 @@ export function ComposerTranscriptionControl({
|
|
|
165
269
|
) : null}
|
|
166
270
|
<VoiceWaveform
|
|
167
271
|
stream={status === "recording" ? transcription.stream : null}
|
|
168
|
-
mode={status === "
|
|
272
|
+
mode={status === "recording" ? "recording" : "transcribing"}
|
|
169
273
|
/>
|
|
170
274
|
</span>
|
|
171
275
|
)}
|
|
@@ -180,7 +284,7 @@ export function ComposerTranscriptionControl({
|
|
|
180
284
|
className={cn(
|
|
181
285
|
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
182
286
|
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
183
|
-
"hover:bg-og-surface-3 hover:text-og-fg pointer-coarse:size-
|
|
287
|
+
"hover:bg-og-surface-3 hover:text-og-fg pointer-coarse:size-11",
|
|
184
288
|
)}
|
|
185
289
|
>
|
|
186
290
|
<XIcon className="size-3.5" />
|
|
@@ -194,16 +298,16 @@ export function ComposerTranscriptionControl({
|
|
|
194
298
|
className={cn(
|
|
195
299
|
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
196
300
|
"bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
|
|
197
|
-
"hover:bg-og-fg-muted pointer-coarse:size-
|
|
301
|
+
"hover:bg-og-fg-muted pointer-coarse:size-11",
|
|
198
302
|
)}
|
|
199
303
|
>
|
|
200
304
|
<SquareIcon className="size-2.5 fill-current" />
|
|
201
305
|
</button>
|
|
202
306
|
</Tip>
|
|
203
307
|
</>
|
|
204
|
-
) : status === "transcribing" ? (
|
|
308
|
+
) : status === "transcribing" || status === "saving" ? (
|
|
205
309
|
<span className="og-shimmer-text px-1.5 text-og-xs font-medium whitespace-nowrap">
|
|
206
|
-
{messages.transcribing}
|
|
310
|
+
{status === "saving" ? messages.saving : messages.transcribing}
|
|
207
311
|
</span>
|
|
208
312
|
) : (
|
|
209
313
|
<span className="px-1.5 text-og-xs text-og-fg-muted whitespace-nowrap">
|
|
@@ -224,7 +328,7 @@ export function ComposerTranscriptionControl({
|
|
|
224
328
|
aria-pressed={false}
|
|
225
329
|
aria-disabled={unavailableMessage !== null}
|
|
226
330
|
className={cn(
|
|
227
|
-
"inline-flex size-8 shrink-0 items-center justify-center rounded-og-md pointer-coarse:size-
|
|
331
|
+
"inline-flex size-8 shrink-0 items-center justify-center rounded-og-md pointer-coarse:size-11",
|
|
228
332
|
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
229
333
|
unavailableMessage
|
|
230
334
|
? "cursor-not-allowed opacity-45"
|
|
@@ -236,7 +340,7 @@ export function ComposerTranscriptionControl({
|
|
|
236
340
|
</Tip>
|
|
237
341
|
)}
|
|
238
342
|
</AnimatePresence>
|
|
239
|
-
{status === "error" && errorMessage ? (
|
|
343
|
+
{status === "error" && errorMessage && !recoverable ? (
|
|
240
344
|
<Tip tip={errorMessage}>
|
|
241
345
|
<span
|
|
242
346
|
aria-hidden="true"
|
|
@@ -364,6 +468,14 @@ function transcriptionErrorMessage(code: string, messages: ComposerTranscription
|
|
|
364
468
|
return messages.errorTooLarge;
|
|
365
469
|
case "invalid_audio":
|
|
366
470
|
return messages.errorInvalidAudio;
|
|
471
|
+
case "storage_unavailable":
|
|
472
|
+
return messages.errorStorageUnavailable;
|
|
473
|
+
case "network":
|
|
474
|
+
case "provider":
|
|
475
|
+
case "timeout":
|
|
476
|
+
return messages.errorRetryable;
|
|
477
|
+
case "handoff_uncertain":
|
|
478
|
+
return messages.errorHandoffUncertain;
|
|
367
479
|
case "unknown":
|
|
368
480
|
return messages.errorUnknown;
|
|
369
481
|
default:
|
package/src/composer.ts
CHANGED
|
@@ -71,9 +71,38 @@ export type {
|
|
|
71
71
|
ComposerTranscriptionControlProps,
|
|
72
72
|
ComposerTranscriptionMessages,
|
|
73
73
|
} from "./components/composer-transcription-control";
|
|
74
|
-
export {
|
|
74
|
+
export {
|
|
75
|
+
VOICE_RECORDING_CLIENT_MAX_DURATION_SECONDS,
|
|
76
|
+
VOICE_RECORDING_OWNER_HEARTBEAT_MILLISECONDS,
|
|
77
|
+
VOICE_RECORDING_OWNER_STALE_MILLISECONDS,
|
|
78
|
+
VOICE_RECORDING_TIMESLICE_MILLISECONDS,
|
|
79
|
+
useVoiceInput,
|
|
80
|
+
} from "./hooks/use-voice-input";
|
|
75
81
|
export type {
|
|
76
82
|
UseVoiceInputOptions,
|
|
77
83
|
UseVoiceInputResult,
|
|
78
84
|
VoiceInputStatus,
|
|
79
85
|
} from "./hooks/use-voice-input";
|
|
86
|
+
export {
|
|
87
|
+
IndexedDbVoiceRecordingStore,
|
|
88
|
+
VoiceRecordingChunkConflictError,
|
|
89
|
+
VoiceRecordingChunkSequenceError,
|
|
90
|
+
VoiceRecordingNotFoundError,
|
|
91
|
+
VoiceRecordingOwnedError,
|
|
92
|
+
VoiceRecordingStorageUnavailableError,
|
|
93
|
+
createVoiceRecordingManifest,
|
|
94
|
+
planVoiceRecordingChunkCommit,
|
|
95
|
+
prepareVoiceRecordingChunk,
|
|
96
|
+
} from "./voice-recording-store";
|
|
97
|
+
export type {
|
|
98
|
+
PersistVoiceRecordingChunkInput,
|
|
99
|
+
PersistVoiceRecordingChunkResult,
|
|
100
|
+
VoiceRecordingCaptureState,
|
|
101
|
+
VoiceRecordingChunk,
|
|
102
|
+
VoiceRecordingChunkUploadState,
|
|
103
|
+
VoiceRecordingFinalizationState,
|
|
104
|
+
VoiceRecordingManifest,
|
|
105
|
+
VoiceRecordingStore,
|
|
106
|
+
VoiceRecordingTranscriptionState,
|
|
107
|
+
VoiceRecordingUploadState,
|
|
108
|
+
} from "./voice-recording-store";
|