/**
 * ChatMessage — B 端设计系统 · AI 对话页「一条消息」原子单元
 *
 * 行业对齐：assistant-ui Message / Vercel AI SDK Message / Microsoft Copilot ChatMessage。
 * 与 ChatBubble（仅气泡）、ChatInput（输入器）共同组成 Chat 命名空间。
 * 样式依赖：入口 CSS 须 @import "@tfdesign/b-end/theme.css"（提供 --color-border-default、--color-blueGrey-*、--tfds-ai-execution-* 等）；缺失时 var() 无效，追问/执行流等易出现深黑描边。
 * 一个 ChatMessage = 一条会话消息，按 role 渲染：
 *   · role='ai'   AI 输出消息（默认）：header / thinking / plan / 执行流(title+steps/taskGroups)
 *                  / 结果(resultText+resultArtifacts+confirms) / taskBadge / actions / followUps
 *   · role='user' 用户提问气泡：userQuote + userAttachments + userContent + actions
 *
 * status 状态机：
 *   · 'requesting'  请求中（仅显示 header + 三圆点 loading）
 *   · 'thinking'    深度思考中（仅显示 header + "深度思考中..."）
 *   · 'processing'  执行中（标题旋转环 + 最新操作卡片文案 TextShimmer 扫光）
 *   · 'completed'   已完成（标题对钩 + 完整展示）
 *
 * 命名设计原则（AI 友好）：
 *   1. 名字即结构：<ChatMessage header thinking plan ... result actions /> 读完即知 DOM。
 *   2. 同义短词优先：aiHeader→header / thinkingProcess→thinking / taskPlan→plan
 *      / humanConfirmNodes→confirms / followUpQuestions→followUps / messageActions→actions
 *      / userMessage→userContent。
 *   3. role 决定形态：role 切换时只渲染该角色合法的 part。
 *
 * @prop {'ai'|'user'} [role='ai'] — 消息角色
 * @prop {null|true|object} [header=null] — AI 头像区（原 aiHeader），true 启用默认 OLA AI + catcat 头像，对象自定义 { name, avatarSrc }
 * @prop {null|object} [thinking=null] — 深度思考块（原 thinkingProcess）{ state, durationLabel, content, defaultExpanded }
 * @prop {string} [leadText=''] — 引导文案（在 plan / confirms 之上，用于"我开始规划啦..."这类前导提示）
 * @prop {null|object} [plan=null] — 任务规划卡片（原 taskPlan）{ tasks, primaryActionLabel, defaultExpanded, taskCountLabel }
 * @prop {null|string[]|object} [followUps=null] — 追问按钮组（原 followUpQuestions），传字符串数组或 { items, onSelect }
 * @prop {null|true|object} [actions=null] — 消息操作组（原 messageActions），true 启用默认四件套，对象自定义 { showCopy/Quote/Like/Dislike, copyCount }
 * @prop {null|string} [taskBadge=null] — 任务进行中徽章文案
 * @prop {Array|null} [userContent=null] — role='user' 富文本（原 userMessage）：[{ type:'text', value }] | [{ type:'entity', icon, label, tone }]
 * @prop {Array|null} [userAttachments=null] — role='user' 附件卡数组
 * @prop {object|null} [userQuote=null] — role='user' 紫色划词条
 * @prop {'auto'|'surface'|'fill'} [userBubbleTone='auto'] — role='user' 气泡背景；auto 会根据最近父级背景自动适配：白底用 fill 灰气泡，非白底用 surface 白气泡
 * @prop {string} [timestamp=''] — 消息时间戳（actions 与用户气泡共用）
 *
 * @prop {string} [title=DEFAULT_CHAT_TITLE] — 执行流标题（role='ai'）
 * @prop {'completed'|'processing'|'thinking'|'requesting'} [status='completed'] — 标题节点状态
 * @prop {string} [statusIconName='check-circle-stroked'] — 标题左侧状态图标名（completed 时使用）
 * @prop {Array|null} [steps=DEFAULT_CHAT_STEPS] — 执行步骤数组
 * @prop {Array|null} [taskGroups=null] — 多任务组（每组可独立折叠 + 流式输出）
 * @prop {Array|null} [confirms=null] — 人工确认节点数组（原 humanConfirmNodes），支持 mode='text-card'|'card-only'|'option-card'|'form-card'（option-card = 澄清确认卡片1，form-card = 澄清确认卡片2）
 * @prop {string} [resultText=''] — 结果节点文案
 * @prop {object|null} [resultArtifact=null] — 结果节点单个产物卡片（兼容旧 API）
 * @prop {Array|null} [resultArtifacts=null] — 结果节点产物卡片数组
 * @prop {boolean} [defaultExpanded=true] — 非受控初始展开态（兼容标记）
 * @prop {boolean|undefined} [expanded=undefined] — 受控展开态（兼容标记）
 * @prop {Function|null} [onExpandedChange=null] — 展开态切换回调（兼容标记）
 * @prop {boolean} [continuedBelow=false] — 下方是否仍有后续节点
 * @prop {string} [className=''] — 额外类名
 * @prop {boolean} [bubbleEnter=true] — 挂载时是否播放气泡入场动效（tfds-message-bubble-pop）
 * @prop {React.CSSProperties|undefined} [style=undefined] — 内联样式
 */

import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import Button from './Button';
import Icon from './Icon';
import IconLoading from './IconLoading';
import RadioGroup, { Radio } from './Radio';
import Form from './Form';
import Tooltip from './Tooltip';
import TextShimmer from './TextShimmer';
import catcatSvg from './file-type-assets/catcat.svg';
import { getFileTypeIcon } from './file-type-assets';
import { messageBubbleEnterClass } from '../motion-bubble-enter.js';
/* 用户附件场景沿用语义化别名，避免散点修改业务调用 */
const getUserAttachmentIcon = getFileTypeIcon;
function formatUserAttachmentSize(bytes) {
  if (typeof bytes !== 'number' || Number.isNaN(bytes)) return '';
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
  return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
}

/* ── 默认示例数据：执行流（保持向后兼容）── */
export const DEFAULT_CHAT_TITLE = '梳理抖音电商客服售后退换货政策并生成标准答复口径';

export const DEFAULT_CHAT_STEPS = [
  {
    id: 'collect-rules',
    title: '收集平台售后规则与商家补充条款',
    actionLabel: '正在调用分析 Agent',
    actionDetail: 'Policy_Agent /douyin_ecommerce/after_sales/rules',
    actionIconName: 'line-chart-up-04-stroked',
  },
  {
    id: 'compare-policy',
    title: '归纳退货退款、换货与拒收判责口径',
    actionLabel: '正在调用分析 Agent',
    actionDetail: 'Analyze_agent/API/after_sales/responsibility_rules',
    actionIconName: 'line-chart-up-04-stroked',
  },
  {
    id: 'generate-policy',
    title: '输出客服售后退换货政策说明',
    actionLabel: '正在调用生成 Agent',
    actionDetail: 'Code_Agent /doc/douyin-after-sales-policy',
    actionIconName: 'code-square-01-stroked',
  },
];

/* 执行中 mock：第1步已完成，第2步加载中（用于预览执行中状态） */
export const DEFAULT_CHAT_STEPS_PROCESSING = [
  {
    id: 'collect-rules',
    title: '收集平台售后规则与商家补充条款',
    actionLabel: '正在调用分析 Agent',
    actionDetail: 'Policy_Agent /douyin_ecommerce/after_sales/rules',
    actionIconName: 'line-chart-up-04-stroked',
  },
  {
    id: 'compare-policy',
    title: '归纳退货退款、换货与拒收判责口径',
    actionLabel: '正在调用分析 Agent',
    actionDetail: 'Analyze_agent/API/after_sales/responsibility_rules',
    actionIconName: 'line-chart-up-04-stroked',
  },
];

/* 多任务组 mock：执行流由多个可独立折叠的任务组组成 */
export const DEFAULT_CHAT_TASK_GROUPS = [
  {
    id: 'group-0',
    title:
      '梳理抖音电商客服售后退换货政策并生成标准答复口径，覆盖退货退款、仅退款、换货与拒收等全场景判责口径，输出客服一次性可用的统一答复模板，并按订单状态、商品类目、签收时效与凭证材料分别给出处理建议与升级路径',
    status: 'completed',
    defaultExpanded: true,
    steps: [
      {
        id: 'g0-collect',
        title: '收集平台售后规则与商家补充条款',
        actionLabel: '正在调用分析 Agent',
        actionDetail: 'Policy_Agent /douyin_ecommerce/after_sales/rules',
        actionIconName: 'line-chart-up-04-stroked',
      },
    ],
  },
  {
    id: 'group-1',
    title: '梳理抖音电商客服售后退换货政策并生成标准答复口径',
    status: 'completed',
    defaultExpanded: true,
    steps: [
      {
        id: 'g1-collect',
        title: '收集平台售后规则与商家补充条款',
        actionLabel: '正在调用分析 Agent',
        actionDetail: 'Policy_Agent /douyin_ecommerce/after_sales/rules',
        actionIconName: 'line-chart-up-04-stroked',
      },
      {
        id: 'g1-compare',
        title: '归纳退货退款、换货与拒收判责口径',
        actionLabel: '正在调用分析 Agent',
        actionDetail: 'Analyze_agent/API/after_sales/responsibility_rules',
        actionIconName: 'line-chart-up-04-stroked',
      },
      {
        id: 'g1-generate',
        title: '输出客服售后退换货政策说明',
        actionLabel: '正在调用生成 Agent',
        actionDetail: 'Code_Agent /doc/douyin-after-sales-policy',
        actionIconName: 'code-square-01-stroked',
      },
    ],
  },
  {
    id: 'group-2',
    title: '生成 AB 实验方案并评估指标体系',
    status: 'processing',
    defaultExpanded: true,
    steps: [
      {
        id: 'g2-fetch',
        title: '获取近 7 天客服会话核心指标',
        actionLabel: '正在调用查询 Agent',
        actionDetail: 'Metric_Agent /cs/sessions/last7d',
        actionIconName: 'line-chart-up-04-stroked',
      },
      {
        id: 'g2-design',
        title: '设计 AB 实验分流与回收链路',
        actionLabel: '正在调用实验 Agent',
        actionDetail: 'Experiment_Agent /ab/design',
        actionIconName: 'code-square-01-stroked',
      },
    ],
  },
];

export const DEFAULT_CHAT_RESULT =
  '已整理出抖音电商客服售后退换货政策说明：用户可在平台规则允许范围内申请退货退款、仅退款或换货，客服需优先核验订单状态、商品类目、签收时效、凭证材料与运费归属，再按统一口径答复处理时限、审核条件和升级路径。';

export const DEFAULT_CHAT_RESULT_ARTIFACTS = [
  {
    id: 'web',
    type: 'web',
    title: '抖音电商客服售后退换货政策说明与统一答复口径网页',
    meta: '370.3 KB',
    actionIconName: 'dots-horizontal-stroked',
  },
  {
    id: 'code',
    type: 'code',
    title: '售后规则解析代码',
    meta: '214.6 KB',
    actionIconName: 'dots-horizontal-stroked',
  },
  {
    id: 'table',
    type: 'table',
    title: '退换货判责对照表',
    meta: '158.2 KB',
    actionIconName: 'dots-horizontal-stroked',
  },
];

export const DEFAULT_CHAT_RESULT_ARTIFACT = DEFAULT_CHAT_RESULT_ARTIFACTS[0];

/* 全量产物类型（用于「产物组」枚举预览，与 file-type-assets 共享 lookup 一一对应） */
export const ALL_ARTIFACT_TYPES = [
  { id: 'art-pdf', type: 'pdf', title: 'AI 品牌提案.pdf', meta: '370.3 KB' },
  { id: 'art-code', type: 'code', title: '售后规则解析代码', meta: '4 个文件 · 214.6 KB' },
  { id: 'art-pe', type: 'pe', title: 'PE 工程文件', meta: 'PE · 88.6 KB' },
  { id: 'art-xmind', type: 'xmind', title: '业务架构脑图.xmind', meta: 'XMind · 124.0 KB' },
  { id: 'art-image', type: 'image', title: '品牌主视觉设计稿.png', meta: '图片 · 2.3 MB' },
  { id: 'art-document', type: 'document', title: '抖音电商售后政策汇编.docx', meta: '文档 · 320.0 KB' },
  { id: 'art-table', type: 'table', title: '退换货判责对照表.xlsx', meta: '表格 · 158.2 KB' },
  { id: 'art-webpage', type: 'webpage', title: '客服售后政策说明网页', meta: '网页 · 370.3 KB' },
  { id: 'art-knowledge', type: 'knowledge', title: '电商售后知识库条目', meta: '知识 · 56 条' },
  { id: 'art-strategy', type: 'strategy', title: '统一答复口径策略', meta: '策略 · v1.2' },
  { id: 'art-conversation', type: 'conversation', title: '客户会话上下文', meta: '会话 · 12 条' },
  { id: 'art-batch-report', type: 'batch-report', title: '批量测试报告', meta: '批测 · 240/240 通过' },
  { id: 'art-feishu', type: 'feishu', title: '飞书任务跟进文档', meta: '飞书 · 项目协作' },
  { id: 'art-feishu-card', type: 'feishu-card', title: '飞书消息卡片', meta: '飞书 · 卡片' },
  { id: 'art-feishu-sheet', type: 'feishu-sheet', title: '飞书多维表格', meta: '飞书 · 多维表' },
];

