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