// Universal + Individual knowledge workbench. All persistence goes through
// KnowledgeDocumentRepository; uploaded text is evidence, never executable instructions.
const klUseState = React.useState;
const klUseEffect = React.useEffect;
const klUseMemo = React.useMemo;

const KNOWLEDGE_CSS = `
.knowledge-axis-page{width:100%;min-width:0;min-height:100%;padding:24px 26px 72px;background:var(--bg);color:var(--text)}
.kl-hero{display:flex;align-items:flex-end;gap:18px;margin-bottom:18px}.kl-hero>div:first-child{min-width:220px}.kl-hero h1{margin:0;font-size:25px;letter-spacing:-.035em}.kl-hero p{margin:4px 0 0;color:var(--muted);font-size:12px}.kl-tabs{display:flex;gap:3px;padding:3px;border:1px solid var(--border);border-radius:10px;background:var(--panel-2)}.kl-tabs button{min-height:32px;padding:0 11px;border:0;border-radius:7px;background:transparent;color:var(--muted);font-size:11px;cursor:pointer}.kl-tabs button.on{background:var(--panel);color:var(--text);font-weight:750;box-shadow:0 1px 2px rgba(30,40,54,.06)}.kl-actions{margin-left:auto;display:flex;gap:8px}.kl-actions input{width:230px}
.kl-btn,.kl-input,.kl-textarea{border:1px solid var(--border-strong);border-radius:8px;background:var(--panel);color:var(--text);font:inherit}.kl-btn{min-height:36px;padding:0 12px;cursor:pointer}.kl-btn.primary{border-color:#303b4d;background:#303b4d;color:#fff;font-weight:750}.kl-btn.small{min-height:29px;padding:0 9px;font-size:10px}.kl-btn.danger{color:#8a4d4a;border-color:#dec5c3}.kl-btn:disabled{opacity:.45;cursor:not-allowed}.kl-input{min-height:36px;padding:0 10px;outline:0}.kl-textarea{width:100%;min-height:150px;padding:10px;resize:vertical;outline:0}.kl-input:focus,.kl-textarea:focus{border-color:#909aa8;box-shadow:0 0 0 3px rgba(48,59,77,.07)}
.kl-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));overflow:hidden;border:1px solid var(--border);border-radius:11px;background:var(--border);gap:1px}.kl-stat{min-height:72px;padding:11px 13px;background:var(--panel)}.kl-stat span,.kl-stat b,.kl-stat small{display:block}.kl-stat span{color:var(--muted);font-size:9px}.kl-stat b{margin-top:4px;font-size:20px;letter-spacing:-.035em}.kl-stat small{color:var(--muted);font-size:9px}
.kl-layout{display:grid;grid-template-columns:245px minmax(0,1fr) 290px;gap:13px;margin-top:14px;align-items:start}.kl-panel{min-width:0;border:1px solid var(--border);border-radius:12px;background:var(--panel);overflow:hidden}.kl-panel-head{min-height:56px;display:flex;align-items:center;gap:10px;padding:0 14px;border-bottom:1px solid var(--border)}.kl-panel-head h2{margin:0;font-size:13px}.kl-panel-head p{margin:2px 0 0;color:var(--muted);font-size:9.5px}.kl-panel-head .tail{margin-left:auto}.kl-panel-body{padding:14px}.kl-scope{width:100%;padding:11px;border:1px solid var(--border);border-radius:9px;background:#fff;text-align:left;cursor:pointer}.kl-scope+.kl-scope{margin-top:7px}.kl-scope.on{border-color:#aeb6c1;background:#f3f5f7;box-shadow:inset 3px 0 0 #303b4d}.kl-scope b,.kl-scope small{display:block}.kl-scope b{font-size:11px}.kl-scope small{margin-top:3px;color:var(--muted);font-size:9px}.kl-scope em{float:right;color:var(--muted);font:10px ui-monospace,monospace}.kl-boundary{margin-top:13px;padding:10px;border-left:3px solid #aab2bd;background:var(--panel-2);color:var(--muted);font-size:9.5px}
.kl-doc{display:grid;grid-template-columns:34px minmax(0,1fr) 88px 72px;gap:10px;align-items:center;width:100%;min-height:68px;padding:9px 13px;border:0;border-bottom:1px solid var(--border);background:#fff;text-align:left;cursor:pointer}.kl-doc:hover,.kl-doc.on{background:#f7f8f9}.kl-doc:last-child{border-bottom:0}.kl-docmark{width:31px;height:37px;display:grid;place-items:center;border:1px solid var(--border);border-radius:7px;background:var(--panel-2);color:var(--muted);font:8px ui-monospace,monospace}.kl-doc b,.kl-doc small{display:block}.kl-doc b{font-size:11px}.kl-doc small{margin-top:2px;color:var(--muted);font-size:9px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kl-chips{display:flex;gap:4px;margin-top:5px}.kl-chip{display:inline-flex;align-items:center;min-height:21px;padding:0 7px;border:1px solid var(--border);border-radius:999px;color:var(--muted);font-size:8.5px;font-style:normal}.kl-chip.personal{background:#e9ecf1;color:#3f4a5b}.kl-state{font-size:9px;color:#39785c}.kl-empty{min-height:220px;display:grid;place-items:center;padding:30px;text-align:center;color:var(--muted);font-size:10px}.kl-empty b{display:block;color:var(--text);font-size:12px}
.kl-detail h3{margin:0;font-size:15px;line-height:1.35}.kl-detail>p{color:var(--muted);font-size:10px}.kl-dl{display:grid;gap:8px;margin:14px 0 0}.kl-dl div{display:grid;grid-template-columns:70px minmax(0,1fr);gap:7px;font-size:9.5px}.kl-dl dt{color:var(--muted)}.kl-dl dd{margin:0;overflow-wrap:anywhere}.kl-excerpt{margin-top:14px;padding:11px;border:1px solid var(--border);border-radius:9px;background:var(--panel-2);color:var(--muted);font-size:9.5px;white-space:pre-wrap;max-height:230px;overflow:auto}.kl-detail-actions{display:flex;gap:7px;margin-top:12px}
.kl-search-layout{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:13px;margin-top:14px}.kl-searchbox{display:flex;gap:8px}.kl-searchbox input{flex:1}.kl-result{padding:13px 14px;border-bottom:1px solid var(--border)}.kl-result:last-child{border-bottom:0}.kl-result header{display:flex;gap:8px;align-items:center}.kl-result header b{font-size:11.5px}.kl-result header span{margin-left:auto;color:var(--muted);font-size:9px}.kl-result p{margin:7px 0;color:#596576;font-size:10px;white-space:pre-wrap}.kl-citation{font:8.5px ui-monospace,monospace;color:var(--muted);overflow-wrap:anywhere}
.kl-index-grid{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin-top:14px}.kl-index-card{padding:16px}.kl-index-card h3{margin:0;font-size:13px}.kl-index-card p{color:var(--muted);font-size:10px}.kl-index-card code{display:block;margin-top:10px;padding:10px;border-radius:8px;background:var(--panel-2);font-size:9px;white-space:pre-wrap}
.kl-overlay{position:fixed;inset:0;z-index:120;display:grid;place-items:center;padding:24px;background:rgba(29,37,49,.36);backdrop-filter:blur(3px)}.kl-dialog{width:min(680px,calc(100vw - 48px));max-height:calc(100vh - 48px);overflow:auto;border:1px solid var(--border-strong);border-radius:15px;background:#fff;box-shadow:0 24px 80px rgba(24,32,44,.18)}.kl-dialog>header,.kl-dialog>footer{display:flex;align-items:center;gap:8px;padding:14px 16px}.kl-dialog>header{border-bottom:1px solid var(--border)}.kl-dialog>header button{margin-left:auto}.kl-dialog>footer{justify-content:flex-end;border-top:1px solid var(--border)}.kl-form{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:16px}.kl-field{display:grid;gap:5px;color:#4e5969;font-size:10px;font-weight:650}.kl-field.wide{grid-column:1/-1}.kl-file{padding:13px;border:1px dashed #aeb7c2;border-radius:10px;background:#f7f8f9}.kl-file input{display:block;margin-top:7px}.kl-note{grid-column:1/-1;padding:10px;border-left:3px solid #aab2bd;background:var(--panel-2);color:var(--muted);font-size:9.5px}.kl-error{padding:9px 11px;border:1px solid #e2c5c3;border-radius:8px;background:#f8eeee;color:#8c4f4c;font-size:10px}
.kl-ingest-summary{grid-column:1/-1;padding:11px;border:1px solid var(--border);border-radius:9px;background:#f7f8f9;color:#596576;font-size:9.5px;white-space:pre-wrap}.kl-ingest-summary b{display:block;margin-bottom:5px;color:#334052}.kl-ingest-warning{display:block;margin-top:6px;color:#8a6843}
@media(max-width:1350px){.kl-layout{grid-template-columns:220px minmax(0,1fr)}.kl-layout>.kl-panel:last-child{grid-column:1/-1}.kl-detail{display:grid;grid-template-columns:1fr 1fr;gap:0 18px}.kl-detail .kl-excerpt{margin-top:0}.kl-detail-actions{grid-column:1/-1}.kl-search-layout{grid-template-columns:1fr}}
@media(max-width:900px){.knowledge-axis-page{padding:18px 12px 72px}.kl-hero{align-items:flex-start;flex-direction:column}.kl-actions{width:100%;margin:0}.kl-actions input{flex:1;width:auto}.kl-stats{grid-template-columns:1fr 1fr}.kl-layout,.kl-index-grid{grid-template-columns:1fr}.kl-layout>.kl-panel:last-child{grid-column:auto}.kl-detail{display:block}.kl-form{grid-template-columns:1fr}.kl-field.wide,.kl-note{grid-column:auto}}
`;

