/** * Current Processing Card Component * Shows real-time automation progress with pause/resume/cancel controls */ import React, { useEffect, useState } from 'react'; import { automationService, ProcessingState, AutomationRun, PipelineStage } from '../../services/automationService'; import { fetchKeywords, fetchClusters, fetchContentIdeas, fetchTasks, fetchContent, fetchContentImages, } from '../../services/api'; import { useToast } from '../ui/toast/ToastContainer'; import Button from '../ui/button/Button'; import { PlayIcon, PauseIcon, XMarkIcon, ClockIcon, BoltIcon } from '../../icons'; interface CurrentProcessingCardProps { runId: string; siteId: number; currentRun: AutomationRun; onUpdate: () => void; onClose: () => void; pipelineOverview?: PipelineStage[]; } const CurrentProcessingCard: React.FC = ({ runId, siteId, currentRun, onUpdate, onClose, pipelineOverview, }) => { const [processingState, setProcessingState] = useState(null); const [error, setError] = useState(null); const [isPausing, setIsPausing] = useState(false); const [isResuming, setIsResuming] = useState(false); const [isCancelling, setIsCancelling] = useState(false); const [fetchedCurrently, setFetchedCurrently] = useState([]); const [fetchedUpNext, setFetchedUpNext] = useState([]); const [isLocallyPaused, setIsLocallyPaused] = useState(false); const [showDebugTable, setShowDebugTable] = useState(false); const toast = useToast(); useEffect(() => { let isMounted = true; const fetchState = async () => { try { const state = await automationService.getCurrentProcessing(siteId, runId); console.debug('getCurrentProcessing response for run', runId, state); if (!isMounted) return; setProcessingState(state); setError(null); // If stage completed (all items processed), trigger page refresh if (state && state.processed_items >= state.total_items && state.total_items > 0) { onUpdate(); } } catch (err) { if (!isMounted) return; console.error('Error fetching processing state:', err); setError('Failed to load processing state'); } }; // Only fetch if status is running or paused and not locally paused if (!isLocallyPaused && (currentRun.status === 'running' || currentRun.status === 'paused')) { // Initial fetch fetchState(); // Poll every 3 seconds const interval = setInterval(fetchState, 3000); return () => { isMounted = false; clearInterval(interval); }; } return () => { isMounted = false; }; }, [siteId, runId, currentRun.status, currentRun.current_stage, onUpdate, isLocallyPaused]); // Attempt to fetch example items for the current stage when the API does not provide up_next/currently_processing useEffect(() => { let isMounted = true; const stageNumber = currentRun.current_stage; const loadStageQueue = async () => { try { switch (stageNumber) { case 1: { const res = await fetchKeywords({ page_size: 5, site_id: siteId, status: 'new' }); if (!isMounted) return; const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'keyword' })); setFetchedUpNext(items); setFetchedCurrently(items.slice(0, 1)); break; } case 2: { const res = await fetchClusters({ page_size: 5, site_id: siteId, status: 'new' }); if (!isMounted) return; const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.name || String(r.id), type: 'cluster' })); setFetchedUpNext(items); setFetchedCurrently(items.slice(0, 1)); break; } case 3: { const res = await fetchContentIdeas({ page_size: 5, site_id: siteId, status: 'queued' }); if (!isMounted) return; const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || String(r.id), type: 'idea' })); setFetchedUpNext(items); setFetchedCurrently(items.slice(0, 1)); break; } case 4: { // Tasks -> Content (show queued tasks) try { const res = await fetchTasks({ page_size: 5, site_id: siteId, status: 'queued' }); if (!isMounted) return; const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'task' })); setFetchedUpNext(items); setFetchedCurrently(items.slice(0, 1)); } catch (e) { // ignore } break; } case 5: { // Content -> Image Prompts (show content items awaiting prompts) try { const res = await fetchContent({ page_size: 5, site_id: siteId, status: 'queued' }); if (!isMounted) return; const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'content' })); setFetchedUpNext(items); setFetchedCurrently(items.slice(0, 1)); } catch (e) { // ignore } break; } case 6: { const res = await fetchContentImages({ page_size: 5, site_id: siteId, status: 'pending' }); if (!isMounted) return; const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.filename || String(r.id), type: 'image' })); setFetchedUpNext(items); setFetchedCurrently(items.slice(0, 1)); break; } default: // For stages without a clear read API, clear fetched lists setFetchedUpNext([]); setFetchedCurrently([]); } } catch (err) { console.warn('Failed to fetch stage queue samples:', err); } }; // Only attempt when there's no live up_next data if ((!processingState || (processingState && (processingState.up_next || []).length === 0)) && currentRun.status === 'running') { loadStageQueue(); } return () => { isMounted = false; }; }, [siteId, currentRun.current_stage, currentRun.status]); const handlePause = async () => { setIsPausing(true); try { await automationService.pause(siteId, runId); toast?.success('Automation pausing... will complete current item'); // Optimistically mark paused locally so UI stays paused until backend confirms setIsLocallyPaused(true); // Trigger update to refresh run status setTimeout(onUpdate, 1000); } catch (error: any) { toast?.error(error?.message || 'Failed to pause automation'); } finally { setIsPausing(false); } }; const handleResume = async () => { setIsResuming(true); try { await automationService.resume(siteId, runId); toast?.success('Automation resumed'); // Clear local paused flag setIsLocallyPaused(false); // Trigger update to refresh run status setTimeout(onUpdate, 1000); } catch (error: any) { toast?.error(error?.message || 'Failed to resume automation'); } finally { setIsResuming(false); } }; const handleCancel = async () => { if (!confirm('Are you sure you want to cancel this automation run? This cannot be undone.')) { return; } setIsCancelling(true); try { await automationService.cancel(siteId, runId); toast?.success('Automation cancelling... will complete current item'); // Trigger update to refresh run status setTimeout(onUpdate, 1500); } catch (error: any) { toast?.error(error?.message || 'Failed to cancel automation'); } finally { setIsCancelling(false); } }; const formatDuration = (startTime: string) => { const start = new Date(startTime).getTime(); const now = Date.now(); const diffMs = now - start; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); if (diffHours > 0) { return `${diffHours}h ${diffMins % 60}m`; } return `${diffMins}m`; }; if (error) { return (

{error}

); } // Build a fallback processing state from currentRun and pipelineOverview when API doesn't return live state const currentStageIndex = (currentRun.current_stage || 1) - 1; const stageOverview = pipelineOverview && pipelineOverview[currentStageIndex] ? pipelineOverview[currentStageIndex] : null; const stageResult = (currentRun as any)[`stage_${currentRun.current_stage}_result`]; // FIXED: Helper to get processed count using correct key const getProcessedFromResult = (result: any, stageNumber: number): number => { if (!result) return 0; const keyMap: Record = { 1: 'keywords_processed', 2: 'clusters_processed', 3: 'ideas_processed', 4: 'tasks_processed', 5: 'content_processed', 6: 'images_processed', 7: 'ready_for_review' }; return result[keyMap[stageNumber]] ?? 0; }; const fallbackState: ProcessingState | null = ((): ProcessingState | null => { if (!processingState && (stageOverview || stageResult)) { // FIXED: Use stage-specific key instead of summing all numeric values const processed = getProcessedFromResult(stageResult, currentRun.current_stage); const total = (stageOverview?.pending || 0) + processed; const percentage = total > 0 ? Math.round((processed / total) * 100) : 0; return { stage_number: currentRun.current_stage, stage_name: stageOverview?.name || `Stage ${currentRun.current_stage}`, stage_type: stageOverview?.type || 'AI', total_items: total, processed_items: processed, percentage, currently_processing: [], up_next: [], remaining_count: Math.max(0, total - processed), }; } return null; })(); const displayState = processingState || fallbackState; // If we don't have a live displayState, keep rendering the card using computed values // Computed processed/total (use processingState when available, otherwise derive from stageResult + overview) const computedProcessed = ((): number => { if (displayState && typeof displayState.processed_items === 'number') return displayState.processed_items; if (stageResult) { // FIXED: Use stage-specific key for processed count return getProcessedFromResult(stageResult, currentRun.current_stage); } return 0; })(); const computedTotal = ((): number => { if (displayState && typeof displayState.total_items === 'number' && displayState.total_items > 0) return displayState.total_items; const pending = stageOverview?.pending ?? 0; return Math.max(pending + computedProcessed, 0); })(); const percentage = computedTotal > 0 ? Math.round((computedProcessed / computedTotal) * 100) : 0; const isPaused = currentRun.status === 'paused'; // Choose stage accent color (simple map matching AutomationPage STAGE_CONFIG) const stageColors = [ 'from-blue-500 to-blue-600', 'from-purple-500 to-purple-600', 'from-indigo-500 to-indigo-600', 'from-green-500 to-green-600', 'from-amber-500 to-amber-600', 'from-pink-500 to-pink-600', 'from-teal-500 to-teal-600', ]; const stageColorClass = stageColors[(currentRun.current_stage || 1) - 1] || 'from-blue-500 to-blue-600'; return (
{/* Header Row with Main Info and Close */}
{/* Left Side - Main Info (75%) */}
{isPaused ? ( ) : ( )}

{isPaused ? 'Automation Paused' : 'Automation In Progress'}

{/* Centered stage row + dynamic action text */} {displayState && (
Stage {currentRun.current_stage}: {displayState.stage_name} {displayState.stage_type}
{(() => { // Build dynamic action text based on stage type and counts const verb = displayState.stage_type === 'AI' ? 'Generating' : 'Processing'; // target label for the current stage (what is being produced) const targetLabelMap: Record = { 1: 'Clusters', 2: 'Ideas', 3: 'Tasks', 4: 'Content', 5: 'Image Prompts', 6: 'Images', 7: 'Review', }; const label = targetLabelMap[displayState.stage_number] || 'Items'; return `${verb} ${computedProcessed}/${computedTotal} ${label}`; })()}
)}
{/* Progress Info */} {displayState && ( <>
{percentage}%
{computedProcessed}/{computedTotal} completed
{/* Progress Bar */}
{/* Currently Processing and Up Next */}
{/* Currently Processing */}

Currently Processing:

{((displayState.currently_processing && displayState.currently_processing.length > 0) ? displayState.currently_processing : fetchedCurrently).length > 0 ? ( ((displayState.currently_processing && displayState.currently_processing.length > 0) ? displayState.currently_processing : fetchedCurrently).map((item, idx) => (
{item.title}
)) ) : (
{isPaused ? 'Paused' : 'No items currently processing'}
)}
{/* Up Next */}

Up Next:

{((displayState.up_next && displayState.up_next.length > 0) ? displayState.up_next : fetchedUpNext).length > 0 ? ( <> {((displayState.up_next && displayState.up_next.length > 0) ? displayState.up_next : fetchedUpNext).map((item, idx) => (
{item.title}
))} {displayState.remaining_count > ((displayState.up_next?.length || 0) + (displayState.currently_processing?.length || 0)) && (
+ {displayState.remaining_count - ((displayState.up_next?.length || 0) + (displayState.currently_processing?.length || 0))} more in queue
)} ) : (
Queue empty
)}
{/* Control Buttons */}
{currentRun.status === 'running' ? ( ) : currentRun.status === 'paused' ? ( ) : null}
)}
{/* Right Side - Metrics and Close (25%) */}
{/* Close Button */}
{/* Metrics Cards */}
{/* Duration */}
Duration
{formatDuration(currentRun.started_at)}
{/* Credits Used */}
Credits Used
{currentRun.total_credits_used}
{/* Current Stage */}
Stage
{currentRun.current_stage} of 7
{/* Status */}
Status
{isPaused ? 'Paused' : 'Running'}
{/* Debug table toggle + table for stage data */}
{showDebugTable && (
Stage Data
{(pipelineOverview || []).map((stage) => { const result = (currentRun as any)[`stage_${stage.number}_result`]; const processed = result ? Object.values(result).reduce((s: number, v: any) => typeof v === 'number' ? s + v : s, 0) : 0; const total = Math.max((stage.pending || 0) + processed, 0); const currently = currentRun.current_stage === stage.number ? (processingState?.currently_processing?.slice(0,3) || fetchedCurrently) : []; const upnext = currentRun.current_stage === stage.number ? (processingState?.up_next?.slice(0,5) || fetchedUpNext) : []; return ( ); })}
Stage Pending Processed Total Currently (sample) Up Next (sample)
{stage.number} — {stage.name} {stage.pending} {processed} {total} {currently.map(c => c.title).join(', ') || '-'} {upnext.map(u => u.title).join(', ') || '-'}
)}
); }; export default CurrentProcessingCard;