@copilotz/chat-adapter 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 +27 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +1614 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1614 @@
|
|
|
1
|
+
// src/CopilotzChat.tsx
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { ChatUI, ChatUserContextProvider } from "@copilotz/chat-ui";
|
|
4
|
+
|
|
5
|
+
// ../node_modules/lucide-react/dist/esm/createLucideIcon.js
|
|
6
|
+
import { forwardRef as forwardRef2, createElement as createElement2 } from "react";
|
|
7
|
+
|
|
8
|
+
// ../node_modules/lucide-react/dist/esm/shared/src/utils.js
|
|
9
|
+
var toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
10
|
+
var toCamelCase = (string) => string.replace(
|
|
11
|
+
/^([A-Z])|[\s-_]+(\w)/g,
|
|
12
|
+
(match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase()
|
|
13
|
+
);
|
|
14
|
+
var toPascalCase = (string) => {
|
|
15
|
+
const camelCase = toCamelCase(string);
|
|
16
|
+
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
|
|
17
|
+
};
|
|
18
|
+
var mergeClasses = (...classes) => classes.filter((className, index, array) => {
|
|
19
|
+
return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
|
|
20
|
+
}).join(" ").trim();
|
|
21
|
+
var hasA11yProp = (props) => {
|
|
22
|
+
for (const prop in props) {
|
|
23
|
+
if (prop.startsWith("aria-") || prop === "role" || prop === "title") {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ../node_modules/lucide-react/dist/esm/Icon.js
|
|
30
|
+
import { forwardRef, createElement } from "react";
|
|
31
|
+
|
|
32
|
+
// ../node_modules/lucide-react/dist/esm/defaultAttributes.js
|
|
33
|
+
var defaultAttributes = {
|
|
34
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
35
|
+
width: 24,
|
|
36
|
+
height: 24,
|
|
37
|
+
viewBox: "0 0 24 24",
|
|
38
|
+
fill: "none",
|
|
39
|
+
stroke: "currentColor",
|
|
40
|
+
strokeWidth: 2,
|
|
41
|
+
strokeLinecap: "round",
|
|
42
|
+
strokeLinejoin: "round"
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ../node_modules/lucide-react/dist/esm/Icon.js
|
|
46
|
+
var Icon = forwardRef(
|
|
47
|
+
({
|
|
48
|
+
color = "currentColor",
|
|
49
|
+
size = 24,
|
|
50
|
+
strokeWidth = 2,
|
|
51
|
+
absoluteStrokeWidth,
|
|
52
|
+
className = "",
|
|
53
|
+
children,
|
|
54
|
+
iconNode,
|
|
55
|
+
...rest
|
|
56
|
+
}, ref) => createElement(
|
|
57
|
+
"svg",
|
|
58
|
+
{
|
|
59
|
+
ref,
|
|
60
|
+
...defaultAttributes,
|
|
61
|
+
width: size,
|
|
62
|
+
height: size,
|
|
63
|
+
stroke: color,
|
|
64
|
+
strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
|
|
65
|
+
className: mergeClasses("lucide", className),
|
|
66
|
+
...!children && !hasA11yProp(rest) && { "aria-hidden": "true" },
|
|
67
|
+
...rest
|
|
68
|
+
},
|
|
69
|
+
[
|
|
70
|
+
...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),
|
|
71
|
+
...Array.isArray(children) ? children : [children]
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// ../node_modules/lucide-react/dist/esm/createLucideIcon.js
|
|
77
|
+
var createLucideIcon = (iconName, iconNode) => {
|
|
78
|
+
const Component = forwardRef2(
|
|
79
|
+
({ className, ...props }, ref) => createElement2(Icon, {
|
|
80
|
+
ref,
|
|
81
|
+
iconNode,
|
|
82
|
+
className: mergeClasses(
|
|
83
|
+
`lucide-${toKebabCase(toPascalCase(iconName))}`,
|
|
84
|
+
`lucide-${iconName}`,
|
|
85
|
+
className
|
|
86
|
+
),
|
|
87
|
+
...props
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
Component.displayName = toPascalCase(iconName);
|
|
91
|
+
return Component;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ../node_modules/lucide-react/dist/esm/icons/user.js
|
|
95
|
+
var __iconNode = [
|
|
96
|
+
["path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2", key: "975kel" }],
|
|
97
|
+
["circle", { cx: "12", cy: "7", r: "4", key: "17ys0d" }]
|
|
98
|
+
];
|
|
99
|
+
var User = createLucideIcon("user", __iconNode);
|
|
100
|
+
|
|
101
|
+
// src/useCopilotzChat.ts
|
|
102
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
103
|
+
|
|
104
|
+
// src/copilotzService.ts
|
|
105
|
+
var rawBaseValue = import.meta.env?.VITE_API_URL;
|
|
106
|
+
var rawBase = typeof rawBaseValue === "string" && rawBaseValue.length > 0 ? rawBaseValue : "/api";
|
|
107
|
+
var normalizedBase = rawBase.replace(/\/$/, "");
|
|
108
|
+
var API_BASE = normalizedBase.startsWith("http") || normalizedBase.startsWith("/") ? normalizedBase : `/${normalizedBase}`;
|
|
109
|
+
var apiUrl = (path) => `${API_BASE}${path}`;
|
|
110
|
+
var runtimeProcess = typeof process !== "undefined" ? process : void 0;
|
|
111
|
+
var API_KEY = (() => {
|
|
112
|
+
const env = import.meta.env ?? {};
|
|
113
|
+
const candidates = [
|
|
114
|
+
env.VITE_API_KEY,
|
|
115
|
+
env.VITE_COPILOTZ_API_KEY,
|
|
116
|
+
runtimeProcess?.env?.COPILOTZ_API_KEY,
|
|
117
|
+
runtimeProcess?.env?.API_KEY
|
|
118
|
+
];
|
|
119
|
+
return candidates.find((value) => typeof value === "string" && value.length > 0);
|
|
120
|
+
})();
|
|
121
|
+
var withAuthHeaders = (headers = {}) => {
|
|
122
|
+
if (API_KEY) {
|
|
123
|
+
return { ...headers, Authorization: `Bearer ${API_KEY}` };
|
|
124
|
+
}
|
|
125
|
+
return headers;
|
|
126
|
+
};
|
|
127
|
+
var SSE_LINE_BREAK = "\n\n";
|
|
128
|
+
var appendChunk = (buffer, chunk) => {
|
|
129
|
+
if (!buffer) return chunk;
|
|
130
|
+
if (!chunk) return buffer;
|
|
131
|
+
if (chunk.startsWith(buffer)) return chunk;
|
|
132
|
+
if (buffer.startsWith(chunk)) return buffer;
|
|
133
|
+
const maxOverlap = Math.min(buffer.length, chunk.length);
|
|
134
|
+
for (let i = maxOverlap; i > 0; i--) {
|
|
135
|
+
if (buffer.endsWith(chunk.slice(0, i))) {
|
|
136
|
+
return buffer + chunk.slice(i);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return buffer + chunk;
|
|
140
|
+
};
|
|
141
|
+
var toAttachmentPayload = (attachments) => {
|
|
142
|
+
if (!attachments || attachments.length === 0) return void 0;
|
|
143
|
+
return attachments.map((att) => {
|
|
144
|
+
const base = {
|
|
145
|
+
kind: att.kind,
|
|
146
|
+
dataUrl: att.dataUrl,
|
|
147
|
+
mimeType: att.mimeType,
|
|
148
|
+
fileName: att.fileName
|
|
149
|
+
};
|
|
150
|
+
if (att.kind === "audio" || att.kind === "video") {
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
durationMs: att.durationMs,
|
|
154
|
+
...att.kind === "video" && "poster" in att ? { poster: att.poster } : {}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return base;
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
var base64FromUint8 = (bytes) => {
|
|
161
|
+
let binary = "";
|
|
162
|
+
const chunkSize = 32768;
|
|
163
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
164
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
165
|
+
binary += String.fromCharCode.apply(null, Array.from(chunk));
|
|
166
|
+
}
|
|
167
|
+
return btoa(binary);
|
|
168
|
+
};
|
|
169
|
+
var parseDataUrl = (dataUrl) => {
|
|
170
|
+
const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
171
|
+
if (!match) return null;
|
|
172
|
+
return { mime: match[1], base64: match[2] };
|
|
173
|
+
};
|
|
174
|
+
var dataUrlToArrayBuffer = (dataUrl) => {
|
|
175
|
+
const parsed = parseDataUrl(dataUrl);
|
|
176
|
+
if (!parsed) return new ArrayBuffer(0);
|
|
177
|
+
const binaryString = atob(parsed.base64);
|
|
178
|
+
const len = binaryString.length;
|
|
179
|
+
const bytes = new Uint8Array(len);
|
|
180
|
+
for (let i = 0; i < len; i++) {
|
|
181
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
182
|
+
}
|
|
183
|
+
return bytes.buffer;
|
|
184
|
+
};
|
|
185
|
+
var encodeWav16BitPCM = (audioBuffer) => {
|
|
186
|
+
const numChannels = audioBuffer.numberOfChannels;
|
|
187
|
+
const sampleRate = audioBuffer.sampleRate;
|
|
188
|
+
const numFrames = audioBuffer.length;
|
|
189
|
+
const bytesPerSample = 2;
|
|
190
|
+
const dataSize = numFrames * numChannels * bytesPerSample;
|
|
191
|
+
const buffer = new ArrayBuffer(44 + dataSize);
|
|
192
|
+
const view = new DataView(buffer);
|
|
193
|
+
const writeString = (offset2, str) => {
|
|
194
|
+
for (let i = 0; i < str.length; i++) {
|
|
195
|
+
view.setUint8(offset2 + i, str.charCodeAt(i));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
let offset = 0;
|
|
199
|
+
writeString(offset, "RIFF");
|
|
200
|
+
offset += 4;
|
|
201
|
+
view.setUint32(offset, 36 + dataSize, true);
|
|
202
|
+
offset += 4;
|
|
203
|
+
writeString(offset, "WAVE");
|
|
204
|
+
offset += 4;
|
|
205
|
+
writeString(offset, "fmt ");
|
|
206
|
+
offset += 4;
|
|
207
|
+
view.setUint32(offset, 16, true);
|
|
208
|
+
offset += 4;
|
|
209
|
+
view.setUint16(offset, 1, true);
|
|
210
|
+
offset += 2;
|
|
211
|
+
view.setUint16(offset, numChannels, true);
|
|
212
|
+
offset += 2;
|
|
213
|
+
view.setUint32(offset, sampleRate, true);
|
|
214
|
+
offset += 4;
|
|
215
|
+
view.setUint32(offset, sampleRate * numChannels * bytesPerSample, true);
|
|
216
|
+
offset += 4;
|
|
217
|
+
view.setUint16(offset, numChannels * bytesPerSample, true);
|
|
218
|
+
offset += 2;
|
|
219
|
+
view.setUint16(offset, 16, true);
|
|
220
|
+
offset += 2;
|
|
221
|
+
writeString(offset, "data");
|
|
222
|
+
offset += 4;
|
|
223
|
+
view.setUint32(offset, dataSize, true);
|
|
224
|
+
offset += 4;
|
|
225
|
+
const channelData = [];
|
|
226
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
227
|
+
channelData.push(audioBuffer.getChannelData(ch));
|
|
228
|
+
}
|
|
229
|
+
let idx = 0;
|
|
230
|
+
for (let i = 0; i < numFrames; i++) {
|
|
231
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
232
|
+
let sample = channelData[ch][i];
|
|
233
|
+
sample = Math.max(-1, Math.min(1, sample));
|
|
234
|
+
const s = sample < 0 ? sample * 32768 : sample * 32767;
|
|
235
|
+
view.setInt16(offset + idx, s, true);
|
|
236
|
+
idx += 2;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return new Uint8Array(buffer);
|
|
240
|
+
};
|
|
241
|
+
var convertAudioDataUrlToWavBase64 = async (dataUrl) => {
|
|
242
|
+
try {
|
|
243
|
+
const ab = dataUrlToArrayBuffer(dataUrl);
|
|
244
|
+
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
245
|
+
const audioBuffer = await ctx.decodeAudioData(ab.slice(0));
|
|
246
|
+
const wavBytes = encodeWav16BitPCM(audioBuffer);
|
|
247
|
+
return base64FromUint8(wavBytes);
|
|
248
|
+
} catch (_err) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
async function runCopilotzStream(options) {
|
|
253
|
+
const {
|
|
254
|
+
threadId,
|
|
255
|
+
threadExternalId,
|
|
256
|
+
content,
|
|
257
|
+
user,
|
|
258
|
+
attachments,
|
|
259
|
+
metadata,
|
|
260
|
+
threadMetadata,
|
|
261
|
+
toolCalls,
|
|
262
|
+
onToken,
|
|
263
|
+
onMessageEvent,
|
|
264
|
+
onAssetEvent,
|
|
265
|
+
signal
|
|
266
|
+
} = options;
|
|
267
|
+
const controller = new AbortController();
|
|
268
|
+
if (signal) {
|
|
269
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
|
|
270
|
+
}
|
|
271
|
+
const audioAttachments = attachments?.filter((att) => att.kind === "audio") ?? [];
|
|
272
|
+
const nonAudioAttachments = attachments?.filter((att) => att.kind !== "audio") ?? [];
|
|
273
|
+
const attachmentPayload = toAttachmentPayload(nonAudioAttachments);
|
|
274
|
+
const normalizedToolCalls = toolCalls?.map((call) => ({
|
|
275
|
+
id: call.id ?? crypto.randomUUID(),
|
|
276
|
+
name: call.name,
|
|
277
|
+
args: call.args ?? {}
|
|
278
|
+
})) ?? [];
|
|
279
|
+
const metadataToolCalls = normalizedToolCalls.length > 0 ? normalizedToolCalls.map((tc) => ({
|
|
280
|
+
id: tc.id ?? void 0,
|
|
281
|
+
name: tc.name,
|
|
282
|
+
args: JSON.stringify(tc.args ?? {})
|
|
283
|
+
})) : void 0;
|
|
284
|
+
const baseMetadata = {
|
|
285
|
+
...metadata ?? {},
|
|
286
|
+
...attachmentPayload ? { attachments: attachmentPayload } : {},
|
|
287
|
+
...metadataToolCalls ? { toolCalls: metadataToolCalls } : {},
|
|
288
|
+
userExternalId: user.externalId
|
|
289
|
+
};
|
|
290
|
+
const messageMetadata = Object.keys(baseMetadata).length > 0 ? baseMetadata : void 0;
|
|
291
|
+
const senderMetadata = {
|
|
292
|
+
...user.metadata ?? {},
|
|
293
|
+
...user.email ? { email: user.email } : {}
|
|
294
|
+
};
|
|
295
|
+
const mergedThreadMetadata = {
|
|
296
|
+
...threadMetadata ?? {}
|
|
297
|
+
};
|
|
298
|
+
if (mergedThreadMetadata.userExternalId === void 0) {
|
|
299
|
+
mergedThreadMetadata.userExternalId = user.externalId;
|
|
300
|
+
}
|
|
301
|
+
const threadName = mergedThreadMetadata.name ?? null;
|
|
302
|
+
const { name: _threadName, ...restThreadMetadata } = mergedThreadMetadata;
|
|
303
|
+
const threadPayload = threadId || threadExternalId || threadName || Object.keys(restThreadMetadata).length > 0 ? {
|
|
304
|
+
id: threadId ?? null,
|
|
305
|
+
externalId: threadExternalId ?? null,
|
|
306
|
+
name: threadName,
|
|
307
|
+
participants: ["assistant"],
|
|
308
|
+
metadata: Object.keys(restThreadMetadata).length > 0 ? restThreadMetadata : null
|
|
309
|
+
} : void 0;
|
|
310
|
+
const preparedAudioParts = [];
|
|
311
|
+
for (const audioAtt of audioAttachments) {
|
|
312
|
+
if (!audioAtt.dataUrl) continue;
|
|
313
|
+
const parsed = parseDataUrl(audioAtt.dataUrl);
|
|
314
|
+
if (parsed && (parsed.mime.includes("wav") || parsed.mime.includes("mp3") || parsed.mime.includes("mpeg"))) {
|
|
315
|
+
preparedAudioParts.push({
|
|
316
|
+
type: "audio",
|
|
317
|
+
dataBase64: parsed.base64,
|
|
318
|
+
mimeType: parsed.mime.includes("wav") ? "audio/wav" : "audio/mp3"
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const wavBase64 = await convertAudioDataUrlToWavBase64(audioAtt.dataUrl);
|
|
323
|
+
if (wavBase64) {
|
|
324
|
+
preparedAudioParts.push({
|
|
325
|
+
type: "audio",
|
|
326
|
+
dataBase64: wavBase64,
|
|
327
|
+
mimeType: "audio/wav"
|
|
328
|
+
});
|
|
329
|
+
} else {
|
|
330
|
+
preparedAudioParts.push({
|
|
331
|
+
type: "audio",
|
|
332
|
+
url: audioAtt.dataUrl,
|
|
333
|
+
mimeType: audioAtt.mimeType || "audio/webm"
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const contentParts = (() => {
|
|
338
|
+
const parts = [];
|
|
339
|
+
const text = typeof content === "string" && content.trim().length > 0 ? content : "";
|
|
340
|
+
parts.push({ type: "text", text });
|
|
341
|
+
for (const p of preparedAudioParts) parts.push(p);
|
|
342
|
+
if (parts.length === 1 && parts[0].type === "text") return parts[0].text;
|
|
343
|
+
return parts;
|
|
344
|
+
})();
|
|
345
|
+
const payload = {
|
|
346
|
+
content: contentParts,
|
|
347
|
+
sender: {
|
|
348
|
+
type: normalizedToolCalls.length > 0 ? "agent" : "user",
|
|
349
|
+
externalId: user.externalId,
|
|
350
|
+
id: normalizedToolCalls.length > 0 ? "assistant" : void 0,
|
|
351
|
+
name: normalizedToolCalls.length > 0 ? "assistant" : user.name ?? null,
|
|
352
|
+
metadata: Object.keys(senderMetadata).length > 0 ? senderMetadata : null
|
|
353
|
+
},
|
|
354
|
+
metadata: messageMetadata ?? null,
|
|
355
|
+
thread: threadPayload ?? null,
|
|
356
|
+
toolCalls: normalizedToolCalls.length > 0 ? normalizedToolCalls : null
|
|
357
|
+
};
|
|
358
|
+
const response = await fetch(apiUrl("/v1/providers/web"), {
|
|
359
|
+
method: "POST",
|
|
360
|
+
headers: {
|
|
361
|
+
"Content-Type": "application/json"
|
|
362
|
+
},
|
|
363
|
+
body: JSON.stringify(payload),
|
|
364
|
+
signal: controller.signal
|
|
365
|
+
});
|
|
366
|
+
if (!response.ok || !response.body) {
|
|
367
|
+
const errorText = await response.text().catch(() => response.statusText);
|
|
368
|
+
throw new Error(errorText || "Failed to run Copilotz agent");
|
|
369
|
+
}
|
|
370
|
+
const reader = response.body.getReader();
|
|
371
|
+
const decoder = new TextDecoder("utf-8");
|
|
372
|
+
let buffer = "";
|
|
373
|
+
let aggregatedText = "";
|
|
374
|
+
const collectedMessages = [];
|
|
375
|
+
let collectedMedia = null;
|
|
376
|
+
const processEvent = (eventChunk) => {
|
|
377
|
+
if (!eventChunk.trim()) return;
|
|
378
|
+
const lines = eventChunk.split("\n");
|
|
379
|
+
let eventType = "message";
|
|
380
|
+
let dataRaw = "";
|
|
381
|
+
for (const line of lines) {
|
|
382
|
+
if (line.startsWith("event:")) {
|
|
383
|
+
eventType = line.slice(6).trim();
|
|
384
|
+
} else if (line.startsWith("data:")) {
|
|
385
|
+
dataRaw += line.slice(5).trim();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (!dataRaw) return;
|
|
389
|
+
let payload2;
|
|
390
|
+
try {
|
|
391
|
+
payload2 = JSON.parse(dataRaw);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.warn("copilotzService: failed to parse SSE payload", error, dataRaw);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
switch (eventType) {
|
|
397
|
+
case "TOKEN": {
|
|
398
|
+
const chunk = typeof payload2?.payload?.token === "string" ? payload2.payload.token : typeof payload2?.token === "string" ? payload2.token : "";
|
|
399
|
+
if (chunk) {
|
|
400
|
+
aggregatedText = appendChunk(aggregatedText, chunk);
|
|
401
|
+
}
|
|
402
|
+
const isComplete = Boolean(
|
|
403
|
+
(payload2 && payload2.payload && payload2.payload.isComplete) ?? payload2?.isComplete
|
|
404
|
+
);
|
|
405
|
+
if (chunk || isComplete) {
|
|
406
|
+
onToken?.(aggregatedText, isComplete, payload2);
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
case "MESSAGE": {
|
|
411
|
+
collectedMessages.push(payload2);
|
|
412
|
+
onMessageEvent?.(payload2);
|
|
413
|
+
const senderType = payload2?.payload?.senderType ?? payload2?.payload?.sender?.type;
|
|
414
|
+
if (senderType === "agent" && typeof payload2?.payload?.content === "string") {
|
|
415
|
+
aggregatedText = payload2.payload.content;
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
case "TOOL_CALL": {
|
|
420
|
+
onMessageEvent?.(payload2);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
case "ASSET_CREATED": {
|
|
424
|
+
const assetPayload = payload2 && typeof payload2 === "object" && "payload" in payload2 ? payload2.payload : payload2;
|
|
425
|
+
if (assetPayload?.dataUrl) {
|
|
426
|
+
collectedMedia = {
|
|
427
|
+
[assetPayload.assetId || "0"]: assetPayload.dataUrl
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
onAssetEvent?.(assetPayload);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
case "ERROR":
|
|
434
|
+
throw new Error(payload2?.error || "Copilotz stream error");
|
|
435
|
+
default:
|
|
436
|
+
onMessageEvent?.({ type: eventType, payload: payload2 });
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
while (true) {
|
|
440
|
+
const { value, done } = await reader.read();
|
|
441
|
+
if (done) break;
|
|
442
|
+
buffer += decoder.decode(value, { stream: true });
|
|
443
|
+
if (buffer.includes("\r")) {
|
|
444
|
+
buffer = buffer.replace(/\r/g, "");
|
|
445
|
+
}
|
|
446
|
+
let eventBoundary = buffer.indexOf(SSE_LINE_BREAK);
|
|
447
|
+
while (eventBoundary >= 0) {
|
|
448
|
+
const chunk = buffer.slice(0, eventBoundary);
|
|
449
|
+
buffer = buffer.slice(eventBoundary + SSE_LINE_BREAK.length);
|
|
450
|
+
processEvent(chunk);
|
|
451
|
+
eventBoundary = buffer.indexOf(SSE_LINE_BREAK);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (buffer.length > 0) {
|
|
455
|
+
processEvent(buffer);
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
text: aggregatedText,
|
|
459
|
+
messages: collectedMessages,
|
|
460
|
+
media: collectedMedia
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
async function fetchThreads(userId) {
|
|
464
|
+
const params = new URLSearchParams();
|
|
465
|
+
params.set("filters", JSON.stringify({ "metadata.userExternalId": userId }));
|
|
466
|
+
params.set("sort", "-updatedAt");
|
|
467
|
+
const res = await fetch(apiUrl(`/v1/rest/threads?${params.toString()}`), {
|
|
468
|
+
headers: withAuthHeaders({ Accept: "application/json" })
|
|
469
|
+
});
|
|
470
|
+
if (!res.ok) {
|
|
471
|
+
const errorText = await res.text().catch(() => res.statusText);
|
|
472
|
+
throw new Error(errorText || `Failed to load threads (${res.status})`);
|
|
473
|
+
}
|
|
474
|
+
const { data } = await res.json();
|
|
475
|
+
if (!Array.isArray(data)) {
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
return data;
|
|
479
|
+
}
|
|
480
|
+
async function fetchThreadMessages(threadId) {
|
|
481
|
+
const params = new URLSearchParams();
|
|
482
|
+
params.set("filters", JSON.stringify({ threadId }));
|
|
483
|
+
params.set("sort", "createdAt:asc");
|
|
484
|
+
const res = await fetch(apiUrl(`/v1/rest/messages?${params.toString()}`), {
|
|
485
|
+
headers: withAuthHeaders({ Accept: "application/json" })
|
|
486
|
+
});
|
|
487
|
+
if (!res.ok) {
|
|
488
|
+
const errorText = await res.text().catch(() => res.statusText);
|
|
489
|
+
throw new Error(errorText || `Failed to load thread messages (${res.status})`);
|
|
490
|
+
}
|
|
491
|
+
const { data } = await res.json();
|
|
492
|
+
if (!Array.isArray(data)) {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
return data;
|
|
496
|
+
}
|
|
497
|
+
async function updateThread(threadId, updates) {
|
|
498
|
+
const res = await fetch(apiUrl(`/v1/rest/threads/${threadId}`), {
|
|
499
|
+
method: "PUT",
|
|
500
|
+
headers: withAuthHeaders({ "Content-Type": "application/json", Accept: "application/json" }),
|
|
501
|
+
body: JSON.stringify(updates)
|
|
502
|
+
});
|
|
503
|
+
if (!res.ok) {
|
|
504
|
+
const errorText = await res.text().catch(() => res.statusText);
|
|
505
|
+
throw new Error(errorText || `Failed to update thread (${res.status})`);
|
|
506
|
+
}
|
|
507
|
+
const data = await res.json();
|
|
508
|
+
return data?.body ?? data;
|
|
509
|
+
}
|
|
510
|
+
async function deleteMessagesByThreadId(threadId) {
|
|
511
|
+
const params = new URLSearchParams();
|
|
512
|
+
params.set("filters", JSON.stringify({ threadId }));
|
|
513
|
+
const res = await fetch(apiUrl(`/v1/rest/messages?${params.toString()}`), {
|
|
514
|
+
headers: withAuthHeaders({ Accept: "application/json" })
|
|
515
|
+
});
|
|
516
|
+
if (!res.ok) {
|
|
517
|
+
console.warn("Could not fetch messages for deletion:", res.status);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const { data } = await res.json();
|
|
521
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
for (const msg of data) {
|
|
525
|
+
if (msg?.id) {
|
|
526
|
+
try {
|
|
527
|
+
await fetch(apiUrl(`/v1/rest/messages/${msg.id}`), {
|
|
528
|
+
method: "DELETE",
|
|
529
|
+
headers: withAuthHeaders({ Accept: "application/json" })
|
|
530
|
+
});
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function deleteThread(threadId) {
|
|
537
|
+
await deleteMessagesByThreadId(threadId);
|
|
538
|
+
const res = await fetch(apiUrl(`/v1/rest/threads/${threadId}`), {
|
|
539
|
+
method: "DELETE",
|
|
540
|
+
headers: withAuthHeaders({ Accept: "application/json" })
|
|
541
|
+
});
|
|
542
|
+
if (!res.ok) {
|
|
543
|
+
const errorText = await res.text().catch(() => res.statusText);
|
|
544
|
+
throw new Error(errorText || `Failed to delete thread (${res.status})`);
|
|
545
|
+
}
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
var copilotzService = {
|
|
549
|
+
runCopilotzStream,
|
|
550
|
+
fetchThreads,
|
|
551
|
+
fetchThreadMessages,
|
|
552
|
+
updateThread,
|
|
553
|
+
deleteThread
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/assetsService.ts
|
|
557
|
+
var rawBaseValue2 = import.meta.env?.VITE_API_URL;
|
|
558
|
+
var rawBase2 = typeof rawBaseValue2 === "string" && rawBaseValue2.length > 0 ? rawBaseValue2 : "/api";
|
|
559
|
+
var normalizedBase2 = rawBase2.replace(/\/$/, "");
|
|
560
|
+
var API_BASE2 = normalizedBase2.startsWith("http") || normalizedBase2.startsWith("/") ? normalizedBase2 : `/${normalizedBase2}`;
|
|
561
|
+
var apiUrl2 = (path) => `${API_BASE2}${path}`;
|
|
562
|
+
var extractAssetId = (refOrId) => refOrId.startsWith("asset://") ? refOrId.slice("asset://".length) : refOrId;
|
|
563
|
+
async function getAssetDataUrl(refOrId) {
|
|
564
|
+
const id = extractAssetId(refOrId);
|
|
565
|
+
const res = await fetch(apiUrl2(`/v1/assets/${encodeURIComponent(id)}?format=dataUrl`), {
|
|
566
|
+
method: "GET",
|
|
567
|
+
headers: { Accept: "application/json" }
|
|
568
|
+
});
|
|
569
|
+
if (!res.ok) {
|
|
570
|
+
const text = await res.text().catch(() => res.statusText);
|
|
571
|
+
throw new Error(text || `Failed to fetch asset ${refOrId}`);
|
|
572
|
+
}
|
|
573
|
+
const data = await res.json();
|
|
574
|
+
if (!data?.dataUrl) {
|
|
575
|
+
throw new Error(data?.error || `Asset ${refOrId} has no dataUrl`);
|
|
576
|
+
}
|
|
577
|
+
return { dataUrl: data.dataUrl, mime: data.mime, assetId: data.assetId };
|
|
578
|
+
}
|
|
579
|
+
async function resolveAssetsInMessages(messages) {
|
|
580
|
+
const resolved = [];
|
|
581
|
+
for (const msg of messages) {
|
|
582
|
+
const meta = msg.metadata ?? void 0;
|
|
583
|
+
const attachments = Array.isArray(meta?.attachments) ? meta.attachments : void 0;
|
|
584
|
+
if (!attachments || attachments.length === 0) {
|
|
585
|
+
resolved.push(msg);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const newAttachments = [];
|
|
589
|
+
for (const att of attachments) {
|
|
590
|
+
const assetRef = typeof att?.assetRef === "string" ? att.assetRef : void 0;
|
|
591
|
+
if (assetRef) {
|
|
592
|
+
try {
|
|
593
|
+
const { dataUrl, mime } = await getAssetDataUrl(assetRef);
|
|
594
|
+
const kind = typeof att.kind === "string" ? att.kind : "image";
|
|
595
|
+
newAttachments.push({
|
|
596
|
+
kind,
|
|
597
|
+
dataUrl,
|
|
598
|
+
mimeType: typeof att.mimeType === "string" ? att.mimeType : mime ?? void 0
|
|
599
|
+
});
|
|
600
|
+
} catch {
|
|
601
|
+
newAttachments.push(att);
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
newAttachments.push(att);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const newMeta = { ...meta ?? {}, attachments: newAttachments };
|
|
608
|
+
resolved.push({ ...msg, metadata: newMeta });
|
|
609
|
+
}
|
|
610
|
+
return resolved;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/useCopilotzChat.ts
|
|
614
|
+
var nowTs = () => Date.now();
|
|
615
|
+
var generateId = () => globalThis.crypto?.randomUUID?.() ?? `id-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
616
|
+
var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError" || typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
617
|
+
var convertServerMessage = (msg) => {
|
|
618
|
+
const timestamp = msg.createdAt ? new Date(msg.createdAt).getTime() : nowTs();
|
|
619
|
+
const metadata = msg.metadata ?? void 0;
|
|
620
|
+
const attachmentsMeta = Array.isArray(metadata?.attachments) ? metadata.attachments : [];
|
|
621
|
+
const attachments = attachmentsMeta.flatMap((att) => {
|
|
622
|
+
const kind = typeof att.kind === "string" ? att.kind : void 0;
|
|
623
|
+
const dataUrl = typeof att.dataUrl === "string" ? att.dataUrl : void 0;
|
|
624
|
+
const mimeType = typeof att.mimeType === "string" ? att.mimeType : void 0;
|
|
625
|
+
if (!dataUrl) return [];
|
|
626
|
+
if (kind === "image") {
|
|
627
|
+
return [{ kind: "image", dataUrl, mimeType: mimeType ?? "image/jpeg" }];
|
|
628
|
+
}
|
|
629
|
+
if (kind === "audio") {
|
|
630
|
+
return [{
|
|
631
|
+
kind: "audio",
|
|
632
|
+
dataUrl,
|
|
633
|
+
mimeType: mimeType ?? "audio/webm",
|
|
634
|
+
durationMs: typeof att.durationMs === "number" ? att.durationMs : void 0
|
|
635
|
+
}];
|
|
636
|
+
}
|
|
637
|
+
if (kind === "video") {
|
|
638
|
+
return [{
|
|
639
|
+
kind: "video",
|
|
640
|
+
dataUrl,
|
|
641
|
+
mimeType: mimeType ?? "video/mp4",
|
|
642
|
+
durationMs: typeof att.durationMs === "number" ? att.durationMs : void 0,
|
|
643
|
+
poster: typeof att.poster === "string" ? att.poster : void 0
|
|
644
|
+
}];
|
|
645
|
+
}
|
|
646
|
+
return [];
|
|
647
|
+
});
|
|
648
|
+
const role = msg.senderType === "agent" ? "assistant" : msg.senderType === "user" ? "user" : "assistant";
|
|
649
|
+
const mappedToolCalls = Array.isArray(msg.toolCalls) ? (msg.toolCalls || []).map((tc) => ({
|
|
650
|
+
id: typeof tc?.id === "string" ? tc.id : generateId(),
|
|
651
|
+
name: typeof tc?.name === "string" ? tc.name : "tool",
|
|
652
|
+
arguments: tc?.args || {},
|
|
653
|
+
status: "completed"
|
|
654
|
+
})) : void 0;
|
|
655
|
+
const hasToolCalls = Array.isArray(mappedToolCalls) && mappedToolCalls.length > 0;
|
|
656
|
+
const isToolSender = msg.senderType === "tool";
|
|
657
|
+
const content = isToolSender ? "" : (msg.content ?? "") || (hasToolCalls ? "" : "");
|
|
658
|
+
return {
|
|
659
|
+
id: msg.id,
|
|
660
|
+
role,
|
|
661
|
+
content,
|
|
662
|
+
timestamp,
|
|
663
|
+
attachments: attachments.length > 0 ? attachments : void 0,
|
|
664
|
+
isStreaming: false,
|
|
665
|
+
isComplete: true,
|
|
666
|
+
metadata,
|
|
667
|
+
toolCalls: hasToolCalls ? mappedToolCalls : void 0
|
|
668
|
+
};
|
|
669
|
+
};
|
|
670
|
+
function useCopilotz({ userId, initialContext, bootstrap, defaultThreadName, onToolOutput }) {
|
|
671
|
+
const [threads, setThreads] = useState([]);
|
|
672
|
+
const [threadMetadataMap, setThreadMetadataMap] = useState({});
|
|
673
|
+
const [threadExternalIdMap, setThreadExternalIdMap] = useState({});
|
|
674
|
+
const [currentThreadId, setCurrentThreadId] = useState(null);
|
|
675
|
+
const [currentThreadExternalId, setCurrentThreadExternalId] = useState(null);
|
|
676
|
+
const [messages, setMessages] = useState([]);
|
|
677
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
678
|
+
const [userContextSeed, setUserContextSeed] = useState(initialContext || {});
|
|
679
|
+
const threadsRef = useRef(threads);
|
|
680
|
+
const threadMetadataMapRef = useRef(threadMetadataMap);
|
|
681
|
+
const threadExternalIdMapRef = useRef(threadExternalIdMap);
|
|
682
|
+
const currentThreadIdRef = useRef(currentThreadId);
|
|
683
|
+
const currentThreadExternalIdRef = useRef(currentThreadExternalId);
|
|
684
|
+
const userContextSeedRef = useRef(userContextSeed);
|
|
685
|
+
useEffect(() => {
|
|
686
|
+
threadsRef.current = threads;
|
|
687
|
+
}, [threads]);
|
|
688
|
+
useEffect(() => {
|
|
689
|
+
threadMetadataMapRef.current = threadMetadataMap;
|
|
690
|
+
}, [threadMetadataMap]);
|
|
691
|
+
useEffect(() => {
|
|
692
|
+
threadExternalIdMapRef.current = threadExternalIdMap;
|
|
693
|
+
}, [threadExternalIdMap]);
|
|
694
|
+
useEffect(() => {
|
|
695
|
+
currentThreadIdRef.current = currentThreadId;
|
|
696
|
+
}, [currentThreadId]);
|
|
697
|
+
useEffect(() => {
|
|
698
|
+
currentThreadExternalIdRef.current = currentThreadExternalId;
|
|
699
|
+
}, [currentThreadExternalId]);
|
|
700
|
+
useEffect(() => {
|
|
701
|
+
userContextSeedRef.current = userContextSeed;
|
|
702
|
+
}, [userContextSeed]);
|
|
703
|
+
const abortControllerRef = useRef(null);
|
|
704
|
+
const messagesRequestRef = useRef(0);
|
|
705
|
+
const initializationRef = useRef({ userId: null, started: false });
|
|
706
|
+
useEffect(() => {
|
|
707
|
+
if (initialContext) {
|
|
708
|
+
setUserContextSeed((prev) => ({ ...prev, ...initialContext }));
|
|
709
|
+
}
|
|
710
|
+
}, [initialContext]);
|
|
711
|
+
const processToolOutput = useCallback((output) => {
|
|
712
|
+
if (!output) return;
|
|
713
|
+
const contextPatch = {};
|
|
714
|
+
if (output.userContext && typeof output.userContext === "object") {
|
|
715
|
+
Object.assign(contextPatch, output.userContext);
|
|
716
|
+
}
|
|
717
|
+
if (Object.keys(contextPatch).length > 0) {
|
|
718
|
+
setUserContextSeed((prev) => ({ ...prev, ...contextPatch }));
|
|
719
|
+
}
|
|
720
|
+
onToolOutput?.(output);
|
|
721
|
+
}, [onToolOutput]);
|
|
722
|
+
const handleStreamMessageEvent = useCallback((event) => {
|
|
723
|
+
const payload = event?.payload;
|
|
724
|
+
if (!payload) return;
|
|
725
|
+
if (payload.senderType === "tool") {
|
|
726
|
+
const metadata = payload.metadata ?? event.metadata ?? {};
|
|
727
|
+
const output = metadata?.output ?? metadata;
|
|
728
|
+
if (output) processToolOutput(output);
|
|
729
|
+
const toolName = metadata?.toolName || metadata?.tool || "tool";
|
|
730
|
+
let argsObj = {};
|
|
731
|
+
try {
|
|
732
|
+
const argStr = metadata?.arguments ?? "{}";
|
|
733
|
+
argsObj = typeof argStr === "string" ? JSON.parse(argStr) : argStr;
|
|
734
|
+
} catch (_) {
|
|
735
|
+
}
|
|
736
|
+
const resultObj = metadata?.output;
|
|
737
|
+
const callId = payload.toolCallId || generateId();
|
|
738
|
+
setMessages((prev) => {
|
|
739
|
+
const next = [...prev];
|
|
740
|
+
for (let i = next.length - 1; i >= 0; i--) {
|
|
741
|
+
const m = next[i];
|
|
742
|
+
if (m.role === "assistant") {
|
|
743
|
+
const existing = Array.isArray(m.toolCalls) ? m.toolCalls : [];
|
|
744
|
+
next[i] = {
|
|
745
|
+
...m,
|
|
746
|
+
toolCalls: [
|
|
747
|
+
...existing,
|
|
748
|
+
{
|
|
749
|
+
id: callId,
|
|
750
|
+
name: toolName,
|
|
751
|
+
arguments: argsObj,
|
|
752
|
+
result: resultObj,
|
|
753
|
+
status: "completed",
|
|
754
|
+
endTime: Date.now()
|
|
755
|
+
}
|
|
756
|
+
]
|
|
757
|
+
};
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return next;
|
|
762
|
+
});
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (payload.senderType === "agent" && typeof payload.content === "string") {
|
|
766
|
+
setMessages((prev) => {
|
|
767
|
+
const next = [...prev];
|
|
768
|
+
for (let i = next.length - 1; i >= 0; i--) {
|
|
769
|
+
const m = next[i];
|
|
770
|
+
if (m.role === "assistant" && m.isStreaming) {
|
|
771
|
+
next[i] = { ...m, content: payload.content, isStreaming: false, isComplete: true };
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return next;
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}, [processToolOutput]);
|
|
779
|
+
const updateThreadsState = useCallback((rawThreads, preferredExternalId) => {
|
|
780
|
+
const metadataMap = {};
|
|
781
|
+
const externalMap = {};
|
|
782
|
+
const normalized = rawThreads.map((thread) => {
|
|
783
|
+
metadataMap[thread.id] = thread.metadata ?? void 0;
|
|
784
|
+
externalMap[thread.id] = thread.externalId ?? null;
|
|
785
|
+
const updatedAt = thread.updatedAt ? new Date(thread.updatedAt).getTime() : nowTs();
|
|
786
|
+
const createdAt = thread.createdAt ? new Date(thread.createdAt).getTime() : updatedAt;
|
|
787
|
+
return {
|
|
788
|
+
id: thread.id,
|
|
789
|
+
title: thread.name || "Chat",
|
|
790
|
+
createdAt,
|
|
791
|
+
updatedAt,
|
|
792
|
+
messageCount: typeof thread.metadata?.messageCount === "number" ? thread.metadata.messageCount : 0,
|
|
793
|
+
isArchived: thread.status === "archived",
|
|
794
|
+
metadata: thread.metadata ?? void 0
|
|
795
|
+
};
|
|
796
|
+
});
|
|
797
|
+
setThreadMetadataMap(metadataMap);
|
|
798
|
+
setThreadExternalIdMap(externalMap);
|
|
799
|
+
setThreads(normalized);
|
|
800
|
+
const curExtId = currentThreadExternalIdRef.current;
|
|
801
|
+
const curId = currentThreadIdRef.current;
|
|
802
|
+
let nextThreadId = null;
|
|
803
|
+
if (preferredExternalId) {
|
|
804
|
+
const preferred = rawThreads.find((thread) => (thread.externalId ?? thread.id) === preferredExternalId);
|
|
805
|
+
if (preferred) nextThreadId = preferred.id;
|
|
806
|
+
}
|
|
807
|
+
if (!nextThreadId && curExtId) {
|
|
808
|
+
const match = rawThreads.find((thread) => (thread.externalId ?? thread.id) === curExtId);
|
|
809
|
+
if (match) nextThreadId = match.id;
|
|
810
|
+
}
|
|
811
|
+
if (!nextThreadId && curId && rawThreads.some((thread) => thread.id === curId)) {
|
|
812
|
+
nextThreadId = curId;
|
|
813
|
+
}
|
|
814
|
+
if (!nextThreadId && normalized.length > 0) {
|
|
815
|
+
nextThreadId = normalized[0].id;
|
|
816
|
+
}
|
|
817
|
+
setCurrentThreadId(nextThreadId ?? null);
|
|
818
|
+
setCurrentThreadExternalId(nextThreadId ? externalMap[nextThreadId] ?? null : null);
|
|
819
|
+
return nextThreadId;
|
|
820
|
+
}, []);
|
|
821
|
+
const fetchAndSetThreadsState = useCallback(async (uid, preferredExternalId) => {
|
|
822
|
+
try {
|
|
823
|
+
const rawThreads = await fetchThreads(uid);
|
|
824
|
+
return updateThreadsState(rawThreads, preferredExternalId);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
if (isAbortError(error)) return;
|
|
827
|
+
console.error("Error loading threads", error);
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
}, [updateThreadsState]);
|
|
831
|
+
const loadThreadMessages = useCallback(async (threadId) => {
|
|
832
|
+
const requestId = Date.now();
|
|
833
|
+
messagesRequestRef.current = requestId;
|
|
834
|
+
try {
|
|
835
|
+
const rawMessages = await fetchThreadMessages(threadId);
|
|
836
|
+
const resolvedMessages = await resolveAssetsInMessages(rawMessages);
|
|
837
|
+
if (messagesRequestRef.current !== requestId) return;
|
|
838
|
+
resolvedMessages.forEach((msg) => {
|
|
839
|
+
if (msg.senderType === "tool") {
|
|
840
|
+
const metadata = msg.metadata;
|
|
841
|
+
const output = metadata?.output ?? metadata;
|
|
842
|
+
if (output) processToolOutput(output);
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
const viewMessages = resolvedMessages.filter((msg) => {
|
|
846
|
+
const text = (typeof msg.content === "string" ? msg.content : "").trim();
|
|
847
|
+
const hasText = text.length > 0;
|
|
848
|
+
const hasToolCalls = Array.isArray(msg.toolCalls) && msg.toolCalls.length > 0;
|
|
849
|
+
const meta = msg.metadata ?? {};
|
|
850
|
+
const hasAttachments = Array.isArray(meta.attachments) && meta.attachments.length > 0;
|
|
851
|
+
if (msg.senderType === "tool") {
|
|
852
|
+
return hasAttachments;
|
|
853
|
+
}
|
|
854
|
+
return hasText || hasToolCalls || hasAttachments;
|
|
855
|
+
}).map(convertServerMessage);
|
|
856
|
+
setMessages(viewMessages);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
if (isAbortError(error)) return;
|
|
859
|
+
console.error(`Error loading messages for thread ${threadId}`, error);
|
|
860
|
+
}
|
|
861
|
+
}, [processToolOutput]);
|
|
862
|
+
const handleSelectThread = useCallback(async (threadId) => {
|
|
863
|
+
setCurrentThreadId(threadId);
|
|
864
|
+
const extMap = threadExternalIdMapRef.current;
|
|
865
|
+
setCurrentThreadExternalId(extMap[threadId] ?? null);
|
|
866
|
+
await loadThreadMessages(threadId);
|
|
867
|
+
}, [loadThreadMessages]);
|
|
868
|
+
const handleCreateThread = useCallback((title) => {
|
|
869
|
+
const id = generateId();
|
|
870
|
+
const now = nowTs();
|
|
871
|
+
const newThread = {
|
|
872
|
+
id,
|
|
873
|
+
title: title?.trim() || "New Chat",
|
|
874
|
+
createdAt: now,
|
|
875
|
+
updatedAt: now,
|
|
876
|
+
messageCount: 0,
|
|
877
|
+
metadata: { pendingTitle: title?.trim() || void 0 }
|
|
878
|
+
};
|
|
879
|
+
setThreads((prev) => [newThread, ...prev]);
|
|
880
|
+
setThreadMetadataMap((prev) => ({ ...prev, [id]: { pendingTitle: title?.trim() || void 0 } }));
|
|
881
|
+
setThreadExternalIdMap((prev) => ({ ...prev, [id]: id }));
|
|
882
|
+
setCurrentThreadId(id);
|
|
883
|
+
setCurrentThreadExternalId(id);
|
|
884
|
+
setMessages([]);
|
|
885
|
+
}, []);
|
|
886
|
+
const handleRenameThread = useCallback(async (threadId, newTitle) => {
|
|
887
|
+
const trimmedTitle = newTitle.trim();
|
|
888
|
+
if (!trimmedTitle) return;
|
|
889
|
+
setThreads(
|
|
890
|
+
(prev) => prev.map((t) => t.id === threadId ? { ...t, title: trimmedTitle, updatedAt: nowTs() } : t)
|
|
891
|
+
);
|
|
892
|
+
const extMap = threadExternalIdMapRef.current;
|
|
893
|
+
const isPlaceholder = extMap[threadId] === threadId;
|
|
894
|
+
if (isPlaceholder) {
|
|
895
|
+
setThreadMetadataMap((prev) => ({
|
|
896
|
+
...prev,
|
|
897
|
+
[threadId]: { ...prev[threadId], pendingTitle: trimmedTitle }
|
|
898
|
+
}));
|
|
899
|
+
} else {
|
|
900
|
+
try {
|
|
901
|
+
await updateThread(threadId, { name: trimmedTitle });
|
|
902
|
+
} catch (error) {
|
|
903
|
+
console.error("Failed to rename thread:", error);
|
|
904
|
+
if (userId) {
|
|
905
|
+
await fetchAndSetThreadsState(userId, currentThreadExternalIdRef.current);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}, [userId, fetchAndSetThreadsState]);
|
|
910
|
+
const handleArchiveThread = useCallback(async (threadId) => {
|
|
911
|
+
const thread = threadsRef.current.find((t) => t.id === threadId);
|
|
912
|
+
if (!thread) return;
|
|
913
|
+
const newArchivedStatus = !thread.isArchived;
|
|
914
|
+
setThreads(
|
|
915
|
+
(prev) => prev.map((t) => t.id === threadId ? { ...t, isArchived: newArchivedStatus, updatedAt: nowTs() } : t)
|
|
916
|
+
);
|
|
917
|
+
const extMap = threadExternalIdMapRef.current;
|
|
918
|
+
const isPlaceholder = extMap[threadId] === threadId;
|
|
919
|
+
if (!isPlaceholder) {
|
|
920
|
+
try {
|
|
921
|
+
await updateThread(threadId, { status: newArchivedStatus ? "archived" : "active" });
|
|
922
|
+
} catch (error) {
|
|
923
|
+
console.error("Failed to archive thread:", error);
|
|
924
|
+
if (userId) {
|
|
925
|
+
await fetchAndSetThreadsState(userId, currentThreadExternalIdRef.current);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}, [userId, fetchAndSetThreadsState]);
|
|
930
|
+
const handleDeleteThread = useCallback(async (threadId) => {
|
|
931
|
+
const extMap = threadExternalIdMapRef.current;
|
|
932
|
+
const isPlaceholder = extMap[threadId] === threadId;
|
|
933
|
+
setThreads((prev) => prev.filter((t) => t.id !== threadId));
|
|
934
|
+
setThreadMetadataMap((prev) => {
|
|
935
|
+
const next = { ...prev };
|
|
936
|
+
delete next[threadId];
|
|
937
|
+
return next;
|
|
938
|
+
});
|
|
939
|
+
setThreadExternalIdMap((prev) => {
|
|
940
|
+
const next = { ...prev };
|
|
941
|
+
delete next[threadId];
|
|
942
|
+
return next;
|
|
943
|
+
});
|
|
944
|
+
if (currentThreadIdRef.current === threadId) {
|
|
945
|
+
const remaining = threadsRef.current.filter((t) => t.id !== threadId);
|
|
946
|
+
if (remaining.length > 0) {
|
|
947
|
+
setCurrentThreadId(remaining[0].id);
|
|
948
|
+
setCurrentThreadExternalId(extMap[remaining[0].id] ?? null);
|
|
949
|
+
await loadThreadMessages(remaining[0].id);
|
|
950
|
+
} else {
|
|
951
|
+
setCurrentThreadId(null);
|
|
952
|
+
setCurrentThreadExternalId(null);
|
|
953
|
+
setMessages([]);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (!isPlaceholder) {
|
|
957
|
+
try {
|
|
958
|
+
await deleteThread(threadId);
|
|
959
|
+
} catch (error) {
|
|
960
|
+
console.error("Failed to delete thread:", error);
|
|
961
|
+
if (userId) {
|
|
962
|
+
await fetchAndSetThreadsState(userId, currentThreadExternalIdRef.current);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}, [userId, fetchAndSetThreadsState, loadThreadMessages]);
|
|
967
|
+
const handleStop = useCallback(() => {
|
|
968
|
+
abortControllerRef.current?.abort();
|
|
969
|
+
abortControllerRef.current = null;
|
|
970
|
+
setIsStreaming(false);
|
|
971
|
+
setMessages((prev) => prev.map((msg) => msg.isStreaming ? { ...msg, isStreaming: false, isComplete: true } : msg));
|
|
972
|
+
}, []);
|
|
973
|
+
const handleStreamAssetEvent = useCallback((payload, assistantMessageId) => {
|
|
974
|
+
if (!payload?.dataUrl) return;
|
|
975
|
+
const mimeType = payload.mime || "image/png";
|
|
976
|
+
const dataUrl = payload.dataUrl;
|
|
977
|
+
let kind = "image";
|
|
978
|
+
if (mimeType.startsWith("audio/")) {
|
|
979
|
+
kind = "audio";
|
|
980
|
+
} else if (mimeType.startsWith("video/")) {
|
|
981
|
+
kind = "video";
|
|
982
|
+
}
|
|
983
|
+
const mediaAttachment = {
|
|
984
|
+
kind,
|
|
985
|
+
dataUrl,
|
|
986
|
+
mimeType
|
|
987
|
+
};
|
|
988
|
+
setMessages((prev) => prev.map((msg) => msg.id === assistantMessageId ? {
|
|
989
|
+
...msg,
|
|
990
|
+
attachments: [...msg.attachments || [], mediaAttachment],
|
|
991
|
+
isStreaming: false,
|
|
992
|
+
isComplete: true
|
|
993
|
+
} : msg));
|
|
994
|
+
}, []);
|
|
995
|
+
const sendCopilotzMessage = useCallback(async (params) => {
|
|
996
|
+
let currentAssistantId = generateId();
|
|
997
|
+
params.onBeforeStart?.(currentAssistantId);
|
|
998
|
+
let hasStreamProgress = false;
|
|
999
|
+
let pendingStartNewAssistantBubble = false;
|
|
1000
|
+
const ensureStreamingBubble = () => {
|
|
1001
|
+
setMessages((prev) => {
|
|
1002
|
+
const idx = prev.findIndex((m) => m.id === currentAssistantId);
|
|
1003
|
+
if (idx >= 0 && prev[idx].role === "assistant" && prev[idx].isStreaming) {
|
|
1004
|
+
return prev;
|
|
1005
|
+
}
|
|
1006
|
+
const last = prev[prev.length - 1];
|
|
1007
|
+
if (last && last.role === "assistant" && last.isStreaming) {
|
|
1008
|
+
currentAssistantId = last.id;
|
|
1009
|
+
pendingStartNewAssistantBubble = false;
|
|
1010
|
+
return prev;
|
|
1011
|
+
}
|
|
1012
|
+
if (pendingStartNewAssistantBubble || !prev.length || (prev[prev.length - 1].role !== "assistant" || !prev[prev.length - 1].isStreaming)) {
|
|
1013
|
+
const newId = generateId();
|
|
1014
|
+
currentAssistantId = newId;
|
|
1015
|
+
pendingStartNewAssistantBubble = false;
|
|
1016
|
+
return [
|
|
1017
|
+
...prev,
|
|
1018
|
+
{
|
|
1019
|
+
id: newId,
|
|
1020
|
+
role: "assistant",
|
|
1021
|
+
content: "",
|
|
1022
|
+
timestamp: nowTs(),
|
|
1023
|
+
isStreaming: true,
|
|
1024
|
+
isComplete: false
|
|
1025
|
+
}
|
|
1026
|
+
];
|
|
1027
|
+
}
|
|
1028
|
+
return prev;
|
|
1029
|
+
});
|
|
1030
|
+
};
|
|
1031
|
+
const updateStreamingMessage = (partial, isComplete) => {
|
|
1032
|
+
if (partial && partial.length > 0) {
|
|
1033
|
+
hasStreamProgress = true;
|
|
1034
|
+
}
|
|
1035
|
+
ensureStreamingBubble();
|
|
1036
|
+
setMessages((prev) => prev.map((msg) => msg.id === currentAssistantId ? { ...msg, content: partial, isStreaming: !isComplete, isComplete } : msg));
|
|
1037
|
+
};
|
|
1038
|
+
const finalizeCurrentAssistantBubble = () => {
|
|
1039
|
+
setMessages((prev) => prev.map((msg) => msg.id === currentAssistantId ? { ...msg, isStreaming: false, isComplete: true } : msg));
|
|
1040
|
+
};
|
|
1041
|
+
const curThreadId = currentThreadIdRef.current;
|
|
1042
|
+
const toServerMessageFromEvent = async (event) => {
|
|
1043
|
+
if (!event) return null;
|
|
1044
|
+
const type = event?.type || "";
|
|
1045
|
+
const payload = event?.payload ?? event;
|
|
1046
|
+
if (type === "TOOL_CALL") {
|
|
1047
|
+
const metadata = payload?.metadata ?? {};
|
|
1048
|
+
const call = payload?.call ?? metadata?.call;
|
|
1049
|
+
const func = call?.function ?? payload?.function;
|
|
1050
|
+
const toolName = func?.name || payload?.name || call?.name || metadata.toolName || metadata.tool || "tool";
|
|
1051
|
+
let argsObj = {};
|
|
1052
|
+
const possibleArgs = [
|
|
1053
|
+
func?.arguments,
|
|
1054
|
+
// Try call.function.arguments first (most specific for this event structure)
|
|
1055
|
+
payload?.args,
|
|
1056
|
+
call?.arguments,
|
|
1057
|
+
metadata?.args,
|
|
1058
|
+
metadata?.arguments
|
|
1059
|
+
];
|
|
1060
|
+
for (const candidate of possibleArgs) {
|
|
1061
|
+
if (candidate === void 0 || candidate === null) continue;
|
|
1062
|
+
try {
|
|
1063
|
+
if (typeof candidate === "string") {
|
|
1064
|
+
argsObj = JSON.parse(candidate);
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
if (typeof candidate === "object") {
|
|
1068
|
+
argsObj = candidate;
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const output = metadata?.output !== void 0 ? metadata.output : payload?.output !== void 0 ? payload.output : void 0;
|
|
1075
|
+
const callId = call?.id || func?.id || payload?.id || generateId();
|
|
1076
|
+
const statusVal = payload?.status || event?.status || "pending";
|
|
1077
|
+
return {
|
|
1078
|
+
id: generateId(),
|
|
1079
|
+
threadId: curThreadId ?? "",
|
|
1080
|
+
senderType: "tool",
|
|
1081
|
+
content: "",
|
|
1082
|
+
toolCalls: [{
|
|
1083
|
+
id: callId,
|
|
1084
|
+
name: toolName,
|
|
1085
|
+
args: argsObj,
|
|
1086
|
+
output,
|
|
1087
|
+
status: statusVal
|
|
1088
|
+
}]
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
if (type === "MESSAGE" || type === "NEW_MESSAGE") {
|
|
1092
|
+
const senderType = payload?.senderType || payload?.sender?.type;
|
|
1093
|
+
if (senderType !== "agent") {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
const content = typeof payload?.content === "string" ? payload.content : "";
|
|
1097
|
+
if (!content.trim()) {
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
id: generateId(),
|
|
1102
|
+
threadId: curThreadId ?? "",
|
|
1103
|
+
senderType: "agent",
|
|
1104
|
+
content,
|
|
1105
|
+
metadata: payload?.metadata ?? {}
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
if (type === "ASSET_CREATED") {
|
|
1109
|
+
const by = payload?.by || "";
|
|
1110
|
+
if (by && by !== "tool") return null;
|
|
1111
|
+
const mime = payload?.mime || "image/png";
|
|
1112
|
+
const ref = payload?.ref || payload?.assetRef || "";
|
|
1113
|
+
if (!ref) return null;
|
|
1114
|
+
const kind = mime.startsWith("audio/") ? "audio" : mime.startsWith("video/") ? "video" : "image";
|
|
1115
|
+
const msgLike = {
|
|
1116
|
+
id: generateId(),
|
|
1117
|
+
threadId: curThreadId ?? "",
|
|
1118
|
+
senderType: "tool",
|
|
1119
|
+
content: "",
|
|
1120
|
+
metadata: {
|
|
1121
|
+
attachments: [{ kind, assetRef: ref, mimeType: mime }]
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
const [resolved] = await resolveAssetsInMessages([msgLike]);
|
|
1125
|
+
return resolved;
|
|
1126
|
+
}
|
|
1127
|
+
return null;
|
|
1128
|
+
};
|
|
1129
|
+
const abortController = new AbortController();
|
|
1130
|
+
abortControllerRef.current?.abort();
|
|
1131
|
+
abortControllerRef.current = abortController;
|
|
1132
|
+
setIsStreaming(true);
|
|
1133
|
+
try {
|
|
1134
|
+
const normalizedUserMetadata = params.userMetadata ? JSON.parse(JSON.stringify(params.userMetadata)) : void 0;
|
|
1135
|
+
const contextSeed = userContextSeedRef.current;
|
|
1136
|
+
const contextMetadata = contextSeed ? JSON.parse(JSON.stringify(contextSeed)) : void 0;
|
|
1137
|
+
const requestContent = params.content && params.content.length > 0 ? params.content : "";
|
|
1138
|
+
const metadataKey = params.threadId ?? params.threadExternalId ?? void 0;
|
|
1139
|
+
const currentThreadMetadataMap = threadMetadataMapRef.current;
|
|
1140
|
+
const messageMetadata = metadataKey ? currentThreadMetadataMap[metadataKey]?.userContext : void 0;
|
|
1141
|
+
const threadMetadata = metadataKey ? currentThreadMetadataMap[metadataKey] : void 0;
|
|
1142
|
+
await runCopilotzStream({
|
|
1143
|
+
threadId: params.threadId ?? void 0,
|
|
1144
|
+
threadExternalId: params.threadExternalId ?? void 0,
|
|
1145
|
+
content: requestContent,
|
|
1146
|
+
user: {
|
|
1147
|
+
externalId: params.userId,
|
|
1148
|
+
name: params.userName ?? params.userId,
|
|
1149
|
+
metadata: {
|
|
1150
|
+
...contextMetadata ? contextMetadata : {},
|
|
1151
|
+
...normalizedUserMetadata ?? {}
|
|
1152
|
+
}
|
|
1153
|
+
},
|
|
1154
|
+
attachments: params.attachments,
|
|
1155
|
+
metadata: params.metadata ?? messageMetadata,
|
|
1156
|
+
threadMetadata: params.threadMetadata ?? threadMetadata,
|
|
1157
|
+
toolCalls: params.toolCalls,
|
|
1158
|
+
onToken: (token, isComplete) => updateStreamingMessage(token, isComplete),
|
|
1159
|
+
onMessageEvent: async (event) => {
|
|
1160
|
+
const type = event?.type || "";
|
|
1161
|
+
const payload = event?.payload ?? event;
|
|
1162
|
+
if (type === "MESSAGE" || type === "NEW_MESSAGE") {
|
|
1163
|
+
const senderType = payload?.senderType || payload?.sender?.type;
|
|
1164
|
+
if (senderType === "tool") {
|
|
1165
|
+
const metadata = payload?.metadata ?? {};
|
|
1166
|
+
const toolCallsArray = metadata?.toolCalls;
|
|
1167
|
+
const toolCallData = toolCallsArray && toolCallsArray.length > 0 ? toolCallsArray[0] : void 0;
|
|
1168
|
+
if (!toolCallData) {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
processToolOutput(metadata);
|
|
1172
|
+
const toolCallId = toolCallData.id;
|
|
1173
|
+
const toolCallName = toolCallData.name;
|
|
1174
|
+
const toolResult = toolCallData.output || payload?.content;
|
|
1175
|
+
const toolStatus = toolCallData.status || "completed";
|
|
1176
|
+
const isFailed = toolStatus === "failed" || toolCallData?.error;
|
|
1177
|
+
setMessages((prev) => {
|
|
1178
|
+
const updated = [...prev];
|
|
1179
|
+
for (let i = updated.length - 1; i >= 0; i--) {
|
|
1180
|
+
if (updated[i].role === "assistant" && updated[i].toolCalls) {
|
|
1181
|
+
const toolCalls = updated[i].toolCalls;
|
|
1182
|
+
if (toolCalls) {
|
|
1183
|
+
let toolCallIndex = toolCallId ? toolCalls.findIndex((tc) => tc.id === toolCallId) : -1;
|
|
1184
|
+
if (toolCallIndex === -1 && toolCallName) {
|
|
1185
|
+
toolCallIndex = toolCalls.findIndex(
|
|
1186
|
+
(tc) => tc.name === toolCallName && (tc.status === "pending" || tc.status === "running")
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
if (toolCallIndex !== -1) {
|
|
1190
|
+
const updatedToolCalls = [...toolCalls];
|
|
1191
|
+
updatedToolCalls[toolCallIndex] = {
|
|
1192
|
+
...updatedToolCalls[toolCallIndex],
|
|
1193
|
+
status: isFailed ? "failed" : "completed",
|
|
1194
|
+
result: toolResult,
|
|
1195
|
+
endTime: Date.now()
|
|
1196
|
+
};
|
|
1197
|
+
updated[i] = {
|
|
1198
|
+
...updated[i],
|
|
1199
|
+
toolCalls: updatedToolCalls
|
|
1200
|
+
};
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return updated;
|
|
1207
|
+
});
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (type === "TOOL_CALL") {
|
|
1213
|
+
const sm2 = await toServerMessageFromEvent(event);
|
|
1214
|
+
const toolCalls = sm2?.toolCalls;
|
|
1215
|
+
const toolCall = toolCalls && toolCalls[0];
|
|
1216
|
+
if (!toolCall) return;
|
|
1217
|
+
setMessages(
|
|
1218
|
+
(prev) => (() => {
|
|
1219
|
+
const appendToolCall = (msg) => ({
|
|
1220
|
+
...msg,
|
|
1221
|
+
toolCalls: [
|
|
1222
|
+
...Array.isArray(msg.toolCalls) ? msg.toolCalls : [],
|
|
1223
|
+
{
|
|
1224
|
+
id: toolCall.id ?? generateId(),
|
|
1225
|
+
name: toolCall.name ?? "tool",
|
|
1226
|
+
arguments: toolCall.args ?? toolCall.arguments ?? {},
|
|
1227
|
+
result: toolCall.output,
|
|
1228
|
+
status: toolCall.status ?? "running",
|
|
1229
|
+
startTime: Date.now()
|
|
1230
|
+
}
|
|
1231
|
+
]
|
|
1232
|
+
});
|
|
1233
|
+
for (let i = prev.length - 1; i >= 0; i--) {
|
|
1234
|
+
if (prev[i].role === "assistant") {
|
|
1235
|
+
const next = [...prev];
|
|
1236
|
+
next[i] = appendToolCall({
|
|
1237
|
+
...next[i],
|
|
1238
|
+
isStreaming: false,
|
|
1239
|
+
isComplete: true
|
|
1240
|
+
});
|
|
1241
|
+
return next;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return [
|
|
1245
|
+
...prev,
|
|
1246
|
+
appendToolCall({
|
|
1247
|
+
id: generateId(),
|
|
1248
|
+
role: "assistant",
|
|
1249
|
+
content: "",
|
|
1250
|
+
timestamp: nowTs(),
|
|
1251
|
+
isStreaming: false,
|
|
1252
|
+
isComplete: true
|
|
1253
|
+
})
|
|
1254
|
+
];
|
|
1255
|
+
})()
|
|
1256
|
+
);
|
|
1257
|
+
hasStreamProgress = true;
|
|
1258
|
+
pendingStartNewAssistantBubble = true;
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const sm = await toServerMessageFromEvent(event);
|
|
1262
|
+
if (sm) {
|
|
1263
|
+
const viewMsg = convertServerMessage(sm);
|
|
1264
|
+
finalizeCurrentAssistantBubble();
|
|
1265
|
+
setMessages((prev) => [...prev, viewMsg]);
|
|
1266
|
+
pendingStartNewAssistantBubble = true;
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
handleStreamMessageEvent(event);
|
|
1270
|
+
},
|
|
1271
|
+
onAssetEvent: async (payload) => {
|
|
1272
|
+
await (async () => {
|
|
1273
|
+
if (!hasStreamProgress) return;
|
|
1274
|
+
finalizeCurrentAssistantBubble();
|
|
1275
|
+
const evt = { type: "ASSET_CREATED", payload };
|
|
1276
|
+
const sm = await toServerMessageFromEvent(evt);
|
|
1277
|
+
if (sm) {
|
|
1278
|
+
const viewMsg = convertServerMessage(sm);
|
|
1279
|
+
setMessages((prev) => [...prev, viewMsg]);
|
|
1280
|
+
}
|
|
1281
|
+
pendingStartNewAssistantBubble = true;
|
|
1282
|
+
})();
|
|
1283
|
+
},
|
|
1284
|
+
signal: abortController.signal
|
|
1285
|
+
});
|
|
1286
|
+
} finally {
|
|
1287
|
+
setIsStreaming(false);
|
|
1288
|
+
abortControllerRef.current = null;
|
|
1289
|
+
}
|
|
1290
|
+
return currentAssistantId;
|
|
1291
|
+
}, [handleStreamMessageEvent, handleStreamAssetEvent]);
|
|
1292
|
+
const handleSendMessage = useCallback(async (content, attachments = []) => {
|
|
1293
|
+
if (!content.trim() && attachments.length === 0) return;
|
|
1294
|
+
if (!userId) return;
|
|
1295
|
+
const timestamp = nowTs();
|
|
1296
|
+
const curThreadId = currentThreadIdRef.current;
|
|
1297
|
+
const curThreadExtId = currentThreadExternalIdRef.current;
|
|
1298
|
+
const existingThreadId = curThreadId ?? void 0;
|
|
1299
|
+
const extMap = threadExternalIdMapRef.current;
|
|
1300
|
+
const isPlaceholderThread = existingThreadId ? extMap[existingThreadId] === existingThreadId : false;
|
|
1301
|
+
const threadIdForSend = isPlaceholderThread ? void 0 : existingThreadId;
|
|
1302
|
+
let effectiveThreadExternalId = curThreadExtId ?? (isPlaceholderThread ? existingThreadId : void 0);
|
|
1303
|
+
if (!threadIdForSend) {
|
|
1304
|
+
if (!effectiveThreadExternalId) {
|
|
1305
|
+
effectiveThreadExternalId = generateId();
|
|
1306
|
+
}
|
|
1307
|
+
setCurrentThreadExternalId(effectiveThreadExternalId);
|
|
1308
|
+
} else if (curThreadExtId !== (effectiveThreadExternalId ?? null)) {
|
|
1309
|
+
setCurrentThreadExternalId(effectiveThreadExternalId ?? null);
|
|
1310
|
+
}
|
|
1311
|
+
const conversationKey = threadIdForSend ?? effectiveThreadExternalId;
|
|
1312
|
+
const currentMetadata = threadMetadataMapRef.current[conversationKey];
|
|
1313
|
+
const pendingTitle = currentMetadata?.pendingTitle;
|
|
1314
|
+
const userMessage = {
|
|
1315
|
+
id: generateId(),
|
|
1316
|
+
role: "user",
|
|
1317
|
+
content,
|
|
1318
|
+
timestamp,
|
|
1319
|
+
attachments: attachments.length > 0 ? attachments : void 0,
|
|
1320
|
+
isComplete: true
|
|
1321
|
+
};
|
|
1322
|
+
const assistantPlaceholder = {
|
|
1323
|
+
id: generateId(),
|
|
1324
|
+
role: "assistant",
|
|
1325
|
+
content: "",
|
|
1326
|
+
timestamp: timestamp + 1,
|
|
1327
|
+
isStreaming: true,
|
|
1328
|
+
isComplete: false
|
|
1329
|
+
};
|
|
1330
|
+
setMessages((prev) => [...prev, userMessage, assistantPlaceholder]);
|
|
1331
|
+
if (!threadsRef.current.some((t) => t.id === conversationKey)) {
|
|
1332
|
+
const newThread = {
|
|
1333
|
+
id: conversationKey,
|
|
1334
|
+
title: content.slice(0, 40) || "Nova conversa",
|
|
1335
|
+
createdAt: timestamp,
|
|
1336
|
+
updatedAt: timestamp,
|
|
1337
|
+
messageCount: 0
|
|
1338
|
+
};
|
|
1339
|
+
setThreads((prev) => [newThread, ...prev]);
|
|
1340
|
+
setThreadMetadataMap((prev) => ({ ...prev, [conversationKey]: {} }));
|
|
1341
|
+
setThreadExternalIdMap((prev) => ({ ...prev, [conversationKey]: effectiveThreadExternalId ?? null }));
|
|
1342
|
+
}
|
|
1343
|
+
try {
|
|
1344
|
+
await sendCopilotzMessage({
|
|
1345
|
+
threadId: threadIdForSend,
|
|
1346
|
+
threadExternalId: effectiveThreadExternalId,
|
|
1347
|
+
content,
|
|
1348
|
+
attachments,
|
|
1349
|
+
userId,
|
|
1350
|
+
// userName can be anything, but let's try to find it in context or just fallback
|
|
1351
|
+
userName: userContextSeedRef.current?.profile?.full_name ?? userId,
|
|
1352
|
+
// Include pending title for new threads
|
|
1353
|
+
threadMetadata: pendingTitle ? { name: pendingTitle } : void 0
|
|
1354
|
+
});
|
|
1355
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1356
|
+
await fetchAndSetThreadsState(userId, effectiveThreadExternalId ?? existingThreadId ?? null);
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
if (isAbortError(error)) return;
|
|
1359
|
+
console.error("Error sending Copilotz message", error);
|
|
1360
|
+
setMessages((prev) => prev.map((msg) => msg.isStreaming ? {
|
|
1361
|
+
...msg,
|
|
1362
|
+
isStreaming: false,
|
|
1363
|
+
isComplete: true,
|
|
1364
|
+
content: "Desculpe, ocorreu um erro ao gerar a resposta. Por favor, tente novamente."
|
|
1365
|
+
} : msg));
|
|
1366
|
+
}
|
|
1367
|
+
}, [userId, fetchAndSetThreadsState, loadThreadMessages, sendCopilotzMessage]);
|
|
1368
|
+
const bootstrapConversation = useCallback(async (uid) => {
|
|
1369
|
+
if (!bootstrap?.initialToolCalls && !bootstrap?.initialMessage) return;
|
|
1370
|
+
const bootstrapThreadExternalId = generateId();
|
|
1371
|
+
setCurrentThreadId(bootstrapThreadExternalId);
|
|
1372
|
+
setCurrentThreadExternalId(bootstrapThreadExternalId);
|
|
1373
|
+
setThreadExternalIdMap((prev) => ({ ...prev, [bootstrapThreadExternalId]: bootstrapThreadExternalId }));
|
|
1374
|
+
setThreadMetadataMap((prev) => ({ ...prev, [bootstrapThreadExternalId]: {} }));
|
|
1375
|
+
setMessages([]);
|
|
1376
|
+
try {
|
|
1377
|
+
await sendCopilotzMessage({
|
|
1378
|
+
threadExternalId: bootstrapThreadExternalId,
|
|
1379
|
+
content: bootstrap.initialMessage || "",
|
|
1380
|
+
toolCalls: bootstrap.initialToolCalls,
|
|
1381
|
+
userId: uid,
|
|
1382
|
+
threadMetadata: {
|
|
1383
|
+
name: defaultThreadName || "Main Thread"
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1387
|
+
await fetchAndSetThreadsState(uid, bootstrapThreadExternalId);
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
if (isAbortError(error)) return;
|
|
1390
|
+
console.error("Error bootstrapping conversation", error);
|
|
1391
|
+
setMessages([
|
|
1392
|
+
{
|
|
1393
|
+
id: generateId(),
|
|
1394
|
+
role: "assistant",
|
|
1395
|
+
content: "N\xE3o foi poss\xEDvel iniciar a conversa. Tente novamente mais tarde.",
|
|
1396
|
+
timestamp: nowTs(),
|
|
1397
|
+
isStreaming: false,
|
|
1398
|
+
isComplete: true
|
|
1399
|
+
}
|
|
1400
|
+
]);
|
|
1401
|
+
}
|
|
1402
|
+
}, [fetchAndSetThreadsState, loadThreadMessages, sendCopilotzMessage, bootstrap, defaultThreadName]);
|
|
1403
|
+
const reset = useCallback(() => {
|
|
1404
|
+
setThreads([]);
|
|
1405
|
+
setThreadMetadataMap({});
|
|
1406
|
+
setThreadExternalIdMap({});
|
|
1407
|
+
setCurrentThreadId(null);
|
|
1408
|
+
setCurrentThreadExternalId(null);
|
|
1409
|
+
setMessages([]);
|
|
1410
|
+
setUserContextSeed({});
|
|
1411
|
+
setIsStreaming(false);
|
|
1412
|
+
abortControllerRef.current?.abort();
|
|
1413
|
+
}, []);
|
|
1414
|
+
useEffect(() => {
|
|
1415
|
+
if (userId) {
|
|
1416
|
+
if (initializationRef.current.userId === userId && initializationRef.current.started) {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
initializationRef.current = { userId, started: true };
|
|
1420
|
+
const init = async () => {
|
|
1421
|
+
const preferredThreadId = await fetchAndSetThreadsState(userId, void 0);
|
|
1422
|
+
if (preferredThreadId) {
|
|
1423
|
+
await loadThreadMessages(preferredThreadId);
|
|
1424
|
+
} else if (bootstrap) {
|
|
1425
|
+
await bootstrapConversation(userId);
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
init();
|
|
1429
|
+
} else {
|
|
1430
|
+
initializationRef.current = { userId: null, started: false };
|
|
1431
|
+
reset();
|
|
1432
|
+
}
|
|
1433
|
+
}, [userId, fetchAndSetThreadsState, loadThreadMessages, bootstrapConversation, reset, bootstrap]);
|
|
1434
|
+
useEffect(() => {
|
|
1435
|
+
if (!currentThreadId) return;
|
|
1436
|
+
const metadata = threadMetadataMap[currentThreadId];
|
|
1437
|
+
if (!metadata) return;
|
|
1438
|
+
if (metadata.userContext && typeof metadata.userContext === "object") {
|
|
1439
|
+
setUserContextSeed((prev) => ({ ...prev, ...metadata.userContext }));
|
|
1440
|
+
}
|
|
1441
|
+
}, [currentThreadId, threadMetadataMap]);
|
|
1442
|
+
return {
|
|
1443
|
+
messages,
|
|
1444
|
+
threads,
|
|
1445
|
+
currentThreadId,
|
|
1446
|
+
isStreaming,
|
|
1447
|
+
userContextSeed,
|
|
1448
|
+
sendMessage: handleSendMessage,
|
|
1449
|
+
createThread: handleCreateThread,
|
|
1450
|
+
selectThread: handleSelectThread,
|
|
1451
|
+
renameThread: handleRenameThread,
|
|
1452
|
+
archiveThread: handleArchiveThread,
|
|
1453
|
+
deleteThread: handleDeleteThread,
|
|
1454
|
+
stopGeneration: handleStop,
|
|
1455
|
+
fetchAndSetThreadsState,
|
|
1456
|
+
loadThreadMessages,
|
|
1457
|
+
reset
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/CopilotzChat.tsx
|
|
1462
|
+
import { jsx } from "react/jsx-runtime";
|
|
1463
|
+
var CopilotzChat = ({
|
|
1464
|
+
userId,
|
|
1465
|
+
userName,
|
|
1466
|
+
userAvatar,
|
|
1467
|
+
userEmail,
|
|
1468
|
+
initialContext,
|
|
1469
|
+
bootstrap,
|
|
1470
|
+
config: userConfig,
|
|
1471
|
+
callbacks: userCallbacks,
|
|
1472
|
+
customComponent,
|
|
1473
|
+
onToolOutput,
|
|
1474
|
+
onLogout,
|
|
1475
|
+
onViewProfile,
|
|
1476
|
+
onAddMemory,
|
|
1477
|
+
onUpdateMemory,
|
|
1478
|
+
onDeleteMemory,
|
|
1479
|
+
className
|
|
1480
|
+
}) => {
|
|
1481
|
+
const {
|
|
1482
|
+
messages,
|
|
1483
|
+
threads,
|
|
1484
|
+
currentThreadId,
|
|
1485
|
+
isStreaming,
|
|
1486
|
+
userContextSeed,
|
|
1487
|
+
sendMessage,
|
|
1488
|
+
createThread,
|
|
1489
|
+
selectThread,
|
|
1490
|
+
renameThread,
|
|
1491
|
+
archiveThread,
|
|
1492
|
+
deleteThread: deleteThread2,
|
|
1493
|
+
stopGeneration
|
|
1494
|
+
} = useCopilotz({
|
|
1495
|
+
userId,
|
|
1496
|
+
initialContext,
|
|
1497
|
+
bootstrap,
|
|
1498
|
+
defaultThreadName: userConfig?.labels?.defaultThreadName,
|
|
1499
|
+
onToolOutput
|
|
1500
|
+
});
|
|
1501
|
+
const chatCallbacks = useMemo(() => ({
|
|
1502
|
+
onSendMessage: (content, attachments) => {
|
|
1503
|
+
void sendMessage(content, attachments);
|
|
1504
|
+
userCallbacks?.onSendMessage?.(content, attachments);
|
|
1505
|
+
},
|
|
1506
|
+
onStopGeneration: () => {
|
|
1507
|
+
stopGeneration();
|
|
1508
|
+
userCallbacks?.onStopGeneration?.();
|
|
1509
|
+
},
|
|
1510
|
+
onCreateThread: (title) => {
|
|
1511
|
+
createThread(title);
|
|
1512
|
+
userCallbacks?.onCreateThread?.(title);
|
|
1513
|
+
},
|
|
1514
|
+
onSelectThread: (threadId) => {
|
|
1515
|
+
void selectThread(threadId);
|
|
1516
|
+
userCallbacks?.onSelectThread?.(threadId);
|
|
1517
|
+
},
|
|
1518
|
+
onRenameThread: (threadId, newTitle) => {
|
|
1519
|
+
void renameThread(threadId, newTitle);
|
|
1520
|
+
userCallbacks?.onRenameThread?.(threadId, newTitle);
|
|
1521
|
+
},
|
|
1522
|
+
onArchiveThread: (threadId) => {
|
|
1523
|
+
void archiveThread(threadId);
|
|
1524
|
+
userCallbacks?.onArchiveThread?.(threadId);
|
|
1525
|
+
},
|
|
1526
|
+
onDeleteThread: (threadId) => {
|
|
1527
|
+
void deleteThread2(threadId);
|
|
1528
|
+
userCallbacks?.onDeleteThread?.(threadId);
|
|
1529
|
+
},
|
|
1530
|
+
onCopyMessage: async (messageId, content) => {
|
|
1531
|
+
try {
|
|
1532
|
+
await navigator.clipboard.writeText(content);
|
|
1533
|
+
userCallbacks?.onCopyMessage?.(messageId, content);
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
console.error("Failed to copy message", error);
|
|
1536
|
+
}
|
|
1537
|
+
},
|
|
1538
|
+
// User menu callbacks
|
|
1539
|
+
onLogout,
|
|
1540
|
+
onViewProfile,
|
|
1541
|
+
...userCallbacks
|
|
1542
|
+
}), [sendMessage, stopGeneration, createThread, selectThread, renameThread, archiveThread, deleteThread2, userCallbacks, onLogout, onViewProfile]);
|
|
1543
|
+
const mergedConfig = useMemo(() => {
|
|
1544
|
+
const base = userConfig || {};
|
|
1545
|
+
if (!customComponent) {
|
|
1546
|
+
return base;
|
|
1547
|
+
}
|
|
1548
|
+
return {
|
|
1549
|
+
...base,
|
|
1550
|
+
customComponent: {
|
|
1551
|
+
...base.customComponent,
|
|
1552
|
+
component: customComponent,
|
|
1553
|
+
icon: base.customComponent?.icon || /* @__PURE__ */ jsx(User, { className: "h-6 w-6" })
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
}, [userConfig, customComponent]);
|
|
1557
|
+
const effectiveUserName = userName || userId;
|
|
1558
|
+
const effectiveUserAvatar = userAvatar;
|
|
1559
|
+
return /* @__PURE__ */ jsx(ChatUserContextProvider, { initial: userContextSeed, children: /* @__PURE__ */ jsx(
|
|
1560
|
+
ChatUI,
|
|
1561
|
+
{
|
|
1562
|
+
messages,
|
|
1563
|
+
threads,
|
|
1564
|
+
currentThreadId,
|
|
1565
|
+
config: mergedConfig,
|
|
1566
|
+
callbacks: chatCallbacks,
|
|
1567
|
+
isGenerating: isStreaming,
|
|
1568
|
+
user: {
|
|
1569
|
+
id: userId,
|
|
1570
|
+
name: effectiveUserName,
|
|
1571
|
+
email: userEmail,
|
|
1572
|
+
avatar: effectiveUserAvatar
|
|
1573
|
+
},
|
|
1574
|
+
assistant: {
|
|
1575
|
+
name: userConfig?.branding?.title,
|
|
1576
|
+
avatar: userConfig?.branding?.avatar,
|
|
1577
|
+
description: userConfig?.branding?.subtitle
|
|
1578
|
+
},
|
|
1579
|
+
onAddMemory,
|
|
1580
|
+
onUpdateMemory,
|
|
1581
|
+
onDeleteMemory,
|
|
1582
|
+
className
|
|
1583
|
+
}
|
|
1584
|
+
) });
|
|
1585
|
+
};
|
|
1586
|
+
export {
|
|
1587
|
+
CopilotzChat,
|
|
1588
|
+
copilotzService,
|
|
1589
|
+
deleteMessagesByThreadId,
|
|
1590
|
+
deleteThread,
|
|
1591
|
+
fetchThreadMessages,
|
|
1592
|
+
fetchThreads,
|
|
1593
|
+
getAssetDataUrl,
|
|
1594
|
+
resolveAssetsInMessages,
|
|
1595
|
+
runCopilotzStream,
|
|
1596
|
+
updateThread,
|
|
1597
|
+
useCopilotz
|
|
1598
|
+
};
|
|
1599
|
+
/*! Bundled license information:
|
|
1600
|
+
|
|
1601
|
+
lucide-react/dist/esm/shared/src/utils.js:
|
|
1602
|
+
lucide-react/dist/esm/defaultAttributes.js:
|
|
1603
|
+
lucide-react/dist/esm/Icon.js:
|
|
1604
|
+
lucide-react/dist/esm/createLucideIcon.js:
|
|
1605
|
+
lucide-react/dist/esm/icons/user.js:
|
|
1606
|
+
lucide-react/dist/esm/lucide-react.js:
|
|
1607
|
+
(**
|
|
1608
|
+
* @license lucide-react v0.540.0 - ISC
|
|
1609
|
+
*
|
|
1610
|
+
* This source code is licensed under the ISC license.
|
|
1611
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1612
|
+
*)
|
|
1613
|
+
*/
|
|
1614
|
+
//# sourceMappingURL=index.js.map
|