ajaxter-chat 3.0.6 → 3.0.8

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.
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React, { useState, useRef, useEffect, useCallback } from 'react';
3
3
  import { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from '../../utils/chat';
4
+ import { shouldShowPrivacyNotice, dismissPrivacyNotice } from '../../utils/privacyConsent';
4
5
  import { EmojiPicker } from '../EmojiPicker';
5
6
  import { SlideNavMenu } from '../SlideNavMenu';
6
7
  export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported, isBlocked, onSend, onBack, onClose, onTogglePause, onReport, onBlock, onStartCall, onNavAction, otherDevelopers = [], onTransferToDeveloper, }) => {
@@ -13,6 +14,7 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
13
14
  const [isRecording, setIsRecording] = useState(false);
14
15
  const [recordSec, setRecordSec] = useState(0);
15
16
  const [showConfirm, setShowConfirm] = useState(null);
17
+ const [showPrivacy, setShowPrivacy] = useState(false);
16
18
  const endRef = useRef(null);
17
19
  const inputRef = useRef(null);
18
20
  const fileRef = useRef(null);
@@ -20,6 +22,24 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
20
22
  const mediaRecorder = useRef(null);
21
23
  const recordChunks = useRef([]);
22
24
  useEffect(() => { var _a; (_a = endRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
25
+ const privacyEnabled = config.showPrivacyNotice !== false;
26
+ useEffect(() => {
27
+ if (!privacyEnabled)
28
+ return;
29
+ setShowPrivacy(shouldShowPrivacyNotice(config.id));
30
+ }, [config.id, privacyEnabled]);
31
+ useEffect(() => {
32
+ if (!privacyEnabled)
33
+ return;
34
+ const id = window.setInterval(() => {
35
+ setShowPrivacy(shouldShowPrivacyNotice(config.id));
36
+ }, 60000);
37
+ return () => window.clearInterval(id);
38
+ }, [config.id, privacyEnabled]);
39
+ const dismissPrivacy = useCallback(() => {
40
+ dismissPrivacyNotice(config.id);
41
+ setShowPrivacy(false);
42
+ }, [config.id]);
23
43
  const handleSend = useCallback(() => {
24
44
  var _a;
25
45
  if (!text.trim() || isPaused || isBlocked)
@@ -105,7 +125,30 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
105
125
  return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease', position: 'relative', overflow: 'hidden' }, children: [_jsx(SlideNavMenu, { open: slideMenuOpen, onClose: () => setSlideMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (_a = config.viewerType) !== null && _a !== void 0 ? _a : 'user', onSelect: onNavAction, onBackHome: onBack }), _jsxs("div", { style: {
106
126
  background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
107
127
  padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0,
108
- }, children: [_jsx("button", { type: "button", onClick: () => setSlideMenuOpen(true), style: hdrBtn, "aria-label": "Open menu", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "3", y1: "6", x2: "21", y2: "6", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }), _jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }), _jsx("line", { x1: "3", y1: "18", x2: "21", y2: "18", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" })] }) }), _jsxs("div", { style: { width: 36, height: 36, borderRadius: '50%', backgroundColor: peerAvatar, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 13, flexShrink: 0, position: 'relative' }, children: [peerInit, _jsx("span", { style: { position: 'absolute', bottom: 0, right: 0, width: 9, height: 9, borderRadius: '50%', border: '2px solid', borderColor: 'transparent', backgroundColor: activeUser.status === 'online' ? '#22c55e' : activeUser.status === 'away' ? '#f59e0b' : '#9ca3af' } })] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: activeUser.name }), _jsx("div", { style: { fontSize: 11, color: 'rgba(255,255,255,0.8)' }, children: activeUser.designation })] }), _jsx("span", { style: { fontSize: 13, fontWeight: 700, color: '#fff', opacity: 0.95, flexShrink: 0 }, children: headerRole }), config.allowWebCall && (_jsx("button", { type: "button", onClick: () => onStartCall(false), style: hdrBtn, title: "Voice Call", children: _jsx("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z", fill: "#fff" }) }) })), _jsx("button", { type: "button", onClick: () => setShowMenu(v => !v), style: Object.assign(Object.assign({}, hdrBtn), { background: 'rgba(255,255,255,0.2)' }), title: "More options", "aria-expanded": showMenu, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "19", r: "1.5", fill: "#fff" })] }) })] }), showMenu && (_jsxs("div", { style: { position: 'absolute', top: 52, right: 12, zIndex: 120, background: '#fff', borderRadius: 12, boxShadow: '0 8px 30px rgba(0,0,0,0.16)', padding: '6px', minWidth: 200, animation: 'cw-fadeUp 0.18s ease' }, children: [config.allowTranscriptDownload && (_jsx(MenuItem, { icon: "\uD83D\uDCE5", label: "Download Transcript", onClick: handleTranscript })), viewerIsDev && activeUser.type === 'user' && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx(MenuItem, { icon: "\uD83D\uDD00", label: "Transfer to developer", onClick: () => { setShowMenu(false); setTransferOpen(true); } })), _jsx(MenuItem, { icon: isPaused ? '▶️' : '⏸', label: isPaused ? 'Resume Chat' : 'Pause Chat', onClick: () => { setShowMenu(false); setShowConfirm('pause'); } }), config.allowReport && !isReported && (_jsx(MenuItem, { icon: "\u26A0\uFE0F", label: "Report Chat", onClick: () => { setShowMenu(false); setShowConfirm('report'); } })), config.allowBlock && activeUser.type === 'user' && !isBlocked && (_jsx(MenuItem, { icon: "\uD83D\uDEAB", label: "Block User", onClick: () => { setShowMenu(false); setShowConfirm('block'); }, danger: true })), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), _jsx(MenuItem, { icon: "\u2715", label: "Close Chat", onClick: onClose })] })), isPaused && (_jsxs("div", { style: { background: '#fef3c7', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#92400e', textAlign: 'center', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }, children: ["\u23F8 Chat is paused \u2014 users cannot send messages", _jsx("button", { type: "button", onClick: onTogglePause, style: { background: '#92400e', color: '#fff', border: 'none', borderRadius: 6, padding: '2px 8px', fontSize: 11, cursor: 'pointer', marginLeft: 4 }, children: "Resume" })] })), isBlocked && (_jsx("div", { style: { background: '#fee2e2', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#991b1b', textAlign: 'center', flexShrink: 0 }, children: "\uD83D\uDEAB This user is blocked" })), isReported && (_jsx("div", { style: { background: '#fef3c7', padding: '6px 16px', fontSize: 11, color: '#92400e', textAlign: 'center', flexShrink: 0 }, children: "\u26A0\uFE0F This chat has been reported" })), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '14px', display: 'flex', flexDirection: 'column', gap: 10, background: '#f8f9fc' }, className: "cw-scroll", children: [grouped.map(({ date, msgs }) => (_jsxs(React.Fragment, { children: [_jsx(DateDivider, { label: date }), msgs.map(msg => (_jsx(Bubble, { msg: msg, primaryColor: config.primaryColor }, msg.id)))] }, date))), messages.length === 0 && (_jsxs("div", { style: { margin: 'auto', textAlign: 'center', color: '#c4cad4', fontSize: 13 }, children: [_jsx("div", { style: { fontSize: 28, marginBottom: 8 }, children: "\uD83D\uDCAC" }), "Say hello to ", activeUser.name, "!"] })), _jsx("div", { ref: endRef })] }), _jsxs("div", { style: { borderTop: '1px solid #eef0f5', padding: '10px 12px 8px', background: '#fff', flexShrink: 0, position: 'relative' }, children: [showEmoji && config.allowEmoji && (_jsx(EmojiPicker, { primaryColor: config.primaryColor, onSelect: e => setText(t => t + e), onClose: () => setShowEmoji(false) })), isRecording && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, padding: '8px 12px', background: '#fee2e2', borderRadius: 10 }, children: [_jsx("span", { style: { width: 8, height: 8, borderRadius: '50%', background: '#ef4444', display: 'inline-block', animation: 'cw-pulse 1s infinite' } }), _jsxs("span", { style: { fontSize: 13, color: '#991b1b', fontWeight: 600 }, children: ["Recording ", recordSec, "s"] }), _jsx("button", { type: "button", onClick: stopRecording, style: { marginLeft: 'auto', background: '#ef4444', color: '#fff', border: 'none', borderRadius: 6, padding: '4px 12px', fontSize: 12, cursor: 'pointer', fontWeight: 600 }, children: "Stop & send" })] })), _jsxs("div", { style: {
128
+ }, children: [_jsx("button", { type: "button", onClick: () => setSlideMenuOpen(true), style: hdrBtn, "aria-label": "Open menu", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "3", y1: "6", x2: "21", y2: "6", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }), _jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }), _jsx("line", { x1: "3", y1: "18", x2: "21", y2: "18", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" })] }) }), _jsxs("div", { style: { width: 36, height: 36, borderRadius: '50%', backgroundColor: peerAvatar, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 13, flexShrink: 0, position: 'relative' }, children: [peerInit, _jsx("span", { style: { position: 'absolute', bottom: 0, right: 0, width: 9, height: 9, borderRadius: '50%', border: '2px solid', borderColor: 'transparent', backgroundColor: activeUser.status === 'online' ? '#22c55e' : activeUser.status === 'away' ? '#f59e0b' : '#9ca3af' } })] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: activeUser.name }), _jsx("div", { style: { fontSize: 11, color: 'rgba(255,255,255,0.8)' }, children: activeUser.designation })] }), _jsx("span", { style: { fontSize: 13, fontWeight: 700, color: '#fff', opacity: 0.95, flexShrink: 0 }, children: headerRole }), config.allowWebCall && (_jsx("button", { type: "button", onClick: () => onStartCall(false), style: hdrBtn, title: "Voice Call", children: _jsx("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z", fill: "#fff" }) }) })), _jsx("button", { type: "button", onClick: () => setShowMenu(v => !v), style: Object.assign(Object.assign({}, hdrBtn), { background: 'rgba(255,255,255,0.2)' }), title: "More options", "aria-expanded": showMenu, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "19", r: "1.5", fill: "#fff" })] }) })] }), showMenu && (_jsxs("div", { style: { position: 'absolute', top: 52, right: 12, zIndex: 120, background: '#fff', borderRadius: 12, boxShadow: '0 8px 30px rgba(0,0,0,0.16)', padding: '6px', minWidth: 200, animation: 'cw-fadeUp 0.18s ease' }, children: [config.allowTranscriptDownload && (_jsx(MenuItem, { icon: "\uD83D\uDCE5", label: "Download Transcript", onClick: handleTranscript })), viewerIsDev && activeUser.type === 'user' && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx(MenuItem, { icon: "\uD83D\uDD00", label: "Transfer to developer", onClick: () => { setShowMenu(false); setTransferOpen(true); } })), _jsx(MenuItem, { icon: isPaused ? '▶️' : '⏸', label: isPaused ? 'Resume Chat' : 'Pause Chat', onClick: () => { setShowMenu(false); setShowConfirm('pause'); } }), config.allowReport && !isReported && (_jsx(MenuItem, { icon: "\u26A0\uFE0F", label: "Report Chat", onClick: () => { setShowMenu(false); setShowConfirm('report'); } })), config.allowBlock && activeUser.type === 'user' && !isBlocked && (_jsx(MenuItem, { icon: "\uD83D\uDEAB", label: "Block User", onClick: () => { setShowMenu(false); setShowConfirm('block'); }, danger: true })), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), _jsx(MenuItem, { icon: "\u2715", label: "Close Chat", onClick: onClose })] })), isPaused && (_jsxs("div", { style: { background: '#fef3c7', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#92400e', textAlign: 'center', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }, children: ["\u23F8 Chat is paused \u2014 users cannot send messages", _jsx("button", { type: "button", onClick: onTogglePause, style: { background: '#92400e', color: '#fff', border: 'none', borderRadius: 6, padding: '2px 8px', fontSize: 11, cursor: 'pointer', marginLeft: 4 }, children: "Resume" })] })), isBlocked && (_jsx("div", { style: { background: '#fee2e2', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#991b1b', textAlign: 'center', flexShrink: 0 }, children: "\uD83D\uDEAB This user is blocked" })), isReported && (_jsx("div", { style: { background: '#fef3c7', padding: '6px 16px', fontSize: 11, color: '#92400e', textAlign: 'center', flexShrink: 0 }, children: "\u26A0\uFE0F This chat has been reported" })), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '14px', display: 'flex', flexDirection: 'column', gap: 10, background: '#f8f9fc' }, className: "cw-scroll", children: [grouped.map(({ date, msgs }) => (_jsxs(React.Fragment, { children: [_jsx(DateDivider, { label: date }), msgs.map(msg => (_jsx(Bubble, { msg: msg, primaryColor: config.primaryColor }, msg.id)))] }, date))), messages.length === 0 && (_jsxs("div", { style: { margin: 'auto', textAlign: 'center', color: '#c4cad4', fontSize: 13 }, children: [_jsx("div", { style: { fontSize: 28, marginBottom: 8 }, children: "\uD83D\uDCAC" }), "Say hello to ", activeUser.name, "!"] })), _jsx("div", { ref: endRef })] }), _jsxs("div", { style: { borderTop: '1px solid #eef0f5', padding: '10px 12px 8px', background: '#fff', flexShrink: 0, position: 'relative' }, children: [privacyEnabled && showPrivacy && (_jsxs("div", { style: {
129
+ position: 'relative',
130
+ marginBottom: 10,
131
+ padding: '12px 36px 12px 12px',
132
+ borderRadius: 12,
133
+ background: '#fff',
134
+ border: '1px solid #e8ecf1',
135
+ boxShadow: '0 1px 4px rgba(15,23,42,0.06)',
136
+ }, children: [_jsx("button", { type: "button", "aria-label": "Dismiss privacy notice", onClick: dismissPrivacy, style: {
137
+ position: 'absolute',
138
+ top: 8,
139
+ right: 8,
140
+ width: 26,
141
+ height: 26,
142
+ borderRadius: '50%',
143
+ border: 'none',
144
+ background: '#f1f5f9',
145
+ cursor: 'pointer',
146
+ display: 'flex',
147
+ alignItems: 'center',
148
+ justifyContent: 'center',
149
+ padding: 0,
150
+ lineHeight: 1,
151
+ }, children: _jsx("span", { style: { fontSize: 14, color: '#475569', fontWeight: 700 }, children: "\u00D7" }) }), _jsxs("p", { style: { margin: 0, fontSize: 12, color: '#64748b', lineHeight: 1.55 }, children: ["By chatting here, you agree we and authorized partners may process, monitor, and record this chat and your data in line with", ' ', config.privacyPolicyUrl ? (_jsx("a", { href: config.privacyPolicyUrl, target: "_blank", rel: "noopener noreferrer", style: { color: config.primaryColor, textDecoration: 'underline', fontWeight: 600 }, children: "Privacy Policy" })) : (_jsx("span", { style: { textDecoration: 'underline', fontWeight: 600 }, children: "Privacy Policy" })), "."] })] })), showEmoji && config.allowEmoji && (_jsx(EmojiPicker, { primaryColor: config.primaryColor, onSelect: e => setText(t => t + e), onClose: () => setShowEmoji(false) })), isRecording && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, padding: '8px 12px', background: '#fee2e2', borderRadius: 10 }, children: [_jsx("span", { style: { width: 8, height: 8, borderRadius: '50%', background: '#ef4444', display: 'inline-block', animation: 'cw-pulse 1s infinite' } }), _jsxs("span", { style: { fontSize: 13, color: '#991b1b', fontWeight: 600 }, children: ["Recording ", recordSec, "s"] }), _jsx("button", { type: "button", onClick: stopRecording, style: { marginLeft: 'auto', background: '#ef4444', color: '#fff', border: 'none', borderRadius: 6, padding: '4px 12px', fontSize: 12, cursor: 'pointer', fontWeight: 600 }, children: "Stop & send" })] })), _jsxs("div", { style: {
109
152
  border: `1.5px solid ${isPaused || isBlocked ? '#e5e7eb' : '#bfdbfe'}`,
110
153
  borderRadius: 16,
111
154
  padding: '10px 12px 8px',
@@ -15,9 +15,6 @@ import { BlockListScreen } from './BlockList';
15
15
  import { CallScreen } from './CallScreen';
16
16
  import { MaintenanceView } from './MaintenanceView';
17
17
  import { BottomTabs } from './Tabs/BottomTabs';
18
- /* ─── Drawer width ─────────────────────────────────────────────────────────── */
19
- const DRAWER_W_NORMAL = 380;
20
- const DRAWER_W_MAX = 480;
21
18
  export const ChatWidget = ({ theme: localTheme }) => {
22
19
  var _a, _b, _c, _d;
23
20
  /* SSR guard */
@@ -31,7 +28,6 @@ export const ChatWidget = ({ theme: localTheme }) => {
31
28
  const theme = mergeTheme((data === null || data === void 0 ? void 0 : data.widget) ? { primaryColor: data.widget.primaryColor, buttonLabel: data.widget.buttonLabel, buttonPosition: data.widget.buttonPosition } : undefined, localTheme);
32
29
  /* Drawer open state */
33
30
  const [isOpen, setIsOpen] = useState(false);
34
- const [isMaximized, setIsMaximized] = useState(false);
35
31
  const [closing, setClosing] = useState(false); // for slide-out animation
36
32
  /* Navigation */
37
33
  const [activeTab, setActiveTab] = useState('home');
@@ -168,7 +164,6 @@ export const ChatWidget = ({ theme: localTheme }) => {
168
164
  }, [endCall]);
169
165
  /* ── Derived ─────────────────────────────────────────────────────────── */
170
166
  const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
171
- const drawerW = isMaximized ? DRAWER_W_MAX : DRAWER_W_NORMAL;
172
167
  const widgetConfig = data === null || data === void 0 ? void 0 : data.widget;
173
168
  const primaryColor = theme.primaryColor;
174
169
  const allUsers = data ? [...data.developers, ...data.users] : [];
@@ -209,9 +204,20 @@ export const ChatWidget = ({ theme: localTheme }) => {
209
204
  const posStyle = theme.buttonPosition === 'bottom-left'
210
205
  ? { left: 24, right: 'auto' }
211
206
  : { right: 24, left: 'auto' };
207
+ /* No radius on top-left / bottom-left; left-docked panel keeps inner TR/BR curve */
212
208
  const drawerPosStyle = theme.buttonPosition === 'bottom-left'
213
- ? { left: 0, borderRadius: '0 16px 16px 0' }
214
- : { right: 0, borderRadius: '16px 0 0 16px' };
209
+ ? {
210
+ left: 0,
211
+ borderTopLeftRadius: 0,
212
+ borderBottomLeftRadius: 0,
213
+ borderTopRightRadius: 16,
214
+ borderBottomRightRadius: 16,
215
+ }
216
+ : {
217
+ right: 0,
218
+ borderTopLeftRadius: 0,
219
+ borderBottomLeftRadius: 0,
220
+ };
215
221
  /* ── Don't render until mounted (SSR safe) ──────────────────────────── */
216
222
  if (!mounted)
217
223
  return null;
@@ -235,6 +241,15 @@ export const ChatWidget = ({ theme: localTheme }) => {
235
241
 
236
242
  .cw-drawer-enter { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideInLeft' : 'cw-slideInRight'} 0.32s cubic-bezier(0.22,1,0.36,1) both; }
237
243
  .cw-drawer-exit { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideOutLeft' : 'cw-slideOutRight'} 0.28s cubic-bezier(0.55,0,1,0.45) both; }
244
+
245
+ .cw-drawer-panel {
246
+ width: 30%;
247
+ max-width: 100vw;
248
+ min-width: 0;
249
+ }
250
+ @media (max-width: 1024px) {
251
+ .cw-drawer-panel { width: 100%; }
252
+ }
238
253
  ` }), !isOpen && (_jsxs("button", { className: "cw-root", onClick: openDrawer, "aria-label": theme.buttonLabel, style: Object.assign(Object.assign({ position: 'fixed', bottom: 24, zIndex: 9999 }, posStyle), { display: 'flex', alignItems: 'center', gap: 10, padding: '13px 22px', backgroundColor: theme.buttonColor, color: theme.buttonTextColor, border: 'none', borderRadius: 50, cursor: 'pointer', fontSize: 15, fontWeight: 700, boxShadow: `0 8px 28px ${theme.buttonColor}55`, animation: 'cw-btnPop 0.4s cubic-bezier(0.34,1.56,0.64,1)', transition: 'transform 0.2s, box-shadow 0.2s' }), onMouseEnter: e => {
239
254
  e.currentTarget.style.transform = 'scale(1.06) translateY(-2px)';
240
255
  e.currentTarget.style.boxShadow = `0 14px 36px ${theme.buttonColor}66`;
@@ -246,21 +261,19 @@ export const ChatWidget = ({ theme: localTheme }) => {
246
261
  backgroundColor: 'rgba(0,0,0,0.35)',
247
262
  opacity: closing ? 0 : 1,
248
263
  transition: 'opacity 0.3s',
249
- } })), isOpen && (_jsxs("div", { className: `cw-root ${closing ? 'cw-drawer-exit' : 'cw-drawer-enter'}`, style: Object.assign(Object.assign({ position: 'fixed', top: 0, bottom: 0 }, drawerPosStyle), { zIndex: 9998, width: drawerW, maxWidth: '100vw', backgroundColor: '#fff', boxShadow: theme.buttonPosition === 'bottom-left'
264
+ } })), isOpen && (_jsxs("div", { className: `cw-root cw-drawer-panel ${closing ? 'cw-drawer-exit' : 'cw-drawer-enter'}`, style: Object.assign(Object.assign({ position: 'fixed', top: 0, bottom: 0 }, drawerPosStyle), { zIndex: 9998, backgroundColor: '#fff', boxShadow: theme.buttonPosition === 'bottom-left'
250
265
  ? '4px 0 40px rgba(0,0,0,0.18)'
251
- : '-4px 0 40px rgba(0,0,0,0.18)', display: 'flex', flexDirection: 'column', overflow: 'hidden', transition: 'width 0.28s ease' }), children: [cfgLoading && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 16 }, children: [_jsx("div", { style: {
266
+ : '-4px 0 40px rgba(0,0,0,0.18)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }), children: [cfgLoading && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 16 }, children: [_jsx("div", { style: {
252
267
  width: 40, height: 40, borderRadius: '50%',
253
268
  border: `3px solid ${primaryColor}30`,
254
269
  borderTopColor: primaryColor,
255
270
  animation: 'spin 0.8s linear infinite',
256
- } }), _jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` }), _jsx("p", { style: { fontSize: 14, color: '#7b8fa1' }, children: "Loading chat\u2026" })] })), cfgError && !cfgLoading && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 12, padding: 32, textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\u26A0\uFE0F" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Could not load chat configuration" }), _jsx("p", { style: { fontSize: 13, color: '#7b8fa1', lineHeight: 1.6 }, children: cfgError }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), !cfgLoading && !cfgError && widgetConfig && (_jsxs(_Fragment, { children: [screen !== 'chat' && screen !== 'call' && (_jsxs("div", { style: {
271
+ } }), _jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` }), _jsx("p", { style: { fontSize: 14, color: '#7b8fa1' }, children: "Loading chat\u2026" })] })), cfgError && !cfgLoading && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 12, padding: 32, textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\u26A0\uFE0F" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Could not load chat configuration" }), _jsx("p", { style: { fontSize: 13, color: '#7b8fa1', lineHeight: 1.6 }, children: cfgError }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), !cfgLoading && !cfgError && widgetConfig && (_jsxs(_Fragment, { children: [screen !== 'chat' && screen !== 'call' && (_jsx("div", { style: {
257
272
  position: 'absolute', top: 12,
258
273
  right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
259
274
  left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
260
275
  zIndex: 20, display: 'flex', gap: 6,
261
- }, children: [_jsx(CornerBtn, { onClick: () => setIsMaximized(m => !m), title: isMaximized ? 'Minimize' : 'Maximize', children: isMaximized
262
- ? _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M8 3v5H3M21 8h-5V3M3 16h5v5M16 21v-5h5", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }) })
263
- : _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M8 3H5a2 2 0 00-2 2v3M21 8V5a2 2 0 00-2-2h-3M3 16v3a2 2 0 002 2h3M16 21h3a2 2 0 002-2v-3", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round" }) }) }), _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) })] })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, onNavigate: handleCardClick, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_d = widgetConfig.viewerType) !== null && _d !== void 0 ? _d : 'user', onBack: () => setScreen('home'), onSelectUser: handleSelectUser })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: () => { clearChat(); setScreen('home'); setActiveTab('home'); }, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: handleSelectUser })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onRaiseTicket: handleRaiseTicket })), screen === 'block-list' && (_jsx(BlockListScreen, { blockedUsers: blockedUsers, config: widgetConfig, onUnblock: handleUnblock, onBack: () => { setScreen('home'); setActiveTab('home'); } }))] })), widgetConfig.status === 'ACTIVE' &&
276
+ }, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, onNavigate: handleCardClick, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_d = widgetConfig.viewerType) !== null && _d !== void 0 ? _d : 'user', onBack: () => setScreen('home'), onSelectUser: handleSelectUser })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: () => { clearChat(); setScreen('home'); setActiveTab('home'); }, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: handleSelectUser })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onRaiseTicket: handleRaiseTicket })), screen === 'block-list' && (_jsx(BlockListScreen, { blockedUsers: blockedUsers, config: widgetConfig, onUnblock: handleUnblock, onBack: () => { setScreen('home'); setActiveTab('home'); } }))] })), widgetConfig.status === 'ACTIVE' &&
264
277
  screen !== 'chat' &&
265
278
  screen !== 'call' &&
266
279
  screen !== 'user-list' &&
@@ -1,13 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useState, useMemo } from 'react';
3
3
  import { SlideNavMenu } from '../SlideNavMenu';
4
+ import { truncateWords } from '../../utils/chat';
4
5
  export const HomeScreen = ({ config, onNavigate, tickets }) => {
5
- var _a;
6
+ var _a, _b, _c, _d;
6
7
  const [menuOpen, setMenuOpen] = useState(false);
7
8
  const showSupport = config.chatType === 'SUPPORT' || config.chatType === 'BOTH';
8
9
  const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
9
10
  const viewerIsDev = config.viewerType === 'developer';
10
- const continueItems = tickets.slice(0, 2);
11
+ const pendingTickets = useMemo(() => tickets
12
+ .filter(t => t.status === 'open' || t.status === 'in-progress')
13
+ .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
14
+ .slice(0, 5), [tickets]);
15
+ const brand = ((_a = config.brandName) === null || _a === void 0 ? void 0 : _a.trim()) || 'Ajaxter';
16
+ const promotionLead = ((_b = config.promotionLead) === null || _b === void 0 ? void 0 : _b.trim()) ||
17
+ 'Need specialized help? Our teams are ready to assist you with any questions.';
18
+ const tourUrl = (_c = config.websiteTourUrl) === null || _c === void 0 ? void 0 : _c.trim();
11
19
  const handleCallUs = () => {
12
20
  var _a;
13
21
  const raw = (_a = config.supportPhone) === null || _a === void 0 ? void 0 : _a.trim();
@@ -15,7 +23,7 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
15
23
  return;
16
24
  window.location.href = `tel:${raw.replace(/\s/g, '')}`;
17
25
  };
18
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }, children: [_jsx(SlideNavMenu, { open: menuOpen, onClose: () => setMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (_a = config.viewerType) !== null && _a !== void 0 ? _a : 'user', onSelect: onNavigate }), _jsx("div", { style: {
26
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }, children: [_jsx(SlideNavMenu, { open: menuOpen, onClose: () => setMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (_d = config.viewerType) !== null && _d !== void 0 ? _d : 'user', onSelect: onNavigate }), _jsx("div", { style: {
19
27
  flexShrink: 0,
20
28
  padding: '14px 16px 10px',
21
29
  display: 'flex',
@@ -43,7 +51,7 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
43
51
  color: '#0f172a',
44
52
  letterSpacing: '-0.03em',
45
53
  lineHeight: 1.2,
46
- }, children: config.welcomeTitle }), _jsx("p", { style: { margin: '0 0 28px', fontSize: 14, color: '#64748b', lineHeight: 1.55 }, children: config.welcomeSubtitle }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: "Continue Conversations" }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }, children: continueItems.length > 0 ? (continueItems.map(t => (_jsx("button", { type: "button", onClick: () => onNavigate('ticket'), style: {
54
+ }, children: config.welcomeTitle }), _jsx("p", { style: { margin: '0 0 28px', fontSize: 14, color: '#64748b', lineHeight: 1.55 }, children: config.welcomeSubtitle }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: "Continue Conversations" }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }, children: pendingTickets.length > 0 ? (pendingTickets.map(t => (_jsxs("button", { type: "button", onClick: () => onNavigate('ticket'), style: {
47
55
  width: '100%',
48
56
  textAlign: 'left',
49
57
  padding: '14px 16px',
@@ -51,11 +59,9 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
51
59
  border: 'none',
52
60
  background: '#e0f2fe',
53
61
  color: '#0369a1',
54
- fontSize: 14,
55
- fontWeight: 600,
56
62
  cursor: 'pointer',
57
63
  boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
58
- }, children: t.title }, t.id)))) : (_jsxs(_Fragment, { children: [_jsx("div", { style: {
64
+ }, children: [_jsx("div", { style: { fontSize: 14, fontWeight: 700, color: '#0c4a6e', marginBottom: 6 }, children: t.title }), _jsx("div", { style: { fontSize: 12, fontWeight: 500, color: '#64748b', lineHeight: 1.45 }, children: truncateWords(t.description, 50) })] }, t.id)))) : (_jsxs(_Fragment, { children: [_jsx("div", { style: {
59
65
  padding: '14px 16px',
60
66
  borderRadius: 14,
61
67
  background: '#e0f2fe',
@@ -117,18 +123,31 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
117
123
  background: 'linear-gradient(145deg, #fce7f3 0%, #e9d5ff 45%, #ddd6fe 100%)',
118
124
  position: 'relative',
119
125
  overflow: 'hidden',
120
- }, children: [_jsx("div", { style: { position: 'absolute', top: -20, right: -20, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.35)' } }), _jsx("p", { style: { margin: '0 0 16px', fontSize: 15, fontWeight: 700, color: '#4c1d95', lineHeight: 1.45, position: 'relative' }, children: "Need specialized help? Our teams are ready to assist you with any questions." }), _jsxs("button", { type: "button", onClick: handleCallUs, disabled: !config.supportPhone, style: {
121
- display: 'inline-flex',
122
- alignItems: 'center',
123
- gap: 8,
124
- padding: '10px 18px',
125
- borderRadius: 12,
126
- border: 'none',
127
- background: config.supportPhone ? config.primaryColor : '#94a3b8',
128
- color: '#fff',
129
- fontSize: 14,
130
- fontWeight: 700,
131
- cursor: config.supportPhone ? 'pointer' : 'not-allowed',
132
- position: 'relative',
133
- }, children: [_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z", fill: "#fff" }) }), "Call Us"] })] })] })] }));
126
+ }, children: [_jsx("div", { style: { position: 'absolute', top: -20, right: -20, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.35)' } }), _jsx("p", { style: { margin: '0 0 10px', fontSize: 15, fontWeight: 700, color: '#4c1d95', lineHeight: 1.45, position: 'relative' }, children: promotionLead }), _jsxs("p", { style: { margin: '0 0 16px', fontSize: 13, fontWeight: 500, color: '#5b21b6', lineHeight: 1.5, position: 'relative' }, children: [_jsx("strong", { style: { fontWeight: 800 }, children: brand }), " \u2014 embedded chat for your workspace.", ' ', _jsx("span", { style: { whiteSpace: 'nowrap' }, children: "Free for users." }), " 24\u00D77 availability. Dedicated workspace experience."] }), _jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 10, position: 'relative' }, children: [tourUrl && (_jsx("a", { href: tourUrl, target: "_blank", rel: "noopener noreferrer", style: {
127
+ display: 'inline-flex',
128
+ alignItems: 'center',
129
+ justifyContent: 'center',
130
+ gap: 6,
131
+ padding: '10px 16px',
132
+ borderRadius: 12,
133
+ border: 'none',
134
+ background: '#fff',
135
+ color: '#5b21b6',
136
+ fontSize: 13,
137
+ fontWeight: 700,
138
+ textDecoration: 'none',
139
+ boxShadow: '0 2px 8px rgba(91,33,182,0.15)',
140
+ }, children: "Take a Website Tour" })), _jsxs("button", { type: "button", onClick: handleCallUs, disabled: !config.supportPhone, style: {
141
+ display: 'inline-flex',
142
+ alignItems: 'center',
143
+ gap: 8,
144
+ padding: '10px 18px',
145
+ borderRadius: 12,
146
+ border: 'none',
147
+ background: config.supportPhone ? config.primaryColor : '#94a3b8',
148
+ color: '#fff',
149
+ fontSize: 14,
150
+ fontWeight: 700,
151
+ cursor: config.supportPhone ? 'pointer' : 'not-allowed',
152
+ }, children: [_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z", fill: "#fff" }) }), "Call Us"] })] })] })] })] }));
134
153
  };
package/dist/index.d.ts CHANGED
@@ -13,7 +13,8 @@ export { SlideNavMenu } from './components/SlideNavMenu';
13
13
  export { useChat } from './hooks/useChat';
14
14
  export { useWebRTC } from './hooks/useWebRTC';
15
15
  export { useRemoteConfig } from './hooks/useRemoteConfig';
16
+ export { shouldShowPrivacyNotice, dismissPrivacyNotice, getPrivacyDismissedAt } from './utils/privacyConsent';
16
17
  export { loadLocalConfig, fetchRemoteChatData } from './config';
17
18
  export { mergeTheme, darken } from './utils/theme';
18
- export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from './utils/chat';
19
+ export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText, truncateWords } from './utils/chat';
19
20
  export type { ChatWidgetProps, ChatWidgetTheme, WidgetConfig, RemoteChatData, ChatUser, ChatMessage, Ticket, RecentChat, CallSession, CallState, ChatStatus, ChatType, UserType, OnlineStatus, Screen, BottomTab, UserListContext, MessageType, LocalEnvConfig, } from './types';
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export { SlideNavMenu } from './components/SlideNavMenu';
13
13
  export { useChat } from './hooks/useChat';
14
14
  export { useWebRTC } from './hooks/useWebRTC';
15
15
  export { useRemoteConfig } from './hooks/useRemoteConfig';
16
+ export { shouldShowPrivacyNotice, dismissPrivacyNotice, getPrivacyDismissedAt } from './utils/privacyConsent';
16
17
  export { loadLocalConfig, fetchRemoteChatData } from './config';
17
18
  export { mergeTheme, darken } from './utils/theme';
18
- export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from './utils/chat';
19
+ export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText, truncateWords } from './utils/chat';
@@ -23,6 +23,16 @@ export interface WidgetConfig {
23
23
  viewerUid?: string;
24
24
  /** Display name for transfer notes (optional) */
25
25
  viewerName?: string;
26
+ /** Privacy Policy URL (linked from chat consent banner) */
27
+ privacyPolicyUrl?: string;
28
+ /** Set false to hide the consent note above the composer */
29
+ showPrivacyNotice?: boolean;
30
+ /** Product brand (e.g. Ajaxter) */
31
+ brandName?: string;
32
+ /** Home promotion: “Take a Website Tour” link */
33
+ websiteTourUrl?: string;
34
+ /** Optional override for the lead line in the promotion card */
35
+ promotionLead?: string;
26
36
  allowVoiceMessage: boolean;
27
37
  allowAttachment: boolean;
28
38
  allowEmoji: boolean;
@@ -9,5 +9,7 @@ export declare function formatTime(ts: string | Date): string;
9
9
  export declare function formatDate(ts: string | Date): string;
10
10
  /** Generate a plain-text transcript from messages */
11
11
  export declare function generateTranscript(messages: ChatMessage[], peer: ChatUser, myName?: string): string;
12
+ /** Truncate to max words, append ellipsis (…) if shortened */
13
+ export declare function truncateWords(text: string, maxWords: number): string;
12
14
  /** Trigger a file download in the browser */
13
15
  export declare function downloadText(content: string, filename: string): void;
@@ -50,6 +50,15 @@ export function generateTranscript(messages, peer, myName = 'Me') {
50
50
  }).join('\n');
51
51
  return header + body;
52
52
  }
53
+ /** Truncate to max words, append ellipsis (…) if shortened */
54
+ export function truncateWords(text, maxWords) {
55
+ const w = text.trim().split(/\s+/).filter(Boolean);
56
+ if (w.length === 0)
57
+ return '';
58
+ if (w.length <= maxWords)
59
+ return w.join(' ');
60
+ return `${w.slice(0, maxWords).join(' ')}…`;
61
+ }
53
62
  /** Trigger a file download in the browser */
54
63
  export function downloadText(content, filename) {
55
64
  const blob = new Blob([content], { type: 'text/plain' });
@@ -0,0 +1,4 @@
1
+ export declare function getPrivacyDismissedAt(widgetId: string): number | null;
2
+ /** After dismiss, banner stays hidden until one hour has passed */
3
+ export declare function shouldShowPrivacyNotice(widgetId: string): boolean;
4
+ export declare function dismissPrivacyNotice(widgetId: string): void;
@@ -0,0 +1,33 @@
1
+ const HOUR_MS = 60 * 60 * 1000;
2
+ function key(widgetId) {
3
+ return `ajaxter_privacy_dismiss_${widgetId}`;
4
+ }
5
+ export function getPrivacyDismissedAt(widgetId) {
6
+ if (typeof window === 'undefined')
7
+ return null;
8
+ try {
9
+ const v = localStorage.getItem(key(widgetId));
10
+ if (v == null)
11
+ return null;
12
+ const n = parseInt(v, 10);
13
+ return Number.isFinite(n) ? n : null;
14
+ }
15
+ catch (_a) {
16
+ return null;
17
+ }
18
+ }
19
+ /** After dismiss, banner stays hidden until one hour has passed */
20
+ export function shouldShowPrivacyNotice(widgetId) {
21
+ const at = getPrivacyDismissedAt(widgetId);
22
+ if (at == null)
23
+ return true;
24
+ return Date.now() - at >= HOUR_MS;
25
+ }
26
+ export function dismissPrivacyNotice(widgetId) {
27
+ try {
28
+ localStorage.setItem(key(widgetId), String(Date.now()));
29
+ }
30
+ catch (_a) {
31
+ /* ignore quota / private mode */
32
+ }
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ajaxter-chat",
3
- "version": "3.0.6",
3
+ "version": "3.0.8",
4
4
  "description": "Drawer-based chat widget with support chat, tickets, WebRTC calling, voice messages, block list, and transcript download.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,6 +15,9 @@
15
15
  "branch": "Mumbai HQ",
16
16
  "footerPoweredBy": "Answers by",
17
17
  "supportPhone": "+919876543210",
18
+ "privacyPolicyUrl": "https://ajaxter.com/privacy",
19
+ "websiteTourUrl": "https://ajaxter.com",
20
+ "brandName": "Ajaxter",
18
21
  "allowVoiceMessage": true,
19
22
  "allowAttachment": true,
20
23
  "allowEmoji": true,
@@ -136,31 +139,61 @@
136
139
  {
137
140
  "id": "TKT-0001",
138
141
  "title": "Login page 500 error",
139
- "description": "Getting a server error when logging in with valid credentials.",
140
- "status": "resolved",
142
+ "description": "Getting a server error when logging in with valid credentials. Stack trace references timeout on auth service. Reproduced on Chrome and Safari. Need priority fix before release branch cut.",
143
+ "status": "open",
141
144
  "priority": "high",
142
145
  "createdAt": "2025-03-25T10:00:00Z",
143
- "updatedAt": "2025-03-27T09:22:00Z",
146
+ "updatedAt": "2025-03-28T11:00:00Z",
144
147
  "assignedTo": "dev_001"
145
148
  },
146
149
  {
147
150
  "id": "TKT-0002",
148
151
  "title": "Export to CSV not working",
149
- "description": "The export button on the reports page does nothing when clicked.",
152
+ "description": "The export button on the reports page does nothing when clicked. Browser console shows CORS blocked response. Works on staging but fails in production behind new CDN rules.",
150
153
  "status": "in-progress",
151
154
  "priority": "medium",
152
155
  "createdAt": "2025-03-26T14:30:00Z",
153
- "updatedAt": "2025-03-27T08:00:00Z",
156
+ "updatedAt": "2025-03-28T10:30:00Z",
154
157
  "assignedTo": "dev_002"
155
158
  },
156
159
  {
157
160
  "id": "TKT-0003",
158
161
  "title": "Add dark mode support",
159
- "description": "Feature request to add a dark mode toggle for the dashboard.",
162
+ "description": "Feature request to add a dark mode toggle for the dashboard and persist preference per user account across sessions and devices.",
160
163
  "status": "open",
161
164
  "priority": "low",
162
165
  "createdAt": "2025-03-27T07:00:00Z",
163
- "updatedAt": "2025-03-27T07:00:00Z",
166
+ "updatedAt": "2025-03-28T09:15:00Z",
167
+ "assignedTo": null
168
+ },
169
+ {
170
+ "id": "TKT-0004",
171
+ "title": "Webhook retries failing",
172
+ "description": "Outbound webhooks to partner endpoints return 429 after deploy. Need backoff review and idempotency keys. Partner asked for ETA this week.",
173
+ "status": "open",
174
+ "priority": "high",
175
+ "createdAt": "2025-03-28T06:00:00Z",
176
+ "updatedAt": "2025-03-28T09:00:00Z",
177
+ "assignedTo": "dev_001"
178
+ },
179
+ {
180
+ "id": "TKT-0005",
181
+ "title": "Mobile layout overflow",
182
+ "description": "On small screens the chat drawer overlaps the footer actions. QA attached screenshots. Repro on iPhone 14 and Pixel 8.",
183
+ "status": "in-progress",
184
+ "priority": "medium",
185
+ "createdAt": "2025-03-27T16:00:00Z",
186
+ "updatedAt": "2025-03-28T08:45:00Z",
187
+ "assignedTo": "dev_002"
188
+ },
189
+ {
190
+ "id": "TKT-0006",
191
+ "title": "Audit log export",
192
+ "description": "Compliance needs CSV export of admin actions for last 90 days with filters by actor and resource type including soft-deleted records.",
193
+ "status": "open",
194
+ "priority": "low",
195
+ "createdAt": "2025-03-28T05:00:00Z",
196
+ "updatedAt": "2025-03-28T07:30:00Z",
164
197
  "assignedTo": null
165
198
  }
166
199
  ],
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useRef, useEffect, useCallback } from 'react';
2
2
  import { ChatMessage, ChatUser, WidgetConfig, UserListContext } from '../../types';
3
3
  import { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from '../../utils/chat';
4
+ import { shouldShowPrivacyNotice, dismissPrivacyNotice } from '../../utils/privacyConsent';
4
5
  import { EmojiPicker } from '../EmojiPicker';
5
6
  import { SlideNavMenu } from '../SlideNavMenu';
6
7
 
@@ -38,6 +39,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
38
39
  const [isRecording, setIsRecording] = useState(false);
39
40
  const [recordSec, setRecordSec] = useState(0);
40
41
  const [showConfirm, setShowConfirm] = useState<'report'|'block'|'pause'|null>(null);
42
+ const [showPrivacy, setShowPrivacy] = useState(false);
41
43
 
42
44
  const endRef = useRef<HTMLDivElement>(null);
43
45
  const inputRef = useRef<HTMLTextAreaElement>(null);
@@ -48,6 +50,25 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
48
50
 
49
51
  useEffect(() => { endRef.current?.scrollIntoView({ behavior:'smooth' }); }, [messages]);
50
52
 
53
+ const privacyEnabled = config.showPrivacyNotice !== false;
54
+ useEffect(() => {
55
+ if (!privacyEnabled) return;
56
+ setShowPrivacy(shouldShowPrivacyNotice(config.id));
57
+ }, [config.id, privacyEnabled]);
58
+
59
+ useEffect(() => {
60
+ if (!privacyEnabled) return;
61
+ const id = window.setInterval(() => {
62
+ setShowPrivacy(shouldShowPrivacyNotice(config.id));
63
+ }, 60_000);
64
+ return () => window.clearInterval(id);
65
+ }, [config.id, privacyEnabled]);
66
+
67
+ const dismissPrivacy = useCallback(() => {
68
+ dismissPrivacyNotice(config.id);
69
+ setShowPrivacy(false);
70
+ }, [config.id]);
71
+
51
72
  const handleSend = useCallback(() => {
52
73
  if (!text.trim() || isPaused || isBlocked) return;
53
74
  onSend(text.trim());
@@ -244,6 +265,60 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
244
265
  {/* Composer — reference layout */}
245
266
  <div style={{ borderTop:'1px solid #eef0f5', padding:'10px 12px 8px', background:'#fff', flexShrink:0, position:'relative' }}>
246
267
 
268
+ {privacyEnabled && showPrivacy && (
269
+ <div
270
+ style={{
271
+ position: 'relative',
272
+ marginBottom: 10,
273
+ padding: '12px 36px 12px 12px',
274
+ borderRadius: 12,
275
+ background: '#fff',
276
+ border: '1px solid #e8ecf1',
277
+ boxShadow: '0 1px 4px rgba(15,23,42,0.06)',
278
+ }}
279
+ >
280
+ <button
281
+ type="button"
282
+ aria-label="Dismiss privacy notice"
283
+ onClick={dismissPrivacy}
284
+ style={{
285
+ position: 'absolute',
286
+ top: 8,
287
+ right: 8,
288
+ width: 26,
289
+ height: 26,
290
+ borderRadius: '50%',
291
+ border: 'none',
292
+ background: '#f1f5f9',
293
+ cursor: 'pointer',
294
+ display: 'flex',
295
+ alignItems: 'center',
296
+ justifyContent: 'center',
297
+ padding: 0,
298
+ lineHeight: 1,
299
+ }}
300
+ >
301
+ <span style={{ fontSize: 14, color: '#475569', fontWeight: 700 }}>×</span>
302
+ </button>
303
+ <p style={{ margin: 0, fontSize: 12, color: '#64748b', lineHeight: 1.55 }}>
304
+ By chatting here, you agree we and authorized partners may process, monitor, and record this chat and your data in line with{' '}
305
+ {config.privacyPolicyUrl ? (
306
+ <a
307
+ href={config.privacyPolicyUrl}
308
+ target="_blank"
309
+ rel="noopener noreferrer"
310
+ style={{ color: config.primaryColor, textDecoration: 'underline', fontWeight: 600 }}
311
+ >
312
+ Privacy Policy
313
+ </a>
314
+ ) : (
315
+ <span style={{ textDecoration: 'underline', fontWeight: 600 }}>Privacy Policy</span>
316
+ )}
317
+ .
318
+ </p>
319
+ </div>
320
+ )}
321
+
247
322
  {showEmoji && config.allowEmoji && (
248
323
  <EmojiPicker
249
324
  primaryColor={config.primaryColor}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { useState, useEffect, useCallback, useRef } from 'react';
3
+ import React, { useState, useEffect, useCallback } from 'react';
4
4
  import { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket, RecentChat, ChatMessage } from '../types';
5
5
  import { loadLocalConfig } from '../config';
6
6
  import { mergeTheme } from '../utils/theme';
@@ -18,10 +18,6 @@ import { CallScreen } from './CallScreen';
18
18
  import { MaintenanceView } from './MaintenanceView';
19
19
  import { BottomTabs } from './Tabs/BottomTabs';
20
20
 
21
- /* ─── Drawer width ─────────────────────────────────────────────────────────── */
22
- const DRAWER_W_NORMAL = 380;
23
- const DRAWER_W_MAX = 480;
24
-
25
21
  export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) => {
26
22
  /* SSR guard */
27
23
  const [mounted, setMounted] = useState(false);
@@ -41,7 +37,6 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
41
37
 
42
38
  /* Drawer open state */
43
39
  const [isOpen, setIsOpen] = useState(false);
44
- const [isMaximized, setIsMaximized] = useState(false);
45
40
  const [closing, setClosing] = useState(false); // for slide-out animation
46
41
 
47
42
  /* Navigation */
@@ -191,7 +186,6 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
191
186
 
192
187
  /* ── Derived ─────────────────────────────────────────────────────────── */
193
188
  const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
194
- const drawerW = isMaximized ? DRAWER_W_MAX : DRAWER_W_NORMAL;
195
189
  const widgetConfig = data?.widget;
196
190
  const primaryColor = theme.primaryColor;
197
191
 
@@ -235,9 +229,21 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
235
229
  ? { left: 24, right: 'auto' }
236
230
  : { right: 24, left: 'auto' };
237
231
 
238
- const drawerPosStyle: React.CSSProperties = theme.buttonPosition === 'bottom-left'
239
- ? { left: 0, borderRadius: '0 16px 16px 0' }
240
- : { right: 0, borderRadius: '16px 0 0 16px' };
232
+ /* No radius on top-left / bottom-left; left-docked panel keeps inner TR/BR curve */
233
+ const drawerPosStyle: React.CSSProperties =
234
+ theme.buttonPosition === 'bottom-left'
235
+ ? {
236
+ left: 0,
237
+ borderTopLeftRadius: 0,
238
+ borderBottomLeftRadius: 0,
239
+ borderTopRightRadius: 16,
240
+ borderBottomRightRadius: 16,
241
+ }
242
+ : {
243
+ right: 0,
244
+ borderTopLeftRadius: 0,
245
+ borderBottomLeftRadius: 0,
246
+ };
241
247
 
242
248
  /* ── Don't render until mounted (SSR safe) ──────────────────────────── */
243
249
  if (!mounted) return null;
@@ -265,6 +271,15 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
265
271
 
266
272
  .cw-drawer-enter { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideInLeft' : 'cw-slideInRight'} 0.32s cubic-bezier(0.22,1,0.36,1) both; }
267
273
  .cw-drawer-exit { animation: ${theme.buttonPosition === 'bottom-left' ? 'cw-slideOutLeft' : 'cw-slideOutRight'} 0.28s cubic-bezier(0.55,0,1,0.45) both; }
274
+
275
+ .cw-drawer-panel {
276
+ width: 30%;
277
+ max-width: 100vw;
278
+ min-width: 0;
279
+ }
280
+ @media (max-width: 1024px) {
281
+ .cw-drawer-panel { width: 100%; }
282
+ }
268
283
  `}</style>
269
284
 
270
285
  {/* ── Floating Button ── */}
@@ -319,15 +334,13 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
319
334
  {/* ── Drawer / Slider ── */}
320
335
  {isOpen && (
321
336
  <div
322
- className={`cw-root ${closing ? 'cw-drawer-exit' : 'cw-drawer-enter'}`}
337
+ className={`cw-root cw-drawer-panel ${closing ? 'cw-drawer-exit' : 'cw-drawer-enter'}`}
323
338
  style={{
324
339
  position: 'fixed',
325
340
  top: 0,
326
341
  bottom: 0,
327
342
  ...drawerPosStyle,
328
343
  zIndex: 9998,
329
- width: drawerW,
330
- maxWidth: '100vw',
331
344
  backgroundColor: '#fff',
332
345
  boxShadow: theme.buttonPosition === 'bottom-left'
333
346
  ? '4px 0 40px rgba(0,0,0,0.18)'
@@ -335,7 +348,6 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
335
348
  display: 'flex',
336
349
  flexDirection: 'column',
337
350
  overflow: 'hidden',
338
- transition: 'width 0.28s ease',
339
351
  }}
340
352
  >
341
353
  {/* ── Loading state ── */}
@@ -373,12 +385,6 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
373
385
  left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
374
386
  zIndex: 20, display: 'flex', gap: 6,
375
387
  }}>
376
- <CornerBtn onClick={() => setIsMaximized(m => !m)} title={isMaximized ? 'Minimize' : 'Maximize'}>
377
- {isMaximized
378
- ? <svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M8 3v5H3M21 8h-5V3M3 16h5v5M16 21v-5h5" stroke="#fff" strokeWidth="2.2" strokeLinecap="round"/></svg>
379
- : <svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M8 3H5a2 2 0 00-2 2v3M21 8V5a2 2 0 00-2-2h-3M3 16v3a2 2 0 002 2h3M16 21h3a2 2 0 002-2v-3" stroke="#fff" strokeWidth="2.2" strokeLinecap="round"/></svg>
380
- }
381
- </CornerBtn>
382
388
  <CornerBtn onClick={closeDrawer} title="Close">
383
389
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none">
384
390
  <path d="M18 6L6 18M6 6l12 12" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"/>
@@ -1,6 +1,7 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useMemo } from 'react';
2
2
  import { WidgetConfig, UserListContext, Ticket } from '../../types';
3
3
  import { SlideNavMenu } from '../SlideNavMenu';
4
+ import { truncateWords } from '../../utils/chat';
4
5
 
5
6
  interface HomeScreenProps {
6
7
  config: WidgetConfig;
@@ -14,7 +15,20 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
14
15
  const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
15
16
  const viewerIsDev = config.viewerType === 'developer';
16
17
 
17
- const continueItems = tickets.slice(0, 2);
18
+ const pendingTickets = useMemo(
19
+ () =>
20
+ tickets
21
+ .filter(t => t.status === 'open' || t.status === 'in-progress')
22
+ .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
23
+ .slice(0, 5),
24
+ [tickets]
25
+ );
26
+
27
+ const brand = config.brandName?.trim() || 'Ajaxter';
28
+ const promotionLead =
29
+ config.promotionLead?.trim() ||
30
+ 'Need specialized help? Our teams are ready to assist you with any questions.';
31
+ const tourUrl = config.websiteTourUrl?.trim();
18
32
 
19
33
  const handleCallUs = () => {
20
34
  const raw = config.supportPhone?.trim();
@@ -91,8 +105,8 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
91
105
  {/* Continue Conversations */}
92
106
  <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>Continue Conversations</h2>
93
107
  <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }}>
94
- {continueItems.length > 0 ? (
95
- continueItems.map(t => (
108
+ {pendingTickets.length > 0 ? (
109
+ pendingTickets.map(t => (
96
110
  <button
97
111
  key={t.id}
98
112
  type="button"
@@ -105,13 +119,14 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
105
119
  border: 'none',
106
120
  background: '#e0f2fe',
107
121
  color: '#0369a1',
108
- fontSize: 14,
109
- fontWeight: 600,
110
122
  cursor: 'pointer',
111
123
  boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
112
124
  }}
113
125
  >
114
- {t.title}
126
+ <div style={{ fontSize: 14, fontWeight: 700, color: '#0c4a6e', marginBottom: 6 }}>{t.title}</div>
127
+ <div style={{ fontSize: 12, fontWeight: 500, color: '#64748b', lineHeight: 1.45 }}>
128
+ {truncateWords(t.description, 50)}
129
+ </div>
115
130
  </button>
116
131
  ))
117
132
  ) : (
@@ -233,36 +248,65 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
233
248
  }}
234
249
  >
235
250
  <div style={{ position: 'absolute', top: -20, right: -20, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.35)' }} />
236
- <p style={{ margin: '0 0 16px', fontSize: 15, fontWeight: 700, color: '#4c1d95', lineHeight: 1.45, position: 'relative' }}>
237
- Need specialized help? Our teams are ready to assist you with any questions.
251
+ <p style={{ margin: '0 0 10px', fontSize: 15, fontWeight: 700, color: '#4c1d95', lineHeight: 1.45, position: 'relative' }}>
252
+ {promotionLead}
238
253
  </p>
239
- <button
240
- type="button"
241
- onClick={handleCallUs}
242
- disabled={!config.supportPhone}
243
- style={{
244
- display: 'inline-flex',
245
- alignItems: 'center',
246
- gap: 8,
247
- padding: '10px 18px',
248
- borderRadius: 12,
249
- border: 'none',
250
- background: config.supportPhone ? config.primaryColor : '#94a3b8',
251
- color: '#fff',
252
- fontSize: 14,
253
- fontWeight: 700,
254
- cursor: config.supportPhone ? 'pointer' : 'not-allowed',
255
- position: 'relative',
256
- }}
257
- >
258
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
259
- <path
260
- d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z"
261
- fill="#fff"
262
- />
263
- </svg>
264
- Call Us
265
- </button>
254
+ <p style={{ margin: '0 0 16px', fontSize: 13, fontWeight: 500, color: '#5b21b6', lineHeight: 1.5, position: 'relative' }}>
255
+ <strong style={{ fontWeight: 800 }}>{brand}</strong> — embedded chat for your workspace.{' '}
256
+ <span style={{ whiteSpace: 'nowrap' }}>Free for users.</span> 24×7 availability. Dedicated workspace experience.
257
+ </p>
258
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, position: 'relative' }}>
259
+ {tourUrl && (
260
+ <a
261
+ href={tourUrl}
262
+ target="_blank"
263
+ rel="noopener noreferrer"
264
+ style={{
265
+ display: 'inline-flex',
266
+ alignItems: 'center',
267
+ justifyContent: 'center',
268
+ gap: 6,
269
+ padding: '10px 16px',
270
+ borderRadius: 12,
271
+ border: 'none',
272
+ background: '#fff',
273
+ color: '#5b21b6',
274
+ fontSize: 13,
275
+ fontWeight: 700,
276
+ textDecoration: 'none',
277
+ boxShadow: '0 2px 8px rgba(91,33,182,0.15)',
278
+ }}
279
+ >
280
+ Take a Website Tour
281
+ </a>
282
+ )}
283
+ <button
284
+ type="button"
285
+ onClick={handleCallUs}
286
+ disabled={!config.supportPhone}
287
+ style={{
288
+ display: 'inline-flex',
289
+ alignItems: 'center',
290
+ gap: 8,
291
+ padding: '10px 18px',
292
+ borderRadius: 12,
293
+ border: 'none',
294
+ background: config.supportPhone ? config.primaryColor : '#94a3b8',
295
+ color: '#fff',
296
+ fontSize: 14,
297
+ fontWeight: 700,
298
+ cursor: config.supportPhone ? 'pointer' : 'not-allowed',
299
+ }}
300
+ >
301
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
302
+ <path
303
+ d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z"
304
+ fill="#fff"
305
+ />
306
+ </svg>
307
+ Call Us
308
+ </button>
309
+ </div>
266
310
  </div>
267
311
  </div>
268
312
  </div>
package/src/index.ts CHANGED
@@ -15,9 +15,10 @@ export { useChat } from './hooks/useChat';
15
15
  export { useWebRTC } from './hooks/useWebRTC';
16
16
  export { useRemoteConfig } from './hooks/useRemoteConfig';
17
17
 
18
+ export { shouldShowPrivacyNotice, dismissPrivacyNotice, getPrivacyDismissedAt } from './utils/privacyConsent';
18
19
  export { loadLocalConfig, fetchRemoteChatData } from './config';
19
20
  export { mergeTheme, darken } from './utils/theme';
20
- export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from './utils/chat';
21
+ export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText, truncateWords } from './utils/chat';
21
22
 
22
23
  export type {
23
24
  ChatWidgetProps, ChatWidgetTheme,
@@ -24,6 +24,16 @@ export interface WidgetConfig {
24
24
  viewerUid?: string;
25
25
  /** Display name for transfer notes (optional) */
26
26
  viewerName?: string;
27
+ /** Privacy Policy URL (linked from chat consent banner) */
28
+ privacyPolicyUrl?: string;
29
+ /** Set false to hide the consent note above the composer */
30
+ showPrivacyNotice?: boolean;
31
+ /** Product brand (e.g. Ajaxter) */
32
+ brandName?: string;
33
+ /** Home promotion: “Take a Website Tour” link */
34
+ websiteTourUrl?: string;
35
+ /** Optional override for the lead line in the promotion card */
36
+ promotionLead?: string;
27
37
  allowVoiceMessage: boolean;
28
38
  allowAttachment: boolean;
29
39
  allowEmoji: boolean;
package/src/utils/chat.ts CHANGED
@@ -58,6 +58,14 @@ export function generateTranscript(
58
58
  return header + body;
59
59
  }
60
60
 
61
+ /** Truncate to max words, append ellipsis (…) if shortened */
62
+ export function truncateWords(text: string, maxWords: number): string {
63
+ const w = text.trim().split(/\s+/).filter(Boolean);
64
+ if (w.length === 0) return '';
65
+ if (w.length <= maxWords) return w.join(' ');
66
+ return `${w.slice(0, maxWords).join(' ')}…`;
67
+ }
68
+
61
69
  /** Trigger a file download in the browser */
62
70
  export function downloadText(content: string, filename: string) {
63
71
  const blob = new Blob([content], { type: 'text/plain' });
@@ -0,0 +1,32 @@
1
+ const HOUR_MS = 60 * 60 * 1000;
2
+
3
+ function key(widgetId: string): string {
4
+ return `ajaxter_privacy_dismiss_${widgetId}`;
5
+ }
6
+
7
+ export function getPrivacyDismissedAt(widgetId: string): number | null {
8
+ if (typeof window === 'undefined') return null;
9
+ try {
10
+ const v = localStorage.getItem(key(widgetId));
11
+ if (v == null) return null;
12
+ const n = parseInt(v, 10);
13
+ return Number.isFinite(n) ? n : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ /** After dismiss, banner stays hidden until one hour has passed */
20
+ export function shouldShowPrivacyNotice(widgetId: string): boolean {
21
+ const at = getPrivacyDismissedAt(widgetId);
22
+ if (at == null) return true;
23
+ return Date.now() - at >= HOUR_MS;
24
+ }
25
+
26
+ export function dismissPrivacyNotice(widgetId: string): void {
27
+ try {
28
+ localStorage.setItem(key(widgetId), String(Date.now()));
29
+ } catch {
30
+ /* ignore quota / private mode */
31
+ }
32
+ }