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.
Files changed (44) hide show
  1. package/.env.development +2 -0
  2. package/dist/jaweb-chatbot.css +1 -1
  3. package/dist/jaweb-chatbot.iife.js +192 -599
  4. package/package.json +1 -1
  5. package/src/App.tsx +9 -0
  6. package/src/Chatbot/Chatbot.css +193 -113
  7. package/src/Chatbot/Chatbot.tsx +26 -19
  8. package/src/Chatbot/components/Cart/Index.tsx +10 -8
  9. package/src/Chatbot/components/ChatInput.tsx +11 -11
  10. package/src/Chatbot/components/Home/Home.css +456 -0
  11. package/src/Chatbot/components/Home/Home.tsx +293 -0
  12. package/src/Chatbot/components/Messages/Addcart.tsx +1 -1
  13. package/src/Chatbot/components/Messages/AssistantMesage.tsx +78 -37
  14. package/src/Chatbot/components/Messages/CardSwiper.tsx +1 -1
  15. package/src/Chatbot/components/Messages/Carousel/CardSwiper.tsx +87 -92
  16. package/src/Chatbot/components/Messages/Carousel/ProductSwiper.css +361 -0
  17. package/src/Chatbot/components/Messages/Carousel/TryOn.tsx +2 -2
  18. package/src/Chatbot/components/Messages/ChatMessages.css +32 -1
  19. package/src/Chatbot/components/Messages/ChatMessages.tsx +21 -38
  20. package/src/Chatbot/components/Messages/OrderCard.css +177 -0
  21. package/src/Chatbot/components/Messages/OrderCard.tsx +120 -0
  22. package/src/Chatbot/components/Messages/ProductCard.css +1 -1
  23. package/src/Chatbot/components/Messages/Support.tsx +311 -118
  24. package/src/Chatbot/components/Messages/Typing.css +31 -35
  25. package/src/Chatbot/components/Messages/Typing.tsx +19 -18
  26. package/src/Chatbot/components/Messenger/Messenger.tsx +67 -14
  27. package/src/Chatbot/components/Powered.css +26 -28
  28. package/src/Chatbot/components/Powered.tsx +1 -1
  29. package/src/Chatbot/components/Sessions/CreateSession.tsx +12 -10
  30. package/src/Chatbot/components/TryOn/View.tsx +2 -2
  31. package/src/Chatbot/components/VoiceMode.css +247 -0
  32. package/src/Chatbot/components/VoiceMode.tsx +613 -0
  33. package/src/hooks/config.tsx +9 -10
  34. package/src/hooks/useChatbotAPI.tsx +146 -61
  35. package/src/hooks/useChatbotData.tsx +8 -5
  36. package/src/hooks/useSessionMessages.tsx +0 -1
  37. package/src/hooks/useWebsocke.tsx +40 -18
  38. package/src/i18n.tsx +384 -10
  39. package/src/Chatbot/components/Messages/Carousel/CardSwiperSalla.tsx +0 -187
  40. package/src/Chatbot/components/Messages/Carousel/CardSwiperZid.tsx +0 -208
  41. package/src/Chatbot/components/Messages/Carousel/SallaSwiper.css +0 -245
  42. package/src/Chatbot/components/Messages/SallaAssistantMessage.tsx +0 -202
  43. package/src/Chatbot/components/Messages/ZidAssistantMesage.tsx +0 -203
  44. 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: "#00b3bc" }}
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 { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
5
- import { Button } from 'antd';
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: '#00b3bc' }}
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 = message.split('|||').map(p => p.trim()).filter(Boolean);
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
- }, [message]);
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: 'free',
139
- slides: { perView: 1, spacing: 16 },
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
- {Object.keys(groupedProducts).length > 1 && (
161
- <div
162
- style={{
163
- display: 'flex',
164
- margin: 0,
165
- marginBottom: '6px',
166
- justifyContent: 'space-between',
167
- paddingInline: '4px',
168
- }}
169
- >
170
- <Button
171
- shape="circle"
172
- icon={<ArrowLeftOutlined />}
173
- onClick={() => slider.current?.prev()}
174
- disabled={currentSlide === 0}
175
- />
176
- <Button
177
- shape="circle"
178
- icon={<ArrowRightOutlined />}
179
- onClick={() => slider.current?.next()}
180
- disabled={currentSlide === totalSlides - 1}
181
- />
182
- </div>
183
- )}
184
- <div ref={sliderRef} className="keen-slider">
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>
@@ -39,7 +39,7 @@ const ProductCarousel: FC<Props> = ({
39
39
  setCounts,
40
40
  handleAddToCart,
41
41
  handleRemoveFromCart,
42
- colorCode = '#7F28F8',
42
+ colorCode = '#111111',
43
43
  shortenName,
44
44
  sliderRef,
45
45
  totalSlides,