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

528 lines
19 KiB
TypeScript
Raw Normal View History

2026-03-31 04:31:47 +00:00
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import axios from 'axios';
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
import { APP_ENDPOINTS } from '../../../config/env';
2026-03-31 06:56:31 +00:00
import { sortBotsByCreatedAtDesc } from '../../dashboard/utils';
2026-03-31 04:31:47 +00:00
import { useAppStore } from '../../../store/appStore';
import type { BotState } from '../../../types/bot';
import {
normalizePlatformPageSize,
readCachedPlatformPageSize,
writeCachedPlatformPageSize,
} from '../../../utils/platformPageSize';
import type {
2026-04-02 05:00:15 +00:00
BotActivityStatsItem,
2026-03-31 04:31:47 +00:00
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 [showImageFactory, setShowImageFactory] = useState(false);
const [showTemplateManager, setShowTemplateManager] = useState(false);
const [showPlatformSettings, setShowPlatformSettings] = useState(false);
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);
2026-04-02 05:00:15 +00:00
const [activityStatsData, setActivityStatsData] = useState<BotActivityStatsItem[] | null>(null);
const [activityLoading, setActivityLoading] = useState(false);
2026-03-31 04:31:47 +00:00
const [usagePage, setUsagePage] = useState(1);
const [usagePageSize, setUsagePageSize] = useState(() => readCachedPlatformPageSize(10));
const [pageSizeReady, setPageSizeReady] = useState(true);
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(() => {
2026-03-31 06:56:31 +00:00
return sortBotsByCreatedAtDesc(Object.values(activeBots)) as BotState[];
}, [activeBots]);
2026-03-31 04:31:47 +00:00
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 {
setPageSizeReady(true);
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]);
2026-04-02 05:00:15 +00:00
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]);
2026-03-31 04:31:47 +00:00
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();
2026-03-31 06:56:31 +00:00
}, [loadOverview]);
2026-03-31 04:31:47 +00:00
useEffect(() => {
if (!pageSizeReady) return;
void loadUsage(1);
}, [loadUsage, pageSizeReady, usagePageSize]);
2026-04-02 05:00:15 +00:00
useEffect(() => {
void loadActivityStats();
}, [loadActivityStats]);
2026-03-31 04:31:47 +00:00
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;
2026-04-02 05:00:15 +00:00
const activityStats = activityStatsData || overview?.activity_stats;
2026-03-31 04:31:47 +00:00
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 () => {
2026-04-02 05:00:15 +00:00
const jobs: Promise<unknown>[] = [loadOverview(), loadBots(), loadUsage(usagePage), loadActivityStats()];
2026-03-31 04:31:47 +00:00
if (selectedBotId) jobs.push(loadSelectedBotUsageSummary(selectedBotId));
await Promise.allSettled(jobs);
2026-04-02 05:00:15 +00:00
}, [loadActivityStats, loadBots, loadOverview, loadSelectedBotUsageSummary, loadUsage, selectedBotId, usagePage]);
2026-03-31 04:31:47 +00:00
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,
pageSizeReady,
pagedBots,
refreshAll,
removeBot,
resourceBot,
resourceBotId,
resourceError,
resourceLoading,
resourceSnapshot,
search,
selectedBotId,
selectedBotInfo,
selectedBotUsageSummary,
setBotEnabled,
setBotListPage,
setSearch,
setShowBotLastActionModal,
setShowImageFactory,
setShowPlatformSettings,
setShowTemplateManager,
showBotLastActionModal,
showCompactBotSheet,
showImageFactory,
showPlatformSettings,
showResourceModal,
showTemplateManager,
storagePercent,
toggleBot,
usageAnalytics,
2026-04-02 04:14:08 +00:00
activityStats,
2026-04-02 05:00:15 +00:00
activityLoading,
2026-03-31 04:31:47 +00:00
usageAnalyticsMax,
usageAnalyticsSeries,
usageAnalyticsTicks,
usageData,
usageLoading,
usagePage,
usageSummary,
};
}