@runtypelabs/persona 1.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1080 -0
- package/dist/index.cjs +140 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2626 -0
- package/dist/index.d.ts +2626 -0
- package/dist/index.global.js +1843 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +140 -0
- package/dist/index.js.map +1 -0
- package/dist/install.global.js +2 -0
- package/dist/install.global.js.map +1 -0
- package/dist/widget.css +1627 -0
- package/package.json +79 -0
- package/src/@types/idiomorph.d.ts +37 -0
- package/src/client.test.ts +387 -0
- package/src/client.ts +1589 -0
- package/src/components/composer-builder.ts +530 -0
- package/src/components/feedback.ts +379 -0
- package/src/components/forms.ts +170 -0
- package/src/components/header-builder.ts +455 -0
- package/src/components/header-layouts.ts +303 -0
- package/src/components/launcher.ts +193 -0
- package/src/components/message-bubble.ts +528 -0
- package/src/components/messages.ts +54 -0
- package/src/components/panel.ts +204 -0
- package/src/components/reasoning-bubble.ts +144 -0
- package/src/components/registry.ts +87 -0
- package/src/components/suggestions.ts +97 -0
- package/src/components/tool-bubble.ts +288 -0
- package/src/defaults.ts +321 -0
- package/src/index.ts +175 -0
- package/src/install.ts +284 -0
- package/src/plugins/registry.ts +77 -0
- package/src/plugins/types.ts +95 -0
- package/src/postprocessors.ts +194 -0
- package/src/runtime/init.ts +162 -0
- package/src/session.ts +376 -0
- package/src/styles/tailwind.css +20 -0
- package/src/styles/widget.css +1627 -0
- package/src/types.ts +1635 -0
- package/src/ui.ts +3341 -0
- package/src/utils/actions.ts +227 -0
- package/src/utils/attachment-manager.ts +384 -0
- package/src/utils/code-generators.test.ts +500 -0
- package/src/utils/code-generators.ts +1806 -0
- package/src/utils/component-middleware.ts +137 -0
- package/src/utils/component-parser.ts +119 -0
- package/src/utils/constants.ts +16 -0
- package/src/utils/content.ts +306 -0
- package/src/utils/dom.ts +25 -0
- package/src/utils/events.ts +41 -0
- package/src/utils/formatting.test.ts +166 -0
- package/src/utils/formatting.ts +470 -0
- package/src/utils/icons.ts +92 -0
- package/src/utils/message-id.ts +37 -0
- package/src/utils/morph.ts +36 -0
- package/src/utils/positioning.ts +17 -0
- package/src/utils/storage.ts +72 -0
- package/src/utils/theme.ts +105 -0
- package/src/widget.css +1 -0
- package/widget.css +1 -0
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@runtypelabs/persona",
|
|
3
|
+
"version": "1.36.0",
|
|
4
|
+
"description": "Themeable, plugable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./widget.css": {
|
|
16
|
+
"import": "./widget.css",
|
|
17
|
+
"default": "./dist/widget.css"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"widget.css",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"idiomorph": "^0.7.4",
|
|
27
|
+
"lucide": "^0.552.0",
|
|
28
|
+
"marked": "^12.0.2",
|
|
29
|
+
"partial-json": "^0.1.7",
|
|
30
|
+
"zod": "^3.22.4"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20.12.7",
|
|
34
|
+
"@vitest/ui": "^4.0.9",
|
|
35
|
+
"eslint": "^8.57.0",
|
|
36
|
+
"eslint-config-prettier": "^9.1.0",
|
|
37
|
+
"postcss": "^8.4.38",
|
|
38
|
+
"rimraf": "^5.0.5",
|
|
39
|
+
"tailwindcss": "^3.4.10",
|
|
40
|
+
"tsup": "^8.0.1",
|
|
41
|
+
"typescript": "^5.4.5",
|
|
42
|
+
"vitest": "^4.0.9"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.17.0"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"keywords": [
|
|
49
|
+
"chat",
|
|
50
|
+
"widget",
|
|
51
|
+
"streaming",
|
|
52
|
+
"typescript",
|
|
53
|
+
"persona",
|
|
54
|
+
"agent"
|
|
55
|
+
],
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "git+https://github.com/runtypelabs/persona.git",
|
|
59
|
+
"directory": "packages/widget"
|
|
60
|
+
},
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/runtypelabs/persona/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/runtypelabs/persona/tree/main/packages/widget#readme",
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "rimraf dist && npm run build:styles && npm run build:client && npm run build:installer",
|
|
70
|
+
"build:styles": "node -e \"const fs=require('fs');fs.mkdirSync('dist',{recursive:true});fs.copyFileSync('src/styles/widget.css','dist/widget.css');\"",
|
|
71
|
+
"build:client": "tsup src/index.ts --format esm,cjs,iife --global-name AgentWidget --minify --sourcemap --splitting false --dts --loader \".css=text\"",
|
|
72
|
+
"build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
|
|
73
|
+
"lint": "eslint . --ext .ts",
|
|
74
|
+
"typecheck": "tsc --noEmit",
|
|
75
|
+
"test": "vitest",
|
|
76
|
+
"test:ui": "vitest --ui",
|
|
77
|
+
"test:run": "vitest run"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
declare module "idiomorph" {
|
|
2
|
+
export interface IdiomorphCallbacks {
|
|
3
|
+
beforeNodeAdded?: (node: Node) => boolean | void;
|
|
4
|
+
afterNodeAdded?: (node: Node) => void;
|
|
5
|
+
beforeNodeMorphed?: (oldNode: Node, newNode: Node) => boolean | void;
|
|
6
|
+
afterNodeMorphed?: (oldNode: Node, newNode: Node) => void;
|
|
7
|
+
beforeNodeRemoved?: (node: Node) => boolean | void;
|
|
8
|
+
afterNodeRemoved?: (node: Node) => void;
|
|
9
|
+
beforeAttributeUpdated?: (
|
|
10
|
+
attributeName: string,
|
|
11
|
+
node: Node,
|
|
12
|
+
mutationType: "update" | "remove"
|
|
13
|
+
) => boolean | void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IdiomorphOptions {
|
|
17
|
+
morphStyle?: "outerHTML" | "innerHTML";
|
|
18
|
+
ignoreActive?: boolean;
|
|
19
|
+
ignoreActiveValue?: boolean;
|
|
20
|
+
restoreFocus?: boolean;
|
|
21
|
+
callbacks?: IdiomorphCallbacks;
|
|
22
|
+
head?: {
|
|
23
|
+
style?: "merge" | "append" | "morph" | "none";
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IdiomorphStatic {
|
|
28
|
+
morph(
|
|
29
|
+
oldNode: Element | Document,
|
|
30
|
+
newContent: Element | Node | string,
|
|
31
|
+
options?: IdiomorphOptions
|
|
32
|
+
): void;
|
|
33
|
+
defaults: IdiomorphOptions;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Idiomorph: IdiomorphStatic;
|
|
37
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { AgentWidgetClient } from './client';
|
|
3
|
+
import { AgentWidgetEvent, AgentWidgetMessage } from './types';
|
|
4
|
+
import { createJsonStreamParser } from './utils/formatting';
|
|
5
|
+
|
|
6
|
+
describe('AgentWidgetClient - Empty Message Filtering', () => {
|
|
7
|
+
let client: AgentWidgetClient;
|
|
8
|
+
let events: AgentWidgetEvent[] = [];
|
|
9
|
+
let capturedPayload: any = null;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
events = [];
|
|
13
|
+
capturedPayload = null;
|
|
14
|
+
client = new AgentWidgetClient({
|
|
15
|
+
apiUrl: 'http://localhost:8000',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should filter out messages with empty content before sending', async () => {
|
|
20
|
+
// Create a mock fetch that captures the request payload
|
|
21
|
+
global.fetch = vi.fn().mockImplementation(async (url: string, options: any) => {
|
|
22
|
+
capturedPayload = JSON.parse(options.body);
|
|
23
|
+
// Return a minimal successful response
|
|
24
|
+
const encoder = new TextEncoder();
|
|
25
|
+
const stream = new ReadableStream({
|
|
26
|
+
start(controller) {
|
|
27
|
+
controller.enqueue(encoder.encode('data: {"type":"flow_complete","success":true}\n\n'));
|
|
28
|
+
controller.close();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return { ok: true, body: stream };
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Messages array with an empty assistant message (simulating failed API response)
|
|
35
|
+
const messages: AgentWidgetMessage[] = [
|
|
36
|
+
{
|
|
37
|
+
id: 'usr_1',
|
|
38
|
+
role: 'user',
|
|
39
|
+
content: 'What can you help me with?',
|
|
40
|
+
createdAt: '2025-01-01T00:00:00.000Z',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'ast_1',
|
|
44
|
+
role: 'assistant',
|
|
45
|
+
content: '', // Empty content from failed request - THIS SHOULD BE FILTERED OUT
|
|
46
|
+
createdAt: '2025-01-01T00:00:01.000Z',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'usr_2',
|
|
50
|
+
role: 'user',
|
|
51
|
+
content: 'test',
|
|
52
|
+
createdAt: '2025-01-01T00:00:02.000Z',
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
await client.dispatch(
|
|
57
|
+
{ messages },
|
|
58
|
+
(event) => events.push(event)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Verify the empty message was filtered out
|
|
62
|
+
expect(capturedPayload).toBeDefined();
|
|
63
|
+
expect(capturedPayload.messages).toHaveLength(2);
|
|
64
|
+
expect(capturedPayload.messages[0].content).toBe('What can you help me with?');
|
|
65
|
+
expect(capturedPayload.messages[1].content).toBe('test');
|
|
66
|
+
|
|
67
|
+
// Verify no message has empty content
|
|
68
|
+
const hasEmptyContent = capturedPayload.messages.some(
|
|
69
|
+
(m: any) => !m.content || (typeof m.content === 'string' && m.content.trim() === '')
|
|
70
|
+
);
|
|
71
|
+
expect(hasEmptyContent).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should filter out messages with whitespace-only content', async () => {
|
|
75
|
+
global.fetch = vi.fn().mockImplementation(async (url: string, options: any) => {
|
|
76
|
+
capturedPayload = JSON.parse(options.body);
|
|
77
|
+
const encoder = new TextEncoder();
|
|
78
|
+
const stream = new ReadableStream({
|
|
79
|
+
start(controller) {
|
|
80
|
+
controller.enqueue(encoder.encode('data: {"type":"flow_complete","success":true}\n\n'));
|
|
81
|
+
controller.close();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return { ok: true, body: stream };
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const messages: AgentWidgetMessage[] = [
|
|
88
|
+
{
|
|
89
|
+
id: 'usr_1',
|
|
90
|
+
role: 'user',
|
|
91
|
+
content: 'Hello',
|
|
92
|
+
createdAt: '2025-01-01T00:00:00.000Z',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'ast_1',
|
|
96
|
+
role: 'assistant',
|
|
97
|
+
content: ' ', // Whitespace-only content - SHOULD BE FILTERED OUT
|
|
98
|
+
createdAt: '2025-01-01T00:00:01.000Z',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'ast_2',
|
|
102
|
+
role: 'assistant',
|
|
103
|
+
content: '\n\t', // Whitespace-only content - SHOULD BE FILTERED OUT
|
|
104
|
+
createdAt: '2025-01-01T00:00:02.000Z',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'usr_2',
|
|
108
|
+
role: 'user',
|
|
109
|
+
content: 'World',
|
|
110
|
+
createdAt: '2025-01-01T00:00:03.000Z',
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
await client.dispatch(
|
|
115
|
+
{ messages },
|
|
116
|
+
(event) => events.push(event)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(capturedPayload.messages).toHaveLength(2);
|
|
120
|
+
expect(capturedPayload.messages[0].content).toBe('Hello');
|
|
121
|
+
expect(capturedPayload.messages[1].content).toBe('World');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should preserve messages with valid contentParts even if content is empty', async () => {
|
|
125
|
+
global.fetch = vi.fn().mockImplementation(async (url: string, options: any) => {
|
|
126
|
+
capturedPayload = JSON.parse(options.body);
|
|
127
|
+
const encoder = new TextEncoder();
|
|
128
|
+
const stream = new ReadableStream({
|
|
129
|
+
start(controller) {
|
|
130
|
+
controller.enqueue(encoder.encode('data: {"type":"flow_complete","success":true}\n\n'));
|
|
131
|
+
controller.close();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return { ok: true, body: stream };
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const messages: AgentWidgetMessage[] = [
|
|
138
|
+
{
|
|
139
|
+
id: 'usr_1',
|
|
140
|
+
role: 'user',
|
|
141
|
+
content: '', // Empty content but has contentParts - SHOULD BE PRESERVED
|
|
142
|
+
contentParts: [{ type: 'image', data: 'base64data' }] as any,
|
|
143
|
+
createdAt: '2025-01-01T00:00:00.000Z',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'ast_1',
|
|
147
|
+
role: 'assistant',
|
|
148
|
+
content: '', // Empty content, no contentParts - SHOULD BE FILTERED OUT
|
|
149
|
+
createdAt: '2025-01-01T00:00:01.000Z',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
await client.dispatch(
|
|
154
|
+
{ messages },
|
|
155
|
+
(event) => events.push(event)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(capturedPayload.messages).toHaveLength(1);
|
|
159
|
+
// The message with contentParts should be preserved
|
|
160
|
+
expect(capturedPayload.messages[0].content).toEqual([{ type: 'image', data: 'base64data' }]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should preserve messages with valid rawContent even if content is empty', async () => {
|
|
164
|
+
global.fetch = vi.fn().mockImplementation(async (url: string, options: any) => {
|
|
165
|
+
capturedPayload = JSON.parse(options.body);
|
|
166
|
+
const encoder = new TextEncoder();
|
|
167
|
+
const stream = new ReadableStream({
|
|
168
|
+
start(controller) {
|
|
169
|
+
controller.enqueue(encoder.encode('data: {"type":"flow_complete","success":true}\n\n'));
|
|
170
|
+
controller.close();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
return { ok: true, body: stream };
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const messages: AgentWidgetMessage[] = [
|
|
177
|
+
{
|
|
178
|
+
id: 'ast_1',
|
|
179
|
+
role: 'assistant',
|
|
180
|
+
content: '', // Empty content but has rawContent - SHOULD BE PRESERVED
|
|
181
|
+
rawContent: '{"action": "message", "text": "Hello"}',
|
|
182
|
+
createdAt: '2025-01-01T00:00:00.000Z',
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
await client.dispatch(
|
|
187
|
+
{ messages },
|
|
188
|
+
(event) => events.push(event)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(capturedPayload.messages).toHaveLength(1);
|
|
192
|
+
expect(capturedPayload.messages[0].content).toBe('{"action": "message", "text": "Hello"}');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('AgentWidgetClient - JSON Streaming', () => {
|
|
197
|
+
let client: AgentWidgetClient;
|
|
198
|
+
let events: AgentWidgetEvent[] = [];
|
|
199
|
+
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
events = [];
|
|
202
|
+
client = new AgentWidgetClient({
|
|
203
|
+
apiUrl: 'http://localhost:8000',
|
|
204
|
+
streamParser: createJsonStreamParser
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should stream text incrementally and not show raw JSON at the end', async () => {
|
|
209
|
+
// Simulate the SSE stream from the user's example
|
|
210
|
+
const sseEvents = [
|
|
211
|
+
'data: {"type":"flow_start","flowId":"flow_01k9pfnztzfag9tfz4t65c9c5q","flowName":"Shopping Assistant","totalSteps":1,"startedAt":"2025-11-12T23:47:39.565Z","executionId":"exec_standalone_1762991259266_7wz736k7n","executionContext":{"source":"standalone","record":{"id":"-1","name":"Streaming Chat Widget","created":false},"flow":{"id":"flow_01k9pfnztzfag9tfz4t65c9c5q","name":"Shopping Assistant","created":false}}}',
|
|
212
|
+
'',
|
|
213
|
+
'data: {"type":"step_start","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","stepType":"prompt","index":1,"totalSteps":1,"startedAt":"2025-11-12T23:47:39.565Z"}',
|
|
214
|
+
'',
|
|
215
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"{\\n"}',
|
|
216
|
+
'',
|
|
217
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" "}',
|
|
218
|
+
'',
|
|
219
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" \\""}',
|
|
220
|
+
'',
|
|
221
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"action"}',
|
|
222
|
+
'',
|
|
223
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"\\":"}',
|
|
224
|
+
'',
|
|
225
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" \\""}',
|
|
226
|
+
'',
|
|
227
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"message"}',
|
|
228
|
+
'',
|
|
229
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"\\",\\n"}',
|
|
230
|
+
'',
|
|
231
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" "}',
|
|
232
|
+
'',
|
|
233
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" \\""}',
|
|
234
|
+
'',
|
|
235
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"text"}',
|
|
236
|
+
'',
|
|
237
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"\\":"}',
|
|
238
|
+
'',
|
|
239
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" \\""}',
|
|
240
|
+
'',
|
|
241
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"Great"}',
|
|
242
|
+
'',
|
|
243
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"!"}',
|
|
244
|
+
'',
|
|
245
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" If"}',
|
|
246
|
+
'',
|
|
247
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" you"}',
|
|
248
|
+
'',
|
|
249
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" have"}',
|
|
250
|
+
'',
|
|
251
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" any"}',
|
|
252
|
+
'',
|
|
253
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" questions"}',
|
|
254
|
+
'',
|
|
255
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" or"}',
|
|
256
|
+
'',
|
|
257
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" need"}',
|
|
258
|
+
'',
|
|
259
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" help"}',
|
|
260
|
+
'',
|
|
261
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" finding"}',
|
|
262
|
+
'',
|
|
263
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" something"}',
|
|
264
|
+
'',
|
|
265
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":","}',
|
|
266
|
+
'',
|
|
267
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" just"}',
|
|
268
|
+
'',
|
|
269
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" let"}',
|
|
270
|
+
'',
|
|
271
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" me"}',
|
|
272
|
+
'',
|
|
273
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" know"}',
|
|
274
|
+
'',
|
|
275
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"!\\"\\n"}',
|
|
276
|
+
'',
|
|
277
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"}"}',
|
|
278
|
+
'',
|
|
279
|
+
'data: {"type":"step_complete","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":1,"success":true,"result":{"promptId":"step_01k9x5db72fzwvmdenryn0qm48","promptName":"Prompt 1","processedPrompt":"ok","response":"{\\"\\n \\"action\\": \\"message\\",\\n \\"text\\": \\"Great! If you have any questions or need help finding something, just let me know!\\"\\n}","tokens":{"input":1833,"output":34,"total":1867},"cost":0.000700125,"executionTime":2222,"order":2},"executionTime":2222}',
|
|
280
|
+
'',
|
|
281
|
+
'data: {"type":"flow_complete","flowId":"flow_01k9pfnztzfag9tfz4t65c9c5q","success":true,"duration":2968,"completedAt":"2025-11-12T23:47:42.234Z","totalTokensUsed":0}'
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
// Create a ReadableStream from the SSE events
|
|
285
|
+
const encoder = new TextEncoder();
|
|
286
|
+
const stream = new ReadableStream({
|
|
287
|
+
start(controller) {
|
|
288
|
+
for (const event of sseEvents) {
|
|
289
|
+
controller.enqueue(encoder.encode(event + '\n'));
|
|
290
|
+
}
|
|
291
|
+
controller.close();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Mock fetch to return our stream
|
|
296
|
+
global.fetch = async () => ({
|
|
297
|
+
ok: true,
|
|
298
|
+
body: stream
|
|
299
|
+
}) as any;
|
|
300
|
+
|
|
301
|
+
// Dispatch and collect events
|
|
302
|
+
await client.dispatch(
|
|
303
|
+
{
|
|
304
|
+
messages: [{ role: 'user', content: 'ok' }]
|
|
305
|
+
},
|
|
306
|
+
(event) => {
|
|
307
|
+
events.push(event);
|
|
308
|
+
if (event.type === 'message') {
|
|
309
|
+
console.log('Message event:', {
|
|
310
|
+
content: event.message.content,
|
|
311
|
+
streaming: event.message.streaming,
|
|
312
|
+
contentLength: event.message.content.length
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Filter for assistant message events
|
|
319
|
+
const messageEvents = events.filter(
|
|
320
|
+
(e) => e.type === 'message' && e.message.role === 'assistant'
|
|
321
|
+
) as Extract<AgentWidgetEvent, { type: 'message' }>[];
|
|
322
|
+
|
|
323
|
+
// Validate behavior
|
|
324
|
+
expect(messageEvents.length).toBeGreaterThan(0);
|
|
325
|
+
|
|
326
|
+
// 1. Check that text starts streaming incrementally (not all at once)
|
|
327
|
+
const streamingMessages = messageEvents.filter((e) => e.message.streaming);
|
|
328
|
+
expect(streamingMessages.length).toBeGreaterThan(1);
|
|
329
|
+
console.log(`Found ${streamingMessages.length} streaming message events`);
|
|
330
|
+
|
|
331
|
+
// 2. Check that text content appears progressively
|
|
332
|
+
let hasPartialText = false;
|
|
333
|
+
const expectedFinalText = "Great! If you have any questions or need help finding something, just let me know!";
|
|
334
|
+
|
|
335
|
+
for (const msgEvent of streamingMessages) {
|
|
336
|
+
const content = msgEvent.message.content;
|
|
337
|
+
|
|
338
|
+
// Should not contain raw JSON during streaming
|
|
339
|
+
if (content.includes('"action"') || content.includes('"text"')) {
|
|
340
|
+
console.error('Found raw JSON in streaming content:', content);
|
|
341
|
+
}
|
|
342
|
+
expect(content).not.toMatch(/"action"|"text":/);
|
|
343
|
+
|
|
344
|
+
// Check for partial text (text that's incomplete)
|
|
345
|
+
if (content.length > 0 && content.length < expectedFinalText.length) {
|
|
346
|
+
hasPartialText = true;
|
|
347
|
+
// Partial text should be a prefix of the final text
|
|
348
|
+
expect(expectedFinalText.startsWith(content)).toBe(true);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
expect(hasPartialText).toBe(true);
|
|
353
|
+
console.log('✓ Text streamed incrementally with partial values');
|
|
354
|
+
|
|
355
|
+
// 3. Check final message (streaming: false)
|
|
356
|
+
const finalMessages = messageEvents.filter((e) => !e.message.streaming);
|
|
357
|
+
expect(finalMessages.length).toBeGreaterThan(0);
|
|
358
|
+
|
|
359
|
+
const finalMessage = finalMessages[finalMessages.length - 1].message;
|
|
360
|
+
console.log('Final message content:', finalMessage.content);
|
|
361
|
+
|
|
362
|
+
// Final content should be ONLY the extracted text, not raw JSON
|
|
363
|
+
expect(finalMessage.content).toBe(expectedFinalText);
|
|
364
|
+
expect(finalMessage.content).not.toContain('"action"');
|
|
365
|
+
expect(finalMessage.content).not.toContain('"text"');
|
|
366
|
+
expect(finalMessage.content).not.toContain('{\n');
|
|
367
|
+
|
|
368
|
+
console.log('✓ Final message contains only extracted text, no raw JSON');
|
|
369
|
+
|
|
370
|
+
// 4. Verify no raw JSON was ever displayed
|
|
371
|
+
const allContents = messageEvents.map((e) => e.message.content);
|
|
372
|
+
const hasRawJson = allContents.some(
|
|
373
|
+
(content) => content.includes('{\n "action": "message"')
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (hasRawJson) {
|
|
377
|
+
const rawJsonMessage = allContents.find((content) =>
|
|
378
|
+
content.includes('{\n "action": "message"')
|
|
379
|
+
);
|
|
380
|
+
console.error('Found raw JSON in message content:', rawJsonMessage);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
expect(hasRawJson).toBe(false);
|
|
384
|
+
console.log('✓ No raw JSON was displayed at any point');
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|