export const DEFAULT_CHAT_ARTIFACT_GROUP = ALL_ARTIFACT_TYPES;

export const DEFAULT_CHAT_CONFIRMS = [
  {
    id: 'confirm-form',
    mode: 'text-card',
    introText: '已输出抖音电商售后政策审核建议，以下关键信息需要人工确认后再继续生成标准答复口径。',
    title: '售后政策确认',
    description: '请确认退货退款、换货与仅退款的适用范围，以及特殊类目限制、签收时效、凭证要求和运费承担口径；若涉及生鲜、定制、虚拟商品或超时售后，请补充例外说明与升级处理路径。',
    iconName: 'sticker-square-stroked',
    primaryActionLabel: '确认继续',
    secondaryActionLabel: '返回修改',
  },
];

export const DEFAULT_CHAT_OPTION_CONFIRM = [
  {
    id: 'confirm-option-card',
    mode: 'option-card',
    title: '澄清确认',
    iconName: 'clipboard-check-stroked',
    primaryActionLabel: '确认',
    secondaryActionLabel: '',
    defaultSelectedValue: 'handoff-guide',
    options: [
      {
        value: 'handoff-guide',
        label: '方案1： 用户请求转人工但系统未触发转接，持续引导自助操作',
      },
      {
        value: 'handoff',
        label: '方案2： 用户请求转人工',
      },
      {
        value: 'self-service',
        label: '方案3： 持续引导自助操作',
      },
    ],
  },
];

export const DEFAULT_CHAT_FORM_CONFIRM = [
  {
    id: 'confirm-form-card',
    mode: 'form-card',
    title: '澄清确认',
    iconName: 'clipboard-check-stroked',
    primaryActionLabel: '确认',
    secondaryActionLabel: '',
    formItems: [
      {
        id: 'scene',
        label: '业务场景',
        type: 'select',
        placeholder: '请选择业务场景',
        defaultValue: 'after-sales',
        options: [
          { value: 'after-sales', label: '售后咨询' },
          { value: 'pre-sales', label: '售前咨询' },
          { value: 'logistics', label: '物流问题' },
        ],
        fullWidth: true,
      },
      {
        id: 'channel',
        label: '处理渠道',
        type: 'select',
        placeholder: '请选择处理渠道',
        defaultValue: 'self-service',
        options: [
          { value: 'self-service', label: '自助引导' },
          { value: 'handoff', label: '转人工' },
        ],
        fullWidth: true,
      },
      {
        id: 'remark',
        label: '补充说明',
        type: 'input',
        placeholder: '请输入补充说明（可选）',
        fullWidth: true,
      },
    ],
  },
];

/* ── 默认示例数据：新增模块 ── */
export const DEFAULT_CHAT_HEADER = {
  name: 'OLA AI',
  avatarSrc: catcatSvg,
};

export const DEFAULT_CHAT_THINKING = {
  state: 'completed',
  durationLabel: '深度思考（用时 21.00 秒）',
  inProgressLabel: '深度思考中 ...',
  content:
    '深度思考结合上下文，猜测用户想问的是在2025年5月16日到5月27日期间人工解决率的变化情况，历史问题中提到了人工指标，所以这里的解决率为人工解决率。\n用户希望查询2025年5月16日到2025年5月27日期间的人工解决率变化情况。需要计算每日的人工解决率，并提供环比和同比变化情况。\n维度：会话开始日期\n指标：解决率  解决率(环比变化)  解决率(同比变化)\n筛选项：会话开始日期(2025-05-16至2025-05-27)  限制1000条',
  defaultExpanded: true,
};

export const DEFAULT_CHAT_PLAN = {
  iconName: 'dotpoints-01-stroked',
  title: '任务规划',
  primaryActionLabel: '开始执行任务',
  secondaryActionLabel: '取消',
  defaultExpanded: true,
  tasks: [
    {
      id: 'plan-1',
      title: '信息准备',
      items: ['分析相关背景，获取接入方基本信息、issue tag 下 kit 能力', '明确 Workflow 改动范围和目标'],
    },
    {
      id: 'plan-3',
      title: 'Workflow 迭代',
      items: ['基于飞书文档内容初步生成 DSL 文件', '补充注释说明并检查语法结构/逻辑完整性'],
    },
    {
      id: 'plan-4',
      title: '批量测试 case 优化 Workflow',
      items: ['抽样 1000 个相关 case 构建评测集', '测试运行并记录输出结论，根据评测结果提出优化建议，并更新飞书文档 DSL 文件'],
    },
    {
      id: 'plan-5',
      title: '总结',
      items: ['产出《社交-私信 Ola Workflow v13》+《优化建议摘要报告》'],
    },
  ],
};

export const DEFAULT_CHAT_FOLLOW_UPS = ['批量测试当前策略', '批量测试当前节点', '开启 AB 实验'];

export const DEFAULT_CHAT_ACTIONS = {
  showCopy: true,
  showQuote: true,
  showLike: true,
  showDislike: true,
  copyCount: 66,
};

export const DEFAULT_CHAT_USER_CONTENT = [
  { type: 'text', value: '分析一批' },
  {
    type: 'entity',
    icon: 'message-chat-square-stroked',
    label: '智能会话:社交私信',
    tone: 'teal',
    showChevron: true,
  },
  { type: 'text', value: '的问题与机会点' },
];

export const DEFAULT_CHAT_USER_ATTACHMENTS = [
  { id: 'att-1', name: '抖音电商售后政策汇编.pdf', size: 320 * 1024 },
  { id: 'att-2', name: '商家补充协议-生鲜定制.pdf', size: 180 * 1024 },
];

/* 用户消息中的紫色引用 / 划词条（quote bar） */
export const DEFAULT_CHAT_USER_QUOTE = {
  iconName: 'type-01-stroked',
  cornerIconName: 'corner-up-right-stroked',
  title: '接口出参',
  description: '具体内容具体内容具体内容具体内容具体内容具体内',
};

/* ── 基础容器类名 ── */
const ROOT = 'tfds-chat-message group/chat-msg flex w-full min-w-0 max-w-full flex-col items-start justify-end';
const BODY = 'relative flex w-full min-w-0 max-w-full flex-col items-start gap-1';
const HEADER = 'group/exec-header relative flex w-full min-w-0 max-w-full items-start gap-3 py-2 cursor-pointer select-none';
const HEADER_ICON_WRAP = 'mt-[2px] flex shrink-0 items-center';
/* 标题：单行截断；不占满宽度，让 chevron 紧贴文字（或省略号）右侧；
   颜色用 text token className（不用 inline style，否则 hover 失效）；
   整行 hover 时仅标题色加深为 foreground，其他元素无任何动效 */
const HEADER_TITLE = [
  'min-w-0 truncate text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0]',
  'text-foreground-secondary transition-colors tfds-motion-ui-fast group-hover/exec-header:text-foreground',
].join(' ');
const ICON_BUTTON_INTERACTIVE = [
  'cursor-pointer transition-colors transition-transform tfds-motion-ui-fast',
  '[outline:2px_solid_transparent] outline-offset-2',
  'hover:bg-fill',
  'active:bg-fill-hover active:scale-[0.97]',
  'focus-visible:outline-blueGrey-400',
  'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
].join(' ');
/* 折叠 chevron：紧跟标题文字（或省略号）右侧，不参与点击交互（整行可点击触发折叠） */
const TOGGLE = 'mt-[2px] ml-2 flex size-4 shrink-0 items-center justify-center';
const GLOBAL_RAIL = 'absolute left-[7.5px] top-[34px] w-px';

/* ── 步骤节点类名 ── */
const STEP_ROW = 'flex min-h-20 w-full min-w-0 max-w-full items-start gap-3';
const STEP_RAIL_WRAP = 'h-full w-4 shrink-0';
const STEP_BODY = 'flex min-w-0 flex-1 flex-col items-start gap-1 pt-1 pb-2';
const STEP_TITLE = ['w-full min-w-0', 'text-sm font-normal leading-5 tracking-[0]'].join(' ');

/* ── 结果节点类名 ── */
const RESULT_ROW = 'w-full min-w-0 max-w-full';
const RESULT_NODE = 'flex w-full min-w-0 flex-col items-start gap-2';
const RESULT_TEXT = ['w-full min-w-0 break-words', 'text-sm font-normal leading-5 tracking-[0]'].join(' ');
const RESULT_ARTIFACT_CARD =
  'relative flex w-[300px] max-w-full items-center gap-2 overflow-visible rounded-lg border border-[0.5px] border-border-default bg-surface p-3';
/** 产物卡选中态：1px 内描边（inset），不外扩 ring/border，避免布局跳动 */
const RESULT_ARTIFACT_CARD_SELECTED_INSET =
  'inset 0 0 0 1px var(--color-brand-500, #56D3BC)';
/** 产物卡 hover：浅灰底，不用 fill-hover（12% 过深） */
const RESULT_ARTIFACT_CARD_HOVER = 'hover:bg-blueGrey-50';
const RESULT_ARTIFACT_ICON = 'flex size-8 shrink-0 items-center justify-center';
const RESULT_ARTIFACT_TEXT_GROUP = 'flex min-w-0 flex-1 flex-col items-start justify-center';
const RESULT_ARTIFACT_TITLE = 'w-full truncate text-xs [font-weight:var(--font-semibold)] leading-4 tracking-[0]';
const RESULT_ARTIFACT_META = 'text-xs font-normal leading-4 tracking-[0]';
const RESULT_ARTIFACT_ACTION = [
  'flex size-8 shrink-0 items-center justify-center rounded-[6px] p-2',
  ICON_BUTTON_INTERACTIVE,
].join(' ');
const RESULT_ARTIFACT_TITLE_WRAP = 'group/artifact-title relative w-full min-w-0';

/* ── 操作卡片类名 ── */
const ACTION_CARD = [
  'inline-flex w-fit min-w-0 max-w-full items-center gap-2',
  'rounded-lg border border-border-default px-2 py-2 pr-3',
].join(' ');
const ACTION_ICON_WRAP = ['flex size-6 shrink-0 items-center justify-center rounded-md border border-border-default'].join(' ');
const ACTION_TEXT_GROUP = 'flex min-w-0 max-w-full items-center gap-2 overflow-hidden';
const ACTION_TEXT_WRAP = 'flex min-w-0 max-w-full flex-1 items-center';
const ACTION_LABEL = ['min-w-0 shrink', 'truncate', 'text-xs font-normal leading-4 tracking-[0]'].join(' ');
const ACTION_DETAIL = [
  'min-w-0 flex-1 shrink truncate',
  'text-xs font-normal leading-4 tracking-[0]',
].join(' ');

/* ── 人工确认节点类名 ── */
const HUMAN_CONFIRM_SECTION = 'flex w-full min-w-0 max-w-full flex-col items-start gap-2';
const HUMAN_CONFIRM_STACK = 'flex w-full min-w-0 max-w-full flex-col items-start gap-1';
const HUMAN_CONFIRM_INTRO = 'w-full min-w-0 text-sm font-normal leading-[26px] tracking-[0]';
const HUMAN_CONFIRM_CARD = 'flex w-full min-w-0 max-w-full flex-col items-stretch gap-3 rounded-[12px] border border-border-default px-4 py-3';
const HUMAN_CONFIRM_HEADER = 'flex w-full min-w-0 items-center gap-2';
const HUMAN_CONFIRM_ICON_WRAP = 'flex size-8 shrink-0 items-center justify-center rounded-md border border-border-default';
const HUMAN_CONFIRM_TITLE = 'min-w-0 flex-1 text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0]';
/* 24px iconButton（与消息操作栏按钮规格一致） */
const HUMAN_CONFIRM_TOGGLE = [
  'flex size-6 shrink-0 items-center justify-center rounded-[6px] p-1',
  ICON_BUTTON_INTERACTIVE,
].join(' ');
const HUMAN_CONFIRM_BODY = 'flex w-full min-w-0 flex-col items-stretch justify-center rounded-[12px] border border-border-default px-[19px] py-[15px]';
const HUMAN_CONFIRM_DESCRIPTION = 'w-full min-w-0 text-sm font-normal leading-5 tracking-[0] text-justify';
const HUMAN_CONFIRM_ACTIONS = 'flex w-full min-w-0 items-center justify-end gap-3';
const HUMAN_CONFIRM_COLLAPSED = 'flex w-full min-w-0 max-w-full items-center gap-2 rounded-[12px] border border-border-default px-4 py-3';
const HUMAN_CONFIRM_OPTION_CARD = HUMAN_CONFIRM_CARD;
const HUMAN_CONFIRM_OPTION_HEADER = HUMAN_CONFIRM_HEADER;
const HUMAN_CONFIRM_OPTION_ICON_WRAP = HUMAN_CONFIRM_ICON_WRAP;
const HUMAN_CONFIRM_OPTION_TITLE = HUMAN_CONFIRM_TITLE;
const HUMAN_CONFIRM_OPTION_BODY = HUMAN_CONFIRM_BODY;
const HUMAN_CONFIRM_OPTION_GROUP = 'w-full gap-2';
const HUMAN_CONFIRM_OPTION_RADIO = 'w-full min-w-0 !justify-start';
const HUMAN_CONFIRM_OPTION_ACTIONS = HUMAN_CONFIRM_ACTIONS;

