shopify-chatbot-widget 1.0.8 → 1.0.10
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/.env.development +2 -0
- package/dist/jaweb-chatbot.css +1 -1
- package/dist/jaweb-chatbot.iife.js +192 -599
- package/home.html +1 -1
- package/jawebDirect.js +1 -1
- package/package.json +1 -1
- package/src/App.tsx +9 -0
- package/src/Chatbot/Chatbot.css +193 -113
- package/src/Chatbot/Chatbot.tsx +26 -19
- package/src/Chatbot/components/Cart/Index.tsx +10 -8
- package/src/Chatbot/components/ChatInput.tsx +11 -11
- package/src/Chatbot/components/Home/Home.css +456 -0
- package/src/Chatbot/components/Home/Home.tsx +293 -0
- package/src/Chatbot/components/Messages/Addcart.tsx +1 -1
- package/src/Chatbot/components/Messages/AssistantMesage.tsx +78 -37
- package/src/Chatbot/components/Messages/CardSwiper.tsx +1 -1
- package/src/Chatbot/components/Messages/Carousel/CardSwiper.tsx +87 -92
- package/src/Chatbot/components/Messages/Carousel/ProductSwiper.css +361 -0
- package/src/Chatbot/components/Messages/Carousel/TryOn.tsx +2 -2
- package/src/Chatbot/components/Messages/ChatMessages.css +32 -1
- package/src/Chatbot/components/Messages/ChatMessages.tsx +21 -38
- package/src/Chatbot/components/Messages/OrderCard.css +177 -0
- package/src/Chatbot/components/Messages/OrderCard.tsx +120 -0
- package/src/Chatbot/components/Messages/ProductCard.css +1 -1
- package/src/Chatbot/components/Messages/Support.tsx +311 -118
- package/src/Chatbot/components/Messages/Typing.css +31 -35
- package/src/Chatbot/components/Messages/Typing.tsx +19 -18
- package/src/Chatbot/components/Messenger/Messenger.tsx +67 -14
- package/src/Chatbot/components/Powered.css +26 -28
- package/src/Chatbot/components/Powered.tsx +1 -1
- package/src/Chatbot/components/Sessions/CreateSession.tsx +12 -10
- package/src/Chatbot/components/TryOn/View.tsx +2 -2
- package/src/Chatbot/components/VoiceMode.css +247 -0
- package/src/Chatbot/components/VoiceMode.tsx +613 -0
- package/src/hooks/config.tsx +9 -10
- package/src/hooks/useChatbotAPI.tsx +146 -61
- package/src/hooks/useChatbotData.tsx +8 -5
- package/src/hooks/useSessionMessages.tsx +0 -1
- package/src/hooks/useWebsocke.tsx +40 -18
- package/src/i18n.tsx +384 -10
- package/src/Chatbot/components/Messages/Carousel/CardSwiperSalla.tsx +0 -187
- package/src/Chatbot/components/Messages/Carousel/CardSwiperZid.tsx +0 -208
- package/src/Chatbot/components/Messages/Carousel/SallaSwiper.css +0 -245
- package/src/Chatbot/components/Messages/SallaAssistantMessage.tsx +0 -202
- package/src/Chatbot/components/Messages/ZidAssistantMesage.tsx +0 -203
- package/src/Chatbot/components/Sessions/RenderList.tsx +0 -519
|
@@ -1,14 +1,117 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
-
import {
|
|
3
|
+
import { Ticket as TicketIcon, MailCheck } from 'lucide-react';
|
|
4
4
|
import config from '../../../hooks/config';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import i18nInstance from '../../../i18n';
|
|
7
|
+
|
|
8
|
+
export interface SupportTicket {
|
|
9
|
+
ticket_id: number;
|
|
10
|
+
status?: string;
|
|
11
|
+
created_at?: string;
|
|
12
|
+
}
|
|
5
13
|
|
|
6
14
|
interface SupportConnectProps {
|
|
7
15
|
sessionId?: string;
|
|
8
16
|
colorCode?: string;
|
|
17
|
+
ticket?: SupportTicket | null;
|
|
9
18
|
}
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
const timeAgo = (iso?: string): string => {
|
|
21
|
+
if (!iso) return '';
|
|
22
|
+
const mins = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 60000));
|
|
23
|
+
if (mins < 1) return i18nInstance.t('time.justNow');
|
|
24
|
+
if (mins < 60) return i18nInstance.t('time.m', { n: mins });
|
|
25
|
+
const hours = Math.round(mins / 60);
|
|
26
|
+
if (hours < 24) return i18nInstance.t('time.h', { n: hours });
|
|
27
|
+
return i18nInstance.t('time.d', { n: Math.round(hours / 24) });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** "Ticket submitted" divider + ticket card. Used by the live support flow
|
|
31
|
+
* and by AssistantMessage when a saved message carries a ticket block. */
|
|
32
|
+
export const TicketCard: React.FC<{ ticket: SupportTicket; colorCode?: string }> = ({
|
|
33
|
+
ticket,
|
|
34
|
+
colorCode = '#111111',
|
|
35
|
+
}) => {
|
|
36
|
+
const { t } = useTranslation();
|
|
37
|
+
const statusColor = (ticket.status || 'Open') === 'Solved' ? '#16a34a' : colorCode;
|
|
38
|
+
return (
|
|
39
|
+
<div style={{ maxWidth: 320, marginBottom: 10, fontFamily: "'Inter', -apple-system, sans-serif" }}>
|
|
40
|
+
<div
|
|
41
|
+
style={{
|
|
42
|
+
display: 'flex',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
gap: 8,
|
|
45
|
+
margin: '4px 0 10px',
|
|
46
|
+
color: '#9ca3af',
|
|
47
|
+
fontSize: 11,
|
|
48
|
+
fontWeight: 600,
|
|
49
|
+
letterSpacing: '0.05em',
|
|
50
|
+
textTransform: 'uppercase',
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<span style={{ flex: 1, height: 1, background: '#ececec' }} />
|
|
54
|
+
<TicketIcon size={12} />
|
|
55
|
+
{t('support.ticketSubmitted')}
|
|
56
|
+
<span style={{ flex: 1, height: 1, background: '#ececec' }} />
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div
|
|
60
|
+
style={{
|
|
61
|
+
background: '#ffffff',
|
|
62
|
+
border: '1px solid #ececec',
|
|
63
|
+
borderRadius: 16,
|
|
64
|
+
padding: '12px 14px',
|
|
65
|
+
boxShadow: '0 1px 2px rgba(17,24,39,0.05)',
|
|
66
|
+
display: 'flex',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
gap: 12,
|
|
69
|
+
position: 'relative',
|
|
70
|
+
overflow: 'hidden',
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<span
|
|
74
|
+
style={{
|
|
75
|
+
width: 36,
|
|
76
|
+
height: 36,
|
|
77
|
+
borderRadius: 10,
|
|
78
|
+
background: `color-mix(in srgb, ${colorCode} 10%, #ffffff)`,
|
|
79
|
+
display: 'flex',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
justifyContent: 'center',
|
|
82
|
+
color: colorCode,
|
|
83
|
+
flexShrink: 0,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<TicketIcon size={17} />
|
|
87
|
+
</span>
|
|
88
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
89
|
+
<p style={{ margin: 0, fontSize: 13, fontWeight: 700, color: '#111827' }}>
|
|
90
|
+
{t('home.ticketN', { id: ticket.ticket_id })}
|
|
91
|
+
</p>
|
|
92
|
+
<p style={{ margin: 0, fontSize: 12, fontWeight: 600, color: statusColor }}>
|
|
93
|
+
{t(`status.${ticket.status || 'Open'}`, ticket.status || 'Open')}
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
<span style={{ fontSize: 11, color: '#9ca3af', flexShrink: 0 }}>
|
|
97
|
+
{timeAgo(ticket.created_at)}
|
|
98
|
+
</span>
|
|
99
|
+
<span
|
|
100
|
+
style={{
|
|
101
|
+
position: 'absolute',
|
|
102
|
+
left: 0,
|
|
103
|
+
right: 0,
|
|
104
|
+
bottom: 0,
|
|
105
|
+
height: 3,
|
|
106
|
+
background: `linear-gradient(90deg, ${colorCode}, color-mix(in srgb, ${colorCode} 30%, #ffffff))`,
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
type Stage = 'email_sent' | 'connecting' | 'auto' | 'notified';
|
|
12
115
|
|
|
13
116
|
interface SupportData {
|
|
14
117
|
userImage?: string;
|
|
@@ -17,80 +120,161 @@ interface SupportData {
|
|
|
17
120
|
agentAvatar?: string;
|
|
18
121
|
}
|
|
19
122
|
|
|
123
|
+
/** Circle with the agent's initial — used when no real avatar exists. */
|
|
124
|
+
const Initial: React.FC<{ name?: string; src?: string; accent: string }> = ({
|
|
125
|
+
name,
|
|
126
|
+
src,
|
|
127
|
+
accent,
|
|
128
|
+
}) => {
|
|
129
|
+
const [broken, setBroken] = useState(false);
|
|
130
|
+
const showImg = src && !broken && src.startsWith('http');
|
|
131
|
+
if (showImg) {
|
|
132
|
+
return (
|
|
133
|
+
<img
|
|
134
|
+
src={src}
|
|
135
|
+
alt={name || 'Agent'}
|
|
136
|
+
onError={() => setBroken(true)}
|
|
137
|
+
style={{
|
|
138
|
+
width: 36,
|
|
139
|
+
height: 36,
|
|
140
|
+
borderRadius: '50%',
|
|
141
|
+
objectFit: 'cover',
|
|
142
|
+
border: '1px solid #ececec',
|
|
143
|
+
flexShrink: 0,
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return (
|
|
149
|
+
<span
|
|
150
|
+
style={{
|
|
151
|
+
width: 36,
|
|
152
|
+
height: 36,
|
|
153
|
+
borderRadius: '50%',
|
|
154
|
+
background: accent,
|
|
155
|
+
color: '#fff',
|
|
156
|
+
display: 'flex',
|
|
157
|
+
alignItems: 'center',
|
|
158
|
+
justifyContent: 'center',
|
|
159
|
+
fontSize: 14,
|
|
160
|
+
fontWeight: 700,
|
|
161
|
+
flexShrink: 0,
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{(name || 'S').slice(0, 1).toUpperCase()}
|
|
165
|
+
</span>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const card: React.CSSProperties = {
|
|
170
|
+
background: '#ffffff',
|
|
171
|
+
border: '1px solid #ececec',
|
|
172
|
+
borderRadius: 16,
|
|
173
|
+
padding: '12px 14px',
|
|
174
|
+
boxShadow: '0 1px 2px rgba(17,24,39,0.05)',
|
|
175
|
+
display: 'flex',
|
|
176
|
+
alignItems: 'center',
|
|
177
|
+
gap: 12,
|
|
178
|
+
maxWidth: 320,
|
|
179
|
+
fontFamily: "'Inter', -apple-system, sans-serif",
|
|
180
|
+
};
|
|
181
|
+
|
|
20
182
|
const SupportConnect: React.FC<SupportConnectProps> = ({
|
|
21
183
|
sessionId,
|
|
22
|
-
colorCode = '#
|
|
184
|
+
colorCode = '#111111',
|
|
185
|
+
ticket,
|
|
23
186
|
}) => {
|
|
24
|
-
const
|
|
187
|
+
const { t } = useTranslation();
|
|
188
|
+
const [stage, setStage] = useState<Stage>('email_sent');
|
|
25
189
|
const [data, setData] = useState<SupportData>({});
|
|
26
|
-
const
|
|
27
|
-
|
|
190
|
+
const stageRef = useRef<Stage>('email_sent');
|
|
191
|
+
stageRef.current = stage;
|
|
28
192
|
|
|
193
|
+
// Live stage updates, with reconnect — the consumer replays the last
|
|
194
|
+
// cached stage on connect, so late joins and refreshes recover state.
|
|
29
195
|
useEffect(() => {
|
|
30
196
|
if (!sessionId) return;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
197
|
+
let closed = false;
|
|
198
|
+
let attempts = 0;
|
|
199
|
+
let timer: number | undefined;
|
|
200
|
+
let ws: WebSocket;
|
|
34
201
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
202
|
+
const connect = () => {
|
|
203
|
+
ws = new WebSocket(`${config.websocketUrl}/support/${sessionId}/`);
|
|
204
|
+
ws.onopen = () => {
|
|
205
|
+
attempts = 0;
|
|
206
|
+
};
|
|
207
|
+
ws.onmessage = ({ data: text }) => {
|
|
208
|
+
try {
|
|
209
|
+
const msg = JSON.parse(text);
|
|
210
|
+
if (!msg.stage) return;
|
|
211
|
+
// A replayed "in progress" stage older than 2 minutes means nobody
|
|
212
|
+
// picked up — show the honest offline state, don't re-run the show.
|
|
213
|
+
const ageSec = msg.ts ? Date.now() / 1000 - msg.ts : 0;
|
|
214
|
+
if (
|
|
215
|
+
ageSec > 120 &&
|
|
216
|
+
(msg.stage === 'email_sent' || msg.stage === 'connecting')
|
|
217
|
+
) {
|
|
218
|
+
setStage('notified');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
setStage(msg.stage as Stage);
|
|
222
|
+
setData(msg.data || {});
|
|
223
|
+
} catch {
|
|
224
|
+
/* ignore malformed frames */
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
ws.onclose = () => {
|
|
228
|
+
if (closed) return;
|
|
229
|
+
const delay = Math.min(1000 * 2 ** attempts, 15000);
|
|
230
|
+
attempts += 1;
|
|
231
|
+
timer = window.setTimeout(connect, delay);
|
|
232
|
+
};
|
|
39
233
|
};
|
|
40
|
-
ws.onclose = () => console.log('Support socket closed');
|
|
41
|
-
return () => ws.close();
|
|
42
|
-
}, [sessionId]);
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
let timerId: number;
|
|
46
|
-
let start: number;
|
|
47
|
-
|
|
48
|
-
if (stage === 'connecting') {
|
|
49
|
-
start = Date.now();
|
|
50
|
-
setProgress(100);
|
|
51
|
-
|
|
52
|
-
timerId = window.setInterval(() => {
|
|
53
|
-
const elapsed = (Date.now() - start) / 1000; // seconds
|
|
54
|
-
const pct = Math.max(100 - (elapsed / 7) * 100, 0);
|
|
55
|
-
setProgress(pct);
|
|
56
|
-
if (pct <= 0) window.clearInterval(timerId);
|
|
57
|
-
}, 100);
|
|
58
|
-
}
|
|
59
234
|
|
|
235
|
+
connect();
|
|
60
236
|
return () => {
|
|
61
|
-
|
|
237
|
+
closed = true;
|
|
238
|
+
if (timer) window.clearTimeout(timer);
|
|
239
|
+
ws.close();
|
|
62
240
|
};
|
|
63
|
-
}, [
|
|
241
|
+
}, [sessionId]);
|
|
242
|
+
|
|
243
|
+
// Never strand the visitor on an animation: if nobody picks up within
|
|
244
|
+
// 75s (celery down, empty team, lost events), fall back to "notified".
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
const t = window.setTimeout(() => {
|
|
247
|
+
if (stageRef.current === 'email_sent' || stageRef.current === 'connecting') {
|
|
248
|
+
setStage('notified');
|
|
249
|
+
}
|
|
250
|
+
}, 75000);
|
|
251
|
+
return () => window.clearTimeout(t);
|
|
252
|
+
}, []);
|
|
64
253
|
|
|
65
|
-
|
|
254
|
+
const agent = data.agentName || t('support.teamMember');
|
|
66
255
|
|
|
67
256
|
return (
|
|
68
|
-
<div
|
|
69
|
-
<
|
|
257
|
+
<div style={{ marginTop: 8 }}>
|
|
258
|
+
{ticket && <TicketCard ticket={ticket} colorCode={colorCode} />}
|
|
259
|
+
<AnimatePresence mode="wait">
|
|
70
260
|
{stage === 'email_sent' && (
|
|
71
261
|
<motion.div
|
|
72
262
|
key="email"
|
|
73
|
-
|
|
74
|
-
initial={{
|
|
75
|
-
animate={{
|
|
76
|
-
exit={{
|
|
263
|
+
style={card}
|
|
264
|
+
initial={{ opacity: 0, y: 10 }}
|
|
265
|
+
animate={{ opacity: 1, y: 0 }}
|
|
266
|
+
exit={{ opacity: 0, y: -8 }}
|
|
77
267
|
>
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
animate={{ scale: [0.7, 1.3, 0.7] }}
|
|
89
|
-
transition={{ delay: i * 0.2, repeat: Infinity, duration: 0.8 }}
|
|
90
|
-
>
|
|
91
|
-
•
|
|
92
|
-
</motion.span>
|
|
93
|
-
))}
|
|
268
|
+
<span className="jwbS-bars" style={{ '--jwbS-accent': colorCode } as React.CSSProperties}>
|
|
269
|
+
<span /><span /><span />
|
|
270
|
+
</span>
|
|
271
|
+
<div>
|
|
272
|
+
<p style={{ margin: 0, fontSize: 13, fontWeight: 700, color: '#111827' }}>
|
|
273
|
+
{t('support.reaching')}
|
|
274
|
+
</p>
|
|
275
|
+
<p style={{ margin: 0, fontSize: 12, color: '#6b7280' }}>
|
|
276
|
+
{t('support.requestSent')}
|
|
277
|
+
</p>
|
|
94
278
|
</div>
|
|
95
279
|
</motion.div>
|
|
96
280
|
)}
|
|
@@ -98,75 +282,84 @@ const SupportConnect: React.FC<SupportConnectProps> = ({
|
|
|
98
282
|
{stage === 'connecting' && (
|
|
99
283
|
<motion.div
|
|
100
284
|
key="connecting"
|
|
101
|
-
|
|
102
|
-
initial={{
|
|
103
|
-
animate={{
|
|
104
|
-
exit={{
|
|
285
|
+
style={card}
|
|
286
|
+
initial={{ opacity: 0, y: 10 }}
|
|
287
|
+
animate={{ opacity: 1, y: 0 }}
|
|
288
|
+
exit={{ opacity: 0, y: -8 }}
|
|
105
289
|
>
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<p className="text-lg">
|
|
114
|
-
Connecting you to{' '}
|
|
115
|
-
<span className="text-indigo-600">
|
|
116
|
-
{data.agentName || 'Support Agent'}
|
|
117
|
-
</span>
|
|
290
|
+
<Initial name={agent} src={data.userImage} accent={colorCode} />
|
|
291
|
+
<div style={{ minWidth: 0 }}>
|
|
292
|
+
<p style={{ margin: 0, fontSize: 13, fontWeight: 700, color: '#111827' }}>
|
|
293
|
+
{t('support.connectingTo', { name: agent })}
|
|
294
|
+
</p>
|
|
295
|
+
<p style={{ margin: 0, fontSize: 12, color: '#6b7280' }}>
|
|
296
|
+
{t('support.oneMoment')}
|
|
118
297
|
</p>
|
|
119
|
-
<img
|
|
120
|
-
src={data.companyImage || 'https://cdn-icons-png.flaticon.com/512/4812/4812244.png'}
|
|
121
|
-
alt="Company"
|
|
122
|
-
className="w-12 h-12 rounded-full border"
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
125
|
-
<div className="flex space-x-2 mt-3">
|
|
126
|
-
{dots.map((_, i) => (
|
|
127
|
-
<motion.span
|
|
128
|
-
key={i}
|
|
129
|
-
className="text-2xl"
|
|
130
|
-
style={{ color: colorCode }}
|
|
131
|
-
animate={{ scale: [0.7, 1.3, 0.7] }}
|
|
132
|
-
transition={{ delay: i * 0.2, repeat: Infinity, duration: 0.8 }}
|
|
133
|
-
>
|
|
134
|
-
•
|
|
135
|
-
</motion.span>
|
|
136
|
-
))}
|
|
137
|
-
</div>
|
|
138
|
-
<div className="w-full bg-gray-200 h-1 rounded-full mt-4 overflow-hidden">
|
|
139
|
-
<motion.div
|
|
140
|
-
className="h-full"
|
|
141
|
-
style={{ width: `${progress}%`, backgroundColor: colorCode }}
|
|
142
|
-
transition={{ ease: 'linear', duration: 1 }}
|
|
143
|
-
/>
|
|
144
298
|
</div>
|
|
299
|
+
<span
|
|
300
|
+
className="jwbS-bars"
|
|
301
|
+
style={{ '--jwbS-accent': colorCode, marginLeft: 'auto' } as React.CSSProperties}
|
|
302
|
+
>
|
|
303
|
+
<span /><span /><span />
|
|
304
|
+
</span>
|
|
145
305
|
</motion.div>
|
|
146
306
|
)}
|
|
147
307
|
|
|
148
308
|
{stage === 'auto' && (
|
|
149
309
|
<motion.div
|
|
150
|
-
key="
|
|
151
|
-
|
|
152
|
-
initial={{
|
|
153
|
-
animate={{
|
|
154
|
-
exit={{ y: -50, opacity: 0 }}
|
|
310
|
+
key="connected"
|
|
311
|
+
style={card}
|
|
312
|
+
initial={{ opacity: 0, y: 10 }}
|
|
313
|
+
animate={{ opacity: 1, y: 0 }}
|
|
155
314
|
>
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
'
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
315
|
+
<Initial name={agent} src={data.agentAvatar} accent={colorCode} />
|
|
316
|
+
<div style={{ minWidth: 0 }}>
|
|
317
|
+
<p style={{ margin: 0, fontSize: 13, fontWeight: 700, color: '#111827' }}>
|
|
318
|
+
{t('support.chattingWith', { name: agent })}
|
|
319
|
+
</p>
|
|
320
|
+
<p
|
|
321
|
+
style={{
|
|
322
|
+
margin: 0,
|
|
323
|
+
fontSize: 12,
|
|
324
|
+
color: '#16a34a',
|
|
325
|
+
display: 'flex',
|
|
326
|
+
alignItems: 'center',
|
|
327
|
+
gap: 5,
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
<span
|
|
331
|
+
style={{
|
|
332
|
+
width: 6,
|
|
333
|
+
height: 6,
|
|
334
|
+
borderRadius: '50%',
|
|
335
|
+
background: '#16a34a',
|
|
336
|
+
display: 'inline-block',
|
|
337
|
+
}}
|
|
338
|
+
/>
|
|
339
|
+
{t('support.liveNow')}
|
|
340
|
+
</p>
|
|
341
|
+
</div>
|
|
342
|
+
</motion.div>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
{stage === 'notified' && (
|
|
346
|
+
<motion.div
|
|
347
|
+
key="notified"
|
|
348
|
+
style={card}
|
|
349
|
+
initial={{ opacity: 0, y: 10 }}
|
|
350
|
+
animate={{ opacity: 1, y: 0 }}
|
|
351
|
+
>
|
|
352
|
+
<span style={{ color: '#6b7280', display: 'flex' }}>
|
|
353
|
+
<MailCheck size={20} />
|
|
354
|
+
</span>
|
|
355
|
+
<div style={{ minWidth: 0 }}>
|
|
356
|
+
<p style={{ margin: 0, fontSize: 13, fontWeight: 700, color: '#111827' }}>
|
|
357
|
+
{t('support.teamOffline')}
|
|
358
|
+
</p>
|
|
359
|
+
<p style={{ margin: 0, fontSize: 12, color: '#6b7280' }}>
|
|
360
|
+
{t('support.savedSpot')}
|
|
361
|
+
</p>
|
|
362
|
+
</div>
|
|
170
363
|
</motion.div>
|
|
171
364
|
)}
|
|
172
365
|
</AnimatePresence>
|
|
@@ -1,55 +1,51 @@
|
|
|
1
|
-
/* Typing
|
|
1
|
+
/* Typing indicator — mini equalizer bars in a message bubble */
|
|
2
2
|
|
|
3
3
|
.typing-container {
|
|
4
|
-
/* Layout */
|
|
5
4
|
display: inline-flex;
|
|
6
5
|
align-items: center;
|
|
7
6
|
justify-content: center;
|
|
8
7
|
gap: var(--typing-gap);
|
|
9
|
-
|
|
10
|
-
/* Appearance */
|
|
8
|
+
|
|
11
9
|
background-color: var(--typing-bg);
|
|
12
10
|
padding: var(--typing-padding);
|
|
13
|
-
border
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
17
|
-
|
|
18
|
-
/* Prevent layout shift */
|
|
19
|
-
will-change: transform;
|
|
20
|
-
}
|
|
11
|
+
border: 1px solid #ececec;
|
|
12
|
+
border-radius: 16px 16px 16px 5px; /* matches assistant bubbles */
|
|
13
|
+
box-shadow: 0 1px 2px rgba(17, 24, 39, 0.04);
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
width: var(--typing-dot-size);
|
|
24
|
-
height: var(--typing-dot-size);
|
|
25
|
-
background-color: var(--typing-dot-color);
|
|
26
|
-
border-radius: 50%;
|
|
27
|
-
|
|
28
|
-
/* Animation definition */
|
|
29
|
-
animation: typingBounce 1.4s infinite ease-in-out both;
|
|
15
|
+
will-change: transform;
|
|
30
16
|
}
|
|
31
17
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
.typing-bar {
|
|
19
|
+
width: var(--typing-bar-w);
|
|
20
|
+
height: var(--typing-bar-h);
|
|
21
|
+
border-radius: 3px;
|
|
22
|
+
background: var(--typing-accent);
|
|
23
|
+
transform-origin: center;
|
|
24
|
+
animation: typingWave 1.1s ease-in-out infinite;
|
|
35
25
|
}
|
|
36
26
|
|
|
37
|
-
.typing-
|
|
38
|
-
animation-delay:
|
|
27
|
+
.typing-bar:nth-child(2) {
|
|
28
|
+
animation-delay: 0.18s;
|
|
39
29
|
}
|
|
40
30
|
|
|
41
|
-
.typing-
|
|
42
|
-
animation-delay:
|
|
31
|
+
.typing-bar:nth-child(3) {
|
|
32
|
+
animation-delay: 0.36s;
|
|
43
33
|
}
|
|
44
34
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
opacity: 0.5;
|
|
35
|
+
@keyframes typingWave {
|
|
36
|
+
0%, 100% {
|
|
37
|
+
transform: scaleY(0.35);
|
|
38
|
+
opacity: 0.55;
|
|
50
39
|
}
|
|
51
|
-
|
|
52
|
-
transform:
|
|
40
|
+
50% {
|
|
41
|
+
transform: scaleY(1);
|
|
53
42
|
opacity: 1;
|
|
54
43
|
}
|
|
55
|
-
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@media (prefers-reduced-motion: reduce) {
|
|
47
|
+
.typing-bar {
|
|
48
|
+
animation: none;
|
|
49
|
+
transform: scaleY(0.7);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -2,43 +2,44 @@ import React from 'react';
|
|
|
2
2
|
import './Typing.css';
|
|
3
3
|
|
|
4
4
|
interface TypingProps {
|
|
5
|
-
/**
|
|
5
|
+
/** Accent color of the animated bars */
|
|
6
6
|
dotColor?: string;
|
|
7
7
|
/** Color of the bubble background */
|
|
8
8
|
bubbleColor?: string;
|
|
9
|
-
/**
|
|
9
|
+
/** Base size in pixels (drives bar dimensions) */
|
|
10
10
|
size?: number;
|
|
11
11
|
/** Optional class name for the container */
|
|
12
12
|
className?: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
/** Typing indicator — mini equalizer bars, same motif as voice mode. */
|
|
16
|
+
const Typing: React.FC<TypingProps> = ({
|
|
17
|
+
dotColor = '#111827',
|
|
18
|
+
bubbleColor = '#ffffff',
|
|
19
|
+
size = 10,
|
|
20
|
+
className = '',
|
|
20
21
|
}) => {
|
|
21
|
-
// We pass dynamic values as CSS variables for better performance
|
|
22
22
|
const style = {
|
|
23
|
-
'--typing-
|
|
23
|
+
'--typing-accent': dotColor,
|
|
24
24
|
'--typing-bg': bubbleColor,
|
|
25
|
-
'--typing-
|
|
26
|
-
'--typing-
|
|
27
|
-
'--typing-
|
|
25
|
+
'--typing-bar-w': `${Math.max(3, Math.round(size * 0.4))}px`,
|
|
26
|
+
'--typing-bar-h': `${Math.round(size * 1.6)}px`,
|
|
27
|
+
'--typing-gap': `${Math.max(2, Math.round(size * 0.35))}px`,
|
|
28
|
+
'--typing-padding': `${size}px ${Math.round(size * 1.4)}px`,
|
|
28
29
|
} as React.CSSProperties;
|
|
29
30
|
|
|
30
31
|
return (
|
|
31
|
-
<div
|
|
32
|
-
className={`typing-container ${className}`}
|
|
32
|
+
<div
|
|
33
|
+
className={`typing-container ${className}`}
|
|
33
34
|
style={style}
|
|
34
35
|
role="status"
|
|
35
36
|
aria-label="Typing..."
|
|
36
37
|
>
|
|
37
|
-
<span className="typing-
|
|
38
|
-
<span className="typing-
|
|
39
|
-
<span className="typing-
|
|
38
|
+
<span className="typing-bar" />
|
|
39
|
+
<span className="typing-bar" />
|
|
40
|
+
<span className="typing-bar" />
|
|
40
41
|
</div>
|
|
41
42
|
);
|
|
42
43
|
};
|
|
43
44
|
|
|
44
|
-
export default Typing;
|
|
45
|
+
export default Typing;
|