ajaxter-chat 3.0.6 → 3.0.7
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/components/ChatScreen/index.js +44 -1
- package/dist/components/HomeScreen/index.js +41 -22
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/utils/chat.d.ts +2 -0
- package/dist/utils/chat.js +9 -0
- package/dist/utils/privacyConsent.d.ts +4 -0
- package/dist/utils/privacyConsent.js +33 -0
- package/package.json +1 -1
- package/public/chatData.json +40 -7
- package/src/components/ChatScreen/index.tsx +75 -0
- package/src/components/HomeScreen/index.tsx +80 -36
- package/src/index.ts +2 -1
- package/src/types/index.ts +10 -0
- package/src/utils/chat.ts +8 -0
- package/src/utils/privacyConsent.ts +32 -0
|
@@ -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: [
|
|
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',
|
|
@@ -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
|
|
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: (
|
|
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:
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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';
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/chat.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/chat.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "3.0.7",
|
|
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",
|
package/public/chatData.json
CHANGED
|
@@ -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": "
|
|
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-
|
|
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-
|
|
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-
|
|
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,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
|
|
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
|
-
{
|
|
95
|
-
|
|
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
|
|
237
|
-
|
|
251
|
+
<p style={{ margin: '0 0 10px', fontSize: 15, fontWeight: 700, color: '#4c1d95', lineHeight: 1.45, position: 'relative' }}>
|
|
252
|
+
{promotionLead}
|
|
238
253
|
</p>
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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,
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
+
}
|