@runtypelabs/persona 4.14.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/README.md +4 -4
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
- package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-QJ4LV2KW.js +1 -0
- package/dist/codegen.cjs +12 -11
- package/dist/codegen.js +14 -13
- package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
- package/dist/context-mentions.cjs +11 -8
- package/dist/context-mentions.d.cts +520 -21
- package/dist/context-mentions.d.ts +520 -21
- package/dist/context-mentions.js +11 -8
- package/dist/index.cjs +69 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +595 -46
- package/dist/index.d.ts +595 -46
- package/dist/index.global.js +60 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +69 -68
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +520 -21
- package/dist/smart-dom-reader.d.ts +520 -21
- 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 +67 -67
- package/dist/theme-editor-preview.d.cts +534 -22
- package/dist/theme-editor-preview.d.ts +534 -22
- package/dist/theme-editor-preview.js +57 -57
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +521 -23
- package/dist/theme-editor.d.ts +521 -23
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +104 -3
- package/dist/theme-reference.d.ts +104 -3
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/ask-user-question-tool.ts +10 -7
- package/src/components/artifact-pane.test.ts +27 -0
- package/src/components/artifact-pane.ts +7 -3
- package/src/components/artifact-preview.ts +1 -1
- package/src/components/composer-parts.test.ts +103 -4
- package/src/components/composer-parts.ts +53 -15
- 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/messages.ts +5 -2
- package/src/components/panel.ts +92 -71
- package/src/components/pill-composer-builder.test.ts +18 -0
- package/src/components/suggestions.test.ts +273 -0
- package/src/components/suggestions.ts +528 -86
- 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 +18 -7
- package/src/defaults.ts +7 -5
- package/src/generated/runtype-openapi-contract.ts +17 -0
- package/src/index-core.ts +29 -2
- package/src/launcher-global.ts +11 -8
- package/src/plugins/types.ts +136 -3
- package/src/presets.ts +3 -3
- package/src/session.ts +10 -8
- package/src/styles/widget-styles.test.ts +382 -0
- package/src/styles/widget.css +778 -4
- package/src/suggest-replies-tool.test.ts +233 -2
- package/src/suggest-replies-tool.ts +115 -39
- package/src/testing/mock-stream.test.ts +19 -0
- package/src/testing/mock-stream.ts +20 -0
- package/src/theme-editor/preview-utils.test.ts +34 -0
- package/src/theme-editor/preview-utils.ts +77 -2
- package/src/theme-editor/sections.test.ts +33 -0
- package/src/theme-editor/sections.ts +83 -6
- package/src/theme-reference.ts +34 -5
- package/src/types/theme.ts +72 -3
- package/src/types.ts +312 -18
- package/src/ui.body-top-slot.test.ts +95 -0
- package/src/ui.content-width.test.ts +91 -0
- package/src/ui.detached-panel.test.ts +1 -1
- 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 +1112 -1
- package/src/ui.ts +1008 -149
- package/src/ui.welcome.test.ts +504 -0
- package/src/utils/artifact-file.test.ts +1 -1
- 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/code-generators.test.ts +40 -0
- package/src/utils/code-generators.ts +22 -0
- package/src/utils/config-merge.test.ts +24 -0
- 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/icons.test.ts +26 -0
- package/src/utils/icons.ts +8 -0
- package/src/utils/plugin-storage.test.ts +94 -0
- package/src/utils/plugin-storage.ts +98 -0
- package/src/utils/theme.test.ts +216 -0
- package/src/utils/tokens.ts +113 -3
- package/src/welcome.test.ts +213 -0
- package/src/welcome.ts +130 -0
- package/dist/chunk-MMUPR2JW.js +0 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
animateWelcomeOut,
|
|
7
|
+
applyWelcomeConfig,
|
|
8
|
+
applyWelcomeVisibility,
|
|
9
|
+
buildGreetingHost,
|
|
10
|
+
buildWelcomeHost,
|
|
11
|
+
renderWelcomeGreeting,
|
|
12
|
+
} from "./welcome";
|
|
13
|
+
import { resolveWelcomeConfig } from "../welcome";
|
|
14
|
+
import type { AgentWidgetConfig } from "../types";
|
|
15
|
+
|
|
16
|
+
const makeHost = () => {
|
|
17
|
+
const starters = document.createElement("div");
|
|
18
|
+
return buildWelcomeHost(undefined, starters);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("welcome host", () => {
|
|
22
|
+
it("carries the permanent host attributes", () => {
|
|
23
|
+
const { host } = makeHost();
|
|
24
|
+
expect(host.hasAttribute("data-persona-welcome")).toBe(true);
|
|
25
|
+
expect(host.hasAttribute("data-persona-intro-card")).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("gives the title and subtitle their tokenized typography classes", () => {
|
|
29
|
+
const { title, subtitle } = makeHost();
|
|
30
|
+
expect(title.tagName).toBe("H2");
|
|
31
|
+
expect(title.className).toBe("persona-welcome-title");
|
|
32
|
+
expect(subtitle.tagName).toBe("P");
|
|
33
|
+
expect(subtitle.className).toBe("persona-welcome-subtitle");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("applies resolved config in place", () => {
|
|
37
|
+
const elements = makeHost();
|
|
38
|
+
applyWelcomeConfig(
|
|
39
|
+
elements,
|
|
40
|
+
resolveWelcomeConfig({
|
|
41
|
+
welcome: { title: "Hi", subtitle: "Scope", variant: "hero" },
|
|
42
|
+
} as AgentWidgetConfig)
|
|
43
|
+
);
|
|
44
|
+
expect(elements.title.textContent).toBe("Hi");
|
|
45
|
+
expect(elements.subtitle.textContent).toBe("Scope");
|
|
46
|
+
expect(elements.host.getAttribute("data-persona-welcome-variant")).toBe(
|
|
47
|
+
"hero"
|
|
48
|
+
);
|
|
49
|
+
expect(elements.host.getAttribute("data-persona-welcome-dismiss")).toBe(
|
|
50
|
+
"on-first-message"
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("tightens the body gap when the surface is hidden", () => {
|
|
55
|
+
const { host } = makeHost();
|
|
56
|
+
const body = document.createElement("div");
|
|
57
|
+
body.className = "persona-gap-6";
|
|
58
|
+
applyWelcomeVisibility(body, host, false);
|
|
59
|
+
expect(body.classList.contains("persona-gap-3")).toBe(true);
|
|
60
|
+
expect(body.classList.contains("persona-gap-6")).toBe(false);
|
|
61
|
+
applyWelcomeVisibility(body, host, true);
|
|
62
|
+
expect(body.classList.contains("persona-gap-6")).toBe(true);
|
|
63
|
+
expect(host.hidden).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("greeting bubble", () => {
|
|
68
|
+
it("renders the greeting as plain text, never markup", () => {
|
|
69
|
+
const host = buildGreetingHost();
|
|
70
|
+
renderWelcomeGreeting(host, "<b>bold</b> & fine", undefined);
|
|
71
|
+
expect(host.querySelector("b")).toBeNull();
|
|
72
|
+
expect(host.textContent).toBe("<b>bold</b> & fine");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("clears and hides when the greeting is removed", () => {
|
|
76
|
+
const host = buildGreetingHost();
|
|
77
|
+
renderWelcomeGreeting(host, "Hi", undefined);
|
|
78
|
+
expect(host.hidden).toBe(false);
|
|
79
|
+
renderWelcomeGreeting(host, undefined, undefined);
|
|
80
|
+
expect(host.hidden).toBe(true);
|
|
81
|
+
expect(host.childElementCount).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("hero dismiss animation", () => {
|
|
86
|
+
it("runs on the Web Animations API and settles after it finishes", async () => {
|
|
87
|
+
const { host } = makeHost();
|
|
88
|
+
document.body.appendChild(host);
|
|
89
|
+
let resolveFinished: () => void = () => {};
|
|
90
|
+
const finished = new Promise<void>((resolve) => {
|
|
91
|
+
resolveFinished = resolve;
|
|
92
|
+
});
|
|
93
|
+
const animate = vi.fn(
|
|
94
|
+
(_keyframes: unknown, _options?: { fill?: string }) =>
|
|
95
|
+
({ finished }) as unknown as Animation
|
|
96
|
+
);
|
|
97
|
+
(host as unknown as { animate: typeof animate }).animate = animate;
|
|
98
|
+
|
|
99
|
+
const onFinished = vi.fn();
|
|
100
|
+
const animation = animateWelcomeOut(host, onFinished);
|
|
101
|
+
expect(animate).toHaveBeenCalledTimes(1);
|
|
102
|
+
// `forwards`: `backwards` only fills the delay phase, so the element would
|
|
103
|
+
// flash back to opacity 1 after the keyframes end.
|
|
104
|
+
expect(animate.mock.calls[0]![1]?.fill).toBe("forwards");
|
|
105
|
+
expect(animation).toBeTruthy();
|
|
106
|
+
expect(onFinished).not.toHaveBeenCalled();
|
|
107
|
+
|
|
108
|
+
resolveFinished();
|
|
109
|
+
await finished;
|
|
110
|
+
await Promise.resolve();
|
|
111
|
+
expect(onFinished).toHaveBeenCalledWith(animation);
|
|
112
|
+
host.remove();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("settles immediately without Web Animations support", () => {
|
|
116
|
+
const { host } = makeHost();
|
|
117
|
+
const onFinished = vi.fn();
|
|
118
|
+
expect(animateWelcomeOut(host, onFinished)).toBeNull();
|
|
119
|
+
expect(onFinished).toHaveBeenCalledWith(null);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { createElement, createNode } from "../utils/dom";
|
|
2
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
3
|
+
import { getBubbleClasses } from "./message-bubble";
|
|
4
|
+
import type {
|
|
5
|
+
AgentWidgetConfig,
|
|
6
|
+
AgentWidgetWelcomeIcon,
|
|
7
|
+
} from "../types";
|
|
8
|
+
import type { ResolvedWelcomeConfig } from "../welcome";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Welcome surface DOM. The host is permanent: it mounts once with the panel
|
|
12
|
+
* and every variant/visibility change is applied to it in place, so live
|
|
13
|
+
* `controller.update()` never rebuilds it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface WelcomeHostElements {
|
|
17
|
+
/** Permanent host; also carries `data-persona-intro-card` for slot parity. */
|
|
18
|
+
host: HTMLElement;
|
|
19
|
+
iconHolder: HTMLElement;
|
|
20
|
+
title: HTMLElement;
|
|
21
|
+
subtitle: HTMLElement;
|
|
22
|
+
starterSuggestions: HTMLElement;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const WELCOME_ICON_SIZE = 40;
|
|
26
|
+
|
|
27
|
+
/** Replaces the holder's content; hides it when no icon is configured. */
|
|
28
|
+
export const applyWelcomeIcon = (
|
|
29
|
+
holder: HTMLElement,
|
|
30
|
+
icon: AgentWidgetWelcomeIcon | undefined
|
|
31
|
+
): void => {
|
|
32
|
+
holder.replaceChildren();
|
|
33
|
+
if (!icon) {
|
|
34
|
+
holder.hidden = true;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof icon === "function") {
|
|
39
|
+
let custom: HTMLElement | SVGElement | null = null;
|
|
40
|
+
try {
|
|
41
|
+
custom = icon();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn("[persona] welcome.icon renderer threw", error);
|
|
44
|
+
}
|
|
45
|
+
if (!custom) {
|
|
46
|
+
holder.hidden = true;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
holder.appendChild(custom);
|
|
50
|
+
holder.hidden = false;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (icon.type === "lucide") {
|
|
55
|
+
const svg = renderLucideIcon(icon.name, WELCOME_ICON_SIZE);
|
|
56
|
+
if (!svg) {
|
|
57
|
+
holder.hidden = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
holder.appendChild(svg);
|
|
61
|
+
} else if (icon.type === "image") {
|
|
62
|
+
holder.appendChild(
|
|
63
|
+
createNode("img", {
|
|
64
|
+
className: "persona-welcome-icon-image",
|
|
65
|
+
attrs: { src: icon.url, alt: icon.alt },
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
holder.appendChild(
|
|
70
|
+
createNode("span", {
|
|
71
|
+
className: "persona-welcome-icon-text",
|
|
72
|
+
text: icon.text,
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
holder.hidden = false;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build the welcome host. `starterSuggestions` is created by the caller so the
|
|
81
|
+
* suggestion surface keeps its existing wiring.
|
|
82
|
+
*/
|
|
83
|
+
export const buildWelcomeHost = (
|
|
84
|
+
config: AgentWidgetConfig | undefined,
|
|
85
|
+
starterSuggestions: HTMLElement,
|
|
86
|
+
options: { flatShadow?: boolean } = {}
|
|
87
|
+
): WelcomeHostElements => {
|
|
88
|
+
const iconHolder = createNode("div", {
|
|
89
|
+
className: "persona-welcome-icon",
|
|
90
|
+
attrs: { "data-persona-welcome-icon": "" },
|
|
91
|
+
});
|
|
92
|
+
iconHolder.hidden = true;
|
|
93
|
+
|
|
94
|
+
// Typography flows through `components.introCard.title` / `.subtitle`;
|
|
95
|
+
// the CSS fallbacks reproduce the utility classes these replaced.
|
|
96
|
+
const title = createElement("h2", "persona-welcome-title");
|
|
97
|
+
const subtitle = createElement("p", "persona-welcome-subtitle");
|
|
98
|
+
|
|
99
|
+
// Background and box-shadow flow through the themable `components.introCard`
|
|
100
|
+
// tokens; both default to flat. Docked mode always stays flat. Padding comes
|
|
101
|
+
// from `--persona-intro-card-padding` (see widget.css), which zeroes its
|
|
102
|
+
// horizontal component when the card resolves flat so the text shares the
|
|
103
|
+
// content column's left edge instead of carrying a phantom card inset.
|
|
104
|
+
const host = createNode(
|
|
105
|
+
"div",
|
|
106
|
+
{
|
|
107
|
+
className: "persona-welcome persona-rounded-2xl",
|
|
108
|
+
attrs: {
|
|
109
|
+
"data-persona-welcome": "",
|
|
110
|
+
"data-persona-intro-card": "",
|
|
111
|
+
"data-persona-welcome-variant": "card",
|
|
112
|
+
},
|
|
113
|
+
style: {
|
|
114
|
+
background: "var(--persona-intro-card-bg, transparent)",
|
|
115
|
+
boxShadow: options.flatShadow
|
|
116
|
+
? "none"
|
|
117
|
+
: "var(--persona-intro-card-shadow, none)",
|
|
118
|
+
border: "var(--persona-intro-card-border, none)",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
iconHolder,
|
|
122
|
+
title,
|
|
123
|
+
subtitle,
|
|
124
|
+
starterSuggestions
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return { host, iconHolder, title, subtitle, starterSuggestions };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Show/hide the permanent host. The body gap tightens when nothing renders
|
|
132
|
+
* there so a hidden welcome leaves no phantom section spacing.
|
|
133
|
+
*/
|
|
134
|
+
export const applyWelcomeVisibility = (
|
|
135
|
+
body: HTMLElement,
|
|
136
|
+
host: HTMLElement,
|
|
137
|
+
visible: boolean
|
|
138
|
+
): void => {
|
|
139
|
+
host.hidden = !visible;
|
|
140
|
+
host.style.display = visible ? "" : "none";
|
|
141
|
+
body.classList.toggle("persona-gap-6", visible);
|
|
142
|
+
body.classList.toggle("persona-gap-3", !visible);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Apply resolved config to the permanent host. Every field is set on every
|
|
147
|
+
* call so `controller.update()` can drop a field and see it revert.
|
|
148
|
+
*/
|
|
149
|
+
export const applyWelcomeConfig = (
|
|
150
|
+
elements: WelcomeHostElements,
|
|
151
|
+
resolved: ResolvedWelcomeConfig
|
|
152
|
+
): void => {
|
|
153
|
+
elements.title.textContent = resolved.title;
|
|
154
|
+
elements.subtitle.textContent = resolved.subtitle;
|
|
155
|
+
// An empty subtitle omits the paragraph entirely, margin included.
|
|
156
|
+
elements.subtitle.hidden = resolved.subtitle === "";
|
|
157
|
+
applyWelcomeIcon(elements.iconHolder, resolved.icon);
|
|
158
|
+
elements.host.setAttribute("data-persona-welcome-variant", resolved.variant);
|
|
159
|
+
elements.host.setAttribute("data-persona-welcome-dismiss", resolved.dismiss);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** Marks the element a `renderWelcome` plugin returned. */
|
|
163
|
+
export const WELCOME_PLUGIN_CONTENT_ATTR = "data-persona-welcome-plugin";
|
|
164
|
+
const WELCOME_OVERLAY_ATTR = "data-persona-welcome-overlay";
|
|
165
|
+
const WELCOME_OVERLAY_BODY_CLASS = "persona-welcome-overlay-active";
|
|
166
|
+
|
|
167
|
+
// An absolute child of a scrolled scroller sits at the content origin, not
|
|
168
|
+
// the visible viewport, and overflow: hidden freezes scrollTop rather than
|
|
169
|
+
// resetting it. Zero the scroll while the overlay is up, restore on clear.
|
|
170
|
+
const overlayScrollTop = new WeakMap<HTMLElement, number>();
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Mount plugin content inside the permanent host. The default children are
|
|
174
|
+
* hidden by CSS while the content attribute is set, so nothing about their
|
|
175
|
+
* own hidden state has to be saved and restored.
|
|
176
|
+
*/
|
|
177
|
+
export const mountWelcomePluginContent = (
|
|
178
|
+
body: HTMLElement,
|
|
179
|
+
host: HTMLElement,
|
|
180
|
+
element: HTMLElement
|
|
181
|
+
): void => {
|
|
182
|
+
element.setAttribute(WELCOME_PLUGIN_CONTENT_ATTR, "");
|
|
183
|
+
host.appendChild(element);
|
|
184
|
+
host.setAttribute("data-persona-welcome-content", "plugin");
|
|
185
|
+
// Plugin content renders regardless of derived visibility and overlays the
|
|
186
|
+
// transcript, which is what makes "return to home" over a conversation work.
|
|
187
|
+
host.setAttribute(WELCOME_OVERLAY_ATTR, "");
|
|
188
|
+
if (!body.classList.contains(WELCOME_OVERLAY_BODY_CLASS)) {
|
|
189
|
+
overlayScrollTop.set(body, body.scrollTop);
|
|
190
|
+
body.classList.add(WELCOME_OVERLAY_BODY_CLASS);
|
|
191
|
+
body.scrollTop = 0;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** Remove plugin content and hand the host back to the default renderer. */
|
|
196
|
+
export const clearWelcomePluginContent = (
|
|
197
|
+
body: HTMLElement,
|
|
198
|
+
host: HTMLElement,
|
|
199
|
+
element: HTMLElement | null
|
|
200
|
+
): void => {
|
|
201
|
+
if (element && element !== host) element.remove();
|
|
202
|
+
host.removeAttribute("data-persona-welcome-content");
|
|
203
|
+
host.removeAttribute(WELCOME_OVERLAY_ATTR);
|
|
204
|
+
if (body.classList.contains(WELCOME_OVERLAY_BODY_CLASS)) {
|
|
205
|
+
body.classList.remove(WELCOME_OVERLAY_BODY_CLASS);
|
|
206
|
+
body.scrollTop = overlayScrollTop.get(body) ?? body.scrollTop;
|
|
207
|
+
overlayScrollTop.delete(body);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Greeting host: a sibling above `messagesWrapper`, so it is pinned at
|
|
213
|
+
* transcript position zero without ever entering the morph target.
|
|
214
|
+
*/
|
|
215
|
+
export const buildGreetingHost = (): HTMLElement => {
|
|
216
|
+
const host = createNode("div", {
|
|
217
|
+
className: "persona-welcome-greeting persona-flex persona-flex-col",
|
|
218
|
+
attrs: { "data-persona-welcome-greeting": "" },
|
|
219
|
+
});
|
|
220
|
+
host.hidden = true;
|
|
221
|
+
return host;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Render the display-only greeting bubble. Plain text by contract: no
|
|
226
|
+
* markdown pipeline, no action affordances, no message id, never a session
|
|
227
|
+
* message.
|
|
228
|
+
*/
|
|
229
|
+
export const renderWelcomeGreeting = (
|
|
230
|
+
host: HTMLElement,
|
|
231
|
+
text: string | undefined,
|
|
232
|
+
config: AgentWidgetConfig | undefined
|
|
233
|
+
): void => {
|
|
234
|
+
host.replaceChildren();
|
|
235
|
+
if (!text) {
|
|
236
|
+
host.hidden = true;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const layout = config?.layout?.messages?.layout ?? "bubble";
|
|
241
|
+
const bubble = createNode("div", {
|
|
242
|
+
className: getBubbleClasses("assistant", layout).join(" "),
|
|
243
|
+
attrs: { "data-persona-theme-zone": "assistant-message" },
|
|
244
|
+
text,
|
|
245
|
+
style:
|
|
246
|
+
layout === "flat"
|
|
247
|
+
? {
|
|
248
|
+
backgroundColor: "transparent",
|
|
249
|
+
color: "var(--persona-message-assistant-text, var(--persona-text))",
|
|
250
|
+
}
|
|
251
|
+
: {
|
|
252
|
+
backgroundColor:
|
|
253
|
+
"var(--persona-message-assistant-bg, var(--persona-container))",
|
|
254
|
+
color: "var(--persona-message-assistant-text, var(--persona-text))",
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
host.appendChild(createNode("div", { className: "persona-flex" }, bubble));
|
|
259
|
+
host.hidden = false;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const prefersReducedMotion = (): boolean =>
|
|
263
|
+
typeof window !== "undefined" &&
|
|
264
|
+
typeof window.matchMedia === "function" &&
|
|
265
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Hero dismiss. WAAPI, not a CSS transition: morph re-renders strip
|
|
269
|
+
* post-render CSS state and cancel transitions mid-stream. Shares the
|
|
270
|
+
* starters' entrance motion spec so the two read as one system.
|
|
271
|
+
*
|
|
272
|
+
* Returns the animation so the caller can hold the host visible while it runs
|
|
273
|
+
* and cancel it on a re-show. `onFinished` receives it, or null when the motion
|
|
274
|
+
* was skipped and the callback ran synchronously.
|
|
275
|
+
*/
|
|
276
|
+
export const animateWelcomeOut = (
|
|
277
|
+
host: HTMLElement,
|
|
278
|
+
onFinished: (animation: Animation | null) => void
|
|
279
|
+
): Animation | null => {
|
|
280
|
+
if (
|
|
281
|
+
prefersReducedMotion() ||
|
|
282
|
+
typeof host.animate !== "function" ||
|
|
283
|
+
!host.isConnected
|
|
284
|
+
) {
|
|
285
|
+
onFinished(null);
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
// `forwards`, not `backwards`: backwards only fills the delay phase, so the
|
|
289
|
+
// element snaps back to opacity 1 for a frame after the keyframes end.
|
|
290
|
+
const animation = host.animate(
|
|
291
|
+
[
|
|
292
|
+
{ opacity: "1", transform: "none" },
|
|
293
|
+
{ opacity: "0", transform: "translateY(-4px)" },
|
|
294
|
+
],
|
|
295
|
+
{ duration: 180, easing: "ease-out", fill: "forwards" }
|
|
296
|
+
);
|
|
297
|
+
let settled = false;
|
|
298
|
+
const settle = () => {
|
|
299
|
+
if (settled) return;
|
|
300
|
+
settled = true;
|
|
301
|
+
onFinished(animation);
|
|
302
|
+
};
|
|
303
|
+
animation.finished.then(settle).catch(settle);
|
|
304
|
+
return animation;
|
|
305
|
+
};
|
|
@@ -129,15 +129,17 @@ describe("createWidgetView: replacement helpers", () => {
|
|
|
129
129
|
describe("resolveLauncher", () => {
|
|
130
130
|
const config: AgentWidgetConfig = { apiUrl: "/api" };
|
|
131
131
|
|
|
132
|
-
it("builds the default launcher
|
|
132
|
+
it("builds the default launcher surface and wires onToggle", () => {
|
|
133
133
|
const onToggle = vi.fn();
|
|
134
134
|
const { instance, element } = resolveLauncher({ config, plugins: [], onToggle });
|
|
135
135
|
|
|
136
136
|
expect(instance).not.toBeNull();
|
|
137
137
|
expect(element).toBe(instance?.element);
|
|
138
|
-
|
|
138
|
+
// The mounted element is the surface wrapper; the button is inside it.
|
|
139
|
+
expect(element.tagName).toBe("DIV");
|
|
140
|
+
expect(instance?.launcher.element.tagName).toBe("BUTTON");
|
|
139
141
|
|
|
140
|
-
element.dispatchEvent(new Event("click"));
|
|
142
|
+
instance!.launcher.element.dispatchEvent(new Event("click"));
|
|
141
143
|
expect(onToggle).toHaveBeenCalledTimes(1);
|
|
142
144
|
});
|
|
143
145
|
|
|
@@ -2,7 +2,7 @@ import { AgentWidgetConfig } from "../types";
|
|
|
2
2
|
import { AgentWidgetPlugin } from "../plugins/types";
|
|
3
3
|
import { createWrapper, buildPanel, PanelElements } from "./panel";
|
|
4
4
|
import { HeaderElements } from "./header-builder";
|
|
5
|
-
import { createLauncherButton,
|
|
5
|
+
import { createLauncherButton, createLauncherSurface, LauncherSurface } from "./launcher";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Widget view assembly layer.
|
|
@@ -30,8 +30,13 @@ export interface WidgetTranscriptRefs {
|
|
|
30
30
|
container: HTMLElement;
|
|
31
31
|
body: HTMLElement;
|
|
32
32
|
messagesWrapper: HTMLElement;
|
|
33
|
+
starterSuggestions: HTMLElement;
|
|
34
|
+
transcriptSuggestions: HTMLElement;
|
|
33
35
|
/** Absolute slot above the composer where interactive sheets mount. */
|
|
34
36
|
composerOverlay: HTMLElement;
|
|
37
|
+
welcomeHost: HTMLElement;
|
|
38
|
+
welcomeIconHolder: HTMLElement;
|
|
39
|
+
greetingHost: HTMLElement;
|
|
35
40
|
introTitle: HTMLElement;
|
|
36
41
|
introSubtitle: HTMLElement;
|
|
37
42
|
}
|
|
@@ -119,7 +124,12 @@ export const createWidgetView = ({
|
|
|
119
124
|
container: panelElements.container,
|
|
120
125
|
body: panelElements.body,
|
|
121
126
|
messagesWrapper: panelElements.messagesWrapper,
|
|
127
|
+
starterSuggestions: panelElements.starterSuggestions,
|
|
128
|
+
transcriptSuggestions: panelElements.transcriptSuggestions,
|
|
122
129
|
composerOverlay: panelElements.composerOverlay,
|
|
130
|
+
welcomeHost: panelElements.welcomeHost,
|
|
131
|
+
welcomeIconHolder: panelElements.welcomeIconHolder,
|
|
132
|
+
greetingHost: panelElements.greetingHost,
|
|
123
133
|
introTitle: panelElements.introTitle,
|
|
124
134
|
introSubtitle: panelElements.introSubtitle,
|
|
125
135
|
};
|
|
@@ -195,11 +205,11 @@ export interface ResolveLauncherOptions {
|
|
|
195
205
|
|
|
196
206
|
export interface ResolvedLauncher {
|
|
197
207
|
/**
|
|
198
|
-
* The default launcher
|
|
208
|
+
* The default launcher surface (with `update`/`destroy`), or null when a
|
|
199
209
|
* plugin supplied a custom launcher element.
|
|
200
210
|
*/
|
|
201
|
-
instance:
|
|
202
|
-
/** The element to mount: either the default
|
|
211
|
+
instance: LauncherSurface | null;
|
|
212
|
+
/** The element to mount: either the default surface or the plugin's element. */
|
|
203
213
|
element: HTMLElement;
|
|
204
214
|
}
|
|
205
215
|
|
|
@@ -209,8 +219,9 @@ export interface ResolvedLauncher {
|
|
|
209
219
|
* both the initial-build and re-enable call sites in ui.ts.
|
|
210
220
|
*
|
|
211
221
|
* When a plugin returns a custom element, `instance` is null (the plugin owns
|
|
212
|
-
* its own updates); otherwise the default
|
|
213
|
-
* returned so the caller can drive
|
|
222
|
+
* its own updates, including any teaser); otherwise the default
|
|
223
|
+
* `LauncherSurface` controller is returned so the caller can drive
|
|
224
|
+
* `update()`/`destroy()`.
|
|
214
225
|
*/
|
|
215
226
|
export const resolveLauncher = ({
|
|
216
227
|
config,
|
|
@@ -229,6 +240,6 @@ export const resolveLauncher = ({
|
|
|
229
240
|
}
|
|
230
241
|
}
|
|
231
242
|
|
|
232
|
-
const instance =
|
|
243
|
+
const instance = createLauncherSurface(config, onToggle);
|
|
233
244
|
return { instance, element: instance.element };
|
|
234
245
|
};
|
package/src/defaults.ts
CHANGED
|
@@ -79,8 +79,8 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
|
|
|
79
79
|
colorScheme: "light",
|
|
80
80
|
launcher: DEFAULT_LAUNCHER_CONFIG,
|
|
81
81
|
copy: {
|
|
82
|
-
welcomeTitle:
|
|
83
|
-
|
|
82
|
+
// No welcomeTitle / welcomeSubtitle here on purpose: `resolveWelcomeConfig`
|
|
83
|
+
// owns those defaults so presence still means "the host set this".
|
|
84
84
|
inputPlaceholder: "How can I help...",
|
|
85
85
|
sendButtonLabel: "Send",
|
|
86
86
|
},
|
|
@@ -169,10 +169,12 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
|
|
|
169
169
|
submitLabel: "Send",
|
|
170
170
|
},
|
|
171
171
|
},
|
|
172
|
+
// Placeholder copy that models the right shape: verb-first, user-voice,
|
|
173
|
+
// one line. Integrators should swap in domain prompts.
|
|
172
174
|
suggestionChips: [
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
175
|
+
"Show me what you can help with",
|
|
176
|
+
"Walk me through getting started",
|
|
177
|
+
"Answer a question about this page",
|
|
176
178
|
],
|
|
177
179
|
suggestionChipsConfig: {
|
|
178
180
|
fontFamily: "sans-serif",
|
|
@@ -260,7 +260,24 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
260
260
|
type: "source";
|
|
261
261
|
url?: string;
|
|
262
262
|
[key: string]: unknown;
|
|
263
|
+
} | {
|
|
264
|
+
executionId: string;
|
|
265
|
+
iteration?: number;
|
|
266
|
+
seq: number;
|
|
267
|
+
state: Record<string, unknown>;
|
|
268
|
+
type: "state_snapshot";
|
|
263
269
|
} | ({
|
|
270
|
+
executionId: string;
|
|
271
|
+
iteration?: number;
|
|
272
|
+
patch: Array<{
|
|
273
|
+
from?: string;
|
|
274
|
+
op: "add" | "remove" | "replace" | "move" | "copy" | "test";
|
|
275
|
+
path: string;
|
|
276
|
+
value?: unknown;
|
|
277
|
+
}>;
|
|
278
|
+
seq: number;
|
|
279
|
+
type: "state_delta";
|
|
280
|
+
}) | ({
|
|
264
281
|
executionId: string;
|
|
265
282
|
hiddenParameterNames?: Array<string>;
|
|
266
283
|
iteration?: number;
|
package/src/index-core.ts
CHANGED
|
@@ -31,6 +31,7 @@ export type {
|
|
|
31
31
|
AgentWidgetInitOptions,
|
|
32
32
|
AgentWidgetMessage,
|
|
33
33
|
AgentWidgetLauncherConfig,
|
|
34
|
+
AgentWidgetLauncherTeaserConfig,
|
|
34
35
|
AgentWidgetDockConfig,
|
|
35
36
|
AgentWidgetEvent,
|
|
36
37
|
AgentWidgetStreamParser,
|
|
@@ -140,7 +141,22 @@ export type {
|
|
|
140
141
|
AskUserQuestionPrompt,
|
|
141
142
|
AskUserQuestionOption,
|
|
142
143
|
AgentWidgetAskUserQuestionFeature,
|
|
143
|
-
AgentWidgetAskUserQuestionStyles
|
|
144
|
+
AgentWidgetAskUserQuestionStyles,
|
|
145
|
+
AgentWidgetSuggestion,
|
|
146
|
+
AgentWidgetSuggestionChipsConfig,
|
|
147
|
+
AgentWidgetResolvedSuggestion,
|
|
148
|
+
AgentWidgetSuggestionBehavior,
|
|
149
|
+
AgentWidgetSuggestionVariant,
|
|
150
|
+
AgentWidgetSuggestionSurface,
|
|
151
|
+
AgentWidgetSuggestionSource,
|
|
152
|
+
AgentWidgetSuggestionSurfaceConfig,
|
|
153
|
+
AgentWidgetStarterSuggestionsConfig,
|
|
154
|
+
AgentWidgetFollowUpSuggestionsConfig,
|
|
155
|
+
AgentWidgetSuggestionsConfig,
|
|
156
|
+
AgentWidgetWelcomeConfig,
|
|
157
|
+
AgentWidgetWelcomeIcon,
|
|
158
|
+
AgentWidgetWelcomeVariant,
|
|
159
|
+
AgentWidgetWelcomeDismiss
|
|
144
160
|
} from "./types";
|
|
145
161
|
|
|
146
162
|
export {
|
|
@@ -326,7 +342,17 @@ export { VERSION } from "./version";
|
|
|
326
342
|
export type { AgentWidgetInitHandle };
|
|
327
343
|
|
|
328
344
|
// Plugin system exports
|
|
329
|
-
export type {
|
|
345
|
+
export type {
|
|
346
|
+
AgentWidgetPlugin,
|
|
347
|
+
AgentWidgetTransformSuggestionsContext,
|
|
348
|
+
AgentWidgetRenderSuggestionContext,
|
|
349
|
+
AgentWidgetRenderWelcomeContext,
|
|
350
|
+
AgentWidgetSuggestionSelectContext
|
|
351
|
+
} from "./plugins/types";
|
|
352
|
+
// Welcome resolver: `renderWelcome` receives the resolved shape.
|
|
353
|
+
export { resolveWelcomeConfig, isWelcomeVisible } from "./welcome";
|
|
354
|
+
export type { ResolvedWelcomeConfig } from "./welcome";
|
|
355
|
+
export type { AgentWidgetPluginStorage } from "./utils/plugin-storage";
|
|
330
356
|
export { pluginRegistry } from "./plugins/registry";
|
|
331
357
|
|
|
332
358
|
// Stream animation plugin API: lets consumers register custom animations
|
|
@@ -431,6 +457,7 @@ export type {
|
|
|
431
457
|
SemanticSpacing,
|
|
432
458
|
SemanticTypography,
|
|
433
459
|
ComponentTokens,
|
|
460
|
+
TextStyleTokens,
|
|
434
461
|
ArtifactToolbarTokens,
|
|
435
462
|
ArtifactTabTokens,
|
|
436
463
|
ArtifactPaneTokens,
|
package/src/launcher-global.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Critical-path launcher entry: built to `launcher.global.js` (IIFE).
|
|
3
3
|
*
|
|
4
|
-
* Ships ONLY the real collapsed launcher (`
|
|
4
|
+
* Ships ONLY the real collapsed launcher surface (`createLauncherSurface`,
|
|
5
|
+
* which carries the optional launcher teaser) plus the
|
|
5
6
|
* theme application path, so the launcher paints pixel-identically to the full
|
|
6
7
|
* widget from a tiny bundle. The heavy conversation panel is deferred until
|
|
7
8
|
* first open by the installer (Phase 2).
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
* window.AgentWidgetLauncher.mount({ target, config, onOpen })
|
|
17
18
|
* → { root, element, update, destroy }
|
|
18
19
|
*/
|
|
19
|
-
import {
|
|
20
|
+
import { createLauncherSurface } from "./components/launcher";
|
|
20
21
|
import { applyThemeVariables } from "./utils/theme";
|
|
21
22
|
import { DEFAULT_LAUNCHER_CONFIG } from "./defaults";
|
|
22
23
|
import type { AgentWidgetConfig } from "./types";
|
|
@@ -76,7 +77,7 @@ const mergeCriticalLauncherConfig = (
|
|
|
76
77
|
* Mirrors the full widget's DOM exactly (`runtime/init.ts` + `ui.ts`): a
|
|
77
78
|
* `[data-persona-root]` wrapper carries the theme CSS variables and the launcher
|
|
78
79
|
* button is its child. Keeping the theme vars on the wrapper (not the button)
|
|
79
|
-
* leaves the button's own inline style
|
|
80
|
+
* leaves the button's own inline style matching the full widget's, so
|
|
80
81
|
* the eventual mount-then-remove handoff is invisible.
|
|
81
82
|
*/
|
|
82
83
|
export const mount = (
|
|
@@ -100,20 +101,22 @@ export const mount = (
|
|
|
100
101
|
root.setAttribute(CRITICAL_LAUNCHER_ATTR, "true");
|
|
101
102
|
applyThemeVariables(root, config);
|
|
102
103
|
|
|
103
|
-
const
|
|
104
|
-
root.appendChild(
|
|
104
|
+
const surface = createLauncherSurface(config, onOpen);
|
|
105
|
+
root.appendChild(surface.element);
|
|
105
106
|
target.appendChild(root);
|
|
106
107
|
|
|
107
108
|
return {
|
|
108
109
|
root,
|
|
109
|
-
element: launcher.element,
|
|
110
|
+
element: surface.launcher.element,
|
|
110
111
|
update: (next: AgentWidgetConfig) => {
|
|
111
112
|
const merged = mergeCriticalLauncherConfig(next);
|
|
112
113
|
applyThemeVariables(root, merged);
|
|
113
|
-
|
|
114
|
+
surface.update(merged);
|
|
114
115
|
},
|
|
116
|
+
// Handoff destroys the whole surface, so a pending teaser timer cannot
|
|
117
|
+
// outlive the critical launcher.
|
|
115
118
|
destroy: () => {
|
|
116
|
-
|
|
119
|
+
surface.destroy();
|
|
117
120
|
root.remove();
|
|
118
121
|
},
|
|
119
122
|
};
|