veo3/frontend/src/components/QueueManager.jsx
2025-10-08 19:03:04 +05:30

304 lines
No EOL
10 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import {
Paper,
Typography,
Box,
List,
ListItem,
Chip,
Button,
LinearProgress,
Grid,
Divider,
Alert,
IconButton,
Tooltip
} from '@mui/material';
import {
CheckCircleRounded,
ErrorRounded,
HourglassEmptyRounded,
PlayArrowRounded,
CancelRounded,
DownloadRounded,
RefreshRounded
} from '@mui/icons-material';
import { JOB_STATUS, API_BASE_URL } from '../utils/constants';
import { getUserJobs, getQueueStatus, cancelJob } from '../services/api';
const QueueManager = ({ userEmail }) => {
const [jobs, setJobs] = useState([]);
const [queueStatus, setQueueStatus] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const fetchUserJobs = async () => {
try {
console.log('Fetching user jobs for:', userEmail);
const data = await getUserJobs(userEmail);
console.log('User jobs data:', data);
setJobs(data.jobs || []);
} catch (err) {
console.error('fetchUserJobs error:', err);
setError(`Failed to load jobs: ${err.message}`);
}
};
const fetchQueueStatusData = async () => {
try {
console.log('Fetching queue status');
const data = await getQueueStatus();
console.log('Queue status data:', data);
setQueueStatus(data);
} catch (err) {
console.error('fetchQueueStatus error:', err);
}
};
const handleCancelJob = async (jobId) => {
try {
await cancelJob(jobId);
await fetchUserJobs(); // Refresh the list
} catch (err) {
setError(`Failed to cancel job: ${err.message}`);
}
};
useEffect(() => {
const loadData = async () => {
setLoading(true);
await Promise.all([fetchUserJobs(), fetchQueueStatusData()]);
setLoading(false);
};
loadData();
// Poll for updates every 2 seconds
const interval = setInterval(() => {
fetchUserJobs();
fetchQueueStatusData();
}, 2000);
return () => clearInterval(interval);
}, [userEmail]);
const getStatusIcon = (status) => {
if (status === JOB_STATUS.COMPLETED) return <CheckCircleRounded color="success" />;
if (status === JOB_STATUS.FAILED) return <ErrorRounded color="error" />;
if (status === JOB_STATUS.CANCELLED) return <CancelRounded color="disabled" />;
if (status === JOB_STATUS.QUEUED) return <HourglassEmptyRounded color="info" />;
if (status.startsWith('retry_')) return <RefreshRounded color="warning" />;
return <PlayArrowRounded color="primary" />;
};
const getStatusColor = (status) => {
if (status === JOB_STATUS.COMPLETED) return 'success';
if (status === JOB_STATUS.FAILED) return 'error';
if (status === JOB_STATUS.CANCELLED) return 'default';
if (status === JOB_STATUS.QUEUED) return 'info';
if (status.startsWith('retry_')) return 'warning';
return 'primary';
};
const getProgressValue = (job) => {
if (job.status === JOB_STATUS.COMPLETED) return 100;
if (job.status === JOB_STATUS.FAILED || job.status === JOB_STATUS.CANCELLED) return 0;
return job.progress || 0;
};
const renderDownloadButtons = (job) => {
if (job.status !== JOB_STATUS.COMPLETED) return null;
const videoCount = job.video_count || 1;
const buttons = [];
// Main download button (zip for multiple videos, single video for one)
buttons.push(
<Button
key="main-download"
variant="contained"
size="small"
startIcon={<DownloadRounded />}
href={`${API_BASE_URL}/api/download/${job.job_id}`}
sx={{ mr: 1, mb: 1 }}
>
Download {videoCount > 1 ? 'All' : 'Video'}
</Button>
);
// Individual download buttons for multiple videos
if (videoCount > 1) {
for (let i = 1; i <= videoCount; i++) {
buttons.push(
<Button
key={`video-${i}`}
variant="outlined"
size="small"
href={`${API_BASE_URL}/api/download/${job.job_id}/video/${i}`}
sx={{ mr: 0.5, mb: 1 }}
>
Video {i}
</Button>
);
}
}
return <Box sx={{ mt: 1 }}>{buttons}</Box>;
};
if (loading) {
return (
<Paper elevation={2} sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>Job Queue</Typography>
<LinearProgress />
</Paper>
);
}
return (
<Paper elevation={2} sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">Job Queue</Typography>
<Tooltip title="Refresh">
<IconButton onClick={() => { fetchUserJobs(); fetchQueueStatusData(); }}>
<RefreshRounded />
</IconButton>
</Tooltip>
</Box>
{/* Queue Status Summary */}
{queueStatus.queue_length !== undefined && (
<Alert severity="info" sx={{ mb: 2 }}>
Queue: {queueStatus.queue_length} waiting
Processing: {queueStatus.processing_jobs}/{queueStatus.concurrent_limit}
Your jobs: {jobs.length}/4
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError('')}>
{error}
</Alert>
)}
{jobs.length === 0 ? (
<Typography color="text.secondary" sx={{ textAlign: 'center', py: 4 }}>
No jobs found. Submit a video generation request to see it here.
</Typography>
) : (
<List>
{jobs.map((job, index) => (
<React.Fragment key={job.job_id}>
<ListItem sx={{ flexDirection: 'column', alignItems: 'stretch', py: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getStatusIcon(job.status)}
<Typography variant="subtitle2" sx={{ fontWeight: 'bold' }}>
{job.videos_requested} video{job.videos_requested > 1 ? 's' : ''}
</Typography>
<Chip
label={job.status.replace(/_/g, ' ').toUpperCase()}
color={getStatusColor(job.status)}
size="small"
/>
{job.status === JOB_STATUS.QUEUED && job.queue_position > 0 && (
<Chip
label={`Position: ${job.queue_position}`}
variant="outlined"
size="small"
/>
)}
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
{job.can_cancel && job.status === JOB_STATUS.QUEUED && (
<Button
variant="outlined"
color="error"
size="small"
startIcon={<CancelRounded />}
onClick={() => handleCancelJob(job.job_id)}
>
Cancel
</Button>
)}
</Box>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{job.prompt.length > 100 ? `${job.prompt.substring(0, 100)}...` : job.prompt}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ mb: 1 }}>
{job.message || 'Processing...'}
</Typography>
{/* Progress Bar */}
<LinearProgress
variant="determinate"
value={getProgressValue(job)}
sx={{ mb: 1 }}
/>
{/* Download Buttons */}
{renderDownloadButtons(job)}
{/* Job Details */}
<Grid container spacing={1} sx={{ mt: 1 }}>
<Grid item xs={6} sm={3}>
<Typography variant="caption" color="text.secondary">
Created: {new Date(job.created_at).toLocaleString()}
</Typography>
</Grid>
{job.started_at && (
<Grid item xs={6} sm={3}>
<Typography variant="caption" color="text.secondary">
Started: {new Date(job.started_at).toLocaleString()}
</Typography>
</Grid>
)}
<Grid item xs={6} sm={3}>
<Typography variant="caption" color="text.secondary">
Model: {job.model_name?.includes('fast') ? 'Fast' : 'Standard'}
</Typography>
</Grid>
<Grid item xs={6} sm={3}>
<Typography variant="caption" color="text.secondary">
Length: {job.video_length_sec}s
</Typography>
</Grid>
</Grid>
{/* Retry Information */}
{job.retry_count > 0 && (
<Alert severity="warning" sx={{ mt: 1 }}>
<strong>Retry attempt {job.retry_count} of {job.max_retries}</strong>
<br />
{job.status.includes('retry') && job.message && (
<span>{job.message}</span>
)}
{job.error && job.error.includes('timeout') && (
<div style={{ marginTop: '8px', fontSize: '0.9em' }}>
<strong>Note:</strong> This is likely due to network connectivity issues. The system will automatically retry with longer timeouts.
</div>
)}
</Alert>
)}
{/* Final Failure Information */}
{job.final_failure && (
<Alert severity="error" sx={{ mt: 1 }}>
Failed after {job.retry_count} retries. {job.error}
</Alert>
)}
</ListItem>
{index < jobs.length - 1 && <Divider />}
</React.Fragment>
))}
</List>
)}
</Paper>
);
};
export default QueueManager;