function AiQuadrantChart({ receipt }) {
  const stats = receipt.statistics || {};
  const points = stats.points || [];
  const valuesX = points.map(p => p.x);
  const valuesY = points.map(p => p.y);
  const minX0 = Math.min(...valuesX), maxX0 = Math.max(...valuesX);
  const minY0 = Math.min(...valuesY), maxY0 = Math.max(...valuesY);
  const padX = (maxX0 - minX0 || Math.max(Math.abs(maxX0 || 1), 1)) * .12;
  const padY = (maxY0 - minY0 || Math.max(Math.abs(maxY0 || 1), 1)) * .12;
  const minX = minX0 - padX, maxX = maxX0 + padX, minY = minY0 - padY, maxY = maxY0 + padY;
  const left = 48, right = 406, top = 22, bottom = 252;
  const sx = value => left + ((value - minX) / (maxX - minX || 1)) * (right - left);
  const sy = value => bottom - ((value - minY) / (maxY - minY || 1)) * (bottom - top);
  const cutX = sx(stats.xMean), cutY = sy(stats.yMean);
  const color = { both_better: '#43a776', x_better: '#7184dd', y_better: '#d6984b', both_lower: '#9da5b2' };
  return (
    <div className="ai-chart-wrap">
      <svg className="ai-quadrant-chart" viewBox="0 0 430 300" role="img"
        aria-label={`${receipt.xMetric.label} 与 ${receipt.yMetric.label} 四象限图`}>
        <rect x={left} y={top} width={right-left} height={bottom-top} rx="10" fill="#f8f9fb" />
        <rect x={cutX} y={top} width={Math.max(0, right-cutX)} height={Math.max(0, cutY-top)} fill="rgba(67,167,118,.08)" />
        <line x1={cutX} x2={cutX} y1={top} y2={bottom} stroke="#aeb6c4" strokeDasharray="4 4" />
        <line x1={left} x2={right} y1={cutY} y2={cutY} stroke="#aeb6c4" strokeDasharray="4 4" />
        {points.map(point => (
          <g key={point.athleteId}>
            <circle cx={sx(point.x)} cy={sy(point.y)} r="6" fill={color[point.quadrant] || '#7184dd'} stroke="#fff" strokeWidth="2">
              <title>{point.athleteName}: {point.x} / {point.y} · {point.date}</title>
            </circle>
            <text x={sx(point.x)+8} y={sy(point.y)-7} fontSize="8.5" fill="#596274">{point.athleteName.split(' ')[0]}</text>
          </g>
        ))}
        <text x={(left+right)/2} y="286" textAnchor="middle" fontSize="10" fill="#657083">
          {receipt.xMetric.label} ({receipt.xMetric.unit || 'value'})
        </text>
        <text transform="translate(13 145) rotate(-90)" textAnchor="middle" fontSize="10" fill="#657083">
          {receipt.yMetric.label} ({receipt.yMetric.unit || 'value'})
        </text>
        <text x={cutX+4} y={bottom+13} fontSize="8" fill="#8b94a4">均值 {AiAssistantFormat.number(stats.xMean)}</text>
        <text x={left+4} y={cutY-5} fontSize="8" fill="#8b94a4">均值 {AiAssistantFormat.number(stats.yMean)}</text>
      </svg>
      <div className="ai-chart-legend">
        <span><i style={{background:'#43a776'}} />两项相对更优</span>
        <span><i style={{background:'#7184dd'}} />横轴相对更优</span>
        <span><i style={{background:'#d6984b'}} />纵轴相对更优</span>
        <span><i style={{background:'#9da5b2'}} />两项相对较低</span>
      </div>
    </div>
  );
}

const AiAssistantFormat = {
  number(value) {
    if (value == null || !Number.isFinite(value)) return '—';
    return Math.abs(value) >= 100 ? value.toFixed(1) : value.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
  },
};

const AI_DOMAIN_LABELS = {
  training: '训练',
  injury: '损伤与康复',
  movement: '动作筛查',
  norms: '常模',
};

const AI_RESULT_LABELS = {
  athlete: '运动员',
  metric: '指标',
  observation: '观测',
  review: '审核',
  training: '训练',
  injury: '损伤',
  rehab_training: '康复',
  movement_screen: '筛查',
  norm: '常模',
  knowledge: '知识',
};

const AI_AGENT_RESTORE_TIMEOUT_MS = 10000;
function aiAgentWithTimeout(task, timeoutMs = AI_AGENT_RESTORE_TIMEOUT_MS) {
  let timer = 0;
  return Promise.race([
    Promise.resolve(task),
    new Promise((_, reject) => {
      timer = window.setTimeout(() => reject(new Error('恢复 AI 会话超时，请重试。')), timeoutMs);
    }),
  ]).finally(() => window.clearTimeout(timer));
}

