vanilla-agent 0.1.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 +310 -0
- package/dist/index.cjs +14 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +370 -0
- package/dist/index.d.ts +370 -0
- package/dist/index.global.js +1710 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +14 -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 +752 -0
- package/package.json +61 -0
- package/src/client.ts +577 -0
- package/src/components/forms.ts +165 -0
- package/src/components/launcher.ts +184 -0
- package/src/components/message-bubble.ts +52 -0
- package/src/components/messages.ts +43 -0
- package/src/components/panel.ts +555 -0
- package/src/components/reasoning-bubble.ts +114 -0
- package/src/components/suggestions.ts +52 -0
- package/src/components/tool-bubble.ts +158 -0
- package/src/index.ts +37 -0
- package/src/install.ts +159 -0
- package/src/plugins/registry.ts +72 -0
- package/src/plugins/types.ts +90 -0
- package/src/postprocessors.ts +76 -0
- package/src/runtime/init.ts +116 -0
- package/src/session.ts +206 -0
- package/src/styles/tailwind.css +19 -0
- package/src/styles/widget.css +752 -0
- package/src/types.ts +194 -0
- package/src/ui.ts +1325 -0
- package/src/utils/constants.ts +11 -0
- package/src/utils/dom.ts +20 -0
- package/src/utils/formatting.ts +77 -0
- package/src/utils/icons.ts +92 -0
- package/src/utils/positioning.ts +12 -0
- package/src/utils/theme.ts +20 -0
- package/src/widget.css +1 -0
- package/widget.css +1 -0
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vanilla-agent",
|
|
3
|
+
"version": "0.1.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
|
+
"lucide": "^0.552.0",
|
|
27
|
+
"marked": "^12.0.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.12.7",
|
|
31
|
+
"eslint": "^8.57.0",
|
|
32
|
+
"eslint-config-prettier": "^9.1.0",
|
|
33
|
+
"postcss": "^8.4.38",
|
|
34
|
+
"rimraf": "^5.0.5",
|
|
35
|
+
"tailwindcss": "^3.4.10",
|
|
36
|
+
"tsup": "^8.0.1",
|
|
37
|
+
"typescript": "^5.4.5"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.17.0"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"keywords": [
|
|
44
|
+
"chat",
|
|
45
|
+
"widget",
|
|
46
|
+
"streaming",
|
|
47
|
+
"typescript"
|
|
48
|
+
],
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/becomevocal/chaty.git"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "rimraf dist && npm run build:styles && npm run build:client && npm run build:installer",
|
|
55
|
+
"build:styles": "node -e \"const fs=require('fs');fs.mkdirSync('dist',{recursive:true});fs.copyFileSync('src/styles/widget.css','dist/widget.css');\"",
|
|
56
|
+
"build:client": "tsup src/index.ts --format esm,cjs,iife --global-name ChatWidget --minify --sourcemap --splitting false --dts --loader \".css=text\"",
|
|
57
|
+
"build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
|
|
58
|
+
"lint": "eslint . --ext .ts",
|
|
59
|
+
"typecheck": "tsc --noEmit"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { ChatWidgetConfig, ChatWidgetMessage, ChatWidgetEvent } from "./types";
|
|
2
|
+
|
|
3
|
+
type DispatchOptions = {
|
|
4
|
+
messages: ChatWidgetMessage[];
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type SSEHandler = (event: ChatWidgetEvent) => void;
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ENDPOINT = "https://api.travrse.ai/v1/dispatch";
|
|
11
|
+
|
|
12
|
+
export class ChatWidgetClient {
|
|
13
|
+
private readonly apiUrl: string;
|
|
14
|
+
private readonly headers: Record<string, string>;
|
|
15
|
+
private readonly debug: boolean;
|
|
16
|
+
|
|
17
|
+
constructor(private config: ChatWidgetConfig = {}) {
|
|
18
|
+
this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
|
|
19
|
+
this.headers = {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
...config.headers
|
|
22
|
+
};
|
|
23
|
+
this.debug = Boolean(config.debug);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
if (options.signal) {
|
|
29
|
+
options.signal.addEventListener("abort", () => controller.abort());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onEvent({ type: "status", status: "connecting" });
|
|
33
|
+
|
|
34
|
+
// Build simplified payload with just messages and optional flowId
|
|
35
|
+
// Sort by createdAt to ensure chronological order (not local sequence)
|
|
36
|
+
const body = {
|
|
37
|
+
messages: options.messages
|
|
38
|
+
.slice()
|
|
39
|
+
.sort((a, b) => {
|
|
40
|
+
const timeA = new Date(a.createdAt).getTime();
|
|
41
|
+
const timeB = new Date(b.createdAt).getTime();
|
|
42
|
+
return timeA - timeB;
|
|
43
|
+
})
|
|
44
|
+
.map((message) => ({
|
|
45
|
+
role: message.role,
|
|
46
|
+
content: message.content,
|
|
47
|
+
createdAt: message.createdAt
|
|
48
|
+
})),
|
|
49
|
+
...(this.config.flowId && { flowId: this.config.flowId })
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (this.debug) {
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.debug("[ChatWidgetClient] dispatch body", body);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const response = await fetch(this.apiUrl, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: this.headers,
|
|
60
|
+
body: JSON.stringify(body),
|
|
61
|
+
signal: controller.signal
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!response.ok || !response.body) {
|
|
65
|
+
const error = new Error(
|
|
66
|
+
`Chat backend request failed: ${response.status} ${response.statusText}`
|
|
67
|
+
);
|
|
68
|
+
onEvent({ type: "error", error });
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onEvent({ type: "status", status: "connected" });
|
|
73
|
+
try {
|
|
74
|
+
await this.streamResponse(response.body, onEvent);
|
|
75
|
+
} finally {
|
|
76
|
+
onEvent({ type: "status", status: "idle" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async streamResponse(
|
|
81
|
+
body: ReadableStream<Uint8Array>,
|
|
82
|
+
onEvent: SSEHandler
|
|
83
|
+
) {
|
|
84
|
+
const reader = body.getReader();
|
|
85
|
+
const decoder = new TextDecoder();
|
|
86
|
+
let buffer = "";
|
|
87
|
+
|
|
88
|
+
const baseSequence = Date.now();
|
|
89
|
+
let sequenceCounter = 0;
|
|
90
|
+
const nextSequence = () => baseSequence + sequenceCounter++;
|
|
91
|
+
|
|
92
|
+
const cloneMessage = (msg: ChatWidgetMessage): ChatWidgetMessage => {
|
|
93
|
+
const reasoning = msg.reasoning
|
|
94
|
+
? {
|
|
95
|
+
...msg.reasoning,
|
|
96
|
+
chunks: [...msg.reasoning.chunks]
|
|
97
|
+
}
|
|
98
|
+
: undefined;
|
|
99
|
+
const toolCall = msg.toolCall
|
|
100
|
+
? {
|
|
101
|
+
...msg.toolCall,
|
|
102
|
+
chunks: msg.toolCall.chunks ? [...msg.toolCall.chunks] : undefined
|
|
103
|
+
}
|
|
104
|
+
: undefined;
|
|
105
|
+
const tools = msg.tools
|
|
106
|
+
? msg.tools.map((tool) => ({
|
|
107
|
+
...tool,
|
|
108
|
+
chunks: tool.chunks ? [...tool.chunks] : undefined
|
|
109
|
+
}))
|
|
110
|
+
: undefined;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
...msg,
|
|
114
|
+
reasoning,
|
|
115
|
+
toolCall,
|
|
116
|
+
tools
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const emitMessage = (msg: ChatWidgetMessage) => {
|
|
121
|
+
onEvent({
|
|
122
|
+
type: "message",
|
|
123
|
+
message: cloneMessage(msg)
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
let assistantMessage: ChatWidgetMessage | null = null;
|
|
128
|
+
const reasoningMessages = new Map<string, ChatWidgetMessage>();
|
|
129
|
+
const toolMessages = new Map<string, ChatWidgetMessage>();
|
|
130
|
+
const reasoningContext = {
|
|
131
|
+
lastId: null as string | null,
|
|
132
|
+
byStep: new Map<string, string>()
|
|
133
|
+
};
|
|
134
|
+
const toolContext = {
|
|
135
|
+
lastId: null as string | null,
|
|
136
|
+
byCall: new Map<string, string>()
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const normalizeKey = (value: unknown): string | null => {
|
|
140
|
+
if (value === null || value === undefined) return null;
|
|
141
|
+
try {
|
|
142
|
+
return String(value);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const getStepKey = (payload: Record<string, any>) =>
|
|
149
|
+
normalizeKey(
|
|
150
|
+
payload.stepId ??
|
|
151
|
+
payload.step_id ??
|
|
152
|
+
payload.step ??
|
|
153
|
+
payload.parentId ??
|
|
154
|
+
payload.flowStepId ??
|
|
155
|
+
payload.flow_step_id
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const getToolCallKey = (payload: Record<string, any>) =>
|
|
159
|
+
normalizeKey(
|
|
160
|
+
payload.callId ??
|
|
161
|
+
payload.call_id ??
|
|
162
|
+
payload.requestId ??
|
|
163
|
+
payload.request_id ??
|
|
164
|
+
payload.toolCallId ??
|
|
165
|
+
payload.tool_call_id ??
|
|
166
|
+
payload.stepId ??
|
|
167
|
+
payload.step_id
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const ensureAssistantMessage = () => {
|
|
171
|
+
if (assistantMessage) return assistantMessage;
|
|
172
|
+
assistantMessage = {
|
|
173
|
+
id: `assistant-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
174
|
+
role: "assistant",
|
|
175
|
+
content: "",
|
|
176
|
+
createdAt: new Date().toISOString(),
|
|
177
|
+
streaming: true,
|
|
178
|
+
variant: "assistant",
|
|
179
|
+
sequence: nextSequence()
|
|
180
|
+
};
|
|
181
|
+
emitMessage(assistantMessage);
|
|
182
|
+
return assistantMessage;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const trackReasoningId = (stepKey: string | null, id: string) => {
|
|
186
|
+
reasoningContext.lastId = id;
|
|
187
|
+
if (stepKey) {
|
|
188
|
+
reasoningContext.byStep.set(stepKey, id);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const resolveReasoningId = (
|
|
193
|
+
payload: Record<string, any>,
|
|
194
|
+
allowCreate: boolean
|
|
195
|
+
): string | null => {
|
|
196
|
+
const rawId = payload.reasoningId ?? payload.id;
|
|
197
|
+
const stepKey = getStepKey(payload);
|
|
198
|
+
if (rawId) {
|
|
199
|
+
const resolved = String(rawId);
|
|
200
|
+
trackReasoningId(stepKey, resolved);
|
|
201
|
+
return resolved;
|
|
202
|
+
}
|
|
203
|
+
if (stepKey) {
|
|
204
|
+
const existing = reasoningContext.byStep.get(stepKey);
|
|
205
|
+
if (existing) {
|
|
206
|
+
reasoningContext.lastId = existing;
|
|
207
|
+
return existing;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (reasoningContext.lastId && !allowCreate) {
|
|
211
|
+
return reasoningContext.lastId;
|
|
212
|
+
}
|
|
213
|
+
if (!allowCreate) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const generated = `reason-${nextSequence()}`;
|
|
217
|
+
trackReasoningId(stepKey, generated);
|
|
218
|
+
return generated;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const ensureReasoningMessage = (reasoningId: string) => {
|
|
222
|
+
const existing = reasoningMessages.get(reasoningId);
|
|
223
|
+
if (existing) {
|
|
224
|
+
return existing;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const message: ChatWidgetMessage = {
|
|
228
|
+
id: `reason-${reasoningId}`,
|
|
229
|
+
role: "assistant",
|
|
230
|
+
content: "",
|
|
231
|
+
createdAt: new Date().toISOString(),
|
|
232
|
+
streaming: true,
|
|
233
|
+
variant: "reasoning",
|
|
234
|
+
sequence: nextSequence(),
|
|
235
|
+
reasoning: {
|
|
236
|
+
id: reasoningId,
|
|
237
|
+
status: "streaming",
|
|
238
|
+
chunks: []
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
reasoningMessages.set(reasoningId, message);
|
|
243
|
+
emitMessage(message);
|
|
244
|
+
return message;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const trackToolId = (callKey: string | null, id: string) => {
|
|
248
|
+
toolContext.lastId = id;
|
|
249
|
+
if (callKey) {
|
|
250
|
+
toolContext.byCall.set(callKey, id);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const resolveToolId = (
|
|
255
|
+
payload: Record<string, any>,
|
|
256
|
+
allowCreate: boolean
|
|
257
|
+
): string | null => {
|
|
258
|
+
const rawId = payload.toolId ?? payload.id;
|
|
259
|
+
const callKey = getToolCallKey(payload);
|
|
260
|
+
if (rawId) {
|
|
261
|
+
const resolved = String(rawId);
|
|
262
|
+
trackToolId(callKey, resolved);
|
|
263
|
+
return resolved;
|
|
264
|
+
}
|
|
265
|
+
if (callKey) {
|
|
266
|
+
const existing = toolContext.byCall.get(callKey);
|
|
267
|
+
if (existing) {
|
|
268
|
+
toolContext.lastId = existing;
|
|
269
|
+
return existing;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (toolContext.lastId && !allowCreate) {
|
|
273
|
+
return toolContext.lastId;
|
|
274
|
+
}
|
|
275
|
+
if (!allowCreate) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const generated = `tool-${nextSequence()}`;
|
|
279
|
+
trackToolId(callKey, generated);
|
|
280
|
+
return generated;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const ensureToolMessage = (toolId: string) => {
|
|
284
|
+
const existing = toolMessages.get(toolId);
|
|
285
|
+
if (existing) {
|
|
286
|
+
return existing;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const message: ChatWidgetMessage = {
|
|
290
|
+
id: `tool-${toolId}`,
|
|
291
|
+
role: "assistant",
|
|
292
|
+
content: "",
|
|
293
|
+
createdAt: new Date().toISOString(),
|
|
294
|
+
streaming: true,
|
|
295
|
+
variant: "tool",
|
|
296
|
+
sequence: nextSequence(),
|
|
297
|
+
toolCall: {
|
|
298
|
+
id: toolId,
|
|
299
|
+
status: "pending"
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
toolMessages.set(toolId, message);
|
|
304
|
+
emitMessage(message);
|
|
305
|
+
return message;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const resolveTimestamp = (value: unknown) => {
|
|
309
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
if (typeof value === "string") {
|
|
313
|
+
const parsed = Number(value);
|
|
314
|
+
if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {
|
|
315
|
+
return parsed;
|
|
316
|
+
}
|
|
317
|
+
const dateParsed = Date.parse(value);
|
|
318
|
+
if (!Number.isNaN(dateParsed)) {
|
|
319
|
+
return dateParsed;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return Date.now();
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
while (true) {
|
|
326
|
+
const { done, value } = await reader.read();
|
|
327
|
+
if (done) break;
|
|
328
|
+
|
|
329
|
+
buffer += decoder.decode(value, { stream: true });
|
|
330
|
+
const events = buffer.split("\n\n");
|
|
331
|
+
buffer = events.pop() ?? "";
|
|
332
|
+
|
|
333
|
+
for (const event of events) {
|
|
334
|
+
const lines = event.split("\n");
|
|
335
|
+
let eventType = "message";
|
|
336
|
+
let data = "";
|
|
337
|
+
|
|
338
|
+
for (const line of lines) {
|
|
339
|
+
if (line.startsWith("event:")) {
|
|
340
|
+
eventType = line.replace("event:", "").trim();
|
|
341
|
+
} else if (line.startsWith("data:")) {
|
|
342
|
+
data += line.replace("data:", "").trim();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!data) continue;
|
|
347
|
+
let payload: any;
|
|
348
|
+
try {
|
|
349
|
+
payload = JSON.parse(data);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
onEvent({
|
|
352
|
+
type: "error",
|
|
353
|
+
error:
|
|
354
|
+
error instanceof Error
|
|
355
|
+
? error
|
|
356
|
+
: new Error("Failed to parse chat stream payload")
|
|
357
|
+
});
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const payloadType =
|
|
362
|
+
eventType !== "message" ? eventType : payload.type ?? "message";
|
|
363
|
+
|
|
364
|
+
if (payloadType === "reason_start") {
|
|
365
|
+
const reasoningId =
|
|
366
|
+
resolveReasoningId(payload, true) ?? `reason-${nextSequence()}`;
|
|
367
|
+
const reasoningMessage = ensureReasoningMessage(reasoningId);
|
|
368
|
+
reasoningMessage.reasoning = reasoningMessage.reasoning ?? {
|
|
369
|
+
id: reasoningId,
|
|
370
|
+
status: "streaming",
|
|
371
|
+
chunks: []
|
|
372
|
+
};
|
|
373
|
+
reasoningMessage.reasoning.startedAt =
|
|
374
|
+
reasoningMessage.reasoning.startedAt ??
|
|
375
|
+
resolveTimestamp(payload.startedAt ?? payload.timestamp);
|
|
376
|
+
reasoningMessage.reasoning.completedAt = undefined;
|
|
377
|
+
reasoningMessage.reasoning.durationMs = undefined;
|
|
378
|
+
reasoningMessage.streaming = true;
|
|
379
|
+
reasoningMessage.reasoning.status = "streaming";
|
|
380
|
+
emitMessage(reasoningMessage);
|
|
381
|
+
} else if (payloadType === "reason_chunk") {
|
|
382
|
+
const reasoningId =
|
|
383
|
+
resolveReasoningId(payload, false) ??
|
|
384
|
+
resolveReasoningId(payload, true) ??
|
|
385
|
+
`reason-${nextSequence()}`;
|
|
386
|
+
const reasoningMessage = ensureReasoningMessage(reasoningId);
|
|
387
|
+
reasoningMessage.reasoning = reasoningMessage.reasoning ?? {
|
|
388
|
+
id: reasoningId,
|
|
389
|
+
status: "streaming",
|
|
390
|
+
chunks: []
|
|
391
|
+
};
|
|
392
|
+
reasoningMessage.reasoning.startedAt =
|
|
393
|
+
reasoningMessage.reasoning.startedAt ??
|
|
394
|
+
resolveTimestamp(payload.startedAt ?? payload.timestamp);
|
|
395
|
+
const chunk =
|
|
396
|
+
payload.reasoningText ??
|
|
397
|
+
payload.text ??
|
|
398
|
+
payload.delta ??
|
|
399
|
+
"";
|
|
400
|
+
if (chunk && payload.hidden !== true) {
|
|
401
|
+
reasoningMessage.reasoning.chunks.push(String(chunk));
|
|
402
|
+
}
|
|
403
|
+
reasoningMessage.reasoning.status = payload.done ? "complete" : "streaming";
|
|
404
|
+
if (payload.done) {
|
|
405
|
+
reasoningMessage.reasoning.completedAt = resolveTimestamp(
|
|
406
|
+
payload.completedAt ?? payload.timestamp
|
|
407
|
+
);
|
|
408
|
+
const start = reasoningMessage.reasoning.startedAt ?? Date.now();
|
|
409
|
+
reasoningMessage.reasoning.durationMs = Math.max(
|
|
410
|
+
0,
|
|
411
|
+
(reasoningMessage.reasoning.completedAt ?? Date.now()) - start
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
reasoningMessage.streaming = reasoningMessage.reasoning.status !== "complete";
|
|
415
|
+
emitMessage(reasoningMessage);
|
|
416
|
+
} else if (payloadType === "reason_complete") {
|
|
417
|
+
const reasoningId =
|
|
418
|
+
resolveReasoningId(payload, false) ??
|
|
419
|
+
resolveReasoningId(payload, true) ??
|
|
420
|
+
`reason-${nextSequence()}`;
|
|
421
|
+
const reasoningMessage = reasoningMessages.get(reasoningId);
|
|
422
|
+
if (reasoningMessage?.reasoning) {
|
|
423
|
+
reasoningMessage.reasoning.status = "complete";
|
|
424
|
+
reasoningMessage.reasoning.completedAt = resolveTimestamp(
|
|
425
|
+
payload.completedAt ?? payload.timestamp
|
|
426
|
+
);
|
|
427
|
+
const start = reasoningMessage.reasoning.startedAt ?? Date.now();
|
|
428
|
+
reasoningMessage.reasoning.durationMs = Math.max(
|
|
429
|
+
0,
|
|
430
|
+
(reasoningMessage.reasoning.completedAt ?? Date.now()) - start
|
|
431
|
+
);
|
|
432
|
+
reasoningMessage.streaming = false;
|
|
433
|
+
emitMessage(reasoningMessage);
|
|
434
|
+
}
|
|
435
|
+
const stepKey = getStepKey(payload);
|
|
436
|
+
if (stepKey) {
|
|
437
|
+
reasoningContext.byStep.delete(stepKey);
|
|
438
|
+
}
|
|
439
|
+
} else if (payloadType === "tool_start") {
|
|
440
|
+
const toolId =
|
|
441
|
+
resolveToolId(payload, true) ?? `tool-${nextSequence()}`;
|
|
442
|
+
const toolMessage = ensureToolMessage(toolId);
|
|
443
|
+
const tool = toolMessage.toolCall ?? {
|
|
444
|
+
id: toolId,
|
|
445
|
+
status: "pending"
|
|
446
|
+
};
|
|
447
|
+
tool.name = payload.toolName ?? tool.name;
|
|
448
|
+
tool.status = "running";
|
|
449
|
+
if (payload.args !== undefined) {
|
|
450
|
+
tool.args = payload.args;
|
|
451
|
+
}
|
|
452
|
+
tool.startedAt =
|
|
453
|
+
tool.startedAt ??
|
|
454
|
+
resolveTimestamp(payload.startedAt ?? payload.timestamp);
|
|
455
|
+
tool.completedAt = undefined;
|
|
456
|
+
tool.durationMs = undefined;
|
|
457
|
+
toolMessage.toolCall = tool;
|
|
458
|
+
toolMessage.streaming = true;
|
|
459
|
+
emitMessage(toolMessage);
|
|
460
|
+
} else if (payloadType === "tool_chunk") {
|
|
461
|
+
const toolId =
|
|
462
|
+
resolveToolId(payload, false) ??
|
|
463
|
+
resolveToolId(payload, true) ??
|
|
464
|
+
`tool-${nextSequence()}`;
|
|
465
|
+
const toolMessage = ensureToolMessage(toolId);
|
|
466
|
+
const tool = toolMessage.toolCall ?? {
|
|
467
|
+
id: toolId,
|
|
468
|
+
status: "running"
|
|
469
|
+
};
|
|
470
|
+
tool.startedAt =
|
|
471
|
+
tool.startedAt ??
|
|
472
|
+
resolveTimestamp(payload.startedAt ?? payload.timestamp);
|
|
473
|
+
const chunkText =
|
|
474
|
+
payload.text ?? payload.delta ?? payload.message ?? "";
|
|
475
|
+
if (chunkText) {
|
|
476
|
+
tool.chunks = tool.chunks ?? [];
|
|
477
|
+
tool.chunks.push(String(chunkText));
|
|
478
|
+
}
|
|
479
|
+
tool.status = "running";
|
|
480
|
+
toolMessage.toolCall = tool;
|
|
481
|
+
toolMessage.streaming = true;
|
|
482
|
+
emitMessage(toolMessage);
|
|
483
|
+
} else if (payloadType === "tool_complete") {
|
|
484
|
+
const toolId =
|
|
485
|
+
resolveToolId(payload, false) ??
|
|
486
|
+
resolveToolId(payload, true) ??
|
|
487
|
+
`tool-${nextSequence()}`;
|
|
488
|
+
const toolMessage = ensureToolMessage(toolId);
|
|
489
|
+
const tool = toolMessage.toolCall ?? {
|
|
490
|
+
id: toolId,
|
|
491
|
+
status: "running"
|
|
492
|
+
};
|
|
493
|
+
tool.status = "complete";
|
|
494
|
+
if (payload.result !== undefined) {
|
|
495
|
+
tool.result = payload.result;
|
|
496
|
+
}
|
|
497
|
+
if (typeof payload.duration === "number") {
|
|
498
|
+
tool.duration = payload.duration;
|
|
499
|
+
}
|
|
500
|
+
tool.completedAt = resolveTimestamp(
|
|
501
|
+
payload.completedAt ?? payload.timestamp
|
|
502
|
+
);
|
|
503
|
+
if (typeof payload.duration === "number") {
|
|
504
|
+
tool.durationMs = payload.duration;
|
|
505
|
+
} else {
|
|
506
|
+
const start = tool.startedAt ?? Date.now();
|
|
507
|
+
tool.durationMs = Math.max(
|
|
508
|
+
0,
|
|
509
|
+
(tool.completedAt ?? Date.now()) - start
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
toolMessage.toolCall = tool;
|
|
513
|
+
toolMessage.streaming = false;
|
|
514
|
+
emitMessage(toolMessage);
|
|
515
|
+
const callKey = getToolCallKey(payload);
|
|
516
|
+
if (callKey) {
|
|
517
|
+
toolContext.byCall.delete(callKey);
|
|
518
|
+
}
|
|
519
|
+
} else if (payloadType === "step_chunk") {
|
|
520
|
+
const assistant = ensureAssistantMessage();
|
|
521
|
+
const chunk = payload.text ?? payload.delta ?? payload.content ?? "";
|
|
522
|
+
if (chunk) {
|
|
523
|
+
assistant.content += chunk;
|
|
524
|
+
emitMessage(assistant);
|
|
525
|
+
}
|
|
526
|
+
if (payload.isComplete) {
|
|
527
|
+
const finalContent = payload.result?.response ?? assistant.content;
|
|
528
|
+
if (finalContent) {
|
|
529
|
+
assistant.content = finalContent;
|
|
530
|
+
assistant.streaming = false;
|
|
531
|
+
emitMessage(assistant);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
} else if (payloadType === "step_complete") {
|
|
535
|
+
const finalContent = payload.result?.response;
|
|
536
|
+
const assistant = ensureAssistantMessage();
|
|
537
|
+
if (finalContent) {
|
|
538
|
+
assistant.content = finalContent;
|
|
539
|
+
assistant.streaming = false;
|
|
540
|
+
emitMessage(assistant);
|
|
541
|
+
} else {
|
|
542
|
+
// No final content, just mark as complete
|
|
543
|
+
assistant.streaming = false;
|
|
544
|
+
emitMessage(assistant);
|
|
545
|
+
}
|
|
546
|
+
} else if (payloadType === "flow_complete") {
|
|
547
|
+
const finalContent = payload.result?.response;
|
|
548
|
+
if (finalContent) {
|
|
549
|
+
const assistant = ensureAssistantMessage();
|
|
550
|
+
if (finalContent !== assistant.content) {
|
|
551
|
+
assistant.content = finalContent;
|
|
552
|
+
emitMessage(assistant);
|
|
553
|
+
}
|
|
554
|
+
assistant.streaming = false;
|
|
555
|
+
emitMessage(assistant);
|
|
556
|
+
} else {
|
|
557
|
+
const existingAssistant = assistantMessage;
|
|
558
|
+
if (existingAssistant) {
|
|
559
|
+
const assistantFinal = existingAssistant as ChatWidgetMessage;
|
|
560
|
+
assistantFinal.streaming = false;
|
|
561
|
+
emitMessage(assistantFinal);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
onEvent({ type: "status", status: "idle" });
|
|
565
|
+
} else if (payloadType === "error" && payload.error) {
|
|
566
|
+
onEvent({
|
|
567
|
+
type: "error",
|
|
568
|
+
error:
|
|
569
|
+
payload.error instanceof Error
|
|
570
|
+
? payload.error
|
|
571
|
+
: new Error(String(payload.error))
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|