@runtypelabs/persona 3.34.1 → 3.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
- package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +10 -10
- package/dist/index.cjs +46 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +514 -158
- package/dist/index.d.ts +514 -158
- package/dist/index.global.js +41 -89
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -44
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +53 -0
- package/dist/plugin-kit.d.cts +4 -4
- package/dist/plugin-kit.d.ts +4 -4
- package/dist/runtype-tts.js +1 -0
- package/dist/smart-dom-reader.cjs +1 -1
- package/dist/smart-dom-reader.d.cts +352 -103
- package/dist/smart-dom-reader.d.ts +352 -103
- package/dist/smart-dom-reader.js +1 -1
- package/dist/theme-editor-preview.cjs +203 -0
- package/dist/theme-editor-preview.d.cts +5748 -0
- package/dist/theme-editor-preview.d.ts +5748 -0
- package/dist/theme-editor-preview.js +203 -0
- package/dist/theme-editor.cjs +14 -131
- package/dist/theme-editor.d.cts +362 -574
- package/dist/theme-editor.d.ts +362 -574
- package/dist/theme-editor.js +14 -131
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1 -1
- package/dist/theme-reference.d.ts +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +80 -0
- package/dist/voice-worklet-player.d.cts +215 -0
- package/dist/voice-worklet-player.d.ts +215 -0
- package/dist/voice-worklet-player.js +80 -0
- package/dist/widget.css +54 -36
- package/package.json +16 -2
- package/src/animations/glyph-cycle.ts +11 -14
- package/src/animations/wipe.ts +1 -1
- package/src/ask-user-question-tool.test.ts +1 -1
- package/src/ask-user-question-tool.ts +6 -8
- package/src/client.test.ts +12 -12
- package/src/client.ts +30 -31
- package/src/codegen.test.ts +1 -1
- package/src/codegen.ts +1 -1
- package/src/components/approval-bubble.test.ts +1 -1
- package/src/components/approval-bubble.ts +3 -3
- package/src/components/artifact-pane.ts +3 -3
- package/src/components/ask-user-question-bubble.test.ts +14 -14
- package/src/components/ask-user-question-bubble.ts +10 -10
- package/src/components/composer-builder.ts +17 -16
- package/src/components/composer-parts.test.ts +54 -0
- package/src/components/composer-parts.ts +95 -126
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +7 -7
- package/src/components/event-stream-view.ts +5 -5
- package/src/components/header-builder.ts +33 -27
- package/src/components/header-layouts.ts +1 -1
- package/src/components/header-parts.test.ts +90 -0
- package/src/components/header-parts.ts +62 -93
- package/src/components/launcher.test.ts +11 -0
- package/src/components/launcher.ts +1 -1
- package/src/components/message-bubble.test.ts +74 -2
- package/src/components/message-bubble.ts +25 -11
- package/src/components/messages.ts +2 -2
- package/src/components/panel.test.ts +1 -1
- package/src/components/panel.ts +92 -97
- package/src/components/pill-composer-builder.ts +28 -24
- package/src/components/reasoning-bubble.ts +2 -2
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +1 -1
- package/src/components/widget-view.test.ts +181 -0
- package/src/components/widget-view.ts +234 -0
- package/src/defaults.ts +54 -51
- package/src/generated/runtype-openapi-contract.ts +5 -0
- package/src/index-core.ts +30 -4
- package/src/index-global.ts +73 -6
- package/src/index.ts +7 -1
- package/src/install.test.ts +11 -11
- package/src/install.ts +28 -18
- package/src/launcher-global.ts +25 -14
- package/src/markdown-parsers-eager.ts +20 -0
- package/src/markdown-parsers-entry.ts +4 -0
- package/src/markdown-parsers-loader.ts +49 -0
- package/src/plugin-kit.test.ts +1 -1
- package/src/plugin-kit.ts +7 -7
- package/src/plugins/types.ts +5 -5
- package/src/postprocessors.ts +27 -14
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +4 -5
- package/src/runtime/init.ts +2 -2
- package/src/session.test.ts +5 -6
- package/src/session.ts +333 -227
- package/src/session.voice.test.ts +146 -0
- package/src/session.webmcp.test.ts +24 -25
- package/src/smart-dom-reader.ts +3 -4
- package/src/styles/widget.css +54 -36
- package/src/suggest-replies-tool.test.ts +3 -3
- package/src/suggest-replies-tool.ts +5 -5
- package/src/testing/index.ts +1 -1
- package/src/theme-editor/color-utils.ts +2 -2
- package/src/theme-editor/index.ts +4 -13
- package/src/theme-editor/presets.ts +1 -1
- package/src/theme-editor/preview-utils.ts +22 -20
- package/src/theme-editor/preview.ts +4 -1
- package/src/theme-editor/role-mappings.ts +3 -3
- package/src/theme-editor/sections.ts +21 -20
- package/src/theme-editor/types.ts +1 -1
- package/src/theme-editor/webmcp/index.ts +1 -1
- package/src/theme-editor/webmcp/summary.ts +1 -2
- package/src/theme-editor/webmcp/tools.ts +30 -30
- package/src/theme-editor/webmcp/types.ts +2 -2
- package/src/theme-editor-preview.ts +10 -0
- package/src/theme-reference.ts +26 -26
- package/src/types.ts +368 -99
- package/src/ui.approval-plugin.test.ts +3 -3
- package/src/ui.ask-user-question-plugin.test.ts +9 -9
- package/src/ui.component-directive.test.ts +4 -4
- package/src/ui.composer-bar.test.ts +10 -11
- package/src/ui.composer-keyboard.test.ts +5 -5
- package/src/ui.header-icon-color.test.ts +59 -0
- package/src/ui.scroll.test.ts +5 -6
- package/src/ui.stop-button.test.ts +3 -4
- package/src/ui.suggest-replies.test.ts +2 -2
- package/src/ui.ts +292 -131
- package/src/utils/buttons.ts +1 -1
- package/src/utils/code-generators.ts +1 -1
- package/src/utils/composer-history.ts +3 -3
- package/src/utils/dom-context.test.ts +2 -2
- package/src/utils/dom-context.ts +7 -7
- package/src/utils/dom.test.ts +67 -0
- package/src/utils/dom.ts +75 -0
- package/src/utils/dropdown.ts +2 -2
- package/src/utils/event-stream-buffer.test.ts +2 -2
- package/src/utils/message-fingerprint.test.ts +9 -0
- package/src/utils/message-fingerprint.ts +5 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/sanitize.ts +32 -16
- package/src/utils/sequence-buffer.test.ts +8 -8
- package/src/utils/sequence-buffer.ts +4 -4
- package/src/utils/smart-dom-adapter.test.ts +1 -1
- package/src/utils/smart-dom-adapter.ts +4 -5
- package/src/utils/speech-text.test.ts +122 -0
- package/src/utils/speech-text.ts +101 -0
- package/src/utils/stream-animation.test.ts +3 -3
- package/src/utils/stream-animation.ts +6 -6
- package/src/utils/theme.test.ts +1 -1
- package/src/utils/throughput-tracker.test.ts +11 -11
- package/src/utils/throughput-tracker.ts +9 -9
- package/src/utils/tokens.ts +13 -13
- package/src/utils/virtual-scroller.ts +1 -1
- package/src/vendor/smart-dom-reader/README.md +4 -4
- package/src/vendor/smart-dom-reader/index.d.ts +1 -1
- package/src/vendor/smart-dom-reader/index.js +2 -2
- package/src/version.ts +1 -1
- package/src/voice/audio-playback-manager.test.ts +159 -0
- package/src/voice/audio-playback-manager.ts +145 -30
- package/src/voice/browser-speech-engine.ts +130 -0
- package/src/voice/fallback-speech-engine.ts +89 -0
- package/src/voice/index.ts +21 -0
- package/src/voice/read-aloud-controller.test.ts +143 -0
- package/src/voice/read-aloud-controller.ts +136 -0
- package/src/voice/runtype-speech-engine.test.ts +280 -0
- package/src/voice/runtype-speech-engine.ts +211 -0
- package/src/voice/runtype-tts-entry.ts +10 -0
- package/src/voice/runtype-tts-loader.test.ts +47 -0
- package/src/voice/runtype-tts-loader.ts +42 -0
- package/src/voice/runtype-voice-provider.ts +332 -537
- package/src/voice/voice-factory.ts +26 -4
- package/src/voice/voice.test.ts +321 -7
- package/src/voice/worklet-playback-engine.ts +272 -0
- package/src/voice-worklet-player.ts +39 -0
- package/src/webmcp-bridge.test.ts +6 -6
- package/src/webmcp-bridge.ts +24 -25
- package/src/webmcp-polyfill.ts +1 -1
- package/src/voice/voice-activity-detector.ts +0 -90
|
@@ -36,7 +36,7 @@ export function createArtifactPane(
|
|
|
36
36
|
config: AgentWidgetConfig,
|
|
37
37
|
options: {
|
|
38
38
|
onSelect: (id: string) => void;
|
|
39
|
-
/** User closed the pane (mobile drawer or split sidebar)
|
|
39
|
+
/** User closed the pane (mobile drawer or split sidebar): parent should persist “hidden until reopened”. */
|
|
40
40
|
onDismiss?: () => void;
|
|
41
41
|
}
|
|
42
42
|
): ArtifactPaneApi {
|
|
@@ -203,7 +203,7 @@ export function createArtifactPane(
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
if (copyMenuChevronBtn && copyMenuItems?.length) {
|
|
206
|
-
// Resolve the portal target
|
|
206
|
+
// Resolve the portal target: widget root for CSS var inheritance, escaping overflow: hidden
|
|
207
207
|
const resolvePortal = (): HTMLElement => shell.closest("[data-persona-root]") as HTMLElement ?? document.body;
|
|
208
208
|
|
|
209
209
|
const initDropdown = () => {
|
|
@@ -414,7 +414,7 @@ export function createArtifactPane(
|
|
|
414
414
|
backdrop.classList.add("persona-hidden");
|
|
415
415
|
shell.classList.remove("persona-artifact-drawer-open");
|
|
416
416
|
} else {
|
|
417
|
-
// isMobile && !(has && mobileOpen): e.g. dismissed drawer
|
|
417
|
+
// isMobile && !(has && mobileOpen): e.g. dismissed drawer: keep closed chrome in sync
|
|
418
418
|
backdrop.classList.add("persona-hidden");
|
|
419
419
|
shell.classList.remove("persona-artifact-drawer-open");
|
|
420
420
|
}
|
|
@@ -103,11 +103,11 @@ describe("ensureAskUserQuestionSheet", () => {
|
|
|
103
103
|
expect(sheet).not.toBeNull();
|
|
104
104
|
const pills = sheet!.querySelectorAll('[data-ask-user-action="pick"]');
|
|
105
105
|
expect(pills.length).toBe(3);
|
|
106
|
-
// Default rows layout
|
|
106
|
+
// Default rows layout: label is in `.persona-ask-row-label`, alongside
|
|
107
107
|
// a number-badge affordance. Read the dedicated label slot.
|
|
108
108
|
const firstLabel = pills[0].querySelector(".persona-ask-row-label");
|
|
109
109
|
expect(firstLabel?.textContent).toBe("Hobbyists");
|
|
110
|
-
// Free-text affordance present by default
|
|
110
|
+
// Free-text affordance present by default: rows mode embeds the input
|
|
111
111
|
// inside the Other row via `focus-free-text` (digit shortcut + click chrome).
|
|
112
112
|
const custom = sheet!.querySelector('[data-ask-user-action="focus-free-text"]');
|
|
113
113
|
expect(custom).not.toBeNull();
|
|
@@ -144,7 +144,7 @@ describe("ensureAskUserQuestionSheet", () => {
|
|
|
144
144
|
});
|
|
145
145
|
ensureAskUserQuestionSheet(msg, {} as AgentWidgetConfig, overlay);
|
|
146
146
|
|
|
147
|
-
// Simulate more chunks arriving
|
|
147
|
+
// Simulate more chunks arriving: options now parseable
|
|
148
148
|
msg.toolCall!.chunks = ['{"questions":[{"question":"X","options":[{"label":"A"},{"label":"B"}'];
|
|
149
149
|
ensureAskUserQuestionSheet(msg, {} as AgentWidgetConfig, overlay);
|
|
150
150
|
|
|
@@ -155,7 +155,7 @@ describe("ensureAskUserQuestionSheet", () => {
|
|
|
155
155
|
expect(pills[1].querySelector(".persona-ask-row-label")?.textContent).toBe("B");
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
it("is idempotent
|
|
158
|
+
it("is idempotent: re-invoking does not duplicate sheets", () => {
|
|
159
159
|
const overlay = makeOverlay();
|
|
160
160
|
ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
|
|
161
161
|
ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
|
|
@@ -163,7 +163,7 @@ describe("ensureAskUserQuestionSheet", () => {
|
|
|
163
163
|
expect(sheets.length).toBe(1);
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
it("respects enabled: false
|
|
166
|
+
it("respects enabled: false: sheet is not mounted", () => {
|
|
167
167
|
const overlay = makeOverlay();
|
|
168
168
|
ensureAskUserQuestionSheet(
|
|
169
169
|
makeMessage(),
|
|
@@ -192,7 +192,7 @@ describe("ensureAskUserQuestionSheet", () => {
|
|
|
192
192
|
expect(sheet.getAttribute("data-multi-select")).toBe("true");
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
-
it("does not render a dismiss button
|
|
195
|
+
it("does not render a dismiss button: Skip in the nav row is the canonical escape", () => {
|
|
196
196
|
const overlay = makeOverlay();
|
|
197
197
|
ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
|
|
198
198
|
const sheet = overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')!;
|
|
@@ -221,7 +221,7 @@ describe("removeAskUserQuestionSheet", () => {
|
|
|
221
221
|
ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
|
|
222
222
|
expect(overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')).not.toBeNull();
|
|
223
223
|
removeAskUserQuestionSheet(overlay, "tool-1");
|
|
224
|
-
// The implementation defers removal with setTimeout
|
|
224
|
+
// The implementation defers removal with setTimeout: flush it.
|
|
225
225
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
226
226
|
expect(overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')).toBeNull();
|
|
227
227
|
});
|
|
@@ -249,7 +249,7 @@ const makeGroupedMessage = (
|
|
|
249
249
|
const sheetFor = (overlay: Element, toolCallId: string): HTMLElement =>
|
|
250
250
|
overlay.querySelector<HTMLElement>(`[data-persona-ask-sheet-for="${toolCallId}"]`)!;
|
|
251
251
|
|
|
252
|
-
describe("grouped questions
|
|
252
|
+
describe("grouped questions: stepper UI", () => {
|
|
253
253
|
it("renders 'Question 1 of N' chip and Back/Next nav row when N > 1", () => {
|
|
254
254
|
const overlay = makeOverlay();
|
|
255
255
|
const msg = makeGroupedMessage([
|
|
@@ -384,7 +384,7 @@ describe("grouped questions — stepper UI", () => {
|
|
|
384
384
|
});
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
-
it("hydrates from agentMetadata
|
|
387
|
+
it("hydrates from agentMetadata: restores index and prior answers on a fresh mount", () => {
|
|
388
388
|
const overlay = makeOverlay();
|
|
389
389
|
const msg = makeGroupedMessage(
|
|
390
390
|
[
|
|
@@ -464,7 +464,7 @@ describe("rows layout (default)", () => {
|
|
|
464
464
|
expect(firstRow.querySelector(".persona-ask-row-description")?.textContent).toBe(
|
|
465
465
|
"Real Discord invite to link"
|
|
466
466
|
);
|
|
467
|
-
// No title-attr fallback in rows layout
|
|
467
|
+
// No title-attr fallback in rows layout: description is inline.
|
|
468
468
|
expect(firstRow.getAttribute("title")).toBeNull();
|
|
469
469
|
});
|
|
470
470
|
|
|
@@ -507,11 +507,11 @@ describe("rows layout (default)", () => {
|
|
|
507
507
|
expect(rows[0].querySelector(".persona-ask-row-badge")).toBeNull();
|
|
508
508
|
});
|
|
509
509
|
|
|
510
|
-
it("embeds the free-text input INSIDE the Other row (rows mode)
|
|
510
|
+
it("embeds the free-text input INSIDE the Other row (rows mode): no separate row, no Send button", () => {
|
|
511
511
|
const overlay = makeOverlay();
|
|
512
512
|
ensureAskUserQuestionSheet(makeMessage(), {} as AgentWidgetConfig, overlay);
|
|
513
513
|
const sheet = overlay.querySelector('[data-persona-ask-sheet-for="tool-1"]')!;
|
|
514
|
-
// No standalone free-text row in rows mode
|
|
514
|
+
// No standalone free-text row in rows mode: input lives inside the Other row.
|
|
515
515
|
expect(sheet.querySelector('[data-ask-free-text-row="true"]')).toBeNull();
|
|
516
516
|
const otherRow = sheet.querySelector<HTMLElement>('[data-ask-other-row="true"]')!;
|
|
517
517
|
expect(otherRow).not.toBeNull();
|
|
@@ -520,12 +520,12 @@ describe("rows layout (default)", () => {
|
|
|
520
520
|
expect(otherRow.querySelector('[data-ask-user-action="submit-free-text"]')).toBeNull();
|
|
521
521
|
// Other row carries the focus-free-text action (digit shortcut + click chrome).
|
|
522
522
|
expect(otherRow.getAttribute("data-ask-user-action")).toBe("focus-free-text");
|
|
523
|
-
// Number badge for the Other row (N+1
|
|
523
|
+
// Number badge for the Other row (N+1: 3 real options + Other = 4).
|
|
524
524
|
const badge = otherRow.querySelector<HTMLElement>(".persona-ask-row-badge");
|
|
525
525
|
expect(badge?.textContent).toBe("4");
|
|
526
526
|
});
|
|
527
527
|
|
|
528
|
-
it("respects layout: 'pills' opt-out
|
|
528
|
+
it("respects layout: 'pills' opt-out: no row affordances, free-text starts hidden, Send button still rendered", () => {
|
|
529
529
|
const overlay = makeOverlay();
|
|
530
530
|
const config = {
|
|
531
531
|
features: { askUserQuestion: { layout: "pills" } },
|
|
@@ -60,7 +60,7 @@ const resolveFeature = (config?: AgentWidgetConfig): AgentWidgetAskUserQuestionF
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Parse an `ask_user_question` tool-variant message into a partial payload.
|
|
63
|
-
* Safe to call mid-stream
|
|
63
|
+
* Safe to call mid-stream: will walk the tool call's `chunks` via
|
|
64
64
|
* `partial-json` and return `{ payload: null, complete: false }` when there
|
|
65
65
|
* isn't enough data yet. `complete` flips to `true` once the tool call
|
|
66
66
|
* reports status `"complete"`.
|
|
@@ -570,7 +570,7 @@ const syncNavState = (sheet: HTMLElement): void => {
|
|
|
570
570
|
);
|
|
571
571
|
if (next) next.disabled = !hasAnswer;
|
|
572
572
|
|
|
573
|
-
// Multi-select submit (1-question mode)
|
|
573
|
+
// Multi-select submit (1-question mode): keep existing behavior.
|
|
574
574
|
const multi = sheet.querySelector<HTMLButtonElement>('[data-ask-user-action="submit-multi"]');
|
|
575
575
|
if (multi) {
|
|
576
576
|
const labels = Array.from(
|
|
@@ -625,7 +625,7 @@ const renderCurrentPage = (
|
|
|
625
625
|
pillList.replaceWith(fresh);
|
|
626
626
|
}
|
|
627
627
|
|
|
628
|
-
// Free-text row
|
|
628
|
+
// Free-text row: re-build to clear stale input value across pages.
|
|
629
629
|
// Only present in pills (legacy) mode; in rows mode the input lives inside
|
|
630
630
|
// the Other row of the pill list, which is rebuilt above.
|
|
631
631
|
if (layout !== "rows") {
|
|
@@ -633,7 +633,7 @@ const renderCurrentPage = (
|
|
|
633
633
|
if (oldFree) oldFree.replaceWith(buildFreeTextRow(feature, layout));
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
// Multi-select action row
|
|
636
|
+
// Multi-select action row: only relevant in 1-question mode.
|
|
637
637
|
const oldMulti = sheet.querySelector<HTMLElement>('[data-ask-multi-actions="true"]');
|
|
638
638
|
if (!grouped && multiSelect && !oldMulti) {
|
|
639
639
|
sheet.appendChild(buildMultiSelectActions(feature));
|
|
@@ -698,7 +698,7 @@ const buildSheet = (
|
|
|
698
698
|
|
|
699
699
|
// Header: question text (flex-1) + compact "N/M" stepper indicator on the
|
|
700
700
|
// right (grouped only). Skip in the nav row is the canonical escape hatch
|
|
701
|
-
|
|
701
|
+
//: plugins that want a different escape model render their own UX.
|
|
702
702
|
const header = createElement(
|
|
703
703
|
"div",
|
|
704
704
|
"persona-ask-sheet-header persona-flex persona-items-center persona-gap-3"
|
|
@@ -721,7 +721,7 @@ const buildSheet = (
|
|
|
721
721
|
|
|
722
722
|
sheet.appendChild(header);
|
|
723
723
|
|
|
724
|
-
// Skeleton placeholders
|
|
724
|
+
// Skeleton placeholders: these get replaced wholesale by renderCurrentPage.
|
|
725
725
|
const skeletonClass =
|
|
726
726
|
layout === "rows"
|
|
727
727
|
? "persona-ask-pills persona-ask-pills--rows persona-flex persona-flex-col persona-gap-2"
|
|
@@ -768,7 +768,7 @@ const syncSheetFromMessage = (
|
|
|
768
768
|
|
|
769
769
|
/**
|
|
770
770
|
* Create the small in-transcript stub for an `ask_user_question` tool call.
|
|
771
|
-
* The stub is passive
|
|
771
|
+
* The stub is passive: the interactive sheet is mounted separately into
|
|
772
772
|
* the composer overlay via `ensureAskUserQuestionSheet`.
|
|
773
773
|
*/
|
|
774
774
|
export const createAskUserQuestionBubble = (
|
|
@@ -796,7 +796,7 @@ export const createAskUserQuestionBubble = (
|
|
|
796
796
|
|
|
797
797
|
/**
|
|
798
798
|
* Mount or update the interactive answer-pill sheet for a given message.
|
|
799
|
-
* Idempotent
|
|
799
|
+
* Idempotent: if a sheet already exists for the tool-call id, it is hydrated
|
|
800
800
|
* in-place instead of remounted, so streaming updates don't flicker.
|
|
801
801
|
*/
|
|
802
802
|
export const ensureAskUserQuestionSheet = (
|
|
@@ -812,7 +812,7 @@ export const ensureAskUserQuestionSheet = (
|
|
|
812
812
|
|
|
813
813
|
const toolCallId = message.toolCall!.id;
|
|
814
814
|
|
|
815
|
-
// Only keep the latest sheet in the overlay
|
|
815
|
+
// Only keep the latest sheet in the overlay: clear any stale siblings.
|
|
816
816
|
const siblings = overlay.querySelectorAll<HTMLElement>(`[${SHEET_SENTINEL}]`);
|
|
817
817
|
siblings.forEach((el) => {
|
|
818
818
|
if (el.getAttribute(SHEET_SENTINEL) !== toolCallId) {
|
|
@@ -916,7 +916,7 @@ export const refreshNavState = (sheet: HTMLElement): void => {
|
|
|
916
916
|
};
|
|
917
917
|
|
|
918
918
|
/**
|
|
919
|
-
* Test seam
|
|
919
|
+
* Test seam: reset the one-shot truncation warning so each test can assert
|
|
920
920
|
* the warn fires exactly once.
|
|
921
921
|
*/
|
|
922
922
|
export const __resetTruncateWarn = (): void => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement } from "../utils/dom";
|
|
1
|
+
import { createElement, createNode } from "../utils/dom";
|
|
2
2
|
import { AgentWidgetConfig, ContentPart } from "../types";
|
|
3
3
|
import {
|
|
4
4
|
createAttachmentControls,
|
|
@@ -57,20 +57,20 @@ export interface ComposerBuildContext {
|
|
|
57
57
|
export const buildComposer = (context: ComposerBuildContext): ComposerElements => {
|
|
58
58
|
const { config } = context;
|
|
59
59
|
|
|
60
|
-
const footer =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
const footer = createNode("div", {
|
|
61
|
+
className:
|
|
62
|
+
"persona-widget-footer persona-border-t-persona-divider persona-bg-persona-surface persona-px-6 persona-py-4",
|
|
63
|
+
attrs: { "data-persona-theme-zone": "composer" },
|
|
64
|
+
});
|
|
65
65
|
|
|
66
66
|
const suggestions = createSuggestionsRow();
|
|
67
67
|
|
|
68
|
-
const composerForm =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
const composerForm = createNode("form", {
|
|
69
|
+
className:
|
|
70
|
+
"persona-widget-composer persona-flex persona-flex-col persona-gap-2 persona-rounded-2xl persona-border persona-border-gray-200 persona-bg-persona-input-background persona-px-4 persona-py-3",
|
|
71
|
+
attrs: { "data-persona-composer-form": "" },
|
|
72
|
+
style: { outline: "none" },
|
|
73
|
+
});
|
|
74
74
|
|
|
75
75
|
const { textarea, attachAutoResize } = createComposerTextarea(config);
|
|
76
76
|
attachAutoResize();
|
|
@@ -93,10 +93,11 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
|
|
|
93
93
|
// The bare class names (persona-widget-composer__actions / __left-actions /
|
|
94
94
|
// __right-actions) are stable CSS hooks. The pill composer reuses
|
|
95
95
|
// __left-actions / __right-actions as semantic markers in its grid.
|
|
96
|
-
const actionsRow =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
const actionsRow = createNode("div", {
|
|
97
|
+
className:
|
|
98
|
+
"persona-widget-composer__actions persona-flex persona-items-center persona-justify-between persona-w-full",
|
|
99
|
+
attrs: { "data-persona-composer-actions": "" },
|
|
100
|
+
});
|
|
100
101
|
const leftActions = createElement(
|
|
101
102
|
"div",
|
|
102
103
|
"persona-widget-composer__left-actions persona-flex persona-items-center persona-gap-2"
|
|
@@ -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
|
+
});
|