@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
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
isAskUserQuestionMessage,
|
|
10
10
|
removeAskUserQuestionSheet,
|
|
11
11
|
} from "./ask-user-question-bubble";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
isSuggestRepliesMessage,
|
|
14
|
+
resolveFollowUpsFeature,
|
|
15
|
+
} from "../suggest-replies-tool";
|
|
13
16
|
|
|
14
17
|
export const renderMessages = (
|
|
15
18
|
container: HTMLElement,
|
|
@@ -43,7 +46,7 @@ export const renderMessages = (
|
|
|
43
46
|
return;
|
|
44
47
|
} else if (
|
|
45
48
|
isSuggestRepliesMessage(message) &&
|
|
46
|
-
config
|
|
49
|
+
resolveFollowUpsFeature(config).enabled
|
|
47
50
|
) {
|
|
48
51
|
// No transcript bubble: the chips above the composer are the only UI.
|
|
49
52
|
// When the feature is disabled the message falls through to the generic
|
package/src/components/panel.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createElement, createNode } from "../utils/dom";
|
|
2
2
|
import { DEFAULT_FLOATING_LAUNCHER_WIDTH } from "../defaults";
|
|
3
|
-
import { AgentWidgetConfig } from "../types";
|
|
3
|
+
import { AgentWidgetConfig, AgentWidgetSuggestionSurface } from "../types";
|
|
4
|
+
import { suggestionSurfaceAttr } from "./suggestions";
|
|
4
5
|
import { positionMap } from "../utils/positioning";
|
|
5
6
|
import { isComposerBarMountMode, isDockedMountMode } from "../utils/dock";
|
|
6
7
|
import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
@@ -9,6 +10,16 @@ import { buildHeaderWithLayout } from "./header-layouts";
|
|
|
9
10
|
import { createCloseButton, createClearChatButton } from "./header-parts";
|
|
10
11
|
import { buildComposer, ComposerElements } from "./composer-builder";
|
|
11
12
|
import { buildPillComposer, buildPillPeekBanner } from "./pill-composer-builder";
|
|
13
|
+
import {
|
|
14
|
+
applyWelcomeConfig,
|
|
15
|
+
applyWelcomeVisibility,
|
|
16
|
+
buildGreetingHost,
|
|
17
|
+
buildWelcomeHost,
|
|
18
|
+
renderWelcomeGreeting,
|
|
19
|
+
WelcomeHostElements,
|
|
20
|
+
} from "./welcome";
|
|
21
|
+
import { isWelcomeVisible, resolveWelcomeConfig } from "../welcome";
|
|
22
|
+
import { resolveContentMaxWidth } from "../utils/content-width";
|
|
12
23
|
|
|
13
24
|
export interface PanelWrapper {
|
|
14
25
|
wrapper: HTMLElement;
|
|
@@ -132,6 +143,10 @@ export interface PanelElements {
|
|
|
132
143
|
container: HTMLElement;
|
|
133
144
|
body: HTMLElement;
|
|
134
145
|
messagesWrapper: HTMLElement;
|
|
146
|
+
/** Starter prompts rendered inside the welcome card. */
|
|
147
|
+
starterSuggestions: HTMLElement;
|
|
148
|
+
/** Follow-up suggestions rendered after the transcript. */
|
|
149
|
+
transcriptSuggestions: HTMLElement;
|
|
135
150
|
/**
|
|
136
151
|
* Absolute-positioned slot above the composer footer. Interactive sheets
|
|
137
152
|
* (e.g. the answer-pill sheet for the ask_user_question tool) mount here
|
|
@@ -146,6 +161,11 @@ export interface PanelElements {
|
|
|
146
161
|
micButtonWrapper: HTMLElement | null;
|
|
147
162
|
composerForm: HTMLFormElement;
|
|
148
163
|
statusText: HTMLElement;
|
|
164
|
+
/** Permanent welcome host: variant and visibility change on it in place. */
|
|
165
|
+
welcomeHost: HTMLElement;
|
|
166
|
+
welcomeIconHolder: HTMLElement;
|
|
167
|
+
/** Display-only greeting bubble host, pinned above the transcript. */
|
|
168
|
+
greetingHost: HTMLElement;
|
|
149
169
|
introTitle: HTMLElement;
|
|
150
170
|
introSubtitle: HTMLElement;
|
|
151
171
|
closeButton: HTMLButtonElement;
|
|
@@ -178,6 +198,41 @@ export interface PanelElements {
|
|
|
178
198
|
peekTextNode?: HTMLElement;
|
|
179
199
|
}
|
|
180
200
|
|
|
201
|
+
const createSuggestionHost = (
|
|
202
|
+
surface: AgentWidgetSuggestionSurface
|
|
203
|
+
): HTMLElement => {
|
|
204
|
+
const host = createNode("div", {
|
|
205
|
+
className: "persona-suggestions",
|
|
206
|
+
attrs: { "data-persona-suggestions": suggestionSurfaceAttr(surface) },
|
|
207
|
+
});
|
|
208
|
+
host.hidden = true;
|
|
209
|
+
return host;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Welcome surface for either panel builder: permanent host plus the greeting
|
|
214
|
+
* host that sits between it and the transcript.
|
|
215
|
+
*/
|
|
216
|
+
const buildWelcomeSurface = (
|
|
217
|
+
config: AgentWidgetConfig | undefined,
|
|
218
|
+
body: HTMLElement
|
|
219
|
+
): { welcome: WelcomeHostElements; greetingHost: HTMLElement } => {
|
|
220
|
+
const starterSuggestions = createSuggestionHost("starter");
|
|
221
|
+
const welcome = buildWelcomeHost(config, starterSuggestions, {
|
|
222
|
+
flatShadow: isDockedMountMode(config),
|
|
223
|
+
});
|
|
224
|
+
const resolved = resolveWelcomeConfig(config);
|
|
225
|
+
applyWelcomeConfig(welcome, resolved);
|
|
226
|
+
applyWelcomeVisibility(
|
|
227
|
+
body,
|
|
228
|
+
welcome.host,
|
|
229
|
+
isWelcomeVisible(resolved, config?.initialMessages)
|
|
230
|
+
);
|
|
231
|
+
const greetingHost = buildGreetingHost();
|
|
232
|
+
renderWelcomeGreeting(greetingHost, resolved.message, config);
|
|
233
|
+
return { welcome, greetingHost };
|
|
234
|
+
};
|
|
235
|
+
|
|
181
236
|
/**
|
|
182
237
|
* Composer-bar panel: minimal close-only header (× in the top-right of the
|
|
183
238
|
* chat chrome). The pill (`footer`) and `peekBanner` are NOT children of
|
|
@@ -266,35 +321,17 @@ const buildComposerBarPanel = (
|
|
|
266
321
|
// when streaming content first overflows and the scrollbar appears.
|
|
267
322
|
body.style.setProperty("scrollbar-gutter", "stable");
|
|
268
323
|
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const introSubtitle = createNode("p", {
|
|
274
|
-
className: "persona-mt-2 persona-text-sm persona-text-persona-muted",
|
|
275
|
-
text:
|
|
276
|
-
config?.copy?.welcomeSubtitle ??
|
|
277
|
-
"Ask anything about your account or products.",
|
|
278
|
-
});
|
|
279
|
-
const introCard = createNode(
|
|
280
|
-
"div",
|
|
281
|
-
{
|
|
282
|
-
className: "persona-rounded-2xl persona-p-6",
|
|
283
|
-
attrs: { "data-persona-intro-card": "" },
|
|
284
|
-
style: {
|
|
285
|
-
background: "var(--persona-intro-card-bg, transparent)",
|
|
286
|
-
boxShadow: "var(--persona-intro-card-shadow, none)",
|
|
287
|
-
},
|
|
288
|
-
},
|
|
289
|
-
introTitle,
|
|
290
|
-
introSubtitle
|
|
291
|
-
);
|
|
324
|
+
const { welcome, greetingHost } = buildWelcomeSurface(config, body);
|
|
325
|
+
const { host: welcomeHost, title: introTitle, subtitle: introSubtitle } =
|
|
326
|
+
welcome;
|
|
327
|
+
const starterSuggestions = welcome.starterSuggestions;
|
|
292
328
|
|
|
293
329
|
const messagesWrapper = createElement(
|
|
294
330
|
"div",
|
|
295
|
-
"persona-flex persona-flex-col persona-gap-3"
|
|
331
|
+
"persona-widget-messages persona-flex persona-flex-col persona-gap-3"
|
|
296
332
|
);
|
|
297
|
-
const
|
|
333
|
+
const transcriptSuggestions = createSuggestionHost("followUp");
|
|
334
|
+
const contentMaxWidth = resolveContentMaxWidth(config, true);
|
|
298
335
|
if (contentMaxWidth) {
|
|
299
336
|
messagesWrapper.style.maxWidth = contentMaxWidth;
|
|
300
337
|
messagesWrapper.style.marginLeft = "auto";
|
|
@@ -302,13 +339,12 @@ const buildComposerBarPanel = (
|
|
|
302
339
|
messagesWrapper.style.width = "100%";
|
|
303
340
|
}
|
|
304
341
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
body.append(introCard, messagesWrapper);
|
|
342
|
+
body.append(
|
|
343
|
+
welcomeHost,
|
|
344
|
+
greetingHost,
|
|
345
|
+
messagesWrapper,
|
|
346
|
+
transcriptSuggestions
|
|
347
|
+
);
|
|
312
348
|
|
|
313
349
|
// Composer overlay (interactive sheets like ask_user_question slide up here).
|
|
314
350
|
// Anchored to the bottom of the container (which is `position: relative`),
|
|
@@ -338,6 +374,8 @@ const buildComposerBarPanel = (
|
|
|
338
374
|
container,
|
|
339
375
|
body,
|
|
340
376
|
messagesWrapper,
|
|
377
|
+
starterSuggestions,
|
|
378
|
+
transcriptSuggestions,
|
|
341
379
|
composerOverlay,
|
|
342
380
|
suggestions: composerElements.suggestions,
|
|
343
381
|
textarea: composerElements.textarea,
|
|
@@ -347,6 +385,9 @@ const buildComposerBarPanel = (
|
|
|
347
385
|
micButtonWrapper: composerElements.micButtonWrapper,
|
|
348
386
|
composerForm: composerElements.composerForm,
|
|
349
387
|
statusText: composerElements.statusText,
|
|
388
|
+
welcomeHost,
|
|
389
|
+
welcomeIconHolder: welcome.iconHolder,
|
|
390
|
+
greetingHost,
|
|
350
391
|
introTitle,
|
|
351
392
|
introSubtitle,
|
|
352
393
|
closeButton,
|
|
@@ -403,42 +444,18 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
403
444
|
// when streaming content first overflows and the scrollbar appears.
|
|
404
445
|
body.style.setProperty("scrollbar-gutter", "stable");
|
|
405
446
|
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const introSubtitle = createNode("p", {
|
|
411
|
-
className: "persona-mt-2 persona-text-sm persona-text-persona-muted",
|
|
412
|
-
text:
|
|
413
|
-
config?.copy?.welcomeSubtitle ??
|
|
414
|
-
"Ask anything about your account or products.",
|
|
415
|
-
});
|
|
416
|
-
// Background and box-shadow flow through the themable `components.introCard`
|
|
417
|
-
// tokens (--persona-intro-card-bg / --persona-intro-card-shadow); both
|
|
418
|
-
// default to flat (transparent / none). Docked mode always stays flat,
|
|
419
|
-
// even when a theme sets an introCard shadow.
|
|
420
|
-
const introCard = createNode(
|
|
421
|
-
"div",
|
|
422
|
-
{
|
|
423
|
-
className: "persona-rounded-2xl persona-p-6",
|
|
424
|
-
attrs: { "data-persona-intro-card": "" },
|
|
425
|
-
style: {
|
|
426
|
-
background: "var(--persona-intro-card-bg, transparent)",
|
|
427
|
-
boxShadow: isDockedMountMode(config)
|
|
428
|
-
? "none"
|
|
429
|
-
: "var(--persona-intro-card-shadow, none)",
|
|
430
|
-
},
|
|
431
|
-
},
|
|
432
|
-
introTitle,
|
|
433
|
-
introSubtitle
|
|
434
|
-
);
|
|
447
|
+
const { welcome, greetingHost } = buildWelcomeSurface(config, body);
|
|
448
|
+
const { host: welcomeHost, title: introTitle, subtitle: introSubtitle } =
|
|
449
|
+
welcome;
|
|
450
|
+
const starterSuggestions = welcome.starterSuggestions;
|
|
435
451
|
|
|
436
452
|
const messagesWrapper = createElement(
|
|
437
453
|
"div",
|
|
438
|
-
"persona-flex persona-flex-col persona-gap-3"
|
|
454
|
+
"persona-widget-messages persona-flex persona-flex-col persona-gap-3"
|
|
439
455
|
);
|
|
456
|
+
const transcriptSuggestions = createSuggestionHost("followUp");
|
|
440
457
|
|
|
441
|
-
const contentMaxWidth = config
|
|
458
|
+
const contentMaxWidth = resolveContentMaxWidth(config, false);
|
|
442
459
|
if (contentMaxWidth) {
|
|
443
460
|
messagesWrapper.style.maxWidth = contentMaxWidth;
|
|
444
461
|
messagesWrapper.style.marginLeft = "auto";
|
|
@@ -446,13 +463,12 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
446
463
|
messagesWrapper.style.width = "100%";
|
|
447
464
|
}
|
|
448
465
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
body.append(introCard, messagesWrapper);
|
|
466
|
+
body.append(
|
|
467
|
+
welcomeHost,
|
|
468
|
+
greetingHost,
|
|
469
|
+
messagesWrapper,
|
|
470
|
+
transcriptSuggestions
|
|
471
|
+
);
|
|
456
472
|
|
|
457
473
|
// composer-bar mode early-returned above with its own pill builder; this
|
|
458
474
|
// path always uses the standard column-stacked composer card.
|
|
@@ -500,6 +516,8 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
500
516
|
container,
|
|
501
517
|
body,
|
|
502
518
|
messagesWrapper,
|
|
519
|
+
starterSuggestions,
|
|
520
|
+
transcriptSuggestions,
|
|
503
521
|
composerOverlay,
|
|
504
522
|
suggestions: composerElements.suggestions,
|
|
505
523
|
textarea: composerElements.textarea,
|
|
@@ -509,6 +527,9 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
509
527
|
micButtonWrapper: composerElements.micButtonWrapper,
|
|
510
528
|
composerForm: composerElements.composerForm,
|
|
511
529
|
statusText: composerElements.statusText,
|
|
530
|
+
welcomeHost,
|
|
531
|
+
welcomeIconHolder: welcome.iconHolder,
|
|
532
|
+
greetingHost,
|
|
512
533
|
introTitle,
|
|
513
534
|
introSubtitle,
|
|
514
535
|
closeButton: headerElements.closeButton,
|
|
@@ -79,6 +79,24 @@ describe("buildPillComposer (single-row pill composer)", () => {
|
|
|
79
79
|
expect(formChildren[2]).toBe(elements.rightActions);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
it("keeps its 100px max-height cap, even under a themed line height", () => {
|
|
83
|
+
const elements = buildPillComposer({ config: { apiUrl: "/api" } });
|
|
84
|
+
expect(elements.textarea.style.maxHeight).toBe("100px");
|
|
85
|
+
|
|
86
|
+
// A themed line height must not re-derive the cap: the pill's explicit
|
|
87
|
+
// post-construction override wins over the 3-line default.
|
|
88
|
+
elements.textarea.style.lineHeight = "24px";
|
|
89
|
+
document.body.appendChild(elements.textarea);
|
|
90
|
+
Object.defineProperty(elements.textarea, "scrollHeight", {
|
|
91
|
+
configurable: true,
|
|
92
|
+
value: 10000,
|
|
93
|
+
});
|
|
94
|
+
elements.textarea.dispatchEvent(new Event("input"));
|
|
95
|
+
expect(elements.textarea.style.maxHeight).toBe("100px");
|
|
96
|
+
expect(elements.textarea.style.height).toBe("100px");
|
|
97
|
+
document.body.removeChild(elements.textarea);
|
|
98
|
+
});
|
|
99
|
+
|
|
82
100
|
it("returns null for optional controls when disabled (matching ComposerElements contract)", () => {
|
|
83
101
|
const elements = buildPillComposer({ config: { apiUrl: "/api" } });
|
|
84
102
|
expect(elements.attachmentButton).toBeNull();
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createSuggestions } from "./suggestions";
|
|
6
|
+
import type { AgentWidgetSession } from "../session";
|
|
7
|
+
|
|
8
|
+
const makeSession = (): AgentWidgetSession =>
|
|
9
|
+
({
|
|
10
|
+
isStreaming: () => false,
|
|
11
|
+
getMessages: () => [],
|
|
12
|
+
sendMessage: () => {},
|
|
13
|
+
}) as unknown as AgentWidgetSession;
|
|
14
|
+
|
|
15
|
+
const setup = () => {
|
|
16
|
+
const container = document.createElement("div");
|
|
17
|
+
document.body.appendChild(container);
|
|
18
|
+
const textarea = document.createElement("textarea");
|
|
19
|
+
document.body.appendChild(textarea);
|
|
20
|
+
return {
|
|
21
|
+
container,
|
|
22
|
+
textarea,
|
|
23
|
+
manager: createSuggestions(container),
|
|
24
|
+
session: makeSession(),
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const withAnimate = (): ReturnType<typeof vi.fn> => {
|
|
29
|
+
const animate = vi.fn(() => ({}));
|
|
30
|
+
Object.defineProperty(Element.prototype, "animate", {
|
|
31
|
+
value: animate,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
});
|
|
35
|
+
return animate;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
Reflect.deleteProperty(Element.prototype, "animate");
|
|
40
|
+
vi.unstubAllGlobals();
|
|
41
|
+
document.body.replaceChildren();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("suggestion entrance animation", () => {
|
|
45
|
+
it("animates each item once for a newly shown set", () => {
|
|
46
|
+
const animate = withAnimate();
|
|
47
|
+
const { manager, session, textarea } = setup();
|
|
48
|
+
|
|
49
|
+
manager.render(["One", "Two"], session, textarea, [], undefined, {
|
|
50
|
+
variant: "card",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(animate).toHaveBeenCalledTimes(2);
|
|
54
|
+
const [, secondOptions] = animate.mock.calls[1] as [
|
|
55
|
+
unknown,
|
|
56
|
+
{ delay: number; duration: number; fill: string },
|
|
57
|
+
];
|
|
58
|
+
expect(secondOptions.delay).toBe(60);
|
|
59
|
+
expect(secondOptions.duration).toBe(250);
|
|
60
|
+
expect(secondOptions.fill).toBe("backwards");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not re-animate when render repeats the same items", () => {
|
|
64
|
+
const animate = withAnimate();
|
|
65
|
+
const { manager, session, textarea } = setup();
|
|
66
|
+
const items = ["One", "Two"];
|
|
67
|
+
|
|
68
|
+
manager.render(items, session, textarea, [], undefined, {
|
|
69
|
+
variant: "card",
|
|
70
|
+
});
|
|
71
|
+
manager.render(items, session, textarea, [], undefined, {
|
|
72
|
+
variant: "card",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(animate).toHaveBeenCalledTimes(2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("animates again once the item set changes", () => {
|
|
79
|
+
const animate = withAnimate();
|
|
80
|
+
const { manager, session, textarea } = setup();
|
|
81
|
+
|
|
82
|
+
manager.render(["One"], session, textarea, [], undefined, {
|
|
83
|
+
variant: "card",
|
|
84
|
+
});
|
|
85
|
+
manager.render(["Two"], session, textarea, [], undefined, {
|
|
86
|
+
variant: "card",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(animate).toHaveBeenCalledTimes(2);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("skips the animation under prefers-reduced-motion", () => {
|
|
93
|
+
const animate = withAnimate();
|
|
94
|
+
vi.stubGlobal(
|
|
95
|
+
"matchMedia",
|
|
96
|
+
vi.fn(() => ({ matches: true }))
|
|
97
|
+
);
|
|
98
|
+
const { manager, session, textarea } = setup();
|
|
99
|
+
|
|
100
|
+
manager.render(["One", "Two"], session, textarea, [], undefined, {
|
|
101
|
+
variant: "card",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(animate).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("still renders when the platform has no Web Animations API", () => {
|
|
108
|
+
const { container, manager, session, textarea } = setup();
|
|
109
|
+
|
|
110
|
+
expect(typeof Element.prototype.animate).toBe("undefined");
|
|
111
|
+
expect(() =>
|
|
112
|
+
manager.render(["One", "Two"], session, textarea, [], undefined, {
|
|
113
|
+
variant: "card",
|
|
114
|
+
})
|
|
115
|
+
).not.toThrow();
|
|
116
|
+
expect(container.querySelectorAll(".persona-suggestion")).toHaveLength(2);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("per-item icon color", () => {
|
|
121
|
+
const iconOf = (container: HTMLElement): SVGElement =>
|
|
122
|
+
container.querySelector(".persona-suggestion__icon") as SVGElement;
|
|
123
|
+
|
|
124
|
+
it("tints only the icon element", () => {
|
|
125
|
+
const { manager, session, textarea, container } = setup();
|
|
126
|
+
|
|
127
|
+
manager.render(
|
|
128
|
+
[{ label: "Compare plans", icon: "dollar-sign", iconColor: "#16a34a" }],
|
|
129
|
+
session,
|
|
130
|
+
textarea,
|
|
131
|
+
[],
|
|
132
|
+
undefined,
|
|
133
|
+
{ variant: "card" }
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const button = container.querySelector("button") as HTMLButtonElement;
|
|
137
|
+
expect(iconOf(container).style.color).toBe("rgb(22, 163, 74)");
|
|
138
|
+
expect(button.style.color).toBe("");
|
|
139
|
+
expect(button.style.borderColor).toBe("");
|
|
140
|
+
expect(button.style.background).toBe("");
|
|
141
|
+
expect(
|
|
142
|
+
(container.querySelector(".persona-suggestion__label") as HTMLElement)
|
|
143
|
+
.style.color
|
|
144
|
+
).toBe("");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("wins over the emphasis-primary icon tint without recoloring the button", () => {
|
|
148
|
+
const { manager, session, textarea, container } = setup();
|
|
149
|
+
|
|
150
|
+
manager.render(
|
|
151
|
+
[
|
|
152
|
+
{
|
|
153
|
+
label: "Compare plans",
|
|
154
|
+
icon: "dollar-sign",
|
|
155
|
+
iconColor: "#16a34a",
|
|
156
|
+
emphasis: "primary",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
session,
|
|
160
|
+
textarea,
|
|
161
|
+
[],
|
|
162
|
+
undefined,
|
|
163
|
+
{ variant: "card" }
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const button = container.querySelector("button") as HTMLButtonElement;
|
|
167
|
+
expect(button.dataset.emphasis).toBe("primary");
|
|
168
|
+
expect(iconOf(container).style.color).toBe("rgb(22, 163, 74)");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("leaves the icon in currentColor when iconColor is absent", () => {
|
|
172
|
+
const { manager, session, textarea, container } = setup();
|
|
173
|
+
|
|
174
|
+
manager.render(
|
|
175
|
+
[{ label: "Compare plans", icon: "dollar-sign" }],
|
|
176
|
+
session,
|
|
177
|
+
textarea,
|
|
178
|
+
[],
|
|
179
|
+
undefined,
|
|
180
|
+
{ variant: "card" }
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const icon = iconOf(container);
|
|
184
|
+
expect(icon.style.color).toBe("");
|
|
185
|
+
expect(icon.getAttribute("stroke")).toBe("currentColor");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("leaves string shorthand items unaffected", () => {
|
|
189
|
+
const { manager, session, textarea, container } = setup();
|
|
190
|
+
|
|
191
|
+
manager.render(["Compare plans"], session, textarea, [], undefined, {
|
|
192
|
+
variant: "card",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(container.querySelector(".persona-suggestion__icon")).toBeNull();
|
|
196
|
+
const button = container.querySelector("button") as HTMLButtonElement;
|
|
197
|
+
expect(button.style.color).toBe("");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("description rendering per variant", () => {
|
|
202
|
+
const items = [
|
|
203
|
+
{
|
|
204
|
+
label: "Track an order",
|
|
205
|
+
description: "Look up status by order number",
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
it("keeps the chip variant to a single-line label", () => {
|
|
210
|
+
const { manager, session, textarea, container } = setup();
|
|
211
|
+
|
|
212
|
+
manager.render(items, session, textarea, [], undefined, {
|
|
213
|
+
variant: "chip",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(
|
|
217
|
+
container.querySelector(".persona-suggestion__description")
|
|
218
|
+
).toBeNull();
|
|
219
|
+
expect(
|
|
220
|
+
(container.querySelector(".persona-suggestion__label") as HTMLElement)
|
|
221
|
+
.textContent
|
|
222
|
+
).toBe("Track an order");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("renders the description in the card and list variants", () => {
|
|
226
|
+
const { manager, session, textarea, container } = setup();
|
|
227
|
+
|
|
228
|
+
for (const variant of ["card", "list"] as const) {
|
|
229
|
+
manager.render(items, session, textarea, [], undefined, { variant });
|
|
230
|
+
expect(
|
|
231
|
+
(
|
|
232
|
+
container.querySelector(
|
|
233
|
+
".persona-suggestion__description"
|
|
234
|
+
) as HTMLElement
|
|
235
|
+
).textContent
|
|
236
|
+
).toBe("Look up status by order number");
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("legacy suggestionChipsConfig scoping", () => {
|
|
242
|
+
const chipsConfig = {
|
|
243
|
+
fontFamily: "sans-serif" as const,
|
|
244
|
+
fontWeight: "500",
|
|
245
|
+
paddingX: "12px",
|
|
246
|
+
paddingY: "6px",
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
it("applies inline chip styles to the chip variant", () => {
|
|
250
|
+
const { manager, session, textarea, container } = setup();
|
|
251
|
+
|
|
252
|
+
manager.render(["One"], session, textarea, [], chipsConfig, {
|
|
253
|
+
variant: "chip",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const button = container.querySelector("button") as HTMLButtonElement;
|
|
257
|
+
expect(button.style.paddingLeft).toBe("12px");
|
|
258
|
+
expect(button.style.paddingTop).toBe("6px");
|
|
259
|
+
expect(button.style.fontWeight).toBe("500");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("does not let chip config override card and list padding tokens", () => {
|
|
263
|
+
const { manager, session, textarea, container } = setup();
|
|
264
|
+
|
|
265
|
+
for (const variant of ["card", "list"] as const) {
|
|
266
|
+
manager.render(["One"], session, textarea, [], chipsConfig, { variant });
|
|
267
|
+
const button = container.querySelector("button") as HTMLButtonElement;
|
|
268
|
+
expect(button.style.paddingLeft).toBe("");
|
|
269
|
+
expect(button.style.paddingTop).toBe("");
|
|
270
|
+
expect(button.style.fontFamily).toBe("");
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|