fixes for image gen

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-11 21:35:54 +00:00
parent 298b7bc625
commit 253d2e989d
4 changed files with 432 additions and 31 deletions

View File

@@ -0,0 +1,281 @@
/**
* Image Queue Modal - Shows per-image progress for image generation
* Similar to WordPress plugin's image queue processor
*/
import React, { useState, useEffect, useRef } from 'react';
import { Modal } from '../ui/modal';
export interface ImageQueueItem {
image_id: number;
index: number;
label: string;
content_title: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
progress: number;
image_url: string | null;
error: string | null;
}
interface ImageQueueModalProps {
isOpen: boolean;
queue: ImageQueueItem[];
onClose: () => void;
}
export default function ImageQueueModal({
isOpen,
queue,
onClose,
}: ImageQueueModalProps) {
const [localQueue, setLocalQueue] = useState<ImageQueueItem[]>(queue);
const progressIntervalsRef = useRef<Map<number, NodeJS.Timeout>>(new Map());
// Update local queue when prop changes
useEffect(() => {
setLocalQueue(queue);
}, [queue]);
// Simulate smooth progress for processing images (like WP plugin)
useEffect(() => {
if (!isOpen) return;
localQueue.forEach((item) => {
if (item.status === 'processing' && item.progress < 95) {
// Clear existing interval for this item
const existing = progressIntervalsRef.current.get(item.image_id);
if (existing) {
clearInterval(existing);
}
// Progressive loading: 50% in 7s, 75% in next 5s, then 5% every second until 95%
let currentProgress = item.progress;
let phase = 1;
let phaseStartTime = Date.now();
const interval = setInterval(() => {
const elapsed = Date.now() - phaseStartTime;
if (phase === 1 && currentProgress < 50) {
// Phase 1: 0% to 50% in 7 seconds (7.14% per second)
currentProgress += 0.714;
if (currentProgress >= 50 || elapsed >= 7000) {
currentProgress = 50;
phase = 2;
phaseStartTime = Date.now();
}
} else if (phase === 2 && currentProgress < 75) {
// Phase 2: 50% to 75% in 5 seconds (5% per second)
currentProgress += 0.5;
if (currentProgress >= 75 || elapsed >= 5000) {
currentProgress = 75;
phase = 3;
phaseStartTime = Date.now();
}
} else if (phase === 3 && currentProgress < 95) {
// Phase 3: 75% to 95% - 5% every second
if (elapsed >= 1000) {
currentProgress = Math.min(95, currentProgress + 5);
phaseStartTime = Date.now();
}
}
// Update local state
setLocalQueue((prev) =>
prev.map((q) =>
q.image_id === item.image_id
? { ...q, progress: Math.min(95, currentProgress) }
: q
)
);
}, 100);
progressIntervalsRef.current.set(item.image_id, interval);
} else {
// Clear interval if not processing
const existing = progressIntervalsRef.current.get(item.image_id);
if (existing) {
clearInterval(existing);
progressIntervalsRef.current.delete(item.image_id);
}
}
});
return () => {
// Cleanup intervals on unmount
progressIntervalsRef.current.forEach((interval) => clearInterval(interval));
progressIntervalsRef.current.clear();
};
}, [isOpen, localQueue]);
// Check if all images are completed
const allCompleted = localQueue.every(
(item) => item.status === 'completed' || item.status === 'failed'
);
const completedCount = localQueue.filter(
(item) => item.status === 'completed'
).length;
const failedCount = localQueue.filter((item) => item.status === 'failed').length;
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800';
case 'failed':
return 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800';
case 'processing':
return 'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800';
default:
return 'bg-gray-50 border-gray-200 dark:bg-gray-800 dark:border-gray-700';
}
};
const getProgressColor = (status: string) => {
switch (status) {
case 'completed':
return 'bg-green-500';
case 'failed':
return 'bg-red-500';
case 'processing':
return 'bg-blue-500';
default:
return 'bg-gray-300';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'completed':
return '✅';
case 'failed':
return '❌';
case 'processing':
return '⏳';
default:
return '⏸️';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'completed':
return 'Complete';
case 'failed':
return 'Failed';
case 'processing':
return 'Generating...';
default:
return 'Pending';
}
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
className="max-w-4xl"
showCloseButton={!allCompleted}
>
<div className="p-6">
{/* Header */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1 text-center">
🎨 Generating Images
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 text-center">
Total: {localQueue.length} image{localQueue.length !== 1 ? 's' : ''} in queue
{allCompleted && (
<span className="ml-2">
({completedCount} completed{failedCount > 0 ? `, ${failedCount} failed` : ''})
</span>
)}
</p>
</div>
{/* Image Queue */}
<div className="space-y-3 max-h-[60vh] overflow-y-auto">
{localQueue.map((item) => (
<div
key={item.image_id}
className={`p-4 rounded-lg border-2 transition-colors ${getStatusColor(
item.status
)}`}
>
<div className="flex items-center gap-4">
{/* Left side: Info and progress */}
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<div className="w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold flex items-center justify-center flex-shrink-0">
{item.index}
</div>
<span className="font-semibold text-sm text-gray-900 dark:text-white">
{item.label}
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 flex-1 truncate">
{item.content_title}
</span>
<span className="text-xs font-semibold text-gray-700 dark:text-gray-300">
{getStatusIcon(item.status)} {getStatusText(item.status)}
</span>
</div>
{/* Progress bar */}
<div className="relative h-5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full ${getProgressColor(
item.status
)} transition-all duration-300 ease-out`}
style={{ width: `${item.progress}%` }}
/>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xs font-bold text-gray-900 dark:text-white">
{item.status === 'processing'
? `${Math.round(item.progress)}%`
: item.status === 'completed'
? '100%'
: item.status === 'failed'
? 'Failed'
: '0%'}
</span>
</div>
</div>
{/* Error message */}
{item.status === 'failed' && item.error && (
<div className="mt-2 p-2 bg-red-100 dark:bg-red-900/30 border-l-3 border-red-500 rounded text-xs text-red-700 dark:text-red-300">
{item.error}
</div>
)}
</div>
{/* Right side: Thumbnail */}
<div className="w-20 h-20 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center overflow-hidden flex-shrink-0">
{item.image_url ? (
<img
src={item.image_url}
alt={item.label}
className="w-full h-full object-cover"
/>
) : (
<span className="text-xs text-gray-400">No image</span>
)}
</div>
</div>
</div>
))}
</div>
{/* Success message when all done */}
{allCompleted && (
<div className="mt-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<p className="text-sm text-green-800 dark:text-green-200 text-center font-medium">
Image generation complete! {completedCount} image
{completedCount !== 1 ? 's' : ''} generated successfully
{failedCount > 0 && `, ${failedCount} failed`}.
</p>
</div>
)}
</div>
</Modal>
);
}

View File

@@ -14,6 +14,17 @@ export interface ProgressState {
};
}
export interface ImageQueueItem {
image_id: number;
index: number;
label: string;
content_title: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
progress: number;
image_url: string | null;
error: string | null;
}
export interface UseProgressModalReturn {
progress: ProgressState;
isOpen: boolean;
@@ -32,6 +43,7 @@ export interface UseProgressModalReturn {
message: string;
timestamp?: number;
}>; // Step logs for debugging
imageQueue?: ImageQueueItem[]; // Image queue for image generation
}
export function useProgressModal(): UseProgressModalReturn {
@@ -54,6 +66,9 @@ export function useProgressModal(): UseProgressModalReturn {
timestamp?: number;
}>>([]);
// Image queue state for image generation
const [imageQueue, setImageQueue] = useState<ImageQueueItem[] | undefined>(undefined);
// Track displayed percentage and current step for step-based progress
const displayedPercentageRef = useRef(0);
const currentStepRef = useRef<string | null>(null);
@@ -458,6 +473,11 @@ export function useProgressModal(): UseProgressModalReturn {
}
}
// Extract image queue if available (for image generation)
if (meta.image_queue && Array.isArray(meta.image_queue)) {
setImageQueue(meta.image_queue as ImageQueueItem[]);
}
// Update step logs if available
if (meta.request_steps || meta.response_steps) {
// Collect all steps for display in modal
@@ -736,6 +756,7 @@ export function useProgressModal(): UseProgressModalReturn {
displayedPercentageRef.current = 0;
currentStepRef.current = null;
setStepLogs([]); // Clear step logs when closing modal
setImageQueue(undefined); // Clear image queue when closing modal
setIsOpen(false);
// Clear taskId to stop polling when modal closes
setTaskId(null);
@@ -768,6 +789,8 @@ export function useProgressModal(): UseProgressModalReturn {
message: 'Getting started...',
status: 'pending',
});
setStepLogs([]);
setImageQueue(undefined);
setTaskId(null);
setTitle('');
setIsOpen(false);
@@ -785,6 +808,7 @@ export function useProgressModal(): UseProgressModalReturn {
taskId, // Expose taskId for use in ProgressModal
functionId, // Expose functionId for use in ProgressModal
stepLogs, // Expose step logs for debugging
imageQueue, // Expose image queue for image generation
};
}

View File

@@ -15,6 +15,7 @@ import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon, DownloadIcon, BoltIcon } from '../../icons';
import { createImagesPageConfig } from '../../config/pages/images.config';
import ProgressModal from '../../components/common/ProgressModal';
import ImageQueueModal from '../../components/common/ImageQueueModal';
import { useProgressModal } from '../../hooks/useProgressModal';
export default function Images() {
@@ -284,29 +285,49 @@ export default function Images() {
}}
/>
{/* Progress Modal for AI Functions */}
<ProgressModal
isOpen={progressModal.isOpen}
title={progressModal.title}
percentage={progressModal.progress.percentage}
status={progressModal.progress.status}
message={progressModal.progress.message}
details={progressModal.progress.details}
taskId={progressModal.taskId || undefined}
functionId={progressModal.functionId}
onClose={() => {
const wasCompleted = progressModal.progress.status === 'completed';
progressModal.closeModal();
// Reload data after modal closes (if completed)
if (wasCompleted && !hasReloadedRef.current) {
hasReloadedRef.current = true;
loadImages();
setTimeout(() => {
hasReloadedRef.current = false;
}, 1000);
}
}}
/>
{/* Image Queue Modal for Image Generation */}
{progressModal.imageQueue && progressModal.imageQueue.length > 0 ? (
<ImageQueueModal
isOpen={progressModal.isOpen}
queue={progressModal.imageQueue}
onClose={() => {
const wasCompleted = progressModal.progress.status === 'completed';
progressModal.closeModal();
// Reload data after modal closes (if completed)
if (wasCompleted && !hasReloadedRef.current) {
hasReloadedRef.current = true;
loadImages();
setTimeout(() => {
hasReloadedRef.current = false;
}, 1000);
}
}}
/>
) : (
/* Progress Modal for other AI Functions */
<ProgressModal
isOpen={progressModal.isOpen}
title={progressModal.title}
percentage={progressModal.progress.percentage}
status={progressModal.progress.status}
message={progressModal.progress.message}
details={progressModal.progress.details}
taskId={progressModal.taskId || undefined}
functionId={progressModal.functionId}
onClose={() => {
const wasCompleted = progressModal.progress.status === 'completed';
progressModal.closeModal();
// Reload data after modal closes (if completed)
if (wasCompleted && !hasReloadedRef.current) {
hasReloadedRef.current = true;
loadImages();
setTimeout(() => {
hasReloadedRef.current = false;
}, 1000);
}
}}
/>
)}
</>
);
}