Files
jiggablend/web/src/components/JobSubmission.jsx

317 lines
11 KiB
JavaScript

import { useState, useEffect } from 'react';
import { jobs } from '../utils/api';
export default function JobSubmission({ onSuccess }) {
const [formData, setFormData] = useState({
name: '',
frame_start: 1,
frame_end: 10,
output_format: 'PNG',
allow_parallel_runners: true,
});
const [file, setFile] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const [metadataStatus, setMetadataStatus] = useState(null); // 'extracting', 'completed', 'error'
const [metadata, setMetadata] = useState(null);
const [currentJobId, setCurrentJobId] = useState(null);
// Poll for metadata after file upload
useEffect(() => {
if (!currentJobId || metadataStatus !== 'extracting') return;
let pollCount = 0;
const maxPolls = 30; // 60 seconds max (30 * 2 seconds)
let timeoutId = null;
const pollMetadata = async () => {
pollCount++;
// Stop polling after timeout
if (pollCount > maxPolls) {
setMetadataStatus('error');
// Cancel temp job on timeout
try {
await jobs.cancel(currentJobId);
} catch (err) {
// Ignore errors when canceling
}
return;
}
try {
const metadata = await jobs.getMetadata(currentJobId);
if (metadata) {
setMetadata(metadata);
setMetadataStatus('completed');
// Auto-populate form fields
setFormData(prev => ({
...prev,
frame_start: metadata.frame_start || prev.frame_start,
frame_end: metadata.frame_end || prev.frame_end,
output_format: metadata.render_settings?.output_format || prev.output_format,
}));
}
} catch (err) {
// Metadata not ready yet, continue polling (only if 404/not found)
if (err.message.includes('404') || err.message.includes('not found')) {
// Continue polling via interval
} else {
setMetadataStatus('error');
}
}
};
const interval = setInterval(pollMetadata, 2000);
// Set timeout to stop polling after 60 seconds
timeoutId = setTimeout(() => {
clearInterval(interval);
if (metadataStatus === 'extracting') {
setMetadataStatus('error');
// Cancel temp job on timeout
jobs.cancel(currentJobId).catch(() => {});
}
}, 60000);
return () => {
clearInterval(interval);
if (timeoutId) clearTimeout(timeoutId);
// Cleanup: cancel temp job if component unmounts during extraction
if (currentJobId && metadataStatus === 'extracting') {
jobs.cancel(currentJobId).catch(() => {});
}
};
}, [currentJobId, metadataStatus]);
const handleFileChange = async (e) => {
const selectedFile = e.target.files[0];
if (!selectedFile) {
setFile(null);
return;
}
setFile(selectedFile);
setMetadataStatus(null);
setMetadata(null);
setCurrentJobId(null);
// If it's a blend file, create a temporary job to extract metadata
if (selectedFile.name.toLowerCase().endsWith('.blend')) {
try {
// Create a temporary job for metadata extraction
const tempJob = await jobs.create({
name: 'Metadata Extraction',
frame_start: 1,
frame_end: 10,
output_format: 'PNG',
allow_parallel_runners: true,
});
setCurrentJobId(tempJob.id);
setMetadataStatus('extracting');
// Upload file to trigger metadata extraction
await jobs.uploadFile(tempJob.id, selectedFile);
} catch (err) {
console.error('Failed to start metadata extraction:', err);
setMetadataStatus('error');
}
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setSubmitting(true);
try {
if (!file) {
throw new Error('Please select a Blender file');
}
if (formData.frame_start < 0 || formData.frame_end < formData.frame_start) {
throw new Error('Invalid frame range');
}
// If we have a temporary job for metadata extraction, cancel it
if (currentJobId) {
try {
await jobs.cancel(currentJobId);
} catch (err) {
// Ignore errors when canceling temp job
}
}
// Create actual job
const job = await jobs.create({
name: formData.name,
frame_start: parseInt(formData.frame_start),
frame_end: parseInt(formData.frame_end),
output_format: formData.output_format,
allow_parallel_runners: formData.allow_parallel_runners,
});
// Upload file
await jobs.uploadFile(job.id, file);
// Reset form
setFormData({
name: '',
frame_start: 1,
frame_end: 10,
output_format: 'PNG',
allow_parallel_runners: true,
});
setFile(null);
setMetadata(null);
setMetadataStatus(null);
setCurrentJobId(null);
e.target.reset();
if (onSuccess) {
onSuccess();
}
} catch (err) {
setError(err.message || 'Failed to submit job');
} finally {
setSubmitting(false);
}
};
return (
<div className="bg-white rounded-lg shadow-md p-8 max-w-2xl mx-auto">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Submit New Job</h2>
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Job Name
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-600 focus:border-transparent"
placeholder="My Render Job"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Frame Start
</label>
<input
type="number"
value={formData.frame_start}
onChange={(e) => setFormData({ ...formData, frame_start: e.target.value })}
required
min="0"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-600 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Frame End
</label>
<input
type="number"
value={formData.frame_end}
onChange={(e) => setFormData({ ...formData, frame_end: e.target.value })}
required
min={formData.frame_start}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-600 focus:border-transparent"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Output Format
</label>
<select
value={formData.output_format}
onChange={(e) => setFormData({ ...formData, output_format: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-600 focus:border-transparent"
>
<option value="PNG">PNG</option>
<option value="JPEG">JPEG</option>
<option value="EXR">EXR</option>
<option value="MP4">MP4</option>
</select>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="allow_parallel_runners"
checked={formData.allow_parallel_runners}
onChange={(e) => setFormData({ ...formData, allow_parallel_runners: e.target.checked })}
className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded"
/>
<label htmlFor="allow_parallel_runners" className="ml-2 block text-sm text-gray-700">
Allow multiple runners to work on this job simultaneously
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Blender File (.blend)
</label>
<input
type="file"
accept=".blend"
onChange={handleFileChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-600 focus:border-transparent file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-purple-50 file:text-purple-700 hover:file:bg-purple-100"
/>
{metadataStatus === 'extracting' && (
<div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg text-blue-700 text-sm">
Extracting metadata from blend file...
</div>
)}
{metadataStatus === 'completed' && metadata && (
<div className="mt-2 p-3 bg-green-50 border border-green-200 rounded-lg text-sm">
<div className="text-green-700 font-semibold mb-1">Metadata extracted successfully!</div>
<div className="text-green-600 text-xs space-y-1">
<div>Frames: {metadata.frame_start} - {metadata.frame_end}</div>
<div>Resolution: {metadata.render_settings?.resolution_x} x {metadata.render_settings?.resolution_y}</div>
<div>Engine: {metadata.render_settings?.engine}</div>
<div>Samples: {metadata.render_settings?.samples}</div>
<div className="text-gray-600 mt-2">Form fields have been auto-populated. You can adjust them if needed.</div>
{(formData.frame_start < metadata.frame_start || formData.frame_end > metadata.frame_end) && (
<div className="mt-2 p-2 bg-yellow-50 border border-yellow-200 rounded text-yellow-800 text-xs">
<strong>Warning:</strong> Your frame range ({formData.frame_start}-{formData.frame_end}) exceeds the blend file's range ({metadata.frame_start}-{metadata.frame_end}). This may cause errors.
</div>
)}
</div>
</div>
)}
{metadataStatus === 'error' && (
<div className="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-yellow-700 text-sm">
Could not extract metadata. Please fill in the form manually.
</div>
)}
</div>
<button
type="submit"
disabled={submitting}
className="w-full px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
>
{submitting ? 'Submitting...' : 'Submit Job'}
</button>
</form>
</div>
);
}