/** * Automation Dashboard Page * Main page for managing AI automation pipeline */ import React, { useState, useEffect } from 'react'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { useSiteStore } from '../../store/siteStore'; import { automationService, AutomationRun, AutomationConfig, PipelineStage } from '../../services/automationService'; import { fetchKeywords, fetchClusters, fetchContentIdeas, fetchTasks, fetchContent, fetchContentImages, } from '../../services/api'; import ActivityLog from '../../components/Automation/ActivityLog'; import ConfigModal from '../../components/Automation/ConfigModal'; import RunHistory from '../../components/Automation/RunHistory'; import CurrentProcessingCard from '../../components/Automation/CurrentProcessingCard'; import PageMeta from '../../components/common/PageMeta'; import ComponentCard from '../../components/common/ComponentCard'; import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard'; import DebugSiteSelector from '../../components/common/DebugSiteSelector'; import Button from '../../components/ui/button/Button'; import { BoltIcon, ListIcon, GroupIcon, FileTextIcon, PencilIcon, FileIcon, CheckCircleIcon, ClockIcon, PaperPlaneIcon, ArrowRightIcon } from '../../icons'; const STAGE_CONFIG = [ { icon: ListIcon, color: 'from-blue-500 to-blue-600', textColor: 'text-blue-600', hoverColor: 'hover:border-blue-500', name: 'Keywords → Clusters' }, { icon: GroupIcon, color: 'from-purple-500 to-purple-600', textColor: 'text-purple-600', hoverColor: 'hover:border-purple-500', name: 'Clusters → Ideas' }, { icon: CheckCircleIcon, color: 'from-indigo-500 to-indigo-600', textColor: 'text-indigo-600', hoverColor: 'hover:border-indigo-500', name: 'Ideas → Tasks' }, { icon: PencilIcon, color: 'from-green-500 to-green-600', textColor: 'text-green-600', hoverColor: 'hover:border-green-500', name: 'Tasks → Content' }, { icon: FileIcon, color: 'from-amber-500 to-amber-600', textColor: 'text-amber-600', hoverColor: 'hover:border-amber-500', name: 'Content → Image Prompts' }, { icon: FileTextIcon, color: 'from-pink-500 to-pink-600', textColor: 'text-pink-600', hoverColor: 'hover:border-pink-500', name: 'Image Prompts → Images' }, { icon: PaperPlaneIcon, color: 'from-teal-500 to-teal-600', textColor: 'text-teal-600', hoverColor: 'hover:border-teal-500', name: 'Manual Review Gate' }, ]; const AutomationPage: React.FC = () => { const { activeSite } = useSiteStore(); const toast = useToast(); const [config, setConfig] = useState(null); const [currentRun, setCurrentRun] = useState(null); const [pipelineOverview, setPipelineOverview] = useState([]); const [metrics, setMetrics] = useState(null); const [showConfigModal, setShowConfigModal] = useState(false); const [loading, setLoading] = useState(true); const [estimate, setEstimate] = useState<{ estimated_credits: number; current_balance: number; sufficient: boolean } | null>(null); useEffect(() => { if (!activeSite) return; loadData(); const interval = setInterval(() => { if (currentRun && (currentRun.status === 'running' || currentRun.status === 'paused')) { loadCurrentRun(); } else { loadPipelineOverview(); } }, 5000); return () => clearInterval(interval); }, [activeSite, currentRun?.status]); const loadData = async () => { if (!activeSite) return; try { setLoading(true); const [configData, runData, estimateData, pipelineData] = await Promise.all([ automationService.getConfig(activeSite.id), automationService.getCurrentRun(activeSite.id), automationService.estimate(activeSite.id), automationService.getPipelineOverview(activeSite.id), ]); // Also fetch the same low-level metrics used by Home page so we can show authoritative totals try { const siteId = activeSite.id; const [ keywordsTotalRes, keywordsNewRes, keywordsMappedRes, clustersTotalRes, clustersNewRes, clustersMappedRes, ideasTotalRes, ideasNewRes, ideasQueuedRes, ideasCompletedRes, tasksTotalRes, contentTotalRes, contentDraftRes, contentReviewRes, contentPublishedRes, imagesTotalRes, imagesPendingRes, ] = await Promise.all([ fetchKeywords({ page_size: 1, site_id: siteId }), fetchKeywords({ page_size: 1, site_id: siteId, status: 'new' }), fetchKeywords({ page_size: 1, site_id: siteId, status: 'mapped' }), fetchClusters({ page_size: 1, site_id: siteId }), fetchClusters({ page_size: 1, site_id: siteId, status: 'new' }), fetchClusters({ page_size: 1, site_id: siteId, status: 'mapped' }), fetchContentIdeas({ page_size: 1, site_id: siteId }), fetchContentIdeas({ page_size: 1, site_id: siteId, status: 'new' }), fetchContentIdeas({ page_size: 1, site_id: siteId, status: 'queued' }), fetchContentIdeas({ page_size: 1, site_id: siteId, status: 'completed' }), fetchTasks({ page_size: 1, site_id: siteId }), fetchContent({ page_size: 1, site_id: siteId }), fetchContent({ page_size: 1, site_id: siteId, status: 'draft' }), fetchContent({ page_size: 1, site_id: siteId, status: 'review' }), fetchContent({ page_size: 1, site_id: siteId, status: 'published' }), fetchContentImages({ page_size: 1, site_id: siteId }), fetchContentImages({ page_size: 1, site_id: siteId, status: 'pending' }), ]); setMetrics({ keywords: { total: keywordsTotalRes.count || 0, new: keywordsNewRes.count || 0, mapped: keywordsMappedRes.count || 0 }, clusters: { total: clustersTotalRes.count || 0, new: clustersNewRes.count || 0, mapped: clustersMappedRes.count || 0 }, ideas: { total: ideasTotalRes.count || 0, new: ideasNewRes.count || 0, queued: ideasQueuedRes.count || 0, completed: ideasCompletedRes.count || 0 }, tasks: { total: tasksTotalRes.count || 0 }, content: { total: contentTotalRes.count || 0, draft: contentDraftRes.count || 0, review: contentReviewRes.count || 0, published: contentPublishedRes.count || 0, }, images: { total: imagesTotalRes.count || 0, pending: imagesPendingRes.count || 0 }, }); } catch (e) { // Non-fatal: keep metrics null if any of the low-level calls fail console.warn('Failed to fetch low-level metrics for automation page', e); } setConfig(configData); setCurrentRun(runData.run); setEstimate(estimateData); setPipelineOverview(pipelineData.stages); } catch (error: any) { toast.error('Failed to load automation data'); console.error(error); } finally { setLoading(false); } }; const loadCurrentRun = async () => { if (!activeSite) return; try { const data = await automationService.getCurrentRun(activeSite.id); setCurrentRun(data.run); } catch (error) { console.error('Failed to poll current run', error); } }; const loadPipelineOverview = async () => { if (!activeSite) return; try { const data = await automationService.getPipelineOverview(activeSite.id); setPipelineOverview(data.stages); } catch (error) { console.error('Failed to poll pipeline overview', error); } }; const handleRunNow = async () => { if (!activeSite) return; if (estimate && !estimate.sufficient) { toast.error(`Insufficient credits. Need ~${estimate.estimated_credits}, you have ${estimate.current_balance}`); return; } try { const result = await automationService.runNow(activeSite.id); toast.success('Automation started successfully'); loadCurrentRun(); } catch (error: any) { toast.error(error.response?.data?.error || 'Failed to start automation'); } }; const handlePause = async () => { if (!currentRun) return; try { await automationService.pause(currentRun.run_id); toast.success('Automation paused'); loadCurrentRun(); } catch (error) { toast.error('Failed to pause automation'); } }; const handleResume = async () => { if (!currentRun) return; try { await automationService.resume(currentRun.run_id); toast.success('Automation resumed'); loadCurrentRun(); } catch (error) { toast.error('Failed to resume automation'); } }; const handleSaveConfig = async (newConfig: Partial) => { if (!activeSite) return; try { await automationService.updateConfig(activeSite.id, newConfig); toast.success('Configuration saved'); setShowConfigModal(false); loadData(); } catch (error) { toast.error('Failed to save configuration'); } }; const handlePublishAllWithoutReview = async () => { if (!activeSite) return; if (!confirm('Publish all content without review? This cannot be undone.')) return; try { await automationService.publishWithoutReview(activeSite.id); toast.success('Publish job started (without review)'); // refresh metrics and pipeline loadData(); loadPipelineOverview(); } catch (error: any) { console.error('Failed to publish without review', error); toast.error(error?.response?.data?.error || 'Failed to publish without review'); } }; if (loading) { return (
Loading automation...
); } if (!activeSite) { return (
Please select a site to view automation
); } const totalPending = pipelineOverview.reduce((sum, stage) => sum + stage.pending, 0); const getStageResult = (stageNumber: number) => { if (!currentRun) return null; return (currentRun as any)[`stage_${stageNumber}_result`]; }; const renderMetricRow = (items: Array<{ label: string; value: any; colorCls?: string }>) => { const visible = items.filter(i => i && (i.value !== undefined && i.value !== null)); if (visible.length === 0) { return (
); } if (visible.length === 2) { return (
{visible.map((it, idx) => (
{it.label}
{Number(it.value) || 0}
))}
); } // default to 3 columns (equally spaced) return (
{items.concat(Array(Math.max(0, 3 - items.length)).fill({ label: '', value: '' })).slice(0,3).map((it, idx) => (
{it.label}
{it.value !== undefined && it.value !== null ? Number(it.value) : ''}
))}
); }; return ( <>
{/* Header */}

AI Automation Pipeline

{activeSite && (

Site: {activeSite.name}

)}
{/* Compact Ready-to-Run card (header) - absolutely centered in header */}
0 ? 'border-success-500 bg-success-50' : 'border-slate-300 bg-slate-50'}`}>
0 ? 'bg-gradient-to-br from-success-500 to-success-600' : 'bg-gradient-to-br from-slate-400 to-slate-500'}`}> {!currentRun && totalPending > 0 ? : currentRun?.status === 'running' ? : currentRun?.status === 'paused' ? : }
{currentRun?.status === 'running' && `Running - Stage ${currentRun.current_stage}/7`} {currentRun?.status === 'paused' && 'Paused'} {!currentRun && totalPending > 0 && 'Ready to Run'} {!currentRun && totalPending === 0 && 'No Items Pending'}
{currentRun ? `Started: ${new Date(currentRun.started_at).toLocaleTimeString()}` : (totalPending > 0 ? `${totalPending} items in pipeline` : 'All stages clear')}
{/* Compact Schedule & Controls Panel */} {config && (
{config.is_enabled ? (
Enabled
) : (
Disabled
)}
{config.frequency} at {config.scheduled_time}
Last: {config.last_run_at ? new Date(config.last_run_at).toLocaleDateString() : 'Never'}
Est:{' '} {estimate?.estimated_credits || 0} credits {estimate && !estimate.sufficient && ( (Low) )}
{currentRun?.status === 'running' && ( )} {currentRun?.status === 'paused' && ( )} {!currentRun && ( )}
)} {/* Metrics Summary Cards */}
{/* Keywords */}
Keywords
{(() => { const res = getStageResult(1); const total = res?.total ?? pipelineOverview[0]?.counts?.total ?? metrics?.keywords?.total ?? pipelineOverview[0]?.pending ?? 0; return (
{total}
); })()}
{(() => { const res = getStageResult(1); const newCount = res?.new ?? res?.new_items ?? pipelineOverview[0]?.counts?.new ?? metrics?.keywords?.new ?? 0; const mapped = res?.mapped ?? pipelineOverview[0]?.counts?.mapped ?? metrics?.keywords?.mapped ?? 0; return ( renderMetricRow([ { label: 'New:', value: newCount, colorCls: 'text-blue-700' }, { label: 'Mapped:', value: mapped, colorCls: 'text-blue-700' }, ]) ); })()}
{/* Clusters */}
Clusters
{(() => { const res = getStageResult(2); const total = res?.total ?? pipelineOverview[1]?.counts?.total ?? metrics?.clusters?.total ?? pipelineOverview[1]?.pending ?? 0; return (
{total}
); })()}
{(() => { const res = getStageResult(2); const newCount = res?.new ?? res?.new_items ?? pipelineOverview[1]?.counts?.new ?? metrics?.clusters?.new ?? 0; const mapped = res?.mapped ?? pipelineOverview[1]?.counts?.mapped ?? metrics?.clusters?.mapped ?? 0; return ( renderMetricRow([ { label: 'New:', value: newCount, colorCls: 'text-purple-700' }, { label: 'Mapped:', value: mapped, colorCls: 'text-purple-700' }, ]) ); })()}
{/* Ideas */}
Ideas
{(() => { const res = getStageResult(3); const total = res?.total ?? pipelineOverview[2]?.counts?.total ?? metrics?.ideas?.total ?? pipelineOverview[2]?.pending ?? 0; return (
{total}
); })()}
{(() => { const res = getStageResult(3); const newCount = res?.new ?? res?.new_items ?? pipelineOverview[2]?.counts?.new ?? metrics?.ideas?.new ?? 0; const queued = res?.queued ?? pipelineOverview[2]?.counts?.queued ?? metrics?.ideas?.queued ?? 0; const completed = res?.completed ?? pipelineOverview[2]?.counts?.completed ?? metrics?.ideas?.completed ?? 0; return ( renderMetricRow([ { label: 'New:', value: newCount, colorCls: 'text-indigo-700' }, { label: 'Queued:', value: queued, colorCls: 'text-indigo-700' }, { label: 'Completed:', value: completed, colorCls: 'text-indigo-700' }, ]) ); })()}
{/* Content */}
Content
{(() => { const res = getStageResult(4); const total = res?.total ?? pipelineOverview[3]?.counts?.total ?? metrics?.content?.total ?? pipelineOverview[3]?.pending ?? 0; return (
{total}
); })()}
{(() => { const res = getStageResult(4); const draft = res?.draft ?? res?.drafts ?? pipelineOverview[3]?.counts?.draft ?? metrics?.content?.draft ?? 0; const review = res?.review ?? res?.in_review ?? pipelineOverview[3]?.counts?.review ?? metrics?.content?.review ?? 0; const publish = res?.published ?? res?.publish ?? pipelineOverview[3]?.counts?.published ?? metrics?.content?.published ?? 0; return ( renderMetricRow([ { label: 'Draft:', value: draft, colorCls: 'text-green-700' }, { label: 'Review:', value: review, colorCls: 'text-green-700' }, { label: 'Publish:', value: publish, colorCls: 'text-green-700' }, ]) ); })()}
{/* Images */}
Images
{(() => { const res = getStageResult(6); const total = res?.total ?? pipelineOverview[5]?.counts?.total ?? metrics?.images?.total ?? pipelineOverview[5]?.pending ?? 0; return (
{total}
); })()}
{(() => { const res = getStageResult(6); // stage 6 is Image Prompts -> Images if (res && typeof res === 'object') { const entries = Object.entries(res); const items = entries.slice(0,3).map(([k, v]) => ({ label: `${k.replace(/_/g, ' ')}:`, value: Number(v) || 0, colorCls: 'text-pink-700' })); return renderMetricRow(items); } const counts = pipelineOverview[5]?.counts ?? metrics?.images ?? null; if (counts && typeof counts === 'object') { const entries = Object.entries(counts); const items = entries.slice(0,3).map(([k, v]) => ({ label: `${k.replace(/_/g, ' ')}:`, value: Number(v) || 0, colorCls: 'text-pink-700' })); return renderMetricRow(items); } return renderMetricRow([ { label: 'Pending:', value: pipelineOverview[5]?.pending ?? metrics?.images?.pending ?? 0, colorCls: 'text-pink-700' }, ]); })()}
{/* Current Processing Card - Shows real-time automation progress */} {currentRun && (currentRun.status === 'running' || currentRun.status === 'paused') && activeSite && ( { // Refresh current run status loadCurrentRun(); }} onClose={() => { // Card will remain in DOM but user acknowledged it // Can add state here to minimize it if needed }} /> )} {/* Pipeline Stages */} {/* Row 1: Stages 1-4 */}
{pipelineOverview.slice(0, 4).map((stage, index) => { const stageConfig = STAGE_CONFIG[index]; const StageIcon = stageConfig.icon; const isActive = currentRun?.current_stage === stage.number; const isComplete = currentRun && currentRun.current_stage > stage.number; const result = currentRun ? (currentRun[`stage_${stage.number}_result` as keyof AutomationRun] as any) : null; const processed = result ? Object.values(result).reduce((sum: number, val) => typeof val === 'number' ? sum + val : sum, 0) : 0; const progressPercent = stage.pending > 0 ? Math.round((processed / (processed + stage.pending)) * 100) : 0; return (
0 ? `border-slate-200 bg-white dark:bg-white/[0.03] dark:border-gray-800 ${stageConfig.hoverColor} hover:shadow-lg` : 'border-slate-200 bg-slate-50 dark:bg-white/[0.02] dark:border-gray-800' } `} > {/* Compact Header */}
Stage {stage.number}
{isActive && ● Active} {isComplete && } {!isActive && !isComplete && stage.pending > 0 && Ready}
{stageConfig.name}
{/* Queue Metrics */}
Total Queue: {stage.pending}
Processed: {processed}
Remaining: {stage.pending}
{/* Credits and Time - Section 6 Enhancement */} {result && result.credits_used !== undefined && (
Credits Used: {result.credits_used}
)} {result && result.time_elapsed && (
Duration: {result.time_elapsed}
)}
{/* Progress Bar */} {(isActive || isComplete || processed > 0) && (
Progress {isComplete ? '100' : progressPercent}%
)}
); })}
{/* Row 2: Stages 5-7 + Status Summary */}
{/* Stages 5-6 */} {pipelineOverview.slice(4, 6).map((stage, index) => { const actualIndex = index + 4; const stageConfig = STAGE_CONFIG[actualIndex]; const StageIcon = stageConfig.icon; const isActive = currentRun?.current_stage === stage.number; const isComplete = currentRun && currentRun.current_stage > stage.number; const result = currentRun ? (currentRun[`stage_${stage.number}_result` as keyof AutomationRun] as any) : null; const processed = result ? Object.values(result).reduce((sum: number, val) => typeof val === 'number' ? sum + val : sum, 0) : 0; const progressPercent = stage.pending > 0 ? Math.round((processed / (processed + stage.pending)) * 100) : 0; return (
0 ? `border-slate-200 bg-white dark:bg-white/[0.03] dark:border-gray-800 ${stageConfig.hoverColor} hover:shadow-lg` : 'border-slate-200 bg-slate-50 dark:bg-white/[0.02] dark:border-gray-800' } `} >
Stage {stage.number}
{isActive && ● Active} {isComplete && } {!isActive && !isComplete && stage.pending > 0 && Ready}
{stageConfig.name}
Total Queue: {stage.pending}
Processed: {processed}
Remaining: {stage.pending}
{/* Credits and Time - Section 6 Enhancement */} {result && result.credits_used !== undefined && (
Credits Used: {result.credits_used}
)} {result && result.time_elapsed && (
Duration: {result.time_elapsed}
)}
{(isActive || isComplete || processed > 0) && (
Progress {isComplete ? '100' : progressPercent}%
)}
); })} {/* Stage 7 - Manual Review Gate */} {pipelineOverview[6] && (() => { const stage7 = pipelineOverview[6]; const isActive = currentRun?.current_stage === 7; const isComplete = currentRun && currentRun.current_stage > 7; return (
0 ? 'border-amber-300 bg-amber-50 dark:bg-amber-900/20 dark:border-amber-700' : 'border-slate-200 bg-slate-50 dark:bg-white/[0.02] dark:border-gray-800' } `} >
Stage 7
🚫 Stop
Manual Review Gate
{stage7.pending > 0 && (
{stage7.pending}
ready for review
)}
); })()} {/* Published summary card (placed after Stage 7 in the same row) */}
Published
 
{metrics?.content?.published ?? pipelineOverview[3]?.counts?.published ?? getStageResult(4)?.published ?? 0}
{/* Status Summary Card */} {currentRun && (
Current Status
Run Summary
Run ID: {currentRun.run_id.split('_').pop()}
Started: {new Date(currentRun.started_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
Current Stage: {currentRun.current_stage}/7
Credits Used: {currentRun.total_credits_used}
Completion: {Math.round((currentRun.current_stage / 7) * 100)}%
{currentRun.status === 'running' &&
} {currentRun.status === 'paused' && } {currentRun.status === 'completed' && }
{currentRun.status}
)}
{/* Current Run Details */} {currentRun && (
: currentRun.status === 'paused' ? : currentRun.status === 'completed' ? : } accentColor={ currentRun.status === 'running' ? 'blue' : currentRun.status === 'paused' ? 'orange' : currentRun.status === 'completed' ? 'success' : 'red' } /> } accentColor="blue" /> } accentColor="blue" /> } accentColor="green" />
)} {/* Activity Log */} {currentRun && } {/* Run History */} {/* Config Modal */} {showConfigModal && config && ( setShowConfigModal(false)} /> )}
); }; export default AutomationPage;