From a842087590ad3eda62f7e43a1a8fe1335beff0ff Mon Sep 17 00:00:00 2001 From: root Date: Wed, 18 Feb 2026 00:24:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(workbench-ui):=20=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E4=B8=8E=E5=A2=9E=E5=BC=BA=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E5=86=85=E5=B5=8C=E5=88=B0=E6=8F=90=E7=A4=BA=E8=AF=8D=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/public/css/imagine_workbench.css | 16 + app/static/public/js/imagine_workbench.js | 978 ++++++++++++++++++++ 2 files changed, 994 insertions(+) create mode 100644 app/static/public/js/imagine_workbench.js diff --git a/app/static/public/css/imagine_workbench.css b/app/static/public/css/imagine_workbench.css index b2f4d4d..1830c8a 100644 --- a/app/static/public/css/imagine_workbench.css +++ b/app/static/public/css/imagine_workbench.css @@ -224,10 +224,22 @@ body { width: 100%; display: block; box-sizing: border-box; + min-height: 150px; + padding-left: 112px; padding-right: 116px; padding-bottom: 44px; } +.field .prompt-enhance-wrap > .inline-submit-btn { + position: absolute; + left: 8px; + bottom: 8px; + z-index: 3; + height: 30px; + min-width: 88px; + padding: 0 10px; +} + .field .prompt-enhance-wrap > .prompt-enhance-btn { right: 8px; bottom: 8px; @@ -245,6 +257,10 @@ body { gap: 8px; } +.action-row.action-row-inline { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + .edit-progress-wrap { margin-top: 10px; width: 100%; diff --git a/app/static/public/js/imagine_workbench.js b/app/static/public/js/imagine_workbench.js new file mode 100644 index 0000000..b20ea81 --- /dev/null +++ b/app/static/public/js/imagine_workbench.js @@ -0,0 +1,978 @@ +(() => { + const seedImageInput = document.getElementById('seedImageInput'); + const selectSeedBtn = document.getElementById('selectSeedBtn'); + const seedFileName = document.getElementById('seedFileName'); + const parentPostInput = document.getElementById('parentPostInput'); + const applyParentBtn = document.getElementById('applyParentBtn'); + const previewShell = document.getElementById('previewShell'); + const currentImage = document.getElementById('currentImage'); + const previewEmpty = document.getElementById('previewEmpty'); + const currentParentId = document.getElementById('currentParentId'); + const currentMode = document.getElementById('currentMode'); + const editPromptInput = document.getElementById('editPromptInput'); + const submitEditBtn = document.getElementById('submitEditBtn'); + const resetCycleBtn = document.getElementById('resetCycleBtn'); + const clearHistoryBtn = document.getElementById('clearHistoryBtn'); + const editStatusText = document.getElementById('editStatusText'); + const historyCount = document.getElementById('historyCount'); + const historyEmpty = document.getElementById('historyEmpty'); + const historyList = document.getElementById('historyList'); + const editProgressWrap = document.getElementById('editProgressWrap'); + const editProgressBar = document.getElementById('editProgressBar'); + const editProgressText = document.getElementById('editProgressText'); + + const state = { + editing: false, + seedImageBase64: '', + seedFileName: '', + currentImageUrl: '', + currentParentPostId: '', + currentSourceImageUrl: '', + currentModeValue: 'upload', + history: [], + editRound: 0, + }; + let editProgressTimer = null; + let editProgressHideTimer = null; + let editProgressValue = 0; + let dragCounter = 0; + + function toast(message, type) { + if (typeof showToast === 'function') { + showToast(message, type); + } + } + + function getParentMemoryApi() { + return window.ParentPostMemory || null; + } + + function rememberParentPost(entry) { + const api = getParentMemoryApi(); + if (!api || !entry) return; + try { + api.remember(entry); + } catch (e) { + // ignore + } + } + + async function copyText(value) { + const text = String(value || '').trim(); + if (!text) return false; + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + return true; + } + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.setAttribute('readonly', 'readonly'); + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + textarea.style.pointerEvents = 'none'; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + const copied = document.execCommand('copy'); + document.body.removeChild(textarea); + return copied; + } + + function shortId(value) { + const raw = String(value || '').trim(); + if (!raw) return '-'; + if (raw.length <= 14) return raw; + return `${raw.slice(0, 7)}...${raw.slice(-7)}`; + } + + function formatTime(ts) { + try { + return new Date(ts).toLocaleString('zh-CN', { + hour12: false, + }); + } catch (e) { + return '-'; + } + } + + function maskPrompt(prompt) { + const raw = String(prompt || '').trim(); + if (!raw) return '-'; + return raw; + } + + function buildImaginePublicUrl(parentPostId) { + return `https://imagine-public.x.ai/imagine-public/images/${parentPostId}.jpg`; + } + + function normalizeHttpSourceUrl(value) { + const raw = String(value || '').trim(); + if (!raw) return ''; + if (raw.startsWith('data:')) return ''; + if (raw.startsWith('http://') || raw.startsWith('https://')) { + return raw; + } + if (raw.startsWith('/')) { + return `${window.location.origin}${raw}`; + } + return ''; + } + + function pickSourceImageUrl(hit, parentPostId, fallbackValue = '') { + const candidates = [ + hit && hit.current_source_image_url, + hit && hit.currentSourceImageUrl, + hit && hit.source_image_url, + hit && hit.sourceImageUrl, + hit && hit.image_url, + hit && hit.imageUrl, + fallbackValue, + ]; + for (const candidate of candidates) { + const normalized = normalizeHttpSourceUrl(candidate); + if (normalized) return normalized; + } + return parentPostId ? buildImaginePublicUrl(parentPostId) : ''; + } + + function pickPreviewUrl(hit, parentPostId) { + const candidates = [ + hit && hit.imageUrl, + hit && hit.image_url, + hit && hit.url, + hit && hit.sourceImageUrl, + hit && hit.source_image_url, + ]; + for (const candidate of candidates) { + const raw = String(candidate || '').trim(); + if (raw) return raw; + } + const source = pickSourceImageUrl(hit, parentPostId); + return source || (parentPostId ? buildImaginePublicUrl(parentPostId) : ''); + } + + function extractParentPostId(value) { + const raw = String(value || '').trim(); + if (!raw) return ''; + const direct = raw.match(/^[0-9a-fA-F-]{32,36}$/); + if (direct) return direct[0]; + const generated = raw.match(/\/generated\/([0-9a-fA-F-]{32,36})(?:\/|$)/); + if (generated) return generated[1]; + const imaginePublic = raw.match(/\/imagine-public\/images\/([0-9a-fA-F-]{32,36})(?:\.jpg|\/|$)/); + if (imaginePublic) return imaginePublic[1]; + const imagePath = raw.match(/\/images\/([0-9a-fA-F-]{32,36})(?:\.jpg|\/|$)/); + if (imagePath) return imagePath[1]; + const all = raw.match(/([0-9a-fA-F-]{32,36})/g); + return all && all.length ? all[all.length - 1] : ''; + } + + function resolveParentMemoryByText(text) { + const input = String(text || '').trim(); + if (!input) return null; + const api = getParentMemoryApi(); + if (api && typeof api.resolveByText === 'function') { + try { + const hit = api.resolveByText(input); + if (hit && hit.parentPostId) { + const parentPostId = String(hit.parentPostId || '').trim(); + return { + ...hit, + parentPostId, + }; + } + return hit; + } catch (e) { + // ignore + } + } + const parentPostId = extractParentPostId(input); + if (!parentPostId) return null; + return { + parentPostId, + sourceImageUrl: buildImaginePublicUrl(parentPostId), + imageUrl: buildImaginePublicUrl(parentPostId), + origin: 'fallback', + }; + } + + function applyParentPostFromText(text, options = {}) { + const silent = Boolean(options.silent); + const hit = resolveParentMemoryByText(text); + if (!hit || !hit.parentPostId) { + if (!silent) { + toast('未识别到有效 parentPostId', 'warning'); + } + return false; + } + const parentPostId = String(hit.parentPostId || '').trim(); + const sourceImageUrl = pickSourceImageUrl(hit, parentPostId); + const previewUrl = pickPreviewUrl(hit, parentPostId); + + state.currentParentPostId = parentPostId; + state.currentSourceImageUrl = sourceImageUrl; + state.currentModeValue = 'parent_post'; + setPreview(previewUrl); + updateMeta(); + setStatus('done', `已加载 parentPostId:${shortId(parentPostId)}`); + if (parentPostInput) { + parentPostInput.value = parentPostId; + } + rememberParentPost({ + parentPostId, + sourceImageUrl, + imageUrl: previewUrl, + origin: 'workbench_paste_apply', + }); + if (!silent) { + toast('已载入 parentPostId,可直接继续编辑', 'success'); + } + return true; + } + + function setStatus(stateName, text) { + if (!editStatusText) return; + editStatusText.textContent = text; + editStatusText.classList.remove('running', 'done', 'error'); + if (stateName) { + editStatusText.classList.add(stateName); + } + } + + function clearEditProgressTimer() { + if (editProgressTimer) { + clearInterval(editProgressTimer); + editProgressTimer = null; + } + if (editProgressHideTimer) { + clearTimeout(editProgressHideTimer); + editProgressHideTimer = null; + } + } + + function setEditProgress(value, text) { + const safe = Math.max(0, Math.min(100, Math.round(value || 0))); + editProgressValue = safe; + if (editProgressBar) { + editProgressBar.style.width = `${safe}%`; + } + if (editProgressText) { + editProgressText.textContent = text || `编辑中 ${safe}%`; + } + } + + function showEditProgress() { + if (editProgressWrap) { + editProgressWrap.classList.add('active'); + editProgressWrap.classList.remove('is-success', 'is-error'); + } + if (editProgressText) { + editProgressText.classList.add('active'); + } + setEditProgress(4, '编辑中 4%'); + } + + function hideEditProgress() { + clearEditProgressTimer(); + if (editProgressWrap) { + editProgressWrap.classList.remove('active', 'is-success', 'is-error'); + } + if (editProgressText) { + editProgressText.classList.remove('active'); + editProgressText.textContent = '编辑中 0%'; + } + if (editProgressBar) { + editProgressBar.style.width = '0%'; + } + editProgressValue = 0; + } + + function startEditProgress() { + clearEditProgressTimer(); + showEditProgress(); + } + + function finishEditProgress(success, text) { + clearEditProgressTimer(); + if (!editProgressWrap) return; + editProgressWrap.classList.add('active'); + editProgressWrap.classList.remove('is-success', 'is-error'); + editProgressWrap.classList.add(success ? 'is-success' : 'is-error'); + if (editProgressText) { + editProgressText.classList.add('active'); + } + setEditProgress(100, text || (success ? '编辑完成 100%' : '编辑失败')); + editProgressHideTimer = setTimeout(() => { + hideEditProgress(); + editProgressHideTimer = null; + }, 900); + } + + async function readFileAsDataUrl(file) { + return await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result || '')); + reader.onerror = () => reject(new Error('读取文件失败')); + reader.readAsDataURL(file); + }); + } + + async function applySeedImageFile(file, source) { + if (!file) return; + if (state.editing) { + toast('编辑进行中,暂时不能替换首图', 'warning'); + return; + } + const mimeType = String(file.type || ''); + if (mimeType && !mimeType.startsWith('image/')) { + toast('仅支持图片文件', 'warning'); + return; + } + const dataUrl = await readFileAsDataUrl(file); + if (!dataUrl.startsWith('data:image/')) { + throw new Error('图片格式不受支持'); + } + + state.seedImageBase64 = dataUrl; + state.seedFileName = file.name || source || '未命名图片'; + clearHistory(); + resetCycle(true); + setStatus('', '已载入首图,可开始编辑'); + updateMeta(); + toast(`${source || '图片'}已载入,链路已重置`, 'success'); + } + + function setDragActive(active) { + if (!previewShell) return; + previewShell.classList.toggle('dragover', Boolean(active)); + } + + function pickImageFileFromDataTransfer(dataTransfer) { + if (!dataTransfer) return null; + if (dataTransfer.files && dataTransfer.files.length) { + for (const file of dataTransfer.files) { + if (file && String(file.type || '').startsWith('image/')) { + return file; + } + } + } + if (dataTransfer.items && dataTransfer.items.length) { + for (const item of dataTransfer.items) { + if (!item) continue; + if (item.kind === 'file') { + const file = item.getAsFile ? item.getAsFile() : null; + if (file && String(file.type || '').startsWith('image/')) { + return file; + } + } + } + } + return null; + } + + function hasFiles(dataTransfer) { + if (!dataTransfer) return false; + if (dataTransfer.files && dataTransfer.files.length > 0) return true; + const types = dataTransfer.types; + if (!types) return false; + return Array.from(types).includes('Files'); + } + + function updateMeta() { + if (currentParentId) { + currentParentId.textContent = state.currentParentPostId || '-'; + } + if (currentMode) { + currentMode.textContent = state.currentModeValue || '-'; + } + if (seedFileName) { + seedFileName.textContent = state.seedFileName || '未选择文件'; + } + } + + function setPreview(url) { + const displayUrl = String(url || '').trim(); + state.currentImageUrl = displayUrl; + if (!currentImage || !previewEmpty) return; + if (!displayUrl) { + currentImage.src = ''; + currentImage.classList.add('hidden'); + previewEmpty.classList.remove('hidden'); + return; + } + currentImage.src = displayUrl; + currentImage.classList.remove('hidden'); + previewEmpty.classList.add('hidden'); + } + + function setEditing(loading) { + state.editing = Boolean(loading); + if (submitEditBtn) submitEditBtn.disabled = state.editing; + if (selectSeedBtn) selectSeedBtn.disabled = state.editing; + if (resetCycleBtn) resetCycleBtn.disabled = state.editing; + if (clearHistoryBtn) clearHistoryBtn.disabled = state.editing; + if (editPromptInput) editPromptInput.disabled = state.editing; + + if (state.editing) { + setStatus('running', '编辑中...'); + if (submitEditBtn) submitEditBtn.textContent = '编辑中...'; + } else { + if (submitEditBtn) submitEditBtn.textContent = '执行编辑'; + } + } + + function setCurrentFromEntry(entry) { + if (!entry) return; + state.currentParentPostId = String(entry.parentPostId || '').trim(); + state.currentSourceImageUrl = String(entry.sourceImageUrl || '').trim(); + state.currentModeValue = String(entry.mode || '').trim() || 'parent_post'; + if (parentPostInput) { + parentPostInput.value = state.currentParentPostId; + } + setPreview(entry.imageUrl || ''); + rememberParentPost({ + parentPostId: state.currentParentPostId, + sourceImageUrl: state.currentSourceImageUrl, + imageUrl: entry.imageUrl || '', + origin: 'workbench_history_apply', + }); + updateMeta(); + setStatus('done', `已切换到历史版本 #${entry.round}`); + } + + function renderHistory() { + if (!historyList || !historyEmpty || !historyCount) return; + historyList.innerHTML = ''; + historyCount.textContent = `${state.history.length} 条`; + + if (!state.history.length) { + historyEmpty.classList.remove('hidden'); + return; + } + historyEmpty.classList.add('hidden'); + + state.history.forEach((entry) => { + const item = document.createElement('div'); + item.className = 'history-item'; + + const thumb = document.createElement('img'); + thumb.className = 'history-thumb'; + thumb.src = entry.imageUrl || ''; + thumb.alt = `history-${entry.round}`; + thumb.loading = 'lazy'; + thumb.decoding = 'async'; + + const main = document.createElement('div'); + main.className = 'history-main'; + + const line1 = document.createElement('div'); + line1.className = 'history-line'; + line1.innerHTML = `#${entry.round} · ${formatTime(entry.createdAt)} · ${entry.elapsedMs}ms`; + + const line2 = document.createElement('div'); + line2.className = 'history-line'; + line2.innerHTML = `mode=${entry.mode} · parentPostId=${shortId(entry.parentPostId)}`; + + const prompt = document.createElement('div'); + prompt.className = 'history-prompt'; + prompt.textContent = maskPrompt(entry.prompt); + + const actions = document.createElement('div'); + actions.className = 'history-actions'; + + const applyBtn = document.createElement('button'); + applyBtn.type = 'button'; + applyBtn.className = 'geist-button-outline'; + applyBtn.textContent = '设为当前'; + applyBtn.addEventListener('click', () => { + setCurrentFromEntry(entry); + }); + + const copyBtn = document.createElement('button'); + copyBtn.type = 'button'; + copyBtn.className = 'geist-button-outline'; + copyBtn.textContent = '复制ID'; + copyBtn.addEventListener('click', async () => { + try { + if (!entry.parentPostId) { + toast('当前记录没有 parentPostId', 'warning'); + return; + } + const copied = await copyText(entry.parentPostId); + if (!copied) { + throw new Error('copy_failed'); + } + toast('已复制 parentPostId', 'success'); + } catch (e) { + toast('复制失败', 'error'); + } + }); + + actions.appendChild(applyBtn); + actions.appendChild(copyBtn); + + main.appendChild(line1); + main.appendChild(line2); + main.appendChild(prompt); + main.appendChild(actions); + + item.appendChild(thumb); + item.appendChild(main); + historyList.appendChild(item); + }); + } + + function resetCycle(keepSeedPreview = true) { + state.currentParentPostId = ''; + state.currentSourceImageUrl = ''; + state.currentModeValue = 'upload'; + if (parentPostInput) { + parentPostInput.value = ''; + } + updateMeta(); + + if (keepSeedPreview && state.seedImageBase64) { + setPreview(state.seedImageBase64); + } else { + setPreview(''); + } + setStatus('', '未开始'); + } + + function clearHistory() { + state.history = []; + state.editRound = 0; + renderHistory(); + } + + async function parseErrorText(res) { + const text = await res.text(); + if (!text) return `请求失败:HTTP ${res.status}`; + try { + const data = JSON.parse(text); + if (data && typeof data === 'object' && data.detail) { + return String(data.detail); + } + } catch (e) { + // ignore + } + return text; + } + + async function requestWorkbenchEditStream(authHeader, body, onProgress) { + const payload = { + ...body, + stream: true, + }; + const res = await fetch('/v1/public/imagine/workbench/edit', { + method: 'POST', + headers: { + ...buildAuthHeaders(authHeader), + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + throw new Error(await parseErrorText(res)); + } + + const contentType = String(res.headers.get('content-type') || '').toLowerCase(); + if (!contentType.includes('text/event-stream')) { + return await res.json(); + } + + const reader = res.body && res.body.getReader ? res.body.getReader() : null; + if (!reader) { + throw new Error('stream_not_supported'); + } + + const decoder = new TextDecoder('utf-8'); + let buffer = ''; + let finalResult = null; + let finalError = ''; + + function handleChunk(chunkText) { + let eventName = 'message'; + const dataLines = []; + const lines = String(chunkText || '').split('\n'); + for (const lineRaw of lines) { + const line = lineRaw.trim(); + if (!line) continue; + if (line.startsWith('event:')) { + eventName = line.slice(6).trim(); + continue; + } + if (line.startsWith('data:')) { + dataLines.push(line.slice(5).trim()); + } + } + if (!dataLines.length) return; + let eventPayload = null; + try { + eventPayload = JSON.parse(dataLines.join('\n')); + } catch (e) { + return; + } + if (eventName === 'progress') { + if (onProgress && typeof onProgress === 'function') { + onProgress(eventPayload || {}); + } + } else if (eventName === 'result') { + finalResult = eventPayload || {}; + } else if (eventName === 'error') { + finalError = String((eventPayload && eventPayload.message) || 'workbench_edit_failed'); + } + } + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + while (true) { + const idx = buffer.indexOf('\n\n'); + if (idx < 0) break; + const block = buffer.slice(0, idx); + buffer = buffer.slice(idx + 2); + handleChunk(block); + } + } + + if (buffer.trim()) { + handleChunk(buffer); + buffer = ''; + } + + if (finalError) { + throw new Error(finalError); + } + if (finalResult) { + return finalResult; + } + throw new Error('workbench_edit_stream_empty_result'); + } + + async function ensurePublicAuth() { + if (typeof ensurePublicKey !== 'function') { + throw new Error('鉴权脚本未加载'); + } + const authHeader = await ensurePublicKey(); + if (authHeader === null) { + throw new Error('请先登录公开页面'); + } + return authHeader; + } + + async function runEdit() { + if (state.editing) return; + + const prompt = String(editPromptInput ? editPromptInput.value : '').trim(); + if (!prompt) { + toast('请输入编辑提示词', 'warning'); + return; + } + + if (!state.currentParentPostId && !state.seedImageBase64) { + toast('请先上传首图', 'warning'); + return; + } + + let authHeader = ''; + try { + authHeader = await ensurePublicAuth(); + } catch (e) { + toast(String(e.message || e), 'error'); + if (String(e.message || '').includes('登录')) { + window.location.href = '/login'; + } + return; + } + + const body = { + prompt, + }; + + if (state.currentParentPostId) { + body.parent_post_id = state.currentParentPostId; + if (state.currentSourceImageUrl) { + body.source_image_url = state.currentSourceImageUrl; + } + } else { + body.image_base64 = state.seedImageBase64; + } + + setEditing(true); + startEditProgress(); + + try { + const payload = await requestWorkbenchEditStream(authHeader, body, (evt) => { + const next = Number(evt && evt.progress ? evt.progress : 0); + const message = String((evt && evt.message) || '').trim(); + if (Number.isFinite(next) && next > 0) { + const safe = Math.max(editProgressValue, Math.min(99, next)); + setEditProgress(safe, message ? `${message} · ${safe}%` : `编辑中 ${safe}%`); + setStatus('running', message || `编辑中 ${safe}%`); + } else if (message) { + setEditProgress(editProgressValue, message); + setStatus('running', message); + } + }); + const imageUrl = String(payload?.data?.[0]?.url || '').trim(); + if (!imageUrl) { + throw new Error('返回结果缺少图片 URL'); + } + + const previousParentPostId = state.currentParentPostId; + const generatedParent = String( + payload.current_parent_post_id + || payload.generated_parent_post_id + || payload.input_parent_post_id + || extractParentPostId(imageUrl) + ).trim(); + const resolvedParentPostId = generatedParent || previousParentPostId; + + const sourceImageUrl = pickSourceImageUrl( + { + current_source_image_url: payload.current_source_image_url, + source_image_url: payload.source_image_url, + imageUrl, + }, + resolvedParentPostId, + state.currentSourceImageUrl + ); + + const mode = String(payload.mode || (state.currentParentPostId ? 'parent_post' : 'upload')).trim(); + const elapsedMs = Number(payload.elapsed_ms || 0); + + state.currentParentPostId = resolvedParentPostId; + state.currentSourceImageUrl = sourceImageUrl; + state.currentModeValue = mode || 'parent_post'; + if (parentPostInput) { + parentPostInput.value = state.currentParentPostId || ''; + } + setPreview(imageUrl); + updateMeta(); + + state.editRound += 1; + const entry = { + id: `${Date.now()}_${Math.random().toString(16).slice(2, 8)}`, + round: state.editRound, + prompt, + mode: state.currentModeValue, + imageUrl, + parentPostId: resolvedParentPostId, + sourceImageUrl, + elapsedMs: Number.isFinite(elapsedMs) ? Math.max(0, Math.round(elapsedMs)) : 0, + createdAt: Date.now(), + }; + state.history.unshift(entry); + renderHistory(); + rememberParentPost({ + parentPostId: resolvedParentPostId, + sourceImageUrl, + imageUrl, + origin: 'workbench_edit', + }); + + finishEditProgress(true, '编辑完成 100%'); + setStatus('done', `编辑完成 · round #${entry.round}`); + toast('编辑成功,已更新当前画面', 'success'); + } catch (e) { + finishEditProgress(false, '编辑失败'); + setStatus('error', '编辑失败'); + toast(String(e.message || e), 'error'); + } finally { + setEditing(false); + } + } + + function bindEvents() { + if (selectSeedBtn && seedImageInput) { + selectSeedBtn.addEventListener('click', () => { + seedImageInput.click(); + }); + + seedImageInput.addEventListener('change', async (event) => { + const file = event.target && event.target.files ? event.target.files[0] : null; + if (!file) return; + + try { + await applySeedImageFile(file, '本地图片'); + } catch (e) { + toast(String(e.message || e), 'error'); + } finally { + seedImageInput.value = ''; + } + }); + } + + if (submitEditBtn) { + submitEditBtn.addEventListener('click', runEdit); + } + + if (applyParentBtn) { + applyParentBtn.addEventListener('click', () => { + applyParentPostFromText(parentPostInput ? parentPostInput.value : ''); + }); + } + + if (parentPostInput) { + parentPostInput.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + applyParentPostFromText(parentPostInput.value); + } + }); + parentPostInput.addEventListener('input', () => { + const raw = parentPostInput.value.trim(); + if (!raw) return; + applyParentPostFromText(raw, { silent: true }); + }); + parentPostInput.addEventListener('paste', (event) => { + const text = String(event.clipboardData ? event.clipboardData.getData('text') || '' : '').trim(); + if (!text) return; + event.preventDefault(); + parentPostInput.value = text; + applyParentPostFromText(text, { silent: true }); + }); + } + + if (editPromptInput) { + editPromptInput.addEventListener('keydown', (event) => { + if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { + event.preventDefault(); + runEdit(); + } + }); + } + + if (resetCycleBtn) { + resetCycleBtn.addEventListener('click', () => { + resetCycle(true); + toast('已重置编辑链路', 'success'); + }); + } + + if (clearHistoryBtn) { + clearHistoryBtn.addEventListener('click', () => { + clearHistory(); + toast('历史记录已清空', 'success'); + }); + } + + document.addEventListener('paste', async (event) => { + const dataTransfer = event.clipboardData; + const file = pickImageFileFromDataTransfer(dataTransfer); + if (file) { + event.preventDefault(); + try { + await applySeedImageFile(file, '粘贴图片'); + } catch (e) { + toast(String(e.message || e), 'error'); + } + return; + } + const text = dataTransfer ? String(dataTransfer.getData('text') || '').trim() : ''; + if (!text) return; + const target = event.target; + const isTypingInPrompt = target === editPromptInput; + const isTypingInParentInput = target === parentPostInput; + if (isTypingInPrompt) return; + if (!isTypingInParentInput && (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) { + return; + } + const ok = applyParentPostFromText(text, { silent: true }); + if (ok) { + event.preventDefault(); + } + }); + + if (previewShell) { + previewShell.addEventListener('dragenter', (event) => { + if (!hasFiles(event.dataTransfer)) return; + event.preventDefault(); + dragCounter += 1; + setDragActive(true); + }); + + previewShell.addEventListener('dragover', (event) => { + if (!hasFiles(event.dataTransfer)) return; + event.preventDefault(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'copy'; + } + setDragActive(true); + }); + + previewShell.addEventListener('dragleave', (event) => { + if (!hasFiles(event.dataTransfer)) return; + event.preventDefault(); + dragCounter = Math.max(0, dragCounter - 1); + if (dragCounter === 0) { + setDragActive(false); + } + }); + + previewShell.addEventListener('drop', async (event) => { + event.preventDefault(); + dragCounter = 0; + setDragActive(false); + const file = pickImageFileFromDataTransfer(event.dataTransfer); + if (!file) { + toast('未检测到可用图片文件', 'warning'); + return; + } + try { + await applySeedImageFile(file, '拖拽图片'); + } catch (e) { + toast(String(e.message || e), 'error'); + } + }); + } + + window.addEventListener('dragover', (event) => { + if (!hasFiles(event.dataTransfer)) return; + event.preventDefault(); + }); + + window.addEventListener('drop', (event) => { + if (!hasFiles(event.dataTransfer)) return; + if (previewShell && previewShell.contains(event.target)) { + return; + } + event.preventDefault(); + setDragActive(false); + dragCounter = 0; + }); + } + + function mountInlineSubmitButton() { + if (!editPromptInput || !submitEditBtn) return false; + const promptWrap = editPromptInput.closest('.prompt-enhance-wrap'); + if (!promptWrap) return false; + if (!promptWrap.contains(submitEditBtn)) { + const actionRow = submitEditBtn.parentElement; + promptWrap.appendChild(submitEditBtn); + if (actionRow && actionRow.classList && actionRow.classList.contains('action-row')) { + actionRow.classList.add('action-row-inline'); + } + } + submitEditBtn.classList.add('inline-submit-btn'); + return true; + } + + function ensureInlineSubmitButton(attempt = 0) { + if (mountInlineSubmitButton()) return; + if (attempt >= 20) return; + setTimeout(() => ensureInlineSubmitButton(attempt + 1), 50); + } + + function init() { + ensureInlineSubmitButton(); + bindEvents(); + renderHistory(); + resetCycle(false); + updateMeta(); + } + + init(); +})();