/** * Generate Post Modal Component * * Modal for generating content for a specific planned post */ import React, { useState } from 'react'; import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import { useContentPlan } from '../context/ContentPlanContext'; import { ContentPlanItem } from '../types'; import StructureDisplay from './StructureDisplay'; import KeywordDisplay from './KeywordDisplay'; import OptInPrompt from './OptInPrompt'; interface GeneratePostModalProps { isOpen: boolean; onClose: () => void; item: ContentPlanItem | null; } const GeneratePostModal: React.FC = ({ isOpen, onClose, item, }) => { const { getString, settings, setCurrentTaskId, fetchTasks, addFailedTask, refreshUsage, opted, optinUrl, } = useContentPlan(); // Dev mode detection - check settings from API first, then fallback to URL/localStorage const isDevMode = () => { // First check the settings from API if (settings.dev_mode === true) { return true; } // Fallback to URL parameter or localStorage const urlParams = new URLSearchParams(window.location.search); return ( urlParams.get('dev') === 'true' || localStorage.getItem('cpepai-dev-mode') === 'true' ); }; const [devModePrompt, setDevModePrompt] = useState(''); const [showDevPrompt, setShowDevPrompt] = useState(false); const [isGenerating, setIsGenerating] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [is401Error, setIs401Error] = useState(false); const [regenerationInstructions, setRegenerationInstructions] = useState(''); const handleGenerate = async () => { if (!item) return; setIsGenerating(true); setErrorMessage(''); // Clear any previous error try { // Make API call to generate post const requestBody: any = {}; if (regenerationInstructions.trim()) { requestBody.regeneration_instructions = regenerationInstructions.trim(); } const result: any = await apiFetch({ path: `/cpepai/v1/items/${item.id}/generate-post`, method: 'POST', data: requestBody, }); // Check for error responses - API might return error object even with 200 status // Check for error field, status_code indicating error (4xx, 5xx), or success === false const hasError = result.error || (result.status_code && result.status_code >= 400) || result.success === false; if (hasError) { // Extract error message from various possible locations let errorMsg = result.error || result.message || (result.validation_errors && typeof result.validation_errors === 'object' ? (Object.values(result.validation_errors)[0] as string) : null) || 'Failed to generate post content'; // Check if this is a 401 error (unauthorized - user not opted in) const statusCode = result.status_code; 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(''); setIsGenerating(false); return; } // Show error in dialog - don't close modal or create task setIs401Error(false); setErrorMessage(errorMsg); setIsGenerating(false); return; } // Extract taskId from response (could be at top level or in data) const taskId = result.taskId || result.data?.taskId; // Only proceed if we have a taskId (successful task creation) if (!taskId) { // No taskId means task wasn't created - treat as error const errorMsg = result.message || 'Failed to create generation task'; setErrorMessage(errorMsg); setIsGenerating(false); return; } // Success - close modal and proceed with task tracking onClose(); // Smoothly scroll to top of the page window.scrollTo({ top: 0, behavior: 'smooth' }); setCurrentTaskId(taskId); // Fetch tasks immediately to show the newly created task fetchTasks({ limit: 20 }); // Also fetch again after a short 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 }); }, 1000); // Refresh usage data after generating post refreshUsage().catch(err => { console.error( 'Error refreshing usage after post generation:', err ); }); } catch (error: any) { console.error('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( 'generate_post_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 or create task setErrorMessage(errorMsg); } finally { setIsGenerating(false); } }; const handleClose = () => { setDevModePrompt(''); setShowDevPrompt(false); setIsGenerating(false); setErrorMessage(''); setIs401Error(false); setRegenerationInstructions(''); onClose(); }; if (!isOpen || !item) return null; return (
{/* Backdrop */}
{/* Modal */}
{/* Header */}

{getString( 'generate_post_modal', 'title', 'Generate Post Content' )}

{getString( 'generate_post_modal', 'subtitle', 'Generate AI-powered content for this planned post' )}

{isDevMode() && (
DEV MODE{' '} {settings.dev_mode ? '(Settings)' : '(Fallback)'} DEV
)}
{/* Content */}
{/* Post Title */}
{item.title || getString( 'table_cells', 'untitled', 'Untitled' )}
{/* Post Structure */}
{/* Keywords (optional) */}
{/* Scheduled Date (optional) */} {item.scheduled_on && (
{new Date( item.scheduled_on ).toLocaleDateString()}
)} {/* Regeneration Instructions - Only show if item already has a post (regeneration) */} {item.post && (