import React, { useEffect, useRef, useState } from 'react'; import { useAppStateContext } from '../../context/user.data.context'; import { inlineGenerateFromContentCheck } from '../../service/content-generator/content-generator.service'; import type { InlineGenerateMode } from '../../service/content-generator/content-generator.interface'; import { LANGUAGE_OPTIONS } from '../../data/locales'; import { CREATIVITY_OPTIONS, FAQ_COUNT_OPTIONS, } from '../../lib/catalog-action-options'; import InlineResultCard from './InlineResultCard'; const COPY: Record< 'gap' | 'faq', { title: string; description: string; placeholder: string } > = { gap: { title: 'Add missing content', description: 'Generates an improved title plus the content the description is missing. Steer it with an optional instruction, then preview and copy.', placeholder: "Optional: which details matter most (dimensions, materials, what's in the box)...", }, faq: { title: 'Generate FAQ', description: 'Writes the questions shoppers ask, answered from the description. Steer it with an optional instruction, then preview and copy.', placeholder: 'Optional: topics to focus on (fit, care, compatibility)...', }, }; 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 ProductActionModal}. */ interface ProductActionModalProps { open: boolean; onOpenChange: (next: boolean) => void; /** ``gap`` (Add missing) or ``faq``. */ mode: 'gap' | 'faq'; jobId: string; sku: string; productName: string; language: string; /** Audit's title issues, shown under "What's missing" for gap. */ titleIssues: string[]; /** Audit's description issues, shown under "What's missing" for gap. */ descriptionIssues: string[]; } /** * Per-product modal for the Add-missing / FAQ actions. Collects an optional * instruction, language, creativity (and FAQ count), generates synchronously * against the inline endpoint, and renders the preview right in the modal with * Copy buttons. Closes on Esc / backdrop click while idle. * * @param {ProductActionModalProps} props - Modal config. * @return {JSX.Element | null} The modal, or null while closed. */ const ProductActionModal = ({ open, onOpenChange, mode, jobId, sku, productName, language, titleIssues, descriptionIssues, }: ProductActionModalProps): JSX.Element | null => { const { clientId } = useAppStateContext(); const [instruction, setInstruction] = useState(''); const [titleInstruction, setTitleInstruction] = useState(''); const [descriptionInstruction, setDescriptionInstruction] = useState(''); const [lang, setLang] = useState(language); const [temperature, setTemperature] = useState('0.7'); const [faqCount, setFaqCount] = useState('3'); const [generating, setGenerating] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState<{ mode: InlineGenerateMode; content: string; } | null>(null); const resultRef = useRef(null); // Reset the previous result when switching action (gap <-> faq) or product so // the panel below never shows stale content from the other mode. useEffect(() => { setResult(null); setError(null); }, [mode, sku]); // Keep the language picker in sync with the audit's detected language. useEffect(() => { setLang(language); }, [language]); useEffect(() => { if (!open) return undefined; const handleKey = (event: KeyboardEvent): void => { if (event.key === 'Escape' && !generating) onOpenChange(false); }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, [open, generating, onOpenChange]); useEffect(() => { if (result) { resultRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }, [result]); const handleGenerate = async (): Promise => { if (!clientId || generating) return; setGenerating(true); setError(null); setResult(null); try { const trimmedTitle = titleInstruction.trim(); const trimmedDescription = descriptionInstruction.trim(); const trimmedCustom = instruction.trim(); const response = await inlineGenerateFromContentCheck( jobId, sku, { mode, title_prompt: mode === 'gap' && trimmedTitle ? trimmedTitle : undefined, description_prompt: mode === 'gap' && trimmedDescription ? trimmedDescription : undefined, custom_prompt: mode === 'faq' && trimmedCustom ? trimmedCustom : undefined, language: lang || undefined, temperature: Number(temperature), faq_count: mode === 'faq' ? Number(faqCount) : undefined, }, clientId ); if (response?.errors || !response?.data?.content) { setError(response?.message ?? 'Could not generate content.'); return; } setResult({ mode, content: response.data.content }); } catch { setError('Could not generate content. Try again.'); } finally { setGenerating(false); } }; if (!open) return null; const copy = COPY[mode]; return (
{ if (!generating) onOpenChange(false); }} />

{copy.title}

{productName || sku}

{copy.description}

{mode === 'gap' && (titleIssues.length > 0 || descriptionIssues.length > 0) && (

What's missing

{titleIssues.length > 0 && (

Title

    {titleIssues.map((issue, issueIndex) => (
  • · {issue}
  • ))}
)} {descriptionIssues.length > 0 && (

Description

    {descriptionIssues.map((issue, issueIndex) => (
  • · {issue}
  • ))}
)}
)} {mode === 'faq' && (
)} {mode === 'gap' ? (