@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createElement } from "../utils/dom";
|
|
2
|
-
import { AgentWidgetConfig } from "../types";
|
|
1
|
+
import { createElement, createNode, cx } from "../utils/dom";
|
|
2
|
+
import { AgentWidgetConfig, AgentWidgetLauncherTeaserConfig } from "../types";
|
|
3
3
|
import { positionMap } from "../utils/positioning";
|
|
4
4
|
import { isDockedMountMode } from "../utils/dock";
|
|
5
5
|
import { renderLucideIcon } from "../utils/icons";
|
|
@@ -240,3 +240,298 @@ export const createLauncherButton = (
|
|
|
240
240
|
destroy
|
|
241
241
|
};
|
|
242
242
|
};
|
|
243
|
+
|
|
244
|
+
/** Teaser bubble handle. `dismiss(true)` also writes the persisted flag. */
|
|
245
|
+
export interface LauncherTeaser {
|
|
246
|
+
element: HTMLElement;
|
|
247
|
+
show: () => void;
|
|
248
|
+
dismiss: (persist: boolean) => void;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** "auto" covers restored/hook-driven opens: they never persist the teaser flag. */
|
|
252
|
+
export type LauncherOpenOrigin = "user" | "auto";
|
|
253
|
+
|
|
254
|
+
export interface LauncherSurface {
|
|
255
|
+
/** Positioned wrapper. Sibling teaser + launcher live inside it. */
|
|
256
|
+
element: HTMLElement;
|
|
257
|
+
launcher: LauncherButton;
|
|
258
|
+
/** Null when no teaser is configured for the current config. */
|
|
259
|
+
teaser: LauncherTeaser | null;
|
|
260
|
+
/** Opening the panel consumes the teaser; closing never resurrects it. */
|
|
261
|
+
setPanelOpen: (open: boolean, origin?: LauncherOpenOrigin) => void;
|
|
262
|
+
update: (config: AgentWidgetConfig) => void;
|
|
263
|
+
destroy: () => void;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const TEASER_KEY_SUFFIX = "teaser-dismissed";
|
|
267
|
+
|
|
268
|
+
/** Consumed teaser keys for this page load; also the blocked-storage fallback. */
|
|
269
|
+
const teaserMemoryDismissed = new Set<string>();
|
|
270
|
+
|
|
271
|
+
const teaserStorageKey = (config?: AgentWidgetConfig): string => {
|
|
272
|
+
const persist = config?.persistState;
|
|
273
|
+
const prefix =
|
|
274
|
+
(typeof persist === "object" ? persist?.keyPrefix : undefined) ?? "persona-";
|
|
275
|
+
return `${prefix}${TEASER_KEY_SUFFIX}`;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// localStorage access throws (not just fails) in Safari private mode and
|
|
279
|
+
// partitioned iframes, so every access falls back to the in-memory set.
|
|
280
|
+
const readTeaserDismissed = (key: string, useStorage: boolean): boolean => {
|
|
281
|
+
if (teaserMemoryDismissed.has(key)) return true;
|
|
282
|
+
if (!useStorage) return false;
|
|
283
|
+
try {
|
|
284
|
+
return window.localStorage.getItem(key) === "1";
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const writeTeaserDismissed = (key: string, useStorage: boolean): void => {
|
|
291
|
+
teaserMemoryDismissed.add(key);
|
|
292
|
+
if (!useStorage) return;
|
|
293
|
+
try {
|
|
294
|
+
window.localStorage.setItem(key, "1");
|
|
295
|
+
} catch {
|
|
296
|
+
/* blocked storage: the in-memory flag above is the fallback */
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const prefersReducedMotion = (): boolean =>
|
|
301
|
+
typeof window !== "undefined" &&
|
|
302
|
+
typeof window.matchMedia === "function" &&
|
|
303
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
304
|
+
|
|
305
|
+
const resolveTeaserConfig = (
|
|
306
|
+
config: AgentWidgetConfig | undefined
|
|
307
|
+
): AgentWidgetLauncherTeaserConfig | null => {
|
|
308
|
+
const teaser = config?.launcher?.teaser;
|
|
309
|
+
if (!teaser || !teaser.text) return null;
|
|
310
|
+
if (config?.launcher?.enabled === false) return null;
|
|
311
|
+
if (isDockedMountMode(config ?? {})) return null;
|
|
312
|
+
return teaser;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Launcher wrapper owning the collapsed button and its optional teaser bubble.
|
|
317
|
+
* Both the critical `launcher.global.js` bundle and the full widget build the
|
|
318
|
+
* launcher through this, so teaser behavior and cleanup are identical on the
|
|
319
|
+
* eager and deferred paths.
|
|
320
|
+
*/
|
|
321
|
+
export const createLauncherSurface = (
|
|
322
|
+
config: AgentWidgetConfig | undefined,
|
|
323
|
+
onToggle: () => void
|
|
324
|
+
): LauncherSurface => {
|
|
325
|
+
const wrapper = createElement("div", "persona-launcher-surface");
|
|
326
|
+
wrapper.setAttribute("data-persona-launcher-surface", "true");
|
|
327
|
+
|
|
328
|
+
let currentConfig: AgentWidgetConfig = config ?? {};
|
|
329
|
+
let teaserConfig: AgentWidgetLauncherTeaserConfig | null = null;
|
|
330
|
+
let teaserElement: HTMLElement | null = null;
|
|
331
|
+
let dismissButton: HTMLButtonElement | null = null;
|
|
332
|
+
let teaserTextButton: HTMLButtonElement | null = null;
|
|
333
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
334
|
+
let visible = false;
|
|
335
|
+
let shown = false;
|
|
336
|
+
let consumed = false;
|
|
337
|
+
let panelOpen = false;
|
|
338
|
+
|
|
339
|
+
const usesStorage = (): boolean =>
|
|
340
|
+
(teaserConfig?.frequency ?? "once") === "once" &&
|
|
341
|
+
currentConfig.persistState !== false;
|
|
342
|
+
|
|
343
|
+
const clearTimer = () => {
|
|
344
|
+
if (timer !== null) {
|
|
345
|
+
clearTimeout(timer);
|
|
346
|
+
timer = null;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const hideBubble = () => {
|
|
351
|
+
if (!teaserElement || !visible) return;
|
|
352
|
+
visible = false;
|
|
353
|
+
teaserElement.hidden = true;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const dismiss = (persist: boolean) => {
|
|
357
|
+
clearTimer();
|
|
358
|
+
consumed = true;
|
|
359
|
+
hideBubble();
|
|
360
|
+
writeTeaserDismissed(teaserStorageKey(currentConfig), persist && usesStorage());
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Click-through and launcher clicks consume; only a seen teaser persists.
|
|
365
|
+
* An automatic open is never user intent, so it consumes in memory only.
|
|
366
|
+
*/
|
|
367
|
+
const consumeForOpen = (origin: LauncherOpenOrigin) => {
|
|
368
|
+
if (!teaserConfig || consumed) return;
|
|
369
|
+
dismiss(origin === "user" && visible);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const show = () => {
|
|
373
|
+
timer = null;
|
|
374
|
+
if (!teaserElement || consumed || panelOpen) return;
|
|
375
|
+
shown = true;
|
|
376
|
+
visible = true;
|
|
377
|
+
teaserElement.hidden = false;
|
|
378
|
+
if (prefersReducedMotion() || typeof teaserElement.animate !== "function") return;
|
|
379
|
+
teaserElement.animate(
|
|
380
|
+
[
|
|
381
|
+
{ opacity: "0", transform: "translateY(6px)" },
|
|
382
|
+
{ opacity: "1", transform: "none" },
|
|
383
|
+
],
|
|
384
|
+
{ duration: 220, easing: "ease-out", fill: "backwards" }
|
|
385
|
+
);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const schedule = () => {
|
|
389
|
+
if (!teaserConfig || !teaserElement) return;
|
|
390
|
+
if (consumed || shown || panelOpen) return;
|
|
391
|
+
if (readTeaserDismissed(teaserStorageKey(currentConfig), usesStorage())) return;
|
|
392
|
+
clearTimer();
|
|
393
|
+
timer = setTimeout(show, Math.max(0, teaserConfig.delayMs ?? 0));
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const handleTeaserClick = () => {
|
|
397
|
+
dismiss(usesStorage());
|
|
398
|
+
onToggle();
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const handleDismissClick = (event: MouseEvent) => {
|
|
402
|
+
event.stopPropagation();
|
|
403
|
+
dismiss(usesStorage());
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const handleLauncherToggle = () => {
|
|
407
|
+
consumeForOpen("user");
|
|
408
|
+
onToggle();
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const launcher = createLauncherButton(config, handleLauncherToggle);
|
|
412
|
+
|
|
413
|
+
const removeTeaserElement = () => {
|
|
414
|
+
clearTimer();
|
|
415
|
+
teaserTextButton?.removeEventListener("click", handleTeaserClick);
|
|
416
|
+
dismissButton?.removeEventListener("click", handleDismissClick);
|
|
417
|
+
teaserElement?.remove();
|
|
418
|
+
teaserElement = null;
|
|
419
|
+
teaserTextButton = null;
|
|
420
|
+
dismissButton = null;
|
|
421
|
+
visible = false;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const buildTeaserElement = () => {
|
|
425
|
+
teaserTextButton = createNode("button", {
|
|
426
|
+
className: "persona-launcher-teaser-text",
|
|
427
|
+
attrs: { type: "button" },
|
|
428
|
+
});
|
|
429
|
+
teaserTextButton.addEventListener("click", handleTeaserClick);
|
|
430
|
+
|
|
431
|
+
dismissButton = createNode("button", {
|
|
432
|
+
className: "persona-launcher-teaser-dismiss",
|
|
433
|
+
attrs: { type: "button" },
|
|
434
|
+
text: "\u00d7",
|
|
435
|
+
});
|
|
436
|
+
dismissButton.addEventListener("click", handleDismissClick);
|
|
437
|
+
|
|
438
|
+
teaserElement = createNode(
|
|
439
|
+
"div",
|
|
440
|
+
{
|
|
441
|
+
className: "persona-launcher-teaser",
|
|
442
|
+
attrs: {
|
|
443
|
+
"data-persona-launcher-teaser": "true",
|
|
444
|
+
role: "status",
|
|
445
|
+
"aria-live": "polite",
|
|
446
|
+
hidden: "",
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
teaserTextButton,
|
|
450
|
+
dismissButton
|
|
451
|
+
);
|
|
452
|
+
wrapper.insertBefore(teaserElement, launcher.element);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* The wrapper is `display: contents` until a teaser exists, so the launcher
|
|
457
|
+
* keeps its own fixed placement; with a teaser the wrapper takes over the
|
|
458
|
+
* corner and the button becomes static inside it.
|
|
459
|
+
*/
|
|
460
|
+
const applyPlacement = () => {
|
|
461
|
+
const position = currentConfig.launcher?.position ?? "bottom-right";
|
|
462
|
+
const positionClass = positionMap[position] ?? positionMap["bottom-right"];
|
|
463
|
+
|
|
464
|
+
if (!teaserElement) {
|
|
465
|
+
wrapper.className = "persona-launcher-surface";
|
|
466
|
+
wrapper.removeAttribute("data-teaser");
|
|
467
|
+
wrapper.style.zIndex = "";
|
|
468
|
+
launcher.element.style.position = "";
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
wrapper.className = cx("persona-launcher-surface", positionClass);
|
|
473
|
+
wrapper.setAttribute("data-teaser", position);
|
|
474
|
+
wrapper.style.zIndex = String(
|
|
475
|
+
currentConfig.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX
|
|
476
|
+
);
|
|
477
|
+
launcher.element.style.position = "static";
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const syncTeaser = (nextConfig: AgentWidgetConfig) => {
|
|
481
|
+
const next = resolveTeaserConfig(nextConfig);
|
|
482
|
+
teaserConfig = next;
|
|
483
|
+
|
|
484
|
+
if (!next) {
|
|
485
|
+
removeTeaserElement();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// `shown` is deliberately not reset here: at most one appearance per load.
|
|
490
|
+
if (!teaserElement) buildTeaserElement();
|
|
491
|
+
|
|
492
|
+
teaserTextButton!.textContent = next.text;
|
|
493
|
+
dismissButton!.hidden = next.dismissible === false;
|
|
494
|
+
dismissButton!.setAttribute(
|
|
495
|
+
"aria-label",
|
|
496
|
+
next.dismissLabel ?? "Dismiss message"
|
|
497
|
+
);
|
|
498
|
+
schedule();
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const update = (nextConfig: AgentWidgetConfig) => {
|
|
502
|
+
currentConfig = nextConfig;
|
|
503
|
+
launcher.update(nextConfig);
|
|
504
|
+
syncTeaser(nextConfig);
|
|
505
|
+
applyPlacement();
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const setPanelOpen = (open: boolean, origin: LauncherOpenOrigin = "user") => {
|
|
509
|
+
panelOpen = open;
|
|
510
|
+
if (!open) return;
|
|
511
|
+
consumeForOpen(origin);
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const destroy = () => {
|
|
515
|
+
removeTeaserElement();
|
|
516
|
+
launcher.destroy();
|
|
517
|
+
wrapper.remove();
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
wrapper.appendChild(launcher.element);
|
|
521
|
+
if (config) {
|
|
522
|
+
syncTeaser(config);
|
|
523
|
+
applyPlacement();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
element: wrapper,
|
|
528
|
+
launcher,
|
|
529
|
+
get teaser(): LauncherTeaser | null {
|
|
530
|
+
if (!teaserElement) return null;
|
|
531
|
+
return { element: teaserElement, show, dismiss };
|
|
532
|
+
},
|
|
533
|
+
setPanelOpen,
|
|
534
|
+
update,
|
|
535
|
+
destroy,
|
|
536
|
+
};
|
|
537
|
+
};
|
package/src/components/panel.ts
CHANGED
|
@@ -10,6 +10,16 @@ import { buildHeaderWithLayout } from "./header-layouts";
|
|
|
10
10
|
import { createCloseButton, createClearChatButton } from "./header-parts";
|
|
11
11
|
import { buildComposer, ComposerElements } from "./composer-builder";
|
|
12
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";
|
|
13
23
|
|
|
14
24
|
export interface PanelWrapper {
|
|
15
25
|
wrapper: HTMLElement;
|
|
@@ -151,6 +161,11 @@ export interface PanelElements {
|
|
|
151
161
|
micButtonWrapper: HTMLElement | null;
|
|
152
162
|
composerForm: HTMLFormElement;
|
|
153
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;
|
|
154
169
|
introTitle: HTMLElement;
|
|
155
170
|
introSubtitle: HTMLElement;
|
|
156
171
|
closeButton: HTMLButtonElement;
|
|
@@ -194,6 +209,30 @@ const createSuggestionHost = (
|
|
|
194
209
|
return host;
|
|
195
210
|
};
|
|
196
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
|
+
|
|
197
236
|
/**
|
|
198
237
|
* Composer-bar panel: minimal close-only header (× in the top-right of the
|
|
199
238
|
* chat chrome). The pill (`footer`) and `peekBanner` are NOT children of
|
|
@@ -282,38 +321,17 @@ const buildComposerBarPanel = (
|
|
|
282
321
|
// when streaming content first overflows and the scrollbar appears.
|
|
283
322
|
body.style.setProperty("scrollbar-gutter", "stable");
|
|
284
323
|
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const introSubtitle = createNode("p", {
|
|
290
|
-
className: "persona-mt-2 persona-text-sm persona-text-persona-muted",
|
|
291
|
-
text:
|
|
292
|
-
config?.copy?.welcomeSubtitle ??
|
|
293
|
-
"Ask anything about your account or products.",
|
|
294
|
-
});
|
|
295
|
-
const starterSuggestions = createSuggestionHost("starter");
|
|
296
|
-
const introCard = createNode(
|
|
297
|
-
"div",
|
|
298
|
-
{
|
|
299
|
-
className: "persona-rounded-2xl persona-p-6",
|
|
300
|
-
attrs: { "data-persona-intro-card": "" },
|
|
301
|
-
style: {
|
|
302
|
-
background: "var(--persona-intro-card-bg, transparent)",
|
|
303
|
-
boxShadow: "var(--persona-intro-card-shadow, none)",
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
introTitle,
|
|
307
|
-
introSubtitle,
|
|
308
|
-
starterSuggestions
|
|
309
|
-
);
|
|
324
|
+
const { welcome, greetingHost } = buildWelcomeSurface(config, body);
|
|
325
|
+
const { host: welcomeHost, title: introTitle, subtitle: introSubtitle } =
|
|
326
|
+
welcome;
|
|
327
|
+
const starterSuggestions = welcome.starterSuggestions;
|
|
310
328
|
|
|
311
329
|
const messagesWrapper = createElement(
|
|
312
330
|
"div",
|
|
313
|
-
"persona-flex persona-flex-col persona-gap-3"
|
|
331
|
+
"persona-widget-messages persona-flex persona-flex-col persona-gap-3"
|
|
314
332
|
);
|
|
315
333
|
const transcriptSuggestions = createSuggestionHost("followUp");
|
|
316
|
-
const contentMaxWidth = config
|
|
334
|
+
const contentMaxWidth = resolveContentMaxWidth(config, true);
|
|
317
335
|
if (contentMaxWidth) {
|
|
318
336
|
messagesWrapper.style.maxWidth = contentMaxWidth;
|
|
319
337
|
messagesWrapper.style.marginLeft = "auto";
|
|
@@ -321,13 +339,12 @@ const buildComposerBarPanel = (
|
|
|
321
339
|
messagesWrapper.style.width = "100%";
|
|
322
340
|
}
|
|
323
341
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
body.append(introCard, messagesWrapper, transcriptSuggestions);
|
|
342
|
+
body.append(
|
|
343
|
+
welcomeHost,
|
|
344
|
+
greetingHost,
|
|
345
|
+
messagesWrapper,
|
|
346
|
+
transcriptSuggestions
|
|
347
|
+
);
|
|
331
348
|
|
|
332
349
|
// Composer overlay (interactive sheets like ask_user_question slide up here).
|
|
333
350
|
// Anchored to the bottom of the container (which is `position: relative`),
|
|
@@ -368,6 +385,9 @@ const buildComposerBarPanel = (
|
|
|
368
385
|
micButtonWrapper: composerElements.micButtonWrapper,
|
|
369
386
|
composerForm: composerElements.composerForm,
|
|
370
387
|
statusText: composerElements.statusText,
|
|
388
|
+
welcomeHost,
|
|
389
|
+
welcomeIconHolder: welcome.iconHolder,
|
|
390
|
+
greetingHost,
|
|
371
391
|
introTitle,
|
|
372
392
|
introSubtitle,
|
|
373
393
|
closeButton,
|
|
@@ -424,45 +444,18 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
424
444
|
// when streaming content first overflows and the scrollbar appears.
|
|
425
445
|
body.style.setProperty("scrollbar-gutter", "stable");
|
|
426
446
|
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const introSubtitle = createNode("p", {
|
|
432
|
-
className: "persona-mt-2 persona-text-sm persona-text-persona-muted",
|
|
433
|
-
text:
|
|
434
|
-
config?.copy?.welcomeSubtitle ??
|
|
435
|
-
"Ask anything about your account or products.",
|
|
436
|
-
});
|
|
437
|
-
const starterSuggestions = createSuggestionHost("starter");
|
|
438
|
-
// Background and box-shadow flow through the themable `components.introCard`
|
|
439
|
-
// tokens (--persona-intro-card-bg / --persona-intro-card-shadow); both
|
|
440
|
-
// default to flat (transparent / none). Docked mode always stays flat,
|
|
441
|
-
// even when a theme sets an introCard shadow.
|
|
442
|
-
const introCard = createNode(
|
|
443
|
-
"div",
|
|
444
|
-
{
|
|
445
|
-
className: "persona-rounded-2xl persona-p-6",
|
|
446
|
-
attrs: { "data-persona-intro-card": "" },
|
|
447
|
-
style: {
|
|
448
|
-
background: "var(--persona-intro-card-bg, transparent)",
|
|
449
|
-
boxShadow: isDockedMountMode(config)
|
|
450
|
-
? "none"
|
|
451
|
-
: "var(--persona-intro-card-shadow, none)",
|
|
452
|
-
},
|
|
453
|
-
},
|
|
454
|
-
introTitle,
|
|
455
|
-
introSubtitle,
|
|
456
|
-
starterSuggestions
|
|
457
|
-
);
|
|
447
|
+
const { welcome, greetingHost } = buildWelcomeSurface(config, body);
|
|
448
|
+
const { host: welcomeHost, title: introTitle, subtitle: introSubtitle } =
|
|
449
|
+
welcome;
|
|
450
|
+
const starterSuggestions = welcome.starterSuggestions;
|
|
458
451
|
|
|
459
452
|
const messagesWrapper = createElement(
|
|
460
453
|
"div",
|
|
461
|
-
"persona-flex persona-flex-col persona-gap-3"
|
|
454
|
+
"persona-widget-messages persona-flex persona-flex-col persona-gap-3"
|
|
462
455
|
);
|
|
463
456
|
const transcriptSuggestions = createSuggestionHost("followUp");
|
|
464
457
|
|
|
465
|
-
const contentMaxWidth = config
|
|
458
|
+
const contentMaxWidth = resolveContentMaxWidth(config, false);
|
|
466
459
|
if (contentMaxWidth) {
|
|
467
460
|
messagesWrapper.style.maxWidth = contentMaxWidth;
|
|
468
461
|
messagesWrapper.style.marginLeft = "auto";
|
|
@@ -470,13 +463,12 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
470
463
|
messagesWrapper.style.width = "100%";
|
|
471
464
|
}
|
|
472
465
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
body.append(introCard, messagesWrapper, transcriptSuggestions);
|
|
466
|
+
body.append(
|
|
467
|
+
welcomeHost,
|
|
468
|
+
greetingHost,
|
|
469
|
+
messagesWrapper,
|
|
470
|
+
transcriptSuggestions
|
|
471
|
+
);
|
|
480
472
|
|
|
481
473
|
// composer-bar mode early-returned above with its own pill builder; this
|
|
482
474
|
// path always uses the standard column-stacked composer card.
|
|
@@ -535,6 +527,9 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
535
527
|
micButtonWrapper: composerElements.micButtonWrapper,
|
|
536
528
|
composerForm: composerElements.composerForm,
|
|
537
529
|
statusText: composerElements.statusText,
|
|
530
|
+
welcomeHost,
|
|
531
|
+
welcomeIconHolder: welcome.iconHolder,
|
|
532
|
+
greetingHost,
|
|
538
533
|
introTitle,
|
|
539
534
|
introSubtitle,
|
|
540
535
|
closeButton: headerElements.closeButton,
|
|
@@ -198,6 +198,46 @@ describe("per-item icon color", () => {
|
|
|
198
198
|
});
|
|
199
199
|
});
|
|
200
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
|
+
|
|
201
241
|
describe("legacy suggestionChipsConfig scoping", () => {
|
|
202
242
|
const chipsConfig = {
|
|
203
243
|
fontFamily: "sans-serif" as const,
|