249 lines
9.1 KiB
TypeScript
249 lines
9.1 KiB
TypeScript
/**
|
|
* Global Progress Bar Component
|
|
* Shows full pipeline progress across all 7 automation stages.
|
|
* Persists until 100% complete or run is finished.
|
|
*/
|
|
import React from 'react';
|
|
import { AutomationRun, InitialSnapshot, StageProgress, GlobalProgress } from '../../services/automationService';
|
|
import { BoltIcon, CheckCircleIcon, PauseIcon } from '../../icons';
|
|
|
|
// Stage colors matching AutomationPage STAGE_CONFIG exactly
|
|
const STAGE_COLORS = [
|
|
'from-brand-500 to-brand-600', // Stage 1: Keywords → Clusters (brand/teal)
|
|
'from-purple-500 to-purple-600', // Stage 2: Clusters → Ideas (purple)
|
|
'from-warning-500 to-warning-600', // Stage 3: Ideas → Tasks (amber)
|
|
'from-brand-500 to-brand-600', // Stage 4: Tasks → Content (brand/teal)
|
|
'from-success-500 to-success-600', // Stage 5: Content → Image Prompts (green)
|
|
'from-purple-500 to-purple-600', // Stage 6: Image Prompts → Images (purple)
|
|
'from-success-500 to-success-600', // Stage 7: Review Gate (green)
|
|
];
|
|
|
|
const STAGE_NAMES = [
|
|
'Keywords',
|
|
'Clusters',
|
|
'Ideas',
|
|
'Tasks',
|
|
'Content',
|
|
'Prompts',
|
|
'Publish',
|
|
];
|
|
|
|
// Helper to get processed count from stage result using correct key
|
|
export 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;
|
|
};
|
|
|
|
interface GlobalProgressBarProps {
|
|
currentRun: AutomationRun | null;
|
|
globalProgress?: GlobalProgress | null;
|
|
stages?: StageProgress[];
|
|
initialSnapshot?: InitialSnapshot | null;
|
|
}
|
|
|
|
const GlobalProgressBar: React.FC<GlobalProgressBarProps> = ({
|
|
currentRun,
|
|
globalProgress,
|
|
stages,
|
|
initialSnapshot,
|
|
}) => {
|
|
// Don't render if no run or run is completed with 100%
|
|
if (!currentRun) {
|
|
return null;
|
|
}
|
|
|
|
// Calculate global progress if not provided from API
|
|
const calculateGlobalProgress = (): { percentage: number; completed: number; total: number } => {
|
|
// If we have API-provided global progress, use it
|
|
if (globalProgress) {
|
|
return {
|
|
percentage: globalProgress.percentage,
|
|
completed: globalProgress.completed_items,
|
|
total: globalProgress.total_items,
|
|
};
|
|
}
|
|
|
|
// Fallback: Calculate from currentRun and initialSnapshot
|
|
const snapshot = initialSnapshot || (currentRun as any)?.initial_snapshot;
|
|
|
|
// Calculate total completed from all stage results
|
|
let totalCompleted = 0;
|
|
for (let i = 1; i <= 7; i++) {
|
|
const result = (currentRun as any)[`stage_${i}_result`];
|
|
if (result) {
|
|
totalCompleted += getProcessedFromResult(result, i);
|
|
}
|
|
}
|
|
|
|
// Calculate total items - sum of ALL stage initials from snapshot (updated after each stage)
|
|
// This accounts for items created during the run (e.g., keywords create clusters, clusters create ideas)
|
|
let totalItems = 0;
|
|
if (snapshot) {
|
|
for (let i = 1; i <= 7; i++) {
|
|
const stageInitial = snapshot[`stage_${i}_initial`] || 0;
|
|
totalItems += stageInitial;
|
|
}
|
|
}
|
|
|
|
// Use the updated total from snapshot, or fallback to total_initial_items
|
|
const finalTotal = totalItems > 0 ? totalItems : (snapshot?.total_initial_items || 0);
|
|
|
|
// Ensure completed never exceeds total (clamp percentage to 100%)
|
|
const percentage = finalTotal > 0 ? Math.round((totalCompleted / finalTotal) * 100) : 0;
|
|
|
|
return {
|
|
percentage: Math.min(percentage, 100),
|
|
completed: totalCompleted,
|
|
total: finalTotal,
|
|
};
|
|
};
|
|
|
|
const { percentage: realPercent, completed, total } = calculateGlobalProgress();
|
|
|
|
// REMOVED: Animated progress that was causing confusion
|
|
// Now using real percentage directly from backend
|
|
const percentage = realPercent;
|
|
|
|
// Hide if completed and at 100%
|
|
if (currentRun.status === 'completed' && percentage >= 100) {
|
|
return null;
|
|
}
|
|
|
|
const isPaused = currentRun.status === 'paused';
|
|
const currentStage = currentRun.current_stage;
|
|
|
|
// Get stage status for segmented bar
|
|
const getStageStatus = (stageNum: number): 'completed' | 'active' | 'pending' => {
|
|
if (stages && stages[stageNum - 1]) {
|
|
return stages[stageNum - 1].status as 'completed' | 'active' | 'pending';
|
|
}
|
|
if (currentStage > stageNum) return 'completed';
|
|
if (currentStage === stageNum) return 'active';
|
|
return 'pending';
|
|
};
|
|
|
|
const formatDuration = (): string => {
|
|
if (!currentRun.started_at) return '';
|
|
const start = new Date(currentRun.started_at).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`;
|
|
};
|
|
|
|
return (
|
|
<div className={`
|
|
rounded-2xl 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-300 dark:border-warning-700'
|
|
: 'bg-gradient-to-r from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/20 border-brand-300 dark:border-brand-700'
|
|
}
|
|
`}>
|
|
{/* Header Row */}
|
|
<div className="flex justify-between items-center mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`
|
|
size-12 rounded-xl flex items-center justify-center shadow-lg
|
|
${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-6 h-6 text-white" />
|
|
) : percentage >= 100 ? (
|
|
<CheckCircleIcon className="w-6 h-6 text-white" />
|
|
) : (
|
|
<BoltIcon className="w-6 h-6 text-white animate-pulse" />
|
|
)}
|
|
</div>
|
|
<div>
|
|
<div className={`text-lg font-bold ${isPaused ? 'text-warning-800 dark:text-warning-200' : 'text-brand-800 dark:text-brand-200'}`}>
|
|
{isPaused ? 'Pipeline Paused' : 'Full Pipeline Progress'}
|
|
</div>
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
Stage {currentStage} of 7 • {formatDuration()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className={`text-4xl font-bold ${isPaused ? 'text-warning-600 dark:text-warning-400' : 'text-brand-600 dark:text-brand-400'}`}>
|
|
{percentage}%
|
|
</div>
|
|
</div>
|
|
|
|
{/* Segmented Progress Bar - Taller & More Vibrant */}
|
|
<div className="flex h-5 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700 gap-0.5 mb-3 shadow-inner">
|
|
{[1, 2, 3, 4, 5, 6, 7].map(stageNum => {
|
|
const status = getStageStatus(stageNum);
|
|
const stageColor = STAGE_COLORS[stageNum - 1];
|
|
|
|
return (
|
|
<div
|
|
key={stageNum}
|
|
className={`flex-1 transition-all duration-500 relative group ${
|
|
status === 'completed'
|
|
? `bg-gradient-to-r ${stageColor} shadow-sm`
|
|
: status === 'active'
|
|
? `bg-gradient-to-r ${stageColor} opacity-70 ${!isPaused ? 'animate-pulse' : ''} shadow-sm`
|
|
: 'bg-gray-300 dark:bg-gray-600'
|
|
}`}
|
|
title={`Stage ${stageNum}: ${STAGE_NAMES[stageNum - 1]}`}
|
|
>
|
|
{/* Tooltip on hover */}
|
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
|
|
S{stageNum}: {STAGE_NAMES[stageNum - 1]}
|
|
{status === 'completed' && ' ✓'}
|
|
{status === 'active' && ' ●'}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Footer Row - Larger Font for Stage Numbers */}
|
|
<div className="flex justify-between items-center text-sm text-gray-700 dark:text-gray-300">
|
|
<span className="font-medium">{completed} / {total} items processed</span>
|
|
<div className="flex gap-3">
|
|
{[1, 2, 3, 4, 5, 6, 7].map(stageNum => {
|
|
const status = getStageStatus(stageNum);
|
|
return (
|
|
<span
|
|
key={stageNum}
|
|
className={`text-base font-semibold ${
|
|
status === 'completed' ? 'text-success-600 dark:text-success-400' : ''
|
|
} ${
|
|
status === 'active' ? `${isPaused ? 'text-warning-600 dark:text-warning-400' : 'text-brand-600 dark:text-brand-400'}` : ''
|
|
} ${
|
|
status === 'pending' ? 'text-gray-400 dark:text-gray-500' : ''
|
|
}`}
|
|
>
|
|
{stageNum}
|
|
{status === 'completed' && '✓'}
|
|
{status === 'active' && '●'}
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default GlobalProgressBar;
|