diff --git a/components/canvas/AiWashWorkspace.tsx b/components/canvas/AiWashWorkspace.tsx index 17c1d8557..529bf4286 100644 --- a/components/canvas/AiWashWorkspace.tsx +++ b/components/canvas/AiWashWorkspace.tsx @@ -1,6 +1,7 @@ "use client"; import { + ChevronDown, Clock3, History as HistoryIcon, ImageIcon, @@ -304,6 +305,22 @@ function getControlLevelByIndex( return control.levels[clampedIndex] || control.levels[0] || null; } +function getAiWashStatusLabel(status?: AiWashResultStatus) { + switch (status) { + case "submitting": + return "提交中"; + case "running": + return "生成中"; + case "succeeded": + return "已完成"; + case "failed": + return "失败"; + case "idle": + default: + return "待生成"; + } +} + function composePresetPrompt(params: { preset: AiWashPreset; selectedLevelIndexByControl: Record; @@ -392,6 +409,7 @@ export function AiWashWorkspace({ useState>({}); const [historySourceOverride, setHistorySourceOverride] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); + const [isHistoryExpanded, setIsHistoryExpanded] = useState(false); const [lastErrorMessage, setLastErrorMessage] = useState(""); const [comparePositionById, setComparePositionById] = useState< Record @@ -449,12 +467,18 @@ export function AiWashWorkspace({ () => createHistoryScopeKey(resolvedHistoryScopeInput), [resolvedHistoryScopeInput] ); + const selectedPreviewResult = useMemo( + () => + results.find((item) => item.presetId === selectedResultId) || + results.find((item) => item.isDefault) || + results[0] || + null, + [results, selectedResultId] + ); const selectedResult = useMemo( () => - results.find( - (item) => item.presetId === selectedResultId && item.status === "succeeded" - ) || null, - [results, selectedResultId] + selectedPreviewResult?.status === "succeeded" ? selectedPreviewResult : null, + [selectedPreviewResult] ); const visibleHistoryBatches = useMemo(() => { const scoped = persistedHistoryBatches @@ -905,6 +929,11 @@ export function AiWashWorkspace({ [persistHistoryBatches] ); + const previewComparePosition = selectedPreviewResult + ? comparePositionById[selectedPreviewResult.presetId] ?? 50 + : 50; + const previewHasImage = Boolean(selectedPreviewResult?.imageUrl); + return (
@@ -1036,30 +1065,71 @@ export function AiWashWorkspace({
- {presetExecutionStates.map((item) => ( -
-
-
- {item.preset.title} + {presetExecutionStates.map((item) => { + const targetResult = results.find( + (result) => result.presetId === item.preset.id + ); + const isActive = + selectedResultId === item.preset.id || + (!selectedResultId && + selectedPreviewResult?.presetId === item.preset.id); + return ( +
-
- - {item.resolvedModel?.displayName || - item.resolvedModel?.modelId || - (item.usesUserSelectableModel - ? "前台自选模型" - : "待配置模型")} - -
-
- ))} +
+ + {item.resolvedModel?.displayName || + item.resolvedModel?.modelId || + (item.usesUserSelectableModel + ? "前台自选模型" + : "待配置模型")} + + + {item.unitCost > 0 ? `${item.unitCost} 积分/张` : "未计费"} + +
+ + ); + })}
@@ -1178,177 +1248,179 @@ export function AiWashWorkspace({
-
- {results.length > 0 ? ( - results.map((result) => { - const hasImage = Boolean(result.imageUrl); - const comparePosition = comparePositionById[result.presetId] ?? 50; - const isSelected = selectedResultId === result.presetId && hasImage; - - return ( - - ); - }) - ) : ( -
- -
AI洗图准备就绪
-
- 上传底图后,系统会按管理员配置好的多个模式同时生成洗图结果。 +
+
+
+
+
+ {selectedPreviewResult?.title || "AI洗图结果预览"} +
+ + {getAiWashStatusLabel(selectedPreviewResult?.status)} + +
+
+ {selectedPreviewResult?.description || + "左侧选择渲染目标后,所有洗图结果都会共用这里的大尺寸预览区。"}
- )} -
+
+ {selectedPreviewResult?.modelId ? ( + + {selectedPreviewResult.modelId} + + ) : null} + {selectedPreviewResult?.createdAt ? ( + + {new Date(selectedPreviewResult.createdAt).toLocaleTimeString( + "zh-CN", + { + hour: "2-digit", + minute: "2-digit", + } + )} + + ) : null} +
+
+ +
+ {selectedPreviewResult ? ( + previewHasImage ? ( + <> + {selectedPreviewResult.title} + {compareSourceUrl ? ( + <> +
+ {`${selectedPreviewResult.title} +
+
+
+ finishCompareDrag(selectedPreviewResult.presetId, event) + } + onPointerDown={(event) => + handleComparePointerDown( + selectedPreviewResult.presetId, + event + ) + } + onPointerMove={(event) => + handleComparePointerMove( + selectedPreviewResult.presetId, + event + ) + } + onPointerUp={(event) => + finishCompareDrag(selectedPreviewResult.presetId, event) + } + > +
+
+
+ +
+
+
+
+ 原图 +
+
+ 洗后 +
+ + ) : null} + + ) : ( +
+
+ {selectedPreviewResult.status === "running" || + selectedPreviewResult.status === "submitting" ? ( + <> + +
+ {selectedPreviewResult.status === "submitting" + ? "正在提交模型任务" + : "模型正在生成结果"} +
+
+ 生成完成后,这里会自动切换为大图对比预览。 +
+ + ) : selectedPreviewResult.status === "failed" ? ( + <> +
+ 该目标生成失败 +
+
+ {selectedPreviewResult.error || "请调整配置后重新生成。"} +
+ + ) : ( + <> + +
+ 固定单图预览区 +
+
+ 先上传底图并点击开始 AI洗图,结果会在这里完整展示。 +
+ + )} +
+
+ ) + ) : ( +
+
+ +
+ AI洗图准备就绪 +
+
+ 管理员配置预设后,这里会展示共享结果预览。 +
+
+
+ )} +
+
{selectedResult ? `已选中 ${selectedResult.title},可直接发送回画布` - : "请选择或生成一张洗图结果"} + : selectedPreviewResult + ? `当前预览 ${selectedPreviewResult.title},生成完成后可发送回画布` + : "请选择或生成一张洗图结果"}
-
-
+
+
-
- 最近 {visibleHistoryBatches.length} 条 +
+
+ 最近 {visibleHistoryBatches.length} 条 +
+
+ {isHistoryExpanded ? "收起" : "展开"} + +
-
+ - {visibleHistoryBatches.length > 0 ? ( -
+ {isHistoryExpanded ? ( + visibleHistoryBatches.length > 0 ? ( +
{visibleHistoryBatches.map((entry) => { const cover = entry.items[0]; return ( @@ -1457,12 +1545,13 @@ export function AiWashWorkspace({
); })} -
- ) : ( -
- 还没有历史洗图记录,生成一次后会自动出现在这里。 -
- )} +
+ ) : ( +
+ 还没有历史洗图记录,生成一次后会自动出现在这里。 +
+ ) + ) : null}