@runtypelabs/persona 3.16.0 → 3.17.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.cjs +279 -0
- package/dist/animations/glyph-cycle.d.cts +5 -0
- package/dist/animations/glyph-cycle.d.ts +5 -0
- package/dist/animations/glyph-cycle.js +252 -0
- package/dist/animations/types-HPZY7oAI.d.cts +282 -0
- package/dist/animations/types-HPZY7oAI.d.ts +282 -0
- package/dist/animations/wipe.cjs +107 -0
- package/dist/animations/wipe.d.cts +5 -0
- package/dist/animations/wipe.d.ts +5 -0
- package/dist/animations/wipe.js +80 -0
- package/dist/index.cjs +48 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +205 -1
- package/dist/index.d.ts +205 -1
- package/dist/index.global.js +136 -81
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +48 -47
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +85 -0
- package/dist/testing.d.cts +39 -0
- package/dist/testing.d.ts +39 -0
- package/dist/testing.js +56 -0
- package/dist/theme-editor.cjs +714 -99
- package/dist/theme-editor.d.cts +214 -2
- package/dist/theme-editor.d.ts +214 -2
- package/dist/theme-editor.js +712 -99
- package/dist/widget.css +133 -0
- package/package.json +20 -3
- package/src/animations/glyph-cycle.ts +332 -0
- package/src/animations/wipe.ts +66 -0
- package/src/client.test.ts +141 -0
- package/src/client.ts +28 -0
- package/src/components/composer-builder.ts +61 -10
- package/src/components/message-bubble.test.ts +181 -2
- package/src/components/message-bubble.ts +209 -14
- package/src/components/panel.ts +4 -1
- package/src/defaults.ts +16 -0
- package/src/index-global.ts +31 -0
- package/src/index.ts +18 -0
- package/src/session.test.ts +93 -1
- package/src/session.ts +5 -0
- package/src/styles/widget.css +133 -0
- package/src/testing/index.ts +11 -0
- package/src/testing/mock-stream.test.ts +80 -0
- package/src/testing/mock-stream.ts +94 -0
- package/src/testing.ts +2 -0
- package/src/theme-editor/index.ts +4 -0
- package/src/theme-editor/preview-utils.test.ts +60 -0
- package/src/theme-editor/preview-utils.ts +129 -0
- package/src/theme-editor/sections.test.ts +19 -0
- package/src/theme-editor/sections.ts +84 -1
- package/src/types.ts +210 -0
- package/src/ui.stop-button.test.ts +165 -0
- package/src/ui.ts +75 -6
- package/src/utils/message-fingerprint.ts +2 -0
- package/src/utils/morph.ts +7 -0
- package/src/utils/stream-animation.test.ts +417 -0
- package/src/utils/stream-animation.ts +449 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildAssistantTurnFrames,
|
|
5
|
+
createMockSSEResponse,
|
|
6
|
+
createMockSSEStream,
|
|
7
|
+
} from "./mock-stream";
|
|
8
|
+
|
|
9
|
+
async function readAll(stream: ReadableStream<Uint8Array>): Promise<string> {
|
|
10
|
+
const reader = stream.getReader();
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
let out = "";
|
|
13
|
+
for (;;) {
|
|
14
|
+
const { done, value } = await reader.read();
|
|
15
|
+
if (done) break;
|
|
16
|
+
out += decoder.decode(value);
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("createMockSSEStream", () => {
|
|
22
|
+
it("emits bare `data:` frames by default", async () => {
|
|
23
|
+
const frames = [
|
|
24
|
+
{ type: "agent_turn_start", executionId: "e-1", turnId: "t-1" },
|
|
25
|
+
{ type: "agent_turn_delta", executionId: "e-1", turnId: "t-1", delta: "hi" },
|
|
26
|
+
{ type: "agent_turn_complete", executionId: "e-1", turnId: "t-1" },
|
|
27
|
+
];
|
|
28
|
+
const text = await readAll(createMockSSEStream(frames, { delayMs: 0 }));
|
|
29
|
+
|
|
30
|
+
expect(text).not.toContain("event:");
|
|
31
|
+
expect(text.split("\n\n").filter(Boolean)).toHaveLength(3);
|
|
32
|
+
expect(text).toContain('"type":"agent_turn_delta"');
|
|
33
|
+
expect(text).toContain('"delta":"hi"');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("emits named event frames when eventName is set", async () => {
|
|
37
|
+
const text = await readAll(
|
|
38
|
+
createMockSSEStream([{ type: "ping" }], { delayMs: 0, eventName: "message" })
|
|
39
|
+
);
|
|
40
|
+
expect(text.startsWith("event: message\n")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("buildAssistantTurnFrames", () => {
|
|
45
|
+
it("chunks text into delta frames bracketed by start/complete", () => {
|
|
46
|
+
const frames = buildAssistantTurnFrames({
|
|
47
|
+
executionId: "exec-1",
|
|
48
|
+
turnId: "turn-1",
|
|
49
|
+
text: "abcdefghij",
|
|
50
|
+
chunkSize: 4,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(frames[0]).toEqual({ type: "agent_turn_start", executionId: "exec-1", turnId: "turn-1" });
|
|
54
|
+
expect(frames[frames.length - 1]).toEqual({
|
|
55
|
+
type: "agent_turn_complete",
|
|
56
|
+
executionId: "exec-1",
|
|
57
|
+
turnId: "turn-1",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const deltas = frames.filter((f) => f.type === "agent_turn_delta");
|
|
61
|
+
expect(deltas.map((f) => f.delta)).toEqual(["abcd", "efgh", "ij"]);
|
|
62
|
+
expect(deltas.every((f) => f.executionId === "exec-1" && f.turnId === "turn-1")).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("defaults turnId and chunkSize", () => {
|
|
66
|
+
const frames = buildAssistantTurnFrames({ executionId: "exec-2", text: "hello" });
|
|
67
|
+
expect(frames[0].turnId).toBe("turn-1");
|
|
68
|
+
const deltaCount = frames.filter((f) => f.type === "agent_turn_delta").length;
|
|
69
|
+
expect(deltaCount).toBeGreaterThanOrEqual(1);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("createMockSSEResponse", () => {
|
|
74
|
+
it("wraps the stream in a text/event-stream Response", async () => {
|
|
75
|
+
const res = createMockSSEResponse([{ type: "ping" }], { delayMs: 0 });
|
|
76
|
+
expect(res.status).toBe(200);
|
|
77
|
+
expect(res.headers.get("Content-Type")).toBe("text/event-stream");
|
|
78
|
+
expect(await res.text()).toContain('"type":"ping"');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Shared helpers for mocking SSE streams in demos, previews, and tests. */
|
|
2
|
+
|
|
3
|
+
export interface MockSSEFrame {
|
|
4
|
+
type: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CreateMockSSEStreamOptions {
|
|
9
|
+
/** Delay in ms between emitted frames. Default: 100. */
|
|
10
|
+
delayMs?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Named event name. When set, each frame is emitted as `event: <name>\ndata: ...\n\n`.
|
|
13
|
+
* Omit for bare `data: ...\n\n` form (both are valid SSE and the widget parser accepts either).
|
|
14
|
+
*/
|
|
15
|
+
eventName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
|
|
20
|
+
export function createMockSSEStream(
|
|
21
|
+
frames: ReadonlyArray<MockSSEFrame>,
|
|
22
|
+
options?: CreateMockSSEStreamOptions
|
|
23
|
+
): ReadableStream<Uint8Array> {
|
|
24
|
+
const delayMs = options?.delayMs ?? 100;
|
|
25
|
+
const prefix = options?.eventName ? `event: ${options.eventName}\n` : "";
|
|
26
|
+
let index = 0;
|
|
27
|
+
|
|
28
|
+
return new ReadableStream<Uint8Array>({
|
|
29
|
+
async pull(controller) {
|
|
30
|
+
if (index >= frames.length) {
|
|
31
|
+
controller.close();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|
|
35
|
+
const payload = JSON.stringify(frames[index]);
|
|
36
|
+
controller.enqueue(encoder.encode(`${prefix}data: ${payload}\n\n`));
|
|
37
|
+
index += 1;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AssistantTurnFramesOptions {
|
|
43
|
+
/** Execution id shared across the turn's frames. */
|
|
44
|
+
executionId: string;
|
|
45
|
+
/** Turn id. Default: `turn-1`. */
|
|
46
|
+
turnId?: string;
|
|
47
|
+
/** Assistant text content to stream. */
|
|
48
|
+
text: string;
|
|
49
|
+
/** Approximate characters per `agent_turn_delta` frame. Default: 32. */
|
|
50
|
+
chunkSize?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Builds the standard `agent_turn_start` → many `agent_turn_delta` → `agent_turn_complete`
|
|
55
|
+
* frame sequence for simulating a streaming assistant reply. The frames drive the same
|
|
56
|
+
* client pipeline as real SSE, so stream animations (typewriter, word-fade, etc.) engage.
|
|
57
|
+
*/
|
|
58
|
+
export function buildAssistantTurnFrames(options: AssistantTurnFramesOptions): MockSSEFrame[] {
|
|
59
|
+
const { executionId, text } = options;
|
|
60
|
+
const turnId = options.turnId ?? "turn-1";
|
|
61
|
+
const chunkSize = Math.max(1, options.chunkSize ?? 32);
|
|
62
|
+
|
|
63
|
+
const frames: MockSSEFrame[] = [{ type: "agent_turn_start", executionId, turnId }];
|
|
64
|
+
for (let i = 0; i < text.length; i += chunkSize) {
|
|
65
|
+
frames.push({
|
|
66
|
+
type: "agent_turn_delta",
|
|
67
|
+
executionId,
|
|
68
|
+
turnId,
|
|
69
|
+
delta: text.slice(i, i + chunkSize),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
frames.push({ type: "agent_turn_complete", executionId, turnId });
|
|
73
|
+
return frames;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface MockSSEResponseOptions extends CreateMockSSEStreamOptions {
|
|
77
|
+
status?: number;
|
|
78
|
+
headers?: Record<string, string>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Convenience wrapper: returns a `Response` ready to hand back from a `customFetch` implementation. */
|
|
82
|
+
export function createMockSSEResponse(
|
|
83
|
+
frames: ReadonlyArray<MockSSEFrame>,
|
|
84
|
+
options?: MockSSEResponseOptions
|
|
85
|
+
): Response {
|
|
86
|
+
const stream = createMockSSEStream(frames, options);
|
|
87
|
+
return new Response(stream, {
|
|
88
|
+
status: options?.status ?? 200,
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "text/event-stream",
|
|
91
|
+
...options?.headers,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
package/src/testing.ts
ADDED
|
@@ -84,6 +84,8 @@ export {
|
|
|
84
84
|
getPreviewTranscriptPresetLabel,
|
|
85
85
|
createPreviewTranscriptEntry,
|
|
86
86
|
appendPreviewTranscriptEntry,
|
|
87
|
+
presetStreamsText,
|
|
88
|
+
buildTranscriptStreamFrames,
|
|
87
89
|
createPreviewMessages,
|
|
88
90
|
applySceneConfig,
|
|
89
91
|
buildPreviewConfig,
|
|
@@ -94,6 +96,8 @@ export type {
|
|
|
94
96
|
PreviewTranscriptEntryPreset,
|
|
95
97
|
PreviewShellPalette,
|
|
96
98
|
PreviewConfigOptions,
|
|
99
|
+
TranscriptStreamFrame,
|
|
100
|
+
BuildTranscriptStreamFramesOptions,
|
|
97
101
|
} from './preview-utils';
|
|
98
102
|
|
|
99
103
|
// Role mappings (Interface Roles editor)
|
|
@@ -3,7 +3,9 @@ import { describe, expect, it } from "vitest";
|
|
|
3
3
|
import {
|
|
4
4
|
appendPreviewTranscriptEntry,
|
|
5
5
|
buildPreviewConfig,
|
|
6
|
+
buildTranscriptStreamFrames,
|
|
6
7
|
createPreviewTranscriptEntry,
|
|
8
|
+
presetStreamsText,
|
|
7
9
|
} from "./preview-utils";
|
|
8
10
|
|
|
9
11
|
describe("theme editor preview demo data", () => {
|
|
@@ -55,4 +57,62 @@ describe("theme editor preview demo data", () => {
|
|
|
55
57
|
expect(updated[0].variant).toBe("tool");
|
|
56
58
|
expect(updated[1].variant).toBe("reasoning");
|
|
57
59
|
});
|
|
60
|
+
|
|
61
|
+
it("creates assistant code-block, table, and image presets with markdown content", () => {
|
|
62
|
+
const code = createPreviewTranscriptEntry("assistant-code-block", 1);
|
|
63
|
+
const table = createPreviewTranscriptEntry("assistant-markdown-table", 2);
|
|
64
|
+
const image = createPreviewTranscriptEntry("assistant-image", 3);
|
|
65
|
+
|
|
66
|
+
for (const message of [code, table, image]) {
|
|
67
|
+
expect(message.role).toBe("assistant");
|
|
68
|
+
expect(message.variant).toBeUndefined();
|
|
69
|
+
expect(typeof message.content).toBe("string");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
expect(code.content).toContain("```ts");
|
|
73
|
+
expect(table.content).toContain("| Preset");
|
|
74
|
+
expect(image.content).toContain("![");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("flags assistant text presets as streaming and non-text presets as not", () => {
|
|
78
|
+
expect(presetStreamsText("assistant-message")).toBe(true);
|
|
79
|
+
expect(presetStreamsText("assistant-code-block")).toBe(true);
|
|
80
|
+
expect(presetStreamsText("assistant-markdown-table")).toBe(true);
|
|
81
|
+
expect(presetStreamsText("assistant-image")).toBe(true);
|
|
82
|
+
expect(presetStreamsText("user-message")).toBe(false);
|
|
83
|
+
expect(presetStreamsText("reasoning-streaming")).toBe(false);
|
|
84
|
+
expect(presetStreamsText("tool-running")).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("builds a single done frame for non-streaming presets", () => {
|
|
88
|
+
const frames = buildTranscriptStreamFrames("user-message", 0);
|
|
89
|
+
expect(frames).toHaveLength(1);
|
|
90
|
+
expect(frames[0].done).toBe(true);
|
|
91
|
+
expect(frames[0].delayMs).toBe(0);
|
|
92
|
+
expect(frames[0].message.role).toBe("user");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("builds progressive snapshots for assistant text presets ending with streaming:false", () => {
|
|
96
|
+
const frames = buildTranscriptStreamFrames("assistant-message", 0, { chunkSize: 10, delayMs: 5 });
|
|
97
|
+
expect(frames.length).toBeGreaterThan(1);
|
|
98
|
+
|
|
99
|
+
const first = frames[0];
|
|
100
|
+
expect(first.message.content).toBe("");
|
|
101
|
+
expect(first.message.streaming).toBe(true);
|
|
102
|
+
expect(first.delayMs).toBe(0);
|
|
103
|
+
expect(first.done).toBe(false);
|
|
104
|
+
|
|
105
|
+
for (let i = 1; i < frames.length - 1; i += 1) {
|
|
106
|
+
expect(frames[i].message.streaming).toBe(true);
|
|
107
|
+
expect(frames[i].done).toBe(false);
|
|
108
|
+
expect(frames[i].delayMs).toBe(5);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const last = frames[frames.length - 1];
|
|
112
|
+
expect(last.message.streaming).toBe(false);
|
|
113
|
+
expect(last.done).toBe(true);
|
|
114
|
+
const completed = createPreviewTranscriptEntry("assistant-message", 0);
|
|
115
|
+
expect(last.message.content).toBe(completed.content);
|
|
116
|
+
expect(last.message.id).toBe(completed.id);
|
|
117
|
+
});
|
|
58
118
|
});
|
|
@@ -200,6 +200,9 @@ export type PreviewScene = 'home' | 'conversation' | 'minimized' | 'artifact';
|
|
|
200
200
|
export type PreviewTranscriptEntryPreset =
|
|
201
201
|
| 'user-message'
|
|
202
202
|
| 'assistant-message'
|
|
203
|
+
| 'assistant-code-block'
|
|
204
|
+
| 'assistant-markdown-table'
|
|
205
|
+
| 'assistant-image'
|
|
203
206
|
| 'reasoning-streaming'
|
|
204
207
|
| 'reasoning-complete'
|
|
205
208
|
| 'tool-running'
|
|
@@ -208,6 +211,9 @@ export type PreviewTranscriptEntryPreset =
|
|
|
208
211
|
const PREVIEW_TRANSCRIPT_PRESET_LABELS: Record<PreviewTranscriptEntryPreset, string> = {
|
|
209
212
|
'user-message': 'User message',
|
|
210
213
|
'assistant-message': 'Assistant message',
|
|
214
|
+
'assistant-code-block': 'Assistant — code block',
|
|
215
|
+
'assistant-markdown-table': 'Assistant — markdown table',
|
|
216
|
+
'assistant-image': 'Assistant — image',
|
|
211
217
|
'reasoning-streaming': 'Reasoning (streaming)',
|
|
212
218
|
'reasoning-complete': 'Reasoning (complete)',
|
|
213
219
|
'tool-running': 'Tool call (running)',
|
|
@@ -240,6 +246,56 @@ export function createPreviewTranscriptEntry(
|
|
|
240
246
|
content: 'Absolutely. I can keep going and explain what happens next.',
|
|
241
247
|
createdAt,
|
|
242
248
|
};
|
|
249
|
+
case 'assistant-code-block':
|
|
250
|
+
return {
|
|
251
|
+
id: `preview-seq-assistant-code-${suffix}`,
|
|
252
|
+
role: 'assistant',
|
|
253
|
+
content: [
|
|
254
|
+
"Here's how you'd wire up a streaming animation:",
|
|
255
|
+
'',
|
|
256
|
+
'```ts',
|
|
257
|
+
"import { createAgentExperience } from '@runtypelabs/persona';",
|
|
258
|
+
'',
|
|
259
|
+
'createAgentExperience(el, {',
|
|
260
|
+
' features: {',
|
|
261
|
+
' streamAnimation: { type: "letter-rise", speed: 120 },',
|
|
262
|
+
' },',
|
|
263
|
+
'});',
|
|
264
|
+
'```',
|
|
265
|
+
'',
|
|
266
|
+
'Swap the `type` value to try the other presets.',
|
|
267
|
+
].join('\n'),
|
|
268
|
+
createdAt,
|
|
269
|
+
};
|
|
270
|
+
case 'assistant-markdown-table':
|
|
271
|
+
return {
|
|
272
|
+
id: `preview-seq-assistant-table-${suffix}`,
|
|
273
|
+
role: 'assistant',
|
|
274
|
+
content: [
|
|
275
|
+
'Here are the built-in streaming animations at a glance:',
|
|
276
|
+
'',
|
|
277
|
+
'| Preset | Wrap unit | Best for |',
|
|
278
|
+
'| ------------ | --------- | --------------------------- |',
|
|
279
|
+
'| Typewriter | Character | Classic terminal feel |',
|
|
280
|
+
'| Letter rise | Character | Soft, staggered entrance |',
|
|
281
|
+
'| Word fade | Word | Longer-form assistant replies |',
|
|
282
|
+
'| Pop bubble | Bubble | Short, punchy affirmations |',
|
|
283
|
+
].join('\n'),
|
|
284
|
+
createdAt,
|
|
285
|
+
};
|
|
286
|
+
case 'assistant-image':
|
|
287
|
+
return {
|
|
288
|
+
id: `preview-seq-assistant-image-${suffix}`,
|
|
289
|
+
role: 'assistant',
|
|
290
|
+
content: [
|
|
291
|
+
"Here's the reference diagram you asked for — let me know if you'd like a different view:",
|
|
292
|
+
'',
|
|
293
|
+
'',
|
|
294
|
+
'',
|
|
295
|
+
'The gradient shows how per-unit delays stagger across the reply.',
|
|
296
|
+
].join('\n'),
|
|
297
|
+
createdAt,
|
|
298
|
+
};
|
|
243
299
|
case 'reasoning-streaming':
|
|
244
300
|
return {
|
|
245
301
|
id: `preview-seq-reasoning-stream-${suffix}`,
|
|
@@ -312,6 +368,79 @@ export function appendPreviewTranscriptEntry(
|
|
|
312
368
|
return [...messages, createPreviewTranscriptEntry(preset, messages.length)];
|
|
313
369
|
}
|
|
314
370
|
|
|
371
|
+
/** Presets whose assistant content should stream in so Stream Animation settings engage. */
|
|
372
|
+
export function presetStreamsText(preset: PreviewTranscriptEntryPreset): boolean {
|
|
373
|
+
return (
|
|
374
|
+
preset === 'assistant-message' ||
|
|
375
|
+
preset === 'assistant-code-block' ||
|
|
376
|
+
preset === 'assistant-markdown-table' ||
|
|
377
|
+
preset === 'assistant-image'
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export interface TranscriptStreamFrame {
|
|
382
|
+
/** Message to upsert into the session. */
|
|
383
|
+
message: AgentWidgetMessage;
|
|
384
|
+
/** Delay from the previous frame in ms. The first frame uses 0. */
|
|
385
|
+
delayMs: number;
|
|
386
|
+
/** True when this is the final frame (message is no longer streaming). */
|
|
387
|
+
done: boolean;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface BuildTranscriptStreamFramesOptions {
|
|
391
|
+
/** Characters per progressive chunk. Default: 24. */
|
|
392
|
+
chunkSize?: number;
|
|
393
|
+
/** Delay between chunks in ms. Default: 42. */
|
|
394
|
+
delayMs?: number;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Builds progressive snapshots for a transcript preset suitable for feeding into
|
|
399
|
+
* `injectTestMessage({ type: 'message', message })` on a timer. Each frame upserts
|
|
400
|
+
* the same message id with more content, ending with `streaming: false`.
|
|
401
|
+
*
|
|
402
|
+
* - Streaming-capable presets (assistant text) yield many frames.
|
|
403
|
+
* - All other presets yield a single `done` frame matching `createPreviewTranscriptEntry`.
|
|
404
|
+
*/
|
|
405
|
+
export function buildTranscriptStreamFrames(
|
|
406
|
+
preset: PreviewTranscriptEntryPreset,
|
|
407
|
+
suffix: number,
|
|
408
|
+
options?: BuildTranscriptStreamFramesOptions
|
|
409
|
+
): TranscriptStreamFrame[] {
|
|
410
|
+
const completed = createPreviewTranscriptEntry(preset, suffix);
|
|
411
|
+
if (!presetStreamsText(preset) || typeof completed.content !== 'string') {
|
|
412
|
+
return [{ message: completed, delayMs: 0, done: true }];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const chunkSize = Math.max(1, options?.chunkSize ?? 24);
|
|
416
|
+
const delayMs = Math.max(0, options?.delayMs ?? 42);
|
|
417
|
+
const fullText = completed.content;
|
|
418
|
+
const frames: TranscriptStreamFrame[] = [];
|
|
419
|
+
|
|
420
|
+
// Seed with an empty streaming bubble so the animation plugin can attach from the first tick.
|
|
421
|
+
frames.push({
|
|
422
|
+
message: { ...completed, content: '', streaming: true },
|
|
423
|
+
delayMs: 0,
|
|
424
|
+
done: false,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
for (let i = chunkSize; i < fullText.length; i += chunkSize) {
|
|
428
|
+
frames.push({
|
|
429
|
+
message: { ...completed, content: fullText.slice(0, i), streaming: true },
|
|
430
|
+
delayMs,
|
|
431
|
+
done: false,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
frames.push({
|
|
436
|
+
message: { ...completed, content: fullText, streaming: false },
|
|
437
|
+
delayMs,
|
|
438
|
+
done: true,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
return frames;
|
|
442
|
+
}
|
|
443
|
+
|
|
315
444
|
const createAdvancedTranscriptPreviewMessages = (): AgentWidgetMessage[] => [
|
|
316
445
|
{
|
|
317
446
|
id: "preview-adv-1",
|
|
@@ -60,4 +60,23 @@ describe("theme editor scroll-to-bottom controls", () => {
|
|
|
60
60
|
expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.previewMaxLines")).toBe(true);
|
|
61
61
|
expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.activeMinHeight")).toBe(true);
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
it("exposes stream animation controls", () => {
|
|
65
|
+
const section = CONFIGURE_SECTIONS.find((entry) => entry.id === "stream-animation");
|
|
66
|
+
|
|
67
|
+
expect(section).toBeDefined();
|
|
68
|
+
const paths = section?.fields.map((field) => field.path) ?? [];
|
|
69
|
+
expect(paths).toEqual(
|
|
70
|
+
expect.arrayContaining([
|
|
71
|
+
"features.streamAnimation.type",
|
|
72
|
+
"features.streamAnimation.placeholder",
|
|
73
|
+
"features.streamAnimation.buffer",
|
|
74
|
+
"features.streamAnimation.speed",
|
|
75
|
+
"features.streamAnimation.duration",
|
|
76
|
+
])
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const speedField = section?.fields.find((field) => field.path === "features.streamAnimation.speed");
|
|
80
|
+
expect(speedField?.parseValue?.("240")).toBe(240);
|
|
81
|
+
});
|
|
63
82
|
});
|
|
@@ -697,6 +697,89 @@ const featuresSectionDef: SectionDef = {
|
|
|
697
697
|
],
|
|
698
698
|
};
|
|
699
699
|
|
|
700
|
+
const streamAnimationSectionDef: SectionDef = {
|
|
701
|
+
id: 'stream-animation', title: 'Stream Animation', description: 'Control how assistant text appears while streaming.', collapsed: true,
|
|
702
|
+
fields: [
|
|
703
|
+
{
|
|
704
|
+
id: 'stream-anim-type',
|
|
705
|
+
label: 'Animation',
|
|
706
|
+
description: 'Reveal effect applied to each assistant reply as it streams.',
|
|
707
|
+
type: 'select',
|
|
708
|
+
path: 'features.streamAnimation.type',
|
|
709
|
+
defaultValue: 'none',
|
|
710
|
+
options: [
|
|
711
|
+
{ value: 'none', label: 'None' },
|
|
712
|
+
{ value: 'typewriter', label: 'Typewriter' },
|
|
713
|
+
{ value: 'word-fade', label: 'Word fade' },
|
|
714
|
+
{ value: 'letter-rise', label: 'Letter rise' },
|
|
715
|
+
{ value: 'glyph-cycle', label: 'Glyph cycle' },
|
|
716
|
+
{ value: 'wipe', label: 'Wipe' },
|
|
717
|
+
{ value: 'pop-bubble', label: 'Pop bubble' },
|
|
718
|
+
],
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: 'stream-anim-placeholder',
|
|
722
|
+
label: 'Pre-first-token Placeholder',
|
|
723
|
+
description: 'What to show before the first token arrives.',
|
|
724
|
+
type: 'select',
|
|
725
|
+
path: 'features.streamAnimation.placeholder',
|
|
726
|
+
defaultValue: 'none',
|
|
727
|
+
options: [
|
|
728
|
+
{ value: 'none', label: 'Typing indicator (default)' },
|
|
729
|
+
{ value: 'skeleton', label: 'Skeleton shimmer' },
|
|
730
|
+
],
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
id: 'stream-anim-buffer',
|
|
734
|
+
label: 'Content Buffering',
|
|
735
|
+
description: 'Trim in-progress units so only complete words/lines reveal.',
|
|
736
|
+
type: 'select',
|
|
737
|
+
path: 'features.streamAnimation.buffer',
|
|
738
|
+
defaultValue: 'none',
|
|
739
|
+
options: [
|
|
740
|
+
{ value: 'none', label: 'None — stream every character' },
|
|
741
|
+
{ value: 'word', label: 'Word — hold until whitespace' },
|
|
742
|
+
{ value: 'line', label: 'Line — hold until newline' },
|
|
743
|
+
],
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
id: 'stream-anim-speed',
|
|
747
|
+
label: 'Per-unit Duration (ms)',
|
|
748
|
+
description: 'Animation length for each character or word.',
|
|
749
|
+
type: 'select',
|
|
750
|
+
path: 'features.streamAnimation.speed',
|
|
751
|
+
defaultValue: 120,
|
|
752
|
+
options: [
|
|
753
|
+
{ value: '40', label: '40ms — snappy' },
|
|
754
|
+
{ value: '80', label: '80ms' },
|
|
755
|
+
{ value: '120', label: '120ms (default)' },
|
|
756
|
+
{ value: '200', label: '200ms' },
|
|
757
|
+
{ value: '320', label: '320ms' },
|
|
758
|
+
{ value: '480', label: '480ms — slow' },
|
|
759
|
+
],
|
|
760
|
+
formatValue: (v: unknown) => String(v ?? 120),
|
|
761
|
+
parseValue: (v: unknown) => Number(v),
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
id: 'stream-anim-duration',
|
|
765
|
+
label: 'Container Duration (ms)',
|
|
766
|
+
description: 'Length of container-level effects (pop-bubble, custom plugins).',
|
|
767
|
+
type: 'select',
|
|
768
|
+
path: 'features.streamAnimation.duration',
|
|
769
|
+
defaultValue: 1800,
|
|
770
|
+
options: [
|
|
771
|
+
{ value: '600', label: '600ms' },
|
|
772
|
+
{ value: '1200', label: '1200ms' },
|
|
773
|
+
{ value: '1800', label: '1800ms (default)' },
|
|
774
|
+
{ value: '2400', label: '2400ms' },
|
|
775
|
+
{ value: '3600', label: '3600ms — slow' },
|
|
776
|
+
],
|
|
777
|
+
formatValue: (v: unknown) => String(v ?? 1800),
|
|
778
|
+
parseValue: (v: unknown) => Number(v),
|
|
779
|
+
},
|
|
780
|
+
],
|
|
781
|
+
};
|
|
782
|
+
|
|
700
783
|
const attachmentsSectionDef: SectionDef = {
|
|
701
784
|
id: 'attachments-config', title: 'Attachments', collapsed: true,
|
|
702
785
|
fields: [
|
|
@@ -773,7 +856,7 @@ export const CONFIGURE_SUB_GROUPS: SubGroupDef[] = [
|
|
|
773
856
|
{ label: 'Content', sections: [copySectionDef, suggestionsSectionDef] },
|
|
774
857
|
{ label: 'Layout', sections: [generalLayoutSectionDef, headerLayoutSectionDef, messagesLayoutSectionDef, messageActionsSectionDef] },
|
|
775
858
|
{ label: 'Widget', sections: [launcherBasicsSectionDef, launcherAdvancedSectionDef, sendButtonSectionDef, closeButtonSectionDef, clearChatSectionDef, statusIndicatorSectionDef] },
|
|
776
|
-
{ label: 'Features', sections: [featuresSectionDef, attachmentsSectionDef, artifactsSectionDef, artifactCustomizationSectionDef] },
|
|
859
|
+
{ label: 'Features', sections: [featuresSectionDef, streamAnimationSectionDef, attachmentsSectionDef, artifactsSectionDef, artifactCustomizationSectionDef] },
|
|
777
860
|
{ label: 'Developer', collapsedByDefault: true, sections: [apiIntegrationSectionDef, debugSectionDef, markdownSectionDef] },
|
|
778
861
|
];
|
|
779
862
|
|