/* ── 新增：AI 头像 / 深度思考 / 任务规划 / 追问 / 消息操作 / 任务徽章 / 用户气泡 类名 ── */
const AI_HEADER = 'flex w-full min-w-0 items-center gap-2';
const AI_HEADER_AVATAR = 'block size-5 shrink-0 select-none';
const AI_HEADER_NAME = 'text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0]';

const THINKING_BLOCK = 'flex w-full min-w-0 flex-col items-start';
/* 颜色用 text token className（不用 inline style，否则 hover 失效） */
const THINKING_HEADER = [
  'flex w-full min-w-0 items-center gap-1 py-1',
  'text-xs font-normal leading-4 tracking-[0] text-foreground-muted',
].join(' ');
/* 深度思考 header 按钮：仅文字 hover 色加深为 foreground，不要背景/scale/padding 等动效 */
const THINKING_HEADER_INTERACTIVE = 'cursor-pointer transition-colors tfds-motion-ui-fast hover:text-foreground focus-visible:[outline:2px_solid_var(--color-blueGrey-400)] focus-visible:outline-offset-2';
const THINKING_BODY = 'flex w-full min-w-0 items-start gap-2 pl-px pt-1';
const THINKING_RAIL = 'w-px self-stretch shrink-0';
const THINKING_TEXT = [
  'flex-1 min-w-0 whitespace-pre-wrap break-words',
  'text-xs font-normal leading-5 tracking-[0]',
].join(' ');

const TASK_PLAN_CARD = [
  'flex w-full min-w-0 max-w-full flex-col items-stretch overflow-hidden',
  'rounded-[12px] border-[0.5px] border-border-default px-4 py-3',
].join(' ');
const TASK_PLAN_HEADER = 'flex w-full min-w-0 items-center gap-2';
const TASK_PLAN_HEADER_ICON_WRAP = 'flex size-8 shrink-0 items-center justify-center rounded-md border-[0.5px] border-border-default';
const TASK_PLAN_HEADER_TITLE = 'min-w-0 flex-1 text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0]';
const TASK_PLAN_COUNT_BADGE = [
  'inline-flex shrink-0 items-center justify-center rounded-md border border-border-default px-2 py-1',
  'text-xs font-normal leading-4 tracking-[0]',
].join(' ');
const TASK_PLAN_TOGGLE = [
  'flex size-7 shrink-0 items-center justify-center rounded-md',
  ICON_BUTTON_INTERACTIVE,
].join(' ');
const TASK_PLAN_BODY = [
  'mt-3 flex w-full min-w-0 flex-col items-stretch rounded-[12px] border border-border-default',
  'px-5 py-4',
].join(' ');
const TASK_PLAN_LIST = 'flex w-full min-w-0 flex-col items-stretch gap-4';
const TASK_PLAN_TASK_ROW = 'flex w-full min-w-0 flex-col items-start gap-1';
const TASK_PLAN_TASK_HEADER = 'flex w-full min-w-0 items-center gap-2';
const TASK_PLAN_TASK_BADGE = [
  'flex size-4 shrink-0 items-center justify-center rounded-sm border-[0.5px] border-border-default',
  'text-[10px] font-normal leading-4 text-center',
].join(' ');
const TASK_PLAN_TASK_TITLE = 'min-w-0 flex-1 text-sm [font-weight:var(--font-semibold)] leading-5 tracking-[0]';
const TASK_PLAN_ITEM_ROW = 'flex w-full min-w-0 items-start gap-2';
/* 16px 宽 × 20px 高（=单行文本行高）容器 + 8px 灰色空心圆环；
   ROW 用 items-start，靠这里把 dot 中线对齐到首行文本中线 */
const TASK_PLAN_ITEM_DOT_WRAP = 'flex h-5 w-4 shrink-0 items-center justify-center';
const TASK_PLAN_ITEM_DOT = 'block size-2 rounded-full border border-border-default';
const TASK_PLAN_ITEM_TEXT = [
  'flex-1 min-w-0 text-sm font-normal leading-5 tracking-[0]',
].join(' ');
const TASK_PLAN_FOOTER = 'mt-3 flex w-full min-w-0 items-center justify-end gap-3';
/* TaskPlan / HumanConfirm 的主次操作统一复用基础 Button */

const FOLLOW_UP_GROUP = 'flex w-full min-w-0 flex-col items-stretch gap-2';
const FOLLOW_UP_BUTTON = [
  'inline-flex h-10 w-fit max-w-full items-center justify-start',
  'rounded-lg border border-border-default bg-surface px-3 py-2',
  'text-sm font-normal leading-5 tracking-[0] text-left',
  'cursor-pointer transition-colors transition-transform tfds-motion-ui-fast',
  messageBubbleEnterClass('incoming'),
  '[outline:2px_solid_transparent] outline-offset-2',
  'hover:bg-blueGrey-50 hover:border-border-strong',
  'active:bg-blueGrey-100 active:scale-[0.99]',
  'focus-visible:outline-blueGrey-400',
].join(' ');

const MESSAGE_ACTIONS_GROUP = 'flex items-center gap-3 transition-opacity tfds-motion-ui-fast';
const MESSAGE_ACTIONS_HISTORY_HIDDEN = 'opacity-0 group-hover/chat-msg:opacity-100 group-focus-within/chat-msg:opacity-100';
const MESSAGE_ACTION_BUTTON = [
  'flex size-6 shrink-0 items-center justify-center rounded-[6px] p-1',
  ICON_BUTTON_INTERACTIVE,
].join(' ');
const MESSAGE_ACTION_LABEL = 'text-xs font-normal leading-4 tracking-[0]';
const MESSAGE_ACTION_TIMESTAMP = 'text-xs font-normal leading-4 tracking-[0]';
const MESSAGE_ACTION_COPY_COUNT = 'text-xs font-normal leading-4 tracking-[0]';

const TASK_BADGE = [
  'inline-flex w-fit items-center gap-2 rounded-lg border border-border-default px-2 py-1',
  'text-sm font-normal leading-5 tracking-[0]',
].join(' ');

const REQUESTING_DOTS_WRAP = 'flex h-2 w-full items-center';
const REQUESTING_DOT = 'block size-[6px] rounded-full';

/* 思考中行内容器：与 THINKING_HEADER 保持同样的 py-1，确保三种变体首行视觉一致 */
const USER_BUBBLE_WRAP = 'flex w-full min-w-0 flex-col items-end gap-2';
/* 用户气泡：左上 / 左下 / 右下 12px 圆角，右上尖角（IM 发送方向暗示），
   padding 上下 12 / 左右 16，背景随会话背景选择 */
const USER_BUBBLE = [
  'flex w-fit max-w-full flex-col items-stretch gap-2 px-4 py-3',
  'rounded-tl-[12px] rounded-bl-[12px] rounded-br-[12px]',
].join(' ');
const USER_BUBBLE_TONE_CLASS = {
  surface: 'bg-surface',
  fill: 'bg-fill',
};
const DEFAULT_USER_BUBBLE_TONE = 'surface';
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;

function parseRgbColor(value) {
  if (typeof value !== 'string') return null;
  const match = value.trim().match(/^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)$/i);
  if (!match) return null;
  return {
    r: Number(match[1]),
    g: Number(match[2]),
    b: Number(match[3]),
    a: match[4] == null ? 1 : Number(match[4]),
  };
}

function isWhiteBackgroundColor(value) {
  const rgba = parseRgbColor(value);
  if (!rgba || rgba.a <= 0.02) return false;
  return rgba.r >= 250 && rgba.g >= 250 && rgba.b >= 250;
}

function resolveAutoUserBubbleTone(node) {
  if (typeof window === 'undefined' || !node) return DEFAULT_USER_BUBBLE_TONE;

  let current = node.parentElement;
  while (current && current !== document.documentElement) {
    const backgroundColor = window.getComputedStyle(current).backgroundColor;
    const rgba = parseRgbColor(backgroundColor);

    if (rgba && rgba.a > 0.02) {
      return isWhiteBackgroundColor(backgroundColor) ? 'fill' : 'surface';
    }

    current = current.parentElement;
  }

  return DEFAULT_USER_BUBBLE_TONE;
}

function getBackgroundAncestorNodes(node) {
  const nodes = [];
  if (!node) return nodes;

  let current = node.parentElement;
  while (current && current !== document.documentElement) {
    nodes.push(current);
    current = current.parentElement;
  }

  return nodes;
}
/* 用户附件行：右对齐，flex-wrap，gap-2，与气泡并列；max-w-full 跟随容器 */
const USER_ATTACHMENT_ROW = 'flex w-full max-w-full flex-wrap items-start justify-end gap-2';
/* 与 ChatInput 的 ATTACH_CARD 完全对齐：180px 文件卡，bg-fill + border + hover 浅蓝灰；
   展示场景不带删除按钮，左侧只渲染文件类型彩色图标 */
const USER_ATTACHMENT_CARD = [
  'inline-flex items-center gap-2 shrink-0',
  'w-[180px] max-w-[180px] p-2 rounded-lg',
  'bg-fill border border-border-default transition-colors tfds-motion-ui-fast',
  'hover:bg-blueGrey-100',
].join(' ');
const USER_ATTACHMENT_ICON_WRAP = 'shrink-0 relative w-8 h-[34px] inline-flex items-center justify-center';
const USER_ATTACHMENT_TEXT_GROUP = 'flex-1 min-w-0 flex flex-col gap-0.5';
const USER_ATTACHMENT_NAME = 'text-xs leading-4 text-foreground truncate';
const USER_ATTACHMENT_SIZE = 'text-[10px] leading-3 text-foreground-muted';

/* ── 用户引用条（紫色划词条 / quote bar）── */
const USER_QUOTE_ROW = 'flex w-full max-w-full justify-end';
const USER_QUOTE_CARD = [
  'inline-flex max-w-full items-center gap-3 p-3 rounded-[8px]',
  'bg-violet-50 border border-violet-100',
].join(' ');
const USER_QUOTE_ICON_WRAP = [
  'flex size-8 shrink-0 items-center justify-center rounded-[8px]',
  'border-[0.5px] border-violet-200',
].join(' ');
const USER_QUOTE_BODY = 'flex min-w-0 flex-col items-start gap-1';
const USER_QUOTE_TITLE_ROW = 'flex w-full min-w-0 items-center gap-2';
const USER_QUOTE_TITLE = 'min-w-0 truncate text-xs [font-weight:var(--font-semibold)] leading-4 text-foreground';
const USER_QUOTE_DESC = 'min-w-0 truncate text-xs font-normal leading-4 text-foreground-muted';
const USER_BUBBLE_CONTENT = 'flex flex-wrap items-start content-start gap-x-2 gap-y-1';
/* 纯文本段允许换行；break-words 处理超长无空格字符串；
   TAG 实体本身是 inline-flex 不会被影响 */
const USER_BUBBLE_TEXT = [
  'text-sm font-normal leading-[26px] tracking-[0] break-words',
].join(' ');
/* 与 ChatInput 的 ENTITY_CHIP 完全对齐：rounded-md 8px, gap-1, h-26, px-2,
   bg-brand-50 + border-brand-100 + text-foreground，icon 用 brand-500 */
const USER_ENTITY_TAG = [
  'inline-flex items-center gap-1 shrink-0 align-middle',
  'h-[26px] px-2 rounded-md',
  'bg-brand-50 border border-brand-100',
  'text-sm leading-[26px] text-foreground',
].join(' ');

/* ENTITY_TONE_PALETTE 已废弃：用户消息 TAG 现统一复用 ChatInput 的品牌色 chip 样式 */

