import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import axios from 'axios';
import { Activity, MessageSquareText, X } from 'lucide-react';
import { APP_ENDPOINTS } from '../../config/env';
import { useAppStore } from '../../store/appStore';
import { normalizeAssistantMessageText } from './messageParser';
import './BotDashboardModule.css';
import { channelsZhCn } from '../../i18n/channels.zh-cn';
import { channelsEn } from '../../i18n/channels.en';
import { pickLocale } from '../../i18n';
import { dashboardZhCn } from '../../i18n/dashboard.zh-cn';
import { dashboardEn } from '../../i18n/dashboard.en';
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
import { TopicFeedPanel } from './topic/TopicFeedPanel';
import {
  ChatDisabledMask,
  ChatModeRail,
  ConversationViewport,
} from './components/ChatPanelParts';
import { ChatComposerDock } from './components/ChatComposerDock';
import { WorkspaceHoverTooltip, WorkspacePreviewModal } from './components/WorkspacePanelParts';
import { ResourceMonitorModal } from './components/RuntimePanelParts';
import { BotDashboardBotListPanel } from './components/BotDashboardBotListPanel';
import { BotDashboardConfigDrawers } from './components/BotDashboardConfigDrawers';
import { BotDashboardConversationNodes } from './components/BotDashboardConversationNodes';
import { BotDashboardRuntimePanel } from './components/BotDashboardRuntimePanel';
import { isChannelConfigured, renderBotChannelFields } from './components/BotChannelFieldEditor';
import {
  DEFAULT_WORKSPACE_DOWNLOAD_EXTENSIONS,
  RUNTIME_STALE_MS,
  clampTemperature,
  isPreviewableWorkspacePath,
  mergeConversation,
  normalizeRuntimeState,
  parseBotTimestamp,
  topicDraftUiKey,
  resolveWorkspaceDocumentPath,
  workspaceFileAction,
} from './botDashboardShared';
import type {
  BotChannel,
  CompactPanelTab,
  NodeBotGroup,
  RuntimeViewMode,
  WorkspaceTreeResponse,
} from './botDashboardShared';
import { useDashboardChat } from './hooks/useDashboardChat';
import { useBotDashboardConfigState } from './hooks/useBotDashboardConfigState';
import { useDashboardChannelConfig } from './hooks/useDashboardChannelConfig';
import { useDashboardMcpCronConfig } from './hooks/useDashboardMcpCronConfig';
import { useDashboardRuntimeControl } from './hooks/useDashboardRuntimeControl';
import { useDashboardSkillsEnvConfig } from './hooks/useDashboardSkillsEnvConfig';
import { useDashboardTopicConfig } from './hooks/useDashboardTopicConfig';
import { useDashboardWorkspace } from './hooks/useDashboardWorkspace';
import {
  readCachedPlatformPageSize,
} from '../../utils/platformPageSize';

interface BotDashboardModuleProps {
  onOpenCreateWizard?: () => void;
  onOpenImageFactory?: () => void;
  forcedBotId?: string;
  forcedNodeId?: string;
  compactMode?: boolean;
  initialCompactPanelTab?: 'chat' | 'runtime';
  hideCompactFab?: boolean;
}

