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