document.addEventListener("DOMContentLoaded", () => { const state = { mode: "edit", info: null, capabilities: [], presets: [], sourceFile: null, referenceFile: null, backgroundFile: null, batchFiles: [], sourceDataUrl: "", resultDataUrl: "", isBusy: false, compareMode: true, fullscreen: false, history: [], historyIndex: -1, storageKey: "imageeditter_history_v2", progressTimer: null, stageScale: 1, stageOffsetX: 0, stageOffsetY: 0, stageDragging: false, stageDragOrigin: null, mask: { tool: "brush", size: 24, drawing: false, hasData: false, lastPoint: null, enabled: false, }, }; const modeDescriptions = { edit: "Prompt-driven image editing with CV planning and optional diffusion harmonization.", generate: "Generate a fresh local scene or stylized image from text.", inpaint: "Paint a mask over the source and repair or replace the selected region.", style: "Transfer the palette and texture mood from a reference image.", background: "Replace or redesign the background using text or a background image.", upscale: "Scale up the source with local super-resolution style detail recovery.", batch: "Apply the same edit pipeline across multiple images in one run.", }; const progressMessages = { edit: ["Parsing prompt...", "Planning operations...", "Running local stack...", "Refining output..."], generate: ["Building base scene...", "Expanding prompt...", "Rendering locally...", "Finishing highlights..."], inpaint: ["Reading mask...", "Cleaning masked region...", "Repairing pixels...", "Polishing result..."], style: ["Reading reference style...", "Matching colors...", "Transferring texture cues...", "Finalizing output..."], background: ["Segmenting subject...", "Designing backdrop...", "Blending edges...", "Refining scene..."], upscale: ["Upscaling image...", "Injecting detail...", "Sharpening output..."], batch: ["Preparing batch queue...", "Applying edits...", "Collecting results..."], }; const dom = { statusDot: document.getElementById("status-dot"), statusText: document.getElementById("status-text"), capabilityCount: document.getElementById("capability-count"), presetCount: document.getElementById("preset-count"), promptHint: document.getElementById("prompt-hint-text"), modeTabs: [...document.querySelectorAll(".mode-tab")], modeBlurb: document.getElementById("mode-blurb"), modeCopy: document.getElementById("mode-copy"), sourceDrop: document.getElementById("source-drop"), referenceDrop: document.getElementById("reference-drop"), backgroundDrop: document.getElementById("background-drop"), batchDrop: document.getElementById("batch-drop"), sourceInput: document.getElementById("source-input"), referenceInput: document.getElementById("reference-input"), backgroundInput: document.getElementById("background-input"), batchInput: document.getElementById("batch-input"), sourceMeta: document.getElementById("source-meta"), referenceMeta: document.getElementById("reference-meta"), backgroundMeta: document.getElementById("background-meta"), batchMeta: document.getElementById("batch-meta"), sourcePreview: document.getElementById("source-preview"), resultPreview: document.getElementById("result-preview"), compareOverlay: document.getElementById("compare-overlay"), compareWrap: document.getElementById("compare-wrap"), compareSlider: document.getElementById("compare-slider"), stageFrame: document.getElementById("stage-frame"), stageMedia: document.getElementById("stage-media"), stageEmpty: document.getElementById("stage-empty"), maskCanvas: document.getElementById("mask-canvas"), maskPanel: document.getElementById("mask-panel"), maskBrush: document.getElementById("mask-brush"), maskErase: document.getElementById("mask-erase"), maskMagic: document.getElementById("mask-magic"), maskFill: document.getElementById("mask-fill"), maskClear: document.getElementById("mask-clear"), maskSize: document.getElementById("mask-size"), maskSizeValue: document.getElementById("mask-size-value"), btnClearAll: document.getElementById("btn-clear-all"), btnViewToggle: document.getElementById("btn-view-toggle"), btnDownload: document.getElementById("btn-download"), btnFullscreen: document.getElementById("btn-fullscreen"), promptInput: document.getElementById("prompt-input"), runButton: document.getElementById("run-button"), btnUndo: document.getElementById("btn-undo"), btnRedo: document.getElementById("btn-redo"), responseBanner: document.getElementById("response-banner"), progressPill: document.getElementById("progress-pill"), pipelineSteps: document.getElementById("pipeline-steps"), capabilityTags: document.getElementById("capability-tags"), presetGrid: document.getElementById("preset-grid"), historyList: document.getElementById("history-list"), layerList: document.getElementById("layer-list"), batchPanel: document.getElementById("batch-panel"), batchSourceGrid: document.getElementById("batch-source-grid"), batchResultGrid: document.getElementById("batch-result-grid"), generateControls: document.getElementById("generate-controls"), generateWidth: document.getElementById("generate-width"), generateHeight: document.getElementById("generate-height"), upscaleControls: document.getElementById("upscale-controls"), upscaleScale: document.getElementById("upscale-scale"), upscaleScaleValue: document.getElementById("upscale-scale-value"), stepsSlider: document.getElementById("steps-slider"), stepsValue: document.getElementById("steps-value"), textScaleSlider: document.getElementById("text-scale-slider"), textScaleValue: document.getElementById("text-scale-value"), imageScaleSlider: document.getElementById("image-scale-slider"), imageScaleValue: document.getElementById("image-scale-value"), seedInput: document.getElementById("seed-input"), toastStack: document.getElementById("toast-stack"), adjustWarmth: document.getElementById("adjust-warmth"), adjustBrightness: document.getElementById("adjust-brightness"), adjustContrast: document.getElementById("adjust-contrast"), adjustClarity: document.getElementById("adjust-clarity"), adjustSharpness: document.getElementById("adjust-sharpness"), adjustVignette: document.getElementById("adjust-vignette"), adjustBloom: document.getElementById("adjust-bloom"), btnResetAdjust: document.getElementById("btn-reset-adjust"), }; const maskCtx = dom.maskCanvas.getContext("2d"); function escapeHtml(text) { return String(text) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function showToast(title, body) { const toast = document.createElement("div"); toast.className = "toast"; toast.innerHTML = `${escapeHtml(title)}

${escapeHtml(body)}

`; dom.toastStack.appendChild(toast); setTimeout(() => toast.remove(), 4200); } function setResponse(message = "", isError = false) { if (!message) { dom.responseBanner.hidden = true; dom.responseBanner.textContent = ""; dom.responseBanner.classList.remove("error"); return; } dom.responseBanner.hidden = false; dom.responseBanner.textContent = message; dom.responseBanner.classList.toggle("error", isError); } function setProgress(text) { dom.progressPill.textContent = text; } function renderStepChips(steps = []) { dom.pipelineSteps.innerHTML = ""; steps.forEach((step) => { const chip = document.createElement("span"); chip.className = "step-chip"; chip.textContent = step.replaceAll("_", " "); dom.pipelineSteps.appendChild(chip); }); } function renderCapabilityTags() { dom.capabilityTags.innerHTML = ""; const suggestions = { edit: [ { label: "✨ Warm Sunset", prompt: "Apply warm cinematic golden hour sunset lighting with gorgeous glowing highlights" }, { label: "📸 Glamour Editorial", prompt: "Create a high-end luxury fashion editorial, DSLR photography, shallow depth of field, magazine-cover quality" }, { label: "🌃 Cyberpunk Neon", prompt: "Transform the scene into a vibrant cyberpunk night style with glowing neon lights and high clarity" }, { label: "🎭 Dramatic Matte", prompt: "Enhance image with dramatic matte curves, lifted shadows, and moody ambient colors" } ], generate: [ { label: "🏝️ Sunset Beach", prompt: "A gorgeous luxury tropical beach resort at sunset with towering palm trees, cinematic DSLR photography" }, { label: "🚀 Neon Future", prompt: "A futuristic cyberpunk skyline at night with neon signs, wet rainy streets reflecting lights, high detail" }, { label: "🌄 Alpine Lake", prompt: "A serene pristine mountain lake mirroring snowy alpine peaks at sunrise, warm morning sun" } ], inpaint: [ { label: "👚 Change Outfit", prompt: "change the outfit of the person in the masked region to elegant high-fashion designer wear" }, { label: "🚫 Remove Object", prompt: "remove the object in the masked region completely, inpaint and blend the background seamlessly" }, { label: "🎨 Add Custom Patch", prompt: "add a custom visual design element inside the masked region, matching the lighting" } ], style: [ { label: "🌅 Golden Warmth", prompt: "Apply warm rich color harmony matching a gorgeous golden hour landscape" }, { label: "🌌 Cyber Neon", prompt: "Match the style to a cold neon city night look with high teal and magenta saturation" } ], background: [ { label: "🌴 Sunset Shoreline", prompt: "seamlessly replace the background with a tropical sunset beach and palm trees at golden hour" }, { label: "🌆 Cyberpunk Street", prompt: "replace the background with a vibrant cyberpunk neon street at night, wet asphalt reflections" }, { label: "📸 Portrait Studio", prompt: "replace the background with a soft, warm dreamy studio backdrop, high-end portrait lighting" } ], upscale: [ { label: "✨ Super Details", prompt: "Enhance high-frequency textures, restore micro-contrast, and sharpen edges naturally" } ], batch: [ { label: "🌅 Warm Grade Batch", prompt: "Apply warm color grade, soft contrast, and subtle vignettes across all images" } ] }[state.mode] || []; suggestions.forEach((item) => { const chip = document.createElement("button"); chip.type = "button"; chip.className = "mini-chip suggestion-chip"; chip.textContent = item.label; chip.addEventListener("click", () => { dom.promptInput.value = item.prompt; showToast("Suggestion Applied", item.label); updateRunButton(); }); dom.capabilityTags.appendChild(chip); }); } function renderPresets() { dom.presetGrid.innerHTML = ""; state.presets.forEach((preset) => { const card = document.createElement("button"); card.type = "button"; card.className = "preset-card"; const tags = (preset.steps || []).slice(0, 3).map((step) => `${escapeHtml(step.name.replaceAll("_", " "))}`).join(""); card.innerHTML = `

${escapeHtml(preset.name)}

${escapeHtml(preset.description)}

${tags}
`; card.addEventListener("click", () => { dom.promptInput.value = preset.name + " look"; setResponse(`Loaded preset "${preset.name}". You can run it directly or adapt the prompt.`); renderStepChips((preset.steps || []).map((step) => step.name)); showToast("Preset Loaded", preset.name); updateRunButton(); }); dom.presetGrid.appendChild(card); }); } function renderLayers(steps = []) { dom.layerList.innerHTML = ""; if (!steps.length) { dom.layerList.innerHTML = `

Waiting for output

Pipeline steps from the next result will appear here.

`; return; } steps.forEach((step, index) => { const item = document.createElement("div"); item.className = "layer-item"; item.innerHTML = `

Layer ${index + 1}

${escapeHtml(step.replaceAll("_", " "))}

active
`; dom.layerList.appendChild(item); }); } function saveHistory() { localStorage.setItem(state.storageKey, JSON.stringify({ history: state.history.slice(-24), historyIndex: state.historyIndex, })); } function loadHistory() { try { const raw = localStorage.getItem(state.storageKey); if (!raw) return; const parsed = JSON.parse(raw); state.history = Array.isArray(parsed.history) ? parsed.history : []; state.historyIndex = typeof parsed.historyIndex === "number" ? parsed.historyIndex : state.history.length - 1; } catch { state.history = []; state.historyIndex = -1; } } function renderHistory() { dom.historyList.innerHTML = ""; if (!state.history.length) { dom.historyList.innerHTML = `

No edits yet

Your recent outputs will be stored locally here.

`; return; } [...state.history].reverse().forEach((entry) => { const active = state.history[state.historyIndex] && state.history[state.historyIndex].id === entry.id; const item = document.createElement("button"); item.type = "button"; item.className = "history-card-item"; item.style.borderColor = active ? "rgba(255, 179, 106, 0.42)" : ""; item.innerHTML = ` ${escapeHtml(entry.mode)} history preview

${escapeHtml(entry.mode.toUpperCase())}

${escapeHtml(entry.prompt || "No prompt")}

${(entry.steps || []).slice(0, 3).map((step) => `${escapeHtml(step.replaceAll("_", " "))}`).join("")}
`; item.addEventListener("click", () => restoreHistoryEntry(entry.id)); dom.historyList.appendChild(item); }); } function pushHistory(entry) { const historyEntry = { id: crypto.randomUUID(), timestamp: Date.now(), ...entry, }; state.history = state.history.slice(0, state.historyIndex + 1); state.history.push(historyEntry); state.historyIndex = state.history.length - 1; saveHistory(); renderHistory(); } function restoreHistoryEntry(id) { const index = state.history.findIndex((entry) => entry.id === id); if (index < 0) return; state.historyIndex = index; const entry = state.history[index]; state.mode = entry.mode || state.mode; state.resultDataUrl = entry.resultDataUrl || ""; state.sourceDataUrl = entry.sourceDataUrl || state.sourceDataUrl; dom.promptInput.value = entry.prompt || ""; updateModeUI(); renderStage(); renderLayers(entry.steps || []); setResponse(`Restored history item from ${new Date(entry.timestamp).toLocaleString()}.`); saveHistory(); renderHistory(); } function undoHistory() { if (state.historyIndex <= 0) return; state.historyIndex -= 1; restoreHistoryEntry(state.history[state.historyIndex].id); } function redoHistory() { if (state.historyIndex >= state.history.length - 1) return; state.historyIndex += 1; restoreHistoryEntry(state.history[state.historyIndex].id); } function applyStageTransform() { dom.stageMedia.style.transform = `translate(${state.stageOffsetX}px, ${state.stageOffsetY}px) scale(${state.stageScale})`; } function resetStageTransform() { state.stageScale = 1; state.stageOffsetX = 0; state.stageOffsetY = 0; applyStageTransform(); } function applyCompare() { const compareValue = state.mask.enabled ? 100 : dom.compareSlider.value; document.documentElement.style.setProperty("--compare", `${compareValue}%`); dom.compareOverlay.hidden = !state.sourceDataUrl || (!state.compareMode && !state.mask.enabled); dom.compareSlider.hidden = !state.compareMode || !state.sourceDataUrl || state.mask.enabled; } function renderStage() { const hasSource = Boolean(state.sourceDataUrl); const resultUrl = state.resultDataUrl || state.sourceDataUrl; dom.sourcePreview.hidden = !hasSource; dom.sourcePreview.src = hasSource ? state.sourceDataUrl : ""; dom.resultPreview.hidden = !resultUrl; dom.resultPreview.src = resultUrl || ""; dom.stageEmpty.hidden = Boolean(hasSource || state.resultDataUrl); dom.btnDownload.disabled = !state.resultDataUrl; applyCompare(); syncMaskCanvas(); } function updateRunButton() { const hasPrompt = Boolean(dom.promptInput.value.trim()); const hasSource = Boolean(state.sourceFile || state.sourceDataUrl); const hasReference = Boolean(state.referenceFile); const hasBatch = state.batchFiles.length > 0; let canRun = false; if (state.mode === "edit") canRun = hasSource && hasPrompt; if (state.mode === "generate") canRun = hasPrompt; if (state.mode === "inpaint") canRun = hasSource && hasPrompt && state.mask.hasData; if (state.mode === "style") canRun = hasSource && hasReference; if (state.mode === "background") canRun = hasSource && hasPrompt; if (state.mode === "upscale") canRun = hasSource; if (state.mode === "batch") canRun = hasBatch && hasPrompt; dom.runButton.disabled = state.isBusy || !canRun; dom.btnUndo.disabled = state.historyIndex <= 0; dom.btnRedo.disabled = state.historyIndex >= state.history.length - 1 || state.history.length === 0; } function setMode(mode) { state.mode = mode; updateModeUI(); renderCapabilityTags(); updateRunButton(); setResponse(""); } function updateModeUI() { dom.modeTabs.forEach((button) => button.classList.toggle("active", button.dataset.mode === state.mode)); dom.modeBlurb.textContent = modeDescriptions[state.mode]; dom.modeCopy.textContent = modeDescriptions[state.mode]; dom.runButton.textContent = { edit: "Run Edit", generate: "Generate", inpaint: "Apply Inpaint", style: "Transfer Style", background: "Replace Background", upscale: "Upscale", batch: "Run Batch", }[state.mode]; dom.generateControls.hidden = state.mode !== "generate"; dom.upscaleControls.hidden = state.mode !== "upscale"; dom.maskPanel.hidden = state.mode !== "inpaint"; dom.batchPanel.hidden = state.mode !== "batch"; state.mask.enabled = state.mode === "inpaint"; dom.referenceDrop.hidden = !(state.mode === "style" || state.mode === "edit"); dom.backgroundDrop.hidden = !(state.mode === "background" || state.mode === "edit"); dom.batchDrop.hidden = state.mode !== "batch"; dom.sourceDrop.hidden = state.mode === "generate"; dom.sourceMeta.textContent = state.mode === "inpaint" ? "Drop an image to paint a repair mask" : "Drop or browse PNG, JPG, WebP"; applyCompare(); syncMaskCanvas(); } function updateSliderLabels() { dom.stepsValue.textContent = dom.stepsSlider.value; dom.textScaleValue.textContent = dom.textScaleSlider.value; dom.imageScaleValue.textContent = dom.imageScaleSlider.value; dom.upscaleScaleValue.textContent = `${Number(dom.upscaleScale.value).toFixed(1)}x`; dom.maskSizeValue.textContent = dom.maskSize.value; state.mask.size = Number(dom.maskSize.value); if (dom.adjustWarmth) { document.getElementById("adjust-warmth-value").textContent = Number(dom.adjustWarmth.value).toFixed(2); document.getElementById("adjust-brightness-value").textContent = Number(dom.adjustBrightness.value).toFixed(2); document.getElementById("adjust-contrast-value").textContent = Number(dom.adjustContrast.value).toFixed(2); document.getElementById("adjust-clarity-value").textContent = Number(dom.adjustClarity.value).toFixed(2); document.getElementById("adjust-sharpness-value").textContent = Number(dom.adjustSharpness.value).toFixed(2); document.getElementById("adjust-vignette-value").textContent = Number(dom.adjustVignette.value).toFixed(2); document.getElementById("adjust-bloom-value").textContent = Number(dom.adjustBloom.value).toFixed(2); } } async function dataUrlToBlob(dataUrl) { const res = await fetch(dataUrl); return await res.blob(); } let adjustLock = false; let adjustPending = false; async function triggerAdjustment() { if (!state.sourceDataUrl) return; if (adjustLock) { adjustPending = true; return; } adjustLock = true; adjustPending = false; try { const sourceBlob = await dataUrlToBlob(state.sourceDataUrl); const formData = new FormData(); formData.append("image", sourceBlob, "source.png"); formData.append("warmth", dom.adjustWarmth.value); formData.append("brightness", dom.adjustBrightness.value); formData.append("contrast", dom.adjustContrast.value); formData.append("clarity", dom.adjustClarity.value); formData.append("sharpness", dom.adjustSharpness.value); formData.append("vignette", dom.adjustVignette.value); formData.append("bloom", dom.adjustBloom.value); const res = await fetch("/adjust", { method: "POST", body: formData, }); const data = await res.json(); if (data.success && data.image_base64) { state.resultDataUrl = `data:image/png;base64,${data.image_base64}`; renderStage(); } } catch (e) { console.error("Adjustment error:", e); } finally { adjustLock = false; if (adjustPending) { triggerAdjustment(); } } } async function fileToDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); } async function loadSourceFile(file) { if (!file || !file.type.startsWith("image/")) return; state.sourceFile = file; state.sourceDataUrl = await fileToDataUrl(file); state.resultDataUrl = ""; dom.sourceMeta.textContent = `${file.name} - ${(file.size / 1024 / 1024).toFixed(2)} MB`; clearMask(); renderStage(); updateRunButton(); } async function loadReferenceFile(file) { if (!file || !file.type.startsWith("image/")) return; state.referenceFile = file; dom.referenceMeta.textContent = file.name; updateRunButton(); showToast("Reference Loaded", file.name); } async function loadBackgroundFile(file) { if (!file || !file.type.startsWith("image/")) return; state.backgroundFile = file; dom.backgroundMeta.textContent = file.name; updateRunButton(); showToast("Background Loaded", file.name); } function renderBatchSources() { dom.batchSourceGrid.innerHTML = ""; state.batchFiles.forEach((file) => { const card = document.createElement("div"); card.className = "thumb-card"; const url = URL.createObjectURL(file); card.innerHTML = `${escapeHtml(file.name)}${escapeHtml(file.name)}`; dom.batchSourceGrid.appendChild(card); }); dom.batchMeta.textContent = state.batchFiles.length ? `${state.batchFiles.length} file(s) queued` : "Multiple inputs for batch mode"; } function renderBatchResults(items = []) { dom.batchResultGrid.innerHTML = ""; items.forEach((item, index) => { if (!item.image_base64) return; const card = document.createElement("div"); card.className = "thumb-card"; card.innerHTML = `Batch result ${index + 1}Result ${index + 1}`; dom.batchResultGrid.appendChild(card); }); } function setupUploadTile(tile, input, onFiles, multiple = false) { tile.addEventListener("click", () => input.click()); tile.addEventListener("dragover", (event) => { event.preventDefault(); tile.style.borderColor = "rgba(255, 179, 106, 0.55)"; }); tile.addEventListener("dragleave", () => { tile.style.borderColor = ""; }); tile.addEventListener("drop", async (event) => { event.preventDefault(); tile.style.borderColor = ""; const files = multiple ? [...event.dataTransfer.files] : [event.dataTransfer.files[0]]; await onFiles(files); }); input.addEventListener("change", async () => { const files = multiple ? [...input.files] : [input.files[0]]; await onFiles(files); }); } function ensureMaskBase() { if (!dom.maskCanvas.width || !dom.maskCanvas.height) { syncMaskCanvas(true); } maskCtx.save(); maskCtx.globalCompositeOperation = "source-over"; maskCtx.fillStyle = "black"; maskCtx.fillRect(0, 0, dom.maskCanvas.width, dom.maskCanvas.height); maskCtx.restore(); } function clearMask() { ensureMaskBase(); state.mask.hasData = false; dom.maskCanvas.hidden = true; } function syncMaskCanvas(force = false) { if (!state.sourceDataUrl || !state.mask.enabled) { dom.maskCanvas.hidden = true; return; } const image = dom.sourcePreview; if (!image.naturalWidth || !image.naturalHeight) return; if (force || dom.maskCanvas.width !== image.naturalWidth || dom.maskCanvas.height !== image.naturalHeight) { dom.maskCanvas.width = image.naturalWidth; dom.maskCanvas.height = image.naturalHeight; maskCtx.fillStyle = "black"; maskCtx.fillRect(0, 0, dom.maskCanvas.width, dom.maskCanvas.height); state.mask.hasData = false; } dom.maskCanvas.hidden = false; } function getCanvasPoint(event) { const rect = dom.maskCanvas.getBoundingClientRect(); const scaleX = dom.maskCanvas.width / rect.width; const scaleY = dom.maskCanvas.height / rect.height; return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY, }; } function drawMaskLine(from, to, pressure = 0.5) { const size = state.mask.size * Math.max(0.4, pressure || 0.5); maskCtx.save(); if (state.mask.tool === "erase") { maskCtx.globalCompositeOperation = "destination-out"; maskCtx.strokeStyle = "rgba(0,0,0,1)"; } else { maskCtx.globalCompositeOperation = "source-over"; maskCtx.strokeStyle = "white"; } maskCtx.lineCap = "round"; maskCtx.lineJoin = "round"; maskCtx.lineWidth = size; maskCtx.beginPath(); maskCtx.moveTo(from.x, from.y); maskCtx.lineTo(to.x, to.y); maskCtx.stroke(); maskCtx.restore(); state.mask.hasData = true; } function exportMaskBlob() { return new Promise((resolve) => { if (!state.mask.hasData) { resolve(null); return; } dom.maskCanvas.toBlob((blob) => resolve(blob), "image/png"); }); } function setMaskTool(tool) { state.mask.tool = tool; dom.maskBrush.classList.toggle("active", tool === "brush"); dom.maskErase.classList.toggle("active", tool === "erase"); } function startProgress() { stopProgress(); const messages = progressMessages[state.mode] || ["Working..."]; let index = 0; setProgress(messages[index]); state.progressTimer = window.setInterval(() => { index = (index + 1) % messages.length; setProgress(messages[index]); }, 1800); } function stopProgress() { if (state.progressTimer) { window.clearInterval(state.progressTimer); state.progressTimer = null; } setProgress("Idle"); } async function fetchHealth() { try { const response = await fetch("/health"); const data = await response.json(); state.info = data; dom.statusDot.className = "status-dot online"; dom.statusText.textContent = data.model_loaded ? `Ready - ${data.provider_label}` : `Starting - ${data.provider_label}`; dom.promptHint.textContent = data.prompt_hint || "Describe your edit."; dom.capabilityCount.textContent = data.capability_count; dom.presetCount.textContent = data.preset_count; } catch { dom.statusDot.className = "status-dot error"; dom.statusText.textContent = "Disconnected"; dom.promptHint.textContent = "The local server could not be reached."; } } async function fetchCapabilities() { try { const response = await fetch("/capabilities"); const data = await response.json(); state.capabilities = data.capabilities || []; renderCapabilityTags(); } catch { state.capabilities = []; } } async function fetchPresets() { try { const response = await fetch("/presets"); const data = await response.json(); state.presets = data.presets || []; renderPresets(); } catch { state.presets = []; renderPresets(); } } function buildCommonFormData(formData) { formData.append("num_steps", dom.stepsSlider.value); formData.append("text_guidance_scale", dom.textScaleSlider.value); formData.append("image_guidance_scale", dom.imageScaleSlider.value); if (dom.seedInput.value) formData.append("seed", dom.seedInput.value); } async function runCurrentMode() { if (state.isBusy) return; const prompt = dom.promptInput.value.trim(); const seed = dom.seedInput.value; try { state.isBusy = true; updateRunButton(); setResponse(""); startProgress(); renderStepChips([]); let endpoint = "/edit"; let response; if (state.mode === "edit") { const formData = new FormData(); formData.append("image", state.sourceFile); formData.append("prompt", prompt); buildCommonFormData(formData); if (state.referenceFile) formData.append("reference_image", state.referenceFile); if (state.backgroundFile) formData.append("background_image", state.backgroundFile); const maskBlob = await exportMaskBlob(); if (maskBlob) formData.append("mask", new File([maskBlob], "mask.png", { type: "image/png" })); response = await fetch("/edit", { method: "POST", body: formData }); } else if (state.mode === "generate") { const formData = new FormData(); formData.append("prompt", prompt); formData.append("width", dom.generateWidth.value); formData.append("height", dom.generateHeight.value); buildCommonFormData(formData); response = await fetch("/generate", { method: "POST", body: formData }); } else if (state.mode === "inpaint") { const maskBlob = await exportMaskBlob(); if (!maskBlob) throw new Error("Paint a mask before running inpaint."); const formData = new FormData(); formData.append("image", state.sourceFile); formData.append("mask", new File([maskBlob], "mask.png", { type: "image/png" })); formData.append("prompt", prompt || "repair the masked region naturally"); buildCommonFormData(formData); response = await fetch("/inpaint", { method: "POST", body: formData }); } else if (state.mode === "style") { const formData = new FormData(); formData.append("image", state.sourceFile); formData.append("reference_image", state.referenceFile); formData.append("prompt", prompt); if (seed) formData.append("seed", seed); response = await fetch("/style-transfer", { method: "POST", body: formData }); } else if (state.mode === "background") { const formData = new FormData(); formData.append("image", state.sourceFile); formData.append("prompt", prompt); if (state.backgroundFile) formData.append("background_image", state.backgroundFile); if (seed) formData.append("seed", seed); response = await fetch("/background", { method: "POST", body: formData }); } else if (state.mode === "upscale") { const formData = new FormData(); formData.append("image", state.sourceFile); formData.append("scale", dom.upscaleScale.value); formData.append("prompt", prompt); if (seed) formData.append("seed", seed); response = await fetch("/upscale", { method: "POST", body: formData }); } else if (state.mode === "batch") { const formData = new FormData(); state.batchFiles.forEach((file) => formData.append("images", file)); formData.append("prompt", prompt); buildCommonFormData(formData); response = await fetch("/batch", { method: "POST", body: formData }); } const data = await response.json(); if (state.mode === "batch") { if (!data.success) throw new Error(data.message || "Batch request failed."); renderBatchResults(data.items || []); if (data.items && data.items[0] && data.items[0].image_base64) { state.resultDataUrl = `data:image/png;base64,${data.items[0].image_base64}`; } renderStage(); setResponse(data.message || "Batch complete."); renderLayers(((data.items || [])[0] || {}).steps || []); showToast("Batch Complete", `${(data.items || []).length} result(s) rendered.`); return; } if (!data.success || !data.image_base64) { throw new Error(data.message || "Request failed."); } state.resultDataUrl = `data:image/png;base64,${data.image_base64}`; renderStage(); renderLayers(data.steps || []); renderStepChips(data.steps || []); setResponse(data.message || "Operation complete."); pushHistory({ mode: state.mode, prompt, resultDataUrl: state.resultDataUrl, sourceDataUrl: state.sourceDataUrl, steps: data.steps || [], message: data.message || "", }); showToast("Completed", `${state.mode} finished successfully.`); } catch (error) { setResponse(error.message || "Something went wrong.", true); showToast("Request Failed", error.message || "Something went wrong."); } finally { state.isBusy = false; stopProgress(); updateRunButton(); } } function downloadResult() { if (!state.resultDataUrl) return; const link = document.createElement("a"); link.href = state.resultDataUrl; link.download = `imageeditter_${state.mode}_${Date.now()}.png`; link.click(); } function handleKeyboard(event) { const isModifier = event.ctrlKey || event.metaKey; if (isModifier && event.key.toLowerCase() === "z" && !event.shiftKey) { event.preventDefault(); undoHistory(); } if (isModifier && (event.key.toLowerCase() === "y" || (event.shiftKey && event.key.toLowerCase() === "z"))) { event.preventDefault(); redoHistory(); } if (isModifier && event.key.toLowerCase() === "s") { event.preventDefault(); downloadResult(); } if (state.mode === "inpaint" && event.key === "[") { event.preventDefault(); dom.maskSize.value = Math.max(4, Number(dom.maskSize.value) - 2); updateSliderLabels(); } if (state.mode === "inpaint" && event.key === "]") { event.preventDefault(); dom.maskSize.value = Math.min(80, Number(dom.maskSize.value) + 2); updateSliderLabels(); } } setupUploadTile(dom.sourceDrop, dom.sourceInput, async (files) => { if (files[0]) await loadSourceFile(files[0]); }); setupUploadTile(dom.referenceDrop, dom.referenceInput, async (files) => { if (files[0]) await loadReferenceFile(files[0]); }); setupUploadTile(dom.backgroundDrop, dom.backgroundInput, async (files) => { if (files[0]) await loadBackgroundFile(files[0]); }); setupUploadTile(dom.batchDrop, dom.batchInput, async (files) => { state.batchFiles = files.filter((file) => file && file.type.startsWith("image/")); renderBatchSources(); updateRunButton(); }, true); dom.modeTabs.forEach((button) => button.addEventListener("click", () => setMode(button.dataset.mode))); dom.runButton.addEventListener("click", runCurrentMode); dom.btnUndo.addEventListener("click", undoHistory); dom.btnRedo.addEventListener("click", redoHistory); dom.btnDownload.addEventListener("click", downloadResult); dom.btnClearAll.addEventListener("click", () => { state.sourceFile = null; state.referenceFile = null; state.backgroundFile = null; state.batchFiles = []; state.sourceDataUrl = ""; state.resultDataUrl = ""; dom.sourceMeta.textContent = "Drop or browse PNG, JPG, WebP"; dom.referenceMeta.textContent = "Used for style transfer"; dom.backgroundMeta.textContent = "Optional replacement backdrop"; dom.batchMeta.textContent = "Multiple inputs for batch mode"; dom.batchSourceGrid.innerHTML = ""; dom.batchResultGrid.innerHTML = ""; dom.promptInput.value = ""; clearMask(); renderStage(); renderLayers([]); renderStepChips([]); setResponse(""); resetStageTransform(); updateRunButton(); }); dom.btnViewToggle.addEventListener("click", () => { state.compareMode = !state.compareMode; dom.btnViewToggle.textContent = state.compareMode ? "Compare" : "Result Only"; applyCompare(); }); dom.btnFullscreen.addEventListener("click", () => { state.fullscreen = !state.fullscreen; document.body.classList.toggle("fullscreen-mode", state.fullscreen); }); dom.compareSlider.addEventListener("input", applyCompare); dom.stepsSlider.addEventListener("input", updateSliderLabels); dom.textScaleSlider.addEventListener("input", updateSliderLabels); dom.imageScaleSlider.addEventListener("input", updateSliderLabels); dom.upscaleScale.addEventListener("input", updateSliderLabels); dom.maskSize.addEventListener("input", updateSliderLabels); dom.promptInput.addEventListener("input", updateRunButton); const adjustSliders = [ dom.adjustWarmth, dom.adjustBrightness, dom.adjustContrast, dom.adjustClarity, dom.adjustSharpness, dom.adjustVignette, dom.adjustBloom ]; adjustSliders.forEach(slider => { if (slider) { slider.addEventListener("input", () => { updateSliderLabels(); triggerAdjustment(); }); } }); if (dom.btnResetAdjust) { dom.btnResetAdjust.addEventListener("click", () => { dom.adjustWarmth.value = "1.0"; dom.adjustBrightness.value = "1.0"; dom.adjustContrast.value = "1.0"; dom.adjustClarity.value = "1.0"; dom.adjustSharpness.value = "0.0"; dom.adjustVignette.value = "0.0"; dom.adjustBloom.value = "0.0"; updateSliderLabels(); triggerAdjustment(); showToast("Adjustments Reset", "All fine-tuning sliders set to defaults"); }); } dom.maskMagic.addEventListener("click", async () => { if (!state.sourceDataUrl) { showToast("No Source Image", "Please upload a source image first."); return; } try { state.isBusy = true; updateRunButton(); showToast("Magic Selecting...", "Segmenting foreground subject using local GrabCut AI..."); const sourceBlob = await dataUrlToBlob(state.sourceDataUrl); const formData = new FormData(); formData.append("image", sourceBlob, "source.png"); const res = await fetch("/segment", { method: "POST", body: formData, }); const data = await res.json(); if (data.success && data.mask_base64) { const maskImg = new Image(); maskImg.onload = () => { syncMaskCanvas(true); maskCtx.save(); maskCtx.globalCompositeOperation = "source-over"; maskCtx.drawImage(maskImg, 0, 0, dom.maskCanvas.width, dom.maskCanvas.height); maskCtx.restore(); state.mask.hasData = true; showToast("Magic Selected!", "Subject successfully isolated! You can now run inpaint or background replace."); updateRunButton(); }; maskImg.src = `data:image/png;base64,${data.mask_base64}`; } else { throw new Error(data.message || "Failed to segment."); } } catch (e) { showToast("Selection Failed", e.message || "Something went wrong."); } finally { state.isBusy = false; updateRunButton(); } }); dom.maskBrush.addEventListener("click", () => setMaskTool("brush")); dom.maskErase.addEventListener("click", () => setMaskTool("erase")); dom.maskFill.addEventListener("click", () => { syncMaskCanvas(true); maskCtx.save(); maskCtx.globalCompositeOperation = "source-over"; maskCtx.fillStyle = "white"; maskCtx.fillRect(0, 0, dom.maskCanvas.width, dom.maskCanvas.height); maskCtx.restore(); state.mask.hasData = true; }); dom.maskClear.addEventListener("click", clearMask); dom.maskCanvas.addEventListener("pointerdown", (event) => { if (!state.mask.enabled || !state.sourceDataUrl) return; syncMaskCanvas(); state.mask.drawing = true; dom.maskCanvas.style.pointerEvents = "auto"; state.mask.lastPoint = getCanvasPoint(event); drawMaskLine(state.mask.lastPoint, state.mask.lastPoint, event.pressure || 0.5); dom.maskCanvas.setPointerCapture(event.pointerId); }); dom.maskCanvas.addEventListener("pointermove", (event) => { if (!state.mask.drawing) return; const point = getCanvasPoint(event); drawMaskLine(state.mask.lastPoint, point, event.pressure || 0.5); state.mask.lastPoint = point; }); const stopMaskDrawing = () => { state.mask.drawing = false; state.mask.lastPoint = null; }; dom.maskCanvas.addEventListener("pointerup", stopMaskDrawing); dom.maskCanvas.addEventListener("pointercancel", stopMaskDrawing); dom.maskCanvas.addEventListener("pointerleave", () => { if (!state.mask.drawing) return; stopMaskDrawing(); }); dom.stageFrame.addEventListener("wheel", (event) => { if (state.mask.enabled && state.mask.drawing) return; event.preventDefault(); const direction = event.deltaY > 0 ? -0.08 : 0.08; state.stageScale = Math.min(4, Math.max(0.6, state.stageScale + direction)); applyStageTransform(); }); dom.stageFrame.addEventListener("pointerdown", (event) => { if (state.mask.enabled) return; state.stageDragging = true; state.stageDragOrigin = { x: event.clientX - state.stageOffsetX, y: event.clientY - state.stageOffsetY, }; }); window.addEventListener("pointermove", (event) => { if (!state.stageDragging || !state.stageDragOrigin) return; state.stageOffsetX = event.clientX - state.stageDragOrigin.x; state.stageOffsetY = event.clientY - state.stageDragOrigin.y; applyStageTransform(); }); window.addEventListener("pointerup", () => { state.stageDragging = false; state.stageDragOrigin = null; }); dom.stageFrame.addEventListener("dblclick", resetStageTransform); document.addEventListener("keydown", handleKeyboard); dom.sourcePreview.addEventListener("load", () => { syncMaskCanvas(true); renderStage(); }); updateSliderLabels(); loadHistory(); renderHistory(); renderLayers([]); renderCapabilityTags(); updateModeUI(); renderStage(); updateRunButton(); fetchHealth(); fetchCapabilities(); fetchPresets(); setInterval(fetchHealth, 12000); });