diff --git a/static/js/galleryEditor.js b/static/js/galleryEditor.js index d6a6b9e31..abbc17e4c 100644 --- a/static/js/galleryEditor.js +++ b/static/js/galleryEditor.js @@ -265,6 +265,134 @@ const _applyImageTool = createApplyImageTool({ uiModule, }); +function _setAiCommandStatus(text, kind = '') { + const el = document.getElementById('ge-ai-command-status'); + if (!el) return; + el.textContent = text || ''; + el.dataset.kind = kind || ''; +} + +function _clickToolButton(toolId) { + const btn = state.container?.querySelector(`.ge-tool-btn[data-tool="${toolId}"]`); + if (btn) btn.click(); +} + +function _runExistingButton(id, status) { + const btn = document.getElementById(id); + if (!btn) { + _setAiCommandStatus('That edit is not available in this editor state.', 'error'); + return false; + } + if (status) _setAiCommandStatus(status, 'running'); + btn.click(); + return true; +} + +function _buildAiCommandBox() { + const wrap = document.createElement('div'); + wrap.className = 'ge-ai-command ge-ai-command-collapsed'; + wrap.id = 'ge-ai-command'; + wrap.innerHTML = ` + +
+ + `; + return wrap; +} + +function _wireAiCommandBox() { + const wrap = document.getElementById('ge-ai-command'); + const toggle = document.getElementById('ge-ai-command-toggle'); + const closeBtn = document.getElementById('ge-ai-command-close'); + const form = document.getElementById('ge-ai-command-form'); + const input = document.getElementById('ge-ai-command-input'); + const runBtn = document.getElementById('ge-ai-command-run'); + if (!wrap || !form || !input || !runBtn) return; + wrap.addEventListener('pointerdown', (e) => e.stopPropagation()); + wrap.addEventListener('click', (e) => e.stopPropagation()); + const setOpen = (open) => { + wrap.classList.toggle('ge-ai-command-collapsed', !open); + toggle?.setAttribute('aria-expanded', open ? 'true' : 'false'); + if (open) requestAnimationFrame(() => input.focus()); + }; + toggle?.addEventListener('click', () => setOpen(wrap.classList.contains('ge-ai-command-collapsed'))); + closeBtn?.addEventListener('click', () => setOpen(false)); + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const prompt = input.value.trim(); + if (!prompt) { + _setAiCommandStatus('Type what you want changed.', 'error'); + input.focus(); + return; + } + const p = prompt.toLowerCase(); + try { + if (/\b(remove|erase|cut\s*out|transparent)\b.*\b(bg|background)\b|\b(bg|background)\b.*\b(remove|erase|transparent)\b/.test(p)) { + _clickToolButton('rembg'); + _runExistingButton('ge-rembg-run', 'Removing background...'); + return; + } + if (/\b(upscale|higher\s*res|increase\s*resolution|bigger|2x|4x)\b/.test(p)) { + _clickToolButton('upscale'); + _runExistingButton('ge-upscale-ai', 'Upscaling image...'); + return; + } + if (/\b(denoise|noise|grain|grainy|clean\s*up)\b/.test(p)) { + _setAiCommandStatus('Denoising image...', 'running'); + await _applyImageTool('/api/image/denoise', { strength: 0.55 }, 'Denoised', runBtn, { busyLabel: 'Denoising...' }); + _setAiCommandStatus('Added denoised layer.', 'done'); + return; + } + if (/\b(face|portrait|skin|selfie|restore)\b/.test(p)) { + _setAiCommandStatus('Enhancing face/portrait...', 'running'); + await _applyImageTool('/api/image/enhance-face', {}, 'Enhanced Face', runBtn, { busyLabel: 'Enhancing...' }); + _setAiCommandStatus('Added enhanced layer.', 'done'); + return; + } + if (/\b(sharpen|sharp|crisp|clearer|make it look better|enhance|improve|better)\b/.test(p)) { + const amount = document.getElementById('ge-sharpen-amount'); + if (amount) { + amount.value = '65'; + amount.dispatchEvent(new Event('input', { bubbles: true })); + } + _clickToolButton('sharpen'); + _runExistingButton('ge-sharpen-run', 'Sharpening image...'); + return; + } + + const stylePrompt = document.getElementById('ge-style-prompt'); + const styleStrength = document.getElementById('ge-style-strength'); + if (stylePrompt) stylePrompt.value = prompt; + if (styleStrength) { + styleStrength.value = /\b(subtle|slight|small)\b/.test(p) ? '35' : '55'; + styleStrength.dispatchEvent(new Event('input', { bubbles: true })); + } + _clickToolButton('style'); + _runExistingButton('ge-style-run', 'Running full-image AI edit...'); + } catch (err) { + console.error('[ge-ai-command] failed', err); + _setAiCommandStatus(err?.message || 'AI edit failed', 'error'); + } + }); +} + // Layer offsets for move tool // ── Layer class ── @@ -2551,6 +2679,7 @@ function _buildEditor(container) { state.transformOverlay.className = 'ge-transform-overlay'; state.transformOverlayCtx = state.transformOverlay.getContext('2d'); canvasArea.appendChild(state.transformOverlay); + canvasArea.appendChild(_buildAiCommandBox()); // Keep the transform handles glued to the photo while the canvas-area // scrolls (the overlay is anchored to the canvas's live rect, so a // re-draw on scroll re-reads its position). @@ -2879,6 +3008,7 @@ function _buildEditor(container) { applyImageTool: _applyImageTool, uiModule, }); + _wireAiCommandBox(); // Merge / Flatten buttons (layer-panel footer) — full // implementation in editor/wire-merge-buttons.js. diff --git a/static/style.css b/static/style.css index 33751e7ed..ea04342f1 100644 --- a/static/style.css +++ b/static/style.css @@ -26002,6 +26002,126 @@ details.hwfit-serve-advanced > .hwfit-serve-checks:last-of-type { background: color-mix(in srgb, var(--bg) 30%, transparent); pointer-events: none; } +.ge-ai-command { + position: absolute; + left: 50%; + bottom: 12px; + z-index: 18; + width: min(430px, calc(100% - 28px)); + transform: translateX(-50%); + padding: 7px; + box-sizing: border-box; + border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 42%, var(--border)); + border-radius: 8px; + background: color-mix(in srgb, var(--panel, var(--bg)) 94%, transparent); + box-shadow: 0 8px 22px color-mix(in srgb, #000 26%, transparent); + backdrop-filter: blur(8px); +} +.ge-ai-command-toggle { + width: 100%; + height: 28px; + display: none; + align-items: center; + justify-content: center; + gap: 5px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--accent, var(--red)); + font: inherit; + font-size: 11px; + font-weight: 600; + cursor: pointer; +} +.ge-ai-command-collapsed { + width: auto; + min-width: 112px; + padding: 2px 8px; +} +.ge-ai-command-collapsed .ge-ai-command-toggle { + display: inline-flex; +} +.ge-ai-command-collapsed .ge-ai-command-form, +.ge-ai-command-collapsed .ge-ai-command-status { + display: none; +} +.ge-ai-command-form { + display: flex; + align-items: center; + gap: 6px; +} +.ge-ai-command-input { + flex: 1 1 auto; + min-width: 0; + height: 32px; + box-sizing: border-box; + padding: 0 9px; + border: 1px solid var(--border); + border-radius: 8px; + background: color-mix(in srgb, var(--bg) 76%, #000 24%); + color: var(--fg); + font: inherit; + font-size: 12px; +} +.ge-ai-command-input:focus { + outline: none; + border-color: var(--accent, var(--red)); +} +.ge-ai-command-run { + background: var(--send-btn-bg, var(--red)); + color: #fff; + border: none; + border-radius: 8px; + min-width: 32px; + width: 32px; + height: 32px !important; + padding: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + transition: background 0.25s, opacity 0.12s; + overflow: hidden; +} +.ge-ai-command-run:hover { + background: var(--send-btn-hover, color-mix(in srgb, var(--red) 80%, white)); +} +.ge-ai-command-close { + width: 28px; + height: 32px; + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--fg); + opacity: 0.52; + cursor: pointer; + padding: 0; +} +.ge-ai-command-close:hover { + opacity: 0.9; + background: color-mix(in srgb, var(--fg) 10%, transparent); +} +.ge-ai-command-status { + min-height: 13px; + margin-top: 4px; + font-size: 10px; + line-height: 1.3; + color: var(--fg-muted); +} +.ge-ai-command-status[data-kind="running"] { + color: var(--accent, var(--red)); +} +.ge-ai-command-status[data-kind="done"] { + color: var(--green, #4caf50); +} +.ge-ai-command-status[data-kind="error"] { + color: var(--red, #e5484d); +} /* Loading overlay shown while a large image is decoding — centered whirlpool + "Loading" caption over a translucent dim. */ .ge-loading-overlay {