304 lines
No EOL
10 KiB
JavaScript
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; |