const AI_AGENT_CSS = `
.axis-ai-agent-nav{display:flex;align-items:center;gap:4px;margin-left:auto}.axis-ai-agent-nav button{min-height:27px;padding:0 7px;border:1px solid #dde2e8;border-radius:7px;background:#fff;color:#697486;font-size:8.5px;cursor:pointer}.axis-ai-agent-nav button.on{background:#eef0f3;color:#2f3947;font-weight:700}
.axis-ai-thread{display:grid;gap:8px;margin-bottom:10px}.axis-ai-message{max-width:88%;padding:9px 11px;border:1px solid #e0e4e9;border-radius:11px;background:#fff;color:#4d596a;font-size:10px;line-height:1.5;white-space:pre-wrap}.axis-ai-message.user{justify-self:end;border-color:#d7dce3;background:#eef0f3;color:#2e3948}.axis-ai-message.assistant{justify-self:start}.axis-ai-message small{display:block;margin-top:4px;color:#8992a0;font-size:8px}.axis-ai-message-tools{display:flex;flex-wrap:wrap;gap:4px;margin-top:5px}.axis-ai-message-tools i{padding:2px 5px;border-radius:999px;background:#f0f2f4;color:#747f8e;font-size:7.5px;font-style:normal}
.axis-ai-agent-list{display:grid;gap:7px}.axis-ai-agent-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:center;width:100%;padding:10px 11px;border:1px solid #e0e4e9;border-radius:9px;background:#fff;text-align:left}.axis-ai-agent-row button,.axis-ai-agent-row{cursor:pointer}.axis-ai-agent-row b,.axis-ai-agent-row small{display:block}.axis-ai-agent-row b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px}.axis-ai-agent-row small{margin-top:2px;color:#838d9b;font-size:8px}.axis-ai-memory-origin{padding:3px 6px;border-radius:999px;background:#eef0f3;color:#687384;font-size:7.5px}.axis-ai-memory-forget,.axis-ai-memory-confirm,.axis-ai-memory-dismiss,.axis-ai-memory-edit{border:0;background:transparent;font-size:8px;cursor:pointer}.axis-ai-memory-forget,.axis-ai-memory-dismiss{color:#8b5551}.axis-ai-memory-confirm,.axis-ai-memory-edit{color:#4f6f5d;font-weight:700}.axis-ai-memory-candidates{display:grid;gap:7px;margin-bottom:10px}.axis-ai-memory-candidates h4{margin:0;color:#566273;font-size:9px}.axis-ai-memory-candidates .axis-ai-agent-row{border-style:dashed;background:#fafaf9}.axis-ai-memory-actions{display:flex;align-items:center;gap:5px}.axis-ai-memory-editor{display:flex;gap:5px;align-items:center}.axis-ai-memory-editor input{min-width:220px;border:1px solid #d9dee5;border-radius:6px;padding:6px 7px;color:#364252;font:9px inherit}.axis-ai-page-command{display:grid;gap:6px}.axis-ai-page-command strong{font-size:11px;color:#2f3947}.axis-ai-page-command p{margin:0;color:#697486;font-size:9px;line-height:1.55}.axis-ai-page-command span{width:max-content;padding:3px 7px;border-radius:999px;background:#eef0f3;color:#596575;font-size:8px}.axis-ai-error-action{margin-left:8px;border:1px solid #ddb5b5;border-radius:6px;padding:4px 7px;background:#fff;color:#8f3f3f;font:650 9px inherit;cursor:pointer}
.axis-ai-insight-list{display:grid;gap:8px}.axis-ai-insight{border:1px solid #e0e4e9;border-radius:10px;padding:10px;background:#fff}.axis-ai-insight.warning{border-left:3px solid #a76d43}.axis-ai-insight-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.axis-ai-insight-head span{border-radius:999px;padding:3px 6px;background:#eef0f3;color:#687384;font-size:7.5px}.axis-ai-insight h4{margin:7px 0 4px;color:#303a49;font-size:11px}.axis-ai-insight p{margin:0;color:#6f7a8a;font-size:9.5px;line-height:1.55}.axis-ai-insight-facts{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.axis-ai-insight-facts span{border:1px solid #e2e5ea;border-radius:6px;padding:4px 6px;background:#f8f9fa;color:#596575;font-size:8px}.axis-ai-insight footer{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:9px;color:#929aa6;font-size:7.5px}.axis-ai-insight footer button{border:1px solid #dce1e7;border-radius:6px;padding:5px 7px;background:#fff;color:#4f5b6b;font:650 8.5px inherit;cursor:pointer}
.axis-ai-draft{display:grid;gap:8px}.axis-ai-draft-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.axis-ai-draft-head span{padding:3px 7px;border-radius:999px;background:#f3eee4;color:#7b6744;font-size:8px}.axis-ai-draft textarea{width:100%;min-height:132px;resize:vertical;border:1px solid #dce1e7;border-radius:9px;background:#fbfbfc;padding:10px;color:#374253;font:10px/1.6 inherit}.axis-ai-draft-actions{display:flex;justify-content:flex-end}.axis-ai-draft-actions button{min-height:30px;padding:0 10px;border:1px solid #d9dee5;border-radius:7px;background:#fff;color:#4d5969;font-size:9px;cursor:pointer}
`;

