"use client";

import {
  Bot,
  Boxes,
  Brain,
  Check,
  Copy,
  Database,
  MessageSquareText,
  MoreHorizontal,
  Pencil,
  Plus,
  Rocket,
  Sparkles,
  Trash2,
  Workflow,
  ChevronLeft,
  ChevronUp,
  ChevronDown,
  Save,
  Mic,
  Send,
  HelpCircle,
  Waypoints,
  AudioLines,
  Terminal,
  Loader2,
  PhoneOff,
  Orbit,
  Waves,
  Bug,
  Video,
  Smartphone,
  Wrench,
  Settings2,
  X,
} from "lucide-react";

import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { NetworkQualityIndicator } from "@/components/network-quality-indicator";
import { TurnConfigEditor } from "@/components/turn-config-editor";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
} from "@/components/ui/sheet";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { AuraVisualizer } from "@/components/ui/aura-visualizer";
import { NebulaVisualizer } from "@/components/ui/nebula-visualizer";
import { SpectrumVisualizer } from "@/components/ui/spectrum-visualizer";
import { WaveVisualizer } from "@/components/ui/wave-visualizer";
import { WaveformTimelinePanel } from "@/components/ui/waveform-timeline";
import { DataList } from "@/components/ui/data-list";
import { PageHeader } from "@/components/ui/page-header";
import { FilterPills } from "@/components/ui/filter-pills";
import { SearchInput } from "@/components/ui/search-input";
import { ListToolbar } from "@/components/ui/list-toolbar";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import {
  assistantsApi,
  knowledgeBasesApi,
  modelResourcesApi,
  toolsApi,
  type Assistant,
  type AssistantType as ApiAssistantType,
  type AssistantUpsert,
  type KnowledgeBase,
  type KnowledgeRetrievalConfig,
  type ModelResource,
  type Tool,
  type TurnConfig,
} from "@/lib/api";
import {
  useVoicePreview,
  type ChatMessage,
  type VoicePreview,
  type VoicePreviewStatus,
} from "@/hooks/use-voice-preview";
import {
  useCameraPreview,
  type CameraPreview,
} from "@/hooks/use-camera-preview";
import {
  WorkflowEditor,
  type WorkflowSettings,
} from "@/components/workflow/WorkflowEditor";
import {
  defaultGraph,
  type WorkflowGraph,
} from "@/components/workflow/specs";

type RuntimeMode = "pipeline" | "realtime";

type AssistantForm = {
  name: string;
  greeting: string;
  prompt: string;
  runtimeMode: RuntimeMode;
  realtimeModel: string;
  model: string;
  asr: string;
  voice: string;
  knowledgeBase: string;
  knowledgeRetrievalConfig: KnowledgeRetrievalConfig;
  enableInterrupt: boolean;
  turnConfig: TurnConfig;
  visionEnabled: boolean;
  visionModelResourceId: string;
  toolIds: string[];
};

type FastGptForm = {
  name: string;
  agent: string;
  asr: string;
  voice: string;
  enableInterrupt: boolean;
  turnConfig: TurnConfig;
};

type DifyForm = {
  name: string;
  agent: string;
  asr: string;
  voice: string;
  enableInterrupt: boolean;
  turnConfig: TurnConfig;
};

type OpenCodeForm = {
  name: string;
  prompt: string;
  agent: string;
  model: string;
  asr: string;
  voice: string;
  enableInterrupt: boolean;
  turnConfig: TurnConfig;
  visionEnabled: boolean;
  visionModelResourceId: string;
};

type AssistantType = "提示词" | "工作流" | "Dify" | "FastGPT" | "OpenCode";

const assistantTypes: AssistantType[] = [
  "提示词",
  "工作流",
  "Dify",
  "FastGPT",
  "OpenCode",
];

function defaultTurnConfig(): TurnConfig {
  return {
    bargeIn: {
      strategy: "vad",
    },
    vad: {
      confidence: 0.7,
      startSecs: 0.2,
      stopSecs: 0.2,
      minVolume: 0.6,
    },
    turnDetection: {
      strategy: "silence",
      silenceTimeoutSecs: 0.6,
    },
  };
}

function defaultKnowledgeRetrievalConfig(): KnowledgeRetrievalConfig {
  return {
    mode: "automatic",
    topN: 5,
    scoreThreshold: 0,
  };
}

// 后端 type(英文) ↔ 列表展示标签(中文)
const typeToLabel: Record<ApiAssistantType, AssistantType> = {
  prompt: "提示词",
  workflow: "工作流",
  dify: "Dify",
  fastgpt: "FastGPT",
  opencode: "OpenCode",
};
const typeFromLabel: Record<AssistantType, ApiAssistantType> = {
  提示词: "prompt",
  工作流: "workflow",
  Dify: "dify",
  FastGPT: "fastgpt",
  OpenCode: "opencode",
};

// 后端 type → 编辑器视图(工作流暂为占位页)
const typeToView = {
  prompt: "create",
  dify: "create-dify",
  fastgpt: "create-fastgpt",
  opencode: "create-opencode",
  workflow: "workflow",
} as const;

type View = "list" | "choose" | "loading" | (typeof typeToView)[ApiAssistantType];

// 路由驱动的页面模式:
// /assistants → list | /assistants/new → choose(引导,确认即建库) | /assistants/[id] → edit
export type AssistantPageProps =
  | { mode: "list" }
  | { mode: "choose" }
  | { mode: "edit"; assistantId: string };

// 各类型的空白表单模板(新建用)
function blankPromptForm(name: string): AssistantForm {
  return {
    name,
    greeting: "",
    prompt: "",
    runtimeMode: "pipeline",
    realtimeModel: "",
    model: "",
    asr: "",
    voice: "",
    knowledgeBase: "",
    knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
    enableInterrupt: true,
    turnConfig: defaultTurnConfig(),
    visionEnabled: false,
    visionModelResourceId: "",
    toolIds: [],
  };
}

function blankFastGptForm(name: string): FastGptForm {
  return {
    name,
    agent: "",
    asr: "",
    voice: "",
    enableInterrupt: true,
    turnConfig: defaultTurnConfig(),
  };
}

function blankDifyForm(name: string): DifyForm {
  return {
    name,
    agent: "",
    asr: "",
    voice: "",
    enableInterrupt: true,
    turnConfig: defaultTurnConfig(),
  };
}

