@runtypelabs/persona 3.6.0 → 3.8.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/index.cjs +40 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -4
- package/dist/index.d.ts +73 -4
- package/dist/index.global.js +69 -69
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +40 -40
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +704 -243
- package/dist/theme-editor.d.cts +75 -5
- package/dist/theme-editor.d.ts +75 -5
- package/dist/theme-editor.js +703 -243
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +53 -0
- package/dist/theme-reference.d.ts +53 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +44 -0
- package/package.json +1 -1
- package/src/components/artifact-card.ts +1 -1
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +142 -0
- package/src/components/event-stream-view.ts +67 -28
- package/src/components/header-builder.ts +3 -0
- package/src/components/launcher.ts +7 -2
- package/src/components/panel.ts +3 -1
- package/src/defaults.ts +15 -0
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +2 -1
- package/src/scroll-to-bottom-defaults.test.ts +13 -0
- package/src/styles/widget.css +44 -0
- package/src/theme-editor/index.ts +1 -0
- package/src/theme-editor/role-mappings.ts +12 -0
- package/src/theme-editor/sections.test.ts +43 -0
- package/src/theme-editor/sections.ts +42 -0
- package/src/theme-reference.ts +8 -0
- package/src/types/theme.ts +45 -0
- package/src/types.ts +31 -4
- package/src/ui.overlay-z-index.test.ts +34 -2
- package/src/ui.scroll.test.ts +554 -0
- package/src/ui.ts +264 -90
- package/src/utils/auto-follow.test.ts +110 -0
- package/src/utils/auto-follow.ts +112 -0
- package/src/utils/constants.ts +13 -0
- package/src/utils/dropdown.ts +2 -1
- package/src/utils/overlay-host-stacking.test.ts +61 -0
- package/src/utils/overlay-host-stacking.ts +38 -0
- package/src/utils/scroll-lock.test.ts +64 -0
- package/src/utils/scroll-lock.ts +62 -0
- package/src/utils/theme.test.ts +34 -0
- package/src/utils/tokens.ts +112 -0
package/src/types.ts
CHANGED
|
@@ -552,10 +552,32 @@ export type AgentWidgetArtifactsFeature = {
|
|
|
552
552
|
}) => HTMLElement | null;
|
|
553
553
|
};
|
|
554
554
|
|
|
555
|
+
export type AgentWidgetScrollToBottomFeature = {
|
|
556
|
+
/**
|
|
557
|
+
* When true, Persona shows a scroll-to-bottom affordance when the user breaks
|
|
558
|
+
* away from the latest transcript or event stream content.
|
|
559
|
+
* @default true
|
|
560
|
+
*/
|
|
561
|
+
enabled?: boolean;
|
|
562
|
+
/**
|
|
563
|
+
* Lucide icon name used for the affordance.
|
|
564
|
+
* @default "arrow-down"
|
|
565
|
+
*/
|
|
566
|
+
iconName?: string;
|
|
567
|
+
/**
|
|
568
|
+
* Optional label text shown next to the icon. Set to an empty string for an
|
|
569
|
+
* icon-only affordance.
|
|
570
|
+
* @default ""
|
|
571
|
+
*/
|
|
572
|
+
label?: string;
|
|
573
|
+
};
|
|
574
|
+
|
|
555
575
|
export type AgentWidgetFeatureFlags = {
|
|
556
576
|
showReasoning?: boolean;
|
|
557
577
|
showToolCalls?: boolean;
|
|
558
578
|
showEventStreamToggle?: boolean;
|
|
579
|
+
/** Shared transcript + event stream scroll-to-bottom affordance. */
|
|
580
|
+
scrollToBottom?: AgentWidgetScrollToBottomFeature;
|
|
559
581
|
/** Configuration for the Event Stream inspector view */
|
|
560
582
|
eventStream?: EventStreamConfig;
|
|
561
583
|
/** Optional artifact sidebar (split pane / mobile drawer) */
|
|
@@ -791,11 +813,16 @@ export type AgentWidgetLauncherConfig = {
|
|
|
791
813
|
*/
|
|
792
814
|
mobileBreakpoint?: number;
|
|
793
815
|
/**
|
|
794
|
-
* CSS z-index applied to the widget wrapper
|
|
795
|
-
* (floating panel, mobile fullscreen,
|
|
796
|
-
*
|
|
816
|
+
* CSS z-index applied to the widget wrapper and launcher button in all
|
|
817
|
+
* positioned modes (floating panel, mobile fullscreen, sidebar, docked
|
|
818
|
+
* mobile fullscreen). Increase this value if other elements on the host
|
|
819
|
+
* page appear on top of the widget.
|
|
820
|
+
*
|
|
821
|
+
* In viewport-covering modes (sidebar, mobile fullscreen), the widget
|
|
822
|
+
* also elevates the host element's stacking context and locks
|
|
823
|
+
* document scroll to prevent background scrolling.
|
|
797
824
|
*
|
|
798
|
-
* @default
|
|
825
|
+
* @default 100000
|
|
799
826
|
*/
|
|
800
827
|
zIndex?: number;
|
|
801
828
|
callToActionIconText?: string;
|
|
@@ -39,7 +39,7 @@ describe("createAgentExperience overlay z-index", () => {
|
|
|
39
39
|
const wrapper = mount.firstElementChild as HTMLElement | null;
|
|
40
40
|
|
|
41
41
|
expect(wrapper).not.toBeNull();
|
|
42
|
-
expect(wrapper?.style.zIndex).toBe("
|
|
42
|
+
expect(wrapper?.style.zIndex).toBe("100000");
|
|
43
43
|
|
|
44
44
|
controller.destroy();
|
|
45
45
|
});
|
|
@@ -55,7 +55,39 @@ describe("createAgentExperience overlay z-index", () => {
|
|
|
55
55
|
const wrapper = mount.firstElementChild as HTMLElement | null;
|
|
56
56
|
|
|
57
57
|
expect(wrapper).not.toBeNull();
|
|
58
|
-
expect(wrapper?.style.zIndex).toBe("
|
|
58
|
+
expect(wrapper?.style.zIndex).toBe("100000");
|
|
59
|
+
|
|
60
|
+
controller.destroy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("defaults floating panel wrapper to the overlay z-index", () => {
|
|
64
|
+
setInnerWidth(1024);
|
|
65
|
+
|
|
66
|
+
const mount = createMount();
|
|
67
|
+
const controller = createAgentExperience(mount, {
|
|
68
|
+
apiUrl: "https://api.example.com/chat",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const wrapper = mount.firstElementChild as HTMLElement | null;
|
|
72
|
+
|
|
73
|
+
expect(wrapper).not.toBeNull();
|
|
74
|
+
expect(wrapper?.style.zIndex).toBe("100000");
|
|
75
|
+
|
|
76
|
+
controller.destroy();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("respects a custom zIndex", () => {
|
|
80
|
+
const mount = createMount();
|
|
81
|
+
const controller = createAgentExperience(mount, {
|
|
82
|
+
apiUrl: "https://api.example.com/chat",
|
|
83
|
+
launcher: {
|
|
84
|
+
sidebarMode: true,
|
|
85
|
+
zIndex: 42,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const wrapper = mount.firstElementChild as HTMLElement | null;
|
|
90
|
+
expect(wrapper?.style.zIndex).toBe("42");
|
|
59
91
|
|
|
60
92
|
controller.destroy();
|
|
61
93
|
});
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createAgentExperience } from "./ui";
|
|
6
|
+
|
|
7
|
+
type RafCallback = (time: number) => void;
|
|
8
|
+
|
|
9
|
+
const STREAM_MESSAGE_ID = "ast-stream";
|
|
10
|
+
const STREAM_CREATED_AT = "2026-03-29T00:00:00.000Z";
|
|
11
|
+
|
|
12
|
+
const createMount = () => {
|
|
13
|
+
const mount = document.createElement("div");
|
|
14
|
+
document.body.appendChild(mount);
|
|
15
|
+
return mount;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const getScrollToBottomButton = (mount: HTMLElement) =>
|
|
19
|
+
mount.querySelector<HTMLElement>("[data-persona-scroll-to-bottom]");
|
|
20
|
+
|
|
21
|
+
const installRafMock = () => {
|
|
22
|
+
let nextId = 1;
|
|
23
|
+
let now = performance.now();
|
|
24
|
+
const callbacks = new Map<number, RafCallback>();
|
|
25
|
+
|
|
26
|
+
vi.stubGlobal("requestAnimationFrame", (callback: RafCallback) => {
|
|
27
|
+
const id = nextId++;
|
|
28
|
+
callbacks.set(id, callback);
|
|
29
|
+
return id;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
vi.stubGlobal("cancelAnimationFrame", (id: number) => {
|
|
33
|
+
callbacks.delete(id);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
flush(maxFrames = 80) {
|
|
38
|
+
let frames = 0;
|
|
39
|
+
while (callbacks.size > 0 && frames < maxFrames) {
|
|
40
|
+
const pending = [...callbacks.entries()];
|
|
41
|
+
callbacks.clear();
|
|
42
|
+
frames += 1;
|
|
43
|
+
now += 16;
|
|
44
|
+
pending.forEach(([, callback]) => callback(now));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (callbacks.size > 0) {
|
|
48
|
+
throw new Error("requestAnimationFrame queue did not settle");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const installScrollMetrics = (
|
|
55
|
+
element: HTMLElement,
|
|
56
|
+
initial: { scrollHeight: number; clientHeight: number }
|
|
57
|
+
) => {
|
|
58
|
+
let scrollTop = 0;
|
|
59
|
+
let scrollHeight = initial.scrollHeight;
|
|
60
|
+
const clientHeight = initial.clientHeight;
|
|
61
|
+
|
|
62
|
+
Object.defineProperties(element, {
|
|
63
|
+
scrollTop: {
|
|
64
|
+
configurable: true,
|
|
65
|
+
get: () => scrollTop,
|
|
66
|
+
set: (value: number) => {
|
|
67
|
+
const maxScrollTop = Math.max(0, scrollHeight - clientHeight);
|
|
68
|
+
scrollTop = Math.max(0, Math.min(value, maxScrollTop));
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
scrollHeight: {
|
|
72
|
+
configurable: true,
|
|
73
|
+
get: () => scrollHeight
|
|
74
|
+
},
|
|
75
|
+
clientHeight: {
|
|
76
|
+
configurable: true,
|
|
77
|
+
get: () => clientHeight
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
getScrollTop: () => scrollTop,
|
|
83
|
+
getBottomScrollTop: () => Math.max(0, scrollHeight - clientHeight),
|
|
84
|
+
setScrollTop: (value: number) => {
|
|
85
|
+
element.scrollTop = value;
|
|
86
|
+
},
|
|
87
|
+
setScrollHeight: (value: number) => {
|
|
88
|
+
scrollHeight = value;
|
|
89
|
+
if (scrollTop > scrollHeight - clientHeight) {
|
|
90
|
+
scrollTop = Math.max(0, scrollHeight - clientHeight);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const emitStreamingStatus = (controller: ReturnType<typeof createAgentExperience>) => {
|
|
97
|
+
controller.injectTestMessage({ type: "status", status: "connecting" });
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const emitStreamingMessage = (
|
|
101
|
+
controller: ReturnType<typeof createAgentExperience>,
|
|
102
|
+
content: string
|
|
103
|
+
) => {
|
|
104
|
+
controller.injectTestMessage({
|
|
105
|
+
type: "message",
|
|
106
|
+
message: {
|
|
107
|
+
id: STREAM_MESSAGE_ID,
|
|
108
|
+
role: "assistant",
|
|
109
|
+
content,
|
|
110
|
+
createdAt: STREAM_CREATED_AT,
|
|
111
|
+
streaming: true
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const emitReasoningMessage = (
|
|
117
|
+
controller: ReturnType<typeof createAgentExperience>,
|
|
118
|
+
chunks: string[]
|
|
119
|
+
) => {
|
|
120
|
+
controller.injectTestMessage({
|
|
121
|
+
type: "message",
|
|
122
|
+
message: {
|
|
123
|
+
id: STREAM_MESSAGE_ID,
|
|
124
|
+
role: "assistant",
|
|
125
|
+
content: "",
|
|
126
|
+
createdAt: STREAM_CREATED_AT,
|
|
127
|
+
streaming: true,
|
|
128
|
+
variant: "reasoning",
|
|
129
|
+
reasoning: {
|
|
130
|
+
id: "reason-1",
|
|
131
|
+
status: "streaming",
|
|
132
|
+
chunks
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const createCustomComposer = () => {
|
|
139
|
+
const footer = document.createElement("div");
|
|
140
|
+
footer.className = "persona-widget-footer";
|
|
141
|
+
|
|
142
|
+
const form = document.createElement("form");
|
|
143
|
+
form.setAttribute("data-persona-composer-form", "");
|
|
144
|
+
|
|
145
|
+
const textarea = document.createElement("textarea");
|
|
146
|
+
textarea.setAttribute("data-persona-composer-input", "");
|
|
147
|
+
|
|
148
|
+
const status = document.createElement("div");
|
|
149
|
+
status.setAttribute("data-persona-composer-status", "");
|
|
150
|
+
|
|
151
|
+
form.appendChild(textarea);
|
|
152
|
+
footer.append(form, status);
|
|
153
|
+
return footer;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
describe("createAgentExperience streaming scroll", () => {
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
installRafMock();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
afterEach(() => {
|
|
162
|
+
document.body.innerHTML = "";
|
|
163
|
+
vi.restoreAllMocks();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("stops auto-follow after a small upward scroll during streaming", () => {
|
|
167
|
+
const raf = installRafMock();
|
|
168
|
+
const mount = createMount();
|
|
169
|
+
const controller = createAgentExperience(mount, {
|
|
170
|
+
apiUrl: "https://api.example.com/chat",
|
|
171
|
+
launcher: { enabled: false }
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
175
|
+
expect(scrollContainer).not.toBeNull();
|
|
176
|
+
|
|
177
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
178
|
+
scrollHeight: 1000,
|
|
179
|
+
clientHeight: 400
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
emitStreamingStatus(controller);
|
|
183
|
+
emitStreamingMessage(controller, "First chunk");
|
|
184
|
+
raf.flush();
|
|
185
|
+
|
|
186
|
+
expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
|
|
187
|
+
|
|
188
|
+
metrics.setScrollTop(metrics.getBottomScrollTop() - 3);
|
|
189
|
+
scrollContainer!.dispatchEvent(new Event("scroll"));
|
|
190
|
+
|
|
191
|
+
metrics.setScrollHeight(1040);
|
|
192
|
+
emitStreamingMessage(controller, "Second chunk");
|
|
193
|
+
raf.flush();
|
|
194
|
+
|
|
195
|
+
expect(metrics.getScrollTop()).toBe(597);
|
|
196
|
+
|
|
197
|
+
controller.destroy();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("pauses auto-follow on upward wheel intent before the next streamed update", () => {
|
|
201
|
+
const raf = installRafMock();
|
|
202
|
+
const mount = createMount();
|
|
203
|
+
const controller = createAgentExperience(mount, {
|
|
204
|
+
apiUrl: "https://api.example.com/chat",
|
|
205
|
+
launcher: { enabled: false }
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
209
|
+
expect(scrollContainer).not.toBeNull();
|
|
210
|
+
|
|
211
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
212
|
+
scrollHeight: 1000,
|
|
213
|
+
clientHeight: 400
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
emitStreamingStatus(controller);
|
|
217
|
+
emitStreamingMessage(controller, "First chunk");
|
|
218
|
+
raf.flush();
|
|
219
|
+
|
|
220
|
+
scrollContainer!.dispatchEvent(new WheelEvent("wheel", { deltaY: -24 }));
|
|
221
|
+
metrics.setScrollTop(580);
|
|
222
|
+
metrics.setScrollHeight(1060);
|
|
223
|
+
|
|
224
|
+
emitStreamingMessage(controller, "Second chunk");
|
|
225
|
+
raf.flush();
|
|
226
|
+
|
|
227
|
+
expect(metrics.getScrollTop()).toBe(580);
|
|
228
|
+
|
|
229
|
+
controller.destroy();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("resumes auto-follow when the user scrolls back to the bottom", () => {
|
|
233
|
+
const raf = installRafMock();
|
|
234
|
+
const mount = createMount();
|
|
235
|
+
const controller = createAgentExperience(mount, {
|
|
236
|
+
apiUrl: "https://api.example.com/chat",
|
|
237
|
+
launcher: { enabled: false }
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
241
|
+
expect(scrollContainer).not.toBeNull();
|
|
242
|
+
|
|
243
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
244
|
+
scrollHeight: 1000,
|
|
245
|
+
clientHeight: 400
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
emitStreamingStatus(controller);
|
|
249
|
+
emitStreamingMessage(controller, "First chunk");
|
|
250
|
+
raf.flush();
|
|
251
|
+
|
|
252
|
+
scrollContainer!.dispatchEvent(new WheelEvent("wheel", { deltaY: -24 }));
|
|
253
|
+
metrics.setScrollTop(560);
|
|
254
|
+
metrics.setScrollHeight(1060);
|
|
255
|
+
emitStreamingMessage(controller, "Second chunk");
|
|
256
|
+
raf.flush();
|
|
257
|
+
|
|
258
|
+
expect(metrics.getScrollTop()).toBe(560);
|
|
259
|
+
|
|
260
|
+
metrics.setScrollTop(metrics.getBottomScrollTop() - 2);
|
|
261
|
+
scrollContainer!.dispatchEvent(new Event("scroll"));
|
|
262
|
+
|
|
263
|
+
metrics.setScrollHeight(1100);
|
|
264
|
+
emitStreamingMessage(controller, "Third chunk");
|
|
265
|
+
raf.flush();
|
|
266
|
+
|
|
267
|
+
expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
|
|
268
|
+
|
|
269
|
+
controller.destroy();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("does not immediately resume after an upward scroll while still near the bottom", () => {
|
|
273
|
+
const raf = installRafMock();
|
|
274
|
+
const mount = createMount();
|
|
275
|
+
const controller = createAgentExperience(mount, {
|
|
276
|
+
apiUrl: "https://api.example.com/chat",
|
|
277
|
+
launcher: { enabled: false }
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
281
|
+
expect(scrollContainer).not.toBeNull();
|
|
282
|
+
|
|
283
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
284
|
+
scrollHeight: 1000,
|
|
285
|
+
clientHeight: 400
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
emitStreamingStatus(controller);
|
|
289
|
+
emitStreamingMessage(controller, "First chunk");
|
|
290
|
+
raf.flush();
|
|
291
|
+
|
|
292
|
+
scrollContainer!.dispatchEvent(new WheelEvent("wheel", { deltaY: -24 }));
|
|
293
|
+
metrics.setScrollTop(metrics.getBottomScrollTop() - 3);
|
|
294
|
+
scrollContainer!.dispatchEvent(new Event("scroll"));
|
|
295
|
+
|
|
296
|
+
metrics.setScrollHeight(1040);
|
|
297
|
+
emitStreamingMessage(controller, "Second chunk");
|
|
298
|
+
raf.flush();
|
|
299
|
+
|
|
300
|
+
expect(metrics.getScrollTop()).toBe(597);
|
|
301
|
+
expect(getScrollToBottomButton(mount)?.style.display).not.toBe("none");
|
|
302
|
+
|
|
303
|
+
controller.destroy();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("keeps following the stream when the user does not scroll", () => {
|
|
307
|
+
const raf = installRafMock();
|
|
308
|
+
const mount = createMount();
|
|
309
|
+
const controller = createAgentExperience(mount, {
|
|
310
|
+
apiUrl: "https://api.example.com/chat",
|
|
311
|
+
launcher: { enabled: false }
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
315
|
+
expect(scrollContainer).not.toBeNull();
|
|
316
|
+
|
|
317
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
318
|
+
scrollHeight: 900,
|
|
319
|
+
clientHeight: 400
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
emitStreamingStatus(controller);
|
|
323
|
+
emitStreamingMessage(controller, "Chunk one");
|
|
324
|
+
raf.flush();
|
|
325
|
+
|
|
326
|
+
expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
|
|
327
|
+
|
|
328
|
+
metrics.setScrollHeight(980);
|
|
329
|
+
emitStreamingMessage(controller, "Chunk two");
|
|
330
|
+
raf.flush();
|
|
331
|
+
|
|
332
|
+
expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
|
|
333
|
+
|
|
334
|
+
controller.destroy();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("lets the user break away during reasoning streaming", () => {
|
|
338
|
+
const raf = installRafMock();
|
|
339
|
+
const mount = createMount();
|
|
340
|
+
const controller = createAgentExperience(mount, {
|
|
341
|
+
apiUrl: "https://api.example.com/chat",
|
|
342
|
+
launcher: { enabled: false }
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
346
|
+
expect(scrollContainer).not.toBeNull();
|
|
347
|
+
|
|
348
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
349
|
+
scrollHeight: 960,
|
|
350
|
+
clientHeight: 400
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
emitStreamingStatus(controller);
|
|
354
|
+
emitReasoningMessage(controller, ["Thinking"]);
|
|
355
|
+
raf.flush();
|
|
356
|
+
|
|
357
|
+
expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
|
|
358
|
+
|
|
359
|
+
scrollContainer!.dispatchEvent(new WheelEvent("wheel", { deltaY: -18 }));
|
|
360
|
+
metrics.setScrollTop(metrics.getBottomScrollTop() - 4);
|
|
361
|
+
metrics.setScrollHeight(1010);
|
|
362
|
+
|
|
363
|
+
emitReasoningMessage(controller, ["Thinking", " harder"]);
|
|
364
|
+
raf.flush();
|
|
365
|
+
|
|
366
|
+
expect(metrics.getScrollTop()).toBe(556);
|
|
367
|
+
|
|
368
|
+
controller.destroy();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("uses icon-only arrow-down defaults for the transcript affordance", () => {
|
|
372
|
+
const raf = installRafMock();
|
|
373
|
+
const mount = createMount();
|
|
374
|
+
const controller = createAgentExperience(mount, {
|
|
375
|
+
apiUrl: "https://api.example.com/chat",
|
|
376
|
+
launcher: { enabled: false }
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
380
|
+
expect(scrollContainer).not.toBeNull();
|
|
381
|
+
|
|
382
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
383
|
+
scrollHeight: 1000,
|
|
384
|
+
clientHeight: 400
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
emitStreamingStatus(controller);
|
|
388
|
+
emitStreamingMessage(controller, "First chunk");
|
|
389
|
+
raf.flush();
|
|
390
|
+
|
|
391
|
+
scrollContainer!.dispatchEvent(new WheelEvent("wheel", { deltaY: -18 }));
|
|
392
|
+
metrics.setScrollTop(560);
|
|
393
|
+
metrics.setScrollHeight(1060);
|
|
394
|
+
emitStreamingMessage(controller, "Second chunk");
|
|
395
|
+
raf.flush();
|
|
396
|
+
|
|
397
|
+
const button = getScrollToBottomButton(mount);
|
|
398
|
+
expect(button).not.toBeNull();
|
|
399
|
+
expect(button?.textContent?.trim()).toBe("");
|
|
400
|
+
expect(button?.querySelector("svg")).not.toBeNull();
|
|
401
|
+
|
|
402
|
+
controller.destroy();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("anchors the transcript affordance outside the scroll container", () => {
|
|
406
|
+
const raf = installRafMock();
|
|
407
|
+
const mount = createMount();
|
|
408
|
+
const controller = createAgentExperience(mount, {
|
|
409
|
+
apiUrl: "https://api.example.com/chat",
|
|
410
|
+
launcher: { enabled: false }
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
414
|
+
expect(scrollContainer).not.toBeNull();
|
|
415
|
+
|
|
416
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
417
|
+
scrollHeight: 1000,
|
|
418
|
+
clientHeight: 400
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
emitStreamingStatus(controller);
|
|
422
|
+
emitStreamingMessage(controller, "First chunk");
|
|
423
|
+
raf.flush();
|
|
424
|
+
|
|
425
|
+
scrollContainer!.dispatchEvent(new WheelEvent("wheel", { deltaY: -18 }));
|
|
426
|
+
metrics.setScrollTop(560);
|
|
427
|
+
metrics.setScrollHeight(1060);
|
|
428
|
+
emitStreamingMessage(controller, "Second chunk");
|
|
429
|
+
raf.flush();
|
|
430
|
+
|
|
431
|
+
const button = getScrollToBottomButton(mount);
|
|
432
|
+
expect(button).not.toBeNull();
|
|
433
|
+
expect(button?.parentElement).not.toBe(scrollContainer);
|
|
434
|
+
|
|
435
|
+
controller.destroy();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("keeps the transcript affordance outside the scroll container with a custom composer", () => {
|
|
439
|
+
const raf = installRafMock();
|
|
440
|
+
const mount = createMount();
|
|
441
|
+
const controller = createAgentExperience(mount, {
|
|
442
|
+
apiUrl: "https://api.example.com/chat",
|
|
443
|
+
launcher: { enabled: false },
|
|
444
|
+
plugins: [
|
|
445
|
+
{
|
|
446
|
+
id: "custom-composer",
|
|
447
|
+
renderComposer: () => createCustomComposer()
|
|
448
|
+
}
|
|
449
|
+
]
|
|
450
|
+
} as any);
|
|
451
|
+
|
|
452
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
453
|
+
expect(scrollContainer).not.toBeNull();
|
|
454
|
+
|
|
455
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
456
|
+
scrollHeight: 1000,
|
|
457
|
+
clientHeight: 400
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
emitStreamingStatus(controller);
|
|
461
|
+
emitStreamingMessage(controller, "First chunk");
|
|
462
|
+
raf.flush();
|
|
463
|
+
|
|
464
|
+
scrollContainer!.dispatchEvent(new WheelEvent("wheel", { deltaY: -18 }));
|
|
465
|
+
metrics.setScrollTop(560);
|
|
466
|
+
metrics.setScrollHeight(1060);
|
|
467
|
+
emitStreamingMessage(controller, "Second chunk");
|
|
468
|
+
raf.flush();
|
|
469
|
+
|
|
470
|
+
const button = getScrollToBottomButton(mount);
|
|
471
|
+
expect(button).not.toBeNull();
|
|
472
|
+
expect(button?.parentElement).not.toBe(scrollContainer);
|
|
473
|
+
|
|
474
|
+
controller.destroy();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("hides the transcript affordance when scroll-to-bottom is disabled", () => {
|
|
478
|
+
const raf = installRafMock();
|
|
479
|
+
const mount = createMount();
|
|
480
|
+
const controller = createAgentExperience(mount, {
|
|
481
|
+
apiUrl: "https://api.example.com/chat",
|
|
482
|
+
launcher: { enabled: false },
|
|
483
|
+
features: {
|
|
484
|
+
scrollToBottom: {
|
|
485
|
+
enabled: false
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
} as any);
|
|
489
|
+
|
|
490
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
491
|
+
expect(scrollContainer).not.toBeNull();
|
|
492
|
+
|
|
493
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
494
|
+
scrollHeight: 1000,
|
|
495
|
+
clientHeight: 400
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
emitStreamingStatus(controller);
|
|
499
|
+
emitStreamingMessage(controller, "First chunk");
|
|
500
|
+
raf.flush();
|
|
501
|
+
|
|
502
|
+
scrollContainer!.dispatchEvent(new WheelEvent("wheel", { deltaY: -18 }));
|
|
503
|
+
metrics.setScrollTop(560);
|
|
504
|
+
metrics.setScrollHeight(1060);
|
|
505
|
+
emitStreamingMessage(controller, "Second chunk");
|
|
506
|
+
raf.flush();
|
|
507
|
+
|
|
508
|
+
expect(getScrollToBottomButton(mount)).toBeNull();
|
|
509
|
+
|
|
510
|
+
controller.destroy();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("renders the transcript affordance as icon-only when label is empty", () => {
|
|
514
|
+
const raf = installRafMock();
|
|
515
|
+
const mount = createMount();
|
|
516
|
+
const controller = createAgentExperience(mount, {
|
|
517
|
+
apiUrl: "https://api.example.com/chat",
|
|
518
|
+
launcher: { enabled: false },
|
|
519
|
+
features: {
|
|
520
|
+
scrollToBottom: {
|
|
521
|
+
enabled: true,
|
|
522
|
+
iconName: "arrow-down",
|
|
523
|
+
label: ""
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} as any);
|
|
527
|
+
|
|
528
|
+
const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
|
|
529
|
+
expect(scrollContainer).not.toBeNull();
|
|
530
|
+
|
|
531
|
+
const metrics = installScrollMetrics(scrollContainer!, {
|
|
532
|
+
scrollHeight: 1000,
|
|
533
|
+
clientHeight: 400
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
emitStreamingStatus(controller);
|
|
537
|
+
emitStreamingMessage(controller, "First chunk");
|
|
538
|
+
raf.flush();
|
|
539
|
+
|
|
540
|
+
scrollContainer!.dispatchEvent(new WheelEvent("wheel", { deltaY: -18 }));
|
|
541
|
+
metrics.setScrollTop(560);
|
|
542
|
+
metrics.setScrollHeight(1060);
|
|
543
|
+
emitStreamingMessage(controller, "Second chunk");
|
|
544
|
+
raf.flush();
|
|
545
|
+
|
|
546
|
+
const button = getScrollToBottomButton(mount);
|
|
547
|
+
expect(button).not.toBeNull();
|
|
548
|
+
expect(button?.textContent?.trim()).toBe("");
|
|
549
|
+
expect(button?.querySelector("svg")).not.toBeNull();
|
|
550
|
+
|
|
551
|
+
controller.destroy();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
});
|