Add AI edit command box to gallery editor

This commit is contained in:
pewdiepie-archdaemon
2026-07-03 01:43:37 +00:00
parent 0fc98c4a17
commit 5777cf2d00
2 changed files with 250 additions and 0 deletions
+130
View File
@@ -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 = `
<button type="button" class="ge-ai-command-toggle" id="ge-ai-command-toggle" aria-expanded="false">
<span class="ge-btn-ai-mark" aria-hidden="true">✦</span>
<span>AI Edit</span>
</button>
<form class="ge-ai-command-form" id="ge-ai-command-form">
<input type="text" class="ge-ai-command-input" id="ge-ai-command-input" autocomplete="off" />
<button type="submit" class="ge-ai-command-run" id="ge-ai-command-run" title="Run AI edit" aria-label="Run AI edit">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</button>
<button type="button" class="ge-ai-command-close" id="ge-ai-command-close" title="Close AI edit" aria-label="Close AI edit">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</form>
<div class="ge-ai-command-status" id="ge-ai-command-status" aria-live="polite"></div>
`;
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.
+120
View File
@@ -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 {