/* ── 工具：归一化 ── */
function normalizeStep(step, index) {
  return {
    id: step?.id ?? `step-${index}`,
    title: step?.title ?? '',
    actionLabel: step?.actionLabel ?? '',
    actionDetail: step?.actionDetail ?? '',
    actionIconName: step?.actionIconName ?? 'code-square-01-stroked',
  };
}

/* 把任意输入归一化为「任务组」数组 */
function normalizeTaskGroups({ taskGroups, title, status, statusIconName, defaultExpanded, steps }) {
  if (Array.isArray(taskGroups) && taskGroups.length > 0) {
    return taskGroups.map((g, i) => ({
      id: g?.id ?? `group-${i}`,
      title: g?.title ?? '',
      status: g?.status ?? 'completed',
      statusIconName: g?.statusIconName ?? 'check-circle-stroked',
      defaultExpanded: g?.defaultExpanded ?? true,
      steps: Array.isArray(g?.steps) ? g.steps.map(normalizeStep) : [],
    }));
  }
  /* 旧 API 兜底：用顶层 title/steps/status 包装成单组 */
  if (Array.isArray(steps) && steps.length > 0) {
    return [
      {
        id: 'default-group',
        title: title ?? '',
        status: status ?? 'completed',
        statusIconName: statusIconName ?? 'check-circle-stroked',
        defaultExpanded: defaultExpanded ?? true,
        steps: steps.map(normalizeStep),
      },
    ];
  }
  return [];
}

/* 老 type 别名 → 新 PNG icon 类型映射（保持旧 API 数据可用） */
const ARTIFACT_TYPE_ALIAS = {
  web: 'webpage',
};

function normalizeResultArtifact(resultArtifact) {
  if (!resultArtifact) return null;
  const rawType = resultArtifact?.type ?? 'webpage';
  const artifactType = ARTIFACT_TYPE_ALIAS[rawType] ?? rawType;

  return {
    id: resultArtifact?.id ?? artifactType ?? 'artifact',
    type: artifactType,
    title: resultArtifact?.title ?? '',
    meta: resultArtifact?.meta ?? '',
    /* iconName 字段已废弃（保留只为向后兼容数据），实际渲染走 type → PNG */
    iconName: resultArtifact?.iconName ?? null,
    actionIconName: resultArtifact?.actionIconName ?? 'dots-horizontal-stroked',
  };
}

function normalizeResultArtifacts(resultArtifacts, resultArtifact) {
  if (Array.isArray(resultArtifacts) && resultArtifacts.length > 0) {
    return resultArtifacts.map(normalizeResultArtifact).filter(Boolean);
  }
  const normalizedSingle = normalizeResultArtifact(resultArtifact);
  return normalizedSingle ? [normalizedSingle] : [];
}

function normalizeConfirmOption(option, index) {
  if (typeof option === 'string') {
    return {
      value: option,
      label: option,
    };
  }
  const value = option?.value ?? option?.id ?? `option-${index}`;
  return {
    value,
    label: option?.label ?? option?.title ?? String(value),
    disabled: option?.disabled === true,
  };
}

function normalizeHumanConfirmNode(node, index) {
  const mode = ['text-card', 'card-only', 'option-card', 'form-card', 'collapsed'].includes(node?.mode) ? node.mode : 'text-card';
  const options = Array.isArray(node?.options)
    ? node.options.map(normalizeConfirmOption).filter((option) => option.label)
    : [];
  const defaultSelectedValue = node?.defaultSelectedValue ?? node?.selectedValue ?? options[0]?.value;
  const formItems = Array.isArray(node?.formItems) ? node.formItems : [];
  return {
    id: node?.id ?? `human-confirm-${index}`,
    mode,
    collapsed: node?.collapsed === true || mode === 'collapsed',
    introText: node?.introText ?? '',
    title: node?.title ?? '配置表单',
    description: node?.description ?? '',
    iconName: node?.iconName ?? 'sticker-square-stroked',
    primaryActionLabel: node?.primaryActionLabel ?? '确认执行',
    secondaryActionLabel: node?.secondaryActionLabel ?? '编辑',
    defaultConfirmed: node?.defaultConfirmed === true,
    options,
    defaultSelectedValue,
    formItems,
    formValues: node?.formValues && typeof node.formValues === 'object' ? node.formValues : null,
    onOptionChange: typeof node?.onOptionChange === 'function' ? node.onOptionChange : null,
    onFormChange: typeof node?.onFormChange === 'function' ? node.onFormChange : null,
    onPrimaryAction: typeof node?.onPrimaryAction === 'function' ? node.onPrimaryAction : null,
    onSecondaryAction: typeof node?.onSecondaryAction === 'function' ? node.onSecondaryAction : null,
    onToggleCollapsed: typeof node?.onToggleCollapsed === 'function' ? node.onToggleCollapsed : null,
  };
}

function normalizeHumanConfirmNodes(humanConfirmNodes) {
  if (!Array.isArray(humanConfirmNodes) || humanConfirmNodes.length === 0) return [];
  return humanConfirmNodes.map(normalizeHumanConfirmNode).filter(Boolean);
}

function normalizeAiHeader(input) {
  if (!input) return null;
  if (input === true) return { ...DEFAULT_CHAT_HEADER };
  return {
    name: input.name ?? DEFAULT_CHAT_HEADER.name,
    avatarSrc: input.avatarSrc ?? DEFAULT_CHAT_HEADER.avatarSrc,
  };
}

function normalizeThinkingProcess(input) {
  if (!input) return null;
  const state = ['thinking', 'completed'].includes(input.state) ? input.state : 'completed';
  return {
    state,
    durationLabel: input.durationLabel ?? DEFAULT_CHAT_THINKING.durationLabel,
    inProgressLabel: input.inProgressLabel ?? DEFAULT_CHAT_THINKING.inProgressLabel,
    content: input.content ?? '',
    defaultExpanded: input.defaultExpanded !== false,
  };
}

function normalizeTaskPlan(input) {
  if (!input) return null;
  const tasks = Array.isArray(input.tasks)
    ? input.tasks.map((task, idx) => ({
        id: task?.id ?? `task-${idx + 1}`,
        title: task?.title ?? '',
        items: Array.isArray(task?.items) ? task.items.filter(Boolean) : [],
      }))
    : [];
  return {
    iconName: input.iconName ?? DEFAULT_CHAT_PLAN.iconName,
    title: input.title ?? DEFAULT_CHAT_PLAN.title,
    taskCountLabel: input.taskCountLabel ?? `${tasks.length} 个任务`,
    primaryActionLabel: input.primaryActionLabel ?? DEFAULT_CHAT_PLAN.primaryActionLabel,
    secondaryActionLabel: input.secondaryActionLabel ?? DEFAULT_CHAT_PLAN.secondaryActionLabel,
    defaultExpanded: input.defaultExpanded !== false,
    /* defaultConfirmed=true 让历史消息里的卡片初始就处于收起态（按钮不可再点）
     * 这里必须显式透传，否则 TaskPlanCard 拿不到 → 历史卡片可被反复点击 */
    defaultConfirmed: input.defaultConfirmed === true,
    tasks,
    onPrimaryAction: typeof input.onPrimaryAction === 'function' ? input.onPrimaryAction : null,
    onSecondaryAction: typeof input.onSecondaryAction === 'function' ? input.onSecondaryAction : null,
  };
}

function normalizeFollowUpQuestions(input) {
  if (!input) return null;
  const items = Array.isArray(input)
    ? input
    : Array.isArray(input?.items)
    ? input.items
    : [];
  if (items.length === 0) return null;
  return {
    items: items.filter(Boolean).map((item, idx) =>
      typeof item === 'string'
        ? { id: `followup-${idx}`, label: item }
        : { id: item?.id ?? `followup-${idx}`, label: item?.label ?? '' },
    ),
    onSelect: typeof input?.onSelect === 'function' ? input.onSelect : null,
  };
}

function normalizeMessageActions(input) {
  if (!input) return null;
  if (input === true) return { ...DEFAULT_CHAT_ACTIONS, historyMode: false };
  return {
    showCopy: input.showCopy !== false,
    showQuote: input.showQuote !== false,
    showLike: input.showLike !== false,
    showDislike: input.showDislike !== false,
    copyCount: typeof input.copyCount === 'number' ? input.copyCount : null,
    historyMode: input.historyMode === true,
    onCopy: typeof input?.onCopy === 'function' ? input.onCopy : null,
    onQuote: typeof input?.onQuote === 'function' ? input.onQuote : null,
    onLike: typeof input?.onLike === 'function' ? input.onLike : null,
    onDislike: typeof input?.onDislike === 'function' ? input.onDislike : null,
  };
}

function normalizeUserAttachments(input) {
  if (!Array.isArray(input) || input.length === 0) return null;
  return input.map((file, idx) => ({
    id: file?.id ?? `att-${idx}`,
    name: file?.name ?? '未命名文件',
    size: typeof file?.size === 'number' ? file.size : null,
    iconSrc: file?.iconSrc ?? null,
  }));
}

function normalizeUserQuote(input) {
  if (!input || typeof input !== 'object') return null;
  const title = typeof input.title === 'string' ? input.title : '';
  const description = typeof input.description === 'string' ? input.description : '';
  if (!title && !description) return null;
  return {
    iconName: typeof input.iconName === 'string' ? input.iconName : 'type-01-stroked',
    cornerIconName:
      typeof input.cornerIconName === 'string' ? input.cornerIconName : 'corner-up-right-stroked',
    title,
    description,
  };
}

function normalizeUserMessage(input) {
  if (!Array.isArray(input) || input.length === 0) return null;
  return input.map((token, idx) => {
    if (token?.type === 'entity') {
      return {
        type: 'entity',
        id: token.id ?? `entity-${idx}`,
        icon: token.icon ?? 'message-chat-square-stroked',
        label: token.label ?? '',
        showChevron: token.showChevron !== false,
      };
    }
    return {
      type: 'text',
      id: token?.id ?? `text-${idx}`,
      value: token?.value ?? '',
    };
  });
}

/* ── 工具：截断检测 ── */
function isTextTruncated(node) {
  if (!node) return false;
  return node.scrollWidth > node.clientWidth || node.scrollHeight > node.clientHeight;
}

function ThinkingInProgressLabel({ children, className = '' }) {
  const label = children || '深度思考中 ...';
  return (
    <TextShimmer
      size="sm"
      spread="sm"
      baseColor="var(--color-neutral-500)"
      className={['font-normal tracking-[0]', className].filter(Boolean).join(' ')}
    >
      {label}
    </TextShimmer>
  );
}

function ActionCardText({ text, textClassName, textStyle, textRef, shimmer = false }) {
  if (!text) return null;
  const baseColor = textStyle?.color;
  if (shimmer) {
    return (
      <span ref={textRef} className="min-w-0 max-w-full shrink truncate">
        <TextShimmer
          size="sm"
          spread="sm"
          baseColor={baseColor || 'var(--color-blueGrey-600)'}
          className={[textClassName, 'font-normal tracking-[0]'].filter(Boolean).join(' ')}
        >
          {text}
        </TextShimmer>
      </span>
    );
  }
  return (
    <span ref={textRef} className={textClassName} style={textStyle}>
      {text}
    </span>
  );
}

function TruncationTooltipText({ label, detail, labelStyle, detailStyle, shimmer = false }) {
  const labelRef = useRef(null);
  const detailRef = useRef(null);
  const [showTooltip, setShowTooltip] = useState(false);
  const tooltipText = [label, detail].filter(Boolean).join(' ');

  useEffect(() => {
    const update = () => {
      setShowTooltip(isTextTruncated(labelRef.current) || isTextTruncated(detailRef.current));
    };
    update();
    if (typeof ResizeObserver === 'undefined') {
      window.addEventListener('resize', update);
      return () => window.removeEventListener('resize', update);
    }
    const observer = new ResizeObserver(update);
    if (labelRef.current) observer.observe(labelRef.current);
    if (detailRef.current) observer.observe(detailRef.current);
    return () => observer.disconnect();
  }, [label, detail, shimmer]);

  const content = (
    <div className={ACTION_TEXT_GROUP}>
      <ActionCardText
        text={label}
        textClassName={ACTION_LABEL}
        textStyle={labelStyle}
        textRef={labelRef}
        shimmer={shimmer}
      />
      <ActionCardText
        text={detail}
        textClassName={ACTION_DETAIL}
        textStyle={detailStyle}
        textRef={detailRef}
        shimmer={shimmer}
      />
    </div>
  );

  if (!showTooltip || !tooltipText) {
    return <div className={ACTION_TEXT_WRAP}>{content}</div>;
  }

  return (
    <Tooltip content={tooltipText} placement="top" triggerClassName={ACTION_TEXT_WRAP}>
      {content}
    </Tooltip>
  );
}

