@hissuno/widget 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2943 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ChatBubble: () => ChatBubble,
24
+ ChatDialog: () => ChatDialog,
25
+ ChatMessages: () => ChatMessages,
26
+ ChatPopup: () => ChatPopup,
27
+ ChatSidepanel: () => ChatSidepanel,
28
+ ConversationHistory: () => ConversationHistory,
29
+ DrawerBadge: () => DrawerBadge,
30
+ HissunoWidget: () => HissunoWidget,
31
+ HistoryIcon: () => HistoryIcon,
32
+ SupportWidget: () => HissunoWidget,
33
+ formatShortcut: () => formatShortcut,
34
+ useFocusTrap: () => useFocusTrap,
35
+ useHissunoChat: () => useHissunoChat,
36
+ useKeyboardShortcut: () => useKeyboardShortcut,
37
+ useResolvedTheme: () => useResolvedTheme
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/HissunoWidget.tsx
42
+ var import_react7 = require("react");
43
+
44
+ // src/shared/Icons.tsx
45
+ var import_jsx_runtime = require("react/jsx-runtime");
46
+ function CloseIcon({ size = 20 }) {
47
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
48
+ "svg",
49
+ {
50
+ xmlns: "http://www.w3.org/2000/svg",
51
+ width: size,
52
+ height: size,
53
+ viewBox: "0 0 24 24",
54
+ fill: "none",
55
+ stroke: "currentColor",
56
+ strokeWidth: "2",
57
+ strokeLinecap: "round",
58
+ strokeLinejoin: "round",
59
+ children: [
60
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
61
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
62
+ ]
63
+ }
64
+ );
65
+ }
66
+ function NewThreadIcon() {
67
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
68
+ "svg",
69
+ {
70
+ xmlns: "http://www.w3.org/2000/svg",
71
+ width: "18",
72
+ height: "18",
73
+ viewBox: "0 0 24 24",
74
+ fill: "none",
75
+ stroke: "currentColor",
76
+ strokeWidth: "2",
77
+ strokeLinecap: "round",
78
+ strokeLinejoin: "round",
79
+ children: [
80
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 20h9" }),
81
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" })
82
+ ]
83
+ }
84
+ );
85
+ }
86
+ function SpinnerIcon() {
87
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
88
+ "svg",
89
+ {
90
+ xmlns: "http://www.w3.org/2000/svg",
91
+ width: "18",
92
+ height: "18",
93
+ viewBox: "0 0 24 24",
94
+ fill: "none",
95
+ stroke: "currentColor",
96
+ strokeWidth: "2",
97
+ strokeLinecap: "round",
98
+ strokeLinejoin: "round",
99
+ style: { animation: "hissuno-spin 1s linear infinite" },
100
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
101
+ }
102
+ );
103
+ }
104
+ function ChatIcon({ size = 24 }) {
105
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
106
+ "svg",
107
+ {
108
+ xmlns: "http://www.w3.org/2000/svg",
109
+ width: size,
110
+ height: size,
111
+ viewBox: "0 0 24 24",
112
+ fill: "none",
113
+ stroke: "currentColor",
114
+ strokeWidth: "2",
115
+ strokeLinecap: "round",
116
+ strokeLinejoin: "round",
117
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
118
+ }
119
+ );
120
+ }
121
+ function TrashIcon() {
122
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
123
+ "svg",
124
+ {
125
+ xmlns: "http://www.w3.org/2000/svg",
126
+ width: "16",
127
+ height: "16",
128
+ viewBox: "0 0 24 24",
129
+ fill: "none",
130
+ stroke: "currentColor",
131
+ strokeWidth: "2",
132
+ strokeLinecap: "round",
133
+ strokeLinejoin: "round",
134
+ children: [
135
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "3 6 5 6 21 6" }),
136
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
137
+ ]
138
+ }
139
+ );
140
+ }
141
+ function HistoryIcon() {
142
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
143
+ "svg",
144
+ {
145
+ xmlns: "http://www.w3.org/2000/svg",
146
+ width: "18",
147
+ height: "18",
148
+ viewBox: "0 0 24 24",
149
+ fill: "none",
150
+ stroke: "currentColor",
151
+ strokeWidth: "2",
152
+ strokeLinecap: "round",
153
+ strokeLinejoin: "round",
154
+ children: [
155
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
156
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3 3v5h5" }),
157
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 7v5l4 2" })
158
+ ]
159
+ }
160
+ );
161
+ }
162
+
163
+ // src/triggers/ChatBubble.tsx
164
+ var import_jsx_runtime2 = require("react/jsx-runtime");
165
+ function getPositionStyles(position, offset) {
166
+ const x = offset.x ?? 20;
167
+ const y = offset.y ?? 20;
168
+ switch (position) {
169
+ case "bottom-left":
170
+ return { bottom: y, left: x };
171
+ case "top-right":
172
+ return { top: y, right: x };
173
+ case "top-left":
174
+ return { top: y, left: x };
175
+ case "bottom-right":
176
+ default:
177
+ return { bottom: y, right: x };
178
+ }
179
+ }
180
+ function ChatBubble({
181
+ isOpen,
182
+ onClick,
183
+ position = "bottom-right",
184
+ offset = {},
185
+ theme = "light"
186
+ }) {
187
+ const positionStyles = getPositionStyles(position, offset);
188
+ const isDark = theme === "dark";
189
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
190
+ "button",
191
+ {
192
+ type: "button",
193
+ onClick,
194
+ className: "hissuno-bubble",
195
+ style: {
196
+ position: "fixed",
197
+ ...positionStyles,
198
+ width: 56,
199
+ height: 56,
200
+ borderRadius: "50%",
201
+ border: "none",
202
+ cursor: "pointer",
203
+ display: "flex",
204
+ alignItems: "center",
205
+ justifyContent: "center",
206
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
207
+ transition: "transform 0.2s ease, box-shadow 0.2s ease",
208
+ zIndex: 9998,
209
+ backgroundColor: isDark ? "#1a1a1a" : "#2563eb",
210
+ color: "#ffffff"
211
+ },
212
+ "aria-label": isOpen ? "Close chat" : "Open chat",
213
+ children: isOpen ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CloseIcon, { size: 24 }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ChatIcon, { size: 24 })
214
+ }
215
+ );
216
+ }
217
+
218
+ // src/triggers/DrawerBadge.tsx
219
+ var import_jsx_runtime3 = require("react/jsx-runtime");
220
+ function DrawerBadge({
221
+ isOpen,
222
+ onClick,
223
+ label = "Support",
224
+ theme = "light"
225
+ }) {
226
+ const isDark = theme === "dark";
227
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
228
+ "button",
229
+ {
230
+ type: "button",
231
+ onClick,
232
+ className: "hissuno-drawer-badge",
233
+ style: {
234
+ position: "fixed",
235
+ right: 0,
236
+ top: "50%",
237
+ transform: "translateY(-50%)",
238
+ width: 40,
239
+ minHeight: 100,
240
+ padding: "16px 8px",
241
+ border: "none",
242
+ borderRadius: "8px 0 0 8px",
243
+ cursor: "pointer",
244
+ display: "flex",
245
+ alignItems: "center",
246
+ justifyContent: "center",
247
+ boxShadow: "-2px 0 8px rgba(0, 0, 0, 0.15)",
248
+ transition: "transform 0.2s ease, box-shadow 0.2s ease",
249
+ zIndex: 9998,
250
+ backgroundColor: isDark ? "#1a1a1a" : "#2563eb",
251
+ color: "#ffffff",
252
+ writingMode: "vertical-rl",
253
+ textOrientation: "mixed",
254
+ fontFamily: "system-ui, -apple-system, sans-serif",
255
+ fontSize: 14,
256
+ fontWeight: 500,
257
+ letterSpacing: "0.02em"
258
+ },
259
+ "aria-label": isOpen ? "Close chat" : "Open chat",
260
+ onMouseEnter: (e) => {
261
+ e.currentTarget.style.transform = "translateY(-50%) translateX(-4px)";
262
+ e.currentTarget.style.boxShadow = "-4px 0 12px rgba(0, 0, 0, 0.2)";
263
+ },
264
+ onMouseLeave: (e) => {
265
+ e.currentTarget.style.transform = "translateY(-50%)";
266
+ e.currentTarget.style.boxShadow = "-2px 0 8px rgba(0, 0, 0, 0.15)";
267
+ },
268
+ children: isOpen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
269
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChatIcon, { size: 16 }) }),
270
+ label
271
+ ] })
272
+ }
273
+ );
274
+ }
275
+
276
+ // src/shared/ChatMessages.tsx
277
+ var import_react = require("react");
278
+ var import_jsx_runtime4 = require("react/jsx-runtime");
279
+ function MessageBubble({ message, theme = "light" }) {
280
+ const isUser = message.role === "user";
281
+ const isHumanAgent = message.senderType === "human_agent";
282
+ const isDark = theme === "dark";
283
+ const userBg = "#2563eb";
284
+ const userColor = "#ffffff";
285
+ const assistantBg = isDark ? "#2a2a2a" : "#f3f4f6";
286
+ const assistantColor = isDark ? "#e5e5e5" : "#1f2937";
287
+ const humanAgentBg = isDark ? "#4c1d95" : "#ede9fe";
288
+ const humanAgentColor = isDark ? "#e5e5e5" : "#1f2937";
289
+ const humanAgentBorder = "#8b5cf6";
290
+ const senderLabel = isUser ? "You" : isHumanAgent ? "Support agent" : "Assistant";
291
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
292
+ "article",
293
+ {
294
+ role: "article",
295
+ "aria-label": `Message from ${senderLabel}`,
296
+ style: {
297
+ display: "flex",
298
+ justifyContent: isUser ? "flex-end" : "flex-start",
299
+ marginBottom: 12
300
+ },
301
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
302
+ "div",
303
+ {
304
+ style: {
305
+ maxWidth: "80%",
306
+ padding: "10px 14px",
307
+ borderRadius: 12,
308
+ backgroundColor: isUser ? userBg : isHumanAgent ? humanAgentBg : assistantBg,
309
+ color: isUser ? userColor : isHumanAgent ? humanAgentColor : assistantColor,
310
+ fontSize: 14,
311
+ lineHeight: 1.5,
312
+ whiteSpace: "pre-wrap",
313
+ wordBreak: "break-word",
314
+ border: isHumanAgent ? `1px solid ${humanAgentBorder}` : "none"
315
+ },
316
+ children: [
317
+ isHumanAgent && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
318
+ "div",
319
+ {
320
+ style: {
321
+ display: "flex",
322
+ alignItems: "center",
323
+ gap: 4,
324
+ marginBottom: 6,
325
+ fontSize: 11,
326
+ fontWeight: 600,
327
+ color: "#8b5cf6",
328
+ textTransform: "uppercase",
329
+ letterSpacing: "0.05em"
330
+ },
331
+ children: [
332
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
333
+ "svg",
334
+ {
335
+ width: "12",
336
+ height: "12",
337
+ viewBox: "0 0 24 24",
338
+ fill: "none",
339
+ stroke: "currentColor",
340
+ strokeWidth: "2",
341
+ strokeLinecap: "round",
342
+ strokeLinejoin: "round",
343
+ children: [
344
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" }),
345
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: "12", cy: "7", r: "4" })
346
+ ]
347
+ }
348
+ ),
349
+ "Human Agent"
350
+ ]
351
+ }
352
+ ),
353
+ message.content
354
+ ]
355
+ }
356
+ )
357
+ }
358
+ );
359
+ }
360
+ function LoadingIndicator({ theme = "light" }) {
361
+ const isDark = theme === "dark";
362
+ const dotColor = isDark ? "#666666" : "#9ca3af";
363
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
364
+ "div",
365
+ {
366
+ role: "status",
367
+ "aria-live": "polite",
368
+ "aria-label": "Assistant is typing",
369
+ style: { display: "flex", justifyContent: "flex-start", marginBottom: 12 },
370
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
371
+ "div",
372
+ {
373
+ style: {
374
+ display: "flex",
375
+ alignItems: "center",
376
+ gap: 4,
377
+ padding: "10px 14px",
378
+ borderRadius: 12,
379
+ backgroundColor: isDark ? "#2a2a2a" : "#f3f4f6"
380
+ },
381
+ children: [
382
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
383
+ "span",
384
+ {
385
+ className: "hissuno-loading-dot",
386
+ style: {
387
+ width: 8,
388
+ height: 8,
389
+ borderRadius: "50%",
390
+ backgroundColor: dotColor,
391
+ animation: "hissuno-bounce 1s infinite",
392
+ animationDelay: "-0.3s"
393
+ }
394
+ }
395
+ ),
396
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
397
+ "span",
398
+ {
399
+ className: "hissuno-loading-dot",
400
+ style: {
401
+ width: 8,
402
+ height: 8,
403
+ borderRadius: "50%",
404
+ backgroundColor: dotColor,
405
+ animation: "hissuno-bounce 1s infinite",
406
+ animationDelay: "-0.15s"
407
+ }
408
+ }
409
+ ),
410
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
411
+ "span",
412
+ {
413
+ className: "hissuno-loading-dot",
414
+ style: {
415
+ width: 8,
416
+ height: 8,
417
+ borderRadius: "50%",
418
+ backgroundColor: dotColor,
419
+ animation: "hissuno-bounce 1s infinite"
420
+ }
421
+ }
422
+ )
423
+ ]
424
+ }
425
+ )
426
+ }
427
+ );
428
+ }
429
+ function ChatMessages({
430
+ messages,
431
+ isLoading,
432
+ isStreaming = false,
433
+ streamingContent = "",
434
+ theme = "light"
435
+ }) {
436
+ const messagesEndRef = (0, import_react.useRef)(null);
437
+ (0, import_react.useEffect)(() => {
438
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
439
+ }, [messages, isLoading, streamingContent]);
440
+ const showLoading = isLoading && messages.length > 0 && messages[messages.length - 1].role === "user" && !streamingContent;
441
+ const showStreamingBubble = isStreaming && streamingContent;
442
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
443
+ "div",
444
+ {
445
+ className: "hissuno-messages",
446
+ role: "log",
447
+ "aria-label": "Chat messages",
448
+ "aria-live": "polite",
449
+ "aria-relevant": "additions",
450
+ style: {
451
+ flex: 1,
452
+ overflowY: "auto",
453
+ padding: 16
454
+ },
455
+ children: [
456
+ messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MessageBubble, { message, theme }, message.id)),
457
+ showLoading && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LoadingIndicator, { theme }),
458
+ showStreamingBubble && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { "aria-live": "polite", "aria-atomic": "false", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
459
+ MessageBubble,
460
+ {
461
+ message: {
462
+ id: "streaming",
463
+ role: "assistant",
464
+ content: streamingContent
465
+ },
466
+ theme
467
+ }
468
+ ) }),
469
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: messagesEndRef })
470
+ ]
471
+ }
472
+ );
473
+ }
474
+
475
+ // src/shared/ConversationHistory.tsx
476
+ var import_jsx_runtime5 = require("react/jsx-runtime");
477
+ function formatDate(dateString) {
478
+ const date = new Date(dateString);
479
+ const now = /* @__PURE__ */ new Date();
480
+ const diffMs = now.getTime() - date.getTime();
481
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
482
+ if (diffDays === 0) {
483
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
484
+ } else if (diffDays === 1) {
485
+ return "Yesterday";
486
+ } else if (diffDays < 7) {
487
+ return date.toLocaleDateString([], { weekday: "short" });
488
+ } else {
489
+ return date.toLocaleDateString([], { month: "short", day: "numeric" });
490
+ }
491
+ }
492
+ function ConversationHistory({
493
+ isOpen,
494
+ onClose,
495
+ sessions,
496
+ onSelectSession,
497
+ onDeleteSession,
498
+ currentSessionId,
499
+ theme = "light"
500
+ }) {
501
+ const isDark = theme === "dark";
502
+ const bgColor = isDark ? "#1a1a1a" : "#ffffff";
503
+ const borderColor = isDark ? "#333333" : "#e5e7eb";
504
+ const textColor = isDark ? "#e5e5e5" : "#1f2937";
505
+ const secondaryTextColor = isDark ? "#999999" : "#6b7280";
506
+ const hoverBg = isDark ? "#2a2a2a" : "#f3f4f6";
507
+ const activeBg = isDark ? "#333333" : "#e5e7eb";
508
+ const backdropColor = "rgba(0, 0, 0, 0.3)";
509
+ const handleBackdropClick = (e) => {
510
+ if (e.target === e.currentTarget) {
511
+ onClose();
512
+ }
513
+ };
514
+ const handleSessionClick = (sessionId) => {
515
+ onSelectSession(sessionId);
516
+ onClose();
517
+ };
518
+ const handleDeleteClick = (e, sessionId) => {
519
+ e.stopPropagation();
520
+ onDeleteSession(sessionId);
521
+ };
522
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
523
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
524
+ "div",
525
+ {
526
+ onClick: handleBackdropClick,
527
+ style: {
528
+ position: "absolute",
529
+ top: 0,
530
+ left: 0,
531
+ right: 0,
532
+ bottom: 0,
533
+ backgroundColor: backdropColor,
534
+ opacity: isOpen ? 1 : 0,
535
+ visibility: isOpen ? "visible" : "hidden",
536
+ transition: "opacity 0.2s ease-in-out, visibility 0.2s ease-in-out",
537
+ zIndex: 1
538
+ }
539
+ }
540
+ ),
541
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
542
+ "div",
543
+ {
544
+ style: {
545
+ position: "absolute",
546
+ top: 0,
547
+ left: 0,
548
+ bottom: 0,
549
+ width: "80%",
550
+ maxWidth: 300,
551
+ backgroundColor: bgColor,
552
+ borderRight: `1px solid ${borderColor}`,
553
+ transform: isOpen ? "translateX(0)" : "translateX(-100%)",
554
+ transition: "transform 0.25s ease-in-out",
555
+ zIndex: 2,
556
+ display: "flex",
557
+ flexDirection: "column",
558
+ overflow: "hidden"
559
+ },
560
+ children: [
561
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
562
+ "div",
563
+ {
564
+ style: {
565
+ display: "flex",
566
+ alignItems: "center",
567
+ justifyContent: "space-between",
568
+ padding: "16px 16px",
569
+ borderBottom: `1px solid ${borderColor}`
570
+ },
571
+ children: [
572
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
573
+ "h3",
574
+ {
575
+ style: {
576
+ margin: 0,
577
+ fontSize: 15,
578
+ fontWeight: 600,
579
+ color: textColor,
580
+ fontFamily: "system-ui, -apple-system, sans-serif"
581
+ },
582
+ children: "Conversations"
583
+ }
584
+ ),
585
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
586
+ "button",
587
+ {
588
+ type: "button",
589
+ onClick: onClose,
590
+ style: {
591
+ background: "none",
592
+ border: "none",
593
+ cursor: "pointer",
594
+ padding: 4,
595
+ display: "flex",
596
+ alignItems: "center",
597
+ justifyContent: "center",
598
+ color: secondaryTextColor,
599
+ borderRadius: 4
600
+ },
601
+ "aria-label": "Close history",
602
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CloseIcon, { size: 18 })
603
+ }
604
+ )
605
+ ]
606
+ }
607
+ ),
608
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
609
+ "div",
610
+ {
611
+ style: {
612
+ flex: 1,
613
+ overflowY: "auto",
614
+ padding: "8px 0"
615
+ },
616
+ children: sessions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
617
+ "div",
618
+ {
619
+ style: {
620
+ padding: "24px 16px",
621
+ textAlign: "center",
622
+ color: secondaryTextColor,
623
+ fontSize: 14,
624
+ fontFamily: "system-ui, -apple-system, sans-serif"
625
+ },
626
+ children: "No previous conversations"
627
+ }
628
+ ) : sessions.map((session) => {
629
+ const isActive = session.sessionId === currentSessionId;
630
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
631
+ "div",
632
+ {
633
+ onClick: () => handleSessionClick(session.sessionId),
634
+ style: {
635
+ display: "flex",
636
+ alignItems: "center",
637
+ padding: "12px 16px",
638
+ cursor: "pointer",
639
+ backgroundColor: isActive ? activeBg : "transparent",
640
+ borderLeft: isActive ? "3px solid #2563eb" : "3px solid transparent",
641
+ transition: "background-color 0.15s ease"
642
+ },
643
+ onMouseEnter: (e) => {
644
+ if (!isActive) {
645
+ e.currentTarget.style.backgroundColor = hoverBg;
646
+ }
647
+ },
648
+ onMouseLeave: (e) => {
649
+ if (!isActive) {
650
+ e.currentTarget.style.backgroundColor = "transparent";
651
+ }
652
+ },
653
+ children: [
654
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
655
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
656
+ "div",
657
+ {
658
+ style: {
659
+ fontSize: 14,
660
+ fontWeight: 500,
661
+ color: textColor,
662
+ fontFamily: "system-ui, -apple-system, sans-serif",
663
+ whiteSpace: "nowrap",
664
+ overflow: "hidden",
665
+ textOverflow: "ellipsis"
666
+ },
667
+ children: session.title
668
+ }
669
+ ),
670
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
671
+ "div",
672
+ {
673
+ style: {
674
+ fontSize: 12,
675
+ color: secondaryTextColor,
676
+ fontFamily: "system-ui, -apple-system, sans-serif",
677
+ marginTop: 2
678
+ },
679
+ children: [
680
+ formatDate(session.lastMessageAt),
681
+ " \xB7 ",
682
+ session.messageCount,
683
+ " messages"
684
+ ]
685
+ }
686
+ )
687
+ ] }),
688
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
689
+ "button",
690
+ {
691
+ type: "button",
692
+ onClick: (e) => handleDeleteClick(e, session.sessionId),
693
+ style: {
694
+ background: "none",
695
+ border: "none",
696
+ cursor: "pointer",
697
+ padding: 4,
698
+ display: "flex",
699
+ alignItems: "center",
700
+ justifyContent: "center",
701
+ color: secondaryTextColor,
702
+ borderRadius: 4,
703
+ opacity: 0.6,
704
+ marginLeft: 8,
705
+ flexShrink: 0
706
+ },
707
+ "aria-label": "Delete conversation",
708
+ title: "Delete conversation",
709
+ onMouseEnter: (e) => {
710
+ e.currentTarget.style.opacity = "1";
711
+ e.currentTarget.style.color = "#dc2626";
712
+ },
713
+ onMouseLeave: (e) => {
714
+ e.currentTarget.style.opacity = "0.6";
715
+ e.currentTarget.style.color = secondaryTextColor;
716
+ },
717
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TrashIcon, {})
718
+ }
719
+ )
720
+ ]
721
+ },
722
+ session.sessionId
723
+ );
724
+ })
725
+ }
726
+ )
727
+ ]
728
+ }
729
+ )
730
+ ] });
731
+ }
732
+
733
+ // src/displays/ChatPopup.tsx
734
+ var import_jsx_runtime6 = require("react/jsx-runtime");
735
+ function getPopupPositionStyles(position, offset) {
736
+ const x = offset.x ?? 20;
737
+ const y = offset.y ?? 20;
738
+ const popupOffset = 70;
739
+ switch (position) {
740
+ case "bottom-left":
741
+ return { bottom: y + popupOffset, left: x };
742
+ case "top-right":
743
+ return { top: y + popupOffset, right: x };
744
+ case "top-left":
745
+ return { top: y + popupOffset, left: x };
746
+ case "bottom-right":
747
+ default:
748
+ return { bottom: y + popupOffset, right: x };
749
+ }
750
+ }
751
+ function ChatPopup({
752
+ isOpen,
753
+ onClose,
754
+ messages,
755
+ input,
756
+ setInput,
757
+ handleSubmit,
758
+ isLoading,
759
+ isStreaming = false,
760
+ streamingContent = "",
761
+ error,
762
+ title = "Support",
763
+ placeholder = "Ask a question or report an issue...",
764
+ theme = "light",
765
+ position = "bottom-right",
766
+ offset = {},
767
+ onClearHistory,
768
+ onCancelChat,
769
+ onOpenHistory,
770
+ isHistoryOpen = false,
771
+ sessionHistory = [],
772
+ currentSessionId,
773
+ onCloseHistory,
774
+ onSelectSession,
775
+ onDeleteSession
776
+ }) {
777
+ if (!isOpen) return null;
778
+ const isDark = theme === "dark";
779
+ const positionStyles = getPopupPositionStyles(position, offset);
780
+ const bgColor = isDark ? "#1a1a1a" : "#ffffff";
781
+ const borderColor = isDark ? "#333333" : "#e5e7eb";
782
+ const textColor = isDark ? "#e5e5e5" : "#1f2937";
783
+ const secondaryTextColor = isDark ? "#999999" : "#6b7280";
784
+ const inputBg = isDark ? "#2a2a2a" : "#f9fafb";
785
+ const inputBorder = isDark ? "#444444" : "#d1d5db";
786
+ const handleKeyDown = (e) => {
787
+ if (e.key === "Enter" && !e.shiftKey) {
788
+ e.preventDefault();
789
+ handleSubmit();
790
+ }
791
+ };
792
+ const onSubmit = (e) => {
793
+ e.preventDefault();
794
+ handleSubmit(e);
795
+ };
796
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
797
+ "div",
798
+ {
799
+ className: "hissuno-popup",
800
+ style: {
801
+ position: "fixed",
802
+ ...positionStyles,
803
+ width: 380,
804
+ height: 520,
805
+ display: "flex",
806
+ flexDirection: "column",
807
+ borderRadius: 12,
808
+ boxShadow: "0 10px 40px rgba(0, 0, 0, 0.15)",
809
+ backgroundColor: bgColor,
810
+ border: `1px solid ${borderColor}`,
811
+ zIndex: 9999,
812
+ overflow: "hidden",
813
+ animation: "hissuno-scale-in 0.2s ease-out"
814
+ },
815
+ children: [
816
+ onCloseHistory && onSelectSession && onDeleteSession && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
817
+ ConversationHistory,
818
+ {
819
+ isOpen: isHistoryOpen,
820
+ onClose: onCloseHistory,
821
+ sessions: sessionHistory,
822
+ onSelectSession,
823
+ onDeleteSession,
824
+ currentSessionId,
825
+ theme
826
+ }
827
+ ),
828
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
829
+ "div",
830
+ {
831
+ style: {
832
+ display: "flex",
833
+ alignItems: "center",
834
+ justifyContent: "space-between",
835
+ padding: "16px 20px",
836
+ borderBottom: `1px solid ${borderColor}`
837
+ },
838
+ children: [
839
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
840
+ onOpenHistory && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
841
+ "button",
842
+ {
843
+ type: "button",
844
+ onClick: onOpenHistory,
845
+ style: {
846
+ background: "none",
847
+ border: "none",
848
+ cursor: "pointer",
849
+ padding: 4,
850
+ display: "flex",
851
+ alignItems: "center",
852
+ justifyContent: "center",
853
+ color: secondaryTextColor,
854
+ borderRadius: 4
855
+ },
856
+ "aria-label": "Conversation history",
857
+ title: "Conversation history",
858
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HistoryIcon, {})
859
+ }
860
+ ),
861
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
862
+ "h2",
863
+ {
864
+ style: {
865
+ margin: 0,
866
+ fontSize: 16,
867
+ fontWeight: 600,
868
+ color: textColor,
869
+ fontFamily: "system-ui, -apple-system, sans-serif"
870
+ },
871
+ children: title
872
+ }
873
+ )
874
+ ] }),
875
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", gap: 8 }, children: [
876
+ onClearHistory && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
877
+ "button",
878
+ {
879
+ type: "button",
880
+ onClick: onClearHistory,
881
+ style: {
882
+ background: "none",
883
+ border: "none",
884
+ cursor: "pointer",
885
+ padding: 4,
886
+ display: "flex",
887
+ alignItems: "center",
888
+ justifyContent: "center",
889
+ color: secondaryTextColor,
890
+ borderRadius: 4
891
+ },
892
+ "aria-label": "New conversation",
893
+ title: "New conversation",
894
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(NewThreadIcon, {})
895
+ }
896
+ ),
897
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
898
+ "button",
899
+ {
900
+ type: "button",
901
+ onClick: onClose,
902
+ style: {
903
+ background: "none",
904
+ border: "none",
905
+ cursor: "pointer",
906
+ padding: 4,
907
+ display: "flex",
908
+ alignItems: "center",
909
+ justifyContent: "center",
910
+ color: secondaryTextColor,
911
+ borderRadius: 4
912
+ },
913
+ "aria-label": "Close",
914
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, {})
915
+ }
916
+ )
917
+ ] })
918
+ ]
919
+ }
920
+ ),
921
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
922
+ ChatMessages,
923
+ {
924
+ messages,
925
+ isLoading,
926
+ isStreaming,
927
+ streamingContent,
928
+ theme
929
+ }
930
+ ),
931
+ error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
932
+ "div",
933
+ {
934
+ role: "alert",
935
+ "aria-live": "assertive",
936
+ style: {
937
+ margin: "0 16px 12px",
938
+ padding: "8px 12px",
939
+ borderRadius: 8,
940
+ backgroundColor: isDark ? "#3b1515" : "#fef2f2",
941
+ border: `1px solid ${isDark ? "#5c2020" : "#fecaca"}`,
942
+ color: isDark ? "#fca5a5" : "#dc2626",
943
+ fontSize: 13
944
+ },
945
+ children: error.message || "Something went wrong. Please try again."
946
+ }
947
+ ),
948
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
949
+ "form",
950
+ {
951
+ onSubmit,
952
+ style: {
953
+ display: "flex",
954
+ gap: 8,
955
+ padding: 16,
956
+ borderTop: `1px solid ${borderColor}`
957
+ },
958
+ children: [
959
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
960
+ "input",
961
+ {
962
+ type: "text",
963
+ value: input,
964
+ onChange: (e) => setInput(e.target.value),
965
+ onKeyDown: handleKeyDown,
966
+ placeholder,
967
+ disabled: isLoading,
968
+ "aria-label": "Type your message",
969
+ style: {
970
+ flex: 1,
971
+ padding: "10px 14px",
972
+ borderRadius: 8,
973
+ border: `1px solid ${inputBorder}`,
974
+ backgroundColor: inputBg,
975
+ color: textColor,
976
+ fontSize: 14,
977
+ outline: "none",
978
+ fontFamily: "system-ui, -apple-system, sans-serif"
979
+ }
980
+ }
981
+ ),
982
+ isStreaming && onCancelChat ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
983
+ "button",
984
+ {
985
+ type: "button",
986
+ onClick: onCancelChat,
987
+ style: {
988
+ padding: "10px 16px",
989
+ borderRadius: 8,
990
+ border: "none",
991
+ backgroundColor: "#dc2626",
992
+ color: "#ffffff",
993
+ fontSize: 14,
994
+ fontWeight: 500,
995
+ cursor: "pointer",
996
+ fontFamily: "system-ui, -apple-system, sans-serif",
997
+ display: "flex",
998
+ alignItems: "center",
999
+ justifyContent: "center",
1000
+ minWidth: 60
1001
+ },
1002
+ children: "Cancel"
1003
+ }
1004
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1005
+ "button",
1006
+ {
1007
+ type: "submit",
1008
+ disabled: isLoading || !input.trim(),
1009
+ style: {
1010
+ padding: "10px 16px",
1011
+ borderRadius: 8,
1012
+ border: "none",
1013
+ backgroundColor: "#2563eb",
1014
+ color: "#ffffff",
1015
+ fontSize: 14,
1016
+ fontWeight: 500,
1017
+ cursor: isLoading || !input.trim() ? "not-allowed" : "pointer",
1018
+ opacity: isLoading || !input.trim() ? 0.5 : 1,
1019
+ fontFamily: "system-ui, -apple-system, sans-serif",
1020
+ display: "flex",
1021
+ alignItems: "center",
1022
+ justifyContent: "center",
1023
+ minWidth: 60
1024
+ },
1025
+ children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SpinnerIcon, {}) : "Send"
1026
+ }
1027
+ )
1028
+ ]
1029
+ }
1030
+ )
1031
+ ]
1032
+ }
1033
+ );
1034
+ }
1035
+
1036
+ // src/displays/ChatSidepanel.tsx
1037
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1038
+ function ChatSidepanel({
1039
+ isOpen,
1040
+ onClose,
1041
+ messages,
1042
+ input,
1043
+ setInput,
1044
+ handleSubmit,
1045
+ isLoading,
1046
+ isStreaming = false,
1047
+ streamingContent = "",
1048
+ error,
1049
+ title = "Support",
1050
+ placeholder = "Ask a question or report an issue...",
1051
+ theme = "light",
1052
+ onClearHistory,
1053
+ onCancelChat,
1054
+ onOpenHistory,
1055
+ isHistoryOpen = false,
1056
+ sessionHistory = [],
1057
+ currentSessionId,
1058
+ onCloseHistory,
1059
+ onSelectSession,
1060
+ onDeleteSession
1061
+ }) {
1062
+ if (!isOpen) return null;
1063
+ const isDark = theme === "dark";
1064
+ const bgColor = isDark ? "#1a1a1a" : "#ffffff";
1065
+ const borderColor = isDark ? "#333333" : "#e5e7eb";
1066
+ const textColor = isDark ? "#e5e5e5" : "#1f2937";
1067
+ const secondaryTextColor = isDark ? "#999999" : "#6b7280";
1068
+ const inputBg = isDark ? "#2a2a2a" : "#f9fafb";
1069
+ const inputBorder = isDark ? "#444444" : "#d1d5db";
1070
+ const handleKeyDown = (e) => {
1071
+ if (e.key === "Enter" && !e.shiftKey) {
1072
+ e.preventDefault();
1073
+ handleSubmit();
1074
+ }
1075
+ };
1076
+ const onSubmit = (e) => {
1077
+ e.preventDefault();
1078
+ handleSubmit(e);
1079
+ };
1080
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1081
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1082
+ "div",
1083
+ {
1084
+ className: "hissuno-backdrop",
1085
+ onClick: onClose,
1086
+ style: {
1087
+ position: "fixed",
1088
+ inset: 0,
1089
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
1090
+ zIndex: 9998
1091
+ }
1092
+ }
1093
+ ),
1094
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1095
+ "div",
1096
+ {
1097
+ className: "hissuno-sidepanel",
1098
+ style: {
1099
+ position: "fixed",
1100
+ top: 0,
1101
+ right: 0,
1102
+ width: 400,
1103
+ height: "100vh",
1104
+ display: "flex",
1105
+ flexDirection: "column",
1106
+ backgroundColor: bgColor,
1107
+ borderLeft: `1px solid ${borderColor}`,
1108
+ boxShadow: "-4px 0 20px rgba(0, 0, 0, 0.15)",
1109
+ zIndex: 9999,
1110
+ overflow: "hidden",
1111
+ animation: "hissuno-slide-in-right 0.25s ease-out"
1112
+ },
1113
+ children: [
1114
+ onCloseHistory && onSelectSession && onDeleteSession && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1115
+ ConversationHistory,
1116
+ {
1117
+ isOpen: isHistoryOpen,
1118
+ onClose: onCloseHistory,
1119
+ sessions: sessionHistory,
1120
+ onSelectSession,
1121
+ onDeleteSession,
1122
+ currentSessionId,
1123
+ theme
1124
+ }
1125
+ ),
1126
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1127
+ "div",
1128
+ {
1129
+ style: {
1130
+ display: "flex",
1131
+ alignItems: "center",
1132
+ justifyContent: "space-between",
1133
+ padding: "16px 20px",
1134
+ borderBottom: `1px solid ${borderColor}`
1135
+ },
1136
+ children: [
1137
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
1138
+ onOpenHistory && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1139
+ "button",
1140
+ {
1141
+ type: "button",
1142
+ onClick: onOpenHistory,
1143
+ style: {
1144
+ background: "none",
1145
+ border: "none",
1146
+ cursor: "pointer",
1147
+ padding: 4,
1148
+ display: "flex",
1149
+ alignItems: "center",
1150
+ justifyContent: "center",
1151
+ color: secondaryTextColor,
1152
+ borderRadius: 4
1153
+ },
1154
+ "aria-label": "Conversation history",
1155
+ title: "Conversation history",
1156
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(HistoryIcon, {})
1157
+ }
1158
+ ),
1159
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1160
+ "h2",
1161
+ {
1162
+ style: {
1163
+ margin: 0,
1164
+ fontSize: 16,
1165
+ fontWeight: 600,
1166
+ color: textColor,
1167
+ fontFamily: "system-ui, -apple-system, sans-serif"
1168
+ },
1169
+ children: title
1170
+ }
1171
+ )
1172
+ ] }),
1173
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", gap: 8 }, children: [
1174
+ onClearHistory && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1175
+ "button",
1176
+ {
1177
+ type: "button",
1178
+ onClick: onClearHistory,
1179
+ style: {
1180
+ background: "none",
1181
+ border: "none",
1182
+ cursor: "pointer",
1183
+ padding: 4,
1184
+ display: "flex",
1185
+ alignItems: "center",
1186
+ justifyContent: "center",
1187
+ color: secondaryTextColor,
1188
+ borderRadius: 4
1189
+ },
1190
+ "aria-label": "New conversation",
1191
+ title: "New conversation",
1192
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(NewThreadIcon, {})
1193
+ }
1194
+ ),
1195
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1196
+ "button",
1197
+ {
1198
+ type: "button",
1199
+ onClick: onClose,
1200
+ style: {
1201
+ background: "none",
1202
+ border: "none",
1203
+ cursor: "pointer",
1204
+ padding: 4,
1205
+ display: "flex",
1206
+ alignItems: "center",
1207
+ justifyContent: "center",
1208
+ color: secondaryTextColor,
1209
+ borderRadius: 4
1210
+ },
1211
+ "aria-label": "Close",
1212
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CloseIcon, {})
1213
+ }
1214
+ )
1215
+ ] })
1216
+ ]
1217
+ }
1218
+ ),
1219
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1220
+ ChatMessages,
1221
+ {
1222
+ messages,
1223
+ isLoading,
1224
+ isStreaming,
1225
+ streamingContent,
1226
+ theme
1227
+ }
1228
+ ),
1229
+ error && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1230
+ "div",
1231
+ {
1232
+ role: "alert",
1233
+ "aria-live": "assertive",
1234
+ style: {
1235
+ margin: "0 16px 12px",
1236
+ padding: "8px 12px",
1237
+ borderRadius: 8,
1238
+ backgroundColor: isDark ? "#3b1515" : "#fef2f2",
1239
+ border: `1px solid ${isDark ? "#5c2020" : "#fecaca"}`,
1240
+ color: isDark ? "#fca5a5" : "#dc2626",
1241
+ fontSize: 13
1242
+ },
1243
+ children: error.message || "Something went wrong. Please try again."
1244
+ }
1245
+ ),
1246
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1247
+ "form",
1248
+ {
1249
+ onSubmit,
1250
+ style: {
1251
+ display: "flex",
1252
+ gap: 8,
1253
+ padding: 16,
1254
+ borderTop: `1px solid ${borderColor}`
1255
+ },
1256
+ children: [
1257
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1258
+ "input",
1259
+ {
1260
+ type: "text",
1261
+ value: input,
1262
+ onChange: (e) => setInput(e.target.value),
1263
+ onKeyDown: handleKeyDown,
1264
+ placeholder,
1265
+ disabled: isLoading,
1266
+ "aria-label": "Type your message",
1267
+ style: {
1268
+ flex: 1,
1269
+ padding: "10px 14px",
1270
+ borderRadius: 8,
1271
+ border: `1px solid ${inputBorder}`,
1272
+ backgroundColor: inputBg,
1273
+ color: textColor,
1274
+ fontSize: 14,
1275
+ outline: "none",
1276
+ fontFamily: "system-ui, -apple-system, sans-serif"
1277
+ }
1278
+ }
1279
+ ),
1280
+ isStreaming && onCancelChat ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1281
+ "button",
1282
+ {
1283
+ type: "button",
1284
+ onClick: onCancelChat,
1285
+ style: {
1286
+ padding: "10px 16px",
1287
+ borderRadius: 8,
1288
+ border: "none",
1289
+ backgroundColor: "#dc2626",
1290
+ color: "#ffffff",
1291
+ fontSize: 14,
1292
+ fontWeight: 500,
1293
+ cursor: "pointer",
1294
+ fontFamily: "system-ui, -apple-system, sans-serif",
1295
+ display: "flex",
1296
+ alignItems: "center",
1297
+ justifyContent: "center",
1298
+ minWidth: 60
1299
+ },
1300
+ children: "Cancel"
1301
+ }
1302
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1303
+ "button",
1304
+ {
1305
+ type: "submit",
1306
+ disabled: isLoading || !input.trim(),
1307
+ style: {
1308
+ padding: "10px 16px",
1309
+ borderRadius: 8,
1310
+ border: "none",
1311
+ backgroundColor: "#2563eb",
1312
+ color: "#ffffff",
1313
+ fontSize: 14,
1314
+ fontWeight: 500,
1315
+ cursor: isLoading || !input.trim() ? "not-allowed" : "pointer",
1316
+ opacity: isLoading || !input.trim() ? 0.5 : 1,
1317
+ fontFamily: "system-ui, -apple-system, sans-serif",
1318
+ display: "flex",
1319
+ alignItems: "center",
1320
+ justifyContent: "center",
1321
+ minWidth: 60
1322
+ },
1323
+ children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SpinnerIcon, {}) : "Send"
1324
+ }
1325
+ )
1326
+ ]
1327
+ }
1328
+ )
1329
+ ]
1330
+ }
1331
+ )
1332
+ ] });
1333
+ }
1334
+
1335
+ // src/displays/ChatDialog.tsx
1336
+ var import_react3 = require("react");
1337
+
1338
+ // src/hooks/useFocusTrap.ts
1339
+ var import_react2 = require("react");
1340
+ var FOCUSABLE_SELECTORS = [
1341
+ "button:not([disabled])",
1342
+ "input:not([disabled])",
1343
+ "select:not([disabled])",
1344
+ "textarea:not([disabled])",
1345
+ "a[href]",
1346
+ '[tabindex]:not([tabindex="-1"])'
1347
+ ].join(", ");
1348
+ function useFocusTrap(isActive) {
1349
+ const containerRef = (0, import_react2.useRef)(null);
1350
+ const previousActiveElement = (0, import_react2.useRef)(null);
1351
+ const getFocusableElements = (0, import_react2.useCallback)(() => {
1352
+ if (!containerRef.current) return [];
1353
+ return Array.from(
1354
+ containerRef.current.querySelectorAll(FOCUSABLE_SELECTORS)
1355
+ ).filter((el) => el.offsetParent !== null);
1356
+ }, []);
1357
+ const handleKeyDown = (0, import_react2.useCallback)(
1358
+ (event) => {
1359
+ if (!isActive || event.key !== "Tab") return;
1360
+ const focusableElements = getFocusableElements();
1361
+ if (focusableElements.length === 0) return;
1362
+ const firstElement = focusableElements[0];
1363
+ const lastElement = focusableElements[focusableElements.length - 1];
1364
+ if (event.shiftKey && document.activeElement === firstElement) {
1365
+ event.preventDefault();
1366
+ lastElement.focus();
1367
+ } else if (!event.shiftKey && document.activeElement === lastElement) {
1368
+ event.preventDefault();
1369
+ firstElement.focus();
1370
+ }
1371
+ },
1372
+ [isActive, getFocusableElements]
1373
+ );
1374
+ (0, import_react2.useEffect)(() => {
1375
+ if (!isActive) return;
1376
+ previousActiveElement.current = document.activeElement;
1377
+ const focusableElements = getFocusableElements();
1378
+ if (focusableElements.length > 0) {
1379
+ requestAnimationFrame(() => {
1380
+ focusableElements[0].focus();
1381
+ });
1382
+ }
1383
+ document.addEventListener("keydown", handleKeyDown);
1384
+ return () => {
1385
+ document.removeEventListener("keydown", handleKeyDown);
1386
+ if (previousActiveElement.current && previousActiveElement.current instanceof HTMLElement) {
1387
+ previousActiveElement.current.focus();
1388
+ }
1389
+ };
1390
+ }, [isActive, getFocusableElements, handleKeyDown]);
1391
+ return containerRef;
1392
+ }
1393
+
1394
+ // src/displays/ChatDialog.tsx
1395
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1396
+ function ChatDialog({
1397
+ isOpen,
1398
+ onClose,
1399
+ messages,
1400
+ input,
1401
+ setInput,
1402
+ handleSubmit,
1403
+ isLoading,
1404
+ isStreaming = false,
1405
+ streamingContent = "",
1406
+ error,
1407
+ title = "Support",
1408
+ placeholder = "Ask a question or report an issue...",
1409
+ theme = "light",
1410
+ width = 600,
1411
+ height = 500,
1412
+ onClearHistory,
1413
+ onCancelChat,
1414
+ onOpenHistory,
1415
+ isHistoryOpen = false,
1416
+ sessionHistory = [],
1417
+ currentSessionId,
1418
+ onCloseHistory,
1419
+ onSelectSession,
1420
+ onDeleteSession
1421
+ }) {
1422
+ const isDark = theme === "dark";
1423
+ const bgColor = isDark ? "#1a1a1a" : "#ffffff";
1424
+ const borderColor = isDark ? "#333333" : "#e5e7eb";
1425
+ const textColor = isDark ? "#e5e5e5" : "#1f2937";
1426
+ const secondaryTextColor = isDark ? "#999999" : "#6b7280";
1427
+ const inputBg = isDark ? "#2a2a2a" : "#f9fafb";
1428
+ const inputBorder = isDark ? "#444444" : "#d1d5db";
1429
+ const dialogRef = useFocusTrap(isOpen);
1430
+ const handleEscapeKey = (0, import_react3.useCallback)((e) => {
1431
+ if (e.key === "Escape" && isOpen) {
1432
+ onClose();
1433
+ }
1434
+ }, [isOpen, onClose]);
1435
+ (0, import_react3.useEffect)(() => {
1436
+ document.addEventListener("keydown", handleEscapeKey);
1437
+ return () => document.removeEventListener("keydown", handleEscapeKey);
1438
+ }, [handleEscapeKey]);
1439
+ const handleBackdropClick = (0, import_react3.useCallback)((e) => {
1440
+ if (e.target === e.currentTarget) {
1441
+ onClose();
1442
+ }
1443
+ }, [onClose]);
1444
+ const handleKeyDown = (e) => {
1445
+ if (e.key === "Enter" && !e.shiftKey) {
1446
+ e.preventDefault();
1447
+ handleSubmit();
1448
+ }
1449
+ };
1450
+ const onSubmit = (e) => {
1451
+ e.preventDefault();
1452
+ handleSubmit(e);
1453
+ };
1454
+ if (!isOpen) return null;
1455
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1456
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1457
+ "div",
1458
+ {
1459
+ className: "hissuno-dialog-backdrop",
1460
+ onClick: handleBackdropClick,
1461
+ style: {
1462
+ position: "fixed",
1463
+ inset: 0,
1464
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
1465
+ backdropFilter: "blur(4px)",
1466
+ WebkitBackdropFilter: "blur(4px)",
1467
+ display: "flex",
1468
+ alignItems: "center",
1469
+ justifyContent: "center",
1470
+ zIndex: 9998
1471
+ },
1472
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1473
+ "div",
1474
+ {
1475
+ ref: dialogRef,
1476
+ role: "dialog",
1477
+ "aria-modal": "true",
1478
+ "aria-labelledby": "hissuno-dialog-title",
1479
+ className: "hissuno-dialog",
1480
+ style: {
1481
+ width,
1482
+ height,
1483
+ maxWidth: "calc(100vw - 40px)",
1484
+ maxHeight: "calc(100vh - 40px)",
1485
+ display: "flex",
1486
+ flexDirection: "column",
1487
+ borderRadius: 16,
1488
+ boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
1489
+ backgroundColor: bgColor,
1490
+ border: `1px solid ${borderColor}`,
1491
+ overflow: "hidden",
1492
+ animation: "hissuno-dialog-scale-in 0.2s ease-out"
1493
+ },
1494
+ onClick: (e) => e.stopPropagation(),
1495
+ children: [
1496
+ onCloseHistory && onSelectSession && onDeleteSession && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1497
+ ConversationHistory,
1498
+ {
1499
+ isOpen: isHistoryOpen,
1500
+ onClose: onCloseHistory,
1501
+ sessions: sessionHistory,
1502
+ onSelectSession,
1503
+ onDeleteSession,
1504
+ currentSessionId,
1505
+ theme
1506
+ }
1507
+ ),
1508
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1509
+ "div",
1510
+ {
1511
+ style: {
1512
+ display: "flex",
1513
+ alignItems: "center",
1514
+ justifyContent: "space-between",
1515
+ padding: "16px 24px",
1516
+ borderBottom: `1px solid ${borderColor}`
1517
+ },
1518
+ children: [
1519
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
1520
+ onOpenHistory && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1521
+ "button",
1522
+ {
1523
+ type: "button",
1524
+ onClick: onOpenHistory,
1525
+ style: {
1526
+ background: "none",
1527
+ border: "none",
1528
+ cursor: "pointer",
1529
+ padding: 4,
1530
+ display: "flex",
1531
+ alignItems: "center",
1532
+ justifyContent: "center",
1533
+ color: secondaryTextColor,
1534
+ borderRadius: 4
1535
+ },
1536
+ "aria-label": "Conversation history",
1537
+ title: "Conversation history",
1538
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(HistoryIcon, {})
1539
+ }
1540
+ ),
1541
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1542
+ "h2",
1543
+ {
1544
+ id: "hissuno-dialog-title",
1545
+ style: {
1546
+ margin: 0,
1547
+ fontSize: 18,
1548
+ fontWeight: 600,
1549
+ color: textColor,
1550
+ fontFamily: "system-ui, -apple-system, sans-serif"
1551
+ },
1552
+ children: title
1553
+ }
1554
+ )
1555
+ ] }),
1556
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", gap: 8 }, children: [
1557
+ onClearHistory && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1558
+ "button",
1559
+ {
1560
+ type: "button",
1561
+ onClick: onClearHistory,
1562
+ style: {
1563
+ background: "none",
1564
+ border: "none",
1565
+ cursor: "pointer",
1566
+ padding: 4,
1567
+ display: "flex",
1568
+ alignItems: "center",
1569
+ justifyContent: "center",
1570
+ color: secondaryTextColor,
1571
+ borderRadius: 4
1572
+ },
1573
+ "aria-label": "New conversation",
1574
+ title: "New conversation",
1575
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(NewThreadIcon, {})
1576
+ }
1577
+ ),
1578
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1579
+ "button",
1580
+ {
1581
+ type: "button",
1582
+ onClick: onClose,
1583
+ style: {
1584
+ background: "none",
1585
+ border: "none",
1586
+ cursor: "pointer",
1587
+ padding: 4,
1588
+ display: "flex",
1589
+ alignItems: "center",
1590
+ justifyContent: "center",
1591
+ color: secondaryTextColor,
1592
+ borderRadius: 4
1593
+ },
1594
+ "aria-label": "Close",
1595
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CloseIcon, {})
1596
+ }
1597
+ )
1598
+ ] })
1599
+ ]
1600
+ }
1601
+ ),
1602
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1603
+ ChatMessages,
1604
+ {
1605
+ messages,
1606
+ isLoading,
1607
+ isStreaming,
1608
+ streamingContent,
1609
+ theme
1610
+ }
1611
+ ),
1612
+ error && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1613
+ "div",
1614
+ {
1615
+ role: "alert",
1616
+ "aria-live": "assertive",
1617
+ style: {
1618
+ margin: "0 16px 12px",
1619
+ padding: "8px 12px",
1620
+ borderRadius: 8,
1621
+ backgroundColor: isDark ? "#3b1515" : "#fef2f2",
1622
+ border: `1px solid ${isDark ? "#5c2020" : "#fecaca"}`,
1623
+ color: isDark ? "#fca5a5" : "#dc2626",
1624
+ fontSize: 13
1625
+ },
1626
+ children: error.message || "Something went wrong. Please try again."
1627
+ }
1628
+ ),
1629
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1630
+ "form",
1631
+ {
1632
+ onSubmit,
1633
+ style: {
1634
+ display: "flex",
1635
+ gap: 8,
1636
+ padding: 16,
1637
+ borderTop: `1px solid ${borderColor}`
1638
+ },
1639
+ children: [
1640
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1641
+ "input",
1642
+ {
1643
+ type: "text",
1644
+ value: input,
1645
+ onChange: (e) => setInput(e.target.value),
1646
+ onKeyDown: handleKeyDown,
1647
+ placeholder,
1648
+ disabled: isLoading,
1649
+ autoFocus: true,
1650
+ "aria-label": "Type your message",
1651
+ style: {
1652
+ flex: 1,
1653
+ padding: "12px 16px",
1654
+ borderRadius: 8,
1655
+ border: `1px solid ${inputBorder}`,
1656
+ backgroundColor: inputBg,
1657
+ color: textColor,
1658
+ fontSize: 14,
1659
+ outline: "none",
1660
+ fontFamily: "system-ui, -apple-system, sans-serif"
1661
+ }
1662
+ }
1663
+ ),
1664
+ isStreaming && onCancelChat ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1665
+ "button",
1666
+ {
1667
+ type: "button",
1668
+ onClick: onCancelChat,
1669
+ style: {
1670
+ padding: "12px 20px",
1671
+ borderRadius: 8,
1672
+ border: "none",
1673
+ backgroundColor: "#dc2626",
1674
+ color: "#ffffff",
1675
+ fontSize: 14,
1676
+ fontWeight: 500,
1677
+ cursor: "pointer",
1678
+ fontFamily: "system-ui, -apple-system, sans-serif",
1679
+ display: "flex",
1680
+ alignItems: "center",
1681
+ justifyContent: "center",
1682
+ minWidth: 70
1683
+ },
1684
+ children: "Cancel"
1685
+ }
1686
+ ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1687
+ "button",
1688
+ {
1689
+ type: "submit",
1690
+ disabled: isLoading || !input.trim(),
1691
+ style: {
1692
+ padding: "12px 20px",
1693
+ borderRadius: 8,
1694
+ border: "none",
1695
+ backgroundColor: "#2563eb",
1696
+ color: "#ffffff",
1697
+ fontSize: 14,
1698
+ fontWeight: 500,
1699
+ cursor: isLoading || !input.trim() ? "not-allowed" : "pointer",
1700
+ opacity: isLoading || !input.trim() ? 0.5 : 1,
1701
+ fontFamily: "system-ui, -apple-system, sans-serif",
1702
+ display: "flex",
1703
+ alignItems: "center",
1704
+ justifyContent: "center",
1705
+ minWidth: 70
1706
+ },
1707
+ children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SpinnerIcon, {}) : "Send"
1708
+ }
1709
+ )
1710
+ ]
1711
+ }
1712
+ )
1713
+ ]
1714
+ }
1715
+ )
1716
+ }
1717
+ ),
1718
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("style", { children: `
1719
+ @keyframes hissuno-dialog-scale-in {
1720
+ from {
1721
+ opacity: 0;
1722
+ transform: scale(0.95);
1723
+ }
1724
+ to {
1725
+ opacity: 1;
1726
+ transform: scale(1);
1727
+ }
1728
+ }
1729
+ ` })
1730
+ ] });
1731
+ }
1732
+
1733
+ // src/hooks/useHissunoChat.ts
1734
+ var import_react4 = require("react");
1735
+
1736
+ // src/utils/validation.ts
1737
+ var MAX_MESSAGE_LENGTH = 4096;
1738
+ var SENSITIVE_PARAMS = [
1739
+ "token",
1740
+ "access_token",
1741
+ "api_key",
1742
+ "apikey",
1743
+ "key",
1744
+ "secret",
1745
+ "password",
1746
+ "pwd",
1747
+ "auth",
1748
+ "authorization",
1749
+ "session",
1750
+ "sessionid",
1751
+ "session_id",
1752
+ "jwt",
1753
+ "bearer",
1754
+ "refresh_token",
1755
+ "code",
1756
+ "state",
1757
+ "nonce"
1758
+ ];
1759
+ function validateMessageContent(content) {
1760
+ if (typeof content !== "string") {
1761
+ return null;
1762
+ }
1763
+ const byteLength = new TextEncoder().encode(content).length;
1764
+ if (byteLength > MAX_MESSAGE_LENGTH) {
1765
+ return null;
1766
+ }
1767
+ return content;
1768
+ }
1769
+ var VALID_CHAT_SSE_TYPES = ["connected", "message-start", "message-chunk", "message-complete", "error"];
1770
+ var VALID_UPDATE_SSE_TYPES = ["connected", "message", "status-change", "error", "heartbeat"];
1771
+ var VALID_MESSAGE_ROLES = ["user", "assistant"];
1772
+ var VALID_SENDER_TYPES = ["ai", "human_agent", "system"];
1773
+ function validateChatSSEEvent(data) {
1774
+ if (!data || typeof data !== "object") {
1775
+ return null;
1776
+ }
1777
+ const obj = data;
1778
+ if (typeof obj.type !== "string" || !VALID_CHAT_SSE_TYPES.includes(obj.type)) {
1779
+ return null;
1780
+ }
1781
+ if (obj.content !== void 0 && typeof obj.content !== "string") {
1782
+ return null;
1783
+ }
1784
+ if (obj.message !== void 0 && typeof obj.message !== "string") {
1785
+ return null;
1786
+ }
1787
+ if (typeof obj.timestamp !== "string") {
1788
+ return {
1789
+ type: obj.type,
1790
+ content: obj.content,
1791
+ message: obj.message,
1792
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1793
+ };
1794
+ }
1795
+ return {
1796
+ type: obj.type,
1797
+ content: obj.content,
1798
+ message: obj.message,
1799
+ timestamp: obj.timestamp
1800
+ };
1801
+ }
1802
+ function validateUpdateSSEEvent(data) {
1803
+ if (!data || typeof data !== "object") {
1804
+ return null;
1805
+ }
1806
+ const obj = data;
1807
+ if (typeof obj.type !== "string" || !VALID_UPDATE_SSE_TYPES.includes(obj.type)) {
1808
+ return null;
1809
+ }
1810
+ let validatedMessage;
1811
+ if (obj.message !== void 0) {
1812
+ if (typeof obj.message !== "object" || obj.message === null) {
1813
+ return null;
1814
+ }
1815
+ const msg = obj.message;
1816
+ if (typeof msg.id !== "string" || !msg.id) {
1817
+ return null;
1818
+ }
1819
+ if (typeof msg.role !== "string" || !VALID_MESSAGE_ROLES.includes(msg.role)) {
1820
+ return null;
1821
+ }
1822
+ if (typeof msg.content !== "string") {
1823
+ return null;
1824
+ }
1825
+ if (typeof msg.createdAt !== "string") {
1826
+ return null;
1827
+ }
1828
+ if (msg.senderType !== void 0) {
1829
+ if (typeof msg.senderType !== "string" || !VALID_SENDER_TYPES.includes(msg.senderType)) {
1830
+ return null;
1831
+ }
1832
+ }
1833
+ validatedMessage = {
1834
+ id: msg.id,
1835
+ role: msg.role,
1836
+ content: msg.content,
1837
+ createdAt: msg.createdAt,
1838
+ senderType: msg.senderType
1839
+ };
1840
+ }
1841
+ if (obj.status !== void 0 && typeof obj.status !== "string") {
1842
+ return null;
1843
+ }
1844
+ const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : (/* @__PURE__ */ new Date()).toISOString();
1845
+ return {
1846
+ type: obj.type,
1847
+ message: validatedMessage,
1848
+ status: obj.status,
1849
+ timestamp
1850
+ };
1851
+ }
1852
+ function sanitizePageUrl(url) {
1853
+ if (typeof url !== "string" || !url) {
1854
+ return "";
1855
+ }
1856
+ try {
1857
+ const parsed = new URL(url);
1858
+ for (const param of SENSITIVE_PARAMS) {
1859
+ parsed.searchParams.delete(param);
1860
+ parsed.searchParams.delete(param.toLowerCase());
1861
+ parsed.searchParams.delete(param.toUpperCase());
1862
+ }
1863
+ const paramsToRemove = [];
1864
+ parsed.searchParams.forEach((_, key) => {
1865
+ const lowerKey = key.toLowerCase();
1866
+ if (SENSITIVE_PARAMS.some((p) => lowerKey.includes(p))) {
1867
+ paramsToRemove.push(key);
1868
+ }
1869
+ });
1870
+ for (const key of paramsToRemove) {
1871
+ parsed.searchParams.delete(key);
1872
+ }
1873
+ return parsed.toString();
1874
+ } catch {
1875
+ return "";
1876
+ }
1877
+ }
1878
+ var VALID_TRIGGERS = ["bubble", "drawer-badge", "headless"];
1879
+ var VALID_DISPLAYS = ["popup", "sidepanel", "dialog"];
1880
+ var VALID_POSITIONS = ["bottom-right", "bottom-left", "top-right", "top-left"];
1881
+ var VALID_THEMES = ["light", "dark", "auto"];
1882
+ function validateTrigger(value, fallback = "bubble") {
1883
+ if (typeof value === "string" && VALID_TRIGGERS.includes(value)) {
1884
+ return value;
1885
+ }
1886
+ return fallback;
1887
+ }
1888
+ function validateDisplay(value, fallback = "sidepanel") {
1889
+ if (typeof value === "string" && VALID_DISPLAYS.includes(value)) {
1890
+ return value;
1891
+ }
1892
+ return fallback;
1893
+ }
1894
+ function validatePosition(value, fallback = "bottom-right") {
1895
+ if (typeof value === "string" && VALID_POSITIONS.includes(value)) {
1896
+ return value;
1897
+ }
1898
+ return fallback;
1899
+ }
1900
+ function validateTheme(value, fallback = "light") {
1901
+ if (typeof value === "string" && VALID_THEMES.includes(value)) {
1902
+ return value;
1903
+ }
1904
+ return fallback;
1905
+ }
1906
+
1907
+ // src/hooks/useHissunoChat.ts
1908
+ var STORAGE_KEY_PREFIX = "hissuno_chat_";
1909
+ var SESSIONS_REGISTRY_PREFIX = "hissuno_sessions_";
1910
+ var DEFAULT_INACTIVITY_TIMEOUT = 30 * 60 * 1e3;
1911
+ function generateMessageId() {
1912
+ return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
1913
+ }
1914
+ function generateSessionId() {
1915
+ const timestamp = Date.now().toString(36);
1916
+ const random = Math.random().toString(36).slice(2, 9);
1917
+ return `session_${timestamp}_${random}`;
1918
+ }
1919
+ function getStorageKey(projectId, sessionId) {
1920
+ if (sessionId) {
1921
+ return `${STORAGE_KEY_PREFIX}${projectId}_${sessionId}`;
1922
+ }
1923
+ return `${STORAGE_KEY_PREFIX}${projectId}`;
1924
+ }
1925
+ function loadMessagesFromStorage(projectId, sessionId) {
1926
+ if (typeof window === "undefined") return [];
1927
+ try {
1928
+ const stored = localStorage.getItem(getStorageKey(projectId, sessionId));
1929
+ if (!stored) return [];
1930
+ const parsed = JSON.parse(stored);
1931
+ return parsed.map((msg) => ({
1932
+ id: msg.id,
1933
+ role: msg.role,
1934
+ content: msg.content,
1935
+ createdAt: msg.createdAt ? new Date(msg.createdAt) : void 0,
1936
+ senderType: msg.senderType
1937
+ }));
1938
+ } catch {
1939
+ return [];
1940
+ }
1941
+ }
1942
+ function saveMessagesToStorage(projectId, messages, sessionId) {
1943
+ if (typeof window === "undefined") return;
1944
+ try {
1945
+ const toStore = messages.map((msg) => ({
1946
+ id: msg.id,
1947
+ role: msg.role,
1948
+ content: msg.content,
1949
+ createdAt: msg.createdAt?.toISOString(),
1950
+ senderType: msg.senderType
1951
+ }));
1952
+ localStorage.setItem(getStorageKey(projectId, sessionId), JSON.stringify(toStore));
1953
+ } catch {
1954
+ }
1955
+ }
1956
+ function clearMessagesFromStorage(projectId, sessionId) {
1957
+ if (typeof window === "undefined") return;
1958
+ try {
1959
+ localStorage.removeItem(getStorageKey(projectId, sessionId));
1960
+ } catch {
1961
+ }
1962
+ }
1963
+ function getSessionsRegistryKey(projectId, userId) {
1964
+ return `${SESSIONS_REGISTRY_PREFIX}${projectId}_${userId}`;
1965
+ }
1966
+ function loadSessionsRegistry(projectId, userId) {
1967
+ if (typeof window === "undefined" || !userId) return [];
1968
+ try {
1969
+ const stored = localStorage.getItem(getSessionsRegistryKey(projectId, userId));
1970
+ if (!stored) return [];
1971
+ return JSON.parse(stored);
1972
+ } catch {
1973
+ return [];
1974
+ }
1975
+ }
1976
+ function saveSessionToRegistry(projectId, userId, sessionId, messages) {
1977
+ if (typeof window === "undefined" || !userId) return;
1978
+ try {
1979
+ const registry = loadSessionsRegistry(projectId, userId);
1980
+ const firstUserMessage = messages.find((m) => m.role === "user");
1981
+ const title = firstUserMessage?.content?.slice(0, 50) || "New conversation";
1982
+ const lastMessage = messages[messages.length - 1];
1983
+ const lastMessageAt = lastMessage?.createdAt?.toISOString() || (/* @__PURE__ */ new Date()).toISOString();
1984
+ const existingIndex = registry.findIndex((s) => s.sessionId === sessionId);
1985
+ const entry = {
1986
+ sessionId,
1987
+ userId,
1988
+ title: title.length >= 50 ? title + "..." : title,
1989
+ lastMessageAt,
1990
+ messageCount: messages.length
1991
+ };
1992
+ if (existingIndex >= 0) {
1993
+ registry[existingIndex] = entry;
1994
+ } else {
1995
+ registry.unshift(entry);
1996
+ }
1997
+ const trimmed = registry.slice(0, 50);
1998
+ localStorage.setItem(getSessionsRegistryKey(projectId, userId), JSON.stringify(trimmed));
1999
+ } catch {
2000
+ }
2001
+ }
2002
+ function deleteSessionFromRegistry(projectId, userId, sessionId) {
2003
+ if (typeof window === "undefined" || !userId) return;
2004
+ try {
2005
+ const registry = loadSessionsRegistry(projectId, userId);
2006
+ const filtered = registry.filter((s) => s.sessionId !== sessionId);
2007
+ localStorage.setItem(getSessionsRegistryKey(projectId, userId), JSON.stringify(filtered));
2008
+ clearMessagesFromStorage(projectId, sessionId);
2009
+ } catch {
2010
+ }
2011
+ }
2012
+ function useHissunoChat({
2013
+ projectId,
2014
+ widgetToken,
2015
+ apiUrl = "/api/agent",
2016
+ initialMessage,
2017
+ headers = {},
2018
+ userId,
2019
+ userMetadata,
2020
+ sessionId: providedSessionId,
2021
+ inactivityTimeout = DEFAULT_INACTIVITY_TIMEOUT,
2022
+ onSessionClose
2023
+ }) {
2024
+ const hasInitialized = (0, import_react4.useRef)(false);
2025
+ const inactivityTimerRef = (0, import_react4.useRef)(null);
2026
+ const sessionClosedRef = (0, import_react4.useRef)(false);
2027
+ const eventSourceRef = (0, import_react4.useRef)(null);
2028
+ const updatesEventSourceRef = (0, import_react4.useRef)(null);
2029
+ const mountedRef = (0, import_react4.useRef)(true);
2030
+ const isConnectingRef = (0, import_react4.useRef)(false);
2031
+ const seenMessageIds = (0, import_react4.useRef)(/* @__PURE__ */ new Set());
2032
+ const streamingContentRef = (0, import_react4.useRef)("");
2033
+ const pendingMessageRef = (0, import_react4.useRef)(null);
2034
+ const [currentSessionId, setCurrentSessionId] = (0, import_react4.useState)(() => providedSessionId || generateSessionId());
2035
+ const sessionId = currentSessionId;
2036
+ const [messages, setMessages] = (0, import_react4.useState)(() => {
2037
+ const stored = loadMessagesFromStorage(projectId, sessionId);
2038
+ if (stored.length > 0) {
2039
+ return stored;
2040
+ }
2041
+ if (initialMessage) {
2042
+ return [
2043
+ {
2044
+ id: generateMessageId(),
2045
+ role: "assistant",
2046
+ content: initialMessage,
2047
+ createdAt: /* @__PURE__ */ new Date(),
2048
+ senderType: "ai"
2049
+ }
2050
+ ];
2051
+ }
2052
+ return [];
2053
+ });
2054
+ const [input, setInput] = (0, import_react4.useState)("");
2055
+ const [isLoading, setIsLoading] = (0, import_react4.useState)(false);
2056
+ const [isStreaming, setIsStreaming] = (0, import_react4.useState)(false);
2057
+ const [streamingContent, setStreamingContent] = (0, import_react4.useState)("");
2058
+ const [error, setError] = (0, import_react4.useState)(void 0);
2059
+ const pageUrl = typeof window !== "undefined" ? sanitizePageUrl(window.location.href) : "";
2060
+ const pageTitle = typeof window !== "undefined" ? document.title : "";
2061
+ (0, import_react4.useEffect)(() => {
2062
+ if (hasInitialized.current && messages.length > 0) {
2063
+ saveMessagesToStorage(projectId, messages, sessionId);
2064
+ }
2065
+ hasInitialized.current = true;
2066
+ }, [messages, projectId, sessionId]);
2067
+ (0, import_react4.useEffect)(() => {
2068
+ mountedRef.current = true;
2069
+ return () => {
2070
+ mountedRef.current = false;
2071
+ if (eventSourceRef.current) {
2072
+ eventSourceRef.current.close();
2073
+ eventSourceRef.current = null;
2074
+ }
2075
+ if (updatesEventSourceRef.current) {
2076
+ updatesEventSourceRef.current.close();
2077
+ updatesEventSourceRef.current = null;
2078
+ }
2079
+ };
2080
+ }, []);
2081
+ const connectToStream = (0, import_react4.useCallback)(() => {
2082
+ if (isConnectingRef.current || eventSourceRef.current) {
2083
+ return;
2084
+ }
2085
+ isConnectingRef.current = true;
2086
+ streamingContentRef.current = "";
2087
+ pendingMessageRef.current = null;
2088
+ setStreamingContent("");
2089
+ setIsStreaming(true);
2090
+ const streamUrl = `${apiUrl}/stream?projectId=${encodeURIComponent(projectId)}&sessionId=${encodeURIComponent(sessionId)}`;
2091
+ const eventSource = new EventSource(streamUrl);
2092
+ eventSourceRef.current = eventSource;
2093
+ eventSource.onmessage = (event) => {
2094
+ if (!mountedRef.current) return;
2095
+ try {
2096
+ const rawData = JSON.parse(event.data);
2097
+ const data = validateChatSSEEvent(rawData);
2098
+ if (!data) {
2099
+ console.warn("[useHissunoChat] Received invalid SSE event, skipping");
2100
+ return;
2101
+ }
2102
+ if (data.type === "message-chunk" && data.content) {
2103
+ const newContent = streamingContentRef.current + data.content;
2104
+ if (new TextEncoder().encode(newContent).length <= MAX_MESSAGE_LENGTH * 10) {
2105
+ streamingContentRef.current = newContent;
2106
+ setStreamingContent(streamingContentRef.current);
2107
+ }
2108
+ }
2109
+ if (data.type === "message-complete") {
2110
+ eventSource.close();
2111
+ eventSourceRef.current = null;
2112
+ isConnectingRef.current = false;
2113
+ const completedContent = streamingContentRef.current;
2114
+ streamingContentRef.current = "";
2115
+ setStreamingContent("");
2116
+ if (completedContent && mountedRef.current && pendingMessageRef.current !== completedContent) {
2117
+ pendingMessageRef.current = completedContent;
2118
+ setMessages((prev) => [
2119
+ ...prev,
2120
+ {
2121
+ id: generateMessageId(),
2122
+ role: "assistant",
2123
+ content: completedContent,
2124
+ createdAt: /* @__PURE__ */ new Date()
2125
+ }
2126
+ ]);
2127
+ }
2128
+ setIsStreaming(false);
2129
+ setIsLoading(false);
2130
+ }
2131
+ if (data.type === "error") {
2132
+ eventSource.close();
2133
+ eventSourceRef.current = null;
2134
+ isConnectingRef.current = false;
2135
+ setError(new Error(data.message ?? "Chat failed"));
2136
+ setIsStreaming(false);
2137
+ setIsLoading(false);
2138
+ setStreamingContent("");
2139
+ }
2140
+ } catch (err) {
2141
+ console.error("[useHissunoChat] Failed to parse SSE event:", err);
2142
+ }
2143
+ };
2144
+ eventSource.onerror = () => {
2145
+ if (eventSource.readyState === EventSource.CLOSED) {
2146
+ eventSourceRef.current = null;
2147
+ isConnectingRef.current = false;
2148
+ if (mountedRef.current) {
2149
+ setIsStreaming(false);
2150
+ setIsLoading(false);
2151
+ }
2152
+ }
2153
+ };
2154
+ }, [apiUrl, projectId, sessionId]);
2155
+ (0, import_react4.useEffect)(() => {
2156
+ const checkRunningChat = async () => {
2157
+ try {
2158
+ const statusUrl = `${apiUrl}?projectId=${encodeURIComponent(projectId)}&sessionId=${encodeURIComponent(sessionId)}`;
2159
+ const response = await fetch(statusUrl);
2160
+ if (!response.ok) return;
2161
+ const status = await response.json();
2162
+ if (status.isRunning) {
2163
+ setIsLoading(true);
2164
+ connectToStream();
2165
+ }
2166
+ } catch (err) {
2167
+ console.error("[useHissunoChat] Failed to check running chat:", err);
2168
+ }
2169
+ };
2170
+ checkRunningChat();
2171
+ }, [apiUrl, projectId, sessionId, connectToStream]);
2172
+ (0, import_react4.useEffect)(() => {
2173
+ if (sessionClosedRef.current || !sessionId) return;
2174
+ const updatesUrl = `${apiUrl}/session/updates?sessionId=${encodeURIComponent(sessionId)}&projectId=${encodeURIComponent(projectId)}`;
2175
+ const connectUpdates = () => {
2176
+ if (updatesEventSourceRef.current) return;
2177
+ const eventSource = new EventSource(updatesUrl);
2178
+ updatesEventSourceRef.current = eventSource;
2179
+ eventSource.onmessage = (event) => {
2180
+ if (!mountedRef.current) return;
2181
+ try {
2182
+ const rawData = JSON.parse(event.data);
2183
+ const data = validateUpdateSSEEvent(rawData);
2184
+ if (!data) {
2185
+ console.warn("[useHissunoChat] Received invalid updates SSE event, skipping");
2186
+ return;
2187
+ }
2188
+ if (data.type === "message" && data.message) {
2189
+ const validContent = validateMessageContent(data.message.content);
2190
+ if (!validContent) {
2191
+ console.warn("[useHissunoChat] Message content invalid or too long, skipping");
2192
+ return;
2193
+ }
2194
+ if (!seenMessageIds.current.has(data.message.id)) {
2195
+ seenMessageIds.current.add(data.message.id);
2196
+ setMessages((prev) => [
2197
+ ...prev,
2198
+ {
2199
+ id: data.message.id,
2200
+ role: data.message.role,
2201
+ content: validContent,
2202
+ createdAt: new Date(data.message.createdAt),
2203
+ senderType: data.message.senderType || "human_agent"
2204
+ }
2205
+ ]);
2206
+ }
2207
+ }
2208
+ if (data.type === "status-change" && data.status === "closed") {
2209
+ sessionClosedRef.current = true;
2210
+ eventSource.close();
2211
+ updatesEventSourceRef.current = null;
2212
+ onSessionClose?.();
2213
+ }
2214
+ } catch (err) {
2215
+ console.error("[useHissunoChat] Failed to parse updates SSE event:", err);
2216
+ }
2217
+ };
2218
+ eventSource.onerror = () => {
2219
+ if (eventSource.readyState === EventSource.CLOSED && mountedRef.current && !sessionClosedRef.current) {
2220
+ updatesEventSourceRef.current = null;
2221
+ setTimeout(connectUpdates, 5e3);
2222
+ }
2223
+ };
2224
+ };
2225
+ if (messages.length > 1) {
2226
+ connectUpdates();
2227
+ }
2228
+ return () => {
2229
+ if (updatesEventSourceRef.current) {
2230
+ updatesEventSourceRef.current.close();
2231
+ updatesEventSourceRef.current = null;
2232
+ }
2233
+ };
2234
+ }, [apiUrl, projectId, sessionId, messages.length, onSessionClose]);
2235
+ const chatMessages = (0, import_react4.useMemo)(() => {
2236
+ return messages.map((msg) => ({
2237
+ id: msg.id,
2238
+ role: msg.role,
2239
+ content: msg.content,
2240
+ createdAt: msg.createdAt,
2241
+ senderType: msg.senderType
2242
+ }));
2243
+ }, [messages]);
2244
+ const clearHistory = (0, import_react4.useCallback)(() => {
2245
+ if (userId && messages.length > 0) {
2246
+ const hasUserMessages = messages.some((m) => m.role === "user");
2247
+ if (hasUserMessages) {
2248
+ saveSessionToRegistry(projectId, userId, sessionId, messages);
2249
+ }
2250
+ }
2251
+ const newSessionId = generateSessionId();
2252
+ setCurrentSessionId(newSessionId);
2253
+ sessionClosedRef.current = false;
2254
+ seenMessageIds.current = /* @__PURE__ */ new Set();
2255
+ if (eventSourceRef.current) {
2256
+ eventSourceRef.current.close();
2257
+ eventSourceRef.current = null;
2258
+ }
2259
+ if (updatesEventSourceRef.current) {
2260
+ updatesEventSourceRef.current.close();
2261
+ updatesEventSourceRef.current = null;
2262
+ }
2263
+ if (initialMessage) {
2264
+ setMessages([
2265
+ {
2266
+ id: generateMessageId(),
2267
+ role: "assistant",
2268
+ content: initialMessage,
2269
+ createdAt: /* @__PURE__ */ new Date(),
2270
+ senderType: "ai"
2271
+ }
2272
+ ]);
2273
+ } else {
2274
+ setMessages([]);
2275
+ }
2276
+ setError(void 0);
2277
+ setStreamingContent("");
2278
+ }, [projectId, userId, initialMessage, sessionId, messages]);
2279
+ const loadSession = (0, import_react4.useCallback)((newSessionId, sessionMessages) => {
2280
+ sessionClosedRef.current = false;
2281
+ seenMessageIds.current = /* @__PURE__ */ new Set();
2282
+ if (eventSourceRef.current) {
2283
+ eventSourceRef.current.close();
2284
+ eventSourceRef.current = null;
2285
+ }
2286
+ if (updatesEventSourceRef.current) {
2287
+ updatesEventSourceRef.current.close();
2288
+ updatesEventSourceRef.current = null;
2289
+ }
2290
+ setCurrentSessionId(newSessionId);
2291
+ if (sessionMessages && sessionMessages.length > 0) {
2292
+ setMessages(sessionMessages);
2293
+ saveMessagesToStorage(projectId, sessionMessages, newSessionId);
2294
+ } else {
2295
+ const storedMessages = loadMessagesFromStorage(projectId, newSessionId);
2296
+ if (storedMessages.length > 0) {
2297
+ setMessages(storedMessages);
2298
+ } else if (initialMessage) {
2299
+ setMessages([
2300
+ {
2301
+ id: generateMessageId(),
2302
+ role: "assistant",
2303
+ content: initialMessage,
2304
+ createdAt: /* @__PURE__ */ new Date(),
2305
+ senderType: "ai"
2306
+ }
2307
+ ]);
2308
+ } else {
2309
+ setMessages([]);
2310
+ }
2311
+ }
2312
+ setError(void 0);
2313
+ setStreamingContent("");
2314
+ }, [projectId, initialMessage]);
2315
+ const getSessionHistory = (0, import_react4.useCallback)(() => {
2316
+ if (!userId) return [];
2317
+ return loadSessionsRegistry(projectId, userId);
2318
+ }, [projectId, userId]);
2319
+ const deleteSession = (0, import_react4.useCallback)((sessionIdToDelete) => {
2320
+ if (!userId) return;
2321
+ deleteSessionFromRegistry(projectId, userId, sessionIdToDelete);
2322
+ }, [projectId, userId]);
2323
+ const closeSession = (0, import_react4.useCallback)(async () => {
2324
+ if (!sessionId || sessionClosedRef.current) return;
2325
+ sessionClosedRef.current = true;
2326
+ if (inactivityTimerRef.current) {
2327
+ clearTimeout(inactivityTimerRef.current);
2328
+ inactivityTimerRef.current = null;
2329
+ }
2330
+ try {
2331
+ await fetch(`${apiUrl}/session/close`, {
2332
+ method: "POST",
2333
+ headers: {
2334
+ "Content-Type": "application/json",
2335
+ ...headers
2336
+ },
2337
+ body: JSON.stringify({ sessionId, projectId })
2338
+ });
2339
+ onSessionClose?.();
2340
+ } catch (error2) {
2341
+ console.error("[HissunoWidget] Failed to close session:", error2);
2342
+ }
2343
+ }, [sessionId, apiUrl, projectId, headers, onSessionClose]);
2344
+ const cancelChat = (0, import_react4.useCallback)(async () => {
2345
+ try {
2346
+ const cancelUrl = `${apiUrl}/cancel`;
2347
+ const response = await fetch(cancelUrl, {
2348
+ method: "POST",
2349
+ headers: {
2350
+ "Content-Type": "application/json",
2351
+ ...headers
2352
+ },
2353
+ body: JSON.stringify({
2354
+ projectId,
2355
+ sessionId
2356
+ })
2357
+ });
2358
+ if (!response.ok) {
2359
+ const data = await response.json();
2360
+ throw new Error(data.error ?? "Failed to cancel");
2361
+ }
2362
+ if (eventSourceRef.current) {
2363
+ eventSourceRef.current.close();
2364
+ eventSourceRef.current = null;
2365
+ }
2366
+ setIsLoading(false);
2367
+ setIsStreaming(false);
2368
+ setStreamingContent("");
2369
+ } catch (err) {
2370
+ const message = err instanceof Error ? err.message : "Failed to cancel";
2371
+ setError(new Error(message));
2372
+ }
2373
+ }, [apiUrl, projectId, sessionId, headers]);
2374
+ (0, import_react4.useEffect)(() => {
2375
+ if (inactivityTimerRef.current) {
2376
+ clearTimeout(inactivityTimerRef.current);
2377
+ }
2378
+ if (messages.length > 0 && !sessionClosedRef.current && inactivityTimeout > 0) {
2379
+ inactivityTimerRef.current = setTimeout(() => {
2380
+ closeSession();
2381
+ }, inactivityTimeout);
2382
+ }
2383
+ return () => {
2384
+ if (inactivityTimerRef.current) {
2385
+ clearTimeout(inactivityTimerRef.current);
2386
+ }
2387
+ };
2388
+ }, [messages, inactivityTimeout, closeSession]);
2389
+ (0, import_react4.useEffect)(() => {
2390
+ const handleBeforeUnload = () => {
2391
+ if (!sessionId || sessionClosedRef.current) return;
2392
+ const url = `${apiUrl}/session/close`;
2393
+ const data = JSON.stringify({ sessionId, projectId });
2394
+ navigator.sendBeacon(url, data);
2395
+ };
2396
+ window.addEventListener("beforeunload", handleBeforeUnload);
2397
+ return () => window.removeEventListener("beforeunload", handleBeforeUnload);
2398
+ }, [sessionId, apiUrl, projectId]);
2399
+ const handleSubmit = (0, import_react4.useCallback)(
2400
+ async (e) => {
2401
+ if (e) {
2402
+ e.preventDefault();
2403
+ }
2404
+ const messageContent = input.trim();
2405
+ if (!messageContent) return;
2406
+ const validatedContent = validateMessageContent(messageContent);
2407
+ if (!validatedContent) {
2408
+ setError(new Error(`Message too long. Maximum ${MAX_MESSAGE_LENGTH} bytes allowed.`));
2409
+ return;
2410
+ }
2411
+ setError(void 0);
2412
+ setIsLoading(true);
2413
+ setInput("");
2414
+ const userMessage = {
2415
+ id: generateMessageId(),
2416
+ role: "user",
2417
+ content: messageContent,
2418
+ createdAt: /* @__PURE__ */ new Date()
2419
+ };
2420
+ setMessages((prev) => [...prev, userMessage]);
2421
+ try {
2422
+ const allMessages = [...messages, userMessage].map((msg) => ({
2423
+ role: msg.role,
2424
+ content: msg.content
2425
+ }));
2426
+ const response = await fetch(apiUrl, {
2427
+ method: "POST",
2428
+ headers: {
2429
+ "Content-Type": "application/json",
2430
+ ...headers
2431
+ },
2432
+ body: JSON.stringify({
2433
+ messages: allMessages,
2434
+ projectId,
2435
+ userId,
2436
+ userMetadata,
2437
+ pageUrl,
2438
+ pageTitle,
2439
+ sessionId,
2440
+ widgetToken
2441
+ })
2442
+ });
2443
+ if (!response.ok) {
2444
+ const data = await response.json();
2445
+ if (response.status === 409) {
2446
+ connectToStream();
2447
+ return;
2448
+ }
2449
+ throw new Error(data.error ?? "Failed to send message");
2450
+ }
2451
+ connectToStream();
2452
+ } catch (err) {
2453
+ const message = err instanceof Error ? err.message : "Failed to send message";
2454
+ setError(new Error(message));
2455
+ setIsLoading(false);
2456
+ }
2457
+ },
2458
+ [input, messages, apiUrl, headers, projectId, widgetToken, userId, userMetadata, pageUrl, pageTitle, sessionId, connectToStream]
2459
+ );
2460
+ return (0, import_react4.useMemo)(
2461
+ () => ({
2462
+ messages: chatMessages,
2463
+ input,
2464
+ setInput,
2465
+ handleSubmit,
2466
+ isLoading,
2467
+ isStreaming,
2468
+ streamingContent,
2469
+ error,
2470
+ clearHistory,
2471
+ currentSessionId: sessionId || null,
2472
+ loadSession,
2473
+ closeSession,
2474
+ cancelChat,
2475
+ getSessionHistory,
2476
+ deleteSession
2477
+ }),
2478
+ [chatMessages, input, setInput, handleSubmit, isLoading, isStreaming, streamingContent, error, clearHistory, sessionId, loadSession, closeSession, cancelChat, getSessionHistory, deleteSession]
2479
+ );
2480
+ }
2481
+
2482
+ // src/hooks/useKeyboardShortcut.ts
2483
+ var import_react5 = require("react");
2484
+ function isMac() {
2485
+ if (typeof window === "undefined") return false;
2486
+ return navigator.platform.toUpperCase().indexOf("MAC") >= 0;
2487
+ }
2488
+ function parseShortcut(shortcut) {
2489
+ const parts = shortcut.toLowerCase().split("+");
2490
+ const key = parts[parts.length - 1];
2491
+ const modifiers = parts.slice(0, -1);
2492
+ const mac = isMac();
2493
+ return {
2494
+ key,
2495
+ ctrl: modifiers.includes("ctrl") || !mac && modifiers.includes("mod"),
2496
+ meta: modifiers.includes("cmd") || modifiers.includes("meta") || mac && modifiers.includes("mod"),
2497
+ alt: modifiers.includes("alt") || modifiers.includes("option"),
2498
+ shift: modifiers.includes("shift")
2499
+ };
2500
+ }
2501
+ function matchesShortcut(event, parsed) {
2502
+ const keyMatch = event.key.toLowerCase() === parsed.key.toLowerCase();
2503
+ const ctrlMatch = event.ctrlKey === parsed.ctrl;
2504
+ const metaMatch = event.metaKey === parsed.meta;
2505
+ const altMatch = event.altKey === parsed.alt;
2506
+ const shiftMatch = event.shiftKey === parsed.shift;
2507
+ return keyMatch && ctrlMatch && metaMatch && altMatch && shiftMatch;
2508
+ }
2509
+ function useKeyboardShortcut({
2510
+ shortcut,
2511
+ onTrigger,
2512
+ enabled = true
2513
+ }) {
2514
+ const handleKeyDown = (0, import_react5.useCallback)(
2515
+ (event) => {
2516
+ if (!shortcut || !enabled) return;
2517
+ const parsed = parseShortcut(shortcut);
2518
+ if (matchesShortcut(event, parsed)) {
2519
+ event.preventDefault();
2520
+ event.stopPropagation();
2521
+ onTrigger();
2522
+ }
2523
+ },
2524
+ [shortcut, enabled, onTrigger]
2525
+ );
2526
+ (0, import_react5.useEffect)(() => {
2527
+ if (!shortcut || !enabled) return;
2528
+ document.addEventListener("keydown", handleKeyDown);
2529
+ return () => document.removeEventListener("keydown", handleKeyDown);
2530
+ }, [shortcut, enabled, handleKeyDown]);
2531
+ }
2532
+ function formatShortcut(shortcut) {
2533
+ const mac = isMac();
2534
+ const parts = shortcut.split("+");
2535
+ return parts.map((part) => {
2536
+ const lower = part.toLowerCase();
2537
+ switch (lower) {
2538
+ case "mod":
2539
+ return mac ? "\u2318" : "Ctrl";
2540
+ // Command symbol or Ctrl
2541
+ case "ctrl":
2542
+ return mac ? "\u2303" : "Ctrl";
2543
+ // Control symbol or Ctrl
2544
+ case "cmd":
2545
+ case "meta":
2546
+ return mac ? "\u2318" : "Win";
2547
+ // Command symbol or Win
2548
+ case "alt":
2549
+ case "option":
2550
+ return mac ? "\u2325" : "Alt";
2551
+ // Option symbol or Alt
2552
+ case "shift":
2553
+ return mac ? "\u21E7" : "Shift";
2554
+ // Shift symbol
2555
+ default:
2556
+ return part.toUpperCase();
2557
+ }
2558
+ }).join(mac ? "" : "+");
2559
+ }
2560
+
2561
+ // src/hooks/useResolvedTheme.ts
2562
+ var import_react6 = require("react");
2563
+ function useResolvedTheme(theme) {
2564
+ const [resolved, setResolved] = (0, import_react6.useState)(() => {
2565
+ if (theme !== "auto") return theme;
2566
+ if (typeof window === "undefined") return "light";
2567
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
2568
+ });
2569
+ (0, import_react6.useEffect)(() => {
2570
+ if (theme !== "auto") {
2571
+ setResolved(theme);
2572
+ return;
2573
+ }
2574
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
2575
+ const handleChange = (e) => {
2576
+ setResolved(e.matches ? "dark" : "light");
2577
+ };
2578
+ setResolved(mediaQuery.matches ? "dark" : "light");
2579
+ mediaQuery.addEventListener("change", handleChange);
2580
+ return () => mediaQuery.removeEventListener("change", handleChange);
2581
+ }, [theme]);
2582
+ return resolved;
2583
+ }
2584
+
2585
+ // src/HissunoWidget.tsx
2586
+ var import_jsx_runtime9 = require("react/jsx-runtime");
2587
+ var DEFAULT_API_URL = "/api/agent";
2588
+ function HissunoWidget({
2589
+ projectId,
2590
+ widgetToken,
2591
+ trigger: propTrigger,
2592
+ display: propDisplay,
2593
+ shortcut: propShortcut,
2594
+ fetchDefaults = true,
2595
+ userId,
2596
+ userMetadata,
2597
+ apiUrl = DEFAULT_API_URL,
2598
+ theme: propTheme,
2599
+ bubblePosition: propBubblePosition,
2600
+ bubbleOffset,
2601
+ drawerBadgeLabel: propDrawerBadgeLabel,
2602
+ dialogWidth: propDialogWidth,
2603
+ dialogHeight: propDialogHeight,
2604
+ renderTrigger,
2605
+ title: propTitle,
2606
+ placeholder = "Ask a question or report an issue...",
2607
+ initialMessage: propInitialMessage,
2608
+ defaultOpen = false,
2609
+ onOpen,
2610
+ onClose,
2611
+ className,
2612
+ headers = {}
2613
+ }) {
2614
+ const [isOpen, setIsOpen] = (0, import_react7.useState)(defaultOpen);
2615
+ const [isHistoryOpen, setIsHistoryOpen] = (0, import_react7.useState)(false);
2616
+ const [sessionHistory, setSessionHistory] = (0, import_react7.useState)([]);
2617
+ const {
2618
+ settings: serverSettings,
2619
+ blocked,
2620
+ loading: settingsLoading,
2621
+ error: settingsError
2622
+ } = useWidgetSettings(projectId || "", fetchDefaults && !!projectId, apiUrl, widgetToken);
2623
+ const resolveTrigger = () => {
2624
+ if (propTrigger) return propTrigger;
2625
+ if (renderTrigger) return "headless";
2626
+ if (serverSettings?.trigger) return serverSettings.trigger;
2627
+ return "bubble";
2628
+ };
2629
+ const resolvedTrigger = resolveTrigger();
2630
+ const resolveDisplay = () => {
2631
+ if (propDisplay) return propDisplay;
2632
+ if (serverSettings?.display) return serverSettings.display;
2633
+ return "sidepanel";
2634
+ };
2635
+ const resolvedDisplay = resolveDisplay();
2636
+ const resolvedShortcut = propShortcut !== void 0 ? propShortcut : serverSettings?.shortcut !== void 0 ? serverSettings.shortcut : "mod+k";
2637
+ const resolvedBaseTheme = propTheme ?? serverSettings?.theme ?? "light";
2638
+ const resolvedBubblePosition = propBubblePosition ?? serverSettings?.position ?? "bottom-right";
2639
+ const resolvedTitle = propTitle ?? serverSettings?.title ?? "Support";
2640
+ const resolvedInitialMessage = propInitialMessage ?? serverSettings?.initialMessage ?? "Hi! How can I help you today?";
2641
+ const resolvedDrawerBadgeLabel = propDrawerBadgeLabel ?? serverSettings?.drawerBadgeLabel ?? "Support";
2642
+ const resolvedDialogWidth = propDialogWidth ?? 600;
2643
+ const resolvedDialogHeight = propDialogHeight ?? 500;
2644
+ const {
2645
+ messages,
2646
+ input,
2647
+ setInput,
2648
+ handleSubmit,
2649
+ isLoading,
2650
+ isStreaming,
2651
+ streamingContent,
2652
+ error,
2653
+ clearHistory,
2654
+ closeSession,
2655
+ cancelChat,
2656
+ currentSessionId,
2657
+ loadSession,
2658
+ getSessionHistory,
2659
+ deleteSession
2660
+ } = useHissunoChat({
2661
+ projectId: projectId || "",
2662
+ widgetToken,
2663
+ apiUrl,
2664
+ initialMessage: resolvedInitialMessage,
2665
+ headers,
2666
+ userId,
2667
+ userMetadata
2668
+ });
2669
+ const canShowHistory = !!userId;
2670
+ const handleOpenHistory = (0, import_react7.useCallback)(() => {
2671
+ if (!canShowHistory) return;
2672
+ setSessionHistory(getSessionHistory());
2673
+ setIsHistoryOpen(true);
2674
+ }, [canShowHistory, getSessionHistory]);
2675
+ const handleCloseHistory = (0, import_react7.useCallback)(() => {
2676
+ setIsHistoryOpen(false);
2677
+ }, []);
2678
+ const handleSelectSession = (0, import_react7.useCallback)(
2679
+ (sessionId) => {
2680
+ loadSession(sessionId);
2681
+ setIsHistoryOpen(false);
2682
+ },
2683
+ [loadSession]
2684
+ );
2685
+ const handleDeleteSession = (0, import_react7.useCallback)(
2686
+ (sessionId) => {
2687
+ deleteSession(sessionId);
2688
+ setSessionHistory(getSessionHistory());
2689
+ },
2690
+ [deleteSession, getSessionHistory]
2691
+ );
2692
+ const open = (0, import_react7.useCallback)(() => {
2693
+ setIsOpen(true);
2694
+ onOpen?.();
2695
+ }, [onOpen]);
2696
+ const close = (0, import_react7.useCallback)(() => {
2697
+ setIsOpen(false);
2698
+ closeSession();
2699
+ onClose?.();
2700
+ }, [onClose, closeSession]);
2701
+ const toggle = (0, import_react7.useCallback)(() => {
2702
+ if (isOpen) {
2703
+ close();
2704
+ } else {
2705
+ open();
2706
+ }
2707
+ }, [isOpen, open, close]);
2708
+ useKeyboardShortcut({
2709
+ shortcut: resolvedShortcut,
2710
+ onTrigger: toggle,
2711
+ enabled: true
2712
+ });
2713
+ (0, import_react7.useEffect)(() => {
2714
+ if (defaultOpen && !isOpen) {
2715
+ setIsOpen(true);
2716
+ }
2717
+ }, [defaultOpen]);
2718
+ (0, import_react7.useEffect)(() => {
2719
+ const styleId = "hissuno-widget-styles";
2720
+ if (document.getElementById(styleId)) return;
2721
+ const style = document.createElement("style");
2722
+ style.id = styleId;
2723
+ style.textContent = `
2724
+ @keyframes hissuno-bounce {
2725
+ 0%, 80%, 100% { transform: translateY(0); }
2726
+ 40% { transform: translateY(-6px); }
2727
+ }
2728
+ @keyframes hissuno-spin {
2729
+ from { transform: rotate(0deg); }
2730
+ to { transform: rotate(360deg); }
2731
+ }
2732
+ @keyframes hissuno-scale-in {
2733
+ from { opacity: 0; transform: scale(0.95); }
2734
+ to { opacity: 1; transform: scale(1); }
2735
+ }
2736
+ @keyframes hissuno-slide-in-right {
2737
+ from { transform: translateX(100%); }
2738
+ to { transform: translateX(0); }
2739
+ }
2740
+ `;
2741
+ document.head.appendChild(style);
2742
+ }, []);
2743
+ const resolvedTheme = useResolvedTheme(resolvedBaseTheme);
2744
+ if (!projectId) {
2745
+ console.error("[HissunoWidget] projectId is required");
2746
+ return null;
2747
+ }
2748
+ if (settingsLoading) {
2749
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "hissuno-widget hissuno-widget-loading" });
2750
+ }
2751
+ if (blocked) {
2752
+ console.warn("[HissunoWidget] Widget blocked: origin not allowed for this project");
2753
+ return null;
2754
+ }
2755
+ if (settingsError && !serverSettings) {
2756
+ console.warn("[HissunoWidget] Widget not rendered: failed to fetch settings");
2757
+ return null;
2758
+ }
2759
+ if (serverSettings?.tokenRequired && !widgetToken) {
2760
+ console.warn(
2761
+ "[HissunoWidget] This project requires a widgetToken for secure authentication. Requests may fail."
2762
+ );
2763
+ }
2764
+ const renderWidgetTrigger = () => {
2765
+ if (renderTrigger) {
2766
+ return renderTrigger({ open, close, toggle, isOpen });
2767
+ }
2768
+ switch (resolvedTrigger) {
2769
+ case "bubble":
2770
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2771
+ ChatBubble,
2772
+ {
2773
+ isOpen,
2774
+ onClick: toggle,
2775
+ position: resolvedBubblePosition,
2776
+ offset: bubbleOffset,
2777
+ theme: resolvedTheme
2778
+ }
2779
+ );
2780
+ case "drawer-badge":
2781
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2782
+ DrawerBadge,
2783
+ {
2784
+ isOpen,
2785
+ onClick: toggle,
2786
+ label: resolvedDrawerBadgeLabel,
2787
+ theme: resolvedTheme
2788
+ }
2789
+ );
2790
+ case "headless":
2791
+ default:
2792
+ return null;
2793
+ }
2794
+ };
2795
+ const displayProps = {
2796
+ isOpen,
2797
+ onClose: close,
2798
+ messages,
2799
+ input,
2800
+ setInput,
2801
+ handleSubmit,
2802
+ isLoading,
2803
+ isStreaming,
2804
+ streamingContent,
2805
+ error,
2806
+ title: resolvedTitle,
2807
+ placeholder,
2808
+ theme: resolvedTheme,
2809
+ onClearHistory: clearHistory,
2810
+ onCancelChat: cancelChat,
2811
+ onOpenHistory: canShowHistory ? handleOpenHistory : void 0,
2812
+ isHistoryOpen,
2813
+ sessionHistory,
2814
+ currentSessionId,
2815
+ onCloseHistory: handleCloseHistory,
2816
+ onSelectSession: handleSelectSession,
2817
+ onDeleteSession: handleDeleteSession
2818
+ };
2819
+ const renderDisplay = () => {
2820
+ switch (resolvedDisplay) {
2821
+ case "popup":
2822
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2823
+ ChatPopup,
2824
+ {
2825
+ ...displayProps,
2826
+ position: resolvedBubblePosition,
2827
+ offset: bubbleOffset
2828
+ }
2829
+ );
2830
+ case "dialog":
2831
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2832
+ ChatDialog,
2833
+ {
2834
+ ...displayProps,
2835
+ width: resolvedDialogWidth,
2836
+ height: resolvedDialogHeight
2837
+ }
2838
+ );
2839
+ case "sidepanel":
2840
+ default:
2841
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ChatSidepanel, { ...displayProps });
2842
+ }
2843
+ };
2844
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(WidgetErrorBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: `hissuno-widget ${className ?? ""}`, children: [
2845
+ renderWidgetTrigger(),
2846
+ renderDisplay()
2847
+ ] }) });
2848
+ }
2849
+ function useWidgetSettings(projectId, enabled, apiUrl, widgetToken) {
2850
+ const [settings, setSettings] = (0, import_react7.useState)(null);
2851
+ const [blocked, setBlocked] = (0, import_react7.useState)(false);
2852
+ const [loading, setLoading] = (0, import_react7.useState)(enabled);
2853
+ const [error, setError] = (0, import_react7.useState)(false);
2854
+ (0, import_react7.useEffect)(() => {
2855
+ if (!enabled || !projectId) {
2856
+ setLoading(false);
2857
+ return;
2858
+ }
2859
+ setLoading(true);
2860
+ setError(false);
2861
+ const settingsUrl = `${apiUrl.replace(/\/api\/agent\/?$/, "/api/integrations/widget")}?projectId=${encodeURIComponent(projectId)}`;
2862
+ const controller = new AbortController();
2863
+ fetch(settingsUrl, {
2864
+ signal: controller.signal
2865
+ }).then((res) => {
2866
+ if (res.status === 403) {
2867
+ console.warn("[HissunoWidget] Origin not allowed for this project");
2868
+ setBlocked(true);
2869
+ return null;
2870
+ }
2871
+ if (!res.ok) {
2872
+ console.warn("[HissunoWidget] Failed to fetch widget settings:", res.status);
2873
+ return null;
2874
+ }
2875
+ return res.json();
2876
+ }).then((data) => {
2877
+ if (data) {
2878
+ const mappedSettings = {
2879
+ trigger: validateTrigger(data.trigger, "bubble"),
2880
+ display: validateDisplay(data.display, "sidepanel"),
2881
+ shortcut: data.shortcut ?? "mod+k",
2882
+ theme: validateTheme(data.theme, "light"),
2883
+ position: validatePosition(data.position, "bottom-right"),
2884
+ title: typeof data.title === "string" ? data.title : "Support",
2885
+ initialMessage: typeof data.initialMessage === "string" ? data.initialMessage : "Hi! How can I help you today?",
2886
+ drawerBadgeLabel: typeof data.drawerBadgeLabel === "string" ? data.drawerBadgeLabel : "Support",
2887
+ tokenRequired: Boolean(data.tokenRequired),
2888
+ blocked: Boolean(data.blocked)
2889
+ };
2890
+ setSettings(mappedSettings);
2891
+ if (data.blocked) {
2892
+ setBlocked(true);
2893
+ }
2894
+ }
2895
+ }).catch((err) => {
2896
+ if (err.name !== "AbortError") {
2897
+ console.warn("[HissunoWidget] Failed to fetch widget settings:", err);
2898
+ setError(true);
2899
+ }
2900
+ }).finally(() => {
2901
+ setLoading(false);
2902
+ });
2903
+ return () => controller.abort();
2904
+ }, [projectId, enabled, apiUrl]);
2905
+ const effectiveLoading = loading || enabled && !settings && !error && !blocked;
2906
+ return { settings, blocked, loading: effectiveLoading, error };
2907
+ }
2908
+ var WidgetErrorBoundary = class extends import_react7.Component {
2909
+ constructor() {
2910
+ super(...arguments);
2911
+ this.state = { hasError: false };
2912
+ }
2913
+ static getDerivedStateFromError() {
2914
+ return { hasError: true };
2915
+ }
2916
+ componentDidCatch(error, errorInfo) {
2917
+ console.error("[HissunoWidget] Error:", error, errorInfo);
2918
+ }
2919
+ render() {
2920
+ if (this.state.hasError) {
2921
+ return null;
2922
+ }
2923
+ return this.props.children;
2924
+ }
2925
+ };
2926
+ // Annotate the CommonJS export names for ESM import in node:
2927
+ 0 && (module.exports = {
2928
+ ChatBubble,
2929
+ ChatDialog,
2930
+ ChatMessages,
2931
+ ChatPopup,
2932
+ ChatSidepanel,
2933
+ ConversationHistory,
2934
+ DrawerBadge,
2935
+ HissunoWidget,
2936
+ HistoryIcon,
2937
+ SupportWidget,
2938
+ formatShortcut,
2939
+ useFocusTrap,
2940
+ useHissunoChat,
2941
+ useKeyboardShortcut,
2942
+ useResolvedTheme
2943
+ });