From 0fa4a14ac6e926dfdb6e61e4358e404587d7e06e Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 22 Feb 2026 04:08:59 +0800 Subject: [PATCH] feat: refine detail generation copy strategy and canvas layout --- app/api/canvas/detail-plan/route.ts | 176 +++- app/globals.css | 279 ++----- components/canvas/CanvasChatPanel.tsx | 899 +++++++++++++-------- components/canvas/RealtimeCanvasDialog.tsx | 555 ++++++++----- components/ui/slider.tsx | 20 +- 5 files changed, 1129 insertions(+), 800 deletions(-) diff --git a/app/api/canvas/detail-plan/route.ts b/app/api/canvas/detail-plan/route.ts index 7adb468a9..210009a0d 100644 --- a/app/api/canvas/detail-plan/route.ts +++ b/app/api/canvas/detail-plan/route.ts @@ -179,6 +179,25 @@ const AGENT_PLANNING_PHRASE_PATTERNS = [ /第一屏展示啥/gi, ]; +const TITLE_META_TERMS = [ + "核心价值", + "场景痛点", + "方案拆解", + "细节证明", + "信任收口", + "卖点延展", + "解决方案", + "价值延展", + "证据背书", +]; + +function hasPlanningTitleMeta(value: string): boolean { + const text = String(value || "").trim(); + if (!text) return false; + if (/第\s*\d+\s*屏|screen\s*\d+/i.test(text)) return true; + return TITLE_META_TERMS.some((term) => text.includes(term)); +} + function stripAgentPlanningPhrases(value: string): string { let result = value; for (const pattern of AGENT_PLANNING_PHRASE_PATTERNS) { @@ -187,6 +206,49 @@ function stripAgentPlanningPhrases(value: string): string { return result.replace(/\s+/g, " ").trim(); } +function desensitizeAudienceCopy(value: string, language: "zh" | "en"): string { + let result = String(value || ""); + if (!result) return result; + + if (language === "en") { + result = result + .replace(/\b\d{1,2}\s*(?:-|~|to)\s*\d{1,2}\s*years?\s*old\b/gi, "target users") + .replace( + /\b(?:for|toward|targeting)\s+[^,.;\n]{0,48}\b(?:women|men|female|male|audience|users?)\b/gi, + "for target scenario users" + ) + .replace(/\b(?:primary audience|target audience)\s*:\s*[^,.;\n]+/gi, "target scenario users"); + } else { + result = result + .replace(/\d{1,2}\s*(?:-|~|~|到|至)\s*\d{1,2}\s*岁(?:[男女]性)?/g, "目标用户") + .replace( + /(?:面向|针对)[^,。;\n]{0,40}(?:人群|用户|女性|男性|宝妈|白领|学生|新手)/g, + "围绕目标场景用户" + ) + .replace( + /(?:主购买人群|目标人群|适用人群)\s*[::]\s*[^,。;\n]+/g, + "目标场景用户" + ); + } + + return result.replace(/\s+/g, " ").replace(/,{2,}/g, ",").trim(); +} + +const GENERIC_FLUFF_WORDS_ZH = [ + "高级感", + "轻奢", + "超好用", + "巨能装", + "适合所有场景", + "适合所有人", +]; + +function visibleTitleLength(value: string): number { + return value + .replace(/[^\p{L}\p{N}]+/gu, "") + .trim().length; +} + function buildTypographySpec(language: "zh" | "en") { if (language === "en") { return { @@ -448,8 +510,8 @@ function buildFallbackScreens(params: { const screenTypeSequence: DetailScreenType[] = ["B", "A", "B", "C", "B"]; const defaultTitles = language === "en" - ? ["Core Value", "Pain Resonance", "Solution Logic", "Detail Proof", "Trust Closure"] - : ["核心价值", "场景痛点", "方案拆解", "细节证明", "信任收口"]; + ? ["Long-Sit Comfort", "Waist Support", "One-Step Adjust", "Stable Structure", "Verified Trust"] + : ["久坐不累", "贴腰承托", "一键调节", "稳固支撑", "安心可证"]; return Array.from({ length: count }).map((_, index) => { const screenType = @@ -466,16 +528,16 @@ function buildFallbackScreens(params: { ? "Bundle Cross-sell" : "搭配推荐" : language === "en" - ? defaultTitles[index] || `Selling Point Extension ${index + 1}` - : defaultTitles[index] || "卖点延展"; + ? defaultTitles[index] || `Comfort Upgrade ${index + 1}` + : defaultTitles[index] || "舒适升级"; const subtitle = 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}”。`; + ? `Solve "${coreNeed}" in ${mainScenario} with clear benefit-focused messaging.` + : `在${mainScenario}场景解决“${coreNeed}”,突出结果导向表达。`; const copy = isBundleScreen ? language === "en" @@ -529,7 +591,8 @@ function buildFallbackScreens(params: { `E-commerce detail frame ${index + 1}, objective: ${objective}.`, `Fixed vertical canvas 790x1300 pixels.`, `Keep exact product identity consistent across all frames.`, - `Main scene: ${mainScenario}; audience: ${brief.targetAudience}; core demand: ${coreNeed}.`, + `Main scene: ${mainScenario}; core demand: ${coreNeed}.`, + "Audience profile is internal strategy only; do not expose explicit age/gender labels in visible copy.", `Positioning statement: ${positioning}.`, `Global style: ${globalStyle.designLanguage}.`, `Typography: ${globalStyle.typography.fontFamily}; ${globalStyle.typography.title}; ${globalStyle.typography.subtitle}; ${globalStyle.typography.body}.`, @@ -551,7 +614,8 @@ function buildFallbackScreens(params: { `电商详情页第${index + 1}屏,目标:${objective}。`, "画布固定竖版 790x1300 像素。", "全套保持同一产品外观一致(结构、材质、颜色、比例、Logo)。", - `主场景:${mainScenario};人群:${brief.targetAudience};核心需求:${coreNeed}。`, + `主场景:${mainScenario};核心需求:${coreNeed}。`, + "人群画像仅用于内部策略,画面可见文案不得直写年龄/性别标签。", `一句话定位:${positioning}。`, `全局风格:${globalStyle.designLanguage}。`, `字体规范:${globalStyle.typography.fontFamily};${globalStyle.typography.title};${globalStyle.typography.subtitle};${globalStyle.typography.body}。`, @@ -627,7 +691,8 @@ function buildFallbackPlan(params: { function sanitizeScreen( raw: unknown, - fallback: DetailPlanScreen + fallback: DetailPlanScreen, + language: "zh" | "en" ): DetailPlanScreen { const source = (raw || {}) as Record; const safeText = (value: unknown, defaultValue: string) => { @@ -636,7 +701,10 @@ function sanitizeScreen( }; const fabeSource = (source.fabe || {}) as Record; - const visualPrompt = safeText(source.visualPrompt, fallback.visualPrompt); + const visualPrompt = desensitizeAudienceCopy( + safeText(source.visualPrompt, fallback.visualPrompt), + language + ); const normalizeTitle = (value: string) => { const cleaned = stripAgentPlanningPhrases( value @@ -646,7 +714,9 @@ function sanitizeScreen( .replace(/\s+/g, " ") .trim() ); - return cleaned || fallback.title; + if (!cleaned) return fallback.title; + if (hasPlanningTitleMeta(cleaned)) return fallback.title; + return cleaned; }; const normalizeSubtitle = (value: string) => stripAgentPlanningPhrases( @@ -656,9 +726,10 @@ function sanitizeScreen( .replace(/\s+/g, " ") .trim() ); - const normalizeCopy = (value: string) => stripAgentPlanningPhrases(value) || fallback.copy; + const normalizeCopy = (value: string) => + desensitizeAudienceCopy(stripAgentPlanningPhrases(value) || fallback.copy, language); const normalizeLayoutNote = (value: string) => - stripAgentPlanningPhrases(value) || fallback.layoutNote; + desensitizeAudienceCopy(stripAgentPlanningPhrases(value) || fallback.layoutNote, language); const rawScreenType = String(source.screenType || "").trim().toUpperCase(); const screenType: DetailScreenType = rawScreenType === "A" || rawScreenType === "B" || rawScreenType === "C" @@ -667,19 +738,23 @@ function sanitizeScreen( return { index: fallback.index, screenType, - objective: safeText(source.objective, fallback.objective), - title: normalizeTitle(safeText(source.title, fallback.title)), - subtitle: normalizeSubtitle(safeText(source.subtitle, fallback.subtitle)) || fallback.subtitle, + objective: desensitizeAudienceCopy(safeText(source.objective, fallback.objective), language), + title: desensitizeAudienceCopy(normalizeTitle(safeText(source.title, fallback.title)), language), + subtitle: + desensitizeAudienceCopy( + normalizeSubtitle(safeText(source.subtitle, fallback.subtitle)) || fallback.subtitle, + language + ) || fallback.subtitle, copy: normalizeCopy(safeText(source.copy, fallback.copy)), layoutNote: normalizeLayoutNote(safeText(source.layoutNote, fallback.layoutNote)), useModel: typeof source.useModel === "boolean" ? source.useModel : fallback.useModel, - modelReason: safeText(source.modelReason, fallback.modelReason), + modelReason: desensitizeAudienceCopy(safeText(source.modelReason, fallback.modelReason), language), bundleShowcase: typeof source.bundleShowcase === "boolean" ? source.bundleShowcase : fallback.bundleShowcase, - bundleReason: safeText(source.bundleReason, fallback.bundleReason), + bundleReason: desensitizeAudienceCopy(safeText(source.bundleReason, fallback.bundleReason), language), visualPrompt: visualPrompt.includes("790x1300") ? visualPrompt : `${visualPrompt} 画布固定 790x1300 像素。`, @@ -695,11 +770,12 @@ function sanitizeScreen( function sanitizePlannedScreens( raw: unknown, fallbackScreens: DetailPlanScreen[], + language: "zh" | "en" ): DetailPlanScreen[] { if (!Array.isArray(raw)) { return fallbackScreens; } - return fallbackScreens.map((fallback, index) => sanitizeScreen(raw[index], fallback)); + return fallbackScreens.map((fallback, index) => sanitizeScreen(raw[index], fallback, language)); } function sanitizeGlobalStyle( @@ -781,9 +857,33 @@ function evaluatePlanQuality(params: { issues.push("存在分镜FABE不完整。"); } - const hasScreenIndexInTitle = screens.some((screen) => /第\s*\d+\s*屏|screen\s*\d+/i.test(screen.title)); - if (hasScreenIndexInTitle) { - issues.push("主标题不应出现“第X屏/Screen X”类标记。"); + const hasMetaTitle = screens.some((screen) => hasPlanningTitleMeta(screen.title)); + if (hasMetaTitle) { + issues.push("主标题不应使用规划节奏词(如“方案拆解/细节证明/信任收口”等)。"); + } + + const titleLengthOutOfRange = screens.some((screen) => { + const len = visibleTitleLength(screen.title); + return len < 4 || len > 10; + }); + if (titleLengthOutOfRange) { + issues.push("存在分镜主标题不在4-10字区间。"); + } + + const subtitleTooLong = screens.some((screen) => { + const normalized = screen.subtitle.replace(/\s+/g, ""); + return normalized.length > 38 || /[。!?.!?].+[。!?.!?]/.test(screen.subtitle); + }); + if (subtitleTooLong) { + issues.push("存在分镜副标题过长或多句,建议保持一条结果句。"); + } + + const hasGenericFluff = screens.some((screen) => { + const fullText = `${screen.title} ${screen.subtitle} ${screen.copy}`; + return GENERIC_FLUFF_WORDS_ZH.some((word) => fullText.includes(word)); + }); + if (hasGenericFluff) { + issues.push("存在空泛形容词(如“高级感/轻奢/超好用”),需改为可感知结果。"); } if (hasModelImage) { @@ -855,6 +955,8 @@ async function runDualDirectorPlanPreflight(params: { "Role A: Art Director (visual hierarchy, typography, spacing rhythm, composition).", "Role B: Marketing Director (pain point to benefit logic, trust proof, conversion clarity).", "Forbidden wording in title/subtitle/copy: \"Screen X\", \"This screen shows\", any planning meta phrasing.", + "Title must not use planning rhythm terms like: core value / pain resonance / solution logic / detail proof / trust closure.", + "Do not expose explicit age/gender labels in visible copy (e.g., \"25-35 female\"); rewrite to scenario-based wording.", "Must keep canvas 790x1300, unified typography hierarchy, and consistent style.", params.hasModelImage ? "Model image is provided: only use where needed, keep identity consistent." @@ -875,6 +977,8 @@ async function runDualDirectorPlanPreflight(params: { "角色A:平面设计总监(视觉层级、字体统一、留白节奏、构图平衡)。", "角色B:营销总监(痛点到买点转译、证据可信度、转化路径清晰度)。", "禁用表达:主标题/副标题/正文中禁止出现“第X屏”“本屏展示”“这一屏”等规划词。", + "禁用表达:主标题禁止出现“核心价值/场景痛点/方案拆解/细节证明/信任收口/卖点延展”等节奏词。", + "禁用表达:对外文案不得直写具体年龄/性别标签(如“25-35岁女性”),必须改为场景化表达。", "必须维持 790x1300 固定规格、全套统一字体层级与统一风格。", params.hasModelImage ? "已提供模特图:仅在必要分镜使用且保持同一人物一致性。" @@ -940,7 +1044,7 @@ async function runDualDirectorPlanPreflight(params: { rewriteApplied = true; } if (Array.isArray(parsed?.revisedScreens) && parsed.revisedScreens.length > 0) { - candidateScreens = sanitizePlannedScreens(parsed.revisedScreens, params.screens); + candidateScreens = sanitizePlannedScreens(parsed.revisedScreens, params.screens, params.language); rewriteApplied = true; } @@ -1099,16 +1203,23 @@ export async function POST(request: Request) { `5) 结构遵循“黄金五屏原则”:屏1-2核心卖点,屏3-4差异强化,屏5证据背书;更多屏作为延展。\n` + `6) 每屏都要有 FABE 闭环(B利益/F特征/A优势/E证据),且文案符合电商转化表达。\n` + `7) 文案用词需贴近用户语言,不写空泛形容词,不泄露系统流程。\n` + + `7.1) 必须执行“卖点→用户任务(JTBD)→买点句”转译,买点句优先用“不用___也能___ / 在___场景也能___”句式。\n` + + `7.2) 核心需求最多保留3-5条(1条必备 + 1-2条期待 + 1条魅力),避免页面过载。\n` + + `7.3) 每个核心卖点需覆盖“体验/社交/实用”三维中的至少2维,并落地到 copy 或 layoutNote。\n` + `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) 主标题禁止使用规划节奏词:核心价值/场景痛点/方案拆解/细节证明/信任收口/卖点延展。\n` + `11) 在 globalStyle.typography 中写明明确字号与字重(示例:主标题64px/700,副标题34px/600,正文24px/500),并在每屏 layoutNote 里落实排版层级。\n` + + `11.1) 副标题必须为单句结果句,禁止写成长段说明。\n` + + `12) 对外文案禁止直写具体年龄/性别标签(如“25-35岁女性”),改为场景化表达(如“久坐办公场景用户”)。\n` + (hasModelImage - ? `12) 已提供模特图:涉及人物场景时必须复用同一模特(人脸、发型、体型、肤色与服装风格一致),不得替换其他人物。\n` - : `12) 未提供模特图:所有分镜均设置 useModel=false。\n`) + + ? `13) 已提供模特图:涉及人物场景时必须复用同一模特(人脸、发型、体型、肤色与服装风格一致),不得替换其他人物。\n` + : `13) 未提供模特图:所有分镜均设置 useModel=false。\n`) + (hasBundleImages - ? `13) 已提供搭配产品图:必须新增且仅新增1屏“场景化产品搭配展示”,该屏设定 bundleShowcase=true,并在 bundleReason 说明搭配策略;其余屏设为 bundleShowcase=false。\n` - : `13) 未提供搭配产品图:所有分镜均设置 bundleShowcase=false。\n`) + + ? `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":"4-8字标题","subtitle":"一句副标题","copy":"正文文案","layoutNote":"排版说明","useModel":false,"modelReason":"是否需要人物及原因","bundleShowcase":false,"bundleReason":"是否为搭配展示页及原因","fabe":{"b":"利益","f":"特征","a":"优势","e":"证据"},"visualPrompt":"用于生图的完整提示词,必须包含790x1300与统一字体排版"}]}`, temperature: 0.25, @@ -1147,7 +1258,7 @@ export async function POST(request: Request) { const parsed = extractJsonBlock<{ summary?: unknown; globalStyle?: unknown; screens?: unknown }>(text || ""); const globalStyleInitial = sanitizeGlobalStyle(parsed?.globalStyle, fallback.globalStyle); - const rawScreens = sanitizePlannedScreens(parsed?.screens, fallback.screens).map((item) => + const rawScreens = sanitizePlannedScreens(parsed?.screens, fallback.screens, language).map((item) => hasModelImage ? item : { @@ -1220,9 +1331,12 @@ export async function POST(request: Request) { }; const prompts = screens.map((item) => item.visualPrompt); const summaryRaw = String(parsed?.summary || "").trim(); - const summary = !shouldFallback - ? summaryRaw || fallback.summary - : `${summaryRaw || fallback.summary}\n双总监预审未通过,已自动回退到稳定分镜模板。`; + const summary = desensitizeAudienceCopy( + !shouldFallback + ? summaryRaw || fallback.summary + : `${summaryRaw || fallback.summary}\n双总监预审未通过,已自动回退到稳定分镜模板。`, + language + ); const response: DetailPlanResponse = { summary, diff --git a/app/globals.css b/app/globals.css index 1f6eb23c1..cfc56e44b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -98,52 +98,55 @@ --radius-md: calc(var(--radius) - 2px); --radius-sm: calc(var(--radius) - 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + --animate-caret-blink: caret-blink 1.25s ease-out infinite; - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); + @keyframes caret-blink { + 0%, + 70%, + 100% { + opacity: 1; + } + 20%, + 50% { + opacity: 0; + } + } +} - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - - --color-sidebar: var(--sidebar-background); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); +/* utility classes */ +@utility container { + margin-inline: auto; + padding-inline: 2rem; + @media (width >= --theme(--breakpoint-sm)) { + max-width: none; + } + @media (width >= 1400px) { + max-width: 1400px; + } } /* - The default border color has changed to `currentcolor` in Tailwind CSS v4, + The default border color has changed to `currentColor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the same as it did with Tailwind CSS v3. @@ -156,200 +159,24 @@ ::before, ::backdrop, ::file-selector-button { - border-color: var(--color-gray-200, currentcolor); + border-color: var(--border, currentColor); } } -@utility text-balance { - text-wrap: balance; -} - -@utility -webkit-overflow-scrolling-touch { - -webkit-overflow-scrolling: touch; -} - -@utility touch-pan-y { - touch-action: pan-y; -} - -@utility overscroll-behavior-contain { - overscroll-behavior: contain; -} - @layer utilities { - :root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; + .step { + counter-increment: step; } - @media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } - } -} - -@layer base { - * { - @apply border-border; + .step:before { + @apply absolute w-9 h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 border-background; + @apply ml-[-50px] mt-[-4px]; + content: counter(step); } - body { - @apply bg-background text-foreground; - overflow-x: hidden; - position: relative; - } - - html { - overflow-x: hidden; - } -} - -.skeleton { - * { - pointer-events: none !important; - } - - *[class^="text-"] { - color: transparent; - @apply rounded-md bg-foreground/20 select-none animate-pulse; - } - - .skeleton-bg { - @apply bg-foreground/10; - } - - .skeleton-div { - @apply bg-foreground/20 animate-pulse; - } -} - -.ProseMirror { - outline: none; -} - -.cm-editor, -.cm-gutters { - @apply bg-background! dark:bg-zinc-800! outline-hidden! selection:bg-zinc-900!; -} - -.ͼo.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, -.ͼo.cm-selectionBackground, -.ͼo.cm-content::selection { - @apply bg-zinc-200! dark:bg-zinc-900!; -} - -.cm-activeLine, -.cm-activeLineGutter { - @apply bg-transparent!; -} - -.cm-activeLine { - @apply rounded-r-sm!; -} - -.cm-lineNumbers { - @apply min-w-7; -} - -.cm-foldGutter { - @apply min-w-3; -} - -@layer base { - a[href], - summary, - button:not(:disabled), - label[for], - [role="button"], - [role="link"], - [role="menuitem"], - [role="option"], - [role="radio"], - [role="switch"], - [role="tab"], - [role="checkbox"], - [data-radix-collection-item], - input[type="checkbox"], - input[type="radio"], - input[type="range"] { - cursor: pointer; - } - - button:disabled, - [aria-disabled="true"], - [data-disabled] { - cursor: not-allowed; - } -} - -.cm-lineNumbers .cm-activeLineGutter { - @apply rounded-l-sm!; -} - -.suggestion-highlight { - @apply bg-blue-200 hover:bg-blue-300 dark:hover:bg-blue-400/50 dark:text-blue-50 dark:bg-blue-500/40; -} - -/* minimal scrollbar styling */ -::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: var(--border); - border-radius: 3px; - transition: background 0.2s ease; -} - -::-webkit-scrollbar-thumb:hover { - background: --alpha(var(--muted-foreground) / 0.5); -} - -::-webkit-scrollbar-corner { - background: transparent; -} - -/* firefox scrollbar styling */ -* { - scrollbar-width: thin; - scrollbar-color: var(--border) transparent; -} - -@theme inline { - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} - -/* AI 思考动画 */ -@keyframes shimmer { - 0% { - background-position: -200% 0; - } - 100% { - background-position: 200% 0; + .pattern-grid-lg { + background-image: linear-gradient(to right, currentColor 1px, transparent 1px), + linear-gradient(to bottom, currentColor 1px, transparent 1px); + background-size: 40px 40px; } } diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 860c6b81e..cc3c1cfb0 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -1261,7 +1261,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF autoTitle: title, }, }, - }); + } as any); }; const cachedTitle = imageAutoTitleCacheRef.current.get(imageUrl); @@ -3477,6 +3477,12 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF const detailPlaceholderItemHeight = DETAIL_TARGET_HEIGHT; const detailPlaceholderStartX = 120; const detailPlaceholderStartY = 120; + const getDetailPlaceholderPosition = (index: number) => ({ + x: detailPlaceholderStartX, + y: + detailPlaceholderStartY + + index * (detailPlaceholderItemHeight + detailPlaceholderSpacing), + }); const expectedPlaceholderCount = bundleAttachments.length > 0 ? Math.min(20, detailCount + 1) : detailCount; const patchDetailPlaceholder = ( @@ -3529,14 +3535,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF }; const ensureDetailPlaceholders = (count: number, defaultLabel: string) => { for (let index = detailPlaceholderNodeIds.length; index < count; index += 1) { - const column = index % 3; - const row = Math.floor(index / 3); - const x = - detailPlaceholderStartX + - column * (detailPlaceholderItemWidth + detailPlaceholderSpacing); - const y = - detailPlaceholderStartY + - row * (detailPlaceholderItemHeight + detailPlaceholderSpacing); + const { x, y } = getDetailPlaceholderPosition(index); const nodeId = addNode({ type: "image", position: { x, y }, @@ -4042,12 +4041,11 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF const reviewGroups: DetailReviewGroup[] = []; const reviewFailures: string[] = []; - const detailRows = Math.ceil(prompts.length / 3); const detailStartX = 120; const detailStartY = 120; const detailSpacing = 24; const reviewStartY = - detailStartY + detailRows * (DETAIL_TARGET_HEIGHT + detailSpacing) + 120; + detailStartY + prompts.length * (DETAIL_TARGET_HEIGHT + detailSpacing) + 120; for (let reviewIndex = 0; reviewIndex < reviewPlans.length; reviewIndex += 1) { const reviewPlan = reviewPlans[reviewIndex]!; @@ -5340,6 +5338,33 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF return (
+ {/* 全局遮罩提示 - 当处于画布选图模式时显示 */} + {detailSlotPickerIndex !== null && ( +
+
+
+ +
+
+

正在从画布选择图片

+

+ 请点击画布中的任意图片以填入 + + “{DETAIL_SLOT_CONFIGS[detailSlotPickerIndex]?.label || "素材槽位"}” + +

+
+ +
+
+ )} {/* Header - Minimal with Icons */}
@@ -5980,278 +6005,381 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF renderableVisibleMessages.length === 0 ? "max-h-[62vh]" : "max-h-[54vh]" )} > -
-
-

