/* ===== Screens Part 4: Longform Agent ===== */

const MONTX_DEFAULT_SKILL = `# MONTX Digital Editorial Design System

你是顶级数字媒体画册设计师。你的任务是严格执行 **Avant-garde Bauhaus Grid（先锋建筑与包豪斯网格）** 风格排版，专用于静态长图图文设计。

## 1. 核心视觉哲学
- **工业秩序感**：利用粗犷的几何分割替代诗意留白。
- **硬核对比**：大面积纯色块撞色（酒红 vs 骨白），严禁使用渐变、毛玻璃或阴影。
- **图文分离**：图片必须全通栏撑满，文字区块独立呈现，绝不允许在图片上覆盖文字。

## 2. 品牌规格 (Style Tokens)
- **画布限制**：最外层容器 \`width: 1080px\`，居中对齐。
- **色彩规范 (Strict)**：
  - 全局背景：\`#E1E0D8\` (骨白)
  - 强调色/模块色：\`#580018\` (酒红)
  - 文本色：\`#000000\` (黑) 于骨白底；\`#FFFFFF\` (白) 于酒红底。
- **字体规范**：
  - 英文：\`Helvetica\`
  - 中文：\`SourceHanSansCN-Bold\` (思源黑体加粗)
  - 字号：正文 \`34px - 38px\`，大标题 \`110px+\`，小标题 \`40px - 70px\`。
- **线条规范**：装饰线及边框统一使用酒红色，厚度为 \`12px\` 或 \`24px\`。

## 3. 结构组件库 (Components)
### A. Header (页眉)
- 顶部 \`24px\` 酒红实线封顶。
- 标题 \`MONTX V10\` 单行大写，使用 \`title-giant\` (110px) 样式。
- 下方紧跟 \`12px\` 装饰线及引言段落。

### B. Image Block (全通栏图片)
- 属性：\`w-full block\`，\`referrerpolicy="no-referrer"\`。
- 禁止：圆角、外边距、拼图。

### C. Narrative Block (文本模块)
- **水印背景**：使用极其巨大的实心数字（如 01, 02），颜色 \`#580018\`，透明度 \`10%\`。
- **排版逻辑**：
  - 章节标题：酒红底白字块，内边距 \`px-16 py-8\`。
  - 正文段落：左侧 \`12px\` 酒红实心垂直线（border-l），字体样式为常规权重，严禁单字加粗。

### D. Grid Content (网格化内容)
- 采用 \`grid-cols-12\` 结构。
- 左侧（4-5列）通常为酒红纯色块，用于平衡视觉权重。
- 右侧（7-8列）为文字细分项，使用 \`border-t-[2px]\` 进行横向切割。

### E. Footer (页脚)
- 背景：\`#580018\`。
- 内容：单行大标题 + 官网链接。

## 4. 交付约束
- **Single-File Mandate**：所有 HTML、Tailwind CSS 必须集成在一个文件内。
- **No Interaction**：移除所有 hover、动画、滚动效果。
- **Text Weight**：除标题外，所有段落文本强制执行 \`font-weight: normal\`，禁止任何形式的单字加粗。`;

const LONGFORM_DEFAULT_STYLE_TEMPLATE_ID = 'montx-default';
const LONGFORM_CUSTOM_STYLE_TEMPLATE_ID = 'custom-upload';

const LONGFORM_DEFAULT_STYLE_TEMPLATE = {
  id: LONGFORM_DEFAULT_STYLE_TEMPLATE_ID,
  name: 'MONTX 默认',
  fileName: 'MONTX_DEFAULT_SKILL.md',
  markdown: MONTX_DEFAULT_SKILL,
};

const LONGFORM_NARRATIVE_MODES = [
  { id: 'brand_product', label: '品牌 + 产品', description: '先建立 MONTX 的判断，再用 P10/V10 作为证据落点。' },
  { id: 'brand', label: '品牌向', description: '强调 MONTX 的世界观、价值主张和表达边界，避免过度产品广告化。' },
  { id: 'product', label: '产品向', description: '围绕所选产品的能力、场景、证据和边界展开。' },
  { id: 'brief_only', label: '仅 Brief', description: '优先执行当前 brief，不主动加入品牌或产品叙事。' },
];

const getLongformNarrativeMode = (mode = 'brand_product') => LONGFORM_NARRATIVE_MODES.find(item => item.id === mode) || LONGFORM_NARRATIVE_MODES[0];

const MONTX_PRODUCT_PROFILES = [
  {
    id: 'montx-p10',
    name: 'P10',
    aliases: ['MONTX P10', 'P10', 'Pickup 01', '未来先锋皮卡', '全域移动地空母舰'],
    positioning: 'MONTX P10 是概念阶段的未来先锋皮卡，以“全域移动地空母舰”承载地空双栖、全域适应与机能美学叙事。',
    audience: ['跨界多用途尝鲜型用户', '户外探索者', '科考/摄影/露营/指挥等复合场景用户'],
    tone: ['开拓', '机能', '先锋', '克制', '概念边界清晰'],
    requiredClaims: ['标注概念车叙事', '强调全域适应/全域智能/全域美学', '把载人飞行器等表达限定为概念愿景'],
    forbiddenClaims: ['量产承诺', '完全自动驾驶', '无人驾驶', '绝对安全', '已上市/可交付', '确定配置/价格/交付时间'],
    narrative: {
      positioning: 'P10 是征服地表与天空的“全域移动地空母舰”，将皮卡从单一装载工具升维为支持立体出行的未来装备。',
      coreCapabilities: ['地空双栖概念', '四轮分布式驱动电机', '智能底盘与底盘扭矩矢量控制', '车顶/车斗模块化', '战机灰机能涂装与雨林线'],
      userScenarios: ['越野穿越', '摄影/科考', '露营', '指挥车', '无人区探索', '空中侦察/补给概念'],
      proofPoints: ['地空一体视觉张力', '全域机能美学', '主动生态伴随', '从工具到平台的升维'],
      comparisonBoundaries: ['不做竞品压制', '不把概念车能力写成量产承诺', '不暗示自动驾驶替代驾驶员'],
      requiredMentions: ['概念车', '全域移动地空母舰', '全域适应', '全域智能', '全域美学'],
      forbiddenClaims: ['量产承诺', '完全自动驾驶', '无人驾驶', '绝对安全', '已上市/可交付', '确定配置/价格/交付时间'],
    },
    source: 'gac-brand-assets-2026-04',
  },
  {
    id: 'montx-v10',
    name: 'V10',
    aliases: ['MONTX V10', 'V10', '概念 VAN', '全域移动整备舱', '数字游民大本营'],
    positioning: 'MONTX V10 是概念阶段的全域移动整备舱，面向数字游民、户外探索与移动办公/旅居复合场景。',
    audience: ['数字游民', '户外探索者', '移动办公用户', '长周期旅居用户', '跨界多用途尝鲜型用户'],
    tone: ['理性浪漫', '工业秩序感', '克制', '温度感', '概念边界清晰'],
    requiredClaims: ['标注概念车叙事', '说明移动办公/旅居第三空间', '强调模块化空间与文明秩序感'],
    forbiddenClaims: ['量产承诺', '完全自动驾驶', '替代驾驶员', '绝对安全', '已上市/可交付', '确定配置/价格/交付时间'],
    narrative: {
      positioning: 'V10 是能在任意场景下切换工作、生活与旅行的“全域移动整备舱”，是数字游民的大本营和户外探险移动的家。',
      coreCapabilities: ['线控滑移方向盘概念', '智能双舱', '模块化座舱', '工作/社交/睡眠场景切换', '洗漱系统/模块化收纳/基础卫浴功能', '时间感知自动进化概念'],
      userScenarios: ['移动办公', '长周期旅居', '荒野驻留', '社交沙龙', '户外探险', '跨市场左右舵切换概念'],
      proofPoints: ['工业硬装与户外软装融合', '四开式尾门展开', '自循环基地', '隐形管家', '文明秩序与尊严'],
      comparisonBoundaries: ['不把自动切换、无感调节写成量产承诺', '不暗示无人驾驶或替代驾驶员', '不使用夸张竞品对比'],
      requiredMentions: ['概念车', '全域移动整备舱', '数字游民', '移动办公/旅居第三空间', '工业秩序感'],
      forbiddenClaims: ['量产承诺', '完全自动驾驶', '替代驾驶员', '绝对安全', '已上市/可交付', '确定配置/价格/交付时间'],
    },
    source: 'gac-brand-assets-2026-04',
  },
];

const LONGFORM_BRANDS = [
  {
    id: 'montx',
    name: 'MONTX',
    activeSkillId: 'montx-digital-editorial',
    products: MONTX_PRODUCT_PROFILES,
    narrative: {
      positioning: 'MONTX / 领越是广汽领程面向全球市场的“全域跨界新物种”，服务希望一部车满足商业、生活与探索多重需求的用户。',
      worldview: '移动出行世界中，场景、功能与用途的边界正在融合和重塑；MONTX 以跨界开拓精神，让用户在事业与生活之间获得更自由的全域场景体验。',
      valueProposition: '以一车多能、自由切换、全域适应和共创生态，帮助用户拓展事业与生活的边界。',
      voice: ['开拓', '全域', '机能', '先锋', '共创', '克制', '概念边界清晰'],
      requiredThemes: ['全域跨界新物种', 'New Breed. No Boundaries.', '一车多能', '自由切换', '全域智能', '全域适应', '全域美学', '共创更自由美好的事业与生活'],
      forbiddenAngles: ['把概念车写成量产承诺', '完全自动驾驶承诺', '无人驾驶暗示', '绝对安全', '过度玩梗', '轻浮自嘲', '竞品碾压', '财富自由夸大承诺'],
      proofTypes: ['品牌核心讯息', '产品概念车叙事', '真实用户场景', '共创机制', '视觉规范', '领导讲话/新闻稿'],
    },
    constraints: [
      { id: 'concept-boundary', label: '概念车能力必须标注边界', severity: 'required', source: 'gac-brand-assets-2026-04', description: 'P10/V10 的载人飞行器、线控滑移方向盘、时间感知自动进化等表达必须标注为概念车/愿景叙事，不能写成量产承诺。', defaultLocked: true, forbiddenTerms: ['已量产', '即将交付', '确定上市', '标配', '价格'] },
      { id: 'no-autonomous-overclaim', label: '禁止自动驾驶过度承诺', severity: 'required', source: 'gac-brand-assets-2026-04', description: '涉及自动识别、自动接管、主动调整、无人区侦察等表达时，不得暗示完全自动驾驶、无人驾驶、替代驾驶员或绝对安全。', defaultLocked: true, forbiddenTerms: ['完全自动驾驶', '无人驾驶', '替代驾驶员', '绝对安全', '解放双手'] },
      { id: 'niuma-context-only', label: '“牛马”仅限共创项目语境', severity: 'required', source: 'gac-brand-assets-2026-04', description: '“牛马”只允许出现在“牛马野逍遥”共创计划相关语境，不进入品牌常规口吻，不替用户自嘲。', defaultLocked: true, forbiddenTerms: ['牛马'] },
      { id: 'wealth-freedom-caution', label: '慎用财富自由', severity: 'required', source: 'gac-brand-assets-2026-04', description: '“财富自由”只能作为讲话稿历史素材引用，常规生成中需避免金融收益、成本收益或商业成功的夸大承诺。', defaultLocked: true, forbiddenTerms: ['财富自由'] },
      { id: 'no-gradient', label: '禁止渐变 / 毛玻璃 / 阴影', severity: 'required', source: 'brand-policy', description: '保持硬核对比和纯色块，不使用柔化视觉效果。', defaultLocked: true },
      { id: 'no-image-text-overlay', label: '禁止图片压字', severity: 'required', source: 'brand-policy', description: '图片必须独立呈现，文字区块与图片分离。', defaultLocked: true },
      { id: 'full-bleed-image', label: '图片必须全通栏', severity: 'required', source: 'brand-policy', description: '图片模块以 1080px 宽度铺满，不做拼图和局部圆角。', defaultLocked: true },
      { id: 'no-rounded-collage', label: '禁止圆角与拼图', severity: 'required', source: 'brand-policy', description: 'Image Block 必须 w-full block，不加圆角、边距、拼接。', defaultLocked: true },
      { id: 'normal-body-weight', label: '正文禁止单字加粗', severity: 'required', source: 'brand-policy', description: '除标题外，段落文本强制 normal，不做局部加粗强调。', defaultLocked: true },
      { id: 'canvas-1080', label: '画布宽度固定 1080px', severity: 'required', source: 'brand-policy', description: 'HTML 预览与 PNG 导出目标均为 1080px 宽。', defaultLocked: true },
      { id: 'single-file-html', label: '单文件 HTML', severity: 'required', source: 'brand-policy', description: '所有样式集成在导出的 HTML 中，便于后续转图。', defaultLocked: true },
      { id: 'no-interaction', label: '无交互动画', severity: 'required', source: 'brand-policy', description: '长图图文为静态交付，移除 hover、动画和滚动效果。', defaultLocked: true },
    ],
  },
  {
    id: 'aurelia',
    name: 'Aurelia Motors',
    activeSkillId: 'aurelia-editorial-reserved',
    products: [
      {
        id: 'aurelia-ax7',
        name: 'AX-7',
        aliases: ['Aurelia AX-7', 'AX-7'],
        positioning: 'Aurelia Motors 旗舰纯电轿跑 SUV 首发产品，承载静奢、东方家庭和高端智能出行叙事。',
        audience: ['中高端家庭用户', '纯电 SUV 关注者', '品牌首发关注者'],
        tone: ['静奢', '克制', '高级', '家庭场景'],
        requiredClaims: ['旗舰首发', '静奢气质', '家庭第二台车场景'],
        forbiddenClaims: ['促销秒杀', '赛道碾压', '暴力加速'],
        narrative: {
          positioning: 'AX-7 是 Aurelia Motors 的旗舰首发产品，承担品牌进入中高端纯电轿跑 SUV 市场的叙事任务。',
          coreCapabilities: ['静奢设计', '家庭场景舒适性', '高端纯电体验'],
          userScenarios: ['家庭第二台车', '城市周末出行', '品牌首发认知建立'],
          proofPoints: ['设计语言', '空间与舒适性', '纯电体验', '品牌调性'],
          comparisonBoundaries: ['不以赛道和暴力加速作为主叙事', '不做低价促销化表达'],
          requiredMentions: ['AX-7 旗舰首发', '静奢', '家庭场景'],
          forbiddenClaims: ['促销秒杀', '碾压竞品', '暴力加速'],
        },
        source: 'workspace-seed',
      },
      {
        id: 'aurelia-ax5',
        name: 'AX-5',
        aliases: ['Aurelia AX-5', 'AX-5'],
        positioning: 'Aurelia Motors 城市家庭产品线。',
        audience: ['城市家庭用户'],
        tone: ['克制', '实用', '温和'],
        requiredClaims: ['城市家庭', '日常使用'],
        forbiddenClaims: ['赛道化表达'],
        narrative: { positioning: 'AX-5 面向城市家庭日常使用。' },
        source: 'workspace-seed',
      },
      {
        id: 'aurelia-concept-s',
        name: 'Concept-S',
        aliases: ['Aurelia Concept-S', 'Concept-S'],
        positioning: 'Aurelia Motors 概念产品线，用于未来设计方向表达。',
        audience: ['品牌关注者', '设计媒体'],
        tone: ['前瞻', '克制', '设计感'],
        requiredClaims: ['未来设计方向'],
        forbiddenClaims: ['量产承诺'],
        narrative: { positioning: 'Concept-S 用于表达 Aurelia Motors 的未来设计方向。' },
        source: 'workspace-seed',
      },
    ],
    narrative: {
      positioning: 'Aurelia Motors 是中高端纯电出行品牌，强调静奢、克制和东方家庭场景。',
      worldview: '电动豪华不应依赖喧闹性能叙事，而应通过克制设计、家庭场景和长期信任建立心智。',
      valueProposition: '以静奢设计和可靠体验服务高品质家庭出行。',
      voice: ['克制', '静奢', '高级', '温和'],
      requiredThemes: ['静奢', '东方家庭', '纯电体验', '长期信任'],
      forbiddenAngles: ['促销化表达', '赛道暴力叙事', '竞品碾压'],
      proofTypes: ['设计语言', '家庭场景', '用户洞察', '产品体验证据'],
    },
    constraints: [
      { id: 'aurelia-tone', label: '克制 / 静奢 / 非促销', severity: 'required', source: 'brand-policy', description: '避免硬广、秒杀、碾压等转化型表达。', defaultLocked: true },
      { id: 'aurelia-no-racing', label: '回避赛车对比', severity: 'required', source: 'brand-policy', description: '不以赛道、暴力加速、竞品压制作为主叙事。', defaultLocked: true },
    ],
  },
];

const LONGFORM_STEPS = ['需求输入', '内容推演', '图片分配', 'HTML 预览', 'PNG 导出'];

const LONGFORM_BLOCK_TYPES = [
  { id: 'hero', label: '开场 Hero', type: 'Header', layoutModule: 'Header', description: '主标题、导语和首张主视觉，负责建立第一屏判断。', required: true },
  { id: 'narrative', label: '叙事段落', type: 'Narrative', layoutModule: 'Narrative Block', description: '观点推进、用户问题、背景说明或论证正文。', required: true },
  { id: 'tech_breakdown', label: '技术拆解', type: 'Narrative', layoutModule: 'Narrative Block', description: '把复杂机制拆成可理解的步骤、原理和边界。', required: false },
  { id: 'image_proof', label: '图片证据', type: 'Image', layoutModule: 'Image Block', description: '用于承载真实场景、产品图、道路图或视觉证据组。', required: false },
  { id: 'metrics', label: '指标信息', type: 'Grid', layoutModule: 'Grid Content', description: '适合参数、能力清单、场景覆盖和高密度信息。', required: false },
  { id: 'compare', label: '对比解释', type: 'Grid', layoutModule: 'Grid Content', description: '适合能力边界、前后对比、同类方案差异说明。', required: false },
  { id: 'timeline', label: '流程时间线', type: 'Narrative', layoutModule: 'Narrative Block', description: '适合流程、决策链路、阶段递进和使用步骤。', required: false },
  { id: 'cta', label: '收束 CTA', type: 'Footer', layoutModule: 'Footer', description: '结尾行动、品牌收束、官网或预约入口。', required: true },
];

const getLongformBlockType = (section = {}) => {
  if (typeof section === 'string') return LONGFORM_BLOCK_TYPES.find(block => block.id === section) || LONGFORM_BLOCK_TYPES[1];
  if (section.blockType) return getLongformBlockType(section.blockType);
  const haystack = `${section.type || ''} ${section.layoutModule || ''} ${section.title || ''}`.toLowerCase();
  if (/header|hero|开场|标题/.test(haystack)) return LONGFORM_BLOCK_TYPES[0];
  if (/footer|cta|官网|预约|收束/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'cta');
  if (/image|图片|证据|场景/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'image_proof');
  if (/grid|指标|参数|覆盖|清单/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'metrics');
  if (/对比|差异|竞品/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'compare');
  if (/流程|路径|时间线|阶段|链路/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'timeline');
  if (/技术|原理|算法|传感器|决策/.test(haystack)) return LONGFORM_BLOCK_TYPES.find(block => block.id === 'tech_breakdown');
  return LONGFORM_BLOCK_TYPES[1];
};

const normalizeLongformSection = (section = {}, index = 0) => {
  const block = getLongformBlockType(section);
  return {
    id: section.id || `${block.type.toLowerCase()}-${Date.now()}-${index}`,
    type: block.type,
    blockType: block.id,
    title: section.title || LONGFORM_DEFAULT_SECTIONS[index % LONGFORM_DEFAULT_SECTIONS.length]?.title || block.label,
    body: section.body || '',
    imageIds: Array.isArray(section.imageIds) ? section.imageIds : [],
    layoutModule: block.layoutModule,
    imageHint: section.imageHint || '',
  };
};

const LONGFORM_DEFAULT_SECTIONS = [
  { id: 'hero', type: 'Header', blockType: 'hero', title: 'MONTX V10', body: '一篇面向微信公众号的长图文，先建立观点，再用强秩序视觉把阅读节奏压实。', imageIds: [], layoutModule: 'Header', imageHint: '可放置一张建立主题气质的全通栏主视觉，避免文字压图。' },
  { id: 's1', type: 'Narrative', blockType: 'narrative', title: '为什么现在需要一篇长文', body: '用户不是缺少信息，而是缺少一个被组织过的判断。长图文的任务，是把分散需求压缩成可阅读、可转发、可记忆的结构。', imageIds: [], layoutModule: 'Narrative Block', imageHint: '选择能证明问题现场或用户情境的图片，作为段落后的全通栏证据。' },
  { id: 's2', type: 'Image', blockType: 'image_proof', title: '场景证据', body: '图片用于承载情绪和现实感，必须全通栏展示，不在图上叠字。', imageIds: [], layoutModule: 'Image Block', imageHint: '优先放最有信息量的实拍图，可连续添加多张形成证据组。' },
  { id: 's3', type: 'Grid', blockType: 'metrics', title: '内容推演框架', body: '目标读者 / 核心论点 / 证据链 / CTA 被拆成网格化模块，便于后续转成长图。', imageIds: [], layoutModule: 'Grid Content', imageHint: '适合补充细节、对比、局部特写或流程节点图片。' },
  { id: 'footer', type: 'Footer', blockType: 'cta', title: '前往官网', body: 'montx.example.com', imageIds: [], layoutModule: 'Footer', imageHint: '通常不配图；如需品牌收束，可放产品或品牌环境图。' },
];

const LONGFORM_MOCK_TASKS = [
  {
    id: 'lf-ax-001',
    brandId: 'montx',
    productId: 'montx-v10',
    narrativeMode: 'brand_product',
    title: 'MONTX V10 旅行编辑长图',
    prompt: '为微信公众号设计一篇面向高净值旅行用户的长图文，主题是大溪地海岛资产与现代生活方式。',
    status: 'HTML 已生成',
    activeStyleTemplateId: LONGFORM_DEFAULT_STYLE_TEMPLATE_ID,
    skillMarkdown: MONTX_DEFAULT_SKILL,
    images: [],
    sections: LONGFORM_DEFAULT_SECTIONS,
    html: '',
    messages: [
      { role: 'assistant', text: '把这篇公众号长图文的目标、读者、语气和必须覆盖的观点发给我。我会先推演结构，再把它拆成可排版的 section。' },
      { role: 'user', text: '为微信公众号设计一篇面向高净值旅行用户的长图文，主题是大溪地海岛资产与现代生活方式。' },
    ],
    overrides: [],
    qa: { score: 92, issues: ['2 个图片位待上传'], exportReady: true },
    updatedAt: '2026-04-27 09:40',
  },
  {
    id: 'lf-ax-002',
    brandId: 'montx',
    productId: 'montx-v10',
    narrativeMode: 'brand',
    title: '城市更新观点稿',
    prompt: '从设计媒体视角写一篇观点长文，解释为什么工业秩序感正在回到数字出版。',
    status: '待上传图片',
    activeStyleTemplateId: LONGFORM_DEFAULT_STYLE_TEMPLATE_ID,
    skillMarkdown: MONTX_DEFAULT_SKILL,
    images: [],
    sections: LONGFORM_DEFAULT_SECTIONS.map((s, i) => ({ ...s, id: `${s.id}-2`, title: i === 0 ? 'GRID IS BACK' : s.title })),
    html: '',
    messages: [
      { role: 'assistant', text: '请给我这次长图文的目标和读者，我会推演文章结构。' },
      { role: 'user', text: '从设计媒体视角写一篇观点长文，解释为什么工业秩序感正在回到数字出版。' },
    ],
    overrides: [{ constraintId: 'full-bleed-image', reason: '客户要求保留一张证件式人物肖像，暂不全通栏。', createdAt: '2026-04-27 10:12' }],
    qa: { score: 76, issues: ['存在 1 项解除约束'], exportReady: true },
    updatedAt: '2026-04-27 10:12',
  },
];

