@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/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