/* ============================================================
 * HECIAN · AI Pet 桌面宠物组件 v0.2 (demo)
 *
 * 设计目标：作为 ai-chip.jsx 的并行 AI 入口，与 Sidebar 上的 chip 共存。
 *   - ai-chip：严肃业务查询入口（Sidebar 上的紫色按钮 + 弹出大面板）
 *   - ai-pet ：轻量陪伴 + 主动提醒（右下角悬浮，气泡 + 小面板）
 *
 * 架构：
 *   - 悬浮容器 (fixed 右下角，可拖动)
 *   - 角色层 (sprite 切换 / emoji 占位 + CSS 头顶光环动画 overlay)
 *   - 气泡层 (短回复) / 面板层 (长回复 + 历史 + 输入 + markdown + 折叠)
 *   - 状态机：idle / think / speak / happy / error
 *   - 网络：MOCK_MODE=true 走假流式，false 走真 SSE
 *
 * 迁移到 Hecian 项目时（与 ai-chip 共存方案）：
 *   1. 拷贝整个文件到 public/prototypes/.../shared/ai-pet.jsx
 *   2. 拷贝 sprite 文件夹到 public/prototypes/.../shared/pet-sprites/
 *      并改本文件顶部 SPRITES_PATH 为 './pet-sprites'
 *   3. 把 MOCK_MODE 改为 false
 *   4. 在 chrome.jsx 或 workbench-*.html 里追加一行（不要删 ai-chip）：
 *        <script type="text/babel" data-presets="react" src=".../shared/ai-pet.jsx?v=..."></script>
 *   5. 两者会自动共存，互不干扰（ai-chip 在 Sidebar，pet 在右下角悬浮）
 *
 * 暴露：window.HX_AIPet = { mount, unmount }
 * ============================================================ */