function OverflowTooltipText({ text, textClassName, textStyle }) {
  const textRef = useRef(null);
  const [showTooltip, setShowTooltip] = useState(false);

  useEffect(() => {
    const update = () => {
      setShowTooltip(isTextTruncated(textRef.current));
    };
    update();
    if (typeof ResizeObserver === 'undefined') {
      window.addEventListener('resize', update);
      return () => window.removeEventListener('resize', update);
    }
    const observer = new ResizeObserver(update);
    if (textRef.current) observer.observe(textRef.current);
    return () => observer.disconnect();
  }, [text]);

  const content = (
    <p ref={textRef} className={textClassName} style={textStyle}>
      {text}
    </p>
  );

  if (!showTooltip || !text) {
    return <div className={RESULT_ARTIFACT_TITLE_WRAP}>{content}</div>;
  }

  return (
    <Tooltip content={text} placement="top" triggerClassName={RESULT_ARTIFACT_TITLE_WRAP}>
      {content}
    </Tooltip>
  );
}

/* ── 标题状态指示（completed/processing）── */
function StatusIndicator({ status = 'completed', statusIconName = 'check-circle-stroked' }) {
  if (status === 'processing') {
    return <IconLoading size="sm" aria-hidden="true" />;
  }
  return (
    <Icon name={statusIconName} size="sm" color="var(--color-brand-500)" aria-hidden="true" />
  );
}

/* ── AI 头像区 ── */
function AIHeader({ aiHeader }) {
  if (!aiHeader) return null;
  return (
    <div className={AI_HEADER} data-tfds-component="ChatMessage.Header">
      <img className={AI_HEADER_AVATAR} src={aiHeader.avatarSrc} alt={aiHeader.name} />
      <p className={AI_HEADER_NAME} style={{ color: 'var(--color-blueGrey-800)' }}>
        {aiHeader.name}
      </p>
    </div>
  );
}

/* ── 请求中三圆点 loading ── */
function RequestingDots() {
  return (
    <div className={REQUESTING_DOTS_WRAP} aria-label="请求中">
      <div className="flex items-center gap-[4px]">
        <span
          className={[REQUESTING_DOT, 'tfds-ai-dot-bounce'].join(' ')}
          style={{ background: 'var(--color-blueGrey-600)', opacity: 0.3 }}
        />
        <span
          className={[REQUESTING_DOT, 'tfds-ai-dot-bounce', 'tfds-ai-dot-bounce--delay-1'].join(' ')}
          style={{ background: 'var(--color-blueGrey-600)', opacity: 0.3 }}
        />
        <span
          className={[REQUESTING_DOT, 'tfds-ai-dot-bounce', 'tfds-ai-dot-bounce--delay-2'].join(' ')}
          style={{ background: 'var(--color-blueGrey-600)', opacity: 0.3 }}
        />
      </div>
    </div>
  );
}

/* ── 深度思考块（思考中/完成）── */
function ThinkingProcess({ thinkingProcess, tokenStyles }) {
  const [internalExpanded, setInternalExpanded] = useState(thinkingProcess.defaultExpanded);
  const isCompleted = thinkingProcess.state === 'completed';
  const showToggle = isCompleted && thinkingProcess.content;
  const showBody = isCompleted && internalExpanded && thinkingProcess.content;

  const headerLabel = isCompleted ? thinkingProcess.durationLabel : thinkingProcess.inProgressLabel;

  return (
    <div className={THINKING_BLOCK}>
      {showToggle ? (
        <button
          type="button"
          className={[THINKING_HEADER, THINKING_HEADER_INTERACTIVE].join(' ')}
          onClick={() => setInternalExpanded((v) => !v)}
          aria-expanded={internalExpanded}
        >
          <span>{headerLabel}</span>
          <Icon
            name={internalExpanded ? 'chevron-up-stroked' : 'chevron-down-stroked'}
            size="sm"
            color="currentColor"
            aria-hidden="true"
          />
        </button>
      ) : (
        <div className={THINKING_HEADER}>
          {isCompleted ? (
            <span>{headerLabel}</span>
          ) : (
            <ThinkingInProgressLabel>{headerLabel}</ThinkingInProgressLabel>
          )}
        </div>
      )}

      {showBody ? (
        <div className={THINKING_BODY}>
          <div
            className={THINKING_RAIL}
            style={{ backgroundColor: 'var(--color-border-default)' }}
            aria-hidden="true"
          />
          <p className={THINKING_TEXT} style={tokenStyles.muted}>
            {thinkingProcess.content}
          </p>
        </div>
      ) : null}
    </div>
  );
}

/* ── 任务规划卡片 ── */
function TaskPlanCard({ taskPlan, tokenStyles }) {
  const isHistoryConfirmed = taskPlan.defaultConfirmed === true;
  const [internalExpanded, setInternalExpanded] = useState(
    !isHistoryConfirmed && taskPlan.defaultExpanded !== false,
  );
  /* 主/次按钮点击后：收起卡片并隐藏操作区；历史消息 defaultConfirmed 初始即收起 */
  const [confirmed, setConfirmed] = useState(isHistoryConfirmed);
  useEffect(() => {
    if (taskPlan.defaultConfirmed === true) {
      setConfirmed(true);
      setInternalExpanded(false);
    }
  }, [taskPlan.defaultConfirmed]);
  const finalizeCard = () => {
    setConfirmed(true);
    setInternalExpanded(false);
  };
  const handlePrimary = () => {
    if (typeof taskPlan.onPrimaryAction === 'function') taskPlan.onPrimaryAction();
    finalizeCard();
  };
  const handleSecondary = () => {
    if (typeof taskPlan.onSecondaryAction === 'function') taskPlan.onSecondaryAction();
    finalizeCard();
  };

  return (
    <div
      className={TASK_PLAN_CARD}
      style={{
        backgroundColor: 'var(--color-fill)',
        borderColor: 'var(--color-border-default)',
      }}
      data-tfds-component="ChatMessage.TaskPlan"
    >
      <div className={TASK_PLAN_HEADER}>
        <div
          className={TASK_PLAN_HEADER_ICON_WRAP}
          style={tokenStyles.iconWrap}
          aria-hidden="true"
        >
          <Icon
            name={taskPlan.iconName}
            size={16}
            color="var(--color-foreground-secondary)"
            aria-hidden="true"
          />
        </div>
        <p className={TASK_PLAN_HEADER_TITLE} style={tokenStyles.title}>
          {taskPlan.title}
        </p>
        {!internalExpanded && taskPlan.taskCountLabel ? (
          <span
            className={TASK_PLAN_COUNT_BADGE}
            style={{
              backgroundColor: 'rgba(255,255,255,0.6)',
              borderColor: 'var(--color-white)',
              color: 'var(--color-blueGrey-700)',
            }}
          >
            {taskPlan.taskCountLabel}
          </span>
        ) : null}
        <button
          type="button"
          className={TASK_PLAN_TOGGLE}
          onClick={() => setInternalExpanded((v) => !v)}
          aria-expanded={internalExpanded}
          aria-label={internalExpanded ? '收起任务规划' : '展开任务规划'}
          style={tokenStyles.muted}
          data-tfds-component="ChatMessage.TaskPlanToggle"
        >
          <Icon
            name={internalExpanded ? 'chevron-up-stroked' : 'chevron-down-stroked'}
            size="sm"
            color="var(--color-blueGrey-600)"
            aria-hidden="true"
          />
        </button>
      </div>

      {internalExpanded ? (
        <>
          <div
            className={TASK_PLAN_BODY}
            style={{
              backgroundColor: 'rgba(255,255,255,0.6)',
              borderColor: 'var(--color-white)',
            }}
          >
            <div className={TASK_PLAN_LIST}>
              {taskPlan.tasks.map((task, idx) => (
                <div key={task.id} className={TASK_PLAN_TASK_ROW}>
                  <div className={TASK_PLAN_TASK_HEADER}>
                    <span
                      className={TASK_PLAN_TASK_BADGE}
                      style={{
                        background: 'linear-gradient(180deg, rgba(240,240,255,0.2) 0%, rgba(136,112,255,0.2) 100%)',
                        borderColor: 'var(--color-violet-200)',
                        color: 'var(--color-violet-500)',
                      }}
                      aria-hidden="true"
                    >
                      {idx + 1}
                    </span>
                    <p className={TASK_PLAN_TASK_TITLE} style={tokenStyles.title}>
                      {task.title}
                    </p>
                  </div>
                  {task.items.map((itemText, itemIdx) => (
                    <div key={`${task.id}-item-${itemIdx}`} className={TASK_PLAN_ITEM_ROW}>
                      <div className={TASK_PLAN_ITEM_DOT_WRAP} aria-hidden="true">
                        <span
                          className={TASK_PLAN_ITEM_DOT}
                          style={{ borderColor: 'var(--color-blueGrey-400)' }}
                        />
                      </div>
                      <p
                        className={TASK_PLAN_ITEM_TEXT}
                        style={{ color: 'var(--color-blueGrey-700)' }}
                      >
                        {itemText}
                      </p>
                    </div>
                  ))}
                </div>
              ))}
            </div>
          </div>
          {taskPlan.primaryActionLabel || taskPlan.secondaryActionLabel ? (
            <div className={TASK_PLAN_FOOTER}>
              {taskPlan.secondaryActionLabel ? (
                <Button
                  variant="outline-black"
                  size="md"
                  onClick={handleSecondary}
                  disabled={confirmed}
                  data-tfds-component="ChatMessage.TaskPlanAction"
                >
                  {taskPlan.secondaryActionLabel}
                </Button>
              ) : null}
              {taskPlan.primaryActionLabel ? (
                <Button
                  variant="primary"
                  size="md"
                  onClick={handlePrimary}
                  disabled={confirmed}
                  data-tfds-component="ChatMessage.TaskPlanAction"
                >
                  {taskPlan.primaryActionLabel}
                </Button>
              ) : null}
            </div>
          ) : null}
        </>
      ) : null}
    </div>
  );
}

/* ── 追问按钮组 ── */
function FollowUpQuestions({ followUpQuestions, tokenStyles }) {
  return (
    <div className={FOLLOW_UP_GROUP} role="group" aria-label="建议追问">
      {followUpQuestions.items.map((item) => (
        <button
          key={item.id}
          type="button"
          className={FOLLOW_UP_BUTTON}
          style={{
            color: 'var(--color-blueGrey-800)',
            ...tokenStyles.title,
          }}
          onClick={
            followUpQuestions.onSelect
              ? () => followUpQuestions.onSelect(item)
              : undefined
          }
          data-tfds-component="ChatMessage.FollowUp"
        >
          {item.label}
        </button>
      ))}
    </div>
  );
}

/* ── 消息操作组（支持 historyMode：默认隐藏 + 父级 hover 显示，但占位保留）── */
function MessageActions({ messageActions, timestamp, layout = 'left', tokenStyles }) {
  const items = [];
  const historyClass = messageActions.historyMode ? MESSAGE_ACTIONS_HISTORY_HIDDEN : '';
  const groupClass = [MESSAGE_ACTIONS_GROUP, historyClass].filter(Boolean).join(' ');

  if (layout === 'right' && timestamp) {
    items.push(
      <span
        key="ts-left"
        className={MESSAGE_ACTION_TIMESTAMP}
        style={tokenStyles.muted}
      >
        {timestamp}
      </span>,
    );
  }

  if (messageActions.showCopy) {
    items.push(
      <button
        key="copy"
        type="button"
        className={MESSAGE_ACTION_BUTTON}
        aria-label="复制"
        onClick={messageActions.onCopy || undefined}
        style={tokenStyles.muted}
      >
        <Icon
          name="copy-06-stroked"
          size="sm"
          color="var(--color-blueGrey-600)"
          aria-hidden="true"
        />
      </button>,
    );
  }
  if (messageActions.showQuote) {
    items.push(
      <button
        key="quote"
        type="button"
        className={MESSAGE_ACTION_BUTTON}
        aria-label="引用"
        onClick={messageActions.onQuote || undefined}
        style={tokenStyles.muted}
      >
        <Icon
          name="ai-quote"
          size="sm"
          color="var(--color-blueGrey-600)"
          aria-hidden="true"
        />
      </button>,
    );
  }
  if (messageActions.showLike) {
    items.push(
      <button
        key="like"
        type="button"
        className={MESSAGE_ACTION_BUTTON}
        aria-label="赞"
        onClick={messageActions.onLike || undefined}
        style={tokenStyles.muted}
      >
        <Icon
          name="thumbs-up-stroked"
          size="sm"
          color="var(--color-blueGrey-600)"
          aria-hidden="true"
        />
      </button>,
    );
  }
  if (messageActions.showDislike) {
    items.push(
      <button
        key="dislike"
        type="button"
        className={MESSAGE_ACTION_BUTTON}
        aria-label="踩"
        onClick={messageActions.onDislike || undefined}
        style={tokenStyles.muted}
      >
        <Icon
          name="thumbs-down-stroked"
          size="sm"
          color="var(--color-blueGrey-600)"
          aria-hidden="true"
        />
      </button>,
    );
  }

  if (layout === 'left' && timestamp) {
    items.push(
      <span
        key="ts-right"
        className={MESSAGE_ACTION_TIMESTAMP}
        style={tokenStyles.muted}
      >
        {timestamp}
      </span>,
    );
  }

  if (items.length === 0) return null;

  return <div className={groupClass} data-tfds-component="ChatMessage.Actions">{items}</div>;
}

