dashboard-nanobot/frontend/src/modules/platform/hooks/usePlatformDashboard.ts

515 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<PlatformOverviewResponse | null>(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<BotState | null>(null);
const [selectedBotUsageSummary, setSelectedBotUsageSummary] = useState<PlatformUsageResponse['summary'] | null>(null);
const [resourceBotId, setResourceBotId] = useState('');
const [resourceSnapshot, setResourceSnapshot] = useState<PlatformBotResourceSnapshot | null>(null);
const [resourceLoading, setResourceLoading] = useState(false);
const [resourceError, setResourceError] = useState('');
const [usageData, setUsageData] = useState<PlatformUsageResponse | null>(null);
const [usageLoading, setUsageLoading] = useState(false);
const [activityStatsData, setActivityStatsData] = useState<BotActivityStatsItem[] | null>(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<number | null>(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<BotState[]>(`${APP_ENDPOINTS.apiBase}/bots`);
setBots(res.data);
}, [setBots]);
const loadOverview = useCallback(async () => {
setLoading(true);
try {
const res = await axios.get<PlatformOverviewResponse>(`${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<PlatformUsageResponse>(`${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<PlatformUsageResponse>(`${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<PlatformBotResourceSnapshot>(`${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<BotState>(`${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<PlatformUsageAnalyticsSeriesItem[]>(
() => 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<unknown>[] = [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,
};
}