@runtypelabs/persona 4.23.2 → 4.25.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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CI7OckWI.d.cts → types-Db61aI9J.d.cts} +2 -0
- package/dist/animations/{types-CI7OckWI.d.ts → types-Db61aI9J.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +2 -0
- package/dist/animations-extra.d.ts +2 -0
- package/dist/approval-ui-3NMYNXBD.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +278 -37
- package/dist/approval-ui.d.ts +278 -37
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
- package/dist/artifacts-ui.d.cts +273 -25
- package/dist/artifacts-ui.d.ts +273 -25
- package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +13 -13
- package/dist/context-mentions.d.cts +273 -25
- package/dist/context-mentions.d.ts +273 -25
- package/dist/event-stream-view.d.cts +273 -25
- package/dist/event-stream-view.d.ts +273 -25
- package/dist/forms-ui.d.cts +319 -27
- package/dist/forms-ui.d.ts +319 -27
- package/dist/history-view.d.cts +2 -0
- package/dist/history-view.d.ts +2 -0
- package/dist/index.cjs +70 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +344 -39
- package/dist/index.d.ts +344 -39
- package/dist/index.global.js +57 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +273 -25
- package/dist/session-reconnect.d.ts +273 -25
- package/dist/smart-dom-reader.d.cts +273 -25
- package/dist/smart-dom-reader.d.ts +273 -25
- package/dist/theme-editor-preview.cjs +74 -73
- package/dist/theme-editor-preview.d.cts +280 -25
- package/dist/theme-editor-preview.d.ts +280 -25
- package/dist/theme-editor-preview.js +51 -50
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +293 -27
- package/dist/theme-editor.d.ts +293 -27
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1522 -11
- package/dist/theme-reference.d.ts +1522 -11
- package/dist/theme-reference.js +1 -1
- package/dist/voice-runtime-7DA7MJH4.js +1 -0
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +33 -0
- package/dist/voice-runtime.d.ts +33 -0
- package/dist/voice-runtime.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +3 -2
- package/src/__fixtures__/defaults-parity.v4.json +776 -0
- package/src/approval-ui-entry.ts +0 -1
- package/src/approval-ui-loader.ts +5 -4
- package/src/approval-ui.ts +0 -1
- package/src/client-identity-proof.test.ts +193 -0
- package/src/client-session-warm.test.ts +216 -0
- package/src/client-visitor-history.test.ts +6 -0
- package/src/client.ts +122 -30
- package/src/codegen.test.ts +24 -0
- package/src/components/activity-row.test.ts +100 -0
- package/src/components/activity-row.ts +136 -0
- package/src/components/approval-actions.test.ts +8 -5
- package/src/components/approval-actions.ts +19 -6
- package/src/components/approval-bubble.test.ts +56 -9
- package/src/components/approval-bubble.ts +36 -41
- package/src/components/composer-builder.ts +5 -2
- package/src/components/composer-parts.ts +10 -2
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +7 -4
- package/src/components/header-layouts.test.ts +12 -0
- package/src/components/header-layouts.ts +3 -2
- package/src/components/launcher.ts +11 -2
- package/src/components/message-bubble.test.ts +22 -0
- package/src/components/message-bubble.ts +7 -3
- package/src/components/panel.test.ts +52 -1
- package/src/components/panel.ts +47 -8
- package/src/components/pill-composer-builder.test.ts +2 -0
- package/src/components/pill-composer-builder.ts +0 -1
- package/src/components/reasoning-bubble.ts +29 -17
- package/src/components/suggestions.test.ts +1 -0
- package/src/components/suggestions.ts +0 -16
- package/src/components/tool-bubble.ts +131 -114
- package/src/components/tool-details.test.ts +87 -0
- package/src/components/tool-details.ts +102 -0
- package/src/components/welcome.test.ts +8 -0
- package/src/components/welcome.ts +4 -2
- package/src/defaults-parity.test.ts +158 -0
- package/src/defaults.test.ts +69 -0
- package/src/defaults.ts +128 -49
- package/src/generated/runtype-openapi-contract.ts +2 -2
- package/src/index-core.ts +4 -0
- package/src/install.test.ts +24 -0
- package/src/install.ts +3 -1
- package/src/launcher-global.ts +2 -1
- package/src/launcher-variants.test.ts +52 -0
- package/src/runtime/init-update-reset.test.ts +21 -0
- package/src/session.ts +59 -7
- package/src/session.voice.test.ts +72 -1
- package/src/styles/widget-styles.test.ts +19 -0
- package/src/styles/widget.css +295 -23
- package/src/theme-editor/index.ts +2 -0
- package/src/theme-editor/preview-utils.test.ts +8 -0
- package/src/theme-editor/preview-utils.ts +7 -3
- package/src/theme-editor/sections.test.ts +31 -0
- package/src/theme-editor/sections.ts +55 -1
- package/src/theme-editor/state-preferences.test.ts +49 -0
- package/src/theme-editor/state.ts +73 -15
- package/src/theme-reference.test.ts +20 -0
- package/src/theme-reference.ts +11 -5
- package/src/tool-call-display-defaults.test.ts +33 -1
- package/src/types/theme.ts +80 -3
- package/src/types.ts +207 -22
- package/src/ui.activity-row.test.ts +91 -0
- package/src/ui.approval-plugin.test.ts +63 -0
- package/src/ui.composer-compact.test.ts +24 -2
- package/src/ui.composer-placement.test.ts +23 -0
- package/src/ui.detached-panel.test.ts +33 -6
- package/src/ui.docked.test.ts +19 -0
- package/src/ui.header-update-stability.test.ts +25 -0
- package/src/ui.identity-proof.test.ts +58 -0
- package/src/ui.launcher-teaser.test.ts +25 -0
- package/src/ui.message-width.test.ts +17 -2
- package/src/ui.reasoning-display.test.ts +92 -0
- package/src/ui.rebuild-composer.test.ts +20 -3
- package/src/ui.scroll.test.ts +32 -0
- package/src/ui.session-init.test.ts +312 -0
- package/src/ui.status-indicator.test.ts +82 -0
- package/src/ui.suggest-replies.test.ts +11 -0
- package/src/ui.ts +422 -74
- package/src/ui.v5-defaults.test.ts +144 -0
- package/src/ui.voice-prewarm.test.ts +222 -0
- package/src/ui.welcome.test.ts +92 -3
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
- package/src/utils/code-generators.ts +14 -0
- package/src/utils/composer-compact.test.ts +0 -2
- package/src/utils/composer-compact.ts +2 -4
- package/src/utils/composer-placement.ts +2 -3
- package/src/utils/config-merge.test.ts +81 -5
- package/src/utils/config-merge.ts +13 -3
- package/src/utils/defaults-provenance.ts +67 -0
- package/src/utils/dock.ts +7 -1
- package/src/utils/launcher-variant.ts +16 -0
- package/src/utils/panel-config.ts +97 -0
- package/src/utils/status-indicator.ts +39 -0
- package/src/utils/theme.test.ts +182 -0
- package/src/utils/theme.ts +131 -18
- package/src/utils/tokens.test.ts +122 -1
- package/src/utils/tokens.ts +177 -17
- package/src/voice/runtype-voice-provider.ts +238 -27
- package/src/voice/voice.test.ts +272 -2
- package/src/welcome.test.ts +23 -0
- package/src/welcome.ts +16 -9
- package/dist/approval-ui-K7R5Q5OC.js +0 -1
- package/dist/voice-runtime-EFR3R5ES.js +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createAgentExperience } from "./ui";
|
|
4
|
+
|
|
5
|
+
const controllers: ReturnType<typeof createAgentExperience>[] = [];
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
controllers.splice(0).forEach((controller) => controller.destroy());
|
|
8
|
+
document.body.replaceChildren();
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe.each([false, true])("core defaults lifecycle (v5=%s)", (v5Defaults) => {
|
|
13
|
+
it("applies a live defaults switch to existing messages and header", () => {
|
|
14
|
+
window.scrollTo = vi.fn();
|
|
15
|
+
const mount = document.createElement("div");
|
|
16
|
+
document.body.append(mount);
|
|
17
|
+
const controller = createAgentExperience(mount, {
|
|
18
|
+
apiUrl: "/dispatch", persistState: false,
|
|
19
|
+
future: { v5Defaults }, launcher: { autoExpand: true },
|
|
20
|
+
});
|
|
21
|
+
controllers.push(controller);
|
|
22
|
+
controller.injectUserMessage({ content: "Hello" });
|
|
23
|
+
controller.injectAssistantMessage({ content: "Welcome" });
|
|
24
|
+
const assertState = (v5: boolean) => {
|
|
25
|
+
expect(mount.dataset.personaDefaults).toBe(v5 ? "v5" : "v4");
|
|
26
|
+
const header = mount.querySelector<HTMLElement>(".persona-widget-header")!;
|
|
27
|
+
expect((header.firstElementChild as HTMLElement).style.width).toBe(v5 ? "20px" : "40px");
|
|
28
|
+
expect(mount.style.getPropertyValue("--persona-components-header-padding"))
|
|
29
|
+
.toBe(v5 ? "8px 8px 8px 16px" : "20px 24px");
|
|
30
|
+
const bubble = mount.querySelector<HTMLElement>(".persona-message-row-assistant .persona-message-bubble")!;
|
|
31
|
+
expect(bubble.style.lineHeight).toBe(v5 ? "var(--persona-message-assistant-line-height, 1.75)" : "");
|
|
32
|
+
const status = mount.querySelector<HTMLElement>("[data-persona-composer-status]")!;
|
|
33
|
+
expect(status.style.display).toBe(v5 ? "none" : "");
|
|
34
|
+
};
|
|
35
|
+
assertState(v5Defaults);
|
|
36
|
+
controller.update({ future: { v5Defaults: !v5Defaults } });
|
|
37
|
+
assertState(!v5Defaults);
|
|
38
|
+
controller.update({ future: { v5Defaults } });
|
|
39
|
+
assertState(v5Defaults);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("exposes composer chrome and honors explicit glyph overrides", () => {
|
|
43
|
+
window.scrollTo = vi.fn();
|
|
44
|
+
const mount = document.createElement("div");
|
|
45
|
+
document.body.append(mount);
|
|
46
|
+
const controller = createAgentExperience(mount, {
|
|
47
|
+
apiUrl: "/dispatch", persistState: false, future: { v5Defaults },
|
|
48
|
+
launcher: { autoExpand: true },
|
|
49
|
+
});
|
|
50
|
+
controllers.push(controller);
|
|
51
|
+
expect(mount.style.getPropertyValue("--persona-components-composer-sendIconSize"))
|
|
52
|
+
.toBe(v5Defaults ? "18px" : "");
|
|
53
|
+
expect(mount.style.getPropertyValue("--persona-components-composer-sendButtonRadius"))
|
|
54
|
+
.toBe(v5Defaults ? "9999px" : "");
|
|
55
|
+
expect(mount.style.getPropertyValue("--persona-components-composer-footerBorder"))
|
|
56
|
+
.toBe(v5Defaults ? "none" : "");
|
|
57
|
+
controller.update({
|
|
58
|
+
sendButton: { iconName: "send", iconSize: "15px" },
|
|
59
|
+
theme: { components: { composer: {
|
|
60
|
+
sendIconSize: "22px", sendButtonRadius: "8px", footerBorder: "2px solid red",
|
|
61
|
+
} } },
|
|
62
|
+
});
|
|
63
|
+
const svg = mount.querySelector<SVGElement>("[data-persona-composer-submit] svg")!;
|
|
64
|
+
expect(svg.getAttribute("width")).toBe("15");
|
|
65
|
+
expect(svg.style.width).toBe("");
|
|
66
|
+
expect(mount.style.getPropertyValue("--persona-components-composer-sendButtonRadius")).toBe("8px");
|
|
67
|
+
expect(mount.style.getPropertyValue("--persona-components-composer-footerBorder")).toBe("2px solid red");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("switches composer focus defaults live and retains explicit focus and resting chrome", () => {
|
|
71
|
+
window.scrollTo = vi.fn();
|
|
72
|
+
const mount = document.createElement("div");
|
|
73
|
+
document.body.append(mount);
|
|
74
|
+
const controller = createAgentExperience(mount, {
|
|
75
|
+
apiUrl: "/dispatch", persistState: false, future: { v5Defaults },
|
|
76
|
+
launcher: { autoExpand: true },
|
|
77
|
+
theme: { components: { composer: { borderColor: "#123456", shadow: "0 3px 8px #123456" } } },
|
|
78
|
+
});
|
|
79
|
+
controllers.push(controller);
|
|
80
|
+
const assertDefaults = (v5: boolean) => {
|
|
81
|
+
expect(mount.style.getPropertyValue("--persona-components-composer-focusBorderColor"))
|
|
82
|
+
.toBe(v5 ? "color-mix(in srgb, var(--persona-text) 28%, var(--persona-border))" : "");
|
|
83
|
+
expect(mount.style.getPropertyValue("--persona-components-composer-focusRing"))
|
|
84
|
+
.toBe(v5 ? "1px solid color-mix(in srgb, var(--persona-text) 12%, transparent)" : "");
|
|
85
|
+
expect(mount.style.getPropertyValue("--persona-composer-border-color")).toBe("#123456");
|
|
86
|
+
expect(mount.style.getPropertyValue("--persona-composer-shadow")).toBe("0 3px 8px #123456");
|
|
87
|
+
// No inline outline may suppress the stylesheet's ring.
|
|
88
|
+
expect(mount.querySelector<HTMLElement>("[data-persona-composer-form]")!.style.outline).toBe("");
|
|
89
|
+
};
|
|
90
|
+
assertDefaults(v5Defaults);
|
|
91
|
+
controller.update({ future: { v5Defaults: !v5Defaults } });
|
|
92
|
+
assertDefaults(!v5Defaults);
|
|
93
|
+
controller.update({ theme: { components: { composer: {
|
|
94
|
+
focusBorderColor: "#abcdef", focusRing: "2px solid #fedcba",
|
|
95
|
+
} } } });
|
|
96
|
+
for (const state of [v5Defaults, !v5Defaults]) {
|
|
97
|
+
controller.update({ future: { v5Defaults: state } });
|
|
98
|
+
expect(mount.style.getPropertyValue("--persona-components-composer-focusBorderColor")).toBe("#abcdef");
|
|
99
|
+
expect(mount.style.getPropertyValue("--persona-components-composer-focusRing")).toBe("2px solid #fedcba");
|
|
100
|
+
}
|
|
101
|
+
controller.update({ theme: { components: { composer: { focusRing: "none" } } } });
|
|
102
|
+
expect(mount.style.getPropertyValue("--persona-components-composer-focusRing")).toBe("none");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("switches default placement and fade while retaining explicit choices", () => {
|
|
106
|
+
window.scrollTo = vi.fn();
|
|
107
|
+
const mount = document.createElement("div");
|
|
108
|
+
document.body.append(mount);
|
|
109
|
+
const controller = createAgentExperience(mount, {
|
|
110
|
+
apiUrl: "/dispatch", persistState: false, future: { v5Defaults },
|
|
111
|
+
launcher: { autoExpand: true },
|
|
112
|
+
});
|
|
113
|
+
controllers.push(controller);
|
|
114
|
+
for (const state of [v5Defaults, !v5Defaults]) {
|
|
115
|
+
controller.update({ future: { v5Defaults: state } });
|
|
116
|
+
expect(mount.dataset.personaComposerPlacement).toBe(state ? "overlay" : "block");
|
|
117
|
+
expect(mount.style.getPropertyValue("--persona-composer-overlay-band"))
|
|
118
|
+
.toBe(state ? "linear-gradient(to bottom, transparent, var(--persona-container) 24px)" : "transparent");
|
|
119
|
+
}
|
|
120
|
+
controller.update({ composer: { placement: "block" }, theme: { components: { composer: { overlayBand: "transparent" } } } });
|
|
121
|
+
controller.update({ future: { v5Defaults: !v5Defaults } });
|
|
122
|
+
expect(mount.dataset.personaComposerPlacement).toBe("block");
|
|
123
|
+
expect(mount.style.getPropertyValue("--persona-composer-overlay-band")).toBe("transparent");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("uses fullscreen turn spacing in full-height embeds and honors explicit spacing", () => {
|
|
127
|
+
window.scrollTo = vi.fn();
|
|
128
|
+
const mount = document.createElement("div");
|
|
129
|
+
document.body.append(mount);
|
|
130
|
+
const controller = createAgentExperience(mount, {
|
|
131
|
+
apiUrl: "/dispatch", persistState: false, future: { v5Defaults },
|
|
132
|
+
launcher: { enabled: false, fullHeight: true },
|
|
133
|
+
});
|
|
134
|
+
controllers.push(controller);
|
|
135
|
+
const messages = mount.querySelector<HTMLElement>(".persona-widget-messages")!;
|
|
136
|
+
expect(messages.style.gap).toContain("--persona-components-message-fullscreenGap");
|
|
137
|
+
expect(mount.style.getPropertyValue("--persona-components-message-fullscreenGap"))
|
|
138
|
+
.toBe(v5Defaults ? "28px" : "");
|
|
139
|
+
controller.update({ theme: { components: { message: { gap: "33px" } } } });
|
|
140
|
+
expect(mount.style.getPropertyValue("--persona-components-message-fullscreenGap")).toBe("33px");
|
|
141
|
+
controller.update({ theme: { components: { message: { fullscreenGap: "42px" } } } });
|
|
142
|
+
expect(mount.style.getPropertyValue("--persona-components-message-fullscreenGap")).toBe("42px");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { createAgentExperience } from "./ui";
|
|
5
|
+
import type {
|
|
6
|
+
AgentWidgetConfig,
|
|
7
|
+
AgentWidgetVoicePrewarmHook,
|
|
8
|
+
AgentWidgetVoiceRecognitionConfig,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
let controller: ReturnType<typeof createAgentExperience> | undefined;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
window.scrollTo = vi.fn();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
controller?.destroy();
|
|
19
|
+
controller = undefined;
|
|
20
|
+
document.body.replaceChildren();
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
vi.unstubAllGlobals();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const initResponse = () =>
|
|
26
|
+
Response.json({
|
|
27
|
+
sessionId: "session-1",
|
|
28
|
+
expiresAt: new Date(Date.now() + 600_000).toISOString(),
|
|
29
|
+
flow: { id: "agent-1", name: "Agent", description: null },
|
|
30
|
+
config: { welcomeMessage: null, placeholder: "Ask...", theme: null },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const runtypeVoice = (
|
|
34
|
+
extra: Partial<AgentWidgetVoiceRecognitionConfig> = {}
|
|
35
|
+
): AgentWidgetVoiceRecognitionConfig => ({
|
|
36
|
+
enabled: true,
|
|
37
|
+
provider: { type: "runtype", runtype: { agentId: "agent-1" } },
|
|
38
|
+
...extra,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function mountWidget(config: Partial<AgentWidgetConfig>) {
|
|
42
|
+
const prewarms: Array<{ url: string; init?: RequestInit }> = [];
|
|
43
|
+
const inits: string[] = [];
|
|
44
|
+
vi.stubGlobal(
|
|
45
|
+
"fetch",
|
|
46
|
+
vi.fn(async (url: string, init?: RequestInit) => {
|
|
47
|
+
if (url.endsWith("/voice/prewarm")) {
|
|
48
|
+
prewarms.push({ url, init });
|
|
49
|
+
return new Response(null, { status: 204 });
|
|
50
|
+
}
|
|
51
|
+
if (url.endsWith("/v1/client/init")) {
|
|
52
|
+
inits.push(url);
|
|
53
|
+
return initResponse();
|
|
54
|
+
}
|
|
55
|
+
return new Response('data: {"type":"done"}\n\n', {
|
|
56
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
57
|
+
});
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
const mount = document.createElement("div");
|
|
61
|
+
document.body.appendChild(mount);
|
|
62
|
+
controller = createAgentExperience(mount, {
|
|
63
|
+
apiUrl: "https://api.example.com",
|
|
64
|
+
agentId: "agent-1",
|
|
65
|
+
launcher: { enabled: false },
|
|
66
|
+
persistState: false,
|
|
67
|
+
...config,
|
|
68
|
+
} as AgentWidgetConfig);
|
|
69
|
+
const mic = () => mount.querySelector<HTMLButtonElement>("[data-persona-composer-mic]")!;
|
|
70
|
+
const hover = () => mic().dispatchEvent(new Event("pointerover", { bubbles: true }));
|
|
71
|
+
return { mount, mic, hover, prewarms, inits };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Lets the lazy voice runtime install and any fire-and-forget fetch run.
|
|
75
|
+
const settle = async () => {
|
|
76
|
+
for (let i = 0; i < 5; i += 1) await new Promise((r) => setTimeout(r, 0));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
describe("voiceRecognition.prewarm: default", () => {
|
|
80
|
+
it("prewarms once on mic hover in client token mode (latched)", async () => {
|
|
81
|
+
const w = mountWidget({ clientToken: "ct_test", voiceRecognition: runtypeVoice() });
|
|
82
|
+
await settle();
|
|
83
|
+
expect(w.prewarms).toHaveLength(0);
|
|
84
|
+
|
|
85
|
+
w.hover();
|
|
86
|
+
await vi.waitFor(() => expect(w.prewarms).toHaveLength(1));
|
|
87
|
+
expect(w.prewarms[0].url).toBe(
|
|
88
|
+
"https://api.example.com/v1/client/agents/agent-1/voice/prewarm"
|
|
89
|
+
);
|
|
90
|
+
expect(w.prewarms[0].init?.headers).toEqual({ Authorization: "Bearer ct_test" });
|
|
91
|
+
|
|
92
|
+
w.hover();
|
|
93
|
+
w.mic().dispatchEvent(new Event("pointerdown", { bubbles: true }));
|
|
94
|
+
w.mic().dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
|
|
95
|
+
await settle();
|
|
96
|
+
expect(w.prewarms).toHaveLength(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("also prewarms on keyboard focus", async () => {
|
|
100
|
+
const w = mountWidget({ clientToken: "ct_test", voiceRecognition: runtypeVoice() });
|
|
101
|
+
await settle();
|
|
102
|
+
w.mic().dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
|
|
103
|
+
await vi.waitFor(() => expect(w.prewarms).toHaveLength(1));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("ignores hover elsewhere in the widget", async () => {
|
|
107
|
+
const w = mountWidget({ clientToken: "ct_test", voiceRecognition: runtypeVoice() });
|
|
108
|
+
await settle();
|
|
109
|
+
w.mount
|
|
110
|
+
.querySelector("[data-persona-composer-input]")!
|
|
111
|
+
.dispatchEvent(new Event("pointerover", { bubbles: true }));
|
|
112
|
+
await settle();
|
|
113
|
+
expect(w.prewarms).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("does not auto-prewarm outside client token mode", async () => {
|
|
117
|
+
const w = mountWidget({
|
|
118
|
+
voiceRecognition: {
|
|
119
|
+
enabled: true,
|
|
120
|
+
provider: { type: "runtype", runtype: { agentId: "agent-1", clientToken: "ct_voice" } },
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
await settle();
|
|
124
|
+
w.hover();
|
|
125
|
+
await settle();
|
|
126
|
+
expect(w.prewarms).toHaveLength(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("warms the client session on the browser (Web Speech) path", async () => {
|
|
130
|
+
vi.stubGlobal("SpeechRecognition", vi.fn());
|
|
131
|
+
const w = mountWidget({ clientToken: "ct_test", voiceRecognition: { enabled: true } });
|
|
132
|
+
await settle();
|
|
133
|
+
expect(w.inits).toHaveLength(0);
|
|
134
|
+
w.hover();
|
|
135
|
+
await vi.waitFor(() => expect(w.inits).toHaveLength(1));
|
|
136
|
+
expect(w.prewarms).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("voiceRecognition.prewarm: explicit settings", () => {
|
|
141
|
+
it("false opts out", async () => {
|
|
142
|
+
const w = mountWidget({
|
|
143
|
+
clientToken: "ct_test",
|
|
144
|
+
voiceRecognition: runtypeVoice({ prewarm: false }),
|
|
145
|
+
});
|
|
146
|
+
await settle();
|
|
147
|
+
w.hover();
|
|
148
|
+
await settle();
|
|
149
|
+
expect(w.prewarms).toHaveLength(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("'hover' enables it outside client token mode", async () => {
|
|
153
|
+
const w = mountWidget({
|
|
154
|
+
voiceRecognition: {
|
|
155
|
+
enabled: true,
|
|
156
|
+
prewarm: "hover",
|
|
157
|
+
provider: { type: "runtype", runtype: { agentId: "agent-1", clientToken: "ct_voice" } },
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
await settle();
|
|
161
|
+
w.hover();
|
|
162
|
+
await vi.waitFor(() => expect(w.prewarms).toHaveLength(1));
|
|
163
|
+
expect(w.prewarms[0].init?.headers).toEqual({ Authorization: "Bearer ct_voice" });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("a custom hook receives warm and the mic button, replaces hover, and is cleaned up", async () => {
|
|
167
|
+
const cleanup = vi.fn();
|
|
168
|
+
let context: Parameters<AgentWidgetVoicePrewarmHook>[0] | undefined;
|
|
169
|
+
const hook = vi.fn((ctx: Parameters<AgentWidgetVoicePrewarmHook>[0]) => {
|
|
170
|
+
context = ctx;
|
|
171
|
+
return cleanup;
|
|
172
|
+
});
|
|
173
|
+
const w = mountWidget({
|
|
174
|
+
clientToken: "ct_test",
|
|
175
|
+
voiceRecognition: runtypeVoice({ prewarm: hook }),
|
|
176
|
+
});
|
|
177
|
+
await settle();
|
|
178
|
+
expect(hook).toHaveBeenCalledOnce();
|
|
179
|
+
expect(context?.micButton).toBe(w.mic());
|
|
180
|
+
expect(context?.mount).toBe(w.mount);
|
|
181
|
+
|
|
182
|
+
w.hover();
|
|
183
|
+
await settle();
|
|
184
|
+
expect(w.prewarms).toHaveLength(0);
|
|
185
|
+
|
|
186
|
+
context!.warm();
|
|
187
|
+
await vi.waitFor(() => expect(w.prewarms).toHaveLength(1));
|
|
188
|
+
|
|
189
|
+
controller!.destroy();
|
|
190
|
+
controller = undefined;
|
|
191
|
+
expect(cleanup).toHaveBeenCalledOnce();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("update() swaps the hook and turning prewarm off releases it", async () => {
|
|
195
|
+
const cleanupA = vi.fn();
|
|
196
|
+
const hookA = vi.fn(() => cleanupA);
|
|
197
|
+
const hookB = vi.fn();
|
|
198
|
+
mountWidget({ clientToken: "ct_test", voiceRecognition: runtypeVoice({ prewarm: hookA }) });
|
|
199
|
+
await settle();
|
|
200
|
+
|
|
201
|
+
controller!.update({ voiceRecognition: runtypeVoice({ prewarm: hookB }) });
|
|
202
|
+
expect(cleanupA).toHaveBeenCalledOnce();
|
|
203
|
+
expect(hookB).toHaveBeenCalledOnce();
|
|
204
|
+
|
|
205
|
+
controller!.update({ voiceRecognition: runtypeVoice({ prewarm: false }) });
|
|
206
|
+
expect(hookB).toHaveBeenCalledOnce();
|
|
207
|
+
expect(hookA).toHaveBeenCalledOnce();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("a throwing hook does not break mount", async () => {
|
|
211
|
+
const w = mountWidget({
|
|
212
|
+
clientToken: "ct_test",
|
|
213
|
+
voiceRecognition: runtypeVoice({
|
|
214
|
+
prewarm: () => {
|
|
215
|
+
throw new Error("host bug");
|
|
216
|
+
},
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
await settle();
|
|
220
|
+
expect(w.mic()).toBeTruthy();
|
|
221
|
+
});
|
|
222
|
+
});
|
package/src/ui.welcome.test.ts
CHANGED
|
@@ -328,8 +328,8 @@ describe("welcome visibility state machine", () => {
|
|
|
328
328
|
});
|
|
329
329
|
|
|
330
330
|
describe("welcome greeting bubble", () => {
|
|
331
|
-
it("renders an assistant-styled bubble above the transcript", () => {
|
|
332
|
-
const { mount } = makeController({ welcome: { message: "Hi, I'm Ada." } });
|
|
331
|
+
it.each([false, true])("renders an assistant-styled bubble above the transcript (v5=%s)", (v5Defaults) => {
|
|
332
|
+
const { mount } = makeController({ future: { v5Defaults }, welcome: { message: "Hi, I'm Ada." } });
|
|
333
333
|
const greeting = greetingHost(mount);
|
|
334
334
|
expect(isVisible(greeting)).toBe(true);
|
|
335
335
|
expect(greeting.textContent).toBe("Hi, I'm Ada.");
|
|
@@ -342,7 +342,7 @@ describe("welcome greeting bubble", () => {
|
|
|
342
342
|
const body = mount.querySelector<HTMLElement>("#persona-scroll-container")!;
|
|
343
343
|
const children = Array.from(body.children);
|
|
344
344
|
const transcript = body.querySelector<HTMLElement>(
|
|
345
|
-
":scope > .persona-
|
|
345
|
+
":scope > .persona-widget-messages"
|
|
346
346
|
)!;
|
|
347
347
|
expect(children.indexOf(greeting)).toBeLessThan(
|
|
348
348
|
children.indexOf(transcript)
|
|
@@ -508,3 +508,92 @@ describe("welcome live updates", () => {
|
|
|
508
508
|
expect(isVisible(welcomeHost(mount))).toBe(true);
|
|
509
509
|
});
|
|
510
510
|
});
|
|
511
|
+
|
|
512
|
+
describe("centered welcome", () => {
|
|
513
|
+
it("groups legacy starters in the V5 welcome and restores them after clear", () => {
|
|
514
|
+
const { mount, controller } = makeController({ future: { v5Defaults: true }, suggestionChips: ["One", "Two", "Three", "Four", "Five"] });
|
|
515
|
+
const host = welcomeHost(mount);
|
|
516
|
+
expect(host.getAttribute("data-persona-welcome-layout")).toBe("centered");
|
|
517
|
+
expect(host.querySelector("h2")?.textContent).toBe("What can I help with?");
|
|
518
|
+
expect(host.querySelectorAll(".persona-suggestion")).toHaveLength(3);
|
|
519
|
+
expect(host.querySelector(".persona-suggestions")?.getAttribute("data-variant")).toBe("chip");
|
|
520
|
+
controller.injectTestMessage({ type: "message", message: assistantMessage() });
|
|
521
|
+
expect(isVisible(host)).toBe(true);
|
|
522
|
+
sendUserMessage(controller);
|
|
523
|
+
expect(isVisible(host)).toBe(false);
|
|
524
|
+
controller.clearChat();
|
|
525
|
+
expect(isVisible(host)).toBe(true);
|
|
526
|
+
expect(host.querySelectorAll(".persona-suggestion")).toHaveLength(3);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("raises fullscreen empty composers and defaults starters to four cards", () => {
|
|
530
|
+
const { mount } = makeController({ future: { v5Defaults: true }, launcher: { enabled: false, fullHeight: true }, suggestionChips: ["One", "Two", "Three", "Four", "Five"] });
|
|
531
|
+
expect(mount.getAttribute("data-persona-welcome-anchor")).toBe("center");
|
|
532
|
+
expect(welcomeHost(mount).querySelectorAll(".persona-suggestion")).toHaveLength(4);
|
|
533
|
+
expect(welcomeHost(mount).querySelector(".persona-suggestions")?.getAttribute("data-variant")).toBe("card");
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("updates starter density when the floating panel becomes mobile fullscreen", () => {
|
|
538
|
+
const originalWidth = window.innerWidth;
|
|
539
|
+
try {
|
|
540
|
+
Object.defineProperty(window, "innerWidth", { configurable: true, value: 1000 });
|
|
541
|
+
const { mount } = makeController({ future: { v5Defaults: true }, launcher: { enabled: true }, suggestionChips: ["One", "Two", "Three", "Four", "Five"] });
|
|
542
|
+
expect(welcomeHost(mount).querySelectorAll(".persona-suggestion")).toHaveLength(3);
|
|
543
|
+
Object.defineProperty(window, "innerWidth", { configurable: true, value: 500 });
|
|
544
|
+
window.dispatchEvent(new Event("resize"));
|
|
545
|
+
expect(mount.getAttribute("data-persona-welcome-anchor")).toBe("center");
|
|
546
|
+
expect(welcomeHost(mount).querySelectorAll(".persona-suggestion")).toHaveLength(4);
|
|
547
|
+
Object.defineProperty(window, "innerWidth", { configurable: true, value: 1000 });
|
|
548
|
+
window.dispatchEvent(new Event("resize"));
|
|
549
|
+
expect(mount.getAttribute("data-persona-welcome-anchor")).toBe("bottom");
|
|
550
|
+
expect(welcomeHost(mount).querySelectorAll(".persona-suggestion")).toHaveLength(3);
|
|
551
|
+
} finally {
|
|
552
|
+
Object.defineProperty(window, "innerWidth", { configurable: true, value: originalWidth });
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
describe.each([false, true])("explicit welcome layout (V5 defaults: %s)", (v5Defaults) => {
|
|
557
|
+
it("makes top override hero centering in empty and active states", () => {
|
|
558
|
+
const { mount, controller } = makeController({ future: { v5Defaults }, welcome: { layout: "top", variant: "hero", dismiss: "never" } });
|
|
559
|
+
expect(mount.getAttribute("data-persona-welcome-layout-explicit")).toBe("top");
|
|
560
|
+
expect(isVisible(welcomeHost(mount))).toBe(true);
|
|
561
|
+
sendUserMessage(controller);
|
|
562
|
+
expect(mount.getAttribute("data-persona-welcome-layout-explicit")).toBe("top");
|
|
563
|
+
expect(isVisible(welcomeHost(mount))).toBe(true);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("centers and resets explicit greeting/starters while honoring overrides", () => {
|
|
567
|
+
const { mount, controller } = makeController({
|
|
568
|
+
future: { v5Defaults },
|
|
569
|
+
welcome: { layout: "centered", title: "Custom", subtitle: "Scope", dismiss: "on-first-message", align: "start", anchor: "bottom" },
|
|
570
|
+
suggestions: { starters: { items: ["One", "Two", "Three", "Four", "Five"], maxItems: 2, variant: "list", behavior: "fill" } },
|
|
571
|
+
});
|
|
572
|
+
expect(welcomeHost(mount).getAttribute("data-persona-welcome-layout")).toBe("centered");
|
|
573
|
+
expect(welcomeHost(mount).getAttribute("data-persona-welcome-align")).toBe("start");
|
|
574
|
+
expect(mount.getAttribute("data-persona-welcome-anchor")).toBe("bottom");
|
|
575
|
+
expect(welcomeHost(mount).querySelector("h2")?.textContent).toBe("Custom");
|
|
576
|
+
expect(welcomeHost(mount).querySelectorAll(".persona-suggestion")).toHaveLength(2);
|
|
577
|
+
controller.injectTestMessage({ type: "message", message: assistantMessage() });
|
|
578
|
+
expect(isVisible(welcomeHost(mount))).toBe(true);
|
|
579
|
+
sendUserMessage(controller);
|
|
580
|
+
expect(isVisible(welcomeHost(mount))).toBe(false);
|
|
581
|
+
controller.clearChat();
|
|
582
|
+
expect(isVisible(welcomeHost(mount))).toBe(true);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("centers the measured fullscreen group and preserves an explicit percentage", () => {
|
|
586
|
+
const { mount, controller } = makeController({ future: { v5Defaults }, launcher: { enabled: false, fullHeight: true }, composer: { placement: "overlay" }, welcome: { layout: "centered" } });
|
|
587
|
+
const footer = mount.querySelector<HTMLElement>(".persona-widget-footer")!;
|
|
588
|
+
const container = footer.parentElement!;
|
|
589
|
+
Object.defineProperty(container, "clientHeight", { configurable: true, value: 800 });
|
|
590
|
+
Object.defineProperty(footer, "offsetHeight", { configurable: true, value: 100 });
|
|
591
|
+
Object.defineProperty(welcomeHost(mount), "offsetHeight", { configurable: true, value: 200 });
|
|
592
|
+
window.dispatchEvent(new Event("resize"));
|
|
593
|
+
// jsdom has no stylesheet geometry: the zero gap/body origin gives
|
|
594
|
+
// (800 - 200 - 100) / 2 rather than the old 44%-top anchoring.
|
|
595
|
+
expect(mount.style.getPropertyValue("--persona-composer-lift")).toBe("250px");
|
|
596
|
+
controller.update({ welcome: { layout: "centered", anchorComposerTop: "40%" } });
|
|
597
|
+
expect(mount.style.getPropertyValue("--persona-composer-lift")).toBe("380px");
|
|
598
|
+
});
|
|
599
|
+
});
|