@reechdesk/react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,760 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
3
+ import PusherJS from 'pusher-js';
4
+
5
+ const HelpdeskContext = createContext(null);
6
+ function useHelpdesk() {
7
+ const ctx = useContext(HelpdeskContext);
8
+ if (!ctx)
9
+ throw new Error('useHelpdesk must be used within HelpdeskProvider');
10
+ return ctx;
11
+ }
12
+ const DEFAULT_API_URL = 'https://api.reechdesk.com';
13
+ const DEFAULT_BRANDING = {
14
+ entityName: 'Support',
15
+ logoUrl: null,
16
+ primaryColor: '#f26522',
17
+ greeting: 'Hi! How can we help you today?',
18
+ position: 'bottom-right',
19
+ borderRadius: 12,
20
+ poweredBy: true,
21
+ soundEnabled: true,
22
+ };
23
+ function HelpdeskProvider({ entityId, apiUrl, user, pusherKey, children, }) {
24
+ const [sessionId, setSessionId] = useState(null);
25
+ useEffect(() => {
26
+ const params = new URLSearchParams(window.location.search);
27
+ if (params.get('chat') === 'resume' && params.get('id')) {
28
+ setSessionId(params.get('id'));
29
+ }
30
+ }, []);
31
+ const [pusher, setPusher] = useState(null);
32
+ const [resolvedApiUrl] = useState(apiUrl || DEFAULT_API_URL);
33
+ const [branding, setBranding] = useState(DEFAULT_BRANDING);
34
+ useEffect(() => {
35
+ if (!resolvedApiUrl)
36
+ return;
37
+ fetch(`${resolvedApiUrl}/api/chat/pusher-config?entityId=${entityId}`)
38
+ .then((r) => r.json())
39
+ .then((data) => {
40
+ var _a, _b, _c;
41
+ if (data.pusherKey && !pusherKey) {
42
+ const client = new PusherJS(data.pusherKey, { cluster: 'us2' });
43
+ setPusher(client);
44
+ }
45
+ if (data.entityName) {
46
+ setBranding({
47
+ entityName: data.entityName,
48
+ logoUrl: data.logoUrl || null,
49
+ primaryColor: data.primaryColor || '#f26522',
50
+ greeting: data.greeting || 'Hi! How can we help you today?',
51
+ position: data.position || 'bottom-right',
52
+ borderRadius: (_a = data.borderRadius) !== null && _a !== void 0 ? _a : 12,
53
+ poweredBy: (_b = data.poweredBy) !== null && _b !== void 0 ? _b : true,
54
+ soundEnabled: (_c = data.soundEnabled) !== null && _c !== void 0 ? _c : true,
55
+ });
56
+ }
57
+ })
58
+ .catch(() => { });
59
+ }, [pusherKey, resolvedApiUrl, entityId]);
60
+ useEffect(() => {
61
+ if (pusherKey) {
62
+ const client = new PusherJS(pusherKey, { cluster: 'us2' });
63
+ setPusher(client);
64
+ return () => client.disconnect();
65
+ }
66
+ }, [pusherKey]);
67
+ return (jsx(HelpdeskContext.Provider, { value: { entityId, apiUrl: resolvedApiUrl, user, sessionId, setSessionId, pusher, branding }, children: children }));
68
+ }
69
+
70
+ function ChatIcon({ size = 24 }) {
71
+ return (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "currentColor", children: 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" }) }));
72
+ }
73
+ function CloseIcon({ size = 20 }) {
74
+ return (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }));
75
+ }
76
+ function UserIcon() {
77
+ return (jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "#9c948a", children: jsx("path", { d: "M12 12c2.7 0 5-2.3 5-5s-2.3-5-5-5-5 2.3-5 5 2.3 5 5 5zm0 2c-3.3 0-10 1.7-10 5v2h20v-2c0-3.3-6.7-5-10-5z" }) }));
78
+ }
79
+ function MailIcon() {
80
+ return (jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "#9c948a", children: jsx("path", { d: "M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4-8 5-8-5V6l8 5 8-5v2z" }) }));
81
+ }
82
+ function SendIcon() {
83
+ return (jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }), jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })] }));
84
+ }
85
+ function PaperclipIcon() {
86
+ return (jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#9c948a", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" }) }));
87
+ }
88
+ function ReechdeskLogo() {
89
+ return (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: [jsx("rect", { x: "2", y: "2", width: "20", height: "20", rx: "4", fill: "#f26522" }), jsx("path", { d: "M8 8h8v2H8zM8 12h6v2H8z", fill: "#fff" })] }));
90
+ }
91
+ const REECHDESK_LOGO = 'data:image/svg+xml,' + encodeURIComponent('<svg width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="4" fill="#f26522"/><path d="M8 8h8v2H8zM8 12h6v2H8z" fill="#fff"/></svg>');
92
+ function ChatWidget({ position, primaryColor, greeting, entityName, logoUrl, borderRadius, inputBorderRadius = 4, poweredBy, soundEnabled, }) {
93
+ const { entityId, apiUrl, user, sessionId, setSessionId, pusher, branding } = useHelpdesk();
94
+ const color = primaryColor || branding.primaryColor;
95
+ const name = entityName || branding.entityName;
96
+ const logo = logoUrl || branding.logoUrl || REECHDESK_LOGO;
97
+ const pos = position || branding.position;
98
+ const rad = borderRadius !== null && borderRadius !== void 0 ? borderRadius : branding.borderRadius;
99
+ const greet = greeting || branding.greeting;
100
+ const showPoweredBy = poweredBy !== null && poweredBy !== void 0 ? poweredBy : branding.poweredBy;
101
+ const showSound = soundEnabled !== null && soundEnabled !== void 0 ? soundEnabled : branding.soundEnabled;
102
+ const [open, setOpen] = useState(false);
103
+ const [messages, setMessages] = useState([]);
104
+ const [input, setInput] = useState('');
105
+ const [visitorName, setVisitorName] = useState((user === null || user === void 0 ? void 0 : user.name) || '');
106
+ const [visitorEmail, setVisitorEmail] = useState((user === null || user === void 0 ? void 0 : user.email) || '');
107
+ const [started, setStarted] = useState(false);
108
+ const [sending, setSending] = useState(false);
109
+ const [pendingFiles, setPendingFiles] = useState([]);
110
+ const [agents, setAgents] = useState([]);
111
+ const [agentsOnline, setAgentsOnline] = useState(false);
112
+ const messagesEnd = useRef(null);
113
+ const fileInputRef = useRef(null);
114
+ const nameInputRef = useRef(null);
115
+ const visitorIdRef = useRef('');
116
+ useEffect(() => {
117
+ let stored = sessionStorage.getItem('rd_visitor_id');
118
+ if (!stored) {
119
+ stored = `v-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
120
+ sessionStorage.setItem('rd_visitor_id', stored);
121
+ }
122
+ visitorIdRef.current = stored;
123
+ }, []);
124
+ function playChime() {
125
+ if (!showSound || typeof window === 'undefined')
126
+ return;
127
+ try {
128
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
129
+ if (!AudioContextClass)
130
+ return;
131
+ const ctx = new AudioContextClass();
132
+ const osc = ctx.createOscillator();
133
+ const gain = ctx.createGain();
134
+ osc.connect(gain);
135
+ gain.connect(ctx.destination);
136
+ osc.type = 'sine';
137
+ osc.frequency.setValueAtTime(880, ctx.currentTime);
138
+ osc.frequency.setValueAtTime(1100, ctx.currentTime + 0.08);
139
+ gain.gain.setValueAtTime(0.3, ctx.currentTime);
140
+ gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3);
141
+ osc.start(ctx.currentTime);
142
+ osc.stop(ctx.currentTime + 0.3);
143
+ }
144
+ catch (_a) {
145
+ // AudioContext not available
146
+ }
147
+ }
148
+ useEffect(() => {
149
+ if (!apiUrl || !entityId)
150
+ return;
151
+ const vid = visitorIdRef.current;
152
+ if (!vid)
153
+ return;
154
+ const ping = () => {
155
+ fetch(`${apiUrl}/api/chat/presence`, {
156
+ method: 'POST',
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify({ entityId, visitorId: vid, visitorName: visitorName || 'Anonymous Visitor' }),
159
+ }).catch(() => { });
160
+ };
161
+ ping();
162
+ const interval = setInterval(ping, 15000);
163
+ const handleBeforeUnload = () => {
164
+ try {
165
+ const blob = new Blob([JSON.stringify({ entityId, visitorId: vid })], { type: 'application/json' });
166
+ navigator.sendBeacon(`${apiUrl}/api/chat/presence`, blob);
167
+ }
168
+ catch (_a) {
169
+ // ignore
170
+ }
171
+ };
172
+ window.addEventListener('beforeunload', handleBeforeUnload);
173
+ return () => {
174
+ clearInterval(interval);
175
+ window.removeEventListener('beforeunload', handleBeforeUnload);
176
+ ping();
177
+ };
178
+ }, [apiUrl, entityId, visitorName]);
179
+ // Fetch agent presence when widget opens
180
+ useEffect(() => {
181
+ if (!apiUrl || !entityId || !open)
182
+ return;
183
+ const fetchAgentPresence = () => {
184
+ fetch(`${apiUrl}/api/chat/agent-presence?entityId=${entityId}`)
185
+ .then((r) => r.json())
186
+ .then((data) => {
187
+ if (Array.isArray(data)) {
188
+ setAgents(data);
189
+ setAgentsOnline(data.length > 0);
190
+ }
191
+ })
192
+ .catch(() => { });
193
+ };
194
+ fetchAgentPresence();
195
+ const interval = setInterval(fetchAgentPresence, 20000);
196
+ return () => clearInterval(interval);
197
+ }, [apiUrl, entityId, open]);
198
+ // Resume chat from email link - load existing messages
199
+ useEffect(() => {
200
+ if (!sessionId || started || !apiUrl)
201
+ return;
202
+ fetch(`${apiUrl}/api/chat/sessions/${sessionId}`)
203
+ .then((r) => r.json())
204
+ .then((msgs) => {
205
+ if (Array.isArray(msgs) && msgs.length > 0) {
206
+ setMessages(msgs.map((m) => ({
207
+ id: m.id,
208
+ senderType: m.senderType,
209
+ body: m.body,
210
+ createdAt: m.createdAt,
211
+ senderId: m.senderId,
212
+ senderName: m.senderName,
213
+ senderAvatar: m.senderAvatar,
214
+ attachments: m.attachments,
215
+ })));
216
+ setStarted(true);
217
+ setOpen(true);
218
+ }
219
+ })
220
+ .catch(() => { });
221
+ }, [sessionId, started, apiUrl]);
222
+ useEffect(() => {
223
+ var _a;
224
+ (_a = messagesEnd.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' });
225
+ }, [messages]);
226
+ useEffect(() => {
227
+ if (open && !started) {
228
+ setTimeout(() => { var _a; return (_a = nameInputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, 100);
229
+ }
230
+ }, [open, started]);
231
+ useEffect(() => {
232
+ if (sessionId && pusher) {
233
+ const channel = pusher.subscribe(`chat.${sessionId}`);
234
+ channel.bind('chat:message', (msg) => {
235
+ setMessages((prev) => [...prev, msg]);
236
+ if (msg.senderType !== 'visitor') {
237
+ playChime();
238
+ }
239
+ });
240
+ return () => {
241
+ pusher.unsubscribe(`chat.${sessionId}`);
242
+ };
243
+ }
244
+ }, [sessionId, pusher]);
245
+ const startChat = async () => {
246
+ if (!visitorName || !visitorEmail)
247
+ return;
248
+ try {
249
+ const res = await fetch(`${apiUrl}/api/chat/sessions`, {
250
+ method: 'POST',
251
+ headers: { 'Content-Type': 'application/json' },
252
+ body: JSON.stringify({
253
+ entityId,
254
+ visitorId: (user === null || user === void 0 ? void 0 : user.id) || `anon-${Date.now()}`,
255
+ visitorName,
256
+ visitorEmail,
257
+ }),
258
+ });
259
+ const data = await res.json();
260
+ setSessionId(data.sessionId);
261
+ setStarted(true);
262
+ const personalizedGreeting = greet
263
+ .replace('{name}', visitorName)
264
+ .replace('{Name}', visitorName.charAt(0).toUpperCase() + visitorName.slice(1));
265
+ setMessages([{
266
+ id: 'greeting',
267
+ senderType: 'bot',
268
+ body: personalizedGreeting,
269
+ createdAt: new Date().toISOString(),
270
+ }]);
271
+ }
272
+ catch (err) {
273
+ console.error('Failed to start chat', err);
274
+ }
275
+ };
276
+ const sendMessage = async () => {
277
+ if ((!input.trim() && pendingFiles.length === 0) || !sessionId)
278
+ return;
279
+ setSending(true);
280
+ const msgBody = input;
281
+ setInput('');
282
+ const filesToSend = [...pendingFiles];
283
+ setPendingFiles([]);
284
+ setMessages((prev) => [...prev, {
285
+ id: `temp-${Date.now()}`,
286
+ senderType: 'visitor',
287
+ body: msgBody,
288
+ createdAt: new Date().toISOString(),
289
+ attachments: filesToSend.map((f, i) => (Object.assign(Object.assign({}, f), { id: `temp-att-${i}` }))),
290
+ }]);
291
+ try {
292
+ await fetch(`${apiUrl}/api/chat/sessions/${sessionId}`, {
293
+ method: 'POST',
294
+ headers: { 'Content-Type': 'application/json' },
295
+ body: JSON.stringify({
296
+ body: msgBody || '(attachment)',
297
+ senderType: 'visitor',
298
+ senderId: user === null || user === void 0 ? void 0 : user.id,
299
+ attachments: filesToSend,
300
+ }),
301
+ });
302
+ // If no agents online, show offline acknowledgment
303
+ if (!agentsOnline) {
304
+ setTimeout(() => {
305
+ setMessages((prev) => [...prev, {
306
+ id: `offline-ack-${Date.now()}`,
307
+ senderType: 'bot',
308
+ body: `Thanks ${visitorName}! Our team is currently offline. We've received your message and will get back to you at ${visitorEmail} as soon as someone is available.`,
309
+ createdAt: new Date().toISOString(),
310
+ }]);
311
+ }, 800);
312
+ }
313
+ }
314
+ catch (err) {
315
+ console.error('Failed to send message', err);
316
+ }
317
+ finally {
318
+ setSending(false);
319
+ }
320
+ };
321
+ const handleFileSelect = async (e) => {
322
+ const files = e.target.files;
323
+ if (!files)
324
+ return;
325
+ for (const file of Array.from(files)) {
326
+ if (file.size > 5 * 1024 * 1024) {
327
+ alert(`${file.name} exceeds 5MB limit`);
328
+ continue;
329
+ }
330
+ const reader = new FileReader();
331
+ const dataUrl = await new Promise((resolve) => {
332
+ reader.onload = () => resolve(reader.result);
333
+ reader.readAsDataURL(file);
334
+ });
335
+ setPendingFiles((prev) => [...prev, {
336
+ fileName: file.name,
337
+ fileUrl: dataUrl,
338
+ fileSize: file.size,
339
+ mimeType: file.type,
340
+ }]);
341
+ }
342
+ if (fileInputRef.current)
343
+ fileInputRef.current.value = '';
344
+ };
345
+ const handlePaste = async (e) => {
346
+ const items = Array.from(e.clipboardData.items);
347
+ for (const item of items) {
348
+ if (item.kind === 'file') {
349
+ e.preventDefault();
350
+ const file = item.getAsFile();
351
+ if (!file)
352
+ continue;
353
+ if (file.size > 5 * 1024 * 1024) {
354
+ alert(`${file.name || 'File'} exceeds 5MB limit`);
355
+ continue;
356
+ }
357
+ const reader = new FileReader();
358
+ const dataUrl = await new Promise((resolve) => {
359
+ reader.onload = () => resolve(reader.result);
360
+ reader.readAsDataURL(file);
361
+ });
362
+ setPendingFiles((prev) => [...prev, {
363
+ fileName: file.name || `pasted-${Date.now()}`,
364
+ fileUrl: dataUrl,
365
+ fileSize: file.size,
366
+ mimeType: file.type,
367
+ }]);
368
+ }
369
+ }
370
+ };
371
+ const positionStyle = pos === 'bottom-left'
372
+ ? { left: '20px', bottom: '20px' }
373
+ : { right: '20px', bottom: '20px' };
374
+ const chatPositionStyle = pos === 'bottom-left'
375
+ ? { left: '20px', bottom: '80px' }
376
+ : { right: '20px', bottom: '80px' };
377
+ return (jsxs(Fragment, { children: [jsx("button", { onClick: () => setOpen(!open), style: Object.assign(Object.assign({}, positionStyle), { position: 'fixed', width: '56px', height: '56px', borderRadius: '50%', backgroundColor: color, color: '#fff', border: 'none', cursor: 'pointer', boxShadow: `0 4px 16px ${color}44`, zIndex: 9999, display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'transform 0.2s, box-shadow 0.2s' }), onMouseEnter: (e) => {
378
+ e.currentTarget.style.transform = 'scale(1.08)';
379
+ e.currentTarget.style.boxShadow = `0 6px 24px ${color}66`;
380
+ }, onMouseLeave: (e) => {
381
+ e.currentTarget.style.transform = 'scale(1)';
382
+ e.currentTarget.style.boxShadow = `0 4px 16px ${color}44`;
383
+ }, "aria-label": "Open chat", children: open ? (jsx(CloseIcon, { size: 22 })) : (jsx("img", { src: logo, alt: name, style: { width: '28px', height: '28px', borderRadius: '6px', objectFit: 'cover' } })) }), open && (jsxs("div", { style: Object.assign(Object.assign({}, chatPositionStyle), { position: 'fixed', width: 'min(380px, calc(100vw - 32px))', height: 'min(520px, calc(100vh - 100px))', backgroundColor: '#fff', borderRadius: `${rad}px`, boxShadow: '0 12px 48px rgba(0,0,0,0.18), 0 2px 8px rgba(0,0,0,0.08)', zIndex: 10000, display: 'flex', flexDirection: 'column', overflow: 'hidden', fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif' }), children: [jsxs("div", { style: {
384
+ background: `linear-gradient(135deg, ${color} 0%, ${color}dd 100%)`,
385
+ padding: '18px 20px 16px',
386
+ display: 'flex',
387
+ alignItems: 'center',
388
+ justifyContent: 'space-between',
389
+ position: 'relative',
390
+ }, children: [jsxs("div", { style: { minWidth: 0, flex: 1 }, children: [jsx("div", { style: {
391
+ color: '#fff',
392
+ fontSize: '15px',
393
+ fontWeight: 700,
394
+ lineHeight: 1.2,
395
+ letterSpacing: '-0.01em',
396
+ }, children: name }), jsx("div", { style: {
397
+ color: 'rgba(255,255,255,0.85)',
398
+ fontSize: '12px',
399
+ marginTop: '3px',
400
+ display: 'flex',
401
+ alignItems: 'center',
402
+ gap: '6px',
403
+ }, children: !started ? ('Start a conversation') : agentsOnline ? (jsxs(Fragment, { children: [jsx("span", { style: {
404
+ width: '7px',
405
+ height: '7px',
406
+ borderRadius: '50%',
407
+ backgroundColor: '#4ade80',
408
+ boxShadow: '0 0 6px rgba(74, 222, 128, 0.6)',
409
+ } }), jsxs("span", { children: [agents.length, " agent", agents.length > 1 ? 's' : '', " online"] }), jsxs("span", { style: { display: 'flex', marginLeft: '4px' }, children: [agents.slice(0, 3).map((agent, i) => (jsx("span", { style: {
410
+ width: '20px',
411
+ height: '20px',
412
+ borderRadius: '50%',
413
+ backgroundColor: agent.avatarUrl ? 'transparent' : 'rgba(255,255,255,0.3)',
414
+ border: '1.5px solid rgba(255,255,255,0.8)',
415
+ marginLeft: i > 0 ? '-6px' : '0',
416
+ display: 'flex',
417
+ alignItems: 'center',
418
+ justifyContent: 'center',
419
+ fontSize: '8px',
420
+ fontWeight: 600,
421
+ color: '#fff',
422
+ overflow: 'hidden',
423
+ }, children: agent.avatarUrl ? (jsx("img", { src: agent.avatarUrl, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } })) : (agent.name.charAt(0).toUpperCase()) }, agent.userId))), agents.length > 3 && (jsxs("span", { style: {
424
+ width: '20px',
425
+ height: '20px',
426
+ borderRadius: '50%',
427
+ backgroundColor: 'rgba(255,255,255,0.2)',
428
+ border: '1.5px solid rgba(255,255,255,0.6)',
429
+ marginLeft: '-6px',
430
+ display: 'flex',
431
+ alignItems: 'center',
432
+ justifyContent: 'center',
433
+ fontSize: '7px',
434
+ fontWeight: 600,
435
+ color: '#fff',
436
+ }, children: ["+", agents.length - 3] }))] })] })) : (jsx("span", { style: { color: 'rgba(255,255,255,0.65)' }, children: "Leave a message - we'll respond via email" })) })] }), jsx("button", { onClick: () => setOpen(false), style: {
437
+ background: 'rgba(255,255,255,0.15)',
438
+ border: 'none',
439
+ borderRadius: '50%',
440
+ width: '32px',
441
+ height: '32px',
442
+ display: 'flex',
443
+ alignItems: 'center',
444
+ justifyContent: 'center',
445
+ cursor: 'pointer',
446
+ color: '#fff',
447
+ flexShrink: 0,
448
+ transition: 'background 0.15s',
449
+ }, onMouseEnter: (e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.28)'; }, onMouseLeave: (e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.15)'; }, "aria-label": "Close chat", children: jsx(CloseIcon, { size: 16 }) })] }), jsx("div", { style: { flex: 1, overflowY: 'auto', backgroundColor: '#faf9f7' }, children: !started ? (
450
+ /* Pre-chat form */
451
+ jsxs("div", { style: { padding: '24px 20px 16px' }, children: [agentsOnline && agents.length > 0 && (jsxs("div", { style: {
452
+ display: 'flex',
453
+ alignItems: 'center',
454
+ gap: '10px',
455
+ marginBottom: '16px',
456
+ padding: '10px 12px',
457
+ backgroundColor: '#f0fdf4',
458
+ borderRadius: '8px',
459
+ border: '1px solid #bbf7d0',
460
+ }, children: [jsx("div", { style: { display: 'flex' }, children: agents.slice(0, 4).map((agent, i) => (jsx("span", { style: {
461
+ width: '28px',
462
+ height: '28px',
463
+ borderRadius: '50%',
464
+ backgroundColor: agent.avatarUrl ? 'transparent' : '#16a34a',
465
+ border: '2px solid #fff',
466
+ marginLeft: i > 0 ? '-8px' : '0',
467
+ display: 'flex',
468
+ alignItems: 'center',
469
+ justifyContent: 'center',
470
+ fontSize: '10px',
471
+ fontWeight: 600,
472
+ color: '#fff',
473
+ overflow: 'hidden',
474
+ }, children: agent.avatarUrl ? (jsx("img", { src: agent.avatarUrl, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } })) : (agent.name.charAt(0).toUpperCase()) }, agent.userId))) }), jsxs("div", { children: [jsx("p", { style: { margin: 0, fontSize: '12px', fontWeight: 600, color: '#16a34a' }, children: "Our team is online" }), jsxs("p", { style: { margin: 0, fontSize: '11px', color: '#15803d' }, children: [agents.length, " agent", agents.length > 1 ? 's' : '', " ready to help"] })] })] })), jsx("p", { style: {
475
+ margin: '0 0 6px',
476
+ color: '#1a1410',
477
+ fontSize: '14px',
478
+ fontWeight: 600,
479
+ }, children: "Start a conversation" }), jsx("p", { style: {
480
+ margin: '0 0 16px',
481
+ color: '#9c948a',
482
+ fontSize: '12px',
483
+ lineHeight: 1.4,
484
+ }, children: "Tell us a bit about yourself and we'll connect you with the right person." }), jsxs("div", { style: { position: 'relative', marginBottom: '10px' }, children: [jsx("div", { style: {
485
+ position: 'absolute',
486
+ left: '12px',
487
+ top: '50%',
488
+ transform: 'translateY(-50%)',
489
+ display: 'flex',
490
+ alignItems: 'center',
491
+ pointerEvents: 'none',
492
+ }, children: jsx(UserIcon, {}) }), jsx("input", { ref: nameInputRef, type: "text", placeholder: "Your name", value: visitorName, onChange: (e) => setVisitorName(e.target.value), onKeyDown: (e) => { var _a; return e.key === 'Enter' && ((_a = document.querySelector('[data-chat-email]')) === null || _a === void 0 ? void 0 : _a.focus()); }, style: {
493
+ width: '100%',
494
+ padding: '11px 12px 11px 38px',
495
+ border: '1px solid #e0dcd8',
496
+ borderRadius: `${inputBorderRadius}px`,
497
+ fontSize: '14px',
498
+ boxSizing: 'border-box',
499
+ outline: 'none',
500
+ backgroundColor: '#fff',
501
+ color: '#1a1410',
502
+ transition: 'border-color 0.15s',
503
+ }, onFocus: (e) => { e.currentTarget.style.borderColor = color; }, onBlur: (e) => { e.currentTarget.style.borderColor = '#e0dcd8'; } })] }), jsxs("div", { style: { position: 'relative', marginBottom: '6px' }, children: [jsx("div", { style: {
504
+ position: 'absolute',
505
+ left: '12px',
506
+ top: '50%',
507
+ transform: 'translateY(-50%)',
508
+ display: 'flex',
509
+ alignItems: 'center',
510
+ pointerEvents: 'none',
511
+ }, children: jsx(MailIcon, {}) }), jsx("input", { "data-chat-email": true, type: "email", placeholder: "Email address", value: visitorEmail, onChange: (e) => setVisitorEmail(e.target.value), onKeyDown: (e) => e.key === 'Enter' && startChat(), style: {
512
+ width: '100%',
513
+ padding: '11px 12px 11px 38px',
514
+ border: '1px solid #e0dcd8',
515
+ borderRadius: `${inputBorderRadius}px`,
516
+ fontSize: '14px',
517
+ boxSizing: 'border-box',
518
+ outline: 'none',
519
+ backgroundColor: '#fff',
520
+ color: '#1a1410',
521
+ transition: 'border-color 0.15s',
522
+ }, onFocus: (e) => { e.currentTarget.style.borderColor = color; }, onBlur: (e) => { e.currentTarget.style.borderColor = '#e0dcd8'; } })] }), jsx("p", { style: {
523
+ margin: '0 0 16px',
524
+ color: '#b0a89e',
525
+ fontSize: '11px',
526
+ lineHeight: 1.4,
527
+ }, children: "We'll use this to follow up if you leave." }), jsx("button", { onClick: startChat, disabled: !visitorName || !visitorEmail, style: {
528
+ width: '100%',
529
+ padding: '12px',
530
+ backgroundColor: color,
531
+ color: '#fff',
532
+ border: 'none',
533
+ borderRadius: `${inputBorderRadius}px`,
534
+ cursor: visitorName && visitorEmail ? 'pointer' : 'not-allowed',
535
+ fontWeight: 600,
536
+ fontSize: '14px',
537
+ opacity: visitorName && visitorEmail ? 1 : 0.5,
538
+ transition: 'opacity 0.15s',
539
+ letterSpacing: '0.01em',
540
+ }, children: "Start Chat" })] })) : (
541
+ /* Messages */
542
+ jsxs("div", { style: { padding: '16px 16px 8px' }, children: [messages.map((msg) => (jsxs("div", { style: {
543
+ marginBottom: '10px',
544
+ display: 'flex',
545
+ justifyContent: msg.senderType === 'visitor' ? 'flex-end' : 'flex-start',
546
+ }, children: [msg.senderType !== 'visitor' && (jsx("div", { style: {
547
+ width: '28px',
548
+ height: '28px',
549
+ borderRadius: '50%',
550
+ backgroundColor: msg.senderAvatar ? 'transparent' : color,
551
+ display: 'flex',
552
+ alignItems: 'center',
553
+ justifyContent: 'center',
554
+ marginRight: '8px',
555
+ flexShrink: 0,
556
+ marginTop: '2px',
557
+ overflow: 'hidden',
558
+ }, children: msg.senderAvatar ? (jsx("img", { src: msg.senderAvatar, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } })) : (jsx(ChatIcon, { size: 13 })) })), jsxs("div", { style: {
559
+ maxWidth: '72%',
560
+ padding: '10px 14px',
561
+ borderRadius: msg.senderType === 'visitor'
562
+ ? `${rad}px ${rad}px ${Math.max(rad - 8, 2)}px ${rad}px`
563
+ : `${rad}px ${rad}px ${rad}px ${Math.max(rad - 8, 2)}px`,
564
+ backgroundColor: msg.senderType === 'visitor' ? color : '#fff',
565
+ color: msg.senderType === 'visitor' ? '#fff' : '#1a1410',
566
+ fontSize: '13px',
567
+ lineHeight: 1.5,
568
+ boxShadow: msg.senderType === 'visitor' ? 'none' : '0 1px 3px rgba(0,0,0,0.06)',
569
+ border: msg.senderType === 'visitor' ? 'none' : '1px solid #eee',
570
+ }, children: [msg.body, msg.attachments && msg.attachments.length > 0 && (jsx("div", { style: { marginTop: '6px' }, children: msg.attachments.map((att) => (jsx("div", { style: { fontSize: '11px', opacity: 0.8, marginTop: '2px' }, children: jsx("a", { href: att.fileUrl, target: "_blank", rel: "noopener noreferrer", style: { color: msg.senderType === 'visitor' ? '#fff' : color, textDecoration: 'underline' }, children: att.fileName }) }, att.id))) })), jsx("div", { style: {
571
+ fontSize: '10px',
572
+ marginTop: '4px',
573
+ opacity: 0.5,
574
+ textAlign: msg.senderType === 'visitor' ? 'right' : 'left',
575
+ }, children: new Date(msg.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) })] })] }, msg.id))), jsx("div", { ref: messagesEnd })] })) }), started && (jsxs("div", { style: {
576
+ padding: '12px 16px',
577
+ borderTop: '1px solid #eee',
578
+ backgroundColor: '#fff',
579
+ }, children: [pendingFiles.length > 0 && (jsx("div", { style: { marginBottom: '8px', display: 'flex', flexWrap: 'wrap', gap: '4px' }, children: pendingFiles.map((f, i) => (jsxs("span", { style: {
580
+ fontSize: '11px',
581
+ padding: '3px 8px',
582
+ borderRadius: `${inputBorderRadius}px`,
583
+ backgroundColor: '#f5f3f0',
584
+ color: '#6b625a',
585
+ display: 'flex',
586
+ alignItems: 'center',
587
+ gap: '4px',
588
+ }, children: [f.fileName, jsx("button", { onClick: () => setPendingFiles((prev) => prev.filter((_, idx) => idx !== i)), style: { background: 'none', border: 'none', cursor: 'pointer', color: '#9c948a', padding: 0, fontSize: '12px' }, children: "\u00D7" })] }, i))) })), jsxs("div", { style: { display: 'flex', gap: '8px', alignItems: 'center' }, children: [jsx("input", { ref: fileInputRef, type: "file", multiple: true, onChange: handleFileSelect, style: { display: 'none' }, accept: "image/*,.pdf,.doc,.docx,.txt,.csv,.xlsx,.xls,.zip" }), jsx("button", { onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, style: {
589
+ padding: '8px',
590
+ backgroundColor: 'transparent',
591
+ border: 'none',
592
+ borderRadius: `${inputBorderRadius}px`,
593
+ cursor: 'pointer',
594
+ display: 'flex',
595
+ alignItems: 'center',
596
+ justifyContent: 'center',
597
+ flexShrink: 0,
598
+ }, title: "Attach file", children: jsx(PaperclipIcon, {}) }), jsx("input", { type: "text", value: input, onChange: (e) => setInput(e.target.value), onKeyDown: (e) => e.key === 'Enter' && sendMessage(), onPaste: handlePaste, placeholder: "Type a message...", disabled: sending, style: {
599
+ flex: 1,
600
+ padding: '9px 12px',
601
+ border: '1px solid #e0dcd8',
602
+ borderRadius: `${inputBorderRadius}px`,
603
+ fontSize: '13px',
604
+ outline: 'none',
605
+ backgroundColor: '#faf9f7',
606
+ color: '#1a1410',
607
+ transition: 'border-color 0.15s',
608
+ }, onFocus: (e) => { e.currentTarget.style.borderColor = color; }, onBlur: (e) => { e.currentTarget.style.borderColor = '#e0dcd8'; } }), jsx("button", { onClick: sendMessage, disabled: sending || (!input.trim() && pendingFiles.length === 0), style: {
609
+ width: '36px',
610
+ height: '36px',
611
+ backgroundColor: color,
612
+ color: '#fff',
613
+ border: 'none',
614
+ borderRadius: `${inputBorderRadius}px`,
615
+ cursor: sending || (!input.trim() && pendingFiles.length === 0) ? 'not-allowed' : 'pointer',
616
+ display: 'flex',
617
+ alignItems: 'center',
618
+ justifyContent: 'center',
619
+ flexShrink: 0,
620
+ opacity: sending || (!input.trim() && pendingFiles.length === 0) ? 0.5 : 1,
621
+ transition: 'opacity 0.15s',
622
+ }, children: jsx(SendIcon, {}) })] })] })), showPoweredBy && (jsxs("div", { style: {
623
+ padding: '6px 16px',
624
+ borderTop: '1px solid #f0eeeb',
625
+ backgroundColor: '#faf9f7',
626
+ display: 'flex',
627
+ alignItems: 'center',
628
+ justifyContent: 'center',
629
+ gap: '5px',
630
+ }, children: [jsx(ReechdeskLogo, {}), jsx("span", { style: {
631
+ fontSize: '10px',
632
+ color: '#b0a89e',
633
+ letterSpacing: '0.02em',
634
+ }, children: "Powered by Reechdesk" })] }))] }))] }));
635
+ }
636
+
637
+ const STATUS_COLORS = {
638
+ OPEN: '#f26522',
639
+ IN_PROGRESS: '#2563eb',
640
+ PENDING: '#d97706',
641
+ ESCALATED: '#9333ea',
642
+ RESOLVED: '#16a34a',
643
+ CLOSED: '#9c948a',
644
+ };
645
+ function TicketStatus({ ticketNumber: initialTicket }) {
646
+ const { apiUrl, entityId, branding } = useHelpdesk();
647
+ const color = branding.primaryColor;
648
+ const [ticketNumber, setTicketNumber] = useState(initialTicket || '');
649
+ const [ticket, setTicket] = useState(null);
650
+ const [error, setError] = useState('');
651
+ const [loading, setLoading] = useState(false);
652
+ const fetchTicket = async () => {
653
+ var _a, _b;
654
+ if (!ticketNumber)
655
+ return;
656
+ setLoading(true);
657
+ setError('');
658
+ try {
659
+ const res = await fetch(`${apiUrl}/api/tickets?search=${ticketNumber}&entityId=${entityId}&limit=1`);
660
+ const data = await res.json();
661
+ if (((_a = data.data) === null || _a === void 0 ? void 0 : _a.length) > 0) {
662
+ const t = data.data[0];
663
+ setTicket({
664
+ ticketNumber: t.ticketNumber,
665
+ subject: t.subject,
666
+ status: t.status,
667
+ updatedAt: t.updatedAt,
668
+ assignedTo: (_b = t.assignedTo) === null || _b === void 0 ? void 0 : _b.name,
669
+ });
670
+ }
671
+ else {
672
+ setError('Ticket not found');
673
+ setTicket(null);
674
+ }
675
+ }
676
+ catch (_c) {
677
+ setError('Failed to fetch ticket');
678
+ }
679
+ finally {
680
+ setLoading(false);
681
+ }
682
+ };
683
+ useEffect(() => {
684
+ if (initialTicket) {
685
+ setTicketNumber(initialTicket);
686
+ }
687
+ }, [initialTicket]);
688
+ return (jsxs("div", { style: {
689
+ padding: '16px',
690
+ border: '1px solid #e0dcd8',
691
+ borderRadius: '4px',
692
+ backgroundColor: '#fff',
693
+ fontFamily: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif',
694
+ }, children: [!ticket && (jsxs("div", { style: { display: 'flex', gap: '8px', marginBottom: '12px' }, children: [jsx("input", { type: "text", value: ticketNumber, onChange: (e) => setTicketNumber(e.target.value), placeholder: "Enter ticket number (e.g. TKT-00001)", style: {
695
+ flex: 1, padding: '8px 12px', border: '1px solid #e0dcd8',
696
+ borderRadius: '4px', fontSize: '14px',
697
+ } }), jsx("button", { onClick: fetchTicket, disabled: loading || !ticketNumber, style: {
698
+ padding: '8px 16px', backgroundColor: color,
699
+ color: '#fff', border: 'none', borderRadius: '4px',
700
+ cursor: 'pointer', fontWeight: 600, fontSize: '14px',
701
+ }, children: loading ? '...' : 'Check' })] })), error && jsx("p", { style: { color: '#dc2626', fontSize: '14px' }, children: error }), ticket && (jsxs("div", { children: [jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }, children: [jsx("span", { style: { fontFamily: 'monospace', color: '#6b625a', fontSize: '13px' }, children: ticket.ticketNumber }), jsx("span", { style: {
702
+ padding: '2px 8px',
703
+ borderRadius: '100px',
704
+ fontSize: '12px',
705
+ fontWeight: 600,
706
+ backgroundColor: (STATUS_COLORS[ticket.status] || '#9c948a') + '20',
707
+ color: STATUS_COLORS[ticket.status] || '#9c948a',
708
+ }, children: ticket.status.replace('_', ' ') })] }), jsx("p", { style: { fontSize: '14px', color: '#1a1410', margin: '0 0 4px' }, children: ticket.subject }), jsxs("p", { style: { fontSize: '12px', color: '#9c948a', margin: 0 }, children: ["Last updated: ", new Date(ticket.updatedAt).toLocaleDateString(), ticket.assignedTo && ` • Assigned to: ${ticket.assignedTo}`] })] }))] }));
709
+ }
710
+
711
+ function KBSearch({ placeholder = 'Search help articles...', maxResults = 5, }) {
712
+ const { apiUrl, entityId } = useHelpdesk();
713
+ const [query, setQuery] = useState('');
714
+ const [results, setResults] = useState([]);
715
+ const [loading, setLoading] = useState(false);
716
+ const [open, setOpen] = useState(false);
717
+ const search = useCallback(async (q) => {
718
+ if (q.length < 2) {
719
+ setResults([]);
720
+ setOpen(false);
721
+ return;
722
+ }
723
+ setLoading(true);
724
+ try {
725
+ const res = await fetch(`${apiUrl}/api/kb/search?q=${encodeURIComponent(q)}&entityId=${entityId}`);
726
+ const data = await res.json();
727
+ setResults((Array.isArray(data) ? data : []).slice(0, maxResults));
728
+ setOpen(true);
729
+ }
730
+ catch (_a) {
731
+ // ignore
732
+ }
733
+ finally {
734
+ setLoading(false);
735
+ }
736
+ }, [apiUrl, entityId, maxResults]);
737
+ return (jsxs("div", { style: { position: 'relative' }, children: [jsx("input", { type: "text", value: query, onChange: (e) => {
738
+ setQuery(e.target.value);
739
+ search(e.target.value);
740
+ }, onFocus: () => query.length >= 2 && setOpen(true), placeholder: placeholder, style: {
741
+ width: '100%', padding: '8px 12px', border: '1px solid #e0dcd8',
742
+ borderRadius: '4px', fontSize: '14px', boxSizing: 'border-box',
743
+ } }), open && results.length > 0 && (jsx("div", { style: {
744
+ position: 'absolute', top: '100%', left: 0, right: 0,
745
+ backgroundColor: '#fff', border: '1px solid #e0dcd8',
746
+ borderRadius: '4px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
747
+ zIndex: 100, maxHeight: '300px', overflowY: 'auto',
748
+ }, children: results.map((article) => (jsxs("a", { href: `${apiUrl}/knowledge-base/${article.slug}`, style: {
749
+ display: 'block', padding: '10px 12px', textDecoration: 'none',
750
+ borderBottom: '1px solid #f0f0f0',
751
+ }, children: [jsx("div", { style: { fontSize: '13px', fontWeight: 600, color: '#1a1410' }, children: article.title }), jsx("div", { style: { fontSize: '12px', color: '#9c948a', marginTop: '2px' }, children: article.category.name }), article.excerpt && (jsxs("div", { style: { fontSize: '12px', color: '#6b625a', marginTop: '2px' }, children: [article.excerpt.slice(0, 80), "..."] }))] }, article.id))) })), open && query.length >= 2 && results.length === 0 && !loading && (jsx("div", { style: {
752
+ position: 'absolute', top: '100%', left: 0, right: 0,
753
+ backgroundColor: '#fff', border: '1px solid #e0dcd8',
754
+ borderRadius: '4px', padding: '12px', textAlign: 'center',
755
+ color: '#9c948a', fontSize: '13px', zIndex: 100,
756
+ }, children: "No articles found" }))] }));
757
+ }
758
+
759
+ export { ChatWidget, HelpdeskProvider, KBSearch, TicketStatus, useHelpdesk };
760
+ //# sourceMappingURL=index.mjs.map