/** * Content Plan Generation Modal Component */ import React, { useState } from 'react'; import { __ } from '@wordpress/i18n'; import Select from 'react-select'; import NumericInput from 'react-numeric-input'; import { useContentPlan } from '../context/ContentPlanContext'; import { ContentPlanGenerationFormData } from '../types'; import ErrorMessage from './ErrorMessage'; import OptInPrompt from './OptInPrompt'; import { LANGUAGE_OPTIONS } from '../utils/languageOptions'; interface ContentPlanGenerationModalProps { isOpen: boolean; onClose: () => void; } const ContentPlanGenerationModal: React.FC = ({ isOpen, onClose, }) => { const { getString, saving, generateContentPlan, createMultipleContentPlanItems, contentPlan, settings, setCurrentTaskId, closeGenerationModal, fetchTasks, opted, optinUrl, } = useContentPlan(); const [formData, setFormData] = useState({ goals: ['traffic'], publishInterval: 7, // Default: Publish every 7 days (not shown in UI) totalPosts: 10, // Total number of posts to generate language: 'English', customLanguage: '', customRecommendations: '', }); const [errors, setErrors] = useState<{ [key: string]: string }>({}); const [showAdvanced, setShowAdvanced] = useState(false); const [latestScheduledDate, setLatestScheduledDate] = useState< string | null >(null); const [isGenerating, setIsGenerating] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [is401Error, setIs401Error] = useState(false); // Available goals const goalOptions = [ { value: 'traffic', label: getString('generation_modal', 'goal_traffic', 'Traffic'), }, { value: 'leads', label: getString('generation_modal', 'goal_leads', 'Leads'), }, { value: 'authority', label: getString('generation_modal', 'goal_authority', 'Authority'), }, ]; const handleGoalChange = (goalValue: string, checked: boolean) => { setFormData(prev => ({ ...prev, goals: checked ? [...prev.goals, goalValue] : prev.goals.filter(g => g !== goalValue), })); }; const handleInputChange = ( field: keyof ContentPlanGenerationFormData, value: any ) => { setFormData(prev => ({ ...prev, [field]: value, })); // Clear error for this field if (errors[field]) { setErrors(prev => ({ ...prev, [field]: '', })); } }; const validateForm = (): boolean => { const newErrors: { [key: string]: string } = {}; if (formData.goals.length === 0) { newErrors.goals = getString( 'generation_modal', 'error_goals_required', 'Please select at least one goal' ); } if (formData.totalPosts < 1 || formData.totalPosts > 100) { newErrors.totalPosts = getString( 'generation_modal', 'error_total_posts', 'Total posts must be between 1 and 100' ); } if (!formData.language) { newErrors.language = getString( 'generation_modal', 'error_language_required', 'Please select a language' ); } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (validateForm()) { setIsGenerating(true); setErrorMessage(''); // Clear any previous error try { // Call the context function which will handle the API call and return generated items const result = await generateContentPlan(formData); const generatedItems = result.items; const latestDateFromBackend = result.latestScheduledDate; const taskId = result.taskId; setLatestScheduledDate(latestDateFromBackend || null); // If taskId is present, set it in context (tasks panel will show in main view) if (taskId) { setCurrentTaskId(taskId); // Fetch tasks immediately to get the latest status // Don't await - let it run in background so modal can close quickly fetchTasks({ limit: 20 }).catch(err => { console.error('Error fetching tasks:', err); }); // Also fetch again after a delay to catch quick failures (e.g., moderation blocks) // This ensures we get the error message even if the task fails very quickly setTimeout(() => { fetchTasks({ limit: 20 }).catch(err => { console.error( 'Error fetching tasks (delayed):', err ); }); }, 1500); } else if (generatedItems && generatedItems.length > 0) { // Automatically add all generated items to the plan await createMultipleContentPlanItems( generatedItems, formData.publishInterval, 1, // planId latestDateFromBackend ); } // Success - close the modal after generation handleClose(); } catch (error: any) { console.error('Content generation failed:', error); // Extract error message from error object first // WordPress apiFetch may throw errors with response data nested in error.data let errorMsg = 'Unknown error'; if (error && typeof error === 'object') { // Check if error has response data (WordPress apiFetch structure) const responseData = error.data || error; // Check for error field in response if (responseData.error) { errorMsg = responseData.error; } // Check if error has a message property else if (error.message) { errorMsg = error.message; } // Check if error has validation_errors else if ( responseData.validation_errors && typeof responseData.validation_errors === 'object' ) { const firstError = Object.values( responseData.validation_errors )[0]; errorMsg = typeof firstError === 'string' ? firstError : 'Validation error'; } // Check for message in response data else if (responseData.message) { errorMsg = responseData.message; } } else if (typeof error === 'string') { errorMsg = error; } // Check if this is a 401 error (unauthorized - user not opted in) // Check status code, error message content, or opted status const statusCode = error?.data?.status || error?.status || error?.statusCode; const is401Status = statusCode === 401 || statusCode === '401'; // Check for secret key error messages (indicates user not opted in) const secretKeyErrorPatterns = [ 'Secret key not available', 'secret key', 'not opted in', 'not activated', ]; const isSecretKeyError = secretKeyErrorPatterns.some(pattern => errorMsg.toLowerCase().includes(pattern.toLowerCase()) ); // Also check if user is not opted in (from context) const isNotOptedIn = opted === false; // Show opt-in UI if it's a 401, secret key error, or user is not opted in if (is401Status || isSecretKeyError || isNotOptedIn) { setIs401Error(true); setErrorMessage(''); return; } setIs401Error(false); // Check if this is a connection error and replace with friendly message const connectionErrorPatterns = [ 'cURL error 7', 'Failed to connect', "Couldn't connect to server", 'Connection refused', 'Connection timeout', 'Network error', ]; const isConnectionError = connectionErrorPatterns.some( pattern => errorMsg.toLowerCase().includes(pattern.toLowerCase()) ); if (isConnectionError) { errorMsg = getString( 'generation_modal', 'connection_error', "🚀 Houston, we have a connection problem.\n\nWe can't reach our backend right now.\n\nPlease try again in a moment or contact support if this keeps happening." ); } // Show error in dialog - don't close modal setErrorMessage(errorMsg); } finally { setIsGenerating(false); } } }; const handleClose = () => { setFormData({ goals: ['traffic'], publishInterval: 7, // Default value (not shown in UI) totalPosts: 12, language: 'English', customLanguage: '', customRecommendations: '', }); setErrors({}); setShowAdvanced(false); setLatestScheduledDate(null); setIsGenerating(false); setErrorMessage(''); setIs401Error(false); closeGenerationModal(); }; if (!isOpen) return null; return (
{/* Backdrop */}
{/* Modal */}
{/* Header */}

{getString( 'generation_modal', 'title', 'Generate Content Plan' )}

{getString( 'generation_modal', 'subtitle', 'Configure your content plan generation settings' )}

{/* Content */}
{/* Parameters Form */}
{/* Top Section - Form Controls */}
{/* Goals */}
{goalOptions.map(goal => ( ))}
{errors.goals && (

{errors.goals}

)}
{/* Total Posts */}
handleInputChange( 'totalPosts', value ?? 1 ) } min={1} max={100} step={1} precision={0} mobile={false} />
{errors.totalPosts && (

{errors.totalPosts}

)}
{/* Language */}