shopify-chatbot-widget 1.0.4 → 1.0.5

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shopify-chatbot-widget",
3
3
  "private": false,
4
- "version": "1.0.4",
4
+ "version": "1.0.5",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useState, useEffect,useCallback } from 'react';
2
2
  import { useChatbotData } from '../hooks/useChatbotData';
3
3
  import ChatSessions from './components/Sessions/RenderList';
4
4
  import CreateSession from './components/Sessions/CreateSession';
@@ -10,15 +10,18 @@ import { LoadingOutlined } from '@ant-design/icons';
10
10
  import { Spin } from 'antd';
11
11
  import { motion, AnimatePresence } from 'framer-motion';
12
12
  import CartLayout from './components/Cart/Index';
13
-
13
+ import TryOnLayout from './components/TryOn/View';
14
+ import { useCart } from '../hooks/useCart';
14
15
  const Chatbot: React.FC = () => {
15
- const [open, setOpen] = useState(false);
16
- const [viewMode, setViewMode] = useState<'sessions' | 'create' | 'messenger' | 'cartView' >('sessions');
16
+ const [open, setOpen] = useState(true);
17
+ const [viewMode, setViewMode] = useState<'sessions' | 'create' | 'messenger' | 'cartView' | 'tryOn'>('sessions');
17
18
  const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
18
19
  const [HasInfo, setHasInfo] = useState(false);
19
20
  const [activeAgentName, setActiveAgentName] = useState('');
20
21
  const [activeAgentImage, setActiveAgentImage] = useState('');
21
22
  const [newSession, setNewSession] = useState(false);
23
+ const [tryOnProduct, setTryOnProduct] = useState<any>(null);
24
+ const [counts, setCounts] = useState<Record<string, number>>({});
22
25
 
23
26
 
24
27
  const {
@@ -32,7 +35,8 @@ const Chatbot: React.FC = () => {
32
35
  calendly,
33
36
  disbleCheckout,
34
37
  chatbotName,
35
- setMessagesSuggestions
38
+ setMessagesSuggestions,
39
+ enableTryOn
36
40
  } = useChatbotData();
37
41
 
38
42
  const [visitorData, setVisitorData] = useState<{ visitorId: string; country: string } | null>(null);
@@ -141,6 +145,30 @@ const Chatbot: React.FC = () => {
141
145
  }
142
146
  }, [isFetchingDetails]);
143
147
 
