@runtypelabs/persona 3.34.1 → 3.35.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 +11 -11
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
- package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +10 -10
- package/dist/index.cjs +46 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +514 -158
- package/dist/index.d.ts +514 -158
- package/dist/index.global.js +41 -89
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -44
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +53 -0
- package/dist/plugin-kit.d.cts +4 -4
- package/dist/plugin-kit.d.ts +4 -4
- package/dist/runtype-tts.js +1 -0
- package/dist/smart-dom-reader.cjs +1 -1
- package/dist/smart-dom-reader.d.cts +352 -103
- package/dist/smart-dom-reader.d.ts +352 -103
- package/dist/smart-dom-reader.js +1 -1
- package/dist/theme-editor-preview.cjs +203 -0
- package/dist/theme-editor-preview.d.cts +5748 -0
- package/dist/theme-editor-preview.d.ts +5748 -0
- package/dist/theme-editor-preview.js +203 -0
- package/dist/theme-editor.cjs +14 -131
- package/dist/theme-editor.d.cts +362 -574
- package/dist/theme-editor.d.ts +362 -574
- package/dist/theme-editor.js +14 -131
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1 -1
- package/dist/theme-reference.d.ts +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +80 -0
- package/dist/voice-worklet-player.d.cts +215 -0
- package/dist/voice-worklet-player.d.ts +215 -0
- package/dist/voice-worklet-player.js +80 -0
- package/dist/widget.css +54 -36
- package/package.json +16 -2
- package/src/animations/glyph-cycle.ts +11 -14
- package/src/animations/wipe.ts +1 -1
- package/src/ask-user-question-tool.test.ts +1 -1
- package/src/ask-user-question-tool.ts +6 -8
- package/src/client.test.ts +12 -12
- package/src/client.ts +30 -31
- package/src/codegen.test.ts +1 -1
- package/src/codegen.ts +1 -1
- package/src/components/approval-bubble.test.ts +1 -1
- package/src/components/approval-bubble.ts +3 -3
- package/src/components/artifact-pane.ts +3 -3
- package/src/components/ask-user-question-bubble.test.ts +14 -14
- package/src/components/ask-user-question-bubble.ts +10 -10
- package/src/components/composer-builder.ts +17 -16
- package/src/components/composer-parts.test.ts +54 -0
- package/src/components/composer-parts.ts +95 -126
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +7 -7
- package/src/components/event-stream-view.ts +5 -5
- package/src/components/header-builder.ts +33 -27
- package/src/components/header-layouts.ts +1 -1
- package/src/components/header-parts.test.ts +90 -0
- package/src/components/header-parts.ts +62 -93
- package/src/components/launcher.test.ts +11 -0
- package/src/components/launcher.ts +1 -1
- package/src/components/message-bubble.test.ts +74 -2
- package/src/components/message-bubble.ts +25 -11
- package/src/components/messages.ts +2 -2
- package/src/components/panel.test.ts +1 -1
- package/src/components/panel.ts +92 -97
- package/src/components/pill-composer-builder.ts +28 -24
- package/src/components/reasoning-bubble.ts +2 -2
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +1 -1
- package/src/components/widget-view.test.ts +181 -0
- package/src/components/widget-view.ts +234 -0
- package/src/defaults.ts +54 -51
- package/src/generated/runtype-openapi-contract.ts +5 -0
- package/src/index-core.ts +30 -4
- package/src/index-global.ts +73 -6
- package/src/index.ts +7 -1
- package/src/install.test.ts +11 -11
- package/src/install.ts +28 -18
- package/src/launcher-global.ts +25 -14
- package/src/markdown-parsers-eager.ts +20 -0
- package/src/markdown-parsers-entry.ts +4 -0
- package/src/markdown-parsers-loader.ts +49 -0
- package/src/plugin-kit.test.ts +1 -1
- package/src/plugin-kit.ts +7 -7
- package/src/plugins/types.ts +5 -5
- package/src/postprocessors.ts +27 -14
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +4 -5
- package/src/runtime/init.ts +2 -2
- package/src/session.test.ts +5 -6
- package/src/session.ts +333 -227
- package/src/session.voice.test.ts +146 -0
- package/src/session.webmcp.test.ts +24 -25
- package/src/smart-dom-reader.ts +3 -4
- package/src/styles/widget.css +54 -36
- package/src/suggest-replies-tool.test.ts +3 -3
- package/src/suggest-replies-tool.ts +5 -5
- package/src/testing/index.ts +1 -1
- package/src/theme-editor/color-utils.ts +2 -2
- package/src/theme-editor/index.ts +4 -13
- package/src/theme-editor/presets.ts +1 -1
- package/src/theme-editor/preview-utils.ts +22 -20
- package/src/theme-editor/preview.ts +4 -1
- package/src/theme-editor/role-mappings.ts +3 -3
- package/src/theme-editor/sections.ts +21 -20
- package/src/theme-editor/types.ts +1 -1
- package/src/theme-editor/webmcp/index.ts +1 -1
- package/src/theme-editor/webmcp/summary.ts +1 -2
- package/src/theme-editor/webmcp/tools.ts +30 -30
- package/src/theme-editor/webmcp/types.ts +2 -2
- package/src/theme-editor-preview.ts +10 -0
- package/src/theme-reference.ts +26 -26
- package/src/types.ts +368 -99
- package/src/ui.approval-plugin.test.ts +3 -3
- package/src/ui.ask-user-question-plugin.test.ts +9 -9
- package/src/ui.component-directive.test.ts +4 -4
- package/src/ui.composer-bar.test.ts +10 -11
- package/src/ui.composer-keyboard.test.ts +5 -5
- package/src/ui.header-icon-color.test.ts +59 -0
- package/src/ui.scroll.test.ts +5 -6
- package/src/ui.stop-button.test.ts +3 -4
- package/src/ui.suggest-replies.test.ts +2 -2
- package/src/ui.ts +292 -131
- package/src/utils/buttons.ts +1 -1
- package/src/utils/code-generators.ts +1 -1
- package/src/utils/composer-history.ts +3 -3
- package/src/utils/dom-context.test.ts +2 -2
- package/src/utils/dom-context.ts +7 -7
- package/src/utils/dom.test.ts +67 -0
- package/src/utils/dom.ts +75 -0
- package/src/utils/dropdown.ts +2 -2
- package/src/utils/event-stream-buffer.test.ts +2 -2
- package/src/utils/message-fingerprint.test.ts +9 -0
- package/src/utils/message-fingerprint.ts +5 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/sanitize.ts +32 -16
- package/src/utils/sequence-buffer.test.ts +8 -8
- package/src/utils/sequence-buffer.ts +4 -4
- package/src/utils/smart-dom-adapter.test.ts +1 -1
- package/src/utils/smart-dom-adapter.ts +4 -5
- package/src/utils/speech-text.test.ts +122 -0
- package/src/utils/speech-text.ts +101 -0
- package/src/utils/stream-animation.test.ts +3 -3
- package/src/utils/stream-animation.ts +6 -6
- package/src/utils/theme.test.ts +1 -1
- package/src/utils/throughput-tracker.test.ts +11 -11
- package/src/utils/throughput-tracker.ts +9 -9
- package/src/utils/tokens.ts +13 -13
- package/src/utils/virtual-scroller.ts +1 -1
- package/src/vendor/smart-dom-reader/README.md +4 -4
- package/src/vendor/smart-dom-reader/index.d.ts +1 -1
- package/src/vendor/smart-dom-reader/index.js +2 -2
- package/src/version.ts +1 -1
- package/src/voice/audio-playback-manager.test.ts +159 -0
- package/src/voice/audio-playback-manager.ts +145 -30
- package/src/voice/browser-speech-engine.ts +130 -0
- package/src/voice/fallback-speech-engine.ts +89 -0
- package/src/voice/index.ts +21 -0
- package/src/voice/read-aloud-controller.test.ts +143 -0
- package/src/voice/read-aloud-controller.ts +136 -0
- package/src/voice/runtype-speech-engine.test.ts +280 -0
- package/src/voice/runtype-speech-engine.ts +211 -0
- package/src/voice/runtype-tts-entry.ts +10 -0
- package/src/voice/runtype-tts-loader.test.ts +47 -0
- package/src/voice/runtype-tts-loader.ts +42 -0
- package/src/voice/runtype-voice-provider.ts +332 -537
- package/src/voice/voice-factory.ts +26 -4
- package/src/voice/voice.test.ts +321 -7
- package/src/voice/worklet-playback-engine.ts +272 -0
- package/src/voice-worklet-player.ts +39 -0
- package/src/webmcp-bridge.test.ts +6 -6
- package/src/webmcp-bridge.ts +24 -25
- package/src/webmcp-polyfill.ts +1 -1
- package/src/voice/voice-activity-detector.ts +0 -90
|
@@ -815,7 +815,7 @@ describe("createEventStreamView", () => {
|
|
|
815
815
|
const buffer = createMockBuffer(events);
|
|
816
816
|
const { element, update } = createEventStreamView({ buffer: buffer as any });
|
|
817
817
|
|
|
818
|
-
// Initial render (Path A
|
|
818
|
+
// Initial render (Path A: first render)
|
|
819
819
|
update();
|
|
820
820
|
|
|
821
821
|
const eventsList = getEventsList(element);
|
|
@@ -825,7 +825,7 @@ describe("createEventStreamView", () => {
|
|
|
825
825
|
const row1Ref = eventsList.children[0];
|
|
826
826
|
const row2Ref = eventsList.children[1];
|
|
827
827
|
|
|
828
|
-
// Add a new event and update (Path C
|
|
828
|
+
// Add a new event and update (Path C: incremental append)
|
|
829
829
|
vi.advanceTimersByTime(150);
|
|
830
830
|
buffer.push(makeEvent("step_chunk", 3));
|
|
831
831
|
update();
|
|
@@ -1173,7 +1173,7 @@ describe("createEventStreamView", () => {
|
|
|
1173
1173
|
// Wait past the throttle interval
|
|
1174
1174
|
vi.advanceTimersByTime(150);
|
|
1175
1175
|
|
|
1176
|
-
// Add event and update
|
|
1176
|
+
// Add event and update: should render immediately since 150ms > 100ms
|
|
1177
1177
|
buffer.push(makeEvent("flow_complete", 2));
|
|
1178
1178
|
update();
|
|
1179
1179
|
|
|
@@ -1223,7 +1223,7 @@ describe("createEventStreamView", () => {
|
|
|
1223
1223
|
const filterSelect = getFilterSelect(element);
|
|
1224
1224
|
const copyAllBtn = getCopyAllBtn(element);
|
|
1225
1225
|
|
|
1226
|
-
// Immediately change filter
|
|
1226
|
+
// Immediately change filter: this should bypass throttle (uses updateNow internally)
|
|
1227
1227
|
filterSelect.value = "step_chunk";
|
|
1228
1228
|
filterSelect.__fireEvent("change");
|
|
1229
1229
|
|
|
@@ -1237,17 +1237,17 @@ describe("createEventStreamView", () => {
|
|
|
1237
1237
|
const buffer = createMockBuffer([makeEvent("step_chunk", 1)]);
|
|
1238
1238
|
const { update, destroy } = createEventStreamView({ buffer: buffer as any });
|
|
1239
1239
|
|
|
1240
|
-
// First update
|
|
1240
|
+
// First update: immediate
|
|
1241
1241
|
update();
|
|
1242
1242
|
|
|
1243
1243
|
// Schedule a throttled update
|
|
1244
1244
|
buffer.push(makeEvent("step_chunk", 2));
|
|
1245
1245
|
update();
|
|
1246
1246
|
|
|
1247
|
-
// Destroy before rAF fires
|
|
1247
|
+
// Destroy before rAF fires: should not throw
|
|
1248
1248
|
expect(() => destroy()).not.toThrow();
|
|
1249
1249
|
|
|
1250
|
-
// Advancing timers to flush rAF
|
|
1250
|
+
// Advancing timers to flush rAF: should not throw even though view is destroyed
|
|
1251
1251
|
vi.advanceTimersByTime(20);
|
|
1252
1252
|
vi.useRealTimers();
|
|
1253
1253
|
});
|
|
@@ -568,7 +568,7 @@ export function createEventStreamView(
|
|
|
568
568
|
);
|
|
569
569
|
throughputValueEl.textContent = "-- tok/s";
|
|
570
570
|
|
|
571
|
-
// Custom hover tooltip
|
|
571
|
+
// Custom hover tooltip: appears instantly (no native title delay).
|
|
572
572
|
// Appended to the (non-clipping) toolbar wrapper rather than the header
|
|
573
573
|
// bar, which has overflow-hidden and would clip a dropdown. Position is
|
|
574
574
|
// measured on hover so it sits just under the throughput group.
|
|
@@ -899,7 +899,7 @@ export function createEventStreamView(
|
|
|
899
899
|
expandedSet.add(eventId);
|
|
900
900
|
}
|
|
901
901
|
dirtyExpandId = eventId;
|
|
902
|
-
// Save scroll position
|
|
902
|
+
// Save scroll position: user-initiated expand/collapse should not auto-scroll
|
|
903
903
|
const savedScrollTop = eventsList.scrollTop;
|
|
904
904
|
const wasAutoFollowing = autoFollow.isFollowing();
|
|
905
905
|
suppressScrollHandler = true;
|
|
@@ -993,7 +993,7 @@ export function createEventStreamView(
|
|
|
993
993
|
const isFirstRender = rowElements.size === 0 && filteredEvents.length > 0;
|
|
994
994
|
|
|
995
995
|
if (filterChanged || isFirstRender || filteredEvents.length === 0) {
|
|
996
|
-
// Path A
|
|
996
|
+
// Path A: Full rebuild (filter/search changed, first render, or empty list)
|
|
997
997
|
eventsList.innerHTML = "";
|
|
998
998
|
rowElements.clear();
|
|
999
999
|
const fragment = document.createDocumentFragment();
|
|
@@ -1016,7 +1016,7 @@ export function createEventStreamView(
|
|
|
1016
1016
|
lastRenderedSearch = searchTerm;
|
|
1017
1017
|
dirtyExpandId = null;
|
|
1018
1018
|
} else {
|
|
1019
|
-
// Path B
|
|
1019
|
+
// Path B: Single row replace (expand/collapse)
|
|
1020
1020
|
if (dirtyExpandId !== null) {
|
|
1021
1021
|
const oldRow = rowElements.get(dirtyExpandId);
|
|
1022
1022
|
if (oldRow && oldRow.parentNode === eventsList) {
|
|
@@ -1043,7 +1043,7 @@ export function createEventStreamView(
|
|
|
1043
1043
|
dirtyExpandId = null;
|
|
1044
1044
|
}
|
|
1045
1045
|
|
|
1046
|
-
// Path C
|
|
1046
|
+
// Path C: Incremental append (default streaming path)
|
|
1047
1047
|
// Remove evicted rows
|
|
1048
1048
|
const activeIds = new Set(filteredEvents.map((e) => e.id));
|
|
1049
1049
|
for (const [id, el] of rowElements) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement } from "../utils/dom";
|
|
1
|
+
import { createElement, createNode } from "../utils/dom";
|
|
2
2
|
import { renderLucideIcon } from "../utils/icons";
|
|
3
3
|
import { AgentWidgetConfig } from "../types";
|
|
4
4
|
import { createCloseButton, createClearChatButton } from "./header-parts";
|
|
@@ -38,15 +38,18 @@ export interface HeaderBuildContext {
|
|
|
38
38
|
export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
39
39
|
const { config, showClose = true } = context;
|
|
40
40
|
|
|
41
|
-
const header =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
const header = createNode("div", {
|
|
42
|
+
className:
|
|
43
|
+
"persona-widget-header persona-flex persona-items-center persona-gap-3 persona-px-6 persona-py-5",
|
|
44
|
+
attrs: { "data-persona-theme-zone": "header" },
|
|
45
|
+
style: {
|
|
46
|
+
backgroundColor: "var(--persona-header-bg, var(--persona-surface, #ffffff))",
|
|
47
|
+
borderBottomColor: "var(--persona-header-border, var(--persona-divider, #f1f5f9))",
|
|
48
|
+
boxShadow: "var(--persona-header-shadow, none)",
|
|
49
|
+
borderBottom:
|
|
50
|
+
"var(--persona-header-border-bottom, 1px solid var(--persona-header-border, var(--persona-divider, #f1f5f9)))",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
50
53
|
|
|
51
54
|
const launcher = config?.launcher ?? {};
|
|
52
55
|
const headerIconSize = launcher.headerIconSize ?? "48px";
|
|
@@ -54,16 +57,16 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
54
57
|
const headerIconHidden = launcher.headerIconHidden ?? false;
|
|
55
58
|
const headerIconName = launcher.headerIconName;
|
|
56
59
|
|
|
57
|
-
const iconHolder =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
const iconHolder = createNode("div", {
|
|
61
|
+
className:
|
|
62
|
+
"persona-flex persona-items-center persona-justify-center persona-rounded-xl persona-text-xl",
|
|
63
|
+
style: {
|
|
64
|
+
height: headerIconSize,
|
|
65
|
+
width: headerIconSize,
|
|
66
|
+
backgroundColor: "var(--persona-header-icon-bg, var(--persona-primary, #0f0f0f))",
|
|
67
|
+
color: "var(--persona-header-icon-fg, var(--persona-text-inverse, #ffffff))",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
67
70
|
|
|
68
71
|
// Render icon based on priority: Lucide icon > iconUrl > agentIconText
|
|
69
72
|
if (!headerIconHidden) {
|
|
@@ -93,13 +96,16 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
|
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
const headerCopy = createElement("div", "persona-flex persona-flex-col persona-flex-1 persona-min-w-0");
|
|
96
|
-
const title =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
subtitle
|
|
102
|
-
|
|
99
|
+
const title = createNode("span", {
|
|
100
|
+
className: "persona-text-base persona-font-semibold",
|
|
101
|
+
text: config?.launcher?.title ?? "Chat Assistant",
|
|
102
|
+
style: { color: HEADER_THEME_CSS.titleColor },
|
|
103
|
+
});
|
|
104
|
+
const subtitle = createNode("span", {
|
|
105
|
+
className: "persona-text-xs",
|
|
106
|
+
text: config?.launcher?.subtitle ?? "Here to help you get answers fast",
|
|
107
|
+
style: { color: HEADER_THEME_CSS.subtitleColor },
|
|
108
|
+
});
|
|
103
109
|
|
|
104
110
|
headerCopy.append(title, subtitle);
|
|
105
111
|
|
|
@@ -121,7 +121,7 @@ export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
|
121
121
|
header.style.borderBottom =
|
|
122
122
|
'var(--persona-header-border-bottom, 1px solid var(--persona-header-border, var(--persona-divider, #f1f5f9)))';
|
|
123
123
|
|
|
124
|
-
// Build the title area
|
|
124
|
+
// Build the title area: either a combo button (titleMenu) or standard title row
|
|
125
125
|
const titleMenuConfig = layoutHeaderConfig?.titleMenu;
|
|
126
126
|
let titleRow: HTMLElement;
|
|
127
127
|
let headerTitle: HTMLElement;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { createCloseButton, createClearChatButton } from "./header-parts";
|
|
5
|
+
import type { AgentWidgetConfig } from "../types";
|
|
6
|
+
|
|
7
|
+
const baseConfig: AgentWidgetConfig = { apiUrl: "/api" };
|
|
8
|
+
|
|
9
|
+
describe("createCloseButton", () => {
|
|
10
|
+
it("keeps the default utility classes when no style overrides are set", () => {
|
|
11
|
+
const { button } = createCloseButton(baseConfig, {});
|
|
12
|
+
expect(button.tagName).toBe("BUTTON");
|
|
13
|
+
expect(button.type).toBe("button");
|
|
14
|
+
expect(button.getAttribute("aria-label")).toBe("Close chat");
|
|
15
|
+
expect(button.classList.contains("persona-rounded-full")).toBe(true);
|
|
16
|
+
expect(button.classList.contains("persona-border-none")).toBe(true);
|
|
17
|
+
expect(button.classList.contains("hover:persona-bg-gray-100")).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("hides the button when showClose is false", () => {
|
|
21
|
+
const { button } = createCloseButton(baseConfig, { showClose: false });
|
|
22
|
+
expect(button.style.display).toBe("none");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("drops each default class and sets the inline style for the matching override", () => {
|
|
26
|
+
const { button } = createCloseButton(
|
|
27
|
+
{
|
|
28
|
+
...baseConfig,
|
|
29
|
+
launcher: {
|
|
30
|
+
closeButtonBackgroundColor: "rgb(1, 2, 3)",
|
|
31
|
+
closeButtonBorderRadius: "4px",
|
|
32
|
+
closeButtonBorderWidth: "2px",
|
|
33
|
+
closeButtonBorderColor: "rgb(4, 5, 6)",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{}
|
|
37
|
+
);
|
|
38
|
+
expect(button.classList.contains("hover:persona-bg-gray-100")).toBe(false);
|
|
39
|
+
expect(button.style.backgroundColor).toBe("rgb(1, 2, 3)");
|
|
40
|
+
expect(button.classList.contains("persona-rounded-full")).toBe(false);
|
|
41
|
+
expect(button.style.borderRadius).toBe("4px");
|
|
42
|
+
expect(button.classList.contains("persona-border-none")).toBe(false);
|
|
43
|
+
expect(button.style.borderWidth).toBe("2px");
|
|
44
|
+
expect(button.style.borderStyle).toBe("solid");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("defaults border width to 0px when only border color is provided", () => {
|
|
48
|
+
const { button } = createCloseButton(
|
|
49
|
+
{ ...baseConfig, launcher: { closeButtonBorderColor: "rgb(7, 8, 9)" } },
|
|
50
|
+
{}
|
|
51
|
+
);
|
|
52
|
+
expect(button.classList.contains("persona-border-none")).toBe(false);
|
|
53
|
+
expect(button.style.borderWidth).toBe("0px");
|
|
54
|
+
expect(button.style.borderStyle).toBe("solid");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("createClearChatButton", () => {
|
|
59
|
+
it("keeps the default utility classes when no style overrides are set", () => {
|
|
60
|
+
const { button } = createClearChatButton(baseConfig, {});
|
|
61
|
+
expect(button.type).toBe("button");
|
|
62
|
+
expect(button.classList.contains("persona-rounded-full")).toBe(true);
|
|
63
|
+
expect(button.classList.contains("persona-border-none")).toBe(true);
|
|
64
|
+
expect(button.classList.contains("hover:persona-bg-gray-100")).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("drops each default class and sets the inline style for the matching override", () => {
|
|
68
|
+
const { button } = createClearChatButton(
|
|
69
|
+
{
|
|
70
|
+
...baseConfig,
|
|
71
|
+
launcher: {
|
|
72
|
+
clearChat: {
|
|
73
|
+
backgroundColor: "rgb(1, 2, 3)",
|
|
74
|
+
borderRadius: "4px",
|
|
75
|
+
borderWidth: "2px",
|
|
76
|
+
borderColor: "rgb(4, 5, 6)",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{}
|
|
81
|
+
);
|
|
82
|
+
expect(button.classList.contains("hover:persona-bg-gray-100")).toBe(false);
|
|
83
|
+
expect(button.style.backgroundColor).toBe("rgb(1, 2, 3)");
|
|
84
|
+
expect(button.classList.contains("persona-rounded-full")).toBe(false);
|
|
85
|
+
expect(button.style.borderRadius).toBe("4px");
|
|
86
|
+
expect(button.classList.contains("persona-border-none")).toBe(false);
|
|
87
|
+
expect(button.style.borderWidth).toBe("2px");
|
|
88
|
+
expect(button.style.borderStyle).toBe("solid");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement, createElementInDocument } from "../utils/dom";
|
|
1
|
+
import { createElement, createElementInDocument, createNode, cx } from "../utils/dom";
|
|
2
2
|
import { renderLucideIcon } from "../utils/icons";
|
|
3
3
|
import { AgentWidgetConfig } from "../types";
|
|
4
4
|
import { PORTALED_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
@@ -25,14 +25,14 @@ export interface CreateCloseButtonOptions {
|
|
|
25
25
|
/**
|
|
26
26
|
* Explicit button-size override that wins over `launcher.closeButtonSize`.
|
|
27
27
|
* Use when the call site has its own opinion about the size that should
|
|
28
|
-
* take precedence over the global launcher config
|
|
28
|
+
* take precedence over the global launcher config: e.g. composer-bar's
|
|
29
29
|
* minimal close icon, where size is part of the mode's UX, not something
|
|
30
30
|
* that should inherit from the floating launcher's button size.
|
|
31
31
|
*/
|
|
32
32
|
buttonSize?: string;
|
|
33
33
|
/**
|
|
34
34
|
* Override the rendered icon size (default: "28px"). Pair with
|
|
35
|
-
* `buttonSize` when scaling the whole control down
|
|
35
|
+
* `buttonSize` when scaling the whole control down: otherwise the
|
|
36
36
|
* 28px icon will overflow a smaller button.
|
|
37
37
|
*/
|
|
38
38
|
iconSize?: string;
|
|
@@ -85,24 +85,41 @@ export const createCloseButton = (
|
|
|
85
85
|
|
|
86
86
|
const wrapper = createElement("div", wrapperClassName);
|
|
87
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
88
|
const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
|
|
97
89
|
const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
|
|
98
|
-
|
|
99
|
-
button.setAttribute("aria-label", closeButtonTooltipText);
|
|
100
|
-
button.style.display = showClose ? "" : "none";
|
|
101
|
-
|
|
102
90
|
const closeButtonIconName = launcher.closeButtonIconName ?? "x";
|
|
103
91
|
const closeButtonIconText = launcher.closeButtonIconText ?? "×";
|
|
104
|
-
|
|
105
|
-
|
|
92
|
+
// hover-bg / border-none / rounded-full are default utility classes that
|
|
93
|
+
// apply only when the matching style override is absent; an override sets
|
|
94
|
+
// the inline style instead (cx omits the class).
|
|
95
|
+
const hasCloseBorder = Boolean(
|
|
96
|
+
launcher.closeButtonBorderWidth || launcher.closeButtonBorderColor
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const button = createNode("button", {
|
|
100
|
+
className: cx(
|
|
101
|
+
"persona-inline-flex persona-items-center persona-justify-center persona-cursor-pointer",
|
|
102
|
+
!launcher.closeButtonBackgroundColor && "hover:persona-bg-gray-100",
|
|
103
|
+
!hasCloseBorder && "persona-border-none",
|
|
104
|
+
!launcher.closeButtonBorderRadius && "persona-rounded-full"
|
|
105
|
+
),
|
|
106
|
+
attrs: { type: "button", "aria-label": closeButtonTooltipText },
|
|
107
|
+
style: {
|
|
108
|
+
height: closeButtonSize,
|
|
109
|
+
width: closeButtonSize,
|
|
110
|
+
display: showClose ? undefined : "none",
|
|
111
|
+
color: launcher.closeButtonColor || HEADER_THEME_CSS.actionIconColor,
|
|
112
|
+
backgroundColor: launcher.closeButtonBackgroundColor || undefined,
|
|
113
|
+
border: hasCloseBorder
|
|
114
|
+
? `${launcher.closeButtonBorderWidth || "0px"} solid ${launcher.closeButtonBorderColor || "transparent"}`
|
|
115
|
+
: undefined,
|
|
116
|
+
borderRadius: launcher.closeButtonBorderRadius || undefined,
|
|
117
|
+
paddingLeft: launcher.closeButtonPaddingX || undefined,
|
|
118
|
+
paddingRight: launcher.closeButtonPaddingX || undefined,
|
|
119
|
+
paddingTop: launcher.closeButtonPaddingY || undefined,
|
|
120
|
+
paddingBottom: launcher.closeButtonPaddingY || undefined,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
106
123
|
|
|
107
124
|
// The X glyph's paths occupy only the middle 50% of its 24x24 viewBox
|
|
108
125
|
// (from 6,6 to 18,18), while other header icons (e.g. refresh-cw) span
|
|
@@ -118,47 +135,6 @@ export const createCloseButton = (
|
|
|
118
135
|
button.textContent = closeButtonIconText;
|
|
119
136
|
}
|
|
120
137
|
|
|
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
138
|
wrapper.appendChild(button);
|
|
163
139
|
|
|
164
140
|
if (closeButtonShowTooltip && closeButtonTooltipText) {
|
|
@@ -259,15 +235,34 @@ export const createClearChatButton = (
|
|
|
259
235
|
|
|
260
236
|
const wrapper = createElement("div", wrapperClassName);
|
|
261
237
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
button
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
238
|
+
// hover-bg / border-none / rounded-full are default utility classes that
|
|
239
|
+
// apply only when the matching style override is absent; an override sets
|
|
240
|
+
// the inline style instead (cx omits the class).
|
|
241
|
+
const hasClearChatBorder = Boolean(clearChatBorderWidth || clearChatBorderColor);
|
|
242
|
+
|
|
243
|
+
const button = createNode("button", {
|
|
244
|
+
className: cx(
|
|
245
|
+
"persona-inline-flex persona-items-center persona-justify-center persona-cursor-pointer",
|
|
246
|
+
!clearChatBgColor && "hover:persona-bg-gray-100",
|
|
247
|
+
!hasClearChatBorder && "persona-border-none",
|
|
248
|
+
!clearChatBorderRadius && "persona-rounded-full"
|
|
249
|
+
),
|
|
250
|
+
attrs: { type: "button", "aria-label": clearChatTooltipText },
|
|
251
|
+
style: {
|
|
252
|
+
height: clearChatSize,
|
|
253
|
+
width: clearChatSize,
|
|
254
|
+
color: clearChatIconColor || HEADER_THEME_CSS.actionIconColor,
|
|
255
|
+
backgroundColor: clearChatBgColor || undefined,
|
|
256
|
+
border: hasClearChatBorder
|
|
257
|
+
? `${clearChatBorderWidth || "0px"} solid ${clearChatBorderColor || "transparent"}`
|
|
258
|
+
: undefined,
|
|
259
|
+
borderRadius: clearChatBorderRadius || undefined,
|
|
260
|
+
paddingLeft: clearChatPaddingX || undefined,
|
|
261
|
+
paddingRight: clearChatPaddingX || undefined,
|
|
262
|
+
paddingTop: clearChatPaddingY || undefined,
|
|
263
|
+
paddingBottom: clearChatPaddingY || undefined,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
271
266
|
|
|
272
267
|
const iconSvg = renderLucideIcon(clearChatIconName, iconSize, "currentColor", 1);
|
|
273
268
|
if (iconSvg) {
|
|
@@ -275,32 +270,6 @@ export const createClearChatButton = (
|
|
|
275
270
|
button.appendChild(iconSvg);
|
|
276
271
|
}
|
|
277
272
|
|
|
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
273
|
wrapper.appendChild(button);
|
|
305
274
|
|
|
306
275
|
if (clearChatShowTooltip && clearChatTooltipText) {
|
|
@@ -5,6 +5,17 @@ import { createLauncherButton } from "./launcher";
|
|
|
5
5
|
import { DEFAULT_WIDGET_CONFIG } from "../defaults";
|
|
6
6
|
|
|
7
7
|
describe("createLauncherButton", () => {
|
|
8
|
+
it("uses the package default subtitle when no subtitle is configured", () => {
|
|
9
|
+
const { element, update } = createLauncherButton(undefined, () => {});
|
|
10
|
+
update({});
|
|
11
|
+
const subtitleEl = element.querySelector("[data-role='launcher-subtitle']");
|
|
12
|
+
expect(subtitleEl?.textContent).toBe("Here to help you get answers fast");
|
|
13
|
+
expect(subtitleEl?.getAttribute("title")).toBe(
|
|
14
|
+
"Here to help you get answers fast"
|
|
15
|
+
);
|
|
16
|
+
element.remove();
|
|
17
|
+
});
|
|
18
|
+
|
|
8
19
|
it("applies collapsedMaxWidth when set", () => {
|
|
9
20
|
const { element, update } = createLauncherButton(undefined, () => {});
|
|
10
21
|
update({
|
|
@@ -41,7 +41,7 @@ export const createLauncherButton = (
|
|
|
41
41
|
|
|
42
42
|
const subtitleEl = button.querySelector("[data-role='launcher-subtitle']");
|
|
43
43
|
if (subtitleEl) {
|
|
44
|
-
const s = launcher.subtitle ?? "
|
|
44
|
+
const s = launcher.subtitle ?? "Here to help you get answers fast";
|
|
45
45
|
subtitleEl.textContent = s;
|
|
46
46
|
subtitleEl.setAttribute("title", s);
|
|
47
47
|
}
|
|
@@ -155,7 +155,7 @@ describe("resolveStopReasonNoticeText", () => {
|
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
describe("createStandardBubble
|
|
158
|
+
describe("createStandardBubble: stopReason notice", () => {
|
|
159
159
|
const renderWithStopReason = (
|
|
160
160
|
overrides: Partial<AgentWidgetMessage>,
|
|
161
161
|
widgetConfig?: Partial<AgentWidgetConfig>
|
|
@@ -333,7 +333,7 @@ describe("isSafeMediaSrc", () => {
|
|
|
333
333
|
});
|
|
334
334
|
});
|
|
335
335
|
|
|
336
|
-
describe("createStandardBubble
|
|
336
|
+
describe("createStandardBubble: audio/video/file content parts", () => {
|
|
337
337
|
it("renders an <audio> element with controls for an audio content part", () => {
|
|
338
338
|
const bubble = createStandardBubble(
|
|
339
339
|
makeMessage({
|
|
@@ -466,3 +466,75 @@ describe("createStandardBubble — audio/video/file content parts", () => {
|
|
|
466
466
|
expect(link?.getAttribute("href")).toBe("https://example.com/report.pdf");
|
|
467
467
|
});
|
|
468
468
|
});
|
|
469
|
+
|
|
470
|
+
describe("createStandardBubble: timestamp position", () => {
|
|
471
|
+
it("tucks an inline timestamp inside the last content block so it trails the text", () => {
|
|
472
|
+
const bubble = createStandardBubble(
|
|
473
|
+
makeMessage({ content: "Hello there" }),
|
|
474
|
+
({ text }) => `<p>${text}</p>`,
|
|
475
|
+
{ timestamp: { show: true, position: "inline" } }
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
// Markdown content is block-wrapped; the inline timestamp must live inside
|
|
479
|
+
// that block (trailing the text), not as a block sibling under the bubble.
|
|
480
|
+
const inline = bubble.querySelector("p .persona-timestamp-inline");
|
|
481
|
+
expect(inline).not.toBeNull();
|
|
482
|
+
expect(inline?.textContent).toMatch(/\d/);
|
|
483
|
+
expect(Array.from(bubble.children)).not.toContain(inline);
|
|
484
|
+
// Must be an inline-level <span>; a <div> here is invalid inside a <p>.
|
|
485
|
+
expect(inline?.tagName).toBe("SPAN");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("uses an inline element that survives an innerHTML re-parse (idiomorph round-trip)", () => {
|
|
489
|
+
const bubble = createStandardBubble(
|
|
490
|
+
makeMessage({ content: "Hello there" }),
|
|
491
|
+
({ text }) => `<p>${text}</p>`,
|
|
492
|
+
{ timestamp: { show: true, position: "inline" } }
|
|
493
|
+
);
|
|
494
|
+
const content = bubble.querySelector(".persona-message-content") as HTMLElement;
|
|
495
|
+
|
|
496
|
+
// Re-serializing + re-parsing is what the re-render path does. A <div>
|
|
497
|
+
// inside a <p> would be auto-closed out to a sibling (leaving an empty
|
|
498
|
+
// <p>), dropping the timestamp onto its own line; a <span> stays put.
|
|
499
|
+
const serialized = content.innerHTML;
|
|
500
|
+
content.innerHTML = serialized;
|
|
501
|
+
|
|
502
|
+
expect(content.querySelector("p .persona-timestamp-inline")).not.toBeNull();
|
|
503
|
+
const emptyParagraphs = Array.from(content.querySelectorAll("p")).filter(
|
|
504
|
+
(p) => p.textContent === ""
|
|
505
|
+
);
|
|
506
|
+
expect(emptyParagraphs.length).toBe(0);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("falls back to the content container for plain-text (non-block) content", () => {
|
|
510
|
+
const bubble = createStandardBubble(
|
|
511
|
+
makeMessage({ content: "Hello there" }),
|
|
512
|
+
({ text }) => text,
|
|
513
|
+
{ timestamp: { show: true, position: "inline" } }
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const content = bubble.querySelector(".persona-message-content")!;
|
|
517
|
+
const inline = content.querySelector(".persona-timestamp-inline");
|
|
518
|
+
expect(inline).not.toBeNull();
|
|
519
|
+
expect(content.contains(inline)).toBe(true);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("renders a below timestamp as a block under the content (direct bubble child)", () => {
|
|
523
|
+
const bubble = createStandardBubble(
|
|
524
|
+
makeMessage({ content: "Hello there" }),
|
|
525
|
+
({ text }) => `<p>${text}</p>`,
|
|
526
|
+
{ timestamp: { show: true, position: "below" } }
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// "below" must not produce an inline timestamp, and the timestamp block is
|
|
530
|
+
// appended to the bubble (under the content), not nested in the content.
|
|
531
|
+
expect(bubble.querySelector(".persona-timestamp-inline")).toBeNull();
|
|
532
|
+
const below = Array.from(bubble.children).find(
|
|
533
|
+
(el) =>
|
|
534
|
+
el.classList.contains("persona-mt-1") &&
|
|
535
|
+
el.classList.contains("persona-text-xs")
|
|
536
|
+
);
|
|
537
|
+
expect(below).toBeDefined();
|
|
538
|
+
expect(below?.textContent).toMatch(/\d/);
|
|
539
|
+
});
|
|
540
|
+
});
|