@runtypelabs/persona 3.34.1 → 3.36.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 +56 -12
- package/src/client.ts +35 -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
|
@@ -70,6 +70,60 @@ describe("createSendButton", () => {
|
|
|
70
70
|
expect(send.button.textContent).toBe("Send");
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
it("applies configured icon-button styles and skips the primary-bg fallback class", () => {
|
|
74
|
+
const send = createSendButton({
|
|
75
|
+
...baseConfig,
|
|
76
|
+
sendButton: {
|
|
77
|
+
useIcon: true,
|
|
78
|
+
iconName: "send",
|
|
79
|
+
size: "48px",
|
|
80
|
+
backgroundColor: "rgb(1, 2, 3)",
|
|
81
|
+
textColor: "rgb(4, 5, 6)",
|
|
82
|
+
borderWidth: "2px",
|
|
83
|
+
borderColor: "rgb(7, 8, 9)",
|
|
84
|
+
paddingX: "6px",
|
|
85
|
+
paddingY: "4px",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const s = send.button.style;
|
|
89
|
+
expect(s.width).toBe("48px");
|
|
90
|
+
expect(s.height).toBe("48px");
|
|
91
|
+
expect(s.minWidth).toBe("48px");
|
|
92
|
+
expect(s.minHeight).toBe("48px");
|
|
93
|
+
expect(s.color).toBe("rgb(4, 5, 6)");
|
|
94
|
+
expect(s.backgroundColor).toBe("rgb(1, 2, 3)");
|
|
95
|
+
expect(s.borderWidth).toBe("2px");
|
|
96
|
+
expect(s.borderStyle).toBe("solid");
|
|
97
|
+
expect(s.borderColor).toBe("rgb(7, 8, 9)");
|
|
98
|
+
expect(s.paddingLeft).toBe("6px");
|
|
99
|
+
expect(s.paddingRight).toBe("6px");
|
|
100
|
+
expect(s.paddingTop).toBe("4px");
|
|
101
|
+
expect(s.paddingBottom).toBe("4px");
|
|
102
|
+
// An explicit backgroundColor means the fallback class is not added.
|
|
103
|
+
expect(send.button.classList.contains("persona-bg-persona-primary")).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("toggles the white-text fallback class based on textColor (text mode)", () => {
|
|
107
|
+
const plain = createSendButton(baseConfig);
|
|
108
|
+
expect(plain.button.classList.contains("persona-text-white")).toBe(true);
|
|
109
|
+
expect(plain.button.style.color).toBe("");
|
|
110
|
+
|
|
111
|
+
const colored = createSendButton({
|
|
112
|
+
...baseConfig,
|
|
113
|
+
sendButton: { textColor: "rgb(4, 5, 6)" },
|
|
114
|
+
});
|
|
115
|
+
expect(colored.button.classList.contains("persona-text-white")).toBe(false);
|
|
116
|
+
expect(colored.button.style.color).toBe("rgb(4, 5, 6)");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("adds the primary-bg fallback class in icon mode without an explicit background", () => {
|
|
120
|
+
const send = createSendButton({
|
|
121
|
+
...baseConfig,
|
|
122
|
+
sendButton: { useIcon: true, iconName: "send" },
|
|
123
|
+
});
|
|
124
|
+
expect(send.button.classList.contains("persona-bg-persona-primary")).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
73
127
|
describe("icon mode", () => {
|
|
74
128
|
const iconConfig: AgentWidgetConfig = {
|
|
75
129
|
...baseConfig,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement } from "../utils/dom";
|
|
1
|
+
import { createElement, createNode, cx } from "../utils/dom";
|
|
2
2
|
import { renderLucideIcon } from "../utils/icons";
|
|
3
3
|
import { AgentWidgetConfig } from "../types";
|
|
4
4
|
import { ALL_SUPPORTED_MIME_TYPES } from "../utils/content";
|
|
@@ -6,7 +6,7 @@ import { ALL_SUPPORTED_MIME_TYPES } from "../utils/content";
|
|
|
6
6
|
/**
|
|
7
7
|
* Low-level composer control factories. Both `buildComposer` (full,
|
|
8
8
|
* column-stacked card) and `buildPillComposer` (single-row pill) consume
|
|
9
|
-
* these
|
|
9
|
+
* these: the only meaningful difference between the two composers is the
|
|
10
10
|
* layout shell + className. No DOM assembly here; each factory returns the
|
|
11
11
|
* element plus any handles the caller needs.
|
|
12
12
|
*
|
|
@@ -61,7 +61,7 @@ export const createComposerTextarea = (config?: AgentWidgetConfig): ComposerText
|
|
|
61
61
|
});
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
// Strip browser default focus rings
|
|
64
|
+
// Strip browser default focus rings: the composer wraps the textarea in
|
|
65
65
|
// its own surface, so the textarea itself must be visually transparent.
|
|
66
66
|
textarea.style.border = "none";
|
|
67
67
|
textarea.style.outline = "none";
|
|
@@ -112,36 +112,47 @@ export const createSendButton = (config?: AgentWidgetConfig): SendButtonParts =>
|
|
|
112
112
|
|
|
113
113
|
const wrapper = createElement("div", "persona-send-button-wrapper");
|
|
114
114
|
|
|
115
|
-
const button =
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
115
|
+
const button = createNode("button", {
|
|
116
|
+
className: cx(
|
|
117
|
+
"persona-rounded-button disabled:persona-opacity-50 persona-cursor-pointer",
|
|
118
|
+
useIcon
|
|
119
|
+
? "persona-flex persona-items-center persona-justify-center"
|
|
120
|
+
: "persona-bg-persona-accent persona-px-4 persona-py-2 persona-text-sm persona-font-semibold",
|
|
121
|
+
// Icon mode without an explicit background falls back to the primary bg
|
|
122
|
+
// class; text mode without an explicit color falls back to white text.
|
|
123
|
+
useIcon && !backgroundColor && "persona-bg-persona-primary",
|
|
124
|
+
!useIcon && !textColor && "persona-text-white"
|
|
125
|
+
),
|
|
126
|
+
attrs: { type: "submit", "data-persona-composer-submit": "" },
|
|
127
|
+
style: {
|
|
128
|
+
// Sizing is icon-mode-only (text mode is sized by its padding classes).
|
|
129
|
+
width: useIcon ? buttonSize : undefined,
|
|
130
|
+
height: useIcon ? buttonSize : undefined,
|
|
131
|
+
minWidth: useIcon ? buttonSize : undefined,
|
|
132
|
+
minHeight: useIcon ? buttonSize : undefined,
|
|
133
|
+
fontSize: useIcon ? "18px" : undefined,
|
|
134
|
+
lineHeight: useIcon ? "1" : undefined,
|
|
135
|
+
// Icon mode always sets a color; text mode only when textColor is given.
|
|
136
|
+
color: useIcon
|
|
137
|
+
? textColor || "var(--persona-button-primary-fg, #ffffff)"
|
|
138
|
+
: textColor || undefined,
|
|
139
|
+
// backgroundColor is honored in icon mode only.
|
|
140
|
+
backgroundColor: useIcon ? backgroundColor || undefined : undefined,
|
|
141
|
+
borderWidth: sendButtonConfig.borderWidth || undefined,
|
|
142
|
+
borderStyle: sendButtonConfig.borderWidth ? "solid" : undefined,
|
|
143
|
+
borderColor: sendButtonConfig.borderColor || undefined,
|
|
144
|
+
paddingLeft: sendButtonConfig.paddingX || undefined,
|
|
145
|
+
paddingRight: sendButtonConfig.paddingX || undefined,
|
|
146
|
+
paddingTop: sendButtonConfig.paddingY || undefined,
|
|
147
|
+
paddingBottom: sendButtonConfig.paddingY || undefined,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
124
150
|
|
|
125
151
|
// Both icons are pre-rendered so setMode can swap cheaply.
|
|
126
152
|
let sendIcon: SVGElement | null = null;
|
|
127
153
|
let stopIcon: SVGElement | null = null;
|
|
128
154
|
|
|
129
155
|
if (useIcon) {
|
|
130
|
-
button.style.width = buttonSize;
|
|
131
|
-
button.style.height = buttonSize;
|
|
132
|
-
button.style.minWidth = buttonSize;
|
|
133
|
-
button.style.minHeight = buttonSize;
|
|
134
|
-
button.style.fontSize = "18px";
|
|
135
|
-
button.style.lineHeight = "1";
|
|
136
|
-
|
|
137
|
-
button.innerHTML = "";
|
|
138
|
-
|
|
139
|
-
if (textColor) {
|
|
140
|
-
button.style.color = textColor;
|
|
141
|
-
} else {
|
|
142
|
-
button.style.color = "var(--persona-button-primary-fg, #ffffff)";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
156
|
const iconSize = parseFloat(buttonSize) || 24;
|
|
146
157
|
const iconColor = textColor?.trim() || "currentColor";
|
|
147
158
|
|
|
@@ -157,42 +168,8 @@ export const createSendButton = (config?: AgentWidgetConfig): SendButtonParts =>
|
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
stopIcon = renderLucideIcon(stopIconName, iconSize, iconColor, 2);
|
|
160
|
-
|
|
161
|
-
if (backgroundColor) {
|
|
162
|
-
button.style.backgroundColor = backgroundColor;
|
|
163
|
-
} else {
|
|
164
|
-
button.classList.add("persona-bg-persona-primary");
|
|
165
|
-
}
|
|
166
171
|
} else {
|
|
167
172
|
button.textContent = sendLabel;
|
|
168
|
-
if (textColor) {
|
|
169
|
-
button.style.color = textColor;
|
|
170
|
-
} else {
|
|
171
|
-
button.classList.add("persona-text-white");
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (sendButtonConfig.borderWidth) {
|
|
176
|
-
button.style.borderWidth = sendButtonConfig.borderWidth;
|
|
177
|
-
button.style.borderStyle = "solid";
|
|
178
|
-
}
|
|
179
|
-
if (sendButtonConfig.borderColor) {
|
|
180
|
-
button.style.borderColor = sendButtonConfig.borderColor;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (sendButtonConfig.paddingX) {
|
|
184
|
-
button.style.paddingLeft = sendButtonConfig.paddingX;
|
|
185
|
-
button.style.paddingRight = sendButtonConfig.paddingX;
|
|
186
|
-
} else {
|
|
187
|
-
button.style.paddingLeft = "";
|
|
188
|
-
button.style.paddingRight = "";
|
|
189
|
-
}
|
|
190
|
-
if (sendButtonConfig.paddingY) {
|
|
191
|
-
button.style.paddingTop = sendButtonConfig.paddingY;
|
|
192
|
-
button.style.paddingBottom = sendButtonConfig.paddingY;
|
|
193
|
-
} else {
|
|
194
|
-
button.style.paddingTop = "";
|
|
195
|
-
button.style.paddingBottom = "";
|
|
196
173
|
}
|
|
197
174
|
|
|
198
175
|
let tooltip: HTMLElement | null = null;
|
|
@@ -218,7 +195,7 @@ export const createSendButton = (config?: AgentWidgetConfig): SendButtonParts =>
|
|
|
218
195
|
if (useIcon) {
|
|
219
196
|
if (sendIcon && stopIcon) {
|
|
220
197
|
const next = mode === "stop" ? stopIcon : sendIcon;
|
|
221
|
-
// Replace whatever icon is currently mounted
|
|
198
|
+
// Replace whatever icon is currently mounted: the button only ever
|
|
222
199
|
// holds the single active icon. We use replaceChildren(next) rather
|
|
223
200
|
// than replaceChild(next, prev) against a captured `prev` reference:
|
|
224
201
|
// an external re-render/morph can swap the live icon child out from
|
|
@@ -259,36 +236,40 @@ export const createMicButton = (config?: AgentWidgetConfig): MicButtonParts | nu
|
|
|
259
236
|
if (!hasVoiceInput) return null;
|
|
260
237
|
|
|
261
238
|
const buttonSize = config?.sendButton?.size ?? "40px";
|
|
262
|
-
const wrapper = createElement("div", "persona-send-button-wrapper");
|
|
263
|
-
const button = createElement(
|
|
264
|
-
"button",
|
|
265
|
-
"persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer"
|
|
266
|
-
) as HTMLButtonElement;
|
|
267
|
-
|
|
268
|
-
button.type = "button";
|
|
269
|
-
button.setAttribute("data-persona-composer-mic", "");
|
|
270
|
-
button.setAttribute("aria-label", "Start voice recognition");
|
|
271
|
-
|
|
272
239
|
const micIconName = voiceRecognitionConfig.iconName ?? "mic";
|
|
273
240
|
const micIconSize = voiceRecognitionConfig.iconSize ?? buttonSize;
|
|
274
241
|
const micIconSizeNum = parseFloat(micIconSize) || 24;
|
|
275
|
-
|
|
276
242
|
const micBackgroundColor =
|
|
277
243
|
voiceRecognitionConfig.backgroundColor ?? config?.sendButton?.backgroundColor;
|
|
278
244
|
const micIconColor = voiceRecognitionConfig.iconColor ?? config?.sendButton?.textColor;
|
|
279
245
|
|
|
280
|
-
|
|
281
|
-
button
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
246
|
+
const wrapper = createElement("div", "persona-send-button-wrapper");
|
|
247
|
+
const button = createNode("button", {
|
|
248
|
+
className:
|
|
249
|
+
"persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer",
|
|
250
|
+
attrs: {
|
|
251
|
+
type: "button",
|
|
252
|
+
"data-persona-composer-mic": "",
|
|
253
|
+
"aria-label": "Start voice recognition",
|
|
254
|
+
},
|
|
255
|
+
style: {
|
|
256
|
+
width: micIconSize,
|
|
257
|
+
height: micIconSize,
|
|
258
|
+
minWidth: micIconSize,
|
|
259
|
+
minHeight: micIconSize,
|
|
260
|
+
fontSize: "18px",
|
|
261
|
+
lineHeight: "1",
|
|
262
|
+
color: micIconColor || "var(--persona-text, #111827)",
|
|
263
|
+
backgroundColor: micBackgroundColor || undefined,
|
|
264
|
+
borderWidth: voiceRecognitionConfig.borderWidth || undefined,
|
|
265
|
+
borderStyle: voiceRecognitionConfig.borderWidth ? "solid" : undefined,
|
|
266
|
+
borderColor: voiceRecognitionConfig.borderColor || undefined,
|
|
267
|
+
paddingLeft: voiceRecognitionConfig.paddingX || undefined,
|
|
268
|
+
paddingRight: voiceRecognitionConfig.paddingX || undefined,
|
|
269
|
+
paddingTop: voiceRecognitionConfig.paddingY || undefined,
|
|
270
|
+
paddingBottom: voiceRecognitionConfig.paddingY || undefined,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
292
273
|
|
|
293
274
|
const iconColorValue = micIconColor || "currentColor";
|
|
294
275
|
const micIconSvg = renderLucideIcon(micIconName, micIconSizeNum, iconColorValue, 1.5);
|
|
@@ -298,27 +279,6 @@ export const createMicButton = (config?: AgentWidgetConfig): MicButtonParts | nu
|
|
|
298
279
|
button.textContent = "🎤";
|
|
299
280
|
}
|
|
300
281
|
|
|
301
|
-
if (micBackgroundColor) {
|
|
302
|
-
button.style.backgroundColor = micBackgroundColor;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (voiceRecognitionConfig.borderWidth) {
|
|
306
|
-
button.style.borderWidth = voiceRecognitionConfig.borderWidth;
|
|
307
|
-
button.style.borderStyle = "solid";
|
|
308
|
-
}
|
|
309
|
-
if (voiceRecognitionConfig.borderColor) {
|
|
310
|
-
button.style.borderColor = voiceRecognitionConfig.borderColor;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (voiceRecognitionConfig.paddingX) {
|
|
314
|
-
button.style.paddingLeft = voiceRecognitionConfig.paddingX;
|
|
315
|
-
button.style.paddingRight = voiceRecognitionConfig.paddingX;
|
|
316
|
-
}
|
|
317
|
-
if (voiceRecognitionConfig.paddingY) {
|
|
318
|
-
button.style.paddingTop = voiceRecognitionConfig.paddingY;
|
|
319
|
-
button.style.paddingBottom = voiceRecognitionConfig.paddingY;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
282
|
wrapper.appendChild(button);
|
|
323
283
|
|
|
324
284
|
const micTooltipText = voiceRecognitionConfig.tooltipText ?? "Start voice recognition";
|
|
@@ -355,39 +315,45 @@ export const createAttachmentControls = (config?: AgentWidgetConfig): Attachment
|
|
|
355
315
|
"div",
|
|
356
316
|
"persona-attachment-previews persona-flex persona-flex-wrap persona-gap-2 persona-mb-2"
|
|
357
317
|
);
|
|
318
|
+
previewsContainer.setAttribute("data-persona-composer-attachment-previews", "");
|
|
358
319
|
previewsContainer.style.display = "none";
|
|
359
320
|
|
|
360
321
|
const input = createElement("input") as HTMLInputElement;
|
|
361
322
|
input.type = "file";
|
|
323
|
+
input.setAttribute("data-persona-composer-attachment-input", "");
|
|
362
324
|
input.accept = (attachmentsConfig.allowedTypes ?? ALL_SUPPORTED_MIME_TYPES).join(",");
|
|
363
325
|
input.multiple = (attachmentsConfig.maxFiles ?? 4) > 1;
|
|
364
326
|
input.style.display = "none";
|
|
365
327
|
input.setAttribute("aria-label", "Attach files");
|
|
366
328
|
|
|
367
|
-
const wrapper = createElement("div", "persona-send-button-wrapper");
|
|
368
|
-
const button = createElement(
|
|
369
|
-
"button",
|
|
370
|
-
"persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer persona-attachment-button"
|
|
371
|
-
) as HTMLButtonElement;
|
|
372
|
-
button.type = "button";
|
|
373
|
-
button.setAttribute("aria-label", attachmentsConfig.buttonTooltipText ?? "Attach file");
|
|
374
|
-
|
|
375
329
|
const attachIconName = attachmentsConfig.buttonIconName ?? "paperclip";
|
|
376
330
|
const attachIconSize = buttonSize;
|
|
377
331
|
const buttonSizeNum = parseFloat(attachIconSize) || 40;
|
|
378
332
|
const attachIconSizeNum = Math.round(buttonSizeNum * 0.6);
|
|
379
333
|
|
|
380
|
-
|
|
381
|
-
button
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
334
|
+
const wrapper = createElement("div", "persona-send-button-wrapper");
|
|
335
|
+
const button = createNode("button", {
|
|
336
|
+
className:
|
|
337
|
+
"persona-rounded-button persona-flex persona-items-center persona-justify-center disabled:persona-opacity-50 persona-cursor-pointer persona-attachment-button",
|
|
338
|
+
attrs: {
|
|
339
|
+
type: "button",
|
|
340
|
+
"data-persona-composer-attachment-button": "",
|
|
341
|
+
"aria-label": attachmentsConfig.buttonTooltipText ?? "Attach file",
|
|
342
|
+
},
|
|
343
|
+
style: {
|
|
344
|
+
width: attachIconSize,
|
|
345
|
+
height: attachIconSize,
|
|
346
|
+
minWidth: attachIconSize,
|
|
347
|
+
minHeight: attachIconSize,
|
|
348
|
+
fontSize: "18px",
|
|
349
|
+
lineHeight: "1",
|
|
350
|
+
backgroundColor: "transparent",
|
|
351
|
+
color: "var(--persona-primary, #111827)",
|
|
352
|
+
border: "none",
|
|
353
|
+
borderRadius: "6px",
|
|
354
|
+
transition: "background-color 0.15s ease",
|
|
355
|
+
},
|
|
356
|
+
});
|
|
391
357
|
|
|
392
358
|
button.addEventListener("mouseenter", () => {
|
|
393
359
|
button.style.backgroundColor = "var(--persona-palette-colors-black-alpha-50, rgba(0, 0, 0, 0.05))";
|
|
@@ -452,4 +418,7 @@ export const createStatusText = (config?: AgentWidgetConfig): HTMLElement => {
|
|
|
452
418
|
};
|
|
453
419
|
|
|
454
420
|
export const createSuggestionsRow = (): HTMLElement =>
|
|
455
|
-
|
|
421
|
+
createNode("div", {
|
|
422
|
+
className: "persona-mb-3 persona-flex persona-flex-wrap persona-gap-2",
|
|
423
|
+
attrs: { "data-persona-composer-suggestions": "" },
|
|
424
|
+
});
|
|
@@ -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;
|