- AI详情生成工作台(PC) -

-

- 固定规格 {DETAIL_TARGET_WIDTH} x {DETAIL_TARGET_HEIGHT},先分析后生成,保持整套详情图风格统一。 -

+
+
+
+ +
+
+
+

+ AI详情生成工作台 +

+ + PC端 + +
+

+ 全链路智能生成 · {DETAIL_TARGET_WIDTH}x{DETAIL_TARGET_HEIGHT}px +

+
- - +
{detailProgress.status === "running" - ? "执行中" + ? "生成中" : detailProgress.status === "done" ? "已完成" : detailProgress.status === "error" ? "异常" : "待启动"} - +
+
{isDetailWorkbenchCollapsed ? ( ) : ( <> -
-
- {detailProgress.stage || "待开始:请先补齐输入信息与素材。"} - - {detailProgress.current}/{detailProgress.total || 0} +
+
+ + {detailProgress.stage || "准备就绪"} + + + {detailProgress.current}/{detailProgress.total || 0} 步骤
-
+
+ {!detailProgress.stage && ( +

+ 请先补齐必填信息与素材,然后点击下方发送按钮开始生成。 +

+ )}
-
-

提示:鼠标悬停输入框可查看填写示例。

-
- - {detailSections.required ? ( -
- updateDetailBriefField("productSpu", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.productSpu} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="商品SPU(建议填写)" - /> - updateDetailBriefField("demandType", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.demandType} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="需求类型(建议填写)" - /> - updateDetailBriefField("targetAudience", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.targetAudience} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="主购买人群 *" - /> - updateDetailBriefField("usageScenario", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.usageScenario} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="需求场景 *" - /> - updateDetailBriefField("coreNeed", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.coreNeed} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="核心需求 *" - /> - updateDetailBriefField("productName", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.productName} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="产品名称(选填)" - /> - updateDetailBriefField("mainUser", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.mainUser} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="主使用者(选填)" - /> - updateDetailBriefField("triggerMoment", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.triggerMoment} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="触发时刻(选填)" - /> - updateDetailBriefField("riskConcern", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.riskConcern} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="用户顾虑(选填)" - /> - updateDetailBriefField("expectedResult", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.expectedResult} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="期望结果(选填)" - /> - updateDetailBriefField("coreSellingPoint", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.coreSellingPoint} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400 xl:col-span-2" - placeholder="核心卖点关键词(选填)" - /> - updateDetailBriefField("positioningStatement", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.positioningStatement} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400 xl:col-span-2" - placeholder="一句话定位(选填)" - /> -
- ) : null} -
- -
- - {detailSections.strategy ? ( -
- updateDetailBriefField("priceBand", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.priceBand} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="价格带(选填)" - /> - updateDetailBriefField("brandTone", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.brandTone} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400" - placeholder="品牌语气(选填)" - /> - updateDetailBriefField("competitorReference", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.competitorReference} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400 xl:col-span-2" - placeholder="竞品对标(选填)" - /> - updateDetailBriefField("bannedWords", event.target.value)} - title={DETAIL_FIELD_EXAMPLES.bannedWords} - className="h-8 rounded-lg border border-black/15 bg-white px-2 text-xs outline-none focus:border-blue-400 xl:col-span-2" - placeholder="禁用表达(选填,逗号分隔)" - /> -