|
@@ -1,6 +1,6 @@
|
|
|
'use client';
|
|
'use client';
|
|
|
|
|
|
|
|
-import { useState, useMemo } from 'react';
|
|
|
|
|
|
|
+import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
import { Button } from '@/components/ui/button';
|
|
import { Button } from '@/components/ui/button';
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Badge } from '@/components/ui/badge';
|
|
@@ -38,6 +38,39 @@ interface EnhancedQueueListProps {
|
|
|
|
|
|
|
|
type ViewMode = 'compact' | 'detailed';
|
|
type ViewMode = 'compact' | 'detailed';
|
|
|
|
|
|
|
|
|
|
+// Debounce utility for frequent updates
|
|
|
|
|
+function useDebounce<T>(value: T, delay: number): T {
|
|
|
|
|
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const handler = setTimeout(() => {
|
|
|
|
|
+ setDebouncedValue(value);
|
|
|
|
|
+ }, delay);
|
|
|
|
|
+
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ clearTimeout(handler);
|
|
|
|
|
+ };
|
|
|
|
|
+ }, [value, delay]);
|
|
|
|
|
+
|
|
|
|
|
+ return debouncedValue;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Throttle utility for performance
|
|
|
|
|
+function useThrottle<T>(value: T, delay: number): T {
|
|
|
|
|
+ const [throttledValue, setThrottledValue] = useState<T>(value);
|
|
|
|
|
+ const lastExecuted = useRef<number>(0);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const now = Date.now();
|
|
|
|
|
+ if (now - lastExecuted.current >= delay) {
|
|
|
|
|
+ setThrottledValue(value);
|
|
|
|
|
+ lastExecuted.current = now;
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [value, delay]);
|
|
|
|
|
+
|
|
|
|
|
+ return throttledValue;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export function EnhancedQueueList({
|
|
export function EnhancedQueueList({
|
|
|
queueStatus,
|
|
queueStatus,
|
|
|
loading,
|
|
loading,
|
|
@@ -49,110 +82,161 @@ export function EnhancedQueueList({
|
|
|
}: EnhancedQueueListProps) {
|
|
}: EnhancedQueueListProps) {
|
|
|
const [viewMode, setViewMode] = useState<ViewMode>('detailed');
|
|
const [viewMode, setViewMode] = useState<ViewMode>('detailed');
|
|
|
const [selectedJob, setSelectedJob] = useState<string | null>(null);
|
|
const [selectedJob, setSelectedJob] = useState<string | null>(null);
|
|
|
|
|
+ const [lastUpdateTime, setLastUpdateTime] = useState(Date.now());
|
|
|
|
|
+
|
|
|
|
|
+ // Debounce the queue status to prevent excessive updates
|
|
|
|
|
+ const debouncedQueueStatus = useDebounce(queueStatus, 100);
|
|
|
|
|
+
|
|
|
|
|
+ // Throttle progress updates to reduce rendering frequency
|
|
|
|
|
+ const throttledJobs = useThrottle(debouncedQueueStatus?.jobs || [], 200);
|
|
|
|
|
+
|
|
|
|
|
+ // Update the last update time when we get new data
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (queueStatus?.jobs) {
|
|
|
|
|
+ setLastUpdateTime(Date.now());
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [queueStatus?.jobs]);
|
|
|
|
|
|
|
|
- // Calculate queue statistics
|
|
|
|
|
|
|
+ // Calculate queue statistics with memoization and throttling
|
|
|
const queueStats = useMemo(() => {
|
|
const queueStats = useMemo(() => {
|
|
|
- if (!queueStatus?.jobs) return { total: 0, active: 0, queued: 0, completed: 0, failed: 0 };
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- total: queueStatus.jobs.length,
|
|
|
|
|
- active: queueStatus.jobs.filter(job => job.status === 'processing').length,
|
|
|
|
|
- queued: queueStatus.jobs.filter(job => job.status === 'queued').length,
|
|
|
|
|
- completed: queueStatus.jobs.filter(job => job.status === 'completed').length,
|
|
|
|
|
- failed: queueStatus.jobs.filter(job => job.status === 'failed').length,
|
|
|
|
|
|
|
+ if (!throttledJobs.length) return { total: 0, active: 0, queued: 0, completed: 0, failed: 0 };
|
|
|
|
|
+
|
|
|
|
|
+ // Use throttled jobs to reduce computation frequency
|
|
|
|
|
+ const stats = {
|
|
|
|
|
+ total: throttledJobs.length,
|
|
|
|
|
+ active: 0,
|
|
|
|
|
+ queued: 0,
|
|
|
|
|
+ completed: 0,
|
|
|
|
|
+ failed: 0,
|
|
|
};
|
|
};
|
|
|
- }, [queueStatus]);
|
|
|
|
|
|
|
|
|
|
- // Sort jobs by status and creation time
|
|
|
|
|
|
|
+ // Optimized counting with single pass
|
|
|
|
|
+ throttledJobs.forEach(job => {
|
|
|
|
|
+ switch (job.status) {
|
|
|
|
|
+ case 'processing':
|
|
|
|
|
+ stats.active++;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'queued':
|
|
|
|
|
+ stats.queued++;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'completed':
|
|
|
|
|
+ stats.completed++;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'failed':
|
|
|
|
|
+ stats.failed++;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return stats;
|
|
|
|
|
+ }, [throttledJobs]);
|
|
|
|
|
+
|
|
|
|
|
+ // Memoized job sorting with better performance
|
|
|
const sortedJobs = useMemo(() => {
|
|
const sortedJobs = useMemo(() => {
|
|
|
- if (!queueStatus?.jobs) return [];
|
|
|
|
|
|
|
+ if (!throttledJobs.length) return [];
|
|
|
|
|
|
|
|
const statusPriority = { processing: 0, queued: 1, pending: 2, completed: 3, failed: 4, cancelled: 5 };
|
|
const statusPriority = { processing: 0, queued: 1, pending: 2, completed: 3, failed: 4, cancelled: 5 };
|
|
|
|
|
|
|
|
- return [...queueStatus.jobs].sort((a, b) => {
|
|
|
|
|
- const statusDiff = statusPriority[a.status as keyof typeof statusPriority] -
|
|
|
|
|
- statusPriority[b.status as keyof typeof statusPriority];
|
|
|
|
|
- if (statusDiff !== 0) return statusDiff;
|
|
|
|
|
|
|
+ // Use a more efficient sorting approach
|
|
|
|
|
+ const jobsByStatus: Record<string, JobInfo[]> = {
|
|
|
|
|
+ processing: [],
|
|
|
|
|
+ queued: [],
|
|
|
|
|
+ pending: [],
|
|
|
|
|
+ completed: [],
|
|
|
|
|
+ failed: [],
|
|
|
|
|
+ cancelled: [],
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- // If same status, sort by creation time (newest first)
|
|
|
|
|
- const timeA = new Date(a.created_at || 0).getTime();
|
|
|
|
|
- const timeB = new Date(b.created_at || 0).getTime();
|
|
|
|
|
- return timeB - timeA;
|
|
|
|
|
|
|
+ // Group jobs by status first
|
|
|
|
|
+ throttledJobs.forEach(job => {
|
|
|
|
|
+ const status = job.status as keyof typeof jobsByStatus;
|
|
|
|
|
+ if (jobsByStatus[status]) {
|
|
|
|
|
+ jobsByStatus[status].push(job);
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
- }, [queueStatus]);
|
|
|
|
|
-
|
|
|
|
|
- const getStatusIcon = (status: string) => {
|
|
|
|
|
- switch (status) {
|
|
|
|
|
- case 'completed':
|
|
|
|
|
- return <CheckCircle2 className="h-4 w-4 text-green-500" />;
|
|
|
|
|
- case 'failed':
|
|
|
|
|
- return <XCircle className="h-4 w-4 text-red-500" />;
|
|
|
|
|
- case 'processing':
|
|
|
|
|
- return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />;
|
|
|
|
|
- case 'queued':
|
|
|
|
|
- return <Clock className="h-4 w-4 text-yellow-500" />;
|
|
|
|
|
- case 'cancelled':
|
|
|
|
|
- return <XCircle className="h-4 w-4 text-gray-500" />;
|
|
|
|
|
- default:
|
|
|
|
|
- return <AlertCircle className="h-4 w-4 text-gray-500" />;
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const getStatusColor = (status: string) => {
|
|
|
|
|
- switch (status) {
|
|
|
|
|
- case 'completed':
|
|
|
|
|
- return 'text-green-600 dark:text-green-400';
|
|
|
|
|
- case 'failed':
|
|
|
|
|
- return 'text-red-600 dark:text-red-400';
|
|
|
|
|
- case 'processing':
|
|
|
|
|
- return 'text-blue-600 dark:text-blue-400';
|
|
|
|
|
- case 'queued':
|
|
|
|
|
- return 'text-yellow-600 dark:text-yellow-400';
|
|
|
|
|
- case 'cancelled':
|
|
|
|
|
- return 'text-gray-600 dark:text-gray-400';
|
|
|
|
|
- default:
|
|
|
|
|
- return 'text-gray-600 dark:text-gray-400';
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const getStatusBadgeVariant = (status: string): 'default' | 'secondary' | 'destructive' | 'outline' => {
|
|
|
|
|
- switch (status) {
|
|
|
|
|
- case 'completed':
|
|
|
|
|
- return 'default';
|
|
|
|
|
- case 'failed':
|
|
|
|
|
- return 'destructive';
|
|
|
|
|
- case 'processing':
|
|
|
|
|
- return 'secondary';
|
|
|
|
|
- case 'queued':
|
|
|
|
|
- return 'outline';
|
|
|
|
|
- case 'cancelled':
|
|
|
|
|
- return 'outline';
|
|
|
|
|
- default:
|
|
|
|
|
- return 'outline';
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Sort within each status group and concatenate
|
|
|
|
|
+ const result: JobInfo[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ Object.entries(statusPriority)
|
|
|
|
|
+ .sort(([,a], [,b]) => a - b)
|
|
|
|
|
+ .forEach(([status]) => {
|
|
|
|
|
+ const statusJobs = jobsByStatus[status] || [];
|
|
|
|
|
+ statusJobs.sort((a, b) => {
|
|
|
|
|
+ const timeA = new Date(a.created_at || 0).getTime();
|
|
|
|
|
+ const timeB = new Date(b.created_at || 0).getTime();
|
|
|
|
|
+ return timeB - timeA; // Newest first
|
|
|
|
|
+ });
|
|
|
|
|
+ result.push(...statusJobs);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }, [throttledJobs]);
|
|
|
|
|
+
|
|
|
|
|
+ // Memoized status icons and colors to prevent recreation
|
|
|
|
|
+ const statusConfig = useMemo(() => ({
|
|
|
|
|
+ icons: {
|
|
|
|
|
+ completed: <CheckCircle2 className="h-4 w-4 text-green-500" />,
|
|
|
|
|
+ failed: <XCircle className="h-4 w-4 text-red-500" />,
|
|
|
|
|
+ processing: <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />,
|
|
|
|
|
+ queued: <Clock className="h-4 w-4 text-yellow-500" />,
|
|
|
|
|
+ cancelled: <XCircle className="h-4 w-4 text-gray-500" />,
|
|
|
|
|
+ pending: <AlertCircle className="h-4 w-4 text-gray-500" />,
|
|
|
|
|
+ },
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ completed: 'text-green-600 dark:text-green-400',
|
|
|
|
|
+ failed: 'text-red-600 dark:text-red-400',
|
|
|
|
|
+ processing: 'text-blue-600 dark:text-blue-400',
|
|
|
|
|
+ queued: 'text-yellow-600 dark:text-yellow-400',
|
|
|
|
|
+ cancelled: 'text-gray-600 dark:text-gray-400',
|
|
|
|
|
+ pending: 'text-gray-600 dark:text-gray-400',
|
|
|
|
|
+ },
|
|
|
|
|
+ badges: {
|
|
|
|
|
+ completed: 'default' as const,
|
|
|
|
|
+ failed: 'destructive' as const,
|
|
|
|
|
+ processing: 'secondary' as const,
|
|
|
|
|
+ queued: 'outline' as const,
|
|
|
|
|
+ cancelled: 'outline' as const,
|
|
|
|
|
+ pending: 'outline' as const,
|
|
|
}
|
|
}
|
|
|
- };
|
|
|
|
|
|
|
+ }), []);
|
|
|
|
|
+
|
|
|
|
|
+ const getStatusIcon = useCallback((status: string) => {
|
|
|
|
|
+ return statusConfig.icons[status as keyof typeof statusConfig.icons] || statusConfig.icons.pending;
|
|
|
|
|
+ }, [statusConfig]);
|
|
|
|
|
+
|
|
|
|
|
+ const getStatusColor = useCallback((status: string) => {
|
|
|
|
|
+ return statusConfig.colors[status as keyof typeof statusConfig.colors] || statusConfig.colors.pending;
|
|
|
|
|
+ }, [statusConfig]);
|
|
|
|
|
|
|
|
- const formatDuration = (startTime: string, endTime?: string) => {
|
|
|
|
|
|
|
+ const getStatusBadgeVariant = useCallback((status: string) => {
|
|
|
|
|
+ return statusConfig.badges[status as keyof typeof statusConfig.badges] || 'outline';
|
|
|
|
|
+ }, [statusConfig]);
|
|
|
|
|
+
|
|
|
|
|
+ // Optimized duration formatting
|
|
|
|
|
+ const formatDuration = useCallback((startTime: string, endTime?: string) => {
|
|
|
|
|
+ if (!startTime) return 'Unknown';
|
|
|
|
|
+
|
|
|
const start = new Date(startTime).getTime();
|
|
const start = new Date(startTime).getTime();
|
|
|
const end = endTime ? new Date(endTime).getTime() : Date.now();
|
|
const end = endTime ? new Date(endTime).getTime() : Date.now();
|
|
|
- const duration = Math.floor((end - start) / 1000);
|
|
|
|
|
|
|
+ const duration = Math.max(0, Math.floor((end - start) / 1000));
|
|
|
|
|
|
|
|
if (duration < 60) return `${duration}s`;
|
|
if (duration < 60) return `${duration}s`;
|
|
|
if (duration < 3600) return `${Math.floor(duration / 60)}m ${duration % 60}s`;
|
|
if (duration < 3600) return `${Math.floor(duration / 60)}m ${duration % 60}s`;
|
|
|
return `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
|
|
return `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
|
|
|
- };
|
|
|
|
|
|
|
+ }, []);
|
|
|
|
|
|
|
|
- const getJobType = (job: JobInfo) => {
|
|
|
|
|
- // Try to determine job type from message or other properties
|
|
|
|
|
- const message = job.message?.toLowerCase() || '';
|
|
|
|
|
|
|
+ // Memoized job type detection
|
|
|
|
|
+ const getJobType = useCallback((job: JobInfo) => {
|
|
|
|
|
+ const message = (job.message || '').toLowerCase();
|
|
|
if (message.includes('text2img') || message.includes('text to image')) return 'Text to Image';
|
|
if (message.includes('text2img') || message.includes('text to image')) return 'Text to Image';
|
|
|
if (message.includes('img2img') || message.includes('image to image')) return 'Image to Image';
|
|
if (message.includes('img2img') || message.includes('image to image')) return 'Image to Image';
|
|
|
if (message.includes('upscale') || message.includes('upscaler')) return 'Upscale';
|
|
if (message.includes('upscale') || message.includes('upscaler')) return 'Upscale';
|
|
|
if (message.includes('convert') || message.includes('conversion')) return 'Model Conversion';
|
|
if (message.includes('convert') || message.includes('conversion')) return 'Model Conversion';
|
|
|
return 'Unknown';
|
|
return 'Unknown';
|
|
|
- };
|
|
|
|
|
|
|
+ }, []);
|
|
|
|
|
|
|
|
- const getJobTypeIcon = (job: JobInfo) => {
|
|
|
|
|
|
|
+ const getJobTypeIcon = useCallback((job: JobInfo) => {
|
|
|
const type = getJobType(job);
|
|
const type = getJobType(job);
|
|
|
switch (type) {
|
|
switch (type) {
|
|
|
case 'Text to Image':
|
|
case 'Text to Image':
|
|
@@ -166,43 +250,101 @@ export function EnhancedQueueList({
|
|
|
default:
|
|
default:
|
|
|
return <FileText className="h-4 w-4" />;
|
|
return <FileText className="h-4 w-4" />;
|
|
|
}
|
|
}
|
|
|
- };
|
|
|
|
|
|
|
+ }, [getJobType]);
|
|
|
|
|
|
|
|
- const extractParameters = (job: JobInfo) => {
|
|
|
|
|
- // Try to extract parameters from message or other job properties
|
|
|
|
|
|
|
+ // Optimized parameter extraction
|
|
|
|
|
+ const extractParameters = useCallback((job: JobInfo) => {
|
|
|
|
|
+ if (!job.message) return {};
|
|
|
|
|
+
|
|
|
const params: Record<string, any> = {};
|
|
const params: Record<string, any> = {};
|
|
|
-
|
|
|
|
|
- if (job.message) {
|
|
|
|
|
- // Try to parse parameters from message (this is a simplified approach)
|
|
|
|
|
- const message = job.message;
|
|
|
|
|
- const promptMatch = message.match(/prompt[:\s]+(.+?)(?:\n|$)/i);
|
|
|
|
|
- if (promptMatch) params.prompt = promptMatch[1].trim();
|
|
|
|
|
-
|
|
|
|
|
- const stepsMatch = message.match(/steps[:\s]+(\d+)/i);
|
|
|
|
|
- if (stepsMatch) params.steps = parseInt(stepsMatch[1]);
|
|
|
|
|
-
|
|
|
|
|
- const cfgMatch = message.match(/cfg[:\s]+([\d.]+)/i);
|
|
|
|
|
- if (cfgMatch) params.cfg_scale = parseFloat(cfgMatch[1]);
|
|
|
|
|
-
|
|
|
|
|
- const sizeMatch = message.match(/(\d+)x(\d+)/);
|
|
|
|
|
- if (sizeMatch) {
|
|
|
|
|
- params.width = parseInt(sizeMatch[1]);
|
|
|
|
|
- params.height = parseInt(sizeMatch[2]);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const message = job.message;
|
|
|
|
|
+
|
|
|
|
|
+ // Use more efficient regex patterns
|
|
|
|
|
+ const promptMatch = message.match(/prompt[:\s]+([^\n]+)/i);
|
|
|
|
|
+ if (promptMatch) params.prompt = promptMatch[1].trim().substring(0, 100); // Limit length
|
|
|
|
|
+
|
|
|
|
|
+ const stepsMatch = message.match(/steps[:\s]+(\d+)/i);
|
|
|
|
|
+ if (stepsMatch) params.steps = parseInt(stepsMatch[1], 10);
|
|
|
|
|
+
|
|
|
|
|
+ const cfgMatch = message.match(/cfg[:\s]+([\d.]+)/i);
|
|
|
|
|
+ if (cfgMatch) params.cfg_scale = parseFloat(cfgMatch[1]);
|
|
|
|
|
+
|
|
|
|
|
+ const sizeMatch = message.match(/(\d+)x(\d+)/);
|
|
|
|
|
+ if (sizeMatch) {
|
|
|
|
|
+ params.width = parseInt(sizeMatch[1], 10);
|
|
|
|
|
+ params.height = parseInt(sizeMatch[2], 10);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return params;
|
|
return params;
|
|
|
- };
|
|
|
|
|
|
|
+ }, []);
|
|
|
|
|
|
|
|
- const copyParameters = (job: JobInfo) => {
|
|
|
|
|
|
|
+ // Debounced copy function to prevent spam
|
|
|
|
|
+ const copyParameters = useCallback((job: JobInfo) => {
|
|
|
const params = extractParameters(job);
|
|
const params = extractParameters(job);
|
|
|
|
|
+ if (Object.keys(params).length === 0) return;
|
|
|
|
|
+
|
|
|
const paramsText = Object.entries(params)
|
|
const paramsText = Object.entries(params)
|
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
|
.join('\n');
|
|
.join('\n');
|
|
|
-
|
|
|
|
|
- navigator.clipboard.writeText(paramsText);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Use clipboard API with fallback
|
|
|
|
|
+ if (navigator.clipboard?.writeText) {
|
|
|
|
|
+ navigator.clipboard.writeText(paramsText).catch(() => {
|
|
|
|
|
+ // Fallback for older browsers
|
|
|
|
|
+ const textArea = document.createElement('textarea');
|
|
|
|
|
+ textArea.value = paramsText;
|
|
|
|
|
+ document.body.appendChild(textArea);
|
|
|
|
|
+ textArea.select();
|
|
|
|
|
+ document.execCommand('copy');
|
|
|
|
|
+ document.body.removeChild(textArea);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
onCopyParameters?.(job);
|
|
onCopyParameters?.(job);
|
|
|
- };
|
|
|
|
|
|
|
+ }, [extractParameters, onCopyParameters]);
|
|
|
|
|
+
|
|
|
|
|
+ // Memoized event handlers
|
|
|
|
|
+ const handleSelectedJobToggle = useCallback((jobId: string | null) => {
|
|
|
|
|
+ setSelectedJob(current => current === jobId ? null : jobId);
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ const handleCancelJob = useCallback((jobId: string) => {
|
|
|
|
|
+ onCancelJob(jobId);
|
|
|
|
|
+ }, [onCancelJob]);
|
|
|
|
|
+
|
|
|
|
|
+ // Progress update optimization - only update progress if it changed significantly
|
|
|
|
|
+ const ProgressBar = useCallback(({ job }: { job: JobInfo }) => {
|
|
|
|
|
+ if (job.progress === undefined) return null;
|
|
|
|
|
+
|
|
|
|
|
+ const progressValue = job.progress * 100;
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <div className="flex items-center justify-between text-sm">
|
|
|
|
|
+ <span className={cn("font-medium", getStatusColor(job.status))}>
|
|
|
|
|
+ {job.message || 'Processing...'}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span className="text-muted-foreground">
|
|
|
|
|
+ {Math.round(progressValue)}%
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Progress value={progressValue} className="h-2" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }, [getStatusColor]);
|
|
|
|
|
+
|
|
|
|
|
+ // Skip rendering if no meaningful changes
|
|
|
|
|
+ if (!queueStatus && !loading) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="space-y-6">
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardContent className="text-center py-12">
|
|
|
|
|
+ <Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
|
|
|
|
+ <p className="text-muted-foreground">Loading queue status...</p>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className="space-y-6">
|
|
<div className="space-y-6">
|
|
@@ -214,6 +356,11 @@ export function EnhancedQueueList({
|
|
|
<CardTitle>Queue Status</CardTitle>
|
|
<CardTitle>Queue Status</CardTitle>
|
|
|
<CardDescription>
|
|
<CardDescription>
|
|
|
Current queue status and statistics
|
|
Current queue status and statistics
|
|
|
|
|
+ {lastUpdateTime && (
|
|
|
|
|
+ <span className="text-xs text-muted-foreground block">
|
|
|
|
|
+ Last updated: {new Date(lastUpdateTime).toLocaleTimeString()}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
</CardDescription>
|
|
</CardDescription>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="flex gap-2">
|
|
<div className="flex gap-2">
|
|
@@ -237,7 +384,7 @@ export function EnhancedQueueList({
|
|
|
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
|
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
|
|
Refresh
|
|
Refresh
|
|
|
</Button>
|
|
</Button>
|
|
|
- <Button variant="outline" onClick={onClearQueue} disabled={actionLoading || !queueStatus?.jobs.length}>
|
|
|
|
|
|
|
+ <Button variant="outline" onClick={onClearQueue} disabled={actionLoading || !throttledJobs.length}>
|
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
|
Clear Queue
|
|
Clear Queue
|
|
|
</Button>
|
|
</Button>
|
|
@@ -272,255 +419,254 @@ export function EnhancedQueueList({
|
|
|
|
|
|
|
|
{/* Jobs List */}
|
|
{/* Jobs List */}
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
|
- {sortedJobs.map(job => (
|
|
|
|
|
- <Card key={job.id || job.request_id} className={cn(
|
|
|
|
|
- "transition-all hover:shadow-md",
|
|
|
|
|
- job.status === 'processing' && 'border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/20',
|
|
|
|
|
- job.status === 'completed' && 'border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20',
|
|
|
|
|
- job.status === 'failed' && 'border-red-200 bg-red-50/50 dark:border-red-800 dark:bg-red-950/20'
|
|
|
|
|
- )}>
|
|
|
|
|
- <CardContent className="p-6">
|
|
|
|
|
- {viewMode === 'detailed' ? (
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- {/* Header */}
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <div className="flex items-center gap-3">
|
|
|
|
|
- {getStatusIcon(job.status)}
|
|
|
|
|
- <div>
|
|
|
|
|
- <h3 className="font-semibold">Job {job.id || job.request_id}</h3>
|
|
|
|
|
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
|
|
- {getJobTypeIcon(job)}
|
|
|
|
|
- <span>{getJobType(job)}</span>
|
|
|
|
|
- {job.queue_position !== undefined && (
|
|
|
|
|
- <span>• Position: {job.queue_position}</span>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ {sortedJobs.map(job => {
|
|
|
|
|
+ const jobId = job.id || job.request_id;
|
|
|
|
|
+ if (!jobId) return null;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Card key={jobId} className={cn(
|
|
|
|
|
+ "transition-all hover:shadow-md",
|
|
|
|
|
+ job.status === 'processing' && 'border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/20',
|
|
|
|
|
+ job.status === 'completed' && 'border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20',
|
|
|
|
|
+ job.status === 'failed' && 'border-red-200 bg-red-50/50 dark:border-red-800 dark:bg-red-950/20'
|
|
|
|
|
+ )}>
|
|
|
|
|
+ <CardContent className="p-6">
|
|
|
|
|
+ {viewMode === 'detailed' ? (
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ {/* Header */}
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ {getStatusIcon(job.status)}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h3 className="font-semibold">Job {jobId}</h3>
|
|
|
|
|
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
|
|
+ {getJobTypeIcon(job)}
|
|
|
|
|
+ <span>{getJobType(job)}</span>
|
|
|
|
|
+ {job.queue_position !== undefined && (
|
|
|
|
|
+ <span>• Position: {job.queue_position}</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- <div className="flex items-center gap-2">
|
|
|
|
|
- <Badge variant={getStatusBadgeVariant(job.status)}>
|
|
|
|
|
- {job.status}
|
|
|
|
|
- </Badge>
|
|
|
|
|
- {(job.status === 'queued' || job.status === 'processing') && (
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- onClick={() => onCancelJob(job.id || job.request_id!)}
|
|
|
|
|
- disabled={actionLoading}
|
|
|
|
|
- >
|
|
|
|
|
- <XCircle className="h-4 w-4" />
|
|
|
|
|
- Cancel
|
|
|
|
|
- </Button>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* Progress */}
|
|
|
|
|
- {job.progress !== undefined && (
|
|
|
|
|
- <div className="space-y-2">
|
|
|
|
|
- <div className="flex items-center justify-between text-sm">
|
|
|
|
|
- <span className={cn("font-medium", getStatusColor(job.status))}>
|
|
|
|
|
- {job.message || 'Processing...'}
|
|
|
|
|
- </span>
|
|
|
|
|
- <span className="text-muted-foreground">
|
|
|
|
|
- {Math.round(job.progress * 100)}%
|
|
|
|
|
- </span>
|
|
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Badge variant={getStatusBadgeVariant(job.status)}>
|
|
|
|
|
+ {job.status}
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+ {(job.status === 'queued' || job.status === 'processing') && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => handleCancelJob(jobId)}
|
|
|
|
|
+ disabled={actionLoading}
|
|
|
|
|
+ >
|
|
|
|
|
+ <XCircle className="h-4 w-4" />
|
|
|
|
|
+ Cancel
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
- <Progress value={job.progress} className="h-2" />
|
|
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* Details Grid */}
|
|
|
|
|
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
|
|
|
- <div>
|
|
|
|
|
- <span className="text-muted-foreground flex items-center gap-1">
|
|
|
|
|
- <Calendar className="h-3 w-3" />
|
|
|
|
|
- Created
|
|
|
|
|
- </span>
|
|
|
|
|
- <p className="font-medium">
|
|
|
|
|
- {job.created_at ? new Date(job.created_at).toLocaleString() : 'Unknown'}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- {job.updated_at && job.updated_at !== job.created_at && (
|
|
|
|
|
|
|
+
|
|
|
|
|
+ {/* Progress */}
|
|
|
|
|
+ <ProgressBar job={job} />
|
|
|
|
|
+
|
|
|
|
|
+ {/* Details Grid */}
|
|
|
|
|
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
|
<div>
|
|
<div>
|
|
|
<span className="text-muted-foreground flex items-center gap-1">
|
|
<span className="text-muted-foreground flex items-center gap-1">
|
|
|
- <Clock className="h-3 w-3" />
|
|
|
|
|
- Updated
|
|
|
|
|
|
|
+ <Calendar className="h-3 w-3" />
|
|
|
|
|
+ Created
|
|
|
</span>
|
|
</span>
|
|
|
<p className="font-medium">
|
|
<p className="font-medium">
|
|
|
- {new Date(job.updated_at).toLocaleString()}
|
|
|
|
|
|
|
+ {job.created_at ? new Date(job.created_at).toLocaleString() : 'Unknown'}
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
- {job.created_at && (
|
|
|
|
|
|
|
+ {job.updated_at && job.updated_at !== job.created_at && (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <span className="text-muted-foreground flex items-center gap-1">
|
|
|
|
|
+ <Clock className="h-3 w-3" />
|
|
|
|
|
+ Updated
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <p className="font-medium">
|
|
|
|
|
+ {new Date(job.updated_at).toLocaleString()}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {job.created_at && (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <span className="text-muted-foreground flex items-center gap-1">
|
|
|
|
|
+ <Clock className="h-3 w-3" />
|
|
|
|
|
+ Duration
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <p className="font-medium">
|
|
|
|
|
+ {formatDuration(job.created_at, job.updated_at)}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
<div>
|
|
<div>
|
|
|
<span className="text-muted-foreground flex items-center gap-1">
|
|
<span className="text-muted-foreground flex items-center gap-1">
|
|
|
- <Clock className="h-3 w-3" />
|
|
|
|
|
- Duration
|
|
|
|
|
|
|
+ <FileText className="h-3 w-3" />
|
|
|
|
|
+ Parameters
|
|
|
</span>
|
|
</span>
|
|
|
- <p className="font-medium">
|
|
|
|
|
- {formatDuration(job.created_at, job.updated_at)}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- <div>
|
|
|
|
|
- <span className="text-muted-foreground flex items-center gap-1">
|
|
|
|
|
- <FileText className="h-3 w-3" />
|
|
|
|
|
- Parameters
|
|
|
|
|
- </span>
|
|
|
|
|
- <div className="flex gap-1">
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- className="h-6 px-2 text-xs"
|
|
|
|
|
- onClick={() => copyParameters(job)}
|
|
|
|
|
- >
|
|
|
|
|
- <Copy className="h-3 w-3 mr-1" />
|
|
|
|
|
- Copy
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- className="h-6 px-2 text-xs"
|
|
|
|
|
- onClick={() => setSelectedJob(selectedJob === (job.id || job.request_id) ? null : (job.id || job.request_id || null))}
|
|
|
|
|
- >
|
|
|
|
|
- {selectedJob === (job.id || job.request_id) ? 'Hide' : 'Show'}
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+ <div className="flex gap-1">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ className="h-6 px-2 text-xs"
|
|
|
|
|
+ onClick={() => copyParameters(job)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Copy className="h-3 w-3 mr-1" />
|
|
|
|
|
+ Copy
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ className="h-6 px-2 text-xs"
|
|
|
|
|
+ onClick={() => handleSelectedJobToggle(selectedJob === jobId ? null : jobId)}
|
|
|
|
|
+ >
|
|
|
|
|
+ {selectedJob === jobId ? 'Hide' : 'Show'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- {/* Expanded Parameters */}
|
|
|
|
|
- {selectedJob === (job.id || job.request_id) && (
|
|
|
|
|
- <div className="bg-muted/50 rounded-lg p-4 space-y-2">
|
|
|
|
|
- <h4 className="font-medium text-sm">Job Parameters</h4>
|
|
|
|
|
- <div className="grid gap-2 text-sm">
|
|
|
|
|
- {Object.entries(extractParameters(job)).map(([key, value]) => (
|
|
|
|
|
- <div key={key} className="flex justify-between">
|
|
|
|
|
- <span className="text-muted-foreground capitalize">{key}:</span>
|
|
|
|
|
- <span className="font-mono text-xs">{String(value)}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
- {Object.keys(extractParameters(job)).length === 0 && (
|
|
|
|
|
- <p className="text-muted-foreground text-sm">No parameters available</p>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* Results */}
|
|
|
|
|
- {job.status === 'completed' && job.result?.images && (
|
|
|
|
|
- <div className="space-y-2">
|
|
|
|
|
- <h4 className="font-medium text-sm flex items-center gap-1">
|
|
|
|
|
- <Image className="h-4 w-4" />
|
|
|
|
|
- Generated Images ({job.result.images.length})
|
|
|
|
|
- </h4>
|
|
|
|
|
- <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
|
|
|
|
- {job.result.images.map((imageData, index) => (
|
|
|
|
|
- <div key={index} className="relative group">
|
|
|
|
|
- <div className="aspect-square bg-muted rounded-lg overflow-hidden">
|
|
|
|
|
- {imageData.startsWith('data:image') ? (
|
|
|
|
|
- <img
|
|
|
|
|
- src={imageData}
|
|
|
|
|
- alt={`Generated image ${index + 1}`}
|
|
|
|
|
- className="w-full h-full object-cover"
|
|
|
|
|
- />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <div className="w-full h-full flex items-center justify-center text-muted-foreground">
|
|
|
|
|
- <Image className="h-8 w-8" />
|
|
|
|
|
|
|
+ {/* Expanded Parameters */}
|
|
|
|
|
+ {selectedJob === jobId && (
|
|
|
|
|
+ <div className="bg-muted/50 rounded-lg p-4 space-y-2">
|
|
|
|
|
+ <h4 className="font-medium text-sm">Job Parameters</h4>
|
|
|
|
|
+ <div className="grid gap-2 text-sm">
|
|
|
|
|
+ {(() => {
|
|
|
|
|
+ const params = extractParameters(job);
|
|
|
|
|
+ const entries = Object.entries(params);
|
|
|
|
|
+ return entries.length > 0 ? (
|
|
|
|
|
+ entries.map(([key, value]) => (
|
|
|
|
|
+ <div key={key} className="flex justify-between">
|
|
|
|
|
+ <span className="text-muted-foreground capitalize">{key}:</span>
|
|
|
|
|
+ <span className="font-mono text-xs">{String(value)}</span>
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center">
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="secondary"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- onClick={() => {
|
|
|
|
|
- if (imageData.startsWith('data:image')) {
|
|
|
|
|
- const link = document.createElement('a');
|
|
|
|
|
- link.href = imageData;
|
|
|
|
|
- link.download = `generated-image-${index + 1}.png`;
|
|
|
|
|
- link.click();
|
|
|
|
|
- }
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- Download
|
|
|
|
|
- </Button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ ))
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <p className="text-muted-foreground text-sm">No parameters available</p>
|
|
|
|
|
+ );
|
|
|
|
|
+ })()}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* Error */}
|
|
|
|
|
- {job.error && (
|
|
|
|
|
- <div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
|
|
|
|
|
- <p className="text-sm text-red-700 dark:text-red-300">
|
|
|
|
|
- <strong>Error:</strong> {job.error}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- ) : (
|
|
|
|
|
- /* Compact View */
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <div className="flex items-center gap-4">
|
|
|
|
|
- {getStatusIcon(job.status)}
|
|
|
|
|
- <div>
|
|
|
|
|
- <h3 className="font-semibold">Job {job.id || job.request_id}</h3>
|
|
|
|
|
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
|
|
- {getJobTypeIcon(job)}
|
|
|
|
|
- <span>{getJobType(job)}</span>
|
|
|
|
|
- <span>•</span>
|
|
|
|
|
- <span className={getStatusColor(job.status)}>{job.status}</span>
|
|
|
|
|
- {job.queue_position !== undefined && (
|
|
|
|
|
- <span>• Position: {job.queue_position}</span>
|
|
|
|
|
- )}
|
|
|
|
|
- {job.created_at && (
|
|
|
|
|
- <span>• {new Date(job.created_at).toLocaleTimeString()}</span>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Results */}
|
|
|
|
|
+ {job.status === 'completed' && job.result?.images && (
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <h4 className="font-medium text-sm flex items-center gap-1">
|
|
|
|
|
+ <Image className="h-4 w-4" />
|
|
|
|
|
+ Generated Images ({job.result.images.length})
|
|
|
|
|
+ </h4>
|
|
|
|
|
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
|
|
|
|
+ {job.result.images.map((imageData, index) => (
|
|
|
|
|
+ <div key={index} className="relative group">
|
|
|
|
|
+ <div className="aspect-square bg-muted rounded-lg overflow-hidden">
|
|
|
|
|
+ {imageData.startsWith('data:image') ? (
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={imageData}
|
|
|
|
|
+ alt={`Generated image ${index + 1}`}
|
|
|
|
|
+ className="w-full h-full object-cover"
|
|
|
|
|
+ loading="lazy"
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="w-full h-full flex items-center justify-center text-muted-foreground">
|
|
|
|
|
+ <Image className="h-8 w-8" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="secondary"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ if (imageData.startsWith('data:image')) {
|
|
|
|
|
+ const link = document.createElement('a');
|
|
|
|
|
+ link.href = imageData;
|
|
|
|
|
+ link.download = `generated-image-${index + 1}.png`;
|
|
|
|
|
+ link.click();
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ Download
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- {job.message && (
|
|
|
|
|
- <p className="text-sm text-muted-foreground truncate max-w-md">
|
|
|
|
|
- {job.message}
|
|
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Error */}
|
|
|
|
|
+ {job.error && (
|
|
|
|
|
+ <div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
|
|
|
|
|
+ <p className="text-sm text-red-700 dark:text-red-300">
|
|
|
|
|
+ <strong>Error:</strong> {job.error}
|
|
|
</p>
|
|
</p>
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="flex items-center gap-4">
|
|
|
|
|
- {job.progress !== undefined && (
|
|
|
|
|
- <div className="text-right">
|
|
|
|
|
- <div className="text-sm font-medium">
|
|
|
|
|
- {Math.round(job.progress * 100)}%
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="w-24 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
|
|
|
|
- <div
|
|
|
|
|
- className="h-full bg-blue-500 transition-all duration-300"
|
|
|
|
|
- style={{ width: `${job.progress * 100}%` }}
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ /* Compact View */
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div className="flex items-center gap-4">
|
|
|
|
|
+ {getStatusIcon(job.status)}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h3 className="font-semibold">Job {jobId}</h3>
|
|
|
|
|
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
|
|
+ {getJobTypeIcon(job)}
|
|
|
|
|
+ <span>{getJobType(job)}</span>
|
|
|
|
|
+ <span>•</span>
|
|
|
|
|
+ <span className={getStatusColor(job.status)}>{job.status}</span>
|
|
|
|
|
+ {job.queue_position !== undefined && (
|
|
|
|
|
+ <span>• Position: {job.queue_position}</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {job.created_at && (
|
|
|
|
|
+ <span>• {new Date(job.created_at).toLocaleTimeString()}</span>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ {job.message && (
|
|
|
|
|
+ <p className="text-sm text-muted-foreground truncate max-w-md">
|
|
|
|
|
+ {job.message}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
- {(job.status === 'queued' || job.status === 'processing') && (
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- onClick={() => onCancelJob(job.id || job.request_id!)}
|
|
|
|
|
- disabled={actionLoading}
|
|
|
|
|
- >
|
|
|
|
|
- <XCircle className="h-4 w-4" />
|
|
|
|
|
- </Button>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex items-center gap-4">
|
|
|
|
|
+ {job.progress !== undefined && (
|
|
|
|
|
+ <div className="text-right">
|
|
|
|
|
+ <div className="text-sm font-medium">
|
|
|
|
|
+ {Math.round(job.progress * 100)}%
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="w-24 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="h-full bg-blue-500 transition-all duration-300"
|
|
|
|
|
+ style={{ width: `${job.progress * 100}%` }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {(job.status === 'queued' || job.status === 'processing') && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => handleCancelJob(jobId)}
|
|
|
|
|
+ disabled={actionLoading}
|
|
|
|
|
+ >
|
|
|
|
|
+ <XCircle className="h-4 w-4" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ )}
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {(!queueStatus?.jobs || queueStatus.jobs.length === 0) && (
|
|
|
|
|
|
|
+ {(!throttledJobs || throttledJobs.length === 0) && (
|
|
|
<Card>
|
|
<Card>
|
|
|
<CardContent className="text-center py-12">
|
|
<CardContent className="text-center py-12">
|
|
|
<Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
|
<Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
|
@@ -530,4 +676,4 @@ export function EnhancedQueueList({
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
-}
|
|
|
|
|
|
|
+}
|