/* ── 用户附件卡（参考 ChatInput 附件卡样式，180px 文件卡）── */
function UserAttachmentRow({ attachments }) {
  return (
    <div className={USER_ATTACHMENT_ROW} data-tfds-component="ChatMessage.Attachments">
      {attachments.map((file) => (
        <span key={file.id} className={USER_ATTACHMENT_CARD}>
          <span className={USER_ATTACHMENT_ICON_WRAP} aria-hidden="true">
            <img
              src={getUserAttachmentIcon(file)}
              alt=""
              className="block w-8 h-[34px] object-contain"
            />
          </span>
          <span className={USER_ATTACHMENT_TEXT_GROUP}>
            <span className={USER_ATTACHMENT_NAME} title={file.name}>
              {file.name}
            </span>
            {file.size != null ? (
              <span className={USER_ATTACHMENT_SIZE}>
                {formatUserAttachmentSize(file.size)}
              </span>
            ) : null}
          </span>
        </span>
      ))}
    </div>
  );
}

/* ── 产物图标（atom）：PNG 渲染，type 取自 file-type-assets 共享 lookup ──
 *  为什么单独抽出来：让产物图标可作为独立子组件在预览体系中枚举 / 复用
 */
export function ArtifactIcon({ type = 'webpage', size = 32, className = '', alt = '' }) {
  const src = getFileTypeIcon({ iconType: type });
  return (
    <img
      src={src}
      alt={alt}
      aria-hidden={alt ? undefined : 'true'}
      className={['block object-contain', className].filter(Boolean).join(' ')}
      style={{ width: size, height: size }}
    />
  );
}

/* ── 产物卡片（molecule）：32px ArtifactIcon + 标题 / 描述 + 右侧操作按钮 ──
 *  独立 export：预览或外部组件可单卡复用
 *  数据兼容旧 API：传入对象会先经 normalizeResultArtifact
 */
export function ResultArtifactCard({
  artifact,
  className = '',
  onSelect = null,
  onActionClick = null,
  selected = false,
}) {
  const a = normalizeResultArtifact(artifact);
  if (!a) return null;
  const isInteractive = typeof onSelect === 'function';
  return (
    <div
      role={isInteractive ? 'button' : undefined}
      tabIndex={isInteractive ? 0 : undefined}
      onClick={isInteractive ? () => onSelect(a) : undefined}
      onKeyDown={isInteractive ? (event) => {
        if (event.key === 'Enter' || event.key === ' ') {
          event.preventDefault();
          onSelect(a);
        }
      } : undefined}
      className={[
        RESULT_ARTIFACT_CARD,
        isInteractive ? `cursor-pointer transition-colors ${RESULT_ARTIFACT_CARD_HOVER}` : '',
        className,
      ].filter(Boolean).join(' ')}
      style={{
        boxShadow: selected ? RESULT_ARTIFACT_CARD_SELECTED_INSET : undefined,
      }}
      data-tfds-component="ChatMessage.ResultArtifactCard"
      data-selected={selected || undefined}
    >
      <div className={RESULT_ARTIFACT_ICON} aria-hidden="true">
        <ArtifactIcon type={a.type} size={32} />
      </div>
      <div className={RESULT_ARTIFACT_TEXT_GROUP}>
        <OverflowTooltipText
          text={a.title}
          textClassName={RESULT_ARTIFACT_TITLE}
          textStyle={{ color: 'var(--color-blueGrey-800)' }}
        />
        {a.meta ? (
          <p className={RESULT_ARTIFACT_META} style={{ color: 'var(--color-blueGrey-600)' }}>
            {a.meta}
          </p>
        ) : null}
      </div>
      <button
        type="button"
        className={RESULT_ARTIFACT_ACTION}
        aria-label={`${a.title || '产物'}更多操作`}
        style={{ color: 'var(--color-blueGrey-600)' }}
        onClick={onActionClick ? (event) => {
          event.stopPropagation();
          onActionClick(a);
        } : undefined}
      >
        <Icon
          name={a.actionIconName}
          size={16}
          color="var(--color-blueGrey-600)"
          aria-hidden="true"
        />
      </button>
    </div>
  );
}

/* ── 产物组（organism）：纵向排列多张产物卡片 ── */
export function ResultArtifactGroup({
  artifacts,
  className = '',
  onArtifactSelect = null,
  selectedArtifactId = null,
  onArtifactActionClick = null,
}) {
  const list = normalizeResultArtifacts(artifacts);
  if (!list.length) return null;
  return (
    <div
      className={['flex w-full min-w-0 flex-col items-stretch gap-2', className]
        .filter(Boolean)
        .join(' ')}
    >
      {list.map((a) => (
        <ResultArtifactCard
          key={a.id}
          artifact={a}
          onSelect={onArtifactSelect}
          onActionClick={onArtifactActionClick}
          selected={selectedArtifactId != null && selectedArtifactId === a.id}
        />
      ))}
    </div>
  );
}

/* ── 用户引用条（紫色划词条 / quote bar）── */
function UserQuote({ quote }) {
  return (
    <div className={USER_QUOTE_ROW}>
      <div
        className={USER_QUOTE_CARD}
        style={{
          /* 紫色渐变背景细微调，参考 Figma：violet-50 + violet-100 边框 */
          background: 'var(--color-violet-50)',
          borderColor: 'var(--color-violet-100)',
        }}
      >
        {/* 左侧 32x32 紫色 icon 容器（淡紫渐变 + violet-200 边框）*/}
        <div
          className={USER_QUOTE_ICON_WRAP}
          style={{
            background:
              'linear-gradient(180deg, rgba(240,240,255,0.35) 0%, rgba(136,112,255,0.18) 100%)',
            borderColor: 'var(--color-violet-200)',
          }}
          aria-hidden="true"
        >
          <Icon name={quote.iconName} size={16} color="var(--color-violet-600)" />
        </div>
        {/* 右侧标题 + 描述 */}
        <div className={USER_QUOTE_BODY}>
          <div className={USER_QUOTE_TITLE_ROW}>
            {quote.cornerIconName ? (
              <Icon
                name={quote.cornerIconName}
                size={12}
                color="var(--color-foreground-secondary)"
                aria-hidden="true"
              />
            ) : null}
            <span className={USER_QUOTE_TITLE}>{quote.title}</span>
          </div>
          {quote.description ? (
            <p className={USER_QUOTE_DESC}>{quote.description}</p>
          ) : null}
        </div>
      </div>
    </div>
  );
}

/* ── 任务进行中徽章 ── */
function TaskBadge({ taskBadge }) {
  return (
    <div
      className={TASK_BADGE}
      style={{
        backgroundColor: 'var(--color-indigo-50)',
        borderColor: 'var(--color-indigo-100)',
        color: 'var(--color-indigo-400)',
      }}
    >
      <Icon name="star-04-stroked" size="sm" color="var(--color-indigo-400)" aria-hidden="true" />
      <span>{taskBadge}</span>
    </div>
  );
}

/* ── 用户气泡富文本 token 渲染 ── */
function UserMessageContent({ tokens }) {
  return (
    <div className={USER_BUBBLE_CONTENT} data-tfds-component="ChatMessage.UserContent">
      {tokens.map((token) => {
        if (token.type === 'entity') {
          return (
            <span key={token.id} className={USER_ENTITY_TAG}>
              {token.icon ? (
                <span className="shrink-0 flex text-brand-500">
                  <Icon name={token.icon} size={14} color="currentColor" aria-hidden="true" />
                </span>
              ) : null}
              <span className="truncate max-w-[220px]">{token.label}</span>
              {token.showChevron ? (
                <Icon
                  name="chevron-down-stroked"
                  size="sm"
                  color="var(--color-brand-500)"
                  aria-hidden="true"
                />
              ) : null}
            </span>
          );
        }
        return (
          <span
            key={token.id}
            className={USER_BUBBLE_TEXT}
            style={{ color: 'var(--color-blueGrey-800)' }}
          >
            {token.value}
          </span>
        );
      })}
    </div>
  );
}

function getInitialFormValues(formItems = []) {
  return formItems.reduce((values, item) => {
    const key = item?.id || item?.name;
    if (!key) return values;
    if (item.value !== undefined) values[key] = item.value;
    else if (item.defaultValue !== undefined) values[key] = item.defaultValue;
    else if (item.defaultChecked !== undefined) values[key] = item.defaultChecked;
    else values[key] = '';
    return values;
  }, {});
}

function getFieldChangeValue(raw) {
  if (raw && typeof raw === 'object' && raw.target) {
    const { type, checked, value } = raw.target;
    return type === 'checkbox' ? checked : value;
  }
  return raw;
}

