"use client";

import React, { useEffect, useMemo, useRef, useState } from "react";
import { ChevronLeft, History, Network, Plus, Settings, Settings2, Send, Sparkles, User, Wrench, X } from "lucide-react";
import type { Json } from "@/types/supabase";
import { useEditorBridgeStore } from "@/store/editor-bridge";
import { useAiAgentUiStore } from "@/store/ai-agent-ui";
import { useAppPreferencesStore } from "@/store/app-preferences";
import { Button } from "@/components/ui/button";
import { AiBridgePanel } from "@/components/ai-agent/AiBridgePanel";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Textarea } from "@/components/ui/textarea";
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
import { clamp, MIN_AGENT_STEPS, MAX_AGENT_STEPS } from "@/lib/constants";
import { parseSseChunks, readAiPanelPrefs, type AiProvider, writeAiPanelPrefs } from "@/components/ai-agent/panelShared";
import {
  executePageBodyCommand,
  type ExecutePageBodyCommandInput,
  type PageBodyPersistedMeta,
} from "@/lib/documents/page-command-client";
import type { DocumentAiCapabilityConfig } from "@/lib/ai-agent/document-config";
import type { PageAggregateAiSnapshot } from "@/components/editor/DocumentAiAgentPanel";

type AgentMessage = { role: "user" | "assistant"; content: string };
type CodexMode = "chat" | "test" | "dev";

const OLLAMA_QWEN3_30B = "qwen3:30b-a3b-instruct-2507-q4_K_M";

