@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
package/src/components/panel.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement } from "../utils/dom";
|
|
1
|
+
import { createElement, createNode } from "../utils/dom";
|
|
2
2
|
import { DEFAULT_FLOATING_LAUNCHER_WIDTH } from "../defaults";
|
|
3
3
|
import { AgentWidgetConfig } from "../types";
|
|
4
4
|
import { positionMap } from "../utils/positioning";
|
|
@@ -16,8 +16,7 @@ export interface PanelWrapper {
|
|
|
16
16
|
/**
|
|
17
17
|
* Composer-bar mode only: viewport-fixed sibling of `wrapper` that owns
|
|
18
18
|
* the persistent pill (`footer`) and peek banner. Lives outside the
|
|
19
|
-
* wrapper so it never inherits the wrapper's geometry transitions
|
|
20
|
-
* critical for modal mode where the wrapper is `transform: translate(-50%, -50%)`
|
|
19
|
+
* wrapper so it never inherits the wrapper's geometry transitions: * critical for modal mode where the wrapper is `transform: translate(-50%, -50%)`
|
|
21
20
|
* (a transformed ancestor establishes a containing block for `position: fixed`
|
|
22
21
|
* descendants, which would trap the pill inside the wrapper otherwise).
|
|
23
22
|
*/
|
|
@@ -32,7 +31,7 @@ export const createWrapper = (config?: AgentWidgetConfig): PanelWrapper => {
|
|
|
32
31
|
if (composerBarMode) {
|
|
33
32
|
const cb = config?.launcher?.composerBar ?? {};
|
|
34
33
|
// Geometry (left/transform/bottom/top/width/max-width) is intentionally
|
|
35
|
-
// NOT set here
|
|
34
|
+
// NOT set here: it's owned entirely by `applyComposerBarGeometry()` in
|
|
36
35
|
// ui.ts so that collapsed → expanded transitions can clear the previous
|
|
37
36
|
// state's inline styles cleanly. Setting geometry here would persist
|
|
38
37
|
// across state changes and override the per-state values, which
|
|
@@ -55,7 +54,7 @@ export const createWrapper = (config?: AgentWidgetConfig): PanelWrapper => {
|
|
|
55
54
|
panel.style.width = "100%";
|
|
56
55
|
wrapper.appendChild(panel);
|
|
57
56
|
|
|
58
|
-
// Pill lives in a separate viewport-fixed sibling
|
|
57
|
+
// Pill lives in a separate viewport-fixed sibling: see PanelWrapper
|
|
59
58
|
// docs above. ui.ts appends `peekBanner` and `footer` here, then
|
|
60
59
|
// appends pillRoot to `mount` immediately after the wrapper.
|
|
61
60
|
const pillRoot = createElement("div", "persona-widget-pill-root");
|
|
@@ -182,7 +181,7 @@ export interface PanelElements {
|
|
|
182
181
|
/**
|
|
183
182
|
* Composer-bar panel: minimal close-only header (× in the top-right of the
|
|
184
183
|
* chat chrome). The pill (`footer`) and `peekBanner` are NOT children of
|
|
185
|
-
* the panel
|
|
184
|
+
* the panel: caller (`ui.ts`) appends them to the `pillRoot` returned by
|
|
186
185
|
* `createWrapper`, which is a viewport-fixed sibling of the wrapper.
|
|
187
186
|
* This decouples the pill from the wrapper's geometry transitions (so the
|
|
188
187
|
* pill stays anchored at the viewport bottom regardless of expanded mode).
|
|
@@ -200,7 +199,7 @@ const buildComposerBarPanel = (
|
|
|
200
199
|
);
|
|
201
200
|
container.setAttribute("data-persona-theme-zone", "container");
|
|
202
201
|
|
|
203
|
-
// Minimal header
|
|
202
|
+
// Minimal header: just an absolutely-positioned close button.
|
|
204
203
|
// The wrapper uses inline styles (top/right/z-index values) because the
|
|
205
204
|
// widget's hand-authored CSS doesn't ship every Tailwind utility.
|
|
206
205
|
// Composer-bar's defaults are roughly half the floating-launcher's
|
|
@@ -220,7 +219,7 @@ const buildComposerBarPanel = (
|
|
|
220
219
|
closeButtonWrapper.style.right = "8px";
|
|
221
220
|
closeButtonWrapper.style.zIndex = "10";
|
|
222
221
|
|
|
223
|
-
// Clear / "start over" button
|
|
222
|
+
// Clear / "start over" button: sits immediately to the left of the close
|
|
224
223
|
// button in the panel chrome's top-right corner. Same minimal sizing as
|
|
225
224
|
// the close icon (16px button, 14px icon) so the two read as a paired
|
|
226
225
|
// action group rather than a header strip. Wired by `setupClearChatButton()`
|
|
@@ -246,46 +245,50 @@ const buildComposerBarPanel = (
|
|
|
246
245
|
}
|
|
247
246
|
// Placeholder header element so PanelElements.header exists (some downstream
|
|
248
247
|
// code reads it). It carries `data-persona-widget-header` for plugin /
|
|
249
|
-
// selector parity but renders nothing visible
|
|
248
|
+
// selector parity but renders nothing visible: the close button is the only
|
|
250
249
|
// header chrome in composer-bar mode.
|
|
251
|
-
const headerPlaceholder =
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
const headerPlaceholder = createNode("span", {
|
|
251
|
+
className: "persona-widget-header",
|
|
252
|
+
attrs: { "data-persona-theme-zone": "header" },
|
|
253
|
+
style: { display: "none" },
|
|
254
|
+
});
|
|
254
255
|
|
|
255
|
-
// Body
|
|
256
|
+
// Body: extra top padding (set inline so the hand-authored widget.css
|
|
256
257
|
// doesn't need a `pt-12` utility) so the absolute close button doesn't
|
|
257
258
|
// overlap the welcome card / first message.
|
|
258
|
-
const body =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
body.setAttribute("data-persona-theme-zone", "messages");
|
|
259
|
+
const body = createNode("div", {
|
|
260
|
+
className:
|
|
261
|
+
"persona-widget-body persona-flex persona-flex-1 persona-min-h-0 persona-flex-col persona-gap-6 persona-overflow-y-auto persona-bg-persona-container persona-px-6 persona-py-6",
|
|
262
|
+
attrs: { id: "persona-scroll-container", "data-persona-theme-zone": "messages" },
|
|
263
|
+
style: { paddingTop: "48px" },
|
|
264
|
+
});
|
|
265
265
|
// Reserve the scrollbar gutter so the transcript doesn't shift horizontally
|
|
266
266
|
// when streaming content first overflows and the scrollbar appears.
|
|
267
267
|
body.style.setProperty("scrollbar-gutter", "stable");
|
|
268
268
|
|
|
269
|
-
const
|
|
269
|
+
const introTitle = createNode("h2", {
|
|
270
|
+
className: "persona-text-lg persona-font-semibold persona-text-persona-primary",
|
|
271
|
+
text: config?.copy?.welcomeTitle ?? "Hello 👋",
|
|
272
|
+
});
|
|
273
|
+
const introSubtitle = createNode("p", {
|
|
274
|
+
className: "persona-mt-2 persona-text-sm persona-text-persona-muted",
|
|
275
|
+
text:
|
|
276
|
+
config?.copy?.welcomeSubtitle ??
|
|
277
|
+
"Ask anything about your account or products.",
|
|
278
|
+
});
|
|
279
|
+
const introCard = createNode(
|
|
270
280
|
"div",
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const introSubtitle = createElement(
|
|
282
|
-
"p",
|
|
283
|
-
"persona-mt-2 persona-text-sm persona-text-persona-muted"
|
|
281
|
+
{
|
|
282
|
+
className: "persona-rounded-2xl persona-bg-persona-surface persona-p-6",
|
|
283
|
+
attrs: { "data-persona-intro-card": "" },
|
|
284
|
+
style: {
|
|
285
|
+
boxShadow:
|
|
286
|
+
"var(--persona-intro-card-shadow, 0 5px 15px rgba(15, 23, 42, 0.08))",
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
introTitle,
|
|
290
|
+
introSubtitle
|
|
284
291
|
);
|
|
285
|
-
introSubtitle.textContent =
|
|
286
|
-
config?.copy?.welcomeSubtitle ??
|
|
287
|
-
"Ask anything about your account or products.";
|
|
288
|
-
introCard.append(introTitle, introSubtitle);
|
|
289
292
|
|
|
290
293
|
const messagesWrapper = createElement(
|
|
291
294
|
"div",
|
|
@@ -309,23 +312,18 @@ const buildComposerBarPanel = (
|
|
|
309
312
|
|
|
310
313
|
// Composer overlay (interactive sheets like ask_user_question slide up here).
|
|
311
314
|
// Anchored to the bottom of the container (which is `position: relative`),
|
|
312
|
-
// so sheets render at the bottom of the chat chrome
|
|
315
|
+
// so sheets render at the bottom of the chat chrome: just above the gap +
|
|
313
316
|
// pill that sit below the container.
|
|
314
|
-
const composerOverlay =
|
|
315
|
-
"
|
|
316
|
-
"persona-composer-overlay
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
composerOverlay.style.right = "0";
|
|
322
|
-
composerOverlay.style.bottom = "0";
|
|
323
|
-
composerOverlay.style.zIndex = "20";
|
|
324
|
-
|
|
325
|
-
// Pill composer — caller appends as a sibling of container in the panel.
|
|
317
|
+
const composerOverlay = createNode("div", {
|
|
318
|
+
className: "persona-composer-overlay persona-pointer-events-none",
|
|
319
|
+
attrs: { "data-persona-composer-overlay": "" },
|
|
320
|
+
style: { position: "absolute", left: "0", right: "0", bottom: "0", zIndex: "20" },
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Pill composer: caller appends as a sibling of container in the panel.
|
|
326
324
|
const composerElements: ComposerElements = buildPillComposer({ config });
|
|
327
325
|
|
|
328
|
-
// Peek banner
|
|
326
|
+
// Peek banner: caller inserts as a sibling of container/footer between
|
|
329
327
|
// them in the panel. Hidden by default; ui.ts toggles visibility.
|
|
330
328
|
const { root: peekBanner, textNode: peekTextNode } = buildPillPeekBanner();
|
|
331
329
|
|
|
@@ -382,11 +380,11 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
382
380
|
return buildComposerBarPanel(config, showClose);
|
|
383
381
|
}
|
|
384
382
|
|
|
385
|
-
const container =
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
383
|
+
const container = createNode("div", {
|
|
384
|
+
className:
|
|
385
|
+
"persona-widget-container persona-flex persona-h-full persona-w-full persona-flex-1 persona-min-h-0 persona-flex-col persona-text-persona-primary persona-bg-persona-surface persona-rounded-2xl persona-overflow-hidden persona-border persona-border-persona-border",
|
|
386
|
+
attrs: { "data-persona-theme-zone": "container" },
|
|
387
|
+
});
|
|
390
388
|
|
|
391
389
|
// Build header using layout config if available, otherwise use standard builder
|
|
392
390
|
const headerLayoutConfig = config?.layout?.header;
|
|
@@ -396,40 +394,43 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
396
394
|
: buildHeader({ config, showClose });
|
|
397
395
|
|
|
398
396
|
// Build body with intro card and messages wrapper
|
|
399
|
-
const body =
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
body.setAttribute("data-persona-theme-zone", "messages");
|
|
397
|
+
const body = createNode("div", {
|
|
398
|
+
className:
|
|
399
|
+
"persona-widget-body persona-flex persona-flex-1 persona-min-h-0 persona-flex-col persona-gap-6 persona-overflow-y-auto persona-bg-persona-container persona-px-6 persona-py-6",
|
|
400
|
+
attrs: { id: "persona-scroll-container", "data-persona-theme-zone": "messages" },
|
|
401
|
+
});
|
|
405
402
|
// Reserve the scrollbar gutter so the transcript doesn't shift horizontally
|
|
406
403
|
// when streaming content first overflows and the scrollbar appears.
|
|
407
404
|
body.style.setProperty("scrollbar-gutter", "stable");
|
|
408
405
|
|
|
409
|
-
const
|
|
410
|
-
"
|
|
411
|
-
|
|
412
|
-
);
|
|
406
|
+
const introTitle = createNode("h2", {
|
|
407
|
+
className: "persona-text-lg persona-font-semibold persona-text-persona-primary",
|
|
408
|
+
text: config?.copy?.welcomeTitle ?? "Hello 👋",
|
|
409
|
+
});
|
|
410
|
+
const introSubtitle = createNode("p", {
|
|
411
|
+
className: "persona-mt-2 persona-text-sm persona-text-persona-muted",
|
|
412
|
+
text:
|
|
413
|
+
config?.copy?.welcomeSubtitle ??
|
|
414
|
+
"Ask anything about your account or products.",
|
|
415
|
+
});
|
|
413
416
|
// Box-shadow flows through the themable `components.introCard.shadow` token
|
|
414
417
|
// (--persona-intro-card-shadow). Docked mode keeps a flat look by default;
|
|
415
418
|
// floating mode falls back to the legacy `persona-shadow-sm` value when no
|
|
416
419
|
// token is set.
|
|
417
|
-
introCard
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
420
|
+
const introCard = createNode(
|
|
421
|
+
"div",
|
|
422
|
+
{
|
|
423
|
+
className: "persona-rounded-2xl persona-bg-persona-surface persona-p-6",
|
|
424
|
+
attrs: { "data-persona-intro-card": "" },
|
|
425
|
+
style: {
|
|
426
|
+
boxShadow: isDockedMountMode(config)
|
|
427
|
+
? "none"
|
|
428
|
+
: "var(--persona-intro-card-shadow, 0 5px 15px rgba(15, 23, 42, 0.08))",
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
introTitle,
|
|
432
|
+
introSubtitle
|
|
428
433
|
);
|
|
429
|
-
introSubtitle.textContent =
|
|
430
|
-
config?.copy?.welcomeSubtitle ??
|
|
431
|
-
"Ask anything about your account or products.";
|
|
432
|
-
introCard.append(introTitle, introSubtitle);
|
|
433
434
|
|
|
434
435
|
const messagesWrapper = createElement(
|
|
435
436
|
"div",
|
|
@@ -444,7 +445,6 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
444
445
|
messagesWrapper.style.width = "100%";
|
|
445
446
|
}
|
|
446
447
|
|
|
447
|
-
introCard.setAttribute("data-persona-intro-card", "");
|
|
448
448
|
const showWelcomeCard = config?.copy?.showWelcomeCard !== false;
|
|
449
449
|
if (!showWelcomeCard) {
|
|
450
450
|
introCard.style.display = "none";
|
|
@@ -473,21 +473,16 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
473
473
|
// above the composer so sheets (e.g. the ask_user_question answer-pill sheet)
|
|
474
474
|
// can slide up without reflowing the chat transcript above. Uses inline
|
|
475
475
|
// styles for left/right/bottom because widget.css is hand-authored and
|
|
476
|
-
// doesn't ship `.persona-left-0` / `.persona-right-0` rules
|
|
476
|
+
// doesn't ship `.persona-left-0` / `.persona-right-0` rules: without
|
|
477
477
|
// them the overlay shrink-wraps to content and collapses the sheet width.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
composerOverlay.style.bottom = "0";
|
|
487
|
-
// Above .persona-scroll-to-bottom-indicator (z-index 10, sibling in the
|
|
488
|
-
// container) so suggestion chips and the ask-user-question sheet are not
|
|
489
|
-
// covered by the "jump to latest" button.
|
|
490
|
-
composerOverlay.style.zIndex = "20";
|
|
478
|
+
// zIndex 20 sits above .persona-scroll-to-bottom-indicator (z-index 10,
|
|
479
|
+
// sibling in the container) so suggestion chips and the ask-user-question
|
|
480
|
+
// sheet are not covered by the "jump to latest" button.
|
|
481
|
+
const composerOverlay = createNode("div", {
|
|
482
|
+
className: "persona-composer-overlay persona-pointer-events-none",
|
|
483
|
+
attrs: { "data-persona-composer-overlay": "" },
|
|
484
|
+
style: { position: "absolute", left: "0", right: "0", bottom: "0", zIndex: "20" },
|
|
485
|
+
});
|
|
491
486
|
|
|
492
487
|
if (showFooter) {
|
|
493
488
|
container.append(composerElements.footer);
|
|
@@ -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 { ComposerBuildContext, ComposerElements } from "./composer-builder";
|
|
4
4
|
import {
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
export interface PillPeekBanner {
|
|
14
14
|
/**
|
|
15
|
-
* The peek button itself
|
|
15
|
+
* The peek button itself: a chrome-less row that floats above the pill,
|
|
16
16
|
* showing a chat-bubble icon, a trailing-100-char preview of the most
|
|
17
17
|
* recent assistant message, and a chevron-up. Rendered hidden by default
|
|
18
18
|
* (opacity 0, pointer-events none); ui.ts toggles
|
|
@@ -25,8 +25,7 @@ export interface PillPeekBanner {
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Build the peek banner for `launcher.mountMode: "composer-bar"`. The peek
|
|
28
|
-
* is the user's path back into the expanded chat from the collapsed pill
|
|
29
|
-
* it fades in during streaming OR on composer hover, and clicking it opens
|
|
28
|
+
* is the user's path back into the expanded chat from the collapsed pill: * it fades in during streaming OR on composer hover, and clicking it opens
|
|
30
29
|
* the panel. ui.ts owns visibility + content updates via
|
|
31
30
|
* `syncComposerBarPeek`; this factory just produces the inert DOM shell.
|
|
32
31
|
*
|
|
@@ -34,11 +33,15 @@ export interface PillPeekBanner {
|
|
|
34
33
|
* just above the pill in the collapsed-state UI.
|
|
35
34
|
*/
|
|
36
35
|
export const buildPillPeekBanner = (): PillPeekBanner => {
|
|
37
|
-
const root =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
const root = createNode("button", {
|
|
37
|
+
className: "persona-pill-peek",
|
|
38
|
+
attrs: {
|
|
39
|
+
type: "button",
|
|
40
|
+
"data-persona-pill-peek": "",
|
|
41
|
+
"aria-label": "Show conversation",
|
|
42
|
+
tabindex: "-1",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
42
45
|
|
|
43
46
|
const iconHolder = createElement("span", "persona-pill-peek__icon");
|
|
44
47
|
const messageIcon = renderLucideIcon("message-square", 16, "currentColor", 1.5);
|
|
@@ -61,7 +64,7 @@ export const buildPillPeekBanner = (): PillPeekBanner => {
|
|
|
61
64
|
/**
|
|
62
65
|
* Single-row pill composer for `launcher.mountMode: "composer-bar"`.
|
|
63
66
|
*
|
|
64
|
-
* Same control factories as `buildComposer
|
|
67
|
+
* Same control factories as `buildComposer`: the only difference is the
|
|
65
68
|
* layout shell + className. The form ships with `persona-pill-composer`
|
|
66
69
|
* (no `persona-flex-col` / `persona-rounded-2xl` baggage), so the CSS
|
|
67
70
|
* layout rules apply at normal specificity without `!important` fights.
|
|
@@ -71,7 +74,7 @@ export const buildPillPeekBanner = (): PillPeekBanner => {
|
|
|
71
74
|
*
|
|
72
75
|
* Suggestions row + status text are built (so plugin code that mutates
|
|
73
76
|
* them keeps working and `bindComposerRefsFromFooter` finds them) but are
|
|
74
|
-
* `display: none` by default
|
|
77
|
+
* `display: none` by default: pill UX is just textarea + 3 buttons.
|
|
75
78
|
*
|
|
76
79
|
* Attachment previews float ABOVE the pill in their own row when
|
|
77
80
|
* AttachmentManager toggles the previews container's `display` property
|
|
@@ -80,8 +83,10 @@ export const buildPillPeekBanner = (): PillPeekBanner => {
|
|
|
80
83
|
export const buildPillComposer = (context: ComposerBuildContext): ComposerElements => {
|
|
81
84
|
const { config } = context;
|
|
82
85
|
|
|
83
|
-
const footer =
|
|
84
|
-
|
|
86
|
+
const footer = createNode("div", {
|
|
87
|
+
className: "persona-widget-footer persona-widget-footer--pill",
|
|
88
|
+
attrs: { "data-persona-theme-zone": "composer" },
|
|
89
|
+
});
|
|
85
90
|
|
|
86
91
|
const suggestions = createSuggestionsRow();
|
|
87
92
|
suggestions.style.display = "none";
|
|
@@ -105,12 +110,11 @@ export const buildPillComposer = (context: ComposerBuildContext): ComposerElemen
|
|
|
105
110
|
|
|
106
111
|
// Pill form: NO `persona-flex-col`. Only the marker classes that the rest
|
|
107
112
|
// of the codebase queries by name.
|
|
108
|
-
const composerForm =
|
|
109
|
-
"
|
|
110
|
-
"persona-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
composerForm.style.outline = "none";
|
|
113
|
+
const composerForm = createNode("form", {
|
|
114
|
+
className: "persona-widget-composer persona-pill-composer",
|
|
115
|
+
attrs: { "data-persona-composer-form": "" },
|
|
116
|
+
style: { outline: "none" },
|
|
117
|
+
});
|
|
114
118
|
|
|
115
119
|
// Three columns of the grid: [paperclip?] · textarea · mic + send.
|
|
116
120
|
// The empty leftActions wrapper still ships when attachments are off so
|
|
@@ -154,11 +158,11 @@ export const buildPillComposer = (context: ComposerBuildContext): ComposerElemen
|
|
|
154
158
|
|
|
155
159
|
// The pill flattens left/right into the form's grid; there's no separate
|
|
156
160
|
// wrapper. Surface the form itself as `actionsRow` to satisfy the
|
|
157
|
-
// ComposerElements contract
|
|
158
|
-
// opaque ref.
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
161
|
+
// ComposerElements contract: downstream code only treats it as an
|
|
162
|
+
// opaque ref. The pill form intentionally carries no
|
|
163
|
+
// `data-persona-composer-actions` marker, so `bindComposerRefsFromFooter`
|
|
164
|
+
// finds no actions row in pill mode; that lookup writes to `_actionsRow`
|
|
165
|
+
// (the underscore prefix marks it as soft-optional).
|
|
162
166
|
const actionsRow = composerForm;
|
|
163
167
|
|
|
164
168
|
return {
|
|
@@ -114,7 +114,7 @@ export const createReasoningBubble = (message: AgentWidgetMessage, config?: Agen
|
|
|
114
114
|
const defaultSummary = "Thinking...";
|
|
115
115
|
const reasoningConfig = config?.reasoning ?? {};
|
|
116
116
|
|
|
117
|
-
// Elapsed helpers
|
|
117
|
+
// Elapsed helpers: defined early so they're available to renderCollapsedSummary
|
|
118
118
|
const startedAt = String(reasoning.startedAt ?? Date.now());
|
|
119
119
|
|
|
120
120
|
const createElapsedSpan = (): HTMLElement => {
|
|
@@ -144,7 +144,7 @@ export const createReasoningBubble = (message: AgentWidgetMessage, config?: Agen
|
|
|
144
144
|
headerContent.appendChild(title);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
// Status span
|
|
147
|
+
// Status span: used in the legacy (no-template) path
|
|
148
148
|
const status = createElement("span", "persona-text-xs persona-text-persona-primary");
|
|
149
149
|
status.textContent = describeReasonStatus(reasoning);
|
|
150
150
|
headerContent.appendChild(status);
|
|
@@ -45,7 +45,7 @@ export const createSuggestions = (container: HTMLElement): SuggestionButtons =>
|
|
|
45
45
|
if (!chips || !chips.length) return;
|
|
46
46
|
|
|
47
47
|
// Hide config suggestions after the first user message is sent.
|
|
48
|
-
// Agent-pushed chips skip this gate
|
|
48
|
+
// Agent-pushed chips skip this gate: their visibility is the caller's
|
|
49
49
|
// latest-turn rule (last suggest_replies call with no user message after).
|
|
50
50
|
// Use provided messages or get from session
|
|
51
51
|
if (!agentPushed) {
|
|
@@ -197,7 +197,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
197
197
|
title.style.color = toolCallConfig.headerTextColor;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
// Elapsed helpers
|
|
200
|
+
// Elapsed helpers: defined early so they're available to renderCollapsedSummary
|
|
201
201
|
const startedAt = String(tool.startedAt ?? Date.now());
|
|
202
202
|
|
|
203
203
|
// Helper: build a <span data-tool-elapsed> that the global timer in ui.ts updates
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { createWidgetView, resolveLauncher } from "./widget-view";
|
|
5
|
+
import type { HeaderElements } from "./header-builder";
|
|
6
|
+
import type { AgentWidgetConfig } from "../types";
|
|
7
|
+
import type { AgentWidgetPlugin } from "../plugins/types";
|
|
8
|
+
|
|
9
|
+
describe("createWidgetView: default assembly", () => {
|
|
10
|
+
const config: AgentWidgetConfig = {
|
|
11
|
+
apiUrl: "/api",
|
|
12
|
+
voiceRecognition: { enabled: true, provider: { type: "runtype" } },
|
|
13
|
+
attachments: { enabled: true },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
it("groups shell, transcript, header, and composer refs over the real nodes", () => {
|
|
17
|
+
const view = createWidgetView({ config, showClose: true });
|
|
18
|
+
|
|
19
|
+
// Shell. The empty `panel` is wired to the `container` later by ui.ts
|
|
20
|
+
// (panel.appendChild(container)); createWidgetView preserves that split.
|
|
21
|
+
expect(view.shell.wrapper.contains(view.shell.panel)).toBe(true);
|
|
22
|
+
expect(view.shell.pillRoot).toBeUndefined();
|
|
23
|
+
|
|
24
|
+
// Transcript subtree is wired by buildPanel
|
|
25
|
+
expect(view.transcript.container.contains(view.transcript.body)).toBe(true);
|
|
26
|
+
expect(view.transcript.body.contains(view.transcript.messagesWrapper)).toBe(true);
|
|
27
|
+
|
|
28
|
+
// Header + composer are mounted in the container
|
|
29
|
+
expect(view.transcript.container.contains(view.header.element)).toBe(true);
|
|
30
|
+
expect(view.transcript.container.contains(view.composer.footer)).toBe(true);
|
|
31
|
+
|
|
32
|
+
// Grouped refs are the same nodes as the raw flat panelElements
|
|
33
|
+
expect(view.composer.textarea).toBe(view.panelElements.textarea);
|
|
34
|
+
expect(view.composer.footer).toBe(view.panelElements.footer);
|
|
35
|
+
expect(view.header.element).toBe(view.panelElements.header);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("exposes the stable composer ref attributes", () => {
|
|
39
|
+
const view = createWidgetView({ config, showClose: true });
|
|
40
|
+
|
|
41
|
+
expect(view.composer.textarea.getAttribute("data-persona-composer-input")).toBe("");
|
|
42
|
+
expect(view.composer.sendButton.getAttribute("data-persona-composer-submit")).toBe("");
|
|
43
|
+
expect(view.composer.statusText.getAttribute("data-persona-composer-status")).toBe("");
|
|
44
|
+
expect(view.composer.suggestions.getAttribute("data-persona-composer-suggestions")).toBe("");
|
|
45
|
+
expect(view.composer.actionsRow.getAttribute("data-persona-composer-actions")).toBe("");
|
|
46
|
+
expect(view.composer.micButton).not.toBeNull();
|
|
47
|
+
expect(
|
|
48
|
+
view.composer.attachmentButton?.getAttribute("data-persona-composer-attachment-button")
|
|
49
|
+
).toBe("");
|
|
50
|
+
expect(
|
|
51
|
+
view.composer.attachmentInput?.getAttribute("data-persona-composer-attachment-input")
|
|
52
|
+
).toBe("");
|
|
53
|
+
expect(
|
|
54
|
+
view.composer.attachmentPreviewsContainer?.getAttribute(
|
|
55
|
+
"data-persona-composer-attachment-previews"
|
|
56
|
+
)
|
|
57
|
+
).toBe("");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("hides header and footer when layout disables them", () => {
|
|
61
|
+
const view = createWidgetView({
|
|
62
|
+
config: { apiUrl: "/api", layout: { showHeader: false, showFooter: false } },
|
|
63
|
+
showClose: true,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(view.header.element.style.display).toBe("none");
|
|
67
|
+
expect(view.composer.footer.style.display).toBe("none");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("createWidgetView: composer-bar assembly", () => {
|
|
72
|
+
const config: AgentWidgetConfig = {
|
|
73
|
+
apiUrl: "/api",
|
|
74
|
+
launcher: { mountMode: "composer-bar" },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
it("exposes the pill root and peek banner", () => {
|
|
78
|
+
const view = createWidgetView({ config, showClose: true });
|
|
79
|
+
|
|
80
|
+
expect(view.shell.pillRoot).toBeInstanceOf(HTMLElement);
|
|
81
|
+
expect(view.composer.peekBanner).toBeInstanceOf(HTMLElement);
|
|
82
|
+
expect(view.composer.peekTextNode).toBeInstanceOf(HTMLElement);
|
|
83
|
+
// Composer-bar uses a hidden placeholder header (close button is the chrome).
|
|
84
|
+
expect(view.header.element.style.display).toBe("none");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("createWidgetView: replacement helpers", () => {
|
|
89
|
+
it("replaceHeader swaps the mounted element and mirrors sub-refs", () => {
|
|
90
|
+
const view = createWidgetView({ config: { apiUrl: "/api" }, showClose: true });
|
|
91
|
+
const original = view.header.element;
|
|
92
|
+
const parent = original.parentElement;
|
|
93
|
+
|
|
94
|
+
const next: HeaderElements = {
|
|
95
|
+
header: document.createElement("header"),
|
|
96
|
+
iconHolder: document.createElement("span"),
|
|
97
|
+
headerTitle: document.createElement("span"),
|
|
98
|
+
headerSubtitle: document.createElement("span"),
|
|
99
|
+
closeButton: document.createElement("button"),
|
|
100
|
+
closeButtonWrapper: document.createElement("div"),
|
|
101
|
+
clearChatButton: null,
|
|
102
|
+
clearChatButtonWrapper: null,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const returned = view.replaceHeader(next);
|
|
106
|
+
|
|
107
|
+
expect(returned).toBe(next);
|
|
108
|
+
expect(view.header.element).toBe(next.header);
|
|
109
|
+
expect(view.header.iconHolder).toBe(next.iconHolder);
|
|
110
|
+
expect(view.header.closeButton).toBe(next.closeButton);
|
|
111
|
+
expect(parent?.contains(next.header)).toBe(true);
|
|
112
|
+
expect(parent?.contains(original)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("replaceComposer swaps the mounted footer and updates the ref", () => {
|
|
116
|
+
const view = createWidgetView({ config: { apiUrl: "/api" }, showClose: true });
|
|
117
|
+
const original = view.composer.footer;
|
|
118
|
+
const parent = original.parentElement;
|
|
119
|
+
const nextFooter = document.createElement("div");
|
|
120
|
+
|
|
121
|
+
view.replaceComposer(nextFooter);
|
|
122
|
+
|
|
123
|
+
expect(view.composer.footer).toBe(nextFooter);
|
|
124
|
+
expect(parent?.contains(nextFooter)).toBe(true);
|
|
125
|
+
expect(parent?.contains(original)).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("resolveLauncher", () => {
|
|
130
|
+
const config: AgentWidgetConfig = { apiUrl: "/api" };
|
|
131
|
+
|
|
132
|
+
it("builds the default launcher controller and wires onToggle", () => {
|
|
133
|
+
const onToggle = vi.fn();
|
|
134
|
+
const { instance, element } = resolveLauncher({ config, plugins: [], onToggle });
|
|
135
|
+
|
|
136
|
+
expect(instance).not.toBeNull();
|
|
137
|
+
expect(element).toBe(instance?.element);
|
|
138
|
+
expect(element.tagName).toBe("BUTTON");
|
|
139
|
+
|
|
140
|
+
element.dispatchEvent(new Event("click"));
|
|
141
|
+
expect(onToggle).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("uses a plugin-provided launcher element and returns no controller", () => {
|
|
145
|
+
const custom = document.createElement("div");
|
|
146
|
+
custom.id = "custom-launcher";
|
|
147
|
+
const plugin: AgentWidgetPlugin = {
|
|
148
|
+
id: "custom",
|
|
149
|
+
renderLauncher: ({ defaultRenderer }) => {
|
|
150
|
+
// The default renderer is still callable; the plugin opts to ignore it.
|
|
151
|
+
expect(defaultRenderer().tagName).toBe("BUTTON");
|
|
152
|
+
return custom;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const { instance, element } = resolveLauncher({
|
|
157
|
+
config,
|
|
158
|
+
plugins: [plugin],
|
|
159
|
+
onToggle: () => {},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(instance).toBeNull();
|
|
163
|
+
expect(element).toBe(custom);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("falls back to the default launcher when the plugin returns null", () => {
|
|
167
|
+
const plugin: AgentWidgetPlugin = {
|
|
168
|
+
id: "passthrough",
|
|
169
|
+
renderLauncher: () => null,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const { instance, element } = resolveLauncher({
|
|
173
|
+
config,
|
|
174
|
+
plugins: [plugin],
|
|
175
|
+
onToggle: () => {},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(instance).not.toBeNull();
|
|
179
|
+
expect(element).toBe(instance?.element);
|
|
180
|
+
});
|
|
181
|
+
});
|