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