shopify-chatbot-widget 1.0.9 → 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/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
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
X,
|
|
4
|
+
MessageCircle,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
Ticket as TicketIcon,
|
|
7
|
+
Plus,
|
|
8
|
+
} from 'lucide-react';
|
|
9
|
+
import config from '../../../hooks/config';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
import i18nInstance, { setWidgetLang, WIDGET_LANGS } from '../../../i18n';
|
|
12
|
+
import type { WidgetLang } from '../../../i18n';
|
|
13
|
+
import './Home.css';
|
|
14
|
+
|
|
15
|
+
interface HomeTicket {
|
|
16
|
+
ticket_id: number;
|
|
17
|
+
status: string;
|
|
18
|
+
created_at: string;
|
|
19
|
+
session_id?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface HomeSession {
|
|
23
|
+
id: string;
|
|
24
|
+
agentName: string;
|
|
25
|
+
agentImage?: string;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
ended: boolean;
|
|
28
|
+
preview: string;
|
|
29
|
+
unread: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
chatbotLogo: string;
|
|
34
|
+
chatbotName: string;
|
|
35
|
+
companyName?: string;
|
|
36
|
+
colorCode?: string;
|
|
37
|
+
visitorId: string;
|
|
38
|
+
merchantPhone?: string;
|
|
39
|
+
onClose: () => void;
|
|
40
|
+
onNewSession: () => void;
|
|
41
|
+
onSessionSelect: (sessionId: string, agentName: string, agentImage: string) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const timeAgo = (iso?: string): string => {
|
|
45
|
+
if (!iso) return '';
|
|
46
|
+
const mins = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 60000));
|
|
47
|
+
if (mins < 1) return i18nInstance.t('time.justNow');
|
|
48
|
+
if (mins < 60) return i18nInstance.t('time.m', { n: mins });
|
|
49
|
+
const hours = Math.round(mins / 60);
|
|
50
|
+
if (hours < 24) return i18nInstance.t('time.h', { n: hours });
|
|
51
|
+
return i18nInstance.t('time.d', { n: Math.round(hours / 24) });
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const WhatsAppGlyph: React.FC<{ size?: number }> = ({ size = 16 }) => (
|
|
55
|
+
<svg viewBox="0 0 24 24" width={size} height={size} fill="currentColor" aria-hidden>
|
|
56
|
+
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" />
|
|
57
|
+
</svg>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const Home: React.FC<Props> = ({
|
|
61
|
+
chatbotLogo,
|
|
62
|
+
chatbotName,
|
|
63
|
+
companyName,
|
|
64
|
+
colorCode = '#111111',
|
|
65
|
+
visitorId,
|
|
66
|
+
merchantPhone,
|
|
67
|
+
onClose,
|
|
68
|
+
onNewSession,
|
|
69
|
+
onSessionSelect,
|
|
70
|
+
}) => {
|
|
71
|
+
const { t, i18n } = useTranslation();
|
|
72
|
+
const [tickets, setTickets] = useState<HomeTicket[]>([]);
|
|
73
|
+
const [sessions, setSessions] = useState<HomeSession[]>([]);
|
|
74
|
+
const [loaded, setLoaded] = useState(false);
|
|
75
|
+
const [showAll, setShowAll] = useState(false);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!visitorId) return;
|
|
79
|
+
const company = localStorage.getItem('company_username');
|
|
80
|
+
|
|
81
|
+
void (async () => {
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(
|
|
84
|
+
`${config.apiUrl}get-chatlogs-by-ip/?visitor_id=${visitorId}&company_username=${company}`
|
|
85
|
+
);
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
const mapped: HomeSession[] = (Array.isArray(data) ? data : []).map((item: any) => {
|
|
88
|
+
const created = new Date(item.date);
|
|
89
|
+
const days = (Date.now() - created.getTime()) / 86400000;
|
|
90
|
+
return {
|
|
91
|
+
id: item.user_session_id,
|
|
92
|
+
agentName: item.assignee || 'AI Agent',
|
|
93
|
+
agentImage: item.assignee_picture,
|
|
94
|
+
createdAt: item.date,
|
|
95
|
+
ended: !!item.closed || days >= 5,
|
|
96
|
+
preview: item.preview || '',
|
|
97
|
+
unread: Number(item.visitor_unread) || 0,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
setSessions(mapped);
|
|
101
|
+
} catch {
|
|
102
|
+
/* ignore */
|
|
103
|
+
} finally {
|
|
104
|
+
setLoaded(true);
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
|
|
108
|
+
void (async () => {
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(
|
|
111
|
+
`${config.apiUrl}visitor-tickets/?visitor_id=${visitorId}&company_username=${company}`
|
|
112
|
+
);
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
setTickets(data.tickets || []);
|
|
115
|
+
} catch {
|
|
116
|
+
/* home stays useful without tickets */
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
}, [visitorId]);
|
|
120
|
+
|
|
121
|
+
// Unread replies first, then active, then ended; a handful unless expanded
|
|
122
|
+
const ordered = [
|
|
123
|
+
...sessions.filter((s) => !s.ended && s.unread > 0),
|
|
124
|
+
...sessions.filter((s) => !s.ended && s.unread === 0),
|
|
125
|
+
...sessions.filter((s) => s.ended),
|
|
126
|
+
];
|
|
127
|
+
const visibleSessions = showAll ? ordered : ordered.slice(0, 3);
|
|
128
|
+
const hiddenCount = ordered.length - 3;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="jwbH" style={{ '--jwbH-accent': colorCode } as React.CSSProperties}>
|
|
132
|
+
{/* Hero */}
|
|
133
|
+
<div className="jwbH-hero">
|
|
134
|
+
<div className="jwbH-heroGlow" />
|
|
135
|
+
<div className="jwbH-heroTop">
|
|
136
|
+
<span className="jwbH-logoWrap">
|
|
137
|
+
{chatbotLogo ? (
|
|
138
|
+
<img src={chatbotLogo} alt="" className="jwbH-logo" />
|
|
139
|
+
) : (
|
|
140
|
+
<span className="jwbH-logoFallback">
|
|
141
|
+
{(companyName || chatbotName || 'J').slice(0, 1).toUpperCase()}
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
<span className="jwbH-logoDot" aria-label={t("home.online")} />
|
|
145
|
+
</span>
|
|
146
|
+
|
|
147
|
+
<span className="jwbH-heroCtrls">
|
|
148
|
+
<span className="jwbH-lang" role="group" aria-label={t('home.language')}>
|
|
149
|
+
{WIDGET_LANGS.map((lng) => (
|
|
150
|
+
<button
|
|
151
|
+
key={lng}
|
|
152
|
+
type="button"
|
|
153
|
+
className={`jwbH-langBtn ${i18n.language === lng ? 'jwbH-langBtn--active' : ''}`}
|
|
154
|
+
onClick={() => setWidgetLang(lng as WidgetLang)}
|
|
155
|
+
>
|
|
156
|
+
{lng === 'ar' ? 'ع' : lng.toUpperCase()}
|
|
157
|
+
</button>
|
|
158
|
+
))}
|
|
159
|
+
</span>
|
|
160
|
+
<button className="jwbH-close" onClick={onClose} aria-label={t("home.closeChat")}>
|
|
161
|
+
<X size={17} />
|
|
162
|
+
</button>
|
|
163
|
+
</span>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<h2 className="jwbH-greeting">
|
|
167
|
+
{t('home.greeting')}<span className="jwbH-greetAccent">.</span>
|
|
168
|
+
</h2>
|
|
169
|
+
<p className="jwbH-sub">
|
|
170
|
+
{companyName ? t('home.subCompany', { company: companyName }) : ''}
|
|
171
|
+
{t('home.sub')}
|
|
172
|
+
</p>
|
|
173
|
+
|
|
174
|
+
{/* Channel choice */}
|
|
175
|
+
<div className="jwbH-channels">
|
|
176
|
+
<button className="jwbH-channel jwbH-channel--primary" onClick={onNewSession}>
|
|
177
|
+
<MessageCircle size={15} />
|
|
178
|
+
{t('home.startChat')}
|
|
179
|
+
</button>
|
|
180
|
+
{merchantPhone && (
|
|
181
|
+
<a
|
|
182
|
+
className="jwbH-channel jwbH-channel--whatsapp"
|
|
183
|
+
href={`https://wa.me/${merchantPhone.replace(/[^\d]/g, '')}`}
|
|
184
|
+
target="_blank"
|
|
185
|
+
rel="noopener noreferrer"
|
|
186
|
+
>
|
|
187
|
+
<WhatsAppGlyph size={15} />
|
|
188
|
+
{t('home.whatsapp')}
|
|
189
|
+
</a>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="jwbH-body">
|
|
195
|
+
{/* Conversations */}
|
|
196
|
+
{loaded && sessions.length > 0 && (
|
|
197
|
+
<section className="jwbH-section">
|
|
198
|
+
<div className="jwbH-sectionHead">
|
|
199
|
+
<h3>{t('home.conversations')}</h3>
|
|
200
|
+
<button onClick={onNewSession} className="jwbH-newBtn" aria-label={t("home.newChat")}>
|
|
201
|
+
<Plus size={13} /> {t('home.new')}
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div className={`jwbH-rows ${showAll ? 'jwbH-rows--expanded' : ''}`}>
|
|
206
|
+
{visibleSessions.map((s) =>
|
|
207
|
+
s.ended ? (
|
|
208
|
+
<div key={s.id} className="jwbH-row jwbH-row--ended">
|
|
209
|
+
<span className="jwbH-rowIcon">
|
|
210
|
+
<MessageCircle size={14} />
|
|
211
|
+
</span>
|
|
212
|
+
<span className="jwbH-rowText">
|
|
213
|
+
<span className="jwbH-rowTop">
|
|
214
|
+
<span className="jwbH-rowTitle">{s.agentName}</span>
|
|
215
|
+
<span className="jwbH-rowTime">{timeAgo(s.createdAt)}</span>
|
|
216
|
+
</span>
|
|
217
|
+
<span className="jwbH-rowSub">{s.preview || t('home.conversationEnded')}</span>
|
|
218
|
+
</span>
|
|
219
|
+
</div>
|
|
220
|
+
) : (
|
|
221
|
+
<button
|
|
222
|
+
key={s.id}
|
|
223
|
+
className="jwbH-row"
|
|
224
|
+
onClick={() => onSessionSelect(s.id, s.agentName, s.agentImage || '')}
|
|
225
|
+
>
|
|
226
|
+
{s.agentImage ? (
|
|
227
|
+
<img src={s.agentImage} alt="" className="jwbH-rowAvatar" />
|
|
228
|
+
) : (
|
|
229
|
+
<span className="jwbH-rowIcon">
|
|
230
|
+
<MessageCircle size={14} />
|
|
231
|
+
</span>
|
|
232
|
+
)}
|
|
233
|
+
<span className="jwbH-rowText">
|
|
234
|
+
<span className="jwbH-rowTop">
|
|
235
|
+
<span className="jwbH-rowTitle">{s.agentName}</span>
|
|
236
|
+
<span className="jwbH-rowTime">{timeAgo(s.createdAt)}</span>
|
|
237
|
+
</span>
|
|
238
|
+
<span className={`jwbH-rowSub ${s.unread > 0 ? 'jwbH-rowSub--unread' : ''}`}>
|
|
239
|
+
{s.unread > 0 ? t('home.newReplies', { count: s.unread }) : s.preview || t('home.tapToContinue')}
|
|
240
|
+
</span>
|
|
241
|
+
</span>
|
|
242
|
+
{s.unread > 0 ? (
|
|
243
|
+
<span className="jwbH-unread">{s.unread}</span>
|
|
244
|
+
) : (
|
|
245
|
+
<ChevronRight size={14} className="jwbH-rowArrow" />
|
|
246
|
+
)}
|
|
247
|
+
</button>
|
|
248
|
+
)
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{hiddenCount > 0 && (
|
|
253
|
+
<button className="jwbH-more" onClick={() => setShowAll((v) => !v)}>
|
|
254
|
+
{showAll ? t('home.showLess') : t('home.showMore', { count: hiddenCount })}
|
|
255
|
+
</button>
|
|
256
|
+
)}
|
|
257
|
+
</section>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
{/* Tickets */}
|
|
261
|
+
{tickets.length > 0 && (
|
|
262
|
+
<section className="jwbH-section">
|
|
263
|
+
<div className="jwbH-sectionHead">
|
|
264
|
+
<h3>{t('home.yourTickets')}</h3>
|
|
265
|
+
</div>
|
|
266
|
+
<div className="jwbH-rows jwbH-rows--tickets">
|
|
267
|
+
{tickets.map((tk) => (
|
|
268
|
+
<div key={tk.ticket_id} className="jwbH-row jwbH-row--static">
|
|
269
|
+
<span className="jwbH-rowIcon">
|
|
270
|
+
<TicketIcon size={14} />
|
|
271
|
+
</span>
|
|
272
|
+
<span className="jwbH-rowText">
|
|
273
|
+
<span className="jwbH-rowTitle">{t('home.ticketN', { id: tk.ticket_id })}</span>
|
|
274
|
+
<span className="jwbH-rowSub">{timeAgo(tk.created_at)}</span>
|
|
275
|
+
</span>
|
|
276
|
+
<span
|
|
277
|
+
className={`jwbH-status ${
|
|
278
|
+
tk.status === 'Solved' ? 'jwbH-status--solved' : ''
|
|
279
|
+
}`}
|
|
280
|
+
>
|
|
281
|
+
{t(`status.${tk.status}`, tk.status)}
|
|
282
|
+
</span>
|
|
283
|
+
</div>
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
</section>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export default Home;
|
|
@@ -14,7 +14,7 @@ const AddToCartConfirmation: React.FC<AddToCartConfirmationProps> = ({ message }
|
|
|
14
14
|
<a
|
|
15
15
|
key={i}
|
|
16
16
|
href={part.trim()}
|
|
17
|
-
style={{ color: "#
|
|
17
|
+
style={{ color: "#111827" }}
|
|
18
18
|
target="_blank"
|
|
19
19
|
rel="noopener noreferrer"
|
|
20
20
|
className="underline break-words"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React, { useState, useMemo, type FC } from 'react';
|
|
2
2
|
import './ProductCard.css';
|
|
3
3
|
import ProductCarousel from './Carousel/CardSwiper';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import OrderCard, { type OrderStatusData } from './OrderCard';
|
|
5
|
+
import { TicketCard, type SupportTicket } from './Support';
|
|
6
6
|
import { useKeenSlider } from 'keen-slider/react';
|
|
7
7
|
import 'keen-slider/keen-slider.min.css';
|
|
8
8
|
|
|
@@ -48,7 +48,7 @@ const AssistantMessage: FC<AssistantMessageProps> = ({
|
|
|
48
48
|
<a
|
|
49
49
|
key={i}
|
|
50
50
|
href={part.trim()}
|
|
51
|
-
style={{ color: '#
|
|
51
|
+
style={{ color: '#111827' }}
|
|
52
52
|
target="_blank"
|
|
53
53
|
rel="noopener noreferrer"
|
|
54
54
|
className="underline break-words"
|
|
@@ -86,8 +86,42 @@ const AssistantMessage: FC<AssistantMessageProps> = ({
|
|
|
86
86
|
};
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
// Structured blocks render as visual cards; the text fallback that follows
|
|
90
|
+
// them in the same message is suppressed (it's a duplicate).
|
|
91
|
+
const { orderCards, ticketCards, messageBody } = useMemo(() => {
|
|
92
|
+
const cards: OrderStatusData[] = [];
|
|
93
|
+
const tickets: SupportTicket[] = [];
|
|
94
|
+
let body = message;
|
|
95
|
+
|
|
96
|
+
const orderRe = /\[\[ORDER_STATUS\]\](.*?)\[\[\/ORDER_STATUS\]\]/gs;
|
|
97
|
+
let match;
|
|
98
|
+
while ((match = orderRe.exec(message)) !== null) {
|
|
99
|
+
try {
|
|
100
|
+
cards.push(JSON.parse(match[1]));
|
|
101
|
+
} catch {
|
|
102
|
+
/* malformed block — leave the text fallback visible */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (cards.length > 0) {
|
|
106
|
+
body = body.replace(orderRe, '').trim();
|
|
107
|
+
if (body.startsWith('📋')) body = '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ticketRe = /\[\[SUPPORT_TICKET\]\](.*?)\[\[\/SUPPORT_TICKET\]\]/gs;
|
|
111
|
+
while ((match = ticketRe.exec(message)) !== null) {
|
|
112
|
+
try {
|
|
113
|
+
tickets.push(JSON.parse(match[1]));
|
|
114
|
+
} catch {
|
|
115
|
+
/* malformed block */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
body = body.replace(ticketRe, '').trim();
|
|
119
|
+
|
|
120
|
+
return { orderCards: cards, ticketCards: tickets, messageBody: body };
|
|
121
|
+
}, [message]);
|
|
122
|
+
|
|
89
123
|
const parsedSegments = useMemo(() => {
|
|
90
|
-
const parts =
|
|
124
|
+
const parts = messageBody.split('|||').map(p => p.trim()).filter(Boolean);
|
|
91
125
|
const segments: { type: 'text' | 'product'; data: any }[] = [];
|
|
92
126
|
let lastProduct: ProductVariant | null = null;
|
|
93
127
|
|
|
@@ -112,7 +146,7 @@ const AssistantMessage: FC<AssistantMessageProps> = ({
|
|
|
112
146
|
});
|
|
113
147
|
|
|
114
148
|
return segments;
|
|
115
|
-
}, [
|
|
149
|
+
}, [messageBody]);
|
|
116
150
|
|
|
117
151
|
const groupedProducts = useMemo(() => {
|
|
118
152
|
const groups: Record<string, ProductVariant[]> = {};
|
|
@@ -132,11 +166,12 @@ const AssistantMessage: FC<AssistantMessageProps> = ({
|
|
|
132
166
|
|
|
133
167
|
|
|
134
168
|
const [currentSlide, setCurrentSlide] = useState(0);
|
|
135
|
-
const totalSlides =Object.keys(groupedProducts).length
|
|
169
|
+
const totalSlides = Object.keys(groupedProducts).length;
|
|
136
170
|
const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({
|
|
137
171
|
loop: false,
|
|
138
|
-
mode: '
|
|
139
|
-
|
|
172
|
+
mode: 'snap',
|
|
173
|
+
// peek the next card so visitors know there's more to swipe
|
|
174
|
+
slides: { perView: totalSlides > 1 ? 1.12 : 1, spacing: 10 },
|
|
140
175
|
slideChanged(s) {
|
|
141
176
|
setCurrentSlide(s.track.details.rel);
|
|
142
177
|
},
|
|
@@ -153,35 +188,45 @@ const AssistantMessage: FC<AssistantMessageProps> = ({
|
|
|
153
188
|
</p>
|
|
154
189
|
))}
|
|
155
190
|
|
|
191
|
+
{orderCards.map((card, i) => (
|
|
192
|
+
<OrderCard key={`order-${i}`} data={card} colorCode={colorCode} />
|
|
193
|
+
))}
|
|
194
|
+
|
|
195
|
+
{ticketCards.map((ticket, i) => (
|
|
196
|
+
<TicketCard key={`ticket-${i}`} ticket={ticket} colorCode={colorCode} />
|
|
197
|
+
))}
|
|
198
|
+
|
|
156
199
|
|
|
157
200
|
|
|
158
201
|
{Object.keys(groupedProducts).length > 0 && (
|
|
159
202
|
<>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
203
|
+
{totalSlides > 1 && (
|
|
204
|
+
<div className="jwbX-nav">
|
|
205
|
+
<span className="jwbX-navCount">
|
|
206
|
+
{currentSlide + 1} / {totalSlides}
|
|
207
|
+
</span>
|
|
208
|
+
<div className="jwbX-navBtns">
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
className="jwbX-navBtn"
|
|
212
|
+
onClick={() => slider.current?.prev()}
|
|
213
|
+
disabled={currentSlide === 0}
|
|
214
|
+
aria-label="Previous product"
|
|
215
|
+
>
|
|
216
|
+
‹
|
|
217
|
+
</button>
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
className="jwbX-navBtn"
|
|
221
|
+
onClick={() => slider.current?.next()}
|
|
222
|
+
disabled={currentSlide === totalSlides - 1}
|
|
223
|
+
aria-label="Next product"
|
|
224
|
+
>
|
|
225
|
+
›
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
185
230
|
<ProductCarousel
|
|
186
231
|
groupedProducts={groupedProducts}
|
|
187
232
|
selectedVariants={selectedVariants}
|
|
@@ -199,11 +244,7 @@ const AssistantMessage: FC<AssistantMessageProps> = ({
|
|
|
199
244
|
disableCheckout={disableCheckout}
|
|
200
245
|
onTryOnSelect={onTryOnSelect}
|
|
201
246
|
enableTry={enableTry}
|
|
202
|
-
// hides the – + controls
|
|
203
|
-
|
|
204
|
-
|
|
205
247
|
/>
|
|
206
|
-
</div>
|
|
207
248
|
</>
|
|
208
249
|
)}
|
|
209
250
|
</div>
|