From 537a9d10c22a97a2d1753c2bbcc87f064892a2ea Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 2 Jun 2026 14:10:08 +0800 Subject: [PATCH] fix: update detail replacement and UI behavior --- .../api/chat/_lib/detail-replacement.ts | 425 +++++++++++++++++- app/(chat)/api/chat/route.ts | 20 + .../agents/AgentGenerationWorkspaceDarkV2.tsx | 253 +++-------- components/home/floating-sidebar-shell.tsx | 4 +- components/home/lovart-home.tsx | 2 +- components/home/lovart/lovart-sidebar.tsx | 2 +- components/ui/searchable-select.tsx | 10 +- docs/更新通知-2026-04-29至2026-05-29.md | 201 +++++++++ docs/更新通知-2026-05-14至2026-05-29.md | 186 ++++++++ docs/风格化功能商业宣传片旁白教程.md | 410 +++++++++++++++++ 10 files changed, 1288 insertions(+), 225 deletions(-) create mode 100644 docs/更新通知-2026-04-29至2026-05-29.md create mode 100644 docs/更新通知-2026-05-14至2026-05-29.md create mode 100644 docs/风格化功能商业宣传片旁白教程.md diff --git a/app/(chat)/api/chat/_lib/detail-replacement.ts b/app/(chat)/api/chat/_lib/detail-replacement.ts index 88936ad9b..c3ac8a05a 100644 --- a/app/(chat)/api/chat/_lib/detail-replacement.ts +++ b/app/(chat)/api/chat/_lib/detail-replacement.ts @@ -174,6 +174,18 @@ export async function normalizeReplacementResultImage(params: { export type AgentReplacementModePayload = NonNullable; export type AgentModeSourceImagePayload = NonNullable; export type AgentDetailReplacementStrategy = "scene_replace" | "product_only"; +export type DetailReplacementGenerationRoute = + | "prompt_pack_scene" + | "layout_preserve_replace" + | "copy_locked_regenerate" + | "people_relation_locked" + | "direct_passthrough"; +export type DetailReplacementPromptPackPolicy = "allow" | "soft" | "forbid"; +export type DetailReplacementTextPolicy = "free" | "preserve" | "lock"; +export type DetailReplacementPeoplePolicy = + | "none" + | "preserve_relation" + | "require_user_model"; export type NormalizedAgentModeSourceImage = { url: string; @@ -196,6 +208,13 @@ export type ReplacementReferenceDecision = { }; export type ReplacementScenePlan = { + generationRoute: DetailReplacementGenerationRoute; + promptPackPolicy: DetailReplacementPromptPackPolicy; + textPolicy: DetailReplacementTextPolicy; + peoplePolicy: DetailReplacementPeoplePolicy; + whiteBackgroundLikely: boolean; + copyImageCoupled: boolean; + routeReason: string; useModelImage: boolean; modelImageReason: string; sceneSummary: string; @@ -240,11 +259,196 @@ export function truncateReplacementAnalysisText(value: unknown, maxLength = 240) return String(value || "").trim().replace(/\s+/g, " ").slice(0, maxLength); } +function normalizeDetailGenerationRoute( + value: unknown, +): DetailReplacementGenerationRoute | null { + const normalized = String(value || "") + .trim() + .toLowerCase() + .replace(/[\s-]+/g, "_"); + if (!normalized) { + return null; + } + if ( + normalized === "prompt_pack_scene" || + normalized === "scene" || + normalized === "prompt_scene" || + normalized === "use_prompt_pack" + ) { + return "prompt_pack_scene"; + } + if ( + normalized === "layout_preserve_replace" || + normalized === "layout_preserve" || + normalized === "preserve_layout" || + normalized === "product_replace" + ) { + return "layout_preserve_replace"; + } + if ( + normalized === "copy_locked_regenerate" || + normalized === "copy_locked" || + normalized === "text_locked" || + normalized === "lock_copy" + ) { + return "copy_locked_regenerate"; + } + if ( + normalized === "people_relation_locked" || + normalized === "people_locked" || + normalized === "model_relation_locked" || + normalized === "preserve_people" + ) { + return "people_relation_locked"; + } + if ( + normalized === "direct_passthrough" || + normalized === "passthrough" || + normalized === "keep_original" || + normalized === "skip" + ) { + return "direct_passthrough"; + } + return null; +} + +function normalizePromptPackPolicy( + value: unknown, +): DetailReplacementPromptPackPolicy | null { + if (typeof value === "boolean") { + return value ? "allow" : "forbid"; + } + const normalized = String(value || "").trim().toLowerCase(); + if (!normalized) { + return null; + } + if (["allow", "allowed", "true", "yes", "use", "enable", "启用", "使用", "允许"].includes(normalized)) { + return "allow"; + } + if (["soft", "weak", "limited", "reference", "参考", "弱化", "柔性"].includes(normalized)) { + return "soft"; + } + if (["forbid", "forbidden", "false", "no", "disable", "禁用", "禁止", "不使用"].includes(normalized)) { + return "forbid"; + } + return null; +} + +function normalizeTextPolicy(value: unknown): DetailReplacementTextPolicy | null { + const normalized = String(value || "").trim().toLowerCase(); + if (!normalized) { + return null; + } + if (["free", "rewrite", "自由", "可改写"].includes(normalized)) { + return "free"; + } + if (["preserve", "keep", "保留", "保持"].includes(normalized)) { + return "preserve"; + } + if (["lock", "locked", "copy_locked", "锁定", "锁文案", "严格保留"].includes(normalized)) { + return "lock"; + } + return null; +} + +function normalizePeoplePolicy(value: unknown): DetailReplacementPeoplePolicy | null { + const normalized = String(value || "").trim().toLowerCase(); + if (!normalized) { + return null; + } + if (["none", "no_people", "无", "无人"].includes(normalized)) { + return "none"; + } + if ( + [ + "preserve_relation", + "preserve", + "relation_locked", + "keep_people", + "保留关系", + "锁定人物关系", + ].includes(normalized) + ) { + return "preserve_relation"; + } + if ( + [ + "require_user_model", + "require_model", + "user_model_only", + "需要用户模特", + "必须用户模特", + ].includes(normalized) + ) { + return "require_user_model"; + } + return null; +} + +function inferDetailGenerationRoute(params: { + shouldReplace: boolean; + isProductOnly: boolean; + whiteBackgroundLikely: boolean; + copyImageCoupled: boolean; + hasImportantText: boolean; + containsPeople: boolean; + containsMultiplePeople: boolean; +}): DetailReplacementGenerationRoute { + if (!params.shouldReplace) { + return "direct_passthrough"; + } + if (params.isProductOnly) { + return "layout_preserve_replace"; + } + if (params.whiteBackgroundLikely) { + return "layout_preserve_replace"; + } + if (params.copyImageCoupled || params.hasImportantText) { + return "copy_locked_regenerate"; + } + if (params.containsMultiplePeople || params.containsPeople) { + return "people_relation_locked"; + } + return "prompt_pack_scene"; +} + +function inferPromptPackPolicy( + route: DetailReplacementGenerationRoute, +): DetailReplacementPromptPackPolicy { + return route === "prompt_pack_scene" ? "allow" : "forbid"; +} + +function inferTextPolicy(params: { + route: DetailReplacementGenerationRoute; + hasImportantText: boolean; + copyImageCoupled: boolean; +}): DetailReplacementTextPolicy { + if (params.route === "copy_locked_regenerate" || params.copyImageCoupled) { + return "lock"; + } + if (params.hasImportantText || params.route === "layout_preserve_replace") { + return "preserve"; + } + return "free"; +} + +function inferPeoplePolicy(params: { + containsPeople: boolean; + containsMultiplePeople: boolean; + hasModelImage: boolean; +}): DetailReplacementPeoplePolicy { + if (!params.containsPeople && !params.containsMultiplePeople) { + return "none"; + } + return params.hasModelImage ? "preserve_relation" : "require_user_model"; +} + export function parseReplacementScenePlan( text: string, params: { productImageCount: number; hasModelImage: boolean; + replacementStrategy?: AgentDetailReplacementStrategy; }, ): ReplacementScenePlan | null { const jsonText = extractJsonObjectText(text); @@ -254,6 +458,69 @@ export function parseReplacementScenePlan( try { const parsed = JSON.parse(jsonText) as Record; + const isProductOnly = params.replacementStrategy === "product_only"; + const shouldReplace = + normalizeBooleanLike(parsed.shouldReplace) ?? + normalizeBooleanLike(parsed.replace) ?? + normalizeBooleanLike(parsed.isReplaceable) ?? + true; + const whiteBackgroundLikely = + normalizeBooleanLike(parsed.whiteBackgroundLikely) ?? + normalizeBooleanLike(parsed.whiteBackground) ?? + normalizeBooleanLike(parsed.isWhiteBackground) ?? + false; + const copyImageCoupled = + normalizeBooleanLike(parsed.copyImageCoupled) ?? + normalizeBooleanLike(parsed.textImageCoupled) ?? + normalizeBooleanLike(parsed.copyMatchesImage) ?? + false; + const hasImportantText = + normalizeBooleanLike(parsed.hasImportantText) ?? + normalizeBooleanLike(parsed.textHeavy) ?? + normalizeBooleanLike(parsed.containsMarketingCopy) ?? + normalizeBooleanLike(parsed.containsImportantCopy) ?? + false; + const containsPeople = + normalizeBooleanLike(parsed.containsPeople) ?? + normalizeBooleanLike(parsed.hasPeople) ?? + normalizeBooleanLike(parsed.peoplePresent) ?? + false; + const containsMultiplePeople = + normalizeBooleanLike(parsed.containsMultiplePeople) ?? + normalizeBooleanLike(parsed.multiPerson) ?? + normalizeBooleanLike(parsed.multiplePeople) ?? + false; + const generationRoute = + normalizeDetailGenerationRoute( + parsed.generationRoute || parsed.route || parsed.recommendedRoute, + ) || + inferDetailGenerationRoute({ + shouldReplace, + isProductOnly, + whiteBackgroundLikely, + copyImageCoupled, + hasImportantText, + containsPeople, + containsMultiplePeople, + }); + const promptPackPolicy = + normalizePromptPackPolicy( + parsed.promptPackPolicy ?? parsed.usePromptPack ?? parsed.allowPromptPack, + ) || inferPromptPackPolicy(generationRoute); + const textPolicy = + normalizeTextPolicy(parsed.textPolicy || parsed.copyPolicy) || + inferTextPolicy({ + route: generationRoute, + hasImportantText, + copyImageCoupled, + }); + const peoplePolicy = + normalizePeoplePolicy(parsed.peoplePolicy || parsed.modelPolicy) || + inferPeoplePolicy({ + containsPeople, + containsMultiplePeople, + hasModelImage: params.hasModelImage, + }); const useModelImageRaw = normalizeBooleanLike(parsed.useModelImage) ?? normalizeBooleanLike(parsed.needModelImage) ?? @@ -266,6 +533,16 @@ export function parseReplacementScenePlan( ); const scenePlan: ReplacementScenePlan = { + generationRoute, + promptPackPolicy, + textPolicy, + peoplePolicy, + whiteBackgroundLikely, + copyImageCoupled, + routeReason: truncateReplacementAnalysisText( + parsed.routeReason || parsed.strategyReason || parsed.policyReason || parsed.reason, + 180, + ), useModelImage: params.hasModelImage ? Boolean(useModelImageRaw) : false, modelImageReason: truncateReplacementAnalysisText( parsed.modelImageReason || parsed.modelReason, @@ -310,6 +587,7 @@ export function parseReplacementScenePlan( if (!scenePlan.generationPrompt) { scenePlan.generationPrompt = [ + scenePlan.routeReason, scenePlan.sceneSummary, scenePlan.cameraSummary, scenePlan.placementSummary, @@ -335,7 +613,23 @@ export function buildHeuristicReplacementScenePlan(params: { replacementStrategy?: AgentDetailReplacementStrategy; }): ReplacementScenePlan { const isProductOnly = params.replacementStrategy === "product_only"; + const generationRoute = params.shouldReplace + ? isProductOnly + ? "layout_preserve_replace" + : "prompt_pack_scene" + : "direct_passthrough"; return { + generationRoute, + promptPackPolicy: inferPromptPackPolicy(generationRoute), + textPolicy: generationRoute === "direct_passthrough" ? "preserve" : "free", + peoplePolicy: "none", + whiteBackgroundLikely: false, + copyImageCoupled: false, + routeReason: params.shouldReplace + ? isProductOnly + ? "兜底按仅替换产品执行,不使用词包。" + : "兜底未检测到强约束,允许使用词包生成场景。" + : params.reason, selectedProductImageIndexes: Array.from( { length: Math.min(Math.max(params.productImageCount, 1), 2) }, (_, index) => index + 1, @@ -845,7 +1139,7 @@ export async function analyzeReplacementExecutionPlan(params: { isProductOnlyDetailReplacement ? "这张参考图要求最大程度原样保留。后续生图只允许替换其中的产品主体/产品展示区域,背景、文案、按钮、图标、分割线、品牌标识、留白、装饰和光影氛围都必须保留。" : isDetailReplacement - ? "这张参考图不是要求原样保留的场景照片。后续生图只允许替换/重建背景图、产品场景图、商品主体图或人物展示图区域;文字、标题、卖点标签、UI图标、按钮、分割线、品牌标识、留白和排版必须保留。" + ? "这张参考图可能是场景图、白底说明图、强文案图、人物关系图或纯版式图。你必须先判断它适合哪一种执行路由,再决定是否允许前置工作流词包参与。文字、标题、卖点标签、UI图标、按钮、分割线、品牌标识、留白和排版必须保留。" : "这张参考图不是要求原样保留的场景照片。原图里的沙发、床、柜体、墙面、地面、窗帘、灯具、摆件、桌面道具、背景家具和环境光氛围,都可以在后续生图阶段按前置工作流风格重建。", "请完成以下任务:", isProductOnlyDetailReplacement @@ -854,27 +1148,47 @@ export async function analyzeReplacementExecutionPlan(params: { ? "1. 判断 shouldReplace:当前参考图里是否存在适合被替换/重建的背景图、产品场景图、商品主体图、人物展示图或大面积视觉图片区。纯文字/纯参数/纯品牌介绍且没有视觉图片区时才为 false。" : "1. 判断 shouldReplace:当前参考图里是否存在清晰、主要、适合被替换的家具商品主体。", "2. 判断 useModelImage,并说明原因。", - "3. 提炼 sceneSummary、cameraSummary、placementSummary、preserveSummary、styleSummary、avoidSummary。", + isDetailReplacement + ? "3. 判断 generationRoute:prompt_pack_scene=通用场景图且可吃词包;layout_preserve_replace=锁定版式/白底/产品说明,只做有限替换;copy_locked_regenerate=文案与画面强绑定,必须锁文案;people_relation_locked=多人/模特/互动关系图,必须锁人物关系;direct_passthrough=纯文字/白底说明/无可替换视觉区,直接沿用。" + : "3. 提炼 sceneSummary、cameraSummary、placementSummary、preserveSummary、styleSummary、avoidSummary。", + isDetailReplacement + ? "4. 判断 promptPackPolicy:只有通用空间场景且没有强文案、白底、多人关系、词图强绑定限制时才 allow;白底图、强文案图、多人关系图、图文匹配强的详情页必须 forbid。" + : "", + isDetailReplacement + ? "5. 判断 textPolicy:画面中存在营销标题、卖点、编号、参数、标签且与图匹配时必须 lock 或 preserve,避免后续生图改字、混色或重排。" + : "", + isDetailReplacement + ? "6. 判断 peoplePolicy:有人物/多人/自拍/聚会/模特互动时必须 preserve_relation;如果没有提供用户模特图但参考图有人物身份,不允许把参考人物身份当成用户模特直接迁移。" + : "", + isDetailReplacement + ? "7. 提炼 sceneSummary、cameraSummary、placementSummary、preserveSummary、styleSummary、avoidSummary。" + : "", isProductOnlyDetailReplacement ? "4. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令,要求只替换产品主体,保留其余背景、文案、排版、装饰和光影完全不变。" : isDetailReplacement - ? "4. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令,描述需要替换的背景/场景图片区、产品放置方式、模特使用规则,以及必须保留的文字/UI/图标/排版。" + ? "8. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令。若 promptPackPolicy=forbid,必须明确“不使用前置词包,参考图语义优先”;若 textPolicy=lock,必须明确文字内容、颜色、层级和位置不可改;若 peoplePolicy=preserve_relation,必须明确人物数量、关系、动作和身份约束。" : "4. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令,描述这一屏需要的场景、机位、主体位置、周边环境重建方向以及必须保留的内容。", isProductOnlyDetailReplacement ? "5. sceneSummary、styleSummary 和 generationPrompt 必须强调‘只替换产品,不动其他内容’,不要引导重建场景。" + : isDetailReplacement + ? "9. 如果 generationRoute 不是 prompt_pack_scene,sceneSummary、styleSummary 和 generationPrompt 必须优先服务参考图原有语义、版式、文案和人物关系,不要强行套前置词包。" : "5. sceneSummary、styleSummary 和 generationPrompt 必须优先描述“按前置工作流重建后的目标场景”,而不是只复述参考图当前已有场景。", isProductOnlyDetailReplacement ? "6. avoidSummary 里要明确避免改背景、改文字、改按钮、改图标、改版式、改留白、改装饰、改光影氛围。" + : isDetailReplacement + ? "10. 如果前置工作流风格与当前参考图原场景冲突,只有 promptPackPolicy=allow 时才以前置词包为主;其他情况必须以参考图语义和版式为主。" : "6. 如果前置工作流风格与当前参考图原场景冲突,必须保留版式骨架,但放弃原场景物件本身,以前置工作流指定的空间风格、软装、灯光和氛围为准。", isProductOnlyDetailReplacement ? "7. 不要根据用户提示词或工作流风格改造场景,只服务于产品主体替换。" + : isDetailReplacement + ? "11. avoidSummary 必须包含该路由下最容易失败的风险,例如:词图不匹配、文案变色、人物换脸、白底图被场景化、多人关系丢失。" : "7. avoidSummary 里要明确避免“只替换产品、不重建场景”这类结果。", workflowPromptExcerpt ? `当前风格与场景限制(来自前置工作流与模式模板):\n${workflowPromptExcerpt}` : "", "只返回严格 JSON,不要任何额外解释。", `JSON 格式: -{"shouldReplace":true,"containsFurnitureProduct":true,"reason":"不超过30字","useModelImage":false,"modelImageReason":"不超过40字","sceneSummary":"不超过80字","cameraSummary":"不超过60字","placementSummary":"不超过60字","preserveSummary":"不超过80字","styleSummary":"不超过80字","avoidSummary":"不超过80字","generationPrompt":"不超过300字"}`, +{"shouldReplace":true,"containsFurnitureProduct":true,"reason":"不超过30字","generationRoute":"prompt_pack_scene|layout_preserve_replace|copy_locked_regenerate|people_relation_locked|direct_passthrough","promptPackPolicy":"allow|soft|forbid","textPolicy":"free|preserve|lock","peoplePolicy":"none|preserve_relation|require_user_model","whiteBackgroundLikely":false,"copyImageCoupled":false,"containsPeople":false,"containsMultiplePeople":false,"routeReason":"不超过50字","useModelImage":false,"modelImageReason":"不超过40字","sceneSummary":"不超过80字","cameraSummary":"不超过60字","placementSummary":"不超过60字","preserveSummary":"不超过80字","styleSummary":"不超过80字","avoidSummary":"不超过80字","generationPrompt":"不超过300字"}`, ] .filter(Boolean) .join("\n"); @@ -909,6 +1223,7 @@ export async function analyzeReplacementExecutionPlan(params: { hasModelImage: isProductOnlyDetailReplacement ? false : Boolean(params.modelImageUrl), + replacementStrategy: params.replacementStrategy, }); if (parsedDecision && parsedScenePlan) { @@ -958,6 +1273,31 @@ export async function analyzeReplacementExecutionPlan(params: { }; } +function shouldApplyPromptPackToDetailReplacement(params: { + replacementStrategy?: AgentDetailReplacementStrategy; + scenePlan?: ReplacementScenePlan | null; +}) { + if (params.replacementStrategy === "product_only") { + return false; + } + const scenePlan = params.scenePlan || null; + if (!scenePlan) { + return true; + } + if (scenePlan.promptPackPolicy === "forbid") { + return false; + } + if ( + scenePlan.generationRoute === "direct_passthrough" || + scenePlan.generationRoute === "layout_preserve_replace" || + scenePlan.generationRoute === "copy_locked_regenerate" || + scenePlan.generationRoute === "people_relation_locked" + ) { + return scenePlan.promptPackPolicy === "allow"; + } + return true; +} + export function buildAgentReplacementPrompt(params: { basePrompt: string; modeType: "detail_replacement" | "sku_replacement"; @@ -975,6 +1315,12 @@ export function buildAgentReplacementPrompt(params: { }) { const imageOrderLines: string[] = []; let nextImageIndex = 1; + const shouldApplyPromptPack = + params.modeType !== "detail_replacement" || + shouldApplyPromptPackToDetailReplacement({ + replacementStrategy: params.replacementStrategy, + scenePlan: params.scenePlan, + }); if (params.productImageCount > 0) { const productRangeLabel = @@ -1012,6 +1358,19 @@ export function buildAgentReplacementPrompt(params: { const scenePlanLines = params.scenePlan ? [ "【本屏场景预分析】", + `自动路由:${params.scenePlan.generationRoute}`, + `词包策略:${params.scenePlan.promptPackPolicy}`, + `文案策略:${params.scenePlan.textPolicy}`, + `人物策略:${params.scenePlan.peoplePolicy}`, + params.scenePlan.routeReason + ? `路由理由:${params.scenePlan.routeReason}` + : "", + params.scenePlan.whiteBackgroundLikely + ? "白底/说明图判断:是,禁止强行场景化。" + : "", + params.scenePlan.copyImageCoupled + ? "图文绑定判断:是,文案与画面语义必须保持匹配。" + : "", params.scenePlan.sceneSummary ? `场景理解:${params.scenePlan.sceneSummary}` : "", @@ -1082,29 +1441,60 @@ export function buildAgentReplacementPrompt(params: { ...imageOrderLines, ...scenePlanLines, "你现在做的是“详情参考图背景/场景图替换”,不是重新设计整张详情图。", - "要把最后一张参考图理解为“页面版式底板 + 背景图/场景图区参考”,不是“旧场景照片必须原样保留”。", - "前面“工作流设定”里的场景组合、空间风格、硬装方案、软装方案、灯光氛围、人群设定,必须作为当前这一屏的强制场景改造要求生效。", - "场景风格必须优先执行前面用户提示词与工作流设定中给出的空间风格、材质、灯光和氛围描述,不要被参考图原有旧场景绑死。", - "执行时要基于这些场景组合要求,重建参考图中的背景图/产品场景图区域,使当前这一屏呈现出与第一步已选风格一致的新背景画面。", + shouldApplyPromptPack + ? "要把最后一张参考图理解为“页面版式底板 + 背景图/场景图区参考”,不是“旧场景照片必须原样保留”。" + : "本屏已自动判定为“参考图语义优先”,不要把最后一张参考图当成可随意重写的场景模板。", + shouldApplyPromptPack + ? "前面“工作流设定”里的场景组合、空间风格、硬装方案、软装方案、灯光氛围、人群设定,必须作为当前这一屏的强制场景改造要求生效。" + : "本屏不使用前置工作流词包;如果词包里的空间、人群、灯光、风格与参考图冲突,必须忽略词包,以参考图当前图文语义为准。", + shouldApplyPromptPack + ? "场景风格必须优先执行前面用户提示词与工作流设定中给出的空间风格、材质、灯光和氛围描述,不要被参考图原有旧场景绑死。" + : "不要把厨房、灯光、人群、主题风格等前置选择强行套入本屏;本屏只做必要的产品/视觉区替换和自然融合。", + shouldApplyPromptPack + ? "执行时要基于这些场景组合要求,重建参考图中的背景图/产品场景图区域,使当前这一屏呈现出与第一步已选风格一致的新背景画面。" + : "执行时必须保持参考图原有画面主题、文案语义、人物关系、构图和版式,只在可替换视觉区做有限替换。", "允许替换/重建的范围是参考图里的背景图、产品场景图、商品主体图和必要的人物展示图区;不允许改动版式层。", "如果前面商品图、模特图与最后一张参考图底板存在冲突,始终以最后一张参考图底板的版式、机位、构图和信息层级为最高优先级。", "最后一张参考图是唯一底板,必须完整保留这张图的文字排版、品牌字样、标题区、卖点区、UI图标、按钮、标签、装饰元素、分割线、留白、画布比例和信息层级。", + params.scenePlan?.textPolicy === "lock" + ? "本屏文案已判定为强锁定:标题、卖点、参数、编号、标签文字的内容、颜色层级、位置和排版都不得改写、变色或拆成多种颜色。" + : "如果参考图里存在文字或标签,必须保持文字区位置和信息层级,不要擅自改写或重排。", + params.scenePlan?.peoplePolicy === "require_user_model" + ? "本屏包含人物但没有提供用户模特图:禁止把参考图中的人物身份当作用户模特迁移;只能保留人物数量、关系、动作和构图语义,不要换成不相关强人物主体。" + : params.scenePlan?.peoplePolicy === "preserve_relation" + ? "本屏包含人物关系:必须保留人物数量、相对位置、互动关系、动作方向和出镜比例,不要把多人图改成单人图或无关模特图。" + : "如果参考图没有明确人物展示需求,不要凭空新增人物主体。", + params.scenePlan?.whiteBackgroundLikely + ? "本屏疑似白底/说明图:禁止场景化、生活化或套空间词包,只允许保留白底/说明图语义并做必要的局部替换。" + : "", params.referenceGeometry ? `当前参考图画布尺寸为 ${params.referenceGeometry.width}x${params.referenceGeometry.height},输出必须保持这个页面画布比例。替换背景/商品时只能等比缩放后裁切或延展背景,绝对禁止把商品、人物、文字、图标横向或纵向拉伸变形。` : "输出必须保持当前参考图页面画布比例。替换背景/商品时只能等比缩放后裁切或延展背景,绝对禁止把商品、人物、文字、图标横向或纵向拉伸变形。", "镜头角度、透视关系、视平线、主体朝向和主体占比必须尽量跟随最后一张参考图,方便无缝替换当前版式,不要擅自改成完全不同的视角。", - "前面的商品图和模特图用于生成新的背景图/产品场景图内容,并按工作流设定替换参考图中的旧背景画面,但不能改变参考图的版式设计。", - "可被替换重建的内容包括但不限于:背景照片、产品展示照片、墙面、地面材质、沙发、床、背景柜、窗帘、灯具、地毯、挂画、摆件、桌面道具、空间色调、环境光氛围。", + shouldApplyPromptPack + ? "前面的商品图和模特图用于生成新的背景图/产品场景图内容,并按工作流设定替换参考图中的旧背景画面,但不能改变参考图的版式设计。" + : "前面的商品图用于必要的产品/视觉区替换;模特图只在本屏明确需要人物且策略允许时使用,不得覆盖参考图原有语义、版式和图文关系。", + shouldApplyPromptPack + ? "可被替换重建的内容包括但不限于:背景照片、产品展示照片、墙面、地面材质、沙发、床、背景柜、窗帘、灯具、地毯、挂画、摆件、桌面道具、空间色调、环境光氛围。" + : "可被替换的内容仅限参考图里明确的产品/视觉图片区;不要新增不属于参考图语义的空间、人物、道具、主题和文案。", params.productImageCount > 1 ? `前面的 ${params.productImageCount} 张商品输入图都属于同一产品的多角度/多细节白底图,请综合这些图片理解产品结构、材质、颜色、比例、边角、开孔、五金与细节,不可只参考第一张。` : "前面的商品输入图就是当前要替换进去的产品白底图,请准确保持该产品的结构、材质、颜色、比例和细节。", - "请先识别最后一张参考图中需要被替换的背景图/场景图区域,并在同一版式骨架下生成新的背景画面;不要只做局部抠图式换产品。", - "请严格保留最后一张参考图的版式结构、镜头关系、构图、文字排版、UI图标、标签、按钮、留白位置和信息层级;但旧背景图里的家具、背景物件、材质、灯光氛围、软装小物、空间质感需要按第一步工作流设定进行明显重建。", - "如果第一步工作流风格与参考图原场景冲突,必须保留版式骨架和UI层,但放弃原背景图物件本身,以第一步工作流风格为准。", + shouldApplyPromptPack + ? "请先识别最后一张参考图中需要被替换的背景图/场景图区域,并在同一版式骨架下生成新的背景画面;不要只做局部抠图式换产品。" + : "请先识别最后一张参考图中真正允许替换的视觉区;如果只是白底说明、强文案页、多人关系图或图文匹配强的内容,不要强行重建整张场景。", + shouldApplyPromptPack + ? "请严格保留最后一张参考图的版式结构、镜头关系、构图、文字排版、UI图标、标签、按钮、留白位置和信息层级;但旧背景图里的家具、背景物件、材质、灯光氛围、软装小物、空间质感需要按第一步工作流设定进行明显重建。" + : "请严格保留最后一张参考图的版式结构、镜头关系、构图、文字排版、UI图标、标签、按钮、留白位置、人物关系和信息层级;不要因为前置词包改变这些内容。", + shouldApplyPromptPack + ? "如果第一步工作流风格与参考图原场景冲突,必须保留版式骨架和UI层,但放弃原背景图物件本身,以第一步工作流风格为准。" + : "如果第一步工作流风格与参考图原场景冲突,必须保留参考图语义与版式,不要放弃原有图文关系。", "前面的输入商品图用于提供商品主体、材质、颜色和细节特征;如有模特图,请只在参考图本身包含人物展示时再参考模特图。", "如果提供了模特图且这一屏确实有人物展示需求,必须尽量复用同一人物身份与体态关系,不要明显换脸、换发型、换体型,服装风格也尽量保持一致。", "绝对不要忽略最后一张参考图去生成一张无关的新场景图。", - "如果生成结果仍然保留了参考图原有的大部分背景图物件,只是把产品换掉,则视为执行失败;你必须让背景图/场景图发生清晰可见的变化。", + shouldApplyPromptPack + ? "如果生成结果仍然保留了参考图原有的大部分背景图物件,只是把产品换掉,则视为执行失败;你必须让背景图/场景图发生清晰可见的变化。" + : "如果本屏已禁用词包,生成结果不应追求明显改造背景,而应追求图文匹配、文案稳定、人物关系稳定和局部替换自然。", "绝对不要移动、删除、改写、重排参考图里的任何文字、UI图标、按钮、标签、装饰元素与排版元素。", "输出结果必须保持最后一张参考图底板的原始纵横比与画布尺寸,不允许擅自改成长图、横图或裁掉任何版面区域。", "如果参考图中没有可替换/重建的背景图、产品场景图或视觉图片区,应直接沿用参考图,不要硬生成。", @@ -1133,17 +1523,16 @@ export function buildAgentReplacementPrompt(params: { ]; const instructionLines = - params.modeType === "detail_replacement" + (params.modeType === "detail_replacement" ? productDetailInstructionLines - : skuInstructionLines; + : skuInstructionLines).filter(Boolean); if (params.hasModelImage) { instructionLines.push("如有模特图,请在需要人物展示时保持模特体态、穿搭和出镜关系一致。"); } return [ - params.modeType === "detail_replacement" && - params.replacementStrategy === "product_only" + params.modeType === "detail_replacement" && !shouldApplyPromptPack ? "" : params.basePrompt, instructionLines.join("\n"), diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 09866011c..3bb818aae 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -3398,6 +3398,19 @@ export async function POST(request: Request) { planningRuntime: replacementScenePlanningRuntime, models: replacementVisionModels, }); + chatApiLogger.info("[Chat API] Direct detail replacement reference decision:", { + referenceIndex: referenceIndex + 1, + totalReferences: replacementReferences.length, + referenceLabel: reference.label, + shouldReplace: replacementExecutionPlan.shouldReplace, + reason: replacementExecutionPlan.reason, + detectionSource: replacementExecutionPlan.detectionSource, + scenePlanSource: replacementExecutionPlan.scenePlan.analysisSource, + generationRoute: replacementExecutionPlan.scenePlan.generationRoute, + promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, + textPolicy: replacementExecutionPlan.scenePlan.textPolicy, + peoplePolicy: replacementExecutionPlan.scenePlan.peoplePolicy, + }); const referenceSourceImageUrls = Array.from( new Set([ ...productInputImageUrls, @@ -4117,6 +4130,10 @@ export async function POST(request: Request) { detectionModelName: replacementExecutionPlan.detectionModelName, detectionSource: replacementExecutionPlan.detectionSource, scenePlanSource: replacementExecutionPlan.scenePlan.analysisSource, + generationRoute: replacementExecutionPlan.scenePlan.generationRoute, + promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, + textPolicy: replacementExecutionPlan.scenePlan.textPolicy, + peoplePolicy: replacementExecutionPlan.scenePlan.peoplePolicy, selectedProductImageIndexes: replacementExecutionPlan.scenePlan.selectedProductImageIndexes, useModelImage: replacementExecutionPlan.scenePlan.useModelImage, @@ -4133,6 +4150,9 @@ export async function POST(request: Request) { replacementExecutionPlan.scenePlan.productSelectionReason, useModelImage: replacementExecutionPlan.scenePlan.useModelImage, modelImageReason: replacementExecutionPlan.scenePlan.modelImageReason, + generationRoute: replacementExecutionPlan.scenePlan.generationRoute, + promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, + routeReason: replacementExecutionPlan.scenePlan.routeReason, }); const referenceSourceImageUrls = Array.from( new Set([ diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index de499049d..13086d339 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -773,28 +773,6 @@ function buildReferenceCardsFromCandidate(params: { })); } -function readLocalImageDimensions(file: File): Promise<{ width: number | null; height: number | null }> { - if (typeof window === "undefined") { - return Promise.resolve({ width: null, height: null }); - } - - return new Promise((resolve) => { - const objectUrl = URL.createObjectURL(file); - const image = new Image(); - const finish = (width: number | null, height: number | null) => { - URL.revokeObjectURL(objectUrl); - resolve({ width, height }); - }; - image.onload = () => { - finish(image.naturalWidth || null, image.naturalHeight || null); - }; - image.onerror = () => { - finish(null, null); - }; - image.src = objectUrl; - }); -} - function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: AgentTaskDetailReferenceSource) { const rawImageCount = target === "main" @@ -880,6 +858,19 @@ function getCandidateTargetCount( return rawCount; } +const IMAGE_OVERLAY_BADGE_CLASS = + "rounded-full border border-white/25 bg-zinc-950/82 px-3 py-1.5 font-semibold text-[11px] text-white shadow-[0_12px_34px_rgba(0,0,0,0.48)] backdrop-blur-md ring-1 ring-black/20"; +const IMAGE_OVERLAY_ACTIVE_BADGE_CLASS = + "rounded-full border border-emerald-100/80 bg-emerald-600/92 px-3 py-1.5 font-semibold text-[11px] text-white shadow-[0_12px_34px_rgba(16,185,129,0.30),0_10px_28px_rgba(0,0,0,0.48)] backdrop-blur-md"; +const IMAGE_OVERLAY_STATUS_BASE_CLASS = + "rounded-full border px-3 py-1.5 font-semibold text-[11px] shadow-[0_12px_34px_rgba(0,0,0,0.50)] backdrop-blur-md ring-1 ring-black/20"; +const IMAGE_OVERLAY_ACTION_CLASS = + "rounded-full border border-white/25 bg-zinc-950/82 px-3.5 py-1.5 font-semibold text-[11px] text-white shadow-[0_12px_34px_rgba(0,0,0,0.48)] backdrop-blur-md transition-colors hover:border-emerald-200/70 hover:bg-zinc-900/92"; +const IMAGE_OVERLAY_DANGER_TEXT_CLASS = + "rounded-full border border-rose-200/55 bg-zinc-950/82 px-3 py-1.5 font-semibold text-[11px] text-rose-50 shadow-[0_12px_34px_rgba(0,0,0,0.48)] backdrop-blur-md transition-colors hover:border-rose-100 hover:bg-rose-500/30"; +const IMAGE_OVERLAY_DANGER_ICON_CLASS = + "absolute right-3 top-3 flex h-9 w-9 items-center justify-center rounded-full border border-rose-200/55 bg-zinc-950/82 text-rose-50 shadow-[0_12px_34px_rgba(0,0,0,0.50)] backdrop-blur-md transition-colors hover:border-rose-100 hover:bg-rose-500/35"; + type ReplacementCardStatus = | "waiting" | "queued" @@ -998,37 +989,37 @@ function getReplacementCardStatusMeta(status: ReplacementCardStatus) { return { label: "不处理直出", className: - "border-cyan-200/70 bg-cyan-500/85 text-white shadow-[0_8px_24px_rgba(6,182,212,0.28)]", + "border-cyan-100/80 bg-cyan-600/92 text-white", }; case "fallback": return { label: "失败已沿用", - className: "border-amber-400/30 bg-amber-500/[0.10] text-amber-200", + className: "border-amber-100/80 bg-amber-500/92 text-zinc-950", }; case "completed": return { label: "已生成", - className: "border-emerald-400/30 bg-emerald-500/[0.10] text-emerald-200", + className: "border-emerald-100/80 bg-emerald-600/92 text-white", }; case "generating": return { label: "生成中", - className: "border-sky-400/30 bg-sky-500/[0.10] text-sky-200", + className: "border-sky-100/80 bg-sky-600/92 text-white", }; case "queued": return { label: "排队中", - className: "border-white/12 bg-white/[0.05] text-white/60", + className: "border-white/25 bg-zinc-950/82 text-white", }; case "missing": return { label: "未回传", - className: "border-amber-400/30 bg-amber-500/[0.10] text-amber-200", + className: "border-amber-100/80 bg-amber-500/92 text-zinc-950", }; default: return { label: "待生成", - className: "border-white/12 bg-white/[0.05] text-white/60", + className: "border-white/25 bg-zinc-950/82 text-white", }; } } @@ -1331,14 +1322,6 @@ export function AgentGenerationWorkspaceDarkV2({ isLoading: false, lastQuery: null, }); - const detailReferenceUploadInputRef = useRef(null); - const [detailReferenceUploadState, setDetailReferenceUploadState] = useState<{ - isUploading: boolean; - count: number; - }>({ - isUploading: false, - count: 0, - }); const [detailReferenceStitchDialogOpen, setDetailReferenceStitchDialogOpen] = useState(false); const [activeDetailReferenceCardId, setActiveDetailReferenceCardId] = useState(null); const [skuReplacementSearchState, setSkuReplacementSearchState] = useState<{ @@ -4868,108 +4851,6 @@ export function AgentGenerationWorkspaceDarkV2({ }, [updateDetailReplacementState] ); - const handleDetailReferenceUploadChange = useCallback( - async (event: React.ChangeEvent) => { - const inputElement = event.currentTarget; - const rawFiles = Array.from(inputElement.files || []); - const imageFiles = rawFiles.filter((file) => file.type.startsWith("image/")); - if (rawFiles.length > 0 && imageFiles.length === 0) { - toast.error("请上传图片文件"); - inputElement.value = ""; - return; - } - - const availableSlotCount = Math.max( - MAX_AGENT_MODE_REFERENCE_COUNT - detailReplacementReferenceCards.length, - 0 - ); - if (availableSlotCount <= 0) { - toast.error(`最多支持 ${MAX_AGENT_MODE_REFERENCE_COUNT} 张参考图,请先删除部分图片`); - inputElement.value = ""; - return; - } - - const uploadFiles = imageFiles.slice(0, availableSlotCount); - if (imageFiles.length > uploadFiles.length) { - toast.warning(`已达到参考图上限,本次只上传前 ${uploadFiles.length} 张`); - } - if (uploadFiles.length === 0) { - inputElement.value = ""; - return; - } - - setDetailReferenceUploadState({ - isUploading: true, - count: uploadFiles.length, - }); - - try { - const uploadedItems = await Promise.all( - uploadFiles.map(async (file, index) => { - const dimensions = await readLocalImageDimensions(file); - const formData = new FormData(); - formData.append("file", file); - const response = await fetchUploadWithRetry(formData); - const data = await response.json().catch(() => ({})); - if (!response.ok || !data?.url) { - throw new Error(data?.error || `${file.name || `第 ${index + 1} 张`}上传失败`); - } - return { - index, - file, - url: String(data.url), - previewUrl: String(data.previewUrl || data.thumbnailUrl || data.url || ""), - width: dimensions.width, - height: dimensions.height, - }; - }) - ); - const timestamp = Date.now(); - const orderedItems = uploadedItems.slice().sort((left, right) => left.index - right.index); - - updateDetailReplacementState((current) => { - const baseLength = current.references.length; - const nextReferences = [ - ...current.references, - ...orderedItems.map((item, index) => ({ - id: `detail-upload-${timestamp}-${index + 1}-${generateUUID().slice(0, 8)}`, - label: item.file.name - ? `上传详情图 ${baseLength + index + 1} · ${item.file.name}` - : `上传详情图 ${baseLength + index + 1}`, - imageUrl: item.url, - previewUrl: item.previewUrl || item.url, - width: item.width, - height: item.height, - candidateId: null, - sortOrder: baseLength + index, - passthrough: false, - assignedProductIndex: null, - })), - ] - .slice(0, MAX_AGENT_MODE_REFERENCE_COUNT) - .map((card, index) => ({ - ...card, - sortOrder: index, - })); - - return { - ...current, - references: nextReferences, - }; - }); - toast.success(`已按文件选择顺序上传 ${orderedItems.length} 张参考详情图`); - } catch (error) { - toast.error(error instanceof Error ? error.message : "上传参考详情图失败"); - } finally { - setDetailReferenceUploadState({ - isUploading: false, - count: 0, - }); - inputElement.value = ""; - } - }, - [detailReplacementReferenceCards.length, updateDetailReplacementState] - ); const handleDetailReplacementSearch = useCallback(async ( queryType: Extract ) => { @@ -6249,14 +6130,6 @@ export function AgentGenerationWorkspaceDarkV2({
-
@@ -6321,27 +6194,8 @@ export function AgentGenerationWorkspaceDarkV2({
-
- - {detailReplacementReferenceCards.length > 0 ? ( + {detailReplacementReferenceCards.length > 0 ? ( +
- ) : null} - {detailReplacementReferenceCards.length > 0 ? ( - ) : null} -
+
+ ) : null} {(detailReplacementSearchState.lastQuery || detailReplacementQueryInput.trim()) ? (
@@ -6826,7 +6678,7 @@ export function AgentGenerationWorkspaceDarkV2({ event.stopPropagation(); handleRemoveDetailReferenceCard(card.id); }} - className="rounded-full border border-white/10 bg-black/40 px-2 py-1 text-[11px] text-white/70 transition-colors hover:border-white/20 hover:text-white" + className={IMAGE_OVERLAY_DANGER_TEXT_CLASS} > 删除 @@ -6837,10 +6689,10 @@ export function AgentGenerationWorkspaceDarkV2({ handleToggleDetailReferenceCardPassthrough(card.id); }} className={cn( - "inline-flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-[12px] font-semibold backdrop-blur-md shadow-[0_10px_28px_rgba(0,0,0,0.35)] transition-colors", + "inline-flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 font-semibold text-[12px] backdrop-blur-md shadow-[0_12px_34px_rgba(0,0,0,0.50)] transition-colors", card.passthrough - ? "border-emerald-200 bg-emerald-500 text-white shadow-[0_10px_28px_rgba(16,185,129,0.36)]" - : "border-emerald-300/85 bg-black/72 text-emerald-50 hover:border-emerald-200 hover:bg-emerald-500/22" + ? "border-emerald-100/85 bg-emerald-600/92 text-white shadow-[0_12px_34px_rgba(16,185,129,0.34),0_10px_28px_rgba(0,0,0,0.45)]" + : "border-emerald-100/70 bg-zinc-950/82 text-emerald-50 hover:border-emerald-50 hover:bg-emerald-700/80" )} > @@ -6859,18 +6711,19 @@ export function AgentGenerationWorkspaceDarkV2({ } /> ) : null} -
+
参考图 {index + 1}
{card.passthrough ? ( -
+
不处理直出
) : null} {isActive ? (
@@ -6955,11 +6808,11 @@ export function AgentGenerationWorkspaceDarkV2({ setFullscreenPreview(card.resultUrl!); }} /> -
+
{statusMeta.label}
{isActive ? ( -
+
当前联动
) : null} @@ -6968,7 +6821,7 @@ export function AgentGenerationWorkspaceDarkV2({ onClick={() => handleDeleteResultImage("detail_replacement", card.resultUrl!) } - className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full border border-rose-400/25 bg-black/60 text-rose-100 transition-colors hover:border-rose-300/45 hover:bg-rose-500/15" + className={IMAGE_OVERLAY_DANGER_ICON_CLASS} aria-label="删除结果图" > @@ -6978,7 +6831,7 @@ export function AgentGenerationWorkspaceDarkV2({ onClick={() => { void handleDownloadOriginalImage(card.resultUrl!); }} - className="absolute bottom-3 right-3 rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20" + className={cn("absolute bottom-3 right-3", IMAGE_OVERLAY_ACTION_CLASS)} > 下载 @@ -6993,7 +6846,7 @@ export function AgentGenerationWorkspaceDarkV2({ title: `${selectedAgentPreset?.name || "参考图替换"} 结果图 ${card.index + 1}`, }) } - className="rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20" + className={IMAGE_OVERLAY_ACTION_CLASS} > 当前页改字 @@ -7009,7 +6862,7 @@ export function AgentGenerationWorkspaceDarkV2({ }); setReplacementRerunPrompt(""); }} - className="rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20 disabled:cursor-not-allowed disabled:opacity-40" + className={cn(IMAGE_OVERLAY_ACTION_CLASS, "disabled:cursor-not-allowed disabled:opacity-45")} > 重新生图 @@ -7352,7 +7205,7 @@ export function AgentGenerationWorkspaceDarkV2({ @@ -7368,11 +7221,11 @@ export function AgentGenerationWorkspaceDarkV2({ } /> ) : null} -
+
参考图 {index + 1}
{activeSkuReferenceCardId === card.id ? ( -
+
当前联动
) : null} @@ -7482,7 +7335,7 @@ export function AgentGenerationWorkspaceDarkV2({ setFullscreenPreview(card.resultUrl!); }} /> -
+
{statusMeta.label}
@@ -7515,7 +7368,7 @@ export function AgentGenerationWorkspaceDarkV2({ title: `${selectedAgentPreset?.name || "SKU替换"} 结果图 ${card.index + 1}`, }) } - className="rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20" + className={IMAGE_OVERLAY_ACTION_CLASS} > 当前页改字 @@ -7531,7 +7384,7 @@ export function AgentGenerationWorkspaceDarkV2({ }); setReplacementRerunPrompt(""); }} - className="rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20 disabled:cursor-not-allowed disabled:opacity-40" + className={cn(IMAGE_OVERLAY_ACTION_CLASS, "disabled:cursor-not-allowed disabled:opacity-45")} > 重新生图 @@ -7648,7 +7501,7 @@ export function AgentGenerationWorkspaceDarkV2({ setSelectedResultUrl(card.url!); setSceneResizeEditorTargetUrl(card.url!); }} - className="absolute left-3 top-3 flex h-9 w-9 items-center justify-center rounded-full border border-emerald-400/28 bg-black/60 text-emerald-100 transition-colors hover:border-emerald-300/45 hover:bg-emerald-500/[0.16]" + className={cn("absolute left-3 top-3 flex h-9 w-9 items-center justify-center", IMAGE_OVERLAY_ACTIVE_BADGE_CLASS, "px-0 py-0")} aria-label="调整尺寸" > @@ -7664,14 +7517,14 @@ export function AgentGenerationWorkspaceDarkV2({ mode: getPsdTemplateModeByResultMode(activeResultModeType), }); }} - className="absolute left-14 top-3 flex items-center gap-1 rounded-full border border-white/10 bg-black/60 px-2.5 py-1 text-[11px] text-white transition-colors hover:border-white/20" + className={cn("absolute left-14 top-3 flex items-center gap-1", IMAGE_OVERLAY_ACTION_CLASS)} aria-label="套用 PSD 模板" > 套PSD ) : null} -
+
双击查看大图
@@ -7687,7 +7540,7 @@ export function AgentGenerationWorkspaceDarkV2({ mode: getPsdTemplateModeByResultMode(activeResultModeType), }); }} - className="absolute left-3 top-3 flex items-center gap-1 rounded-full border border-white/10 bg-black/60 px-2.5 py-1 text-[11px] text-white transition-colors hover:border-white/20" + className={cn("absolute left-3 top-3 flex items-center gap-1", IMAGE_OVERLAY_ACTION_CLASS)} aria-label="套用 PSD 模板" > @@ -7700,7 +7553,7 @@ export function AgentGenerationWorkspaceDarkV2({ event.stopPropagation(); handleDeleteResultImage(activeResultModeType, card.url!); }} - className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full border border-rose-400/25 bg-black/60 text-rose-100 transition-colors hover:border-rose-300/45 hover:bg-rose-500/15" + className={IMAGE_OVERLAY_DANGER_ICON_CLASS} aria-label="删除结果图" > @@ -7713,7 +7566,7 @@ export function AgentGenerationWorkspaceDarkV2({ event.stopPropagation(); void handleDownloadOriginalImage(card.url!); }} - className="absolute bottom-3 right-3 rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20" + className={cn("absolute bottom-3 right-3", IMAGE_OVERLAY_ACTION_CLASS)} > 下载 @@ -7726,7 +7579,7 @@ export function AgentGenerationWorkspaceDarkV2({ title: `AI详情 ${card.index + 1}`, }); }} - className="absolute bottom-3 left-3 rounded-full border border-white/10 bg-black/55 px-3 py-1 text-[11px] text-white transition-colors hover:border-white/20" + className={cn("absolute bottom-3 left-3", IMAGE_OVERLAY_ACTION_CLASS)} > 送入画布改字 diff --git a/components/home/floating-sidebar-shell.tsx b/components/home/floating-sidebar-shell.tsx index e4a43db90..78fd0a359 100644 --- a/components/home/floating-sidebar-shell.tsx +++ b/components/home/floating-sidebar-shell.tsx @@ -122,7 +122,7 @@ export function FloatingSidebarRail({ : "border border-white/8 bg-[#17181d]/94 text-white shadow-[0_22px_60px_rgba(6,8,14,0.34),inset_0_1px_0_rgba(255,255,255,0.05)]", isHorizontal ? "h-[56px] w-auto flex-row overflow-hidden rounded-[22px] px-3 py-2" - : "w-[56px] flex-col overflow-x-hidden overflow-y-auto overscroll-contain rounded-[24px] px-2 py-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", + : "w-[56px] max-w-[56px] min-w-0 flex-col overflow-x-hidden overflow-y-auto overscroll-contain rounded-[24px] px-2 py-2 [overflow-x:clip] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", className )} > @@ -153,7 +153,7 @@ export function FloatingSidebarAction({