export function BotDashboardModule({
  onOpenCreateWizard,
  onOpenImageFactory,
  forcedBotId,
  forcedNodeId,
  compactMode = false,
  initialCompactPanelTab = 'chat',
  hideCompactFab = false,
}: BotDashboardModuleProps) {
  const {
    activeBots,
    setBots,
    mergeBot,
    updateBotStatus,
    locale,
    addBotMessage,
    setBotMessages,
    setBotMessageFeedback,
  } = useAppStore();
  const { notify, confirm } = useLucentPrompt();
  const fileNotPreviewableLabel = locale === 'zh' ? '当前文件类型不支持预览' : 'This file type is not previewable';
  const [selectedBotId, setSelectedBotId] = useState('');
  const [speechEnabled, setSpeechEnabled] = useState(true);
  const [showChannelModal, setShowChannelModal] = useState(false);
  const [showTopicModal, setShowTopicModal] = useState(false);
  const [showSkillsModal, setShowSkillsModal] = useState(false);
  const [showMcpModal, setShowMcpModal] = useState(false);
  const [showEnvParamsModal, setShowEnvParamsModal] = useState(false);
  const [showCronModal, setShowCronModal] = useState(false);
  const channelCreateMenuRef = useRef<HTMLDivElement | null>(null);
  const topicPresetMenuRef = useRef<HTMLDivElement | null>(null);
  const skillZipPickerRef = useRef<HTMLInputElement | null>(null);
  const skillAddMenuRef = useRef<HTMLDivElement | null>(null);
  const [uploadMaxMb, setUploadMaxMb] = useState(100);
  const [allowedAttachmentExtensions, setAllowedAttachmentExtensions] = useState<string[]>([]);
  const [botListPageSize, setBotListPageSize] = useState(() => readCachedPlatformPageSize(10));
  const [botListPageSizeReady, setBotListPageSizeReady] = useState(
    () => readCachedPlatformPageSize(0) > 0,
  );
  const [chatPullPageSize, setChatPullPageSize] = useState(60);
  const [commandAutoUnlockSeconds, setCommandAutoUnlockSeconds] = useState(10);
  const [workspaceDownloadExtensions, setWorkspaceDownloadExtensions] = useState<string[]>(
    DEFAULT_WORKSPACE_DOWNLOAD_EXTENSIONS,
  );
  const [runtimeViewMode, setRuntimeViewMode] = useState<RuntimeViewMode>('visual');
  const [runtimeMenuOpen, setRuntimeMenuOpen] = useState(false);
  const [botListMenuOpen, setBotListMenuOpen] = useState(false);
  const [topicDetailOpen, setTopicDetailOpen] = useState(false);
  const [compactPanelTab, setCompactPanelTab] = useState<CompactPanelTab>(initialCompactPanelTab);
  const [isCompactMobile, setIsCompactMobile] = useState(false);
  const [botListQuery, setBotListQuery] = useState('');
  const [botListPage, setBotListPage] = useState(1);
  const [expandedProgressByKey, setExpandedProgressByKey] = useState<Record<string, boolean>>({});
  const [expandedUserByKey, setExpandedUserByKey] = useState<Record<string, boolean>>({});
  const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
  const botSearchInputName = useMemo(
    () => `nbot-search-${Math.random().toString(36).slice(2, 10)}`,
    [],
  );
  const workspaceSearchInputName = useMemo(
    () => `nbot-workspace-search-${Math.random().toString(36).slice(2, 10)}`,
    [],
  );
  const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
  const botListMenuRef = useRef<HTMLDivElement | null>(null);
  const botOrderRef = useRef<Record<string, number>>({});
  const nextBotOrderRef = useRef(1);
  const copyWorkspacePreviewUrl = async (filePath: string) => {
    const normalized = String(filePath || '').trim();
    if (!selectedBotId || !normalized) return;
    const hrefRaw = buildWorkspacePreviewHref(normalized);
    const href = (() => {
      try {
        return new URL(hrefRaw, window.location.origin).href;
      } catch {
        return hrefRaw;
      }
    })();
    try {
      if (navigator.clipboard?.writeText) {
        await navigator.clipboard.writeText(href);
      } else {
        const ta = document.createElement('textarea');
        ta.value = href;
        document.body.appendChild(ta);
        ta.select();
        document.execCommand('copy');
        ta.remove();
      }
      notify(t.urlCopied, { tone: 'success' });
    } catch {
      notify(t.urlCopyFail, { tone: 'error' });
    }
  };
  const copyWorkspacePreviewPath = async (filePath: string) => {
    const normalized = String(filePath || '').trim();
    if (!normalized) return;
    await copyTextToClipboard(
      normalized,
      isZh ? '文件路径已复制' : 'File path copied',
      isZh ? '文件路径复制失败' : 'Failed to copy file path',
    );
  };
  const copyTextToClipboard = async (textRaw: string, successMsg: string, failMsg: string) => {
    const text = String(textRaw || '');
    if (!text.trim()) return;
    try {
      if (navigator.clipboard?.writeText) {
        await navigator.clipboard.writeText(text);
      } else {
        const ta = document.createElement('textarea');
        ta.value = text;
        document.body.appendChild(ta);
        ta.select();
        document.execCommand('copy');
        ta.remove();
      }
      notify(successMsg, { tone: 'success' });
    } catch {
      notify(failMsg, { tone: 'error' });
    }
  };
  const openWorkspacePathFromChat = async (path: string) => {
    const normalized = String(path || '').trim();
    if (!normalized) return;
    const action = workspaceFileAction(normalized, workspaceDownloadExtensionSet);
    if (action === 'download') {
      triggerWorkspaceFileDownload(normalized);
      return;
    }
    if (action === 'preview') {
      void openWorkspaceFilePreview(normalized);
      return;
    }
    try {
      await axios.get<WorkspaceTreeResponse>(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/tree`, {
        params: { path: normalized },
      });
      await loadWorkspaceTree(selectedBotId, normalized);
      return;
    } catch {
      if (!isPreviewableWorkspacePath(normalized, workspaceDownloadExtensionSet) || action === 'unsupported') {
        notify(fileNotPreviewableLabel, { tone: 'warning' });
        return;
      }
    }
  };
  const resolveWorkspaceMediaSrc = useCallback((srcRaw: string, baseFilePath?: string): string => {
    const src = String(srcRaw || '').trim();
    if (!src || !selectedBotId) return src;
    const resolvedWorkspacePath = resolveWorkspaceDocumentPath(src, baseFilePath);
    if (resolvedWorkspacePath) {
      return buildWorkspacePreviewHref(resolvedWorkspacePath);
    }
    const lower = src.toLowerCase();
    if (lower.startsWith('data:') || lower.startsWith('blob:') || lower.startsWith('http://') || lower.startsWith('https://')) {
      return src;
    }
    return src;
  }, [selectedBotId]);

  useEffect(() => {
    const ordered = Object.values(activeBots).sort((a, b) => {
      const aCreated = parseBotTimestamp(a.created_at);
      const bCreated = parseBotTimestamp(b.created_at);
      if (aCreated !== bCreated) return aCreated - bCreated;
      return String(a.id || '').localeCompare(String(b.id || ''));
    });

    ordered.forEach((bot) => {
      const id = String(bot.id || '').trim();
      if (!id) return;
      if (botOrderRef.current[id] !== undefined) return;
      botOrderRef.current[id] = nextBotOrderRef.current;
      nextBotOrderRef.current += 1;
    });

    const alive = new Set(ordered.map((bot) => String(bot.id || '').trim()).filter(Boolean));
    Object.keys(botOrderRef.current).forEach((id) => {
      if (!alive.has(id)) delete botOrderRef.current[id];
    });
  }, [activeBots]);

  const bots = useMemo(
    () =>
      Object.values(activeBots)
        .filter((bot) => {
          const expectedNodeId = String(forcedNodeId || '').trim().toLowerCase();
          if (!expectedNodeId) return true;
          return String(bot.node_id || 'local').trim().toLowerCase() === expectedNodeId;
        })
        .sort((a, b) => {
        const aId = String(a.id || '').trim();
        const bId = String(b.id || '').trim();
        const aOrder = botOrderRef.current[aId] ?? Number.MAX_SAFE_INTEGER;
        const bOrder = botOrderRef.current[bId] ?? Number.MAX_SAFE_INTEGER;
        if (aOrder !== bOrder) return aOrder - bOrder;
        return aId.localeCompare(bId);
      }),
    [activeBots, forcedNodeId],
  );
  const hasForcedBot = Boolean(String(forcedBotId || '').trim());
  const compactListFirstMode = compactMode && !hasForcedBot;
  const isCompactListPage = compactListFirstMode && !selectedBotId;
  const showCompactBotPageClose = compactListFirstMode && Boolean(selectedBotId);
  const showBotListPanel = !hasForcedBot && (!compactMode || isCompactListPage);
  const normalizedBotListQuery = botListQuery.trim().toLowerCase();
  const filteredBots = useMemo(() => {
    if (!normalizedBotListQuery) return bots;
    return bots.filter((bot) => {
      const id = String(bot.id || '').toLowerCase();
      const name = String(bot.name || '').toLowerCase();
      const nodeId = String(bot.node_id || '').toLowerCase();
      const nodeName = String(bot.node_display_name || '').toLowerCase();
      return (
        id.includes(normalizedBotListQuery) ||
        name.includes(normalizedBotListQuery) ||
        nodeId.includes(normalizedBotListQuery) ||
        nodeName.includes(normalizedBotListQuery)
      );
    });
  }, [bots, normalizedBotListQuery]);
  const botListTotalPages = Math.max(1, Math.ceil(filteredBots.length / botListPageSize));
  const pagedBots = useMemo(() => {
    const page = Math.min(Math.max(1, botListPage), botListTotalPages);
    const start = (page - 1) * botListPageSize;
    return filteredBots.slice(start, start + botListPageSize);
  }, [filteredBots, botListPage, botListTotalPages, botListPageSize]);
  const pagedBotGroups = useMemo<NodeBotGroup[]>(() => {
    const unknownNodeLabel = locale === 'zh' ? '未命名节点' : 'Unnamed node';
    const groups = new Map<string, NodeBotGroup>();
    pagedBots.forEach((bot) => {
      const nodeId = String(bot.node_id || 'local').trim() || 'local';
      const label = String(bot.node_display_name || '').trim() || nodeId || unknownNodeLabel;
      const key = nodeId.toLowerCase();
      const existing = groups.get(key);
      if (existing) {
        existing.bots.push(bot);
        return;
      }
      groups.set(key, {
        key,
        label,
        nodeId,
        bots: [bot],
      });
    });
    return Array.from(groups.values()).sort((a, b) => {
      if (a.key === b.key) return 0;
      if (a.key === 'local') return -1;
      if (b.key === 'local') return 1;
      return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' });
    });
  }, [locale, pagedBots]);
  const selectedBot = selectedBotId ? activeBots[selectedBotId] : undefined;
  const forcedBotMissing = Boolean(forcedBotId && bots.length > 0 && !activeBots[String(forcedBotId).trim()]);
  const messages = selectedBot?.messages || [];
  const events = selectedBot?.events || [];
  const isZh = locale === 'zh';
  const noteLocale = pickLocale(locale, { 'zh-cn': 'zh-cn' as const, en: 'en' as const });
  const t = pickLocale(locale, { 'zh-cn': dashboardZhCn, en: dashboardEn });
  const refresh = async () => {
    const forced = String(forcedBotId || '').trim();
    if (forced) {
      const targetId = String(selectedBotId || forced).trim() || forced;
      const botRes = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetId)}`);
      setBots(botRes.data ? [botRes.data] : []);
      return;
    }

    const botsRes = await axios.get(`${APP_ENDPOINTS.apiBase}/bots`);
    setBots(botsRes.data);
  };
  const {
    addChannel,
    addableChannelTypes,
    beginChannelCreate,
    channelCreateMenuOpen,
    channelDraftUiKey,
    channels,
    expandedChannelByKey,
    globalDelivery,
    isDashboardChannel,
    isSavingChannel,
    isSavingGlobalDelivery,
    newChannelDraft,
    newChannelPanelOpen,
    openChannelModal,
    removeChannel,
    resetChannelCollection,
    resetNewChannelDraft,
    saveChannel,
    saveGlobalDelivery,
    setChannelCreateMenuOpen,
    setExpandedChannelByKey,
    setGlobalDelivery,
    setNewChannelDraft,
    setNewChannelPanelOpen,
    updateChannelLocal,
    updateGlobalDeliveryFlag,
  } = useDashboardChannelConfig({
    confirm,
    notify,
    refresh,
    selectedBot,
    setShowChannelModal,
    t,
  });
  const {
    botSkills,
    envDraftKey,
    envDraftValue,
    envParams,
    installMarketSkill,
    isMarketSkillsLoading,
    isSkillUploading,
    loadBotEnvParams,
    loadBotSkills,
    loadMarketSkills,
    marketSkillInstallingId,
    marketSkills,
    onPickSkillZip,
    removeBotSkill,
    removeEnvParam,
    resetSkillsEnvState,
    saveBotEnvParams,
    setEnvDraftKey,
    setEnvDraftValue,
    setShowSkillMarketInstallModal,
    setSkillAddMenuOpen,
    showSkillMarketInstallModal,
    skillAddMenuOpen,
    triggerSkillZipUpload,
    upsertEnvParam,
  } = useDashboardSkillsEnvConfig({
    confirm,
    isZh,
    notify,
    selectedBot,
    setShowEnvParamsModal,
    skillZipPickerRef,
    t,
  });
  const {
    beginMcpCreate,
    canRemoveMcpServer,
    cronActionJobId,
    cronJobs,
    cronLoading,
    deleteCronJob,
    expandedMcpByKey,
    isSavingMcp,
    loadCronJobs,
    mcpDraftUiKey,
    mcpServers,
    newMcpDraft,
    newMcpPanelOpen,
    openMcpModal,
    removeMcpServer,
    resetCronState,
    resetMcpConfigState,
    resetNewMcpDraft,
    saveNewMcpServer,
    saveSingleMcpServer,
    setExpandedMcpByKey,
    setNewMcpDraft,
    setNewMcpPanelOpen,
    stopCronJob,
    updateMcpServer,
  } = useDashboardMcpCronConfig({
    confirm,
    isZh,
    notify,
    selectedBot,
    setShowMcpModal,
    t,
  });
  const {
    batchStartBots,
    batchStopBots,
    controlStateByBot,
    isBatchOperating,
    loadResourceSnapshot,
    openResourceMonitor,
    operatingBotId,
    resourceBot,
    resourceBotId,
    resourceError,
    resourceLoading,
    resourceSnapshot,
    restartBot,
    setBotEnabled,
    setShowResourceModal,
    showResourceModal,
    startBot,
    stopBot,
  } = useDashboardRuntimeControl({
    bots,
    confirm,
    isZh,
    notify,
    refresh,
    t,
    updateBotStatus,
  });
  const {
    buildWorkspaceDownloadHref,
    buildWorkspacePreviewHref,
    buildWorkspaceRawHref,
    closeWorkspacePreview,
    filteredWorkspaceEntries,
    hideWorkspaceHoverCard,
    loadWorkspaceTree,
    openWorkspaceFilePreview,
    resetWorkspaceBrowserState,
    saveWorkspacePreviewMarkdown,
    setWorkspaceAutoRefresh,
    setWorkspaceHoverCard,
    setWorkspacePreviewDraft,
    setWorkspacePreviewFullscreen,
    setWorkspacePreviewMode,
    setWorkspaceQuery,
    showWorkspaceHoverCard,
    triggerWorkspaceFileDownload,
    workspaceAutoRefresh,
    workspaceCurrentPath,
    workspaceDownloadExtensionSet,
    workspaceError,
    workspaceFileLoading,
    workspaceFiles,
    workspaceHoverCard,
    workspaceLoading,
    workspaceParentPath,
    workspacePathDisplay,
    workspacePreview,
    workspacePreviewCanEdit,
    workspacePreviewDraft,
    workspacePreviewEditorEnabled,
    workspacePreviewFullscreen,
    workspacePreviewSaving,
    workspaceSearchLoading,
    workspaceSearchQuery: workspaceQuery,
  } = useDashboardWorkspace({
    notify,
    selectedBotDockerStatus: selectedBot?.docker_status,
    selectedBotId,
    t,
    workspaceDownloadExtensions,
  });
  const {
    activeTopicOptions,
    addTopic,
    beginTopicCreate,
    deleteTopicFeedItem,
    effectiveTopicPresetTemplates,
    expandedTopicByKey,
    hasTopicUnread,
    isSavingTopic,
    loadTopicFeed,
    loadTopicFeedStats,
    loadTopics,
    markTopicFeedItemRead,
    newTopicAdvancedOpen,
    newTopicDescription,
    newTopicExamplesNegative,
    newTopicExamplesPositive,
    newTopicExcludeWhen,
    newTopicIncludeWhen,
    newTopicKey,
    newTopicName,
    newTopicPanelOpen,
    newTopicPriority,
    newTopicPurpose,
    newTopicSourceLabel,
    normalizeTopicKeyInput,
    openTopicModal,
    removeTopic,
    resetNewTopicDraft,
    resetTopicConfigState,
    resetTopicFeedState,
    saveTopic,
    setExpandedTopicByKey,
    setNewTopicAdvancedOpen,
    setNewTopicDescription,
    setNewTopicExamplesNegative,
    setNewTopicExamplesPositive,
    setNewTopicExcludeWhen,
    setNewTopicIncludeWhen,
    setNewTopicKey,
    setNewTopicName,
    setNewTopicPanelOpen,
    setNewTopicPriority,
    setNewTopicPurpose,
    setTopicFeedTopicKey,
    setTopicPresetMenuOpen,
    setTopicPresetTemplates,
    topicFeedDeleteSavingById,
    topicFeedError,
    topicFeedItems,
    topicFeedLoading,
    topicFeedLoadingMore,
    topicFeedNextCursor,
    topicFeedReadSavingById,
    topicFeedTopicKey,
    topicPanelState,
    topicPresetMenuOpen,
    topics,
    updateTopicLocal,
  } = useDashboardTopicConfig({
    confirm,
    isZh,
    notify,
    selectedBot,
    setShowTopicModal,
    t,
  });
  const passwordToggleLabels = isZh
    ? { show: '显示密码', hide: '隐藏密码' }
    : { show: 'Show password', hide: 'Hide password' };
  const lc = isZh ? channelsZhCn : channelsEn;
  const runtimeMoreLabel = isZh ? '更多' : 'More';
  const selectedBotControlState = selectedBot ? controlStateByBot[selectedBot.id] : undefined;
  const selectedBotEnabled = Boolean(selectedBot && selectedBot.enabled !== false);
  const canChat = Boolean(selectedBotEnabled && selectedBot && selectedBot.docker_status === 'RUNNING' && !selectedBotControlState);
  const conversation = useMemo(() => mergeConversation(messages), [messages]);
  const latestEvent = useMemo(() => [...events].reverse()[0], [events]);
  const envEntries = useMemo(
    () =>
      Object.entries(envParams || {})
        .filter(([k]) => String(k || '').trim().length > 0)
        .sort(([a], [b]) => a.localeCompare(b)),
    [envParams],
  );
  const lastUserTs = useMemo(() => [...conversation].reverse().find((m) => m.role === 'user')?.ts || 0, [conversation]);
  const lastAssistantFinalTs = useMemo(
    () =>
      [...conversation].reverse().find((m) => m.role === 'assistant' && (m.kind || 'final') !== 'progress')?.ts || 0,
    [conversation],
  );
  const botUpdatedAtTs = useMemo(() => parseBotTimestamp(selectedBot?.updated_at), [selectedBot?.updated_at]);
  const latestRuntimeSignalTs = useMemo(() => {
    const latestEventTs = latestEvent?.ts || 0;
    return Math.max(latestEventTs, botUpdatedAtTs, lastUserTs);
  }, [latestEvent?.ts, botUpdatedAtTs, lastUserTs]);
  const hasFreshRuntimeSignal = useMemo(
    () => latestRuntimeSignalTs > 0 && Date.now() - latestRuntimeSignalTs < RUNTIME_STALE_MS,
    [latestRuntimeSignalTs],
  );
  const isThinking = useMemo(() => {
    if (!selectedBot || selectedBot.docker_status !== 'RUNNING') return false;
    if (lastUserTs <= 0) return false;
    if (lastAssistantFinalTs >= lastUserTs) return false;
    return hasFreshRuntimeSignal;
  }, [selectedBot, lastUserTs, lastAssistantFinalTs, hasFreshRuntimeSignal]);
  const {
    activeControlCommand,
    attachmentUploadPercent,
    canSendControlCommand,
    chatBottomRef,
    chatDateJumping,
    chatDatePanelPosition,
    chatDatePickerOpen,
    chatDateTriggerRef,
    chatDateValue,
    chatScrollRef,
    command,
    composerTextareaRef,
    controlCommandPanelOpen,
    controlCommandPanelRef,
    copyAssistantReply,
    copyUserPrompt,
    editUserPrompt,
    feedbackSavingByMessageId,
    filePickerRef,
    interruptExecution,
    isInterrupting,
    isSendingBlocked,
    isUploadingAttachments,
    isVoiceRecording,
    isVoiceTranscribing,
    jumpConversationToDate,
    loadInitialChatMessages,
    onChatScroll,
    onComposerKeyDown,
    onPickAttachments,
    onVoiceInput,
    pendingAttachments,
    quoteAssistantReply,
    quotedReply,
    send,
    sendControlCommand,
    setChatDatePickerOpen,
    setChatDateValue,
    setCommand,
    setControlCommandPanelOpen,
    setPendingAttachments,
    setQuotedReply,
    setVoiceCountdown,
    setVoiceMaxSeconds,
    showInterruptSubmitAction,
    submitAssistantFeedback,
    syncChatScrollToBottom,
    toggleChatDatePicker,
    triggerPickAttachments,
    voiceCountdown,
  } = useDashboardChat({
    activeBots,
    addBotMessage,
    allowedAttachmentExtensions,
    canChat,
    chatPullPageSize,
    commandAutoUnlockSeconds,
    isThinking,
    isZh,
    loadWorkspaceTree,
    messages,
    notify,
    selectedBot,
    selectedBotId,
    setAllowedAttachmentExtensions,
    setBotMessageFeedback,
    setBotMessages,
    setUploadMaxMb,
    speechEnabled,
    t,
    uploadMaxMb,
    workspaceCurrentPath,
  });
  const isChatEnabled = Boolean(canChat && !isSendingBlocked);
  const displayState = useMemo(() => {
    if (!selectedBot) return 'IDLE';
    const backendState = normalizeRuntimeState(selectedBot.current_state);
    if (selectedBot.docker_status !== 'RUNNING') return backendState;

    if (hasFreshRuntimeSignal && (backendState === 'TOOL_CALL' || backendState === 'THINKING' || backendState === 'ERROR')) {
      return backendState;
    }

    if (isThinking) {
      if (latestEvent?.state === 'TOOL_CALL') return 'TOOL_CALL';
      return 'THINKING';
    }

    if (
      latestEvent &&
      ['THINKING', 'TOOL_CALL', 'ERROR'].includes(latestEvent.state) &&
      Date.now() - latestEvent.ts < 15000
    ) {
      return latestEvent.state;
    }

    if (latestEvent?.state === 'ERROR') return 'ERROR';
    return 'IDLE';
  }, [selectedBot, isThinking, latestEvent, hasFreshRuntimeSignal]);
  const runtimeAction = useMemo(() => {
    const action = normalizeAssistantMessageText(selectedBot?.last_action || '').trim();
    if (action) return action;
    const eventText = normalizeAssistantMessageText(latestEvent?.text || '').trim();
    if (eventText) return eventText;
    return '-';
  }, [selectedBot, latestEvent]);
  const conversationNodes = (
    <BotDashboardConversationNodes
      conversation={conversation}
      expandedProgressByKey={expandedProgressByKey}
      expandedUserByKey={expandedUserByKey}
      feedbackSavingByMessageId={feedbackSavingByMessageId}
      isZh={isZh}
      onCopyAssistantReply={copyAssistantReply}
      onCopyUserPrompt={copyUserPrompt}
      onEditUserPrompt={editUserPrompt}
      onOpenWorkspacePath={(path) => {
        void openWorkspacePathFromChat(path);
      }}
      onQuoteAssistantReply={quoteAssistantReply}
      onResolveMediaSrc={resolveWorkspaceMediaSrc}
      onSubmitAssistantFeedback={submitAssistantFeedback}
      onToggleProgressExpansion={(key) => {
        setExpandedProgressByKey((prev) => ({
          ...prev,
          [key]: !prev[key],
        }));
      }}
      onToggleUserExpansion={(key) => {
        setExpandedUserByKey((prev) => ({
          ...prev,
          [key]: !prev[key],
        }));
      }}
      t={t}
      workspaceDownloadExtensionSet={workspaceDownloadExtensionSet}
    />
  );

  useEffect(() => {
    setBotListPage(1);
  }, [normalizedBotListQuery]);

  useEffect(() => {
    setBotListPage((prev) => Math.min(Math.max(prev, 1), botListTotalPages));
  }, [botListTotalPages]);

  useEffect(() => {
    const forced = String(forcedBotId || '').trim();
    if (forced) {
      if (activeBots[forced]) {
        if (selectedBotId !== forced) setSelectedBotId(forced);
      } else if (selectedBotId) {
        setSelectedBotId('');
      }
      return;
    }
    if (compactListFirstMode) {
      if (selectedBotId && !activeBots[selectedBotId]) {
        setSelectedBotId('');
      }
      return;
    }
    if (!selectedBotId && bots.length > 0) setSelectedBotId(bots[0].id);
    if (selectedBotId && !activeBots[selectedBotId] && bots.length > 0) setSelectedBotId(bots[0].id);
  }, [bots, selectedBotId, activeBots, forcedBotId, compactListFirstMode]);

  useEffect(() => {
    const onPointerDown = (event: MouseEvent) => {
      if (runtimeMenuRef.current && !runtimeMenuRef.current.contains(event.target as Node)) {
        setRuntimeMenuOpen(false);
      }
      if (botListMenuRef.current && !botListMenuRef.current.contains(event.target as Node)) {
        setBotListMenuOpen(false);
      }
      if (controlCommandPanelRef.current && !controlCommandPanelRef.current.contains(event.target as Node)) {
        setChatDatePickerOpen(false);
      }
      if (channelCreateMenuRef.current && !channelCreateMenuRef.current.contains(event.target as Node)) {
        setChannelCreateMenuOpen(false);
      }
      if (topicPresetMenuRef.current && !topicPresetMenuRef.current.contains(event.target as Node)) {
        setTopicPresetMenuOpen(false);
      }
      if (skillAddMenuRef.current && !skillAddMenuRef.current.contains(event.target as Node)) {
        setSkillAddMenuOpen(false);
      }
    };
    const onKeyDown = (event: globalThis.KeyboardEvent) => {
      if (event.key !== 'Escape') return;
      setChatDatePickerOpen(false);
      setChannelCreateMenuOpen(false);
      setTopicPresetMenuOpen(false);
      setSkillAddMenuOpen(false);
    };
    document.addEventListener('mousedown', onPointerDown);
    document.addEventListener('keydown', onKeyDown);
    return () => {
      document.removeEventListener('mousedown', onPointerDown);
      document.removeEventListener('keydown', onKeyDown);
    };
  }, []);

  useEffect(() => {
    setRuntimeMenuOpen(false);
    setBotListMenuOpen(false);
  }, [selectedBotId]);

  useEffect(() => {
    setExpandedProgressByKey({});
    setExpandedUserByKey({});
    setShowRuntimeActionModal(false);
    setWorkspaceHoverCard(null);
  }, [selectedBotId]);

  useEffect(() => {
    if (!selectedBotId) return;
    let alive = true;
    const loadBotDetail = async () => {
      try {
        const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}`);
        if (alive) mergeBot(res.data);
      } catch (error) {
        console.error(`Failed to fetch bot detail for ${selectedBotId}`, error);
      }
    };
    void loadBotDetail();
    return () => {
      alive = false;
    };
  }, [selectedBotId, mergeBot]);

  useEffect(() => {
    if (!compactMode) {
      setIsCompactMobile(false);
      setCompactPanelTab('chat');
      return;
    }
    const media = window.matchMedia('(max-width: 980px)');
    const apply = () => setIsCompactMobile(media.matches);
    apply();
    media.addEventListener('change', apply);
    return () => media.removeEventListener('change', apply);
  }, [compactMode]);

  useEffect(() => {
    if (compactMode) {
      setCompactPanelTab(initialCompactPanelTab);
    }
  }, [compactMode, initialCompactPanelTab]);

  useEffect(() => {
    if (!selectedBotId || !selectedBot) {
      setGlobalDelivery({ sendProgress: false, sendToolHints: false });
      return;
    }
    setGlobalDelivery({
      sendProgress: Boolean(selectedBot.send_progress),
      sendToolHints: Boolean(selectedBot.send_tool_hints),
    });
  }, [selectedBotId, selectedBot?.send_progress, selectedBot?.send_tool_hints]);

  const renderChannelFields = (channel: BotChannel, onPatch: (patch: Partial<BotChannel>) => void) =>
    renderBotChannelFields({
      channel,
      lc,
      onPatch,
      passwordToggleLabels,
    });

  useEffect(() => {
    if (!selectedBotId) {
      resetWorkspaceBrowserState();
      resetChannelCollection();
      resetTopicConfigState('full');
      setPendingAttachments([]);
      resetCronState();
      resetSkillsEnvState();
      resetMcpConfigState('full');
      resetTopicFeedState(true);
      return;
    }
    resetTopicConfigState('bot-switch');
    resetMcpConfigState('bot-switch');
    resetTopicFeedState();
    let cancelled = false;
    const loadAll = async () => {
      try {
        if (cancelled) return;
        await loadInitialChatMessages(selectedBotId);
        if (cancelled) return;
        await Promise.all([
          loadWorkspaceTree(selectedBotId, ''),
          loadCronJobs(selectedBotId),
          loadBotSkills(selectedBotId),
          loadBotEnvParams(selectedBotId),
          loadTopics(selectedBotId),
          loadTopicFeedStats(selectedBotId),
        ]);
        requestAnimationFrame(() => syncChatScrollToBottom('auto'));
      } catch (error: any) {
        const detail = String(error?.response?.data?.detail || '').trim();
        if (!cancelled && detail) {
          notify(detail, { tone: 'error' });
        }
      }
    };
    void loadAll();
    return () => {
      cancelled = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [selectedBotId, loadInitialChatMessages, syncChatScrollToBottom]);

  useEffect(() => {
    if (!topicFeedTopicKey || topicFeedTopicKey === '__all__') return;
    const exists = activeTopicOptions.some((row) => row.key === topicFeedTopicKey);
    if (!exists) {
      setTopicFeedTopicKey('__all__');
    }
  }, [activeTopicOptions, topicFeedTopicKey]);

  useEffect(() => {
    if (!selectedBotId || runtimeViewMode !== 'topic') return;
    if (topics.length === 0) {
      void loadTopics(selectedBotId);
    }
  }, [runtimeViewMode, selectedBotId, topics.length]);

  useEffect(() => {
    if (!selectedBot || runtimeViewMode !== 'topic') return;
    void loadTopicFeed({ append: false, topicKey: topicFeedTopicKey });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [selectedBot?.id, runtimeViewMode, topicFeedTopicKey]);

  useEffect(() => {
    if (!selectedBot || runtimeViewMode !== 'topic') return;
    if (topicDetailOpen) return;
    const timer = window.setInterval(() => {
      void loadTopicFeed({ append: false, topicKey: topicFeedTopicKey });
    }, 15000);
    return () => window.clearInterval(timer);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [selectedBot?.id, runtimeViewMode, topicFeedTopicKey, topicDetailOpen]);

  useEffect(() => {
    if (!selectedBotId) return;
    void loadTopicFeedStats(selectedBotId);
    const timer = window.setInterval(() => {
      void loadTopicFeedStats(selectedBotId);
    }, 15000);
    return () => window.clearInterval(timer);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [selectedBotId]);

  const {
    agentTab,
    applyEditFormFromBot,
    defaultSystemTimezone,
    editForm,
    ensureSelectedBotDetail,
    isSaving,
    isTestingProvider,
    onBaseProviderChange,
    paramDraft,
    providerTestResult,
    saveBot,
    setAgentTab,
    setEditForm,
    setParamDraft,
    setProviderTestResult,
    setShowAgentModal,
    setShowBaseModal,
    setShowParamModal,
    showAgentModal,
    showBaseModal,
    showParamModal,
    systemTimezoneOptions,
    testProviderConnection,
  } = useBotDashboardConfigState({
    isZh,
    mergeBot,
    notify,
    refresh,
    selectedBot,
    selectedBotId,
    setAllowedAttachmentExtensions,
    setBotListPageSize,
    setBotListPageSizeReady,
    setChatPullPageSize,
    setCommandAutoUnlockSeconds,
    setSpeechEnabled,
    setTopicPresetTemplates,
    setUploadMaxMb,
    setVoiceCountdown,
    setVoiceMaxSeconds,
    setWorkspaceDownloadExtensions,
    t,
  });


  const removeBot = async (botId?: string) => {
    const targetId = botId || selectedBot?.id;
    if (!targetId) return;
    const ok = await confirm({
      title: t.delete,
      message: t.deleteBotConfirm(targetId),
      tone: 'warning',
    });
    if (!ok) return;
    try {
      await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${targetId}`, { params: { delete_workspace: true } });
      await refresh();
      if (selectedBotId === targetId) setSelectedBotId('');
      notify(t.deleteBotDone, { tone: 'success' });
    } catch {
      notify(t.deleteFail, { tone: 'error' });
    }
  };

  const clearConversationHistory = async () => {
    if (!selectedBot) return;
    const target = selectedBot.name || selectedBot.id;
    const ok = await confirm({
      title: t.clearHistory,
      message: t.clearHistoryConfirm(target),
      tone: 'warning',
    });
    if (!ok) return;
    try {
      await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/messages`);
      setBotMessages(selectedBot.id, []);
      notify(t.clearHistoryDone, { tone: 'success' });
    } catch (error: any) {
      const msg = error?.response?.data?.detail || t.clearHistoryFail;
      notify(msg, { tone: 'error' });
    }
  };

  const exportConversationJson = () => {
    if (!selectedBot) return;
    try {
      const payload = {
        bot_id: selectedBot.id,
        bot_name: selectedBot.name || selectedBot.id,
        exported_at: new Date().toISOString(),
        message_count: conversation.length,
        messages: conversation.map((m) => ({
          id: m.id || null,
          role: m.role,
          text: m.text,
          attachments: m.attachments || [],
          kind: m.kind || 'final',
          feedback: m.feedback || null,
          ts: m.ts,
          datetime: new Date(m.ts).toISOString(),
        })),
      };
      const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json;charset=utf-8' });
      const url = URL.createObjectURL(blob);
      const stamp = new Date().toISOString().replace(/[:.]/g, '-');
      const filename = `${selectedBot.id}-conversation-${stamp}.json`;
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      a.remove();
      URL.revokeObjectURL(url);
    } catch {
      notify(t.exportHistoryFail, { tone: 'error' });
    }
  };

  return (
    <>
      <div className={`grid-ops ${compactMode ? 'grid-ops-compact' : ''} ${hasForcedBot && !compactMode ? 'grid-ops-forced' : ''}`}>
        {showBotListPanel ? (
          <BotDashboardBotListPanel
            botCount={bots.length}
            botListMenuOpen={botListMenuOpen}
            botListMenuRef={botListMenuRef}
            botListPage={botListPage}
            botListPageSizeReady={botListPageSizeReady}
            botListQuery={botListQuery}
            botListTotalPages={botListTotalPages}
            botSearchInputName={botSearchInputName}
            controlStateByBot={controlStateByBot}
            filteredBotCount={filteredBots.length}
            isBatchOperating={isBatchOperating}
            isZh={isZh}
            normalizedBotListQuery={normalizedBotListQuery}
            onBatchStartBots={() => {
              setBotListMenuOpen(false);
              void batchStartBots();
            }}
            onBatchStopBots={() => {
              setBotListMenuOpen(false);
              void batchStopBots();
            }}
            onNextPage={() => setBotListPage((page) => Math.min(botListTotalPages, page + 1))}
            onOpenCreateWizard={onOpenCreateWizard}
            onOpenImageFactory={onOpenImageFactory}
            onOpenResourceMonitor={openResourceMonitor}
            onPrevPage={() => setBotListPage((page) => Math.max(1, page - 1))}
            onQueryChange={(value) => {
              setBotListQuery(value);
              setBotListPage(1);
            }}
            onRemoveBot={(botId) => {
              void removeBot(botId);
            }}
            onSelectBot={(botId) => {
              setSelectedBotId(botId);
              if (compactMode) setCompactPanelTab('chat');
            }}
            onSetBotEnabled={(botId, enabled) => {
              void setBotEnabled(botId, enabled);
            }}
            onToggleBotListMenu={() => setBotListMenuOpen((prev) => !prev)}
            onToggleBotRunning={(botId, dockerStatus) => {
              void (String(dockerStatus || '').toUpperCase() === 'RUNNING'
                ? stopBot(botId, dockerStatus)
                : startBot(botId, dockerStatus));
            }}
            operatingBotId={operatingBotId}
            pagedBotGroups={pagedBotGroups}
            selectedBotId={selectedBotId}
            t={t}
          />
        ) : null}

        <section className={`panel ops-chat-panel ${compactMode && (isCompactListPage || compactPanelTab !== 'chat') ? 'ops-compact-hidden' : ''} ${showCompactBotPageClose ? 'ops-compact-bot-surface' : ''}`}>
          {selectedBot ? (
            <div className="ops-chat-shell">
              <div className="ops-main-content-shell">
                <div className="ops-main-content-frame">
                  <div className="ops-main-content-head">
                    <ChatModeRail
                      hasTopicUnread={hasTopicUnread}
                      isZh={isZh}
                      runtimeViewMode={runtimeViewMode}
                      onChange={setRuntimeViewMode}
                    />
                  </div>
                  <div className="ops-main-content-body">
                    {runtimeViewMode === 'topic' ? (
                      <TopicFeedPanel
                        isZh={isZh}
                        topicKey={topicFeedTopicKey}
                        topicOptions={activeTopicOptions}
                        topicState={topicPanelState}
                        items={topicFeedItems}
                        loading={topicFeedLoading}
                        loadingMore={topicFeedLoadingMore}
                        nextCursor={topicFeedNextCursor}
                        error={topicFeedError}
                        readSavingById={topicFeedReadSavingById}
                        deleteSavingById={topicFeedDeleteSavingById}
                        onTopicChange={setTopicFeedTopicKey}
                        onRefresh={() => void loadTopicFeed({ append: false, topicKey: topicFeedTopicKey })}
                        onMarkRead={(itemId) => void markTopicFeedItemRead(itemId)}
                        onDeleteItem={(item) => void deleteTopicFeedItem(item)}
                        onLoadMore={() => void loadTopicFeed({ append: true, cursor: topicFeedNextCursor, topicKey: topicFeedTopicKey })}
                        onOpenWorkspacePath={(path) => void openWorkspacePathFromChat(path)}
                        onOpenTopicSettings={() => {
                          if (selectedBot) openTopicModal(selectedBot.id);
                        }}
                        onDetailOpenChange={setTopicDetailOpen}
                        layout="panel"
                      />
                    ) : (
                      <>
                      <ConversationViewport
                        bottomRef={chatBottomRef}
                        chatScrollRef={chatScrollRef}
                        conversationNodes={conversationNodes}
                        emptyLabel={t.noConversation}
                        hasConversation={conversation.length > 0}
                        isChatEnabled={isChatEnabled}
                        isThinking={isThinking}
                        onScroll={onChatScroll}
                        thinkingLabel={t.thinking}
                      />
                      <ChatComposerDock
                        activeControlCommand={activeControlCommand}
                        allowedAttachmentExtensions={allowedAttachmentExtensions}
                        attachmentUploadPercent={attachmentUploadPercent}
                        canChat={canChat}
                        canSendControlCommand={canSendControlCommand}
                        chatDateJumping={chatDateJumping}
                        chatDatePanelPosition={chatDatePanelPosition}
                        chatDatePickerOpen={chatDatePickerOpen}
                        chatDateTriggerRef={chatDateTriggerRef}
                        chatDateValue={chatDateValue}
                        command={command}
                        composerTextareaRef={composerTextareaRef}
                        controlCommandPanelOpen={controlCommandPanelOpen}
                        controlCommandPanelRef={controlCommandPanelRef}
                        disabledSend={
                          showInterruptSubmitAction
                            ? isInterrupting
                            : (
                              !isChatEnabled
                              || isVoiceRecording
                              || isVoiceTranscribing
                              || (!command.trim() && pendingAttachments.length === 0 && !quotedReply)
                            )
                        }
                        filePickerRef={filePickerRef}
                        isCompactMobile={isCompactMobile}
                        isInterrupting={isInterrupting}
                        isUploadingAttachments={isUploadingAttachments}
                        isVoiceRecording={isVoiceRecording}
                        isVoiceTranscribing={isVoiceTranscribing}
                        isZh={isZh}
                        onClearQuotedReply={() => setQuotedReply(null)}
                        onCommandChange={setCommand}
                        onCloseChatDatePicker={() => setChatDatePickerOpen(false)}
                        onComposerKeyDown={onComposerKeyDown}
                        onDateValueChange={setChatDateValue}
                        onInterruptExecution={() => void interruptExecution()}
                        onJumpConversationToDate={() => void jumpConversationToDate()}
                        onOpenWorkspacePath={(path) => void openWorkspacePathFromChat(path)}
                        onPickAttachments={onPickAttachments}
                        onRemovePendingAttachment={(path) => setPendingAttachments((prev) => prev.filter((v) => v !== path))}
                        onSend={() => void (showInterruptSubmitAction ? interruptExecution() : send())}
                        onSendControlCommand={(value) => void sendControlCommand(value)}
                        onToggleChatDatePicker={toggleChatDatePicker}
                        onToggleControlPanel={() => {
                          setChatDatePickerOpen(false);
                          setControlCommandPanelOpen((prev) => !prev);
                        }}
                        onTriggerPickAttachments={triggerPickAttachments}
                        onVoiceInput={onVoiceInput}
                        pendingAttachments={pendingAttachments}
                        quotedReply={quotedReply}
                        selectedBotId={selectedBotId}
                        showInterruptSubmitAction={showInterruptSubmitAction}
                        t={t}
                        voiceCountdown={voiceCountdown}
                        workspaceDownloadExtensionSet={workspaceDownloadExtensionSet}
                      />
                      <ChatDisabledMask
                        botDisabledHint={t.botDisabledHint}
                        botStarting={t.botStarting}
                        botStopping={t.botStopping}
                        canChat={canChat}
                        chatDisabled={t.chatDisabled}
                        selectedBotControlState={selectedBotControlState}
                        selectedBotEnabled={selectedBotEnabled}
                      />
                      </>
                    )}
                  </div>
                </div>
              </div>
            </div>
          ) : (
            <div style={{ color: 'var(--muted)' }}>
              {forcedBotMissing
                ? `${t.selectBot}: ${String(forcedBotId).trim()}`
                : t.selectBot}
            </div>
          )}
        </section>

        <BotDashboardRuntimePanel
          compactMode={compactMode}
          compactPanelTab={compactPanelTab}
          displayState={displayState}
          filteredWorkspaceEntries={filteredWorkspaceEntries}
          forcedBotId={forcedBotId}
          forcedBotMissing={forcedBotMissing}
          hideWorkspaceHoverCard={hideWorkspaceHoverCard}
          isCompactListPage={isCompactListPage}
          lc={lc}
          loadBotEnvParams={loadBotEnvParams}
          loadBotSkills={loadBotSkills}
          loadCronJobs={loadCronJobs}
          loadWorkspaceTree={loadWorkspaceTree}
          onClearConversationHistory={() => {
            void clearConversationHistory();
          }}
          onEnsureSelectedBotDetail={async () => {
            const detail = await ensureSelectedBotDetail();
            applyEditFormFromBot(detail);
            return detail;
          }}
          onExportConversationJson={exportConversationJson}
          onOpenChannelModal={openChannelModal}
          onOpenMcpModal={(botId) => {
            void openMcpModal(botId);
          }}
          onOpenTopicModal={openTopicModal}
          onOpenWorkspaceFilePreview={(path) => {
            void openWorkspaceFilePreview(path);
          }}
          onRestartBot={(botId, dockerStatus) => {
            void restartBot(botId, dockerStatus);
          }}
          onSetRuntimeMenuOpen={setRuntimeMenuOpen}
          onShowWorkspaceHoverCard={showWorkspaceHoverCard}
          operatingBotId={operatingBotId}
          runtimeMenuOpen={runtimeMenuOpen}
          runtimeMenuRef={runtimeMenuRef}
          runtimeMoreLabel={runtimeMoreLabel}
          selectedBot={selectedBot}
          selectedBotEnabled={selectedBotEnabled}
          selectedBotId={selectedBotId}
          setProviderTestResult={setProviderTestResult}
          setShowAgentModal={setShowAgentModal}
          setShowBaseModal={setShowBaseModal}
          setShowCronModal={setShowCronModal}
          setShowEnvParamsModal={setShowEnvParamsModal}
          setShowParamModal={setShowParamModal}
          setShowSkillsModal={setShowSkillsModal}
          setWorkspaceAutoRefresh={setWorkspaceAutoRefresh}
          setWorkspaceQuery={setWorkspaceQuery}
          showCompactBotPageClose={showCompactBotPageClose}
          t={t}
          workspaceAutoRefresh={workspaceAutoRefresh}
          workspaceCurrentPath={workspaceCurrentPath}
          workspaceDownloadExtensionSet={workspaceDownloadExtensionSet}
          workspaceError={workspaceError}
          workspaceFileLoading={workspaceFileLoading}
          workspaceFiles={workspaceFiles}
          workspaceLoading={workspaceLoading}
          workspaceParentPath={workspaceParentPath}
          workspacePathDisplay={workspacePathDisplay}
          workspaceQuery={workspaceQuery}
          workspaceSearchInputName={workspaceSearchInputName}
          workspaceSearchLoading={workspaceSearchLoading}
        />
      </div>
      {showCompactBotPageClose ? (
        <LucentIconButton
          className="ops-compact-close-btn"
          onClick={() => {
            setSelectedBotId('');
            setCompactPanelTab('chat');
          }}
          tooltip={isZh ? '关闭并返回 Bot 列表' : 'Close and back to bot list'}
          aria-label={isZh ? '关闭并返回 Bot 列表' : 'Close and back to bot list'}
        >
          <X size={16} />
        </LucentIconButton>
      ) : null}
      {compactMode && !isCompactListPage && !hideCompactFab ? (
        <div className="ops-compact-fab-stack">
          <LucentIconButton
            className={`ops-compact-fab-switch ${compactPanelTab === 'chat' ? 'is-chat' : 'is-runtime'}`}
            onClick={() => setCompactPanelTab((v) => (v === 'runtime' ? 'chat' : 'runtime'))}
            tooltip={compactPanelTab === 'runtime' ? (isZh ? '切换到对话面板' : 'Switch to chat') : (isZh ? '切换到运行面板' : 'Switch to runtime')}
            aria-label={compactPanelTab === 'runtime' ? (isZh ? '切换到对话面板' : 'Switch to chat') : (isZh ? '切换到运行面板' : 'Switch to runtime')}
          >
            {compactPanelTab === 'runtime' ? <MessageSquareText size={18} /> : <Activity size={18} />}
          </LucentIconButton>
        </div>
      ) : null}

      <ResourceMonitorModal
        botId={resourceBotId}
        botImageTag={resourceBot?.image_tag || '-'}
        botModel={resourceBot?.llm_model || '-'}
        botName={resourceBot?.name || ''}
        botProvider={resourceBot?.llm_provider || '-'}
        error={resourceError}
        isOpen={showResourceModal}
        labels={{
          baseImage: isZh ? '基础镜像' : 'Base Image',
          blockIo: isZh ? '磁盘 I/O' : 'Block I/O',
          close: t.close,
          collected: isZh ? '采样时间' : 'Collected',
          configuredLimits: isZh ? '配置配额' : 'Configured Limits',
          container: isZh ? '容器状态' : 'Container',
          containerName: isZh ? '容器名' : 'Container Name',
          cpu: 'CPU',
          dockerRuntimeLimits: isZh ? 'Docker 实际限制' : 'Docker Runtime Limits',
          liveUsage: isZh ? '实时使用' : 'Live Usage',
          loading: isZh ? '读取中...' : 'Loading...',
          memory: isZh ? '内存' : 'Memory',
          memoryPercent: isZh ? '内存占比' : 'Memory %',
          networkIo: isZh ? '网络 I/O' : 'Network I/O',
          noMetrics: isZh ? '暂无监控数据' : 'No metrics',
          policy: isZh ? '策略说明' : 'Policy',
          providerModel: 'Provider/Model',
          refreshNow: isZh ? '立即刷新' : 'Refresh now',
          resourceMonitor: isZh ? '资源监测' : 'Resource Monitor',
          sizeUnlimited: isZh ? '不限' : 'Unlimited',
          storage: isZh ? '存储' : 'Storage',
          uiRuleHint: isZh ? '（界面规则：资源配置填写 0 表示不限制）' : ' (UI rule: value 0 means unlimited)',
          workspacePercent: isZh ? '工作区占比' : 'Workspace %',
          workspaceUsage: isZh ? '工作区占用' : 'Workspace Usage',
        }}
        loading={resourceLoading}
        onClose={() => setShowResourceModal(false)}
        onRefresh={() => void loadResourceSnapshot(resourceBotId)}
        snapshot={resourceSnapshot}
      />
      <BotDashboardConfigDrawers
        isZh={isZh}
        t={t}
        lc={lc}
        noteLocale={noteLocale}
        passwordToggleLabels={passwordToggleLabels}
        defaultSystemTimezone={defaultSystemTimezone}
        systemTimezoneOptions={systemTimezoneOptions}
        selectedBotId={selectedBot?.id || ''}
        selectedBotExists={Boolean(selectedBot)}
        editForm={editForm}
        setEditForm={setEditForm}
        paramDraft={paramDraft}
        setParamDraft={setParamDraft}
        agentTab={agentTab}
        setAgentTab={setAgentTab}
        isSaving={isSaving}
        saveBot={saveBot}
        showBaseModal={showBaseModal}
        setShowBaseModal={setShowBaseModal}
        showParamModal={showParamModal}
        setShowParamModal={setShowParamModal}
        isTestingProvider={isTestingProvider}
        providerTestResult={providerTestResult}
        onBaseProviderChange={onBaseProviderChange}
        onTestProviderConnection={testProviderConnection}
        onTemperatureChange={(value) => setEditForm((prev) => ({ ...prev, temperature: clampTemperature(value) }))}
        showChannelModal={showChannelModal}
        setShowChannelModal={setShowChannelModal}
        channelCreateMenuOpen={channelCreateMenuOpen}
        setChannelCreateMenuOpen={setChannelCreateMenuOpen}
        channelCreateMenuRef={channelCreateMenuRef}
        addableChannelTypes={addableChannelTypes}
        channels={channels}
        expandedChannelByKey={expandedChannelByKey}
        setExpandedChannelByKey={setExpandedChannelByKey}
        globalDelivery={globalDelivery}
        isSavingChannel={isSavingChannel}
        isSavingGlobalDelivery={isSavingGlobalDelivery}
        newChannelDraft={newChannelDraft}
        setNewChannelDraft={setNewChannelDraft}
        newChannelPanelOpen={newChannelPanelOpen}
        setNewChannelPanelOpen={setNewChannelPanelOpen}
        resetNewChannelDraft={resetNewChannelDraft}
        addChannel={addChannel}
        saveChannel={saveChannel}
        saveGlobalDelivery={saveGlobalDelivery}
        updateChannelLocal={updateChannelLocal}
        updateGlobalDeliveryFlag={updateGlobalDeliveryFlag}
        removeChannel={removeChannel}
        renderChannelFields={renderChannelFields}
        isChannelConfigured={isChannelConfigured}
        channelDraftUiKey={channelDraftUiKey}
        isDashboardChannel={isDashboardChannel}
        beginChannelCreate={beginChannelCreate}
        showTopicModal={showTopicModal}
        setShowTopicModal={setShowTopicModal}
        effectiveTopicPresetTemplates={effectiveTopicPresetTemplates}
        topicPresetMenuOpen={topicPresetMenuOpen}
        setTopicPresetMenuOpen={setTopicPresetMenuOpen}
        topicPresetMenuRef={topicPresetMenuRef}
        beginTopicCreate={beginTopicCreate}
        expandedTopicByKey={expandedTopicByKey}
        setExpandedTopicByKey={setExpandedTopicByKey}
        isSavingTopic={isSavingTopic}
        topics={topics}
        newTopicAdvancedOpen={newTopicAdvancedOpen}
        setNewTopicAdvancedOpen={setNewTopicAdvancedOpen}
        newTopicDescription={newTopicDescription}
        setNewTopicDescription={setNewTopicDescription}
        newTopicExamplesNegative={newTopicExamplesNegative}
        setNewTopicExamplesNegative={setNewTopicExamplesNegative}
        newTopicExamplesPositive={newTopicExamplesPositive}
        setNewTopicExamplesPositive={setNewTopicExamplesPositive}
        newTopicExcludeWhen={newTopicExcludeWhen}
        setNewTopicExcludeWhen={setNewTopicExcludeWhen}
        newTopicIncludeWhen={newTopicIncludeWhen}
        setNewTopicIncludeWhen={setNewTopicIncludeWhen}
        newTopicKey={newTopicKey}
        setNewTopicKey={setNewTopicKey}
        newTopicName={newTopicName}
        setNewTopicName={setNewTopicName}
        newTopicPanelOpen={newTopicPanelOpen}
        setNewTopicPanelOpen={setNewTopicPanelOpen}
        newTopicPriority={newTopicPriority}
        setNewTopicPriority={setNewTopicPriority}
        newTopicPurpose={newTopicPurpose}
        setNewTopicPurpose={setNewTopicPurpose}
        newTopicSourceLabel={newTopicSourceLabel}
        resetNewTopicDraft={resetNewTopicDraft}
        addTopic={addTopic}
        normalizeTopicKeyInput={normalizeTopicKeyInput}
        removeTopic={removeTopic}
        saveTopic={saveTopic}
        updateTopicLocal={updateTopicLocal}
        topicDraftUiKey={topicDraftUiKey}
        showSkillsModal={showSkillsModal}
        setShowSkillsModal={setShowSkillsModal}
        skillZipPickerRef={skillZipPickerRef}
        skillAddMenuOpen={skillAddMenuOpen}
        setSkillAddMenuOpen={setSkillAddMenuOpen}
        isSkillUploading={isSkillUploading}
        skillAddMenuRef={skillAddMenuRef}
        onPickSkillZip={onPickSkillZip}
        triggerSkillZipUpload={triggerSkillZipUpload}
        botSkills={botSkills}
        removeBotSkill={removeBotSkill}
        showSkillMarketInstallModal={showSkillMarketInstallModal}
        setShowSkillMarketInstallModal={setShowSkillMarketInstallModal}
        marketSkills={marketSkills}
        isMarketSkillsLoading={isMarketSkillsLoading}
        marketSkillInstallingId={marketSkillInstallingId}
        loadMarketSkills={loadMarketSkills}
        loadBotSkills={loadBotSkills}
        installMarketSkill={installMarketSkill}
        showMcpModal={showMcpModal}
        setShowMcpModal={setShowMcpModal}
        newMcpPanelOpen={newMcpPanelOpen}
        setNewMcpPanelOpen={setNewMcpPanelOpen}
        newMcpDraft={newMcpDraft}
        setNewMcpDraft={setNewMcpDraft}
        resetNewMcpDraft={resetNewMcpDraft}
        beginMcpCreate={beginMcpCreate}
        canRemoveMcpServer={canRemoveMcpServer}
        expandedMcpByKey={expandedMcpByKey}
        setExpandedMcpByKey={setExpandedMcpByKey}
        isSavingMcp={isSavingMcp}
        removeMcpServer={removeMcpServer}
        saveSingleMcpServer={saveSingleMcpServer}
        updateMcpServer={updateMcpServer}
        mcpDraftUiKey={mcpDraftUiKey}
        mcpServers={mcpServers}
        saveNewMcpServer={saveNewMcpServer}
        showEnvParamsModal={showEnvParamsModal}
        setShowEnvParamsModal={setShowEnvParamsModal}
        envDraftKey={envDraftKey}
        setEnvDraftKey={setEnvDraftKey}
        envDraftValue={envDraftValue}
        setEnvDraftValue={setEnvDraftValue}
        envEntries={envEntries}
        upsertEnvParam={upsertEnvParam}
        removeEnvParam={removeEnvParam}
        saveBotEnvParams={saveBotEnvParams}
        showCronModal={showCronModal}
        setShowCronModal={setShowCronModal}
        cronActionJobId={cronActionJobId}
        cronJobs={cronJobs}
        cronLoading={cronLoading}
        loadCronJobs={loadCronJobs}
        deleteCronJob={deleteCronJob}
        stopCronJob={stopCronJob}
        showAgentModal={showAgentModal}
        setShowAgentModal={setShowAgentModal}
      />

      {showRuntimeActionModal && (
        <div className="modal-mask" onClick={() => setShowRuntimeActionModal(false)}>
          <div className="modal-card modal-preview" onClick={(e) => e.stopPropagation()}>
            <div className="modal-title-row modal-title-with-close">
              <div className="modal-title-main">
                <h3>{t.lastAction}</h3>
              </div>
              <div className="modal-title-actions">
                <LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowRuntimeActionModal(false)} tooltip={t.close} aria-label={t.close}>
                  <X size={14} />
                </LucentIconButton>
              </div>
            </div>
            <div className="workspace-preview-body">
              <pre>{runtimeAction}</pre>
            </div>
          </div>
        </div>
      )}

      {workspacePreview ? (
        <WorkspacePreviewModal
          buildWorkspaceDownloadHref={buildWorkspaceDownloadHref}
          buildWorkspaceRawHref={buildWorkspaceRawHref}
          closeLabel={t.close}
          closePreview={closeWorkspacePreview}
          copyAddressLabel={t.copyAddress}
          copyPathLabel={isZh ? '复制路径' : 'Copy path'}
          downloadLabel={t.download}
          editFileLabel={t.editFile}
          filePreviewLabel={t.filePreview}
          fileTruncatedLabel={t.fileTruncated}
          fullscreenEditLabel={isZh ? '全屏编辑' : 'Full screen editor'}
          fullscreenExitLabel={isZh ? '退出全屏' : 'Exit full screen'}
          fullscreenPreviewLabel={isZh ? '全屏预览' : 'Full screen'}
          isZh={isZh}
          onCopyPreviewPath={(path) => void copyWorkspacePreviewPath(path)}
          onCopyPreviewUrl={(path) => void copyWorkspacePreviewUrl(path)}
          onOpenWorkspacePath={(path) => void openWorkspacePathFromChat(path)}
          onSaveShortcut={() => {
            void saveWorkspacePreviewMarkdown();
          }}
          preview={workspacePreview}
          previewCanEdit={workspacePreviewCanEdit}
          previewDraft={workspacePreviewDraft}
          previewEditorEnabled={workspacePreviewEditorEnabled}
          previewFullscreen={workspacePreviewFullscreen}
          previewSaving={workspacePreviewSaving}
          resolveMediaSrc={resolveWorkspaceMediaSrc}
          saveLabel={t.save}
          setPreviewDraft={setWorkspacePreviewDraft}
          setPreviewFullscreen={setWorkspacePreviewFullscreen}
          setPreviewMode={setWorkspacePreviewMode}
        />
      ) : null}
      {workspaceHoverCard ? <WorkspaceHoverTooltip hoverCard={workspaceHoverCard} isZh={isZh} /> : null}
    </>
  );
}
