
import React, { useState, useEffect, useMemo, useRef } from 'react';
import { createPortal } from 'react-dom';
import { Plus, Search, Play, Square, Copy, Trash2, Mic, MessageSquare, Save, Video, PhoneOff, Camera, ArrowLeftRight, Send, Phone, Rocket, AlertTriangle, PhoneCall, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Wrench, Globe, Terminal, X, ClipboardCheck, Sparkles, Volume2, Timer, ChevronDown, Database, Server, Zap, ExternalLink, Key, BrainCircuit, Ear, Book, Filter } from 'lucide-react';
import { Button, Input, Badge, Drawer, Dialog, Switch } from '../components/UI';
import { ASRModel, Assistant, AssistantOpenerToolCall, KnowledgeBase, LLMModel, TabValue, Tool, Voice } from '../types';
import { createAssistant, deleteAssistant, fetchASRModels, fetchAssistantOpenerAudioPcmBuffer, fetchAssistants, fetchKnowledgeBases, fetchLLMModels, fetchTools, fetchVoices, generateAssistantOpenerAudio, previewVoice, updateAssistant as updateAssistantApi } from '../services/backendApi';
import { useDebugPrefsStore } from '../stores/debugPrefsStore';

const isOpenAICompatibleVendor = (vendor?: string) => {
  const normalized = String(vendor || '').trim().toLowerCase();
  return (
    normalized === 'siliconflow' ||
    normalized === '硅基流动' ||
    normalized === 'openai compatible' ||
    normalized === 'openai-compatible'
  );
};

const OPENAI_COMPATIBLE_DEFAULT_MODEL = 'FunAudioLLM/CosyVoice2-0.5B';
const OPENAI_COMPATIBLE_KNOWN_VOICES = new Set([
  'alex',
  'anna',
  'bella',
  'benjamin',
  'charles',
  'claire',
  'david',
  'diana',
]);

const normalizeOpenAICompatibleVoiceKey = (voiceValue: string, model?: string) => {
  const raw = String(voiceValue || '').trim();
  const modelName = String(model || '').trim() || OPENAI_COMPATIBLE_DEFAULT_MODEL;
  if (!raw) return `${modelName}:anna`;

  if (raw.includes(':')) {
    const [prefix, ...rest] = raw.split(':');
    const voiceIdRaw = rest.join(':').trim();
    const voiceIdLower = voiceIdRaw.toLowerCase();
    const normalizedVoiceId = OPENAI_COMPATIBLE_KNOWN_VOICES.has(voiceIdLower) ? voiceIdLower : voiceIdRaw;
    return `${(prefix || modelName).trim()}:${normalizedVoiceId}`;
  }

  const rawLower = raw.toLowerCase();
  const normalizedVoiceId = OPENAI_COMPATIBLE_KNOWN_VOICES.has(rawLower) ? rawLower : raw;
  return `${modelName}:${normalizedVoiceId}`;
};

const buildOpenAICompatibleVoiceKey = (voiceId: string, model?: string) => {
  return normalizeOpenAICompatibleVoiceKey(voiceId, model);
};

const resolveRuntimeTtsVoice = (selectedVoiceId: string, voice: Voice) => {
  const explicitKey = String(voice.voiceKey || '').trim();
  if (!isOpenAICompatibleVendor(voice.vendor)) {
    return explicitKey || selectedVoiceId;
  }
  const resolved = normalizeOpenAICompatibleVoiceKey(explicitKey || selectedVoiceId, voice.model);
  if (voice.isSystem) {
    const canonical = normalizeOpenAICompatibleVoiceKey(selectedVoiceId, voice.model);
    if (!explicitKey) return canonical;
    const explicitSuffix = explicitKey.includes(':') ? explicitKey.split(':').pop() : explicitKey;
    if (explicitSuffix && explicitSuffix !== selectedVoiceId) return canonical;
  }
  return resolved;
};

const renderToolIcon = (icon: string) => {
  const className = 'w-4 h-4';
  const map: Record<string, React.ReactNode> = {
    Camera: <Camera className={className} />,
    CameraOff: <CameraOff className={className} />,
    Image: <Image className={className} />,
    Images: <Images className={className} />,
    CloudSun: <CloudSun className={className} />,
    Calendar: <Calendar className={className} />,
    Phone: <Phone className={className} />,
    Volume2: <Volume2 className={className} />,
    TrendingUp: <TrendingUp className={className} />,
    Coins: <Coins className={className} />,
    Terminal: <Terminal className={className} />,
    Globe: <Globe className={className} />,
    Wrench: <Wrench className={className} />,
  };
  return map[icon] || <Wrench className={className} />;
};

const TOOL_ID_ALIASES: Record<string, string> = {
  voice_message_prompt: 'voice_msg_prompt',
};

const normalizeToolId = (raw: unknown): string => {
  const toolId = String(raw || '').trim();
  if (!toolId) return '';
  return TOOL_ID_ALIASES[toolId] || toolId;
};

const OPENER_TOOL_ARGUMENT_TEMPLATES: Record<string, Record<string, any>> = {
  text_msg_prompt: {
    msg: '您好，请先描述您要咨询的问题。',
  },
  voice_msg_prompt: {
    msg: '您好，请先描述您要咨询的问题。',
  },
  text_choice_prompt: {
    question: '请选择需要办理的业务',
    options: [
      { id: 'billing', label: '账单咨询', value: 'billing' },
      { id: 'repair', label: '故障报修', value: 'repair' },
      { id: 'manual', label: '人工客服', value: 'manual' },
    ],
  },
  voice_choice_prompt: {
    question: '请选择需要办理的业务',
    options: [
      { id: 'billing', label: '账单咨询', value: 'billing' },
      { id: 'repair', label: '故障报修', value: 'repair' },
      { id: 'manual', label: '人工客服', value: 'manual' },
    ],
    voice_text: '请从以下选项中选择：账单咨询、故障报修或人工客服。',
  },
};

const normalizeManualOpenerToolCallsForRuntime = (
  calls: AssistantOpenerToolCall[] | undefined,
  options?: { strictJson?: boolean }
): { calls: Array<{ toolName: string; arguments: Record<string, any> }>; error?: string } => {
  const strictJson = options?.strictJson === true;
  const normalized: Array<{ toolName: string; arguments: Record<string, any> }> = [];
  if (!Array.isArray(calls)) return { calls: normalized };

  for (let i = 0; i < calls.length; i += 1) {
    const item = calls[i];
    if (!item || typeof item !== 'object') continue;
    const toolName = normalizeToolId(item.toolName || '');
    if (!toolName) continue;

    const argsRaw = item.arguments;
    let args: Record<string, any> = {};
    if (argsRaw && typeof argsRaw === 'object' && !Array.isArray(argsRaw)) {
      args = argsRaw as Record<string, any>;
    } else if (typeof argsRaw === 'string' && argsRaw.trim()) {
      try {
        const parsed = JSON.parse(argsRaw);
        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
          args = parsed as Record<string, any>;
        } else if (strictJson) {
          return { calls: normalized, error: `Opener tool call #${i + 1} arguments must be a JSON object.` };
        }
      } catch {
        if (strictJson) {
          return { calls: normalized, error: `Opener tool call #${i + 1} has invalid JSON arguments.` };
        }
      }
    }
    normalized.push({ toolName, arguments: args });
  }

  return { calls: normalized.slice(0, 8) };
};

