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 (

Submit New Job

{error && (
{error}
)}
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" />
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" />
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" />
setFormData({ ...formData, allow_parallel_runners: e.target.checked })} className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded" />
{metadataStatus === 'extracting' && (
Extracting metadata from blend file...
)} {metadataStatus === 'completed' && metadata && (
Metadata extracted successfully!
Frames: {metadata.frame_start} - {metadata.frame_end}
Resolution: {metadata.render_settings?.resolution_x} x {metadata.render_settings?.resolution_y}
Engine: {metadata.render_settings?.engine}
Samples: {metadata.render_settings?.samples}
Form fields have been auto-populated. You can adjust them if needed.
{(formData.frame_start < metadata.frame_start || formData.frame_end > metadata.frame_end) && (
Warning: 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.
)}
)} {metadataStatus === 'error' && (
Could not extract metadata. Please fill in the form manually.
)}
); }