From 3cfeeb43e5a465ac94bf88f4be4cbad58ef5be2b Mon Sep 17 00:00:00 2001 From: Dmytro Asieiev Date: Wed, 4 Sep 2024 13:52:11 +0300 Subject: [PATCH 01/26] Init composer.json. --- composer.json | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 composer.json diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..fdbfe4a --- /dev/null +++ b/composer.json @@ -0,0 +1,42 @@ +{ + "name": "spryker-eco/image-search-ai", + "type": "library", + "description": "ImageSearchAi module", + "license": "MIT", + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "spryker/code-sniffer": "*" + }, + "autoload": { + "psr-4": { + "SprykerEco\\": "src/SprykerEco/" + } + }, + "autoload-dev": { + "psr-4": { + "SprykerEcoTest\\": "tests/SprykerEcoTest/" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "scripts": { + "cs-check": "phpcs -p -s --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/ tests/", + "cs-fix": "phpcbf -p --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/ tests/", + "stan": "phpstan analyse -c phpstan.neon -l 6 src/", + "stan-setup": "cp composer.json composer.backup && COMPOSER_MEMORY_LIMIT=-1 composer require --dev phpstan/phpstan:^0.12 && mv composer.backup composer.json" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} \ No newline at end of file From 9aec9d0e4de9c79ee4b91afa798f511e465b2060 Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Fri, 6 Sep 2024 14:10:58 +0300 Subject: [PATCH 02/26] Implemented SprykerEco/ImageSearchAi. --- .editorconfig | 13 ++ .gitattributes | 35 ++++ .gitignore | 37 ++++ CHANGELOG.md | 3 + LICENSE | 21 +++ README.md | 10 +- architecture-baseline.json | 1 + composer.json | 84 +++++---- phpstan.neon | 3 + psalm-report.json | 5 + .../Transfer/image_search_ai.transfer.xml | 18 ++ .../Controller/ImageSearchController.php | 71 +++++++ .../ImageSearchAiToCatalogClientBridge.php | 37 ++++ .../ImageSearchAiToCatalogClientInterface.php | 19 ++ .../ImageSearchAiToOpenAiClientBridge.php | 38 ++++ .../ImageSearchAiToOpenAiClientInterface.php | 21 +++ ...ageSearchAiToUtilEncodingServiceBridge.php | 39 ++++ ...SearchAiToUtilEncodingServiceInterface.php | 21 +++ .../ImageSearchAi/ImageSearchAiConfig.php | 41 ++++ .../ImageSearchAiDependencyProvider.php | 103 ++++++++++ .../ImageSearchAi/ImageSearchAiFactory.php | 77 ++++++++ .../ImageSearchAiRouteProviderPlugin.php | 51 +++++ .../ImageToSearchTermsTransformer.php | 61 ++++++ ...ImageToSearchTermsTransformerInterface.php | 20 ++ .../Validator/Base64ImageValidator.php | 94 +++++++++ .../Base64ImageValidatorInterface.php | 20 ++ .../molecules/search-by-image/index.ts | 12 ++ .../search-by-image/search-by-image.scss | 178 ++++++++++++++++++ .../search-by-image/search-by-image.ts | 81 ++++++++ .../search-by-image/search-by-image.twig | 62 ++++++ .../image-search-ai/image-search-ai.twig | 17 ++ .../Widget/ImageSearchAiWidget.php | 29 +++ tooling.yml | 5 + 33 files changed, 1286 insertions(+), 41 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 architecture-baseline.json create mode 100644 phpstan.neon create mode 100644 psalm-report.json create mode 100644 src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientBridge.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientInterface.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Plugin/Router/ImageSearchAiRouteProviderPlugin.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformerInterface.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidator.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php create mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/index.ts create mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.scss create mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.ts create mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.twig create mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/views/image-search-ai/image-search-ai.twig create mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Widget/ImageSearchAiWidget.php create mode 100644 tooling.yml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a11af76 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at https://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.bat] +end_of_line = crlf diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..babca79 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,35 @@ +# Define the line ending behavior of the different file extensions +# Set the default behavior, in case people don't have core.autocrlf set. +* text text=auto eol=lf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.gif binary +*.jpeg binary +*.zip binary +*.phar binary +*.ttf binary +*.woff binary +*.woff2 binary +*.eot binary +*.ico binary +*.mo binary +*.pdf binary +*.xsd binary +*.ts binary +*.exe binary + +# Remove files for archives generated using `git archive` +dependency.json export-ignore +phpstan.json export-ignore +phpstan.neon export-ignore +tooling.yml export-ignore +.coveralls.yml export-ignore +.travis.yml export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +architecture-baseline.json export-ignore +psalm-report.json export-ignore +.github/ export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..669cf87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# IDE +.idea/ +.project/ +nbproject/ +.buildpath/ +.settings/ +*.sublime-* + +# OS +.DS_Store +*.AppleDouble +*.AppleDB +*.AppleDesktop + +# grunt stuff +.grunt +.sass-cache +/node_modules/ + +# tooling +vendor/ +composer.lock +.phpunit.result.cache + +# built client resources +src/*/Zed/*/Static/Public +src/*/Zed/*/Static/Assets/sprite + +# Propel classes +src/*/Zed/*/Persistence/Propel/Base/* +src/*/Zed/*/Persistence/Propel/Map/* + +# tests +tests/**/_generated/ +tests/_output/* +!tests/_output/.gitkeep +tests/app/* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8ad25cc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# ImageSearchAi Changelog + +[Release Changelog](https://github.com/spryker-eco/image-search-ai/releases) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..73dcc7b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016-present Spryker Systems GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index ee80cbd..971a77c 100644 --- a/README.md +++ b/README.md @@ -1 +1,9 @@ -# image-search-ai \ No newline at end of file +# ImageSearchAi Module + +ImageSearchAi Module. + +## Installation + +``` +composer require spryker-eco/image-search-ai +``` diff --git a/architecture-baseline.json b/architecture-baseline.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/architecture-baseline.json @@ -0,0 +1 @@ +[] diff --git a/composer.json b/composer.json index fdbfe4a..70a04ba 100644 --- a/composer.json +++ b/composer.json @@ -1,42 +1,46 @@ { - "name": "spryker-eco/image-search-ai", - "type": "library", - "description": "ImageSearchAi module", - "license": "MIT", - "require": { - "php": ">=8.1" - }, - "require-dev": { - "phpstan/phpstan": "*", - "spryker/code-sniffer": "*" - }, - "autoload": { - "psr-4": { - "SprykerEco\\": "src/SprykerEco/" + "name": "spryker-eco/image-search-ai", + "type": "library", + "description": "ImageSearchAi module", + "license": "MIT", + "require": { + "php": ">=8.1", + "spryker/kernel": "^3.73.0", + "spryker/catalog": "^5.10.0", + "spryker/util-encoding": "^2.1.1", + "spryker-eco/open-ai": "dev-feature/demo/dev-ai-integrations" + }, + "require-dev": { + "phpstan/phpstan": "*", + "spryker/code-sniffer": "*" + }, + "autoload": { + "psr-4": { + "SprykerEco\\": "src/SprykerEco/" + } + }, + "autoload-dev": { + "psr-4": { + "SprykerEcoTest\\": "tests/SprykerEcoTest/" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "scripts": { + "cs-check": "phpcs -p -s --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/ tests/", + "cs-fix": "phpcbf -p --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/ tests/", + "stan": "phpstan analyse -c phpstan.neon -l 6 src/", + "stan-setup": "cp composer.json composer.backup && COMPOSER_MEMORY_LIMIT=-1 composer require --dev phpstan/phpstan:^0.12 && mv composer.backup composer.json" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } - }, - "autoload-dev": { - "psr-4": { - "SprykerEcoTest\\": "tests/SprykerEcoTest/" - } - }, - "minimum-stability": "dev", - "prefer-stable": true, - "scripts": { - "cs-check": "phpcs -p -s --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/ tests/", - "cs-fix": "phpcbf -p --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/ tests/", - "stan": "phpstan analyse -c phpstan.neon -l 6 src/", - "stan-setup": "cp composer.json composer.backup && COMPOSER_MEMORY_LIMIT=-1 composer require --dev phpstan/phpstan:^0.12 && mv composer.backup composer.json" - }, - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "config": { - "sort-packages": true, - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true - } - } -} \ No newline at end of file +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..e435fc2 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,3 @@ +parameters: + level: 8 + checkGenericClassInNonGenericObjectType: false diff --git a/psalm-report.json b/psalm-report.json new file mode 100644 index 0000000..a6ed8da --- /dev/null +++ b/psalm-report.json @@ -0,0 +1,5 @@ +{ + "error": [], + "warning": [], + "deprecation": [] +} diff --git a/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml new file mode 100644 index 0000000..189a1d4 --- /dev/null +++ b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php new file mode 100644 index 0000000..40dba99 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php @@ -0,0 +1,71 @@ +getFactory() + ->getUtilEncodingService() + ->decodeJson((string)$request->getContent(), true); + + $errors = $this->getFactory()->createBase64ImageValidator()->validate($requestBodyContent); + if (count($errors)) { + $errorResponse = new ImageSearchFindTermsErrorResponseTransfer(); + $errorResponse->setError('Bad request'); + foreach ($errors as $error) { + $errorMessage = new ImageSearchFindTermsErrorMessageTransfer(); + $errorMessage->setMessage($error->getMessage()); + $errorResponse->addImageSearchFindTermsErrorMessage($errorMessage); + } + + return new JsonResponse($errorResponse->toArray(), Response::HTTP_BAD_REQUEST); + } + + $searchString = $this->getFactory()->createImageToSearchTermsTransformer()->transform( + $requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_IMAGE], + )->getMessage(); + + if (!$searchString) { + return new JsonResponse((new ImageSearchFindTermsResponseTransfer())->toArray()); + } + + $searchResults = $this + ->getFactory() + ->getCatalogClient() + ->catalogSuggestSearch($searchString, []); + + return new JsonResponse([ + 'searchUrl' => Url::generate('/search', ['q' => $searchString])->build(), + 'firstMatchProductUrl' => Url::generate($searchResults['suggestionByType']['product_abstract'][0]['url'])->build(), + ]); + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php new file mode 100644 index 0000000..eae2e2d --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php @@ -0,0 +1,37 @@ +catalogClient = $catalogClient; + } + + /** + * @param string $searchString + * @param array $requestParameters + * + * @return array + */ + public function catalogSuggestSearch($searchString, array $requestParameters = []) + { + return $this->catalogClient->catalogSuggestSearch($searchString, $requestParameters); + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php new file mode 100644 index 0000000..b23a398 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php @@ -0,0 +1,19 @@ + $requestParameters + * + * @return array + */ + public function catalogSuggestSearch($searchString, array $requestParameters = []); +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientBridge.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientBridge.php new file mode 100644 index 0000000..9063db6 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientBridge.php @@ -0,0 +1,38 @@ +openAiClient = $openAiClient; + } + + /** + * @param \Generated\Shared\Transfer\OpenAiChatRequestTransfer $openAiRequestTransfer + * + * @return \Generated\Shared\Transfer\OpenAiChatResponseTransfer + */ + public function chat(OpenAiChatRequestTransfer $openAiRequestTransfer): OpenAiChatResponseTransfer + { + return $this->openAiClient->chat($openAiRequestTransfer); + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientInterface.php new file mode 100644 index 0000000..07d5c0f --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientInterface.php @@ -0,0 +1,21 @@ +utilEncodingService = $utilEncodingService; + } + + /** + * @param string $jsonValue + * @param bool $assoc Deprecated: `false` is deprecated, always use `true` for array return. + * @param int|null $depth + * @param int|null $options + * + * @return object|array|null + */ + public function decodeJson($jsonValue, $assoc = false, $depth = null, $options = null) + { + return $this->utilEncodingService->decodeJson($jsonValue, $assoc, $depth, $options); + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php new file mode 100644 index 0000000..69c5b1f --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php @@ -0,0 +1,21 @@ +|null + */ + public function decodeJson($jsonValue, $assoc = false, $depth = null, $options = null); +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php new file mode 100644 index 0000000..6f720e9 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php @@ -0,0 +1,41 @@ + + */ + public static function getAllowedMimeTypes(): array + { + return [ + 'image/jpeg', + 'image/png', + ]; + } + + /** + * @api + * + * @return string + */ + public function getOpenAiModel(): string + { + return static::OPEN_AI_GPT4O_MINI_MODEL; + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php new file mode 100644 index 0000000..60e0ae5 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php @@ -0,0 +1,103 @@ +addCatalogClient($container); + $container = $this->addOpenAiClient($container); + $container = $this->addUtilEncodingService($container); + + return $container; + } + + /** + * @param \Spryker\Yves\Kernel\Container $container + * + * @return \Spryker\Yves\Kernel\Container + */ + protected function addCatalogClient(Container $container): Container + { + $container->set( + static::CLIENT_CATALOG, + function (Container $container): ImageSearchAiToCatalogClientInterface { + return new ImageSearchAiToCatalogClientBridge($container->getLocator()->catalog()->client()); + }, + ); + + return $container; + } + + /** + * @param \Spryker\Yves\Kernel\Container $container + * + * @return \Spryker\Yves\Kernel\Container + */ + protected function addUtilEncodingService(Container $container): Container + { + $container->set( + static::SERVICE_UTIL_ENCODING, + function (Container $container): ImageSearchAiToUtilEncodingServiceInterface { + return new ImageSearchAiToUtilEncodingServiceBridge($container->getLocator()->utilEncoding()->service()); + }, + ); + + return $container; + } + + /** + * @param \Spryker\Yves\Kernel\Container $container + * + * @return \Spryker\Yves\Kernel\Container + */ + protected function addOpenAiClient(Container $container): Container + { + $container->set( + static::CLIENT_OPEN_AI, + function (Container $container): ImageSearchAiToOpenAiClientInterface { + return new ImageSearchAiToOpenAiClientBridge($container->getLocator()->openAi()->client()); + }, + ); + + return $container; + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php new file mode 100644 index 0000000..4076f87 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php @@ -0,0 +1,77 @@ +getOpenAiClient(), + $this->getConfig(), + ); + } + + /** + * @return \SprykerEco\Yves\ImageSearchAi\Dependency\Client\ImageSearchAiToCatalogClientInterface + */ + public function getCatalogClient(): ImageSearchAiToCatalogClientInterface + { + return $this->getProvidedDependency(ImageSearchAiDependencyProvider::CLIENT_CATALOG); + } + + /** + * @return \SprykerEco\Yves\ImageSearchAi\Dependency\Service\ImageSearchAiToUtilEncodingServiceInterface + */ + public function getUtilEncodingService(): ImageSearchAiToUtilEncodingServiceInterface + { + return $this->getProvidedDependency(ImageSearchAiDependencyProvider::SERVICE_UTIL_ENCODING); + } + + /** + * @return \SprykerEco\Yves\ImageSearchAi\Validator\Base64ImageValidatorInterface + */ + public function createBase64ImageValidator(): Base64ImageValidatorInterface + { + return new Base64ImageValidator( + $this->createValidator(), + ); + } + + /** + * @return \Symfony\Component\Validator\Validator\ValidatorInterface + */ + public function createValidator(): ValidatorInterface + { + return Validation::createValidator(); + } + + /** + * @return \SprykerEco\Yves\ImageSearchAi\Dependency\Client\ImageSearchAiToOpenAiClientInterface + */ + protected function getOpenAiClient(): ImageSearchAiToOpenAiClientInterface + { + return $this->getProvidedDependency(ImageSearchAiDependencyProvider::CLIENT_OPEN_AI); + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/Plugin/Router/ImageSearchAiRouteProviderPlugin.php b/src/SprykerEco/Yves/ImageSearchAi/Plugin/Router/ImageSearchAiRouteProviderPlugin.php new file mode 100644 index 0000000..72460a9 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Plugin/Router/ImageSearchAiRouteProviderPlugin.php @@ -0,0 +1,51 @@ +addImageSearchRoute($routeCollection); + + return $routeCollection; + } + + /** + * @param \Spryker\Yves\Router\Route\RouteCollection $routeCollection + * + * @return \Spryker\Yves\Router\Route\RouteCollection + */ + protected function addImageSearchRoute(RouteCollection $routeCollection): RouteCollection + { + $route = $this->buildRoute( + '/search-ai/image', + 'ImageSearchAi', + 'ImageSearch', + 'findTermsAction', + )->setMethods(Request::METHOD_POST); + + $routeCollection->add(static::ROUTE_IMAGE_SEARCH, $route); + + return $routeCollection; + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php b/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php new file mode 100644 index 0000000..b9d4bd5 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php @@ -0,0 +1,61 @@ +openAiClient = $openAiClient; + $this->config = $config; + } + + /** + * @param string $base64Image + * + * @return \Generated\Shared\Transfer\OpenAiChatResponseTransfer + */ + public function transform(string $base64Image): OpenAiChatResponseTransfer + { + $openAiChatRequestTransfer = (new OpenAiChatRequestTransfer())->setPromptData([ + [ + 'type' => 'text', + 'text' => 'Describe the most important characteristics of the main object you can identify in the image e.g. manufacturer, model, color, part number or any identification number that help me to find the product using a search engine. Your output must be ony a list of the product attributes. Remove the attribute names and keep only the values from your output and provide them in a single line.', + ], + [ + 'type' => 'image_url', + 'image_url' => [ + 'url' => $base64Image, + ], + ], + ])->setModel($this->config->getOpenAiModel()); + + return $this->openAiClient->chat($openAiChatRequestTransfer); + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformerInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformerInterface.php new file mode 100644 index 0000000..b429668 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformerInterface.php @@ -0,0 +1,20 @@ +validator = $validator; + } + + /** + * @param array $requestBodyContent + * + * @return \Symfony\Component\Validator\ConstraintViolationListInterface + */ + public function validate(array $requestBodyContent): ConstraintViolationListInterface + { + $constraint = new Collection( + $this->getBase64ImageFieldValidationConstraints(), + ); + + return $this->validator->validate($requestBodyContent, $constraint); + } + + /** + * @return array> + */ + protected function getBase64ImageFieldValidationConstraints(): array + { + return [ + 'image' => [ + new NotBlank(), + new Callback([static::class, 'validateIsBase64']), + ], + ]; + } + + /** + * @param mixed $value + * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context + * + * @return void + */ + public static function validateIsBase64(mixed $value, ExecutionContextInterface $context): void + { + $value = preg_replace('#data:image/[^;]+;base64,#', '', $value); + + $decodedFile = base64_decode($value); + if (!$decodedFile) { + $context->buildViolation('This is not a base64 file') + ->atPath('image') + ->addViolation(); + } + + $tmpFilename = tempnam(sys_get_temp_dir(), 'guessMimeType_'); + file_put_contents($tmpFilename, $decodedFile); + $mimeTypes = new MimeTypes(); + $mimeType = $mimeTypes->guessMimeType($tmpFilename); + unlink($tmpFilename); + + if (!in_array($mimeType, ImageSearchAiConfig::getAllowedMimeTypes(), true)) { + $context->buildViolation(sprintf( + 'Invalid mime type %s. Accepted types are %s.', + $mimeType, + implode(', ', ImageSearchAiConfig::getAllowedMimeTypes()), + )) + ->atPath('image') + ->addViolation(); + } + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php new file mode 100644 index 0000000..9d26fa4 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php @@ -0,0 +1,20 @@ + $requestBodyContent + * + * @return \Symfony\Component\Validator\ConstraintViolationListInterface + */ + public function validate(array $requestBodyContent): ConstraintViolationListInterface; +} diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/index.ts b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/index.ts new file mode 100644 index 0000000..168f9ab --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/index.ts @@ -0,0 +1,12 @@ +import './search-by-image.scss'; +import register from 'ShopUi/app/registry'; + +export default register( + 'search-by-image', + () => + import( + /* webpackMode: "lazy" */ + /* webpackChunkName: "search-by-image" */ + './search-by-image' + ), +); diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.scss b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.scss new file mode 100644 index 0000000..529b527 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.scss @@ -0,0 +1,178 @@ +@mixin shop-ui-search-by-image($name: '.search-by-image') { + #{$name} { + &__dialog { + @include helper-breakpoint-media-max(lg) { + width: 100%; + height: 100%; + border: none; + padding: 0; + margin: 0; + max-width: none; + max-height: none; + } + + @include helper-breakpoint(lg) { + margin: auto; + box-shadow: 0 rem(4) rem(12) 0 #12121226; + border-radius: rem(12); + width: rem(900); + max-width: 98%; + + &::backdrop { + background: rgb(0 0 0 / 30%); + } + } + } + + &__wrapper { + background: $setting-color-white; + border-radius: rem(12); + padding: rem(19); + } + + &__heading { + position: relative; + padding: 0 rem(55); + text-align: center; + margin-bottom: rem(30); + } + + &__title { + font-size: rem(16); + line-height: 1.5; + + @include helper-breakpoint(lg) { + font-size: rem(24); + } + } + + &__close-button { + position: absolute; + right: rem(7); + top: rem(3); + background: none; + cursor: pointer; + color: var(--gray-12); + } + + &__grid { + display: flex; + gap: rem(16); + width: 100%; + } + + &__col { + flex-grow: 1; + text-align: center; + } + + &__file-input-button { + @include helper-breakpoint-media-max(lg) { + width: 100%; + } + } + + &__close-icon { + pointer-events: none; + } + + &__form { + border: rem(1) dashed var(--gray-6); + border-radius: rem(8); + display: flex; + flex-direction: column; + align-items: center; + padding: rem(11) rem(11) rem(113); + } + + &__info { + font-size: rem(16); + line-height: 1.5; + color: var(--gray-9); + margin-bottom: rem(32); + + @include helper-breakpoint(lg) { + font-size: rem(21); + } + } + + &__image-wrapper { + display: flex; + height: rem(153); + flex-direction: column; + align-items: center; + justify-content: flex-end; + margin-bottom: rem(14); + } + + &__file-input-wrapper { + @include helper-breakpoint-media-max(lg) { + width: 100%; + } + display: inline-block; + vertical-align: top; + position: relative; + overflow: clip; + } + + &__file-input { + position: absolute; + top: 0; + right: 0; + width: rem(999); + height: rem(999); + opacity: 0; + font-size: rem(200); + cursor: pointer; + } + + &__image-placeholder { + max-width: rem(100); + max-height: rem(100); + vertical-align: top; + } + + &__loading-frame { + position: relative; + overflow: clip; + + &::after { + content: ''; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 100%; + background: linear-gradient(to top, rgb(255 255 255 / 0%), rgb(255 255 255 / 100%)); + display: none; + } + + &--active { + &::after { + display: block; + animation: loading 1s linear infinite; + } + } + } + + &__service-message { + text-align: center; + padding: rem(10) 0; + color: var(--red-9); + } + + @content; + } +} + +@keyframes loading { + 0% { + translate: 0 200%; + } + + 100% { + translate: 0 -100%; + } +} + +@include shop-ui-search-by-image; diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.ts b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.ts new file mode 100644 index 0000000..9f67f41 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.ts @@ -0,0 +1,81 @@ +import Component from 'ShopUi/models/component'; + +const TIMEOUT = 15000; +export default class SearchByImage extends Component { + protected dialog: HTMLDialogElement; + protected fileInputField: HTMLInputElement[]; + protected loadingFrame: HTMLDivElement; + protected closeButton: HTMLButtonElement; + protected openButton: HTMLButtonElement[]; + protected serviceMessage: HTMLDivElement; + protected form: HTMLFormElement; + + protected init(): void { + this.dialog = this.querySelector(`.${this.jsName}__dialog`) as HTMLDialogElement; + this.fileInputField = Array.from(this.querySelectorAll(`.${this.jsName}__file-input`)) as HTMLInputElement[]; + this.loadingFrame = this.querySelector(`.${this.jsName}__loading-frame`) as HTMLDivElement; + this.closeButton = this.querySelector(`.${this.jsName}__close-button`) as HTMLButtonElement; + this.openButton = Array.from( + document.querySelectorAll(`.js-image-search-ai__button--image-search`), + ) as HTMLButtonElement[]; + this.serviceMessage = this.querySelector(`.${this.jsName}__service-message`) as HTMLDivElement; + this.form = this.querySelector(`.${this.jsName}__form`) as HTMLFormElement; + this.mapEvents(); + } + + protected mapEvents(): void { + this.fileInputField?.map((field) => { + field.addEventListener('change', (event) => this.onFileInputChange(event)); + }); + this.openButton?.map((button) => { + button.addEventListener('click', () => { + this.dialog.showModal(); + }); + }); + this.closeButton?.addEventListener('click', () => this.dialog.close()); + } + + protected onFileInputChange(event): void { + const file = event.target.files[0]; + if (file && file.type.startsWith('image/')) { + const reader = new FileReader(); + + reader.onload = (event) => { + this.displayImagePreview(event.target.result as string); + }; + + reader.readAsDataURL(file); + } + } + + protected displayImagePreview(imageUrl: string): void { + const imgPlaceholder = document.querySelector(`.${this.jsName}__image-placeholder`) as HTMLImageElement; + imgPlaceholder.src = imageUrl; + this.loadingFrame.classList.add(`${this.name}__loading-frame--active`); + this.sendRequest(imageUrl); + } + + protected async sendRequest(imageCode) { + setTimeout(() => { + this.loadingFrame.classList.remove(`${this.name}__loading-frame--active`); + this.showServiceMessage(); + }, TIMEOUT); + fetch(`${this.form.getAttribute('action')}`, { + method: this.form.getAttribute('method'), + signal: AbortSignal.timeout(TIMEOUT), + body: JSON.stringify({ image: imageCode }), + }) + .then((response) => response.json()) + .then((data) => { + window.location.href = data.firstMatchProductUrl; + }) + .catch(() => { + this.loadingFrame.classList.remove(`${this.name}__loading-frame--active`); + this.showServiceMessage(); + }); + } + + protected showServiceMessage(): void { + this.serviceMessage.classList.remove('is-hidden'); + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.twig b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.twig new file mode 100644 index 0000000..867798e --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.twig @@ -0,0 +1,62 @@ +{% extends model('component') %} + +{% define config = { + name: 'search-by-image', + tag: 'search-by-image', +} %} + +{% block body %} + +
+
+

{{ 'search.with.your-images.title' | trans }}

+ +
+
+
+
+
+ +
+
+ +
{{ 'search.with.your-images.content' | trans }}
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+
+{% endblock %} diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/views/image-search-ai/image-search-ai.twig b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/views/image-search-ai/image-search-ai.twig new file mode 100644 index 0000000..14bbeb6 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/views/image-search-ai/image-search-ai.twig @@ -0,0 +1,17 @@ +{% extends template('widget') %} + +{% define config = { + name: 'image-search-ai', + jsName: 'js-image-search-ai', +} %} + +{% block body %} + +{% endblock %} diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Widget/ImageSearchAiWidget.php b/src/SprykerEco/Yves/ImageSearchAiWidget/Widget/ImageSearchAiWidget.php new file mode 100644 index 0000000..3656f0f --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Widget/ImageSearchAiWidget.php @@ -0,0 +1,29 @@ + Date: Sat, 7 Sep 2024 15:58:22 +0300 Subject: [PATCH 03/26] Normalize ImageSearchAiWidget --- .../image-uploader-popup.twig | 51 +++++ .../image-uploader/image-uploader.scss | 42 +++++ .../image-uploader/image-uploader.ts | 95 ++++++++++ .../image-uploader/image-uploader.twig | 72 +++++++ .../index.ts | 8 +- .../search-by-image/search-by-image.scss | 178 ------------------ .../search-by-image/search-by-image.ts | 81 -------- .../search-by-image/search-by-image.twig | 62 ------ .../image-search-ai/image-search-ai.twig | 14 +- 9 files changed, 265 insertions(+), 338 deletions(-) create mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader-popup/image-uploader-popup.twig create mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.scss create mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.ts create mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.twig rename src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/{search-by-image => image-uploader}/index.ts (51%) delete mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.scss delete mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.ts delete mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.twig diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader-popup/image-uploader-popup.twig b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader-popup/image-uploader-popup.twig new file mode 100644 index 0000000..4ca37fd --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader-popup/image-uploader-popup.twig @@ -0,0 +1,51 @@ +{% extends model('component') %} + +{% define config = { + name: 'image-uploader-popup', +} %} + +{% define data = { + icon: 'file-image', + text: null, + id: random('abcdefghijklmnopqrstuvwxyz') ~ random(), +} %} + +{% block content %} + {% include molecule('image-uploader', 'ImageSearchAiWidget') only %} +{% endblock %} + +{% block body %} + {% set triggerClassName = "#{config.jsName}__button--image-search-#{data.id}" %} + + {% block trigger %} + + {% endblock %} + + {% block popup %} + {% include molecule('main-popup') with { + modifiers: modifiers, + class: "#{config.jsName}__popup", + data: { + title: 'search.with.your-images.title' | trans, + content: block('content'), + }, + attributes: { + 'content-id': "#{config.jsName}__popup-content-#{data.id}", + 'trigger-class-name': triggerClassName, + 'has-content-mount': true, + 'has-content-reload': true, + }, + } only %} + {% endblock %} +{% endblock %} diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.scss b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.scss new file mode 100644 index 0000000..4854fee --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.scss @@ -0,0 +1,42 @@ +@mixin image-search-ai-image-uploader($name: '.image-uploader') { + #{$name} { + &.is-loading & { + &__spinner { + display: block; + } + + &__image-wrapper::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: $setting-color-white; + opacity: 0.5; + } + } + + &.is-error & { + &__service-message { + display: block; + } + } + + &__spinner { + position: absolute; + top: 50%; + left: 50%; + translate: -50%; + } + + &__spinner, + &__service-message { + display: none; + } + + @content; + } +} + +@include image-search-ai-image-uploader; diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.ts b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.ts new file mode 100644 index 0000000..a27aafe --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.ts @@ -0,0 +1,95 @@ +import Component from 'ShopUi/models/component'; + +export default class ImageUploader extends Component { + protected timeout = 15000; + protected fields: HTMLInputElement[]; + protected states = { + loading: 'is-loading', + error: 'is-error', + } + + protected readyCallback(): void {} + protected init(): void { + this.fields = [...this.querySelectorAll(`.${this.jsName}__file-input`)]; + + this.mapEvents(); + } + + protected mapEvents(): void { + this.classList.remove(this.states.error); + this.classList.remove(this.states.loading); + + this.fields.forEach((field) => { + field.addEventListener('change', this.onFileInputChange.bind(this)); + }); + } + + disconnectedCallback(): void { + this.fields.forEach((field) => { + field.removeEventListener('change', this.onFileInputChange.bind(this)); + }); + } + + protected onFileInputChange(event: Event): void { + const file = ((event.target as HTMLInputElement).files as FileList)[0]; + const reader = new FileReader(); + + reader.onload = (event) => { + const src = event.target.result.toString(); + const image = this.querySelector(`.${this.jsName}__image-placeholder`); + + if (image) { + image.src = src; + } + + this.normalizeImage(src, this.sendRequest.bind(this)); + }; + + reader.readAsDataURL(file); + } + + protected normalizeImage(src: string, callback: (image: string) => void): void { + const image = new Image(); + + image.onload = () => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + canvas.width = image.width; + canvas.height = image.height; + + ctx.drawImage(image, 0, 0); + + callback(canvas.toDataURL('image/png')); + }; + + image.src = src; + } + + protected async sendRequest(image: string): Promise { + this.classList.add(this.states.loading); + + try { + const response = await fetch(this.action, { + method: 'POST', + signal: AbortSignal.timeout(this.timeout), + body: JSON.stringify({ image, _token: this._token }), + }); + const data = await response.json(); + + window.location.href = data.firstMatchProductUrl; + } catch (error) { + this.classList.add(this.states.error); + } finally { + this.classList.remove(this.states.loading); + } + } + + protected get action(): string { + return this.getAttribute('action'); + } + + protected get _token(): string { + return this.getAttribute('_token'); + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.twig b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.twig new file mode 100644 index 0000000..7d62bab --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.twig @@ -0,0 +1,72 @@ +{% extends model('component') %} + +{% define config = { + name: 'image-uploader', + tag: 'image-uploader', +} %} + +{% define attributes = { + action: '/search-ai/image', + _token: csrf_token('image_search_ai_csrf'), +} %} + +{% block body %} + {% block preview %} +
+ {% embed molecule('lazy-image') with { + modifiers: ['small'], + data: { + imageSrc: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAADb0lEQVR4nO2av08UQRTHPxhiI2phbGzwd+NfIBYKFlZ2GAoJJZQWRm1ttbNS6C3VoA3JSWi0wVq0sjVRDFGJjcQzk8wll8ns7e7c7t3bu+8neQnczd4O8+G9Nzd3IIQQQgghhBBCCFu0FVhag6FPQIGEtA3/Iwx9Agp6CxGDJTdDxGCREGNIiDEkxBgSYgwJMYaEGENCjCEhxpAQY0iIMSSkBNeBNeATsO/D/bwKzFENElKA88BWgZPoTeAc/SEhOVwBdkt8LODGzpCOhPTgDPAt4TOaHz6rUpCQHsTK1EvgKnDExzXgVWTcW9KQkB4NPFyce1mDgfuR8bOUR0IyWItkRh7rwTXPKI+EZPA5WBxXpvKYDa5xW+KySEgGv4PFmSKfo8E17jXKIiEZ/EoQciy45ieGhUwyfiVrx6qQBWADmKA5rAbr4La2ebwOrnlqUYiT8de/1grNYS6yFm5rm8WDyHj3HsWUkG4ZbV+Xp2kOm5H1WPelacrHXCQzXLQS71mbkFuBjE5sNqh0nU48Otnt45CxFiFhZoTRpNI1U/Jw8TtwuY/7VS4kT0YTS9dZfzaVJ6PlDySxIiQmw/1+N7KvbzWodHWY9cchO/5Nn4uP/rGUBl6rkFjPOABu++dXGl66BkUlQvJk4LNhIxiz78uBqFBIERkdpiOlq0m7LvNCsnqGezyLWOlaLjjZCeAko02ykDKZQWLpOgXcBB4Cb/yW0t1jkdElSUhKZuSVrpaX/MhvMfd6bC/L3GvkhaRmRpHSVSYORjRTSgmpSkZW6ZIUigvpt0wVLV3dsefL12N/n+Ua5tBIIVVmRshy16dr74AnwBJwCTgUGT+fMZfFcRFSR2aEpavs+c/CCGdKrhCrf/iC4bnVKqSOMlUV8wVOlvPiD3CHBgqxJqNKKS5eACdoiBCrMqqW8hW4gXEh1mVULeWf3+kdxqiQpjfJXlwAtjPEbPvnzQkZdSb94eWBkYY/9kI6uC8mfDHQ8CWki+PA8yE3fAmJsBT59vugGr6EZOCOc95nZMsH4CL1ICGJDX9QMXa7rH4bvoQYbPjKEIMNXyVrRFEPMYaEGENCjCEhxpAQY0iIMSTEGBJiDAkxhoQYQ0KMISHGkBBjSIgxJKRpQhQMdQ0kAFtrMPQJKJCQtuF/BCGEEEIIIYQQQmCF/43qyUAhbVxQAAAAAElFTkSuQmCC', + imageTitle: 'image-preview', + }, + embed: { + jsName: config.jsName, + }, + } only %} + {% block content %} + {% set imageExtraClasses = "#{embed.jsName}__image-placeholder" %} + {{ parent() }} + {% endblock %} + {% endembed %} + + {% include atom('icon') with { + modifiers: ['spinner', 'big'], + class: "#{config.name}__spinner", + data: { + name: 'spinner', + }, + } only %} +
+ + {% block empty %} +
+ {{ 'search.with.your-images.no-results' | trans }} +
+ {% endblock %} + {% endblock %} + + {% block info %} +
{{ 'search.with.your-images.content' | trans }}
+ {% endblock %} + + {% block actions %} +
+
+ +
+
+ +
+
+ {% endblock %} +{% endblock %} diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/index.ts b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/index.ts similarity index 51% rename from src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/index.ts rename to src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/index.ts index 168f9ab..5388f72 100644 --- a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/index.ts +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/index.ts @@ -1,12 +1,12 @@ -import './search-by-image.scss'; import register from 'ShopUi/app/registry'; +import './image-uploader.scss'; export default register( - 'search-by-image', + 'image-uploader', () => import( /* webpackMode: "lazy" */ - /* webpackChunkName: "search-by-image" */ - './search-by-image' + /* webpackChunkName: "image-uploader" */ + './image-uploader' ), ); diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.scss b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.scss deleted file mode 100644 index 529b527..0000000 --- a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.scss +++ /dev/null @@ -1,178 +0,0 @@ -@mixin shop-ui-search-by-image($name: '.search-by-image') { - #{$name} { - &__dialog { - @include helper-breakpoint-media-max(lg) { - width: 100%; - height: 100%; - border: none; - padding: 0; - margin: 0; - max-width: none; - max-height: none; - } - - @include helper-breakpoint(lg) { - margin: auto; - box-shadow: 0 rem(4) rem(12) 0 #12121226; - border-radius: rem(12); - width: rem(900); - max-width: 98%; - - &::backdrop { - background: rgb(0 0 0 / 30%); - } - } - } - - &__wrapper { - background: $setting-color-white; - border-radius: rem(12); - padding: rem(19); - } - - &__heading { - position: relative; - padding: 0 rem(55); - text-align: center; - margin-bottom: rem(30); - } - - &__title { - font-size: rem(16); - line-height: 1.5; - - @include helper-breakpoint(lg) { - font-size: rem(24); - } - } - - &__close-button { - position: absolute; - right: rem(7); - top: rem(3); - background: none; - cursor: pointer; - color: var(--gray-12); - } - - &__grid { - display: flex; - gap: rem(16); - width: 100%; - } - - &__col { - flex-grow: 1; - text-align: center; - } - - &__file-input-button { - @include helper-breakpoint-media-max(lg) { - width: 100%; - } - } - - &__close-icon { - pointer-events: none; - } - - &__form { - border: rem(1) dashed var(--gray-6); - border-radius: rem(8); - display: flex; - flex-direction: column; - align-items: center; - padding: rem(11) rem(11) rem(113); - } - - &__info { - font-size: rem(16); - line-height: 1.5; - color: var(--gray-9); - margin-bottom: rem(32); - - @include helper-breakpoint(lg) { - font-size: rem(21); - } - } - - &__image-wrapper { - display: flex; - height: rem(153); - flex-direction: column; - align-items: center; - justify-content: flex-end; - margin-bottom: rem(14); - } - - &__file-input-wrapper { - @include helper-breakpoint-media-max(lg) { - width: 100%; - } - display: inline-block; - vertical-align: top; - position: relative; - overflow: clip; - } - - &__file-input { - position: absolute; - top: 0; - right: 0; - width: rem(999); - height: rem(999); - opacity: 0; - font-size: rem(200); - cursor: pointer; - } - - &__image-placeholder { - max-width: rem(100); - max-height: rem(100); - vertical-align: top; - } - - &__loading-frame { - position: relative; - overflow: clip; - - &::after { - content: ''; - position: absolute; - left: 0; - bottom: 0; - width: 100%; - height: 100%; - background: linear-gradient(to top, rgb(255 255 255 / 0%), rgb(255 255 255 / 100%)); - display: none; - } - - &--active { - &::after { - display: block; - animation: loading 1s linear infinite; - } - } - } - - &__service-message { - text-align: center; - padding: rem(10) 0; - color: var(--red-9); - } - - @content; - } -} - -@keyframes loading { - 0% { - translate: 0 200%; - } - - 100% { - translate: 0 -100%; - } -} - -@include shop-ui-search-by-image; diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.ts b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.ts deleted file mode 100644 index 9f67f41..0000000 --- a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.ts +++ /dev/null @@ -1,81 +0,0 @@ -import Component from 'ShopUi/models/component'; - -const TIMEOUT = 15000; -export default class SearchByImage extends Component { - protected dialog: HTMLDialogElement; - protected fileInputField: HTMLInputElement[]; - protected loadingFrame: HTMLDivElement; - protected closeButton: HTMLButtonElement; - protected openButton: HTMLButtonElement[]; - protected serviceMessage: HTMLDivElement; - protected form: HTMLFormElement; - - protected init(): void { - this.dialog = this.querySelector(`.${this.jsName}__dialog`) as HTMLDialogElement; - this.fileInputField = Array.from(this.querySelectorAll(`.${this.jsName}__file-input`)) as HTMLInputElement[]; - this.loadingFrame = this.querySelector(`.${this.jsName}__loading-frame`) as HTMLDivElement; - this.closeButton = this.querySelector(`.${this.jsName}__close-button`) as HTMLButtonElement; - this.openButton = Array.from( - document.querySelectorAll(`.js-image-search-ai__button--image-search`), - ) as HTMLButtonElement[]; - this.serviceMessage = this.querySelector(`.${this.jsName}__service-message`) as HTMLDivElement; - this.form = this.querySelector(`.${this.jsName}__form`) as HTMLFormElement; - this.mapEvents(); - } - - protected mapEvents(): void { - this.fileInputField?.map((field) => { - field.addEventListener('change', (event) => this.onFileInputChange(event)); - }); - this.openButton?.map((button) => { - button.addEventListener('click', () => { - this.dialog.showModal(); - }); - }); - this.closeButton?.addEventListener('click', () => this.dialog.close()); - } - - protected onFileInputChange(event): void { - const file = event.target.files[0]; - if (file && file.type.startsWith('image/')) { - const reader = new FileReader(); - - reader.onload = (event) => { - this.displayImagePreview(event.target.result as string); - }; - - reader.readAsDataURL(file); - } - } - - protected displayImagePreview(imageUrl: string): void { - const imgPlaceholder = document.querySelector(`.${this.jsName}__image-placeholder`) as HTMLImageElement; - imgPlaceholder.src = imageUrl; - this.loadingFrame.classList.add(`${this.name}__loading-frame--active`); - this.sendRequest(imageUrl); - } - - protected async sendRequest(imageCode) { - setTimeout(() => { - this.loadingFrame.classList.remove(`${this.name}__loading-frame--active`); - this.showServiceMessage(); - }, TIMEOUT); - fetch(`${this.form.getAttribute('action')}`, { - method: this.form.getAttribute('method'), - signal: AbortSignal.timeout(TIMEOUT), - body: JSON.stringify({ image: imageCode }), - }) - .then((response) => response.json()) - .then((data) => { - window.location.href = data.firstMatchProductUrl; - }) - .catch(() => { - this.loadingFrame.classList.remove(`${this.name}__loading-frame--active`); - this.showServiceMessage(); - }); - } - - protected showServiceMessage(): void { - this.serviceMessage.classList.remove('is-hidden'); - } -} diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.twig b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.twig deleted file mode 100644 index 867798e..0000000 --- a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/search-by-image/search-by-image.twig +++ /dev/null @@ -1,62 +0,0 @@ -{% extends model('component') %} - -{% define config = { - name: 'search-by-image', - tag: 'search-by-image', -} %} - -{% block body %} - -
-
-

{{ 'search.with.your-images.title' | trans }}

- -
-
-
-
-
- -
-
- -
{{ 'search.with.your-images.content' | trans }}
- -
-
-
- - -
-
-
-
- - -
-
-
- -
-
-
-
-{% endblock %} diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/views/image-search-ai/image-search-ai.twig b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/views/image-search-ai/image-search-ai.twig index 14bbeb6..99d1e7c 100644 --- a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/views/image-search-ai/image-search-ai.twig +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/views/image-search-ai/image-search-ai.twig @@ -1,17 +1,5 @@ {% extends template('widget') %} -{% define config = { - name: 'image-search-ai', - jsName: 'js-image-search-ai', -} %} - {% block body %} - + {% include molecule('image-uploader-popup', 'ImageSearchAiWidget') only %} {% endblock %} From 2d58b12356eb9de05d5b68249c86ef0c92cc46cb Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Mon, 9 Sep 2024 11:55:21 +0300 Subject: [PATCH 04/26] Fixed code review comments. --- composer.json | 5 ++-- .../Transfer/image_search_ai.transfer.xml | 6 ++--- .../Controller/ImageSearchController.php | 25 +++++++++++++++++ .../ImageSearchAiToCatalogClientBridge.php | 6 ++--- .../ImageSearchAiToCatalogClientInterface.php | 2 +- .../ImageSearchAiToOpenAiClientBridge.php | 3 +-- ...ageSearchAiToUtilEncodingServiceBridge.php | 4 +-- .../ImageSearchAi/ImageSearchAiConfig.php | 2 +- .../ImageSearchAiDependencyProvider.php | 20 ++++++++++++++ .../ImageSearchAi/ImageSearchAiFactory.php | 12 ++++++++- .../ImageToSearchTermsTransformer.php | 6 ++--- .../Validator/Base64ImageValidator.php | 27 ++++++++++++------- .../Base64ImageValidatorInterface.php | 4 +-- 13 files changed, 91 insertions(+), 31 deletions(-) diff --git a/composer.json b/composer.json index 70a04ba..a8880af 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,9 @@ "require": { "php": ">=8.1", "spryker/kernel": "^3.73.0", - "spryker/catalog": "^5.10.0", - "spryker/util-encoding": "^2.1.1", + "spryker/transfer": "^3.0.0", + "spryker/catalog": "^5.0.0", + "spryker/util-encoding": "^2.0.0", "spryker-eco/open-ai": "dev-feature/demo/dev-ai-integrations" }, "require-dev": { diff --git a/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml index 189a1d4..56098e8 100644 --- a/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml +++ b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml @@ -1,17 +1,17 @@ - + - + - + diff --git a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php index 40dba99..a80ebd3 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php @@ -7,6 +7,7 @@ namespace SprykerEco\Yves\ImageSearchAi\Controller; +use Codeception\Util\HttpCode; use Generated\Shared\Transfer\ImageSearchFindTermsErrorMessageTransfer; use Generated\Shared\Transfer\ImageSearchFindTermsErrorResponseTransfer; use Generated\Shared\Transfer\ImageSearchFindTermsResponseTransfer; @@ -26,6 +27,11 @@ class ImageSearchController extends AbstractController */ protected const REQUEST_BODY_CONTENT_KEY_IMAGE = 'image'; + /** + * @var string + */ + protected const REQUEST_BODY_CONTENT_KEY_TOKEN = '_token'; + /** * @param \Symfony\Component\HttpFoundation\Request $request * @@ -37,6 +43,14 @@ public function findTermsAction(Request $request): JsonResponse ->getUtilEncodingService() ->decodeJson((string)$request->getContent(), true); + if (!$this->getFactory()->getCsrfTokenManager()->isTokenValid($requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN])) { + return $this->createAjaxErrorResponse([ + 'error' => 'form.csrf.error.text', + ], HttpCode::BAD_REQUEST); + } + + unset($requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]); + $errors = $this->getFactory()->createBase64ImageValidator()->validate($requestBodyContent); if (count($errors)) { $errorResponse = new ImageSearchFindTermsErrorResponseTransfer(); @@ -68,4 +82,15 @@ public function findTermsAction(Request $request): JsonResponse 'firstMatchProductUrl' => Url::generate($searchResults['suggestionByType']['product_abstract'][0]['url'])->build(), ]); } + + /** + * @param array $errorData + * @param int $statusCode + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + protected function createAjaxErrorResponse(array $errorData, int $statusCode): JsonResponse + { + return $this->jsonResponse($errorData, $statusCode); + } } diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php index eae2e2d..c4c7f21 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php @@ -7,14 +7,12 @@ namespace SprykerEco\Yves\ImageSearchAi\Dependency\Client; -use Spryker\Client\Catalog\CatalogClientInterface; - class ImageSearchAiToCatalogClientBridge implements ImageSearchAiToCatalogClientInterface { /** * @var \Spryker\Client\Catalog\CatalogClientInterface */ - protected CatalogClientInterface $catalogClient; + protected $catalogClient; /** * @param \Spryker\Client\Catalog\CatalogClientInterface $catalogClient @@ -30,7 +28,7 @@ public function __construct($catalogClient) * * @return array */ - public function catalogSuggestSearch($searchString, array $requestParameters = []) + public function catalogSuggestSearch($searchString, array $requestParameters = []): array { return $this->catalogClient->catalogSuggestSearch($searchString, $requestParameters); } diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php index b23a398..53a1108 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php @@ -15,5 +15,5 @@ interface ImageSearchAiToCatalogClientInterface * * @return array */ - public function catalogSuggestSearch($searchString, array $requestParameters = []); + public function catalogSuggestSearch($searchString, array $requestParameters = []): array; } diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientBridge.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientBridge.php index 9063db6..ea76c38 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientBridge.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToOpenAiClientBridge.php @@ -9,14 +9,13 @@ use Generated\Shared\Transfer\OpenAiChatRequestTransfer; use Generated\Shared\Transfer\OpenAiChatResponseTransfer; -use SprykerEco\Client\OpenAi\OpenAiClientInterface; class ImageSearchAiToOpenAiClientBridge implements ImageSearchAiToOpenAiClientInterface { /** * @var \SprykerEco\Client\OpenAi\OpenAiClientInterface */ - protected OpenAiClientInterface $openAiClient; + protected $openAiClient; /** * @param \SprykerEco\Client\OpenAi\OpenAiClientInterface $openAiClient diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php index 25cbf6a..b22839a 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php @@ -7,14 +7,12 @@ namespace SprykerEco\Yves\ImageSearchAi\Dependency\Service; -use Spryker\Service\UtilEncoding\UtilEncodingServiceInterface; - class ImageSearchAiToUtilEncodingServiceBridge implements ImageSearchAiToUtilEncodingServiceInterface { /** * @var \Spryker\Service\UtilEncoding\UtilEncodingServiceInterface */ - protected UtilEncodingServiceInterface $utilEncodingService; + protected $utilEncodingService; /** * @param \Spryker\Service\UtilEncoding\UtilEncodingServiceInterface $utilEncodingService diff --git a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php index 6f720e9..1437b53 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php +++ b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php @@ -21,7 +21,7 @@ class ImageSearchAiConfig extends AbstractBundleConfig * * @return array */ - public static function getAllowedMimeTypes(): array + public function getAllowedMimeTypes(): array { return [ 'image/jpeg', diff --git a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php index 60e0ae5..c935fa0 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php +++ b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php @@ -36,6 +36,11 @@ class ImageSearchAiDependencyProvider extends AbstractBundleDependencyProvider */ public const CLIENT_OPEN_AI = 'CLIENT_OPEN_AI'; + /** + * @var string + */ + public const SERVICE_FORM_CSRF_PROVIDER = 'form.csrf_provider'; + /** * @param \Spryker\Yves\Kernel\Container $container * @@ -46,6 +51,7 @@ public function provideDependencies(Container $container): Container $container = $this->addCatalogClient($container); $container = $this->addOpenAiClient($container); $container = $this->addUtilEncodingService($container); + $container = $this->addCsrfProviderService($container); return $container; } @@ -100,4 +106,18 @@ function (Container $container): ImageSearchAiToOpenAiClientInterface { return $container; } + + /** + * @param \Spryker\Yves\Kernel\Container $container + * + * @return \Spryker\Yves\Kernel\Container + */ + protected function addCsrfProviderService(Container $container): Container + { + $container->set(static::SERVICE_FORM_CSRF_PROVIDER, function (Container $container) { + return $container->getApplicationService(static::SERVICE_FORM_CSRF_PROVIDER); + }); + + return $container; + } } diff --git a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php index 4076f87..e41d7a5 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php +++ b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php @@ -14,6 +14,7 @@ use SprykerEco\Yves\ImageSearchAi\Transformer\ImageToSearchTermsTransformer; use SprykerEco\Yves\ImageSearchAi\Validator\Base64ImageValidator; use SprykerEco\Yves\ImageSearchAi\Validator\Base64ImageValidatorInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -56,6 +57,7 @@ public function createBase64ImageValidator(): Base64ImageValidatorInterface { return new Base64ImageValidator( $this->createValidator(), + $this->getConfig(), ); } @@ -70,8 +72,16 @@ public function createValidator(): ValidatorInterface /** * @return \SprykerEco\Yves\ImageSearchAi\Dependency\Client\ImageSearchAiToOpenAiClientInterface */ - protected function getOpenAiClient(): ImageSearchAiToOpenAiClientInterface + public function getOpenAiClient(): ImageSearchAiToOpenAiClientInterface { return $this->getProvidedDependency(ImageSearchAiDependencyProvider::CLIENT_OPEN_AI); } + + /** + * @return \Symfony\Component\Security\Csrf\CsrfTokenManagerInterface + */ + public function getCsrfTokenManager(): CsrfTokenManagerInterface + { + return $this->getProvidedDependency(ImageSearchAiDependencyProvider::SERVICE_FORM_CSRF_PROVIDER); + } } diff --git a/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php b/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php index b9d4bd5..816c1c7 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php @@ -26,14 +26,14 @@ class ImageToSearchTermsTransformer implements ImageToSearchTermsTransformerInte /** * @param \SprykerEco\Yves\ImageSearchAi\Dependency\Client\ImageSearchAiToOpenAiClientInterface $openAiClient - * @param \SprykerEco\Yves\ImageSearchAi\ImageSearchAiConfig $config + * @param \SprykerEco\Yves\ImageSearchAi\ImageSearchAiConfig $imageSearchAiConfig */ public function __construct( ImageSearchAiToOpenAiClientInterface $openAiClient, - ImageSearchAiConfig $config + ImageSearchAiConfig $imageSearchAiConfig ) { $this->openAiClient = $openAiClient; - $this->config = $config; + $this->config = $imageSearchAiConfig; } /** diff --git a/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidator.php b/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidator.php index a225560..a1de293 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidator.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidator.php @@ -23,26 +23,35 @@ class Base64ImageValidator implements Base64ImageValidatorInterface */ protected ValidatorInterface $validator; + /** + * @var \SprykerEco\Yves\ImageSearchAi\ImageSearchAiConfig + */ + protected ImageSearchAiConfig $config; + /** * @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator + * @param \SprykerEco\Yves\ImageSearchAi\ImageSearchAiConfig $imageSearchAiConfig */ - public function __construct(ValidatorInterface $validator) - { + public function __construct( + ValidatorInterface $validator, + ImageSearchAiConfig $imageSearchAiConfig + ) { $this->validator = $validator; + $this->config = $imageSearchAiConfig; } /** - * @param array $requestBodyContent + * @param array $imageContent * * @return \Symfony\Component\Validator\ConstraintViolationListInterface */ - public function validate(array $requestBodyContent): ConstraintViolationListInterface + public function validate(array $imageContent): ConstraintViolationListInterface { $constraint = new Collection( $this->getBase64ImageFieldValidationConstraints(), ); - return $this->validator->validate($requestBodyContent, $constraint); + return $this->validator->validate($imageContent, $constraint); } /** @@ -53,7 +62,7 @@ protected function getBase64ImageFieldValidationConstraints(): array return [ 'image' => [ new NotBlank(), - new Callback([static::class, 'validateIsBase64']), + new Callback(['callback' => [$this, 'validateIsBase64']]), ], ]; } @@ -64,7 +73,7 @@ protected function getBase64ImageFieldValidationConstraints(): array * * @return void */ - public static function validateIsBase64(mixed $value, ExecutionContextInterface $context): void + public function validateIsBase64(mixed $value, ExecutionContextInterface $context): void { $value = preg_replace('#data:image/[^;]+;base64,#', '', $value); @@ -81,11 +90,11 @@ public static function validateIsBase64(mixed $value, ExecutionContextInterface $mimeType = $mimeTypes->guessMimeType($tmpFilename); unlink($tmpFilename); - if (!in_array($mimeType, ImageSearchAiConfig::getAllowedMimeTypes(), true)) { + if (!in_array($mimeType, $this->config->getAllowedMimeTypes(), true)) { $context->buildViolation(sprintf( 'Invalid mime type %s. Accepted types are %s.', $mimeType, - implode(', ', ImageSearchAiConfig::getAllowedMimeTypes()), + implode(', ', $this->config->getAllowedMimeTypes()), )) ->atPath('image') ->addViolation(); diff --git a/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php index 9d26fa4..508b8da 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php @@ -12,9 +12,9 @@ interface Base64ImageValidatorInterface { /** - * @param array $requestBodyContent + * @param array $imageContent * * @return \Symfony\Component\Validator\ConstraintViolationListInterface */ - public function validate(array $requestBodyContent): ConstraintViolationListInterface; + public function validate(array $imageContent): ConstraintViolationListInterface; } From 9823d08552c630df7a4fdf11c25af4b4134ba8bd Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Mon, 9 Sep 2024 12:17:05 +0300 Subject: [PATCH 05/26] Fixed code review comments. --- .../Client/ImageSearchAiToCatalogClientBridge.php | 2 +- .../Client/ImageSearchAiToCatalogClientInterface.php | 2 +- .../Service/ImageSearchAiToUtilEncodingServiceBridge.php | 2 +- .../ImageSearchAiToUtilEncodingServiceInterface.php | 2 +- .../Yves/ImageSearchAi/Validator/Base64ImageValidator.php | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php index c4c7f21..2a4f41e 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php @@ -28,7 +28,7 @@ public function __construct($catalogClient) * * @return array */ - public function catalogSuggestSearch($searchString, array $requestParameters = []): array + public function catalogSuggestSearch(string $searchString, array $requestParameters = []): array { return $this->catalogClient->catalogSuggestSearch($searchString, $requestParameters); } diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php index 53a1108..5b96471 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientInterface.php @@ -15,5 +15,5 @@ interface ImageSearchAiToCatalogClientInterface * * @return array */ - public function catalogSuggestSearch($searchString, array $requestParameters = []): array; + public function catalogSuggestSearch(string $searchString, array $requestParameters = []): array; } diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php index b22839a..2cee94c 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php @@ -30,7 +30,7 @@ public function __construct($utilEncodingService) * * @return object|array|null */ - public function decodeJson($jsonValue, $assoc = false, $depth = null, $options = null) + public function decodeJson(string $jsonValue, bool $assoc = false, ?int $depth = null, ?int $options = null): ?array { return $this->utilEncodingService->decodeJson($jsonValue, $assoc, $depth, $options); } diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php index 69c5b1f..77899c0 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php @@ -17,5 +17,5 @@ interface ImageSearchAiToUtilEncodingServiceInterface * * @return object|array|null */ - public function decodeJson($jsonValue, $assoc = false, $depth = null, $options = null); + public function decodeJson(string $jsonValue, bool $assoc = false, ?int $depth = null, ?int $options = null): ?array; } diff --git a/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidator.php b/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidator.php index a1de293..ee4cb2d 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidator.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidator.php @@ -26,7 +26,7 @@ class Base64ImageValidator implements Base64ImageValidatorInterface /** * @var \SprykerEco\Yves\ImageSearchAi\ImageSearchAiConfig */ - protected ImageSearchAiConfig $config; + protected ImageSearchAiConfig $imageSearchAiConfig; /** * @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator @@ -37,7 +37,7 @@ public function __construct( ImageSearchAiConfig $imageSearchAiConfig ) { $this->validator = $validator; - $this->config = $imageSearchAiConfig; + $this->imageSearchAiConfig = $imageSearchAiConfig; } /** @@ -90,11 +90,11 @@ public function validateIsBase64(mixed $value, ExecutionContextInterface $contex $mimeType = $mimeTypes->guessMimeType($tmpFilename); unlink($tmpFilename); - if (!in_array($mimeType, $this->config->getAllowedMimeTypes(), true)) { + if (!in_array($mimeType, $this->imageSearchAiConfig->getAllowedMimeTypes(), true)) { $context->buildViolation(sprintf( 'Invalid mime type %s. Accepted types are %s.', $mimeType, - implode(', ', $this->config->getAllowedMimeTypes()), + implode(', ', $this->imageSearchAiConfig->getAllowedMimeTypes()), )) ->atPath('image') ->addViolation(); From 30ec25c7de90568e567672cef3266f9968dab080 Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Mon, 9 Sep 2024 13:10:17 +0300 Subject: [PATCH 06/26] Fixed CSRF token. --- .../Controller/ImageSearchController.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php index a80ebd3..1465fa2 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Csrf\CsrfToken; /** * @method \SprykerEco\Yves\ImageSearchAi\ImageSearchAiFactory getFactory() @@ -32,6 +33,11 @@ class ImageSearchController extends AbstractController */ protected const REQUEST_BODY_CONTENT_KEY_TOKEN = '_token'; + /** + * @var string + */ + protected const CSRF_TOKEN_ID = 'image_search_ai_csrf'; + /** * @param \Symfony\Component\HttpFoundation\Request $request * @@ -43,7 +49,12 @@ public function findTermsAction(Request $request): JsonResponse ->getUtilEncodingService() ->decodeJson((string)$request->getContent(), true); - if (!$this->getFactory()->getCsrfTokenManager()->isTokenValid($requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN])) { + if ( + !isset($requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]) || + !$this->getFactory()->getCsrfTokenManager()->isTokenValid( + new CsrfToken(static::CSRF_TOKEN_ID, $requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]) + ) + ) { return $this->createAjaxErrorResponse([ 'error' => 'form.csrf.error.text', ], HttpCode::BAD_REQUEST); From e943a39acdc7a965fd0c4e0df39cc5a5f05c83bd Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Mon, 9 Sep 2024 13:11:50 +0300 Subject: [PATCH 07/26] Fixed code style. --- .../Yves/ImageSearchAi/Controller/ImageSearchController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php index 1465fa2..15e298a 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php @@ -52,7 +52,7 @@ public function findTermsAction(Request $request): JsonResponse if ( !isset($requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]) || !$this->getFactory()->getCsrfTokenManager()->isTokenValid( - new CsrfToken(static::CSRF_TOKEN_ID, $requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]) + new CsrfToken(static::CSRF_TOKEN_ID, $requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]), ) ) { return $this->createAjaxErrorResponse([ From 5a59a0ca51d9ee8b0985b215729045ffb83995ae Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Mon, 9 Sep 2024 18:58:15 +0300 Subject: [PATCH 08/26] Fixed transfer quotes. --- .../ImageSearchAi/Transfer/image_search_ai.transfer.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml index 56098e8..1c47482 100644 --- a/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml +++ b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml @@ -1,17 +1,17 @@ - + - + - + From 0fb58ed696689a6aca3d2c2294841168a53ced7c Mon Sep 17 00:00:00 2001 From: Dmytro Asieiev Date: Tue, 10 Sep 2024 16:07:59 +0300 Subject: [PATCH 09/26] Adjusted image search ai module. --- .gitattributes | 10 ++-- .gitignore | 47 ++++++++----------- .license | 4 ++ README.md | 2 +- composer.json | 23 ++++----- dependency.json | 5 ++ phpstan.neon | 8 +++- .../Controller/ImageSearchController.php | 7 ++- .../ImageSearchAiToCatalogClientBridge.php | 4 +- .../ImageSearchAiToCatalogClientInterface.php | 4 +- .../ImageSearchAiToOpenAiClientBridge.php | 4 +- .../ImageSearchAiToOpenAiClientInterface.php | 4 +- ...ageSearchAiToUtilEncodingServiceBridge.php | 6 +-- ...SearchAiToUtilEncodingServiceInterface.php | 6 +-- .../ImageSearchAi/ImageSearchAiConfig.php | 19 +++++++- .../ImageSearchAiDependencyProvider.php | 28 ++++------- .../ImageSearchAi/ImageSearchAiFactory.php | 4 +- .../ImageSearchAiRouteProviderPlugin.php | 13 ++--- .../image-uploader-popup.twig | 2 +- .../image-uploader/image-uploader.scss | 0 .../image-uploader/image-uploader.ts | 0 .../image-uploader/image-uploader.twig | 0 .../molecules/image-uploader/index.ts | 0 .../image-search-ai/image-search-ai.twig | 5 ++ .../ImageToSearchTermsTransformer.php | 12 ++--- ...ImageToSearchTermsTransformerInterface.php | 4 +- .../Validator/Base64ImageValidator.php | 5 +- .../Base64ImageValidatorInterface.php | 4 +- .../Widget/ImageSearchAiWidget.php | 8 ++-- .../image-search-ai/image-search-ai.twig | 5 -- tooling.yml | 2 +- 31 files changed, 124 insertions(+), 121 deletions(-) create mode 100644 .license create mode 100644 dependency.json rename src/SprykerEco/Yves/{ImageSearchAiWidget => ImageSearchAi}/Theme/default/components/molecules/image-uploader-popup/image-uploader-popup.twig (95%) rename src/SprykerEco/Yves/{ImageSearchAiWidget => ImageSearchAi}/Theme/default/components/molecules/image-uploader/image-uploader.scss (100%) rename src/SprykerEco/Yves/{ImageSearchAiWidget => ImageSearchAi}/Theme/default/components/molecules/image-uploader/image-uploader.ts (100%) rename src/SprykerEco/Yves/{ImageSearchAiWidget => ImageSearchAi}/Theme/default/components/molecules/image-uploader/image-uploader.twig (100%) rename src/SprykerEco/Yves/{ImageSearchAiWidget => ImageSearchAi}/Theme/default/components/molecules/image-uploader/index.ts (100%) create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Theme/default/views/image-search-ai/image-search-ai.twig rename src/SprykerEco/Yves/{ImageSearchAiWidget => ImageSearchAi}/Widget/ImageSearchAiWidget.php (53%) delete mode 100644 src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/views/image-search-ai/image-search-ai.twig diff --git a/.gitattributes b/.gitattributes index babca79..7aa6a68 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ -# Define the line ending behavior of the different file extensions # Set the default behavior, in case people don't have core.autocrlf set. -* text text=auto eol=lf +* eol=lf +* text=auto # Denote all files that are truly binary and should not be modified. *.png binary @@ -22,14 +22,10 @@ # Remove files for archives generated using `git archive` dependency.json export-ignore -phpstan.json export-ignore -phpstan.neon export-ignore -tooling.yml export-ignore .coveralls.yml export-ignore .travis.yml export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore architecture-baseline.json export-ignore -psalm-report.json export-ignore -.github/ export-ignore +psalm-report.json export-ignore linguist-generated=true diff --git a/.gitignore b/.gitignore index 669cf87..09c33fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,30 @@ -# IDE -.idea/ -.project/ -nbproject/ -.buildpath/ -.settings/ +# IDEs +/.idea +/.project +/nbproject +/.buildpath +/.settings *.sublime-* - -# OS -.DS_Store *.AppleDouble *.AppleDB *.AppleDesktop +# built client resources +/src/*/Zed/*/Static/Public +/src/*/Zed/*/Static/Assets/sprite + +# propel classes +/src/*/Zed/*/Persistence/Propel/Base/* +/src/*/Zed/*/Persistence/Propel/Map/* + +# OS +.DS_Store + # grunt stuff .grunt .sass-cache /node_modules/ - -# tooling -vendor/ +/tests/_output/* +!/tests/_output/.gitkeep +vendor composer.lock -.phpunit.result.cache - -# built client resources -src/*/Zed/*/Static/Public -src/*/Zed/*/Static/Assets/sprite - -# Propel classes -src/*/Zed/*/Persistence/Propel/Base/* -src/*/Zed/*/Persistence/Propel/Map/* - -# tests -tests/**/_generated/ -tests/_output/* -!tests/_output/.gitkeep -tests/app/* diff --git a/.license b/.license new file mode 100644 index 0000000..591bbb5 --- /dev/null +++ b/.license @@ -0,0 +1,4 @@ +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ diff --git a/README.md b/README.md index 971a77c..73abf14 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ImageSearchAi Module -ImageSearchAi Module. +ImageSearchAi module provides the functionality to search by image using AI. ## Installation diff --git a/composer.json b/composer.json index a8880af..06921f8 100644 --- a/composer.json +++ b/composer.json @@ -5,31 +5,32 @@ "license": "MIT", "require": { "php": ">=8.1", - "spryker/kernel": "^3.73.0", - "spryker/transfer": "^3.0.0", + "spryker-eco/open-ai": "dev-feature/demo/dev-ai-integrations", "spryker/catalog": "^5.0.0", + "spryker/kernel": "^3.73.0", + "spryker/symfony": "^3.0.0", + "spryker/transfer": "^3.27.0", "spryker/util-encoding": "^2.0.0", - "spryker-eco/open-ai": "dev-feature/demo/dev-ai-integrations" + "spryker/util-text": "^1.1.0" }, "require-dev": { "phpstan/phpstan": "*", - "spryker/code-sniffer": "*" + "spryker/code-sniffer": "*", + "spryker/router": "*" + }, + "suggest": { + "spryker/router": "Use this module when you want to use the Router." }, "autoload": { "psr-4": { "SprykerEco\\": "src/SprykerEco/" } }, - "autoload-dev": { - "psr-4": { - "SprykerEcoTest\\": "tests/SprykerEcoTest/" - } - }, "minimum-stability": "dev", "prefer-stable": true, "scripts": { - "cs-check": "phpcs -p -s --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/ tests/", - "cs-fix": "phpcbf -p --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/ tests/", + "cs-check": "phpcs -p -s --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", + "cs-fix": "phpcbf -p --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", "stan": "phpstan analyse -c phpstan.neon -l 6 src/", "stan-setup": "cp composer.json composer.backup && COMPOSER_MEMORY_LIMIT=-1 composer require --dev phpstan/phpstan:^0.12 && mv composer.backup composer.json" }, diff --git a/dependency.json b/dependency.json new file mode 100644 index 0000000..1932e90 --- /dev/null +++ b/dependency.json @@ -0,0 +1,5 @@ +{ + "include": { + "spryker/transfer": "Provides transfer objects definition with strict types." + } +} diff --git a/phpstan.neon b/phpstan.neon index e435fc2..7f92b02 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,9 @@ parameters: level: 8 - checkGenericClassInNonGenericObjectType: false + paths: + - src/ + ignoreErrors: + - '#Call to method .+ on an unknown class .+Transfer#' + - '#Parameter .+ of method .+ has invalid typehint type .+Transfer#' + - '#Return typehint of method .+ has invalid type .+Transfer#' + - '#Instantiated class .+Transfer not found#' diff --git a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php index 15e298a..125641a 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php @@ -1,13 +1,12 @@ createAjaxErrorResponse([ 'error' => 'form.csrf.error.text', - ], HttpCode::BAD_REQUEST); + ], Response::HTTP_BAD_REQUEST); } unset($requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]); diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php index 2a4f41e..b2ecf83 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Client/ImageSearchAiToCatalogClientBridge.php @@ -1,8 +1,8 @@ |null + * @return array|null */ public function decodeJson(string $jsonValue, bool $assoc = false, ?int $depth = null, ?int $options = null): ?array { diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php index 77899c0..faff07e 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php @@ -1,8 +1,8 @@ |null + * @return array|null */ public function decodeJson(string $jsonValue, bool $assoc = false, ?int $depth = null, ?int $options = null): ?array; } diff --git a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php index 1437b53..e925f9a 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php +++ b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiConfig.php @@ -1,8 +1,8 @@ set( - static::CLIENT_CATALOG, - function (Container $container): ImageSearchAiToCatalogClientInterface { + $container->set(static::CLIENT_CATALOG, function (Container $container) { return new ImageSearchAiToCatalogClientBridge($container->getLocator()->catalog()->client()); - }, - ); + }); return $container; } @@ -80,12 +74,9 @@ function (Container $container): ImageSearchAiToCatalogClientInterface { */ protected function addUtilEncodingService(Container $container): Container { - $container->set( - static::SERVICE_UTIL_ENCODING, - function (Container $container): ImageSearchAiToUtilEncodingServiceInterface { + $container->set(static::SERVICE_UTIL_ENCODING, function (Container $container) { return new ImageSearchAiToUtilEncodingServiceBridge($container->getLocator()->utilEncoding()->service()); - }, - ); + }); return $container; } @@ -97,12 +88,9 @@ function (Container $container): ImageSearchAiToUtilEncodingServiceInterface { */ protected function addOpenAiClient(Container $container): Container { - $container->set( - static::CLIENT_OPEN_AI, - function (Container $container): ImageSearchAiToOpenAiClientInterface { + $container->set(static::CLIENT_OPEN_AI, function (Container $container) { return new ImageSearchAiToOpenAiClientBridge($container->getLocator()->openAi()->client()); - }, - ); + }); return $container; } diff --git a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php index e41d7a5..99ba711 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php +++ b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php @@ -1,8 +1,8 @@ buildRoute( - '/search-ai/image', - 'ImageSearchAi', - 'ImageSearch', - 'findTermsAction', - )->setMethods(Request::METHOD_POST); - + $route = $this->buildRoute('/search-ai/image', 'ImageSearchAi', 'ImageSearch', 'findTermsAction'); + $route = $route->setMethods(Request::METHOD_POST); $routeCollection->add(static::ROUTE_IMAGE_SEARCH, $route); return $routeCollection; diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader-popup/image-uploader-popup.twig b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader-popup/image-uploader-popup.twig similarity index 95% rename from src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader-popup/image-uploader-popup.twig rename to src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader-popup/image-uploader-popup.twig index 4ca37fd..4eecb96 100644 --- a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader-popup/image-uploader-popup.twig +++ b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader-popup/image-uploader-popup.twig @@ -11,7 +11,7 @@ } %} {% block content %} - {% include molecule('image-uploader', 'ImageSearchAiWidget') only %} + {% include molecule('image-uploader', 'ImageSearchAi') only %} {% endblock %} {% block body %} diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.scss b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.scss similarity index 100% rename from src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.scss rename to src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.scss diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.ts b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.ts similarity index 100% rename from src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.ts rename to src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.ts diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.twig b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.twig similarity index 100% rename from src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.twig rename to src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.twig diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/index.ts b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/index.ts similarity index 100% rename from src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/index.ts rename to src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/index.ts diff --git a/src/SprykerEco/Yves/ImageSearchAi/Theme/default/views/image-search-ai/image-search-ai.twig b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/views/image-search-ai/image-search-ai.twig new file mode 100644 index 0000000..cb40c31 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/views/image-search-ai/image-search-ai.twig @@ -0,0 +1,5 @@ +{% extends template('widget') %} + +{% block body %} + {% include molecule('image-uploader-popup', 'ImageSearchAi') only %} +{% endblock %} diff --git a/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php b/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php index 816c1c7..5ddf937 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformer.php @@ -1,8 +1,8 @@ openAiClient = $openAiClient; - $this->config = $imageSearchAiConfig; + $this->imageSearchAiConfig = $imageSearchAiConfig; } /** @@ -46,7 +46,7 @@ public function transform(string $base64Image): OpenAiChatResponseTransfer $openAiChatRequestTransfer = (new OpenAiChatRequestTransfer())->setPromptData([ [ 'type' => 'text', - 'text' => 'Describe the most important characteristics of the main object you can identify in the image e.g. manufacturer, model, color, part number or any identification number that help me to find the product using a search engine. Your output must be ony a list of the product attributes. Remove the attribute names and keep only the values from your output and provide them in a single line.', + 'text' => $this->imageSearchAiConfig->getOpenAiImageSearchPrompt(), ], [ 'type' => 'image_url', @@ -54,7 +54,7 @@ public function transform(string $base64Image): OpenAiChatResponseTransfer 'url' => $base64Image, ], ], - ])->setModel($this->config->getOpenAiModel()); + ])->setModel($this->imageSearchAiConfig->getOpenAiModel()); return $this->openAiClient->chat($openAiChatRequestTransfer); } diff --git a/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformerInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformerInterface.php index b429668..6044f41 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformerInterface.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Transformer/ImageToSearchTermsTransformerInterface.php @@ -1,8 +1,8 @@ addViolation(); } + /** @var string $tmpFilename */ $tmpFilename = tempnam(sys_get_temp_dir(), 'guessMimeType_'); file_put_contents($tmpFilename, $decodedFile); $mimeTypes = new MimeTypes(); diff --git a/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php index 508b8da..b08386d 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Validator/Base64ImageValidatorInterface.php @@ -1,8 +1,8 @@ Date: Tue, 10 Sep 2024 16:19:30 +0300 Subject: [PATCH 10/26] Added CI. --- .github/workflows/ci.yml | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..99d049b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: + - 'master' + pull_request: + workflow_dispatch: + +jobs: + validation: + name: Validation + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: mbstring, intl, bcmath + coverage: none + + - name: Composer Install + run: composer install --prefer-dist --no-interaction --profile + + - name: Run validation + run: composer validate + + - name: Syntax check + run: find ./src -path src -prune -o -type f -name '*.php' -print0 | xargs -0 -n1 -P4 php -l -n | (! grep -v "No syntax errors detected" ) + + - name: Run CodeStyle checks + run: composer cs-check + + - name: PHPStan setup + run: composer stan-setup + + - name: Run PHPStan + run: composer stan + + lowest: + name: Prefer Lowest + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + extensions: mbstring, intl, bcmath + coverage: none + + - name: Composer Install + run: composer install --prefer-dist --no-interaction --profile + + - name: Composer Update + run: composer update --prefer-lowest --prefer-dist --no-interaction --profile -vvv From 08dc66a441b132b77390bed0718a311ba26c3d8c Mon Sep 17 00:00:00 2001 From: Dmytro Asieiev Date: Tue, 10 Sep 2024 16:29:11 +0300 Subject: [PATCH 11/26] Adjusted CI. --- composer.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/composer.json b/composer.json index 06921f8..aa13e2f 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,12 @@ }, "minimum-stability": "dev", "prefer-stable": true, + "repositories": [ + { + "type": "vcs", + "url": "git@github.com:spryker-eco/open-ai.git" + } + ], "scripts": { "cs-check": "phpcs -p -s --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", "cs-fix": "phpcbf -p --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", From 34f1d08c04b198db2bee594069e6a57fd8e98ea5 Mon Sep 17 00:00:00 2001 From: Dmytro Asieiev Date: Tue, 10 Sep 2024 16:40:08 +0300 Subject: [PATCH 12/26] Adjusted CI. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index aa13e2f..2bd845b 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "scripts": { "cs-check": "phpcs -p -s --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", "cs-fix": "phpcbf -p --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", - "stan": "phpstan analyse -c phpstan.neon -l 6 src/", + "stan": "phpstan analyse -c phpstan.neon -l 8 src/", "stan-setup": "cp composer.json composer.backup && COMPOSER_MEMORY_LIMIT=-1 composer require --dev phpstan/phpstan:^0.12 && mv composer.backup composer.json" }, "extra": { From b66a878e9bb3a85cd11c6b49228e6030b18afb38 Mon Sep 17 00:00:00 2001 From: Aleksey Belan Date: Tue, 10 Sep 2024 19:07:59 +0300 Subject: [PATCH 13/26] add handling empty result --- .../components/molecules/image-uploader/image-uploader.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.ts b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.ts index a27aafe..1b5926f 100644 --- a/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.ts +++ b/src/SprykerEco/Yves/ImageSearchAiWidget/Theme/default/components/molecules/image-uploader/image-uploader.ts @@ -8,7 +8,7 @@ export default class ImageUploader extends Component { error: 'is-error', } - protected readyCallback(): void {} + protected readyCallback(): void { } protected init(): void { this.fields = [...this.querySelectorAll(`.${this.jsName}__file-input`)]; @@ -77,6 +77,12 @@ export default class ImageUploader extends Component { }); const data = await response.json(); + if (!data?.firstMatchProductUrl) { + this.classList.add(this.states.error); + + return; + } + window.location.href = data.firstMatchProductUrl; } catch (error) { this.classList.add(this.states.error); From 1137ae45b96735e5a332abfde86e12c49404c4cd Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Wed, 11 Sep 2024 14:38:36 +0300 Subject: [PATCH 14/26] Added ImageSearchAiValidator.php. --- .../Transfer/image_search_ai.transfer.xml | 13 +-- .../Controller/ImageSearchController.php | 61 +++---------- .../ImageSearchAiDependencyProvider.php | 2 + .../ImageSearchAi/ImageSearchAiFactory.php | 13 +++ .../Validator/ImageSearchAiValidator.php | 88 +++++++++++++++++++ .../ImageSearchAiValidatorInterface.php | 18 ++++ 6 files changed, 135 insertions(+), 60 deletions(-) create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidator.php create mode 100644 src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidatorInterface.php diff --git a/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml index 1c47482..77d1523 100644 --- a/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml +++ b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml @@ -1,17 +1,12 @@ - - - + + + - - - - - - + diff --git a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php index 125641a..2870598 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php @@ -7,15 +7,11 @@ namespace SprykerEco\Yves\ImageSearchAi\Controller; -use Generated\Shared\Transfer\ImageSearchFindTermsErrorMessageTransfer; -use Generated\Shared\Transfer\ImageSearchFindTermsErrorResponseTransfer; -use Generated\Shared\Transfer\ImageSearchFindTermsResponseTransfer; use Spryker\Service\UtilText\Model\Url\Url; use Spryker\Yves\Kernel\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Csrf\CsrfToken; /** * @method \SprykerEco\Yves\ImageSearchAi\ImageSearchAiFactory getFactory() @@ -27,16 +23,6 @@ class ImageSearchController extends AbstractController */ protected const REQUEST_BODY_CONTENT_KEY_IMAGE = 'image'; - /** - * @var string - */ - protected const REQUEST_BODY_CONTENT_KEY_TOKEN = '_token'; - - /** - * @var string - */ - protected const CSRF_TOKEN_ID = 'image_search_ai_csrf'; - /** * @param \Symfony\Component\HttpFoundation\Request $request * @@ -48,30 +34,12 @@ public function findTermsAction(Request $request): JsonResponse ->getUtilEncodingService() ->decodeJson((string)$request->getContent(), true); - if ( - !isset($requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]) || - !$this->getFactory()->getCsrfTokenManager()->isTokenValid( - new CsrfToken(static::CSRF_TOKEN_ID, $requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]), - ) - ) { - return $this->createAjaxErrorResponse([ - 'error' => 'form.csrf.error.text', - ], Response::HTTP_BAD_REQUEST); - } - - unset($requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]); - - $errors = $this->getFactory()->createBase64ImageValidator()->validate($requestBodyContent); - if (count($errors)) { - $errorResponse = new ImageSearchFindTermsErrorResponseTransfer(); - $errorResponse->setError('Bad request'); - foreach ($errors as $error) { - $errorMessage = new ImageSearchFindTermsErrorMessageTransfer(); - $errorMessage->setMessage($error->getMessage()); - $errorResponse->addImageSearchFindTermsErrorMessage($errorMessage); - } + $isRequestValid = $this->getFactory() + ->createImageSearchAiValidator() + ->validate($requestBodyContent); - return new JsonResponse($errorResponse->toArray(), Response::HTTP_BAD_REQUEST); + if (!$isRequestValid) { + return $this->jsonResponse(['success' => false], Response::HTTP_BAD_REQUEST); } $searchString = $this->getFactory()->createImageToSearchTermsTransformer()->transform( @@ -79,7 +47,7 @@ public function findTermsAction(Request $request): JsonResponse )->getMessage(); if (!$searchString) { - return new JsonResponse((new ImageSearchFindTermsResponseTransfer())->toArray()); + return $this->jsonResponse(); } $searchResults = $this @@ -88,19 +56,10 @@ public function findTermsAction(Request $request): JsonResponse ->catalogSuggestSearch($searchString, []); return new JsonResponse([ - 'searchUrl' => Url::generate('/search', ['q' => $searchString])->build(), - 'firstMatchProductUrl' => Url::generate($searchResults['suggestionByType']['product_abstract'][0]['url'])->build(), + 'success' => true, + 'firstMatchProductUrl' => !empty($searchResults['suggestionByType']['product_abstract'][0]['url']) + ? Url::generate($searchResults['suggestionByType']['product_abstract'][0]['url'])->build() + : '', ]); } - - /** - * @param array $errorData - * @param int $statusCode - * - * @return \Symfony\Component\HttpFoundation\JsonResponse - */ - protected function createAjaxErrorResponse(array $errorData, int $statusCode): JsonResponse - { - return $this->jsonResponse($errorData, $statusCode); - } } diff --git a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php index 02a9f9d..1c701bb 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php +++ b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiDependencyProvider.php @@ -45,6 +45,8 @@ class ImageSearchAiDependencyProvider extends AbstractBundleDependencyProvider */ public function provideDependencies(Container $container): Container { + $container = parent::provideDependencies($container); + $container = $this->addCatalogClient($container); $container = $this->addOpenAiClient($container); $container = $this->addUtilEncodingService($container); diff --git a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php index 99ba711..dabee6a 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php +++ b/src/SprykerEco/Yves/ImageSearchAi/ImageSearchAiFactory.php @@ -14,6 +14,8 @@ use SprykerEco\Yves\ImageSearchAi\Transformer\ImageToSearchTermsTransformer; use SprykerEco\Yves\ImageSearchAi\Validator\Base64ImageValidator; use SprykerEco\Yves\ImageSearchAi\Validator\Base64ImageValidatorInterface; +use SprykerEco\Yves\ImageSearchAi\Validator\ImageSearchAiValidator; +use SprykerEco\Yves\ImageSearchAi\Validator\ImageSearchAiValidatorInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -34,6 +36,17 @@ public function createImageToSearchTermsTransformer(): ImageToSearchTermsTransfo ); } + /** + * @return \SprykerEco\Yves\ImageSearchAi\Validator\ImageSearchAiValidatorInterface + */ + public function createImageSearchAiValidator(): ImageSearchAiValidatorInterface + { + return new ImageSearchAiValidator( + $this->getCsrfTokenManager(), + $this->createBase64ImageValidator(), + ); + } + /** * @return \SprykerEco\Yves\ImageSearchAi\Dependency\Client\ImageSearchAiToCatalogClientInterface */ diff --git a/src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidator.php b/src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidator.php new file mode 100644 index 0000000..578ed2d --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidator.php @@ -0,0 +1,88 @@ +csrfTokenManager = $csrfTokenManager; + $this->base64ImageValidator = $base64ImageValidator; + } + + /** + * @param array $requestBodyContent + * + * @return bool + */ + public function validate(array $requestBodyContent): bool + { + return $this->isCsrfTokenValid($requestBodyContent) && + $this->isBase64ImageValid($requestBodyContent); + } + + /** + * @param array $requestBodyContent + * + * @return bool + */ + protected function isCsrfTokenValid(array $requestBodyContent): bool + { + return isset($requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]) && + $this->csrfTokenManager->isTokenValid( + new CsrfToken(static::CSRF_TOKEN_ID, $requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]), + ); + } + + /** + * @param array $requestBodyContent + * + * @return bool + */ + protected function isBase64ImageValid(array $requestBodyContent): bool + { + unset($requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_TOKEN]); + + $errors = $this->base64ImageValidator->validate($requestBodyContent); + + if (count($errors)) { + return false; + } + + return true; + } +} diff --git a/src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidatorInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidatorInterface.php new file mode 100644 index 0000000..19c4f63 --- /dev/null +++ b/src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidatorInterface.php @@ -0,0 +1,18 @@ + $requestBodyContent + * + * @return bool + */ + public function validate(array $requestBodyContent): bool; +} From cf8952be772a98070cd8e443c392ce5a2294f48f Mon Sep 17 00:00:00 2001 From: Aleksey Belan Date: Wed, 11 Sep 2024 14:49:27 +0300 Subject: [PATCH 15/26] add error state --- .../image-uploader/image-uploader.scss | 9 ++++++++- .../molecules/image-uploader/image-uploader.ts | 18 ++++++++++++------ .../image-uploader/image-uploader.twig | 8 +++++++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.scss b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.scss index 4854fee..a550413 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.scss +++ b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.scss @@ -17,12 +17,18 @@ } } - &.is-error & { + &.is-empty & { &__service-message { display: block; } } + &.is-error & { + &__error { + display: block; + } + } + &__spinner { position: absolute; top: 50%; @@ -30,6 +36,7 @@ translate: -50%; } + &__error, &__spinner, &__service-message { display: none; diff --git a/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.ts b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.ts index 1b5926f..f26fc25 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.ts +++ b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.ts @@ -5,6 +5,7 @@ export default class ImageUploader extends Component { protected fields: HTMLInputElement[]; protected states = { loading: 'is-loading', + empty: 'is-empty', error: 'is-error', } @@ -16,8 +17,7 @@ export default class ImageUploader extends Component { } protected mapEvents(): void { - this.classList.remove(this.states.error); - this.classList.remove(this.states.loading); + this.classList.remove(this.states.error, this.states.loading, this.states.empty); this.fields.forEach((field) => { field.addEventListener('change', this.onFileInputChange.bind(this)); @@ -67,22 +67,28 @@ export default class ImageUploader extends Component { } protected async sendRequest(image: string): Promise { + this.classList.remove(this.states.error, this.states.empty); this.classList.add(this.states.loading); try { - const response = await fetch(this.action, { + const data = await (await fetch(this.action, { method: 'POST', signal: AbortSignal.timeout(this.timeout), body: JSON.stringify({ image, _token: this._token }), - }); - const data = await response.json(); + })).json(); - if (!data?.firstMatchProductUrl) { + if (data.success) { this.classList.add(this.states.error); return; } + if (!data?.firstMatchProductUrl) { + this.classList.add(this.states.empty); + + return; + } + window.location.href = data.firstMatchProductUrl; } catch (error) { this.classList.add(this.states.error); diff --git a/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.twig b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.twig index 7d62bab..2fb74cc 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.twig +++ b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.twig @@ -39,10 +39,16 @@ {% block empty %} -
+
{{ 'search.with.your-images.no-results' | trans }}
{% endblock %} + + {% block error %} +
+ {{ 'search.with.your-images.error' | trans }} +
+ {% endblock %} {% endblock %} {% block info %} From d52cec12777115e28181bad0fb2f5a148428c0da Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Wed, 11 Sep 2024 15:10:25 +0300 Subject: [PATCH 16/26] Fixed CI. --- .../Yves/ImageSearchAi/Validator/ImageSearchAiValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidator.php b/src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidator.php index 578ed2d..5aea251 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidator.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Validator/ImageSearchAiValidator.php @@ -38,7 +38,7 @@ class ImageSearchAiValidator implements ImageSearchAiValidatorInterface */ public function __construct( CsrfTokenManagerInterface $csrfTokenManager, - Base64ImageValidatorInterface $base64ImageValidator, + Base64ImageValidatorInterface $base64ImageValidator ) { $this->csrfTokenManager = $csrfTokenManager; $this->base64ImageValidator = $base64ImageValidator; From ac54cf2e2b1a116e68dca86b625e0747fb85ce3c Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Wed, 11 Sep 2024 15:57:51 +0300 Subject: [PATCH 17/26] Fixed CI. --- .../Controller/ImageSearchController.php | 22 +++++++++++++++++-- ...ageSearchAiToUtilEncodingServiceBridge.php | 4 ++-- ...SearchAiToUtilEncodingServiceInterface.php | 4 ++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php index 2870598..52490fa 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php @@ -30,16 +30,26 @@ class ImageSearchController extends AbstractController */ public function findTermsAction(Request $request): JsonResponse { + $requestContent = (string)$request->getContent(); + + if (!$requestContent) { + return $this->createAjaxErrorResponse(); + } + $requestBodyContent = $this->getFactory() ->getUtilEncodingService() - ->decodeJson((string)$request->getContent(), true); + ->decodeJson($requestContent, true); + + if (!is_array($requestBodyContent)) { + return $this->createAjaxErrorResponse(); + } $isRequestValid = $this->getFactory() ->createImageSearchAiValidator() ->validate($requestBodyContent); if (!$isRequestValid) { - return $this->jsonResponse(['success' => false], Response::HTTP_BAD_REQUEST); + return $this->createAjaxErrorResponse(); } $searchString = $this->getFactory()->createImageToSearchTermsTransformer()->transform( @@ -62,4 +72,12 @@ public function findTermsAction(Request $request): JsonResponse : '', ]); } + + /** + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + protected function createAjaxErrorResponse(): JsonResponse + { + return $this->jsonResponse(['success' => false], Response::HTTP_BAD_REQUEST); + } } diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php index 58ac291..f421d0a 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceBridge.php @@ -28,9 +28,9 @@ public function __construct($utilEncodingService) * @param int|null $depth * @param int|null $options * - * @return array|null + * @return object|array|null */ - public function decodeJson(string $jsonValue, bool $assoc = false, ?int $depth = null, ?int $options = null): ?array + public function decodeJson(string $jsonValue, bool $assoc = false, ?int $depth = null, ?int $options = null): array|object|null { return $this->utilEncodingService->decodeJson($jsonValue, $assoc, $depth, $options); } diff --git a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php index faff07e..84f0f7b 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Dependency/Service/ImageSearchAiToUtilEncodingServiceInterface.php @@ -15,7 +15,7 @@ interface ImageSearchAiToUtilEncodingServiceInterface * @param int|null $depth * @param int|null $options * - * @return array|null + * @return object|array|null */ - public function decodeJson(string $jsonValue, bool $assoc = false, ?int $depth = null, ?int $options = null): ?array; + public function decodeJson(string $jsonValue, bool $assoc = false, ?int $depth = null, ?int $options = null): array|object|null; } From f167e61474139033567dc4b6e6d93b9c295b4725 Mon Sep 17 00:00:00 2001 From: Dmytro Asieiev Date: Wed, 11 Sep 2024 16:18:37 +0300 Subject: [PATCH 18/26] Fixed CI issues. --- composer.json | 3 ++- phpstan.neon | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 2bd845b..3be9291 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,8 @@ "config": { "sort-packages": true, "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true + "dealerdirect/phpcodesniffer-composer-installer": true, + "php-http/discovery": false } } } diff --git a/phpstan.neon b/phpstan.neon index 7f92b02..a00fd9f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,6 +4,7 @@ parameters: - src/ ignoreErrors: - '#Call to method .+ on an unknown class .+Transfer#' - - '#Parameter .+ of method .+ has invalid typehint type .+Transfer#' - - '#Return typehint of method .+ has invalid type .+Transfer#' - '#Instantiated class .+Transfer not found#' + - '#Method .+ has invalid return type .+Transfer#' + - '#Parameter .+ has invalid type .+Transfer#' + - '#Call .+ on an unknown class .+AutoCompletion.#' From 2d1c323312787823a0e970a94151cd71c4e38e1d Mon Sep 17 00:00:00 2001 From: Dmytro Asieiev Date: Wed, 11 Sep 2024 16:30:01 +0300 Subject: [PATCH 19/26] Fixed CI issues. --- .github/workflows/ci.yml | 3 --- composer.json | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99d049b..89cf091 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,9 +34,6 @@ jobs: - name: Run CodeStyle checks run: composer cs-check - - name: PHPStan setup - run: composer stan-setup - - name: Run PHPStan run: composer stan diff --git a/composer.json b/composer.json index 3be9291..3f36568 100644 --- a/composer.json +++ b/composer.json @@ -37,8 +37,7 @@ "scripts": { "cs-check": "phpcs -p -s --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", "cs-fix": "phpcbf -p --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", - "stan": "phpstan analyse -c phpstan.neon -l 8 src/", - "stan-setup": "cp composer.json composer.backup && COMPOSER_MEMORY_LIMIT=-1 composer require --dev phpstan/phpstan:^0.12 && mv composer.backup composer.json" + "stan": "phpstan analyse -c phpstan.neon -l 8 src/" }, "extra": { "branch-alias": { From bad7dee88928b70c5f826e677b8a637ab0eda7a7 Mon Sep 17 00:00:00 2001 From: Aleksey Belan Date: Wed, 11 Sep 2024 20:21:06 +0300 Subject: [PATCH 20/26] fix image --- .../components/molecules/image-uploader/image-uploader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.ts b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.ts index f26fc25..08d7eb2 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.ts +++ b/src/SprykerEco/Yves/ImageSearchAi/Theme/default/components/molecules/image-uploader/image-uploader.ts @@ -77,7 +77,7 @@ export default class ImageUploader extends Component { body: JSON.stringify({ image, _token: this._token }), })).json(); - if (data.success) { + if (!data.success) { this.classList.add(this.states.error); return; From b87c440494a45b964352195eafa5d87b14d589bc Mon Sep 17 00:00:00 2001 From: Dmytro Asieiev Date: Wed, 11 Sep 2024 20:58:29 +0300 Subject: [PATCH 21/26] DPIB-496: Adjusted composer files [skip ci] --- composer.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 3f36568..532b10a 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": ">=8.1", - "spryker-eco/open-ai": "dev-feature/demo/dev-ai-integrations", + "spryker-eco/open-ai": "^1.0.0", "spryker/catalog": "^5.0.0", "spryker/kernel": "^3.73.0", "spryker/symfony": "^3.0.0", @@ -28,12 +28,6 @@ }, "minimum-stability": "dev", "prefer-stable": true, - "repositories": [ - { - "type": "vcs", - "url": "git@github.com:spryker-eco/open-ai.git" - } - ], "scripts": { "cs-check": "phpcs -p -s --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", "cs-fix": "phpcbf -p --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", From 9dcc9c5e0e6315308947b7284b54045435f356be Mon Sep 17 00:00:00 2001 From: Dmytro Asieiev Date: Wed, 11 Sep 2024 21:10:03 +0300 Subject: [PATCH 22/26] DPIB-496: Adjusted composer files. --- composer.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 532b10a..3f36568 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": ">=8.1", - "spryker-eco/open-ai": "^1.0.0", + "spryker-eco/open-ai": "dev-feature/demo/dev-ai-integrations", "spryker/catalog": "^5.0.0", "spryker/kernel": "^3.73.0", "spryker/symfony": "^3.0.0", @@ -28,6 +28,12 @@ }, "minimum-stability": "dev", "prefer-stable": true, + "repositories": [ + { + "type": "vcs", + "url": "git@github.com:spryker-eco/open-ai.git" + } + ], "scripts": { "cs-check": "phpcs -p -s --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", "cs-fix": "phpcbf -p --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", From bb2a8337b3961132edb7e1fedf65baefdc145dfd Mon Sep 17 00:00:00 2001 From: Mantas Muliarcikas Date: Wed, 11 Sep 2024 21:58:08 +0300 Subject: [PATCH 23/26] added open ai success check --- .../ImageSearchAi/Transfer/image_search_ai.transfer.xml | 1 + .../ImageSearchAi/Controller/ImageSearchController.php | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml index 77d1523..df1cf0b 100644 --- a/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml +++ b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml @@ -8,6 +8,7 @@ + diff --git a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php index 52490fa..8b97381 100644 --- a/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php +++ b/src/SprykerEco/Yves/ImageSearchAi/Controller/ImageSearchController.php @@ -52,18 +52,18 @@ public function findTermsAction(Request $request): JsonResponse return $this->createAjaxErrorResponse(); } - $searchString = $this->getFactory()->createImageToSearchTermsTransformer()->transform( + $openAiChatResponseTransfer = $this->getFactory()->createImageToSearchTermsTransformer()->transform( $requestBodyContent[static::REQUEST_BODY_CONTENT_KEY_IMAGE], - )->getMessage(); + ); - if (!$searchString) { + if (!$openAiChatResponseTransfer->getIsSuccessful() || !$openAiChatResponseTransfer->getMessage()) { return $this->jsonResponse(); } $searchResults = $this ->getFactory() ->getCatalogClient() - ->catalogSuggestSearch($searchString, []); + ->catalogSuggestSearch($openAiChatResponseTransfer->getMessage()); return new JsonResponse([ 'success' => true, From 7f6c6f13544387d81fe552fcfb1c8d2770796663 Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Thu, 12 Sep 2024 12:23:16 +0300 Subject: [PATCH 24/26] Updated composer.json. --- composer.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 3f36568..532b10a 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": ">=8.1", - "spryker-eco/open-ai": "dev-feature/demo/dev-ai-integrations", + "spryker-eco/open-ai": "^1.0.0", "spryker/catalog": "^5.0.0", "spryker/kernel": "^3.73.0", "spryker/symfony": "^3.0.0", @@ -28,12 +28,6 @@ }, "minimum-stability": "dev", "prefer-stable": true, - "repositories": [ - { - "type": "vcs", - "url": "git@github.com:spryker-eco/open-ai.git" - } - ], "scripts": { "cs-check": "phpcs -p -s --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", "cs-fix": "phpcbf -p --standard=vendor/spryker/code-sniffer/SprykerStrict/ruleset.xml src/", From 6acd785deba9a2e46d347071ec5094e8ea04bedd Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Thu, 12 Sep 2024 15:47:39 +0300 Subject: [PATCH 25/26] Updated composer.json. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 532b10a..61700d1 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": ">=8.1", - "spryker-eco/open-ai": "^1.0.0", + "spryker-eco/open-ai": "^0.1.0", "spryker/catalog": "^5.0.0", "spryker/kernel": "^3.73.0", "spryker/symfony": "^3.0.0", From 3307aa30f95868f2f24e2d8e790d37957fab73b7 Mon Sep 17 00:00:00 2001 From: bohdanyevtukhov Date: Thu, 12 Sep 2024 16:02:18 +0300 Subject: [PATCH 26/26] Fixed CI validation. --- .../Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml index df1cf0b..b2bee0c 100644 --- a/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml +++ b/src/SprykerEco/Shared/ImageSearchAi/Transfer/image_search_ai.transfer.xml @@ -2,7 +2,7 @@ - +