2026-03-09 04:53:15 +00:00
|
|
|
import { useEffect, useState } from 'react';
|
2026-03-01 16:26:03 +00:00
|
|
|
import axios from 'axios';
|
|
|
|
|
import { useAppStore } from '../../store/appStore';
|
|
|
|
|
import { Power, PowerOff, Terminal, ShieldCheck, Plus, Bot, Cpu, Layers, RefreshCw } from 'lucide-react';
|
|
|
|
|
import { CreateBotModal } from './components/CreateBotModal';
|
|
|
|
|
import { KernelManagerModal } from './components/KernelManagerModal';
|
|
|
|
|
import { APP_ENDPOINTS } from '../../config/env';
|
|
|
|
|
import { pickLocale } from '../../i18n';
|
|
|
|
|
import { managementZhCn } from '../../i18n/management.zh-cn';
|
|
|
|
|
import { managementEn } from '../../i18n/management.en';
|
|
|
|
|
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
|
|
|
|
|
|
|
|
|
export function ManagementModule() {
|
2026-03-09 04:53:15 +00:00
|
|
|
const { activeBots, setBots, mergeBot, updateBotStatus, locale } = useAppStore();
|
2026-03-01 16:26:03 +00:00
|
|
|
const { notify } = useLucentPrompt();
|
|
|
|
|
const t = pickLocale(locale, { 'zh-cn': managementZhCn, en: managementEn });
|
|
|
|
|
const [selectedBotId, setSelectedBotId] = useState<string | null>(null);
|
|
|
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
|
|
const [isKernelModalOpen, setIsKernelModalOpen] = useState(false);
|
|
|
|
|
const [operatingBotId, setOperatingBotId] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const fetchBots = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots`);
|
|
|
|
|
setBots(res.data);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectedBot = selectedBotId ? activeBots[selectedBotId] : null;
|
|
|
|
|
|
2026-03-09 04:53:15 +00:00
|
|
|
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 (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
void loadDetail();
|
|
|
|
|
return () => {
|
|
|
|
|
alive = false;
|
|
|
|
|
};
|
|
|
|
|
}, [selectedBotId, mergeBot]);
|
|
|
|
|
|
2026-03-01 16:26:03 +00:00
|
|
|
return (
|
|
|
|
|
<div className="flex h-full gap-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
|
|
|
<div className="w-1/3 flex flex-col gap-4">
|
|
|
|
|
<div className="flex justify-between items-center px-2">
|
|
|
|
|
<h2 className="text-sm font-black text-slate-500 uppercase tracking-widest">{t.botInstances}</h2>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<button onClick={() => setIsKernelModalOpen(true)} className="p-2 bg-slate-800 hover:bg-slate-700 rounded-lg transition-all text-blue-400 border border-blue-500/20 shadow-lg shadow-blue-500/10">
|
|
|
|
|
<ShieldCheck size={16} />
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={() => setIsModalOpen(true)} className="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 px-3 py-2 rounded-lg text-white font-bold text-xs transition-all active:scale-95 shadow-lg shadow-blue-600/30">
|
|
|
|
|
<Plus size={14} /> {t.newBot}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 overflow-y-auto space-y-2 pr-2 scrollbar-thin">
|
|
|
|
|
{Object.values(activeBots).map((bot) => (
|
|
|
|
|
<div
|
|
|
|
|
key={bot.id}
|
|
|
|
|
onClick={() => setSelectedBotId(bot.id)}
|
|
|
|
|
className={`p-4 rounded-xl border cursor-pointer transition-all ${
|
|
|
|
|
selectedBotId === bot.id ? 'bg-blue-600/10 border-blue-500/50' : 'bg-slate-900/50 border-white/5 hover:border-white/10'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className={`p-2 rounded-lg ${bot.docker_status === 'RUNNING' ? 'bg-green-500/10 text-green-500' : 'bg-slate-800 text-slate-500'}`}>
|
|
|
|
|
<Bot size={18} />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-sm font-bold">{bot.name}</h3>
|
|
|
|
|
<p className="text-[10px] font-mono text-slate-500">{bot.id}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
disabled={operatingBotId === bot.id}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
toggleBot(bot.id, bot.docker_status);
|
|
|
|
|
}}
|
|
|
|
|
className={`p-2 rounded-lg transition-all ${operatingBotId === bot.id ? 'opacity-50 cursor-not-allowed' : ''} ${bot.docker_status === 'RUNNING' ? 'bg-red-500/10 text-red-400' : 'bg-green-500/10 text-green-400'}`}
|
|
|
|
|
>
|
|
|
|
|
{operatingBotId === bot.id ? (
|
|
|
|
|
<RefreshCw size={14} className="animate-spin" />
|
|
|
|
|
) : bot.docker_status === 'RUNNING' ? (
|
|
|
|
|
<PowerOff size={14} />
|
|
|
|
|
) : (
|
|
|
|
|
<Power size={14} />
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 flex flex-col gap-6">
|
|
|
|
|
{selectedBot ? (
|
|
|
|
|
<>
|
|
|
|
|
<div className="bg-slate-900/50 border border-white/5 rounded-2xl p-6">
|
|
|
|
|
<div className="flex justify-between items-start mb-6">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 className="text-2xl font-black">{selectedBot.name}</h2>
|
|
|
|
|
<p className="text-xs text-slate-500 mt-1">{t.detailsTitle}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`px-3 py-1 rounded-full text-[10px] font-black uppercase ${selectedBot.docker_status === 'RUNNING' ? 'bg-green-500/10 text-green-500 border border-green-500/20' : 'bg-slate-800 text-slate-500 border border-white/5'}`}>
|
|
|
|
|
{selectedBot.docker_status}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-3 gap-4">
|
|
|
|
|
<div className="bg-black/20 p-4 rounded-xl border border-white/5">
|
|
|
|
|
<div className="flex items-center gap-2 text-slate-500 mb-2">
|
|
|
|
|
<Layers size={14} /> <span className="text-[10px] font-bold uppercase tracking-widest">{t.kernel}</span>
|
|
|
|
|
</div>
|
2026-03-23 13:36:41 +00:00
|
|
|
<p className="text-sm font-mono text-blue-400 truncate">{selectedBot.image_tag || 'nanobot-base:v0.1.5'}</p>
|
2026-03-01 16:26:03 +00:00
|
|
|
</div>
|
|
|
|
|
<div className="bg-black/20 p-4 rounded-xl border border-white/5">
|
|
|
|
|
<div className="flex items-center gap-2 text-slate-500 mb-2">
|
|
|
|
|
<Cpu size={14} /> <span className="text-[10px] font-bold uppercase tracking-widest">{t.model}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm font-bold text-white">{selectedBot.llm_model || '-'}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-black/20 p-4 rounded-xl border border-white/5">
|
|
|
|
|
<div className="flex items-center gap-2 text-slate-500 mb-2">
|
|
|
|
|
<Terminal size={14} /> <span className="text-[10px] font-bold uppercase tracking-widest">{t.status}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm font-bold text-white">{selectedBot.current_state || t.idle}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 bg-black rounded-2xl border border-white/5 flex flex-col overflow-hidden shadow-2xl">
|
|
|
|
|
<div className="px-6 py-3 bg-white/[0.02] border-b border-white/5 flex items-center gap-3">
|
|
|
|
|
<Terminal size={14} className="text-blue-500" />
|
|
|
|
|
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">{t.consoleOutput}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 p-6 font-mono text-[11px] overflow-y-auto space-y-1 scrollbar-thin scrollbar-thumb-white/10">
|
|
|
|
|
{selectedBot.logs.map((log, i) => (
|
|
|
|
|
<div key={i} className="flex gap-4 border-b border-white/[0.01] pb-1">
|
|
|
|
|
<span className="text-slate-700 shrink-0">{(i + 1).toString().padStart(3, '0')}</span>
|
|
|
|
|
<span className={log.includes('Executing') ? 'text-amber-400' : 'text-slate-400'}>{log}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex-1 flex flex-col items-center justify-center border border-dashed border-white/5 rounded-2xl text-slate-600">
|
|
|
|
|
<Bot size={48} className="mb-4 opacity-20" />
|
|
|
|
|
<p className="text-sm italic tracking-wide">{t.selectHint}</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<CreateBotModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} onSuccess={fetchBots} />
|
|
|
|
|
<KernelManagerModal isOpen={isKernelModalOpen} onClose={() => setIsKernelModalOpen(false)} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|