@wewear/virtual-try-on 1.4.30 → 2.0.1
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/dist/components/index.d.ts +0 -2
- package/dist/constants.d.ts +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.esm.js +363 -531
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +363 -531
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +0 -1
- package/dist/widget/modal-manager.d.ts +15 -0
- package/dist/widget/vto-api-client.d.ts +20 -0
- package/dist/widget.d.ts +6 -7
- package/package.json +3 -2
- package/dist/api.d.ts +0 -8
- package/dist/camera.d.ts +0 -10
- package/dist/components/alert-overlay.d.ts +0 -2
- package/dist/components/badge.d.ts +0 -1
- package/dist/components/camera-modal.d.ts +0 -7
- package/dist/components/loading-overlay.d.ts +0 -3
- package/dist/components/review-modal.d.ts +0 -8
- package/dist/components/status-badge.d.ts +0 -3
package/dist/index.esm.js
CHANGED
|
@@ -5,7 +5,6 @@ const CSS_CLASSES = {
|
|
|
5
5
|
};
|
|
6
6
|
const Z_INDEX = {
|
|
7
7
|
BUTTON: 10,
|
|
8
|
-
MODAL: 99999,
|
|
9
8
|
};
|
|
10
9
|
|
|
11
10
|
function getPositionStyles(position) {
|
|
@@ -21,140 +20,96 @@ function getPositionStyles(position) {
|
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
22
|
|
|
23
|
+
const CAMERA_ICON = `
|
|
24
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block;">
|
|
25
|
+
<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"></path>
|
|
26
|
+
<circle cx="12" cy="13" r="3"></circle>
|
|
27
|
+
</svg>
|
|
28
|
+
`;
|
|
29
|
+
const SPARKLES_ICON = `
|
|
30
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block; flex-shrink: 0;">
|
|
31
|
+
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/>
|
|
32
|
+
<path d="M20 2v4"/>
|
|
33
|
+
<path d="M22 4h-4"/>
|
|
34
|
+
<circle cx="4" cy="20" r="2"/>
|
|
35
|
+
</svg>
|
|
36
|
+
`;
|
|
37
|
+
const REFRESH_ICON = `
|
|
38
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block;">
|
|
39
|
+
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
|
|
40
|
+
<path d="M3 3v5h5"></path>
|
|
41
|
+
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path>
|
|
42
|
+
<path d="M16 16h5v5"></path>
|
|
43
|
+
</svg>
|
|
44
|
+
`;
|
|
45
|
+
const CONTAINER_STYLE = `
|
|
46
|
+
position: absolute;
|
|
47
|
+
display: flex;
|
|
48
|
+
gap: 4px;
|
|
49
|
+
border-radius: 100px;
|
|
50
|
+
background: white;
|
|
51
|
+
padding: 4px;
|
|
52
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
53
|
+
z-index: ${Z_INDEX.BUTTON};
|
|
54
|
+
transition: all 0.2s ease;
|
|
55
|
+
`;
|
|
56
|
+
const ICON_BUTTON_STYLE = `
|
|
57
|
+
display: flex;
|
|
58
|
+
width: 40px;
|
|
59
|
+
height: 40px;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: center;
|
|
63
|
+
border-radius: 100px;
|
|
64
|
+
padding: 0;
|
|
65
|
+
border: none;
|
|
66
|
+
transition: all 0.2s ease;
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
`;
|
|
69
|
+
const PRIMARY_STYLE = `${ICON_BUTTON_STYLE}
|
|
70
|
+
background: white;
|
|
71
|
+
color: #000000;
|
|
72
|
+
`;
|
|
73
|
+
function createIconButton(className, ariaLabel, iconHtml, styleCss, onClick) {
|
|
74
|
+
const button = document.createElement("button");
|
|
75
|
+
button.type = "button";
|
|
76
|
+
button.className = className;
|
|
77
|
+
button.setAttribute("aria-label", ariaLabel);
|
|
78
|
+
button.style.cssText = styleCss;
|
|
79
|
+
button.innerHTML = iconHtml;
|
|
80
|
+
button.onclick = onClick;
|
|
81
|
+
return button;
|
|
82
|
+
}
|
|
24
83
|
function createButtonContainer(buttonPosition, hasVirtualTryOn = false, onCameraClick, onRefreshClick, onToggleClick, isShowingVirtualTryOn = false) {
|
|
25
84
|
const container = document.createElement("div");
|
|
26
85
|
container.className = `${CSS_CLASSES.BUTTON_CONTAINER} ww-button-group`;
|
|
27
86
|
const positionStyles = getPositionStyles(buttonPosition);
|
|
28
|
-
container.style.cssText =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const cameraButton = document.createElement("button");
|
|
41
|
-
cameraButton.type = "button";
|
|
42
|
-
cameraButton.className = `${CSS_CLASSES.BUTTON} ww-camera-btn`;
|
|
43
|
-
cameraButton.setAttribute("aria-label", "Virtual Try-On");
|
|
44
|
-
if (hasVirtualTryOn) {
|
|
45
|
-
cameraButton.style.cssText = `
|
|
46
|
-
display: flex;
|
|
47
|
-
width: 40px;
|
|
48
|
-
height: 40px;
|
|
49
|
-
cursor: pointer;
|
|
50
|
-
align-items: center;
|
|
51
|
-
justify-content: center;
|
|
52
|
-
border-radius: 100px;
|
|
53
|
-
padding: 0;
|
|
54
|
-
border: none;
|
|
55
|
-
background: white;
|
|
56
|
-
color: #000000;
|
|
57
|
-
transition: all 0.2s ease;
|
|
58
|
-
flex-shrink: 0;
|
|
59
|
-
`;
|
|
60
|
-
cameraButton.innerHTML = `
|
|
61
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block;">
|
|
62
|
-
<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"></path>
|
|
63
|
-
<circle cx="12" cy="13" r="3"></circle>
|
|
64
|
-
</svg>
|
|
65
|
-
`;
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
cameraButton.style.cssText = `
|
|
69
|
-
display: flex;
|
|
70
|
-
cursor: pointer;
|
|
71
|
-
align-items: center;
|
|
72
|
-
justify-content: center;
|
|
73
|
-
border-radius: 100px;
|
|
74
|
-
padding: 10px 16px;
|
|
75
|
-
border: none;
|
|
76
|
-
background: white;
|
|
77
|
-
color: #000000;
|
|
78
|
-
transition: all 0.2s ease;
|
|
79
|
-
flex-shrink: 0;
|
|
80
|
-
gap: 8px;
|
|
81
|
-
font-size: 14px;
|
|
82
|
-
font-weight: 600;
|
|
83
|
-
white-space: nowrap;
|
|
84
|
-
`;
|
|
85
|
-
cameraButton.innerHTML = `
|
|
86
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block; flex-shrink: 0;">
|
|
87
|
-
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/>
|
|
88
|
-
<path d="M20 2v4"/>
|
|
89
|
-
<path d="M22 4h-4"/>
|
|
90
|
-
<circle cx="4" cy="20" r="2"/>
|
|
91
|
-
</svg>
|
|
92
|
-
<span style="display: block;">TRY THIS LOOK</span>
|
|
93
|
-
`;
|
|
94
|
-
}
|
|
95
|
-
cameraButton.onclick = onCameraClick;
|
|
87
|
+
container.style.cssText = `${CONTAINER_STYLE} ${positionStyles}`;
|
|
88
|
+
const cameraButton = hasVirtualTryOn
|
|
89
|
+
? createIconButton(`${CSS_CLASSES.BUTTON} ww-camera-btn`, "Virtual Try-On", CAMERA_ICON, PRIMARY_STYLE, onCameraClick)
|
|
90
|
+
: createIconButton(`${CSS_CLASSES.BUTTON} ww-camera-btn`, "Virtual Try-On", `${SPARKLES_ICON}<span style="display: block;">TRY THIS LOOK</span>`, `${PRIMARY_STYLE}
|
|
91
|
+
padding: 10px 16px;
|
|
92
|
+
gap: 8px;
|
|
93
|
+
font-size: 14px;
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
width: auto;
|
|
97
|
+
height: auto;
|
|
98
|
+
`, onCameraClick);
|
|
96
99
|
container.appendChild(cameraButton);
|
|
97
100
|
if (hasVirtualTryOn) {
|
|
98
101
|
if (onRefreshClick) {
|
|
99
|
-
const refreshButton =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
refreshButton.style.cssText = `
|
|
104
|
-
display: flex;
|
|
105
|
-
width: 40px;
|
|
106
|
-
height: 40px;
|
|
107
|
-
cursor: pointer;
|
|
108
|
-
align-items: center;
|
|
109
|
-
justify-content: center;
|
|
110
|
-
border-radius: 100px;
|
|
111
|
-
padding: 0;
|
|
112
|
-
border: none;
|
|
113
|
-
background: white;
|
|
114
|
-
color: #333333;
|
|
115
|
-
transition: all 0.2s ease;
|
|
116
|
-
flex-shrink: 0;
|
|
117
|
-
`;
|
|
118
|
-
refreshButton.innerHTML = `
|
|
119
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block;">
|
|
120
|
-
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
|
|
121
|
-
<path d="M3 3v5h5"></path>
|
|
122
|
-
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path>
|
|
123
|
-
<path d="M16 16h5v5"></path>
|
|
124
|
-
</svg>
|
|
125
|
-
`;
|
|
126
|
-
refreshButton.onclick = onRefreshClick;
|
|
127
|
-
refreshButton.onclick = onRefreshClick;
|
|
102
|
+
const refreshButton = createIconButton("ww-refresh-btn", "Refresh virtual try-on", REFRESH_ICON, `${ICON_BUTTON_STYLE}
|
|
103
|
+
background: white;
|
|
104
|
+
color: #333333;
|
|
105
|
+
`, onRefreshClick);
|
|
128
106
|
container.appendChild(refreshButton);
|
|
129
107
|
}
|
|
130
108
|
if (onToggleClick) {
|
|
131
|
-
const toggleButton =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
toggleButton.style.cssText = `
|
|
136
|
-
display: flex;
|
|
137
|
-
width: 40px;
|
|
138
|
-
height: 40px;
|
|
139
|
-
cursor: pointer;
|
|
140
|
-
align-items: center;
|
|
141
|
-
justify-content: center;
|
|
142
|
-
border-radius: 100px;
|
|
143
|
-
padding: 0;
|
|
144
|
-
border: none;
|
|
145
|
-
background: ${isShowingVirtualTryOn ? "#333333" : "#e5e7eb"};
|
|
146
|
-
color: ${isShowingVirtualTryOn ? "white" : "#333333"};
|
|
147
|
-
transition: all 0.2s ease;
|
|
148
|
-
flex-shrink: 0;
|
|
149
|
-
`;
|
|
150
|
-
toggleButton.innerHTML = `
|
|
151
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block;">
|
|
152
|
-
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/>
|
|
153
|
-
<path d="M20 2v4"/>
|
|
154
|
-
<path d="M22 4h-4"/>
|
|
155
|
-
<circle cx="4" cy="20" r="2"/>
|
|
156
|
-
</svg>
|
|
157
|
-
`;
|
|
109
|
+
const toggleButton = createIconButton("ww-toggle-btn", isShowingVirtualTryOn ? "Show Original Image" : "Show Virtual Try-On", SPARKLES_ICON, `${ICON_BUTTON_STYLE}
|
|
110
|
+
background: ${isShowingVirtualTryOn ? "#333333" : "#e5e7eb"};
|
|
111
|
+
color: ${isShowingVirtualTryOn ? "white" : "#333333"};
|
|
112
|
+
`, onToggleClick);
|
|
158
113
|
toggleButton.onmouseover = () => {
|
|
159
114
|
if (isShowingVirtualTryOn) {
|
|
160
115
|
toggleButton.style.background = "#4a4a4a";
|
|
@@ -191,205 +146,253 @@ function createButton(buttonPosition, onClick, disabled = false) {
|
|
|
191
146
|
return container;
|
|
192
147
|
}
|
|
193
148
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
@keyframes ww-dot-bounce {
|
|
240
|
-
0%, 80%, 100% {
|
|
241
|
-
transform: scale(0);
|
|
242
|
-
opacity: 0.5;
|
|
243
|
-
}
|
|
244
|
-
40% {
|
|
245
|
-
transform: scale(1);
|
|
246
|
-
opacity: 1;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
.ww-loading-logo {
|
|
251
|
-
animation: ww-pulse-logo 2s ease-in-out infinite;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
.ww-loading-spinner {
|
|
255
|
-
animation: ww-spinner-rotate 1s linear infinite;
|
|
256
|
-
}
|
|
257
|
-
`;
|
|
258
|
-
document.head.appendChild(style);
|
|
259
|
-
}
|
|
260
|
-
overlay.innerHTML = `
|
|
261
|
-
<div style="display: flex; flex-direction: column; align-items: center; gap: 32px;">
|
|
262
|
-
<div style="position: relative; width: 120px; height: 120px; display: flex; align-items: center; justify-content: center;">
|
|
263
|
-
<!-- Outer spinning circle -->
|
|
264
|
-
<div class="ww-loading-spinner" style="
|
|
265
|
-
position: absolute;
|
|
266
|
-
width: 120px;
|
|
267
|
-
height: 120px;
|
|
268
|
-
border: 3px solid transparent;
|
|
269
|
-
border-top: 3px solid #333333;
|
|
270
|
-
border-right: 3px solid #333333;
|
|
271
|
-
border-radius: 50%;
|
|
272
|
-
"></div>
|
|
273
|
-
|
|
274
|
-
<!-- Logo -->
|
|
275
|
-
<svg class="ww-loading-logo" width="80" height="50" viewBox="0 0 214 135" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
276
|
-
<path d="M102.906 74.8679C102.906 77.9717 101.574 80.7453 98.9104 83.1887C96.6871 85.1918 93.9025 86.6997 90.5566 87.7123C87.695 88.5708 84.6462 89 81.4104 89C73.8821 89 68.0047 87.0189 63.7783 83.0566C59.5519 87.0189 53.6855 89 46.1792 89C42.9434 89 39.9057 88.5708 37.066 87.7123C33.7201 86.6997 30.9245 85.1918 28.6792 83.1887C26.0157 80.7453 24.684 77.9717 24.684 74.8679V41.6509H32.3774V74.8679C32.3774 76.2547 33.489 77.5645 35.7123 78.7972C37.3632 79.7217 39.0692 80.3711 40.8302 80.7453C42.5252 81.1195 44.3082 81.3066 46.1792 81.3066C48.0063 81.3066 49.7673 81.1195 51.4623 80.7453C53.2453 80.3711 54.9623 79.7217 56.6132 78.7972C58.8585 77.5645 59.9811 76.2547 59.9811 74.8679V41.6509H67.6085V74.8679C67.6085 76.2547 68.7311 77.5645 70.9764 78.7972C72.6274 79.7217 74.3443 80.3711 76.1274 80.7453C77.8223 81.1195 79.5833 81.3066 81.4104 81.3066C83.2814 81.3066 85.0755 81.1195 86.7925 80.7453C88.5314 80.3711 90.2264 79.7217 91.8774 78.7972C94.1006 77.5645 95.2123 76.2547 95.2123 74.8679V41.6509H102.906V74.8679ZM189.283 74.8679C189.283 77.9717 187.951 80.7453 185.288 83.1887C183.064 85.1918 180.28 86.6997 176.934 87.7123C174.072 88.5708 171.024 89 167.788 89C160.259 89 154.382 87.0189 150.156 83.0566C145.929 87.0189 140.063 89 132.557 89C129.321 89 126.283 88.5708 123.443 87.7123C120.097 86.6997 117.302 85.1918 115.057 83.1887C112.393 80.7453 111.061 77.9717 111.061 74.8679V41.6509H118.755V74.8679C118.755 76.2547 119.866 77.5645 122.09 78.7972C123.741 79.7217 125.447 80.3711 127.208 80.7453C128.903 81.1195 130.686 81.3066 132.557 81.3066C134.384 81.3066 136.145 81.1195 137.84 80.7453C139.623 80.3711 141.34 79.7217 142.991 78.7972C145.236 77.5645 146.358 76.2547 146.358 74.8679V41.6509H153.986V74.8679C153.986 76.2547 155.108 77.5645 157.354 78.7972C159.005 79.7217 160.722 80.3711 162.505 80.7453C164.2 81.1195 165.961 81.3066 167.788 81.3066C169.659 81.3066 171.453 81.1195 173.17 80.7453C174.909 80.3711 176.604 79.7217 178.255 78.7972C180.478 77.5645 181.59 76.2547 181.59 74.8679V41.6509H189.283V74.8679Z" fill="white"/>
|
|
277
|
-
</svg>
|
|
278
|
-
</div>
|
|
279
|
-
|
|
280
|
-
<div style="font-size: 17px; font-weight: 500; text-align: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: white; letter-spacing: 0.5px; max-width: 280px;">${text}</div>
|
|
281
|
-
|
|
282
|
-
<div style="display: flex; gap: 10px; margin-top: 4px;">
|
|
283
|
-
<div style="width: 10px; height: 10px; border-radius: 50%; background: #333333; animation: ww-dot-bounce 1.4s ease-in-out infinite;"></div>
|
|
284
|
-
<div style="width: 10px; height: 10px; border-radius: 50%; background: #333333; animation: ww-dot-bounce 1.4s ease-in-out 0.2s infinite;"></div>
|
|
285
|
-
<div style="width: 10px; height: 10px; border-radius: 50%; background: #333333; animation: ww-dot-bounce 1.4s ease-in-out 0.4s infinite;"></div>
|
|
286
|
-
</div>
|
|
287
|
-
</div>
|
|
288
|
-
`;
|
|
289
|
-
return overlay;
|
|
290
|
-
}
|
|
291
|
-
function showProductLoading(container, text = "Generating virtual try-on...") {
|
|
292
|
-
const existingOverlay = container.querySelector(".ww-loading-overlay");
|
|
293
|
-
if (existingOverlay) {
|
|
294
|
-
const textElement = existingOverlay.querySelector('div[style*="font-size: 17px"]');
|
|
295
|
-
if (textElement) {
|
|
296
|
-
textElement.textContent = text;
|
|
149
|
+
class ModalManager {
|
|
150
|
+
constructor(baseUrl) {
|
|
151
|
+
this.modalElement = null;
|
|
152
|
+
this.pendingModalPrefill = null;
|
|
153
|
+
this.modalOrigin = new URL(baseUrl).origin;
|
|
154
|
+
}
|
|
155
|
+
open(url, options, onClose) {
|
|
156
|
+
this.close();
|
|
157
|
+
this.pendingModalPrefill = (options === null || options === void 0 ? void 0 : options.prefillImage)
|
|
158
|
+
? {
|
|
159
|
+
image: options.prefillImage,
|
|
160
|
+
autoSubmit: options.autoSubmit === true,
|
|
161
|
+
}
|
|
162
|
+
: null;
|
|
163
|
+
const modal = document.createElement("div");
|
|
164
|
+
modal.className = CSS_CLASSES.MODAL;
|
|
165
|
+
modal.style.cssText = `
|
|
166
|
+
position: fixed;
|
|
167
|
+
top: 0;
|
|
168
|
+
left: 0;
|
|
169
|
+
right: 0;
|
|
170
|
+
bottom: 0;
|
|
171
|
+
width: 100%;
|
|
172
|
+
height: 100%;
|
|
173
|
+
background-color: rgba(0, 0, 0, 0.8);
|
|
174
|
+
display: flex;
|
|
175
|
+
justify-content: center;
|
|
176
|
+
align-items: center;
|
|
177
|
+
z-index: 999999999;
|
|
178
|
+
animation: ww-modal-fade-in 0.3s ease;
|
|
179
|
+
`;
|
|
180
|
+
if (!document.getElementById("ww-modal-animation")) {
|
|
181
|
+
const style = document.createElement("style");
|
|
182
|
+
style.id = "ww-modal-animation";
|
|
183
|
+
style.textContent = `
|
|
184
|
+
@keyframes ww-modal-fade-in {
|
|
185
|
+
from { opacity: 0; }
|
|
186
|
+
to { opacity: 1; }
|
|
187
|
+
}
|
|
188
|
+
@keyframes ww-modal-scale-in {
|
|
189
|
+
from { transform: scale(0.95); opacity: 0; }
|
|
190
|
+
to { transform: scale(1); opacity: 1; }
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
document.head.appendChild(style);
|
|
297
194
|
}
|
|
298
|
-
|
|
195
|
+
const iframeContainer = document.createElement("div");
|
|
196
|
+
iframeContainer.style.cssText = `
|
|
197
|
+
position: relative;
|
|
198
|
+
width: 90%;
|
|
199
|
+
height: 90%;
|
|
200
|
+
max-width: 480px;
|
|
201
|
+
max-height: 720px;
|
|
202
|
+
border-radius: 20px;
|
|
203
|
+
overflow: hidden;
|
|
204
|
+
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.3), 0 12px 24px rgba(0, 0, 0, 0.2);
|
|
205
|
+
animation: ww-modal-scale-in 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
206
|
+
background: white;
|
|
207
|
+
`;
|
|
208
|
+
const iframe = document.createElement("iframe");
|
|
209
|
+
iframe.src = url;
|
|
210
|
+
iframe.style.cssText = `
|
|
211
|
+
width: 100%;
|
|
212
|
+
height: 100%;
|
|
213
|
+
border: none;
|
|
214
|
+
`;
|
|
215
|
+
const closeButton = document.createElement("button");
|
|
216
|
+
closeButton.innerHTML = `
|
|
217
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
218
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
219
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
220
|
+
</svg>
|
|
221
|
+
`;
|
|
222
|
+
closeButton.setAttribute("aria-label", "Close modal");
|
|
223
|
+
closeButton.style.cssText = `
|
|
224
|
+
position: absolute;
|
|
225
|
+
top: -48px;
|
|
226
|
+
right: 0;
|
|
227
|
+
background: rgba(255, 255, 255, 0.95);
|
|
228
|
+
backdrop-filter: blur(8px);
|
|
229
|
+
-webkit-backdrop-filter: blur(8px);
|
|
230
|
+
border: none;
|
|
231
|
+
border-radius: 50%;
|
|
232
|
+
width: 40px;
|
|
233
|
+
height: 40px;
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
justify-content: center;
|
|
238
|
+
color: #1f2937;
|
|
239
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
240
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
241
|
+
`;
|
|
242
|
+
closeButton.onmouseover = () => {
|
|
243
|
+
closeButton.style.transform = "scale(1.1) rotate(90deg)";
|
|
244
|
+
closeButton.style.background = "#ef4444";
|
|
245
|
+
closeButton.style.color = "white";
|
|
246
|
+
};
|
|
247
|
+
closeButton.onmouseout = () => {
|
|
248
|
+
closeButton.style.transform = "scale(1) rotate(0deg)";
|
|
249
|
+
closeButton.style.background = "rgba(255, 255, 255, 0.95)";
|
|
250
|
+
closeButton.style.color = "#1f2937";
|
|
251
|
+
};
|
|
252
|
+
closeButton.onclick = () => {
|
|
253
|
+
modal.style.opacity = "0";
|
|
254
|
+
iframeContainer.style.transform = "scale(0.95)";
|
|
255
|
+
iframeContainer.style.opacity = "0";
|
|
256
|
+
setTimeout(onClose, 300);
|
|
257
|
+
};
|
|
258
|
+
iframeContainer.appendChild(iframe);
|
|
259
|
+
iframeContainer.appendChild(closeButton);
|
|
260
|
+
modal.appendChild(iframeContainer);
|
|
261
|
+
document.body.appendChild(modal);
|
|
262
|
+
modal.onclick = (e) => {
|
|
263
|
+
if (e.target === modal) {
|
|
264
|
+
closeButton.click();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
this.modalElement = modal;
|
|
299
268
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
269
|
+
close() {
|
|
270
|
+
if (this.modalElement) {
|
|
271
|
+
this.modalElement.remove();
|
|
272
|
+
this.modalElement = null;
|
|
273
|
+
}
|
|
274
|
+
this.pendingModalPrefill = null;
|
|
303
275
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
276
|
+
sendPendingPrefillToModal() {
|
|
277
|
+
if (!this.pendingModalPrefill || !this.modalElement) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const iframe = this.modalElement.querySelector("iframe");
|
|
281
|
+
if (!(iframe instanceof HTMLIFrameElement) || !iframe.contentWindow) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
iframe.contentWindow.postMessage({
|
|
285
|
+
type: "VTO_PREFILL_IMAGE",
|
|
286
|
+
image: this.pendingModalPrefill.image,
|
|
287
|
+
autoSubmit: this.pendingModalPrefill.autoSubmit,
|
|
288
|
+
}, this.modalOrigin);
|
|
289
|
+
this.pendingModalPrefill = null;
|
|
290
|
+
}
|
|
291
|
+
postMessageToModal(type) {
|
|
292
|
+
if (!this.modalElement) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const iframe = this.modalElement.querySelector("iframe");
|
|
296
|
+
if (!(iframe instanceof HTMLIFrameElement) || !iframe.contentWindow) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
iframe.contentWindow.postMessage({ type }, this.modalOrigin);
|
|
311
300
|
}
|
|
312
301
|
}
|
|
313
302
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
gap: 10px;
|
|
336
|
-
animation: ww-badge-fade-in 0.3s ease;
|
|
337
|
-
letter-spacing: 0.3px;
|
|
338
|
-
`;
|
|
339
|
-
if (!document.getElementById("ww-status-badge-animation")) {
|
|
340
|
-
const style = document.createElement("style");
|
|
341
|
-
style.id = "ww-status-badge-animation";
|
|
342
|
-
style.textContent = `
|
|
343
|
-
@keyframes ww-badge-fade-in {
|
|
344
|
-
from { opacity: 0; transform: translateX(-50%) translateY(-10px); }
|
|
345
|
-
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
@keyframes ww-badge-pulse {
|
|
349
|
-
0%, 100% { opacity: 0.6; }
|
|
350
|
-
50% { opacity: 1; }
|
|
351
|
-
}
|
|
352
|
-
`;
|
|
353
|
-
document.head.appendChild(style);
|
|
354
|
-
}
|
|
355
|
-
badge.innerHTML = `
|
|
356
|
-
<div style="display: flex; gap: 4px;">
|
|
357
|
-
<div style="width: 6px; height: 6px; border-radius: 50%; background: white; animation: ww-badge-pulse 1.4s ease-in-out infinite;"></div>
|
|
358
|
-
<div style="width: 6px; height: 6px; border-radius: 50%; background: white; animation: ww-badge-pulse 1.4s ease-in-out 0.2s infinite;"></div>
|
|
359
|
-
<div style="width: 6px; height: 6px; border-radius: 50%; background: white; animation: ww-badge-pulse 1.4s ease-in-out 0.4s infinite;"></div>
|
|
360
|
-
</div>
|
|
361
|
-
<span>${message}</span>
|
|
362
|
-
`;
|
|
363
|
-
return badge;
|
|
364
|
-
}
|
|
365
|
-
function showStatusBadge(container, message) {
|
|
366
|
-
const existingBadge = container.querySelector(".ww-status-badge");
|
|
367
|
-
if (existingBadge) {
|
|
368
|
-
const textElement = existingBadge.querySelector("span");
|
|
369
|
-
if (textElement) {
|
|
370
|
-
textElement.textContent = message;
|
|
303
|
+
class VtoApiClient {
|
|
304
|
+
constructor(baseUrl) {
|
|
305
|
+
this.baseUrl = baseUrl;
|
|
306
|
+
}
|
|
307
|
+
async fetchVtoData(outfitId) {
|
|
308
|
+
try {
|
|
309
|
+
const vtoDataUrl = new URL("/api/vto/data", this.baseUrl);
|
|
310
|
+
vtoDataUrl.searchParams.set("outfit_id", outfitId);
|
|
311
|
+
const response = await fetch(vtoDataUrl.toString());
|
|
312
|
+
if (response.status === 404)
|
|
313
|
+
return null;
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
const errorText = await response.text();
|
|
316
|
+
console.warn(`[WeWear VTO] Failed to retrieve VTO data (${response.status}): ${errorText}`);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
return (await response.json());
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
console.warn("[WeWear VTO] Error retrieving VTO data:", error);
|
|
323
|
+
return null;
|
|
371
324
|
}
|
|
372
|
-
return;
|
|
373
325
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
326
|
+
async submitTryOn(modelImage, productImages, options) {
|
|
327
|
+
var _a;
|
|
328
|
+
const outfitID = (_a = options.outfitID) === null || _a === void 0 ? void 0 : _a.trim();
|
|
329
|
+
let resolvedProductImages = productImages;
|
|
330
|
+
let resolvedGeminiModel = null;
|
|
331
|
+
let resolvedAdditionalPrompt = null;
|
|
332
|
+
if (outfitID) {
|
|
333
|
+
const vtoData = await this.fetchVtoData(outfitID);
|
|
334
|
+
if (vtoData) {
|
|
335
|
+
if (Array.isArray(vtoData.images) && vtoData.images.length > 0) {
|
|
336
|
+
resolvedProductImages = vtoData.images;
|
|
337
|
+
}
|
|
338
|
+
if (typeof vtoData.gemini_model === "string" && vtoData.gemini_model) {
|
|
339
|
+
resolvedGeminiModel = vtoData.gemini_model;
|
|
340
|
+
}
|
|
341
|
+
if (typeof vtoData.additional_prompt === "string" &&
|
|
342
|
+
vtoData.additional_prompt) {
|
|
343
|
+
resolvedAdditionalPrompt = vtoData.additional_prompt;
|
|
344
|
+
}
|
|
345
|
+
console.log(`[WeWear VTO] Using stored VTO data for outfit_id=${outfitID}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const formData = new FormData();
|
|
349
|
+
formData.append("model_image", modelImage, "model_image.png");
|
|
350
|
+
formData.append("product_image_urls", JSON.stringify(resolvedProductImages));
|
|
351
|
+
formData.append("page_url", window.location.href);
|
|
352
|
+
formData.append("model_tier", options.modelTier);
|
|
353
|
+
if (resolvedGeminiModel)
|
|
354
|
+
formData.append("gemini_model", resolvedGeminiModel);
|
|
355
|
+
if (resolvedAdditionalPrompt) {
|
|
356
|
+
formData.append("additional_prompt", resolvedAdditionalPrompt);
|
|
357
|
+
}
|
|
358
|
+
if (outfitID)
|
|
359
|
+
formData.append("outfit_id", outfitID);
|
|
360
|
+
const res = await fetch(`${this.baseUrl}/api/vto`, {
|
|
361
|
+
method: "POST",
|
|
362
|
+
body: formData,
|
|
363
|
+
});
|
|
364
|
+
if (!res.ok) {
|
|
365
|
+
const errorText = await res.text();
|
|
366
|
+
console.error("[WeWear VTO] API submission failed:", res.status, errorText);
|
|
367
|
+
throw new Error(`API submission failed: ${res.status}`);
|
|
368
|
+
}
|
|
369
|
+
return res.json();
|
|
370
|
+
}
|
|
371
|
+
async fetchJobStatus(jobId) {
|
|
372
|
+
const res = await fetch(`${this.baseUrl}/api/vto/status?job_id=${jobId}`);
|
|
373
|
+
if (!res.ok)
|
|
374
|
+
throw new Error(`Status check failed: ${res.status}`);
|
|
375
|
+
return res.json();
|
|
376
|
+
}
|
|
377
|
+
async fetchJobImage(jobId) {
|
|
378
|
+
const url = `${this.baseUrl}/api/vto/image?job_id=${jobId}`;
|
|
379
|
+
const res = await fetch(url);
|
|
380
|
+
if (res.status === 202)
|
|
381
|
+
throw new Error("202_PROCESSING");
|
|
382
|
+
if (!res.ok)
|
|
383
|
+
throw new Error(`Image fetch failed: ${res.status}`);
|
|
384
|
+
const blob = await res.blob();
|
|
385
|
+
return URL.createObjectURL(blob);
|
|
381
386
|
}
|
|
382
387
|
}
|
|
383
388
|
|
|
384
389
|
class VirtualTryOnWidget {
|
|
385
390
|
constructor(config) {
|
|
386
391
|
this.virtualTryOnImageUrl = null;
|
|
387
|
-
this.originalProductImageUrl = null;
|
|
388
392
|
this.originalProductImages = []; // Store all original product images
|
|
389
393
|
this.isShowingVirtualTryOn = false;
|
|
390
394
|
this.lastModelImage = null;
|
|
391
395
|
this.cameraButton = null;
|
|
392
|
-
this.modalElement = null;
|
|
393
396
|
this.iframeMessageListener = null;
|
|
394
397
|
this.instanceId = ++VirtualTryOnWidget.instanceCounter;
|
|
395
398
|
this.config = {
|
|
@@ -403,6 +406,8 @@ class VirtualTryOnWidget {
|
|
|
403
406
|
singleton: config.singleton,
|
|
404
407
|
galleryElement: config.galleryElement,
|
|
405
408
|
};
|
|
409
|
+
this.modal = new ModalManager(this.config.baseUrl);
|
|
410
|
+
this.vtoApiClient = new VtoApiClient(this.config.baseUrl);
|
|
406
411
|
}
|
|
407
412
|
getContainer() {
|
|
408
413
|
const configContainer = this.config.galleryElement;
|
|
@@ -456,17 +461,40 @@ class VirtualTryOnWidget {
|
|
|
456
461
|
console.warn("[WeWear VTO] Product images not found:", this.config.productImageSelector);
|
|
457
462
|
return;
|
|
458
463
|
}
|
|
459
|
-
|
|
460
|
-
this.originalProductImageUrl = this.originalProductImages[0];
|
|
461
|
-
}
|
|
462
|
-
const photoUploadUrl = new URL(`${this.config.baseUrl}`);
|
|
463
|
-
photoUploadUrl.searchParams.append("ww_product_images", this.originalProductImages.join(","));
|
|
464
|
+
const photoUploadUrl = this.buildPhotoUploadUrl();
|
|
464
465
|
this.showPhotoUploadModal(photoUploadUrl.toString());
|
|
465
466
|
}
|
|
466
467
|
catch (error) {
|
|
467
468
|
console.error("[WeWear VTO] Try-on request failed:", error);
|
|
468
469
|
}
|
|
469
470
|
}
|
|
471
|
+
async handleRegenerateClick() {
|
|
472
|
+
if (!this.lastModelImage) {
|
|
473
|
+
await this.handleTryOnClick();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (this.originalProductImages.length === 0) {
|
|
477
|
+
this.originalProductImages = this.getAllProductImages();
|
|
478
|
+
}
|
|
479
|
+
if (this.originalProductImages.length === 0) {
|
|
480
|
+
console.warn("[WeWear VTO] No product images found for regenerate.");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const photoUploadUrl = this.buildPhotoUploadUrl({ startAtConfirm: true });
|
|
484
|
+
this.showPhotoUploadModal(photoUploadUrl.toString(), {
|
|
485
|
+
prefillImage: this.lastModelImage,
|
|
486
|
+
autoSubmit: true,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
buildPhotoUploadUrl(options) {
|
|
490
|
+
const photoUploadUrl = new URL(`${this.config.baseUrl}`);
|
|
491
|
+
photoUploadUrl.searchParams.append("ww_product_images", this.originalProductImages.join(","));
|
|
492
|
+
if (options === null || options === void 0 ? void 0 : options.startAtConfirm) {
|
|
493
|
+
photoUploadUrl.searchParams.append("ww_step", "2");
|
|
494
|
+
photoUploadUrl.searchParams.append("ww_regenerate", "1");
|
|
495
|
+
}
|
|
496
|
+
return photoUploadUrl;
|
|
497
|
+
}
|
|
470
498
|
getAllProductImages() {
|
|
471
499
|
const container = this.getContainer();
|
|
472
500
|
const scopedElements = container === null || container === void 0 ? void 0 : container.querySelectorAll(this.config.productImageSelector);
|
|
@@ -486,124 +514,23 @@ class VirtualTryOnWidget {
|
|
|
486
514
|
});
|
|
487
515
|
return images;
|
|
488
516
|
}
|
|
489
|
-
showPhotoUploadModal(url) {
|
|
517
|
+
showPhotoUploadModal(url, options) {
|
|
490
518
|
this.closeModal();
|
|
491
519
|
VirtualTryOnWidget.activeWidgetId = this.instanceId;
|
|
492
|
-
|
|
493
|
-
modal.className = CSS_CLASSES.MODAL;
|
|
494
|
-
modal.style.cssText = `
|
|
495
|
-
position: fixed;
|
|
496
|
-
top: 0;
|
|
497
|
-
left: 0;
|
|
498
|
-
right: 0;
|
|
499
|
-
bottom: 0;
|
|
500
|
-
width: 100%;
|
|
501
|
-
height: 100%;
|
|
502
|
-
background-color: rgba(0, 0, 0, 0.8);
|
|
503
|
-
display: flex;
|
|
504
|
-
justify-content: center;
|
|
505
|
-
align-items: center;
|
|
506
|
-
z-index: 999999999;
|
|
507
|
-
animation: ww-modal-fade-in 0.3s ease;
|
|
508
|
-
`;
|
|
509
|
-
if (!document.getElementById("ww-modal-animation")) {
|
|
510
|
-
const style = document.createElement("style");
|
|
511
|
-
style.id = "ww-modal-animation";
|
|
512
|
-
style.textContent = `
|
|
513
|
-
@keyframes ww-modal-fade-in {
|
|
514
|
-
from { opacity: 0; }
|
|
515
|
-
to { opacity: 1; }
|
|
516
|
-
}
|
|
517
|
-
@keyframes ww-modal-scale-in {
|
|
518
|
-
from { transform: scale(0.95); opacity: 0; }
|
|
519
|
-
to { transform: scale(1); opacity: 1; }
|
|
520
|
-
}
|
|
521
|
-
`;
|
|
522
|
-
document.head.appendChild(style);
|
|
523
|
-
}
|
|
524
|
-
const iframeContainer = document.createElement("div");
|
|
525
|
-
iframeContainer.style.cssText = `
|
|
526
|
-
position: relative;
|
|
527
|
-
width: 90%;
|
|
528
|
-
height: 90%;
|
|
529
|
-
max-width: 480px;
|
|
530
|
-
max-height: 720px;
|
|
531
|
-
border-radius: 20px;
|
|
532
|
-
overflow: hidden;
|
|
533
|
-
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.3), 0 12px 24px rgba(0, 0, 0, 0.2);
|
|
534
|
-
animation: ww-modal-scale-in 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
535
|
-
background: white;
|
|
536
|
-
`;
|
|
537
|
-
const iframe = document.createElement("iframe");
|
|
538
|
-
iframe.src = url;
|
|
539
|
-
iframe.style.cssText = `
|
|
540
|
-
width: 100%;
|
|
541
|
-
height: 100%;
|
|
542
|
-
border: none;
|
|
543
|
-
`;
|
|
544
|
-
const closeButton = document.createElement("button");
|
|
545
|
-
closeButton.innerHTML = `
|
|
546
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
547
|
-
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
548
|
-
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
549
|
-
</svg>
|
|
550
|
-
`;
|
|
551
|
-
closeButton.setAttribute("aria-label", "Close modal");
|
|
552
|
-
closeButton.style.cssText = `
|
|
553
|
-
position: absolute;
|
|
554
|
-
top: -48px;
|
|
555
|
-
right: 0;
|
|
556
|
-
background: rgba(255, 255, 255, 0.95);
|
|
557
|
-
backdrop-filter: blur(8px);
|
|
558
|
-
-webkit-backdrop-filter: blur(8px);
|
|
559
|
-
border: none;
|
|
560
|
-
border-radius: 50%;
|
|
561
|
-
width: 40px;
|
|
562
|
-
height: 40px;
|
|
563
|
-
cursor: pointer;
|
|
564
|
-
display: flex;
|
|
565
|
-
align-items: center;
|
|
566
|
-
justify-content: center;
|
|
567
|
-
color: #1f2937;
|
|
568
|
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
569
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
570
|
-
`;
|
|
571
|
-
closeButton.onmouseover = () => {
|
|
572
|
-
closeButton.style.transform = "scale(1.1) rotate(90deg)";
|
|
573
|
-
closeButton.style.background = "#ef4444";
|
|
574
|
-
closeButton.style.color = "white";
|
|
575
|
-
};
|
|
576
|
-
closeButton.onmouseout = () => {
|
|
577
|
-
closeButton.style.transform = "scale(1) rotate(0deg)";
|
|
578
|
-
closeButton.style.background = "rgba(255, 255, 255, 0.95)";
|
|
579
|
-
closeButton.style.color = "#1f2937";
|
|
580
|
-
};
|
|
581
|
-
closeButton.onclick = () => {
|
|
582
|
-
modal.style.opacity = "0";
|
|
583
|
-
iframeContainer.style.transform = "scale(0.95)";
|
|
584
|
-
iframeContainer.style.opacity = "0";
|
|
585
|
-
setTimeout(() => this.closeModal(), 300);
|
|
586
|
-
};
|
|
587
|
-
iframeContainer.appendChild(iframe);
|
|
588
|
-
iframeContainer.appendChild(closeButton);
|
|
589
|
-
modal.appendChild(iframeContainer);
|
|
590
|
-
document.body.appendChild(modal);
|
|
591
|
-
modal.onclick = (e) => {
|
|
592
|
-
if (e.target === modal) {
|
|
593
|
-
closeButton.click();
|
|
594
|
-
}
|
|
595
|
-
};
|
|
596
|
-
this.modalElement = modal;
|
|
520
|
+
this.modal.open(url, options, () => this.closeModal());
|
|
597
521
|
}
|
|
598
522
|
closeModal() {
|
|
599
|
-
|
|
600
|
-
this.modalElement.remove();
|
|
601
|
-
this.modalElement = null;
|
|
602
|
-
}
|
|
523
|
+
this.modal.close();
|
|
603
524
|
if (VirtualTryOnWidget.activeWidgetId === this.instanceId) {
|
|
604
525
|
VirtualTryOnWidget.activeWidgetId = null;
|
|
605
526
|
}
|
|
606
527
|
}
|
|
528
|
+
sendPendingPrefillToModal() {
|
|
529
|
+
this.modal.sendPendingPrefillToModal();
|
|
530
|
+
}
|
|
531
|
+
postMessageToModal(type) {
|
|
532
|
+
this.modal.postMessageToModal(type);
|
|
533
|
+
}
|
|
607
534
|
setupIframeListener() {
|
|
608
535
|
if (this.iframeMessageListener) {
|
|
609
536
|
window.removeEventListener("message", this.iframeMessageListener);
|
|
@@ -616,9 +543,13 @@ class VirtualTryOnWidget {
|
|
|
616
543
|
return;
|
|
617
544
|
}
|
|
618
545
|
switch (event.data.type) {
|
|
546
|
+
case "VTO_CONFIRM_READY":
|
|
547
|
+
this.sendPendingPrefillToModal();
|
|
548
|
+
break;
|
|
619
549
|
case "VTO_IMAGE_SELECTED":
|
|
620
550
|
if (event.data.image) {
|
|
621
551
|
this.lastModelImage = event.data.image;
|
|
552
|
+
this.postMessageToModal("VTO_SCAN_START");
|
|
622
553
|
this.startVirtualTryOn();
|
|
623
554
|
}
|
|
624
555
|
break;
|
|
@@ -644,142 +575,45 @@ class VirtualTryOnWidget {
|
|
|
644
575
|
console.warn("[WeWear VTO] No product images found.");
|
|
645
576
|
return;
|
|
646
577
|
}
|
|
647
|
-
const modal = document.querySelector(`.${CSS_CLASSES.MODAL}`);
|
|
648
|
-
if (modal && modal instanceof HTMLElement) {
|
|
649
|
-
modal.style.display = "none";
|
|
650
|
-
}
|
|
651
578
|
const container = this.getContainer();
|
|
652
|
-
if (container) {
|
|
653
|
-
showProductLoading(container, "Preparing your personalized look");
|
|
654
|
-
}
|
|
655
579
|
try {
|
|
656
|
-
const submitResponse = await this.
|
|
657
|
-
|
|
580
|
+
const submitResponse = await this.vtoApiClient.submitTryOn(this.lastModelImage, this.originalProductImages, {
|
|
581
|
+
modelTier: this.config.modelTier,
|
|
582
|
+
outfitID: this.config.outfitID,
|
|
583
|
+
});
|
|
584
|
+
let status = await this.vtoApiClient.fetchJobStatus(submitResponse.job_id);
|
|
658
585
|
while (status.status !== "COMPLETED" && status.status !== "FAILED") {
|
|
659
|
-
if (container instanceof HTMLElement) {
|
|
660
|
-
const statusMessage = "Preparing your personalized look";
|
|
661
|
-
showProductLoading(container, statusMessage);
|
|
662
|
-
}
|
|
663
586
|
await new Promise((r) => setTimeout(r, 3000));
|
|
664
|
-
status = await this.fetchJobStatus(submitResponse.job_id);
|
|
587
|
+
status = await this.vtoApiClient.fetchJobStatus(submitResponse.job_id);
|
|
665
588
|
}
|
|
666
589
|
if (status.status === "COMPLETED") {
|
|
667
|
-
const finalImage = await this.fetchJobImage(submitResponse.job_id);
|
|
590
|
+
const finalImage = await this.vtoApiClient.fetchJobImage(submitResponse.job_id);
|
|
668
591
|
if (finalImage) {
|
|
592
|
+
this.postMessageToModal("VTO_SCAN_COMPLETE");
|
|
593
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
669
594
|
this.replaceProductImage(finalImage);
|
|
670
595
|
}
|
|
671
596
|
if (container instanceof HTMLElement) {
|
|
672
|
-
removeStatusBadge(container);
|
|
673
597
|
this.showButtonContainer(container);
|
|
674
598
|
}
|
|
675
599
|
}
|
|
676
600
|
if (status.status === "FAILED") {
|
|
677
601
|
console.error("[WeWear VTO] VTO process failed:", status.message);
|
|
678
|
-
if (container instanceof HTMLElement) {
|
|
679
|
-
showStatusBadge(container, "We are experiencing some technical issues and apologise for any inconvenience caused. Please come back later again");
|
|
680
|
-
}
|
|
681
602
|
await new Promise((r) => setTimeout(r, 5000));
|
|
682
603
|
}
|
|
683
604
|
}
|
|
684
605
|
catch (error) {
|
|
685
606
|
console.error("[WeWear VTO] Error during virtual try-on process:", error);
|
|
686
|
-
if (container instanceof HTMLElement) {
|
|
687
|
-
showProductLoading(container, "We are experiencing technical issues and apologise for any inconvenience caused. Please come back later again");
|
|
688
|
-
}
|
|
689
607
|
await new Promise((r) => setTimeout(r, 5000));
|
|
690
608
|
}
|
|
691
609
|
finally {
|
|
610
|
+
this.postMessageToModal("VTO_SCAN_STOP");
|
|
692
611
|
if (container instanceof HTMLElement) {
|
|
693
|
-
removeProductLoading(container);
|
|
694
|
-
removeStatusBadge(container);
|
|
695
612
|
this.showButtonContainer(container);
|
|
696
613
|
}
|
|
697
614
|
this.closeModal();
|
|
698
615
|
}
|
|
699
616
|
}
|
|
700
|
-
async fetchVtoData(outfitId) {
|
|
701
|
-
try {
|
|
702
|
-
const vtoDataUrl = new URL("/api/vto/data", this.config.baseUrl);
|
|
703
|
-
vtoDataUrl.searchParams.set("outfit_id", outfitId);
|
|
704
|
-
const response = await fetch(vtoDataUrl.toString());
|
|
705
|
-
if (response.status === 404)
|
|
706
|
-
return null;
|
|
707
|
-
if (!response.ok) {
|
|
708
|
-
const errorText = await response.text();
|
|
709
|
-
console.warn(`[WeWear VTO] Failed to retrieve VTO data (${response.status}): ${errorText}`);
|
|
710
|
-
return null;
|
|
711
|
-
}
|
|
712
|
-
return (await response.json());
|
|
713
|
-
}
|
|
714
|
-
catch (error) {
|
|
715
|
-
console.warn("[WeWear VTO] Error retrieving VTO data:", error);
|
|
716
|
-
return null;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
async callVtoApi(modelImage, productImages) {
|
|
720
|
-
var _a;
|
|
721
|
-
const outfitID = (_a = this.config.outfitID) === null || _a === void 0 ? void 0 : _a.trim();
|
|
722
|
-
let resolvedProductImages = productImages;
|
|
723
|
-
let resolvedGeminiModel = null;
|
|
724
|
-
let resolvedAdditionalPrompt = null;
|
|
725
|
-
if (outfitID) {
|
|
726
|
-
const vtoData = await this.fetchVtoData(outfitID);
|
|
727
|
-
if (vtoData) {
|
|
728
|
-
if (Array.isArray(vtoData.images) && vtoData.images.length > 0) {
|
|
729
|
-
resolvedProductImages = vtoData.images;
|
|
730
|
-
}
|
|
731
|
-
if (typeof vtoData.gemini_model === "string" && vtoData.gemini_model) {
|
|
732
|
-
resolvedGeminiModel = vtoData.gemini_model;
|
|
733
|
-
}
|
|
734
|
-
if (typeof vtoData.additional_prompt === "string" &&
|
|
735
|
-
vtoData.additional_prompt) {
|
|
736
|
-
resolvedAdditionalPrompt = vtoData.additional_prompt;
|
|
737
|
-
}
|
|
738
|
-
console.log(`[WeWear VTO] Using stored VTO data for outfit_id=${outfitID}`);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
const formData = new FormData();
|
|
742
|
-
formData.append("model_image", modelImage, "model_image.png");
|
|
743
|
-
formData.append("product_image_urls", JSON.stringify(resolvedProductImages));
|
|
744
|
-
formData.append("page_url", window.location.href);
|
|
745
|
-
formData.append("model_tier", this.config.modelTier);
|
|
746
|
-
if (resolvedGeminiModel)
|
|
747
|
-
formData.append("gemini_model", resolvedGeminiModel);
|
|
748
|
-
if (resolvedAdditionalPrompt) {
|
|
749
|
-
formData.append("additional_prompt", resolvedAdditionalPrompt);
|
|
750
|
-
}
|
|
751
|
-
if (outfitID)
|
|
752
|
-
formData.append("outfit_id", outfitID);
|
|
753
|
-
const res = await fetch(`${this.config.baseUrl}/api/vto`, {
|
|
754
|
-
method: "POST",
|
|
755
|
-
body: formData,
|
|
756
|
-
});
|
|
757
|
-
if (!res.ok) {
|
|
758
|
-
const errorText = await res.text();
|
|
759
|
-
console.error("[WeWear VTO] API submission failed:", res.status, errorText);
|
|
760
|
-
throw new Error(`API submission failed: ${res.status}`);
|
|
761
|
-
}
|
|
762
|
-
return res.json();
|
|
763
|
-
}
|
|
764
|
-
async fetchJobStatus(jobId) {
|
|
765
|
-
const res = await fetch(`${this.config.baseUrl}/api/vto/status?job_id=${jobId}`);
|
|
766
|
-
if (!res.ok)
|
|
767
|
-
throw new Error(`Status check failed: ${res.status}`);
|
|
768
|
-
return res.json();
|
|
769
|
-
}
|
|
770
|
-
async fetchJobImage(jobId) {
|
|
771
|
-
const url = `${this.config.baseUrl}/api/vto/image?job_id=${jobId}`;
|
|
772
|
-
const res = await fetch(url);
|
|
773
|
-
if (res.status === 202)
|
|
774
|
-
throw new Error("202_PROCESSING");
|
|
775
|
-
if (!res.ok)
|
|
776
|
-
throw new Error(`Image fetch failed: ${res.status}`);
|
|
777
|
-
const blob = await res.blob();
|
|
778
|
-
return URL.createObjectURL(blob);
|
|
779
|
-
}
|
|
780
|
-
async refreshVirtualTryOn() {
|
|
781
|
-
await this.startVirtualTryOn(true);
|
|
782
|
-
}
|
|
783
617
|
replaceProductImage(imageUrl) {
|
|
784
618
|
try {
|
|
785
619
|
this.virtualTryOnImageUrl = imageUrl;
|
|
@@ -835,7 +669,7 @@ class VirtualTryOnWidget {
|
|
|
835
669
|
const buttonContainer = createButtonContainer(this.config.buttonPosition, this.virtualTryOnImageUrl !== null, async () => {
|
|
836
670
|
await this.handleTryOnClick();
|
|
837
671
|
}, async () => {
|
|
838
|
-
await this.
|
|
672
|
+
await this.handleRegenerateClick();
|
|
839
673
|
}, () => {
|
|
840
674
|
if (this.isShowingVirtualTryOn) {
|
|
841
675
|
this.showOriginalImage(container);
|
|
@@ -843,7 +677,6 @@ class VirtualTryOnWidget {
|
|
|
843
677
|
else {
|
|
844
678
|
this.showVirtualTryOnImage(container);
|
|
845
679
|
}
|
|
846
|
-
removeProductLoading(container);
|
|
847
680
|
this.updateButtonContainer(container);
|
|
848
681
|
}, this.isShowingVirtualTryOn);
|
|
849
682
|
container.appendChild(buttonContainer);
|
|
@@ -925,7 +758,6 @@ class VirtualTryOnWidget {
|
|
|
925
758
|
this.virtualTryOnImageUrl = null;
|
|
926
759
|
this.isShowingVirtualTryOn = false;
|
|
927
760
|
this.lastModelImage = null;
|
|
928
|
-
this.originalProductImageUrl = null;
|
|
929
761
|
this.originalProductImages = [];
|
|
930
762
|
console.log("[WeWear VTO] Widget destroyed successfully");
|
|
931
763
|
}
|