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