@xcelsior/ui-chat 1.0.4 → 1.0.5

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.mjs ADDED
@@ -0,0 +1,1576 @@
1
+ // src/components/ChatWidget.tsx
2
+ import { useCallback as useCallback4, useEffect as useEffect6, useState as useState6 } from "react";
3
+
4
+ // src/hooks/useWebSocket.ts
5
+ import { useEffect, useRef, useState, useCallback } from "react";
6
+ function useWebSocket(config, externalWebSocket) {
7
+ const [isConnected, setIsConnected] = useState(false);
8
+ const [lastMessage, setLastMessage] = useState(null);
9
+ const [error, setError] = useState(null);
10
+ const wsRef = useRef(null);
11
+ const reconnectTimeoutRef = useRef(null);
12
+ const reconnectAttemptsRef = useRef(0);
13
+ const messageHandlerRef = useRef(null);
14
+ const maxReconnectAttempts = 5;
15
+ const reconnectDelay = 3e3;
16
+ const isUsingExternalWs = !!externalWebSocket;
17
+ const subscribeToMessage = useCallback((webSocket) => {
18
+ if (messageHandlerRef.current) {
19
+ webSocket.removeEventListener("message", messageHandlerRef.current);
20
+ }
21
+ const handler = (event) => {
22
+ try {
23
+ const message = JSON.parse(event.data);
24
+ setLastMessage(message);
25
+ if (message.type === "message" && message.data) {
26
+ config.onMessageReceived?.(message.data);
27
+ } else if (message.type === "error") {
28
+ const err = new Error(message.data?.message || "WebSocket error");
29
+ setError(err);
30
+ config.onError?.(err);
31
+ }
32
+ } catch (err) {
33
+ console.error("Failed to parse WebSocket message:", err);
34
+ }
35
+ };
36
+ webSocket.addEventListener("message", handler);
37
+ messageHandlerRef.current = handler;
38
+ return () => {
39
+ webSocket.removeEventListener("message", handler);
40
+ if (messageHandlerRef.current === handler) {
41
+ messageHandlerRef.current = null;
42
+ }
43
+ };
44
+ }, []);
45
+ const connect = useCallback(() => {
46
+ console.log("connecting to WebSocket...", config.currentUser, config.conversationId);
47
+ try {
48
+ if (wsRef.current) {
49
+ if (messageHandlerRef.current) {
50
+ wsRef.current.removeEventListener("message", messageHandlerRef.current);
51
+ messageHandlerRef.current = null;
52
+ }
53
+ wsRef.current.close();
54
+ }
55
+ const url = new URL(config.websocketUrl);
56
+ url.searchParams.set("user", JSON.stringify(config.currentUser));
57
+ if (config.conversationId) {
58
+ url.searchParams.set("conversationId", config.conversationId);
59
+ }
60
+ if (config.apiKey) {
61
+ url.searchParams.set("apiKey", config.apiKey);
62
+ }
63
+ const ws = new WebSocket(url.toString());
64
+ ws.onopen = () => {
65
+ console.log("WebSocket connected");
66
+ setIsConnected(true);
67
+ setError(null);
68
+ reconnectAttemptsRef.current = 0;
69
+ config.onConnectionChange?.(true);
70
+ };
71
+ ws.onerror = (event) => {
72
+ console.error("WebSocket error:", event);
73
+ const err = new Error("WebSocket connection error");
74
+ setError(err);
75
+ config.onError?.(err);
76
+ };
77
+ ws.onclose = (event) => {
78
+ console.log("WebSocket closed:", event.code, event.reason);
79
+ setIsConnected(false);
80
+ config.onConnectionChange?.(false);
81
+ wsRef.current = null;
82
+ if (event.code !== 1e3 && reconnectAttemptsRef.current < maxReconnectAttempts) {
83
+ reconnectAttemptsRef.current += 1;
84
+ console.log(
85
+ `Reconnecting... (${reconnectAttemptsRef.current}/${maxReconnectAttempts})`
86
+ );
87
+ reconnectTimeoutRef.current = setTimeout(() => {
88
+ connect();
89
+ }, reconnectDelay);
90
+ }
91
+ };
92
+ subscribeToMessage(ws);
93
+ wsRef.current = ws ?? null;
94
+ } catch (err) {
95
+ console.error("Failed to create WebSocket connection:", err);
96
+ const error2 = err instanceof Error ? err : new Error("Failed to connect");
97
+ setError(error2);
98
+ config.onError?.(error2);
99
+ }
100
+ }, [JSON.stringify([config.currentUser, config.conversationId])]);
101
+ const sendMessage = useCallback(
102
+ (action, data) => {
103
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
104
+ console.error("WebSocket is not connected");
105
+ config.toast?.error("Not connected to chat server");
106
+ return;
107
+ }
108
+ try {
109
+ wsRef.current.send(
110
+ JSON.stringify({
111
+ action,
112
+ data
113
+ })
114
+ );
115
+ } catch (err) {
116
+ console.error("Failed to send message:", err);
117
+ const error2 = err instanceof Error ? err : new Error("Failed to send message");
118
+ setError(error2);
119
+ config.onError?.(error2);
120
+ }
121
+ },
122
+ [config]
123
+ );
124
+ const reconnect = useCallback(() => {
125
+ reconnectAttemptsRef.current = 0;
126
+ connect();
127
+ }, [connect]);
128
+ useEffect(() => {
129
+ if (isUsingExternalWs) {
130
+ setIsConnected(externalWebSocket?.readyState === WebSocket.OPEN || false);
131
+ wsRef.current = externalWebSocket;
132
+ const cleanup = subscribeToMessage(externalWebSocket);
133
+ return cleanup;
134
+ }
135
+ connect();
136
+ return () => {
137
+ if (reconnectTimeoutRef.current) {
138
+ clearTimeout(reconnectTimeoutRef.current);
139
+ }
140
+ if (wsRef.current) {
141
+ if (messageHandlerRef.current) {
142
+ wsRef.current.removeEventListener("message", messageHandlerRef.current);
143
+ messageHandlerRef.current = null;
144
+ }
145
+ wsRef.current.close(1e3, "Component unmounted");
146
+ }
147
+ };
148
+ }, [connect, isUsingExternalWs, externalWebSocket, subscribeToMessage]);
149
+ const effectiveIsConnected = isUsingExternalWs ? externalWebSocket?.readyState === WebSocket.OPEN || false : isConnected;
150
+ return {
151
+ isConnected: effectiveIsConnected,
152
+ sendMessage,
153
+ lastMessage,
154
+ error,
155
+ reconnect
156
+ };
157
+ }
158
+
159
+ // src/hooks/useMessages.ts
160
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo, useState as useState2 } from "react";
161
+
162
+ // src/utils/api.ts
163
+ import axios from "axios";
164
+ async function fetchMessages(baseUrl, params, headers) {
165
+ try {
166
+ const response = await axios.get(`${baseUrl}/messages`, {
167
+ params: {
168
+ conversationId: params.conversationId,
169
+ limit: params.limit || 50,
170
+ pageToken: params.pageToken
171
+ },
172
+ headers: {
173
+ "Content-Type": "application/json",
174
+ ...headers
175
+ }
176
+ });
177
+ return {
178
+ data: response.data.data ?? [],
179
+ nextPageToken: response.data.pagination?.nextPageToken
180
+ };
181
+ } catch (error) {
182
+ if (axios.isAxiosError(error)) {
183
+ throw new Error(
184
+ error.response?.data?.error?.message || error.message || "Failed to fetch messages"
185
+ );
186
+ }
187
+ throw error;
188
+ }
189
+ }
190
+
191
+ // src/hooks/useMessages.ts
192
+ function useMessages(websocket, config) {
193
+ const [messages, setMessages] = useState2([]);
194
+ const [isLoading, setIsLoading] = useState2(false);
195
+ const [error, setError] = useState2(null);
196
+ const [nextPageToken, setNextPageToken] = useState2(void 0);
197
+ const [hasMore, setHasMore] = useState2(true);
198
+ const [isLoadingMore, setIsLoadingMore] = useState2(false);
199
+ const { httpApiUrl, conversationId, headers, onError, toast } = config;
200
+ const headersWithApiKey = useMemo(
201
+ () => ({
202
+ ...headers,
203
+ "x-api-key": config.apiKey
204
+ }),
205
+ [headers, config.apiKey]
206
+ );
207
+ useEffect2(() => {
208
+ const loadMessages = async () => {
209
+ if (!httpApiUrl || !conversationId) {
210
+ return;
211
+ }
212
+ setIsLoading(true);
213
+ setError(null);
214
+ try {
215
+ const result = await fetchMessages(
216
+ httpApiUrl,
217
+ { conversationId, limit: 20 },
218
+ headersWithApiKey
219
+ );
220
+ setMessages(result.data);
221
+ setNextPageToken(result.nextPageToken);
222
+ setHasMore(!!result.nextPageToken);
223
+ } catch (err) {
224
+ const error2 = err instanceof Error ? err : new Error("Failed to load messages");
225
+ setError(error2);
226
+ onError?.(error2);
227
+ toast?.error("Failed to load existing messages");
228
+ } finally {
229
+ setIsLoading(false);
230
+ }
231
+ };
232
+ loadMessages();
233
+ }, [conversationId, httpApiUrl, headersWithApiKey, onError, toast]);
234
+ const { onMessageReceived } = config;
235
+ useEffect2(() => {
236
+ if (websocket.lastMessage?.type === "message" && websocket.lastMessage.data) {
237
+ const newMessage = websocket.lastMessage.data;
238
+ if (conversationId && newMessage.conversationId !== conversationId) {
239
+ return;
240
+ }
241
+ setMessages((prev) => {
242
+ if (prev.some((msg) => msg.id === newMessage.id)) {
243
+ return prev;
244
+ }
245
+ return [...prev, newMessage];
246
+ });
247
+ onMessageReceived?.(newMessage);
248
+ }
249
+ }, [websocket.lastMessage, onMessageReceived, conversationId]);
250
+ const addMessage = useCallback2((message) => {
251
+ setMessages((prev) => {
252
+ if (prev.some((msg) => msg.id === message.id)) {
253
+ return prev;
254
+ }
255
+ return [...prev, message];
256
+ });
257
+ }, []);
258
+ const updateMessageStatus = useCallback2((messageId, status) => {
259
+ setMessages((prev) => prev.map((msg) => msg.id === messageId ? { ...msg, status } : msg));
260
+ }, []);
261
+ const clearMessages = useCallback2(() => {
262
+ setMessages([]);
263
+ }, []);
264
+ const loadMore = useCallback2(async () => {
265
+ if (!hasMore || isLoadingMore || !httpApiUrl || !conversationId || !nextPageToken) {
266
+ return;
267
+ }
268
+ setIsLoadingMore(true);
269
+ setError(null);
270
+ try {
271
+ const result = await fetchMessages(
272
+ httpApiUrl,
273
+ {
274
+ conversationId,
275
+ limit: 20,
276
+ pageToken: nextPageToken
277
+ },
278
+ headersWithApiKey
279
+ );
280
+ setMessages((prev) => [...result.data, ...prev]);
281
+ setNextPageToken(result.nextPageToken);
282
+ setHasMore(!!result.nextPageToken);
283
+ } catch (err) {
284
+ const error2 = err instanceof Error ? err : new Error("Failed to load more messages");
285
+ setError(error2);
286
+ onError?.(error2);
287
+ } finally {
288
+ setIsLoadingMore(false);
289
+ }
290
+ }, [
291
+ hasMore,
292
+ isLoadingMore,
293
+ httpApiUrl,
294
+ conversationId,
295
+ nextPageToken,
296
+ headersWithApiKey,
297
+ onError
298
+ ]);
299
+ return {
300
+ messages,
301
+ addMessage,
302
+ updateMessageStatus,
303
+ clearMessages,
304
+ isLoading,
305
+ error,
306
+ loadMore,
307
+ hasMore,
308
+ isLoadingMore
309
+ };
310
+ }
311
+
312
+ // src/hooks/useFileUpload.ts
313
+ import { useState as useState3 } from "react";
314
+ import axios2 from "axios";
315
+ function useFileUpload(apiKey, config) {
316
+ const [isUploading, setIsUploading] = useState3(false);
317
+ const [uploadProgress, setUploadProgress] = useState3(0);
318
+ const [error, setError] = useState3(null);
319
+ const defaultConfig = {
320
+ maxFileSize: 10 * 1024 * 1024,
321
+ // 10MB default
322
+ allowedTypes: [
323
+ "image/jpeg",
324
+ "image/jpg",
325
+ "image/png",
326
+ "image/gif",
327
+ "image/webp",
328
+ "application/pdf",
329
+ "application/msword",
330
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
331
+ "text/plain",
332
+ "text/csv"
333
+ ]
334
+ };
335
+ const finalConfig = { ...defaultConfig, ...config };
336
+ const canUpload = !!config?.uploadUrl;
337
+ const validateFile = (file) => {
338
+ if (!finalConfig.allowedTypes.includes(file.type)) {
339
+ return `File type ${file.type} is not supported. Allowed types: ${finalConfig.allowedTypes.join(", ")}`;
340
+ }
341
+ if (file.size > finalConfig.maxFileSize) {
342
+ return `File size ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds the maximum allowed size of ${(finalConfig.maxFileSize / 1024 / 1024).toFixed(2)}MB`;
343
+ }
344
+ return null;
345
+ };
346
+ const uploadFile = async (file) => {
347
+ if (!config?.uploadUrl) {
348
+ const err = new Error("File upload URL is not configured");
349
+ setError(err);
350
+ throw err;
351
+ }
352
+ const validationError = validateFile(file);
353
+ if (validationError) {
354
+ const err = new Error(validationError);
355
+ setError(err);
356
+ throw err;
357
+ }
358
+ setIsUploading(true);
359
+ setUploadProgress(0);
360
+ setError(null);
361
+ try {
362
+ const uploadUrlResponse = await axios2.post(
363
+ config.uploadUrl,
364
+ {
365
+ fileName: file.name,
366
+ contentType: file.type,
367
+ fileSize: file.size
368
+ },
369
+ {
370
+ headers: {
371
+ "Content-Type": "application/json",
372
+ "x-api-key": apiKey || "",
373
+ ...config.headers
374
+ }
375
+ }
376
+ );
377
+ const { uploadUrl, attachmentUrl } = uploadUrlResponse.data.data || uploadUrlResponse.data;
378
+ if (!uploadUrl || !attachmentUrl) {
379
+ throw new Error("Failed to get upload URL from server");
380
+ }
381
+ await axios2.put(uploadUrl, file, {
382
+ headers: {
383
+ "Content-Type": file.type
384
+ },
385
+ onUploadProgress: (progressEvent) => {
386
+ if (progressEvent.total) {
387
+ const progress = Math.round(
388
+ progressEvent.loaded * 100 / progressEvent.total
389
+ );
390
+ setUploadProgress(progress);
391
+ }
392
+ }
393
+ });
394
+ return {
395
+ url: attachmentUrl,
396
+ name: file.name,
397
+ size: file.size,
398
+ type: file.type,
399
+ markdown: file.type.startsWith("image/") ? `![${file.name}](${attachmentUrl})` : `[${file.name}](${attachmentUrl})`
400
+ };
401
+ } catch (err) {
402
+ console.error("File upload failed:", err);
403
+ const error2 = err instanceof Error ? err : new Error("Upload failed");
404
+ setError(error2);
405
+ throw error2;
406
+ } finally {
407
+ setIsUploading(false);
408
+ setUploadProgress(0);
409
+ }
410
+ };
411
+ return {
412
+ uploadFile,
413
+ isUploading,
414
+ uploadProgress,
415
+ error,
416
+ canUpload
417
+ };
418
+ }
419
+
420
+ // src/hooks/useTypingIndicator.ts
421
+ import { useEffect as useEffect3, useState as useState4 } from "react";
422
+ function useTypingIndicator(websocket) {
423
+ const [typingUsers, setTypingUsers] = useState4([]);
424
+ useEffect3(() => {
425
+ if (websocket.lastMessage?.type === "typing" && websocket.lastMessage.data) {
426
+ const { userId, isTyping } = websocket.lastMessage.data;
427
+ if (isTyping) {
428
+ setTypingUsers((prev) => {
429
+ if (!prev.includes(userId)) {
430
+ return [...prev, userId];
431
+ }
432
+ return prev;
433
+ });
434
+ } else {
435
+ setTypingUsers((prev) => prev.filter((id) => id !== userId));
436
+ }
437
+ }
438
+ }, [websocket.lastMessage]);
439
+ return {
440
+ isTyping: typingUsers.length > 0,
441
+ typingUsers
442
+ };
443
+ }
444
+
445
+ // src/components/ChatHeader.tsx
446
+ import { jsx, jsxs } from "react/jsx-runtime";
447
+ function ChatHeader({ agent, onClose, onMinimize }) {
448
+ return /* @__PURE__ */ jsxs("div", { className: "bg-gradient-to-r from-blue-600 to-purple-600 text-white p-4 flex items-center justify-between", children: [
449
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
450
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
451
+ /* @__PURE__ */ jsx("div", { className: "h-10 w-10 rounded-full bg-white/20 flex items-center justify-center text-lg font-medium", children: agent?.avatar ? /* @__PURE__ */ jsx(
452
+ "img",
453
+ {
454
+ src: agent.avatar,
455
+ alt: agent.name,
456
+ className: "h-10 w-10 rounded-full object-cover"
457
+ }
458
+ ) : "\u{1F3A7}" }),
459
+ agent?.status === "online" && /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500 border-2 border-white" })
460
+ ] }),
461
+ /* @__PURE__ */ jsxs("div", { children: [
462
+ /* @__PURE__ */ jsx("h3", { className: "font-semibold text-base", children: agent?.name || "Support Team" }),
463
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-white/80", children: agent?.status === "online" ? "Online" : agent?.status === "away" ? "Away" : "We'll reply as soon as possible" })
464
+ ] })
465
+ ] }),
466
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
467
+ onMinimize && /* @__PURE__ */ jsx(
468
+ "button",
469
+ {
470
+ type: "button",
471
+ onClick: onMinimize,
472
+ className: "p-2 hover:bg-white/10 rounded-full transition-colors",
473
+ "aria-label": "Minimize chat",
474
+ children: /* @__PURE__ */ jsxs(
475
+ "svg",
476
+ {
477
+ className: "w-5 h-5",
478
+ fill: "none",
479
+ viewBox: "0 0 24 24",
480
+ stroke: "currentColor",
481
+ "aria-hidden": "true",
482
+ children: [
483
+ /* @__PURE__ */ jsx("title", { children: "Minimize icon" }),
484
+ /* @__PURE__ */ jsx(
485
+ "path",
486
+ {
487
+ strokeLinecap: "round",
488
+ strokeLinejoin: "round",
489
+ strokeWidth: 2,
490
+ d: "M20 12H4"
491
+ }
492
+ )
493
+ ]
494
+ }
495
+ )
496
+ }
497
+ ),
498
+ onClose && /* @__PURE__ */ jsx(
499
+ "button",
500
+ {
501
+ type: "button",
502
+ onClick: onClose,
503
+ className: "p-2 hover:bg-white/10 rounded-full transition-colors",
504
+ "aria-label": "Close chat",
505
+ children: /* @__PURE__ */ jsxs(
506
+ "svg",
507
+ {
508
+ className: "w-5 h-5",
509
+ fill: "none",
510
+ viewBox: "0 0 24 24",
511
+ stroke: "currentColor",
512
+ "aria-hidden": "true",
513
+ children: [
514
+ /* @__PURE__ */ jsx("title", { children: "Close icon" }),
515
+ /* @__PURE__ */ jsx(
516
+ "path",
517
+ {
518
+ strokeLinecap: "round",
519
+ strokeLinejoin: "round",
520
+ strokeWidth: 2,
521
+ d: "M6 18L18 6M6 6l12 12"
522
+ }
523
+ )
524
+ ]
525
+ }
526
+ )
527
+ }
528
+ )
529
+ ] })
530
+ ] });
531
+ }
532
+
533
+ // src/components/MessageList.tsx
534
+ import { useEffect as useEffect4, useRef as useRef2, useCallback as useCallback3 } from "react";
535
+ import { Spinner } from "@xcelsior/design-system";
536
+
537
+ // src/components/MessageItem.tsx
538
+ import { formatDistanceToNow } from "date-fns";
539
+ import ReactMarkdown from "react-markdown";
540
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
541
+ function MessageItem({
542
+ message,
543
+ currentUser,
544
+ showAvatar = true,
545
+ showTimestamp = true
546
+ }) {
547
+ const isOwnMessage = message.senderType === currentUser.type;
548
+ const isSystemMessage = message.senderType === "system";
549
+ const isAIMessage = message.metadata?.isAI === true;
550
+ if (isSystemMessage) {
551
+ return /* @__PURE__ */ jsx2("div", { className: "flex justify-center my-4", children: /* @__PURE__ */ jsx2("div", { className: "px-4 py-2 bg-gray-100 dark:bg-gray-800 rounded-full", children: /* @__PURE__ */ jsx2("p", { className: "text-xs text-gray-600 dark:text-gray-400", children: message.content }) }) });
552
+ }
553
+ const getAvatarIcon = () => {
554
+ if (isAIMessage) {
555
+ return "\u{1F916}";
556
+ }
557
+ if (message.senderType === "agent") {
558
+ return "\u{1F3A7}";
559
+ }
560
+ return "\u{1F464}";
561
+ };
562
+ return /* @__PURE__ */ jsxs2("div", { className: `flex gap-2 mb-4 ${!isOwnMessage ? "flex-row-reverse" : "flex-row"}`, children: [
563
+ showAvatar && /* @__PURE__ */ jsx2("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsx2("div", { className: "h-8 w-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white text-sm font-medium", children: getAvatarIcon() }) }),
564
+ /* @__PURE__ */ jsxs2(
565
+ "div",
566
+ {
567
+ className: `flex flex-col max-w-[70%] ${!isOwnMessage ? "items-end" : "items-start"}`,
568
+ children: [
569
+ /* @__PURE__ */ jsxs2(
570
+ "div",
571
+ {
572
+ className: `rounded-2xl px-4 py-2 ${isOwnMessage ? "bg-blue-600 text-white" : "bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100"}`,
573
+ children: [
574
+ message.messageType === "text" && /* @__PURE__ */ jsx2("div", { className: "prose prose-sm dark:prose-invert max-w-none", children: /* @__PURE__ */ jsx2(
575
+ ReactMarkdown,
576
+ {
577
+ components: {
578
+ p: ({ children }) => /* @__PURE__ */ jsx2("p", { className: "mb-0", children }),
579
+ img: ({ src, alt, ...props }) => /* @__PURE__ */ jsx2(
580
+ "img",
581
+ {
582
+ ...props,
583
+ src,
584
+ alt,
585
+ className: "max-w-full h-auto rounded-lg shadow-sm my-2",
586
+ loading: "lazy"
587
+ }
588
+ ),
589
+ a: ({ href, children, ...props }) => /* @__PURE__ */ jsx2(
590
+ "a",
591
+ {
592
+ ...props,
593
+ href,
594
+ target: "_blank",
595
+ rel: "noopener noreferrer",
596
+ className: `${isOwnMessage ? "text-blue-200 hover:text-blue-100" : "text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"} underline`,
597
+ children
598
+ }
599
+ )
600
+ },
601
+ children: message.content
602
+ }
603
+ ) }),
604
+ message.messageType === "image" && /* @__PURE__ */ jsx2("div", { children: /* @__PURE__ */ jsx2(
605
+ "img",
606
+ {
607
+ src: message.content,
608
+ alt: "Attachment",
609
+ className: "max-w-full h-auto rounded-lg",
610
+ loading: "lazy"
611
+ }
612
+ ) }),
613
+ message.messageType === "file" && /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
614
+ /* @__PURE__ */ jsx2("span", { className: "text-2xl", children: "\u{1F4CE}" }),
615
+ /* @__PURE__ */ jsx2(
616
+ "a",
617
+ {
618
+ href: message.content,
619
+ target: "_blank",
620
+ rel: "noopener noreferrer",
621
+ className: `${isOwnMessage ? "text-blue-200 hover:text-blue-100" : "text-blue-600 hover:text-blue-700 dark:text-blue-400"} underline`,
622
+ children: message.metadata?.fileName || "Download file"
623
+ }
624
+ )
625
+ ] })
626
+ ]
627
+ }
628
+ ),
629
+ showTimestamp && /* @__PURE__ */ jsxs2(
630
+ "div",
631
+ {
632
+ className: `flex items-center gap-2 mt-1 px-2 ${isOwnMessage ? "flex-row-reverse" : "flex-row"}`,
633
+ children: [
634
+ /* @__PURE__ */ jsx2("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: formatDistanceToNow(new Date(message.createdAt), {
635
+ addSuffix: true
636
+ }) }),
637
+ isOwnMessage && message.status && /* @__PURE__ */ jsxs2("span", { className: "text-xs", children: [
638
+ message.status === "sent" && "\u2713",
639
+ message.status === "delivered" && "\u2713\u2713",
640
+ message.status === "read" && /* @__PURE__ */ jsx2("span", { className: "text-blue-600", children: "\u2713\u2713" })
641
+ ] })
642
+ ]
643
+ }
644
+ )
645
+ ]
646
+ }
647
+ )
648
+ ] });
649
+ }
650
+
651
+ // src/components/MessageList.tsx
652
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
653
+ function MessageList({
654
+ messages,
655
+ currentUser,
656
+ isLoading = false,
657
+ isTyping = false,
658
+ typingUser,
659
+ autoScroll = true,
660
+ onLoadMore,
661
+ hasMore = false,
662
+ isLoadingMore = false
663
+ }) {
664
+ const messagesEndRef = useRef2(null);
665
+ const containerRef = useRef2(null);
666
+ const prevLengthRef = useRef2(messages.length);
667
+ const loadMoreTriggerRef = useRef2(null);
668
+ const prevScrollHeightRef = useRef2(0);
669
+ const hasInitialScrolledRef = useRef2(false);
670
+ const isUserScrollingRef = useRef2(false);
671
+ useEffect4(() => {
672
+ if (autoScroll && messagesEndRef.current) {
673
+ if (messages.length > prevLengthRef.current && !isLoadingMore) {
674
+ messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
675
+ }
676
+ prevLengthRef.current = messages.length;
677
+ }
678
+ }, [messages.length, autoScroll, isLoadingMore]);
679
+ useEffect4(() => {
680
+ if (messages.length > 0 && messagesEndRef.current && !isLoading && !hasInitialScrolledRef.current) {
681
+ setTimeout(() => {
682
+ messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
683
+ setTimeout(() => {
684
+ isUserScrollingRef.current = true;
685
+ }, 200);
686
+ }, 100);
687
+ hasInitialScrolledRef.current = true;
688
+ } else if (!isLoading && messages.length === 0 && !hasInitialScrolledRef.current) {
689
+ isUserScrollingRef.current = true;
690
+ hasInitialScrolledRef.current = true;
691
+ }
692
+ }, [isLoading, messages.length]);
693
+ useEffect4(() => {
694
+ if (isLoadingMore) {
695
+ prevScrollHeightRef.current = containerRef.current?.scrollHeight || 0;
696
+ } else if (prevScrollHeightRef.current > 0 && containerRef.current) {
697
+ const newScrollHeight = containerRef.current.scrollHeight;
698
+ const scrollDiff = newScrollHeight - prevScrollHeightRef.current;
699
+ containerRef.current.scrollTop = scrollDiff;
700
+ prevScrollHeightRef.current = 0;
701
+ }
702
+ }, [isLoadingMore]);
703
+ const handleScroll = useCallback3(() => {
704
+ if (!containerRef.current || !onLoadMore || !hasMore || isLoadingMore) return;
705
+ if (!isUserScrollingRef.current) return;
706
+ const { scrollTop } = containerRef.current;
707
+ if (scrollTop < 100) {
708
+ onLoadMore();
709
+ }
710
+ }, [onLoadMore, hasMore, isLoadingMore]);
711
+ useEffect4(() => {
712
+ const container = containerRef.current;
713
+ if (!container) return;
714
+ container.addEventListener("scroll", handleScroll);
715
+ return () => container.removeEventListener("scroll", handleScroll);
716
+ }, [handleScroll]);
717
+ if (isLoading) {
718
+ return /* @__PURE__ */ jsx3("div", { className: "flex items-center justify-center h-full", children: /* @__PURE__ */ jsx3(Spinner, { size: "lg" }) });
719
+ }
720
+ if (messages.length === 0) {
721
+ return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-center justify-center h-full text-center p-8", children: [
722
+ /* @__PURE__ */ jsx3("div", { className: "text-6xl mb-4", children: "\u{1F4AC}" }),
723
+ /* @__PURE__ */ jsx3("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2", children: "No messages yet" }),
724
+ /* @__PURE__ */ jsx3("p", { className: "text-sm text-gray-600 dark:text-gray-400", children: "Start the conversation by sending a message below" })
725
+ ] });
726
+ }
727
+ return /* @__PURE__ */ jsxs3(
728
+ "div",
729
+ {
730
+ ref: containerRef,
731
+ className: "flex-1 overflow-y-auto p-4 space-y-2",
732
+ style: { scrollBehavior: "smooth" },
733
+ children: [
734
+ isLoadingMore && /* @__PURE__ */ jsx3("div", { className: "flex justify-center py-4", children: /* @__PURE__ */ jsx3(Spinner, { size: "sm" }) }),
735
+ /* @__PURE__ */ jsx3("div", { ref: loadMoreTriggerRef }),
736
+ messages.map((message) => /* @__PURE__ */ jsx3(
737
+ MessageItem,
738
+ {
739
+ message,
740
+ currentUser,
741
+ showAvatar: true,
742
+ showTimestamp: true
743
+ },
744
+ message.id
745
+ )),
746
+ isTyping && /* @__PURE__ */ jsxs3("div", { className: "flex gap-2 mb-4", children: [
747
+ /* @__PURE__ */ jsx3("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsx3("div", { className: "h-8 w-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white text-sm font-medium", children: "\u{1F3A7}" }) }),
748
+ /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-start", children: [
749
+ /* @__PURE__ */ jsx3("div", { className: "rounded-2xl px-4 py-3 bg-gray-100 dark:bg-gray-800", children: /* @__PURE__ */ jsxs3("div", { className: "flex gap-1", children: [
750
+ /* @__PURE__ */ jsx3("span", { className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce" }),
751
+ /* @__PURE__ */ jsx3(
752
+ "span",
753
+ {
754
+ className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce",
755
+ style: { animationDelay: "0.1s" }
756
+ }
757
+ ),
758
+ /* @__PURE__ */ jsx3(
759
+ "span",
760
+ {
761
+ className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce",
762
+ style: { animationDelay: "0.2s" }
763
+ }
764
+ )
765
+ ] }) }),
766
+ typingUser && /* @__PURE__ */ jsxs3("span", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1 px-2", children: [
767
+ typingUser,
768
+ " is typing..."
769
+ ] })
770
+ ] })
771
+ ] }),
772
+ /* @__PURE__ */ jsx3("div", { ref: messagesEndRef })
773
+ ]
774
+ }
775
+ );
776
+ }
777
+
778
+ // src/components/ChatInput.tsx
779
+ import { useEffect as useEffect5, useRef as useRef3, useState as useState5 } from "react";
780
+ import { createPortal } from "react-dom";
781
+ import { Button, TextArea } from "@xcelsior/design-system";
782
+ import Picker from "@emoji-mart/react";
783
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
784
+ function ChatInput({
785
+ onSend,
786
+ onTyping,
787
+ config,
788
+ fileUpload,
789
+ disabled = false
790
+ }) {
791
+ const [message, setMessage] = useState5("");
792
+ const [showEmojiPicker, setShowEmojiPicker] = useState5(false);
793
+ const [emojiData, setEmojiData] = useState5();
794
+ const [emojiPickerPosition, setEmojiPickerPosition] = useState5(null);
795
+ const textAreaRef = useRef3(null);
796
+ const emojiPickerRef = useRef3(null);
797
+ const emojiButtonRef = useRef3(null);
798
+ const fileInputRef = useRef3(null);
799
+ const typingTimeoutRef = useRef3(null);
800
+ const startTypingTimeoutRef = useRef3(null);
801
+ const isTypingRef = useRef3(false);
802
+ const enableEmoji = config.enableEmoji ?? true;
803
+ const enableFileUpload = config.enableFileUpload ?? true;
804
+ useEffect5(() => {
805
+ if (!enableEmoji) return;
806
+ (async () => {
807
+ try {
808
+ const response = await fetch("https://cdn.jsdelivr.net/npm/@emoji-mart/data");
809
+ setEmojiData(await response.json());
810
+ } catch (error) {
811
+ console.error("Failed to load emoji data:", error);
812
+ }
813
+ })();
814
+ }, [enableEmoji]);
815
+ useEffect5(() => {
816
+ const handleClickOutside = (event) => {
817
+ if (emojiPickerRef.current && !emojiPickerRef.current.contains(event.target)) {
818
+ setShowEmojiPicker(false);
819
+ }
820
+ };
821
+ if (showEmojiPicker) {
822
+ document.addEventListener("mousedown", handleClickOutside);
823
+ }
824
+ return () => {
825
+ document.removeEventListener("mousedown", handleClickOutside);
826
+ };
827
+ }, [showEmojiPicker]);
828
+ useEffect5(() => {
829
+ return () => {
830
+ if (typingTimeoutRef.current) {
831
+ clearTimeout(typingTimeoutRef.current);
832
+ }
833
+ if (startTypingTimeoutRef.current) {
834
+ clearTimeout(startTypingTimeoutRef.current);
835
+ }
836
+ };
837
+ }, []);
838
+ useEffect5(() => {
839
+ if (!showEmojiPicker) return;
840
+ const updatePosition = () => {
841
+ if (emojiButtonRef.current) {
842
+ const rect = emojiButtonRef.current.getBoundingClientRect();
843
+ setEmojiPickerPosition({
844
+ top: rect.top - 370,
845
+ left: rect.left - 300
846
+ });
847
+ }
848
+ };
849
+ window.addEventListener("resize", updatePosition);
850
+ window.addEventListener("scroll", updatePosition, true);
851
+ return () => {
852
+ window.removeEventListener("resize", updatePosition);
853
+ window.removeEventListener("scroll", updatePosition, true);
854
+ };
855
+ }, [showEmojiPicker]);
856
+ const handleTyping = (value) => {
857
+ setMessage(value);
858
+ if (onTyping && config.enableTypingIndicator !== false) {
859
+ if (startTypingTimeoutRef.current) {
860
+ clearTimeout(startTypingTimeoutRef.current);
861
+ }
862
+ if (typingTimeoutRef.current) {
863
+ clearTimeout(typingTimeoutRef.current);
864
+ }
865
+ if (!isTypingRef.current) {
866
+ startTypingTimeoutRef.current = setTimeout(() => {
867
+ onTyping(true);
868
+ isTypingRef.current = true;
869
+ }, 300);
870
+ }
871
+ typingTimeoutRef.current = setTimeout(() => {
872
+ if (isTypingRef.current) {
873
+ onTyping(false);
874
+ isTypingRef.current = false;
875
+ }
876
+ }, 1500);
877
+ }
878
+ };
879
+ const handleSend = () => {
880
+ const trimmedMessage = message.trim();
881
+ if (!trimmedMessage || disabled) return;
882
+ onSend(trimmedMessage);
883
+ setMessage("");
884
+ setShowEmojiPicker(false);
885
+ if (onTyping) {
886
+ if (typingTimeoutRef.current) {
887
+ clearTimeout(typingTimeoutRef.current);
888
+ }
889
+ if (startTypingTimeoutRef.current) {
890
+ clearTimeout(startTypingTimeoutRef.current);
891
+ }
892
+ if (isTypingRef.current) {
893
+ onTyping(false);
894
+ isTypingRef.current = false;
895
+ }
896
+ }
897
+ textAreaRef.current?.focus();
898
+ };
899
+ const handleKeyDown = (e) => {
900
+ if (e.key === "Enter" && !e.shiftKey) {
901
+ e.preventDefault();
902
+ handleSend();
903
+ }
904
+ };
905
+ const insertAtCursor = (text) => {
906
+ const textarea = textAreaRef.current;
907
+ if (!textarea) {
908
+ setMessage((prev) => prev + text);
909
+ return;
910
+ }
911
+ const start = textarea.selectionStart;
912
+ const end = textarea.selectionEnd;
913
+ const newValue = message.slice(0, start) + text + message.slice(end);
914
+ setMessage(newValue);
915
+ setTimeout(() => {
916
+ const newCursorPos = start + text.length;
917
+ textarea.setSelectionRange(newCursorPos, newCursorPos);
918
+ textarea.focus();
919
+ }, 0);
920
+ };
921
+ const handleFileSelect = async (event) => {
922
+ const files = event.target.files;
923
+ if (!files || files.length === 0) return;
924
+ const file = files[0];
925
+ try {
926
+ config.toast?.info("Uploading file...");
927
+ const uploadedFile = await fileUpload.uploadFile(file);
928
+ if (uploadedFile?.markdown) {
929
+ insertAtCursor(`
930
+ ${uploadedFile.markdown}
931
+ `);
932
+ config.toast?.success("File uploaded successfully");
933
+ }
934
+ } catch (error) {
935
+ console.error("File upload failed:", error);
936
+ config.toast?.error(error.message);
937
+ } finally {
938
+ if (fileInputRef.current) {
939
+ fileInputRef.current.value = "";
940
+ }
941
+ }
942
+ };
943
+ return /* @__PURE__ */ jsxs4("div", { className: "border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3", children: [
944
+ /* @__PURE__ */ jsx4("div", { className: "relative flex-1", children: /* @__PURE__ */ jsxs4("div", { className: "relative", children: [
945
+ /* @__PURE__ */ jsx4(
946
+ TextArea,
947
+ {
948
+ ref: textAreaRef,
949
+ value: message,
950
+ onChange: (e) => handleTyping(e.target.value),
951
+ onKeyDown: handleKeyDown,
952
+ placeholder: "Type a message...",
953
+ rows: 1,
954
+ className: "resize-none pr-24 pl-4 py-3 rounded-full bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-sm leading-5 placeholder-gray-500 dark:placeholder-gray-400",
955
+ disabled
956
+ }
957
+ ),
958
+ /* @__PURE__ */ jsxs4("div", { className: "absolute right-12 top-1/2 -translate-y-1/2 flex items-center gap-1", children: [
959
+ enableEmoji && /* @__PURE__ */ jsx4("div", { className: "relative", children: /* @__PURE__ */ jsx4(
960
+ "button",
961
+ {
962
+ ref: emojiButtonRef,
963
+ type: "button",
964
+ onClick: () => {
965
+ if (!showEmojiPicker && emojiButtonRef.current) {
966
+ const rect = emojiButtonRef.current.getBoundingClientRect();
967
+ setEmojiPickerPosition({
968
+ top: rect.top - 450,
969
+ left: rect.left - 290
970
+ });
971
+ }
972
+ setShowEmojiPicker((v) => !v);
973
+ },
974
+ className: "p-1.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors",
975
+ disabled,
976
+ "aria-label": "Add emoji",
977
+ children: /* @__PURE__ */ jsx4("span", { className: "text-lg", children: "\u{1F60A}" })
978
+ }
979
+ ) }),
980
+ enableFileUpload && fileUpload.canUpload && /* @__PURE__ */ jsxs4(Fragment, { children: [
981
+ /* @__PURE__ */ jsx4(
982
+ "button",
983
+ {
984
+ type: "button",
985
+ onClick: () => fileInputRef.current?.click(),
986
+ className: "p-1.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors",
987
+ disabled: disabled || fileUpload.isUploading,
988
+ "aria-label": "Attach file",
989
+ children: fileUpload.isUploading ? /* @__PURE__ */ jsx4("span", { className: "text-lg animate-spin", children: "\u23F3" }) : /* @__PURE__ */ jsx4("span", { className: "text-lg", children: "\u{1F4CE}" })
990
+ }
991
+ ),
992
+ /* @__PURE__ */ jsx4(
993
+ "input",
994
+ {
995
+ ref: fileInputRef,
996
+ type: "file",
997
+ accept: "image/*,application/pdf,.doc,.docx",
998
+ className: "hidden",
999
+ onChange: handleFileSelect
1000
+ }
1001
+ )
1002
+ ] })
1003
+ ] }),
1004
+ /* @__PURE__ */ jsx4("div", { className: "absolute right-2 top-1/2 -translate-y-1/2", children: /* @__PURE__ */ jsx4(
1005
+ Button,
1006
+ {
1007
+ onClick: handleSend,
1008
+ disabled: !message.trim() || disabled,
1009
+ variant: "primary",
1010
+ size: "sm",
1011
+ className: "h-9 w-9 p-0 rounded-full flex items-center justify-center shadow-sm",
1012
+ children: /* @__PURE__ */ jsx4("span", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxs4(
1013
+ "svg",
1014
+ {
1015
+ className: "w-4 h-4 rotate-90",
1016
+ fill: "none",
1017
+ viewBox: "0 0 24 24",
1018
+ stroke: "currentColor",
1019
+ "aria-hidden": "true",
1020
+ children: [
1021
+ /* @__PURE__ */ jsx4("title", { children: "Send icon" }),
1022
+ /* @__PURE__ */ jsx4(
1023
+ "path",
1024
+ {
1025
+ strokeLinecap: "round",
1026
+ strokeLinejoin: "round",
1027
+ strokeWidth: 2,
1028
+ d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
1029
+ }
1030
+ )
1031
+ ]
1032
+ }
1033
+ ) })
1034
+ }
1035
+ ) })
1036
+ ] }) }),
1037
+ fileUpload.isUploading && /* @__PURE__ */ jsxs4("div", { className: "mt-2", children: [
1038
+ /* @__PURE__ */ jsx4("div", { className: "w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5", children: /* @__PURE__ */ jsx4(
1039
+ "div",
1040
+ {
1041
+ className: "bg-blue-600 h-1.5 rounded-full transition-all duration-300",
1042
+ style: { width: `${fileUpload.uploadProgress}%` }
1043
+ }
1044
+ ) }),
1045
+ /* @__PURE__ */ jsxs4("p", { className: "text-xs text-gray-600 dark:text-gray-400 mt-1", children: [
1046
+ "Uploading... ",
1047
+ fileUpload.uploadProgress,
1048
+ "%"
1049
+ ] })
1050
+ ] }),
1051
+ showEmojiPicker && emojiData && emojiPickerPosition && typeof document !== "undefined" && createPortal(
1052
+ /* @__PURE__ */ jsx4(
1053
+ "div",
1054
+ {
1055
+ ref: emojiPickerRef,
1056
+ className: "fixed rounded-lg border bg-white dark:bg-gray-800 dark:border-gray-700 shadow-xl",
1057
+ style: {
1058
+ top: `${emojiPickerPosition.top}px`,
1059
+ left: `${emojiPickerPosition.left}px`,
1060
+ zIndex: 9999
1061
+ },
1062
+ children: /* @__PURE__ */ jsx4(
1063
+ Picker,
1064
+ {
1065
+ data: emojiData,
1066
+ onEmojiSelect: (emoji) => {
1067
+ insertAtCursor(emoji.native || emoji.shortcodes || "");
1068
+ setShowEmojiPicker(false);
1069
+ },
1070
+ previewPosition: "none",
1071
+ skinTonePosition: "none",
1072
+ navPosition: "bottom",
1073
+ perLine: 8,
1074
+ searchPosition: "sticky",
1075
+ theme: "auto"
1076
+ }
1077
+ )
1078
+ }
1079
+ ),
1080
+ document.body
1081
+ )
1082
+ ] });
1083
+ }
1084
+
1085
+ // src/components/ChatWidget.tsx
1086
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1087
+ function ChatWidget({
1088
+ config,
1089
+ className = "",
1090
+ variant = "popover",
1091
+ externalWebSocket
1092
+ }) {
1093
+ const [isMinimized, setIsMinimized] = useState6(false);
1094
+ const [isClosed, setIsClosed] = useState6(false);
1095
+ const isFullPage = variant === "fullPage";
1096
+ const websocket = useWebSocket(config, externalWebSocket);
1097
+ const { messages, addMessage, isLoading, loadMore, hasMore, isLoadingMore } = useMessages(
1098
+ websocket,
1099
+ config
1100
+ );
1101
+ const fileUpload = useFileUpload(config.apiKey, config.fileUpload);
1102
+ const { isTyping, typingUsers } = useTypingIndicator(websocket);
1103
+ const handleSendMessage = useCallback4(
1104
+ (content) => {
1105
+ if (!websocket.isConnected) {
1106
+ config.toast?.error("Not connected to chat server");
1107
+ return;
1108
+ }
1109
+ const optimisticMessage = {
1110
+ id: `temp-${Date.now()}`,
1111
+ conversationId: config.conversationId || "",
1112
+ senderId: config.currentUser.email,
1113
+ senderType: config.currentUser.type,
1114
+ content,
1115
+ messageType: "text",
1116
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1117
+ status: "sent"
1118
+ };
1119
+ addMessage(optimisticMessage);
1120
+ websocket.sendMessage("sendMessage", {
1121
+ conversationId: config.conversationId,
1122
+ content,
1123
+ messageType: "text"
1124
+ });
1125
+ config.onMessageSent?.(optimisticMessage);
1126
+ },
1127
+ [websocket, config, addMessage]
1128
+ );
1129
+ const handleTyping = useCallback4(
1130
+ (isTyping2) => {
1131
+ if (!websocket.isConnected || config.enableTypingIndicator === false) {
1132
+ return;
1133
+ }
1134
+ websocket.sendMessage("typing", {
1135
+ conversationId: config.conversationId,
1136
+ isTyping: isTyping2
1137
+ });
1138
+ },
1139
+ [websocket, config]
1140
+ );
1141
+ useEffect6(() => {
1142
+ if (websocket.error) {
1143
+ config.toast?.error(websocket.error.message || "An error occurred");
1144
+ }
1145
+ }, [websocket.error, config]);
1146
+ if (!isFullPage) {
1147
+ if (isClosed) {
1148
+ return null;
1149
+ }
1150
+ if (isMinimized) {
1151
+ return /* @__PURE__ */ jsx5("div", { className: `fixed bottom-4 right-4 z-50 ${className}`, children: /* @__PURE__ */ jsxs5(
1152
+ "button",
1153
+ {
1154
+ type: "button",
1155
+ onClick: () => setIsMinimized(false),
1156
+ className: "h-14 w-14 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 text-white shadow-lg hover:shadow-xl transition-all flex items-center justify-center relative",
1157
+ "aria-label": "Open chat",
1158
+ children: [
1159
+ /* @__PURE__ */ jsx5("span", { className: "text-2xl", children: "\u{1F4AC}" }),
1160
+ messages.some(
1161
+ (msg) => msg.senderId !== config.currentUser.email && msg.status !== "read"
1162
+ ) && /* @__PURE__ */ jsx5("span", { className: "absolute -top-1 -right-1 h-5 w-5 rounded-full bg-red-500 text-white text-xs flex items-center justify-center", children: "!" })
1163
+ ]
1164
+ }
1165
+ ) });
1166
+ }
1167
+ }
1168
+ const containerClasses = isFullPage ? `flex flex-col bg-white dark:bg-gray-900 h-full ${className}` : `fixed bottom-4 right-4 z-50 flex flex-col bg-white dark:bg-gray-900 rounded-lg shadow-2xl overflow-hidden ${className}`;
1169
+ const containerStyle = isFullPage ? void 0 : {
1170
+ width: "400px",
1171
+ height: "600px",
1172
+ maxHeight: "calc(100vh - 2rem)"
1173
+ };
1174
+ return /* @__PURE__ */ jsxs5("div", { className: containerClasses, style: containerStyle, children: [
1175
+ !isFullPage && /* @__PURE__ */ jsx5(
1176
+ ChatHeader,
1177
+ {
1178
+ agent: config.currentUser.type === "customer" ? {
1179
+ email: "contact@xcelsior.co",
1180
+ name: "Support Agent",
1181
+ type: "agent",
1182
+ status: websocket.isConnected ? "online" : "offline"
1183
+ } : void 0,
1184
+ onMinimize: () => setIsMinimized(true),
1185
+ onClose: () => setIsClosed(true)
1186
+ }
1187
+ ),
1188
+ !websocket.isConnected && /* @__PURE__ */ jsx5("div", { className: "bg-yellow-50 dark:bg-yellow-900/30 border-b border-yellow-200 dark:border-yellow-800 px-4 py-2", children: /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [
1189
+ /* @__PURE__ */ jsx5("div", { className: "w-2 h-2 rounded-full bg-yellow-500 animate-pulse" }),
1190
+ /* @__PURE__ */ jsx5("span", { className: "text-sm text-yellow-800 dark:text-yellow-200", children: "Reconnecting..." }),
1191
+ /* @__PURE__ */ jsx5(
1192
+ "button",
1193
+ {
1194
+ type: "button",
1195
+ onClick: websocket.reconnect,
1196
+ className: "ml-auto text-xs text-yellow-700 dark:text-yellow-300 hover:underline",
1197
+ children: "Retry"
1198
+ }
1199
+ )
1200
+ ] }) }),
1201
+ isLoading ? /* @__PURE__ */ jsx5("div", { className: "flex-1 flex items-center justify-center", children: /* @__PURE__ */ jsxs5("div", { className: "text-center", children: [
1202
+ /* @__PURE__ */ jsx5("div", { className: "w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-2" }),
1203
+ /* @__PURE__ */ jsx5("p", { className: "text-sm text-gray-600 dark:text-gray-400", children: "Loading messages..." })
1204
+ ] }) }) : /* @__PURE__ */ jsx5(
1205
+ MessageList,
1206
+ {
1207
+ messages,
1208
+ currentUser: config.currentUser,
1209
+ isTyping,
1210
+ typingUser: typingUsers[0],
1211
+ autoScroll: true,
1212
+ onLoadMore: loadMore,
1213
+ hasMore,
1214
+ isLoadingMore
1215
+ }
1216
+ ),
1217
+ /* @__PURE__ */ jsx5(
1218
+ ChatInput,
1219
+ {
1220
+ onSend: handleSendMessage,
1221
+ onTyping: handleTyping,
1222
+ config,
1223
+ fileUpload,
1224
+ disabled: !websocket.isConnected
1225
+ }
1226
+ ),
1227
+ !isFullPage && /* @__PURE__ */ jsx5("div", { className: "bg-gray-50 dark:bg-gray-950 px-4 py-2 text-center border-t border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxs5("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: [
1228
+ "Powered by ",
1229
+ /* @__PURE__ */ jsx5("span", { className: "font-semibold", children: "Xcelsior Chat" })
1230
+ ] }) })
1231
+ ] });
1232
+ }
1233
+
1234
+ // src/components/Chat.tsx
1235
+ import { useCallback as useCallback5, useEffect as useEffect7, useState as useState8 } from "react";
1236
+
1237
+ // src/components/PreChatForm.tsx
1238
+ import { useState as useState7 } from "react";
1239
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1240
+ function PreChatForm({
1241
+ onSubmit,
1242
+ className = "",
1243
+ initialName = "",
1244
+ initialEmail = ""
1245
+ }) {
1246
+ const [name, setName] = useState7(initialName);
1247
+ const [email, setEmail] = useState7(initialEmail);
1248
+ const [errors, setErrors] = useState7({});
1249
+ const [isSubmitting, setIsSubmitting] = useState7(false);
1250
+ const validateForm = () => {
1251
+ const newErrors = {};
1252
+ if (!name.trim()) {
1253
+ newErrors.name = "Name is required";
1254
+ } else if (name.trim().length < 2) {
1255
+ newErrors.name = "Name must be at least 2 characters";
1256
+ }
1257
+ if (!email.trim()) {
1258
+ newErrors.email = "Email is required";
1259
+ } else {
1260
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1261
+ if (!emailRegex.test(email)) {
1262
+ newErrors.email = "Please enter a valid email address";
1263
+ }
1264
+ }
1265
+ setErrors(newErrors);
1266
+ return Object.keys(newErrors).length === 0;
1267
+ };
1268
+ const handleSubmit = async (e) => {
1269
+ e.preventDefault();
1270
+ if (!validateForm()) {
1271
+ return;
1272
+ }
1273
+ setIsSubmitting(true);
1274
+ try {
1275
+ onSubmit(name.trim(), email.trim());
1276
+ } catch (error) {
1277
+ console.error("Error submitting form:", error);
1278
+ } finally {
1279
+ setIsSubmitting(false);
1280
+ }
1281
+ };
1282
+ return /* @__PURE__ */ jsxs6(
1283
+ "div",
1284
+ {
1285
+ className: `fixed bottom-4 right-4 z-50 flex flex-col bg-white dark:bg-gray-900 rounded-lg shadow-2xl overflow-hidden ${className}`,
1286
+ style: {
1287
+ width: "400px",
1288
+ maxHeight: "calc(100vh - 2rem)"
1289
+ },
1290
+ children: [
1291
+ /* @__PURE__ */ jsxs6("div", { className: "bg-gradient-to-r from-blue-600 to-purple-600 text-white px-6 py-4", children: [
1292
+ /* @__PURE__ */ jsx6("h2", { className: "text-lg font-semibold", children: "Start a Conversation" }),
1293
+ /* @__PURE__ */ jsx6("p", { className: "text-sm text-blue-100 mt-1", children: "Please provide your details to continue" })
1294
+ ] }),
1295
+ /* @__PURE__ */ jsxs6("form", { onSubmit: handleSubmit, className: "p-6 space-y-5", children: [
1296
+ /* @__PURE__ */ jsxs6("div", { children: [
1297
+ /* @__PURE__ */ jsxs6(
1298
+ "label",
1299
+ {
1300
+ htmlFor: "chat-name",
1301
+ className: "block mb-2 text-sm font-medium text-gray-900 dark:text-gray-200",
1302
+ children: [
1303
+ "Name ",
1304
+ /* @__PURE__ */ jsx6("span", { className: "text-red-500", children: "*" })
1305
+ ]
1306
+ }
1307
+ ),
1308
+ /* @__PURE__ */ jsx6(
1309
+ "input",
1310
+ {
1311
+ type: "text",
1312
+ id: "chat-name",
1313
+ value: name,
1314
+ onChange: (e) => {
1315
+ setName(e.target.value);
1316
+ if (errors.name) {
1317
+ setErrors((prev) => ({ ...prev, name: void 0 }));
1318
+ }
1319
+ },
1320
+ className: `block w-full px-4 py-2.5 text-sm text-gray-900 bg-gray-50 rounded-lg border ${errors.name ? "border-red-500 focus:ring-red-500 focus:border-red-500" : "border-gray-300 focus:ring-blue-500 focus:border-blue-500"} dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500`,
1321
+ placeholder: "John Doe",
1322
+ disabled: isSubmitting,
1323
+ autoComplete: "name"
1324
+ }
1325
+ ),
1326
+ errors.name && /* @__PURE__ */ jsx6("p", { className: "mt-2 text-sm text-red-600 dark:text-red-500", role: "alert", children: errors.name })
1327
+ ] }),
1328
+ /* @__PURE__ */ jsxs6("div", { children: [
1329
+ /* @__PURE__ */ jsxs6(
1330
+ "label",
1331
+ {
1332
+ htmlFor: "chat-email",
1333
+ className: "block mb-2 text-sm font-medium text-gray-900 dark:text-gray-200",
1334
+ children: [
1335
+ "Email ",
1336
+ /* @__PURE__ */ jsx6("span", { className: "text-red-500", children: "*" })
1337
+ ]
1338
+ }
1339
+ ),
1340
+ /* @__PURE__ */ jsx6(
1341
+ "input",
1342
+ {
1343
+ type: "email",
1344
+ id: "chat-email",
1345
+ value: email,
1346
+ onChange: (e) => {
1347
+ setEmail(e.target.value);
1348
+ if (errors.email) {
1349
+ setErrors((prev) => ({ ...prev, email: void 0 }));
1350
+ }
1351
+ },
1352
+ className: `block w-full px-4 py-2.5 text-sm text-gray-900 bg-gray-50 rounded-lg border ${errors.email ? "border-red-500 focus:ring-red-500 focus:border-red-500" : "border-gray-300 focus:ring-blue-500 focus:border-blue-500"} dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500`,
1353
+ placeholder: "john@example.com",
1354
+ disabled: isSubmitting,
1355
+ autoComplete: "email"
1356
+ }
1357
+ ),
1358
+ errors.email && /* @__PURE__ */ jsx6("p", { className: "mt-2 text-sm text-red-600 dark:text-red-500", role: "alert", children: errors.email })
1359
+ ] }),
1360
+ /* @__PURE__ */ jsx6(
1361
+ "button",
1362
+ {
1363
+ type: "submit",
1364
+ disabled: isSubmitting,
1365
+ className: "w-full px-5 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg hover:from-blue-700 hover:to-purple-700 focus:outline-none focus:ring-4 focus:ring-blue-300 disabled:opacity-50 disabled:cursor-not-allowed transition-all",
1366
+ children: isSubmitting ? /* @__PURE__ */ jsxs6("span", { className: "flex items-center justify-center", children: [
1367
+ /* @__PURE__ */ jsxs6(
1368
+ "svg",
1369
+ {
1370
+ className: "animate-spin -ml-1 mr-3 h-5 w-5 text-white",
1371
+ xmlns: "http://www.w3.org/2000/svg",
1372
+ fill: "none",
1373
+ viewBox: "0 0 24 24",
1374
+ "aria-label": "Loading",
1375
+ children: [
1376
+ /* @__PURE__ */ jsx6("title", { children: "Loading" }),
1377
+ /* @__PURE__ */ jsx6(
1378
+ "circle",
1379
+ {
1380
+ className: "opacity-25",
1381
+ cx: "12",
1382
+ cy: "12",
1383
+ r: "10",
1384
+ stroke: "currentColor",
1385
+ strokeWidth: "4"
1386
+ }
1387
+ ),
1388
+ /* @__PURE__ */ jsx6(
1389
+ "path",
1390
+ {
1391
+ className: "opacity-75",
1392
+ fill: "currentColor",
1393
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
1394
+ }
1395
+ )
1396
+ ]
1397
+ }
1398
+ ),
1399
+ "Starting Chat..."
1400
+ ] }) : "Start Chat"
1401
+ }
1402
+ ),
1403
+ /* @__PURE__ */ jsx6("p", { className: "text-xs text-gray-500 dark:text-gray-400 text-center", children: "We respect your privacy. Your information will only be used to assist you." })
1404
+ ] }),
1405
+ /* @__PURE__ */ jsx6("div", { className: "bg-gray-50 dark:bg-gray-950 px-4 py-2 text-center border-t border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxs6("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: [
1406
+ "Powered by ",
1407
+ /* @__PURE__ */ jsx6("span", { className: "font-semibold", children: "Xcelsior Chat" })
1408
+ ] }) })
1409
+ ]
1410
+ }
1411
+ );
1412
+ }
1413
+
1414
+ // src/components/Chat.tsx
1415
+ import { jsx as jsx7 } from "react/jsx-runtime";
1416
+ function generateSessionId() {
1417
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
1418
+ return crypto.randomUUID();
1419
+ }
1420
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
1421
+ }
1422
+ function Chat({
1423
+ config,
1424
+ className = "",
1425
+ storageKeyPrefix = "xcelsior_chat",
1426
+ onPreChatSubmit
1427
+ }) {
1428
+ const [userInfo, setUserInfo] = useState8(null);
1429
+ const [conversationId, setConversationId] = useState8("");
1430
+ const [isLoading, setIsLoading] = useState8(true);
1431
+ useEffect7(() => {
1432
+ const initializeSession = () => {
1433
+ try {
1434
+ if (config.currentUser?.email && config.currentUser?.name) {
1435
+ const convId2 = config.conversationId || generateSessionId();
1436
+ const user = {
1437
+ name: config.currentUser.name,
1438
+ email: config.currentUser.email,
1439
+ avatar: config.currentUser.avatar,
1440
+ type: "customer",
1441
+ status: config.currentUser.status
1442
+ };
1443
+ setUserInfo(user);
1444
+ setConversationId(convId2);
1445
+ setIsLoading(false);
1446
+ return;
1447
+ }
1448
+ const storedDataJson = localStorage.getItem(`${storageKeyPrefix}_user`);
1449
+ if (storedDataJson) {
1450
+ const storedData = JSON.parse(storedDataJson);
1451
+ const isExpired = Date.now() - storedData.timestamp > 24 * 60 * 60 * 1e3;
1452
+ if (!isExpired && storedData.email && storedData.name) {
1453
+ const user = {
1454
+ name: storedData.name,
1455
+ email: storedData.email,
1456
+ type: "customer",
1457
+ status: "online"
1458
+ };
1459
+ setUserInfo(user);
1460
+ setConversationId(storedData.conversationId);
1461
+ setIsLoading(false);
1462
+ return;
1463
+ }
1464
+ }
1465
+ const convId = config.conversationId || generateSessionId();
1466
+ setConversationId(convId);
1467
+ if (config.currentUser?.email && config.currentUser?.name) {
1468
+ const user = {
1469
+ name: config.currentUser.name,
1470
+ email: config.currentUser.email,
1471
+ avatar: config.currentUser.avatar,
1472
+ type: "customer",
1473
+ status: "online"
1474
+ };
1475
+ setUserInfo(user);
1476
+ }
1477
+ } catch (error) {
1478
+ console.error("Error initializing chat session:", error);
1479
+ setConversationId(config.conversationId || generateSessionId());
1480
+ } finally {
1481
+ setIsLoading(false);
1482
+ }
1483
+ };
1484
+ initializeSession();
1485
+ }, [config, storageKeyPrefix]);
1486
+ const handlePreChatSubmit = useCallback5(
1487
+ (name, email) => {
1488
+ const convId = conversationId || generateSessionId();
1489
+ const user = {
1490
+ name,
1491
+ email,
1492
+ type: "customer",
1493
+ status: "online"
1494
+ };
1495
+ const storageData = {
1496
+ name,
1497
+ email,
1498
+ conversationId: convId,
1499
+ timestamp: Date.now()
1500
+ };
1501
+ try {
1502
+ localStorage.setItem(`${storageKeyPrefix}_user`, JSON.stringify(storageData));
1503
+ } catch (error) {
1504
+ console.error("Error storing user data:", error);
1505
+ }
1506
+ setUserInfo(user);
1507
+ setConversationId(convId);
1508
+ onPreChatSubmit?.(user);
1509
+ },
1510
+ [conversationId, storageKeyPrefix, onPreChatSubmit]
1511
+ );
1512
+ if (isLoading) {
1513
+ return null;
1514
+ }
1515
+ if (!userInfo || !userInfo.email || !userInfo.name) {
1516
+ return /* @__PURE__ */ jsx7(
1517
+ PreChatForm,
1518
+ {
1519
+ onSubmit: handlePreChatSubmit,
1520
+ className,
1521
+ initialName: config.currentUser?.name,
1522
+ initialEmail: config.currentUser?.email
1523
+ }
1524
+ );
1525
+ }
1526
+ const fullConfig = {
1527
+ ...config,
1528
+ conversationId,
1529
+ currentUser: userInfo
1530
+ };
1531
+ return /* @__PURE__ */ jsx7(ChatWidget, { config: fullConfig, className });
1532
+ }
1533
+
1534
+ // src/components/TypingIndicator.tsx
1535
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1536
+ function TypingIndicator({ isTyping, userName }) {
1537
+ if (!isTyping) {
1538
+ return null;
1539
+ }
1540
+ return /* @__PURE__ */ jsx8("div", { className: "px-4 py-2 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
1541
+ /* @__PURE__ */ jsxs7("div", { className: "flex gap-1", children: [
1542
+ /* @__PURE__ */ jsx8("span", { className: "w-2 h-2 bg-blue-500 rounded-full animate-bounce" }),
1543
+ /* @__PURE__ */ jsx8(
1544
+ "span",
1545
+ {
1546
+ className: "w-2 h-2 bg-blue-500 rounded-full animate-bounce",
1547
+ style: { animationDelay: "0.1s" }
1548
+ }
1549
+ ),
1550
+ /* @__PURE__ */ jsx8(
1551
+ "span",
1552
+ {
1553
+ className: "w-2 h-2 bg-blue-500 rounded-full animate-bounce",
1554
+ style: { animationDelay: "0.2s" }
1555
+ }
1556
+ )
1557
+ ] }),
1558
+ /* @__PURE__ */ jsx8("span", { className: "text-xs text-gray-600 dark:text-gray-400", children: userName ? `${userName} is typing...` : "Someone is typing..." })
1559
+ ] }) });
1560
+ }
1561
+ export {
1562
+ Chat,
1563
+ ChatHeader,
1564
+ ChatInput,
1565
+ ChatWidget,
1566
+ MessageItem,
1567
+ MessageList,
1568
+ PreChatForm,
1569
+ TypingIndicator,
1570
+ fetchMessages,
1571
+ useFileUpload,
1572
+ useMessages,
1573
+ useTypingIndicator,
1574
+ useWebSocket
1575
+ };
1576
+ //# sourceMappingURL=index.mjs.map