148
+
149
+
150
+
151
+ const { addToCart, removeFromCart } = useCart(activeSessionId ?? "");
152
+
153
+ const handleToCart = useCallback(
154
+ async (variantId: string, name: string, variantImage: string,price: string ,qty = 1) => {
155
+ try {
156
+ await addToCart(variantId, name, qty, variantImage,price);
157
+
158
+ } catch (err: any) {
159
+ }
160
+ },
161
+ [addToCart]
162
+ );
163
+
164
+
165
+ const handleRemoveFromCart = useCallback(
166
+ async (variantId: string, qty = 1) => {
167
+ await removeFromCart(variantId, qty);
168
+ },
169
+ [removeFromCart]
170
+ );
171
+
144
172
  return (
145
173
  <div>
146
174
  {open && (
@@ -222,6 +250,29 @@ const Chatbot: React.FC = () => {
222
250
 
223
251
  </motion.div>
224
252
  )}
253
+
254
+ {viewMode === 'tryOn' && (
255
+ <motion.div
256
+ key="tryOn"
257
+ initial={{ opacity: 0, scale: 0.9 }}
258
+ animate={{ opacity: 1, scale: 1 }}
259
+ exit={{ opacity: 0, scale: 0.9 }}
260
+ transition={{ duration: 0.3 }}
261
+ style={{ height: '100%' }}
262
+ >
263
+ <TryOnLayout
264
+ product={tryOnProduct} // <--- PASS DATA HERE
265
+ colorCode={colorCode}
266
+ setOpen={(v: boolean) => (v ? requestOpen() : requestClose())}
267
+ onBack={() => setViewMode("messenger")}
268
+ handleAddToCart={handleToCart}
269
+ handleRemoveFromCart={handleRemoveFromCart}
270
+ counts={counts}
271
+ setCounts={setCounts}
272
+ />
273
+ </motion.div>
274
+ )}
275
+
225
276
 
226
277
 
227
278
  {viewMode === 'messenger' && activeSessionId && (
@@ -246,9 +297,15 @@ const Chatbot: React.FC = () => {
246
297
  setNewSession={setNewSession}
247
298
  Opened={open}
248
299
  setView={setViewMode}
300
+ onTryOnSelect={(product: any) => {
301
+ setTryOnProduct(product);
302
+ setViewMode('tryOn');
303
+
304
+ }}
249
305
  userType={userType}
250
306
  disbleCheckout={disbleCheckout}
251
307
  setMessagesSuggestions={setMessagesSuggestions}
308
+ enableTry={enableTryOn}
252
309
  />
253
310
  </motion.div>
254
311
  )}
@@ -24,6 +24,8 @@ interface AssistantMessageProps {
24
24
  disableCheckout?:boolean;
25
25
  handleAddToCart: (variantId: string, name: string,variantImage:string, price:string,qty?: number) => Promise<void>;
26
26
  handleRemoveFromCart: (variantId: string, qty?: number) => Promise<void>;
27
+ onTryOnSelect: (product: any) => void; // Add this
28
+ enableTry:boolean;
27
29
  }
28
30
 
29
31
  const AssistantMessage: FC<AssistantMessageProps> = ({
@@ -31,7 +33,9 @@ const AssistantMessage: FC<AssistantMessageProps> = ({
31
33
  colorCode,
32
34
  disableCheckout,
33
35
  handleAddToCart,
34
- handleRemoveFromCart
36
+ handleRemoveFromCart,
37
+ onTryOnSelect,
38
+ enableTry
35
39
 
36
40
  }) => {
37
41
  const [counts, setCounts] = useState<Record<string, number>>({});
@@ -192,7 +196,10 @@ const AssistantMessage: FC<AssistantMessageProps> = ({
192
196
  currentSlide={currentSlide}
193
197
  totalSlides={totalSlides}
194
198
  sliderRef={sliderRef}
195
- disableCheckout={disableCheckout} // hides the – + controls
199
+ disableCheckout={disableCheckout}
200
+ onTryOnSelect={onTryOnSelect}
201
+ enableTry={enableTry}
202
+ // hides the – + controls
196
203
 
197
204
 
198
205
  />
@@ -1,10 +1,9 @@
1
- import React, { useState } from "react";
1
+ import React from "react";
2
2
  import type { FC } from "react";
3
3
  import "keen-slider/keen-slider.min.css";
4
4
  import type { KeenSliderInstance } from "keen-slider/react";
5
5
  import "./SallaSwiper.css";
6
6
  import { Button } from "antd";
7
- import TryOnBox from "./TryOn";
8
7
 
9
8
  interface ProductVariant {
10
9
  variant_id: string;
@@ -34,6 +33,8 @@ interface Props {
34
33
  sliderInstance: React.MutableRefObject<KeenSliderInstance | null>;
35
34
  currentSlide: number;
36
35
  totalSlides: number;
36
+ onTryOnSelect: (product: ProductVariant) => void;
37
+ enableTry:boolean;
37
38
  }
38
39
 
39
40
  const ProductCarousel: FC<Props> = ({
@@ -49,11 +50,13 @@ const ProductCarousel: FC<Props> = ({
49
50
  sliderRef,
50
51
  totalSlides,
51
52
  disableCheckout = false,
53
+ onTryOnSelect,
54
+ enableTry
55
+
52
56
  }) => {
53
57
  if (!totalSlides || totalSlides === 0) return null;
54
58
 
55
59
  // track which variant is open for Try On
56
- const [openTryOn, setOpenTryOn] = useState<string | null>(null);
57
60
 
58
61
  return (
59
62
  <section
@@ -125,7 +128,6 @@ const ProductCarousel: FC<Props> = ({
125
128
  await handleRemoveFromCart(currentVariant.variant_id, 1);
126
129
  };
127
130
 
128
- const isTryOnOpen = openTryOn === currentVariant.variant_id;
129
131
 
130
132
  return (
131
133
  <article
@@ -227,24 +229,24 @@ const ProductCarousel: FC<Props> = ({
227
229
 
228
230
  {/* Try On toggle button (kept) */}
229
231
 
232
+ {enableTry?
230
233
  <Button
231
- style={{ backgroundColor: colorCode, color: "white" }}
232
- className="px-6 py-2 rounded-lg shadow-md transition"
233
- onClick={() =>
234
- setOpenTryOn(isTryOnOpen ? null : currentVariant.variant_id)
235
- }
236
- >
237
- {isTryOnOpen ? "Close Try On" : "Try On"}
238
- </Button>
234
+ style={{ backgroundColor: colorCode, color: "white" }}
235
+ className="px-6 py-2 rounded-lg shadow-md transition"
236
+ onClick={() => {
237
+ onTryOnSelect(currentVariant);
238
+ }}
239
+ >
240
+ Try On
241
+ </Button>
242
+ :<></>
243
+ }
244
+
239
245
 
240
246
  </div>
241
247
 
242
248
  {/* Try On panel */}
243
- { isTryOnOpen && (
244
- <div className="mt-4">
245
- <TryOnBox productID={currentVariant.variant_id} productImage={currentVariant.image} />
246
- </div>
247
- )}
249
+
248
250
  </div>
249
251
  </article>
250
252
  );
@@ -29,6 +29,7 @@ interface Message {
29
29
  isBookings?: boolean;
30
30
  }
31
31
 
32
+
32
33
  interface ChatMessagesProps {
33
34
  messages: Message[];
34
35
  colorCode: string;
@@ -53,9 +54,11 @@ interface ChatMessagesProps {
53
54
  calendly: string;
54
55
  disableCheckout: boolean;
55
56
  isfetchingMessage: boolean;
56
-
57
57
  messagesSuggestions: string[];
58
58
  sendMessageSuggestion: (message: string) => Promise<void>;
59
+ onTryOnSelect: (product: any) => void; // Add this
60
+ enableTry:boolean;
61
+
59
62
  }
60
63
 
61
64
  const InlineSuggestions: React.FC<{
@@ -120,6 +123,8 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
120
123
  isfetchingMessage,
121
124
  messagesSuggestions,
122
125
  sendMessageSuggestion,
126
+ onTryOnSelect,
127
+ enableTry
123
128
  }) => {
124
129
  const bottomRef = useRef<HTMLDivElement>(null);
125
130
 
@@ -173,7 +178,11 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
173
178
  if (msg.typing) {
174
179
  return (
175
180
  <div key={i} className="chat-msg typing">
176
- <Typing color={colorCode} />
181
+ <Typing
182
+ dotColor={colorCode}
183
+ bubbleColor="#FFFF"
184
+ size={10}
185
+ />
177
186
  </div>
178
187
  );
179
188
  }
@@ -252,6 +261,8 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
252
261
  disableCheckout={disableCheckout}
253
262
  handleAddToCart={handleAddToCart}
254
263
  handleRemoveFromCart={handleRemoveFromCart}
264
+ onTryOnSelect={onTryOnSelect}
265
+ enableTry={enableTry}
255
266
  />
256
267
  ) : userType === "salla" ? (
257
268
  <SallaAsssiatnt
@@ -259,13 +270,17 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
259
270
  colorCode={colorCode}
260
271
  handleAddToCart={handleAddToCart}
261
272
  handleRemoveFromCart={handleRemoveFromCart}
262
- />
273
+ // enableTry={enableTry}
274
+
275
+ />
263
276
  ) : (
264
277
  <ZidAssistantChatMessage
265
278
  message={msg.content || msg.text || ""}
266
279
  colorCode={colorCode}
267
280
  handleAddToCart={handleAddToCart}
268
281
  handleRemoveFromCart={handleRemoveFromCart}
282
+ // enableTry={enableTry}
283
+
269
284
  />
270
285
  )}
271
286
  </div>
@@ -22,6 +22,7 @@ interface AssistantMessageProps {
22
22
  isBusiness?: boolean;
23
23
  handleAddToCart: (variantId: string, name: string,variantImage:string,price:string, qty?: number) => Promise<void>;
24
24
  handleRemoveFromCart: (variantId: string, qty?: number) => Promise<void>;
25
+
25
26
  }
26
27
 
27
28
  const SallaAsssiatnt: FC<AssistantMessageProps> = ({
@@ -1,134 +1,55 @@
1
- /* ===== BASE TYPING INDICATOR ===== */
2
- .typing-indicator {
1
+ /* Typing.css */
2
+
3
+ .typing-container {
4
+ /* Layout */
3
5
  display: inline-flex;
4
- gap: 6px;
5
6
  align-items: center;
6
- justify-content: flex-start;
7
- padding: 8px 12px;
8
- background: #eff0f1;
9
- border-color: black;
10
- border-radius: 18px;
11
- width: fit-content;
7
+ justify-content: center;
8
+ gap: var(--typing-gap);
9
+
10
+ /* Appearance */
11
+ background-color: var(--typing-bg);
12
+ padding: var(--typing-padding);
13
+ border-radius: 9999px; /* Pill shape */
14
+
15
+ /* Optional: Add a subtle shadow for depth */
16
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
17
+
18
+ /* Prevent layout shift */
19
+ will-change: transform;
12
20
  }
13
21
 
14
22
  .typing-dot {
23
+ width: var(--typing-dot-size);
24
+ height: var(--typing-dot-size);
25
+ background-color: var(--typing-dot-color);
15
26
  border-radius: 50%;
16
- display: inline-block;
17
- }
18
-
19
- /* ===== DOTS VARIANT (Default - Smooth bounce) ===== */
20
- .typing-dots .typing-dot {
21
- animation: bounceDot 1.4s infinite ease-in-out both;
27
+
28
+ /* Animation definition */
29
+ animation: typingBounce 1.4s infinite ease-in-out both;
22
30
  }
23
31
 
24
- .typing-dots .typing-dot:nth-child(1) {
32
+ /* Stagger the animations */
33
+ .typing-dot:nth-child(1) {
25
34
  animation-delay: -0.32s;
26
35
  }
27
36
 
28
- .typing-dots .typing-dot:nth-child(2) {
37
+ .typing-dot:nth-child(2) {
29
38
  animation-delay: -0.16s;
30
39
  }
31
40
 
32
- @keyframes bounceDot {
33
- 0%, 80%, 100% {
34
- transform: translateY(0) scale(1);
35
- opacity: 0.7;
36
- }
37
- 40% {
38
- transform: translateY(-10px) scale(1.1);
39
- opacity: 1;
40
- }
41
- }
42
-
43
- /* ===== BOXES VARIANT (Pulsing squares) ===== */
44
- .typing-boxes .typing-dot {
45
- border-radius: 3px;
46
- animation: pulseBox 1s infinite ease-in-out both;
47
- }
48
-
49
- .typing-boxes .typing-dot:nth-child(1) {
41
+ .typing-dot:nth-child(3) {
50
42
  animation-delay: 0s;
51
43
  }
52
44
 
53
- .typing-boxes .typing-dot:nth-child(2) {
54
- animation-delay: 0.2s;
55
- }
56
-
57
- .typing-boxes .typing-dot:nth-child(3) {
58
- animation-delay: 0.4s;
59
- }
60
-
61
- @keyframes pulseBox {
62
- 0%, 100% {
45
+ /* The Animation Keyframes */
46
+ @keyframes typingBounce {
47
+ 0%, 80%, 100% {
63
48
  transform: scale(0.8);
64
49
  opacity: 0.5;
65
50
  }
66
- 50% {
67
- transform: scale(1.2);
68
- opacity: 1;
69
- }
70
- }
71
-
72
- /* ===== WAVE VARIANT (Wave motion) ===== */
73
- .typing-wave .typing-dot {
74
- animation: wave 1.2s infinite ease-in-out;
75
- }
76
-
77
- .typing-wave .typing-dot:nth-child(1) {
78
- animation-delay: 0s;
79
- }
80
-
81
- .typing-wave .typing-dot:nth-child(2) {
82
- animation-delay: 0.15s;
83
- }
84
-
85
- .typing-wave .typing-dot:nth-child(3) {
86
- animation-delay: 0.3s;
87
- }
88
-
89
- @keyframes wave {
90
- 0%, 60%, 100% {
91
- transform: translateY(0);
92
- opacity: 0.6;
93
- }
94
- 30% {
95
- transform: translateY(-12px);
51
+ 40% {
52
+ transform: scale(1) translateY(-4px); /* Slight lift */
96
53
  opacity: 1;
97
54
  }
98
- }
99
-
100
- /* ===== MINIMAL VARIANT (No background bubble) ===== */
101
- .typing-indicator.minimal {
102
- background: transparent;
103
- padding: 0;
104
- }
105
-
106
- /* ===== DARK MODE SUPPORT ===== */
107
- .typing-indicator.dark {
108
- background: #374151;
109
- }
110
-
111
- /* ===== SIZE VARIANTS ===== */
112
- .typing-indicator.small {
113
- padding: 6px 10px;
114
- gap: 4px;
115
- }
116
-
117
- .typing-indicator.large {
118
- padding: 10px 16px;
119
- gap: 8px;
120
- }
121
-
122
- /* ===== RESPONSIVE ===== */
123
- @media (max-width: 600px) {
124
- .typing-indicator {
125
- padding: 6px 10px;
126
- gap: 5px;
127
- }
128
- }
129
-
130
- /* ===== SMOOTH PERFORMANCE ===== */
131
- .typing-dot {
132
- will-change: transform, opacity;
133
- backface-visibility: hidden;
134
55
  }
@@ -2,30 +2,41 @@ import React from 'react';
2
2
  import './Typing.css';
3
3
 
4
4
  interface TypingProps {
5
- color?: string;
5
+ /** Color of the moving dots */
6
+ dotColor?: string;
7
+ /** Color of the bubble background */
8
+ bubbleColor?: string;
9
+ /** Size of the dots in pixels */
6
10
  size?: number;
7
- variant?: 'dots' | 'boxes' | 'wave'; // Different animation styles
11
+ /** Optional class name for the container */
12
+ className?: string;
8
13
  }
9
14
 
10
15
  const Typing: React.FC<TypingProps> = ({
11
- color = '#6b7280',
12
- size = 8,
13
- variant = 'dots'
16
+ dotColor = '#6b7280', // Default dark grey
17
+ bubbleColor = '#e5e7eb', // Default light grey bubble
18
+ size = 8,
19
+ className = ''
14
20
  }) => {
21
+ // We pass dynamic values as CSS variables for better performance
22
+ const style = {
23
+ '--typing-dot-color': dotColor,
24
+ '--typing-bg': bubbleColor,
25
+ '--typing-dot-size': `${size}px`,
26
+ '--typing-gap': `${size * 0.5}px`, // Gap scales with size
27
+ '--typing-padding': `${size * 1.25}px ${size * 1.5}px` // Padding scales with size
28
+ } as React.CSSProperties;
29
+
15
30
  return (
16
- <div className={`typing-indicator typing-${variant}`}>
17
- <span
18
- className="typing-dot"
19
- style={{ backgroundColor: color, width: size, height: size }}
20
- />
21
- <span
22
- className="typing-dot"
23
- style={{ backgroundColor: color, width: size, height: size }}
24
- />
25
- <span
26
- className="typing-dot"
27
- style={{ backgroundColor: color, width: size, height: size }}
28
- />
31
+ <div
32
+ className={`typing-container ${className}`}
33
+ style={style}
34
+ role="status"
35
+ aria-label="Typing..."
36
+ >
37
+ <span className="typing-dot" />
38
+ <span className="typing-dot" />
39
+ <span className="typing-dot" />
29
40
  </div>
30
41
  );
31
42
  };
@@ -22,6 +22,7 @@ interface AssistantMessageProps {
22
22
  isBusiness?: boolean;
23
23
  handleAddToCart: (variantId: string, name: string,variantImage:string,price:string, qty?: number) => Promise<void>;
24
24
  handleRemoveFromCart: (variantId: string, qty?: number) => Promise<void>;
25
+
25
26
  }
26
27
 
27
28
  const SallaAsssiatnt: FC<AssistantMessageProps> = ({
@@ -29,6 +30,7 @@ const SallaAsssiatnt: FC<AssistantMessageProps> = ({
29
30
  colorCode,
30
31
  handleAddToCart,
31
32
  handleRemoveFromCart,
33
+
32
34
  }) => {
33
35
  const [counts, setCounts] = useState<Record<string, number>>({});
34
36
  const [selectedVariants, setSelectedVariants] = useState<Record<string, string>>({});
@@ -27,11 +27,14 @@ type MessengerProps = {
27
27
  disableForm: boolean;
28
28
  sessionId:string;
29
29
  setOpen: (v: boolean) => void;
30
- setView: React.Dispatch<React.SetStateAction<'sessions' | 'create' | 'messenger' | 'cartView'>>;
30
+ setView: React.Dispatch<React.SetStateAction<'sessions' | 'create' | 'messenger' | 'cartView' | 'tryOn'>>;
31
31
  setNewSession: (v: boolean) => void;
32
32
  Opened: boolean;
33
33
  calendly:string;
34
34
  disbleCheckout:boolean;
35
+ onTryOnSelect: (product: any) => void;
36
+ enableTry: boolean;
37
+
35
38
  };
36
39
 
37
40
  interface UploadImageResponse {
@@ -59,7 +62,9 @@ const Messenger: React.FC<MessengerProps> = ({
59
62
  Opened,
60
63
  setView,
61
64
  calendly,
62
- disbleCheckout
65
+ disbleCheckout,
66
+ onTryOnSelect,
67
+ enableTry
63
68
  }) => {
64
69
  const [input, setInput] = useState('');
65
70
  const [selectedImage, setSelectedImage] = useState<File | null>(null);
@@ -550,7 +555,9 @@ const socket = useChatSocket(sessionId, handleSocketData);
550
555
  isfetchingMessage={isFetchingMessages}
551
556
  messagesSuggestions={messagesSuggestions}
552
557
  sendMessageSuggestion={sendMessageSuggestion}
553
- />
558
+ onTryOnSelect={onTryOnSelect}
559
+ enableTry={enableTry}
560
+ />
554
561
 
555
562
 
556
563
 
@@ -45,6 +45,7 @@ const ChatSessions: React.FC<ChatSessionsProps> = ({
45
45
  `${config.apiUrl}get-chatlogs-by-ip/?visitor_id=${visitorId}&company_username=${localStorage.getItem('company_username')}`
46
46
  );
47
47
  const data = await res.json();
48
+
48
49
 
49
50
  const mapped: ChatSession[] = data.map((item: any) => {
50
51
  const createdDate = new Date(item.date);
@@ -63,6 +64,7 @@ const ChatSessions: React.FC<ChatSessionsProps> = ({
63
64
  });
64
65
 
65
66
  setSessions(mapped);
67
+
66
68
  } catch (err) {
67
69
  console.error('Failed to load sessions:', err);
68
70
  } finally {
@@ -70,6 +72,7 @@ const ChatSessions: React.FC<ChatSessionsProps> = ({
70
72
  }
71
73
  };
72
74
 
75
+
73
76
  useEffect(() => {
74
77
  if (visitorId) {
75
78
  fetchSessions();