feat: imagine-workbench 支持多图参考并完善编辑回退
This commit is contained in:
1 parent
703cb7e340
commit
537514f78c
5 files changed
+417
-80
No files matched your search
@@ -234,6 +234,31 @@ def _normalize_image_input(image_base64: str, image_url: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _normalize_image_references(image_references: Optional[List[str]]) -> List[str]:
|
||||
refs = image_references or []
|
||||
if not isinstance(refs, list):
|
||||
raise HTTPException(status_code=400, detail="image_references must be a list")
|
||||
|
||||
cleaned: List[str] = []
|
||||
for raw in refs:
|
||||
item = str(raw or "").strip()
|
||||
if not item:
|
||||
continue
|
||||
cleaned.append(item)
|
||||
|
||||
if len(cleaned) > 3:
|
||||
raise HTTPException(status_code=400, detail="image_references supports at most 3 images")
|
||||
|
||||
normalized: List[str] = []
|
||||
for item in cleaned:
|
||||
if item.startswith("data:"):
|
||||
normalized.append(_normalize_image_input(image_base64=item, image_url=""))
|
||||
else:
|
||||
normalized.append(_normalize_image_input(image_base64="", image_url=item))
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
async def _clean_sessions(now: float) -> None:
|
||||
expired = [
|
||||
key
|
||||
@@ -992,6 +1017,7 @@ class ImagineWorkbenchEditRequest(BaseModel):
|
||||
source_image_url: Optional[str] = None
|
||||
image_base64: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
image_references: Optional[List[str]] = None
|
||||
stream: Optional[bool] = False
|
||||
|
||||
|
||||
@@ -1062,16 +1088,19 @@ async def public_imagine_workbench_edit(data: ImagineWorkbenchEditRequest, reque
|
||||
)
|
||||
mode = "parent_post"
|
||||
else:
|
||||
image_input = _normalize_image_input(
|
||||
image_base64=str(data.image_base64 or ""),
|
||||
image_url=str(data.image_url or ""),
|
||||
)
|
||||
image_inputs = _normalize_image_references(data.image_references)
|
||||
if not image_inputs:
|
||||
image_input = _normalize_image_input(
|
||||
image_base64=str(data.image_base64 or ""),
|
||||
image_url=str(data.image_url or ""),
|
||||
)
|
||||
image_inputs = [image_input]
|
||||
result = await edit_service.edit(
|
||||
token_mgr=token_mgr,
|
||||
token=token,
|
||||
model_info=model_info,
|
||||
prompt=prompt,
|
||||
images=[image_input],
|
||||
images=image_inputs,
|
||||
n=1,
|
||||
response_format="url",
|
||||
stream=False,
|
||||
|
||||
@@ -89,6 +89,18 @@ def _is_upload_network_error(exc: Exception) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _normalize_fallback_image_url(url: str) -> str:
|
||||
"""下载失败时的兜底 URL 规范化。"""
|
||||
raw = str(url or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
if raw.startswith("http://") or raw.startswith("https://"):
|
||||
return raw
|
||||
if raw.startswith("/"):
|
||||
return f"https://assets.grok.com{raw}"
|
||||
return f"https://assets.grok.com/{raw}"
|
||||
|
||||
|
||||
class ImageEditService:
|
||||
"""Image edit orchestration service."""
|
||||
|
||||
@@ -652,7 +664,14 @@ class ImageStreamProcessor(BaseProcessor):
|
||||
if urls := _collect_images(mr):
|
||||
for url in urls:
|
||||
if self.response_format == "url":
|
||||
processed = await self.process_url(url, "image")
|
||||
try:
|
||||
processed = await self.process_url(url, "image")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Image stream URL resolve failed, fallback to raw URL: "
|
||||
f"error={e}"
|
||||
)
|
||||
processed = _normalize_fallback_image_url(url)
|
||||
if processed:
|
||||
final_images.append(processed)
|
||||
continue
|
||||
@@ -797,7 +816,14 @@ class ImageCollectProcessor(BaseProcessor):
|
||||
if urls := _collect_images(mr):
|
||||
for url in urls:
|
||||
if self.response_format == "url":
|
||||
processed = await self.process_url(url, "image")
|
||||
try:
|
||||
processed = await self.process_url(url, "image")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Image collect URL resolve failed, fallback to raw URL: "
|
||||
f"error={e}"
|
||||
)
|
||||
processed = _normalize_fallback_image_url(url)
|
||||
if processed:
|
||||
images.append(processed)
|
||||
progress = min(90, 64 + len(images) * 12)
|
||||
|
||||
@@ -91,6 +91,89 @@ body {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.reference-strip {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.reference-empty {
|
||||
font-size: 12px;
|
||||
color: var(--workbench-muted);
|
||||
}
|
||||
|
||||
.reference-item {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--workbench-border);
|
||||
background: #f3f4f6;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reference-item.is-primary {
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.35);
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
|
||||
.reference-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.reference-primary-badge {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
padding: 3px 5px;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.78);
|
||||
color: #fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.reference-remove-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.78);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reference-add-slot {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 12px;
|
||||
border: 1px dashed var(--workbench-border);
|
||||
background: #fff;
|
||||
color: var(--workbench-muted);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.parent-quick-row {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -110,7 +193,7 @@ body {
|
||||
.file-name {
|
||||
font-size: 12px;
|
||||
color: var(--workbench-muted);
|
||||
word-break: break-all;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.preview-shell {
|
||||
@@ -428,6 +511,17 @@ html[data-theme='dark'] .history-item {
|
||||
border-color: #324154;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .reference-item {
|
||||
background: #111821;
|
||||
border-color: #324154;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .reference-add-slot {
|
||||
background: #111821;
|
||||
border-color: #324154;
|
||||
color: #b7c4d4;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .history-line {
|
||||
color: #b7c4d4;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const seedImageInput = document.getElementById('seedImageInput');
|
||||
const selectSeedBtn = document.getElementById('selectSeedBtn');
|
||||
const seedFileName = document.getElementById('seedFileName');
|
||||
const referenceStrip = document.getElementById('referenceStrip');
|
||||
const parentPostInput = document.getElementById('parentPostInput');
|
||||
const applyParentBtn = document.getElementById('applyParentBtn');
|
||||
const previewShell = document.getElementById('previewShell');
|
||||
@@ -20,11 +21,11 @@
|
||||
const editProgressWrap = document.getElementById('editProgressWrap');
|
||||
const editProgressBar = document.getElementById('editProgressBar');
|
||||
const editProgressText = document.getElementById('editProgressText');
|
||||
const REFERENCE_LIMIT = 3;
|
||||
|
||||
const state = {
|
||||
editing: false,
|
||||
seedImageBase64: '',
|
||||
seedFileName: '',
|
||||
referenceImages: [],
|
||||
currentImageUrl: '',
|
||||
currentParentPostId: '',
|
||||
currentSourceImageUrl: '',
|
||||
@@ -317,29 +318,214 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function applySeedImageFile(file, source) {
|
||||
if (!file) return;
|
||||
if (state.editing) {
|
||||
toast('编辑进行中,暂时不能替换首图', 'warning');
|
||||
function buildRefId() {
|
||||
return `ref_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function getPrimaryReference() {
|
||||
if (!state.referenceImages.length) return null;
|
||||
return state.referenceImages.find((item) => item.isPrimary) || state.referenceImages[0];
|
||||
}
|
||||
|
||||
function normalizeReferenceOrder() {
|
||||
if (!state.referenceImages.length) return;
|
||||
const primary = getPrimaryReference();
|
||||
const primaryId = primary ? primary.id : '';
|
||||
state.referenceImages = state.referenceImages.map((item) => ({
|
||||
...item,
|
||||
isPrimary: item.id === primaryId,
|
||||
}));
|
||||
state.referenceImages.sort((a, b) => {
|
||||
if (a.isPrimary && !b.isPrimary) return -1;
|
||||
if (!a.isPrimary && b.isPrimary) return 1;
|
||||
return a.createdAt - b.createdAt;
|
||||
});
|
||||
}
|
||||
|
||||
function updateReferenceSummary() {
|
||||
if (!seedFileName) return;
|
||||
seedFileName.textContent = `已添加 ${state.referenceImages.length}/${REFERENCE_LIMIT} 张`;
|
||||
}
|
||||
|
||||
function renderReferenceStrip() {
|
||||
if (!referenceStrip) return;
|
||||
referenceStrip.innerHTML = '';
|
||||
if (!state.referenceImages.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'reference-empty';
|
||||
empty.textContent = '可上传 / 粘贴 / 拖拽参考图(最多 3 张)';
|
||||
referenceStrip.appendChild(empty);
|
||||
updateReferenceSummary();
|
||||
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');
|
||||
state.referenceImages.forEach((item) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = `reference-item${item.isPrimary ? ' is-primary' : ''}`;
|
||||
card.title = item.isPrimary ? `主图:${item.name}` : `设为主图:${item.name}`;
|
||||
card.dataset.id = item.id;
|
||||
card.setAttribute('role', 'button');
|
||||
card.setAttribute('tabindex', '0');
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.className = 'reference-thumb';
|
||||
img.src = item.data;
|
||||
img.alt = item.name || 'reference';
|
||||
img.loading = 'lazy';
|
||||
card.appendChild(img);
|
||||
|
||||
if (item.isPrimary) {
|
||||
const badge = document.createElement('div');
|
||||
badge.className = 'reference-primary-badge';
|
||||
badge.textContent = '主图';
|
||||
card.appendChild(badge);
|
||||
}
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'reference-remove-btn';
|
||||
removeBtn.textContent = '×';
|
||||
removeBtn.title = '删除';
|
||||
removeBtn.dataset.id = item.id;
|
||||
removeBtn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
removeReferenceImage(item.id);
|
||||
});
|
||||
card.appendChild(removeBtn);
|
||||
|
||||
card.addEventListener('click', () => {
|
||||
setPrimaryReference(item.id);
|
||||
});
|
||||
card.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
setPrimaryReference(item.id);
|
||||
}
|
||||
});
|
||||
referenceStrip.appendChild(card);
|
||||
});
|
||||
|
||||
if (state.referenceImages.length < REFERENCE_LIMIT && seedImageInput) {
|
||||
const addSlot = document.createElement('button');
|
||||
addSlot.type = 'button';
|
||||
addSlot.className = 'reference-add-slot';
|
||||
addSlot.title = '继续添加';
|
||||
addSlot.textContent = '+';
|
||||
addSlot.addEventListener('click', () => seedImageInput.click());
|
||||
referenceStrip.appendChild(addSlot);
|
||||
}
|
||||
updateReferenceSummary();
|
||||
}
|
||||
|
||||
function setPrimaryReference(id) {
|
||||
if (!id) return;
|
||||
let changed = false;
|
||||
let primaryData = '';
|
||||
state.referenceImages = state.referenceImages.map((item) => {
|
||||
const isPrimary = item.id === id;
|
||||
if (isPrimary) {
|
||||
primaryData = String(item.data || '');
|
||||
}
|
||||
const next = { ...item, isPrimary };
|
||||
if (next.isPrimary !== item.isPrimary) changed = true;
|
||||
return next;
|
||||
});
|
||||
if (!changed) return;
|
||||
normalizeReferenceOrder();
|
||||
renderReferenceStrip();
|
||||
if (!state.currentParentPostId && primaryData) {
|
||||
setPreview(primaryData);
|
||||
setStatus('', '已切换主图');
|
||||
}
|
||||
}
|
||||
|
||||
function removeReferenceImage(id) {
|
||||
const before = state.referenceImages.length;
|
||||
state.referenceImages = state.referenceImages.filter((item) => item.id !== id);
|
||||
if (state.referenceImages.length === before) return;
|
||||
normalizeReferenceOrder();
|
||||
renderReferenceStrip();
|
||||
if (!state.referenceImages.length) {
|
||||
setStatus('', '参考图已清空');
|
||||
}
|
||||
}
|
||||
|
||||
function clearReferenceImages() {
|
||||
state.referenceImages = [];
|
||||
if (seedImageInput) seedImageInput.value = '';
|
||||
renderReferenceStrip();
|
||||
}
|
||||
|
||||
function pickImageFilesFromDataTransfer(dataTransfer) {
|
||||
if (!dataTransfer) return [];
|
||||
const files = [];
|
||||
const pushIfImage = (file) => {
|
||||
if (!file) return;
|
||||
if (!String(file.type || '').startsWith('image/')) return;
|
||||
files.push(file);
|
||||
};
|
||||
if (dataTransfer.files && dataTransfer.files.length) {
|
||||
Array.from(dataTransfer.files).forEach(pushIfImage);
|
||||
}
|
||||
if (!files.length && dataTransfer.items && dataTransfer.items.length) {
|
||||
Array.from(dataTransfer.items).forEach((item) => {
|
||||
if (!item || item.kind !== 'file') return;
|
||||
const file = item.getAsFile ? item.getAsFile() : null;
|
||||
pushIfImage(file);
|
||||
});
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function addReferenceFiles(files, source) {
|
||||
if (!Array.isArray(files) || !files.length) return 0;
|
||||
if (state.editing) {
|
||||
toast('编辑进行中,暂时不能修改参考图', 'warning');
|
||||
return 0;
|
||||
}
|
||||
|
||||
const slotsLeft = Math.max(0, REFERENCE_LIMIT - state.referenceImages.length);
|
||||
if (slotsLeft <= 0) {
|
||||
toast(`最多支持 ${REFERENCE_LIMIT} 张参考图`, 'warning');
|
||||
return 0;
|
||||
}
|
||||
const targets = files.slice(0, slotsLeft);
|
||||
const ignoredCount = Math.max(0, files.length - targets.length);
|
||||
|
||||
let added = 0;
|
||||
for (const file of targets) {
|
||||
const mimeType = String(file.type || '');
|
||||
if (mimeType && !mimeType.startsWith('image/')) continue;
|
||||
const dataUrl = await readFileAsDataUrl(file);
|
||||
if (!dataUrl.startsWith('data:image/')) continue;
|
||||
state.referenceImages.push({
|
||||
id: buildRefId(),
|
||||
name: file.name || source || '未命名图片',
|
||||
mime: file.type || '',
|
||||
data: dataUrl,
|
||||
source: source || 'upload',
|
||||
isPrimary: false,
|
||||
createdAt: Date.now() + added,
|
||||
});
|
||||
added += 1;
|
||||
}
|
||||
if (added > 0 && !state.referenceImages.some((item) => item.isPrimary)) {
|
||||
state.referenceImages[0].isPrimary = true;
|
||||
}
|
||||
normalizeReferenceOrder();
|
||||
renderReferenceStrip();
|
||||
|
||||
if (added > 0) {
|
||||
clearHistory();
|
||||
resetCycle(false);
|
||||
setStatus('', '已载入参考图,可开始编辑');
|
||||
}
|
||||
|
||||
if (ignoredCount > 0) {
|
||||
toast(`最多支持 ${REFERENCE_LIMIT} 张参考图,已忽略超出部分`, 'warning');
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
function setDragActive(active) {
|
||||
@@ -347,29 +533,6 @@
|
||||
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;
|
||||
@@ -385,9 +548,7 @@
|
||||
if (currentMode) {
|
||||
currentMode.textContent = state.currentModeValue || '-';
|
||||
}
|
||||
if (seedFileName) {
|
||||
seedFileName.textContent = state.seedFileName || '未选择文件';
|
||||
}
|
||||
updateReferenceSummary();
|
||||
}
|
||||
|
||||
function setPreview(url) {
|
||||
@@ -544,8 +705,9 @@
|
||||
}
|
||||
updateMeta();
|
||||
|
||||
if (keepSeedPreview && state.seedImageBase64) {
|
||||
setPreview(state.seedImageBase64);
|
||||
const primary = getPrimaryReference();
|
||||
if (keepSeedPreview && primary && primary.data) {
|
||||
setPreview(primary.data);
|
||||
} else {
|
||||
setPreview('');
|
||||
}
|
||||
@@ -691,8 +853,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.currentParentPostId && !state.seedImageBase64) {
|
||||
toast('请先上传首图', 'warning');
|
||||
if (!state.currentParentPostId && !state.referenceImages.length) {
|
||||
toast('请先添加参考图', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -711,13 +873,28 @@
|
||||
prompt,
|
||||
};
|
||||
|
||||
const references = state.referenceImages
|
||||
.slice(0, REFERENCE_LIMIT)
|
||||
.map((item) => String(item.data || '').trim())
|
||||
.filter(Boolean);
|
||||
if (references.length) {
|
||||
body.image_references = references;
|
||||
const firstRef = references[0];
|
||||
if (firstRef.startsWith('data:image/')) {
|
||||
body.image_base64 = firstRef;
|
||||
} else {
|
||||
body.image_url = firstRef;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.currentParentPostId) {
|
||||
body.parent_post_id = state.currentParentPostId;
|
||||
if (state.currentSourceImageUrl) {
|
||||
body.source_image_url = state.currentSourceImageUrl;
|
||||
}
|
||||
} else {
|
||||
body.image_base64 = state.seedImageBase64;
|
||||
} else if (!references.length) {
|
||||
toast('请先添加参考图', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
setEditing(true);
|
||||
@@ -822,11 +999,14 @@
|
||||
});
|
||||
|
||||
seedImageInput.addEventListener('change', async (event) => {
|
||||
const file = event.target && event.target.files ? event.target.files[0] : null;
|
||||
if (!file) return;
|
||||
const files = event.target && event.target.files ? Array.from(event.target.files) : [];
|
||||
if (!files.length) return;
|
||||
|
||||
try {
|
||||
await applySeedImageFile(file, '本地图像');
|
||||
const added = await addReferenceFiles(files, 'upload');
|
||||
if (added > 0) {
|
||||
toast(`已添加 ${added} 张参考图`, 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
toast(String(e.message || e), 'error');
|
||||
} finally {
|
||||
@@ -891,11 +1071,14 @@
|
||||
|
||||
document.addEventListener('paste', async (event) => {
|
||||
const dataTransfer = event.clipboardData;
|
||||
const file = pickImageFileFromDataTransfer(dataTransfer);
|
||||
if (file) {
|
||||
const files = pickImageFilesFromDataTransfer(dataTransfer);
|
||||
if (files.length) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await applySeedImageFile(file, '粘贴图片');
|
||||
const added = await addReferenceFiles(files, 'paste');
|
||||
if (added > 0) {
|
||||
toast(`已粘贴 ${added} 张参考图`, 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
toast(String(e.message || e), 'error');
|
||||
}
|
||||
@@ -946,13 +1129,16 @@
|
||||
event.preventDefault();
|
||||
dragCounter = 0;
|
||||
setDragActive(false);
|
||||
const file = pickImageFileFromDataTransfer(event.dataTransfer);
|
||||
if (!file) {
|
||||
const files = pickImageFilesFromDataTransfer(event.dataTransfer);
|
||||
if (!files.length) {
|
||||
toast('未检测到可用图片文件', 'warning');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await applySeedImageFile(file, '拖拽图片');
|
||||
const added = await addReferenceFiles(files, 'drop');
|
||||
if (added > 0) {
|
||||
toast(`已拖入 ${added} 张参考图`, 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
toast(String(e.message || e), 'error');
|
||||
}
|
||||
@@ -999,6 +1185,7 @@
|
||||
function init() {
|
||||
ensureInlineSubmitButton();
|
||||
bindEvents();
|
||||
renderReferenceStrip();
|
||||
renderHistory();
|
||||
resetCycle(false);
|
||||
updateMeta();
|
||||
@@ -1006,6 +1193,3 @@
|
||||
|
||||
init();
|
||||
})();
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold tracking-tight">图片编辑工作台</h2>
|
||||
<p class="text-[var(--accents-4)] mt-1 text-sm">
|
||||
首次上传图片后开始编辑链路。之后每次编辑都会沿用上一次输出的 parentPostId 循环编辑。
|
||||
支持上传/粘贴/拖拽最多 3 张参考图进行编辑;编辑后沿用上一次输出的 parentPostId 继续链路。
|
||||
</p>
|
||||
</div>
|
||||
<div id="editStatusText" class="status-chip">未开始</div>
|
||||
@@ -45,9 +45,13 @@
|
||||
<div class="panel-title">当前画面</div>
|
||||
|
||||
<div class="upload-row">
|
||||
<input id="seedImageInput" class="hidden" type="file" accept="image/png,image/jpeg,image/webp,image/jpg">
|
||||
<button id="selectSeedBtn" type="button" class="geist-button-outline">选择首图</button>
|
||||
<span id="seedFileName" class="file-name">未选择文件</span>
|
||||
<input id="seedImageInput" class="hidden" type="file" accept="image/*" multiple>
|
||||
<button id="selectSeedBtn" type="button" class="geist-button-outline">添加参考图</button>
|
||||
<span id="seedFileName" class="file-name">已添加 0/3 张</span>
|
||||
</div>
|
||||
|
||||
<div id="referenceStrip" class="reference-strip">
|
||||
<div class="reference-empty">可上传 / 粘贴 / 拖拽参考图(最多 3 张)</div>
|
||||
</div>
|
||||
|
||||
<div class="parent-quick-row">
|
||||
@@ -60,7 +64,7 @@
|
||||
|
||||
<div id="previewShell" class="preview-shell">
|
||||
<img id="currentImage" class="current-image hidden" alt="current-image">
|
||||
<div id="previewEmpty" class="preview-empty">请先上传首图</div>
|
||||
<div id="previewEmpty" class="preview-empty">请先添加参考图</div>
|
||||
<div id="dropHint" class="drop-hint">支持拖拽图片到此处,或按 Ctrl+V 粘贴图片</div>
|
||||
</div>
|
||||
|
||||
@@ -107,7 +111,7 @@
|
||||
<div class="mobile-fab" data-mobile-fab>
|
||||
<div class="mobile-fab-panel">
|
||||
<div class="mobile-fab-actions">
|
||||
<button class="geist-button-outline" data-proxy-click="#selectSeedBtn" type="button">选择首图</button>
|
||||
<button class="geist-button-outline" data-proxy-click="#selectSeedBtn" type="button">添加参考图</button>
|
||||
<button class="geist-button-outline" data-proxy-click="#applyParentBtn" type="button">使用ID</button>
|
||||
<button class="geist-button" data-proxy-click="#submitEditBtn" type="button">执行编辑</button>
|
||||
<button class="geist-button-outline" data-proxy-click="#resetCycleBtn" type="button">重置链路</button>
|
||||
|
||||
Reference in new issue
Block a user