@customerhero/react 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,916 @@
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
+ ChatWidget: () => ChatWidget
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/components/chat-widget.tsx
28
+ var import_react9 = require("react");
29
+
30
+ // src/context.tsx
31
+ var import_react = require("react");
32
+ var import_js = require("@customerhero/js");
33
+ var import_jsx_runtime = require("react/jsx-runtime");
34
+ var CustomerHeroContext = (0, import_react.createContext)(null);
35
+ function CustomerHeroProvider({
36
+ children,
37
+ ...config
38
+ }) {
39
+ const clientRef = (0, import_react.useRef)(null);
40
+ if (!clientRef.current) {
41
+ clientRef.current = new import_js.CustomerHeroChat(config);
42
+ }
43
+ (0, import_react.useEffect)(() => {
44
+ clientRef.current?.fetchConfig();
45
+ }, []);
46
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CustomerHeroContext.Provider, { value: clientRef.current, children });
47
+ }
48
+ function useCustomerHeroClient() {
49
+ const client = (0, import_react.useContext)(CustomerHeroContext);
50
+ if (!client) {
51
+ throw new Error(
52
+ "useCustomerHeroClient must be used within a <CustomerHeroProvider>"
53
+ );
54
+ }
55
+ return client;
56
+ }
57
+
58
+ // src/components/chat-bubble.tsx
59
+ var import_react4 = require("react");
60
+
61
+ // src/use-chat.ts
62
+ var import_react2 = require("react");
63
+ function useChat() {
64
+ const client = useCustomerHeroClient();
65
+ const state = (0, import_react2.useSyncExternalStore)(
66
+ (0, import_react2.useCallback)((cb) => client.subscribe(cb), [client]),
67
+ () => client.getState(),
68
+ () => client.getState()
69
+ );
70
+ return {
71
+ ...state,
72
+ t: client.t,
73
+ sendMessage: (0, import_react2.useCallback)(
74
+ (message) => client.sendMessage(message),
75
+ [client]
76
+ ),
77
+ rateMessage: (0, import_react2.useCallback)(
78
+ (messageId, rating) => client.rateMessage(messageId, rating),
79
+ [client]
80
+ ),
81
+ toggle: (0, import_react2.useCallback)(() => client.toggle(), [client]),
82
+ open: (0, import_react2.useCallback)(() => client.open(), [client]),
83
+ close: (0, import_react2.useCallback)(() => client.close(), [client]),
84
+ reset: (0, import_react2.useCallback)(() => client.reset(), [client]),
85
+ identify: (0, import_react2.useCallback)(
86
+ (payload) => client.identify(payload),
87
+ [client]
88
+ )
89
+ };
90
+ }
91
+
92
+ // src/use-reduced-motion.ts
93
+ var import_react3 = require("react");
94
+ function useReducedMotion() {
95
+ const [reduced, setReduced] = (0, import_react3.useState)(() => {
96
+ if (typeof window === "undefined") return false;
97
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
98
+ });
99
+ (0, import_react3.useEffect)(() => {
100
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
101
+ const handler = (e) => setReduced(e.matches);
102
+ mq.addEventListener("change", handler);
103
+ return () => mq.removeEventListener("change", handler);
104
+ }, []);
105
+ return reduced;
106
+ }
107
+
108
+ // src/components/chat-bubble.tsx
109
+ var import_jsx_runtime2 = require("react/jsx-runtime");
110
+ function ChatBubble() {
111
+ const { toggle, config, t } = useChat();
112
+ const reduced = useReducedMotion();
113
+ const [mounted, setMounted] = (0, import_react4.useState)(false);
114
+ (0, import_react4.useEffect)(() => {
115
+ const id = requestAnimationFrame(() => setMounted(true));
116
+ return () => cancelAnimationFrame(id);
117
+ }, []);
118
+ const visible = mounted;
119
+ const style = {
120
+ position: "fixed",
121
+ bottom: 20,
122
+ [config.position === "bottom-left" ? "left" : "right"]: 20,
123
+ width: 56,
124
+ height: 56,
125
+ borderRadius: "50%",
126
+ background: config.primaryColor,
127
+ color: "white",
128
+ display: "flex",
129
+ alignItems: "center",
130
+ justifyContent: "center",
131
+ cursor: "pointer",
132
+ boxShadow: "0 4px 20px rgba(0,0,0,0.15)",
133
+ zIndex: 99999,
134
+ border: "none",
135
+ padding: 0,
136
+ opacity: visible ? 1 : 0,
137
+ transform: visible ? "scale(1)" : "scale(0.6)",
138
+ transition: reduced ? "none" : "opacity 0.25s ease, transform 0.25s ease",
139
+ pointerEvents: visible ? "auto" : "none"
140
+ };
141
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
142
+ "button",
143
+ {
144
+ onClick: toggle,
145
+ style,
146
+ "aria-label": t("open_chat"),
147
+ onMouseEnter: (e) => {
148
+ if (!reduced) e.currentTarget.style.transform = "scale(1.1)";
149
+ },
150
+ onMouseLeave: (e) => {
151
+ if (!reduced) e.currentTarget.style.transform = "scale(1)";
152
+ },
153
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
154
+ "svg",
155
+ {
156
+ width: "24",
157
+ height: "24",
158
+ viewBox: "0 0 24 24",
159
+ fill: "none",
160
+ stroke: "currentColor",
161
+ strokeWidth: "2",
162
+ strokeLinecap: "round",
163
+ strokeLinejoin: "round",
164
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.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" })
165
+ }
166
+ )
167
+ }
168
+ );
169
+ }
170
+
171
+ // src/components/chat-window.tsx
172
+ var import_react8 = require("react");
173
+
174
+ // src/components/chat-header.tsx
175
+ var import_react5 = require("react");
176
+ var import_jsx_runtime3 = require("react/jsx-runtime");
177
+ function ChatHeader() {
178
+ const { config, close, reset, t } = useChat();
179
+ const reduced = useReducedMotion();
180
+ const [menuOpen, setMenuOpen] = (0, import_react5.useState)(false);
181
+ const menuRef = (0, import_react5.useRef)(null);
182
+ (0, import_react5.useEffect)(() => {
183
+ if (!menuOpen) return;
184
+ const handleClick = (e) => {
185
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
186
+ setMenuOpen(false);
187
+ }
188
+ };
189
+ document.addEventListener("mousedown", handleClick);
190
+ return () => document.removeEventListener("mousedown", handleClick);
191
+ }, [menuOpen]);
192
+ const headerStyle = {
193
+ background: config.primaryColor,
194
+ padding: 16,
195
+ display: "flex",
196
+ alignItems: "center",
197
+ gap: 12
198
+ };
199
+ const avatarStyle = {
200
+ width: 36,
201
+ height: 36,
202
+ borderRadius: "50%",
203
+ background: "rgba(255,255,255,0.2)",
204
+ display: "flex",
205
+ alignItems: "center",
206
+ justifyContent: "center",
207
+ flexShrink: 0
208
+ };
209
+ const titleStyle = {
210
+ fontSize: 14,
211
+ fontWeight: 600,
212
+ color: "white",
213
+ margin: 0
214
+ };
215
+ const subtitleStyle = {
216
+ fontSize: 11,
217
+ color: "rgba(255,255,255,0.7)",
218
+ margin: 0
219
+ };
220
+ const headerButtonStyle = {
221
+ background: "none",
222
+ border: "none",
223
+ color: "white",
224
+ cursor: "pointer",
225
+ opacity: 0.7,
226
+ padding: 4
227
+ };
228
+ const menuStyle = {
229
+ position: "absolute",
230
+ top: "100%",
231
+ right: 0,
232
+ marginTop: 4,
233
+ background: "white",
234
+ borderRadius: 8,
235
+ boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
236
+ minWidth: 180,
237
+ overflow: "hidden",
238
+ zIndex: 10,
239
+ transformOrigin: "top right",
240
+ animation: reduced ? "none" : "ch-menu-in 0.15s ease"
241
+ };
242
+ const menuItemStyle = {
243
+ display: "flex",
244
+ alignItems: "center",
245
+ gap: 8,
246
+ width: "100%",
247
+ padding: "10px 14px",
248
+ border: "none",
249
+ background: "none",
250
+ cursor: "pointer",
251
+ fontSize: 13,
252
+ color: "#333",
253
+ textAlign: "left",
254
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
255
+ };
256
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: headerStyle, children: [
257
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: avatarStyle, children: config.avatarUrl ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
258
+ "img",
259
+ {
260
+ src: config.avatarUrl,
261
+ alt: "",
262
+ style: { width: 36, height: 36, borderRadius: "50%" }
263
+ }
264
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
265
+ "svg",
266
+ {
267
+ width: "18",
268
+ height: "18",
269
+ viewBox: "0 0 24 24",
270
+ fill: "none",
271
+ stroke: "white",
272
+ strokeWidth: "2",
273
+ strokeLinecap: "round",
274
+ strokeLinejoin: "round",
275
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.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" })
276
+ }
277
+ ) }),
278
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1 }, children: [
279
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { style: titleStyle, children: config.title }),
280
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: subtitleStyle, children: t("online") })
281
+ ] }),
282
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ref: menuRef, style: { position: "relative" }, children: [
283
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
284
+ "button",
285
+ {
286
+ onClick: () => setMenuOpen(!menuOpen),
287
+ style: headerButtonStyle,
288
+ "aria-label": t("menu"),
289
+ onMouseEnter: (e) => {
290
+ e.currentTarget.style.opacity = "1";
291
+ },
292
+ onMouseLeave: (e) => {
293
+ e.currentTarget.style.opacity = "0.7";
294
+ },
295
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
296
+ "svg",
297
+ {
298
+ width: "18",
299
+ height: "18",
300
+ viewBox: "0 0 24 24",
301
+ fill: "none",
302
+ stroke: "currentColor",
303
+ strokeWidth: "2",
304
+ strokeLinecap: "round",
305
+ strokeLinejoin: "round",
306
+ children: [
307
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "5", r: "1" }),
308
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "1" }),
309
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "19", r: "1" })
310
+ ]
311
+ }
312
+ )
313
+ }
314
+ ),
315
+ menuOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: menuStyle, children: [
316
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: `@keyframes ch-menu-in {
317
+ from { opacity: 0; transform: scale(0.9); }
318
+ to { opacity: 1; transform: scale(1); }
319
+ }` }),
320
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
321
+ "button",
322
+ {
323
+ style: menuItemStyle,
324
+ onClick: () => {
325
+ setMenuOpen(false);
326
+ reset();
327
+ },
328
+ onMouseEnter: (e) => {
329
+ e.currentTarget.style.background = "#f5f5f5";
330
+ },
331
+ onMouseLeave: (e) => {
332
+ e.currentTarget.style.background = "none";
333
+ },
334
+ children: [
335
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
336
+ "svg",
337
+ {
338
+ width: "14",
339
+ height: "14",
340
+ viewBox: "0 0 24 24",
341
+ fill: "none",
342
+ stroke: "currentColor",
343
+ strokeWidth: "2",
344
+ strokeLinecap: "round",
345
+ strokeLinejoin: "round",
346
+ children: [
347
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 20h9" }),
348
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" })
349
+ ]
350
+ }
351
+ ),
352
+ t("new_conversation")
353
+ ]
354
+ }
355
+ )
356
+ ] })
357
+ ] }),
358
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
359
+ "button",
360
+ {
361
+ onClick: close,
362
+ style: headerButtonStyle,
363
+ "aria-label": t("close_chat"),
364
+ onMouseEnter: (e) => {
365
+ e.currentTarget.style.opacity = "1";
366
+ },
367
+ onMouseLeave: (e) => {
368
+ e.currentTarget.style.opacity = "0.7";
369
+ },
370
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
371
+ "svg",
372
+ {
373
+ width: "20",
374
+ height: "20",
375
+ viewBox: "0 0 24 24",
376
+ fill: "none",
377
+ stroke: "currentColor",
378
+ strokeWidth: "2",
379
+ children: [
380
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
381
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
382
+ ]
383
+ }
384
+ )
385
+ }
386
+ )
387
+ ] });
388
+ }
389
+
390
+ // src/components/chat-messages.tsx
391
+ var import_react6 = require("react");
392
+ var import_jsx_runtime4 = require("react/jsx-runtime");
393
+ function MessageRatingButtons({
394
+ messageId,
395
+ onRate,
396
+ primaryColor,
397
+ t,
398
+ reduced
399
+ }) {
400
+ const [rated, setRated] = (0, import_react6.useState)(null);
401
+ const handleRate = (rating) => {
402
+ setRated(rating);
403
+ onRate(messageId, rating);
404
+ };
405
+ const buttonStyle = (isRated) => ({
406
+ background: isRated ? `${primaryColor}11` : "none",
407
+ border: `1px solid ${isRated ? primaryColor : "#ddd"}`,
408
+ borderRadius: 4,
409
+ padding: "4px 6px",
410
+ cursor: rated ? "default" : "pointer",
411
+ color: isRated ? primaryColor : "#888",
412
+ display: "flex",
413
+ alignItems: "center",
414
+ justifyContent: "center",
415
+ transition: reduced ? "color 0.15s" : "all 0.15s",
416
+ transform: isRated && !reduced ? "scale(1.15)" : "scale(1)"
417
+ });
418
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: 4, marginTop: 4 }, children: [
419
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
420
+ "button",
421
+ {
422
+ onClick: () => handleRate("positive"),
423
+ disabled: rated !== null,
424
+ style: buttonStyle(rated === "positive"),
425
+ title: t("helpful"),
426
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
427
+ "svg",
428
+ {
429
+ width: "14",
430
+ height: "14",
431
+ viewBox: "0 0 24 24",
432
+ fill: "none",
433
+ stroke: "currentColor",
434
+ strokeWidth: "2",
435
+ strokeLinecap: "round",
436
+ strokeLinejoin: "round",
437
+ children: [
438
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M7 10v12" }),
439
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z" })
440
+ ]
441
+ }
442
+ )
443
+ }
444
+ ),
445
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
446
+ "button",
447
+ {
448
+ onClick: () => handleRate("negative"),
449
+ disabled: rated !== null,
450
+ style: buttonStyle(rated === "negative"),
451
+ title: t("not_helpful"),
452
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
453
+ "svg",
454
+ {
455
+ width: "14",
456
+ height: "14",
457
+ viewBox: "0 0 24 24",
458
+ fill: "none",
459
+ stroke: "currentColor",
460
+ strokeWidth: "2",
461
+ strokeLinecap: "round",
462
+ strokeLinejoin: "round",
463
+ children: [
464
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M17 14V2" }),
465
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22h0a3.13 3.13 0 0 1-3-3.88Z" })
466
+ ]
467
+ }
468
+ )
469
+ }
470
+ )
471
+ ] });
472
+ }
473
+ function AnimatedMessage({
474
+ children,
475
+ isUser,
476
+ animate,
477
+ reduced
478
+ }) {
479
+ const [visible, setVisible] = (0, import_react6.useState)(!animate);
480
+ (0, import_react6.useEffect)(() => {
481
+ if (animate && !reduced) {
482
+ const id = requestAnimationFrame(() => setVisible(true));
483
+ return () => cancelAnimationFrame(id);
484
+ }
485
+ }, [animate, reduced]);
486
+ const style = {
487
+ alignSelf: isUser ? "flex-end" : "flex-start",
488
+ maxWidth: "80%",
489
+ opacity: visible ? 1 : 0,
490
+ transform: visible ? "translateX(0)" : `translateX(${isUser ? "12px" : "-12px"})`,
491
+ transition: animate && !reduced ? "opacity 0.2s ease, transform 0.2s ease" : "none"
492
+ };
493
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style, children });
494
+ }
495
+ function Message({
496
+ message,
497
+ config,
498
+ onRate,
499
+ hasConversation,
500
+ t,
501
+ animate,
502
+ reduced
503
+ }) {
504
+ const isUser = message.role === "user";
505
+ const bubbleStyle = {
506
+ padding: "10px 14px",
507
+ borderRadius: 16,
508
+ fontSize: 14,
509
+ lineHeight: 1.5,
510
+ wordBreak: "break-word",
511
+ ...isUser ? {
512
+ background: config.primaryColor,
513
+ color: "white",
514
+ borderBottomRightRadius: 4
515
+ } : {
516
+ background: "#f0f0f0",
517
+ color: config.textColor,
518
+ borderBottomLeftRadius: 4
519
+ }
520
+ };
521
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(AnimatedMessage, { isUser, animate, reduced, children: [
522
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: bubbleStyle, children: message.content }),
523
+ message.role === "bot" && message.id && hasConversation && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
524
+ MessageRatingButtons,
525
+ {
526
+ messageId: message.id,
527
+ onRate,
528
+ primaryColor: config.primaryColor,
529
+ t,
530
+ reduced
531
+ }
532
+ )
533
+ ] });
534
+ }
535
+ function TypingDots({ reduced }) {
536
+ const dotStyle = (delay) => ({
537
+ width: 6,
538
+ height: 6,
539
+ borderRadius: "50%",
540
+ background: "#999",
541
+ animation: reduced ? "none" : `ch-dot-pulse 1.2s ease-in-out ${delay}s infinite`
542
+ });
543
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
544
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `@keyframes ch-dot-pulse {
545
+ 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
546
+ 40% { opacity: 1; transform: scale(1); }
547
+ }` }),
548
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
549
+ "div",
550
+ {
551
+ style: {
552
+ alignSelf: "flex-start",
553
+ padding: "12px 16px",
554
+ borderRadius: 16,
555
+ borderBottomLeftRadius: 4,
556
+ background: "#f0f0f0",
557
+ display: "flex",
558
+ gap: 4,
559
+ alignItems: "center"
560
+ },
561
+ children: [
562
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: dotStyle(0) }),
563
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: dotStyle(0.2) }),
564
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: dotStyle(0.4) })
565
+ ]
566
+ }
567
+ )
568
+ ] });
569
+ }
570
+ function ChatMessages() {
571
+ const { messages, isLoading, error, config, conversationId, rateMessage, t } = useChat();
572
+ const reduced = useReducedMotion();
573
+ const messagesEndRef = (0, import_react6.useRef)(null);
574
+ const isFirstRender = (0, import_react6.useRef)(true);
575
+ const prevMessageCount = (0, import_react6.useRef)(0);
576
+ (0, import_react6.useEffect)(() => {
577
+ if (messagesEndRef.current) {
578
+ messagesEndRef.current.scrollIntoView({
579
+ behavior: isFirstRender.current || reduced ? "auto" : "smooth"
580
+ });
581
+ isFirstRender.current = false;
582
+ }
583
+ }, [messages, isLoading, reduced]);
584
+ const newStartIndex = isFirstRender.current ? messages.length : prevMessageCount.current;
585
+ (0, import_react6.useEffect)(() => {
586
+ prevMessageCount.current = messages.length;
587
+ }, [messages.length]);
588
+ const containerStyle = {
589
+ flex: 1,
590
+ overflowY: "auto",
591
+ padding: 16,
592
+ display: "flex",
593
+ flexDirection: "column",
594
+ gap: 12
595
+ };
596
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: containerStyle, children: [
597
+ messages.map((msg, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
598
+ Message,
599
+ {
600
+ message: msg,
601
+ config,
602
+ onRate: rateMessage,
603
+ hasConversation: conversationId !== null,
604
+ t,
605
+ animate: i >= newStartIndex,
606
+ reduced
607
+ },
608
+ i
609
+ )),
610
+ isLoading && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TypingDots, { reduced }),
611
+ error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
612
+ "div",
613
+ {
614
+ style: {
615
+ alignSelf: "flex-start",
616
+ padding: "10px 14px",
617
+ borderRadius: 16,
618
+ borderBottomLeftRadius: 4,
619
+ fontSize: 13,
620
+ background: "#fee2e2",
621
+ color: "#b91c1c"
622
+ },
623
+ children: error
624
+ }
625
+ ),
626
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: messagesEndRef })
627
+ ] });
628
+ }
629
+
630
+ // src/components/chat-suggestions.tsx
631
+ var import_jsx_runtime5 = require("react/jsx-runtime");
632
+ function ChatSuggestions() {
633
+ const { messages, isLoading, config, sendMessage } = useChat();
634
+ const reduced = useReducedMotion();
635
+ const hasUserMessage = messages.some((m) => m.role === "user");
636
+ if (config.suggestedMessages.length === 0 || hasUserMessage || isLoading) {
637
+ return null;
638
+ }
639
+ const containerStyle = {
640
+ padding: "8px 16px",
641
+ display: "flex",
642
+ flexWrap: "wrap",
643
+ gap: 6,
644
+ justifyContent: "flex-end"
645
+ };
646
+ const chipStyle = {
647
+ background: "none",
648
+ border: "1px solid #e0e0e0",
649
+ borderRadius: 20,
650
+ padding: "7px 14px",
651
+ fontSize: 13,
652
+ color: "#333",
653
+ cursor: "pointer",
654
+ textAlign: "left",
655
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
656
+ transition: reduced ? "none" : "border-color 0.15s, background 0.15s"
657
+ };
658
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: containerStyle, children: config.suggestedMessages.map((text) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
659
+ "button",
660
+ {
661
+ style: chipStyle,
662
+ onClick: () => sendMessage(text),
663
+ onMouseEnter: (e) => {
664
+ e.currentTarget.style.borderColor = config.primaryColor;
665
+ e.currentTarget.style.background = `${config.primaryColor}08`;
666
+ },
667
+ onMouseLeave: (e) => {
668
+ e.currentTarget.style.borderColor = "#e0e0e0";
669
+ e.currentTarget.style.background = "none";
670
+ },
671
+ children: text
672
+ },
673
+ text
674
+ )) });
675
+ }
676
+
677
+ // src/components/chat-input.tsx
678
+ var import_react7 = require("react");
679
+ var import_jsx_runtime6 = require("react/jsx-runtime");
680
+ function ChatInput() {
681
+ const { sendMessage, isLoading, config, t } = useChat();
682
+ const reduced = useReducedMotion();
683
+ const [value, setValue] = (0, import_react7.useState)("");
684
+ const handleSend = () => {
685
+ if (!value.trim() || isLoading) return;
686
+ sendMessage(value);
687
+ setValue("");
688
+ };
689
+ const handleKeyDown = (e) => {
690
+ if (e.key === "Enter" && !e.shiftKey) {
691
+ e.preventDefault();
692
+ handleSend();
693
+ }
694
+ };
695
+ const containerStyle = {
696
+ padding: "12px 16px",
697
+ borderTop: "1px solid #eee",
698
+ display: "flex",
699
+ alignItems: "center",
700
+ gap: 8
701
+ };
702
+ const inputStyle = {
703
+ flex: 1,
704
+ border: "1px solid #e0e0e0",
705
+ borderRadius: 24,
706
+ padding: "10px 16px",
707
+ fontSize: 14,
708
+ outline: "none",
709
+ background: "#fafafa",
710
+ color: config.textColor,
711
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
712
+ };
713
+ const buttonStyle = {
714
+ width: 36,
715
+ height: 36,
716
+ borderRadius: "50%",
717
+ background: config.primaryColor,
718
+ border: "none",
719
+ color: "white",
720
+ cursor: isLoading ? "not-allowed" : "pointer",
721
+ display: "flex",
722
+ alignItems: "center",
723
+ justifyContent: "center",
724
+ opacity: isLoading ? 0.5 : 1,
725
+ transition: reduced ? "opacity 0.2s" : "opacity 0.2s, transform 0.15s",
726
+ flexShrink: 0,
727
+ transform: "scale(1)"
728
+ };
729
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: containerStyle, children: [
730
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
731
+ "input",
732
+ {
733
+ type: "text",
734
+ value,
735
+ onChange: (e) => setValue(e.target.value),
736
+ onKeyDown: handleKeyDown,
737
+ placeholder: config.placeholderText,
738
+ style: inputStyle,
739
+ disabled: isLoading
740
+ }
741
+ ),
742
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
743
+ "button",
744
+ {
745
+ onClick: handleSend,
746
+ disabled: isLoading || !value.trim(),
747
+ style: buttonStyle,
748
+ "aria-label": t("send_message"),
749
+ onMouseEnter: (e) => {
750
+ if (!reduced && !isLoading)
751
+ e.currentTarget.style.transform = "scale(1.1)";
752
+ },
753
+ onMouseLeave: (e) => {
754
+ if (!reduced) e.currentTarget.style.transform = "scale(1)";
755
+ },
756
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
757
+ "svg",
758
+ {
759
+ width: "16",
760
+ height: "16",
761
+ viewBox: "0 0 24 24",
762
+ fill: "none",
763
+ stroke: "currentColor",
764
+ strokeWidth: "2",
765
+ strokeLinecap: "round",
766
+ strokeLinejoin: "round",
767
+ children: [
768
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
769
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
770
+ ]
771
+ }
772
+ )
773
+ }
774
+ )
775
+ ] });
776
+ }
777
+
778
+ // src/components/chat-window.tsx
779
+ var import_jsx_runtime7 = require("react/jsx-runtime");
780
+ function ConfigError({ message, title }) {
781
+ const errorStyle = {
782
+ flex: 1,
783
+ display: "flex",
784
+ flexDirection: "column",
785
+ alignItems: "center",
786
+ justifyContent: "center",
787
+ padding: 24,
788
+ textAlign: "center",
789
+ gap: 8
790
+ };
791
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: errorStyle, children: [
792
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
793
+ "svg",
794
+ {
795
+ width: "32",
796
+ height: "32",
797
+ viewBox: "0 0 24 24",
798
+ fill: "none",
799
+ stroke: "#999",
800
+ strokeWidth: "2",
801
+ strokeLinecap: "round",
802
+ strokeLinejoin: "round",
803
+ children: [
804
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
805
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
806
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
807
+ ]
808
+ }
809
+ ),
810
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { style: { fontSize: 14, fontWeight: 500, color: "#333", margin: 0 }, children: title }),
811
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { style: { fontSize: 12, color: "#999", margin: 0 }, children: message })
812
+ ] });
813
+ }
814
+ function ChatWindow() {
815
+ const { isOpen, config, configError, t } = useChat();
816
+ const reduced = useReducedMotion();
817
+ const [visible, setVisible] = (0, import_react8.useState)(false);
818
+ const [shouldRender, setShouldRender] = (0, import_react8.useState)(false);
819
+ (0, import_react8.useEffect)(() => {
820
+ if (isOpen) {
821
+ setShouldRender(true);
822
+ requestAnimationFrame(() => {
823
+ requestAnimationFrame(() => setVisible(true));
824
+ });
825
+ } else {
826
+ setVisible(false);
827
+ if (reduced) {
828
+ setShouldRender(false);
829
+ } else {
830
+ const timer = setTimeout(() => setShouldRender(false), 250);
831
+ return () => clearTimeout(timer);
832
+ }
833
+ }
834
+ }, [isOpen, reduced]);
835
+ if (!shouldRender) return null;
836
+ const style = {
837
+ position: "fixed",
838
+ bottom: 90,
839
+ [config.position === "bottom-left" ? "left" : "right"]: 20,
840
+ width: 380,
841
+ maxWidth: "calc(100vw - 40px)",
842
+ height: 520,
843
+ maxHeight: "calc(100vh - 120px)",
844
+ borderRadius: 16,
845
+ overflow: "hidden",
846
+ display: "flex",
847
+ flexDirection: "column",
848
+ boxShadow: "0 8px 40px rgba(0,0,0,0.15)",
849
+ zIndex: 99999,
850
+ background: config.backgroundColor,
851
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
852
+ opacity: visible ? 1 : 0,
853
+ transform: visible ? "translateY(0) scale(1)" : "translateY(16px) scale(0.97)",
854
+ transition: reduced ? "none" : "opacity 0.25s ease, transform 0.25s ease"
855
+ };
856
+ const poweredStyle = {
857
+ textAlign: "center",
858
+ padding: 6,
859
+ fontSize: 10,
860
+ color: "#aaa"
861
+ };
862
+ const linkStyle = {
863
+ color: "#888",
864
+ textDecoration: "underline",
865
+ textUnderlineOffset: 2
866
+ };
867
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style, children: [
868
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChatHeader, {}),
869
+ configError ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ConfigError, { title: t("unable_to_load"), message: configError }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
870
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChatMessages, {}),
871
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChatSuggestions, {}),
872
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChatInput, {})
873
+ ] }),
874
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: poweredStyle, children: [
875
+ t("powered_by"),
876
+ " ",
877
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
878
+ "a",
879
+ {
880
+ href: "https://customerhero.app",
881
+ target: "_blank",
882
+ rel: "noopener noreferrer",
883
+ style: linkStyle,
884
+ children: "CustomerHero"
885
+ }
886
+ )
887
+ ] })
888
+ ] });
889
+ }
890
+
891
+ // src/components/chat-widget.tsx
892
+ var import_jsx_runtime8 = require("react/jsx-runtime");
893
+ function ChatWidgetInner({ identity }) {
894
+ const client = useCustomerHeroClient();
895
+ const prevIdentityRef = (0, import_react9.useRef)(void 0);
896
+ (0, import_react9.useEffect)(() => {
897
+ const key = identity ? JSON.stringify(identity) : void 0;
898
+ if (key !== prevIdentityRef.current) {
899
+ prevIdentityRef.current = key;
900
+ if (identity) {
901
+ client.identify(identity);
902
+ }
903
+ }
904
+ }, [identity, client]);
905
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
906
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ChatBubble, {}),
907
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ChatWindow, {})
908
+ ] });
909
+ }
910
+ function ChatWidget({ identity, ...config }) {
911
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CustomerHeroProvider, { ...config, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ChatWidgetInner, { identity }) });
912
+ }
913
+ // Annotate the CommonJS export names for ESM import in node:
914
+ 0 && (module.exports = {
915
+ ChatWidget
916
+ });