@agent-native/toolkit 0.10.0 → 0.10.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/composer/PromptComposer.d.ts +1 -0
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +4 -1
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/PromptComposer.spec.js +8 -1
- package/dist/composer/PromptComposer.spec.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/RealtimeVoiceMode.js +56 -11
- package/dist/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.spec.js +59 -7
- package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +32 -0
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.spec.js +25 -2
- package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
- package/dist/conformance/conformance.spec.js +30 -1
- package/dist/conformance/conformance.spec.js.map +1 -1
- package/dist/conformance/runner.d.ts.map +1 -1
- package/dist/conformance/runner.js +75 -1
- package/dist/conformance/runner.js.map +1 -1
- package/dist/editor/SharedRichEditor.d.ts.map +1 -1
- package/dist/editor/SharedRichEditor.js +3 -2
- package/dist/editor/SharedRichEditor.js.map +1 -1
- package/dist/editor/SlashCommandMenu.d.ts +8 -0
- package/dist/editor/SlashCommandMenu.d.ts.map +1 -1
- package/dist/editor/SlashCommandMenu.js +6 -0
- package/dist/editor/SlashCommandMenu.js.map +1 -1
- package/dist/editor/SlashCommandMenu.spec.d.ts +2 -0
- package/dist/editor/SlashCommandMenu.spec.d.ts.map +1 -0
- package/dist/editor/SlashCommandMenu.spec.js +20 -0
- package/dist/editor/SlashCommandMenu.spec.js.map +1 -0
- package/dist/editor/index.d.ts +1 -1
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/editor/index.js.map +1 -1
- package/dist/styles.css +52 -23
- package/package.json +1 -1
- package/src/composer/PromptComposer.spec.ts +12 -1
- package/src/composer/PromptComposer.tsx +5 -1
- package/src/composer/RealtimeVoiceMode.spec.tsx +117 -9
- package/src/composer/RealtimeVoiceMode.tsx +119 -31
- package/src/composer/useRealtimeVoiceMode.spec.ts +28 -2
- package/src/composer/useRealtimeVoiceMode.tsx +30 -0
- package/src/conformance/conformance.spec.tsx +61 -1
- package/src/conformance/runner.tsx +153 -1
- package/src/editor/SharedRichEditor.tsx +13 -2
- package/src/editor/SlashCommandMenu.spec.ts +33 -0
- package/src/editor/SlashCommandMenu.tsx +20 -0
- package/src/editor/index.ts +2 -0
- package/src/styles.css +52 -23
|
@@ -89,9 +89,28 @@ const copy: RealtimeVoiceModeCopy = {
|
|
|
89
89
|
describe("RealtimeVoiceMode", () => {
|
|
90
90
|
let container: HTMLDivElement;
|
|
91
91
|
let root: Root;
|
|
92
|
+
let animate: ReturnType<typeof vi.fn>;
|
|
93
|
+
let cancelGlowAnimation: ReturnType<typeof vi.fn>;
|
|
94
|
+
let updateGlowPlaybackRate: ReturnType<typeof vi.fn>;
|
|
95
|
+
let originalAnimate: typeof HTMLElement.prototype.animate | undefined;
|
|
92
96
|
|
|
93
97
|
beforeEach(() => {
|
|
94
98
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
99
|
+
originalAnimate = HTMLElement.prototype.animate;
|
|
100
|
+
cancelGlowAnimation = vi.fn();
|
|
101
|
+
updateGlowPlaybackRate = vi.fn();
|
|
102
|
+
animate = vi.fn(
|
|
103
|
+
() =>
|
|
104
|
+
({
|
|
105
|
+
cancel: cancelGlowAnimation,
|
|
106
|
+
playbackRate: 1,
|
|
107
|
+
updatePlaybackRate: updateGlowPlaybackRate,
|
|
108
|
+
}) as unknown as Animation,
|
|
109
|
+
);
|
|
110
|
+
Object.defineProperty(HTMLElement.prototype, "animate", {
|
|
111
|
+
configurable: true,
|
|
112
|
+
value: animate,
|
|
113
|
+
});
|
|
95
114
|
container = document.createElement("div");
|
|
96
115
|
document.body.appendChild(container);
|
|
97
116
|
root = createRoot(container);
|
|
@@ -100,6 +119,14 @@ describe("RealtimeVoiceMode", () => {
|
|
|
100
119
|
afterEach(() => {
|
|
101
120
|
act(() => root.unmount());
|
|
102
121
|
container.remove();
|
|
122
|
+
if (originalAnimate) {
|
|
123
|
+
Object.defineProperty(HTMLElement.prototype, "animate", {
|
|
124
|
+
configurable: true,
|
|
125
|
+
value: originalAnimate,
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
Reflect.deleteProperty(HTMLElement.prototype, "animate");
|
|
129
|
+
}
|
|
103
130
|
vi.unstubAllGlobals();
|
|
104
131
|
});
|
|
105
132
|
|
|
@@ -291,22 +318,21 @@ describe("RealtimeVoiceMode", () => {
|
|
|
291
318
|
expect(document.body.textContent).toContain("Set up voice mode");
|
|
292
319
|
const prompt = document.querySelector<HTMLElement>('[role="dialog"]');
|
|
293
320
|
expect(prompt?.className).toContain(
|
|
294
|
-
"w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,
|
|
321
|
+
"w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,24rem),24rem)]",
|
|
295
322
|
);
|
|
296
323
|
expect(prompt?.dataset.collisionBoundary).toBe("agent-panel");
|
|
297
324
|
const actions = Array.from(prompt?.querySelectorAll("div") ?? []).find(
|
|
298
|
-
(element) => element.className.includes("
|
|
325
|
+
(element) => element.className.includes("bg-muted/30"),
|
|
299
326
|
);
|
|
300
327
|
const actionClasses = actions?.className.split(/\s+/) ?? [];
|
|
301
|
-
expect(actionClasses).toContain("
|
|
302
|
-
expect(actionClasses).
|
|
328
|
+
expect(actionClasses).toContain("grid");
|
|
329
|
+
expect(actionClasses).toContain("rounded-lg");
|
|
303
330
|
const buttons = Array.from(
|
|
304
331
|
document.querySelectorAll<HTMLButtonElement>("button"),
|
|
305
332
|
);
|
|
306
333
|
expect(
|
|
307
|
-
buttons.find((button) => button.textContent === "Connect Builder.io")
|
|
308
|
-
|
|
309
|
-
).toContain("whitespace-nowrap");
|
|
334
|
+
buttons.find((button) => button.textContent === "Connect Builder.io"),
|
|
335
|
+
).toBeDefined();
|
|
310
336
|
act(() =>
|
|
311
337
|
buttons
|
|
312
338
|
.find((button) => button.textContent === "Connect Builder.io")
|
|
@@ -766,7 +792,28 @@ describe("RealtimeVoiceMode", () => {
|
|
|
766
792
|
},
|
|
767
793
|
);
|
|
768
794
|
|
|
769
|
-
it("
|
|
795
|
+
it.each(["listening", "speaking"] as const)(
|
|
796
|
+
"keeps a soft session glow while %s",
|
|
797
|
+
(state) => {
|
|
798
|
+
render(
|
|
799
|
+
<RealtimeVoiceModeDock
|
|
800
|
+
state={state}
|
|
801
|
+
copy={copy}
|
|
802
|
+
chatVisible={false}
|
|
803
|
+
onToggleChat={vi.fn()}
|
|
804
|
+
onEndVoiceMode={vi.fn()}
|
|
805
|
+
/>,
|
|
806
|
+
);
|
|
807
|
+
expect(
|
|
808
|
+
document.querySelector(".agent-realtime-voice-glow"),
|
|
809
|
+
).not.toBeNull();
|
|
810
|
+
expect(
|
|
811
|
+
document.querySelector(".agent-realtime-voice-working"),
|
|
812
|
+
).toBeNull();
|
|
813
|
+
},
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
it("brightens the live glow while working", () => {
|
|
770
817
|
render(
|
|
771
818
|
<RealtimeVoiceModeDock
|
|
772
819
|
state="working"
|
|
@@ -776,10 +823,14 @@ describe("RealtimeVoiceMode", () => {
|
|
|
776
823
|
onEndVoiceMode={vi.fn()}
|
|
777
824
|
/>,
|
|
778
825
|
);
|
|
826
|
+
expect(document.querySelector(".agent-realtime-voice-glow")).not.toBeNull();
|
|
779
827
|
expect(
|
|
780
828
|
document.querySelector(".agent-realtime-voice-working"),
|
|
781
829
|
).not.toBeNull();
|
|
830
|
+
});
|
|
782
831
|
|
|
832
|
+
it("changes glow speed without restarting or jumping its rotation", () => {
|
|
833
|
+
vi.useFakeTimers();
|
|
783
834
|
render(
|
|
784
835
|
<RealtimeVoiceModeDock
|
|
785
836
|
state="listening"
|
|
@@ -789,9 +840,66 @@ describe("RealtimeVoiceMode", () => {
|
|
|
789
840
|
onEndVoiceMode={vi.fn()}
|
|
790
841
|
/>,
|
|
791
842
|
);
|
|
792
|
-
|
|
843
|
+
const glow = document.querySelector('[data-realtime-voice-glow="true"]');
|
|
844
|
+
|
|
845
|
+
expect(animate).toHaveBeenCalledTimes(1);
|
|
846
|
+
expect(updateGlowPlaybackRate).toHaveBeenLastCalledWith(1);
|
|
847
|
+
|
|
848
|
+
render(
|
|
849
|
+
<RealtimeVoiceModeDock
|
|
850
|
+
state="working"
|
|
851
|
+
copy={copy}
|
|
852
|
+
chatVisible={false}
|
|
853
|
+
onToggleChat={vi.fn()}
|
|
854
|
+
onEndVoiceMode={vi.fn()}
|
|
855
|
+
/>,
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
expect(document.querySelector('[data-realtime-voice-glow="true"]')).toBe(
|
|
859
|
+
glow,
|
|
860
|
+
);
|
|
861
|
+
expect(animate).toHaveBeenCalledTimes(1);
|
|
862
|
+
expect(cancelGlowAnimation).not.toHaveBeenCalled();
|
|
863
|
+
expect(updateGlowPlaybackRate).toHaveBeenLastCalledWith(2.4);
|
|
864
|
+
|
|
865
|
+
render(
|
|
866
|
+
<RealtimeVoiceModeDock
|
|
867
|
+
state="speaking"
|
|
868
|
+
copy={copy}
|
|
869
|
+
chatVisible={false}
|
|
870
|
+
onToggleChat={vi.fn()}
|
|
871
|
+
onEndVoiceMode={vi.fn()}
|
|
872
|
+
/>,
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
expect(animate).toHaveBeenCalledTimes(1);
|
|
876
|
+
expect(cancelGlowAnimation).not.toHaveBeenCalled();
|
|
877
|
+
expect(updateGlowPlaybackRate).toHaveBeenLastCalledWith(2.4);
|
|
878
|
+
|
|
879
|
+
act(() => vi.advanceTimersByTime(800));
|
|
880
|
+
expect(updateGlowPlaybackRate).toHaveBeenLastCalledWith(1);
|
|
881
|
+
vi.useRealTimers();
|
|
793
882
|
});
|
|
794
883
|
|
|
884
|
+
it.each(["connecting", "error", "ending"] as const)(
|
|
885
|
+
"does not show the live glow while %s",
|
|
886
|
+
(state) => {
|
|
887
|
+
render(
|
|
888
|
+
<RealtimeVoiceModeDock
|
|
889
|
+
state={state}
|
|
890
|
+
copy={copy}
|
|
891
|
+
chatVisible={false}
|
|
892
|
+
onToggleChat={vi.fn()}
|
|
893
|
+
onEndVoiceMode={vi.fn()}
|
|
894
|
+
/>,
|
|
895
|
+
);
|
|
896
|
+
expect(document.querySelector(".agent-realtime-voice-glow")).toBeNull();
|
|
897
|
+
expect(
|
|
898
|
+
document.querySelector(".agent-realtime-voice-working"),
|
|
899
|
+
).toBeNull();
|
|
900
|
+
},
|
|
901
|
+
);
|
|
902
|
+
|
|
795
903
|
it("shows an unmistakable loader and ignores early audio until connected", () => {
|
|
796
904
|
const audioLevels = createRealtimeVoiceAudioLevelStore();
|
|
797
905
|
audioLevels.set({ input: 0.8, output: 0 });
|
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
IconAlertTriangle,
|
|
12
12
|
IconBrain,
|
|
13
|
+
IconKey,
|
|
13
14
|
IconLanguage,
|
|
14
15
|
IconLoader2,
|
|
15
16
|
IconMicrophone,
|
|
16
17
|
IconPhoneOff,
|
|
18
|
+
IconPlugConnected,
|
|
17
19
|
IconSettings,
|
|
18
20
|
IconVolume,
|
|
19
21
|
} from "@tabler/icons-react";
|
|
@@ -225,72 +227,90 @@ export function RealtimeVoiceModeEntry({
|
|
|
225
227
|
collisionPadding={16}
|
|
226
228
|
data-collision-boundary={collisionBoundary ? "agent-panel" : "viewport"}
|
|
227
229
|
className={cn(
|
|
228
|
-
setupRequired ? "p-
|
|
230
|
+
setupRequired ? "p-3.5" : "p-3",
|
|
229
231
|
setupRequired
|
|
230
|
-
? "w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,
|
|
232
|
+
? "w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,24rem),24rem)]"
|
|
231
233
|
: "w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,18rem),18rem)]",
|
|
232
234
|
)}
|
|
233
235
|
aria-labelledby={titleId}
|
|
234
236
|
aria-describedby={setupRequired ? descriptionId : undefined}
|
|
235
237
|
>
|
|
236
|
-
<div className={cn("grid", setupRequired ? "gap-3" : "gap-2.5")}>
|
|
237
|
-
<div
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
<div className={cn("grid", setupRequired ? "gap-3.5" : "gap-2.5")}>
|
|
239
|
+
<div
|
|
240
|
+
className={cn(
|
|
241
|
+
setupRequired ? "flex items-start gap-3" : "grid gap-1",
|
|
242
|
+
)}
|
|
243
|
+
>
|
|
241
244
|
{setupRequired ? (
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
>
|
|
246
|
-
{copy.setupDescription}
|
|
247
|
-
</p>
|
|
245
|
+
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
246
|
+
<IconMicrophone aria-hidden="true" />
|
|
247
|
+
</div>
|
|
248
248
|
) : null}
|
|
249
|
+
<div className="grid gap-1">
|
|
250
|
+
<h2
|
|
251
|
+
id={titleId}
|
|
252
|
+
className="text-sm font-semibold leading-5 text-foreground"
|
|
253
|
+
>
|
|
254
|
+
{setupRequired ? copy.setupTitle : copy.promptTitle}
|
|
255
|
+
</h2>
|
|
256
|
+
{setupRequired ? (
|
|
257
|
+
<p
|
|
258
|
+
id={descriptionId}
|
|
259
|
+
className="text-xs leading-5 text-muted-foreground"
|
|
260
|
+
>
|
|
261
|
+
{copy.setupDescription}
|
|
262
|
+
</p>
|
|
263
|
+
) : null}
|
|
264
|
+
</div>
|
|
249
265
|
</div>
|
|
250
266
|
|
|
251
267
|
<div
|
|
252
268
|
className={cn(
|
|
253
|
-
"gap-2",
|
|
254
|
-
setupRequired
|
|
255
|
-
? "flex flex-col-reverse sm:flex-row sm:flex-wrap sm:justify-end"
|
|
256
|
-
: "grid",
|
|
269
|
+
"grid gap-2",
|
|
270
|
+
setupRequired ? "rounded-lg bg-muted/30 p-1" : null,
|
|
257
271
|
)}
|
|
258
272
|
>
|
|
259
273
|
{setupRequired ? (
|
|
260
274
|
<>
|
|
261
275
|
<Button
|
|
262
276
|
type="button"
|
|
263
|
-
variant="ghost"
|
|
264
277
|
size="sm"
|
|
265
|
-
|
|
278
|
+
className="w-full justify-start px-3"
|
|
279
|
+
disabled={connectingBuilder}
|
|
280
|
+
onClick={() =>
|
|
281
|
+
choose("realtime", onConnectBuilder ?? onStartVoiceMode)
|
|
282
|
+
}
|
|
266
283
|
>
|
|
267
|
-
{
|
|
284
|
+
{connectingBuilder ? (
|
|
285
|
+
<IconLoader2 className="animate-spin" />
|
|
286
|
+
) : (
|
|
287
|
+
<IconPlugConnected aria-hidden="true" />
|
|
288
|
+
)}
|
|
289
|
+
{copy.connectBuilder}
|
|
268
290
|
</Button>
|
|
269
291
|
<Button
|
|
270
292
|
type="button"
|
|
271
293
|
variant="outline"
|
|
272
294
|
size="sm"
|
|
295
|
+
className="w-full justify-start bg-background px-3"
|
|
273
296
|
onClick={() =>
|
|
274
297
|
choose("realtime", onUseOpenAiKey ?? onStartVoiceMode)
|
|
275
298
|
}
|
|
276
299
|
>
|
|
300
|
+
<IconKey aria-hidden="true" />
|
|
277
301
|
{openAiConfigured
|
|
278
302
|
? copy.startWithOpenAiKey
|
|
279
303
|
: copy.useOpenAiKey}
|
|
280
304
|
</Button>
|
|
281
305
|
<Button
|
|
282
306
|
type="button"
|
|
307
|
+
variant="ghost"
|
|
283
308
|
size="sm"
|
|
284
|
-
className="
|
|
285
|
-
|
|
286
|
-
onClick={() =>
|
|
287
|
-
choose("realtime", onConnectBuilder ?? onStartVoiceMode)
|
|
288
|
-
}
|
|
309
|
+
className="w-full justify-start px-3 text-muted-foreground"
|
|
310
|
+
onClick={() => choose("dictation", onKeepDictating)}
|
|
289
311
|
>
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
) : null}
|
|
293
|
-
{copy.connectBuilder}
|
|
312
|
+
<IconMicrophone aria-hidden="true" />
|
|
313
|
+
{copy.keepDictating}
|
|
294
314
|
</Button>
|
|
295
315
|
</>
|
|
296
316
|
) : (
|
|
@@ -382,9 +402,14 @@ export interface RealtimeVoiceModeInlineSettings {
|
|
|
382
402
|
const SILENT_AUDIO_LEVELS = createRealtimeVoiceAudioLevelStore();
|
|
383
403
|
const WAVEFORM_WEIGHTS = [0.55, 0.82, 1, 0.82, 0.55];
|
|
384
404
|
const AUDIO_ACTIVITY_THRESHOLD = 0.1;
|
|
405
|
+
const WORKING_GLOW_HOLD_MS = 800;
|
|
385
406
|
|
|
386
407
|
function usePrefersReducedMotion(): boolean {
|
|
387
|
-
const [reduced, setReduced] = useState(
|
|
408
|
+
const [reduced, setReduced] = useState(
|
|
409
|
+
() =>
|
|
410
|
+
typeof window !== "undefined" &&
|
|
411
|
+
Boolean(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches),
|
|
412
|
+
);
|
|
388
413
|
useEffect(() => {
|
|
389
414
|
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
390
415
|
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
@@ -697,6 +722,9 @@ export function RealtimeVoiceModeDock({
|
|
|
697
722
|
const controlsId = useId();
|
|
698
723
|
const [controlsOpen, setControlsOpen] = useState(false);
|
|
699
724
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
725
|
+
const [workingGlowHeld, setWorkingGlowHeld] = useState(state === "working");
|
|
726
|
+
const glowRef = useRef<HTMLSpanElement>(null);
|
|
727
|
+
const glowAnimationRef = useRef<Animation | null>(null);
|
|
700
728
|
const selectInteractionRef = useRef(false);
|
|
701
729
|
const selectInteractionFrameRef = useRef<number | null>(null);
|
|
702
730
|
const levels = useSyncExternalStore(
|
|
@@ -724,6 +752,56 @@ export function RealtimeVoiceModeDock({
|
|
|
724
752
|
const errorDetailVisible = state === "error" && Boolean(errorMessage);
|
|
725
753
|
const controlsVisible = controlsOpen || settingsOpen;
|
|
726
754
|
const chatPanelTranslation = useChatPanelTranslation(chatVisible);
|
|
755
|
+
const workingGlowActive = state === "working" || workingGlowHeld;
|
|
756
|
+
|
|
757
|
+
useEffect(() => {
|
|
758
|
+
if (state === "working") {
|
|
759
|
+
setWorkingGlowHeld(true);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (!workingGlowHeld) return;
|
|
763
|
+
const timer = window.setTimeout(
|
|
764
|
+
() => setWorkingGlowHeld(false),
|
|
765
|
+
WORKING_GLOW_HOLD_MS,
|
|
766
|
+
);
|
|
767
|
+
return () => window.clearTimeout(timer);
|
|
768
|
+
}, [state, workingGlowHeld]);
|
|
769
|
+
|
|
770
|
+
useEffect(() => {
|
|
771
|
+
if (!connected || reducedMotion || !glowRef.current) {
|
|
772
|
+
glowAnimationRef.current?.cancel();
|
|
773
|
+
glowAnimationRef.current = null;
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const animation = glowRef.current.animate(
|
|
778
|
+
[{ transform: "rotate(0turn)" }, { transform: "rotate(1turn)" }],
|
|
779
|
+
{
|
|
780
|
+
duration: 12_000,
|
|
781
|
+
easing: "linear",
|
|
782
|
+
iterations: Number.POSITIVE_INFINITY,
|
|
783
|
+
},
|
|
784
|
+
);
|
|
785
|
+
glowAnimationRef.current = animation;
|
|
786
|
+
|
|
787
|
+
return () => {
|
|
788
|
+
animation.cancel();
|
|
789
|
+
if (glowAnimationRef.current === animation) {
|
|
790
|
+
glowAnimationRef.current = null;
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
}, [connected, reducedMotion]);
|
|
794
|
+
|
|
795
|
+
useEffect(() => {
|
|
796
|
+
const animation = glowAnimationRef.current;
|
|
797
|
+
if (!animation) return;
|
|
798
|
+
const playbackRate = workingGlowActive ? 2.4 : 1;
|
|
799
|
+
if (typeof animation.updatePlaybackRate === "function") {
|
|
800
|
+
animation.updatePlaybackRate(playbackRate);
|
|
801
|
+
} else {
|
|
802
|
+
animation.playbackRate = playbackRate;
|
|
803
|
+
}
|
|
804
|
+
}, [workingGlowActive]);
|
|
727
805
|
|
|
728
806
|
const handleSelectOpenChange = useCallback((open: boolean) => {
|
|
729
807
|
if (selectInteractionFrameRef.current !== null) {
|
|
@@ -900,9 +978,19 @@ export function RealtimeVoiceModeDock({
|
|
|
900
978
|
className={cn(
|
|
901
979
|
"relative isolate size-16 overflow-visible rounded-full ring-1 backdrop-blur-xl transition-transform duration-150 ease-out focus-visible:ring-offset-2 active:scale-[0.97] motion-reduce:transition-none",
|
|
902
980
|
ORB_STATE_CLASSES[state],
|
|
903
|
-
state === "working" && "agent-realtime-voice-working",
|
|
904
981
|
)}
|
|
905
982
|
>
|
|
983
|
+
{connected ? (
|
|
984
|
+
<span
|
|
985
|
+
ref={glowRef}
|
|
986
|
+
aria-hidden="true"
|
|
987
|
+
data-realtime-voice-glow="true"
|
|
988
|
+
className={cn(
|
|
989
|
+
"agent-realtime-voice-glow",
|
|
990
|
+
workingGlowActive && "agent-realtime-voice-working",
|
|
991
|
+
)}
|
|
992
|
+
/>
|
|
993
|
+
) : null}
|
|
906
994
|
<span className="relative z-10 flex items-center justify-center">
|
|
907
995
|
{state === "connecting" ? (
|
|
908
996
|
<VoiceConnectingIndicator />
|
|
@@ -522,7 +522,7 @@ describe("Realtime voice dynamic tool manifests", () => {
|
|
|
522
522
|
});
|
|
523
523
|
|
|
524
524
|
describe("extractRealtimeVoiceFunctionCalls", () => {
|
|
525
|
-
it("
|
|
525
|
+
it("returns a function call as soon as its final arguments arrive", () => {
|
|
526
526
|
expect(
|
|
527
527
|
extractRealtimeVoiceFunctionCalls({
|
|
528
528
|
type: "response.function_call_arguments.done",
|
|
@@ -530,7 +530,13 @@ describe("extractRealtimeVoiceFunctionCalls", () => {
|
|
|
530
530
|
call_id: "call-1",
|
|
531
531
|
arguments: '{"path":"/inbox"}',
|
|
532
532
|
}),
|
|
533
|
-
).toEqual([
|
|
533
|
+
).toEqual([
|
|
534
|
+
{
|
|
535
|
+
name: "navigate",
|
|
536
|
+
callId: "call-1",
|
|
537
|
+
argumentsText: '{"path":"/inbox"}',
|
|
538
|
+
},
|
|
539
|
+
]);
|
|
534
540
|
});
|
|
535
541
|
|
|
536
542
|
it("falls back to completed function items on response.done", () => {
|
|
@@ -559,6 +565,26 @@ describe("extractRealtimeVoiceFunctionCalls", () => {
|
|
|
559
565
|
]);
|
|
560
566
|
});
|
|
561
567
|
|
|
568
|
+
it("accepts a completed function output item as a provider fallback", () => {
|
|
569
|
+
expect(
|
|
570
|
+
extractRealtimeVoiceFunctionCalls({
|
|
571
|
+
type: "response.output_item.done",
|
|
572
|
+
item: {
|
|
573
|
+
type: "function_call",
|
|
574
|
+
name: "navigate",
|
|
575
|
+
call_id: "call-output-item",
|
|
576
|
+
arguments: '{"path":"/todos"}',
|
|
577
|
+
},
|
|
578
|
+
}),
|
|
579
|
+
).toEqual([
|
|
580
|
+
{
|
|
581
|
+
name: "navigate",
|
|
582
|
+
callId: "call-output-item",
|
|
583
|
+
argumentsText: '{"path":"/todos"}',
|
|
584
|
+
},
|
|
585
|
+
]);
|
|
586
|
+
});
|
|
587
|
+
|
|
562
588
|
it("ignores function items from failed or cancelled responses", () => {
|
|
563
589
|
const output = [
|
|
564
590
|
{
|
|
@@ -1005,6 +1005,36 @@ export function createRealtimeVoiceToolManifestCoordinator(
|
|
|
1005
1005
|
export function extractRealtimeVoiceFunctionCalls(
|
|
1006
1006
|
event: RealtimeServerEvent,
|
|
1007
1007
|
): Array<{ name: string; callId: string; argumentsText: string }> {
|
|
1008
|
+
if (event.type === "response.function_call_arguments.done") {
|
|
1009
|
+
const name = typeof event.name === "string" ? event.name : "";
|
|
1010
|
+
const callId = typeof event.call_id === "string" ? event.call_id : "";
|
|
1011
|
+
if (!name || !callId) return [];
|
|
1012
|
+
return [
|
|
1013
|
+
{
|
|
1014
|
+
name,
|
|
1015
|
+
callId,
|
|
1016
|
+
argumentsText:
|
|
1017
|
+
typeof event.arguments === "string" ? event.arguments : "{}",
|
|
1018
|
+
},
|
|
1019
|
+
];
|
|
1020
|
+
}
|
|
1021
|
+
if (event.type === "response.output_item.done") {
|
|
1022
|
+
const item = event.item;
|
|
1023
|
+
if (!item || typeof item !== "object") return [];
|
|
1024
|
+
const record = item as Record<string, unknown>;
|
|
1025
|
+
if (record.type !== "function_call") return [];
|
|
1026
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
1027
|
+
const callId = typeof record.call_id === "string" ? record.call_id : "";
|
|
1028
|
+
if (!name || !callId) return [];
|
|
1029
|
+
return [
|
|
1030
|
+
{
|
|
1031
|
+
name,
|
|
1032
|
+
callId,
|
|
1033
|
+
argumentsText:
|
|
1034
|
+
typeof record.arguments === "string" ? record.arguments : "{}",
|
|
1035
|
+
},
|
|
1036
|
+
];
|
|
1037
|
+
}
|
|
1008
1038
|
if (event.type !== "response.done") return [];
|
|
1009
1039
|
const response = event.response;
|
|
1010
1040
|
if (!response || typeof response !== "object") return [];
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
5
|
import { defaultDesignSystemComponents } from "../design-system/default-adapter.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
DESIGN_SYSTEM_CONTRACT_VERSION,
|
|
8
|
+
type DesignSystemComponents,
|
|
9
|
+
} from "../design-system/types.js";
|
|
7
10
|
import { cssInJsFixtureAdapter } from "./__fixtures__/css-in-js-adapter.js";
|
|
8
11
|
import {
|
|
9
12
|
assertDesignSystemConformance,
|
|
@@ -80,4 +83,61 @@ describe("design-system conformance kit", () => {
|
|
|
80
83
|
});
|
|
81
84
|
expect(report.results.filter((result) => !result.passed)).toEqual([]);
|
|
82
85
|
});
|
|
86
|
+
|
|
87
|
+
it("rejects tabs that stack an icon above its label", async () => {
|
|
88
|
+
const stackedTabsAdapter: DesignSystemComponents = {
|
|
89
|
+
...cssInJsFixtureAdapter,
|
|
90
|
+
Tabs: ({ items, value, onChange, orientation }) => (
|
|
91
|
+
<div>
|
|
92
|
+
<div role="tablist" aria-orientation={orientation}>
|
|
93
|
+
{items.map((item) => (
|
|
94
|
+
<button
|
|
95
|
+
key={String(item.value)}
|
|
96
|
+
role="tab"
|
|
97
|
+
aria-selected={item.value === value}
|
|
98
|
+
style={{ display: "flex", flexDirection: "column" }}
|
|
99
|
+
onClick={() => onChange(item.value)}
|
|
100
|
+
>
|
|
101
|
+
{item.icon}
|
|
102
|
+
{item.label}
|
|
103
|
+
</button>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
{items.find((item) => item.value === value)?.content}
|
|
107
|
+
</div>
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
const report = await runDesignSystemConformance({
|
|
111
|
+
adapterName: "stacked-tabs fixture",
|
|
112
|
+
components: stackedTabsAdapter,
|
|
113
|
+
contractVersion: DESIGN_SYSTEM_CONTRACT_VERSION,
|
|
114
|
+
});
|
|
115
|
+
const tabsResult = report.results.find(
|
|
116
|
+
(result) => result.id === "behavior.tabs",
|
|
117
|
+
);
|
|
118
|
+
expect(tabsResult?.passed).toBe(false);
|
|
119
|
+
expect(tabsResult?.message).toContain("same row");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("rejects dialogs that render footer controls outside the dialog", async () => {
|
|
123
|
+
const detachedFooterAdapter: DesignSystemComponents = {
|
|
124
|
+
...cssInJsFixtureAdapter,
|
|
125
|
+
Dialog: ({ footer, ...props }) => (
|
|
126
|
+
<>
|
|
127
|
+
<cssInJsFixtureAdapter.Dialog {...props} />
|
|
128
|
+
{footer}
|
|
129
|
+
</>
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
const report = await runDesignSystemConformance({
|
|
133
|
+
adapterName: "detached-footer fixture",
|
|
134
|
+
components: detachedFooterAdapter,
|
|
135
|
+
contractVersion: DESIGN_SYSTEM_CONTRACT_VERSION,
|
|
136
|
+
});
|
|
137
|
+
const dialogResult = report.results.find(
|
|
138
|
+
(result) => result.id === "behavior.dialog",
|
|
139
|
+
);
|
|
140
|
+
expect(dialogResult?.passed).toBe(false);
|
|
141
|
+
expect(dialogResult?.message).toContain("footer");
|
|
142
|
+
});
|
|
83
143
|
});
|