import { useCallback, useEffect, useRef, useState } from "react";
import {
  Upload,
  FileUp,
  FolderUp,
  Plus,
  Search,
  Network,
  Database,
  FileText,
  Scissors,
  Sparkles,
  Share2,
  CheckCircle2,
  XCircle,
  Loader2,
  Trash2,
  Lock,
  BookOpen,
  Link2,
  RefreshCw,
  Pencil,
  Save,
  X,
  Waypoints,
  type LucideIcon,
} from "lucide-react";
import {
  ingestKb,
  ingestFile,
  streamIngest,
  searchKb,
  graphKb,
  listKb,
  createKb,
  listVault,
  getDoc,
  listLinks,
  saveNote,
  type IngestEvent,
  type KbHit,
  type Triple,
  type KbInfo,
  type VaultDoc,
  type DocLink,
  type Identity,
} from "../lib/api";
import { GraphView } from "../components/GraphView";
import { Markdown } from "../components/Markdown";
import { Button, Input, Textarea, Select, Badge, Tabs, EmptyState, cn, useToast, type TabDef } from "../ui";

interface IngestLog {
  t: string;
  msg: string;
  ok: boolean;
}
interface Step {
  stage: string;
  msg: string;
}
interface Progress {
  active: boolean;
  stage: string;
  chunks: string[];
  preview?: string;
  triples: Triple[];
  error?: string;
  steps: Step[];
  vecDone?: number;
  vecTotal?: number;
}
interface FileJob {
  id: string;
  name: string;
  status: string;
  error?: string;
}
type KbTab = "ingest" | "vault" | "search" | "graph";

const STAGE: Record<string, { icon: LucideIcon; label: string }> = {
  解析: { icon: Upload, label: "解析文件" },
  解析完成: { icon: FileText, label: "解析完成" },
  切块: { icon: Scissors, label: "切块" },
  向量化: { icon: Sparkles, label: "向量化" },
  写Milvus: { icon: Database, label: "写入向量库" },
  写Bleve: { icon: Search, label: "写入全文索引" },
  抽实体: { icon: Network, label: "抽取知识" },
  写Neo4j: { icon: Share2, label: "写入图谱" },
  完成: { icon: CheckCircle2, label: "完成" },
  失败: { icon: XCircle, label: "失败" },
};
const KIND_LABEL: Record<string, string> = { folder: "文件夹", project: "项目", case: "案件", general: "通用" };

function stageToStatus(stage: string): string {
  if (stage === "解析" || stage === "解析完成") return "解析";
  if (stage === "切块" || stage === "向量化") return "向量化";
  if (stage === "写Milvus" || stage === "写Bleve") return "写入";
  if (stage === "抽实体" || stage === "写Neo4j") return "抽取";
  if (stage === "完成") return "完成";
  if (stage === "失败" || stage === "连接中断") return "失败";
  return "排队";
}
function dedupTriples(ts: Triple[]): Triple[] {
  const seen = new Set<string>();
  const out: Triple[] = [];
  for (const t of ts) {
    const k = `${t.s}|${t.p}|${t.o}`;
    if (!seen.has(k)) {
      seen.add(k);
      out.push(t);
    }
  }
  return out;
}

