@runtypelabs/persona 4.15.0 → 4.16.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/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +283 -9
- package/dist/context-mentions.d.ts +283 -9
- package/dist/index.cjs +68 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +299 -10
- package/dist/index.d.ts +299 -10
- package/dist/index.global.js +57 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +69 -69
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +283 -9
- package/dist/smart-dom-reader.d.ts +283 -9
- package/dist/testing.cjs +3 -3
- package/dist/testing.d.cts +7 -0
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +64 -64
- package/dist/theme-editor-preview.d.cts +283 -9
- package/dist/theme-editor-preview.d.ts +283 -9
- package/dist/theme-editor-preview.js +56 -56
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +283 -10
- package/dist/theme-editor.d.ts +283 -10
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +66 -3
- package/dist/theme-reference.d.ts +66 -3
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/components/artifact-pane.test.ts +27 -0
- package/src/components/artifact-pane.ts +7 -3
- package/src/components/header-builder.ts +27 -2
- package/src/components/header-layouts.test.ts +56 -0
- package/src/components/header-layouts.ts +29 -9
- package/src/components/launcher-surface.test.ts +403 -0
- package/src/components/launcher.ts +297 -2
- package/src/components/panel.ts +69 -74
- package/src/components/suggestions.test.ts +40 -0
- package/src/components/suggestions.ts +213 -153
- package/src/components/welcome.test.ts +121 -0
- package/src/components/welcome.ts +305 -0
- package/src/components/widget-view.test.ts +5 -3
- package/src/components/widget-view.ts +14 -7
- package/src/defaults.ts +2 -2
- package/src/index-core.ts +12 -1
- package/src/launcher-global.ts +10 -7
- package/src/plugins/types.ts +66 -1
- package/src/styles/widget-styles.test.ts +220 -0
- package/src/styles/widget.css +353 -9
- package/src/testing/mock-stream.test.ts +19 -0
- package/src/testing/mock-stream.ts +20 -0
- package/src/theme-editor/sections.ts +30 -1
- package/src/theme-reference.ts +26 -4
- package/src/types/theme.ts +40 -3
- package/src/types.ts +164 -6
- package/src/ui.body-top-slot.test.ts +95 -0
- package/src/ui.content-width.test.ts +91 -0
- package/src/ui.docked.test.ts +7 -4
- package/src/ui.launcher-teaser.test.ts +162 -0
- package/src/ui.rebuild-composer.test.ts +491 -0
- package/src/ui.render-welcome.test.ts +505 -0
- package/src/ui.scroll-additive.test.ts +91 -21
- package/src/ui.scroll.test.ts +302 -14
- package/src/ui.suggest-replies.test.ts +46 -0
- package/src/ui.ts +747 -136
- package/src/ui.welcome.test.ts +504 -0
- package/src/utils/artifact-gate.test.ts +27 -0
- package/src/utils/artifact-gate.ts +9 -0
- package/src/utils/attachment-manager.test.ts +37 -0
- package/src/utils/attachment-manager.ts +15 -0
- package/src/utils/auto-follow.test.ts +23 -0
- package/src/utils/auto-follow.ts +11 -1
- package/src/utils/config-merge.ts +2 -0
- package/src/utils/content-width.test.ts +37 -0
- package/src/utils/content-width.ts +17 -0
- package/src/utils/plugin-storage.test.ts +94 -0
- package/src/utils/plugin-storage.ts +98 -0
- package/src/utils/theme.test.ts +137 -0
- package/src/utils/tokens.ts +49 -3
- package/src/welcome.test.ts +213 -0
- package/src/welcome.ts +130 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createAgentExperience } from "./ui";
|
|
6
|
+
|
|
7
|
+
const mounts: HTMLElement[] = [];
|
|
8
|
+
const controllers: ReturnType<typeof createAgentExperience>[] = [];
|
|
9
|
+
|
|
10
|
+
const makeController = (config: Record<string, unknown> = {}) => {
|
|
11
|
+
const mount = document.createElement("div");
|
|
12
|
+
document.body.appendChild(mount);
|
|
13
|
+
mounts.push(mount);
|
|
14
|
+
const controller = createAgentExperience(mount, {
|
|
15
|
+
apiUrl: "https://api.example.com/chat",
|
|
16
|
+
launcher: { enabled: false },
|
|
17
|
+
persistState: false,
|
|
18
|
+
suggestionChips: [],
|
|
19
|
+
...config,
|
|
20
|
+
} as unknown as Parameters<typeof createAgentExperience>[1]);
|
|
21
|
+
controllers.push(controller);
|
|
22
|
+
return { mount, controller };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const wrapper = (mount: HTMLElement) =>
|
|
26
|
+
mount.querySelector<HTMLElement>("#persona-scroll-container > .persona-widget-messages")!;
|
|
27
|
+
|
|
28
|
+
describe("default content column", () => {
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
controllers.splice(0).forEach((controller) => controller.destroy());
|
|
31
|
+
mounts.splice(0).forEach((mount) => mount.remove());
|
|
32
|
+
document.body.innerHTML = "";
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("keeps the transcript wrapper :empty pre-conversation so CSS drops it from the flex flow", () => {
|
|
36
|
+
// The stylesheet's .persona-widget-messages:empty { display: none } only
|
|
37
|
+
// works if nothing seeds the wrapper with placeholder nodes.
|
|
38
|
+
const { mount, controller } = makeController();
|
|
39
|
+
const messagesWrapper = wrapper(mount);
|
|
40
|
+
expect(messagesWrapper.matches(":empty")).toBe(true);
|
|
41
|
+
|
|
42
|
+
controller.injectUserMessage({ content: "hi" });
|
|
43
|
+
expect(messagesWrapper.matches(":empty")).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("caps and centers the transcript at 768px by default", () => {
|
|
47
|
+
const { mount } = makeController();
|
|
48
|
+
const messagesWrapper = wrapper(mount);
|
|
49
|
+
expect(messagesWrapper.style.maxWidth).toBe("768px");
|
|
50
|
+
expect(messagesWrapper.style.marginLeft).toBe("auto");
|
|
51
|
+
expect(messagesWrapper.style.marginRight).toBe("auto");
|
|
52
|
+
expect(messagesWrapper.style.width).toBe("100%");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("honors an explicit layout.contentMaxWidth", () => {
|
|
56
|
+
const { mount } = makeController({ layout: { contentMaxWidth: "90ch" } });
|
|
57
|
+
expect(wrapper(mount).style.maxWidth).toBe("90ch");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("opts out with \"none\" for a full-width column", () => {
|
|
61
|
+
const { mount } = makeController({ layout: { contentMaxWidth: "none" } });
|
|
62
|
+
expect(wrapper(mount).style.maxWidth).toBe("none");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("caps the composer status text to the same column as the composer form", () => {
|
|
66
|
+
const { mount, controller } = makeController();
|
|
67
|
+
const statusText = mount.querySelector<HTMLElement>("[data-persona-composer-status]")!;
|
|
68
|
+
expect(statusText.style.maxWidth).toBe("768px");
|
|
69
|
+
expect(statusText.style.marginLeft).toBe("auto");
|
|
70
|
+
expect(statusText.style.marginRight).toBe("auto");
|
|
71
|
+
|
|
72
|
+
controller.update({ layout: { contentMaxWidth: "90ch" } });
|
|
73
|
+
expect(statusText.style.maxWidth).toBe("90ch");
|
|
74
|
+
|
|
75
|
+
controller.update({ layout: { contentMaxWidth: "" } });
|
|
76
|
+
expect(statusText.style.maxWidth).toBe("");
|
|
77
|
+
expect(statusText.style.marginLeft).toBe("");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("publishes the resolved column as a CSS var for plugin content", () => {
|
|
81
|
+
const { mount, controller } = makeController();
|
|
82
|
+
expect(
|
|
83
|
+
mount.style.getPropertyValue("--persona-content-max-width")
|
|
84
|
+
).toBe("768px");
|
|
85
|
+
|
|
86
|
+
controller.update({ layout: { contentMaxWidth: "90ch" } });
|
|
87
|
+
expect(
|
|
88
|
+
mount.style.getPropertyValue("--persona-content-max-width")
|
|
89
|
+
).toBe("90ch");
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/ui.docked.test.ts
CHANGED
|
@@ -27,24 +27,27 @@ describe("createAgentExperience docked mode", () => {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
const wrapper = mount.firstElementChild as HTMLElement | null;
|
|
30
|
-
|
|
30
|
+
// The launcher mounts inside its surface wrapper; visibility is driven on
|
|
31
|
+
// the wrapper so a teaser bubble hides with the button.
|
|
32
|
+
const launcherSurface = mount.lastElementChild as HTMLElement | null;
|
|
33
|
+
const launcherButton = launcherSurface?.querySelector("button") ?? null;
|
|
31
34
|
|
|
32
35
|
expect(wrapper).not.toBeNull();
|
|
33
36
|
expect(wrapper?.style.display).toBe("flex");
|
|
34
37
|
expect(launcherButton).not.toBeNull();
|
|
35
38
|
expect(launcherButton?.tagName).toBe("BUTTON");
|
|
36
|
-
expect(
|
|
39
|
+
expect(launcherSurface?.style.display).toBe("none");
|
|
37
40
|
expect(launcherButton?.className).toContain("persona-mt-4");
|
|
38
41
|
|
|
39
42
|
controller.close();
|
|
40
43
|
|
|
41
44
|
expect(wrapper?.style.display).toBe("none");
|
|
42
|
-
expect(
|
|
45
|
+
expect(launcherSurface?.style.display).toBe("none");
|
|
43
46
|
|
|
44
47
|
controller.open();
|
|
45
48
|
|
|
46
49
|
expect(wrapper?.style.display).toBe("flex");
|
|
47
|
-
expect(
|
|
50
|
+
expect(launcherSurface?.style.display).toBe("none");
|
|
48
51
|
|
|
49
52
|
controller.destroy();
|
|
50
53
|
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createAgentExperience } from "./ui";
|
|
6
|
+
import { mount as mountCriticalLauncher } from "./launcher-global";
|
|
7
|
+
|
|
8
|
+
const TEASER = "[data-persona-launcher-teaser]";
|
|
9
|
+
|
|
10
|
+
const createMount = () => {
|
|
11
|
+
const mount = document.createElement("div");
|
|
12
|
+
document.body.appendChild(mount);
|
|
13
|
+
return mount;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let keyCounter = 0;
|
|
17
|
+
const nextPrefix = () => `ui-teaser-test-${++keyCounter}-`;
|
|
18
|
+
|
|
19
|
+
describe("launcher teaser", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
window.scrollTo = vi.fn();
|
|
22
|
+
window.localStorage.clear();
|
|
23
|
+
window.sessionStorage.clear();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
document.body.innerHTML = "";
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
vi.useRealTimers();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders the same teaser from the full widget and the critical bundle", () => {
|
|
33
|
+
const prefix = nextPrefix();
|
|
34
|
+
const config = {
|
|
35
|
+
apiUrl: "https://api.example.com/chat",
|
|
36
|
+
persistState: { keyPrefix: prefix },
|
|
37
|
+
launcher: { teaser: { text: "Questions about pricing?" } },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const host = createMount();
|
|
41
|
+
const controller = createAgentExperience(host, config);
|
|
42
|
+
const eager = host.querySelector<HTMLElement>(TEASER);
|
|
43
|
+
|
|
44
|
+
const criticalTarget = createMount();
|
|
45
|
+
const critical = mountCriticalLauncher({
|
|
46
|
+
target: criticalTarget,
|
|
47
|
+
config,
|
|
48
|
+
onOpen: () => {},
|
|
49
|
+
});
|
|
50
|
+
const deferred = criticalTarget.querySelector<HTMLElement>(TEASER);
|
|
51
|
+
|
|
52
|
+
expect(eager).not.toBeNull();
|
|
53
|
+
expect(deferred).not.toBeNull();
|
|
54
|
+
expect(deferred!.outerHTML).toBe(eager!.outerHTML);
|
|
55
|
+
expect(deferred!.parentElement!.getAttribute("data-teaser")).toBe(
|
|
56
|
+
eager!.parentElement!.getAttribute("data-teaser")
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
critical.destroy();
|
|
60
|
+
controller.destroy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("live-updates the teaser through controller.update()", () => {
|
|
64
|
+
const prefix = nextPrefix();
|
|
65
|
+
const host = createMount();
|
|
66
|
+
const controller = createAgentExperience(host, {
|
|
67
|
+
apiUrl: "https://api.example.com/chat",
|
|
68
|
+
persistState: { keyPrefix: prefix },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(host.querySelector(TEASER)).toBeNull();
|
|
72
|
+
|
|
73
|
+
controller.update({ launcher: { teaser: { text: "Need a hand?" } } });
|
|
74
|
+
const teaser = host.querySelector<HTMLElement>(TEASER);
|
|
75
|
+
expect(teaser).not.toBeNull();
|
|
76
|
+
expect(teaser!.querySelector("button")!.textContent).toBe("Need a hand?");
|
|
77
|
+
|
|
78
|
+
controller.update({ launcher: { teaser: { text: "Still here to help" } } });
|
|
79
|
+
expect(host.querySelector<HTMLElement>(TEASER)!.querySelector("button")!.textContent).toBe(
|
|
80
|
+
"Still here to help"
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
controller.update({ launcher: { teaser: undefined } });
|
|
84
|
+
expect(host.querySelector(TEASER)).toBeNull();
|
|
85
|
+
|
|
86
|
+
controller.destroy();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("suppresses the teaser when the panel starts open", () => {
|
|
90
|
+
vi.useFakeTimers();
|
|
91
|
+
const prefix = nextPrefix();
|
|
92
|
+
const host = createMount();
|
|
93
|
+
const controller = createAgentExperience(host, {
|
|
94
|
+
apiUrl: "https://api.example.com/chat",
|
|
95
|
+
persistState: { keyPrefix: prefix },
|
|
96
|
+
launcher: { autoExpand: true, teaser: { text: "Need a hand?" } },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
vi.advanceTimersByTime(1000);
|
|
100
|
+
expect(host.querySelector<HTMLElement>(TEASER)!.hidden).toBe(true);
|
|
101
|
+
|
|
102
|
+
controller.destroy();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("suppresses the teaser when restored open state opens the panel", () => {
|
|
106
|
+
vi.useFakeTimers();
|
|
107
|
+
const prefix = nextPrefix();
|
|
108
|
+
window.sessionStorage.setItem(`${prefix}widget-open`, "true");
|
|
109
|
+
const host = createMount();
|
|
110
|
+
const controller = createAgentExperience(host, {
|
|
111
|
+
apiUrl: "https://api.example.com/chat",
|
|
112
|
+
persistState: { keyPrefix: prefix, persist: { openState: true } },
|
|
113
|
+
launcher: { teaser: { text: "Need a hand?" } },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
vi.advanceTimersByTime(1000);
|
|
117
|
+
|
|
118
|
+
expect(host.querySelector<HTMLElement>(TEASER)!.hidden).toBe(true);
|
|
119
|
+
expect(window.localStorage.getItem(`${prefix}teaser-dismissed`)).toBeNull();
|
|
120
|
+
|
|
121
|
+
controller.destroy();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("suppresses the teaser when onStateLoaded requests an open", () => {
|
|
125
|
+
vi.useFakeTimers();
|
|
126
|
+
const prefix = nextPrefix();
|
|
127
|
+
const host = createMount();
|
|
128
|
+
const controller = createAgentExperience(host, {
|
|
129
|
+
apiUrl: "https://api.example.com/chat",
|
|
130
|
+
persistState: { keyPrefix: prefix },
|
|
131
|
+
launcher: { teaser: { text: "Need a hand?" } },
|
|
132
|
+
onStateLoaded: (state) => ({ state, open: true }),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
vi.advanceTimersByTime(1000);
|
|
136
|
+
|
|
137
|
+
expect(host.querySelector<HTMLElement>(TEASER)!.hidden).toBe(true);
|
|
138
|
+
expect(window.localStorage.getItem(`${prefix}teaser-dismissed`)).toBeNull();
|
|
139
|
+
|
|
140
|
+
controller.destroy();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("clears the pending teaser timer when the widget is destroyed", () => {
|
|
144
|
+
vi.useFakeTimers();
|
|
145
|
+
const prefix = nextPrefix();
|
|
146
|
+
const host = createMount();
|
|
147
|
+
const controller = createAgentExperience(host, {
|
|
148
|
+
apiUrl: "https://api.example.com/chat",
|
|
149
|
+
persistState: { keyPrefix: prefix },
|
|
150
|
+
launcher: { teaser: { text: "Need a hand?", delayMs: 5000 } },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const teaser = host.querySelector<HTMLElement>(TEASER);
|
|
154
|
+
expect(teaser!.hidden).toBe(true);
|
|
155
|
+
|
|
156
|
+
controller.destroy();
|
|
157
|
+
vi.advanceTimersByTime(10000);
|
|
158
|
+
|
|
159
|
+
expect(teaser!.hidden).toBe(true);
|
|
160
|
+
expect(host.querySelector(TEASER)).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
});
|