(function() {
  if (typeof React === 'undefined') {
    console.warn('[HX_AIPet] React 未加载');
    return;
  }

  // ============================================================
  // 全局配置
  // ============================================================
  const PET_NAME = '小绚';          // 宠物名字（取自和绚的"绚"字）
  const USE_SPRITES = true;        // false 强制用 emoji 占位
  // ⚠ 2026-05-22 Hecian 接入 (HANDOVER §2 step 2): demo→生产 切真 LLM
  const MOCK_MODE = false;         // false = 真接 /api/ai-copilot/stream
  // AI 端点单源(2026-07-01):默认走哪条路由后端 /api/ai-copilot/config(同一个 AGENT_SERVICE_* env 单源):
  //   服务机(配了 env)→ 新底座 stream-via-agent · Vercel(未配)→ 老路 stream · 不再靠改本机文件。
  //   localStorage.hx_ai_via_agent 可临时覆盖('1' 强制新 / '0' 强制老)。ai-chip.jsx 同源共用此解析器 + 缓存。
  if (typeof window !== 'undefined' && !window.HX_resolveAiEndpoint) {
    window.HX_resolveAiEndpoint = function () {
      try {
        var ls = localStorage.getItem('hx_ai_via_agent');
        if (ls === '1') return Promise.resolve('/api/ai-copilot/stream-via-agent');
        if (ls === '0') return Promise.resolve('/api/ai-copilot/stream');
      } catch (e) {}
      if (!window.__hxAiEndpointPromise) {
        window.__hxAiEndpointPromise = fetch('/api/ai-copilot/config')
          .then(function (r) { return r.ok ? r.json() : { viaAgent: false }; })
          .then(function (c) { return (c && c.viaAgent) ? '/api/ai-copilot/stream-via-agent' : '/api/ai-copilot/stream'; })
          .catch(function () { return '/api/ai-copilot/stream'; });
      }
      return window.__hxAiEndpointPromise;
    };
  }
  const PET_SIZE = 96;
  // ⚠ 2026-05-22 Hecian 接入: sprite 路径相对 design-refs/*/index.html(跟 ai-chip 同位置)
  //   不再有 assets/ 层级 · sprite 直接放 shared/pet-sprites/
  const SPRITES_PATH = '../../prototypes/20260426-1530-平台原型-完整版/shared/pet-sprites';
  const BRAND_RED = '#B91C2C';
  const BRAND_RED_SOFT = '#FEF2F2';

  // 状态对应的 sprite 文件 / emoji 占位
  const STATES = {
    idle:  { file: 'idle.png',  emoji: '🤖', hint: '待机' },
    think: { file: 'think.png', emoji: '🤔', hint: '思考中' },
    speak: { file: 'speak.png', emoji: '💬', hint: '回复中' },
    happy: { file: 'happy.png', emoji: '🎉', hint: '完成' },
    error: { file: 'idle.png',  emoji: '😵', hint: '出错' },
  };

  // ============================================================
  // 0a. 自动加载 marked.js CDN（bot 消息 markdown 渲染）
  //     失败 → 降级 plain text，不破坏功能
  //     与 Hecian ai-chip.jsx 同源方案，避免重复加载
  // ============================================================
  (function autoLoadMarked() {
    if (window.marked || document.getElementById('hx-marked-loader')) return;
    const s = document.createElement('script');
    s.id = 'hx-marked-loader';
    s.src = 'https://cdn.jsdelivr.net/npm/marked@13/marked.min.js';
    s.async = true;
    s.onerror = () => console.warn('[HX_AIPet] marked.js 加载失败，降级 plain text');
    document.head.appendChild(s);
  })();

  // ============================================================
  // 0b. 注入全局样式
  // ============================================================
  (function injectStyles() {
    if (document.getElementById('hx-aipet-styles')) return;
    const css = `
      .hx-pet-root {
        position: fixed;
        z-index: 99999;
        user-select: none;
        font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
      }
      .hx-pet-char {
        width: ${PET_SIZE}px;
        height: ${PET_SIZE}px;
        cursor: grab;
        position: relative;
        transition: transform 0.2s ease;
        filter: drop-shadow(0 4px 8px rgba(0,0,0,0.15));
      }
      .hx-pet-char:hover {
        transform: scale(1.08) translateY(-2px);
      }
      .hx-pet-char:active {
        cursor: grabbing;
        transform: scale(1.05);
      }
      .hx-pet-img {
        width: 100%; height: 100%;
        image-rendering: pixelated;
        pointer-events: none;
        position: relative;
        z-index: 1;
      }
      /* CSS 头顶光环 overlay - 扇形旋转光晕，叠在 sprite 外圈 */
      .hx-pet-halo {
        position: absolute;
        top: -10%; left: -10%;
        width: 120%; height: 120%;
        border-radius: 50%;
        pointer-events: none;
        z-index: 0;
        background: conic-gradient(
          from 0deg,
          rgba(185, 28, 44, 0)   0deg,
          rgba(185, 28, 44, 0.55) 30deg,
          rgba(185, 28, 44, 0)   80deg,
          rgba(185, 28, 44, 0)   150deg,
          rgba(185, 28, 44, 0.55) 180deg,
          rgba(185, 28, 44, 0)   230deg,
          rgba(185, 28, 44, 0)   330deg,
          rgba(185, 28, 44, 0.3) 360deg
        );
        filter: blur(6px);
        opacity: 0.55;
        --halo-scale: 1;
        animation: hx-halo-spin 25s linear infinite;
      }
      @keyframes hx-halo-spin {
        from { transform: rotate(0deg)   scale(var(--halo-scale, 1)); }
        to   { transform: rotate(360deg) scale(var(--halo-scale, 1)); }
      }
      @keyframes hx-halo-pulse {
        0%, 100% { filter: blur(6px) brightness(1);   opacity: 0.55; }
        50%      { filter: blur(11px) brightness(1.6); opacity: 0.95; }
      }
      /* 状态联动 */
      .hx-pet-char[data-state="idle"] .hx-pet-halo {
        animation-duration: 25s;
        opacity: 0.5;
      }
      .hx-pet-char[data-state="think"] .hx-pet-halo {
        animation-duration: 2.5s;
        --halo-scale: 0.82;
        opacity: 0.75;
        filter: blur(4px) brightness(1.3);
      }
      .hx-pet-char[data-state="speak"] .hx-pet-halo {
        animation: hx-halo-spin 8s linear infinite, hx-halo-pulse 0.75s ease-in-out infinite;
      }
      .hx-pet-char[data-state="happy"] .hx-pet-halo {
        animation-duration: 1.2s;
        --halo-scale: 1.3;
        opacity: 0.95;
        filter: blur(9px) brightness(1.7);
      }
      .hx-pet-char[data-state="error"] .hx-pet-halo {
        animation-duration: 60s;
        opacity: 0.25;
        filter: blur(8px) grayscale(0.8);
      }
      .hx-pet-emoji {
        width: 100%; height: 100%;
        display: flex; align-items: center; justify-content: center;
        font-size: ${PET_SIZE * 0.7}px;
        background: radial-gradient(circle, #fff 0%, ${BRAND_RED_SOFT} 100%);
        border-radius: 50%;
        border: 2px solid ${BRAND_RED};
        box-shadow: inset 0 -4px 0 rgba(0,0,0,0.05);
        position: relative;
        z-index: 1;
      }
      .hx-pet-bubble {
        position: absolute;
        bottom: 100%;
        left: 50%;
        transform: translateX(-50%) translateY(-8px);
        min-width: 120px;
        max-width: 260px;
        padding: 8px 12px;
        background: #fff;
        border: 1.5px solid ${BRAND_RED};
        border-radius: 12px;
        font-size: 13px;
        line-height: 1.5;
        color: #2d2d2d;
        box-shadow: 0 4px 12px rgba(0,0,0,0.1);
        white-space: pre-wrap;
        word-break: break-word;
        animation: hx-pet-bubble-in 0.25s ease;
      }
      .hx-pet-bubble::after {
        content: '';
        position: absolute;
        top: 100%;
        left: 50%;
        transform: translateX(-50%);
        border: 6px solid transparent;
        border-top-color: ${BRAND_RED};
      }
      .hx-pet-bubble::before {
        content: '';
        position: absolute;
        top: 100%;
        left: 50%;
        transform: translateX(-50%) translateY(-2px);
        border: 5px solid transparent;
        border-top-color: #fff;
        z-index: 1;
      }
      @keyframes hx-pet-bubble-in {
        from { opacity: 0; transform: translateX(-50%) translateY(0); }
        to   { opacity: 1; transform: translateX(-50%) translateY(-8px); }
      }
      .hx-pet-state-badge {
        position: absolute;
        top: -4px;
        right: -4px;
        font-size: 10px;
        background: ${BRAND_RED};
        color: #fff;
        padding: 2px 6px;
        border-radius: 10px;
        font-weight: 500;
      }
      .hx-pet-panel {
        position: absolute;
        bottom: 100%;
        right: 0;
        margin-bottom: 12px;
        width: 380px;
        max-height: 480px;
        background: #fff;
        border: 1.5px solid ${BRAND_RED};
        border-radius: 12px;
        box-shadow: 0 8px 24px rgba(0,0,0,0.15);
        display: flex;
        flex-direction: column;
        overflow: hidden;
        animation: hx-pet-panel-in 0.25s ease;
      }
      @keyframes hx-pet-panel-in {
        from { opacity: 0; transform: translateY(8px); }
        to   { opacity: 1; transform: translateY(0); }
      }
      .hx-pet-panel-header {
        padding: 10px 14px;
        background: linear-gradient(135deg, ${BRAND_RED_SOFT} 0%, #fff 100%);
        border-bottom: 1px solid #f0e8e8;
        display: flex;
        align-items: center;
        gap: 8px;
        font-weight: 600;
        font-size: 13px;
        color: ${BRAND_RED};
      }
      .hx-pet-panel-close {
        margin-left: auto;
        cursor: pointer;
        border: none;
        background: transparent;
        font-size: 18px;
        color: #888;
        padding: 0 4px;
      }
      .hx-pet-panel-close:hover { color: #2d2d2d; }
      .hx-pet-msgs {
        flex: 1;
        overflow-y: auto;
        padding: 12px;
        display: flex;
        flex-direction: column;
        gap: 10px;
      }
      .hx-pet-msg {
        max-width: 85%;
        padding: 8px 12px;
        border-radius: 10px;
        font-size: 13px;
        line-height: 1.5;
        white-space: pre-wrap;
        word-break: break-word;
      }
      .hx-pet-msg-user {
        align-self: flex-end;
        background: ${BRAND_RED};
        color: #fff;
      }
      .hx-pet-msg-bot {
        align-self: flex-start;
        background: #f4f4f5;
        color: #2d2d2d;
      }
      .hx-pet-msg-tool {
        align-self: flex-start;
        background: #fffbeb;
        border: 1px dashed #fbbf24;
        color: #92400e;
        font-size: 12px;
        font-family: monospace;
      }
      .hx-pet-input-row {
        display: flex;
        padding: 10px;
        border-top: 1px solid #f0e8e8;
        gap: 8px;
      }
      .hx-pet-input {
        flex: 1;
        padding: 8px 10px;
        border: 1px solid #ddd;
        border-radius: 8px;
        font-size: 13px;
        outline: none;
        font-family: inherit;
      }
      .hx-pet-input:focus { border-color: ${BRAND_RED}; }
      .hx-pet-send {
        padding: 8px 14px;
        background: ${BRAND_RED};
        color: #fff;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 13px;
        font-weight: 500;
      }
      .hx-pet-send:hover { opacity: 0.9; }
      .hx-pet-send:disabled { opacity: 0.5; cursor: not-allowed; }

      /* Markdown 渲染样式（复用 Hecian ai-chip 方案） */
      .ai-md-content p { margin: 4px 0; }
      .ai-md-content ul, .ai-md-content ol { margin: 4px 0; padding-left: 18px; }
      .ai-md-content li { margin: 2px 0; }
      .ai-md-content h1,
      .ai-md-content h2,
      .ai-md-content h3 { font-size: 13px; font-weight: 600; margin: 6px 0 3px; color: #2d2d2d; }
      .ai-md-content code {
        background: rgba(0,0,0,.06);
        padding: 1px 4px;
        border-radius: 3px;
        font-size: 12px;
        font-family: 'JetBrains Mono', monospace;
      }
      .ai-md-content pre {
        background: rgba(0,0,0,.06);
        padding: 6px 8px;
        border-radius: 4px;
        overflow-x: auto;
        margin: 4px 0;
      }
      .ai-md-content pre code { background: none; padding: 0; }
      .ai-md-content strong { font-weight: 600; }
      .ai-md-content em { font-style: italic; }
      .ai-md-content blockquote {
        border-left: 3px solid ${BRAND_RED};
        padding-left: 8px;
        margin: 4px 0;
        color: #6b7280;
      }
      .ai-md-content a { color: ${BRAND_RED}; text-decoration: underline; }
      .ai-md-content hr { border: none; border-top: 1px dashed #e5e7eb; margin: 8px 0; }
      .ai-md-content table { border-collapse: collapse; margin: 6px 0; font-size: 12px; }
      .ai-md-content th, .ai-md-content td {
        border: 1px solid #e5e7eb;
        padding: 3px 8px;
        text-align: left;
      }
      .ai-md-content th { background: #f9fafb; font-weight: 600; }

      /* 折叠按钮 */
      .hx-pet-fold-btn {
        display: block;
        margin-top: 6px;
        padding: 3px 10px;
        font-size: 11px;
        background: transparent;
        border: 1px solid ${BRAND_RED};
        color: ${BRAND_RED};
        border-radius: 12px;
        cursor: pointer;
        font-family: inherit;
      }
      .hx-pet-fold-btn:hover { background: ${BRAND_RED_SOFT}; }
    `;
    const st = document.createElement('style');
    st.id = 'hx-aipet-styles';
    st.textContent = css;
    document.head.appendChild(st);
  })();

  // ============================================================
  // 1. SpriteImage 组件 - 带 emoji fallback
  // ============================================================
  function SpriteImage({ state }) {
    const conf = STATES[state] || STATES.idle;
    const [imgFailed, setImgFailed] = React.useState(false);

    if (!USE_SPRITES || imgFailed) {
      return React.createElement('div', { className: 'hx-pet-emoji' }, conf.emoji);
    }

    return React.createElement('img', {
      className: 'hx-pet-img',
      src: `${SPRITES_PATH}/${conf.file}`,
      alt: conf.hint,
      onError: () => setImgFailed(true),
      draggable: false,
    });
  }

  // ============================================================
  // 1b. BotMessage 组件 - markdown 渲染 + 长回复折叠
  // ============================================================
  const FOLD_THRESHOLD = 200; // 超过 N 字自动折叠

  function escapeHtml(s) {
    return String(s).replace(/[&<>"']/g, (c) => ({
      '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
    }[c]));
  }

  function renderMarkdown(text) {
    if (window.marked && typeof window.marked.parse === 'function') {
      try {
        return window.marked.parse(text, { breaks: true, gfm: true });
      } catch (e) {
        console.warn('[HX_AIPet] marked parse failed', e);
      }
    }
    // 降级：纯文本，保留换行
    return '<p>' + escapeHtml(text).replace(/\n/g, '<br/>') + '</p>';
  }

  function BotMessage({ content, streaming }) {
    const [expanded, setExpanded] = React.useState(false);
    const isLong = content.length > FOLD_THRESHOLD;
    // 流式输出中不折叠（避免内容跳动），完成后才折叠
    const shouldFold = isLong && !streaming && !expanded;
    const shown = shouldFold ? content.slice(0, FOLD_THRESHOLD) + '...' : content;

    return React.createElement(
      'div',
      { className: 'hx-pet-msg hx-pet-msg-bot' },
      React.createElement('div', {
        className: 'ai-md-content',
        dangerouslySetInnerHTML: { __html: renderMarkdown(shown) },
      }),
      isLong && !streaming && React.createElement(
        'button',
        {
          className: 'hx-pet-fold-btn',
          onClick: () => setExpanded(!expanded),
        },
        expanded ? '收起 ▲' : `展开全文 (${content.length} 字) ▼`
      )
    );
  }

  // ============================================================
  // 2. Mock SSE 流式响应（demo 阶段用）
  //    模拟 Hecian /api/ai-copilot/stream 的事件序列：
  //    start → tool_call → tool_result → chunk × N → end
  // ============================================================
  async function mockStream(query, onEvent) {
    const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

    onEvent('start', { ts: new Date().toISOString() });
    await sleep(300);

    // 模拟一次 tool call（让用户看到 think 状态）
    const needsTool = /谁|什么|怎么|哪|多少|查|数据|项目|客户|员工/.test(query);
    if (needsTool) {
      onEvent('tool_call', { name: 'list_employees', args: {}, round: 0 });
      await sleep(800);
      onEvent('tool_result', {
        name: 'list_employees',
        result: { count: 22, sample: ['张治华', 'Will Lian', 'Vea Xu'] },
        round: 0,
      });
      await sleep(400);
    }

    // 模拟流式回复
    const wantsLong = /介绍|详细|说明|讲讲|科普|markdown|长/.test(query);
    let reply;
    if (wantsLong) {
      reply = `好的，我**介绍**一下「${query}」的相关内容：

### 📌 关于HECIAN

和绚是一家**中大型装饰总包公司**，主要服务于：

- 互联网公司总部装饰
- 酒店改造工程
- 商业综合体精装
- 办公楼室内设计 + 施工

### 🏗️ 核心战略

1. **工厂预制化** —— 把工地变工厂，降低现场工艺风险
2. **BIM 数字化** —— 全流程 BIM 协同
3. **VDC 虚拟建造** —— 施工前的虚拟预演

### 📊 10 大业务模块

| 编号 | 模块 | 简介 |
|---|---|---|
| M01 | CRM | 客户关系管理 |
| M02 | 成本 | 项目成本测算 |
| M03 | 项目 | 项目全生命周期 |
| M04 | 采购 | 采购 + 分包 |
| M05 | 工作台 | 个人 / 团队工作面 |
| M06 | 数据库 | 知识库 + 复盘 |
| M07 | 决策 | BD + 投标决策 |
| M08 | 门户 | 客户 / 供应商门户 |
| M09 | 财务 | 财务 + 资金 |
| M10 | 人力 | 22 人花名册 |

### 💡 关于这个 AI 宠物

我是 **ai-pet**，和 Sidebar 上的 ai-chip 是同事，共用同一套 DeepSeek + MCP tools 后端。

> 当前是 **Demo 模式**，所有回复都是模拟的。接入 Hecian 项目后就能真聊业务了 ✨

\`\`\`js
// 切换到生产模式只需改一行
const MOCK_MODE = false;
\`\`\`

需要别的信息可以继续问～`;
    } else {
      const replies = [
        `你问的是「${query}」对吧？这是 **demo 模式**的假回复 —— 接入 Hecian 后会走真实的 \`DeepSeek\` + MCP tool calling。`,
        `收到「${query}」。当前是离线 demo 状态。把 \`MOCK_MODE\` 改成 \`false\` 部署到 Hecian 就能真聊了。`,
        `「${query}」—— 这只是占位回复 ✨ 我能感知到你的提问，业务能力要等接后端。`,
      ];
      reply = replies[Math.floor(Math.random() * replies.length)];
    }

    for (let i = 0; i < reply.length; i += 3) {
      const chunk = reply.slice(i, i + 3);
      onEvent('chunk', { text: chunk });
      await sleep(35);
    }

    onEvent('end', { full: reply, tokens: reply.length, totalMs: 0 });
  }

  // ============================================================
  // 3. 真 SSE 流式（接 Hecian 时用）
  // ============================================================
  async function realStream(query, history, context, onEvent) {
    // ⚠ 2026-05-22 Hecian 接入 (HANDOVER §7 #1):必传 Bearer token 走鉴权
    //   (跟 ai-chip.jsx L262-274 同款 · prototypes/index.html supabase-js UMD 把
    //    session 存 localStorage 不写 cookies · ai-copilot/stream 从 header 读 token)
    const headers = window.HX_authHeaders({ 'Content-Type': 'application/json' });

    const resp = await fetch(await window.HX_resolveAiEndpoint(), {
      method: 'POST',
      headers: headers,
      body: JSON.stringify({ query, history, context }),
    });
    if (!resp.ok || !resp.body) {
      // 尝试读 error body · 给用户清晰提示
      let errMsg = `HTTP ${resp.status}`;
      try {
        const errBody = await resp.json();
        if (errBody && errBody.error) errMsg = errBody.error;
      } catch (e) {}
      throw new Error(errMsg);
    }
    const reader = resp.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      const parts = buffer.split(/\r?\n\r?\n/);
      buffer = parts.pop() || '';
      for (const block of parts) {
        const lines = block.split(/\r?\n/);
        let event = 'message';
        let data = '';
        for (const ln of lines) {
          if (ln.startsWith('event:')) event = ln.slice(6).trim();
          else if (ln.startsWith('data:')) data += ln.slice(5).trim();
        }
        if (data) {
          try { onEvent(event, JSON.parse(data)); }
          catch { onEvent(event, data); }
        }
      }
    }
  }

  // ============================================================
  // 4. AIPet 主组件
  // ============================================================
  function AIPet() {
    // ⚠ 2026-05-22 jarvi 反馈:默认在右下角撞 nav-hud bar(.nav-hud 在 right:16 bottom:16)
    //   把小绚放在 nav-hud bar 的左侧 · 跟 bar 底部对齐 · 不挡 bar 点击
    //   x: window.innerWidth - 540(估测 admin 7-chip bar ~440 + gap 8 + pet 96 = 544)
    //   y: window.innerHeight - 110(bar bottom 16 + pet height 96 + 视觉融合 -2)
    //   不同 role bar 宽度不同(bd 5-chip 短 · admin 7-chip 长)· 取 admin 兜底
    const [pos, setPos] = React.useState({
      x: window.innerWidth - 540,
      y: window.innerHeight - 110,
    });
    const [state, setState] = React.useState('idle');
    const [bubble, setBubble] = React.useState(`你好呀，我是${PET_NAME}！点我聊聊~`);
    const [panelOpen, setPanelOpen] = React.useState(false);
    const [messages, setMessages] = React.useState([]);  // {role, content, kind?}
    const [input, setInput] = React.useState('');
    const [sending, setSending] = React.useState(false);

    const dragRef = React.useRef({ dragging: false, sx: 0, sy: 0, px: 0, py: 0, moved: false });
    const msgsEndRef = React.useRef(null);

    // 启动时打招呼后 4 秒自动收起气泡
    React.useEffect(() => {
      const t = setTimeout(() => setBubble(null), 5000);
      return () => clearTimeout(t);
    }, []);

    // 新消息时自动滚到底部
    React.useEffect(() => {
      if (msgsEndRef.current) {
        msgsEndRef.current.scrollIntoView({ behavior: 'smooth' });
      }
    }, [messages]);

    // 拖动逻辑
    const onMouseDown = (e) => {
      dragRef.current = {
        dragging: true,
        sx: e.clientX, sy: e.clientY,
        px: pos.x, py: pos.y,
        moved: false,
      };
      e.preventDefault();
    };

    React.useEffect(() => {
      const onMove = (e) => {
        const d = dragRef.current;
        if (!d.dragging) return;
        const dx = e.clientX - d.sx;
        const dy = e.clientY - d.sy;
        if (Math.abs(dx) > 5 || Math.abs(dy) > 5) d.moved = true;
        setPos({
          x: Math.max(0, Math.min(window.innerWidth - PET_SIZE, d.px + dx)),
          y: Math.max(0, Math.min(window.innerHeight - PET_SIZE, d.py + dy)),
        });
      };
      const onUp = (e) => {
        const d = dragRef.current;
        if (d.dragging && !d.moved) {
          // 当做点击
          setPanelOpen((v) => !v);
          setBubble(null);
        }
        d.dragging = false;
      };
      window.addEventListener('mousemove', onMove);
      window.addEventListener('mouseup', onUp);
      return () => {
        window.removeEventListener('mousemove', onMove);
        window.removeEventListener('mouseup', onUp);
      };
    }, []);

    // Y 方案 2026-05-22 · ConfirmationCard 确认/取消 handlers(跟 ai-chip 同模式)
    const handleConfirmDraft = async (draftId, msgIndex) => {
      // mark submitting 防重复点击
      setMessages((ms) => ms.map((m, i) =>
        i === msgIndex && m.role === 'confirmation' ? { ...m, status: 'submitting' } : m
      ));

      // 拿 Bearer token(同 realStream)
      const headers = window.HX_authHeaders({ 'Content-Type': 'application/json' });

      try {
        const resp = await fetch('/api/ai-copilot/confirm-draft', {
          method: 'POST',
          headers,
          body: JSON.stringify({ draft_id: draftId }),
        });
        const data = await resp.json();
        if (resp.ok && data.success) {
          const result = data.result || {};
          const toolName = data.tool_name;
          const idShort = (result.customer_id || result.interaction_id || '').slice(0, 8);
          const actionLabel = toolName === 'create_customer' ? '已录入新客户'
            : toolName === 'update_customer_status' ? '已更新状态'
            : toolName === 'log_customer_interaction' ? '已记录跟进'
            : '已完成';
          const successText = `✅ ${actionLabel}${idShort ? ' (id: ' + idShort + '...)' : ''}`;
          setMessages((ms) => ms.map((m, i) =>
            i === msgIndex && m.role === 'confirmation' ? { ...m, status: 'confirmed' } : m
          ).concat([{ role: 'system', content: successText }]));
        } else {
          const errMsg = (data && (data.message || data.error)) || `HTTP ${resp.status}`;
          setMessages((ms) => ms.map((m, i) =>
            i === msgIndex && m.role === 'confirmation' ? { ...m, status: 'error' } : m
          ).concat([{ role: 'system', content: `❌ ${errMsg}` }]));
        }
      } catch (err) {
        setMessages((ms) => ms.map((m, i) =>
          i === msgIndex && m.role === 'confirmation' ? { ...m, status: 'error' } : m
        ).concat([{ role: 'system', content: `❌ 网络错误: ${String(err && err.message || err)}` }]));
      }
    };

    const handleCancelDraft = (draftId, msgIndex) => {
      setMessages((ms) => ms.map((m, i) =>
        i === msgIndex && m.role === 'confirmation' ? { ...m, status: 'cancelled' } : m
      ).concat([{ role: 'system', content: '🚫 已取消' }]));
    };

    // 发送消息
    const send = async () => {
      const q = input.trim();
      if (!q || sending) return;
      setInput('');
      setSending(true);
      setMessages((m) => [...m, { role: 'user', content: q }]);

      // 添加一个空的 bot 占位消息，流式时往里填
      let botIdx;
      setMessages((m) => {
        botIdx = m.length;
        return [...m, { role: 'bot', content: '' }];
      });

      let accumulated = '';
      const onEvent = (event, data) => {
        if (event === 'start') {
          setState('think');
        } else if (event === 'tool_call') {
          setState('think');
          setMessages((m) => [
            ...m,
            { role: 'tool', kind: 'call', content: `🔧 调用 ${data.name}(${JSON.stringify(data.args)})` },
          ]);
        } else if (event === 'tool_result') {
          setMessages((m) => [
            ...m,
            { role: 'tool', kind: 'result', content: `✅ ${data.name} → ${JSON.stringify(data.result).slice(0, 120)}` },
          ]);
          // Y 方案 2026-05-22 · 拦截 requires_confirmation tool_result · 注入 confirmation card 消息
          if (data.result && data.result.requires_confirmation === true && data.result.draft_id) {
            setMessages((m) => [
              ...m,
              {
                role: 'confirmation',
                tool_name: data.name,
                draft_id: data.result.draft_id,
                human_preview: data.result.human_preview || '请确认',
                status: 'pending', // pending / submitting / confirmed / cancelled / error
              },
            ]);
          }
        } else if (event === 'chunk') {
          if (state !== 'speak') setState('speak');
          accumulated += data.text;
          setMessages((m) => {
            const copy = [...m];
            // 找到最后一个 bot 消息更新（占位的那个，可能因为 tool 消息插入而位置变了）
            for (let i = copy.length - 1; i >= 0; i--) {
              if (copy[i].role === 'bot') {
                copy[i] = { ...copy[i], content: accumulated };
                break;
              }
            }
            return copy;
          });
          // 同时更新小气泡（只显示前 50 字）
          if (!panelOpen) {
            setBubble(accumulated.length > 50 ? accumulated.slice(0, 50) + '...' : accumulated);
          }
        } else if (event === 'end') {
          setState('happy');
          setTimeout(() => setState('idle'), 1500);
        } else if (event === 'error') {
          setState('error');
          setMessages((m) => [...m, { role: 'bot', content: `❌ ${data.message || '出错了'}` }]);
          setTimeout(() => setState('idle'), 2000);
        }
      };

      try {
        if (MOCK_MODE) {
          await mockStream(q, onEvent);
        } else {
          // ⚠ 2026-05-22 Hecian 接入 (HANDOVER §7 #2):注入页面上下文
          //   (跟 ai-chip.jsx 同款 · 让小绚也能感知当前在哪个页面)
          let pageCtx = {};
          try {
            if (window.HX_AI && typeof window.HX_AI.getCurrentContext === 'function') {
              pageCtx = window.HX_AI.getCurrentContext() || {};
            }
          } catch (e) { /* HX_AI 未加载 · 走空 ctx */ }
          await realStream(q, messages.slice(-8), pageCtx, onEvent);
        }
      } catch (e) {
        onEvent('error', { message: String(e?.message || e) });
      } finally {
        setSending(false);
      }
    };

    // ============================================================
    // 渲染
    // ============================================================
    return React.createElement(
      'div',
      {
        className: 'hx-pet-root',
        style: { left: pos.x + 'px', top: pos.y + 'px' },
      },
      // 气泡（panel 关闭时显示）
      bubble && !panelOpen && React.createElement(
        'div',
        { className: 'hx-pet-bubble' },
        bubble
      ),
      // 面板
      panelOpen && React.createElement(
        'div',
        { className: 'hx-pet-panel', onMouseDown: (e) => e.stopPropagation() },
        React.createElement(
          'div',
          { className: 'hx-pet-panel-header' },
          React.createElement('span', null, `✨ ${PET_NAME}`),
          React.createElement('span', { style: { fontSize: 11, color: '#888', fontWeight: 400 } },
            MOCK_MODE ? 'Demo 模式' : 'Live'),
          React.createElement(
            'button',
            { className: 'hx-pet-panel-close', onClick: () => setPanelOpen(false) },
            '×'
          )
        ),
        React.createElement(
          'div',
          { className: 'hx-pet-msgs' },
          messages.length === 0 && React.createElement(
            'div',
            { style: { color: '#999', fontSize: 12, textAlign: 'center', padding: 20 } },
            '试试问：「公司有多少员工？」或「写一段较长的 markdown 介绍和绚」'
          ),
          (() => {
            // 找到最后一条 bot 消息的 index，用于判断"是否流式中"
            let lastBotIdx = -1;
            for (let j = messages.length - 1; j >= 0; j--) {
              if (messages[j].role === 'bot') { lastBotIdx = j; break; }
            }
            return messages.map((m, i) => {
              if (m.role === 'bot') {
                return React.createElement(BotMessage, {
                  key: i,
                  content: m.content || (sending && i === lastBotIdx ? '...' : ''),
                  streaming: sending && i === lastBotIdx,
                });
              }
              // Y 方案 · confirmation card 渲染(对话流内嵌 · 非 modal)
              if (m.role === 'confirmation') {
                const s = m.status || 'pending';
                const isDone = s === 'confirmed' || s === 'cancelled' || s === 'error';
                const badge = s === 'confirmed' ? '✅ 已确认'
                  : s === 'cancelled' ? '🚫 已取消'
                  : s === 'error' ? '❌ 失败'
                  : s === 'submitting' ? '⏳ 提交中...'
                  : '📋 待确认';
                const badgeColor = s === 'confirmed' ? '#2f6b3a'
                  : (s === 'cancelled' || s === 'error') ? '#a52a2a'
                  : BRAND_RED;
                return React.createElement('div', {
                  key: i,
                  style: {
                    margin: '6px 0', padding: '10px 12px',
                    background: BRAND_RED_SOFT, border: '1px solid ' + BRAND_RED,
                    borderRadius: 8, fontSize: 12, lineHeight: 1.5,
                  }
                },
                  React.createElement('div', {
                    style: { fontSize: 10, fontWeight: 600, color: badgeColor, marginBottom: 4 }
                  }, badge),
                  React.createElement('div', {
                    style: { color: '#333', marginBottom: isDone ? 0 : 8, whiteSpace: 'pre-wrap' }
                  }, m.human_preview),
                  !isDone && React.createElement('div', {
                    style: { display: 'flex', gap: 6, justifyContent: 'flex-end' }
                  },
                    React.createElement('button', {
                      onClick: () => { if (s !== 'submitting') handleCancelDraft(m.draft_id, i); },
                      disabled: s === 'submitting',
                      style: {
                        padding: '4px 10px', borderRadius: 4, border: '1px solid #ccc',
                        background: '#fff', color: '#666', fontSize: 11,
                        cursor: s === 'submitting' ? 'not-allowed' : 'pointer',
                      }
                    }, '取消'),
                    React.createElement('button', {
                      onClick: () => { if (s !== 'submitting') handleConfirmDraft(m.draft_id, i); },
                      disabled: s === 'submitting',
                      style: {
                        padding: '4px 12px', borderRadius: 4, border: 'none',
                        background: BRAND_RED, color: '#fff', fontSize: 11, fontWeight: 600,
                        cursor: s === 'submitting' ? 'not-allowed' : 'pointer',
                      }
                    }, s === 'submitting' ? '⏳' : '✓ 确认')
                  )
                );
              }
              // system message (✅ 已录入 / 🚫 已取消 / ❌ 错误)
              if (m.role === 'system') {
                return React.createElement('div', {
                  key: i,
                  style: {
                    margin: '4px 0', padding: '6px 10px',
                    background: '#fafafa', border: '1px dashed #ddd',
                    borderRadius: 5, fontSize: 11, color: '#555',
                  }
                }, m.content);
              }
              return React.createElement(
                'div',
                {
                  key: i,
                  className: 'hx-pet-msg ' + (
                    m.role === 'user' ? 'hx-pet-msg-user' : 'hx-pet-msg-tool'
                  ),
                },
                m.content
              );
            });
          })(),
          React.createElement('div', { ref: msgsEndRef })
        ),
        React.createElement(
          'div',
          { className: 'hx-pet-input-row' },
          React.createElement('input', {
            className: 'hx-pet-input',
            type: 'text',
            placeholder: '问点什么...',
            value: input,
            onChange: (e) => setInput(e.target.value),
            onKeyDown: (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } },
            disabled: sending,
          }),
          React.createElement(
            'button',
            { className: 'hx-pet-send', onClick: send, disabled: sending || !input.trim() },
            sending ? '...' : '发送'
          )
        )
      ),
      // 角色本体
      React.createElement(
        'div',
        {
          className: 'hx-pet-char',
          'data-state': state,
          onMouseDown: onMouseDown,
          title: `${PET_NAME} · ${STATES[state].hint}（点击展开 / 拖动移动）`,
        },
        // 头顶 CSS 光环 overlay（叠在 sprite 静态光环外圈，随状态旋转/脉冲）
        React.createElement('div', { className: 'hx-pet-halo' }),
        React.createElement(SpriteImage, { state: state }),
        // 调试用：状态徽章（生产可去掉）
        state !== 'idle' && React.createElement(
          'div',
          { className: 'hx-pet-state-badge' },
          STATES[state].hint
        )
      )
    );
  }

  // ============================================================
  // 5. 挂载入口
  // ============================================================
  let rootEl = null;
  let reactRoot = null;

  function mount(opts = {}) {
    if (rootEl) return;
    rootEl = document.createElement('div');
    rootEl.id = 'hx-aipet-mount';
    document.body.appendChild(rootEl);

    if (ReactDOM.createRoot) {
      reactRoot = ReactDOM.createRoot(rootEl);
      reactRoot.render(React.createElement(AIPet));
    } else {
      ReactDOM.render(React.createElement(AIPet), rootEl);
    }
  }

  function unmount() {
    if (reactRoot) reactRoot.unmount();
    else if (rootEl) ReactDOM.unmountComponentAtNode(rootEl);
    if (rootEl) rootEl.remove();
    rootEl = null;
    reactRoot = null;
  }

  window.HX_AIPet = { mount, unmount, AIPet };

  // 自动挂载
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => mount());
  } else {
    mount();
  }
})();
