const { useState, useEffect, useMemo, useRef } = React;

// 简单 Markdown 渲染组件，优先使用 window.marked
function Markdown({ content }) {
  const html = useMemo(() => {
    if (!content) return "";
    if (window.marked && window.marked.parse) {
      return window.marked.parse(content, { breaks: true });
    }
    // 兜底：仅做换行处理，避免完全不可读
    return String(content).replace(/\n/g, "<br />");
  }, [content]);

  return <div className="markdown-body" dangerouslySetInnerHTML={{ __html: html }} />;
}

// 将字符串尽量按 JSON 美化后用于展示（不会修改原数据）
function formatJsonForDisplay(value) {
  if (value == null) return "";
  if (typeof value !== "string") {
    try {
      return JSON.stringify(value, null, 2);
    } catch {
      return String(value);
    }
  }
  const trimmed = value.trim();
  if (!trimmed) return "";
  try {
    // 尝试把字符串当 JSON 解析，再格式化
    const parsed = JSON.parse(trimmed);
    return JSON.stringify(parsed, null, 2);
  } catch {
    // 不是合法 JSON，则原样返回（保持和原始内容一致）
    return value;
  }
}

// 专门用于 Response JSON 的展示：
// - 如果是 { streaming: [...] }，只展开 streaming 数组，每项一行
// - 如果是纯数组，也按「每项一行」展示
// - 其它情况回退到通用 JSON 美化
function formatResponseJsonForDisplay(value) {
  if (value == null) return "";

  const raw =
    typeof value === "string"
      ? value
      : (() => {
          try {
            return JSON.stringify(value);
          } catch {
            return String(value);
          }
        })();

  try {
    const parsed = JSON.parse(raw);

    // 兼容 { streaming: [...] } 结构
    if (parsed && Array.isArray(parsed.streaming)) {
      return parsed.streaming.map((item) => JSON.stringify(item)).join("\n");
    }

    // 兼容直接存数组（当前后端实现）
    if (Array.isArray(parsed)) {
      return parsed.map((item) => JSON.stringify(item)).join("\n");
    }

    // 其它情况：正常美化整个 JSON
    return formatJsonForDisplay(raw);
  } catch {
    // 不是合法 JSON，则原样返回
    return raw;
  }
}

const TOKEN_KEY = "agents_console_token";
const VARIABLES_STORAGE_KEY = "agents_console_variables";

// ===== Variables helpers =====

function createEmptyVariable() {
  return {
    id: crypto.randomUUID(),
    name: "",
    type: "short", // 'short' | 'long' | 'rule'
    ruleType: "current_time", // for type === 'rule'
    value: "",
  };
}

// 计算单个变量当前值（静态或规则）
function computeVariableValue(v) {
  if (!v) return "";
  if (v.type === "rule") {
    const now = new Date();
    if (v.ruleType === "current_time") {
      return now.toLocaleString();
    }
    if (v.ruleType === "hour_to_season") {
      const h = now.getHours(); // 0-23
      // 0-6: 春, 7-12: 夏, 13-16: 秋, 17-23: 冬
      if (h >= 0 && h <= 6) return "春";
      if (h >= 7 && h <= 12) return "夏";
      if (h >= 13 && h <= 16) return "秋";
      return "冬";
    }
    return "";
  }
  return v.value || "";
}

// 把文本中的 @变量名 替换为变量值（仅用于预览/发送）
function resolveVariablesInText(text, variables) {
  if (!text || !variables || variables.length === 0) return text;
  const map = {};
  for (const v of variables) {
    if (!v.name) continue;
    map[v.name] = computeVariableValue(v);
  }
  // 支持中文等 Unicode 字符的变量名
  const re = /@([\p{L}\p{N}_]+)/gu;
  return String(text).replace(re, (match, name) => {
    if (Object.prototype.hasOwnProperty.call(map, name)) {
      return map[name];
    }
    return match;
  });
}

// ===== Types (JS 版本，仅做注释参考) =====
// OpenAIConfig: { baseUrl, apiKey, organization?, project? }
// Agent: { id, name, description, model, temperature, topP, maxTokens, presencePenalty, frequencyPenalty, seed?, stop? }
// Conversation: { id, agentId, title?, messages: Array<Message> }
// Message: { id, role, content, createdAt? }

// 将后端 AgentOut + config 转成前端 Agent 对象
function normalizeAgent(agent) {
  const cfg = agent.config || {};
  return {
    id: String(agent.id),
    name: agent.name || "",
    description: agent.description || "",
    model: agent.model || "",
    temperature: cfg.temperature ?? 1,
    topP: cfg.top_p ?? 1,
    maxTokens: cfg.max_tokens ?? undefined,
    presencePenalty: cfg.presence_penalty ?? 0,
    frequencyPenalty: cfg.frequency_penalty ?? 0,
    seed: cfg.extra?.seed ?? undefined,
    stop: cfg.extra?.stop ?? "",
    note: cfg.extra?.note ?? "",
  };
}

// 将后端 ConversationOut 转成前端使用的结构
function normalizeConversation(conv) {
  return {
    id: conv.id,
    agentId: String(conv.agent_id),
    title: conv.title || "",
    messages: (conv.messages || []).map((m) => ({
      id: m.id,
      role: m.role,
      content: m.content,
      createdAt: null,
      requestJson: m.request_json || null,
      responseJson: m.response_json || null,
      isError: m.is_error || false,
    })),
  };
}

