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