@runtypelabs/persona 4.13.0 → 4.15.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 +4 -4
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
- package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-QJ4LV2KW.js +1 -0
- package/dist/codegen.cjs +12 -11
- package/dist/codegen.js +14 -13
- package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
- package/dist/context-mentions.cjs +11 -8
- package/dist/context-mentions.d.cts +339 -22
- package/dist/context-mentions.d.ts +339 -22
- package/dist/context-mentions.js +11 -8
- package/dist/index.cjs +68 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +483 -48
- package/dist/index.d.ts +483 -48
- package/dist/index.global.js +61 -58
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +339 -22
- package/dist/smart-dom-reader.d.ts +339 -22
- package/dist/theme-editor-preview.cjs +67 -67
- package/dist/theme-editor-preview.d.cts +355 -23
- package/dist/theme-editor-preview.d.ts +355 -23
- package/dist/theme-editor-preview.js +75 -75
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +376 -24
- package/dist/theme-editor.d.ts +376 -24
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +38 -0
- package/dist/theme-reference.d.ts +38 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/artifacts-session.test.ts +106 -0
- package/src/ask-user-question-tool.ts +10 -7
- package/src/client.test.ts +2 -2
- package/src/client.ts +4 -1
- package/src/components/artifact-card.ts +13 -2
- package/src/components/artifact-inline.ts +11 -2
- package/src/components/artifact-preview.ts +1 -1
- package/src/components/composer-builder.test.ts +2 -0
- package/src/components/composer-builder.ts +1 -1
- package/src/components/composer-parts.test.ts +105 -4
- package/src/components/composer-parts.ts +54 -16
- package/src/components/message-bubble.test.ts +7 -1
- package/src/components/message-bubble.ts +11 -8
- package/src/components/messages.ts +5 -2
- package/src/components/panel.ts +31 -5
- package/src/components/pill-composer-builder.test.ts +18 -0
- package/src/components/suggestions.test.ts +233 -0
- package/src/components/suggestions.ts +463 -81
- package/src/components/widget-view.ts +4 -0
- package/src/defaults.ts +5 -3
- package/src/generated/runtype-openapi-contract.ts +17 -0
- package/src/index-core.ts +52 -2
- package/src/launcher-global.ts +1 -1
- package/src/plugins/types.ts +71 -3
- package/src/presets.ts +3 -3
- package/src/session.ts +55 -12
- package/src/styles/widget-styles.test.ts +162 -0
- package/src/styles/widget.css +440 -1
- package/src/suggest-replies-tool.test.ts +233 -2
- package/src/suggest-replies-tool.ts +115 -39
- package/src/theme-editor/index.ts +5 -0
- package/src/theme-editor/presets.test.ts +43 -0
- package/src/theme-editor/presets.ts +101 -87
- package/src/theme-editor/preview-utils.test.ts +34 -0
- package/src/theme-editor/preview-utils.ts +77 -2
- package/src/theme-editor/sections.test.ts +72 -7
- package/src/theme-editor/sections.ts +113 -13
- package/src/theme-editor/state-preferences.test.ts +15 -0
- package/src/theme-editor/state.ts +46 -0
- package/src/theme-editor/types.ts +15 -1
- package/src/theme-editor/webmcp/tools.test.ts +13 -0
- package/src/theme-editor/webmcp/tools.ts +5 -0
- package/src/theme-reference.ts +9 -2
- package/src/types/theme.ts +32 -0
- package/src/types.ts +291 -25
- package/src/ui.artifact-pane-gating.test.ts +5 -5
- package/src/ui.config-preferences.test.ts +166 -0
- package/src/ui.detached-panel.test.ts +1 -1
- package/src/ui.suggest-replies.test.ts +1066 -1
- package/src/ui.ts +311 -45
- package/src/ui.typing-indicator.test.ts +135 -0
- package/src/utils/artifact-display.test.ts +180 -16
- package/src/utils/artifact-display.ts +155 -14
- package/src/utils/artifact-file.test.ts +1 -1
- package/src/utils/code-generators.test.ts +40 -0
- package/src/utils/code-generators.ts +22 -0
- package/src/utils/config-merge.test.ts +84 -0
- package/src/utils/config-merge.ts +6 -1
- package/src/utils/feature-preferences.test.ts +326 -0
- package/src/utils/feature-preferences.ts +498 -0
- package/src/utils/icons.test.ts +26 -0
- package/src/utils/icons.ts +8 -0
- package/src/utils/theme.test.ts +171 -0
- package/src/utils/tokens.ts +71 -7
- package/dist/chunk-MMUPR2JW.js +0 -1
|
@@ -5,15 +5,18 @@
|
|
|
5
5
|
* `clientTools[]` (set `features.askUserQuestion.expose: true`): the same
|
|
6
6
|
* wire surface WebMCP page tools ride. The server registers it as a LOCAL
|
|
7
7
|
* tool under its bare name (`origin: 'sdk'` tools are not `webmcp:`-prefixed),
|
|
8
|
-
* so when the model calls it the execution pauses with a `step_await
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* so when the model calls it the execution pauses with a `step_await`, the
|
|
9
|
+
* widget's answer-pill sheet renders, and `session.resolveAskUserQuestion()`
|
|
10
|
+
* resumes the execution with the structured answers. The public await frame
|
|
11
|
+
* for a local-tool pause carries no `awaitReason`; that discriminator is
|
|
12
|
+
* internal, and its absence is what marks a client-resolvable pause.
|
|
12
13
|
*
|
|
13
14
|
* This replaces the previous integrator burden of hand-declaring the tool in
|
|
14
15
|
* a flow's `runtimeTools` and keeping that schema in sync with the renderer.
|
|
15
16
|
* Flows that already declare `ask_user_question` server-side should leave
|
|
16
|
-
* `expose` off: the
|
|
17
|
+
* `expose` off: the API dedupes a double declaration by name with
|
|
18
|
+
* `clientTools[]` winning, so the model still sees exactly one tool, but the
|
|
19
|
+
* widget's description and schema silently replace the flow's.
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
22
|
import {
|
|
@@ -22,7 +25,7 @@ import {
|
|
|
22
25
|
} from "./components/ask-user-question-bubble";
|
|
23
26
|
import {
|
|
24
27
|
SUGGEST_REPLIES_CLIENT_TOOL,
|
|
25
|
-
|
|
28
|
+
resolveFollowUpsFeature,
|
|
26
29
|
} from "./suggest-replies-tool";
|
|
27
30
|
import type { AgentWidgetConfig, ClientToolDefinition } from "./types";
|
|
28
31
|
|
|
@@ -129,7 +132,7 @@ export const builtInClientToolsForDispatch = (
|
|
|
129
132
|
if (ask?.expose === true && ask.enabled !== false) {
|
|
130
133
|
tools.push(ASK_USER_QUESTION_CLIENT_TOOL);
|
|
131
134
|
}
|
|
132
|
-
if (
|
|
135
|
+
if (resolveFollowUpsFeature(config).expose) {
|
|
133
136
|
tools.push(SUGGEST_REPLIES_CLIENT_TOOL);
|
|
134
137
|
}
|
|
135
138
|
return tools;
|
package/src/client.test.ts
CHANGED
|
@@ -4505,10 +4505,10 @@ describe('AgentWidgetClient - Artifact display modes', () => {
|
|
|
4505
4505
|
});
|
|
4506
4506
|
});
|
|
4507
4507
|
|
|
4508
|
-
it('resolves the mode per artifact
|
|
4508
|
+
it('resolves the mode per artifact kind via display.byKind', async () => {
|
|
4509
4509
|
const display: AgentWidgetArtifactsFeature['display'] = {
|
|
4510
4510
|
default: 'card',
|
|
4511
|
-
|
|
4511
|
+
byKind: { component: 'inline' },
|
|
4512
4512
|
};
|
|
4513
4513
|
|
|
4514
4514
|
const mdEvents = await dispatchAndCollect(markdownArtifactFrames(), {
|
package/src/client.ts
CHANGED
|
@@ -3184,7 +3184,10 @@ export class AgentWidgetClient {
|
|
|
3184
3184
|
artifactIdsWithCards.add(artId);
|
|
3185
3185
|
const displayMode = resolveArtifactDisplayMode(
|
|
3186
3186
|
this.config.features?.artifacts,
|
|
3187
|
-
|
|
3187
|
+
{
|
|
3188
|
+
artifactType: at,
|
|
3189
|
+
...(artFile ? { file: artFile } : {}),
|
|
3190
|
+
}
|
|
3188
3191
|
);
|
|
3189
3192
|
const artComponent =
|
|
3190
3193
|
typeof payload.component === "string" ? payload.component : undefined;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ComponentContext, ComponentRenderer } from "./registry";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
PersonaArtifactFileMeta,
|
|
4
|
+
PersonaArtifactPresentation,
|
|
5
|
+
PersonaArtifactRecord
|
|
6
|
+
} from "../types";
|
|
3
7
|
import { fileTypeLabel, basenameOf } from "../utils/artifact-file";
|
|
4
8
|
import {
|
|
5
9
|
applyArtifactStatus,
|
|
@@ -33,6 +37,12 @@ function renderDefaultArtifactCard(
|
|
|
33
37
|
const status = props.status === "streaming" ? "streaming" : "complete";
|
|
34
38
|
const artifactType =
|
|
35
39
|
typeof props.artifactType === "string" ? props.artifactType : "markdown";
|
|
40
|
+
const presentation =
|
|
41
|
+
props.presentation &&
|
|
42
|
+
typeof props.presentation === "object" &&
|
|
43
|
+
!Array.isArray(props.presentation)
|
|
44
|
+
? (props.presentation as PersonaArtifactPresentation)
|
|
45
|
+
: undefined;
|
|
36
46
|
const subtitle = file
|
|
37
47
|
? fileTypeLabel(file)
|
|
38
48
|
: artifactType === "component"
|
|
@@ -83,7 +93,8 @@ function renderDefaultArtifactCard(
|
|
|
83
93
|
title: rawTitle,
|
|
84
94
|
status: "streaming",
|
|
85
95
|
...(typeof props.markdown === "string" ? { markdown: props.markdown } : {}),
|
|
86
|
-
...(file ? { file } : {})
|
|
96
|
+
...(file ? { file } : {}),
|
|
97
|
+
...(presentation ? { presentation } : {})
|
|
87
98
|
};
|
|
88
99
|
const resolved = resolveArtifactStatusLabel(record, artifactsCfg, "card");
|
|
89
100
|
applyArtifactStatus(subtitleEl, resolved, artifactsCfg);
|
|
@@ -2,6 +2,7 @@ import type { ComponentContext, ComponentRenderer } from "./registry";
|
|
|
2
2
|
import type {
|
|
3
3
|
AgentWidgetArtifactsFeature,
|
|
4
4
|
PersonaArtifactFileMeta,
|
|
5
|
+
PersonaArtifactPresentation,
|
|
5
6
|
PersonaArtifactRecord
|
|
6
7
|
} from "../types";
|
|
7
8
|
import { createElement } from "../utils/dom";
|
|
@@ -126,6 +127,12 @@ function recordFromProps(props: Record<string, unknown>): PersonaArtifactRecord
|
|
|
126
127
|
const title =
|
|
127
128
|
typeof props.title === "string" && props.title ? props.title : undefined;
|
|
128
129
|
const status = props.status === "streaming" ? "streaming" : "complete";
|
|
130
|
+
const presentation =
|
|
131
|
+
props.presentation &&
|
|
132
|
+
typeof props.presentation === "object" &&
|
|
133
|
+
!Array.isArray(props.presentation)
|
|
134
|
+
? (props.presentation as PersonaArtifactPresentation)
|
|
135
|
+
: undefined;
|
|
129
136
|
if (props.artifactType === "component") {
|
|
130
137
|
const embedded = props.componentProps;
|
|
131
138
|
const componentProps =
|
|
@@ -138,7 +145,8 @@ function recordFromProps(props: Record<string, unknown>): PersonaArtifactRecord
|
|
|
138
145
|
title,
|
|
139
146
|
status,
|
|
140
147
|
component: typeof props.component === "string" ? props.component : "",
|
|
141
|
-
props: componentProps
|
|
148
|
+
props: componentProps,
|
|
149
|
+
...(presentation ? { presentation } : {})
|
|
142
150
|
};
|
|
143
151
|
}
|
|
144
152
|
const file =
|
|
@@ -151,7 +159,8 @@ function recordFromProps(props: Record<string, unknown>): PersonaArtifactRecord
|
|
|
151
159
|
title,
|
|
152
160
|
status,
|
|
153
161
|
markdown: typeof props.markdown === "string" ? props.markdown : "",
|
|
154
|
-
...(file ? { file } : {})
|
|
162
|
+
...(file ? { file } : {}),
|
|
163
|
+
...(presentation ? { presentation } : {})
|
|
155
164
|
};
|
|
156
165
|
}
|
|
157
166
|
|
|
@@ -670,7 +670,7 @@ export function renderArtifactPreviewBody(
|
|
|
670
670
|
|
|
671
671
|
// Render the syntax-highlighted source. On the inline path the <pre> lives in
|
|
672
672
|
// a scroll window (`fixed` → reserved height + internal scroll + follow/fade);
|
|
673
|
-
// on the pane path it is a bare child of `el`,
|
|
673
|
+
// on the pane path it is a bare child of `el`, unchanged from before.
|
|
674
674
|
const renderSource = (
|
|
675
675
|
text: string,
|
|
676
676
|
opts: { language?: string; path?: string } | undefined,
|
|
@@ -17,6 +17,8 @@ describe("buildComposer (full column-stacked composer)", () => {
|
|
|
17
17
|
expect(elements.composerForm.tagName).toBe("FORM");
|
|
18
18
|
expect(elements.composerForm.getAttribute("data-persona-composer-form")).toBe("");
|
|
19
19
|
expect(elements.composerForm.classList.contains("persona-flex-col")).toBe(true);
|
|
20
|
+
expect(elements.composerForm.classList.contains("persona-border-persona-border")).toBe(true);
|
|
21
|
+
expect(elements.composerForm.classList.contains("persona-border-gray-200")).toBe(false);
|
|
20
22
|
|
|
21
23
|
expect(elements.textarea.getAttribute("data-persona-composer-input")).toBe("");
|
|
22
24
|
expect(elements.sendButton.getAttribute("data-persona-composer-submit")).toBe("");
|
|
@@ -67,7 +67,7 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
|
|
|
67
67
|
|
|
68
68
|
const composerForm = createNode("form", {
|
|
69
69
|
className:
|
|
70
|
-
"persona-widget-composer persona-flex persona-flex-col persona-gap-2 persona-rounded-2xl persona-border persona-border-
|
|
70
|
+
"persona-widget-composer persona-flex persona-flex-col persona-gap-2 persona-rounded-2xl persona-border persona-border-persona-border persona-bg-persona-input-background persona-px-4 persona-py-3",
|
|
71
71
|
attrs: { "data-persona-composer-form": "" },
|
|
72
72
|
style: { outline: "none" },
|
|
73
73
|
});
|
|
@@ -19,6 +19,8 @@ describe("createComposerTextarea", () => {
|
|
|
19
19
|
expect(textarea.tagName).toBe("TEXTAREA");
|
|
20
20
|
expect(textarea.getAttribute("data-persona-composer-input")).toBe("");
|
|
21
21
|
expect(textarea.classList.contains("persona-composer-textarea")).toBe(true);
|
|
22
|
+
expect(textarea.classList.contains("persona-text-persona-text")).toBe(true);
|
|
23
|
+
expect(textarea.classList.contains("persona-text-persona-primary")).toBe(false);
|
|
22
24
|
});
|
|
23
25
|
|
|
24
26
|
it("attachAutoResize wires an input listener that grows up to maxHeight", () => {
|
|
@@ -42,8 +44,59 @@ describe("createComposerTextarea", () => {
|
|
|
42
44
|
Object.defineProperty(textarea, "scrollHeight", { configurable: true, value: 10000 });
|
|
43
45
|
textarea.dispatchEvent(new Event("input"));
|
|
44
46
|
expect(textarea.style.height).toBe("200px");
|
|
47
|
+
// The override keeps winning on later input events (pill composer path).
|
|
48
|
+
textarea.dispatchEvent(new Event("input"));
|
|
49
|
+
expect(textarea.style.height).toBe("200px");
|
|
50
|
+
expect(textarea.style.maxHeight).toBe("200px");
|
|
51
|
+
document.body.removeChild(textarea);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("caps at 3 lines of the rendered line height when themed", () => {
|
|
55
|
+
const { textarea, attachAutoResize } = createComposerTextarea(baseConfig);
|
|
56
|
+
textarea.style.lineHeight = "24px";
|
|
57
|
+
document.body.appendChild(textarea);
|
|
58
|
+
attachAutoResize();
|
|
59
|
+
Object.defineProperty(textarea, "scrollHeight", { configurable: true, value: 10000 });
|
|
60
|
+
textarea.dispatchEvent(new Event("input"));
|
|
61
|
+
expect(textarea.style.height).toBe("72px");
|
|
62
|
+
expect(textarea.style.maxHeight).toBe("72px");
|
|
45
63
|
document.body.removeChild(textarea);
|
|
46
64
|
});
|
|
65
|
+
|
|
66
|
+
it("defaults to 60px (3 × 20px) before any resize", () => {
|
|
67
|
+
const { textarea } = createComposerTextarea(baseConfig);
|
|
68
|
+
expect(textarea.style.maxHeight).toBe("60px");
|
|
69
|
+
expect(textarea.style.overflowY).toBe("auto");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("falls back to 60px when the line height is `normal`", () => {
|
|
73
|
+
const { textarea, attachAutoResize } = createComposerTextarea(baseConfig);
|
|
74
|
+
textarea.style.lineHeight = "normal";
|
|
75
|
+
document.body.appendChild(textarea);
|
|
76
|
+
attachAutoResize();
|
|
77
|
+
Object.defineProperty(textarea, "scrollHeight", { configurable: true, value: 10000 });
|
|
78
|
+
textarea.dispatchEvent(new Event("input"));
|
|
79
|
+
expect(textarea.style.height).toBe("60px");
|
|
80
|
+
document.body.removeChild(textarea);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("falls back to 60px for unitless line heights and detached nodes", () => {
|
|
84
|
+
const unitless = createComposerTextarea(baseConfig);
|
|
85
|
+
unitless.textarea.style.lineHeight = "1.5";
|
|
86
|
+
document.body.appendChild(unitless.textarea);
|
|
87
|
+
unitless.attachAutoResize();
|
|
88
|
+
Object.defineProperty(unitless.textarea, "scrollHeight", { configurable: true, value: 10000 });
|
|
89
|
+
unitless.textarea.dispatchEvent(new Event("input"));
|
|
90
|
+
expect(unitless.textarea.style.height).toBe("60px");
|
|
91
|
+
document.body.removeChild(unitless.textarea);
|
|
92
|
+
|
|
93
|
+
// Never appended: getComputedStyle still must not throw or produce NaN.
|
|
94
|
+
const detached = createComposerTextarea(baseConfig);
|
|
95
|
+
detached.attachAutoResize();
|
|
96
|
+
Object.defineProperty(detached.textarea, "scrollHeight", { configurable: true, value: 10000 });
|
|
97
|
+
detached.textarea.dispatchEvent(new Event("input"));
|
|
98
|
+
expect(detached.textarea.style.height).toBe("60px");
|
|
99
|
+
});
|
|
47
100
|
});
|
|
48
101
|
|
|
49
102
|
describe("createSendButton", () => {
|
|
@@ -95,10 +148,12 @@ describe("createSendButton", () => {
|
|
|
95
148
|
expect(s.borderWidth).toBe("2px");
|
|
96
149
|
expect(s.borderStyle).toBe("solid");
|
|
97
150
|
expect(s.borderColor).toBe("rgb(7, 8, 9)");
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
expect(s.
|
|
101
|
-
expect(s.
|
|
151
|
+
// Icon mode zeroes padding even when configured: the glyph is drawn at
|
|
152
|
+
// the button size, so padding would crush it.
|
|
153
|
+
expect(s.paddingLeft).toBe("0px");
|
|
154
|
+
expect(s.paddingRight).toBe("0px");
|
|
155
|
+
expect(s.paddingTop).toBe("0px");
|
|
156
|
+
expect(s.paddingBottom).toBe("0px");
|
|
102
157
|
// An explicit backgroundColor means the fallback class is not added.
|
|
103
158
|
expect(send.button.classList.contains("persona-bg-persona-primary")).toBe(false);
|
|
104
159
|
});
|
|
@@ -140,6 +195,52 @@ describe("createSendButton", () => {
|
|
|
140
195
|
expect(iconCount(send.button)).toBe(1);
|
|
141
196
|
});
|
|
142
197
|
|
|
198
|
+
it("zeroes padding so configured padding cannot crush the glyph", () => {
|
|
199
|
+
const send = createSendButton({
|
|
200
|
+
...baseConfig,
|
|
201
|
+
sendButton: {
|
|
202
|
+
useIcon: true,
|
|
203
|
+
iconName: "send",
|
|
204
|
+
size: "40px",
|
|
205
|
+
paddingX: "12px",
|
|
206
|
+
paddingY: "10px",
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
expect(send.button.style.paddingLeft).toBe("0px");
|
|
210
|
+
expect(send.button.style.paddingTop).toBe("0px");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("draws the glyph at half the button by default and honors iconSize", () => {
|
|
214
|
+
const send = createSendButton({
|
|
215
|
+
...baseConfig,
|
|
216
|
+
sendButton: { useIcon: true, iconName: "send", size: "32px" },
|
|
217
|
+
});
|
|
218
|
+
expect(send.button.querySelector("svg")?.getAttribute("width")).toBe("16");
|
|
219
|
+
|
|
220
|
+
const sized = createSendButton({
|
|
221
|
+
...baseConfig,
|
|
222
|
+
sendButton: {
|
|
223
|
+
useIcon: true,
|
|
224
|
+
iconName: "send",
|
|
225
|
+
size: "32px",
|
|
226
|
+
iconSize: "26px",
|
|
227
|
+
iconStrokeWidth: 2.5,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
const svg = sized.button.querySelector("svg");
|
|
231
|
+
expect(svg?.getAttribute("width")).toBe("26");
|
|
232
|
+
expect(svg?.getAttribute("stroke-width")).toBe("2.5");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("keeps configured padding in text mode", () => {
|
|
236
|
+
const send = createSendButton({
|
|
237
|
+
...baseConfig,
|
|
238
|
+
sendButton: { useIcon: false, paddingX: "12px", paddingY: "10px" },
|
|
239
|
+
});
|
|
240
|
+
expect(send.button.style.paddingLeft).toBe("12px");
|
|
241
|
+
expect(send.button.style.paddingTop).toBe("10px");
|
|
242
|
+
});
|
|
243
|
+
|
|
143
244
|
it("does not stack a stale icon when an external re-render swapped the live icon node", () => {
|
|
144
245
|
const send = createSendButton(iconConfig);
|
|
145
246
|
// Simulate a DOM morph/re-render (e.g. a host calling controller.update())
|
|
@@ -32,26 +32,54 @@ export const createComposerTextarea = (config?: AgentWidgetConfig): ComposerText
|
|
|
32
32
|
textarea.setAttribute("data-persona-composer-input", "");
|
|
33
33
|
textarea.placeholder = config?.copy?.inputPlaceholder ?? "Type your message…";
|
|
34
34
|
textarea.className =
|
|
35
|
-
"persona-w-full persona-min-h-[24px] persona-resize-none persona-border-none persona-bg-transparent persona-text-sm persona-text-persona-
|
|
35
|
+
"persona-w-full persona-min-h-[24px] persona-resize-none persona-border-none persona-bg-transparent persona-text-sm persona-text-persona-text focus:persona-outline-none focus:persona-border-none persona-composer-textarea";
|
|
36
36
|
textarea.rows = 1;
|
|
37
37
|
|
|
38
38
|
textarea.style.fontFamily =
|
|
39
39
|
'var(--persona-input-font-family, var(--persona-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif))';
|
|
40
40
|
textarea.style.fontWeight = "var(--persona-input-font-weight, var(--persona-font-weight, 400))";
|
|
41
41
|
|
|
42
|
-
// Auto-resize: expand up to 3 lines for the full composer
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
42
|
+
// Auto-resize: expand up to 3 lines for the full composer. The pill composer
|
|
43
|
+
// overrides this maxHeight after construction (allowing more growth in
|
|
44
|
+
// expanded mode), and the closure below honors whatever maxHeight is set at
|
|
45
|
+
// the time of the input event.
|
|
46
46
|
const defaultMaxLines = 3;
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Used only when the rendered line height can't be resolved (detached node,
|
|
48
|
+
// jsdom, `normal`); matches the `persona-text-sm` / composer default.
|
|
49
|
+
const fallbackLineHeight = 20;
|
|
50
|
+
|
|
51
|
+
// `theme.components.composer.lineHeight` (--persona-composer-line-height) can
|
|
52
|
+
// change the rendered line height, so 3 lines is only correct when measured
|
|
53
|
+
// from the live element. Read lazily: at construction the textarea is usually
|
|
54
|
+
// still detached and getComputedStyle would report nothing useful.
|
|
55
|
+
const readLineHeight = (): number => {
|
|
56
|
+
const view = textarea.ownerDocument?.defaultView;
|
|
57
|
+
if (!view?.getComputedStyle) return fallbackLineHeight;
|
|
58
|
+
// Only px values are meaningful here: `normal` and unitless ratios aren't
|
|
59
|
+
// line box heights we can multiply.
|
|
60
|
+
const computed = view.getComputedStyle(textarea).lineHeight;
|
|
61
|
+
if (!computed || !computed.trim().endsWith("px")) return fallbackLineHeight;
|
|
62
|
+
const parsed = parseFloat(computed);
|
|
63
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackLineHeight;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Tracks the last maxHeight this module wrote, so a caller's post-construction
|
|
67
|
+
// override is distinguishable from our own default and always wins.
|
|
68
|
+
let managedMaxHeight = `${defaultMaxLines * fallbackLineHeight}px`;
|
|
69
|
+
textarea.style.maxHeight = managedMaxHeight;
|
|
49
70
|
textarea.style.overflowY = "auto";
|
|
50
71
|
|
|
51
72
|
// Read maxHeight at event time so callers can change it after construction.
|
|
52
73
|
const readMaxHeight = (): number => {
|
|
53
|
-
const
|
|
54
|
-
|
|
74
|
+
const current = textarea.style.maxHeight;
|
|
75
|
+
if (current !== managedMaxHeight) {
|
|
76
|
+
const overridden = parseFloat(current);
|
|
77
|
+
if (Number.isFinite(overridden) && overridden > 0) return overridden;
|
|
78
|
+
}
|
|
79
|
+
const derived = defaultMaxLines * readLineHeight();
|
|
80
|
+
managedMaxHeight = `${derived}px`;
|
|
81
|
+
textarea.style.maxHeight = managedMaxHeight;
|
|
82
|
+
return derived;
|
|
55
83
|
};
|
|
56
84
|
|
|
57
85
|
const attachAutoResize = () => {
|
|
@@ -142,10 +170,13 @@ export const createSendButton = (config?: AgentWidgetConfig): SendButtonParts =>
|
|
|
142
170
|
borderWidth: sendButtonConfig.borderWidth || undefined,
|
|
143
171
|
borderStyle: sendButtonConfig.borderWidth ? "solid" : undefined,
|
|
144
172
|
borderColor: sendButtonConfig.borderColor || undefined,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
// Padding is text-mode-only: icon mode is a fixed `size` box with the
|
|
174
|
+
// glyph drawn at that size, so padding just crushes the SVG (the
|
|
175
|
+
// always-present defaults squeezed a 40px glyph into 16x20).
|
|
176
|
+
paddingLeft: useIcon ? "0" : sendButtonConfig.paddingX || undefined,
|
|
177
|
+
paddingRight: useIcon ? "0" : sendButtonConfig.paddingX || undefined,
|
|
178
|
+
paddingTop: useIcon ? "0" : sendButtonConfig.paddingY || undefined,
|
|
179
|
+
paddingBottom: useIcon ? "0" : sendButtonConfig.paddingY || undefined,
|
|
149
180
|
},
|
|
150
181
|
});
|
|
151
182
|
|
|
@@ -154,11 +185,18 @@ export const createSendButton = (config?: AgentWidgetConfig): SendButtonParts =>
|
|
|
154
185
|
let stopIcon: SVGElement | null = null;
|
|
155
186
|
|
|
156
187
|
if (useIcon) {
|
|
157
|
-
|
|
188
|
+
// Default glyph box is half the button, the closest clean ratio to the
|
|
189
|
+
// pre-fix live rendering (a 40px glyph crushed to a ~16px content box).
|
|
190
|
+
// Dense glyphs like the default "send" plane fill most of their viewBox;
|
|
191
|
+
// sparse ones (arrow-up) may want a larger explicit `iconSize`.
|
|
192
|
+
const iconSize =
|
|
193
|
+
parseFloat(sendButtonConfig.iconSize ?? "") ||
|
|
194
|
+
Math.round((parseFloat(buttonSize) || 24) * 0.5);
|
|
195
|
+
const iconStroke = sendButtonConfig.iconStrokeWidth ?? 2;
|
|
158
196
|
const iconColor = textColor?.trim() || "currentColor";
|
|
159
197
|
|
|
160
198
|
if (iconName) {
|
|
161
|
-
sendIcon = renderLucideIcon(iconName, iconSize, iconColor,
|
|
199
|
+
sendIcon = renderLucideIcon(iconName, iconSize, iconColor, iconStroke);
|
|
162
200
|
if (sendIcon) {
|
|
163
201
|
button.appendChild(sendIcon);
|
|
164
202
|
} else {
|
|
@@ -168,7 +206,7 @@ export const createSendButton = (config?: AgentWidgetConfig): SendButtonParts =>
|
|
|
168
206
|
button.textContent = iconText;
|
|
169
207
|
}
|
|
170
208
|
|
|
171
|
-
stopIcon = renderLucideIcon(stopIconName, iconSize, iconColor,
|
|
209
|
+
stopIcon = renderLucideIcon(stopIconName, iconSize, iconColor, iconStroke);
|
|
172
210
|
} else {
|
|
173
211
|
button.textContent = sendLabel;
|
|
174
212
|
}
|
|
@@ -276,7 +276,7 @@ describe("createStandardBubble", () => {
|
|
|
276
276
|
expect(userBubble.classList.contains("persona-bg-persona-accent")).toBe(true);
|
|
277
277
|
expect(userBubble.classList.contains("persona-rounded-lg")).toBe(true);
|
|
278
278
|
expect(userBubble.classList.contains("persona-px-3")).toBe(true);
|
|
279
|
-
expect(assistantBubble.classList.contains("persona-bg-persona-
|
|
279
|
+
expect(assistantBubble.classList.contains("persona-bg-persona-container")).toBe(
|
|
280
280
|
true
|
|
281
281
|
);
|
|
282
282
|
expect(assistantBubble.classList.contains("persona-rounded-lg")).toBe(true);
|
|
@@ -302,6 +302,12 @@ describe("createStandardBubble", () => {
|
|
|
302
302
|
|
|
303
303
|
expect(userMessage.style.backgroundColor).toBe("transparent");
|
|
304
304
|
expect(assistantMessage.style.backgroundColor).toBe("transparent");
|
|
305
|
+
expect(assistantMessage.style.color).toBe(
|
|
306
|
+
"var(--persona-message-assistant-text, var(--persona-text))"
|
|
307
|
+
);
|
|
308
|
+
expect(assistantMessage.classList.contains("persona-text-persona-text")).toBe(
|
|
309
|
+
true
|
|
310
|
+
);
|
|
305
311
|
expect(userMessage.classList.contains("persona-px-3")).toBe(false);
|
|
306
312
|
expect(assistantMessage.classList.contains("persona-px-3")).toBe(false);
|
|
307
313
|
});
|
|
@@ -649,7 +649,7 @@ const createTimestamp = (
|
|
|
649
649
|
/**
|
|
650
650
|
* Get bubble classes based on layout preset
|
|
651
651
|
*/
|
|
652
|
-
const getBubbleClasses = (
|
|
652
|
+
export const getBubbleClasses = (
|
|
653
653
|
role: "user" | "assistant" | "system",
|
|
654
654
|
layout: AgentWidgetMessageLayoutConfig["layout"] = "bubble"
|
|
655
655
|
): string[] => {
|
|
@@ -668,7 +668,7 @@ const getBubbleClasses = (
|
|
|
668
668
|
} else {
|
|
669
669
|
baseClasses.push(
|
|
670
670
|
"persona-message-assistant-bubble",
|
|
671
|
-
"persona-text-persona-
|
|
671
|
+
"persona-text-persona-text",
|
|
672
672
|
"persona-py-2"
|
|
673
673
|
);
|
|
674
674
|
}
|
|
@@ -693,8 +693,8 @@ const getBubbleClasses = (
|
|
|
693
693
|
} else {
|
|
694
694
|
baseClasses.push(
|
|
695
695
|
"persona-message-assistant-bubble",
|
|
696
|
-
"persona-bg-persona-
|
|
697
|
-
"persona-text-persona-
|
|
696
|
+
"persona-bg-persona-container",
|
|
697
|
+
"persona-text-persona-text",
|
|
698
698
|
"persona-px-3",
|
|
699
699
|
"persona-py-2",
|
|
700
700
|
"persona-rounded-lg"
|
|
@@ -723,10 +723,10 @@ const getBubbleClasses = (
|
|
|
723
723
|
} else {
|
|
724
724
|
baseClasses.push(
|
|
725
725
|
"persona-message-assistant-bubble",
|
|
726
|
-
"persona-bg-persona-
|
|
726
|
+
"persona-bg-persona-container",
|
|
727
727
|
"persona-border",
|
|
728
728
|
"persona-border-persona-message-border",
|
|
729
|
-
"persona-text-persona-
|
|
729
|
+
"persona-text-persona-text",
|
|
730
730
|
"persona-px-5",
|
|
731
731
|
"persona-py-3"
|
|
732
732
|
);
|
|
@@ -879,12 +879,15 @@ export const createStandardBubble = (
|
|
|
879
879
|
// Apply component-level color overrides via CSS variables
|
|
880
880
|
if (layout === "flat") {
|
|
881
881
|
bubble.style.backgroundColor = "transparent";
|
|
882
|
-
bubble.style.color =
|
|
882
|
+
bubble.style.color =
|
|
883
|
+
message.role === "user"
|
|
884
|
+
? "var(--persona-primary, var(--persona-text))"
|
|
885
|
+
: "var(--persona-message-assistant-text, var(--persona-text))";
|
|
883
886
|
} else if (message.role === "user") {
|
|
884
887
|
bubble.style.backgroundColor = 'var(--persona-message-user-bg, var(--persona-accent))';
|
|
885
888
|
bubble.style.color = 'var(--persona-message-user-text, white)';
|
|
886
889
|
} else if (message.role === "assistant") {
|
|
887
|
-
bubble.style.backgroundColor = 'var(--persona-message-assistant-bg, var(--persona-
|
|
890
|
+
bubble.style.backgroundColor = 'var(--persona-message-assistant-bg, var(--persona-container))';
|
|
888
891
|
bubble.style.color = 'var(--persona-message-assistant-text, var(--persona-text))';
|
|
889
892
|
}
|
|
890
893
|
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
isAskUserQuestionMessage,
|
|
10
10
|
removeAskUserQuestionSheet,
|
|
11
11
|
} from "./ask-user-question-bubble";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
isSuggestRepliesMessage,
|
|
14
|
+
resolveFollowUpsFeature,
|
|
15
|
+
} from "../suggest-replies-tool";
|
|
13
16
|
|
|
14
17
|
export const renderMessages = (
|
|
15
18
|
container: HTMLElement,
|
|
@@ -43,7 +46,7 @@ export const renderMessages = (
|
|
|
43
46
|
return;
|
|
44
47
|
} else if (
|
|
45
48
|
isSuggestRepliesMessage(message) &&
|
|
46
|
-
config
|
|
49
|
+
resolveFollowUpsFeature(config).enabled
|
|
47
50
|
) {
|
|
48
51
|
// No transcript bubble: the chips above the composer are the only UI.
|
|
49
52
|
// When the feature is disabled the message falls through to the generic
|
package/src/components/panel.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createElement, createNode } from "../utils/dom";
|
|
2
2
|
import { DEFAULT_FLOATING_LAUNCHER_WIDTH } from "../defaults";
|
|
3
|
-
import { AgentWidgetConfig } from "../types";
|
|
3
|
+
import { AgentWidgetConfig, AgentWidgetSuggestionSurface } from "../types";
|
|
4
|
+
import { suggestionSurfaceAttr } from "./suggestions";
|
|
4
5
|
import { positionMap } from "../utils/positioning";
|
|
5
6
|
import { isComposerBarMountMode, isDockedMountMode } from "../utils/dock";
|
|
6
7
|
import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
|
|
@@ -132,6 +133,10 @@ export interface PanelElements {
|
|
|
132
133
|
container: HTMLElement;
|
|
133
134
|
body: HTMLElement;
|
|
134
135
|
messagesWrapper: HTMLElement;
|
|
136
|
+
/** Starter prompts rendered inside the welcome card. */
|
|
137
|
+
starterSuggestions: HTMLElement;
|
|
138
|
+
/** Follow-up suggestions rendered after the transcript. */
|
|
139
|
+
transcriptSuggestions: HTMLElement;
|
|
135
140
|
/**
|
|
136
141
|
* Absolute-positioned slot above the composer footer. Interactive sheets
|
|
137
142
|
* (e.g. the answer-pill sheet for the ask_user_question tool) mount here
|
|
@@ -178,6 +183,17 @@ export interface PanelElements {
|
|
|
178
183
|
peekTextNode?: HTMLElement;
|
|
179
184
|
}
|
|
180
185
|
|
|
186
|
+
const createSuggestionHost = (
|
|
187
|
+
surface: AgentWidgetSuggestionSurface
|
|
188
|
+
): HTMLElement => {
|
|
189
|
+
const host = createNode("div", {
|
|
190
|
+
className: "persona-suggestions",
|
|
191
|
+
attrs: { "data-persona-suggestions": suggestionSurfaceAttr(surface) },
|
|
192
|
+
});
|
|
193
|
+
host.hidden = true;
|
|
194
|
+
return host;
|
|
195
|
+
};
|
|
196
|
+
|
|
181
197
|
/**
|
|
182
198
|
* Composer-bar panel: minimal close-only header (× in the top-right of the
|
|
183
199
|
* chat chrome). The pill (`footer`) and `peekBanner` are NOT children of
|
|
@@ -276,6 +292,7 @@ const buildComposerBarPanel = (
|
|
|
276
292
|
config?.copy?.welcomeSubtitle ??
|
|
277
293
|
"Ask anything about your account or products.",
|
|
278
294
|
});
|
|
295
|
+
const starterSuggestions = createSuggestionHost("starter");
|
|
279
296
|
const introCard = createNode(
|
|
280
297
|
"div",
|
|
281
298
|
{
|
|
@@ -287,13 +304,15 @@ const buildComposerBarPanel = (
|
|
|
287
304
|
},
|
|
288
305
|
},
|
|
289
306
|
introTitle,
|
|
290
|
-
introSubtitle
|
|
307
|
+
introSubtitle,
|
|
308
|
+
starterSuggestions
|
|
291
309
|
);
|
|
292
310
|
|
|
293
311
|
const messagesWrapper = createElement(
|
|
294
312
|
"div",
|
|
295
313
|
"persona-flex persona-flex-col persona-gap-3"
|
|
296
314
|
);
|
|
315
|
+
const transcriptSuggestions = createSuggestionHost("followUp");
|
|
297
316
|
const contentMaxWidth = config?.layout?.contentMaxWidth;
|
|
298
317
|
if (contentMaxWidth) {
|
|
299
318
|
messagesWrapper.style.maxWidth = contentMaxWidth;
|
|
@@ -308,7 +327,7 @@ const buildComposerBarPanel = (
|
|
|
308
327
|
body.classList.remove("persona-gap-6");
|
|
309
328
|
body.classList.add("persona-gap-3");
|
|
310
329
|
}
|
|
311
|
-
body.append(introCard, messagesWrapper);
|
|
330
|
+
body.append(introCard, messagesWrapper, transcriptSuggestions);
|
|
312
331
|
|
|
313
332
|
// Composer overlay (interactive sheets like ask_user_question slide up here).
|
|
314
333
|
// Anchored to the bottom of the container (which is `position: relative`),
|
|
@@ -338,6 +357,8 @@ const buildComposerBarPanel = (
|
|
|
338
357
|
container,
|
|
339
358
|
body,
|
|
340
359
|
messagesWrapper,
|
|
360
|
+
starterSuggestions,
|
|
361
|
+
transcriptSuggestions,
|
|
341
362
|
composerOverlay,
|
|
342
363
|
suggestions: composerElements.suggestions,
|
|
343
364
|
textarea: composerElements.textarea,
|
|
@@ -413,6 +434,7 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
413
434
|
config?.copy?.welcomeSubtitle ??
|
|
414
435
|
"Ask anything about your account or products.",
|
|
415
436
|
});
|
|
437
|
+
const starterSuggestions = createSuggestionHost("starter");
|
|
416
438
|
// Background and box-shadow flow through the themable `components.introCard`
|
|
417
439
|
// tokens (--persona-intro-card-bg / --persona-intro-card-shadow); both
|
|
418
440
|
// default to flat (transparent / none). Docked mode always stays flat,
|
|
@@ -430,13 +452,15 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
430
452
|
},
|
|
431
453
|
},
|
|
432
454
|
introTitle,
|
|
433
|
-
introSubtitle
|
|
455
|
+
introSubtitle,
|
|
456
|
+
starterSuggestions
|
|
434
457
|
);
|
|
435
458
|
|
|
436
459
|
const messagesWrapper = createElement(
|
|
437
460
|
"div",
|
|
438
461
|
"persona-flex persona-flex-col persona-gap-3"
|
|
439
462
|
);
|
|
463
|
+
const transcriptSuggestions = createSuggestionHost("followUp");
|
|
440
464
|
|
|
441
465
|
const contentMaxWidth = config?.layout?.contentMaxWidth;
|
|
442
466
|
if (contentMaxWidth) {
|
|
@@ -452,7 +476,7 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
452
476
|
body.classList.remove("persona-gap-6");
|
|
453
477
|
body.classList.add("persona-gap-3");
|
|
454
478
|
}
|
|
455
|
-
body.append(introCard, messagesWrapper);
|
|
479
|
+
body.append(introCard, messagesWrapper, transcriptSuggestions);
|
|
456
480
|
|
|
457
481
|
// composer-bar mode early-returned above with its own pill builder; this
|
|
458
482
|
// path always uses the standard column-stacked composer card.
|
|
@@ -500,6 +524,8 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
500
524
|
container,
|
|
501
525
|
body,
|
|
502
526
|
messagesWrapper,
|
|
527
|
+
starterSuggestions,
|
|
528
|
+
transcriptSuggestions,
|
|
503
529
|
composerOverlay,
|
|
504
530
|
suggestions: composerElements.suggestions,
|
|
505
531
|
textarea: composerElements.textarea,
|