@min98/chat-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1068 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/context/ChatContext.tsx
6
+ import { createContext, useContext, useState, useEffect, useRef, useCallback } from "react";
7
+
8
+ // src/core/api-client.ts
9
+ import axios from "axios";
10
+ var ChatApiClient = class {
11
+ constructor(apiBaseUrl, token) {
12
+ __publicField(this, "axiosInstance");
13
+ this.axiosInstance = axios.create({
14
+ baseURL: apiBaseUrl
15
+ });
16
+ this.axiosInstance.interceptors.request.use((config) => {
17
+ const activeToken = typeof token === "function" ? token() : token;
18
+ if (activeToken) {
19
+ config.headers.Authorization = `Bearer ${activeToken}`;
20
+ }
21
+ return config;
22
+ });
23
+ }
24
+ async getActiveSession(userId) {
25
+ const res = await this.axiosInstance.get("/api/chat/session", {
26
+ params: userId ? { userId } : {}
27
+ });
28
+ return res.data?.data;
29
+ }
30
+ async startSession(title, userId) {
31
+ const res = await this.axiosInstance.post("/api/chat/session", { title, userId });
32
+ return res.data?.data;
33
+ }
34
+ async closeSession(userId) {
35
+ const res = await this.axiosInstance.patch("/api/chat/session", { userId });
36
+ return res.data?.data;
37
+ }
38
+ async getMessages(userId, limit = 15, before) {
39
+ const res = await this.axiosInstance.get("/api/chat/messages", {
40
+ params: {
41
+ limit,
42
+ userId,
43
+ before
44
+ }
45
+ });
46
+ return res.data;
47
+ }
48
+ async sendMessage(content, userId, attachments) {
49
+ const res = await this.axiosInstance.post("/api/chat/messages", {
50
+ content,
51
+ userId,
52
+ attachments
53
+ });
54
+ return res.data?.data;
55
+ }
56
+ async markAsRead(userId) {
57
+ const res = await this.axiosInstance.post("/api/chat/read", { userId });
58
+ return res.data;
59
+ }
60
+ async uploadFile(file) {
61
+ const formData = new FormData();
62
+ formData.append("file", file);
63
+ const res = await this.axiosInstance.post("/api/chat/upload", formData, {
64
+ headers: {
65
+ "Content-Type": "multipart/form-data"
66
+ }
67
+ });
68
+ return res.data?.data;
69
+ }
70
+ async getRooms() {
71
+ const res = await this.axiosInstance.get("/api/chat/rooms");
72
+ return res.data?.data;
73
+ }
74
+ async getSseTicket() {
75
+ const res = await this.axiosInstance.post("/api/chat/sse-ticket");
76
+ return res.data?.data?.ticket;
77
+ }
78
+ };
79
+
80
+ // src/core/sse-client.ts
81
+ var ChatSseClient = class {
82
+ constructor(apiClient, apiBaseUrl, onMessageReceived, onStatusChange) {
83
+ __publicField(this, "apiClient");
84
+ __publicField(this, "apiBaseUrl");
85
+ __publicField(this, "eventSource", null);
86
+ __publicField(this, "reconnectTimeout", null);
87
+ __publicField(this, "onMessageReceived");
88
+ __publicField(this, "onStatusChange");
89
+ __publicField(this, "isConnected", false);
90
+ __publicField(this, "isDestroyed", false);
91
+ this.apiClient = apiClient;
92
+ this.apiBaseUrl = apiBaseUrl;
93
+ this.onMessageReceived = onMessageReceived;
94
+ this.onStatusChange = onStatusChange;
95
+ }
96
+ async connect() {
97
+ this.disconnect();
98
+ this.isDestroyed = false;
99
+ try {
100
+ const ticket = await this.apiClient.getSseTicket();
101
+ if (!ticket) {
102
+ throw new Error("Kh\xF4ng nh\u1EADn \u0111\u01B0\u1EE3c ticket k\u1EBFt n\u1ED1i realtime");
103
+ }
104
+ if (this.isDestroyed) return;
105
+ const streamUrl = `${this.apiBaseUrl.replace(/\/$/, "")}/api/v1/sse/stream?ticket=${ticket}`;
106
+ const eventSource = new EventSource(streamUrl);
107
+ this.eventSource = eventSource;
108
+ eventSource.onopen = () => {
109
+ if (this.isDestroyed) return;
110
+ this.isConnected = true;
111
+ this.onStatusChange(true, null);
112
+ };
113
+ eventSource.addEventListener("connected", () => {
114
+ if (this.isDestroyed) return;
115
+ this.isConnected = true;
116
+ this.onStatusChange(true, null);
117
+ });
118
+ eventSource.addEventListener("new_chat_message", (event) => {
119
+ if (this.isDestroyed) return;
120
+ try {
121
+ const parsed = JSON.parse(event.data);
122
+ this.onMessageReceived({
123
+ event: "new_chat_message",
124
+ data: parsed.data,
125
+ ts: parsed.ts
126
+ });
127
+ } catch (err) {
128
+ console.error("[sse-client] L\u1ED7i parse new_chat_message:", err);
129
+ }
130
+ });
131
+ eventSource.addEventListener("chat_session_updated", (event) => {
132
+ if (this.isDestroyed) return;
133
+ try {
134
+ const parsed = JSON.parse(event.data);
135
+ this.onMessageReceived({
136
+ event: "chat_session_updated",
137
+ data: parsed.data,
138
+ ts: parsed.ts
139
+ });
140
+ } catch (err) {
141
+ console.error("[sse-client] L\u1ED7i parse chat_session_updated:", err);
142
+ }
143
+ });
144
+ eventSource.addEventListener("__end_session__", () => {
145
+ if (this.isDestroyed) return;
146
+ this.disconnect();
147
+ this.onStatusChange(false, "Session ended by admin");
148
+ });
149
+ eventSource.onerror = (err) => {
150
+ if (this.isDestroyed) return;
151
+ console.warn("[sse-client] EventSource error, reconnecting...", err);
152
+ this.isConnected = false;
153
+ this.onStatusChange(false, "M\u1EA5t k\u1EBFt n\u1ED1i. \u0110ang k\u1EBFt n\u1ED1i l\u1EA1i...");
154
+ this.disconnect(true);
155
+ this.scheduleReconnect();
156
+ };
157
+ } catch (err) {
158
+ if (this.isDestroyed) return;
159
+ console.error("[sse-client] Connection failed:", err);
160
+ this.isConnected = false;
161
+ this.onStatusChange(false, err instanceof Error ? err.message : "L\u1ED7i k\u1EBFt n\u1ED1i");
162
+ this.scheduleReconnect();
163
+ }
164
+ }
165
+ scheduleReconnect() {
166
+ if (this.reconnectTimeout) {
167
+ clearTimeout(this.reconnectTimeout);
168
+ }
169
+ this.reconnectTimeout = setTimeout(() => {
170
+ if (!this.isDestroyed) {
171
+ void this.connect();
172
+ }
173
+ }, 5e3);
174
+ }
175
+ disconnect(keepReconnectStatus = false) {
176
+ if (this.eventSource) {
177
+ this.eventSource.close();
178
+ this.eventSource = null;
179
+ }
180
+ if (this.reconnectTimeout && !keepReconnectStatus) {
181
+ clearTimeout(this.reconnectTimeout);
182
+ this.reconnectTimeout = null;
183
+ }
184
+ if (!keepReconnectStatus) {
185
+ this.isConnected = false;
186
+ this.isDestroyed = true;
187
+ }
188
+ }
189
+ };
190
+
191
+ // src/context/ChatContext.tsx
192
+ import { jsx } from "react/jsx-runtime";
193
+ var ChatContext = createContext(void 0);
194
+ var ChatProvider = ({
195
+ apiBaseUrl,
196
+ token,
197
+ isAdmin = false,
198
+ children
199
+ }) => {
200
+ const [messages, setMessages] = useState([]);
201
+ const [activeSession, setActiveSession] = useState(null);
202
+ const [isConnected, setIsConnected] = useState(false);
203
+ const [sseError, setSseError] = useState(null);
204
+ const [isLoading, setIsLoading] = useState(false);
205
+ const [isLoadingSession, setIsLoadingSession] = useState(false);
206
+ const [isUploading, setIsUploading] = useState(false);
207
+ const [hasMore, setHasMore] = useState(false);
208
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
209
+ const [unreadCount, setUnreadCount] = useState(0);
210
+ const [rooms, setRooms] = useState([]);
211
+ const [selectedRoom, setSelectedRoomState] = useState(null);
212
+ const apiClientRef = useRef(null);
213
+ const sseClientRef = useRef(null);
214
+ if (!apiClientRef.current) {
215
+ apiClientRef.current = new ChatApiClient(apiBaseUrl, token);
216
+ }
217
+ const apiClient = apiClientRef.current;
218
+ const getUserIdFromToken = useCallback(() => {
219
+ try {
220
+ const activeToken = typeof token === "function" ? token() : token;
221
+ if (!activeToken) return void 0;
222
+ const payloadBase64 = activeToken.split(".")[1];
223
+ if (!payloadBase64) return void 0;
224
+ const decodedPayload = JSON.parse(atob(payloadBase64));
225
+ return decodedPayload.id || decodedPayload.sub;
226
+ } catch {
227
+ return void 0;
228
+ }
229
+ }, [token]);
230
+ const currentUserId = getUserIdFromToken();
231
+ const loadRooms = useCallback(async () => {
232
+ if (!isAdmin) return;
233
+ setIsLoading(true);
234
+ try {
235
+ const data = await apiClient.getRooms();
236
+ setRooms(data || []);
237
+ } catch (err) {
238
+ console.error("[ChatSDK] L\u1ED7i load rooms:", err);
239
+ } finally {
240
+ setIsLoading(false);
241
+ }
242
+ }, [apiClient, isAdmin]);
243
+ const setSelectedRoom = useCallback((room) => {
244
+ setSelectedRoomState(room);
245
+ }, []);
246
+ const loadMessages = useCallback(async (userId) => {
247
+ setIsLoading(true);
248
+ try {
249
+ const targetUid = isAdmin ? userId : void 0;
250
+ const res = await apiClient.getMessages(targetUid, 15);
251
+ const data = res?.data || [];
252
+ setMessages(data);
253
+ setHasMore(!!res?.hasMore);
254
+ if (!isAdmin && currentUserId) {
255
+ const unreads = data.filter((m) => m.senderId !== currentUserId && !m.isRead).length;
256
+ setUnreadCount(unreads);
257
+ }
258
+ } catch (err) {
259
+ console.error("[ChatSDK] L\u1ED7i load tin nh\u1EAFn:", err);
260
+ } finally {
261
+ setIsLoading(false);
262
+ }
263
+ }, [apiClient, isAdmin, currentUserId]);
264
+ const loadMoreMessages = useCallback(async (userId) => {
265
+ if (!hasMore || isLoadingMore || messages.length === 0) return;
266
+ setIsLoadingMore(true);
267
+ try {
268
+ const firstMsg = messages[0];
269
+ const targetUid = isAdmin ? userId : void 0;
270
+ const res = await apiClient.getMessages(targetUid, 15, firstMsg.createdAt);
271
+ const newMsgs = res?.data || [];
272
+ if (newMsgs.length > 0) {
273
+ setMessages((prev) => [...newMsgs, ...prev]);
274
+ setHasMore(!!res?.hasMore);
275
+ } else {
276
+ setHasMore(false);
277
+ }
278
+ } catch (err) {
279
+ console.error("[ChatSDK] L\u1ED7i load th\xEAm tin nh\u1EAFn:", err);
280
+ } finally {
281
+ setIsLoadingMore(false);
282
+ }
283
+ }, [apiClient, hasMore, isLoadingMore, messages, isAdmin]);
284
+ const loadActiveSession = useCallback(async (userId) => {
285
+ setIsLoadingSession(true);
286
+ try {
287
+ const targetUid = isAdmin ? userId : void 0;
288
+ const sessionData = await apiClient.getActiveSession(targetUid);
289
+ setActiveSession(sessionData || null);
290
+ } catch (err) {
291
+ console.error("[ChatSDK] L\u1ED7i load session active:", err);
292
+ } finally {
293
+ setIsLoadingSession(false);
294
+ }
295
+ }, [apiClient, isAdmin]);
296
+ const sendMessage = useCallback(async (content, attachments) => {
297
+ const targetUid = isAdmin ? selectedRoom?.userId : void 0;
298
+ try {
299
+ const newMsg = await apiClient.sendMessage(content, targetUid, attachments);
300
+ if (newMsg) {
301
+ setMessages((prev) => {
302
+ if (prev.some((m) => m._id === newMsg._id)) return prev;
303
+ return [...prev, newMsg];
304
+ });
305
+ }
306
+ } catch (err) {
307
+ console.error("[ChatSDK] G\u1EEDi tin nh\u1EAFn th\u1EA5t b\u1EA1i:", err);
308
+ throw err;
309
+ }
310
+ }, [apiClient, isAdmin, selectedRoom]);
311
+ const startSession = useCallback(async (title, userId) => {
312
+ setIsLoadingSession(true);
313
+ try {
314
+ const sess = await apiClient.startSession(title, userId);
315
+ if (sess) {
316
+ setActiveSession(sess);
317
+ if (isAdmin && selectedRoom && selectedRoom.userId === userId) {
318
+ setSelectedRoomState((prev) => prev ? { ...prev, activeSession: sess } : null);
319
+ }
320
+ }
321
+ } catch (err) {
322
+ console.error("[ChatSDK] B\u1EAFt \u0111\u1EA7u phi\xEAn th\u1EA5t b\u1EA1i:", err);
323
+ throw err;
324
+ } finally {
325
+ setIsLoadingSession(false);
326
+ }
327
+ }, [apiClient, isAdmin, selectedRoom]);
328
+ const closeSession = useCallback(async (userId) => {
329
+ setIsLoadingSession(true);
330
+ try {
331
+ await apiClient.closeSession(userId);
332
+ setActiveSession(null);
333
+ if (isAdmin && selectedRoom && selectedRoom.userId === userId) {
334
+ setSelectedRoomState((prev) => prev ? { ...prev, activeSession: null } : null);
335
+ }
336
+ } catch (err) {
337
+ console.error("[ChatSDK] \u0110\xF3ng phi\xEAn th\u1EA5t b\u1EA1i:", err);
338
+ throw err;
339
+ } finally {
340
+ setIsLoadingSession(false);
341
+ }
342
+ }, [apiClient, isAdmin, selectedRoom]);
343
+ const markAsRead = useCallback(async (userId) => {
344
+ try {
345
+ await apiClient.markAsRead(userId);
346
+ setUnreadCount(0);
347
+ setMessages(
348
+ (prev) => prev.map((m) => {
349
+ if (isAdmin) {
350
+ return m.senderId === userId ? { ...m, isRead: true } : m;
351
+ } else {
352
+ return m.senderId !== currentUserId ? { ...m, isRead: true } : m;
353
+ }
354
+ })
355
+ );
356
+ } catch (err) {
357
+ console.error("[ChatSDK] L\u1ED7i \u0111\xE1nh d\u1EA5u \u0111\u1ECDc tin nh\u1EAFn:", err);
358
+ }
359
+ }, [apiClient, isAdmin, currentUserId]);
360
+ const uploadFile = useCallback(async (file) => {
361
+ setIsUploading(true);
362
+ try {
363
+ const data = await apiClient.uploadFile(file);
364
+ return data;
365
+ } catch (err) {
366
+ console.error("[ChatSDK] Upload file th\u1EA5t b\u1EA1i:", err);
367
+ throw err;
368
+ } finally {
369
+ setIsUploading(false);
370
+ }
371
+ }, [apiClient]);
372
+ const handleRealtimeEvent = useCallback((event) => {
373
+ if (event.event === "new_chat_message") {
374
+ const newMsg = event.data;
375
+ if (isAdmin) {
376
+ if (selectedRoom && newMsg.userId === selectedRoom.userId) {
377
+ setMessages((prev) => {
378
+ if (prev.some((m) => m._id === newMsg._id)) return prev;
379
+ return [...prev, newMsg];
380
+ });
381
+ apiClient.markAsRead(selectedRoom.userId).catch(() => void 0);
382
+ }
383
+ setRooms((prev) => {
384
+ const idx = prev.findIndex((r) => r.userId === newMsg.userId);
385
+ if (idx !== -1) {
386
+ const updated = [...prev];
387
+ const oldRoom = updated[idx];
388
+ updated[idx] = {
389
+ ...oldRoom,
390
+ lastMessage: {
391
+ content: newMsg.content,
392
+ attachments: newMsg.attachments || [],
393
+ senderId: newMsg.senderId,
394
+ createdAt: newMsg.createdAt
395
+ },
396
+ unreadCount: (!selectedRoom || selectedRoom.userId !== newMsg.userId) && newMsg.senderId === newMsg.userId ? oldRoom.unreadCount + 1 : oldRoom.unreadCount
397
+ };
398
+ return updated.sort((a, b) => new Date(b.lastMessage.createdAt).getTime() - new Date(a.lastMessage.createdAt).getTime());
399
+ } else {
400
+ void loadRooms();
401
+ return prev;
402
+ }
403
+ });
404
+ } else {
405
+ setMessages((prev) => {
406
+ if (prev.some((m) => m._id === newMsg._id)) return prev;
407
+ return [...prev, newMsg];
408
+ });
409
+ if (newMsg.senderId !== currentUserId) {
410
+ setUnreadCount((c) => c + 1);
411
+ }
412
+ }
413
+ } else if (event.event === "chat_session_updated") {
414
+ const updatedSess = event.data;
415
+ if (isAdmin) {
416
+ if (updatedSess && updatedSess.userId) {
417
+ const targetUid = updatedSess.userId.toString();
418
+ const isActive = updatedSess.status === "active";
419
+ setRooms(
420
+ (prev) => prev.map((r) => r.userId === targetUid ? { ...r, activeSession: isActive ? updatedSess : null } : r)
421
+ );
422
+ if (selectedRoom && selectedRoom.userId === targetUid) {
423
+ setSelectedRoomState((prev) => prev ? { ...prev, activeSession: isActive ? updatedSess : null } : null);
424
+ setActiveSession(isActive ? updatedSess : null);
425
+ }
426
+ }
427
+ } else {
428
+ setActiveSession(updatedSess || null);
429
+ }
430
+ }
431
+ }, [isAdmin, selectedRoom, currentUserId, apiClient, loadRooms]);
432
+ useEffect(() => {
433
+ if (!currentUserId) return;
434
+ const sseClient = new ChatSseClient(
435
+ apiClient,
436
+ apiBaseUrl,
437
+ handleRealtimeEvent,
438
+ (connected, error) => {
439
+ setIsConnected(connected);
440
+ setSseError(error);
441
+ }
442
+ );
443
+ sseClientRef.current = sseClient;
444
+ void sseClient.connect();
445
+ return () => {
446
+ sseClient.disconnect();
447
+ sseClientRef.current = null;
448
+ };
449
+ }, [apiBaseUrl, apiClient, currentUserId, handleRealtimeEvent]);
450
+ useEffect(() => {
451
+ if (!isAdmin && currentUserId) {
452
+ void loadActiveSession();
453
+ void loadMessages();
454
+ }
455
+ }, [isAdmin, currentUserId, loadActiveSession, loadMessages]);
456
+ useEffect(() => {
457
+ if (isAdmin) {
458
+ void loadRooms();
459
+ }
460
+ }, [isAdmin, loadRooms]);
461
+ useEffect(() => {
462
+ if (isAdmin && selectedRoom) {
463
+ void loadMessages(selectedRoom.userId);
464
+ void loadActiveSession(selectedRoom.userId);
465
+ void markAsRead(selectedRoom.userId);
466
+ }
467
+ }, [isAdmin, selectedRoom?.userId]);
468
+ return /* @__PURE__ */ jsx(
469
+ ChatContext.Provider,
470
+ {
471
+ value: {
472
+ apiClient,
473
+ messages,
474
+ activeSession,
475
+ isConnected,
476
+ sseError,
477
+ isLoading,
478
+ isLoadingSession,
479
+ isUploading,
480
+ hasMore,
481
+ isLoadingMore,
482
+ unreadCount,
483
+ rooms,
484
+ selectedRoom,
485
+ setSelectedRoom,
486
+ sendMessage,
487
+ startSession,
488
+ closeSession,
489
+ markAsRead,
490
+ loadMessages,
491
+ loadMoreMessages,
492
+ uploadFile,
493
+ loadRooms
494
+ },
495
+ children
496
+ }
497
+ );
498
+ };
499
+ var useChatContext = () => {
500
+ const context = useContext(ChatContext);
501
+ if (!context) {
502
+ throw new Error("useChatContext must be used within a ChatProvider");
503
+ }
504
+ return context;
505
+ };
506
+
507
+ // src/hooks/useChat.ts
508
+ function useChat() {
509
+ const {
510
+ messages,
511
+ activeSession,
512
+ isConnected,
513
+ sseError,
514
+ isLoading,
515
+ isLoadingSession,
516
+ isUploading,
517
+ hasMore,
518
+ isLoadingMore,
519
+ unreadCount,
520
+ sendMessage,
521
+ startSession,
522
+ closeSession,
523
+ markAsRead,
524
+ loadMessages,
525
+ loadMoreMessages,
526
+ uploadFile
527
+ } = useChatContext();
528
+ return {
529
+ messages,
530
+ activeSession,
531
+ isConnected,
532
+ sseError,
533
+ isLoading,
534
+ isLoadingSession,
535
+ isUploading,
536
+ hasMore,
537
+ isLoadingMore,
538
+ unreadCount,
539
+ sendMessage,
540
+ startSession: (title) => startSession(title),
541
+ closeSession: () => closeSession(),
542
+ markAsRead: () => markAsRead(),
543
+ loadMessages: () => loadMessages(),
544
+ loadMoreMessages: () => loadMoreMessages(),
545
+ uploadFile
546
+ };
547
+ }
548
+
549
+ // src/hooks/useChatAdmin.ts
550
+ function useChatAdmin() {
551
+ const {
552
+ messages,
553
+ isConnected,
554
+ sseError,
555
+ isLoading,
556
+ isLoadingSession,
557
+ isUploading,
558
+ hasMore,
559
+ isLoadingMore,
560
+ rooms,
561
+ selectedRoom,
562
+ setSelectedRoom,
563
+ sendMessage,
564
+ startSession,
565
+ closeSession,
566
+ markAsRead,
567
+ loadMessages,
568
+ loadMoreMessages,
569
+ uploadFile,
570
+ loadRooms
571
+ } = useChatContext();
572
+ return {
573
+ messages,
574
+ isConnected,
575
+ sseError,
576
+ isLoading,
577
+ isLoadingSession,
578
+ isUploading,
579
+ hasMore,
580
+ isLoadingMore,
581
+ rooms,
582
+ selectedRoom,
583
+ setSelectedRoom,
584
+ sendMessage,
585
+ startSession: (title) => {
586
+ if (selectedRoom) {
587
+ return startSession(title, selectedRoom.userId);
588
+ }
589
+ return Promise.reject(new Error("No selected room"));
590
+ },
591
+ closeSession: () => {
592
+ if (selectedRoom) {
593
+ return closeSession(selectedRoom.userId);
594
+ }
595
+ return Promise.reject(new Error("No selected room"));
596
+ },
597
+ markAsRead: () => {
598
+ if (selectedRoom) {
599
+ return markAsRead(selectedRoom.userId);
600
+ }
601
+ return Promise.resolve();
602
+ },
603
+ loadMessages: () => {
604
+ if (selectedRoom) {
605
+ return loadMessages(selectedRoom.userId);
606
+ }
607
+ return Promise.resolve();
608
+ },
609
+ loadMoreMessages: () => {
610
+ if (selectedRoom) {
611
+ return loadMoreMessages(selectedRoom.userId);
612
+ }
613
+ return Promise.resolve();
614
+ },
615
+ uploadFile,
616
+ loadRooms
617
+ };
618
+ }
619
+
620
+ // src/components/ChatWindow.tsx
621
+ import { useState as useState2, useRef as useRef2, useEffect as useEffect2 } from "react";
622
+ import {
623
+ Send,
624
+ Paperclip,
625
+ Loader2,
626
+ FileIcon,
627
+ MessageSquare,
628
+ Sparkles,
629
+ Clock,
630
+ RefreshCw,
631
+ X,
632
+ ChevronRight,
633
+ LogOut,
634
+ HelpCircle
635
+ } from "lucide-react";
636
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
637
+ var ChatWindow = ({
638
+ isAdminMode = false,
639
+ onClose,
640
+ title = "H\u1ED7 tr\u1EE3 tr\u1EF1c tuy\u1EBFn",
641
+ quickQuestions = [
642
+ "L\xE0m th\u1EBF n\xE0o \u0111\u1EC3 c\xE0i \u0111\u1EB7t v\xE0 k\xEDch ho\u1EA1t Extension?",
643
+ "T\xF4i mu\u1ED1n h\u1ECFi v\u1EC1 c\xE1ch c\u1EA5u h\xECnh workflow t\u1EF1 \u0111\u1ED9ng",
644
+ "Giao d\u1ECBch n\u1EA1p ti\u1EC1n c\u1EE7a t\xF4i g\u1EB7p l\u1ED7i, c\u1EA7n \u0111\u1ED1i so\xE1t",
645
+ "H\u1EA1n ng\u1EA1ch s\u1EED d\u1EE5ng h\xE0ng ng\xE0y c\u1EE7a t\xF4i b\u1ECB h\u1EBFt, l\xE0m th\u1EBF n\xE0o \u0111\u1EC3 gia h\u1EA1n?"
646
+ ],
647
+ cannedResponses = [
648
+ "Ch\xE0o b\u1EA1n, Admin c\xF3 th\u1EC3 h\u1ED7 tr\u1EE3 g\xEC cho b\u1EA1n \u1EA1?",
649
+ "C\u1EA3m \u01A1n b\u1EA1n \u0111\xE3 ph\u1EA3n h\u1ED3i, \u0111\u1ED9i ng\u0169 k\u1EF9 thu\u1EADt \u0111ang ki\u1EC3m tra l\u1ED7i n\xE0y v\xE0 s\u1EBD b\xE1o l\u1EA1i b\u1EA1n s\u1EDBm nh\u1EA5t.",
650
+ "B\u1EA1n vui l\xF2ng ch\u1EE5p \u1EA3nh m\xE0n h\xECnh l\u1ED7i \u0111\u1EC3 Admin ki\u1EC3m tra nhanh h\u01A1n nh\xE9.",
651
+ "Giao d\u1ECBch n\u1EA1p ti\u1EC1n c\u1EE7a b\u1EA1n \u0111\xE3 \u0111\u01B0\u1EE3c \u0111\u1ED1i so\xE1t th\xE0nh c\xF4ng, vui l\xF2ng ki\u1EC3m tra l\u1EA1i s\u1ED1 d\u01B0 v\xED."
652
+ ]
653
+ }) => {
654
+ const userChat = useChat();
655
+ const adminChat = useChatAdmin();
656
+ const chat = isAdminMode ? adminChat : userChat;
657
+ const [inputText, setInputText] = useState2("");
658
+ const [showConfirmModal, setShowConfirmModal] = useState2(false);
659
+ const [showCannedList, setShowCannedList] = useState2(false);
660
+ const [attachment, setAttachment] = useState2(null);
661
+ const messagesEndRef = useRef2(null);
662
+ const scrollContainerRef = useRef2(null);
663
+ const fileInputRef = useRef2(null);
664
+ const oldScrollHeightRef = useRef2(null);
665
+ const lastMessageIdRef = useRef2(null);
666
+ const isInitialLoad = useRef2(true);
667
+ const messages = chat.messages;
668
+ const isConnected = chat.isConnected;
669
+ const isLoading = chat.isLoading;
670
+ const isUploading = chat.isUploading;
671
+ const isLoadingMore = chat.isLoadingMore;
672
+ const activeSession = isAdminMode ? adminChat.selectedRoom?.activeSession : userChat.activeSession;
673
+ const scrollToBottom = (behavior = "smooth") => {
674
+ setTimeout(() => {
675
+ if (scrollContainerRef.current) {
676
+ const { scrollHeight, clientHeight } = scrollContainerRef.current;
677
+ scrollContainerRef.current.scrollTo({
678
+ top: scrollHeight - clientHeight,
679
+ behavior
680
+ });
681
+ }
682
+ }, 60);
683
+ };
684
+ useEffect2(() => {
685
+ if (messages.length > 0 && !isLoading) {
686
+ const lastMsg = messages[messages.length - 1];
687
+ const hasNewLastMessage = lastMessageIdRef.current !== lastMsg._id;
688
+ lastMessageIdRef.current = lastMsg._id;
689
+ if (isInitialLoad.current) {
690
+ scrollToBottom("auto");
691
+ isInitialLoad.current = false;
692
+ } else if (hasNewLastMessage) {
693
+ const container = scrollContainerRef.current;
694
+ if (container) {
695
+ const isNearBottom = container.scrollHeight - container.clientHeight - container.scrollTop <= 150;
696
+ if (isNearBottom || lastMsg.senderId === lastMsg.userId) {
697
+ scrollToBottom("smooth");
698
+ }
699
+ }
700
+ }
701
+ }
702
+ }, [messages, isLoading]);
703
+ const handleScroll = (e) => {
704
+ if (e.currentTarget.scrollTop === 0) {
705
+ const container = e.currentTarget;
706
+ oldScrollHeightRef.current = container.scrollHeight;
707
+ void chat.loadMoreMessages().then(() => {
708
+ if (oldScrollHeightRef.current !== null && scrollContainerRef.current) {
709
+ scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight - oldScrollHeightRef.current;
710
+ oldScrollHeightRef.current = null;
711
+ }
712
+ });
713
+ }
714
+ };
715
+ const handleStartSession = async (sessionTitle = "H\u1ED7 tr\u1EE3 chung", firstMessage) => {
716
+ try {
717
+ await chat.startSession(sessionTitle);
718
+ if (firstMessage) {
719
+ await chat.sendMessage(firstMessage);
720
+ }
721
+ scrollToBottom("smooth");
722
+ } catch (err) {
723
+ console.error("Kh\xF4ng th\u1EC3 b\u1EAFt \u0111\u1EA7u phi\xEAn:", err);
724
+ }
725
+ };
726
+ const handleSend = async (e) => {
727
+ if (e) e.preventDefault();
728
+ if (!inputText.trim() && !attachment) return;
729
+ const text = inputText;
730
+ const attToSend = attachment ? [attachment] : void 0;
731
+ setInputText("");
732
+ setAttachment(null);
733
+ setShowCannedList(false);
734
+ try {
735
+ await chat.sendMessage(text, attToSend);
736
+ scrollToBottom("smooth");
737
+ } catch (err) {
738
+ console.error("L\u1ED7i g\u1EEDi tin nh\u1EAFn:", err);
739
+ }
740
+ };
741
+ const handleFileUpload = async (file) => {
742
+ try {
743
+ const data = await chat.uploadFile(file);
744
+ if (data) {
745
+ setAttachment({
746
+ type: data.type,
747
+ url: data.url,
748
+ name: data.name
749
+ });
750
+ }
751
+ } catch (err) {
752
+ console.error("Upload th\u1EA5t b\u1EA1i:", err);
753
+ }
754
+ };
755
+ const handleFileChange = (e) => {
756
+ const file = e.target.files?.[0];
757
+ if (file) void handleFileUpload(file);
758
+ };
759
+ const handlePaste = (e) => {
760
+ const items = e.clipboardData.items;
761
+ for (let i = 0; i < items.length; i++) {
762
+ if (items[i].type.indexOf("image") !== -1) {
763
+ const file = items[i].getAsFile();
764
+ if (file) {
765
+ e.preventDefault();
766
+ void handleFileUpload(file);
767
+ break;
768
+ }
769
+ }
770
+ }
771
+ };
772
+ const handleDragOver = (e) => {
773
+ e.preventDefault();
774
+ };
775
+ const handleDrop = (e) => {
776
+ e.preventDefault();
777
+ const file = e.dataTransfer.files?.[0];
778
+ if (file) {
779
+ void handleFileUpload(file);
780
+ }
781
+ };
782
+ return /* @__PURE__ */ jsxs(
783
+ "div",
784
+ {
785
+ onDragOver: handleDragOver,
786
+ onDrop: handleDrop,
787
+ className: "flex h-full w-full flex-col bg-white dark:bg-slate-900 overflow-hidden relative border border-slate-200 dark:border-slate-800 rounded-2xl shadow-lg",
788
+ children: [
789
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-slate-100 dark:border-slate-800/60 px-6 py-4 bg-slate-50/50 dark:bg-slate-950/20", children: [
790
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
791
+ /* @__PURE__ */ jsx2("div", { className: "relative flex h-10 w-10 items-center justify-center rounded-xl bg-teal-500/10 text-teal-600 dark:text-teal-400", children: /* @__PURE__ */ jsx2(MessageSquare, { className: "h-5 w-5" }) }),
792
+ /* @__PURE__ */ jsxs("div", { children: [
793
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
794
+ /* @__PURE__ */ jsx2("h2", { className: "text-sm font-bold text-slate-800 dark:text-slate-100", children: title }),
795
+ activeSession && /* @__PURE__ */ jsx2("span", { className: "px-2 py-0.5 text-[10px] font-semibold rounded-full bg-teal-500/15 text-teal-600 dark:text-teal-400 max-w-[150px] truncate", children: activeSession.title })
796
+ ] }),
797
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 mt-0.5", children: [
798
+ /* @__PURE__ */ jsx2("span", { className: `h-2 w-2 rounded-full ${isConnected ? "bg-emerald-500 animate-pulse" : "bg-amber-500 animate-pulse"}` }),
799
+ /* @__PURE__ */ jsx2("span", { className: "text-[10px] text-slate-500", children: isConnected ? "\u0110\xE3 k\u1EBFt n\u1ED1i" : "\u0110ang k\u1EBFt n\u1ED1i l\u1EA1i..." })
800
+ ] })
801
+ ] })
802
+ ] }),
803
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
804
+ activeSession && /* @__PURE__ */ jsxs(
805
+ "button",
806
+ {
807
+ type: "button",
808
+ onClick: () => setShowConfirmModal(true),
809
+ className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold rounded-xl text-red-600 hover:bg-red-500/10 border border-red-500/20 transition",
810
+ children: [
811
+ /* @__PURE__ */ jsx2(LogOut, { className: "h-3.5 w-3.5" }),
812
+ "\u0110\xF3ng phi\xEAn"
813
+ ]
814
+ }
815
+ ),
816
+ /* @__PURE__ */ jsx2(
817
+ "button",
818
+ {
819
+ type: "button",
820
+ onClick: () => chat.loadMessages(),
821
+ className: "rounded-lg p-2 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800",
822
+ children: /* @__PURE__ */ jsx2(RefreshCw, { className: "h-4 w-4" })
823
+ }
824
+ ),
825
+ onClose && /* @__PURE__ */ jsx2(
826
+ "button",
827
+ {
828
+ type: "button",
829
+ onClick: onClose,
830
+ className: "rounded-lg p-2 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800",
831
+ children: /* @__PURE__ */ jsx2(X, { className: "h-4 w-4" })
832
+ }
833
+ )
834
+ ] })
835
+ ] }),
836
+ /* @__PURE__ */ jsx2(
837
+ "div",
838
+ {
839
+ ref: scrollContainerRef,
840
+ onScroll: handleScroll,
841
+ className: "flex-1 overflow-y-auto p-6 space-y-4",
842
+ children: isLoading ? /* @__PURE__ */ jsx2("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx2(Loader2, { className: "h-8 w-8 animate-spin text-teal-500" }) }) : !activeSession ? (
843
+ // Màn hình chào khi chưa bắt đầu phiên
844
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center min-h-full py-8 text-center px-4 space-y-6", children: [
845
+ /* @__PURE__ */ jsx2("div", { className: "p-4 bg-teal-500/10 text-teal-500 rounded-3xl animate-bounce", children: /* @__PURE__ */ jsx2(Sparkles, { className: "h-10 w-10" }) }),
846
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2 max-w-md", children: [
847
+ /* @__PURE__ */ jsx2("h3", { className: "text-sm font-bold text-slate-800 dark:text-slate-200", children: "Ch\xE0o m\u1EEBng b\u1EA1n \u0111\u1EBFn v\u1EDBi h\u1ED7 tr\u1EE3 tr\u1EF1c tuy\u1EBFn" }),
848
+ /* @__PURE__ */ jsx2("p", { className: "text-xs text-slate-500 leading-normal", children: "Vui l\xF2ng b\u1EA5m b\u1EAFt \u0111\u1EA7u phi\xEAn h\u1ED7 tr\u1EE3 \u0111\u1EC3 chat tr\u1EF1c ti\u1EBFp v\u1EDBi ban qu\u1EA3n tr\u1ECB vi\xEAn ho\u1EB7c ch\u1ECDn c\xE2u h\u1ECFi nhanh th\u01B0\u1EDDng g\u1EB7p \u1EDF d\u01B0\u1EDBi." })
849
+ ] }),
850
+ /* @__PURE__ */ jsxs(
851
+ "button",
852
+ {
853
+ type: "button",
854
+ onClick: () => handleStartSession("H\u1ED7 tr\u1EE3 chung"),
855
+ className: "flex items-center gap-2 px-5 py-2.5 text-xs font-bold text-white bg-teal-500 hover:bg-teal-600 rounded-xl transition",
856
+ children: [
857
+ /* @__PURE__ */ jsx2(MessageSquare, { className: "h-4 w-4" }),
858
+ "B\u1EAFt \u0111\u1EA7u phi\xEAn h\u1ED7 tr\u1EE3 m\u1EDBi"
859
+ ]
860
+ }
861
+ ),
862
+ !isAdminMode && quickQuestions.length > 0 && /* @__PURE__ */ jsxs("div", { className: "w-full max-w-xl space-y-2 pt-4 border-t border-slate-100 dark:border-slate-800/60 text-left", children: [
863
+ /* @__PURE__ */ jsxs("h4", { className: "text-[10px] font-bold text-slate-600 dark:text-slate-400 flex items-center gap-1", children: [
864
+ /* @__PURE__ */ jsx2(HelpCircle, { className: "h-3.5 w-3.5 text-teal-500" }),
865
+ "B\u1EA1n c\u1EA7n h\u1ED7 tr\u1EE3 v\u1EC1 ch\u1EE7 \u0111\u1EC1 n\xE0o?"
866
+ ] }),
867
+ /* @__PURE__ */ jsx2("div", { className: "flex flex-col gap-2", children: quickQuestions.map((q, idx) => /* @__PURE__ */ jsxs(
868
+ "button",
869
+ {
870
+ type: "button",
871
+ onClick: () => handleStartSession(q, q),
872
+ className: "flex items-center justify-between p-3 text-xs text-slate-700 dark:text-slate-300 bg-slate-50 hover:bg-slate-100/80 border border-slate-150 rounded-xl transition text-left",
873
+ children: [
874
+ /* @__PURE__ */ jsx2("span", { className: "line-clamp-2 leading-relaxed", children: q }),
875
+ /* @__PURE__ */ jsx2(ChevronRight, { className: "h-4 w-4 text-slate-400" })
876
+ ]
877
+ },
878
+ idx
879
+ )) })
880
+ ] })
881
+ ] })
882
+ ) : messages.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center text-center p-6", children: [
883
+ /* @__PURE__ */ jsx2(MessageSquare, { className: "h-12 w-12 text-slate-300 mb-3 animate-pulse" }),
884
+ /* @__PURE__ */ jsx2("h3", { className: "text-sm font-bold text-slate-800 dark:text-slate-200", children: "Phi\xEAn h\u1ED7 tr\u1EE3 \u0111\xE3 m\u1EDF" }),
885
+ /* @__PURE__ */ jsx2("p", { className: "text-xs text-slate-500 max-w-xs mt-1", children: "H\xE3y g\u1EEDi c\xE2u h\u1ECFi c\u1EE7a b\u1EA1n t\u1EA1i \u0111\xE2y \u0111\u1EC3 \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3." })
886
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
887
+ isLoadingMore && /* @__PURE__ */ jsx2("div", { className: "flex justify-center py-2", children: /* @__PURE__ */ jsx2(Loader2, { className: "h-4 w-4 animate-spin text-teal-500" }) }),
888
+ messages.map((msg) => {
889
+ const isMe = isAdminMode ? msg.senderId !== msg.userId : msg.senderId === msg.userId;
890
+ return /* @__PURE__ */ jsxs("div", { className: `flex flex-col ${isMe ? "items-end" : "items-start"}`, children: [
891
+ /* @__PURE__ */ jsxs(
892
+ "div",
893
+ {
894
+ className: `max-w-[75%] rounded-2xl px-4 py-2.5 text-xs shadow-sm ${isMe ? "bg-teal-500 text-white rounded-br-none" : "bg-slate-100 dark:bg-slate-850 text-slate-800 dark:text-slate-200 rounded-bl-none"}`,
895
+ children: [
896
+ msg.attachments && msg.attachments.map((att, idx) => /* @__PURE__ */ jsx2("div", { className: "mb-2 max-w-full", children: att.type === "image" ? /* @__PURE__ */ jsx2(
897
+ "img",
898
+ {
899
+ src: att.url,
900
+ alt: att.name,
901
+ className: "max-h-48 rounded-lg cursor-zoom-in object-contain max-w-full",
902
+ onClick: () => window.open(att.url, "_blank")
903
+ }
904
+ ) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-black/10 px-3 py-2", children: [
905
+ /* @__PURE__ */ jsx2(FileIcon, { className: "h-4 w-4 text-sky-500" }),
906
+ /* @__PURE__ */ jsx2("a", { href: att.url, target: "_blank", rel: "noopener noreferrer", className: "underline line-clamp-1", children: att.name })
907
+ ] }) }, idx)),
908
+ msg.content && /* @__PURE__ */ jsx2("p", { className: "whitespace-pre-wrap break-words", children: msg.content })
909
+ ]
910
+ }
911
+ ),
912
+ /* @__PURE__ */ jsx2("span", { className: "text-[9px] text-slate-400 mt-1 px-1", children: new Date(msg.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) })
913
+ ] }, msg._id);
914
+ }),
915
+ /* @__PURE__ */ jsx2("div", { ref: messagesEndRef })
916
+ ] })
917
+ }
918
+ ),
919
+ attachment && /* @__PURE__ */ jsxs("div", { className: "relative mx-6 mb-3 p-3 bg-slate-50 dark:bg-slate-950/40 border border-slate-100 dark:border-slate-800 rounded-xl flex items-center justify-between", children: [
920
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
921
+ attachment.type === "image" ? /* @__PURE__ */ jsx2("img", { src: attachment.url, alt: "Preview", className: "h-12 w-12 object-cover rounded-lg border" }) : /* @__PURE__ */ jsx2("div", { className: "h-12 w-12 flex items-center justify-center rounded-lg bg-slate-100", children: /* @__PURE__ */ jsx2(FileIcon, { className: "h-6 w-6 text-sky-500" }) }),
922
+ /* @__PURE__ */ jsx2("span", { className: "text-xs font-semibold text-slate-800 dark:text-slate-200 line-clamp-1 max-w-[200px]", children: attachment.name })
923
+ ] }),
924
+ /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => setAttachment(null), className: "p-1 hover:text-red-500", children: /* @__PURE__ */ jsx2(X, { className: "h-4 w-4" }) })
925
+ ] }),
926
+ /* @__PURE__ */ jsx2("form", { onSubmit: handleSend, className: "border-t border-slate-100 dark:border-slate-850 p-4 bg-slate-50/20 dark:bg-slate-950/20", children: /* @__PURE__ */ jsxs("div", { className: "relative flex items-center gap-3", children: [
927
+ /* @__PURE__ */ jsx2(
928
+ "button",
929
+ {
930
+ type: "button",
931
+ onClick: () => fileInputRef.current?.click(),
932
+ disabled: isUploading || !activeSession,
933
+ className: "rounded-xl p-2.5 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-50",
934
+ children: isUploading ? /* @__PURE__ */ jsx2(Loader2, { className: "h-5 w-5 animate-spin" }) : /* @__PURE__ */ jsx2(Paperclip, { className: "h-5 w-5" })
935
+ }
936
+ ),
937
+ /* @__PURE__ */ jsx2("input", { type: "file", ref: fileInputRef, onChange: handleFileChange, accept: "image/*,audio/*", className: "hidden" }),
938
+ isAdminMode && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
939
+ /* @__PURE__ */ jsx2(
940
+ "button",
941
+ {
942
+ type: "button",
943
+ onClick: () => setShowCannedList(!showCannedList),
944
+ className: "rounded-xl p-2.5 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800",
945
+ children: /* @__PURE__ */ jsx2(Clock, { className: "h-5 w-5" })
946
+ }
947
+ ),
948
+ showCannedList && /* @__PURE__ */ jsx2("div", { className: "absolute bottom-14 left-0 z-50 w-72 max-h-48 overflow-y-auto rounded-xl border border-slate-200 bg-white p-2 shadow-xl dark:border-slate-800 dark:bg-slate-950", children: cannedResponses.map((resp, idx) => /* @__PURE__ */ jsx2(
949
+ "button",
950
+ {
951
+ type: "button",
952
+ onClick: () => {
953
+ setInputText((prev) => prev ? prev + " " + resp : resp);
954
+ setShowCannedList(false);
955
+ },
956
+ className: "w-full text-left text-xs p-2.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 truncate",
957
+ title: resp,
958
+ children: resp
959
+ },
960
+ idx
961
+ )) })
962
+ ] }),
963
+ /* @__PURE__ */ jsx2(
964
+ "textarea",
965
+ {
966
+ value: inputText,
967
+ onChange: (e) => setInputText(e.target.value),
968
+ onKeyDown: (e) => {
969
+ if (e.key === "Enter" && !e.shiftKey) {
970
+ e.preventDefault();
971
+ void handleSend();
972
+ }
973
+ },
974
+ onPaste: handlePaste,
975
+ disabled: !activeSession,
976
+ placeholder: activeSession ? "Nh\u1EADp tin nh\u1EAFn... (H\u1ED7 tr\u1EE3 Ctrl+V d\xE1n \u1EA3nh)" : "Vui l\xF2ng b\u1EAFt \u0111\u1EA7u phi\xEAn h\u1ED7 tr\u1EE3...",
977
+ rows: 2,
978
+ className: "flex-1 max-h-24 min-h-[50px] resize-none rounded-xl border border-slate-200 dark:border-slate-800 px-3.5 py-2.5 text-xs text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-950 outline-none"
979
+ }
980
+ ),
981
+ /* @__PURE__ */ jsx2(
982
+ "button",
983
+ {
984
+ type: "submit",
985
+ disabled: !inputText.trim() && !attachment || isUploading || !activeSession,
986
+ className: "flex h-10 w-10 items-center justify-center rounded-xl bg-teal-500 text-white disabled:opacity-50",
987
+ children: /* @__PURE__ */ jsx2(Send, { className: "h-4 w-4" })
988
+ }
989
+ )
990
+ ] }) }),
991
+ showConfirmModal && /* @__PURE__ */ jsx2("div", { className: "absolute inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-[280px] p-5 bg-white dark:bg-slate-900 border dark:border-slate-800 rounded-xl shadow-xl", children: [
992
+ /* @__PURE__ */ jsx2("h4", { className: "text-xs font-bold text-slate-800 dark:text-slate-200", children: "X\xE1c nh\u1EADn k\u1EBFt th\xFAc" }),
993
+ /* @__PURE__ */ jsx2("p", { className: "text-[10px] text-slate-500 mt-2 leading-relaxed", children: "B\u1EA1n c\xF3 ch\u1EAFc ch\u1EAFn mu\u1ED1n \u0111\xF3ng phi\xEAn h\u1ED7 tr\u1EE3 n\xE0y kh\xF4ng? Cu\u1ED9c tr\xF2 chuy\u1EC7n n\xE0y s\u1EBD \u0111\u01B0\u1EE3c k\u1EBFt th\xFAc." }),
994
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 mt-4", children: [
995
+ /* @__PURE__ */ jsx2(
996
+ "button",
997
+ {
998
+ type: "button",
999
+ onClick: () => setShowConfirmModal(false),
1000
+ className: "px-3 py-1.5 text-[10px] font-semibold rounded-lg border text-slate-700 dark:text-slate-350 hover:bg-slate-50 dark:hover:bg-slate-800",
1001
+ children: "H\u1EE7y"
1002
+ }
1003
+ ),
1004
+ /* @__PURE__ */ jsx2(
1005
+ "button",
1006
+ {
1007
+ type: "button",
1008
+ onClick: () => {
1009
+ setShowConfirmModal(false);
1010
+ void chat.closeSession();
1011
+ },
1012
+ className: "px-3 py-1.5 text-[10px] font-semibold rounded-lg bg-red-500 text-white hover:bg-red-600",
1013
+ children: "X\xE1c nh\u1EADn"
1014
+ }
1015
+ )
1016
+ ] })
1017
+ ] }) })
1018
+ ]
1019
+ }
1020
+ );
1021
+ };
1022
+
1023
+ // src/components/ChatWidget.tsx
1024
+ import { useState as useState3, useEffect as useEffect3 } from "react";
1025
+ import { MessageSquare as MessageSquare2, X as X2 } from "lucide-react";
1026
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1027
+ var ChatWidget = ({
1028
+ title = "H\u1ED7 tr\u1EE3 tr\u1EF1c tuy\u1EBFn",
1029
+ quickQuestions
1030
+ }) => {
1031
+ const [isOpen, setIsOpen] = useState3(false);
1032
+ const { unreadCount, markAsRead } = useChat();
1033
+ useEffect3(() => {
1034
+ if (isOpen && unreadCount > 0) {
1035
+ void markAsRead();
1036
+ }
1037
+ }, [isOpen, unreadCount, markAsRead]);
1038
+ return /* @__PURE__ */ jsxs2("div", { className: "fixed bottom-6 right-6 z-50 flex flex-col items-end", children: [
1039
+ isOpen && /* @__PURE__ */ jsx3("div", { className: "mb-4 h-[520px] w-[360px] sm:w-[380px] transition-all duration-300", children: /* @__PURE__ */ jsx3(
1040
+ ChatWindow,
1041
+ {
1042
+ onClose: () => setIsOpen(false),
1043
+ title,
1044
+ quickQuestions
1045
+ }
1046
+ ) }),
1047
+ /* @__PURE__ */ jsxs2(
1048
+ "button",
1049
+ {
1050
+ type: "button",
1051
+ onClick: () => setIsOpen(!isOpen),
1052
+ className: "relative flex h-14 w-14 items-center justify-center rounded-full bg-teal-500 hover:bg-teal-600 text-white shadow-xl transition-all duration-300 hover:scale-105 active:scale-95",
1053
+ children: [
1054
+ isOpen ? /* @__PURE__ */ jsx3(X2, { className: "h-6 w-6" }) : /* @__PURE__ */ jsx3(MessageSquare2, { className: "h-6 w-6" }),
1055
+ !isOpen && unreadCount > 0 && /* @__PURE__ */ jsx3("span", { className: "absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-[10px] font-bold text-white ring-2 ring-white animate-bounce", children: unreadCount })
1056
+ ]
1057
+ }
1058
+ )
1059
+ ] });
1060
+ };
1061
+ export {
1062
+ ChatProvider,
1063
+ ChatWidget,
1064
+ ChatWindow,
1065
+ useChat,
1066
+ useChatAdmin,
1067
+ useChatContext
1068
+ };