diff --git a/backend/services/bot_service.py b/backend/services/bot_service.py
index 2e26818..c169d2c 100644
--- a/backend/services/bot_service.py
+++ b/backend/services/bot_service.py
@@ -195,6 +195,8 @@ def _read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]:
def _serialize_bot(bot: BotInstance) -> Dict[str, Any]:
runtime = _read_bot_runtime_snapshot(bot)
+ created_at = bot.created_at.isoformat() + "Z" if bot.created_at else None
+ updated_at = bot.updated_at.isoformat() + "Z" if bot.updated_at else None
return {
"id": bot.id,
"name": bot.name,
@@ -226,11 +228,13 @@ def _serialize_bot(bot: BotInstance) -> Dict[str, Any]:
"docker_status": bot.docker_status,
"current_state": bot.current_state,
"last_action": bot.last_action,
- "created_at": bot.created_at,
- "updated_at": bot.updated_at,
+ "created_at": created_at,
+ "updated_at": updated_at,
}
def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]:
+ created_at = bot.created_at.isoformat() + "Z" if bot.created_at else None
+ updated_at = bot.updated_at.isoformat() + "Z" if bot.updated_at else None
return {
"id": bot.id,
"name": bot.name,
@@ -240,7 +244,8 @@ def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]:
"docker_status": bot.docker_status,
"current_state": bot.current_state,
"last_action": bot.last_action,
- "updated_at": bot.updated_at,
+ "created_at": created_at,
+ "updated_at": updated_at,
}
def _sync_workspace_channels(
diff --git a/backend/services/cache_service.py b/backend/services/cache_service.py
index cd697eb..3e4153d 100644
--- a/backend/services/cache_service.py
+++ b/backend/services/cache_service.py
@@ -2,10 +2,10 @@ from typing import Optional
from core.cache import cache
def _cache_key_bots_list() -> str:
- return "bot:list:v2"
+ return "bot:list:v3"
def _cache_key_bot_detail(bot_id: str) -> str:
- return f"bot:detail:v2:{bot_id}"
+ return f"bot:detail:v3:{bot_id}"
def _cache_key_bot_messages(bot_id: str, limit: int) -> str:
return f"bot:messages:list:v2:{bot_id}:limit:{limit}"
diff --git a/backend/services/platform_overview_service.py b/backend/services/platform_overview_service.py
index 866f442..62e5e1e 100644
--- a/backend/services/platform_overview_service.py
+++ b/backend/services/platform_overview_service.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, List
+from typing import Any, Dict
from sqlmodel import Session, select
@@ -14,7 +14,9 @@ def build_platform_overview(session: Session, docker_manager: Any) -> Dict[str,
deleted = prune_expired_activity_events(session, force=False)
if deleted > 0:
session.commit()
- bots = session.exec(select(BotInstance)).all()
+ bots = session.exec(
+ select(BotInstance).order_by(BotInstance.created_at.desc(), BotInstance.id.asc())
+ ).all()
images = session.exec(select(NanobotImage).order_by(NanobotImage.created_at.desc())).all()
settings = get_platform_settings(session)
@@ -30,7 +32,6 @@ def build_platform_overview(session: Session, docker_manager: Any) -> Dict[str,
live_memory_used_total = 0
live_memory_limit_total = 0
- bot_rows: List[Dict[str, Any]] = []
for bot in bots:
enabled = bool(getattr(bot, "enabled", True))
runtime_status = docker_manager.get_bot_status(bot.id) if docker_manager else str(bot.docker_status or "STOPPED")
@@ -60,23 +61,6 @@ def build_platform_overview(session: Session, docker_manager: Any) -> Dict[str,
else:
stopped += 1
- bot_rows.append(
- {
- "id": bot.id,
- "name": bot.name,
- "enabled": enabled,
- "docker_status": runtime_status,
- "image_tag": bot.image_tag,
- "llm_provider": getattr(bot, "llm_provider", None),
- "llm_model": getattr(bot, "llm_model", None),
- "current_state": bot.current_state,
- "last_action": bot.last_action,
- "resources": resources,
- "workspace_usage_bytes": workspace_used,
- "workspace_limit_bytes": workspace_limit if workspace_limit > 0 else None,
- }
- )
-
usage = list_usage(session, limit=20)
events = list_activity_events(session, limit=20)
@@ -114,7 +98,6 @@ def build_platform_overview(session: Session, docker_manager: Any) -> Dict[str,
}
for row in images
],
- "bots": bot_rows,
"settings": settings.model_dump(),
"usage": usage,
"events": events,
diff --git a/frontend/src/components/BotManagementView.tsx b/frontend/src/components/BotManagementView.tsx
deleted file mode 100644
index 99eb800..0000000
--- a/frontend/src/components/BotManagementView.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { ManagementModule as BotManagementView } from '../modules/management/ManagementModule';
diff --git a/frontend/src/components/CreateBotModal.tsx b/frontend/src/components/CreateBotModal.tsx
deleted file mode 100644
index 8d1ba26..0000000
--- a/frontend/src/components/CreateBotModal.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { CreateBotModal } from '../modules/management/components/CreateBotModal';
diff --git a/frontend/src/components/KernelManagerModal.tsx b/frontend/src/components/KernelManagerModal.tsx
deleted file mode 100644
index f6f7489..0000000
--- a/frontend/src/components/KernelManagerModal.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { KernelManagerModal } from '../modules/management/components/KernelManagerModal';
diff --git a/frontend/src/components/VisualDeckView.tsx b/frontend/src/components/VisualDeckView.tsx
deleted file mode 100644
index dcfc25f..0000000
--- a/frontend/src/components/VisualDeckView.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { DashboardModule as VisualDeckView } from '../modules/dashboard/DashboardModule';
diff --git a/frontend/src/components/VoxelBot.tsx b/frontend/src/components/VoxelBot.tsx
deleted file mode 100644
index d7954e5..0000000
--- a/frontend/src/components/VoxelBot.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { VoxelBot } from '../modules/dashboard/components/VoxelBot';
diff --git a/frontend/src/components/WorkingDeck.tsx b/frontend/src/components/WorkingDeck.tsx
deleted file mode 100644
index 21e9e20..0000000
--- a/frontend/src/components/WorkingDeck.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { WorkingDeck } from '../modules/dashboard/components/WorkingDeck';
diff --git a/frontend/src/i18n/legacy-deck.en.ts b/frontend/src/i18n/legacy-deck.en.ts
deleted file mode 100644
index 2927248..0000000
--- a/frontend/src/i18n/legacy-deck.en.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export const legacyDeckEn = {
- operationsDeck: 'OPERATIONS DECK',
- sandbox: 'Neural Network Sandbox',
- activeNodes: 'ACTIVE NODES',
- globalEvents: 'Global Events',
- listening: 'listening for commands',
- standby: 'All units in standby mode. Waiting for deployment...',
- deckTitle: 'Digital Employee Deck',
- deckSub: 'Realtime bot execution and data flow visibility',
- idle: 'IDLE',
-};
diff --git a/frontend/src/i18n/legacy-deck.zh-cn.ts b/frontend/src/i18n/legacy-deck.zh-cn.ts
deleted file mode 100644
index ed6c180..0000000
--- a/frontend/src/i18n/legacy-deck.zh-cn.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export const legacyDeckZhCn = {
- operationsDeck: '运维控制台',
- sandbox: '神经网络沙盒',
- activeNodes: '活跃节点',
- globalEvents: '全局事件',
- listening: '等待指令',
- standby: '所有单元待命中,等待部署...',
- deckTitle: '数字员工工位',
- deckSub: '实时观察机器人执行状态与数据流转',
- idle: 'IDLE',
-};
diff --git a/frontend/src/index.css b/frontend/src/index.css
index d3147e6..5843beb 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -5,3 +5,16 @@ body,
width: 100%;
min-height: 100vh;
}
+
+.animate-spin {
+ animation: app-spin 1s linear infinite;
+}
+
+@keyframes app-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/frontend/src/modules/dashboard/DashboardModule.tsx b/frontend/src/modules/dashboard/DashboardModule.tsx
deleted file mode 100644
index 6c7d707..0000000
--- a/frontend/src/modules/dashboard/DashboardModule.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { Activity, Radio, Terminal } from 'lucide-react';
-import { useAppStore } from '../../store/appStore';
-import { WorkingDeck } from './components/WorkingDeck';
-import { pickLocale } from '../../i18n';
-import { legacyDeckZhCn } from '../../i18n/legacy-deck.zh-cn';
-import { legacyDeckEn } from '../../i18n/legacy-deck.en';
-
-export function DashboardModule() {
- const activeBots = useAppStore((state) => state.activeBots);
- const locale = useAppStore((state) => state.locale);
- const t = pickLocale(locale, { 'zh-cn': legacyDeckZhCn, en: legacyDeckEn });
- const runningBots = Object.values(activeBots).filter((b) => b.docker_status === 'RUNNING');
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {t.operationsDeck}
-
-
{t.sandbox}
-
-
-
-
- {t.activeNodes}
-
- {runningBots.length} / {Object.keys(activeBots).length}
-
-
-
-
-
-
-
- {runningBots.slice(0, 3).map((bot) => (
-
-
-
-
- {bot.name}
- {bot.current_state || t.idle}
-
-
{bot.last_action || t.standby}
-
-
- ))}
-
-
-
-
-
- {t.globalEvents}
-
-
-
- {runningBots.map((b) => `[${b.name}] ${b.last_action || t.listening}`).join(' • ') ||
- t.standby}
-
-
-
-
- );
-}
diff --git a/frontend/src/modules/dashboard/components/BotListPanel.tsx b/frontend/src/modules/dashboard/components/BotListPanel.tsx
index 8e0443f..070d84d 100644
--- a/frontend/src/modules/dashboard/components/BotListPanel.tsx
+++ b/frontend/src/modules/dashboard/components/BotListPanel.tsx
@@ -215,10 +215,12 @@ export function BotListPanel({
const controlState = controlStateByBot[bot.id];
const isOperating = operatingBotId === bot.id;
const isEnabled = bot.enabled !== false;
+ const isStarting = controlState === 'starting';
+ const isStopping = controlState === 'stopping';
const isEnabling = controlState === 'enabling';
const isDisabling = controlState === 'disabling';
const isRunning = String(bot.docker_status || '').toUpperCase() === 'RUNNING';
- const showActionPending = isOperating && !isEnabling && !isDisabling;
+ const showActionPending = isStarting || isStopping;
return (
(null);
-
- useFrame((state) => {
- if (!meshRef.current) return;
- if (currentState === 'THINKING') {
- meshRef.current.position.y = Math.sin(state.clock.elapsedTime * 10) * 0.1;
- } else if (currentState === 'TOOL_CALL') {
- meshRef.current.rotation.y += 0.1;
- } else {
- meshRef.current.position.y = 0;
- }
- });
-
- const stateColor =
- currentState === 'THINKING'
- ? '#3b82f6'
- : currentState === 'TOOL_CALL'
- ? '#f59e0b'
- : currentState === 'SUCCESS'
- ? '#10b981'
- : '#4b5563';
-
- return (
-
-
-
-
-
-
-
-
-
{name}
- {lastAction &&
{lastAction}
}
-
-
-
- );
-}
diff --git a/frontend/src/modules/dashboard/components/WorkingDeck.tsx b/frontend/src/modules/dashboard/components/WorkingDeck.tsx
deleted file mode 100644
index e6a6251..0000000
--- a/frontend/src/modules/dashboard/components/WorkingDeck.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Canvas } from '@react-three/fiber';
-import { ContactShadows, OrbitControls } from '@react-three/drei';
-import { useAppStore } from '../../../store/appStore';
-import { VoxelBot } from './VoxelBot';
-import { pickLocale } from '../../../i18n';
-import { legacyDeckZhCn } from '../../../i18n/legacy-deck.zh-cn';
-import { legacyDeckEn } from '../../../i18n/legacy-deck.en';
-
-export function WorkingDeck() {
- const activeBots = useAppStore((state) => state.activeBots);
- const locale = useAppStore((state) => state.locale);
- const t = pickLocale(locale, { 'zh-cn': legacyDeckZhCn, en: legacyDeckEn });
-
- return (
-
-
-
-
-
- {t.deckTitle}
-
-
{t.deckSub}
-
-
- );
-}
diff --git a/frontend/src/modules/dashboard/hooks/useDashboardShellState.ts b/frontend/src/modules/dashboard/hooks/useDashboardShellState.ts
index 0636e6b..c275484 100644
--- a/frontend/src/modules/dashboard/hooks/useDashboardShellState.ts
+++ b/frontend/src/modules/dashboard/hooks/useDashboardShellState.ts
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { BotState } from '../../../types/bot';
import type { CompactPanelTab, RuntimeViewMode } from '../types';
-import { parseBotTimestamp } from '../utils';
+import { sortBotsByCreatedAtDesc } from '../utils';
interface UseDashboardShellStateOptions {
activeBots: Record
;
@@ -38,13 +38,7 @@ export function useDashboardShellState({
const controlCommandPanelRef = useRef(null);
const bots = useMemo(
- () =>
- Object.values(activeBots).sort((a, b) => {
- const aCreated = parseBotTimestamp(a.created_at);
- const bCreated = parseBotTimestamp(b.created_at);
- if (aCreated !== bCreated) return bCreated - aCreated;
- return String(a.id || '').localeCompare(String(b.id || ''));
- }),
+ () => sortBotsByCreatedAtDesc(Object.values(activeBots)),
[activeBots],
);
diff --git a/frontend/src/modules/dashboard/utils.tsx b/frontend/src/modules/dashboard/utils.tsx
index 9d85763..109e883 100644
--- a/frontend/src/modules/dashboard/utils.tsx
+++ b/frontend/src/modules/dashboard/utils.tsx
@@ -150,7 +150,7 @@ export function normalizeRuntimeState(s?: string) {
return raw;
}
-export function parseBotTimestamp(raw?: string | number) {
+export function parseBotTimestamp(raw?: string | number | null) {
if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
const text = String(raw || '').trim();
if (!text) return 0;
@@ -158,6 +158,17 @@ export function parseBotTimestamp(raw?: string | number) {
return Number.isFinite(ms) ? ms : 0;
}
+export function sortBotsByCreatedAtDesc(
+ bots: readonly T[],
+): T[] {
+ return [...bots].sort((left, right) => {
+ const leftCreated = parseBotTimestamp(left.created_at);
+ const rightCreated = parseBotTimestamp(right.created_at);
+ if (leftCreated !== rightCreated) return rightCreated - leftCreated;
+ return String(left.id || '').localeCompare(String(right.id || ''));
+ });
+}
+
export function normalizeWorkspaceExtension(raw: unknown): string {
const value = String(raw ?? '').trim().toLowerCase();
if (!value) return '';
diff --git a/frontend/src/modules/management/ManagementModule.tsx b/frontend/src/modules/management/ManagementModule.tsx
deleted file mode 100644
index d340015..0000000
--- a/frontend/src/modules/management/ManagementModule.tsx
+++ /dev/null
@@ -1,324 +0,0 @@
-import { Power, PowerOff, Terminal, ShieldCheck, Plus, Bot, Cpu, Layers, RefreshCw } from 'lucide-react';
-import { ProtectedSearchInput } from '../../components/ProtectedSearchInput';
-import { LucentIconButton } from '../../components/lucent/LucentIconButton';
-import { CreateBotModal } from './components/CreateBotModal';
-import { KernelManagerModal } from './components/KernelManagerModal';
-import { WorkspaceEntriesList } from '../dashboard/components/WorkspaceEntriesList';
-import { WorkspaceHoverCard } from '../dashboard/components/WorkspaceHoverCard';
-import { WorkspacePreviewModal } from '../dashboard/components/WorkspacePreviewModal';
-import { formatBytes, formatWorkspaceTime } from '../dashboard/utils';
-import { useManagementModule } from './hooks/useManagementModule';
-import '../dashboard/components/BotListPanel.css';
-import '../dashboard/components/RuntimePanel.css';
-import '../dashboard/components/DashboardShared.css';
-import '../../components/ui/SharedUi.css';
-
-export function ManagementModule() {
- const management = useManagementModule();
- const {
- closeWorkspacePreview,
- copyWorkspacePreviewPath,
- copyWorkspacePreviewUrl,
- filteredWorkspaceEntries,
- getWorkspaceDownloadHref,
- getWorkspaceRawHref,
- hideWorkspaceHoverCard,
- loadWorkspaceTree,
- openWorkspaceFilePreview,
- saveWorkspacePreviewMarkdown,
- setWorkspaceAutoRefresh,
- setWorkspacePreviewDraft,
- setWorkspacePreviewFullscreen,
- setWorkspacePreviewMode,
- setWorkspaceQuery,
- showWorkspaceHoverCard,
- workspaceAutoRefresh,
- workspaceCurrentPath,
- workspaceDownloadExtensionSet,
- workspaceError,
- workspaceFileLoading,
- workspaceFiles,
- workspaceHoverCard,
- workspaceLoading,
- workspaceParentPath,
- workspacePathDisplay,
- workspacePreview,
- workspacePreviewCanEdit,
- workspacePreviewDraft,
- workspacePreviewEditorEnabled,
- workspacePreviewFullscreen,
- workspacePreviewMarkdownComponents,
- workspacePreviewSaving,
- workspaceQuery,
- workspaceSearchLoading,
- } = management.workspace;
-
- return (
-
-
-
-
{management.t.botInstances}
-
-
-
-
-
-
-
- {Object.values(management.activeBots).map((bot) => (
-
management.setSelectedBotId(bot.id)}
- className={`p-4 rounded-xl border cursor-pointer transition-all ${
- management.selectedBotId === bot.id ? 'bg-blue-600/10 border-blue-500/50' : 'bg-slate-900/50 border-white/5 hover:border-white/10'
- }`}
- >
-
-
-
-
-
-
-
{bot.name}
-
{bot.id}
-
-
-
-
-
- ))}
-
-
-
-
- {management.selectedBot ? (
- <>
-
-
-
-
{management.selectedBot.name}
-
{management.t.botOverview}
-
-
- {management.selectedBot.docker_status}
-
-
-
-
-
-
- {management.t.kernel}
-
-
{management.selectedBot.image_tag || 'nanobot-base:v0.1.4'}
-
-
-
- {management.t.model}
-
-
{management.selectedBot.llm_model || '-'}
-
-
-
- {management.t.status}
-
-
{management.selectedBot.current_state || management.t.idle}
-
-
-
-
-
-
-
-
-
{management.t.workspace}
-
{management.dashboardT.workspaceHint}
-
-
-
- {workspaceError ?
{workspaceError}
: null}
-
-
-
-
- {workspacePathDisplay}
-
-
-
- {
- if (!management.selectedBot) return;
- void loadWorkspaceTree(management.selectedBot.id, workspaceCurrentPath);
- }}
- tooltip={management.isZh ? '刷新工作区' : 'Refresh workspace'}
- aria-label={management.isZh ? '刷新工作区' : 'Refresh workspace'}
- >
-
-
-
-
-
-
-
-
setWorkspaceQuery('')}
- onSearchAction={() => setWorkspaceQuery(workspaceQuery.trim())}
- debounceMs={200}
- placeholder={management.dashboardT.workspaceSearchPlaceholder}
- ariaLabel={management.dashboardT.workspaceSearchPlaceholder}
- clearTitle={management.dashboardT.clearSearch}
- searchTitle={management.dashboardT.searchAction}
- name={management.workspaceSearchInputName}
- id={management.workspaceSearchInputName}
- />
-
-
-
-
- {workspaceLoading || workspaceSearchLoading ? (
-
{management.dashboardT.loadingDir}
- ) : filteredWorkspaceEntries.length === 0 && workspaceParentPath === null ? (
-
- {workspaceQuery.trim() ? management.dashboardT.workspaceSearchNoResult : management.dashboardT.emptyDir}
-
- ) : (
-
- )}
-
-
- {workspaceFileLoading ? management.dashboardT.openingPreview : management.dashboardT.workspaceHint}
-
-
-
- {!workspaceFiles.length ? (
-
{management.dashboardT.noPreviewFile}
- ) : null}
-
-
-
-
-
-
- {management.t.dockerLogs}
-
-
{management.t.dockerLogsHint}
-
-
- {management.recentLogs.length > 0 ? management.recentLogs.map((log, i) => {
- const totalLogs = management.selectedBot?.logs.length ?? management.recentLogs.length;
- const logNumber = (totalLogs - management.recentLogs.length + i + 1).toString().padStart(3, '0');
- return (
-
- {logNumber}
- {log}
-
- );
- }) : (
-
{management.t.dockerLogsEmpty}
- )}
-
-
-
- >
- ) : (
-
-
-
{management.t.selectHint}
-
- )}
-
-
-
management.setIsModalOpen(false)} onSuccess={management.fetchBots} />
- management.setIsKernelModalOpen(false)} />
- setWorkspacePreviewFullscreen((value) => !value)}
- onCopyPreviewPath={copyWorkspacePreviewPath}
- onCopyPreviewUrl={copyWorkspacePreviewUrl}
- onPreviewDraftChange={setWorkspacePreviewDraft}
- onSavePreviewMarkdown={saveWorkspacePreviewMarkdown}
- onEnterEditMode={() => setWorkspacePreviewMode('edit')}
- onExitEditMode={() => {
- setWorkspacePreviewMode('preview');
- setWorkspacePreviewDraft(workspacePreview?.content || '');
- }}
- getWorkspaceDownloadHref={getWorkspaceDownloadHref}
- getWorkspaceRawHref={getWorkspaceRawHref}
- />
-
-
- );
-}
diff --git a/frontend/src/modules/management/components/CreateBotModal.tsx b/frontend/src/modules/management/components/CreateBotModal.tsx
deleted file mode 100644
index 4c14944..0000000
--- a/frontend/src/modules/management/components/CreateBotModal.tsx
+++ /dev/null
@@ -1,199 +0,0 @@
-import { useState, useEffect } from 'react';
-import { X, Save, Bot, Cpu, Key, FileText, Layers } from 'lucide-react';
-import axios from 'axios';
-import { APP_ENDPOINTS } from '../../../config/env';
-import { useAppStore } from '../../../store/appStore';
-import { pickLocale } from '../../../i18n';
-import { managementZhCn } from '../../../i18n/management.zh-cn';
-import { managementEn } from '../../../i18n/management.en';
-import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
-import { LucentSelect } from '../../../components/lucent/LucentSelect';
-import { PasswordInput } from '../../../components/PasswordInput';
-
-interface CreateBotModalProps {
- isOpen: boolean;
- onClose: () => void;
- onSuccess: () => void;
-}
-
-interface NanobotImage {
- tag: string;
- status: string;
- source_dir?: string;
-}
-
-export function CreateBotModal({ isOpen, onClose, onSuccess }: CreateBotModalProps) {
- const locale = useAppStore((s) => s.locale);
- const { notify } = useLucentPrompt();
- const t = pickLocale(locale, { 'zh-cn': managementZhCn, en: managementEn }).create;
- const passwordToggleLabels = locale === 'zh'
- ? { show: '显示密码', hide: '隐藏密码' }
- : { show: 'Show password', hide: 'Hide password' };
- const [formData, setFormData] = useState({
- id: '',
- name: '',
- llm_provider: 'openai',
- llm_model: 'gpt-4o',
- api_key: '',
- system_prompt: t.systemPrompt,
- image_tag: 'nanobot-base:v0.1.4',
- });
-
- const [availableImages, setAvailableImages] = useState([]);
- const [isSubmitting, setIsSubmitting] = useState(false);
-
- useEffect(() => {
- if (!isOpen) {
- return;
- }
- axios
- .get(`${APP_ENDPOINTS.apiBase}/images`)
- .then((res) => setAvailableImages(res.data.filter((img) => img.status === 'READY' || img.status === 'UNKNOWN')))
- .catch((err) => console.error('Failed to fetch images', err));
- }, [isOpen]);
-
- if (!isOpen) return null;
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setIsSubmitting(true);
- try {
- await axios.post(`${APP_ENDPOINTS.apiBase}/bots`, formData);
- onSuccess();
- onClose();
- } catch {
- notify(t.createFail, { tone: 'error' });
- } finally {
- setIsSubmitting(false);
- }
- };
-
- return (
-
- );
-}
diff --git a/frontend/src/modules/management/components/KernelManagerModal.tsx b/frontend/src/modules/management/components/KernelManagerModal.tsx
deleted file mode 100644
index b6371af..0000000
--- a/frontend/src/modules/management/components/KernelManagerModal.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { useState, useEffect } from 'react';
-import { X, Cpu, CheckCircle, AlertCircle, Loader2, HardDrive, Trash2 } from 'lucide-react';
-import axios from 'axios';
-import { APP_ENDPOINTS } from '../../../config/env';
-import { useAppStore } from '../../../store/appStore';
-import { pickLocale } from '../../../i18n';
-import { managementZhCn } from '../../../i18n/management.zh-cn';
-import { managementEn } from '../../../i18n/management.en';
-import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
-import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
-
-interface KernelManagerModalProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-interface NanobotImage {
- tag: string;
- status: string;
- source_dir?: string;
-}
-
-export function KernelManagerModal({ isOpen, onClose }: KernelManagerModalProps) {
- const locale = useAppStore((s) => s.locale);
- const { notify, confirm } = useLucentPrompt();
- const t = pickLocale(locale, { 'zh-cn': managementZhCn, en: managementEn }).kernelModal;
- const [images, setImages] = useState([]);
-
- const fetchData = async () => {
- try {
- const imgRes = await axios.get(`${APP_ENDPOINTS.apiBase}/images`);
- setImages(imgRes.data);
- } catch (err) {
- console.error(t.fetchFailed, err);
- }
- };
-
- useEffect(() => {
- if (isOpen) {
- fetchData();
- }
- }, [isOpen]);
-
- const handleRemoveImage = async (tag: string) => {
- const ok = await confirm({
- title: t.removeRecord,
- message: t.removeConfirm(tag),
- tone: 'warning',
- });
- if (!ok) return;
- try {
- await axios.delete(`${APP_ENDPOINTS.apiBase}/images/${encodeURIComponent(tag)}`);
- fetchData();
- } catch (error: any) {
- notify(error.response?.data?.detail || t.removeFail, { tone: 'error' });
- }
- };
-
- if (!isOpen) return null;
-
- return (
-
-
-
-
-
-
{t.title}
-
-
-
-
-
-
-
-
-
{t.readySection}
-
- {images.map((img) => (
-
-
-
{img.tag}
-
{t.source}: {img.source_dir || t.pypi}
-
-
- {img.status === 'READY' &&
}
- {img.status === 'BUILDING' &&
}
- {img.status === 'ERROR' &&
}
-
- {img.status}
-
-
handleRemoveImage(img.tag)}
- className="ml-2 p-1.5 hover:bg-red-500/20 text-slate-500 hover:text-red-500 rounded transition-colors"
- tooltip={t.removeRecord}
- aria-label={t.removeRecord}
- >
-
-
-
-
- ))}
-
-
-
-
-
-
- {t.footerHint}
-
-
-
- );
-}
diff --git a/frontend/src/modules/management/hooks/useManagementModule.ts b/frontend/src/modules/management/hooks/useManagementModule.ts
deleted file mode 100644
index 9d69f7c..0000000
--- a/frontend/src/modules/management/hooks/useManagementModule.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { useEffect, useMemo, useState } from 'react';
-import axios from 'axios';
-
-import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
-import { APP_ENDPOINTS } from '../../../config/env';
-import { pickLocale } from '../../../i18n';
-import { dashboardEn } from '../../../i18n/dashboard.en';
-import { dashboardZhCn } from '../../../i18n/dashboard.zh-cn';
-import { managementEn } from '../../../i18n/management.en';
-import { managementZhCn } from '../../../i18n/management.zh-cn';
-import { useAppStore } from '../../../store/appStore';
-import { useDashboardSystemDefaults } from '../../dashboard/hooks/useDashboardSystemDefaults';
-import { useDashboardWorkspace } from '../../dashboard/hooks/useDashboardWorkspace';
-
-export function useManagementModule() {
- const { activeBots, setBots, mergeBot, updateBotStatus, locale } = useAppStore();
- const { notify } = useLucentPrompt();
- const t = pickLocale(locale, { 'zh-cn': managementZhCn, en: managementEn });
- const dashboardT = pickLocale(locale, { 'zh-cn': dashboardZhCn, en: dashboardEn });
- const isZh = locale === 'zh';
- const [selectedBotId, setSelectedBotId] = useState(null);
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [isKernelModalOpen, setIsKernelModalOpen] = useState(false);
- const [operatingBotId, setOperatingBotId] = useState(null);
- const [, setBotListPageSize] = useState(10);
- const workspaceSearchInputName = useMemo(
- () => `management-workspace-search-${Math.random().toString(36).slice(2, 10)}`,
- [],
- );
-
- const {
- refreshAttachmentPolicy,
- workspaceDownloadExtensions,
- } = useDashboardSystemDefaults({
- setBotListPageSize,
- });
-
- const selectedBot = selectedBotId ? activeBots[selectedBotId] : null;
- const recentLogs = useMemo(() => (selectedBot?.logs || []).slice(-10), [selectedBot?.logs]);
-
- const workspace = useDashboardWorkspace({
- selectedBotId: selectedBotId || '',
- selectedBotDockerStatus: selectedBot?.docker_status || '',
- workspaceDownloadExtensions,
- refreshAttachmentPolicy,
- notify,
- t: dashboardT,
- isZh,
- fileNotPreviewableLabel: dashboardT.fileNotPreviewable,
- });
- const { loadWorkspaceTree, resetWorkspaceState } = workspace;
-
- const fetchBots = async () => {
- try {
- const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots`);
- setBots(res.data);
- } catch (error) {
- console.error(error);
- }
- };
-
- const toggleBot = async (botId: string, status: string) => {
- setOperatingBotId(botId);
- try {
- if (status === 'RUNNING') {
- await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${botId}/stop`);
- updateBotStatus(botId, 'STOPPED');
- } else {
- await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${botId}/start`);
- updateBotStatus(botId, 'RUNNING');
- }
- await fetchBots();
- } catch {
- notify(t.opFail, { tone: 'error' });
- } finally {
- setOperatingBotId(null);
- }
- };
-
- useEffect(() => {
- if (!selectedBotId) return;
- let alive = true;
- const loadDetail = async () => {
- try {
- const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}`);
- if (alive) {
- mergeBot(res.data);
- }
- } catch (error) {
- console.error(error);
- }
- };
- void loadDetail();
- return () => {
- alive = false;
- };
- }, [selectedBotId, mergeBot]);
-
- useEffect(() => {
- if (!selectedBotId) {
- resetWorkspaceState();
- return;
- }
- resetWorkspaceState();
- void loadWorkspaceTree(selectedBotId, '');
- }, [loadWorkspaceTree, resetWorkspaceState, selectedBotId]);
-
- return {
- activeBots,
- dashboardT,
- fetchBots,
- isKernelModalOpen,
- isModalOpen,
- isZh,
- operatingBotId,
- recentLogs,
- selectedBot,
- selectedBotId,
- setIsKernelModalOpen,
- setIsModalOpen,
- setSelectedBotId,
- t,
- toggleBot,
- workspace,
- workspaceSearchInputName,
- };
-}
diff --git a/frontend/src/modules/platform/hooks/usePlatformDashboard.ts b/frontend/src/modules/platform/hooks/usePlatformDashboard.ts
index 2bb520e..31b0378 100644
--- a/frontend/src/modules/platform/hooks/usePlatformDashboard.ts
+++ b/frontend/src/modules/platform/hooks/usePlatformDashboard.ts
@@ -3,6 +3,7 @@ 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 {
@@ -62,15 +63,8 @@ export function usePlatformDashboard({ compactMode }: UsePlatformDashboardOption
const compactSheetTimerRef = useRef(null);
const botList = useMemo(() => {
- const overviewBots = overview?.bots || [];
- if (overviewBots.length > 0) {
- return overviewBots.map((bot) => ({
- ...(activeBots[bot.id] || { logs: [], messages: [], events: [] }),
- ...bot,
- })) as BotState[];
- }
- return Object.values(activeBots);
- }, [activeBots, overview?.bots]);
+ return sortBotsByCreatedAtDesc(Object.values(activeBots)) as BotState[];
+ }, [activeBots]);
const filteredBots = useMemo(() => {
const keyword = search.trim().toLowerCase();
@@ -173,8 +167,7 @@ export function usePlatformDashboard({ compactMode }: UsePlatformDashboardOption
useEffect(() => {
void loadOverview();
- void loadBots();
- }, [loadBots, loadOverview]);
+ }, [loadOverview]);
useEffect(() => {
if (!pageSizeReady) return;
diff --git a/frontend/src/modules/platform/types.ts b/frontend/src/modules/platform/types.ts
index 4eae3e0..c6483d8 100644
--- a/frontend/src/modules/platform/types.ts
+++ b/frontend/src/modules/platform/types.ts
@@ -131,24 +131,6 @@ export interface PlatformOverviewResponse {
source_dir?: string;
created_at: string;
}>;
- bots: Array<{
- id: string;
- name: string;
- enabled: boolean;
- docker_status: string;
- image_tag?: string;
- llm_provider?: string;
- llm_model?: string;
- current_state?: string;
- last_action?: string;
- resources: {
- cpu_cores: number;
- memory_mb: number;
- storage_gb: number;
- };
- workspace_usage_bytes: number;
- workspace_limit_bytes?: number | null;
- }>;
settings: PlatformSettings;
usage: {
summary: {
diff --git a/frontend/src/store/appStore.ts b/frontend/src/store/appStore.ts
index 66fc625..7be27de 100644
--- a/frontend/src/store/appStore.ts
+++ b/frontend/src/store/appStore.ts
@@ -66,6 +66,8 @@ export const useAppStore = create((set) => ({
...prev,
...bot,
enabled: preferDefined(bot.enabled, prev?.enabled),
+ created_at: preferDefined(bot.created_at, prev?.created_at),
+ updated_at: preferDefined(bot.updated_at, prev?.updated_at),
image_tag: preferDefined(bot.image_tag, prev?.image_tag),
llm_provider: preferDefined(bot.llm_provider, prev?.llm_provider),
llm_model: preferDefined(bot.llm_model, prev?.llm_model),
@@ -105,6 +107,8 @@ export const useAppStore = create((set) => ({
...prev,
...bot,
enabled: preferDefined(bot.enabled, prev?.enabled),
+ created_at: preferDefined(bot.created_at, prev?.created_at),
+ updated_at: preferDefined(bot.updated_at, prev?.updated_at),
logs: prev?.logs ?? bot.logs ?? [],
messages: prev?.messages ?? bot.messages ?? [],
events: prev?.events ?? bot.events ?? [],