8 Phases refactor
This commit is contained in:
@@ -22,6 +22,8 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
stage_4_batch_size: config.stage_4_batch_size,
|
||||
stage_5_batch_size: config.stage_5_batch_size,
|
||||
stage_6_batch_size: config.stage_6_batch_size,
|
||||
within_stage_delay: config.within_stage_delay || 3,
|
||||
between_stage_delay: config.between_stage_delay || 5,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
@@ -212,6 +214,60 @@ const ConfigModal: React.FC<ConfigModalProps> = ({ config, onSave, onCancel }) =
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Request Delays */}
|
||||
<div className="mb-4 border-t pt-4">
|
||||
<h3 className="font-semibold mb-2">AI Request Delays</h3>
|
||||
<p className="text-sm text-gray-600 mb-3">
|
||||
Configure delays to prevent rate limiting and manage API load
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Within-Stage Delay (seconds)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.within_stage_delay || 3}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
within_stage_delay: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
min={0}
|
||||
max={30}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Delay between batches within a stage
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Between-Stage Delay (seconds)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.between_stage_delay || 5}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
between_stage_delay: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
min={0}
|
||||
max={60}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Delay between stage transitions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-end gap-2 mt-6">
|
||||
<button
|
||||
|
||||
@@ -28,13 +28,13 @@ import {
|
||||
} from '../../icons';
|
||||
|
||||
const STAGE_CONFIG = [
|
||||
{ icon: ListIcon, color: 'from-blue-500 to-blue-600', hoverColor: 'hover:border-blue-500', name: 'Keywords → Clusters' },
|
||||
{ icon: GroupIcon, color: 'from-purple-500 to-purple-600', hoverColor: 'hover:border-purple-500', name: 'Clusters → Ideas' },
|
||||
{ icon: CheckCircleIcon, color: 'from-indigo-500 to-indigo-600', hoverColor: 'hover:border-indigo-500', name: 'Ideas → Tasks' },
|
||||
{ icon: PencilIcon, color: 'from-green-500 to-green-600', hoverColor: 'hover:border-green-500', name: 'Tasks → Content' },
|
||||
{ icon: FileIcon, color: 'from-amber-500 to-amber-600', hoverColor: 'hover:border-amber-500', name: 'Content → Image Prompts' },
|
||||
{ icon: FileTextIcon, color: 'from-pink-500 to-pink-600', hoverColor: 'hover:border-pink-500', name: 'Image Prompts → Images' },
|
||||
{ icon: PaperPlaneIcon, color: 'from-teal-500 to-teal-600', hoverColor: 'hover:border-teal-500', name: 'Manual Review Gate' },
|
||||
{ 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 = () => {
|
||||
@@ -196,47 +196,41 @@ const AutomationPage: React.FC = () => {
|
||||
<DebugSiteSelector />
|
||||
</div>
|
||||
|
||||
{/* Schedule & Controls */}
|
||||
{/* Compact Schedule & Controls Panel */}
|
||||
{config && (
|
||||
<ComponentCard className="border-2 border-slate-200 dark:border-gray-800">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||||
<div className="flex-1 grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5">Status</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{config.is_enabled ? (
|
||||
<>
|
||||
<div className="size-2 bg-success-500 rounded-full animate-pulse"></div>
|
||||
<span className="text-sm font-semibold text-success-600 dark:text-success-400">Enabled</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="size-2 bg-gray-400 rounded-full"></div>
|
||||
<span className="text-sm font-semibold text-gray-600 dark:text-gray-400">Disabled</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-3 py-1">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
{config.is_enabled ? (
|
||||
<>
|
||||
<div className="size-2 bg-success-500 rounded-full animate-pulse"></div>
|
||||
<span className="text-sm font-semibold text-success-600 dark:text-success-400">Enabled</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="size-2 bg-gray-400 rounded-full"></div>
|
||||
<span className="text-sm font-semibold text-gray-600 dark:text-gray-400">Disabled</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5">Schedule</div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-white/90 capitalize">
|
||||
{config.frequency} at {config.scheduled_time}
|
||||
</div>
|
||||
<div className="h-4 w-px bg-slate-300 dark:bg-gray-700"></div>
|
||||
<div className="text-sm text-slate-700 dark:text-gray-300">
|
||||
<span className="font-medium capitalize">{config.frequency}</span> at {config.scheduled_time}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5">Last Run</div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-white/90">
|
||||
{config.last_run_at ? new Date(config.last_run_at).toLocaleDateString() : 'Never'}
|
||||
</div>
|
||||
<div className="h-4 w-px bg-slate-300 dark:bg-gray-700"></div>
|
||||
<div className="text-sm text-slate-600 dark:text-gray-400">
|
||||
Last: {config.last_run_at ? new Date(config.last_run_at).toLocaleDateString() : 'Never'}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5">Estimated Credits</div>
|
||||
<div className="text-sm font-semibold text-brand-600 dark:text-brand-400">
|
||||
<div className="h-4 w-px bg-slate-300 dark:bg-gray-700"></div>
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">Est:</span>{' '}
|
||||
<span className="font-semibold text-brand-600 dark:text-brand-400">
|
||||
{estimate?.estimated_credits || 0} credits
|
||||
{estimate && !estimate.sufficient && (
|
||||
<span className="ml-1 text-error-600 dark:text-error-400">(Low)</span>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
{estimate && !estimate.sufficient && (
|
||||
<span className="ml-1 text-error-600 dark:text-error-400 font-semibold">(Low)</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -263,323 +257,426 @@ const AutomationPage: React.FC = () => {
|
||||
</ComponentCard>
|
||||
)}
|
||||
|
||||
{/* Pipeline Overview */}
|
||||
<ComponentCard title="📊 Pipeline Overview" desc="Complete view of automation pipeline status and pending items">
|
||||
<div className="mb-6 flex items-center justify-between px-4 py-3 bg-slate-50 dark:bg-white/[0.02] rounded-lg border border-slate-200 dark:border-gray-800">
|
||||
<div className="text-sm font-medium">
|
||||
{currentRun ? (
|
||||
<span className="text-blue-600 dark:text-blue-400">
|
||||
<span className="inline-block size-2 bg-blue-500 rounded-full animate-pulse mr-2"></span>
|
||||
Live Run Active - Stage {currentRun.current_stage} of 7
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-700 dark:text-gray-300">Pipeline Status - Ready to run</span>
|
||||
)}
|
||||
{/* Metrics Summary Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div className="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-xl p-4 border-2 border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="size-10 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center">
|
||||
<ListIcon className="size-5 text-white" />
|
||||
</div>
|
||||
<div className="text-sm font-bold text-blue-900 dark:text-blue-100">Keywords</div>
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-slate-600 dark:text-gray-400">
|
||||
{totalPending} items pending
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-blue-700 dark:text-blue-300">Total:</span>
|
||||
<span className="font-bold text-blue-900 dark:text-blue-100">{pipelineOverview[0]?.pending || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pipeline Overview - 5 cards spread to full width */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||
{/* Stages 1-6 in main row (with 3+4 combined) */}
|
||||
{pipelineOverview.slice(0, 6).map((stage, index) => {
|
||||
// Combine stages 3 and 4 into one card
|
||||
if (index === 2) {
|
||||
const stage3 = pipelineOverview[2];
|
||||
const stage4 = pipelineOverview[3];
|
||||
const isActive3 = currentRun?.current_stage === 3;
|
||||
const isActive4 = currentRun?.current_stage === 4;
|
||||
const isComplete3 = currentRun && currentRun.current_stage > 3;
|
||||
const isComplete4 = currentRun && currentRun.current_stage > 4;
|
||||
const result3 = currentRun ? (currentRun[`stage_3_result` as keyof AutomationRun] as any) : null;
|
||||
const result4 = currentRun ? (currentRun[`stage_4_result` as keyof AutomationRun] as any) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key="stage-3-4"
|
||||
className={`
|
||||
relative rounded-xl border-2 p-6 transition-all
|
||||
${isActive3 || isActive4
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-500/10 shadow-lg'
|
||||
: isComplete3 && isComplete4
|
||||
? 'border-success-500 bg-success-50 dark:bg-success-500/10'
|
||||
: (stage3.pending > 0 || stage4.pending > 0)
|
||||
? `border-slate-200 bg-white dark:bg-white/[0.03] dark:border-gray-800 hover:border-indigo-500 hover:shadow-lg`
|
||||
: 'border-slate-200 bg-slate-50 dark:bg-white/[0.02] dark:border-gray-800'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="text-base font-bold text-gray-500 dark:text-gray-400">Stages 3 & 4</div>
|
||||
<div className={`size-14 rounded-xl bg-gradient-to-br from-indigo-500 to-green-600 flex items-center justify-center shadow-lg`}>
|
||||
<ArrowRightIcon className="size-7 text-white" />
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-xl p-4 border-2 border-purple-200 dark:border-purple-800">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="size-10 rounded-lg bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center">
|
||||
<GroupIcon className="size-5 text-white" />
|
||||
</div>
|
||||
<div className="text-sm font-bold text-purple-900 dark:text-purple-100">Clusters</div>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-purple-700 dark:text-purple-300">Pending:</span>
|
||||
<span className="font-bold text-purple-900 dark:text-purple-100">{pipelineOverview[1]?.pending || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-indigo-50 to-indigo-100 dark:from-indigo-900/20 dark:to-indigo-800/20 rounded-xl p-4 border-2 border-indigo-200 dark:border-indigo-800">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="size-10 rounded-lg bg-gradient-to-br from-indigo-500 to-indigo-600 flex items-center justify-center">
|
||||
<CheckCircleIcon className="size-5 text-white" />
|
||||
</div>
|
||||
<div className="text-sm font-bold text-indigo-900 dark:text-indigo-100">Ideas</div>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-indigo-700 dark:text-indigo-300">Pending:</span>
|
||||
<span className="font-bold text-indigo-900 dark:text-indigo-100">{pipelineOverview[2]?.pending || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-xl p-4 border-2 border-green-200 dark:border-green-800">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="size-10 rounded-lg bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center">
|
||||
<FileTextIcon className="size-5 text-white" />
|
||||
</div>
|
||||
<div className="text-sm font-bold text-green-900 dark:text-green-100">Content</div>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-green-700 dark:text-green-300">Tasks:</span>
|
||||
<span className="font-bold text-green-900 dark:text-green-100">{pipelineOverview[3]?.pending || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-pink-50 to-pink-100 dark:from-pink-900/20 dark:to-pink-800/20 rounded-xl p-4 border-2 border-pink-200 dark:border-pink-800">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="size-10 rounded-lg bg-gradient-to-br from-pink-500 to-pink-600 flex items-center justify-center">
|
||||
<FileIcon className="size-5 text-white" />
|
||||
</div>
|
||||
<div className="text-sm font-bold text-pink-900 dark:text-pink-100">Images</div>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-pink-700 dark:text-pink-300">Pending:</span>
|
||||
<span className="font-bold text-pink-900 dark:text-pink-100">{pipelineOverview[5]?.pending || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pipeline Status Card - Centered */}
|
||||
<div className="flex justify-center">
|
||||
<div className="max-w-2xl w-full">
|
||||
<div className={`
|
||||
rounded-2xl border-3 p-6 shadow-xl transition-all
|
||||
${currentRun?.status === 'running'
|
||||
? 'border-blue-500 bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/30 dark:to-blue-800/30'
|
||||
: currentRun?.status === 'paused'
|
||||
? 'border-amber-500 bg-gradient-to-br from-amber-50 to-amber-100 dark:from-amber-900/30 dark:to-amber-800/30'
|
||||
: totalPending > 0
|
||||
? 'border-success-500 bg-gradient-to-br from-success-50 to-success-100 dark:from-success-900/30 dark:to-success-800/30'
|
||||
: 'border-slate-300 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-gray-800/30 dark:to-gray-700/30'
|
||||
}
|
||||
`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`
|
||||
size-16 rounded-2xl flex items-center justify-center shadow-lg
|
||||
${currentRun?.status === 'running'
|
||||
? 'bg-gradient-to-br from-blue-500 to-blue-600'
|
||||
: currentRun?.status === 'paused'
|
||||
? 'bg-gradient-to-br from-amber-500 to-amber-600'
|
||||
: totalPending > 0
|
||||
? 'bg-gradient-to-br from-success-500 to-success-600'
|
||||
: 'bg-gradient-to-br from-slate-400 to-slate-500'
|
||||
}
|
||||
`}>
|
||||
{currentRun?.status === 'running' && <div className="size-3 bg-white rounded-full animate-pulse"></div>}
|
||||
{currentRun?.status === 'paused' && <ClockIcon className="size-8 text-white" />}
|
||||
{!currentRun && totalPending > 0 && <CheckCircleIcon className="size-8 text-white" />}
|
||||
{!currentRun && totalPending === 0 && <BoltIcon className="size-8 text-white" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-slate-900 dark:text-white mb-1">
|
||||
{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'}
|
||||
</div>
|
||||
<div className="text-base font-bold text-slate-900 dark:text-white/90 mb-5 leading-tight">
|
||||
Ideas → Tasks → Content
|
||||
</div>
|
||||
|
||||
{/* Queue Details - Always Show */}
|
||||
<div className="space-y-3">
|
||||
{/* Stage 3 queue */}
|
||||
<div className="pb-3 border-b border-slate-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-semibold text-indigo-600 dark:text-indigo-400">Ideas → Tasks</span>
|
||||
{isActive3 && <span className="text-xs px-2 py-0.5 bg-blue-500 text-white rounded-full">● Processing</span>}
|
||||
{isComplete3 && <span className="text-xs px-2 py-0.5 bg-success-500 text-white rounded-full">✓ Completed</span>}
|
||||
</div>
|
||||
{result3 ? (
|
||||
<div className="text-xs space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Processed:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{result3.ideas_processed || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Created:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{result3.tasks_created || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs space-y-1.5">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Total Queue:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{stage3.pending}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Processed:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">0</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Remaining:</span>
|
||||
<span className="font-bold text-indigo-600 dark:text-indigo-400">{stage3.pending}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stage 4 queue */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-semibold text-green-600 dark:text-green-400">Tasks → Content</span>
|
||||
{isActive4 && <span className="text-xs px-2 py-0.5 bg-blue-500 text-white rounded-full">● Processing</span>}
|
||||
{isComplete4 && <span className="text-xs px-2 py-0.5 bg-success-500 text-white rounded-full">✓ Completed</span>}
|
||||
</div>
|
||||
{result4 ? (
|
||||
<div className="text-xs space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Processed:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{result4.tasks_processed || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Created:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{result4.content_created || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Credits:</span>
|
||||
<span className="font-bold text-amber-600 dark:text-amber-400">{result4.credits_used || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs space-y-1.5">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Total Queue:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{stage4.pending}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Processed:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">0</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Remaining:</span>
|
||||
<span className="font-bold text-green-600 dark:text-green-400">{stage4.pending}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-slate-600 dark:text-gray-300">
|
||||
{currentRun && `Started: ${new Date(currentRun.started_at).toLocaleTimeString()}`}
|
||||
{!currentRun && totalPending > 0 && `${totalPending} items in pipeline`}
|
||||
{!currentRun && totalPending === 0 && 'All stages clear'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
{currentRun && (
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-slate-600 dark:text-gray-400">Credits Used</div>
|
||||
<div className="text-3xl font-bold text-brand-600 dark:text-brand-400">{currentRun.total_credits_used}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
// Skip stage 4 since it's combined with 3
|
||||
if (index === 3) return null;
|
||||
|
||||
// Adjust index for stages 5 and 6 (shift by 1 because we skip stage 4)
|
||||
const actualStage = index < 3 ? stage : pipelineOverview[index + 1];
|
||||
const stageConfig = STAGE_CONFIG[index < 3 ? index : index + 1];
|
||||
{/* Overall Progress Bar */}
|
||||
{currentRun && currentRun.status === 'running' && (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between text-xs text-slate-600 dark:text-gray-400 mb-2">
|
||||
<span>Overall Progress</span>
|
||||
<span>{Math.round((currentRun.current_stage / 7) * 100)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-600 h-3 rounded-full transition-all duration-500 animate-pulse"
|
||||
style={{ width: `${(currentRun.current_stage / 7) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pipeline Stages */}
|
||||
<ComponentCard>
|
||||
{/* Row 1: Stages 1-4 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
{pipelineOverview.slice(0, 4).map((stage, index) => {
|
||||
const stageConfig = STAGE_CONFIG[index];
|
||||
const StageIcon = stageConfig.icon;
|
||||
const isActive = currentRun?.current_stage === actualStage.number;
|
||||
const isComplete = currentRun && currentRun.current_stage > actualStage.number;
|
||||
const result = currentRun ? (currentRun[`stage_${actualStage.number}_result` as keyof AutomationRun] as any) : null;
|
||||
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 (
|
||||
<div
|
||||
key={actualStage.number}
|
||||
key={stage.number}
|
||||
className={`
|
||||
relative rounded-xl border-2 p-6 transition-all
|
||||
relative rounded-xl border-2 p-5 transition-all
|
||||
${isActive
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-500/10 shadow-lg'
|
||||
: isComplete
|
||||
? 'border-success-500 bg-success-50 dark:bg-success-500/10'
|
||||
: actualStage.pending > 0
|
||||
: stage.pending > 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'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
{/* Compact Header */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="text-base font-bold text-gray-500 dark:text-gray-400 mb-1">Stage {actualStage.number}</div>
|
||||
{isActive && <span className="text-xs px-2 py-0.5 bg-blue-500 text-white rounded-full">● Processing</span>}
|
||||
{isComplete && <span className="text-xs px-2 py-0.5 bg-success-500 text-white rounded-full">✓ Completed</span>}
|
||||
{!isActive && !isComplete && actualStage.pending > 0 && <span className="text-xs px-2 py-0.5 bg-gray-400 text-white rounded-full">Ready</span>}
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="text-sm font-bold text-gray-900 dark:text-white">Stage {stage.number}</div>
|
||||
{isActive && <span className="text-xs px-2 py-0.5 bg-blue-500 text-white rounded-full">● Active</span>}
|
||||
{isComplete && <span className="text-xs px-2 py-0.5 bg-success-500 text-white rounded-full">✓</span>}
|
||||
{!isActive && !isComplete && stage.pending > 0 && <span className="text-xs px-2 py-0.5 bg-gray-400 text-white rounded-full">Ready</span>}
|
||||
</div>
|
||||
<div className="text-xs font-medium text-gray-600 dark:text-gray-400">{stageConfig.name}</div>
|
||||
</div>
|
||||
<div className={`size-14 rounded-xl bg-gradient-to-br ${stageConfig.color} flex items-center justify-center shadow-lg`}>
|
||||
<StageIcon className="size-7 text-white" />
|
||||
<div className={`size-8 rounded-lg bg-gradient-to-br ${stageConfig.color} flex items-center justify-center shadow-md flex-shrink-0`}>
|
||||
<StageIcon className="size-4 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stage Name */}
|
||||
<div className="text-base font-bold text-slate-900 dark:text-white/90 mb-5 leading-tight min-h-[40px]">
|
||||
{actualStage.name}
|
||||
{/* Queue Metrics */}
|
||||
<div className="space-y-1.5 text-xs mb-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Total Queue:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{stage.pending}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Processed:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{processed}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Remaining:</span>
|
||||
<span className={`font-bold ${stageConfig.textColor} dark:${stageConfig.textColor}`}>
|
||||
{stage.pending}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Details - Always Show */}
|
||||
<div className="space-y-3">
|
||||
{/* Show results if completed */}
|
||||
{result && (
|
||||
<div className="text-xs space-y-1.5">
|
||||
{Object.entries(result).map(([key, value]) => (
|
||||
<div key={key} className="flex justify-between items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400 capitalize">{key.replace(/_/g, ' ')}:</span>
|
||||
<span className={`font-bold ${key.includes('credits') ? 'text-amber-600 dark:text-amber-400' : 'text-slate-900 dark:text-white'}`}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Progress Bar */}
|
||||
{(isActive || isComplete || processed > 0) && (
|
||||
<div className="mt-3 pt-3 border-t border-slate-200 dark:border-gray-700">
|
||||
<div className="flex justify-between text-xs text-gray-600 dark:text-gray-400 mb-1.5">
|
||||
<span>Progress</span>
|
||||
<span>{isComplete ? '100' : progressPercent}%</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show queue details if not completed */}
|
||||
{!result && (
|
||||
<div className="text-xs space-y-1.5 border-t border-slate-200 dark:border-gray-700 pt-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Total Queue:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{actualStage.pending}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Processed:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">0</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Remaining:</span>
|
||||
<span className={`font-bold ${stageConfig.color.split(' ')[1]}`}>
|
||||
{actualStage.pending}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className={`bg-gradient-to-r ${stageConfig.color} h-2 rounded-full transition-all duration-500 ${isActive ? 'animate-pulse' : ''}`}
|
||||
style={{ width: `${isComplete ? 100 : progressPercent}%` }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show processing indicator if active */}
|
||||
{isActive && (
|
||||
<div className="pt-3 border-t border-blue-200 dark:border-blue-700">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="size-2 bg-blue-500 rounded-full animate-pulse"></div>
|
||||
<span className="text-xs text-blue-600 dark:text-blue-400 font-semibold">Processing...</span>
|
||||
</div>
|
||||
{/* Progress bar placeholder */}
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
||||
<div className="bg-blue-500 h-1.5 rounded-full animate-pulse" style={{ width: '45%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show empty state */}
|
||||
{!result && actualStage.pending === 0 && !isActive && (
|
||||
<div className="pt-3 border-t border-slate-200 dark:border-gray-700 text-center">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">No items to process</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Stage 7 - Manual Review Gate (Separate Row) */}
|
||||
{pipelineOverview[6] && (
|
||||
<div className="mt-8">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{(() => {
|
||||
const stage7 = pipelineOverview[6];
|
||||
const isActive = currentRun?.current_stage === 7;
|
||||
const isComplete = currentRun && currentRun.current_stage > 7;
|
||||
const result = currentRun ? (currentRun[`stage_7_result` as keyof AutomationRun] as any) : null;
|
||||
{/* Row 2: Stages 5-7 + Status Summary */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* 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 (
|
||||
<div
|
||||
key={stage.number}
|
||||
className={`
|
||||
relative rounded-xl border-2 p-5 transition-all
|
||||
${isActive
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-500/10 shadow-lg'
|
||||
: isComplete
|
||||
? 'border-success-500 bg-success-50 dark:bg-success-500/10'
|
||||
: stage.pending > 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'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="text-sm font-bold text-gray-900 dark:text-white">Stage {stage.number}</div>
|
||||
{isActive && <span className="text-xs px-2 py-0.5 bg-blue-500 text-white rounded-full">● Active</span>}
|
||||
{isComplete && <span className="text-xs px-2 py-0.5 bg-success-500 text-white rounded-full">✓</span>}
|
||||
{!isActive && !isComplete && stage.pending > 0 && <span className="text-xs px-2 py-0.5 bg-gray-400 text-white rounded-full">Ready</span>}
|
||||
</div>
|
||||
<div className="text-xs font-medium text-gray-600 dark:text-gray-400">{stageConfig.name}</div>
|
||||
</div>
|
||||
<div className={`size-8 rounded-lg bg-gradient-to-br ${stageConfig.color} flex items-center justify-center shadow-md flex-shrink-0`}>
|
||||
<StageIcon className="size-4 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
relative rounded-2xl border-3 p-8 transition-all text-center shadow-xl
|
||||
${isActive
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-500/10'
|
||||
: isComplete
|
||||
? 'border-success-500 bg-success-50 dark:bg-success-500/10'
|
||||
: stage7.pending > 0
|
||||
? `border-slate-300 bg-white dark:bg-white/[0.05] dark:border-gray-700 hover:border-teal-500`
|
||||
: 'border-slate-200 bg-slate-50 dark:bg-white/[0.02] dark:border-gray-800'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`size-20 mb-5 rounded-2xl bg-gradient-to-br ${STAGE_CONFIG[6].color} flex items-center justify-center shadow-2xl`}>
|
||||
<PaperPlaneIcon className="size-10 text-white" />
|
||||
</div>
|
||||
<div className="text-sm font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Stage 7</div>
|
||||
<div className="text-2xl font-bold text-slate-900 dark:text-white/90 mb-3">
|
||||
Manual Review Gate
|
||||
</div>
|
||||
<div className="text-lg text-red-600 dark:text-red-400 font-semibold mb-6">
|
||||
🚫 Automation Stops Here
|
||||
</div>
|
||||
|
||||
{stage7.pending > 0 && (
|
||||
<div className="mb-6 p-6 bg-teal-50 dark:bg-teal-900/20 rounded-xl border-2 border-teal-200 dark:border-teal-800">
|
||||
<div className="text-base text-gray-600 dark:text-gray-300 mb-3 font-medium">Content Ready for Manual Review</div>
|
||||
<div className="text-6xl font-bold text-teal-600 dark:text-teal-400">{stage7.pending}</div>
|
||||
<div className="mt-3 text-sm text-gray-500 dark:text-gray-400">pieces of content waiting</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div className="pt-6 border-t-2 border-slate-200 dark:border-gray-700 w-full max-w-lg mx-auto">
|
||||
<div className="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-4">Last Run Results</div>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{Object.entries(result).map(([key, value]) => (
|
||||
<div key={key} className="text-center p-4 bg-slate-50 dark:bg-white/5 rounded-lg">
|
||||
<div className="text-gray-600 dark:text-gray-400 capitalize text-sm mb-2">{key.replace(/_/g, ' ')}</div>
|
||||
<div className="font-bold text-2xl text-slate-900 dark:text-white/90">{value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-800 max-w-xl">
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
<strong>Note:</strong> Automation ends when content reaches draft status with all images generated.
|
||||
Please review content quality, accuracy, and brand voice manually before publishing to WordPress.
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5 text-xs mb-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Total Queue:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{stage.pending}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Processed:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{processed}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Remaining:</span>
|
||||
<span className={`font-bold ${stageConfig.textColor}`}>
|
||||
{stage.pending}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(isActive || isComplete || processed > 0) && (
|
||||
<div className="mt-3 pt-3 border-t border-slate-200 dark:border-gray-700">
|
||||
<div className="flex justify-between text-xs text-gray-600 dark:text-gray-400 mb-1.5">
|
||||
<span>Progress</span>
|
||||
<span>{isComplete ? '100' : progressPercent}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className={`bg-gradient-to-r ${stageConfig.color} h-2 rounded-full transition-all duration-500 ${isActive ? 'animate-pulse' : ''}`}
|
||||
style={{ width: `${isComplete ? 100 : progressPercent}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 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 (
|
||||
<div
|
||||
className={`
|
||||
relative rounded-xl border-3 p-5 transition-all
|
||||
${isActive
|
||||
? 'border-amber-500 bg-amber-50 dark:bg-amber-500/10 shadow-lg'
|
||||
: isComplete
|
||||
? 'border-success-500 bg-success-50 dark:bg-success-500/10'
|
||||
: stage7.pending > 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'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="text-sm font-bold text-gray-900 dark:text-white">Stage 7</div>
|
||||
<span className="text-xs px-2 py-0.5 bg-amber-500 text-white rounded-full">🚫 Stop</span>
|
||||
</div>
|
||||
<div className="text-xs font-medium text-amber-700 dark:text-amber-300">Manual Review Gate</div>
|
||||
</div>
|
||||
<div className="size-8 rounded-lg bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center shadow-md">
|
||||
<PaperPlaneIcon className="size-4 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stage7.pending > 0 && (
|
||||
<div className="text-center py-4">
|
||||
<div className="text-3xl font-bold text-amber-600 dark:text-amber-400">{stage7.pending}</div>
|
||||
<div className="text-xs text-amber-700 dark:text-amber-300 mt-1">ready for review</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 pt-3 border-t border-amber-200 dark:border-amber-700">
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
size="sm"
|
||||
className="w-full text-xs"
|
||||
disabled={stage7.pending === 0}
|
||||
>
|
||||
Go to Review →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Status Summary Card */}
|
||||
{currentRun && (
|
||||
<div className="relative rounded-xl border-2 border-slate-300 dark:border-gray-700 p-5 bg-gradient-to-br from-slate-100 to-slate-200 dark:from-gray-800/50 dark:to-gray-700/50">
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-bold text-gray-900 dark:text-white mb-1">Current Status</div>
|
||||
<div className="text-xs font-medium text-gray-600 dark:text-gray-400">Run Summary</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Run ID:</span>
|
||||
<span className="font-mono text-xs text-slate-900 dark:text-white">{currentRun.run_id.split('_').pop()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Started:</span>
|
||||
<span className="font-semibold text-slate-900 dark:text-white">
|
||||
{new Date(currentRun.started_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Current Stage:</span>
|
||||
<span className="font-bold text-blue-600 dark:text-blue-400">{currentRun.current_stage}/7</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Credits Used:</span>
|
||||
<span className="font-bold text-brand-600 dark:text-brand-400">{currentRun.total_credits_used}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Completion:</span>
|
||||
<span className="font-bold text-slate-900 dark:text-white">{Math.round((currentRun.current_stage / 7) * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-3 border-t border-slate-300 dark:border-gray-600">
|
||||
<div className={`
|
||||
size-12 mx-auto rounded-full flex items-center justify-center
|
||||
${currentRun.status === 'running'
|
||||
? 'bg-gradient-to-br from-blue-500 to-blue-600 animate-pulse'
|
||||
: currentRun.status === 'paused'
|
||||
? 'bg-gradient-to-br from-amber-500 to-amber-600'
|
||||
: 'bg-gradient-to-br from-success-500 to-success-600'
|
||||
}
|
||||
`}>
|
||||
{currentRun.status === 'running' && <div className="size-3 bg-white rounded-full"></div>}
|
||||
{currentRun.status === 'paused' && <ClockIcon className="size-6 text-white" />}
|
||||
{currentRun.status === 'completed' && <CheckCircleIcon className="size-6 text-white" />}
|
||||
</div>
|
||||
<div className="text-center mt-2 text-xs font-semibold text-gray-700 dark:text-gray-300 capitalize">
|
||||
{currentRun.status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Current Run Details */}
|
||||
|
||||
@@ -1,411 +1,43 @@
|
||||
/**
|
||||
* Deployment Panel
|
||||
* Stage 4: Deployment readiness and publishing
|
||||
* Deployment Panel - DEPRECATED
|
||||
*
|
||||
* Displays readiness checklist and deploy/rollback controls
|
||||
* Legacy SiteBlueprint deployment functionality has been removed.
|
||||
* Use WordPress integration sync and publishing features instead.
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Button from '../../components/ui/button/Button';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ErrorIcon,
|
||||
AlertIcon,
|
||||
BoltIcon,
|
||||
ArrowRightIcon,
|
||||
FileIcon,
|
||||
BoxIcon,
|
||||
CheckLineIcon,
|
||||
GridIcon
|
||||
} from '../../icons';
|
||||
import {
|
||||
fetchDeploymentReadiness,
|
||||
// fetchSiteBlueprints,
|
||||
DeploymentReadiness,
|
||||
} from '../../services/api';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
import { AlertIcon } from '../../icons';
|
||||
|
||||
export default function DeploymentPanel() {
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [readiness, setReadiness] = useState<DeploymentReadiness | null>(null);
|
||||
const [blueprints, setBlueprints] = useState<any[]>([]);
|
||||
const [selectedBlueprintId, setSelectedBlueprintId] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deploying, setDeploying] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (siteId) {
|
||||
loadData();
|
||||
}
|
||||
}, [siteId]);
|
||||
|
||||
const loadData = async () => {
|
||||
if (!siteId) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
// const blueprintsData = await fetchSiteBlueprints({ site_id: Number(siteId) });
|
||||
const blueprintsData = null;
|
||||
if (blueprintsData?.results && blueprintsData.results.length > 0) {
|
||||
setBlueprints(blueprintsData.results);
|
||||
const firstBlueprint = blueprintsData.results[0];
|
||||
setSelectedBlueprintId(firstBlueprint.id);
|
||||
await loadReadiness(firstBlueprint.id);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to load deployment data: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadReadiness = async (blueprintId: number) => {
|
||||
try {
|
||||
const readinessData = await fetchDeploymentReadiness(blueprintId);
|
||||
setReadiness(readinessData);
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to load readiness: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedBlueprintId) {
|
||||
loadReadiness(selectedBlueprintId);
|
||||
}
|
||||
}, [selectedBlueprintId]);
|
||||
|
||||
const handleDeploy = async () => {
|
||||
if (!selectedBlueprintId) return;
|
||||
try {
|
||||
setDeploying(true);
|
||||
const result = await fetchAPI(`/v1/publisher/deploy/${selectedBlueprintId}/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ check_readiness: true }),
|
||||
});
|
||||
toast.success('Deployment initiated successfully');
|
||||
await loadReadiness(selectedBlueprintId); // Refresh readiness
|
||||
} catch (error: any) {
|
||||
toast.error(`Deployment failed: ${error.message}`);
|
||||
} finally {
|
||||
setDeploying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRollback = async () => {
|
||||
if (!selectedBlueprintId) return;
|
||||
try {
|
||||
// TODO: Implement rollback endpoint
|
||||
toast.info('Rollback functionality coming soon');
|
||||
} catch (error: any) {
|
||||
toast.error(`Rollback failed: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getCheckIcon = (passed: boolean) => {
|
||||
return passed ? (
|
||||
<CheckCircleIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||
) : (
|
||||
<ErrorIcon className="w-5 h-5 text-red-600 dark:text-red-400" />
|
||||
);
|
||||
};
|
||||
|
||||
const getCheckBadge = (passed: boolean) => {
|
||||
return (
|
||||
<Badge color={passed ? 'success' : 'error'} size="sm">
|
||||
{passed ? 'Pass' : 'Fail'}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Deployment Panel" />
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500">Loading deployment data...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (blueprints.length === 0) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Deployment Panel" />
|
||||
<Card className="p-6">
|
||||
<div className="text-center py-8">
|
||||
<AlertIcon className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-2">No blueprints found for this site</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => navigate(`/sites/${siteId}/builder`)}
|
||||
>
|
||||
Create Blueprint
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedBlueprint = blueprints.find((b) => b.id === selectedBlueprintId);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Deployment Panel" />
|
||||
|
||||
<PageHeader
|
||||
<div className="space-y-6">
|
||||
<PageMeta
|
||||
title="Deployment Panel"
|
||||
badge={{ icon: <BoltIcon />, color: 'orange' }}
|
||||
hideSiteSector
|
||||
description="Legacy deployment features"
|
||||
/>
|
||||
<div className="mb-6 flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate(`/sites/${siteId}`)}
|
||||
>
|
||||
Back to Dashboard
|
||||
<PageHeader
|
||||
title="Deployment Panel"
|
||||
subtitle="This feature has been deprecated"
|
||||
backLink="../"
|
||||
/>
|
||||
|
||||
<Card className="p-8 text-center">
|
||||
<AlertIcon className="w-16 h-16 text-amber-500 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold mb-2">Feature Deprecated</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||
The SiteBlueprint deployment system has been removed.
|
||||
Please use WordPress integration sync features for publishing content.
|
||||
</p>
|
||||
<Button onClick={() => navigate('../')} variant="primary">
|
||||
Return to Sites
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRollback}
|
||||
disabled={!selectedBlueprintId}
|
||||
>
|
||||
<ArrowRightIcon className="w-4 h-4 mr-2 rotate-180" />
|
||||
Rollback
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleDeploy}
|
||||
disabled={deploying || !readiness?.ready || !selectedBlueprintId}
|
||||
>
|
||||
<BoltIcon className={`w-4 h-4 mr-2 ${deploying ? 'animate-pulse' : ''}`} />
|
||||
{deploying ? 'Deploying...' : 'Deploy'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Blueprint Selector */}
|
||||
{blueprints.length > 1 && (
|
||||
<Card className="p-4 mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Blueprint
|
||||
</label>
|
||||
<select
|
||||
value={selectedBlueprintId || ''}
|
||||
onChange={(e) => setSelectedBlueprintId(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
>
|
||||
{blueprints.map((bp) => (
|
||||
<option key={bp.id} value={bp.id}>
|
||||
{bp.name} ({bp.status})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{selectedBlueprint && (
|
||||
<Card className="p-4 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{selectedBlueprint.name}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{selectedBlueprint.description || 'No description'}
|
||||
</p>
|
||||
</div>
|
||||
<Badge color={selectedBlueprint.status === 'active' ? 'success' : 'info'} size="md">
|
||||
{selectedBlueprint.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Overall Readiness Status */}
|
||||
{readiness && (
|
||||
<>
|
||||
<Card className="p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Deployment Readiness
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{getCheckIcon(readiness.ready)}
|
||||
<Badge color={readiness.ready ? 'success' : 'error'} size="md">
|
||||
{readiness.ready ? 'Ready' : 'Not Ready'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Errors */}
|
||||
{readiness.errors.length > 0 && (
|
||||
<div className="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<h3 className="text-sm font-semibold text-red-800 dark:text-red-300 mb-2">
|
||||
Blocking Issues
|
||||
</h3>
|
||||
<ul className="space-y-1">
|
||||
{readiness.errors.map((error, idx) => (
|
||||
<li key={idx} className="text-sm text-red-700 dark:text-red-400">
|
||||
• {error}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warnings */}
|
||||
{readiness.warnings.length > 0 && (
|
||||
<div className="mb-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<h3 className="text-sm font-semibold text-yellow-800 dark:text-yellow-300 mb-2">
|
||||
Warnings
|
||||
</h3>
|
||||
<ul className="space-y-1">
|
||||
{readiness.warnings.map((warning, idx) => (
|
||||
<li key={idx} className="text-sm text-yellow-700 dark:text-yellow-400">
|
||||
• {warning}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Readiness Checks */}
|
||||
<div className="space-y-4">
|
||||
{/* Cluster Coverage */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<GridIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
<h3 className="font-medium text-gray-900 dark:text-white">
|
||||
Cluster Coverage
|
||||
</h3>
|
||||
</div>
|
||||
{getCheckBadge(readiness.checks.cluster_coverage)}
|
||||
</div>
|
||||
{readiness.details.cluster_coverage && (
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
<p>
|
||||
{readiness.details.cluster_coverage.covered_clusters} /{' '}
|
||||
{readiness.details.cluster_coverage.total_clusters} clusters covered
|
||||
</p>
|
||||
{readiness.details.cluster_coverage.incomplete_clusters.length > 0 && (
|
||||
<p className="mt-1 text-yellow-600 dark:text-yellow-400">
|
||||
{readiness.details.cluster_coverage.incomplete_clusters.length} incomplete
|
||||
cluster(s)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Validation */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckLineIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
<h3 className="font-medium text-gray-900 dark:text-white">
|
||||
Content Validation
|
||||
</h3>
|
||||
</div>
|
||||
{getCheckBadge(readiness.checks.content_validation)}
|
||||
</div>
|
||||
{readiness.details.content_validation && (
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
<p>
|
||||
{readiness.details.content_validation.valid_content} /{' '}
|
||||
{readiness.details.content_validation.total_content} content items valid
|
||||
</p>
|
||||
{readiness.details.content_validation.invalid_content.length > 0 && (
|
||||
<p className="mt-1 text-red-600 dark:text-red-400">
|
||||
{readiness.details.content_validation.invalid_content.length} invalid
|
||||
content item(s)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Taxonomy Completeness */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<BoxIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
<h3 className="font-medium text-gray-900 dark:text-white">
|
||||
Taxonomy Completeness
|
||||
</h3>
|
||||
</div>
|
||||
{getCheckBadge(readiness.checks.taxonomy_completeness)}
|
||||
</div>
|
||||
{readiness.details.taxonomy_completeness && (
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
<p>
|
||||
{readiness.details.taxonomy_completeness.total_taxonomies} taxonomies
|
||||
defined
|
||||
</p>
|
||||
{readiness.details.taxonomy_completeness.missing_taxonomies.length > 0 && (
|
||||
<p className="mt-1 text-yellow-600 dark:text-yellow-400">
|
||||
Missing: {readiness.details.taxonomy_completeness.missing_taxonomies.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sync Status */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<BoltIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
<h3 className="font-medium text-gray-900 dark:text-white">Sync Status</h3>
|
||||
</div>
|
||||
{getCheckBadge(readiness.checks.sync_status)}
|
||||
</div>
|
||||
{readiness.details.sync_status && (
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
<p>
|
||||
{readiness.details.sync_status.has_integration
|
||||
? 'Integration configured'
|
||||
: 'No integration configured'}
|
||||
</p>
|
||||
{readiness.details.sync_status.mismatch_count > 0 && (
|
||||
<p className="mt-1 text-yellow-600 dark:text-yellow-400">
|
||||
{readiness.details.sync_status.mismatch_count} sync mismatch(es) detected
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => loadReadiness(selectedBlueprintId!)}
|
||||
startIcon={<BoltIcon className="w-4 h-4" />}
|
||||
>
|
||||
Refresh Checks
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleDeploy}
|
||||
disabled={deploying || !readiness.ready}
|
||||
>
|
||||
<BoltIcon className={`w-4 h-4 mr-2 ${deploying ? 'animate-pulse' : ''}`} />
|
||||
{deploying ? 'Deploying...' : 'Deploy Now'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,210 +1,43 @@
|
||||
/**
|
||||
* Site Content Editor
|
||||
* Phase 6: Site Integration & Multi-Destination Publishing
|
||||
* Core CMS features: View all pages/posts, edit page content
|
||||
* Site Editor - DEPRECATED
|
||||
*
|
||||
* Legacy SiteBlueprint page editor has been removed.
|
||||
* Use Writer module for content creation and editing.
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { EditIcon, EyeIcon, FileTextIcon } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Button from '../../components/ui/button/Button';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
import { AlertIcon } from '../../icons';
|
||||
|
||||
interface PageBlueprint {
|
||||
id: number;
|
||||
slug: string;
|
||||
title: string;
|
||||
type: string;
|
||||
status: string;
|
||||
order: number;
|
||||
blocks_json: any[];
|
||||
site_blueprint: number;
|
||||
}
|
||||
|
||||
interface SiteBlueprint {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export default function SiteContentEditor() {
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
export default function Editor() {
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [blueprints, setBlueprints] = useState<SiteBlueprint[]>([]);
|
||||
const [selectedBlueprint, setSelectedBlueprint] = useState<number | null>(null);
|
||||
const [pages, setPages] = useState<PageBlueprint[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedPage, setSelectedPage] = useState<PageBlueprint | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (siteId) {
|
||||
loadBlueprints();
|
||||
}
|
||||
}, [siteId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedBlueprint) {
|
||||
loadPages(selectedBlueprint);
|
||||
}
|
||||
}, [selectedBlueprint]);
|
||||
|
||||
const loadBlueprints = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchAPI(`/v1/site-builder/blueprints/?site=${siteId}`);
|
||||
const blueprintsList = Array.isArray(data?.results) ? data.results : Array.isArray(data) ? data : [];
|
||||
setBlueprints(blueprintsList);
|
||||
if (blueprintsList.length > 0) {
|
||||
setSelectedBlueprint(blueprintsList[0].id);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to load blueprints: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadPages = async (blueprintId: number) => {
|
||||
try {
|
||||
const data = await fetchAPI(`/v1/site-builder/pages/?site_blueprint=${blueprintId}`);
|
||||
const pagesList = Array.isArray(data?.results) ? data.results : Array.isArray(data) ? data : [];
|
||||
setPages(pagesList.sort((a, b) => a.order - b.order));
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to load pages: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditPage = (page: PageBlueprint) => {
|
||||
navigate(`/sites/${siteId}/pages/${page.id}/edit`);
|
||||
};
|
||||
|
||||
const handleViewPage = (page: PageBlueprint) => {
|
||||
navigate(`/sites/${siteId}/pages/${page.id}`);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Site Content Editor" />
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500">Loading pages...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Site Content Editor - IGNY8" />
|
||||
<div className="space-y-6">
|
||||
<PageMeta
|
||||
title="Site Editor"
|
||||
description="Legacy site editor features"
|
||||
/>
|
||||
<PageHeader
|
||||
title="Site Editor"
|
||||
subtitle="This feature has been deprecated"
|
||||
backLink="../"
|
||||
/>
|
||||
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Site Content Editor
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
View and edit content for site pages
|
||||
<Card className="p-8 text-center">
|
||||
<AlertIcon className="w-16 h-16 text-amber-500 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold mb-2">Feature Deprecated</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||
The SiteBlueprint page editor has been removed.
|
||||
Please use the Writer module to create and edit content.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{blueprints.length === 0 ? (
|
||||
<Card className="p-12 text-center">
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||
No site blueprints found for this site
|
||||
</p>
|
||||
<Button onClick={() => navigate('/sites/builder')} variant="primary">
|
||||
Create Site Blueprint
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{blueprints.length > 1 && (
|
||||
<Card className="p-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Blueprint
|
||||
</label>
|
||||
<select
|
||||
value={selectedBlueprint || ''}
|
||||
onChange={(e) => setSelectedBlueprint(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
>
|
||||
{blueprints.map((bp) => (
|
||||
<option key={bp.id} value={bp.id}>
|
||||
{bp.name} ({bp.status})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{pages.length === 0 ? (
|
||||
<Card className="p-12 text-center">
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||
No pages found in this blueprint
|
||||
</p>
|
||||
<Button onClick={() => navigate('/sites/builder')} variant="primary">
|
||||
Generate Pages
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="p-6">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Pages ({pages.length})
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{pages.map((page) => (
|
||||
<div
|
||||
key={page.id}
|
||||
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<FileTextIcon className="w-5 h-5 text-gray-400" />
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||
{page.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
/{page.slug} • {page.type} • {page.status}
|
||||
</p>
|
||||
{page.blocks_json && page.blocks_json.length > 0 && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
||||
{page.blocks_json.length} block{page.blocks_json.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleViewPage(page)}
|
||||
title="View"
|
||||
>
|
||||
<EyeIcon className="w-4 h-4 mr-1" />
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => handleEditPage(page)}
|
||||
title="Edit"
|
||||
>
|
||||
<EditIcon className="w-4 h-4 mr-1" />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button onClick={() => navigate('../')} variant="primary">
|
||||
Return to Sites
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2432,121 +2432,9 @@ export interface DeploymentReadiness {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchDeploymentReadiness(blueprintId: number): Promise<DeploymentReadiness> {
|
||||
return fetchAPI(`/v1/publisher/blueprints/${blueprintId}/readiness/`);
|
||||
}
|
||||
// Legacy: Site Builder API removed
|
||||
// SiteBlueprint, PageBlueprint, and related functions deprecated
|
||||
|
||||
export async function createSiteBlueprint(data: Partial<SiteBlueprint>): Promise<SiteBlueprint> {
|
||||
return fetchAPI('/v1/site-builder/blueprints/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSiteBlueprint(id: number, data: Partial<SiteBlueprint>): Promise<SiteBlueprint> {
|
||||
return fetchAPI(`/v1/site-builder/blueprints/${id}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Cluster attachment endpoints
|
||||
export async function attachClustersToBlueprint(
|
||||
blueprintId: number,
|
||||
clusterIds: number[],
|
||||
role: 'hub' | 'supporting' | 'attribute' = 'hub'
|
||||
): Promise<{ attached_count: number; clusters: Array<{ id: number; name: string; role: string; link_id: number }> }> {
|
||||
return fetchAPI(`/v1/site-builder/blueprints/${blueprintId}/clusters/attach/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cluster_ids: clusterIds, role }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function detachClustersFromBlueprint(
|
||||
blueprintId: number,
|
||||
clusterIds?: number[],
|
||||
role?: 'hub' | 'supporting' | 'attribute'
|
||||
): Promise<{ detached_count: number }> {
|
||||
return fetchAPI(`/v1/site-builder/blueprints/${blueprintId}/clusters/detach/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cluster_ids: clusterIds, role }),
|
||||
});
|
||||
}
|
||||
|
||||
// Taxonomy endpoints
|
||||
export interface Taxonomy {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
taxonomy_type: 'blog_category' | 'blog_tag' | 'product_category' | 'product_tag' | 'product_attribute' | 'service_category';
|
||||
description?: string;
|
||||
cluster_ids: number[];
|
||||
external_reference?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface TaxonomyCreateData {
|
||||
name: string;
|
||||
slug: string;
|
||||
taxonomy_type: 'blog_category' | 'blog_tag' | 'product_category' | 'product_tag' | 'product_attribute' | 'service_category';
|
||||
description?: string;
|
||||
cluster_ids?: number[];
|
||||
external_reference?: string;
|
||||
}
|
||||
|
||||
export interface TaxonomyImportRecord {
|
||||
name: string;
|
||||
slug: string;
|
||||
taxonomy_type?: string;
|
||||
description?: string;
|
||||
external_reference?: string;
|
||||
}
|
||||
|
||||
export async function fetchBlueprintsTaxonomies(blueprintId: number): Promise<{ count: number; taxonomies: Taxonomy[] }> {
|
||||
return fetchAPI(`/v1/site-builder/blueprints/${blueprintId}/taxonomies/`);
|
||||
}
|
||||
|
||||
export async function createBlueprintTaxonomy(
|
||||
blueprintId: number,
|
||||
data: TaxonomyCreateData
|
||||
): Promise<Taxonomy> {
|
||||
return fetchAPI(`/v1/site-builder/blueprints/${blueprintId}/taxonomies/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function importBlueprintsTaxonomies(
|
||||
blueprintId: number,
|
||||
records: TaxonomyImportRecord[],
|
||||
defaultType: string = 'blog_category'
|
||||
): Promise<{ imported_count: number; taxonomies: Taxonomy[] }> {
|
||||
return fetchAPI(`/v1/site-builder/blueprints/${blueprintId}/taxonomies/import/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ records, default_type: defaultType }),
|
||||
});
|
||||
}
|
||||
|
||||
// Page blueprint endpoints
|
||||
export async function updatePageBlueprint(
|
||||
pageId: number,
|
||||
data: Partial<PageBlueprint>
|
||||
): Promise<PageBlueprint> {
|
||||
return fetchAPI(`/v1/site-builder/pages/${pageId}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function regeneratePageBlueprint(
|
||||
pageId: number
|
||||
): Promise<{ success: boolean; task_id?: string }> {
|
||||
return fetchAPI(`/v1/site-builder/pages/${pageId}/regenerate/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function generatePageContent(
|
||||
pageId: number,
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface AutomationConfig {
|
||||
stage_4_batch_size: number;
|
||||
stage_5_batch_size: number;
|
||||
stage_6_batch_size: number;
|
||||
within_stage_delay: number;
|
||||
between_stage_delay: number;
|
||||
last_run_at: string | null;
|
||||
next_run_at: string | null;
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
import type {
|
||||
PageBlueprint,
|
||||
SiteStructure,
|
||||
} from "../types/siteBuilder";
|
||||
|
||||
interface SiteDefinitionState {
|
||||
structure?: SiteStructure;
|
||||
pages: PageBlueprint[];
|
||||
selectedSlug?: string;
|
||||
setStructure: (structure: SiteStructure) => void;
|
||||
setPages: (pages: PageBlueprint[]) => void;
|
||||
selectPage: (slug: string) => void;
|
||||
}
|
||||
|
||||
export const useSiteDefinitionStore = create<SiteDefinitionState>((set) => ({
|
||||
pages: [],
|
||||
setStructure: (structure) =>
|
||||
set({
|
||||
structure,
|
||||
selectedSlug: structure.pages?.[0]?.slug,
|
||||
}),
|
||||
setPages: (pages) =>
|
||||
set((state) => ({
|
||||
pages,
|
||||
selectedSlug: state.selectedSlug ?? pages[0]?.slug,
|
||||
})),
|
||||
selectPage: (slug) => set({ selectedSlug: slug }),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user