@akinon/pz-similar-products 1.92.0-rc.31 → 1.92.0-rc.32
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 +6 -0
- package/README.md +202 -0
- package/package.json +1 -1
- package/src/data/endpoints.ts +14 -5
- package/src/hooks/use-similar-products.ts +148 -23
- package/src/types/index.ts +51 -0
- package/src/utils/index.ts +38 -3
- package/src/views/filters.tsx +27 -3
- package/src/views/header-image-search-feature.tsx +9 -1
- package/src/views/image-search-button.tsx +20 -18
- package/src/views/main.tsx +82 -5
- package/src/views/product-image-search-feature.tsx +7 -2
- package/src/views/search-button.tsx +12 -2
- package/src/views/search-modal.tsx +169 -10
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
- [Features](#markdown-header-features)
|
|
12
12
|
- [Installation](#markdown-header-installation)
|
|
13
|
+
- [Icon Customization](#markdown-header-icon-customization)
|
|
13
14
|
- [Quick Start](#markdown-header-quick-start)
|
|
14
15
|
- [Available Components](#markdown-header-available-components)
|
|
15
16
|
- [Configuration](#markdown-header-configuration)
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
|
|
65
66
|
- **🖼️ Visual Search**: AI-powered image-based product discovery
|
|
66
67
|
- **✂️ Image Cropping**: Built-in cropping functionality with manual confirmation for precise searches
|
|
68
|
+
- **🔤 Text Search**: Search similar products by text description with customizable input
|
|
67
69
|
- **🎛️ Advanced Filtering**: Dynamic facet-based filtering system
|
|
68
70
|
- **📄 Multiple Pagination Modes**: Traditional pagination, load more button, and infinite scroll
|
|
69
71
|
- **🎨 Granular Customization**: 50+ style targets and 30+ render points
|
|
@@ -79,6 +81,92 @@ npx @akinon/projectzero@latest --plugins
|
|
|
79
81
|
|
|
80
82
|
Select `pz-similar-products` from the plugin list during installation.
|
|
81
83
|
|
|
84
|
+
## Icon Customization
|
|
85
|
+
|
|
86
|
+
The plugin provides complete control over icon appearance and behavior through two complementary approaches:
|
|
87
|
+
|
|
88
|
+
### Icon Names (`iconNames`)
|
|
89
|
+
|
|
90
|
+
Change which icons are displayed by specifying different icon names:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
const iconCustomization = {
|
|
94
|
+
iconNames: {
|
|
95
|
+
searchButton: 'magnifier', // Search button (default: 'search')
|
|
96
|
+
modalClose: 'x', // Modal close buttons (default: 'close')
|
|
97
|
+
filter: 'funnel', // Filter button (default: 'filter')
|
|
98
|
+
filterRemove: 'trash', // Filter tag remove buttons (default: 'close')
|
|
99
|
+
modalSearch: 'search-outline', // Modal search button (default: 'search')
|
|
100
|
+
modalSearchClear: 'clear', // Search input clear button (default: 'close')
|
|
101
|
+
imageSearchButton: 'upload' // Image search button (default: 'search')
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Icon Styles (`customStyles`)
|
|
107
|
+
|
|
108
|
+
Customize the visual appearance of icons with CSS classes:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
const iconStyling = {
|
|
112
|
+
customStyles: {
|
|
113
|
+
searchButtonIcon: 'fill-blue-500 hover:fill-blue-600 transition-colors',
|
|
114
|
+
modalCloseIcon: 'text-red-500 hover:text-red-700 w-4 h-4',
|
|
115
|
+
filterIcon: 'text-purple-500 w-5 h-5',
|
|
116
|
+
filterRemoveIcon: 'text-orange-500 hover:text-orange-700 w-3 h-3',
|
|
117
|
+
imageSearchButtonIcon: 'text-blue-500 hover:text-blue-600 w-5 h-5'
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Complete Icon Customization Example
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
const completeIconSettings = {
|
|
126
|
+
// Change icon names
|
|
127
|
+
iconNames: {
|
|
128
|
+
searchButton: 'search-alt',
|
|
129
|
+
modalClose: 'times',
|
|
130
|
+
filter: 'filter-alt',
|
|
131
|
+
filterRemove: 'remove',
|
|
132
|
+
modalSearch: 'magnifying-glass',
|
|
133
|
+
modalSearchClear: 'clear-alt',
|
|
134
|
+
imageSearchButton: 'upload-cloud'
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Style the icons
|
|
138
|
+
customStyles: {
|
|
139
|
+
searchButtonIcon: 'fill-indigo-500 hover:fill-indigo-600 transition-all duration-200',
|
|
140
|
+
modalCloseIcon: 'text-gray-500 hover:text-gray-700 w-5 h-5',
|
|
141
|
+
filterIcon: 'text-blue-500 hover:text-blue-600 w-4 h-4',
|
|
142
|
+
filterRemoveIcon: 'text-red-400 hover:text-red-600 w-3 h-3 transition-colors',
|
|
143
|
+
modalSearchIcon: 'text-green-500 hover:text-green-600',
|
|
144
|
+
modalSearchClearIcon: 'text-gray-400 hover:text-gray-600',
|
|
145
|
+
imageSearchButtonIcon: 'text-indigo-500 hover:text-indigo-600 w-5 h-5'
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Usage
|
|
150
|
+
<PluginModule
|
|
151
|
+
component={Component.ProductImageSearchFeature}
|
|
152
|
+
props={{
|
|
153
|
+
product,
|
|
154
|
+
activeIndex,
|
|
155
|
+
settings: completeIconSettings
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Available Icon Customization Points
|
|
161
|
+
|
|
162
|
+
- **Search Button** (`searchButton`): Main search button icon in product images
|
|
163
|
+
- **Modal Close** (`modalClose`): Close buttons in modals and filter tags
|
|
164
|
+
- **Filter** (`filter`): Filter toggle button icon
|
|
165
|
+
- **Filter Remove** (`filterRemove`): Remove icons in active filter tags
|
|
166
|
+
- **Modal Search** (`modalSearch`): Search button icon in text search input
|
|
167
|
+
- **Modal Search Clear** (`modalSearchClear`): Clear button icon in text search input
|
|
168
|
+
- **Image Search Button** (`imageSearchButton`): Camera/upload icon in header search feature
|
|
169
|
+
|
|
82
170
|
## Quick Start
|
|
83
171
|
|
|
84
172
|
### Plugin Module Integration
|
|
@@ -167,6 +255,7 @@ interface SimilarProductsSettings {
|
|
|
167
255
|
resultsPerPage?: number; // Products per page (default: 20)
|
|
168
256
|
enableCropping?: boolean; // Enable image cropping (default: true)
|
|
169
257
|
enableFileUpload?: boolean; // Enable file upload (default: true)
|
|
258
|
+
enableTextSearch?: boolean; // Enable text search input (default: false)
|
|
170
259
|
|
|
171
260
|
// Pagination settings
|
|
172
261
|
paginationType?: 'pagination' | 'load-more' | 'infinite-scroll'; // Pagination mode (default: 'pagination')
|
|
@@ -199,6 +288,21 @@ interface SimilarProductsSettings {
|
|
|
199
288
|
itemCount?: string;
|
|
200
289
|
sortDropdown?: string;
|
|
201
290
|
filterToggleButton?: string;
|
|
291
|
+
|
|
292
|
+
// Text search input
|
|
293
|
+
modalSearchInput?: string;
|
|
294
|
+
modalSearchContainer?: string;
|
|
295
|
+
modalSearchButton?: string;
|
|
296
|
+
modalSearchIcon?: string;
|
|
297
|
+
modalSearchClearButton?: string;
|
|
298
|
+
modalSearchClearIcon?: string;
|
|
299
|
+
|
|
300
|
+
// Icon customization
|
|
301
|
+
searchButtonIcon?: string; // Search button icon styling
|
|
302
|
+
modalCloseIcon?: string; // Modal close button icons styling
|
|
303
|
+
filterIcon?: string; // Filter button icon styling
|
|
304
|
+
filterRemoveIcon?: string; // Filter tag remove icons styling
|
|
305
|
+
imageSearchButtonIcon?: string; // Image search button icon styling
|
|
202
306
|
|
|
203
307
|
// Filter sidebar mobile
|
|
204
308
|
filterSidebarMobileHeader?: string;
|
|
@@ -286,6 +390,17 @@ interface SimilarProductsSettings {
|
|
|
286
390
|
cropSelectionHighlight?: string;
|
|
287
391
|
};
|
|
288
392
|
|
|
393
|
+
// Icon name customization
|
|
394
|
+
iconNames?: {
|
|
395
|
+
searchButton?: string; // Search button icon name (default: 'search')
|
|
396
|
+
modalClose?: string; // Modal close button icon name (default: 'close')
|
|
397
|
+
filter?: string; // Filter button icon name (default: 'filter')
|
|
398
|
+
filterRemove?: string; // Filter tag remove icon name (default: 'close')
|
|
399
|
+
modalSearch?: string; // Modal search button icon name (default: 'search')
|
|
400
|
+
modalSearchClear?: string; // Modal search clear button icon name (default: 'close')
|
|
401
|
+
imageSearchButton?: string; // Image search button icon name (default: 'search')
|
|
402
|
+
};
|
|
403
|
+
|
|
289
404
|
// 25+ render functions for granular control
|
|
290
405
|
customRenderers?: {
|
|
291
406
|
// Component-level renderers (full control)
|
|
@@ -304,6 +419,8 @@ interface SimilarProductsSettings {
|
|
|
304
419
|
renderItemCount?: (props) => React.ReactNode;
|
|
305
420
|
renderSortDropdown?: (props) => React.ReactNode;
|
|
306
421
|
renderFilterToggleButton?: (props) => React.ReactNode;
|
|
422
|
+
renderModalSearchInput?: (props) => React.ReactNode;
|
|
423
|
+
renderSearchIcon?: (props) => React.ReactNode;
|
|
307
424
|
// ... see examples for complete list
|
|
308
425
|
};
|
|
309
426
|
filterSidebar?: {
|
|
@@ -341,6 +458,91 @@ interface SimilarProductsSettings {
|
|
|
341
458
|
}
|
|
342
459
|
```
|
|
343
460
|
|
|
461
|
+
## Text Search Integration
|
|
462
|
+
|
|
463
|
+
The plugin features a built-in text search functionality that allows users to search for similar products using descriptive text.
|
|
464
|
+
|
|
465
|
+
### Text Search Features
|
|
466
|
+
|
|
467
|
+
1. **Search Input**: Appears only when API results are available
|
|
468
|
+
2. **Enter Key Support**: Press Enter to trigger search
|
|
469
|
+
3. **Search Icon**: Clickable icon for mobile users
|
|
470
|
+
4. **Clear Button**: X button to clear text and trigger search without text
|
|
471
|
+
5. **Custom Styling**: Fully customizable input, button, and icon
|
|
472
|
+
6. **Custom Icon**: Replaceable search icon via render functions
|
|
473
|
+
|
|
474
|
+
### Basic Text Search Configuration
|
|
475
|
+
|
|
476
|
+
```tsx
|
|
477
|
+
const textSearchSettings = {
|
|
478
|
+
enableTextSearch: true, // Enable text search input
|
|
479
|
+
|
|
480
|
+
customStyles: {
|
|
481
|
+
modalSearchContainer: 'relative flex items-center',
|
|
482
|
+
modalSearchInput: 'h-10 px-4 pr-20 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500',
|
|
483
|
+
modalSearchButton: 'absolute right-2 top-1/2 -translate-y-1/2 p-2 hover:bg-gray-100 rounded-md',
|
|
484
|
+
modalSearchIcon: 'text-gray-500',
|
|
485
|
+
modalSearchClearButton: 'absolute right-12 top-1/2 -translate-y-1/2 p-2 hover:bg-gray-100 rounded-md',
|
|
486
|
+
modalSearchClearIcon: 'text-gray-400 hover:text-gray-600'
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Custom Search Icon
|
|
492
|
+
|
|
493
|
+
```tsx
|
|
494
|
+
const customIconSettings = {
|
|
495
|
+
enableTextSearch: true,
|
|
496
|
+
|
|
497
|
+
customRenderers: {
|
|
498
|
+
render: {
|
|
499
|
+
modal: {
|
|
500
|
+
renderSearchIcon: ({ disabled, onClick }) => (
|
|
501
|
+
<svg
|
|
502
|
+
className="w-5 h-5 text-blue-500 cursor-pointer hover:text-blue-700"
|
|
503
|
+
onClick={onClick}
|
|
504
|
+
fill="none"
|
|
505
|
+
stroke="currentColor"
|
|
506
|
+
viewBox="0 0 24 24"
|
|
507
|
+
>
|
|
508
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
509
|
+
</svg>
|
|
510
|
+
),
|
|
511
|
+
renderModalSearchInput: ({ searchText, setSearchText, isLoading, placeholder, onSearch }) => (
|
|
512
|
+
<div className="relative">
|
|
513
|
+
<input
|
|
514
|
+
type="text"
|
|
515
|
+
value={searchText}
|
|
516
|
+
onChange={(e) => setSearchText(e.target.value)}
|
|
517
|
+
onKeyDown={(e) => e.key === 'Enter' && onSearch && onSearch()}
|
|
518
|
+
placeholder={placeholder}
|
|
519
|
+
className="w-full h-10 px-4 pr-20 border-2 border-blue-300 rounded-lg focus:border-blue-500"
|
|
520
|
+
disabled={isLoading}
|
|
521
|
+
/>
|
|
522
|
+
{searchText && (
|
|
523
|
+
<button
|
|
524
|
+
onClick={() => setSearchText('')}
|
|
525
|
+
disabled={isLoading}
|
|
526
|
+
className="absolute right-12 top-1/2 -translate-y-1/2 p-2 text-gray-400 hover:text-gray-600"
|
|
527
|
+
>
|
|
528
|
+
✕
|
|
529
|
+
</button>
|
|
530
|
+
)}
|
|
531
|
+
<button
|
|
532
|
+
onClick={onSearch}
|
|
533
|
+
disabled={isLoading}
|
|
534
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-blue-500 hover:text-blue-700"
|
|
535
|
+
>
|
|
536
|
+
🔍
|
|
537
|
+
</button>
|
|
538
|
+
</div>
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
```
|
|
545
|
+
|
|
344
546
|
## Image Cropping with Manual Confirmation
|
|
345
547
|
|
|
346
548
|
The plugin features an advanced image cropping system with manual confirmation for precise control over search queries.
|
package/package.json
CHANGED
package/src/data/endpoints.ts
CHANGED
|
@@ -27,9 +27,9 @@ export const similarProductsApi = api.injectEndpoints({
|
|
|
27
27
|
endpoints: (build) => ({
|
|
28
28
|
getSimilarProductsByUrl: build.query<
|
|
29
29
|
SimilarProductsResponse,
|
|
30
|
-
{ url: string; limit?: number; excluded_product_ids?: number[] }
|
|
30
|
+
{ url: string; limit?: number; excluded_product_ids?: number[]; text?: string }
|
|
31
31
|
>({
|
|
32
|
-
query: ({ url, limit = 20, excluded_product_ids }) => {
|
|
32
|
+
query: ({ url, limit = 20, excluded_product_ids, text }) => {
|
|
33
33
|
const params = new URLSearchParams();
|
|
34
34
|
params.append('limit', String(limit));
|
|
35
35
|
params.append('url', url);
|
|
@@ -38,6 +38,10 @@ export const similarProductsApi = api.injectEndpoints({
|
|
|
38
38
|
params.append('excluded_product_ids', excluded_product_ids.join(','));
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
if (text) {
|
|
42
|
+
params.append('text', text);
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
return {
|
|
42
46
|
url: `/api/similar-products?${params.toString()}`,
|
|
43
47
|
method: 'GET',
|
|
@@ -50,12 +54,17 @@ export const similarProductsApi = api.injectEndpoints({
|
|
|
50
54
|
|
|
51
55
|
getSimilarProductsByImage: build.mutation<
|
|
52
56
|
SimilarProductsResponse,
|
|
53
|
-
{ image: string; limit?: number; excluded_product_ids?: number[] }
|
|
57
|
+
{ image: string; limit?: number; excluded_product_ids?: number[]; text?: string }
|
|
54
58
|
>({
|
|
55
|
-
query: ({ image, limit = 20, excluded_product_ids }) => {
|
|
59
|
+
query: ({ image, limit = 20, excluded_product_ids, text }) => {
|
|
56
60
|
const params = new URLSearchParams();
|
|
57
61
|
params.append('limit', String(limit));
|
|
58
62
|
|
|
63
|
+
const bodyData: any = { image, excluded_product_ids };
|
|
64
|
+
if (text) {
|
|
65
|
+
bodyData.text = text;
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
return {
|
|
60
69
|
url: `/api/similar-products?${params.toString()}`,
|
|
61
70
|
method: 'POST',
|
|
@@ -63,7 +72,7 @@ export const similarProductsApi = api.injectEndpoints({
|
|
|
63
72
|
'Content-Type': 'application/json',
|
|
64
73
|
Accept: 'application/json'
|
|
65
74
|
},
|
|
66
|
-
body: JSON.stringify(
|
|
75
|
+
body: JSON.stringify(bodyData)
|
|
67
76
|
};
|
|
68
77
|
}
|
|
69
78
|
}),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
2
|
import { Product } from '@akinon/next/types';
|
|
3
3
|
import {
|
|
4
4
|
useLazyGetSimilarProductsByUrlQuery,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
validateImageFromDataUrl,
|
|
14
14
|
type ImageValidationResult
|
|
15
15
|
} from '../utils/image-validation';
|
|
16
|
+
import { debounce } from '../utils';
|
|
16
17
|
|
|
17
18
|
type SearchResults = SimilarProductsListResponse;
|
|
18
19
|
|
|
@@ -24,6 +25,7 @@ export function useSimilarProducts(product: Product) {
|
|
|
24
25
|
const { t } = useLocalization();
|
|
25
26
|
const [currentImageUrl, setCurrentImageUrl] = useState('');
|
|
26
27
|
const [fileError, setFileError] = useState('');
|
|
28
|
+
const [searchText, setSearchText] = useState('');
|
|
27
29
|
const [searchResults, setSearchResults] = useState<SearchResults | null>(
|
|
28
30
|
null
|
|
29
31
|
);
|
|
@@ -33,6 +35,16 @@ export function useSimilarProducts(product: Product) {
|
|
|
33
35
|
const [isCropProcessing, setIsCropProcessing] = useState(false);
|
|
34
36
|
const [loadedPages, setLoadedPages] = useState<Set<number>>(new Set([1]));
|
|
35
37
|
const [allLoadedProducts, setAllLoadedProducts] = useState<Product[]>([]);
|
|
38
|
+
const [currentImageBase64, setCurrentImageBase64] = useState<string>('');
|
|
39
|
+
const [hasCroppedImage, setHasCroppedImage] = useState(false);
|
|
40
|
+
|
|
41
|
+
const searchTextRef = useRef<string>('');
|
|
42
|
+
const hasUploadedImageRef = useRef<boolean>(false);
|
|
43
|
+
|
|
44
|
+
const setSearchTextWithRef = useCallback((text: string) => {
|
|
45
|
+
setSearchText(text);
|
|
46
|
+
searchTextRef.current = text;
|
|
47
|
+
}, []);
|
|
36
48
|
|
|
37
49
|
const [fetchSimilarProductsByUrl, { isLoading: isUrlSearchLoading }] =
|
|
38
50
|
useLazyGetSimilarProductsByUrlQuery();
|
|
@@ -426,17 +438,24 @@ export function useSimilarProducts(product: Product) {
|
|
|
426
438
|
);
|
|
427
439
|
|
|
428
440
|
const fetchSimilarProductsByImageUrl = useCallback(
|
|
429
|
-
async (imageUrl: string) => {
|
|
441
|
+
async (imageUrl: string, overrideText?: string) => {
|
|
430
442
|
setFileError('');
|
|
443
|
+
|
|
431
444
|
try {
|
|
432
445
|
const productPk = product?.pk;
|
|
433
446
|
const excludedIds = productPk ? [productPk] : undefined;
|
|
434
447
|
|
|
435
|
-
const
|
|
448
|
+
const textToUse =
|
|
449
|
+
overrideText !== undefined ? overrideText : searchTextRef.current;
|
|
450
|
+
|
|
451
|
+
const requestParams = {
|
|
436
452
|
url: imageUrl,
|
|
437
453
|
limit: 20,
|
|
438
|
-
excluded_product_ids: excludedIds
|
|
439
|
-
|
|
454
|
+
excluded_product_ids: excludedIds,
|
|
455
|
+
text: textToUse || undefined
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const result = await fetchSimilarProductsByUrl(requestParams).unwrap();
|
|
440
459
|
|
|
441
460
|
await handleSearchResults(result);
|
|
442
461
|
return result;
|
|
@@ -450,6 +469,8 @@ export function useSimilarProducts(product: Product) {
|
|
|
450
469
|
[
|
|
451
470
|
fetchSimilarProductsByUrl,
|
|
452
471
|
product?.pk,
|
|
472
|
+
searchText,
|
|
473
|
+
searchTextRef,
|
|
453
474
|
handleSearchResults,
|
|
454
475
|
updateResultsAndKey,
|
|
455
476
|
createEmptySearchResults,
|
|
@@ -462,31 +483,62 @@ export function useSimilarProducts(product: Product) {
|
|
|
462
483
|
if (product?.productimage_set?.length > 0) {
|
|
463
484
|
const initialImageUrl = product.productimage_set[0].image;
|
|
464
485
|
setCurrentImageUrl(initialImageUrl);
|
|
486
|
+
setHasCroppedImage(false);
|
|
487
|
+
setCurrentImageBase64('');
|
|
488
|
+
setHasUploadedImage(false);
|
|
489
|
+
hasUploadedImageRef.current = false;
|
|
465
490
|
}
|
|
466
491
|
}, [product]);
|
|
467
492
|
|
|
468
493
|
const fetchSimilarProductsByBase64 = useCallback(
|
|
469
|
-
async (
|
|
494
|
+
async (
|
|
495
|
+
base64Image: string,
|
|
496
|
+
overrideText?: string,
|
|
497
|
+
forceExclude?: boolean
|
|
498
|
+
) => {
|
|
470
499
|
const base64Data = base64Image.startsWith('data:')
|
|
471
500
|
? base64Image.split(',')[1]
|
|
472
501
|
: base64Image;
|
|
473
502
|
|
|
503
|
+
const textToUse =
|
|
504
|
+
overrideText !== undefined ? overrideText : searchTextRef.current;
|
|
505
|
+
|
|
506
|
+
const shouldExclude =
|
|
507
|
+
forceExclude !== undefined ? forceExclude : !hasUploadedImage;
|
|
508
|
+
|
|
509
|
+
const requestData = {
|
|
510
|
+
image: base64Data,
|
|
511
|
+
limit: 20,
|
|
512
|
+
excluded_product_ids: shouldExclude
|
|
513
|
+
? product?.pk
|
|
514
|
+
? [product.pk]
|
|
515
|
+
: undefined
|
|
516
|
+
: undefined,
|
|
517
|
+
text: textToUse || undefined
|
|
518
|
+
};
|
|
519
|
+
|
|
474
520
|
return handleImageSearch(
|
|
475
|
-
async () =>
|
|
476
|
-
getSimilarProductsByImage({
|
|
477
|
-
image: base64Data,
|
|
478
|
-
limit: 20
|
|
479
|
-
}).unwrap(),
|
|
521
|
+
async () => getSimilarProductsByImage(requestData).unwrap(),
|
|
480
522
|
base64Image,
|
|
481
523
|
'Image search'
|
|
482
524
|
);
|
|
483
525
|
},
|
|
484
|
-
[
|
|
526
|
+
[
|
|
527
|
+
getSimilarProductsByImage,
|
|
528
|
+
searchText,
|
|
529
|
+
searchTextRef,
|
|
530
|
+
handleImageSearch,
|
|
531
|
+
hasUploadedImage,
|
|
532
|
+
product?.pk
|
|
533
|
+
]
|
|
485
534
|
);
|
|
486
535
|
|
|
487
536
|
const fetchSimilarProductsByImageCrop = useCallback(
|
|
488
|
-
async (dataString: string) => {
|
|
537
|
+
async (dataString: string, excludeCurrentProduct: boolean = true) => {
|
|
489
538
|
setFileError('');
|
|
539
|
+
|
|
540
|
+
setCurrentImageBase64(dataString);
|
|
541
|
+
setHasCroppedImage(true);
|
|
490
542
|
if (dataString.startsWith('data:application/x-cors-fallback;base64,')) {
|
|
491
543
|
try {
|
|
492
544
|
const fallbackDataEncoded = dataString.replace(
|
|
@@ -549,9 +601,12 @@ export function useSimilarProducts(product: Product) {
|
|
|
549
601
|
getSimilarProductsByImage({
|
|
550
602
|
image: base64Data,
|
|
551
603
|
limit: 20,
|
|
552
|
-
excluded_product_ids:
|
|
553
|
-
?
|
|
554
|
-
|
|
604
|
+
excluded_product_ids: excludeCurrentProduct
|
|
605
|
+
? product?.pk
|
|
606
|
+
? [product.pk]
|
|
607
|
+
: undefined
|
|
608
|
+
: undefined,
|
|
609
|
+
text: searchTextRef.current || undefined
|
|
555
610
|
}).unwrap(),
|
|
556
611
|
croppedBase64,
|
|
557
612
|
'Proxy crop search'
|
|
@@ -601,7 +656,12 @@ export function useSimilarProducts(product: Product) {
|
|
|
601
656
|
getSimilarProductsByImage({
|
|
602
657
|
image: base64Data,
|
|
603
658
|
limit: 20,
|
|
604
|
-
excluded_product_ids:
|
|
659
|
+
excluded_product_ids: excludeCurrentProduct
|
|
660
|
+
? product?.pk
|
|
661
|
+
? [product.pk]
|
|
662
|
+
: undefined
|
|
663
|
+
: undefined,
|
|
664
|
+
text: searchTextRef.current || undefined
|
|
605
665
|
}).unwrap(),
|
|
606
666
|
dataString,
|
|
607
667
|
'Image crop search'
|
|
@@ -609,11 +669,14 @@ export function useSimilarProducts(product: Product) {
|
|
|
609
669
|
},
|
|
610
670
|
[
|
|
611
671
|
getSimilarProductsByImage,
|
|
672
|
+
searchText,
|
|
673
|
+
searchTextRef,
|
|
612
674
|
handleImageSearch,
|
|
613
675
|
product,
|
|
614
676
|
fetchSimilarProductsByImageUrl,
|
|
615
677
|
setFileError,
|
|
616
|
-
t
|
|
678
|
+
t,
|
|
679
|
+
hasUploadedImage
|
|
617
680
|
]
|
|
618
681
|
);
|
|
619
682
|
|
|
@@ -624,6 +687,8 @@ export function useSimilarProducts(product: Product) {
|
|
|
624
687
|
const file = event.target.files?.[0];
|
|
625
688
|
if (!file) return;
|
|
626
689
|
|
|
690
|
+
setSearchTextWithRef('');
|
|
691
|
+
|
|
627
692
|
try {
|
|
628
693
|
let processedFile = file;
|
|
629
694
|
|
|
@@ -650,6 +715,9 @@ export function useSimilarProducts(product: Product) {
|
|
|
650
715
|
const dataUrl = e.target?.result as string;
|
|
651
716
|
setCurrentImageUrl(dataUrl);
|
|
652
717
|
setHasUploadedImage(true);
|
|
718
|
+
hasUploadedImageRef.current = true;
|
|
719
|
+
setHasCroppedImage(false);
|
|
720
|
+
setCurrentImageBase64('');
|
|
653
721
|
|
|
654
722
|
const metadataValidation = await validateImageFromDataUrl(dataUrl);
|
|
655
723
|
if (!metadataValidation.isValid) {
|
|
@@ -658,7 +726,7 @@ export function useSimilarProducts(product: Product) {
|
|
|
658
726
|
);
|
|
659
727
|
return;
|
|
660
728
|
}
|
|
661
|
-
fetchSimilarProductsByBase64(dataUrl);
|
|
729
|
+
fetchSimilarProductsByBase64(dataUrl, undefined, false);
|
|
662
730
|
};
|
|
663
731
|
|
|
664
732
|
reader.onerror = () =>
|
|
@@ -679,6 +747,8 @@ export function useSimilarProducts(product: Product) {
|
|
|
679
747
|
|
|
680
748
|
try {
|
|
681
749
|
setCurrentImageUrl(base64Image);
|
|
750
|
+
setCurrentImageBase64(base64Image);
|
|
751
|
+
setHasCroppedImage(true);
|
|
682
752
|
|
|
683
753
|
const metadataValidation = await validateImageFromDataUrl(base64Image);
|
|
684
754
|
if (!metadataValidation.isValid) {
|
|
@@ -688,10 +758,14 @@ export function useSimilarProducts(product: Product) {
|
|
|
688
758
|
return;
|
|
689
759
|
}
|
|
690
760
|
|
|
691
|
-
if (
|
|
692
|
-
await fetchSimilarProductsByBase64(
|
|
761
|
+
if (hasUploadedImageRef.current) {
|
|
762
|
+
await fetchSimilarProductsByBase64(
|
|
763
|
+
base64Image,
|
|
764
|
+
searchTextRef.current,
|
|
765
|
+
false
|
|
766
|
+
);
|
|
693
767
|
} else {
|
|
694
|
-
await fetchSimilarProductsByImageCrop(base64Image);
|
|
768
|
+
await fetchSimilarProductsByImageCrop(base64Image, true);
|
|
695
769
|
}
|
|
696
770
|
} catch (error) {
|
|
697
771
|
setFileError(t('common.similar_products.errors.crop_processing_error'));
|
|
@@ -882,6 +956,11 @@ export function useSimilarProducts(product: Product) {
|
|
|
882
956
|
setLoadedPages(new Set([1]));
|
|
883
957
|
}, []);
|
|
884
958
|
|
|
959
|
+
const resetCropState = useCallback(() => {
|
|
960
|
+
setHasCroppedImage(false);
|
|
961
|
+
setCurrentImageBase64('');
|
|
962
|
+
}, []);
|
|
963
|
+
|
|
885
964
|
const clearFileInput = useCallback(
|
|
886
965
|
(fileInputRef: React.RefObject<HTMLInputElement>) => {
|
|
887
966
|
if (fileInputRef.current) {
|
|
@@ -891,6 +970,47 @@ export function useSimilarProducts(product: Product) {
|
|
|
891
970
|
[]
|
|
892
971
|
);
|
|
893
972
|
|
|
973
|
+
const handleTextSearch = useCallback(async () => {
|
|
974
|
+
const textToUse = searchTextRef.current || searchText;
|
|
975
|
+
|
|
976
|
+
if (hasCroppedImage && currentImageBase64) {
|
|
977
|
+
await fetchSimilarProductsByBase64(currentImageBase64, textToUse, !hasUploadedImage);
|
|
978
|
+
} else if (hasUploadedImage && currentImageUrl) {
|
|
979
|
+
await fetchSimilarProductsByBase64(currentImageUrl, textToUse, false);
|
|
980
|
+
} else if (currentImageUrl) {
|
|
981
|
+
await fetchSimilarProductsByImageUrl(currentImageUrl, textToUse);
|
|
982
|
+
}
|
|
983
|
+
}, [
|
|
984
|
+
searchText,
|
|
985
|
+
searchTextRef,
|
|
986
|
+
currentImageUrl,
|
|
987
|
+
currentImageBase64,
|
|
988
|
+
hasCroppedImage,
|
|
989
|
+
hasUploadedImage,
|
|
990
|
+
fetchSimilarProductsByImageUrl,
|
|
991
|
+
fetchSimilarProductsByBase64
|
|
992
|
+
]);
|
|
993
|
+
|
|
994
|
+
const handleClearText = useCallback(async () => {
|
|
995
|
+
setSearchTextWithRef('');
|
|
996
|
+
|
|
997
|
+
if (hasCroppedImage && currentImageBase64) {
|
|
998
|
+
await fetchSimilarProductsByBase64(currentImageBase64, '', !hasUploadedImage);
|
|
999
|
+
} else if (hasUploadedImage && currentImageUrl) {
|
|
1000
|
+
await fetchSimilarProductsByBase64(currentImageUrl, '', false);
|
|
1001
|
+
} else if (currentImageUrl) {
|
|
1002
|
+
await fetchSimilarProductsByImageUrl(currentImageUrl, '');
|
|
1003
|
+
}
|
|
1004
|
+
}, [
|
|
1005
|
+
hasCroppedImage,
|
|
1006
|
+
currentImageBase64,
|
|
1007
|
+
currentImageUrl,
|
|
1008
|
+
hasUploadedImage,
|
|
1009
|
+
fetchSimilarProductsByBase64,
|
|
1010
|
+
fetchSimilarProductsByImageUrl,
|
|
1011
|
+
setSearchTextWithRef
|
|
1012
|
+
]);
|
|
1013
|
+
|
|
894
1014
|
const handleLoadMore = async () => {
|
|
895
1015
|
if (!searchResults?.pagination || isLoading) return;
|
|
896
1016
|
|
|
@@ -931,6 +1051,8 @@ export function useSimilarProducts(product: Product) {
|
|
|
931
1051
|
setCurrentImageUrl,
|
|
932
1052
|
isLoading,
|
|
933
1053
|
fileError,
|
|
1054
|
+
searchText,
|
|
1055
|
+
setSearchText: setSearchTextWithRef,
|
|
934
1056
|
searchResults,
|
|
935
1057
|
resultsKey,
|
|
936
1058
|
hasUploadedImage,
|
|
@@ -949,6 +1071,9 @@ export function useSimilarProducts(product: Product) {
|
|
|
949
1071
|
allLoadedProducts,
|
|
950
1072
|
clearError,
|
|
951
1073
|
clearResults,
|
|
952
|
-
clearFileInput
|
|
1074
|
+
clearFileInput,
|
|
1075
|
+
handleTextSearch,
|
|
1076
|
+
handleClearText,
|
|
1077
|
+
resetCropState
|
|
953
1078
|
};
|
|
954
1079
|
}
|