ajaxter-chat 3.0.5 → 3.0.6
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.d.ts +3 -0
- package/dist/components/ChatScreen/index.js +55 -4
- package/dist/components/ChatWidget.js +32 -3
- package/dist/components/HomeScreen/index.js +6 -4
- package/dist/components/SlideNavMenu.d.ts +2 -0
- package/dist/components/SlideNavMenu.js +4 -3
- package/dist/components/UserListScreen/index.d.ts +2 -0
- package/dist/components/UserListScreen/index.js +8 -3
- package/dist/types/index.d.ts +9 -0
- package/package.json +1 -1
- package/public/chatData.json +3 -0
- package/src/components/ChatScreen/index.tsx +104 -1
- package/src/components/ChatWidget.tsx +34 -2
- package/src/components/HomeScreen/index.tsx +8 -4
- package/src/components/SlideNavMenu.tsx +6 -2
- package/src/components/UserListScreen/index.tsx +10 -3
- package/src/types/index.ts +9 -0
|
@@ -16,6 +16,9 @@ interface ChatScreenProps {
|
|
|
16
16
|
onStartCall: (withVideo: boolean) => void;
|
|
17
17
|
/** Navigate to support list, colleague list, or tickets (from slide menu) */
|
|
18
18
|
onNavAction: (ctx: UserListContext | 'ticket') => void;
|
|
19
|
+
/** Other devs (excl. viewer) — for transfer when staff chats with a customer */
|
|
20
|
+
otherDevelopers?: ChatUser[];
|
|
21
|
+
onTransferToDeveloper?: (dev: ChatUser) => void;
|
|
19
22
|
}
|
|
20
23
|
export declare const ChatScreen: React.FC<ChatScreenProps>;
|
|
21
24
|
export {};
|
|
@@ -3,11 +3,13 @@ import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
|
3
3
|
import { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText } from '../../utils/chat';
|
|
4
4
|
import { EmojiPicker } from '../EmojiPicker';
|
|
5
5
|
import { SlideNavMenu } from '../SlideNavMenu';
|
|
6
|
-
export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported, isBlocked, onSend, onBack, onClose, onTogglePause, onReport, onBlock, onStartCall, onNavAction, }) => {
|
|
6
|
+
export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported, isBlocked, onSend, onBack, onClose, onTogglePause, onReport, onBlock, onStartCall, onNavAction, otherDevelopers = [], onTransferToDeveloper, }) => {
|
|
7
|
+
var _a;
|
|
7
8
|
const [text, setText] = useState('');
|
|
8
9
|
const [showEmoji, setShowEmoji] = useState(false);
|
|
9
10
|
const [showMenu, setShowMenu] = useState(false);
|
|
10
11
|
const [slideMenuOpen, setSlideMenuOpen] = useState(false);
|
|
12
|
+
const [transferOpen, setTransferOpen] = useState(false);
|
|
11
13
|
const [isRecording, setIsRecording] = useState(false);
|
|
12
14
|
const [recordSec, setRecordSec] = useState(0);
|
|
13
15
|
const [showConfirm, setShowConfirm] = useState(null);
|
|
@@ -96,10 +98,14 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
|
|
|
96
98
|
const peerAvatar = avatarColor(activeUser.name);
|
|
97
99
|
const peerInit = initials(activeUser.name);
|
|
98
100
|
const grouped = groupByDate(messages);
|
|
99
|
-
|
|
101
|
+
const viewerIsDev = config.viewerType === 'developer';
|
|
102
|
+
const headerRole = viewerIsDev
|
|
103
|
+
? (activeUser.type === 'user' ? 'Customer' : 'Developer')
|
|
104
|
+
: 'Support';
|
|
105
|
+
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: {
|
|
100
106
|
background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
|
|
101
107
|
padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0,
|
|
102
|
-
}, 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:
|
|
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: {
|
|
103
109
|
border: `1.5px solid ${isPaused || isBlocked ? '#e5e7eb' : '#bfdbfe'}`,
|
|
104
110
|
borderRadius: 16,
|
|
105
111
|
padding: '10px 12px 8px',
|
|
@@ -129,7 +135,52 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
|
|
|
129
135
|
justifyContent: 'center',
|
|
130
136
|
flexShrink: 0,
|
|
131
137
|
transition: 'background 0.15s',
|
|
132
|
-
}, title: "Send", children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z", stroke: text.trim() && !isPaused && !isBlocked ? '#fff' : '#94a3b8', strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] }), (config.footerPoweredBy || config.branch) && (_jsxs("p", { style: { margin: '10px 0 0', textAlign: 'center', fontSize: 12, color: '#94a3b8' }, children: [config.footerPoweredBy, config.footerPoweredBy && config.branch ? ' · ' : '', config.branch && _jsx("span", { style: { fontWeight: 600, color: '#64748b' }, children: config.branch })] }))] }),
|
|
138
|
+
}, title: "Send", children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z", stroke: text.trim() && !isPaused && !isBlocked ? '#fff' : '#94a3b8', strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] }), (config.footerPoweredBy || config.branch) && (_jsxs("p", { style: { margin: '10px 0 0', textAlign: 'center', fontSize: 12, color: '#94a3b8' }, children: [config.footerPoweredBy, config.footerPoweredBy && config.branch ? ' · ' : '', config.branch && _jsx("span", { style: { fontWeight: 600, color: '#64748b' }, children: config.branch })] }))] }), transferOpen && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx("div", { style: {
|
|
139
|
+
position: 'absolute',
|
|
140
|
+
inset: 0,
|
|
141
|
+
background: 'rgba(0,0,0,0.45)',
|
|
142
|
+
display: 'flex',
|
|
143
|
+
alignItems: 'center',
|
|
144
|
+
justifyContent: 'center',
|
|
145
|
+
zIndex: 280,
|
|
146
|
+
padding: 16,
|
|
147
|
+
}, children: _jsxs("div", { style: {
|
|
148
|
+
background: '#fff',
|
|
149
|
+
borderRadius: 16,
|
|
150
|
+
padding: '18px 16px',
|
|
151
|
+
width: '100%',
|
|
152
|
+
maxWidth: 320,
|
|
153
|
+
maxHeight: '70%',
|
|
154
|
+
overflow: 'hidden',
|
|
155
|
+
display: 'flex',
|
|
156
|
+
flexDirection: 'column',
|
|
157
|
+
boxShadow: '0 16px 48px rgba(0,0,0,0.22)',
|
|
158
|
+
}, children: [_jsx("div", { style: { fontWeight: 800, fontSize: 16, color: '#1a2332', marginBottom: 6 }, children: "Transfer chat to" }), _jsx("p", { style: { fontSize: 12, color: '#7b8fa1', margin: '0 0 12px', lineHeight: 1.5 }, children: "Assign this conversation to another developer. History is kept and a handoff note is added." }), _jsx("div", { className: "cw-scroll", style: { flex: 1, overflowY: 'auto', margin: '0 -4px' }, children: otherDevelopers.map(dev => (_jsxs("button", { type: "button", onClick: () => {
|
|
159
|
+
onTransferToDeveloper(dev);
|
|
160
|
+
setTransferOpen(false);
|
|
161
|
+
}, style: {
|
|
162
|
+
width: '100%',
|
|
163
|
+
textAlign: 'left',
|
|
164
|
+
padding: '12px 12px',
|
|
165
|
+
marginBottom: 6,
|
|
166
|
+
border: '1px solid #eef0f5',
|
|
167
|
+
borderRadius: 12,
|
|
168
|
+
background: '#f8fafc',
|
|
169
|
+
cursor: 'pointer',
|
|
170
|
+
fontSize: 14,
|
|
171
|
+
fontWeight: 600,
|
|
172
|
+
color: '#1e293b',
|
|
173
|
+
}, children: [dev.name, _jsx("span", { style: { display: 'block', fontSize: 11, fontWeight: 500, color: '#64748b', marginTop: 2 }, children: dev.designation })] }, dev.uid))) }), _jsx("button", { type: "button", onClick: () => setTransferOpen(false), style: {
|
|
174
|
+
marginTop: 12,
|
|
175
|
+
padding: '10px',
|
|
176
|
+
borderRadius: 10,
|
|
177
|
+
border: '1.5px solid #e5e7eb',
|
|
178
|
+
background: '#fff',
|
|
179
|
+
fontWeight: 600,
|
|
180
|
+
fontSize: 13,
|
|
181
|
+
color: '#475569',
|
|
182
|
+
cursor: 'pointer',
|
|
183
|
+
}, children: "Cancel" })] }) })), showConfirm && (_jsx("div", { style: {
|
|
133
184
|
position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.45)',
|
|
134
185
|
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 300,
|
|
135
186
|
borderRadius: 'inherit',
|
|
@@ -19,7 +19,7 @@ import { BottomTabs } from './Tabs/BottomTabs';
|
|
|
19
19
|
const DRAWER_W_NORMAL = 380;
|
|
20
20
|
const DRAWER_W_MAX = 480;
|
|
21
21
|
export const ChatWidget = ({ theme: localTheme }) => {
|
|
22
|
-
var _a, _b;
|
|
22
|
+
var _a, _b, _c, _d;
|
|
23
23
|
/* SSR guard */
|
|
24
24
|
const [mounted, setMounted] = useState(false);
|
|
25
25
|
useEffect(() => { setMounted(true); }, []);
|
|
@@ -172,10 +172,39 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
172
172
|
const widgetConfig = data === null || data === void 0 ? void 0 : data.widget;
|
|
173
173
|
const primaryColor = theme.primaryColor;
|
|
174
174
|
const allUsers = data ? [...data.developers, ...data.users] : [];
|
|
175
|
+
const viewerIsDev = (widgetConfig === null || widgetConfig === void 0 ? void 0 : widgetConfig.viewerType) === 'developer';
|
|
176
|
+
const viewerUid = widgetConfig === null || widgetConfig === void 0 ? void 0 : widgetConfig.viewerUid;
|
|
175
177
|
const filteredUsers = screen === 'user-list'
|
|
176
|
-
? allUsers.filter(u =>
|
|
178
|
+
? allUsers.filter(u => {
|
|
179
|
+
if (userListCtx === 'support') {
|
|
180
|
+
if (viewerIsDev)
|
|
181
|
+
return u.type === 'user';
|
|
182
|
+
return u.type === 'developer';
|
|
183
|
+
}
|
|
184
|
+
if (viewerIsDev) {
|
|
185
|
+
return u.type === 'developer' && u.uid !== viewerUid;
|
|
186
|
+
}
|
|
187
|
+
return u.type === 'user';
|
|
188
|
+
})
|
|
177
189
|
: [];
|
|
190
|
+
const otherDevelopers = (_c = data === null || data === void 0 ? void 0 : data.developers.filter(d => d.uid !== viewerUid)) !== null && _c !== void 0 ? _c : [];
|
|
178
191
|
const blockedUsers = allUsers.filter(u => blockedUids.includes(u.uid));
|
|
192
|
+
const handleTransferToDeveloper = useCallback((dev) => {
|
|
193
|
+
var _a;
|
|
194
|
+
if (!activeUser || !widgetConfig)
|
|
195
|
+
return;
|
|
196
|
+
const agent = ((_a = widgetConfig.viewerName) === null || _a === void 0 ? void 0 : _a.trim()) || 'Agent';
|
|
197
|
+
const transferNote = {
|
|
198
|
+
id: `tr_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
199
|
+
senderId: 'me',
|
|
200
|
+
receiverId: dev.uid,
|
|
201
|
+
text: `— ${agent} transferred this conversation from ${activeUser.name} to ${dev.name} —`,
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
type: 'text',
|
|
204
|
+
status: 'sent',
|
|
205
|
+
};
|
|
206
|
+
selectUser(dev, [...messages, transferNote]);
|
|
207
|
+
}, [activeUser, messages, selectUser, widgetConfig]);
|
|
179
208
|
/* Position */
|
|
180
209
|
const posStyle = theme.buttonPosition === 'bottom-left'
|
|
181
210
|
? { left: 24, right: 'auto' }
|
|
@@ -231,7 +260,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
231
260
|
zIndex: 20, display: 'flex', gap: 6,
|
|
232
261
|
}, children: [_jsx(CornerBtn, { onClick: () => setIsMaximized(m => !m), title: isMaximized ? 'Minimize' : 'Maximize', children: isMaximized
|
|
233
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" }) })
|
|
234
|
-
: _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, 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 })), 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' &&
|
|
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' &&
|
|
235
264
|
screen !== 'chat' &&
|
|
236
265
|
screen !== 'call' &&
|
|
237
266
|
screen !== 'user-list' &&
|
|
@@ -2,9 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { SlideNavMenu } from '../SlideNavMenu';
|
|
4
4
|
export const HomeScreen = ({ config, onNavigate, tickets }) => {
|
|
5
|
+
var _a;
|
|
5
6
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
6
7
|
const showSupport = config.chatType === 'SUPPORT' || config.chatType === 'BOTH';
|
|
7
8
|
const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
|
|
9
|
+
const viewerIsDev = config.viewerType === 'developer';
|
|
8
10
|
const continueItems = tickets.slice(0, 2);
|
|
9
11
|
const handleCallUs = () => {
|
|
10
12
|
var _a;
|
|
@@ -13,7 +15,7 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
|
|
|
13
15
|
return;
|
|
14
16
|
window.location.href = `tel:${raw.replace(/\s/g, '')}`;
|
|
15
17
|
};
|
|
16
|
-
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, onSelect: onNavigate }), _jsx("div", { style: {
|
|
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: {
|
|
17
19
|
flexShrink: 0,
|
|
18
20
|
padding: '14px 16px 10px',
|
|
19
21
|
display: 'flex',
|
|
@@ -67,7 +69,7 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
|
|
|
67
69
|
color: '#64748b',
|
|
68
70
|
fontSize: 14,
|
|
69
71
|
fontWeight: 500,
|
|
70
|
-
}, children: "Start via Raise ticket below" })] })) }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children:
|
|
72
|
+
}, children: "Start via Raise ticket below" })] })) }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: viewerIsDev ? 'Support tools' : 'Talk to our experts' }), showSupport && (_jsxs("button", { type: "button", onClick: () => onNavigate('support'), style: {
|
|
71
73
|
width: '100%',
|
|
72
74
|
display: 'flex',
|
|
73
75
|
alignItems: 'center',
|
|
@@ -83,7 +85,7 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
|
|
|
83
85
|
fontWeight: 700,
|
|
84
86
|
cursor: 'pointer',
|
|
85
87
|
boxShadow: '0 2px 8px rgba(91,33,182,0.12)',
|
|
86
|
-
}, children: [_jsx("span", { style: { fontSize: 18 }, children: "\uD83D\uDC64" }),
|
|
88
|
+
}, children: [_jsx("span", { style: { fontSize: 18 }, children: "\uD83D\uDC64" }), viewerIsDev ? 'Provide Support' : 'Support'] })), showChat && showSupport && (_jsx("button", { type: "button", onClick: () => onNavigate('conversation'), style: {
|
|
87
89
|
width: '100%',
|
|
88
90
|
padding: '12px 16px',
|
|
89
91
|
marginBottom: 14,
|
|
@@ -94,7 +96,7 @@ export const HomeScreen = ({ config, onNavigate, tickets }) => {
|
|
|
94
96
|
fontSize: 14,
|
|
95
97
|
fontWeight: 600,
|
|
96
98
|
cursor: 'pointer',
|
|
97
|
-
}, children:
|
|
99
|
+
}, children: viewerIsDev ? 'Chat with a developer' : 'New Conversation' })), showChat && !showSupport && (_jsxs("button", { type: "button", onClick: () => onNavigate('conversation'), style: {
|
|
98
100
|
width: '100%',
|
|
99
101
|
display: 'flex',
|
|
100
102
|
alignItems: 'center',
|
|
@@ -5,6 +5,8 @@ export interface SlideNavMenuProps {
|
|
|
5
5
|
onClose: () => void;
|
|
6
6
|
primaryColor: string;
|
|
7
7
|
chatType: ChatType;
|
|
8
|
+
/** When `developer`, relabels the first two entries for staff */
|
|
9
|
+
viewerType?: 'user' | 'developer';
|
|
8
10
|
onSelect: (ctx: UserListContext | 'ticket') => void;
|
|
9
11
|
/** When set, shows “Back to home” at the bottom (e.g. chat screen) */
|
|
10
12
|
onBackHome?: () => void;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
export const SlideNavMenu = ({ open, onClose, primaryColor, chatType, onSelect, onBackHome, }) => {
|
|
2
|
+
export const SlideNavMenu = ({ open, onClose, primaryColor, chatType, viewerType = 'user', onSelect, onBackHome, }) => {
|
|
3
3
|
const showSupport = chatType === 'SUPPORT' || chatType === 'BOTH';
|
|
4
4
|
const showChat = chatType === 'CHAT' || chatType === 'BOTH';
|
|
5
|
+
const isStaff = viewerType === 'developer';
|
|
5
6
|
const items = [
|
|
6
|
-
showSupport ? { key: 'support', icon: '🛠', title: 'Need Support' } : null,
|
|
7
|
-
showChat ? { key: 'conversation', icon: '💬', title: 'New Conversation' } : null,
|
|
7
|
+
showSupport ? { key: 'support', icon: '🛠', title: isStaff ? 'Provide Support' : 'Need Support' } : null,
|
|
8
|
+
showChat ? { key: 'conversation', icon: '💬', title: isStaff ? 'Chat with developer' : 'New Conversation' } : null,
|
|
8
9
|
{ key: 'ticket', icon: '🎫', title: 'Raise ticket' },
|
|
9
10
|
];
|
|
10
11
|
if (!open)
|
|
@@ -4,6 +4,8 @@ interface UserListScreenProps {
|
|
|
4
4
|
context: UserListContext;
|
|
5
5
|
users: ChatUser[];
|
|
6
6
|
primaryColor: string;
|
|
7
|
+
/** `developer` = staff using the widget (lists customers vs teammates) */
|
|
8
|
+
viewerType?: 'user' | 'developer';
|
|
7
9
|
onBack: () => void;
|
|
8
10
|
onSelectUser: (user: ChatUser) => void;
|
|
9
11
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { avatarColor, initials } from '../../utils/chat';
|
|
3
|
-
export const UserListScreen = ({ context, users, primaryColor, onBack, onSelectUser, }) => {
|
|
4
|
-
const
|
|
5
|
-
const
|
|
3
|
+
export const UserListScreen = ({ context, users, primaryColor, viewerType = 'user', onBack, onSelectUser, }) => {
|
|
4
|
+
const isStaff = viewerType === 'developer';
|
|
5
|
+
const title = context === 'support'
|
|
6
|
+
? (isStaff ? 'Provide Support' : 'Need Support')
|
|
7
|
+
: (isStaff ? 'Developers' : 'New Conversation');
|
|
8
|
+
const subtitle = context === 'support'
|
|
9
|
+
? (isStaff ? 'All chat users — choose who to help' : 'Choose a support agent')
|
|
10
|
+
: (isStaff ? 'Chat with another developer or coordinate handoff' : 'Choose a colleague');
|
|
6
11
|
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease' }, children: [_jsxs("div", { style: { background: `linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }, children: [_jsx(BackBtn, { onClick: onBack }), _jsxs("div", { children: [_jsx("div", { style: { fontWeight: 700, fontSize: 16, color: '#fff' }, children: title }), _jsx("div", { style: { fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: subtitle })] })] }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: users.length === 0 ? (_jsx(Empty, {})) : users.map((u, i) => (_jsxs("button", { onClick: () => onSelectUser(u), style: {
|
|
7
12
|
width: '100%', padding: '13px 18px', display: 'flex',
|
|
8
13
|
alignItems: 'center', gap: 13, background: 'transparent',
|
package/dist/types/index.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export interface WidgetConfig {
|
|
|
14
14
|
footerPoweredBy?: string;
|
|
15
15
|
/** Shown on home “Call Us” (tel: link) */
|
|
16
16
|
supportPhone?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Who is using the widget. `developer` = support staff: “Need Support” becomes “Provide Support”
|
|
19
|
+
* and lists customers; “New Conversation” lists other developers (excl. viewerUid).
|
|
20
|
+
*/
|
|
21
|
+
viewerType?: 'user' | 'developer';
|
|
22
|
+
/** Current user id when viewerType is developer — excluded from developer pick lists */
|
|
23
|
+
viewerUid?: string;
|
|
24
|
+
/** Display name for transfer notes (optional) */
|
|
25
|
+
viewerName?: string;
|
|
17
26
|
allowVoiceMessage: boolean;
|
|
18
27
|
allowAttachment: boolean;
|
|
19
28
|
allowEmoji: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ajaxter-chat",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.6",
|
|
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
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"buttonPosition": "bottom-right",
|
|
10
10
|
"welcomeTitle": "Hi there 👋",
|
|
11
11
|
"welcomeSubtitle": "Need help? Start a conversation:",
|
|
12
|
+
"viewerType": "user",
|
|
13
|
+
"viewerUid": "",
|
|
14
|
+
"viewerName": "",
|
|
12
15
|
"branch": "Mumbai HQ",
|
|
13
16
|
"footerPoweredBy": "Answers by",
|
|
14
17
|
"supportPhone": "+919876543210",
|
|
@@ -20,16 +20,21 @@ interface ChatScreenProps {
|
|
|
20
20
|
onStartCall: (withVideo: boolean) => void;
|
|
21
21
|
/** Navigate to support list, colleague list, or tickets (from slide menu) */
|
|
22
22
|
onNavAction: (ctx: UserListContext | 'ticket') => void;
|
|
23
|
+
/** Other devs (excl. viewer) — for transfer when staff chats with a customer */
|
|
24
|
+
otherDevelopers?: ChatUser[];
|
|
25
|
+
onTransferToDeveloper?: (dev: ChatUser) => void;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export const ChatScreen: React.FC<ChatScreenProps> = ({
|
|
26
29
|
activeUser, messages, config, isPaused, isReported, isBlocked,
|
|
27
30
|
onSend, onBack, onClose, onTogglePause, onReport, onBlock, onStartCall, onNavAction,
|
|
31
|
+
otherDevelopers = [], onTransferToDeveloper,
|
|
28
32
|
}) => {
|
|
29
33
|
const [text, setText] = useState('');
|
|
30
34
|
const [showEmoji, setShowEmoji] = useState(false);
|
|
31
35
|
const [showMenu, setShowMenu] = useState(false);
|
|
32
36
|
const [slideMenuOpen, setSlideMenuOpen] = useState(false);
|
|
37
|
+
const [transferOpen, setTransferOpen] = useState(false);
|
|
33
38
|
const [isRecording, setIsRecording] = useState(false);
|
|
34
39
|
const [recordSec, setRecordSec] = useState(0);
|
|
35
40
|
const [showConfirm, setShowConfirm] = useState<'report'|'block'|'pause'|null>(null);
|
|
@@ -114,6 +119,12 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
|
|
|
114
119
|
const peerInit = initials(activeUser.name);
|
|
115
120
|
const grouped = groupByDate(messages);
|
|
116
121
|
|
|
122
|
+
const viewerIsDev = config.viewerType === 'developer';
|
|
123
|
+
const headerRole =
|
|
124
|
+
viewerIsDev
|
|
125
|
+
? (activeUser.type === 'user' ? 'Customer' : 'Developer')
|
|
126
|
+
: 'Support';
|
|
127
|
+
|
|
117
128
|
return (
|
|
118
129
|
<div style={{ display:'flex', flexDirection:'column', height:'100%', animation:'cw-slideIn 0.22s ease', position:'relative', overflow: 'hidden' }}>
|
|
119
130
|
|
|
@@ -122,6 +133,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
|
|
|
122
133
|
onClose={() => setSlideMenuOpen(false)}
|
|
123
134
|
primaryColor={config.primaryColor}
|
|
124
135
|
chatType={config.chatType}
|
|
136
|
+
viewerType={config.viewerType ?? 'user'}
|
|
125
137
|
onSelect={onNavAction}
|
|
126
138
|
onBackHome={onBack}
|
|
127
139
|
/>
|
|
@@ -149,7 +161,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
|
|
|
149
161
|
<div style={{ fontSize:11, color:'rgba(255,255,255,0.8)' }}>{activeUser.designation}</div>
|
|
150
162
|
</div>
|
|
151
163
|
|
|
152
|
-
<span style={{ fontSize:13, fontWeight:700, color:'#fff', opacity:0.95, flexShrink:0 }}>
|
|
164
|
+
<span style={{ fontSize:13, fontWeight:700, color:'#fff', opacity:0.95, flexShrink:0 }}>{headerRole}</span>
|
|
153
165
|
|
|
154
166
|
{config.allowWebCall && (
|
|
155
167
|
<button type="button" onClick={() => onStartCall(false)} style={hdrBtn} title="Voice Call">
|
|
@@ -173,6 +185,13 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
|
|
|
173
185
|
{config.allowTranscriptDownload && (
|
|
174
186
|
<MenuItem icon="📥" label="Download Transcript" onClick={handleTranscript} />
|
|
175
187
|
)}
|
|
188
|
+
{viewerIsDev && activeUser.type === 'user' && otherDevelopers.length > 0 && onTransferToDeveloper && (
|
|
189
|
+
<MenuItem
|
|
190
|
+
icon="🔀"
|
|
191
|
+
label="Transfer to developer"
|
|
192
|
+
onClick={() => { setShowMenu(false); setTransferOpen(true); }}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
176
195
|
<MenuItem icon={isPaused ? '▶️' : '⏸'} label={isPaused ? 'Resume Chat' : 'Pause Chat'} onClick={() => { setShowMenu(false); setShowConfirm('pause'); }} />
|
|
177
196
|
{config.allowReport && !isReported && (
|
|
178
197
|
<MenuItem icon="⚠️" label="Report Chat" onClick={() => { setShowMenu(false); setShowConfirm('report'); }} />
|
|
@@ -335,6 +354,90 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
|
|
|
335
354
|
)}
|
|
336
355
|
</div>
|
|
337
356
|
|
|
357
|
+
{transferOpen && otherDevelopers.length > 0 && onTransferToDeveloper && (
|
|
358
|
+
<div
|
|
359
|
+
style={{
|
|
360
|
+
position: 'absolute',
|
|
361
|
+
inset: 0,
|
|
362
|
+
background: 'rgba(0,0,0,0.45)',
|
|
363
|
+
display: 'flex',
|
|
364
|
+
alignItems: 'center',
|
|
365
|
+
justifyContent: 'center',
|
|
366
|
+
zIndex: 280,
|
|
367
|
+
padding: 16,
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
<div
|
|
371
|
+
style={{
|
|
372
|
+
background: '#fff',
|
|
373
|
+
borderRadius: 16,
|
|
374
|
+
padding: '18px 16px',
|
|
375
|
+
width: '100%',
|
|
376
|
+
maxWidth: 320,
|
|
377
|
+
maxHeight: '70%',
|
|
378
|
+
overflow: 'hidden',
|
|
379
|
+
display: 'flex',
|
|
380
|
+
flexDirection: 'column',
|
|
381
|
+
boxShadow: '0 16px 48px rgba(0,0,0,0.22)',
|
|
382
|
+
}}
|
|
383
|
+
>
|
|
384
|
+
<div style={{ fontWeight: 800, fontSize: 16, color: '#1a2332', marginBottom: 6 }}>
|
|
385
|
+
Transfer chat to
|
|
386
|
+
</div>
|
|
387
|
+
<p style={{ fontSize: 12, color: '#7b8fa1', margin: '0 0 12px', lineHeight: 1.5 }}>
|
|
388
|
+
Assign this conversation to another developer. History is kept and a handoff note is added.
|
|
389
|
+
</p>
|
|
390
|
+
<div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', margin: '0 -4px' }}>
|
|
391
|
+
{otherDevelopers.map(dev => (
|
|
392
|
+
<button
|
|
393
|
+
key={dev.uid}
|
|
394
|
+
type="button"
|
|
395
|
+
onClick={() => {
|
|
396
|
+
onTransferToDeveloper(dev);
|
|
397
|
+
setTransferOpen(false);
|
|
398
|
+
}}
|
|
399
|
+
style={{
|
|
400
|
+
width: '100%',
|
|
401
|
+
textAlign: 'left',
|
|
402
|
+
padding: '12px 12px',
|
|
403
|
+
marginBottom: 6,
|
|
404
|
+
border: '1px solid #eef0f5',
|
|
405
|
+
borderRadius: 12,
|
|
406
|
+
background: '#f8fafc',
|
|
407
|
+
cursor: 'pointer',
|
|
408
|
+
fontSize: 14,
|
|
409
|
+
fontWeight: 600,
|
|
410
|
+
color: '#1e293b',
|
|
411
|
+
}}
|
|
412
|
+
>
|
|
413
|
+
{dev.name}
|
|
414
|
+
<span style={{ display: 'block', fontSize: 11, fontWeight: 500, color: '#64748b', marginTop: 2 }}>
|
|
415
|
+
{dev.designation}
|
|
416
|
+
</span>
|
|
417
|
+
</button>
|
|
418
|
+
))}
|
|
419
|
+
</div>
|
|
420
|
+
<button
|
|
421
|
+
type="button"
|
|
422
|
+
onClick={() => setTransferOpen(false)}
|
|
423
|
+
style={{
|
|
424
|
+
marginTop: 12,
|
|
425
|
+
padding: '10px',
|
|
426
|
+
borderRadius: 10,
|
|
427
|
+
border: '1.5px solid #e5e7eb',
|
|
428
|
+
background: '#fff',
|
|
429
|
+
fontWeight: 600,
|
|
430
|
+
fontSize: 13,
|
|
431
|
+
color: '#475569',
|
|
432
|
+
cursor: 'pointer',
|
|
433
|
+
}}
|
|
434
|
+
>
|
|
435
|
+
Cancel
|
|
436
|
+
</button>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
)}
|
|
440
|
+
|
|
338
441
|
{showConfirm && (
|
|
339
442
|
<div style={{
|
|
340
443
|
position:'absolute', inset:0, background:'rgba(0,0,0,0.45)',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
-
import { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket, RecentChat } from '../types';
|
|
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';
|
|
7
7
|
import { useRemoteConfig } from '../hooks/useRemoteConfig';
|
|
@@ -196,11 +196,40 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
196
196
|
const primaryColor = theme.primaryColor;
|
|
197
197
|
|
|
198
198
|
const allUsers = data ? [...data.developers, ...data.users] : [];
|
|
199
|
+
const viewerIsDev = widgetConfig?.viewerType === 'developer';
|
|
200
|
+
const viewerUid = widgetConfig?.viewerUid;
|
|
201
|
+
|
|
199
202
|
const filteredUsers = screen === 'user-list'
|
|
200
|
-
? allUsers.filter(u =>
|
|
203
|
+
? allUsers.filter(u => {
|
|
204
|
+
if (userListCtx === 'support') {
|
|
205
|
+
if (viewerIsDev) return u.type === 'user';
|
|
206
|
+
return u.type === 'developer';
|
|
207
|
+
}
|
|
208
|
+
if (viewerIsDev) {
|
|
209
|
+
return u.type === 'developer' && u.uid !== viewerUid;
|
|
210
|
+
}
|
|
211
|
+
return u.type === 'user';
|
|
212
|
+
})
|
|
201
213
|
: [];
|
|
214
|
+
|
|
215
|
+
const otherDevelopers = data?.developers.filter(d => d.uid !== viewerUid) ?? [];
|
|
202
216
|
const blockedUsers = allUsers.filter(u => blockedUids.includes(u.uid));
|
|
203
217
|
|
|
218
|
+
const handleTransferToDeveloper = useCallback((dev: ChatUser) => {
|
|
219
|
+
if (!activeUser || !widgetConfig) return;
|
|
220
|
+
const agent = widgetConfig.viewerName?.trim() || 'Agent';
|
|
221
|
+
const transferNote: ChatMessage = {
|
|
222
|
+
id: `tr_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
223
|
+
senderId: 'me',
|
|
224
|
+
receiverId: dev.uid,
|
|
225
|
+
text: `— ${agent} transferred this conversation from ${activeUser.name} to ${dev.name} —`,
|
|
226
|
+
timestamp: new Date().toISOString(),
|
|
227
|
+
type: 'text',
|
|
228
|
+
status: 'sent',
|
|
229
|
+
};
|
|
230
|
+
selectUser(dev, [...messages, transferNote]);
|
|
231
|
+
}, [activeUser, messages, selectUser, widgetConfig]);
|
|
232
|
+
|
|
204
233
|
/* Position */
|
|
205
234
|
const posStyle: React.CSSProperties = theme.buttonPosition === 'bottom-left'
|
|
206
235
|
? { left: 24, right: 'auto' }
|
|
@@ -385,6 +414,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
385
414
|
context={userListCtx}
|
|
386
415
|
users={filteredUsers}
|
|
387
416
|
primaryColor={primaryColor}
|
|
417
|
+
viewerType={widgetConfig.viewerType ?? 'user'}
|
|
388
418
|
onBack={() => setScreen('home')}
|
|
389
419
|
onSelectUser={handleSelectUser}
|
|
390
420
|
/>
|
|
@@ -406,6 +436,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
406
436
|
onBlock={handleBlock}
|
|
407
437
|
onStartCall={handleStartCall}
|
|
408
438
|
onNavAction={handleNavFromMenu}
|
|
439
|
+
otherDevelopers={otherDevelopers}
|
|
440
|
+
onTransferToDeveloper={handleTransferToDeveloper}
|
|
409
441
|
/>
|
|
410
442
|
)}
|
|
411
443
|
|
|
@@ -12,6 +12,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
|
|
|
12
12
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
13
13
|
const showSupport = config.chatType === 'SUPPORT' || config.chatType === 'BOTH';
|
|
14
14
|
const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
|
|
15
|
+
const viewerIsDev = config.viewerType === 'developer';
|
|
15
16
|
|
|
16
17
|
const continueItems = tickets.slice(0, 2);
|
|
17
18
|
|
|
@@ -28,6 +29,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
|
|
|
28
29
|
onClose={() => setMenuOpen(false)}
|
|
29
30
|
primaryColor={config.primaryColor}
|
|
30
31
|
chatType={config.chatType}
|
|
32
|
+
viewerType={config.viewerType ?? 'user'}
|
|
31
33
|
onSelect={onNavigate}
|
|
32
34
|
/>
|
|
33
35
|
|
|
@@ -142,8 +144,10 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
|
|
|
142
144
|
)}
|
|
143
145
|
</div>
|
|
144
146
|
|
|
145
|
-
{/* Talk to our experts */}
|
|
146
|
-
<h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>
|
|
147
|
+
{/* Talk to our experts / staff tools */}
|
|
148
|
+
<h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>
|
|
149
|
+
{viewerIsDev ? 'Support tools' : 'Talk to our experts'}
|
|
150
|
+
</h2>
|
|
147
151
|
|
|
148
152
|
{showSupport && (
|
|
149
153
|
<button
|
|
@@ -168,7 +172,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
|
|
|
168
172
|
}}
|
|
169
173
|
>
|
|
170
174
|
<span style={{ fontSize: 18 }}>👤</span>
|
|
171
|
-
Support
|
|
175
|
+
{viewerIsDev ? 'Provide Support' : 'Support'}
|
|
172
176
|
</button>
|
|
173
177
|
)}
|
|
174
178
|
|
|
@@ -189,7 +193,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tick
|
|
|
189
193
|
cursor: 'pointer',
|
|
190
194
|
}}
|
|
191
195
|
>
|
|
192
|
-
New Conversation
|
|
196
|
+
{viewerIsDev ? 'Chat with a developer' : 'New Conversation'}
|
|
193
197
|
</button>
|
|
194
198
|
)}
|
|
195
199
|
|
|
@@ -6,6 +6,8 @@ export interface SlideNavMenuProps {
|
|
|
6
6
|
onClose: () => void;
|
|
7
7
|
primaryColor: string;
|
|
8
8
|
chatType: ChatType;
|
|
9
|
+
/** When `developer`, relabels the first two entries for staff */
|
|
10
|
+
viewerType?: 'user' | 'developer';
|
|
9
11
|
onSelect: (ctx: UserListContext | 'ticket') => void;
|
|
10
12
|
/** When set, shows “Back to home” at the bottom (e.g. chat screen) */
|
|
11
13
|
onBackHome?: () => void;
|
|
@@ -16,15 +18,17 @@ export const SlideNavMenu: React.FC<SlideNavMenuProps> = ({
|
|
|
16
18
|
onClose,
|
|
17
19
|
primaryColor,
|
|
18
20
|
chatType,
|
|
21
|
+
viewerType = 'user',
|
|
19
22
|
onSelect,
|
|
20
23
|
onBackHome,
|
|
21
24
|
}) => {
|
|
22
25
|
const showSupport = chatType === 'SUPPORT' || chatType === 'BOTH';
|
|
23
26
|
const showChat = chatType === 'CHAT' || chatType === 'BOTH';
|
|
27
|
+
const isStaff = viewerType === 'developer';
|
|
24
28
|
|
|
25
29
|
const items: Array<{ key: UserListContext | 'ticket'; icon: string; title: string } | null> = [
|
|
26
|
-
showSupport ? { key: 'support', icon: '🛠', title: 'Need Support' } : null,
|
|
27
|
-
showChat ? { key: 'conversation', icon: '💬', title: 'New Conversation' } : null,
|
|
30
|
+
showSupport ? { key: 'support', icon: '🛠', title: isStaff ? 'Provide Support' : 'Need Support' } : null,
|
|
31
|
+
showChat ? { key: 'conversation', icon: '💬', title: isStaff ? 'Chat with developer' : 'New Conversation' } : null,
|
|
28
32
|
{ key: 'ticket', icon: '🎫', title: 'Raise ticket' },
|
|
29
33
|
];
|
|
30
34
|
|
|
@@ -6,15 +6,22 @@ interface UserListScreenProps {
|
|
|
6
6
|
context: UserListContext;
|
|
7
7
|
users: ChatUser[];
|
|
8
8
|
primaryColor: string;
|
|
9
|
+
/** `developer` = staff using the widget (lists customers vs teammates) */
|
|
10
|
+
viewerType?: 'user' | 'developer';
|
|
9
11
|
onBack: () => void;
|
|
10
12
|
onSelectUser: (user: ChatUser) => void;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export const UserListScreen: React.FC<UserListScreenProps> = ({
|
|
14
|
-
context, users, primaryColor, onBack, onSelectUser,
|
|
16
|
+
context, users, primaryColor, viewerType = 'user', onBack, onSelectUser,
|
|
15
17
|
}) => {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
+
const isStaff = viewerType === 'developer';
|
|
19
|
+
const title = context === 'support'
|
|
20
|
+
? (isStaff ? 'Provide Support' : 'Need Support')
|
|
21
|
+
: (isStaff ? 'Developers' : 'New Conversation');
|
|
22
|
+
const subtitle = context === 'support'
|
|
23
|
+
? (isStaff ? 'All chat users — choose who to help' : 'Choose a support agent')
|
|
24
|
+
: (isStaff ? 'Chat with another developer or coordinate handoff' : 'Choose a colleague');
|
|
18
25
|
|
|
19
26
|
return (
|
|
20
27
|
<div style={{ display:'flex', flexDirection:'column', height:'100%', animation:'cw-slideIn 0.22s ease' }}>
|
package/src/types/index.ts
CHANGED
|
@@ -15,6 +15,15 @@ export interface WidgetConfig {
|
|
|
15
15
|
footerPoweredBy?: string;
|
|
16
16
|
/** Shown on home “Call Us” (tel: link) */
|
|
17
17
|
supportPhone?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Who is using the widget. `developer` = support staff: “Need Support” becomes “Provide Support”
|
|
20
|
+
* and lists customers; “New Conversation” lists other developers (excl. viewerUid).
|
|
21
|
+
*/
|
|
22
|
+
viewerType?: 'user' | 'developer';
|
|
23
|
+
/** Current user id when viewerType is developer — excluded from developer pick lists */
|
|
24
|
+
viewerUid?: string;
|
|
25
|
+
/** Display name for transfer notes (optional) */
|
|
26
|
+
viewerName?: string;
|
|
18
27
|
allowVoiceMessage: boolean;
|
|
19
28
|
allowAttachment: boolean;
|
|
20
29
|
allowEmoji: boolean;
|