export const AssistantsPage: React.FC = () => {
  const [assistants, setAssistants] = useState<Assistant[]>([]);
  const [voices, setVoices] = useState<Voice[]>([]);
  const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
  const [llmModels, setLlmModels] = useState<LLMModel[]>([]);
  const [asrModels, setAsrModels] = useState<ASRModel[]>([]);
  const [tools, setTools] = useState<Tool[]>([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [activeTab, setActiveTab] = useState<TabValue>(TabValue.GLOBAL);
  const [debugOpen, setDebugOpen] = useState(false);
  const [hotwordInput, setHotwordInput] = useState('');
  const [toolPickerOpen, setToolPickerOpen] = useState(false);
  
  // Publish Modal State
  const [isPublishModalOpen, setIsPublishModalOpen] = useState(false);
  const [publishTab, setPublishTab] = useState<'web' | 'api'>('web');
  
  const [deleteId, setDeleteId] = useState<string | null>(null);
  const [copySuccess, setCopySuccess] = useState(false);
  const [saveLoading, setSaveLoading] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const [templateSuggestion, setTemplateSuggestion] = useState<TemplateSuggestionState | null>(null);
  const [persistedAssistantSnapshotById, setPersistedAssistantSnapshotById] = useState<Record<string, string>>({});
  const [unsavedDebugConfirmOpen, setUnsavedDebugConfirmOpen] = useState(false);
  const [openerAudioGenerating, setOpenerAudioGenerating] = useState(false);
  const [openerAudioPreviewing, setOpenerAudioPreviewing] = useState(false);
  const openerPreviewAudioCtxRef = useRef<AudioContext | null>(null);
  const openerPreviewSourceRef = useRef<AudioBufferSourceNode | null>(null);

  const selectedAssistant = assistants.find(a => a.id === selectedId) || null;
  const serializeAssistant = (assistant: Assistant) => JSON.stringify(assistant);
  const selectedAssistantHasUnsavedChanges = Boolean(
    selectedAssistant
    && persistedAssistantSnapshotById[selectedAssistant.id] !== serializeAssistant(selectedAssistant)
  );

  const filteredAssistants = assistants.filter(a => 
    a.name.toLowerCase().includes(searchTerm.toLowerCase())
  );

  useEffect(() => {
    setTemplateSuggestion(null);
  }, [selectedId, activeTab]);

  useEffect(() => {
    const loadInitialData = async () => {
      setIsLoading(true);
      try {
        const [assistantList, voiceList, kbList, llmList, asrList, toolList] = await Promise.all([
          fetchAssistants(),
          fetchVoices(),
          fetchKnowledgeBases(),
          fetchLLMModels(),
          fetchASRModels(),
          fetchTools(),
        ]);
        setAssistants(assistantList);
        setVoices(voiceList);
        setKnowledgeBases(kbList);
        setLlmModels(llmList);
        setAsrModels(asrList);
        setTools(toolList);
        setPersistedAssistantSnapshotById(
          assistantList.reduce<Record<string, string>>((acc, item) => {
            acc[item.id] = serializeAssistant(item);
            return acc;
          }, {})
        );
        if (assistantList.length > 0) {
          setSelectedId(assistantList[0].id);
        }
      } catch (error) {
        console.error(error);
        alert('加载助手数据失败，请检查后端服务是否启动。');
      } finally {
        setIsLoading(false);
      }
    };

    loadInitialData();
  }, []);

  const handleCreate = async () => {
    const newAssistantPayload: Partial<Assistant> = {
      name: 'New Assistant',
      firstTurnMode: 'bot_first',
      opener: '',
      manualOpenerToolCalls: [],
      generatedOpenerEnabled: false,
      openerAudioEnabled: false,
      prompt: '',
      knowledgeBaseId: '',
      language: 'zh',
      voiceOutputEnabled: true,
      voice: voices[0]?.id || '',
      speed: 1,
      hotwords: [],
      tools: [],
      botCannotBeInterrupted: false,
      interruptionSensitivity: 180,
      configMode: 'platform',
    };
    try {
      const created = await createAssistant(newAssistantPayload);
      setAssistants((prev) => [created, ...prev]);
      setPersistedAssistantSnapshotById((prev) => ({ ...prev, [created.id]: serializeAssistant(created) }));
      setSelectedId(created.id);
      setActiveTab(TabValue.GLOBAL);
    } catch (error) {
      console.error(error);
      alert('创建助手失败。');
    }
  };

  const handleSave = async () => {
    if (!selectedAssistant) return;
    const normalizedManualCalls = normalizeManualOpenerToolCallsForRuntime(selectedAssistant.manualOpenerToolCalls, { strictJson: true });
    if (normalizedManualCalls.error) {
      alert(normalizedManualCalls.error);
      return;
    }
    setSaveLoading(true);
    try {
      const updated = await updateAssistantApi(selectedAssistant.id, {
        ...selectedAssistant,
        manualOpenerToolCalls: normalizedManualCalls.calls,
      });
      setAssistants((prev) => prev.map((item) => (item.id === updated.id ? { ...item, ...updated } : item)));
      setPersistedAssistantSnapshotById((prev) => ({ ...prev, [updated.id]: serializeAssistant(updated) }));
    } catch (error) {
      console.error(error);
      alert('保存失败，请稍后重试。');
    } finally {
      setSaveLoading(false);
    }
  };

  const handleCopyId = (id: string, text?: string) => {
    navigator.clipboard.writeText(text || id);
    setCopySuccess(true);
    setTimeout(() => setCopySuccess(false), 2000);
  };

  const handleCopy = async (e: React.MouseEvent, assistant: Assistant) => {
    e.stopPropagation();
    try {
      const copied = await createAssistant({
        ...assistant,
        name: `${assistant.name} (Copy)`,
      });
      setAssistants((prev) => [copied, ...prev]);
    } catch (error) {
      console.error(error);
      alert('复制助手失败。');
    }
  };

  const handleDeleteClick = (e: React.MouseEvent, id: string) => {
    e.stopPropagation();
    setDeleteId(id);
  };

  const confirmDelete = async () => {
    if (deleteId) {
        try {
          await deleteAssistant(deleteId);
          setAssistants(prev => prev.filter(a => a.id !== deleteId));
          setPersistedAssistantSnapshotById((prev) => {
            const next = { ...prev };
            delete next[deleteId];
            return next;
          });
          if (selectedId === deleteId) setSelectedId(null);
          setDeleteId(null);
        } catch (error) {
          console.error(error);
          alert('删除失败，请稍后重试。');
        }
    }
  };

  const updateAssistant = (field: keyof Assistant, value: any) => {
    if (!selectedId) return;
    setAssistants(prev => prev.map(a => a.id === selectedId ? { ...a, [field]: value } : a));
    
    if (field === 'configMode') {
      if (value === 'platform') {
        setActiveTab(TabValue.GLOBAL);
      } else if (value === 'dify' || value === 'fastgpt') {
        setActiveTab(TabValue.LINK);
      }
    }
  };

  const updateTemplateSuggestionState = (
    field: 'prompt' | 'opener',
    value: string,
    caret: number | null,
    control?: HTMLTextAreaElement | HTMLInputElement | null
  ) => {
    if (caret === null || caret < 0) {
      setTemplateSuggestion(null);
      return;
    }
    const probe = value.slice(0, caret);
    const start = probe.lastIndexOf('{{');
    if (start < 0) {
      setTemplateSuggestion(null);
      return;
    }
    const token = value.slice(start + 2, caret);
    if (token.includes('}')) {
      setTemplateSuggestion(null);
      return;
    }
    if (!/^[a-zA-Z0-9_]*$/.test(token)) {
      setTemplateSuggestion(null);
      return;
    }
    if (!control) {
      setTemplateSuggestion(null);
      return;
    }
    const anchor = getTemplateSuggestionAnchor(control, caret);
    setTemplateSuggestion({
      field,
      start,
      end: caret,
      query: token,
      anchorLeft: anchor.left,
      anchorTop: anchor.top,
    });
  };

  const applySystemTemplateVariable = (field: 'prompt' | 'opener', key: string) => {
    if (!selectedAssistant || !templateSuggestion || templateSuggestion.field !== field) {
      return;
    }
    const current = String(selectedAssistant[field] || '');
    const before = current.slice(0, templateSuggestion.start);
    const after = current.slice(templateSuggestion.end);
    const nextValue = `${before}{{${key}}}${after}`;
    updateAssistant(field, nextValue);
    setTemplateSuggestion(null);
  };

  const filteredSystemTemplateVariables = useMemo(() => {
    if (!templateSuggestion) return [];
    const query = templateSuggestion.query.trim().toLowerCase();
    if (!query) return SYSTEM_DYNAMIC_VARIABLE_OPTIONS;
    return SYSTEM_DYNAMIC_VARIABLE_OPTIONS.filter((item) => item.key.toLowerCase().includes(query));
  }, [templateSuggestion]);

  const handleOpenDebug = () => {
    if (!selectedAssistant) return;
    if (selectedAssistantHasUnsavedChanges) {
      setUnsavedDebugConfirmOpen(true);
      return;
    }
    setDebugOpen(true);
  };

  const handleGenerateOpenerAudio = async () => {
    if (!selectedAssistant) return;
    setOpenerAudioGenerating(true);
    try {
      const status = await generateAssistantOpenerAudio(selectedAssistant.id, {
        text: selectedAssistant.opener || '',
      });
      setAssistants((prev) => prev.map((item) => {
        if (item.id !== selectedAssistant.id) return item;
        return {
          ...item,
          openerAudioEnabled: status.enabled,
          openerAudioReady: status.ready,
          openerAudioDurationMs: status.duration_ms,
          openerAudioUpdatedAt: status.updated_at || '',
        };
      }));
    } catch (error) {
      console.error(error);
      alert((error as Error)?.message || '生成开场白预加载音频失败');
    } finally {
      setOpenerAudioGenerating(false);
    }
  };

  const stopOpenerAudioPreview = () => {
    if (openerPreviewSourceRef.current) {
      try {
        openerPreviewSourceRef.current.stop();
      } catch {
        // no-op
      }
      try {
        openerPreviewSourceRef.current.disconnect();
      } catch {
        // no-op
      }
      openerPreviewSourceRef.current = null;
    }
    setOpenerAudioPreviewing(false);
  };

  const handlePreviewOpenerAudio = async () => {
    if (!selectedAssistant?.id || !selectedAssistant.openerAudioReady) return;
    try {
      stopOpenerAudioPreview();
      const pcmBuffer = await fetchAssistantOpenerAudioPcmBuffer(selectedAssistant.id);
      const int16 = new Int16Array(pcmBuffer);
      if (int16.length === 0) return;

      let ctx = openerPreviewAudioCtxRef.current;
      if (!ctx) {
        ctx = new AudioContext();
        openerPreviewAudioCtxRef.current = ctx;
      }
      if (ctx.state === 'suspended') {
        await ctx.resume();
      }

      const float32 = new Float32Array(int16.length);
      for (let i = 0; i < int16.length; i += 1) {
        float32[i] = int16[i] / 32768;
      }
      const audioBuffer = ctx.createBuffer(1, float32.length, 16000);
      audioBuffer.copyToChannel(float32, 0);

      const source = ctx.createBufferSource();
      source.buffer = audioBuffer;
      source.connect(ctx.destination);
      source.onended = () => {
        if (openerPreviewSourceRef.current === source) {
          openerPreviewSourceRef.current = null;
          setOpenerAudioPreviewing(false);
        }
        try {
          source.disconnect();
        } catch {
          // no-op
        }
      };
      openerPreviewSourceRef.current = source;
      setOpenerAudioPreviewing(true);
      source.start();
    } catch (error) {
      console.error(error);
      setOpenerAudioPreviewing(false);
      alert((error as Error)?.message || '预览预加载开场音频失败');
    }
  };

  const handleConfirmOpenDebug = () => {
    setUnsavedDebugConfirmOpen(false);
    setDebugOpen(true);
  };

  const toggleTool = (toolId: string) => {
    if (!selectedAssistant) return;
    const canonicalToolId = normalizeToolId(toolId);
    const currentTools = (selectedAssistant.tools || []).map((id) => normalizeToolId(id));
    const newTools = currentTools.includes(canonicalToolId)
        ? currentTools.filter(id => id !== canonicalToolId)
        : [...currentTools, canonicalToolId];
    updateAssistant('tools', newTools);
  };

  const removeImportedTool = (e: React.MouseEvent, tool: Tool) => {
    e.stopPropagation();
    if (!selectedAssistant) return;
    const canonicalToolId = normalizeToolId(tool.id);
    updateAssistant('tools', (selectedAssistant.tools || []).filter((id) => normalizeToolId(id) !== canonicalToolId));
  };

  const addHotword = () => {
    if (hotwordInput.trim() && selectedAssistant) {
      updateAssistant('hotwords', [...selectedAssistant.hotwords, hotwordInput.trim()]);
      setHotwordInput('');
    }
  };

  const removeHotword = (word: string) => {
    if (selectedAssistant) {
      updateAssistant('hotwords', selectedAssistant.hotwords.filter(w => w !== word));
    }
  };

  const addManualOpenerToolCall = () => {
    if (!selectedAssistant) return;
    const current = selectedAssistant.manualOpenerToolCalls || [];
    if (current.length >= 8) return;
    const fallbackTool = normalizeToolId(
      (selectedAssistant.tools || []).find((id) =>
        tools.some((tool) => normalizeToolId(tool.id) === normalizeToolId(id) && tool.enabled !== false)
      ) || ''
    );
    updateAssistant('manualOpenerToolCalls', [
      ...current,
      {
        toolName: fallbackTool,
        arguments: '{}',
      },
    ]);
  };

  const updateManualOpenerToolCall = (index: number, patch: Partial<AssistantOpenerToolCall>) => {
    if (!selectedAssistant) return;
    const current = selectedAssistant.manualOpenerToolCalls || [];
    if (index < 0 || index >= current.length) return;
    const next = [...current];
    const normalizedPatch = { ...patch };
    if (Object.prototype.hasOwnProperty.call(normalizedPatch, 'toolName')) {
      normalizedPatch.toolName = normalizeToolId(normalizedPatch.toolName || '');
    }
    next[index] = { ...next[index], ...normalizedPatch };
    updateAssistant('manualOpenerToolCalls', next);
  };

  const removeManualOpenerToolCall = (index: number) => {
    if (!selectedAssistant) return;
    const current = selectedAssistant.manualOpenerToolCalls || [];
    updateAssistant('manualOpenerToolCalls', current.filter((_, idx) => idx !== index));
  };

  const applyManualOpenerToolTemplate = (index: number) => {
    if (!selectedAssistant) return;
    const current = selectedAssistant.manualOpenerToolCalls || [];
    if (index < 0 || index >= current.length) return;
    const toolName = normalizeToolId(current[index]?.toolName || '');
    const template = OPENER_TOOL_ARGUMENT_TEMPLATES[toolName];
    if (!template) return;
    updateManualOpenerToolCall(index, {
      arguments: JSON.stringify(template, null, 2),
    });
  };

  const systemTools = tools.filter((t) => t.enabled !== false && t.category === 'system');
  const queryTools = tools.filter((t) => t.enabled !== false && t.category === 'query');
  const selectedToolIds = (selectedAssistant?.tools || []).map((id) => normalizeToolId(id));
  const activeSystemTools = systemTools.filter((tool) => selectedToolIds.includes(normalizeToolId(tool.id)));
  const activeQueryTools = queryTools.filter((tool) => selectedToolIds.includes(normalizeToolId(tool.id)));
  const availableSystemTools = systemTools.filter((tool) => !selectedToolIds.includes(normalizeToolId(tool.id)));
  const availableQueryTools = queryTools.filter((tool) => !selectedToolIds.includes(normalizeToolId(tool.id)));
  const openerToolOptions = Array.from(
    new Map(
      tools
        .filter(
          (tool) =>
            tool.enabled !== false &&
            selectedToolIds.some((selectedId) => normalizeToolId(selectedId) === normalizeToolId(tool.id))
        )
        .map((tool) => {
          const toolId = normalizeToolId(tool.id);
          return [toolId, { id: toolId, label: `${tool.name} (${toolId})` }];
        })
    ).values()
  );

  const isExternalConfig = selectedAssistant?.configMode === 'dify' || selectedAssistant?.configMode === 'fastgpt';
  const isNoneConfig = selectedAssistant?.configMode === 'none' || !selectedAssistant?.configMode;
  const canAdjustInterruptionSensitivity = selectedAssistant?.botCannotBeInterrupted !== true;
  const isBotFirstTurn = selectedAssistant?.firstTurnMode !== 'user_first';

  useEffect(() => {
    return () => {
      if (openerPreviewSourceRef.current) {
        try {
          openerPreviewSourceRef.current.stop();
        } catch {
          // no-op
        }
        openerPreviewSourceRef.current = null;
      }
      if (openerPreviewAudioCtxRef.current) {
        void openerPreviewAudioCtxRef.current.close();
        openerPreviewAudioCtxRef.current = null;
      }
    };
  }, []);

  return (
    <div className="flex h-full min-h-0 gap-6 animate-in fade-in">
      {/* LEFT COLUMN: List */}
      <div className="w-80 flex flex-col gap-4 shrink-0">
        <div className="flex items-center justify-between px-1 text-white">
            <h2 className="text-xl font-bold tracking-tight text-white">小助手列表</h2>
        </div>
        
        <div className="flex gap-2">
            <div className="relative flex-1">
                <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
                <Input 
                  placeholder="搜索..." 
                  className="pl-9 bg-card/50 border-white/5"
                  value={searchTerm}
                  onChange={(e) => setSearchTerm(e.target.value)}
                />
            </div>
            <Button size="icon" onClick={handleCreate} title="新建小助手">
                <Plus className="h-5 w-5" />
            </Button>
        </div>

        <div className="flex-1 overflow-y-auto space-y-2 pr-1 custom-scrollbar">
            {!isLoading && filteredAssistants.map(assistant => (
                <div 
                    key={assistant.id}
                    onClick={() => setSelectedId(assistant.id)}
                    className={`group relative flex flex-col p-4 rounded-xl border transition-all cursor-pointer ${
                        selectedId === assistant.id 
                        ? 'bg-primary/10 border-primary/40 shadow-[0_0_15px_rgba(6,182,212,0.15)]' 
                        : 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10'
                    }`}
                >
                    <div className="flex flex-col gap-1.5 mb-2 pr-16 overflow-hidden">
                        <span className={`font-semibold truncate ${selectedId === assistant.id ? 'text-primary' : 'text-foreground'}`}>
                            {assistant.name}
                        </span>
                        {assistant.configMode && assistant.configMode !== 'none' && (
                          <div className="flex">
                            <Badge 
                              variant="outline" 
                              className={`text-[9px] uppercase tracking-tighter shrink-0 opacity-70 border-white/10 ${
                                assistant.configMode === 'platform' ? 'text-cyan-400 bg-cyan-400/5' : 
                                assistant.configMode === 'dify' ? 'text-blue-400 bg-blue-400/5' : 
                                'text-purple-400 bg-purple-400/5'
                              }`}
                            >
                              {assistant.configMode === 'platform' ? '内置' : assistant.configMode}
                            </Badge>
                          </div>
                        )}
                    </div>
                    
                    <div className="flex items-center text-xs text-muted-foreground">
                        <Phone className="h-3 w-3 mr-1.5 opacity-70" />
                        <span>{assistant.callCount} 次通话</span>
                    </div>

                    <div className="absolute right-2 top-2 flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity bg-background/50 backdrop-blur-sm rounded-lg p-0.5 shadow-lg border border-white/5">
                        <Button variant="ghost" size="icon" className="h-7 w-7" onClick={(e) => handleCopy(e, assistant)} title="复制">
                            <Copy className="h-3.5 w-3.5" />
                        </Button>
                        <Button variant="ghost" size="icon" className="h-7 w-7 text-destructive hover:text-destructive" onClick={(e) => handleDeleteClick(e, assistant.id)} title="删除">
                            <Trash2 className="h-3.5 w-3.5" />
                        </Button>
                    </div>
                </div>
            ))}
            {!isLoading && filteredAssistants.length === 0 && (
                <div className="text-center py-10 text-muted-foreground text-sm">
                    未找到小助手
                </div>
            )}
            {isLoading && (
                <div className="text-center py-10 text-muted-foreground text-sm">
                    加载中...
                </div>
            )}
        </div>
      </div>

      {/* RIGHT COLUMN: Config Panel */}
      <div className="flex-1 bg-card/20 backdrop-blur-sm border border-white/5 rounded-2xl overflow-hidden flex flex-col relative shadow-xl">
        {selectedAssistant ? (
            <>
                {/* Header Area */}
                <div className="p-6 border-b border-white/5 bg-white/[0.02] space-y-5">
                    <div className="flex items-start justify-between gap-4">
                        <div className="flex-1">
                            <div className="flex items-center gap-2 mb-2">
                                <label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase ml-1">小助手名称</label>
                                <div className="flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-white/5 border border-white/10 group/id transition-all hover:bg-white/10">
                                    <span className="text-[10px] font-mono text-muted-foreground/60 select-all tracking-tight">UUID: {selectedAssistant.id}</span>
                                    <button 
                                        onClick={() => handleCopyId(selectedAssistant.id)}
                                        className="text-muted-foreground hover:text-primary transition-colors flex items-center"
                                        title="复制ID"
                                    >
                                        {copySuccess ? <ClipboardCheck className="h-3 w-3 text-green-400" /> : <Copy className="h-3 w-3" />}
                                    </button>
                                </div>
                            </div>
                            <Input 
                                value={selectedAssistant.name}
                                onChange={(e) => updateAssistant('name', e.target.value)}
                                className="font-bold bg-white/5 border-white/10 focus:border-primary/50 text-base"
                            />
                        </div>
                        <div className="flex items-center space-x-2 pt-6">
                             <Button 
                                variant="secondary" 
                                onClick={handleOpenDebug} 
                                className="border border-primary/20 hover:border-primary/50 text-primary hover:text-primary hover:bg-primary/10 shadow-[0_0_15px_rgba(6,182,212,0.1)]"
                             >
                                <Play className="mr-2 h-4 w-4" /> 调试
                            </Button>
                            <Button 
                                variant="outline" 
                                onClick={handleSave} 
                                disabled={saveLoading}
                                className="border border-white/10 hover:border-primary/50 text-foreground"
                             >
                                <Save className={`mr-2 h-4 w-4 ${saveLoading ? 'animate-pulse' : ''}`} /> {saveLoading ? '正在保存...' : '保存'}
                            </Button>
                            <Button 
                                onClick={() => setIsPublishModalOpen(true)} 
                                className="shadow-[0_0_20px_rgba(6,182,212,0.3)]"
                            >
                                <Rocket className="mr-2 h-4 w-4" /> 发布
                            </Button>
                        </div>
                    </div>

                    <div className="space-y-2">
                        <label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase ml-1">配置方式</label>
                        <div className="relative group w-full">
                          <select 
                            className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-foreground appearance-none cursor-pointer [&>option]:bg-card"
                            value={selectedAssistant.configMode || 'none'}
                            onChange={(e) => updateAssistant('configMode', e.target.value as any)}
                          >
                            <option value="none">无</option>
                            <option value="platform">平台配置</option>
                            <option value="dify">Dify 接入</option>
                            <option value="fastgpt">FastGPT 接入</option>
                          </select>
                          <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none group-hover:text-primary transition-colors" />
                        </div>
                    </div>

                    {!isNoneConfig && (
                      <div className="flex bg-white/5 p-1 rounded-lg w-fit border border-white/5 animate-in fade-in slide-in-from-top-1">
                          {selectedAssistant.configMode === 'platform' ? (
                            <>
                              <button
                                  onClick={() => setActiveTab(TabValue.GLOBAL)}
                                  className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.GLOBAL ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
                              >
                                  全局配置
                              </button>
                              <button
                                  onClick={() => setActiveTab(TabValue.VOICE)}
                                  className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.VOICE ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
                              >
                                  语音配置
                              </button>
                              <button
                                  onClick={() => setActiveTab(TabValue.TOOLS)}
                                  className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.TOOLS ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
                              >
                                  工具配置
                              </button>
                              <button
                                  onClick={() => setActiveTab(TabValue.KNOWLEDGE)}
                                  className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.KNOWLEDGE ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
                              >
                                  知识库配置
                              </button>
                            </>
                          ) : (
                            <>
                              <button
                                  onClick={() => setActiveTab(TabValue.LINK)}
                                  className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.LINK ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
                              >
                                  链接设置
                              </button>
                              <button
                                  onClick={() => setActiveTab(TabValue.VOICE)}
                                  className={`px-6 py-1.5 text-sm font-medium rounded-md transition-all ${activeTab === TabValue.VOICE ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-white/5'}`}
                              >
                                  语音设置
                              </button>
                            </>
                          )}
                      </div>
                    )}
                </div>

                <div className="flex-1 overflow-y-auto p-8 custom-scrollbar">
                    {isNoneConfig ? (
                      <div className="h-full flex flex-col items-center justify-center text-muted-foreground opacity-40 animate-in fade-in">
                          <AlertTriangle className="w-12 h-12 mb-4" />
                          <p className="text-sm font-medium">请先选择配置方式以展开详细设置</p>
                      </div>
                    ) : (
                      <div className="max-w-4xl mx-auto space-y-8 animate-in slide-in-from-bottom-2 duration-300">
                        {activeTab === TabValue.LINK && isExternalConfig && (
                           <div className="space-y-6 animate-in fade-in">
                              <div className="p-4 rounded-xl bg-primary/5 border border-primary/20 flex items-start gap-3 mb-4">
                                <Database className="w-5 h-5 text-primary shrink-0 mt-0.5" />
                                <div>
                                  <h4 className="text-sm font-bold text-white mb-1">
                                    接入 {selectedAssistant.configMode === 'dify' ? 'Dify' : 'FastGPT'} 引擎
                                  </h4>
                                  <p className="text-xs text-muted-foreground leading-relaxed">
                                    配置后，视频通话过程中的对话逻辑、知识库检索以及工作流将由外部引擎托管。
                                  </p>
                                </div>
                              </div>

                              <div className="space-y-2">
                                  <label className="text-sm font-medium text-white flex items-center">
                                      <Globe className="w-4 h-4 mr-2 text-primary"/> 接口地址 (API URL)
                                  </label>
                                  <Input 
                                      value={selectedAssistant.apiUrl || ''}
                                      onChange={(e) => updateAssistant('apiUrl', e.target.value)}
                                      placeholder={selectedAssistant.configMode === 'dify' ? "https://api.dify.ai/v1" : "https://api.fastgpt.in/api/v1"}
                                      className="bg-white/5 border-white/10 focus:border-primary/50 font-mono text-xs"
                                  />
                              </div>

                              <div className="space-y-2">
                                  <label className="text-sm font-medium text-white flex items-center">
                                      <Terminal className="w-4 h-4 mr-2 text-primary"/> 密钥 (API KEY)
                                  </label>
                                  <Input 
                                      type="password"
                                      value={selectedAssistant.apiKey || ''}
                                      onChange={(e) => updateAssistant('apiKey', e.target.value)}
                                      placeholder="请输入应用 API 密钥..."
                                      className="bg-white/5 border-white/10 focus:border-primary/50 font-mono text-xs"
                                  />
                              </div>
                           </div>
                        )}

                        {activeTab === TabValue.GLOBAL && selectedAssistant.configMode === 'platform' && (
                            <div className="space-y-6">
                                <div className="space-y-2">
                                    <label className="text-sm font-medium text-white flex items-center">
                                        <BrainCircuit className="w-4 h-4 mr-2 text-primary"/> 大模型 (LLM Model)
                                    </label>
                                    <div className="relative group">
                                        <select
                                            className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground appearance-none cursor-pointer"
                                            value={selectedAssistant.llmModelId || ''}
                                            onChange={(e) => updateAssistant('llmModelId', e.target.value)}
                                        >
                                            <option value="">使用系统默认模型</option>
                                            {llmModels.filter(m => m.type === 'text').map(model => (
                                                <option key={model.id} value={model.id}>{model.name} ({model.vendor})</option>
                                            ))}
                                        </select>
                                        <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none group-hover:text-primary transition-colors" />
                                    </div>
                                    <p className="text-xs text-muted-foreground">选择用于驱动该助手对话的大语言模型。</p>
                                </div>

                                <div className="space-y-2">
                                    <label className="text-sm font-medium text-white flex items-center">
                                        <BotIcon className="w-4 h-4 mr-2 text-primary"/> 提示词 (Prompt)
                                    </label>
                                    <div className="relative">
                                      <textarea 
                                          className="flex min-h-[200px] w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 resize-y text-white"
                                          value={selectedAssistant.prompt}
                                          onChange={(e) => {
                                            const next = e.target.value;
                                            updateAssistant('prompt', next);
                                            updateTemplateSuggestionState('prompt', next, e.currentTarget.selectionStart, e.currentTarget);
                                          }}
                                          onKeyUp={(e) => {
                                            updateTemplateSuggestionState('prompt', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
                                          }}
                                          onClick={(e) => {
                                            updateTemplateSuggestionState('prompt', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
                                          }}
                                          onFocus={(e) => {
                                            updateTemplateSuggestionState('prompt', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
                                          }}
                                          onBlur={() => {
                                            window.setTimeout(() => {
                                              setTemplateSuggestion((prev) => (prev?.field === 'prompt' ? null : prev));
                                            }, 120);
                                          }}
                                          placeholder="设定小助手的人设、语气、行为规范以及业务逻辑..."
                                      />
                                      {templateSuggestion?.field === 'prompt' &&
                                        filteredSystemTemplateVariables.length > 0 &&
                                        typeof document !== 'undefined' &&
                                        createPortal(
                                          <div
                                            className="fixed z-[100] w-[320px] max-w-[calc(100vw-1rem)] rounded-md border border-white/15 bg-black/95 shadow-xl backdrop-blur-md max-h-48 overflow-auto"
                                            style={{
                                              left: templateSuggestion.anchorLeft,
                                              top: templateSuggestion.anchorTop,
                                            }}
                                          >
                                            {filteredSystemTemplateVariables.map((item) => (
                                              <button
                                                key={item.key}
                                                type="button"
                                                className="w-full text-left px-3 py-2 hover:bg-white/10 transition-colors"
                                                onMouseDown={(e) => {
                                                  e.preventDefault();
                                                  applySystemTemplateVariable('prompt', item.key);
                                                }}
                                              >
                                                <div className="text-xs text-cyan-100">{`{{${item.key}}}`}</div>
                                                <div className="text-[10px] text-muted-foreground mt-0.5">{item.description}</div>
                                              </button>
                                            ))}
                                          </div>,
                                          document.body
                                        )}
                                    </div>
                                </div>

                                <div className="space-y-2">
                                    <div className="flex items-center justify-between gap-3">
                                        <label className="text-sm font-medium text-white flex items-center">
                                            <PhoneCall className="w-4 h-4 mr-2 text-primary"/> 首轮发言方
                                        </label>
                                        <div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
                                            <button
                                              type="button"
                                              onClick={() => updateAssistant('firstTurnMode', 'bot_first')}
                                              className={`px-3 py-1 text-xs rounded-md transition-colors ${
                                                isBotFirstTurn
                                                  ? 'bg-primary text-primary-foreground shadow-sm'
                                                  : 'text-muted-foreground hover:text-foreground'
                                              }`}
                                            >
                                              机器人先说
                                            </button>
                                            <button
                                              type="button"
                                              onClick={() => updateAssistant('firstTurnMode', 'user_first')}
                                              className={`px-3 py-1 text-xs rounded-md transition-colors ${
                                                isBotFirstTurn
                                                  ? 'text-muted-foreground hover:text-foreground'
                                                  : 'bg-primary text-primary-foreground shadow-sm'
                                              }`}
                                            >
                                              用户先说
                                            </button>
                                        </div>
                                    </div>
                                    <p className="text-xs text-muted-foreground">
                                      {isBotFirstTurn ? '决定通话接通后由谁先开始第一句对话。' : '当前为“用户先说”，已隐藏开场白设置。'}
                                    </p>
                                </div>

                                {isBotFirstTurn && (
                                <div className="space-y-2">
                                    <div className="flex items-center justify-between gap-3">
                                        <label className="text-sm font-medium flex items-center text-white">
                                            <MessageSquare className="w-4 h-4 mr-2 text-primary"/> 开场白
                                        </label>
                                        <div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
                                            <button
                                              type="button"
                                              onClick={() => updateAssistant('generatedOpenerEnabled', false)}
                                              className={`px-3 py-1 text-xs rounded-md transition-colors ${
                                                selectedAssistant.generatedOpenerEnabled === true
                                                  ? 'text-muted-foreground hover:text-foreground'
                                                  : 'bg-primary text-primary-foreground shadow-sm'
                                              }`}
                                            >
                                              手动输入
                                            </button>
                                            <button
                                              type="button"
                                              onClick={() => updateAssistant('generatedOpenerEnabled', true)}
                                              className={`px-3 py-1 text-xs rounded-md transition-colors ${
                                                selectedAssistant.generatedOpenerEnabled === true
                                                  ? 'bg-primary text-primary-foreground shadow-sm'
                                                  : 'text-muted-foreground hover:text-foreground'
                                              }`}
                                            >
                                              自动生成
                                            </button>
                                        </div>
                                    </div>
                                    {selectedAssistant.generatedOpenerEnabled === true ? (
                                      <div className="rounded-md border border-dashed border-white/15 bg-white/5 px-3 py-2 text-xs text-muted-foreground">
                                        自动生成模式下不使用固定开场白文本，仅依据系统提示词生成首句。
                                      </div>
                                    ) : (
                                      <div className="relative">
                                        <Input
                                          value={selectedAssistant.opener}
                                          onChange={(e) => {
                                            const next = e.target.value;
                                            updateAssistant('opener', next);
                                            updateTemplateSuggestionState('opener', next, e.currentTarget.selectionStart, e.currentTarget);
                                          }}
                                          onKeyUp={(e) => {
                                            updateTemplateSuggestionState('opener', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
                                          }}
                                          onClick={(e) => {
                                            updateTemplateSuggestionState('opener', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
                                          }}
                                          onFocus={(e) => {
                                            updateTemplateSuggestionState('opener', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
                                          }}
                                          onBlur={() => {
                                            window.setTimeout(() => {
                                              setTemplateSuggestion((prev) => (prev?.field === 'opener' ? null : prev));
                                            }, 120);
                                          }}
                                          placeholder="例如：您好，我是您的专属AI助手..."
                                          className="bg-white/5 border-white/10 focus:border-primary/50"
                                        />
                                        {templateSuggestion?.field === 'opener' &&
                                          filteredSystemTemplateVariables.length > 0 &&
                                          typeof document !== 'undefined' &&
                                          createPortal(
                                            <div
                                              className="fixed z-[100] w-[320px] max-w-[calc(100vw-1rem)] rounded-md border border-white/15 bg-black/95 shadow-xl backdrop-blur-md max-h-48 overflow-auto"
                                              style={{
                                                left: templateSuggestion.anchorLeft,
                                                top: templateSuggestion.anchorTop,
                                              }}
                                            >
                                              {filteredSystemTemplateVariables.map((item) => (
                                                <button
                                                  key={item.key}
                                                  type="button"
                                                  className="w-full text-left px-3 py-2 hover:bg-white/10 transition-colors"
                                                  onMouseDown={(e) => {
                                                    e.preventDefault();
                                                    applySystemTemplateVariable('opener', item.key);
                                                  }}
                                                >
                                                  <div className="text-xs text-cyan-100">{`{{${item.key}}}`}</div>
                                                  <div className="text-[10px] text-muted-foreground mt-0.5">{item.description}</div>
                                                </button>
                                              ))}
                                            </div>,
                                            document.body
                                          )}
                                      </div>
                                    )}
                                    {selectedAssistant.generatedOpenerEnabled !== true && (
                                      <div className="mt-3 p-3 rounded-lg border border-white/10 bg-white/[0.03] space-y-3">
                                        <div className="flex items-center justify-between gap-3">
                                          <label className="text-xs font-semibold text-white flex items-center">
                                            <Wrench className="w-4 h-4 mr-2 text-primary" />
                                            开场工具调用（手动模式）
                                          </label>
                                          <Button
                                            type="button"
                                            variant="secondary"
                                            size="sm"
                                            onClick={addManualOpenerToolCall}
                                            disabled={(selectedAssistant.manualOpenerToolCalls || []).length >= 8}
                                          >
                                            <Plus className="w-3.5 h-3.5 mr-1.5" />
                                            添加
                                          </Button>
                                        </div>
                                        {(selectedAssistant.manualOpenerToolCalls || []).length === 0 ? (
                                          <div className="text-[11px] text-muted-foreground border border-dashed border-white/10 rounded-md px-2 py-2">
                                            未配置。可添加 text_msg_prompt / voice_msg_prompt 等工具作为开场动作。
                                          </div>
                                        ) : (
                                          <div className="space-y-2">
                                            {(selectedAssistant.manualOpenerToolCalls || []).map((call, idx) => (
                                              <div key={`manual-opener-tool-${idx}`} className="rounded-md border border-white/10 bg-black/20 p-2 space-y-2">
                                                <div className="grid grid-cols-[1fr_auto] gap-2 items-center">
                                                  <select
                                                    className="h-9 rounded-md border border-white/10 bg-white/5 px-2 text-xs text-foreground"
                                                    value={normalizeToolId(call.toolName || '')}
                                                    onChange={(e) => updateManualOpenerToolCall(idx, { toolName: e.target.value })}
                                                  >
                                                    <option value="">选择已启用工具</option>
                                                    {normalizeToolId(call.toolName || '') && !openerToolOptions.some((tool) => tool.id === normalizeToolId(call.toolName || '')) && (
                                                      <option value={normalizeToolId(call.toolName || '')}>
                                                        {`${normalizeToolId(call.toolName || '')} (未启用/不存在)`}
                                                      </option>
                                                    )}
                                                    {openerToolOptions.map((tool) => (
                                                      <option key={tool.id} value={tool.id}>
                                                        {tool.label}
                                                      </option>
                                                    ))}
                                                  </select>
                                                  <Button
                                                    type="button"
                                                    variant="ghost"
                                                    size="icon"
                                                    className="h-9 w-9 text-muted-foreground hover:text-red-300"
                                                    onClick={() => removeManualOpenerToolCall(idx)}
                                                    title="删除该工具调用"
                                                  >
                                                    <Trash2 className="w-3.5 h-3.5" />
                                                  </Button>
                                                </div>
                                                <div className="flex items-center justify-between gap-2">
                                                  <p className="text-[11px] text-muted-foreground">参数 JSON</p>
                                                  {OPENER_TOOL_ARGUMENT_TEMPLATES[normalizeToolId(call.toolName || '')] && (
                                                    <Button
                                                      type="button"
                                                      variant="outline"
                                                      size="sm"
                                                      className="h-7 px-2 text-[11px]"
                                                      onClick={() => applyManualOpenerToolTemplate(idx)}
                                                    >
                                                      填充模板
                                                    </Button>
                                                  )}
                                                </div>
                                                <textarea
                                                  value={typeof call.arguments === 'string' ? call.arguments : JSON.stringify(call.arguments || {}, null, 2)}
                                                  onChange={(e) => updateManualOpenerToolCall(idx, { arguments: e.target.value })}
                                                  rows={3}
                                                  className="w-full rounded-md border border-white/10 bg-white/5 px-2 py-1.5 text-xs text-foreground"
                                                  placeholder='参数 JSON（可选），例如 {"msg":"您好，请先选择业务类型"}。可点“填充模板”自动生成。'
                                                />
                                              </div>
                                            ))}
                                          </div>
                                        )}
                                        {openerToolOptions.length === 0 && (
                                          <p className="text-[11px] text-amber-300/90">
                                            当前助手未启用任何工具，请先在“工具配置”里添加后再选择。
                                          </p>
                                        )}
                                        <p className="text-[11px] text-muted-foreground">
                                          按列表顺序执行。参数必须是 JSON 对象；保存时会校验格式。text_msg_prompt / voice_msg_prompt / text_choice_prompt / voice_choice_prompt 支持一键填充模板。
                                        </p>
                                      </div>
                                    )}
                                    <p className="text-xs text-muted-foreground">
                                      {selectedAssistant.generatedOpenerEnabled === true
                                        ? '通话接通后将根据提示词自动生成开场白。'
                                        : '接通通话后的第一句话。'}
                                    </p>

                                    <div className="mt-3 p-3 rounded-lg border border-white/10 bg-white/[0.03] space-y-2">
                                      <div className="flex items-center justify-between gap-3">
                                        <label className="text-xs font-semibold text-white flex items-center">
                                          <Volume2 className="w-4 h-4 mr-2 text-primary" />
                                          使用预加载开场音频
                                        </label>
                                        <div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
                                          <button
                                            type="button"
                                            onClick={() => updateAssistant('openerAudioEnabled', false)}
                                            className={`px-3 py-1 text-xs rounded-md transition-colors ${
                                              selectedAssistant.openerAudioEnabled === true
                                                ? 'text-muted-foreground hover:text-foreground'
                                                : 'bg-primary text-primary-foreground shadow-sm'
                                            }`}
                                          >
                                            关闭
                                          </button>
                                          <button
                                            type="button"
                                            onClick={() => updateAssistant('openerAudioEnabled', true)}
                                            className={`px-3 py-1 text-xs rounded-md transition-colors ${
                                              selectedAssistant.openerAudioEnabled === true
                                                ? 'bg-primary text-primary-foreground shadow-sm'
                                                : 'text-muted-foreground hover:text-foreground'
                                            }`}
                                          >
                                            开启
                                          </button>
                                        </div>
                                      </div>
                                      <div className="flex items-center justify-between gap-3">
                                        <p className="text-[11px] text-muted-foreground">
                                          状态：
                                          {selectedAssistant.openerAudioReady
                                            ? `已生成 (${Math.round((selectedAssistant.openerAudioDurationMs || 0) / 1000)}s)`
                                            : '未生成'}
                                        </p>
                                        <div className="flex items-center gap-2">
                                          <Button
                                            variant="secondary"
                                            size="sm"
                                            onClick={handlePreviewOpenerAudio}
                                            disabled={!selectedAssistant.openerAudioReady || openerAudioGenerating}
                                          >
                                            <Play className="w-3.5 h-3.5 mr-1.5" />
                                            {openerAudioPreviewing ? '播放中...' : '预览'}
                                          </Button>
                                          {openerAudioPreviewing && (
                                            <Button variant="ghost" size="sm" onClick={stopOpenerAudioPreview}>
                                              <Square className="w-3.5 h-3.5 mr-1.5" />
                                              停止
                                            </Button>
                                          )}
                                          <Button
                                            variant="secondary"
                                            size="sm"
                                            onClick={handleGenerateOpenerAudio}
                                            disabled={openerAudioGenerating || selectedAssistant.generatedOpenerEnabled === true}
                                          >
                                            {openerAudioGenerating ? '生成中...' : '生成开场预加载音频'}
                                          </Button>
                                        </div>
                                      </div>
                                      <p className="text-[11px] text-muted-foreground">
                                        使用当前 TTS 配置生成并保存到后端；引擎可直接播放以降低首包延迟。
                                      </p>
                                    </div>
                                </div>
                                )}

                            </div>
                        )}

                        {activeTab === TabValue.KNOWLEDGE && selectedAssistant.configMode === 'platform' && (
                            <div className="space-y-8 animate-in fade-in">
                                <div className="space-y-2">
                                    <label className="text-sm font-medium text-white flex items-center">
                                        <BrainCircuit className="w-4 h-4 mr-2 text-primary"/> 嵌入模型 (Embedding Model)
                                    </label>
                                    <div className="relative group">
                                        <select
                                            className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground appearance-none cursor-pointer"
                                            value={selectedAssistant.embeddingModelId || ''}
                                            onChange={(e) => updateAssistant('embeddingModelId', e.target.value)}
                                        >
                                            <option value="">使用系统默认</option>
                                            {llmModels.filter(m => m.type === 'embedding').map(model => (
                                                <option key={model.id} value={model.id}>{model.name} ({model.vendor})</option>
                                            ))}
                                        </select>
                                        <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none group-hover:text-primary transition-colors" />
                                    </div>
                                    <p className="text-xs text-muted-foreground">用于将文本转换为向量的嵌入模型。</p>
                                </div>

                                <div className="space-y-2">
                                    <label className="text-sm font-medium text-white flex items-center">
                                        <Filter className="w-4 h-4 mr-2 text-primary"/> 重排模型 (Rerank Model)
                                    </label>
                                    <div className="relative group">
                                        <select
                                            className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground appearance-none cursor-pointer"
                                            value={selectedAssistant.rerankModelId || ''}
                                            onChange={(e) => updateAssistant('rerankModelId', e.target.value)}
                                        >
                                            <option value="">不使用重排</option>
                                            {llmModels.filter(m => m.type === 'rerank').map(model => (
                                                <option key={model.id} value={model.id}>{model.name} ({model.vendor})</option>
                                            ))}
                                        </select>
                                        <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none group-hover:text-primary transition-colors" />
                                    </div>
                                    <p className="text-xs text-muted-foreground">可选配置。重排模型可优化检索结果的相关性排序。</p>
                                </div>

                                <div className="space-y-2">
                                    <label className="text-sm font-medium text-white flex items-center">
                                        <Book className="w-4 h-4 mr-2 text-primary"/> 知识库挂载 (Knowledge Base)
                                    </label>
                                    <div className="relative group">
                                        <select 
                                            className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground appearance-none cursor-pointer"
                                            value={selectedAssistant.knowledgeBaseId}
                                            onChange={(e) => updateAssistant('knowledgeBaseId', e.target.value)}
                                        >
                                            <option value="">如果不选择，则使用通用大模型知识</option>
                                            {knowledgeBases.map(kb => (
                                                <option key={kb.id} value={kb.id}>{kb.name}</option>
                                            ))}
                                        </select>
                                        <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none group-hover:text-primary transition-colors" />
                                    </div>
                                    <p className="text-xs text-muted-foreground">选择助手回答问题时参考的私有知识库。</p>
                                </div>
                            </div>
                        )}

                        {activeTab === TabValue.VOICE && !isNoneConfig && (
                            <div className="space-y-8">
                                <div className="space-y-2">
                                    <label className="text-sm font-medium text-white flex items-center">
                                        <Ear className="w-4 h-4 mr-2 text-primary"/> 语音识别 (ASR Model)
                                    </label>
                                    <div className="relative group">
                                        <select 
                                            className="flex h-12 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-1 text-sm shadow-sm transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground appearance-none cursor-pointer"
                                            value={selectedAssistant.asrModelId || ''}
                                            onChange={(e) => updateAssistant('asrModelId', e.target.value)}
                                        >
                                            <option value="">使用系统默认模型</option>
                                            {asrModels.map(model => (
                                                <option key={model.id} value={model.id}>
                                                    {model.name} ({model.vendor})
                                                </option>
                                            ))}
                                        </select>
                                        <ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none group-hover:text-primary transition-colors" />
                                    </div>
                                    <p className="text-xs text-muted-foreground flex items-center mt-1">
                                        选择用于识别用户语音输入的模型。
                                    </p>
                                </div>

                                <div className="space-y-3">
                                    <div className="flex items-center justify-between gap-3">
                                        <label className="text-sm font-medium text-white flex items-center">
                                            <Volume2 className="w-4 h-4 mr-2 text-primary"/> 语音输出
                                        </label>
                                        <div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
                                            <button
                                              type="button"
                                              onClick={() => updateAssistant('voiceOutputEnabled', false)}
                                              className={`px-3 py-1 text-xs rounded-md transition-colors ${
                                                selectedAssistant.voiceOutputEnabled !== false
                                                  ? 'text-muted-foreground hover:text-foreground'
                                                  : 'bg-primary text-primary-foreground shadow-sm'
                                              }`}
                                            >
                                              关闭
                                            </button>
                                            <button
                                              type="button"
                                              onClick={() => updateAssistant('voiceOutputEnabled', true)}
                                              className={`px-3 py-1 text-xs rounded-md transition-colors ${
                                                selectedAssistant.voiceOutputEnabled !== false
                                                  ? 'bg-primary text-primary-foreground shadow-sm'
                                                  : 'text-muted-foreground hover:text-foreground'
                                              }`}
                                            >
                                              开启
                                            </button>
                                        </div>
                                    </div>

                                    <div className="relative group">
                                        <select 
                                            className="flex h-12 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-1 text-sm shadow-sm transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground appearance-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
                                            value={selectedAssistant.voice}
                                            onChange={(e) => updateAssistant('voice', e.target.value)}
                                            disabled={selectedAssistant.voiceOutputEnabled === false}
                                        >
                                            <option value="" disabled>请选择声音库中的声音...</option>
                                            {voices.map(voice => (
                                                <option key={voice.id} value={voice.id}>
                                                    {voice.name} ({voice.vendor} - {voice.gender === 'Male' ? '男' : '女'})
                                                </option>
                                            ))}
                                        </select>
                                        <ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none group-hover:text-primary transition-colors" />
                                    </div>
                                    <p className="text-xs text-muted-foreground flex items-center mt-1">
                                        <Sparkles className="w-3 h-3 mr-1 text-primary opacity-70" />
                                        {selectedAssistant.voiceOutputEnabled === false
                                          ? '已关闭语音输出，仅文本回复。开启后可选择音色。'
                                          : '音色配置同步自声音库。如需添加更多音色，请前往“声音库”模块。'}
                                    </p>
                                </div>

                                <div className="space-y-4 pt-2">
                                    <div className="flex items-center justify-between gap-3">
                                        <label className="text-sm font-medium text-white flex items-center">
                                            <ArrowLeftRight className="w-4 h-4 mr-2 text-primary" /> 打断控制
                                        </label>
                                        <div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
                                            <button
                                              type="button"
                                              onClick={() => updateAssistant('botCannotBeInterrupted', false)}
                                              className={`px-3 py-1 text-xs rounded-md transition-colors ${
                                                selectedAssistant.botCannotBeInterrupted === true
                                                  ? 'text-muted-foreground hover:text-foreground'
                                                  : 'bg-primary text-primary-foreground shadow-sm'
                                              }`}
                                            >
                                              可打断
                                            </button>
                                            <button
                                              type="button"
                                              onClick={() => updateAssistant('botCannotBeInterrupted', true)}
                                              className={`px-3 py-1 text-xs rounded-md transition-colors ${
                                                selectedAssistant.botCannotBeInterrupted === true
                                                  ? 'bg-primary text-primary-foreground shadow-sm'
                                                  : 'text-muted-foreground hover:text-foreground'
                                              }`}
                                            >
                                              不可打断
                                            </button>
                                        </div>
                                    </div>

                                    {canAdjustInterruptionSensitivity && (
                                      <>
                                        <div className="flex justify-between items-center mb-1">
                                            <label className="text-sm font-medium flex items-center text-white">
                                                <Timer className="w-4 h-4 mr-2 text-primary"/> 打断灵敏度
                                            </label>
                                            <div className="flex items-center gap-2">
                                                <div className="relative">
                                                    <Input 
                                                        type="number"
                                                        value={selectedAssistant.interruptionSensitivity || 180}
                                                        onChange={(e) => updateAssistant('interruptionSensitivity', parseInt(e.target.value) || 0)}
                                                        className="w-20 h-8 text-right pr-7 text-xs font-mono bg-black/40 border-white/5"
                                                    />
                                                    <span className="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground font-mono">ms</span>
                                                </div>
                                            </div>
                                        </div>
                                        <div className="flex items-center gap-6">
                                            <input 
                                                type="range" 
                                                min="0" 
                                                max="2000" 
                                                step="50" 
                                                value={selectedAssistant.interruptionSensitivity || 180}
                                                onChange={(e) => updateAssistant('interruptionSensitivity', parseInt(e.target.value))}
                                                className="flex-1 h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
                                            />
                                        </div>
                                        <div className="flex justify-between text-[10px] font-mono uppercase tracking-widest px-0.5 text-muted-foreground opacity-50">
                                            <span>0ms（极易打断）</span>
                                            <span>1000ms</span>
                                            <span>2000ms（较难打断）</span>
                                        </div>
                                        <p className="text-xs pt-1 italic text-muted-foreground opacity-60">
                                            用户持续说话达到该时长后，AI 将停止当前发言并响应。数值越小越敏感，也更容易被噪音触发。
                                        </p>
                                      </>
                                    )}
                                </div>

                                <div className="space-y-3">
                                    <label className="text-sm font-medium text-white flex items-center">
                                        <Mic className="w-4 h-4 mr-2 text-primary"/> ASR 热词优化 (Hotwords)
                                    </label>
                                    <div className="flex space-x-2">
                                        <Input 
                                            value={hotwordInput}
                                            onChange={(e) => setHotwordInput(e.target.value)}
                                            placeholder="输入专有名词或高频词汇..."
                                            onKeyDown={(e) => e.key === 'Enter' && addHotword()}
                                            className="bg-white/5 border-white/10"
                                        />
                                        <Button variant="secondary" onClick={addHotword} className="px-10 whitespace-nowrap">添加</Button>
                                    </div>
                                    <div className="flex flex-wrap gap-2 min-h-[40px] p-2 rounded-lg border border-dashed border-white/10">
                                        {selectedAssistant.hotwords.length === 0 && (
                                            <span className="text-xs text-muted-foreground py-1 px-1">暂无热词</span>
                                        )}
                                        {selectedAssistant.hotwords.map((word, idx) => (
                                            <Badge key={idx} variant="outline" className="py-1">
                                                {word}
                                                <button onClick={() => removeHotword(word)} className="ml-2 hover:text-destructive transition-colors text-lg leading-none">×</button>
                                            </Badge>
                                        ))}
                                    </div>
                                    <p className="text-xs text-muted-foreground">添加热词可以提高语音识别特定词汇的准确率。</p>
                                </div>
                            </div>
                        )}

                        {activeTab === TabValue.TOOLS && selectedAssistant.configMode === 'platform' && (
                            <div className="space-y-8 animate-in fade-in">
                                <div className="flex items-center justify-between">
                                    <h3 className="text-[10px] font-black flex items-center text-white uppercase tracking-[0.2em]">
                                        <Wrench className="w-3.5 h-3.5 mr-2 text-primary" /> 已启用工具
                                    </h3>
                                    <button
                                        type="button"
                                        onClick={() => setToolPickerOpen(true)}
                                        className="h-7 w-7 rounded-full border border-white/15 bg-white/5 text-muted-foreground hover:text-white hover:border-primary/50 hover:bg-primary/10 flex items-center justify-center transition-colors"
                                        title="添加工具"
                                    >
                                        <Plus className="w-3.5 h-3.5" />
                                    </button>
                                </div>

                                <div className="space-y-4">
                                    <div className="flex items-center justify-between">
                                        <h3 className="text-[10px] font-black flex items-center text-primary uppercase tracking-[0.2em]">
                                            <Wrench className="w-3.5 h-3.5 mr-2" /> 系统指令
                                        </h3>
                                    </div>
                                    <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
                                        {activeSystemTools.map(tool => (
                                            <div
                                                key={tool.id}
                                                className="p-4 rounded-xl border border-primary/40 bg-primary/10 shadow-[0_0_15px_rgba(6,182,212,0.1)] relative flex items-start space-x-3"
                                            >
                                                <button
                                                    type="button"
                                                    onClick={(e) => removeImportedTool(e, tool)}
                                                    className="absolute -top-1 -right-1 p-0.5 rounded-full bg-destructive text-white hover:scale-110 transition-transform shadow-lg z-10"
                                                    title="移除工具"
                                                >
                                                    <X className="w-3 h-3" />
                                                </button>
                                                <div className="p-2 rounded-lg shrink-0 bg-primary text-primary-foreground">
                                                    {renderToolIcon(tool.icon)}
                                                </div>
                                                <div className="flex-1 min-w-0">
                                                    <div className="text-sm font-bold text-white">{tool.name}</div>
                                                    <p className="text-[10px] text-muted-foreground line-clamp-1 opacity-70">{tool.description}</p>
                                                </div>
                                            </div>
                                        ))}
                                        {activeSystemTools.length === 0 && (
                                            <div className="col-span-full p-4 rounded-xl border border-dashed border-white/10 text-xs text-muted-foreground">
                                                暂未启用系统指令工具
                                            </div>
                                        )}
                                    </div>
                                </div>

                                <div className="space-y-4">
                                    <div className="flex items-center justify-between">
                                        <h3 className="text-[10px] font-black flex items-center text-blue-400 uppercase tracking-[0.2em]">
                                            <TrendingUp className="w-3.5 h-3.5 mr-2" /> 信息查询
                                        </h3>
                                    </div>
                                    <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
                                        {activeQueryTools.map(tool => (
                                            <div
                                                key={tool.id}
                                                className="p-4 rounded-xl border border-blue-500/40 bg-blue-500/10 shadow-[0_0_15px_rgba(59,130,246,0.1)] relative flex items-start space-x-3"
                                            >
                                                <button
                                                    type="button"
                                                    onClick={(e) => removeImportedTool(e, tool)}
                                                    className="absolute -top-1 -right-1 p-0.5 rounded-full bg-destructive text-white hover:scale-110 transition-transform shadow-lg z-10"
                                                    title="移除工具"
                                                >
                                                    <X className="w-3 h-3" />
                                                </button>
                                                <div className="p-2 rounded-lg shrink-0 bg-blue-500 text-white">
                                                    {renderToolIcon(tool.icon)}
                                                </div>
                                                <div className="flex-1 min-w-0">
                                                    <div className="text-sm font-bold text-white">{tool.name}</div>
                                                    <p className="text-[10px] text-muted-foreground line-clamp-1 opacity-70">{tool.description}</p>
                                                </div>
                                            </div>
                                        ))}
                                        {activeQueryTools.length === 0 && (
                                            <div className="col-span-full p-4 rounded-xl border border-dashed border-white/10 text-xs text-muted-foreground">
                                                暂未启用信息查询工具
                                            </div>
                                        )}
                                    </div>
                                </div>

                                <div className="p-4 bg-white/5 border border-white/5 rounded-xl text-[10px] text-muted-foreground flex items-center gap-3">
                                    <Rocket className="w-4 h-4 text-primary shrink-0" />
                                    <span>提示：此处仅导入工具库中的已有工具。移除仅对当前小助手生效，不会删除工具库。</span>
                                </div>
                            </div>
                        )}
                      </div>
                    )}
                </div>
            </>
        ) : (
            <div className="flex-1 flex flex-col items-center justify-center text-muted-foreground">
                <div className="w-16 h-16 rounded-2xl bg-white/5 flex items-center justify-center mb-4">
                    <BotIcon className="h-8 w-8 opacity-50" />
                </div>
                <p className="text-lg font-medium">请选择一个小助手</p>
                <p className="text-sm opacity-60">从左侧列表选择或创建一个新的小助手以开始配置</p>
            </div>
        )}
      </div>

      {/* Publish Modal */}
      <Dialog
        isOpen={isPublishModalOpen}
        onClose={() => setIsPublishModalOpen(false)}
        title="发布小助手"
        footer={
            <Button onClick={() => setIsPublishModalOpen(false)}>确认</Button>
        }
      >
        <div className="space-y-6">
            <div className="flex bg-white/5 p-1 rounded-lg border border-white/10">
                <button 
                    onClick={() => setPublishTab('web')}
                    className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${publishTab === 'web' ? 'bg-primary text-primary-foreground shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
                >
                    <ExternalLink className="w-3.5 h-3.5 mr-2" /> 网页分享
                </button>
                <button 
                    onClick={() => setPublishTab('api')}
                    className={`flex-1 flex items-center justify-center py-2 text-xs font-bold rounded-md transition-all ${publishTab === 'api' ? 'bg-primary text-primary-foreground shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
                >
                    <Server className="w-3.5 h-3.5 mr-2" /> API 接入
                </button>
            </div>

            {publishTab === 'web' ? (
                <div className="space-y-4 animate-in fade-in slide-in-from-top-1">
                    <div className="p-4 rounded-xl bg-primary/5 border border-primary/20 space-y-3">
                        <div className="flex items-center gap-2 text-primary">
                            <Zap className="w-4 h-4" />
                            <h4 className="text-sm font-bold">交互体验站</h4>
                        </div>
                        <p className="text-xs text-muted-foreground leading-relaxed">
                            该链接允许用户通过独立的浏览器页面与您的智能体进行交互。支持：文本对话、实时音频通话以及双向视频通话。
                        </p>
                    </div>
                    <div className="space-y-2">
                        <label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase">公开访问链接</label>
                        <div className="flex gap-2">
                            <Input 
                                readOnly 
                                value={`https://ai-video.com/share/${selectedAssistant?.id}`} 
                                className="bg-white/5 border-white/10 font-mono text-[11px] text-primary"
                            />
                            <Button variant="secondary" size="icon" onClick={() => handleCopyId('', `https://ai-video.com/share/${selectedAssistant?.id}`)}>
                                {copySuccess ? <ClipboardCheck className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4" />}
                            </Button>
                        </div>
                    </div>
                </div>
            ) : (
                <div className="space-y-5 animate-in fade-in slide-in-from-top-1">
                    <div className="space-y-2">
                        <label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase">API Endpoint (v1)</label>
                        <div className="flex gap-2">
                            <Input 
                                readOnly 
                                value={`https://api.ai-video.com/v1/call/${selectedAssistant?.id}`} 
                                className="bg-white/5 border-white/10 font-mono text-[11px]"
                            />
                            <Button variant="ghost" size="icon" onClick={() => handleCopyId('', `https://api.ai-video.com/v1/call/${selectedAssistant?.id}`)}>
                                <Copy className="h-4 w-4" />
                            </Button>
                        </div>
                    </div>
                    <div className="space-y-2">
                        <div className="flex items-center justify-between">
                            <label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase">Secret API KEY</label>
                            <Badge variant="outline" className="text-[8px] opacity-50">PRIVATE</Badge>
                        </div>
                        <div className="flex gap-2">
                            <Input 
                                readOnly 
                                type="password"
                                value="sk-ai-video-78x29jKkL1M90vX..." 
                                className="bg-white/5 border-white/10 font-mono text-[11px]"
                            />
                            <Button variant="ghost" size="icon" onClick={() => handleCopyId('', "sk-ai-video-78x29jKkL1M90vX...")}>
                                <Copy className="h-4 w-4" />
                            </Button>
                        </div>
                        <p className="text-[10px] text-muted-foreground italic flex items-center gap-1.5 px-1 mt-2">
                            <Key className="w-3 h-3" /> API Key 仅用于身份鉴权，请务必妥善保存。
                        </p>
                    </div>
                </div>
            )}
        </div>
      </Dialog>

      {/* Tool Picker Modal */}
      <Dialog
        isOpen={toolPickerOpen}
        onClose={() => setToolPickerOpen(false)}
        title="添加工具"
      >
        <div className="space-y-6">
          <div className="space-y-3">
            <h5 className="text-[10px] font-black flex items-center text-primary uppercase tracking-[0.2em]">
              <Wrench className="w-3.5 h-3.5 mr-2" /> 系统指令可选
            </h5>
            <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
              {availableSystemTools.map((tool) => (
                <button
                  key={tool.id}
                  type="button"
                  onClick={() => toggleTool(tool.id)}
                  className="text-left p-3 rounded-lg border border-white/10 bg-white/5 hover:bg-primary/10 hover:border-primary/40 transition-colors"
                >
                  <div className="flex items-center gap-2">
                    <div className="p-1.5 rounded-md bg-white/10 text-primary">{renderToolIcon(tool.icon)}</div>
                    <div className="text-sm font-bold text-white">{tool.name}</div>
                  </div>
                  <p className="text-[10px] text-muted-foreground mt-1 line-clamp-1">{tool.description}</p>
                </button>
              ))}
              {availableSystemTools.length === 0 && (
                <div className="col-span-full p-3 rounded-lg border border-dashed border-white/10 text-xs text-muted-foreground">
                  没有可添加的系统指令工具
                </div>
              )}
            </div>
          </div>
          <div className="space-y-3">
            <h5 className="text-[10px] font-black flex items-center text-blue-400 uppercase tracking-[0.2em]">
              <TrendingUp className="w-3.5 h-3.5 mr-2" /> 信息查询可选
            </h5>
            <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
              {availableQueryTools.map((tool) => (
                <button
                  key={tool.id}
                  type="button"
                  onClick={() => toggleTool(tool.id)}
                  className="text-left p-3 rounded-lg border border-white/10 bg-white/5 hover:bg-blue-500/10 hover:border-blue-500/40 transition-colors"
                >
                  <div className="flex items-center gap-2">
                    <div className="p-1.5 rounded-md bg-white/10 text-blue-300">{renderToolIcon(tool.icon)}</div>
                    <div className="text-sm font-bold text-white">{tool.name}</div>
                  </div>
                  <p className="text-[10px] text-muted-foreground mt-1 line-clamp-1">{tool.description}</p>
                </button>
              ))}
              {availableQueryTools.length === 0 && (
                <div className="col-span-full p-3 rounded-lg border border-dashed border-white/10 text-xs text-muted-foreground">
                  没有可添加的信息查询工具
                </div>
              )}
            </div>
          </div>
        </div>
      </Dialog>

      {selectedAssistant && (
         <DebugDrawer 
           isOpen={debugOpen} 
           onClose={() => setDebugOpen(false)} 
           assistant={selectedAssistant}
           voices={voices}
           llmModels={llmModels}
           asrModels={asrModels}
           tools={tools}
           textTtsEnabled={selectedAssistant.voiceOutputEnabled !== false}
         />
      )}

      <Dialog
        isOpen={unsavedDebugConfirmOpen}
        onClose={() => setUnsavedDebugConfirmOpen(false)}
        title="未保存配置提醒"
        footer={
          <>
            <Button variant="ghost" onClick={() => setUnsavedDebugConfirmOpen(false)}>
              取消
            </Button>
            <Button onClick={handleConfirmOpenDebug}>
              继续调试
            </Button>
          </>
        }
      >
        <div className="flex items-center space-x-4">
          <div className="p-3 bg-amber-500/10 rounded-full">
            <AlertTriangle className="h-6 w-6 text-amber-400" />
          </div>
          <div>
            <p className="text-sm text-foreground text-white">
              当前助手配置尚未保存，仍要打开调试窗口吗？
            </p>
            <p className="text-xs text-muted-foreground mt-1">
              建议先保存配置，以确保调试使用最新参数。
            </p>
          </div>
        </div>
      </Dialog>

      {/* Delete Confirmation Dialog */}
      <Dialog 
        isOpen={!!deleteId} 
        onClose={() => setDeleteId(null)} 
        title="确认删除"
        footer={
            <>
                <Button variant="ghost" onClick={() => setDeleteId(null)}>取消</Button>
                <Button variant="destructive" onClick={confirmDelete}>确认删除</Button>
            </>
        }
      >
        <div className="flex items-center space-x-4">
            <div className="p-3 bg-destructive/10 rounded-full">
                <AlertTriangle className="h-6 w-6 text-destructive" />
            </div>
            <div>
                <p className="text-sm text-foreground text-white">
                    您确定要删除此小助手吗？此操作无法撤销。
                </p>
                {deleteId && (
                    <p className="text-xs text-muted-foreground mt-1">
                        将要删除: {assistants.find(a => a.id === deleteId)?.name}
                    </p>
                )}
            </div>
        </div>
      </Dialog>
    </div>
  );
};

// Icon helper
const BotIcon = ({className}: {className?: string}) => (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
        <path d="M12 8V4H8" />
        <rect width="16" height="12" x="4" y="8" rx="2" />
        <path d="M2 14h2" />
        <path d="M20 14h2" />
        <path d="M15 13v2" />
        <path d="M9 13v2" />
    </svg>
);

const TOOL_PARAMETER_HINTS: Record<string, any> = {
  search: {
    type: 'object',
    properties: { query: { type: 'string', description: 'Search query' } },
    required: ['query'],
  },
  calculator: {
    type: 'object',
    properties: { expression: { type: 'string', description: 'Math expression' } },
    required: ['expression'],
  },
  weather: {
    type: 'object',
    properties: { city: { type: 'string', description: 'City name' } },
    required: ['city'],
  },
  translate: {
    type: 'object',
    properties: {
      text: { type: 'string', description: 'Text to translate' },
      target_lang: { type: 'string', description: 'Target language' },
    },
    required: ['text', 'target_lang'],
  },
  knowledge: {
    type: 'object',
    properties: {
      query: { type: 'string', description: 'Knowledge query' },
      kb_id: { type: 'string', description: 'Knowledge base id' },
    },
    required: ['query'],
  },
  current_time: {
    type: 'object',
    properties: {},
    required: [],
  },
  take_phone: {
    type: 'object',
    properties: {},
    required: [],
  },
  increase_volume: {
    type: 'object',
    properties: {
      step: { type: 'integer', description: 'Volume step, default 1' },
    },
    required: [],
  },
  decrease_volume: {
    type: 'object',
    properties: {
      step: { type: 'integer', description: 'Volume step, default 1' },
    },
    required: [],
  },
  voice_msg_prompt: {
    type: 'object',
    properties: {
      msg: { type: 'string', description: 'Message text to speak' },
    },
    required: ['msg'],
  },
  text_msg_prompt: {
    type: 'object',
    properties: {
      msg: { type: 'string', description: 'Message text to display in debug drawer modal' },
    },
    required: ['msg'],
  },
  voice_choice_prompt: {
    type: 'object',
    properties: {
      question: { type: 'string', description: 'Question text to ask the user' },
      options: {
        type: 'array',
        description: 'Selectable options (string or object with id/label/value)',
        minItems: 2,
        items: {
          anyOf: [
            { type: 'string' },
            {
              type: 'object',
              properties: {
                id: { type: 'string' },
                label: { type: 'string' },
                value: { type: 'string' },
              },
              required: ['label'],
            },
          ],
        },
      },
      voice_text: { type: 'string', description: 'Optional custom voice text, defaults to question' },
    },
    required: ['question', 'options'],
  },
  text_choice_prompt: {
    type: 'object',
    properties: {
      question: { type: 'string', description: 'Question text to ask the user' },
      options: {
        type: 'array',
        description: 'Selectable options (string or object with id/label/value)',
        minItems: 2,
        items: {
          anyOf: [
            { type: 'string' },
            {
              type: 'object',
              properties: {
                id: { type: 'string' },
                label: { type: 'string' },
                value: { type: 'string' },
              },
              required: ['label'],
            },
          ],
        },
      },
    },
    required: ['question', 'options'],
  },
  code_interpreter: {
    type: 'object',
    properties: {
      code: { type: 'string', description: 'Python code' },
    },
    required: ['code'],
  },
};

const getDefaultToolParameters = (toolId: string) =>
  TOOL_PARAMETER_HINTS[toolId] || { type: 'object', properties: {} };

const DEBUG_CLIENT_TOOLS = [
  { id: 'turn_on_camera', name: 'turn_on_camera', description: '打开摄像头' },
  { id: 'turn_off_camera', name: 'turn_off_camera', description: '关闭摄像头' },
  { id: 'increase_volume', name: 'increase_volume', description: '调高音量' },
  { id: 'decrease_volume', name: 'decrease_volume', description: '调低音量' },
  { id: 'voice_msg_prompt', name: 'voice_msg_prompt', description: '语音消息提示' },
  { id: 'text_msg_prompt', name: 'text_msg_prompt', description: '文本消息提示' },
  { id: 'voice_choice_prompt', name: 'voice_choice_prompt', description: '语音选项提示（原子）' },
  { id: 'text_choice_prompt', name: 'text_choice_prompt', description: '文本选项提示（等待选择）' },
] as const;
const DEBUG_CLIENT_TOOL_ID_SET = new Set<string>(DEBUG_CLIENT_TOOLS.map((item) => item.id));
const DEBUG_CLIENT_TOOL_WAIT_DEFAULTS: Record<string, boolean> = {
  text_msg_prompt: true,
  voice_choice_prompt: true,
  text_choice_prompt: true,
};

type DynamicVariableEntry = {
  id: string;
  key: string;
  value: string;
};

type TemplateSuggestionState = {
  field: 'prompt' | 'opener';
  start: number;
  end: number;
  query: string;
  anchorLeft: number;
  anchorTop: number;
};

const DYNAMIC_VARIABLE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/;
const DYNAMIC_VARIABLE_PLACEHOLDER_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
const DYNAMIC_VARIABLE_MAX_ITEMS = 30;
const DYNAMIC_VARIABLE_MAX_VALUE_LENGTH = 1000;
const TEMPLATE_SUGGESTION_PANEL_WIDTH = 320;
const TEMPLATE_SUGGESTION_PANEL_MAX_HEIGHT = 240;
const TEMPLATE_SUGGESTION_VIEWPORT_PADDING = 8;
const SYSTEM_DYNAMIC_VARIABLE_OPTIONS = [
  {
    key: 'system__time',
    description: 'Session local time (YYYY-MM-DD HH:mm:ss)',
  },
  {
    key: 'system_utc',
    description: 'Session UTC time (YYYY-MM-DD HH:mm:ss)',
  },
  {
    key: 'system_timezone',
    description: 'Session local timezone',
  },
];
const SYSTEM_DYNAMIC_VARIABLE_KEY_SET = new Set(SYSTEM_DYNAMIC_VARIABLE_OPTIONS.map((item) => item.key));

const clampTemplateSuggestionPosition = (left: number, top: number) => {
  if (typeof window === 'undefined') return { left, top };
  const maxLeft = Math.max(
    TEMPLATE_SUGGESTION_VIEWPORT_PADDING,
    window.innerWidth - TEMPLATE_SUGGESTION_PANEL_WIDTH - TEMPLATE_SUGGESTION_VIEWPORT_PADDING
  );
  const maxTop = Math.max(
    TEMPLATE_SUGGESTION_VIEWPORT_PADDING,
    window.innerHeight - TEMPLATE_SUGGESTION_PANEL_MAX_HEIGHT - TEMPLATE_SUGGESTION_VIEWPORT_PADDING
  );
  return {
    left: Math.min(Math.max(left, TEMPLATE_SUGGESTION_VIEWPORT_PADDING), maxLeft),
    top: Math.min(Math.max(top, TEMPLATE_SUGGESTION_VIEWPORT_PADDING), maxTop),
  };
};

const getTemplateSuggestionAnchor = (
  control: HTMLTextAreaElement | HTMLInputElement,
  caret: number | null
) => {
  const rect = control.getBoundingClientRect();
  const fallback = clampTemplateSuggestionPosition(rect.left + 12, rect.bottom + 8);
  if (
    caret === null ||
    caret < 0 ||
    typeof window === 'undefined' ||
    typeof document === 'undefined'
  ) {
    return fallback;
  }

  const computedStyle = window.getComputedStyle(control);
  const isInput = control instanceof HTMLInputElement;
  const mirror = document.createElement('div');
  const marker = document.createElement('span');
  const propertiesToCopy = [
    'direction',
    'boxSizing',
    'width',
    'overflowX',
    'overflowY',
    'paddingTop',
    'paddingRight',
    'paddingBottom',
    'paddingLeft',
    'borderTopWidth',
    'borderRightWidth',
    'borderBottomWidth',
    'borderLeftWidth',
    'borderTopStyle',
    'borderRightStyle',
    'borderBottomStyle',
    'borderLeftStyle',
    'fontFamily',
    'fontSize',
    'fontStyle',
    'fontVariant',
    'fontWeight',
    'fontStretch',
    'letterSpacing',
    'lineHeight',
    'textAlign',
    'textIndent',
    'textTransform',
    'textDecoration',
    'wordSpacing',
    'tabSize',
    'overflowWrap',
  ] as const;

  mirror.style.position = 'absolute';
  mirror.style.left = '0px';
  mirror.style.top = '0px';
  mirror.style.visibility = 'hidden';
  mirror.style.pointerEvents = 'none';
  mirror.style.whiteSpace = isInput ? 'pre' : 'pre-wrap';
  mirror.style.wordBreak = 'break-word';
  mirror.style.wordWrap = 'break-word';
  mirror.style.overflow = 'hidden';

  if (isInput) {
    mirror.style.height = computedStyle.height;
    mirror.style.lineHeight = computedStyle.height;
  }

  for (const property of propertiesToCopy) {
    (mirror.style as any)[property] = (computedStyle as any)[property];
  }

  const value = control.value || '';
  const safeCaret = Math.min(caret, value.length);
  mirror.textContent = value.slice(0, safeCaret);
  marker.textContent = value.slice(safeCaret) || '.';
  marker.style.display = 'inline-block';
  marker.style.width = '1px';
  mirror.appendChild(marker);
  document.body.appendChild(mirror);
  const markerLeft = marker.offsetLeft;
  const markerTop = marker.offsetTop;
  document.body.removeChild(mirror);

  if (!Number.isFinite(markerLeft) || !Number.isFinite(markerTop)) {
    return fallback;
  }

  const lineHeightRaw = Number.parseFloat(computedStyle.lineHeight);
  const fontSizeRaw = Number.parseFloat(computedStyle.fontSize);
  const lineHeight = Number.isFinite(lineHeightRaw)
    ? lineHeightRaw
    : Number.isFinite(fontSizeRaw)
      ? fontSizeRaw * 1.2
      : 16;
  const caretLeft = rect.left + markerLeft - control.scrollLeft;
  const caretTop = rect.top + markerTop - control.scrollTop;
  return clampTemplateSuggestionPosition(caretLeft, caretTop + lineHeight + 6);
};

const extractDynamicTemplateKeys = (text: string): string[] => {
  if (!text) return [];
  const keys = new Set<string>();
  const pattern = new RegExp(DYNAMIC_VARIABLE_PLACEHOLDER_RE);
  for (const match of text.matchAll(pattern)) {
    if (match[1]) {
      keys.add(match[1]);
    }
  }
  return Array.from(keys);
};

type DebugTranscriptMessage = {
  role: 'user' | 'model' | 'tool';
  text: string;
  responseId?: string;
  ttfbMs?: number;
};

type DebugPromptPendingResult = {
  toolCallId: string;
  toolName: string;
  toolDisplayName: string;
  waitForResponse: boolean;
};

type DebugChoicePromptOption = {
  id: string;
  label: string;
  value: string;
};

type DebugTextPromptDialogState = {
  open: boolean;
  message: string;
  pendingResult?: DebugPromptPendingResult;
  promptType?: 'text' | 'voice';
  voiceText?: string;
};

type DebugChoicePromptDialogState = {
  open: boolean;
  question: string;
  options: DebugChoicePromptOption[];
  pendingResult?: DebugPromptPendingResult;
  requireSelection?: boolean;
  voiceText?: string;
};

type DebugPromptQueueItem =
  | { kind: 'text'; payload: Omit<DebugTextPromptDialogState, 'open'> }
  | { kind: 'choice'; payload: Omit<DebugChoicePromptDialogState, 'open'> };

const normalizeChoicePromptOptions = (rawOptions: unknown[]): DebugChoicePromptOption[] => {
  const usedIds = new Set<string>();
  const resolved: DebugChoicePromptOption[] = [];
  rawOptions.forEach((rawOption, index) => {
    let id = `opt_${index + 1}`;
    let label = '';
    let value = '';
    if (typeof rawOption === 'string' || typeof rawOption === 'number' || typeof rawOption === 'boolean') {
      label = String(rawOption).trim();
      value = label;
    } else if (rawOption && typeof rawOption === 'object') {
      const row = rawOption as Record<string, unknown>;
      const labelCandidate = row.label ?? row.text ?? row.name;
      label = String(labelCandidate ?? '').trim();
      id = String(row.id ?? id).trim() || id;
      const valueCandidate = row.value;
      value = valueCandidate === undefined || valueCandidate === null ? label : String(valueCandidate);
    }
    if (!label) return;
    if (usedIds.has(id)) {
      let suffix = 2;
      while (usedIds.has(`${id}_${suffix}`)) suffix += 1;
      id = `${id}_${suffix}`;
    }
    usedIds.add(id);
    resolved.push({ id, label, value });
  });
  return resolved;
};

// Stable transcription log so the scroll container is not recreated on every render (avoids scroll jumping)
const TranscriptionLog: React.FC<{
  scrollRef: React.RefObject<HTMLDivElement | null>;
  messages: DebugTranscriptMessage[];
  isLoading: boolean;
  className?: string;
}> = ({ scrollRef, messages, isLoading, className = '' }) => (
  <div ref={scrollRef} className={`overflow-y-auto overflow-x-hidden space-y-4 p-2 border border-white/5 rounded-md bg-black/20 min-h-0 custom-scrollbar ${className}`}>
    {messages.length === 0 && <div className="text-center text-muted-foreground text-xs py-4">暂无转写记录</div>}
    {messages.map((m, i) => (
      <div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
        <div className={`max-w-[85%] rounded-lg px-3 py-2 text-sm ${m.role === 'user' ? 'bg-primary text-primary-foreground' : m.role === 'tool' ? 'bg-amber-500/10 border border-amber-400/30 text-amber-100' : 'bg-card border border-white/10 shadow-sm text-foreground'}`}>
          <div className="mb-0.5 flex items-center gap-1.5">
            <span className="text-[10px] opacity-70 uppercase tracking-wider">{m.role === 'user' ? 'Me' : m.role === 'tool' ? 'Tool' : 'AI'}</span>
            {m.role === 'model' && typeof m.ttfbMs === 'number' && Number.isFinite(m.ttfbMs) && (
              <span className="rounded border border-cyan-300/40 bg-cyan-500/10 px-1.5 py-0.5 text-[10px] text-cyan-200">
                TTFB {Math.round(m.ttfbMs)}ms
              </span>
            )}
          </div>
          {m.text}
        </div>
      </div>
    ))}
    {isLoading && <div className="text-xs text-muted-foreground ml-2 animate-pulse">Thinking...</div>}
  </div>
);

// --- Debug Drawer Component ---
export const DebugDrawer: React.FC<{
  isOpen: boolean;
  onClose: () => void;
  assistant: Assistant;
  voices?: Voice[];
  llmModels?: LLMModel[];
  asrModels?: ASRModel[];
  tools?: Tool[];
  textTtsEnabled: boolean;
  sessionMetadataExtras?: Record<string, any>;
  onProtocolEvent?: (event: Record<string, any>) => void;
}> = ({
  isOpen,
  onClose,
  assistant,
  voices = [],
  llmModels = [],
  asrModels = [],
  tools = [],
  textTtsEnabled,
  sessionMetadataExtras,
  onProtocolEvent,
}) => {
  const TARGET_SAMPLE_RATE = 16000;
  const PCM_FRAME_BYTES = 640; // WS v1 fixed 20ms frame for 16k mono pcm_s16le
  const downsampleTo16k = (input: Float32Array, inputSampleRate: number): Float32Array => {
    if (inputSampleRate === TARGET_SAMPLE_RATE) return input;
    if (inputSampleRate < TARGET_SAMPLE_RATE) return input;
    const ratio = inputSampleRate / TARGET_SAMPLE_RATE;
    const outputLength = Math.max(1, Math.round(input.length / ratio));
    const output = new Float32Array(outputLength);
    let offsetInput = 0;
    for (let i = 0; i < outputLength; i += 1) {
      const nextOffsetInput = Math.min(input.length, Math.round((i + 1) * ratio));
      let accum = 0;
      let count = 0;
      for (let j = offsetInput; j < nextOffsetInput; j += 1) {
        accum += input[j];
        count += 1;
      }
      output[i] = count > 0 ? accum / count : input[Math.min(offsetInput, input.length - 1)] || 0;
      offsetInput = nextOffsetInput;
    }
    return output;
  };

  const float32ToPcm16 = (input: Float32Array): Int16Array => {
    const output = new Int16Array(input.length);
    for (let i = 0; i < input.length; i += 1) {
      const s = Math.max(-1, Math.min(1, input[i]));
      output[i] = s < 0 ? Math.round(s * 0x8000) : Math.round(s * 0x7fff);
    }
    return output;
  };

  const [mode, setMode] = useState<'text' | 'voice' | 'video'>('text');
  const [messages, setMessages] = useState<DebugTranscriptMessage[]>([]);
  const [inputText, setInputText] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [callStatus, setCallStatus] = useState<'idle' | 'calling' | 'active'>('idle');
  const [textPromptDialog, setTextPromptDialog] = useState<DebugTextPromptDialogState>({
    open: false,
    message: '',
    promptType: 'text',
  });
  const [choicePromptDialog, setChoicePromptDialog] = useState<DebugChoicePromptDialogState>({ open: false, question: '', options: [] });
  const textPromptDialogRef = useRef(textPromptDialog);
  const choicePromptDialogRef = useRef(choicePromptDialog);
  const promptDialogQueueRef = useRef<DebugPromptQueueItem[]>([]);
  const promptAudioRef = useRef<HTMLAudioElement | null>(null);
  const [textSessionStarted, setTextSessionStarted] = useState(false);
  const [wsStatus, setWsStatus] = useState<'disconnected' | 'connecting' | 'ready' | 'error'>('disconnected');
  const [wsError, setWsError] = useState('');
  const wsStatusRef = useRef<'disconnected' | 'connecting' | 'ready' | 'error'>('disconnected');
  const [resolvedConfigOpen, setResolvedConfigOpen] = useState(false);
  const [resolvedConfigView, setResolvedConfigView] = useState<string>('');
  const [captureConfigOpen, setCaptureConfigOpen] = useState(false);
  const [captureConfigView, setCaptureConfigView] = useState<string>('');
  const [settingsDrawerOpen, setSettingsDrawerOpen] = useState(false);
  const [dynamicVariables, setDynamicVariables] = useState<DynamicVariableEntry[]>([]);
  const [dynamicVariablesError, setDynamicVariablesError] = useState('');
  const dynamicVariableSeqRef = useRef(0);
  const wsUrl = useDebugPrefsStore((state) => state.wsUrl);
  const setWsUrl = useDebugPrefsStore((state) => state.setWsUrl);
  const aecEnabled = useDebugPrefsStore((state) => state.aecEnabled);
  const setAecEnabled = useDebugPrefsStore((state) => state.setAecEnabled);
  const nsEnabled = useDebugPrefsStore((state) => state.nsEnabled);
  const setNsEnabled = useDebugPrefsStore((state) => state.setNsEnabled);
  const agcEnabled = useDebugPrefsStore((state) => state.agcEnabled);
  const setAgcEnabled = useDebugPrefsStore((state) => state.setAgcEnabled);
  const clientToolEnabledMap = useDebugPrefsStore((state) => state.clientToolEnabledMap);
  const setClientToolEnabled = useDebugPrefsStore((state) => state.setClientToolEnabled);
  const hydrateClientToolDefaults = useDebugPrefsStore((state) => state.hydrateClientToolDefaults);
  const nextDynamicVariableId = () => {
    dynamicVariableSeqRef.current += 1;
    return `var_${dynamicVariableSeqRef.current}`;
  };
  const isDynamicVariablesLocked =
    wsStatus === 'connecting'
    || wsStatus === 'ready'
    || callStatus === 'calling'
    || callStatus === 'active'
    || textSessionStarted;
  const requiredTemplateVariableKeys = useMemo(() => {
    const keys = new Set<string>();
    const includeOpenerTemplate = assistant.generatedOpenerEnabled !== true;
    extractDynamicTemplateKeys(String(assistant.prompt || '')).forEach((key) => keys.add(key));
    if (includeOpenerTemplate) {
      extractDynamicTemplateKeys(String(assistant.opener || '')).forEach((key) => keys.add(key));
    }
    return Array.from(keys).sort();
  }, [assistant.generatedOpenerEnabled, assistant.opener, assistant.prompt]);
  const missingRequiredDynamicVariableKeys = useMemo(() => {
    const valuesByKey = new Map<string, string>();
    for (const row of dynamicVariables) {
      const key = row.key.trim();
      if (!key || valuesByKey.has(key)) continue;
      valuesByKey.set(key, row.value);
    }
    return requiredTemplateVariableKeys.filter((key) => {
      if (SYSTEM_DYNAMIC_VARIABLE_KEY_SET.has(key)) return false;
      if (!valuesByKey.has(key)) return true;
      return valuesByKey.get(key) === '';
    });
  }, [dynamicVariables, requiredTemplateVariableKeys]);
  
  // Media State
  const videoRef = useRef<HTMLVideoElement>(null);
  const streamRef = useRef<MediaStream | null>(null);
  const scrollRef = useRef<HTMLDivElement>(null);
  const wsRef = useRef<WebSocket | null>(null);
  const wsReadyRef = useRef(false);
  const pendingResolveRef = useRef<(() => void) | null>(null);
  const pendingRejectRef = useRef<((e: Error) => void) | null>(null);
  const submittedMetadataRef = useRef<Record<string, any> | null>(null);
  const assistantDraftIndexRef = useRef<number | null>(null);
  const assistantResponseIndexByIdRef = useRef<Map<string, number>>(new Map());
  const pendingTtfbByResponseIdRef = useRef<Map<string, number>>(new Map());
  const interruptedResponseIdsRef = useRef<Set<string>>(new Set());
  const interruptedDropNoticeKeysRef = useRef<Set<string>>(new Set());
  const audioCtxRef = useRef<AudioContext | null>(null);
  const playbackTimeRef = useRef<number>(0);
  const activeAudioSourcesRef = useRef<Set<AudioBufferSourceNode>>(new Set());
  const queuedAudioBuffersRef = useRef<AudioBuffer[]>([]);
  const queuedAudioDurationRef = useRef<number>(0);

  const PLAYBACK_INITIAL_BUFFER_SECONDS = 0.25;
  const PLAYBACK_MAX_AHEAD_SECONDS = 0.8;
  const PLAYBACK_SCHEDULE_LEAD_SECONDS = 0.04;

  const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
  const [selectedCamera, setSelectedCamera] = useState<string>('');
  const [selectedMic, setSelectedMic] = useState<string>('');
  const [isSwapped, setIsSwapped] = useState(false); 

  const micAudioCtxRef = useRef<AudioContext | null>(null);
  const micSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
  const micProcessorRef = useRef<ScriptProcessorNode | null>(null);
  const micGainRef = useRef<GainNode | null>(null);
  const micFrameBufferRef = useRef<Uint8Array>(new Uint8Array(0));
  const userDraftIndexRef = useRef<number | null>(null);
  const lastUserFinalRef = useRef<string>('');
  const debugVolumePercentRef = useRef<number>(50);
  const clientToolEnabledMapRef = useRef<Record<string, boolean>>(clientToolEnabledMap);
  const isClientToolEnabled = (toolId: string) => clientToolEnabledMap[normalizeToolId(toolId)] !== false;
  const isClientToolEnabledLive = (toolId: string) => clientToolEnabledMapRef.current[normalizeToolId(toolId)] !== false;
  const selectedToolSchemas = useMemo(() => {
    const ids = Array.from(new Set((assistant.tools || []).map((id) => normalizeToolId(id))));
    const byId = new Map(tools.map((t) => [normalizeToolId(t.id), { ...t, id: normalizeToolId(t.id) }]));
    return ids.map((id) => {
      const item = byId.get(id);
      const toolId = item?.id || id;
      const debugClientTool = DEBUG_CLIENT_TOOLS.find((tool) => tool.id === toolId);
      if (debugClientTool && !isClientToolEnabled(toolId)) {
        return null;
      }
      const isClientTool = debugClientTool ? true : (item?.category || 'query') === 'system';
      const waitForResponse = isClientTool
        ? (item?.waitForResponse ?? DEBUG_CLIENT_TOOL_WAIT_DEFAULTS[toolId] ?? false)
        : false;
      const parameterSchema = (item?.parameterSchema && typeof item.parameterSchema === 'object')
        ? item.parameterSchema
        : getDefaultToolParameters(toolId);
      const parameterDefaults = (item?.parameterDefaults && typeof item.parameterDefaults === 'object')
        ? item.parameterDefaults
        : undefined;
      return {
        type: 'function',
        executor: isClientTool ? 'client' : 'server',
        waitForResponse,
        ...(parameterDefaults && Object.keys(parameterDefaults).length > 0 ? { defaultArgs: parameterDefaults } : {}),
        function: {
          name: toolId,
          description: item?.description || item?.name || debugClientTool?.description || id,
          parameters: parameterSchema,
        },
      };
    }).filter(Boolean) as Array<Record<string, any>>;
  }, [assistant.tools, tools, clientToolEnabledMap]);

  const clearResponseTracking = () => {
    assistantDraftIndexRef.current = null;
    assistantResponseIndexByIdRef.current.clear();
    pendingTtfbByResponseIdRef.current.clear();
    interruptedResponseIdsRef.current.clear();
    interruptedDropNoticeKeysRef.current.clear();
  };

  const extractResponseId = (payload: any): string | undefined => {
    const responseIdRaw = payload?.data?.response_id ?? payload?.response_id ?? payload?.responseId;
    const responseId = String(responseIdRaw || '').trim();
    return responseId || undefined;
  };

  const noteInterruptedDrop = (responseId: string, kind: 'ttfb' | 'delta' | 'final') => {
    const key = `${responseId}:${kind}`;
    if (interruptedDropNoticeKeysRef.current.has(key)) return;
    interruptedDropNoticeKeysRef.current.add(key);
    if (interruptedDropNoticeKeysRef.current.size > 256) {
      const oldest = interruptedDropNoticeKeysRef.current.values().next().value as string | undefined;
      if (oldest) interruptedDropNoticeKeysRef.current.delete(oldest);
    }
    setMessages((prev) => [
      ...prev,
      {
        role: 'tool',
        text: `drop stale ${kind} from interrupted response ${responseId}`,
      },
    ]);
  };

  // Initialize
  useEffect(() => {
    if (isOpen) {
      if (mode === 'text') {
        clearResponseTracking();
        setMessages([]);
        setTextSessionStarted(false);
      } else {
        clearResponseTracking();
        setMessages([]);
        setCallStatus('idle');
      }
    } else {
        setMode('text');
        if (textPromptDialogRef.current.open) {
          closeTextPromptDialog('dismiss', { force: true, skipQueueAdvance: true });
        }
        if (choicePromptDialogRef.current.open) {
          closeChoicePromptDialog('dismiss', undefined, { force: true, skipQueueAdvance: true });
        }
        stopVoiceCapture();
        stopMedia();
        closeWs();
        stopPromptVoicePlayback();
        promptDialogQueueRef.current = [];
        setTextPromptDialog({ open: false, message: '', promptType: 'text' });
        setChoicePromptDialog({ open: false, question: '', options: [] });
        if (audioCtxRef.current) {
          void audioCtxRef.current.close();
          audioCtxRef.current = null;
        }
        setSettingsDrawerOpen(false);
        setIsSwapped(false);
        setCallStatus('idle');
    }
  }, [isOpen, assistant, mode]);

  useEffect(() => {
    hydrateClientToolDefaults(DEBUG_CLIENT_TOOLS.map((tool) => tool.id));
  }, [hydrateClientToolDefaults]);

  useEffect(() => {
    wsStatusRef.current = wsStatus;
  }, [wsStatus]);

  useEffect(() => {
    textPromptDialogRef.current = textPromptDialog;
  }, [textPromptDialog]);

  useEffect(() => {
    choicePromptDialogRef.current = choicePromptDialog;
  }, [choicePromptDialog]);

  useEffect(() => {
    dynamicVariableSeqRef.current = 0;
    setDynamicVariables([]);
    setDynamicVariablesError('');
  }, [assistant.id, isOpen]);

  useEffect(() => {
    clientToolEnabledMapRef.current = clientToolEnabledMap;
  }, [clientToolEnabledMap]);

  // Auto-scroll logic
  useEffect(() => {
      if (scrollRef.current) {
          scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
      }
  }, [messages, mode]);

  // Fetch Devices
  useEffect(() => {
    if (isOpen && (mode === 'video' || mode === 'voice')) {
        const getDevices = async () => {
            let permissionStream: MediaStream | null = null;
            try {
                permissionStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: mode === 'video' });
                const dev = await navigator.mediaDevices.enumerateDevices();
                setDevices(dev);
                const cams = dev.filter(d => d.kind === 'videoinput');
                const mics = dev.filter(d => d.kind === 'audioinput');
                if (cams.length > 0 && !selectedCamera) setSelectedCamera(cams[0].deviceId);
                if (mics.length > 0 && !selectedMic) setSelectedMic(mics[0].deviceId);
            } catch (e) {
                console.error("Error enumerating devices", e);
            } finally {
                permissionStream?.getTracks().forEach((track) => track.stop());
            }
        };
        getDevices();
    }
  }, [isOpen, mode]);

  const stopMedia = () => {
      if (streamRef.current) {
        streamRef.current.getTracks().forEach(track => track.stop());
        streamRef.current = null;
      }
  };

  const stopVoiceCapture = () => {
    if (micProcessorRef.current) {
      micProcessorRef.current.onaudioprocess = null;
      try {
        micProcessorRef.current.disconnect();
      } catch {
        // no-op
      }
      micProcessorRef.current = null;
    }
    if (micSourceRef.current) {
      try {
        micSourceRef.current.disconnect();
      } catch {
        // no-op
      }
      micSourceRef.current = null;
    }
    if (micGainRef.current) {
      try {
        micGainRef.current.disconnect();
      } catch {
        // no-op
      }
      micGainRef.current = null;
    }
    if (micAudioCtxRef.current) {
      void micAudioCtxRef.current.close();
      micAudioCtxRef.current = null;
    }
    micFrameBufferRef.current = new Uint8Array(0);
    setCaptureConfigView('');
    stopMedia();
  };

  const sendFramedMicAudio = (pcm16: Int16Array) => {
    const ws = wsRef.current;
    if (!ws || ws.readyState !== WebSocket.OPEN || !wsReadyRef.current) return;
    if (pcm16.byteLength <= 0) return;

    const chunkBytes = new Uint8Array(pcm16.buffer, pcm16.byteOffset, pcm16.byteLength);
    const pending = micFrameBufferRef.current;
    const merged = new Uint8Array(pending.length + chunkBytes.length);
    merged.set(pending, 0);
    merged.set(chunkBytes, pending.length);

    let offset = 0;
    while (merged.length - offset >= PCM_FRAME_BYTES) {
      ws.send(merged.subarray(offset, offset + PCM_FRAME_BYTES));
      offset += PCM_FRAME_BYTES;
    }

    micFrameBufferRef.current = offset >= merged.length ? new Uint8Array(0) : merged.slice(offset);
  };

  const buildMicConstraints = (): MediaTrackConstraints => ({
    deviceId: selectedMic ? { exact: selectedMic } : undefined,
    echoCancellation: aecEnabled,
    noiseSuppression: nsEnabled,
    autoGainControl: agcEnabled,
    channelCount: 1,
    sampleRate: TARGET_SAMPLE_RATE,
  });

  const startVoiceCapture = async () => {
    stopVoiceCapture();
    const requestedConstraints = buildMicConstraints();
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: requestedConstraints,
      video: false,
    });
    streamRef.current = stream;
    const track = stream.getAudioTracks()[0];
    if (track) {
      console.log('Voice capture settings', track.getSettings());
      setCaptureConfigView(
        JSON.stringify(
          {
            requested: requestedConstraints,
            applied: track.getSettings(),
            capabilities: typeof track.getCapabilities === 'function' ? track.getCapabilities() : undefined,
          },
          null,
          2
        )
      );
    }

    const ctx = new AudioContext();
    if (ctx.state === 'suspended') {
      await ctx.resume();
    }
    micAudioCtxRef.current = ctx;
    const source = ctx.createMediaStreamSource(stream);
    const processor = ctx.createScriptProcessor(4096, 1, 1);
    const silentGain = ctx.createGain();
    silentGain.gain.value = 0;

    source.connect(processor);
    processor.connect(silentGain);
    silentGain.connect(ctx.destination);

    processor.onaudioprocess = (event) => {
      if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN || !wsReadyRef.current) return;
      const inChannel = event.inputBuffer.getChannelData(0);
      const downsampled = downsampleTo16k(inChannel, event.inputBuffer.sampleRate);
      const pcm16 = float32ToPcm16(downsampled);
      sendFramedMicAudio(pcm16);
    };

    micSourceRef.current = source;
    micProcessorRef.current = processor;
    micGainRef.current = silentGain;
  };

  const ensureAudioContext = async () => {
    if (!audioCtxRef.current) {
      audioCtxRef.current = new AudioContext();
      playbackTimeRef.current = audioCtxRef.current.currentTime;
    }
    if (audioCtxRef.current.state === 'suspended') {
      await audioCtxRef.current.resume();
    }
    return audioCtxRef.current;
  };

  const clearPlaybackQueue = () => {
    const ctx = audioCtxRef.current;
    const now = ctx ? ctx.currentTime : 0;
    playbackTimeRef.current = now;
    queuedAudioBuffersRef.current = [];
    queuedAudioDurationRef.current = 0;
  };

  const stopPlaybackImmediately = () => {
    activeAudioSourcesRef.current.forEach((source) => {
      try {
        source.stop();
      } catch {
        // no-op
      }
      source.disconnect();
    });
    activeAudioSourcesRef.current.clear();
    clearPlaybackQueue();
  };

  const emitClientToolResult = (resultPayload: any, toolDisplayName?: string) => {
    const ws = wsRef.current;
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(
        JSON.stringify({
          type: 'tool_call.results',
          results: [resultPayload],
        })
      );
    }
    const statusCode = Number(resultPayload?.status?.code || 500);
    const statusMessage = String(resultPayload?.status?.message || 'error');
    const displayName = toolDisplayName || String(resultPayload?.name || 'unknown_tool');
    const resultText =
      statusCode === 200 && typeof resultPayload?.output?.result === 'number'
        ? `result ${displayName} = ${resultPayload.output.result}`
        : `result ${displayName} status=${statusCode} ${statusMessage}`;
    setMessages((prev) => [
      ...prev,
      {
        role: 'tool',
        text: resultText,
      },
    ]);
  };

  const stopPromptVoicePlayback = () => {
    if (promptAudioRef.current) {
      try {
        promptAudioRef.current.pause();
      } catch {
        // no-op
      }
      promptAudioRef.current = null;
    }
    if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
      window.speechSynthesis.cancel();
    }
  };

  const playPromptVoice = async (text: string) => {
    const phrase = String(text || '').trim();
    if (!phrase) return;
    stopPromptVoicePlayback();

    const canUseAssistantTts = assistant.voiceOutputEnabled !== false && Boolean(assistant.voice);
    if (canUseAssistantTts) {
      const selectedVoice = voices.find((item) => item.id === assistant.voice);
      if (selectedVoice) {
        try {
          const audioUrl = await previewVoice(selectedVoice.id, phrase, assistant.speed);
          const audio = new Audio(audioUrl);
          promptAudioRef.current = audio;
          audio.onended = () => {
            if (promptAudioRef.current === audio) {
              promptAudioRef.current = null;
            }
          };
          audio.onerror = () => {
            if (promptAudioRef.current === audio) {
              promptAudioRef.current = null;
            }
          };
          await audio.play();
          return;
        } catch (err) {
          console.warn('Assistant TTS preview failed, falling back to speechSynthesis', err);
        }
      }
    }

    if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
      const utterance = new SpeechSynthesisUtterance(phrase);
      utterance.lang = assistant.language === 'en' ? 'en-US' : 'zh-CN';
      window.speechSynthesis.cancel();
      window.speechSynthesis.speak(utterance);
    }
  };

  const hasActivePromptDialog = () => textPromptDialogRef.current.open || choicePromptDialogRef.current.open;

  const activatePromptDialog = (item: DebugPromptQueueItem) => {
    if (item.kind === 'text') {
      const nextVoiceText = String(item.payload.voiceText || '').trim();
      setTextPromptDialog({
        open: true,
        message: item.payload.message,
        pendingResult: item.payload.pendingResult,
        promptType: item.payload.promptType || 'text',
        voiceText: nextVoiceText || undefined,
      });
      if (nextVoiceText) {
        void playPromptVoice(nextVoiceText);
      }
      return;
    }
    const nextVoiceText = String(item.payload.voiceText || '').trim();
    setChoicePromptDialog({
      open: true,
      question: item.payload.question,
      options: item.payload.options,
      pendingResult: item.payload.pendingResult,
      requireSelection: item.payload.requireSelection === true,
      voiceText: nextVoiceText || undefined,
    });
    if (nextVoiceText) {
      void playPromptVoice(nextVoiceText);
    }
  };

  const enqueuePromptDialog = (item: DebugPromptQueueItem) => {
    if (hasActivePromptDialog()) {
      promptDialogQueueRef.current.push(item);
      return;
    }
    activatePromptDialog(item);
  };

  const openNextPromptDialog = (force = false) => {
    if (!force && hasActivePromptDialog()) return;
    const next = promptDialogQueueRef.current.shift();
    if (!next) return;
    activatePromptDialog(next);
  };

  const closeTextPromptDialog = (action: 'confirm' | 'dismiss', opts?: { force?: boolean; skipQueueAdvance?: boolean }) => {
    const snapshot = textPromptDialogRef.current;
    if (!snapshot.open && !opts?.force) return;
    const pending = snapshot?.pendingResult;
    const message = snapshot?.message || '';
    const promptType = snapshot?.promptType === 'voice' ? 'voice' : 'text';
    stopPromptVoicePlayback();
    setTextPromptDialog({ open: false, message: '', promptType: 'text' });
    if (pending?.waitForResponse) {
      emitClientToolResult(
        {
          tool_call_id: pending.toolCallId,
          name: pending.toolName,
          output: {
            message: promptType === 'voice' ? 'voice_prompt_closed' : 'text_prompt_closed',
            action,
            msg: message,
            prompt_type: promptType,
          },
          status: { code: 200, message: 'ok' },
        },
        pending.toolDisplayName
      );
    }
    if (!opts?.skipQueueAdvance) {
      openNextPromptDialog(true);
    }
  };

  const closeChoicePromptDialog = (
    action: 'select' | 'dismiss',
    selectedOption?: DebugChoicePromptOption,
    opts?: { force?: boolean; skipQueueAdvance?: boolean }
  ) => {
    const snapshot = choicePromptDialogRef.current;
    if (!snapshot.open && !opts?.force) return;
    if (snapshot.requireSelection && action !== 'select' && !opts?.force) {
      return;
    }
    const pending = snapshot?.pendingResult;
    const question = snapshot?.question || '';
    const options = snapshot?.options || [];
    stopPromptVoicePlayback();
    setChoicePromptDialog({ open: false, question: '', options: [] });
    if (pending?.waitForResponse) {
      emitClientToolResult(
        {
          tool_call_id: pending.toolCallId,
          name: pending.toolName,
          output: {
            message: action === 'select' ? 'choice_selected' : 'choice_dismissed',
            action,
            question,
            option: selectedOption
              ? {
                  id: selectedOption.id,
                  label: selectedOption.label,
                  value: selectedOption.value,
                }
              : null,
            options,
          },
          status: { code: 200, message: 'ok' },
        },
        pending.toolDisplayName
      );
    }
    if (!opts?.skipQueueAdvance) {
      openNextPromptDialog(true);
    }
  };

  const scheduleQueuedPlayback = (ctx: AudioContext) => {
    const queue = queuedAudioBuffersRef.current;
    if (queue.length === 0) return;

    const now = ctx.currentTime;
    if (playbackTimeRef.current < now) {
      playbackTimeRef.current = now;
    }

    const hasActivePlayback = activeAudioSourcesRef.current.size > 0;
    const minBufferSeconds = hasActivePlayback
      ? 0
      : PLAYBACK_INITIAL_BUFFER_SECONDS;

    if (queuedAudioDurationRef.current < minBufferSeconds) {
      return;
    }

    while (queue.length > 0 && (playbackTimeRef.current - now) < PLAYBACK_MAX_AHEAD_SECONDS) {
      const audioBuffer = queue.shift();
      if (!audioBuffer) break;
      queuedAudioDurationRef.current = Math.max(0, queuedAudioDurationRef.current - audioBuffer.duration);

      const source = ctx.createBufferSource();
      source.buffer = audioBuffer;
      source.connect(ctx.destination);
      activeAudioSourcesRef.current.add(source);
      source.onended = () => {
        activeAudioSourcesRef.current.delete(source);
        try {
          source.disconnect();
        } catch {
          // no-op
        }
        scheduleQueuedPlayback(ctx);
      };

      const startAt = Math.max(ctx.currentTime + PLAYBACK_SCHEDULE_LEAD_SECONDS, playbackTimeRef.current);
      source.start(startAt);
      playbackTimeRef.current = startAt + audioBuffer.duration;
    }
  };

  const playPcm16Chunk = async (pcmBuffer: ArrayBuffer) => {
    if (!textTtsEnabled) return;
    const ctx = await ensureAudioContext();
    const int16 = new Int16Array(pcmBuffer);
    if (int16.length === 0) return;

    const float32 = new Float32Array(int16.length);
    for (let i = 0; i < int16.length; i += 1) {
      float32[i] = int16[i] / 32768;
    }

    const sampleRate = 16000;
    const audioBuffer = ctx.createBuffer(1, float32.length, sampleRate);
    audioBuffer.copyToChannel(float32, 0);
    queuedAudioBuffersRef.current.push(audioBuffer);
    queuedAudioDurationRef.current += audioBuffer.duration;
    scheduleQueuedPlayback(ctx);
  };

  useEffect(() => {
    const handleStream = async () => {
      if (isOpen && mode === 'video' && callStatus === 'active') {
        try {
          stopMedia();
          const constraints = {
              video: selectedCamera ? { deviceId: { exact: selectedCamera } } : true,
              audio: selectedMic ? { deviceId: { exact: selectedMic } } : true
          };
          const stream = await navigator.mediaDevices.getUserMedia(constraints);
          streamRef.current = stream;
          if (videoRef.current) {
            videoRef.current.srcObject = stream;
          }
        } catch (err) {
          console.error("Failed to access camera/mic:", err);
        }
      } else if (callStatus !== 'active') {
        stopMedia();
      }
    };

    handleStream();
    return () => stopMedia();
  }, [mode, isOpen, selectedCamera, selectedMic, callStatus]);

  const handleCall = () => {
    if (mode !== 'voice') {
      setCallStatus('calling');
      setTimeout(() => {
          setCallStatus('active');
          setMessages([{ role: 'model', text: assistant.opener || "Hello!" }]);
      }, 1500);
      return;
    }
    const launchVoice = async () => {
      try {
        setCallStatus('calling');
        clearResponseTracking();
        setMessages([]);
        lastUserFinalRef.current = '';
        setWsError('');
        setDynamicVariablesError('');
        closeWs();
        if (textTtsEnabled) await ensureAudioContext();
        // Start mic capture before session.start so barge-in works from opener start.
        await startVoiceCapture();
        await ensureWsSession();
        setCallStatus('active');
      } catch (e) {
        console.error(e);
        stopVoiceCapture();
        setCallStatus('idle');
        const err = e as Error & { __dynamicVariables?: boolean };
        if (err.__dynamicVariables) {
          setWsStatus('disconnected');
          return;
        }
        setWsStatus('error');
        setWsError(err?.message || 'Failed to start voice call');
      }
    };
    void launchVoice();
  };

  const handleHangup = () => {
    if (textPromptDialog.open) {
      closeTextPromptDialog('dismiss', { force: true, skipQueueAdvance: true });
    }
    if (choicePromptDialog.open) {
      closeChoicePromptDialog('dismiss', undefined, { force: true, skipQueueAdvance: true });
    }
    stopPromptVoicePlayback();
    promptDialogQueueRef.current = [];
    stopVoiceCapture();
    stopMedia();
    closeWs();
    setCallStatus('idle');
    clearResponseTracking();
    setMessages([]);
    setTextPromptDialog({ open: false, message: '', promptType: 'text' });
    setChoicePromptDialog({ open: false, question: '', options: [] });
    lastUserFinalRef.current = '';
    setIsLoading(false);
  };

  const handleSend = async () => {
    if (!inputText.trim()) return;
    const userMsg = inputText;
    assistantDraftIndexRef.current = null;
    setMessages(prev => [...prev, { role: 'user', text: userMsg }]);
    setInputText('');
    setIsLoading(true);

    try {
      setDynamicVariablesError('');
      if (mode === 'text') {
        if (textTtsEnabled) await ensureAudioContext();
        await ensureWsSession();
        // Interrupt any in-flight response/audio before sending new user utterance.
        if (wsRef.current?.readyState === WebSocket.OPEN) {
          wsRef.current.send(JSON.stringify({ type: 'response.cancel', graceful: false }));
        }
        stopPlaybackImmediately();
        wsRef.current?.send(JSON.stringify({ type: 'input.text', text: userMsg }));
      } else if (mode === 'voice') {
        await ensureWsSession();
        wsRef.current?.send(JSON.stringify({ type: 'input.text', text: userMsg }));
      } else {
        setTimeout(() => {
          setMessages(prev => [...prev, { role: 'model', text: `[Mock Response]: Received "${userMsg}"` }]);
          setIsLoading(false);
        }, 1000);
      }
    } catch (e) {
      console.error(e);
      const err = e as Error & { __dynamicVariables?: boolean };
      if (err.__dynamicVariables) {
        setIsLoading(false);
        return;
      }
      const errMessage = err?.message || 'Failed to connect to AI service.';
      setMessages(prev => [...prev, { role: 'model', text: `Error: ${errMessage}` }]);
      setWsError(errMessage);
      setIsLoading(false);
    } finally {
      if (mode !== 'text') setIsLoading(false);
    }
  };

  const handleTextLaunch = async () => {
    try {
      setWsError('');
      setDynamicVariablesError('');
      // Start every text debug run as a fresh session transcript.
      clearResponseTracking();
      setMessages([]);
      lastUserFinalRef.current = '';
      // Force a fresh WS session so updated assistant runtime config
      // (voice/model/provider/speed) is applied on session.start.
      closeWs();
      if (textTtsEnabled) await ensureAudioContext();
      await ensureWsSession();
      setTextSessionStarted(true);
    } catch (e) {
      console.error(e);
      const err = e as Error & { __dynamicVariables?: boolean };
      if (err.__dynamicVariables) {
        setWsStatus('disconnected');
        setTextSessionStarted(false);
        return;
      }
      setWsStatus('error');
      setWsError(err?.message || 'Failed to connect');
      setTextSessionStarted(false);
    }
  };

  const addDynamicVariableRow = () => {
    if (isDynamicVariablesLocked) return;
    setDynamicVariablesError('');
    setDynamicVariables((prev) => {
      if (prev.length >= DYNAMIC_VARIABLE_MAX_ITEMS) return prev;
      return [...prev, { id: nextDynamicVariableId(), key: '', value: '' }];
    });
  };

  const updateDynamicVariableRow = (rowId: string, field: 'key' | 'value', value: string) => {
    if (isDynamicVariablesLocked) return;
    setDynamicVariablesError('');
    setDynamicVariables((prev) => prev.map((item) => (item.id === rowId ? { ...item, [field]: value } : item)));
  };

  const removeDynamicVariableRow = (rowId: string) => {
    if (isDynamicVariablesLocked) return;
    setDynamicVariablesError('');
    setDynamicVariables((prev) => prev.filter((item) => item.id !== rowId));
  };

  const importDynamicVariablesFromPlaceholders = () => {
    if (isDynamicVariablesLocked) return;
    setDynamicVariablesError('');
    setDynamicVariables((prev) => {
      if (!requiredTemplateVariableKeys.length || prev.length >= DYNAMIC_VARIABLE_MAX_ITEMS) {
        return prev;
      }
      const existingKeys = new Set(
        prev
          .map((item) => item.key.trim())
          .filter((key) => key !== '')
      );
      const next = [...prev];
      for (const key of requiredTemplateVariableKeys) {
        if (next.length >= DYNAMIC_VARIABLE_MAX_ITEMS) break;
        if (existingKeys.has(key)) continue;
        next.push({
          id: nextDynamicVariableId(),
          key,
          value: '',
        });
        existingKeys.add(key);
      }
      return next;
    });
  };

  const createDynamicVariablesError = (message: string): Error & { __dynamicVariables?: boolean } => {
    const error = new Error(message) as Error & { __dynamicVariables?: boolean };
    error.__dynamicVariables = true;
    return error;
  };

  const METADATA_OVERRIDE_WHITELIST = new Set([
    'firstTurnMode',
    'greeting',
    'generatedOpenerEnabled',
    'manualOpenerToolCalls',
    'systemPrompt',
    'output',
    'bargeIn',
    'knowledge',
    'knowledgeBaseId',
    'openerAudio',
    'tools',
  ]);
  const METADATA_FORBIDDEN_SECRET_TOKENS = ['apikey', 'token', 'secret', 'password', 'authorization'];
  const isPlainObject = (value: unknown): value is Record<string, any> => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
  const isForbiddenSecretKey = (key: string): boolean => {
    const normalized = key.toLowerCase().replace(/[_-]/g, '');
    return METADATA_FORBIDDEN_SECRET_TOKENS.some((token) => normalized.includes(token));
  };
  const stripForbiddenSecretKeysDeep = (value: any): any => {
    if (Array.isArray(value)) return value.map(stripForbiddenSecretKeysDeep);
    if (!isPlainObject(value)) return value;
    return Object.entries(value).reduce<Record<string, any>>((acc, [key, nested]) => {
      if (isForbiddenSecretKey(key)) return acc;
      acc[key] = stripForbiddenSecretKeysDeep(nested);
      return acc;
    }, {});
  };
  const sanitizeMetadataForWs = (raw: unknown): Record<string, any> => {
    if (!isPlainObject(raw)) return { overrides: {} };
    const sanitized: Record<string, any> = { overrides: {} };

    if (typeof raw.channel === 'string' && raw.channel.trim()) {
      sanitized.channel = raw.channel.trim();
    }
    if (typeof raw.source === 'string' && raw.source.trim()) {
      sanitized.source = raw.source.trim();
    }
    if (isPlainObject(raw.history) && raw.history.userId !== undefined) {
      sanitized.history = { userId: raw.history.userId };
    }
    if (isPlainObject(raw.dynamicVariables)) {
      sanitized.dynamicVariables = raw.dynamicVariables;
    }
    if (isPlainObject(raw.overrides)) {
      const overrides = Object.entries(raw.overrides).reduce<Record<string, any>>((acc, [key, value]) => {
        if (!METADATA_OVERRIDE_WHITELIST.has(key)) return acc;
        if (isForbiddenSecretKey(key)) return acc;
        acc[key] = stripForbiddenSecretKeysDeep(value);
        return acc;
      }, {});
      sanitized.overrides = overrides;
    }
    return sanitized;
  };

  const formatRuntimeConfigDebugView = (args: {
    submittedSessionStartMetadata?: Record<string, any> | null;
    wsResolvedConfig?: Record<string, any> | null;
  }): string => {
    const hasWsResolved = Boolean(args.wsResolvedConfig && Object.keys(args.wsResolvedConfig).length > 0);
    const payload: Record<string, any> = {
      protocol: 'ws_v1',
      wsConfigResolvedEvent: hasWsResolved ? 'received' : 'not_emitted',
      note: hasWsResolved
        ? 'Optional ws config.resolved event received.'
        : 'ws config.resolved is optional and disabled by default. Showing local session.start metadata preview.',
    };
    if (args.submittedSessionStartMetadata) {
      payload.submittedSessionStartMetadata = args.submittedSessionStartMetadata;
    }
    if (hasWsResolved) {
      payload.wsResolvedConfig = args.wsResolvedConfig;
    }
    return JSON.stringify(payload, null, 2);
  };

  const buildDynamicVariablesPayload = (): { variables: Record<string, string>; error?: string } => {
    const variables: Record<string, string> = {};
    const nonEmptyRows = dynamicVariables
      .map((row, index) => ({ ...row, index, key: row.key.trim() }))
      .filter((row) => row.key !== '' || row.value !== '');

    if (nonEmptyRows.length > DYNAMIC_VARIABLE_MAX_ITEMS) {
      return {
        variables,
        error: `Dynamic variable count cannot exceed ${DYNAMIC_VARIABLE_MAX_ITEMS}.`,
      };
    }

    for (const row of nonEmptyRows) {
      if (!row.key) {
        return {
          variables,
          error: `Dynamic variable row ${row.index + 1} is missing key.`,
        };
      }
      if (!DYNAMIC_VARIABLE_KEY_RE.test(row.key)) {
        return {
          variables,
          error: `Invalid dynamic variable key "${row.key}". Use letters, digits, underscore and start with a letter/underscore.`,
        };
      }
      if (SYSTEM_DYNAMIC_VARIABLE_KEY_SET.has(row.key)) {
        continue;
      }
      if (row.value === '') {
        return {
          variables,
          error: `Dynamic variable "${row.key}" is missing value.`,
        };
      }
      if (row.value.length > DYNAMIC_VARIABLE_MAX_VALUE_LENGTH) {
        return {
          variables,
          error: `Dynamic variable "${row.key}" exceeds ${DYNAMIC_VARIABLE_MAX_VALUE_LENGTH} characters.`,
        };
      }
      if (Object.prototype.hasOwnProperty.call(variables, row.key)) {
        return {
          variables,
          error: `Duplicate dynamic variable key "${row.key}".`,
        };
      }
      variables[row.key] = row.value;
    }

    const missingTemplateKeys = requiredTemplateVariableKeys.filter((key) => {
      if (SYSTEM_DYNAMIC_VARIABLE_KEY_SET.has(key)) return false;
      return !Object.prototype.hasOwnProperty.call(variables, key);
    });
    if (missingTemplateKeys.length > 0) {
      return {
        variables,
        error: `Missing required dynamic variables: ${missingTemplateKeys.join(', ')}`,
      };
    }

    return { variables };
  };

  const buildLocalResolvedRuntime = () => {
    const warnings: string[] = [];
    const ttsEnabled = Boolean(textTtsEnabled);
    const generatedOpenerEnabled = assistant.generatedOpenerEnabled === true;
    const normalizedManualCalls = normalizeManualOpenerToolCallsForRuntime(assistant.manualOpenerToolCalls, { strictJson: true });
    if (normalizedManualCalls.error) {
      setDynamicVariablesError(normalizedManualCalls.error);
      throw createDynamicVariablesError(normalizedManualCalls.error);
    }
    const knowledgeBaseId = String(assistant.knowledgeBaseId || '').trim();
    const knowledge = knowledgeBaseId
      ? { enabled: true, kbId: knowledgeBaseId, nResults: 5 }
      : { enabled: false };

    const localResolved = {
      warnings,
      sessionStartMetadata: {
        overrides: {
          output: {
            mode: ttsEnabled ? 'audio' : 'text',
          },
          systemPrompt: assistant.prompt || '',
          firstTurnMode: assistant.firstTurnMode || 'bot_first',
          greeting: generatedOpenerEnabled ? '' : (assistant.opener || ''),
          generatedOpenerEnabled,
          manualOpenerToolCalls: generatedOpenerEnabled ? [] : normalizedManualCalls.calls,
          bargeIn: {
            enabled: assistant.botCannotBeInterrupted !== true,
            minDurationMs: Math.max(0, Number(assistant.interruptionSensitivity ?? 180)),
          },
          knowledgeBaseId,
          knowledge,
          tools: selectedToolSchemas,
        },
        history: {
          userId: 1,
        },
        source: 'web_debug',
      },
    };

    return localResolved;
  };

  const fetchRuntimeMetadata = async (): Promise<Record<string, any>> => {
    const dynamicVariablesResult = buildDynamicVariablesPayload();
    if (dynamicVariablesResult.error) {
      setDynamicVariablesError(dynamicVariablesResult.error);
      throw createDynamicVariablesError(dynamicVariablesResult.error);
    }
    setDynamicVariablesError('');
    const localResolved = buildLocalResolvedRuntime();
    const mergedMetadata: Record<string, any> = sanitizeMetadataForWs({
      ...localResolved.sessionStartMetadata,
      ...(sessionMetadataExtras || {}),
    });
    if (Object.keys(dynamicVariablesResult.variables).length > 0) {
      mergedMetadata.dynamicVariables = dynamicVariablesResult.variables;
    }
    if (!mergedMetadata.channel) {
      mergedMetadata.channel = 'web_debug';
    }
    submittedMetadataRef.current = mergedMetadata;
    setResolvedConfigView(
      formatRuntimeConfigDebugView({
        submittedSessionStartMetadata: mergedMetadata,
      })
    );
    return mergedMetadata;
  };

  const closeWs = () => {
    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({ type: 'session.stop', reason: 'debug_drawer_closed' }));
    }
    wsRef.current?.close();
    wsRef.current = null;
    wsReadyRef.current = false;
    pendingResolveRef.current = null;
    pendingRejectRef.current = null;
    clearResponseTracking();
    userDraftIndexRef.current = null;
    lastUserFinalRef.current = '';
    micFrameBufferRef.current = new Uint8Array(0);
    stopPromptVoicePlayback();
    promptDialogQueueRef.current = [];
    setTextPromptDialog({ open: false, message: '', promptType: 'text' });
    setChoicePromptDialog({ open: false, question: '', options: [] });
    setTextSessionStarted(false);
    stopPlaybackImmediately();
    if (isOpen) setWsStatus('disconnected');
  };

  const buildSessionWsUrl = () => {
    const base = wsUrl.trim();
    if (!base) return '';
    try {
      const parsed = new URL(base);
      parsed.searchParams.set('assistant_id', assistant.id);
      return parsed.toString();
    } catch {
      try {
        const parsed = new URL(base, window.location.href);
        parsed.searchParams.set('assistant_id', assistant.id);
        return parsed.toString();
      } catch {
        return base;
      }
    }
  };

  const ensureWsSession = async () => {
    if (wsRef.current && wsReadyRef.current && wsRef.current.readyState === WebSocket.OPEN) {
      return;
    }

    if (wsRef.current && wsRef.current.readyState === WebSocket.CONNECTING) {
      await new Promise<void>((resolve, reject) => {
        pendingResolveRef.current = resolve;
        pendingRejectRef.current = reject;
      });
      return;
    }

    const metadata = await fetchRuntimeMetadata();
    const sessionWsUrl = buildSessionWsUrl();
    setWsStatus('connecting');
    setWsError('');

    await new Promise<void>((resolve, reject) => {
      pendingResolveRef.current = resolve;
      pendingRejectRef.current = reject;
      const ws = new WebSocket(sessionWsUrl);
      ws.binaryType = 'arraybuffer';
      wsRef.current = ws;

      ws.onopen = () => {
        ws.send(
          JSON.stringify({
            type: 'session.start',
            audio: { encoding: 'pcm_s16le', sample_rate_hz: 16000, channels: 1 },
            metadata,
          })
        );
      };

      ws.onmessage = (event) => {
        if (event.data instanceof ArrayBuffer) {
          void playPcm16Chunk(event.data);
          return;
        }
        if (event.data instanceof Blob) {
          void event.data.arrayBuffer().then((buf) => playPcm16Chunk(buf));
          return;
        }
        if (typeof event.data !== 'string') return;
        let payload: any;
        try {
          payload = JSON.parse(event.data);
        } catch {
          return;
        }

        const type = payload?.type;
        if (onProtocolEvent) {
          onProtocolEvent(payload);
        }
        if (type === 'output.audio.start') {
          // New utterance audio starts: cancel old queued/playing audio to avoid overlap.
          stopPlaybackImmediately();
          return;
        }

        if (type === 'response.interrupted') {
          const interruptedResponseId = extractResponseId(payload);
          if (interruptedResponseId) {
            interruptedResponseIdsRef.current.add(interruptedResponseId);
            if (interruptedResponseIdsRef.current.size > 64) {
              const oldest = interruptedResponseIdsRef.current.values().next().value as string | undefined;
              if (oldest) interruptedResponseIdsRef.current.delete(oldest);
            }
          }
          assistantDraftIndexRef.current = null;
          setIsLoading(false);
          stopPlaybackImmediately();
          return;
        }

        if (type === 'metrics.ttfb') {
          const maybeTtfb = Number(payload?.latencyMs ?? payload?.data?.latencyMs);
          if (!Number.isFinite(maybeTtfb) || maybeTtfb < 0) return;
          const ttfbMs = Math.round(maybeTtfb);
          const responseId = extractResponseId(payload);
          if (responseId && interruptedResponseIdsRef.current.has(responseId)) {
            noteInterruptedDrop(responseId, 'ttfb');
            return;
          }
          if (responseId) {
            const indexed = assistantResponseIndexByIdRef.current.get(responseId);
            if (typeof indexed === 'number') {
              setMessages((prev) => {
                if (!prev[indexed] || prev[indexed].role !== 'model') return prev;
                const next = [...prev];
                next[indexed] = { ...next[indexed], ttfbMs };
                return next;
              });
            } else {
              pendingTtfbByResponseIdRef.current.set(responseId, ttfbMs);
            }
            return;
          }
          setMessages((prev) => {
            for (let i = prev.length - 1; i >= 0; i -= 1) {
              if (prev[i]?.role === 'model') {
                const next = [...prev];
                next[i] = { ...next[i], ttfbMs };
                return next;
              }
            }
            return prev;
          });
          return;
        }

        if (type === 'assistant.tool_call') {
          const toolCall = payload?.tool_call || {};
          const toolCallId = String(payload?.tool_call_id || toolCall?.id || '').trim();
          const toolName = normalizeToolId(toolCall?.function?.name || toolCall?.name || 'unknown_tool');
          const toolDisplayName = String(payload?.tool_display_name || toolCall?.displayName || toolName);
          const executor = String(toolCall?.executor || 'server').toLowerCase();
          const rawArgs = String(toolCall?.function?.arguments || '');
          const argText = rawArgs.length > 160 ? `${rawArgs.slice(0, 160)}...` : rawArgs;
          setMessages((prev) => [
            ...prev,
            {
              role: 'tool',
              text: `call ${toolDisplayName} executor=${executor}${argText ? ` args=${argText}` : ''}`,
            },
          ]);
          if (executor === 'client' && toolCallId && ws.readyState === WebSocket.OPEN) {
            let parsedArgs: Record<string, any> = {};
            if (rawArgs) {
              try {
                const candidate = JSON.parse(rawArgs);
                parsedArgs = candidate && typeof candidate === 'object' ? candidate : {};
              } catch {
                parsedArgs = {};
              }
            }
            const waitForResponseRaw = Boolean(
              payload?.wait_for_response ?? toolCall?.wait_for_response ?? toolCall?.waitForResponse ?? false
            );
            const waitForResponse =
              toolName === 'voice_choice_prompt' || toolName === 'text_choice_prompt' ? true : waitForResponseRaw;
            const resultPayload: any = {
              tool_call_id: toolCallId,
              name: toolName,
              output: { message: `Unhandled client tool '${toolName}'` },
              status: { code: 501, message: 'not_implemented' },
            };
            if (DEBUG_CLIENT_TOOL_ID_SET.has(toolName) && !isClientToolEnabledLive(toolName)) {
              resultPayload.output = { message: `Client tool '${toolName}' is disabled in debug settings` };
              resultPayload.status = { code: 503, message: 'tool_disabled' };
            }
            try {
              if (resultPayload.status.code === 503) {
                // Keep disabled result as-is.
              } else if (toolName === 'turn_on_camera') {
                if (!waitForResponse) {
                  emitClientToolResult(
                    {
                      tool_call_id: toolCallId,
                      name: toolName,
                      output: { message: 'camera_on_dispatched' },
                      status: { code: 200, message: 'ok' },
                    },
                    toolDisplayName
                  );
                }
                navigator.mediaDevices
                  .getUserMedia({
                    video: selectedCamera ? { deviceId: { exact: selectedCamera } } : true,
                    audio: false,
                  })
                  .then((stream) => {
                    if (videoRef.current) videoRef.current.srcObject = stream;
                    streamRef.current = stream;
                    if (waitForResponse) {
                      emitClientToolResult(
                        {
                          tool_call_id: toolCallId,
                          name: toolName,
                          output: {
                            message: 'camera_on',
                            tracks: stream.getVideoTracks().length,
                          },
                          status: { code: 200, message: 'ok' },
                        },
                        toolDisplayName
                      );
                    }
                  })
                  .catch((err) => {
                    if (waitForResponse) {
                      emitClientToolResult(
                        {
                          tool_call_id: toolCallId,
                          name: toolName,
                          output: {
                            message: `Client tool '${toolName}' failed`,
                            error: err instanceof Error ? err.message : String(err),
                          },
                          status: { code: 500, message: 'client_tool_failed' },
                        },
                        toolDisplayName
                      );
                    }
                  });
                return;
              } else if (toolName === 'turn_off_camera') {
                stopMedia();
                if (videoRef.current) videoRef.current.srcObject = null;
                resultPayload.output = { message: 'camera_off' };
                resultPayload.status = { code: 200, message: 'ok' };
              } else if (toolName === 'increase_volume') {
                const rawStep = Number(parsedArgs?.step);
                const step = Number.isFinite(rawStep) ? Math.max(1, Math.floor(rawStep)) : 1;
                debugVolumePercentRef.current = Math.min(100, debugVolumePercentRef.current + step);
                resultPayload.output = {
                  message: 'volume_increased',
                  level: debugVolumePercentRef.current,
                };
                resultPayload.status = { code: 200, message: 'ok' };
              } else if (toolName === 'decrease_volume') {
                const rawStep = Number(parsedArgs?.step);
                const step = Number.isFinite(rawStep) ? Math.max(1, Math.floor(rawStep)) : 1;
                debugVolumePercentRef.current = Math.max(0, debugVolumePercentRef.current - step);
                resultPayload.output = {
                  message: 'volume_decreased',
                  level: debugVolumePercentRef.current,
                };
                resultPayload.status = { code: 200, message: 'ok' };
              } else if (toolName === 'voice_msg_prompt' || toolName === 'voice_message_prompt') {
                const msg = String(parsedArgs?.msg || '').trim();
                if (!msg) {
                  resultPayload.output = { message: "Missing required argument 'msg'" };
                  resultPayload.status = { code: 422, message: 'invalid_arguments' };
                } else {
                  enqueuePromptDialog({
                    kind: 'text',
                    payload: {
                      message: msg,
                      promptType: 'voice',
                      voiceText: msg,
                      pendingResult: {
                        toolCallId: toolCallId,
                        toolName,
                        toolDisplayName,
                        waitForResponse,
                      },
                    },
                  });
                  if (!waitForResponse) {
                    resultPayload.output = { message: 'voice_prompt_shown', msg };
                    resultPayload.status = { code: 200, message: 'ok' };
                  } else {
                    return;
                  }
                }
              } else if (toolName === 'text_msg_prompt') {
                const msg = String(parsedArgs?.msg || '').trim();
                if (!msg) {
                  resultPayload.output = { message: "Missing required argument 'msg'" };
                  resultPayload.status = { code: 422, message: 'invalid_arguments' };
                } else {
                  enqueuePromptDialog({
                    kind: 'text',
                    payload: {
                      message: msg,
                      pendingResult: {
                        toolCallId: toolCallId,
                        toolName,
                        toolDisplayName,
                        waitForResponse,
                      },
                    },
                  });
                  if (!waitForResponse) {
                    resultPayload.output = { message: 'text_prompt_shown', msg };
                    resultPayload.status = { code: 200, message: 'ok' };
                  } else {
                    return;
                  }
                }
              } else if (toolName === 'text_choice_prompt' || toolName === 'voice_choice_prompt') {
                const question = String(parsedArgs?.question || '').trim();
                const rawOptions = Array.isArray(parsedArgs?.options) ? parsedArgs.options : [];
                const options = normalizeChoicePromptOptions(rawOptions);
                const isVoiceChoicePrompt = toolName === 'voice_choice_prompt';
                const voiceText = isVoiceChoicePrompt
                  ? String(parsedArgs?.voice_text || parsedArgs?.voiceText || parsedArgs?.msg || question || '').trim()
                  : '';
                const requireSelection = toolName === 'voice_choice_prompt' || toolName === 'text_choice_prompt';
                if (!question) {
                  resultPayload.output = { message: "Missing required argument 'question'" };
                  resultPayload.status = { code: 422, message: 'invalid_arguments' };
                } else if (options.length < 2) {
                  resultPayload.output = { message: "Argument 'options' requires at least 2 valid entries" };
                  resultPayload.status = { code: 422, message: 'invalid_arguments' };
                } else {
                  enqueuePromptDialog({
                    kind: 'choice',
                    payload: {
                      question,
                      options,
                      pendingResult: {
                        toolCallId: toolCallId,
                        toolName,
                        toolDisplayName,
                        waitForResponse,
                      },
                      requireSelection,
                      voiceText,
                    },
                  });
                  if (!waitForResponse && !requireSelection) {
                    resultPayload.output = {
                      message: `${toolName}_shown`,
                      question,
                      options,
                    };
                    resultPayload.status = { code: 200, message: 'ok' };
                  } else {
                    return;
                  }
                }
              }
            } catch (err) {
              resultPayload.output = {
                message: `Client tool '${toolName}' failed`,
                error: err instanceof Error ? err.message : String(err),
              };
              resultPayload.status = { code: 500, message: 'client_tool_failed' };
            }
            emitClientToolResult(resultPayload, toolDisplayName);
          }
          return;
        }

        if (type === 'assistant.tool_result') {
          const result = payload?.result || {};
          const toolName = normalizeToolId(result?.name || 'unknown_tool');
          const toolDisplayName = String(payload?.tool_display_name || toolName);
          const statusCode = Number(result?.status?.code || 500);
          const statusMessage = String(result?.status?.message || 'error');
          const source = String(payload?.source || 'server');
          const output = result?.output;
          const resultText =
            statusCode === 200
              ? `result ${toolDisplayName} source=${source} ${JSON.stringify(output)}`
              : `result ${toolDisplayName} source=${source} status=${statusCode} ${statusMessage}`;
          setMessages((prev) => [...prev, { role: 'tool', text: resultText }]);
          return;
        }

        if (type === 'session.started') {
          wsReadyRef.current = true;
          setWsStatus('ready');
          pendingResolveRef.current?.();
          pendingResolveRef.current = null;
          pendingRejectRef.current = null;
          return;
        }

        if (type === 'config.resolved') {
          const resolved = payload?.config || payload?.data?.config;
          if (resolved) {
            setResolvedConfigView(
              formatRuntimeConfigDebugView({
                submittedSessionStartMetadata: submittedMetadataRef.current,
                wsResolvedConfig: resolved,
              })
            );
          }
          return;
        }

        if (type === 'input.speech_started') {
          setIsLoading(true);
          return;
        }

        if (type === 'input.speech_stopped') {
          setIsLoading(false);
          return;
        }

        if (type === 'transcript.delta') {
          const delta = String(payload.text || '');
          if (!delta) return;
          setMessages((prev) => {
            const idx = userDraftIndexRef.current;
            if (idx === null || !prev[idx] || prev[idx].role !== 'user') {
              const next = [...prev, { role: 'user' as const, text: delta }];
              userDraftIndexRef.current = next.length - 1;
              return next;
            }
            const next = [...prev];
            // ASR interim is typically the latest partial text, not a true text delta.
            next[idx] = { ...next[idx], text: delta };
            return next;
          });
          return;
        }

        if (type === 'transcript.final') {
          const finalText = String(payload.text || '');
          if (!finalText) {
            userDraftIndexRef.current = null;
            return;
          }
          if (lastUserFinalRef.current === finalText) {
            userDraftIndexRef.current = null;
            return;
          }
          setMessages((prev) => {
            const idx = userDraftIndexRef.current;
            userDraftIndexRef.current = null;
            if (idx !== null && prev[idx] && prev[idx].role === 'user') {
              const next = [...prev];
              next[idx] = { ...next[idx], text: finalText || next[idx].text };
              lastUserFinalRef.current = finalText;
              return next;
            }
            const last = prev[prev.length - 1];
            if (last?.role === 'user') {
              const next = [...prev];
              next[next.length - 1] = { ...last, text: finalText };
              lastUserFinalRef.current = finalText;
              return next;
            }
            lastUserFinalRef.current = finalText;
            return [...prev, { role: 'user', text: finalText }];
          });
          return;
        }

        if (type === 'assistant.response.delta') {
          const delta = String(payload.text || '');
          if (!delta) return;
          const responseId = extractResponseId(payload);
          if (responseId && interruptedResponseIdsRef.current.has(responseId)) {
            noteInterruptedDrop(responseId, 'delta');
            return;
          }
          setMessages((prev) => {
            let idx = assistantDraftIndexRef.current;
            if (idx === null || !prev[idx] || prev[idx].role !== 'model') {
              // Tool records can be appended between assistant chunks; recover the
              // latest model row instead of creating a duplicate assistant row.
              for (let i = prev.length - 1; i >= 0; i -= 1) {
                if (prev[i]?.role === 'model') {
                  if (
                    responseId
                    && prev[i].responseId
                    && prev[i].responseId !== responseId
                  ) {
                    break;
                  }
                  idx = i;
                  assistantDraftIndexRef.current = i;
                  break;
                }
                if (prev[i]?.role === 'user') break;
              }
            }
            if (idx === null || !prev[idx] || prev[idx].role !== 'model') {
              const last = prev[prev.length - 1];
              if (last?.role === 'model' && last.text === delta) {
                return prev;
              }
              const nextMessage: DebugTranscriptMessage = { role: 'model' as const, text: delta };
              if (responseId) {
                nextMessage.responseId = responseId;
                if (pendingTtfbByResponseIdRef.current.has(responseId)) {
                  nextMessage.ttfbMs = pendingTtfbByResponseIdRef.current.get(responseId);
                  pendingTtfbByResponseIdRef.current.delete(responseId);
                }
              }
              const next = [...prev, nextMessage];
              assistantDraftIndexRef.current = next.length - 1;
              if (responseId) {
                assistantResponseIndexByIdRef.current.set(responseId, next.length - 1);
              }
              return next;
            }
            const next = [...prev];
            const nextMessage = { ...next[idx], text: next[idx].text + delta };
            if (responseId && !nextMessage.responseId) {
              nextMessage.responseId = responseId;
            }
            if (
              responseId
              && typeof nextMessage.ttfbMs !== 'number'
              && pendingTtfbByResponseIdRef.current.has(responseId)
            ) {
              nextMessage.ttfbMs = pendingTtfbByResponseIdRef.current.get(responseId);
              pendingTtfbByResponseIdRef.current.delete(responseId);
            }
            next[idx] = nextMessage;
            if (responseId) {
              assistantResponseIndexByIdRef.current.set(responseId, idx);
            }
            return next;
          });
          return;
        }

        if (type === 'assistant.response.final') {
          const finalText = String(payload.text || '');
          const responseId = extractResponseId(payload);
          if (responseId && interruptedResponseIdsRef.current.has(responseId)) {
            noteInterruptedDrop(responseId, 'final');
            return;
          }
          setMessages((prev) => {
            let idx = assistantDraftIndexRef.current;
            assistantDraftIndexRef.current = null;
            if (idx === null || !prev[idx] || prev[idx].role !== 'model') {
              for (let i = prev.length - 1; i >= 0; i -= 1) {
                if (prev[i]?.role === 'model') {
                  if (
                    responseId
                    && prev[i].responseId
                    && prev[i].responseId !== responseId
                  ) {
                    break;
                  }
                  idx = i;
                  break;
                }
                if (prev[i]?.role === 'user') break;
              }
            }
            if (idx !== null && prev[idx] && prev[idx].role === 'model') {
              const next = [...prev];
              const nextMessage = { ...next[idx], text: finalText || next[idx].text };
              if (responseId && !nextMessage.responseId) {
                nextMessage.responseId = responseId;
              }
              if (
                responseId
                && typeof nextMessage.ttfbMs !== 'number'
                && pendingTtfbByResponseIdRef.current.has(responseId)
              ) {
                nextMessage.ttfbMs = pendingTtfbByResponseIdRef.current.get(responseId);
                pendingTtfbByResponseIdRef.current.delete(responseId);
              }
              next[idx] = nextMessage;
              if (responseId) {
                assistantResponseIndexByIdRef.current.set(responseId, idx);
              }
              return next;
            }
            if (!finalText) return prev;
            const last = prev[prev.length - 1];
            if (last?.role === 'model') {
              if (last.text === finalText) return prev;
              if (finalText.startsWith(last.text) || last.text.startsWith(finalText)) {
                const next = [...prev];
                next[next.length - 1] = { ...last, text: finalText };
                return next;
              }
            }
            const nextMessage: DebugTranscriptMessage = { role: 'model', text: finalText };
            if (responseId) {
              nextMessage.responseId = responseId;
              if (pendingTtfbByResponseIdRef.current.has(responseId)) {
                nextMessage.ttfbMs = pendingTtfbByResponseIdRef.current.get(responseId);
                pendingTtfbByResponseIdRef.current.delete(responseId);
              }
            }
            const next = [...prev, nextMessage];
            if (responseId) {
              assistantResponseIndexByIdRef.current.set(responseId, next.length - 1);
            }
            return next;
          });
          setIsLoading(false);
          return;
        }

        if (type === 'error') {
          const message = String(payload?.message || payload?.data?.error?.message || 'Unknown error');
          setWsStatus('error');
          setWsError(message);
          setIsLoading(false);
          const err = new Error(message);
          pendingRejectRef.current?.(err);
          pendingResolveRef.current = null;
          pendingRejectRef.current = null;
        }
      };

      ws.onerror = () => {
        const err = new Error('WebSocket connection error');
        setWsStatus('error');
        setWsError(err.message);
        setIsLoading(false);
        pendingRejectRef.current?.(err);
        pendingResolveRef.current = null;
        pendingRejectRef.current = null;
      };

      ws.onclose = () => {
        wsReadyRef.current = false;
        setTextSessionStarted(false);
        userDraftIndexRef.current = null;
        stopPlaybackImmediately();
        if (wsStatusRef.current !== 'error') setWsStatus('disconnected');
      };
    });
  };

  useEffect(() => {
    if (!isOpen) return;
    if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
    // If core TTS-related settings changed while drawer stays open,
    // reset the active WS session so the next launch uses new metadata.
    closeWs();
  }, [isOpen, assistant.id, assistant.voice, assistant.speed]);

  useEffect(() => {
    if (!textTtsEnabled) {
      stopPlaybackImmediately();
    }
  }, [textTtsEnabled]);

  useEffect(() => {
    if (!isOpen || mode !== 'voice' || callStatus !== 'active') return;
    const restartCapture = async () => {
      try {
        await startVoiceCapture();
      } catch (e) {
        console.error('Failed to restart voice capture with new 3A settings', e);
      }
    };
    void restartCapture();
  }, [aecEnabled, nsEnabled, agcEnabled, selectedMic, mode, callStatus, isOpen]);

  useEffect(() => {
    if (!isOpen) return;
    setResolvedConfigView(
      formatRuntimeConfigDebugView({
        submittedSessionStartMetadata: submittedMetadataRef.current,
      })
    );
  }, [isOpen, assistant, voices, llmModels, asrModels, tools]);

  const renderLocalVideo = (isSmall: boolean) => (
      <div className={`relative w-full h-full bg-black overflow-hidden ${isSmall ? 'rounded-lg border border-white/20 shadow-lg' : ''}`}>
          <video ref={videoRef} autoPlay muted playsInline className="w-full h-full object-cover transform scale-x-[-1]" />
          <div className="absolute top-2 left-2 bg-black/50 px-2 py-0.5 rounded text-[10px] text-white/80">Me</div>
      </div>
  );

  const renderRemoteVideo = (isSmall: boolean) => (
      <div className={`relative w-full h-full bg-slate-900 overflow-hidden flex flex-col items-center justify-center ${isSmall ? 'rounded-lg border border-white/20 shadow-lg' : ''}`}>
           <div className="relative flex items-center justify-center">
               <div className={`rounded-full bg-primary/20 animate-pulse ${isSmall ? 'w-16 h-16' : 'w-32 h-32'}`}></div>
               <div className={`absolute rounded-full bg-primary flex items-center justify-center shadow-[0_0_30px_hsl(var(--primary))] ${isSmall ? 'w-12 h-12' : 'w-24 h-24'}`}>
                   <Video className={`${isSmall ? 'w-6 h-6' : 'w-10 h-10'} text-primary-foreground`} />
               </div>
           </div>
           {!isSmall && <div className="mt-4 font-mono text-primary animate-pulse text-sm">{assistant.name}</div>}
      </div>
  );

  const settingsPanel = (
    <div className="h-full border-r border-white/10 bg-background/95 backdrop-blur-md shadow-2xl p-3">
      <div className="flex items-center justify-between mb-2">
        <h3 className="text-xs font-black tracking-[0.15em] uppercase text-muted-foreground">调试设置</h3>
        <Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => setSettingsDrawerOpen(false)}>
          <X className="h-3.5 w-3.5" />
        </Button>
      </div>

      <div className="space-y-3">
        <div className="space-y-1.5">
          <label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase block">WebSocket Endpoint</label>
          <Input value={wsUrl} onChange={(e) => setWsUrl(e.target.value)} placeholder="ws://localhost:8000/ws" />
        </div>
        <div className="flex items-center justify-between gap-2">
          <Badge variant="outline" className="text-xs">WS: {wsStatus}</Badge>
          <Badge variant={textTtsEnabled ? 'outline' : 'secondary'} className="text-xs">
            TTS: {textTtsEnabled ? 'ON' : 'OFF'}
          </Badge>
        </div>
        <div className="rounded-md border border-white/10 bg-black/20 p-2 space-y-2">
          <p className="text-[10px] uppercase tracking-widest text-muted-foreground">Audio 3A</p>
          <label className="inline-flex items-center gap-2 text-xs text-muted-foreground">
            <input type="checkbox" checked={aecEnabled} onChange={(e) => setAecEnabled(e.target.checked)} className="accent-primary" />
            Echo Cancellation (AEC)
          </label>
          <label className="inline-flex items-center gap-2 text-xs text-muted-foreground">
            <input type="checkbox" checked={nsEnabled} onChange={(e) => setNsEnabled(e.target.checked)} className="accent-primary" />
            Noise Suppression (NS)
          </label>
          <label className="inline-flex items-center gap-2 text-xs text-muted-foreground">
            <input type="checkbox" checked={agcEnabled} onChange={(e) => setAgcEnabled(e.target.checked)} className="accent-primary" />
            Auto Gain Control (AGC)
          </label>
        </div>
        <div className="rounded-md border border-white/10 bg-black/20 p-2 space-y-2">
          <p className="text-[10px] uppercase tracking-widest text-muted-foreground">Client Tools</p>
          <p className="text-[11px] text-muted-foreground">滑块控制调试会话内客户端工具是否启用。</p>
          <div className="space-y-2">
            {DEBUG_CLIENT_TOOLS.map((tool) => {
              const enabled = isClientToolEnabled(tool.id);
              return (
                <div key={tool.id} className="flex items-center justify-between gap-3 rounded-md border border-white/10 bg-black/20 px-2 py-1.5">
                  <div className="min-w-0">
                    <div className="text-[11px] font-mono text-foreground truncate">{tool.name}</div>
                    <div className="text-[10px] text-muted-foreground truncate">{tool.description}</div>
                  </div>
                  <Switch
                    checked={enabled}
                    onCheckedChange={(next) => setClientToolEnabled(tool.id, next)}
                    title={enabled ? '点击关闭' : '点击开启'}
                    aria-label={`${tool.name} ${enabled ? '开启' : '关闭'}`}
                  />
                </div>
              );
            })}
          </div>
        </div>
        <div className="rounded-md border border-white/10 bg-black/20 p-2 space-y-2">
          <div className="flex items-center justify-between gap-2">
            <p className="text-[10px] uppercase tracking-widest text-muted-foreground">Dynamic Variables</p>
            <div className="flex items-center gap-1.5">
              <Button
                size="sm"
                variant="ghost"
                className="h-6 px-2 text-[10px]"
                onClick={importDynamicVariablesFromPlaceholders}
                disabled={isDynamicVariablesLocked || requiredTemplateVariableKeys.length === 0 || dynamicVariables.length >= DYNAMIC_VARIABLE_MAX_ITEMS}
                title={`Import keys from {{placeholder}} in prompt${assistant.generatedOpenerEnabled === true ? '' : '/opener'}`}
              >
                Import
              </Button>
              <Button
                size="icon"
                variant="ghost"
                className="h-6 w-6"
                onClick={addDynamicVariableRow}
                disabled={isDynamicVariablesLocked || dynamicVariables.length >= DYNAMIC_VARIABLE_MAX_ITEMS}
                title={isDynamicVariablesLocked ? 'Disable editing while session is active' : 'Add variable'}
              >
                <Plus className="h-3.5 w-3.5" />
              </Button>
            </div>
          </div>
          <p className="text-[11px] text-muted-foreground">
            Use placeholders like {'{{customer_name}}'} in prompt{assistant.generatedOpenerEnabled === true ? '' : '/opener'}.
          </p>
          <p className="text-[11px] text-muted-foreground">
            Built-in system vars: {'{{system__time}}'}, {'{{system_utc}}'}, {'{{system_timezone}}'}.
          </p>
          {requiredTemplateVariableKeys.length > 0 && (
            <p className="text-[11px] text-amber-300/90">
              Required: {requiredTemplateVariableKeys.join(', ')}
            </p>
          )}
          {missingRequiredDynamicVariableKeys.length > 0 && (
            <p className="text-[11px] text-red-300/90">
              Missing required dynamic variable: {missingRequiredDynamicVariableKeys.join(', ')}
            </p>
          )}
          {dynamicVariablesError && (
            <p className="text-[11px] text-red-300/90">
              {dynamicVariablesError}
            </p>
          )}
          {dynamicVariables.length === 0 ? (
            <div className="text-[11px] text-muted-foreground/80 border border-dashed border-white/10 rounded-md px-2 py-2">
              No variables added.
            </div>
          ) : (
            <div className="space-y-2 max-h-52 overflow-auto pr-1">
              {dynamicVariables.map((row, index) => (
                <div key={row.id} className="grid grid-cols-[1fr_1fr_auto] gap-2 items-center">
                  <Input
                    value={row.key}
                    onChange={(e) => updateDynamicVariableRow(row.id, 'key', e.target.value)}
                    placeholder={`key_${index + 1}`}
                    disabled={isDynamicVariablesLocked}
                    className="h-8 text-xs"
                  />
                  <Input
                    value={row.value}
                    onChange={(e) => updateDynamicVariableRow(row.id, 'value', e.target.value)}
                    placeholder="value"
                    disabled={isDynamicVariablesLocked}
                    className="h-8 text-xs"
                  />
                  <Button
                    size="icon"
                    variant="ghost"
                    className="h-8 w-8 text-muted-foreground hover:text-red-300"
                    onClick={() => removeDynamicVariableRow(row.id)}
                    disabled={isDynamicVariablesLocked}
                    title="Remove variable"
                  >
                    <Trash2 className="h-3.5 w-3.5" />
                  </Button>
                </div>
              ))}
            </div>
          )}
          {isDynamicVariablesLocked && (
            <p className="text-[11px] text-muted-foreground">
              Editing is locked while conversation is starting/active.
            </p>
          )}
        </div>
        <div className="rounded-md border border-white/10 bg-black/30">
          <button
            className="w-full px-3 py-2 text-left text-xs text-muted-foreground hover:text-foreground flex items-center justify-between"
            onClick={() => setCaptureConfigOpen((v) => !v)}
          >
            <span>Capture Config Echo</span>
            <ChevronDown className={`h-3.5 w-3.5 transition-transform ${captureConfigOpen ? 'rotate-180' : ''}`} />
          </button>
          {captureConfigOpen && (
            <pre className="px-3 pb-3 text-[11px] leading-5 text-cyan-100/90 whitespace-pre-wrap break-all max-h-64 overflow-auto">
              {captureConfigView || 'Voice call not started yet.'}
            </pre>
          )}
        </div>
        {wsError && <p className="text-xs text-red-400">{wsError}</p>}

        <div className="rounded-md border border-white/10 bg-black/30">
          <button
            className="w-full px-3 py-2 text-left text-xs text-muted-foreground hover:text-foreground flex items-center justify-between"
            onClick={() => setResolvedConfigOpen((v) => !v)}
          >
            <span>Session Config Debug</span>
            <ChevronDown className={`h-3.5 w-3.5 transition-transform ${resolvedConfigOpen ? 'rotate-180' : ''}`} />
          </button>
          {resolvedConfigOpen && (
            <pre className="px-3 pb-3 text-[11px] leading-5 text-cyan-100/90 whitespace-pre-wrap break-all max-h-64 overflow-auto">
              {resolvedConfigView || 'Preparing session.start metadata preview...'}
            </pre>
          )}
        </div>
      </div>
    </div>
  );

  return (
    <>
    <Drawer isOpen={isOpen} onClose={() => { handleHangup(); onClose(); }} title={`调试: ${assistant.name}`}>
      <div className="relative flex flex-col h-full min-h-0 overflow-hidden">
        <div className="flex items-center gap-2 mb-4 shrink-0">
          <div className="flex-1 flex justify-center bg-white/5 p-1 rounded-lg">
            {(['text', 'voice', 'video'] as const).map(m => (
              <button key={m} className={`flex-1 py-1 text-sm rounded-md transition-all ${mode === m ? 'bg-primary text-primary-foreground shadow' : 'text-muted-foreground hover:bg-white/5'}`} onClick={() => setMode(m)}>
                {m === 'text' && <MessageSquare className="inline w-4 h-4 mr-1" />}
                {m === 'voice' && <Mic className="inline w-4 h-4 mr-1" />}
                {m === 'video' && <Video className="inline w-4 h-4 mr-1" />}
                {m === 'text' ? '文本' : m === 'voice' ? '语音' : '视频'}
              </button>
            ))}
          </div>
        </div>

        <div className="flex-1 min-h-0 overflow-hidden flex flex-col">
            <div className={`flex-1 overflow-hidden flex flex-col min-h-0 ${mode === 'text' && textSessionStarted ? 'mb-0' : 'mb-4'}`}>
              {mode === 'text' ? (
                textSessionStarted ? (
                  <div className="flex-1 min-h-0 overflow-hidden animate-in fade-in flex flex-col">
                    <div className="h-[68vh] min-h-[420px] max-h-[68vh] w-full flex flex-col min-h-0 overflow-hidden">
                      <TranscriptionLog scrollRef={scrollRef} messages={messages} isLoading={isLoading} className="flex-1 min-h-0 h-full" />
                    </div>
                  </div>
                ) : wsStatus === 'connecting' ? (
                  <div className="flex-1 flex flex-col items-center justify-center space-y-6">
                    <div className="h-24 w-24 rounded-full bg-primary/20 flex items-center justify-center animate-bounce">
                      <MessageSquare className="h-10 w-10 text-primary" />
                    </div>
                    <div className="text-center">
                      <p className="text-primary font-mono text-sm tracking-widest animate-pulse">CONNECTING...</p>
                      <p className="text-xs text-muted-foreground mt-2">正在连接文本调试服务</p>
                    </div>
                    <Button onClick={closeWs} variant="destructive" className="rounded-full h-10 px-8">取消</Button>
                  </div>
                ) : (
                  <div className="flex-1 flex flex-col items-center justify-center space-y-6 border border-white/5 rounded-xl bg-black/20 animate-in fade-in zoom-in-95 px-6">
                    <div className="relative">
                      <div className="absolute inset-0 bg-primary/20 rounded-full blur-2xl animate-pulse"></div>
                      <div className="relative h-24 w-24 rounded-full bg-white/5 border border-white/10 flex items-center justify-center">
                        <MessageSquare className="h-10 w-10 text-muted-foreground" />
                      </div>
                    </div>
                    <div className="text-center">
                      <h3 className="text-lg font-bold text-white mb-1">准备就绪</h3>
                      <p className="text-xs text-muted-foreground">点击下方按钮开启人机交互测试</p>
                    </div>
                    <Button
                      onClick={handleTextLaunch}
                      disabled={wsStatus === 'connecting'}
                      className="w-48 h-12 rounded-full bg-green-500 hover:bg-green-600 shadow-[0_0_20px_rgba(34,197,94,0.4)] text-base font-bold"
                    >
                      <PhoneCall className="mr-2 h-5 w-5" /> 发起呼叫
                    </Button>
                  </div>
                )
              ) : callStatus === 'idle' ? (
                <div className="flex-1 flex flex-col items-center justify-center space-y-6 border border-white/5 rounded-xl bg-black/20 animate-in fade-in zoom-in-95">
                  <div className="relative">
                    <div className="absolute inset-0 bg-primary/20 rounded-full blur-2xl animate-pulse"></div>
                    <div className="relative h-24 w-24 rounded-full bg-white/5 border border-white/10 flex items-center justify-center">
                      {mode === 'voice' ? <Mic className="h-10 w-10 text-muted-foreground" /> : <Video className="h-10 w-10 text-muted-foreground" />}
                    </div>
                  </div>
                  <div className="text-center">
                    <h3 className="text-lg font-bold text-white mb-1">准备就绪</h3>
                    <p className="text-xs text-muted-foreground">点击下方按钮开启人机交互测试</p>
                  </div>
                  <Button onClick={handleCall} className="w-48 h-12 rounded-full bg-green-500 hover:bg-green-600 shadow-[0_0_20px_rgba(34,197,94,0.4)] text-base font-bold">
                    <PhoneCall className="mr-2 h-5 w-5" /> 发起呼叫
                  </Button>
                </div>
              ) : callStatus === 'calling' ? (
                mode === 'voice' ? (
                  <div className="flex-1 flex flex-col items-center justify-center space-y-6">
                    <div className="h-24 w-24 rounded-full bg-primary/20 flex items-center justify-center animate-bounce">
                      <Mic className="h-10 w-10 text-primary" />
                    </div>
                    <div className="text-center">
                      <p className="text-primary font-mono text-sm tracking-widest animate-pulse">CONNECTING...</p>
                      <p className="text-xs text-muted-foreground mt-2">正在连接语音调试服务</p>
                    </div>
                    <Button onClick={handleHangup} variant="destructive" className="rounded-full h-10 px-8">取消</Button>
                  </div>
                ) : (
                  <div className="flex-1 flex flex-col items-center justify-center space-y-6">
                    <div className="h-24 w-24 rounded-full bg-primary/20 flex items-center justify-center animate-bounce">
                      <PhoneCall className="h-10 w-10 text-primary" />
                    </div>
                    <div className="text-center">
                      <p className="text-primary font-mono text-sm tracking-widest animate-pulse">CALLING...</p>
                      <p className="text-xs text-muted-foreground mt-2">正在连接 AI 服务</p>
                    </div>
                    <Button onClick={handleHangup} variant="destructive" className="rounded-full h-10 px-8">取消</Button>
                  </div>
                )
              ) : (
                <div className="flex-1 flex flex-col min-h-0 space-y-2">
                  {mode === 'voice' ? (
                    <div className="h-[68vh] min-h-[420px] max-h-[68vh] w-full flex flex-col min-h-0 overflow-hidden animate-in fade-in">
                      <TranscriptionLog scrollRef={scrollRef} messages={messages} isLoading={isLoading} className="flex-1 min-h-0 h-full" />
                    </div>
                  ) : (
                    <div className="flex flex-col h-full min-h-0 space-y-2 animate-in fade-in">
                      <div className="h-3/5 shrink-0 flex flex-col gap-2">
                        <div className="flex gap-2 shrink-0">
                          <select className="flex-1 text-xs rounded-md border border-white/10 bg-white/5 px-2 py-1 text-foreground appearance-none cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card [&>option]:text-foreground" value={selectedCamera} onChange={e => setSelectedCamera(e.target.value)}>
                            {devices.filter(d => d.kind === 'videoinput').map(d => <option key={d.deviceId} value={d.deviceId}>{d.label || 'Camera'}</option>)}
                          </select>
                          <select className="flex-1 text-xs rounded-md border border-white/10 bg-white/5 px-2 py-1 text-foreground appearance-none cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card [&>option]:text-foreground" value={selectedMic} onChange={e => setSelectedMic(e.target.value)}>
                            {devices.filter(d => d.kind === 'audioinput').map(d => <option key={d.deviceId} value={d.deviceId}>{d.label || 'Mic'}</option>)}
                          </select>
                        </div>
                        <div className="flex-1 relative rounded-lg overflow-hidden border border-white/10 bg-black min-h-0">
                          <div className="absolute inset-0">{isSwapped ? renderLocalVideo(false) : renderRemoteVideo(false)}</div>
                          <div className="absolute bottom-2 right-2 w-24 h-36 z-10">{isSwapped ? renderRemoteVideo(true) : renderLocalVideo(true)}</div>
                          <button className="absolute top-2 right-2 z-20 h-8 w-8 rounded-full bg-black/50 backdrop-blur flex items-center justify-center text-white border border-white/10 hover:bg-primary/80" onClick={() => setIsSwapped(!isSwapped)}><ArrowLeftRight className="h-3.5 w-3.5" /></button>
                        </div>
                      </div>
                      <TranscriptionLog scrollRef={scrollRef} messages={messages} isLoading={isLoading} className="flex-1 min-h-0 overflow-y-auto" />
                    </div>
                  )}
                </div>
              )}
            </div>

	            <div className={mode === 'text' && textSessionStarted ? 'shrink-0 mt-3 px-1 mb-3' : mode === 'voice' ? 'shrink-0 space-y-3 mt-3 px-1 mb-3' : 'shrink-0 space-y-2 mt-2 px-1 mb-3'}>
              {mode === 'voice' && (
                <div className="w-full flex items-center gap-2 pb-1">
                  <span className="text-xs text-muted-foreground shrink-0">麦克风</span>
                  <select
                    className="flex-1 text-xs rounded-md border border-white/10 bg-white/5 px-2 py-1 text-foreground appearance-none cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card [&>option]:text-foreground"
                    value={selectedMic}
                    onChange={(e) => setSelectedMic(e.target.value)}
                  >
                    {devices.filter(d => d.kind === 'audioinput').map(d => (
                      <option key={d.deviceId} value={d.deviceId}>{d.label || 'Mic'}</option>
                    ))}
                  </select>
                </div>
              )}
              <div className="w-full flex items-center gap-2 min-w-0">
                {mode === 'text' && textSessionStarted && (
                  <Button
                    variant="destructive"
                    size="sm"
                    className="h-9 px-3 font-bold shrink-0 whitespace-nowrap"
                    onClick={closeWs}
                  >
                    <PhoneOff className="h-4 w-4" />
                    <span className="ml-1.5">结束测试</span>
                  </Button>
                )}
                {mode === 'voice' && callStatus === 'active' && (
                  <Button
                    variant="destructive"
                    size="sm"
                    className="h-9 px-3 font-bold shrink-0 whitespace-nowrap"
                    onClick={handleHangup}
                  >
                    <PhoneOff className="h-4 w-4" />
                    <span className="ml-1.5">结束通话</span>
                  </Button>
                )}
                <Input
                  value={inputText}
                  onChange={e => setInputText(e.target.value)}
                  placeholder={mode === 'text' && !textSessionStarted ? "请先发起呼叫后输入消息..." : (mode === 'text' ? "输入消息..." : "输入文本模拟交互...")}
                  onKeyDown={e => e.key === 'Enter' && handleSend()}
                  disabled={mode === 'text' ? !textSessionStarted : (isLoading || callStatus !== 'active')}
                  className="flex-1 min-w-0"
                />
                <Button
                  size="icon"
                  className="h-9 w-9 shrink-0"
                  onClick={handleSend}
                  disabled={mode === 'text' ? !textSessionStarted : (isLoading || callStatus !== 'active')}
                >
                  <Send className="h-4 w-4" />
                </Button>
	              </div>
	            </div>
	        </div>
          {textPromptDialog.open && (
            <div className="absolute inset-0 z-40 flex items-center justify-center bg-black/55 backdrop-blur-[1px]">
              <div className="relative w-[92%] max-w-md rounded-xl border border-white/15 bg-card/95 p-4 shadow-2xl animate-in zoom-in-95 duration-200">
                <button
                  type="button"
                  onClick={() => closeTextPromptDialog('dismiss')}
                  className="absolute right-3 top-3 rounded-sm opacity-70 hover:opacity-100 text-muted-foreground hover:text-foreground transition-opacity"
                  title="关闭"
                >
                  <X className="h-4 w-4" />
                </button>
                <div className="mb-3 pr-6">
                  <div className="text-[10px] font-black tracking-[0.14em] uppercase text-amber-300">
                    {textPromptDialog.promptType === 'voice' ? '语音消息提示' : '文本消息提示'}
                  </div>
                  <p className="mt-2 text-sm leading-6 text-foreground whitespace-pre-wrap break-words">{textPromptDialog.message}</p>
                </div>
                <div className="flex justify-end">
                  <Button size="sm" onClick={() => closeTextPromptDialog('confirm')}>
                    确认
                  </Button>
                </div>
              </div>
            </div>
          )}
          {choicePromptDialog.open && (
            <div className="absolute inset-0 z-40 flex items-center justify-center bg-black/55 backdrop-blur-[1px]">
              <div className="relative w-[92%] max-w-md rounded-xl border border-white/15 bg-card/95 p-4 shadow-2xl animate-in zoom-in-95 duration-200">
                {!choicePromptDialog.requireSelection && (
                  <button
                    type="button"
                    onClick={() => closeChoicePromptDialog('dismiss')}
                    className="absolute right-3 top-3 rounded-sm opacity-70 hover:opacity-100 text-muted-foreground hover:text-foreground transition-opacity"
                    title="关闭"
                  >
                    <X className="h-4 w-4" />
                  </button>
                )}
                <div className="mb-3 pr-6">
                  <div className="text-[10px] font-black tracking-[0.14em] uppercase text-cyan-300">
                    {choicePromptDialog.requireSelection
                      ? (choicePromptDialog.voiceText ? '语音选项提示' : '文本选项提示')
                      : '选项问题提示'}
                  </div>
                  <p className="mt-2 text-sm leading-6 text-foreground whitespace-pre-wrap break-words">{choicePromptDialog.question}</p>
                  {choicePromptDialog.requireSelection && (
                    <p className="mt-1 text-[11px] text-cyan-200/80">请点击一个选项继续。</p>
                  )}
                </div>
                <div className="space-y-2">
                  {choicePromptDialog.options.map((option) => (
                    <Button
                      key={option.id}
                      variant="outline"
                      className="w-full justify-start text-left h-auto py-2.5 px-3"
                      onClick={() => closeChoicePromptDialog('select', option)}
                    >
                      {option.label}
                    </Button>
                  ))}
                </div>
                {!choicePromptDialog.requireSelection && (
                  <div className="mt-3 flex justify-end">
                    <Button size="sm" variant="ghost" onClick={() => closeChoicePromptDialog('dismiss')}>
                      跳过
                    </Button>
                  </div>
                )}
              </div>
            </div>
          )}
	      </div>
	    </Drawer>
    {isOpen && (
      <div className="fixed inset-y-0 z-[51] right-[min(100vw,32rem)]">
        <button
          className={`absolute inset-y-0 w-10 border-r border-white/10 bg-background/90 backdrop-blur-md text-muted-foreground hover:text-foreground hover:bg-background/95 transition-[right,color,background-color] duration-300 flex flex-col items-center justify-center gap-2 ${settingsDrawerOpen ? 'right-[min(78vw,28rem)]' : 'right-0'}`}
          onClick={() => setSettingsDrawerOpen((v) => !v)}
          title={settingsDrawerOpen ? '收起调试设置' : '展开调试设置'}
        >
          <span className={`text-[10px] font-mono tracking-tight ${settingsDrawerOpen ? 'text-primary animate-pulse' : 'opacity-90 animate-pulse'}`}>
            {settingsDrawerOpen ? '>>' : '<<'}
          </span>
          <Wrench className="h-4 w-4" />
          <span className="text-[10px] tracking-widest [writing-mode:vertical-rl]">调试设置</span>
        </button>
        <div className="absolute inset-y-0 right-0 w-[78vw] max-w-md overflow-hidden pointer-events-none">
          <div className={`h-full transition-transform duration-300 ease-out will-change-transform ${settingsDrawerOpen ? 'translate-x-0 pointer-events-auto' : 'translate-x-full pointer-events-none'}`}>
            {settingsPanel}
          </div>
        </div>
      </div>
    )}
    </>
  );
};
