@standardagents/react 0.10.1-next.bbd142a → 0.11.0-next.99fb790
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 +85 -170
- package/dist/index.js +449 -396
- 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, 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,38 @@ function ThreadProvider({
|
|
|
296
130
|
const [connectionStatus, setConnectionStatus] = useState(
|
|
297
131
|
live ? "connecting" : "disconnected"
|
|
298
132
|
);
|
|
133
|
+
const [pendingFiles, setPendingFiles] = useState([]);
|
|
134
|
+
const [serverFiles, setServerFiles] = useState([]);
|
|
135
|
+
const [attachments, setAttachments] = useState([]);
|
|
136
|
+
const messagesRef = useRef(messages);
|
|
137
|
+
messagesRef.current = messages;
|
|
138
|
+
const files = useMemo(() => {
|
|
139
|
+
const serverPaths = new Set(serverFiles.map((f) => f.path));
|
|
140
|
+
const uniquePendingFiles = pendingFiles.filter(
|
|
141
|
+
(f) => !f.path || !serverPaths.has(f.path)
|
|
142
|
+
);
|
|
143
|
+
return [...uniquePendingFiles, ...serverFiles];
|
|
144
|
+
}, [pendingFiles, serverFiles]);
|
|
145
|
+
const workblocks = useMemo(() => {
|
|
146
|
+
if (!useWorkblocks) {
|
|
147
|
+
return messages;
|
|
148
|
+
}
|
|
149
|
+
return transformToWorkblocks(messages);
|
|
150
|
+
}, [messages, useWorkblocks]);
|
|
299
151
|
const eventListenersRef = useRef(/* @__PURE__ */ new Map());
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
152
|
+
const connectionManagerRef = useRef(null);
|
|
153
|
+
const fileToBase64 = useCallback((file) => {
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const reader = new FileReader();
|
|
156
|
+
reader.onload = () => {
|
|
157
|
+
const result = reader.result;
|
|
158
|
+
const base64 = result.split(",")[1];
|
|
159
|
+
resolve(base64);
|
|
160
|
+
};
|
|
161
|
+
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
162
|
+
reader.readAsDataURL(file);
|
|
163
|
+
});
|
|
164
|
+
}, []);
|
|
306
165
|
const subscribeToEvent = useCallback(
|
|
307
166
|
(eventType, listener) => {
|
|
308
167
|
if (!eventListenersRef.current.has(eventType)) {
|
|
@@ -333,60 +192,239 @@ function ThreadProvider({
|
|
|
333
192
|
});
|
|
334
193
|
}
|
|
335
194
|
}, []);
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
195
|
+
const addFiles = useCallback(
|
|
196
|
+
(filesToAdd) => {
|
|
197
|
+
const items = uploadManager.queueFiles(filesToAdd);
|
|
198
|
+
setPendingFiles((prev) => [...prev, ...items.map((i) => i.pending)]);
|
|
199
|
+
for (const { pending, file } of items) {
|
|
200
|
+
uploadManager.executeUpload(
|
|
201
|
+
threadId,
|
|
202
|
+
file,
|
|
203
|
+
pending.id,
|
|
204
|
+
clientRef.current,
|
|
205
|
+
(updates) => {
|
|
206
|
+
setPendingFiles(
|
|
207
|
+
(prev) => prev.map(
|
|
208
|
+
(f) => f.id === pending.id ? { ...f, ...updates } : f
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
).catch(() => {
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
[threadId]
|
|
217
|
+
);
|
|
218
|
+
const removeFile = useCallback((id) => {
|
|
219
|
+
setPendingFiles((prev) => prev.filter((f) => f.id !== id));
|
|
345
220
|
}, []);
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
221
|
+
const getFileUrl = useCallback(
|
|
222
|
+
(file) => {
|
|
223
|
+
if (!file.path) return "";
|
|
224
|
+
return clientRef.current.getFileUrl(threadId, file.path);
|
|
225
|
+
},
|
|
226
|
+
[threadId]
|
|
227
|
+
);
|
|
228
|
+
const getThumbnailUrl = useCallback(
|
|
229
|
+
(file) => {
|
|
230
|
+
if (!file.path) return "";
|
|
231
|
+
return clientRef.current.getThumbnailUrl(threadId, file.path);
|
|
232
|
+
},
|
|
233
|
+
[threadId]
|
|
234
|
+
);
|
|
235
|
+
const getPreviewUrl = useCallback(
|
|
236
|
+
(file) => {
|
|
237
|
+
if (!file.isImage) return null;
|
|
238
|
+
if (file.localPreviewUrl) return file.localPreviewUrl;
|
|
239
|
+
if (file.path) return clientRef.current.getThumbnailUrl(threadId, file.path);
|
|
240
|
+
return null;
|
|
241
|
+
},
|
|
242
|
+
[threadId]
|
|
243
|
+
);
|
|
244
|
+
const loadFiles = useCallback(async () => {
|
|
245
|
+
try {
|
|
246
|
+
const fileList = await clientRef.current.listFiles(threadId);
|
|
247
|
+
setServerFiles(
|
|
248
|
+
fileList.filter((f) => !f.isDirectory).map((f) => ({
|
|
249
|
+
id: f.path,
|
|
250
|
+
name: f.name,
|
|
251
|
+
path: f.path,
|
|
252
|
+
mimeType: f.mimeType,
|
|
253
|
+
size: f.size,
|
|
254
|
+
isImage: f.mimeType.startsWith("image/"),
|
|
255
|
+
status: "committed",
|
|
256
|
+
localPreviewUrl: null
|
|
257
|
+
}))
|
|
258
|
+
);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error("Failed to load files:", err);
|
|
349
261
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
262
|
+
}, [threadId]);
|
|
263
|
+
const handleFileCreated = useCallback(
|
|
264
|
+
(data) => {
|
|
265
|
+
if (data.file.isDirectory) return;
|
|
266
|
+
const newFile = {
|
|
267
|
+
id: data.path,
|
|
268
|
+
name: data.file.name,
|
|
269
|
+
path: data.path,
|
|
270
|
+
mimeType: data.file.mimeType,
|
|
271
|
+
size: data.file.size,
|
|
272
|
+
isImage: data.file.mimeType?.startsWith("image/") || false,
|
|
273
|
+
status: "committed",
|
|
274
|
+
localPreviewUrl: null
|
|
275
|
+
};
|
|
276
|
+
setServerFiles((prev) => {
|
|
277
|
+
const existingIndex = prev.findIndex((f) => f.path === data.path);
|
|
278
|
+
if (existingIndex >= 0) {
|
|
279
|
+
const updated = [...prev];
|
|
280
|
+
updated[existingIndex] = newFile;
|
|
281
|
+
return updated;
|
|
282
|
+
}
|
|
283
|
+
return [...prev, newFile];
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
[]
|
|
287
|
+
);
|
|
288
|
+
const handleFileUpdated = useCallback(
|
|
289
|
+
(data) => {
|
|
290
|
+
if (data.file.isDirectory) return;
|
|
291
|
+
setServerFiles(
|
|
292
|
+
(prev) => prev.map(
|
|
293
|
+
(f) => f.path === data.path ? {
|
|
294
|
+
...f,
|
|
295
|
+
name: data.file.name,
|
|
296
|
+
mimeType: data.file.mimeType,
|
|
297
|
+
size: data.file.size,
|
|
298
|
+
isImage: data.file.mimeType?.startsWith("image/") || false
|
|
299
|
+
} : f
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
[]
|
|
304
|
+
);
|
|
305
|
+
const handleFileDeleted = useCallback((data) => {
|
|
306
|
+
setServerFiles((prev) => prev.filter((f) => f.path !== data.path));
|
|
307
|
+
}, []);
|
|
308
|
+
const addAttachment = useCallback((input) => {
|
|
309
|
+
const files2 = input instanceof FileList ? Array.from(input) : Array.isArray(input) ? input : [input];
|
|
310
|
+
const newAttachments = files2.map((file) => {
|
|
311
|
+
const isImage = file.type.startsWith("image/");
|
|
312
|
+
return {
|
|
313
|
+
id: crypto.randomUUID(),
|
|
314
|
+
file,
|
|
315
|
+
name: file.name,
|
|
316
|
+
mimeType: file.type,
|
|
317
|
+
size: file.size,
|
|
318
|
+
isImage,
|
|
319
|
+
previewUrl: isImage ? URL.createObjectURL(file) : null
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
setAttachments((prev) => [...prev, ...newAttachments]);
|
|
323
|
+
}, []);
|
|
324
|
+
const removeAttachment = useCallback((id) => {
|
|
325
|
+
setAttachments((prev) => {
|
|
326
|
+
const attachment = prev.find((a) => a.id === id);
|
|
327
|
+
if (attachment?.previewUrl) {
|
|
328
|
+
URL.revokeObjectURL(attachment.previewUrl);
|
|
353
329
|
}
|
|
354
|
-
|
|
330
|
+
return prev.filter((a) => a.id !== id);
|
|
331
|
+
});
|
|
332
|
+
}, []);
|
|
333
|
+
const clearAttachments = useCallback(() => {
|
|
334
|
+
setAttachments((prev) => {
|
|
335
|
+
prev.forEach((a) => {
|
|
336
|
+
if (a.previewUrl) URL.revokeObjectURL(a.previewUrl);
|
|
337
|
+
});
|
|
338
|
+
return [];
|
|
339
|
+
});
|
|
355
340
|
}, []);
|
|
341
|
+
const sendMessage2 = useCallback(
|
|
342
|
+
async (payload) => {
|
|
343
|
+
const optimisticId = `optimistic-${crypto.randomUUID()}`;
|
|
344
|
+
const optimisticAttachments = attachments.map((a) => ({
|
|
345
|
+
id: a.id,
|
|
346
|
+
type: "file",
|
|
347
|
+
path: "",
|
|
348
|
+
// No path yet - will be assigned by server
|
|
349
|
+
name: a.name,
|
|
350
|
+
mimeType: a.mimeType,
|
|
351
|
+
size: a.size,
|
|
352
|
+
width: a.width,
|
|
353
|
+
height: a.height,
|
|
354
|
+
localPreviewUrl: a.previewUrl || void 0
|
|
355
|
+
}));
|
|
356
|
+
const optimisticMessage = {
|
|
357
|
+
id: optimisticId,
|
|
358
|
+
role: payload.role,
|
|
359
|
+
content: payload.content,
|
|
360
|
+
attachments: optimisticAttachments.length > 0 ? JSON.stringify(optimisticAttachments) : null,
|
|
361
|
+
created_at: Date.now() * 1e3,
|
|
362
|
+
// microseconds
|
|
363
|
+
status: "pending"
|
|
364
|
+
};
|
|
365
|
+
setMessages((prev) => [...prev, optimisticMessage]);
|
|
366
|
+
const currentAttachments = [...attachments];
|
|
367
|
+
setAttachments([]);
|
|
368
|
+
try {
|
|
369
|
+
const attachmentPayloads = await Promise.all(
|
|
370
|
+
currentAttachments.map(async (a) => ({
|
|
371
|
+
name: a.name,
|
|
372
|
+
mimeType: a.mimeType,
|
|
373
|
+
data: await fileToBase64(a.file),
|
|
374
|
+
width: a.width,
|
|
375
|
+
height: a.height
|
|
376
|
+
}))
|
|
377
|
+
);
|
|
378
|
+
const result = await clientRef.current.sendMessage(threadId, {
|
|
379
|
+
...payload,
|
|
380
|
+
attachments: attachmentPayloads.length > 0 ? attachmentPayloads : void 0
|
|
381
|
+
});
|
|
382
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
383
|
+
currentAttachments.forEach((a) => {
|
|
384
|
+
if (a.previewUrl) URL.revokeObjectURL(a.previewUrl);
|
|
385
|
+
});
|
|
386
|
+
return result;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
setAttachments(currentAttachments);
|
|
389
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
390
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
391
|
+
throw err;
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
[threadId, attachments, fileToBase64]
|
|
395
|
+
);
|
|
396
|
+
const stopExecution = useCallback(async () => {
|
|
397
|
+
try {
|
|
398
|
+
await clientRef.current.stopExecution(threadId);
|
|
399
|
+
} catch (err) {
|
|
400
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
401
|
+
throw err;
|
|
402
|
+
}
|
|
403
|
+
}, [threadId]);
|
|
404
|
+
const deleteMessage = useCallback(
|
|
405
|
+
async (messageId) => {
|
|
406
|
+
const previousMessages = messagesRef.current;
|
|
407
|
+
setMessages((prev) => prev.filter((m) => m.id !== messageId));
|
|
408
|
+
try {
|
|
409
|
+
await clientRef.current.deleteMessage(threadId, messageId);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
setMessages(previousMessages);
|
|
412
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
413
|
+
throw err;
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
[threadId]
|
|
417
|
+
);
|
|
356
418
|
useEffect(() => {
|
|
357
419
|
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
420
|
console.log(`[ThreadProvider] Connecting WebSocket for thread ${threadId}`);
|
|
378
|
-
|
|
379
|
-
|
|
421
|
+
const manager = new ThreadConnectionManager(
|
|
422
|
+
clientRef.current,
|
|
380
423
|
threadId,
|
|
381
424
|
{
|
|
382
|
-
|
|
383
|
-
console.log(
|
|
384
|
-
|
|
385
|
-
);
|
|
386
|
-
setConnectionStatus("connected");
|
|
387
|
-
reconnectAttempts.current = 0;
|
|
388
|
-
isReconnectingRef.current = false;
|
|
389
|
-
startHeartbeat(ws);
|
|
425
|
+
onStatusChange: (status) => {
|
|
426
|
+
console.log(`[ThreadProvider] Connection status: ${status}`);
|
|
427
|
+
setConnectionStatus(status);
|
|
390
428
|
},
|
|
391
429
|
onMessage: (event) => {
|
|
392
430
|
setMessages((prev) => {
|
|
@@ -412,35 +450,18 @@ function ThreadProvider({
|
|
|
412
450
|
});
|
|
413
451
|
},
|
|
414
452
|
onEvent: (event) => {
|
|
453
|
+
if (event.eventType === "file_created") {
|
|
454
|
+
handleFileCreated(event.data);
|
|
455
|
+
} else if (event.eventType === "file_updated") {
|
|
456
|
+
handleFileUpdated(event.data);
|
|
457
|
+
} else if (event.eventType === "file_deleted") {
|
|
458
|
+
handleFileDeleted(event.data);
|
|
459
|
+
}
|
|
415
460
|
dispatchEvent(event.eventType, event.data);
|
|
416
461
|
},
|
|
417
462
|
onError: (event) => {
|
|
418
463
|
console.error("[ThreadProvider] WebSocket error:", event.error);
|
|
419
464
|
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
465
|
}
|
|
445
466
|
},
|
|
446
467
|
{
|
|
@@ -448,16 +469,13 @@ function ThreadProvider({
|
|
|
448
469
|
includeSilent
|
|
449
470
|
}
|
|
450
471
|
);
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
startHeartbeat,
|
|
459
|
-
clearTimers
|
|
460
|
-
]);
|
|
472
|
+
connectionManagerRef.current = manager;
|
|
473
|
+
manager.connect();
|
|
474
|
+
return () => {
|
|
475
|
+
manager.disconnect();
|
|
476
|
+
connectionManagerRef.current = null;
|
|
477
|
+
};
|
|
478
|
+
}, [threadId, live, depth, includeSilent, dispatchEvent, handleFileCreated, handleFileUpdated, handleFileDeleted]);
|
|
461
479
|
useEffect(() => {
|
|
462
480
|
if (!preload || !threadId) return;
|
|
463
481
|
const fetchMessages = async () => {
|
|
@@ -482,171 +500,192 @@ function ThreadProvider({
|
|
|
482
500
|
fetchMessages();
|
|
483
501
|
}, [threadId, preload, depth, includeSilent]);
|
|
484
502
|
useEffect(() => {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const contextValue = useMemo(
|
|
503
|
+
if (!threadId) return;
|
|
504
|
+
setServerFiles([]);
|
|
505
|
+
loadFiles();
|
|
506
|
+
}, [threadId, loadFiles]);
|
|
507
|
+
const staticValue = useMemo(
|
|
491
508
|
() => ({
|
|
492
509
|
threadId,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
510
|
+
options: { preload, live, useWorkblocks, depth, includeSilent },
|
|
511
|
+
sendMessage: sendMessage2,
|
|
512
|
+
stopExecution,
|
|
513
|
+
deleteMessage,
|
|
497
514
|
subscribeToEvent,
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
515
|
+
addFiles,
|
|
516
|
+
removeFile,
|
|
517
|
+
getFileUrl,
|
|
518
|
+
getThumbnailUrl,
|
|
519
|
+
getPreviewUrl,
|
|
520
|
+
addAttachment,
|
|
521
|
+
removeAttachment,
|
|
522
|
+
clearAttachments
|
|
503
523
|
}),
|
|
504
524
|
[
|
|
505
525
|
threadId,
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
connectionStatus,
|
|
510
|
-
subscribeToEvent,
|
|
526
|
+
preload,
|
|
527
|
+
live,
|
|
528
|
+
useWorkblocks,
|
|
511
529
|
depth,
|
|
512
530
|
includeSilent,
|
|
513
|
-
|
|
531
|
+
sendMessage2,
|
|
532
|
+
stopExecution,
|
|
533
|
+
deleteMessage,
|
|
534
|
+
subscribeToEvent,
|
|
535
|
+
addFiles,
|
|
536
|
+
removeFile,
|
|
537
|
+
getFileUrl,
|
|
538
|
+
getThumbnailUrl,
|
|
539
|
+
getPreviewUrl,
|
|
540
|
+
addAttachment,
|
|
541
|
+
removeAttachment,
|
|
542
|
+
clearAttachments
|
|
514
543
|
]
|
|
515
544
|
);
|
|
516
|
-
|
|
545
|
+
const messagesValue = useMemo(
|
|
546
|
+
() => ({
|
|
547
|
+
messages,
|
|
548
|
+
workblocks,
|
|
549
|
+
loading
|
|
550
|
+
}),
|
|
551
|
+
[messages, workblocks, loading]
|
|
552
|
+
);
|
|
553
|
+
const filesValue = useMemo(
|
|
554
|
+
() => ({
|
|
555
|
+
files
|
|
556
|
+
}),
|
|
557
|
+
[files]
|
|
558
|
+
);
|
|
559
|
+
const connectionValue = useMemo(
|
|
560
|
+
() => ({
|
|
561
|
+
connectionStatus,
|
|
562
|
+
error
|
|
563
|
+
}),
|
|
564
|
+
[connectionStatus, error]
|
|
565
|
+
);
|
|
566
|
+
const attachmentsValue = useMemo(
|
|
567
|
+
() => ({
|
|
568
|
+
attachments
|
|
569
|
+
}),
|
|
570
|
+
[attachments]
|
|
571
|
+
);
|
|
572
|
+
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
573
|
}
|
|
518
|
-
function
|
|
519
|
-
const context = useContext(
|
|
574
|
+
function useStaticContext() {
|
|
575
|
+
const context = useContext(StaticContext);
|
|
520
576
|
if (!context) {
|
|
521
|
-
throw new Error("
|
|
577
|
+
throw new Error("useThread must be used within a ThreadProvider");
|
|
522
578
|
}
|
|
523
579
|
return context;
|
|
524
580
|
}
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
// src/utils/workblocks.ts
|
|
530
|
-
function transformToWorkblocks(messages) {
|
|
531
|
-
if (messages.length === 0) {
|
|
532
|
-
return [];
|
|
581
|
+
function useMessagesContext() {
|
|
582
|
+
const context = useContext(MessagesContext);
|
|
583
|
+
if (!context) {
|
|
584
|
+
throw new Error("useThread must be used within a ThreadProvider");
|
|
533
585
|
}
|
|
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
|
-
}
|
|
586
|
+
return context;
|
|
587
|
+
}
|
|
588
|
+
function useFilesContext() {
|
|
589
|
+
const context = useContext(FilesContext);
|
|
590
|
+
if (!context) {
|
|
591
|
+
throw new Error("useThread must be used within a ThreadProvider");
|
|
607
592
|
}
|
|
608
|
-
return
|
|
593
|
+
return context;
|
|
609
594
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
function useThread(options = {}) {
|
|
613
|
-
const {
|
|
614
|
-
useWorkblocks = true
|
|
615
|
-
} = options;
|
|
616
|
-
const context = useContext(ThreadContext);
|
|
595
|
+
function useConnectionContext() {
|
|
596
|
+
const context = useContext(ConnectionContext);
|
|
617
597
|
if (!context) {
|
|
618
|
-
throw new Error(
|
|
619
|
-
'useThread must be used within a ThreadProvider. Wrap your component with <ThreadProvider threadId="...">.'
|
|
620
|
-
);
|
|
598
|
+
throw new Error("useThread must be used within a ThreadProvider");
|
|
621
599
|
}
|
|
622
|
-
|
|
623
|
-
const transformedMessages = useMemo(() => {
|
|
624
|
-
return useWorkblocks ? transformToWorkblocks(messages) : messages;
|
|
625
|
-
}, [messages, useWorkblocks]);
|
|
626
|
-
return transformedMessages;
|
|
600
|
+
return context;
|
|
627
601
|
}
|
|
628
|
-
function
|
|
629
|
-
const context = useContext(
|
|
602
|
+
function useAttachmentsContext() {
|
|
603
|
+
const context = useContext(AttachmentsContext);
|
|
630
604
|
if (!context) {
|
|
631
|
-
throw new Error(
|
|
632
|
-
|
|
605
|
+
throw new Error("useThread must be used within a ThreadProvider");
|
|
606
|
+
}
|
|
607
|
+
return context;
|
|
608
|
+
}
|
|
609
|
+
function useThreadContextInternal() {
|
|
610
|
+
const static_ = useStaticContext();
|
|
611
|
+
const messages_ = useMessagesContext();
|
|
612
|
+
const files_ = useFilesContext();
|
|
613
|
+
const attachments_ = useAttachmentsContext();
|
|
614
|
+
const connection_ = useConnectionContext();
|
|
615
|
+
return {
|
|
616
|
+
threadId: static_.threadId,
|
|
617
|
+
options: static_.options,
|
|
618
|
+
sendMessage: static_.sendMessage,
|
|
619
|
+
stopExecution: static_.stopExecution,
|
|
620
|
+
deleteMessage: static_.deleteMessage,
|
|
621
|
+
subscribeToEvent: static_.subscribeToEvent,
|
|
622
|
+
onEvent: static_.subscribeToEvent,
|
|
623
|
+
// File management (uploads to filesystem)
|
|
624
|
+
addFiles: static_.addFiles,
|
|
625
|
+
removeFile: static_.removeFile,
|
|
626
|
+
getFileUrl: static_.getFileUrl,
|
|
627
|
+
getThumbnailUrl: static_.getThumbnailUrl,
|
|
628
|
+
getPreviewUrl: static_.getPreviewUrl,
|
|
629
|
+
// Attachment management (sent inline with messages)
|
|
630
|
+
addAttachment: static_.addAttachment,
|
|
631
|
+
removeAttachment: static_.removeAttachment,
|
|
632
|
+
clearAttachments: static_.clearAttachments,
|
|
633
|
+
attachments: attachments_.attachments,
|
|
634
|
+
// Messages
|
|
635
|
+
messages: messages_.messages,
|
|
636
|
+
workblocks: messages_.workblocks,
|
|
637
|
+
loading: messages_.loading,
|
|
638
|
+
isLoading: messages_.loading,
|
|
639
|
+
files: files_.files,
|
|
640
|
+
connectionStatus: connection_.connectionStatus,
|
|
641
|
+
status: connection_.connectionStatus,
|
|
642
|
+
error: connection_.error
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
var hasWarnedAboutUseThreadContext = false;
|
|
646
|
+
function useThreadContext() {
|
|
647
|
+
if (!hasWarnedAboutUseThreadContext && process.env.NODE_ENV !== "production") {
|
|
648
|
+
hasWarnedAboutUseThreadContext = true;
|
|
649
|
+
console.warn(
|
|
650
|
+
"[DEPRECATED] useThreadContext() is deprecated.\nUse: const { messages, sendMessage, ... } = useThread()"
|
|
633
651
|
);
|
|
634
652
|
}
|
|
635
|
-
|
|
653
|
+
return useThreadContextInternal();
|
|
654
|
+
}
|
|
655
|
+
function useThreadId() {
|
|
656
|
+
return useStaticContext().threadId;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/hooks/useThread.ts
|
|
660
|
+
var hasWarnedAboutArrayUsage = false;
|
|
661
|
+
function useThread() {
|
|
662
|
+
const context = useThreadContextInternal();
|
|
663
|
+
if (process.env.NODE_ENV !== "production") {
|
|
664
|
+
const arrayMethods = ["map", "filter", "forEach", "find", "some", "every", "reduce", "length", "push", "pop", "slice", "splice"];
|
|
665
|
+
return new Proxy(context, {
|
|
666
|
+
get(target, prop) {
|
|
667
|
+
if (arrayMethods.includes(prop) && !hasWarnedAboutArrayUsage) {
|
|
668
|
+
hasWarnedAboutArrayUsage = true;
|
|
669
|
+
console.warn(
|
|
670
|
+
"[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."
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
return target[prop];
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
return context;
|
|
678
|
+
}
|
|
679
|
+
function onThreadEvent(type, callback) {
|
|
680
|
+
const { subscribeToEvent } = useThreadContextInternal();
|
|
636
681
|
useEffect(() => {
|
|
637
682
|
const unsubscribe = subscribeToEvent(type, callback);
|
|
638
683
|
return unsubscribe;
|
|
639
684
|
}, [subscribeToEvent, type, callback]);
|
|
640
685
|
}
|
|
641
686
|
function useThreadEvent(type) {
|
|
642
|
-
const
|
|
687
|
+
const { subscribeToEvent } = useThreadContextInternal();
|
|
643
688
|
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
689
|
useEffect(() => {
|
|
651
690
|
const unsubscribe = subscribeToEvent(type, (data) => {
|
|
652
691
|
setEventData(data);
|
|
@@ -655,7 +694,14 @@ function useThreadEvent(type) {
|
|
|
655
694
|
}, [subscribeToEvent, type]);
|
|
656
695
|
return eventData;
|
|
657
696
|
}
|
|
697
|
+
var hasWarned = false;
|
|
658
698
|
function useSendMessage() {
|
|
699
|
+
if (!hasWarned && process.env.NODE_ENV !== "production") {
|
|
700
|
+
hasWarned = true;
|
|
701
|
+
console.warn(
|
|
702
|
+
'[DEPRECATED] useSendMessage() is deprecated.\nUse: const { sendMessage } = useThread()\nThen: await sendMessage({ role: "user", content: "Hello!" })'
|
|
703
|
+
);
|
|
704
|
+
}
|
|
659
705
|
let context;
|
|
660
706
|
try {
|
|
661
707
|
context = useThreadContext();
|
|
@@ -670,7 +716,14 @@ function useSendMessage() {
|
|
|
670
716
|
[threadId]
|
|
671
717
|
);
|
|
672
718
|
}
|
|
719
|
+
var hasWarned2 = false;
|
|
673
720
|
function useStopThread() {
|
|
721
|
+
if (!hasWarned2 && process.env.NODE_ENV !== "production") {
|
|
722
|
+
hasWarned2 = true;
|
|
723
|
+
console.warn(
|
|
724
|
+
"[DEPRECATED] useStopThread() is deprecated.\nUse: const { stopExecution } = useThread()"
|
|
725
|
+
);
|
|
726
|
+
}
|
|
674
727
|
let context;
|
|
675
728
|
try {
|
|
676
729
|
context = useThreadContext();
|