diff --git a/app/api/v1/public_api/imagine.py b/app/api/v1/public_api/imagine.py index b234c02..a7ca9ae 100644 --- a/app/api/v1/public_api/imagine.py +++ b/app/api/v1/public_api/imagine.py @@ -1084,6 +1084,7 @@ async def public_imagine_workbench_edit(data: ImagineWorkbenchEditRequest, reque source_image_url=source_image_url, response_format="url", stream=False, + return_all_images=True, progress_cb=progress_cb, ) mode = "parent_post" @@ -1104,6 +1105,7 @@ async def public_imagine_workbench_edit(data: ImagineWorkbenchEditRequest, reque n=1, response_format="url", stream=False, + return_all_images=True, progress_cb=progress_cb, ) mode = "upload" @@ -1112,7 +1114,11 @@ async def public_imagine_workbench_edit(data: ImagineWorkbenchEditRequest, reque if not images: raise HTTPException(status_code=502, detail="Image edit returned no results") - image_url = str(images[0]) + normalized_images = [str(item or "").strip() for item in images if str(item or "").strip()] + if not normalized_images: + raise HTTPException(status_code=502, detail="Image edit returned no results") + + image_url = normalized_images[0] generated_parent_post_id = _extract_parent_post_id_from_url(image_url) current_parent_post_id = generated_parent_post_id or parent_post_id if current_parent_post_id: @@ -1126,7 +1132,7 @@ async def public_imagine_workbench_edit(data: ImagineWorkbenchEditRequest, reque return { "created": int(time.time()), - "data": [{"url": image_url}], + "data": [{"url": url} for url in normalized_images], "mode": mode, "input_parent_post_id": parent_post_id if use_parent_mode else "", "generated_parent_post_id": generated_parent_post_id, diff --git a/app/services/grok/services/image_edit.py b/app/services/grok/services/image_edit.py index 7085fbf..b0c66c6 100644 --- a/app/services/grok/services/image_edit.py +++ b/app/services/grok/services/image_edit.py @@ -135,6 +135,7 @@ class ImageEditService: n: int, response_format: str, stream: bool, + return_all_images: bool = False, progress_cb: Callable[[str, dict], Any] | None = None, ) -> ImageEditResult: max_token_retries = int(get_config("retry.max_retry")) @@ -251,6 +252,7 @@ class ImageEditService: response_format=response_format, tool_overrides=tool_overrides, model_config_override=model_config_override, + return_all_images=return_all_images, progress_cb=progress_cb, ) await self._emit_progress( @@ -310,6 +312,7 @@ class ImageEditService: source_image_url: str, response_format: str, stream: bool, + return_all_images: bool = False, progress_cb: Callable[[str, dict], Any] | None = None, ) -> ImageEditResult: """基于 parentPostId 进行编辑,不上传图片。""" @@ -437,6 +440,7 @@ class ImageEditService: response_format=response_format, tool_overrides=tool_overrides, model_config_override=model_config_override, + return_all_images=return_all_images, progress_cb=progress_cb, ) await self._emit_progress( @@ -567,6 +571,7 @@ class ImageEditService: response_format: str, tool_overrides: dict, model_config_override: dict, + return_all_images: bool = False, progress_cb: Callable[[str, dict], Any] | None = None, ) -> List[str]: async def _call_edit(): @@ -594,6 +599,8 @@ class ImageEditService: raise UpstreamException( "Image edit returned no results", details={"error": "empty_result"} ) + if return_all_images: + return all_images return [all_images[0]] diff --git a/app/static/public/css/imagine_workbench.css b/app/static/public/css/imagine_workbench.css index a9981c3..f97e40e 100644 --- a/app/static/public/css/imagine_workbench.css +++ b/app/static/public/css/imagine_workbench.css @@ -216,17 +216,47 @@ body { box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.22); } -.current-image { +.current-gallery { width: 100%; - display: block; + height: 100%; + min-height: 240px; + display: grid; + grid-template-columns: 1fr; + gap: 8px; + padding: 8px; + box-sizing: border-box; position: relative; z-index: 1; } -.current-image.hidden { +.current-gallery.hidden { display: none; } +.current-gallery[data-count="2"] { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.current-gallery[data-count="3"], +.current-gallery[data-count="4"] { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.current-gallery-item { + border-radius: 10px; + overflow: hidden; + border: 1px solid var(--workbench-border); + background: #0b1220; + min-height: 210px; +} + +.current-gallery-item img { + width: 100%; + height: 100%; + display: block; + object-fit: contain; +} + .preview-empty { color: var(--workbench-muted); font-size: 13px; diff --git a/app/static/public/js/imagine_workbench.js b/app/static/public/js/imagine_workbench.js index cdac10d..ec710f5 100644 --- a/app/static/public/js/imagine_workbench.js +++ b/app/static/public/js/imagine_workbench.js @@ -6,7 +6,7 @@ const parentPostInput = document.getElementById('parentPostInput'); const applyParentBtn = document.getElementById('applyParentBtn'); const previewShell = document.getElementById('previewShell'); - const currentImage = document.getElementById('currentImage'); + const currentGallery = document.getElementById('currentGallery'); const previewEmpty = document.getElementById('previewEmpty'); const currentParentId = document.getElementById('currentParentId'); const currentMode = document.getElementById('currentMode'); @@ -27,6 +27,7 @@ editing: false, referenceImages: [], currentImageUrl: '', + currentImageUrls: [], currentParentPostId: '', currentSourceImageUrl: '', currentModeValue: 'upload', @@ -587,21 +588,47 @@ updateReferenceSummary(); } - function setPreview(url) { - const displayUrl = String(url || '').trim(); - state.currentImageUrl = displayUrl; - if (!currentImage || !previewEmpty) return; - if (!displayUrl) { - currentImage.src = ''; - currentImage.classList.add('hidden'); + function setPreviewImages(urls) { + const list = (Array.isArray(urls) ? urls : []) + .map((item) => String(item || '').trim()) + .filter(Boolean); + state.currentImageUrls = list; + state.currentImageUrl = list[0] || ''; + + if (!currentGallery || !previewEmpty) return; + currentGallery.innerHTML = ''; + const primaryUrl = list[0] || ''; + if (!primaryUrl) { + currentGallery.dataset.count = '0'; + currentGallery.classList.add('hidden'); previewEmpty.classList.remove('hidden'); return; } - currentImage.src = displayUrl; - currentImage.classList.remove('hidden'); + + currentGallery.dataset.count = '1'; + const item = document.createElement('div'); + item.className = 'current-gallery-item'; + const img = document.createElement('img'); + img.src = primaryUrl; + img.alt = 'result-current'; + img.loading = 'lazy'; + img.decoding = 'async'; + item.appendChild(img); + currentGallery.appendChild(item); + + currentGallery.classList.remove('hidden'); previewEmpty.classList.add('hidden'); } + function setPreview(url) { + const displayUrl = String(url || '').trim(); + if (!displayUrl) { + setPreviewImages([]); + return; + } + setPreviewImages([displayUrl]); + } + function setEditing(loading) { state.editing = Boolean(loading); if (submitEditBtn) { @@ -677,7 +704,10 @@ const line1 = document.createElement('div'); line1.className = 'history-line'; - line1.innerHTML = `#${entry.round} 路 ${formatTime(entry.createdAt)} 路 ${entry.elapsedMs}ms`; + const roundLabel = entry.roundTotal && entry.roundTotal > 1 + ? `#${entry.round}-${entry.roundIndex || 1}` + : `#${entry.round}`; + line1.innerHTML = `${roundLabel} 路 ${formatTime(entry.createdAt)} 路 ${entry.elapsedMs}ms`; const line2 = document.createElement('div'); line2.className = 'history-line'; @@ -952,7 +982,12 @@ setStatus('running', message); } }, workbenchEditAbortController ? workbenchEditAbortController.signal : undefined); - const imageUrl = String(payload?.data?.[0]?.url || '').trim(); + const imageUrls = Array.isArray(payload?.data) + ? payload.data + .map((item) => String((item && item.url) || '').trim()) + .filter(Boolean) + : []; + const imageUrl = imageUrls[0] || ''; if (!imageUrl) { throw new Error('返回结果缺少图片 URL'); } @@ -985,32 +1020,41 @@ if (parentPostInput) { parentPostInput.value = state.currentParentPostId || ''; } - setPreview(imageUrl); + setPreviewImages(imageUrls); 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); + const createdAt = Date.now(); + const historyEntries = imageUrls.map((url, index) => { + const perParentPostId = String(extractParentPostId(url) || resolvedParentPostId || '').trim(); + return { + id: `${createdAt}_${index}_${Math.random().toString(16).slice(2, 8)}`, + round: state.editRound, + roundIndex: index + 1, + roundTotal: imageUrls.length, + prompt, + mode: state.currentModeValue, + imageUrl: url, + imageUrls, + parentPostId: perParentPostId, + sourceImageUrl: String(url || sourceImageUrl || '').trim(), + elapsedMs: Number.isFinite(elapsedMs) ? Math.max(0, Math.round(elapsedMs)) : 0, + createdAt, + }; + }); + state.history = [...historyEntries, ...state.history]; renderHistory(); - rememberParentPost({ - parentPostId: resolvedParentPostId, - sourceImageUrl, - imageUrl, - origin: 'workbench_edit', + historyEntries.forEach((entry) => { + rememberParentPost({ + parentPostId: entry.parentPostId || resolvedParentPostId, + sourceImageUrl: entry.sourceImageUrl || sourceImageUrl, + imageUrl: entry.imageUrl || imageUrl, + origin: 'workbench_edit', + }); }); finishEditProgress(true, '编辑完成 100%'); - setStatus('done', `编辑完成 · round #${entry.round}`); + setStatus('done', `编辑完成 · round #${state.editRound}(${imageUrls.length} 张)`); toast('编辑成功,已更新当前画面', 'success'); } catch (e) { if (e && e.name === 'AbortError') { diff --git a/app/static/public/pages/imagine_workbench.html b/app/static/public/pages/imagine_workbench.html index c8390b7..4ffef3f 100644 --- a/app/static/public/pages/imagine_workbench.html +++ b/app/static/public/pages/imagine_workbench.html @@ -63,7 +63,7 @@