@opengeni/react 0.41.0 → 0.44.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-SJKT4TKW.js → chunk-23EJ676W.js} +3 -1
- package/dist/chunk-23EJ676W.js.map +1 -0
- package/dist/{chunk-OMCFRWHL.js → chunk-KC27K42G.js} +1443 -101
- package/dist/chunk-KC27K42G.js.map +1 -0
- package/dist/{chunk-KR2SK5GJ.js → chunk-KG5F2OKE.js} +260 -93
- package/dist/chunk-KG5F2OKE.js.map +1 -0
- package/dist/{chunk-4IJCL7YO.js → chunk-LWR4MXSS.js} +4 -1
- package/dist/chunk-LWR4MXSS.js.map +1 -0
- package/dist/{chunk-JALF5FI3.js → chunk-SRFUT2ZU.js} +2 -2
- package/dist/{chunk-WZT5G5OR.js → chunk-U6K24XQD.js} +5 -4
- package/dist/{chunk-WZT5G5OR.js.map → chunk-U6K24XQD.js.map} +1 -1
- package/dist/components/chat-composer.d.ts +5 -1
- package/dist/components/composer-transcription-control.d.ts +16 -1
- package/dist/components/composer.d.ts +6 -4
- package/dist/components/session-chrome.d.ts +1 -1
- package/dist/composer.d.ts +3 -1
- package/dist/composer.js +29 -3
- package/dist/hooks/use-voice-input.d.ts +25 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.js +95 -20
- package/dist/index.js.map +1 -1
- package/dist/model-policy.d.ts +2 -0
- package/dist/model-policy.js +1 -1
- package/dist/realtime/realtime-control.d.ts +29 -1
- package/dist/realtime.d.ts +1 -1
- package/dist/realtime.js +269 -151
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.js +2 -2
- package/dist/session.js +2 -2
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/parsers.d.ts +13 -8
- 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/chat-composer.tsx +58 -19
- package/src/components/composer-transcription-control.tsx +258 -117
- package/src/components/composer.tsx +78 -14
- package/src/components/model-policy-picker.tsx +7 -2
- package/src/components/session-chrome.tsx +89 -78
- package/src/composer.ts +30 -1
- package/src/hooks/use-voice-input.ts +893 -67
- package/src/index.ts +32 -1
- package/src/model-policy.ts +4 -0
- package/src/realtime/realtime-control.tsx +307 -137
- package/src/realtime.ts +1 -0
- package/src/timeline/index.ts +2 -0
- package/src/timeline/parsers.ts +201 -19
- package/src/timeline/projection.ts +11 -0
- package/src/timeline/tool-renderers.tsx +7 -5
- package/src/timeline/turn-summary.tsx +7 -2
- package/src/voice-recording-owner.ts +251 -0
- package/src/voice-recording-store.ts +528 -0
- package/styles/tokens.css +8 -5
- package/dist/chunk-4IJCL7YO.js.map +0 -1
- package/dist/chunk-KR2SK5GJ.js.map +0 -1
- package/dist/chunk-OMCFRWHL.js.map +0 -1
- package/dist/chunk-SJKT4TKW.js.map +0 -1
- /package/dist/{chunk-JALF5FI3.js.map → chunk-SRFUT2ZU.js.map} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClientModel, EffectiveSessionControl } from "@opengeni/sdk";
|
|
2
|
+
import { LayoutGroup, motion } from "motion/react";
|
|
2
3
|
import type { ClipboardEvent, ReactNode } from "react";
|
|
3
4
|
import type { SlashCommand } from "../commands/types";
|
|
4
5
|
import type { ComposerState } from "../hooks/use-composer";
|
|
@@ -61,6 +62,10 @@ export type ChatComposerProps = {
|
|
|
61
62
|
attachButtonClassName?: string | undefined;
|
|
62
63
|
/** Provider-neutral speech capability. Provider configuration stays in workspace settings. */
|
|
63
64
|
transcription?: ComposerTranscriptionControlProps | undefined;
|
|
65
|
+
/** Extra classes on the transcription control. */
|
|
66
|
+
transcriptionClassName?: string | undefined;
|
|
67
|
+
/** Soft-hide dictate while realtime voice is active (animated collapse). */
|
|
68
|
+
transcriptionSuppressed?: boolean | undefined;
|
|
64
69
|
/** Content rendered above the textarea, inside the field chrome. */
|
|
65
70
|
header?: ReactNode | undefined;
|
|
66
71
|
/** Paste hook composed with the attachment paste path. */
|
|
@@ -99,6 +104,8 @@ export function ChatComposer({
|
|
|
99
104
|
actionsStart,
|
|
100
105
|
attachButtonClassName,
|
|
101
106
|
transcription,
|
|
107
|
+
transcriptionClassName,
|
|
108
|
+
transcriptionSuppressed = false,
|
|
102
109
|
header,
|
|
103
110
|
onPaste,
|
|
104
111
|
attachments,
|
|
@@ -145,25 +152,57 @@ export function ChatComposer({
|
|
|
145
152
|
{controller.confirmState ? (
|
|
146
153
|
<Confirmation />
|
|
147
154
|
) : (
|
|
148
|
-
<Footer className={stackActions ? "flex-wrap" : undefined}>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
155
|
+
<Footer className={stackActions ? "max-sm:flex-nowrap sm:flex-wrap" : undefined}>
|
|
156
|
+
<LayoutGroup id="og-composer-footer">
|
|
157
|
+
{hasControls ? (
|
|
158
|
+
<Controls
|
|
159
|
+
className={stackActions ? "min-w-0 max-sm:flex-1 sm:w-auto" : undefined}
|
|
160
|
+
>
|
|
161
|
+
{controlsLeading}
|
|
162
|
+
<AttachButton className={attachButtonClassName} />
|
|
163
|
+
{transcription ? (
|
|
164
|
+
<ComposerTranscriptionControl
|
|
165
|
+
{...transcription}
|
|
166
|
+
suppressed={transcriptionSuppressed}
|
|
167
|
+
className={[transcription.className, transcriptionClassName]
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.join(" ")}
|
|
170
|
+
/>
|
|
171
|
+
) : null}
|
|
172
|
+
{models ? (
|
|
173
|
+
<motion.span
|
|
174
|
+
layout
|
|
175
|
+
transition={{ duration: 0.36, ease: [0.22, 1, 0.36, 1] }}
|
|
176
|
+
className="inline-flex min-w-0"
|
|
177
|
+
>
|
|
178
|
+
<ModelPicker
|
|
179
|
+
models={models}
|
|
180
|
+
value={selectedModel}
|
|
181
|
+
onChange={onSelectModel}
|
|
182
|
+
/>
|
|
183
|
+
</motion.span>
|
|
184
|
+
) : null}
|
|
185
|
+
{controlsStart ? (
|
|
186
|
+
<motion.span
|
|
187
|
+
layout
|
|
188
|
+
transition={{ duration: 0.36, ease: [0.22, 1, 0.36, 1] }}
|
|
189
|
+
className="inline-flex min-w-0 items-center gap-1.5"
|
|
190
|
+
>
|
|
191
|
+
{controlsStart}
|
|
192
|
+
</motion.span>
|
|
193
|
+
) : null}
|
|
194
|
+
</Controls>
|
|
195
|
+
) : (
|
|
196
|
+
<Hint>{hint}</Hint>
|
|
197
|
+
)}
|
|
198
|
+
<Actions
|
|
199
|
+
className={stackActions ? "max-sm:shrink-0 sm:w-auto sm:justify-end" : undefined}
|
|
200
|
+
>
|
|
201
|
+
{actionsStart}
|
|
202
|
+
<PauseButton />
|
|
203
|
+
<SendButton />
|
|
204
|
+
</Actions>
|
|
205
|
+
</LayoutGroup>
|
|
167
206
|
</Footer>
|
|
168
207
|
)}
|
|
169
208
|
</Surface>
|
|
@@ -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,12 @@ 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;
|
|
104
|
+
/** Hide while realtime voice owns the mic — collapses with a layout-friendly exit. */
|
|
105
|
+
suppressed?: boolean | undefined;
|
|
75
106
|
};
|
|
76
107
|
|
|
77
108
|
const WAVEFORM_BARS = 18;
|
|
@@ -88,6 +119,9 @@ export function ComposerTranscriptionControl({
|
|
|
88
119
|
workspaceEnabled = false,
|
|
89
120
|
messages: overrides,
|
|
90
121
|
className,
|
|
122
|
+
createRecordingStore,
|
|
123
|
+
createOwnerId,
|
|
124
|
+
suppressed = false,
|
|
91
125
|
}: ComposerTranscriptionControlProps) {
|
|
92
126
|
const composer = useChatComposer();
|
|
93
127
|
const messages = { ...defaultMessages, ...overrides };
|
|
@@ -95,20 +129,39 @@ export function ComposerTranscriptionControl({
|
|
|
95
129
|
client,
|
|
96
130
|
workspaceId,
|
|
97
131
|
capability,
|
|
98
|
-
enabled: workspaceEnabled,
|
|
132
|
+
enabled: workspaceEnabled && !suppressed,
|
|
99
133
|
value: composer.value,
|
|
100
134
|
setValue: composer.setValue,
|
|
101
135
|
focusInput: composer.focusInput,
|
|
102
136
|
disabled: composer.disabled,
|
|
137
|
+
createRecordingStore,
|
|
138
|
+
createOwnerId,
|
|
103
139
|
});
|
|
104
140
|
const { status } = transcription;
|
|
141
|
+
|
|
142
|
+
const cancelTranscription = transcription.cancel;
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!suppressed) return;
|
|
145
|
+
if (status === "recording" || status === "requesting-permission") {
|
|
146
|
+
cancelTranscription();
|
|
147
|
+
}
|
|
148
|
+
}, [cancelTranscription, status, suppressed]);
|
|
105
149
|
const active =
|
|
106
|
-
status === "requesting-permission" ||
|
|
150
|
+
status === "requesting-permission" ||
|
|
151
|
+
status === "recording" ||
|
|
152
|
+
status === "saving" ||
|
|
153
|
+
status === "transcribing";
|
|
154
|
+
const recoverable =
|
|
155
|
+
transcription.hasRecoverableRecording &&
|
|
156
|
+
(status === "recovered" || status === "transcript-ready" || status === "error");
|
|
157
|
+
const savedTranscript = status === "transcript-ready" && transcription.savedTranscript !== null;
|
|
107
158
|
const unavailableMessage = composer.disabled
|
|
108
159
|
? messages.unavailableDisabled
|
|
109
160
|
: !capability?.available || !workspaceEnabled
|
|
110
161
|
? messages.unavailable
|
|
111
|
-
:
|
|
162
|
+
: !transcription.available
|
|
163
|
+
? messages.errorStorageUnavailable
|
|
164
|
+
: null;
|
|
112
165
|
const idleLabel = unavailableMessage ?? (status === "error" ? messages.retry : messages.start);
|
|
113
166
|
const errorMessage = transcription.error
|
|
114
167
|
? transcriptionErrorMessage(transcription.error, messages)
|
|
@@ -118,11 +171,17 @@ export function ComposerTranscriptionControl({
|
|
|
118
171
|
? messages.requestingPermission
|
|
119
172
|
: status === "recording"
|
|
120
173
|
? messages.recording
|
|
121
|
-
: status === "
|
|
122
|
-
? messages.
|
|
123
|
-
: status === "
|
|
124
|
-
?
|
|
125
|
-
:
|
|
174
|
+
: status === "saving"
|
|
175
|
+
? messages.saving
|
|
176
|
+
: status === "transcribing"
|
|
177
|
+
? messages.transcribing
|
|
178
|
+
: status === "recovered"
|
|
179
|
+
? messages.recovered
|
|
180
|
+
: status === "transcript-ready"
|
|
181
|
+
? messages.recoveredTranscript
|
|
182
|
+
: status === "error"
|
|
183
|
+
? (errorMessage ?? messages.errorUnknown)
|
|
184
|
+
: unavailableMessage;
|
|
126
185
|
|
|
127
186
|
function start(event: MouseEvent<HTMLButtonElement>) {
|
|
128
187
|
if (unavailableMessage) {
|
|
@@ -133,123 +192,197 @@ export function ComposerTranscriptionControl({
|
|
|
133
192
|
}
|
|
134
193
|
|
|
135
194
|
return (
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<span
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
195
|
+
<AnimatePresence initial={false}>
|
|
196
|
+
{suppressed ? null : (
|
|
197
|
+
<motion.span
|
|
198
|
+
key="composer-dictate"
|
|
199
|
+
initial={{ opacity: 0, width: 0 }}
|
|
200
|
+
animate={{ opacity: 1, width: "auto" }}
|
|
201
|
+
exit={{ opacity: 0, width: 0 }}
|
|
202
|
+
transition={{ duration: 0.36, ease: [0.22, 1, 0.36, 1] }}
|
|
203
|
+
className={cn("inline-flex min-w-0 overflow-hidden", className)}
|
|
204
|
+
data-transcription-status={status}
|
|
205
|
+
>
|
|
206
|
+
<span className="inline-flex min-w-0 items-center gap-1.5">
|
|
207
|
+
<AnimatePresence mode="popLayout" initial={false}>
|
|
208
|
+
{recoverable ? (
|
|
209
|
+
<motion.span
|
|
210
|
+
key="recovered"
|
|
211
|
+
initial={{ opacity: 0, scale: 0.96 }}
|
|
212
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
213
|
+
exit={{ opacity: 0, scale: 0.96 }}
|
|
214
|
+
transition={{ duration: 0.16, ease: [0.22, 1, 0.36, 1] }}
|
|
215
|
+
className={cn(
|
|
216
|
+
"inline-flex h-8 min-w-0 items-center gap-1 rounded-og-md border border-og-border/80",
|
|
217
|
+
"bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11",
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
<span className="max-w-44 truncate text-og-xs text-og-fg-muted max-sm:max-w-28">
|
|
221
|
+
{savedTranscript
|
|
222
|
+
? (errorMessage ?? messages.recoveredTranscript)
|
|
223
|
+
: status === "error"
|
|
224
|
+
? (errorMessage ?? messages.errorRetryable)
|
|
225
|
+
: messages.recovered}
|
|
226
|
+
</span>
|
|
227
|
+
<Tip tip={savedTranscript ? messages.insertRecoveredTranscript : messages.retry}>
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
onClick={() =>
|
|
231
|
+
savedTranscript
|
|
232
|
+
? void transcription.insertSavedTranscript()
|
|
233
|
+
: transcription.retry()
|
|
234
|
+
}
|
|
235
|
+
aria-label={
|
|
236
|
+
savedTranscript ? messages.insertRecoveredTranscript : messages.retry
|
|
237
|
+
}
|
|
238
|
+
className={cn(
|
|
239
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
240
|
+
"bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
|
|
241
|
+
"hover:bg-og-fg-muted pointer-coarse:size-11",
|
|
242
|
+
)}
|
|
243
|
+
>
|
|
244
|
+
{savedTranscript ? (
|
|
245
|
+
<ClipboardPasteIcon className="size-3.5" />
|
|
246
|
+
) : (
|
|
247
|
+
<RefreshCwIcon className="size-3.5" />
|
|
248
|
+
)}
|
|
249
|
+
</button>
|
|
250
|
+
</Tip>
|
|
251
|
+
<Tip tip={messages.discardRecovered}>
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
onClick={() => void transcription.discard()}
|
|
255
|
+
aria-label={messages.discardRecovered}
|
|
256
|
+
className={cn(
|
|
257
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
258
|
+
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
259
|
+
"hover:bg-og-surface-3 hover:text-og-status-failed pointer-coarse:size-11",
|
|
260
|
+
)}
|
|
261
|
+
>
|
|
262
|
+
<Trash2Icon className="size-3.5" />
|
|
263
|
+
</button>
|
|
264
|
+
</Tip>
|
|
265
|
+
</motion.span>
|
|
266
|
+
) : active ? (
|
|
267
|
+
<motion.span
|
|
268
|
+
key={status === "transcribing" || status === "saving" ? "processing" : "capture"}
|
|
269
|
+
initial={{ opacity: 0, scale: 0.96 }}
|
|
270
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
271
|
+
exit={{ opacity: 0, scale: 0.96 }}
|
|
272
|
+
transition={{ duration: 0.16, ease: [0.22, 1, 0.36, 1] }}
|
|
273
|
+
className={cn(
|
|
274
|
+
"inline-flex h-8 items-center gap-1 rounded-og-md border border-og-border/80",
|
|
275
|
+
"bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11",
|
|
276
|
+
)}
|
|
277
|
+
>
|
|
278
|
+
{status === "requesting-permission" ? (
|
|
279
|
+
<LoaderCircleIcon
|
|
280
|
+
className="size-3.5 shrink-0 text-og-fg-muted animate-og-spin motion-reduce:animate-none"
|
|
281
|
+
aria-hidden
|
|
282
|
+
/>
|
|
283
|
+
) : (
|
|
284
|
+
<span className="flex items-center gap-1.5">
|
|
285
|
+
{status === "recording" ? (
|
|
286
|
+
<span
|
|
287
|
+
aria-hidden
|
|
288
|
+
className="size-1.5 shrink-0 rounded-full bg-og-status-failed animate-og-pulse motion-reduce:animate-none"
|
|
289
|
+
/>
|
|
290
|
+
) : null}
|
|
291
|
+
<VoiceWaveform
|
|
292
|
+
stream={status === "recording" ? transcription.stream : null}
|
|
293
|
+
mode={status === "recording" ? "recording" : "transcribing"}
|
|
294
|
+
/>
|
|
295
|
+
</span>
|
|
296
|
+
)}
|
|
297
|
+
{status === "recording" ? (
|
|
298
|
+
<>
|
|
299
|
+
<Tip tip={messages.cancel}>
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
onClick={() => transcription.cancel()}
|
|
303
|
+
aria-label={messages.cancel}
|
|
304
|
+
aria-keyshortcuts="Escape"
|
|
305
|
+
className={cn(
|
|
306
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
307
|
+
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
308
|
+
"hover:bg-og-surface-3 hover:text-og-fg pointer-coarse:size-11",
|
|
309
|
+
)}
|
|
310
|
+
>
|
|
311
|
+
<XIcon className="size-3.5" />
|
|
312
|
+
</button>
|
|
313
|
+
</Tip>
|
|
314
|
+
<Tip tip={messages.stop}>
|
|
315
|
+
<button
|
|
316
|
+
type="button"
|
|
317
|
+
onClick={() => transcription.stop()}
|
|
318
|
+
aria-label={messages.stop}
|
|
319
|
+
className={cn(
|
|
320
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
321
|
+
"bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
|
|
322
|
+
"hover:bg-og-fg-muted pointer-coarse:size-11",
|
|
323
|
+
)}
|
|
324
|
+
>
|
|
325
|
+
<SquareIcon className="size-2.5 fill-current" />
|
|
326
|
+
</button>
|
|
327
|
+
</Tip>
|
|
328
|
+
</>
|
|
329
|
+
) : status === "transcribing" || status === "saving" ? (
|
|
330
|
+
<span className="og-shimmer-text px-1.5 text-og-xs font-medium whitespace-nowrap">
|
|
331
|
+
{status === "saving" ? messages.saving : messages.transcribing}
|
|
332
|
+
</span>
|
|
333
|
+
) : (
|
|
334
|
+
<span className="px-1.5 text-og-xs text-og-fg-muted whitespace-nowrap">
|
|
335
|
+
{messages.requestingPermission}
|
|
336
|
+
</span>
|
|
337
|
+
)}
|
|
338
|
+
</motion.span>
|
|
339
|
+
) : (
|
|
340
|
+
<Tip key="idle" tip={idleLabel}>
|
|
341
|
+
<motion.button
|
|
176
342
|
type="button"
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
343
|
+
data-og-composer-dictate
|
|
344
|
+
initial={{ opacity: 0, scale: 0.96 }}
|
|
345
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
346
|
+
exit={{ opacity: 0, scale: 0.96 }}
|
|
347
|
+
transition={{ duration: 0.14, ease: [0.22, 1, 0.36, 1] }}
|
|
348
|
+
onClick={start}
|
|
349
|
+
aria-label={idleLabel}
|
|
350
|
+
aria-pressed={false}
|
|
351
|
+
aria-disabled={unavailableMessage !== null}
|
|
180
352
|
className={cn(
|
|
181
|
-
"inline-flex size-
|
|
353
|
+
"inline-flex size-8 shrink-0 items-center justify-center rounded-og-md pointer-coarse:size-11",
|
|
182
354
|
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
183
|
-
|
|
355
|
+
unavailableMessage
|
|
356
|
+
? "cursor-not-allowed opacity-45"
|
|
357
|
+
: "hover:bg-og-surface-2 hover:text-og-fg",
|
|
184
358
|
)}
|
|
185
359
|
>
|
|
186
|
-
<
|
|
187
|
-
</button>
|
|
360
|
+
<MicIcon className="size-4" />
|
|
361
|
+
</motion.button>
|
|
188
362
|
</Tip>
|
|
189
|
-
<Tip tip={messages.stop}>
|
|
190
|
-
<button
|
|
191
|
-
type="button"
|
|
192
|
-
onClick={() => transcription.stop()}
|
|
193
|
-
aria-label={messages.stop}
|
|
194
|
-
className={cn(
|
|
195
|
-
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
196
|
-
"bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
|
|
197
|
-
"hover:bg-og-fg-muted pointer-coarse:size-9",
|
|
198
|
-
)}
|
|
199
|
-
>
|
|
200
|
-
<SquareIcon className="size-2.5 fill-current" />
|
|
201
|
-
</button>
|
|
202
|
-
</Tip>
|
|
203
|
-
</>
|
|
204
|
-
) : status === "transcribing" ? (
|
|
205
|
-
<span className="og-shimmer-text px-1.5 text-og-xs font-medium whitespace-nowrap">
|
|
206
|
-
{messages.transcribing}
|
|
207
|
-
</span>
|
|
208
|
-
) : (
|
|
209
|
-
<span className="px-1.5 text-og-xs text-og-fg-muted whitespace-nowrap">
|
|
210
|
-
{messages.requestingPermission}
|
|
211
|
-
</span>
|
|
212
|
-
)}
|
|
213
|
-
</motion.span>
|
|
214
|
-
) : (
|
|
215
|
-
<Tip key="idle" tip={idleLabel}>
|
|
216
|
-
<motion.button
|
|
217
|
-
type="button"
|
|
218
|
-
initial={{ opacity: 0, scale: 0.96 }}
|
|
219
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
220
|
-
exit={{ opacity: 0, scale: 0.96 }}
|
|
221
|
-
transition={{ duration: 0.14, ease: [0.22, 1, 0.36, 1] }}
|
|
222
|
-
onClick={start}
|
|
223
|
-
aria-label={idleLabel}
|
|
224
|
-
aria-pressed={false}
|
|
225
|
-
aria-disabled={unavailableMessage !== null}
|
|
226
|
-
className={cn(
|
|
227
|
-
"inline-flex size-8 shrink-0 items-center justify-center rounded-og-md pointer-coarse:size-9",
|
|
228
|
-
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
229
|
-
unavailableMessage
|
|
230
|
-
? "cursor-not-allowed opacity-45"
|
|
231
|
-
: "hover:bg-og-surface-2 hover:text-og-fg",
|
|
232
363
|
)}
|
|
364
|
+
</AnimatePresence>
|
|
365
|
+
{status === "error" && errorMessage && !recoverable ? (
|
|
366
|
+
<Tip tip={errorMessage}>
|
|
367
|
+
<span
|
|
368
|
+
aria-hidden="true"
|
|
369
|
+
className="max-w-40 truncate text-og-xs text-og-status-failed max-sm:max-w-24"
|
|
370
|
+
>
|
|
371
|
+
{errorMessage}
|
|
372
|
+
</span>
|
|
373
|
+
</Tip>
|
|
374
|
+
) : null}
|
|
375
|
+
<span
|
|
376
|
+
className="sr-only"
|
|
377
|
+
role={status === "error" ? "alert" : "status"}
|
|
378
|
+
aria-live="polite"
|
|
233
379
|
>
|
|
234
|
-
|
|
235
|
-
</
|
|
236
|
-
</Tip>
|
|
237
|
-
)}
|
|
238
|
-
</AnimatePresence>
|
|
239
|
-
{status === "error" && errorMessage ? (
|
|
240
|
-
<Tip tip={errorMessage}>
|
|
241
|
-
<span
|
|
242
|
-
aria-hidden="true"
|
|
243
|
-
className="max-w-40 truncate text-og-xs text-og-status-failed max-sm:max-w-24"
|
|
244
|
-
>
|
|
245
|
-
{errorMessage}
|
|
380
|
+
{announcement}
|
|
381
|
+
</span>
|
|
246
382
|
</span>
|
|
247
|
-
</
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
{announcement}
|
|
251
|
-
</span>
|
|
252
|
-
</span>
|
|
383
|
+
</motion.span>
|
|
384
|
+
)}
|
|
385
|
+
</AnimatePresence>
|
|
253
386
|
);
|
|
254
387
|
}
|
|
255
388
|
|
|
@@ -364,6 +497,14 @@ function transcriptionErrorMessage(code: string, messages: ComposerTranscription
|
|
|
364
497
|
return messages.errorTooLarge;
|
|
365
498
|
case "invalid_audio":
|
|
366
499
|
return messages.errorInvalidAudio;
|
|
500
|
+
case "storage_unavailable":
|
|
501
|
+
return messages.errorStorageUnavailable;
|
|
502
|
+
case "network":
|
|
503
|
+
case "provider":
|
|
504
|
+
case "timeout":
|
|
505
|
+
return messages.errorRetryable;
|
|
506
|
+
case "handoff_uncertain":
|
|
507
|
+
return messages.errorHandoffUncertain;
|
|
367
508
|
case "unknown":
|
|
368
509
|
return messages.errorUnknown;
|
|
369
510
|
default:
|