function AiDraftCard({ draft }) {
  const [value, setValue] = React.useState(draft.content || '');
  React.useEffect(() => setValue(draft.content || ''), [draft.content]);
  const copy = async () => {
    try { await navigator.clipboard.writeText(value); }
    catch (_) {}
  };
  return <section className="axis-ai-card axis-ai-draft" data-ai-draft-status={draft.status}>
    <div className="axis-ai-draft-head"><b>{draft.draftKind === 'report_comment' ? '报告评语草案' : '文本草案'}</b><span>未保存 · 可编辑</span></div>
    <textarea aria-label="AI 生成草案" value={value} onChange={event=>setValue(event.target.value)} />
    <small>编辑仅停留在当前草案框；不会写入报告、训练计划或运动员档案。</small>
    <div className="axis-ai-draft-actions"><button type="button" onClick={copy}>复制草案</button></div>
  </section>;
}

function AiFilterReceipt({ receipt }) {
  const filters = receipt?.appliedFilters || {};
  const chips = [];
  (receipt?.domains || []).forEach(domain => chips.push(`数据域 · ${AI_DOMAIN_LABELS[domain] || domain}`));
  if (filters.team) chips.push(`队伍 · ${filters.team}`);
  if (filters.position) chips.push(`位置 · ${filters.position}`);
  if (filters.fromDate || filters.toDate) chips.push(`${filters.fromDate || '最早'} 至 ${filters.toDate || '最新'}`);
  if (!chips.length) return null;
  return <div className="axis-ai-filter-receipt" aria-label="本次查询筛选条件">{chips.map(chip => <span key={chip}>{chip}</span>)}</div>;
}

function aiPlannerModeFromMessage(message) {
  const tools = Array.isArray(message?.toolCalls) ? message.toolCalls : [];
  const gateway = tools.find(tool => tool.name === 'gateway.converse');
  const specialist = tools.find(tool => tool.name === 'sportsScience.consult' && tool.status === 'succeeded');
  if (gateway?.status === 'succeeded' && specialist) return 'conversational-expert';
  if (gateway?.status === 'succeeded') return 'conversational';
  if (gateway && gateway.status !== 'succeeded') return 'grounded-conversation';
  if (specialist) return 'semantic-expert';
  return 'local';
}