function KnowledgeLibraryPage({ repository, retrieval, ownerId, ready }) {
  const [tab, setTab] = klUseState('documents');
  const [scope, setScope] = klUseState('all');
  const [documents, setDocuments] = klUseState([]);
  const [summary, setSummary] = klUseState({ universalDocuments:0, personalDocuments:0, enabledChunks:0, needsAttention:0 });
  const [selectedId, setSelectedId] = klUseState('');
  const [query, setQuery] = klUseState('');
  const [results, setResults] = klUseState([]);
  const [busy, setBusy] = klUseState(false);
  const [error, setError] = klUseState('');
  const [uploadOpen, setUploadOpen] = klUseState(false);
  const [form, setForm] = klUseState({ title:'', sourceLabel:'个人笔记', tags:'', content:'', sections:[], summary:'', warnings:[], fileName:'', mimeType:'text/plain', provenance:null });
  const ingestor = klUseMemo(() => window.KnowledgeDocumentIngestion?.create?.() || null, []);

  const refresh = React.useCallback(async () => {
    if (!repository) return;
    setError('');
    try {
      if (ready) {
        const state = await ready;
        if (state && state.ok === false) throw state.error;
      }
      const rows = await repository.list({});
      setDocuments(rows);
      setSummary(await repository.summary(ownerId));
      setSelectedId(current => current && rows.some(row => row.id === current) ? current : (rows[0]?.id || ''));
    } catch (cause) {
      setError(cause?.message || '知识库暂时不可用。');
    }
  }, [repository, ownerId, ready]);

  klUseEffect(() => { refresh(); }, [refresh]);

  const visible = klUseMemo(() => documents.filter(documentRecord => {
    if (scope !== 'all' && documentRecord.scope !== scope) return false;
    if (documentRecord.scope === 'personal' && String(documentRecord.ownerId) !== String(ownerId)) return false;
    const needle = query.trim().toLowerCase();
    if (!needle) return true;
    return [documentRecord.title, documentRecord.author, documentRecord.source?.label, ...(documentRecord.tags || [])]
      .join(' ').toLowerCase().includes(needle);
  }), [documents, scope, query, ownerId]);
  const selected = documents.find(row => row.id === selectedId) || null;

  const runSearch = async () => {
    if (!query.trim() || !retrieval) return;
    setBusy(true); setError('');
    try {
      const receipt = await retrieval.search({
        query,
        scopes: scope === 'all' ? ['universal','personal'] : [scope],
        ownerId,
        limit: 12,
      });
      setResults(receipt.results || []);
      setTab('search');
    } catch (cause) {
      setError(cause?.message || '检索失败。');
    } finally {
      setBusy(false);
    }
  };

  const readFile = async event => {
    const file = event.target.files?.[0];
    if (!file) return;
    if (!ingestor) { setError('文档摄取模块尚未就绪，请刷新页面。'); return; }
    setBusy(true); setError('');
    try {
      const result = await ingestor.ingestFile(file);
      setForm(current => ({
        ...current,
        title:current.title || result.title,
        sourceLabel:current.sourceLabel === '个人笔记' ? result.source.fileName : current.sourceLabel,
        tags:current.tags || result.suggestedTags.join('，'),
        content:result.content,
        sections:result.sections,
        summary:result.summary,
        warnings:result.warnings,
        fileName:result.source.fileName,
        mimeType:result.source.mimeType,
        provenance:result.provenance,
      }));
    } catch (cause) {
      setError('文件读取失败：' + (cause?.message || cause));
    } finally {
      setBusy(false);
    }
  };

  const savePersonal = async () => {
    if (!form.title.trim() || !form.content.trim()) { setError('标题和可检索正文不能为空。'); return; }
    setBusy(true); setError('');
    try {
      const saved = await repository.savePersonal({
        title:form.title,
        content:form.content,
        sections:form.sections,
        tags:form.tags.split(/[,，]/).map(value => value.trim()).filter(Boolean),
        topics:form.tags.split(/[,，]/).map(value => value.trim()).filter(Boolean),
        source:{
          label:form.sourceLabel || '个人资料',
          fileName:form.fileName,
          mimeType:form.mimeType,
          copyrightStatus:'user-provided-private',
        },
        provenance:form.provenance || {ingestion:'manual',parserVersion:'knowledge-text-v1'},
      }, ownerId);
      setUploadOpen(false);
      setForm({ title:'', sourceLabel:'个人笔记', tags:'', content:'', sections:[], summary:'', warnings:[], fileName:'', mimeType:'text/plain', provenance:null });
      await refresh();
      setSelectedId(saved.id);
    } catch (cause) {
      setError(cause?.message || '资料保存失败。');
    } finally {
      setBusy(false);
    }
  };

  const toggleSelected = async () => {
    if (!selected || selected.scope !== 'personal') return;
    setBusy(true); setError('');
    try {
      await repository.setEnabled(selected.id, selected.status !== 'enabled', ownerId);
      await refresh();
    } catch (cause) {
      setError(cause?.message || '状态更新失败。');
    } finally { setBusy(false); }
  };

  const removeSelected = async () => {
    if (!selected || selected.scope !== 'personal') return;
    if (!window.confirm(`彻底删除“${selected.title}”及其全部检索分块？`)) return;
    setBusy(true); setError('');
    try {
      await repository.removePersonal(selected.id, ownerId);
      await refresh();
    } catch (cause) {
      setError(cause?.message || '删除失败。');
    } finally { setBusy(false); }
  };

  return (
    <main className="knowledge-axis-page" data-ai-knowledge-page data-knowledge-owner={ownerId}>
      <style>{KNOWLEDGE_CSS}</style>
      <header className="kl-hero">
        <div><h1>知识库</h1><p>让解释可追溯，而不是让文档替 AI 作决定。</p></div>
        <nav className="kl-tabs" aria-label="知识库视图">
          <button className={tab==='documents'?'on':''} onClick={() => setTab('documents')}>资料</button>
          <button className={tab==='search'?'on':''} onClick={() => setTab('search')}>检索测试</button>
          <button className={tab==='index'?'on':''} onClick={() => setTab('index')}>版本与索引</button>
        </nav>
        <div className="kl-actions">
          <input className="kl-input" value={query} onChange={event => setQuery(event.target.value)}
            onKeyDown={event => { if (event.key === 'Enter') runSearch(); }} placeholder="搜索资料或测试一次检索" aria-label="搜索知识库" />
          <button className="kl-btn" onClick={runSearch} disabled={busy || !query.trim()}>搜索</button>
          <button className="kl-btn primary" onClick={() => setUploadOpen(true)}>添加个人资料</button>
        </div>
      </header>

      {error && <div className="kl-error" role="alert">{error}</div>}
      <section className="kl-stats" aria-label="知识库状态">
        <div className="kl-stat"><span>通用知识</span><b>{summary.universalDocuments}</b><small>开发者维护 · 运行时只读</small></div>
        <div className="kl-stat"><span>我的知识</span><b>{summary.personalDocuments}</b><small>仅当前 owner 可见</small></div>
        <div className="kl-stat"><span>可检索分块</span><b>{summary.enabledChunks}</b><small>当前 lexical · vector 接口预留</small></div>
        <div className="kl-stat"><span>需要处理</span><b>{summary.needsAttention}</b><small>草稿或解析失败</small></div>
      </section>

      {tab === 'documents' && (
        <div className="kl-layout">
          <aside className="kl-panel">
            <header className="kl-panel-head"><div><h2>知识范围</h2><p>业务数据不存入知识库</p></div></header>
            <div className="kl-panel-body">
              {[['all','全部资料'],['universal','通用知识'],['personal','我的知识']].map(([id,label]) => (
                <button key={id} className={`kl-scope ${scope===id?'on':''}`} onClick={() => setScope(id)}>
                  <em>{id==='all' ? summary.universalDocuments+summary.personalDocuments : (id==='universal'?summary.universalDocuments:summary.personalDocuments)}</em>
                  <b>{label}</b><small>{id==='universal'?'开发者维护的方法与解释原则':id==='personal'?'个人笔记与自有材料':'结果保留 Universal / Individual 标签'}</small>
                </button>
              ))}
              <div className="kl-boundary"><b>第一性边界</b><br/>运动员测试、训练、损伤和康复记录仍在业务仓库；知识库只保存解释材料。</div>
            </div>
          </aside>
          <section className="kl-panel">
            <header className="kl-panel-head"><div><h2>资料清单</h2><p>来源、版本、范围与指纹可追溯</p></div><span className="tail kl-chip">{visible.length} 项</span></header>
            {!visible.length && <div className="kl-empty"><span><b>当前范围暂无资料</b>添加个人正文，或切换到“全部资料”。</span></div>}
            {visible.map(documentRecord => (
              <button key={documentRecord.id} className={`kl-doc ${selectedId===documentRecord.id?'on':''}`}
                onClick={() => setSelectedId(documentRecord.id)} data-knowledge-document-id={documentRecord.id} data-knowledge-scope={documentRecord.scope}>
                <span className="kl-docmark">{documentRecord.source?.fileName ? String(documentRecord.source.fileName).split('.').pop().toUpperCase().slice(0,5) : 'DOC'}</span>
                <span><b>{documentRecord.title}</b><small>{documentRecord.source?.label || documentRecord.author || '未标来源'}</small>
                  <span className="kl-chips"><i className={`kl-chip ${documentRecord.scope==='personal'?'personal':''}`}>{documentRecord.scope==='personal'?'我的知识':'通用'}</i>{(documentRecord.tags||[]).slice(0,1).map(tag=><i key={tag} className="kl-chip">{tag}</i>)}</span>
                </span>
                <span><small>版本</small><b>{documentRecord.version}</b></span>
                <span className="kl-state">{documentRecord.status==='enabled'?'可检索':documentRecord.status}</span>
              </button>
            ))}
          </section>
          <aside className="kl-panel">
            <header className="kl-panel-head"><div><h2>来源详情</h2><p>当前选中资料</p></div>{selected&&<span className="tail kl-chip">{selected.status}</span>}</header>
            <div className="kl-panel-body kl-detail">
              {!selected ? <div className="kl-empty"><span><b>未选择资料</b>从清单选择一项。</span></div> : <>
                <div><h3>{selected.title}</h3><p>{selected.scope==='universal'?'通用知识 · 开发者维护，运行时只读。':'我的知识 · 当前 owner 可管理。'}</p>
                  <dl className="kl-dl">
                    <div><dt>来源</dt><dd>{selected.source?.label || '—'}</dd></div>
                    <div><dt>版本</dt><dd>{selected.version}</dd></div>
                    <div><dt>主题</dt><dd>{[...(selected.topics||[]),...(selected.tags||[])].join('、') || '—'}</dd></div>
                    <div><dt>文件</dt><dd>{selected.source?.fileName || '开发者知识包'}</dd></div>
                    <div><dt>权限</dt><dd>{selected.scope==='personal'?'仅当前 owner':'全体用户只读'}</dd></div>
                    <div><dt>指纹</dt><dd>{selected.contentFingerprint}</dd></div>
                  </dl>
                </div>
                <div className="kl-excerpt"><b>正文预览</b>{'\n'}{String(selected.content||'').slice(0,1200)}</div>
                {selected.scope==='personal'&&<div className="kl-detail-actions"><button className="kl-btn small" onClick={toggleSelected} disabled={busy}>{selected.status==='enabled'?'停用检索':'启用检索'}</button><button className="kl-btn small danger" onClick={removeSelected} disabled={busy}>彻底删除</button></div>}
              </>}
            </div>
          </aside>
        </div>
      )}

      {tab === 'search' && (
        <div className="kl-search-layout">
          <section className="kl-panel">
            <header className="kl-panel-head"><div><h2>检索结果</h2><p>命中片段不被当作系统指令</p></div><span className="tail kl-chip">{results.length} 条</span></header>
            {!results.length&&<div className="kl-empty"><span><b>输入问题测试检索</b>例如：CMJ 下降时先核对什么？</span></div>}
            {results.map(row=><article className="kl-result" key={row.ref} data-knowledge-citation={row.citation?.chunkId}>
              <header><span className={`kl-chip ${row.scope==='personal'?'personal':''}`}>{row.scope==='personal'?'我的知识':'通用'}</span><b>{row.title}</b><span>相关度 {row.relevance?.combined}</span></header>
              <p>{row.excerpt}</p><div className="kl-citation">{row.citation?.documentId} · {row.citation?.locator?.section || `page ${row.citation?.locator?.page || '—'}`} · {row.citation?.contentFingerprint}</div>
            </article>)}
          </section>
          <aside className="kl-panel"><header className="kl-panel-head"><div><h2>检索口径</h2><p>当前实现与扩展接口</p></div></header><div className="kl-panel-body kl-detail"><div className="kl-boundary">当前使用确定性 lexical 检索；KnowledgeRetrieval 已预留 vectorAdapter，未来接入 embedding 不需要修改页面或文档仓库。</div><dl className="kl-dl"><div><dt>范围</dt><dd>{scope==='all'?'Universal + Individual':scope}</dd></div><div><dt>Owner</dt><dd>{ownerId}</dd></div><div><dt>执行权限</dt><dd>无 · 只读证据</dd></div></dl></div></aside>
        </div>
      )}

      {tab === 'index' && (
        <div className="kl-index-grid">
          <section className="kl-panel kl-index-card"><h3>文档契约 · knowledge-document-v1</h3><p>保存来源范围、owner、版本、正文指纹、分块定位与版权状态。Universal 和 Individual 使用同一深层接口，但权限策略不同。</p><code>{`scope: universal | personal\ncontentRole: untrusted_evidence\noperationalAthleteData: excluded`}</code></section>
          <section className="kl-panel kl-index-card"><h3>检索接口 · lexical → hybrid</h3><p>当前本地模式无需云服务。未来 vectorAdapter 可接本地向量或 Supabase pgvector；引用契约保持不变。</p><code>{`search({ query, scopes, ownerId })\n→ excerpt + citation + trust\nexecutableInstructions: false`}</code></section>
          <section className="kl-panel kl-index-card"><h3>支持的本地导入</h3><p>PDF 按页、DOCX 正文、XLS/XLSX 按工作表，以及 CSV/TSV、TXT、Markdown、JSON。解析后先生成确定性摘要与主题，再保存正文和定位信息。</p></section>
          <section className="kl-panel kl-index-card"><h3>与 Decision Case 的关系</h3><p>知识命中进入“证据”层；运动员记录来自独立业务仓库；统计由确定性模块计算；最终动作仍需用户确认。</p></section>
        </div>
      )}

      {uploadOpen&&<div className="kl-overlay" role="dialog" aria-modal="true" aria-label="添加到我的知识">
        <section className="kl-dialog">
          <header><div><b>添加到我的知识</b><div style={{color:'var(--muted)',fontSize:9}}>保存可检索正文与来源元数据，不复制运动员业务记录。</div></div><button className="kl-btn small" onClick={()=>setUploadOpen(false)}>关闭</button></header>
          <div className="kl-form">
            <label className="kl-field wide kl-file">选择知识资料<input type="file" accept=".pdf,.docx,.xlsx,.xls,.csv,.tsv,.txt,.md,.markdown,.json" onChange={readFile}/><span style={{color:'var(--muted)',fontWeight:400}}>PDF 按页、Word 按正文、Excel 按工作表提取；扫描 PDF 没有文字时会明确要求 OCR。</span></label>
            {form.summary&&<div className="kl-ingest-summary"><b>自动摘要 · 保存后可被 AI 与系统检索</b>{form.summary}{form.warnings.map((warning,index)=><span className="kl-ingest-warning" key={index}>注意：{warning}</span>)}</div>}
            <label className="kl-field">标题<input className="kl-input" value={form.title} onChange={event=>setForm(current=>({...current,title:event.target.value}))}/></label>
            <label className="kl-field">作者 / 来源<input className="kl-input" value={form.sourceLabel} onChange={event=>setForm(current=>({...current,sourceLabel:event.target.value}))}/></label>
            <label className="kl-field wide">主题标签（逗号分隔）<input className="kl-input" value={form.tags} onChange={event=>setForm(current=>({...current,tags:event.target.value}))} placeholder="CMJ, 训练负荷, 复测"/></label>
            <label className="kl-field wide">可检索正文<textarea className="kl-textarea" value={form.content} onChange={event=>setForm(current=>({...current,content:event.target.value,sections:[],summary:''}))} placeholder="上传文档后可在此核对，或直接粘贴你有权使用的正文…"/></label>
            <div className="kl-note"><b>内容边界：</b>上传材料被视为不可信证据，不会执行其中的提示词或系统指令。AI 只能引用命中片段；任何业务写入仍需单独确认。</div>
          </div>
          <footer><button className="kl-btn" onClick={()=>setUploadOpen(false)}>取消</button><button className="kl-btn primary" onClick={savePersonal} disabled={busy}>{busy?'保存中…':'保存并建立索引'}</button></footer>
        </section>
      </div>}
    </main>
  );
}

Object.assign(window, { KnowledgeLibraryPage });