// 将前端 Agent 转成后端需要的 config 结构
function buildAgentConfig(agent) {
  return {
    temperature: agent.temperature,
    top_p: agent.topP,
    max_tokens: agent.maxTokens ?? null,
    presence_penalty: agent.presencePenalty,
    frequency_penalty: agent.frequencyPenalty,
    extra: {
      seed: agent.seed ?? null,
      stop: agent.stop || "",
      note: agent.note || "",
    },
  };
}

// ===== Login =====

function Login({ onLogin }) {
  const [secret, setSecret] = useState("");
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const trimmed = secret.trim();
    if (!trimmed) {
      setError("请输入密钥");
      return;
    }
    setLoading(true);
    setError(null);
    try {
      const res = await fetch("/api/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ secret: trimmed }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok || !data.token) {
        setError(data.detail || data.error || "登录失败，请检查密钥");
        return;
      }
      const token = data.token;
      localStorage.setItem(TOKEN_KEY, token);
      onLogin(token);
    } catch (err) {
      setError(err?.message || "登录请求失败");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="app-root">
      <main className="main">
        <div className="panel">
          <div className="panel-header">
            <h1>登录 🤖 Agents 控制台</h1>
            <p className="muted">
              请输入本地配置文件中的密钥，以访问本地 API。
            </p>
          </div>
          <form onSubmit={handleSubmit} className="form-grid">
            <label className="field">
              <span className="label">密钥</span>
              <input
                className="input"
                type="password"
                value={secret}
                onChange={(e) => setSecret(e.target.value)}
                placeholder="请输入本地配置中的 app_secret"
              />
            </label>
            <div className="panel-actions">
              <button className="primary-btn" type="submit" disabled={loading}>
                {loading ? "登录中..." : "登录"}
              </button>
              {error && <span className="error-text">{error}</span>}
            </div>
          </form>
        </div>
      </main>
    </div>
  );
}

// ===== AgentsPanel =====

function createEmptyAgent(modelOptions = []) {
  const defaultModel =
    Array.isArray(modelOptions) && modelOptions.length > 0 ? modelOptions[0] : "";
  return {
    id: crypto.randomUUID(),
    name: "",
    description: "",
    model: defaultModel,
    temperature: 0.7,
    topP: 1,
    maxTokens: 2048,
    presencePenalty: 0,
    frequencyPenalty: 0,
    seed: undefined,
    stop: "",
    note: "",
  };
}

