@runtypelabs/persona 3.17.0 → 3.19.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 +143 -1
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-HPZY7oAI.d.cts → types-cwY5HaFD.d.cts} +25 -0
- package/dist/animations/{types-HPZY7oAI.d.ts → types-cwY5HaFD.d.ts} +25 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/index.cjs +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +580 -4
- package/dist/index.d.ts +580 -4
- package/dist/index.global.js +102 -1636
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +45 -45
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +2844 -752
- package/dist/theme-editor.d.cts +337 -1
- package/dist/theme-editor.d.ts +337 -1
- package/dist/theme-editor.js +2958 -752
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +14 -0
- package/dist/theme-reference.d.ts +14 -0
- package/dist/widget.css +780 -0
- package/package.json +1 -1
- package/src/client.test.ts +134 -0
- package/src/client.ts +71 -0
- package/src/components/ask-user-question-bubble.test.ts +583 -0
- package/src/components/ask-user-question-bubble.ts +924 -0
- package/src/components/composer-builder.test.ts +52 -0
- package/src/components/composer-builder.ts +67 -490
- package/src/components/composer-parts.test.ts +152 -0
- package/src/components/composer-parts.ts +452 -0
- package/src/components/header-builder.ts +22 -299
- package/src/components/header-parts.ts +360 -0
- package/src/components/messages.ts +33 -1
- package/src/components/panel.test.ts +61 -0
- package/src/components/panel.ts +303 -9
- package/src/components/pill-composer-builder.test.ts +85 -0
- package/src/components/pill-composer-builder.ts +183 -0
- package/src/defaults.ts +21 -0
- package/src/index.ts +20 -1
- package/src/plugins/types.ts +57 -0
- package/src/runtime/init.ts +4 -2
- package/src/runtime/persist-state.test.ts +152 -0
- package/src/session.test.ts +183 -0
- package/src/session.ts +242 -3
- package/src/styles/widget.css +780 -0
- package/src/types/theme.ts +15 -0
- package/src/types.ts +271 -1
- package/src/ui.ask-user-question-plugin.test.ts +649 -0
- package/src/ui.component-directive.test.ts +183 -0
- package/src/ui.composer-bar.test.ts +1009 -0
- package/src/ui.ts +1439 -76
- package/src/utils/attachment-manager.ts +1 -1
- package/src/utils/dock.test.ts +45 -0
- package/src/utils/dock.ts +3 -0
- package/src/utils/icons.ts +314 -58
- package/src/utils/storage.ts +10 -2
- package/src/utils/stream-animation.ts +7 -2
- package/src/utils/theme.test.ts +36 -0
- package/src/utils/tokens.ts +23 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { createElement, createElementInDocument } from "../utils/dom";
|
|
2
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
3
|
+
import { AgentWidgetConfig } from "../types";
|
|
4
|
+
import { PORTALED_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
5
|
+
import { HEADER_THEME_CSS } from "./header-builder";
|
|
6
|
+
|
|
7
|
+
export interface CloseButtonParts {
|
|
8
|
+
button: HTMLButtonElement;
|
|
9
|
+
wrapper: HTMLElement;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ClearChatButtonParts {
|
|
13
|
+
button: HTMLButtonElement;
|
|
14
|
+
wrapper: HTMLElement;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CreateCloseButtonOptions {
|
|
18
|
+
showClose?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Override the wrapper className. The full header passes its own
|
|
21
|
+
* placement-aware class string; composer-bar mode passes a class that
|
|
22
|
+
* positions the wrapper absolutely in the top-right of the panel chrome.
|
|
23
|
+
*/
|
|
24
|
+
wrapperClassName?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Explicit button-size override that wins over `launcher.closeButtonSize`.
|
|
27
|
+
* Use when the call site has its own opinion about the size that should
|
|
28
|
+
* take precedence over the global launcher config — e.g. composer-bar's
|
|
29
|
+
* minimal close icon, where size is part of the mode's UX, not something
|
|
30
|
+
* that should inherit from the floating launcher's button size.
|
|
31
|
+
*/
|
|
32
|
+
buttonSize?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Override the rendered icon size (default: "28px"). Pair with
|
|
35
|
+
* `buttonSize` when scaling the whole control down — otherwise the
|
|
36
|
+
* 28px icon will overflow a smaller button.
|
|
37
|
+
*/
|
|
38
|
+
iconSize?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CreateClearChatButtonOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Override the wrapper className. Header builder passes its own
|
|
44
|
+
* placement-aware class string; composer-bar mode passes a class that
|
|
45
|
+
* positions the wrapper absolutely (next to the close button).
|
|
46
|
+
*/
|
|
47
|
+
wrapperClassName?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Explicit button-size override that wins over `launcher.clearChat.size`.
|
|
50
|
+
* Composer-bar mode uses this so the clear icon visually matches the
|
|
51
|
+
* shrunken close button (16px) and doesn't render at the floating
|
|
52
|
+
* launcher's 32px default.
|
|
53
|
+
*/
|
|
54
|
+
buttonSize?: string;
|
|
55
|
+
/** Override the rendered icon size (default: "20px"). */
|
|
56
|
+
iconSize?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const DEFAULT_WRAPPER_CLASS =
|
|
60
|
+
"persona-relative persona-ml-auto persona-inline-flex persona-items-center persona-justify-center";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the close (×) button + tooltip used in the panel header. Lifted
|
|
64
|
+
* verbatim from header-builder.ts so composer-bar mode can render just a
|
|
65
|
+
* close button (no full header strip) without duplicating the tooltip
|
|
66
|
+
* + config-driven styling logic.
|
|
67
|
+
*/
|
|
68
|
+
export const createCloseButton = (
|
|
69
|
+
config: AgentWidgetConfig | undefined,
|
|
70
|
+
options: CreateCloseButtonOptions = {},
|
|
71
|
+
): CloseButtonParts => {
|
|
72
|
+
const {
|
|
73
|
+
showClose = true,
|
|
74
|
+
wrapperClassName = DEFAULT_WRAPPER_CLASS,
|
|
75
|
+
buttonSize,
|
|
76
|
+
iconSize = "28px",
|
|
77
|
+
} = options;
|
|
78
|
+
const launcher = config?.launcher ?? {};
|
|
79
|
+
// Call-site `buttonSize` (if provided) wins over launcher config. The
|
|
80
|
+
// launcher's `closeButtonSize` is set in DEFAULT_WIDGET_CONFIG so it's
|
|
81
|
+
// never undefined, which means the call-site override is the only way
|
|
82
|
+
// to opt a specific render path (like composer-bar's minimal close) into
|
|
83
|
+
// a different size.
|
|
84
|
+
const closeButtonSize = buttonSize ?? launcher.closeButtonSize ?? "32px";
|
|
85
|
+
|
|
86
|
+
const wrapper = createElement("div", wrapperClassName);
|
|
87
|
+
|
|
88
|
+
const button = createElement(
|
|
89
|
+
"button",
|
|
90
|
+
"persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
|
|
91
|
+
) as HTMLButtonElement;
|
|
92
|
+
button.style.height = closeButtonSize;
|
|
93
|
+
button.style.width = closeButtonSize;
|
|
94
|
+
button.type = "button";
|
|
95
|
+
|
|
96
|
+
const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
|
|
97
|
+
const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
|
|
98
|
+
|
|
99
|
+
button.setAttribute("aria-label", closeButtonTooltipText);
|
|
100
|
+
button.style.display = showClose ? "" : "none";
|
|
101
|
+
|
|
102
|
+
const closeButtonIconName = launcher.closeButtonIconName ?? "x";
|
|
103
|
+
const closeButtonIconText = launcher.closeButtonIconText ?? "×";
|
|
104
|
+
button.style.color =
|
|
105
|
+
launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor;
|
|
106
|
+
|
|
107
|
+
// The X glyph's paths occupy only the middle 50% of its 24x24 viewBox
|
|
108
|
+
// (from 6,6 to 18,18), while other header icons (e.g. refresh-cw) span
|
|
109
|
+
// ~75% of the viewBox. Rendering X at a larger intrinsic size brings
|
|
110
|
+
// its visible extent into parity with sibling icons in the header.
|
|
111
|
+
// display:block eliminates inline-baseline spacing that can push the
|
|
112
|
+
// icon a fractional pixel off-center inside the button.
|
|
113
|
+
const closeIconSvg = renderLucideIcon(closeButtonIconName, iconSize, "currentColor", 1);
|
|
114
|
+
if (closeIconSvg) {
|
|
115
|
+
closeIconSvg.style.display = "block";
|
|
116
|
+
button.appendChild(closeIconSvg);
|
|
117
|
+
} else {
|
|
118
|
+
button.textContent = closeButtonIconText;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (launcher.closeButtonBackgroundColor) {
|
|
122
|
+
button.style.backgroundColor = launcher.closeButtonBackgroundColor;
|
|
123
|
+
button.classList.remove("hover:persona-bg-gray-100");
|
|
124
|
+
} else {
|
|
125
|
+
button.style.backgroundColor = "";
|
|
126
|
+
button.classList.add("hover:persona-bg-gray-100");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (launcher.closeButtonBorderWidth || launcher.closeButtonBorderColor) {
|
|
130
|
+
const borderWidth = launcher.closeButtonBorderWidth || "0px";
|
|
131
|
+
const borderColor = launcher.closeButtonBorderColor || "transparent";
|
|
132
|
+
button.style.border = `${borderWidth} solid ${borderColor}`;
|
|
133
|
+
button.classList.remove("persona-border-none");
|
|
134
|
+
} else {
|
|
135
|
+
button.style.border = "";
|
|
136
|
+
button.classList.add("persona-border-none");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (launcher.closeButtonBorderRadius) {
|
|
140
|
+
button.style.borderRadius = launcher.closeButtonBorderRadius;
|
|
141
|
+
button.classList.remove("persona-rounded-full");
|
|
142
|
+
} else {
|
|
143
|
+
button.style.borderRadius = "";
|
|
144
|
+
button.classList.add("persona-rounded-full");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (launcher.closeButtonPaddingX) {
|
|
148
|
+
button.style.paddingLeft = launcher.closeButtonPaddingX;
|
|
149
|
+
button.style.paddingRight = launcher.closeButtonPaddingX;
|
|
150
|
+
} else {
|
|
151
|
+
button.style.paddingLeft = "";
|
|
152
|
+
button.style.paddingRight = "";
|
|
153
|
+
}
|
|
154
|
+
if (launcher.closeButtonPaddingY) {
|
|
155
|
+
button.style.paddingTop = launcher.closeButtonPaddingY;
|
|
156
|
+
button.style.paddingBottom = launcher.closeButtonPaddingY;
|
|
157
|
+
} else {
|
|
158
|
+
button.style.paddingTop = "";
|
|
159
|
+
button.style.paddingBottom = "";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
wrapper.appendChild(button);
|
|
163
|
+
|
|
164
|
+
if (closeButtonShowTooltip && closeButtonTooltipText) {
|
|
165
|
+
let portaledTooltip: HTMLElement | null = null;
|
|
166
|
+
|
|
167
|
+
const showTooltip = () => {
|
|
168
|
+
if (portaledTooltip) return;
|
|
169
|
+
|
|
170
|
+
const tooltipDocument = button.ownerDocument;
|
|
171
|
+
const tooltipContainer = tooltipDocument.body;
|
|
172
|
+
if (!tooltipContainer) return;
|
|
173
|
+
|
|
174
|
+
portaledTooltip = createElementInDocument(
|
|
175
|
+
tooltipDocument,
|
|
176
|
+
"div",
|
|
177
|
+
"persona-clear-chat-tooltip"
|
|
178
|
+
);
|
|
179
|
+
portaledTooltip.textContent = closeButtonTooltipText;
|
|
180
|
+
|
|
181
|
+
const arrow = createElementInDocument(tooltipDocument, "div");
|
|
182
|
+
arrow.className = "persona-clear-chat-tooltip-arrow";
|
|
183
|
+
portaledTooltip.appendChild(arrow);
|
|
184
|
+
|
|
185
|
+
const buttonRect = button.getBoundingClientRect();
|
|
186
|
+
|
|
187
|
+
portaledTooltip.style.position = "fixed";
|
|
188
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
189
|
+
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
190
|
+
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
191
|
+
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
192
|
+
|
|
193
|
+
tooltipContainer.appendChild(portaledTooltip);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const hideTooltip = () => {
|
|
197
|
+
if (portaledTooltip && portaledTooltip.parentNode) {
|
|
198
|
+
portaledTooltip.parentNode.removeChild(portaledTooltip);
|
|
199
|
+
portaledTooltip = null;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
wrapper.addEventListener("mouseenter", showTooltip);
|
|
204
|
+
wrapper.addEventListener("mouseleave", hideTooltip);
|
|
205
|
+
button.addEventListener("focus", showTooltip);
|
|
206
|
+
button.addEventListener("blur", hideTooltip);
|
|
207
|
+
|
|
208
|
+
(wrapper as any)._cleanupTooltip = () => {
|
|
209
|
+
hideTooltip();
|
|
210
|
+
wrapper.removeEventListener("mouseenter", showTooltip);
|
|
211
|
+
wrapper.removeEventListener("mouseleave", hideTooltip);
|
|
212
|
+
button.removeEventListener("focus", showTooltip);
|
|
213
|
+
button.removeEventListener("blur", hideTooltip);
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { button, wrapper };
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const DEFAULT_CLEAR_CHAT_WRAPPER_CLASS =
|
|
221
|
+
"persona-relative persona-ml-auto persona-clear-chat-button-wrapper";
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build the clear-chat (refresh) button + tooltip used in the panel header.
|
|
225
|
+
* Extracted from `header-builder.ts` so composer-bar mode can render a
|
|
226
|
+
* "start over" button alongside its close icon without duplicating the
|
|
227
|
+
* tooltip + config-driven styling logic.
|
|
228
|
+
*
|
|
229
|
+
* The factory only handles construction. Wiring the click to the
|
|
230
|
+
* clear-history handler is owned by `setupClearChatButton()` in `ui.ts`,
|
|
231
|
+
* which keys off `panelElements.clearChatButton`.
|
|
232
|
+
*/
|
|
233
|
+
export const createClearChatButton = (
|
|
234
|
+
config: AgentWidgetConfig | undefined,
|
|
235
|
+
options: CreateClearChatButtonOptions = {},
|
|
236
|
+
): ClearChatButtonParts => {
|
|
237
|
+
const {
|
|
238
|
+
wrapperClassName = DEFAULT_CLEAR_CHAT_WRAPPER_CLASS,
|
|
239
|
+
buttonSize,
|
|
240
|
+
iconSize = "20px",
|
|
241
|
+
} = options;
|
|
242
|
+
|
|
243
|
+
const launcher = config?.launcher ?? {};
|
|
244
|
+
const clearChatConfig = launcher.clearChat ?? {};
|
|
245
|
+
// Call-site `buttonSize` (when provided) wins over launcher.clearChat.size.
|
|
246
|
+
// Same precedence rule as createCloseButton: callers like composer-bar
|
|
247
|
+
// intentionally override the inherited launcher default to fit their UX.
|
|
248
|
+
const clearChatSize = buttonSize ?? clearChatConfig.size ?? "32px";
|
|
249
|
+
const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
|
|
250
|
+
const clearChatIconColor = clearChatConfig.iconColor ?? "";
|
|
251
|
+
const clearChatBgColor = clearChatConfig.backgroundColor ?? "";
|
|
252
|
+
const clearChatBorderWidth = clearChatConfig.borderWidth ?? "";
|
|
253
|
+
const clearChatBorderColor = clearChatConfig.borderColor ?? "";
|
|
254
|
+
const clearChatBorderRadius = clearChatConfig.borderRadius ?? "";
|
|
255
|
+
const clearChatPaddingX = clearChatConfig.paddingX ?? "";
|
|
256
|
+
const clearChatPaddingY = clearChatConfig.paddingY ?? "";
|
|
257
|
+
const clearChatTooltipText = clearChatConfig.tooltipText ?? "Clear chat";
|
|
258
|
+
const clearChatShowTooltip = clearChatConfig.showTooltip ?? true;
|
|
259
|
+
|
|
260
|
+
const wrapper = createElement("div", wrapperClassName);
|
|
261
|
+
|
|
262
|
+
const button = createElement(
|
|
263
|
+
"button",
|
|
264
|
+
"persona-inline-flex persona-items-center persona-justify-center persona-rounded-full hover:persona-bg-gray-100 persona-cursor-pointer persona-border-none"
|
|
265
|
+
) as HTMLButtonElement;
|
|
266
|
+
button.style.height = clearChatSize;
|
|
267
|
+
button.style.width = clearChatSize;
|
|
268
|
+
button.type = "button";
|
|
269
|
+
button.setAttribute("aria-label", clearChatTooltipText);
|
|
270
|
+
button.style.color = clearChatIconColor || HEADER_THEME_CSS.actionIconColor;
|
|
271
|
+
|
|
272
|
+
const iconSvg = renderLucideIcon(clearChatIconName, iconSize, "currentColor", 1);
|
|
273
|
+
if (iconSvg) {
|
|
274
|
+
iconSvg.style.display = "block";
|
|
275
|
+
button.appendChild(iconSvg);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (clearChatBgColor) {
|
|
279
|
+
button.style.backgroundColor = clearChatBgColor;
|
|
280
|
+
button.classList.remove("hover:persona-bg-gray-100");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (clearChatBorderWidth || clearChatBorderColor) {
|
|
284
|
+
const borderWidth = clearChatBorderWidth || "0px";
|
|
285
|
+
const borderColor = clearChatBorderColor || "transparent";
|
|
286
|
+
button.style.border = `${borderWidth} solid ${borderColor}`;
|
|
287
|
+
button.classList.remove("persona-border-none");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (clearChatBorderRadius) {
|
|
291
|
+
button.style.borderRadius = clearChatBorderRadius;
|
|
292
|
+
button.classList.remove("persona-rounded-full");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (clearChatPaddingX) {
|
|
296
|
+
button.style.paddingLeft = clearChatPaddingX;
|
|
297
|
+
button.style.paddingRight = clearChatPaddingX;
|
|
298
|
+
}
|
|
299
|
+
if (clearChatPaddingY) {
|
|
300
|
+
button.style.paddingTop = clearChatPaddingY;
|
|
301
|
+
button.style.paddingBottom = clearChatPaddingY;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
wrapper.appendChild(button);
|
|
305
|
+
|
|
306
|
+
if (clearChatShowTooltip && clearChatTooltipText) {
|
|
307
|
+
let portaledTooltip: HTMLElement | null = null;
|
|
308
|
+
|
|
309
|
+
const showTooltip = () => {
|
|
310
|
+
if (portaledTooltip) return;
|
|
311
|
+
|
|
312
|
+
const tooltipDocument = button.ownerDocument;
|
|
313
|
+
const tooltipContainer = tooltipDocument.body;
|
|
314
|
+
if (!tooltipContainer) return;
|
|
315
|
+
|
|
316
|
+
portaledTooltip = createElementInDocument(
|
|
317
|
+
tooltipDocument,
|
|
318
|
+
"div",
|
|
319
|
+
"persona-clear-chat-tooltip"
|
|
320
|
+
);
|
|
321
|
+
portaledTooltip.textContent = clearChatTooltipText;
|
|
322
|
+
|
|
323
|
+
const arrow = createElementInDocument(tooltipDocument, "div");
|
|
324
|
+
arrow.className = "persona-clear-chat-tooltip-arrow";
|
|
325
|
+
portaledTooltip.appendChild(arrow);
|
|
326
|
+
|
|
327
|
+
const buttonRect = button.getBoundingClientRect();
|
|
328
|
+
|
|
329
|
+
portaledTooltip.style.position = "fixed";
|
|
330
|
+
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
331
|
+
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
332
|
+
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
333
|
+
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
334
|
+
|
|
335
|
+
tooltipContainer.appendChild(portaledTooltip);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const hideTooltip = () => {
|
|
339
|
+
if (portaledTooltip && portaledTooltip.parentNode) {
|
|
340
|
+
portaledTooltip.parentNode.removeChild(portaledTooltip);
|
|
341
|
+
portaledTooltip = null;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
wrapper.addEventListener("mouseenter", showTooltip);
|
|
346
|
+
wrapper.addEventListener("mouseleave", hideTooltip);
|
|
347
|
+
button.addEventListener("focus", showTooltip);
|
|
348
|
+
button.addEventListener("blur", hideTooltip);
|
|
349
|
+
|
|
350
|
+
(wrapper as any)._cleanupTooltip = () => {
|
|
351
|
+
hideTooltip();
|
|
352
|
+
wrapper.removeEventListener("mouseenter", showTooltip);
|
|
353
|
+
wrapper.removeEventListener("mouseleave", hideTooltip);
|
|
354
|
+
button.removeEventListener("focus", showTooltip);
|
|
355
|
+
button.removeEventListener("blur", hideTooltip);
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return { button, wrapper };
|
|
360
|
+
};
|
|
@@ -4,6 +4,11 @@ import { MessageTransform, MessageActionCallbacks } from "./message-bubble";
|
|
|
4
4
|
import { createStandardBubble } from "./message-bubble";
|
|
5
5
|
import { createReasoningBubble } from "./reasoning-bubble";
|
|
6
6
|
import { createToolBubble } from "./tool-bubble";
|
|
7
|
+
import {
|
|
8
|
+
ensureAskUserQuestionSheet,
|
|
9
|
+
isAskUserQuestionMessage,
|
|
10
|
+
removeAskUserQuestionSheet,
|
|
11
|
+
} from "./ask-user-question-bubble";
|
|
7
12
|
|
|
8
13
|
export const renderMessages = (
|
|
9
14
|
container: HTMLElement,
|
|
@@ -12,16 +17,29 @@ export const renderMessages = (
|
|
|
12
17
|
showReasoning: boolean,
|
|
13
18
|
showToolCalls: boolean,
|
|
14
19
|
config?: AgentWidgetConfig,
|
|
15
|
-
actionCallbacks?: MessageActionCallbacks
|
|
20
|
+
actionCallbacks?: MessageActionCallbacks,
|
|
21
|
+
composerOverlay?: HTMLElement | null
|
|
16
22
|
) => {
|
|
17
23
|
container.innerHTML = "";
|
|
18
24
|
const fragment = createFragment();
|
|
19
25
|
|
|
26
|
+
// Track which ask_user_question tool-call ids are currently in the message
|
|
27
|
+
// list, so we can prune stale sheets from the overlay afterward.
|
|
28
|
+
const liveAskToolIds = new Set<string>();
|
|
29
|
+
|
|
20
30
|
messages.forEach((message) => {
|
|
21
31
|
let bubble: HTMLElement;
|
|
22
32
|
if (message.variant === "reasoning" && message.reasoning) {
|
|
23
33
|
if (!showReasoning) return;
|
|
24
34
|
bubble = createReasoningBubble(message, config);
|
|
35
|
+
} else if (isAskUserQuestionMessage(message)) {
|
|
36
|
+
// No transcript bubble — the overlay sheet is the only question UI.
|
|
37
|
+
if (config?.features?.askUserQuestion?.enabled === false) return;
|
|
38
|
+
if (!message.agentMetadata?.askUserQuestionAnswered) {
|
|
39
|
+
if (message.toolCall?.id) liveAskToolIds.add(message.toolCall.id);
|
|
40
|
+
ensureAskUserQuestionSheet(message, config, composerOverlay ?? null);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
25
43
|
} else if (message.variant === "tool" && message.toolCall) {
|
|
26
44
|
if (!showToolCalls) return;
|
|
27
45
|
bubble = createToolBubble(message, config);
|
|
@@ -45,6 +63,20 @@ export const renderMessages = (
|
|
|
45
63
|
|
|
46
64
|
container.appendChild(fragment);
|
|
47
65
|
container.scrollTop = container.scrollHeight;
|
|
66
|
+
|
|
67
|
+
// Clean up any orphaned ask_user_question sheets whose source message is no
|
|
68
|
+
// longer in the list (e.g. after clearChat or a message splice).
|
|
69
|
+
if (composerOverlay) {
|
|
70
|
+
const sheets = composerOverlay.querySelectorAll<HTMLElement>(
|
|
71
|
+
'[data-persona-ask-sheet-for]'
|
|
72
|
+
);
|
|
73
|
+
sheets.forEach((sheet) => {
|
|
74
|
+
const id = sheet.getAttribute('data-persona-ask-sheet-for');
|
|
75
|
+
if (id && !liveAskToolIds.has(id)) {
|
|
76
|
+
removeAskUserQuestionSheet(composerOverlay, id);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
48
80
|
};
|
|
49
81
|
|
|
50
82
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { createWrapper } from "./panel";
|
|
5
|
+
import type { AgentWidgetConfig } from "../types";
|
|
6
|
+
|
|
7
|
+
describe("createWrapper — composer-bar mode", () => {
|
|
8
|
+
it("marks the wrapper with composer-bar data-attrs and leaves geometry to updateOpenState", () => {
|
|
9
|
+
const config: AgentWidgetConfig = {
|
|
10
|
+
apiUrl: "/api",
|
|
11
|
+
launcher: { mountMode: "composer-bar" },
|
|
12
|
+
};
|
|
13
|
+
const { wrapper, panel } = createWrapper(config);
|
|
14
|
+
|
|
15
|
+
expect(wrapper.getAttribute("data-persona-composer-bar")).toBe("");
|
|
16
|
+
expect(wrapper.dataset.state).toBe("collapsed");
|
|
17
|
+
// Default expandedSize is now "anchored" (was "fullscreen").
|
|
18
|
+
expect(wrapper.dataset.expandedSize).toBe("anchored");
|
|
19
|
+
expect(wrapper.classList.contains("persona-fixed")).toBe(true);
|
|
20
|
+
|
|
21
|
+
// Geometry is owned entirely by applyComposerBarGeometry() in ui.ts so
|
|
22
|
+
// collapsed → expanded transitions can clear stale inline styles.
|
|
23
|
+
// createWrapper must not set any positioning/sizing inline.
|
|
24
|
+
expect(wrapper.style.left).toBe("");
|
|
25
|
+
expect(wrapper.style.transform).toBe("");
|
|
26
|
+
expect(wrapper.style.bottom).toBe("");
|
|
27
|
+
expect(wrapper.style.top).toBe("");
|
|
28
|
+
expect(wrapper.style.width).toBe("");
|
|
29
|
+
expect(wrapper.style.maxWidth).toBe("");
|
|
30
|
+
|
|
31
|
+
// The panel keeps width: 100% so it fills whatever width ui.ts assigns.
|
|
32
|
+
expect(panel.style.width).toBe("100%");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("honors composerBar.expandedSize override", () => {
|
|
36
|
+
const config: AgentWidgetConfig = {
|
|
37
|
+
apiUrl: "/api",
|
|
38
|
+
launcher: {
|
|
39
|
+
mountMode: "composer-bar",
|
|
40
|
+
composerBar: { expandedSize: "modal" },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const { wrapper } = createWrapper(config);
|
|
44
|
+
|
|
45
|
+
expect(wrapper.dataset.expandedSize).toBe("modal");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("applies launcher.zIndex to the composer-bar wrapper", () => {
|
|
49
|
+
const config: AgentWidgetConfig = {
|
|
50
|
+
apiUrl: "/api",
|
|
51
|
+
launcher: { mountMode: "composer-bar", zIndex: 12345 },
|
|
52
|
+
};
|
|
53
|
+
const { wrapper } = createWrapper(config);
|
|
54
|
+
expect(wrapper.style.zIndex).toBe("12345");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("does not apply composer-bar markers in floating mode", () => {
|
|
58
|
+
const { wrapper } = createWrapper({ apiUrl: "/api" });
|
|
59
|
+
expect(wrapper.hasAttribute("data-persona-composer-bar")).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|