import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import axios from 'axios'; import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; import { APP_ENDPOINTS } from '../../../config/env'; import { sortBotsByCreatedAtDesc } from '../../dashboard/utils'; import { useAppStore } from '../../../store/appStore'; import type { BotState } from '../../../types/bot'; import { normalizePlatformPageSize, readCachedPlatformPageSize, writeCachedPlatformPageSize, } from '../../../utils/platformPageSize'; import type { BotActivityStatsItem, PlatformBotResourceSnapshot, PlatformOverviewResponse, PlatformSettings, PlatformUsageAnalyticsSeriesItem, PlatformUsageResponse, } from '../types'; import { buildBotPanelHref, buildPlatformUsageAnalyticsSeries, buildPlatformUsageAnalyticsTicks, clampPlatformPercent, getPlatformChartCeiling, } from '../utils'; interface UsePlatformDashboardOptions { compactMode: boolean; } export function usePlatformDashboard({ compactMode }: UsePlatformDashboardOptions) { const { activeBots, setBots, updateBotStatus, locale } = useAppStore(); const { notify, confirm } = useLucentPrompt(); const isZh = locale === 'zh'; const [overview, setOverview] = useState(null); const [loading, setLoading] = useState(false); const [selectedBotId, setSelectedBotId] = useState(''); const [search, setSearch] = useState(''); const [operatingBotId, setOperatingBotId] = useState(''); const [showBotLastActionModal, setShowBotLastActionModal] = useState(false); const [showResourceModal, setShowResourceModal] = useState(false); const [selectedBotDetail, setSelectedBotDetail] = useState(null); const [selectedBotUsageSummary, setSelectedBotUsageSummary] = useState(null); const [resourceBotId, setResourceBotId] = useState(''); const [resourceSnapshot, setResourceSnapshot] = useState(null); const [resourceLoading, setResourceLoading] = useState(false); const [resourceError, setResourceError] = useState(''); const [usageData, setUsageData] = useState(null); const [usageLoading, setUsageLoading] = useState(false); const [activityStatsData, setActivityStatsData] = useState(null); const [activityLoading, setActivityLoading] = useState(false); const [usagePage, setUsagePage] = useState(1); const [usagePageSize, setUsagePageSize] = useState(() => readCachedPlatformPageSize(10)); const [botListPage, setBotListPage] = useState(1); const [botListPageSize, setBotListPageSize] = useState(() => readCachedPlatformPageSize(10)); const [showCompactBotSheet, setShowCompactBotSheet] = useState(false); const [compactSheetClosing, setCompactSheetClosing] = useState(false); const [compactSheetMounted, setCompactSheetMounted] = useState(false); const compactSheetTimerRef = useRef(null); const botList = useMemo(() => { return sortBotsByCreatedAtDesc(Object.values(activeBots)) as BotState[]; }, [activeBots]); const filteredBots = useMemo(() => { const keyword = search.trim().toLowerCase(); if (!keyword) return botList; return botList.filter((bot) => `${bot.name} ${bot.id}`.toLowerCase().includes(keyword)); }, [botList, search]); const botListPageCount = useMemo( () => Math.max(1, Math.ceil(filteredBots.length / botListPageSize)), [filteredBots.length, botListPageSize], ); const pagedBots = useMemo(() => { const page = Math.min(Math.max(1, botListPage), botListPageCount); const start = (page - 1) * botListPageSize; return filteredBots.slice(start, start + botListPageSize); }, [filteredBots, botListPage, botListPageCount, botListPageSize]); const selectedBot = useMemo( () => (selectedBotId ? botList.find((bot) => bot.id === selectedBotId) : undefined), [botList, selectedBotId], ); const loadBots = useCallback(async () => { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots`); setBots(res.data); }, [setBots]); const loadOverview = useCallback(async () => { setLoading(true); try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/platform/overview`); setOverview(res.data); const normalizedPageSize = normalizePlatformPageSize( res.data?.settings?.page_size, readCachedPlatformPageSize(10), ); writeCachedPlatformPageSize(normalizedPageSize); setUsagePageSize(normalizedPageSize); setBotListPageSize(normalizedPageSize); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' }); } finally { setLoading(false); } }, [isZh, notify]); const loadUsage = useCallback(async (page = 1) => { setUsageLoading(true); try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/platform/usage`, { params: { limit: usagePageSize, offset: Math.max(0, page - 1) * usagePageSize, }, }); setUsageData(res.data); setUsagePage(page); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '读取用量统计失败。' : 'Failed to load usage analytics.'), { tone: 'error' }); } finally { setUsageLoading(false); } }, [isZh, notify, usagePageSize]); const loadActivityStats = useCallback(async () => { setActivityLoading(true); try { const res = await axios.get<{ items: BotActivityStatsItem[] }>(`${APP_ENDPOINTS.apiBase}/platform/activity-stats`); setActivityStatsData(Array.isArray(res.data?.items) ? res.data.items : []); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '读取 Bot 活跃度统计失败。' : 'Failed to load bot activity analytics.'), { tone: 'error' }); } finally { setActivityLoading(false); } }, [isZh, notify]); const loadSelectedBotUsageSummary = useCallback(async (botId: string) => { if (!botId) { setSelectedBotUsageSummary(null); return; } try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/platform/usage`, { params: { bot_id: botId, limit: 1, offset: 0, }, }); setSelectedBotUsageSummary(res.data?.summary || null); } catch { setSelectedBotUsageSummary(null); } }, []); const loadResourceSnapshot = useCallback(async (botId: string) => { if (!botId) return; setResourceLoading(true); setResourceError(''); try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(botId)}/resources`); setResourceSnapshot(res.data); } catch (error: any) { const msg = error?.response?.data?.detail || (isZh ? '读取资源监控失败。' : 'Failed to load resource metrics.'); setResourceError(String(msg)); } finally { setResourceLoading(false); } }, [isZh]); useEffect(() => { void loadOverview(); }, [loadOverview]); useEffect(() => { void loadUsage(1); }, [loadUsage, usagePageSize]); useEffect(() => { void loadActivityStats(); }, [loadActivityStats]); useEffect(() => { setBotListPage(1); }, [search, botListPageSize]); useEffect(() => { setBotListPage((prev) => Math.min(Math.max(prev, 1), botListPageCount)); }, [botListPageCount]); useEffect(() => { if (!selectedBotId && filteredBots[0]?.id) setSelectedBotId(filteredBots[0].id); }, [filteredBots, selectedBotId]); useEffect(() => { if (!compactMode) { setShowCompactBotSheet(false); setCompactSheetClosing(false); setCompactSheetMounted(false); return; } if (selectedBotId && showCompactBotSheet) return; if (!selectedBotId) setShowCompactBotSheet(false); }, [compactMode, selectedBotId, showCompactBotSheet]); useEffect(() => { if (!selectedBotId) { setSelectedBotDetail(null); setSelectedBotUsageSummary(null); return; } let alive = true; void (async () => { try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(selectedBotId)}`); if (alive) { setSelectedBotDetail(res.data); } } catch { if (alive) { setSelectedBotDetail(null); } } })(); void loadSelectedBotUsageSummary(selectedBotId); return () => { alive = false; }; }, [loadSelectedBotUsageSummary, selectedBotId]); const resourceBot = useMemo( () => (resourceBotId ? botList.find((bot) => bot.id === resourceBotId) : undefined), [botList, resourceBotId], ); const selectedBotInfo = useMemo(() => { if (selectedBotDetail && selectedBotDetail.id === selectedBotId) { return { ...selectedBot, ...selectedBotDetail, logs: (selectedBotDetail.logs && selectedBotDetail.logs.length > 0) ? selectedBotDetail.logs : (selectedBot?.logs || []), messages: (selectedBotDetail.messages && selectedBotDetail.messages.length > 0) ? selectedBotDetail.messages : (selectedBot?.messages || []), events: (selectedBotDetail.events && selectedBotDetail.events.length > 0) ? selectedBotDetail.events : (selectedBot?.events || []), } as BotState; } return selectedBot; }, [selectedBot, selectedBotDetail, selectedBotId]); const lastActionPreview = useMemo( () => selectedBotInfo?.last_action?.trim() || '', [selectedBotInfo?.last_action], ); const overviewBots = overview?.summary.bots; const overviewImages = overview?.summary.images; const overviewResources = overview?.summary.resources; const activityStats = activityStatsData || overview?.activity_stats; const usageSummary = usageData?.summary || overview?.usage.summary; const usageAnalytics = usageData?.analytics || overview?.usage.analytics || null; const memoryPercent = overviewResources && overviewResources.live_memory_limit_bytes > 0 ? clampPlatformPercent((overviewResources.live_memory_used_bytes / overviewResources.live_memory_limit_bytes) * 100) : 0; const storagePercent = overviewResources && overviewResources.workspace_limit_bytes > 0 ? clampPlatformPercent((overviewResources.workspace_used_bytes / overviewResources.workspace_limit_bytes) * 100) : 0; const usageAnalyticsSeries = useMemo( () => buildPlatformUsageAnalyticsSeries(usageAnalytics, isZh), [isZh, usageAnalytics], ); const usageAnalyticsMax = useMemo(() => { const maxDailyRequests = usageAnalyticsSeries.reduce( (max, item) => Math.max(max, ...item.daily_counts.map((count) => Number(count || 0))), 0, ); return getPlatformChartCeiling(maxDailyRequests); }, [usageAnalyticsSeries]); const usageAnalyticsTicks = useMemo(() => buildPlatformUsageAnalyticsTicks(usageAnalyticsMax), [usageAnalyticsMax]); const refreshAll = useCallback(async () => { const jobs: Promise[] = [loadOverview(), loadBots(), loadUsage(usagePage), loadActivityStats()]; if (selectedBotId) jobs.push(loadSelectedBotUsageSummary(selectedBotId)); await Promise.allSettled(jobs); }, [loadActivityStats, loadBots, loadOverview, loadSelectedBotUsageSummary, loadUsage, selectedBotId, usagePage]); const toggleBot = useCallback(async (bot: BotState) => { setOperatingBotId(bot.id); try { if (bot.docker_status === 'RUNNING') { await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${bot.id}/stop`); updateBotStatus(bot.id, 'STOPPED'); } else { await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${bot.id}/start`); updateBotStatus(bot.id, 'RUNNING'); } await refreshAll(); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? 'Bot 操作失败。' : 'Bot action failed.'), { tone: 'error' }); } finally { setOperatingBotId(''); } }, [isZh, notify, refreshAll, updateBotStatus]); const setBotEnabled = useCallback(async (bot: BotState, enabled: boolean) => { setOperatingBotId(bot.id); try { await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${bot.id}/${enabled ? 'enable' : 'disable'}`); await refreshAll(); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '更新 Bot 状态失败。' : 'Failed to update bot status.'), { tone: 'error' }); } finally { setOperatingBotId(''); } }, [isZh, notify, refreshAll]); const removeBot = useCallback(async (bot: BotState) => { const targetId = String(bot.id || '').trim(); if (!targetId) return; const ok = await confirm({ title: isZh ? '删除 Bot' : 'Delete Bot', message: isZh ? `确认删除 Bot ${targetId}?将删除对应 workspace。` : `Delete Bot ${targetId}? Its workspace will also be removed.`, tone: 'warning', }); if (!ok) return; setOperatingBotId(targetId); try { await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetId)}`, { params: { delete_workspace: true }, }); if (selectedBotId === targetId) { setSelectedBotId(''); setSelectedBotDetail(null); setShowBotLastActionModal(false); } await refreshAll(); notify(isZh ? 'Bot 已删除。' : 'Bot deleted.', { tone: 'success' }); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '删除 Bot 失败。' : 'Failed to delete bot.'), { tone: 'error' }); } finally { setOperatingBotId(''); } }, [confirm, isZh, notify, refreshAll, selectedBotId]); const clearDashboardDirectSession = useCallback(async (bot: BotState) => { const targetId = String(bot.id || '').trim(); if (!targetId) return; const ok = await confirm({ title: isZh ? '清除面板 Session' : 'Clear Dashboard Session', message: isZh ? `确认清空 Bot ${targetId} 的 dashboard_direct.jsonl 内容?\n\n这会重置面板对话上下文;若 Bot 正在运行,还会同步切到新会话。` : `Clear dashboard_direct.jsonl for Bot ${targetId}?\n\nThis resets the dashboard conversation context. If the bot is running, it will also switch to a fresh session.`, tone: 'warning', confirmText: isZh ? '清除' : 'Clear', }); if (!ok) return; setOperatingBotId(targetId); try { await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetId)}/sessions/dashboard-direct/clear`); notify(isZh ? '面板 Session 已清空。' : 'Dashboard session cleared.', { tone: 'success' }); await refreshAll(); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '清空面板 Session 失败。' : 'Failed to clear dashboard session.'), { tone: 'error' }); } finally { setOperatingBotId(''); } }, [confirm, isZh, notify, refreshAll]); const openResourceMonitor = useCallback((botId: string) => { setResourceBotId(botId); setShowResourceModal(true); void loadResourceSnapshot(botId); }, [loadResourceSnapshot]); useEffect(() => { if (compactMode && showCompactBotSheet && selectedBotInfo) { if (compactSheetTimerRef.current) { window.clearTimeout(compactSheetTimerRef.current); compactSheetTimerRef.current = null; } setCompactSheetMounted(true); setCompactSheetClosing(false); return; } if (!compactSheetMounted) return; setCompactSheetClosing(true); compactSheetTimerRef.current = window.setTimeout(() => { setCompactSheetMounted(false); setCompactSheetClosing(false); compactSheetTimerRef.current = null; }, 240); return () => { if (compactSheetTimerRef.current) { window.clearTimeout(compactSheetTimerRef.current); compactSheetTimerRef.current = null; } }; }, [compactMode, compactSheetMounted, selectedBotInfo, showCompactBotSheet]); useEffect(() => { if (!showResourceModal || !resourceBotId) return; let stopped = false; const tick = async () => { if (stopped) return; await loadResourceSnapshot(resourceBotId); }; const timer = window.setInterval(() => { void tick(); }, 2000); return () => { stopped = true; window.clearInterval(timer); }; }, [loadResourceSnapshot, resourceBotId, showResourceModal]); const handleSelectBot = useCallback((botId: string) => { setSelectedBotId(botId); if (compactMode) setShowCompactBotSheet(true); }, [compactMode]); const closeCompactBotSheet = useCallback(() => setShowCompactBotSheet(false), []); const openBotPanel = useCallback((botId: string) => { if (!botId || typeof window === 'undefined') return; window.open(buildBotPanelHref(botId), '_blank', 'noopener,noreferrer'); }, []); const handlePlatformSettingsSaved = useCallback((settings: PlatformSettings) => { setOverview((prev) => (prev ? { ...prev, settings } : prev)); const normalizedPageSize = normalizePlatformPageSize(settings.page_size, 10); writeCachedPlatformPageSize(normalizedPageSize); setUsagePageSize(normalizedPageSize); setBotListPageSize(normalizedPageSize); }, []); const closeResourceModal = useCallback(() => setShowResourceModal(false), []); return { botList, botListPage, botListPageCount, botListPageSize, closeCompactBotSheet, closeResourceModal, clearDashboardDirectSession, compactSheetClosing, compactSheetMounted, filteredBots, handlePlatformSettingsSaved, handleSelectBot, isZh, lastActionPreview, loadResourceSnapshot, loading, memoryPercent, openBotPanel, openResourceMonitor, operatingBotId, overview, overviewBots, overviewImages, overviewResources, pagedBots, refreshAll, removeBot, resourceBot, resourceBotId, resourceError, resourceLoading, resourceSnapshot, search, selectedBotId, selectedBotInfo, selectedBotUsageSummary, setBotEnabled, setBotListPage, setSearch, setShowBotLastActionModal, showBotLastActionModal, showCompactBotSheet, showResourceModal, storagePercent, toggleBot, usageAnalytics, activityStats, activityLoading, usageAnalyticsMax, usageAnalyticsSeries, usageAnalyticsTicks, usageData, usageLoading, usagePage, usageSummary, }; }