const longformId = (prefix = 'lf') => `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 7)}`;

const MONTX_EXPORT_CSS = `
.montx-canvas{width:1080px;margin:0 auto;background:var(--lf-bg,#E1E0D8);color:var(--lf-text,#000);font-family:var(--lf-font,Helvetica,Arial,"Source Han Sans CN","Noto Sans SC",sans-serif);overflow:hidden;}
.montx-topline{height:24px;background:var(--lf-accent,#580018);}
.montx-giant{padding:58px 64px 24px;font-size:118px;line-height:.9;font-weight:900;letter-spacing:-.06em;text-transform:uppercase;}
.montx-rule{height:12px;background:var(--lf-accent,#580018);margin:0 64px 36px;}
.montx-lead{padding:0 64px 72px;font-size:38px;line-height:1.35;font-weight:400;}
.montx-image{display:block;width:100%;border:0;border-radius:0;margin:0;}
.montx-narrative{position:relative;min-height:360px;padding:88px 64px 92px;}
.montx-watermark{position:absolute;right:48px;top:18px;font-size:220px;line-height:1;color:var(--lf-watermark,rgba(88,0,24,.1));font-weight:900;}
.montx-section-title{display:inline-block;background:var(--lf-accent,#580018);color:var(--lf-inverse,#fff);font-size:54px;line-height:1;font-weight:900;padding:18px 28px;margin-bottom:36px;}
.montx-body{position:relative;z-index:1;border-left:12px solid var(--lf-accent,#580018);padding-left:34px;font-size:36px;line-height:1.55;font-weight:400;}
.montx-grid{display:grid;grid-template-columns:4fr 8fr;border-top:24px solid var(--lf-accent,#580018);border-bottom:24px solid var(--lf-accent,#580018);}
.montx-grid-left{background:var(--lf-accent,#580018);color:var(--lf-inverse,#fff);font-size:64px;line-height:1.05;font-weight:900;padding:54px 44px;}
.montx-grid-right{padding:50px 58px;font-size:34px;line-height:1.5;font-weight:400;}
.montx-footer{background:var(--lf-accent,#580018);color:var(--lf-inverse,#fff);padding:60px 64px 72px;}
.montx-footer h2{margin:0 0 18px;font-size:86px;line-height:.95;font-weight:900;letter-spacing:-.04em;}
.montx-footer p{margin:0;font-size:34px;font-weight:400;}
`;

const escapeHtml = (value = '') => String(value)
  .replaceAll('&', '&amp;')
  .replaceAll('<', '&lt;')
  .replaceAll('>', '&gt;')
  .replaceAll('"', '&quot;')
  .replaceAll("'", '&#039;');

const inferLongformConstraintCategory = (constraint = {}) => {
  const text = `${constraint.label || ''} ${constraint.description || ''}`.toLowerCase();
  if (/图片|image|圆角|拼图|压字|全通栏/.test(text)) return 'image';
  if (/字号|画布|宽度|html|动画|交互|渐变|阴影|毛玻璃|layout|style/.test(text)) return 'style';
  if (/自动驾驶|解放双手|承诺|竞品|促销|话术|claim/.test(text)) return 'claim';
  return 'brand';
};

const normalizeLongformConstraint = (constraint, index = 0) => ({
  id: constraint.id || longformId('guard'),
  label: constraint.label || `品牌护栏 ${index + 1}`,
  description: constraint.description || '',
  severity: constraint.severity || 'required',
  category: constraint.category || inferLongformConstraintCategory(constraint),
  scope: constraint.scope || ['generation', 'preview', 'export', 'qa'],
  defaultLocked: constraint.defaultLocked !== false,
  enabled: constraint.enabled !== false,
  forbiddenTerms: constraint.forbiddenTerms || [],
  requiredTerms: constraint.requiredTerms || [],
  rules: constraint.rules || {},
  examples: constraint.examples || { bad: [], good: [] },
  source: constraint.source || 'brand-policy',
  createdAt: constraint.createdAt || 'seed',
  updatedAt: constraint.updatedAt || constraint.createdAt || 'seed',
});

const longformListFromValue = (value = '') => Array.isArray(value)
  ? value
  : String(value || '').split(/[；;，,、\n]/).map(item => item.trim()).filter(Boolean);

const normalizeBrandNarrative = (narrative = {}) => ({
  positioning: narrative.positioning || '',
  worldview: narrative.worldview || '',
  valueProposition: narrative.valueProposition || '',
  voice: longformListFromValue(narrative.voice),
  requiredThemes: longformListFromValue(narrative.requiredThemes),
  forbiddenAngles: longformListFromValue(narrative.forbiddenAngles),
  proofTypes: longformListFromValue(narrative.proofTypes),
});

const normalizeProductNarrative = (narrative = {}, product = {}) => ({
  positioning: narrative.positioning || product.positioning || '',
  coreCapabilities: longformListFromValue(narrative.coreCapabilities),
  userScenarios: longformListFromValue(narrative.userScenarios),
  proofPoints: longformListFromValue(narrative.proofPoints),
  comparisonBoundaries: longformListFromValue(narrative.comparisonBoundaries),
  requiredMentions: longformListFromValue(narrative.requiredMentions || product.requiredClaims),
  forbiddenClaims: longformListFromValue(narrative.forbiddenClaims || product.forbiddenClaims),
});

const normalizeLongformProduct = (product = {}, index = 0) => ({
  id: product.id || longformId('product'),
  name: product.name || `Product ${index + 1}`,
  aliases: longformListFromValue(product.aliases),
  positioning: product.positioning || '',
  audience: longformListFromValue(product.audience),
  tone: longformListFromValue(product.tone),
  requiredClaims: longformListFromValue(product.requiredClaims),
  forbiddenClaims: longformListFromValue(product.forbiddenClaims),
  narrative: normalizeProductNarrative(product.narrative, product),
  source: product.source || 'brand-product-profile',
  updatedAt: product.updatedAt || product.createdAt || 'seed',
});

const normalizeLongformBrandProfiles = (brands = LONGFORM_BRANDS) => brands.map(brand => ({
  ...brand,
  narrative: normalizeBrandNarrative(brand.narrative),
  products: (Array.isArray(brand.products) ? brand.products : brand.id === 'montx' ? MONTX_PRODUCT_PROFILES : []).map(normalizeLongformProduct),
  constraints: (brand.constraints || []).map(normalizeLongformConstraint),
}));

const normalizeSharedBrandProfiles = (brands) => {
  const source = brands || window.BrandMemoryStore?.DEFAULT_BRAND_PROFILES || LONGFORM_BRANDS;
  return window.BrandMemoryStore?.normalizeBrandProfiles
    ? window.BrandMemoryStore.normalizeBrandProfiles(source)
    : normalizeLongformBrandProfiles(source);
};

const getLongformBrand = (brandId, brands) => {
  const source = brands || window.BrandMemoryStore?.DEFAULT_BRAND_PROFILES || LONGFORM_BRANDS;
  return window.BrandMemoryStore?.getBrandProfile
    ? window.BrandMemoryStore.getBrandProfile(brandId, source)
    : source.find(b => b.id === brandId) || source[0] || LONGFORM_BRANDS[0];
};
const getLongformProduct = (brand, productId) => {
  if (window.BrandMemoryStore?.getProductProfile) return window.BrandMemoryStore.getProductProfile(brand, productId);
  const products = brand?.products || [];
  return products.find(product => product.id === productId) || products[0] || null;
};

const inferLongformProductId = (task = {}, brand) => {
  const text = `${task.productId || ''} ${task.title || ''} ${task.prompt || ''} ${(task.sections || []).map(section => section.title).join(' ')}`.toLowerCase();
  return (brand?.products || []).find(product => {
    const names = [product.name, ...(product.aliases || [])].filter(Boolean).map(item => item.toLowerCase());
    return names.some(name => text.includes(name));
  })?.id || '';
};

const normalizeLongformTask = (task, brands = LONGFORM_BRANDS) => {
  const brand = getLongformBrand(task.brandId, brands);
  const inferredProductId = inferLongformProductId(task, brand);
  const product = getLongformProduct(brand, task.productId || inferredProductId);
  return {
    ...task,
    images: Array.isArray(task.images) ? task.images : [],
    overrides: Array.isArray(task.overrides) ? task.overrides : [],
    messages: Array.isArray(task.messages) ? task.messages : [],
    productId: task.productId || product?.id || '',
    narrativeMode: task.narrativeMode || 'brand_product',
    sections: (task.sections || LONGFORM_DEFAULT_SECTIONS).map(normalizeLongformSection),
  };
};

