@akinon/pz-virtual-try-on 2.0.34-beta.0 → 2.0.34
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/CHANGELOG.md +4 -3
- package/README.md +96 -23
- package/package.json +3 -2
- package/src/data/endpoints.ts +50 -5
- package/src/hooks/use-virtual-try-on-async.ts +115 -43
- package/src/hooks/use-virtual-try-on.ts +49 -28
- package/src/index.ts +5 -0
- package/src/types/index.ts +48 -8
- package/src/utils/polling.ts +6 -3
- package/src/views/basket-async-modal.tsx +38 -0
- package/src/views/basket-virtual-try-on.tsx +107 -9
- package/src/views/main.tsx +63 -19
- package/src/views/virtual-try-on-multiple-modal.tsx +8 -2
- package/src/views/virtual-try-on-multiple-result-modal.tsx +36 -4
- package/src/views/virtual-try-on-product-selector.tsx +5 -1
- package/src/views/virtual-try-on-result-modal.tsx +40 -2
|
@@ -8,12 +8,17 @@ import type {
|
|
|
8
8
|
VirtualTryOnJobStatus
|
|
9
9
|
} from '../types';
|
|
10
10
|
import {
|
|
11
|
+
useVirtualTryOnSingleMutation,
|
|
11
12
|
useVirtualTryOnAsyncMutation,
|
|
12
13
|
useLazyGetVirtualTryOnJobStatusQuery,
|
|
13
14
|
useSubmitVirtualTryOnFeedbackMutation
|
|
14
15
|
} from '../data/endpoints';
|
|
15
16
|
import { VirtualTryOnPoller } from '../utils/polling';
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
hasLegalConsentAccepted,
|
|
19
|
+
setLegalConsentAccepted,
|
|
20
|
+
parseVirtualTryOnError
|
|
21
|
+
} from '../utils';
|
|
17
22
|
import { useImageCropper } from './use-image-cropper';
|
|
18
23
|
|
|
19
24
|
const extractCategoryIds = (
|
|
@@ -66,6 +71,13 @@ const extractCategoryIds = (
|
|
|
66
71
|
return categoryIds;
|
|
67
72
|
};
|
|
68
73
|
|
|
74
|
+
const resolveStartErrorMessage = (err: any, fallback: string): string => {
|
|
75
|
+
if (err?.status === 429 || err?.data?.code === 'TRY_ON_LIMIT_EXCEEDED') {
|
|
76
|
+
return parseVirtualTryOnError(err?.data?.error || err?.data?.message || '');
|
|
77
|
+
}
|
|
78
|
+
return err?.data?.message || err?.message || fallback;
|
|
79
|
+
};
|
|
80
|
+
|
|
69
81
|
export function useVirtualTryOnAsync(
|
|
70
82
|
products: BasketProduct | BasketProduct[],
|
|
71
83
|
categoryMapping: Record<string, number[]> = {}
|
|
@@ -85,11 +97,16 @@ export function useVirtualTryOnAsync(
|
|
|
85
97
|
const [feedbackStates, setFeedbackStates] = useState<
|
|
86
98
|
Record<number, 'positive' | 'negative' | null>
|
|
87
99
|
>({});
|
|
100
|
+
const [limitInfo, setLimitInfo] = useState<{
|
|
101
|
+
tryon_limit?: number;
|
|
102
|
+
used_count?: number;
|
|
103
|
+
} | null>(null);
|
|
88
104
|
const [isCropLoading, setIsCropLoading] = useState(false);
|
|
89
105
|
const [abortController, setAbortController] =
|
|
90
106
|
useState<AbortController | null>(null);
|
|
91
107
|
|
|
92
108
|
const pollerRef = useRef<VirtualTryOnPoller | null>(null);
|
|
109
|
+
const maxAttemptsRef = useRef(productsArray.length > 1 ? 150 : 60);
|
|
93
110
|
|
|
94
111
|
const cropperHook = useImageCropper(
|
|
95
112
|
setIsCropLoading,
|
|
@@ -99,8 +116,11 @@ export function useVirtualTryOnAsync(
|
|
|
99
116
|
() => setFileError('')
|
|
100
117
|
);
|
|
101
118
|
|
|
102
|
-
const [
|
|
119
|
+
const [startSingleTryOn, { isLoading: isStartingSingle }] =
|
|
120
|
+
useVirtualTryOnSingleMutation();
|
|
121
|
+
const [startAsyncTryOn, { isLoading: isStartingMultiple }] =
|
|
103
122
|
useVirtualTryOnAsyncMutation();
|
|
123
|
+
const isStarting = isStartingSingle || isStartingMultiple;
|
|
104
124
|
const [getJobStatus] = useLazyGetVirtualTryOnJobStatusQuery();
|
|
105
125
|
const [submitFeedback] = useSubmitVirtualTryOnFeedbackMutation();
|
|
106
126
|
|
|
@@ -198,6 +218,7 @@ export function useVirtualTryOnAsync(
|
|
|
198
218
|
setResults([]);
|
|
199
219
|
setPollingAttempts(0);
|
|
200
220
|
setFeedbackStates({});
|
|
221
|
+
setLimitInfo(null);
|
|
201
222
|
|
|
202
223
|
try {
|
|
203
224
|
const productsData = productsArray.map((product) => {
|
|
@@ -220,14 +241,18 @@ export function useVirtualTryOnAsync(
|
|
|
220
241
|
};
|
|
221
242
|
});
|
|
222
243
|
|
|
223
|
-
const requestData = {
|
|
224
|
-
products: productsData,
|
|
225
|
-
reference_image: uploadedImage
|
|
226
|
-
};
|
|
227
|
-
|
|
228
244
|
let response;
|
|
229
245
|
try {
|
|
230
|
-
response =
|
|
246
|
+
response =
|
|
247
|
+
productsData.length === 1
|
|
248
|
+
? await startSingleTryOn({
|
|
249
|
+
...productsData[0],
|
|
250
|
+
reference_image: uploadedImage
|
|
251
|
+
}).unwrap()
|
|
252
|
+
: await startAsyncTryOn({
|
|
253
|
+
products: productsData,
|
|
254
|
+
reference_image: uploadedImage
|
|
255
|
+
}).unwrap();
|
|
231
256
|
} catch (apiError: any) {
|
|
232
257
|
console.error('[useVirtualTryOnAsync] API Error:', apiError);
|
|
233
258
|
throw apiError;
|
|
@@ -239,16 +264,19 @@ export function useVirtualTryOnAsync(
|
|
|
239
264
|
return;
|
|
240
265
|
}
|
|
241
266
|
|
|
242
|
-
if (!response || !response.
|
|
267
|
+
if (!response || !response.process_id) {
|
|
243
268
|
throw new Error('Invalid response from server');
|
|
244
269
|
}
|
|
245
270
|
|
|
246
|
-
setReferenceUrl(response.
|
|
271
|
+
setReferenceUrl(response.process_id);
|
|
247
272
|
setJobStatus('pending');
|
|
248
273
|
|
|
274
|
+
const maxAttempts = productsData.length > 1 ? 150 : 60;
|
|
275
|
+
maxAttemptsRef.current = maxAttempts;
|
|
276
|
+
|
|
249
277
|
pollerRef.current = new VirtualTryOnPoller({
|
|
250
|
-
referenceUrl: response.
|
|
251
|
-
maxAttempts
|
|
278
|
+
referenceUrl: response.process_id,
|
|
279
|
+
maxAttempts,
|
|
252
280
|
interval: 2000,
|
|
253
281
|
abortSignal: controller.signal,
|
|
254
282
|
onStatusChange: (status, attempts) => {
|
|
@@ -262,8 +290,16 @@ export function useVirtualTryOnAsync(
|
|
|
262
290
|
|
|
263
291
|
setJobStatus('completed');
|
|
264
292
|
|
|
293
|
+
if (result.tryon_limit !== undefined) {
|
|
294
|
+
setLimitInfo({
|
|
295
|
+
tryon_limit: result.tryon_limit,
|
|
296
|
+
used_count: result.used_count
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
265
300
|
if (
|
|
266
|
-
result.result?.job_type === '
|
|
301
|
+
(result.result?.job_type === 'multiple_try_on' ||
|
|
302
|
+
result.result?.job_type === 'multiple') &&
|
|
267
303
|
result.result?.multiple?.products
|
|
268
304
|
) {
|
|
269
305
|
const apiProducts = result.result.multiple.products;
|
|
@@ -276,6 +312,7 @@ export function useVirtualTryOnAsync(
|
|
|
276
312
|
reference: apiResult.reference,
|
|
277
313
|
generated: apiResult.generated,
|
|
278
314
|
selected: apiResult.selected,
|
|
315
|
+
used_count: apiResult.used_count,
|
|
279
316
|
error: apiResult.error || null
|
|
280
317
|
};
|
|
281
318
|
}
|
|
@@ -326,7 +363,11 @@ export function useVirtualTryOnAsync(
|
|
|
326
363
|
},
|
|
327
364
|
onTimeout: () => {
|
|
328
365
|
setJobStatus('failed');
|
|
329
|
-
setError(
|
|
366
|
+
setError(
|
|
367
|
+
`Processing timeout after ${maxAttempts} attempts (${
|
|
368
|
+
(maxAttempts * 2) / 60
|
|
369
|
+
} minutes)`
|
|
370
|
+
);
|
|
330
371
|
}
|
|
331
372
|
});
|
|
332
373
|
|
|
@@ -346,7 +387,7 @@ export function useVirtualTryOnAsync(
|
|
|
346
387
|
}
|
|
347
388
|
|
|
348
389
|
setJobStatus('failed');
|
|
349
|
-
setError(err
|
|
390
|
+
setError(resolveStartErrorMessage(err, 'Failed to start try-on'));
|
|
350
391
|
} finally {
|
|
351
392
|
setAbortController(null);
|
|
352
393
|
}
|
|
@@ -354,6 +395,7 @@ export function useVirtualTryOnAsync(
|
|
|
354
395
|
uploadedImage,
|
|
355
396
|
productsArray,
|
|
356
397
|
categoryMapping,
|
|
398
|
+
startSingleTryOn,
|
|
357
399
|
startAsyncTryOn,
|
|
358
400
|
getJobStatus,
|
|
359
401
|
abortController
|
|
@@ -407,6 +449,7 @@ export function useVirtualTryOnAsync(
|
|
|
407
449
|
setError(null);
|
|
408
450
|
setFileError('');
|
|
409
451
|
setFeedbackStates({});
|
|
452
|
+
setLimitInfo(null);
|
|
410
453
|
setAbortController(null);
|
|
411
454
|
cropperHook.resetCrop();
|
|
412
455
|
}, [cropperHook, abortController]);
|
|
@@ -480,14 +523,18 @@ export function useVirtualTryOnAsync(
|
|
|
480
523
|
};
|
|
481
524
|
});
|
|
482
525
|
|
|
483
|
-
const requestData = {
|
|
484
|
-
products: productsData,
|
|
485
|
-
reference_image: uploadedImage
|
|
486
|
-
};
|
|
487
|
-
|
|
488
526
|
let response;
|
|
489
527
|
try {
|
|
490
|
-
response =
|
|
528
|
+
response =
|
|
529
|
+
productsData.length === 1
|
|
530
|
+
? await startSingleTryOn({
|
|
531
|
+
...productsData[0],
|
|
532
|
+
reference_image: uploadedImage
|
|
533
|
+
}).unwrap()
|
|
534
|
+
: await startAsyncTryOn({
|
|
535
|
+
products: productsData,
|
|
536
|
+
reference_image: uploadedImage
|
|
537
|
+
}).unwrap();
|
|
491
538
|
} catch (apiError: any) {
|
|
492
539
|
console.error('[useVirtualTryOnAsync] Retry API Error:', apiError);
|
|
493
540
|
throw apiError;
|
|
@@ -498,16 +545,19 @@ export function useVirtualTryOnAsync(
|
|
|
498
545
|
return;
|
|
499
546
|
}
|
|
500
547
|
|
|
501
|
-
if (!response || !response.
|
|
548
|
+
if (!response || !response.process_id) {
|
|
502
549
|
throw new Error('Invalid response from server');
|
|
503
550
|
}
|
|
504
551
|
|
|
505
|
-
setReferenceUrl(response.
|
|
552
|
+
setReferenceUrl(response.process_id);
|
|
506
553
|
setJobStatus('pending');
|
|
507
554
|
|
|
555
|
+
const maxAttempts = productsData.length > 1 ? 150 : 60;
|
|
556
|
+
maxAttemptsRef.current = maxAttempts;
|
|
557
|
+
|
|
508
558
|
pollerRef.current = new VirtualTryOnPoller({
|
|
509
|
-
referenceUrl: response.
|
|
510
|
-
maxAttempts
|
|
559
|
+
referenceUrl: response.process_id,
|
|
560
|
+
maxAttempts,
|
|
511
561
|
interval: 2000,
|
|
512
562
|
abortSignal: controller.signal,
|
|
513
563
|
onStatusChange: (status, attempts) => {
|
|
@@ -521,10 +571,18 @@ export function useVirtualTryOnAsync(
|
|
|
521
571
|
|
|
522
572
|
setJobStatus('completed');
|
|
523
573
|
|
|
574
|
+
if (result.tryon_limit !== undefined) {
|
|
575
|
+
setLimitInfo({
|
|
576
|
+
tryon_limit: result.tryon_limit,
|
|
577
|
+
used_count: result.used_count
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
524
581
|
let newResults: VirtualTryOnMultipleResult[] = [];
|
|
525
582
|
|
|
526
583
|
if (
|
|
527
|
-
result.result?.job_type === '
|
|
584
|
+
(result.result?.job_type === 'multiple_try_on' ||
|
|
585
|
+
result.result?.job_type === 'multiple') &&
|
|
528
586
|
result.result?.multiple?.products
|
|
529
587
|
) {
|
|
530
588
|
newResults = result.result.multiple.products.map((item) => ({
|
|
@@ -532,6 +590,7 @@ export function useVirtualTryOnAsync(
|
|
|
532
590
|
reference: item.reference,
|
|
533
591
|
generated: item.generated,
|
|
534
592
|
selected: item.selected,
|
|
593
|
+
used_count: item.used_count,
|
|
535
594
|
error: item.error || null
|
|
536
595
|
}));
|
|
537
596
|
} else {
|
|
@@ -589,7 +648,11 @@ export function useVirtualTryOnAsync(
|
|
|
589
648
|
},
|
|
590
649
|
onTimeout: () => {
|
|
591
650
|
setJobStatus('failed');
|
|
592
|
-
setError(
|
|
651
|
+
setError(
|
|
652
|
+
`Retry processing timeout after ${maxAttempts} attempts (${
|
|
653
|
+
(maxAttempts * 2) / 60
|
|
654
|
+
} minutes)`
|
|
655
|
+
);
|
|
593
656
|
}
|
|
594
657
|
});
|
|
595
658
|
|
|
@@ -608,7 +671,7 @@ export function useVirtualTryOnAsync(
|
|
|
608
671
|
}
|
|
609
672
|
|
|
610
673
|
setJobStatus('failed');
|
|
611
|
-
setError(err
|
|
674
|
+
setError(resolveStartErrorMessage(err, 'Failed to retry try-on'));
|
|
612
675
|
} finally {
|
|
613
676
|
setAbortController(null);
|
|
614
677
|
}
|
|
@@ -617,6 +680,7 @@ export function useVirtualTryOnAsync(
|
|
|
617
680
|
productsArray,
|
|
618
681
|
categoryMapping,
|
|
619
682
|
results,
|
|
683
|
+
startSingleTryOn,
|
|
620
684
|
startAsyncTryOn,
|
|
621
685
|
getJobStatus,
|
|
622
686
|
abortController,
|
|
@@ -660,22 +724,18 @@ export function useVirtualTryOnAsync(
|
|
|
660
724
|
.join('')
|
|
661
725
|
);
|
|
662
726
|
|
|
663
|
-
const
|
|
727
|
+
const requestData = {
|
|
664
728
|
sku: product.sku,
|
|
665
729
|
images: allImages,
|
|
666
730
|
category_ids: categoryIds,
|
|
667
731
|
category_paths: categoryPaths,
|
|
668
|
-
product_description: product.name
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
const requestData = {
|
|
672
|
-
products: [productData],
|
|
732
|
+
product_description: product.name,
|
|
673
733
|
reference_image: uploadedImage
|
|
674
734
|
};
|
|
675
735
|
|
|
676
736
|
let response;
|
|
677
737
|
try {
|
|
678
|
-
response = await
|
|
738
|
+
response = await startSingleTryOn(requestData).unwrap();
|
|
679
739
|
} catch (apiError: any) {
|
|
680
740
|
throw apiError;
|
|
681
741
|
}
|
|
@@ -684,15 +744,17 @@ export function useVirtualTryOnAsync(
|
|
|
684
744
|
return;
|
|
685
745
|
}
|
|
686
746
|
|
|
687
|
-
if (!response || !response.
|
|
747
|
+
if (!response || !response.process_id) {
|
|
688
748
|
throw new Error('Invalid response from server');
|
|
689
749
|
}
|
|
690
750
|
|
|
691
|
-
setReferenceUrl(response.
|
|
751
|
+
setReferenceUrl(response.process_id);
|
|
692
752
|
setJobStatus('pending');
|
|
693
753
|
|
|
754
|
+
maxAttemptsRef.current = 60;
|
|
755
|
+
|
|
694
756
|
pollerRef.current = new VirtualTryOnPoller({
|
|
695
|
-
referenceUrl: response.
|
|
757
|
+
referenceUrl: response.process_id,
|
|
696
758
|
maxAttempts: 60,
|
|
697
759
|
interval: 2000,
|
|
698
760
|
abortSignal: controller.signal,
|
|
@@ -707,10 +769,18 @@ export function useVirtualTryOnAsync(
|
|
|
707
769
|
|
|
708
770
|
setJobStatus('completed');
|
|
709
771
|
|
|
772
|
+
if (result.tryon_limit !== undefined) {
|
|
773
|
+
setLimitInfo({
|
|
774
|
+
tryon_limit: result.tryon_limit,
|
|
775
|
+
used_count: result.used_count
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
710
779
|
let newResult: VirtualTryOnMultipleResult | null = null;
|
|
711
780
|
|
|
712
781
|
if (
|
|
713
|
-
result.result?.job_type === '
|
|
782
|
+
(result.result?.job_type === 'multiple_try_on' ||
|
|
783
|
+
result.result?.job_type === 'multiple') &&
|
|
714
784
|
result.result?.multiple?.products?.[0]
|
|
715
785
|
) {
|
|
716
786
|
const item = result.result.multiple.products[0];
|
|
@@ -719,6 +789,7 @@ export function useVirtualTryOnAsync(
|
|
|
719
789
|
reference: item.reference,
|
|
720
790
|
generated: item.generated,
|
|
721
791
|
selected: item.selected,
|
|
792
|
+
used_count: item.used_count,
|
|
722
793
|
error: item.error || null
|
|
723
794
|
};
|
|
724
795
|
} else {
|
|
@@ -760,7 +831,7 @@ export function useVirtualTryOnAsync(
|
|
|
760
831
|
},
|
|
761
832
|
onTimeout: () => {
|
|
762
833
|
setJobStatus('failed');
|
|
763
|
-
setError('Retry processing timeout after 60 attempts (
|
|
834
|
+
setError('Retry processing timeout after 60 attempts (2 minutes)');
|
|
764
835
|
}
|
|
765
836
|
});
|
|
766
837
|
|
|
@@ -778,7 +849,7 @@ export function useVirtualTryOnAsync(
|
|
|
778
849
|
}
|
|
779
850
|
|
|
780
851
|
setJobStatus('failed');
|
|
781
|
-
setError(err
|
|
852
|
+
setError(resolveStartErrorMessage(err, 'Failed to retry try-on'));
|
|
782
853
|
} finally {
|
|
783
854
|
setAbortController(null);
|
|
784
855
|
}
|
|
@@ -787,7 +858,7 @@ export function useVirtualTryOnAsync(
|
|
|
787
858
|
uploadedImage,
|
|
788
859
|
productsArray,
|
|
789
860
|
categoryMapping,
|
|
790
|
-
|
|
861
|
+
startSingleTryOn,
|
|
791
862
|
getJobStatus,
|
|
792
863
|
abortController,
|
|
793
864
|
submitFeedbackForResult
|
|
@@ -813,6 +884,7 @@ export function useVirtualTryOnAsync(
|
|
|
813
884
|
fileError,
|
|
814
885
|
showLegalConsent,
|
|
815
886
|
feedbackStates,
|
|
887
|
+
limitInfo,
|
|
816
888
|
currentStep: 'upload' as const,
|
|
817
889
|
tryOnResult: null,
|
|
818
890
|
isEnabled: true,
|
|
@@ -844,6 +916,6 @@ export function useVirtualTryOnAsync(
|
|
|
844
916
|
hasResults: results.length > 0,
|
|
845
917
|
hasFailedResults,
|
|
846
918
|
failedResultsCount,
|
|
847
|
-
maxAttemptsReached: pollingAttempts >=
|
|
919
|
+
maxAttemptsReached: pollingAttempts >= maxAttemptsRef.current
|
|
848
920
|
};
|
|
849
921
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
2
2
|
import { Product } from '@akinon/next/types';
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
useVirtualTryOnSingleMutation,
|
|
5
5
|
useGetVirtualTryOnLimitedCategoriesQuery,
|
|
6
6
|
useSubmitVirtualTryOnFeedbackMutation,
|
|
7
7
|
useLazyGetVirtualTryOnJobStatusQuery
|
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
VirtualTryOnCache,
|
|
17
17
|
compressImageToUnder1MB,
|
|
18
18
|
convertWebPToJPEG,
|
|
19
|
-
validateImageAspectRatio
|
|
19
|
+
validateImageAspectRatio,
|
|
20
|
+
parseVirtualTryOnError
|
|
20
21
|
} from '../utils';
|
|
21
22
|
import { useImageCropper } from './use-image-cropper';
|
|
22
23
|
import type { VirtualTryOnResponse } from '../types';
|
|
@@ -94,6 +95,10 @@ export function useVirtualTryOn(product: Product) {
|
|
|
94
95
|
>(null);
|
|
95
96
|
const [abortController, setAbortController] =
|
|
96
97
|
useState<AbortController | null>(null);
|
|
98
|
+
const [limitInfo, setLimitInfo] = useState<{
|
|
99
|
+
tryon_limit?: number;
|
|
100
|
+
used_count?: number;
|
|
101
|
+
} | null>(null);
|
|
97
102
|
|
|
98
103
|
const localStorageEnabled = getVirtualTryOnEnabled();
|
|
99
104
|
const [cachedLimitedCategories, setCachedLimitedCategories] = useState<{
|
|
@@ -101,7 +106,7 @@ export function useVirtualTryOn(product: Product) {
|
|
|
101
106
|
} | null>(() => VirtualTryOnCache.retrieveLimitedCategories());
|
|
102
107
|
|
|
103
108
|
const [processVirtualTryOn, { isLoading: isProcessing }] =
|
|
104
|
-
|
|
109
|
+
useVirtualTryOnSingleMutation();
|
|
105
110
|
const [submitFeedback] = useSubmitVirtualTryOnFeedbackMutation();
|
|
106
111
|
const { data: limitedCategoriesData, isLoading: isLimitedCategoriesLoading } =
|
|
107
112
|
useGetVirtualTryOnLimitedCategoriesQuery(undefined, {
|
|
@@ -217,6 +222,7 @@ export function useVirtualTryOn(product: Product) {
|
|
|
217
222
|
setCurrentStep('processing');
|
|
218
223
|
setFileError('');
|
|
219
224
|
setError('');
|
|
225
|
+
setLimitInfo(null);
|
|
220
226
|
|
|
221
227
|
try {
|
|
222
228
|
const sku = product.sku || product.pk.toString();
|
|
@@ -252,16 +258,12 @@ export function useVirtualTryOn(product: Product) {
|
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
260
|
|
|
255
|
-
const
|
|
261
|
+
const requestData = {
|
|
256
262
|
sku,
|
|
257
263
|
images: productImages,
|
|
258
264
|
category_ids: categoryIds,
|
|
259
265
|
category_paths: categoryPaths,
|
|
260
|
-
product_description: product.name || ''
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const requestData = {
|
|
264
|
-
products: [productData],
|
|
266
|
+
product_description: product.name || '',
|
|
265
267
|
reference_image: uploadedImage
|
|
266
268
|
};
|
|
267
269
|
|
|
@@ -273,11 +275,11 @@ export function useVirtualTryOn(product: Product) {
|
|
|
273
275
|
throw apiError;
|
|
274
276
|
}
|
|
275
277
|
|
|
276
|
-
if (!asyncResponse || !asyncResponse.
|
|
278
|
+
if (!asyncResponse || !asyncResponse.process_id) {
|
|
277
279
|
throw new Error('Invalid response from server');
|
|
278
280
|
}
|
|
279
281
|
|
|
280
|
-
const pollForResult = async (
|
|
282
|
+
const pollForResult = async (processId: string, maxAttempts = 60) => {
|
|
281
283
|
let rateLimitRetries = 0;
|
|
282
284
|
const maxRateLimitRetries = 3;
|
|
283
285
|
|
|
@@ -300,8 +302,8 @@ export function useVirtualTryOn(product: Product) {
|
|
|
300
302
|
|
|
301
303
|
try {
|
|
302
304
|
const response = await fetch(
|
|
303
|
-
`/api/virtual-try-on?endpoint=job-status&
|
|
304
|
-
|
|
305
|
+
`/api/virtual-try-on?endpoint=job-status&process_id=${encodeURIComponent(
|
|
306
|
+
processId
|
|
305
307
|
)}`,
|
|
306
308
|
{
|
|
307
309
|
method: 'GET',
|
|
@@ -332,25 +334,35 @@ export function useVirtualTryOn(product: Product) {
|
|
|
332
334
|
const data = await response.json();
|
|
333
335
|
|
|
334
336
|
if (data.status === 'completed') {
|
|
335
|
-
let generated, selected;
|
|
337
|
+
let generated, selected, reference;
|
|
336
338
|
|
|
337
339
|
if (
|
|
338
|
-
data.result?.job_type === '
|
|
340
|
+
(data.result?.job_type === 'multiple_try_on' ||
|
|
341
|
+
data.result?.job_type === 'multiple') &&
|
|
339
342
|
data.result?.multiple?.products?.[0]
|
|
340
343
|
) {
|
|
341
344
|
generated = data.result.multiple.products[0].generated;
|
|
342
345
|
selected = data.result.multiple.products[0].selected || [];
|
|
346
|
+
reference = data.result.multiple.products[0].reference;
|
|
343
347
|
} else if (data.result?.single) {
|
|
344
348
|
generated = data.result.single.generated;
|
|
345
349
|
selected = data.result.single.selected || [];
|
|
350
|
+
reference = data.result.single.reference;
|
|
346
351
|
} else {
|
|
347
352
|
generated = data.generated;
|
|
348
353
|
selected = data.selected || [];
|
|
349
354
|
}
|
|
350
355
|
|
|
356
|
+
if (data.tryon_limit !== undefined) {
|
|
357
|
+
setLimitInfo({
|
|
358
|
+
tryon_limit: data.tryon_limit,
|
|
359
|
+
used_count: data.used_count
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
351
363
|
if (generated) {
|
|
352
364
|
setTryOnResult({
|
|
353
|
-
reference:
|
|
365
|
+
reference: reference || processId,
|
|
354
366
|
generated: generated,
|
|
355
367
|
selected: selected,
|
|
356
368
|
status: 'success'
|
|
@@ -361,8 +373,13 @@ export function useVirtualTryOn(product: Product) {
|
|
|
361
373
|
throw new Error('No generated image in completed response');
|
|
362
374
|
}
|
|
363
375
|
} else if (data.status === 'failed') {
|
|
376
|
+
const rawError = data.error || data.result?.error;
|
|
364
377
|
const errorMsg =
|
|
365
|
-
|
|
378
|
+
typeof rawError === 'string'
|
|
379
|
+
? parseVirtualTryOnError(rawError)
|
|
380
|
+
: rawError?.detail || rawError?.message
|
|
381
|
+
? parseVirtualTryOnError(rawError.detail || rawError.message)
|
|
382
|
+
: 'Processing failed';
|
|
366
383
|
setError(errorMsg);
|
|
367
384
|
setCurrentStep('processing');
|
|
368
385
|
cropperHook.closeCrop();
|
|
@@ -373,10 +390,10 @@ export function useVirtualTryOn(product: Product) {
|
|
|
373
390
|
throw pollError;
|
|
374
391
|
}
|
|
375
392
|
}
|
|
376
|
-
throw new Error('Processing timeout after 60 attempts');
|
|
393
|
+
throw new Error('Processing timeout after 60 attempts (2 minutes)');
|
|
377
394
|
};
|
|
378
395
|
|
|
379
|
-
await pollForResult(asyncResponse.
|
|
396
|
+
await pollForResult(asyncResponse.process_id);
|
|
380
397
|
} catch (error: any) {
|
|
381
398
|
if (
|
|
382
399
|
error?.name === 'AbortError' ||
|
|
@@ -387,16 +404,18 @@ export function useVirtualTryOn(product: Product) {
|
|
|
387
404
|
return;
|
|
388
405
|
}
|
|
389
406
|
|
|
390
|
-
|
|
391
|
-
error?.data?.
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
error?.data?.message ||
|
|
407
|
+
const isLimitExceeded =
|
|
408
|
+
error?.status === 429 || error?.data?.code === 'TRY_ON_LIMIT_EXCEEDED';
|
|
409
|
+
const errorMessage = isLimitExceeded
|
|
410
|
+
? parseVirtualTryOnError(
|
|
411
|
+
error?.data?.error || error?.data?.message || ''
|
|
412
|
+
)
|
|
413
|
+
: error?.data?.message ||
|
|
397
414
|
error?.message ||
|
|
398
|
-
'Virtual try-on processing failed'
|
|
399
|
-
|
|
415
|
+
'Virtual try-on processing failed';
|
|
416
|
+
|
|
417
|
+
setFileError(errorMessage);
|
|
418
|
+
setError(errorMessage);
|
|
400
419
|
setCurrentStep('processing');
|
|
401
420
|
cropperHook.closeCrop();
|
|
402
421
|
} finally {
|
|
@@ -439,6 +458,7 @@ export function useVirtualTryOn(product: Product) {
|
|
|
439
458
|
setError('');
|
|
440
459
|
setCurrentStep('upload');
|
|
441
460
|
setSelectedFeedback(null);
|
|
461
|
+
setLimitInfo(null);
|
|
442
462
|
cropperHook.resetCrop();
|
|
443
463
|
}, [cropperHook]);
|
|
444
464
|
|
|
@@ -490,6 +510,7 @@ export function useVirtualTryOn(product: Product) {
|
|
|
490
510
|
isEnabled,
|
|
491
511
|
isLimitedCategoriesLoading,
|
|
492
512
|
selectedFeedback,
|
|
513
|
+
limitInfo,
|
|
493
514
|
handleFileUpload,
|
|
494
515
|
processTryOn,
|
|
495
516
|
acceptLegalConsent,
|
package/src/index.ts
CHANGED
|
@@ -23,9 +23,12 @@ export { useBarcodeSearch } from './hooks/use-barcode-search';
|
|
|
23
23
|
export { useVirtualTryOnAsync } from './hooks/use-virtual-try-on-async';
|
|
24
24
|
|
|
25
25
|
export {
|
|
26
|
+
useVirtualTryOnSingleMutation,
|
|
26
27
|
useVirtualTryOnAsyncMutation,
|
|
27
28
|
useGetVirtualTryOnJobStatusQuery,
|
|
28
29
|
useLazyGetVirtualTryOnJobStatusQuery,
|
|
30
|
+
useGetTryOnUsageMutation,
|
|
31
|
+
useSubmitTryOnEventMutation,
|
|
29
32
|
useGetVirtualTryOnLimitedCategoriesQuery,
|
|
30
33
|
useSubmitVirtualTryOnFeedbackMutation
|
|
31
34
|
} from './data/endpoints';
|
|
@@ -48,6 +51,8 @@ export type {
|
|
|
48
51
|
VirtualTryOnJobStatusResponse,
|
|
49
52
|
VirtualTryOnMultipleResult,
|
|
50
53
|
VirtualTryOnCompatibilityError,
|
|
54
|
+
TryOnUsageResponse,
|
|
55
|
+
TryOnEventResponse,
|
|
51
56
|
BasketProduct,
|
|
52
57
|
VirtualTryOnMultipleModalProps,
|
|
53
58
|
BarcodeFormat,
|