From 32f0dbab6386940c89126647e6579fa079115839 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Sat, 19 Sep 2026 11:53:34 +0330 Subject: [PATCH] Add AI fill for product technical data and localize the modal. Users can fill technical fields from a prefilled prompt, and FA locale shows Persian titles and actions; SEO modal no longer shows a search icon. Co-authored-by: Cursor --- .../src/components/ProductSeoModal.module.css | 6 - .../src/components/ProductSeoModal.tsx | 7 +- .../components/ProductTechnicalInfoModal.tsx | 187 +++++++++++----- .../ProductTechnicalValuesAiPromptModal.tsx | 211 ++++++++++++++++++ .../components/TechnicalFormModal.module.css | 12 + apps/business/src/i18n/messages.ts | 32 +++ .../services/productTechnicalInfoService.ts | 14 ++ 7 files changed, 397 insertions(+), 72 deletions(-) create mode 100644 apps/business/src/components/ProductTechnicalValuesAiPromptModal.tsx diff --git a/apps/business/src/components/ProductSeoModal.module.css b/apps/business/src/components/ProductSeoModal.module.css index ae8da99..9420796 100644 --- a/apps/business/src/components/ProductSeoModal.module.css +++ b/apps/business/src/components/ProductSeoModal.module.css @@ -1,9 +1,3 @@ .modal { width: min(560px, calc(100vw - 32px)); } - -.titleRow { - display: inline-flex; - align-items: center; - gap: 8px; -} diff --git a/apps/business/src/components/ProductSeoModal.tsx b/apps/business/src/components/ProductSeoModal.tsx index 59c450d..550b2f1 100644 --- a/apps/business/src/components/ProductSeoModal.tsx +++ b/apps/business/src/components/ProductSeoModal.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' -import { Search, X } from 'lucide-react' +import { X } from 'lucide-react' import { ApiError } from '../lib/api' import { useT } from '../i18n/useT' import { getProduct, updateProduct } from '../services/productService' @@ -107,9 +107,8 @@ export function ProductSeoModal({ >
-

-

diff --git a/apps/business/src/components/ProductTechnicalInfoModal.tsx b/apps/business/src/components/ProductTechnicalInfoModal.tsx index fe1acb8..539bc8e 100644 --- a/apps/business/src/components/ProductTechnicalInfoModal.tsx +++ b/apps/business/src/components/ProductTechnicalInfoModal.tsx @@ -1,8 +1,11 @@ import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' -import { X } from 'lucide-react' +import { Sparkles, X } from 'lucide-react' +import { useLocale } from '@meshkee/dashboard-ui' import { ApiError } from '../lib/api' +import { useT } from '../i18n/useT' import { + fillProductTechnicalInfoByAi, getProductTechnicalInfo, saveProductTechnicalInfo, valuesToFormState, @@ -12,8 +15,10 @@ import type { ProductTechnicalFormValues, TechnicalFieldType, } from '../types/technicalForm' +import { ProductTechnicalValuesAiPromptModal } from './ProductTechnicalValuesAiPromptModal' import styles from './VariationsModal.module.css' import fieldStyles from './TechnicalFormModal.module.css' +import aiStyles from '../styles/ai.module.css' interface ProductTechnicalInfoModalProps { open: boolean @@ -32,12 +37,17 @@ export function ProductTechnicalInfoModal({ categoryId, onClose, }: ProductTechnicalInfoModalProps) { + const t = useT() + const { locale } = useLocale() + const isFa = locale === 'fa' const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) const [form, setForm] = useState(null) const [values, setValues] = useState({}) const [isLoading, setIsLoading] = useState(false) const [isSaving, setIsSaving] = useState(false) + const [isAiOpen, setIsAiOpen] = useState(false) + const [isAiRunning, setIsAiRunning] = useState(false) const [error, setError] = useState('') const [infoMessage, setInfoMessage] = useState('') @@ -45,8 +55,10 @@ export function ProductTechnicalInfoModal({ if (open) { setMounted(true) setClosing(false) + setIsAiOpen(false) } else if (mounted) { setClosing(true) + setIsAiOpen(false) const timer = setTimeout(() => { setMounted(false) setClosing(false) @@ -75,16 +87,16 @@ export function ProductTechnicalInfoModal({ if (data.message) { setInfoMessage(data.message) } else if (!categoryId) { - setInfoMessage('Assign a category to this product before adding technical data.') + setInfoMessage(t('products.technical.needCategory')) } else if (!data.form) { - setInfoMessage('No technical form is defined for this product category.') + setInfoMessage(t('products.technical.noForm')) } } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') return if (err instanceof ApiError) { setError(err.message) } else { - setError('Unable to load technical data.') + setError(t('products.technical.loadError')) } } finally { setIsLoading(false) @@ -92,13 +104,13 @@ export function ProductTechnicalInfoModal({ } useEffect(() => { - if (!mounted || closing) return + if (!mounted || closing || isAiOpen) return const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } document.addEventListener('keydown', onKey) return () => document.removeEventListener('keydown', onKey) - }, [mounted, closing, onClose]) + }, [mounted, closing, isAiOpen, onClose]) if (!mounted) return null @@ -145,13 +157,26 @@ export function ProductTechnicalInfoModal({ if (err instanceof ApiError) { setError(err.message) } else { - setError('Unable to save technical data.') + setError(t('products.technical.saveError')) } } finally { setIsSaving(false) } } + async function handleAiRun(prompt: string) { + setIsAiRunning(true) + setError('') + try { + const result = await fillProductTechnicalInfoByAi(productId, prompt) + setValues((prev) => ({ ...prev, ...result.values })) + setInfoMessage(t('products.technical.ai.filled')) + setIsAiOpen(false) + } finally { + setIsAiRunning(false) + } + } + function renderField(field: CategoryTechnicalForm['fields'][number]) { const value = values[field.key] const type = field.type as TechnicalFieldType @@ -173,7 +198,7 @@ export function ProductTechnicalInfoModal({ value={typeof value === 'string' ? value : ''} onChange={(e) => setFieldValue(field.key, e.target.value)} > - + {field.options.map((option) => (