feat(canvas): strengthen detail first-screen naming and poster constraints
This commit is contained in:
parent
9874dcc1da
commit
4993354969
@ -173,6 +173,7 @@ type DetailPlanPreflight = {
|
||||
type DetailCoreFocusContext = {
|
||||
anchor: string;
|
||||
primaryNeed: string;
|
||||
namingToken: string;
|
||||
firstScreenTitle: string;
|
||||
firstScreenSubtitle: string;
|
||||
focusTerms: string[];
|
||||
@ -395,7 +396,7 @@ function toColloquialZhStem(stem: string): string {
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildZhCoreNamingTitle(brief: DetailBrief, anchor: string, primaryNeed: string): string {
|
||||
function buildZhCoreNamingWord(brief: DetailBrief, anchor: string, primaryNeed: string): string {
|
||||
const suffix = inferZhProductSuffix(brief.productName);
|
||||
let stem = toColloquialZhStem(inferZhNamingStem(anchor, primaryNeed));
|
||||
if (!suffix) return compactZhTitle(anchor);
|
||||
@ -417,6 +418,43 @@ function buildZhCoreNamingTitle(brief: DetailBrief, anchor: string, primaryNeed:
|
||||
return title;
|
||||
}
|
||||
|
||||
function inferZhNeedTag(params: {
|
||||
anchor: string;
|
||||
primaryNeed: string;
|
||||
expectedText: string;
|
||||
scenario: string;
|
||||
}): string {
|
||||
const source = `${params.anchor} ${params.primaryNeed} ${params.expectedText} ${params.scenario}`;
|
||||
const rules: Array<{ pattern: RegExp; phrase: string }> = [
|
||||
{ pattern: /自拍|拍摄|防抖|不抖|稳拍/, phrase: "自拍不手抖" },
|
||||
{ pattern: /过敏|低敏|敏感|红痒/, phrase: "敏肌不红痒" },
|
||||
{ pattern: /漏|防漏|整夜|夜用/, phrase: "整夜不漏尿" },
|
||||
{ pattern: /腰|护腰|久坐|腰酸/, phrase: "久坐不腰酸" },
|
||||
{ pattern: /收纳|省空间|不占地/, phrase: "收纳不占地" },
|
||||
{ pattern: /透气|不闷|呼吸感/, phrase: "久用不闷热" },
|
||||
{ pattern: /清洁|去污|去渍|好洗/, phrase: "去污不费劲" },
|
||||
{ pattern: /便携|轻便|小巧|出门/, phrase: "出门更轻松" },
|
||||
];
|
||||
const hit = rules.find((item) => item.pattern.test(source));
|
||||
if (hit) return hit.phrase;
|
||||
|
||||
const compactNeed = compactZhTitle(params.expectedText || params.primaryNeed || params.anchor);
|
||||
if (!compactNeed) return "用着更省心";
|
||||
if (compactNeed.length <= 4) return `${compactNeed}更省心`;
|
||||
return `${compactNeed.slice(0, 6)}更省心`;
|
||||
}
|
||||
|
||||
function buildZhNameNeedTitle(params: { namingToken: string; needTag: string }): string {
|
||||
const left = String(params.namingToken || "").trim() || "好用款";
|
||||
let right = String(params.needTag || "").trim() || "用着更省心";
|
||||
right = right.replace(/[。!!??]+$/g, "");
|
||||
let title = `${left},${right}`;
|
||||
if (title.length > 20) {
|
||||
title = `${left},${right.slice(0, Math.max(4, 19 - left.length))}`;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
function buildCoreFocusContext(brief: DetailBrief, language: "zh" | "en"): DetailCoreFocusContext {
|
||||
const anchorRaw =
|
||||
brief.coreSellingPoint ||
|
||||
@ -442,21 +480,35 @@ function buildCoreFocusContext(brief: DetailBrief, language: "zh" | "en"): Detai
|
||||
return {
|
||||
anchor: anchorRaw,
|
||||
primaryNeed,
|
||||
namingToken: title,
|
||||
firstScreenTitle: title,
|
||||
firstScreenSubtitle: subtitle,
|
||||
focusTerms,
|
||||
};
|
||||
}
|
||||
|
||||
const title = buildZhCoreNamingTitle(brief, anchorRaw, primaryNeed);
|
||||
const namingToken = buildZhCoreNamingWord(brief, anchorRaw, primaryNeed);
|
||||
const needTag = inferZhNeedTag({
|
||||
anchor: anchorRaw,
|
||||
primaryNeed,
|
||||
expectedText,
|
||||
scenario,
|
||||
});
|
||||
const title = buildZhNameNeedTitle({ namingToken, needTag });
|
||||
const subtitle = `在${scenario}场景,不用${riskText}也能${expectedText}。`;
|
||||
return {
|
||||
anchor: anchorRaw,
|
||||
primaryNeed,
|
||||
namingToken,
|
||||
firstScreenTitle: title,
|
||||
firstScreenSubtitle: subtitle,
|
||||
focusTerms: Array.from(
|
||||
new Set([...focusTerms, ...splitToCoreTerms(title, language)])
|
||||
new Set([
|
||||
...focusTerms,
|
||||
...splitToCoreTerms(title, language),
|
||||
...splitToCoreTerms(namingToken, language),
|
||||
...splitToCoreTerms(needTag, language),
|
||||
])
|
||||
),
|
||||
};
|
||||
}
|
||||
@ -467,13 +519,19 @@ function containsCoreTerms(text: string, terms: string[]): boolean {
|
||||
return terms.some((term) => source.includes(String(term || "").toLowerCase()));
|
||||
}
|
||||
|
||||
function hasFirstScreenPosterSignal(screen?: DetailPlanScreen): boolean {
|
||||
if (!screen) return false;
|
||||
const merged = `${screen.objective} ${screen.layoutNote} ${screen.visualPrompt}`.toLowerCase();
|
||||
return /海报|封面|主视觉|kv|hero|poster|cover/.test(merged);
|
||||
}
|
||||
|
||||
function enforceCoreFocusOnScreens(params: {
|
||||
screens: DetailPlanScreen[];
|
||||
coreFocus: DetailCoreFocusContext;
|
||||
language: "zh" | "en";
|
||||
}): DetailPlanScreen[] {
|
||||
const { screens, coreFocus, language } = params;
|
||||
const namingWord = coreFocus.firstScreenTitle;
|
||||
const namingWord = coreFocus.namingToken || coreFocus.firstScreenTitle;
|
||||
return screens.map((screen, index) => {
|
||||
const mergedText = `${screen.title} ${screen.subtitle} ${screen.copy} ${screen.fabe.b}`;
|
||||
const focused = containsCoreTerms(mergedText, coreFocus.focusTerms);
|
||||
@ -481,11 +539,16 @@ function enforceCoreFocusOnScreens(params: {
|
||||
language === "en"
|
||||
? `Core selling-point anchor: ${coreFocus.anchor}. This frame must serve this same anchor and avoid topic drift.`
|
||||
: `核心卖点锚点:${coreFocus.anchor}。本屏必须服务同一核心卖点,不得偏题。`;
|
||||
const firstScreenPosterLine =
|
||||
language === "en"
|
||||
? "Screen 1 must be a hero poster cover: single dominant product visual, cinematic light, strong focal contrast, clean headline-safe top area, no collage."
|
||||
: "首屏必须是封面海报感:单一主视觉主体、电影级光影、强焦点对比、顶部清晰标题安全区,禁止拼贴说明图。";
|
||||
|
||||
let title = screen.title;
|
||||
let subtitle = screen.subtitle;
|
||||
let objective = screen.objective;
|
||||
let copy = screen.copy;
|
||||
let layoutNote = screen.layoutNote;
|
||||
let benefit = screen.fabe.b;
|
||||
|
||||
if (index === 0) {
|
||||
@ -498,6 +561,14 @@ function enforceCoreFocusOnScreens(params: {
|
||||
subtitle = containsCoreTerms(`${subtitle} ${copy}`, coreFocus.focusTerms)
|
||||
? subtitle
|
||||
: coreFocus.firstScreenSubtitle;
|
||||
objective =
|
||||
language === "en"
|
||||
? `Hero poster cover for the One selling-point in one glance, instantly answer why it is worth buying.`
|
||||
: "封面海报化呈现The One核心卖点,一眼回答“为什么值得买”。";
|
||||
layoutNote =
|
||||
language === "en"
|
||||
? "Poster composition: product hero occupies 55-70% visual weight; top text safe area is clean; avoid dense multi-module layout."
|
||||
: "海报封面构图:产品主体占画面55-70%视觉权重,顶部标题区留白清晰,避免密集小模块拼贴。";
|
||||
}
|
||||
|
||||
if (!containsCoreTerms(objective, coreFocus.focusTerms)) {
|
||||
@ -534,13 +605,20 @@ function enforceCoreFocusOnScreens(params: {
|
||||
subtitle,
|
||||
objective,
|
||||
copy,
|
||||
layoutNote,
|
||||
fabe: {
|
||||
...screen.fabe,
|
||||
b: benefit,
|
||||
},
|
||||
visualPrompt: screen.visualPrompt.includes(coreFocus.anchor)
|
||||
? screen.visualPrompt
|
||||
: `${screen.visualPrompt} ${promptCoreLine}`,
|
||||
visualPrompt: (() => {
|
||||
let nextPrompt = screen.visualPrompt.includes(coreFocus.anchor)
|
||||
? screen.visualPrompt
|
||||
: `${screen.visualPrompt} ${promptCoreLine}`;
|
||||
if (index === 0 && !/海报|封面|主视觉|kv|hero|poster|cover/i.test(nextPrompt)) {
|
||||
nextPrompt = `${nextPrompt} ${firstScreenPosterLine}`;
|
||||
}
|
||||
return nextPrompt;
|
||||
})(),
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -564,7 +642,7 @@ function buildTypographySpec(language: "zh" | "en") {
|
||||
return {
|
||||
fontFamily: "Noto Sans SC / PingFang SC / Microsoft YaHei",
|
||||
title:
|
||||
"主标题 64px、字重700、行高1.15,建议单行,最多2行,控制在12字以内,不得出现“第X屏”",
|
||||
"主标题 64px、字重700、行高1.15,建议1-2行,首屏控制在6-20字(命名词+需求句),不得出现“第X屏”",
|
||||
subtitle:
|
||||
"副标题 34px、字重600、行高1.25,位于主标题下方16px,控制在30字以内",
|
||||
body:
|
||||
@ -792,14 +870,14 @@ function buildFallbackScreens(params: {
|
||||
const defaultObjectives =
|
||||
language === "en"
|
||||
? [
|
||||
"Hero scene with one core value",
|
||||
"Hero poster cover with one core value",
|
||||
"Pain-point resonance and trigger moment",
|
||||
"Solution structure and key mechanism",
|
||||
"Differentiation reinforcement",
|
||||
"Evidence and trust closing",
|
||||
]
|
||||
: [
|
||||
"首屏主视觉聚焦单一核心卖点",
|
||||
"首屏海报主视觉聚焦单一核心卖点",
|
||||
"痛点共鸣与触发场景放大",
|
||||
"解决方案与关键机制说明",
|
||||
"差异化优势重复强化",
|
||||
@ -826,7 +904,7 @@ function buildFallbackScreens(params: {
|
||||
return Array.from({ length: count }).map((_, index) => {
|
||||
const screenType =
|
||||
screenTypeSequence[index] || (index % 3 === 0 ? "B" : index % 3 === 1 ? "A" : "C");
|
||||
const objective =
|
||||
let objective =
|
||||
defaultObjectives[index] ||
|
||||
(language === "en"
|
||||
? `Extended detail frame ${index + 1}`
|
||||
@ -858,7 +936,7 @@ function buildFallbackScreens(params: {
|
||||
: language === "en"
|
||||
? `${productLabel} turns key pain points into visible outcomes under the anchor "${coreFocus.anchor}", keeping one consistent decision narrative.`
|
||||
: `${productLabel}围绕“${coreFocus.anchor}”将关键痛点转为可感知结果,形成统一的购买决策路径。`;
|
||||
const layoutNote =
|
||||
let layoutNote =
|
||||
isBundleScreen
|
||||
? language === "en"
|
||||
? "Main product remains primary; companion products are secondary with clear hierarchy and no visual clutter."
|
||||
@ -897,6 +975,17 @@ function buildFallbackScreens(params: {
|
||||
e: "使用检测、认证、对比图等证据收尾。",
|
||||
};
|
||||
|
||||
if (index === 0 && !isBundleScreen) {
|
||||
objective =
|
||||
language === "en"
|
||||
? "Poster-style hero cover for the One selling-point, making the buying reason obvious in one glance."
|
||||
: "海报级首屏封面,聚焦The One核心卖点,一眼说明购买理由。";
|
||||
layoutNote =
|
||||
language === "en"
|
||||
? "Hero poster layout: dominant single product focal point, cinematic lighting and contrast, clear top headline safe area, no collage cards."
|
||||
: "首屏海报布局:单一主体强焦点,电影感光影与对比,顶部标题区清晰,禁止拼贴卡片式说明。";
|
||||
}
|
||||
|
||||
const visualPrompt =
|
||||
language === "en"
|
||||
? [
|
||||
@ -906,6 +995,9 @@ function buildFallbackScreens(params: {
|
||||
`Main scene: ${mainScenario}; core demand: ${coreNeed}.`,
|
||||
"Audience profile is internal strategy only; do not expose explicit age/gender labels in visible copy.",
|
||||
`Core selling-point anchor: ${coreFocus.anchor}; this frame must stay aligned to this anchor.`,
|
||||
index === 0
|
||||
? "Screen 1 hero-cover rule: this must look like a poster key visual with one dominant subject, strong focus contrast, and no collage cards."
|
||||
: "",
|
||||
`Positioning statement: ${positioning}.`,
|
||||
`Global style: ${globalStyle.designLanguage}.`,
|
||||
`Typography: ${globalStyle.typography.fontFamily}; ${globalStyle.typography.title}; ${globalStyle.typography.subtitle}; ${globalStyle.typography.body}.`,
|
||||
@ -930,6 +1022,9 @@ function buildFallbackScreens(params: {
|
||||
`主场景:${mainScenario};核心需求:${coreNeed}。`,
|
||||
"人群画像仅用于内部策略,画面可见文案不得直写年龄/性别标签。",
|
||||
`核心卖点锚点:${coreFocus.anchor},本屏文案必须围绕该锚点。`,
|
||||
index === 0
|
||||
? "首屏封面规则:必须是海报主视觉(非说明图拼贴),单一主体大图、强聚焦、高级商业海报质感。"
|
||||
: "",
|
||||
`一句话定位:${positioning}。`,
|
||||
`全局风格:${globalStyle.designLanguage}。`,
|
||||
`字体规范:${globalStyle.typography.fontFamily};${globalStyle.typography.title};${globalStyle.typography.subtitle};${globalStyle.typography.body}。`,
|
||||
@ -1143,6 +1238,9 @@ function evaluatePlanQuality(params: {
|
||||
if (firstFive.length >= 1 && firstFive[0]?.screenType !== "B") {
|
||||
issues.push("第1屏应为B类场景共鸣页。");
|
||||
}
|
||||
if (!hasFirstScreenPosterSignal(firstFive[0])) {
|
||||
issues.push("首屏缺少“封面海报感”约束(主视觉/海报/封面/KV),需强化首屏主视觉表现。");
|
||||
}
|
||||
if (firstFive.length >= 2 && firstFive[1]?.screenType !== "A") {
|
||||
issues.push("第2屏建议为A类技术公信页。");
|
||||
}
|
||||
@ -1186,11 +1284,17 @@ function evaluatePlanQuality(params: {
|
||||
|
||||
const titleLengthOutOfRange = screens.some((screen, index) => {
|
||||
const len = visibleTitleLength(screen.title);
|
||||
const minLen = index === 0 ? 3 : 4;
|
||||
return len < minLen || len > 10;
|
||||
const minLen = index === 0 ? 6 : 4;
|
||||
const maxLen = index === 0 ? 20 : 10;
|
||||
return len < minLen || len > maxLen;
|
||||
});
|
||||
if (titleLengthOutOfRange) {
|
||||
issues.push("存在分镜主标题长度不符合规范(首屏3-10字,其余4-10字)。");
|
||||
issues.push("存在分镜主标题长度不符合规范(首屏6-20字,其余4-10字)。");
|
||||
}
|
||||
|
||||
const firstTitle = String(screens[0]?.title || "").trim();
|
||||
if (firstTitle && !/[,,::·|]/.test(firstTitle)) {
|
||||
issues.push("首屏主标题需采用“命名词+需求句”结构(示例:甩甩杆,自拍不手抖)。");
|
||||
}
|
||||
|
||||
const subtitleTooLong = screens.some((screen) => {
|
||||
@ -1225,7 +1329,7 @@ function evaluatePlanQuality(params: {
|
||||
issues.push("多屏文案偏离核心卖点,整套详情需围绕同一核心卖点展开。");
|
||||
}
|
||||
|
||||
const namingWord = String(firstScreen?.title || "").trim();
|
||||
const namingWord = String(coreFocus.namingToken || firstScreen?.title || "").trim();
|
||||
if (namingWord) {
|
||||
const repeatCount = screens
|
||||
.slice(1)
|
||||
@ -1332,6 +1436,7 @@ async function runDualDirectorPlanPreflight(params: {
|
||||
"禁用表达:主标题禁止出现“核心价值/场景痛点/方案拆解/细节证明/信任收口/卖点延展”等节奏词。",
|
||||
"禁用表达:对外文案不得直写具体年龄/性别标签(如“25-35岁女性”),必须改为场景化表达。",
|
||||
"必须维持 790x1300 固定规格、全套统一字体层级与统一风格。",
|
||||
"首屏必须具备“封面海报感”:单一主视觉、强焦点、清晰标题区,不得做拼贴式说明页。",
|
||||
`整套文案必须围绕同一核心卖点锚点:“${params.coreFocus.anchor}”,不得跑题。`,
|
||||
params.hasModelImage
|
||||
? "已提供模特图:仅在必要分镜使用且保持同一人物一致性。"
|
||||
@ -1517,7 +1622,7 @@ export async function POST(request: Request) {
|
||||
"必须遵循“详情升级文案方案”:定位卡 -> 卖点到买点转译 -> 黄金五屏 -> FABE闭环。",
|
||||
`核心卖点锚点(整套必须围绕):${coreFocus.anchor}`,
|
||||
`首屏标题参考(必须承接核心卖点):${coreFocus.firstScreenTitle}`,
|
||||
"首屏命名规则:用核心卖点给产品命名(如“甩甩杆/安敏裤”),避免通用标题。",
|
||||
"首屏命名规则:标题必须是“名字+需求一句话”(如“甩甩杆,自拍不手抖”),不能只给一个名字。",
|
||||
"命名风格要求:大白话、易记、朗朗上口,未来可直接成为用户搜索词。",
|
||||
referenceImageNotes.length > 0
|
||||
? `参考图说明(按上传顺序):${referenceImageNotes.join(" | ")}`
|
||||
@ -1560,11 +1665,12 @@ export async function POST(request: Request) {
|
||||
`3) 所有分镜风格统一、构图统一、文案排版风格统一。主标题/副标题/正文的字体家族、字重、字号层级、行距必须全套统一。\n` +
|
||||
`4) 每屏尺寸固定 790x1300 像素,输出提示词必须显式包含该尺寸约束。\n` +
|
||||
`5) 结构遵循“黄金五屏原则”:屏1-2核心卖点,屏3-4差异强化,屏5证据背书;更多屏作为延展。\n` +
|
||||
`5.1) 首屏必须输出“The One核心卖点标题”(3-10字)+ 单句结果副标题,直接回答“为什么值得买”。\n` +
|
||||
`5.1) 首屏主标题必须是“命名词+需求句”结构(6-20字,例如“甩甩杆,自拍不手抖”),并配一条结果副标题,直接回答“为什么值得买”。\n` +
|
||||
`5.2) 全套每一屏都必须服务同一核心卖点锚点,不允许出现与核心卖点无关的新主叙事。\n` +
|
||||
`5.3) 中文首屏标题优先使用“核心卖点命名词+品类后缀”结构(如:甩甩杆、安敏裤)。\n` +
|
||||
`5.4) 命名词必须大白话、好记、可口头传播,避免“智能/科技/方案/系统”等抽象词。\n` +
|
||||
`5.5) 后续至少2屏在副标题或正文复现该命名词,强化用户检索记忆。\n` +
|
||||
`5.6) 首屏必须是封面海报构图:单一主体主视觉、强对比焦点、电影感光影、顶部标题安全区;禁止拼贴说明页。\n` +
|
||||
`6) 每屏都要有 FABE 闭环(B利益/F特征/A优势/E证据),且文案符合电商转化表达。\n` +
|
||||
`7) 文案用词需贴近用户语言,不写空泛形容词,不泄露系统流程。\n` +
|
||||
`7.1) 必须执行“卖点→用户任务(JTBD)→买点句”转译,买点句优先用“不用___也能___ / 在___场景也能___”句式。\n` +
|
||||
@ -1573,7 +1679,7 @@ export async function POST(request: Request) {
|
||||
`8) 对每屏输出 useModel 与 modelReason:useModel=true 表示该屏必须融入固定模特图人物;false 表示该屏禁止出现人物。\n` +
|
||||
`9) 对每屏输出 screenType(A/B/C):A=技术公信,B=场景共鸣,C=细节佐证。\n` +
|
||||
`9.1) 前3屏默认节奏:屏1=场景痛点共鸣(B),屏2=买点总括,屏3=证据验证(对比/实测/口碑/认证)。\n` +
|
||||
`10) 主标题必须是卖点语义,不得出现“第1屏/Screen 1”等编号标识,建议4-10字。\n` +
|
||||
`10) 主标题必须是卖点语义,不得出现“第1屏/Screen 1”等编号标识;首屏6-20字(命名词+需求句),其余屏建议4-10字。\n` +
|
||||
`10.1) 主标题禁止使用规划节奏词:核心价值/场景痛点/方案拆解/细节证明/信任收口/卖点延展。\n` +
|
||||
`11) 在 globalStyle.typography 中写明明确字号与字重(示例:主标题64px/700,副标题34px/600,正文24px/500),并在每屏 layoutNote 里落实排版层级。\n` +
|
||||
`11.1) 副标题必须为单句结果句,禁止写成长段说明。\n` +
|
||||
@ -1585,7 +1691,7 @@ export async function POST(request: Request) {
|
||||
? `14) 已提供搭配产品图:必须新增且仅新增1屏“场景化产品搭配展示”,该屏设定 bundleShowcase=true,并在 bundleReason 说明搭配策略;其余屏设为 bundleShowcase=false。\n`
|
||||
: `14) 未提供搭配产品图:所有分镜均设置 bundleShowcase=false。\n`) +
|
||||
`输出格式:\n` +
|
||||
`{"summary":"简述","globalStyle":{"designLanguage":"一句话风格方向","typography":{"fontFamily":"字体家族","title":"主标题规范","subtitle":"副标题规范","body":"正文规范"},"layout":{"grid":"栅格","safeArea":"安全区","rhythm":"留白节奏"},"palette":"配色规范"},"screens":[{"index":1,"screenType":"B","objective":"本屏目标","title":"3-8字标题","subtitle":"一句副标题","copy":"正文文案","layoutNote":"排版说明","useModel":false,"modelReason":"是否需要人物及原因","bundleShowcase":false,"bundleReason":"是否为搭配展示页及原因","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":"首屏示例:甩甩杆,自拍不手抖(6-20字)","subtitle":"一句副标题","copy":"正文文案","layoutNote":"排版说明","useModel":false,"modelReason":"是否需要人物及原因","bundleShowcase":false,"bundleReason":"是否为搭配展示页及原因","fabe":{"b":"利益","f":"特征","a":"优势","e":"证据"},"visualPrompt":"用于生图的完整提示词,必须包含790x1300与统一字体排版"}]}`,
|
||||
temperature: 0.25,
|
||||
maxTokens: 1800,
|
||||
} as any);
|
||||
|
||||
@ -90,6 +90,7 @@ function buildFallbackPrompt(params: {
|
||||
screenPrompt: string;
|
||||
layoutNote: string;
|
||||
language: "zh" | "en";
|
||||
isFirstScreen: boolean;
|
||||
}) {
|
||||
if (params.language === "en") {
|
||||
return [
|
||||
@ -99,6 +100,9 @@ function buildFallbackPrompt(params: {
|
||||
"Typography lock: title 64px/700, subtitle 34px/600, body 24px/500, keep the same hierarchy used in all other frames.",
|
||||
"Text safe-area lock: y=64~360, avoid text overlap with product hero.",
|
||||
params.layoutNote ? `Layout fix note: ${params.layoutNote}` : "",
|
||||
params.isFirstScreen
|
||||
? "Screen 1 hero-cover lock: poster-like key visual, single dominant product focal point, clear top headline safe area, no collage modules."
|
||||
: "",
|
||||
"Do not put 'Screen X' text in the title.",
|
||||
"Marketing conversion lock: pain point -> benefit -> trust proof path must be immediately understandable.",
|
||||
"Preserve product identity and style consistency with previous frames.",
|
||||
@ -113,6 +117,9 @@ function buildFallbackPrompt(params: {
|
||||
"字体锁定:主标题64px/700,副标题34px/600,正文24px/500,全套层级与其它屏一致。",
|
||||
"文字安全区锁定:y=64~360,避免与产品主体重叠。",
|
||||
params.layoutNote ? `排版修正建议:${params.layoutNote}` : "",
|
||||
params.isFirstScreen
|
||||
? "首屏封面锁定:必须是海报主视觉,单一主体强焦点,顶部标题区清晰,禁止拼贴卡片说明。"
|
||||
: "",
|
||||
"主标题不得出现“第X屏”等编号词。",
|
||||
"营销转化锁定:用户能快速读懂“痛点-利益-证据”路径,避免只好看不转化。",
|
||||
"保持产品外观与整体风格连续一致。",
|
||||
@ -160,6 +167,7 @@ export async function POST(request: Request) {
|
||||
screenPrompt,
|
||||
layoutNote,
|
||||
language,
|
||||
isFirstScreen: Number(body.screen?.index || 0) === 1,
|
||||
});
|
||||
|
||||
if (!reviewerConfig) {
|
||||
@ -216,6 +224,9 @@ export async function POST(request: Request) {
|
||||
`- Screen subtitle: ${String(body.screen?.subtitle || "").trim()}`,
|
||||
`- Screen copy: ${String(body.screen?.copy || "").trim()}`,
|
||||
`- Layout note: ${layoutNote}`,
|
||||
Number(body.screen?.index || 0) === 1
|
||||
? "- Screen 1 must keep poster hero quality: single dominant focal point, clear headline zone, no collage."
|
||||
: "",
|
||||
screenPrompt ? `- Source prompt: ${screenPrompt}` : "",
|
||||
language === "en"
|
||||
? "Return strict JSON: {\"pass\":boolean,\"score\":0-100,\"designDirectorScore\":0-100,\"marketingDirectorScore\":0-100,\"issues\":[\"...\"],\"layoutFixes\":[\"...\"],\"typographyFixes\":[\"...\"],\"conversionFixes\":[\"...\"],\"revisedPrompt\":\"...\"}"
|
||||
|
||||
@ -3833,6 +3833,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
screen?.screenType
|
||||
? `分镜类型:${screen.screenType}(A=技术公信,B=场景共鸣,C=细节佐证)`
|
||||
: "",
|
||||
index === 0
|
||||
? "首屏封面规则:必须做成海报主视觉,单一主体强焦点,产品主体占画面55%-70%,顶部标题区清晰留白,禁止拼贴式说明图。"
|
||||
: "",
|
||||
useModelForScene
|
||||
? `人物规则:本屏涉及人物场景,固定使用提供的模特图同一人物(面部、发型、体型、服装风格保持一致)。`
|
||||
: "人物规则:本屏不要出现人物或人体部位,只展示产品与场景元素。",
|
||||
@ -5298,7 +5301,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
!shouldShowImageLoading;
|
||||
|
||||
const renderModelSelectContent = () => (
|
||||
<SelectContent className="w-[238px] max-h-[340px] overflow-hidden rounded-[18px] border border-[#cfd4dc] bg-[#f3f4f6] p-1.5 shadow-[0_8px_20px_rgba(15,23,42,0.13)]">
|
||||
<SelectContent className="w-[238px] max-h-[340px] overflow-hidden rounded-[18px] border border-[#cfd4dc] bg-[#f3f4f6]/90 p-1.5 shadow-[0_8px_20px_rgba(15,23,42,0.13)]">
|
||||
<div className="max-h-[312px] space-y-0.5 overflow-y-auto pr-1">
|
||||
{selectableModels.map((model) => (
|
||||
<SelectItem
|
||||
@ -5436,7 +5439,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
<History className="h-4 w-4" />
|
||||
</Button>
|
||||
{isSessionMenuOpen && (
|
||||
<div className="absolute right-0 top-10 w-64 rounded-xl border bg-background/95 backdrop-blur-md shadow-xl p-2">
|
||||
<div className="absolute right-0 top-10 w-64 rounded-xl border bg-background/90 backdrop-blur-md shadow-xl p-2">
|
||||
<div className="px-2 py-1 text-xs text-muted-foreground">聊天窗口记录</div>
|
||||
<div className="max-h-56 overflow-y-auto space-y-1">
|
||||
{chatSessions.length === 0 ? (
|
||||
@ -6910,7 +6913,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
align="start"
|
||||
side="top"
|
||||
sideOffset={10}
|
||||
className="w-[156px] rounded-xl border border-[#d6d9df] bg-[#f4f5f7] p-1.5 shadow-[0_8px_18px_rgba(15,23,42,0.14)]"
|
||||
className="w-[156px] rounded-xl border border-[#d6d9df] bg-[#f4f5f7]/90 p-1.5 shadow-[0_8px_18px_rgba(15,23,42,0.14)]"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSelectGenerationMode("agent")}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user