@runtypelabs/persona 3.34.0 → 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 +67 -12
- package/src/client.ts +40 -32
- 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
|
@@ -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
|
+
});
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
* Default copy for the inline notice rendered when a turn ends with a
|
|
29
29
|
* non-natural stop reason. Deployers override per-reason via
|
|
30
30
|
* `config.copy.stopReasonNotice`. Returns `null` for natural completions
|
|
31
|
-
* (`end_turn`) and uninformative reasons (`unknown`)
|
|
31
|
+
* (`end_turn`) and uninformative reasons (`unknown`): those never render
|
|
32
32
|
* an affordance.
|
|
33
33
|
*/
|
|
34
34
|
export const getDefaultStopReasonNoticeCopy = (
|
|
@@ -62,7 +62,7 @@ export const resolveStopReasonNoticeText = (
|
|
|
62
62
|
): string | null => {
|
|
63
63
|
if (!stopReason) return null;
|
|
64
64
|
const fallback = getDefaultStopReasonNoticeCopy(stopReason);
|
|
65
|
-
// Reasons without a default (end_turn, unknown) never render
|
|
65
|
+
// Reasons without a default (end_turn, unknown) never render: overrides
|
|
66
66
|
// for those keys are intentionally ignored.
|
|
67
67
|
if (fallback === null) return null;
|
|
68
68
|
const override = overrides?.[stopReason];
|
|
@@ -498,10 +498,11 @@ const createAvatar = (
|
|
|
498
498
|
*/
|
|
499
499
|
const createTimestamp = (
|
|
500
500
|
message: AgentWidgetMessage,
|
|
501
|
-
timestampConfig: AgentWidgetTimestampConfig
|
|
501
|
+
timestampConfig: AgentWidgetTimestampConfig,
|
|
502
|
+
tagName: "div" | "span" = "div"
|
|
502
503
|
): HTMLElement => {
|
|
503
504
|
const timestamp = createElement(
|
|
504
|
-
|
|
505
|
+
tagName,
|
|
505
506
|
"persona-text-xs persona-text-persona-muted"
|
|
506
507
|
);
|
|
507
508
|
|
|
@@ -629,9 +630,10 @@ export const createMessageActions = (
|
|
|
629
630
|
const showCopy = actionsConfig.showCopy ?? true;
|
|
630
631
|
const showUpvote = actionsConfig.showUpvote ?? true;
|
|
631
632
|
const showDownvote = actionsConfig.showDownvote ?? true;
|
|
633
|
+
const showReadAloud = actionsConfig.showReadAloud ?? false;
|
|
632
634
|
|
|
633
635
|
// Don't render the container at all when no actions are visible
|
|
634
|
-
if (!showCopy && !showUpvote && !showDownvote) {
|
|
636
|
+
if (!showCopy && !showUpvote && !showDownvote && !showReadAloud) {
|
|
635
637
|
const empty = createElement("div");
|
|
636
638
|
empty.style.display = "none";
|
|
637
639
|
empty.id = `actions-${message.id}`;
|
|
@@ -686,6 +688,12 @@ export const createMessageActions = (
|
|
|
686
688
|
container.appendChild(createActionButton("copy", "Copy message", "copy"));
|
|
687
689
|
}
|
|
688
690
|
|
|
691
|
+
// Read-aloud button. Click handling and play/pause/resume state are managed
|
|
692
|
+
// in ui.ts (event delegation + ReadAloudController) so they survive morphs.
|
|
693
|
+
if (showReadAloud) {
|
|
694
|
+
container.appendChild(createActionButton("volume-2", "Read aloud", "read-aloud"));
|
|
695
|
+
}
|
|
696
|
+
|
|
689
697
|
// Upvote button
|
|
690
698
|
if (showUpvote) {
|
|
691
699
|
container.appendChild(createActionButton("thumbs-up", "Upvote", "upvote"));
|
|
@@ -768,7 +776,7 @@ export const createStandardBubble = (
|
|
|
768
776
|
? resolveStreamAnimationPlugin(streamAnimation.type, streamPluginOverrides)
|
|
769
777
|
: null;
|
|
770
778
|
// Stay in "streaming-animated" mode while the plugin reports in-flight
|
|
771
|
-
// work for this message
|
|
779
|
+
// work for this message: e.g. glyph-cycle's tick loops still walking
|
|
772
780
|
// through the tail after the last token arrived. Without this, the final
|
|
773
781
|
// non-animated render rips out the cycling spans mid-animation.
|
|
774
782
|
const pluginStillAnimating =
|
|
@@ -861,11 +869,17 @@ export const createStandardBubble = (
|
|
|
861
869
|
}
|
|
862
870
|
}
|
|
863
871
|
|
|
864
|
-
//
|
|
872
|
+
// Usung <span>, not <div> for timestamp
|
|
873
|
+
// because a block child of a <p> gets re-parented outside of it on re-render (idiomorph).
|
|
865
874
|
if (showTimestamp && timestampPosition === "inline" && message.createdAt) {
|
|
866
|
-
const timestamp = createTimestamp(message, timestampConfig
|
|
867
|
-
timestamp.classList.add("persona-
|
|
868
|
-
contentDiv.
|
|
875
|
+
const timestamp = createTimestamp(message, timestampConfig!, "span");
|
|
876
|
+
timestamp.classList.add("persona-timestamp-inline");
|
|
877
|
+
const lastBlock = contentDiv.lastElementChild;
|
|
878
|
+
if (lastBlock) {
|
|
879
|
+
lastBlock.appendChild(timestamp);
|
|
880
|
+
} else {
|
|
881
|
+
contentDiv.appendChild(timestamp);
|
|
882
|
+
}
|
|
869
883
|
}
|
|
870
884
|
|
|
871
885
|
if (imageParts.length > 0) {
|
|
@@ -930,7 +944,7 @@ export const createStandardBubble = (
|
|
|
930
944
|
: null;
|
|
931
945
|
|
|
932
946
|
// Add typing indicator (or skeleton placeholder) for streaming assistant
|
|
933
|
-
// messages. Check the buffered content
|
|
947
|
+
// messages. Check the buffered content: a plugin's `bufferContent` may
|
|
934
948
|
// hold back the first N chars (e.g. glyph-cycle waits for 50 chars), during
|
|
935
949
|
// which the bubble would otherwise appear empty.
|
|
936
950
|
//
|
|
@@ -34,7 +34,7 @@ export const renderMessages = (
|
|
|
34
34
|
if (!showReasoning) return;
|
|
35
35
|
bubble = createReasoningBubble(message, config);
|
|
36
36
|
} else if (isAskUserQuestionMessage(message)) {
|
|
37
|
-
// No transcript bubble
|
|
37
|
+
// No transcript bubble: the overlay sheet is the only question UI.
|
|
38
38
|
if (config?.features?.askUserQuestion?.enabled === false) return;
|
|
39
39
|
if (!message.agentMetadata?.askUserQuestionAnswered) {
|
|
40
40
|
if (message.toolCall?.id) liveAskToolIds.add(message.toolCall.id);
|
|
@@ -45,7 +45,7 @@ export const renderMessages = (
|
|
|
45
45
|
isSuggestRepliesMessage(message) &&
|
|
46
46
|
config?.features?.suggestReplies?.enabled !== false
|
|
47
47
|
) {
|
|
48
|
-
// No transcript bubble
|
|
48
|
+
// No transcript bubble: the chips above the composer are the only UI.
|
|
49
49
|
// When the feature is disabled the message falls through to the generic
|
|
50
50
|
// tool bubble below (and is never auto-resumed), making the parked
|
|
51
51
|
// execution visible instead of silently swallowed.
|
|
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
|
|
|
4
4
|
import { createWrapper } from "./panel";
|
|
5
5
|
import type { AgentWidgetConfig } from "../types";
|
|
6
6
|
|
|
7
|
-
describe("createWrapper
|
|
7
|
+
describe("createWrapper: composer-bar mode", () => {
|
|
8
8
|
it("marks the wrapper with composer-bar data-attrs and leaves geometry to updateOpenState", () => {
|
|
9
9
|
const config: AgentWidgetConfig = {
|
|
10
10
|
apiUrl: "/api",
|