From 583145c97cfa37221bbb4d52a8f5daee98e815dd Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 23 Mar 2026 18:38:16 +0800 Subject: [PATCH] fix: improve detail replacement result sync --- app/(chat)/api/chat/route.ts | 24 +++- .../AgentDetailReferenceStitchDialog.tsx | 113 +++++++++++++++--- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 48 +++++++- 3 files changed, 158 insertions(+), 27 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 1ffd3f424..91ca2b178 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -3079,6 +3079,7 @@ function buildAgentReplacementPrompt(params: { referenceIndex: number; totalReferences: number; hasModelImage: boolean; + productImageCount: number; pairedProductIndex?: number; }) { const instructionLines = @@ -3086,8 +3087,13 @@ function buildAgentReplacementPrompt(params: { ? [ "【执行模式】详情替换", `当前要处理第 ${params.referenceIndex + 1} / ${params.totalReferences} 张详情参考图:${params.referenceLabel}`, + "图像输入顺序规则必须严格遵守:前面的图片是待替换进去的商品输入图,如有模特图会出现在更前面的输入图里;最后一张图片才是当前详情参考图。", + params.productImageCount > 1 + ? `前面的 ${params.productImageCount} 张商品输入图都属于同一产品的多角度/多细节白底图,请综合这些图片理解产品结构、材质、颜色、比例、边角、开孔、五金与细节,不可只参考第一张。` + : "前面的商品输入图就是当前要替换进去的产品白底图,请准确保持该产品的结构、材质、颜色、比例和细节。", "请严格保留最后一张参考图的版式结构、镜头关系、构图、光影氛围、留白位置和信息层级,只替换其中的商品主体。", - "前面的输入商品图用于提供商品主体、材质、颜色和细节特征;如有模特图,请在需要人物展示时优先参考模特图。", + "前面的输入商品图用于提供商品主体、材质、颜色和细节特征;如有模特图,请只在参考图本身包含人物展示时再参考模特图。", + "绝对不要把最后一张参考图原样返回,也不要忽略前面的商品输入图重新生成无关场景。", "不要改动参考图的整体布局,不要新增无关主体,不要删除关键版式元素。", ] : [ @@ -3096,6 +3102,10 @@ function buildAgentReplacementPrompt(params: { params.pairedProductIndex !== undefined ? `请优先使用第 ${params.pairedProductIndex + 1} 张输入商品图作为本张参考图的一对一替换主体。` : "请使用输入商品图作为本张参考图的一对一替换主体。", + params.productImageCount > 1 + ? "其余商品输入图同样是这个产品的补充角度/细节参考,请一并综合理解产品信息,不可忽略。" + : "输入商品图就是当前产品主体,请保持产品信息准确一致。", + "图像输入顺序规则必须严格遵守:前面的图片是待替换商品图,最后一张图片是当前 SKU 参考图。", "请严格保留最后一张参考图的场景、机位、透视、光影和构图关系,只替换商品主体本身。", "输出要自然融合,不要改变参考图原有风格和景别。", ]; @@ -6124,9 +6134,14 @@ export async function POST(request: Request) { productInputImageUrls.length - 1 ) : undefined; - const currentProductImageUrls = + const orderedProductContextImageUrls = pairedProductIndex !== undefined - ? [productInputImageUrls[pairedProductIndex]!] + ? [ + productInputImageUrls[pairedProductIndex]!, + ...productInputImageUrls.filter( + (_, productIndex) => productIndex !== pairedProductIndex + ), + ] : productInputImageUrls; const referenceDecision = await analyzeReplacementReferenceImage({ imageUrl: reference.imageUrl, @@ -6185,7 +6200,7 @@ export async function POST(request: Request) { const sourceImageUrls = Array.from( new Set( [ - ...currentProductImageUrls, + ...orderedProductContextImageUrls, ...(modelInputImageUrl ? [modelInputImageUrl] : []), ...replacementTemplateReferenceImageUrls, reference.imageUrl, @@ -6203,6 +6218,7 @@ export async function POST(request: Request) { referenceIndex, totalReferences: replacementReferences.length, hasModelImage: Boolean(modelInputImageUrl), + productImageCount: productInputImageUrls.length, pairedProductIndex, }); console.info("[Chat API] Replacement image generation started:", { diff --git a/components/agents/AgentDetailReferenceStitchDialog.tsx b/components/agents/AgentDetailReferenceStitchDialog.tsx index cb93c0554..f17caa105 100644 --- a/components/agents/AgentDetailReferenceStitchDialog.tsx +++ b/components/agents/AgentDetailReferenceStitchDialog.tsx @@ -150,6 +150,8 @@ export function AgentDetailReferenceStitchDialog({ const [snapshot, setSnapshot] = useState(null); const [cutLines, setCutLines] = useState([]); const [draggingLineIndex, setDraggingLineIndex] = useState(null); + const [isAddingCutLine, setIsAddingCutLine] = useState(false); + const [previewCutLineY, setPreviewCutLineY] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isApplying, setIsApplying] = useState(false); const [applyProgressText, setApplyProgressText] = useState(""); @@ -206,6 +208,8 @@ export function AgentDetailReferenceStitchDialog({ useEffect(() => { if (!open) { setDraggingLineIndex(null); + setIsAddingCutLine(false); + setPreviewCutLineY(null); return; } @@ -262,6 +266,37 @@ export function AgentDetailReferenceStitchDialog({ }; }, [open, sourceLabel, validCards]); + const resolveInsertCutLineY = useCallback( + (rawY: number) => { + if (!snapshot) return null; + const sortedLines = [...normalizedCutLines].sort((left, right) => left - right); + const insertIndex = sortedLines.findIndex((value) => value > rawY); + const previousBoundary = + insertIndex <= 0 ? 0 : sortedLines[(insertIndex === -1 ? sortedLines.length : insertIndex) - 1]; + const nextBoundary = + insertIndex === -1 ? snapshot.height : sortedLines[insertIndex]; + const minY = previousBoundary + MIN_SLICE_HEIGHT; + const maxY = nextBoundary - MIN_SLICE_HEIGHT; + if (maxY < minY) { + return null; + } + return clamp(rawY, minY, maxY); + }, + [normalizedCutLines, snapshot] + ); + + const resolveCanvasYFromClient = useCallback( + (clientY: number) => { + const container = previewContainerRef.current; + if (!container || !snapshot) return null; + const rect = container.getBoundingClientRect(); + if (rect.height <= 0) return null; + const rawY = ((clientY - rect.top) / rect.height) * snapshot.height; + return clamp(rawY, 0, snapshot.height); + }, + [snapshot] + ); + useEffect(() => { if (draggingLineIndex === null || !snapshot) { return; @@ -305,26 +340,17 @@ export function AgentDetailReferenceStitchDialog({ const handleAddCutLine = useCallback(() => { if (!snapshot) return; const points = [0, ...normalizedCutLines, snapshot.height]; - let bestIndex = -1; - let bestHeight = 0; - - for (let index = 0; index < points.length - 1; index += 1) { - const currentHeight = points[index + 1] - points[index]; - if (currentHeight > bestHeight) { - bestHeight = currentHeight; - bestIndex = index; - } - } - - if (bestIndex < 0 || bestHeight < MIN_SLICE_HEIGHT * 2) { + const hasSpace = points.some( + (value, index) => + index < points.length - 1 && points[index + 1] - value >= MIN_SLICE_HEIGHT * 2 + ); + if (!hasSpace) { toast.error("当前没有足够空间继续添加切线"); return; } - const nextY = Math.round((points[bestIndex] + points[bestIndex + 1]) / 2); - setCutLines((current) => - [...current, nextY].sort((left, right) => left - right) - ); + setIsAddingCutLine((current) => !current); + setPreviewCutLineY(null); }, [normalizedCutLines, snapshot]); const handleResetCutLines = useCallback(() => { @@ -336,6 +362,40 @@ export function AgentDetailReferenceStitchDialog({ setCutLines((current) => current.filter((_, currentIndex) => currentIndex !== index)); }, []); + const handlePreviewPointerMove = useCallback( + (event: ReactPointerEvent) => { + if (!isAddingCutLine) return; + const rawY = resolveCanvasYFromClient(event.clientY); + if (rawY === null) { + setPreviewCutLineY(null); + return; + } + setPreviewCutLineY(resolveInsertCutLineY(rawY)); + }, + [isAddingCutLine, resolveCanvasYFromClient, resolveInsertCutLineY] + ); + + const handlePreviewPointerLeave = useCallback(() => { + if (!isAddingCutLine) return; + setPreviewCutLineY(null); + }, [isAddingCutLine]); + + const handlePreviewClick = useCallback( + (event: ReactPointerEvent) => { + if (!isAddingCutLine) return; + const rawY = resolveCanvasYFromClient(event.clientY); + const nextY = rawY === null ? null : resolveInsertCutLineY(rawY); + if (nextY === null) { + toast.error("当前位置无法添加切线,请换一个位置"); + return; + } + setCutLines((current) => [...current, Math.round(nextY)].sort((left, right) => left - right)); + setIsAddingCutLine(false); + setPreviewCutLineY(null); + }, + [isAddingCutLine, resolveCanvasYFromClient, resolveInsertCutLineY] + ); + const handleLinePointerDown = useCallback( (index: number) => (event: ReactPointerEvent) => { event.preventDefault(); @@ -477,7 +537,7 @@ export function AgentDetailReferenceStitchDialog({ className="border-white/12 bg-white/[0.03] text-white hover:bg-white/[0.08] hover:text-white" > - 添加切线 + {isAddingCutLine ? "点击长图落线" : "添加切线"}