@standardagents/react 0.10.1-next.bbd142a → 0.11.0-next.ab7e1ea
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/LICENSE.txt +48 -0
- package/README.md +191 -312
- package/dist/index.d.ts +83 -170
- package/dist/index.js +354 -398
- package/dist/index.js.map +1 -1
- package/package.json +14 -11
package/dist/index.js
CHANGED
|
@@ -1,183 +1,10 @@
|
|
|
1
1
|
import { createContext, useMemo, useEffect, useRef, useState, useCallback, useContext } from 'react';
|
|
2
|
+
import { FileUploadManager, AgentBuilderClient, messagesToFiles, transformToWorkblocks, ThreadConnectionManager } from '@standardagents/client';
|
|
3
|
+
export { AgentBuilderClient, FileUploadManager, ThreadConnectionManager, generatePendingFileId, isImageMimeType, messagesToFiles, parseAttachments, readFileAsDataUrl, transformToWorkblocks } from '@standardagents/client';
|
|
2
4
|
import { jsx } from 'react/jsx-runtime';
|
|
3
5
|
|
|
4
6
|
// src/context/AgentBuilderProvider.tsx
|
|
5
7
|
|
|
6
|
-
// src/services/client.ts
|
|
7
|
-
var AgentBuilderClient = class {
|
|
8
|
-
endpoint;
|
|
9
|
-
token;
|
|
10
|
-
constructor(endpoint) {
|
|
11
|
-
this.endpoint = endpoint.replace(/\/$/, "");
|
|
12
|
-
this.token = typeof localStorage !== "undefined" ? localStorage.getItem("agentbuilder_auth_token") : null;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Get thread metadata
|
|
16
|
-
*/
|
|
17
|
-
async getThread(id) {
|
|
18
|
-
const response = await fetch(`${this.endpoint}/threads/${id}`, {
|
|
19
|
-
method: "GET",
|
|
20
|
-
headers: this.getHeaders()
|
|
21
|
-
});
|
|
22
|
-
if (!response.ok) {
|
|
23
|
-
throw new Error(`Failed to get thread: ${response.statusText}`);
|
|
24
|
-
}
|
|
25
|
-
return response.json();
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Get messages from a thread with optional pagination and filtering
|
|
29
|
-
*/
|
|
30
|
-
async getMessages(id, options = {}) {
|
|
31
|
-
const params = new URLSearchParams();
|
|
32
|
-
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
33
|
-
if (options.offset !== void 0) params.set("offset", String(options.offset));
|
|
34
|
-
if (options.depth !== void 0) params.set("depth", String(options.depth));
|
|
35
|
-
if (options.includeSilent !== void 0) params.set("includeSilent", String(options.includeSilent));
|
|
36
|
-
const queryString = params.toString();
|
|
37
|
-
const url = `${this.endpoint}/threads/${id}/messages${queryString ? `?${queryString}` : ""}`;
|
|
38
|
-
const response = await fetch(url, {
|
|
39
|
-
method: "GET",
|
|
40
|
-
headers: this.getHeaders()
|
|
41
|
-
});
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
throw new Error(`Failed to get messages: ${response.statusText}`);
|
|
44
|
-
}
|
|
45
|
-
const data = await response.json();
|
|
46
|
-
return data.messages || [];
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Send a message to a thread
|
|
50
|
-
*/
|
|
51
|
-
async sendMessage(id, payload) {
|
|
52
|
-
const response = await fetch(`${this.endpoint}/threads/${id}/message`, {
|
|
53
|
-
method: "POST",
|
|
54
|
-
headers: {
|
|
55
|
-
...this.getHeaders(),
|
|
56
|
-
"Content-Type": "application/json"
|
|
57
|
-
},
|
|
58
|
-
body: JSON.stringify(payload)
|
|
59
|
-
});
|
|
60
|
-
if (!response.ok) {
|
|
61
|
-
throw new Error(`Failed to send message: ${response.statusText}`);
|
|
62
|
-
}
|
|
63
|
-
return response.json();
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Stop execution of a thread
|
|
67
|
-
*/
|
|
68
|
-
async stopExecution(id) {
|
|
69
|
-
const response = await fetch(`${this.endpoint}/threads/${id}/stop`, {
|
|
70
|
-
method: "POST",
|
|
71
|
-
headers: this.getHeaders()
|
|
72
|
-
});
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
throw new Error(`Failed to stop execution: ${response.statusText}`);
|
|
75
|
-
}
|
|
76
|
-
await response.json();
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Connect to message WebSocket for real-time message updates
|
|
80
|
-
*/
|
|
81
|
-
connectMessageWebSocket(id, callbacks = {}, options = {}) {
|
|
82
|
-
const params = new URLSearchParams();
|
|
83
|
-
if (this.token) params.set("token", this.token);
|
|
84
|
-
if (options.includeSilent !== void 0) params.set("includeSilent", String(options.includeSilent));
|
|
85
|
-
if (options.depth !== void 0) params.set("depth", String(options.depth));
|
|
86
|
-
const wsProtocol = this.endpoint.startsWith("https") ? "wss" : "ws";
|
|
87
|
-
const wsEndpoint = this.endpoint.replace(/^https?/, wsProtocol);
|
|
88
|
-
const url = `${wsEndpoint}/threads/${id}/stream?${params.toString()}`;
|
|
89
|
-
const ws = new WebSocket(url);
|
|
90
|
-
ws.onopen = () => {
|
|
91
|
-
callbacks.onOpen?.();
|
|
92
|
-
};
|
|
93
|
-
ws.onmessage = (event) => {
|
|
94
|
-
try {
|
|
95
|
-
if (typeof event.data === "string" && event.data === "pong") {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const data = JSON.parse(event.data);
|
|
99
|
-
switch (data.type) {
|
|
100
|
-
case "message_data":
|
|
101
|
-
callbacks.onMessage?.(data);
|
|
102
|
-
break;
|
|
103
|
-
case "message_chunk":
|
|
104
|
-
callbacks.onChunk?.(data);
|
|
105
|
-
break;
|
|
106
|
-
case "event":
|
|
107
|
-
callbacks.onEvent?.(data);
|
|
108
|
-
break;
|
|
109
|
-
case "error":
|
|
110
|
-
callbacks.onError?.(data);
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.error("Failed to parse WebSocket message:", error);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
ws.onerror = (event) => {
|
|
118
|
-
console.error("WebSocket error:", event);
|
|
119
|
-
callbacks.onError?.({ type: "error", error: "WebSocket connection error" });
|
|
120
|
-
};
|
|
121
|
-
ws.onclose = (event) => {
|
|
122
|
-
console.log(`[AgentBuilderClient] Message WebSocket closed - code: ${event.code}, reason: ${event.reason || "none"}, wasClean: ${event.wasClean}`);
|
|
123
|
-
callbacks.onClose?.();
|
|
124
|
-
};
|
|
125
|
-
return ws;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Connect to log WebSocket for custom events
|
|
129
|
-
*/
|
|
130
|
-
connectLogWebSocket(id, callbacks = {}) {
|
|
131
|
-
const params = new URLSearchParams();
|
|
132
|
-
if (this.token) params.set("token", this.token);
|
|
133
|
-
const wsProtocol = this.endpoint.startsWith("https") ? "wss" : "ws";
|
|
134
|
-
const wsEndpoint = this.endpoint.replace(/^https?/, wsProtocol);
|
|
135
|
-
const url = `${wsEndpoint}/threads/${id}?${params.toString()}`;
|
|
136
|
-
const ws = new WebSocket(url);
|
|
137
|
-
ws.onopen = () => {
|
|
138
|
-
callbacks.onOpen?.();
|
|
139
|
-
};
|
|
140
|
-
ws.onmessage = (event) => {
|
|
141
|
-
try {
|
|
142
|
-
if (typeof event.data === "string" && event.data === "pong") {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
const data = JSON.parse(event.data);
|
|
146
|
-
switch (data.type) {
|
|
147
|
-
case "log_data":
|
|
148
|
-
callbacks.onLog?.(data);
|
|
149
|
-
break;
|
|
150
|
-
case "custom":
|
|
151
|
-
callbacks.onCustom?.(data);
|
|
152
|
-
break;
|
|
153
|
-
case "stopped_by_user":
|
|
154
|
-
callbacks.onStopped?.(data);
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
} catch (error) {
|
|
158
|
-
console.error("Failed to parse WebSocket message:", error);
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
ws.onerror = (event) => {
|
|
162
|
-
console.error("WebSocket error:", event);
|
|
163
|
-
};
|
|
164
|
-
ws.onclose = () => {
|
|
165
|
-
callbacks.onClose?.();
|
|
166
|
-
};
|
|
167
|
-
return ws;
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Get headers for HTTP requests
|
|
171
|
-
*/
|
|
172
|
-
getHeaders() {
|
|
173
|
-
const headers = {};
|
|
174
|
-
if (this.token) {
|
|
175
|
-
headers["Authorization"] = `Bearer ${this.token}`;
|
|
176
|
-
}
|
|
177
|
-
return headers;
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
8
|
// src/services/sendMessage.ts
|
|
182
9
|
var globalEndpoint = null;
|
|
183
10
|
function __setGlobalEndpoint(endpoint) {
|
|
@@ -196,7 +23,7 @@ async function sendMessage(id, payload) {
|
|
|
196
23
|
if (token) {
|
|
197
24
|
headers["Authorization"] = `Bearer ${token}`;
|
|
198
25
|
}
|
|
199
|
-
const response = await fetch(`${globalEndpoint}/threads/${id}/
|
|
26
|
+
const response = await fetch(`${globalEndpoint}/threads/${id}/messages`, {
|
|
200
27
|
method: "POST",
|
|
201
28
|
headers,
|
|
202
29
|
body: JSON.stringify(payload)
|
|
@@ -266,12 +93,19 @@ function useAgentBuilderConfig() {
|
|
|
266
93
|
}
|
|
267
94
|
return context.config;
|
|
268
95
|
}
|
|
269
|
-
var
|
|
96
|
+
var uploadManager = new FileUploadManager();
|
|
97
|
+
var StaticContext = createContext(null);
|
|
98
|
+
var MessagesContext = createContext(null);
|
|
99
|
+
var FilesContext = createContext(null);
|
|
100
|
+
var AttachmentsContext = createContext(null);
|
|
101
|
+
var ConnectionContext = createContext(null);
|
|
102
|
+
createContext(null);
|
|
270
103
|
function ThreadProvider({
|
|
271
104
|
threadId,
|
|
272
105
|
options = {},
|
|
273
106
|
preload = true,
|
|
274
107
|
live = true,
|
|
108
|
+
useWorkblocks = false,
|
|
275
109
|
depth = 0,
|
|
276
110
|
includeSilent = false,
|
|
277
111
|
endpoint: endpointOverride,
|
|
@@ -296,13 +130,34 @@ function ThreadProvider({
|
|
|
296
130
|
const [connectionStatus, setConnectionStatus] = useState(
|
|
297
131
|
live ? "connecting" : "disconnected"
|
|
298
132
|
);
|
|
133
|
+
const [pendingFiles, setPendingFiles] = useState([]);
|
|
134
|
+
const [attachments, setAttachments] = useState([]);
|
|
135
|
+
const committedFiles = useMemo(() => {
|
|
136
|
+
return messagesToFiles(messages);
|
|
137
|
+
}, [messages]);
|
|
138
|
+
const files = useMemo(() => {
|
|
139
|
+
return [...pendingFiles, ...committedFiles];
|
|
140
|
+
}, [pendingFiles, committedFiles]);
|
|
141
|
+
const workblocks = useMemo(() => {
|
|
142
|
+
if (!useWorkblocks) {
|
|
143
|
+
return messages;
|
|
144
|
+
}
|
|
145
|
+
return transformToWorkblocks(messages);
|
|
146
|
+
}, [messages, useWorkblocks]);
|
|
299
147
|
const eventListenersRef = useRef(/* @__PURE__ */ new Map());
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
148
|
+
const connectionManagerRef = useRef(null);
|
|
149
|
+
const fileToBase64 = useCallback((file) => {
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
const reader = new FileReader();
|
|
152
|
+
reader.onload = () => {
|
|
153
|
+
const result = reader.result;
|
|
154
|
+
const base64 = result.split(",")[1];
|
|
155
|
+
resolve(base64);
|
|
156
|
+
};
|
|
157
|
+
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
158
|
+
reader.readAsDataURL(file);
|
|
159
|
+
});
|
|
160
|
+
}, []);
|
|
306
161
|
const subscribeToEvent = useCallback(
|
|
307
162
|
(eventType, listener) => {
|
|
308
163
|
if (!eventListenersRef.current.has(eventType)) {
|
|
@@ -333,60 +188,161 @@ function ThreadProvider({
|
|
|
333
188
|
});
|
|
334
189
|
}
|
|
335
190
|
}, []);
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
191
|
+
const addFiles = useCallback(
|
|
192
|
+
(filesToAdd) => {
|
|
193
|
+
const items = uploadManager.queueFiles(filesToAdd);
|
|
194
|
+
setPendingFiles((prev) => [...prev, ...items.map((i) => i.pending)]);
|
|
195
|
+
for (const { pending, file } of items) {
|
|
196
|
+
uploadManager.executeUpload(
|
|
197
|
+
threadId,
|
|
198
|
+
file,
|
|
199
|
+
pending.id,
|
|
200
|
+
clientRef.current,
|
|
201
|
+
(updates) => {
|
|
202
|
+
setPendingFiles(
|
|
203
|
+
(prev) => prev.map(
|
|
204
|
+
(f) => f.id === pending.id ? { ...f, ...updates } : f
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
).catch(() => {
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
[threadId]
|
|
213
|
+
);
|
|
214
|
+
const removeFile = useCallback((id) => {
|
|
215
|
+
setPendingFiles((prev) => prev.filter((f) => f.id !== id));
|
|
345
216
|
}, []);
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
217
|
+
const getFileUrl = useCallback(
|
|
218
|
+
(file) => {
|
|
219
|
+
if (!file.path) return "";
|
|
220
|
+
return clientRef.current.getFileUrl(threadId, file.path);
|
|
221
|
+
},
|
|
222
|
+
[threadId]
|
|
223
|
+
);
|
|
224
|
+
const getThumbnailUrl = useCallback(
|
|
225
|
+
(file) => {
|
|
226
|
+
if (!file.path) return "";
|
|
227
|
+
return clientRef.current.getThumbnailUrl(threadId, file.path);
|
|
228
|
+
},
|
|
229
|
+
[threadId]
|
|
230
|
+
);
|
|
231
|
+
const getPreviewUrl = useCallback(
|
|
232
|
+
(file) => {
|
|
233
|
+
if (!file.isImage) return null;
|
|
234
|
+
if (file.localPreviewUrl) return file.localPreviewUrl;
|
|
235
|
+
if (file.path) return clientRef.current.getThumbnailUrl(threadId, file.path);
|
|
236
|
+
return null;
|
|
237
|
+
},
|
|
238
|
+
[threadId]
|
|
239
|
+
);
|
|
240
|
+
const addAttachment = useCallback((input) => {
|
|
241
|
+
const files2 = input instanceof FileList ? Array.from(input) : Array.isArray(input) ? input : [input];
|
|
242
|
+
const newAttachments = files2.map((file) => {
|
|
243
|
+
const isImage = file.type.startsWith("image/");
|
|
244
|
+
return {
|
|
245
|
+
id: crypto.randomUUID(),
|
|
246
|
+
file,
|
|
247
|
+
name: file.name,
|
|
248
|
+
mimeType: file.type,
|
|
249
|
+
size: file.size,
|
|
250
|
+
isImage,
|
|
251
|
+
previewUrl: isImage ? URL.createObjectURL(file) : null
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
setAttachments((prev) => [...prev, ...newAttachments]);
|
|
255
|
+
}, []);
|
|
256
|
+
const removeAttachment = useCallback((id) => {
|
|
257
|
+
setAttachments((prev) => {
|
|
258
|
+
const attachment = prev.find((a) => a.id === id);
|
|
259
|
+
if (attachment?.previewUrl) {
|
|
260
|
+
URL.revokeObjectURL(attachment.previewUrl);
|
|
353
261
|
}
|
|
354
|
-
|
|
262
|
+
return prev.filter((a) => a.id !== id);
|
|
263
|
+
});
|
|
355
264
|
}, []);
|
|
265
|
+
const clearAttachments = useCallback(() => {
|
|
266
|
+
setAttachments((prev) => {
|
|
267
|
+
prev.forEach((a) => {
|
|
268
|
+
if (a.previewUrl) URL.revokeObjectURL(a.previewUrl);
|
|
269
|
+
});
|
|
270
|
+
return [];
|
|
271
|
+
});
|
|
272
|
+
}, []);
|
|
273
|
+
const sendMessage2 = useCallback(
|
|
274
|
+
async (payload) => {
|
|
275
|
+
const optimisticId = `optimistic-${crypto.randomUUID()}`;
|
|
276
|
+
const optimisticAttachments = attachments.map((a) => ({
|
|
277
|
+
id: a.id,
|
|
278
|
+
type: "file",
|
|
279
|
+
path: "",
|
|
280
|
+
// No path yet - will be assigned by server
|
|
281
|
+
name: a.name,
|
|
282
|
+
mimeType: a.mimeType,
|
|
283
|
+
size: a.size,
|
|
284
|
+
width: a.width,
|
|
285
|
+
height: a.height,
|
|
286
|
+
localPreviewUrl: a.previewUrl || void 0
|
|
287
|
+
}));
|
|
288
|
+
const optimisticMessage = {
|
|
289
|
+
id: optimisticId,
|
|
290
|
+
role: payload.role,
|
|
291
|
+
content: payload.content,
|
|
292
|
+
attachments: optimisticAttachments.length > 0 ? JSON.stringify(optimisticAttachments) : null,
|
|
293
|
+
created_at: Date.now() * 1e3,
|
|
294
|
+
// microseconds
|
|
295
|
+
status: "pending"
|
|
296
|
+
};
|
|
297
|
+
setMessages((prev) => [...prev, optimisticMessage]);
|
|
298
|
+
const currentAttachments = [...attachments];
|
|
299
|
+
setAttachments([]);
|
|
300
|
+
try {
|
|
301
|
+
const attachmentPayloads = await Promise.all(
|
|
302
|
+
currentAttachments.map(async (a) => ({
|
|
303
|
+
name: a.name,
|
|
304
|
+
mimeType: a.mimeType,
|
|
305
|
+
data: await fileToBase64(a.file),
|
|
306
|
+
width: a.width,
|
|
307
|
+
height: a.height
|
|
308
|
+
}))
|
|
309
|
+
);
|
|
310
|
+
const result = await clientRef.current.sendMessage(threadId, {
|
|
311
|
+
...payload,
|
|
312
|
+
attachments: attachmentPayloads.length > 0 ? attachmentPayloads : void 0
|
|
313
|
+
});
|
|
314
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
315
|
+
currentAttachments.forEach((a) => {
|
|
316
|
+
if (a.previewUrl) URL.revokeObjectURL(a.previewUrl);
|
|
317
|
+
});
|
|
318
|
+
return result;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
setAttachments(currentAttachments);
|
|
321
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
322
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
[threadId, attachments, fileToBase64]
|
|
327
|
+
);
|
|
328
|
+
const stopExecution = useCallback(async () => {
|
|
329
|
+
try {
|
|
330
|
+
await clientRef.current.stopExecution(threadId);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
333
|
+
throw err;
|
|
334
|
+
}
|
|
335
|
+
}, [threadId]);
|
|
356
336
|
useEffect(() => {
|
|
357
337
|
if (!live || !threadId) return;
|
|
358
|
-
console.log(
|
|
359
|
-
`[ThreadProvider] useEffect running - threadId: ${threadId}, live: ${live}`
|
|
360
|
-
);
|
|
361
|
-
connectWebSocket();
|
|
362
|
-
return () => {
|
|
363
|
-
console.log(
|
|
364
|
-
`[ThreadProvider] useEffect cleanup - threadId: ${threadId}, live: ${live}`
|
|
365
|
-
);
|
|
366
|
-
clearTimers();
|
|
367
|
-
if (wsRef.current) {
|
|
368
|
-
wsRef.current.close();
|
|
369
|
-
wsRef.current = null;
|
|
370
|
-
}
|
|
371
|
-
reconnectAttempts.current = 0;
|
|
372
|
-
isReconnectingRef.current = false;
|
|
373
|
-
};
|
|
374
|
-
}, [threadId, live]);
|
|
375
|
-
const connectWebSocket = useCallback(() => {
|
|
376
|
-
if (!live || !threadId || !isMountedRef.current) return;
|
|
377
338
|
console.log(`[ThreadProvider] Connecting WebSocket for thread ${threadId}`);
|
|
378
|
-
|
|
379
|
-
|
|
339
|
+
const manager = new ThreadConnectionManager(
|
|
340
|
+
clientRef.current,
|
|
380
341
|
threadId,
|
|
381
342
|
{
|
|
382
|
-
|
|
383
|
-
console.log(
|
|
384
|
-
|
|
385
|
-
);
|
|
386
|
-
setConnectionStatus("connected");
|
|
387
|
-
reconnectAttempts.current = 0;
|
|
388
|
-
isReconnectingRef.current = false;
|
|
389
|
-
startHeartbeat(ws);
|
|
343
|
+
onStatusChange: (status) => {
|
|
344
|
+
console.log(`[ThreadProvider] Connection status: ${status}`);
|
|
345
|
+
setConnectionStatus(status);
|
|
390
346
|
},
|
|
391
347
|
onMessage: (event) => {
|
|
392
348
|
setMessages((prev) => {
|
|
@@ -417,30 +373,6 @@ function ThreadProvider({
|
|
|
417
373
|
onError: (event) => {
|
|
418
374
|
console.error("[ThreadProvider] WebSocket error:", event.error);
|
|
419
375
|
setError(new Error(event.error));
|
|
420
|
-
setConnectionStatus("disconnected");
|
|
421
|
-
},
|
|
422
|
-
onClose: () => {
|
|
423
|
-
console.log("[ThreadProvider] WebSocket closed");
|
|
424
|
-
clearTimers();
|
|
425
|
-
if (isMountedRef.current && !isReconnectingRef.current) {
|
|
426
|
-
isReconnectingRef.current = true;
|
|
427
|
-
setConnectionStatus("connecting");
|
|
428
|
-
const delay = Math.min(
|
|
429
|
-
1e3 * Math.pow(2, reconnectAttempts.current),
|
|
430
|
-
3e4
|
|
431
|
-
);
|
|
432
|
-
reconnectAttempts.current++;
|
|
433
|
-
console.log(
|
|
434
|
-
`[ThreadProvider] Reconnecting in ${delay}ms (attempt ${reconnectAttempts.current})`
|
|
435
|
-
);
|
|
436
|
-
reconnectTimeoutRef.current = setTimeout(() => {
|
|
437
|
-
if (isMountedRef.current) {
|
|
438
|
-
connectWebSocket();
|
|
439
|
-
}
|
|
440
|
-
}, delay);
|
|
441
|
-
} else {
|
|
442
|
-
setConnectionStatus("disconnected");
|
|
443
|
-
}
|
|
444
376
|
}
|
|
445
377
|
},
|
|
446
378
|
{
|
|
@@ -448,16 +380,13 @@ function ThreadProvider({
|
|
|
448
380
|
includeSilent
|
|
449
381
|
}
|
|
450
382
|
);
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
startHeartbeat,
|
|
459
|
-
clearTimers
|
|
460
|
-
]);
|
|
383
|
+
connectionManagerRef.current = manager;
|
|
384
|
+
manager.connect();
|
|
385
|
+
return () => {
|
|
386
|
+
manager.disconnect();
|
|
387
|
+
connectionManagerRef.current = null;
|
|
388
|
+
};
|
|
389
|
+
}, [threadId, live, depth, includeSilent, dispatchEvent]);
|
|
461
390
|
useEffect(() => {
|
|
462
391
|
if (!preload || !threadId) return;
|
|
463
392
|
const fetchMessages = async () => {
|
|
@@ -481,172 +410,185 @@ function ThreadProvider({
|
|
|
481
410
|
};
|
|
482
411
|
fetchMessages();
|
|
483
412
|
}, [threadId, preload, depth, includeSilent]);
|
|
484
|
-
|
|
485
|
-
isMountedRef.current = true;
|
|
486
|
-
return () => {
|
|
487
|
-
isMountedRef.current = false;
|
|
488
|
-
};
|
|
489
|
-
}, []);
|
|
490
|
-
const contextValue = useMemo(
|
|
413
|
+
const staticValue = useMemo(
|
|
491
414
|
() => ({
|
|
492
415
|
threadId,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
connectionStatus,
|
|
416
|
+
options: { preload, live, useWorkblocks, depth, includeSilent },
|
|
417
|
+
sendMessage: sendMessage2,
|
|
418
|
+
stopExecution,
|
|
497
419
|
subscribeToEvent,
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
420
|
+
addFiles,
|
|
421
|
+
removeFile,
|
|
422
|
+
getFileUrl,
|
|
423
|
+
getThumbnailUrl,
|
|
424
|
+
getPreviewUrl,
|
|
425
|
+
addAttachment,
|
|
426
|
+
removeAttachment,
|
|
427
|
+
clearAttachments
|
|
503
428
|
}),
|
|
504
429
|
[
|
|
505
430
|
threadId,
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
connectionStatus,
|
|
510
|
-
subscribeToEvent,
|
|
431
|
+
preload,
|
|
432
|
+
live,
|
|
433
|
+
useWorkblocks,
|
|
511
434
|
depth,
|
|
512
435
|
includeSilent,
|
|
513
|
-
|
|
436
|
+
sendMessage2,
|
|
437
|
+
stopExecution,
|
|
438
|
+
subscribeToEvent,
|
|
439
|
+
addFiles,
|
|
440
|
+
removeFile,
|
|
441
|
+
getFileUrl,
|
|
442
|
+
getThumbnailUrl,
|
|
443
|
+
getPreviewUrl,
|
|
444
|
+
addAttachment,
|
|
445
|
+
removeAttachment,
|
|
446
|
+
clearAttachments
|
|
514
447
|
]
|
|
515
448
|
);
|
|
516
|
-
|
|
449
|
+
const messagesValue = useMemo(
|
|
450
|
+
() => ({
|
|
451
|
+
messages,
|
|
452
|
+
workblocks,
|
|
453
|
+
loading
|
|
454
|
+
}),
|
|
455
|
+
[messages, workblocks, loading]
|
|
456
|
+
);
|
|
457
|
+
const filesValue = useMemo(
|
|
458
|
+
() => ({
|
|
459
|
+
files
|
|
460
|
+
}),
|
|
461
|
+
[files]
|
|
462
|
+
);
|
|
463
|
+
const connectionValue = useMemo(
|
|
464
|
+
() => ({
|
|
465
|
+
connectionStatus,
|
|
466
|
+
error
|
|
467
|
+
}),
|
|
468
|
+
[connectionStatus, error]
|
|
469
|
+
);
|
|
470
|
+
const attachmentsValue = useMemo(
|
|
471
|
+
() => ({
|
|
472
|
+
attachments
|
|
473
|
+
}),
|
|
474
|
+
[attachments]
|
|
475
|
+
);
|
|
476
|
+
return /* @__PURE__ */ jsx(StaticContext.Provider, { value: staticValue, children: /* @__PURE__ */ jsx(MessagesContext.Provider, { value: messagesValue, children: /* @__PURE__ */ jsx(FilesContext.Provider, { value: filesValue, children: /* @__PURE__ */ jsx(AttachmentsContext.Provider, { value: attachmentsValue, children: /* @__PURE__ */ jsx(ConnectionContext.Provider, { value: connectionValue, children }) }) }) }) });
|
|
517
477
|
}
|
|
518
|
-
function
|
|
519
|
-
const context = useContext(
|
|
478
|
+
function useStaticContext() {
|
|
479
|
+
const context = useContext(StaticContext);
|
|
520
480
|
if (!context) {
|
|
521
|
-
throw new Error("
|
|
481
|
+
throw new Error("useThread must be used within a ThreadProvider");
|
|
522
482
|
}
|
|
523
483
|
return context;
|
|
524
484
|
}
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
// src/utils/workblocks.ts
|
|
530
|
-
function transformToWorkblocks(messages) {
|
|
531
|
-
if (messages.length === 0) {
|
|
532
|
-
return [];
|
|
485
|
+
function useMessagesContext() {
|
|
486
|
+
const context = useContext(MessagesContext);
|
|
487
|
+
if (!context) {
|
|
488
|
+
throw new Error("useThread must be used within a ThreadProvider");
|
|
533
489
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
try {
|
|
541
|
-
toolCalls = JSON.parse(message.tool_calls);
|
|
542
|
-
} catch (error) {
|
|
543
|
-
result.push(message);
|
|
544
|
-
i++;
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
const workItems = [];
|
|
548
|
-
for (const toolCall of toolCalls) {
|
|
549
|
-
workItems.push({
|
|
550
|
-
id: toolCall.id || message.id,
|
|
551
|
-
type: "tool_call",
|
|
552
|
-
name: toolCall.function?.name,
|
|
553
|
-
content: toolCall.function?.arguments || null,
|
|
554
|
-
status: null,
|
|
555
|
-
// Will be updated below based on matching results
|
|
556
|
-
tool_call_id: toolCall.id
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
let j = i + 1;
|
|
560
|
-
while (j < messages.length && messages[j].role === "tool") {
|
|
561
|
-
const toolMessage = messages[j];
|
|
562
|
-
const resultStatus = toolMessage.tool_status || "pending";
|
|
563
|
-
workItems.push({
|
|
564
|
-
id: toolMessage.id,
|
|
565
|
-
type: "tool_result",
|
|
566
|
-
name: toolMessage.name || void 0,
|
|
567
|
-
content: toolMessage.content,
|
|
568
|
-
status: resultStatus,
|
|
569
|
-
tool_call_id: toolMessage.tool_call_id || void 0
|
|
570
|
-
});
|
|
571
|
-
j++;
|
|
572
|
-
}
|
|
573
|
-
for (const item of workItems) {
|
|
574
|
-
if (item.type === "tool_call" && item.tool_call_id) {
|
|
575
|
-
const matchingResult = workItems.find(
|
|
576
|
-
(wi) => wi.type === "tool_result" && wi.tool_call_id === item.tool_call_id
|
|
577
|
-
);
|
|
578
|
-
if (matchingResult) {
|
|
579
|
-
item.status = matchingResult.status;
|
|
580
|
-
} else {
|
|
581
|
-
item.status = "pending";
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
let status = "completed";
|
|
586
|
-
if (message.status === "pending") {
|
|
587
|
-
status = "pending";
|
|
588
|
-
} else if (message.status === "failed") {
|
|
589
|
-
status = "failed";
|
|
590
|
-
}
|
|
591
|
-
const workblock = {
|
|
592
|
-
id: message.id,
|
|
593
|
-
type: "workblock",
|
|
594
|
-
content: message.content,
|
|
595
|
-
reasoning_content: message.reasoning_content,
|
|
596
|
-
workItems,
|
|
597
|
-
status,
|
|
598
|
-
created_at: message.created_at,
|
|
599
|
-
depth: message.depth
|
|
600
|
-
};
|
|
601
|
-
result.push(workblock);
|
|
602
|
-
i = j;
|
|
603
|
-
} else {
|
|
604
|
-
result.push(message);
|
|
605
|
-
i++;
|
|
606
|
-
}
|
|
490
|
+
return context;
|
|
491
|
+
}
|
|
492
|
+
function useFilesContext() {
|
|
493
|
+
const context = useContext(FilesContext);
|
|
494
|
+
if (!context) {
|
|
495
|
+
throw new Error("useThread must be used within a ThreadProvider");
|
|
607
496
|
}
|
|
608
|
-
return
|
|
497
|
+
return context;
|
|
609
498
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
function useThread(options = {}) {
|
|
613
|
-
const {
|
|
614
|
-
useWorkblocks = true
|
|
615
|
-
} = options;
|
|
616
|
-
const context = useContext(ThreadContext);
|
|
499
|
+
function useConnectionContext() {
|
|
500
|
+
const context = useContext(ConnectionContext);
|
|
617
501
|
if (!context) {
|
|
618
|
-
throw new Error(
|
|
619
|
-
'useThread must be used within a ThreadProvider. Wrap your component with <ThreadProvider threadId="...">.'
|
|
620
|
-
);
|
|
502
|
+
throw new Error("useThread must be used within a ThreadProvider");
|
|
621
503
|
}
|
|
622
|
-
|
|
623
|
-
const transformedMessages = useMemo(() => {
|
|
624
|
-
return useWorkblocks ? transformToWorkblocks(messages) : messages;
|
|
625
|
-
}, [messages, useWorkblocks]);
|
|
626
|
-
return transformedMessages;
|
|
504
|
+
return context;
|
|
627
505
|
}
|
|
628
|
-
function
|
|
629
|
-
const context = useContext(
|
|
506
|
+
function useAttachmentsContext() {
|
|
507
|
+
const context = useContext(AttachmentsContext);
|
|
630
508
|
if (!context) {
|
|
631
|
-
throw new Error(
|
|
632
|
-
|
|
509
|
+
throw new Error("useThread must be used within a ThreadProvider");
|
|
510
|
+
}
|
|
511
|
+
return context;
|
|
512
|
+
}
|
|
513
|
+
function useThreadContextInternal() {
|
|
514
|
+
const static_ = useStaticContext();
|
|
515
|
+
const messages_ = useMessagesContext();
|
|
516
|
+
const files_ = useFilesContext();
|
|
517
|
+
const attachments_ = useAttachmentsContext();
|
|
518
|
+
const connection_ = useConnectionContext();
|
|
519
|
+
return {
|
|
520
|
+
threadId: static_.threadId,
|
|
521
|
+
options: static_.options,
|
|
522
|
+
sendMessage: static_.sendMessage,
|
|
523
|
+
stopExecution: static_.stopExecution,
|
|
524
|
+
subscribeToEvent: static_.subscribeToEvent,
|
|
525
|
+
onEvent: static_.subscribeToEvent,
|
|
526
|
+
// File management (uploads to filesystem)
|
|
527
|
+
addFiles: static_.addFiles,
|
|
528
|
+
removeFile: static_.removeFile,
|
|
529
|
+
getFileUrl: static_.getFileUrl,
|
|
530
|
+
getThumbnailUrl: static_.getThumbnailUrl,
|
|
531
|
+
getPreviewUrl: static_.getPreviewUrl,
|
|
532
|
+
// Attachment management (sent inline with messages)
|
|
533
|
+
addAttachment: static_.addAttachment,
|
|
534
|
+
removeAttachment: static_.removeAttachment,
|
|
535
|
+
clearAttachments: static_.clearAttachments,
|
|
536
|
+
attachments: attachments_.attachments,
|
|
537
|
+
// Messages
|
|
538
|
+
messages: messages_.messages,
|
|
539
|
+
workblocks: messages_.workblocks,
|
|
540
|
+
loading: messages_.loading,
|
|
541
|
+
isLoading: messages_.loading,
|
|
542
|
+
files: files_.files,
|
|
543
|
+
connectionStatus: connection_.connectionStatus,
|
|
544
|
+
status: connection_.connectionStatus,
|
|
545
|
+
error: connection_.error
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
var hasWarnedAboutUseThreadContext = false;
|
|
549
|
+
function useThreadContext() {
|
|
550
|
+
if (!hasWarnedAboutUseThreadContext && process.env.NODE_ENV !== "production") {
|
|
551
|
+
hasWarnedAboutUseThreadContext = true;
|
|
552
|
+
console.warn(
|
|
553
|
+
"[DEPRECATED] useThreadContext() is deprecated.\nUse: const { messages, sendMessage, ... } = useThread()"
|
|
633
554
|
);
|
|
634
555
|
}
|
|
635
|
-
|
|
556
|
+
return useThreadContextInternal();
|
|
557
|
+
}
|
|
558
|
+
function useThreadId() {
|
|
559
|
+
return useStaticContext().threadId;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/hooks/useThread.ts
|
|
563
|
+
var hasWarnedAboutArrayUsage = false;
|
|
564
|
+
function useThread() {
|
|
565
|
+
const context = useThreadContextInternal();
|
|
566
|
+
if (process.env.NODE_ENV !== "production") {
|
|
567
|
+
const arrayMethods = ["map", "filter", "forEach", "find", "some", "every", "reduce", "length", "push", "pop", "slice", "splice"];
|
|
568
|
+
return new Proxy(context, {
|
|
569
|
+
get(target, prop) {
|
|
570
|
+
if (arrayMethods.includes(prop) && !hasWarnedAboutArrayUsage) {
|
|
571
|
+
hasWarnedAboutArrayUsage = true;
|
|
572
|
+
console.warn(
|
|
573
|
+
"[BREAKING CHANGE] useThread() now returns an object, not an array.\nChange: const messages = useThread()\nTo: const { messages } = useThread()\n\nThe returned object includes: messages, sendMessage, stopExecution, files, and more."
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
return target[prop];
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return context;
|
|
581
|
+
}
|
|
582
|
+
function onThreadEvent(type, callback) {
|
|
583
|
+
const { subscribeToEvent } = useThreadContextInternal();
|
|
636
584
|
useEffect(() => {
|
|
637
585
|
const unsubscribe = subscribeToEvent(type, callback);
|
|
638
586
|
return unsubscribe;
|
|
639
587
|
}, [subscribeToEvent, type, callback]);
|
|
640
588
|
}
|
|
641
589
|
function useThreadEvent(type) {
|
|
642
|
-
const
|
|
590
|
+
const { subscribeToEvent } = useThreadContextInternal();
|
|
643
591
|
const [eventData, setEventData] = useState(null);
|
|
644
|
-
if (!context) {
|
|
645
|
-
throw new Error(
|
|
646
|
-
'useThreadEvent must be used within a ThreadProvider. Wrap your component with <ThreadProvider threadId="...">.'
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
const { subscribeToEvent } = context;
|
|
650
592
|
useEffect(() => {
|
|
651
593
|
const unsubscribe = subscribeToEvent(type, (data) => {
|
|
652
594
|
setEventData(data);
|
|
@@ -655,7 +597,14 @@ function useThreadEvent(type) {
|
|
|
655
597
|
}, [subscribeToEvent, type]);
|
|
656
598
|
return eventData;
|
|
657
599
|
}
|
|
600
|
+
var hasWarned = false;
|
|
658
601
|
function useSendMessage() {
|
|
602
|
+
if (!hasWarned && process.env.NODE_ENV !== "production") {
|
|
603
|
+
hasWarned = true;
|
|
604
|
+
console.warn(
|
|
605
|
+
'[DEPRECATED] useSendMessage() is deprecated.\nUse: const { sendMessage } = useThread()\nThen: await sendMessage({ role: "user", content: "Hello!" })'
|
|
606
|
+
);
|
|
607
|
+
}
|
|
659
608
|
let context;
|
|
660
609
|
try {
|
|
661
610
|
context = useThreadContext();
|
|
@@ -670,7 +619,14 @@ function useSendMessage() {
|
|
|
670
619
|
[threadId]
|
|
671
620
|
);
|
|
672
621
|
}
|
|
622
|
+
var hasWarned2 = false;
|
|
673
623
|
function useStopThread() {
|
|
624
|
+
if (!hasWarned2 && process.env.NODE_ENV !== "production") {
|
|
625
|
+
hasWarned2 = true;
|
|
626
|
+
console.warn(
|
|
627
|
+
"[DEPRECATED] useStopThread() is deprecated.\nUse: const { stopExecution } = useThread()"
|
|
628
|
+
);
|
|
629
|
+
}
|
|
674
630
|
let context;
|
|
675
631
|
try {
|
|
676
632
|
context = useThreadContext();
|