515 lines
18 KiB
TypeScript
515 lines
18 KiB
TypeScript
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,
|
||
};
|
||
}
|