@opengeni/react 0.42.1 → 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-UWYTCQWW.js → chunk-KC27K42G.js} +92 -21
- package/dist/{chunk-UWYTCQWW.js.map → chunk-KC27K42G.js.map} +1 -1
- 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 +3 -1
- package/dist/components/composer.d.ts +6 -4
- package/dist/components/session-chrome.d.ts +1 -1
- package/dist/composer.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +69 -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/package.json +2 -2
- package/src/components/chat-composer.tsx +58 -19
- package/src/components/composer-transcription-control.tsx +194 -165
- 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/index.ts +2 -0
- 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/styles/tokens.css +8 -5
- package/dist/chunk-4IJCL7YO.js.map +0 -1
- package/dist/chunk-KR2SK5GJ.js.map +0 -1
- package/dist/chunk-SJKT4TKW.js.map +0 -1
- /package/dist/{chunk-JALF5FI3.js.map → chunk-SRFUT2ZU.js.map} +0 -0
package/dist/model-policy.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export type PickerBillingClass = "opengeni_credits" | "codex_subscription" | "by
|
|
|
3
3
|
export type PickerModelRow<TCatalog extends ClientModel = WorkspaceModelCatalogModel> = {
|
|
4
4
|
id: string;
|
|
5
5
|
label: string;
|
|
6
|
+
/** Catalog-curated compact label for dense UI; fall back to `label` when absent. */
|
|
7
|
+
shortLabel?: string | undefined;
|
|
6
8
|
billingClass: PickerBillingClass;
|
|
7
9
|
billingClassLabel: string;
|
|
8
10
|
selectable: boolean;
|
package/dist/model-policy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type EffectiveSessionControl, type SessionEvent, type SessionStatus } from "@opengeni/sdk";
|
|
2
2
|
import { type SessionRealtimeClientLike, type SessionRealtimeController, type SessionRealtimeControllerSnapshot, type CreateSessionRealtimeControllerOptions, type SessionRealtimeModel } from "@opengeni/sdk/realtime";
|
|
3
|
-
import { type RefObject } from "react";
|
|
3
|
+
import { type ReactNode, type RefObject } from "react";
|
|
4
4
|
import type { EmbeddedRealtimeSessionClientLike } from "../client";
|
|
5
5
|
export type RealtimeModelOption = {
|
|
6
6
|
id: SessionRealtimeModel;
|
|
@@ -65,6 +65,8 @@ export declare function SessionRealtimeControl(props: {
|
|
|
65
65
|
codexConnected: boolean;
|
|
66
66
|
realtimeAutostartModel?: SessionRealtimeModel | undefined;
|
|
67
67
|
onRealtimeAutostartConsumed?: (() => void) | undefined;
|
|
68
|
+
/** Host hides dictate (and can choreograph layout) while realtime voice is live. */
|
|
69
|
+
onVoiceActiveChange?: ((active: boolean) => void) | undefined;
|
|
68
70
|
/** Deterministic browser-test/demo seam. Production hosts should use the SDK default. */
|
|
69
71
|
controllerFactory?: SessionRealtimeControllerFactory | undefined;
|
|
70
72
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -85,6 +87,16 @@ export declare function NewSessionRealtimeControl(props: {
|
|
|
85
87
|
disabled?: boolean | undefined;
|
|
86
88
|
disabledReason?: string | null | undefined;
|
|
87
89
|
onStart: (model: SessionRealtimeModel) => Promise<boolean>;
|
|
90
|
+
/** Parent-owned selection (e.g. shared with the mobile “+” voice-model panel). */
|
|
91
|
+
models?: readonly RealtimeModelOption[] | undefined;
|
|
92
|
+
selectedModel?: RealtimeModelOption | undefined;
|
|
93
|
+
onSelectModel?: ((modelId: string) => void) | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* `split` attaches the model chevron at every breakpoint (public default).
|
|
96
|
+
* `split-desktop` hides it below `sm` when the host owns mobile selection (e.g. “+”).
|
|
97
|
+
* `none` never attaches a model menu to the bar button.
|
|
98
|
+
*/
|
|
99
|
+
modelMenu?: "split" | "split-desktop" | "none" | undefined;
|
|
88
100
|
}): import("react/jsx-runtime").JSX.Element;
|
|
89
101
|
export declare function RealtimeVoiceControl(props: {
|
|
90
102
|
snapshot: SessionRealtimeControllerSnapshot;
|
|
@@ -94,7 +106,15 @@ export declare function RealtimeVoiceControl(props: {
|
|
|
94
106
|
selectionDisabled?: boolean | undefined;
|
|
95
107
|
/** Prefer `bottom` on home/new-session; `top` for the docked session composer. */
|
|
96
108
|
menuSide?: "top" | "bottom" | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* `split` = start + model chevron at every breakpoint (public SDK default).
|
|
111
|
+
* `split-desktop` = chevron from `sm` up; below that the host owns selection.
|
|
112
|
+
* `none` = start button only.
|
|
113
|
+
*/
|
|
114
|
+
modelMenu?: "split" | "split-desktop" | "none" | undefined;
|
|
97
115
|
showDiagnostics?: boolean;
|
|
116
|
+
/** Extra classes on the in-bar mute cluster (e.g. `max-sm:hidden` when mutes live in “+”). */
|
|
117
|
+
muteControlsClassName?: string | undefined;
|
|
98
118
|
audioRef: RefObject<HTMLAudioElement | null>;
|
|
99
119
|
selectedModel?: RealtimeModelOption | undefined;
|
|
100
120
|
models?: readonly RealtimeModelOption[] | undefined;
|
|
@@ -106,6 +126,14 @@ export declare function RealtimeVoiceControl(props: {
|
|
|
106
126
|
onSetInputMuted: (muted: boolean) => void;
|
|
107
127
|
onSetOutputMuted: (muted: boolean) => void;
|
|
108
128
|
}): import("react/jsx-runtime").JSX.Element;
|
|
129
|
+
/** Drill-in body for the mobile composer “+” menu — same catalog as the desktop chevron. */
|
|
130
|
+
export declare function RealtimeVoiceModelPanel(props: {
|
|
131
|
+
models: readonly RealtimeModelOption[];
|
|
132
|
+
selectedModel: RealtimeModelOption;
|
|
133
|
+
disabled?: boolean | undefined;
|
|
134
|
+
leading?: ReactNode | undefined;
|
|
135
|
+
onSelect: (modelId: SessionRealtimeModel) => void;
|
|
136
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
109
137
|
export declare function RealtimeModelPickerMenu(props: {
|
|
110
138
|
models: readonly RealtimeModelOption[];
|
|
111
139
|
selectedModel: RealtimeModelOption;
|
package/dist/realtime.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* base `@opengeni/react` entry remains SSR/browser safe and does not eagerly
|
|
6
6
|
* load microphone, WebRTC, Gateway, or realtime UI code.
|
|
7
7
|
*/
|
|
8
|
-
export { CodexRealtimeControl, NewSessionRealtimeControl, RealtimeModelPickerMenu, RealtimeVoiceControl, SessionCodexRealtimeControl, SessionRealtimeControl, codexRealtimeAdmissionAllowed, codexRealtimeAdmissionBlocker, useRealtimeModelSelection, useSessionCodexRealtime, useSessionRealtime, useWorkspaceRealtimeModelSelection, } from "./realtime/realtime-control";
|
|
8
|
+
export { CodexRealtimeControl, NewSessionRealtimeControl, RealtimeModelPickerMenu, RealtimeVoiceControl, RealtimeVoiceModelPanel, SessionCodexRealtimeControl, SessionRealtimeControl, codexRealtimeAdmissionAllowed, codexRealtimeAdmissionBlocker, useRealtimeModelSelection, useSessionCodexRealtime, useSessionRealtime, useWorkspaceRealtimeModelSelection, } from "./realtime/realtime-control";
|
|
9
9
|
export type { RealtimeModelOption, SessionRealtimeControllerFactory, } from "./realtime/realtime-control";
|
|
10
10
|
export type { EmbeddedRealtimeSessionClientLike } from "./client";
|
|
11
11
|
export type { EmbeddedRealtimeSessionClientOverride } from "./session-context";
|
package/dist/realtime.js
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
PickerAnimatedPage,
|
|
4
4
|
PickerBackHeader,
|
|
5
5
|
PickerNavRow
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-U6K24XQD.js";
|
|
7
7
|
import {
|
|
8
8
|
useEmbeddedRealtimeSession
|
|
9
9
|
} from "./chunk-EKZH6IDG.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-23EJ676W.js";
|
|
11
11
|
import {
|
|
12
12
|
cn
|
|
13
13
|
} from "./chunk-TK7G6XLT.js";
|
|
@@ -31,7 +31,13 @@ import {
|
|
|
31
31
|
VolumeXIcon
|
|
32
32
|
} from "lucide-react";
|
|
33
33
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
useCallback,
|
|
36
|
+
useEffect,
|
|
37
|
+
useMemo,
|
|
38
|
+
useRef,
|
|
39
|
+
useState
|
|
40
|
+
} from "react";
|
|
35
41
|
|
|
36
42
|
// src/realtime/dropdown-menu.tsx
|
|
37
43
|
import { ChevronRightIcon } from "lucide-react";
|
|
@@ -324,6 +330,11 @@ function SessionRealtimeControl(props) {
|
|
|
324
330
|
const { models, selectedModel: selectedRealtimeModel, selectModel } = selection;
|
|
325
331
|
const { canStart, start } = realtime;
|
|
326
332
|
const onRealtimeAutostartConsumed = props.onRealtimeAutostartConsumed;
|
|
333
|
+
const onVoiceActiveChange = props.onVoiceActiveChange;
|
|
334
|
+
const voiceActive = realtime.snapshot.mode?.state === "active";
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
onVoiceActiveChange?.(voiceActive);
|
|
337
|
+
}, [onVoiceActiveChange, voiceActive]);
|
|
327
338
|
useEffect(() => {
|
|
328
339
|
const pending = autostartModelRef.current;
|
|
329
340
|
if (!pending || autostartStartedRef.current || lifecycle?.state === "active") return;
|
|
@@ -440,11 +451,16 @@ var IDLE_REALTIME_SNAPSHOT = {
|
|
|
440
451
|
error: null
|
|
441
452
|
};
|
|
442
453
|
function NewSessionRealtimeControl(props) {
|
|
443
|
-
const
|
|
454
|
+
const internalSelection = useRealtimeModelSelection({
|
|
444
455
|
client: props.client,
|
|
445
456
|
workspaceId: props.workspaceId,
|
|
446
457
|
codexConnected: props.codexConnected
|
|
447
458
|
});
|
|
459
|
+
const selection = props.models && props.selectedModel && props.onSelectModel ? {
|
|
460
|
+
models: props.models,
|
|
461
|
+
selectedModel: props.selectedModel,
|
|
462
|
+
selectModel: props.onSelectModel
|
|
463
|
+
} : internalSelection;
|
|
448
464
|
const audioRef = useRef(null);
|
|
449
465
|
const [starting, setStarting] = useState(false);
|
|
450
466
|
const [error, setError] = useState(null);
|
|
@@ -477,6 +493,7 @@ function NewSessionRealtimeControl(props) {
|
|
|
477
493
|
modelAvailable: selection.selectedModel.available,
|
|
478
494
|
selectionDisabled: starting,
|
|
479
495
|
menuSide: "bottom",
|
|
496
|
+
modelMenu: props.modelMenu ?? "split",
|
|
480
497
|
showDiagnostics: false,
|
|
481
498
|
audioRef,
|
|
482
499
|
selectedModel: selection.selectedModel,
|
|
@@ -513,6 +530,9 @@ function RealtimeVoiceControl(props) {
|
|
|
513
530
|
const mainLabel = audioBlocked ? "Resume voice audio" : retryConnection ? "Retry voice connection" : modeOwned ? "End voice conversation" : `Start voice with ${selectedModel.label}`;
|
|
514
531
|
const runMainAction = audioBlocked ? props.onRetryAudibleOutput : retryConnection ? props.onRetry : modeOwned ? props.onStop : props.onStart;
|
|
515
532
|
const diagnosticsVisible = props.showDiagnostics ?? import.meta.env.DEV;
|
|
533
|
+
const modelMenu = props.modelMenu ?? "split";
|
|
534
|
+
const showAttachedModelMenu = modelMenu === "split" || modelMenu === "split-desktop";
|
|
535
|
+
const desktopOnlyModelMenu = modelMenu === "split-desktop";
|
|
516
536
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
517
537
|
const [pickerProvider, setPickerProvider] = useState(
|
|
518
538
|
selectedModel.provider
|
|
@@ -524,169 +544,217 @@ function RealtimeVoiceControl(props) {
|
|
|
524
544
|
return /* @__PURE__ */ jsxs2(
|
|
525
545
|
"div",
|
|
526
546
|
{
|
|
547
|
+
role: "group",
|
|
527
548
|
"aria-label": "Realtime voice",
|
|
528
549
|
"data-picker-side": props.menuSide ?? "top",
|
|
529
550
|
className: "inline-flex shrink-0 items-center",
|
|
530
551
|
children: [
|
|
531
552
|
/* @__PURE__ */ jsx2("audio", { ref: props.audioRef, autoPlay: true, className: "hidden", "aria-hidden": "true" }),
|
|
532
553
|
/* @__PURE__ */ jsx2(RealtimeComposerGlow, { phase: status.phase, reduceMotion: reduceMotion === true }),
|
|
533
|
-
/* @__PURE__ */ jsx2(AnimatePresence, { initial: false, children: modeOwned ? /* @__PURE__ */
|
|
554
|
+
/* @__PURE__ */ jsx2(AnimatePresence, { initial: false, children: modeOwned ? /* @__PURE__ */ jsx2(
|
|
534
555
|
motion.div,
|
|
535
556
|
{
|
|
536
557
|
"data-testid": "realtime-mute-controls",
|
|
537
|
-
initial: reduceMotion ? false : { opacity: 0, width: 0
|
|
538
|
-
animate: {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
),
|
|
552
|
-
/* @__PURE__ */ jsx2(
|
|
553
|
-
RealtimeMuteButton,
|
|
554
|
-
{
|
|
555
|
-
muted: props.snapshot.outputMuted,
|
|
556
|
-
kind: "output",
|
|
557
|
-
reduceMotion: reduceMotion === true,
|
|
558
|
-
onToggle: () => props.onSetOutputMuted(!props.snapshot.outputMuted)
|
|
559
|
-
}
|
|
560
|
-
)
|
|
561
|
-
]
|
|
562
|
-
},
|
|
563
|
-
"realtime-mute-controls"
|
|
564
|
-
) : null }),
|
|
565
|
-
/* @__PURE__ */ jsx2(
|
|
566
|
-
motion.button,
|
|
567
|
-
{
|
|
568
|
-
type: "button",
|
|
569
|
-
"data-testid": "realtime-primary-action",
|
|
570
|
-
"data-phase": status.phase,
|
|
571
|
-
"aria-label": mainLabel,
|
|
572
|
-
"aria-pressed": modeOwned,
|
|
573
|
-
title: mainLabel,
|
|
574
|
-
disabled: mainDisabled,
|
|
575
|
-
...reduceMotion ? {} : { whileTap: { scale: 0.92 } },
|
|
576
|
-
transition: { type: "spring", stiffness: 520, damping: 30 },
|
|
577
|
-
onClick: () => void runMainAction().catch(() => void 0),
|
|
558
|
+
initial: reduceMotion ? false : { opacity: 0, width: 0 },
|
|
559
|
+
animate: {
|
|
560
|
+
opacity: 1,
|
|
561
|
+
width: "auto",
|
|
562
|
+
transition: reduceMotion ? { duration: 0 } : { delay: 0.3, duration: 0.48, ease: [0.22, 1, 0.36, 1] }
|
|
563
|
+
},
|
|
564
|
+
...reduceMotion ? {} : {
|
|
565
|
+
exit: {
|
|
566
|
+
opacity: 0,
|
|
567
|
+
width: 0,
|
|
568
|
+
transition: { duration: 0.28, ease: [0.4, 0, 1, 1] }
|
|
569
|
+
}
|
|
570
|
+
},
|
|
578
571
|
className: cn(
|
|
579
|
-
"
|
|
580
|
-
|
|
581
|
-
"focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-og-accent/45",
|
|
582
|
-
"disabled:cursor-not-allowed disabled:opacity-45 pointer-coarse:size-11",
|
|
583
|
-
voiceButtonTone(status.phase)
|
|
572
|
+
"inline-flex shrink-0 items-center overflow-hidden",
|
|
573
|
+
props.muteControlsClassName
|
|
584
574
|
),
|
|
585
|
-
children: /* @__PURE__ */
|
|
586
|
-
|
|
575
|
+
children: /* @__PURE__ */ jsxs2(
|
|
576
|
+
motion.div,
|
|
587
577
|
{
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
578
|
+
className: "inline-flex items-center gap-0.5 pr-1",
|
|
579
|
+
initial: reduceMotion ? false : { x: 18, opacity: 0 },
|
|
580
|
+
animate: {
|
|
581
|
+
x: 0,
|
|
582
|
+
opacity: 1,
|
|
583
|
+
transition: reduceMotion ? { duration: 0 } : { delay: 0.32, type: "spring", stiffness: 320, damping: 30, mass: 0.8 }
|
|
584
|
+
},
|
|
585
|
+
...reduceMotion ? {} : {
|
|
586
|
+
exit: {
|
|
587
|
+
x: 14,
|
|
588
|
+
opacity: 0,
|
|
589
|
+
transition: { duration: 0.22, ease: [0.4, 0, 1, 1] }
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
children: [
|
|
593
|
+
/* @__PURE__ */ jsx2(
|
|
594
|
+
RealtimeMuteButton,
|
|
595
|
+
{
|
|
596
|
+
muted: props.snapshot.inputMuted,
|
|
597
|
+
kind: "microphone",
|
|
598
|
+
reduceMotion: reduceMotion === true,
|
|
599
|
+
enterDelay: reduceMotion ? 0 : 0.34,
|
|
600
|
+
onToggle: () => props.onSetInputMuted(!props.snapshot.inputMuted)
|
|
601
|
+
}
|
|
602
|
+
),
|
|
603
|
+
/* @__PURE__ */ jsx2(
|
|
604
|
+
RealtimeMuteButton,
|
|
605
|
+
{
|
|
606
|
+
muted: props.snapshot.outputMuted,
|
|
607
|
+
kind: "output",
|
|
608
|
+
reduceMotion: reduceMotion === true,
|
|
609
|
+
enterDelay: reduceMotion ? 0 : 0.4,
|
|
610
|
+
onToggle: () => props.onSetOutputMuted(!props.snapshot.outputMuted)
|
|
611
|
+
}
|
|
612
|
+
)
|
|
613
|
+
]
|
|
591
614
|
}
|
|
592
615
|
)
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
616
|
+
},
|
|
617
|
+
"realtime-mute-controls"
|
|
618
|
+
) : null }),
|
|
619
|
+
/* @__PURE__ */ jsxs2("div", { className: "inline-flex shrink-0 items-center", children: [
|
|
620
|
+
/* @__PURE__ */ jsx2(
|
|
621
|
+
motion.button,
|
|
598
622
|
{
|
|
599
623
|
type: "button",
|
|
600
|
-
"
|
|
601
|
-
|
|
602
|
-
|
|
624
|
+
"data-testid": "realtime-primary-action",
|
|
625
|
+
"data-phase": status.phase,
|
|
626
|
+
"aria-label": mainLabel,
|
|
627
|
+
"aria-pressed": modeOwned,
|
|
628
|
+
title: mainLabel,
|
|
629
|
+
disabled: mainDisabled,
|
|
630
|
+
...reduceMotion ? {} : { whileTap: { scale: 0.92 } },
|
|
631
|
+
transition: { type: "spring", stiffness: 520, damping: 30 },
|
|
632
|
+
onClick: () => void runMainAction().catch(() => void 0),
|
|
603
633
|
className: cn(
|
|
604
|
-
|
|
605
|
-
|
|
634
|
+
// Match transcription mic: ghost icon when idle; filled only when live.
|
|
635
|
+
// Public contract: coarse pointers keep a 44px target; split menu uses
|
|
636
|
+
// left-only rounding at `sm+` where the chevron attaches.
|
|
637
|
+
"relative inline-flex size-8 items-center justify-center outline-none",
|
|
638
|
+
"rounded-og-md transition-[background-color,border-color,color,box-shadow] duration-200 ease-og-out",
|
|
606
639
|
"focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-og-accent/45",
|
|
607
|
-
"
|
|
640
|
+
"disabled:cursor-not-allowed disabled:opacity-45 pointer-coarse:size-11",
|
|
641
|
+
showAttachedModelMenu && (desktopOnlyModelMenu ? "sm:rounded-l-og-md sm:rounded-r-none" : "rounded-l-og-md rounded-r-none"),
|
|
608
642
|
voiceButtonTone(status.phase)
|
|
609
643
|
),
|
|
610
|
-
children: /* @__PURE__ */ jsx2(
|
|
644
|
+
children: /* @__PURE__ */ jsx2(
|
|
645
|
+
RealtimeActionGlyph,
|
|
646
|
+
{
|
|
647
|
+
phase: status.phase,
|
|
648
|
+
audioBlocked,
|
|
649
|
+
reduceMotion: reduceMotion === true
|
|
650
|
+
}
|
|
651
|
+
)
|
|
611
652
|
}
|
|
612
|
-
)
|
|
613
|
-
/* @__PURE__ */ jsxs2(
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
653
|
+
),
|
|
654
|
+
showAttachedModelMenu ? /* @__PURE__ */ jsxs2(DropdownMenu, { open: pickerOpen, onOpenChange: setPickerOpen, children: [
|
|
655
|
+
/* @__PURE__ */ jsx2(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx2(
|
|
656
|
+
"button",
|
|
657
|
+
{
|
|
658
|
+
type: "button",
|
|
659
|
+
"aria-label": "Choose voice model and options",
|
|
660
|
+
title: `Voice model: ${selectedModel.label}`,
|
|
661
|
+
disabled: props.selectionDisabled,
|
|
662
|
+
className: cn(
|
|
663
|
+
// ≥24px wide so WCAG 2.2 target-size passes when the chevron is shown.
|
|
664
|
+
"inline-flex h-8 w-6 items-center justify-center rounded-r-og-md outline-none",
|
|
665
|
+
"transition-[background-color,border-color,color] duration-200 ease-og-out",
|
|
666
|
+
"focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-og-accent/45",
|
|
667
|
+
"pointer-coarse:h-11 pointer-coarse:w-7",
|
|
668
|
+
desktopOnlyModelMenu && "hidden sm:inline-flex",
|
|
669
|
+
voiceChevronTone(status.phase)
|
|
670
|
+
),
|
|
671
|
+
children: /* @__PURE__ */ jsx2(ChevronDownIcon, { className: "size-3", "aria-hidden": true })
|
|
672
|
+
}
|
|
673
|
+
) }),
|
|
674
|
+
/* @__PURE__ */ jsxs2(
|
|
675
|
+
DropdownMenuContent,
|
|
676
|
+
{
|
|
677
|
+
side: props.menuSide ?? "top",
|
|
678
|
+
align: "end",
|
|
679
|
+
sideOffset: 8,
|
|
680
|
+
collisionPadding: 12,
|
|
681
|
+
className: "flex w-72 max-h-[min(20rem,var(--radix-dropdown-menu-content-available-height))] flex-col overflow-hidden rounded-xl border-border bg-surface p-1.5 shadow-xl",
|
|
682
|
+
onCloseAutoFocus: (event) => event.preventDefault(),
|
|
683
|
+
children: [
|
|
684
|
+
/* @__PURE__ */ jsx2("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain", children: /* @__PURE__ */ jsx2(
|
|
685
|
+
RealtimeModelPickerMenu,
|
|
686
|
+
{
|
|
687
|
+
models,
|
|
688
|
+
selectedModel,
|
|
689
|
+
provider: pickerProvider,
|
|
690
|
+
direction: pickerDirection,
|
|
691
|
+
disabled: props.selectionDisabled === true || props.snapshot.mode?.state === "active",
|
|
692
|
+
onProviderChange: (provider, direction) => {
|
|
693
|
+
setPickerDirection(direction);
|
|
694
|
+
setPickerProvider(provider);
|
|
695
|
+
},
|
|
696
|
+
onSelect: (modelId) => {
|
|
697
|
+
props.onSelectModel?.(modelId);
|
|
698
|
+
setPickerOpen(false);
|
|
699
|
+
}
|
|
638
700
|
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
701
|
+
) }),
|
|
702
|
+
status.phase !== "idle" ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
703
|
+
/* @__PURE__ */ jsx2(DropdownMenuSeparator, { className: "mx-1 bg-og-border" }),
|
|
704
|
+
/* @__PURE__ */ jsxs2("div", { className: "px-2.5 py-2", role: "status", "aria-live": "polite", children: [
|
|
705
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 text-og-sm font-medium text-og-fg", children: [
|
|
706
|
+
/* @__PURE__ */ jsx2(
|
|
707
|
+
RealtimeStatusDot,
|
|
708
|
+
{
|
|
709
|
+
phase: status.phase,
|
|
710
|
+
reduceMotion: reduceMotion === true
|
|
711
|
+
}
|
|
712
|
+
),
|
|
713
|
+
status.label
|
|
714
|
+
] }),
|
|
715
|
+
/* @__PURE__ */ jsx2("p", { className: "mt-1 text-og-xs leading-5 text-og-fg-subtle", children: status.detail })
|
|
716
|
+
] })
|
|
717
|
+
] }) : null,
|
|
718
|
+
audioBlocked ? /* @__PURE__ */ jsxs2(
|
|
719
|
+
DropdownMenuItem,
|
|
720
|
+
{
|
|
721
|
+
className: "rounded-og-md",
|
|
722
|
+
onSelect: () => void props.onRetryAudibleOutput().catch(() => void 0),
|
|
723
|
+
children: [
|
|
724
|
+
/* @__PURE__ */ jsx2(Volume2Icon, {}),
|
|
725
|
+
"Resume audio"
|
|
726
|
+
]
|
|
727
|
+
}
|
|
728
|
+
) : null,
|
|
729
|
+
retryConnection ? /* @__PURE__ */ jsxs2(
|
|
730
|
+
DropdownMenuItem,
|
|
731
|
+
{
|
|
732
|
+
className: "rounded-og-md",
|
|
733
|
+
onSelect: () => void props.onRetry().catch(() => void 0),
|
|
734
|
+
children: [
|
|
735
|
+
/* @__PURE__ */ jsx2(RotateCcwIcon, {}),
|
|
736
|
+
"Retry connection"
|
|
737
|
+
]
|
|
738
|
+
}
|
|
739
|
+
) : null,
|
|
740
|
+
modeOwned && props.snapshot.status !== "lost_owner" ? /* @__PURE__ */ jsxs2(
|
|
741
|
+
DropdownMenuItem,
|
|
742
|
+
{
|
|
743
|
+
variant: "destructive",
|
|
744
|
+
className: "rounded-og-md",
|
|
745
|
+
disabled: props.snapshot.status === "stopping",
|
|
746
|
+
onSelect: () => void props.onStop().catch(() => void 0),
|
|
747
|
+
children: [
|
|
748
|
+
/* @__PURE__ */ jsx2(SquareIcon, {}),
|
|
749
|
+
"End voice conversation"
|
|
750
|
+
]
|
|
751
|
+
}
|
|
752
|
+
) : null,
|
|
753
|
+
diagnosticsVisible ? /* @__PURE__ */ jsx2(RealtimeDiagnosticsMenu, { snapshot: props.snapshot }) : null
|
|
754
|
+
]
|
|
755
|
+
}
|
|
756
|
+
)
|
|
757
|
+
] }) : null
|
|
690
758
|
] }),
|
|
691
759
|
/* @__PURE__ */ jsxs2("span", { className: "sr-only", role: "status", "aria-live": "polite", children: [
|
|
692
760
|
status.label,
|
|
@@ -698,6 +766,34 @@ function RealtimeVoiceControl(props) {
|
|
|
698
766
|
}
|
|
699
767
|
);
|
|
700
768
|
}
|
|
769
|
+
function RealtimeVoiceModelPanel(props) {
|
|
770
|
+
const [provider, setProvider] = useState(null);
|
|
771
|
+
const [direction, setDirection] = useState(1);
|
|
772
|
+
return /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1 flex-col", "data-testid": "realtime-voice-model-panel", children: [
|
|
773
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex shrink-0 items-start gap-1 px-2 pt-1 pb-1.5", children: [
|
|
774
|
+
props.leading,
|
|
775
|
+
/* @__PURE__ */ jsxs2("div", { className: "min-w-0", children: [
|
|
776
|
+
/* @__PURE__ */ jsx2("div", { className: "text-sm font-medium text-og-fg", children: "Voice model" }),
|
|
777
|
+
/* @__PURE__ */ jsx2("p", { className: "mt-0.5 text-xs text-og-fg-subtle", children: "Used when you start a voice conversation." })
|
|
778
|
+
] })
|
|
779
|
+
] }),
|
|
780
|
+
/* @__PURE__ */ jsx2("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-1.5 pb-1.5", children: /* @__PURE__ */ jsx2(
|
|
781
|
+
RealtimeModelPickerMenu,
|
|
782
|
+
{
|
|
783
|
+
models: props.models,
|
|
784
|
+
selectedModel: props.selectedModel,
|
|
785
|
+
provider,
|
|
786
|
+
direction,
|
|
787
|
+
disabled: props.disabled === true,
|
|
788
|
+
onProviderChange: (next, nextDirection) => {
|
|
789
|
+
setDirection(nextDirection);
|
|
790
|
+
setProvider(next);
|
|
791
|
+
},
|
|
792
|
+
onSelect: props.onSelect
|
|
793
|
+
}
|
|
794
|
+
) })
|
|
795
|
+
] });
|
|
796
|
+
}
|
|
701
797
|
function RealtimeMuteButton(props) {
|
|
702
798
|
const microphone = props.kind === "microphone";
|
|
703
799
|
const label = microphone ? props.muted ? "Unmute microphone" : "Mute microphone" : props.muted ? "Unmute voice audio" : "Mute voice audio";
|
|
@@ -709,13 +805,22 @@ function RealtimeMuteButton(props) {
|
|
|
709
805
|
"aria-label": label,
|
|
710
806
|
"aria-pressed": props.muted,
|
|
711
807
|
title: label,
|
|
808
|
+
initial: props.reduceMotion ? false : { scale: 0.72, opacity: 0 },
|
|
809
|
+
animate: { scale: 1, opacity: 1 },
|
|
810
|
+
transition: {
|
|
811
|
+
delay: props.enterDelay ?? 0,
|
|
812
|
+
type: "spring",
|
|
813
|
+
stiffness: 420,
|
|
814
|
+
damping: 26,
|
|
815
|
+
mass: 0.7
|
|
816
|
+
},
|
|
712
817
|
...props.reduceMotion ? {} : { whileTap: { scale: 0.92 } },
|
|
713
818
|
onClick: props.onToggle,
|
|
714
819
|
className: cn(
|
|
715
|
-
"inline-flex size-8 shrink-0 items-center justify-center rounded-og-md
|
|
820
|
+
"inline-flex size-8 shrink-0 items-center justify-center rounded-og-md outline-none",
|
|
716
821
|
"transition-[background-color,border-color,color] duration-150 ease-og-out",
|
|
717
822
|
"focus-visible:ring-2 focus-visible:ring-og-accent/45 pointer-coarse:size-11",
|
|
718
|
-
props.muted ? "border-og-accent/35 bg-og-accent-soft text-og-accent" : "
|
|
823
|
+
props.muted ? "border border-og-accent/35 bg-og-accent-soft text-og-accent" : "text-og-fg-muted hover:bg-og-surface-2 hover:text-og-fg"
|
|
719
824
|
),
|
|
720
825
|
children: /* @__PURE__ */ jsx2(Icon, { className: "size-3.5", "aria-hidden": true })
|
|
721
826
|
}
|
|
@@ -970,15 +1075,27 @@ function RealtimeStatusDot(props) {
|
|
|
970
1075
|
}
|
|
971
1076
|
function voiceButtonTone(phase) {
|
|
972
1077
|
if (phase === "listening" || phase === "speaking") {
|
|
973
|
-
return "border-og-accent/45 bg-og-accent text-og-accent-fg shadow-og-sm hover:bg-og-accent-strong";
|
|
1078
|
+
return "border border-og-accent/45 bg-og-accent text-og-accent-fg shadow-og-sm hover:bg-og-accent-strong";
|
|
1079
|
+
}
|
|
1080
|
+
if (phase === "blocked" || phase === "error") {
|
|
1081
|
+
return "border border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting hover:bg-og-status-waiting/15";
|
|
1082
|
+
}
|
|
1083
|
+
if (phase === "connecting" || phase === "reconnecting" || phase === "stopping") {
|
|
1084
|
+
return "border border-og-accent/35 bg-og-accent-soft text-og-accent";
|
|
1085
|
+
}
|
|
1086
|
+
return "text-og-fg-muted hover:bg-og-surface-2 hover:text-og-fg";
|
|
1087
|
+
}
|
|
1088
|
+
function voiceChevronTone(phase) {
|
|
1089
|
+
if (phase === "listening" || phase === "speaking") {
|
|
1090
|
+
return "border border-l-0 border-og-accent/45 bg-og-accent text-og-accent-fg hover:bg-og-accent-strong";
|
|
974
1091
|
}
|
|
975
1092
|
if (phase === "blocked" || phase === "error") {
|
|
976
|
-
return "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting hover:bg-og-status-waiting/15";
|
|
1093
|
+
return "border border-l-0 border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting hover:bg-og-status-waiting/15";
|
|
977
1094
|
}
|
|
978
1095
|
if (phase === "connecting" || phase === "reconnecting" || phase === "stopping") {
|
|
979
|
-
return "border-og-accent/35 bg-og-accent-soft text-og-accent";
|
|
1096
|
+
return "border border-l-0 border-og-accent/35 bg-og-accent-soft text-og-accent";
|
|
980
1097
|
}
|
|
981
|
-
return "
|
|
1098
|
+
return "text-og-fg-muted hover:bg-og-surface-2 hover:text-og-fg";
|
|
982
1099
|
}
|
|
983
1100
|
function toRealtimeModelOption(model) {
|
|
984
1101
|
return { ...model };
|
|
@@ -1011,6 +1128,7 @@ export {
|
|
|
1011
1128
|
NewSessionRealtimeControl,
|
|
1012
1129
|
RealtimeModelPickerMenu,
|
|
1013
1130
|
RealtimeVoiceControl,
|
|
1131
|
+
RealtimeVoiceModelPanel,
|
|
1014
1132
|
SessionCodexRealtimeControl,
|
|
1015
1133
|
SessionRealtimeControl,
|
|
1016
1134
|
codexRealtimeAdmissionAllowed,
|