teko-chat-sdk 1.0.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.js ADDED
@@ -0,0 +1,1722 @@
1
+ // src/components/TekoChatWidget.tsx
2
+ import { forwardRef, useCallback as useCallback4, useImperativeHandle, useState as useState5 } from "react";
3
+
4
+ // src/utils/chatTheme.ts
5
+ import { createContext, useContext } from "react";
6
+ var DEFAULT_PRIMARY_COLOR = "#1a73e8";
7
+ var ChatThemeContext = createContext({
8
+ primaryColor: DEFAULT_PRIMARY_COLOR
9
+ });
10
+ var useChatTheme = () => useContext(ChatThemeContext);
11
+ function hexToRgba(hex, alpha) {
12
+ const r = parseInt(hex.slice(1, 3), 16);
13
+ const g = parseInt(hex.slice(3, 5), 16);
14
+ const b = parseInt(hex.slice(5, 7), 16);
15
+ return `rgba(${r},${g},${b},${alpha})`;
16
+ }
17
+
18
+ // src/components/ChatBubble.tsx
19
+ import { jsx } from "react/jsx-runtime";
20
+ var ChatBubble = ({
21
+ onClick,
22
+ offsetBottom = 0,
23
+ zIndex = 1031
24
+ }) => {
25
+ const { primaryColor } = useChatTheme();
26
+ return /* @__PURE__ */ jsx(
27
+ "button",
28
+ {
29
+ onClick,
30
+ style: {
31
+ position: "fixed",
32
+ bottom: `calc(2rem + ${offsetBottom}px)`,
33
+ right: "2.5rem",
34
+ width: 52,
35
+ height: 52,
36
+ borderRadius: "50%",
37
+ background: primaryColor,
38
+ border: "none",
39
+ cursor: "pointer",
40
+ display: "flex",
41
+ alignItems: "center",
42
+ justifyContent: "center",
43
+ boxShadow: `0 4px 12px ${hexToRgba(primaryColor, 0.4)}`,
44
+ zIndex,
45
+ transition: "transform 0.2s ease, box-shadow 0.2s ease"
46
+ },
47
+ onMouseEnter: (e) => {
48
+ e.currentTarget.style.transform = "scale(1.08)";
49
+ e.currentTarget.style.boxShadow = `0 6px 16px ${hexToRgba(
50
+ primaryColor,
51
+ 0.5
52
+ )}`;
53
+ },
54
+ onMouseLeave: (e) => {
55
+ e.currentTarget.style.transform = "scale(1)";
56
+ e.currentTarget.style.boxShadow = `0 4px 12px ${hexToRgba(
57
+ primaryColor,
58
+ 0.4
59
+ )}`;
60
+ },
61
+ "aria-label": "M\u1EDF chat t\u01B0 v\u1EA5n",
62
+ children: /* @__PURE__ */ jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx(
63
+ "path",
64
+ {
65
+ d: "M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2Z",
66
+ fill: "white"
67
+ }
68
+ ) })
69
+ }
70
+ );
71
+ };
72
+
73
+ // src/components/ChatMiniPopup.tsx
74
+ import { useCallback, useState as useState2 } from "react";
75
+
76
+ // src/components/MessageList.tsx
77
+ import { useEffect, useRef } from "react";
78
+
79
+ // src/components/MessageBubble.tsx
80
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
81
+ var MessageBubble = ({
82
+ message,
83
+ onOptionClick
84
+ }) => {
85
+ const isUser = message.role === "user";
86
+ const { primaryColor } = useChatTheme();
87
+ return /* @__PURE__ */ jsxs(
88
+ "div",
89
+ {
90
+ style: {
91
+ display: "flex",
92
+ flexDirection: "column",
93
+ alignItems: isUser ? "flex-end" : "flex-start",
94
+ marginBottom: 8
95
+ },
96
+ children: [
97
+ /* @__PURE__ */ jsx2(
98
+ "div",
99
+ {
100
+ style: {
101
+ maxWidth: "80%",
102
+ padding: "8px 12px",
103
+ borderRadius: isUser ? "16px 16px 4px 16px" : "16px 16px 16px 4px",
104
+ background: isUser ? primaryColor : "#f1f3f4",
105
+ color: isUser ? "#fff" : "#202124",
106
+ fontSize: 14,
107
+ lineHeight: 1.5,
108
+ whiteSpace: "pre-wrap",
109
+ wordBreak: "break-word"
110
+ },
111
+ children: message.content
112
+ }
113
+ ),
114
+ !isUser && message.options && message.options.length > 0 && /* @__PURE__ */ jsx2(
115
+ "div",
116
+ {
117
+ style: {
118
+ display: "flex",
119
+ flexWrap: "wrap",
120
+ gap: 6,
121
+ marginTop: 6,
122
+ maxWidth: "80%"
123
+ },
124
+ children: message.options.map((opt) => /* @__PURE__ */ jsx2(
125
+ "button",
126
+ {
127
+ onClick: () => onOptionClick(opt),
128
+ style: {
129
+ padding: "5px 12px",
130
+ borderRadius: 16,
131
+ border: `1px solid ${primaryColor}`,
132
+ background: "#fff",
133
+ color: primaryColor,
134
+ fontSize: 13,
135
+ cursor: "pointer",
136
+ transition: "background 0.15s"
137
+ },
138
+ onMouseEnter: (e) => {
139
+ e.currentTarget.style.background = hexToRgba(primaryColor, 0.1);
140
+ },
141
+ onMouseLeave: (e) => {
142
+ e.currentTarget.style.background = "#fff";
143
+ },
144
+ children: opt.label
145
+ },
146
+ opt.key
147
+ ))
148
+ }
149
+ ),
150
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 11, color: "#9aa0a6", marginTop: 2 }, children: new Date(message.timestamp).toLocaleTimeString("vi-VN", {
151
+ hour: "2-digit",
152
+ minute: "2-digit"
153
+ }) })
154
+ ]
155
+ }
156
+ );
157
+ };
158
+
159
+ // src/components/MessageList.tsx
160
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
161
+ var MessageList = ({
162
+ messages,
163
+ isLoading,
164
+ onOptionClick,
165
+ labels
166
+ }) => {
167
+ const bottomRef = useRef(null);
168
+ useEffect(() => {
169
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
170
+ }, [messages, isLoading]);
171
+ return /* @__PURE__ */ jsxs2(
172
+ "div",
173
+ {
174
+ style: {
175
+ flex: 1,
176
+ overflowY: "auto",
177
+ padding: "12px 16px",
178
+ display: "flex",
179
+ flexDirection: "column"
180
+ },
181
+ children: [
182
+ messages.length === 0 && /* @__PURE__ */ jsx3(
183
+ "div",
184
+ {
185
+ style: {
186
+ flex: 1,
187
+ display: "flex",
188
+ alignItems: "center",
189
+ justifyContent: "center",
190
+ color: "#9aa0a6",
191
+ fontSize: 14,
192
+ textAlign: "center"
193
+ },
194
+ children: labels.emptyState
195
+ }
196
+ ),
197
+ messages.map((msg) => /* @__PURE__ */ jsx3(
198
+ MessageBubble,
199
+ {
200
+ message: msg,
201
+ onOptionClick
202
+ },
203
+ msg.id
204
+ )),
205
+ isLoading && /* @__PURE__ */ jsx3(
206
+ "div",
207
+ {
208
+ style: { display: "flex", alignItems: "flex-start", marginBottom: 8 },
209
+ children: /* @__PURE__ */ jsx3(
210
+ "div",
211
+ {
212
+ style: {
213
+ padding: "8px 14px",
214
+ borderRadius: "16px 16px 16px 4px",
215
+ background: "#f1f3f4",
216
+ display: "flex",
217
+ gap: 4,
218
+ alignItems: "center"
219
+ },
220
+ children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx3(
221
+ "span",
222
+ {
223
+ style: {
224
+ width: 6,
225
+ height: 6,
226
+ borderRadius: "50%",
227
+ background: "#9aa0a6",
228
+ display: "inline-block",
229
+ animation: `tekochat-bounce 1.2s ease-in-out ${i * 0.2}s infinite`
230
+ }
231
+ },
232
+ i
233
+ ))
234
+ }
235
+ )
236
+ }
237
+ ),
238
+ /* @__PURE__ */ jsx3("div", { ref: bottomRef }),
239
+ /* @__PURE__ */ jsx3("style", { children: `
240
+ @keyframes tekochat-bounce {
241
+ 0%, 80%, 100% { transform: translateY(0); }
242
+ 40% { transform: translateY(-6px); }
243
+ }
244
+ ` })
245
+ ]
246
+ }
247
+ );
248
+ };
249
+
250
+ // src/components/MessageInput.tsx
251
+ import { useRef as useRef2, useState } from "react";
252
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
253
+ var MessageInput = ({
254
+ onSend,
255
+ disabled,
256
+ labels
257
+ }) => {
258
+ const [value, setValue] = useState("");
259
+ const textareaRef = useRef2(null);
260
+ const { primaryColor } = useChatTheme();
261
+ const handleSend = () => {
262
+ const text = value.trim();
263
+ if (!text || disabled) return;
264
+ onSend(text);
265
+ setValue("");
266
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
267
+ };
268
+ const handleKeyDown = (e) => {
269
+ if (e.key === "Enter" && !e.shiftKey) {
270
+ e.preventDefault();
271
+ handleSend();
272
+ }
273
+ };
274
+ const handleInput = (e) => {
275
+ setValue(e.target.value);
276
+ e.target.style.height = "auto";
277
+ e.target.style.height = `${Math.min(e.target.scrollHeight, 100)}px`;
278
+ };
279
+ return /* @__PURE__ */ jsxs3(
280
+ "div",
281
+ {
282
+ style: {
283
+ display: "flex",
284
+ alignItems: "flex-end",
285
+ gap: 8,
286
+ padding: "8px 12px",
287
+ borderTop: "1px solid #e8eaed",
288
+ background: "#fff"
289
+ },
290
+ children: [
291
+ /* @__PURE__ */ jsx4(
292
+ "textarea",
293
+ {
294
+ ref: textareaRef,
295
+ value,
296
+ onChange: handleInput,
297
+ onKeyDown: handleKeyDown,
298
+ disabled,
299
+ placeholder: labels.inputPlaceholder,
300
+ rows: 1,
301
+ style: {
302
+ flex: 1,
303
+ resize: "none",
304
+ border: "1px solid #e8eaed",
305
+ borderRadius: 20,
306
+ padding: "8px 12px",
307
+ fontSize: 14,
308
+ lineHeight: 1.4,
309
+ outline: "none",
310
+ fontFamily: "inherit",
311
+ background: disabled ? "#f8f9fa" : "#fff",
312
+ color: "#202124",
313
+ transition: "border-color 0.15s",
314
+ overflowY: "hidden"
315
+ },
316
+ onFocus: (e) => {
317
+ e.target.style.borderColor = primaryColor;
318
+ },
319
+ onBlur: (e) => {
320
+ e.target.style.borderColor = "#e8eaed";
321
+ }
322
+ }
323
+ ),
324
+ /* @__PURE__ */ jsx4(
325
+ "button",
326
+ {
327
+ onClick: handleSend,
328
+ disabled: !value.trim() || disabled,
329
+ style: {
330
+ width: 36,
331
+ height: 36,
332
+ borderRadius: "50%",
333
+ border: "none",
334
+ background: value.trim() && !disabled ? primaryColor : "#e8eaed",
335
+ cursor: value.trim() && !disabled ? "pointer" : "not-allowed",
336
+ display: "flex",
337
+ alignItems: "center",
338
+ justifyContent: "center",
339
+ flexShrink: 0,
340
+ transition: "background 0.15s"
341
+ },
342
+ "aria-label": labels.send,
343
+ children: /* @__PURE__ */ jsx4("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx4("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z", fill: "white" }) })
344
+ }
345
+ )
346
+ ]
347
+ }
348
+ );
349
+ };
350
+
351
+ // src/components/ChatMiniPopup.tsx
352
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
353
+ var ChatHeader = ({ onClose, labels, botAvatar }) => {
354
+ const { primaryColor } = useChatTheme();
355
+ return /* @__PURE__ */ jsxs4(
356
+ "div",
357
+ {
358
+ style: {
359
+ display: "flex",
360
+ alignItems: "center",
361
+ justifyContent: "space-between",
362
+ padding: "12px 16px",
363
+ background: primaryColor,
364
+ color: "#fff",
365
+ flexShrink: 0
366
+ },
367
+ children: [
368
+ /* @__PURE__ */ jsxs4("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
369
+ /* @__PURE__ */ jsx5(
370
+ "div",
371
+ {
372
+ style: {
373
+ width: 32,
374
+ height: 32,
375
+ borderRadius: "50%",
376
+ background: "rgba(255,255,255,0.2)",
377
+ display: "flex",
378
+ alignItems: "center",
379
+ justifyContent: "center",
380
+ overflow: "hidden"
381
+ },
382
+ children: botAvatar ? /* @__PURE__ */ jsx5(
383
+ "img",
384
+ {
385
+ src: botAvatar,
386
+ alt: labels.agentName,
387
+ style: {
388
+ width: 32,
389
+ height: 32,
390
+ borderRadius: "50%",
391
+ objectFit: "cover"
392
+ }
393
+ }
394
+ ) : /* @__PURE__ */ jsx5("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx5(
395
+ "path",
396
+ {
397
+ d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z",
398
+ fill: "white"
399
+ }
400
+ ) })
401
+ }
402
+ ),
403
+ /* @__PURE__ */ jsxs4("div", { children: [
404
+ /* @__PURE__ */ jsx5("div", { style: { fontSize: 14, fontWeight: 600 }, children: labels.agentName }),
405
+ /* @__PURE__ */ jsx5("div", { style: { fontSize: 11, opacity: 0.85 }, children: labels.agentStatus })
406
+ ] })
407
+ ] }),
408
+ /* @__PURE__ */ jsx5(
409
+ "button",
410
+ {
411
+ onClick: onClose,
412
+ style: {
413
+ background: "none",
414
+ border: "none",
415
+ cursor: "pointer",
416
+ color: "#fff",
417
+ padding: 4,
418
+ display: "flex",
419
+ alignItems: "center"
420
+ },
421
+ "aria-label": labels.close,
422
+ children: /* @__PURE__ */ jsx5("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx5(
423
+ "path",
424
+ {
425
+ d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
426
+ fill: "white"
427
+ }
428
+ ) })
429
+ }
430
+ )
431
+ ]
432
+ }
433
+ );
434
+ };
435
+ var ChatMiniPopup = ({
436
+ messages,
437
+ isLoading,
438
+ onSend,
439
+ onOptionClick,
440
+ onClose,
441
+ offsetBottom = 0,
442
+ zIndex = 1031,
443
+ miniWidth = 360,
444
+ miniHeight = 480,
445
+ layoutMode = "desktop",
446
+ labels,
447
+ botAvatar
448
+ }) => {
449
+ const [isClosing, setIsClosing] = useState2(false);
450
+ const handleClose = useCallback(() => {
451
+ setIsClosing(true);
452
+ setTimeout(() => {
453
+ setIsClosing(false);
454
+ onClose();
455
+ }, 300);
456
+ }, [onClose]);
457
+ if (layoutMode === "mobile") {
458
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
459
+ /* @__PURE__ */ jsx5(
460
+ "div",
461
+ {
462
+ style: {
463
+ position: "fixed",
464
+ inset: 0,
465
+ background: "rgba(0,0,0,0.4)",
466
+ zIndex: zIndex - 1,
467
+ animation: isClosing ? "tekochat-overlay-out 0.3s ease forwards" : "tekochat-overlay-in 0.3s ease"
468
+ },
469
+ onClick: handleClose
470
+ }
471
+ ),
472
+ /* @__PURE__ */ jsxs4(
473
+ "div",
474
+ {
475
+ style: {
476
+ position: "fixed",
477
+ bottom: 0,
478
+ left: 0,
479
+ right: 0,
480
+ height: `calc(85vh - ${offsetBottom}px)`,
481
+ background: "#fff",
482
+ borderRadius: "16px 16px 0 0",
483
+ boxShadow: "0 -4px 24px rgba(0,0,0,0.15)",
484
+ display: "flex",
485
+ flexDirection: "column",
486
+ zIndex,
487
+ overflow: "hidden",
488
+ overscrollBehavior: "contain",
489
+ animation: isClosing ? "tekochat-sheet-out 0.3s cubic-bezier(0.32, 0.72, 0, 1) forwards" : "tekochat-sheet-in 0.3s cubic-bezier(0.32, 0.72, 0, 1)"
490
+ },
491
+ children: [
492
+ /* @__PURE__ */ jsx5(
493
+ ChatHeader,
494
+ {
495
+ onClose: handleClose,
496
+ labels,
497
+ botAvatar
498
+ }
499
+ ),
500
+ /* @__PURE__ */ jsx5(
501
+ MessageList,
502
+ {
503
+ messages,
504
+ isLoading,
505
+ onOptionClick,
506
+ labels
507
+ }
508
+ ),
509
+ /* @__PURE__ */ jsx5(MessageInput, { onSend, disabled: isLoading, labels }),
510
+ /* @__PURE__ */ jsx5("style", { children: `
511
+ @keyframes tekochat-sheet-in {
512
+ from { transform: translateY(100%); }
513
+ to { transform: translateY(0); }
514
+ }
515
+ @keyframes tekochat-sheet-out {
516
+ from { transform: translateY(0); }
517
+ to { transform: translateY(100%); }
518
+ }
519
+ @keyframes tekochat-overlay-in {
520
+ from { opacity: 0; }
521
+ to { opacity: 1; }
522
+ }
523
+ @keyframes tekochat-overlay-out {
524
+ from { opacity: 1; }
525
+ to { opacity: 0; }
526
+ }
527
+ ` })
528
+ ]
529
+ }
530
+ )
531
+ ] });
532
+ }
533
+ return /* @__PURE__ */ jsxs4(
534
+ "div",
535
+ {
536
+ style: {
537
+ position: "fixed",
538
+ bottom: `calc(5.5rem + ${offsetBottom}px)`,
539
+ right: "2.5rem",
540
+ width: miniWidth,
541
+ height: miniHeight,
542
+ background: "#fff",
543
+ borderRadius: 16,
544
+ boxShadow: "0 8px 32px rgba(0,0,0,0.18)",
545
+ display: "flex",
546
+ flexDirection: "column",
547
+ zIndex,
548
+ overflow: "hidden",
549
+ animation: "tekochat-popup-in 0.25s cubic-bezier(0.4, 0, 0.2, 1)"
550
+ },
551
+ children: [
552
+ /* @__PURE__ */ jsx5(ChatHeader, { onClose, labels, botAvatar }),
553
+ /* @__PURE__ */ jsx5(
554
+ MessageList,
555
+ {
556
+ messages,
557
+ isLoading,
558
+ onOptionClick,
559
+ labels
560
+ }
561
+ ),
562
+ /* @__PURE__ */ jsx5(MessageInput, { onSend, disabled: isLoading, labels }),
563
+ /* @__PURE__ */ jsx5("style", { children: `
564
+ @keyframes tekochat-popup-in {
565
+ from { opacity: 0; transform: scale(0.85) translateY(16px); transform-origin: bottom right; }
566
+ to { opacity: 1; transform: scale(1) translateY(0); }
567
+ }
568
+ ` })
569
+ ]
570
+ }
571
+ );
572
+ };
573
+
574
+ // src/components/ChatFullscreen.tsx
575
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
576
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
577
+ var EXIT_DURATION = 350;
578
+ var ANCHOR_TO_POSITION = (anchor, offsetTop, offsetBottom) => {
579
+ switch (anchor) {
580
+ case "bottom-left":
581
+ return { bottom: `calc(2rem + ${offsetBottom}px)`, left: "2.5rem" };
582
+ case "top-right":
583
+ return { top: `calc(2rem + ${offsetTop}px)`, right: "2.5rem" };
584
+ case "top-left":
585
+ return { top: `calc(2rem + ${offsetTop}px)`, left: "2.5rem" };
586
+ default:
587
+ return { bottom: `calc(2rem + ${offsetBottom}px)`, right: "2.5rem" };
588
+ }
589
+ };
590
+ var ChatPanelHeader = ({ onClose, primaryColor, labels, botAvatar }) => /* @__PURE__ */ jsxs5(
591
+ "div",
592
+ {
593
+ style: {
594
+ display: "flex",
595
+ alignItems: "center",
596
+ justifyContent: "space-between",
597
+ padding: "12px 16px",
598
+ background: primaryColor,
599
+ color: "#fff",
600
+ flexShrink: 0
601
+ },
602
+ children: [
603
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
604
+ /* @__PURE__ */ jsx6(
605
+ "div",
606
+ {
607
+ style: {
608
+ width: 32,
609
+ height: 32,
610
+ borderRadius: "50%",
611
+ background: "rgba(255,255,255,0.2)",
612
+ display: "flex",
613
+ alignItems: "center",
614
+ justifyContent: "center",
615
+ overflow: "hidden"
616
+ },
617
+ children: botAvatar ? /* @__PURE__ */ jsx6(
618
+ "img",
619
+ {
620
+ src: botAvatar,
621
+ alt: labels.agentName,
622
+ style: {
623
+ width: 32,
624
+ height: 32,
625
+ borderRadius: "50%",
626
+ objectFit: "cover"
627
+ }
628
+ }
629
+ ) : /* @__PURE__ */ jsx6("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx6(
630
+ "path",
631
+ {
632
+ d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z",
633
+ fill: "white"
634
+ }
635
+ ) })
636
+ }
637
+ ),
638
+ /* @__PURE__ */ jsxs5("div", { children: [
639
+ /* @__PURE__ */ jsx6("div", { style: { fontSize: 14, fontWeight: 600 }, children: labels.agentName }),
640
+ /* @__PURE__ */ jsx6("div", { style: { fontSize: 11, opacity: 0.85 }, children: labels.agentStatus })
641
+ ] })
642
+ ] }),
643
+ /* @__PURE__ */ jsx6(
644
+ "button",
645
+ {
646
+ onClick: onClose,
647
+ style: {
648
+ background: "none",
649
+ border: "none",
650
+ cursor: "pointer",
651
+ color: "#fff",
652
+ padding: 4,
653
+ display: "flex",
654
+ alignItems: "center"
655
+ },
656
+ "aria-label": labels.close,
657
+ children: /* @__PURE__ */ jsx6("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx6(
658
+ "path",
659
+ {
660
+ d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
661
+ fill: "white"
662
+ }
663
+ ) })
664
+ }
665
+ )
666
+ ]
667
+ }
668
+ );
669
+ var ChatFullscreen = ({
670
+ messages,
671
+ isLoading,
672
+ componentKey,
673
+ onSend,
674
+ onOptionClick,
675
+ onClose,
676
+ onMinimize,
677
+ renderRightPanel,
678
+ transformOrigin = "bottom right",
679
+ bubbleAnchor = "bottom-right",
680
+ offsetTop = 0,
681
+ offsetBottom = 0,
682
+ zIndex = 1040,
683
+ layoutMode = "desktop",
684
+ showRightPanelTrigger = 0,
685
+ labels,
686
+ botAvatar
687
+ }) => {
688
+ const { primaryColor } = useChatTheme();
689
+ const [isClosing, setIsClosing] = useState3(false);
690
+ const [activeMobilePanel, setActiveMobilePanel] = useState3("chat");
691
+ useEffect2(() => {
692
+ if (layoutMode === "mobile" && showRightPanelTrigger > 0) {
693
+ setActiveMobilePanel("rightPanel");
694
+ }
695
+ }, [showRightPanelTrigger, layoutMode]);
696
+ const [dragY, setDragY] = useState3(0);
697
+ const touchStartY = useRef3(0);
698
+ const isDragging = useRef3(false);
699
+ const handleDragStart = useCallback2((e) => {
700
+ touchStartY.current = e.touches[0].clientY;
701
+ isDragging.current = true;
702
+ }, []);
703
+ const handleDragMove = useCallback2((e) => {
704
+ if (!isDragging.current) return;
705
+ const delta = e.touches[0].clientY - touchStartY.current;
706
+ if (delta > 0) setDragY(delta);
707
+ }, []);
708
+ const handleDragEnd = useCallback2(() => {
709
+ isDragging.current = false;
710
+ if (dragY > 80) {
711
+ setDragY(0);
712
+ setActiveMobilePanel("chat");
713
+ } else {
714
+ setDragY(0);
715
+ }
716
+ }, [dragY]);
717
+ const triggerExit = useCallback2((callback) => {
718
+ setIsClosing(true);
719
+ setTimeout(() => {
720
+ setIsClosing(false);
721
+ callback();
722
+ }, EXIT_DURATION);
723
+ }, []);
724
+ const floatingIconPosition = ANCHOR_TO_POSITION(
725
+ bubbleAnchor,
726
+ offsetTop,
727
+ offsetBottom
728
+ );
729
+ if (layoutMode === "mobile") {
730
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
731
+ activeMobilePanel === "rightPanel" && /* @__PURE__ */ jsx6(
732
+ "div",
733
+ {
734
+ style: {
735
+ position: "fixed",
736
+ top: offsetTop,
737
+ left: 0,
738
+ right: 0,
739
+ bottom: offsetBottom,
740
+ background: "rgba(0,0,0,0.35)",
741
+ zIndex: zIndex + 1
742
+ },
743
+ onClick: () => setActiveMobilePanel("chat")
744
+ }
745
+ ),
746
+ /* @__PURE__ */ jsxs5(
747
+ "div",
748
+ {
749
+ style: {
750
+ position: "fixed",
751
+ top: offsetTop,
752
+ left: 0,
753
+ right: 0,
754
+ bottom: offsetBottom,
755
+ display: "flex",
756
+ flexDirection: "column",
757
+ background: "#fff",
758
+ zIndex,
759
+ animation: isClosing ? `tekochat-overlay-out ${EXIT_DURATION}ms ease forwards` : "tekochat-overlay-in 0.2s ease"
760
+ },
761
+ onClick: (e) => e.stopPropagation(),
762
+ children: [
763
+ /* @__PURE__ */ jsx6(
764
+ ChatPanelHeader,
765
+ {
766
+ primaryColor,
767
+ onClose: () => triggerExit(onClose),
768
+ labels,
769
+ botAvatar
770
+ }
771
+ ),
772
+ /* @__PURE__ */ jsx6(
773
+ MessageList,
774
+ {
775
+ messages,
776
+ isLoading,
777
+ onOptionClick,
778
+ labels
779
+ }
780
+ ),
781
+ /* @__PURE__ */ jsx6(MessageInput, { onSend, disabled: isLoading, labels })
782
+ ]
783
+ }
784
+ ),
785
+ componentKey && !isClosing && /* @__PURE__ */ jsx6(
786
+ "button",
787
+ {
788
+ onClick: () => setActiveMobilePanel(
789
+ (prev) => prev === "chat" ? "rightPanel" : "chat"
790
+ ),
791
+ style: {
792
+ position: "fixed",
793
+ ...floatingIconPosition,
794
+ zIndex: zIndex + 3,
795
+ width: 52,
796
+ height: 52,
797
+ borderRadius: "50%",
798
+ background: primaryColor,
799
+ border: "none",
800
+ cursor: "pointer",
801
+ display: "flex",
802
+ alignItems: "center",
803
+ justifyContent: "center",
804
+ boxShadow: `0 4px 16px ${hexToRgba(primaryColor, 0.45)}`,
805
+ transition: "transform 0.2s ease"
806
+ },
807
+ "aria-label": activeMobilePanel === "rightPanel" ? labels.backToChat : labels.viewContent,
808
+ children: activeMobilePanel === "rightPanel" ? /* @__PURE__ */ jsx6("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx6(
809
+ "path",
810
+ {
811
+ d: "M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2Z",
812
+ fill: "white"
813
+ }
814
+ ) }) : /* @__PURE__ */ jsx6("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx6(
815
+ "path",
816
+ {
817
+ d: "M3 3h8v8H3V3zm10 0h8v8h-8V3zM3 13h8v8H3v-8zm10 0h8v8h-8v-8z",
818
+ fill: "white"
819
+ }
820
+ ) })
821
+ }
822
+ ),
823
+ componentKey && /* @__PURE__ */ jsxs5(
824
+ "div",
825
+ {
826
+ style: {
827
+ position: "fixed",
828
+ bottom: 0,
829
+ left: 0,
830
+ right: 0,
831
+ height: `calc(85vh - ${offsetBottom}px)`,
832
+ borderRadius: "16px 16px 0 0",
833
+ background: "#fff",
834
+ boxShadow: "0 -4px 24px rgba(0,0,0,0.15)",
835
+ zIndex: zIndex + 2,
836
+ display: "flex",
837
+ flexDirection: "column",
838
+ overflow: "hidden",
839
+ transform: activeMobilePanel === "rightPanel" ? `translateY(${dragY}px)` : "translateY(100%)",
840
+ transition: dragY > 0 ? "none" : `transform ${EXIT_DURATION}ms cubic-bezier(0.32, 0.72, 0, 1)`
841
+ },
842
+ onClick: (e) => e.stopPropagation(),
843
+ children: [
844
+ /* @__PURE__ */ jsx6(
845
+ "div",
846
+ {
847
+ style: {
848
+ display: "flex",
849
+ justifyContent: "center",
850
+ padding: "10px 0 6px",
851
+ flexShrink: 0,
852
+ cursor: "grab",
853
+ touchAction: "none"
854
+ },
855
+ onTouchStart: handleDragStart,
856
+ onTouchMove: handleDragMove,
857
+ onTouchEnd: handleDragEnd,
858
+ children: /* @__PURE__ */ jsx6(
859
+ "div",
860
+ {
861
+ style: {
862
+ width: 36,
863
+ height: 4,
864
+ borderRadius: 2,
865
+ background: "#e0e0e0"
866
+ }
867
+ }
868
+ )
869
+ }
870
+ ),
871
+ /* @__PURE__ */ jsx6("div", { style: { flex: 1, overflow: "auto" }, children: renderRightPanel ? renderRightPanel(componentKey) : /* @__PURE__ */ jsx6(
872
+ "div",
873
+ {
874
+ style: {
875
+ height: "100%",
876
+ display: "flex",
877
+ alignItems: "center",
878
+ justifyContent: "center",
879
+ color: "#9aa0a6",
880
+ fontSize: 14
881
+ },
882
+ children: labels.loadingContent
883
+ }
884
+ ) })
885
+ ]
886
+ }
887
+ ),
888
+ /* @__PURE__ */ jsx6("style", { children: `
889
+ @keyframes tekochat-overlay-in {
890
+ from { opacity: 0; }
891
+ to { opacity: 1; }
892
+ }
893
+ @keyframes tekochat-overlay-out {
894
+ from { opacity: 1; }
895
+ to { opacity: 0; }
896
+ }
897
+ ` })
898
+ ] });
899
+ }
900
+ return /* @__PURE__ */ jsxs5(
901
+ "div",
902
+ {
903
+ style: {
904
+ position: "fixed",
905
+ top: offsetTop,
906
+ left: 0,
907
+ right: 0,
908
+ bottom: offsetBottom,
909
+ background: "rgba(0,0,0,0.4)",
910
+ zIndex,
911
+ display: "flex",
912
+ flexDirection: "column",
913
+ animation: isClosing ? `tekochat-overlay-out ${EXIT_DURATION}ms ease forwards` : "tekochat-overlay-in 0.2s ease"
914
+ },
915
+ children: [
916
+ /* @__PURE__ */ jsxs5(
917
+ "div",
918
+ {
919
+ style: {
920
+ flex: 1,
921
+ display: "flex",
922
+ background: "#f8f9fa",
923
+ overflow: "hidden",
924
+ transformOrigin,
925
+ animation: isClosing ? `tekochat-fullscreen-out ${EXIT_DURATION}ms cubic-bezier(0.4, 0, 1, 1) forwards` : "tekochat-fullscreen-in 0.35s cubic-bezier(0, 0, 0.2, 1)"
926
+ },
927
+ onClick: (e) => e.stopPropagation(),
928
+ children: [
929
+ /* @__PURE__ */ jsxs5(
930
+ "div",
931
+ {
932
+ style: {
933
+ width: 380,
934
+ flexShrink: 0,
935
+ display: "flex",
936
+ flexDirection: "column",
937
+ background: "#fff",
938
+ borderRight: "1px solid #e8eaed"
939
+ },
940
+ children: [
941
+ /* @__PURE__ */ jsx6(
942
+ ChatPanelHeader,
943
+ {
944
+ primaryColor,
945
+ onClose: () => triggerExit(onClose),
946
+ labels,
947
+ botAvatar
948
+ }
949
+ ),
950
+ /* @__PURE__ */ jsx6(
951
+ MessageList,
952
+ {
953
+ messages,
954
+ isLoading,
955
+ onOptionClick,
956
+ labels
957
+ }
958
+ ),
959
+ /* @__PURE__ */ jsx6(MessageInput, { onSend, disabled: isLoading, labels })
960
+ ]
961
+ }
962
+ ),
963
+ /* @__PURE__ */ jsxs5(
964
+ "div",
965
+ {
966
+ style: {
967
+ flex: 1,
968
+ display: "flex",
969
+ flexDirection: "column",
970
+ overflow: "hidden",
971
+ background: "#fff"
972
+ },
973
+ children: [
974
+ onMinimize && /* @__PURE__ */ jsx6(
975
+ "div",
976
+ {
977
+ style: {
978
+ display: "flex",
979
+ alignItems: "center",
980
+ justifyContent: "flex-end",
981
+ padding: "8px 12px",
982
+ borderBottom: "1px solid #e8eaed",
983
+ flexShrink: 0
984
+ },
985
+ children: /* @__PURE__ */ jsx6(
986
+ "button",
987
+ {
988
+ onClick: () => triggerExit(onMinimize),
989
+ style: {
990
+ background: "none",
991
+ border: "1px solid #e8eaed",
992
+ borderRadius: 6,
993
+ padding: "4px 6px",
994
+ color: "#5f6368",
995
+ cursor: "pointer",
996
+ display: "flex",
997
+ alignItems: "center"
998
+ },
999
+ "aria-label": labels.minimize,
1000
+ title: labels.minimize,
1001
+ children: /* @__PURE__ */ jsx6("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx6(
1002
+ "path",
1003
+ {
1004
+ d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z",
1005
+ fill: "currentColor"
1006
+ }
1007
+ ) })
1008
+ }
1009
+ )
1010
+ }
1011
+ ),
1012
+ /* @__PURE__ */ jsx6("div", { style: { flex: 1, overflow: "auto" }, children: renderRightPanel ? renderRightPanel(componentKey) : /* @__PURE__ */ jsx6(
1013
+ "div",
1014
+ {
1015
+ style: {
1016
+ height: "100%",
1017
+ display: "flex",
1018
+ alignItems: "center",
1019
+ justifyContent: "center",
1020
+ color: "#9aa0a6",
1021
+ fontSize: 14
1022
+ },
1023
+ children: "\u0110ang t\u1EA3i n\u1ED9i dung..."
1024
+ }
1025
+ ) })
1026
+ ]
1027
+ }
1028
+ )
1029
+ ]
1030
+ }
1031
+ ),
1032
+ /* @__PURE__ */ jsx6("style", { children: `
1033
+ @keyframes tekochat-overlay-in {
1034
+ from { opacity: 0; }
1035
+ to { opacity: 1; }
1036
+ }
1037
+ @keyframes tekochat-overlay-out {
1038
+ from { opacity: 1; }
1039
+ to { opacity: 0; }
1040
+ }
1041
+ @keyframes tekochat-fullscreen-in {
1042
+ from { transform: scale(0.05); opacity: 0; }
1043
+ to { transform: scale(1); opacity: 1; }
1044
+ }
1045
+ @keyframes tekochat-fullscreen-out {
1046
+ from { transform: scale(1); opacity: 1; }
1047
+ to { transform: scale(0.05); opacity: 0; }
1048
+ }
1049
+ ` })
1050
+ ]
1051
+ }
1052
+ );
1053
+ };
1054
+
1055
+ // src/hooks/useChatSession.ts
1056
+ import { useCallback as useCallback3, useRef as useRef4, useState as useState4 } from "react";
1057
+
1058
+ // src/transports/HttpStreamTransport.ts
1059
+ var HttpStreamTransport = class {
1060
+ constructor(url, token) {
1061
+ this.url = url;
1062
+ this.token = token;
1063
+ }
1064
+ onChunk(cb) {
1065
+ this.chunkCb = cb;
1066
+ }
1067
+ onDone(cb) {
1068
+ this.doneCb = cb;
1069
+ }
1070
+ onError(cb) {
1071
+ this.errorCb = cb;
1072
+ }
1073
+ connect() {
1074
+ return Promise.resolve();
1075
+ }
1076
+ disconnect() {
1077
+ }
1078
+ async send(request) {
1079
+ try {
1080
+ const res = await fetch(this.url, {
1081
+ method: "POST",
1082
+ headers: {
1083
+ "Content-Type": "application/json",
1084
+ Accept: "application/x-ndjson",
1085
+ ...this.token && { Authorization: `Bearer ${this.token}` }
1086
+ },
1087
+ body: JSON.stringify(request)
1088
+ });
1089
+ if (!res.ok || !res.body) {
1090
+ throw new Error(`[HttpStreamTransport] HTTP ${res.status}`);
1091
+ }
1092
+ const reader = res.body.getReader();
1093
+ const decoder = new TextDecoder();
1094
+ let buffer = "";
1095
+ while (true) {
1096
+ const { done, value } = await reader.read();
1097
+ if (done) break;
1098
+ buffer += decoder.decode(value, { stream: true });
1099
+ const lines = buffer.split("\n");
1100
+ buffer = lines.pop() ?? "";
1101
+ for (const line of lines) {
1102
+ if (!line.trim()) continue;
1103
+ try {
1104
+ const frame = JSON.parse(line);
1105
+ if (frame.type === "chunk") {
1106
+ this.chunkCb?.(frame.text);
1107
+ } else if (frame.type === "done") {
1108
+ this.doneCb?.(frame.data);
1109
+ } else if (frame.type === "error") {
1110
+ this.errorCb?.(new Error(frame.message));
1111
+ }
1112
+ } catch {
1113
+ this.errorCb?.(
1114
+ new Error("[HttpStreamTransport] Failed to parse frame")
1115
+ );
1116
+ }
1117
+ }
1118
+ }
1119
+ } catch (err) {
1120
+ this.errorCb?.(err);
1121
+ }
1122
+ }
1123
+ };
1124
+
1125
+ // src/utils/mockHandler.ts
1126
+ var MOCK_RESPONSES = [
1127
+ {
1128
+ keywords: [
1129
+ "canh chua",
1130
+ "s\u01B0\u1EDDn non",
1131
+ "th\u1ECBt heo",
1132
+ "th\u1ECBt l\u1EE3n",
1133
+ "g\xE0",
1134
+ "th\u1ECBt g\xE0",
1135
+ "th\u1ECBt b\xF2",
1136
+ "h\u1EA3i s\u1EA3n"
1137
+ ],
1138
+ response: () => ({
1139
+ conversationId: "",
1140
+ message: "T\xF4i \u0111\xE3 t\xECm th\u1EA5y m\u1ED9t s\u1ED1 s\u1EA3n ph\u1EA9m ph\xF9 h\u1EE3p! B\u1EA1n c\xF3 mu\u1ED1n xem gi\u1ECF h\xE0ng v\xE0 \u0111i\u1EC1u ch\u1EC9nh s\u1ED1 l\u01B0\u1EE3ng kh\xF4ng?",
1141
+ intent: "addToCart",
1142
+ action: "show_ui",
1143
+ componentKey: "cart",
1144
+ suggest: {
1145
+ options: [
1146
+ {
1147
+ key: "view-cart",
1148
+ label: "Xem gi\u1ECF h\xE0ng",
1149
+ payload: { action: "view_cart" }
1150
+ },
1151
+ {
1152
+ key: "continue",
1153
+ label: "Ti\u1EBFp t\u1EE5c mua s\u1EAFm",
1154
+ payload: { action: "continue" }
1155
+ }
1156
+ ]
1157
+ },
1158
+ timestamp: Date.now()
1159
+ })
1160
+ },
1161
+ {
1162
+ keywords: ["\u0111\u01A1n h\xE0ng", "\u0111\u01A1n c\u1EE7a t\xF4i", "theo d\xF5i \u0111\u01A1n", "tr\u1EA1ng th\xE1i \u0111\u01A1n"],
1163
+ response: () => ({
1164
+ conversationId: "",
1165
+ message: "T\xF4i s\u1EBD gi\xFAp b\u1EA1n xem th\xF4ng tin \u0111\u01A1n h\xE0ng nh\xE9!",
1166
+ intent: "viewOrder",
1167
+ action: "show_ui",
1168
+ componentKey: "order",
1169
+ timestamp: Date.now()
1170
+ })
1171
+ },
1172
+ {
1173
+ keywords: ["gi\u1ECF h\xE0ng", "cart", "mua"],
1174
+ response: () => ({
1175
+ conversationId: "",
1176
+ message: "\u0110\xE2y l\xE0 gi\u1ECF h\xE0ng c\u1EE7a b\u1EA1n!",
1177
+ intent: "viewCart",
1178
+ action: "show_ui",
1179
+ componentKey: "cart",
1180
+ timestamp: Date.now()
1181
+ })
1182
+ }
1183
+ ];
1184
+ function handleCartUpdated(items) {
1185
+ const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
1186
+ const totalQty = items.reduce((sum, i) => sum + i.qty, 0);
1187
+ const itemList = items.map((i) => `**${i.name}** \xD7${i.qty}`).join(", ");
1188
+ return {
1189
+ conversationId: "",
1190
+ message: `Gi\u1ECF h\xE0ng \u0111\xE3 \u0111\u01B0\u1EE3c c\u1EADp nh\u1EADt! Hi\u1EC7n c\xF3 ${totalQty} s\u1EA3n ph\u1EA9m (${itemList}) \u2014 t\u1ED5ng **${total.toLocaleString(
1191
+ "vi-VN"
1192
+ )}\u0111**. B\u1EA1n c\xF3 mu\u1ED1n ti\u1EBFn h\xE0nh thanh to\xE1n kh\xF4ng?`,
1193
+ intent: "cartUpdated",
1194
+ action: "none",
1195
+ suggest: {
1196
+ options: [
1197
+ {
1198
+ key: "checkout",
1199
+ label: "Thanh to\xE1n ngay",
1200
+ payload: { action: "checkout" }
1201
+ },
1202
+ { key: "continue", label: "Mua th\xEAm", payload: { action: "continue" } }
1203
+ ]
1204
+ },
1205
+ timestamp: Date.now()
1206
+ };
1207
+ }
1208
+ function handleContext(context) {
1209
+ if (context.type === "cart_updated" && Array.isArray(context.items)) {
1210
+ return handleCartUpdated(context.items);
1211
+ }
1212
+ if (context.action === "view_cart") {
1213
+ return {
1214
+ conversationId: "",
1215
+ message: "\u0110\xE2y l\xE0 gi\u1ECF h\xE0ng c\u1EE7a b\u1EA1n!",
1216
+ intent: "viewCart",
1217
+ action: "show_ui",
1218
+ componentKey: "cart",
1219
+ timestamp: Date.now()
1220
+ };
1221
+ }
1222
+ return null;
1223
+ }
1224
+ var defaultResponse = (message) => ({
1225
+ conversationId: "",
1226
+ message: `T\xF4i ch\u01B0a hi\u1EC3u r\xF5 y\xEAu c\u1EA7u "${message}". B\u1EA1n c\xF3 th\u1EC3 h\u1ECFi t\xF4i v\u1EC1 s\u1EA3n ph\u1EA9m, gi\u1ECF h\xE0ng ho\u1EB7c \u0111\u01A1n h\xE0ng nh\xE9!`,
1227
+ intent: "general",
1228
+ action: "none",
1229
+ timestamp: Date.now()
1230
+ });
1231
+ var mockHandler = (req, conversationId) => new Promise((resolve) => {
1232
+ setTimeout(() => {
1233
+ let data;
1234
+ if (req.context) {
1235
+ data = handleContext(req.context) ?? defaultResponse("");
1236
+ } else {
1237
+ const text = (req.message ?? "").toLowerCase();
1238
+ const matched = MOCK_RESPONSES.find(
1239
+ (r) => r.keywords.some((kw) => text.includes(kw))
1240
+ );
1241
+ data = matched ? matched.response(req) : defaultResponse(req.message ?? "");
1242
+ }
1243
+ data.conversationId = conversationId;
1244
+ resolve({ code: 200, message: "ok", data });
1245
+ }, 800);
1246
+ });
1247
+
1248
+ // src/transports/MockTransport.ts
1249
+ var CHUNK_DELAY_MS = 50;
1250
+ function delay(ms) {
1251
+ return new Promise((resolve) => setTimeout(resolve, ms));
1252
+ }
1253
+ var MockTransport = class {
1254
+ constructor(conversationId) {
1255
+ this.conversationId = conversationId;
1256
+ }
1257
+ onChunk(cb) {
1258
+ this.chunkCb = cb;
1259
+ }
1260
+ onDone(cb) {
1261
+ this.doneCb = cb;
1262
+ }
1263
+ onError(cb) {
1264
+ this.errorCb = cb;
1265
+ }
1266
+ connect() {
1267
+ return Promise.resolve();
1268
+ }
1269
+ disconnect() {
1270
+ }
1271
+ async send(request) {
1272
+ try {
1273
+ const response = await mockHandler(request, this.conversationId);
1274
+ const { data } = response;
1275
+ const words = data.message.split(" ");
1276
+ for (const word of words) {
1277
+ await delay(CHUNK_DELAY_MS);
1278
+ this.chunkCb?.(word + " ");
1279
+ }
1280
+ this.doneCb?.(data);
1281
+ } catch (err) {
1282
+ this.errorCb?.(err);
1283
+ }
1284
+ }
1285
+ };
1286
+
1287
+ // src/transports/WebSocketTransport.ts
1288
+ var MAX_RETRIES = 3;
1289
+ var BASE_RETRY_DELAY_MS = 1e3;
1290
+ var WebSocketTransport = class {
1291
+ constructor(url, token) {
1292
+ this.ws = null;
1293
+ this.retryCount = 0;
1294
+ this.url = url;
1295
+ this.token = token;
1296
+ }
1297
+ onChunk(cb) {
1298
+ this.chunkCb = cb;
1299
+ }
1300
+ onDone(cb) {
1301
+ this.doneCb = cb;
1302
+ }
1303
+ onError(cb) {
1304
+ this.errorCb = cb;
1305
+ }
1306
+ connect() {
1307
+ return new Promise((resolve, reject) => {
1308
+ const wsUrl = this.token ? `${this.url}?token=${this.token}` : this.url;
1309
+ this.ws = new WebSocket(wsUrl);
1310
+ this.ws.onopen = () => {
1311
+ this.retryCount = 0;
1312
+ resolve();
1313
+ };
1314
+ this.ws.onerror = () => {
1315
+ reject(
1316
+ new Error(`[WebSocketTransport] Failed to connect to ${this.url}`)
1317
+ );
1318
+ };
1319
+ this.ws.onclose = () => {
1320
+ if (this.retryCount < MAX_RETRIES) {
1321
+ this.retryCount++;
1322
+ const retryDelay = BASE_RETRY_DELAY_MS * this.retryCount;
1323
+ setTimeout(() => this.connect(), retryDelay);
1324
+ }
1325
+ };
1326
+ this.ws.onmessage = (event) => {
1327
+ try {
1328
+ const frame = JSON.parse(event.data);
1329
+ if (frame.type === "chunk") {
1330
+ this.chunkCb?.(frame.text);
1331
+ } else if (frame.type === "done") {
1332
+ this.doneCb?.(frame.data);
1333
+ } else if (frame.type === "error") {
1334
+ this.errorCb?.(new Error(frame.message));
1335
+ }
1336
+ } catch {
1337
+ this.errorCb?.(
1338
+ new Error("[WebSocketTransport] Failed to parse frame")
1339
+ );
1340
+ }
1341
+ };
1342
+ });
1343
+ }
1344
+ send(request) {
1345
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1346
+ this.errorCb?.(new Error("[WebSocketTransport] Connection not open"));
1347
+ return;
1348
+ }
1349
+ this.ws.send(JSON.stringify(request));
1350
+ }
1351
+ disconnect() {
1352
+ this.retryCount = MAX_RETRIES;
1353
+ this.ws?.close();
1354
+ this.ws = null;
1355
+ }
1356
+ };
1357
+
1358
+ // src/transports/createTransport.ts
1359
+ function createTransport(bffUrl, conversationId, token) {
1360
+ if (!bffUrl) {
1361
+ return new MockTransport(conversationId);
1362
+ }
1363
+ if (bffUrl.startsWith("wss://") || bffUrl.startsWith("ws://")) {
1364
+ return new WebSocketTransport(bffUrl, token);
1365
+ }
1366
+ return new HttpStreamTransport(bffUrl, token);
1367
+ }
1368
+
1369
+ // src/hooks/useChatSession.ts
1370
+ async function getTekoAuth() {
1371
+ try {
1372
+ const { default: TekoID } = await import("teko-oauth2");
1373
+ if (!TekoID.user.isLoggedIn()) return {};
1374
+ return {
1375
+ userId: TekoID.user.getUserInfo().sub,
1376
+ token: TekoID.user.getAccessToken()
1377
+ };
1378
+ } catch {
1379
+ return {};
1380
+ }
1381
+ }
1382
+ var generateId = () => Math.random().toString(36).slice(2, 10);
1383
+ var generateConversationId = () => `conv-${Date.now()}-${generateId()}`;
1384
+ var useChatSession = ({
1385
+ chatBffUrl,
1386
+ onIntent,
1387
+ onDebugEvent,
1388
+ getAppContext
1389
+ }) => {
1390
+ const [messages, setMessages] = useState4([]);
1391
+ const [isLoading, setIsLoading] = useState4(false);
1392
+ const conversationIdRef = useRef4(generateConversationId());
1393
+ const addMessage = (msg) => setMessages((prev) => [...prev, msg]);
1394
+ const send = useCallback3(
1395
+ async ({ message, context }) => {
1396
+ if (!message && !context) return;
1397
+ if (message) {
1398
+ addMessage({
1399
+ id: generateId(),
1400
+ role: "user",
1401
+ content: message,
1402
+ timestamp: Date.now()
1403
+ });
1404
+ }
1405
+ setIsLoading(true);
1406
+ const { userId, token } = await getTekoAuth();
1407
+ let appContext;
1408
+ if (getAppContext) {
1409
+ const resolved = await getAppContext();
1410
+ if (Object.keys(resolved).length > 0) {
1411
+ appContext = resolved;
1412
+ }
1413
+ }
1414
+ const req = {
1415
+ conversationId: conversationIdRef.current,
1416
+ timestamp: Date.now(),
1417
+ ...userId && { userId },
1418
+ ...message && { message },
1419
+ ...context && { context },
1420
+ ...appContext && { appContext }
1421
+ };
1422
+ onDebugEvent?.({ type: "request", timestamp: Date.now(), payload: req });
1423
+ const transport = createTransport(
1424
+ chatBffUrl,
1425
+ conversationIdRef.current,
1426
+ token
1427
+ );
1428
+ const streamingIdRef = { current: generateId() };
1429
+ let firstChunk = true;
1430
+ transport.onChunk((text) => {
1431
+ setMessages((prev) => {
1432
+ if (firstChunk) {
1433
+ firstChunk = false;
1434
+ return [
1435
+ ...prev,
1436
+ {
1437
+ id: streamingIdRef.current,
1438
+ role: "ai",
1439
+ content: text,
1440
+ isStreaming: true,
1441
+ timestamp: Date.now()
1442
+ }
1443
+ ];
1444
+ }
1445
+ const last = prev[prev.length - 1];
1446
+ if (last?.id === streamingIdRef.current && last.isStreaming) {
1447
+ return [
1448
+ ...prev.slice(0, -1),
1449
+ { ...last, content: last.content + text }
1450
+ ];
1451
+ }
1452
+ return prev;
1453
+ });
1454
+ });
1455
+ transport.onDone((data) => {
1456
+ conversationIdRef.current = data.conversationId || conversationIdRef.current;
1457
+ onDebugEvent?.({
1458
+ type: "response",
1459
+ timestamp: Date.now(),
1460
+ payload: data
1461
+ });
1462
+ if (data.action !== "none") {
1463
+ const componentKey = data.componentKey ?? data.path;
1464
+ onDebugEvent?.({
1465
+ type: "intent",
1466
+ timestamp: Date.now(),
1467
+ payload: { action: data.action, componentKey }
1468
+ });
1469
+ onIntent?.(data.action, componentKey);
1470
+ }
1471
+ setMessages((prev) => {
1472
+ const last = prev[prev.length - 1];
1473
+ if (last?.id === streamingIdRef.current) {
1474
+ return [
1475
+ ...prev.slice(0, -1),
1476
+ {
1477
+ ...last,
1478
+ content: data.message,
1479
+ isStreaming: false,
1480
+ options: data.suggest?.options,
1481
+ timestamp: data.timestamp
1482
+ }
1483
+ ];
1484
+ }
1485
+ if (data.message) {
1486
+ return [
1487
+ ...prev,
1488
+ {
1489
+ id: streamingIdRef.current,
1490
+ role: "ai",
1491
+ content: data.message,
1492
+ isStreaming: false,
1493
+ options: data.suggest?.options,
1494
+ timestamp: data.timestamp
1495
+ }
1496
+ ];
1497
+ }
1498
+ return prev;
1499
+ });
1500
+ setIsLoading(false);
1501
+ transport.disconnect();
1502
+ });
1503
+ transport.onError((err) => {
1504
+ addMessage({
1505
+ id: generateId(),
1506
+ role: "ai",
1507
+ content: "\u0110\xE3 x\u1EA3y ra l\u1ED7i khi k\u1EBFt n\u1ED1i. Vui l\xF2ng th\u1EED l\u1EA1i.",
1508
+ timestamp: Date.now()
1509
+ });
1510
+ console.error("[TekoChatSDK] transport error:", err);
1511
+ setIsLoading(false);
1512
+ transport.disconnect();
1513
+ });
1514
+ try {
1515
+ await transport.connect();
1516
+ transport.send(req);
1517
+ } catch (err) {
1518
+ addMessage({
1519
+ id: generateId(),
1520
+ role: "ai",
1521
+ content: "\u0110\xE3 x\u1EA3y ra l\u1ED7i khi k\u1EBFt n\u1ED1i. Vui l\xF2ng th\u1EED l\u1EA1i.",
1522
+ timestamp: Date.now()
1523
+ });
1524
+ console.error("[TekoChatSDK] connect error:", err);
1525
+ setIsLoading(false);
1526
+ }
1527
+ },
1528
+ [chatBffUrl, onIntent, onDebugEvent, getAppContext]
1529
+ );
1530
+ const contextDebounceRef = useRef4(null);
1531
+ const sendMessage = useCallback3(
1532
+ (text, context) => send({ message: text, ...context && { context } }),
1533
+ [send]
1534
+ );
1535
+ const sendContext = useCallback3(
1536
+ (data) => {
1537
+ onDebugEvent?.({
1538
+ type: "context",
1539
+ timestamp: Date.now(),
1540
+ payload: data
1541
+ });
1542
+ if (contextDebounceRef.current) clearTimeout(contextDebounceRef.current);
1543
+ contextDebounceRef.current = setTimeout(
1544
+ () => send({ context: data }),
1545
+ 500
1546
+ );
1547
+ },
1548
+ [send, onDebugEvent]
1549
+ );
1550
+ return { messages, isLoading, sendMessage, sendContext };
1551
+ };
1552
+
1553
+ // src/locales/index.ts
1554
+ var BUILT_IN_LABELS = {
1555
+ vi: {
1556
+ agentName: "T\u01B0 v\u1EA5n vi\xEAn AI",
1557
+ agentStatus: "\u25CF Tr\u1EF1c tuy\u1EBFn",
1558
+ close: "\u0110\xF3ng",
1559
+ minimize: "Thu g\u1ECDn",
1560
+ backToChat: "Tr\u1EDF v\u1EC1 khung chat",
1561
+ viewContent: "Xem n\u1ED9i dung",
1562
+ loadingContent: "\u0110ang t\u1EA3i n\u1ED9i dung...",
1563
+ inputPlaceholder: "Nh\u1EADp tin nh\u1EAFn...",
1564
+ send: "G\u1EEDi",
1565
+ emptyState: "Xin ch\xE0o! T\xF4i c\xF3 th\u1EC3 gi\xFAp b\u1EA1n."
1566
+ },
1567
+ en: {
1568
+ agentName: "AI Assistant",
1569
+ agentStatus: "\u25CF Online",
1570
+ close: "Close",
1571
+ minimize: "Minimize",
1572
+ backToChat: "Back to chat",
1573
+ viewContent: "View content",
1574
+ loadingContent: "Loading...",
1575
+ inputPlaceholder: "Type a message...",
1576
+ send: "Send",
1577
+ emptyState: "Hello! How can I help you?"
1578
+ }
1579
+ };
1580
+ function resolveLabels(locale = "vi", overrides) {
1581
+ return { ...BUILT_IN_LABELS[locale], ...overrides };
1582
+ }
1583
+
1584
+ // src/types.ts
1585
+ var SDK_ACTIONS = {
1586
+ SHOW_UI: "show_ui"
1587
+ };
1588
+
1589
+ // src/components/TekoChatWidget.tsx
1590
+ import { Fragment as Fragment3, jsx as jsx7 } from "react/jsx-runtime";
1591
+ var ANCHOR_TO_ORIGIN = {
1592
+ "bottom-right": "bottom right",
1593
+ "bottom-left": "bottom left",
1594
+ "top-right": "top right",
1595
+ "top-left": "top left"
1596
+ };
1597
+ var SHOW_UI_TRANSITION_DELAY_MS = 1e3;
1598
+ var TekoChatWidget = forwardRef(
1599
+ ({
1600
+ appId,
1601
+ chatBffUrl,
1602
+ onIntent,
1603
+ renderRightPanel,
1604
+ renderBubble,
1605
+ bubbleAnchor = "bottom-right",
1606
+ onDebugEvent,
1607
+ zIndex = 1031,
1608
+ offsetTop = 0,
1609
+ offsetBottom = 0,
1610
+ miniWidth = 360,
1611
+ miniHeight = 480,
1612
+ primaryColor = DEFAULT_PRIMARY_COLOR,
1613
+ layoutMode = "desktop",
1614
+ locale = "vi",
1615
+ labels,
1616
+ botAvatar,
1617
+ getAppContext
1618
+ }, ref) => {
1619
+ const resolvedLabels = resolveLabels(locale, labels);
1620
+ const [chatState, setChatState] = useState5("bubble");
1621
+ const [activeComponentKey, setActiveComponentKey] = useState5("");
1622
+ const [showRightPanelTrigger, setShowRightPanelTrigger] = useState5(0);
1623
+ const handleIntent = useCallback4(
1624
+ (action, componentKey) => {
1625
+ if (action === SDK_ACTIONS.SHOW_UI && componentKey) {
1626
+ setActiveComponentKey(componentKey);
1627
+ setTimeout(() => {
1628
+ setChatState("fullscreen");
1629
+ setShowRightPanelTrigger((n) => n + 1);
1630
+ onIntent?.(action, componentKey);
1631
+ }, SHOW_UI_TRANSITION_DELAY_MS);
1632
+ } else {
1633
+ onIntent?.(action, componentKey);
1634
+ }
1635
+ },
1636
+ [onIntent]
1637
+ );
1638
+ const { messages, isLoading, sendMessage, sendContext } = useChatSession({
1639
+ appId,
1640
+ chatBffUrl,
1641
+ onIntent: handleIntent,
1642
+ onDebugEvent,
1643
+ getAppContext
1644
+ });
1645
+ useImperativeHandle(ref, () => ({
1646
+ sendContext,
1647
+ sendMessage,
1648
+ open: () => setChatState("mini"),
1649
+ close: () => setChatState("bubble"),
1650
+ openFullscreen: (componentKey) => {
1651
+ setActiveComponentKey(componentKey);
1652
+ setChatState("fullscreen");
1653
+ },
1654
+ closeFullscreen: () => setChatState("mini")
1655
+ }));
1656
+ const handleOptionClick = useCallback4(
1657
+ (option) => sendMessage(
1658
+ option.label,
1659
+ option.payload
1660
+ ),
1661
+ [sendMessage]
1662
+ );
1663
+ const handleOpen = () => setChatState("mini");
1664
+ let content;
1665
+ if (chatState === "bubble") {
1666
+ content = renderBubble ? /* @__PURE__ */ jsx7(Fragment3, { children: renderBubble(handleOpen) }) : /* @__PURE__ */ jsx7(
1667
+ ChatBubble,
1668
+ {
1669
+ onClick: handleOpen,
1670
+ offsetBottom,
1671
+ zIndex
1672
+ }
1673
+ );
1674
+ } else if (chatState === "mini") {
1675
+ content = /* @__PURE__ */ jsx7(
1676
+ ChatMiniPopup,
1677
+ {
1678
+ messages,
1679
+ isLoading,
1680
+ onSend: sendMessage,
1681
+ onOptionClick: handleOptionClick,
1682
+ onClose: () => setChatState("bubble"),
1683
+ offsetBottom,
1684
+ zIndex,
1685
+ miniWidth,
1686
+ miniHeight,
1687
+ layoutMode,
1688
+ labels: resolvedLabels,
1689
+ botAvatar
1690
+ }
1691
+ );
1692
+ } else {
1693
+ content = /* @__PURE__ */ jsx7(
1694
+ ChatFullscreen,
1695
+ {
1696
+ messages,
1697
+ isLoading,
1698
+ componentKey: activeComponentKey,
1699
+ onSend: sendMessage,
1700
+ onOptionClick: handleOptionClick,
1701
+ onClose: () => setChatState("bubble"),
1702
+ onMinimize: () => setChatState("mini"),
1703
+ renderRightPanel,
1704
+ transformOrigin: ANCHOR_TO_ORIGIN[bubbleAnchor],
1705
+ bubbleAnchor,
1706
+ offsetTop,
1707
+ offsetBottom,
1708
+ zIndex: zIndex + 9,
1709
+ layoutMode,
1710
+ showRightPanelTrigger,
1711
+ labels: resolvedLabels,
1712
+ botAvatar
1713
+ }
1714
+ );
1715
+ }
1716
+ return /* @__PURE__ */ jsx7(ChatThemeContext.Provider, { value: { primaryColor }, children: content });
1717
+ }
1718
+ );
1719
+ TekoChatWidget.displayName = "TekoChatWidget";
1720
+ export {
1721
+ TekoChatWidget
1722
+ };