feat detail mode: optional bundle images and auto add cross-sell showcase screen
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-19 12:26:11 +08:00
parent 9d2b21e1ef
commit 9af7619b7f
3 changed files with 163 additions and 28 deletions

View File

@ -17,6 +17,7 @@ type DetailPlanRequest = {
referenceImages?: string[];
referenceImageNotes?: string[];
modelImageUrl?: string;
bundleImageUrls?: string[];
brief?: {
productName?: string;
targetAudience?: string;
@ -66,6 +67,8 @@ type DetailPlanScreen = {
layoutNote: string;
useModel: boolean;
modelReason: string;
bundleShowcase: boolean;
bundleReason: string;
visualPrompt: string;
fabe: {
b: string;
@ -146,7 +149,7 @@ function buildDetailSkillFlow(language: "zh" | "en") {
function clampCount(value: unknown): number {
const num = Number(value);
if (!Number.isFinite(num)) return 5;
return Math.min(12, Math.max(5, Math.round(num)));
return Math.min(13, Math.max(5, Math.round(num)));
}
function sanitizeReferenceImages(images: unknown): string[] {
@ -154,7 +157,7 @@ function sanitizeReferenceImages(images: unknown): string[] {
return images
.map((item) => String(item || "").trim())
.filter((url) => /^https?:\/\//i.test(url))
.slice(0, 6);
.slice(0, 12);
}
function sanitizeReferenceImageNotes(notes: unknown): string[] {
@ -162,7 +165,15 @@ function sanitizeReferenceImageNotes(notes: unknown): string[] {
return notes
.map((item) => String(item || "").replace(/\s+/g, " ").trim())
.filter(Boolean)
.slice(0, 6);
.slice(0, 12);
}
function sanitizeBundleImages(images: unknown): string[] {
if (!Array.isArray(images)) return [];
return images
.map((item) => String(item || "").trim())
.filter((url) => /^https?:\/\//i.test(url))
.slice(0, 3);
}
function normalizeOutputLanguage(value: unknown): "zh" | "en" {
@ -289,8 +300,20 @@ function buildFallbackScreens(params: {
globalStyle: DetailPlanResponse["globalStyle"];
referenceImageNotes: string[];
hasModelImage: boolean;
hasBundleImages: boolean;
bundleImageCount: number;
}): DetailPlanScreen[] {
const { prompt, count, language, brief, globalStyle, referenceImageNotes, hasModelImage } = params;
const {
prompt,
count,
language,
brief,
globalStyle,
referenceImageNotes,
hasModelImage,
hasBundleImages,
bundleImageCount,
} = params;
const productLabel =
brief.productName || (language === "en" ? "the product" : "该产品");
const keyword =
@ -326,23 +349,40 @@ function buildFallbackScreens(params: {
(language === "en"
? `Extended detail frame ${index + 1}`
: `延展分镜 ${index + 1}`);
const isBundleScreen = hasBundleImages && index === count - 1;
const title =
language === "en"
isBundleScreen
? language === "en"
? "Cross-sell Bundle Scene"
: "关联销售搭配场景"
: language === "en"
? `${keyword} Frame ${index + 1}`
: `${keyword} 第${index + 1}屏`;
const subtitle =
language === "en"
isBundleScreen
? language === "en"
? `Show product pairing with ${bundleImageCount} optional companion SKU(s) to increase basket size.`
: `联动${bundleImageCount}张搭配产品参考图,强化关联销售与加购动机。`
: language === "en"
? `For ${brief.targetAudience}, solve "${coreNeed}" in ${mainScenario}.`
: `面向${brief.targetAudience},在${mainScenario}场景解决“${coreNeed}”。`;
const copy =
language === "en"
isBundleScreen
? language === "en"
? `${productLabel} is presented with matching companion products in one coherent scene to drive add-on purchase.`
: `${productLabel}与可搭配产品同场景呈现,突出组合价值与加购理由。`
: language === "en"
? `${productLabel} turns key pain points into visible outcomes with a clear decision path.`
: `${productLabel}将关键痛点转为可感知结果,形成清晰购买决策路径。`;
const layoutNote =
language === "en"
isBundleScreen
? language === "en"
? "Main product remains primary; companion products are secondary with clear hierarchy and no visual clutter."
: "主产品为视觉主角,搭配产品作为辅助,层级清晰避免喧宾夺主。"
: language === "en"
? `Use fixed layout rhythm. Keep title/subtitle/body hierarchy identical to all other frames.`
: `使用固定版式节奏,标题/副标题/正文层级与其余页面完全一致。`;
const useModel = hasModelImage && (index === 1 || index === 3 || index >= 5);
const useModel = !isBundleScreen && hasModelImage && (index === 1 || index === 3 || index >= 5);
const modelReason = useModel
? language === "en"
? "This frame needs human scenario demonstration, so use the fixed model image."
@ -350,6 +390,14 @@ function buildFallbackScreens(params: {
: language === "en"
? "This frame focuses on product-only communication without human figure."
: "本屏聚焦产品信息展示,不使用人物。";
const bundleShowcase = isBundleScreen;
const bundleReason = bundleShowcase
? language === "en"
? "Optional bundle references were provided; this frame must show scenario-based product pairing for cross-sell."
: "用户提供了搭配产品图,本屏必须展示场景化产品搭配以增强关联销售。"
: language === "en"
? "This frame is not the bundle showcase frame."
: "本屏非搭配展示页。";
const fabe =
language === "en"
? {
@ -380,6 +428,9 @@ function buildFallbackScreens(params: {
referenceImageNotes.length > 0
? `Reference image set roles: ${referenceImageNotes.join(" | ")}. Keep product identity and details fully consistent.`
: "",
bundleShowcase
? `This must be a bundle showcase frame: show the hero product with companion products from optional bundle refs in one coherent scenario.`
: "",
useModel
? "Integrate the same human model from the uploaded model image."
: "No human figure in this frame.",
@ -399,6 +450,9 @@ function buildFallbackScreens(params: {
referenceImageNotes.length > 0
? `参考图角色:${referenceImageNotes.join(";")};必须严格保持产品一致性与细节一致性。`
: "",
bundleShowcase
? "本屏必须是搭配展示页:同屏展示主产品与搭配产品,体现组合使用场景与加购价值。"
: "",
useModel ? "本屏融入固定模特图中的同一人物。" : "本屏不要出现人物。",
"商业摄影质感,留出清晰文案排版区,不要水印。",
`任务上下文:${prompt}。`,
@ -414,6 +468,8 @@ function buildFallbackScreens(params: {
layoutNote,
useModel,
modelReason,
bundleShowcase,
bundleReason,
visualPrompt,
fabe,
};
@ -427,6 +483,8 @@ function buildFallbackPlan(params: {
brief: DetailBrief;
referenceImageNotes: string[];
hasModelImage: boolean;
hasBundleImages: boolean;
bundleImageCount: number;
}): DetailPlanResponse {
const { count, language } = params;
const globalStyle = buildDefaultGlobalStyle(language);
@ -438,6 +496,7 @@ function buildFallbackPlan(params: {
const qualityChecks = evaluatePlanQuality({
screens,
hasModelImage: params.hasModelImage,
hasBundleImages: params.hasBundleImages,
});
return {
summary:
@ -484,6 +543,11 @@ function sanitizeScreen(
useModel:
typeof source.useModel === "boolean" ? source.useModel : fallback.useModel,
modelReason: safeText(source.modelReason, fallback.modelReason),
bundleShowcase:
typeof source.bundleShowcase === "boolean"
? source.bundleShowcase
: fallback.bundleShowcase,
bundleReason: safeText(source.bundleReason, fallback.bundleReason),
visualPrompt: visualPrompt.includes("790x1300")
? visualPrompt
: `${visualPrompt} 画布固定 790x1300 像素。`,
@ -537,8 +601,9 @@ function sanitizeGlobalStyle(
function evaluatePlanQuality(params: {
screens: DetailPlanScreen[];
hasModelImage: boolean;
hasBundleImages: boolean;
}): { passed: boolean; issues: string[] } {
const { screens, hasModelImage } = params;
const { screens, hasModelImage, hasBundleImages } = params;
const issues: string[] = [];
if (screens.length === 0) {
issues.push("未生成任何分镜。");
@ -591,6 +656,13 @@ function evaluatePlanQuality(params: {
}
}
if (hasBundleImages) {
const bundleScreenCount = screens.filter((screen) => screen.bundleShowcase).length;
if (bundleScreenCount === 0) {
issues.push("已提供搭配产品图但没有搭配展示分镜。");
}
}
return {
passed: issues.length === 0,
issues,
@ -610,11 +682,14 @@ export async function POST(request: Request) {
return NextResponse.json({ success: false, error: "请输入详情需求" }, { status: 400 });
}
const count = clampCount(body.count);
const baseCount = clampCount(body.count);
const referenceImages = sanitizeReferenceImages(body.referenceImages);
const referenceImageNotes = sanitizeReferenceImageNotes(body.referenceImageNotes);
const modelImageUrl = String(body.modelImageUrl || "").trim();
const bundleImageUrls = sanitizeBundleImages(body.bundleImageUrls);
const hasModelImage = /^https?:\/\//i.test(modelImageUrl);
const hasBundleImages = bundleImageUrls.length > 0;
const count = hasBundleImages ? Math.min(13, baseCount + 1) : baseCount;
const language = normalizeOutputLanguage(body.outputLanguage);
const brief = normalizeDetailBrief(body.brief);
if (!brief.targetAudience || !brief.usageScenario || !brief.coreNeed) {
@ -633,6 +708,8 @@ export async function POST(request: Request) {
brief,
referenceImageNotes,
hasModelImage,
hasBundleImages,
bundleImageCount: bundleImageUrls.length,
});
const allModels = await getEnabledAiModelsWithProvider(false);
@ -680,6 +757,9 @@ export async function POST(request: Request) {
? `参考图说明(按上传顺序):${referenceImageNotes.join(" | ")}`
: "",
hasModelImage ? "已提供固定模特图,人物场景必须复用该模特。" : "未提供模特图。",
hasBundleImages
? `已提供 ${bundleImageUrls.length} 张搭配产品图,必须增加1屏场景化搭配展示用于关联销售。`
: "未提供搭配产品图。",
referenceImages.length > 0 ? `提供了 ${referenceImages.length} 张参考产品图。` : "本轮未提供参考图。",
]
.filter(Boolean)
@ -719,8 +799,11 @@ export async function POST(request: Request) {
(hasModelImage
? `10) 已提供模特图:涉及人物场景时必须复用同一模特(人脸、发型、体型、肤色与服装风格一致),不得替换其他人物。\n`
: `10) 未提供模特图:所有分镜均设置 useModel=false。\n`) +
(hasBundleImages
? `11) 已提供搭配产品图:必须新增且仅新增1屏“场景化产品搭配展示”,该屏设定 bundleShowcase=true,并在 bundleReason 说明搭配策略;其余屏设为 bundleShowcase=false。\n`
: `11) 未提供搭配产品图:所有分镜均设置 bundleShowcase=false。\n`) +
`输出格式:\n` +
`{"summary":"简述","globalStyle":{"designLanguage":"一句话风格方向","typography":{"fontFamily":"字体家族","title":"主标题规范","subtitle":"副标题规范","body":"正文规范"},"layout":{"grid":"栅格","safeArea":"安全区","rhythm":"留白节奏"},"palette":"配色规范"},"screens":[{"index":1,"screenType":"B","objective":"本屏目标","title":"4-8字标题","subtitle":"一句副标题","copy":"正文文案","layoutNote":"排版说明","useModel":false,"modelReason":"是否需要人物及原因","fabe":{"b":"利益","f":"特征","a":"优势","e":"证据"},"visualPrompt":"用于生图的完整提示词,必须包含790x1300与统一字体排版"}]}`,
`{"summary":"简述","globalStyle":{"designLanguage":"一句话风格方向","typography":{"fontFamily":"字体家族","title":"主标题规范","subtitle":"副标题规范","body":"正文规范"},"layout":{"grid":"栅格","safeArea":"安全区","rhythm":"留白节奏"},"palette":"配色规范"},"screens":[{"index":1,"screenType":"B","objective":"本屏目标","title":"4-8字标题","subtitle":"一句副标题","copy":"正文文案","layoutNote":"排版说明","useModel":false,"modelReason":"是否需要人物及原因","bundleShowcase":false,"bundleReason":"是否为搭配展示页及原因","fabe":{"b":"利益","f":"特征","a":"优势","e":"证据"},"visualPrompt":"用于生图的完整提示词,必须包含790x1300与统一字体排版"}]}`,
temperature: 0.25,
maxTokens: 1800,
} as any);
@ -736,11 +819,21 @@ export async function POST(request: Request) {
modelReason: "未提供模特图,当前分镜不使用人物。",
}
);
const normalizedScreens = rawScreens.map((item) =>
hasBundleImages
? item
: {
...item,
bundleShowcase: false,
bundleReason: "未提供搭配产品图,当前分镜非搭配展示页。",
}
);
const qualityChecks = evaluatePlanQuality({
screens: rawScreens,
screens: normalizedScreens,
hasModelImage,
hasBundleImages,
});
const screens = qualityChecks.passed ? rawScreens : fallback.screens;
const screens = qualityChecks.passed ? normalizedScreens : fallback.screens;
const prompts = screens.map((item) => item.visualPrompt);
const summaryRaw = String(parsed?.summary || "").trim();
const summary = qualityChecks.passed

View File

@ -118,7 +118,7 @@ export async function POST(request: Request) {
.map((item) => String(item || "").trim())
.filter((url) => /^https?:\/\//i.test(url))
)
).slice(0, 6);
).slice(0, 12);
// 构建消息
const messages = [{

View File

@ -305,7 +305,7 @@ type DetailBriefForm = {
type DetailSlotConfig = {
key: string;
label: string;
role: "product" | "model";
role: "product" | "model" | "bundle";
};
type DetailSectionKey = "required" | "strategy" | "assets";
@ -342,6 +342,8 @@ type DetailPlanResponse = {
layoutNote?: string;
useModel?: boolean;
modelReason?: string;
bundleShowcase?: boolean;
bundleReason?: string;
visualPrompt?: string;
fabe?: {
b?: string;
@ -375,7 +377,17 @@ const DETAIL_SLOT_CONFIGS: DetailSlotConfig[] = [
{ key: "product-4", label: "产品图4", role: "product" },
{ key: "product-5", label: "产品图5", role: "product" },
{ key: "model-1", label: "模特图", role: "model" },
{ key: "bundle-1", label: "搭配图1", role: "bundle" },
{ key: "bundle-2", label: "搭配图2", role: "bundle" },
{ key: "bundle-3", label: "搭配图3", role: "bundle" },
];
const DETAIL_PRODUCT_SLOT_INDEXES = DETAIL_SLOT_CONFIGS.map((slot, index) =>
slot.role === "product" ? index : -1
).filter((index) => index >= 0);
const DETAIL_MODEL_SLOT_INDEX = DETAIL_SLOT_CONFIGS.findIndex((slot) => slot.role === "model");
const DETAIL_BUNDLE_SLOT_INDEXES = DETAIL_SLOT_CONFIGS.map((slot, index) =>
slot.role === "bundle" ? index : -1
).filter((index) => index >= 0);
const DETAIL_FIELD_EXAMPLES: Record<keyof DetailBriefForm, string> = {
productName: "示例:Buyi-AI 无线手持吸尘器",
targetAudience: "示例:25-40岁养宠家庭、宝妈",
@ -461,6 +473,8 @@ function buildDetailResultMarkdown(params: {
layoutNote?: string;
useModel?: boolean;
modelReason?: string;
bundleShowcase?: boolean;
bundleReason?: string;
fabe?: {
b?: string;
f?: string;
@ -526,6 +540,12 @@ function buildDetailResultMarkdown(params: {
if (screen?.modelReason) {
lines.push(`人物策略:${screen.modelReason}`);
}
if (screen?.bundleShowcase !== undefined) {
lines.push(screen.bundleShowcase ? "搭配展示:是" : "搭配展示:否");
}
if (screen?.bundleReason) {
lines.push(`搭配策略:${screen.bundleReason}`);
}
if (screen?.fabe) {
const fabeParts = [
screen.fabe.b ? `B ${screen.fabe.b}` : "",
@ -1467,9 +1487,11 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
if (!normalizedDetailBrief.targetAudience) issues.push("缺少主购买人群");
if (!normalizedDetailBrief.usageScenario) issues.push("缺少需求场景");
if (!normalizedDetailBrief.coreNeed) issues.push("缺少核心需求");
const productCount = detailSlots.slice(0, 5).filter((item) => Boolean(item?.url)).length;
const productCount = DETAIL_PRODUCT_SLOT_INDEXES.filter((index) => Boolean(detailSlots[index]?.url)).length;
if (productCount < 5) issues.push(`产品图不足(${productCount}/5)`);
if (!detailSlots[5]?.url) issues.push("缺少模特图(1/1)");
if (DETAIL_MODEL_SLOT_INDEX < 0 || !detailSlots[DETAIL_MODEL_SLOT_INDEX]?.url) {
issues.push("缺少模特图(1/1)");
}
return issues;
}, [detailSlots, normalizedDetailBrief]);
@ -2195,18 +2217,22 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
toast.error("请先补充 AI详情生成必填项:主购买人群、需求场景、核心需求");
return;
}
const productAttachments = detailSlots
.slice(0, 5)
const productAttachments = DETAIL_PRODUCT_SLOT_INDEXES
.map((index) => detailSlots[index])
.filter((item): item is PickedObjectAttachment => Boolean(item?.url));
if (productAttachments.length < 5) {
toast.error("请先上传 5 张产品图后再执行 AI详情生成");
return;
}
const modelAttachment = detailSlots[5];
const modelAttachment =
DETAIL_MODEL_SLOT_INDEX >= 0 ? detailSlots[DETAIL_MODEL_SLOT_INDEX] : null;
if (!modelAttachment?.url) {
toast.error("请上传 1 张人物模特图后再执行 AI详情生成");
return;
}
const bundleAttachments = DETAIL_BUNDLE_SLOT_INDEXES
.map((index) => detailSlots[index])
.filter((item): item is PickedObjectAttachment => Boolean(item?.url));
const userText = input
.replace(/\u200b/g, " ")
@ -2236,7 +2262,11 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
const current = detailSlots[index];
return `${index + 1}. ${config.label}${current?.name ? `(${current.name})` : ""}`;
});
const modelImageHint = `模特图固定策略:涉及人物场景时必须使用第6张模特图同一人物,不得替换人物。`;
const modelImageHint = `模特图固定策略:涉及人物场景时必须使用第${DETAIL_MODEL_SLOT_INDEX + 1}张模特图同一人物,不得替换人物。`;
const bundleImageHint =
bundleAttachments.length > 0
? `已上传${bundleAttachments.length}张搭配产品图:必须新增1屏场景化产品搭配展示,突出关联销售。`
: "未上传搭配产品图:不强制搭配展示屏。";
const detailBriefText = [
"【AI详情生成输入】",
brief.productName ? `产品:${brief.productName}` : "",
@ -2256,6 +2286,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
brief.bannedWords ? `禁用表达:${brief.bannedWords}` : "",
`参考图说明:${referenceImageNotes.join(";")}`,
modelImageHint,
bundleImageHint,
`输出规格:每屏 ${DETAIL_TARGET_WIDTH}x${DETAIL_TARGET_HEIGHT} 像素`,
]
.filter(Boolean)
@ -2320,9 +2351,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
body: JSON.stringify({
prompt: detailPrompt,
count: detailCount,
referenceImages: detailReferenceAttachments.map((item) => item.url).slice(0, 6),
referenceImages: detailReferenceAttachments.map((item) => item.url).slice(0, 12),
referenceImageNotes,
modelImageUrl: modelAttachment.url,
bundleImageUrls: bundleAttachments.map((item) => item.url),
outputLanguage: /[a-zA-Z]/.test(detailPrompt) ? "en" : "zh",
brief,
}),
@ -2366,6 +2398,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
for (let index = 0; index < prompts.length; index += 1) {
const screen = screenSpecs[index];
const useModelForScene = Boolean(screen?.useModel);
const bundleShowcase = Boolean((screen as any)?.bundleShowcase);
const bundleReason = String((screen as any)?.bundleReason || "").trim();
const promptText =
String(screen?.visualPrompt || "").trim() || prompts[index]!;
setDetailProgress({
@ -2418,6 +2452,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
? `人物规则:本屏涉及人物场景,固定使用提供的模特图同一人物(面部、发型、体型、服装风格保持一致)。`
: "人物规则:本屏不要出现人物或人体部位,只展示产品与场景元素。",
screen?.modelReason ? `人物融入依据:${screen.modelReason}` : "",
bundleShowcase
? "搭配展示规则:本屏必须同场景展示主产品与搭配产品,主次层级清晰,突出组合购买价值与加购理由。"
: "",
bundleReason ? `搭配依据:${bundleReason}` : "",
screen?.title ? `本屏标题:${screen.title}` : "",
screen?.subtitle ? `本屏副标题:${screen.subtitle}` : "",
screen?.copy ? `本屏文案说明:${screen.copy}` : "",
@ -2435,6 +2473,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
index > 0 ? generatedImages[0]?.originalUrl || generatedImages[0]?.thumbnailUrl : null;
const generationReferenceImages = [
...productAttachments.map((item) => item.url),
...(bundleShowcase ? bundleAttachments.map((item) => item.url) : []),
...(useModelForScene ? [modelAttachment.url] : []),
...(styleAnchorUrl ? [styleAnchorUrl] : []),
].filter(Boolean);
@ -2493,6 +2532,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
detailLayoutNote: screen?.layoutNote || "",
detailUseModel: useModelForScene,
detailModelReason: screen?.modelReason || "",
detailBundleShowcase: bundleShowcase,
detailBundleReason: bundleReason,
detailGlobalStyle: planData.globalStyle || null,
},
},
@ -2974,8 +3015,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
const isLoading = status === 'streaming' || status === 'submitted';
const isSubmitting = isLoading || isDetailGenerating;
const hasInputOrAttachments = Boolean(input.trim() || attachments.length > 0);
const detailProductCount = detailSlots.slice(0, 5).filter((item) => Boolean(item?.url)).length;
const detailModelReady = Boolean(detailSlots[5]?.url);
const detailProductCount = DETAIL_PRODUCT_SLOT_INDEXES.filter((index) => Boolean(detailSlots[index]?.url)).length;
const detailModelReady = DETAIL_MODEL_SLOT_INDEX >= 0 ? Boolean(detailSlots[DETAIL_MODEL_SLOT_INDEX]?.url) : false;
const detailBundleCount = DETAIL_BUNDLE_SLOT_INDEXES.filter((index) => Boolean(detailSlots[index]?.url)).length;
const detailProgressPercent =
detailProgress.total > 0
? Math.min(100, Math.round((detailProgress.current / detailProgress.total) * 100))
@ -3790,7 +3832,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
>
<div>
<p className="text-xs font-semibold text-black">素材上传</p>
<p className="text-[11px] text-black/55">固定 5 张产品图 + 1 张模特图</p>
<p className="text-[11px] text-black/55">固定 5 张产品图 + 1 张模特图 + 可选 3 张搭配图</p>
</div>
<ChevronDown
className={cn(
@ -3802,7 +3844,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
{detailSections.assets ? (
<div className="border-t border-black/10 p-3">
<div className="mb-2 text-[11px] text-black/55">
Agent 会自动判断是否使用模特图,人物相关分镜将固定使用同一模特。
Agent 会自动判断是否使用模特图;若上传搭配图,将自动新增一屏“场景化产品搭配展示”用于关联销售。
</div>
<div className="grid grid-cols-2 gap-2 xl:grid-cols-6">
{DETAIL_SLOT_CONFIGS.map((config, index) => {
@ -3875,7 +3917,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
})}
</div>
<div className="mt-2 text-[11px] text-black/55">
当前素材:产品图 {detailProductCount}/5,模特图 {detailModelReady ? "1/1" : "0/1"}
当前素材:产品图 {detailProductCount}/5,模特图 {detailModelReady ? "1/1" : "0/1"},搭配图 {detailBundleCount}/3(可选)
</div>
</div>
) : null}