@runtypelabs/persona 4.10.1 → 4.11.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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-Bx_rvjff.d.cts → types-DveIaNx6.d.cts} +2 -0
- package/dist/animations/{types-Bx_rvjff.d.ts → types-DveIaNx6.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-MMUPR2JW.js +1 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/{context-mentions-7S5KVUTG.js → context-mentions-ONG7A7M6.js} +1 -1
- package/dist/context-mentions.d.cts +26 -0
- package/dist/context-mentions.d.ts +26 -0
- package/dist/index.cjs +67 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.global.js +55 -55
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +78 -78
- package/dist/index.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +26 -0
- package/dist/smart-dom-reader.d.ts +26 -0
- package/dist/theme-editor-preview.cjs +66 -66
- package/dist/theme-editor-preview.d.cts +26 -0
- package/dist/theme-editor-preview.d.ts +26 -0
- package/dist/theme-editor-preview.js +55 -55
- package/dist/theme-editor.cjs +5 -5
- package/dist/theme-editor.d.cts +26 -0
- package/dist/theme-editor.d.ts +26 -0
- package/dist/theme-editor.js +9 -9
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/components/approval-bubble.ts +0 -1
- package/src/components/composer-parts.ts +19 -19
- package/src/components/context-mention-button.test.ts +5 -1
- package/src/components/context-mention-button.ts +6 -4
- package/src/components/header-builder.test.ts +3 -3
- package/src/components/header-parts.ts +14 -108
- package/src/components/message-bubble.test.ts +49 -0
- package/src/components/message-bubble.ts +8 -7
- package/src/components/reasoning-bubble.ts +0 -2
- package/src/components/tool-bubble.ts +0 -2
- package/src/generated/runtype-openapi-contract.ts +2 -0
- package/src/index-core.ts +1 -0
- package/src/styles/widget.css +118 -58
- package/src/theme-editor/sections.test.ts +33 -0
- package/src/theme-editor/sections.ts +10 -1
- package/src/types.ts +25 -0
- package/src/ui.attachments-drop.test.ts +2 -1
- package/src/ui.message-width.test.ts +407 -0
- package/src/ui.ts +145 -238
- package/src/utils/table-scroll-fade.test.ts +123 -0
- package/src/utils/table-scroll-fade.ts +75 -0
- package/src/utils/tooltip.test.ts +226 -0
- package/src/utils/tooltip.ts +245 -0
- package/dist/chunk-IPVK3KOM.js +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createElement,
|
|
1
|
+
import { createElement, createNode, cx } from "../utils/dom";
|
|
2
2
|
import { renderLucideIcon } from "../utils/icons";
|
|
3
3
|
import { AgentWidgetConfig } from "../types";
|
|
4
|
-
import {
|
|
4
|
+
import { attachTooltip } from "../utils/tooltip";
|
|
5
5
|
import { HEADER_THEME_CSS } from "./header-builder";
|
|
6
6
|
|
|
7
7
|
export interface CloseButtonParts {
|
|
@@ -136,59 +136,12 @@ export const createCloseButton = (
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
wrapper.appendChild(button);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const tooltipDocument = button.ownerDocument;
|
|
147
|
-
const tooltipContainer = tooltipDocument.body;
|
|
148
|
-
if (!tooltipContainer) return;
|
|
149
|
-
|
|
150
|
-
portaledTooltip = createElementInDocument(
|
|
151
|
-
tooltipDocument,
|
|
152
|
-
"div",
|
|
153
|
-
"persona-clear-chat-tooltip"
|
|
154
|
-
);
|
|
155
|
-
portaledTooltip.textContent = closeButtonTooltipText;
|
|
156
|
-
|
|
157
|
-
const arrow = createElementInDocument(tooltipDocument, "div");
|
|
158
|
-
arrow.className = "persona-clear-chat-tooltip-arrow";
|
|
159
|
-
portaledTooltip.appendChild(arrow);
|
|
160
|
-
|
|
161
|
-
const buttonRect = button.getBoundingClientRect();
|
|
162
|
-
|
|
163
|
-
portaledTooltip.style.position = "fixed";
|
|
164
|
-
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
165
|
-
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
166
|
-
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
167
|
-
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
168
|
-
|
|
169
|
-
tooltipContainer.appendChild(portaledTooltip);
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const hideTooltip = () => {
|
|
173
|
-
if (portaledTooltip && portaledTooltip.parentNode) {
|
|
174
|
-
portaledTooltip.parentNode.removeChild(portaledTooltip);
|
|
175
|
-
portaledTooltip = null;
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
wrapper.addEventListener("mouseenter", showTooltip);
|
|
180
|
-
wrapper.addEventListener("mouseleave", hideTooltip);
|
|
181
|
-
button.addEventListener("focus", showTooltip);
|
|
182
|
-
button.addEventListener("blur", hideTooltip);
|
|
183
|
-
|
|
184
|
-
(wrapper as any)._cleanupTooltip = () => {
|
|
185
|
-
hideTooltip();
|
|
186
|
-
wrapper.removeEventListener("mouseenter", showTooltip);
|
|
187
|
-
wrapper.removeEventListener("mouseleave", hideTooltip);
|
|
188
|
-
button.removeEventListener("focus", showTooltip);
|
|
189
|
-
button.removeEventListener("blur", hideTooltip);
|
|
190
|
-
};
|
|
191
|
-
}
|
|
139
|
+
attachTooltip({
|
|
140
|
+
anchor: button,
|
|
141
|
+
trigger: wrapper,
|
|
142
|
+
text: () => button.getAttribute("aria-label") ?? closeButtonTooltipText,
|
|
143
|
+
enabled: closeButtonShowTooltip,
|
|
144
|
+
});
|
|
192
145
|
|
|
193
146
|
return { button, wrapper };
|
|
194
147
|
};
|
|
@@ -271,59 +224,12 @@ export const createClearChatButton = (
|
|
|
271
224
|
}
|
|
272
225
|
|
|
273
226
|
wrapper.appendChild(button);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const tooltipDocument = button.ownerDocument;
|
|
282
|
-
const tooltipContainer = tooltipDocument.body;
|
|
283
|
-
if (!tooltipContainer) return;
|
|
284
|
-
|
|
285
|
-
portaledTooltip = createElementInDocument(
|
|
286
|
-
tooltipDocument,
|
|
287
|
-
"div",
|
|
288
|
-
"persona-clear-chat-tooltip"
|
|
289
|
-
);
|
|
290
|
-
portaledTooltip.textContent = clearChatTooltipText;
|
|
291
|
-
|
|
292
|
-
const arrow = createElementInDocument(tooltipDocument, "div");
|
|
293
|
-
arrow.className = "persona-clear-chat-tooltip-arrow";
|
|
294
|
-
portaledTooltip.appendChild(arrow);
|
|
295
|
-
|
|
296
|
-
const buttonRect = button.getBoundingClientRect();
|
|
297
|
-
|
|
298
|
-
portaledTooltip.style.position = "fixed";
|
|
299
|
-
portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
|
|
300
|
-
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
301
|
-
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
302
|
-
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
303
|
-
|
|
304
|
-
tooltipContainer.appendChild(portaledTooltip);
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
const hideTooltip = () => {
|
|
308
|
-
if (portaledTooltip && portaledTooltip.parentNode) {
|
|
309
|
-
portaledTooltip.parentNode.removeChild(portaledTooltip);
|
|
310
|
-
portaledTooltip = null;
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
wrapper.addEventListener("mouseenter", showTooltip);
|
|
315
|
-
wrapper.addEventListener("mouseleave", hideTooltip);
|
|
316
|
-
button.addEventListener("focus", showTooltip);
|
|
317
|
-
button.addEventListener("blur", hideTooltip);
|
|
318
|
-
|
|
319
|
-
(wrapper as any)._cleanupTooltip = () => {
|
|
320
|
-
hideTooltip();
|
|
321
|
-
wrapper.removeEventListener("mouseenter", showTooltip);
|
|
322
|
-
wrapper.removeEventListener("mouseleave", hideTooltip);
|
|
323
|
-
button.removeEventListener("focus", showTooltip);
|
|
324
|
-
button.removeEventListener("blur", hideTooltip);
|
|
325
|
-
};
|
|
326
|
-
}
|
|
227
|
+
attachTooltip({
|
|
228
|
+
anchor: button,
|
|
229
|
+
trigger: wrapper,
|
|
230
|
+
text: () => button.getAttribute("aria-label") ?? clearChatTooltipText,
|
|
231
|
+
enabled: clearChatShowTooltip,
|
|
232
|
+
});
|
|
327
233
|
|
|
328
234
|
return { button, wrapper };
|
|
329
235
|
};
|
|
@@ -257,6 +257,55 @@ describe("isSafeImageSrc", () => {
|
|
|
257
257
|
});
|
|
258
258
|
|
|
259
259
|
describe("createStandardBubble", () => {
|
|
260
|
+
it("uses a compact user bubble and borderless assistant response in minimal style", () => {
|
|
261
|
+
const userBubble = createStandardBubble(
|
|
262
|
+
makeMessage({ id: "minimal-user", role: "user", content: "Question" }),
|
|
263
|
+
({ text }) => text,
|
|
264
|
+
{ layout: "minimal" }
|
|
265
|
+
);
|
|
266
|
+
const assistantBubble = createStandardBubble(
|
|
267
|
+
makeMessage({
|
|
268
|
+
id: "minimal-assistant",
|
|
269
|
+
role: "assistant",
|
|
270
|
+
content: "Answer",
|
|
271
|
+
}),
|
|
272
|
+
({ text }) => text,
|
|
273
|
+
{ layout: "minimal" }
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(userBubble.classList.contains("persona-bg-persona-accent")).toBe(true);
|
|
277
|
+
expect(userBubble.classList.contains("persona-rounded-lg")).toBe(true);
|
|
278
|
+
expect(userBubble.classList.contains("persona-px-3")).toBe(true);
|
|
279
|
+
expect(assistantBubble.classList.contains("persona-bg-persona-surface")).toBe(
|
|
280
|
+
true
|
|
281
|
+
);
|
|
282
|
+
expect(assistantBubble.classList.contains("persona-rounded-lg")).toBe(true);
|
|
283
|
+
expect(assistantBubble.classList.contains("persona-border")).toBe(false);
|
|
284
|
+
expect(assistantBubble.classList.contains("persona-shadow-sm")).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("removes role background chrome from flat messages", () => {
|
|
288
|
+
const userMessage = createStandardBubble(
|
|
289
|
+
makeMessage({ id: "flat-user", role: "user", content: "Question" }),
|
|
290
|
+
({ text }) => text,
|
|
291
|
+
{ layout: "flat" }
|
|
292
|
+
);
|
|
293
|
+
const assistantMessage = createStandardBubble(
|
|
294
|
+
makeMessage({
|
|
295
|
+
id: "flat-assistant",
|
|
296
|
+
role: "assistant",
|
|
297
|
+
content: "Answer",
|
|
298
|
+
}),
|
|
299
|
+
({ text }) => text,
|
|
300
|
+
{ layout: "flat" }
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(userMessage.style.backgroundColor).toBe("transparent");
|
|
304
|
+
expect(assistantMessage.style.backgroundColor).toBe("transparent");
|
|
305
|
+
expect(userMessage.classList.contains("persona-px-3")).toBe(false);
|
|
306
|
+
expect(assistantMessage.classList.contains("persona-px-3")).toBe(false);
|
|
307
|
+
});
|
|
308
|
+
|
|
260
309
|
it("skips rendering blocked image previews while keeping safe ones", () => {
|
|
261
310
|
const bubble = createStandardBubble(
|
|
262
311
|
makeMessage({
|
|
@@ -653,7 +653,7 @@ const getBubbleClasses = (
|
|
|
653
653
|
role: "user" | "assistant" | "system",
|
|
654
654
|
layout: AgentWidgetMessageLayoutConfig["layout"] = "bubble"
|
|
655
655
|
): string[] => {
|
|
656
|
-
const baseClasses = ["persona-message-bubble"
|
|
656
|
+
const baseClasses = ["persona-message-bubble"];
|
|
657
657
|
|
|
658
658
|
switch (layout) {
|
|
659
659
|
case "flat":
|
|
@@ -877,7 +877,10 @@ export const createStandardBubble = (
|
|
|
877
877
|
bubble.setAttribute("data-persona-theme-zone", message.role === "user" ? "user-message" : "assistant-message");
|
|
878
878
|
|
|
879
879
|
// Apply component-level color overrides via CSS variables
|
|
880
|
-
if (
|
|
880
|
+
if (layout === "flat") {
|
|
881
|
+
bubble.style.backgroundColor = "transparent";
|
|
882
|
+
bubble.style.color = "var(--persona-primary, var(--persona-text))";
|
|
883
|
+
} else if (message.role === "user") {
|
|
881
884
|
bubble.style.backgroundColor = 'var(--persona-message-user-bg, var(--persona-accent))';
|
|
882
885
|
bubble.style.color = 'var(--persona-message-user-text, white)';
|
|
883
886
|
} else if (message.role === "assistant") {
|
|
@@ -1171,7 +1174,9 @@ export const createStandardBubble = (
|
|
|
1171
1174
|
// Create wrapper with avatar
|
|
1172
1175
|
const wrapper = createElement(
|
|
1173
1176
|
"div",
|
|
1174
|
-
`persona-flex persona-gap-2 ${
|
|
1177
|
+
`persona-message-with-avatar persona-flex persona-gap-2 ${
|
|
1178
|
+
message.role === "user" ? "persona-flex-row-reverse" : ""
|
|
1179
|
+
}`
|
|
1175
1180
|
);
|
|
1176
1181
|
|
|
1177
1182
|
const avatar = createAvatar(avatarConfig!, message.role);
|
|
@@ -1182,10 +1187,6 @@ export const createStandardBubble = (
|
|
|
1182
1187
|
wrapper.append(avatar, bubble);
|
|
1183
1188
|
}
|
|
1184
1189
|
|
|
1185
|
-
// Adjust bubble max-width when avatar is present
|
|
1186
|
-
bubble.classList.remove("persona-max-w-[85%]");
|
|
1187
|
-
bubble.classList.add("persona-max-w-[calc(85%-2.5rem)]");
|
|
1188
|
-
|
|
1189
1190
|
return wrapper;
|
|
1190
1191
|
};
|
|
1191
1192
|
|
|
@@ -72,7 +72,6 @@ export const createReasoningBubble = (message: AgentWidgetMessage, config?: Agen
|
|
|
72
72
|
"persona-message-bubble",
|
|
73
73
|
"persona-reasoning-bubble",
|
|
74
74
|
"persona-w-full",
|
|
75
|
-
"persona-max-w-[85%]",
|
|
76
75
|
"persona-rounded-2xl",
|
|
77
76
|
"persona-bg-persona-surface",
|
|
78
77
|
"persona-border",
|
|
@@ -352,4 +351,3 @@ export const createReasoningBubble = (message: AgentWidgetMessage, config?: Agen
|
|
|
352
351
|
};
|
|
353
352
|
|
|
354
353
|
|
|
355
|
-
|
|
@@ -146,7 +146,6 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
146
146
|
"persona-message-bubble",
|
|
147
147
|
"persona-tool-bubble",
|
|
148
148
|
"persona-w-full",
|
|
149
|
-
"persona-max-w-[85%]",
|
|
150
149
|
"persona-rounded-2xl",
|
|
151
150
|
"persona-bg-persona-surface",
|
|
152
151
|
"persona-border",
|
|
@@ -544,4 +543,3 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
|
|
|
544
543
|
};
|
|
545
544
|
|
|
546
545
|
|
|
547
|
-
|
|
@@ -44,6 +44,7 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
44
44
|
input: number;
|
|
45
45
|
output: number;
|
|
46
46
|
};
|
|
47
|
+
totalTokensUsed?: number;
|
|
47
48
|
type: "execution_complete";
|
|
48
49
|
}) | ({
|
|
49
50
|
blockReason?: string;
|
|
@@ -303,6 +304,7 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
303
304
|
stepId?: string;
|
|
304
305
|
success: boolean;
|
|
305
306
|
toolCallId: string;
|
|
307
|
+
toolCost?: number;
|
|
306
308
|
toolName?: string;
|
|
307
309
|
type: "tool_complete";
|
|
308
310
|
} | {
|
package/src/index-core.ts
CHANGED
package/src/styles/widget.css
CHANGED
|
@@ -163,6 +163,8 @@
|
|
|
163
163
|
--persona-md-table-header-weight: 600;
|
|
164
164
|
--persona-md-table-cell-padding: 0.5rem 0.75rem;
|
|
165
165
|
--persona-md-table-border-radius: 0.375rem;
|
|
166
|
+
/* Width of the horizontal-scroll edge fade on wide tables; set to 0 to disable. */
|
|
167
|
+
--persona-md-table-scroll-fade: 24px;
|
|
166
168
|
|
|
167
169
|
/* Markdown Horizontal Rule Variables */
|
|
168
170
|
--persona-md-hr-color: var(--persona-divider, #e5e7eb);
|
|
@@ -643,6 +645,47 @@
|
|
|
643
645
|
max-width: 85%;
|
|
644
646
|
}
|
|
645
647
|
|
|
648
|
+
/* Message row geometry is role-configurable. The row always spans the
|
|
649
|
+
transcript; its direct content track either shrink-wraps or fills that row.
|
|
650
|
+
`--persona-message-row-max-width` is resolved per role in ui.ts. */
|
|
651
|
+
.persona-message-row {
|
|
652
|
+
width: 100%;
|
|
653
|
+
min-width: 0;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.persona-message-row > * {
|
|
657
|
+
min-width: 0;
|
|
658
|
+
max-width: var(--persona-message-row-max-width, 85%);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.persona-message-row[data-message-width="content"] > * {
|
|
662
|
+
flex: 0 1 auto;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.persona-message-row[data-message-width="full"] > * {
|
|
666
|
+
flex: 1 1 auto;
|
|
667
|
+
width: 100%;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/* Avatars live inside the role-sized content track. The bubble flexes into
|
|
671
|
+
the space left after the fixed avatar + gap instead of making a full row
|
|
672
|
+
overflow by the avatar width. */
|
|
673
|
+
.persona-message-with-avatar {
|
|
674
|
+
min-width: 0;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.persona-message-with-avatar > .persona-message-bubble {
|
|
678
|
+
min-width: 0;
|
|
679
|
+
max-width: 100%;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.persona-message-row[data-message-width="full"]
|
|
683
|
+
> .persona-message-with-avatar
|
|
684
|
+
> .persona-message-bubble {
|
|
685
|
+
flex: 1 1 auto;
|
|
686
|
+
width: auto;
|
|
687
|
+
}
|
|
688
|
+
|
|
646
689
|
.persona-transition {
|
|
647
690
|
transition: transform 160ms ease, background-color 160ms ease,
|
|
648
691
|
box-shadow 160ms ease, opacity 160ms ease;
|
|
@@ -952,7 +995,8 @@
|
|
|
952
995
|
cursor: pointer;
|
|
953
996
|
}
|
|
954
997
|
|
|
955
|
-
/*
|
|
998
|
+
/* Shared icon-control tooltip. It is portaled to the document body (or the
|
|
999
|
+
widget ShadowRoot) and positioned in JS so panel overflow never clips it. */
|
|
956
1000
|
.persona-send-button-wrapper {
|
|
957
1001
|
position: relative;
|
|
958
1002
|
display: inline-flex;
|
|
@@ -960,47 +1004,46 @@
|
|
|
960
1004
|
justify-content: center;
|
|
961
1005
|
}
|
|
962
1006
|
|
|
963
|
-
.persona-
|
|
964
|
-
position:
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
transform: translateX(-50%);
|
|
1007
|
+
.persona-control-tooltip {
|
|
1008
|
+
position: fixed;
|
|
1009
|
+
box-sizing: border-box;
|
|
1010
|
+
max-width: min(320px, calc(100vw - 16px));
|
|
968
1011
|
background-color: var(--persona-tooltip-background, #111827);
|
|
969
1012
|
color: var(--persona-tooltip-foreground, #ffffff);
|
|
970
1013
|
padding: 6px 12px;
|
|
971
1014
|
border-radius: var(--persona-radius-sm, 0.125rem);
|
|
972
1015
|
font-size: 12px;
|
|
973
|
-
|
|
974
|
-
|
|
1016
|
+
line-height: 1.35;
|
|
1017
|
+
white-space: normal;
|
|
1018
|
+
overflow-wrap: anywhere;
|
|
1019
|
+
box-shadow: var(--persona-tooltip-shadow, 0 4px 12px rgba(15, 23, 42, 0.18));
|
|
1020
|
+
opacity: 1;
|
|
975
1021
|
pointer-events: none;
|
|
976
|
-
transition: opacity
|
|
977
|
-
z-index: 1000;
|
|
1022
|
+
transition: opacity 120ms ease;
|
|
978
1023
|
}
|
|
979
1024
|
|
|
980
|
-
.persona-
|
|
981
|
-
|
|
1025
|
+
.persona-control-tooltip[data-state="measuring"] {
|
|
1026
|
+
visibility: hidden;
|
|
1027
|
+
opacity: 0;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
.persona-control-tooltip__arrow {
|
|
982
1031
|
position: absolute;
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1032
|
+
left: var(--persona-tooltip-arrow-x, 50%);
|
|
1033
|
+
width: 8px;
|
|
1034
|
+
height: 8px;
|
|
1035
|
+
background: var(--persona-tooltip-background, #111827);
|
|
1036
|
+
transform: translateX(-50%) rotate(45deg);
|
|
988
1037
|
}
|
|
989
1038
|
|
|
990
|
-
.persona-
|
|
991
|
-
|
|
992
|
-
opacity: 1;
|
|
1039
|
+
.persona-control-tooltip[data-placement="top"] .persona-control-tooltip__arrow {
|
|
1040
|
+
top: calc(100% - 4px);
|
|
993
1041
|
}
|
|
994
1042
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
.persona-send-button-wrapper:hover .persona-send-button-tooltip,
|
|
998
|
-
.persona-send-button-wrapper:focus-within .persona-send-button-tooltip {
|
|
999
|
-
opacity: 0;
|
|
1000
|
-
}
|
|
1043
|
+
.persona-control-tooltip[data-placement="bottom"] .persona-control-tooltip__arrow {
|
|
1044
|
+
bottom: calc(100% - 4px);
|
|
1001
1045
|
}
|
|
1002
1046
|
|
|
1003
|
-
/* Clear chat button tooltip */
|
|
1004
1047
|
.persona-clear-chat-button-wrapper {
|
|
1005
1048
|
position: relative;
|
|
1006
1049
|
display: inline-flex;
|
|
@@ -1008,30 +1051,9 @@
|
|
|
1008
1051
|
justify-content: center;
|
|
1009
1052
|
}
|
|
1010
1053
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
padding: 6px 12px;
|
|
1015
|
-
border-radius: var(--persona-radius-sm, 0.125rem);
|
|
1016
|
-
font-size: 12px;
|
|
1017
|
-
white-space: nowrap;
|
|
1018
|
-
pointer-events: none;
|
|
1019
|
-
z-index: 10000;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
.persona-clear-chat-tooltip-arrow {
|
|
1023
|
-
content: "";
|
|
1024
|
-
position: absolute;
|
|
1025
|
-
top: 100%;
|
|
1026
|
-
left: 50%;
|
|
1027
|
-
transform: translateX(-50%);
|
|
1028
|
-
border: 4px solid transparent;
|
|
1029
|
-
border-top-color: var(--persona-tooltip-background, #111827);
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
@media (hover: none), (max-width: 500px) {
|
|
1033
|
-
.persona-clear-chat-tooltip {
|
|
1034
|
-
display: none !important;
|
|
1054
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1055
|
+
.persona-control-tooltip {
|
|
1056
|
+
transition: none;
|
|
1035
1057
|
}
|
|
1036
1058
|
}
|
|
1037
1059
|
|
|
@@ -1501,12 +1523,45 @@
|
|
|
1501
1523
|
flex-grow: 1;
|
|
1502
1524
|
}
|
|
1503
1525
|
|
|
1526
|
+
/* Wide tables scroll inside this wrapper instead of widening the chat column.
|
|
1527
|
+
The table keeps width:100% so it fills when it fits, but auto-grows past the
|
|
1528
|
+
wrapper when its content is wider, and the wrapper scrolls. Injected by
|
|
1529
|
+
wrapScrollableTables() before each morph. */
|
|
1530
|
+
.persona-table-scroll {
|
|
1531
|
+
display: block;
|
|
1532
|
+
max-width: 100%;
|
|
1533
|
+
overflow-x: auto;
|
|
1534
|
+
margin: 0.5rem 0;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/* Edge fades (set by table-scroll-fade.ts) cue that the table scrolls; each
|
|
1538
|
+
side shows only when there is more content that way. The fade is applied
|
|
1539
|
+
instantly, not transitioned: each morph strips and re-applies the fade state,
|
|
1540
|
+
so a transition would animate it from 0 on every streaming chunk (flicker) and
|
|
1541
|
+
once more when the finished paint re-enables it (a single flash). Instant also
|
|
1542
|
+
tracks the scroll position exactly instead of lagging behind the finger. */
|
|
1543
|
+
.persona-table-scroll[data-persona-scroll-x] {
|
|
1544
|
+
-webkit-mask-image: linear-gradient(
|
|
1545
|
+
to right,
|
|
1546
|
+
transparent 0,
|
|
1547
|
+
#000 var(--persona-fade-l, 0px),
|
|
1548
|
+
#000 calc(100% - var(--persona-fade-r, 0px)),
|
|
1549
|
+
transparent 100%
|
|
1550
|
+
);
|
|
1551
|
+
mask-image: linear-gradient(
|
|
1552
|
+
to right,
|
|
1553
|
+
transparent 0,
|
|
1554
|
+
#000 var(--persona-fade-l, 0px),
|
|
1555
|
+
#000 calc(100% - var(--persona-fade-r, 0px)),
|
|
1556
|
+
transparent 100%
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1504
1560
|
.persona-message-bubble table {
|
|
1505
1561
|
width: 100%;
|
|
1506
1562
|
border-collapse: collapse;
|
|
1507
|
-
margin: 0
|
|
1563
|
+
margin: 0;
|
|
1508
1564
|
font-size: 0.875rem;
|
|
1509
|
-
overflow: hidden;
|
|
1510
1565
|
border-radius: var(--persona-md-table-border-radius);
|
|
1511
1566
|
border: 1px solid var(--persona-md-table-border-color);
|
|
1512
1567
|
}
|
|
@@ -1536,13 +1591,18 @@
|
|
|
1536
1591
|
background-color: rgba(0, 0, 0, 0.02);
|
|
1537
1592
|
}
|
|
1538
1593
|
|
|
1539
|
-
/* While a message is still streaming, lock
|
|
1540
|
-
append without
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1594
|
+
/* While a message is still streaming, lock column widths to the header row
|
|
1595
|
+
(table-layout: fixed) so incoming rows append without per-chunk reflow. width
|
|
1596
|
+
auto sizes columns from the header and lets cells wrap, so a table that fits
|
|
1597
|
+
once wrapped fills the column (min-width: 100%) with no scroll, while one too
|
|
1598
|
+
wide even wrapped scrolls: it only shows the scroll fade when it genuinely
|
|
1599
|
+
needs it, matching the finished table rather than forcing every cell onto one
|
|
1600
|
+
line. The class is removed on the final render, settling columns to
|
|
1601
|
+
content-fit widths. overflow-wrap keeps long words from spilling the column. */
|
|
1544
1602
|
.persona-content-streaming table {
|
|
1545
1603
|
table-layout: fixed;
|
|
1604
|
+
width: auto;
|
|
1605
|
+
min-width: 100%;
|
|
1546
1606
|
}
|
|
1547
1607
|
|
|
1548
1608
|
.persona-content-streaming th,
|
|
@@ -4,6 +4,39 @@ import { COMPONENTS_SECTIONS, CONFIGURE_SECTIONS, INTERFACE_ROLES_SECTION, STYLE
|
|
|
4
4
|
import { ALL_ROLES } from "./role-mappings";
|
|
5
5
|
|
|
6
6
|
describe("theme editor scroll-to-bottom controls", () => {
|
|
7
|
+
it("exposes clear style semantics and independent role width controls", () => {
|
|
8
|
+
const section = CONFIGURE_SECTIONS.find((entry) => entry.id === "messages-layout");
|
|
9
|
+
const fieldsByPath = new Map(
|
|
10
|
+
section?.fields.map((field) => [field.path, field]) ?? []
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
expect(fieldsByPath.get("layout.messages.layout")?.options).toEqual([
|
|
14
|
+
{ value: "bubble", label: "Bubble — bubbles for both" },
|
|
15
|
+
{ value: "minimal", label: "Minimal — user bubble, open assistant" },
|
|
16
|
+
{ value: "flat", label: "Flat — open messages for both" },
|
|
17
|
+
]);
|
|
18
|
+
expect(fieldsByPath.has("layout.messages.user.style")).toBe(false);
|
|
19
|
+
expect(fieldsByPath.has("layout.messages.assistant.style")).toBe(false);
|
|
20
|
+
|
|
21
|
+
expect(fieldsByPath.get("layout.messages.user.width")?.options).toEqual([
|
|
22
|
+
{ value: "content", label: "Content" },
|
|
23
|
+
{ value: "full", label: "Full" },
|
|
24
|
+
]);
|
|
25
|
+
expect(fieldsByPath.get("layout.messages.assistant.width")?.options).toEqual([
|
|
26
|
+
{ value: "content", label: "Content" },
|
|
27
|
+
{ value: "full", label: "Full" },
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const userMaxWidth = fieldsByPath.get("layout.messages.user.maxWidth");
|
|
31
|
+
const assistantMaxWidth = fieldsByPath.get(
|
|
32
|
+
"layout.messages.assistant.maxWidth"
|
|
33
|
+
);
|
|
34
|
+
expect(userMaxWidth?.parseValue?.(" 72ch ")).toBe("72ch");
|
|
35
|
+
expect(userMaxWidth?.parseValue?.(" ")).toBeUndefined();
|
|
36
|
+
expect(assistantMaxWidth?.parseValue?.("80%")).toBe("80%");
|
|
37
|
+
expect(assistantMaxWidth?.parseValue?.("")).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
7
40
|
it("exposes scroll-to-bottom config controls", () => {
|
|
8
41
|
const featureSection = CONFIGURE_SECTIONS.find((section) => section.id === "features");
|
|
9
42
|
|
|
@@ -22,6 +22,11 @@ import {
|
|
|
22
22
|
ROLE_BORDERS,
|
|
23
23
|
} from './role-mappings';
|
|
24
24
|
|
|
25
|
+
const parseOptionalCssValue = (value: unknown): string | undefined => {
|
|
26
|
+
const normalized = String(value ?? '').trim();
|
|
27
|
+
return normalized || undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
25
30
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
26
31
|
// STYLE TAB: brand colors, chat colors, typography, shape, etc.
|
|
27
32
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
@@ -601,7 +606,11 @@ const headerLayoutSectionDef: SectionDef = {
|
|
|
601
606
|
const messagesLayoutSectionDef: SectionDef = {
|
|
602
607
|
id: 'messages-layout', title: 'Messages', collapsed: true,
|
|
603
608
|
fields: [
|
|
604
|
-
{ id: 'layout-messages', label: '
|
|
609
|
+
{ id: 'layout-messages', label: 'Message Style', description: 'Minimal uses a user bubble with an open assistant response', type: 'select', path: 'layout.messages.layout', defaultValue: 'bubble', options: [{ value: 'bubble', label: 'Bubble — bubbles for both' }, { value: 'minimal', label: 'Minimal — user bubble, open assistant' }, { value: 'flat', label: 'Flat — open messages for both' }] },
|
|
610
|
+
{ id: 'layout-message-user-width', label: 'User Width', description: 'Content hugs the message; Full fills the transcript track', type: 'select', path: 'layout.messages.user.width', defaultValue: 'content', options: [{ value: 'content', label: 'Content' }, { value: 'full', label: 'Full' }] },
|
|
611
|
+
{ id: 'layout-message-user-max-width', label: 'User Max Width', description: 'Optional CSS width such as 80%, 42rem, or 72ch; blank uses the width mode default', type: 'text', path: 'layout.messages.user.maxWidth', defaultValue: '', parseValue: parseOptionalCssValue },
|
|
612
|
+
{ id: 'layout-message-assistant-width', label: 'Assistant Width', description: 'Content hugs the message; Full fills the transcript track', type: 'select', path: 'layout.messages.assistant.width', defaultValue: 'content', options: [{ value: 'content', label: 'Content' }, { value: 'full', label: 'Full' }] },
|
|
613
|
+
{ id: 'layout-message-assistant-max-width', label: 'Assistant Max Width', description: 'Optional CSS width such as 80%, 42rem, or 72ch; blank uses the width mode default', type: 'text', path: 'layout.messages.assistant.maxWidth', defaultValue: '', parseValue: parseOptionalCssValue },
|
|
605
614
|
{ id: 'layout-group', label: 'Group Consecutive', type: 'toggle', path: 'layout.messages.groupConsecutive', defaultValue: false },
|
|
606
615
|
{ id: 'layout-avatar-show', label: 'Show Avatars', type: 'toggle', path: 'layout.messages.avatar.show', defaultValue: false },
|
|
607
616
|
{ id: 'layout-avatar-pos', label: 'Avatar Position', type: 'select', path: 'layout.messages.avatar.position', defaultValue: 'left', options: [{ value: 'left', label: 'Left' }, { value: 'right', label: 'Right' }] },
|
package/src/types.ts
CHANGED
|
@@ -3975,6 +3975,27 @@ export type AgentWidgetTimestampConfig = {
|
|
|
3975
3975
|
format?: (date: Date) => string;
|
|
3976
3976
|
};
|
|
3977
3977
|
|
|
3978
|
+
/**
|
|
3979
|
+
* Width configuration for one message role.
|
|
3980
|
+
*
|
|
3981
|
+
* `content` shrink-wraps the rendered message up to `maxWidth`; `full` fills
|
|
3982
|
+
* the available transcript track up to `maxWidth`. The track already accounts
|
|
3983
|
+
* for transcript padding and, when present, the avatar and its gap.
|
|
3984
|
+
*/
|
|
3985
|
+
export type AgentWidgetMessageRoleLayout = {
|
|
3986
|
+
/**
|
|
3987
|
+
* Message width behavior.
|
|
3988
|
+
* - content: size to the rendered content (the backward-compatible default)
|
|
3989
|
+
* - full: fill the available transcript track
|
|
3990
|
+
*/
|
|
3991
|
+
width?: "content" | "full";
|
|
3992
|
+
/**
|
|
3993
|
+
* Optional CSS max-width (for example "80%", "42rem", or "72ch").
|
|
3994
|
+
* Defaults to "85%" for content width and "100%" for full width.
|
|
3995
|
+
*/
|
|
3996
|
+
maxWidth?: string;
|
|
3997
|
+
};
|
|
3998
|
+
|
|
3978
3999
|
/**
|
|
3979
4000
|
* Message layout configuration
|
|
3980
4001
|
* Allows customization of how chat messages are displayed
|
|
@@ -3993,6 +4014,10 @@ export type AgentWidgetMessageLayoutConfig = {
|
|
|
3993
4014
|
timestamp?: AgentWidgetTimestampConfig;
|
|
3994
4015
|
/** Group consecutive messages from the same role */
|
|
3995
4016
|
groupConsecutive?: boolean;
|
|
4017
|
+
/** Width behavior for user-authored message rows */
|
|
4018
|
+
user?: AgentWidgetMessageRoleLayout;
|
|
4019
|
+
/** Width behavior for assistant-authored message rows and assistant UI variants */
|
|
4020
|
+
assistant?: AgentWidgetMessageRoleLayout;
|
|
3996
4021
|
/**
|
|
3997
4022
|
* Custom renderer for user messages
|
|
3998
4023
|
* When provided, replaces the default user message rendering
|
|
@@ -270,7 +270,8 @@ describe("attachment button live config updates", () => {
|
|
|
270
270
|
expect(button().getAttribute("aria-label")).toBe("Add a photo");
|
|
271
271
|
const updatedSvg = button().querySelector("svg")!.outerHTML;
|
|
272
272
|
expect(updatedSvg).not.toBe(initialSvg);
|
|
273
|
-
|
|
273
|
+
button().parentElement!.dispatchEvent(new MouseEvent("mouseenter"));
|
|
274
|
+
const tooltip = document.body.querySelector(".persona-control-tooltip");
|
|
274
275
|
expect(tooltip?.textContent).toBe("Add a photo");
|
|
275
276
|
|
|
276
277
|
controller.destroy();
|