(() => {
  if (window.__tmAiAssistantLoaded) return;
  window.__tmAiAssistantLoaded = true;

  const pathname = window.location.pathname || '/';
  const body = document.body;
  const isAssistantEligiblePage = (targetBody) => {
    if (!targetBody) return false;
    if (targetBody.classList.contains('jr-admin-page') || targetBody.classList.contains('jr-admin-login-page')) return false;
    return targetBody.classList.contains('jr-public-page')
      || targetBody.classList.contains('public-search')
      || targetBody.classList.contains('jr-order-page')
      || targetBody.classList.contains('jr-ticket-board-page');
  };
  if (!isAssistantEligiblePage(body)) return;

  const pageMap = {
    '/': '首页',
    '/home.html': '首页',
    '/order': '线上预定',
    '/ticket-order.html': '线上预定',
    '/search': '车票查询',
    '/ticket-search.html': '车票查询',
    '/ticket-board.html': '车票详情',
    '/token': '订单凭证',
    '/token.html': '订单凭证',
    '/ic-card/order': 'IC 卡线上购卡',
    '/ic-card-order.html': 'IC 卡线上购卡',
    '/ic-card/search': 'IC 卡查询',
    '/ic-card-search.html': 'IC 卡查询'
  };

  const suggestionMap = {
    '首页': ['这个网站可以做什么？', '如何在线订票？', '凭证码有什么用？'],
    '线上预定': ['如何在线订票？', '订票后怎么兑票？', '车型和乘次数量怎么选？'],
    '车票查询': ['怎么查我的车票？', '票据状态怎么看？', '查不到票据怎么办？'],
    '车票详情': ['帮我解释当前票号。', '这张票现在还能使用吗？', '这些状态字段分别代表什么？'],
    '订单凭证': ['凭证码怎么使用？', '这个凭证现在能兑票吗？', '怎么确认是否已经兑票？'],
    'IC 卡线上购卡': ['怎么在线购买 IC 卡？', '购卡后怎么领卡？', '持卡人姓名有什么要求？'],
    'IC 卡查询': ['怎么查询 IC 卡余额？', '输入什么可以查到 IC 卡？', '卡片状态代表什么？']
  };

  const resolvePageName = () => {
    if (pageMap[pathname]) return pageMap[pathname];
    if (body.classList.contains('jr-ticket-board-page')) return '车票详情';
    if (body.classList.contains('jr-order-page')) return '线上预定';
    if (document.querySelector('#vCode, #vCodeTop, .jr-redeem-code-value')) return '订单凭证';
    if (document.querySelector('.jr-ticket-id, .jr-route-board')) return '车票详情';
    if (document.querySelector('#q, #searchBtn')) return '车票查询';
    return document.title || '当前页面';
  };

  const pageName = resolvePageName();
  const history = [];
  let sending = false;
  let lastContextSignature = '';

  const textOf = (selector) => {
    const el = document.querySelector(selector);
    return el ? String(el.textContent || '').trim() : '';
  };

  const valueOf = (selector) => {
    const el = document.querySelector(selector);
    return el ? String(el.value || '').trim() : '';
  };

  const compact = (value, maxLength = 120) => {
    const text = String(value || '').trim().replace(/\s+/g, ' ');
    if (!text) return '';
    return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
  };

  const toContextSignature = (context) => JSON.stringify(context || {});
  const escapeHtml = (value) => String(value || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');

  const renderInlineMarkdown = (text) => escapeHtml(text)
    .replace(/`([^`]+)`/g, '<code>$1</code>')
    .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
    .replace(/\*([^*]+)\*/g, '<em>$1</em>')
    .replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>');

  const markdownToHtml = (source) => {
    const normalized = String(source || '').replace(/\r\n/g, '\n').trim();
    if (!normalized) return '';

    const codeBlocks = [];
    const withPlaceholders = normalized.replace(/```([\w-]*)\n([\s\S]*?)```/g, (_, lang, code) => {
      const token = `__TM_CODE_BLOCK_${codeBlocks.length}__`;
      codeBlocks.push(
        `<pre class="tm-ai-pre"><code${lang ? ` class="language-${escapeHtml(lang)}"` : ''}>${escapeHtml(code.trim())}</code></pre>`
      );
      return token;
    });

    const blocks = withPlaceholders.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
    const html = blocks.map((block) => {
      if (/^__TM_CODE_BLOCK_\d+__$/.test(block)) {
        const index = Number(block.replace(/\D/g, ''));
        return codeBlocks[index] || '';
      }

      const lines = block.split('\n').map((line) => line.trimEnd());
      if (lines.every((line) => /^[-*]\s+/.test(line))) {
        return `<ul>${lines.map((line) => `<li>${renderInlineMarkdown(line.replace(/^[-*]\s+/, ''))}</li>`).join('')}</ul>`;
      }
      if (lines.every((line) => /^\d+\.\s+/.test(line))) {
        return `<ol>${lines.map((line) => `<li>${renderInlineMarkdown(line.replace(/^\d+\.\s+/, ''))}</li>`).join('')}</ol>`;
      }
      if (lines.length === 1 && /^###\s+/.test(lines[0])) return `<h3>${renderInlineMarkdown(lines[0].replace(/^###\s+/, ''))}</h3>`;
      if (lines.length === 1 && /^##\s+/.test(lines[0])) return `<h2>${renderInlineMarkdown(lines[0].replace(/^##\s+/, ''))}</h2>`;
      if (lines.length === 1 && /^#\s+/.test(lines[0])) return `<h1>${renderInlineMarkdown(lines[0].replace(/^#\s+/, ''))}</h1>`;
      return `<p>${lines.map((line) => renderInlineMarkdown(line)).join('<br />')}</p>`;
    }).join('');

    return html.replace(/__TM_CODE_BLOCK_(\d+)__/g, (_, index) => codeBlocks[Number(index)] || '');
  };

  const buildContextActions = (context) => {
    const actions = [];
    if (context.voucher_code) {
      actions.push({
        label: '解释当前凭证',
        prompt: '请结合当前页面识别到的凭证信息，解释这个凭证现在的状态、如何使用，以及我接下来应该做什么。'
      });
    }
    if (context.ticket_id) {
      actions.push({
        label: '解释当前票号',
        prompt: '请结合当前页面识别到的票号与车票信息，解释这张票当前状态、还能不能使用，以及各字段分别代表什么。'
      });
    }
    if (context.query_keyword && !context.ticket_id && !context.voucher_code) {
      actions.push({
        label: '解释当前检索',
        prompt: `我当前检索的是“${context.query_keyword}”，请告诉我应该如何判断它是票号、凭证码还是其他查询关键词。`
      });
    }
    return actions;
  };

  const collectPageContext = () => {
    const params = new URLSearchParams(window.location.search);
    const pathSegments = pathname.split('/').filter(Boolean);
    const trailingSegment = decodeURIComponent(pathSegments[pathSegments.length - 1] || '');
    const context = {
      page_name: pageName
    };

    const fromInput = valueOf('#from');
    const toInput = valueOf('#to');
    const trips = valueOf('#trips');
    const voucherCode = textOf('#vCode') || textOf('#vCodeTop') || textOf('.voucher-code');
    const ticketId = compact(textOf('.jr-ticket-id')) || compact(textOf('.jr-panel-headline .mono')) || compact(params.get('id'));
    const searchKeyword = valueOf('#q') || valueOf('#queryInput') || compact(params.get('q'));
    const status = textOf('#vStatusTop') || textOf('#vStatusTag') || textOf('.jr-status-pill');
    const startName = textOf('.vStartName') || textOf('.jr-route-board .jr-station-block:first-child .jr-station-title');
    const terminalName = textOf('.vTermName') || textOf('.jr-route-board .jr-station-block.is-end .jr-station-title');

    if (voucherCode) context.voucher_code = voucherCode;
    if (ticketId) context.ticket_id = ticketId.replace(/\s+<.*$/, '').trim();
    if (!context.ticket_id && body.classList.contains('jr-ticket-board-page') && trailingSegment && trailingSegment !== 'search' && trailingSegment !== 'ticket-board.html') {
      context.ticket_id = trailingSegment;
    }
    if (searchKeyword) context.query_keyword = searchKeyword;
    if (status) context.status = status;
    if (startName) context.start = startName;
    if (terminalName) context.terminal = terminalName;
    if (fromInput) context.selected_start = fromInput;
    if (toInput) context.selected_terminal = toInput;
    if (trips) context.trips = trips;

    if (pathname === '/order' || pathname === '/ticket-order.html') {
      const typeEl = document.querySelector('input[name="trainType"]:checked');
      const typeCardTitle = typeEl ? compact(typeEl.nextElementSibling?.querySelector('.type-title')?.textContent || typeEl.value) : '';
      if (typeCardTitle) context.train_type = typeCardTitle;
      const totalPrice = compact(textOf('.jr-total-amount'));
      if (totalPrice) context.price = totalPrice;
    }

    if (pathname === '/token' || pathname === '/token.html') {
      const rideDate = textOf('#vDateTop');
      const type = textOf('#vTypeTop');
      const price = textOf('#vPriceTop');
      const voucherHint = textOf('.jr-redeem-copy');
      if (rideDate) context.ride_date = rideDate;
      if (type) context.train_type = type;
      if (price) context.price = price;
      if (voucherHint) context.redeem_tip = compact(voucherHint, 180);
    }

    if (pathname === '/search' || pathname === '/ticket-search.html' || body.classList.contains('jr-ticket-board-page')) {
      const metaItems = Array.from(document.querySelectorAll('.jr-meta-item')).slice(0, 6);
      metaItems.forEach((item) => {
        const key = compact(item.querySelector('span')?.textContent || '', 40);
        const value = compact(item.querySelector('strong')?.textContent || '', 80);
        if (!key || !value) return;
        if (key.includes('车型')) context.train_type = value;
        if (key.includes('票价')) context.price = value;
        if (key.includes('乘次')) context.trips_summary = value;
        if (key.includes('更新')) context.last_update = value;
      });
      const metaValues = metaItems.map((item) => compact(item.querySelector('strong')?.textContent || '', 80));
      if (!context.train_type && metaValues[0]) context.train_type = metaValues[0];
      if (!context.price && metaValues[1]) context.price = metaValues[1];
      if (!context.trips_summary && metaValues[2]) context.trips_summary = metaValues[2];
      if (!context.last_update && metaValues[3]) context.last_update = metaValues[3];
    }

    const availableKeys = Object.keys(context).filter((key) => compact(context[key]));
    return availableKeys.length > 1 ? context : { page_name: pageName };
  };

  const ensureStyle = () => {
    if (document.getElementById('tm-ai-assistant-style')) return;
    const style = document.createElement('style');
    style.id = 'tm-ai-assistant-style';
    style.textContent = `
      .tm-ai-assistant {
        position: fixed;
        right: 22px;
        bottom: 22px;
        z-index: 4200;
        font-family: "Segoe UI Variable Text", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
      }
      .tm-ai-toggle {
        min-width: 138px;
        min-height: 50px;
        padding: 9px 12px;
        border: 1px solid #d7e0d3;
        border-top: 3px solid #2b8a57;
        border-radius: 0;
        background: #fbfdf9;
        color: #183525;
        cursor: pointer;
        display: inline-flex;
        align-items: center;
        gap: 10px;
        justify-content: flex-start;
        box-shadow: 0 12px 28px rgba(36, 74, 50, 0.1);
        transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
      }
      .tm-ai-toggle:hover {
        transform: translateY(-2px);
        box-shadow: 0 20px 38px rgba(36, 74, 50, 0.14);
        border-color: #cadecd;
      }
      .tm-ai-toggle-badge {
        width: 28px;
        height: 28px;
        border-radius: 0;
        background: #2b8a57;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-size: 12px;
        font-weight: 800;
        color: #ffffff;
        letter-spacing: 0.08em;
      }
      .tm-ai-toggle-copy {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        gap: 2px;
      }
      .tm-ai-toggle-copy strong {
        font-size: 13px;
        line-height: 1;
        color: #183525;
      }
      .tm-ai-toggle-copy span {
        font-size: 10px;
        color: #607064;
      }
      .tm-ai-panel {
        width: min(520px, calc(100vw - 24px));
        height: min(780px, calc(100vh - 72px));
        display: none;
        flex-direction: column;
        margin-top: 10px;
        border: 1px solid #d7e0d3;
        border-top: 3px solid #2b8a57;
        border-radius: 0;
        overflow: hidden;
        background: #f9fcf7;
        box-shadow: 0 24px 56px rgba(36, 74, 50, 0.12);
      }
      .tm-ai-assistant.is-open .tm-ai-panel {
        display: flex;
      }
      .tm-ai-header {
        padding: 12px 14px 8px;
        color: #173321;
        background: #fbfdf9;
        border-bottom: 1px solid #dfe8dd;
      }
      .tm-ai-header-row {
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
        gap: 12px;
      }
      .tm-ai-agent {
        display: flex;
        gap: 10px;
        align-items: center;
      }
      .tm-ai-agent-avatar {
        width: 26px;
        height: 26px;
        border-radius: 0;
        background: #2b8a57;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-weight: 800;
        font-size: 11px;
        color: #ffffff;
      }
      .tm-ai-header-title {
        display: flex;
        flex-direction: column;
        gap: 1px;
      }
      .tm-ai-kicker {
        font-size: 9px;
        font-weight: 800;
        letter-spacing: 0.14em;
        color: #2b8a57;
      }
      .tm-ai-title {
        font-size: 15px;
        font-weight: 800;
        color: #183525;
      }
      .tm-ai-subtitle,
      .tm-ai-header-meta {
        font-size: 11px;
        color: #607064;
      }
      .tm-ai-header-meta {
        display: none;
      }
      .tm-ai-close {
        width: 28px;
        height: 28px;
        border: 1px solid #d7e0d3;
        border-radius: 0;
        background: #ffffff;
        color: #385042;
        cursor: pointer;
        transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease;
      }
      .tm-ai-close:hover {
        background: #f3f8f1;
        border-color: #c6d7c1;
        color: #183525;
      }
      .tm-ai-context {
        margin-top: 8px;
        padding: 8px;
        border-radius: 0;
        background: #f7fbf5;
        border: 1px solid #dbe7d8;
      }
      .tm-ai-context-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 10px;
        margin-bottom: 6px;
      }
      .tm-ai-context-title {
        font-size: 11px;
        font-weight: 700;
      }
      .tm-ai-context-tag {
        padding: 2px 6px;
        border-radius: 0;
        background: #edf6ee;
        color: #2b8a57;
        font-size: 10px;
      }
      .tm-ai-context-grid {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: 6px;
      }
      .tm-ai-context-item {
        padding: 6px 8px;
        border-radius: 0;
        background: #ffffff;
        border: 1px solid #e0e9dd;
      }
      .tm-ai-context-item span {
        display: block;
        font-size: 9px;
        letter-spacing: 0.06em;
        color: #6b7c6e;
      }
      .tm-ai-context-item strong {
        display: block;
        margin-top: 2px;
        font-size: 12px;
        line-height: 1.35;
        word-break: break-word;
        color: #173321;
      }
      .tm-ai-context-actions,
      .tm-ai-suggestions {
        display: flex;
        flex-wrap: wrap;
        gap: 6px;
        margin-top: 8px;
      }
      .tm-ai-context-btn,
      .tm-ai-suggestion {
        padding: 6px 10px;
        border-radius: 0;
        border: 1px solid #d5e1d2;
        background: #ffffff;
        color: #355040;
        font-size: 11px;
        cursor: pointer;
        transition: border-color 0.18s ease, background-color 0.18s ease, color 0.18s ease, transform 0.18s ease;
      }
      .tm-ai-context-btn:hover,
      .tm-ai-suggestion:hover {
        background: #f2f8f1;
        border-color: #bfd3ba;
        color: #2b8a57;
        transform: translateY(-1px);
      }
      .tm-ai-suggestions {
        display: none;
      }
      .tm-ai-body {
        flex: 1;
        overflow: auto;
        padding: 14px;
        background: #fcfefb;
      }
      .tm-ai-list {
        display: flex;
        flex-direction: column;
        gap: 10px;
      }
      .tm-ai-row {
        display: flex;
        gap: 10px;
        align-items: flex-end;
      }
      .tm-ai-row-user {
        justify-content: flex-end;
      }
      .tm-ai-bubble-avatar {
        width: 22px;
        height: 22px;
        border-radius: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-size: 10px;
        font-weight: 800;
        flex-shrink: 0;
      }
      .tm-ai-row-assistant .tm-ai-bubble-avatar {
        background: #edf6ee;
        color: #2b8a57;
      }
      .tm-ai-row-user .tm-ai-bubble-avatar {
        background: #7ca986;
        color: #ffffff;
        order: 2;
      }
      .tm-ai-message {
        max-width: 88%;
        border-radius: 0;
        padding: 10px 12px;
        line-height: 1.68;
        white-space: pre-wrap;
        word-break: break-word;
        font-size: 13px;
      }
      .tm-ai-message-assistant {
        background: #ffffff;
        color: #223428;
        border: 1px solid #dbe6d8;
      }
      .tm-ai-message-user {
        background: #eef6ee;
        color: #284233;
        border: 1px solid #dbe7d8;
      }
      .tm-ai-message h1,
      .tm-ai-message h2,
      .tm-ai-message h3 {
        margin: 0 0 8px;
        color: inherit;
        line-height: 1.45;
      }
      .tm-ai-message h1 { font-size: 16px; }
      .tm-ai-message h2 { font-size: 15px; }
      .tm-ai-message h3 { font-size: 14px; }
      .tm-ai-message p {
        margin: 0 0 8px;
      }
      .tm-ai-message p:last-child,
      .tm-ai-message ul:last-child,
      .tm-ai-message ol:last-child,
      .tm-ai-message pre:last-child {
        margin-bottom: 0;
      }
      .tm-ai-message ul,
      .tm-ai-message ol {
        margin: 0 0 8px;
        padding-left: 18px;
      }
      .tm-ai-message li + li {
        margin-top: 4px;
      }
      .tm-ai-message a {
        color: #2f7d55;
        text-decoration: underline;
      }
      .tm-ai-message strong {
        font-weight: 700;
      }
      .tm-ai-message code {
        padding: 1px 5px;
        background: #f1f5ef;
        border: 1px solid #dde7da;
        font-family: Consolas, "Courier New", monospace;
        font-size: 12px;
      }
      .tm-ai-pre {
        margin: 0 0 8px;
        padding: 10px 12px;
        overflow: auto;
        background: #f4f8f2;
        border: 1px solid #dbe6d8;
      }
      .tm-ai-pre code {
        padding: 0;
        border: none;
        background: transparent;
        display: block;
        line-height: 1.55;
      }
      .tm-ai-footer {
        padding: 10px 12px 12px;
        border-top: 1px solid #dfe8dd;
        background: #fbfdf9;
      }
      .tm-ai-status {
        min-height: 14px;
        margin-bottom: 6px;
        color: #647266;
        font-size: 11px;
      }
      .tm-ai-composer {
        border: 1px solid #d6e1d3;
        border-radius: 0;
        background: #ffffff;
        overflow: hidden;
      }
      .tm-ai-input {
        width: 100%;
        min-height: 72px;
        resize: none;
        padding: 10px 12px;
        border: none;
        background: transparent;
        color: #173324;
        outline: none;
        font-size: 13px;
        line-height: 1.6;
      }
      .tm-ai-composer:focus-within {
        border-color: #2b8a57;
        box-shadow: 0 0 0 3px rgba(43, 138, 87, 0.1);
      }
      .tm-ai-actions {
        display: flex;
        align-items: center;
        justify-content: flex-end;
        gap: 12px;
        padding: 0 10px 10px;
      }
      .tm-ai-hint {
        display: none;
      }
      .tm-ai-send {
        min-width: 86px;
        height: 34px;
        border: none;
        border-radius: 0;
        background: #4a9969;
        color: #fff;
        cursor: pointer;
        font-weight: 700;
      }
      .tm-ai-send[disabled],
      .tm-ai-input[disabled] {
        opacity: 0.65;
        cursor: not-allowed;
      }
      @media (max-width: 768px) {
        .tm-ai-assistant {
          right: 12px;
          left: 12px;
          bottom: 12px;
        }
        .tm-ai-toggle {
          width: 100%;
          justify-content: center;
        }
        .tm-ai-panel {
          width: 100%;
          height: min(76vh, 720px);
        }
        .tm-ai-context-grid {
          grid-template-columns: repeat(2, minmax(0, 1fr));
        }
        .tm-ai-actions {
          flex-direction: row;
          justify-content: flex-end;
        }
        .tm-ai-send {
          width: auto;
        }
      }
    `;
    document.head.appendChild(style);
  };

  const root = document.createElement('section');
  root.className = 'tm-ai-assistant';
  root.innerHTML = `
    <button type="button" class="tm-ai-toggle" aria-label="打开在线客服">
      <span class="tm-ai-toggle-badge">客</span>
      <span class="tm-ai-toggle-copy">
        <strong>票务客服台</strong>
        <span>咨询订票、票号与凭证</span>
      </span>
    </button>
    <div class="tm-ai-panel" aria-live="polite">
      <div class="tm-ai-header">
        <div class="tm-ai-header-row">
          <div class="tm-ai-agent">
            <div class="tm-ai-agent-avatar">客服</div>
            <div class="tm-ai-header-title">
              <span class="tm-ai-kicker">FSE TICKET SERVICE DESK</span>
              <strong class="tm-ai-title">票务服务台</strong>
              <span class="tm-ai-subtitle">当前页面：${pageName}</span>
            </div>
          </div>
          <button type="button" class="tm-ai-close" aria-label="关闭在线客服">×</button>
        </div>
        <div class="tm-ai-context"></div>
        <div class="tm-ai-suggestions"></div>
      </div>
      <div class="tm-ai-body">
        <div class="tm-ai-list"></div>
      </div>
      <div class="tm-ai-footer">
        <div class="tm-ai-status"></div>
        <div class="tm-ai-composer">
          <textarea class="tm-ai-input" placeholder="例如：帮我解释当前凭证为什么显示可使用？"></textarea>
          <div class="tm-ai-actions">
            <div class="tm-ai-hint">客服会自动读取当前页面中的票号、凭证码和主要票务信息。</div>
            <button type="button" class="tm-ai-send">发送</button>
          </div>
        </div>
      </div>
    </div>
  `;

  ensureStyle();
  body.appendChild(root);

  const toggleButton = root.querySelector('.tm-ai-toggle');
  const closeButton = root.querySelector('.tm-ai-close');
  const list = root.querySelector('.tm-ai-list');
  const suggestions = root.querySelector('.tm-ai-suggestions');
  const contextBox = root.querySelector('.tm-ai-context');
  const input = root.querySelector('.tm-ai-input');
  const sendButton = root.querySelector('.tm-ai-send');
  const statusText = root.querySelector('.tm-ai-status');
  const bodyBox = root.querySelector('.tm-ai-body');

  const renderMessage = (role, text) => {
    const row = document.createElement('div');
    row.className = `tm-ai-row tm-ai-row-${role}`;
    row.innerHTML = `
      <span class="tm-ai-bubble-avatar">${role === 'assistant' ? '服' : '我'}</span>
      <div class="tm-ai-message tm-ai-message-${role}"></div>
    `;
    const messageBox = row.querySelector('.tm-ai-message');
    if (role === 'assistant') messageBox.innerHTML = markdownToHtml(text);
    else messageBox.textContent = text;
    list.appendChild(row);
    bodyBox.scrollTop = bodyBox.scrollHeight;
  };

  const setStatus = (text) => {
    statusText.textContent = text || '';
  };

  const pushHistory = (role, text) => {
    history.push({ role, content: text });
    if (history.length > 12) history.splice(0, history.length - 12);
  };

  const openPanel = () => {
    root.classList.add('is-open');
    refreshContextUI();
    window.setTimeout(() => input.focus(), 20);
  };

  const closePanel = () => {
    root.classList.remove('is-open');
  };

  const setSending = (value) => {
    sending = value;
    input.disabled = value;
    sendButton.disabled = value;
    setStatus(value ? '客服正在整理当前页面信息并生成答复...' : '');
  };

  const sendPreset = (prompt) => {
    input.value = prompt;
    openPanel();
    input.focus();
  };

  const renderSuggestions = () => {
    const items = suggestionMap[pageName] || suggestionMap['首页'];
    suggestions.innerHTML = '';
    items.forEach((text) => {
      const button = document.createElement('button');
      button.type = 'button';
      button.className = 'tm-ai-suggestion';
      button.textContent = text;
      button.addEventListener('click', () => sendPreset(text));
      suggestions.appendChild(button);
    });
  };

  const renderContext = (context) => {
    const entries = Object.entries(context || {})
      .filter(([key, value]) => key !== 'page_name' && compact(value))
      .slice(0, 6);
    const actionItems = buildContextActions(context);

    if (!entries.length) {
      contextBox.innerHTML = `
        <div class="tm-ai-context-head">
          <span class="tm-ai-context-title">当前页面识别</span>
          <span class="tm-ai-context-tag">未识别到票据</span>
        </div>
        <div class="tm-ai-context-item">
          <span>提示</span>
          <strong>客服会在你打开凭证详情、票据详情或输入检索内容后自动读取并辅助解释。</strong>
        </div>
      `;
      return;
    }

    const labelMap = {
      voucher_code: '凭证码',
      ticket_id: '票号',
      query_keyword: '检索内容',
      status: '当前状态',
      start: '起点',
      terminal: '终点',
      selected_start: '所选起点',
      selected_terminal: '所选终点',
      trips: '乘次数量',
      train_type: '车型',
      price: '票价',
      ride_date: '乘车日期',
      trips_summary: '乘次信息',
      last_update: '最近更新',
      redeem_tip: '兑票说明'
    };

    contextBox.innerHTML = `
      <div class="tm-ai-context-head">
        <span class="tm-ai-context-title">当前页面识别</span>
        <span class="tm-ai-context-tag">已同步票务上下文</span>
      </div>
      <div class="tm-ai-context-grid">
        ${entries.map(([key, value]) => `
          <div class="tm-ai-context-item">
            <span>${labelMap[key] || key}</span>
            <strong>${compact(value, 80)}</strong>
          </div>
        `).join('')}
      </div>
      ${actionItems.length ? `
        <div class="tm-ai-context-actions">
          ${actionItems.map((item, index) => `<button type="button" class="tm-ai-context-btn" data-context-action="${index}">${item.label}</button>`).join('')}
        </div>
      ` : ''}
    `;

    Array.from(contextBox.querySelectorAll('[data-context-action]')).forEach((button) => {
      button.addEventListener('click', () => {
        const idx = Number(button.getAttribute('data-context-action'));
        const item = actionItems[idx];
        if (item) sendPreset(item.prompt);
      });
    });
  };

  const refreshContextUI = () => {
    const context = collectPageContext();
    const signature = toContextSignature(context);
    if (signature === lastContextSignature && contextBox.innerHTML) return;
    lastContextSignature = signature;
    renderContext(context);
  };

  const sendMessage = async () => {
    const message = String(input.value || '').trim();
    if (!message || sending) return;

    const requestHistory = history.slice();
    const context = collectPageContext();

    renderMessage('user', message);
    pushHistory('user', message);
    input.value = '';
    setSending(true);

    try {
      const response = await fetch('/api/ai-assistant', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          message,
          history: requestHistory,
          page: pageName,
          context
        })
      });
      const data = await response.json().catch(() => ({}));
      if (!response.ok || !data.ok || !data.reply) {
        throw new Error(data.error || '在线客服暂时无法回答，请稍后重试。');
      }

      renderMessage('assistant', data.reply);
      pushHistory('assistant', data.reply);
    } catch (error) {
      const fallback = error?.message || '在线客服暂时不可用，请稍后重试。';
      renderMessage('assistant', fallback);
      pushHistory('assistant', fallback);
    } finally {
      setSending(false);
      refreshContextUI();
      input.focus();
    }
  };

  toggleButton.addEventListener('click', () => {
    if (root.classList.contains('is-open')) closePanel();
    else openPanel();
  });
  closeButton.addEventListener('click', closePanel);
  sendButton.addEventListener('click', sendMessage);
  input.addEventListener('keydown', (event) => {
    if (event.key === 'Enter' && !event.shiftKey) {
      event.preventDefault();
      sendMessage();
    } else if (event.key === 'Escape') {
      closePanel();
    }
  });

  const observer = new MutationObserver(() => {
    window.clearTimeout(observer.__tmTimer);
    observer.__tmTimer = window.setTimeout(refreshContextUI, 180);
  });
  observer.observe(document.body, { childList: true, subtree: true, characterData: true });

  renderMessage('assistant', `你好，这里是 FSE 票务在线客服。你现在位于“${pageName}”页面，我会自动读取当前票号、凭证码或检索内容，帮你解释状态、使用方法和下一步操作。`);
  pushHistory('assistant', `你好，这里是 FSE 票务在线客服。你现在位于“${pageName}”页面，我会自动读取当前票号、凭证码或检索内容，帮你解释状态、使用方法和下一步操作。`);
  renderSuggestions();
  refreshContextUI();
})();