const extractCodexMode = (text: string): CodexMode => {
  const s = String(text ?? "");
  const m = s.match(/^\s*#(chat|test|dev)\b/i);
  if (!m) return "chat";
  const mode = String(m[1] ?? "").toLowerCase();
  if (mode === "dev" || mode === "test" || mode === "chat") return mode;
  return "chat";
};

const DEFAULT_SESSION_MESSAGES: AgentMessage[] = [
  {
    role: "assistant",
    content:
      "你好，我是页面 AI Agent。\n- 我可以查找/插入/改写页面内容（直接落入页面）。\n- 我也可以：LightRAG 检索、跨页面搜索/读取、读取图片 OCR、执行斜杠命令（创建/改名）。\n- 建议先说你的目标（例如：把某段改写更简洁，或在某个标题后新增一段总结）。",
  },
];

type ToolName =
  | "search_web"
  | "rag_lightrag_query"
  | "docs_search"
  | "docs_read"
  | "image_read"
  | "slash_run"
  | "doc_get"
  | "doc_find"
  | "doc_insert_blocks"
  | "doc_replace_range";

const TOOL_LABEL: Record<ToolName, string> = {
  search_web: "联网检索（SearxNG）",
  rag_lightrag_query: "LightRAG 检索",
  docs_search: "文档搜索（跨页）",
  docs_read: "文档读取（跨页）",
  image_read: "图片读取（OCR）",
  slash_run: "斜杠命令（写入）",
  doc_get: "读页面（摘要）",
  doc_find: "查找（按块）",
  doc_insert_blocks: "插入块（写入）",
  doc_replace_range: "替换块文本（写入）",
};

const DEFAULT_TOOLS: ToolName[] = [
  "doc_get",
  "doc_find",
  "doc_insert_blocks",
  "doc_replace_range",
  "docs_search",
  "docs_read",
  "image_read",
  "rag_lightrag_query",
  "search_web",
];

type ToolLog =
  | { type: "tool_call"; id: string; tool: string; args: Record<string, unknown> }
  | { type: "tool_result"; id: string; tool: string; ok: boolean; ms: number; result: unknown }
  | { type: "info"; message: string }
  | { type: "error"; message: string };

type ChatSession = {
  id: string;
  title: string;
  createdAt: number;
  updatedAt: number;
  messages: AgentMessage[];
  toolLogs: ToolLog[];
  codexSessionId?: string | null;
  codexMode?: CodexMode | null;
};

type PanelPage = "chat" | "tools" | "history" | "account" | "settings";

const generateId = () => {
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
  return `sess_${Math.random().toString(16).slice(2, 10)}`;
};

const normalizeSessions = (sessions: ChatSession[]) => {
  const maxSessions = 20;
  const maxMessages = 40;
  const maxToolLogs = 80;
  return sessions
    .slice(0, maxSessions)
    .map((s) => ({
      ...s,
      messages: Array.isArray(s.messages) ? s.messages.slice(-maxMessages) : [],
      toolLogs: Array.isArray(s.toolLogs) ? s.toolLogs.slice(-maxToolLogs) : [],
    }))
    .sort((a, b) => b.updatedAt - a.updatedAt);
};

function buildSessionSnapshotKey(input: {
  messages: ChatSession["messages"];
  toolLogs: ChatSession["toolLogs"];
}): string {
  return JSON.stringify({
    messages: input.messages,
    toolLogs: input.toolLogs,
  });
}

export function shouldSyncActiveSessionSnapshot(
  session: Pick<ChatSession, "messages" | "toolLogs"> | null | undefined,
  messages: ChatSession["messages"],
  toolLogs: ChatSession["toolLogs"],
): boolean {
  if (!session) {
    return true;
  }
  return (
    buildSessionSnapshotKey({
      messages: session.messages,
      toolLogs: session.toolLogs,
    }) !==
    buildSessionSnapshotKey({
      messages,
      toolLogs,
    })
  );
}

export function buildAiAgentSessionsStoragePayload(
  activeSessionId: string,
  sessions: ChatSession[],
): string {
  return JSON.stringify({
    activeSessionId,
    sessions: normalizeSessions(sessions),
  });
}

export async function applyDocWriteToolResultToPageBody(input: {
  tool: string;
  ok: boolean;
  result: unknown;
  documentId: string;
  getLatestPageAggregateSnapshot: () => PageAggregateAiSnapshot;
  applyEditorSnapshot?: (blocks: Json) => void;
  onPersistedMetaChange?: (meta: PageBodyPersistedMeta) => void;
  applyPageBodyCommandImpl?: (input: ExecutePageBodyCommandInput) => Promise<PageBodyPersistedMeta>;
}): Promise<boolean> {
  if (!input.ok || (input.tool !== "doc_insert_blocks" && input.tool !== "doc_replace_range")) {
    return false;
  }
  const resultRecord =
    input.result && typeof input.result === "object" ? (input.result as Record<string, unknown>) : null;
  const dataNode = resultRecord && "data" in resultRecord ? resultRecord.data : null;
  if (!Array.isArray(dataNode)) {
    return false;
  }

  const latestPersistedMeta = input.getLatestPageAggregateSnapshot().persistedMeta;
  const applyPageBodyCommandImpl = input.applyPageBodyCommandImpl ?? executePageBodyCommand;
  const persistedMeta = await applyPageBodyCommandImpl({
    documentId: input.documentId,
    workspaceId: latestPersistedMeta.workspaceId,
    revision: latestPersistedMeta.revision,
    conflictDetectionKey: latestPersistedMeta.conflictDetectionKey,
    blocks: dataNode as Json,
    applyEditorSnapshot: input.applyEditorSnapshot,
  });
  input.onPersistedMetaChange?.(persistedMeta);
  return true;
}

export function extractCurrentPageTitleFromSlashToolResult(input: {
  tool: string;
  ok: boolean;
  result: unknown;
  documentId: string;
}): string | null {
  if (!input.ok || input.tool !== "slash_run") {
    return null;
  }

  const resultRecord =
    input.result && typeof input.result === "object" ? (input.result as Record<string, unknown>) : null;
  const parsed =
    resultRecord?.parsed && typeof resultRecord.parsed === "object"
      ? (resultRecord.parsed as Record<string, unknown>)
      : null;
  if (!parsed || String(parsed.command ?? "") !== "rename_doc") {
    return null;
  }

  const params =
    parsed.params && typeof parsed.params === "object" ? (parsed.params as Record<string, unknown>) : null;
  if (!params || String(params.documentId ?? "") !== input.documentId) {
    return null;
  }

  const title = String(params.title ?? "").trim();
  return title || null;
}

const safeJsonStringify = (value: unknown) => {
  try {
    return JSON.stringify(value);
  } catch {
    return "";
  }
};

export function DocumentAiAgentPanelRuntime({
  documentId,
  getLatestPageAggregateSnapshot,
  onPersistedMetaChange,
  onPageHeadTitleChange,
}: {
  documentId: string;
  getLatestPageAggregateSnapshot: () => PageAggregateAiSnapshot;
  onPersistedMetaChange?: (meta: PageBodyPersistedMeta) => void;
  onPageHeadTitleChange?: (title: string) => void;
}) {
  const editorBridge = useEditorBridgeStore((s) => s.bridge);

  const open = useAiAgentUiStore((s) => s.documentAgentOpen);
  const setOpen = useAiAgentUiStore((s) => s.setDocumentAgentOpen);
  const flightMode = useAppPreferencesStore((s) => s.flightMode);
  const [messages, setMessages] = useState<AgentMessage[]>(() => DEFAULT_SESSION_MESSAGES);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const [networkOn, setNetworkOn] = useState(() => !flightMode);
  const [toolAuto, setToolAuto] = useState(true);
  const [selectedTools, setSelectedTools] = useState<ToolName[]>(DEFAULT_TOOLS);
  const [maxSteps, setMaxSteps] = useState<number>(10);
  const [aiProvider, setAiProvider] = useState<AiProvider>("online");
  const [aiModel, setAiModel] = useState<string>("");
  const [aiModelKey, setAiModelKey] = useState<string>("");
  const [aiProfileId, setAiProfileId] = useState<string>("");
  const [page, setPage] = useState<PanelPage>("chat");
  const [onlineConfig, setOnlineConfig] = useState<DocumentAiCapabilityConfig | null>(null);
  const [onlineConfigLoading, setOnlineConfigLoading] = useState(false);
  const [onlineConfigError, setOnlineConfigError] = useState<string>("");

  const [toolLogs, setToolLogs] = useState<ToolLog[]>([]);
  const [sessions, setSessions] = useState<ChatSession[]>([]);
  const [activeSessionId, setActiveSessionId] = useState<string>("");

  // 兼容旧实现（已改为侧边栏内切换页面，不再弹出居中对话框）
  const [toolsDialogOpen, setToolsDialogOpen] = useState(false);
  const [historyDialogOpen, setHistoryDialogOpen] = useState(false);
  const [accountDialogOpen, setAccountDialogOpen] = useState(false);
  const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);

  const abortRef = useRef<AbortController | null>(null);
  const syncTimerRef = useRef<number | null>(null);
  const hydratedSessionsRef = useRef(false);

  useEffect(() => {
    if (flightMode) {
      setNetworkOn(false);
    }
  }, [flightMode]);

  useEffect(() => {
    const prefs = readAiPanelPrefs("doc_ai", { provider: "online", model: "", modelKey: "", profileId: "", maxSteps: 10 });
    setMaxSteps(clamp(prefs.maxSteps, MIN_AGENT_STEPS, MAX_AGENT_STEPS));
    setAiProvider(prefs.provider);
    setAiModel(prefs.model);
    setAiModelKey(prefs.modelKey || "");
    setAiProfileId(prefs.profileId || "");
  }, []);

  useEffect(() => {
    writeAiPanelPrefs("doc_ai", {
      provider: aiProvider,
      model: aiModel,
      modelKey: aiModelKey,
      profileId: aiProfileId,
      maxSteps: clamp(maxSteps, MIN_AGENT_STEPS, MAX_AGENT_STEPS),
    });
  }, [aiModel, aiModelKey, aiProfileId, aiProvider, maxSteps]);

  useEffect(() => {
    // 关闭面板时，回到对话页，避免下次打开还停留在设置/历史等子页
    if (!open) setPage("chat");
  }, [open]);

  useEffect(() => {
    if (aiProvider !== "online") return;
    let cancelled = false;
    setOnlineConfigLoading(true);
    setOnlineConfigError("");
    void fetch("/api/ai-agent/document/config", {
      method: "GET",
      cache: "no-store",
    })
      .then(async (response) => {
        if (!response.ok) {
          const payload = (await response.json().catch(() => null)) as Record<string, unknown> | null;
          throw new Error(String(payload?.error ?? `读取页面 AI 配置失败：${response.status}`));
        }
        return (await response.json()) as DocumentAiCapabilityConfig;
      })
      .then((payload) => {
        if (cancelled) return;
        setOnlineConfig(payload);
        setAiModelKey((prev) => {
          const picked = prev.trim();
          if (picked && payload.models.some((item) => item.key === picked)) {
            return picked;
          }
          return payload.defaultModelKey || picked;
        });
        setAiProfileId((prev) => {
          const picked = prev.trim();
          if (picked && payload.profiles.some((item) => item.id === picked)) {
            return picked;
          }
          return payload.defaultProfileId || picked;
        });
      })
      .catch((error) => {
        if (cancelled) return;
        setOnlineConfigError(error instanceof Error ? error.message : String(error));
      })
      .finally(() => {
        if (!cancelled) {
          setOnlineConfigLoading(false);
        }
      });
    return () => {
      cancelled = true;
    };
  }, [aiProvider]);

  // 会话/历史：按 documentId 隔离持久化
  useEffect(() => {
    try {
      const key = `doc_ai_sessions:${documentId}`;
      const raw = window.localStorage.getItem(key);
      if (!raw) {
        const id = generateId();
        const now = Date.now();
        const session: ChatSession = {
          id,
          title: "新会话",
          createdAt: now,
          updatedAt: now,
          messages: DEFAULT_SESSION_MESSAGES,
          toolLogs: [],
          codexSessionId: null,
          codexMode: null,
        };
        setSessions([session]);
        setActiveSessionId(id);
        setMessages(session.messages);
        setToolLogs([]);
        hydratedSessionsRef.current = true;
        return;
      }

      const parsed = JSON.parse(raw) as unknown;
      const list = typeof parsed === "object" && parsed && "sessions" in parsed ? (parsed as any).sessions : null;
      const active = typeof parsed === "object" && parsed && "activeSessionId" in parsed ? String((parsed as any).activeSessionId ?? "") : "";
      if (!Array.isArray(list) || list.length === 0) return;

      const loaded = normalizeSessions(
        list
          .map((x) => {
            const id = String((x as any)?.id ?? "").trim() || generateId();
            const createdAt = Number((x as any)?.createdAt ?? Date.now());
            const updatedAt = Number((x as any)?.updatedAt ?? createdAt);
            const title = String((x as any)?.title ?? "").trim() || "历史会话";
            const messages = Array.isArray((x as any)?.messages) ? (x as any).messages : DEFAULT_SESSION_MESSAGES;
            const toolLogs = Array.isArray((x as any)?.toolLogs) ? (x as any).toolLogs : [];
            const codexSessionId = String((x as any)?.codexSessionId ?? "").trim() || null;
            const codexModeRaw = String((x as any)?.codexMode ?? "").trim();
            const codexMode = codexModeRaw === "chat" || codexModeRaw === "test" || codexModeRaw === "dev" ? (codexModeRaw as CodexMode) : null;
            return { id, title, createdAt, updatedAt, messages, toolLogs, codexSessionId, codexMode } as ChatSession;
          })
          .filter((s) => s.id),
      );
      setSessions(loaded);
      const picked = active && loaded.some((s) => s.id === active) ? active : loaded[0]!.id;
      setActiveSessionId(picked);
      const cur = loaded.find((s) => s.id === picked) ?? loaded[0]!;
      setMessages(cur.messages?.length ? cur.messages : DEFAULT_SESSION_MESSAGES);
      setToolLogs(cur.toolLogs ?? []);
      hydratedSessionsRef.current = true;
    } catch {
      // ignore
      hydratedSessionsRef.current = true;
    }
    // 只在 documentId 变化时读取一次
     
  }, [documentId]);

  useEffect(() => {
    if (!activeSessionId) return;
    if (!hydratedSessionsRef.current) return;
    if (syncTimerRef.current) window.clearTimeout(syncTimerRef.current);
    syncTimerRef.current = window.setTimeout(() => {
      setSessions((prev) => {
        const current = prev.find((s) => s.id === activeSessionId) ?? null;
        if (shouldSyncActiveSessionSnapshot(current, messages, toolLogs) === false) {
          return prev;
        }
        const now = Date.now();
        const next = prev.some((s) => s.id === activeSessionId)
          ? prev.map((s) =>
              s.id === activeSessionId ? { ...s, messages, toolLogs, updatedAt: now } : s,
            )
          : [
              {
                id: activeSessionId,
                title: "新会话",
                createdAt: now,
                updatedAt: now,
                messages,
                toolLogs,
                codexSessionId: null,
                codexMode: null,
              },
              ...prev,
            ];
        return normalizeSessions(next);
      });
    }, 200);
    return () => {
      if (syncTimerRef.current) window.clearTimeout(syncTimerRef.current);
    };
  }, [activeSessionId, messages, toolLogs]);

  useEffect(() => {
    if (!documentId) return;
    if (!hydratedSessionsRef.current) return;
    try {
      const key = `doc_ai_sessions:${documentId}`;
      const payload = buildAiAgentSessionsStoragePayload(activeSessionId, sessions);
      if (payload.length <= 900_000) window.localStorage.setItem(key, payload);
    } catch {
      // ignore
    }
  }, [activeSessionId, documentId, sessions]);

  const currentSession = useMemo(() => sessions.find((s) => s.id === activeSessionId) ?? null, [activeSessionId, sessions]);
  const currentSessionTitle = currentSession?.title || "新会话";
  const selectedOnlineModel = useMemo(
    () => onlineConfig?.models.find((item) => item.key === aiModelKey) ?? null,
    [aiModelKey, onlineConfig],
  );
  const selectedOnlineProfile = useMemo(
    () => onlineConfig?.profiles.find((item) => item.id === aiProfileId) ?? null,
    [aiProfileId, onlineConfig],
  );
  const onlineSessionId = useMemo(() => {
    if (aiProvider !== "online" || !activeSessionId || !documentId) return null;
    return `doc_ai:${documentId}:${activeSessionId}`;
  }, [activeSessionId, aiProvider, documentId]);

  const [codexSessionDraft, setCodexSessionDraft] = useState("");
  useEffect(() => {
    if (aiProvider !== "codex") return;
    setCodexSessionDraft(String(currentSession?.codexSessionId ?? "").trim());
  }, [aiProvider, currentSession?.codexSessionId]);

  const pageTitle = useMemo(() => {
    switch (page) {
      case "tools":
        return "工具（代替 MCP）";
      case "history":
        return "历史会话";
      case "account":
        return "账户 / 模型";
      case "settings":
        return "设置";
      default:
        return "页面 AI Agent";
    }
  }, [page]);

  const startNewSession = () => {
    if (loading) return;
    const id = generateId();
    const now = Date.now();
    const next: ChatSession = {
      id,
      title: "新会话",
      createdAt: now,
      updatedAt: now,
      messages: DEFAULT_SESSION_MESSAGES,
      toolLogs: [],
      codexSessionId: null,
      codexMode: null,
    };
    setSessions((prev) => normalizeSessions([next, ...prev]));
    setActiveSessionId(id);
    setMessages(next.messages);
    setToolLogs([]);
    setInput("");
  };

  const clearHistory = () => {
    if (loading) return;
    const base: ChatSession =
      currentSession ??
      ({
        id: activeSessionId || generateId(),
        title: "当前会话",
        createdAt: Date.now(),
        updatedAt: Date.now(),
        messages,
        toolLogs,
      } as ChatSession);
    setSessions([
      {
        ...base,
        title: base.title || "当前会话",
        updatedAt: Date.now(),
        messages,
        toolLogs,
      },
    ]);
    setActiveSessionId(base.id);
  };

  const switchSession = (id: string) => {
    if (loading) return;
    const target = sessions.find((s) => s.id === id);
    if (!target) return;
    setActiveSessionId(target.id);
    setMessages(target.messages?.length ? target.messages : DEFAULT_SESSION_MESSAGES);
    setToolLogs(target.toolLogs ?? []);
    setInput("");
  };

  const resetCurrentSession = () => {
    if (loading) return;
    setMessages(DEFAULT_SESSION_MESSAGES);
    setToolLogs([]);
    setInput("");
    if (activeSessionId) {
      setSessions((prev) =>
        normalizeSessions(
          prev.map((s) =>
            s.id === activeSessionId
              ? {
                  ...s,
                  messages: DEFAULT_SESSION_MESSAGES,
                  toolLogs: [],
                  updatedAt: Date.now(),
                  title: s.title || "当前会话",
                  codexSessionId: null,
                  codexMode: null,
                }
              : s,
          ),
        ),
      );
    }
  };

  const deleteSession = (id: string) => {
    if (loading) return;
    setSessions((prev) => {
      const next = prev.filter((s) => s.id !== id);
      return normalizeSessions(next);
    });
    if (id === activeSessionId) {
      const fallback = sessions.filter((s) => s.id !== id)[0];
      if (fallback) {
        setActiveSessionId(fallback.id);
        setMessages(fallback.messages?.length ? fallback.messages : DEFAULT_SESSION_MESSAGES);
        setToolLogs(fallback.toolLogs ?? []);
      } else {
        startNewSession();
      }
    }
  };

  const canSend = useMemo(() => input.trim().length > 0 && !loading, [input, loading]);

  const stop = () => {
    abortRef.current?.abort();
    abortRef.current = null;
    setLoading(false);
    if (aiProvider === "codex") {
      setToolLogs((prev) => [...prev, { type: "info", message: "已暂停（可继续对话）" }]);
    }
  };

  const send = async () => {
    const content = input.trim();
    if (!content) return;
    const hasExplicitCodexMode = /^\s*#(chat|test|dev)\b/i.test(content);
    const intendedCodexMode: CodexMode =
      aiProvider === "codex" ? (hasExplicitCodexMode ? extractCodexMode(content) : "chat") : "chat";
    setToolLogs([]);
    if (activeSessionId && currentSessionTitle === "新会话") {
      const title = content.length > 18 ? `${content.slice(0, 18)}…` : content;
      setSessions((prev) =>
        normalizeSessions(
          prev.map((s) => (s.id === activeSessionId ? { ...s, title, updatedAt: Date.now() } : s)),
        ),
      );
    }

    const nextMessages: AgentMessage[] = [...messages, { role: "user", content }];
    setMessages(nextMessages);
    setInput("");
    setLoading(true);

    abortRef.current?.abort();
    const controller = new AbortController();
    abortRef.current = controller;

    // 不把面板的“欢迎语”当作对话历史发送给服务端，避免影响任务执行
    const payloadMessages = nextMessages.filter(
      (m, idx) => !(idx === 0 && m.role === "assistant" && /页面 AI Agent/.test(m.content)),
    );

    const payloadMessagesForRequest = payloadMessages;

    const pageAggregateSnapshot = getLatestPageAggregateSnapshot();
    const blocks = pageAggregateSnapshot.blocks;
    const blocksJson = blocks ? safeJsonStringify(blocks) : "";
    const shouldSendBlocks = blocksJson && blocksJson.length <= 500_000;
    const pageSubtree = pageAggregateSnapshot.pageSubtree;
    const contextNode = pageSubtree?.rootNode ?? null;
    const contextSubtree = pageSubtree
      ? {
          projectionId: pageSubtree.projectionId,
          rootNodeId: pageSubtree.subtree.rootNodeId,
          stats: pageSubtree.stats,
          nodes: pageSubtree.subtree.nodes.slice(0, 160),
        }
      : null;
    const contextOutline = pageSubtree?.outline.slice(0, 40) ?? null;
    const contextEvidence = pageSubtree?.evidence.slice(0, 32) ?? null;

    const codexSessionIdForRequest =
      aiProvider === "codex" ? String(currentSession?.codexSessionId ?? "").trim() || null : null;
    const onlineModelKeyForRequest =
      aiProvider === "online"
        ? String(aiModelKey || onlineConfig?.defaultModelKey || "")
            .trim() || null
        : null;
    const onlineProfileIdForRequest =
      aiProvider === "online"
        ? String(aiProfileId || onlineConfig?.defaultProfileId || "")
            .trim() || null
        : null;

    if (aiProvider === "codex" && activeSessionId) {
      setSessions((prev) =>
        normalizeSessions(
          prev.map((s) =>
            s.id === activeSessionId ? { ...s, codexMode: intendedCodexMode, updatedAt: Date.now() } : s,
          ),
        ),
      );
    }

    try {
      const res = await fetch("/api/ai-agent/run", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        signal: controller.signal,
        body: JSON.stringify({
          stream: true,
          maxSteps,
          scope: "document",
          messages: payloadMessagesForRequest.slice(-24),
          toolChoice: toolAuto
            ? {
                mode: "auto",
                toolSets: [
                  "toolset.readonly",
                  "toolset.rag_read",
                  "toolset.docs_read",
                  "toolset.media_read",
                  "toolset.doc_read",
                  "toolset.doc_write",
                  "toolset.slash_write",
                ],
              }
            : { mode: "manual", tools: selectedTools },
          context: {
            documentId,
            documentBlocks: shouldSendBlocks ? blocks : null,
            node: contextNode,
            subtree: contextSubtree,
            outline: contextOutline,
            evidence: contextEvidence,
            pageOptions: pageAggregateSnapshot.pageOptions,
          },
          options: {
            searxng: networkOn,
            ai: {
              provider: aiProvider,
              ...(aiProvider === "codex" && codexSessionIdForRequest ? { sessionId: codexSessionIdForRequest } : {}),
              ...(aiProvider === "online" && onlineSessionId ? { sessionId: onlineSessionId } : {}),
              ...(aiProvider === "online" && onlineModelKeyForRequest ? { modelKey: onlineModelKeyForRequest } : {}),
              ...(aiProvider === "online" && onlineProfileIdForRequest ? { profileId: onlineProfileIdForRequest } : {}),
              ...(aiProvider !== "codex" && aiProvider !== "online" && aiModel.trim() ? { model: aiModel.trim() } : {}),
            },
          },
        }),
      });
      if (!res.ok) {
        const j = (await res.json().catch(() => null)) as unknown;
        const err = typeof j === "object" && j && "error" in j ? String((j as Record<string, unknown>).error ?? "") : "";
        throw new Error(err || `请求失败：${res.status}`);
      }

      await parseSseChunks(res, (event, dataText) => {
        if (event === "codex_session") {
          try {
            const data = JSON.parse(dataText || "null") as unknown;
            const obj = (typeof data === "object" && data ? (data as Record<string, unknown>) : {}) as Record<string, unknown>;
            const sessionId = String(obj.sessionId ?? "").trim();
            if (sessionId && activeSessionId) {
              setSessions((prev) =>
                normalizeSessions(
                  prev.map((s) =>
                    s.id === activeSessionId ? { ...s, codexSessionId: sessionId, codexMode: intendedCodexMode, updatedAt: Date.now() } : s,
                  ),
                ),
              );
            }
          } catch {
            // ignore
          }
          return;
        }
        if (event === "tool_call") {
          try {
            const data = JSON.parse(dataText || "null") as unknown;
            const obj = (typeof data === "object" && data ? (data as Record<string, unknown>) : {}) as Record<string, unknown>;
            setToolLogs((prev) => [
              ...prev,
              {
                type: "tool_call",
                id: String(obj.id ?? ""),
                tool: String(obj.tool ?? ""),
                args: (typeof obj.args === "object" && obj.args ? (obj.args as Record<string, unknown>) : {}) as Record<
                  string,
                  unknown
                >,
              },
            ]);
          } catch {
            // ignore
          }
          return;
        }

        if (event === "tool_result") {
          try {
            const data = JSON.parse(dataText || "null") as unknown;
            const obj = (typeof data === "object" && data ? (data as Record<string, unknown>) : {}) as Record<string, unknown>;
            const tool = String(obj.tool ?? "");
            const result = "result" in obj ? obj.result : null;
            setToolLogs((prev) => [
              ...prev,
              { type: "tool_result", id: String(obj.id ?? ""), tool, ok: Boolean(obj.ok), ms: Number(obj.ms ?? 0), result },
            ]);

            const nextPageTitle = extractCurrentPageTitleFromSlashToolResult({
              tool,
              ok: Boolean(obj.ok),
              result,
              documentId,
            });
            if (nextPageTitle) {
              onPageHeadTitleChange?.(nextPageTitle);
            }

            void applyDocWriteToolResultToPageBody({
              tool,
              ok: Boolean(obj.ok),
              result,
              documentId,
              getLatestPageAggregateSnapshot,
              applyEditorSnapshot: editorBridge?.replaceWithSnapshot
                ? (blocks) => {
                    editorBridge.replaceWithSnapshot(blocks);
                  }
                : undefined,
              onPersistedMetaChange,
            }).catch((error) => {
              const message = error instanceof Error ? error.message : "AI 页面正文保存失败";
              setToolLogs((prev) => [...prev, { type: "error", message }]);
            });
          } catch {
            // ignore
          }
          return;
        }

        if (event === "assistant_message") {
          try {
            const data = JSON.parse(dataText || "null") as unknown;
            const assistantText =
              typeof data === "object" && data && "text" in data ? String((data as Record<string, unknown>).text ?? "") : "";
            setMessages((prev) => [...prev, { role: "assistant", content: assistantText.trim() ? assistantText.trim() : "（无输出）" }]);
          } catch {
            setMessages((prev) => [...prev, { role: "assistant", content: "（无输出）" }]);
          }
          return;
        }

        if (event === "error") {
          if (controller.signal.aborted && aiProvider === "codex") return;
          try {
            const data = JSON.parse(dataText || "null") as unknown;
            const message =
              typeof data === "object" && data && "message" in data ? String((data as Record<string, unknown>).message ?? "") : "";
            setToolLogs((prev) => [...prev, { type: "error", message: message || "未知错误" }]);
          } catch {
            setToolLogs((prev) => [...prev, { type: "error", message: "未知错误" }]);
          }
          return;
        }
      });
    } catch (e) {
      if (controller.signal.aborted && aiProvider === "codex") return;
      const msg = e instanceof Error ? e.message : String(e);
      setToolLogs((prev) => [...prev, { type: "error", message: msg }]);
    } finally {
      abortRef.current = null;
      setLoading(false);
    }
  };

  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetContent
        side="right"
        showCloseButton={false}
        className="w-[min(1400px,calc(100vw-24px))] max-w-none border-l-0 bg-transparent p-3 shadow-none sm:max-w-none"
      >
        <SheetTitle className="sr-only">页面 AI</SheetTitle>
        <AiBridgePanel
          title={pageTitle}
          subtitle="页面 AI"
          status={loading ? "运行中" : "待命"}
          onClose={() => setOpen(false)}
          scrollBody={false}
          className="h-[calc(100vh-24px)] min-h-0 rounded-[24px] border-white/10 shadow-none"
          secondaryActions={
            <>
              {page === "chat" ? (
                <Sparkles className="h-4 w-4 text-white/75" />
              ) : (
                <Button
                  variant="ghost"
                  size="icon-sm"
                  onClick={() => setPage("chat")}
                  disabled={loading}
                  title="返回对话"
                  className="rounded-xl border border-white/10 bg-white/[0.03] text-white/80 hover:bg-white/[0.08] hover:text-white"
                >
                  <ChevronLeft className="h-4 w-4" />
                </Button>
              )}
              <Button
                variant="ghost"
                size="icon-sm"
                onClick={startNewSession}
                disabled={loading}
                title="新建会话"
                className="rounded-xl border border-white/10 bg-white/[0.03] text-white/80 hover:bg-white/[0.08] hover:text-white"
              >
                <Plus className="h-4 w-4" />
              </Button>
              <Button
                variant="ghost"
                size="icon-sm"
                onClick={() => setPage("tools")}
                disabled={loading}
                title="工具（代替 MCP）"
                className="rounded-xl border border-white/10 bg-white/[0.03] text-white/80 hover:bg-white/[0.08] hover:text-white"
              >
                <Wrench className="h-4 w-4" />
              </Button>
              <Button
                variant="ghost"
                size="icon-sm"
                onClick={() => setPage("history")}
                disabled={loading}
                title="历史"
                className="rounded-xl border border-white/10 bg-white/[0.03] text-white/80 hover:bg-white/[0.08] hover:text-white"
              >
                <History className="h-4 w-4" />
              </Button>
              <Button
                variant="ghost"
                size="icon-sm"
                onClick={() => setPage("account")}
                disabled={loading}
                title="账户/模型"
                className="rounded-xl border border-white/10 bg-white/[0.03] text-white/80 hover:bg-white/[0.08] hover:text-white"
              >
                <User className="h-4 w-4" />
              </Button>
              <Button
                variant="ghost"
                size="icon-sm"
                onClick={() => setPage("settings")}
                disabled={loading}
                title="设置"
                className="rounded-xl border border-white/10 bg-white/[0.03] text-white/80 hover:bg-white/[0.08] hover:text-white"
              >
                <Settings className="h-4 w-4" />
              </Button>
            </>
          }
        >
          <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
          <div className={page === "chat" ? "" : "hidden"}>
          <div className="hidden">
            <div className="flex items-center gap-2">
              <button
                type="button"
                className={`inline-flex items-center gap-1 rounded border px-2 py-1 ${networkOn ? "bg-white" : "bg-muted"}`}
                onClick={() => setNetworkOn((v) => !v)}
                disabled={loading}
                title="联网检索"
              >
                <Network className="h-3.5 w-3.5" />
                {networkOn ? "联网" : "离线"}
              </button>
              <button
                type="button"
                className={`inline-flex items-center gap-1 rounded border px-2 py-1 ${toolAuto ? "bg-white" : "bg-muted"}`}
                onClick={() => setToolAuto((v) => !v)}
                disabled={loading}
                title="工具自动/手动"
              >
                <Settings2 className="h-3.5 w-3.5" />
                {toolAuto ? "自动工具" : "手动工具"}
              </button>
            </div>

            <div className="flex items-center gap-2">
              <label className="flex items-center gap-2">
                步数
                <input
                  className="w-[72px] rounded border px-2 py-1 text-xs"
                  type="number"
                  min={MIN_AGENT_STEPS}
                  max={MAX_AGENT_STEPS}
                  step={1}
                  value={maxSteps}
                  onChange={(e) => {
                    const v = Number(e.target.value);
                    if (!Number.isFinite(v)) return;
                    setMaxSteps(clamp(Math.floor(v), MIN_AGENT_STEPS, MAX_AGENT_STEPS));
                  }}
                  disabled={loading}
                />
              </label>

              <select
                className="h-8 rounded border bg-white px-2 text-xs"
                value={aiProvider}
                onChange={(e) => {
                  const v = String(e.target.value || "").trim();
                  if (v === "online" || v === "local" || v === "ollama" || v === "codex") setAiProvider(v);
                  else setAiProvider("online");
                }}
                disabled={loading}
              >
                <option value="online">在线</option>
                <option value="local">本地</option>
                <option value="ollama">Ollama</option>
                <option value="codex">Codex</option>
              </select>
              <input
                className="h-8 w-[180px] rounded border px-2 text-xs"
                value={aiModel}
                onChange={(e) => setAiModel(e.target.value)}
                placeholder={aiProvider === "codex" ? "Codex 无需 model" : aiProvider === "ollama" ? `默认：${OLLAMA_QWEN3_30B}` : "model（可选）"}
                disabled={loading || aiProvider === "codex"}
              />
            </div>
          </div>

          {false && (
            <div className="border-b p-3 text-sm">
              <div className="mb-2 text-xs text-muted-foreground">允许使用的工具</div>
              <div className="flex flex-wrap gap-2">
                {(Object.keys(TOOL_LABEL) as ToolName[]).map((t) => {
                  const on = selectedTools.includes(t);
                  return (
                    <button
                      key={t}
                      type="button"
                      className={`rounded border px-2 py-1 text-xs ${on ? "bg-[#111827] text-white" : "bg-white"}`}
                      onClick={() =>
                        setSelectedTools((prev) =>
                          prev.includes(t) ? prev.filter((x) => x !== t) : [...prev, t],
                        )
                      }
                      disabled={loading}
                    >
                      {TOOL_LABEL[t]}
                    </button>
                  );
                })}
              </div>
            </div>
          )}

          <div className="flex min-h-0 flex-1 flex-col">
            <div className="grid min-h-0 flex-1 grid-cols-1 gap-0">
              <div className="min-h-0 border-b">
                <div className="px-3 py-2 text-sm font-medium">对话</div>
                <ScrollArea className="h-[38vh] border-t">
                  <div className="space-y-3 p-3 text-sm">
                    {messages.map((m, idx) => (
                      <div key={idx} className="space-y-1">
                        <div className="text-xs text-muted-foreground">{m.role === "user" ? "用户" : "AI"}</div>
                        <div className="whitespace-pre-wrap">{m.content}</div>
                      </div>
                    ))}
                  </div>
                </ScrollArea>
              </div>

              <div className="min-h-0">
                <div className="px-3 py-2 text-sm font-medium">工具日志</div>
                <ScrollArea className="h-[30vh] border-t">
                  <div className="space-y-3 p-3 text-sm">
                    {toolLogs.length === 0 ? <div className="text-muted-foreground">暂无工具日志</div> : null}
                    {toolLogs.map((l, idx) => {
                      if (l.type === "error") {
                        return (
                          <div key={idx} className="rounded border border-red-200 bg-red-50 p-2 text-red-700">
                            错误：{l.message}
                          </div>
                        );
                      }
                      if (l.type === "info") {
                        return (
                          <div key={idx} className="rounded border bg-muted p-2 text-muted-foreground">
                            {l.message}
                          </div>
                        );
                      }
                      if (l.type === "tool_call") {
                        return (
                          <details key={idx} className="rounded border p-2">
                            <summary className="cursor-pointer select-none text-xs text-muted-foreground">
                              tool_call · {l.tool} · {l.id}
                            </summary>
                            <pre className="mt-2 whitespace-pre-wrap text-xs">{JSON.stringify(l.args, null, 2)}</pre>
                          </details>
                        );
                      }
                      return (
                        <details key={idx} className="rounded border p-2">
                          <summary className="cursor-pointer select-none text-xs text-muted-foreground">
                            tool_result · {l.tool} · {l.id} · {l.ok ? "ok" : "fail"} · {l.ms}ms
                          </summary>
                          <pre className="mt-2 whitespace-pre-wrap text-xs">{JSON.stringify(l.result, null, 2)}</pre>
                        </details>
                      );
                    })}
                  </div>
                </ScrollArea>
              </div>
            </div>
          </div>

          <div className="border-t p-3">
            <div className="flex gap-2">
              <Textarea
                value={input}
                onChange={(e) => setInput(e.target.value)}
                placeholder="输入你的需求（Enter 发送，Shift+Enter 换行）"
                className="min-h-[72px] flex-1"
                disabled={loading}
                onKeyDown={(e) => {
                  if (e.key === "Enter" && !e.shiftKey) {
                    e.preventDefault();
                    if (canSend) void send();
                  }
                }}
              />
              <div className="flex flex-col gap-2">
                <Button disabled={!canSend} onClick={() => void send()}>
                  <Send className="mr-2 h-4 w-4" />
                  发送
                </Button>
                <Button variant="secondary" disabled={!loading} onClick={stop}>
                  <X className="mr-2 h-4 w-4" />
                  {aiProvider === "codex" ? "暂停" : "停止"}
                </Button>
              </div>
            </div>
            {!editorBridge ? (
              <div className="mt-2 text-xs text-muted-foreground">
                提示：编辑器尚未准备好，写入工具可能无法立即落入页面（稍等或刷新）。
              </div>
            ) : null}
          </div>
          </div>

          <div className={page === "chat" ? "hidden" : "min-h-0 flex-1 overflow-hidden"}>
            {page === "tools" ? (
              <ScrollArea className="h-full">
                <div className="space-y-3 p-3 text-sm">
                  {aiProvider === "online" ? (
                    <>
                      <div className="text-xs text-muted-foreground">
                        当前显示的是 online 主编排层真实注册的工具；后续新增 mindmap 等能力时，应同步出现在这里。
                      </div>
                      <div className="space-y-2">
                        {(onlineConfig?.tools || []).map((tool) => (
                          <div key={tool.name} className="rounded border bg-white p-3">
                            <div className="flex flex-wrap items-center gap-2">
                              <div className="font-medium">{tool.title}</div>
                              <code className="rounded bg-muted px-1 py-0.5 text-xs">{tool.name}</code>
                              <span className="rounded border px-1.5 py-0.5 text-xs">{tool.mode}</span>
                              <span className="rounded border px-1.5 py-0.5 text-xs">{tool.scope}</span>
                              <span className="rounded border px-1.5 py-0.5 text-xs">{tool.status}</span>
                            </div>
                            <div className="mt-2 text-xs text-muted-foreground">{tool.description}</div>
                          </div>
                        ))}
                        {onlineConfigLoading ? <div className="text-xs text-muted-foreground">正在读取工具注册表…</div> : null}
                        {!onlineConfigLoading && !onlineConfigError && (onlineConfig?.tools.length ?? 0) === 0 ? (
                          <div className="text-xs text-muted-foreground">当前未返回工具注册表。</div>
                        ) : null}
                        {onlineConfigError ? <div className="text-xs text-red-600">读取失败：{onlineConfigError}</div> : null}
                      </div>
                    </>
                  ) : (
                    <>
                      <div className="flex flex-wrap items-center gap-2">
                        <button
                          type="button"
                          className={`inline-flex items-center gap-1 rounded border px-2 py-1 ${toolAuto ? "bg-white" : "bg-muted"}`}
                          onClick={() => setToolAuto((v) => !v)}
                          disabled={loading}
                          title="工具自动/手动"
                        >
                          <Settings2 className="h-3.5 w-3.5" />
                          {toolAuto ? "自动工具" : "手动工具"}
                        </button>
                      </div>

                      {!toolAuto ? (
                        <div>
                          <div className="mb-2 text-xs text-muted-foreground">允许使用的工具（手动模式）</div>
                          <div className="flex flex-wrap gap-2">
                            {(Object.keys(TOOL_LABEL) as ToolName[]).map((t) => {
                              const on = selectedTools.includes(t);
                              return (
                                <button
                                  key={t}
                                  type="button"
                                  className={`rounded border px-2 py-1 text-xs ${on ? "bg-[#111827] text-white" : "bg-white"}`}
                                  onClick={() =>
                                    setSelectedTools((prev) =>
                                      prev.includes(t) ? prev.filter((x) => x !== t) : [...prev, t],
                                    )
                                  }
                                  disabled={loading}
                                >
                                  {TOOL_LABEL[t]}
                                </button>
                              );
                            })}
                          </div>
                        </div>
                      ) : (
                        <div className="text-xs text-muted-foreground">
                          自动模式下：AI 会在允许的 ToolSet 范围内自行选择工具。
                        </div>
                      )}
                    </>
                  )}
                </div>
              </ScrollArea>
            ) : null}

            {page === "history" ? (
              <ScrollArea className="h-full">
                <div className="p-3 text-sm">
                  <div className="flex flex-wrap items-center justify-between gap-2">
                    <div className="text-xs text-muted-foreground">最多保留 20 个会话</div>
                    <div className="flex flex-wrap items-center gap-2">
                      <Button variant="outline" onClick={resetCurrentSession} disabled={loading}>
                        清空当前
                      </Button>
                      <Button variant="outline" onClick={startNewSession} disabled={loading}>
                        <Plus className="mr-2 h-4 w-4" />
                        新建会话
                      </Button>
                      <Button variant="destructive" onClick={clearHistory} disabled={loading}>
                        清空历史
                      </Button>
                    </div>
                  </div>
                  <div className="mt-3 space-y-2">
                    {sessions.map((s) => {
                      const active = s.id === activeSessionId;
                      const time = new Date(s.updatedAt || s.createdAt).toLocaleString();
                      return (
                        <div
                          key={s.id}
                          className={`flex items-center gap-2 rounded border px-3 py-2 ${active ? "border-[#111827]" : "border-border"}`}
                        >
                          <button
                            type="button"
                            className="min-w-0 flex-1 text-left"
                            onClick={() => {
                              switchSession(s.id);
                              setPage("chat");
                            }}
                            disabled={loading}
                            title={active ? "当前会话" : "切换到该会话"}
                          >
                            <div className="flex items-center justify-between gap-2">
                              <div className="truncate font-medium">{s.title || "未命名"}</div>
                              <div className="shrink-0 text-xs text-muted-foreground">{time}</div>
                            </div>
                            <div className="mt-1 text-xs text-muted-foreground">消息 {s.messages.length} · 日志 {s.toolLogs.length}</div>
                          </button>
                          <Button
                            variant="ghost"
                            size="sm"
                            onClick={() => deleteSession(s.id)}
                            disabled={loading || active}
                            title={active ? "不能删除当前会话" : "删除会话"}
                          >
                            删除
                          </Button>
                        </div>
                      );
                    })}
                  </div>
                </div>
              </ScrollArea>
            ) : null}

            {page === "account" ? (
              <ScrollArea className="h-full">
                <div className="space-y-3 p-3 text-sm">
                  <div className="flex flex-wrap items-center gap-2">
                    <label className="text-xs text-muted-foreground">推理来源</label>
                    <select
                      className="h-9 rounded border bg-white px-2 text-sm"
                      value={aiProvider}
                      onChange={(e) => {
                        const v = String(e.target.value || "").trim();
                        if (v === "online" || v === "local" || v === "ollama" || v === "codex") setAiProvider(v);
                        else setAiProvider("online");
                      }}
                      disabled={loading}
                    >
                      <option value="online">在线</option>
                      <option value="local">本地</option>
                      <option value="ollama">Ollama</option>
                      <option value="codex">Codex</option>
                    </select>

                    {aiProvider === "codex" ? (
                      <div className="ml-2 space-y-2 text-xs text-muted-foreground">
                        <div>
                          使用本地 Codex 配置（<code className="rounded bg-muted px-1 py-0.5">~/.codex/config.toml</code>）。
                          在消息开头加 <code className="rounded bg-muted px-1 py-0.5">#chat</code> /{" "}
                          <code className="rounded bg-muted px-1 py-0.5">#test</code> /{" "}
                          <code className="rounded bg-muted px-1 py-0.5">#dev</code>（默认 <code className="rounded bg-muted px-1 py-0.5">#chat</code>）。
                        </div>
                        <div className="flex flex-wrap items-center gap-2">
                          <span>Codex Session</span>
                          <input
                            className="h-8 w-[360px] rounded border bg-white px-2 text-xs"
                            value={codexSessionDraft}
                            onChange={(e) => setCodexSessionDraft(e.target.value)}
                            placeholder="留空=本会话自动创建；也可粘贴 VSCode/Codex CLI 的 thread_id 续聊"
                            disabled={loading}
                          />
                          <Button
                            type="button"
                            size="sm"
                            variant="secondary"
                            disabled={loading || !activeSessionId}
                            onClick={() => {
                              const nextId = codexSessionDraft.trim() || null;
                              if (!activeSessionId) return;
                              setSessions((prev) =>
                                normalizeSessions(
                                  prev.map((s) =>
                                    s.id === activeSessionId ? { ...s, codexSessionId: nextId, updatedAt: Date.now() } : s,
                                  ),
                                ),
                              );
                            }}
                          >
                            绑定
                          </Button>
                          <Button
                            type="button"
                            size="sm"
                            variant="ghost"
                            disabled={loading || !activeSessionId}
                            onClick={() => {
                              if (!activeSessionId) return;
                              setCodexSessionDraft("");
                              setSessions((prev) =>
                                normalizeSessions(
                                  prev.map((s) =>
                                    s.id === activeSessionId ? { ...s, codexSessionId: null, updatedAt: Date.now() } : s,
                                  ),
                                ),
                              );
                            }}
                          >
                            清除
                          </Button>
                          <Button
                            type="button"
                            size="sm"
                            variant="ghost"
                            disabled={loading || !String(currentSession?.codexSessionId ?? "").trim()}
                            onClick={() => {
                              const sid = String(currentSession?.codexSessionId ?? "").trim();
                              if (!sid) return;
                              void navigator.clipboard?.writeText(sid).catch(() => null);
                            }}
                          >
                            复制当前
                          </Button>
                        </div>
                        <div>
                          提示：你可以在 VSCode 里继续用 Codex CLI 编辑，同时在这里用 <code className="rounded bg-muted px-1 py-0.5">#dev</code> 进行编辑；若要共享上下文，请绑定同一个 SessionId。避免两边同时改同一文件以免冲突。
                        </div>
                      </div>
                    ) : aiProvider === "online" ? (
                      <>
                        <label className="ml-2 text-xs text-muted-foreground">模型键</label>
                        <select
                          className="h-9 min-w-[260px] rounded border bg-white px-2 text-sm"
                          value={aiModelKey}
                          onChange={(e) => setAiModelKey(String(e.target.value || "").trim())}
                          disabled={loading || onlineConfigLoading || !onlineConfig}
                        >
                          {onlineConfig?.models.map((item) => (
                            <option key={item.key} value={item.key}>
                              {item.title} · {item.key}
                            </option>
                          ))}
                        </select>
                      </>
                    ) : (
                      <>
                        <label className="ml-2 text-xs text-muted-foreground">模型（可选）</label>
                        {aiProvider === "ollama" ? (
                          <select
                            className="h-9 rounded border bg-white px-2 text-sm"
                            value={aiModel === "" || aiModel === OLLAMA_QWEN3_30B ? aiModel : ""}
                            onChange={(e) => setAiModel(e.target.value)}
                            disabled={loading}
                          >
                            <option value="">默认：{OLLAMA_QWEN3_30B}</option>
                            <option value={OLLAMA_QWEN3_30B}>{OLLAMA_QWEN3_30B}</option>
                          </select>
                        ) : (
                          <input
                            list="doc-ai-model-suggestions"
                            className="h-9 w-[260px] rounded border px-2 text-sm"
                            value={aiModel}
                            onChange={(e) => setAiModel(e.target.value)}
                            placeholder="例如 gemini-2.5-pro"
                            disabled={loading}
                          />
                        )}
                      </>
                    )}
                  </div>
                  {aiProvider === "online" ? (
                    <div className="space-y-2 rounded border bg-muted/30 p-3 text-xs text-muted-foreground">
                      <div>
                        在线 provider 固定走 OmniRoute。Base URL：
                        <code className="ml-1 rounded bg-muted px-1 py-0.5">{onlineConfig?.baseUrl || "http://localhost:20128/v1"}</code>
                      </div>
                      <div>
                        当前模型键：
                        <code className="ml-1 rounded bg-muted px-1 py-0.5">{selectedOnlineModel?.key || "未选择"}</code>
                        {" · "}
                        Combo：
                        <code className="ml-1 rounded bg-muted px-1 py-0.5">{selectedOnlineModel?.resolvedCombo || "由网关运行时解析"}</code>
                      </div>
                      <div>
                        Runtime Model：
                        <code className="ml-1 rounded bg-muted px-1 py-0.5">
                          {selectedOnlineModel?.resolvedRuntimeModel || "由网关运行时解析"}
                        </code>
                      </div>
                      <div>
                        说明：这里的 Runtime Model 表示当前页面 AI 实际发送到 OmniRoute 的 runtime model 字符串；更底层 provider 映射仍由网关内部维护。
                      </div>
                      {onlineConfigLoading ? <div>正在读取页面 AI 配置…</div> : null}
                      {onlineConfigError ? <div className="text-red-600">读取失败：{onlineConfigError}</div> : null}
                    </div>
                  ) : null}
                  <div className="text-xs text-muted-foreground">
                    说明：在线/本地的 BaseURL 与 Key 仍由配置文件/环境变量控制；这里仅做 provider 与 model 覆盖。Ollama 默认使用 <code className="rounded bg-muted px-1 py-0.5">http://127.0.0.1:11434/v1</code>。
                  </div>
                </div>
              </ScrollArea>
            ) : null}

            {page === "settings" ? (
              <ScrollArea className="h-full">
                <div className="space-y-3 p-3 text-sm">
                  <div className="flex flex-wrap items-center gap-3">
                    <button
                      type="button"
                      className={`inline-flex items-center gap-1 rounded border px-2 py-1 ${networkOn ? "bg-white" : "bg-muted"}`}
                      onClick={() => setNetworkOn((v) => !v)}
                      disabled={loading}
                      title="联网检索"
                    >
                      <Network className="h-3.5 w-3.5" />
                      {networkOn ? "联网" : "离线"}
                    </button>

                    <label className="flex items-center gap-2">
                      步数
                      <input
                        className="w-[92px] rounded border px-2 py-1 text-sm"
                        type="number"
                        min={1}
                        max={24}
                        step={1}
                        value={maxSteps}
                        onChange={(e) => {
                          const v = Number(e.target.value);
                          if (!Number.isFinite(v)) return;
                          setMaxSteps(clamp(Math.floor(v), 1, 24));
                        }}
                        disabled={loading}
                      />
                    </label>
                  </div>
                  {aiProvider === "online" ? (
                    <div className="space-y-3 rounded border bg-white p-3">
                      <div className="flex flex-wrap items-center gap-2">
                        <label className="text-xs text-muted-foreground">Soul / Profile</label>
                        <select
                          className="h-9 min-w-[240px] rounded border bg-white px-2 text-sm"
                          value={aiProfileId}
                          onChange={(e) => setAiProfileId(String(e.target.value || "").trim())}
                          disabled={loading || onlineConfigLoading || !onlineConfig}
                        >
                          {onlineConfig?.profiles.map((profile) => (
                            <option key={profile.id} value={profile.id}>
                              {profile.title}
                            </option>
                          ))}
                        </select>
                      </div>
                      <div className="text-xs text-muted-foreground">
                        {selectedOnlineProfile?.description || "当前未读取到 profile 描述。"}
                      </div>
                      <div className="text-xs text-muted-foreground">
                        说明：Profile 由后端 registry 持有真源，前端只做选择和展示。
                      </div>
                      <div className="text-xs text-muted-foreground">
                        会话记忆：
                        <code className="ml-1 rounded bg-muted px-1 py-0.5">
                          {onlineConfig?.sessionEnabled ? `已启用 · ${onlineSessionId || "等待会话初始化"}` : "未启用"}
                        </code>
                      </div>
                      <div className="space-y-2 rounded border bg-muted/20 p-3">
                        <div className="text-xs font-medium text-foreground">当前 scope 已注册工具</div>
                        <div className="text-xs text-muted-foreground">
                          这里直接显示 online 主编排层返回的真实 tool registry；后续如果新增 mindmap 或页面工具，应在这里同步可见。
                        </div>
                        {onlineConfigLoading ? <div className="text-xs text-muted-foreground">正在读取工具注册表…</div> : null}
                        {!onlineConfigLoading && !onlineConfigError && (onlineConfig?.tools.length ?? 0) === 0 ? (
                          <div className="text-xs text-muted-foreground">当前未返回工具注册表。</div>
                        ) : null}
                        {onlineConfigError ? <div className="text-xs text-red-600">读取失败：{onlineConfigError}</div> : null}
                        <div className="flex flex-wrap gap-2">
                          {(onlineConfig?.tools || []).map((tool) => (
                            <div key={tool.name} className="rounded border bg-white px-2 py-1 text-xs">
                              <div className="font-medium">{tool.title}</div>
                              <div className="text-muted-foreground">
                                <code className="rounded bg-muted px-1 py-0.5">{tool.name}</code>
                                {" · "}
                                {tool.mode}
                                {" / "}
                                {tool.scope}
                              </div>
                            </div>
                          ))}
                        </div>
                      </div>
                    </div>
                  ) : null}
                  <div className="text-xs text-muted-foreground">说明：步数越大越“能做事”，但会更慢且更消耗推理额度。</div>
                </div>
              </ScrollArea>
            ) : null}
          </div>
        </div>

        {/* 工具（代替 MCP） */}
        <Dialog open={toolsDialogOpen} onOpenChange={setToolsDialogOpen}>
          <DialogContent showCloseButton={false} className="max-w-[720px]">
            <DialogHeader>
              <DialogTitle>工具（代替 MCP）</DialogTitle>
            </DialogHeader>
            <div className="space-y-3 text-sm">
              <div className="flex flex-wrap items-center gap-2">
                <button
                  type="button"
                  className={`inline-flex items-center gap-1 rounded border px-2 py-1 ${toolAuto ? "bg-white" : "bg-muted"}`}
                  onClick={() => setToolAuto((v) => !v)}
                  disabled={loading}
                  title="工具自动/手动"
                >
                  <Settings2 className="h-3.5 w-3.5" />
                  {toolAuto ? "自动工具" : "手动工具"}
                </button>
              </div>

              {!toolAuto ? (
                <div>
                  <div className="mb-2 text-xs text-muted-foreground">允许使用的工具（手动模式）</div>
                  <div className="flex flex-wrap gap-2">
                    {(Object.keys(TOOL_LABEL) as ToolName[]).map((t) => {
                      const on = selectedTools.includes(t);
                      return (
                        <button
                          key={t}
                          type="button"
                          className={`rounded border px-2 py-1 text-xs ${on ? "bg-[#111827] text-white" : "bg-white"}`}
                          onClick={() =>
                            setSelectedTools((prev) =>
                              prev.includes(t) ? prev.filter((x) => x !== t) : [...prev, t],
                            )
                          }
                          disabled={loading}
                        >
                          {TOOL_LABEL[t]}
                        </button>
                      );
                    })}
                  </div>
                </div>
              ) : (
                <div className="text-xs text-muted-foreground">
                  自动模式下：AI 会在允许的 ToolSet 范围内自行选择工具。
                </div>
              )}
            </div>
          </DialogContent>
        </Dialog>

        {/* 历史 */}
        <Dialog open={historyDialogOpen} onOpenChange={setHistoryDialogOpen}>
          <DialogContent showCloseButton={false} className="max-w-[720px]">
            <DialogHeader>
              <DialogTitle>历史会话</DialogTitle>
            </DialogHeader>
            <div className="flex flex-wrap items-center justify-between gap-2">
              <div className="text-xs text-muted-foreground">最多保留 20 个会话</div>
              <div className="flex flex-wrap items-center gap-2">
                <Button variant="outline" onClick={resetCurrentSession} disabled={loading}>
                  清空当前
                </Button>
                <Button variant="outline" onClick={startNewSession} disabled={loading}>
                  <Plus className="mr-2 h-4 w-4" />
                  新建会话
                </Button>
                <Button variant="destructive" onClick={clearHistory} disabled={loading}>
                  清空历史
                </Button>
              </div>
            </div>
            <div className="mt-3 space-y-2">
              {sessions.map((s) => {
                const active = s.id === activeSessionId;
                const time = new Date(s.updatedAt || s.createdAt).toLocaleString();
                return (
                  <div key={s.id} className={`flex items-center gap-2 rounded border px-3 py-2 ${active ? "border-[#111827]" : "border-border"}`}>
                    <button
                      type="button"
                      className="min-w-0 flex-1 text-left"
                      onClick={() => {
                        switchSession(s.id);
                        setHistoryDialogOpen(false);
                      }}
                      disabled={loading}
                      title={active ? "当前会话" : "切换到该会话"}
                    >
                      <div className="flex items-center justify-between gap-2">
                        <div className="truncate font-medium">{s.title || "未命名"}</div>
                        <div className="shrink-0 text-xs text-muted-foreground">{time}</div>
                      </div>
                      <div className="mt-1 text-xs text-muted-foreground">
                        消息 {s.messages.length} · 日志 {s.toolLogs.length}
                      </div>
                    </button>
                    <Button
                      variant="ghost"
                      size="sm"
                      onClick={() => deleteSession(s.id)}
                      disabled={loading || active}
                      title={active ? "不能删除当前会话" : "删除会话"}
                    >
                      删除
                    </Button>
                  </div>
                );
              })}
            </div>
          </DialogContent>
        </Dialog>

        {/* 账户/模型 */}
        <Dialog open={accountDialogOpen} onOpenChange={setAccountDialogOpen}>
          <DialogContent showCloseButton={false} className="max-w-[720px]">
            <DialogHeader>
              <DialogTitle>账户 / 模型</DialogTitle>
            </DialogHeader>
            <div className="space-y-3 text-sm">
              <div className="flex flex-wrap items-center gap-2">
                <label className="text-xs text-muted-foreground">推理来源</label>
                <select
                  className="h-9 rounded border bg-white px-2 text-sm"
                  value={aiProvider}
                  onChange={(e) => {
                    const v = String(e.target.value || "").trim();
                    if (v === "online" || v === "local" || v === "ollama" || v === "codex") setAiProvider(v);
                    else setAiProvider("online");
                  }}
                  disabled={loading}
                >
                  <option value="online">在线</option>
                  <option value="local">本地</option>
                  <option value="ollama">Ollama</option>
                  <option value="codex">Codex</option>
                </select>

                {aiProvider === "codex" ? (
                  <div className="ml-2 text-xs text-muted-foreground">
                    消息开头加 <code className="rounded bg-muted px-1 py-0.5">#chat</code> /{" "}
                    <code className="rounded bg-muted px-1 py-0.5">#test</code> /{" "}
                    <code className="rounded bg-muted px-1 py-0.5">#dev</code>（默认 <code className="rounded bg-muted px-1 py-0.5">#chat</code>）。
                  </div>
                ) : (
                  <>
                    <label className="ml-2 text-xs text-muted-foreground">模型（可选）</label>
                    {aiProvider === "ollama" ? (
                      <select
                        className="h-9 rounded border bg-white px-2 text-sm"
                        value={aiModel === "" || aiModel === OLLAMA_QWEN3_30B ? aiModel : ""}
                        onChange={(e) => setAiModel(e.target.value)}
                        disabled={loading}
                      >
                        <option value="">默认：{OLLAMA_QWEN3_30B}</option>
                        <option value={OLLAMA_QWEN3_30B}>{OLLAMA_QWEN3_30B}</option>
                      </select>
                    ) : (
                      <input
                        list="doc-ai-model-suggestions-dialog"
                        className="h-9 w-[260px] rounded border px-2 text-sm"
                        value={aiModel}
                        onChange={(e) => setAiModel(e.target.value)}
                        placeholder="例如 gemini-2.5-pro"
                        disabled={loading}
                      />
                    )}
                    <datalist id="doc-ai-model-suggestions-dialog">
                      <option value={OLLAMA_QWEN3_30B} />
                    </datalist>
                  </>
                )}
              </div>
              <div className="text-xs text-muted-foreground">
                说明：在线/本地的 BaseURL 与 Key 仍由配置文件/环境变量控制；这里仅做 provider 与 model 覆盖。Ollama 默认使用 <code className="rounded bg-muted px-1 py-0.5">http://127.0.0.1:11434/v1</code>。
              </div>
            </div>
          </DialogContent>
        </Dialog>

        {/* 设置 */}
        <Dialog open={settingsDialogOpen} onOpenChange={setSettingsDialogOpen}>
          <DialogContent showCloseButton={false} className="max-w-[720px]">
            <DialogHeader>
              <DialogTitle>设置</DialogTitle>
            </DialogHeader>
            <div className="space-y-3 text-sm">
              <div className="flex flex-wrap items-center gap-3">
                <button
                  type="button"
                  className={`inline-flex items-center gap-1 rounded border px-2 py-1 ${networkOn ? "bg-white" : "bg-muted"}`}
                  onClick={() => setNetworkOn((v) => !v)}
                  disabled={loading}
                  title="联网检索"
                >
                  <Network className="h-3.5 w-3.5" />
                  {networkOn ? "联网" : "离线"}
                </button>

                <label className="flex items-center gap-2">
                  步数
                  <input
                    className="w-[92px] rounded border px-2 py-1 text-sm"
                    type="number"
                    min={1}
                    max={24}
                    step={1}
                    value={maxSteps}
                    onChange={(e) => {
                      const v = Number(e.target.value);
                      if (!Number.isFinite(v)) return;
                      setMaxSteps(clamp(Math.floor(v), 1, 24));
                    }}
                    disabled={loading}
                  />
                </label>
              </div>
              <div className="text-xs text-muted-foreground">说明：步数越大越“能做事”，但会更慢且更消耗推理额度。</div>
            </div>
          </DialogContent>
        </Dialog>
        </AiBridgePanel>
      </SheetContent>
    </Sheet>
  );
}

export { DocumentAiAgentPanelRuntime as DocumentAiAgentPanel };
