@bootdesk/js-web-adapter-react 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.cjs ADDED
@@ -0,0 +1,2536 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AttachmentList: () => AttachmentList,
34
+ CardProvider: () => CardProvider,
35
+ CardRenderer: () => CardRenderer,
36
+ ChatProvider: () => ChatProvider,
37
+ ChatWidget: () => ChatWidget,
38
+ DefaultCard: () => DefaultCard,
39
+ Dropzone: () => Dropzone,
40
+ ErrorBoundary: () => ErrorBoundary,
41
+ FileCardComponent: () => FileCardComponent,
42
+ FloatingButton: () => FloatingButton,
43
+ Header: () => Header,
44
+ ImageCardComponent: () => ImageCardComponent,
45
+ InputArea: () => InputArea,
46
+ LaravelEchoBroadcastClient: () => import_js_web_adapter_core4.LaravelEchoBroadcastClient,
47
+ LocaleProvider: () => LocaleProvider,
48
+ MarkdownRenderer: () => MarkdownRenderer,
49
+ MessageContent: () => MessageContent,
50
+ MessageList: () => MessageList,
51
+ PushManager: () => import_js_web_adapter_core3.PushManager,
52
+ PushPermissionPrompt: () => PushPermissionPrompt,
53
+ PushToggle: () => PushToggle,
54
+ PusherBroadcastClient: () => import_js_web_adapter_core4.PusherBroadcastClient,
55
+ TypingIndicator: () => TypingIndicator,
56
+ WebChatClient: () => import_js_web_adapter_core4.WebChatClient,
57
+ createPushSubscriptionHandlers: () => import_js_web_adapter_core3.createPushSubscriptionHandlers,
58
+ getAvailableLocales: () => getAvailableLocales,
59
+ registerLocale: () => registerLocale,
60
+ renderMarkdown: () => renderMarkdown,
61
+ useAttachmentUpload: () => useAttachmentUpload,
62
+ useCardRegistry: () => useCardRegistry,
63
+ useCardRendererRegistry: () => useCardRegistry,
64
+ useChatClient: () => useChatClient,
65
+ useChatContext: () => useChatContext,
66
+ useLocale: () => useLocale,
67
+ useMessages: () => useMessages,
68
+ usePushNotifications: () => usePushNotifications,
69
+ useStreaming: () => useStreaming,
70
+ useTyping: () => useTyping
71
+ });
72
+ module.exports = __toCommonJS(index_exports);
73
+
74
+ // src/components/ChatWidget.tsx
75
+ var import_react13 = require("react");
76
+
77
+ // src/hooks/useChatClient.ts
78
+ var import_react = require("react");
79
+ var import_js_web_adapter_core = require("@bootdesk/js-web-adapter-core");
80
+ function useChatClient(config) {
81
+ const client = (0, import_react.useMemo)(() => new import_js_web_adapter_core.WebChatClient(config), [config]);
82
+ (0, import_react.useEffect)(() => {
83
+ client.connect().catch((error) => {
84
+ console.error("Failed to connect chat client:", error);
85
+ });
86
+ return () => {
87
+ client.disconnect();
88
+ };
89
+ }, [client]);
90
+ return client;
91
+ }
92
+
93
+ // src/hooks/useMessages.ts
94
+ var import_react2 = require("react");
95
+ function useMessages(client, enabled = true) {
96
+ const [messages, setMessages] = (0, import_react2.useState)([]);
97
+ const [loading, setLoading] = (0, import_react2.useState)(false);
98
+ const [isLoadingHistory, setIsLoadingHistory] = (0, import_react2.useState)(false);
99
+ const [hasMore, setHasMore] = (0, import_react2.useState)(false);
100
+ const [nextCursor, setNextCursor] = (0, import_react2.useState)(void 0);
101
+ const [loadError, setLoadError] = (0, import_react2.useState)(null);
102
+ const abortRef = (0, import_react2.useRef)(null);
103
+ (0, import_react2.useEffect)(() => {
104
+ if (!enabled) return;
105
+ if (abortRef.current) {
106
+ abortRef.current.abort();
107
+ }
108
+ const controller = new AbortController();
109
+ abortRef.current = controller;
110
+ const { signal } = controller;
111
+ setIsLoadingHistory(true);
112
+ setLoadError(null);
113
+ const loadInitial = async () => {
114
+ try {
115
+ const result = await client.loadMessages({ limit: 50, skipStateSeed: true }, signal);
116
+ if (signal.aborted) return;
117
+ setMessages(result.messages);
118
+ setHasMore(result.hasMore);
119
+ setNextCursor(result.nextCursor);
120
+ } catch (error) {
121
+ if (signal.aborted) return;
122
+ setLoadError(error instanceof Error ? error : new Error("Failed to load messages"));
123
+ } finally {
124
+ if (!signal.aborted) {
125
+ setIsLoadingHistory(false);
126
+ }
127
+ }
128
+ };
129
+ loadInitial();
130
+ return () => {
131
+ controller.abort();
132
+ if (abortRef.current === controller) {
133
+ abortRef.current = null;
134
+ }
135
+ };
136
+ }, [client, enabled]);
137
+ const reloadMessages = (0, import_react2.useCallback)(async () => {
138
+ setIsLoadingHistory(true);
139
+ setLoadError(null);
140
+ try {
141
+ const result = await client.loadMessages({ limit: 50, skipStateSeed: true });
142
+ setMessages(result.messages);
143
+ setHasMore(result.hasMore);
144
+ setNextCursor(result.nextCursor);
145
+ } catch (error) {
146
+ setLoadError(error instanceof Error ? error : new Error("Failed to reload messages"));
147
+ } finally {
148
+ setIsLoadingHistory(false);
149
+ }
150
+ }, [client]);
151
+ const retryLoad = (0, import_react2.useCallback)(async () => {
152
+ setLoadError(null);
153
+ setIsLoadingHistory(true);
154
+ try {
155
+ const result = await client.loadMessages({ limit: 50, skipStateSeed: true });
156
+ setMessages(result.messages);
157
+ setHasMore(result.hasMore);
158
+ setNextCursor(result.nextCursor);
159
+ } catch (error) {
160
+ setLoadError(error instanceof Error ? error : new Error("Failed to load messages"));
161
+ } finally {
162
+ setIsLoadingHistory(false);
163
+ }
164
+ }, [client]);
165
+ (0, import_react2.useEffect)(() => {
166
+ const unsubscribes = [];
167
+ unsubscribes.push(
168
+ client.addEventListener("message:added", (message) => {
169
+ setMessages((prev) => {
170
+ if (prev.some((m) => m.id === message.id)) return prev;
171
+ return [...prev, message];
172
+ });
173
+ })
174
+ );
175
+ const features2 = client.getFeatures();
176
+ if (features2.editMessages) {
177
+ unsubscribes.push(
178
+ client.addEventListener(
179
+ "message:edited",
180
+ ({ messageId, newText }) => {
181
+ setMessages(
182
+ (prev) => prev.map(
183
+ (msg) => msg.id === messageId ? { ...msg, content: { ...msg.content, text: newText } } : msg
184
+ )
185
+ );
186
+ }
187
+ )
188
+ );
189
+ }
190
+ if (features2.deleteMessages) {
191
+ unsubscribes.push(
192
+ client.addEventListener("message:deleted", ({ messageId }) => {
193
+ setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
194
+ })
195
+ );
196
+ }
197
+ if (features2.reactions) {
198
+ unsubscribes.push(
199
+ client.addEventListener(
200
+ "reaction:added",
201
+ ({ messageId, emoji }) => {
202
+ setMessages(
203
+ (prev) => prev.map((msg) => {
204
+ if (msg.id !== messageId || !msg.reactions) return msg;
205
+ const existing = msg.reactions.find((r) => r.emoji === emoji);
206
+ if (existing) {
207
+ return {
208
+ ...msg,
209
+ reactions: msg.reactions.map(
210
+ (r) => r.emoji === emoji ? { ...r, count: r.count + 1 } : r
211
+ )
212
+ };
213
+ }
214
+ return {
215
+ ...msg,
216
+ reactions: [...msg.reactions, { emoji, count: 1, users: [] }]
217
+ };
218
+ })
219
+ );
220
+ }
221
+ )
222
+ );
223
+ unsubscribes.push(
224
+ client.addEventListener(
225
+ "reaction:removed",
226
+ ({ messageId, emoji }) => {
227
+ setMessages(
228
+ (prev) => prev.map((msg) => {
229
+ if (msg.id !== messageId || !msg.reactions) return msg;
230
+ const idx = msg.reactions.findIndex((r) => r.emoji === emoji);
231
+ if (idx === -1) return msg;
232
+ const updated = [...msg.reactions];
233
+ if (updated[idx].count <= 1) {
234
+ updated.splice(idx, 1);
235
+ } else {
236
+ updated[idx] = { ...updated[idx], count: updated[idx].count - 1 };
237
+ }
238
+ return { ...msg, reactions: updated };
239
+ })
240
+ );
241
+ }
242
+ )
243
+ );
244
+ }
245
+ return () => {
246
+ unsubscribes.forEach((unsub) => unsub());
247
+ };
248
+ }, [client]);
249
+ const sendMessage = (0, import_react2.useCallback)(
250
+ async (text, attachments) => {
251
+ setLoading(true);
252
+ try {
253
+ await client.sendMessage(text, attachments || []);
254
+ } finally {
255
+ setLoading(false);
256
+ }
257
+ },
258
+ [client]
259
+ );
260
+ const editMessage = (0, import_react2.useCallback)(
261
+ async (id, text) => {
262
+ if (!client.getFeatures().editMessages) {
263
+ throw new Error("Edit messages not enabled. Set features.editMessages = true.");
264
+ }
265
+ const endpoint = client.getEndpoints().editMessage || "/api/chat/messages/{id}/edit";
266
+ await client.getHttpClient().editMessage(id, text, endpoint);
267
+ setMessages(
268
+ (prev) => prev.map((msg) => msg.id === id ? { ...msg, content: { ...msg.content, text } } : msg)
269
+ );
270
+ },
271
+ [client]
272
+ );
273
+ const deleteMessage = (0, import_react2.useCallback)(
274
+ async (id) => {
275
+ if (!client.getFeatures().deleteMessages) {
276
+ throw new Error("Delete messages not enabled. Set features.deleteMessages = true.");
277
+ }
278
+ const endpoint = client.getEndpoints().deleteMessage || "/api/chat/messages/{id}";
279
+ await client.getHttpClient().deleteMessage(id, endpoint);
280
+ setMessages((prev) => prev.filter((msg) => msg.id !== id));
281
+ },
282
+ [client]
283
+ );
284
+ const addReaction = (0, import_react2.useCallback)(
285
+ async (id, emoji) => {
286
+ await client.addReaction(id, emoji);
287
+ },
288
+ [client]
289
+ );
290
+ const removeReaction = (0, import_react2.useCallback)(
291
+ async (id, emoji) => {
292
+ await client.removeReaction(id, emoji);
293
+ },
294
+ [client]
295
+ );
296
+ const loadMore = (0, import_react2.useCallback)(async () => {
297
+ if (!nextCursor || isLoadingHistory) return;
298
+ setIsLoadingHistory(true);
299
+ try {
300
+ const result = await client.loadMessages({
301
+ limit: 50,
302
+ before: nextCursor
303
+ });
304
+ setMessages((prev) => [...result.messages, ...prev]);
305
+ setHasMore(result.hasMore);
306
+ setNextCursor(result.nextCursor);
307
+ } catch (error) {
308
+ console.error("Failed to load more messages:", error);
309
+ } finally {
310
+ setIsLoadingHistory(false);
311
+ }
312
+ }, [client, nextCursor, isLoadingHistory]);
313
+ const features = client.getFeatures();
314
+ const canEdit = !!features.editMessages;
315
+ const canDelete = !!features.deleteMessages;
316
+ const canReact = !!features.reactions;
317
+ return {
318
+ messages,
319
+ loading,
320
+ isLoadingHistory,
321
+ hasMore,
322
+ loadMore,
323
+ reloadMessages,
324
+ retryLoad,
325
+ loadError,
326
+ canEdit,
327
+ canDelete,
328
+ canReact,
329
+ sendMessage,
330
+ editMessage,
331
+ deleteMessage,
332
+ addReaction,
333
+ removeReaction
334
+ };
335
+ }
336
+
337
+ // src/hooks/useStreaming.ts
338
+ var import_react3 = require("react");
339
+ function useStreaming(client) {
340
+ const [streamingMessages, setStreamingMessages] = (0, import_react3.useState)(
341
+ /* @__PURE__ */ new Map()
342
+ );
343
+ (0, import_react3.useEffect)(() => {
344
+ const unsub = client.onStreamingChunk((event) => {
345
+ setStreamingMessages((prev) => {
346
+ const next = new Map(prev);
347
+ const existing = next.get(event.messageId);
348
+ const newText = (existing?.fullText || "") + event.chunk;
349
+ if (event.isFinal) {
350
+ next.delete(event.messageId);
351
+ } else {
352
+ next.set(event.messageId, {
353
+ messageId: event.messageId,
354
+ fullText: newText,
355
+ isComplete: event.isFinal
356
+ });
357
+ }
358
+ return next;
359
+ });
360
+ });
361
+ return unsub;
362
+ }, [client]);
363
+ const isStreaming = streamingMessages.size > 0;
364
+ return { streamingMessages, isStreaming };
365
+ }
366
+
367
+ // src/hooks/useTyping.ts
368
+ var import_react4 = require("react");
369
+ function useTyping(client) {
370
+ const [typingUsers, setTypingUsers] = (0, import_react4.useState)(/* @__PURE__ */ new Set());
371
+ const timeoutsRef = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
372
+ (0, import_react4.useEffect)(() => {
373
+ const unsub = client.onTypingStarted((event) => {
374
+ const existing = timeoutsRef.current.get(event.userId);
375
+ if (existing) clearTimeout(existing);
376
+ setTypingUsers((prev) => new Set(prev).add(event.userId));
377
+ const timeoutId = setTimeout(() => {
378
+ timeoutsRef.current.delete(event.userId);
379
+ setTypingUsers((prev) => {
380
+ const next = new Set(prev);
381
+ next.delete(event.userId);
382
+ return next;
383
+ });
384
+ }, 3e3);
385
+ timeoutsRef.current.set(event.userId, timeoutId);
386
+ });
387
+ return () => {
388
+ unsub();
389
+ for (const id of timeoutsRef.current.values()) {
390
+ clearTimeout(id);
391
+ }
392
+ timeoutsRef.current.clear();
393
+ };
394
+ }, [client]);
395
+ const isSomeoneTyping = typingUsers.size > 0;
396
+ return { typingUsers, isSomeoneTyping };
397
+ }
398
+
399
+ // src/hooks/usePushNotifications.ts
400
+ var import_react5 = require("react");
401
+ var import_js_web_adapter_core2 = require("@bootdesk/js-web-adapter-core");
402
+ function usePushNotifications(options) {
403
+ const { enabled = false } = options;
404
+ const [status, setStatus] = (0, import_react5.useState)("unsupported");
405
+ const pushManagerRef = (0, import_react5.useRef)(null);
406
+ (0, import_react5.useEffect)(() => {
407
+ if (!enabled) return;
408
+ const pushManager = new import_js_web_adapter_core2.PushManager({
409
+ getVapidPublicKey: options.getVapidPublicKey,
410
+ onSubscribe: options.onSubscribe,
411
+ onUnsubscribe: options.onUnsubscribe,
412
+ serviceWorkerUrl: options.serviceWorkerUrl,
413
+ notificationOptions: options.notificationOptions
414
+ });
415
+ pushManagerRef.current = pushManager;
416
+ const unsubscribeStatus = pushManager.onStatusChange(setStatus);
417
+ pushManager.initialize();
418
+ return () => {
419
+ unsubscribeStatus();
420
+ };
421
+ }, [enabled]);
422
+ const subscribe = (0, import_react5.useCallback)(async () => {
423
+ await pushManagerRef.current?.subscribe();
424
+ }, []);
425
+ const unsubscribe = (0, import_react5.useCallback)(async () => {
426
+ await pushManagerRef.current?.unsubscribe();
427
+ }, []);
428
+ return {
429
+ status,
430
+ isSupported: import_js_web_adapter_core2.PushManager.isSupported(),
431
+ isSubscribed: status === "subscribed",
432
+ subscribe,
433
+ unsubscribe
434
+ };
435
+ }
436
+
437
+ // src/hooks/useAttachmentUpload.ts
438
+ var import_react6 = require("react");
439
+
440
+ // src/types/AttachmentUpload.ts
441
+ function isMultiStepUpload(config) {
442
+ return "requestSignedUrl" in config;
443
+ }
444
+
445
+ // src/hooks/useAttachmentUpload.ts
446
+ function generateAttachmentId() {
447
+ return `att-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
448
+ }
449
+ function useAttachmentUpload(uploadConfig) {
450
+ const [attachments, setAttachments] = (0, import_react6.useState)([]);
451
+ const abortControllers = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
452
+ const uploadFileRef = (0, import_react6.useRef)();
453
+ const addFiles = (0, import_react6.useCallback)((files) => {
454
+ const fileArray = Array.isArray(files) ? files : Array.from(files);
455
+ const newAttachments = fileArray.map((file) => ({
456
+ id: generateAttachmentId(),
457
+ file,
458
+ name: file.name,
459
+ mimeType: file.type,
460
+ size: file.size,
461
+ status: "pending",
462
+ progress: 0
463
+ }));
464
+ setAttachments((prev) => [...prev, ...newAttachments]);
465
+ newAttachments.forEach((att) => uploadFileRef.current?.(att));
466
+ }, []);
467
+ const uploadFile = (0, import_react6.useCallback)(
468
+ async (attachment) => {
469
+ const controller = new AbortController();
470
+ abortControllers.current.set(attachment.id, controller);
471
+ try {
472
+ setAttachments(
473
+ (prev) => prev.map(
474
+ (a) => a.id === attachment.id ? { ...a, status: "uploading", progress: 0 } : a
475
+ )
476
+ );
477
+ if (isMultiStepUpload(uploadConfig)) {
478
+ const signedUrl = await uploadConfig.requestSignedUrl({
479
+ name: attachment.name,
480
+ mimeType: attachment.mimeType,
481
+ size: attachment.size
482
+ });
483
+ if (controller.signal.aborted) return;
484
+ const uploadSuccess = await uploadConfig.uploadToSignedUrl(
485
+ signedUrl,
486
+ attachment.file,
487
+ (progress) => {
488
+ setAttachments(
489
+ (prev) => prev.map((a) => a.id === attachment.id ? { ...a, progress } : a)
490
+ );
491
+ }
492
+ );
493
+ if (controller.signal.aborted) return;
494
+ if (!uploadSuccess) {
495
+ throw new Error("Upload to signed URL failed");
496
+ }
497
+ const finalUrl = await uploadConfig.confirmUpload(signedUrl, {
498
+ name: attachment.name,
499
+ mimeType: attachment.mimeType,
500
+ size: attachment.size
501
+ });
502
+ setAttachments(
503
+ (prev) => prev.map(
504
+ (a) => a.id === attachment.id ? { ...a, status: "uploaded", progress: 100, url: finalUrl } : a
505
+ )
506
+ );
507
+ } else {
508
+ const formData = new FormData();
509
+ formData.append("file", attachment.file);
510
+ const xhr = new XMLHttpRequest();
511
+ xhr.upload.addEventListener("progress", (e) => {
512
+ if (e.lengthComputable) {
513
+ const progress = Math.round(e.loaded / e.total * 100);
514
+ setAttachments(
515
+ (prev) => prev.map((a) => a.id === attachment.id ? { ...a, progress } : a)
516
+ );
517
+ }
518
+ });
519
+ const response = await new Promise((resolve, reject) => {
520
+ xhr.onload = () => {
521
+ if (xhr.status >= 200 && xhr.status < 300) {
522
+ resolve(JSON.parse(xhr.responseText));
523
+ } else {
524
+ reject(new Error(`Upload failed: ${xhr.status}`));
525
+ }
526
+ };
527
+ xhr.onerror = () => reject(new Error("Network error"));
528
+ xhr.onabort = () => reject(new Error("Upload cancelled"));
529
+ xhr.open("POST", uploadConfig.endpoint);
530
+ if (uploadConfig.headers) {
531
+ Object.entries(uploadConfig.headers).forEach(([key, value]) => {
532
+ xhr.setRequestHeader(key, value);
533
+ });
534
+ }
535
+ xhr.send(formData);
536
+ });
537
+ setAttachments(
538
+ (prev) => prev.map(
539
+ (a) => a.id === attachment.id ? { ...a, status: "uploaded", progress: 100, url: response.url } : a
540
+ )
541
+ );
542
+ }
543
+ } catch (error) {
544
+ if (controller.signal.aborted) return;
545
+ setAttachments(
546
+ (prev) => prev.map(
547
+ (a) => a.id === attachment.id ? {
548
+ ...a,
549
+ status: "error",
550
+ error: error instanceof Error ? error.message : "Upload failed"
551
+ } : a
552
+ )
553
+ );
554
+ } finally {
555
+ abortControllers.current.delete(attachment.id);
556
+ }
557
+ },
558
+ [uploadConfig]
559
+ );
560
+ uploadFileRef.current = uploadFile;
561
+ const removeAttachment = (0, import_react6.useCallback)((id) => {
562
+ const controller = abortControllers.current.get(id);
563
+ if (controller) {
564
+ controller.abort();
565
+ }
566
+ setAttachments((prev) => prev.filter((a) => a.id !== id));
567
+ }, []);
568
+ const clearAttachments = (0, import_react6.useCallback)(() => {
569
+ abortControllers.current.forEach((c) => c.abort());
570
+ abortControllers.current.clear();
571
+ setAttachments([]);
572
+ }, []);
573
+ const resetUploads = (0, import_react6.useCallback)(() => {
574
+ setAttachments(
575
+ (prev) => prev.map(
576
+ (a) => a.status === "error" ? { ...a, status: "pending", progress: 0, error: void 0 } : a
577
+ )
578
+ );
579
+ }, []);
580
+ const getUploadedAttachments = (0, import_react6.useCallback)(() => {
581
+ return attachments.filter((a) => a.status === "uploaded" && a.url);
582
+ }, [attachments]);
583
+ const isUploading = attachments.some((a) => a.status === "uploading");
584
+ const isComplete = attachments.length > 0 && attachments.every((a) => a.status === "uploaded" || a.status === "error");
585
+ return {
586
+ attachments,
587
+ addFiles,
588
+ removeAttachment,
589
+ clearAttachments,
590
+ resetUploads,
591
+ getUploadedAttachments,
592
+ isUploading,
593
+ isComplete
594
+ };
595
+ }
596
+
597
+ // src/hooks/useBridge.ts
598
+ var import_react7 = require("react");
599
+ function useBridge() {
600
+ const notificationCbRef = (0, import_react7.useRef)(null);
601
+ const [config, setConfig] = (0, import_react7.useState)(null);
602
+ const isInIframe = typeof window !== "undefined" && window !== window.parent;
603
+ const notifyMessage = (0, import_react7.useCallback)(
604
+ (text) => {
605
+ if (!isInIframe) return;
606
+ window.parent.postMessage({ type: "chat-message", text }, "*");
607
+ },
608
+ [isInIframe]
609
+ );
610
+ const notifyViewportConfig = (0, import_react7.useCallback)(
611
+ (viewportContent) => {
612
+ if (!isInIframe) return;
613
+ window.parent.postMessage({ type: "chat-viewport-config", content: viewportContent }, "*");
614
+ },
615
+ [isInIframe]
616
+ );
617
+ const onNotificationClicked = (0, import_react7.useCallback)((cb) => {
618
+ notificationCbRef.current = cb;
619
+ }, []);
620
+ (0, import_react7.useEffect)(() => {
621
+ if (!isInIframe) return;
622
+ function handleMessage(event) {
623
+ const data = event.data;
624
+ if (!data || typeof data !== "object" || !data.type) return;
625
+ if (data.type === "chat-config") {
626
+ const configData = { ...data };
627
+ delete configData.type;
628
+ setConfig(configData);
629
+ }
630
+ if (data.type === "chat-notification-clicked") {
631
+ notificationCbRef.current?.();
632
+ }
633
+ }
634
+ window.addEventListener("message", handleMessage);
635
+ return () => window.removeEventListener("message", handleMessage);
636
+ }, [isInIframe]);
637
+ return { config, isInIframe, notifyMessage, notifyViewportConfig, onNotificationClicked };
638
+ }
639
+
640
+ // src/i18n/LocaleProvider.tsx
641
+ var import_react8 = require("react");
642
+
643
+ // src/i18n/types.ts
644
+ function getBaseLocale(locale) {
645
+ return locale.split("-")[0] || locale;
646
+ }
647
+ function getFallbackChain(locale) {
648
+ const base = getBaseLocale(locale);
649
+ if (locale === base) {
650
+ return [locale, "en"];
651
+ }
652
+ return [locale, base, "en"];
653
+ }
654
+
655
+ // src/i18n/locales/en.json
656
+ var en_default = {
657
+ chatWidget: {
658
+ title: "Chat",
659
+ placeholder: "Type a message...",
660
+ openChat: "Open chat",
661
+ closeChat: "Close chat",
662
+ connectionStatus: {
663
+ connected: "Connected",
664
+ disconnected: "Disconnected"
665
+ }
666
+ },
667
+ inputArea: {
668
+ send: "Send",
669
+ uploading: "Uploading...",
670
+ dropzone: {
671
+ dropFiles: "Drop files here",
672
+ dropOrClick: "Drop or click to attach"
673
+ }
674
+ },
675
+ typingIndicator: {
676
+ typing: "typing",
677
+ isTyping: "is typing..."
678
+ },
679
+ messageList: {
680
+ emptyState: "No messages yet. Start the conversation!"
681
+ },
682
+ attachmentList: {
683
+ remove: "Remove",
684
+ uploadFailed: "Upload failed"
685
+ },
686
+ header: {
687
+ enterFullscreen: "Enter fullscreen",
688
+ exitFullscreen: "Exit fullscreen",
689
+ closeChat: "Close chat",
690
+ lightMode: "Light mode",
691
+ darkMode: "Dark mode",
692
+ autoMode: "System theme"
693
+ },
694
+ floatingButton: {
695
+ openChat: "Open chat",
696
+ closeChat: "Close chat"
697
+ },
698
+ common: {
699
+ loading: "Loading...",
700
+ error: "Error",
701
+ retry: "Retry",
702
+ cancel: "Cancel",
703
+ download: "Download"
704
+ }
705
+ };
706
+
707
+ // src/i18n/locales/en-US.json
708
+ var en_US_default = {
709
+ chatWidget: {
710
+ title: "Chat"
711
+ }
712
+ };
713
+
714
+ // src/i18n/locales/en-GB.json
715
+ var en_GB_default = {
716
+ chatWidget: {
717
+ title: "Chat"
718
+ },
719
+ common: {
720
+ loading: "Loading..."
721
+ }
722
+ };
723
+
724
+ // src/i18n/locales/pt.json
725
+ var pt_default = {
726
+ chatWidget: {
727
+ title: "Chat",
728
+ placeholder: "Digite uma mensagem...",
729
+ openChat: "Abrir chat",
730
+ closeChat: "Fechar chat",
731
+ connectionStatus: {
732
+ connected: "Conectado",
733
+ disconnected: "Desconectado"
734
+ }
735
+ },
736
+ inputArea: {
737
+ send: "Enviar",
738
+ uploading: "Enviando...",
739
+ dropzone: {
740
+ dropFiles: "Solte arquivos aqui",
741
+ dropOrClick: "Solte ou clique para anexar"
742
+ }
743
+ },
744
+ typingIndicator: {
745
+ typing: "digitando",
746
+ isTyping: "est\xE1 digitando..."
747
+ },
748
+ messageList: {
749
+ emptyState: "Nenhuma mensagem ainda. Inicie a conversa!"
750
+ },
751
+ attachmentList: {
752
+ remove: "Remover",
753
+ uploadFailed: "Falha no envio"
754
+ },
755
+ header: {
756
+ enterFullscreen: "Tela cheia",
757
+ exitFullscreen: "Sair da tela cheia",
758
+ closeChat: "Fechar chat"
759
+ },
760
+ floatingButton: {
761
+ openChat: "Abrir chat",
762
+ closeChat: "Fechar chat"
763
+ },
764
+ common: {
765
+ loading: "Carregando...",
766
+ error: "Erro",
767
+ retry: "Tentar novamente",
768
+ cancel: "Cancelar",
769
+ download: "Baixar"
770
+ }
771
+ };
772
+
773
+ // src/i18n/locales/pt-BR.json
774
+ var pt_BR_default = {
775
+ chatWidget: {
776
+ placeholder: "Digite uma mensagem..."
777
+ },
778
+ inputArea: {
779
+ send: "Enviar",
780
+ uploading: "Enviando...",
781
+ dropzone: {
782
+ dropFiles: "Solte arquivos aqui",
783
+ dropOrClick: "Solte ou clique para anexar"
784
+ }
785
+ },
786
+ typingIndicator: {
787
+ isTyping: "est\xE1 digitando..."
788
+ },
789
+ attachmentList: {
790
+ uploadFailed: "Falha no envio"
791
+ },
792
+ common: {
793
+ loading: "Carregando...",
794
+ retry: "Tentar novamente"
795
+ }
796
+ };
797
+
798
+ // src/i18n/locales/pt-PT.json
799
+ var pt_PT_default = {
800
+ chatWidget: {
801
+ placeholder: "Escreva uma mensagem..."
802
+ },
803
+ inputArea: {
804
+ send: "Enviar",
805
+ uploading: "A enviar...",
806
+ dropzone: {
807
+ dropFiles: "Largue ficheiros aqui",
808
+ dropOrClick: "Largue ou clique para anexar"
809
+ }
810
+ },
811
+ typingIndicator: {
812
+ isTyping: "est\xE1 a escrever..."
813
+ },
814
+ attachmentList: {
815
+ uploadFailed: "Falha no envio"
816
+ },
817
+ common: {
818
+ loading: "A carregar...",
819
+ retry: "Tentar novamente"
820
+ }
821
+ };
822
+
823
+ // src/i18n/locales/es.json
824
+ var es_default = {
825
+ chatWidget: {
826
+ title: "Chat",
827
+ placeholder: "Escribe un mensaje...",
828
+ openChat: "Abrir chat",
829
+ closeChat: "Cerrar chat",
830
+ connectionStatus: {
831
+ connected: "Conectado",
832
+ disconnected: "Desconectado"
833
+ }
834
+ },
835
+ inputArea: {
836
+ send: "Enviar",
837
+ uploading: "Subiendo...",
838
+ dropzone: {
839
+ dropFiles: "Suelta archivos aqu\xED",
840
+ dropOrClick: "Suelta o haz clic para adjuntar"
841
+ }
842
+ },
843
+ typingIndicator: {
844
+ typing: "escribiendo",
845
+ isTyping: "est\xE1 escribiendo..."
846
+ },
847
+ messageList: {
848
+ emptyState: "No hay mensajes todav\xEDa. \xA1Inicia la conversaci\xF3n!"
849
+ },
850
+ attachmentList: {
851
+ remove: "Eliminar",
852
+ uploadFailed: "Error al subir"
853
+ },
854
+ header: {
855
+ enterFullscreen: "Pantalla completa",
856
+ exitFullscreen: "Salir de pantalla completa",
857
+ closeChat: "Cerrar chat"
858
+ },
859
+ floatingButton: {
860
+ openChat: "Abrir chat",
861
+ closeChat: "Cerrar chat"
862
+ },
863
+ common: {
864
+ loading: "Cargando...",
865
+ error: "Error",
866
+ retry: "Reintentar",
867
+ cancel: "Cancelar",
868
+ download: "Descargar"
869
+ }
870
+ };
871
+
872
+ // src/i18n/mergeLocale.ts
873
+ function deepMerge(target, source) {
874
+ const result = { ...target };
875
+ for (const key of Object.keys(source)) {
876
+ const sourceVal = source[key];
877
+ const targetVal = target[key];
878
+ if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)) {
879
+ result[key] = deepMerge(targetVal, sourceVal);
880
+ } else if (sourceVal !== void 0) {
881
+ result[key] = sourceVal;
882
+ }
883
+ }
884
+ return result;
885
+ }
886
+ var systemLocales = {
887
+ en: en_default,
888
+ "en-US": deepMerge(en_default, en_US_default),
889
+ "en-GB": deepMerge(en_default, en_GB_default),
890
+ pt: pt_default,
891
+ "pt-BR": deepMerge(pt_default, pt_BR_default),
892
+ "pt-PT": deepMerge(pt_default, pt_PT_default),
893
+ es: es_default
894
+ };
895
+ function registerLocale(locale, strings) {
896
+ systemLocales[locale] = strings;
897
+ }
898
+ function mergeLocale(locale, overrides) {
899
+ const chain = getFallbackChain(locale);
900
+ let base = systemLocales["en"] || en_default;
901
+ for (const code of chain) {
902
+ if (code === "en") continue;
903
+ const localeStrings = systemLocales[code];
904
+ if (localeStrings) {
905
+ base = deepMerge(base, localeStrings);
906
+ }
907
+ }
908
+ if (!overrides) return base;
909
+ return deepMerge(base, overrides);
910
+ }
911
+ function getAvailableLocales() {
912
+ return Object.keys(systemLocales);
913
+ }
914
+
915
+ // src/i18n/LocaleProvider.tsx
916
+ var import_jsx_runtime = require("react/jsx-runtime");
917
+ var LocaleContext = (0, import_react8.createContext)(void 0);
918
+ function LocaleProvider({ children, locale }) {
919
+ const config = (0, import_react8.useMemo)(() => {
920
+ if (!locale) return { locale: "en" };
921
+ if (typeof locale === "string") return { locale };
922
+ return locale;
923
+ }, [locale]);
924
+ const value = (0, import_react8.useMemo)(() => {
925
+ const strings = mergeLocale(config.locale, config.overrides);
926
+ const t = (path) => {
927
+ const parts = path.split(".");
928
+ let current = strings;
929
+ for (const part of parts) {
930
+ if (current == null) return path;
931
+ current = current[part];
932
+ }
933
+ return typeof current === "string" ? current : path;
934
+ };
935
+ return { locale: config.locale, strings, t };
936
+ }, [config.locale, config.overrides]);
937
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LocaleContext.Provider, { value, children });
938
+ }
939
+ function useLocale() {
940
+ const context = (0, import_react8.useContext)(LocaleContext);
941
+ if (!context) {
942
+ const strings = mergeLocale("en");
943
+ return {
944
+ locale: "en",
945
+ strings,
946
+ t: (path) => {
947
+ const parts = path.split(".");
948
+ let current = strings;
949
+ for (const part of parts) {
950
+ if (current == null) return path;
951
+ current = current[part];
952
+ }
953
+ return typeof current === "string" ? current : path;
954
+ }
955
+ };
956
+ }
957
+ return context;
958
+ }
959
+
960
+ // src/components/FloatingButton.tsx
961
+ var import_jsx_runtime2 = require("react/jsx-runtime");
962
+ function FloatingButton({
963
+ onClick,
964
+ isOpen,
965
+ position = "bottom-right",
966
+ className,
967
+ icon,
968
+ openIcon,
969
+ badgeCount,
970
+ size = 60,
971
+ backgroundColor,
972
+ ariaLabel
973
+ }) {
974
+ const { t } = useLocale();
975
+ const positionClasses = {
976
+ "bottom-right": "fixed bottom-5 right-5",
977
+ "bottom-left": "fixed bottom-5 left-5",
978
+ "top-right": "fixed top-5 right-5",
979
+ "top-left": "fixed top-5 left-5"
980
+ };
981
+ const iconSize = Math.floor(size * 0.4);
982
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
983
+ "button",
984
+ {
985
+ onClick,
986
+ className: `${positionClasses[position]} chat-floating-button hover:scale-105 transition-transform ${className || ""}`,
987
+ style: { width: size, height: size, backgroundColor },
988
+ "data-chat-floating-button": "true",
989
+ "data-testid": "chat-floating-button",
990
+ "aria-label": ariaLabel || (isOpen ? t("floatingButton.closeChat") : t("floatingButton.openChat")),
991
+ children: [
992
+ badgeCount && badgeCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
993
+ "span",
994
+ {
995
+ className: "absolute -top-1 -right-1 min-w-5 h-5 rounded-full bg-chat-error text-white text-xs font-bold flex items-center justify-center px-1",
996
+ "data-chat-badge": "true",
997
+ children: badgeCount > 99 ? "99+" : badgeCount
998
+ }
999
+ ),
1000
+ isOpen ? openIcon || icon || /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1001
+ "svg",
1002
+ {
1003
+ width: iconSize,
1004
+ height: iconSize,
1005
+ viewBox: "0 0 24 24",
1006
+ fill: "none",
1007
+ stroke: "#fff",
1008
+ strokeWidth: "2",
1009
+ strokeLinecap: "round",
1010
+ strokeLinejoin: "round",
1011
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 6L6 18M6 6l12 12" })
1012
+ }
1013
+ ) : icon || /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1014
+ "svg",
1015
+ {
1016
+ width: iconSize,
1017
+ height: iconSize,
1018
+ viewBox: "0 0 24 24",
1019
+ fill: "none",
1020
+ stroke: "#fff",
1021
+ strokeWidth: "2",
1022
+ strokeLinecap: "round",
1023
+ strokeLinejoin: "round",
1024
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
1025
+ }
1026
+ )
1027
+ ]
1028
+ }
1029
+ );
1030
+ }
1031
+
1032
+ // src/components/Header.tsx
1033
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1034
+ function Header({
1035
+ title = "Chat",
1036
+ onClose,
1037
+ onToggleFullscreen,
1038
+ isFullscreen = false,
1039
+ showConnectionStatus = true,
1040
+ isConnected = true,
1041
+ className,
1042
+ theme,
1043
+ onThemeChange
1044
+ }) {
1045
+ const { t } = useLocale();
1046
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1047
+ "div",
1048
+ {
1049
+ className: `chat-header ${className || ""}`,
1050
+ "data-chat-header": "true",
1051
+ "data-testid": "chat-header",
1052
+ children: [
1053
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
1054
+ showConnectionStatus && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1055
+ "div",
1056
+ {
1057
+ className: `w-2 h-2 rounded-full ${isConnected ? "bg-chat-success" : "bg-chat-error"}`,
1058
+ "data-chat-connection-status": "true",
1059
+ title: isConnected ? "Connected" : "Disconnected"
1060
+ }
1061
+ ),
1062
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "m-0 text-base font-semibold text-chat-text", children: title })
1063
+ ] }),
1064
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-1", children: [
1065
+ onThemeChange && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1066
+ "button",
1067
+ {
1068
+ onClick: () => onThemeChange(theme === "light" ? "dark" : theme === "dark" ? "auto" : "light"),
1069
+ className: "p-1 bg-transparent border-none cursor-pointer text-chat-text-secondary rounded hover:bg-chat-surface transition",
1070
+ "data-chat-theme-toggle": "true",
1071
+ "aria-label": theme === "light" ? t("header.darkMode") : theme === "dark" ? t("header.autoMode") : t("header.lightMode"),
1072
+ title: theme === "light" ? t("header.darkMode") : theme === "dark" ? t("header.autoMode") : t("header.lightMode"),
1073
+ children: theme === "light" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1074
+ "svg",
1075
+ {
1076
+ width: "18",
1077
+ height: "18",
1078
+ viewBox: "0 0 24 24",
1079
+ fill: "none",
1080
+ stroke: "currentColor",
1081
+ strokeWidth: "2",
1082
+ strokeLinecap: "round",
1083
+ strokeLinejoin: "round",
1084
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" })
1085
+ }
1086
+ ) : theme === "dark" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1087
+ "svg",
1088
+ {
1089
+ width: "18",
1090
+ height: "18",
1091
+ viewBox: "0 0 24 24",
1092
+ fill: "none",
1093
+ stroke: "currentColor",
1094
+ strokeWidth: "2",
1095
+ strokeLinecap: "round",
1096
+ strokeLinejoin: "round",
1097
+ children: [
1098
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "5" }),
1099
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "12", y1: "1", x2: "12", y2: "3" }),
1100
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "12", y1: "21", x2: "12", y2: "23" }),
1101
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "4.22", y1: "4.22", x2: "5.64", y2: "5.64" }),
1102
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "18.36", y1: "18.36", x2: "19.78", y2: "19.78" }),
1103
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "1", y1: "12", x2: "3", y2: "12" }),
1104
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "21", y1: "12", x2: "23", y2: "12" }),
1105
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "4.22", y1: "19.78", x2: "5.64", y2: "18.36" }),
1106
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "18.36", y1: "5.64", x2: "19.78", y2: "4.22" })
1107
+ ]
1108
+ }
1109
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1110
+ "svg",
1111
+ {
1112
+ width: "18",
1113
+ height: "18",
1114
+ viewBox: "0 0 24 24",
1115
+ fill: "none",
1116
+ stroke: "currentColor",
1117
+ strokeWidth: "2",
1118
+ strokeLinecap: "round",
1119
+ strokeLinejoin: "round",
1120
+ children: [
1121
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2", ry: "2" }),
1122
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "8", y1: "21", x2: "16", y2: "21" }),
1123
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "12", y1: "17", x2: "12", y2: "21" })
1124
+ ]
1125
+ }
1126
+ )
1127
+ }
1128
+ ),
1129
+ onToggleFullscreen && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1130
+ "button",
1131
+ {
1132
+ onClick: onToggleFullscreen,
1133
+ className: "p-1 bg-transparent border-none cursor-pointer text-chat-text-secondary rounded hover:bg-chat-surface transition",
1134
+ "data-chat-fullscreen-toggle": "true",
1135
+ "aria-label": isFullscreen ? t("header.exitFullscreen") : t("header.enterFullscreen"),
1136
+ title: isFullscreen ? t("header.exitFullscreen") : t("header.enterFullscreen"),
1137
+ children: isFullscreen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1138
+ "svg",
1139
+ {
1140
+ width: "20",
1141
+ height: "20",
1142
+ viewBox: "0 0 24 24",
1143
+ fill: "none",
1144
+ stroke: "currentColor",
1145
+ strokeWidth: "2",
1146
+ strokeLinecap: "round",
1147
+ strokeLinejoin: "round",
1148
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" })
1149
+ }
1150
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1151
+ "svg",
1152
+ {
1153
+ width: "20",
1154
+ height: "20",
1155
+ viewBox: "0 0 24 24",
1156
+ fill: "none",
1157
+ stroke: "currentColor",
1158
+ strokeWidth: "2",
1159
+ strokeLinecap: "round",
1160
+ strokeLinejoin: "round",
1161
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" })
1162
+ }
1163
+ )
1164
+ }
1165
+ ),
1166
+ onClose && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1167
+ "button",
1168
+ {
1169
+ onClick: onClose,
1170
+ className: "p-1 bg-transparent border-none cursor-pointer text-chat-text-secondary rounded hover:bg-chat-surface transition",
1171
+ "data-chat-close": "true",
1172
+ "aria-label": t("header.closeChat"),
1173
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1174
+ "svg",
1175
+ {
1176
+ width: "20",
1177
+ height: "20",
1178
+ viewBox: "0 0 24 24",
1179
+ fill: "none",
1180
+ stroke: "currentColor",
1181
+ strokeWidth: "2",
1182
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M18 6L6 18M6 6l12 12" })
1183
+ }
1184
+ )
1185
+ }
1186
+ )
1187
+ ] })
1188
+ ]
1189
+ }
1190
+ );
1191
+ }
1192
+
1193
+ // src/components/MessageList.tsx
1194
+ var import_react10 = require("react");
1195
+
1196
+ // src/cards/CardContext.tsx
1197
+ var import_react9 = require("react");
1198
+
1199
+ // src/utils/markdown.tsx
1200
+ var import_marked = require("marked");
1201
+ var import_dompurify = __toESM(require("dompurify"), 1);
1202
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1203
+ var renderer = new import_marked.marked.Renderer();
1204
+ renderer.link = ({ href, text }) => {
1205
+ return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`;
1206
+ };
1207
+ import_marked.marked.setOptions({
1208
+ gfm: true,
1209
+ breaks: true,
1210
+ renderer
1211
+ });
1212
+ function renderMarkdown(text) {
1213
+ const rawHtml = import_marked.marked.parse(text);
1214
+ return import_dompurify.default.sanitize(rawHtml, { ADD_ATTR: ["target"] });
1215
+ }
1216
+ function MarkdownRenderer({
1217
+ text,
1218
+ className
1219
+ }) {
1220
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1221
+ "div",
1222
+ {
1223
+ className: `prose prose-sm max-w-none prose-headings:font-semibold prose-a:text-chat-primary prose-a:no-underline hover:prose-a:underline prose-code:bg-chat-surface prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:font-mono prose-code:text-sm prose-blockquote:border-l-chat-border prose-blockquote:text-chat-text-secondary ${className || ""}`,
1224
+ dangerouslySetInnerHTML: { __html: renderMarkdown(text) }
1225
+ }
1226
+ );
1227
+ }
1228
+
1229
+ // src/cards/DefaultCard.tsx
1230
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1231
+ function DefaultCard({
1232
+ card: rawCard,
1233
+ onActionClick
1234
+ }) {
1235
+ if (rawCard.type !== "card") {
1236
+ return null;
1237
+ }
1238
+ const card = rawCard;
1239
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1240
+ "div",
1241
+ {
1242
+ className: "border border-chat-border rounded-lg overflow-hidden max-w-sm bg-[var(--chat-background)]",
1243
+ "data-chat-card": "default",
1244
+ children: [
1245
+ card.header && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "px-3 py-2 bg-chat-surface border-b border-chat-border font-semibold text-sm", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MarkdownRenderer, { text: card.header }) }),
1246
+ card.image && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1247
+ "img",
1248
+ {
1249
+ src: card.image.url,
1250
+ alt: card.image.alt || "",
1251
+ className: "block w-full h-auto max-h-48 object-cover",
1252
+ "data-chat-card-image": "true"
1253
+ }
1254
+ ),
1255
+ card.sections?.map((section, index) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "px-3 py-2", "data-chat-section": index, children: [
1256
+ section.text && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mb-2 text-sm leading-relaxed", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MarkdownRenderer, { text: section.text }) }),
1257
+ section.fields?.map((field, fieldIndex) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mb-1 last:mb-0", "data-chat-field": fieldIndex, children: [
1258
+ field.title && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-xs text-chat-text-secondary font-medium", children: field.title }),
1259
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-sm text-chat-text", children: field.value })
1260
+ ] }, fieldIndex))
1261
+ ] }, index)),
1262
+ card.elements?.map((element, elIndex) => {
1263
+ switch (element.type) {
1264
+ case "text":
1265
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1266
+ "div",
1267
+ {
1268
+ className: `px-3 py-2 text-sm ${element.style === "muted" ? "text-chat-text-secondary" : element.style === "bold" ? "text-chat-text font-bold" : "text-chat-text"}`,
1269
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MarkdownRenderer, { text: element.content })
1270
+ },
1271
+ `el-${elIndex}`
1272
+ );
1273
+ case "divider":
1274
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("hr", { className: "border-0 border-t border-chat-border" }, `el-${elIndex}`);
1275
+ case "link":
1276
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1277
+ "a",
1278
+ {
1279
+ href: element.url,
1280
+ target: "_blank",
1281
+ rel: "noopener noreferrer",
1282
+ className: "block px-3 py-2 text-chat-primary text-sm no-underline hover:underline",
1283
+ children: element.label
1284
+ },
1285
+ `el-${elIndex}`
1286
+ );
1287
+ case "table":
1288
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("table", { className: "w-full border-collapse text-xs", children: [
1289
+ element.headers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tr", { children: element.headers.map((h, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1290
+ "th",
1291
+ {
1292
+ className: "px-2 py-1 text-left border-b border-chat-border font-semibold",
1293
+ children: h
1294
+ },
1295
+ i
1296
+ )) }) }),
1297
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tbody", { children: element.rows.map((row, rIdx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tr", { children: row.map((cell, cIdx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { className: "px-2 py-1 border-b border-chat-border", children: cell }, cIdx)) }, rIdx)) })
1298
+ ] }, `el-${elIndex}`);
1299
+ case "link_button":
1300
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1301
+ "a",
1302
+ {
1303
+ href: element.url,
1304
+ target: "_blank",
1305
+ rel: "noopener noreferrer",
1306
+ className: `block px-2 py-1.5 mx-2 my-1 rounded text-sm font-medium text-center no-underline ${element.style === "primary" ? "bg-chat-primary text-white" : element.style === "danger" ? "bg-chat-error text-white" : "bg-chat-surface text-chat-text hover:bg-chat-background"}`,
1307
+ children: element.label
1308
+ },
1309
+ `el-${elIndex}`
1310
+ );
1311
+ case "image":
1312
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1313
+ "img",
1314
+ {
1315
+ src: element.url,
1316
+ alt: element.alt || "",
1317
+ className: "block w-full h-auto max-h-36 object-cover"
1318
+ },
1319
+ `el-${elIndex}`
1320
+ );
1321
+ default:
1322
+ return null;
1323
+ }
1324
+ }),
1325
+ card.actions && card.actions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex flex-wrap gap-2 px-3 py-2", "data-chat-actions": "true", children: card.actions.map((action) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1326
+ "button",
1327
+ {
1328
+ onClick: () => onActionClick?.(action.id, action.value || ""),
1329
+ className: `px-3 py-1.5 border border-chat-border rounded text-sm font-medium transition cursor-pointer ${action.style === "primary" ? "bg-chat-primary text-white border-transparent hover:bg-chat-primary-hover" : action.style === "danger" ? "bg-chat-error text-white border-transparent hover:opacity-90" : "bg-chat-surface text-chat-text hover:bg-chat-background"}`,
1330
+ "data-chat-action": action.id,
1331
+ children: action.label
1332
+ },
1333
+ action.id
1334
+ )) })
1335
+ ]
1336
+ }
1337
+ );
1338
+ }
1339
+
1340
+ // src/cards/ImageCard.tsx
1341
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1342
+ function ImageCardComponent({ card: rawCard }) {
1343
+ if (rawCard.type !== "image") {
1344
+ return null;
1345
+ }
1346
+ const card = rawCard;
1347
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "rounded-lg overflow-hidden max-w-full", "data-chat-card": "image", children: [
1348
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1349
+ "img",
1350
+ {
1351
+ src: card.url,
1352
+ alt: card.alt || "",
1353
+ className: "block max-w-full h-auto",
1354
+ "data-chat-image": "true"
1355
+ }
1356
+ ),
1357
+ card.title && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "px-3 py-2 text-sm bg-chat-surface", children: card.title })
1358
+ ] });
1359
+ }
1360
+
1361
+ // src/utils/formatSize.ts
1362
+ function formatSize(bytes) {
1363
+ if (bytes < 1024) return `${bytes} B`;
1364
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1365
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1366
+ }
1367
+
1368
+ // src/cards/FileCard.tsx
1369
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1370
+ function FileCardComponent({ card: rawCard }) {
1371
+ if (rawCard.type !== "file") {
1372
+ return null;
1373
+ }
1374
+ const card = rawCard;
1375
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1376
+ "div",
1377
+ {
1378
+ className: "flex items-center gap-3 p-3 border border-chat-border rounded-lg max-w-xs",
1379
+ "data-chat-card": "file",
1380
+ children: [
1381
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "w-10 h-10 flex items-center justify-center bg-chat-surface rounded", children: "\u{1F4C4}" }),
1382
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 min-w-0", children: [
1383
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "text-sm font-medium truncate", children: card.name }),
1384
+ card.size && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "text-xs text-chat-text-secondary", children: formatSize(card.size) })
1385
+ ] }),
1386
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1387
+ "a",
1388
+ {
1389
+ href: card.url,
1390
+ download: card.name,
1391
+ className: "px-3 py-2 bg-chat-primary text-white rounded text-sm font-medium no-underline hover:opacity-90",
1392
+ "data-chat-file-download": "true",
1393
+ children: "Download"
1394
+ }
1395
+ )
1396
+ ]
1397
+ }
1398
+ );
1399
+ }
1400
+
1401
+ // src/cards/CardContext.tsx
1402
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1403
+ var CardContext = (0, import_react9.createContext)(void 0);
1404
+ function CardProvider({ children, renderers }) {
1405
+ const value = (0, import_react9.useMemo)(() => {
1406
+ const defaultRenderers = /* @__PURE__ */ new Map([
1407
+ ["card", DefaultCard],
1408
+ ["image", ImageCardComponent],
1409
+ ["file", FileCardComponent]
1410
+ ]);
1411
+ if (renderers) {
1412
+ Object.entries(renderers).forEach(([type, renderer2]) => {
1413
+ defaultRenderers.set(type, renderer2);
1414
+ });
1415
+ }
1416
+ return {
1417
+ renderers: defaultRenderers,
1418
+ registerRenderer: (type, renderer2) => {
1419
+ defaultRenderers.set(type, renderer2);
1420
+ },
1421
+ getRenderer: (type) => {
1422
+ return defaultRenderers.get(type);
1423
+ }
1424
+ };
1425
+ }, [renderers]);
1426
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CardContext.Provider, { value, children });
1427
+ }
1428
+ function useCardRegistry() {
1429
+ const context = (0, import_react9.useContext)(CardContext);
1430
+ if (!context) {
1431
+ throw new Error("useCardRegistry must be used within CardProvider");
1432
+ }
1433
+ return context;
1434
+ }
1435
+
1436
+ // src/cards/CardRenderer.tsx
1437
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1438
+ function CardRenderer({ card, onActionClick }) {
1439
+ const { getRenderer } = useCardRegistry();
1440
+ const Renderer = getRenderer(card.type);
1441
+ if (Renderer) {
1442
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Renderer, { card, onActionClick });
1443
+ }
1444
+ if (isPHPCard(card)) {
1445
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DefaultCard, { card, onActionClick });
1446
+ }
1447
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { padding: "8px", background: "#f3f4f6", borderRadius: "4px" }, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { style: { fontSize: "12px", overflow: "auto" }, children: JSON.stringify(card, null, 2) }) });
1448
+ }
1449
+ function isPHPCard(card) {
1450
+ return card.type === "card";
1451
+ }
1452
+
1453
+ // src/components/MessageContent.tsx
1454
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1455
+ function MessageContent({ message, onActionClick }) {
1456
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { "data-chat-message-content": "true", children: [
1457
+ message.content.text && !message.content.cards?.length && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "break-words text-sm leading-relaxed", "data-chat-text": "true", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MarkdownRenderer, { text: message.content.text }) }),
1458
+ message.content.cards?.map((card, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: index > 0 ? "mt-2" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1459
+ CardRenderer,
1460
+ {
1461
+ card,
1462
+ onActionClick: (actionId, value) => onActionClick?.(message.id, actionId, value)
1463
+ }
1464
+ ) }, index)),
1465
+ message.attachments?.map((attachment) => {
1466
+ const isImage = attachment.type === "image" || attachment.mimeType?.startsWith("image/");
1467
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "mt-2", children: isImage ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("a", { href: attachment.url, target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1468
+ "img",
1469
+ {
1470
+ src: attachment.url,
1471
+ alt: attachment.name || "Image",
1472
+ className: "max-w-full rounded object-cover",
1473
+ loading: "lazy",
1474
+ "data-chat-attachment": attachment.id
1475
+ }
1476
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1477
+ "a",
1478
+ {
1479
+ href: attachment.url,
1480
+ target: "_blank",
1481
+ rel: "noopener noreferrer",
1482
+ className: "inline-flex items-center gap-2 px-3 py-2 bg-chat-surface rounded text-sm no-underline text-chat-text hover:bg-chat-background transition",
1483
+ "data-chat-attachment": attachment.id,
1484
+ children: [
1485
+ "\u{1F4CE} ",
1486
+ attachment.name
1487
+ ]
1488
+ }
1489
+ ) }, attachment.id);
1490
+ })
1491
+ ] });
1492
+ }
1493
+
1494
+ // src/utils/formatTimestamp.ts
1495
+ function formatTimestamp(timestamp) {
1496
+ const date = new Date(timestamp);
1497
+ const now = /* @__PURE__ */ new Date();
1498
+ const diffMs = now.getTime() - date.getTime();
1499
+ const diffMins = Math.floor(diffMs / 6e4);
1500
+ if (diffMins < 1) return "Just now";
1501
+ if (diffMins < 60) return `${diffMins}m ago`;
1502
+ if (diffMins < 1440) return `${Math.floor(diffMins / 60)}h ago`;
1503
+ return date.toLocaleDateString();
1504
+ }
1505
+
1506
+ // src/components/MessageList.tsx
1507
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1508
+ function MessageList({
1509
+ messages,
1510
+ currentUserId,
1511
+ isLoading = false,
1512
+ onReactionClick,
1513
+ onActionClick,
1514
+ className
1515
+ }) {
1516
+ const { t } = useLocale();
1517
+ const containerRef = (0, import_react10.useRef)(null);
1518
+ const listEndRef = (0, import_react10.useRef)(null);
1519
+ const hasInitiallyScrolled = (0, import_react10.useRef)(false);
1520
+ const isNearBottom = (0, import_react10.useRef)(true);
1521
+ (0, import_react10.useEffect)(() => {
1522
+ if (!hasInitiallyScrolled.current && messages.length > 0) {
1523
+ listEndRef.current?.scrollIntoView?.();
1524
+ hasInitiallyScrolled.current = true;
1525
+ }
1526
+ }, [messages.length]);
1527
+ const prevMessagesLength = (0, import_react10.useRef)(messages.length);
1528
+ (0, import_react10.useEffect)(() => {
1529
+ if (messages.length > prevMessagesLength.current) {
1530
+ listEndRef.current?.scrollIntoView?.({ behavior: "smooth" });
1531
+ }
1532
+ prevMessagesLength.current = messages.length;
1533
+ }, [messages.length]);
1534
+ (0, import_react10.useEffect)(() => {
1535
+ const el = containerRef.current;
1536
+ if (!el) return;
1537
+ const handleScroll = () => {
1538
+ const threshold = 100;
1539
+ isNearBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
1540
+ };
1541
+ el.addEventListener("scroll", handleScroll, { passive: true });
1542
+ const observer = new ResizeObserver(() => {
1543
+ if (isNearBottom.current) {
1544
+ listEndRef.current?.scrollIntoView?.({ behavior: "smooth" });
1545
+ }
1546
+ });
1547
+ observer.observe(el);
1548
+ return () => {
1549
+ el.removeEventListener("scroll", handleScroll);
1550
+ observer.disconnect();
1551
+ };
1552
+ }, []);
1553
+ const groupedMessages = (0, import_react10.useMemo)(() => {
1554
+ const groups = [];
1555
+ let currentGroup = null;
1556
+ for (const message of messages) {
1557
+ const userId = message.author.id;
1558
+ if (!currentGroup || currentGroup.user !== userId) {
1559
+ if (currentGroup) {
1560
+ groups.push(currentGroup);
1561
+ }
1562
+ currentGroup = { user: userId, messages: [message] };
1563
+ } else {
1564
+ currentGroup.messages.push(message);
1565
+ }
1566
+ }
1567
+ if (currentGroup) {
1568
+ groups.push(currentGroup);
1569
+ }
1570
+ return groups;
1571
+ }, [messages]);
1572
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1573
+ "div",
1574
+ {
1575
+ ref: containerRef,
1576
+ className: `chat-message-list flex-1 min-h-0 overflow-y-auto ${className || ""}`,
1577
+ "data-chat-message-list": "true",
1578
+ "data-testid": "chat-message-list",
1579
+ children: [
1580
+ groupedMessages.length === 0 && !isLoading && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex flex-col items-center justify-center h-full min-h-[200px] text-center px-6", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "text-chat-text-secondary text-sm", children: t("messageList.emptyState") }) }),
1581
+ groupedMessages.map((group, groupIndex) => {
1582
+ const isOwn = group.user === currentUserId;
1583
+ const firstMessage = group.messages[0];
1584
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col gap-1", children: [
1585
+ firstMessage.author.name && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "text-xs text-chat-text-secondary", children: firstMessage.author.name }),
1586
+ group.messages.map((message, msgIndex) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col", "data-chat-message-id": message.id, children: [
1587
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: isOwn ? "chat-message-bubble-own" : "chat-message-bubble-other", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MessageContent, { message, onActionClick }) }),
1588
+ message.reactions && message.reactions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex gap-1 mt-1", children: message.reactions.map((reaction, rIndex) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1589
+ "button",
1590
+ {
1591
+ onClick: () => onReactionClick?.(message.id, reaction.emoji),
1592
+ className: `flex items-center gap-1 px-2 py-0.5 border border-chat-border rounded-full text-sm cursor-pointer transition ${reaction.hasReacted ? "bg-chat-surface" : "bg-transparent hover:bg-chat-surface"}`,
1593
+ "data-chat-reaction": reaction.emoji,
1594
+ children: [
1595
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: reaction.emoji }),
1596
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "text-chat-text-secondary", children: reaction.count })
1597
+ ]
1598
+ },
1599
+ `${reaction.emoji}-${rIndex}`
1600
+ )) }),
1601
+ msgIndex === 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "text-xs text-chat-text-secondary mt-1", children: formatTimestamp(message.timestamp) })
1602
+ ] }, message.id))
1603
+ ] }, `${group.user}-${groupIndex}`);
1604
+ }),
1605
+ isLoading && groupedMessages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex items-center justify-center min-h-[200px]", "data-chat-loading": "true", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-1.5", children: [
1606
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1607
+ "span",
1608
+ {
1609
+ className: "w-2 h-2 rounded-full bg-chat-text-secondary animate-bounce",
1610
+ style: { animationDelay: "0ms" }
1611
+ }
1612
+ ),
1613
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1614
+ "span",
1615
+ {
1616
+ className: "w-2 h-2 rounded-full bg-chat-text-secondary animate-bounce",
1617
+ style: { animationDelay: "160ms" }
1618
+ }
1619
+ ),
1620
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1621
+ "span",
1622
+ {
1623
+ className: "w-2 h-2 rounded-full bg-chat-text-secondary animate-bounce",
1624
+ style: { animationDelay: "320ms" }
1625
+ }
1626
+ )
1627
+ ] }) }),
1628
+ isLoading && groupedMessages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex justify-center py-4", "data-chat-loading": "true", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "text-chat-text-secondary", children: t("common.loading") }) }),
1629
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: listEndRef, className: "h-px" })
1630
+ ]
1631
+ }
1632
+ );
1633
+ }
1634
+
1635
+ // src/components/InputArea.tsx
1636
+ var import_react12 = require("react");
1637
+
1638
+ // src/components/Dropzone.tsx
1639
+ var import_react11 = require("react");
1640
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1641
+ function Dropzone({
1642
+ onFilesSelected,
1643
+ disabled = false,
1644
+ accept,
1645
+ maxSize,
1646
+ multiple = true,
1647
+ className
1648
+ }) {
1649
+ const { t } = useLocale();
1650
+ const [isDragging, setIsDragging] = (0, import_react11.useState)(false);
1651
+ const inputRef = (0, import_react11.useRef)(null);
1652
+ const dragCounter = (0, import_react11.useRef)(0);
1653
+ const handleDragEnter = (0, import_react11.useCallback)((e) => {
1654
+ e.preventDefault();
1655
+ e.stopPropagation();
1656
+ dragCounter.current++;
1657
+ if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
1658
+ setIsDragging(true);
1659
+ }
1660
+ }, []);
1661
+ const handleDragLeave = (0, import_react11.useCallback)((e) => {
1662
+ e.preventDefault();
1663
+ e.stopPropagation();
1664
+ dragCounter.current--;
1665
+ if (dragCounter.current === 0) {
1666
+ setIsDragging(false);
1667
+ }
1668
+ }, []);
1669
+ const handleDragOver = (0, import_react11.useCallback)((e) => {
1670
+ e.preventDefault();
1671
+ e.stopPropagation();
1672
+ }, []);
1673
+ const handleDrop = (0, import_react11.useCallback)(
1674
+ (e) => {
1675
+ e.preventDefault();
1676
+ e.stopPropagation();
1677
+ setIsDragging(false);
1678
+ dragCounter.current = 0;
1679
+ if (disabled) return;
1680
+ const files = e.dataTransfer.files;
1681
+ if (files && files.length > 0) {
1682
+ const filtered = filterFiles(files, maxSize, accept);
1683
+ if (filtered.length > 0) {
1684
+ onFilesSelected(multiple ? filtered : [filtered[0]]);
1685
+ }
1686
+ }
1687
+ },
1688
+ [disabled, maxSize, accept, multiple, onFilesSelected]
1689
+ );
1690
+ const handleInputChange = (0, import_react11.useCallback)(
1691
+ (e) => {
1692
+ if (disabled) return;
1693
+ const files = e.target.files;
1694
+ if (files && files.length > 0) {
1695
+ const filtered = filterFiles(files, maxSize, accept);
1696
+ if (filtered.length > 0) {
1697
+ onFilesSelected(multiple ? filtered : [filtered[0]]);
1698
+ }
1699
+ }
1700
+ if (inputRef.current) inputRef.current.value = "";
1701
+ },
1702
+ [disabled, maxSize, accept, multiple, onFilesSelected]
1703
+ );
1704
+ const handleClick = (0, import_react11.useCallback)(() => {
1705
+ inputRef.current?.click();
1706
+ }, []);
1707
+ function filterFiles(files, maxSize2, accept2) {
1708
+ return Array.from(files).filter((file) => {
1709
+ if (maxSize2 && file.size > maxSize2) return false;
1710
+ if (accept2) {
1711
+ const accepted = accept2.split(",").map((a) => a.trim().toLowerCase());
1712
+ const mimeType = file.type.toLowerCase();
1713
+ const ext = "." + file.name.split(".").pop()?.toLowerCase();
1714
+ return accepted.some((a) => {
1715
+ if (a.endsWith("/*")) return mimeType.startsWith(a.replace("/*", "/"));
1716
+ return a === mimeType || a === ext;
1717
+ });
1718
+ }
1719
+ return true;
1720
+ });
1721
+ }
1722
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1723
+ "div",
1724
+ {
1725
+ role: "button",
1726
+ tabIndex: 0,
1727
+ onClick: handleClick,
1728
+ onKeyDown: (e) => {
1729
+ if (e.key === "Enter" || e.key === " ") handleClick();
1730
+ },
1731
+ onDragEnter: handleDragEnter,
1732
+ onDragLeave: handleDragLeave,
1733
+ onDragOver: handleDragOver,
1734
+ onDrop: handleDrop,
1735
+ className: `flex items-center justify-center p-2 border-2 border-dashed rounded-lg cursor-pointer transition ${isDragging ? "border-chat-primary bg-chat-primary/10" : "border-chat-border"} ${disabled ? "cursor-not-allowed opacity-50" : ""} ${className || ""}`,
1736
+ "data-chat-dropzone": "true",
1737
+ "data-testid": "chat-dropzone",
1738
+ children: [
1739
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1740
+ "input",
1741
+ {
1742
+ ref: inputRef,
1743
+ type: "file",
1744
+ accept,
1745
+ multiple,
1746
+ onChange: handleInputChange,
1747
+ className: "hidden",
1748
+ disabled,
1749
+ "aria-hidden": "true"
1750
+ }
1751
+ ),
1752
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "text-center", children: [
1753
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1754
+ "svg",
1755
+ {
1756
+ width: "20",
1757
+ height: "20",
1758
+ viewBox: "0 0 24 24",
1759
+ fill: "none",
1760
+ stroke: isDragging ? "var(--chat-primary)" : "var(--chat-text-secondary)",
1761
+ strokeWidth: "2",
1762
+ strokeLinecap: "round",
1763
+ strokeLinejoin: "round",
1764
+ className: "mx-auto mb-1",
1765
+ children: [
1766
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
1767
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("polyline", { points: "17 8 12 3 7 8" }),
1768
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
1769
+ ]
1770
+ }
1771
+ ),
1772
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "text-xs text-chat-text-secondary", children: isDragging ? t("inputArea.dropzone.dropFiles") : t("inputArea.dropzone.dropOrClick") })
1773
+ ] })
1774
+ ]
1775
+ }
1776
+ );
1777
+ }
1778
+
1779
+ // src/components/AttachmentList.tsx
1780
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1781
+ function AttachmentList({
1782
+ attachments,
1783
+ onRemove,
1784
+ className
1785
+ }) {
1786
+ const { t } = useLocale();
1787
+ if (attachments.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_jsx_runtime13.Fragment, {});
1788
+ function getFileIcon(mimeType) {
1789
+ if (mimeType.startsWith("image/")) return "\u{1F5BC}\uFE0F";
1790
+ if (mimeType === "application/pdf") return "\u{1F4C4}";
1791
+ if (mimeType.includes("video")) return "\u{1F3AC}";
1792
+ if (mimeType.includes("audio")) return "\u{1F3B5}";
1793
+ return "\u{1F4CE}";
1794
+ }
1795
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: `flex flex-wrap gap-1 p-1 ${className || ""}`, "data-chat-attachment-list": "true", children: attachments.map((att) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1796
+ "div",
1797
+ {
1798
+ className: `flex items-center gap-1 px-2 py-1 text-xs rounded border max-w-[200px] ${att.status === "error" ? "bg-chat-error/15 border-chat-error" : "bg-chat-surface shadow-sm border-chat-border"}`,
1799
+ "data-chat-attachment-item": att.id,
1800
+ children: [
1801
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: getFileIcon(att.mimeType) }),
1802
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex-1 min-w-0", children: [
1803
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1804
+ "div",
1805
+ {
1806
+ className: `overflow-hidden text-ellipsis whitespace-nowrap ${att.status === "error" ? "text-chat-error" : "text-chat-text"}`,
1807
+ children: att.name
1808
+ }
1809
+ ),
1810
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-[10px] text-chat-text-secondary", children: att.status === "uploading" ? `${att.progress}%` : att.status === "error" ? att.error || t("attachmentList.uploadFailed") : formatSize(att.size) }),
1811
+ att.status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "h-0.5 bg-chat-border rounded overflow-hidden mt-0.5", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1812
+ "div",
1813
+ {
1814
+ className: "h-full bg-chat-primary transition-[width] duration-150",
1815
+ style: { width: `${att.progress}%` }
1816
+ }
1817
+ ) })
1818
+ ] }),
1819
+ onRemove && att.status !== "uploading" && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1820
+ "button",
1821
+ {
1822
+ onClick: () => onRemove(att.id),
1823
+ className: "bg-none border-none cursor-pointer text-chat-text-secondary p-0.5 leading-none hover:text-chat-error",
1824
+ "aria-label": `Remove ${att.name}`,
1825
+ children: "\xD7"
1826
+ }
1827
+ )
1828
+ ]
1829
+ },
1830
+ att.id
1831
+ )) });
1832
+ }
1833
+
1834
+ // src/components/InputArea.tsx
1835
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1836
+ function InputArea({
1837
+ onSend,
1838
+ disabled = false,
1839
+ placeholder = "Type a message...",
1840
+ className,
1841
+ enableAttachments = false,
1842
+ uploadConfig,
1843
+ accept,
1844
+ maxFileSize
1845
+ }) {
1846
+ const [text, setText] = (0, import_react12.useState)("");
1847
+ const [showDropzone, setShowDropzone] = (0, import_react12.useState)(false);
1848
+ const textareaRef = (0, import_react12.useRef)(null);
1849
+ const sendingRef = (0, import_react12.useRef)(false);
1850
+ const { attachments, addFiles, removeAttachment, clearAttachments, isUploading } = useAttachmentUpload(uploadConfig);
1851
+ const { t } = useLocale();
1852
+ (0, import_react12.useEffect)(() => {
1853
+ const textarea = textareaRef.current;
1854
+ if (!textarea) return;
1855
+ textarea.style.height = "auto";
1856
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px";
1857
+ }, [text]);
1858
+ const handleSubmit = async () => {
1859
+ const trimmed = text.trim();
1860
+ if (!trimmed && attachments.length === 0 || disabled || sendingRef.current) return;
1861
+ if (isUploading) return;
1862
+ sendingRef.current = true;
1863
+ const uploadedAttachments = attachments.filter((a) => a.status === "uploaded" && a.url).map((a) => ({
1864
+ url: a.url,
1865
+ name: a.name,
1866
+ mimeType: a.mimeType,
1867
+ size: a.size
1868
+ }));
1869
+ setText("");
1870
+ setShowDropzone(false);
1871
+ clearAttachments();
1872
+ try {
1873
+ await onSend(trimmed, uploadedAttachments);
1874
+ } finally {
1875
+ sendingRef.current = false;
1876
+ }
1877
+ setTimeout(() => textareaRef.current?.focus(), 0);
1878
+ };
1879
+ const handleKeyDown = (e) => {
1880
+ if (e.key === "Enter" && !e.shiftKey) {
1881
+ e.preventDefault();
1882
+ handleSubmit();
1883
+ }
1884
+ };
1885
+ const canSend = (text.trim().length > 0 || attachments.some((a) => a.status === "uploaded")) && !isUploading;
1886
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1887
+ "div",
1888
+ {
1889
+ className: `chat-input-area ${className || ""}`,
1890
+ "data-chat-input-area": "true",
1891
+ "data-testid": "chat-input-area",
1892
+ children: [
1893
+ enableAttachments && attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(AttachmentList, { attachments, onRemove: removeAttachment }),
1894
+ enableAttachments && showDropzone && uploadConfig && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1895
+ Dropzone,
1896
+ {
1897
+ onFilesSelected: addFiles,
1898
+ disabled: disabled || isUploading,
1899
+ accept,
1900
+ maxSize: maxFileSize
1901
+ }
1902
+ ),
1903
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex gap-3", children: [
1904
+ enableAttachments && uploadConfig && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1905
+ "button",
1906
+ {
1907
+ onClick: () => setShowDropzone((prev) => !prev),
1908
+ disabled,
1909
+ className: `p-2 rounded-lg cursor-pointer transition ${showDropzone ? "bg-chat-primary/10 text-chat-primary" : "bg-transparent text-chat-text-secondary"} disabled:cursor-not-allowed disabled:opacity-50`,
1910
+ "data-chat-attachment-toggle": "true",
1911
+ "aria-label": "Toggle file attachment",
1912
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1913
+ "svg",
1914
+ {
1915
+ width: "20",
1916
+ height: "20",
1917
+ viewBox: "0 0 24 24",
1918
+ fill: "none",
1919
+ stroke: "currentColor",
1920
+ strokeWidth: "2",
1921
+ strokeLinecap: "round",
1922
+ strokeLinejoin: "round",
1923
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" })
1924
+ }
1925
+ )
1926
+ }
1927
+ ),
1928
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1929
+ "textarea",
1930
+ {
1931
+ ref: textareaRef,
1932
+ value: text,
1933
+ onChange: (e) => setText(e.target.value),
1934
+ onKeyDown: handleKeyDown,
1935
+ placeholder,
1936
+ className: "chat-input",
1937
+ "data-chat-input": "true",
1938
+ rows: 1
1939
+ }
1940
+ ),
1941
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1942
+ "button",
1943
+ {
1944
+ onClick: handleSubmit,
1945
+ disabled: disabled || !canSend,
1946
+ className: "chat-send-button",
1947
+ "data-chat-send-button": "true",
1948
+ "aria-label": t("inputArea.send"),
1949
+ children: isUploading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1950
+ "svg",
1951
+ {
1952
+ width: "20",
1953
+ height: "20",
1954
+ viewBox: "0 0 24 24",
1955
+ fill: "none",
1956
+ stroke: "currentColor",
1957
+ strokeWidth: "2",
1958
+ strokeLinecap: "round",
1959
+ strokeLinejoin: "round",
1960
+ className: "animate-spin",
1961
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
1962
+ }
1963
+ ) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1964
+ "svg",
1965
+ {
1966
+ width: "20",
1967
+ height: "20",
1968
+ viewBox: "0 0 24 24",
1969
+ fill: "none",
1970
+ stroke: "currentColor",
1971
+ strokeWidth: "2",
1972
+ strokeLinecap: "round",
1973
+ strokeLinejoin: "round",
1974
+ children: [
1975
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
1976
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
1977
+ ]
1978
+ }
1979
+ )
1980
+ }
1981
+ )
1982
+ ] })
1983
+ ]
1984
+ }
1985
+ );
1986
+ }
1987
+
1988
+ // src/components/TypingIndicator.tsx
1989
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1990
+ function TypingIndicator({ users = [] }) {
1991
+ const { t } = useLocale();
1992
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1993
+ "div",
1994
+ {
1995
+ className: "chat-typing-indicator",
1996
+ "data-chat-typing-indicator": "true",
1997
+ "data-testid": "chat-typing-indicator",
1998
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "flex items-center gap-2", children: [
1999
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "flex gap-1", children: [
2000
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2001
+ "span",
2002
+ {
2003
+ className: "w-1.5 h-1.5 rounded-full bg-chat-text-secondary animate-bounce",
2004
+ style: { animationDelay: "0ms" }
2005
+ }
2006
+ ),
2007
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2008
+ "span",
2009
+ {
2010
+ className: "w-1.5 h-1.5 rounded-full bg-chat-text-secondary animate-bounce",
2011
+ style: { animationDelay: "160ms" }
2012
+ }
2013
+ ),
2014
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2015
+ "span",
2016
+ {
2017
+ className: "w-1.5 h-1.5 rounded-full bg-chat-text-secondary animate-bounce",
2018
+ style: { animationDelay: "320ms" }
2019
+ }
2020
+ )
2021
+ ] }),
2022
+ users.length > 0 && ` ${users[0]} ${t("typingIndicator.isTyping")}`
2023
+ ] })
2024
+ }
2025
+ );
2026
+ }
2027
+
2028
+ // src/components/ChatWidget.tsx
2029
+ var import_jsx_runtime16 = require("react/jsx-runtime");
2030
+ function ChatWidget({
2031
+ client,
2032
+ initialMode = "floating",
2033
+ theme: themeProp,
2034
+ onThemeChange,
2035
+ position = "bottom-right",
2036
+ className,
2037
+ showClose = true,
2038
+ showFullscreenToggle = true,
2039
+ title = "Chat",
2040
+ placeholder = "Type a message...",
2041
+ onOpen,
2042
+ onClose,
2043
+ embedded,
2044
+ floatingButton,
2045
+ enableAttachments = false,
2046
+ uploadConfig,
2047
+ accept,
2048
+ maxFileSize,
2049
+ renderPushPrompt
2050
+ }) {
2051
+ const {
2052
+ config: iframeConfig,
2053
+ isInIframe,
2054
+ notifyMessage,
2055
+ notifyViewportConfig,
2056
+ onNotificationClicked
2057
+ } = useBridge();
2058
+ const autoEmbedded = isInIframe && embedded !== false;
2059
+ const effectiveEmbedded = embedded === true || autoEmbedded;
2060
+ const effectiveMode = effectiveEmbedded ? "embedded" : initialMode;
2061
+ const [theme, setTheme] = (0, import_react13.useState)(() => {
2062
+ if (themeProp) return themeProp;
2063
+ try {
2064
+ const stored = localStorage.getItem("chat-theme");
2065
+ if (stored === "light" || stored === "dark" || stored === "auto") return stored;
2066
+ } catch {
2067
+ }
2068
+ return "auto";
2069
+ });
2070
+ const [systemDark, setSystemDark] = (0, import_react13.useState)(
2071
+ () => typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches
2072
+ );
2073
+ const effectiveTheme = theme === "auto" ? systemDark ? "dark" : "light" : theme;
2074
+ (0, import_react13.useEffect)(() => {
2075
+ if (themeProp && themeProp !== theme) {
2076
+ setTheme(themeProp);
2077
+ try {
2078
+ localStorage.setItem("chat-theme", themeProp);
2079
+ } catch {
2080
+ }
2081
+ }
2082
+ }, [themeProp]);
2083
+ (0, import_react13.useEffect)(() => {
2084
+ if (theme !== "auto") return;
2085
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
2086
+ const handler = (e) => setSystemDark(e.matches);
2087
+ mq.addEventListener("change", handler);
2088
+ return () => mq.removeEventListener("change", handler);
2089
+ }, [theme]);
2090
+ const handleThemeChange = (newTheme) => {
2091
+ setTheme(newTheme);
2092
+ try {
2093
+ localStorage.setItem("chat-theme", newTheme);
2094
+ } catch {
2095
+ }
2096
+ onThemeChange?.(newTheme);
2097
+ };
2098
+ const [isOpen, setIsOpen] = (0, import_react13.useState)(effectiveMode === "fullscreen");
2099
+ const [displayMode, setDisplayMode] = (0, import_react13.useState)(effectiveMode);
2100
+ const [isConnected] = (0, import_react13.useState)(true);
2101
+ const [isSmallScreen, setIsSmallScreen] = (0, import_react13.useState)(
2102
+ () => typeof window !== "undefined" && window.innerWidth < 800
2103
+ );
2104
+ (0, import_react13.useEffect)(() => {
2105
+ const mq = window.matchMedia("(max-width: 799px)");
2106
+ const handler = (e) => {
2107
+ setIsSmallScreen(e.matches);
2108
+ if (e.matches) setDisplayMode("fullscreen");
2109
+ };
2110
+ setIsSmallScreen(mq.matches);
2111
+ if (mq.matches) setDisplayMode("fullscreen");
2112
+ mq.addEventListener("change", handler);
2113
+ return () => mq.removeEventListener("change", handler);
2114
+ }, []);
2115
+ (0, import_react13.useEffect)(() => {
2116
+ if (initialMode !== "fullscreen") return;
2117
+ if (!isSmallScreen) setIsOpen(true);
2118
+ }, [initialMode, isSmallScreen]);
2119
+ (0, import_react13.useEffect)(() => {
2120
+ if (isInIframe) {
2121
+ notifyViewportConfig("interactive-widget=resizes-content");
2122
+ return () => notifyViewportConfig("");
2123
+ }
2124
+ const isFullscreen = displayMode === "fullscreen" || isSmallScreen;
2125
+ const active = isOpen && isFullscreen;
2126
+ const meta = document.querySelector('meta[name="viewport"]');
2127
+ if (!meta) return;
2128
+ if (active) {
2129
+ const original = meta.getAttribute("content") ?? "";
2130
+ if (!original.includes("interactive-widget=")) {
2131
+ meta.setAttribute("content", `${original}, interactive-widget=resizes-content`);
2132
+ }
2133
+ return () => {
2134
+ meta.setAttribute("content", original);
2135
+ };
2136
+ }
2137
+ }, [isOpen, displayMode, isSmallScreen, isInIframe, notifyViewportConfig]);
2138
+ const { messages, sendMessage, loading, isLoadingHistory, reloadMessages } = useMessages(
2139
+ client,
2140
+ isOpen || effectiveEmbedded
2141
+ );
2142
+ const { isSomeoneTyping } = useTyping(client);
2143
+ (0, import_react13.useEffect)(() => {
2144
+ if (!isInIframe || !iframeConfig) return;
2145
+ if (iframeConfig.theme?.cssVariables) {
2146
+ const root = document.documentElement;
2147
+ for (const [key, value] of Object.entries(iframeConfig.theme.cssVariables)) {
2148
+ root.style.setProperty(key, value);
2149
+ }
2150
+ }
2151
+ const mode = iframeConfig.theme?.mode;
2152
+ if (mode === "light" || mode === "dark" || mode === "auto") {
2153
+ setTheme(mode);
2154
+ }
2155
+ }, [isInIframe, iframeConfig]);
2156
+ (0, import_react13.useEffect)(() => {
2157
+ if (!isInIframe) return;
2158
+ onNotificationClicked(() => {
2159
+ reloadMessages();
2160
+ });
2161
+ }, [isInIframe, onNotificationClicked, reloadMessages]);
2162
+ const effectiveTitle = isInIframe && iframeConfig?.title || title;
2163
+ const effectivePlaceholder = isInIframe && iframeConfig?.placeholder || placeholder;
2164
+ const currentUserId = "getCurrentUserId" in client ? client.getCurrentUserId() : "";
2165
+ const handleSend = (0, import_react13.useCallback)(
2166
+ async (text, attachments = []) => {
2167
+ await sendMessage(text, attachments);
2168
+ if (isInIframe) {
2169
+ notifyMessage(text);
2170
+ }
2171
+ },
2172
+ [sendMessage, isInIframe, notifyMessage]
2173
+ );
2174
+ const handleActionClick = (0, import_react13.useCallback)(
2175
+ (messageId, actionId, value) => {
2176
+ client.sendAction(messageId, actionId, value).catch((err) => {
2177
+ console.error("Action failed:", err);
2178
+ });
2179
+ },
2180
+ [client]
2181
+ );
2182
+ const handleReactionClick = (0, import_react13.useCallback)((_messageId, _emoji) => {
2183
+ }, []);
2184
+ const toggleOpen = (0, import_react13.useCallback)(() => {
2185
+ setIsOpen((prev) => {
2186
+ if (!prev) onOpen?.();
2187
+ else onClose?.();
2188
+ return !prev;
2189
+ });
2190
+ }, [onOpen, onClose]);
2191
+ const toggleFullscreen = (0, import_react13.useCallback)(() => {
2192
+ setDisplayMode((prev) => prev === "fullscreen" ? "floating" : "fullscreen");
2193
+ }, []);
2194
+ const close = (0, import_react13.useCallback)(() => {
2195
+ setIsOpen(false);
2196
+ setDisplayMode("floating");
2197
+ onClose?.();
2198
+ }, [onClose]);
2199
+ const embeddedClose = (0, import_react13.useCallback)(() => {
2200
+ if (isInIframe) {
2201
+ window.parent.postMessage({ type: "chat-close" }, "*");
2202
+ }
2203
+ }, [isInIframe]);
2204
+ if (effectiveEmbedded) {
2205
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
2206
+ "div",
2207
+ {
2208
+ className: "flex flex-col h-full min-h-[300px] overflow-hidden bg-chat-background",
2209
+ "data-chat-widget": "embedded",
2210
+ "data-chat-theme": effectiveTheme,
2211
+ children: [
2212
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2213
+ Header,
2214
+ {
2215
+ title: effectiveTitle,
2216
+ isFullscreen: false,
2217
+ showConnectionStatus: true,
2218
+ isConnected,
2219
+ className: className?.header,
2220
+ theme,
2221
+ onThemeChange: handleThemeChange,
2222
+ onClose: isInIframe ? embeddedClose : void 0
2223
+ }
2224
+ ),
2225
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2226
+ MessageList,
2227
+ {
2228
+ messages,
2229
+ currentUserId,
2230
+ isLoading: isLoadingHistory || loading,
2231
+ onActionClick: handleActionClick,
2232
+ onReactionClick: handleReactionClick,
2233
+ className: className?.messageList
2234
+ }
2235
+ ),
2236
+ isSomeoneTyping && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TypingIndicator, {}),
2237
+ renderPushPrompt?.(),
2238
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2239
+ InputArea,
2240
+ {
2241
+ onSend: handleSend,
2242
+ placeholder: effectivePlaceholder,
2243
+ className: className?.inputArea,
2244
+ enableAttachments,
2245
+ uploadConfig,
2246
+ accept,
2247
+ maxFileSize
2248
+ }
2249
+ )
2250
+ ]
2251
+ }
2252
+ );
2253
+ }
2254
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
2255
+ !effectiveEmbedded && !isOpen && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2256
+ FloatingButton,
2257
+ {
2258
+ onClick: toggleOpen,
2259
+ isOpen,
2260
+ position,
2261
+ icon: floatingButton?.icon,
2262
+ openIcon: floatingButton?.openIcon,
2263
+ badgeCount: floatingButton?.badgeCount,
2264
+ size: floatingButton?.size,
2265
+ backgroundColor: floatingButton?.backgroundColor,
2266
+ ariaLabel: floatingButton?.ariaLabel,
2267
+ className: floatingButton?.className
2268
+ }
2269
+ ),
2270
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
2271
+ "div",
2272
+ {
2273
+ className: `flex flex-col overflow-hidden ${displayMode === "fullscreen" ? "fixed inset-0 z-50" : `absolute ${position === "bottom-right" ? "bottom-20 right-5" : position === "bottom-left" ? "bottom-20 left-5" : ""} w-[480px] max-w-[min(800px,calc(100dvw-40px))] h-dvh max-h-[min(600px,80dvh)] z-10 shadow-xl border border-chat-border rounded-2xl`} bg-chat-background`,
2274
+ "data-chat-widget": displayMode,
2275
+ "data-chat-position": position,
2276
+ "data-chat-theme": effectiveTheme,
2277
+ children: [
2278
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2279
+ Header,
2280
+ {
2281
+ title: effectiveTitle,
2282
+ onClose: displayMode === "floating" ? close : showClose ? close : void 0,
2283
+ onToggleFullscreen: showFullscreenToggle && !isSmallScreen ? toggleFullscreen : void 0,
2284
+ isFullscreen: displayMode === "fullscreen",
2285
+ showConnectionStatus: true,
2286
+ isConnected,
2287
+ className: className?.header,
2288
+ theme,
2289
+ onThemeChange: handleThemeChange
2290
+ }
2291
+ ),
2292
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2293
+ MessageList,
2294
+ {
2295
+ messages,
2296
+ currentUserId,
2297
+ isLoading: isLoadingHistory || loading,
2298
+ onActionClick: handleActionClick,
2299
+ onReactionClick: handleReactionClick,
2300
+ className: className?.messageList
2301
+ }
2302
+ ),
2303
+ isSomeoneTyping && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TypingIndicator, {}),
2304
+ renderPushPrompt?.(),
2305
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2306
+ InputArea,
2307
+ {
2308
+ onSend: handleSend,
2309
+ placeholder: effectivePlaceholder,
2310
+ disabled: loading,
2311
+ className: className?.inputArea,
2312
+ enableAttachments,
2313
+ uploadConfig,
2314
+ accept,
2315
+ maxFileSize
2316
+ }
2317
+ )
2318
+ ]
2319
+ }
2320
+ )
2321
+ ] });
2322
+ }
2323
+
2324
+ // src/providers/ChatProvider.tsx
2325
+ var import_react14 = require("react");
2326
+ var import_jsx_runtime17 = require("react/jsx-runtime");
2327
+ var ChatContext = (0, import_react14.createContext)(void 0);
2328
+ function ChatProvider({
2329
+ children,
2330
+ client,
2331
+ cardRenderers
2332
+ }) {
2333
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CardProvider, { renderers: cardRenderers, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ChatContext.Provider, { value: { client }, children }) });
2334
+ }
2335
+ function useChatContext() {
2336
+ const context = (0, import_react14.useContext)(ChatContext);
2337
+ if (!context) {
2338
+ throw new Error("useChatContext must be used within ChatProvider");
2339
+ }
2340
+ return context;
2341
+ }
2342
+
2343
+ // src/components/ErrorBoundary.tsx
2344
+ var import_react15 = require("react");
2345
+ var import_jsx_runtime18 = require("react/jsx-runtime");
2346
+ var ErrorBoundary = class extends import_react15.Component {
2347
+ constructor(props) {
2348
+ super(props);
2349
+ this.state = { error: null };
2350
+ }
2351
+ static getDerivedStateFromError(error) {
2352
+ return { error };
2353
+ }
2354
+ componentDidCatch(error, errorInfo) {
2355
+ this.props.onError?.(error, errorInfo);
2356
+ }
2357
+ render() {
2358
+ if (!this.state.error) return this.props.children;
2359
+ const { fallback } = this.props;
2360
+ if (typeof fallback === "function") {
2361
+ return fallback(this.state.error);
2362
+ }
2363
+ if (fallback) return fallback;
2364
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
2365
+ "div",
2366
+ {
2367
+ className: "flex flex-col items-center justify-center p-6 text-center",
2368
+ "data-chat-error-boundary": "true",
2369
+ children: [
2370
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-chat-error text-lg font-semibold mb-2", children: "Something went wrong" }),
2371
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-chat-text-secondary text-sm mb-4", children: this.state.error.message }),
2372
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2373
+ "button",
2374
+ {
2375
+ onClick: () => this.setState({ error: null }),
2376
+ className: "px-4 py-2 bg-chat-primary text-white rounded text-sm cursor-pointer hover:opacity-90",
2377
+ children: "Try again"
2378
+ }
2379
+ )
2380
+ ]
2381
+ }
2382
+ );
2383
+ }
2384
+ };
2385
+
2386
+ // src/components/PushPermissionPrompt.tsx
2387
+ var import_react16 = require("react");
2388
+ var import_jsx_runtime19 = require("react/jsx-runtime");
2389
+ function PushPermissionPrompt({
2390
+ autoHide = true,
2391
+ title,
2392
+ description,
2393
+ onStatusChange,
2394
+ getVapidPublicKey,
2395
+ onSubscribe,
2396
+ onUnsubscribe
2397
+ }) {
2398
+ const { t } = useLocale();
2399
+ const { status, isSupported, isSubscribed, subscribe, unsubscribe } = usePushNotifications({
2400
+ enabled: true,
2401
+ getVapidPublicKey,
2402
+ onSubscribe,
2403
+ onUnsubscribe
2404
+ });
2405
+ const [dismissed, setDismissed] = (0, import_react16.useState)(false);
2406
+ if (!isSupported) return null;
2407
+ if (autoHide && (isSubscribed || status === "denied")) return null;
2408
+ if (dismissed) return null;
2409
+ const handleEnable = async () => {
2410
+ await subscribe();
2411
+ onStatusChange?.(true);
2412
+ };
2413
+ const handleDisable = async () => {
2414
+ await unsubscribe();
2415
+ onStatusChange?.(false);
2416
+ };
2417
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
2418
+ "div",
2419
+ {
2420
+ className: "p-3 bg-chat-surface rounded-lg flex items-start gap-3",
2421
+ "data-chat-push-prompt": "true",
2422
+ children: [
2423
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex-1", children: [
2424
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "font-semibold mb-1 text-chat-text", children: title || t("push.title") }),
2425
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-sm text-chat-text-secondary", children: description || t("push.description") })
2426
+ ] }),
2427
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex gap-2", children: [
2428
+ !isSubscribed && status !== "denied" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2429
+ "button",
2430
+ {
2431
+ onClick: handleEnable,
2432
+ className: "px-3 py-1.5 bg-chat-primary text-white border-none rounded cursor-pointer text-sm hover:opacity-90",
2433
+ children: t("push.enable")
2434
+ }
2435
+ ),
2436
+ isSubscribed && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2437
+ "button",
2438
+ {
2439
+ onClick: handleDisable,
2440
+ className: "px-3 py-1.5 bg-transparent text-chat-text-secondary border border-chat-border rounded cursor-pointer text-sm hover:bg-chat-surface",
2441
+ children: t("push.disable")
2442
+ }
2443
+ ),
2444
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2445
+ "button",
2446
+ {
2447
+ onClick: () => setDismissed(true),
2448
+ className: "px-1.5 bg-transparent border-none cursor-pointer text-chat-text-secondary hover:text-chat-text",
2449
+ children: "\u2715"
2450
+ }
2451
+ )
2452
+ ] })
2453
+ ]
2454
+ }
2455
+ );
2456
+ }
2457
+
2458
+ // src/components/PushToggle.tsx
2459
+ var import_jsx_runtime20 = require("react/jsx-runtime");
2460
+ function PushToggle({ getVapidPublicKey, onSubscribe, onUnsubscribe }) {
2461
+ const { t } = useLocale();
2462
+ const { status, isSupported, isSubscribed, subscribe, unsubscribe } = usePushNotifications({
2463
+ enabled: true,
2464
+ getVapidPublicKey,
2465
+ onSubscribe,
2466
+ onUnsubscribe
2467
+ });
2468
+ if (!isSupported) {
2469
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "text-sm text-chat-text-secondary", children: t("push.unsupported") });
2470
+ }
2471
+ if (status === "denied") {
2472
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "text-sm text-chat-text-secondary", children: t("push.denied") });
2473
+ }
2474
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("label", { className: "flex items-center gap-2 cursor-pointer", children: [
2475
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2476
+ "input",
2477
+ {
2478
+ type: "checkbox",
2479
+ checked: isSubscribed,
2480
+ onChange: async (e) => {
2481
+ if (e.target.checked) await subscribe();
2482
+ else await unsubscribe();
2483
+ },
2484
+ disabled: status === "subscribing",
2485
+ className: "w-4 h-4 cursor-pointer"
2486
+ }
2487
+ ),
2488
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-sm", children: status === "subscribing" ? t("push.subscribing") : t("push.notifications") })
2489
+ ] });
2490
+ }
2491
+
2492
+ // src/index.ts
2493
+ var import_js_web_adapter_core3 = require("@bootdesk/js-web-adapter-core");
2494
+ var import_js_web_adapter_core4 = require("@bootdesk/js-web-adapter-core");
2495
+ // Annotate the CommonJS export names for ESM import in node:
2496
+ 0 && (module.exports = {
2497
+ AttachmentList,
2498
+ CardProvider,
2499
+ CardRenderer,
2500
+ ChatProvider,
2501
+ ChatWidget,
2502
+ DefaultCard,
2503
+ Dropzone,
2504
+ ErrorBoundary,
2505
+ FileCardComponent,
2506
+ FloatingButton,
2507
+ Header,
2508
+ ImageCardComponent,
2509
+ InputArea,
2510
+ LaravelEchoBroadcastClient,
2511
+ LocaleProvider,
2512
+ MarkdownRenderer,
2513
+ MessageContent,
2514
+ MessageList,
2515
+ PushManager,
2516
+ PushPermissionPrompt,
2517
+ PushToggle,
2518
+ PusherBroadcastClient,
2519
+ TypingIndicator,
2520
+ WebChatClient,
2521
+ createPushSubscriptionHandlers,
2522
+ getAvailableLocales,
2523
+ registerLocale,
2524
+ renderMarkdown,
2525
+ useAttachmentUpload,
2526
+ useCardRegistry,
2527
+ useCardRendererRegistry,
2528
+ useChatClient,
2529
+ useChatContext,
2530
+ useLocale,
2531
+ useMessages,
2532
+ usePushNotifications,
2533
+ useStreaming,
2534
+ useTyping
2535
+ });
2536
+ //# sourceMappingURL=index.cjs.map