// 知识库：owner 隔离 + 项目/案件/文件夹组织；Tab 分（入库 / 文库(Obsidian 式) / 检索 / 图谱）。
export function KbView({ identity }: { identity: Identity }) {
  const toast = useToast();
  const [kbs, setKbs] = useState<KbInfo[]>([]);
  const [kb, setKb] = useState("default");
  const [tab, setTab] = useState<KbTab>("ingest");
  const [creating, setCreating] = useState(false);
  const [newName, setNewName] = useState("");
  const [newKind, setNewKind] = useState("project");

  const [text, setText] = useState("");
  const [logs, setLogs] = useState<IngestLog[]>([]);
  const [prog, setProg] = useState<Progress | null>(null);
  const [files, setFiles] = useState<FileJob[]>([]);
  const fileRef = useRef<HTMLInputElement>(null);
  const folderRef = useRef<HTMLInputElement>(null);

  const [q, setQ] = useState("");
  const [topK, setTopK] = useState(5);
  const [hits, setHits] = useState<KbHit[] | null>(null);
  const [searching, setSearching] = useState(false);
  const [graph, setGraph] = useState<Triple[] | null>(null);
  const [dragOver, setDragOver] = useState(false);

  useEffect(() => {
    if (folderRef.current) folderRef.current.setAttribute("webkitdirectory", "");
  }, []);

  const refreshKbs = useCallback(async () => {
    try {
      const list = await listKb(identity);
      setKbs(list);
      setKb((cur) => (list.length && !list.some((k) => k.name === cur) ? list[0].name : cur));
    } catch {
      /* 降级用默认库 */
    }
  }, [identity]);
  useEffect(() => {
    void refreshKbs();
  }, [refreshKbs]);

  const onCreate = async () => {
    const name = newName.trim();
    if (!name) return;
    try {
      const info = await createKb(identity, name, newKind);
      setKbs((ks) => [...ks.filter((k) => k.name !== info.name), info]);
      setKb(info.name);
      setCreating(false);
      setNewName("");
      toast.push("success", `已创建知识库「${info.name}」`);
    } catch (e) {
      toast.push("error", (e as Error).message);
    }
  };

  const stamp = () => new Date().toLocaleTimeString();
  const busy = (prog?.active ?? false) || files.some((f) => f.status !== "完成" && f.status !== "失败");

  const follow = (job: string, label: string, fid?: string) => {
    setProg({ active: true, stage: "提交", chunks: [], triples: [], steps: [{ stage: "提交", msg: label }] });
    streamIngest(
      job,
      (ev: IngestEvent) => {
        if (fid) setFiles((fs) => fs.map((f) => (f.id === fid ? { ...f, status: stageToStatus(ev.stage), error: ev.error } : f)));
        setProg((p) => {
          const base: Progress = p ?? { active: true, stage: "提交", chunks: [], triples: [], steps: [] };
          const steps = [...base.steps];
          const i = steps.findIndex((s) => s.stage === ev.stage);
          const msg = ev.msg ?? (i >= 0 ? steps[i].msg : "");
          if (i >= 0) steps[i] = { stage: ev.stage, msg };
          else steps.push({ stage: ev.stage, msg });
          return {
            active: ev.stage !== "完成" && ev.stage !== "失败",
            stage: ev.stage,
            chunks: ev.chunks ?? base.chunks,
            preview: ev.preview ?? base.preview,
            triples: ev.triples?.length ? dedupTriples([...base.triples, ...ev.triples]) : base.triples,
            error: ev.error,
            steps,
            vecDone: ev.stage === "向量化" ? ev.done : base.vecDone,
            vecTotal: ev.stage === "向量化" ? ev.total : base.vecTotal,
          };
        });
      },
      () => {
        setProg((p) => {
          const ok = p?.stage !== "失败";
          if (fid) setFiles((fs) => fs.map((f) => (f.id === fid ? { ...f, status: ok ? "完成" : "失败" } : f)));
          else setLogs((l) => [{ t: stamp(), msg: ok ? `${label}：入库完成` : `${label}：${p?.error ?? "失败"}`, ok }, ...l]);
          if (ok) void onGraph();
          return p ? { ...p, active: false } : null;
        });
      },
      () => setProg((p) => (p ? { ...p, active: false, stage: "连接中断" } : null)),
    );
  };

  const onIngest = async () => {
    if (!text.trim()) return;
    try {
      const job = await ingestKb(identity, kb, text);
      setText("");
      follow(job, "文本");
    } catch (e) {
      setLogs((l) => [{ t: stamp(), msg: (e as Error).message, ok: false }, ...l]);
    }
  };

  const ingestFiles = (list: FileList | File[] | null | undefined) => {
    const arr = Array.from(list ?? []);
    arr.forEach((file, idx) => {
      const fid = `${file.name}-${idx}-${stamp()}-${Math.round(file.size)}`;
      setFiles((fs) => [...fs, { id: fid, name: file.name, status: "排队" }]);
      ingestFile(identity, kb, file)
        .then((job) => follow(job, file.name, fid))
        .catch((e) => setFiles((fs) => fs.map((f) => (f.id === fid ? { ...f, status: "失败", error: (e as Error).message } : f))));
    });
  };

  const onSearch = async () => {
    if (!q.trim()) return;
    setSearching(true);
    try {
      setHits(await searchKb(identity, kb, q, topK));
    } catch (e) {
      toast.push("error", (e as Error).message);
      setHits(null);
    } finally {
      setSearching(false);
    }
  };

  const onGraph = useCallback(async () => {
    try {
      setGraph(await graphKb(identity, kb));
    } catch (e) {
      toast.push("error", (e as Error).message);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [identity, kb]);

  const vecPct = prog?.vecTotal ? Math.round(((prog.vecDone ?? 0) / prog.vecTotal) * 100) : 0;
  const graphData = graph ?? prog?.triples ?? null;
  const kbOptions = kbs.some((k) => k.name === kb) ? kbs : [{ name: kb, kind: "general" }, ...kbs];
  const doneCount = files.filter((f) => f.status === "完成").length;
  const failCount = files.filter((f) => f.status === "失败").length;

  const tabs: TabDef<KbTab>[] = [
    { key: "ingest", label: "入库" },
    { key: "vault", label: "文库" },
    { key: "search", label: "检索" },
    { key: "graph", label: "图谱" },
  ];

  return (
    <div className="flex h-full flex-col">
      {/* 知识库选择 / 新建 + 隔离徽标 */}
      <div className="flex flex-wrap items-center gap-2 border-b border-line bg-ink-900 px-4 py-2">
        <span className="text-sm font-semibold text-slate-300">知识库</span>
        <Select className="h-8 w-48" value={kb} onChange={(e) => setKb(e.target.value)}>
          {kbOptions.map((k) => (
            <option key={k.name} value={k.name}>
              {k.name}（{KIND_LABEL[k.kind] ?? k.kind}）
            </option>
          ))}
        </Select>
        {!creating ? (
          <Button size="sm" icon={Plus} onClick={() => setCreating(true)}>
            新建
          </Button>
        ) : (
          <div className="flex items-center gap-1.5">
            <Input className="h-8 w-36" value={newName} onChange={(e) => setNewName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && onCreate()} placeholder="如 案件-2024-001" autoFocus />
            <Select className="h-8 w-24" value={newKind} onChange={(e) => setNewKind(e.target.value)}>
              <option value="project">项目</option>
              <option value="case">案件</option>
              <option value="folder">文件夹</option>
              <option value="general">通用</option>
            </Select>
            <Button size="sm" variant="primary" onClick={onCreate}>
              创建
            </Button>
            <Button size="sm" variant="ghost" onClick={() => setCreating(false)}>
              取消
            </Button>
          </div>
        )}
        <span className="ml-auto flex items-center gap-1 text-[11px] text-slate-500">
          <Lock className="h-3 w-3" /> 仅 {identity.userId} 可见（owner 隔离）
        </span>
      </div>

      {/* Tab 条 */}
      <div className="flex items-center border-b border-line px-3">
        <Tabs tabs={tabs} value={tab} onChange={setTab} />
        {busy && <span className="ml-2 flex items-center gap-1 text-[11px] text-accent-400"><Loader2 className="h-3 w-3 animate-spin" /> 入库中</span>}
      </div>

      <div className="min-h-0 flex-1 overflow-hidden">
        {tab === "ingest" && (
          <div className="h-full overflow-y-auto p-4">
            <h3 className="mb-2 text-xs font-semibold text-slate-400">入库到「{kb}」</h3>
            <div
              onDragOver={(e) => {
                e.preventDefault();
                setDragOver(true);
              }}
              onDragLeave={() => setDragOver(false)}
              onDrop={(e) => {
                e.preventDefault();
                setDragOver(false);
                ingestFiles(e.dataTransfer.files);
              }}
              className={cn("relative max-w-2xl rounded-md", dragOver && "ring-2 ring-brand")}
            >
              <Textarea className="h-20 w-full resize-none" value={text} onChange={(e) => setText(e.target.value)} placeholder="每行一条知识，或把一批文件 / 整个文件夹拖到这里（笔记支持 [[双链]]）" />
              {dragOver && <div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-md bg-ink-950/85 text-xs font-medium text-brand-400">松手以批量入库</div>}
            </div>
            <div className="mt-2 flex flex-wrap items-center gap-2">
              <Button variant="primary" size="sm" icon={Upload} onClick={onIngest} disabled={busy || !text.trim()}>
                入库文本
              </Button>
              <Button size="sm" icon={FileUp} onClick={() => fileRef.current?.click()} disabled={busy}>
                选择文件
              </Button>
              <Button size="sm" icon={FolderUp} onClick={() => folderRef.current?.click()} disabled={busy}>
                选择文件夹
              </Button>
              <input ref={fileRef} type="file" multiple accept=".txt,.md,.csv,.docx,.xlsx,.pdf" onChange={(e) => ingestFiles(e.target.files)} className="hidden" />
              <input ref={folderRef} type="file" multiple onChange={(e) => ingestFiles(e.target.files)} className="hidden" />
            </div>
            <span className="mt-1 block text-[10px] text-slate-500">支持批量 txt/md/csv/docx/xlsx/pdf；可整文件夹拖入（docx/xlsx/pdf 经 mcp-py 解析）</span>

            <div className="mt-3 grid max-w-3xl gap-3 lg:grid-cols-2">
              {files.length > 0 && (
                <div className="rounded-lg border border-line bg-ink-850 p-2.5">
                  <div className="mb-1.5 flex items-center text-[11px] text-slate-400">
                    <span>批量入库 {files.length} 个</span>
                    <span className="ml-auto text-slate-500">完成 {doneCount} · 失败 {failCount}</span>
                    <button className="ml-2 text-slate-600 hover:text-slate-400" onClick={() => setFiles([])} title="清空列表">
                      <Trash2 className="h-3.5 w-3.5" />
                    </button>
                  </div>
                  <ul className="max-h-48 space-y-1 overflow-auto">
                    {files.map((f) => (
                      <li key={f.id} className="flex items-center gap-2 rounded bg-ink-900 px-2 py-1 text-[11px]">
                        <FileText className="h-3.5 w-3.5 shrink-0 text-slate-500" />
                        <span className="truncate text-slate-300" title={f.name}>{f.name}</span>
                        <span className="ml-auto"><FileStatus status={f.status} /></span>
                      </li>
                    ))}
                  </ul>
                </div>
              )}
              {prog && (
                <div className={files.length > 0 ? "" : "lg:col-span-2"}>
                  <Timeline prog={prog} vecPct={vecPct} />
                </div>
              )}
            </div>

            {logs.length > 0 && (
              <ul className="mt-3 max-w-2xl space-y-1">
                {logs.map((l, i) => (
                  <li key={i} className={cn("text-xs", l.ok ? "text-success" : "text-danger")}>
                    <span className="text-slate-600">{l.t}</span> {l.ok ? "✓" : "✗"} {l.msg}
                  </li>
                ))}
              </ul>
            )}
          </div>
        )}

        {tab === "vault" && <VaultPanel identity={identity} kb={kb} />}

        {tab === "search" && (
          <div className="h-full overflow-y-auto p-4">
            <h3 className="mb-2 text-xs font-semibold text-slate-400">检索调试台 · 「{kb}」（混合召回 + rerank）</h3>
            <div className="flex max-w-3xl gap-2">
              <Input className="flex-1" value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => e.key === "Enter" && onSearch()} placeholder="输入查询，语义召回相关片段…" />
              <Input type="number" className="w-16" value={topK} min={1} max={20} onChange={(e) => setTopK(Number(e.target.value))} title="TopK" />
              <Button variant="primary" size="md" icon={Search} onClick={onSearch} disabled={searching || !q.trim()}>
                {searching ? "检索中…" : "检索"}
              </Button>
            </div>
            <ul className="mt-3 max-w-3xl space-y-2">
              {hits === null && <li className="text-xs text-slate-600">输入查询后展示命中片段与分数。</li>}
              {hits !== null && hits.length === 0 && <li className="text-xs text-slate-600">无命中（该库为空或 RAG 未配置）。</li>}
              {hits?.map((h, i) => (
                <li key={i} className="rounded-lg border border-line bg-ink-850 p-2">
                  <div className="mb-1 flex items-center gap-2 text-[10px]">
                    <Badge tone="accent">混合检索</Badge>
                    <span className="text-slate-600">#{i + 1}</span>
                    <span className="ml-auto font-mono text-brand-400">{h.score.toFixed(3)}</span>
                  </div>
                  <div className="text-xs text-slate-300">{h.text}</div>
                </li>
              ))}
            </ul>
          </div>
        )}

        {tab === "graph" && (
          <div className="h-full overflow-y-auto p-4">
            <div className="mb-2 flex max-w-3xl items-center justify-between">
              <h3 className="text-xs font-semibold text-slate-400">知识图谱 · 「{kb}」（Neo4j · 力导向）</h3>
              <Button size="sm" icon={Network} onClick={onGraph}>
                刷新图谱
              </Button>
            </div>
            <div className="max-w-3xl">
              <GraphView triples={graphData ?? []} height={460} />
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// VaultPanel：Obsidian 式文库 —— 列表(仅元数据) / 正文按需加载 / [[双链]]可点 / 反链 / 笔记关系图。
// 列表与正文分离 + 链接走服务端索引，不再一次拉回整库正文，可扛十几万字大文件。
function VaultPanel({ identity, kb }: { identity: Identity; kb: string }) {
  const toast = useToast();
  const [docs, setDocs] = useState<VaultDoc[]>([]);
  const [links, setLinks] = useState<DocLink[]>([]);
  const [sel, setSel] = useState<string | null>(null);
  const [content, setContent] = useState("");
  const [loadingDoc, setLoadingDoc] = useState(false);
  const [loading, setLoading] = useState(false);
  const [mode, setMode] = useState<"read" | "graph">("read");
  const [editing, setEditing] = useState(false);
  const [creatingNew, setCreatingNew] = useState(false);
  const [draft, setDraft] = useState("");
  const [draftName, setDraftName] = useState("");
  const [saving, setSaving] = useState(false);

  // sel 为文件主表雪花 ID（不再是文件名）—— 取正文/反链/关系图一律用 ID 关联。
  // preferName 给定时（如保存笔记后）按名定位到其新 ID。
  const load = useCallback(async (preferName?: string) => {
    setLoading(true);
    try {
      const [d, lk] = await Promise.all([listVault(identity, kb), listLinks(identity, kb)]);
      setDocs(d);
      setLinks(lk);
      setSel((s) => {
        if (preferName) {
          const hit = d.find((x) => x.name === preferName);
          if (hit) return hit.id;
        }
        return d.some((x) => x.id === s) ? s : d[0]?.id ?? null;
      });
    } catch {
      setDocs([]);
      setLinks([]);
    } finally {
      setLoading(false);
    }
  }, [identity, kb]);
  useEffect(() => {
    void load();
    setEditing(false);
    setMode("read");
  }, [load]);

  // 选中文档时按 ID 取全文（不随列表一起拉）。
  useEffect(() => {
    if (!sel || editing) return;
    let alive = true;
    setLoadingDoc(true);
    getDoc(identity, sel)
      .then((c) => alive && setContent(c))
      .catch(() => alive && setContent(""))
      .finally(() => alive && setLoadingDoc(false));
    return () => {
      alive = false;
    };
  }, [sel, identity, editing]);

  // 名称↔ID 映射：[[双链]]按名书写、关系图按名展示，但选中/关联走 ID。
  const idToName = new Map(docs.map((d) => [d.id, d.name] as const));
  const nameToId = new Map(docs.map((d) => [d.name, d.id] as const));
  const current = docs.find((d) => d.id === sel);
  const open = (name: string) => {
    const id = nameToId.get(name);
    if (id) {
      setSel(id);
      setMode("read");
      setEditing(false);
    }
  };
  // 反链/笔记关系图来自服务端 [[双链]]索引（FromID→ToID），ID 关联、按名渲染，不扫全文。
  const backlinks = sel
    ? [...new Set(links.filter((l) => l.to === sel).map((l) => idToName.get(l.from)).filter((n): n is string => !!n))]
    : [];
  const noteTriples: Triple[] = links
    .map((l) => ({ s: idToName.get(l.from), o: idToName.get(l.to) }))
    .filter((e): e is { s: string; o: string } => !!e.s && !!e.o)
    .map((e) => ({ s: e.s, p: "链接", o: e.o }));

  const startNew = () => {
    setCreatingNew(true);
    setEditing(true);
    setDraftName("");
    setDraft("");
  };
  const startEdit = () => {
    if (!current) return;
    setCreatingNew(false);
    setEditing(true);
    setDraftName(current.name);
    setDraft(content);
  };
  const onSave = async () => {
    const name = (creatingNew ? draftName : current?.name ?? "").trim();
    if (!name || !draft.trim()) {
      toast.push("error", "笔记名与内容不能为空");
      return;
    }
    setSaving(true);
    try {
      await saveNote(identity, kb, name, draft);
      toast.push("success", `已保存「${name}」（正在重建索引）`);
      setEditing(false);
      setCreatingNew(false);
      setContent(draft);
      setTimeout(() => void load(name), 400);
    } catch (e) {
      toast.push("error", (e as Error).message);
    } finally {
      setSaving(false);
    }
  };

  const empty = !loading && docs.length === 0;

  return (
    <div className="flex h-full min-h-0">
      <aside className="flex w-56 shrink-0 flex-col overflow-y-auto border-r border-line p-2">
        <div className="mb-1 flex items-center gap-1 px-1 text-[11px] text-slate-500">
          <span>文档 {docs.length}</span>
          <button onClick={startNew} className="ml-auto text-slate-500 hover:text-brand-400" title="新建笔记">
            <Plus className="h-3.5 w-3.5" />
          </button>
          <button onClick={() => void load()} className="text-slate-600 hover:text-slate-300" title="刷新">
            <RefreshCw className={cn("h-3.5 w-3.5", loading && "animate-spin")} />
          </button>
        </div>
        <ul className="space-y-0.5">
          {docs.map((d) => (
            <li key={d.id}>
              <button
                onClick={() => {
                  setSel(d.id);
                  setMode("read");
                  setEditing(false);
                }}
                className={cn("flex w-full items-center gap-1.5 rounded px-2 py-1.5 text-left text-xs", d.id === sel ? "bg-brand/15 text-brand-400" : "text-slate-300 hover:bg-ink-800")}
              >
                <FileText className="h-3.5 w-3.5 shrink-0" />
                <span className="truncate" title={d.name}>{d.name}</span>
                {d.ext && <span className="ml-auto shrink-0 text-[9px] uppercase text-slate-600">{d.ext.replace(/^\./, "")}</span>}
              </button>
            </li>
          ))}
        </ul>
      </aside>

      <div className="min-h-0 flex-1 overflow-y-auto p-5">
        {/* 工具条：阅读/关系图 + 编辑 */}
        <div className="mb-3 flex items-center gap-2">
          <div className="flex rounded-md border border-line p-0.5 text-[11px]">
            <button onClick={() => setMode("read")} className={cn("flex items-center gap-1 rounded px-2 py-1", mode === "read" ? "bg-brand/15 text-brand-400" : "text-slate-400")}>
              <BookOpen className="h-3 w-3" /> 阅读
            </button>
            <button onClick={() => setMode("graph")} className={cn("flex items-center gap-1 rounded px-2 py-1", mode === "graph" ? "bg-brand/15 text-brand-400" : "text-slate-400")}>
              <Waypoints className="h-3 w-3" /> 关系图
            </button>
          </div>
          {mode === "read" && !editing && current && (
            <Button size="sm" variant="ghost" icon={Pencil} onClick={startEdit}>
              编辑
            </Button>
          )}
          {editing && (
            <>
              <Button size="sm" variant="primary" icon={Save} onClick={onSave} disabled={saving}>
                {saving ? "保存中…" : "保存"}
              </Button>
              <Button size="sm" variant="ghost" icon={X} onClick={() => setEditing(false)}>
                取消
              </Button>
            </>
          )}
          <span className="ml-auto text-[10px] text-slate-600">笔记支持 [[双链]]；保存后重建索引与图谱</span>
        </div>

        {mode === "graph" ? (
          noteTriples.length > 0 ? (
            <GraphView triples={noteTriples} height={440} onNode={open} />
          ) : (
            <EmptyState icon={Waypoints} title="暂无笔记关系" desc="在笔记里用 [[其它笔记名]] 互相引用，这里会连成关系图（点节点跳转）。" />
          )
        ) : editing ? (
          <div className="flex flex-col gap-2">
            {creatingNew && <Input value={draftName} onChange={(e) => setDraftName(e.target.value)} placeholder="笔记名，如 项目A概述" autoFocus />}
            {!creatingNew && <div className="text-sm font-semibold text-slate-100">{draftName}</div>}
            <Textarea className="min-h-[360px] w-full font-mono" value={draft} onChange={(e) => setDraft(e.target.value)} placeholder={"# 标题\n正文支持 Markdown 与 [[双链]]…"} />
          </div>
        ) : empty ? (
          <EmptyState icon={BookOpen} title="文库为空" desc={`「${kb}」还没有文档。点左上 + 新建笔记，或到「入库」拖入文件。`} action={<Button size="sm" icon={Plus} onClick={startNew}>新建笔记</Button>} />
        ) : current ? (
          <>
            <h2 className="mb-3 flex items-center gap-2 text-base font-semibold text-slate-100">
              <FileText className="h-4 w-4 text-brand-400" />
              {current.name}
              {current.size != null && <span className="text-[11px] font-normal text-slate-600">{current.size} 字</span>}
            </h2>
            {loadingDoc ? (
              <div className="flex items-center gap-1.5 text-xs text-slate-500"><Loader2 className="h-3.5 w-3.5 animate-spin" /> 加载正文…</div>
            ) : (
              <Markdown text={content} className="text-sm" onLink={open} />
            )}
            <div className="mt-6 border-t border-line pt-3">
              <div className="mb-2 flex items-center gap-1.5 text-xs font-medium text-slate-400">
                <Link2 className="h-3.5 w-3.5" /> 反向链接（{backlinks.length}）
              </div>
              {backlinks.length === 0 ? (
                <p className="text-[11px] text-slate-600">暂无其它笔记用 [[{current.name}]] 链接到这里。</p>
              ) : (
                <ul className="space-y-1">
                  {backlinks.map((b) => (
                    <li key={b}>
                      <button onClick={() => open(b)} className="text-xs text-brand-400 hover:underline">
                        {b}
                      </button>
                    </li>
                  ))}
                </ul>
              )}
            </div>
          </>
        ) : (
          <div className="text-sm text-slate-600">选择左侧文档查看，或新建笔记。</div>
        )}
      </div>
    </div>
  );
}

function FileStatus({ status }: { status: string }) {
  if (status === "完成") return <Badge tone="success">完成</Badge>;
  if (status === "失败") return <Badge tone="danger">失败</Badge>;
  if (status === "排队") return <Badge tone="neutral">排队</Badge>;
  return (
    <span className="inline-flex items-center gap-1 rounded bg-accent/15 px-1.5 py-0.5 text-[9px] text-accent-400">
      <Loader2 className="h-3 w-3 animate-spin" />
      {status}
    </span>
  );
}

// Timeline 渲染入库各阶段：状态灯 + 标签 + 详情；解析预览、切块块、抽取的知识三元组逐步呈现。
function Timeline({ prog, vecPct }: { prog: Progress; vecPct: number }) {
  return (
    <div className="rounded-lg border border-line bg-ink-850 p-3">
      <ol className="relative ml-1 space-y-2 border-l border-line pl-4">
        {prog.steps.map((s, i) => {
          const meta = STAGE[s.stage] ?? { icon: Loader2, label: s.stage };
          const Icon = meta.icon;
          const isLast = i === prog.steps.length - 1;
          const isErr = prog.error && s.stage === "失败";
          const status = isErr ? "error" : isLast && prog.active ? "active" : "done";
          return (
            <li key={i} className="relative">
              <span className="absolute -left-[21px] top-0.5 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-ink-850">
                {status === "active" ? (
                  <Loader2 className="h-3.5 w-3.5 animate-spin text-accent-400" />
                ) : status === "error" ? (
                  <XCircle className="h-3.5 w-3.5 text-danger" />
                ) : (
                  <CheckCircle2 className="h-3.5 w-3.5 text-success" />
                )}
              </span>
              <div className="flex items-center gap-2">
                <Icon className="h-3.5 w-3.5 text-slate-400" />
                <span className="text-xs font-medium text-slate-200">{meta.label}</span>
                {s.msg && <span className="text-[11px] text-slate-500">{s.msg}</span>}
              </div>

              {s.stage === "向量化" && prog.vecTotal ? (
                <div className="mt-1">
                  <div className="h-1.5 w-full overflow-hidden rounded-full bg-ink-700">
                    <div className="h-full rounded-full bg-gradient-to-r from-brand to-accent transition-all" style={{ width: `${vecPct}%` }} />
                  </div>
                  <div className="mt-0.5 text-[10px] text-slate-500">{prog.vecDone ?? 0}/{prog.vecTotal} 块（{vecPct}%）</div>
                </div>
              ) : null}

              {s.stage === "解析完成" && prog.preview ? (
                <p className="mt-1 max-h-16 overflow-hidden rounded bg-ink-900 px-2 py-1 text-[11px] leading-relaxed text-slate-400">{prog.preview}</p>
              ) : null}

              {s.stage === "切块" && prog.chunks.length > 0 ? (
                <ul className="mt-1 max-h-20 space-y-0.5 overflow-auto">
                  {prog.chunks.map((c, j) => (
                    <li key={j} className="truncate rounded bg-ink-900 px-1.5 py-0.5 text-[10px] text-slate-400">
                      <span className="text-slate-600">#{j + 1}</span> {c}
                    </li>
                  ))}
                </ul>
              ) : null}
            </li>
          );
        })}
      </ol>

      {prog.triples.length > 0 && (
        <div className="mt-3 border-t border-line pt-2">
          <div className="mb-1.5 flex items-center gap-1.5 text-[11px] font-medium text-slate-400">
            <Network className="h-3.5 w-3.5 text-brand-400" /> 抽取出的知识（{prog.triples.length} 条三元组）
          </div>
          <div className="flex flex-wrap gap-1">
            {prog.triples.slice(0, 12).map((t, i) => (
              <span key={i} className="inline-flex items-center gap-1 rounded bg-ink-900 px-1.5 py-0.5 text-[10px]">
                <span className="text-brand-400">{t.s}</span>
                <span className="text-slate-600">{t.p}</span>
                <span className="text-accent-400">{t.o}</span>
              </span>
            ))}
            {prog.triples.length > 12 && <span className="px-1 text-[10px] text-slate-600">+{prog.triples.length - 12}</span>}
          </div>
          <div className="mt-2">
            <GraphView triples={prog.triples} height={200} />
          </div>
        </div>
      )}
    </div>
  );
}