/* ── 人工确认节点 ── */
function HumanConfirmNode({ node, tokenStyles }) {
  /* 内部自管折叠态（兼容外部受控：node.collapsed + node.onToggleCollapsed 都传时走外部） */
  const isControlled = typeof node.collapsed === 'boolean' && typeof node.onToggleCollapsed === 'function';
  const isHistoryConfirmed = node.defaultConfirmed === true;
  const [internalCollapsed, setInternalCollapsed] = useState(
    node.collapsed === true || isHistoryConfirmed,
  );
  const isCollapsed = isControlled ? node.collapsed : internalCollapsed;
  const handleToggle = isControlled
    ? node.onToggleCollapsed
    : () => setInternalCollapsed((v) => !v);

  const [confirmed, setConfirmed] = useState(isHistoryConfirmed);
  const [selectedOptionValue, setSelectedOptionValue] = useState(node.defaultSelectedValue);
  const [formValues, setFormValues] = useState(() => node.formValues || getInitialFormValues(node.formItems));
  useEffect(() => {
    if (node.defaultConfirmed === true) {
      setConfirmed(true);
      if (!isControlled) setInternalCollapsed(true);
    }
  }, [node.defaultConfirmed, isControlled]);
  useEffect(() => {
    setSelectedOptionValue(node.defaultSelectedValue);
  }, [node.defaultSelectedValue]);
  useEffect(() => {
    setFormValues(node.formValues || getInitialFormValues(node.formItems));
  }, [node.formItems, node.formValues]);
  const handlePrimary = () => {
    const selectedOption = node.options.find((option) => String(option.value) === String(selectedOptionValue)) ?? null;
    if (typeof node.onPrimaryAction === 'function') {
      node.onPrimaryAction({
        value: selectedOptionValue,
        option: selectedOption,
        formValues,
      });
    }
    setConfirmed(true);
    if (!isControlled) setInternalCollapsed(true);
  };
  const handleSecondary = () => {
    if (typeof node.onSecondaryAction === 'function') node.onSecondaryAction();
  };
  const handleOptionChange = (nextValue) => {
    setSelectedOptionValue(nextValue);
    const selectedOption = node.options.find((option) => String(option.value) === String(nextValue)) ?? null;
    if (typeof node.onOptionChange === 'function') {
      node.onOptionChange(nextValue, selectedOption);
    }
  };
  const handleFormValueChange = (item, rawValue) => {
    const key = item?.id || item?.name;
    if (!key) return;
    const nextValue = getFieldChangeValue(rawValue);
    setFormValues((prev) => {
      const nextValues = { ...prev, [key]: nextValue };
      if (typeof node.onFormChange === 'function') {
        node.onFormChange(nextValues, {
          fieldId: key,
          value: nextValue,
          item,
        });
      }
      return nextValues;
    });
  };

  const showIntroText = node.mode === 'text-card' && node.introText && !isCollapsed;
  const showCardBody = !isCollapsed;
  const isOptionCard = node.mode === 'option-card';
  const isFormCard = node.mode === 'form-card';
  const resolvedFormItems = isFormCard
    ? node.formItems.map((item) => {
        const key = item?.id || item?.name;
        if (!key) return item;
        return {
          ...item,
          value: formValues[key],
          onChange: (nextValue, ...args) => {
            handleFormValueChange(item, nextValue);
            if (typeof item.onChange === 'function') item.onChange(nextValue, ...args);
          },
        };
      })
    : node.formItems;

  const headerClassName = isOptionCard ? HUMAN_CONFIRM_OPTION_HEADER : HUMAN_CONFIRM_HEADER;
  const iconWrapClassName = isOptionCard ? HUMAN_CONFIRM_OPTION_ICON_WRAP : HUMAN_CONFIRM_ICON_WRAP;
  const titleClassName = isOptionCard ? HUMAN_CONFIRM_OPTION_TITLE : HUMAN_CONFIRM_TITLE;
  const header = (
    <div className={headerClassName}>
      <div className={iconWrapClassName} style={tokenStyles.iconWrap} aria-hidden="true">
        <Icon name={node.iconName} size={16} color="var(--color-foreground-secondary)" aria-hidden="true" />
      </div>
      <p className={titleClassName} style={tokenStyles.title}>
        {node.title}
      </p>
      <button
        type="button"
        className={HUMAN_CONFIRM_TOGGLE}
        style={tokenStyles.muted}
        onClick={handleToggle}
        aria-label={isCollapsed ? '展开人工确认节点' : '收起人工确认节点'}
        aria-expanded={!isCollapsed}
        data-tfds-component="ChatMessage.ConfirmToggle"
      >
        <Icon
          name={isCollapsed ? 'chevron-down-stroked' : 'chevron-up-stroked'}
          size="sm"
          color="currentColor"
          aria-hidden="true"
        />
      </button>
    </div>
  );

  if (isCollapsed) {
    return (
      <div className={HUMAN_CONFIRM_COLLAPSED} style={tokenStyles.humanConfirmCard}>
        {header}
      </div>
    );
  }

  return (
    <div className={HUMAN_CONFIRM_STACK} data-tfds-component="ChatMessage.Confirm">
      {showIntroText ? (
        <p className={HUMAN_CONFIRM_INTRO} style={tokenStyles.title}>
          {node.introText}
        </p>
      ) : null}
      <div
        className={isOptionCard || isFormCard ? HUMAN_CONFIRM_OPTION_CARD : HUMAN_CONFIRM_CARD}
        style={tokenStyles.humanConfirmCard}
      >
        {header}
        {showCardBody ? (
          <div
            className={isOptionCard || isFormCard ? HUMAN_CONFIRM_OPTION_BODY : HUMAN_CONFIRM_BODY}
            style={tokenStyles.humanConfirmBody}
          >
            {isOptionCard ? (
              <RadioGroup
                variant="brand"
                styleType="pureCard"
                layout="vertical"
                value={selectedOptionValue}
                onChange={handleOptionChange}
                disabled={confirmed}
                className={HUMAN_CONFIRM_OPTION_GROUP}
                data-tfds-component="ChatMessage.OptionCardRadioGroup"
              >
                {node.options.map((option) => (
                  <Radio
                    key={String(option.value)}
                    value={option.value}
                    disabled={option.disabled}
                    className={HUMAN_CONFIRM_OPTION_RADIO}
                  >
                    {option.label}
                  </Radio>
                ))}
              </RadioGroup>
            ) : isFormCard ? (
              <Form
                items={resolvedFormItems}
                layout="vertical"
                size="md"
                disabled={confirmed}
                className="w-full min-w-0 gap-3 pb-2"
                onSubmit={(event) => event.preventDefault()}
                data-tfds-component="ChatMessage.FormCardForm"
              />
            ) : node.description ? (
              <p className={HUMAN_CONFIRM_DESCRIPTION} style={tokenStyles.title}>
                {node.description}
              </p>
            ) : null}
          </div>
        ) : null}
        <div className={isOptionCard || isFormCard ? HUMAN_CONFIRM_OPTION_ACTIONS : HUMAN_CONFIRM_ACTIONS}>
          {node.secondaryActionLabel ? (
            <Button
              variant="outline-black"
              size="md"
              onClick={handleSecondary}
              disabled={confirmed}
              data-tfds-component="ChatMessage.ConfirmAction"
            >
              {node.secondaryActionLabel}
            </Button>
          ) : null}
          <Button
            variant="primary"
            size="md"
            onClick={handlePrimary}
            disabled={confirmed || (isOptionCard && node.options.length === 0)}
            data-tfds-component="ChatMessage.ConfirmAction"
          >
            {node.primaryActionLabel}
          </Button>
        </div>
      </div>
    </div>
  );
}

/* ── 执行流·任务组 子组件 ──
 * 一个任务组 = 标题栏（status icon + title + chevron 整行可点击折叠）+ 子步骤列表
 * processing 状态下只有最后一个 step 的卡片显示扫光动效，前面的 step 视觉上视为已完成
 * 自动折叠规则（业界主流：流式期间不折叠，全部完成后统一折叠）：
 *   · 流式仍在进行（isStreamingActive=true）→ 所有已出现的组保持展开（避免滚动跳闪）
 *   · 流式结束（isStreamingActive=false）→ completed 组统一自动折叠
 *   · 用户手动点击切换后锁定，后续状态变化不再覆盖
 */
function ExecutionGroup({ group, tokenStyles, isStreamingActive = false }) {
  /* 默认展开条件：流式中 或 自身处于 processing；其余都默认折叠（含历史消息） */
  const autoExpanded = isStreamingActive || group.status === 'processing';
  const [internalExpanded, setInternalExpanded] = useState(autoExpanded);
  const userToggledRef = useRef(false);
  const handleToggle = useCallback(() => {
    userToggledRef.current = true;
    setInternalExpanded((v) => !v);
  }, []);
  /* 自动调整：用户没手动操作过才生效；按「流式信号 + 自身状态」联合决定 */
  useEffect(() => {
    if (userToggledRef.current) return;
    setInternalExpanded(isStreamingActive || group.status === 'processing');
  }, [group.status, isStreamingActive]);
  const expanded = internalExpanded;
  const steps = group.steps;
  const showSteps = expanded && steps.length > 0;
  const showHeader = Boolean(group.title);
  const railStyle = { ...tokenStyles.rail, bottom: '0px' };

  if (!showHeader && !showSteps) return null;

  return (
    <div className={BODY} data-tfds-component="ChatMessage.ExecutionGroup">
      {showSteps ? <div className={GLOBAL_RAIL} style={railStyle} aria-hidden="true" /> : null}

      {showHeader ? (
        <div
          className={HEADER}
          role={steps.length > 0 ? 'button' : undefined}
          tabIndex={steps.length > 0 ? 0 : undefined}
          aria-expanded={steps.length > 0 ? expanded : undefined}
          onClick={steps.length > 0 ? handleToggle : undefined}
          onKeyDown={steps.length > 0 ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleToggle(); } } : undefined}
          data-tfds-component="ChatMessage.ExecutionHeader"
        >
          <div className={HEADER_ICON_WRAP}>
            <StatusIndicator status={group.status} statusIconName={group.statusIconName} />
          </div>
          <p className={HEADER_TITLE}>
            {group.title}
          </p>
          {steps.length > 0 ? (
            <span className={TOGGLE} aria-hidden="true" style={tokenStyles.muted}>
              <Icon
                name="chevron-up-stroked"
                size="sm"
                color="var(--color-blueGrey-600)"
                className={!expanded ? 'rotate-180' : undefined}
              />
            </span>
          ) : null}
        </div>
      ) : null}

      {showSteps ? (
        <div className="flex w-full min-w-0 max-w-full flex-col items-start">
          {steps.map((step, idx) => {
            /* processing 组只有最后一条 step 显示扫光，前面的算已完成 */
            const isLastStep = idx === steps.length - 1;
            const isProcessingCard = group.status === 'processing' && isLastStep;
            const hasCard = Boolean(step.actionLabel || step.actionDetail);
            return (
              <div key={step.id} className={STEP_ROW}>
                <div className={STEP_RAIL_WRAP} aria-hidden="true" />
                <div className={STEP_BODY}>
                  <p className={STEP_TITLE} style={tokenStyles.title}>{step.title}</p>
                  {hasCard ? (
                    <div
                      className={ACTION_CARD}
                      style={tokenStyles.card}
                      data-tfds-component="ChatMessage.ExecutionAction"
                      {...(isProcessingCard ? { 'aria-busy': true } : {})}
                    >
                      <div className={ACTION_ICON_WRAP} style={tokenStyles.iconWrap}>
                        <Icon name={step.actionIconName} size={14} color="var(--color-blueGrey-600)" aria-hidden="true" />
                      </div>
                      <TruncationTooltipText
                        label={step.actionLabel}
                        detail={step.actionDetail}
                        labelStyle={tokenStyles.title}
                        detailStyle={tokenStyles.muted}
                        shimmer={isProcessingCard}
                      />
                    </div>
                  ) : null}
                </div>
              </div>
            );
          })}
        </div>
      ) : null}
    </div>
  );
}

