@akinon/pz-similar-products 1.92.0-rc.31 → 1.92.0-rc.33

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @akinon/pz-similar-products
2
2
 
3
+ ## 1.92.0-rc.33
4
+
5
+ ### Minor Changes
6
+
7
+ - 59b275c: ZERO-3532: Pass settings prop to SimilarProductsButton for enhanced functionality
8
+
9
+ ## 1.92.0-rc.32
10
+
11
+ ### Minor Changes
12
+
13
+ - 54e3179: ZERO-3532: Added customize options to icons
14
+
3
15
  ## 1.92.0-rc.31
4
16
 
5
17
  ## 1.92.0-rc.30
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/pz-similar-products",
3
- "version": "1.92.0-rc.31",
3
+ "version": "1.92.0-rc.33",
4
4
  "license": "MIT",
5
5
  "main": "src/index.ts",
6
6
  "peerDependencies": {
@@ -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({ image, excluded_product_ids })
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 result = await fetchSimilarProductsByUrl({
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
- }).unwrap();
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 (base64Image: string) => {
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
- [getSimilarProductsByImage, handleImageSearch]
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: product?.pk
553
- ? [product.pk]
554
- : undefined
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: product?.pk ? [product.pk] : undefined
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 (hasUploadedImage) {
692
- await fetchSimilarProductsByBase64(base64Image);
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
  }