function AgentsPanel({
  agents,
  onChange,
  onSelectAgent,
  modelOptions = [],
  authToken,
  variables = [],
}) {
  const [editingId, setEditingId] = useState(null);

  const editingAgent = useMemo(
    () => agents.find((a) => a.id === editingId) || null,
    [agents, editingId],
  );

  const handleCreate = () => {
    const next = createEmptyAgent(modelOptions);
    if (!next.model?.trim()) {
      alert(
        "请选择或输入模型。若下拉列表为空，请先在设置中配置 OpenAI 并获取模型列表。",
      );
      return;
    }
    const optimisticList = [...agents, next];
    onChange(optimisticList);
    setEditingId(next.id);

    fetch("/api/agents", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${authToken}`,
      },
      body: JSON.stringify({
        name: next.name,
        model: next.model,
        description: next.description,
        config: buildAgentConfig(next),
      }),
    })
      .then(async (res) => {
        if (!res.ok) return null;
        const saved = await res.json().catch(() => null);
        if (!saved || saved.id == null) return null;
        return saved;
      })
      .then((saved) => {
        if (!saved) return;
        const normalized = normalizeAgent(saved);
        const list = optimisticList.map((a) =>
          a.id === next.id ? normalized : a,
        );
        onChange(list);
        setEditingId(normalized.id);
      })
      .catch(() => {});
  };

  const handleUpdate = (patch) => {
    if (!editingAgent) return;
    const updated = { ...editingAgent, ...patch };
    const list = agents.map((a) => (a.id === updated.id ? updated : a));
    onChange(list);

    const numericId = Number(updated.id);

    fetch(`/api/agents/${numericId}`, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${authToken}`,
      },
      body: JSON.stringify({
        name: updated.name,
        model: updated.model,
        description: updated.description,
        config: buildAgentConfig(updated),
      }),
    }).catch(() => {});
  };

  const handleDelete = (id) => {
    const list = agents.filter((a) => a.id !== id);
    onChange(list);
    if (editingId === id) {
      setEditingId(null);
    }

    const numericId = Number(id);
    if (!Number.isFinite(numericId)) return;

    fetch(`/api/agents/${numericId}`, {
      method: "DELETE",
      headers: {
        Authorization: `Bearer ${authToken}`,
      },
    }).catch(() => {});
  };

  const renderVariableHints = (onInsert) => {
    if (!variables || variables.length === 0) {
      return (
        <div className="variable-hints muted small">
          还没有变量，可以在左侧「变量」标签页中创建，然后通过 @变量名 引用。
        </div>
      );
    }
    return (
      <div className="variable-hints small">
        <span className="label">可用变量：</span>
        {variables.map((v) => (
          <button
            key={v.id}
            type="button"
            className="pill pill-clickable"
            onClick={() => onInsert && onInsert(v.name)}
          >
            @{v.name}
          </button>
        ))}
      </div>
    );
  };

  const renderPreview = (value) => {
    if (!value) return null;
    const resolved = resolveVariablesInText(value, variables);
    if (resolved === value) return null;
    return (
      <div className="variable-preview">
        <span className="label small">预览（变量已展开）：</span>
        <div className="variable-preview-box small">{resolved}</div>
      </div>
    );
  };

  return (
    <div className="panel panel-split">
      <div className="panel-left">
        <div className="panel-header">
          <h1>Agents 管理</h1>
          <p className="muted">为不同用途创建多个带有独立参数的 Agent。</p>
        </div>
        <div className="list-header">
          <button className="primary-btn" onClick={handleCreate}>
            新建 Agent
          </button>
        </div>
        <div className="agents-list">
          {agents.length === 0 && (
            <p className="muted">还没有 Agent，点击「新建 Agent」开始。</p>
          )}
          {agents.map((agent) => (
            <button
              key={agent.id}
              className={
                editingId === agent.id ? "agent-item active" : "agent-item"
              }
              onClick={() => setEditingId(agent.id)}
            >
              <div className="agent-name">{agent.name || "未命名 Agent"}</div>
              <div className="agent-meta">
                <span className="pill pill-soft">{agent.model || "未选模型"}</span>
              </div>
            </button>
          ))}
        </div>
      </div>

      <div className="panel-right">
        {!editingAgent ? (
          <div className="empty-state">
            <h2>选择或创建一个 Agent</h2>
            <p className="muted">
              在左侧列表中选择已有 Agent，或点击「新建 Agent」创建新的配置。
            </p>
          </div>
        ) : (
          <>
            <div className="form-grid">
              <label className="field">
                <span className="label">名称</span>
                <input
                  className="input"
                  value={editingAgent.name}
                  onChange={(e) => handleUpdate({ name: e.target.value })}
                  placeholder="例如：代码助手、翻译助手"
                />
              </label>
              <label className="field">
                <span className="label">模型</span>
                {modelOptions && modelOptions.length > 0 ? (
                  <select
                    className="input"
                    value={editingAgent.model}
                    onChange={(e) => handleUpdate({ model: e.target.value })}
                  >
                    <option value="">选择模型</option>
                    {modelOptions.map((m) => (
                      <option key={m} value={m}>
                        {m}
                      </option>
                    ))}
                  </select>
                ) : (
                  <input
                    className="input"
                    value={editingAgent.model}
                    onChange={(e) => handleUpdate({ model: e.target.value })}
                    placeholder="例如：gpt-4.1, gpt-4o, deepseek-chat"
                  />
                )}
              </label>
            </div>

            <label className="field">
              <span className="label">System Prompt</span>
              <textarea
                className="input textarea"
                value={editingAgent.description}
                onChange={(e) => handleUpdate({ description: e.target.value })}
                placeholder="为该 Agent 设定系统提示词，例如：你是一个精通 Python 的代码助手。支持通过 @变量名 引用变量。"
              />
              {renderVariableHints((name) =>
                handleUpdate({
                  description: `${editingAgent.description || ""}@${name}`,
                }),
              )}
              {renderPreview(editingAgent.description)}
            </label>

            <label className="field">
              <span className="label">描述（可选）</span>
              <input
                className="input"
                value={editingAgent.note || ""}
                onChange={(e) =>
                  handleUpdate({
                    note: e.target.value,
                  })
                }
                placeholder="给自己看的备注，例如：用于翻译英文邮件。支持通过 @变量名 引用变量。"
              />
              {renderVariableHints((name) =>
                handleUpdate({
                  note: `${editingAgent.note || ""}@${name}`,
                }),
              )}
              {renderPreview(editingAgent.note || "")}
            </label>

            <div className="form-row-3col">
              <label className="field">
                <span className="label">temperature</span>
                <input
                  className="input"
                  type="number"
                  min={0}
                  max={2}
                  step={0.1}
                  value={editingAgent.temperature}
                  onChange={(e) =>
                    handleUpdate({ temperature: Number(e.target.value) })
                  }
                />
              </label>
              <label className="field">
                <span className="label">top_p</span>
                <input
                  className="input"
                  type="number"
                  min={0}
                  max={1}
                  step={0.05}
                  value={editingAgent.topP}
                  onChange={(e) =>
                    handleUpdate({ topP: Number(e.target.value) })
                  }
                />
              </label>
              <label className="field">
                <span className="label">max_tokens</span>
                <input
                  className="input"
                  type="number"
                  min={1}
                  value={editingAgent.maxTokens ?? ""}
                  onChange={(e) =>
                    handleUpdate({
                      maxTokens: e.target.value
                        ? Number(e.target.value)
                        : undefined,
                    })
                  }
                />
              </label>
            </div>

            <div className="form-row-3col">
              <label className="field">
                <span className="label">presence_penalty</span>
                <input
                  className="input"
                  type="number"
                  min={-2}
                  max={2}
                  step={0.1}
                  value={editingAgent.presencePenalty}
                  onChange={(e) =>
                    handleUpdate({
                      presencePenalty: Number(e.target.value),
                    })
                  }
                />
              </label>
              <label className="field">
                <span className="label">frequency_penalty</span>
                <input
                  className="input"
                  type="number"
                  min={-2}
                  max={2}
                  step={0.1}
                  value={editingAgent.frequencyPenalty}
                  onChange={(e) =>
                    handleUpdate({
                      frequencyPenalty: Number(e.target.value),
                    })
                  }
                />
              </label>
              <label className="field">
                <span className="label">seed</span>
                <input
                  className="input"
                  type="number"
                  value={editingAgent.seed ?? ""}
                  onChange={(e) =>
                    handleUpdate({
                      seed: e.target.value ? Number(e.target.value) : undefined,
                    })
                  }
                />
              </label>
            </div>

            <label className="field">
              <span className="label">stop（可选，逗号分隔）</span>
              <input
                className="input"
                value={editingAgent.stop ?? ""}
                onChange={(e) => handleUpdate({ stop: e.target.value })}
                placeholder={"例如：\nEND,用户："}
              />
            </label>

            <div className="panel-actions">
              <button
                className="secondary-btn"
                onClick={() => onSelectAgent(editingAgent.id)}
              >
                使用该 Agent 开始对话
              </button>
              <button
                className="danger-btn"
                onClick={() => handleDelete(editingAgent.id)}
              >
                删除 Agent
              </button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// ===== ChatPanel =====

function ChatPanel({
  agents,
  activeAgentId,
  onActiveAgentChange,
  conversations,
  onConversationSelect,
  conversation,
  onConversationChange,
  authToken,
  variables = [],
}) {
  const [input, setInput] = useState("");
  const [streaming, setStreaming] = useState(false);
  const [rawRequest, setRawRequest] = useState("");
  const [rawResponse, setRawResponse] = useState("");
  const [showRequest, setShowRequest] = useState(false);
  const [showResponse, setShowResponse] = useState(false);
  const messagesEndRef = useRef(null);
  const inputRef = useRef(null);

  const [mentionQuery, setMentionQuery] = useState("");
  const [mentionOpen, setMentionOpen] = useState(false);
  const [mentionCandidates, setMentionCandidates] = useState([]);

  const resolvedInput = useMemo(
    () => resolveVariablesInText(input, variables),
    [input, variables],
  );

  const activeAgent = useMemo(
    () => agents.find((a) => a.id === activeAgentId) ?? null,
    [agents, activeAgentId],
  );

  const agentConversations = useMemo(
    () =>
      (conversations || []).filter(
        (c) => String(c.agentId) === String(activeAgentId),
      ),
    [conversations, activeAgentId],
  );

  useEffect(() => {
    if (!conversation && activeAgentId) {
      onConversationChange({
        id: null,
        agentId: activeAgentId,
        messages: [],
      });
    }
  }, [activeAgentId, conversation, onConversationChange]);

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [conversation?.messages?.length]);

  const updateMentionState = (value, cursorPos) => {
    if (!variables || variables.length === 0) {
      setMentionQuery("");
      setMentionCandidates([]);
      setMentionOpen(false);
      return;
    }
    const pos = typeof cursorPos === "number" ? cursorPos : value.length;
    const before = value.slice(0, pos);
    const match = before.match(/@([\p{L}\p{N}_]*)$/u);
    if (!match) {
      setMentionQuery("");
      setMentionCandidates([]);
      setMentionOpen(false);
      return;
    }
    const q = match[1] || "";
    const candidates = variables.filter(
      (v) => v.name && v.name.toLowerCase().startsWith(q.toLowerCase()),
    );
    setMentionQuery(q);
    setMentionCandidates(candidates);
    setMentionOpen(candidates.length > 0);
  };

  const insertMention = (name) => {
    if (!name) return;
    const el = inputRef.current;
    const current = input || "";
    if (!el) {
      const next = `${current}@${name} `;
      setInput(next);
      setMentionOpen(false);
      return;
    }
    const cursor = el.selectionStart ?? current.length;
    const before = current.slice(0, cursor);
    const after = current.slice(cursor);
    const match = before.match(/@([\p{L}\p{N}_]*)$/u);
    let nextBefore;
    if (match) {
      nextBefore =
        before.slice(0, before.length - match[0].length) + `@${name} `;
    } else {
      nextBefore = `${before}@${name} `;
    }
    const next = nextBefore + after;
    setInput(next);
    setMentionOpen(false);
    window.requestAnimationFrame(() => {
      try {
        el.focus();
        el.setSelectionRange(nextBefore.length, nextBefore.length);
      } catch {
        // ignore
      }
    });
  };

  const usedVariables = useMemo(() => {
    if (!input || !variables || variables.length === 0) return [];
    const re = /@([\p{L}\p{N}_]+)/gu;
    const names = new Set();
    let m;
    // eslint-disable-next-line no-cond-assign
    while ((m = re.exec(input))) {
      if (m[1]) names.add(m[1]);
    }
    if (names.size === 0) return [];
    return variables.filter((v) => v.name && names.has(v.name));
  }, [input, variables]);

  const handleSend = async () => {
    if (!activeAgent) return;
    const trimmed = input.trim();
    if (!trimmed) return;
    const resolvedContent = resolveVariablesInText(trimmed, variables);

    const conv =
      conversation && conversation.agentId === activeAgent.id
        ? conversation
        : {
            id: null,
            agentId: activeAgent.id,
            messages: [],
          };

    const userMessage = {
      id: crypto.randomUUID(),
      role: "user",
      content: resolvedContent,
      createdAt: Date.now(),
    };

    const nextMessages = [...(conv.messages || []), userMessage];

    const messagesForApi = nextMessages.map((m) => ({
      role: m.role,
      content: m.content,
    }));

    const variablesForApi =
      (variables || [])
        .filter((v) => v.name)
        .map((v) => ({
          name: v.name,
          type: v.type || "short",
          rule_type: v.ruleType || null,
          value: v.value || "",
        })) || [];

    const payload = {
      conversation_id: conv.id,
      agent_id: Number(activeAgent.id),
      messages: messagesForApi,
      variables: variablesForApi,
    };

    const prettyRequest = JSON.stringify(payload, null, 2);
    setRawRequest(prettyRequest);
    setRawResponse("");
    setStreaming(true);
    setInput("");

    let assistantMessage = {
      id: crypto.randomUUID(),
      role: "assistant",
      content: "",
      createdAt: Date.now(),
    };

    // 将本次请求的 JSON 挂在用户消息上，方便在对话中查看
    userMessage.requestJson = prettyRequest;

    onConversationChange({
      ...conv,
      messages: [...nextMessages, assistantMessage],
    });

    // 对于当前实现，我们基于 SSE（EventSourceResponse）进行流式消费
    try {
      const source = new EventSource(
        `/api/chat?token=${encodeURIComponent(
          authToken,
        )}&payload=${encodeURIComponent(JSON.stringify(payload))}`,
      );
      let fullText = "";

      source.onmessage = (ev) => {
        // 默认消息视为 chunk
        fullText += ev.data || "";
        assistantMessage = { ...assistantMessage, content: fullText };
        onConversationChange({
          ...conv,
          messages: [...nextMessages, assistantMessage],
        });
      };

      source.addEventListener("chunk", (ev) => {
        fullText += ev.data || "";
        assistantMessage = { ...assistantMessage, content: fullText };
        onConversationChange({
          ...conv,
          messages: [...nextMessages, assistantMessage],
        });
      });

      source.addEventListener("end", async (ev) => {
        const raw = ev.data || "";
        setRawResponse(raw);
        source.close();
        setStreaming(false);

        // 结束后从后端重新拉取会话，拿到真正的 request_json / response_json
        try {
          const parsed = JSON.parse(raw || "{}");
          const convId = parsed.conversation_id ?? conv.id;
          if (convId != null) {
            const res = await fetch(`/api/conversations/${convId}`, {
              headers: { Authorization: `Bearer ${authToken}` },
            });
            if (res.ok) {
              const data = await res.json();
              const normalized = normalizeConversation(data);
              onConversationChange(normalized);
              return;
            }
          }
        } catch {
          // ignore, fallback 到本地内存中的对话
        }

        onConversationChange({
          ...conv,
          messages: [...nextMessages, assistantMessage],
        });
      });

      source.addEventListener("error", (ev) => {
        setRawResponse(ev.data || "流式响应发生错误");
        assistantMessage = {
          ...assistantMessage,
          content: `请求失败：${ev.data || "流错误"}`,
        };
        onConversationChange({
          ...conv,
          messages: [...nextMessages, assistantMessage],
        });
        source.close();
        setStreaming(false);
      });
    } catch (e) {
      const errText = e?.message || String(e);
      setRawResponse(errText);
      assistantMessage = {
        ...assistantMessage,
        content: `请求失败：${errText}`,
      };
      onConversationChange({
        ...conv,
        messages: [...nextMessages, assistantMessage],
      });
      setStreaming(false);
    }
  };

  return (
    <div className="panel panel-split chat-panel">
      <div className="panel-left chat-sidebar">
        <div className="panel-header">
          <h1>对话</h1>
          <p className="muted">
            左侧选择 Agent 与会话，右侧与 AI 助手继续对话。
          </p>
        </div>

        <div className="chat-toolbar">
          <select
            className="input"
            value={activeAgentId ?? ""}
            onChange={(e) => onActiveAgentChange(e.target.value || null)}
          >
            <option value="">选择 Agent</option>
            {agents.map((a) => (
              <option key={a.id} value={a.id}>
                {a.name || "未命名 Agent"}
              </option>
            ))}
          </select>
          <button
            className="secondary-btn"
            type="button"
            disabled={!activeAgentId}
            onClick={() =>
              onConversationChange(
                activeAgentId
                  ? { id: null, agentId: activeAgentId, messages: [] }
                  : null,
              )
            }
          >
            新建对话
          </button>
        </div>

        <div className="conversations-list-wrapper">
          {agentConversations.length === 0 ? (
            <p className="muted small">该 Agent 暂无历史对话。</p>
          ) : (
            <div className="conversations-list">
              <div className="list-header-small">历史对话</div>
              {agentConversations.map((c) => (
                <button
                  key={c.id}
                  type="button"
                  className={
                    conversation && conversation.id === c.id
                      ? "conversation-item active"
                      : "conversation-item"
                  }
                  onClick={() => onConversationSelect(c.id)}
                >
                  <div className="conversation-title">
                    {c.title || `会话 #${c.id}`}
                  </div>
                  <div className="conversation-preview">
                    {c.messages && c.messages.length > 0
                      ? c.messages[c.messages.length - 1].content.slice(0, 50)
                      : "暂无消息"}
                  </div>
                </button>
              ))}
            </div>
          )}
        </div>
      </div>

      <div className="panel-right chat-main">
        {!conversation || !conversation.messages || conversation.messages.length === 0 ? (
          <div className="empty-state center">
            <h2>开始新的对话</h2>
            <p className="muted">
              在左上角选择一个 Agent，然后点击「新建对话」并在下方输入你的问题。
            </p>
          </div>
        ) : (
          <>
            <div className="chat-main-header">
              <div className="chat-main-title">
                <span className="pill">
                  {activeAgent?.name || "当前 Agent"}
                </span>
                {conversation.id != null && (
                  <span className="conversation-id">
                    会话 #{conversation.id}
                  </span>
                )}
              </div>
            </div>

            <div className="chat-messages">
              {conversation.messages.map((m) => (
                <div
                  key={m.id}
                  className={
                    m.role === "user"
                      ? "chat-bubble user"
                      : "chat-bubble assistant"
                  }
                >
                  <div className="chat-role">
                    {m.role === "user" ? "你" : "Agent"}
                  </div>
                  <div className="chat-content">
                    <Markdown content={m.content} />
                  </div>
                  {(m.requestJson || m.responseJson) && (
                    <details className="chat-debug">
                      <summary>
                        查看原始 {m.role === "user" ? "请求" : "响应"}
                      </summary>
                      {m.requestJson && (
                        <div className="chat-debug-block">
                          <div className="chat-debug-title">Request JSON</div>
                          <pre className="code-block small">
                            {formatJsonForDisplay(m.requestJson)}
                          </pre>
                        </div>
                      )}
                      {m.responseJson && (
                        <div className="chat-debug-block">
                          <div className="chat-debug-title">Response JSON</div>
                          <pre className="code-block small">
                            {formatResponseJsonForDisplay(m.responseJson)}
                          </pre>
                        </div>
                      )}
                    </details>
                  )}
                </div>
              ))}
              <div ref={messagesEndRef} />
            </div>
          </>
        )}

        <div className="chat-input-bar">
          <div className="chat-input-main">
            <textarea
              className="input textarea"
              ref={inputRef}
              placeholder={
                activeAgent
                  ? "输入消息，Shift+Enter 换行，Enter 发送。支持通过 @变量名 引用变量。"
                  : "请先在左上角选择一个 Agent"
              }
              value={input}
              disabled={!activeAgent || streaming}
              onChange={(e) => {
                const val = e.target.value;
                setInput(val);
                updateMentionState(val, e.target.selectionStart);
              }}
              onKeyDown={(e) => {
                if (e.key === "Enter" && !e.shiftKey) {
                  e.preventDefault();
                  handleSend();
                }
              }}
            />
            {mentionOpen && mentionCandidates.length > 0 && (
              <div className="mention-menu">
                {mentionCandidates.map((v) => (
                  <button
                    key={v.id}
                    type="button"
                    className="mention-item"
                    onMouseDown={(e) => {
                      e.preventDefault();
                      insertMention(v.name);
                    }}
                  >
                    <span className="mention-name">@{v.name}</span>
                    <span className="mention-preview-text">
                      {computeVariableValue(v)}
                    </span>
                  </button>
                ))}
              </div>
            )}
            {variables && variables.length > 0 && (
              <div className="variable-hints small">
                <span className="label">可用变量：</span>
                {variables.map((v) => (
                  <button
                    key={v.id}
                    type="button"
                    className="pill pill-clickable"
                    onClick={() => insertMention(v.name)}
                  >
                    @{v.name}
                  </button>
                ))}
              </div>
            )}
            {usedVariables && usedVariables.length > 0 && (
              <div className="mention-used small">
                <span className="label">已引用变量：</span>
                {usedVariables.map((v) => (
                  <span key={v.id} className="pill pill-soft">
                    @{v.name}
                    <span className="mention-used-value">
                      {computeVariableValue(v)}
                    </span>
                  </span>
                ))}
              </div>
            )}
            {resolvedInput && resolvedInput !== input && (
              <div className="variable-preview">
                <span className="label small">预览（变量已展开）：</span>
                <div className="variable-preview-box small">
                  {resolvedInput}
                </div>
              </div>
            )}
          </div>
          <button
            className="primary-btn"
            onClick={handleSend}
            disabled={!activeAgent || streaming || !input.trim()}
          >
            {streaming ? "生成中..." : "发送"}
          </button>
        </div>
      </div>
    </div>
  );
}

// ===== SettingsPanel =====

function SettingsPanel({ config, models, onChange, authToken }) {
  const [local, setLocal] = useState(config || {});
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLocal(config || {});
  }, [config]);

  const handleChange = (field, value) => {
    const next = { ...local, [field]: value };
    setLocal(next);
  };

  const handleSaveAndFetchModels = async () => {
    setError(null);
    onChange(local);
    if (!local.baseUrl || !local.apiKey) {
      setError("请先填写 Base URL 和 API Key");
      return;
    }
    setLoading(true);
    try {
      // 先保存到后端
      await fetch("/api/settings/openai-config", {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${authToken}`,
        },
        body: JSON.stringify({
          base_url: local.baseUrl,
          api_key: local.apiKey,
          organization: local.organization || null,
          project: local.project || null,
        }),
      });

      // 再从后端拉取模型列表
      const res = await fetch("/api/openai/models", {
        headers: {
          Authorization: `Bearer ${authToken}`,
        },
      });
      if (!res.ok) {
        const text = await res.text();
        throw new Error(text || res.statusText);
      }
      const data = await res.json();
      const list =
        Array.isArray(data) && data.length
          ? data
              .map((m) => m.id || m.name || "")
              .filter(Boolean)
          : [];
      onChange(local, list);
    } catch (e) {
      setError(e.message || "拉取模型失败");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="panel">
      <div className="panel-header">
        <h1>全局 OpenAI 设置</h1>
        <p className="muted">
          配置 API 入口和凭证，模型列表会从后端 API 实时拉取。
        </p>
      </div>
      <div className="form-grid">
        <label className="field">
          <span className="label">Base URL</span>
          <input
            className="input"
            placeholder="例如：https://api.openai.com"
            value={local.baseUrl || ""}
            onChange={(e) => handleChange("baseUrl", e.target.value)}
          />
        </label>
        <label className="field">
          <span className="label">API Key</span>
          <input
            className="input"
            type="password"
            placeholder="sk-..."
            value={local.apiKey || ""}
            onChange={(e) => handleChange("apiKey", e.target.value)}
          />
        </label>
        <label className="field">
          <span className="label">Organization（可选）</span>
          <input
            className="input"
            placeholder="组织 ID，可留空"
            value={local.organization ?? ""}
            onChange={(e) => handleChange("organization", e.target.value)}
          />
        </label>
        <label className="field">
          <span className="label">Project（可选）</span>
          <input
            className="input"
            placeholder="项目 ID，可留空"
            value={local.project ?? ""}
            onChange={(e) => handleChange("project", e.target.value)}
          />
        </label>
      </div>

      <div className="panel-actions">
        <button
          className="primary-btn"
          onClick={handleSaveAndFetchModels}
          disabled={loading}
        >
          {loading ? "保存并拉取模型..." : "保存并拉取模型列表"}
        </button>
        {error && <span className="error-text">{error}</span>}
      </div>

      <div className="panel-section">
        <h2>模型列表</h2>
        {models.length === 0 ? (
          <p className="muted">尚未拉取到模型，请先保存配置并拉取。</p>
        ) : (
          <div className="models-list">
            {models.map((m) => (
              <span key={m} className="pill">
                {m}
              </span>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// ===== VariablesPanel =====

function VariablesPanel({ variables, onChange }) {
  const handleAdd = () => {
    const next = [...variables, createEmptyVariable()];
    onChange(next);
  };

  const handleUpdate = (id, patch) => {
    const next = variables.map((v) =>
      v.id === id
        ? {
            ...v,
            ...patch,
          }
        : v,
    );
    onChange(next);
  };

  const handleDelete = (id) => {
    const next = variables.filter((v) => v.id !== id);
    onChange(next);
  };

  return (
    <div className="panel">
      <div className="panel-header">
        <h1>变量</h1>
        <p className="muted">
          在这里维护可复用的变量，在 Agents 配置和对话输入中通过
          <code>@变量名</code> 引用，并实时预览展开后的内容。
        </p>
      </div>

      <div className="panel-actions">
        <button className="primary-btn" type="button" onClick={handleAdd}>
          新建变量
        </button>
      </div>

      {variables.length === 0 ? (
        <p className="muted">还没有变量，点击「新建变量」开始。</p>
      ) : (
        <div className="variables-list">
          {variables.map((v) => {
            const preview = computeVariableValue(v);
            return (
              <div key={v.id} className="variable-item">
                <div className="form-grid">
                  <label className="field">
                    <span className="label">名称（在文本中用 @名称 引用）</span>
                    <input
                      className="input"
                      value={v.name}
                      onChange={(e) =>
                        handleUpdate(v.id, { name: e.target.value })
                      }
                      placeholder="例如：当前时间、当前季节"
                    />
                  </label>
                  <label className="field">
                    <span className="label">类型</span>
                    <select
                      className="input"
                      value={v.type}
                      onChange={(e) =>
                        handleUpdate(v.id, {
                          type: e.target.value,
                        })
                      }
                    >
                      <option value="short">短文本</option>
                      <option value="long">长文本</option>
                      <option value="rule">规则变量</option>
                    </select>
                  </label>
                </div>

                {v.type === "rule" ? (
                  <div className="form-grid">
                    <label className="field">
                      <span className="label">规则</span>
                      <select
                        className="input"
                        value={v.ruleType || "current_time"}
                        onChange={(e) =>
                          handleUpdate(v.id, {
                            ruleType: e.target.value,
                          })
                        }
                      >
                        <option value="current_time">当前时间</option>
                        <option value="hour_to_season">
                          当前小时映射季节（0-6 春，7-12 夏，13-16 秋，17-23 冬）
                        </option>
                      </select>
                    </label>
                  </div>
                ) : (
                  <label className="field">
                    <span className="label">
                      {v.type === "long" ? "值（长文本）" : "值（短文本）"}
                    </span>
                    {v.type === "long" ? (
                      <textarea
                        className="input textarea"
                        value={v.value || ""}
                        onChange={(e) =>
                          handleUpdate(v.id, { value: e.target.value })
                        }
                        placeholder="输入该变量的长文本值"
                      />
                    ) : (
                      <input
                        className="input"
                        value={v.value || ""}
                        onChange={(e) =>
                          handleUpdate(v.id, { value: e.target.value })
                        }
                        placeholder="输入该变量的短文本值"
                      />
                    )}
                  </label>
                )}

                {preview && (
                  <div className="variable-preview">
                    <span className="label small">当前预览值：</span>
                    <div className="variable-preview-box small">{preview}</div>
                  </div>
                )}

                <div className="panel-actions">
                  <button
                    className="danger-btn"
                    type="button"
                    onClick={() => handleDelete(v.id)}
                  >
                    删除变量
                  </button>
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// ===== App 根组件 =====

function App() {
  const [tab, setTab] = useState("chat");
  const [token, setToken] = useState(() => localStorage.getItem(TOKEN_KEY));

  const [agents, setAgents] = useState([]);
  const [activeAgentId, setActiveAgentId] = useState(null);
  const [conversations, setConversations] = useState([]);
  const [conversation, setConversation] = useState(null);

  const [variables, setVariables] = useState(() => {
    try {
      const raw = localStorage.getItem(VARIABLES_STORAGE_KEY);
      if (!raw) return [];
      const parsed = JSON.parse(raw);
      if (Array.isArray(parsed)) return parsed;
      return [];
    } catch {
      return [];
    }
  });

  const [openAIConfig, setOpenAIConfig] = useState({
    baseUrl: "",
    apiKey: "",
    organization: undefined,
    project: undefined,
  });
  const [models, setModels] = useState([]);

  useEffect(() => {
    if (!token) return;
    const load = async () => {
      try {
        // Agents
        const agentsRes = await fetch("/api/agents", {
          headers: { Authorization: `Bearer ${token}` },
        });
        if (agentsRes.ok) {
          const list = await agentsRes.json();
          setAgents(list.map(normalizeAgent));
        } else if (agentsRes.status === 401) {
          localStorage.removeItem(TOKEN_KEY);
          setToken(null);
          return;
        }

        // OpenAI config
        const cfgRes = await fetch("/api/settings/openai-config", {
          headers: { Authorization: `Bearer ${token}` },
        });
        if (cfgRes.ok) {
          const cfg = await cfgRes.json();
          if (cfg) {
            setOpenAIConfig({
              baseUrl: cfg.base_url || "",
              apiKey: cfg.api_key || "",
              organization: cfg.organization || "",
              project: cfg.project || "",
            });
          }
        }

        // Models（如果已经配置好）
        const modelsRes = await fetch("/api/openai/models", {
          headers: { Authorization: `Bearer ${token}` },
        });
        if (modelsRes.ok) {
          const data = await modelsRes.json();
          const list =
            Array.isArray(data) && data.length
              ? data
                  .map((m) => m.id || m.name || "")
                  .filter(Boolean)
              : [];
          setModels(list);
        }

        // Conversations
        const convRes = await fetch("/api/conversations", {
          headers: { Authorization: `Bearer ${token}` },
        });
        if (convRes.ok) {
          const data = await convRes.json();
          if (Array.isArray(data)) {
            setConversations(data.map(normalizeConversation));
          }
        }
      } catch {
        // 忽略加载错误
      }
    };
    load();
  }, [token]);

  useEffect(() => {
    try {
      localStorage.setItem(VARIABLES_STORAGE_KEY, JSON.stringify(variables));
    } catch {
      // ignore
    }
  }, [variables]);

  if (!token) {
    return <Login onLogin={setToken} />;
  }

  return (
    <div className="app-root">
      <aside className="sidebar">
        <div className="sidebar-header">🤖 Agents 控制台</div>
        <nav className="sidebar-nav">
          <button
            className={tab === "chat" ? "nav-btn active" : "nav-btn"}
            onClick={() => setTab("chat")}
          >
            对话
          </button>
          <button
            className={tab === "agents" ? "nav-btn active" : "nav-btn"}
            onClick={() => setTab("agents")}
          >
            Agents
          </button>
          <button
            className={tab === "variables" ? "nav-btn active" : "nav-btn"}
            onClick={() => setTab("variables")}
          >
            变量
          </button>
          <button
            className={tab === "settings" ? "nav-btn active" : "nav-btn"}
            onClick={() => setTab("settings")}
          >
            设置
          </button>
        </nav>
      </aside>
      <main className="main">
        {tab === "agents" && (
          <AgentsPanel
            agents={agents}
            onChange={setAgents}
            modelOptions={models}
            variables={variables}
            onSelectAgent={(id) => {
              setActiveAgentId(id);
              setTab("chat");
            }}
            authToken={token}
          />
        )}
        {tab === "chat" && (
          <ChatPanel
            agents={agents}
            activeAgentId={activeAgentId}
            onActiveAgentChange={setActiveAgentId}
            conversations={conversations}
            onConversationSelect={async (id) => {
              if (!id) {
                setConversation(null);
                return;
              }
              try {
                const res = await fetch(`/api/conversations/${id}`, {
                  headers: { Authorization: `Bearer ${token}` },
                });
                if (!res.ok) return;
                const data = await res.json();
                const conv = normalizeConversation(data);
                setConversation(conv);
                setConversations((prev) => {
                  const others = prev.filter((c) => c.id !== conv.id);
                  return [conv, ...others];
                });
                setActiveAgentId(conv.agentId);
              } catch {
                // ignore
              }
            }}
            conversation={conversation}
            onConversationChange={(conv) => {
              setConversation(conv);
              if (!conv || !conv.id) {
                return;
              }
              setConversations((prev) => {
                const idx = prev.findIndex((c) => c.id === conv.id);
                if (idx === -1) {
                  return [conv, ...prev];
                }
                const next = [...prev];
                next[idx] = conv;
                return next;
              });
            }}
            authToken={token}
            variables={variables}
          />
        )}
        {tab === "variables" && (
          <VariablesPanel
            variables={variables}
            onChange={(vs) => setVariables(vs)}
          />
        )}
        {tab === "settings" && (
          <SettingsPanel
            config={openAIConfig}
            models={models}
            onChange={(cfg, modelsFromApi) => {
              setOpenAIConfig(cfg);
              if (modelsFromApi) {
                setModels(modelsFromApi);
              }
            }}
            authToken={token}
          />
        )}
      </main>
    </div>
  );
}

// ===== 挂载 React 应用 =====

const rootEl = document.getElementById("root");
const root = ReactDOM.createRoot(rootEl);
root.render(<App />);

