shopify-chatbot-widget 1.0.3 → 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.
@@ -0,0 +1,408 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import {
3
+ ArrowLeftOutlined,
4
+ CloseOutlined,
5
+ ThunderboltFilled,
6
+ LoadingOutlined,
7
+ CameraOutlined,
8
+ ReloadOutlined,
9
+ } from '@ant-design/icons';
10
+ import { Button, message, Spin } from 'antd';
11
+ import axios from 'axios';
12
+ import config from '../../../hooks/config';
13
+
14
+ // --- Interfaces ---
15
+
16
+ interface ProductData {
17
+ variant_id: string;
18
+ name: string;
19
+ image: string;
20
+ price: string;
21
+ description?: string;
22
+ }
23
+
24
+ interface TryOnProps {
25
+ product: ProductData | null;
26
+ colorCode: string;
27
+ onBack: () => void;
28
+ setOpen: (open: boolean) => void;
29
+ handleAddToCart: (variantId: string, name: string, variantImage: string, price: string, qty?: number) => Promise<void>;
30
+ handleRemoveFromCart: (variantId: string, qty?: number) => Promise<void>;
31
+ counts: Record<string, number>;
32
+ setCounts: React.Dispatch<React.SetStateAction<Record<string, number>>>;
33
+ visitorId?: string;
34
+ }
35
+
36
+ interface UploadResponse {
37
+ message: string;
38
+ image_url: string;
39
+ }
40
+
41
+ interface TryOnResponse {
42
+ resultImage: string;
43
+ detected_category?: string;
44
+ detail?: string;
45
+ }
46
+
47
+ // --- Component ---
48
+
49
+ const TryOnLayout: React.FC<TryOnProps> = ({
50
+ product,
51
+ colorCode,
52
+ onBack,
53
+ setOpen,
54
+ handleAddToCart,
55
+ handleRemoveFromCart,
56
+ counts,
57
+ setCounts,
58
+ }) => {
59
+ const [isGenerating, setIsGenerating] = useState(false);
60
+ const [generatedImage, setGeneratedImage] = useState<string | null>(null);
61
+ const [userImageBlob, setUserImageBlob] = useState<string | null>(null);
62
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
63
+ const [imageLoaded, setImageLoaded] = useState(false);
64
+
65
+ const fileInputRef = useRef<HTMLInputElement>(null);
66
+
67
+ const themeColor = colorCode || '#7F28F8';
68
+
69
+ const activeProduct = product || {
70
+ variant_id: '0',
71
+ name: 'Unknown Product',
72
+ price: '0.00',
73
+ image: '',
74
+ };
75
+
76
+ const count = counts[activeProduct.variant_id] || 0;
77
+
78
+ const currentState = !userImageBlob ? 'empty' : !generatedImage ? 'ready' : 'generated';
79
+
80
+ // --- Handlers (same pattern as ProductCarousel) ---
81
+
82
+ const increment = async () => {
83
+ setCounts(prev => ({
84
+ ...prev,
85
+ [activeProduct.variant_id]: (prev[activeProduct.variant_id] || 0) + 1,
86
+ }));
87
+ try {
88
+ await handleAddToCart(
89
+ activeProduct.variant_id,
90
+ activeProduct.name,
91
+ activeProduct.image,
92
+ activeProduct.price,
93
+ 1
94
+ );
95
+ } catch (err: any) {
96
+ console.warn("Add to cart failed:", err.message);
97
+ setCounts(prev => ({
98
+ ...prev,
99
+ [activeProduct.variant_id]: (prev[activeProduct.variant_id] || 0) - 1,
100
+ }));
101
+ }
102
+ };
103
+
104
+ const decrement = async () => {
105
+ if (count === 0) return;
106
+ setCounts(prev => ({
107
+ ...prev,
108
+ [activeProduct.variant_id]: Math.max(0, (prev[activeProduct.variant_id] || 0) - 1),
109
+ }));
110
+ await handleRemoveFromCart(activeProduct.variant_id, 1);
111
+ };
112
+
113
+ const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
114
+ const file = event.target.files?.[0];
115
+ if (file) {
116
+ const blobUrl = URL.createObjectURL(file);
117
+ setUserImageBlob(blobUrl);
118
+ setSelectedFile(file);
119
+ setGeneratedImage(null);
120
+ setImageLoaded(false);
121
+ }
122
+ };
123
+
124
+ const triggerFileUpload = () => {
125
+ fileInputRef.current?.click();
126
+ };
127
+
128
+ const handleGenerateTryOn = async () => {
129
+ if (!selectedFile || !activeProduct.image) return;
130
+
131
+ setIsGenerating(true);
132
+ setImageLoaded(false);
133
+
134
+ try {
135
+ const formData = new FormData();
136
+ formData.append('image', selectedFile);
137
+
138
+ const uploadRes = await axios.post<UploadResponse>(
139
+ `${config.apiUrl}upload-image/`,
140
+ formData,
141
+ { headers: { 'Content-Type': 'multipart/form-data' } }
142
+ );
143
+
144
+ const payload = {
145
+ userImageUrl: uploadRes.data.image_url,
146
+ productImageUrl: activeProduct.image,
147
+ username: localStorage.getItem('company_username'),
148
+ productId: activeProduct.variant_id,
149
+ aspect_ratio: "3:4"
150
+ };
151
+
152
+ const response = await axios.post<TryOnResponse>(`${config.apiUrl}ark-tryon/`, payload);
153
+
154
+ if (response.data?.resultImage) {
155
+ setGeneratedImage(response.data.resultImage);
156
+ } else {
157
+ throw new Error("No image returned");
158
+ }
159
+ } catch (error: any) {
160
+ message.error(error.response?.data?.detail || "Generation failed");
161
+ } finally {
162
+ setIsGenerating(false);
163
+ }
164
+ };
165
+
166
+ const handleReset = () => {
167
+ setUserImageBlob(null);
168
+ setSelectedFile(null);
169
+ setGeneratedImage(null);
170
+ setImageLoaded(false);
171
+ };
172
+
173
+ // --- Render States ---
174
+
175
+ const renderEmptyState = () => (
176
+ <div
177
+ onClick={triggerFileUpload}
178
+ style={{
179
+ position: 'absolute', inset: 0,
180
+ display: 'flex', flexDirection: 'column',
181
+ alignItems: 'center', justifyContent: 'center',
182
+ background: '#f5f5f7', cursor: 'pointer',
183
+ }}
184
+ >
185
+ {activeProduct.image && (
186
+ <img src={activeProduct.image} alt=""
187
+ style={{
188
+ position: 'absolute', inset: 0, width: '100%', height: '100%',
189
+ objectFit: 'cover', opacity: 0.08, filter: 'blur(20px)',
190
+ }}
191
+ />
192
+ )}
193
+ <div style={{
194
+ width: 80, height: 80, borderRadius: '50%', background: themeColor,
195
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
196
+ marginBottom: 16, position: 'relative',
197
+ }}>
198
+ <CameraOutlined style={{ fontSize: 32, color: '#fff' }} />
199
+ </div>
200
+ <span style={{ fontSize: 17, fontWeight: 700, color: '#1a1a1a', marginBottom: 4 }}>
201
+ Upload Your Photo
202
+ </span>
203
+ <span style={{ fontSize: 13, color: '#999' }}>See yourself in this outfit</span>
204
+ </div>
205
+ );
206
+
207
+ const renderReadyState = () => (
208
+ <>
209
+ <div style={{ position: 'absolute', inset: 0, display: 'flex', overflow: 'hidden' }}>
210
+ <div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
211
+ <img src={userImageBlob!} alt="Your photo" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
212
+ <div style={{ position: 'absolute', top: 0, right: 0, bottom: 0, width: 1, background: 'rgba(255,255,255,0.6)' }} />
213
+ <div style={{
214
+ position: 'absolute', bottom: 10, left: 10,
215
+ background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(8px)',
216
+ color: '#fff', padding: '4px 10px', borderRadius: 14, fontSize: 11, fontWeight: 600,
217
+ }}>You</div>
218
+ <button
219
+ onClick={(e) => { e.stopPropagation(); triggerFileUpload(); }}
220
+ style={{
221
+ position: 'absolute', top: 10, left: 10,
222
+ background: 'rgba(255,255,255,0.85)', backdropFilter: 'blur(8px)',
223
+ border: 'none', borderRadius: 14, padding: '4px 10px',
224
+ fontSize: 11, fontWeight: 600, cursor: 'pointer',
225
+ display: 'flex', alignItems: 'center', gap: 4, color: '#333',
226
+ }}
227
+ >
228
+ <CameraOutlined style={{ fontSize: 11 }} /> Change
229
+ </button>
230
+ </div>
231
+ <div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
232
+ <img src={activeProduct.image} alt="Product" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
233
+ <div style={{
234
+ position: 'absolute', bottom: 10, right: 10,
235
+ background: themeColor, color: '#fff', padding: '4px 10px',
236
+ borderRadius: 14, fontSize: 11, fontWeight: 600,
237
+ }}>Outfit</div>
238
+ </div>
239
+ </div>
240
+ <button
241
+ onClick={handleGenerateTryOn}
242
+ style={{
243
+ position: 'absolute', left: '50%', bottom: 16, transform: 'translateX(-50%)',
244
+ background: themeColor, color: '#fff', border: 'none', borderRadius: 28,
245
+ padding: '14px 28px', fontSize: 15, fontWeight: 700, cursor: 'pointer',
246
+ display: 'flex', alignItems: 'center', gap: 8,
247
+ boxShadow: `0 8px 24px -4px ${themeColor}99`, zIndex: 10,
248
+ }}
249
+ >
250
+ <ThunderboltFilled style={{ fontSize: 18 }} /> Try It On
251
+ </button>
252
+ </>
253
+ );
254
+
255
+ const renderGeneratingState = () => (
256
+ <div style={{
257
+ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
258
+ alignItems: 'center', justifyContent: 'center', background: '#f5f5f7',
259
+ }}>
260
+ <Spin indicator={<LoadingOutlined style={{ fontSize: 48, color: themeColor }} spin />} />
261
+ <span style={{ marginTop: 20, fontSize: 16, fontWeight: 700, color: '#1a1a1a' }}>Creating your look...</span>
262
+ <span style={{ marginTop: 4, fontSize: 12, color: '#999' }}>About 10 seconds</span>
263
+ </div>
264
+ );
265
+
266
+ const renderGeneratedState = () => (
267
+ <>
268
+ <img
269
+ src={generatedImage!} alt="Try-on result"
270
+ style={{ width: '100%', height: '100%', objectFit: 'cover', opacity: imageLoaded ? 1 : 0, transition: 'opacity 0.4s ease' }}
271
+ onLoad={() => setImageLoaded(true)}
272
+ />
273
+ {!imageLoaded && (
274
+ <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f5f5f7' }}>
275
+ <Spin indicator={<LoadingOutlined style={{ fontSize: 32, color: themeColor }} spin />} />
276
+ </div>
277
+ )}
278
+ <div style={{
279
+ position: 'absolute', bottom: 14, left: '50%', transform: 'translateX(-50%)',
280
+ display: 'flex', gap: 8, zIndex: 10,
281
+ }}>
282
+ <button onClick={handleReset} style={{
283
+ background: 'rgba(255,255,255,0.92)', backdropFilter: 'blur(12px)',
284
+ border: 'none', borderRadius: 22, padding: '10px 16px', fontSize: 13,
285
+ fontWeight: 600, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 5, color: '#333',
286
+ }}>
287
+ <ReloadOutlined style={{ fontSize: 13 }} /> New Photo
288
+ </button>
289
+ <button onClick={handleGenerateTryOn} style={{
290
+ background: themeColor, color: '#fff', border: 'none', borderRadius: 22,
291
+ padding: '10px 16px', fontSize: 13, fontWeight: 600, cursor: 'pointer',
292
+ display: 'flex', alignItems: 'center', gap: 5,
293
+ }}>
294
+ <ThunderboltFilled style={{ fontSize: 13 }} /> Redo
295
+ </button>
296
+ </div>
297
+ </>
298
+ );
299
+
300
+ const showCartControls = currentState === 'generated' && !isGenerating;
301
+
302
+ return (
303
+ <div style={{
304
+ display: 'flex', flexDirection: 'column', height: '100%', background: '#fff',
305
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
306
+ position: 'relative', overflow: 'hidden',
307
+ }}>
308
+
309
+ {/* Header */}
310
+ <div style={{
311
+ padding: '14px 16px', display: 'flex', alignItems: 'center',
312
+ justifyContent: 'space-between', background: '#fff', position: 'relative',
313
+ zIndex: 20, flexShrink: 0, borderBottom: '1px solid #f0f0f0',
314
+ }}>
315
+ <Button type="text" icon={<ArrowLeftOutlined style={{ fontSize: 16 }} />} onClick={onBack} style={{ width: 36, height: 36 }} />
316
+ <span style={{ fontWeight: 700, fontSize: 16, color: '#1a1a1a' }}>Virtual Try-On</span>
317
+ <Button type="text" icon={<CloseOutlined style={{ fontSize: 14 }} />} onClick={() => setOpen(false)} style={{ color: '#bbb', width: 36, height: 36 }} />
318
+ </div>
319
+
320
+ {/* Image Area */}
321
+ <div style={{ flex: 1, position: 'relative', background: '#e8e8e8', overflow: 'hidden', minHeight: 0 }}>
322
+ {isGenerating
323
+ ? renderGeneratingState()
324
+ : currentState === 'empty'
325
+ ? renderEmptyState()
326
+ : currentState === 'ready'
327
+ ? renderReadyState()
328
+ : renderGeneratedState()
329
+ }
330
+ </div>
331
+
332
+ <input type="file" ref={fileInputRef} style={{ display: 'none' }} accept="image/*" onChange={handleFileUpload} />
333
+
334
+ {/* Bottom Bar */}
335
+ <div style={{
336
+ flexShrink: 0, background: '#fff', borderTop: '1px solid #f0f0f0',
337
+ padding: '12px 16px', paddingBottom: 'max(12px, env(safe-area-inset-bottom))', zIndex: 30,
338
+ }}>
339
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
340
+ {/* Thumbnail */}
341
+ <div style={{ width: 44, height: 44, borderRadius: 10, overflow: 'hidden', flexShrink: 0, background: '#f0f0f0' }}>
342
+ {activeProduct.image && <img src={activeProduct.image} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
343
+ </div>
344
+
345
+ {/* Name + Price */}
346
+ <div style={{ flex: 1, minWidth: 0 }}>
347
+ <div style={{ fontSize: 14, fontWeight: 700, color: '#1a1a1a', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
348
+ {activeProduct.name}
349
+ </div>
350
+ <div style={{ fontSize: 15, fontWeight: 700, color: themeColor, marginTop: 1 }}>
351
+ {activeProduct.price}
352
+ </div>
353
+ </div>
354
+
355
+ {/* +/- stepper (only after try-on generated) */}
356
+ {showCartControls ? (
357
+ <div style={{
358
+ display: 'flex', alignItems: 'center', background: '#f5f5f7',
359
+ borderRadius: 12, padding: 4, flexShrink: 0, animation: 'slideUp 0.3s ease',
360
+ }}>
361
+ <button
362
+ onClick={decrement} disabled={count === 0}
363
+ style={{
364
+ width: 36, height: 36, borderRadius: 10, border: 'none',
365
+ background: count > 0 ? '#fff' : 'transparent',
366
+ cursor: count > 0 ? 'pointer' : 'default',
367
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
368
+ color: count > 0 ? '#333' : '#ccc', fontSize: 16,
369
+ boxShadow: count > 0 ? '0 1px 3px rgba(0,0,0,0.08)' : 'none',
370
+ transition: 'all 0.15s ease',
371
+ }}
372
+ aria-label="Decrease quantity"
373
+ >–</button>
374
+ <span style={{ width: 34, textAlign: 'center', fontWeight: 700, fontSize: 15, color: '#1a1a1a' }}>
375
+ {count}
376
+ </span>
377
+ <button
378
+ onClick={increment}
379
+ style={{
380
+ width: 36, height: 36, borderRadius: 10, border: 'none',
381
+ background: '#fff', cursor: 'pointer',
382
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
383
+ color: themeColor, fontSize: 16,
384
+ boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
385
+ transition: 'all 0.15s ease',
386
+ }}
387
+ aria-label="Increase quantity"
388
+ >+</button>
389
+ </div>
390
+ ) : (
391
+ <div style={{ fontSize: 11, color: '#bbb', textAlign: 'right', lineHeight: 1.3, flexShrink: 0 }}>
392
+ Try it on first,<br/>then add to cart
393
+ </div>
394
+ )}
395
+ </div>
396
+ </div>
397
+
398
+ <style>{`
399
+ @keyframes slideUp {
400
+ from { opacity: 0; transform: translateY(10px); }
401
+ to { opacity: 1; transform: translateY(0); }
402
+ }
403
+ `}</style>
404
+ </div>
405
+ );
406
+ };
407
+
408
+ export default TryOnLayout;
@@ -1,11 +1,11 @@
1
1
 
2
2
 
3
3
  const config = {
4
- apiUrl:'https://jawebcrm.onrender.com/api/',
5
- websocketUrl:'wss://jawebcrm.onrender.com/ws' ,
4
+ // apiUrl:'https://jawebcrm.onrender.com/api/',
5
+ // websocketUrl:'wss://jawebcrm.onrender.com/ws' ,
6
6
 
7
- // apiUrl:'http://localhost:8000/api/',
8
- // websocketUrl:'ws://localhost:8000/ws',
7
+ apiUrl:'http://localhost:8000/api/',
8
+ websocketUrl:'ws://localhost:8000/ws',
9
9
 
10
10
  };
11
11
  export default config;
@@ -81,7 +81,7 @@ export const useChatbotAPI = (params: Params) => {
81
81
 
82
82
  if (res.ok) {
83
83
  const data = await res.json();
84
- console.log(data)
84
+
85
85
 
86
86
  if (data.message_history) {
87
87
 
@@ -25,6 +25,7 @@ export const useChatbotData = () => {
25
25
  const [restSuggestions, setRestSuggestions] = useState<string[]>([]);
26
26
  const [userType,setUserType]=useState('')
27
27
  const [disbleCheckout, setDisbleCheckout] = useState(false);
28
+ const [enableTryOn, setEnableTryon] = useState(true);
28
29
  const [chatbotName,setchatbotName]=useState('')
29
30
 
30
31
 
@@ -70,6 +71,7 @@ export const useChatbotData = () => {
70
71
  setCalendly(data.calendly_link ?? null);
71
72
  setDisbleCheckout(data.disable_checkout)
72
73
  setchatbotName(data.chatbot_name)
74
+ setEnableTryon(data.enable_try_on)
73
75
 
74
76
 
75
77
 
@@ -132,6 +134,7 @@ export const useChatbotData = () => {
132
134
  restSuggestions,
133
135
  userType,
134
136
  disbleCheckout,
135
- chatbotName
137
+ chatbotName,
138
+ enableTryOn
136
139
  };
137
140
  };
@@ -15,6 +15,7 @@ export interface ChatbotDetailsResponse {
15
15
  mode:string;
16
16
  disable_checkout:boolean;
17
17
  chatbot_name:string;
18
+ enable_try_on:boolean;
18
19
  };
19
20
  }
20
21