@opengeni/react 0.15.0 → 0.23.0
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 +110 -10
- package/dist/{chunk-TOJR776I.js → chunk-6XUS5VFM.js} +45 -369
- package/dist/chunk-6XUS5VFM.js.map +1 -0
- package/dist/chunk-CATATY77.js +2393 -0
- package/dist/chunk-CATATY77.js.map +1 -0
- package/dist/chunk-I3BJZIG5.js +439 -0
- package/dist/chunk-I3BJZIG5.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/chunk-UAHPKLB6.js +2237 -0
- package/dist/chunk-UAHPKLB6.js.map +1 -0
- package/dist/chunk-UQS7OQDE.js +591 -0
- package/dist/chunk-UQS7OQDE.js.map +1 -0
- package/dist/chunk-YDNWMKXO.js +102 -0
- package/dist/chunk-YDNWMKXO.js.map +1 -0
- package/dist/composer-CVm5uoUI.d.ts +585 -0
- package/dist/composer.d.ts +6 -0
- package/dist/composer.js +56 -0
- package/dist/composer.js.map +1 -0
- package/dist/index.d.ts +1288 -1785
- package/dist/index.js +7140 -8817
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +3 -1
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
- package/dist/queue-surface-implementation-3UW3MIIR.js +890 -0
- package/dist/queue-surface-implementation-3UW3MIIR.js.map +1 -0
- package/dist/session-Db00TRwl.d.ts +446 -0
- package/dist/session-context-rgrfElMl.d.ts +58 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +54 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-By1wn3MM.d.ts +99 -0
- package/package.json +20 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +44 -0
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +94 -993
- package/src/components/code-editor.tsx +104 -26
- package/src/components/command-palette.tsx +8 -2
- package/src/components/composer-transcription-control.tsx +211 -0
- package/src/components/composer.tsx +1536 -0
- package/src/components/desktop-viewer.tsx +3 -3
- package/src/components/enrollment-consent.tsx +2 -2
- package/src/components/file-browser.tsx +367 -29
- package/src/components/human-input-form.tsx +417 -0
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machines-dashboard.tsx +3 -3
- package/src/components/message-timeline.tsx +205 -25
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-draft-policy.ts +18 -0
- package/src/components/queue-surface-implementation.tsx +1037 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +42 -559
- package/src/components/sandbox-files.tsx +138 -14
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +339 -119
- package/src/components/workbench-changes.tsx +84 -33
- package/src/components/workspace-dock.tsx +188 -18
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +385 -104
- package/src/hooks/use-human-input.ts +131 -0
- package/src/hooks/use-machine-chip.ts +2 -2
- package/src/hooks/use-machines.ts +27 -11
- package/src/hooks/use-sandbox-files.ts +310 -62
- package/src/hooks/use-sandbox-git.ts +154 -40
- package/src/hooks/use-sandbox-terminal.ts +28 -6
- package/src/hooks/use-session-capabilities.ts +38 -9
- package/src/hooks/use-session-control.ts +37 -8
- package/src/hooks/use-session-events.ts +522 -48
- package/src/hooks/use-session-mcp-approval-policy.ts +161 -0
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +134 -66
- package/src/hooks/use-windowed-sections.ts +2 -2
- package/src/hooks/use-workspace-capture.ts +146 -40
- package/src/hooks/use-workspace-edit.ts +50 -14
- package/src/hooks/use-workspace-sessions.ts +3 -0
- package/src/human-input.ts +72 -0
- package/src/index.ts +85 -6
- package/src/lib/noto-sans-arabic-loader.ts +21 -0
- package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
- package/src/lib/use-portal-token-style.ts +51 -0
- package/src/lib/use-unicode-fonts.ts +57 -0
- package/src/provider.tsx +4 -69
- package/src/session-context.ts +160 -0
- package/src/session.ts +89 -0
- package/src/timeline/parsers.ts +35 -0
- package/src/timeline/projection.ts +12 -4
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +10 -9
- package/styles/index.css +56 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-TOJR776I.js.map +0 -1
- package/dist/machines-BpdwuQcD.d.ts +0 -449
|
@@ -3,8 +3,6 @@ import {
|
|
|
3
3
|
type ComponentType,
|
|
4
4
|
type CSSProperties,
|
|
5
5
|
type ReactNode,
|
|
6
|
-
lazy,
|
|
7
|
-
Suspense,
|
|
8
6
|
useCallback,
|
|
9
7
|
useEffect,
|
|
10
8
|
useMemo,
|
|
@@ -92,13 +90,6 @@ export function languageForPath(path: string): keyof typeof LANGUAGE_LOADERS | n
|
|
|
92
90
|
}
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
const LazyCodeMirror = lazy(async () => {
|
|
96
|
-
const mod = (await import("@uiw/react-codemirror")) as unknown as {
|
|
97
|
-
default: ReactCodeMirrorComponent;
|
|
98
|
-
};
|
|
99
|
-
return { default: mod.default };
|
|
100
|
-
});
|
|
101
|
-
|
|
102
93
|
export type CodeEditorProps = {
|
|
103
94
|
/** Workspace-relative path — drives language inference (and the save target upstream). */
|
|
104
95
|
path: string;
|
|
@@ -106,6 +97,10 @@ export type CodeEditorProps = {
|
|
|
106
97
|
initialContents: string;
|
|
107
98
|
/** Persist the current buffer. Resolves when the write lands; rejects to surface an error. */
|
|
108
99
|
onSave: (contents: string) => Promise<unknown>;
|
|
100
|
+
/** Explicitly overwrite after a visible expected-content conflict. */
|
|
101
|
+
onOverwrite?: ((contents: string) => Promise<unknown>) | undefined;
|
|
102
|
+
/** Re-read the selected file after a visible save conflict. */
|
|
103
|
+
onReload?: (() => void) | undefined;
|
|
109
104
|
/** Fired once when the buffer FIRST diverges from its baseline (the first real
|
|
110
105
|
* keystroke) — the wake-on-edit intent. The host warms the box on this so the
|
|
111
106
|
* eventual save lands fast; merely opening the file for reading never fires it. */
|
|
@@ -136,6 +131,8 @@ export function CodeEditor({
|
|
|
136
131
|
path,
|
|
137
132
|
initialContents,
|
|
138
133
|
onSave,
|
|
134
|
+
onOverwrite,
|
|
135
|
+
onReload,
|
|
139
136
|
onEditIntent,
|
|
140
137
|
readOnly = false,
|
|
141
138
|
themeType = "dark",
|
|
@@ -156,6 +153,8 @@ export function CodeEditor({
|
|
|
156
153
|
const [savedTick, setSavedTick] = useState(false);
|
|
157
154
|
|
|
158
155
|
const dirty = value !== baseline;
|
|
156
|
+
const saveConflict =
|
|
157
|
+
saveError !== null && (saveError as Error & { code?: unknown }).code === "file_write_conflict";
|
|
159
158
|
|
|
160
159
|
// Reload the buffer when the file identity changes. Guard on initialContents
|
|
161
160
|
// too so an external refresh of the SAME path (e.g. fs.changed re-read) reseeds
|
|
@@ -201,7 +200,16 @@ export function CodeEditor({
|
|
|
201
200
|
keymap: { of: (binds: unknown[]) => unknown };
|
|
202
201
|
Prec: { highest: (ext: unknown) => unknown };
|
|
203
202
|
};
|
|
204
|
-
|
|
203
|
+
// Language peers are optional. A missing grammar must degrade to a plain
|
|
204
|
+
// CodeMirror document, not throw the entire editor down to textarea.
|
|
205
|
+
let languageExtension: unknown | null = null;
|
|
206
|
+
if (langKey) {
|
|
207
|
+
try {
|
|
208
|
+
languageExtension = (await LANGUAGE_LOADERS[langKey]?.()) ?? null;
|
|
209
|
+
} catch {
|
|
210
|
+
languageExtension = null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
205
213
|
if (cancelled) return;
|
|
206
214
|
// A high-precedence Cmd/Ctrl-S keymap that calls back into the latest
|
|
207
215
|
// save handler (kept fresh via a ref) and swallows the browser's
|
|
@@ -222,8 +230,9 @@ export function CodeEditor({
|
|
|
222
230
|
setBundle({
|
|
223
231
|
Editor: cmMod.default,
|
|
224
232
|
saveKeymapExtension,
|
|
225
|
-
languageExtension
|
|
233
|
+
languageExtension,
|
|
226
234
|
});
|
|
235
|
+
setFailed(false);
|
|
227
236
|
} catch {
|
|
228
237
|
if (!cancelled) setFailed(true);
|
|
229
238
|
}
|
|
@@ -256,6 +265,25 @@ export function CodeEditor({
|
|
|
256
265
|
}
|
|
257
266
|
}, [readOnly, value, baseline, onSave, path]);
|
|
258
267
|
|
|
268
|
+
const overwrite = useCallback(async () => {
|
|
269
|
+
if (readOnly || !onOverwrite) return;
|
|
270
|
+
const snapshot = value;
|
|
271
|
+
if (snapshot === baseline) return;
|
|
272
|
+
setSaving(true);
|
|
273
|
+
setSaveError(null);
|
|
274
|
+
setSavedTick(false);
|
|
275
|
+
try {
|
|
276
|
+
await onOverwrite(snapshot);
|
|
277
|
+
setBaseline(snapshot);
|
|
278
|
+
lastSeed.current = { path, contents: snapshot };
|
|
279
|
+
setSavedTick(true);
|
|
280
|
+
} catch (cause) {
|
|
281
|
+
setSaveError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
282
|
+
} finally {
|
|
283
|
+
setSaving(false);
|
|
284
|
+
}
|
|
285
|
+
}, [readOnly, onOverwrite, value, baseline, path]);
|
|
286
|
+
|
|
259
287
|
saveRef.current = () => {
|
|
260
288
|
void save();
|
|
261
289
|
};
|
|
@@ -276,13 +304,7 @@ export function CodeEditor({
|
|
|
276
304
|
return exts;
|
|
277
305
|
}, [bundle]);
|
|
278
306
|
|
|
279
|
-
|
|
280
|
-
return (
|
|
281
|
-
<div className={cn("flex h-full min-h-0 flex-col", className)}>
|
|
282
|
-
{fallback ?? <PlainTextarea value={value} readOnly={readOnly} onChange={noteEdit} />}
|
|
283
|
-
</div>
|
|
284
|
-
);
|
|
285
|
-
}
|
|
307
|
+
const Editor = bundle?.Editor;
|
|
286
308
|
|
|
287
309
|
return (
|
|
288
310
|
<div
|
|
@@ -291,7 +313,7 @@ export function CodeEditor({
|
|
|
291
313
|
style={editorVars}
|
|
292
314
|
>
|
|
293
315
|
{/* Save bar: dirty indicator + status + the explicit Save button. */}
|
|
294
|
-
<div className="flex shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1">
|
|
316
|
+
<div className="flex min-h-9 shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1">
|
|
295
317
|
<span
|
|
296
318
|
className={cn(
|
|
297
319
|
"size-1.5 shrink-0 rounded-full transition-colors",
|
|
@@ -304,7 +326,7 @@ export function CodeEditor({
|
|
|
304
326
|
{dirty && !readOnly ? " •" : ""}
|
|
305
327
|
</span>
|
|
306
328
|
<div className="ml-auto flex shrink-0 items-center gap-2">
|
|
307
|
-
{saveError && (
|
|
329
|
+
{saveError && !saveConflict && (
|
|
308
330
|
<span
|
|
309
331
|
className="max-w-[220px] truncate text-og-xs text-og-status-failed"
|
|
310
332
|
title={saveError.message}
|
|
@@ -315,13 +337,13 @@ export function CodeEditor({
|
|
|
315
337
|
{!saveError && savedTick && <span className="text-og-xs text-og-status-idle">Saved</span>}
|
|
316
338
|
{readOnly ? (
|
|
317
339
|
<span className="text-og-xs uppercase tracking-wide text-og-fg-subtle">Read-only</span>
|
|
318
|
-
) : (
|
|
340
|
+
) : !saveConflict ? (
|
|
319
341
|
<button
|
|
320
342
|
type="button"
|
|
321
343
|
onClick={() => void save()}
|
|
322
344
|
disabled={saving || !dirty}
|
|
323
345
|
className={cn(
|
|
324
|
-
"flex items-center gap-1 rounded-og-sm border border-og-border px-1.5 py-0.5 text-og-xs pointer-coarse:min-h-
|
|
346
|
+
"flex items-center gap-1 rounded-og-sm border border-og-border px-1.5 py-0.5 text-og-xs pointer-coarse:min-h-11",
|
|
325
347
|
saving || !dirty
|
|
326
348
|
? "cursor-default text-og-fg-subtle opacity-60"
|
|
327
349
|
: "text-og-fg hover:bg-og-accent-soft",
|
|
@@ -335,14 +357,59 @@ export function CodeEditor({
|
|
|
335
357
|
)}
|
|
336
358
|
Save
|
|
337
359
|
</button>
|
|
338
|
-
)}
|
|
360
|
+
) : null}
|
|
339
361
|
</div>
|
|
340
362
|
</div>
|
|
341
363
|
|
|
364
|
+
{saveConflict ? (
|
|
365
|
+
<div
|
|
366
|
+
className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-2 border-b border-og-status-failed/25 bg-og-status-failed/8 px-2 py-2"
|
|
367
|
+
role="alert"
|
|
368
|
+
>
|
|
369
|
+
<div className="min-w-[12rem] flex-1 text-og-xs text-og-fg-muted">
|
|
370
|
+
<span className="font-medium text-og-status-failed">File changed on machine.</span>{" "}
|
|
371
|
+
Reloading discards your edits; overwriting replaces the live version.
|
|
372
|
+
</div>
|
|
373
|
+
<div className="ml-auto flex shrink-0 items-center gap-1.5">
|
|
374
|
+
{onReload ? (
|
|
375
|
+
<button
|
|
376
|
+
type="button"
|
|
377
|
+
onClick={() => {
|
|
378
|
+
setSaveError(null);
|
|
379
|
+
onReload();
|
|
380
|
+
}}
|
|
381
|
+
className="min-h-8 rounded-og-sm border border-og-border bg-og-surface-1 px-2 text-og-xs font-medium text-og-fg hover:bg-og-surface-2 pointer-coarse:min-h-11"
|
|
382
|
+
>
|
|
383
|
+
Reload live
|
|
384
|
+
</button>
|
|
385
|
+
) : null}
|
|
386
|
+
{onOverwrite ? (
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
onClick={() => void overwrite()}
|
|
390
|
+
disabled={saving}
|
|
391
|
+
className="min-h-8 rounded-og-sm border border-og-status-failed/40 px-2 text-og-xs font-medium text-og-status-failed hover:bg-og-status-failed/10 disabled:opacity-50 pointer-coarse:min-h-11"
|
|
392
|
+
>
|
|
393
|
+
Overwrite
|
|
394
|
+
</button>
|
|
395
|
+
) : null}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
) : null}
|
|
399
|
+
|
|
342
400
|
{/* The editor surface. */}
|
|
343
401
|
<div className="min-h-0 flex-1 overflow-auto">
|
|
344
|
-
|
|
345
|
-
|
|
402
|
+
{failed ? (
|
|
403
|
+
(fallback ?? (
|
|
404
|
+
<PlainTextarea
|
|
405
|
+
value={value}
|
|
406
|
+
readOnly={readOnly}
|
|
407
|
+
onChange={noteEdit}
|
|
408
|
+
onSave={() => void save()}
|
|
409
|
+
/>
|
|
410
|
+
))
|
|
411
|
+
) : Editor ? (
|
|
412
|
+
<Editor
|
|
346
413
|
value={value}
|
|
347
414
|
theme={themeType === "light" ? "light" : "dark"}
|
|
348
415
|
editable={!readOnly}
|
|
@@ -353,7 +420,9 @@ export function CodeEditor({
|
|
|
353
420
|
className="og-cm-editor min-h-full text-og-sm"
|
|
354
421
|
onChange={readOnly ? undefined : noteEdit}
|
|
355
422
|
/>
|
|
356
|
-
|
|
423
|
+
) : (
|
|
424
|
+
(loading ?? <EditorSkeleton />)
|
|
425
|
+
)}
|
|
357
426
|
</div>
|
|
358
427
|
</div>
|
|
359
428
|
);
|
|
@@ -369,10 +438,12 @@ function PlainTextarea({
|
|
|
369
438
|
value,
|
|
370
439
|
readOnly,
|
|
371
440
|
onChange,
|
|
441
|
+
onSave,
|
|
372
442
|
}: {
|
|
373
443
|
value: string;
|
|
374
444
|
readOnly: boolean;
|
|
375
445
|
onChange: (value: string) => void;
|
|
446
|
+
onSave: () => void;
|
|
376
447
|
}) {
|
|
377
448
|
return (
|
|
378
449
|
<textarea
|
|
@@ -380,6 +451,13 @@ function PlainTextarea({
|
|
|
380
451
|
readOnly={readOnly}
|
|
381
452
|
spellCheck={false}
|
|
382
453
|
onChange={(e) => onChange(e.target.value)}
|
|
454
|
+
onKeyDown={(event) => {
|
|
455
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
|
|
456
|
+
event.preventDefault();
|
|
457
|
+
onSave();
|
|
458
|
+
}
|
|
459
|
+
}}
|
|
460
|
+
aria-label="Plain text file editor"
|
|
383
461
|
className="h-full w-full resize-none bg-transparent p-2 font-og-mono text-og-sm text-og-fg outline-none"
|
|
384
462
|
/>
|
|
385
463
|
);
|
|
@@ -15,6 +15,10 @@ export type CommandPaletteProps = {
|
|
|
15
15
|
argHintText: string;
|
|
16
16
|
/** id used for aria-activedescendant wiring from the textarea. */
|
|
17
17
|
listboxId: string;
|
|
18
|
+
/** Accessible list label. Defaults to "Slash commands". */
|
|
19
|
+
label?: string | undefined;
|
|
20
|
+
/** Badge shown for destructive commands. Defaults to "danger". */
|
|
21
|
+
dangerLabel?: string | undefined;
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -31,6 +35,8 @@ export function CommandPalette({
|
|
|
31
35
|
onRun,
|
|
32
36
|
argHintText,
|
|
33
37
|
listboxId,
|
|
38
|
+
label = "Slash commands",
|
|
39
|
+
dangerLabel = "danger",
|
|
34
40
|
}: CommandPaletteProps) {
|
|
35
41
|
return (
|
|
36
42
|
<AnimatePresence>
|
|
@@ -48,7 +54,7 @@ export function CommandPalette({
|
|
|
48
54
|
<ul
|
|
49
55
|
id={listboxId}
|
|
50
56
|
role="listbox"
|
|
51
|
-
aria-label=
|
|
57
|
+
aria-label={label}
|
|
52
58
|
className="max-h-72 overflow-y-auto py-1"
|
|
53
59
|
>
|
|
54
60
|
{items.map((command, index) => {
|
|
@@ -92,7 +98,7 @@ export function CommandPalette({
|
|
|
92
98
|
<span className="ml-auto flex items-center gap-1.5">
|
|
93
99
|
{command.danger ? (
|
|
94
100
|
<span className="rounded-og-xs bg-og-status-failed/15 px-1 text-[10px] uppercase tracking-wide text-og-status-failed">
|
|
95
|
-
|
|
101
|
+
{dangerLabel}
|
|
96
102
|
</span>
|
|
97
103
|
) : null}
|
|
98
104
|
<span className="truncate text-[12px] text-og-fg-subtle max-sm:hidden">
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TranscriptionAdapter,
|
|
3
|
+
TranscriptionDiagnostic,
|
|
4
|
+
TranscriptionErrorCode,
|
|
5
|
+
TranscriptionTargetSelection,
|
|
6
|
+
WorkspaceTranscriptionPolicy,
|
|
7
|
+
} from "@opengeni/sdk";
|
|
8
|
+
import { LoaderCircleIcon, MicIcon, RotateCwIcon, SquareIcon } from "lucide-react";
|
|
9
|
+
import { type MouseEvent } from "react";
|
|
10
|
+
import { cn } from "../lib/cn";
|
|
11
|
+
import { useTranscription, type TranscriptionLifecycleTimeouts } from "../hooks/use-transcription";
|
|
12
|
+
import { useChatComposer } from "./composer";
|
|
13
|
+
|
|
14
|
+
export type ComposerTranscriptionMessages = {
|
|
15
|
+
start: string;
|
|
16
|
+
stop: string;
|
|
17
|
+
retry: string;
|
|
18
|
+
requestingPermission: string;
|
|
19
|
+
listening: string;
|
|
20
|
+
reconnecting: string;
|
|
21
|
+
cancelling: string;
|
|
22
|
+
unavailableDisabled: string;
|
|
23
|
+
unavailableAdapter: string;
|
|
24
|
+
unavailablePolicy: string;
|
|
25
|
+
errorPermissionDenied: string;
|
|
26
|
+
errorNotSupported: string;
|
|
27
|
+
errorNetwork: string;
|
|
28
|
+
errorProvider: string;
|
|
29
|
+
errorPolicyBlocked: string;
|
|
30
|
+
errorTimeout: string;
|
|
31
|
+
errorCancelled: string;
|
|
32
|
+
errorUnknown: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const defaultMessages: ComposerTranscriptionMessages = {
|
|
36
|
+
start: "Start voice input",
|
|
37
|
+
stop: "Stop voice input",
|
|
38
|
+
retry: "Retry voice input",
|
|
39
|
+
requestingPermission: "Requesting microphone permission…",
|
|
40
|
+
listening: "Listening… Press Escape to cancel.",
|
|
41
|
+
reconnecting: "Reconnecting voice input…",
|
|
42
|
+
cancelling: "Cancelling voice input…",
|
|
43
|
+
unavailableDisabled: "Voice input is unavailable while the composer is disabled.",
|
|
44
|
+
unavailableAdapter: "Voice input needs an approved transcription adapter.",
|
|
45
|
+
unavailablePolicy: "Enable and accept transcription in Workspace settings.",
|
|
46
|
+
errorPermissionDenied: "Microphone permission was denied. Your draft was not changed.",
|
|
47
|
+
errorNotSupported: "Voice input is not supported on this device.",
|
|
48
|
+
errorNetwork: "Voice input lost its network connection.",
|
|
49
|
+
errorProvider: "The transcription service could not continue.",
|
|
50
|
+
errorPolicyBlocked: "Workspace policy does not authorize voice input.",
|
|
51
|
+
errorTimeout: "Voice input took too long to start. Try again.",
|
|
52
|
+
errorCancelled: "Voice input was cancelled.",
|
|
53
|
+
errorUnknown: "Voice input could not start. Try again.",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type ComposerTranscriptionControlProps = {
|
|
57
|
+
adapter: TranscriptionAdapter | null;
|
|
58
|
+
policy: WorkspaceTranscriptionPolicy;
|
|
59
|
+
selection?: TranscriptionTargetSelection | undefined;
|
|
60
|
+
messages?: Partial<ComposerTranscriptionMessages> | undefined;
|
|
61
|
+
className?: string | undefined;
|
|
62
|
+
lifecycleTimeouts?: Partial<TranscriptionLifecycleTimeouts> | undefined;
|
|
63
|
+
onDiagnostic?: ((diagnostic: TranscriptionDiagnostic) => void) | undefined;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** One provider-neutral microphone control for the nearest editable composer. */
|
|
67
|
+
export function ComposerTranscriptionControl({
|
|
68
|
+
adapter,
|
|
69
|
+
policy,
|
|
70
|
+
selection,
|
|
71
|
+
messages: overrides,
|
|
72
|
+
className,
|
|
73
|
+
lifecycleTimeouts,
|
|
74
|
+
onDiagnostic,
|
|
75
|
+
}: ComposerTranscriptionControlProps) {
|
|
76
|
+
const composer = useChatComposer();
|
|
77
|
+
const messages = { ...defaultMessages, ...overrides };
|
|
78
|
+
const transcription = useTranscription({
|
|
79
|
+
adapter,
|
|
80
|
+
policy,
|
|
81
|
+
...(selection ? { selection } : {}),
|
|
82
|
+
value: composer.value,
|
|
83
|
+
setValue: composer.setValue,
|
|
84
|
+
focusInput: composer.focusInput,
|
|
85
|
+
disabled: composer.disabled,
|
|
86
|
+
lifecycleTimeouts,
|
|
87
|
+
onDiagnostic,
|
|
88
|
+
});
|
|
89
|
+
const status = transcription.state.status;
|
|
90
|
+
const active =
|
|
91
|
+
status === "requesting-permission" ||
|
|
92
|
+
status === "listening" ||
|
|
93
|
+
status === "reconnecting" ||
|
|
94
|
+
status === "cancelling";
|
|
95
|
+
const unavailableMessage =
|
|
96
|
+
transcription.unavailableReason === "composer_disabled"
|
|
97
|
+
? messages.unavailableDisabled
|
|
98
|
+
: transcription.unavailableReason === "adapter_missing"
|
|
99
|
+
? messages.unavailableAdapter
|
|
100
|
+
: transcription.unavailableReason
|
|
101
|
+
? messages.unavailablePolicy
|
|
102
|
+
: null;
|
|
103
|
+
const label = unavailableMessage
|
|
104
|
+
? unavailableMessage
|
|
105
|
+
: status === "error"
|
|
106
|
+
? messages.retry
|
|
107
|
+
: active
|
|
108
|
+
? messages.stop
|
|
109
|
+
: messages.start;
|
|
110
|
+
const errorMessage = transcription.state.error
|
|
111
|
+
? transcriptionErrorMessage(transcription.state.error.code, messages)
|
|
112
|
+
: null;
|
|
113
|
+
const announcement =
|
|
114
|
+
transcription.state.partial ||
|
|
115
|
+
(status === "requesting-permission"
|
|
116
|
+
? messages.requestingPermission
|
|
117
|
+
: status === "listening"
|
|
118
|
+
? messages.listening
|
|
119
|
+
: status === "reconnecting"
|
|
120
|
+
? (errorMessage ?? messages.reconnecting)
|
|
121
|
+
: status === "cancelling"
|
|
122
|
+
? messages.cancelling
|
|
123
|
+
: status === "error"
|
|
124
|
+
? (errorMessage ?? messages.errorUnknown)
|
|
125
|
+
: unavailableMessage);
|
|
126
|
+
|
|
127
|
+
function activate(event: MouseEvent<HTMLButtonElement>) {
|
|
128
|
+
if (unavailableMessage || status === "cancelling") {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (active) void transcription.cancel();
|
|
133
|
+
else void transcription.start();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<span
|
|
138
|
+
className={cn("inline-flex min-w-0 items-center gap-1.5", className)}
|
|
139
|
+
data-transcription-status={status}
|
|
140
|
+
>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={activate}
|
|
144
|
+
aria-label={label}
|
|
145
|
+
aria-pressed={active}
|
|
146
|
+
aria-disabled={unavailableMessage !== null || status === "cancelling"}
|
|
147
|
+
aria-keyshortcuts={active ? "Escape" : undefined}
|
|
148
|
+
title={label}
|
|
149
|
+
className={cn(
|
|
150
|
+
"inline-flex size-8 shrink-0 items-center justify-center rounded-og-md pointer-coarse:size-11",
|
|
151
|
+
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
152
|
+
unavailableMessage
|
|
153
|
+
? "cursor-not-allowed opacity-45"
|
|
154
|
+
: status === "cancelling"
|
|
155
|
+
? "cursor-not-allowed"
|
|
156
|
+
: "hover:bg-og-surface-2 hover:text-og-fg",
|
|
157
|
+
status === "listening" && "bg-og-status-failed/10 text-og-status-failed",
|
|
158
|
+
)}
|
|
159
|
+
>
|
|
160
|
+
{status === "requesting-permission" || status === "cancelling" ? (
|
|
161
|
+
<LoaderCircleIcon className="size-4 animate-og-spin motion-reduce:animate-none" />
|
|
162
|
+
) : status === "reconnecting" ? (
|
|
163
|
+
<RotateCwIcon className="size-4 animate-og-spin motion-reduce:animate-none" />
|
|
164
|
+
) : active ? (
|
|
165
|
+
<SquareIcon className="size-3.5 fill-current" />
|
|
166
|
+
) : (
|
|
167
|
+
<MicIcon className="size-4" />
|
|
168
|
+
)}
|
|
169
|
+
</button>
|
|
170
|
+
{announcement && (active || status === "error") ? (
|
|
171
|
+
<span
|
|
172
|
+
aria-hidden="true"
|
|
173
|
+
title={announcement}
|
|
174
|
+
className={cn(
|
|
175
|
+
"max-w-48 truncate text-og-xs text-og-fg-muted max-sm:max-w-28",
|
|
176
|
+
status === "error" && "text-og-status-failed",
|
|
177
|
+
)}
|
|
178
|
+
>
|
|
179
|
+
{announcement}
|
|
180
|
+
</span>
|
|
181
|
+
) : null}
|
|
182
|
+
<span className="sr-only" role={status === "error" ? "alert" : "status"} aria-live="polite">
|
|
183
|
+
{announcement}
|
|
184
|
+
</span>
|
|
185
|
+
</span>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function transcriptionErrorMessage(
|
|
190
|
+
code: TranscriptionErrorCode,
|
|
191
|
+
messages: ComposerTranscriptionMessages,
|
|
192
|
+
): string {
|
|
193
|
+
switch (code) {
|
|
194
|
+
case "permission_denied":
|
|
195
|
+
return messages.errorPermissionDenied;
|
|
196
|
+
case "not_supported":
|
|
197
|
+
return messages.errorNotSupported;
|
|
198
|
+
case "network":
|
|
199
|
+
return messages.errorNetwork;
|
|
200
|
+
case "provider":
|
|
201
|
+
return messages.errorProvider;
|
|
202
|
+
case "policy_blocked":
|
|
203
|
+
return messages.errorPolicyBlocked;
|
|
204
|
+
case "timeout":
|
|
205
|
+
return messages.errorTimeout;
|
|
206
|
+
case "cancelled":
|
|
207
|
+
return messages.errorCancelled;
|
|
208
|
+
case "unknown":
|
|
209
|
+
return messages.errorUnknown;
|
|
210
|
+
}
|
|
211
|
+
}
|