export default function ChatMessage({
  /* 角色与基础 */
  role = 'ai',
  className = '',
  style,
  bubbleEnter = true,
  timestamp = '',

  /* AI 头像（原 aiHeader） */
  header = null,

  /* 深度思考（原 thinkingProcess） */
  thinking = null,

  /* 引导文案：常用于 plan/confirms 上方的"我开始规划啦..." */
  leadText = '',

  /* 卡片回复·任务规划（原 taskPlan） */
  plan = null,

  /* 执行流：标题 + 状态 + 步骤 + 任务组 */
  title = DEFAULT_CHAT_TITLE,
  status = 'completed',
  statusIconName = 'check-circle-stroked',
  steps = DEFAULT_CHAT_STEPS,
  taskGroups = null,

  /* 结果区：文本 + 产物卡 + 人工确认 */
  resultText = '',
  resultArtifact,
  resultArtifacts,
  confirms = null,

  /* 任务进行中徽章 */
  taskBadge = null,

  /* 追问（原 followUpQuestions） */
  followUps = null,

  /* 产物卡点击：打开右侧产物详情等 */
  onArtifactSelect = null,
  selectedArtifactId = null,
  onArtifactActionClick = null,

  /* 操作栏（原 messageActions） */
  actions = null,

  /* 用户气泡（role='user'）*/
  userContent = null,
  userAttachments = null,
  userQuote = null,
  userBubbleTone = 'auto',

  /* 折叠态兼容标记（实际折叠由子 ExecutionGroup 自管） */
  defaultExpanded = true,
  expanded,
  onExpandedChange,
  continuedBelow = false,
}) {
  /* defaultExpanded / expanded / onExpandedChange 现在仅作为旧 API 兼容标记，
     真正的折叠状态由每个 ExecutionGroup 子组件自管 */
  void expanded;
  void onExpandedChange;

  const bubbleEnterClass = bubbleEnter
    ? messageBubbleEnterClass(role === 'user' ? 'outgoing' : 'incoming')
    : '';
  const rootClassName = [ROOT, bubbleEnterClass, className].filter(Boolean).join(' ');

  const tokenStyles = {
    title: { color: 'var(--color-blueGrey-800)' },
    muted: { color: 'var(--color-blueGrey-600)' },
    rail: { backgroundColor: 'var(--color-border-default)' },
    card: {
      borderColor: 'var(--color-border-default)',
      backgroundColor: 'var(--color-fill)',
    },
    iconWrap: {
      borderColor: 'var(--color-border-default)',
      backgroundImage: 'var(--tfds-ai-execution-icon-gradient)',
    },
    humanConfirmCard: {
      borderColor: 'var(--color-border-default)',
      backgroundColor: 'var(--color-fill)',
    },
    humanConfirmBody: {
      borderColor: 'var(--color-white)',
      backgroundColor: 'var(--color-card-secondary)',
    },
  };

  const normalizedAiHeader = normalizeAiHeader(header);
  const normalizedThinking = normalizeThinkingProcess(thinking);
  const normalizedTaskPlan = normalizeTaskPlan(plan);
  const normalizedFollowUp = normalizeFollowUpQuestions(followUps);
  const normalizedMessageActions = normalizeMessageActions(actions);
  const normalizedUserMessage = normalizeUserMessage(userContent);
  const normalizedUserAttachments = normalizeUserAttachments(userAttachments);
  const normalizedUserQuote = normalizeUserQuote(userQuote);
  const rootRef = useRef(null);
  const [autoUserBubbleTone, setAutoUserBubbleTone] = useState(DEFAULT_USER_BUBBLE_TONE);
  useIsomorphicLayoutEffect(() => {
    if (userBubbleTone !== 'auto') return;
    const cleanupFns = [];
    const updateAutoTone = () => {
      const nextTone = resolveAutoUserBubbleTone(rootRef.current);
      setAutoUserBubbleTone((currentTone) => (currentTone === nextTone ? currentTone : nextTone));
    };
    const scheduleAutoToneUpdate = () => {
      updateAutoTone();
      if (typeof window === 'undefined') return;
      const rafId = window.requestAnimationFrame(updateAutoTone);
      const timeoutId = window.setTimeout(updateAutoTone, 240);
      cleanupFns.push(() => window.cancelAnimationFrame(rafId));
      cleanupFns.push(() => window.clearTimeout(timeoutId));
    };

    scheduleAutoToneUpdate();

    const ancestorNodes = getBackgroundAncestorNodes(rootRef.current);

    if (typeof MutationObserver !== 'undefined') {
      const observer = new MutationObserver(scheduleAutoToneUpdate);
      ancestorNodes.forEach((node) => {
        observer.observe(node, { attributes: true, attributeFilter: ['class', 'style'] });
      });
      cleanupFns.push(() => observer.disconnect());
    }

    ancestorNodes.forEach((node) => {
      const handleTransitionEnd = (event) => {
        if (!event.propertyName || event.propertyName.includes('background')) {
          updateAutoTone();
        }
      };
      node.addEventListener('transitionend', handleTransitionEnd);
      cleanupFns.push(() => node.removeEventListener('transitionend', handleTransitionEnd));
    });

    return () => cleanupFns.forEach((cleanup) => cleanup());
  }, [userBubbleTone]);
  const resolvedUserBubbleTone = userBubbleTone === 'auto'
    ? autoUserBubbleTone
    : Object.prototype.hasOwnProperty.call(USER_BUBBLE_TONE_CLASS, userBubbleTone)
      ? userBubbleTone
      : DEFAULT_USER_BUBBLE_TONE;

  /* 待用户处理的卡片（任务规划 / 配置表单）状态：未处理时不显示操作栏；
   * 历史消息（plan.defaultConfirmed=true 或 confirms[i].defaultConfirmed=true）
   * 视为已处理，初始就显示操作栏占位 */
  const initialCardActioned =
    Boolean(plan?.defaultConfirmed) ||
    (Array.isArray(confirms) && confirms.some((n) => n?.defaultConfirmed));
  const [cardActioned, setCardActioned] = useState(initialCardActioned);
  /* defaultConfirmed 后续切到 true 时同步内部 state（防止外部异步标记不生效） */
  useEffect(() => {
    if (initialCardActioned) setCardActioned(true);
  }, [initialCardActioned]);
  const wrapPrimaryAction = (originalFn) => (...args) => {
    if (typeof originalFn === 'function') originalFn(...args);
    setCardActioned(true);
  };
  /* 取消按钮也算作"已处理"：消息变为历史消息后操作栏占位需要保留 */
  const wrapSecondaryAction = (originalFn) => (...args) => {
    if (typeof originalFn === 'function') originalFn(...args);
    setCardActioned(true);
  };

  /* role='user' 分支：用户气泡（按 8% 容器宽度做左侧缩进，500px 容器=40px 缩进）*/
  if (role === 'user') {
    return (
      <div ref={rootRef} className={rootClassName} style={style} data-tfds-component="ChatMessage">
        <div className={USER_BUBBLE_WRAP} style={{ paddingLeft: '8%' }}>
          {/* 附件区：与气泡分离的独立行，右对齐，多个附件可换行 */}
          {normalizedUserAttachments ? (
            <UserAttachmentRow attachments={normalizedUserAttachments} />
          ) : null}
          {/* 紫色划词条 / 引用：位于附件下方、气泡上方 */}
          {normalizedUserQuote ? <UserQuote quote={normalizedUserQuote} /> : null}
          {/* 文本气泡：仅当有文本（含 entity 混排）时才渲染 */}
          {normalizedUserMessage ? (
            <div className={`${USER_BUBBLE} ${USER_BUBBLE_TONE_CLASS[resolvedUserBubbleTone]}`} data-tfds-component="ChatMessage.Bubble">
              <UserMessageContent tokens={normalizedUserMessage} />
            </div>
          ) : null}
          {normalizedMessageActions || timestamp ? (
            <MessageActions
              messageActions={
                normalizedMessageActions || {
                  showCopy: false,
                  showQuote: false,
                  showLike: false,
                  showDislike: false,
                  historyMode: false,
                }
              }
              timestamp={timestamp}
              layout="right"
              tokenStyles={tokenStyles}
            />
          ) : null}
        </div>
      </div>
    );
  }

  /* role='ai' 简化分支：requesting / thinking 状态只显示 AI 头像 + loading 占位 */
  if (status === 'requesting' || status === 'thinking') {
    return (
      <div className={rootClassName} style={style} data-tfds-component="ChatMessage">
        <div className="flex w-full min-w-0 flex-col items-start gap-2">
          {normalizedAiHeader ? <AIHeader aiHeader={normalizedAiHeader} /> : null}
          {status === 'requesting' ? (
            <RequestingDots />
          ) : (
            <div className="py-1">
              <ThinkingInProgressLabel>
                {normalizedThinking?.inProgressLabel || '深度思考中 ...'}
              </ThinkingInProgressLabel>
            </div>
          )}
        </div>
      </div>
    );
  }

  /* role='ai' 完整分支 */
  const normalizedTaskGroups = normalizeTaskGroups({
    taskGroups,
    title,
    status,
    statusIconName,
    defaultExpanded,
    steps,
  });
  const normalizedHumanConfirmNodes = normalizeHumanConfirmNodes(confirms);
  const normalizedResultArtifacts = normalizeResultArtifacts(resultArtifacts, resultArtifact);
  const hasHumanConfirmResult = normalizedHumanConfirmNodes.length > 0;
  const hasResultContent =
    Boolean(resultText) || normalizedResultArtifacts.length > 0 || hasHumanConfirmResult;

  /* 是否含待处理的交互卡片（任务规划 / 配置表单）：未处理状态下不显示操作栏 */
  const hasInteractiveCard = Boolean(normalizedTaskPlan) || hasHumanConfirmResult;
  const trackedTaskPlan = normalizedTaskPlan
    ? {
        ...normalizedTaskPlan,
        onPrimaryAction: wrapPrimaryAction(normalizedTaskPlan.onPrimaryAction),
        onSecondaryAction: wrapSecondaryAction(normalizedTaskPlan.onSecondaryAction),
      }
    : null;
  const trackedHumanConfirmNodes = normalizedHumanConfirmNodes.map((node) => ({
    ...node,
    onPrimaryAction: wrapPrimaryAction(node.onPrimaryAction),
    onSecondaryAction: wrapSecondaryAction(node.onSecondaryAction),
  }));
  const showMessageActions = Boolean(normalizedMessageActions) && (!hasInteractiveCard || cardActioned);

  return (
    <div className={rootClassName} style={style} data-tfds-component="ChatMessage">
      <div className="flex w-full min-w-0 flex-col items-start gap-2">
        {normalizedAiHeader ? <AIHeader aiHeader={normalizedAiHeader} /> : null}

        {normalizedThinking ? (
          <ThinkingProcess thinkingProcess={normalizedThinking} tokenStyles={tokenStyles} />
        ) : null}

        {leadText ? (
          <p className={RESULT_TEXT} style={tokenStyles.title}>
            {leadText}
          </p>
        ) : null}

        {trackedTaskPlan ? (
          <TaskPlanCard taskPlan={trackedTaskPlan} tokenStyles={tokenStyles} />
        ) : null}

        {(() => {
          /* 流式态判定：只要还有 group 处于 processing，就视为执行流仍在进行
           * 流式期间所有已出现的组保持展开（避免逐组折叠造成滚动跳闪）
           * 全部完成后再统一折叠（业界主流做法：Cursor / Claude Code / ChatGPT thinking） */
          const isStreamingActive = normalizedTaskGroups.some((g) => g.status === 'processing');
          return normalizedTaskGroups.map((group) => (
            <ExecutionGroup
              key={group.id}
              group={group}
              tokenStyles={tokenStyles}
              isStreamingActive={isStreamingActive}
            />
          ));
        })()}

        {hasResultContent ? (
          <div className={RESULT_ROW}>
            <div className={RESULT_NODE}>
              {resultText ? (
                <p className={RESULT_TEXT} style={tokenStyles.title}>
                  {resultText}
                </p>
              ) : null}

              {hasHumanConfirmResult ? (
                <div className={HUMAN_CONFIRM_SECTION}>
                  {trackedHumanConfirmNodes.map((node) => (
                    <HumanConfirmNode key={node.id} node={node} tokenStyles={tokenStyles} />
                  ))}
                </div>
              ) : null}

              {normalizedResultArtifacts.map((artifact) => (
                <ResultArtifactCard
                  key={artifact.id}
                  artifact={artifact}
                  onSelect={onArtifactSelect}
                  onActionClick={onArtifactActionClick}
                  selected={selectedArtifactId != null && selectedArtifactId === artifact.id}
                />
              ))}
            </div>
          </div>
        ) : null}

        {taskBadge ? <TaskBadge taskBadge={taskBadge} /> : null}

        {showMessageActions ? (
          <MessageActions
            messageActions={normalizedMessageActions}
            timestamp={timestamp}
            layout="left"
            tokenStyles={tokenStyles}
          />
        ) : null}

        {normalizedFollowUp ? (
          <FollowUpQuestions followUpQuestions={normalizedFollowUp} tokenStyles={tokenStyles} />
        ) : null}
      </div>
    </div>
  );
}

/* ────────────────────────────────────────────────────────────
 * useStreamingTaskGroups — 执行流·流式输出 hook（业务层调用）
 *
 * 把一份「最终态」taskGroups 按节奏逐步推出，模拟 AI 实时执行：
 *   1. 第 1 组首个 step 立即出现，组 status='processing'（带扫光）
 *   2. 每过 intervalMs，向当前组追加下一个 step
 *   3. 当前组所有 step 都出齐后，把该组改为 'completed'，
 *      下一 tick 推下一组首个 step（仍 processing）
 *   4. 全部推完 → 末组转 'completed'，停止计时器
 *
 * 注意：传入数据中 group.status 会被 hook 接管，调用方传啥都行（建议传完整数据）。
 *
 * @param {Array} fullTaskGroups — 完整任务组数据（含所有 group/steps）
 * @param {object} [options]
 * @param {number} [options.intervalMs=600] — 每条 step 出现的间隔（ms）
 * @param {boolean} [options.enabled=true] — 是否启用流式（false 直接返回完整数据）
 * @returns {Array} 当前可见的 taskGroups 切片（结构与入参一致，可直接传给 ChatMessage）
 *
 * @example
 *   function MyExecutionMessage({ taskGroups }) {
 *     const streamed = useStreamingTaskGroups(taskGroups, { intervalMs: 600 });
 *     return <ChatMessage header taskGroups={streamed} />;
 *   }
 */
export function useStreamingTaskGroups(fullTaskGroups, options = {}) {
  const { intervalMs = 600, enabled = true } = options;
  const groups = Array.isArray(fullTaskGroups) ? fullTaskGroups : [];
  const totalSteps = groups.reduce((sum, g) => sum + (g?.steps?.length ?? 0), 0);

  /* pointer：当前进度指针 [groupIdx, stepIdx]，finished 标记全部播放完毕 */
  const [pointer, setPointer] = useState({ groupIdx: 0, stepIdx: 0, finished: false });

  useEffect(() => {
    if (!enabled || groups.length === 0 || totalSteps === 0) {
      return undefined;
    }
    /* 重置：从第 1 组首个 step 开始 */
    setPointer({ groupIdx: 0, stepIdx: 0, finished: false });

    const timer = setInterval(() => {
      setPointer((prev) => {
        if (prev.finished) return prev;
        const { groupIdx, stepIdx } = prev;
        const stepCount = groups[groupIdx]?.steps?.length ?? 0;
        /* 当前组还有下一个 step → 步进 */
        if (stepIdx + 1 < stepCount) {
          return { groupIdx, stepIdx: stepIdx + 1, finished: false };
        }
        /* 当前组完成 → 切到下一组的第 1 个 step */
        if (groupIdx + 1 < groups.length) {
          return { groupIdx: groupIdx + 1, stepIdx: 0, finished: false };
        }
        /* 已是最后一组的最后一步 → 标记完成（最后一组也会被切为 completed）*/
        return { groupIdx, stepIdx, finished: true };
      });
    }, intervalMs);

    return () => clearInterval(timer);
    /* eslint-disable-next-line react-hooks/exhaustive-deps */
  }, [enabled, intervalMs, totalSteps]);

  /* 关闭流式 → 直接返回原数组（不裁剪、不接管 status） */
  if (!enabled) return groups;
  if (groups.length === 0) return groups;

  const { groupIdx, stepIdx, finished } = pointer;

  /* 输出：已完成组完整保留 / 当前组裁切 + processing / 未到达组隐藏 */
  return groups
    .map((g, i) => {
      if (i < groupIdx) {
        return { ...g, status: 'completed' };
      }
      if (i === groupIdx) {
        const visibleSteps = (g.steps ?? []).slice(0, stepIdx + 1);
        return {
          ...g,
          status: finished ? 'completed' : 'processing',
          steps: visibleSteps,
        };
      }
      return null;
    })
    .filter(Boolean);
}
