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..7aa6a68 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,31 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* eol=lf +* text=auto + +# 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 +.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 linguist-generated=true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..89cf091 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +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: 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09c33fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# IDEs +/.idea +/.project +/nbproject +/.buildpath +/.settings +*.sublime-* +*.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/ +/tests/_output/* +!/tests/_output/.gitkeep +vendor +composer.lock 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/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5f754bf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# ProductManagementAi Changelog + +[Release Changelog](https://github.com/spryker-eco/product-management-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 010e1e1..4c01923 100644 --- a/README.md +++ b/README.md @@ -1 +1,9 @@ -# product-management-ai \ No newline at end of file +# ProductManagementAi Module + +ProductManagementAi module provides the functionality to manage products with AI. + +## Installation + +``` +composer require spryker-eco/prduct-management-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/assets/Zed/js/ai-category-suggestion.js b/assets/Zed/js/ai-category-suggestion.js new file mode 100644 index 0000000..f5863b8 --- /dev/null +++ b/assets/Zed/js/ai-category-suggestion.js @@ -0,0 +1,67 @@ +import { AiProductManagement } from './ai-product-management'; + +export class AiCategorySuggestion extends AiProductManagement { + names = ['description', 'name']; + triggerSelector = '.js-ai-category-trigger'; + + preparePayload(trigger) { + const fieldElement = trigger.getAttribute('data-product-info-field'); + const dataFields = document.querySelectorAll(fieldElement); + + this.data = {}; + + for (const { name, value } of dataFields) { + const informationalPart = this.names.find((part) => name.includes(`[${part}]`)); + + if (!informationalPart || !value) { + continue; + } + + this.data[`product_${informationalPart}`] ??= value; + + if (Object.keys(this.data).length === this.names.length) { + return; + } + } + } + + async processAiAction() { + if (Object.keys(this.data).length !== this.names.length) { + this.modal.classList.add(this.states.empty); + this.modal.classList.remove(this.states.loading); + + return; + } + + const select = this.modal.querySelector('.js-ai-category-select'); + + select.replaceChildren(); + + try { + const { categories } = await (await fetch(this.url, { + method: 'POST', + body: new URLSearchParams(this.data), + })).json(); + const fragment = document.createDocumentFragment(); + + for (const [text, id] of Object.entries(categories)) { + fragment.append(new Option(text, id, true, true)); + } + + select.append(fragment); + } finally { + this.modal.classList.remove(this.states.loading); + select.dispatchEvent(new Event('change')); + } + } + + onApply() { + const { selectedOptions } = this.modal.querySelector('.js-ai-category-select'); + + for (const option of this.fieldElement.options) { + option.selected = [...selectedOptions].some(_option => _option.value === option.value); + } + + this.fieldElement.dispatchEvent(new Event('change')); + }; +} diff --git a/assets/Zed/js/ai-image-alt-text.js b/assets/Zed/js/ai-image-alt-text.js new file mode 100644 index 0000000..1e12d03 --- /dev/null +++ b/assets/Zed/js/ai-image-alt-text.js @@ -0,0 +1,52 @@ +import { AiProductManagement } from './ai-product-management'; + +export class AiImageAltText extends AiProductManagement { + triggerSelector = '.js-ai-alt-image-trigger'; + + preparePayload(trigger) { + const inputLocale = this.fieldElement.name.split('[')[1].split(']')[0].replace('image_set_', ''); + const patterns = JSON.parse(trigger.getAttribute('data-product-field-pattern')); + + this.data = { + locale: inputLocale === 'default' ? 'en_US' : inputLocale, + }; + + for (const pattern of patterns) { + const value = document.querySelector(`[name="${pattern.replace('%locale%', this.data.locale)}"]`)?.value; + + if (value) { + this.data.imageUrl = value; + + return; + } + } + } + + async processAiAction() { + if (!this.data.imageUrl) { + this.modal.classList.add(this.states.empty); + this.modal.classList.remove(this.states.loading); + + return; + } + + const input = this.modal.querySelector('.js-ai-alt-text-input'); + + input.value = ''; + + try { + const { altText } = await (await fetch(this.url, { + method: 'POST', + body: new URLSearchParams(this.data), + })).json(); + + input.value = decodeURI(altText || ''); + } finally { + this.modal.classList.remove(this.states.loading); + } + } + + onApply() { + this.fieldElement.value = this.modal.querySelector('.js-ai-alt-text-input').value; + }; +} diff --git a/assets/Zed/js/ai-product-management.js b/assets/Zed/js/ai-product-management.js new file mode 100644 index 0000000..a7a411d --- /dev/null +++ b/assets/Zed/js/ai-product-management.js @@ -0,0 +1,65 @@ +export class AiProductManagement { + constructor() { + this.onApply = this.onApply.bind(this); + this.onAgain = this.onAgain.bind(this); + } + + states = { + loading: 'is-loading', + empty: 'is-empty', + } + + data = null; + modal = null; + fieldElement = null; + triggerSelector = null; + url = null; + + init() { + document.querySelectorAll(this.triggerSelector).forEach((trigger) => { + trigger.addEventListener('click', this.onTriggerClick.bind(this)); + }) + } + + refreshElements() { + this.data = null; + this.modal.classList.remove(this.states.loading, this.states.empty); + this.modal.querySelector('.js-ai-product-management-apply').removeEventListener('click', this.onApply); + this.modal.querySelector('.js-ai-product-management-again').removeEventListener('click', this.onAgain); + + this.modal.querySelector('.js-ai-product-management-apply').addEventListener('click', this.onApply); + this.modal.querySelector('.js-ai-product-management-again').addEventListener('click', this.onAgain); + } + + onTriggerClick(event) { + const trigger = event.currentTarget; + + this.modal = document.getElementById(trigger.getAttribute('popovertarget')); + this.fieldElement = trigger.parentElement.querySelector(`${trigger.getAttribute('data-field-selector')}`); + this.url = trigger.dataset.url; + + this.refreshElements(); + + this.modal.classList.add(this.states.loading); + + this.preparePayload(trigger); + this.processAiAction(); + } + + onAgain() { + this.modal.classList.add(this.states.loading); + this.processAiAction(); + } + + preparePayload() { + throw new Error('Method `preparePayload` at AiProductManagement class is not implemented.'); + } + + processAiAction() { + throw new Error('Method `processAiAction` at AiProductManagement class is not implemented.'); + } + + onApply() { + throw new Error('Method `onApply` at AiProductManagement class is not implemented.'); + } +} diff --git a/assets/Zed/js/ai-translation.js b/assets/Zed/js/ai-translation.js new file mode 100644 index 0000000..10fe517 --- /dev/null +++ b/assets/Zed/js/ai-translation.js @@ -0,0 +1,59 @@ +import { AiProductManagement } from './ai-product-management'; + +export class AiTranslation extends AiProductManagement { + triggerSelector = '.js-ai-translation-trigger'; + translationData = {}; + + preparePayload(trigger) { + this.modal.querySelectorAll('.js-ai-translation-control').forEach((field) => field.classList.remove('is-visible')); + + this.translationData = { + fromLocale: trigger.getAttribute('data-from-locale'), + fieldName: trigger.getAttribute('data-field-name'), + } + this.fieldElement = document.querySelector(`[name$="${trigger.getAttribute('data-to-locale')}][${this.translationData.fieldName}]"]`); + + this.data = { + locale: trigger.getAttribute('data-to-locale'), + text: document.querySelector(`[name$="${trigger.getAttribute('data-field-locale')}][${this.translationData.fieldName}]"]`).value, + } + } + async processAiAction() { + if (!this.data.text) { + this.modal.classList.add(this.states.empty); + this.modal.classList.remove(this.states.loading); + + return; + } + + const data = new FormData(); + + data.append('text', this.data.text); + data.append('locale', this.data.locale); + + if (this.data.cache) { + data.append('invalidate_cache', 1); + } + + const field = this.modal.querySelector(`[name="ai-translation-preview[${this.translationData.fieldName}]"]`); + + this.modal.querySelector('.js-translate-to').innerHTML = this.data.locale; + field.value = ''; + field.classList.add('is-visible'); + + try { + const { translation } = await (await fetch(this.url, { + method: 'POST', + body: data, + })).json(); + + field.value = translation; + this.data.cache = true; + } finally { + this.modal.classList.remove(this.states.loading); + } + } + onApply() { + this.fieldElement.value = this.modal.querySelector(`[name="ai-translation-preview[${this.translationData.fieldName}]"]`).value; + } +} diff --git a/assets/Zed/js/spryker-ai-category-suggestion-product-management-ai.entry.js b/assets/Zed/js/spryker-ai-category-suggestion-product-management-ai.entry.js new file mode 100644 index 0000000..046607e --- /dev/null +++ b/assets/Zed/js/spryker-ai-category-suggestion-product-management-ai.entry.js @@ -0,0 +1,10 @@ +import('../scss/main.scss'); +import { AiCategorySuggestion } from './ai-category-suggestion'; +import { AiImageAltText } from './ai-image-alt-text'; +import { AiTranslation } from './ai-translation'; + +document.addEventListener('DOMContentLoaded', () => { + new AiCategorySuggestion().init(); + new AiImageAltText().init(); + new AiTranslation().init(); +}); diff --git a/assets/Zed/scss/main.scss b/assets/Zed/scss/main.scss new file mode 100644 index 0000000..eebd19e --- /dev/null +++ b/assets/Zed/scss/main.scss @@ -0,0 +1,76 @@ +.ai-product-management-modal { + &.is-loading & { + &__loading { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + } + } + + &.is-empty & { + &__empty { + display: block; + } + } + + &.is-empty &, + &.is-loading & { + &__actions, + &__content { + display: none; + } + } + + &__loading, + &__empty { + display: none; + } +} + +.form-wrapper-clickable-affix { + &:has(.input-group-btn) { + .form-wrapper-clickable-affix__trigger { + right: 40px; + z-index: 10; + } + } + + &:has(.form-wrapper-clickable-affix__trigger:hover) .ai-translator { + opacity: 1; + visibility: visible; + } + + .ai-translator { + transition: all 0.3s ease-in-out; + opacity: 0; + visibility: hidden; + z-index: 5; + position: absolute; + top: 100%; + right: 0; + border-radius: 4px; + background: #fff; + padding: 0; + list-style: none; + width: max-content; + + li:not(:has(.btn-naked)) { + padding: 0 10px; + } + + .btn-naked { + width: 100%; + text-align: left; + padding: 5px 10px; + + &:hover { + background: #f8f8f8; + } + } + } +} + +.ai-translation-control:not(.is-visible) { + display: none; +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..86f11b6 --- /dev/null +++ b/composer.json @@ -0,0 +1,52 @@ +{ + "name": "spryker-eco/product-management-ai", + "type": "library", + "description": "ProductManagementAi module", + "license": "MIT", + "require": { + "php": ">=8.1", + "spryker-eco/open-ai": "^0.1.0", + "spryker/category": "^5.0.0", + "spryker/gui": "^3.45.0", + "spryker/kernel": "^3.33.0", + "spryker/locale": "^4.0.0", + "spryker/product": "^6.0.0", + "spryker/product-category": "^4.17.0", + "spryker/product-extension": "^1.1.0", + "spryker/product-management-extension": "^1.2.0", + "spryker/symfony": "^3.0.0", + "spryker/transfer": "^3.27.0", + "spryker/util-encoding": "^2.0.0" + }, + "require-dev": { + "phpstan/phpstan": "*", + "spryker/code-sniffer": "*" + }, + "suggest": { + "spryker/propel-orm": "Used for Propel schema generation." + }, + "autoload": { + "psr-4": { + "SprykerEco\\": "src/SprykerEco/" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "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/" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "php-http/discovery": false + } + } +} diff --git a/data/translation/Zed/de_DE.csv b/data/translation/Zed/de_DE.csv new file mode 100644 index 0000000..3051978 --- /dev/null +++ b/data/translation/Zed/de_DE.csv @@ -0,0 +1,8 @@ +"AI assistant","KI-Assistent" +"AI is processing your request","KI verarbeitet Ihre Anfrage" +"Please fill in the product name and description for at least one locale","Bitte füllen Sie den Produktnamen und die Beschreibung für mindestens eine Sprache aus" +"Try again","Versuchen Sie es erneut" +"Please fill in the product image url","Bitte fügen Sie die Produktbild-URL ein" +"Please fill in text to translate","Bitte fügen Sie den zu übersetzenden Text ein" +Apply,Anwenden +"Translate to:","Übersetzen in:" diff --git a/data/translation/Zed/en_US.csv b/data/translation/Zed/en_US.csv new file mode 100644 index 0000000..472bb2e --- /dev/null +++ b/data/translation/Zed/en_US.csv @@ -0,0 +1,8 @@ +"AI assistant","AI assistant" +"AI is processing your request","AI is processing your request" +"Please fill in the product name and description for at least one locale","Please fill in the product name and description for at least one locale" +"Please fill in the product image url","Please fill in the product image url" +"Try again","Try again" +"Please fill in text to translate","Please fill in text to translate" +Apply,Apply +"Translate to:","Translate to:" diff --git a/dependency.json b/dependency.json new file mode 100644 index 0000000..53fe525 --- /dev/null +++ b/dependency.json @@ -0,0 +1,7 @@ +{ + "include": { + "spryker/product": "Required to use Product plugins.", + "spryker/gui": "If you want to use GUI layout templates.", + "spryker/transfer": "Provides transfer objects definition with strict types." + } +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..95ff8c5 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,11 @@ +parameters: + level: 8 + paths: + - src/ + ignoreErrors: + - '#Call to method .+ on an unknown class .+Transfer#' + - '#Instantiated class .+Transfer not found#' + - '#Method .+ has invalid return type .+Transfer#' + - '#Parameter .+ has invalid type .+Transfer#' + - '#Call .+ on an unknown class .+AutoCompletion.#' + - '#Access to constant .+ on an unknown class .+Transfer.#' 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/ProductManagementAi/Transfer/product_management_ai.transfer.xml b/src/SprykerEco/Shared/ProductManagementAi/Transfer/product_management_ai.transfer.xml new file mode 100644 index 0000000..a5ad969 --- /dev/null +++ b/src/SprykerEco/Shared/ProductManagementAi/Transfer/product_management_ai.transfer.xml @@ -0,0 +1,50 @@ +<?xml version="1.0"?> +<transfers xmlns="spryker:transfer-01" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="spryker:transfer-01 http://static.spryker.com/transfer-01.xsd"> + + <transfer name="ProductImage"> + <property name="altText" type="string" strict="true"/> + </transfer> + + <transfer name="ProductImageStorage"> + <property name="altText" type="string" strict="true"/> + </transfer> + + <transfer name="OpenAiChatRequest" strict="true"> + <property name="message" type="string"/> + </transfer> + + <transfer name="OpenAiChatResponse" strict="true"> + <property name="isSuccessful" type="bool"/> + <property name="message" type="string"/> + </transfer> + + <transfer name="AiTranslatorResponse" strict="true"> + <property name="originalText" type="string"/> + <property name="sourceLocale" type="string"/> + <property name="targetLocale" type="string"/> + <property name="translation" type="string"/> + </transfer> + + <transfer name="AiTranslatorRequest" strict="true"> + <property name="text" type="string"/> + <property name="sourceLocale" type="string"/> + <property name="targetLocale" type="string"/> + <property name="invalidateCache" type="bool"/> + </transfer> + + <transfer name="Category"> + <property name="idCategory" type="int"/> + <property name="name" type="string"/> + </transfer> + + <transfer name="CategoryCollection"> + <property name="categories" type="Category[]" singular="category"/> + </transfer> + + <transfer name="ProductAbstract"> + </transfer> + + <transfer name="Locale"> + </transfer> + +</transfers> diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/Builder/PromptBuilder.php b/src/SprykerEco/Zed/ProductManagementAi/Business/Builder/PromptBuilder.php new file mode 100644 index 0000000..7a23900 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/Builder/PromptBuilder.php @@ -0,0 +1,48 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business\Builder; + +use SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig; + +class PromptBuilder implements PromptBuilderInterface +{ + /** + * @var \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig + */ + protected ProductManagementAiConfig $productManagementAiConfig; + + /** + * @param \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig $productManagementAiConfig + */ + public function __construct(ProductManagementAiConfig $productManagementAiConfig) + { + $this->productManagementAiConfig = $productManagementAiConfig; + } + + /** + * @param string $imageUrl + * @param string $targetLocale + * + * @return array<array<string, mixed>> + */ + public function buildImageAltTextPrompt(string $imageUrl, string $targetLocale): array + { + return [ + [ + 'type' => 'text', + 'text' => $this->productManagementAiConfig->getImageAltTextPrompt($targetLocale), + ], + [ + 'type' => 'image_url', + 'image_url' => [ + 'url' => $imageUrl, + ], + ], + ]; + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/Builder/PromptBuilderInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Business/Builder/PromptBuilderInterface.php new file mode 100644 index 0000000..f4af536 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/Builder/PromptBuilderInterface.php @@ -0,0 +1,19 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business\Builder; + +interface PromptBuilderInterface +{ + /** + * @param string $imageUrl + * @param string $targetLocale + * + * @return array<array<string, mixed>> + */ + public function buildImageAltTextPrompt(string $imageUrl, string $targetLocale): array; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/Generator/ImageAltTextGenerator.php b/src/SprykerEco/Zed/ProductManagementAi/Business/Generator/ImageAltTextGenerator.php new file mode 100644 index 0000000..ed84044 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/Generator/ImageAltTextGenerator.php @@ -0,0 +1,62 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business\Generator; + +use Generated\Shared\Transfer\OpenAiChatRequestTransfer; +use Generated\Shared\Transfer\OpenAiChatResponseTransfer; +use SprykerEco\Zed\ProductManagementAi\Business\Builder\PromptBuilderInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientInterface; +use SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig; + +class ImageAltTextGenerator implements ImageAltTextGeneratorInterface +{ + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientInterface + */ + protected ProductManagementAiToOpenAiClientInterface $openAiClient; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\Business\Builder\PromptBuilderInterface + */ + protected PromptBuilderInterface $promptBuilder; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig + */ + protected ProductManagementAiConfig $productManagementAiConfig; + + /** + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientInterface $openAiClient + * @param \SprykerEco\Zed\ProductManagementAi\Business\Builder\PromptBuilderInterface $promptBuilder + * @param \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig $productManagementAiConfig + */ + public function __construct( + ProductManagementAiToOpenAiClientInterface $openAiClient, + PromptBuilderInterface $promptBuilder, + ProductManagementAiConfig $productManagementAiConfig + ) { + $this->openAiClient = $openAiClient; + $this->promptBuilder = $promptBuilder; + $this->productManagementAiConfig = $productManagementAiConfig; + } + + /** + * @param string $imageUrl + * @param string $targetLocale + * + * @return \Generated\Shared\Transfer\OpenAiChatResponseTransfer + */ + public function generateImageAltText(string $imageUrl, string $targetLocale): OpenAiChatResponseTransfer + { + $openAiChatRequestTransfer = (new OpenAiChatRequestTransfer())->setPromptData( + $this->promptBuilder->buildImageAltTextPrompt($imageUrl, $targetLocale), + )->setModel($this->productManagementAiConfig->getOpenAiGpt4oMiniModel()); + + return $this->openAiClient->chat($openAiChatRequestTransfer); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/Generator/ImageAltTextGeneratorInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Business/Generator/ImageAltTextGeneratorInterface.php new file mode 100644 index 0000000..c0cb935 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/Generator/ImageAltTextGeneratorInterface.php @@ -0,0 +1,21 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business\Generator; + +use Generated\Shared\Transfer\OpenAiChatResponseTransfer; + +interface ImageAltTextGeneratorInterface +{ + /** + * @param string $imageUrl + * @param string $targetLocale + * + * @return \Generated\Shared\Transfer\OpenAiChatResponseTransfer + */ + public function generateImageAltText(string $imageUrl, string $targetLocale): OpenAiChatResponseTransfer; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/ProductManagementAiBusinessFactory.php b/src/SprykerEco/Zed/ProductManagementAi/Business/ProductManagementAiBusinessFactory.php new file mode 100644 index 0000000..ade2303 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/ProductManagementAiBusinessFactory.php @@ -0,0 +1,118 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business; + +use Spryker\Zed\Kernel\Business\AbstractBusinessFactory; +use SprykerEco\Zed\ProductManagementAi\Business\Builder\PromptBuilder; +use SprykerEco\Zed\ProductManagementAi\Business\Builder\PromptBuilderInterface; +use SprykerEco\Zed\ProductManagementAi\Business\Generator\ImageAltTextGenerator; +use SprykerEco\Zed\ProductManagementAi\Business\Generator\ImageAltTextGeneratorInterface; +use SprykerEco\Zed\ProductManagementAi\Business\Proposer\CategoryProposer; +use SprykerEco\Zed\ProductManagementAi\Business\Proposer\CategoryProposerInterface; +use SprykerEco\Zed\ProductManagementAi\Business\Reader\CategoryReader; +use SprykerEco\Zed\ProductManagementAi\Business\Reader\CategoryReaderInterface; +use SprykerEco\Zed\ProductManagementAi\Business\Translator\Translator; +use SprykerEco\Zed\ProductManagementAi\Business\Translator\TranslatorInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToCategoryFacadeInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Service\ProductManagementAiToUtilEncodingServiceInterface; +use SprykerEco\Zed\ProductManagementAi\ProductManagementAiDependencyProvider; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig getConfig() + */ +class ProductManagementAiBusinessFactory extends AbstractBusinessFactory +{ + /** + * @return \SprykerEco\Zed\ProductManagementAi\Business\Reader\CategoryReaderInterface + */ + public function createCategoryReader(): CategoryReaderInterface + { + return new CategoryReader( + $this->getCategoryFacade(), + $this->getLocaleFacade(), + ); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Business\Proposer\CategoryProposerInterface + */ + public function createCategoryProposer(): CategoryProposerInterface + { + return new CategoryProposer( + $this->getOpenAiClient(), + $this->getUtilEncodingService(), + $this->createCategoryReader(), + $this->getConfig(), + ); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Business\Generator\ImageAltTextGeneratorInterface + */ + public function createImageAltTextGenerator(): ImageAltTextGeneratorInterface + { + return new ImageAltTextGenerator( + $this->getOpenAiClient(), + $this->createPromptBuilder(), + $this->getConfig(), + ); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Business\Builder\PromptBuilderInterface + */ + public function createPromptBuilder(): PromptBuilderInterface + { + return new PromptBuilder($this->getConfig()); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Business\Translator\TranslatorInterface + */ + public function createTranslator(): TranslatorInterface + { + return new Translator( + $this->getOpenAiClient(), + $this->getConfig(), + ); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToCategoryFacadeInterface + */ + public function getCategoryFacade(): ProductManagementAiToCategoryFacadeInterface + { + return $this->getProvidedDependency(ProductManagementAiDependencyProvider::FACADE_CATEGORY); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface + */ + public function getLocaleFacade(): ProductManagementAiToLocaleFacadeInterface + { + return $this->getProvidedDependency(ProductManagementAiDependencyProvider::FACADE_LOCALE); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Dependency\Service\ProductManagementAiToUtilEncodingServiceInterface + */ + public function getUtilEncodingService(): ProductManagementAiToUtilEncodingServiceInterface + { + return $this->getProvidedDependency(ProductManagementAiDependencyProvider::SERVICE_UTIL_ENCODING); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientInterface + */ + public function getOpenAiClient(): ProductManagementAiToOpenAiClientInterface + { + return $this->getProvidedDependency(ProductManagementAiDependencyProvider::CLIENT_OPEN_AI); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/ProductManagementAiFacade.php b/src/SprykerEco/Zed/ProductManagementAi/Business/ProductManagementAiFacade.php new file mode 100644 index 0000000..882226e --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/ProductManagementAiFacade.php @@ -0,0 +1,69 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business; + +use Generated\Shared\Transfer\AiTranslatorRequestTransfer; +use Generated\Shared\Transfer\AiTranslatorResponseTransfer; +use Generated\Shared\Transfer\OpenAiChatResponseTransfer; +use Spryker\Zed\Kernel\Business\AbstractFacade; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\Business\ProductManagementAiBusinessFactory getFactory() + */ +class ProductManagementAiFacade extends AbstractFacade implements ProductManagementAiFacadeInterface +{ + /** + * {@inheritDoc} + * + * @api + * + * @param string $productName + * @param string $description + * + * @return array<string, int> + */ + public function proposeCategorySuggestions(string $productName, string $description): array + { + return $this->getFactory() + ->createCategoryProposer() + ->proposeCategorySuggestions($productName, $description); + } + + /** + * {@inheritDoc} + * + * @api + * + * @param string $imageUrl + * @param string $targetLocale + * + * @return \Generated\Shared\Transfer\OpenAiChatResponseTransfer + */ + public function generateImageAltText(string $imageUrl, string $targetLocale): OpenAiChatResponseTransfer + { + return $this->getFactory() + ->createImageAltTextGenerator() + ->generateImageAltText($imageUrl, $targetLocale); + } + + /** + * {@inheritDoc} + * + * @api + * + * @param \Generated\Shared\Transfer\AiTranslatorRequestTransfer $aiTranslatorRequestTransfer + * + * @return \Generated\Shared\Transfer\AiTranslatorResponseTransfer + */ + public function translate(AiTranslatorRequestTransfer $aiTranslatorRequestTransfer): AiTranslatorResponseTransfer + { + return $this->getFactory() + ->createTranslator() + ->translate($aiTranslatorRequestTransfer); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/ProductManagementAiFacadeInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Business/ProductManagementAiFacadeInterface.php new file mode 100644 index 0000000..5fe9fad --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/ProductManagementAiFacadeInterface.php @@ -0,0 +1,55 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business; + +use Generated\Shared\Transfer\AiTranslatorRequestTransfer; +use Generated\Shared\Transfer\AiTranslatorResponseTransfer; +use Generated\Shared\Transfer\OpenAiChatResponseTransfer; + +interface ProductManagementAiFacadeInterface +{ + /** + * Specification: + * - Proposes category suggestions based on product name, description and existing categories. + * + * @api + * + * @param string $productName + * @param string $description + * + * @return array<string, int> + */ + public function proposeCategorySuggestions(string $productName, string $description): array; + + /** + * Specification: + * - Generates alt text for an image using AI. + * + * @api + * + * @param string $imageUrl + * @param string $targetLocale + * + * @return \Generated\Shared\Transfer\OpenAiChatResponseTransfer + */ + public function generateImageAltText(string $imageUrl, string $targetLocale): OpenAiChatResponseTransfer; + + /** + * Specification: + * - Translates a text from source locale to target locale. + * - Text for translation, source and target locales are provided as properties of `AiTranslatorRequest`. + * - Returns `AiTranslatorResponse`. + * + * @api + * + * @param \Generated\Shared\Transfer\AiTranslatorRequestTransfer $aiTranslatorRequestTransfer + * + * @return \Generated\Shared\Transfer\AiTranslatorResponseTransfer + */ + public function translate(AiTranslatorRequestTransfer $aiTranslatorRequestTransfer): AiTranslatorResponseTransfer; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/Proposer/CategoryProposer.php b/src/SprykerEco/Zed/ProductManagementAi/Business/Proposer/CategoryProposer.php new file mode 100644 index 0000000..db8bff7 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/Proposer/CategoryProposer.php @@ -0,0 +1,101 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business\Proposer; + +use Generated\Shared\Transfer\OpenAiChatRequestTransfer; +use SprykerEco\Zed\ProductManagementAi\Business\Reader\CategoryReaderInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Service\ProductManagementAiToUtilEncodingServiceInterface; +use SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig; + +class CategoryProposer implements CategoryProposerInterface +{ + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientInterface + */ + protected ProductManagementAiToOpenAiClientInterface $openAiClient; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Service\ProductManagementAiToUtilEncodingServiceInterface + */ + protected ProductManagementAiToUtilEncodingServiceInterface $utilEncodingService; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\Business\Reader\CategoryReaderInterface + */ + protected CategoryReaderInterface $categoryReader; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig + */ + protected ProductManagementAiConfig $productManagementAiConfig; + + /** + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientInterface $openAiClient + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Service\ProductManagementAiToUtilEncodingServiceInterface $utilEncodingService + * @param \SprykerEco\Zed\ProductManagementAi\Business\Reader\CategoryReaderInterface $categoryReader + * @param \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig $productManagementAiConfig + */ + public function __construct( + ProductManagementAiToOpenAiClientInterface $openAiClient, + ProductManagementAiToUtilEncodingServiceInterface $utilEncodingService, + CategoryReaderInterface $categoryReader, + ProductManagementAiConfig $productManagementAiConfig + ) { + $this->openAiClient = $openAiClient; + $this->utilEncodingService = $utilEncodingService; + $this->categoryReader = $categoryReader; + $this->productManagementAiConfig = $productManagementAiConfig; + } + + /** + * @param string $productName + * @param string $description + * + * @return array<string, int> + */ + public function proposeCategorySuggestions(string $productName, string $description): array + { + $categories = $this->categoryReader->getCategories(); + if (!count($categories)) { + return []; + } + + $openAiChatRequestTransfer = (new OpenAiChatRequestTransfer()) + ->setMessage($this->generatePrompt($productName, $description, $categories)); + + $openAiChatResponseTransfer = $this->openAiClient->chat($openAiChatRequestTransfer); + + if (!$openAiChatResponseTransfer->getMessage() || !$openAiChatResponseTransfer->getIsSuccessful()) { + return []; + } + + $proposedCategories = $this->utilEncodingService->decodeJson($openAiChatResponseTransfer->getMessage(), true); + + return is_array($proposedCategories) ? $proposedCategories : []; + } + + /** + * @param string $productName + * @param string $description + * @param array<string, int> $categories + * + * @return string + */ + protected function generatePrompt(string $productName, string $description, array $categories): string + { + $categories = $this->utilEncodingService->encodeJson($categories); + + return sprintf( + $this->productManagementAiConfig->getProductCategorySuggestionPromptTemplate(), + $productName, + $description, + $categories, + ); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/Proposer/CategoryProposerInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Business/Proposer/CategoryProposerInterface.php new file mode 100644 index 0000000..c956226 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/Proposer/CategoryProposerInterface.php @@ -0,0 +1,19 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business\Proposer; + +interface CategoryProposerInterface +{ + /** + * @param string $productName + * @param string $description + * + * @return array<string, int> + */ + public function proposeCategorySuggestions(string $productName, string $description): array; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/Reader/CategoryReader.php b/src/SprykerEco/Zed/ProductManagementAi/Business/Reader/CategoryReader.php new file mode 100644 index 0000000..f074e9e --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/Reader/CategoryReader.php @@ -0,0 +1,52 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business\Reader; + +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToCategoryFacadeInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface; + +class CategoryReader implements CategoryReaderInterface +{ + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToCategoryFacadeInterface + */ + protected ProductManagementAiToCategoryFacadeInterface $categoryFacade; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface + */ + protected ProductManagementAiToLocaleFacadeInterface $localeFacade; + + /** + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToCategoryFacadeInterface $categoryFacade + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface $localeFacade + */ + public function __construct( + ProductManagementAiToCategoryFacadeInterface $categoryFacade, + ProductManagementAiToLocaleFacadeInterface $localeFacade + ) { + $this->categoryFacade = $categoryFacade; + $this->localeFacade = $localeFacade; + } + + /** + * @return array<string, int> + */ + public function getCategories(): array + { + $categoryCollectionTransfer = $this->categoryFacade + ->getAllCategoryCollection($this->localeFacade->getCurrentLocale()); + + $categories = []; + foreach ($categoryCollectionTransfer->getCategories() as $categoryTransfer) { + $categories[$categoryTransfer->getName()] = $categoryTransfer->getIdCategoryOrFail(); + } + + return $categories; + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/Reader/CategoryReaderInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Business/Reader/CategoryReaderInterface.php new file mode 100644 index 0000000..8897d62 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/Reader/CategoryReaderInterface.php @@ -0,0 +1,16 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business\Reader; + +interface CategoryReaderInterface +{ + /** + * @return array<string, int> + */ + public function getCategories(): array; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/Translator/Translator.php b/src/SprykerEco/Zed/ProductManagementAi/Business/Translator/Translator.php new file mode 100644 index 0000000..311e991 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/Translator/Translator.php @@ -0,0 +1,113 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business\Translator; + +use Generated\Shared\Transfer\AiTranslatorRequestTransfer; +use Generated\Shared\Transfer\AiTranslatorResponseTransfer; +use Generated\Shared\Transfer\OpenAiChatRequestTransfer; +use Generated\Shared\Transfer\OpenAiChatResponseTransfer; +use SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientInterface; +use SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig; + +class Translator implements TranslatorInterface +{ + /** + * @var string + */ + protected const INVALID_TRANSLATION_MESSAGE = 'Unable to translate provided text.'; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientInterface + */ + protected ProductManagementAiToOpenAiClientInterface $openAiClient; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig + */ + protected ProductManagementAiConfig $productManagementAiConfig; + + /** + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientInterface $openAiClient + * @param \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig $productManagementAiConfig + */ + public function __construct( + ProductManagementAiToOpenAiClientInterface $openAiClient, + ProductManagementAiConfig $productManagementAiConfig + ) { + $this->openAiClient = $openAiClient; + $this->productManagementAiConfig = $productManagementAiConfig; + } + + /** + * @param \Generated\Shared\Transfer\AiTranslatorRequestTransfer $aiTranslatorRequestTransfer + * + * @return \Generated\Shared\Transfer\AiTranslatorResponseTransfer + */ + public function translate(AiTranslatorRequestTransfer $aiTranslatorRequestTransfer): AiTranslatorResponseTransfer + { + $aiTranslatorRequestTransfer = $this->normalizeSourceLocale($aiTranslatorRequestTransfer); + $openAiChatRequestTransfer = (new OpenAiChatRequestTransfer()) + ->setMessage($this->buildTranslationRequestPrompt($aiTranslatorRequestTransfer)); + $openAiChatResponse = $this->openAiClient->chat($openAiChatRequestTransfer); + + return $this->createTranslatorResponse( + $aiTranslatorRequestTransfer, + $openAiChatResponse, + ); + } + + /** + * @param \Generated\Shared\Transfer\AiTranslatorRequestTransfer $aiTranslatorRequestTransfer + * + * @return \Generated\Shared\Transfer\AiTranslatorRequestTransfer + */ + protected function normalizeSourceLocale(AiTranslatorRequestTransfer $aiTranslatorRequestTransfer): AiTranslatorRequestTransfer + { + $sourceLocale = $aiTranslatorRequestTransfer->getSourceLocale() ?? $this->productManagementAiConfig->getDefaultTranslationSourceLocale(); + $aiTranslatorRequestTransfer->setSourceLocale($sourceLocale); + + return $aiTranslatorRequestTransfer; + } + + /** + * @param \Generated\Shared\Transfer\AiTranslatorRequestTransfer $aiTranslatorRequestTransfer + * + * @return string + */ + protected function buildTranslationRequestPrompt(AiTranslatorRequestTransfer $aiTranslatorRequestTransfer): string + { + return sprintf( + $this->productManagementAiConfig->getAiTranslationPromptTemplate(), + $aiTranslatorRequestTransfer->getTextOrFail(), + $aiTranslatorRequestTransfer->getSourceLocaleOrFail(), + $aiTranslatorRequestTransfer->getTargetLocaleOrFail(), + ); + } + + /** + * @param \Generated\Shared\Transfer\AiTranslatorRequestTransfer $aiTranslatorRequestTransfer + * @param \Generated\Shared\Transfer\OpenAiChatResponseTransfer $openAiChatResponseTransfer + * + * @return \Generated\Shared\Transfer\AiTranslatorResponseTransfer + */ + protected function createTranslatorResponse( + AiTranslatorRequestTransfer $aiTranslatorRequestTransfer, + OpenAiChatResponseTransfer $openAiChatResponseTransfer + ): AiTranslatorResponseTransfer { + $aiTranslatorResponseTransfer = (new AiTranslatorResponseTransfer()) + ->setOriginalText($aiTranslatorRequestTransfer->getTextOrFail()) + ->setSourceLocale($aiTranslatorRequestTransfer->getSourceLocaleOrFail()) + ->setTargetLocale($aiTranslatorRequestTransfer->getTargetLocale()); + + if (!$openAiChatResponseTransfer->getIsSuccessful() || !$openAiChatResponseTransfer->getMessage()) { + return $aiTranslatorResponseTransfer->setTranslation(static::INVALID_TRANSLATION_MESSAGE); + } + + return $aiTranslatorResponseTransfer->setTranslation($openAiChatResponseTransfer->getMessage()); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Business/Translator/TranslatorInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Business/Translator/TranslatorInterface.php new file mode 100644 index 0000000..ab4ec7f --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Business/Translator/TranslatorInterface.php @@ -0,0 +1,21 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Business\Translator; + +use Generated\Shared\Transfer\AiTranslatorRequestTransfer; +use Generated\Shared\Transfer\AiTranslatorResponseTransfer; + +interface TranslatorInterface +{ + /** + * @param \Generated\Shared\Transfer\AiTranslatorRequestTransfer $aiTranslatorRequestTransfer + * + * @return \Generated\Shared\Transfer\AiTranslatorResponseTransfer + */ + public function translate(AiTranslatorRequestTransfer $aiTranslatorRequestTransfer): AiTranslatorResponseTransfer; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Controller/CategorySuggestionController.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Controller/CategorySuggestionController.php new file mode 100644 index 0000000..9959b5e --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Controller/CategorySuggestionController.php @@ -0,0 +1,49 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Controller; + +use Spryker\Zed\Kernel\Communication\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\Business\ProductManagementAiFacadeInterface getFacade() + * @method \SprykerEco\Zed\ProductManagementAi\Communication\ProductManagementAiCommunicationFactory getFactory() + */ +class CategorySuggestionController extends AbstractController +{ + /** + * @var string + */ + public const PARAM_PRODUCT_NAME = 'product_name'; + + /** + * @var string + */ + public const PARAM_PRODUCT_DESCRIPTION = 'product_description'; + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function indexAction(Request $request): JsonResponse + { + $productName = $request->get(static::PARAM_PRODUCT_NAME); + $description = $request->get(static::PARAM_PRODUCT_DESCRIPTION); + + if (!$productName || !$description) { + return new JsonResponse(null, Response::HTTP_BAD_REQUEST); + } + + return new JsonResponse([ + 'categories' => $this->getFacade()->proposeCategorySuggestions($productName, $description), + ]); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Controller/ImageAltTextController.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Controller/ImageAltTextController.php new file mode 100644 index 0000000..7db4679 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Controller/ImageAltTextController.php @@ -0,0 +1,82 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Controller; + +use Spryker\Zed\Kernel\Communication\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\Communication\ProductManagementAiCommunicationFactory getFactory() + * @method \SprykerEco\Zed\ProductManagementAi\Business\ProductManagementAiFacadeInterface getFacade() + */ +class ImageAltTextController extends AbstractController +{ + /** + * @var string + */ + protected const PARAM_IMAGE_URL = 'imageUrl'; + + /** + * @var string + */ + protected const PARAM_LOCALE = 'locale'; + + /** + * @var string + */ + protected const PARAM_INVALIDATE_CACHE = 'invalidate_cache'; + + /** + * @var int + */ + protected const STATUS_CODE_BAD_REQUEST = 400; + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function indexAction(Request $request): Response + { + $imageUrl = $request->get(static::PARAM_IMAGE_URL); + $targetLocale = $request->get(static::PARAM_LOCALE); + + if (!$imageUrl || !$targetLocale) { + return $this->getErrorJsonResponse('ImageUrl and/or target locale are missing from request.'); + } + + $openAiChatResponseTransfer = $this->getFacade()->generateImageAltText($imageUrl, $targetLocale); + if (!$openAiChatResponseTransfer->getIsSuccessful()) { + return $this->getErrorJsonResponse($openAiChatResponseTransfer->getMessage()); + } + + return $this->jsonResponse([ + 'altText' => $this->getFacade() + ->generateImageAltText($imageUrl, $targetLocale) + ->getMessageOrFail(), + ]); + } + + /** + * @param string|null $errorMessage + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + protected function getErrorJsonResponse(?string $errorMessage): JsonResponse + { + return $this->jsonResponse( + [ + 'error' => 'Bad request', + 'message' => $errorMessage, + ], + static::STATUS_CODE_BAD_REQUEST, + ); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Controller/TranslateController.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Controller/TranslateController.php new file mode 100644 index 0000000..029eade --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Controller/TranslateController.php @@ -0,0 +1,73 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Controller; + +use Generated\Shared\Transfer\AiTranslatorRequestTransfer; +use Spryker\Zed\Kernel\Communication\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\Communication\ProductManagementAiCommunicationFactory getFactory() + * @method \SprykerEco\Zed\ProductManagementAi\Business\ProductManagementAiFacadeInterface getFacade() + */ +class TranslateController extends AbstractController +{ + /** + * @var string + */ + protected const PARAM_TEXT = 'text'; + + /** + * @var string + */ + protected const PARAM_LOCALE = 'locale'; + + /** + * @var string + */ + protected const PARAM_INVALIDATE_CACHE = 'invalidate_cache'; + + /** + * @var int + */ + protected const STATUS_CODE_BAD_REQUEST = 400; + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function indexAction(Request $request): JsonResponse + { + $text = $request->get(static::PARAM_TEXT); + $targetLocale = $request->get(static::PARAM_LOCALE); + + if (!$text || !$targetLocale) { + return $this->jsonResponse( + [ + 'error' => 'Bad request', + 'message' => 'Text and/or target locale are missing from request.', + ], + Response::HTTP_BAD_REQUEST, + ); + } + + $translatorRequestTransfer = (new AiTranslatorRequestTransfer()) + ->setTargetLocale($targetLocale) + ->setText($text) + ->setInvalidateCache((bool)$request->get(static::PARAM_INVALIDATE_CACHE, false)); + $translationResponseTransfer = $this->getFacade()->translate($translatorRequestTransfer); + + return $this->jsonResponse([ + 'locale' => $translationResponseTransfer->getTargetLocaleOrFail(), + 'translation' => $translationResponseTransfer->getTranslationOrFail(), + ]); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/CategoryIdsProductFormExpander.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/CategoryIdsProductFormExpander.php new file mode 100644 index 0000000..cb55fad --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/CategoryIdsProductFormExpander.php @@ -0,0 +1,87 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Expander; + +use Generated\Shared\Transfer\ProductAbstractTransfer; +use Spryker\Zed\Gui\Communication\Form\Type\Select2ComboBoxType; +use SprykerEco\Zed\ProductManagementAi\Communication\Form\DataProvider\ProductCategoryAbstractFormDataProviderInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +class CategoryIdsProductFormExpander implements CategoryIdsProductFormExpanderInterface +{ + /** + * @see \Spryker\Zed\ProductManagement\Communication\Form\ProductFormAdd::FIELD_ID_PRODUCT_ABSTRACT + * + * @var string + */ + protected const KEY_ID_PRODUCT_ABSTRACT = 'id_product_abstract'; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\Communication\Form\DataProvider\ProductCategoryAbstractFormDataProviderInterface $productCategoryAbstractFormDataProvider + */ + protected ProductCategoryAbstractFormDataProviderInterface $productCategoryAbstractFormDataProvider; + + /** + * @param \SprykerEco\Zed\ProductManagementAi\Communication\Form\DataProvider\ProductCategoryAbstractFormDataProviderInterface $productCategoryAbstractFormDataProvider + */ + public function __construct(ProductCategoryAbstractFormDataProviderInterface $productCategoryAbstractFormDataProvider) + { + $this->productCategoryAbstractFormDataProvider = $productCategoryAbstractFormDataProvider; + } + + /** + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * + * @param \Symfony\Component\Form\FormBuilderInterface $builder + * @param array<string, mixed> $options + * + * @return \Symfony\Component\Form\FormBuilderInterface + */ + public function expand(FormBuilderInterface $builder, array $options): FormBuilderInterface + { + $this->addCategoryIdsField($builder); + + return $builder; + } + + /** + * @param \Symfony\Component\Form\FormBuilderInterface $builder + * + * @return \Symfony\Component\Form\FormBuilderInterface + */ + protected function addCategoryIdsField(FormBuilderInterface $builder): FormBuilderInterface + { + $builder->add(ProductAbstractTransfer::CATEGORY_IDS, Select2ComboBoxType::class, [ + 'label' => 'categories', + 'placeholder' => 'select-category', + 'multiple' => true, + 'required' => false, + 'empty_data' => [], + 'choices' => $this->productCategoryAbstractFormDataProvider->getOptions(), + ]); + + $builder->addEventListener( + FormEvents::PRE_SET_DATA, + function (FormEvent $event): void { + $data = $event->getData(); + + if (!$data || !isset($data[self::KEY_ID_PRODUCT_ABSTRACT])) { + return; + } + + $data[ProductAbstractTransfer::CATEGORY_IDS] = $this->productCategoryAbstractFormDataProvider + ->getData($data[self::KEY_ID_PRODUCT_ABSTRACT]); + $event->setData($data); + }, + ); + + return $builder; + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/CategoryIdsProductFormExpanderInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/CategoryIdsProductFormExpanderInterface.php new file mode 100644 index 0000000..8faee15 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/CategoryIdsProductFormExpanderInterface.php @@ -0,0 +1,21 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Expander; + +use Symfony\Component\Form\FormBuilderInterface; + +interface CategoryIdsProductFormExpanderInterface +{ + /** + * @param \Symfony\Component\Form\FormBuilderInterface $builder + * @param array<string, mixed> $options + * + * @return \Symfony\Component\Form\FormBuilderInterface + */ + public function expand(FormBuilderInterface $builder, array $options): FormBuilderInterface; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/ImageAltTextProductFormExpander.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/ImageAltTextProductFormExpander.php new file mode 100644 index 0000000..9e25a4d --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/ImageAltTextProductFormExpander.php @@ -0,0 +1,122 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Expander; + +use Generated\Shared\Transfer\LocaleTransfer; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Validator\Constraints\Length; + +class ImageAltTextProductFormExpander implements ImageAltTextProductFormExpanderInterface +{ + /** + * @var string + */ + protected const SUBFORM_IMAGE_SET_PREFIX = 'image_set_'; + + /** + * @var string + */ + protected const FIELD_ALT_TEXT = 'alt_text'; + + /** + * @var string + */ + protected const SUBFORM_IMAGE_COLLECTION = 'product_images'; + + /** + * @var string + */ + protected const TEMPLATE_PATH = '@SprykerEco:ProductManagementAi/ProductManagement/_partials/image-alt-text.twig'; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface + */ + protected ProductManagementAiToLocaleFacadeInterface $localeFacade; + + /** + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface $localeFacade + */ + public function __construct(ProductManagementAiToLocaleFacadeInterface $localeFacade) + { + $this->localeFacade = $localeFacade; + } + + /** + * @param \Symfony\Component\Form\FormBuilderInterface $builder + * @param array<string, mixed> $options + * + * @return void + */ + public function expand(FormBuilderInterface $builder, array $options): void + { + $this->addAltTextField($builder); + } + + /** + * @param \Symfony\Component\Form\FormBuilderInterface $builder + * + * @return \Symfony\Component\Form\FormBuilderInterface + */ + protected function addAltTextField(FormBuilderInterface $builder): FormBuilderInterface + { + $builder->addEventListener( + FormEvents::POST_SET_DATA, + function (FormEvent $event): void { + $form = $event->getForm(); + + $localeCollection = $this->localeFacade->getLocaleCollection(); + $localeCollection[] = (new LocaleTransfer())->setLocaleName('default'); + + foreach ($localeCollection as $localeTransfer) { + $localizedImageSetFormName = $this->getLocalizedImageSetFormName($localeTransfer); + if (!$form->has($localizedImageSetFormName)) { + continue; + } + + $imageSetCollection = $form->get($localizedImageSetFormName); + foreach ($imageSetCollection as $imageSet) { + $imageCollection = $imageSet->get(static::SUBFORM_IMAGE_COLLECTION); + + foreach ($imageCollection as $imageField) { + $imageField->add(static::FIELD_ALT_TEXT, TextType::class, [ + 'label' => 'Alt Text', + 'required' => false, + 'constraints' => [ + new Length([ + 'min' => 0, + 'max' => 255, + ]), + ], + 'sanitize_xss' => true, + 'attr' => [ + 'template_path' => static::TEMPLATE_PATH, + ], + ]); + } + } + } + }, + ); + + return $builder; + } + + /** + * @param \Generated\Shared\Transfer\LocaleTransfer $localeTransfer + * + * @return string + */ + protected function getLocalizedImageSetFormName(LocaleTransfer $localeTransfer): string + { + return static::SUBFORM_IMAGE_SET_PREFIX . $localeTransfer->getLocaleName(); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/ImageAltTextProductFormExpanderInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/ImageAltTextProductFormExpanderInterface.php new file mode 100644 index 0000000..ef7ecab --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Expander/ImageAltTextProductFormExpanderInterface.php @@ -0,0 +1,21 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Expander; + +use Symfony\Component\Form\FormBuilderInterface; + +interface ImageAltTextProductFormExpanderInterface +{ + /** + * @param \Symfony\Component\Form\FormBuilderInterface $builder + * @param array<string, mixed> $options + * + * @return void + */ + public function expand(FormBuilderInterface $builder, array $options): void; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Form/DataProvider/ProductCategoryAbstractFormDataProvider.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Form/DataProvider/ProductCategoryAbstractFormDataProvider.php new file mode 100644 index 0000000..c28e261 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Form/DataProvider/ProductCategoryAbstractFormDataProvider.php @@ -0,0 +1,80 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Form\DataProvider; + +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToCategoryFacadeInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToProductCategoryFacadeInterface; + +class ProductCategoryAbstractFormDataProvider implements ProductCategoryAbstractFormDataProviderInterface +{ + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToCategoryFacadeInterface + */ + protected ProductManagementAiToCategoryFacadeInterface $categoryFacade; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface + */ + protected ProductManagementAiToLocaleFacadeInterface $localeFacade; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToProductCategoryFacadeInterface + */ + protected ProductManagementAiToProductCategoryFacadeInterface $productCategoryFacade; + + /** + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToCategoryFacadeInterface $categoryFacade + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface $localeFacade + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToProductCategoryFacadeInterface $productCategoryFacade + */ + public function __construct( + ProductManagementAiToCategoryFacadeInterface $categoryFacade, + ProductManagementAiToLocaleFacadeInterface $localeFacade, + ProductManagementAiToProductCategoryFacadeInterface $productCategoryFacade + ) { + $this->categoryFacade = $categoryFacade; + $this->localeFacade = $localeFacade; + $this->productCategoryFacade = $productCategoryFacade; + } + + /** + * @return array<string, int> + */ + public function getOptions(): array + { + $formOptions = []; + $categoryCollectionTransfer = $this->categoryFacade->getAllCategoryCollection($this->localeFacade->getCurrentLocale()); + + foreach ($categoryCollectionTransfer->getCategories() as $categoryTransfer) { + $formOptions[$categoryTransfer->getNameOrFail()] = $categoryTransfer->getIdCategoryOrFail(); + } + + return $formOptions; + } + + /** + * @param int $idProductAbstract + * + * @return array<int> + */ + public function getData(int $idProductAbstract): array + { + $localeTransfer = $this->localeFacade->getCurrentLocale(); + $categoryCollectionTransfer = $this->productCategoryFacade->getCategoryTransferCollectionByIdProductAbstract( + $idProductAbstract, + $localeTransfer, + ); + $categoryIds = []; + foreach ($categoryCollectionTransfer->getCategories() as $categoryTransfer) { + $categoryIds[] = $categoryTransfer->getIdCategory(); + } + + return $categoryIds; + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Form/DataProvider/ProductCategoryAbstractFormDataProviderInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Form/DataProvider/ProductCategoryAbstractFormDataProviderInterface.php new file mode 100644 index 0000000..9378d00 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Form/DataProvider/ProductCategoryAbstractFormDataProviderInterface.php @@ -0,0 +1,23 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Form\DataProvider; + +interface ProductCategoryAbstractFormDataProviderInterface +{ + /** + * @return array<string, int> + */ + public function getOptions(): array; + + /** + * @param int $idProductAbstract + * + * @return array<int> + */ + public function getData(int $idProductAbstract): array; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Handler/ProductCategoryHandler.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Handler/ProductCategoryHandler.php new file mode 100644 index 0000000..7db2f5d --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Handler/ProductCategoryHandler.php @@ -0,0 +1,102 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Handler; + +use Generated\Shared\Transfer\ProductAbstractTransfer; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToProductCategoryFacadeInterface; + +class ProductCategoryHandler implements ProductCategoryHandlerInterface +{ + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToProductCategoryFacadeInterface + */ + protected ProductManagementAiToProductCategoryFacadeInterface $productCategoryFacade; + + /** + * @var \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface + */ + protected ProductManagementAiToLocaleFacadeInterface $localeFacade; + + /** + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToProductCategoryFacadeInterface $productCategoryFacade + * @param \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface $localeFacade + */ + public function __construct( + ProductManagementAiToProductCategoryFacadeInterface $productCategoryFacade, + ProductManagementAiToLocaleFacadeInterface $localeFacade + ) { + $this->productCategoryFacade = $productCategoryFacade; + $this->localeFacade = $localeFacade; + } + + /** + * @param \Generated\Shared\Transfer\ProductAbstractTransfer $productAbstractTransfer + * + * @return \Generated\Shared\Transfer\ProductAbstractTransfer + */ + public function updateProductCategories(ProductAbstractTransfer $productAbstractTransfer): ProductAbstractTransfer + { + $assignedCategoryIds = $this->getCategoryIdsByIdProductAbstract($productAbstractTransfer->getIdProductAbstract()); + $categoryIdsToAssign = $productAbstractTransfer->getCategoryIds(); + $assignedCategoryIdsToSave = array_diff($categoryIdsToAssign, $assignedCategoryIds); + $assignedCategoryIdsToDelete = array_diff($assignedCategoryIds, $categoryIdsToAssign); + foreach ($assignedCategoryIdsToSave as $idCategory) { + $this->productCategoryFacade->createProductCategoryMappings( + $idCategory, + [$productAbstractTransfer->getIdProductAbstract()], + ); + } + + foreach ($assignedCategoryIdsToDelete as $idCategory) { + $this->productCategoryFacade->removeProductCategoryMappings( + $idCategory, + [$productAbstractTransfer->getIdProductAbstract()], + ); + } + + return $productAbstractTransfer; + } + + /** + * @param \Generated\Shared\Transfer\ProductAbstractTransfer $productAbstractTransfer + * + * @return \Generated\Shared\Transfer\ProductAbstractTransfer + */ + public function createProductCategories(ProductAbstractTransfer $productAbstractTransfer): ProductAbstractTransfer + { + foreach ($productAbstractTransfer->getCategoryIds() as $idCategory) { + $this->productCategoryFacade->createProductCategoryMappings( + $idCategory, + [$productAbstractTransfer->getIdProductAbstract()], + ); + } + + return $productAbstractTransfer; + } + + /** + * @param int $idProductAbstract + * + * @return array<int> + */ + protected function getCategoryIdsByIdProductAbstract(int $idProductAbstract): array + { + $localeTransfer = $this->localeFacade->getCurrentLocale(); + $categoryCollectionTransfer = $this->productCategoryFacade->getCategoryTransferCollectionByIdProductAbstract( + $idProductAbstract, + $localeTransfer, + ); + $categoryIds = []; + foreach ($categoryCollectionTransfer->getCategories() as $categoryTransfer) { + $categoryIds[] = $categoryTransfer->getIdCategory(); + } + + return $categoryIds; + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Handler/ProductCategoryHandlerInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Handler/ProductCategoryHandlerInterface.php new file mode 100644 index 0000000..776920c --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Handler/ProductCategoryHandlerInterface.php @@ -0,0 +1,27 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Handler; + +use Generated\Shared\Transfer\ProductAbstractTransfer; + +interface ProductCategoryHandlerInterface +{ + /** + * @param \Generated\Shared\Transfer\ProductAbstractTransfer $productAbstractTransfer + * + * @return \Generated\Shared\Transfer\ProductAbstractTransfer + */ + public function updateProductCategories(ProductAbstractTransfer $productAbstractTransfer): ProductAbstractTransfer; + + /** + * @param \Generated\Shared\Transfer\ProductAbstractTransfer $productAbstractTransfer + * + * @return \Generated\Shared\Transfer\ProductAbstractTransfer + */ + public function createProductCategories(ProductAbstractTransfer $productAbstractTransfer): ProductAbstractTransfer; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/Product/ProductCategoryProductAbstractAfterUpdatePlugin.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/Product/ProductCategoryProductAbstractAfterUpdatePlugin.php new file mode 100644 index 0000000..91d644e --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/Product/ProductCategoryProductAbstractAfterUpdatePlugin.php @@ -0,0 +1,36 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Plugin\Product; + +use Generated\Shared\Transfer\ProductAbstractTransfer; +use Spryker\Zed\Kernel\Communication\AbstractPlugin; +use Spryker\Zed\Product\Dependency\Plugin\ProductAbstractPluginUpdateInterface; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\Communication\ProductManagementAiCommunicationFactory getFactory() + * @method \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig getConfig() + * @method \SprykerEco\Zed\ProductManagementAi\Business\ProductManagementAiFacadeInterface getFacade() + */ +class ProductCategoryProductAbstractAfterUpdatePlugin extends AbstractPlugin implements ProductAbstractPluginUpdateInterface +{ + /** + * {@inheritDoc} + * - Executed on "after" event when an abstract product is updated. + * - Persists product categories. + * + * @api + * + * @param \Generated\Shared\Transfer\ProductAbstractTransfer $productAbstractTransfer + * + * @return \Generated\Shared\Transfer\ProductAbstractTransfer + */ + public function update(ProductAbstractTransfer $productAbstractTransfer): ProductAbstractTransfer + { + return $this->getFactory()->createProductCategoryHandler()->updateProductCategories($productAbstractTransfer); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/Product/ProductCategoryProductAbstractPostCreatePlugin.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/Product/ProductCategoryProductAbstractPostCreatePlugin.php new file mode 100644 index 0000000..77c1fee --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/Product/ProductCategoryProductAbstractPostCreatePlugin.php @@ -0,0 +1,35 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Plugin\Product; + +use Generated\Shared\Transfer\ProductAbstractTransfer; +use Spryker\Zed\Kernel\Communication\AbstractPlugin; +use Spryker\Zed\ProductExtension\Dependency\Plugin\ProductAbstractPostCreatePluginInterface; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\Communication\ProductManagementAiCommunicationFactory getFactory() + * @method \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig getConfig() + * @method \SprykerEco\Zed\ProductManagementAi\Business\ProductManagementAiFacadeInterface getFacade() + */ +class ProductCategoryProductAbstractPostCreatePlugin extends AbstractPlugin implements ProductAbstractPostCreatePluginInterface +{ + /** + * {@inheritDoc} + * - Assigns categories to the product abstract after the product abstract is created. + * + * @api + * + * @param \Generated\Shared\Transfer\ProductAbstractTransfer $productAbstractTransfer + * + * @return \Generated\Shared\Transfer\ProductAbstractTransfer + */ + public function postCreate(ProductAbstractTransfer $productAbstractTransfer): ProductAbstractTransfer + { + return $this->getFactory()->createProductCategoryHandler()->createProductCategories($productAbstractTransfer); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ImageAltTextProductAbstractFormExpanderPlugin.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ImageAltTextProductAbstractFormExpanderPlugin.php new file mode 100644 index 0000000..967e092 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ImageAltTextProductAbstractFormExpanderPlugin.php @@ -0,0 +1,42 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Plugin\ProductManagement; + +use Spryker\Zed\Kernel\Communication\AbstractPlugin; +use Spryker\Zed\ProductManagementExtension\Dependency\Plugin\ProductAbstractFormExpanderPluginInterface; +use Spryker\Zed\ProductManagementExtension\Dependency\Plugin\ProductConcreteFormExpanderPluginInterface; +use Symfony\Component\Form\FormBuilderInterface; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig getConfig() + * @method \SprykerEco\Zed\ProductManagementAi\Business\ProductManagementAiFacadeInterface getFacade() + * @method \SprykerEco\Zed\ProductManagementAi\Communication\ProductManagementAiCommunicationFactory getFactory() + */ +class ImageAltTextProductAbstractFormExpanderPlugin extends AbstractPlugin implements ProductAbstractFormExpanderPluginInterface, ProductConcreteFormExpanderPluginInterface +{ + /** + * {@inheritDoc} + * - Expands ProductAbstract form with image alt text field. + * - Image alt text field is used to store the alt text of the image. + * + * @api + * + * @param \Symfony\Component\Form\FormBuilderInterface $builder + * @param array<string, mixed> $options + * + * @return \Symfony\Component\Form\FormBuilderInterface + */ + public function expand(FormBuilderInterface $builder, array $options): FormBuilderInterface + { + $this->getFactory() + ->createImageAltTextProductFormExpander() + ->expand($builder, $options); + + return $builder; + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ImageAltTextProductConcreteEditFormExpanderPlugin.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ImageAltTextProductConcreteEditFormExpanderPlugin.php new file mode 100644 index 0000000..8434af8 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ImageAltTextProductConcreteEditFormExpanderPlugin.php @@ -0,0 +1,38 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Plugin\ProductManagement; + +use Spryker\Zed\Kernel\Communication\AbstractPlugin; +use Spryker\Zed\ProductManagementExtension\Dependency\Plugin\ProductConcreteEditFormExpanderPluginInterface; +use Symfony\Component\Form\FormBuilderInterface; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig getConfig() + * @method \SprykerEco\Zed\ProductManagementAi\Business\ProductManagementAiFacadeInterface getFacade() + * @method \SprykerEco\Zed\ProductManagementAi\Communication\ProductManagementAiCommunicationFactory getFactory() + */ +class ImageAltTextProductConcreteEditFormExpanderPlugin extends AbstractPlugin implements ProductConcreteEditFormExpanderPluginInterface +{ + /** + * {@inheritDoc} + * - Expands ProductConcrete edit form with image alt text field. + * + * @api + * + * @param \Symfony\Component\Form\FormBuilderInterface $builder + * @param array<string, mixed> $options + * + * @return void + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $this->getFactory() + ->createImageAltTextProductFormExpander() + ->expand($builder, $options); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ImageAltTextProductConcreteFormExpanderPlugin.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ImageAltTextProductConcreteFormExpanderPlugin.php new file mode 100644 index 0000000..017d238 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ImageAltTextProductConcreteFormExpanderPlugin.php @@ -0,0 +1,40 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Plugin\ProductManagement; + +use Spryker\Zed\Kernel\Communication\AbstractPlugin; +use Spryker\Zed\ProductManagementExtension\Dependency\Plugin\ProductConcreteFormExpanderPluginInterface; +use Symfony\Component\Form\FormBuilderInterface; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig getConfig() + * @method \SprykerEco\Zed\ProductManagementAi\Business\ProductManagementAiFacadeInterface getFacade() + * @method \SprykerEco\Zed\ProductManagementAi\Communication\ProductManagementAiCommunicationFactory getFactory() + */ +class ImageAltTextProductConcreteFormExpanderPlugin extends AbstractPlugin implements ProductConcreteFormExpanderPluginInterface +{ + /** + * {@inheritDoc} + * - Expands ProductConcrete form with image alt text field. + * + * @api + * + * @param \Symfony\Component\Form\FormBuilderInterface $builder + * @param array<string, mixed> $options + * + * @return \Symfony\Component\Form\FormBuilderInterface + */ + public function expand(FormBuilderInterface $builder, array $options): FormBuilderInterface + { + $this->getFactory() + ->createImageAltTextProductFormExpander() + ->expand($builder, $options); + + return $builder; + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ProductCategoryAbstractFormExpanderPlugin.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ProductCategoryAbstractFormExpanderPlugin.php new file mode 100644 index 0000000..11471eb --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/Plugin/ProductManagement/ProductCategoryAbstractFormExpanderPlugin.php @@ -0,0 +1,36 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication\Plugin\ProductManagement; + +use Spryker\Zed\Kernel\Communication\AbstractPlugin; +use Spryker\Zed\ProductManagementExtension\Dependency\Plugin\ProductAbstractFormExpanderPluginInterface; +use Symfony\Component\Form\FormBuilderInterface; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\Communication\ProductManagementAiCommunicationFactory getFactory() + * @method \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig getConfig() + * @method \SprykerEco\Zed\ProductManagementAi\Business\ProductManagementAiFacadeInterface getFacade() + */ +class ProductCategoryAbstractFormExpanderPlugin extends AbstractPlugin implements ProductAbstractFormExpanderPluginInterface +{ + /** + * {@inheritDoc} + * - Expands ProductAbstract form with categoryIds field. + * + * @api + * + * @param \Symfony\Component\Form\FormBuilderInterface $builder + * @param array<string, mixed> $options + * + * @return \Symfony\Component\Form\FormBuilderInterface + */ + public function expand(FormBuilderInterface $builder, array $options): FormBuilderInterface + { + return $this->getFactory()->createCategoryIdsProductFormExpander()->expand($builder, $options); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Communication/ProductManagementAiCommunicationFactory.php b/src/SprykerEco/Zed/ProductManagementAi/Communication/ProductManagementAiCommunicationFactory.php new file mode 100644 index 0000000..f08a27e --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Communication/ProductManagementAiCommunicationFactory.php @@ -0,0 +1,94 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Communication; + +use Spryker\Zed\Kernel\Communication\AbstractCommunicationFactory; +use SprykerEco\Zed\ProductManagementAi\Communication\Expander\CategoryIdsProductFormExpander; +use SprykerEco\Zed\ProductManagementAi\Communication\Expander\CategoryIdsProductFormExpanderInterface; +use SprykerEco\Zed\ProductManagementAi\Communication\Expander\ImageAltTextProductFormExpander; +use SprykerEco\Zed\ProductManagementAi\Communication\Expander\ImageAltTextProductFormExpanderInterface; +use SprykerEco\Zed\ProductManagementAi\Communication\Form\DataProvider\ProductCategoryAbstractFormDataProvider; +use SprykerEco\Zed\ProductManagementAi\Communication\Form\DataProvider\ProductCategoryAbstractFormDataProviderInterface; +use SprykerEco\Zed\ProductManagementAi\Communication\Handler\ProductCategoryHandler; +use SprykerEco\Zed\ProductManagementAi\Communication\Handler\ProductCategoryHandlerInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToCategoryFacadeInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToProductCategoryFacadeInterface; +use SprykerEco\Zed\ProductManagementAi\ProductManagementAiDependencyProvider; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig getConfig() + * @method \SprykerEco\Zed\ProductManagementAi\Business\ProductManagementAiFacadeInterface getFacade() + */ +class ProductManagementAiCommunicationFactory extends AbstractCommunicationFactory +{ + /** + * @return \SprykerEco\Zed\ProductManagementAi\Communication\Form\DataProvider\ProductCategoryAbstractFormDataProviderInterface + */ + public function createProductCategoryAbstractFormDataProvider(): ProductCategoryAbstractFormDataProviderInterface + { + return new ProductCategoryAbstractFormDataProvider( + $this->getCategoryFacade(), + $this->getLocaleFacade(), + $this->getProductCategoryFacade(), + ); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Communication\Expander\ImageAltTextProductFormExpanderInterface + */ + public function createImageAltTextProductFormExpander(): ImageAltTextProductFormExpanderInterface + { + return new ImageAltTextProductFormExpander( + $this->getLocaleFacade(), + ); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Communication\Handler\ProductCategoryHandlerInterface + */ + public function createProductCategoryHandler(): ProductCategoryHandlerInterface + { + return new ProductCategoryHandler( + $this->getProductCategoryFacade(), + $this->getLocaleFacade(), + ); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Communication\Expander\CategoryIdsProductFormExpanderInterface + */ + public function createCategoryIdsProductFormExpander(): CategoryIdsProductFormExpanderInterface + { + return new CategoryIdsProductFormExpander($this->createProductCategoryAbstractFormDataProvider()); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToCategoryFacadeInterface + */ + public function getCategoryFacade(): ProductManagementAiToCategoryFacadeInterface + { + return $this->getProvidedDependency(ProductManagementAiDependencyProvider::FACADE_CATEGORY); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeInterface + */ + public function getLocaleFacade(): ProductManagementAiToLocaleFacadeInterface + { + return $this->getProvidedDependency(ProductManagementAiDependencyProvider::FACADE_LOCALE); + } + + /** + * @return \SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToProductCategoryFacadeInterface + */ + public function getProductCategoryFacade(): ProductManagementAiToProductCategoryFacadeInterface + { + return $this->getProvidedDependency(ProductManagementAiDependencyProvider::FACADE_PRODUCT_CATEGORY); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Dependency/Client/ProductManagementAiToOpenAiClientBridge.php b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Client/ProductManagementAiToOpenAiClientBridge.php new file mode 100644 index 0000000..da742cf --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Client/ProductManagementAiToOpenAiClientBridge.php @@ -0,0 +1,37 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Dependency\Client; + +use Generated\Shared\Transfer\OpenAiChatRequestTransfer; +use Generated\Shared\Transfer\OpenAiChatResponseTransfer; + +class ProductManagementAiToOpenAiClientBridge implements ProductManagementAiToOpenAiClientInterface +{ + /** + * @var \SprykerEco\Client\OpenAi\OpenAiClientInterface + */ + protected $openAiClient; + + /** + * @param \SprykerEco\Client\OpenAi\OpenAiClientInterface $openAiClient + */ + public function __construct($openAiClient) + { + $this->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/Zed/ProductManagementAi/Dependency/Client/ProductManagementAiToOpenAiClientInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Client/ProductManagementAiToOpenAiClientInterface.php new file mode 100644 index 0000000..aa13de4 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Client/ProductManagementAiToOpenAiClientInterface.php @@ -0,0 +1,21 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Dependency\Client; + +use Generated\Shared\Transfer\OpenAiChatRequestTransfer; +use Generated\Shared\Transfer\OpenAiChatResponseTransfer; + +interface ProductManagementAiToOpenAiClientInterface +{ + /** + * @param \Generated\Shared\Transfer\OpenAiChatRequestTransfer $openAiRequestTransfer + * + * @return \Generated\Shared\Transfer\OpenAiChatResponseTransfer + */ + public function chat(OpenAiChatRequestTransfer $openAiRequestTransfer): OpenAiChatResponseTransfer; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToCategoryFacadeBridge.php b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToCategoryFacadeBridge.php new file mode 100644 index 0000000..ed1a657 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToCategoryFacadeBridge.php @@ -0,0 +1,37 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Dependency\Facade; + +use Generated\Shared\Transfer\CategoryCollectionTransfer; +use Generated\Shared\Transfer\LocaleTransfer; + +class ProductManagementAiToCategoryFacadeBridge implements ProductManagementAiToCategoryFacadeInterface +{ + /** + * @var \Spryker\Zed\Category\Business\CategoryFacadeInterface + */ + protected $categoryFacade; + + /** + * @param \Spryker\Zed\Category\Business\CategoryFacadeInterface $categoryFacade + */ + public function __construct($categoryFacade) + { + $this->categoryFacade = $categoryFacade; + } + + /** + * @param \Generated\Shared\Transfer\LocaleTransfer $localeTransfer + * + * @return \Generated\Shared\Transfer\CategoryCollectionTransfer + */ + public function getAllCategoryCollection(LocaleTransfer $localeTransfer): CategoryCollectionTransfer + { + return $this->categoryFacade->getAllCategoryCollection($localeTransfer); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToCategoryFacadeInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToCategoryFacadeInterface.php new file mode 100644 index 0000000..911436e --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToCategoryFacadeInterface.php @@ -0,0 +1,21 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Dependency\Facade; + +use Generated\Shared\Transfer\CategoryCollectionTransfer; +use Generated\Shared\Transfer\LocaleTransfer; + +interface ProductManagementAiToCategoryFacadeInterface +{ + /** + * @param \Generated\Shared\Transfer\LocaleTransfer $localeTransfer + * + * @return \Generated\Shared\Transfer\CategoryCollectionTransfer + */ + public function getAllCategoryCollection(LocaleTransfer $localeTransfer): CategoryCollectionTransfer; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToLocaleFacadeBridge.php b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToLocaleFacadeBridge.php new file mode 100644 index 0000000..59fe1c1 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToLocaleFacadeBridge.php @@ -0,0 +1,42 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Dependency\Facade; + +use Generated\Shared\Transfer\LocaleTransfer; + +class ProductManagementAiToLocaleFacadeBridge implements ProductManagementAiToLocaleFacadeInterface +{ + /** + * @var \Spryker\Zed\Locale\Business\LocaleFacadeInterface + */ + protected $localeFacade; + + /** + * @param \Spryker\Zed\Locale\Business\LocaleFacadeInterface $localeFacade + */ + public function __construct($localeFacade) + { + $this->localeFacade = $localeFacade; + } + + /** + * @return array<\Generated\Shared\Transfer\LocaleTransfer> + */ + public function getLocaleCollection(): array + { + return $this->localeFacade->getLocaleCollection(); + } + + /** + * @return \Generated\Shared\Transfer\LocaleTransfer + */ + public function getCurrentLocale(): LocaleTransfer + { + return $this->localeFacade->getCurrentLocale(); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToLocaleFacadeInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToLocaleFacadeInterface.php new file mode 100644 index 0000000..b092dfc --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToLocaleFacadeInterface.php @@ -0,0 +1,23 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Dependency\Facade; + +use Generated\Shared\Transfer\LocaleTransfer; + +interface ProductManagementAiToLocaleFacadeInterface +{ + /** + * @return array<\Generated\Shared\Transfer\LocaleTransfer> + */ + public function getLocaleCollection(): array; + + /** + * @return \Generated\Shared\Transfer\LocaleTransfer + */ + public function getCurrentLocale(): LocaleTransfer; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToProductCategoryFacadeBridge.php b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToProductCategoryFacadeBridge.php new file mode 100644 index 0000000..060f79b --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToProductCategoryFacadeBridge.php @@ -0,0 +1,62 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Dependency\Facade; + +use Generated\Shared\Transfer\CategoryCollectionTransfer; +use Generated\Shared\Transfer\LocaleTransfer; + +class ProductManagementAiToProductCategoryFacadeBridge implements ProductManagementAiToProductCategoryFacadeInterface +{ + /** + * @var \Spryker\Zed\ProductCategory\Business\ProductCategoryFacadeInterface + */ + protected $productCategoryFacade; + + /** + * @param \Spryker\Zed\ProductCategory\Business\ProductCategoryFacadeInterface $productCategoryFacade + */ + public function __construct($productCategoryFacade) + { + $this->productCategoryFacade = $productCategoryFacade; + } + + /** + * @param int $idCategory + * @param array<int> $productIdsToAssign + * + * @return void + */ + public function createProductCategoryMappings(int $idCategory, array $productIdsToAssign): void + { + $this->productCategoryFacade->createProductCategoryMappings($idCategory, $productIdsToAssign); + } + + /** + * @param int $idCategory + * @param array<int> $productIdsToUnAssign + * + * @return void + */ + public function removeProductCategoryMappings(int $idCategory, array $productIdsToUnAssign): void + { + $this->productCategoryFacade->removeProductCategoryMappings($idCategory, $productIdsToUnAssign); + } + + /** + * @param int $idProductAbstract + * @param \Generated\Shared\Transfer\LocaleTransfer $localeTransfer + * + * @return \Generated\Shared\Transfer\CategoryCollectionTransfer + */ + public function getCategoryTransferCollectionByIdProductAbstract( + int $idProductAbstract, + LocaleTransfer $localeTransfer + ): CategoryCollectionTransfer { + return $this->productCategoryFacade->getCategoryTransferCollectionByIdProductAbstract($idProductAbstract, $localeTransfer); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToProductCategoryFacadeInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToProductCategoryFacadeInterface.php new file mode 100644 index 0000000..48fd64b --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Facade/ProductManagementAiToProductCategoryFacadeInterface.php @@ -0,0 +1,41 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Dependency\Facade; + +use Generated\Shared\Transfer\CategoryCollectionTransfer; +use Generated\Shared\Transfer\LocaleTransfer; + +interface ProductManagementAiToProductCategoryFacadeInterface +{ + /** + * @param int $idCategory + * @param array<int> $productIdsToAssign + * + * @return void + */ + public function createProductCategoryMappings(int $idCategory, array $productIdsToAssign): void; + + /** + * @param int $idCategory + * @param array<int> $productIdsToUnAssign + * + * @return void + */ + public function removeProductCategoryMappings(int $idCategory, array $productIdsToUnAssign): void; + + /** + * @param int $idProductAbstract + * @param \Generated\Shared\Transfer\LocaleTransfer $localeTransfer + * + * @return \Generated\Shared\Transfer\CategoryCollectionTransfer + */ + public function getCategoryTransferCollectionByIdProductAbstract( + int $idProductAbstract, + LocaleTransfer $localeTransfer + ): CategoryCollectionTransfer; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Dependency/Service/ProductManagementAiToUtilEncodingServiceBridge.php b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Service/ProductManagementAiToUtilEncodingServiceBridge.php new file mode 100644 index 0000000..81dfb69 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Service/ProductManagementAiToUtilEncodingServiceBridge.php @@ -0,0 +1,54 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Dependency\Service; + +class ProductManagementAiToUtilEncodingServiceBridge implements ProductManagementAiToUtilEncodingServiceInterface +{ + /** + * @var \Spryker\Service\UtilEncoding\UtilEncodingServiceInterface + */ + protected $utilEncodingService; + + /** + * @param \Spryker\Service\UtilEncoding\UtilEncodingServiceInterface $utilEncodingService + */ + public function __construct($utilEncodingService) + { + $this->utilEncodingService = $utilEncodingService; + } + + /** + * @param array<mixed> $value + * @param int|null $options + * @param int|null $depth + * + * @return string|null + */ + public function encodeJson(array $value, ?int $options = null, ?int $depth = null): ?string + { + return $this->utilEncodingService->encodeJson($value, $options, $depth); + } + + /** + * @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 array<mixed>|null + */ + public function decodeJson(string $jsonValue, bool $assoc = false, ?int $depth = null, ?int $options = null): ?array + { + if ($assoc === false) { + trigger_error('Param #2 `$assoc` must be `true` as return of type `object` is not accepted.', E_USER_DEPRECATED); + } + + /** @phpstan-var array<mixed>|null */ + return $this->utilEncodingService->decodeJson($jsonValue, $assoc, $depth, $options); + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Dependency/Service/ProductManagementAiToUtilEncodingServiceInterface.php b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Service/ProductManagementAiToUtilEncodingServiceInterface.php new file mode 100644 index 0000000..980c626 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Dependency/Service/ProductManagementAiToUtilEncodingServiceInterface.php @@ -0,0 +1,30 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi\Dependency\Service; + +interface ProductManagementAiToUtilEncodingServiceInterface +{ + /** + * @param array<mixed> $value + * @param int|null $options + * @param int|null $depth + * + * @return string|null + */ + public function encodeJson(array $value, ?int $options = null, ?int $depth = null): ?string; + + /** + * @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 array<mixed>|null + */ + public function decodeJson(string $jsonValue, bool $assoc = false, ?int $depth = null, ?int $options = null): ?array; +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Persistence/Propel/Schema/spy_product_image.schema.xml b/src/SprykerEco/Zed/ProductManagementAi/Persistence/Propel/Schema/spy_product_image.schema.xml new file mode 100644 index 0000000..d66209b --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Persistence/Propel/Schema/spy_product_image.schema.xml @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<database xmlns="spryker:schema-01" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="zed" xsi:schemaLocation="spryker:schema-01 https://static.spryker.com/schema-01.xsd" namespace="Orm\Zed\ProductImage\Persistence" package="src.Orm.Zed.ProductImage.Persistence"> + + <table name="spy_product_image"> + <column name="alt_text" type="VARCHAR" size="255" required="false"/> + </table> + +</database> diff --git a/src/SprykerEco/Zed/ProductManagementAi/Presentation/ProductManagement/_partials/image-alt-text.twig b/src/SprykerEco/Zed/ProductManagementAi/Presentation/ProductManagement/_partials/image-alt-text.twig new file mode 100644 index 0000000..2b6ebd6 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Presentation/ProductManagement/_partials/image-alt-text.twig @@ -0,0 +1,19 @@ +{% set fieldSelector = '.js-image-alt-text-field input' %} +{# Programmatically find a field with a pattern name, where "%locale%" dynamically changes based on the image-set's locale. #} +{% set fieldPatter = ['product_form_edit[image_set_%locale%][0][product_images][0][external_url_small]', 'product_form_edit[image_set_%local%][0][product_images][0][external_url_large]'] %} + +{% block body %} + <div class="form-wrapper-clickable-affix"> + {% block input %} + {{ form_row(form, { + row_attr: { class: 'js-image-alt-text-field image-alt-text-field' } + }) }} + {% endblock %} + + {% block trigger %} + <button type="button" data-url="/product-management-ai/image-alt-text" data-product-field-pattern="{{ fieldPatter | json_encode }}" data-field-selector="{{ fieldSelector }}" popovertarget="ai-alt-text-modal" class="form-wrapper-clickable-affix__trigger js-ai-alt-image-trigger btn-naked" title="{{ 'AI assistant' | trans }}"> + <span class="fa fa-magic"></span> + </button> + {% endblock %} + </div> +{% endblock %} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/ai-popover.twig b/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/ai-popover.twig new file mode 100644 index 0000000..17f6577 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/ai-popover.twig @@ -0,0 +1,52 @@ +{% block body %} + <dialog popover id="{{ id }}" class="ai-product-management-modal modal-popover"> + {% block modal %} + <div class="modal"> + <div class="modal-dialog"> + <div class="modal-content"> + {% block header %} + <div class="modal-header"> + <button type="button" class="close" popovertarget="{{ id }}" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + <h4 class="modal-title">{{ 'AI assistant' | trans }}</h4> + </div> + {% endblock %} + + <div class="modal-body js-modal-body"> + {% block loading %} + <div class="ai-product-management-modal__loading"> + <span>{{ 'AI is processing your request' | trans }}</span> + <span class="fa fa-spinner fa-spin"></span> + </div> + {% endblock %} + + {% block empty %} + {% if emptyText %} + <span class="text-center ai-product-management-modal__empty">{{ emptyText }}</span> + {% endif %} + {% endblock %} + + <div class="ai-product-management-modal__content"> + {% block content %}{% endblock %} + </div> + </div> + + {% block footer %} + <div class="modal-footer ai-product-management-modal__actions"> + {% block actions %} + <div class="modal-actions"> + <button type="button" class="js-ai-product-management-again btn btn-outline btn-view">{{ 'Try again' | trans }}</button> + <button type="button" popovertarget="{{ id }}" class="js-ai-product-management-apply btn">{{ 'Apply' | trans }}</button> + </div> + {% endblock %} + </div> + {% endblock %} + </div> + </div> + + <button type="button" tabindex="-1" popovertarget="{{ id }}" class="modal-backdrop"></button> + </div> + {% endblock %} + </dialog> +{% endblock %} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/ai-translation-trigger.twig b/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/ai-translation-trigger.twig new file mode 100644 index 0000000..d9d79c8 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/ai-translation-trigger.twig @@ -0,0 +1,40 @@ +{% set fieldLocale = locale.localeName %} + +{% block body %} + <div class="form-wrapper-clickable-affix"> + {% block input %}{% endblock %} + + {% block trigger %} + {% set fromLocale = locale.localeName %} + + <div class="form-wrapper-clickable-affix__trigger"> + <button type="button" class="btn-naked" title="{{ 'AI assistant' | trans }}"> + <span class="fa fa-magic"></span> + </button> + + <ul class="ai-translator"> + <li><h5>{{ 'Translate to:' | trans }}</h5></li> + + {% for locale in localeCollection %} + {% if fromLocale != locale.localeName %} + <li> + <button + popovertarget="ai-translation-modal" + type="button" + class="js-ai-translation-trigger btn-naked" + data-to-locale="{{ locale.localeName }}" + data-url="/product-management-ai/translate" + data-field-locale="{{ fieldLocale }}" + data-field-name="{{ input.vars.name }}" + data-from-locale="{{ fromLocale }}" + > + {{ locale.localeName }} + </button> + </li> + {% endif %} + {% endfor %} + </ul> + </div> + {% endblock %} + </div> +{% endblock %} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/category-modal-trigger.twig b/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/category-modal-trigger.twig new file mode 100644 index 0000000..17aa6c5 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/category-modal-trigger.twig @@ -0,0 +1,17 @@ +{% set fieldSelector = '.js-ai-categories select' %} + +{% block body %} + <div class="form-wrapper-clickable-affix"> + {% block input %} + {{ form_row(form.categoryIds, { + row_attr: { class: 'js-ai-categories' } + }) }} + {% endblock %} + + {% block trigger %} + <button type="button" data-url="/product-management-ai/category-suggestion" data-product-info-field=".js-infomational-field" data-field-selector="{{ fieldSelector }}" popovertarget="ai-category-modal" class="form-wrapper-clickable-affix__trigger js-ai-category-trigger btn-naked" title="{{ 'AI assistant' | trans }}"> + <span class="fa fa-magic"></span> + </button> + {% endblock %} + </div> +{% endblock %} diff --git a/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/product-management-ai-modals.twig b/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/product-management-ai-modals.twig new file mode 100644 index 0000000..4e39301 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/Presentation/_partials/product-management-ai-modals.twig @@ -0,0 +1,36 @@ +{% block body %} + {% block assets %} + <link rel="stylesheet" href="{{ assetsPath('css/spryker-ai-category-suggestion-product-management-ai.css') }}"/> + <script src="{{ assetsPath('js/spryker-ai-category-suggestion-product-management-ai.js') }}"></script> + {% endblock %} + + {% embed 'ProductManagementAi/_partials/ai-popover.twig' with { + id: 'ai-category-modal', + emptyText: 'Please fill in the product name and description for at least one locale' | trans, + } %} + {% block content %} + <select multiple dropdown-parent=".js-modal-body" class="js-ai-category-select spryker-form-select2combobox"></select> + {% endblock %} + {% endembed %} + + {% embed 'ProductManagementAi/_partials/ai-popover.twig' with { + id: 'ai-alt-text-modal', + emptyText: 'Please fill in the product image url' | trans, + } %} + {% block content %} + <input class="form-control js-ai-alt-text-input"/> + {% endblock %} + {% endembed %} + + {% embed 'ProductManagementAi/_partials/ai-popover.twig' with { + id: 'ai-translation-modal', + emptyText: 'Please fill in text to translate' | trans, + } %} + {% block content %} + <div class="modal-subtitle">{{ 'Translated to:' | trans }} <strong class="js-translate-to"></strong></div> + + <input name="ai-translation-preview[name]" class="ai-translation-control js-ai-translation-control form-control" /> + <textarea name="ai-translation-preview[description]" class="ai-translation-control js-ai-translation-control form-control"></textarea> + {% endblock %} + {% endembed %} +{% endblock %} diff --git a/src/SprykerEco/Zed/ProductManagementAi/ProductManagementAiConfig.php b/src/SprykerEco/Zed/ProductManagementAi/ProductManagementAiConfig.php new file mode 100644 index 0000000..ed3d9c7 --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/ProductManagementAiConfig.php @@ -0,0 +1,106 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi; + +use Spryker\Zed\Kernel\AbstractBundleConfig; + +class ProductManagementAiConfig extends AbstractBundleConfig +{ + /** + * @var string + */ + protected const OPEN_AI_GPT4O_MINI_MODEL = 'gpt-4o-mini'; + + /** + * @var string + */ + protected const DEFAULT_TRANSLATION_SOURCE_LOCALE = 'en_US'; + + /** + * @var string + */ + protected const AI_TRANSLATION_PROMPT_TEMPLATE = 'Support me in translating the following text `%s` from %s locale to %s locale(s) for an online shop, ensuring native speaker fluency. + Generate accurate and contextually fitting translations to enhance the user experience. + The texts to be translated may contain URLs, URL paths, HTML, unicode characters or some word enclosed by the character "%%", please don\'t translate them. + IMPORTANT: ONLY RETURN THE TRANSLATED TEXT AND NOTHING ELSE.'; + + /** + * @var string + */ + protected const PRODUCT_CATEGORY_SUGGESTION_PROMPT_TEMPLATE = 'Based on the provided product name and description, suggest the most fitting product categories from the existing categories list for optimal placement in an e-commerce store. + Product name: %s + Product description: %s + Existing categories: + %s + + Provide only the suggested categories as your output as key and value'; + + /** + * @api + * + * @param string $locale + * + * @return string + */ + public function getImageAltTextPrompt(string $locale): string + { + return sprintf( + '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 define the HTML alt text for best SEO using the language from locale %s. 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.', + $locale, + ); + } + + /** + * @api + * + * @return string + */ + public function getOpenAiGpt4oMiniModel(): string + { + return static::OPEN_AI_GPT4O_MINI_MODEL; + } + + /** + * Specification: + * - Returns the default source locale for translator. + * + * @api + * + * @return string + */ + public function getDefaultTranslationSourceLocale(): string + { + return static::DEFAULT_TRANSLATION_SOURCE_LOCALE; + } + + /** + * Specification: + * - Returns prompt template for AI translation. + * + * @api + * + * @return string + */ + public function getAiTranslationPromptTemplate(): string + { + return static::AI_TRANSLATION_PROMPT_TEMPLATE; + } + + /** + * Specification: + * - Returns prompt template for AI product category suggestion. + * + * @api + * + * @return string + */ + public function getProductCategorySuggestionPromptTemplate(): string + { + return static::PRODUCT_CATEGORY_SUGGESTION_PROMPT_TEMPLATE; + } +} diff --git a/src/SprykerEco/Zed/ProductManagementAi/ProductManagementAiDependencyProvider.php b/src/SprykerEco/Zed/ProductManagementAi/ProductManagementAiDependencyProvider.php new file mode 100644 index 0000000..afbe50c --- /dev/null +++ b/src/SprykerEco/Zed/ProductManagementAi/ProductManagementAiDependencyProvider.php @@ -0,0 +1,146 @@ +<?php + +/** + * MIT License + * For full license information, please view the LICENSE file that was distributed with this source code. + */ + +namespace SprykerEco\Zed\ProductManagementAi; + +use Spryker\Zed\Kernel\AbstractBundleDependencyProvider; +use Spryker\Zed\Kernel\Container; +use SprykerEco\Zed\ProductManagementAi\Dependency\Client\ProductManagementAiToOpenAiClientBridge; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToCategoryFacadeBridge; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToLocaleFacadeBridge; +use SprykerEco\Zed\ProductManagementAi\Dependency\Facade\ProductManagementAiToProductCategoryFacadeBridge; +use SprykerEco\Zed\ProductManagementAi\Dependency\Service\ProductManagementAiToUtilEncodingServiceBridge; + +/** + * @method \SprykerEco\Zed\ProductManagementAi\ProductManagementAiConfig getConfig() + */ +class ProductManagementAiDependencyProvider extends AbstractBundleDependencyProvider +{ + /** + * @var string + */ + public const FACADE_CATEGORY = 'FACADE_CATEGORY'; + + /** + * @var string + */ + public const FACADE_LOCALE = 'FACADE_LOCALE'; + + /** + * @var string + */ + public const FACADE_PRODUCT_CATEGORY = 'FACADE_PRODUCT_CATEGORY'; + + /** + * @var string + */ + public const SERVICE_UTIL_ENCODING = 'SERVICE_UTIL_ENCODING'; + + /** + * @var string + */ + public const CLIENT_OPEN_AI = 'CLIENT_OPEN_AI'; + + /** + * @param \Spryker\Zed\Kernel\Container $container + * + * @return \Spryker\Zed\Kernel\Container + */ + public function provideBusinessLayerDependencies(Container $container): Container + { + $container = $this->addCategoryFacade($container); + $container = $this->addLocaleFacade($container); + $container = $this->addUtilEncodingService($container); + $container = $this->addOpenAiClient($container); + + return $container; + } + + /** + * @param \Spryker\Zed\Kernel\Container $container + * + * @return \Spryker\Zed\Kernel\Container + */ + public function provideCommunicationLayerDependencies(Container $container): Container + { + $container = $this->addCategoryFacade($container); + $container = $this->addLocaleFacade($container); + $container = $this->addProductCategoryFacade($container); + + return $container; + } + + /** + * @param \Spryker\Zed\Kernel\Container $container + * + * @return \Spryker\Zed\Kernel\Container + */ + protected function addCategoryFacade(Container $container): Container + { + $container->set(static::FACADE_CATEGORY, function (Container $container) { + return new ProductManagementAiToCategoryFacadeBridge($container->getLocator()->category()->facade()); + }); + + return $container; + } + + /** + * @param \Spryker\Zed\Kernel\Container $container + * + * @return \Spryker\Zed\Kernel\Container + */ + protected function addLocaleFacade(Container $container): Container + { + $container->set(static::FACADE_LOCALE, function (Container $container) { + return new ProductManagementAiToLocaleFacadeBridge($container->getLocator()->locale()->facade()); + }); + + return $container; + } + + /** + * @param \Spryker\Zed\Kernel\Container $container + * + * @return \Spryker\Zed\Kernel\Container + */ + protected function addUtilEncodingService(Container $container): Container + { + $container->set(static::SERVICE_UTIL_ENCODING, function (Container $container) { + return new ProductManagementAiToUtilEncodingServiceBridge($container->getLocator()->utilEncoding()->service()); + }); + + return $container; + } + + /** + * @param \Spryker\Zed\Kernel\Container $container + * + * @return \Spryker\Zed\Kernel\Container + */ + protected function addOpenAiClient(Container $container): Container + { + $container->set(static::CLIENT_OPEN_AI, function (Container $container) { + return new ProductManagementAiToOpenAiClientBridge($container->getLocator()->openAi()->client()); + }); + + return $container; + } + + /** + * @param \Spryker\Zed\Kernel\Container $container + * + * @return \Spryker\Zed\Kernel\Container + */ + protected function addProductCategoryFacade(Container $container): Container + { + $container->set(static::FACADE_PRODUCT_CATEGORY, function (Container $container) { + return new ProductManagementAiToProductCategoryFacadeBridge($container->getLocator()->productCategory()->facade()); + }); + + return $container; + } +} diff --git a/tooling.yml b/tooling.yml new file mode 100644 index 0000000..669cf46 --- /dev/null +++ b/tooling.yml @@ -0,0 +1,5 @@ +architecture-sniffer: + priority: 2 + +code-sniffer: + level: 2