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