feat detail mode: optional bundle images and auto add cross-sell showcase screen
This commit is contained in:
parent
9d2b21e1ef
commit
9af7619b7f
@ -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
|
||||
|
||||
@ -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 = [{
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user