automation fixes
This commit is contained in:
@@ -140,7 +140,7 @@ class AutomationLogger:
|
|||||||
def _get_stage_log_path(self, account_id: int, site_id: int, run_id: str, stage_number: int) -> str:
|
def _get_stage_log_path(self, account_id: int, site_id: int, run_id: str, stage_number: int) -> str:
|
||||||
"""Get stage log file path"""
|
"""Get stage log file path"""
|
||||||
run_dir = self._get_run_dir(account_id, site_id, run_id)
|
run_dir = self._get_run_dir(account_id, site_id, run_id)
|
||||||
return os.path.join(run_dir, f'stage_{stage_number}.log')
|
return os.path.join(run_dir, f'stage_{str(stage_number)}.log')
|
||||||
|
|
||||||
def _append_to_main_log(self, account_id: int, site_id: int, run_id: str, message: str):
|
def _append_to_main_log(self, account_id: int, site_id: int, run_id: str, message: str):
|
||||||
"""Append message to main log file"""
|
"""Append message to main log file"""
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from igny8_core.auth.models import Account, Site
|
|||||||
from igny8_core.modules.planner.models import Keywords, Clusters, ContentIdeas
|
from igny8_core.modules.planner.models import Keywords, Clusters, ContentIdeas
|
||||||
from igny8_core.modules.writer.models import Tasks, Content, Images
|
from igny8_core.modules.writer.models import Tasks, Content, Images
|
||||||
from igny8_core.ai.models import AITaskLog
|
from igny8_core.ai.models import AITaskLog
|
||||||
|
from igny8_core.ai.engine import AIEngine
|
||||||
|
|
||||||
# AI Functions
|
# AI Functions
|
||||||
from igny8_core.ai.functions.auto_cluster import AutoClusterFunction
|
from igny8_core.ai.functions.auto_cluster import AutoClusterFunction
|
||||||
@@ -79,8 +80,8 @@ class AutomationService:
|
|||||||
|
|
||||||
# Check credit balance (with 20% buffer)
|
# Check credit balance (with 20% buffer)
|
||||||
required_credits = int(estimated_credits * 1.2)
|
required_credits = int(estimated_credits * 1.2)
|
||||||
if self.account.credits_balance < required_credits:
|
if self.account.credits < required_credits:
|
||||||
raise ValueError(f"Insufficient credits. Need ~{required_credits}, you have {self.account.credits_balance}")
|
raise ValueError(f"Insufficient credits. Need ~{required_credits}, you have {self.account.credits}")
|
||||||
|
|
||||||
# Create run_id and log files
|
# Create run_id and log files
|
||||||
run_id = self.logger.start_run(self.account.id, self.site.id, trigger_type)
|
run_id = self.logger.start_run(self.account.id, self.site.id, trigger_type)
|
||||||
@@ -102,7 +103,7 @@ class AutomationService:
|
|||||||
)
|
)
|
||||||
self.logger.log_stage_progress(
|
self.logger.log_stage_progress(
|
||||||
run_id, self.account.id, self.site.id, 0,
|
run_id, self.account.id, self.site.id, 0,
|
||||||
f"Credit check: Account has {self.account.credits_balance} credits, estimated need: {estimated_credits} credits"
|
f"Credit check: Account has {self.account.credits} credits, estimated need: {estimated_credits} credits"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"[AutomationService] Started run: {run_id}")
|
logger.info(f"[AutomationService] Started run: {run_id}")
|
||||||
@@ -164,10 +165,11 @@ class AutomationService:
|
|||||||
stage_number, f"Processing batch {batch_num}/{total_batches} ({len(batch)} keywords)"
|
stage_number, f"Processing batch {batch_num}/{total_batches} ({len(batch)} keywords)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Call AI function
|
# Call AI function via AIEngine
|
||||||
result = AutoClusterFunction().execute(
|
engine = AIEngine(account=self.account)
|
||||||
payload={'ids': batch},
|
result = engine.execute(
|
||||||
account=self.account
|
fn=AutoClusterFunction(),
|
||||||
|
payload={'ids': batch}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Monitor task
|
# Monitor task
|
||||||
@@ -258,10 +260,11 @@ class AutomationService:
|
|||||||
stage_number, f"Generating ideas for cluster: {cluster.name}"
|
stage_number, f"Generating ideas for cluster: {cluster.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Call AI function
|
# Call AI function via AIEngine
|
||||||
result = GenerateIdeasFunction().execute(
|
engine = AIEngine(account=self.account)
|
||||||
payload={'ids': [cluster.id]},
|
result = engine.execute(
|
||||||
account=self.account
|
fn=GenerateIdeasFunction(),
|
||||||
|
payload={'ids': [cluster.id]}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Monitor task
|
# Monitor task
|
||||||
@@ -418,11 +421,10 @@ class AutomationService:
|
|||||||
stage_name = "Tasks → Content (AI)"
|
stage_name = "Tasks → Content (AI)"
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# Query queued tasks
|
# Query queued tasks (all queued tasks need content generated)
|
||||||
pending_tasks = Tasks.objects.filter(
|
pending_tasks = Tasks.objects.filter(
|
||||||
site=self.site,
|
site=self.site,
|
||||||
status='queued',
|
status='queued'
|
||||||
content__isnull=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
total_count = pending_tasks.count()
|
total_count = pending_tasks.count()
|
||||||
@@ -453,10 +455,11 @@ class AutomationService:
|
|||||||
stage_number, f"Generating content for task: {task.title}"
|
stage_number, f"Generating content for task: {task.title}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Call AI function
|
# Call AI function via AIEngine
|
||||||
result = GenerateContentFunction().execute(
|
engine = AIEngine(account=self.account)
|
||||||
payload={'ids': [task.id]},
|
result = engine.execute(
|
||||||
account=self.account
|
fn=GenerateContentFunction(),
|
||||||
|
payload={'ids': [task.id]}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Monitor task
|
# Monitor task
|
||||||
@@ -551,10 +554,11 @@ class AutomationService:
|
|||||||
stage_number, f"Extracting prompts from: {content.title}"
|
stage_number, f"Extracting prompts from: {content.title}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Call AI function
|
# Call AI function via AIEngine
|
||||||
result = GenerateImagePromptsFunction().execute(
|
engine = AIEngine(account=self.account)
|
||||||
payload={'ids': [content.id]},
|
result = engine.execute(
|
||||||
account=self.account
|
fn=GenerateImagePromptsFunction(),
|
||||||
|
payload={'ids': [content.id]}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Monitor task
|
# Monitor task
|
||||||
@@ -641,10 +645,11 @@ class AutomationService:
|
|||||||
stage_number, f"Generating image: {image.image_type} for '{content_title}'"
|
stage_number, f"Generating image: {image.image_type} for '{content_title}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Call AI function
|
# Call AI function via AIEngine
|
||||||
result = GenerateImagesFunction().execute(
|
engine = AIEngine(account=self.account)
|
||||||
payload={'image_ids': [image.id]},
|
result = engine.execute(
|
||||||
account=self.account
|
fn=GenerateImagesFunction(),
|
||||||
|
payload={'image_ids': [image.id]}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Monitor task
|
# Monitor task
|
||||||
|
|||||||
@@ -311,3 +311,117 @@ class AutomationViewSet(viewsets.ViewSet):
|
|||||||
'current_balance': site.account.credits,
|
'current_balance': site.account.credits,
|
||||||
'sufficient': site.account.credits >= (estimated_credits * 1.2)
|
'sufficient': site.account.credits >= (estimated_credits * 1.2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def pipeline_overview(self, request):
|
||||||
|
"""
|
||||||
|
GET /api/v1/automation/pipeline_overview/?site_id=123
|
||||||
|
Get pipeline overview with pending counts for all stages
|
||||||
|
"""
|
||||||
|
site, error_response = self._get_site(request)
|
||||||
|
if error_response:
|
||||||
|
return error_response
|
||||||
|
|
||||||
|
from igny8_core.business.planning.models import Keywords, Clusters, ContentIdeas
|
||||||
|
from igny8_core.business.content.models import Tasks, Content, Images
|
||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
|
# Stage 1: Keywords pending clustering
|
||||||
|
stage_1_pending = Keywords.objects.filter(
|
||||||
|
site=site,
|
||||||
|
status='new',
|
||||||
|
cluster__isnull=True,
|
||||||
|
disabled=False
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Stage 2: Clusters needing ideas
|
||||||
|
stage_2_pending = Clusters.objects.filter(
|
||||||
|
site=site,
|
||||||
|
status='new',
|
||||||
|
disabled=False
|
||||||
|
).exclude(
|
||||||
|
ideas__isnull=False
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Stage 3: Ideas ready to queue
|
||||||
|
stage_3_pending = ContentIdeas.objects.filter(
|
||||||
|
site=site,
|
||||||
|
status='new'
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Stage 4: Tasks ready for content generation
|
||||||
|
# Tasks don't have content FK - check if content exists via task title matching
|
||||||
|
stage_4_pending = Tasks.objects.filter(
|
||||||
|
site=site,
|
||||||
|
status='queued'
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Stage 5: Content ready for image prompts
|
||||||
|
stage_5_pending = Content.objects.filter(
|
||||||
|
site=site,
|
||||||
|
status='draft'
|
||||||
|
).annotate(
|
||||||
|
images_count=Count('images')
|
||||||
|
).filter(
|
||||||
|
images_count=0
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Stage 6: Image prompts ready for generation
|
||||||
|
stage_6_pending = Images.objects.filter(
|
||||||
|
site=site,
|
||||||
|
status='pending'
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Stage 7: Content ready for review
|
||||||
|
stage_7_ready = Content.objects.filter(
|
||||||
|
site=site,
|
||||||
|
status='review'
|
||||||
|
).count()
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'stages': [
|
||||||
|
{
|
||||||
|
'number': 1,
|
||||||
|
'name': 'Keywords → Clusters',
|
||||||
|
'pending': stage_1_pending,
|
||||||
|
'type': 'AI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'number': 2,
|
||||||
|
'name': 'Clusters → Ideas',
|
||||||
|
'pending': stage_2_pending,
|
||||||
|
'type': 'AI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'number': 3,
|
||||||
|
'name': 'Ideas → Tasks',
|
||||||
|
'pending': stage_3_pending,
|
||||||
|
'type': 'Local'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'number': 4,
|
||||||
|
'name': 'Tasks → Content',
|
||||||
|
'pending': stage_4_pending,
|
||||||
|
'type': 'AI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'number': 5,
|
||||||
|
'name': 'Content → Image Prompts',
|
||||||
|
'pending': stage_5_pending,
|
||||||
|
'type': 'AI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'number': 6,
|
||||||
|
'name': 'Image Prompts → Images',
|
||||||
|
'pending': stage_6_pending,
|
||||||
|
'type': 'AI'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'number': 7,
|
||||||
|
'name': 'Manual Review Gate',
|
||||||
|
'pending': stage_7_ready,
|
||||||
|
'type': 'Manual'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,56 +3,88 @@
|
|||||||
* Shows status and results for each automation stage
|
* Shows status and results for each automation stage
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StageResult } from '../../services/automationService';
|
import { StageResult, PipelineStage } from '../../services/automationService';
|
||||||
|
|
||||||
interface StageCardProps {
|
interface StageCardProps {
|
||||||
stageNumber: number;
|
stageNumber: number;
|
||||||
stageName: string;
|
stageName: string;
|
||||||
currentStage: number;
|
currentStage?: number;
|
||||||
result: StageResult | null;
|
result?: StageResult | null;
|
||||||
|
pipelineData?: PipelineStage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StageCard: React.FC<StageCardProps> = ({
|
const StageCard: React.FC<StageCardProps> = ({
|
||||||
stageNumber,
|
stageNumber,
|
||||||
stageName,
|
stageName,
|
||||||
currentStage,
|
currentStage = 0,
|
||||||
result,
|
result = null,
|
||||||
|
pipelineData,
|
||||||
}) => {
|
}) => {
|
||||||
const isPending = stageNumber > currentStage;
|
const isPending = stageNumber > currentStage;
|
||||||
const isActive = stageNumber === currentStage;
|
const isActive = stageNumber === currentStage;
|
||||||
const isComplete = stageNumber < currentStage || (result !== null && stageNumber <= 7);
|
const isComplete = stageNumber < currentStage || (result !== null && stageNumber <= 7);
|
||||||
|
|
||||||
const getStatusColor = () => {
|
const getStatusColor = () => {
|
||||||
if (isActive) return 'border-blue-500 bg-blue-50';
|
if (isActive) return 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 dark:border-blue-400';
|
||||||
if (isComplete) return 'border-green-500 bg-green-50';
|
if (isComplete) return 'border-green-500 bg-green-50 dark:bg-green-900/20 dark:border-green-400';
|
||||||
return 'border-gray-300 bg-gray-50';
|
if (pipelineData && pipelineData.pending > 0) return 'border-purple-400 bg-purple-50 dark:bg-purple-900/20 dark:border-purple-400';
|
||||||
|
return 'border-gray-300 bg-gray-50 dark:bg-gray-800 dark:border-gray-600';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusIcon = () => {
|
const getStatusIcon = () => {
|
||||||
if (isActive) return '🔄';
|
if (isActive) return '🔄';
|
||||||
if (isComplete) return '✅';
|
if (isComplete) return '✅';
|
||||||
return '⏳';
|
if (pipelineData && pipelineData.pending > 0) return '📋';
|
||||||
|
return '⏸';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = () => {
|
||||||
|
if (isActive) return 'Processing';
|
||||||
|
if (isComplete) return 'Completed';
|
||||||
|
if (pipelineData && pipelineData.pending > 0) return 'Ready';
|
||||||
|
return 'Waiting';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`border-2 rounded-lg p-3 ${getStatusColor()}`}>
|
<div className={`border-2 rounded-lg p-3 transition-all ${getStatusColor()}`}>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="text-sm font-bold">Stage {stageNumber}</div>
|
<div className="text-sm font-bold text-gray-700 dark:text-gray-200">Stage {stageNumber}</div>
|
||||||
<div className="text-xl">{getStatusIcon()}</div>
|
<div className="text-xl">{getStatusIcon()}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-700 mb-2">{stageName}</div>
|
<div className="text-xs text-gray-700 dark:text-gray-300 mb-2 font-medium">{stageName}</div>
|
||||||
|
|
||||||
|
{/* Show pipeline pending counts when not running */}
|
||||||
|
{!isActive && !isComplete && pipelineData && (
|
||||||
|
<div className="mt-2 pt-2 border-t border-gray-200 dark:border-gray-600">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-xs text-gray-600 dark:text-gray-400">Pending:</span>
|
||||||
|
<span className="text-lg font-bold text-purple-600 dark:text-purple-400">{pipelineData.pending}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">{getStatusText()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show run results when stage has completed */}
|
||||||
{result && (
|
{result && (
|
||||||
<div className="text-xs space-y-1">
|
<div className="text-xs space-y-1 mt-2 pt-2 border-t border-gray-200 dark:border-gray-600">
|
||||||
{Object.entries(result).map(([key, value]) => (
|
{Object.entries(result).map(([key, value]) => (
|
||||||
<div key={key} className="flex justify-between">
|
<div key={key} className="flex justify-between">
|
||||||
<span className="text-gray-600">{key.replace(/_/g, ' ')}:</span>
|
<span className="text-gray-600 dark:text-gray-400">{key.replace(/_/g, ' ')}:</span>
|
||||||
<span className="font-semibold">{value}</span>
|
<span className="font-semibold text-gray-800 dark:text-gray-200">{value}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Show processing indicator when active */}
|
||||||
|
{isActive && !result && (
|
||||||
|
<div className="mt-2 pt-2 border-t border-blue-200 dark:border-blue-600">
|
||||||
|
<div className="text-xs text-blue-600 dark:text-blue-400 animate-pulse">Processing...</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StageCard;
|
export default StageCard;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import Badge from '../ui/badge/Badge';
|
import Badge from '../ui/badge/Badge';
|
||||||
import { fetchSiteProgress, SiteProgress } from '../../services/api';
|
// import { fetchSiteProgress, SiteProgress } from '../../services/api';
|
||||||
import { CheckCircleIcon, XCircleIcon, AlertCircleIcon, ArrowRightIcon } from 'lucide-react';
|
import { CheckCircleIcon, XCircleIcon, AlertCircleIcon, ArrowRightIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface SiteProgressWidgetProps {
|
interface SiteProgressWidgetProps {
|
||||||
|
|||||||
@@ -5,25 +5,36 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { useSiteStore } from '../../store/siteStore';
|
import { useSiteStore } from '../../store/siteStore';
|
||||||
import { automationService, AutomationRun, AutomationConfig } from '../../services/automationService';
|
import { automationService, AutomationRun, AutomationConfig, PipelineStage } from '../../services/automationService';
|
||||||
import StageCard from '../../components/Automation/StageCard';
|
|
||||||
import ActivityLog from '../../components/Automation/ActivityLog';
|
import ActivityLog from '../../components/Automation/ActivityLog';
|
||||||
import ConfigModal from '../../components/Automation/ConfigModal';
|
import ConfigModal from '../../components/Automation/ConfigModal';
|
||||||
import RunHistory from '../../components/Automation/RunHistory';
|
import RunHistory from '../../components/Automation/RunHistory';
|
||||||
import PageMeta from '../../components/common/PageMeta';
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
import ComponentCard from '../../components/common/ComponentCard';
|
import ComponentCard from '../../components/common/ComponentCard';
|
||||||
|
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
|
||||||
import DebugSiteSelector from '../../components/common/DebugSiteSelector';
|
import DebugSiteSelector from '../../components/common/DebugSiteSelector';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
import { BoltIcon } from '../../icons';
|
import {
|
||||||
|
BoltIcon,
|
||||||
|
ListIcon,
|
||||||
|
GroupIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
PencilIcon,
|
||||||
|
FileIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
ClockIcon,
|
||||||
|
PaperPlaneIcon,
|
||||||
|
ArrowRightIcon
|
||||||
|
} from '../../icons';
|
||||||
|
|
||||||
const STAGE_NAMES = [
|
const STAGE_CONFIG = [
|
||||||
'Keywords → Clusters',
|
{ icon: ListIcon, color: 'from-blue-500 to-blue-600', hoverColor: 'hover:border-blue-500', name: 'Keywords → Clusters' },
|
||||||
'Clusters → Ideas',
|
{ icon: GroupIcon, color: 'from-purple-500 to-purple-600', hoverColor: 'hover:border-purple-500', name: 'Clusters → Ideas' },
|
||||||
'Ideas → Tasks',
|
{ icon: CheckCircleIcon, color: 'from-indigo-500 to-indigo-600', hoverColor: 'hover:border-indigo-500', name: 'Ideas → Tasks' },
|
||||||
'Tasks → Content',
|
{ icon: PencilIcon, color: 'from-green-500 to-green-600', hoverColor: 'hover:border-green-500', name: 'Tasks → Content' },
|
||||||
'Content → Image Prompts',
|
{ icon: FileIcon, color: 'from-amber-500 to-amber-600', hoverColor: 'hover:border-amber-500', name: 'Content → Image Prompts' },
|
||||||
'Image Prompts → Images',
|
{ icon: FileTextIcon, color: 'from-pink-500 to-pink-600', hoverColor: 'hover:border-pink-500', name: 'Image Prompts → Images' },
|
||||||
'Manual Review Gate',
|
{ icon: PaperPlaneIcon, color: 'from-teal-500 to-teal-600', hoverColor: 'hover:border-teal-500', name: 'Manual Review Gate' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const AutomationPage: React.FC = () => {
|
const AutomationPage: React.FC = () => {
|
||||||
@@ -31,41 +42,40 @@ const AutomationPage: React.FC = () => {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [config, setConfig] = useState<AutomationConfig | null>(null);
|
const [config, setConfig] = useState<AutomationConfig | null>(null);
|
||||||
const [currentRun, setCurrentRun] = useState<AutomationRun | null>(null);
|
const [currentRun, setCurrentRun] = useState<AutomationRun | null>(null);
|
||||||
|
const [pipelineOverview, setPipelineOverview] = useState<PipelineStage[]>([]);
|
||||||
const [showConfigModal, setShowConfigModal] = useState(false);
|
const [showConfigModal, setShowConfigModal] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
|
|
||||||
const [estimate, setEstimate] = useState<{ estimated_credits: number; current_balance: number; sufficient: boolean } | null>(null);
|
const [estimate, setEstimate] = useState<{ estimated_credits: number; current_balance: number; sufficient: boolean } | null>(null);
|
||||||
|
|
||||||
// Poll for current run updates
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
// Poll every 5 seconds when run is active
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (currentRun && (currentRun.status === 'running' || currentRun.status === 'paused')) {
|
if (currentRun && (currentRun.status === 'running' || currentRun.status === 'paused')) {
|
||||||
loadCurrentRun();
|
loadCurrentRun();
|
||||||
|
} else {
|
||||||
|
loadPipelineOverview();
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [activeSite, currentRun?.status]);
|
}, [activeSite, currentRun?.status]);
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [configData, runData, estimateData] = await Promise.all([
|
const [configData, runData, estimateData, pipelineData] = await Promise.all([
|
||||||
automationService.getConfig(activeSite.id),
|
automationService.getConfig(activeSite.id),
|
||||||
automationService.getCurrentRun(activeSite.id),
|
automationService.getCurrentRun(activeSite.id),
|
||||||
automationService.estimate(activeSite.id),
|
automationService.estimate(activeSite.id),
|
||||||
|
automationService.getPipelineOverview(activeSite.id),
|
||||||
]);
|
]);
|
||||||
setConfig(configData);
|
setConfig(configData);
|
||||||
setCurrentRun(runData.run);
|
setCurrentRun(runData.run);
|
||||||
setEstimate(estimateData);
|
setEstimate(estimateData);
|
||||||
setLastUpdated(new Date());
|
setPipelineOverview(pipelineData.stages);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to load automation data');
|
toast.error('Failed to load automation data');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -76,7 +86,6 @@ const AutomationPage: React.FC = () => {
|
|||||||
|
|
||||||
const loadCurrentRun = async () => {
|
const loadCurrentRun = async () => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await automationService.getCurrentRun(activeSite.id);
|
const data = await automationService.getCurrentRun(activeSite.id);
|
||||||
setCurrentRun(data.run);
|
setCurrentRun(data.run);
|
||||||
@@ -85,18 +94,25 @@ const AutomationPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadPipelineOverview = async () => {
|
||||||
|
if (!activeSite) return;
|
||||||
|
try {
|
||||||
|
const data = await automationService.getPipelineOverview(activeSite.id);
|
||||||
|
setPipelineOverview(data.stages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to poll pipeline overview', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleRunNow = async () => {
|
const handleRunNow = async () => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
// Check credit balance
|
|
||||||
if (estimate && !estimate.sufficient) {
|
if (estimate && !estimate.sufficient) {
|
||||||
toast.error(`Insufficient credits. Need ~${estimate.estimated_credits}, you have ${estimate.current_balance}`);
|
toast.error(`Insufficient credits. Need ~${estimate.estimated_credits}, you have ${estimate.current_balance}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await automationService.runNow(activeSite.id);
|
const result = await automationService.runNow(activeSite.id);
|
||||||
toast.success('Automation started');
|
toast.success('Automation started successfully');
|
||||||
loadCurrentRun();
|
loadCurrentRun();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(error.response?.data?.error || 'Failed to start automation');
|
toast.error(error.response?.data?.error || 'Failed to start automation');
|
||||||
@@ -105,7 +121,6 @@ const AutomationPage: React.FC = () => {
|
|||||||
|
|
||||||
const handlePause = async () => {
|
const handlePause = async () => {
|
||||||
if (!currentRun) return;
|
if (!currentRun) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await automationService.pause(currentRun.run_id);
|
await automationService.pause(currentRun.run_id);
|
||||||
toast.success('Automation paused');
|
toast.success('Automation paused');
|
||||||
@@ -117,7 +132,6 @@ const AutomationPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleResume = async () => {
|
const handleResume = async () => {
|
||||||
if (!currentRun) return;
|
if (!currentRun) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await automationService.resume(currentRun.run_id);
|
await automationService.resume(currentRun.run_id);
|
||||||
toast.success('Automation resumed');
|
toast.success('Automation resumed');
|
||||||
@@ -129,7 +143,6 @@ const AutomationPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleSaveConfig = async (newConfig: Partial<AutomationConfig>) => {
|
const handleSaveConfig = async (newConfig: Partial<AutomationConfig>) => {
|
||||||
if (!activeSite) return;
|
if (!activeSite) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await automationService.updateConfig(activeSite.id, newConfig);
|
await automationService.updateConfig(activeSite.id, newConfig);
|
||||||
toast.success('Configuration saved');
|
toast.success('Configuration saved');
|
||||||
@@ -156,184 +169,469 @@ const AutomationPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalPending = pipelineOverview.reduce((sum, stage) => sum + stage.pending, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageMeta
|
<PageMeta title="AI Automation Pipeline | IGNY8" description="Automated content creation from keywords to published articles" />
|
||||||
title="AI Automation Pipeline | IGNY8"
|
|
||||||
description="Automated content creation from keywords to published articles"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Page Header with Site Selector (no sector) */}
|
{/* Header */}
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-purple-600 dark:bg-purple-500 flex-shrink-0">
|
<div className="flex items-center justify-center size-10 rounded-xl bg-gradient-to-br from-teal-500 to-teal-600">
|
||||||
<BoltIcon className="text-white size-5" />
|
<BoltIcon className="text-white size-5" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">AI Automation Pipeline</h2>
|
<div>
|
||||||
</div>
|
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">AI Automation Pipeline</h2>
|
||||||
{activeSite && (
|
{activeSite && (
|
||||||
<div className="flex items-center gap-3 mt-1">
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
{lastUpdated && (
|
Site: <span className="font-medium text-brand-600 dark:text-brand-400">{activeSite.name}</span>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Last updated: {lastUpdated.toLocaleTimeString()}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<span className="text-sm text-gray-400 dark:text-gray-600">•</span>
|
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
Site: <span className="text-brand-600 dark:text-brand-400">{activeSite.name}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<DebugSiteSelector />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Schedule Status Card */}
|
|
||||||
{config && (
|
|
||||||
<ComponentCard
|
|
||||||
title="Schedule & Status"
|
|
||||||
desc="Configure and monitor your automation schedule"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">Status</div>
|
|
||||||
<div className="font-semibold mt-1">
|
|
||||||
{config.is_enabled ? (
|
|
||||||
<span className="text-green-600 dark:text-green-400">● Enabled</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">○ Disabled</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">Schedule</div>
|
|
||||||
<div className="font-semibold mt-1 capitalize">
|
|
||||||
{config.frequency} at {config.scheduled_time}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">Last Run</div>
|
|
||||||
<div className="font-semibold mt-1">
|
|
||||||
{config.last_run_at
|
|
||||||
? new Date(config.last_run_at).toLocaleString()
|
|
||||||
: 'Never'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">Estimated Credits</div>
|
|
||||||
<div className="font-semibold mt-1">
|
|
||||||
{estimate?.estimated_credits || 0} credits
|
|
||||||
{estimate && !estimate.sufficient && (
|
|
||||||
<span className="text-red-600 dark:text-red-400 ml-2">(Insufficient)</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<DebugSiteSelector />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 mt-6">
|
{/* Schedule & Controls */}
|
||||||
<Button
|
{config && (
|
||||||
onClick={() => setShowConfigModal(true)}
|
<ComponentCard className="border-2 border-slate-200 dark:border-gray-800">
|
||||||
variant="outline"
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||||||
tone="brand"
|
<div className="flex-1 grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
>
|
<div>
|
||||||
Configure
|
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5">Status</div>
|
||||||
</Button>
|
<div className="flex items-center gap-2">
|
||||||
{currentRun?.status === 'running' && (
|
{config.is_enabled ? (
|
||||||
<Button
|
<>
|
||||||
onClick={handlePause}
|
<div className="size-2 bg-success-500 rounded-full animate-pulse"></div>
|
||||||
variant="primary"
|
<span className="text-sm font-semibold text-success-600 dark:text-success-400">Enabled</span>
|
||||||
tone="warning"
|
</>
|
||||||
>
|
) : (
|
||||||
Pause
|
<>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
{estimate?.estimated_credits || 0} credits
|
||||||
|
{estimate && !estimate.sufficient && (
|
||||||
|
<span className="ml-1 text-error-600 dark:text-error-400">(Low)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button onClick={() => setShowConfigModal(true)} variant="outline" tone="brand" size="sm">
|
||||||
|
Configure
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
{currentRun?.status === 'running' && (
|
||||||
{currentRun?.status === 'paused' && (
|
<Button onClick={handlePause} variant="primary" tone="warning" size="sm">
|
||||||
<Button
|
Pause
|
||||||
onClick={handleResume}
|
</Button>
|
||||||
variant="primary"
|
)}
|
||||||
tone="brand"
|
{currentRun?.status === 'paused' && (
|
||||||
>
|
<Button onClick={handleResume} variant="primary" tone="brand" size="sm">
|
||||||
Resume
|
Resume
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!currentRun && (
|
{!currentRun && (
|
||||||
<Button
|
<Button onClick={handleRunNow} variant="primary" tone="success" size="sm" disabled={!config?.is_enabled}>
|
||||||
onClick={handleRunNow}
|
Run Now
|
||||||
variant="primary"
|
</Button>
|
||||||
tone="success"
|
)}
|
||||||
disabled={!config?.is_enabled}
|
</div>
|
||||||
>
|
|
||||||
Run Now
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</ComponentCard>
|
</ComponentCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Current Run Status */}
|
{/* Pipeline Overview */}
|
||||||
{currentRun && (
|
<ComponentCard title="📊 Pipeline Overview" desc="Complete view of automation pipeline status and pending items">
|
||||||
<ComponentCard
|
<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">
|
||||||
title={`Current Run: ${currentRun.run_id}`}
|
<div className="text-sm font-medium">
|
||||||
desc="Live automation progress and stage details"
|
{currentRun ? (
|
||||||
>
|
<span className="text-blue-600 dark:text-blue-400">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<span className="inline-block size-2 bg-blue-500 rounded-full animate-pulse mr-2"></span>
|
||||||
<div>
|
Live Run Active - Stage {currentRun.current_stage} of 7
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">Status</div>
|
</span>
|
||||||
<div className="font-semibold mt-1 capitalize">{currentRun.status}</div>
|
) : (
|
||||||
</div>
|
<span className="text-slate-700 dark:text-gray-300">Pipeline Status - Ready to run</span>
|
||||||
<div>
|
)}
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">Current Stage</div>
|
</div>
|
||||||
<div className="font-semibold mt-1">
|
<div className="text-sm font-semibold text-slate-600 dark:text-gray-400">
|
||||||
Stage {currentRun.current_stage}: {STAGE_NAMES[currentRun.current_stage - 1]}
|
{totalPending} items pending
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</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];
|
||||||
|
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;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={actualStage.number}
|
||||||
|
className={`
|
||||||
|
relative rounded-xl border-2 p-6 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
|
||||||
|
? `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">
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</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}
|
||||||
|
</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>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
);
|
||||||
<div>
|
})}
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">Started</div>
|
</div>
|
||||||
<div className="font-semibold mt-1">
|
|
||||||
{new Date(currentRun.started_at).toLocaleString()}
|
{/* Stage 7 - Manual Review Gate (Separate Row) */}
|
||||||
</div>
|
{pipelineOverview[6] && (
|
||||||
</div>
|
<div className="mt-8">
|
||||||
<div>
|
<div className="max-w-3xl mx-auto">
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">Credits Used</div>
|
{(() => {
|
||||||
<div className="font-semibold mt-1">{currentRun.total_credits_used}</div>
|
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;
|
||||||
|
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</ComponentCard>
|
||||||
|
|
||||||
{/* Stage Progress */}
|
{/* Current Run Details */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-7 gap-3 mt-6">
|
{currentRun && (
|
||||||
{STAGE_NAMES.map((name, index) => (
|
<ComponentCard title={`Current Run: ${currentRun.run_id}`} desc="Live automation progress and detailed results">
|
||||||
<StageCard
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
key={index}
|
<EnhancedMetricCard
|
||||||
stageNumber={index + 1}
|
title="Status"
|
||||||
stageName={name}
|
value={currentRun.status}
|
||||||
currentStage={currentRun.current_stage}
|
icon={
|
||||||
result={currentRun[`stage_${index + 1}_result` as keyof AutomationRun] as any}
|
currentRun.status === 'running' ? <BoltIcon className="size-6" /> :
|
||||||
/>
|
currentRun.status === 'paused' ? <ClockIcon className="size-6" /> :
|
||||||
))}
|
currentRun.status === 'completed' ? <CheckCircleIcon className="size-6" /> :
|
||||||
|
<FileTextIcon className="size-6" />
|
||||||
|
}
|
||||||
|
accentColor={
|
||||||
|
currentRun.status === 'running' ? 'blue' :
|
||||||
|
currentRun.status === 'paused' ? 'orange' :
|
||||||
|
currentRun.status === 'completed' ? 'success' : 'red'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<EnhancedMetricCard
|
||||||
|
title="Started"
|
||||||
|
value={new Date(currentRun.started_at).toLocaleTimeString()}
|
||||||
|
icon={<ClockIcon className="size-6" />}
|
||||||
|
accentColor="blue"
|
||||||
|
/>
|
||||||
|
<EnhancedMetricCard
|
||||||
|
title="Credits Used"
|
||||||
|
value={currentRun.total_credits_used}
|
||||||
|
icon={<BoltIcon className="size-6" />}
|
||||||
|
accentColor="blue"
|
||||||
|
/>
|
||||||
|
<EnhancedMetricCard
|
||||||
|
title="Trigger"
|
||||||
|
value={currentRun.trigger_type}
|
||||||
|
icon={<PaperPlaneIcon className="size-6" />}
|
||||||
|
accentColor="green"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ComponentCard>
|
</ComponentCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Activity Log */}
|
{/* Activity Log */}
|
||||||
{currentRun && (
|
{currentRun && <ActivityLog runId={currentRun.run_id} />}
|
||||||
<ActivityLog runId={currentRun.run_id} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Run History */}
|
{/* Run History */}
|
||||||
<RunHistory siteId={activeSite.id} />
|
<RunHistory siteId={activeSite.id} />
|
||||||
|
|
||||||
{/* Config Modal */}
|
{/* Config Modal */}
|
||||||
{showConfigModal && config && (
|
{showConfigModal && config && (
|
||||||
<ConfigModal
|
<ConfigModal config={config} onSave={handleSaveConfig} onCancel={() => setShowConfigModal(false)} />
|
||||||
config={config}
|
|
||||||
onSave={handleSaveConfig}
|
|
||||||
onCancel={() => setShowConfigModal(false)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -341,3 +639,4 @@ const AutomationPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default AutomationPage;
|
export default AutomationPage;
|
||||||
|
|
||||||
|
|||||||
389
frontend/src/pages/Automation/AutomationPage_old.tsx
Normal file
389
frontend/src/pages/Automation/AutomationPage_old.tsx
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
/**
|
||||||
|
* Automation Dashboard Page
|
||||||
|
* Main page for managing AI automation pipeline
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
|
import { useSiteStore } from '../../store/siteStore';
|
||||||
|
import { automationService, AutomationRun, AutomationConfig, PipelineStage } from '../../services/automationService';
|
||||||
|
import StageCard from '../../components/Automation/StageCard';
|
||||||
|
import ActivityLog from '../../components/Automation/ActivityLog';
|
||||||
|
import ConfigModal from '../../components/Automation/ConfigModal';
|
||||||
|
import RunHistory from '../../components/Automation/RunHistory';
|
||||||
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
|
import ComponentCard from '../../components/common/ComponentCard';
|
||||||
|
import DebugSiteSelector from '../../components/common/DebugSiteSelector';
|
||||||
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import { BoltIcon } from '../../icons';
|
||||||
|
|
||||||
|
const STAGE_NAMES = [
|
||||||
|
'Keywords → Clusters',
|
||||||
|
'Clusters → Ideas',
|
||||||
|
'Ideas → Tasks',
|
||||||
|
'Tasks → Content',
|
||||||
|
'Content → Image Prompts',
|
||||||
|
'Image Prompts → Images',
|
||||||
|
'Manual Review Gate',
|
||||||
|
];
|
||||||
|
|
||||||
|
const AutomationPage: React.FC = () => {
|
||||||
|
const { activeSite } = useSiteStore();
|
||||||
|
const toast = useToast();
|
||||||
|
const [config, setConfig] = useState<AutomationConfig | null>(null);
|
||||||
|
const [currentRun, setCurrentRun] = useState<AutomationRun | null>(null);
|
||||||
|
const [pipelineOverview, setPipelineOverview] = useState<PipelineStage[]>([]);
|
||||||
|
const [showConfigModal, setShowConfigModal] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
|
||||||
|
const [estimate, setEstimate] = useState<{ estimated_credits: number; current_balance: number; sufficient: boolean } | null>(null);
|
||||||
|
|
||||||
|
// Poll for current run updates
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeSite) return;
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
// Poll every 5 seconds when run is active
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (currentRun && (currentRun.status === 'running' || currentRun.status === 'paused')) {
|
||||||
|
loadCurrentRun();
|
||||||
|
} else {
|
||||||
|
// Refresh pipeline overview when not running
|
||||||
|
loadPipelineOverview();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [activeSite, currentRun?.status]);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
if (!activeSite) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const [configData, runData, estimateData, pipelineData] = await Promise.all([
|
||||||
|
automationService.getConfig(activeSite.id),
|
||||||
|
automationService.getCurrentRun(activeSite.id),
|
||||||
|
automationService.estimate(activeSite.id),
|
||||||
|
automationService.getPipelineOverview(activeSite.id),
|
||||||
|
]);
|
||||||
|
setConfig(configData);
|
||||||
|
setCurrentRun(runData.run);
|
||||||
|
setEstimate(estimateData);
|
||||||
|
setPipelineOverview(pipelineData.stages);
|
||||||
|
setLastUpdated(new Date());
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error('Failed to load automation data');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadCurrentRun = async () => {
|
||||||
|
if (!activeSite) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await automationService.getCurrentRun(activeSite.id);
|
||||||
|
setCurrentRun(data.run);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to poll current run', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPipelineOverview = async () => {
|
||||||
|
if (!activeSite) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await automationService.getPipelineOverview(activeSite.id);
|
||||||
|
setPipelineOverview(data.stages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to poll pipeline overview', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRunNow = async () => {
|
||||||
|
if (!activeSite) return;
|
||||||
|
|
||||||
|
// Check credit balance
|
||||||
|
if (estimate && !estimate.sufficient) {
|
||||||
|
toast.error(`Insufficient credits. Need ~${estimate.estimated_credits}, you have ${estimate.current_balance}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await automationService.runNow(activeSite.id);
|
||||||
|
toast.success('Automation started');
|
||||||
|
loadCurrentRun();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response?.data?.error || 'Failed to start automation');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePause = async () => {
|
||||||
|
if (!currentRun) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await automationService.pause(currentRun.run_id);
|
||||||
|
toast.success('Automation paused');
|
||||||
|
loadCurrentRun();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to pause automation');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResume = async () => {
|
||||||
|
if (!currentRun) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await automationService.resume(currentRun.run_id);
|
||||||
|
toast.success('Automation resumed');
|
||||||
|
loadCurrentRun();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to resume automation');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveConfig = async (newConfig: Partial<AutomationConfig>) => {
|
||||||
|
if (!activeSite) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await automationService.updateConfig(activeSite.id, newConfig);
|
||||||
|
toast.success('Configuration saved');
|
||||||
|
setShowConfigModal(false);
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to save configuration');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<div className="text-lg text-gray-600 dark:text-gray-400">Loading automation...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activeSite) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<div className="text-lg text-gray-600 dark:text-gray-400">Please select a site to view automation</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageMeta
|
||||||
|
title="AI Automation Pipeline | IGNY8"
|
||||||
|
description="Automated content creation from keywords to published articles"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Page Header with Site Selector (no sector) */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-purple-600 dark:bg-purple-500 flex-shrink-0">
|
||||||
|
<BoltIcon className="text-white size-5" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">AI Automation Pipeline</h2>
|
||||||
|
</div>
|
||||||
|
{activeSite && (
|
||||||
|
<div className="flex items-center gap-3 mt-1">
|
||||||
|
{lastUpdated && (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Last updated: {lastUpdated.toLocaleTimeString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-gray-400 dark:text-gray-600">•</span>
|
||||||
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Site: <span className="text-brand-600 dark:text-brand-400">{activeSite.name}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<DebugSiteSelector />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Schedule Status Card */}
|
||||||
|
{config && (
|
||||||
|
<ComponentCard
|
||||||
|
title="Schedule & Status"
|
||||||
|
desc="Configure and monitor your automation schedule"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">Status</div>
|
||||||
|
<div className="font-semibold mt-1">
|
||||||
|
{config.is_enabled ? (
|
||||||
|
<span className="text-green-600 dark:text-green-400">● Enabled</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">○ Disabled</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">Schedule</div>
|
||||||
|
<div className="font-semibold mt-1 capitalize">
|
||||||
|
{config.frequency} at {config.scheduled_time}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">Last Run</div>
|
||||||
|
<div className="font-semibold mt-1">
|
||||||
|
{config.last_run_at
|
||||||
|
? new Date(config.last_run_at).toLocaleString()
|
||||||
|
: 'Never'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">Estimated Credits</div>
|
||||||
|
<div className="font-semibold mt-1">
|
||||||
|
{estimate?.estimated_credits || 0} credits
|
||||||
|
{estimate && !estimate.sufficient && (
|
||||||
|
<span className="text-red-600 dark:text-red-400 ml-2">(Insufficient)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowConfigModal(true)}
|
||||||
|
variant="outline"
|
||||||
|
tone="brand"
|
||||||
|
>
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
|
{currentRun?.status === 'running' && (
|
||||||
|
<Button
|
||||||
|
onClick={handlePause}
|
||||||
|
variant="primary"
|
||||||
|
tone="warning"
|
||||||
|
>
|
||||||
|
Pause
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{currentRun?.status === 'paused' && (
|
||||||
|
<Button
|
||||||
|
onClick={handleResume}
|
||||||
|
variant="primary"
|
||||||
|
tone="brand"
|
||||||
|
>
|
||||||
|
Resume
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!currentRun && (
|
||||||
|
<Button
|
||||||
|
onClick={handleRunNow}
|
||||||
|
variant="primary"
|
||||||
|
tone="success"
|
||||||
|
disabled={!config?.is_enabled}
|
||||||
|
>
|
||||||
|
Run Now
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ComponentCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pipeline Overview - Always Visible */}
|
||||||
|
<ComponentCard
|
||||||
|
title="📊 Pipeline Overview"
|
||||||
|
desc="Complete view of automation pipeline status and pending items"
|
||||||
|
>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{currentRun ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-blue-600 dark:text-blue-400">● Live Run Active</span> - Stage {currentRun.current_stage} of 7
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-gray-700 dark:text-gray-300">Pipeline Status</span> - Ready to run
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
{pipelineOverview.reduce((sum, stage) => sum + stage.pending, 0)} total items pending
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stage Cards Grid */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-7 gap-3">
|
||||||
|
{STAGE_NAMES.map((name, index) => (
|
||||||
|
<StageCard
|
||||||
|
key={index}
|
||||||
|
stageNumber={index + 1}
|
||||||
|
stageName={name}
|
||||||
|
currentStage={currentRun?.current_stage || 0}
|
||||||
|
result={currentRun ? (currentRun[`stage_${index + 1}_result` as keyof AutomationRun] as any) : null}
|
||||||
|
pipelineData={pipelineOverview[index]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ComponentCard>
|
||||||
|
|
||||||
|
{/* Current Run Status */}
|
||||||
|
{currentRun && (
|
||||||
|
<ComponentCard
|
||||||
|
title={`🔄 Current Run: ${currentRun.run_id}`}
|
||||||
|
desc="Live automation progress and detailed results"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">Status</div>
|
||||||
|
<div className="font-semibold mt-1 capitalize">
|
||||||
|
{currentRun.status === 'running' && <span className="text-blue-600 dark:text-blue-400">● {currentRun.status}</span>}
|
||||||
|
{currentRun.status === 'paused' && <span className="text-yellow-600 dark:text-yellow-400">⏸ {currentRun.status}</span>}
|
||||||
|
{currentRun.status === 'completed' && <span className="text-green-600 dark:text-green-400">✓ {currentRun.status}</span>}
|
||||||
|
{currentRun.status === 'failed' && <span className="text-red-600 dark:text-red-400">✗ {currentRun.status}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">Current Stage</div>
|
||||||
|
<div className="font-semibold mt-1">
|
||||||
|
Stage {currentRun.current_stage}: {STAGE_NAMES[currentRun.current_stage - 1]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">Started</div>
|
||||||
|
<div className="font-semibold mt-1">
|
||||||
|
{new Date(currentRun.started_at).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">Credits Used</div>
|
||||||
|
<div className="font-semibold mt-1 text-purple-600 dark:text-purple-400">{currentRun.total_credits_used}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ComponentCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Activity Log */}
|
||||||
|
{currentRun && (
|
||||||
|
<ActivityLog runId={currentRun.run_id} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Run History */}
|
||||||
|
<RunHistory siteId={activeSite.id} />
|
||||||
|
|
||||||
|
{/* Config Modal */}
|
||||||
|
{showConfigModal && config && (
|
||||||
|
<ConfigModal
|
||||||
|
config={config}
|
||||||
|
onSave={handleSaveConfig}
|
||||||
|
onCancel={() => setShowConfigModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AutomationPage;
|
||||||
@@ -27,8 +27,8 @@ import {
|
|||||||
fetchClusters,
|
fetchClusters,
|
||||||
fetchContentIdeas,
|
fetchContentIdeas,
|
||||||
fetchTasks,
|
fetchTasks,
|
||||||
fetchSiteBlueprints,
|
// fetchSiteBlueprints,
|
||||||
SiteBlueprint,
|
// SiteBlueprint,
|
||||||
} from "../../services/api";
|
} from "../../services/api";
|
||||||
import { useSiteStore } from "../../store/siteStore";
|
import { useSiteStore } from "../../store/siteStore";
|
||||||
import { useSectorStore } from "../../store/sectorStore";
|
import { useSectorStore } from "../../store/sectorStore";
|
||||||
@@ -78,12 +78,11 @@ export default function PlannerDashboard() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const [keywordsRes, clustersRes, ideasRes, tasksRes, blueprintsRes] = await Promise.all([
|
const [keywordsRes, clustersRes, ideasRes, tasksRes] = await Promise.all([
|
||||||
fetchKeywords({ page_size: 1000, sector_id: activeSector?.id }),
|
fetchKeywords({ page_size: 1000, sector_id: activeSector?.id }),
|
||||||
fetchClusters({ page_size: 1000, sector_id: activeSector?.id }),
|
fetchClusters({ page_size: 1000, sector_id: activeSector?.id }),
|
||||||
fetchContentIdeas({ page_size: 1000, sector_id: activeSector?.id }),
|
fetchContentIdeas({ page_size: 1000, sector_id: activeSector?.id }),
|
||||||
fetchTasks({ page_size: 1000, sector_id: activeSector?.id }),
|
fetchTasks({ page_size: 1000, sector_id: activeSector?.id })
|
||||||
activeSite?.id ? fetchSiteBlueprints({ site_id: activeSite.id, page_size: 100 }) : Promise.resolve({ results: [] })
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const keywords = keywordsRes.results || [];
|
const keywords = keywordsRes.results || [];
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { Card } from '../../components/ui/card';
|
|||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
|
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { fetchAPI, fetchSiteBlueprints } from '../../services/api';
|
import { fetchAPI } from '../../services/api';
|
||||||
|
// import { fetchSiteBlueprints } from '../../services/api';
|
||||||
import SiteProgressWidget from '../../components/sites/SiteProgressWidget';
|
import SiteProgressWidget from '../../components/sites/SiteProgressWidget';
|
||||||
import {
|
import {
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
@@ -70,10 +71,10 @@ export default function SiteDashboard() {
|
|||||||
const loadSiteData = async () => {
|
const loadSiteData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [siteData, statsData, blueprintsData] = await Promise.all([
|
const [siteData, statsData] = await Promise.all([
|
||||||
fetchAPI(`/v1/auth/sites/${siteId}/`),
|
fetchAPI(`/v1/auth/sites/${siteId}/`),
|
||||||
fetchSiteStats(),
|
fetchSiteStats(),
|
||||||
fetchSiteBlueprints({ site_id: Number(siteId) }),
|
// fetchSiteBlueprints({ site_id: Number(siteId) }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (siteData) {
|
if (siteData) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from '../../icons';
|
} from '../../icons';
|
||||||
import {
|
import {
|
||||||
fetchDeploymentReadiness,
|
fetchDeploymentReadiness,
|
||||||
fetchSiteBlueprints,
|
// fetchSiteBlueprints,
|
||||||
DeploymentReadiness,
|
DeploymentReadiness,
|
||||||
} from '../../services/api';
|
} from '../../services/api';
|
||||||
import { fetchAPI } from '../../services/api';
|
import { fetchAPI } from '../../services/api';
|
||||||
@@ -50,7 +50,8 @@ export default function DeploymentPanel() {
|
|||||||
if (!siteId) return;
|
if (!siteId) return;
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const blueprintsData = await fetchSiteBlueprints({ site_id: Number(siteId) });
|
// const blueprintsData = await fetchSiteBlueprints({ site_id: Number(siteId) });
|
||||||
|
const blueprintsData = null;
|
||||||
if (blueprintsData?.results && blueprintsData.results.length > 0) {
|
if (blueprintsData?.results && blueprintsData.results.length > 0) {
|
||||||
setBlueprints(blueprintsData.results);
|
setBlueprints(blueprintsData.results);
|
||||||
const firstBlueprint = blueprintsData.results[0];
|
const firstBlueprint = blueprintsData.results[0];
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ export interface RunHistoryItem {
|
|||||||
current_stage: number;
|
current_stage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PipelineStage {
|
||||||
|
number: number;
|
||||||
|
name: string;
|
||||||
|
pending: number;
|
||||||
|
type: 'AI' | 'Local' | 'Manual';
|
||||||
|
}
|
||||||
|
|
||||||
function buildUrl(endpoint: string, params?: Record<string, any>): string {
|
function buildUrl(endpoint: string, params?: Record<string, any>): string {
|
||||||
let url = `/v1/automation${endpoint}`;
|
let url = `/v1/automation${endpoint}`;
|
||||||
if (params) {
|
if (params) {
|
||||||
@@ -141,4 +148,11 @@ export const automationService = {
|
|||||||
}> => {
|
}> => {
|
||||||
return fetchAPI(buildUrl('/estimate/', { site_id: siteId }));
|
return fetchAPI(buildUrl('/estimate/', { site_id: siteId }));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pipeline overview with pending counts for all stages
|
||||||
|
*/
|
||||||
|
getPipelineOverview: async (siteId: number): Promise<{ stages: PipelineStage[] }> => {
|
||||||
|
return fetchAPI(buildUrl('/pipeline_overview/', { site_id: siteId }));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user