function blankOpenCodeForm(name: string): OpenCodeForm {
  return {
    name,
    prompt: "",
    agent: "",
    model: "",
    asr: "",
    voice: "",
    enableInterrupt: true,
    turnConfig: defaultTurnConfig(),
    visionEnabled: false,
    visionModelResourceId: "",
  };
}
function formatTimestamp(iso?: string | null): string {
  if (!iso) return "";
  const d = new Date(iso);
  if (Number.isNaN(d.getTime())) return "";
  const pad = (n: number) => String(n).padStart(2, "0");
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(
    d.getHours(),
  )}:${pad(d.getMinutes())}`;
}

type AssistantTypeOption = {
  type: AssistantType;
  label: string;
  description: string;
  icon: React.ReactNode;
  /** 提示词、Dify、FastGPT 类型已落地，工作流暂时显示占位页 */
  available: boolean;
};

const assistantTypeOptions: AssistantTypeOption[] = [
  {
    type: "提示词",
    label: "使用提示词构建",
    description: "通过提示词、模型与语音快速搭建对话助手，适合大多数场景。",
    icon: <MessageSquareText size={20} />,
    available: true,
  },
  {
    type: "工作流",
    label: "使用工作流构建",
    description: "用可视化编排串联多个节点，适合多步骤、带分支的复杂流程。",
    icon: <Workflow size={20} />,
    available: false,
  },
  {
    type: "Dify",
    label: "使用 Dify 构建",
    description: "对接 Dify 应用，复用其编排能力与知识库配置。",
    icon: <Boxes size={20} />,
    available: true,
  },
  {
    type: "FastGPT",
    label: "使用 FastGPT 构建",
    description: "对接 FastGPT 应用，复用其知识库问答与工作流能力。",
    icon: <Database size={20} />,
    available: true,
  },
  {
    type: "OpenCode",
    label: "使用 OpenCode 构建",
    description: "对接 OpenCode 服务，通过提示词驱动代码助手并支持实时语音对话。",
    icon: <Terminal size={20} />,
    available: true,
  },
];

type AssistantListItem = {
  id: string;
  name: string;
  type: AssistantType;
  updatedAt: string;
  /** 原始 ISO 时间戳,用于按时间排序(updatedAt 为展示用整形字符串) */
  updatedAtRaw: string | null | undefined;
};

type TypeFilter = "全部" | AssistantType;

const typeFilters: TypeFilter[] = ["全部", ...assistantTypes];

// 列表按更新时间排序:newest=最近更新在前(倒叙,默认) / oldest=最早更新在前
type SortOrder = "newest" | "oldest";

export function AssistantPage(props: AssistantPageProps) {
  const router = useRouter();
  // 编辑中的助手 id(来自路由)
  const editingId = props.mode === "edit" ? props.assistantId : null;

  const [form, setForm] = useState<AssistantForm>(() => blankPromptForm(""));
  const [fastGptForm, setFastGptForm] = useState<FastGptForm>(() =>
    blankFastGptForm(""),
  );
  const [difyForm, setDifyForm] = useState<DifyForm>(() => blankDifyForm(""));
  const [openCodeForm, setOpenCodeForm] = useState<OpenCodeForm>(() =>
    blankOpenCodeForm(""),
  );
  const [assistants, setAssistants] = useState<Assistant[]>([]);
  const [listLoading, setListLoading] = useState(true);
  const [listError, setListError] = useState<string | null>(null);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState<string | null>(null);
  // 编辑模式:加载指定 id 助手失败时的错误
  const [loadError, setLoadError] = useState<string | null>(null);
  // 编辑模式:后端返回的打码 API Key(用于编辑页展示"当前密钥")
  // 下拉数据源:模型资源 + 知识库
  const [modelResources, setModelResources] = useState<ModelResource[]>([]);
  const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
  const [tools, setTools] = useState<Tool[]>([]);
  // 视图由路由模式决定;仅编辑模式需要先 loading,等拿到助手类型后切换
  const [view, setView] = useState<View>(() => {
    if (props.mode === "choose") return "choose";
    if (props.mode === "edit") return "loading";
    return "list";
  });
  const [searchQuery, setSearchQuery] = useState("");
  const [typeFilter, setTypeFilter] = useState<TypeFilter>("全部");
  const [sortOrder, setSortOrder] = useState<SortOrder>("newest");
  const [currentPage, setCurrentPage] = useState(1);
  // choose 步骤的草稿：名称与已选类型，确认后直接建库并进入编辑页
  // (工作流占位页也用它展示名称与类型)
  const [draftName, setDraftName] = useState("");
  const [draftType, setDraftType] = useState<AssistantType | null>(null);
  // 引导页:创建请求进行中 / 创建失败
  const [creating, setCreating] = useState(false);
  const [createError, setCreateError] = useState<string | null>(null);
  // 已保存基线(当前类型表单的 JSON);与表单不一致时保存按钮才可点击
  const [savedSnapshot, setSavedSnapshot] = useState<string | null>(null);

  // workflow 编辑器:名称 + 图(nodes/edges)。graph 实时由画布回传。
  const [workflowName, setWorkflowName] = useState("");
  const [workflowGraph, setWorkflowGraph] = useState<WorkflowGraph>(() =>
    defaultGraph(),
  );
  const [workflowSettings, setWorkflowSettings] = useState<WorkflowSettings>({
    allowInterrupt: true,
    turnConfig: defaultTurnConfig(),
  });
  const [debugOpen, setDebugOpen] = useState(false);
  const [activeNodeId, setActiveNodeId] = useState<string | null>(null);

  const loadAssistants = useCallback(async () => {
    setListLoading(true);
    setListError(null);
    try {
      setAssistants(await assistantsApi.list());
    } catch (error) {
      setListError(error instanceof Error ? error.message : "加载失败");
    } finally {
      setListLoading(false);
    }
  }, []);

  useEffect(() => {
    // 列表页挂载时拉取助手列表(与后端同步)
    if (props.mode !== "list") return;
    // eslint-disable-next-line react-hooks/set-state-in-effect
    void loadAssistants();
  }, [props.mode, loadAssistants]);

  // 进入创建/编辑前加载下拉数据源(模型资源 + 知识库)
  const loadResources = useCallback(async () => {
    try {
      const [creds, kbs, toolRows] = await Promise.all([
        modelResourcesApi.list(),
        knowledgeBasesApi.list(),
        toolsApi.list(),
      ]);
      setModelResources(creds);
      setKnowledgeBases(kbs);
      setTools(toolRows);
    } catch {
      // 拉取失败时下拉为空,不阻塞表单
    }
  }, []);

  // 编辑页挂载时加载下拉数据源
  useEffect(() => {
    if (props.mode !== "edit") return;
    // eslint-disable-next-line react-hooks/set-state-in-effect
    void loadResources();
  }, [props.mode, loadResources]);

  // 按资源类型生成 {value:id, label:name} 选项
  const credOptions = (type: ModelResource["capability"]) =>
    modelResources
      .filter((c) => c.capability === type)
      .map((c) => ({ value: c.id, label: c.name }));
  const agentOptions = (interfaceType: "dify" | "fastgpt" | "opencode") =>
    modelResources
      .filter(
        (resource) =>
          resource.capability === "Agent" &&
          resource.interfaceType === interfaceType,
      )
      .map((resource) => ({ value: resource.id, label: resource.name }));
  const visionModelOptionsFor = (currentModelId: string) => modelResources
    .filter(
      (c) =>
        c.capability === "LLM" &&
        c.supportImageInput &&
        c.id !== currentModelId,
    )
    .map((c) => ({ value: c.id, label: c.name }));
  const kbOptions = knowledgeBases.map((k) => ({ value: k.id, label: k.name }));

  function handlePromptVisionEnabledChange(enabled: boolean) {
    updateForm("visionEnabled", enabled);
    if (!enabled) updateForm("visionModelResourceId", "");
  }

  function handlePromptModelChange(value: string) {
    updateForm("model", value);
    if (form.visionModelResourceId === value) {
      updateForm("visionModelResourceId", "");
    }
  }

  function handleOpenCodeVisionEnabledChange(enabled: boolean) {
    updateOpenCodeForm("visionEnabled", enabled);
    if (!enabled) updateOpenCodeForm("visionModelResourceId", "");
  }

  function handleOpenCodeModelChange(value: string) {
    updateOpenCodeForm("model", value);
    if (openCodeForm.visionModelResourceId === value) {
      updateOpenCodeForm("visionModelResourceId", "");
    }
  }

  function startCreate() {
    router.push("/assistants/new");
  }

  // 把后端 Assistant 回填进提示词表单(model/asr/voice 等存模型资源 id)
  // 返回回填后的表单,供调用方记录"已保存基线"
  function fillPromptForm(a: Assistant): AssistantForm {
    const next: AssistantForm = {
      name: a.name,
      greeting: a.greeting,
      prompt: a.prompt,
      runtimeMode: a.runtimeMode,
      realtimeModel: a.modelResourceIds.Realtime ?? "",
      model: a.modelResourceIds.LLM ?? "",
      asr: a.modelResourceIds.ASR ?? "",
      voice: a.modelResourceIds.TTS ?? "",
      knowledgeBase: a.knowledgeBaseId ?? "",
      knowledgeRetrievalConfig:
        a.knowledgeRetrievalConfig ?? defaultKnowledgeRetrievalConfig(),
      enableInterrupt: a.enableInterrupt,
      turnConfig: a.turnConfig,
      visionEnabled: a.visionEnabled,
      visionModelResourceId: a.visionModelResourceId ?? "",
      toolIds: a.toolIds ?? [],
    };
    setForm(next);
    return next;
  }

  // 编辑：跳转到 /assistants/[id],由编辑页按助手类型回填表单
  function handleEdit(assistant: AssistantListItem) {
    router.push(`/assistants/${assistant.id}`);
  }

  // 引导页确认：直接创建到数据库拿到 id，再进入该助手的编辑页
  async function confirmType() {
    if (!draftName.trim() || !draftType || creating) {
      return;
    }

    setCreating(true);
    setCreateError(null);
    try {
      const created = await assistantsApi.create(
        baseUpsert({
          name: draftName.trim(),
          type: typeFromLabel[draftType],
        }),
      );
      router.push(`/assistants/${created.id}`);
    } catch (error) {
      setCreateError(error instanceof Error ? error.message : "创建失败");
      setCreating(false);
    }
  }

  // 复制助手：服务端整行复制(含真 key,密钥不经浏览器)
  async function handleDuplicate(assistant: AssistantListItem) {
    try {
      await assistantsApi.duplicate(assistant.id);
      await loadAssistants();
    } catch (error) {
      setListError(error instanceof Error ? error.message : "复制失败");
    }
  }

  // 删除助手
  async function handleDelete(id: string) {
    try {
      await assistantsApi.remove(id);
      await loadAssistants();
    } catch (error) {
      setListError(error instanceof Error ? error.message : "删除失败");
    }
  }

  // 平铺字段的空白 upsert,各类型按需覆盖(后端会按 type 清掉无关字段)
  function baseUpsert(over: Partial<AssistantUpsert>): AssistantUpsert {
    return {
      name: "",
      type: "prompt",
      runtimeMode: "pipeline",
      greeting: "",
      enableInterrupt: true,
      turnConfig: defaultTurnConfig(),
      visionEnabled: false,
      visionModelResourceId: null,
      modelResourceIds: {},
      knowledgeBaseId: null,
      knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(),
      toolIds: [],
      prompt: "",
      apiUrl: "",
      apiKey: "",
      appId: "",
      graph: {},
      ...over,
    };
  }

  // 保存后停留在编辑页，并把当前表单记为已保存基线（按钮随之置灰）。
  async function save(payload: AssistantUpsert) {
    if (!editingId) return;
    setSaving(true);
    setSaveError(null);
    try {
      await assistantsApi.update(editingId, payload);
      if (view === "create") {
        setSavedSnapshot(JSON.stringify(form));
      } else if (view === "create-dify") {
        const next = { ...difyForm };
        setDifyForm(next);
        setSavedSnapshot(JSON.stringify(next));
      } else if (view === "create-fastgpt") {
        const next = { ...fastGptForm };
        setFastGptForm(next);
        setSavedSnapshot(JSON.stringify(next));
      } else if (view === "create-opencode") {
        const next = { ...openCodeForm };
        setOpenCodeForm(next);
        setSavedSnapshot(JSON.stringify(next));
      } else if (view === "workflow") {
        setSavedSnapshot(
          JSON.stringify({
            name: workflowName,
            graph: workflowGraph,
            settings: workflowSettings,
          }),
        );
      }
    } catch (error) {
      setSaveError(error instanceof Error ? error.message : "保存失败");
    } finally {
      setSaving(false);
    }
  }

  function handleSavePrompt() {
    void save(
      baseUpsert({
        name: form.name.trim(),
        type: "prompt",
        runtimeMode: form.runtimeMode,
        greeting: form.greeting,
        enableInterrupt: form.enableInterrupt,
        turnConfig: form.turnConfig,
        visionEnabled: form.visionEnabled,
        visionModelResourceId: form.visionModelResourceId || null,
        modelResourceIds: {
          ...(form.model ? { LLM: form.model } : {}),
          ...(form.asr ? { ASR: form.asr } : {}),
          ...(form.voice ? { TTS: form.voice } : {}),
          ...(form.realtimeModel ? { Realtime: form.realtimeModel } : {}),
        },
        knowledgeBaseId: form.runtimeMode === "pipeline" ? form.knowledgeBase || null : null,
        knowledgeRetrievalConfig: form.knowledgeRetrievalConfig,
        toolIds: form.toolIds,
        prompt: form.prompt,
      }),
    );
  }

  // ---- Dify ----
  function fillDifyForm(a: Assistant): DifyForm {
    const next: DifyForm = {
      name: a.name,
      agent: a.modelResourceIds.Agent ?? "",
      asr: a.modelResourceIds.ASR ?? "",
      voice: a.modelResourceIds.TTS ?? "",
      enableInterrupt: a.enableInterrupt,
      turnConfig: a.turnConfig,
    };
    setDifyForm(next);
    return next;
  }

  function handleSaveDify() {
    void save(
      baseUpsert({
        name: difyForm.name.trim(),
        type: "dify",
        enableInterrupt: difyForm.enableInterrupt,
        turnConfig: difyForm.turnConfig,
        modelResourceIds: {
          ...(difyForm.agent ? { Agent: difyForm.agent } : {}),
          ...(difyForm.asr ? { ASR: difyForm.asr } : {}),
          ...(difyForm.voice ? { TTS: difyForm.voice } : {}),
        },
      }),
    );
  }

  // ---- FastGPT ----
  function fillFastGptForm(a: Assistant): FastGptForm {
    const next: FastGptForm = {
      name: a.name,
      agent: a.modelResourceIds.Agent ?? "",
      asr: a.modelResourceIds.ASR ?? "",
      voice: a.modelResourceIds.TTS ?? "",
      enableInterrupt: a.enableInterrupt,
      turnConfig: a.turnConfig,
    };
    setFastGptForm(next);
    return next;
  }

  function handleSaveFastGpt() {
    void save(
      baseUpsert({
        name: fastGptForm.name.trim(),
        type: "fastgpt",
        enableInterrupt: fastGptForm.enableInterrupt,
        turnConfig: fastGptForm.turnConfig,
        modelResourceIds: {
          ...(fastGptForm.agent ? { Agent: fastGptForm.agent } : {}),
          ...(fastGptForm.asr ? { ASR: fastGptForm.asr } : {}),
          ...(fastGptForm.voice ? { TTS: fastGptForm.voice } : {}),
        },
      }),
    );
  }

  // ---- OpenCode ----
  function fillOpenCodeForm(a: Assistant): OpenCodeForm {
    const next: OpenCodeForm = {
      name: a.name,
      prompt: a.prompt,
      agent: a.modelResourceIds.Agent ?? "",
      model: a.modelResourceIds.LLM ?? "",
      asr: a.modelResourceIds.ASR ?? "",
      voice: a.modelResourceIds.TTS ?? "",
      enableInterrupt: a.enableInterrupt,
      turnConfig: a.turnConfig,
      visionEnabled: a.visionEnabled,
      visionModelResourceId: a.visionModelResourceId ?? "",
    };
    setOpenCodeForm(next);
    return next;
  }

  // 编辑模式:按路由中的 id 拉取助手,回填对应类型的表单后再切换视图
  useEffect(() => {
    if (props.mode !== "edit" || !editingId) return;
    let cancelled = false;

    void (async () => {
      try {
        const assistant = await assistantsApi.get(editingId);
        if (cancelled) return;
        if (assistant.type === "prompt") {
          setSavedSnapshot(JSON.stringify(fillPromptForm(assistant)));
        } else if (assistant.type === "fastgpt") {
          setSavedSnapshot(JSON.stringify(fillFastGptForm(assistant)));
        } else if (assistant.type === "dify") {
          setSavedSnapshot(JSON.stringify(fillDifyForm(assistant)));
        } else if (assistant.type === "opencode") {
          setSavedSnapshot(JSON.stringify(fillOpenCodeForm(assistant)));
        } else {
          // 工作流：回填名称与图(空图回落到默认 开始→智能体→结束)
          const graph =
            assistant.graph &&
            Array.isArray((assistant.graph as WorkflowGraph).nodes) &&
            (assistant.graph as WorkflowGraph).nodes.length > 0
              ? (assistant.graph as WorkflowGraph)
              : defaultGraph();
          const wfSettings: WorkflowSettings = {
            llm: assistant.modelResourceIds.LLM,
            asr: assistant.modelResourceIds.ASR,
            tts: assistant.modelResourceIds.TTS,
            allowInterrupt: assistant.enableInterrupt,
            turnConfig: assistant.turnConfig,
          };
          setWorkflowName(assistant.name);
          setWorkflowGraph(graph);
          setWorkflowSettings(wfSettings);
          setSavedSnapshot(
            JSON.stringify({
              name: assistant.name,
              graph,
              settings: wfSettings,
            }),
          );
        }
        setView(typeToView[assistant.type]);
      } catch (error) {
        if (!cancelled) {
          setLoadError(
            error instanceof Error ? error.message : "加载助手失败",
          );
        }
      }
    })();

    return () => {
      cancelled = true;
    };
  }, [props.mode, editingId]);

  function handleSaveOpenCode() {
    void save(
      baseUpsert({
        name: openCodeForm.name.trim(),
        type: "opencode",
        enableInterrupt: openCodeForm.enableInterrupt,
        turnConfig: openCodeForm.turnConfig,
        visionEnabled: openCodeForm.visionEnabled,
        visionModelResourceId: openCodeForm.visionModelResourceId || null,
        modelResourceIds: {
          ...(openCodeForm.agent ? { Agent: openCodeForm.agent } : {}),
          ...(openCodeForm.model ? { LLM: openCodeForm.model } : {}),
          ...(openCodeForm.asr ? { ASR: openCodeForm.asr } : {}),
          ...(openCodeForm.voice ? { TTS: openCodeForm.voice } : {}),
        },
        prompt: openCodeForm.prompt,
      }),
    );
  }

  function handleSaveWorkflow() {
    void save(
      baseUpsert({
        name: workflowName.trim(),
        type: "workflow",
        enableInterrupt: workflowSettings.allowInterrupt,
        turnConfig: workflowSettings.turnConfig,
        modelResourceIds: {
          ...(workflowSettings.llm ? { LLM: workflowSettings.llm } : {}),
          ...(workflowSettings.asr ? { ASR: workflowSettings.asr } : {}),
          ...(workflowSettings.tts ? { TTS: workflowSettings.tts } : {}),
        },
        graph: workflowGraph as unknown as Record<string, unknown>,
      }),
    );
  }

  // 当前编辑器表单相对已保存基线是否有改动(决定保存按钮是否可点)
  const activeFormJson =
    view === "create"
      ? JSON.stringify(form)
      : view === "create-dify"
        ? JSON.stringify(difyForm)
        : view === "create-fastgpt"
          ? JSON.stringify(fastGptForm)
          : view === "create-opencode"
            ? JSON.stringify(openCodeForm)
            : view === "workflow"
              ? JSON.stringify({
                  name: workflowName,
                  graph: workflowGraph,
                  settings: workflowSettings,
                })
              : null;
  const dirty =
    activeFormJson !== null &&
    savedSnapshot !== null &&
    activeFormJson !== savedSnapshot;

  const listItems: AssistantListItem[] = assistants.map((a) => ({
    id: a.id,
    name: a.name,
    type: typeToLabel[a.type],
    updatedAt: formatTimestamp(a.updatedAt),
    updatedAtRaw: a.updatedAt,
  }));
  const filteredAssistants = listItems.filter((assistant) => {
    if (typeFilter !== "全部" && assistant.type !== typeFilter) {
      return false;
    }

    const keyword = searchQuery.trim().toLowerCase();

    if (!keyword) {
      return true;
    }

    return [assistant.name, assistant.id, assistant.type, assistant.updatedAt]
      .join(" ")
      .toLowerCase()
      .includes(keyword);
  });

  // 按更新时间排序(以原始 ISO 时间戳为准);缺失时间戳视为最早,id 作为稳定的次级排序键
  const timeValue = (iso: string | null | undefined) => {
    const t = iso ? new Date(iso).getTime() : NaN;
    return Number.isNaN(t) ? 0 : t;
  };
  const sortedAssistants = [...filteredAssistants].sort((a, b) => {
    const diff = timeValue(b.updatedAtRaw) - timeValue(a.updatedAtRaw);
    if (diff !== 0) return sortOrder === "newest" ? diff : -diff;
    return a.id.localeCompare(b.id);
  });

  const pageSize = 5;
  const totalPages = Math.max(1, Math.ceil(sortedAssistants.length / pageSize));
  const safeCurrentPage = Math.min(currentPage, totalPages);
  const pageStart = (safeCurrentPage - 1) * pageSize;
  const pageEnd = pageStart + pageSize;
  const paginatedAssistants = sortedAssistants.slice(pageStart, pageEnd);

  function handleSearchChange(value: string) {
    setSearchQuery(value);
    setCurrentPage(1);
  }

  function handleFilterChange(filter: TypeFilter) {
    setTypeFilter(filter);
    setCurrentPage(1);
  }

  function handleSortChange(order: SortOrder) {
    setSortOrder(order);
    setCurrentPage(1);
  }

  function toggleSortOrder() {
    handleSortChange(sortOrder === "newest" ? "oldest" : "newest");
  }

  function updateForm<K extends keyof AssistantForm>(
    key: K,
    value: AssistantForm[K],
  ) {
    setForm((prev) => ({
      ...prev,
      [key]: value,
    }));
  }

  function updateFastGptForm<K extends keyof FastGptForm>(
    key: K,
    value: FastGptForm[K],
  ) {
    setFastGptForm((prev) => ({
      ...prev,
      [key]: value,
    }));
  }

  function updateDifyForm<K extends keyof DifyForm>(
    key: K,
    value: DifyForm[K],
  ) {
    setDifyForm((prev) => ({
      ...prev,
      [key]: value,
    }));
  }

  function updateOpenCodeForm<K extends keyof OpenCodeForm>(
    key: K,
    value: OpenCodeForm[K],
  ) {
    setOpenCodeForm((prev) => ({
      ...prev,
      [key]: value,
    }));
  }
  if (view === "loading") {
    return (
      <div className="flex h-full items-center justify-center">
        {loadError ? (
          <div className="text-center">
            <div className="font-medium text-destructive">加载助手失败</div>
            <div className="mt-2 text-sm text-muted-foreground">
              {loadError}
            </div>
            <Button
              variant="outline"
              size="sm"
              className="mt-4 border-hairline-strong text-muted-foreground hover:text-foreground"
              onClick={() => router.push("/assistants")}
            >
              返回列表
            </Button>
          </div>
        ) : (
          <div className="flex items-center gap-2 text-sm text-muted-foreground">
            <Loader2 size={16} className="animate-spin" />
            正在加载助手…
          </div>
        )}
      </div>
    );
  }

  if (view === "list") {
    return (
      <div className="mx-auto flex w-full max-w-[1440px] flex-col gap-8">
        <PageHeader
          title="助手列表"
          description="管理已有的视频助手，支持提示词、工作流、Dify 和 FastGPT 类型。"
          action={
            <Button
              size="lg"
              className="w-full gap-2 sm:w-auto"
              onClick={startCreate}
            >
              <Plus size={16} />
              创建助手
            </Button>
          }
        />

        <section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
          <ListToolbar
            filters={
              <FilterPills
                options={typeFilters}
                value={typeFilter}
                onChange={handleFilterChange}
              />
            }
            search={
              <SearchInput
                value={searchQuery}
                onChange={handleSearchChange}
                placeholder="搜索助手名称、类型或 ID..."
                className="lg:w-[320px]"
              />
            }
          />

          <DataList<AssistantListItem>
            rows={paginatedAssistants}
            rowKey={(assistant) => assistant.id}
            loading={listLoading}
            loadingText="正在加载助手列表…"
            error={listError}
            onRetry={() => void loadAssistants()}
            empty={{
              title: listItems.length === 0 ? "暂无助手" : "未找到匹配的助手",
              description:
                listItems.length === 0
                  ? "点击右上角「创建助手」开始。"
                  : "请调整关键词或筛选条件后再试。",
            }}
            pagination={{
              page: safeCurrentPage,
              totalPages,
              onPageChange: setCurrentPage,
              summary:
                filteredAssistants.length === 0
                  ? "没有数据"
                  : `显示 ${pageStart + 1}-${Math.min(pageEnd, filteredAssistants.length)} / 共 ${filteredAssistants.length} 个助手`,
            }}
            columns={[
              {
                key: "name",
                header: "助手名称",
                width: "md:w-[360px]",
                cell: (assistant) => (
                  <>
                    <div className="truncate font-medium text-foreground">
                      {assistant.name}
                    </div>
                    <div className="mt-1 text-xs text-muted-soft">
                      {assistant.id}
                    </div>
                  </>
                ),
              },
              {
                key: "type",
                header: "助手类型",
                width: "md:w-[128px]",
                cell: (assistant) => (
                  <Badge
                    variant="secondary"
                    className="h-6 bg-surface-strong px-3 text-muted-foreground"
                  >
                    {assistant.type}
                  </Badge>
                ),
              },
              {
                key: "updatedAt",
                width: "md:w-[176px]",
                header: (
                  <button
                    type="button"
                    onClick={toggleSortOrder}
                    className="caption-label -mx-2 inline-flex items-center gap-1 rounded-md px-2 py-1 text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
                    aria-label={
                      sortOrder === "newest"
                        ? "当前按最近更新排序,点击切换为最早更新"
                        : "当前按最早更新排序,点击切换为最近更新"
                    }
                  >
                    更新时间
                    {sortOrder === "newest" ? (
                      <ChevronDown size={13} />
                    ) : (
                      <ChevronUp size={13} />
                    )}
                  </button>
                ),
                cellClassName:
                  "whitespace-nowrap tabular-nums text-muted-foreground",
                cell: (assistant) => assistant.updatedAt,
              },
              {
                key: "actions",
                header: "操作",
                align: "right",
                cellClassName: "flex justify-end gap-2",
                cell: (assistant) => (
                  <>
                    <Button
                      variant="outline"
                      size="sm"
                      className="gap-1.5 border-hairline-strong text-xs text-muted-foreground hover:text-foreground"
                      onClick={() => handleEdit(assistant)}
                    >
                      <Pencil size={14} />
                      编辑
                    </Button>

                    <DropdownMenu>
                      <DropdownMenuTrigger asChild>
                        <Button
                          variant="outline"
                          size="icon-sm"
                          className="border-hairline-strong text-muted-foreground hover:text-foreground"
                          aria-label={`${assistant.name} 更多操作`}
                        >
                          <MoreHorizontal size={15} />
                        </Button>
                      </DropdownMenuTrigger>
                      <DropdownMenuContent
                        align="end"
                        className="w-32 min-w-32 rounded-xl border border-hairline bg-popover p-1"
                      >
                        <DropdownMenuItem
                          className="rounded-lg"
                          onSelect={() => handleDuplicate(assistant)}
                        >
                          <Copy size={14} />
                          复制
                        </DropdownMenuItem>
                        <DropdownMenuItem
                          variant="destructive"
                          className="rounded-lg"
                          onSelect={(event) => {
                            event.preventDefault();
                            void handleDelete(assistant.id);
                          }}
                        >
                          <Trash2 size={14} />
                          删除
                        </DropdownMenuItem>
                      </DropdownMenuContent>
                    </DropdownMenu>
                  </>
                ),
              },
            ]}
          />
        </section>
      </div>
    );
  }

  if (view === "choose") {
    return (
      <div className="mx-auto flex w-full max-w-[1180px] flex-col gap-8">
        <div className="flex items-start justify-between gap-6">
          <div>
            <h1 className="font-display display-lg text-ink">创建助手</h1>
            <p className="mt-3 max-w-2xl text-[15px] leading-7 text-muted-foreground">
              先为助手取个名字，再选择构建方式。确认后将立即创建助手并进入编辑页。
            </p>
          </div>

          <Button
            variant="outline"
            size="lg"
            className="shrink-0 gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
            onClick={() => router.push("/assistants")}
          >
            <ChevronLeft size={16} />
            返回列表
          </Button>
        </div>

        <section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
          <label className="block">
            <div className="mb-2 text-sm font-medium text-foreground">
              助手名称
            </div>
            <Input
              value={draftName}
              autoFocus
              onChange={(event) => setDraftName(event.target.value)}
              placeholder="请输入助手名称"
              className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
            />
          </label>
        </section>

        <section className="flex flex-col gap-4">
          <div className="text-sm font-medium text-foreground">构建方式</div>

          <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
            {assistantTypeOptions.map((option) => {
              const selected = draftType === option.type;

              return (
                <button
                  key={option.type}
                  type="button"
                  onClick={() => setDraftType(option.type)}
                  className={`group relative flex flex-col gap-4 rounded-2xl border bg-card p-5 text-left transition-colors ${
                    selected
                      ? "border-primary ring-1 ring-primary"
                      : "border-hairline hover:border-hairline-strong"
                  }`}
                >
                  <div className="flex items-center justify-between">
                    <div className="flex h-11 w-11 items-center justify-center rounded-full bg-surface-strong text-foreground">
                      {option.icon}
                    </div>

                    {selected ? (
                      <span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground">
                        <Check size={14} />
                      </span>
                    ) : (
                      !option.available && (
                        <Badge
                          variant="secondary"
                          className="h-6 bg-surface-strong px-3 text-xs text-muted-foreground"
                        >
                          即将上线
                        </Badge>
                      )
                    )}
                  </div>

                  <div>
                    <div className="text-base font-medium text-foreground">
                      {option.label}
                    </div>
                    <p className="mt-1.5 text-sm leading-6 text-muted-foreground">
                      {option.description}
                    </p>
                  </div>
                </button>
              );
            })}
          </div>
        </section>

        <div className="flex items-center justify-end gap-3">
          {createError && (
            <span className="text-xs text-destructive">{createError}</span>
          )}
          <Button
            variant="outline"
            size="lg"
            className="border-hairline-strong text-muted-foreground hover:text-foreground"
            disabled={creating}
            onClick={() => router.push("/assistants")}
          >
            取消
          </Button>
          <Button
            size="lg"
            className="gap-2"
            disabled={!draftName.trim() || !draftType || creating}
            onClick={() => void confirmType()}
          >
            {creating ? (
              <Loader2 size={16} className="animate-spin" />
            ) : (
              <Rocket size={16} />
            )}
            创建助手
          </Button>
        </div>
      </div>
    );
  }

  if (view === "workflow") {
    return (
      <div className="-mt-6 flex h-full flex-col gap-4">
        <div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
          <div className="flex min-w-0 items-center gap-2">
            <EditorBackButton onClick={() => router.push("/assistants")} />
            <EditableTitle value={workflowName} onChange={setWorkflowName} />
            <AssistantIdentity assistantId={editingId} />
          </div>

          <div className="flex shrink-0 gap-2">
            {saveError && (
              <span className="self-center text-xs text-destructive">
                {saveError}
              </span>
            )}
            <Button
              variant="outline"
              className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
              disabled={!editingId}
              onClick={() => setDebugOpen(true)}
            >
              <Bug size={16} />
              调试
            </Button>
            <Button
              className="gap-2"
              disabled={saving || !dirty || !workflowName.trim()}
              onClick={() => handleSaveWorkflow()}
            >
              {saving ? (
                <Loader2 size={16} className="animate-spin" />
              ) : (
                <Save size={16} />
              )}
              保存
            </Button>
          </div>
        </div>

        <div className="min-h-0 flex-1">
          <WorkflowEditor
            value={workflowGraph}
            onChange={setWorkflowGraph}
            settings={workflowSettings}
            onSettingsChange={setWorkflowSettings}
            activeNodeId={activeNodeId}
            modelOptions={{
              llm: credOptions("LLM"),
              asr: credOptions("ASR"),
              tts: credOptions("TTS"),
            }}
          />
        </div>

        <Sheet
          open={debugOpen}
          onOpenChange={(open) => {
            setDebugOpen(open);
            if (!open) setActiveNodeId(null);
          }}
          modal={false}
        >
          <SheetContent
            side="right"
            showOverlay={false}
            onInteractOutside={(e) => e.preventDefault()}
            className="w-[440px] gap-0 border-l border-hairline bg-card p-0 sm:max-w-[440px]"
          >
            <SheetHeader className="sr-only">
              <SheetTitle>语音调试</SheetTitle>
              <SheetDescription>
                与当前助手进行语音对话调试,画布会高亮正在激活的节点。
              </SheetDescription>
            </SheetHeader>
            <DebugDrawer
              assistantId={editingId}
              asSheet
              hasUnsavedChanges={dirty}
              onNodeActive={setActiveNodeId}
            />
          </SheetContent>
        </Sheet>
      </div>
    );
  }

  if (view === "create-dify") {
    return (
      <div className="-mt-6 flex h-full flex-col gap-4">
        <div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
          <div className="flex min-w-0 items-center gap-2">
            <EditorBackButton onClick={() => router.push("/assistants")} />
            <EditableTitle
              value={difyForm.name}
              onChange={(value) => updateDifyForm("name", value)}
            />
            <AssistantIdentity assistantId={editingId} />
          </div>

          <div className="flex shrink-0 gap-2">
            {saveError && (
              <span className="self-center text-xs text-destructive">
                {saveError}
              </span>
            )}
            <Button
              className="gap-2"
              disabled={
                saving || !dirty || !difyForm.name.trim() || !difyForm.agent
              }
              onClick={() => void handleSaveDify()}
            >
              {saving ? (
                <Loader2 size={16} className="animate-spin" />
              ) : (
                <Save size={16} />
              )}
              保存
            </Button>
          </div>
        </div>

        <div className="flex min-h-0 flex-1 gap-6">
          <div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
            <SectionCard
              icon={<Boxes size={18} />}
              title="Dify 应用配置"
              description="从「模型资源」中选择 Dify 应用。开场白、知识库、提示词等对话编排请在 Dify 平台配置，本页不重复设置。"
            >
              <ResourceSelectField
                label="Dify 应用"
                value={difyForm.agent}
                onChange={(value) => updateDifyForm("agent", value)}
                options={agentOptions("dify")}
                noneLabel="请选择"
              />
            </SectionCard>

            <SectionCard
              icon={<Brain size={18} />}
              title="语音配置"
              description="从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 Dify 应用提供，请前往 Dify 平台配置。"
            >
              <ResourceSelectField
                label="语音识别"
                value={difyForm.asr}
                onChange={(value) => updateDifyForm("asr", value)}
                options={credOptions("ASR")}
                noneLabel="无"
              />
              <ResourceSelectField
                label="语音合成"
                value={difyForm.voice}
                onChange={(value) => updateDifyForm("voice", value)}
                options={credOptions("TTS")}
                noneLabel="无"
              />
            </SectionCard>

            <SectionCard
              icon={<Sparkles size={18} />}
              title="交互策略"
              description="设置实时视频对话时的交互体验"
            >
              <TurnConfigEditor
                enabled={difyForm.enableInterrupt}
                config={difyForm.turnConfig}
                onEnabledChange={(checked) => updateDifyForm("enableInterrupt", checked)}
                onConfigChange={(config) => updateDifyForm("turnConfig", config)}
              />
            </SectionCard>
          </div>

          <DebugDrawer assistantId={editingId} hasUnsavedChanges={dirty} />
        </div>
      </div>
    );
  }

  if (view === "create-fastgpt") {
    return (
      <div className="-mt-6 flex h-full flex-col gap-4">
        <div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
          <div className="flex min-w-0 items-center gap-2">
            <EditorBackButton onClick={() => router.push("/assistants")} />
            <EditableTitle
              value={fastGptForm.name}
              onChange={(value) => updateFastGptForm("name", value)}
            />
            <AssistantIdentity assistantId={editingId} />
          </div>

          <div className="flex shrink-0 gap-2">
            {saveError && (
              <span className="self-center text-xs text-destructive">
                {saveError}
              </span>
            )}
            <Button
              className="gap-2"
              disabled={
                saving ||
                !dirty ||
                !fastGptForm.name.trim() ||
                !fastGptForm.agent
              }
              onClick={() => void handleSaveFastGpt()}
            >
              {saving ? (
                <Loader2 size={16} className="animate-spin" />
              ) : (
                <Save size={16} />
              )}
              保存
            </Button>
          </div>
        </div>

        <div className="flex min-h-0 flex-1 gap-6">
          <div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
            <SectionCard
              icon={<Database size={18} />}
              title="FastGPT 应用配置"
              description="从「模型资源」中选择 FastGPT 应用。开场白、知识库、提示词等对话编排请在 FastGPT 平台配置，本页不重复设置。"
            >
              <ResourceSelectField
                label="FastGPT 应用"
                value={fastGptForm.agent}
                onChange={(value) => updateFastGptForm("agent", value)}
                options={agentOptions("fastgpt")}
                noneLabel="请选择"
              />
            </SectionCard>

            <SectionCard
              icon={<Brain size={18} />}
              title="语音配置"
              description="从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 FastGPT 应用提供，请前往 FastGPT 平台配置。"
            >
              <ResourceSelectField
                label="语音识别"
                value={fastGptForm.asr}
                onChange={(value) => updateFastGptForm("asr", value)}
                options={credOptions("ASR")}
                noneLabel="无"
              />
              <ResourceSelectField
                label="语音合成"
                value={fastGptForm.voice}
                onChange={(value) => updateFastGptForm("voice", value)}
                options={credOptions("TTS")}
                noneLabel="无"
              />
            </SectionCard>

            <SectionCard
              icon={<Sparkles size={18} />}
              title="交互策略"
              description="设置实时视频对话时的交互体验"
            >
              <TurnConfigEditor
                enabled={fastGptForm.enableInterrupt}
                config={fastGptForm.turnConfig}
                onEnabledChange={(checked) => updateFastGptForm("enableInterrupt", checked)}
                onConfigChange={(config) => updateFastGptForm("turnConfig", config)}
              />
            </SectionCard>
          </div>

          <DebugDrawer
            assistantId={editingId}
            hasUnsavedChanges={dirty}
            vision={openCodeForm.visionEnabled}
          />
        </div>
      </div>
    );
  }

  if (view === "create-opencode") {
    return (
      <div className="-mt-6 flex h-full flex-col gap-4">
        <div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
          <div className="flex min-w-0 items-center gap-2">
            <EditorBackButton onClick={() => router.push("/assistants")} />
            <EditableTitle
              value={openCodeForm.name}
              onChange={(value) => updateOpenCodeForm("name", value)}
            />
            <AssistantIdentity assistantId={editingId} />
          </div>

          <div className="flex shrink-0 gap-2">
            {saveError && (
              <span className="self-center text-xs text-destructive">
                {saveError}
              </span>
            )}
            <Button
              className="gap-2"
              disabled={
                saving ||
                !dirty ||
                !openCodeForm.name.trim() ||
                !openCodeForm.agent
              }
              onClick={() => void handleSaveOpenCode()}
            >
              {saving ? (
                <Loader2 size={16} className="animate-spin" />
              ) : (
                <Save size={16} />
              )}
              保存
            </Button>
          </div>
        </div>

        <div className="flex min-h-0 flex-1 gap-6">
          <div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
            <SectionCard
              icon={<Terminal size={18} />}
              title="OpenCode 服务配置"
              description="从「模型资源」中选择 OpenCode 服务资源。"
            >
              <ResourceSelectField
                label="OpenCode 服务"
                value={openCodeForm.agent}
                onChange={(value) => updateOpenCodeForm("agent", value)}
                options={agentOptions("opencode")}
                noneLabel="请选择"
              />
            </SectionCard>

            <SectionCard
              icon={<MessageSquareText size={18} />}
              title="提示词"
              description="描述助手的角色、能力和回答要求"
            >
              <TextAreaField
                value={openCodeForm.prompt}
                onChange={(value) => updateOpenCodeForm("prompt", value)}
                placeholder="请输入提示词，描述助手的角色、能力和回答要求"
                rows={8}
              />
            </SectionCard>

            <SectionCard
              icon={<Brain size={18} />}
              title="模型与语音配置"
              description="配置 OpenCode 使用的大语言模型、语音识别与语音合成资源。"
            >
              <ToggleRow
                title="视觉理解"
                hint="开启后，开始对话时会允许助手按需理解当前视频画面。视觉模型选「模型自己」时，大语言模型本身必须支持图片输入。"
                checked={openCodeForm.visionEnabled}
                onChange={handleOpenCodeVisionEnabledChange}
              />
              {openCodeForm.visionEnabled && (
                <ResourceSelectField
                  label="视觉模型"
                  value={openCodeForm.visionModelResourceId}
                  onChange={(value) =>
                    updateOpenCodeForm("visionModelResourceId", value)
                  }
                  options={visionModelOptionsFor(openCodeForm.model)}
                  noneLabel="模型自己"
                />
              )}
              <ResourceSelectField
                label="大语言模型"
                value={openCodeForm.model}
                onChange={handleOpenCodeModelChange}
                options={credOptions("LLM")}
                noneLabel="无"
              />
              <ResourceSelectField
                label="语音识别"
                value={openCodeForm.asr}
                onChange={(value) => updateOpenCodeForm("asr", value)}
                options={credOptions("ASR")}
                noneLabel="无"
              />
              <ResourceSelectField
                label="语音合成"
                value={openCodeForm.voice}
                onChange={(value) => updateOpenCodeForm("voice", value)}
                options={credOptions("TTS")}
                noneLabel="无"
              />
            </SectionCard>

            <SectionCard
              icon={<Sparkles size={18} />}
              title="交互策略"
              description="设置实时视频对话时的交互体验"
            >
              <TurnConfigEditor
                enabled={openCodeForm.enableInterrupt}
                config={openCodeForm.turnConfig}
                onEnabledChange={(checked) => updateOpenCodeForm("enableInterrupt", checked)}
                onConfigChange={(config) => updateOpenCodeForm("turnConfig", config)}
              />
            </SectionCard>
          </div>

          <DebugDrawer assistantId={editingId} hasUnsavedChanges={dirty} />
        </div>
      </div>
    );
  }

  return (
    <div className="-mt-6 flex h-full flex-col gap-4">
      <div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
        <div className="flex min-w-0 items-center gap-2">
          <EditorBackButton onClick={() => router.push("/assistants")} />
          <EditableTitle
            value={form.name}
            onChange={(value) => updateForm("name", value)}
          />
          <AssistantIdentity assistantId={editingId} />
        </div>

        <div className="flex shrink-0 gap-2">
          {saveError && (
            <span className="self-center text-xs text-destructive">
              {saveError}
            </span>
          )}
          <Button
            className="gap-2"
            disabled={saving || !dirty || !form.name.trim()}
            onClick={() => void handleSavePrompt()}
          >
            {saving ? (
              <Loader2 size={16} className="animate-spin" />
            ) : (
              <Save size={16} />
            )}
            保存
          </Button>
        </div>
      </div>

      <div className="flex min-h-0 flex-1 gap-6">
        <div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
          <SectionCard>
            <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
              <div
                role="button"
                tabIndex={0}
                onClick={() => updateForm("runtimeMode", "pipeline")}
                onKeyDown={(event) => {
                  if (event.key === "Enter" || event.key === " ") {
                    event.preventDefault();
                    updateForm("runtimeMode", "pipeline");
                  }
                }}
                className={[
                  "cursor-pointer rounded-2xl border p-5 text-left transition-colors",
                  form.runtimeMode === "pipeline"
                    ? "border-primary bg-primary/5 ring-1 ring-primary"
                    : "border-hairline bg-canvas-soft hover:border-hairline-strong",
                ].join(" ")}
              >
                <div className="flex items-center justify-between gap-3">
                  <div className="flex items-center gap-3">
                    <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
                      <Waypoints size={18} />
                    </div>
                    <div className="flex items-center gap-1.5">
                      <span className="font-medium text-foreground">Pipeline 模式</span>
                      <HelpHint text="通过 ASR、LLM 和 TTS 级联组成语音管线，灵活选配各模块。" />
                    </div>
                  </div>
                  {form.runtimeMode === "pipeline" && (
                    <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
                      <Check size={14} />
                    </span>
                  )}
                </div>
              </div>

              <div
                role="button"
                tabIndex={0}
                onClick={() => updateForm("runtimeMode", "realtime")}
                onKeyDown={(event) => {
                  if (event.key === "Enter" || event.key === " ") {
                    event.preventDefault();
                    updateForm("runtimeMode", "realtime");
                  }
                }}
                className={[
                  "cursor-pointer rounded-2xl border p-5 text-left transition-colors",
                  form.runtimeMode === "realtime"
                    ? "border-primary bg-primary/5 ring-1 ring-primary"
                    : "border-hairline bg-canvas-soft hover:border-hairline-strong",
                ].join(" ")}
              >
                <div className="flex items-center justify-between gap-3">
                  <div className="flex items-center gap-3">
                    <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
                      <AudioLines size={18} />
                    </div>
                    <div className="flex items-center gap-1.5">
                      <span className="font-medium text-foreground">Realtime 模式</span>
                      <HelpHint text="使用原生实时语音模型，模型直接处理音频输入并生成语音回复。" />
                    </div>
                  </div>
                  {form.runtimeMode === "realtime" && (
                    <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
                      <Check size={14} />
                    </span>
                  )}
                </div>
              </div>
            </div>
          </SectionCard>

          <SectionCard
            icon={<MessageSquareText size={18} />}
            title="提示词"
            description="描述助手的角色、能力和回答要求"
          >
            <TextAreaField
              value={form.prompt}
              onChange={(value) => updateForm("prompt", value)}
              placeholder="请输入提示词，描述助手的角色、能力和回答要求"
              rows={8}
            />
          </SectionCard>

          {form.runtimeMode === "pipeline" ? (
            <SectionCard
              icon={<Brain size={18} />}
              title="模型配置"
              description="从「模型资源」中选择大语言模型、语音识别与语音合成"
            >
              <ToggleRow
                title="视觉理解"
                hint="开启后，开始对话时会允许助手按需理解当前视频画面。视觉模型选「模型自己」时，大语言模型本身必须支持图片输入。"
                checked={form.visionEnabled}
                onChange={handlePromptVisionEnabledChange}
              />
              {form.visionEnabled && (
                <ResourceSelectField
                  label="视觉模型"
                  value={form.visionModelResourceId}
                  onChange={(value) =>
                    updateForm("visionModelResourceId", value)
                  }
                  options={visionModelOptionsFor(form.model)}
                  noneLabel="模型自己"
                />
              )}
              <ResourceSelectField
                label="大语言模型"
                value={form.model}
                onChange={handlePromptModelChange}
                options={credOptions("LLM")}
                noneLabel="无"
              />
              <ResourceSelectField
                label="语音识别"
                value={form.asr}
                onChange={(value) => updateForm("asr", value)}
                options={credOptions("ASR")}
                noneLabel="无"
              />
              <ResourceSelectField
                label="语音合成"
                value={form.voice}
                onChange={(value) => updateForm("voice", value)}
                options={credOptions("TTS")}
                noneLabel="无"
              />
            </SectionCard>
          ) : (
            <SectionCard
              icon={<Brain size={18} />}
              title="模型配置"
              description="当前模式下 ASR 与 TTS 由 Realtime 模型内置完成"
            >
              <ResourceSelectField
                label="Realtime 模型"
                value={form.realtimeModel}
                onChange={(value) => updateForm("realtimeModel", value)}
                options={credOptions("Realtime")}
                noneLabel="无"
              />
            </SectionCard>
          )}

          <SectionCard
            icon={<Bot size={18} />}
            title="开场白"
            description="助手与用户首次对话时的开场语"
          >
            <TextAreaField
              value={form.greeting}
              onChange={(value) => updateForm("greeting", value)}
              placeholder="请输入助手开场白"
            />
          </SectionCard>

          {form.runtimeMode === "pipeline" && (
            <SectionCard
              icon={<Database size={18} />}
              title="知识库配置"
              description="选择助手回答时可检索的业务知识来源"
            >
              <div className="flex items-center gap-1.5">
                <span className="text-sm font-medium text-foreground">
                  知识库选择
                </span>
                <KnowledgeRetrievalConfigDialog
                  disabled={!form.knowledgeBase}
                  value={form.knowledgeRetrievalConfig}
                  onChange={(config) =>
                    updateForm("knowledgeRetrievalConfig", config)
                  }
                />
              </div>
              <ResourceSelectField
                value={form.knowledgeBase}
                onChange={(value) => updateForm("knowledgeBase", value)}
                options={kbOptions}
                noneLabel="无"
              />
            </SectionCard>
          )}

          <SectionCard
            icon={<Wrench size={18} />}
            title="工具"
            description="配置该提示词助手可以调用的工具"
          >
            <ToolPicker
              tools={tools.filter((tool) => tool.status === "active")}
              selectedIds={form.toolIds}
              onChange={(toolIds) => updateForm("toolIds", toolIds)}
            />
          </SectionCard>

          <SectionCard
            icon={<Sparkles size={18} />}
            title="交互策略"
            description="设置实时视频对话时的交互体验"
          >
            {form.runtimeMode === "pipeline" ? (
              <TurnConfigEditor
                enabled={form.enableInterrupt}
                config={form.turnConfig}
                onEnabledChange={(checked) => updateForm("enableInterrupt", checked)}
                onConfigChange={(config) => updateForm("turnConfig", config)}
              />
            ) : (
              <p className="text-sm text-muted-foreground">
                高级抢话与轮次检测当前仅支持 Pipeline 模式。
              </p>
            )}
          </SectionCard>
        </div>

        <DebugDrawer
          assistantId={editingId}
          hasUnsavedChanges={dirty}
          vision={form.visionEnabled}
        />
      </div>
    </div>
  );
}

type VizStyle = "aura" | "nebula" | "bars" | "wave";

// 调试面板顶部主视图:聊天记录 / 视频流
type DebugView = "chat" | "video";
type DebugInputMode = "mic" | "text";

const VIZ_OPTIONS: { style: VizStyle; label: string; icon: React.ReactNode }[] =
  [
    { style: "aura", label: "光环", icon: <Orbit size={14} /> },
    { style: "nebula", label: "星云", icon: <Sparkles size={14} /> },
    { style: "bars", label: "频谱", icon: <AudioLines size={14} /> },
    { style: "wave", label: "波形", icon: <Waves size={14} /> },
  ];

// 中央语音可视化(光环/星云/频谱/波形)暂时隐藏:调试面板固定为
// 「上聊天记录 + 下波形监控」布局。置 true 可恢复可视化视图与样式切换。
const SHOW_VOICE_VIZ = false;

function SegmentedIconGroup({
  children,
  label,
}: {
  children: React.ReactNode;
  label: string;
}) {
  return (
    <div
      role="group"
      aria-label={label}
      className="flex items-center gap-0.5 rounded-full border border-hairline bg-canvas-soft p-0.5"
    >
      {children}
    </div>
  );
}

function SegmentedIconButton({
  selected,
  label,
  onClick,
  children,
}: {
  selected: boolean;
  label: string;
  onClick: () => void;
  children: React.ReactNode;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      aria-label={label}
      aria-pressed={selected}
      title={label}
      className={[
        "flex h-7 w-7 items-center justify-center rounded-full transition-colors",
        selected
          ? "bg-surface-strong text-foreground shadow-sm"
          : "text-muted-soft hover:text-foreground",
      ].join(" ")}
    >
      {children}
    </button>
  );
}

function DebugDrawer({
  assistantId,
  asSheet = false,
  hasUnsavedChanges = false,
  onNodeActive,
  vision = false,
}: {
  assistantId: string | null;
  asSheet?: boolean;
  hasUnsavedChanges?: boolean;
  onNodeActive?: (nodeId: string | null) => void;
  vision?: boolean;
}) {
  const preview = useVoicePreview(assistantId, onNodeActive);
  const camera = useCameraPreview();
  const [showTranscript, setShowTranscript] = useState(false);
  const [vizStyle, setVizStyle] = useState<VizStyle>("aura");
  const [view, setView] = useState<DebugView>("chat");
  const stopCamera = camera.stop;
  const startCamera = camera.start;
  const recording =
    preview.status === "connecting" || preview.status === "connected";
  const shouldKeepCamera = vision && recording;

  useEffect(() => {
    if (shouldKeepCamera) {
      void startCamera();
    } else {
      stopCamera();
    }
  }, [shouldKeepCamera, startCamera, stopCamera]);

  const selectCamera = useCallback(
    async (deviceId: string) => {
      const nextStream = await camera.selectCamera(deviceId);
      await preview.replaceVideoStream(nextStream);
    },
    [camera, preview],
  );

  const containerClass = asSheet
    ? "flex h-full min-w-0 flex-1 flex-col overflow-hidden"
    : "hidden min-w-0 flex-1 flex-col overflow-hidden rounded-2xl border border-hairline bg-card shadow-sm lg:flex";

  return (
    <aside className={containerClass}>
      <div className="flex shrink-0 items-center justify-between gap-3 border-b border-hairline px-5 py-3">
        <div className="flex min-w-0 items-center gap-2.5">
          <div className="shrink-0 text-sm font-medium text-foreground">
            调试与预览
          </div>
          <NetworkQualityIndicator
            quality={preview.networkQuality}
            status={preview.status}
          />
          <DebugConnectionStatus
            status={preview.status}
            micWarning={preview.micWarning}
          />
        </div>
        <div className="flex shrink-0 items-center gap-2">
          <CallPreviewLink assistantId={assistantId} />
          {SHOW_VOICE_VIZ && view === "chat" && (
            <>
              {!showTranscript && (
                <SegmentedIconGroup label="可视化样式">
                  {VIZ_OPTIONS.map((option) => (
                    <SegmentedIconButton
                      key={option.style}
                      selected={vizStyle === option.style}
                      label={`可视化样式：${option.label}`}
                      onClick={() => setVizStyle(option.style)}
                    >
                      {option.icon}
                    </SegmentedIconButton>
                  ))}
                </SegmentedIconGroup>
              )}
              <SegmentedIconGroup label="语音视图">
                <SegmentedIconButton
                  selected={!showTranscript}
                  label="语音可视化视图"
                  onClick={() => setShowTranscript(false)}
                >
                  <Mic size={14} />
                </SegmentedIconButton>
                <SegmentedIconButton
                  selected={showTranscript}
                  label="文字聊天记录视图"
                  onClick={() => setShowTranscript(true)}
                >
                  <MessageSquareText size={14} />
                </SegmentedIconButton>
              </SegmentedIconGroup>
            </>
          )}
          <SegmentedIconGroup label="预览视图">
            <SegmentedIconButton
              selected={view === "chat"}
              label="聊天记录"
              onClick={() => setView("chat")}
            >
              <MessageSquareText size={14} />
            </SegmentedIconButton>
            <SegmentedIconButton
              selected={view === "video"}
              label="视频流"
              onClick={() => setView("video")}
            >
              <Video size={14} />
            </SegmentedIconButton>
          </SegmentedIconGroup>
        </div>
      </div>
      <div className="shrink-0 border-b border-hairline px-5 py-2.5">
        <div className="flex h-10 min-w-0 items-center rounded-[1.4rem] border border-hairline-strong bg-background px-2">
          <CameraDeviceField camera={camera} onSelect={selectCamera} />
        </div>
      </div>
      <DebugVoicePanel
        view={view}
        showTranscript={showTranscript}
        vizStyle={vizStyle}
        assistantId={assistantId}
        preview={preview}
        camera={camera}
        hasUnsavedChanges={hasUnsavedChanges}
        vision={vision}
      />
    </aside>
  );
}

function CallPreviewLink({ assistantId }: { assistantId: string | null }) {
  const [callUrl, setCallUrl] = useState("");
  const [copied, setCopied] = useState(false);

  const copyLink = useCallback(async () => {
    if (!callUrl) return;
    await navigator.clipboard.writeText(callUrl);
    setCopied(true);
    window.setTimeout(() => setCopied(false), 1600);
  }, [callUrl]);

  return (
    <Popover
      onOpenChange={(open) => {
        if (open && assistantId) {
          setCallUrl(
            `${window.location.origin}/call/${encodeURIComponent(assistantId)}`,
          );
          setCopied(false);
        }
      }}
    >
      <PopoverTrigger asChild>
        <button
          type="button"
          disabled={!assistantId}
          aria-label="打开手机通话链接"
          title={assistantId ? "手机通话链接" : "请先保存助手"}
          className="flex h-8 w-8 items-center justify-center rounded-full border border-hairline bg-canvas-soft text-muted-foreground transition-colors hover:bg-surface-strong hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
        >
          <Smartphone size={15} />
        </button>
      </PopoverTrigger>
      <PopoverContent align="end" side="bottom" className="w-80 space-y-3">
        <div className="space-y-1">
          <div className="text-sm font-medium text-foreground">手机通话预览</div>
          <p className="text-xs leading-5 text-muted-foreground">
            复制链接并在浏览器中打开，即可进入全屏视频通话界面。
          </p>
        </div>
        <div className="flex items-center gap-2">
          <Input
            readOnly
            value={callUrl}
            aria-label="手机通话链接"
            onFocus={(event) => event.currentTarget.select()}
            className="h-9 min-w-0 flex-1 text-xs"
          />
          <Button
            type="button"
            size="icon"
            variant="secondary"
            className="h-9 w-9"
            onClick={() => void copyLink()}
            aria-label="复制手机通话链接"
          >
            {copied ? <Check size={15} /> : <Copy size={15} />}
          </Button>
        </div>
        {copied && <p className="text-xs text-success">链接已复制</p>}
      </PopoverContent>
    </Popover>
  );
}

function DeviceSelectField({
  icon,
  ariaLabel,
  placeholder,
  fallbackLabel,
  value,
  devices,
  onSelect,
}: {
  icon: React.ReactNode;
  ariaLabel: string;
  placeholder: string;
  fallbackLabel: string;
  value: string;
  devices: MediaDeviceInfo[];
  onSelect: (deviceId: string) => void;
}) {
  return (
    <Select
      value={value || "default"}
      onValueChange={(nextValue) =>
        onSelect(nextValue === "default" ? "" : nextValue)
      }
    >
      <SelectTrigger
        size="sm"
        className="h-10 min-w-0 flex-1 justify-between rounded-full border-transparent bg-transparent px-3 text-sm text-foreground shadow-none hover:bg-surface-strong focus-visible:ring-0 data-[size=sm]:h-10"
        aria-label={ariaLabel}
      >
        <span className="flex min-w-0 items-center gap-2">
          {icon}
          <span className="min-w-0 truncate text-left">
            <SelectValue placeholder={placeholder} />
          </span>
        </span>
      </SelectTrigger>
      <SelectContent
        position="popper"
        side="top"
        align="start"
        className="w-[280px]"
      >
        <SelectItem value="default">{placeholder}</SelectItem>
        {devices.map((device, index) => (
          <SelectItem key={device.deviceId} value={device.deviceId}>
            {device.label || `${fallbackLabel} ${index + 1}`}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  );
}

function MicrophoneDeviceField({ preview }: { preview: VoicePreview }) {
  return (
    <DeviceSelectField
      icon={<Mic size={15} className="shrink-0 text-muted-soft" />}
      ariaLabel="选择麦克风"
      placeholder="默认麦克风"
      fallbackLabel="麦克风"
      value={preview.selectedDeviceId}
      devices={preview.audioInputs}
      onSelect={(deviceId) => preview.selectDevice(deviceId)}
    />
  );
}

function CameraDeviceField({
  camera,
  onSelect,
}: {
  camera: CameraPreview;
  onSelect?: (deviceId: string) => void | Promise<void>;
}) {
  return (
    <DeviceSelectField
      icon={<Video size={15} className="shrink-0 text-muted-soft" />}
      ariaLabel="选择摄像头"
      placeholder="默认摄像头"
      fallbackLabel="摄像头"
      value={camera.deviceId}
      devices={camera.devices}
      onSelect={(deviceId) => void (onSelect ?? camera.selectCamera)(deviceId)}
    />
  );
}

function DebugInputModeButton({
  selected,
  label,
  onClick,
  children,
}: {
  selected: boolean;
  label: string;
  onClick: () => void;
  children: React.ReactNode;
}) {
  return (
    <button
      type="button"
      aria-label={label}
      aria-pressed={selected}
      title={label}
      onClick={onClick}
      className={[
        "flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors",
        selected
          ? "bg-surface-strong text-foreground"
          : "text-muted-soft hover:bg-surface-strong hover:text-foreground",
      ].join(" ")}
    >
      {children}
    </button>
  );
}

function DebugVoicePanel({
  view,
  showTranscript,
  vizStyle,
  assistantId,
  preview,
  camera,
  hasUnsavedChanges,
  vision,
}: {
  view: DebugView;
  showTranscript: boolean;
  vizStyle: VizStyle;
  assistantId: string | null;
  preview: VoicePreview;
  camera: CameraPreview;
  hasUnsavedChanges: boolean;
  vision: boolean;
}) {
  const {
    status,
    error,
    micWarning,
    localStream,
    remoteStream,
    messages,
    sendText,
    connect,
    disconnect,
    audioRef,
  } = preview;
  const recording = status === "connecting" || status === "connected";
  const [textDraft, setTextDraft] = useState("");
  const [inputMode, setInputMode] = useState<DebugInputMode>("mic");
  const inChatView = view === "chat" && (!SHOW_VOICE_VIZ || showTranscript);
  const idleOrFailed = status === "idle" || status === "failed";
  const showIdleHub =
    idleOrFailed &&
    (view === "video" || (messages.length === 0 && inChatView));
  const startBlockedMessage = !assistantId
    ? "请先保存助手，再开始对话。"
    : hasUnsavedChanges
      ? "请先保存当前改动，再开始对话。"
      : "";
  const startDisabled = status === "connecting" || Boolean(startBlockedMessage);

  function handleSendText() {
    if (sendText(textDraft)) {
      setTextDraft("");
    }
  }

  const startConversation = useCallback(async () => {
    if (!assistantId || hasUnsavedChanges) return;
    const videoStream = vision ? await camera.start() : null;
    await connect({
      visionEnabled: vision,
      videoStream,
    });
  }, [assistantId, camera, connect, hasUnsavedChanges, vision]);

  return (
    <div className="flex min-h-0 flex-1 flex-col">
      {/* 后端 TTS 音频经 WebRTC 媒体流过来,挂这里播放 */}
      <audio ref={audioRef} autoPlay playsInline className="hidden" />
      {view === "video" ? (
        showIdleHub ? (
          <DebugIdleHub
            status={status}
            error={error}
            message={startBlockedMessage}
            connect={startConversation}
          />
        ) : (
          <DebugVideoPanel camera={camera} />
        )
      ) : !SHOW_VOICE_VIZ || showTranscript ? (
        showIdleHub ? (
          <DebugIdleHub
            status={status}
            error={error}
            message={startBlockedMessage}
            connect={startConversation}
          />
        ) : (
          <DebugTranscriptPanel messages={messages} recording={recording} />
        )
      ) : (
        <div className="scrollbar-subtle relative flex min-h-0 flex-1 flex-col items-center justify-center gap-3 overflow-y-auto px-6 py-3 text-center">
          <div
            className="pointer-events-none absolute left-1/2 top-2 h-72 w-72 -translate-x-1/2 rounded-full opacity-50 blur-3xl"
            style={{
              background:
                "radial-gradient(circle, color-mix(in srgb, var(--gradient-sky) 42%, transparent), color-mix(in srgb, var(--gradient-lavender) 18%, transparent) 48%, transparent 74%)",
            }}
          />

          <Badge
            variant="secondary"
            className="relative gap-1.5 rounded-full border border-hairline bg-canvas-soft px-3 py-1 text-[11px] font-medium text-muted-foreground shadow-none"
          >
            <span
              className={[
                "h-1.5 w-1.5 rounded-full",
                recording ? "animate-pulse bg-success" : "bg-muted-soft",
              ].join(" ")}
            />
            {recording ? "会话进行中" : "准备开始"}
          </Badge>

          <div className="relative flex h-[200px] w-[240px] shrink-0 items-center justify-center">
            {(() => {
              const shared = {
                active: Boolean(localStream),
                stream: localStream,
                className: "relative shrink-0",
              } as const;
              if (vizStyle === "aura")
                return <AuraVisualizer {...shared} size={200} />;
              if (vizStyle === "nebula")
                return <NebulaVisualizer {...shared} size={200} />;
              if (vizStyle === "bars")
                return <SpectrumVisualizer {...shared} size={200} />;
              return <WaveVisualizer {...shared} size={200} />;
            })()}
          </div>

          <div className="relative max-w-xs space-y-1.5">
            <div className="font-display display-sm text-foreground">
              {status === "connecting"
                ? "连接中…"
                : status === "connected"
                  ? micWarning
                    ? "仅收听模式"
                    : "我在聆听"
                  : "开始一次语音对话"}
            </div>
            <p className="mx-auto text-xs leading-5 text-muted-foreground">
              {status === "failed"
                  ? error ||
                    "连接失败，请确认后端已启动且助手已保存后重试。"
                  : !assistantId
                    ? "请先保存助手，再开始语音预览。"
                    : hasUnsavedChanges
                      ? "请先保存当前改动，再开始语音预览。"
                    : micWarning
                      ? `${micWarning} 可接收助手播报，但无法发送语音。`
                    : recording
                      ? "直接说话即可。助手会在您停顿后自然回应。"
                      : "测试语音识别、响应速度与助手的播报效果。"}
            </p>
          </div>

          <Button
            disabled={recording ? status === "connecting" : startDisabled}
            onClick={() => {
              if (recording) {
                disconnect();
              } else {
                void startConversation();
              }
            }}
            className={[
              "relative h-11 gap-2 rounded-full px-6 text-sm font-medium shadow-sm transition-transform hover:scale-[1.03]",
              recording
                ? "bg-destructive text-white hover:bg-destructive/90"
                : "",
            ].join(" ")}
            aria-label={recording ? "结束语音测试" : "开始语音测试"}
          >
            {status === "connecting" ? (
              <Loader2 size={18} className="animate-spin" />
            ) : recording ? (
              <PhoneOff size={18} />
            ) : (
              <Mic size={18} />
            )}
            {recording ? "结束对话" : "开始对话"}
          </Button>
        </div>
      )}

      <div className="shrink-0 border-t border-hairline bg-card p-3">
        <div className="flex items-center gap-2">
          <div
            className={[
              "flex h-10 min-w-0 items-center gap-1 rounded-[1.4rem] border border-hairline-strong bg-background px-2",
              "flex-1",
            ].join(" ")}
          >
            <div className="flex shrink-0 items-center gap-0.5 rounded-full bg-canvas-soft p-0.5">
              <DebugInputModeButton
                selected={inputMode === "mic"}
                label="选择麦克风设备"
                onClick={() => setInputMode("mic")}
              >
                <Mic size={15} />
              </DebugInputModeButton>
              <DebugInputModeButton
                selected={inputMode === "text"}
                label="文字输入"
                onClick={() => setInputMode("text")}
              >
                <MessageSquareText size={15} />
              </DebugInputModeButton>
            </div>
            {inputMode === "mic" ? (
              <MicrophoneDeviceField preview={preview} />
            ) : (
              <Textarea
                rows={1}
                value={textDraft}
                disabled={status !== "connected"}
                onChange={(event) => setTextDraft(event.target.value)}
                onKeyDown={(event) => {
                  if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
                    event.preventDefault();
                    handleSendText();
                  }
                }}
                placeholder={
                  status === "connected"
                    ? "输入文字发送给助手，将打断当前播报…"
                    : "开始对话后可输入文字…"
                }
                className="h-8 min-h-8 flex-1 resize-none overflow-hidden border-transparent bg-transparent px-2 py-1 text-sm leading-6 text-foreground shadow-none outline-none placeholder:text-muted-soft focus-visible:ring-0 disabled:opacity-100"
              />
            )}
          </div>
          {inputMode === "text" && (
            <Button
              size="icon"
              className="h-10 w-10 shrink-0 rounded-full"
              aria-label="发送调试消息"
              disabled={status !== "connected" || !textDraft.trim()}
              onClick={handleSendText}
            >
              <Send size={16} />
            </Button>
          )}
          {!showIdleHub && (
            <div className="flex shrink-0 flex-col items-end gap-1">
              <Button
                size="sm"
                disabled={recording ? status === "connecting" : startDisabled}
                onClick={() => {
                  if (recording) {
                    disconnect();
                  } else {
                    void startConversation();
                  }
                }}
                className={[
                  "h-10 gap-1.5 rounded-full px-4",
                  recording
                    ? "bg-destructive text-white hover:bg-destructive/90"
                    : "",
                ].join(" ")}
                aria-label={recording ? "结束语音测试" : "开始语音测试"}
              >
                {status === "connecting" ? (
                  <Loader2 size={14} className="animate-spin" />
                ) : recording ? (
                  <PhoneOff size={14} />
                ) : (
                  <Mic size={14} />
                )}
                {recording ? "结束对话" : "开始对话"}
              </Button>
              {!recording && startBlockedMessage && (
                <span className="max-w-40 text-right text-[11px] leading-4 text-destructive">
                  {startBlockedMessage}
                </span>
              )}
            </div>
          )}
        </div>
      </div>

      {/* 底部双轨波形:会话连通后默认展开,空闲时收起 */}
      <WaveformTimelinePanel
        key={status === "connected" ? "active" : "idle"}
        defaultOpen={false}
        userStream={localStream}
        agentStream={remoteStream}
        active={status === "connected"}
      />
    </div>
  );
}

// 空闲态只保留一个明确入口,避免中间区域显得拥挤。
function DebugIdleHub({
  status,
  error,
  message,
  connect,
}: {
  status: VoicePreviewStatus;
  error: string | null;
  message: string;
  connect: () => Promise<void>;
}) {
  const helperText =
    message ||
    (status === "failed"
      ? error || "连接失败，请确认后端已启动且助手已保存后重试。"
      : "");

  return (
    <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-6 py-8 text-center">
      <Button
        disabled={Boolean(message)}
        onClick={() => void connect()}
        className="h-11 gap-2 rounded-full px-6 text-sm font-medium shadow-sm"
        aria-label={status === "failed" ? "重新连接" : "开始语音测试"}
      >
        <Mic size={18} />
        {status === "failed" ? "重新连接" : "开始对话"}
      </Button>
      {helperText && (
        <p
          className={[
            "max-w-xs text-xs leading-5",
            message ? "text-destructive" : "text-muted-foreground",
          ].join(" ")}
        >
          {helperText}
        </p>
      )}
    </div>
  );
}

function DebugVideoPanel({ camera }: { camera: CameraPreview }) {
  const { stream, error, starting, active } = camera;
  const videoRef = useRef<HTMLVideoElement>(null);

  useEffect(() => {
    if (videoRef.current) videoRef.current.srcObject = stream;
  }, [stream]);

  return (
    <div
      className={[
        "relative flex min-h-0 flex-1 items-center justify-center overflow-hidden",
        active ? "bg-black" : "bg-canvas-soft",
      ].join(" ")}
    >
      <video
        ref={videoRef}
        autoPlay
        playsInline
        muted
        className={[
          "h-full w-full -scale-x-100 object-contain",
          active ? "" : "hidden",
        ].join(" ")}
      />
      {active ? (
        <Badge
          variant="secondary"
          className="absolute left-3 top-3 gap-1.5 rounded-full border border-hairline bg-card/80 px-2.5 py-1 text-[11px] font-medium text-muted-foreground shadow-none backdrop-blur"
        >
          <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-success" />
          预览中
        </Badge>
      ) : (
        <div className="flex max-w-xs flex-col items-center gap-2 px-6 text-center">
          <Video size={28} className="text-muted-soft" />
          <div className="text-sm font-medium text-foreground">
            {starting ? "正在开启摄像头…" : "摄像头不可用"}
          </div>
          {error && (
            <p className="text-xs leading-5 text-muted-foreground">{error}</p>
          )}
        </div>
      )}
    </div>
  );
}

function DebugConnectionStatus({
  status,
  micWarning,
}: {
  status: VoicePreviewStatus;
  micWarning: string | null;
}) {
  const recording = status === "connecting" || status === "connected";
  const label =
    status === "connecting"
      ? "连接中…"
      : status === "connected"
        ? micWarning
          ? "仅收听"
          : "进行中"
        : status === "failed"
          ? "连接失败"
          : "准备开始";

  return (
    <span className="flex min-w-0 items-center gap-1.5 truncate text-xs text-muted-foreground">
      <span
        className={[
          "h-1.5 w-1.5 shrink-0 rounded-full",
          recording
            ? "animate-pulse bg-success"
            : status === "failed"
              ? "bg-destructive"
              : "bg-muted-soft",
        ].join(" ")}
      />
      {label}
    </span>
  );
}

// ISO 时间戳 → HH:MM(本地时区),解析失败返回空串
function formatMessageTime(iso: string): string {
  const d = new Date(iso);
  if (Number.isNaN(d.getTime())) return "";
  const pad = (n: number) => String(n).padStart(2, "0");
  return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}

function DebugTranscriptPanel({
  messages,
  recording = false,
}: {
  messages: ChatMessage[];
  recording?: boolean;
}) {
  const scrollRef = useRef<HTMLDivElement>(null);

  // 新消息时滚到底部
  useEffect(() => {
    const el = scrollRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [messages]);

  if (messages.length === 0) {
    return (
      <div
        className={[
          "scrollbar-subtle flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-3",
          recording ? "items-center justify-center" : "pt-5",
        ].join(" ")}
      >
        <div className="text-center">
          <MessageSquareText size={22} className="mx-auto text-muted-soft" />
          <div className="mt-2 text-sm font-medium text-foreground">
            {recording ? "暂无聊天记录" : "尚未开始对话"}
          </div>
          <p className="mx-auto mt-1 max-w-xs text-xs leading-5 text-muted-foreground">
            {recording
              ? "开口说话或在下方输入文字，对话内容会实时显示在这里。"
              : "点击「开始对话」后，语音与文字消息会实时显示在这里。"}
          </p>
        </div>
      </div>
    );
  }

  return (
    <div
      ref={scrollRef}
      className="scrollbar-subtle flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-4"
    >
      <div className="flex flex-col gap-4">
        {messages.map((message) => {
          const time = formatMessageTime(message.timestamp);
          return message.role === "assistant" ? (
            <div
              key={message.id}
              className="flex max-w-[88%] flex-col gap-1 self-start"
            >
              <span className="px-1 text-[11px] text-muted-soft">
                助手{time ? ` · ${time}` : ""}
              </span>
              <div className="whitespace-pre-wrap rounded-2xl rounded-tl-sm bg-surface-strong px-4 py-2.5 text-sm leading-6 text-foreground">
                {message.content}
              </div>
            </div>
          ) : (
            <div
              key={message.id}
              className="flex max-w-[88%] flex-col items-end gap-1 self-end"
            >
              <span className="px-1 text-[11px] text-muted-soft">
                我{time ? ` · ${time}` : ""}
              </span>
              <div className="whitespace-pre-wrap rounded-2xl rounded-tr-sm bg-primary px-4 py-2.5 text-sm leading-6 text-primary-foreground">
                {message.content}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function EditorBackButton({ onClick }: { onClick: () => void }) {
  return (
    <Button
      variant="ghost"
      size="icon"
      className="shrink-0 text-muted-foreground hover:text-foreground"
      onClick={onClick}
      aria-label="返回助手列表"
    >
      <ChevronLeft size={20} />
    </Button>
  );
}

function AssistantIdentity({ assistantId }: { assistantId: string | null }) {
  const [copied, setCopied] = useState(false);

  async function copyId() {
    if (!assistantId) return;
    await navigator.clipboard.writeText(assistantId);
    setCopied(true);
    window.setTimeout(() => setCopied(false), 1600);
  }

  return (
    <div className="ml-1 flex shrink-0 items-center rounded-full border border-hairline bg-canvas-soft pl-2.5 text-[11px] text-muted-foreground">
      <span className="font-mono">
        {assistantId ? `ID · ${assistantId}` : "ID · 保存后生成"}
      </span>
      {assistantId && (
        <button
          type="button"
          onClick={() => void copyId()}
          className="ml-1 flex h-7 w-7 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
          aria-label={copied ? "助手 ID 已复制" : "复制助手 ID"}
          title={copied ? "已复制" : "复制 ID"}
        >
          {copied ? <Check size={13} /> : <Copy size={13} />}
        </button>
      )}
      {!assistantId && <span className="h-7 w-2" aria-hidden />}
    </div>
  );
}

function EditableTitle({
  value,
  onChange,
}: {
  value: string;
  onChange: (value: string) => void;
}) {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(value);
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (editing) {
      inputRef.current?.focus();
      inputRef.current?.select();
    }
  }, [editing]);

  function startEdit() {
    setDraft(value);
    setEditing(true);
  }

  function commit() {
    const next = draft.trim();
    if (next) {
      onChange(next);
    }
    setEditing(false);
  }

  if (editing) {
    return (
      <input
        ref={inputRef}
        value={draft}
        onChange={(event) => setDraft(event.target.value)}
        onBlur={commit}
        onKeyDown={(event) => {
          if (event.key === "Enter") {
            event.preventDefault();
            commit();
          } else if (event.key === "Escape") {
            event.preventDefault();
            setEditing(false);
          }
        }}
        className="font-display display-sm w-[min(60vw,420px)] border-b border-primary bg-transparent text-ink outline-none"
      />
    );
  }

  return (
    <button
      type="button"
      onClick={startEdit}
      title="点击修改助手名称"
      className="group -mx-2 flex min-w-0 items-center gap-2 rounded-lg px-2 py-1 text-left transition-colors hover:bg-surface-strong"
    >
      <span className="font-display display-sm truncate text-ink">
        {value || "未命名助手"}
      </span>
      <Pencil
        size={16}
        className="shrink-0 text-muted-soft opacity-0 transition-opacity group-hover:opacity-100"
      />
    </button>
  );
}

function HelpHint({ text }: { text: string }) {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <button
          type="button"
          aria-label="查看说明"
          onClick={(event) => event.stopPropagation()}
          className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
        >
          <HelpCircle size={14} />
        </button>
      </PopoverTrigger>
      <PopoverContent
        align="start"
        className="w-72 text-sm leading-6 text-muted-foreground"
      >
        {text}
      </PopoverContent>
    </Popover>
  );
}

function SectionCard({
  icon,
  title,
  description,
  children,
}: {
  icon?: React.ReactNode;
  title?: string;
  description?: string;
  children: React.ReactNode;
}) {
  const hasHeader = Boolean(title);

  return (
    <Card className="rounded-2xl border-hairline bg-card text-card-foreground shadow-sm">
      {hasHeader && (
        <CardHeader>
          <div className="flex items-center gap-3">
            {icon && (
              <div className="flex h-10 w-10 items-center justify-center rounded-full bg-surface-strong text-foreground">
                {icon}
              </div>
            )}

            <div className="flex items-center gap-1.5">
              <CardTitle className="text-base font-medium">{title}</CardTitle>
              {description && <HelpHint text={description} />}
            </div>
          </div>
        </CardHeader>
      )}

      <CardContent className={hasHeader ? "space-y-4" : undefined}>
        {children}
      </CardContent>
    </Card>
  );
}

function TextAreaField({
  label,
  value,
  placeholder,
  rows = 4,
  onChange,
}: {
  label?: string;
  value: string;
  placeholder?: string;
  rows?: number;
  onChange: (value: string) => void;
}) {
  return (
    <label className="block">
      {label && (
        <div className="mb-2 text-sm font-medium text-foreground">{label}</div>
      )}
      <Textarea
        value={value}
        placeholder={placeholder}
        onChange={(event) => onChange(event.target.value)}
        rows={rows}
        // Override ui/textarea's field-sizing-content so `rows` sets a real height
        // instead of collapsing to min-h-16 when the value is short.
        className="field-sizing-fixed min-h-32 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
      />
    </label>
  );
}

// Radix Select 不允许空字符串 value,用哨兵表示"未选/无"
const NONE_VALUE = "__none__";

function ResourceSelectField({
  label,
  value,
  options,
  noneLabel,
  onChange,
}: {
  label?: string;
  value: string;
  options: { value: string; label: string }[];
  /** 提供则在顶部加一个"无/默认"选项,选中映射为空串 */
  noneLabel?: string;
  onChange: (value: string) => void;
}) {
  return (
    <div className="block">
      {label && (
        <div className="mb-2 text-sm font-medium text-foreground">{label}</div>
      )}

      <Select
        value={value || NONE_VALUE}
        onValueChange={(v) => onChange(v === NONE_VALUE ? "" : v)}
      >
        <SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
          <SelectValue placeholder={label ? `请选择${label}` : "请选择"} />
        </SelectTrigger>

        <SelectContent className="border-hairline bg-popover text-popover-foreground">
          {noneLabel && (
            <SelectItem value={NONE_VALUE}>{noneLabel}</SelectItem>
          )}
          {options.map((item) => (
            <SelectItem key={item.value} value={item.value}>
              {item.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  );
}

function KnowledgeRetrievalConfigDialog({
  disabled,
  value,
  onChange,
}: {
  disabled: boolean;
  value: KnowledgeRetrievalConfig;
  onChange: (config: KnowledgeRetrievalConfig) => void;
}) {
  const [open, setOpen] = useState(false);
  const [draft, setDraft] = useState(value);
  const [error, setError] = useState<string | null>(null);

  function openDialog() {
    setDraft(value);
    setError(null);
    setOpen(true);
  }

  function saveDraft() {
    if (draft.topN === 0 || draft.topN < -1 || !Number.isInteger(draft.topN)) {
      setError("Top N 必须为 -1 或大于 0 的整数");
      return;
    }
    if (draft.scoreThreshold < 0 || draft.scoreThreshold > 1) {
      setError("最低相关度必须在 0 到 1 之间");
      return;
    }
    onChange(draft);
    setOpen(false);
  }

  return (
    <>
      <button
        type="button"
        disabled={disabled}
        onClick={openDialog}
        aria-label="打开知识库高级配置"
        title={
          disabled
            ? "请先选择知识库"
            : `${value.mode === "automatic" ? "自动检索" : "模型主动检索"} · Top N ${value.topN === -1 ? "不限" : value.topN} · 最低相关度 ${value.scoreThreshold}`
        }
        className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
      >
        <Settings2 size={14} />
      </button>

      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent className="sm:max-w-lg">
          <DialogHeader>
            <DialogTitle>知识库高级配置</DialogTitle>
            <DialogDescription>
              设置检索触发方式、返回数量和相关度过滤条件。
            </DialogDescription>
          </DialogHeader>

          <div className="space-y-5 py-2">
            <div className="space-y-2">
              <div className="text-sm font-medium text-foreground">检索方式</div>
              <Select
                value={draft.mode}
                onValueChange={(mode: "automatic" | "on_demand") =>
                  setDraft({ ...draft, mode })
                }
              >
                <SelectTrigger className="w-full border-hairline-strong bg-background">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="automatic">自动检索</SelectItem>
                  <SelectItem value="on_demand">模型主动检索</SelectItem>
                </SelectContent>
              </Select>
              <p className="text-xs text-muted-foreground">
                {draft.mode === "automatic"
                  ? "每轮用户提问后自动检索，响应行为更稳定。"
                  : "由大模型判断是否调用知识库，依赖模型的工具调用能力。"}
              </p>
            </div>

            <label className="block">
              <span className="mb-2 block text-sm font-medium text-foreground">
                最多返回片段数
              </span>
              <Input
                type="number"
                step="1"
                min="-1"
                value={draft.topN}
                onChange={(event) =>
                  setDraft({ ...draft, topN: Number(event.target.value) })
                }
              />
              <span className="mt-1.5 block text-xs text-muted-foreground">
                填写 -1 时保留所有达到阈值的结果。
              </span>
            </label>

            <label className="block">
              <span className="mb-2 block text-sm font-medium text-foreground">
                最低相关度
              </span>
              <Input
                type="number"
                step="0.01"
                min="0"
                max="1"
                value={draft.scoreThreshold}
                onChange={(event) =>
                  setDraft({
                    ...draft,
                    scoreThreshold: Number(event.target.value),
                  })
                }
              />
              <span className="mt-1.5 block text-xs text-muted-foreground">
                仅保留相关度达到该值的片段，范围 0–1。
              </span>
            </label>

            {error && <p className="text-sm text-destructive">{error}</p>}
          </div>

          <DialogFooter>
            <Button type="button" variant="outline" onClick={() => setOpen(false)}>
              取消
            </Button>
            <Button type="button" onClick={saveDraft}>
              保存配置
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
}

function ToolPicker({
  tools,
  selectedIds,
  onChange,
}: {
  tools: Tool[];
  selectedIds: string[];
  onChange: (ids: string[]) => void;
}) {
  const [open, setOpen] = useState(false);
  const [draftIds, setDraftIds] = useState<string[]>(selectedIds);
  const selectedTools = selectedIds
    .map((id) => tools.find((tool) => tool.id === id))
    .filter((tool): tool is Tool => Boolean(tool));

  function openPicker() {
    setDraftIds(selectedIds);
    setOpen(true);
  }

  return (
    <>
      <div className="flex min-h-9 flex-wrap items-center gap-2">
        {selectedTools.map((tool) => (
          <div
            key={tool.id}
            className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
          >
            {tool.type === "end_call" ? <PhoneOff size={14} /> : <Wrench size={14} />}
            <span className="max-w-48 truncate">{tool.name}</span>
            <button
              type="button"
              onClick={() => onChange(selectedIds.filter((id) => id !== tool.id))}
              className="text-muted-soft transition-colors hover:text-foreground"
              aria-label={`移除工具 ${tool.name}`}
            >
              <X size={13} />
            </button>
          </div>
        ))}
        <Button
          type="button"
          variant="outline"
          size="icon-sm"
          className="border-hairline-strong text-muted-foreground hover:text-foreground"
          onClick={openPicker}
          aria-label="添加工具"
          title="添加工具"
        >
          <Plus size={15} />
        </Button>
      </div>

      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent className="sm:max-w-xl">
          <DialogHeader>
            <DialogTitle>选择工具</DialogTitle>
            <DialogDescription>选择已经在工具资源中启用的工具。</DialogDescription>
          </DialogHeader>

          {tools.length === 0 ? (
            <div className="rounded-xl border border-dashed border-hairline-strong px-4 py-10 text-center text-sm text-muted-foreground">
              暂无可用工具
            </div>
          ) : (
            <div className="max-h-80 divide-y divide-hairline overflow-y-auto rounded-xl border border-hairline">
              {tools.map((tool) => {
                const checked = draftIds.includes(tool.id);
                return (
                  <label
                    key={tool.id}
                    className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-surface-strong/40"
                  >
                    <input
                      type="checkbox"
                      checked={checked}
                      onChange={() =>
                        setDraftIds((current) =>
                          checked
                            ? current.filter((id) => id !== tool.id)
                            : [...current, tool.id],
                        )
                      }
                      className="size-4 accent-primary"
                    />
                    <div className="min-w-0 flex-1">
                      <div className="flex items-center gap-2">
                        <span className="truncate font-medium text-foreground">
                          {tool.name}
                        </span>
                        <Badge variant="secondary">
                          {tool.type === "end_call" ? "End Call" : "HTTP"}
                        </Badge>
                      </div>
                      <div className="mt-0.5 truncate font-mono text-xs text-muted-foreground">
                        {tool.functionName}
                      </div>
                    </div>
                  </label>
                );
              })}
            </div>
          )}

          <DialogFooter>
            <Button variant="outline" onClick={() => setOpen(false)}>
              取消
            </Button>
            <Button
              onClick={() => {
                onChange(draftIds);
                setOpen(false);
              }}
            >
              确认
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
}

function ToggleRow({
  icon,
  title,
  description,
  hint,
  checked,
  onChange,
}: {
  icon?: React.ReactNode;
  title: string;
  description?: string;
  hint?: string;
  checked: boolean;
  onChange: (checked: boolean) => void;
}) {
  const hasIcon = Boolean(icon);

  return (
    <div
      className={[
        "flex items-center justify-between border border-hairline bg-canvas-soft",
        hasIcon ? "rounded-2xl p-5" : "rounded-xl p-4",
      ].join(" ")}
    >
      <div>
        <div
          className={[
            "flex items-center font-medium text-foreground",
            hasIcon ? "gap-3" : "gap-1.5",
          ].join(" ")}
        >
          {icon && (
            <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
              {icon}
            </span>
          )}
          <span className="flex items-center gap-1.5">
            <span>{title}</span>
            {hint && <HelpHint text={hint} />}
          </span>
        </div>
        {description && (
          <div className="mt-1 text-sm text-muted-foreground">
            {description}
          </div>
        )}
      </div>
      <Switch checked={checked} onCheckedChange={onChange} />
    </div>
  );
}
