(function () { const uploadForm = document.getElementById("upload-analyze-form"); const configForm = document.getElementById("job-config-form"); const fileInput = document.getElementById("source-file"); const statusEl = document.getElementById("upload-status"); const errorEl = document.getElementById("job-create-error"); const blendVersionEl = document.getElementById("blender-version"); const mainBlendWrapper = document.getElementById("main-blend-wrapper"); const mainBlendSelect = document.getElementById("main-blend-select"); const metadataPreview = document.getElementById("metadata-preview"); const configSection = document.getElementById("job-config-section"); const uploadSection = document.getElementById("job-upload-section"); const uploadSubmitBtn = uploadForm.querySelector('button[type="submit"]'); const stepUpload = document.getElementById("step-upload"); const stepConfig = document.getElementById("step-config"); const nameInput = document.getElementById("job-name"); const frameStartInput = document.getElementById("frame-start"); const frameEndInput = document.getElementById("frame-end"); const outputFormatInput = document.getElementById("output-format"); const unhideObjectsInput = document.getElementById("unhide-objects"); const enableExecutionInput = document.getElementById("enable-execution"); let sessionID = ""; let pollTimer = null; let uploadInProgress = false; function showError(msg) { errorEl.textContent = msg || ""; errorEl.classList.toggle("hidden", !msg); } function showStatus(msg) { statusEl.classList.remove("hidden"); statusEl.innerHTML = `
${msg}
`; } function setUploadBusy(busy) { uploadInProgress = busy; if (!uploadSubmitBtn) return; uploadSubmitBtn.disabled = busy; } function setStep(step) { const uploadActive = step === 1; stepUpload.classList.toggle("active", uploadActive); stepUpload.classList.toggle("complete", !uploadActive); stepConfig.classList.toggle("active", !uploadActive); uploadSection.classList.toggle("hidden", !uploadActive); configSection.classList.toggle("hidden", uploadActive); } function fileNameToJobName(fileName) { const stem = (fileName || "Render Job").replace(/\.[^/.]+$/, ""); return stem.trim() || "Render Job"; } function prefillFromMetadata(status, fileName) { const metadata = status.metadata || {}; const render = metadata.render_settings || {}; nameInput.value = fileNameToJobName(fileName || status.file_name); frameStartInput.value = Number.isFinite(metadata.frame_start) ? metadata.frame_start : 1; frameEndInput.value = Number.isFinite(metadata.frame_end) ? metadata.frame_end : 250; if (render.output_format && outputFormatInput.querySelector(`option[value="${render.output_format}"]`)) { outputFormatInput.value = render.output_format; } else { outputFormatInput.value = "EXR"; } if (metadata.blender_version && blendVersionEl.querySelector(`option[value="${metadata.blender_version}"]`)) { blendVersionEl.value = metadata.blender_version; } else { blendVersionEl.value = ""; } unhideObjectsInput.checked = Boolean(metadata.unhide_objects); enableExecutionInput.checked = Boolean(metadata.enable_execution); const scenes = metadata.scene_info || {}; metadataPreview.innerHTML = ` `; } async function loadBlenderVersions() { try { const res = await fetch("/api/blender/versions", { credentials: "include" }); if (!res.ok) return; const data = await res.json(); const versions = data.versions || []; versions.slice(0, 30).forEach((v) => { const option = document.createElement("option"); option.value = v.full; option.textContent = v.full; blendVersionEl.appendChild(option); }); } catch (_) {} } function uploadFile(mainBlendFile) { return new Promise((resolve, reject) => { const file = fileInput.files[0]; if (!file) { reject(new Error("Select a file first")); return; } const lowerName = file.name.toLowerCase(); const isAccepted = lowerName.endsWith(".blend") || lowerName.endsWith(".zip"); if (!isAccepted) { reject(new Error("Only .blend or .zip files are supported.")); return; } const fd = new FormData(); fd.append("file", file); if (mainBlendFile) { fd.append("main_blend_file", mainBlendFile); } const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/jobs/upload", true); xhr.withCredentials = true; xhr.upload.addEventListener("progress", (e) => { if (!e.lengthComputable) return; const pct = Math.round((e.loaded / e.total) * 100); showStatus(`Uploading: ${pct}%`); }); xhr.onload = () => { try { const data = JSON.parse(xhr.responseText || "{}"); if (xhr.status >= 400) { reject(new Error(data.error || "Upload failed")); return; } resolve(data); } catch (err) { reject(err); } }; xhr.onerror = () => reject(new Error("Upload failed")); xhr.send(fd); }); } async function pollUploadStatus() { if (!sessionID) return null; const res = await fetch(`/api/jobs/upload/status?session_id=${encodeURIComponent(sessionID)}`, { credentials: "include" }); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(data.error || "Upload status check failed"); } return data; } async function createJob(payload) { const res = await fetch("/api/jobs", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await res.json().catch(() => ({})); if (!res.ok) { const err = new Error(data.error || "Job creation failed"); if (data && typeof data.code === "string") { err.code = data.code; } throw err; } return data; } function resetToUploadStep(message) { sessionID = ""; clearInterval(pollTimer); setUploadBusy(false); mainBlendWrapper.classList.add("hidden"); metadataPreview.innerHTML = ""; configSection.classList.add("hidden"); setStep(1); showStatus("Please upload the file again."); showError(message); } async function runSubmission(mainBlendFile) { showError(""); setStep(1); configSection.classList.add("hidden"); metadataPreview.innerHTML = ""; const upload = await uploadFile(mainBlendFile); sessionID = upload.session_id; showStatus("Upload complete. Processing..."); clearInterval(pollTimer); await new Promise((resolve, reject) => { pollTimer = setInterval(async () => { try { const status = await pollUploadStatus(); if (!status) return; showStatus(`${status.message || status.status} (${Math.round((status.progress || 0) * 100)}%)`); if (status.status === "select_blend") { clearInterval(pollTimer); mainBlendSelect.innerHTML = ""; (status.blend_files || []).forEach((path) => { const option = document.createElement("option"); option.value = path; option.textContent = path; mainBlendSelect.appendChild(option); }); mainBlendWrapper.classList.remove("hidden"); reject(new Error("Select a main blend file and submit again.")); return; } if (status.status === "error") { clearInterval(pollTimer); reject(new Error(status.error || "Upload processing failed")); return; } if (status.status === "completed") { clearInterval(pollTimer); prefillFromMetadata(status, fileInput.files[0]?.name || ""); setStep(2); resolve(); } } catch (err) { clearInterval(pollTimer); reject(err); } }, 1500); }); } async function submitJobConfig() { if (!sessionID) { throw new Error("Upload and analyze a file first."); } const fd = new FormData(configForm); const jobName = String(fd.get("name") || "").trim(); if (!jobName) { throw new Error("Job name is required."); } nameInput.value = jobName; const payload = { job_type: "render", name: jobName, frame_start: Number(fd.get("frame_start")), frame_end: Number(fd.get("frame_end")), output_format: fd.get("output_format"), upload_session_id: sessionID, unhide_objects: Boolean(fd.get("unhide_objects")), enable_execution: Boolean(fd.get("enable_execution")), }; const blenderVersion = fd.get("blender_version"); if (blenderVersion) payload.blender_version = blenderVersion; const job = await createJob(payload); showStatus(`Job created (#${job.id}). Redirecting...`); window.location.href = `/jobs/${job.id}`; } uploadForm.addEventListener("submit", async (e) => { e.preventDefault(); if (uploadInProgress) { return; } try { setUploadBusy(true); const selected = mainBlendWrapper.classList.contains("hidden") ? "" : mainBlendSelect.value; await runSubmission(selected); } catch (err) { showError(err.message || "Failed to create job"); setUploadBusy(false); } }); configForm.addEventListener("submit", async (e) => { e.preventDefault(); try { showError(""); await submitJobConfig(); } catch (err) { if (err && err.code === "UPLOAD_SESSION_EXPIRED") { resetToUploadStep(err.message || "Upload session expired. Please upload the file again."); return; } if (err && err.code === "UPLOAD_SESSION_NOT_READY") { showError(err.message || "Upload session is still processing. Please wait and try again."); return; } showError(err.message || "Failed to create job"); } }); setStep(1); loadBlenderVersions(); })();