fix
This commit is contained in:
@@ -3,7 +3,15 @@
|
||||
* Shows real-time automation progress with pause/resume/cancel controls
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { automationService, ProcessingState, AutomationRun } from '../../services/automationService';
|
||||
import { automationService, ProcessingState, AutomationRun, PipelineStage } from '../../services/automationService';
|
||||
import {
|
||||
fetchKeywords,
|
||||
fetchClusters,
|
||||
fetchContentIdeas,
|
||||
fetchTasks,
|
||||
fetchContent,
|
||||
fetchContentImages,
|
||||
} from '../../services/api';
|
||||
import { useToast } from '../ui/toast/ToastContainer';
|
||||
import Button from '../ui/button/Button';
|
||||
import {
|
||||
@@ -20,6 +28,7 @@ interface CurrentProcessingCardProps {
|
||||
currentRun: AutomationRun;
|
||||
onUpdate: () => void;
|
||||
onClose: () => void;
|
||||
pipelineOverview?: PipelineStage[];
|
||||
}
|
||||
|
||||
const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
@@ -28,12 +37,17 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
currentRun,
|
||||
onUpdate,
|
||||
onClose,
|
||||
pipelineOverview,
|
||||
}) => {
|
||||
const [processingState, setProcessingState] = useState<ProcessingState | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPausing, setIsPausing] = useState(false);
|
||||
const [isResuming, setIsResuming] = useState(false);
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
const [fetchedCurrently, setFetchedCurrently] = useState<ProcessingState['currently_processing']>([]);
|
||||
const [fetchedUpNext, setFetchedUpNext] = useState<ProcessingState['up_next']>([]);
|
||||
const [isLocallyPaused, setIsLocallyPaused] = useState(false);
|
||||
const [showDebugTable, setShowDebugTable] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -42,7 +56,7 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
const fetchState = async () => {
|
||||
try {
|
||||
const state = await automationService.getCurrentProcessing(siteId, runId);
|
||||
|
||||
console.debug('getCurrentProcessing response for run', runId, state);
|
||||
if (!isMounted) return;
|
||||
|
||||
setProcessingState(state);
|
||||
@@ -59,8 +73,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Only fetch if status is running or paused
|
||||
if (currentRun.status === 'running' || currentRun.status === 'paused') {
|
||||
// Only fetch if status is running or paused and not locally paused
|
||||
if (!isLocallyPaused && (currentRun.status === 'running' || currentRun.status === 'paused')) {
|
||||
// Initial fetch
|
||||
fetchState();
|
||||
|
||||
@@ -76,13 +90,98 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [siteId, runId, currentRun.status, onUpdate]);
|
||||
}, [siteId, runId, currentRun.status, currentRun.current_stage, onUpdate, isLocallyPaused]);
|
||||
|
||||
// Attempt to fetch example items for the current stage when the API does not provide up_next/currently_processing
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const stageNumber = currentRun.current_stage;
|
||||
const loadStageQueue = async () => {
|
||||
try {
|
||||
switch (stageNumber) {
|
||||
case 1: {
|
||||
const res = await fetchKeywords({ page_size: 5, site_id: siteId, status: 'new' });
|
||||
if (!isMounted) return;
|
||||
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'keyword' }));
|
||||
setFetchedUpNext(items);
|
||||
setFetchedCurrently(items.slice(0, 1));
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
const res = await fetchClusters({ page_size: 5, site_id: siteId, status: 'new' });
|
||||
if (!isMounted) return;
|
||||
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.name || String(r.id), type: 'cluster' }));
|
||||
setFetchedUpNext(items);
|
||||
setFetchedCurrently(items.slice(0, 1));
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
const res = await fetchContentIdeas({ page_size: 5, site_id: siteId, status: 'queued' });
|
||||
if (!isMounted) return;
|
||||
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || String(r.id), type: 'idea' }));
|
||||
setFetchedUpNext(items);
|
||||
setFetchedCurrently(items.slice(0, 1));
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
// Tasks -> Content (show queued tasks)
|
||||
try {
|
||||
const res = await fetchTasks({ page_size: 5, site_id: siteId, status: 'queued' });
|
||||
if (!isMounted) return;
|
||||
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'task' }));
|
||||
setFetchedUpNext(items);
|
||||
setFetchedCurrently(items.slice(0, 1));
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
// Content -> Image Prompts (show content items awaiting prompts)
|
||||
try {
|
||||
const res = await fetchContent({ page_size: 5, site_id: siteId, status: 'queued' });
|
||||
if (!isMounted) return;
|
||||
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'content' }));
|
||||
setFetchedUpNext(items);
|
||||
setFetchedCurrently(items.slice(0, 1));
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 6: {
|
||||
const res = await fetchContentImages({ page_size: 5, site_id: siteId, status: 'pending' });
|
||||
if (!isMounted) return;
|
||||
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.filename || String(r.id), type: 'image' }));
|
||||
setFetchedUpNext(items);
|
||||
setFetchedCurrently(items.slice(0, 1));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// For stages without a clear read API, clear fetched lists
|
||||
setFetchedUpNext([]);
|
||||
setFetchedCurrently([]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch stage queue samples:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Only attempt when there's no live up_next data
|
||||
if ((!processingState || (processingState && (processingState.up_next || []).length === 0)) && currentRun.status === 'running') {
|
||||
loadStageQueue();
|
||||
}
|
||||
|
||||
return () => { isMounted = false; };
|
||||
}, [siteId, currentRun.current_stage, currentRun.status]);
|
||||
|
||||
const handlePause = async () => {
|
||||
setIsPausing(true);
|
||||
try {
|
||||
await automationService.pause(siteId, runId);
|
||||
toast?.success('Automation pausing... will complete current item');
|
||||
// Optimistically mark paused locally so UI stays paused until backend confirms
|
||||
setIsLocallyPaused(true);
|
||||
// Trigger update to refresh run status
|
||||
setTimeout(onUpdate, 1000);
|
||||
} catch (error: any) {
|
||||
@@ -97,6 +196,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
try {
|
||||
await automationService.resume(siteId, runId);
|
||||
toast?.success('Automation resumed');
|
||||
// Clear local paused flag
|
||||
setIsLocallyPaused(false);
|
||||
// Trigger update to refresh run status
|
||||
setTimeout(onUpdate, 1000);
|
||||
} catch (error: any) {
|
||||
@@ -153,13 +254,67 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!processingState && currentRun.status === 'running') {
|
||||
return null;
|
||||
}
|
||||
// Build a fallback processing state from currentRun and pipelineOverview when API doesn't return live state
|
||||
const currentStageIndex = (currentRun.current_stage || 1) - 1;
|
||||
const stageOverview = pipelineOverview && pipelineOverview[currentStageIndex] ? pipelineOverview[currentStageIndex] : null;
|
||||
const stageResult = (currentRun as any)[`stage_${currentRun.current_stage}_result`];
|
||||
|
||||
const percentage = processingState?.percentage || 0;
|
||||
const fallbackState: ProcessingState | null = ((): ProcessingState | null => {
|
||||
if (!processingState && (stageOverview || stageResult)) {
|
||||
const processed = stageResult ? Object.values(stageResult).reduce((s: number, v: any) => typeof v === 'number' ? s + v : s, 0) : 0;
|
||||
const total = (stageOverview?.pending || 0) + processed;
|
||||
const percentage = total > 0 ? Math.round((processed / total) * 100) : 0;
|
||||
|
||||
return {
|
||||
stage_number: currentRun.current_stage,
|
||||
stage_name: stageOverview?.name || `Stage ${currentRun.current_stage}`,
|
||||
stage_type: stageOverview?.type || 'AI',
|
||||
total_items: total,
|
||||
processed_items: processed,
|
||||
percentage,
|
||||
currently_processing: [],
|
||||
up_next: [],
|
||||
remaining_count: Math.max(0, total - processed),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
|
||||
const displayState = processingState || fallbackState;
|
||||
|
||||
// If we don't have a live displayState, keep rendering the card using computed values
|
||||
|
||||
// Computed processed/total (use processingState when available, otherwise derive from stageResult + overview)
|
||||
const computedProcessed = ((): number => {
|
||||
if (displayState && typeof displayState.processed_items === 'number') return displayState.processed_items;
|
||||
if (stageResult) {
|
||||
// Sum numeric values in stageResult as a heuristic for processed count
|
||||
return Object.values(stageResult).reduce((s: number, v: any) => (typeof v === 'number' ? s + v : s), 0);
|
||||
}
|
||||
return 0;
|
||||
})();
|
||||
|
||||
const computedTotal = ((): number => {
|
||||
if (displayState && typeof displayState.total_items === 'number' && displayState.total_items > 0) return displayState.total_items;
|
||||
const pending = stageOverview?.pending ?? 0;
|
||||
return Math.max(pending + computedProcessed, 0);
|
||||
})();
|
||||
|
||||
const percentage = computedTotal > 0 ? Math.round((computedProcessed / computedTotal) * 100) : 0;
|
||||
const isPaused = currentRun.status === 'paused';
|
||||
|
||||
// Choose stage accent color (simple map matching AutomationPage STAGE_CONFIG)
|
||||
const stageColors = [
|
||||
'from-blue-500 to-blue-600',
|
||||
'from-purple-500 to-purple-600',
|
||||
'from-indigo-500 to-indigo-600',
|
||||
'from-green-500 to-green-600',
|
||||
'from-amber-500 to-amber-600',
|
||||
'from-pink-500 to-pink-600',
|
||||
'from-teal-500 to-teal-600',
|
||||
];
|
||||
const stageColorClass = stageColors[(currentRun.current_stage || 1) - 1] || 'from-blue-500 to-blue-600';
|
||||
|
||||
return (
|
||||
<div className={`border-2 rounded-lg p-6 mb-6 ${
|
||||
isPaused
|
||||
@@ -182,32 +337,55 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{isPaused ? 'Automation Paused' : 'Automation In Progress'}
|
||||
</h2>
|
||||
{processingState && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Stage {currentRun.current_stage}: {processingState.stage_name}
|
||||
<span className={`ml-2 px-2 py-0.5 rounded text-xs ${
|
||||
isPaused
|
||||
? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300'
|
||||
: 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
|
||||
}`}>
|
||||
{processingState.stage_type}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{/* Centered stage row + dynamic action text */}
|
||||
{displayState && (
|
||||
<div className="mt-2">
|
||||
<div className="text-center text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Stage {currentRun.current_stage}: {displayState.stage_name}
|
||||
<span className={`ml-2 px-2 py-0.5 rounded text-xs ${
|
||||
isPaused
|
||||
? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300'
|
||||
: 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
|
||||
}`}>
|
||||
{displayState.stage_type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{(() => {
|
||||
// Build dynamic action text based on stage type and counts
|
||||
const verb = displayState.stage_type === 'AI' ? 'Generating' : 'Processing';
|
||||
// target label for the current stage (what is being produced)
|
||||
const targetLabelMap: Record<number, string> = {
|
||||
1: 'Clusters',
|
||||
2: 'Ideas',
|
||||
3: 'Tasks',
|
||||
4: 'Content',
|
||||
5: 'Image Prompts',
|
||||
6: 'Images',
|
||||
7: 'Review',
|
||||
};
|
||||
const label = targetLabelMap[displayState.stage_number] || 'Items';
|
||||
return `${verb} ${computedProcessed}/${computedTotal} ${label}`;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Info */}
|
||||
{processingState && (
|
||||
{displayState && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
||||
{percentage}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{processingState.processed_items}/{processingState.total_items} completed
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{computedProcessed}/{computedTotal} completed
|
||||
</div>
|
||||
</div>
|
||||
{/* Progress Bar */}
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3">
|
||||
@@ -230,8 +408,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
Currently Processing:
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{processingState.currently_processing.length > 0 ? (
|
||||
processingState.currently_processing.map((item, idx) => (
|
||||
{((displayState.currently_processing && displayState.currently_processing.length > 0) ? displayState.currently_processing : fetchedCurrently).length > 0 ? (
|
||||
((displayState.currently_processing && displayState.currently_processing.length > 0) ? displayState.currently_processing : fetchedCurrently).map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 text-sm">
|
||||
<span className={isPaused ? 'text-yellow-600 dark:text-yellow-400 mt-1' : 'text-blue-600 dark:text-blue-400 mt-1'}>•</span>
|
||||
<span className="text-gray-800 dark:text-gray-200 font-medium line-clamp-2">
|
||||
@@ -253,9 +431,9 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
Up Next:
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{processingState.up_next.length > 0 ? (
|
||||
{((displayState.up_next && displayState.up_next.length > 0) ? displayState.up_next : fetchedUpNext).length > 0 ? (
|
||||
<>
|
||||
{processingState.up_next.map((item, idx) => (
|
||||
{((displayState.up_next && displayState.up_next.length > 0) ? displayState.up_next : fetchedUpNext).map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 text-sm">
|
||||
<span className="text-gray-400 dark:text-gray-500 mt-1">•</span>
|
||||
<span className="text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
@@ -263,9 +441,9 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{processingState.remaining_count > processingState.up_next.length + processingState.currently_processing.length && (
|
||||
{displayState.remaining_count > ((displayState.up_next?.length || 0) + (displayState.currently_processing?.length || 0)) && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
+ {processingState.remaining_count - processingState.up_next.length - processingState.currently_processing.length} more in queue
|
||||
+ {displayState.remaining_count - ((displayState.up_next?.length || 0) + (displayState.currently_processing?.length || 0))} more in queue
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -286,8 +464,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
disabled={isPausing}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
startIcon={<PauseIcon className="w-4 h-4" />}
|
||||
>
|
||||
<PauseIcon className="w-4 h-4 mr-2" />
|
||||
{isPausing ? 'Pausing...' : 'Pause'}
|
||||
</Button>
|
||||
) : currentRun.status === 'paused' ? (
|
||||
@@ -296,8 +474,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
disabled={isResuming}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={<PlayIcon className="w-4 h-4" />}
|
||||
>
|
||||
<PlayIcon className="w-4 h-4 mr-2" />
|
||||
{isResuming ? 'Resuming...' : 'Resume'}
|
||||
</Button>
|
||||
) : null}
|
||||
@@ -307,8 +485,8 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
disabled={isCancelling}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
startIcon={<XMarkIcon className="w-4 h-4" />}
|
||||
>
|
||||
<XMarkIcon className="w-4 h-4 mr-2" />
|
||||
{isCancelling ? 'Cancelling...' : 'Cancel'}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -320,68 +498,102 @@ const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
<div className="w-64 flex-shrink-0">
|
||||
{/* Close Button */}
|
||||
<div className="flex justify-end mb-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
title="Close (card will remain available below)"
|
||||
>
|
||||
<XMarkIcon className="w-6 h-6" />
|
||||
</button>
|
||||
<Button variant="ghost" size="sm" onClick={onClose} startIcon={<XMarkIcon className="w-4 h-4" />}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Metrics Cards */}
|
||||
<div className="space-y-3">
|
||||
{/* Duration */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">
|
||||
Duration
|
||||
<div className="flex items-center justify-between mb-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Duration</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{formatDuration(currentRun.started_at)}
|
||||
<div className="text-sm font-bold text-gray-900 dark:text-white">{formatDuration(currentRun.started_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Credits Used */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<BoltIcon className="w-4 h-4 text-amber-500" />
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">
|
||||
Credits Used
|
||||
<div className="flex items-center justify-between mb-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<BoltIcon className="w-4 h-4 text-amber-500" />
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Credits Used</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-amber-600 dark:text-amber-400">
|
||||
{currentRun.total_credits_used}
|
||||
<div className="text-sm font-bold text-amber-600 dark:text-amber-400">{currentRun.total_credits_used}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Stage */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold mb-1">
|
||||
Stage
|
||||
</div>
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{currentRun.current_stage} of 7
|
||||
<div className="flex items-center justify-between mb-0">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Stage</div>
|
||||
<div className="text-sm font-bold text-gray-900 dark:text-white">{currentRun.current_stage} of 7</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold mb-1">
|
||||
Status
|
||||
</div>
|
||||
<div className={`text-sm font-semibold ${
|
||||
isPaused
|
||||
? 'text-yellow-600 dark:text-yellow-400'
|
||||
: 'text-blue-600 dark:text-blue-400'
|
||||
}`}>
|
||||
{isPaused ? 'Paused' : 'Running'}
|
||||
<div className="flex items-center justify-between mb-0">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Status</div>
|
||||
<div className={`text-sm font-semibold ${isPaused ? 'text-yellow-600 dark:text-yellow-400' : 'text-blue-600 dark:text-blue-400'}`}>
|
||||
{isPaused ? 'Paused' : 'Running'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Debug table toggle + table for stage data */}
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDebugTable(!showDebugTable)}
|
||||
className="text-xs text-slate-600 hover:underline"
|
||||
>
|
||||
{showDebugTable ? 'Hide' : 'Show'} debug table
|
||||
</button>
|
||||
{showDebugTable && (
|
||||
<div className="mt-3 bg-white dark:bg-gray-800 p-3 rounded border">
|
||||
<div className="text-sm font-semibold mb-2">Stage Data</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-left">
|
||||
<th className="pr-4">Stage</th>
|
||||
<th className="pr-4">Pending</th>
|
||||
<th className="pr-4">Processed</th>
|
||||
<th className="pr-4">Total</th>
|
||||
<th className="pr-4">Currently (sample)</th>
|
||||
<th className="pr-4">Up Next (sample)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(pipelineOverview || []).map((stage) => {
|
||||
const result = (currentRun as any)[`stage_${stage.number}_result`];
|
||||
const processed = result ? Object.values(result).reduce((s: number, v: any) => typeof v === 'number' ? s + v : s, 0) : 0;
|
||||
const total = Math.max((stage.pending || 0) + processed, 0);
|
||||
const currently = currentRun.current_stage === stage.number ? (processingState?.currently_processing?.slice(0,3) || fetchedCurrently) : [];
|
||||
const upnext = currentRun.current_stage === stage.number ? (processingState?.up_next?.slice(0,5) || fetchedUpNext) : [];
|
||||
return (
|
||||
<tr key={stage.number} className="border-t">
|
||||
<td className="py-2">{stage.number} — {stage.name}</td>
|
||||
<td className="py-2">{stage.pending}</td>
|
||||
<td className="py-2">{processed}</td>
|
||||
<td className="py-2">{total}</td>
|
||||
<td className="py-2">{currently.map(c => c.title).join(', ') || '-'}</td>
|
||||
<td className="py-2">{upnext.map(u => u.title).join(', ') || '-'}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -140,10 +140,10 @@ export default function FormModal({
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{fields.filter(f => f.key !== 'keyword' && f.key !== 'volume' && f.key !== 'difficulty').map((field) => {
|
||||
{fields.filter(f => f.key !== 'keyword' && f.key !== 'volume' && f.key !== 'difficulty').map((field, idx) => {
|
||||
if (field.type === 'select') {
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<div key={`${field.key}-${idx}`}>
|
||||
<Label className="mb-2">
|
||||
{field.label}
|
||||
{field.required && <span className="text-error-500 ml-1">*</span>}
|
||||
@@ -160,7 +160,7 @@ export default function FormModal({
|
||||
}
|
||||
if (field.type === 'textarea') {
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<div key={`${field.key}-${idx}`}>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{field.label}
|
||||
{field.required && <span className="text-error-500 ml-1">*</span>}
|
||||
@@ -177,7 +177,7 @@ export default function FormModal({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<div key={`${field.key}-${idx}`}>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{field.label}
|
||||
{field.required && <span className="text-error-500 ml-1">*</span>}
|
||||
|
||||
@@ -147,15 +147,13 @@ const AdminBilling: React.FC = () => {
|
||||
Admin controls for credits, pricing, and user billing
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/admin/igny8_core/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
<Button
|
||||
variant="outline"
|
||||
startIcon={<PlugInIcon className="w-4 h-4" />}
|
||||
onClick={() => window.open('/admin/igny8_core/', '_blank')}
|
||||
>
|
||||
<PlugInIcon className="w-4 h-4 mr-2" />
|
||||
Django Admin
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* System Stats */}
|
||||
@@ -163,30 +161,26 @@ const AdminBilling: React.FC = () => {
|
||||
<EnhancedMetricCard
|
||||
title="Total Users"
|
||||
value={stats?.total_users || 0}
|
||||
icon={UserIcon}
|
||||
color="blue"
|
||||
iconColor="text-blue-500"
|
||||
icon={<UserIcon />}
|
||||
accentColor="blue"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Active Users"
|
||||
value={stats?.active_users || 0}
|
||||
icon={CheckCircleIcon}
|
||||
color="green"
|
||||
iconColor="text-green-500"
|
||||
icon={<CheckCircleIcon />}
|
||||
accentColor="green"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Credits Issued"
|
||||
value={stats?.total_credits_issued || 0}
|
||||
icon={DollarLineIcon}
|
||||
color="amber"
|
||||
iconColor="text-amber-500"
|
||||
icon={<DollarLineIcon />}
|
||||
accentColor="orange"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Credits Used"
|
||||
value={stats?.total_credits_used || 0}
|
||||
icon={BoltIcon}
|
||||
color="purple"
|
||||
iconColor="text-purple-500"
|
||||
icon={<BoltIcon />}
|
||||
accentColor="purple"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -231,28 +225,28 @@ const AdminBilling: React.FC = () => {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<ComponentCard title="Quick Actions">
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
<Button
|
||||
variant="primary"
|
||||
fullWidth
|
||||
startIcon={<UserIcon className="w-4 h-4" />}
|
||||
onClick={() => setActiveTab('users')}
|
||||
>
|
||||
<UserIcon className="w-4 h-4 mr-2" />
|
||||
Manage User Credits
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
startIcon={<DollarLineIcon className="w-4 h-4" />}
|
||||
onClick={() => setActiveTab('pricing')}
|
||||
>
|
||||
<DollarLineIcon className="w-4 h-4 mr-2" />
|
||||
Update Credit Costs
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
fullWidth
|
||||
startIcon={<PlugInIcon className="w-4 h-4" />}
|
||||
onClick={() => window.open('/admin/igny8_core/creditcostconfig/', '_blank')}
|
||||
>
|
||||
<PlugInIcon className="w-4 h-4 mr-2" />
|
||||
Full Admin Panel
|
||||
</Button>
|
||||
</div>
|
||||
@@ -309,7 +303,7 @@ const AdminBilling: React.FC = () => {
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<Badge variant="info">{user.subscription_plan || 'Free'}</Badge>
|
||||
<Badge tone="info">{user.subscription_plan || 'Free'}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-right font-bold text-amber-600 dark:text-amber-400">
|
||||
{user.credits}
|
||||
@@ -432,7 +426,7 @@ const AdminBilling: React.FC = () => {
|
||||
{config.cost}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||
<Badge variant={config.is_active ? 'success' : 'warning'}>
|
||||
<Badge tone={config.is_active ? 'success' : 'warning'}>
|
||||
{config.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</td>
|
||||
|
||||
@@ -54,6 +54,7 @@ const AutomationPage: React.FC = () => {
|
||||
const [pipelineOverview, setPipelineOverview] = useState<PipelineStage[]>([]);
|
||||
const [metrics, setMetrics] = useState<any>(null);
|
||||
const [showConfigModal, setShowConfigModal] = useState(false);
|
||||
const [showProcessingCard, setShowProcessingCard] = useState<boolean>(true);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [estimate, setEstimate] = useState<{ estimated_credits: number; current_balance: number; sufficient: boolean } | null>(null);
|
||||
|
||||
@@ -147,6 +148,10 @@ const AutomationPage: React.FC = () => {
|
||||
setCurrentRun(runData.run);
|
||||
setEstimate(estimateData);
|
||||
setPipelineOverview(pipelineData.stages);
|
||||
// show processing card when there's a current run
|
||||
if (runData.run) {
|
||||
setShowProcessingCard(true);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error('Failed to load automation data');
|
||||
console.error(error);
|
||||
@@ -160,6 +165,8 @@ const AutomationPage: React.FC = () => {
|
||||
try {
|
||||
const data = await automationService.getCurrentRun(activeSite.id);
|
||||
setCurrentRun(data.run);
|
||||
// ensure processing card is visible when a run exists
|
||||
if (data.run) setShowProcessingCard(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to poll current run', error);
|
||||
}
|
||||
@@ -251,22 +258,27 @@ const AutomationPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
if (!currentRun) return;
|
||||
if (!currentRun || !activeSite) return;
|
||||
try {
|
||||
await automationService.pause(currentRun.run_id);
|
||||
await automationService.pause(activeSite.id, currentRun.run_id);
|
||||
toast.success('Automation paused');
|
||||
loadCurrentRun();
|
||||
// refresh run and pipeline/metrics
|
||||
await loadCurrentRun();
|
||||
await loadPipelineOverview();
|
||||
await loadMetrics();
|
||||
} catch (error) {
|
||||
toast.error('Failed to pause automation');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResume = async () => {
|
||||
if (!currentRun) return;
|
||||
if (!currentRun || !activeSite) return;
|
||||
try {
|
||||
await automationService.resume(currentRun.run_id);
|
||||
await automationService.resume(activeSite.id, currentRun.run_id);
|
||||
toast.success('Automation resumed');
|
||||
loadCurrentRun();
|
||||
await loadCurrentRun();
|
||||
await loadPipelineOverview();
|
||||
await loadMetrics();
|
||||
} catch (error) {
|
||||
toast.error('Failed to resume automation');
|
||||
}
|
||||
@@ -278,8 +290,13 @@ const AutomationPage: React.FC = () => {
|
||||
await automationService.updateConfig(activeSite.id, newConfig);
|
||||
toast.success('Configuration saved');
|
||||
setShowConfigModal(false);
|
||||
loadData();
|
||||
// Optimistically update config locally and refresh data
|
||||
setConfig((prev) => ({ ...(prev as AutomationConfig), ...newConfig } as AutomationConfig));
|
||||
await loadPipelineOverview();
|
||||
await loadMetrics();
|
||||
await loadCurrentRun();
|
||||
} catch (error) {
|
||||
console.error('Failed to save config:', error);
|
||||
toast.error('Failed to save configuration');
|
||||
}
|
||||
};
|
||||
@@ -665,18 +682,21 @@ const AutomationPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Current Processing Card - Shows real-time automation progress */}
|
||||
{currentRun && (currentRun.status === 'running' || currentRun.status === 'paused') && activeSite && (
|
||||
{currentRun && showProcessingCard && activeSite && (
|
||||
<CurrentProcessingCard
|
||||
runId={currentRun.run_id}
|
||||
siteId={activeSite.id}
|
||||
currentRun={currentRun}
|
||||
onUpdate={() => {
|
||||
// Refresh current run status
|
||||
loadCurrentRun();
|
||||
pipelineOverview={pipelineOverview}
|
||||
onUpdate={async () => {
|
||||
// Refresh current run status, pipeline overview and metrics (no full page reload)
|
||||
await loadCurrentRun();
|
||||
await loadPipelineOverview();
|
||||
await loadMetrics();
|
||||
}}
|
||||
onClose={() => {
|
||||
// Card will remain in DOM but user acknowledged it
|
||||
// Can add state here to minimize it if needed
|
||||
// hide the processing card until next run
|
||||
setShowProcessingCard(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -692,7 +712,8 @@ const AutomationPage: React.FC = () => {
|
||||
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;
|
||||
const total = (stage.pending ?? 0) + processed;
|
||||
const progressPercent = total > 0 ? Math.round((processed / total) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -787,7 +808,8 @@ const AutomationPage: React.FC = () => {
|
||||
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;
|
||||
const total = (stage.pending ?? 0) + processed;
|
||||
const progressPercent = total > 0 ? Math.round((processed / total) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -113,11 +113,14 @@ const CreditsAndBilling: React.FC = () => {
|
||||
Manage your credits, view transactions, and monitor usage
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="primary" onClick={() => {
|
||||
// TODO: Link to purchase credits page
|
||||
toast?.info('Purchase credits feature coming soon');
|
||||
}}>
|
||||
<DollarLineIcon className="w-4 h-4 mr-2" />
|
||||
<Button
|
||||
variant="primary"
|
||||
startIcon={<DollarLineIcon className="w-4 h-4" />}
|
||||
onClick={() => {
|
||||
// TODO: Link to purchase credits page
|
||||
toast?.info('Purchase credits feature coming soon');
|
||||
}}
|
||||
>
|
||||
Purchase Credits
|
||||
</Button>
|
||||
</div>
|
||||
@@ -127,31 +130,27 @@ const CreditsAndBilling: React.FC = () => {
|
||||
<EnhancedMetricCard
|
||||
title="Current Balance"
|
||||
value={balance?.credits || 0}
|
||||
icon={BoltIcon}
|
||||
color="amber"
|
||||
iconColor="text-amber-500"
|
||||
icon={<BoltIcon />}
|
||||
accentColor="orange"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Monthly Included"
|
||||
value={balance?.monthly_credits_included || 0}
|
||||
subtitle={balance?.subscription_plan || 'Free'}
|
||||
icon={CheckCircleIcon}
|
||||
color="green"
|
||||
iconColor="text-green-500"
|
||||
icon={<CheckCircleIcon />}
|
||||
accentColor="green"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Bonus Credits"
|
||||
value={balance?.bonus_credits || 0}
|
||||
icon={DollarLineIcon}
|
||||
color="blue"
|
||||
iconColor="text-blue-500"
|
||||
icon={<DollarLineIcon />}
|
||||
accentColor="blue"
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Total This Month"
|
||||
value={usageLogs.reduce((sum, log) => sum + log.credits_used, 0)}
|
||||
icon={TimeIcon}
|
||||
color="purple"
|
||||
iconColor="text-purple-500"
|
||||
icon={<TimeIcon />}
|
||||
accentColor="purple"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -201,7 +200,7 @@ const CreditsAndBilling: React.FC = () => {
|
||||
<div key={transaction.id} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={getTransactionTypeColor(transaction.transaction_type)}>
|
||||
<Badge tone={getTransactionTypeColor(transaction.transaction_type) as any}>
|
||||
{transaction.transaction_type}
|
||||
</Badge>
|
||||
<span className="text-sm text-gray-900 dark:text-white">
|
||||
@@ -290,7 +289,7 @@ const CreditsAndBilling: React.FC = () => {
|
||||
{new Date(transaction.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Badge variant={getTransactionTypeColor(transaction.transaction_type)}>
|
||||
<Badge tone={getTransactionTypeColor(transaction.transaction_type) as any}>
|
||||
{transaction.transaction_type}
|
||||
</Badge>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user