@runtypelabs/persona 3.5.2 → 3.7.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 +46 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.global.js +70 -70
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -46
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +18015 -0
- package/dist/theme-editor.d.cts +3888 -0
- package/dist/theme-editor.d.ts +3888 -0
- package/dist/theme-editor.js +17909 -0
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +33 -0
- package/dist/theme-reference.d.ts +33 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +69 -25
- package/package.json +9 -7
- package/src/components/artifact-card.ts +1 -1
- package/src/components/composer-builder.ts +16 -29
- package/src/components/demo-carousel.ts +5 -5
- package/src/components/event-stream-view.test.ts +142 -0
- package/src/components/event-stream-view.ts +68 -29
- package/src/components/header-builder.ts +2 -2
- package/src/components/launcher.ts +9 -0
- package/src/components/message-bubble.ts +9 -3
- package/src/components/suggestions.ts +1 -1
- package/src/defaults.ts +24 -9
- package/src/scroll-to-bottom-defaults.test.ts +13 -0
- package/src/styles/widget.css +69 -25
- package/src/theme-editor/color-utils.ts +252 -0
- package/src/theme-editor/index.ts +131 -0
- package/src/theme-editor/presets.ts +144 -0
- package/src/theme-editor/preview-utils.ts +265 -0
- package/src/theme-editor/preview.ts +445 -0
- package/src/theme-editor/role-mappings.ts +343 -0
- package/src/theme-editor/sections.test.ts +43 -0
- package/src/theme-editor/sections.ts +994 -0
- package/src/theme-editor/state.ts +298 -0
- package/src/theme-editor/types.ts +177 -0
- package/src/theme-editor.ts +2 -0
- package/src/theme-reference.ts +8 -0
- package/src/types/theme.ts +11 -0
- package/src/types.ts +22 -0
- package/src/ui.scroll.test.ts +554 -0
- package/src/ui.ts +223 -133
- package/src/utils/auto-follow.test.ts +110 -0
- package/src/utils/auto-follow.ts +112 -0
- package/src/utils/plugins.ts +1 -1
- package/src/utils/theme.test.ts +44 -8
- package/src/utils/theme.ts +11 -11
- package/src/utils/tokens.ts +137 -41
- package/widget.css +0 -1
|
@@ -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
|
+
});
|