mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-08 11:56:59 +00:00
Add AI edit command box to gallery editor
This commit is contained in:
@@ -265,6 +265,134 @@ const _applyImageTool = createApplyImageTool({
|
|||||||
uiModule,
|
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 offsets for move tool
|
||||||
|
|
||||||
// ── Layer class ──
|
// ── Layer class ──
|
||||||
@@ -2551,6 +2679,7 @@ function _buildEditor(container) {
|
|||||||
state.transformOverlay.className = 'ge-transform-overlay';
|
state.transformOverlay.className = 'ge-transform-overlay';
|
||||||
state.transformOverlayCtx = state.transformOverlay.getContext('2d');
|
state.transformOverlayCtx = state.transformOverlay.getContext('2d');
|
||||||
canvasArea.appendChild(state.transformOverlay);
|
canvasArea.appendChild(state.transformOverlay);
|
||||||
|
canvasArea.appendChild(_buildAiCommandBox());
|
||||||
// Keep the transform handles glued to the photo while the canvas-area
|
// 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
|
// scrolls (the overlay is anchored to the canvas's live rect, so a
|
||||||
// re-draw on scroll re-reads its position).
|
// re-draw on scroll re-reads its position).
|
||||||
@@ -2879,6 +3008,7 @@ function _buildEditor(container) {
|
|||||||
applyImageTool: _applyImageTool,
|
applyImageTool: _applyImageTool,
|
||||||
uiModule,
|
uiModule,
|
||||||
});
|
});
|
||||||
|
_wireAiCommandBox();
|
||||||
|
|
||||||
// Merge / Flatten buttons (layer-panel footer) — full
|
// Merge / Flatten buttons (layer-panel footer) — full
|
||||||
// implementation in editor/wire-merge-buttons.js.
|
// implementation in editor/wire-merge-buttons.js.
|
||||||
|
|||||||
@@ -26002,6 +26002,126 @@ details.hwfit-serve-advanced > .hwfit-serve-checks:last-of-type {
|
|||||||
background: color-mix(in srgb, var(--bg) 30%, transparent);
|
background: color-mix(in srgb, var(--bg) 30%, transparent);
|
||||||
pointer-events: none;
|
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
|
/* Loading overlay shown while a large image is decoding — centered
|
||||||
whirlpool + "Loading" caption over a translucent dim. */
|
whirlpool + "Loading" caption over a translucent dim. */
|
||||||
.ge-loading-overlay {
|
.ge-loading-overlay {
|
||||||
|
|||||||
Reference in New Issue
Block a user