@xcelsior/ui-chat 1.0.4 → 1.0.6

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