function AiAssistant({ readModel, decisionTools, view, onNavigate, onSelectAthlete }) {
  const [open, setOpen] = React.useState(false);
  const [text, setText] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [result, setResult] = React.useState(null);
  const [error, setError] = React.useState('');
  const [plannerMode, setPlannerMode] = React.useState('local');
  const [agentReady, setAgentReady] = React.useState(false);
  const [agentStatus, setAgentStatus] = React.useState('idle');
  const [restoreAttempt, setRestoreAttempt] = React.useState(0);
  const [panelMode, setPanelMode] = React.useState('chat');
  const [conversationId, setConversationId] = React.useState('');
  const [messages, setMessages] = React.useState([]);
  const [conversations, setConversations] = React.useState([]);
  const [memories, setMemories] = React.useState([]);
  const [memoryCandidates, setMemoryCandidates] = React.useState([]);
  const [editingMemoryId, setEditingMemoryId] = React.useState('');
  const [editingMemoryText, setEditingMemoryText] = React.useState('');
  const [insightCandidates, setInsightCandidates] = React.useState([]);
  const query = window.AiQueryContract;
  const gateway = React.useMemo(
    () => window.AI_GATEWAY_ENABLED === true ? (window.AiGatewayClient?.create?.() || null) : null,
    []
  );
  const ownerId = decisionTools?.ownerId || 'local-user';
  const agentRepository = React.useMemo(() => {
    if (!window.AiAgentStore?.createAiAgentStore || !window.AiAgentRepository?.createAiAgentRepository) return null;
    return window.AiAgentRepository.createAiAgentRepository({ store:window.AiAgentStore.createAiAgentStore() });
  }, []);
  const capabilities = React.useMemo(
    () => ({
      ...(readModel?.describeCapabilities?.() || { metrics: [], athleteCount: 0 }),
      athletes: decisionTools?.athletes || [],
      currentAthleteId: decisionTools?.currentAthleteId || '',
    }),
    [readModel, decisionTools]
  );

  const executePlan = React.useCallback(async (plan) => {
    const valid = query.validate(plan, capabilities);
    if (!valid.ok) throw new Error(valid.error);
    if (valid.plan.intent === 'needs_clarification') {
      setResult(valid.plan);
      return valid.plan;
    }
    if (valid.plan.intent === 'ui_control') {
      const receipt = await new Promise((resolve, reject) => {
        let timer = 0;
        const finish = event => {
          const detail = event?.detail || {};
          if (detail.action !== valid.plan.action || detail.moduleId !== valid.plan.moduleId) return;
          window.clearTimeout(timer);
          document.removeEventListener('axis:force-report-command-result', finish);
          resolve({
            kind:'ui_control',
            action:detail.action,
            moduleId:detail.moduleId,
            matchedCount:Number(detail.matchedCount || 0),
            status:detail.status || 'preview-opened',
            filters:Object.assign({}, valid.plan.filters),
            organization:detail.organization || valid.plan.organization || '',
            groups:Array.isArray(detail.groups) ? detail.groups.slice() : [],
            warnings:Array.isArray(detail.warnings) ? detail.warnings.slice() : [],
          });
        };
        document.addEventListener('axis:force-report-command-result', finish);
        timer = window.setTimeout(() => {
          document.removeEventListener('axis:force-report-command-result', finish);
          reject(new Error('请先打开测力台报告工作台，再让我配置参照人群。'));
        }, 1500);
        document.dispatchEvent(new CustomEvent('axis:force-report-command', { detail:valid.plan }));
      });
      setResult(receipt);
      return receipt;
    }
    if (valid.plan.intent === 'decision_support') {
      if (!decisionTools?.buildCase) throw new Error('Decision Case 服务尚未就绪。');
      const decisionCase = await decisionTools.buildCase({
        question:valid.plan.question,
        athleteId:valid.plan.athleteId,
        metricId:valid.plan.metricId,
        fromDate:valid.plan.filters?.fromDate,
        toDate:valid.plan.filters?.toDate,
        ownerId:decisionTools.ownerId,
        knowledgeScopes:valid.plan.knowledgeScopes,
      });
      setResult(decisionCase);
      return decisionCase;
    }
    const receipt = await readModel.execute(valid.plan);
    setResult(receipt);
    return receipt;
  }, [readModel, query, decisionTools, capabilities]);

  const agentRuntime = React.useMemo(() => {
    if (!agentRepository || !window.AiAgentRuntime?.create || !query || !readModel) return null;
    return window.AiAgentRuntime.create({
      repository:agentRepository,
      queryContract:query,
      gateway,
      knowledgeRetrieval:decisionTools?.knowledgeRetrieval,
      webResearch:gateway?.researchWeb
        ? { search:request => gateway.researchWeb(request?.query, request?.limit) }
        : null,
      capabilityProvider:() => capabilities,
      executePlan,
    });
  }, [agentRepository, query, gateway, capabilities, executePlan, readModel, decisionTools, restoreAttempt]);

  React.useEffect(() => {
    if (!open) return undefined;
    const closeOnEscape = event => {
      if (event.key === 'Escape') setOpen(false);
    };
    document.addEventListener('keydown', closeOnEscape);
    return () => document.removeEventListener('keydown', closeOnEscape);
  }, [open]);

  React.useEffect(() => {
    if (!open) return undefined;
    let cancelled = false;
    setAgentReady(false);
    setAgentStatus('restoring');
    setError('');
    if (!agentRuntime) {
      setAgentStatus('error');
      setError('AI 会话服务尚未就绪，请重试。');
      return undefined;
    }
    (async () => {
      try {
        const restored = await aiAgentWithTimeout((async () => {
          const rows = await agentRuntime.listConversations(ownerId);
          const targetId = conversationId || rows[0]?.id || '';
          let thread = null;
          let conversation = null;
          if (targetId) {
            thread = await agentRuntime.loadConversation(targetId, ownerId);
          }
          if (!thread) {
            conversation = await agentRuntime.newConversation(ownerId);
          }
          const remembered = await agentRuntime.listMemories(ownerId);
          const memoryCandidates = await agentRuntime.listMemoryCandidates(ownerId);
          let candidates = [];
          if (readModel?.buildInsightCandidates) {
            try {
              const rows = await Promise.resolve(readModel.buildInsightCandidates({ limit:12 }));
              candidates = Array.isArray(rows) ? rows : [];
            } catch (_) {}
          }
          return { rows, thread, conversation, remembered, memoryCandidates, candidates };
        })());
        if (cancelled) return;
        setConversations(restored.rows);
        setMemories(restored.remembered);
        setMemoryCandidates(restored.memoryCandidates);
        setInsightCandidates(restored.candidates);
        if (restored.thread) {
          setConversationId(restored.thread.conversation.id);
          setMessages(restored.thread.messages || []);
          const latest = (restored.thread.messages || []).filter(message => message.role === 'assistant').slice(-1)[0];
          setResult(latest?.result || null);
          setPlannerMode(aiPlannerModeFromMessage(latest));
        } else {
          setConversationId(restored.conversation.id);
          setMessages([]);
          setResult(null);
        }
        setAgentReady(true);
        setAgentStatus('ready');
      } catch (cause) {
        if (!cancelled) {
          setError(cause?.message || '无法恢复 AI 会话。');
          setAgentReady(false);
          setAgentStatus('error');
        }
      }
    })();
    return () => { cancelled = true; };
  }, [open, agentRuntime, ownerId, restoreAttempt, readModel]);

  const retryAgentRestore = () => {
    setError('');
    setRestoreAttempt(value => value + 1);
  };

  const reviewInsight = candidate => {
    setText(candidate.reviewPrompt || `请复核：${candidate.title}`);
    setPanelMode('chat');
  };

  const run = async (overrideText, planOverride) => {
    const prompt = String(overrideText != null ? overrideText : text).trim();
    if (!prompt || !agentRuntime || !agentReady) return;
    setBusy(true); setError('');
    try {
      const turn = await agentRuntime.runTurn({ conversationId, ownerId, userText:prompt, planOverride });
      setConversationId(turn.conversation.id);
      setMessages(turn.messages);
      setResult(turn.result);
      setPlannerMode(turn.plannerMode);
      setText('');
      setConversations(await agentRuntime.listConversations(ownerId));
      setMemories(await agentRuntime.listMemories(ownerId));
      setMemoryCandidates(await agentRuntime.listMemoryCandidates(ownerId));
    } catch (err) {
      setError(err?.message || '暂时无法完成这次查询。');
    } finally {
      setBusy(false);
    }
  };

  const chooseMetric = async (plan, option) => {
    try { await run(`使用 ${option.label}`, query.completeClarification(plan, option.id)); }
    catch (err) { setError(err?.message || '无法执行所选分析。'); }
  };

  const startConversation = async () => {
    if (!agentRuntime) return;
    const conversation = await agentRuntime.newConversation(ownerId);
    setConversationId(conversation.id); setMessages([]); setResult(null); setText(''); setPanelMode('chat');
  };

  const openConversation = async id => {
    if (!agentRuntime) return;
    const thread = await agentRuntime.loadConversation(id, ownerId);
    if (!thread) return;
    setConversationId(id); setMessages(thread.messages || []);
    const latest = (thread.messages || []).filter(message => message.role === 'assistant').slice(-1)[0];
    setResult(latest?.result || null); setPlannerMode(aiPlannerModeFromMessage(latest)); setPanelMode('chat');
  };

  const forgetMemory = async id => {
    if (!agentRuntime) return;
    await agentRuntime.forgetMemory(id, ownerId);
    setMemories(await agentRuntime.listMemories(ownerId));
  };

  const confirmMemory = async id => {
    if (!agentRuntime) return;
    await agentRuntime.confirmMemory(id, ownerId);
    setMemories(await agentRuntime.listMemories(ownerId));
    setMemoryCandidates(await agentRuntime.listMemoryCandidates(ownerId));
  };

  const dismissMemory = async id => {
    if (!agentRuntime) return;
    await agentRuntime.dismissMemory(id, ownerId);
    setMemoryCandidates(await agentRuntime.listMemoryCandidates(ownerId));
  };

  const saveMemoryEdit = async id => {
    const content = editingMemoryText.trim();
    if (!agentRuntime || !content) return;
    await agentRuntime.updateMemory(id, ownerId, content);
    setMemories(await agentRuntime.listMemories(ownerId));
    setEditingMemoryId('');
    setEditingMemoryText('');
  };

  const openResult = row => {
    if (row.athleteId) onSelectAthlete?.(row.athleteId);
    if (row.route) onNavigate?.(row.route);
    setOpen(false);
  };

  return (
    <div className="workspace-ai-wrap">
      <button type="button" className="workspace-ai-button" onClick={() => setOpen(v => !v)}
        aria-expanded={open} aria-controls="axis-ai-assistant-panel">
        ✦ <span>AI 助手</span><small>Beta</small>
      </button>
      {open && ReactDOM.createPortal(
        <aside id="axis-ai-assistant-panel" className={`axis-ai-panel ${result?.kind==='decision_case'?'decision-mode':''}`} role="dialog" aria-label="Axis AI 助手">
          <style>{AI_AGENT_CSS}</style>
          <header className="axis-ai-head">
            <div><strong>Axis AI</strong><small>可追溯检索 · 确定性统计 · 人工确认草案</small></div>
            <nav className="axis-ai-agent-nav" aria-label="AI 会话工具">
              <button type="button" onClick={startConversation} disabled={!agentReady||busy}>新对话</button>
              <button type="button" className={panelMode==='history'?'on':''} onClick={()=>setPanelMode(panelMode==='history'?'chat':'history')}>历史 {conversations.length}</button>
              <button type="button" className={panelMode==='memory'?'on':''} onClick={()=>setPanelMode(panelMode==='memory'?'chat':'memory')}>记忆 {memories.length}</button>
              <button type="button" className={panelMode==='insights'?'on':''} onClick={()=>setPanelMode(panelMode==='insights'?'chat':'insights')}>洞察 {insightCandidates.length}</button>
            </nav>
            <button type="button" onClick={() => setOpen(false)} aria-label="关闭 AI 助手">×</button>
          </header>
          <div className="axis-ai-body">
            {panelMode === 'history' && <section className="axis-ai-card"><div className="axis-ai-card-head"><b>对话历史</b><span>本机 · 当前 owner</span></div><div className="axis-ai-agent-list">
              {!conversations.length&&<div className="axis-ai-empty">还没有持久化对话。</div>}
              {conversations.map(conversation=><button type="button" className="axis-ai-agent-row" key={conversation.id} onClick={()=>openConversation(conversation.id)}><span><b>{conversation.title}</b><small>{conversation.turnCount} 轮 · {String(conversation.updatedAt||'').replace('T',' ').slice(0,16)}</small></span><span>打开 ↗</span></button>)}
            </div></section>}
            {panelMode === 'memory' && <section className="axis-ai-card"><div className="axis-ai-card-head"><b>个人 Agent 记忆</b><span>确认后生效 · owner 隔离</span></div>
              {!!memoryCandidates.length&&<div className="axis-ai-memory-candidates"><h4>待确认的使用习惯</h4>{memoryCandidates.map(memory=><div className="axis-ai-agent-row" key={memory.id}><span><b>{memory.content}</b><small>候选 · {memory.type} · 观察 {memory.usageCount} 次</small></span><span className="axis-ai-memory-actions"><button type="button" className="axis-ai-memory-confirm" onClick={()=>confirmMemory(memory.id)}>确认记住</button><button type="button" className="axis-ai-memory-dismiss" onClick={()=>dismissMemory(memory.id)}>忽略</button></span></div>)}</div>}
              <div className="axis-ai-agent-list axis-ai-memory-confirmed">
                {!memories.length&&<div className="axis-ai-empty">明确说“记住…”会直接保存；使用习惯只有经你确认后才会进入长期记忆。</div>}
                {memories.map(memory=><div className="axis-ai-agent-row" key={memory.id}>{editingMemoryId===memory.id
                  ?<div className="axis-ai-memory-editor"><input aria-label="编辑 Agent 记忆" value={editingMemoryText} onChange={event=>setEditingMemoryText(event.target.value)} /><button type="button" className="axis-ai-memory-edit" onClick={()=>saveMemoryEdit(memory.id)}>保存</button><button type="button" className="axis-ai-memory-dismiss" onClick={()=>{setEditingMemoryId('');setEditingMemoryText('');}}>取消</button></div>
                  :<><span><b>{memory.content}</b><small>{memory.type} · 使用 {memory.usageCount} 次 · 置信度 {Math.round(memory.confidence*100)}%</small></span><span><i className="axis-ai-memory-origin">{memory.origin==='explicit'?'明确记忆':'已确认习惯'}</i><button type="button" className="axis-ai-memory-edit" onClick={()=>{setEditingMemoryId(memory.id);setEditingMemoryText(memory.content);}}>编辑</button><button type="button" className="axis-ai-memory-forget" onClick={()=>forgetMemory(memory.id)}>遗忘</button></span></>}
                </div>)}
              </div><p className="axis-ai-method">只保存当前用户确认的工作偏好与术语，不复制运动员测试、训练、损伤或康复业务记录。</p></section>}
            {panelMode === 'insights' && <section className="axis-ai-card">
              <div className="axis-ai-card-head"><b>待复核洞察</b><span>集中呈现 · 不自动执行</span></div>
              {!insightCandidates.length && <div className="axis-ai-empty">当前没有达到规则阈值的候选。新业务数据会在这里形成可复核线索，不会自动改计划。</div>}
              <div className="axis-ai-insight-list">
                {insightCandidates.map(candidate => <article className={`axis-ai-insight ${candidate.severity||''}`} key={candidate.id} data-ai-insight-type={candidate.type}>
                  <div className="axis-ai-insight-head"><span>{({anomaly:'异常',trend:'趋势',missing:'缺失',correlation:'关联'})[candidate.type]||'线索'}</span><small>{candidate.redFlag?'红旗':'非弹窗候选'}</small></div>
                  <h4>{candidate.title}</h4>
                  <p>{candidate.summary}</p>
                  <div className="axis-ai-insight-facts">{(candidate.facts||[]).slice(0,3).map((fact,index)=><span key={index}>{fact.label} · {typeof fact.value==='number'?AiAssistantFormat.number(fact.value):fact.value}{fact.unit?` ${fact.unit}`:''}</span>)}</div>
                  <footer><span>规则 {candidate.rule?.version||'—'} · 只读</span><button type="button" onClick={()=>reviewInsight(candidate)}>放入对话复核</button></footer>
                </article>)}
              </div>
            </section>}
            {panelMode === 'chat' && <>
            {!!messages.length&&<section className="axis-ai-thread" aria-label="AI 对话记录">
              {messages.map(message=><article className={`axis-ai-message ${message.role}`} key={message.id}><b>{message.role==='user'?'你':'Axis AI'}</b>{'\n'}{message.content}{message.role==='assistant'&&!!message.toolCalls?.length&&<div className="axis-ai-message-tools">{message.toolCalls.map((tool,index)=><i key={index}>{tool.name} · {tool.status}</i>)}</div>}<small>{String(message.createdAt||'').replace('T',' ').slice(0,16)}</small></article>)}
            </section>}
            <section className="axis-ai-intro">
              <b>你可以直接询问已有数据</b>
              <p>AI 负责理解问题；训练、损伤、筛查、常模和统计均由本地只读工具处理，不会改写运动员记录。</p>
            </section>
            {!result && (
              <section className="axis-ai-demo" aria-label="AI 示例数据范围">
                <span>DEMO / 示例数据范围</span>
                <b>{capabilities.athleteCount || 0} 名运动员 · {(capabilities.metrics || []).length} 个指标 · {(capabilities.domains || []).length} 个数据域</b>
                <p>点击下方示例会读取当前工作区数据并生成可核查结果；示例分析不会新增或改写正式记录。</p>
              </section>
            )}
            <div className="axis-ai-examples">
              <button type="button" disabled={!agentReady||busy} onClick={() => { const q='帮我分析队内40m跑和CMJ的相关性，用四象限图呈现'; setText(q); run(q); }}>40m × CMJ 四象限</button>
              <button type="button" disabled={!agentReady||busy} onClick={() => { const q='查找最近30天后卫的训练记录'; setText(q); run(q); }}>最近 30 天后卫训练</button>
              <button type="button" disabled={!agentReady||busy} onClick={() => { const q='检索损伤与康复记录'; setText(q); run(q); }}>检索损伤与康复</button>
              <button type="button" disabled={!agentReady||busy} onClick={() => { const q='查找 CMJ 常模'; setText(q); run(q); }}>查找 CMJ 常模</button>
              {decisionTools&&<button type="button" disabled={!agentReady||busy} onClick={() => { const athlete=decisionTools.athletes?.find(row=>row.id===decisionTools.currentAthleteId)||decisionTools.athletes?.[0]; const q=`${athlete?.name||'当前运动员'} 最近场地 CMJ 变化，结合训练量看下周是否需要调整`; setText(q); run(q); }}>场地 CMJ × 训练 Decision Case</button>}
            </div>
            {result?.intent === 'needs_clarification' && (
              <section className="axis-ai-card">
                <b>{result.question}</b>
                <div className="axis-ai-options">
                  {result.options.filter(option => !(result.pending?.metrics || []).includes(option.id)).map(option => (
                    <button key={option.id} type="button" onClick={() => chooseMetric(result, option)}>
                      <strong>{option.label}</strong><small>{option.origin} · {option.unit || '无单位'}</small>
                    </button>
                  ))}
                </div>
              </section>
            )}
            {result?.kind === 'search' && (
              <section className="axis-ai-card">
                <div className="axis-ai-card-head"><b>检索结果</b><span>{result.totalReturned} 条</span></div>
                <AiFilterReceipt receipt={result} />
                {!result.results.length && <div className="axis-ai-empty">没有找到匹配信息。请尝试运动员姓名、指标名称或测试类型。</div>}
                <div className="axis-ai-results">
                  {result.results.map(row => (
                    <button key={row.ref} type="button" onClick={() => openResult(row)}>
                      <span className="axis-ai-result-kind">{AI_RESULT_LABELS[row.kind] || row.kind}</span>
                      <span><b>{row.title}</b><small>{row.snippet}</small></span><em>打开 ↗</em>
                    </button>
                  ))}
                </div>
              </section>
            )}
            {result?.kind === 'correlation' && (
              <>
                <section className="axis-ai-card">
                  <div className="axis-ai-card-head"><b>{result.xMetric.label} × {result.yMetric.label}</b><span>n={result.statistics.n}</span></div>
                  <AiFilterReceipt receipt={result} />
                  <div className="axis-ai-stats">
                    <div><small>Pearson r</small><strong>{result.statistics.sampleSufficient ? AiAssistantFormat.number(result.statistics.r) : '样本不足'}</strong></div>
                    <div><small>95% CI</small><strong>{result.statistics.confidenceInterval
                      ? `${AiAssistantFormat.number(result.statistics.confidenceInterval.lower)} 至 ${AiAssistantFormat.number(result.statistics.confidenceInterval.upper)}`
                      : '样本不足'}</strong></div>
                    <div><small>横轴均值</small><strong>{AiAssistantFormat.number(result.statistics.xMean)} {result.xMetric.unit}</strong></div>
                    <div><small>纵轴均值</small><strong>{AiAssistantFormat.number(result.statistics.yMean)} {result.yMetric.unit}</strong></div>
                    <div><small>排除</small><strong>{result.excluded.length}</strong></div>
                  </div>
                  <p className="axis-ai-method">口径：{result.pairing.label}。95% CI 使用 Fisher z 变换（n≥4）；四象限以当前样本均值划分；相关不代表因果。</p>
                </section>
                {result.statistics.points.length > 0 && <AiQuadrantChart receipt={result} />}
                <details className="axis-ai-sources"><summary>查看纳入数据与来源</summary>
                  {result.statistics.points.map(point => (
                    <div key={point.athleteId}><b>{point.athleteName}</b><span>{point.date} · {point.x} / {point.y}</span></div>
                  ))}
                  {result.excluded.map(point => (
                    <div key={'x-'+point.athleteId} className="excluded"><b>{point.athleteName}</b><span>排除：{point.reason}</span></div>
                  ))}
                </details>
              </>
            )}
            {result?.kind === 'analysis' && window.AiAnalysisView && (
              <window.AiAnalysisView receipt={result} />
            )}
            {result?.kind === 'draft' && <AiDraftCard draft={result} />}
            {result?.kind === 'ui_control' && (
              <section className="axis-ai-card axis-ai-page-command">
                <span>页面控制 · 仅预览</span>
                {result.action === 'preview-age-grouped-cmj-report-batch' ? (
                  <>
                    <strong>{result.organization} · 已配置 {result.matchedCount} 份 CMJ 报告预览</strong>
                    <p>{result.groups.map(group => `${group.label}（${group.athleteIds.length}人）`).join(' · ') || '没有可纳入的年龄组'}</p>
                    {result.warnings.map((warning, index) => <p key={index}>注意：{warning}</p>)}
                    <p>已配置相对 RFD 剖面、归一化时序、对称性和峰值力/RFD 指标图。请在报告页核对后点击“锁定当前模板”，再执行批量导出。</p>
                  </>
                ) : (
                  <>
                    <strong>参照人群预览已打开，匹配 {result.matchedCount} 人</strong>
                    <p>AI 尚未应用任何更改。请在测力台报告的预览框中核对名单，再点击“应用”。</p>
                  </>
                )}
              </section>
            )}
            {!!result?.knowledgeEvidence?.length && result.kind !== 'search' && result.kind !== 'decision_case' && (
              <details className="axis-ai-sources"><summary>查看 Universal / Individual 知识依据</summary>
                {result.knowledgeEvidence.map((row,index)=><div key={row.ref}><b>[{index+1}] {row.governance?.decisionUse==='scientific_evidence'?'已审核证据':row.scope==='personal'?'我的知识':'通用材料'} · {row.title}</b><span>{row.excerpt}</span></div>)}
              </details>
            )}
            {result?.kind === 'decision_case' && window.AiDecisionCaseView && (
              <window.AiDecisionCaseView decisionCase={result} decisionTools={decisionTools} onNavigate={onNavigate} />
            )}
            </>}
            {error && <div className="axis-ai-error">
              {error}
              {agentStatus === 'error' && <button type="button" className="axis-ai-error-action" onClick={retryAgentRestore}>重试恢复</button>}
            </div>}
          </div>
          {panelMode==='chat'&&<footer className="axis-ai-compose">
            <textarea value={text} onChange={e => setText(e.target.value)}
              onKeyDown={e => {
                const composing = e.isComposing || e.nativeEvent?.isComposing || e.nativeEvent?.keyCode === 229;
                if (composing) return;
                if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); run(); }
              }}
              placeholder="继续追问，或说：记住以后趋势默认用折线图…" />
            <button type="button" onClick={() => run()} disabled={!agentReady || busy || !text.trim()}>
              {busy ? '查询中…' : agentReady ? '发送' : agentStatus === 'error' ? '恢复失败' : '恢复中…'}
            </button>
          </footer>}
          <div className="axis-ai-footnote">当前页面：{view} · 读操作零写入 · {{
            conversational:'对话模型',semantic:'语义规划',override:'人工选择',local:'离线规划','semantic-response':'模型解读',
            'grounded-conversation':'本地知识对话','conversational-expert':'运动科学对话',
            'semantic-expert':'专业分析解释','semantic-action':'页面预览','semantic-draft':'未保存草案',
          }[plannerMode] || '本地规划'} · 草案须人工确认 · 不生成诊断</div>
        </aside>,
        document.body
      )}
    </div>
  );
}

Object.assign(window, { AiAssistant, AiQuadrantChart, AiDraftCard });
