339 lines
12 KiB
TypeScript
339 lines
12 KiB
TypeScript
/**
|
|
* Current Processing Card V2 - Simplified
|
|
* Shows real-time automation progress
|
|
* Clean UI without cluttered "Currently Processing" and "Up Next" sections
|
|
*/
|
|
import React, { useEffect, useState } from 'react';
|
|
import { automationService, ProcessingState, AutomationRun, PipelineStage } from '../../services/automationService';
|
|
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[];
|
|
}
|
|
|
|
// Stage config matching AutomationPage
|
|
const STAGE_NAMES: Record<number, string> = {
|
|
1: 'Keywords → Clusters',
|
|
2: 'Clusters → Ideas',
|
|
3: 'Ideas → Tasks',
|
|
4: 'Tasks → Content',
|
|
5: 'Content → Image Prompts',
|
|
6: 'Image Prompts → Images',
|
|
7: 'Review Gate',
|
|
};
|
|
|
|
const STAGE_OUTPUT_LABELS: Record<number, string> = {
|
|
1: 'Clusters',
|
|
2: 'Ideas',
|
|
3: 'Tasks',
|
|
4: 'Content',
|
|
5: 'Image Prompts',
|
|
6: 'Images',
|
|
7: 'Items',
|
|
};
|
|
|
|
// Helper to get processed count from stage result using correct key
|
|
const getProcessedFromResult = (result: any, stageNumber: number): number => {
|
|
if (!result) return 0;
|
|
const keyMap: Record<number, string> = {
|
|
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;
|
|
};
|
|
|
|
// Helper to get total from stage result
|
|
const getTotalFromResult = (result: any, stageNumber: number): number => {
|
|
if (!result) return 0;
|
|
const keyMap: Record<number, string> = {
|
|
1: 'keywords_total',
|
|
2: 'clusters_total',
|
|
3: 'ideas_total',
|
|
4: 'tasks_total',
|
|
5: 'content_total',
|
|
6: 'images_total',
|
|
7: 'review_total'
|
|
};
|
|
return result[keyMap[stageNumber]] ?? 0;
|
|
};
|
|
|
|
const formatDuration = (startedAt: string): string => {
|
|
if (!startedAt) return '0m';
|
|
const start = new Date(startedAt).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`;
|
|
};
|
|
|
|
const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
|
runId,
|
|
siteId,
|
|
currentRun,
|
|
onUpdate,
|
|
onClose,
|
|
pipelineOverview,
|
|
}) => {
|
|
const [processingState, setProcessingState] = useState<ProcessingState | null>(null);
|
|
const [isPausing, setIsPausing] = useState(false);
|
|
const [isResuming, setIsResuming] = useState(false);
|
|
const [isCancelling, setIsCancelling] = useState(false);
|
|
|
|
const toast = useToast();
|
|
|
|
// Fetch processing state
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
const fetchState = async () => {
|
|
try {
|
|
const state = await automationService.getCurrentProcessing(siteId, runId);
|
|
if (!isMounted) return;
|
|
setProcessingState(state);
|
|
|
|
// If stage completed, trigger update
|
|
if (state && state.processed_items >= state.total_items && state.total_items > 0) {
|
|
onUpdate();
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching processing state:', err);
|
|
}
|
|
};
|
|
|
|
if (currentRun.status === 'running' || currentRun.status === 'paused') {
|
|
fetchState();
|
|
const interval = setInterval(fetchState, 3000);
|
|
return () => {
|
|
isMounted = false;
|
|
clearInterval(interval);
|
|
};
|
|
}
|
|
|
|
return () => { isMounted = false; };
|
|
}, [siteId, runId, currentRun.status, currentRun.current_stage, onUpdate]);
|
|
|
|
// Get real values
|
|
const stageOverview = pipelineOverview?.find(s => s.number === currentRun.current_stage);
|
|
const stageResult = (currentRun as any)[`stage_${currentRun.current_stage}_result`];
|
|
|
|
const realProcessed = processingState?.processed_items ?? getProcessedFromResult(stageResult, currentRun.current_stage);
|
|
const realTotal = processingState?.total_items ?? getTotalFromResult(stageResult, currentRun.current_stage) ?? (stageOverview?.pending ?? 0) + realProcessed;
|
|
const realPercent = realTotal > 0 ? Math.round((realProcessed / realTotal) * 100) : 0;
|
|
|
|
// REMOVED: Animated progress that was causing confusion
|
|
// Now using real percentage directly from backend
|
|
const displayPercent = Math.min(realPercent, 100);
|
|
|
|
const isPaused = currentRun.status === 'paused';
|
|
const stageName = STAGE_NAMES[currentRun.current_stage] || `Stage ${currentRun.current_stage}`;
|
|
const outputLabel = STAGE_OUTPUT_LABELS[currentRun.current_stage] || 'Items';
|
|
|
|
const handlePause = async () => {
|
|
setIsPausing(true);
|
|
try {
|
|
await automationService.pause(siteId, runId);
|
|
toast?.success('Automation pausing...');
|
|
setTimeout(onUpdate, 1000);
|
|
} catch (error: any) {
|
|
toast?.error(error?.message || 'Failed to pause');
|
|
} finally {
|
|
setIsPausing(false);
|
|
}
|
|
};
|
|
|
|
const handleResume = async () => {
|
|
setIsResuming(true);
|
|
try {
|
|
await automationService.resume(siteId, runId);
|
|
toast?.success('Automation resumed');
|
|
setTimeout(onUpdate, 1000);
|
|
} catch (error: any) {
|
|
toast?.error(error?.message || 'Failed to resume');
|
|
} finally {
|
|
setIsResuming(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = async () => {
|
|
if (!confirm('Cancel this automation run? Progress will be saved.')) return;
|
|
setIsCancelling(true);
|
|
try {
|
|
await automationService.cancel(siteId, runId);
|
|
toast?.success('Automation cancelled');
|
|
setTimeout(onUpdate, 500);
|
|
} catch (error: any) {
|
|
toast?.error(error?.message || 'Failed to cancel');
|
|
} finally {
|
|
setIsCancelling(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`rounded-xl p-5 mb-6 border-2 transition-all ${
|
|
isPaused
|
|
? 'bg-gradient-to-r from-warning-50 to-warning-100 dark:from-warning-900/20 dark:to-warning-800/20 border-warning-400'
|
|
: 'bg-gradient-to-r from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/20 border-brand-400'
|
|
}`}>
|
|
{/* Header Row */}
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`size-10 rounded-lg flex items-center justify-center shadow-md ${
|
|
isPaused
|
|
? 'bg-gradient-to-br from-warning-500 to-warning-600'
|
|
: 'bg-gradient-to-br from-brand-500 to-brand-600'
|
|
}`}>
|
|
{isPaused ? (
|
|
<PauseIcon className="w-5 h-5 text-white" />
|
|
) : (
|
|
<BoltIcon className="w-5 h-5 text-white animate-pulse" />
|
|
)}
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-bold text-gray-900 dark:text-white">
|
|
{isPaused ? 'Automation Paused' : 'Automation In Progress'}
|
|
</h2>
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
Stage {currentRun.current_stage}: {stageName}
|
|
<span className={`ml-2 px-1.5 py-0.5 rounded text-xs ${
|
|
isPaused ? 'bg-warning-200 text-warning-800' : 'bg-brand-200 text-brand-800'
|
|
}`}>
|
|
{stageOverview?.type || 'AI'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Close and Actions */}
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
|
<XMarkIcon className="w-4 h-4" />
|
|
<span className="ml-1">Close</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress Section */}
|
|
<div className="flex gap-6">
|
|
{/* Main Progress - 70% width */}
|
|
<div className="flex-1">
|
|
{/* Progress Text */}
|
|
<div className="flex items-baseline justify-between mb-2">
|
|
<div className="flex items-baseline gap-3">
|
|
<span className={`text-4xl font-bold ${isPaused ? 'text-warning-600' : 'text-brand-600'}`}>
|
|
{displayPercent}%
|
|
</span>
|
|
<span className="text-sm text-gray-500">
|
|
{realProcessed}/{realTotal} {outputLabel}
|
|
</span>
|
|
</div>
|
|
<span className="text-xs text-gray-400">
|
|
{realTotal - realProcessed} remaining
|
|
</span>
|
|
</div>
|
|
|
|
{/* Progress Bar */}
|
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
|
|
<div
|
|
className={`h-3 rounded-full transition-all duration-300 ${
|
|
isPaused
|
|
? 'bg-gradient-to-r from-warning-400 to-warning-600'
|
|
: 'bg-gradient-to-r from-brand-400 to-brand-600'
|
|
} ${!isPaused && displayPercent < 100 ? 'animate-pulse' : ''}`}
|
|
style={{ width: `${Math.min(displayPercent, 100)}%` }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Control Buttons */}
|
|
<div className="flex items-center gap-3 mt-4">
|
|
{currentRun.status === 'running' ? (
|
|
<Button
|
|
onClick={handlePause}
|
|
disabled={isPausing}
|
|
variant="secondary"
|
|
size="sm"
|
|
startIcon={<PauseIcon className="w-4 h-4" />}
|
|
>
|
|
{isPausing ? 'Pausing...' : 'Pause'}
|
|
</Button>
|
|
) : currentRun.status === 'paused' ? (
|
|
<Button
|
|
onClick={handleResume}
|
|
disabled={isResuming}
|
|
variant="primary"
|
|
size="sm"
|
|
startIcon={<PlayIcon className="w-4 h-4" />}
|
|
>
|
|
{isResuming ? 'Resuming...' : 'Resume'}
|
|
</Button>
|
|
) : null}
|
|
|
|
<button
|
|
onClick={handleCancel}
|
|
disabled={isCancelling}
|
|
className="text-sm text-gray-500 hover:text-error-600 transition-colors"
|
|
>
|
|
<XMarkIcon className="w-4 h-4 inline mr-1" />
|
|
{isCancelling ? 'Cancelling...' : 'Cancel'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metrics - 30% width */}
|
|
<div className="w-44 space-y-2">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
<div className="flex items-center gap-1.5">
|
|
<ClockIcon className="w-4 h-4 text-gray-400" />
|
|
<span className="text-xs text-gray-500 uppercase">Duration</span>
|
|
</div>
|
|
<span className="text-sm font-bold text-gray-900 dark:text-white">{formatDuration(currentRun.started_at)}</span>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
<div className="flex items-center gap-1.5">
|
|
<BoltIcon className="w-4 h-4 text-warning-500" />
|
|
<span className="text-xs text-gray-500 uppercase">Credits</span>
|
|
</div>
|
|
<span className="text-sm font-bold text-warning-600">{currentRun.total_credits_used}</span>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
<span className="text-xs text-gray-500 uppercase">Stage</span>
|
|
<span className="text-sm font-bold text-gray-900 dark:text-white">{currentRun.current_stage} of 7</span>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
<span className="text-xs text-gray-500 uppercase">Status</span>
|
|
<span className={`text-sm font-bold ${isPaused ? 'text-warning-600' : 'text-success-600'}`}>
|
|
{isPaused ? 'Paused' : 'Running'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CurrentProcessingCard;
|