const parseSkillMarkdown = (markdown = '') => {
  const title = markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || 'Untitled Skill';
  const hex = Array.from(new Set(markdown.match(/#[0-9A-Fa-f]{6}/g) || []));
  const width = markdown.match(/width:\s*(\d+)px/)?.[1] || markdown.match(/画布限制.*?(\d+)px/)?.[1] || '1080';
  const fontLines = (markdown.match(/(?:Helvetica|SourceHanSansCN|思源黑体|字体规范|font-weight)/g) || []).slice(0, 6);
  const components = Array.from(markdown.matchAll(/^###\s+(.+)$/gm)).map(m => m[1].trim());
  const bans = Array.from(markdown.matchAll(/(?:禁止|严禁|移除)([^。\n]+)/g)).map(m => m[0].trim()).slice(0, 8);
  const delivery = Array.from(markdown.matchAll(/\*\*([^*]+)\*\*：([^。\n]+)/g)).map(m => `${m[1]}：${m[2]}`).filter(s => /Mandate|Interaction|Weight|交付|Text/.test(s));
  return { title, hex, width, fontLines, components, bans, delivery, raw: markdown };
};

const LONGFORM_THEME_PRESETS = [
  { key: /Neo Pop|新波普/i, bg: '#F7EA2E', accent: '#F92672', text: '#101010', inverse: '#FFFFFF', font: 'Impact, Helvetica, Arial, "Noto Sans SC", sans-serif' },
  { key: /Nordic|北欧/i, bg: '#F4F0E8', accent: '#8FA99A', text: '#22302A', inverse: '#FFFFFF', font: 'Helvetica, Arial, "Noto Sans SC", sans-serif' },
  { key: /Vogue|高奢/i, bg: '#F7F2EA', accent: '#1A1A1A', text: '#17120E', inverse: '#F7F2EA', font: 'Georgia, "Times New Roman", "Noto Serif SC", serif' },
  { key: /Tech-Nomad|科技游牧/i, bg: '#071A1F', accent: '#00E0C7', text: '#EAFBF8', inverse: '#071A1F', font: 'Helvetica, Arial, "Noto Sans SC", sans-serif' },
  { key: /Clean Data|清洁数据/i, bg: '#F4F7F8', accent: '#126E82', text: '#102027', inverse: '#FFFFFF', font: 'Helvetica, Arial, "Noto Sans SC", sans-serif' },
  { key: /Cinematic|电影剧照/i, bg: '#111111', accent: '#B68D40', text: '#F2E8D8', inverse: '#111111', font: 'Georgia, "Times New Roman", "Noto Serif SC", serif' },
  { key: /Human Documentary|人文纪实/i, bg: '#EFE2D2', accent: '#9A3A2D', text: '#211A16', inverse: '#FFF8EC', font: 'Georgia, "Noto Serif SC", serif' },
  { key: /Archive Academia|档案学术/i, bg: '#ECE6D8', accent: '#2B3A67', text: '#1D2230', inverse: '#F7F1E5', font: 'Georgia, "Noto Serif SC", serif' },
  { key: /Organic Nature|自然有机/i, bg: '#F1EAD9', accent: '#587A46', text: '#263322', inverse: '#FFF8EA', font: 'Helvetica, Arial, "Noto Sans SC", sans-serif' },
  { key: /Bauhaus|包豪斯|MONTX/i, bg: '#E1E0D8', accent: '#580018', text: '#000000', inverse: '#FFFFFF', font: 'Helvetica, Arial, "Source Han Sans CN", "Noto Sans SC", sans-serif' },
];

const LONGFORM_LAYOUT_PRESETS = [
  { id: 'pop', className: 'lf-layout-pop', label: 'Neo Pop Poster', key: /Neo Pop|新波普/i },
  { id: 'nordic', className: 'lf-layout-nordic', label: 'Nordic Minimal', key: /Nordic|北欧/i },
  { id: 'vogue', className: 'lf-layout-vogue', label: 'Vogue Editorial', key: /Vogue|高奢/i },
  { id: 'tech', className: 'lf-layout-tech', label: 'Tech Nomad', key: /Tech-Nomad|科技游牧/i },
  { id: 'data', className: 'lf-layout-data', label: 'Clean Data Report', key: /Clean Data|清洁数据/i },
  { id: 'cinematic', className: 'lf-layout-cinematic', label: 'Cinematic Storyboard', key: /Cinematic|电影剧照/i },
  { id: 'documentary', className: 'lf-layout-documentary', label: 'Human Documentary', key: /Human Documentary|人文纪实/i },
  { id: 'archive', className: 'lf-layout-archive', label: 'Archive Academia', key: /Archive Academia|档案学术/i },
  { id: 'organic', className: 'lf-layout-organic', label: 'Organic Nature', key: /Organic Nature|自然有机/i },
  { id: 'bauhaus', className: 'lf-layout-bauhaus', label: 'Bauhaus Grid', key: /Bauhaus|包豪斯|MONTX/i },
];

const LONGFORM_FONT_SIZE_PRESETS = {
  'lf-layout-bauhaus': { heroTitlePx: 118, leadPx: 38, sectionTitlePx: 54, bodyPx: 36, gridLeftPx: 64, gridRightPx: 34, footerTitlePx: 86, footerBodyPx: 34 },
  'lf-layout-nordic': { heroTitlePx: 74, leadPx: 30, sectionTitlePx: 38, bodyPx: 30, gridLeftPx: 38, gridRightPx: 29, footerTitlePx: 48, footerBodyPx: 34 },
  'lf-layout-pop': { heroTitlePx: 126, leadPx: 34, sectionTitlePx: 48, bodyPx: 31, gridLeftPx: 58, gridRightPx: 30, footerTitlePx: 86, footerBodyPx: 34 },
  'lf-layout-vogue': { heroTitlePx: 92, leadPx: 28, sectionTitlePx: 46, bodyPx: 29, gridLeftPx: 58, gridRightPx: 28, footerTitlePx: 86, footerBodyPx: 34 },
  'lf-layout-tech': { heroTitlePx: 96, leadPx: 30, sectionTitlePx: 34, bodyPx: 28, gridLeftPx: 42, gridRightPx: 28, footerTitlePx: 86, footerBodyPx: 34 },
  'lf-layout-data': { heroTitlePx: 76, leadPx: 28, sectionTitlePx: 32, bodyPx: 27, gridLeftPx: 38, gridRightPx: 27, footerTitlePx: 86, footerBodyPx: 34 },
  'lf-layout-cinematic': { heroTitlePx: 88, leadPx: 28, sectionTitlePx: 32, bodyPx: 29, gridLeftPx: 46, gridRightPx: 28, footerTitlePx: 86, footerBodyPx: 34 },
  'lf-layout-documentary': { heroTitlePx: 82, leadPx: 30, sectionTitlePx: 40, bodyPx: 30, gridLeftPx: 44, gridRightPx: 29, footerTitlePx: 86, footerBodyPx: 34 },
  'lf-layout-archive': { heroTitlePx: 70, leadPx: 28, sectionTitlePx: 36, bodyPx: 28, gridLeftPx: 34, gridRightPx: 27, footerTitlePx: 86, footerBodyPx: 34 },
  'lf-layout-organic': { heroTitlePx: 78, leadPx: 30, sectionTitlePx: 38, bodyPx: 30, gridLeftPx: 44, gridRightPx: 29, footerTitlePx: 86, footerBodyPx: 34 },
};

const LONGFORM_LAYOUT_CSS = `
.longform-render .montx-giant,.longform-render .montx-section-title,.longform-render .montx-footer h2{letter-spacing:0;}
.longform-render.lf-text-center .montx-giant,.longform-render.lf-text-center .montx-lead,.longform-render.lf-text-center .montx-section-title,.longform-render.lf-text-center .montx-body,.longform-render.lf-text-center .montx-grid-left,.longform-render.lf-text-center .montx-grid-right,.longform-render.lf-text-center .montx-footer{text-align:center;}
.longform-render.lf-text-center .montx-section-title,.longform-render.lf-text-center .montx-body,.longform-render.lf-text-center .montx-lead{margin-left:auto;margin-right:auto;}
.longform-render.lf-text-center .montx-body,.longform-render.lf-text-center .montx-grid-right{border-left:0;padding-left:0;}
.longform-render.lf-layout-nordic .montx-topline{height:0;}
.longform-render.lf-layout-nordic .montx-giant{padding:96px 110px 18px;font-size:74px;line-height:1.08;font-weight:400;text-transform:none;}
.longform-render.lf-layout-nordic .montx-rule{height:1px;margin:0 110px 42px;background:color-mix(in oklab,var(--lf-accent) 45%,var(--lf-bg));}
.longform-render.lf-layout-nordic .montx-lead{padding:0 110px 96px;font-size:30px;line-height:1.75;}
.longform-render.lf-layout-nordic .montx-narrative{min-height:0;padding:96px 120px;}
.longform-render.lf-layout-nordic .montx-watermark{display:none;}
.longform-render.lf-layout-nordic .montx-section-title{display:block;background:transparent;color:var(--lf-text);padding:0;margin:0 0 28px;font-size:38px;font-weight:400;border-bottom:1px solid color-mix(in oklab,var(--lf-accent) 35%,var(--lf-bg));padding-bottom:22px;}
.longform-render.lf-layout-nordic .montx-body{border-left:0;padding-left:0;font-size:30px;line-height:1.85;}
.longform-render.lf-layout-nordic .montx-grid{display:block;border:0;padding:92px 110px;background:color-mix(in oklab,var(--lf-accent) 8%,var(--lf-bg));}
.longform-render.lf-layout-nordic .montx-grid-left{background:transparent;color:var(--lf-text);font-size:38px;padding:0 0 28px;font-weight:400;}
.longform-render.lf-layout-nordic .montx-grid-right{padding:0;font-size:29px;line-height:1.8;}
.longform-render.lf-layout-nordic .montx-footer{padding:92px 110px;background:var(--lf-bg);color:var(--lf-text);border-top:1px solid color-mix(in oklab,var(--lf-accent) 35%,var(--lf-bg));}
.longform-render.lf-layout-nordic .montx-footer h2{font-size:48px;font-weight:400;}

.longform-render.lf-layout-pop .montx-topline{height:42px;background:repeating-linear-gradient(90deg,var(--lf-accent) 0 90px,var(--lf-text) 90px 108px);}
.longform-render.lf-layout-pop .montx-giant{padding:56px 48px 22px;font-size:126px;line-height:.88;text-transform:uppercase;}
.longform-render.lf-layout-pop .montx-rule{height:0;margin:0 48px 24px;border-top:16px solid var(--lf-text);border-bottom:16px solid var(--lf-accent);}
.longform-render.lf-layout-pop .montx-lead{padding:28px 48px 72px;font-size:34px;line-height:1.35;background:var(--lf-text);color:var(--lf-inverse);}
.longform-render.lf-layout-pop .montx-narrative{padding:72px 54px;min-height:340px;border-top:10px solid var(--lf-text);}
.longform-render.lf-layout-pop .montx-watermark{right:34px;top:18px;font-size:180px;color:color-mix(in oklab,var(--lf-accent) 30%,transparent);}
.longform-render.lf-layout-pop .montx-section-title{font-size:48px;border:8px solid var(--lf-text);box-shadow:12px 12px 0 var(--lf-text);transform:rotate(-1deg);margin-bottom:44px;}
.longform-render.lf-layout-pop .montx-body{border-left:0;padding:34px;font-size:31px;line-height:1.5;background:color-mix(in oklab,var(--lf-inverse) 88%,var(--lf-bg));outline:6px solid var(--lf-text);}
.longform-render.lf-layout-pop .montx-grid{grid-template-columns:5fr 7fr;border:12px solid var(--lf-text);}
.longform-render.lf-layout-pop .montx-grid-left{font-size:58px;padding:48px 36px;text-transform:uppercase;}
.longform-render.lf-layout-pop .montx-grid-right{font-size:30px;padding:48px 42px;background:var(--lf-bg);}
.longform-render.lf-layout-pop .montx-footer{border-top:18px solid var(--lf-text);}

.longform-render.lf-layout-vogue .montx-topline{height:0;}
.longform-render.lf-layout-vogue .montx-giant{padding:116px 88px 28px;font-size:92px;line-height:1.02;font-weight:400;text-transform:uppercase;}
.longform-render.lf-layout-vogue .montx-rule{width:180px;height:1px;margin:0 88px 42px;background:var(--lf-text);}
.longform-render.lf-layout-vogue .montx-lead{padding:0 88px 120px;font-size:28px;line-height:1.85;max-width:760px;}
.longform-render.lf-layout-vogue .montx-image{margin:0 0 72px;}
.longform-render.lf-layout-vogue .montx-narrative{padding:104px 88px 116px;min-height:0;}
.longform-render.lf-layout-vogue .montx-watermark{font-size:92px;right:88px;top:92px;color:color-mix(in oklab,var(--lf-text) 10%,transparent);}
.longform-render.lf-layout-vogue .montx-section-title{background:transparent;color:var(--lf-text);font-size:46px;font-weight:400;padding:0;margin:0 0 46px;max-width:720px;}
.longform-render.lf-layout-vogue .montx-body{border-left:0;padding-left:0;font-size:29px;line-height:1.9;max-width:720px;}
.longform-render.lf-layout-vogue .montx-grid{display:block;border:0;padding:104px 88px;background:color-mix(in oklab,var(--lf-text) 5%,var(--lf-bg));}
.longform-render.lf-layout-vogue .montx-grid-left{background:transparent;color:var(--lf-text);font-size:58px;font-weight:400;padding:0 0 42px;}
.longform-render.lf-layout-vogue .montx-grid-right{padding:0 0 0 180px;font-size:28px;line-height:1.85;border-left:1px solid var(--lf-text);}
.longform-render.lf-layout-vogue .montx-footer{padding:110px 88px;background:var(--lf-text);color:var(--lf-bg);}

.longform-render.lf-layout-tech{background:linear-gradient(180deg,var(--lf-bg),color-mix(in oklab,var(--lf-accent) 12%,var(--lf-bg)));}
.longform-render.lf-layout-tech .montx-topline{height:18px;}
.longform-render.lf-layout-tech .montx-giant{padding:58px 56px 22px;font-size:96px;line-height:.95;text-transform:uppercase;border-left:18px solid var(--lf-accent);}
.longform-render.lf-layout-tech .montx-rule{height:2px;margin:0 56px 34px;background:var(--lf-accent);}
.longform-render.lf-layout-tech .montx-lead{padding:0 56px 78px;font-size:30px;line-height:1.6;}
.longform-render.lf-layout-tech .montx-narrative{padding:70px 56px;min-height:320px;border-top:1px solid color-mix(in oklab,var(--lf-accent) 50%,var(--lf-bg));}
.longform-render.lf-layout-tech .montx-watermark{font-size:148px;color:color-mix(in oklab,var(--lf-accent) 20%,transparent);}
.longform-render.lf-layout-tech .montx-section-title{background:transparent;color:var(--lf-accent);border:1px solid var(--lf-accent);font-size:34px;padding:14px 18px;}
.longform-render.lf-layout-tech .montx-body{border-left:2px solid var(--lf-accent);font-size:28px;line-height:1.7;}
.longform-render.lf-layout-tech .montx-grid{grid-template-columns:3fr 9fr;border-top:1px solid var(--lf-accent);border-bottom:1px solid var(--lf-accent);}
.longform-render.lf-layout-tech .montx-grid-left{font-size:42px;padding:42px 32px;}
.longform-render.lf-layout-tech .montx-grid-right{font-size:28px;padding:42px 46px;}

.longform-render.lf-layout-data .montx-topline{height:12px;}
.longform-render.lf-layout-data .montx-giant{padding:60px 64px 20px;font-size:76px;line-height:1.02;text-transform:none;}
.longform-render.lf-layout-data .montx-rule{height:1px;margin:0 64px 28px;}
.longform-render.lf-layout-data .montx-lead{padding:0 64px 58px;font-size:28px;line-height:1.6;}
.longform-render.lf-layout-data .montx-narrative{padding:54px 64px;min-height:260px;border-top:1px solid color-mix(in oklab,var(--lf-accent) 25%,var(--lf-bg));}
.longform-render.lf-layout-data .montx-watermark{font-size:72px;right:64px;top:54px;color:color-mix(in oklab,var(--lf-accent) 18%,transparent);}
.longform-render.lf-layout-data .montx-section-title{background:transparent;color:var(--lf-accent);font-size:32px;padding:0;margin-bottom:20px;text-transform:uppercase;}
.longform-render.lf-layout-data .montx-body{border-left:0;padding:26px 30px;font-size:27px;line-height:1.65;background:color-mix(in oklab,var(--lf-accent) 7%,var(--lf-bg));border-top:4px solid var(--lf-accent);}
.longform-render.lf-layout-data .montx-grid{grid-template-columns:4fr 8fr;border-top:4px solid var(--lf-accent);border-bottom:1px solid var(--lf-accent);}
.longform-render.lf-layout-data .montx-grid-left{font-size:38px;padding:36px;}
.longform-render.lf-layout-data .montx-grid-right{font-size:27px;padding:36px;}

.longform-render.lf-layout-cinematic .montx-topline{height:0;}
.longform-render.lf-layout-cinematic .montx-giant{padding:110px 70px 28px;font-size:88px;line-height:.98;text-transform:uppercase;}
.longform-render.lf-layout-cinematic .montx-rule{height:2px;margin:0 70px 36px;background:var(--lf-accent);}
.longform-render.lf-layout-cinematic .montx-lead{padding:0 70px 104px;font-size:28px;line-height:1.8;}
.longform-render.lf-layout-cinematic .montx-image{border-top:48px solid #000;border-bottom:48px solid #000;}
.longform-render.lf-layout-cinematic .montx-narrative{padding:88px 70px;min-height:360px;}
.longform-render.lf-layout-cinematic .montx-watermark{font-size:64px;right:70px;top:52px;color:var(--lf-accent);}
.longform-render.lf-layout-cinematic .montx-section-title{background:transparent;color:var(--lf-accent);font-size:32px;padding:0;margin-bottom:34px;text-transform:uppercase;}
.longform-render.lf-layout-cinematic .montx-body{border-left:0;padding:26px 0 0;font-size:29px;line-height:1.8;border-top:1px solid var(--lf-accent);}
.longform-render.lf-layout-cinematic .montx-grid{display:block;border:0;padding:82px 70px;background:#000;}
.longform-render.lf-layout-cinematic .montx-grid-left{background:transparent;color:var(--lf-accent);font-size:46px;padding:0 0 30px;}
.longform-render.lf-layout-cinematic .montx-grid-right{padding:0;font-size:28px;line-height:1.75;color:var(--lf-text);}

.longform-render.lf-layout-documentary .montx-topline{height:10px;}
.longform-render.lf-layout-documentary .montx-giant{padding:72px 70px 28px;font-size:82px;line-height:1.05;text-transform:none;font-weight:400;}
.longform-render.lf-layout-documentary .montx-rule{height:3px;margin:0 70px 34px;}
.longform-render.lf-layout-documentary .montx-lead{padding:0 70px 84px;font-size:30px;line-height:1.75;}
.longform-render.lf-layout-documentary .montx-narrative{padding:76px 70px;min-height:330px;}
.longform-render.lf-layout-documentary .montx-watermark{left:70px;right:auto;top:54px;font-size:60px;color:var(--lf-accent);}
.longform-render.lf-layout-documentary .montx-section-title{background:transparent;color:var(--lf-text);font-size:40px;padding:0 0 0 92px;margin-bottom:26px;}
.longform-render.lf-layout-documentary .montx-body{border-left:0;padding-left:92px;font-size:30px;line-height:1.78;}
.longform-render.lf-layout-documentary .montx-grid{display:block;border:0;padding:76px 70px;border-top:6px solid var(--lf-accent);}
.longform-render.lf-layout-documentary .montx-grid-left{background:transparent;color:var(--lf-accent);font-size:44px;padding:0 0 26px;}
.longform-render.lf-layout-documentary .montx-grid-right{padding:0;font-size:29px;line-height:1.75;}

.longform-render.lf-layout-archive .montx-topline{height:16px;}
.longform-render.lf-layout-archive .montx-giant{padding:70px 62px 22px;font-size:70px;line-height:1.08;text-transform:uppercase;border-bottom:1px solid var(--lf-accent);}
.longform-render.lf-layout-archive .montx-rule{height:0;margin:0 62px 34px;}
.longform-render.lf-layout-archive .montx-lead{padding:0 62px 74px;font-size:28px;line-height:1.75;}
.longform-render.lf-layout-archive .montx-narrative{padding:66px 62px;min-height:310px;border-top:1px dashed var(--lf-accent);}
.longform-render.lf-layout-archive .montx-watermark{left:62px;right:auto;top:42px;font-size:46px;color:var(--lf-accent);}
.longform-render.lf-layout-archive .montx-section-title{background:transparent;color:var(--lf-text);font-size:36px;padding:0 0 0 78px;margin-bottom:22px;text-decoration:underline;text-underline-offset:8px;}
.longform-render.lf-layout-archive .montx-body{border-left:1px solid var(--lf-accent);padding-left:26px;margin-left:78px;font-size:28px;line-height:1.75;}
.longform-render.lf-layout-archive .montx-grid{grid-template-columns:3fr 9fr;border:1px solid var(--lf-accent);}
.longform-render.lf-layout-archive .montx-grid-left{font-size:34px;padding:34px;background:transparent;color:var(--lf-accent);border-right:1px solid var(--lf-accent);}
.longform-render.lf-layout-archive .montx-grid-right{font-size:27px;padding:34px;}

.longform-render.lf-layout-organic .montx-topline{height:0;}
.longform-render.lf-layout-organic .montx-giant{padding:82px 82px 24px;font-size:78px;line-height:1.08;text-transform:none;font-weight:500;}
.longform-render.lf-layout-organic .montx-rule{height:10px;width:160px;margin:0 82px 36px;border-radius:20px;background:var(--lf-accent);}
.longform-render.lf-layout-organic .montx-lead{padding:0 82px 88px;font-size:30px;line-height:1.78;}
.longform-render.lf-layout-organic .montx-image{padding:0 44px 44px;box-sizing:border-box;}
.longform-render.lf-layout-organic .montx-narrative{padding:82px;min-height:320px;}
.longform-render.lf-layout-organic .montx-watermark{font-size:120px;color:color-mix(in oklab,var(--lf-accent) 16%,transparent);}
.longform-render.lf-layout-organic .montx-section-title{background:color-mix(in oklab,var(--lf-accent) 16%,var(--lf-bg));color:var(--lf-text);font-size:38px;padding:18px 24px;margin-bottom:30px;}
.longform-render.lf-layout-organic .montx-body{border-left:8px solid var(--lf-accent);font-size:30px;line-height:1.78;}
.longform-render.lf-layout-organic .montx-grid{display:block;border:0;padding:82px;background:color-mix(in oklab,var(--lf-accent) 10%,var(--lf-bg));}
.longform-render.lf-layout-organic .montx-grid-left{background:transparent;color:var(--lf-accent);font-size:44px;padding:0 0 28px;}
.longform-render.lf-layout-organic .montx-grid-right{padding:0;font-size:29px;line-height:1.78;}
`;

const hexToRgb = (hex = '#580018') => {
  const clean = hex.replace('#', '');
  const value = clean.length === 3 ? clean.split('').map(ch => ch + ch).join('') : clean;
  const num = Number.parseInt(value, 16);
  return {
    r: (num >> 16) & 255,
    g: (num >> 8) & 255,
    b: num & 255,
  };
};

const getLongformTheme = (markdown = MONTX_DEFAULT_SKILL) => {
  const skill = typeof markdown === 'string' ? parseSkillMarkdown(markdown) : markdown;
  const preset = LONGFORM_THEME_PRESETS.find(item => item.key.test(skill.title)) || LONGFORM_THEME_PRESETS[LONGFORM_THEME_PRESETS.length - 1];
  const bg = skill.hex[0] || preset.bg;
  const accent = skill.hex[1] || preset.accent;
  const text = skill.hex.find(color => ![bg, accent].includes(color)) || preset.text;
  const inverse = preset.inverse;
  const rgb = hexToRgb(accent);
  return {
    bg,
    accent,
    text,
    inverse,
    font: preset.font,
    watermark: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.1)`,
  };
};

const getLongformLayoutPreset = (markdown = MONTX_DEFAULT_SKILL) => {
  const skill = typeof markdown === 'string' ? parseSkillMarkdown(markdown) : markdown;
  return LONGFORM_LAYOUT_PRESETS.find(item => item.key.test(skill.title)) || LONGFORM_LAYOUT_PRESETS[LONGFORM_LAYOUT_PRESETS.length - 1];
};

const getLongformThemeVars = (markdown = MONTX_DEFAULT_SKILL) => {
  const theme = getLongformTheme(markdown);
  return {
    '--lf-bg': theme.bg,
    '--lf-accent': theme.accent,
    '--lf-text': theme.text,
    '--lf-inverse': theme.inverse,
    '--lf-watermark': theme.watermark,
    '--lf-font': theme.font,
  };
};

const getLongformHeroSection = (task) => (task.sections || []).find(section => section.type === 'Header') || (task.sections || [])[0] || {};

const getLongformTextUnits = (text = '') => Array.from(String(text)).reduce((sum, ch) => {
  if (/[，。？！：；、,.!?;:\s]/.test(ch)) return sum + 0.45;
  if (/[\x00-\xff]/.test(ch)) return sum + 0.55;
  return sum + 1;
}, 0);

const getLongformSingleLineHeroPx = (title = '') => {
  const units = Math.max(1, getLongformTextUnits(title));
  return Math.max(46, Math.min(118, Math.floor((900 / units) * 0.96)));
};

const getLongformHeroTitleStyle = (task = {}) => {
  const overrides = task.layoutOverrides || {};
  if (!overrides.heroTitleSingleLine) return {};
  return {
    fontSize: `${overrides.heroTitleFontPx || getLongformSingleLineHeroPx(getLongformHeroSection(task).title)}px`,
    whiteSpace: 'nowrap',
    letterSpacing: '0',
  };
};

const getLongformRenderOverrideClass = (task = {}) => task.layoutOverrides?.textAlign === 'center' ? 'lf-text-center' : '';

const getLongformRenderTypography = (task = {}) => {
  const layout = getLongformLayoutPreset(task.skillMarkdown || MONTX_DEFAULT_SKILL);
  const preset = LONGFORM_FONT_SIZE_PRESETS[layout.className] || LONGFORM_FONT_SIZE_PRESETS['lf-layout-bauhaus'];
  return {
    ...preset,
    heroTitlePx: task.layoutOverrides?.heroTitleSingleLine
      ? (task.layoutOverrides.heroTitleFontPx || getLongformSingleLineHeroPx(getLongformHeroSection(task).title))
      : preset.heroTitlePx,
    layoutId: layout.id,
    layoutLabel: layout.label,
  };
};

const extractLongformTypographyRules = (task = {}, brand = LONGFORM_BRANDS[0]) => {
  const markdown = task.skillMarkdown || MONTX_DEFAULT_SKILL;
  const rules = {};
  const sources = [];
  const heroMin = markdown.match(/(?:大标题|主标题|标题)[^\n\d]{0,24}(\d+)\s*px\s*\+/i)?.[1];
  const bodyRange = markdown.match(/正文[^\n\d]{0,24}(\d+)\s*px\s*(?:-|到|至|~)\s*(\d+)\s*px?/i);
  const sectionRange = markdown.match(/(?:小标题|章节标题)[^\n\d]{0,24}(\d+)\s*px\s*(?:-|到|至|~)\s*(\d+)\s*px?/i);
  if (heroMin) {
    rules.heroTitleMinPx = Number(heroMin);
    sources.push(`style skill: 大标题 ${heroMin}px+`);
  }
  if (bodyRange) {
    rules.bodyFontMinPx = Number(bodyRange[1]);
    rules.bodyFontMaxPx = Number(bodyRange[2]);
    sources.push(`style skill: 正文 ${bodyRange[1]}-${bodyRange[2]}px`);
  }
  if (sectionRange) {
    rules.sectionTitleMinPx = Number(sectionRange[1]);
    rules.sectionTitleMaxPx = Number(sectionRange[2]);
    sources.push(`style skill: 小标题 ${sectionRange[1]}-${sectionRange[2]}px`);
  }
  getActiveLongformConstraints(brand).forEach(rule => {
    if (!rule.rules) return;
    if (rule.rules.heroTitleMinPx) {
      rules.heroTitleMinPx = rule.rules.heroTitleMinPx;
      sources.push(`品牌护栏「${rule.label}」: 主标题 ${rule.rules.heroTitleMinPx}px+`);
    }
    if (rule.rules.bodyFontMinPx && rule.rules.bodyFontMaxPx) {
      rules.bodyFontMinPx = rule.rules.bodyFontMinPx;
      rules.bodyFontMaxPx = rule.rules.bodyFontMaxPx;
      sources.push(`品牌护栏「${rule.label}」: 正文 ${rule.rules.bodyFontMinPx}-${rule.rules.bodyFontMaxPx}px`);
    }
  });
  const normalBodyRule = getActiveLongformConstraints(brand).find(rule => /正文.*加粗|font-weight|单字加粗|normal/i.test(`${rule.label} ${rule.description}`));
  if (normalBodyRule) sources.push(`品牌护栏「${normalBodyRule.label}」: 正文 normal，不做局部加粗`);
  return { rules, sources };
};

const buildLongformTypographyAudit = (task = {}, brand = LONGFORM_BRANDS[0]) => {
  const typography = getLongformRenderTypography(task);
  const { rules, sources } = extractLongformTypographyRules(task, brand);
  const issues = [];
  if (rules.heroTitleMinPx && typography.heroTitlePx < rules.heroTitleMinPx) {
    issues.push(`主标题 ${typography.heroTitlePx}px 低于 ${rules.heroTitleMinPx}px+`);
  }
  const bodyTargets = [
    ['正文', typography.bodyPx],
    ['网格正文', typography.gridRightPx],
    ['引言', typography.leadPx],
  ];
  bodyTargets.forEach(([label, px]) => {
    if (rules.bodyFontMinPx && px < rules.bodyFontMinPx) issues.push(`${label} ${px}px 低于正文下限 ${rules.bodyFontMinPx}px`);
    if (rules.bodyFontMaxPx && px > rules.bodyFontMaxPx) issues.push(`${label} ${px}px 高于正文上限 ${rules.bodyFontMaxPx}px`);
  });
  if (rules.sectionTitleMinPx && typography.sectionTitlePx < rules.sectionTitleMinPx) {
    issues.push(`小标题 ${typography.sectionTitlePx}px 低于 ${rules.sectionTitleMinPx}px`);
  }
  if (rules.sectionTitleMaxPx && typography.sectionTitlePx > rules.sectionTitleMaxPx) {
    issues.push(`小标题 ${typography.sectionTitlePx}px 高于 ${rules.sectionTitleMaxPx}px`);
  }
  return { typography, rules, sources, issues };
};

const LONGFORM_PR_REVIEW_RULES = [
  { level: 'S3', token: '玻璃心', label: '不要默认用户玻璃心' },
  { level: 'S2', token: '你误会了', label: '不要和用户争夺解释权' },
  { level: 'S2', token: '本意不是', label: '避免先解释品牌本意' },
  { level: 'S2', token: '过度解读', label: '避免指责用户解读' },
  { level: 'S2', token: '牛马', label: '品牌不能替用户自嘲' },
  { level: 'S2', token: '买不起', label: '避免轻视用户购买能力' },
  { level: 'S2', token: '秒杀', label: '高风险促销/比较表达' },
  { level: 'S2', token: '解放双手', label: '智能驾驶高风险承诺' },
  { level: 'S2', token: '完全自动驾驶', label: '自动驾驶边界风险' },
  { level: 'S1', token: '很快', label: '模糊能力表达，建议量化' },
];

const buildLongformPrAudit = (task = {}) => {
  const text = [
    task.title || '',
    task.prompt || '',
    ...(task.sections || []).flatMap(section => [section.title || '', section.body || '']),
  ].join('\n');
  const hits = LONGFORM_PR_REVIEW_RULES.filter(rule => text.includes(rule.token));
  const rank = { S0: 0, S1: 1, S2: 2, S3: 3, S4: 4 };
  const level = hits.reduce((max, hit) => rank[hit.level] > rank[max] ? hit.level : max, 'S0');
  const action = {
    S0: '无明显舆情风险，仅需监测',
    S1: '轻微争议，建议优化表达',
    S2: '中度舆情风险，建议修改内容并准备回应',
    S3: '严重舆情风险，建议统一口径并正式说明',
    S4: '重大危机，需启动危机公关机制',
  }[level];
  return { level, hits, action };
};

const styleObjectToHtml = (style = {}) => {
  const css = Object.entries(style).map(([key, value]) => `${key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)}:${value}`).join(';');
  return css ? ` style="${escapeHtml(css)}"` : '';
};

const buildLongformThemeCss = (markdown = MONTX_DEFAULT_SKILL, selector = '.montx') => {
  const theme = getLongformTheme(markdown);
  return `${selector}{--lf-bg:${theme.bg};--lf-accent:${theme.accent};--lf-text:${theme.text};--lf-inverse:${theme.inverse};--lf-watermark:${theme.watermark};--lf-font:${theme.font};}`;
};

const buildLongformHtml = (task, brand) => {
  const theme = getLongformTheme(task.skillMarkdown || MONTX_DEFAULT_SKILL);
  const layout = getLongformLayoutPreset(task.skillMarkdown || MONTX_DEFAULT_SKILL);
  const overrideClass = getLongformRenderOverrideClass(task);
  const cssSource = `${MONTX_EXPORT_CSS}\n${LONGFORM_LAYOUT_CSS}`;
  const css = `body{margin:0;background:${theme.bg};}${buildLongformThemeCss(task.skillMarkdown || MONTX_DEFAULT_SKILL)}${cssSource
    .replaceAll('.montx-canvas', '.montx')
    .replaceAll('.montx-topline', '.topline')
    .replaceAll('.montx-giant', '.giant')
    .replaceAll('.montx-rule', '.rule')
    .replaceAll('.montx-lead', '.lead')
    .replaceAll('.montx-image', '.image')
    .replaceAll('.montx-narrative', '.narrative')
    .replaceAll('.montx-watermark', '.watermark')
    .replaceAll('.montx-section-title', '.section-title')
    .replaceAll('.montx-body', '.body')
    .replaceAll('.montx-grid-left', '.grid-left')
    .replaceAll('.montx-grid-right', '.grid-right')
    .replaceAll('.montx-grid', '.grid')
    .replaceAll('.montx-footer', '.footer')}`;
  const sections = task.sections.map((s, i) => {
    const imgs = task.images.filter(img => (s.imageIds || []).includes(img.id));
    if (s.type === 'Header') {
      return `<div class="topline"></div><div class="giant"${styleObjectToHtml(getLongformHeroTitleStyle(task))}>${escapeHtml(s.title)}</div><div class="rule"></div><div class="lead">${escapeHtml(s.body)}</div>${buildLongformImageHtml(imgs)}`;
    }
    if (s.type === 'Image') {
      return imgs.length
        ? buildLongformImageHtml(imgs)
        : `<div class="narrative"><div class="watermark">${String(i).padStart(2, '0')}</div><div class="section-title">${escapeHtml(s.title)}</div><div class="body">${escapeHtml(s.body)}<br><br>[ 图片待上传并分配 ]</div></div>`;
    }
    if (s.type === 'Grid') {
      return `<section class="grid"><div class="grid-left">${escapeHtml(s.title)}</div><div class="grid-right">${escapeHtml(s.body)}</div></section>${buildLongformImageHtml(imgs)}`;
    }
    if (s.type === 'Footer') {
      return `${buildLongformImageHtml(imgs)}<footer class="footer"><h2>${escapeHtml(s.title)}</h2><p>${escapeHtml(s.body)}</p></footer>`;
    }
    return `<section class="narrative"><div class="watermark">${String(i).padStart(2, '0')}</div><div class="section-title">${escapeHtml(s.title)}</div><div class="body">${escapeHtml(s.body)}</div></section>${buildLongformImageHtml(imgs)}`;
  }).join('\n');
  return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>${escapeHtml(task.title)}</title><style>${css}</style></head><body><main class="montx longform-render ${layout.className} ${overrideClass}" data-brand="${escapeHtml(brand.name)}" data-layout="${layout.id}">${sections}</main></body></html>`;
};

const getLongformGuardrailInput = (task = {}) => [
  task.title,
  task.prompt,
  task.briefing?.coreQuestion,
  task.briefing?.tone,
  ...(task.sections || []).flatMap(section => [section.title, section.body]),
].filter(Boolean).join('\n');

const calculateLongformQa = (task, brand) => {
  const validSectionIds = new Set((task.sections || []).map(section => section.id));
  const unassigned = (task.images || []).filter(img => !img.assignedSectionId || !validSectionIds.has(img.assignedSectionId)).length;
  const overrides = (task.overrides || []).length;
  const widthIssue = getActiveLongformConstraints(brand).find(c => c.id === 'canvas-1080') ? 0 : 1;
  const typographyAudit = buildLongformTypographyAudit(task, brand);
  const prAudit = buildLongformPrAudit(task);
  const guardrailAudit = window.BrandMemoryStore?.runBrandGuardrails
    ? window.BrandMemoryStore.runBrandGuardrails(getLongformGuardrailInput(task), {
        brand,
        brandId: brand?.id,
        productId: task.productId,
        campaignId: task.campaignId,
        campaignName: task.campaignName,
        scope: 'longform',
      })
    : { violations: [], requiredFixes: [] };
  const normalizedSections = (task.sections || []).map(normalizeLongformSection);
  const blockIds = new Set(normalizedSections.map(section => section.blockType));
  const issues = [];
  if (unassigned) issues.push(`${unassigned} 张图片未分配 section`);
  if (overrides) issues.push(`${overrides} 项品牌护栏已申请解除`);
  if (widthIssue) issues.push('缺少 1080px 画布规则');
  if (!task.skillMarkdown) issues.push('尚未上传或选择 markdown skill');
  if (!blockIds.has('hero')) issues.push('缺少开场 Hero 模块');
  if (!blockIds.has('cta')) issues.push('缺少收束 CTA 模块');
  if (['product', 'brand_product'].includes(task.narrativeMode || 'brand_product') && !getLongformProduct(brand, task.productId)) {
    issues.push('叙事模式需要绑定产品 Profile');
  }
  typographyAudit.issues.forEach(issue => issues.push(`字体约束：${issue}`));
  if (!['S0', 'S1'].includes(prAudit.level)) issues.push(`PR 舆情审核：${prAudit.level} ${prAudit.action}`);
  guardrailAudit.violations.forEach(item => issues.push(`品牌护栏：${item.message}`));
  return {
    score: Math.max(52, 96 - unassigned * 6 - overrides * 10 - (task.skillMarkdown ? 0 : 18) - typographyAudit.issues.length * 6 - guardrailAudit.requiredFixes.length * 10 - (prAudit.level === 'S1' ? 4 : prAudit.level === 'S2' ? 10 : prAudit.level === 'S3' ? 18 : prAudit.level === 'S4' ? 28 : 0) - (!blockIds.has('hero') ? 8 : 0) - (!blockIds.has('cta') ? 6 : 0) - ((['product', 'brand_product'].includes(task.narrativeMode || 'brand_product') && !getLongformProduct(brand, task.productId)) ? 8 : 0)),
    issues,
    exportReady: Boolean(task.sections.length && task.skillMarkdown),
    guardrailAudit,
  };
};

const buildLongformInspect = (task, brand, skill) => {
  const sections = (task.sections || []).map(normalizeLongformSection);
  const validSectionIds = new Set(sections.map(section => section.id));
  const assignedImages = (task.images || []).filter(img => img.assignedSectionId && validSectionIds.has(img.assignedSectionId));
  const unassignedImages = (task.images || []).filter(img => !img.assignedSectionId || !validSectionIds.has(img.assignedSectionId));
  const blockIds = new Set(sections.map(section => section.blockType));
  const typographyAudit = buildLongformTypographyAudit(task, brand);
  const prAudit = buildLongformPrAudit(task);
  const guardrailAudit = window.BrandMemoryStore?.runBrandGuardrails
    ? window.BrandMemoryStore.runBrandGuardrails(getLongformGuardrailInput(task), {
        brand,
        brandId: brand?.id,
        productId: task.productId,
        campaignId: task.campaignId,
        campaignName: task.campaignName,
        scope: 'longform',
      })
    : { passed: true, violations: [], checkedRules: getActiveLongformConstraints(brand).length };
  const makeItem = (label, value, state = 'ok', detail = '') => ({ label, value, state, detail });
  const groups = [
    {
      id: 'brief',
      title: 'Brief Readiness',
      items: [
        makeItem('标题', task.title || '未命名', task.title ? 'ok' : 'warn'),
        makeItem('结构化 Brief', task.briefing ? '已解析' : '未导入', task.briefing ? 'ok' : 'warn', task.briefing?.coreQuestion || '可先导入/粘贴 Brief 以提高稳定性'),
        makeItem('叙事模式', getLongformNarrativeMode(task.narrativeMode).label, 'ok'),
        makeItem('产品 Profile', getLongformProduct(brand, task.productId)?.name || '未绑定', getLongformProduct(brand, task.productId) ? 'ok' : 'warn'),
      ],
    },
    {
      id: 'dsl',
      title: 'Block DSL',
      items: [
        makeItem('Section 数量', `${sections.length}`, sections.length >= 5 && sections.length <= 8 ? 'ok' : 'warn', '建议 5-8 个 section'),
        makeItem('开场 Hero', blockIds.has('hero') ? '存在' : '缺失', blockIds.has('hero') ? 'ok' : 'warn'),
        makeItem('收束 CTA', blockIds.has('cta') ? '存在' : '缺失', blockIds.has('cta') ? 'ok' : 'warn'),
        makeItem('模块类型', `${blockIds.size} 类`, blockIds.size >= 3 ? 'ok' : 'warn', '建议包含叙事、证据/图片、信息或 CTA 等类型'),
      ],
    },
    {
      id: 'image',
      title: 'Image Mapping',
      items: [
        makeItem('图片总数', `${(task.images || []).length}`, (task.images || []).length ? 'ok' : 'warn'),
        makeItem('已分配', `${assignedImages.length}/${(task.images || []).length}`, unassignedImages.length ? 'warn' : 'ok'),
        makeItem('图片全通栏', getActiveLongformConstraints(brand).some(rule => rule.id === 'full-bleed-image') ? '护栏启用' : '未启用', getActiveLongformConstraints(brand).some(rule => rule.id === 'full-bleed-image') ? 'ok' : 'warn'),
        makeItem('图片压字', '未检测到', 'ok'),
      ],
    },
    {
      id: 'wechat',
      title: 'WeChat Compatibility',
      items: [
        makeItem('画布宽度', `${skill.width || 1080}px`, String(skill.width || '1080') === '1080' ? 'ok' : 'warn'),
        makeItem('单文件 HTML', getActiveLongformConstraints(brand).some(rule => rule.id === 'single-file-html') ? '护栏启用' : '未启用', getActiveLongformConstraints(brand).some(rule => rule.id === 'single-file-html') ? 'ok' : 'warn'),
        makeItem('公众号 HTML 复制', '未接入', 'idle', '当前仅导出整张 PNG，富文本复制留到下一步'),
        makeItem('PNG 长图', task.html ? '可导出' : '待生成', task.html ? 'ok' : 'warn'),
      ],
    },
    {
      id: 'policy',
      title: 'Brand QA',
      items: [
        makeItem('品牌护栏', `${guardrailAudit.checkedRules} active`, guardrailAudit.violations.length ? 'warn' : 'ok', guardrailAudit.violations.map(item => item.message).join('；')),
        makeItem('Override', `${(task.overrides || []).length}`, (task.overrides || []).length ? 'warn' : 'ok'),
        makeItem('字号约束', typographyAudit.issues.length ? `${typographyAudit.issues.length} risks` : '通过', typographyAudit.issues.length ? 'warn' : 'ok', typographyAudit.issues.join('；')),
        makeItem('QA Score', `${task.qa?.score || 0}`, (task.qa?.score || 0) >= 80 ? 'ok' : 'warn'),
      ],
    },
    {
      id: 'pr',
      title: 'PR Reputation QA',
      items: [
        makeItem('PR Skill', 'Chief Public Relations Officer', 'ok'),
        makeItem('舆情等级', prAudit.level, ['S0', 'S1'].includes(prAudit.level) ? 'ok' : 'warn', prAudit.action),
        makeItem('风险表达', prAudit.hits.length ? `${prAudit.hits.length} hits` : '未命中', prAudit.hits.length ? 'warn' : 'ok', prAudit.hits.map(hit => `${hit.token}: ${hit.label}`).join('；')),
        makeItem('回应原则', '先情绪后事实', 'ok', '不说“你误会了 / 本意不是 / 过度解读”；品牌不与用户争夺解释权'),
      ],
    },
  ];
  return {
    groups,
    issues: groups.flatMap(group => group.items.filter(item => item.state === 'warn').map(item => `${group.title}：${item.label} ${item.value}`)),
  };
};

const normalizeModelSections = (sections = []) => {
  const allowedTypes = new Set(['Header', 'Narrative', 'Image', 'Grid', 'Footer']);
  const fallbackModule = { Header: 'Header', Narrative: 'Narrative Block', Image: 'Image Block', Grid: 'Grid Content', Footer: 'Footer' };
  return sections.slice(0, 8).map((section, i) => {
    const type = allowedTypes.has(section.type) ? section.type : (i === 0 ? 'Header' : i === sections.length - 1 ? 'Footer' : 'Narrative');
    return normalizeLongformSection({
      id: `${type.toLowerCase()}-${Date.now()}-${i}`,
      type,
      blockType: section.blockType,
      title: section.title || LONGFORM_DEFAULT_SECTIONS[i % LONGFORM_DEFAULT_SECTIONS.length].title,
      body: section.body || '',
      imageIds: [],
      layoutModule: section.layoutModule || fallbackModule[type],
      imageHint: section.imageHint || '',
    }, i);
  });
};

const initialLongformMessages = (task) => task.messages?.length ? task.messages : [
  { role: 'assistant', text: '把这篇公众号长图文的目标、读者、语气和必须覆盖的观点发给我。我会先推演结构，再把它拆成可排版的 section。' },
  { role: 'user', text: task.prompt },
];

const buildLocalLongformDraft = (promptOverride = '', fallbackTitle = '') => {
  const title = promptOverride.split(/[，。,.\n]/)[0].replace(/^为微信公众号设计一篇/, '').slice(0, 24) || fallbackTitle || 'LONGFORM V10';
  const sections = LONGFORM_DEFAULT_SECTIONS.map((s, i) => ({
    ...s,
    id: `${s.type.toLowerCase()}-${i}-${Date.now()}`,
    title: i === 0 ? title : s.title,
    body: i === 1 ? `基于输入需求：「${promptOverride}」。Longform Agent 将先确定读者、论点和证据，再交给 MONTX 网格系统完成长图表达。` : s.body,
  })).map(normalizeLongformSection);
  return { title, sections };
};

const syncLongformSectionsWithImages = (sections = [], images = []) => sections.map(section => ({
  ...section,
  imageIds: images.filter(img => img.assignedSectionId === section.id).map(img => img.id),
}));

const getLongformImageHint = (section, index = 0) => {
  if (section?.imageHint) return section.imageHint;
  const hints = {
    Header: '建议使用一张建立主题气质的全通栏主视觉，不在图片上压字。',
    Narrative: '建议补充能证明段落观点的真实场景、人物或环境图片。',
    Image: '建议放置最强证据图，可连续添加多张形成视觉证据组。',
    Grid: '建议选择细节、对比、步骤或局部特写，辅助网格化信息拆解。',
    Footer: '通常不配图；如需收束，可使用品牌环境图或产品全景。',
  };
  return hints[section?.type] || `建议选择与第 ${index + 1} 段标题和正文证据直接相关的图片。`;
};

const buildLongformImageHtml = (images = []) => images
  .map(img => `<img class="image" src="${img.dataUrl}" alt="${escapeHtml(img.name)}" referrerpolicy="no-referrer">`)
  .join('');

const findLongformConstraint = (brand, query = '') => {
  const clean = query.trim().toLowerCase();
  if (!clean) return null;
  return (brand.constraints || []).find(rule => {
    const haystack = `${rule.id} ${rule.label} ${rule.description}`.toLowerCase();
    return haystack.includes(clean) || clean.includes(rule.label.toLowerCase());
  }) || null;
};

const getActiveLongformConstraints = (brand, scope = 'longform') => window.BrandMemoryStore?.getActiveConstraints
  ? window.BrandMemoryStore.getActiveConstraints(brand, scope)
  : (brand?.constraints || []).filter(rule => rule.enabled !== false);

const extractLongformTerms = (text = '') => {
  const quoted = Array.from(text.matchAll(/[“"「『《]([^”"」』》]+)[”"」』》]/g)).map(match => match[1].trim());
  const known = ['完全自动驾驶', '解放双手', '无人驾驶', '绝对安全', '碾压竞品', '秒杀', '促销', '渐变', '毛玻璃', '阴影', '图片压字', '圆角拼图'];
  return Array.from(new Set([...quoted, ...known.filter(term => text.includes(term))]));
};

const inferLongformGuardrailRules = (text = '') => {
  const rules = {};
  const heroMin = text.match(/(?:主标题|大标题|标题)[^\d]*(\d+)\s*px?\+?/i)?.[1];
  const bodyRange = text.match(/正文[^\d]*(\d+)\s*(?:-|到|至|~)\s*(\d+)\s*px?/i);
  const canvasWidth = text.match(/(?:画布|宽度)[^\d]*(\d+)\s*px?/i)?.[1];
  if (heroMin) rules.heroTitleMinPx = Number(heroMin);
  if (bodyRange) {
    rules.bodyFontMinPx = Number(bodyRange[1]);
    rules.bodyFontMaxPx = Number(bodyRange[2]);
  }
  if (canvasWidth) rules.canvasWidthPx = Number(canvasWidth);
  return rules;
};

const buildLongformGuardrailDraft = (input = '', brand) => {
  const text = input.trim();
  if (!text) return null;
  const matched = findLongformConstraint(brand, text);
  const isDisable = /(停用|禁用|关闭|取消|不再启用|disable)/i.test(text);
  const isEdit = /(修改|改成|改为|更新|调整|编辑|补充)/i.test(text) || (matched && !isDisable);
  const terms = extractLongformTerms(text);
  const rules = inferLongformGuardrailRules(text);
  let label = '自定义品牌护栏';
  if (/字号|主标题|大标题|正文|画布|px/i.test(text)) label = '字号与画布约束';
  else if (/自动驾驶|解放双手|无人驾驶|承诺|安全/.test(text)) label = '禁止自动驾驶过度承诺';
  else if (/图片|压字|全通栏|圆角|拼图/.test(text)) label = '图片使用约束';
  else if (/渐变|毛玻璃|阴影|视觉|风格/.test(text)) label = '视觉风格约束';
  const operation = isDisable ? 'disable' : isEdit && matched ? 'edit' : 'add';
  const target = matched ? matched.id : '';
  return {
    id: longformId('draft'),
    operation,
    target,
    targetLabel: matched?.label || '',
    constraint: normalizeLongformConstraint({
      id: operation === 'add' ? longformId('guard') : matched?.id,
      label: operation === 'edit' && matched ? matched.label : label,
      description: text,
      severity: 'required',
      category: inferLongformConstraintCategory({ label, description: text }),
      source: 'user-natural-language',
      forbiddenTerms: /(禁止|避免|不要|不得|不能|风险|承诺)/.test(text) ? terms : [],
      requiredTerms: /(必须|需要|保持|统一|强制)/.test(text) ? terms : [],
      rules,
      createdAt: new Date().toLocaleString('zh-CN', { hour12: false }),
      updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
    }),
  };
};

const runLongformLayoutCommand = (text, task) => {
  const normalized = text.trim();
  const updatedAt = new Date().toLocaleString('zh-CN', { hour12: false });
  const wantsHeroSingleLine = /(主标题|大标题|标题)/.test(normalized) && /(一行|单行|缩小|放一行|不换行)/.test(normalized);
  const wantsTextCenter = /(所有|全部|全局|整篇|全文|文字|文本|正文|标题).*(居中|居中显示)|居中显示|文字居中/.test(normalized);
  const wantsTextDefault = /(取消|恢复|默认).*(居中|对齐)|左对齐|恢复默认对齐|默认对齐/.test(normalized);

  if (wantsTextCenter || wantsTextDefault) {
    const layoutOverrides = {
      ...(task.layoutOverrides || {}),
      textAlignRequest: normalized,
      updatedAt,
    };
    if (wantsTextDefault) {
      delete layoutOverrides.textAlign;
    } else {
      layoutOverrides.textAlign = 'center';
    }
    return {
      taskPatch: { layoutOverrides, status: '排版已调整' },
      reply: wantsTextDefault
        ? '已恢复默认文字对齐方式。正文和 section 内容保持不变。'
        : '已将长图预览中的标题、正文、段落标题和页脚文字设置为居中显示。正文和 section 内容保持不变。',
    };
  }

  if (wantsHeroSingleLine) {
    const title = getLongformHeroSection(task).title || task.title || '';
    const fontPx = getLongformSingleLineHeroPx(title);
    const layoutOverrides = {
      ...(task.layoutOverrides || {}),
      heroTitleSingleLine: true,
      heroTitleFontPx: fontPx,
      heroTitleRequest: normalized,
      updatedAt,
    };
    return {
      taskPatch: { layoutOverrides, status: '排版已调整' },
      reply: `已把主标题设置为单行适配，当前估算字号为 ${fontPx}px。正文和 section 内容保持不变。注意：如果低于 MONTX 大标题 110px+ 的品牌约束，QA 会标记风险。`,
    };
  }

  return null;
};

const runLongformQaCommand = (text, task, brand) => {
  const normalized = text.trim();
  const isTypographyQuestion = /(字体|字号|字重|font|主标题|大标题|正文|小标题).*(约束|限制|规范|合规|符合|检查|多少|多大|被约束|有没有)|(?:约束|限制|规范|合规|符合|检查).*(字体|字号|字重|主标题|大标题|正文|小标题)/i.test(normalized);
  if (!isTypographyQuestion) return null;

  const audit = buildLongformTypographyAudit(task, brand);
  const family = getLongformTheme(task.skillMarkdown || MONTX_DEFAULT_SKILL).font;
  const sourceText = audit.sources.length ? audit.sources.join('；') : '当前未解析到明确字号约束';
  const valueText = [
    `主标题 ${audit.typography.heroTitlePx}px`,
    `引言 ${audit.typography.leadPx}px`,
    `正文 ${audit.typography.bodyPx}px`,
    `网格正文 ${audit.typography.gridRightPx}px`,
    `小标题 ${audit.typography.sectionTitlePx}px`,
  ].join('；');
  const conclusion = audit.issues.length
    ? `结论：存在 ${audit.issues.length} 项风险：${audit.issues.join('；')}。`
    : '结论：当前可解析的字体/字号约束均通过。';
  return {
    taskPatch: {},
    reply: `可以检查。当前字体族为 ${family}。约束来源：${sourceText}。当前渲染：${audit.typography.layoutLabel}，${valueText}。${conclusion}`,
  };
};

const classifyLongformIntentLocal = (input = '') => {
  const text = String(input).trim();
  const hasStyleKeyword = /(字体|字号|字重|font|样式|排版|布局|居中|对齐|主标题|大标题|正文|小标题|颜色|色彩|模板|风格|style)/i.test(text);
  const hasQuestionSignal = /(是否|有没有|有无|吗|么|多少|多大|为什么|检查|合规|符合|约束|限制|规范|被约束)/.test(text);
  const hasAdjustmentSignal = /(改|修改|调整|设为|设置|变成|换成|缩小|放大|居中|左对齐|右对齐|一行|单行|不换行|恢复|取消)/.test(text);
  if (/^(查看护栏|解除护栏[:：]|恢复护栏[:：])/.test(text)) {
    return { type: 'brand_guardrail', target: 'brand_guardrail', shouldRegenerateContent: false, shouldPreserveCopy: true, confidence: 0.98, rationale: '本地规则识别为品牌护栏命令', source: 'local' };
  }
  if (hasStyleKeyword && hasQuestionSignal) {
    return { type: 'style_query', target: 'typography_or_style', shouldRegenerateContent: false, shouldPreserveCopy: true, confidence: 0.9, rationale: '本地规则识别为字体或样式问询', source: 'local' };
  }
  if (hasStyleKeyword && hasAdjustmentSignal) {
    return { type: 'style_adjustment', target: 'typography_or_layout', shouldRegenerateContent: false, shouldPreserveCopy: true, confidence: 0.9, rationale: '本地规则识别为字体或样式调整', source: 'local' };
  }
  if (/(配图|图片|图池|image|主图|插图)/i.test(text) && /(分配|添加|选择|一键|换|删除|移动)/.test(text)) {
    return { type: 'image_assignment', target: 'images', shouldRegenerateContent: false, shouldPreserveCopy: true, confidence: 0.82, rationale: '本地规则识别为图片分配操作', source: 'local' };
  }
  return { type: 'content_generation', target: 'longform_content', shouldRegenerateContent: true, shouldPreserveCopy: false, confidence: 0.68, rationale: '本地规则未识别为局部操作，默认进入内容推演', source: 'local' };
};

const normalizeLongformIntent = (intent = {}, fallbackText = '') => {
  const allowed = new Set(['content_generation', 'style_query', 'style_adjustment', 'brand_guardrail', 'image_assignment', 'export_or_draft', 'unknown']);
  const local = classifyLongformIntentLocal(fallbackText);
  const type = allowed.has(intent.type) ? intent.type : local.type;
  const shouldPreserveCopy = Boolean(intent.shouldPreserveCopy || ['style_query', 'style_adjustment', 'brand_guardrail', 'image_assignment', 'export_or_draft'].includes(type));
  return {
    type,
    target: intent.target || local.target || 'unknown',
    shouldRegenerateContent: shouldPreserveCopy ? false : Boolean(intent.shouldRegenerateContent || type === 'content_generation'),
    shouldPreserveCopy,
    confidence: Number(intent.confidence || local.confidence || 0),
    rationale: intent.rationale || local.rationale || '',
    source: intent.source || 'unknown',
    modelError: intent.modelError || '',
  };
};

const protectLongformIntent = (intent, text) => {
  const normalized = normalizeLongformIntent(intent, text);
  const local = classifyLongformIntentLocal(text);
  if (local.shouldPreserveCopy && normalized.shouldRegenerateContent) {
    return {
      ...local,
      source: `${normalized.source || 'model'} + local-safety`,
      rationale: `本地安全闸覆盖：${local.rationale}`,
      modelIntent: normalized,
    };
  }
  return normalized;
};

const applyLongformGuardrailDraft = (brand, draft) => {
  if (!draft) return brand;
  const updatedAt = new Date().toLocaleString('zh-CN', { hour12: false });
  if (draft.operation === 'disable') {
    return {
      ...brand,
      constraints: brand.constraints.map(rule => rule.id === draft.target ? { ...rule, enabled: false, updatedAt } : rule),
    };
  }
  if (draft.operation === 'edit') {
    return {
      ...brand,
      constraints: brand.constraints.map(rule => rule.id === draft.target ? {
        ...rule,
        description: draft.constraint.description,
        category: draft.constraint.category,
        forbiddenTerms: Array.from(new Set([...(rule.forbiddenTerms || []), ...(draft.constraint.forbiddenTerms || [])])),
        requiredTerms: Array.from(new Set([...(rule.requiredTerms || []), ...(draft.constraint.requiredTerms || [])])),
        rules: { ...(rule.rules || {}), ...(draft.constraint.rules || {}) },
        enabled: true,
        updatedAt,
      } : rule),
    };
  }
  return {
    ...brand,
    constraints: [...brand.constraints, { ...draft.constraint, updatedAt }],
  };
};

const LONGFORM_MODEL_STAGE = {
  idle: '待输入需求',
  classifying: 'LLM 判断需求类型',
  importing: '导入测试素材',
  imported: '测试素材已导入',
  thinking: '解析需求 / DeepSeek 调用中',
  success: '结构生成完成 / HTML 已生成',
  fallback: '本地兜底 / 失败原因',
};

const LongformDB = {
  open() {
    return new Promise((resolve, reject) => {
      const req = indexedDB.open('aurelia-longform-agent', 1);
      req.onupgradeneeded = () => req.result.createObjectStore('kv');
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    });
  },
  async get(key) {
    try {
      const db = await LongformDB.open();
      return await new Promise((resolve, reject) => {
        const req = db.transaction('kv', 'readonly').objectStore('kv').get(key);
        req.onsuccess = () => resolve(req.result);
        req.onerror = () => reject(req.error);
      });
    } catch (e) {
      console.warn('Longform IndexedDB read failed', e);
      return null;
    }
  },
  async set(key, value) {
    try {
      const db = await LongformDB.open();
      await new Promise((resolve, reject) => {
        const req = db.transaction('kv', 'readwrite').objectStore('kv').put(value, key);
        req.onsuccess = () => resolve();
        req.onerror = () => reject(req.error);
      });
    } catch (e) {
      console.warn('Longform IndexedDB write failed', e);
    }
  },
};

const useLongformTasks = () => {
  const [tasks, setTasks] = React.useState([]);
  const [ready, setReady] = React.useState(false);
  React.useEffect(() => {
    let mounted = true;
    LongformDB.get('tasks').then(saved => {
      if (!mounted) return;
      const initialSource = saved?.length ? saved : LONGFORM_MOCK_TASKS;
      const initial = initialSource.map(task => {
        const normalized = normalizeLongformTask(task);
        return { ...normalized, html: normalized.html || buildLongformHtml(normalized, getLongformBrand(normalized.brandId)) };
      });
      setTasks(initial);
      setReady(true);
    });
    return () => { mounted = false; };
  }, []);
  React.useEffect(() => {
    if (ready) LongformDB.set('tasks', tasks);
  }, [ready, tasks]);
  return { tasks, setTasks, ready };
};

const useLongformBrandProfiles = () => {
  if (window.BrandMemoryStore?.useBrandMemoryProfiles) return window.BrandMemoryStore.useBrandMemoryProfiles();
  const [brands, setBrands] = React.useState(() => normalizeSharedBrandProfiles());
  const [ready, setReady] = React.useState(false);
  React.useEffect(() => {
    let mounted = true;
    LongformDB.get('brandProfiles').then(saved => {
      if (!mounted) return;
      setBrands(normalizeSharedBrandProfiles(saved?.length ? saved : LONGFORM_BRANDS));
      setReady(true);
    });
    return () => { mounted = false; };
  }, []);
  React.useEffect(() => {
    if (ready) LongformDB.set('brandProfiles', brands);
  }, [ready, brands]);
  return { brands, setBrands, ready };
};

const useLongformStyleTemplates = () => {
  const [remoteTemplates, setRemoteTemplates] = React.useState([]);
  React.useEffect(() => {
    let mounted = true;
    fetch('/api/longform/style-templates')
      .then(response => response.ok ? response.json() : Promise.reject(new Error(`模板服务返回 ${response.status}`)))
      .then(data => {
        if (mounted) setRemoteTemplates(Array.isArray(data.styleTemplates) ? data.styleTemplates : []);
      })
      .catch(error => {
        console.warn('Longform style templates unavailable', error);
        if (mounted) setRemoteTemplates([]);
      });
    return () => { mounted = false; };
  }, []);
  return React.useMemo(() => [LONGFORM_DEFAULT_STYLE_TEMPLATE, ...remoteTemplates], [remoteTemplates]);
};

const getActiveStyleTemplateId = (task, styleTemplates) => {
  if (task.activeStyleTemplateId) return task.activeStyleTemplateId;
  const matched = styleTemplates.find(template => template.markdown === task.skillMarkdown);
  if (matched) return matched.id;
  return task.skillMarkdown === MONTX_DEFAULT_SKILL ? LONGFORM_DEFAULT_STYLE_TEMPLATE_ID : LONGFORM_CUSTOM_STYLE_TEMPLATE_ID;
};

const LongformAgent = ({ workspaceContext }) => {
  const { tasks, setTasks, ready } = useLongformTasks();
  const { brands, setBrands, ready: brandsReady } = useLongformBrandProfiles();
  const styleTemplates = useLongformStyleTemplates();
  const [selectedId, setSelectedId] = React.useState(null);
  const selected = tasks.find(t => t.id === selectedId);

  const upsertTask = (task) => {
    const normalizedTask = normalizeLongformTask(task, brands);
    const brand = getLongformBrand(normalizedTask.brandId, brands);
    const qa = calculateLongformQa(normalizedTask, brand);
    const next = { ...normalizedTask, qa, html: buildLongformHtml({ ...normalizedTask, qa }, brand), updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) };
    setTasks(prev => prev.map(t => t.id === task.id ? next : t));
    return next;
  };

  const createTask = (brandId = workspaceContext?.brandId || 'montx', productId = '') => {
    const brand = getLongformBrand(brandId, brands);
    const product = getLongformProduct(brand, productId);
    const task = {
      id: longformId(),
      brandId,
      productId: product?.id || '',
      narrativeMode: 'brand_product',
      title: '未命名长图文推演',
      prompt: '输入这篇微信公众号长图文的目标、读者、语气、必须覆盖的观点。',
      status: '推演中',
      activeStyleTemplateId: LONGFORM_DEFAULT_STYLE_TEMPLATE_ID,
      skillMarkdown: MONTX_DEFAULT_SKILL,
      images: [],
      sections: LONGFORM_DEFAULT_SECTIONS.map(s => ({ ...s, id: `${s.id}-${Date.now()}` })),
      html: '',
      messages: [
        { role: 'assistant', text: '把这篇公众号长图文的目标、读者、语气和必须覆盖的观点发给我。我会先推演结构，再把它拆成可排版的 section。' },
        { role: 'user', text: '输入这篇微信公众号长图文的目标、读者、语气、必须覆盖的观点。' },
      ],
      overrides: [],
      qa: { score: 90, issues: [], exportReady: true },
      updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
    };
    const saved = { ...task, html: buildLongformHtml(task, brand) };
    setTasks(prev => [saved, ...prev]);
    setSelectedId(saved.id);
  };

  const updateBrand = (brandId, nextBrand) => setBrands(prev => prev.map(brand => brand.id === brandId ? normalizeSharedBrandProfiles([nextBrand])[0] : brand));

  if (!ready || !brandsReady) {
    return <div className="page wide"><div className="card"><div className="card-body">正在加载 Longform 草稿…</div></div></div>;
  }

  return selected ? (
    <LongformDetail task={selected} brands={brands} styleTemplates={styleTemplates} workspaceContext={workspaceContext} onBack={() => setSelectedId(null)} onChange={upsertTask}/>
  ) : (
    <LongformList tasks={tasks} brands={brands} workspaceContext={workspaceContext} onUpdateBrand={updateBrand} onNew={createTask} onOpen={setSelectedId}/>
  );
};

const LongformList = ({ tasks, brands, workspaceContext, onUpdateBrand, onNew, onOpen }) => {
  const [brandId, setBrandId] = React.useState(workspaceContext?.brandId || 'montx');
  const [guardrailExpanded, setGuardrailExpanded] = React.useState(false);
  React.useEffect(() => {
    if (workspaceContext?.brandId) setBrandId(workspaceContext.brandId);
  }, [workspaceContext?.brandId]);
  const brand = getLongformBrand(brandId, brands);
  const defaultProduct = getLongformProduct(brand);
  const activeConstraints = getActiveLongformConstraints(brand);
  const visibleConstraints = guardrailExpanded ? brand.constraints : brand.constraints.slice(0, 4);
  const brandTasks = tasks.filter(t => t.brandId === brandId);
  const overrideCount = brandTasks.filter(t => t.overrides.length).length;
  const riskCount = brandTasks.filter(t => t.qa.issues?.length || t.overrides.length).length;
  React.useEffect(() => {
    window.__LONGFORM_THINKING_CONTEXT = { mode: 'list', tasks, brand, exportState: '' };
    window.dispatchEvent(new CustomEvent('longform-thinking-change'));
  }, [tasks, brandId, brands]);
  return (
    <div className="page wide">
      <div className="page-header">
        <div className="heading">
          <h1>Longform Agent · <span className="hl">长图文内容工厂</span></h1>
          <p>多品牌长文推演、品牌护栏治理、markdown style skill、图片分配与整张 PNG 导出。</p>
        </div>
        <div className="actions">
          <span className="chip ghost mono" style={{fontSize:10}}>Workspace · {brand.name} · {(brand.products || []).length} products share memory</span>
          <button className="btn primary" onClick={() => onNew(brand.id, defaultProduct?.id || '')}><Icon name="plus" className="icon sm"/>新建长图文推演</button>
        </div>
      </div>

      <div className="card longform-policy-card">
        <div className="card-header">
          <h3>继承策略 · Workspace Policy</h3>
          <span className="count">来自品牌记忆 · {brand.name} · {activeConstraints.length} active</span>
          <div className="actions">
            <button className="btn sm ghost" onClick={() => setGuardrailExpanded(v => !v)}>
              <Icon name="chevron" className="icon sm" style={guardrailExpanded ? {transform:'rotate(90deg)'} : {}}/>{guardrailExpanded ? '收起规则' : '展开规则'}
            </button>
            <button className="btn sm ghost" onClick={() => { location.hash = 'memory'; }}>
              <Icon name="settings" className="icon sm"/>前往品牌记忆维护
            </button>
          </div>
        </div>
        <div className="card-body">
          <div className="longform-policy-summary">
            <MetricCard label="启用规则" value={String(activeConstraints.length)} delta={`${brand.constraints.length - activeConstraints.length} disabled`}/>
            <MetricCard label="产品数" value={String(brand.products?.length || 0)} delta={(brand.products || []).map(product => product.name).join(' / ') || '未配置'}/>
            <MetricCard label="解除任务" value={String(overrideCount)} delta="Override 标记" accent={overrideCount > 0}/>
            <MetricCard label="风险任务" value={String(riskCount)} delta="QA / 护栏"/>
          </div>
          <div className="longform-guardrail-list" style={{marginBottom:10}}>
            {(brand.products || []).map(product => (
              <div key={product.id} className="longform-guardrail">
                <Icon name="content" className="icon sm"/>
                <div>
                  <b>{brand.name} {product.name}</b>
                  <span>{product.positioning || '暂无产品定位'}</span>
                </div>
                <span className="chip ghost mono" style={{fontSize:10}}>{tasks.filter(task => task.productId === product.id).length} tasks</span>
              </div>
            ))}
          </div>
          <div className="longform-guardrail-list">
            {visibleConstraints.map(c => (
              <div key={c.id} className={`longform-guardrail ${c.enabled === false ? 'is-disabled' : ''}`}>
                <Icon name={c.enabled === false ? 'warn' : 'check'} className="icon sm"/>
                <div>
                  <b>{c.label}</b>
                  <span>{c.description}</span>
                </div>
                <span className="chip ghost mono" style={{fontSize:10}}>{c.enabled === false ? 'disabled' : c.category}</span>
              </div>
            ))}
          </div>
          <div className="longform-empty-note" style={{marginTop:12}}>
            Longform 只消费当前 Workspace 的品牌/产品叙事与护栏；规则编辑已归属到「品牌记忆」。
          </div>
        </div>
      </div>

      <div className="card">
        <div className="card-header">
          <h3>长图文推演列表</h3>
          <span className="count">{tasks.length} drafts</span>
          <div className="actions">
            <span className="chip ghost mono" style={{fontSize:10}}>IndexedDB autosave</span>
          </div>
        </div>
        <div className="card-body">
          <div className="longform-task-list">
            {tasks.map(task => {
              const taskBrand = getLongformBrand(task.brandId, brands);
              const taskProduct = getLongformProduct(taskBrand, task.productId);
              return (
                <div key={task.id} className="longform-task-row" onClick={() => onOpen(task.id)}>
                  <div className="longform-task-main">
                    <span className="tag accent">{taskBrand.name}</span>
                    {taskProduct && <span className="tag">{taskProduct.name}</span>}
                    <span className="tag mono">{getLongformNarrativeMode(task.narrativeMode).label}</span>
                    <div>
                      <h3>{task.title}</h3>
                      <p>{task.prompt}</p>
                    </div>
                  </div>
                  <div className="longform-task-meta">
                    <span>{parseSkillMarkdown(task.skillMarkdown).title}</span>
                    <span>{task.images.length} 图</span>
                    <span>{task.sections.length} 段</span>
                    <span>QA {task.qa.score}</span>
                  </div>
                  <div className="longform-task-status">
                    <span className={`chip ${task.overrides.length ? 'warn' : task.qa.issues?.length ? 'info' : ''}`} style={{fontSize:10}}>
                      <span className="dot"></span>{task.overrides.length ? 'Override' : task.status}
                    </span>
                    <span className="mono" style={{fontSize:10, color:'var(--ink-4)'}}>{task.updatedAt}</span>
                    <button className="btn sm">继续 <Icon name="chevron" className="icon sm"/></button>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
};

const GuardrailMaintenancePanel = ({ brand, onApply }) => {
  const [input, setInput] = React.useState('');
  const [draft, setDraft] = React.useState(null);
  const parseDraft = () => setDraft(buildLongformGuardrailDraft(input, brand));
  const applyDraft = () => {
    if (!draft) return;
    onApply(applyLongformGuardrailDraft(brand, draft));
    setInput('');
    setDraft(null);
  };
  return (
    <div className="longform-guardrail-maintenance">
      <div className="longform-maintenance-head">
        <div>
          <b>护栏维护</b>
          <span>自然语言会先解析成变更草案，确认后才写入品牌 profile。</span>
        </div>
        <span className="chip ghost mono" style={{fontSize:10}}>IndexedDB policy</span>
      </div>
      <div className="longform-maintenance-grid">
        <div className="longform-maintenance-current">
          {brand.constraints.map(rule => (
            <div key={rule.id} className={`longform-maintenance-rule ${rule.enabled === false ? 'is-disabled' : ''}`}>
              <span className="tag mono">{rule.enabled === false ? 'disabled' : rule.category}</span>
              <b>{rule.label}</b>
              <p>{rule.description}</p>
            </div>
          ))}
        </div>
        <div className="longform-maintenance-editor">
          <textarea
            className="input"
            value={input}
            placeholder="例如：以后所有智能驾驶内容都不能说完全自动驾驶，也不要出现解放双手。"
            onChange={e => setInput(e.target.value)}
          />
          <div className="row">
            <button className="btn sm" onClick={parseDraft} disabled={!input.trim()}>解析变更</button>
            <button className="btn sm primary" onClick={applyDraft} disabled={!draft}>应用草案</button>
          </div>
          {draft && (
            <div className="longform-draft-preview">
              <div className="row" style={{justifyContent:'space-between'}}>
                <span className={`chip ${draft.operation === 'disable' ? 'warn' : 'info'}`} style={{fontSize:10}}>
                  {draft.operation === 'add' ? '新增' : draft.operation === 'edit' ? '编辑' : '停用'}
                </span>
                {draft.targetLabel && <span className="mono" style={{fontSize:10, color:'var(--ink-4)'}}>target: {draft.targetLabel}</span>}
              </div>
              <b>{draft.constraint.label}</b>
              <p>{draft.operation === 'disable' ? `将停用「${draft.targetLabel || '匹配规则'}」，保留历史但不再进入生成约束。` : draft.constraint.description}</p>
              <div className="row" style={{gap:4, flexWrap:'wrap'}}>
                <span className="tag mono">{draft.constraint.category}</span>
                {draft.constraint.forbiddenTerms.map(term => <span key={term} className="tag">禁用：{term}</span>)}
                {Object.entries(draft.constraint.rules).map(([key, value]) => <span key={key} className="tag">{key}: {value}</span>)}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

const buildBrandProductMaintenanceDraft = (input = '', brand) => {
  const text = input.trim();
  if (!text) return null;
  const changes = [];
  const brandPatch = { narrative: {} };
  const productPatches = [];
  const addProducts = [];
  const parseListValue = (value = '') => longformListFromValue(String(value).replace(/^(是|为|成|：|:)/, ''));
  const findField = (label) => text.match(new RegExp(`(?:${label})[^。；;\\n]*(?:是|为|改成|改为|设为|：|:)\\s*([^。；;\\n]+)`))?.[1]?.trim() || '';
  const findForbidden = () => Array.from(text.matchAll(/(?:禁止|禁用|不要|不得|不能|避免)(?:说|使用|出现|表达)?\s*([^。；;\n]+)/g))
    .flatMap(match => parseListValue(match[1].replace(/等.*$/, '')));
  const findRequired = () => Array.from(text.matchAll(/(?:必须|需要|要|提及|强调)(?:表达|覆盖|说明|提到)?\s*([^。；;\n]+)/g))
    .flatMap(match => parseListValue(match[1].replace(/等.*$/, '')));

  const newProduct = text.match(/(?:新增|添加|创建)产品\s*([A-Za-z0-9-]+)/i)?.[1];
  if (newProduct) {
    const product = normalizeLongformProduct({
      id: longformId('product'),
      name: newProduct,
      aliases: [newProduct],
      source: 'natural-language',
      updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
    });
    addProducts.push(product);
    changes.push(`新增产品「${newProduct}」`);
  }

  const product = (brand.products || []).find(item => {
    const names = [item.name, ...(item.aliases || [])].filter(Boolean);
    return names.some(name => new RegExp(`(^|[^A-Za-z0-9])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^A-Za-z0-9]|$)`, 'i').test(text));
  });

  const brandTone = findField('品牌(?:语气|口吻|voice)');
  if (brandTone) {
    brandPatch.narrative.voice = parseListValue(brandTone);
    changes.push(`更新品牌语气：${brandPatch.narrative.voice.join(' / ')}`);
  }
  const brandPositioning = findField('品牌定位');
  if (brandPositioning) {
    brandPatch.narrative.positioning = brandPositioning;
    changes.push('更新品牌定位');
  }
  const worldview = findField('(?:世界观|品牌世界观)');
  if (worldview) {
    brandPatch.narrative.worldview = worldview;
    changes.push('更新品牌世界观');
  }
  const valueProposition = findField('(?:价值主张|品牌主张)');
  if (valueProposition) {
    brandPatch.narrative.valueProposition = valueProposition;
    changes.push('更新价值主张');
  }

  const forbidden = findForbidden();
  const required = findRequired();
  if (product) {
    const productPatch = { id: product.id, patch: {}, narrativePatch: {} };
    const positioning = findField(`${product.name}.*(?:定位|叙事定位)|产品定位`);
    if (positioning) {
      productPatch.patch.positioning = positioning;
      productPatch.narrativePatch.positioning = positioning;
      changes.push(`更新 ${product.name} 定位`);
    }
    const productTone = findField(`${product.name}.*(?:语气|口吻)|产品语气`);
    if (productTone) {
      productPatch.patch.tone = parseListValue(productTone);
      changes.push(`更新 ${product.name} 语气`);
    }
    if (/核心能力/.test(text)) {
      const value = findField(`${product.name}.*核心能力|核心能力`);
      if (value) {
        productPatch.narrativePatch.coreCapabilities = parseListValue(value);
        changes.push(`更新 ${product.name} 核心能力`);
      }
    }
    if (/场景|用户场景/.test(text)) {
      const value = findField(`${product.name}.*(?:用户场景|场景)|用户场景|场景`);
      if (value) {
        productPatch.narrativePatch.userScenarios = parseListValue(value);
        changes.push(`更新 ${product.name} 用户场景`);
      }
    }
    if (forbidden.length) {
      productPatch.patch.forbiddenClaims = Array.from(new Set([...(product.forbiddenClaims || []), ...forbidden]));
      productPatch.narrativePatch.forbiddenClaims = Array.from(new Set([...(product.narrative?.forbiddenClaims || []), ...forbidden]));
      changes.push(`更新 ${product.name} 禁用表达：${forbidden.join(' / ')}`);
    }
    if (required.length) {
      productPatch.patch.requiredClaims = Array.from(new Set([...(product.requiredClaims || []), ...required]));
      productPatch.narrativePatch.requiredMentions = Array.from(new Set([...(product.narrative?.requiredMentions || []), ...required]));
      changes.push(`更新 ${product.name} 必须提及：${required.join(' / ')}`);
    }
    productPatches.push(productPatch);
  } else {
    if (forbidden.length) {
      brandPatch.narrative.forbiddenAngles = Array.from(new Set([...(brand.narrative?.forbiddenAngles || []), ...forbidden]));
      changes.push(`更新品牌禁用角度：${forbidden.join(' / ')}`);
    }
    if (required.length) {
      brandPatch.narrative.requiredThemes = Array.from(new Set([...(brand.narrative?.requiredThemes || []), ...required]));
      changes.push(`更新品牌必须主题：${required.join(' / ')}`);
    }
  }

  if (!changes.length) {
    changes.push('未识别到明确字段，请补充品牌/产品名和要更新的定位、语气、禁用表达或必须提及。');
  }
  return { input: text, brandPatch, productPatches, addProducts, changes, applyable: changes.some(change => !change.startsWith('未识别')) };
};

const applyBrandProductMaintenanceDraft = (brand, draft) => {
  if (!draft?.applyable) return brand;
  const updatedAt = new Date().toLocaleString('zh-CN', { hour12: false });
  return {
    ...brand,
    narrative: { ...(brand.narrative || {}), ...(draft.brandPatch?.narrative || {}) },
    products: [
      ...(brand.products || []).map(product => {
        const found = draft.productPatches.find(item => item.id === product.id);
        if (!found) return product;
        return {
          ...product,
          ...(found.patch || {}),
          narrative: { ...(product.narrative || {}), ...(found.narrativePatch || {}) },
          updatedAt,
        };
      }),
      ...(draft.addProducts || []),
    ],
    updatedAt,
  };
};

const BrandProductMaintenancePanel = ({ brand, onApply }) => {
  const [input, setInput] = React.useState('');
  const [draft, setDraft] = React.useState(null);
  const [advancedOpen, setAdvancedOpen] = React.useState(false);
  const [draftBrand, setDraftBrand] = React.useState(() => ({
    ...brand,
    narrative: { ...(brand.narrative || {}) },
    products: (brand.products || []).map(product => ({ ...product, narrative: { ...(product.narrative || {}) } })),
  }));
  React.useEffect(() => {
    setDraftBrand({ ...brand, narrative: { ...(brand.narrative || {}) }, products: (brand.products || []).map(product => ({ ...product, narrative: { ...(product.narrative || {}) } })) });
    setDraft(null);
    setInput('');
  }, [brand.id, brand.updatedAt, brand.products?.length]);
  const parseListValue = (value = '') => longformListFromValue(value);
  const parseDraft = () => setDraft(buildBrandProductMaintenanceDraft(input, brand));
  const applyNaturalDraft = () => {
    if (!draft?.applyable) return;
    onApply(applyBrandProductMaintenanceDraft(brand, draft));
    setInput('');
    setDraft(null);
  };
  const updateBrandNarrative = (patch) => setDraftBrand(current => ({ ...current, narrative: { ...(current.narrative || {}), ...patch }, updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) }));
  const updateProduct = (productId, patch) => setDraftBrand(current => ({ ...current, products: current.products.map(product => product.id === productId ? { ...product, ...patch, updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) } : product) }));
  const updateProductNarrative = (productId, patch) => setDraftBrand(current => ({ ...current, products: current.products.map(product => product.id === productId ? { ...product, narrative: { ...(product.narrative || {}), ...patch }, updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) } : product) }));
  const addProduct = () => setDraftBrand(current => ({ ...current, products: [...(current.products || []), normalizeLongformProduct({ id: longformId('product'), name: '新产品', source: 'user-maintained', updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) })] }));
  const removeProduct = (productId) => setDraftBrand(current => ({ ...current, products: current.products.filter(product => product.id !== productId) }));
  const applyAdvancedDraft = () => onApply({ ...draftBrand, updatedAt: new Date().toLocaleString('zh-CN', { hour12: false }) });
  return (
    <div className="longform-guardrail-maintenance">
      <div className="longform-maintenance-head">
        <div>
          <b>品牌与产品维护</b>
          <span>默认使用自然语言维护；完整字段编辑收进高级模式。</span>
        </div>
        <button className="btn sm ghost" onClick={() => setAdvancedOpen(v => !v)}>
          <Icon name="settings" className="icon sm"/>{advancedOpen ? '收起高级编辑' : '高级编辑'}
        </button>
      </div>
      <div className="longform-profile-summary">
        <div>
          <span className="tag mono">BRAND</span>
          <b>{brand.name}</b>
          <p>{brand.narrative?.positioning || '暂无品牌定位'}</p>
          <div className="row" style={{gap:4, flexWrap:'wrap'}}>
            {(brand.narrative?.voice || []).slice(0, 6).map(item => <span key={item} className="tag">{item}</span>)}
          </div>
        </div>
        {(brand.products || []).map(product => (
          <div key={product.id}>
            <span className="tag mono">PRODUCT</span>
            <b>{product.name}</b>
            <p>{product.narrative?.positioning || product.positioning || '暂无产品定位'}</p>
          </div>
        ))}
      </div>
      <div className="longform-maintenance-natural">
        <textarea
          className="input"
          value={input}
          placeholder="例如：MONTX 的品牌语气是专业、克制、可信。P10 禁止说完全自动驾驶。"
          onChange={e => setInput(e.target.value)}
        />
        <div className="row">
          <button className="btn sm" onClick={parseDraft} disabled={!input.trim()}>解析草案</button>
          <button className="btn sm primary" onClick={applyNaturalDraft} disabled={!draft?.applyable}>确认应用</button>
          <span className="mono" style={{fontSize:10, color:'var(--ink-4)'}}>应用后自动保存到 Brand Memory</span>
        </div>
        {draft && (
          <div className="longform-draft-preview">
            <div className="row" style={{justifyContent:'space-between'}}>
              <span className={`chip ${draft.applyable ? 'info' : 'warn'}`} style={{fontSize:10}}>
                <span className="dot"></span>{draft.applyable ? '结构化草案' : '需要补充'}
              </span>
              <span className="mono" style={{fontSize:10, color:'var(--ink-4)'}}>{brand.name}</span>
            </div>
            {draft.changes.map(change => <p key={change}>· {change}</p>)}
          </div>
        )}
      </div>
      {advancedOpen && (
        <div className="longform-maintenance-grid">
          <div className="longform-maintenance-editor">
            <label className="form-label">品牌名称</label>
            <input className="input" value={draftBrand.name} onChange={e => setDraftBrand(current => ({ ...current, name: e.target.value }))}/>
            <label className="form-label">Active Skill ID</label>
            <input className="input" value={draftBrand.activeSkillId || ''} onChange={e => setDraftBrand(current => ({ ...current, activeSkillId: e.target.value }))}/>
            <label className="form-label">品牌定位</label>
            <textarea className="input" value={draftBrand.narrative?.positioning || ''} onChange={e => updateBrandNarrative({ positioning: e.target.value })}/>
            <label className="form-label">品牌世界观</label>
            <textarea className="input" value={draftBrand.narrative?.worldview || ''} onChange={e => updateBrandNarrative({ worldview: e.target.value })}/>
            <label className="form-label">价值主张</label>
            <textarea className="input" value={draftBrand.narrative?.valueProposition || ''} onChange={e => updateBrandNarrative({ valueProposition: e.target.value })}/>
            <label className="form-label">品牌语气</label>
            <input className="input" value={(draftBrand.narrative?.voice || []).join('，')} onChange={e => updateBrandNarrative({ voice: parseListValue(e.target.value) })}/>
            <label className="form-label">必须主题</label>
            <textarea className="input" value={(draftBrand.narrative?.requiredThemes || []).join('；')} onChange={e => updateBrandNarrative({ requiredThemes: parseListValue(e.target.value) })}/>
            <label className="form-label">禁用角度</label>
            <textarea className="input" value={(draftBrand.narrative?.forbiddenAngles || []).join('；')} onChange={e => updateBrandNarrative({ forbiddenAngles: parseListValue(e.target.value) })}/>
            <label className="form-label">证明类型</label>
            <input className="input" value={(draftBrand.narrative?.proofTypes || []).join('，')} onChange={e => updateBrandNarrative({ proofTypes: parseListValue(e.target.value) })}/>
            <div className="row" style={{justifyContent:'space-between'}}>
              <span className="chip ghost mono" style={{fontSize:10}}>{draftBrand.products?.length || 0} products</span>
              <button className="btn sm" onClick={addProduct}><Icon name="plus" className="icon sm"/>新增产品</button>
              <button className="btn sm primary" onClick={applyAdvancedDraft}>保存高级编辑</button>
            </div>
          </div>
          <div className="longform-maintenance-current">
            {(draftBrand.products || []).map(product => (
              <div key={product.id} className="longform-maintenance-rule">
                <div className="row" style={{justifyContent:'space-between', alignItems:'center'}}>
                  <span className="tag mono">{product.id}</span>
                  <button className="btn sm ghost" onClick={() => removeProduct(product.id)}>删除</button>
                </div>
                <label className="form-label">产品名</label>
                <input className="input" value={product.name} onChange={e => updateProduct(product.id, { name: e.target.value })}/>
                <label className="form-label">别名</label>
                <input className="input" value={(product.aliases || []).join('，')} onChange={e => updateProduct(product.id, { aliases: parseListValue(e.target.value) })}/>
                <label className="form-label">产品定位</label>
                <textarea className="input" value={product.positioning || ''} onChange={e => updateProduct(product.id, { positioning: e.target.value })}/>
                <label className="form-label">语气</label>
                <input className="input" value={(product.tone || []).join('，')} onChange={e => updateProduct(product.id, { tone: parseListValue(e.target.value) })}/>
                <label className="form-label">产品禁用表达</label>
                <textarea className="input" value={(product.narrative?.forbiddenClaims || product.forbiddenClaims || []).join('；')} onChange={e => updateProductNarrative(product.id, { forbiddenClaims: parseListValue(e.target.value) })}/>
                <label className="form-label">核心能力</label>
                <textarea className="input" value={(product.narrative?.coreCapabilities || []).join('；')} onChange={e => updateProductNarrative(product.id, { coreCapabilities: parseListValue(e.target.value) })}/>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

const LongformContextStack = ({
  brand,
  product,
  task,
  skill,
  contextTab,
  onContextTab,
  activeStyleTemplateId,
  isCustomStyle,
  styleTemplates,
  onStyleTemplateChange,
  onSkillUpload,
}) => {
  const briefing = task.briefing || {};
  const tabs = [
    ['brand', 'Brand'],
    ['briefing', 'Brief'],
    ['writing', 'Writing'],
    ['style', 'Style'],
  ];
  return (
    <div className="card longform-context-stack">
      <div className="card-header">
        <h3>Context Stack</h3>
        <span className="count">{brand.name} · {briefing.coreQuestion || task.title}</span>
      </div>
      <div className="card-body">
        <div className="longform-equal-tabs longform-context-tabs" role="tablist" aria-label="Longform context stack">
          {tabs.map(([id, label]) => (
            <button key={id} className={contextTab === id ? 'active' : ''} onClick={() => onContextTab(id)}>{label}</button>
          ))}
        </div>

        {contextTab === 'brand' && (
          <div className="longform-context-pane">
            <div className="longform-context-line">
              <span>品牌</span>
              <b>{brand.name}</b>
            </div>
            <p>{brand.narrative?.positioning || '暂无品牌定位'}</p>
            <div className="longform-context-tags">
              {(brand.narrative?.voice || []).slice(0, 6).map(item => <span key={item} className="tag">{item}</span>)}
              <span className="tag mono">{getActiveLongformConstraints(brand).length} guardrails</span>
            </div>
            {product && (
              <div className="longform-context-line">
                <span>产品</span>
                <b>{product.name}</b>
              </div>
            )}
          </div>
        )}

        {contextTab === 'briefing' && (
          <div className="longform-context-pane">
            <div className="longform-context-line">
              <span>核心问题</span>
              <b>{briefing.coreQuestion || task.title}</b>
            </div>
            <p>{briefing.goal || task.prompt}</p>
            <div className="longform-context-tags">
              {(briefing.mustCover || []).slice(0, 5).map(item => <span key={item} className="tag">{item}</span>)}
              {briefing.avoidClaims?.length ? <span className="tag warn">{briefing.avoidClaims.length} avoid</span> : null}
            </div>
          </div>
        )}

        {contextTab === 'writing' && (
          <div className="longform-context-pane">
            <div className="longform-context-line">
              <span>叙事模式</span>
              <b>{getLongformNarrativeMode(task.narrativeMode).label}</b>
            </div>
            <p>{getLongformNarrativeMode(task.narrativeMode).description}</p>
            <div className="longform-context-tags">
              <span className="tag">技术拆解</span>
              <span className="tag">克制</span>
              <span className="tag">高信息密度</span>
              <span className="tag mono">Humanize later</span>
            </div>
          </div>
        )}

        {contextTab === 'style' && (
          <div className="longform-context-pane">
            <div className="longform-context-line">
              <span>视觉模板</span>
              <b>{skill.title}</b>
            </div>
            <select
              className="input longform-style-select wide"
              title="快速切换风格模板"
              aria-label="快速切换风格模板"
              value={activeStyleTemplateId}
              onChange={e => onStyleTemplateChange(e.target.value)}
            >
              {isCustomStyle && <option value={LONGFORM_CUSTOM_STYLE_TEMPLATE_ID}>Custom Upload</option>}
              {styleTemplates.map(template => (
                <option key={template.id} value={template.id}>{template.name}</option>
              ))}
            </select>
            <label className="upload-zone-mini">
              <Icon name="upload" className="icon md"/>
              <span>上传 .md style skill</span>
              <input type="file" accept=".md,text/markdown,text/plain" onChange={e => onSkillUpload(e.target.files?.[0])}/>
            </label>
            <SkillMiniInspector skill={skill}/>
          </div>
        )}
      </div>
    </div>
  );
};

const LongformDetail = ({ task, brands = LONGFORM_BRANDS, styleTemplates = [LONGFORM_DEFAULT_STYLE_TEMPLATE], workspaceContext, onBack, onChange }) => {
  const [step, setStep] = React.useState(1);
  const [exportState, setExportState] = React.useState('');
  const [outlineCollapsed, setOutlineCollapsed] = React.useState(false);
  const [skillCollapsed, setSkillCollapsed] = React.useState(true);
  const [imagesCollapsed, setImagesCollapsed] = React.useState(true);
  const [contextTab, setContextTab] = React.useState('brand');
  const [mainView, setMainView] = React.useState('sections');
  const previewRef = React.useRef(null);
  const previewShellRef = React.useRef(null);
  const [previewScale, setPreviewScale] = React.useState(1);
  const [previewHeight, setPreviewHeight] = React.useState(0);
  const [chatInput, setChatInput] = React.useState('');
  const [chatMessages, setChatMessages] = React.useState(() => initialLongformMessages(task));
  const [modelStatus, setModelStatus] = React.useState('idle');
  const [imagePickerSectionId, setImagePickerSectionId] = React.useState('');
  const brand = getLongformBrand(task.brandId, brands);
  const skill = React.useMemo(() => parseSkillMarkdown(task.skillMarkdown), [task.skillMarkdown]);
  const activeStyleTemplateId = getActiveStyleTemplateId(task, styleTemplates);
  const isCustomStyle = activeStyleTemplateId === LONGFORM_CUSTOM_STYLE_TEMPLATE_ID;
  const product = getLongformProduct(brand, task.productId);
  const inspect = React.useMemo(() => buildLongformInspect(task, brand, skill), [task, brand, skill]);

  const publishThinking = (extra = {}, taskSnapshot = task, messagesSnapshot = chatMessages) => {
    const lastUser = [...messagesSnapshot].reverse().find(message => message.role === 'user');
    const lastAgent = [...messagesSnapshot].reverse().find(message => message.role === 'assistant');
    const status = extra.modelStatus || modelStatus;
    const snapshotSkill = parseSkillMarkdown(taskSnapshot.skillMarkdown || task.skillMarkdown);
    const previousThinking = window.__LONGFORM_THINKING_CONTEXT;
    const nextLastAgent = extra.lastAgentMessage ?? lastAgent?.text ?? '';
    const keepPreviousStage = !extra.generationStage
      && previousThinking?.mode === 'detail'
      && previousThinking?.task?.id === taskSnapshot.id
      && previousThinking?.lastAgentMessage === nextLastAgent
      && previousThinking?.generationStage;
    window.__LONGFORM_THINKING_CONTEXT = {
      mode: 'detail',
      task: taskSnapshot,
      brand,
      product: getLongformProduct(brand, taskSnapshot.productId),
      narrativeMode: taskSnapshot.narrativeMode || 'brand_product',
      skill: snapshotSkill,
      styleTemplateName: styleTemplates.find(template => template.id === getActiveStyleTemplateId(taskSnapshot, styleTemplates))?.name || (taskSnapshot.activeStyleTemplateId === LONGFORM_CUSTOM_STYLE_TEMPLATE_ID ? 'Custom Upload' : snapshotSkill.title),
      exportState,
      modelStatus: status,
      generationStage: extra.generationStage || (keepPreviousStage ? previousThinking.generationStage : LONGFORM_MODEL_STAGE[status] || taskSnapshot.status),
      currentInput: extra.currentInput ?? chatInput,
      messages: messagesSnapshot,
      lastUserMessage: extra.lastUserMessage ?? lastUser?.text ?? '',
      lastAgentMessage: nextLastAgent,
      modelError: extra.modelError || '',
    };
    window.dispatchEvent(new CustomEvent('longform-thinking-change'));
  };

  React.useEffect(() => {
    setChatInput('');
    setChatMessages(initialLongformMessages(task));
    setModelStatus('idle');
    setImagePickerSectionId('');
  }, [task.id]);

  React.useEffect(() => {
    publishThinking();
  }, [task, brand, skill, styleTemplates, exportState, modelStatus, chatInput, chatMessages]);

  React.useEffect(() => {
    const syncPreviewScale = () => {
      const shell = previewShellRef.current;
      const canvas = previewRef.current;
      if (!shell || !canvas) return;
      const available = Math.max(320, shell.clientWidth - 2);
      setPreviewScale(Math.min(1, available / 1080));
      setPreviewHeight(canvas.scrollHeight || canvas.offsetHeight || 0);
    };
    const raf = requestAnimationFrame(syncPreviewScale);
    const observer = window.ResizeObserver ? new ResizeObserver(syncPreviewScale) : null;
    if (observer && previewShellRef.current) observer.observe(previewShellRef.current);
    window.addEventListener('resize', syncPreviewScale);
    return () => {
      cancelAnimationFrame(raf);
      observer?.disconnect();
      window.removeEventListener('resize', syncPreviewScale);
    };
  }, [task.sections, task.images, task.title, task.prompt, task.skillMarkdown, task.layoutOverrides, outlineCollapsed]);

  const patchTask = (patch) => onChange({ ...task, ...patch });

  const generateFromPrompt = (promptOverride = task.prompt, messages = task.messages) => {
    const draft = buildLocalLongformDraft(promptOverride, task.title);
    patchTask({ ...draft, prompt: promptOverride, messages, status: '待上传图片' });
    setStep(2);
  };

  const runGuardrailCommand = (text, nextMessages) => {
    const normalized = text.trim();
    if (normalized === '查看护栏') {
      const reply = `当前 ${brand.name} 强制护栏共 ${brand.constraints.length} 条：${brand.constraints.map(rule => rule.label).join('；')}。可输入「解除护栏：规则名；原因：...」或「恢复护栏：规则名」。`;
      return { taskPatch: {}, reply };
    }

    const releaseMatch = normalized.match(/^解除护栏[:：]\s*([^；;\n]+)(?:[；;]\s*原因[:：]\s*(.+))?$/);
    if (releaseMatch) {
      const rule = findLongformConstraint(brand, releaseMatch[1]);
      const reason = (releaseMatch[2] || '').trim();
      if (!rule) return { taskPatch: {}, reply: `没有找到「${releaseMatch[1].trim()}」对应的品牌护栏。可以输入「查看护栏」确认规则名。` };
      if (!reason) return { taskPatch: {}, reply: `解除「${rule.label}」需要填写原因。格式：解除护栏：${rule.label}；原因：客户明确要求。` };
      const override = { constraintId: rule.id, reason, createdAt: new Date().toLocaleString('zh-CN', { hour12: false }) };
      const overrides = task.overrides.some(item => item.constraintId === rule.id)
        ? task.overrides.map(item => item.constraintId === rule.id ? override : item)
        : [...task.overrides, override];
      return { taskPatch: { overrides }, reply: `已记录解除申请：「${rule.label}」。原因：${reason}。QA 与列表页会标记 Override 风险。` };
    }

    const restoreMatch = normalized.match(/^恢复护栏[:：]\s*(.+)$/);
    if (restoreMatch) {
      const rule = findLongformConstraint(brand, restoreMatch[1]);
      if (!rule) return { taskPatch: {}, reply: `没有找到「${restoreMatch[1].trim()}」对应的品牌护栏。可以输入「查看护栏」确认规则名。` };
      const overrides = task.overrides.filter(item => item.constraintId !== rule.id);
      const changed = overrides.length !== task.overrides.length;
      return {
        taskPatch: { overrides },
        reply: changed ? `已恢复品牌护栏：「${rule.label}」。` : `「${rule.label}」当前没有解除记录，无需恢复。`,
      };
    }

    return null;
  };

  const requestLongformIntent = async (text, nextMessages) => {
    const local = classifyLongformIntentLocal(text);
    try {
      const response = await fetch('/api/longform/intent', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          prompt: text,
          messages: nextMessages,
          brand,
          product,
          narrativeMode: task.narrativeMode || 'brand_product',
          skill,
          skillMarkdown: task.skillMarkdown,
          briefing: task.briefing,
          sections: task.sections.map(section => ({ type: section.type, blockType: section.blockType, title: section.title, body: section.body, layoutModule: section.layoutModule })),
          localIntent: local,
        }),
      });
      const data = await response.json().catch(() => ({}));
      if (!response.ok) throw new Error(data.error || `意图服务返回 ${response.status}`);
      return protectLongformIntent(data, text);
    } catch (error) {
      return protectLongformIntent({ ...local, modelError: error.message }, text);
    }
  };

  const sendLongformMessage = async () => {
    const text = chatInput.trim();
    if (!text || ['thinking', 'classifying'].includes(modelStatus)) return;
    const nextMessages = [...chatMessages, { role: 'user', text }];
    const classifyingMessages = [...nextMessages, { role: 'assistant', text: '正在判断需求类型，确认是否需要重新推演文案。' }];
    const classifyingTask = { ...task, messages: nextMessages, status: '需求类型判断中' };
    setChatMessages(classifyingMessages);
    setChatInput('');
    setModelStatus('classifying');
    patchTask({ messages: nextMessages, status: '需求类型判断中' });
    publishThinking({
      modelStatus: 'classifying',
      generationStage: 'LLM 判断需求类型',
      currentInput: text,
      lastUserMessage: text,
      lastAgentMessage: '正在判断需求类型，确认是否需要重新推演文案。',
    }, classifyingTask, classifyingMessages);

    const intent = await requestLongformIntent(text, nextMessages);
    const finishLocalIntent = (reply, taskPatch = {}, stage = '局部命令已处理', status = task.status) => {
      const finalMessages = [...nextMessages, { role: 'assistant', text: reply }];
      const commandTask = { ...task, ...taskPatch, messages: finalMessages, status };
      setChatMessages(finalMessages);
      setModelStatus('idle');
      patchTask(commandTask);
      publishThinking({
        modelStatus: 'idle',
        generationStage: stage,
        currentInput: '',
        lastUserMessage: text,
        lastAgentMessage: reply,
      }, commandTask, finalMessages);
    };

    if (intent.type === 'brand_guardrail') {
      const guardrailCommand = runGuardrailCommand(text, nextMessages);
      if (guardrailCommand) {
        finishLocalIntent(
          guardrailCommand.reply,
          guardrailCommand.taskPatch,
          `需求类型：品牌护栏 / ${intent.source}`,
          guardrailCommand.taskPatch.overrides ? '护栏已更新' : task.status,
        );
      } else {
        finishLocalIntent(`已识别为品牌护栏需求，但这条指令还不够明确。可输入「查看护栏」「解除护栏：规则名；原因：...」或「恢复护栏：规则名」。文案未被修改。`, {}, `需求类型：品牌护栏 / ${intent.source}`);
      }
      return;
    }

    if (intent.type === 'style_query') {
      const qaCommand = runLongformQaCommand(text, task, brand);
      const reply = qaCommand?.reply || `已识别为字体/样式问询，文案不会被修改。当前未能匹配到更具体的检查项，可以问「主标题字体是否符合品牌约束」「正文字号是多少」或「检查字体约束」。`;
      finishLocalIntent(reply, qaCommand?.taskPatch || {}, `需求类型：样式问询 / ${intent.source}`);
      return;
    }

    if (intent.type === 'style_adjustment') {
      const layoutCommand = runLongformLayoutCommand(text, task);
      const reply = layoutCommand?.reply || `已识别为字体/样式调整，因此没有重新推演文案。当前原型可直接执行：主标题单行适配、全文居中、恢复默认对齐。其他样式调整会在下一步接入更细的布局 override。`;
      finishLocalIntent(reply, layoutCommand?.taskPatch || {}, `需求类型：样式调整 / ${intent.source}`, layoutCommand ? '排版已调整' : task.status);
      return;
    }

    if (intent.type === 'image_assignment' || intent.shouldPreserveCopy) {
      const reply = intent.type === 'image_assignment'
        ? '已识别为图片分配需求，因此不会重新推演文案。请使用图片池的一键配图，或在 Section Map 中点击 no image / add image 直接选图。'
        : `已识别为不应改写文案的局部需求（${intent.type}），本次没有进入内容推演。`;
      finishLocalIntent(reply, {}, `需求类型：${intent.type} / ${intent.source}`);
      return;
    }

    const pendingMessages = [...nextMessages, { role: 'assistant', text: `已识别为内容推演需求（${intent.source}），正在调用 DeepSeek 生成长图文推演，请稍等。` }];
    const pendingTask = { ...task, prompt: text, messages: nextMessages, status: '模型推演中' };
    setChatMessages(pendingMessages);
    setModelStatus('thinking');
    patchTask({ prompt: text, messages: nextMessages, status: '模型推演中' });
    publishThinking({
      modelStatus: 'thinking',
      generationStage: `需求类型：内容推演 / ${intent.source}`,
      currentInput: text,
      lastUserMessage: text,
      lastAgentMessage: pendingMessages[pendingMessages.length - 1].text,
    }, pendingTask, pendingMessages);

    try {
      const response = await fetch('/api/longform/generate', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          prompt: text,
          messages: nextMessages,
          brand,
          product,
          narrativeMode: task.narrativeMode || 'brand_product',
          skill,
          skillMarkdown: task.skillMarkdown,
          briefing: task.briefing,
          images: task.images.map(img => ({ name: img.name, assignedSectionId: img.assignedSectionId })),
          sections: task.sections.map(section => ({ type: section.type, blockType: section.blockType, title: section.title, body: section.body, layoutModule: section.layoutModule })),
        }),
      });
      const data = await response.json().catch(() => ({}));
      if (!response.ok) throw new Error(data.error || `模型服务返回 ${response.status}`);
      const sections = normalizeModelSections(data.sections || []);
      if (!sections.length) throw new Error('模型未返回 sections');
      const assistantMessage = data.assistantMessage || '已完成一版长图文结构推演，并同步生成 1080px HTML 长图预览。';
      const finalMessages = [...nextMessages, { role: 'assistant', text: assistantMessage }];
      const generatedTask = {
        ...task,
        title: data.title || buildLocalLongformDraft(text, task.title).title,
        prompt: text,
        briefing: task.briefing,
        narrativeMode: task.narrativeMode || 'brand_product',
        sections,
        messages: finalMessages,
        status: 'HTML 已生成',
      };
      setChatMessages(finalMessages);
      patchTask(generatedTask);
      setStep(2);
      setModelStatus('success');
      publishThinking({
        modelStatus: 'success',
        generationStage: '结构生成完成 / HTML 已生成',
        currentInput: '',
        lastUserMessage: text,
        lastAgentMessage: assistantMessage,
      }, generatedTask, finalMessages);
    } catch (error) {
      const fallback = buildLocalLongformDraft(text, task.title);
      const failureHint = /Failed to fetch|NetworkError|Load failed/i.test(error.message)
        ? '无法连接本地生成服务。请确认页面通过 http://localhost:4173 打开，并刷新后重试。'
        : '模型服务返回异常。请检查 DeepSeek API key、网络或服务端日志后重试。';
      const finalMessages = [
        ...nextMessages,
        { role: 'assistant', text: `真实模型生成暂不可用（${error.message}），已先用本地规则完成一版推演。${failureHint}` },
      ];
      const fallbackTask = { ...task, ...fallback, prompt: text, messages: finalMessages, status: '本地推演已生成' };
      setChatMessages(finalMessages);
      patchTask(fallbackTask);
      setStep(2);
      setModelStatus('fallback');
      publishThinking({
        modelStatus: 'fallback',
        generationStage: '本地兜底 / 失败原因',
        currentInput: '',
        lastUserMessage: text,
        lastAgentMessage: finalMessages[finalMessages.length - 1].text,
        modelError: error.message,
      }, fallbackTask, finalMessages);
    }
  };

  const importBootstrap = async () => {
    setModelStatus('importing');
    publishThinking({
      modelStatus: 'importing',
      generationStage: '导入测试素材',
      lastAgentMessage: '正在读取本地 Brief 和配图。',
    });
    try {
      const response = await fetch('/api/longform/bootstrap');
      const data = await response.json().catch(() => ({}));
      if (!response.ok) throw new Error(data.error || `素材服务返回 ${response.status}`);
      const prompt = data.briefMarkdown || task.prompt;
      const briefing = data.briefing || null;
      const title = prompt.match(/^#\s+(.+)$/m)?.[1]?.trim() || '智能驾驶技术拆解 ADAS Tech Breakdown';
      const images = (data.images || []).map(img => ({
        id: longformId('img'),
        name: img.name,
        dataUrl: img.dataUrl,
        assignedSectionId: '',
      }));
      const messages = [
        { role: 'assistant', text: `已导入测试 Brief 和 ${images.length} 张本地配图。下一步可直接点击发送并推演，调用真实模型生成 section。` },
      ];
      setChatMessages(messages);
      setChatInput(prompt);
      patchTask({ title, prompt, briefing, messages, images, status: '待模型推演' });
      setStep(1);
      setModelStatus('imported');
      publishThinking({
        modelStatus: 'imported',
        generationStage: '测试素材已导入',
        currentInput: prompt,
        lastAgentMessage: briefing ? `${messages[0].text} 已解析结构化 Briefing：${briefing.coreQuestion || briefing.title}` : messages[0].text,
      }, { ...task, title, prompt, briefing, messages, images, status: '待模型推演' }, messages);
    } catch (error) {
      const finalMessages = [
        ...chatMessages,
        { role: 'assistant', text: `无法导入测试 Brief/配图（${error.message}）。请通过本地测试服务器打开页面后再试。` },
      ];
      setChatMessages(finalMessages);
      patchTask({ messages: finalMessages, status: '素材导入失败' });
      setModelStatus('fallback');
      publishThinking({
        modelStatus: 'fallback',
        generationStage: '本地素材导入失败',
        lastAgentMessage: finalMessages[finalMessages.length - 1].text,
        modelError: error.message,
      }, { ...task, messages: finalMessages, status: '素材导入失败' }, finalMessages);
    }
  };

  const handleSkillUpload = (file) => {
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      const skillMarkdown = String(reader.result || '');
      const nextTask = { ...task, skillMarkdown, activeStyleTemplateId: LONGFORM_CUSTOM_STYLE_TEMPLATE_ID, status: '自定义 STYLE 已上传' };
      patchTask(nextTask);
      publishThinking({
        modelStatus,
        generationStage: '自定义 STYLE 已上传',
        lastAgentMessage: '已读取自定义 markdown style skill，HTML 预览将按当前内容刷新。',
      }, nextTask, chatMessages);
    };
    reader.readAsText(file);
  };

  const switchStyleTemplate = (templateId) => {
    const template = styleTemplates.find(item => item.id === templateId);
    if (!template) return;
    const nextTask = {
      ...task,
      activeStyleTemplateId: template.id,
      skillMarkdown: template.markdown,
      status: 'HTML 已生成',
    };
    patchTask(nextTask);
    publishThinking({
      modelStatus,
      generationStage: `STYLE 已切换：${template.name}`,
      lastAgentMessage: `已切换 STYLE 模板为「${template.name}」，当前内容、图片分配和护栏设置保持不变。`,
    }, nextTask, chatMessages);
  };

  const switchProduct = (productId) => {
    const nextProduct = getLongformProduct(brand, productId);
    const nextTask = { ...task, productId, status: '产品 Profile 已切换' };
    patchTask(nextTask);
    publishThinking({
      modelStatus,
      generationStage: `产品已切换：${nextProduct?.name || productId}`,
      lastAgentMessage: `已将当前任务绑定到 ${brand.name} ${nextProduct?.name || productId}。内容和图片分配保持不变，后续推演会继承该产品 Profile。`,
    }, nextTask, chatMessages);
  };

  const switchNarrativeMode = (narrativeMode) => {
    const mode = getLongformNarrativeMode(narrativeMode);
    const nextTask = { ...task, narrativeMode: mode.id, status: '叙事模式已切换' };
    patchTask(nextTask);
    publishThinking({
      modelStatus,
      generationStage: `叙事模式已切换：${mode.label}`,
      lastAgentMessage: `已切换为「${mode.label}」叙事。${mode.description} 当前内容和配图保持不变，下一次内容推演会按该模式组装 prompt。`,
    }, nextTask, chatMessages);
  };

  const handleImageUpload = (files) => {
    const list = Array.from(files || []);
    Promise.all(list.map(file => new Promise(resolve => {
      const reader = new FileReader();
      reader.onload = () => resolve({ id: longformId('img'), name: file.name, dataUrl: reader.result, assignedSectionId: '' });
      reader.readAsDataURL(file);
    }))).then(images => patchTask({ images: [...task.images, ...images], status: '图片分配中' }));
  };

  const assignImage = (imageId, sectionId) => {
    const images = task.images.map(img => img.id === imageId ? { ...img, assignedSectionId: sectionId } : img);
    const sections = syncLongformSectionsWithImages(task.sections, images);
    patchTask({ images, sections, status: 'HTML 已生成' });
  };

  const toggleSectionImage = (sectionId, imageId) => {
    const images = task.images.map(img => {
      if (img.id !== imageId) return img;
      return { ...img, assignedSectionId: img.assignedSectionId === sectionId ? '' : sectionId };
    });
    const sections = syncLongformSectionsWithImages(task.sections, images);
    patchTask({ images, sections, status: 'HTML 已生成' });
  };

  const updateSectionBlock = (sectionId, blockType) => {
    const block = getLongformBlockType(blockType);
    const sections = task.sections.map(section => section.id === sectionId
      ? normalizeLongformSection({ ...section, blockType: block.id, type: block.type, layoutModule: block.layoutModule }, 0)
      : section);
    patchTask({ sections, status: 'Block DSL 已更新' });
  };

  const autoAssignImages = () => {
    const validSectionIds = new Set(task.sections.map(section => section.id));
    const normalizedImages = task.images.map(img => validSectionIds.has(img.assignedSectionId) ? img : { ...img, assignedSectionId: '' });
    const unassignedImages = normalizedImages.filter(img => !img.assignedSectionId);
    const sectionLoad = new Map(task.sections.map(section => [
      section.id,
      normalizedImages.filter(img => img.assignedSectionId === section.id).length,
    ]));
    const emptySections = task.sections.filter(section => (sectionLoad.get(section.id) || 0) === 0);
    const nextSectionFor = (index) => {
      if (index < emptySections.length) return emptySections[index];
      return task.sections.reduce((best, section) => {
        if (!best) return section;
        return (sectionLoad.get(section.id) || 0) < (sectionLoad.get(best.id) || 0) ? section : best;
      }, null);
    };
    let assignedCount = 0;
    const images = normalizedImages.map(img => {
      if (img.assignedSectionId) return img;
      const nextSection = nextSectionFor(assignedCount);
      if (!nextSection) return img;
      sectionLoad.set(nextSection.id, (sectionLoad.get(nextSection.id) || 0) + 1);
      assignedCount += 1;
      return { ...img, assignedSectionId: nextSection.id };
    });
    const sections = syncLongformSectionsWithImages(task.sections, images);
    const configuredCount = images.filter(img => img.assignedSectionId).length;
    const reply = unassignedImages.length
      ? `已按 section 顺序完成一键配图：新增分配 ${assignedCount} 张，当前 ${configuredCount}/${images.length} 张图片已配置进内容推演。`
      : `当前 ${configuredCount}/${images.length} 张图片已配置进内容推演，无需新增分配。`;
    const messages = [...chatMessages, { role: 'assistant', text: reply }];
    setChatMessages(messages);
    patchTask({ images, sections, messages, status: 'HTML 已生成' });
    publishThinking({
      modelStatus,
      generationStage: '一键配图完成',
      lastAgentMessage: reply,
    }, { ...task, images, sections, messages, status: 'HTML 已生成' }, messages);
  };

  const applyOverride = (constraintId, reason) => {
    const clean = reason.trim();
    if (!clean) return;
    const overrides = task.overrides.some(o => o.constraintId === constraintId)
      ? task.overrides.map(o => o.constraintId === constraintId ? { ...o, reason: clean, createdAt: new Date().toLocaleString('zh-CN', { hour12: false }) } : o)
      : [...task.overrides, { constraintId, reason: clean, createdAt: new Date().toLocaleString('zh-CN', { hour12: false }) }];
    patchTask({ overrides });
  };

  const removeOverride = (constraintId) => patchTask({ overrides: task.overrides.filter(o => o.constraintId !== constraintId) });

  const exportPng = async () => {
    setExportState('preparing');
    try {
      if (!window.html2canvas) throw new Error('html2canvas 未加载');
      const iframe = document.createElement('iframe');
      iframe.style.position = 'fixed';
      iframe.style.left = '-1200px';
      iframe.style.top = '0';
      iframe.style.width = '1080px';
      iframe.style.height = '1px';
      iframe.style.border = '0';
      iframe.srcdoc = task.html || buildLongformHtml(task, brand);
      document.body.appendChild(iframe);
      await new Promise((resolve, reject) => {
        iframe.onload = resolve;
        iframe.onerror = () => reject(new Error('导出 iframe 加载失败'));
      });
      const doc = iframe.contentDocument;
      const node = doc.querySelector('.montx');
      if (!node) throw new Error('找不到长图预览容器');
      const images = Array.from(doc.images || []);
      await Promise.all(images.map(img => img.complete ? Promise.resolve() : new Promise(resolve => {
        img.onload = resolve;
        img.onerror = resolve;
      })));
      iframe.style.height = `${node.scrollHeight}px`;
      const exportTheme = getLongformTheme(task.skillMarkdown || MONTX_DEFAULT_SKILL);
      const canvas = await window.html2canvas(node, {
        backgroundColor: exportTheme.bg,
        scale: 1,
        useCORS: true,
        allowTaint: true,
        width: node.scrollWidth,
        height: node.scrollHeight,
        windowWidth: node.scrollWidth,
        windowHeight: node.scrollHeight,
      });
      iframe.remove();
      const link = document.createElement('a');
      link.download = `${task.title || 'longform-agent'}.png`;
      link.href = canvas.toDataURL('image/png');
      link.click();
      setExportState('success');
    } catch (err) {
      document.querySelectorAll('iframe').forEach(frame => {
        if (frame.style.left === '-1200px') frame.remove();
      });
      console.error(err);
      setExportState(`failed: ${err.message}`);
    }
  };

  return (
    <div className="page wide">
      <div className="page-header">
        <div className="heading">
          <h1>Longform Agent · <span className="hl">{task.title}</span></h1>
          <p className="nowrap">独立长文推演工作台：{brand.name}{product ? ` · ${product.name}` : ''} · 继承品牌护栏 · 生成 1080px HTML 长图并导出 PNG</p>
        </div>
        <div className="actions">
          {(brand.products || []).length > 0 && (
            <select className="input" style={{width:140}} value={task.productId || product?.id || ''} onChange={e => switchProduct(e.target.value)} aria-label="选择产品">
              {(brand.products || []).map(item => <option key={item.id} value={item.id}>{item.name}</option>)}
            </select>
          )}
          <select className="input" style={{width:150}} value={task.narrativeMode || 'brand_product'} onChange={e => switchNarrativeMode(e.target.value)} aria-label="选择叙事模式">
            {LONGFORM_NARRATIVE_MODES.map(mode => <option key={mode.id} value={mode.id}>{mode.label}</option>)}
          </select>
          <button className="btn" onClick={onBack}>返回列表</button>
          <button className="btn primary" onClick={exportPng}><Icon name="external" className="icon sm"/>导出 PNG</button>
        </div>
      </div>

      <div className="longform-steps">
        {LONGFORM_STEPS.map((s, i) => (
          <button key={s} className={`step-pill ${step === i + 1 ? 'active' : i + 1 < step ? 'done' : ''}`} onClick={() => setStep(i + 1)}>
            {String(i + 1).padStart(2, '0')} {s}
          </button>
        ))}
      </div>

      <div className="grid longform-workbench">
        <div className="col">
          <LongformContextStack
            brand={brand}
            product={product}
            task={task}
            skill={skill}
            contextTab={contextTab}
            onContextTab={setContextTab}
            activeStyleTemplateId={activeStyleTemplateId}
            isCustomStyle={isCustomStyle}
            styleTemplates={styleTemplates}
            onStyleTemplateChange={switchStyleTemplate}
            onSkillUpload={handleSkillUpload}
          />

          <div className="card longform-image-card">
            <div className="card-header">
              <h3>图片池</h3>
              <span className="count">{task.images.length}</span>
              <div className="actions">
                <button className="btn sm ghost" onClick={autoAssignImages} disabled={!task.images.length || !task.sections.length}>
                  <Icon name="spark" className="icon sm"/>一键配图
                </button>
                <button className="btn-icon" title={imagesCollapsed ? '展开图片池' : '折叠图片池'} aria-label={imagesCollapsed ? '展开图片池' : '折叠图片池'} onClick={() => setImagesCollapsed(v => !v)}>
                  <Icon name="chevron" className="icon sm" style={imagesCollapsed ? {} : {transform:'rotate(90deg)'}}/>
                </button>
              </div>
            </div>
            {!imagesCollapsed && (
              <div className="card-body" style={{display:'flex', flexDirection:'column', gap:10}}>
                <label className="upload-zone-mini">
                  <Icon name="upload" className="icon md"/>
                  <span>上传图片并本地预览</span>
                  <input type="file" accept="image/*" multiple onChange={e => handleImageUpload(e.target.files)}/>
                </label>
                <div className="longform-image-list">
                  {task.images.map(img => (
                    <div key={img.id} className="longform-image-row">
                      <img src={img.dataUrl} alt={img.name}/>
                      <div>
                        <b>{img.name}</b>
                        <select className="input" value={img.assignedSectionId} onChange={e => assignImage(img.id, e.target.value)}>
                          <option value="">未分配</option>
                          {task.sections.map(s => <option key={s.id} value={s.id}>{s.title}</option>)}
                        </select>
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            )}
          </div>

          <LongformChatComposer
            brand={brand}
            task={task}
            messages={chatMessages}
            input={chatInput}
            onInput={setChatInput}
            onSend={sendLongformMessage}
            onImportBootstrap={importBootstrap}
            onTitleChange={(title) => patchTask({ title })}
            modelStatus={modelStatus}
          />
        </div>

        <div className="col">
          <div className="card longform-main-panel">
            <div className="card-header longform-main-panel-head">
              <div className="longform-equal-tabs longform-main-tabs" role="tablist" aria-label="Longform work view">
                <button className={mainView === 'sections' ? 'active' : ''} onClick={() => setMainView('sections')}>推演结构</button>
                <button className={mainView === 'preview' ? 'active' : ''} onClick={() => setMainView('preview')}>长图预览</button>
                <button className={mainView === 'inspect' ? 'active' : ''} onClick={() => setMainView('inspect')}>Inspect</button>
              </div>
              <div className="actions">
                <span className="count">{mainView === 'sections' ? `${task.sections.length} sections` : mainView === 'preview' ? '1080px preview' : `${inspect.issues.length} risks`}</span>
                <span className="chip live"><span className="dot"></span>{task.status}</span>
              </div>
            </div>
            <div className="longform-main-views">
              <div className={`card-body longform-main-view ${mainView === 'sections' ? 'active' : ''}`} aria-hidden={mainView !== 'sections'}>
                <div className="longform-section-map">
                  {task.sections.map((s, i) => {
                    const sectionImages = task.images.filter(img => img.assignedSectionId === s.id);
                    const pickerOpen = imagePickerSectionId === s.id;
                    return (
                      <div key={s.id} className="longform-section">
                        <span className="mono">{String(i + 1).padStart(2, '0')}</span>
                        <div>
                          <b>{s.title}</b>
                          <div className="longform-block-dsl-row">
                            <span>模块</span>
                            <select className="input" value={s.blockType || getLongformBlockType(s).id} onChange={e => updateSectionBlock(s.id, e.target.value)}>
                              {LONGFORM_BLOCK_TYPES.map(block => <option key={block.id} value={block.id}>{block.label}</option>)}
                            </select>
                            <em>{getLongformBlockType(s).description}</em>
                          </div>
                          <p>{s.body}</p>
                          <div className="longform-image-hint">
                            <Icon name="spark" className="icon sm"/>
                            <span>配图建议：{getLongformImageHint(s, i)}</span>
                          </div>
                          <div className="longform-section-images">
                            {sectionImages.map(img => (
                              <button key={img.id} className="tag accent longform-image-chip" onClick={() => toggleSectionImage(s.id, img.id)} title="点击移除这张图">
                                {img.name}
                              </button>
                            ))}
                            <button className={`tag mono longform-image-trigger ${pickerOpen ? 'active' : ''}`} onClick={() => setImagePickerSectionId(pickerOpen ? '' : s.id)}>
                              {sectionImages.length ? 'add image' : 'no image'}
                            </button>
                          </div>
                          {pickerOpen && (
                            <div className="longform-section-image-picker">
                              {task.images.length ? task.images.map(img => (
                                <button
                                  key={img.id}
                                  className={img.assignedSectionId === s.id ? 'selected' : ''}
                                  onClick={() => toggleSectionImage(s.id, img.id)}
                                  title={img.assignedSectionId && img.assignedSectionId !== s.id ? '点击移动到当前段落' : '点击选择图片'}
                                >
                                  <img src={img.dataUrl} alt={img.name}/>
                                  <span>{img.name}</span>
                                </button>
                              )) : <div className="longform-empty-note">图片池为空，请先上传本地图片。</div>}
                            </div>
                          )}
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
              <div className={`card-body longform-main-view longform-preview-body ${mainView === 'preview' ? 'active' : ''}`} aria-hidden={mainView !== 'preview'}>
                <div className="longform-preview-shell" ref={previewShellRef}>
                  <div className="longform-preview-stage" style={{width: 1080 * previewScale, minHeight: previewHeight * previewScale}}>
                    <LongformCanvas task={task} refEl={previewRef} scale={previewScale}/>
                  </div>
                </div>
              </div>
              <div className={`card-body longform-main-view ${mainView === 'inspect' ? 'active' : ''}`} aria-hidden={mainView !== 'inspect'}>
                <LongformInspectPanel inspect={inspect} task={task}/>
              </div>
            </div>
          </div>
        </div>

      </div>
    </div>
  );
};

const ConstraintOverridePanel = ({ brand, task, onApply, onRemove }) => {
  const [reasons, setReasons] = React.useState({});
  const [collapsed, setCollapsed] = React.useState(true);
  return (
    <div className="card">
      <div className="card-header" style={{cursor:'pointer'}} onClick={() => setCollapsed(v => !v)}>
        <h3>品牌护栏继承</h3>
        <span className="count longform-policy-count" title={`${brand.constraints.length} required · ${task.overrides.length} override`}>
          <Icon name="check" className="icon sm"/>{brand.constraints.length}
          <span className="sep">·</span>
          <Icon name="warn" className="icon sm"/>{task.overrides.length}
        </span>
        <div className="actions">
          <button className="btn-icon" title={collapsed ? '展开品牌护栏' : '折叠品牌护栏'} aria-label={collapsed ? '展开品牌护栏' : '折叠品牌护栏'} onClick={e => { e.stopPropagation(); setCollapsed(v => !v); }}>
            <Icon name="chevron" className="icon sm" style={collapsed ? {} : {transform:'rotate(90deg)'}}/>
          </button>
        </div>
      </div>
      {!collapsed && (
        <div className="card-body" style={{display:'flex', flexDirection:'column', gap:8}}>
          {brand.constraints.map(c => {
            const override = task.overrides.find(o => o.constraintId === c.id);
            return (
              <div key={c.id} className={`longform-policy-row ${override ? 'is-override' : ''}`}>
                <div>
                  <b>{c.label}</b>
                  <span>{override ? `已解除：${override.reason}` : c.description}</span>
                </div>
                {override ? (
                  <button className="btn sm ghost" onClick={() => onRemove(c.id)}>恢复</button>
                ) : (
                  <div className="row" style={{alignItems:'stretch'}}>
                    <input className="input" placeholder="解除原因" value={reasons[c.id] || ''} onChange={e => setReasons(r => ({ ...r, [c.id]: e.target.value }))}/>
                    <button className="btn sm" onClick={() => onApply(c.id, reasons[c.id] || '')}>申请解除</button>
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
};

const LongformChatComposer = ({ brand, task, messages, input, onInput, onSend, onImportBootstrap, onTitleChange, modelStatus }) => {
  const streamRef = React.useRef(null);
  React.useEffect(() => {
    const node = streamRef.current;
    if (!node) return;
    node.scrollTop = node.scrollHeight;
  }, [messages, modelStatus]);
  const statusLabel = {
    idle: 'local draft',
    classifying: 'intent check',
    importing: 'importing',
    imported: 'brief ready',
    thinking: 'DeepSeek',
    success: 'model done',
    fallback: 'local fallback',
  }[modelStatus] || 'local draft';
  return (
    <div className="card longform-chat-card">
      <div className="card-header">
        <h3>对话推演</h3>
        <span className={`chip ${['thinking', 'classifying'].includes(modelStatus) ? 'live' : modelStatus === 'fallback' ? 'warn' : 'ghost'}`} style={{fontSize:10}}>
          <span className="dot"></span>{statusLabel}
        </span>
        <div className="actions">
          <button className="btn sm ghost" onClick={onImportBootstrap} disabled={['thinking', 'classifying', 'importing'].includes(modelStatus)}>
            <Icon name="upload" className="icon sm"/>导入测试素材
          </button>
        </div>
      </div>
      <div className="card-body longform-chat">
        <div className="longform-guardrail-command">
          <span className="longform-guardrail-hover">
            <Icon name="check" className="icon sm"/>护栏 {brand.constraints.length}
            <div className="longform-guardrail-popover">
              <div className="longform-popover-title">{brand.name} 护栏状态</div>
              {brand.constraints.map(rule => {
                const override = (task.overrides || []).find(item => item.constraintId === rule.id);
                return (
                  <div key={rule.id} className={`longform-popover-rule ${override ? 'warn' : 'ok'}`}>
                    <b>{rule.label}</b>
                    <em>{override ? `Override：${override.reason}` : 'Locked · 默认继承'}</em>
                  </div>
                );
              })}
            </div>
          </span>
          <span><Icon name="warn" className="icon sm"/>Override {task.overrides.length}</span>
          <b>命令：查看护栏 / 解除护栏：规则名；原因：... / 恢复护栏：规则名</b>
        </div>
        <input className="input" value={task.title} onChange={e => onTitleChange(e.target.value)} aria-label="任务标题"/>
        <div className="longform-chat-stream" ref={streamRef}>
          {messages.map((message, i) => (
            <div key={i} className={`longform-chat-msg ${message.role}`}>
              <span>{message.role === 'assistant' ? 'Agent' : 'You'}</span>
              <p>{message.text}</p>
            </div>
          ))}
        </div>
        <div className="longform-chat-input">
          <textarea
            className="input"
            value={input}
            placeholder="继续输入需求，例如：改成 CEO 署名稿，读者是潜在合作伙伴，语气更克制。"
            onChange={e => onInput(e.target.value)}
            onKeyDown={e => {
              if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') onSend();
            }}
          />
          <button className="btn accent" onClick={onSend} disabled={['thinking', 'classifying'].includes(modelStatus)}>
            <Icon name="spark" className="icon sm"/>{modelStatus === 'classifying' ? '判断中' : modelStatus === 'thinking' ? '生成中' : '发送并推演'}
          </button>
        </div>
      </div>
    </div>
  );
};

const SkillInspector = ({ markdown }) => {
  const skill = parseSkillMarkdown(markdown);
  return (
    <div className="card">
      <div className="card-header"><h3>Skill Inspector</h3><span className="count">{skill.title}</span></div>
      <div className="card-body" style={{display:'flex', flexDirection:'column', gap:10}}>
        <div className="grid g-3">
          <KV label="width" value={`${skill.width}px`}/>
          <KV label="colors" value={String(skill.hex.length)}/>
          <KV label="modules" value={String(skill.components.length)}/>
        </div>
        <Section label="Color Tokens">
          <div className="row" style={{flexWrap:'wrap'}}>
            {skill.hex.map(c => <span key={c} className="tag mono"><span style={{width:10,height:10,background:c,display:'inline-block'}}></span>{c}</span>)}
          </div>
        </Section>
        <Section label="Components">
          <div className="row" style={{flexWrap:'wrap'}}>
            {skill.components.map(c => <span key={c} className="tag">{c}</span>)}
          </div>
        </Section>
      </div>
    </div>
  );
};

const LongformInspectPanel = ({ inspect, task }) => (
  <div className="longform-inspect-panel">
    <div className="longform-inspect-summary">
      <div>
        <span className="tag mono">READINESS</span>
        <b>{inspect.issues.length ? `${inspect.issues.length} 个风险项` : '可进入导出检查'}</b>
        <p>Inspect 用于在微信 HTML / PNG 交付前检查 Brief、Block DSL、图片、护栏和公众号兼容性。</p>
      </div>
      <div>
        <span className="tag mono">BLOCK DSL</span>
        <b>{(task.sections || []).map(section => getLongformBlockType(section).label).join(' / ')}</b>
        <p>当前 DSL 会随草稿保存，后续可直接映射到微信富文本模块。</p>
      </div>
    </div>
    <div className="longform-inspect-grid">
      {inspect.groups.map(group => (
        <div key={group.id} className="longform-inspect-group">
          <h4>{group.title}</h4>
          {group.items.map(item => (
            <div key={item.label} className={`longform-inspect-item ${item.state}`}>
              <span>{item.label}</span>
              <b>{item.value}</b>
              {item.detail && <em>{item.detail}</em>}
            </div>
          ))}
        </div>
      ))}
    </div>
  </div>
);

const SkillMiniInspector = ({ skill }) => (
  <div style={{display:'flex', flexDirection:'column', gap:6}}>
    <div className="row" style={{flexWrap:'wrap'}}>
      {skill.hex.map(c => <span key={c} className="tag mono"><span style={{width:9,height:9,background:c,display:'inline-block'}}></span>{c}</span>)}
    </div>
    <div className="mono" style={{fontSize:10, color:'var(--ink-4)'}}>{skill.components.join(' / ') || 'No component heading parsed'}</div>
  </div>
);

const LongformThinkingRail = () => {
  const [ctx, setCtx] = React.useState(() => window.__LONGFORM_THINKING_CONTEXT || { mode: 'list', tasks: LONGFORM_MOCK_TASKS, brand: LONGFORM_BRANDS[0] });
  const [history, setHistory] = React.useState([]);
  const thinkingBodyRef = React.useRef(null);
  const latestThinkingRef = React.useRef(null);
  React.useEffect(() => {
    const update = () => setCtx(window.__LONGFORM_THINKING_CONTEXT || ctx);
    window.addEventListener('longform-thinking-change', update);
    return () => window.removeEventListener('longform-thinking-change', update);
  }, []);

  const task = ctx.task;
  const brand = ctx.brand || getLongformBrand(task?.brandId);
  const skill = ctx.skill || parseSkillMarkdown(task?.skillMarkdown || MONTX_DEFAULT_SKILL);
  const tasks = ctx.tasks || [];
  const status = ctx.modelStatus || 'idle';
  const stage = ctx.generationStage || LONGFORM_MODEL_STAGE[status] || '待输入需求';
  const validSectionIds = task ? new Set(task.sections.map(section => section.id)) : new Set();
  const unassigned = task ? task.images.filter(i => !i.assignedSectionId || !validSectionIds.has(i.assignedSectionId)).length : 0;
  const assigned = task ? task.images.filter(i => i.assignedSectionId && validSectionIds.has(i.assignedSectionId)).length : 0;
  const checks = task ? [
    ['生成阶段', stage, status === 'fallback' ? 'warn' : status === 'thinking' ? 'idle' : 'ok'],
    ['1080px 画布', '通过', 'ok'],
    ['图片压字', '未检测到', 'ok'],
    ['未分配图片', `${unassigned} 张`, unassigned ? 'warn' : 'ok'],
    ['解除护栏', `${task.overrides.length} 项`, task.overrides.length ? 'warn' : 'ok'],
    ['Markdown skill', skill.title, task.skillMarkdown ? 'ok' : 'warn'],
    ['导出状态', ctx.exportState || '待导出', String(ctx.exportState).startsWith('failed') ? 'warn' : ctx.exportState === 'success' ? 'ok' : 'idle'],
  ] : [
    ['品牌护栏', `${brand.constraints.length} required`, 'ok'],
    ['任务草稿', `${tasks.length} drafts`, 'idle'],
    ['Override 风险', `${tasks.filter(t => t.overrides?.length).length} tasks`, tasks.some(t => t.overrides?.length) ? 'warn' : 'ok'],
  ];
  const userSummary = ctx.lastUserMessage
    ? `最近需求：${ctx.lastUserMessage.slice(0, 52)}${ctx.lastUserMessage.length > 52 ? '…' : ''}`
    : ctx.currentInput
      ? `待发送输入：${ctx.currentInput.slice(0, 52)}${ctx.currentInput.length > 52 ? '…' : ''}`
      : '等待用户输入需求';
  const agentSummary = ctx.lastAgentMessage
    ? `最近回复：${ctx.lastAgentMessage.slice(0, 52)}${ctx.lastAgentMessage.length > 52 ? '…' : ''}`
    : '等待 Agent 输出';
  const modelMessage = status === 'thinking'
    ? 'DeepSeek 调用中，等待结构化 sections 返回'
    : status === 'success'
      ? 'DeepSeek 已返回，结构生成完成'
      : status === 'fallback'
        ? `已进入本地兜底${ctx.modelError ? `：${ctx.modelError}` : ''}`
        : status === 'importing' || status === 'imported'
          ? stage
          : '模型待命';
  const lines = task ? [
    ['requirement-agent', userSummary],
    ['model-agent', modelMessage],
    ['section-agent', `当前结构 ${task.sections.length} 个 section`],
    ['style-skill-agent', `模板：${ctx.styleTemplateName || skill.title} · 版式：${getLongformLayoutPreset(skill).label} · 读取 ${skill.components.length} 个组件规则`],
    ['image-mapping-agent', `${assigned}/${task.images.length} 张图片已分配`],
    ['brand-guardrail', `${brand.constraints.length - task.overrides.length}/${brand.constraints.length} 条护栏保持锁定`],
    ['html-layout-agent', status === 'thinking' ? '等待模型返回后刷新 HTML 预览' : '预览容器固定 1080px，可转 PNG'],
    ['message-agent', agentSummary],
  ] : [
    ['workspace-policy', `${brand.name} 品牌护栏已加载`],
    ['draft-index', `IndexedDB 草稿列表：${tasks.length} 个任务`],
    ['risk-scan', `发现 ${tasks.filter(t => t.qa?.issues?.length || t.overrides?.length).length} 个需关注任务`],
  ];
  React.useEffect(() => {
    const signature = `${task?.id || 'list'}|${status}|${stage}|${ctx.lastUserMessage || ''}|${ctx.lastAgentMessage || ''}|${assigned}|${unassigned}|${skill.title}|${tasks.length}`;
    const stamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
    setHistory(prev => {
      if (prev[prev.length - 1]?.signature === signature) return prev;
      return [...prev.slice(-18), { signature, stamp, stage, lines }];
    });
    requestAnimationFrame(() => {
      latestThinkingRef.current?.scrollIntoView({ block: 'end' });
    });
  }, [task?.id, status, stage, ctx.lastUserMessage, ctx.lastAgentMessage, assigned, unassigned, skill.title, tasks.length]);

  return (
    <aside className="thinking longform-thinking-rail">
      <div className="thinking-header">
        <span className="orb"></span>
        <h3>Longform Agent · 思考过程</h3>
        <span style={{marginLeft:'auto',color:'oklch(0.55 0.008 80)',fontSize:10}}>{task ? status.toUpperCase() : 'LIST'}</span>
      </div>
      <div className="thinking-body" ref={thinkingBodyRef}>
        <div className="longform-rail-section">
          <div className="longform-rail-title">PNG 导出检查</div>
          {checks.map(([label, value, state]) => (
            <div key={label} className={`longform-rail-check ${state}`}>
              <span>{label}</span>
              <b>{value}</b>
            </div>
          ))}
        </div>
        <div className="longform-rail-section">
          <div className="longform-rail-title">Longform Thinking</div>
          <div className="longform-thinking-history">
            {history.map((entry, entryIndex) => (
              <div key={entry.signature} className="longform-thinking-entry" ref={entryIndex === history.length - 1 ? latestThinkingRef : null}>
                <div className="think-line head">
                  <span>{entry.stamp}</span>
                  <b>{entry.stage}</b>
                </div>
                {entry.lines.map(([src, msg], i) => (
                  <div key={`${src}-${i}`} className="think-line">
                    <span className="src">{src}</span>
                    <span className="msg">{msg}</span>
                  </div>
                ))}
              </div>
            ))}
          </div>
        </div>
        <div className="longform-rail-section">
          <div className="longform-rail-title">Skill Inspector</div>
          <div className="longform-rail-kv"><span>skill</span><b>{skill.title}</b></div>
          <div className="longform-rail-kv"><span>width</span><b>{skill.width}px</b></div>
          <div className="longform-rail-kv"><span>modules</span><b>{skill.components.length}</b></div>
          <div className="longform-rail-tags">
            {skill.hex.map(c => <span key={c}><i style={{background:c}}></i>{c}</span>)}
            {skill.components.map(c => <span key={c}>{c.replace(/\(.+\)/, '').trim()}</span>)}
          </div>
        </div>
      </div>
      <div className="think-status">
        <span className="stat-dot"></span>
        <span>{brand.name} policy</span>
        <span style={{marginLeft:'auto'}}>{task ? `QA ${task.qa.score}` : `${tasks.length} drafts`}</span>
      </div>
    </aside>
  );
};

const LongformProcessPanel = ({ task, brand, skill, exportState }) => (
  <div className="card longform-process">
    <div className="card-header">
      <span className="orb" style={{width:7,height:7,borderRadius:'50%',background:'oklch(0.75 0.15 155)', boxShadow:'0 0 8px oklch(0.75 0.15 155)'}}></span>
      <h3>思考过程 · Longform Agent</h3>
      <span className="count">QA {task.qa.score}</span>
    </div>
    <div className="card-body">
      <div className="longform-process-section">
        <div className="longform-process-title">PNG 导出检查</div>
        <div className="longform-process-stack">
          <DiagLine label="1080px 画布" value="通过" ok/>
          <DiagLine label="图片压字" value="未检测到" ok/>
          <DiagLine label="未分配图片" value={`${task.images.filter(i => !i.assignedSectionId).length} 张`} warn={task.images.some(i => !i.assignedSectionId)}/>
          <DiagLine label="解除护栏" value={`${task.overrides.length} 项`} warn={task.overrides.length > 0}/>
          <DiagLine label="Markdown skill" value={skill.title} ok={Boolean(task.skillMarkdown)}/>
          <DiagLine label="导出状态" value={exportState || '待导出'} ok={exportState === 'success'} warn={String(exportState).startsWith('failed')}/>
          {task.qa.issues?.map(issue => (
            <span key={issue} className="chip warn" style={{fontSize:10}}><span className="dot"></span>{issue}</span>
          ))}
        </div>
      </div>

      <div className="longform-process-section">
        <div className="longform-process-title">Longform Thinking</div>
        {[
          ['requirement-agent', '已从输入中提取目标、读者和文章类型'],
          ['longform-agent', '生成 5 段式长图文叙事骨架'],
          ['style-skill-agent', `读取 ${skill.components.length} 个组件规则`],
          ['image-mapping-agent', `${task.images.filter(i => i.assignedSectionId).length}/${task.images.length} 张图片已分配`],
          ['brand-guardrail', `${brand.constraints.length - task.overrides.length}/${brand.constraints.length} 条护栏保持锁定`],
          ['html-layout-agent', '预览容器固定 1080px，可转 PNG'],
        ].map(([src, msg], i) => (
          <div key={i} className="longform-process-line">
            <span>{src}</span>
            <p>{msg}</p>
          </div>
        ))}
      </div>

      <div className="longform-process-section">
        <div className="longform-process-title">Skill Inspector</div>
        <div className="grid g-3">
          <KV label="width" value={`${skill.width}px`}/>
          <KV label="colors" value={String(skill.hex.length)}/>
          <KV label="modules" value={String(skill.components.length)}/>
        </div>
        <div className="longform-process-tags">
          {skill.hex.map(c => <span key={c} className="tag mono"><span style={{width:10,height:10,background:c,display:'inline-block'}}></span>{c}</span>)}
          {skill.components.map(c => <span key={c} className="tag">{c}</span>)}
        </div>
      </div>
    </div>
  </div>
);

const LongformQaPanel = ({ task, brand, skill, exportState }) => (
  <div className="card">
    <div className="card-header">
      <h3>PNG 导出检查</h3>
      <span className="count">QA {task.qa.score}</span>
    </div>
    <div className="card-body" style={{display:'flex', flexDirection:'column', gap:8}}>
      <DiagLine label="1080px 画布" value="通过" ok/>
      <DiagLine label="图片压字" value="未检测到" ok/>
      <DiagLine label="未分配图片" value={`${task.images.filter(i => !i.assignedSectionId).length} 张`} warn={task.images.some(i => !i.assignedSectionId)}/>
      <DiagLine label="解除护栏" value={`${task.overrides.length} 项`} warn={task.overrides.length > 0}/>
      <DiagLine label="Markdown skill" value={skill.title} ok={Boolean(task.skillMarkdown)}/>
      <DiagLine label="导出状态" value={exportState || '待导出'} ok={exportState === 'success'} warn={String(exportState).startsWith('failed')}/>
      {task.qa.issues?.length > 0 && (
        <div style={{marginTop:6, display:'flex', flexDirection:'column', gap:6}}>
          {task.qa.issues.map(issue => <span key={issue} className="chip warn" style={{fontSize:10}}><span className="dot"></span>{issue}</span>)}
        </div>
      )}
    </div>
  </div>
);

const LongformCanvas = ({ task, refEl, scale = 1 }) => {
  const layout = getLongformLayoutPreset(task.skillMarkdown || MONTX_DEFAULT_SKILL);
  const overrideClass = getLongformRenderOverrideClass(task);
  return (
  <div className={`montx-canvas longform-render ${layout.className} ${overrideClass}`} ref={refEl} data-layout={layout.id} style={{...getLongformThemeVars(task.skillMarkdown || MONTX_DEFAULT_SKILL), transform:`scale(${scale})`}}>
    <style>{LONGFORM_LAYOUT_CSS}</style>
    {task.sections.map((s, i) => {
      const imgs = task.images.filter(img => img.assignedSectionId === s.id);
      const imageNodes = imgs.map(img => <img key={img.id} className="montx-image" src={img.dataUrl} alt={img.name} referrerPolicy="no-referrer"/>);
      if (s.type === 'Header') {
        return <React.Fragment key={s.id}>
          <div className="montx-topline"></div>
          <div className="montx-giant" style={getLongformHeroTitleStyle(task)}>{s.title}</div>
          <div className="montx-rule"></div>
          <div className="montx-lead">{s.body}</div>
          {imageNodes}
        </React.Fragment>;
      }
      if (s.type === 'Image') {
        return imgs.length ? imageNodes : (
          <section key={s.id} className="montx-narrative">
            <div className="montx-watermark">{String(i).padStart(2, '0')}</div>
            <div className="montx-section-title">{s.title}</div>
            <div className="montx-body">{s.body}<br/><br/>[ 图片待上传并分配 ]</div>
          </section>
        );
      }
      if (s.type === 'Grid') {
        return <React.Fragment key={s.id}><section className="montx-grid"><div className="montx-grid-left">{s.title}</div><div className="montx-grid-right">{s.body}</div></section>{imageNodes}</React.Fragment>;
      }
      if (s.type === 'Footer') {
        return <React.Fragment key={s.id}>{imageNodes}<footer className="montx-footer"><h2>{s.title}</h2><p>{s.body}</p></footer></React.Fragment>;
      }
      return (
        <React.Fragment key={s.id}>
          <section className="montx-narrative">
            <div className="montx-watermark">{String(i).padStart(2, '0')}</div>
            <div className="montx-section-title">{s.title}</div>
            <div className="montx-body">{s.body}</div>
          </section>
          {imageNodes}
        </React.Fragment>
      );
    })}
  </div>
  );
};

Object.assign(window, {
  LongformAgent,
  LongformThinkingRail,
  useLongformBrandProfiles,
  getLongformBrand,
  getActiveLongformConstraints,
  normalizeLongformBrandProfiles: normalizeSharedBrandProfiles,
  BrandProductMaintenancePanel,
  GuardrailMaintenancePanel,
});
