import React, { useEffect, useState } from 'react'; import { useAppStateContext } from '../../context/user.data.context'; import { improvePrompt, submitBulkFromContentCheck, } from '../../service/content-generator/content-generator.service'; import type { BulkFromContentCheckRequest, ContentGenerateFields, FieldPrompts, } from '../../service/content-generator/content-generator.interface'; import { CREATIVITY_OPTIONS, FAQ_COUNT_OPTIONS, TONE_OPTIONS, } from '../../lib/catalog-action-options'; /** Which field gets generated / steered; mirrors the bulk field keys. */ type FieldPromptKey = keyof ContentGenerateFields; /** Which bulk operation runs across the audited products. */ type BulkOperation = 'fields' | 'gap' | 'faq'; const OPERATION_OPTIONS: ReadonlyArray<{ value: BulkOperation; label: string; }> = [ { value: 'fields', label: 'Rewrite fields (titles / descriptions / tags / FAQ)', }, { value: 'gap', label: 'Add missing info to descriptions' }, { value: 'faq', label: 'Generate FAQ' }, ]; const OPERATION_HELP: Record = { fields: 'Rewrites the fields you select for every audited product. Use this to refresh weak titles and descriptions or to add tags / FAQ.', gap: 'Keeps each existing description and only adds the facts it is missing (specs, dimensions, materials, compatibility, use-cases). Nothing useful gets thrown away.', faq: 'Writes a shopper-facing FAQ for every product from its existing description, using the same FAQ engine as the Content Generator.', }; // Default directive for the "add missing" operation. The content-gen pipeline // rewrites a field by default, so without this it would replace the whole // description instead of topping it up. Sent as the descriptions prompt when // the merchant does not write their own. const GAP_DIRECTIVE = 'Keep the existing description and only add the information it is missing. Append the missing buyer-relevant facts, specifications, dimensions, materials, compatibility, and use-cases a shopper or AI assistant would need. Do not rewrite or repeat what is already there, and never invent details that are not supported by the product data.'; const FIELD_META: ReadonlyArray<{ key: FieldPromptKey; label: string; placeholder: string; }> = [ { key: 'titles', label: 'Titles', placeholder: 'Optional: how titles should read (length, what to lead with, what to include)...', }, { key: 'descriptions', label: 'Descriptions', placeholder: 'Optional: structure, sections, what to emphasise, length...', }, { key: 'tags', label: 'Tags', placeholder: 'Optional: how many tags, what kind, formatting...', }, { key: 'faq', label: 'FAQ (Frequently Asked Questions)', placeholder: 'Optional: how many questions, what topics to cover...', }, ]; const SELECT_CLASS = 'block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:border-gray-900 focus:outline-none focus:ring-1 focus:ring-gray-900'; const TEXTAREA_CLASS = 'block w-full rounded-md border-gray-300 px-3 py-2 text-sm placeholder:text-gray-400 focus:border-gray-900 focus:outline-none focus:ring-1 focus:ring-gray-900'; /** Props for {@link BulkImproveModal}. */ interface BulkImproveModalProps { open: boolean; onOpenChange: (next: boolean) => void; /** Content-check job whose products the bulk run covers. */ jobId: string; /** Audit's detected language, used by the per-field "Enhance with AI" call. */ language: string; /** When set, scope the run to this single audited SKU. */ sku?: string; /** Display name for the single-SKU scope. */ productName?: string; } /** * "Bulk improve" settings modal for the catalog-quality page. The merchant * picks an operation (rewrite chosen fields, add missing info to descriptions, * or generate FAQ) across every audited product, optionally writing a master * instruction (with "Enhance with AI"), then submits to the bulk endpoint. The * resulting file is emailed, so success here is "job started", not inline * content. * * @param {BulkImproveModalProps} props - Modal config. * @return {JSX.Element | null} The modal, or null while closed. */ const BulkImproveModal = ({ open, onOpenChange, jobId, language, sku, productName, }: BulkImproveModalProps): JSX.Element | null => { const { clientId } = useAppStateContext(); const [operation, setOperation] = useState('fields'); const [fields, setFields] = useState({ titles: true, descriptions: true, tags: false, faq: false, }); const [fieldPrompts, setFieldPrompts] = useState>({}); const [tone, setTone] = useState('professional'); const [customTone, setCustomTone] = useState(''); const [temperature, setTemperature] = useState('0.7'); const [faqInDescription, setFaqInDescription] = useState(true); const [faqCount, setFaqCount] = useState('3'); const [improvingField, setImprovingField] = useState( null ); const [submitting, setSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); const [submitError, setSubmitError] = useState(null); const noFieldsSelected = !fields.titles && !fields.descriptions && !fields.tags && !fields.faq; // Only the field-rewrite operation can be "empty"; gap/faq always have a target. const submitDisabled = submitting || (operation === 'fields' && noFieldsSelected); // Reset the success / error state every time the modal is reopened so a fresh // run starts from the form, not the "job started" confirmation. useEffect(() => { if (open) { setSubmitted(false); setSubmitError(null); } }, [open]); useEffect(() => { if (!open) return undefined; const handleKey = (event: KeyboardEvent): void => { if (event.key === 'Escape' && !submitting) onOpenChange(false); }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, [open, submitting, onOpenChange]); const handleFieldPromptChange = ( field: FieldPromptKey, next: string ): void => { setFieldPrompts(prev => ({ ...prev, [field]: next })); }; const handleImprovePrompt = async (field: FieldPromptKey): Promise => { if (!clientId) return; const current = (fieldPrompts[field] || '').trim(); if (!current) return; setImprovingField(field); try { const response = await improvePrompt( current, field, language || 'en', clientId ); const improved = response?.data?.improved_prompt; if (improved) { setFieldPrompts(prev => ({ ...prev, [field]: improved })); } } catch { /* keep the user's text on failure */ } finally { setImprovingField(null); } }; const buildPayload = (): BulkFromContentCheckRequest => { const base = { tone, custom_tone: tone === 'custom' ? customTone : undefined, temperature: Number(temperature), sku, }; if (operation === 'gap') { return { ...base, generate_fields: { titles: false, descriptions: true, tags: false, faq: false, }, field_prompts: { descriptions: (fieldPrompts.descriptions || '').trim() || GAP_DIRECTIVE, }, }; } if (operation === 'faq') { const custom = (fieldPrompts.faq || '').trim(); return { ...base, generate_fields: { titles: false, descriptions: false, tags: false, faq: true, }, field_prompts: custom ? { faq: custom } : undefined, faq_in_description: faqInDescription, faq_count: Number(faqCount), }; } return { ...base, generate_fields: fields, field_prompts: fieldPrompts, faq_in_description: fields.faq ? faqInDescription : undefined, faq_count: fields.faq ? Number(faqCount) : undefined, }; }; const handleSubmit = async (): Promise => { if (!clientId || submitDisabled) return; setSubmitting(true); setSubmitError(null); try { const response = await submitBulkFromContentCheck( jobId, buildPayload(), clientId ); if (response?.errors || !response?.data?.job_id) { setSubmitError( response?.message ?? 'Could not start the bulk job. A content job may already be running.' ); return; } setSubmitted(true); } catch { setSubmitError('Could not start the bulk job. Try again.'); } finally { setSubmitting(false); } }; if (!open) return null; return (
{ if (!submitting) onOpenChange(false); }} />

{sku ? 'Improve product' : 'Bulk improve catalog'}

{sku ? `Run one operation on ${productName || sku}. The finished file is sent to your email.` : 'Run one operation across every product in this audit. The finished file is sent to your email to batch-upload to your store.'}

{submitted ? (

Bulk job started.

We are generating content for the audited products. The results will arrive in your email as a downloadable file you can batch upload to your store.

) : ( <>

{OPERATION_HELP[operation]}

{tone === 'custom' && (