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.
@@ -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
- 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, onSelect: onNavAction, onBackHome: onBack }), _jsxs("div", { style: {
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: "Support" }), 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 })), _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: {
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 })] }))] }), showConfirm && (_jsx("div", { style: {
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 => userListCtx === 'support' ? u.type === 'developer' : u.type === 'user')
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: "Talk to our experts" }), showSupport && (_jsxs("button", { type: "button", onClick: () => onNavigate('support'), style: {
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" }), "Support"] })), showChat && showSupport && (_jsx("button", { type: "button", onClick: () => onNavigate('conversation'), style: {
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: "New Conversation" })), showChat && !showSupport && (_jsxs("button", { type: "button", onClick: () => onNavigate('conversation'), style: {
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 title = context === 'support' ? 'Need Support' : 'New Conversation';
5
- const subtitle = context === 'support' ? 'Choose a support agent' : 'Choose a colleague';
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',
@@ -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.5",
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",
@@ -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 }}>Support</span>
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 => userListCtx === 'support' ? u.type === 'developer' : u.type === 'user')
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' }}>Talk to our experts</h2>
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 title = context === 'support' ? 'Need Support' : 'New Conversation';
17
- const subtitle = context === 'support' ? 'Choose a support agent' : 'Choose a colleague';
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' }}>
@@ -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;