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 <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
06e2442aaf
commit
32f0dbab63
@@ -1,9 +1,3 @@
|
||||
.modal {
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<h3 id="product-seo-title" className={`${styles.title} ${seoStyles.titleRow}`}>
|
||||
<Search size={18} aria-hidden="true" />
|
||||
<span>{t('products.seo.title')}</span>
|
||||
<h3 id="product-seo-title" className={styles.title}>
|
||||
{t('products.seo.title')}
|
||||
</h3>
|
||||
{productName ? <p className={styles.subtitle}>{productName}</p> : null}
|
||||
</div>
|
||||
|
||||
@@ -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<CategoryTechnicalForm | null>(null)
|
||||
const [values, setValues] = useState<ProductTechnicalFormValues>({})
|
||||
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)}
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
<option value="">{t('products.technical.selectPlaceholder')}</option>
|
||||
{field.options.map((option) => (
|
||||
<option key={option.id} value={option.value}>
|
||||
{option.label}
|
||||
@@ -211,68 +236,106 @@ export function ProductTechnicalInfoModal({
|
||||
)
|
||||
}
|
||||
|
||||
const canUseAi = Boolean(form && form.fields.length > 0)
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`${styles.overlay} ${closing ? styles.overlayOut : styles.overlayIn}`}
|
||||
onClick={onClose}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className={`${styles.modal} ${fieldStyles.modalWide} ${closing ? styles.modalOut : styles.modalIn}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="product-technical-title"
|
||||
className={`${styles.overlay} ${closing ? styles.overlayOut : styles.overlayIn}`}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<h3 id="product-technical-title" className={styles.title}>
|
||||
Technical Data
|
||||
</h3>
|
||||
{productName && <p className={styles.subtitle}>{productName}</p>}
|
||||
</div>
|
||||
<button className={styles.closeBtn} onClick={onClose} aria-label="Close">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.body}>
|
||||
{isLoading ? (
|
||||
<p className={styles.emptyText}>Loading technical data...</p>
|
||||
) : infoMessage && !form ? (
|
||||
<p className={styles.emptyText}>{infoMessage}</p>
|
||||
) : form && form.fields.length === 0 ? (
|
||||
<p className={styles.emptyText}>This category form has no fields yet.</p>
|
||||
) : (
|
||||
form?.fields.map((field) => (
|
||||
<div key={field.id} className={styles.field}>
|
||||
<label htmlFor={`tech-${field.key}`}>
|
||||
{field.label}
|
||||
{field.isRequired && ' *'}
|
||||
</label>
|
||||
{renderField(field)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{error && <p className={styles.errorText}>{error}</p>}
|
||||
|
||||
<div className={styles.actions}>
|
||||
<button type="button" className={styles.cancelBtn} onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<div
|
||||
className={`${styles.modal} ${fieldStyles.modalWide} ${closing ? styles.modalOut : styles.modalIn}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="product-technical-title"
|
||||
lang={isFa ? 'fa' : 'en'}
|
||||
dir={isFa ? 'rtl' : 'ltr'}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<h3 id="product-technical-title" className={styles.title}>
|
||||
{t('products.technical.title')}
|
||||
</h3>
|
||||
{productName && <p className={styles.subtitle}>{productName}</p>}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.submitBtn}
|
||||
onClick={() => void handleSave()}
|
||||
disabled={!form || !isFormValid() || isSaving || isLoading}
|
||||
className={styles.closeBtn}
|
||||
onClick={onClose}
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
{isSaving ? 'Saving…' : 'Save'}
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.body}>
|
||||
{isLoading ? (
|
||||
<p className={styles.emptyText}>{t('products.technical.loading')}</p>
|
||||
) : infoMessage && !form ? (
|
||||
<p className={styles.emptyText}>{infoMessage}</p>
|
||||
) : form && form.fields.length === 0 ? (
|
||||
<p className={styles.emptyText}>{t('products.technical.emptyFields')}</p>
|
||||
) : (
|
||||
form?.fields.map((field) => (
|
||||
<div key={field.id} className={styles.field}>
|
||||
<label htmlFor={`tech-${field.key}`}>
|
||||
{field.label}
|
||||
{field.isRequired && ' *'}
|
||||
</label>
|
||||
{renderField(field)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{infoMessage && form ? (
|
||||
<p className={styles.emptyText}>{infoMessage}</p>
|
||||
) : null}
|
||||
{error && <p className={styles.errorText}>{error}</p>}
|
||||
|
||||
<div className={fieldStyles.productTechnicalActions}>
|
||||
<div className={fieldStyles.actionsMain}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.submitBtn}
|
||||
onClick={() => void handleSave()}
|
||||
disabled={!form || !isFormValid() || isSaving || isLoading || isAiRunning}
|
||||
>
|
||||
{isSaving ? t('products.technical.saving') : t('products.technical.save')}
|
||||
</button>
|
||||
<button type="button" className={styles.cancelBtn} onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
{canUseAi ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`${aiStyles.aiBtn} ${fieldStyles.actionsAi}`}
|
||||
onClick={() => setIsAiOpen(true)}
|
||||
disabled={isLoading || isSaving || isAiRunning}
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
{isAiRunning
|
||||
? t('products.form.aiGenerating')
|
||||
: t('products.technical.generateAi')}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
,
|
||||
document.body,
|
||||
|
||||
<ProductTechnicalValuesAiPromptModal
|
||||
open={isAiOpen}
|
||||
productName={productName}
|
||||
fields={form?.fields ?? []}
|
||||
isRunning={isAiRunning}
|
||||
onClose={() => {
|
||||
if (!isAiRunning) setIsAiOpen(false)
|
||||
}}
|
||||
onRun={handleAiRun}
|
||||
/>
|
||||
</>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Sparkles, X } from 'lucide-react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useT } from '../i18n/useT'
|
||||
import type { CategoryTechnicalForm } from '../types/technicalForm'
|
||||
import modalStyles from './CategoryModal.module.css'
|
||||
import aiStyles from '../styles/ai.module.css'
|
||||
import styles from './CategoryAiPromptModal.module.css'
|
||||
|
||||
export function buildProductTechnicalValuesAiPrompt(ctx: {
|
||||
productName: string
|
||||
fields: CategoryTechnicalForm['fields']
|
||||
locale: 'fa' | 'en'
|
||||
}) {
|
||||
const productName = ctx.productName || '—'
|
||||
const fieldLines = ctx.fields
|
||||
.map((field) => {
|
||||
const options =
|
||||
field.type === 'select' || field.type === 'multi_select'
|
||||
? field.options.map((option) => option.value).join(' | ')
|
||||
: ''
|
||||
const optionPart = options ? ` — options: ${options}` : ''
|
||||
return `- ${field.key} (${field.label}, ${field.type}${field.isRequired ? ', required' : ''})${optionPart}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
if (ctx.locale === 'fa') {
|
||||
return `برای محصول زیر، مقادیر فرم اطلاعات فنی را پر کن.
|
||||
|
||||
محصول: ${productName}
|
||||
|
||||
فیلدهای فرم:
|
||||
${fieldLines || '—'}
|
||||
|
||||
الزامات:
|
||||
- فقط برای همین فیلدها مقدار بده و از کلیدهای دقیق فیلد استفاده کن.
|
||||
- برای select / multi_select فقط از مقادیر مجاز گزینهها استفاده کن.
|
||||
- متنها را به فارسی و کوتاه و فنی بنویس.
|
||||
- مشخصات غیرواقعی اختراع نکن؛ اگر مطمئن نیستی آن فیلد را خالی بگذار.
|
||||
- در صورت نیاز، مرجع یا لینک صفحه محصول / مدل را در ادامه همین پرامپت اضافه کن.`
|
||||
}
|
||||
|
||||
return `Fill the technical data form values for this product.
|
||||
|
||||
Product: ${productName}
|
||||
|
||||
Form fields:
|
||||
${fieldLines || '—'}
|
||||
|
||||
Requirements:
|
||||
- Fill only these fields using the exact field keys.
|
||||
- For select / multi_select use only allowed option values.
|
||||
- Keep text values short, factual, and in the same language as the field labels (usually Persian).
|
||||
- Do not invent unrealistic specs; leave a field empty if unsure.
|
||||
- Optionally add a product page URL / model reference below this prompt.`
|
||||
}
|
||||
|
||||
interface ProductTechnicalValuesAiPromptModalProps {
|
||||
open: boolean
|
||||
productName: string
|
||||
fields: CategoryTechnicalForm['fields']
|
||||
isRunning: boolean
|
||||
onClose: () => void
|
||||
onRun: (prompt: string) => Promise<void>
|
||||
}
|
||||
|
||||
const ANIMATION_MS = 220
|
||||
|
||||
export function ProductTechnicalValuesAiPromptModal({
|
||||
open,
|
||||
productName,
|
||||
fields,
|
||||
isRunning,
|
||||
onClose,
|
||||
onRun,
|
||||
}: ProductTechnicalValuesAiPromptModalProps) {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const isFa = locale === 'fa'
|
||||
const [mounted, setMounted] = useState(open)
|
||||
const [closing, setClosing] = useState(false)
|
||||
const [prompt, setPrompt] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setMounted(true)
|
||||
setClosing(false)
|
||||
setPrompt(
|
||||
buildProductTechnicalValuesAiPrompt({
|
||||
productName,
|
||||
fields,
|
||||
locale: isFa ? 'fa' : 'en',
|
||||
}),
|
||||
)
|
||||
setError('')
|
||||
} else if (mounted) {
|
||||
setClosing(true)
|
||||
const timer = setTimeout(() => {
|
||||
setMounted(false)
|
||||
setClosing(false)
|
||||
}, ANIMATION_MS)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [open, mounted, productName, fields, isFa])
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted || closing) return
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && !isRunning) onClose()
|
||||
}
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => document.removeEventListener('keydown', onKey)
|
||||
}, [mounted, closing, isRunning, onClose])
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
const trimmed = prompt.trim()
|
||||
if (trimmed.length < 10) {
|
||||
setError(t('products.form.aiPromptTooShort'))
|
||||
return
|
||||
}
|
||||
setError('')
|
||||
try {
|
||||
await onRun(trimmed)
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError(t('products.technical.ai.error'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return null
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`${modalStyles.overlay} ${modalStyles.overlayStacked} ${closing ? modalStyles.overlayOut : modalStyles.overlayIn}`}
|
||||
onClick={(e) => e.target === e.currentTarget && !isRunning && onClose()}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
className={`${modalStyles.modal} ${styles.modal} ${closing ? modalStyles.modalOut : modalStyles.modalIn}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="product-technical-ai-prompt-title"
|
||||
lang={isFa ? 'fa' : 'en'}
|
||||
dir={isFa ? 'rtl' : 'ltr'}
|
||||
>
|
||||
<div className={`${modalStyles.header} ${styles.header}`}>
|
||||
<div className={styles.headerBlock}>
|
||||
<h2 id="product-technical-ai-prompt-title" className={modalStyles.title}>
|
||||
{t('products.technical.ai.title')}
|
||||
</h2>
|
||||
<p className={styles.subtitle}>{t('products.technical.ai.subtitle')}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={modalStyles.closeBtn}
|
||||
onClick={onClose}
|
||||
disabled={isRunning}
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form className={modalStyles.form} onSubmit={(e) => void handleSubmit(e)}>
|
||||
<div className={`${modalStyles.field} ${aiStyles.aiPromptField}`}>
|
||||
<label htmlFor="product-technical-ai-prompt">{t('products.form.aiPrompt')}</label>
|
||||
<textarea
|
||||
id="product-technical-ai-prompt"
|
||||
className={aiStyles.aiPromptTextarea}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
rows={14}
|
||||
disabled={isRunning}
|
||||
required
|
||||
lang={isFa ? 'fa' : 'en'}
|
||||
dir={isFa ? 'rtl' : 'ltr'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error ? <p className={styles.error}>{error}</p> : null}
|
||||
|
||||
<div className={modalStyles.actions}>
|
||||
<button
|
||||
type="button"
|
||||
className={modalStyles.cancelBtn}
|
||||
onClick={onClose}
|
||||
disabled={isRunning}
|
||||
>
|
||||
{t('products.form.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className={aiStyles.aiBtn}
|
||||
disabled={isRunning || prompt.trim().length < 10}
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
{isRunning ? t('products.form.aiGenerating') : t('products.form.aiRun')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
@@ -176,3 +176,15 @@
|
||||
white-space: nowrap;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.productTechnicalActions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding-top: 16px;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.2);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@@ -642,6 +642,22 @@ const en = {
|
||||
'products.form.aiTranslateName': 'Translate Persian name to English',
|
||||
'products.form.aiTranslateNameNeedFa': 'Enter the Persian product name first.',
|
||||
'products.form.aiNameEnFilled': 'English name filled with AI.',
|
||||
'products.technical.title': 'Technical Data',
|
||||
'products.technical.loading': 'Loading technical data...',
|
||||
'products.technical.needCategory': 'Assign a category to this product before adding technical data.',
|
||||
'products.technical.noForm': 'No technical form is defined for this product category.',
|
||||
'products.technical.emptyFields': 'This category form has no fields yet.',
|
||||
'products.technical.loadError': 'Unable to load technical data.',
|
||||
'products.technical.saveError': 'Unable to save technical data.',
|
||||
'products.technical.save': 'Save',
|
||||
'products.technical.saving': 'Saving…',
|
||||
'products.technical.selectPlaceholder': 'Select...',
|
||||
'products.technical.generateAi': 'Fill with AI',
|
||||
'products.technical.ai.title': 'Fill Technical Data with AI',
|
||||
'products.technical.ai.subtitle':
|
||||
'Edit the prefilled prompt if you want to add a reference, then run it to fill the fields.',
|
||||
'products.technical.ai.error': 'Unable to fill technical data with AI.',
|
||||
'products.technical.ai.filled': 'Technical fields filled with AI. Review and save.',
|
||||
'products.form.images': 'Product Images',
|
||||
'products.form.imagesAdd': 'Add images',
|
||||
'products.form.imagesHint': 'Upload multiple product images. Click + to add more.',
|
||||
@@ -3213,6 +3229,22 @@ const fa: Record<MessageKey, string> = {
|
||||
'products.form.aiTranslateName': 'ترجمه نام فارسی به انگلیسی',
|
||||
'products.form.aiTranslateNameNeedFa': 'ابتدا نام فارسی محصول را وارد کنید.',
|
||||
'products.form.aiNameEnFilled': 'نام انگلیسی با هوش مصنوعی پر شد.',
|
||||
'products.technical.title': 'اطلاعات فنی',
|
||||
'products.technical.loading': 'در حال بارگذاری اطلاعات فنی…',
|
||||
'products.technical.needCategory': 'قبل از افزودن اطلاعات فنی، یک دستهبندی به این محصول اختصاص دهید.',
|
||||
'products.technical.noForm': 'برای دستهبندی این محصول فرم فنی تعریف نشده است.',
|
||||
'products.technical.emptyFields': 'فرم این دستهبندی هنوز فیلدی ندارد.',
|
||||
'products.technical.loadError': 'بارگذاری اطلاعات فنی ممکن نشد.',
|
||||
'products.technical.saveError': 'ذخیره اطلاعات فنی ممکن نشد.',
|
||||
'products.technical.save': 'ذخیره',
|
||||
'products.technical.saving': 'در حال ذخیره…',
|
||||
'products.technical.selectPlaceholder': 'انتخاب…',
|
||||
'products.technical.generateAi': 'پر کردن با هوش مصنوعی',
|
||||
'products.technical.ai.title': 'پر کردن اطلاعات فنی با هوش مصنوعی',
|
||||
'products.technical.ai.subtitle':
|
||||
'در صورت نیاز پرامپت پیشفرض را ویرایش کنید یا مرجع اضافه کنید، سپس اجرا کنید تا فیلدها پر شوند.',
|
||||
'products.technical.ai.error': 'پر کردن اطلاعات فنی با هوش مصنوعی ممکن نشد.',
|
||||
'products.technical.ai.filled': 'فیلدهای فنی با هوش مصنوعی پر شد. بررسی کنید و ذخیره کنید.',
|
||||
'products.form.images': 'تصاویر محصول',
|
||||
'products.form.imagesAdd': 'افزودن تصاویر',
|
||||
'products.form.imagesHint': 'چند تصویر محصول آپلود کنید. برای افزودن بیشتر روی + کلیک کنید.',
|
||||
|
||||
@@ -68,3 +68,17 @@ export async function saveProductTechnicalInfo(
|
||||
body: formStateToPayload(values),
|
||||
})
|
||||
}
|
||||
|
||||
export async function fillProductTechnicalInfoByAi(
|
||||
productId: string,
|
||||
prompt: string,
|
||||
) {
|
||||
return apiRequest<{ values: ProductTechnicalFormValues }>(
|
||||
`${businessPath(productId)}/ai-fill`,
|
||||
{
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: { prompt },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user