diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index dc34c7e2e..a6bf18d09 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -451,13 +451,13 @@ function buildScaleAdjustPrompt( const top = Math.round(region.rect.y * 1000) / 10; const width = Math.round(region.rect.width * 1000) / 10; const height = Math.round(region.rect.height * 1000) / 10; - return `区域 ${region.index}(原图位置 x=${left}%,y=${top}%,w=${width}%,h=${height}%):识别该框选范围内的完整主体物体,以主体视觉中心为锚点执行整体等比${actionLabel},目标尺寸为原物体的 ${Math.round(scaleFactor * 100)}%。`; + return `区域 ${region.index}(原图位置 x=${left}%,y=${top}%,w=${width}%,h=${height}%):该矩形只是“寻找目标主体”的范围,不是要缩放或贴回的矩形补丁。请识别此范围内最主要的商品/物体主体,以主体自身轮廓和视觉中心为准,执行整体等比${actionLabel},目标视觉尺寸为原主体的 ${Math.round(scaleFactor * 100)}%。如果框中只覆盖到主体的一部分,请根据连接关系推断该主体的完整可见结构,只调整该主体,不调整矩形框内背景。`; }) .join("\n"); return [ "输入图片1是原始干净图,也是最终输出必须继承的唯一画面基底。", - "系统已根据用户框选区域计算出下方每个区域的原图相对坐标;彩色框只存在于产品界面里,不会作为最终画面内容。", + "系统已根据用户框选区域计算出下方每个区域的原图相对坐标;框选区域只是定位目标主体的搜索范围,不是最终画面的裁切块、贴片或蒙版边界。", "最终结果必须保持输入图片1的原始画幅比例、构图边界、镜头透视、光线、色温、材质质感和清晰度。", "请不要生成任何彩色框、矩形边框、编号圆点、提示词气泡、标注文字、UI 控件或半透明色块。", "", @@ -465,11 +465,13 @@ function buildScaleAdjustPrompt( regionLines, "", "关键要求:", - "1. 只调整框选主体物体的整体视觉比例,必须等比缩放,不得压扁、拉伸、扭曲、改变外观结构、改变材质、改变颜色、改变文字或图案。", - "2. 缩小时,主体应围绕原位置中心自然变小,露出的背景需要与原图环境、纹理、阴影和透视自然补全。", - "3. 放大时,主体应围绕原位置中心自然变大,边缘与遮挡关系要合理,不能变形,也不能超出画面造成突兀裁切。", - "4. 未被框选的其它画面区域必须保持原图不变,不允许重绘、改色、补光、移动物体或改变背景细节。", - "5. 最终输出必须是一张干净照片,不得出现任何选区框、说明文字、对比线、比例数字或 UI 元素。", + "1. 只调整目标主体物体本身的整体视觉比例,必须等比缩放,不得压扁、拉伸、扭曲、改变外观结构、改变材质、改变颜色、改变文字或图案。", + "2. 严禁把整个矩形框选区域当成图片块整体缩放、平移、复制、裁切或贴回;最终画面不得出现矩形拼接边缘、色差硬边、补丁感、裁切线或局部贴片痕迹。", + "3. 缩小时,目标主体应围绕原位置视觉中心自然变小;主体缩小后露出的区域必须根据周围真实背景、阴影、遮挡、纹理和透视进行无缝补全。", + "4. 放大时,目标主体应围绕原位置视觉中心自然变大;边缘与遮挡关系要合理,不能变形,也不能产生突兀裁切。", + "5. 框选区域内非目标主体的背景、墙面、桌面、植物、文字和其它物体应保持原场景逻辑,不要跟着主体一起缩放。", + "6. 未受比例调整影响的其它画面区域必须尽量保持原图不变,不允许整体重绘、改色、补光、移动物体或改变背景细节。", + "7. 最终输出必须是一张完整、干净、无缝的照片,不得出现任何选区框、说明文字、对比线、比例数字或 UI 元素。", ].join("\n"); } @@ -572,6 +574,8 @@ export async function POST(request: NextRequest) { operation === SCALE_ADJUST_OPERATION && scalePercent !== null ? buildScaleAdjustPrompt(regions, scalePercent) : buildLocalEditPrompt(regions); + const isScaleAdjustOperation = + operation === SCALE_ADJUST_OPERATION && scalePercent !== null; const regionReferenceImageUrls = regions.flatMap((region) => region.referenceImages.map((reference) => reference.url) ); @@ -594,6 +598,7 @@ export async function POST(request: NextRequest) { regionReferenceImageCount: regionReferenceImageUrls.length, regionCount: regions.length, scalePercent, + scaleAdjustUsesFullFrameOutput: isScaleAdjustOperation, sourceHeight, sourceNodeId: body.sourceNodeId || null, sourceWidth, @@ -608,17 +613,21 @@ export async function POST(request: NextRequest) { ? body.model.trim() : undefined, negativePrompt: - "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸", + isScaleAdjustOperation + ? "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸、矩形贴片、硬边、拼接线、补丁感、局部裁切、色差边缘、背景断裂、主体未缩放、整块框选区域缩放" + : "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸", prompt, promptExtend: false, referenceImages: [ sourceImageUrl, ...regionReferenceImageUrls, ], - localEditComposite: { - sourceImageUrl, - regions: regions.map((region) => region.rect), - }, + localEditComposite: isScaleAdjustOperation + ? undefined + : { + sourceImageUrl, + regions: regions.map((region) => region.rect), + }, ownerKey: session.user.id, }; diff --git a/components/agents/AgentDetailReferenceStitchDialog.tsx b/components/agents/AgentDetailReferenceStitchDialog.tsx index b141f5812..8d712b1b5 100644 --- a/components/agents/AgentDetailReferenceStitchDialog.tsx +++ b/components/agents/AgentDetailReferenceStitchDialog.tsx @@ -6,10 +6,11 @@ import { useMemo, useRef, useState, + type ChangeEvent, type PointerEvent as ReactPointerEvent, type SyntheticEvent, } from "react"; -import { Loader2, Plus, RotateCcw, Scissors, Trash2 } from "lucide-react"; +import { Loader2, Plus, RotateCcw, Scissors, Trash2, Upload } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { @@ -22,6 +23,10 @@ import { import { cn } from "@/lib/utils"; import { buildImageProxyUrl } from "@/lib/image-delivery"; import { createClientLogger } from "@/lib/observability/client-logger"; +import { + createUploadedDetailReferenceCard, + uploadDetailReferenceImageFile, +} from "@/components/agents/detail-reference-upload"; import type { AgentDetailReplacementReferenceItem } from "@/types/agent"; const MIN_SLICE_HEIGHT = 120; @@ -211,6 +216,10 @@ export function AgentDetailReferenceStitchDialog({ const [warningText, setWarningText] = useState(null); const [previewImageLoadFailed, setPreviewImageLoadFailed] = useState(false); const [passthroughSegmentIndexes, setPassthroughSegmentIndexes] = useState([]); + const [segmentReplacementCards, setSegmentReplacementCards] = useState< + Record + >({}); + const [uploadingSegmentIndex, setUploadingSegmentIndex] = useState(null); const hasManualCutLineChangesRef = useRef(false); const onPreparedRef = useRef(onPrepared); @@ -265,6 +274,10 @@ export function AgentDetailReferenceStitchDialog({ () => new Set(passthroughSegmentIndexes), [passthroughSegmentIndexes] ); + const segmentReplacementCount = useMemo( + () => Object.keys(segmentReplacementCards).length, + [segmentReplacementCards] + ); const mergedPreviewDisplayUrl = useMemo( () => snapshot @@ -285,6 +298,8 @@ export function AgentDetailReferenceStitchDialog({ setWarningText(null); setPreviewImageLoadFailed(false); setPassthroughSegmentIndexes([]); + setSegmentReplacementCards({}); + setUploadingSegmentIndex(null); hasManualCutLineChangesRef.current = false; return; } @@ -311,6 +326,8 @@ export function AgentDetailReferenceStitchDialog({ setPreviewImageLoadFailed(true); setIsLoading(false); setIsPreparing(true); + setSegmentReplacementCards({}); + setUploadingSegmentIndex(null); setLoadingProgressText(`正在后台校准 ${setupCards.length} 张参考图尺寸`); const setup = async () => { @@ -431,6 +448,15 @@ export function AgentDetailReferenceStitchDialog({ setPassthroughSegmentIndexes((current) => current.filter((index) => validIndexes.has(index)) ); + setSegmentReplacementCards((current) => { + const nextEntries = Object.entries(current).filter(([index]) => + validIndexes.has(Number(index)) + ); + if (nextEntries.length === Object.keys(current).length) { + return current; + } + return Object.fromEntries(nextEntries); + }); }, [sliceSegments]); useEffect(() => { @@ -658,6 +684,39 @@ export function AgentDetailReferenceStitchDialog({ [] ); + const handleReplaceSegmentImage = useCallback( + async (segmentIndex: number, event: ChangeEvent) => { + const input = event.currentTarget; + const file = input.files?.[0]; + if (!file) return; + + setUploadingSegmentIndex(segmentIndex); + try { + const upload = await uploadDetailReferenceImageFile(file); + const replacementCard = createUploadedDetailReferenceCard({ + upload, + index: segmentIndex, + labelPrefix: "更换切片", + }); + setSegmentReplacementCards((current) => ({ + ...current, + [segmentIndex]: { + ...replacementCard, + sortOrder: segmentIndex, + passthrough: false, + }, + })); + toast.success(`已更换切片 ${segmentIndex + 1}`); + } catch (error) { + toast.error(error instanceof Error ? error.message : "切片图片更换失败"); + } finally { + setUploadingSegmentIndex(null); + input.value = ""; + } + }, + [] + ); + const handleApply = useCallback(async () => { if (!snapshot) { toast.error("当前还没有可用的拼合图"); @@ -668,6 +727,10 @@ export function AgentDetailReferenceStitchDialog({ toast.error("当前没有可切割的参考图"); return; } + if (uploadingSegmentIndex !== null) { + toast.error("请等待切片图片上传完成"); + return; + } setIsApplying(true); setApplyProgressText(`正在服务端切割 ${sliceSegments.length} 张`); @@ -691,11 +754,14 @@ export function AgentDetailReferenceStitchDialog({ throw new Error(result.error || "切割参考图失败"); } - const orderedCards = result.data.cards.map((card, index) => ({ - ...card, - sortOrder: index, - passthrough: passthroughSegmentIndexSet.has(index), - })); + const orderedCards = result.data.cards.map((card, index) => { + const replacementCard = segmentReplacementCards[index]; + return { + ...(replacementCard || card), + sortOrder: index, + passthrough: passthroughSegmentIndexSet.has(index), + }; + }); if (orderedCards.length !== sliceSegments.length) { throw new Error("部分切片处理失败,请稍后重试"); } @@ -728,8 +794,10 @@ export function AgentDetailReferenceStitchDialog({ onOpenChange, passthroughSegmentIndexes, passthroughSegmentIndexSet, + segmentReplacementCards, sliceSegments.length, snapshot, + uploadingSegmentIndex, ]); return ( @@ -770,6 +838,11 @@ export function AgentDetailReferenceStitchDialog({ 直出 {passthroughSegmentIndexes.length} 张 ) : null} + {segmentReplacementCount > 0 ? ( +
+ 已更换 {segmentReplacementCount} 张 +
+ ) : null} @@ -856,28 +929,28 @@ export function AgentDetailReferenceStitchDialog({
4 ? "auto" : undefined, - containIntrinsicSize: - index > 4 ? `${Math.max(segment.height, 1)}px` : undefined, - }} - > + style={{ + aspectRatio: `${snapshot.width} / ${Math.max(segment.height, 1)}`, + contentVisibility: index > 4 ? "auto" : undefined, + containIntrinsicSize: + index > 4 ? `${Math.max(segment.height, 1)}px` : undefined, + }} + >
参考图 {index + 1} 加载失败
- {segmentImageUrl ? ( - {`拼合分段预览 2 ? "lazy" : "eager"} - fetchPriority={index < 2 ? "high" : "low"} - onError={handleSegmentPreviewImageError(originalImageUrl)} - /> - ) : null} + {segmentImageUrl ? ( + {`拼合分段预览 2 ? "lazy" : "eager"} + fetchPriority={index < 2 ? "high" : "low"} + onError={handleSegmentPreviewImageError(originalImageUrl)} + /> + ) : null}
); })} @@ -976,51 +1049,121 @@ export function AgentDetailReferenceStitchDialog({
{sliceSegments.length > 0 ? ( - sliceSegments.map((segment) => ( -
-
-
- 切片 {segment.index + 1} + sliceSegments.map((segment) => { + const replacementCard = segmentReplacementCards[segment.index]; + const replacementUrl = String( + replacementCard?.previewUrl || replacementCard?.imageUrl || "" + ).trim(); + const isUploadingThisSegment = uploadingSegmentIndex === segment.index; + return ( +
+
+
+ 切片 {segment.index + 1} +
+
+ 高度 {segment.height}px +
-
- 高度 {segment.height}px +
+ 从 {Math.round(segment.top)}px 到 {Math.round(segment.top + segment.height)}px +
+ {replacementUrl ? ( +
+ {`已更换切片 +
+ ) : null} +
+
+ {replacementCard + ? "已使用手动更换图片,应用后会替代当前切片。" + : passthroughSegmentIndexSet.has(segment.index) + ? "当前切片不会参与生图,应用后会直接进入结果区。" + : "当前切片会继续参与详情替换生成。"} +
+
+ + {replacementCard ? ( + + ) : null} + +
-
- 从 {Math.round(segment.top)}px 到 {Math.round(segment.top + segment.height)}px -
-
-
- {passthroughSegmentIndexSet.has(segment.index) - ? "当前切片不会参与生图,应用后会直接进入结果区。" - : "当前切片会继续参与详情替换生成。"} -
- -
-
- )) + ); + }) ) : (
当前还没有可切割的长图。 @@ -1029,12 +1172,19 @@ export function AgentDetailReferenceStitchDialog({
-
) : null}
@@ -1920,31 +1931,36 @@ function saveStoredAgentPendingGenerations(items: LocalGenerationPendingState[]) function DetailResultProcessingScanOverlay({ label }: { label: string }) { return ( -
+
+ className="absolute inset-x-[-8%] h-[26%] min-h-[110px]" + animate={{ top: ["-30%", "104%"], opacity: [0.38, 0.95, 0.48] }} + transition={{ duration: 3.2, repeat: Infinity, ease: "linear" }} + > +
+
+
+
+
@@ -1954,6 +1970,23 @@ function DetailResultProcessingScanOverlay({ label }: { label: string }) { ); } +function ResultProcessedHintBubble({ label }: { label: string }) { + return ( + +
+ + {label} +
+
+ ); +} + function AnimatedGenerationPlaceholderCard(params: { title: string; description: string; @@ -2176,6 +2209,8 @@ export function AgentGenerationWorkspaceDarkV2({ const [detailProductIdQueryInput, setDetailProductIdQueryInput] = useState(""); const [detailReferenceStitchDialogOpen, setDetailReferenceStitchDialogOpen] = useState(false); const [activeDetailReferenceCardId, setActiveDetailReferenceCardId] = useState(null); + const [detailReferenceUploadTarget, setDetailReferenceUploadTarget] = + useState(null); const [skuReplacementSearchState, setSkuReplacementSearchState] = useState<{ isLoading: boolean; lastQuery: string | null; @@ -2194,6 +2229,9 @@ export function AgentGenerationWorkspaceDarkV2({ const [detailGenerationRunResults, setDetailGenerationRunResults] = useState< DirectDetailGenerationImage[] >([]); + const [processedResultHintByUrl, setProcessedResultHintByUrl] = useState< + Record + >({}); const [replacementResultState, setReplacementResultState] = useState( () => createEmptyReplacementResultState() ); @@ -2206,6 +2244,7 @@ export function AgentGenerationWorkspaceDarkV2({ const [replacementRerunPrompt, setReplacementRerunPrompt] = useState(""); const [detailWatermarkRemoveCardIds, setDetailWatermarkRemoveCardIds] = useState>(() => new Set()); + const processedResultHintTimerRef = useRef>({}); const closePsdTemplateDialog = useCallback(() => { setIsPsdTemplateDialogOpen(false); setPsdTemplateDialogSourceUrl(null); @@ -3139,6 +3178,15 @@ export function AgentGenerationWorkspaceDarkV2({ ); }, [historyMessages, setMessages]); + useEffect(() => { + return () => { + Object.values(processedResultHintTimerRef.current).forEach((timerId) => { + window.clearTimeout(timerId); + }); + processedResultHintTimerRef.current = {}; + }; + }, []); + useEffect(() => { if (isWorkflowModelLocked) { setSelectedModelId((current) => (current === "auto" ? current : "auto")); @@ -5121,11 +5169,11 @@ export function AgentGenerationWorkspaceDarkV2({ }, [mergedResultCollections.scene_generation, onResultPreviewUrlsChange, uploadBase64ResultToStorage] ); - const appendResultUrlToCollection = useCallback( - (modeType: AgentGenerationResultModeType, imageUrl: string) => { - const normalizedUrl = String(imageUrl || "").trim(); - if (!normalizedUrl || !onResultPreviewUrlsChange) { - return false; + const appendResultUrlToCollection = useCallback( + (modeType: AgentGenerationResultModeType, imageUrl: string) => { + const normalizedUrl = String(imageUrl || "").trim(); + if (!normalizedUrl || !onResultPreviewUrlsChange) { + return false; } const nextUrls = Array.from( @@ -5139,13 +5187,38 @@ export function AgentGenerationWorkspaceDarkV2({ setSelectedResultUrl(normalizedUrl); } return true; - }, - [activeResultModeType, mergedResultCollections, onResultPreviewUrlsChange] - ); - const handleEnhanceDetailGenerationResult = useCallback( - async (params: { - cardId: string; - imageUrl: string; + }, + [activeResultModeType, mergedResultCollections, onResultPreviewUrlsChange] + ); + const showProcessedResultHint = useCallback((imageUrl: string, label: string) => { + const normalizedUrl = String(imageUrl || "").trim(); + const normalizedLabel = String(label || "").trim(); + if (!/^https?:\/\//i.test(normalizedUrl) || !normalizedLabel) return; + + const existingTimer = processedResultHintTimerRef.current[normalizedUrl]; + if (existingTimer) { + window.clearTimeout(existingTimer); + } + + setProcessedResultHintByUrl((current) => ({ + ...current, + [normalizedUrl]: normalizedLabel, + })); + + processedResultHintTimerRef.current[normalizedUrl] = window.setTimeout(() => { + setProcessedResultHintByUrl((current) => { + if (!(normalizedUrl in current)) return current; + const next = { ...current }; + delete next[normalizedUrl]; + return next; + }); + delete processedResultHintTimerRef.current[normalizedUrl]; + }, 2800); + }, []); + const handleEnhanceDetailGenerationResult = useCallback( + async (params: { + cardId: string; + imageUrl: string; resultModeType?: "detail_generation" | "detail_replacement"; }) => { const cardId = String(params.cardId || "").trim(); @@ -5296,6 +5369,10 @@ export function AgentGenerationWorkspaceDarkV2({ setActiveResultModeType("detail_generation"); setSelectedResultUrl(nextUrl); } + showProcessedResultHint( + nextUrl, + isScaleAdjust ? "比例调整完成" : "局部编辑完成" + ); setDetailLocalEditTarget(null); toast.success( resultModeType === "detail_replacement" @@ -5333,6 +5410,7 @@ export function AgentGenerationWorkspaceDarkV2({ detailLocalEditSubmittingId, detailLocalEditTarget, handleUpdateReplacementResultRecord, + showProcessedResultHint, ] ); const handlePsdDerivedImageCreated = useCallback( @@ -5341,13 +5419,13 @@ export function AgentGenerationWorkspaceDarkV2({ }, [appendResultUrlToCollection, psdTemplateDialogResultModeType] ); - const handlePsdRenderedResultCreated = useCallback( - (imageUrl: string) => { - appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl); - }, - [appendResultUrlToCollection, psdTemplateDialogResultModeType] - ); - const handleDeleteResultImage = useCallback( + const handlePsdRenderedResultCreated = useCallback( + (imageUrl: string) => { + appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl); + }, + [appendResultUrlToCollection, psdTemplateDialogResultModeType] + ); + const handleDeleteResultImage = useCallback( (modeType: AgentGenerationResultModeType, imageUrl: string) => { const normalizedUrl = String(imageUrl || "").trim(); if (!normalizedUrl || !onDeleteResultImage) { @@ -5363,6 +5441,17 @@ export function AgentGenerationWorkspaceDarkV2({ if (sceneResizeEditorTargetUrl === normalizedUrl) { setSceneResizeEditorTargetUrl(null); } + const existingTimer = processedResultHintTimerRef.current[normalizedUrl]; + if (existingTimer) { + window.clearTimeout(existingTimer); + delete processedResultHintTimerRef.current[normalizedUrl]; + } + setProcessedResultHintByUrl((current) => { + if (!(normalizedUrl in current)) return current; + const next = { ...current }; + delete next[normalizedUrl]; + return next; + }); onDeleteResultImage(modeType, normalizedUrl); toast.success("结果图已删除"); @@ -6258,6 +6347,101 @@ export function AgentGenerationWorkspaceDarkV2({ references: [], })); }, [updateDetailReplacementState]); + const clearDetailReplacementResultRecords = useCallback((cardIds: string[]) => { + const idSet = new Set(cardIds.map((item) => String(item || "").trim()).filter(Boolean)); + if (idSet.size === 0) return; + + setReplacementResultState((current) => { + const nextDetail = { ...current.detail_replacement }; + let changed = false; + idSet.forEach((cardId) => { + if (cardId in nextDetail) { + delete nextDetail[cardId]; + changed = true; + } + }); + return changed + ? { + ...current, + detail_replacement: nextDetail, + } + : current; + }); + }, []); + const handleAppendDetailReferenceFiles = useCallback( + async (event: React.ChangeEvent) => { + const input = event.currentTarget; + const files = Array.from(input.files || []); + if (files.length === 0) return; + + setDetailReferenceUploadTarget({ type: "append" }); + try { + const uploads: UploadedDetailReferenceImage[] = []; + for (const file of files) { + uploads.push(await uploadDetailReferenceImageFile(file)); + } + + updateDetailReplacementState((current) => { + const startIndex = current.references.length; + const uploadedCards = uploads.map((upload, uploadIndex) => + createUploadedDetailReferenceCard({ + upload, + index: startIndex + uploadIndex, + labelPrefix: "上传目标详情", + }) + ); + return { + ...current, + references: [...current.references, ...uploadedCards].map((card, index) => ({ + ...card, + sortOrder: index, + })), + }; + }); + toast.success(`已上传 ${uploads.length} 张目标详情`); + } catch (error) { + toast.error(error instanceof Error ? error.message : "目标详情上传失败"); + } finally { + setDetailReferenceUploadTarget(null); + input.value = ""; + } + }, + [updateDetailReplacementState] + ); + const handleReplaceDetailReferenceCardFile = useCallback( + async (cardId: string, event: React.ChangeEvent) => { + const input = event.currentTarget; + const file = input.files?.[0]; + if (!file) return; + + setDetailReferenceUploadTarget({ type: "replace", cardId }); + try { + const upload = await uploadDetailReferenceImageFile(file); + let didReplace = false; + updateDetailReplacementState((current) => ({ + ...current, + references: current.references.map((card, index) => { + if (card.id !== cardId) return card; + didReplace = true; + return { + ...applyUploadedDetailReferenceToCard(card, upload), + sortOrder: card.sortOrder ?? index, + }; + }), + })); + if (didReplace) { + clearDetailReplacementResultRecords([cardId]); + toast.success("已更换当前目标详情图"); + } + } catch (error) { + toast.error(error instanceof Error ? error.message : "目标详情更换失败"); + } finally { + setDetailReferenceUploadTarget(null); + input.value = ""; + } + }, + [clearDetailReplacementResultRecords, updateDetailReplacementState] + ); const handlePreparedDetailReferenceCards = useCallback( (preparedCards: AgentModeWorkspaceState["detailReplacement"]["references"]) => { const preparedById = new Map(preparedCards.map((card) => [card.id, card])); @@ -7425,11 +7609,34 @@ export function AgentGenerationWorkspaceDarkV2({ 目标 / {detailReferenceSourceLabel}
-
-
- setDetailProductIdQueryInput(event.target.value)} +
+
+ + setDetailProductIdQueryInput(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); @@ -7924,10 +8131,33 @@ export function AgentGenerationWorkspaceDarkV2({
支持按 SPU / SKU 自动兜底查询任务整套候选,并切换某一套的{detailReferenceSourceLabel}参考图。
-
-
- {detailReplacementReferenceCards.length > 0 ? ( -
@@ -8119,6 +8376,13 @@ export function AgentGenerationWorkspaceDarkV2({
{statusMeta.label}
+ + {card.resultUrl && processedResultHintByUrl[card.resultUrl] ? ( + + ) : null} + {isActive ? (
当前联动 @@ -8967,6 +9231,13 @@ export function AgentGenerationWorkspaceDarkV2({ 套PSD ) : null} + + {card.url && processedResultHintByUrl[card.url] ? ( + + ) : null} +