v0.1.4-p4
parent
c8d11ff654
commit
d196e57804
|
|
@ -195,6 +195,8 @@ def _read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
|
|
||||||
def _serialize_bot(bot: BotInstance) -> Dict[str, Any]:
|
def _serialize_bot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
runtime = _read_bot_runtime_snapshot(bot)
|
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 {
|
return {
|
||||||
"id": bot.id,
|
"id": bot.id,
|
||||||
"name": bot.name,
|
"name": bot.name,
|
||||||
|
|
@ -226,11 +228,13 @@ def _serialize_bot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
"docker_status": bot.docker_status,
|
"docker_status": bot.docker_status,
|
||||||
"current_state": bot.current_state,
|
"current_state": bot.current_state,
|
||||||
"last_action": bot.last_action,
|
"last_action": bot.last_action,
|
||||||
"created_at": bot.created_at,
|
"created_at": created_at,
|
||||||
"updated_at": bot.updated_at,
|
"updated_at": updated_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]:
|
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 {
|
return {
|
||||||
"id": bot.id,
|
"id": bot.id,
|
||||||
"name": bot.name,
|
"name": bot.name,
|
||||||
|
|
@ -240,7 +244,8 @@ def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]:
|
||||||
"docker_status": bot.docker_status,
|
"docker_status": bot.docker_status,
|
||||||
"current_state": bot.current_state,
|
"current_state": bot.current_state,
|
||||||
"last_action": bot.last_action,
|
"last_action": bot.last_action,
|
||||||
"updated_at": bot.updated_at,
|
"created_at": created_at,
|
||||||
|
"updated_at": updated_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _sync_workspace_channels(
|
def _sync_workspace_channels(
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ from typing import Optional
|
||||||
from core.cache import cache
|
from core.cache import cache
|
||||||
|
|
||||||
def _cache_key_bots_list() -> str:
|
def _cache_key_bots_list() -> str:
|
||||||
return "bot:list:v2"
|
return "bot:list:v3"
|
||||||
|
|
||||||
def _cache_key_bot_detail(bot_id: str) -> str:
|
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:
|
def _cache_key_bot_messages(bot_id: str, limit: int) -> str:
|
||||||
return f"bot:messages:list:v2:{bot_id}:limit:{limit}"
|
return f"bot:messages:list:v2:{bot_id}:limit:{limit}"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict
|
||||||
|
|
||||||
from sqlmodel import Session, select
|
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)
|
deleted = prune_expired_activity_events(session, force=False)
|
||||||
if deleted > 0:
|
if deleted > 0:
|
||||||
session.commit()
|
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()
|
images = session.exec(select(NanobotImage).order_by(NanobotImage.created_at.desc())).all()
|
||||||
settings = get_platform_settings(session)
|
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_used_total = 0
|
||||||
live_memory_limit_total = 0
|
live_memory_limit_total = 0
|
||||||
|
|
||||||
bot_rows: List[Dict[str, Any]] = []
|
|
||||||
for bot in bots:
|
for bot in bots:
|
||||||
enabled = bool(getattr(bot, "enabled", True))
|
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")
|
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:
|
else:
|
||||||
stopped += 1
|
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)
|
usage = list_usage(session, limit=20)
|
||||||
events = list_activity_events(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
|
for row in images
|
||||||
],
|
],
|
||||||
"bots": bot_rows,
|
|
||||||
"settings": settings.model_dump(),
|
"settings": settings.model_dump(),
|
||||||
"usage": usage,
|
"usage": usage,
|
||||||
"events": events,
|
"events": events,
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { ManagementModule as BotManagementView } from '../modules/management/ManagementModule';
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { CreateBotModal } from '../modules/management/components/CreateBotModal';
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { KernelManagerModal } from '../modules/management/components/KernelManagerModal';
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { DashboardModule as VisualDeckView } from '../modules/dashboard/DashboardModule';
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { VoxelBot } from '../modules/dashboard/components/VoxelBot';
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { WorkingDeck } from '../modules/dashboard/components/WorkingDeck';
|
|
||||||
|
|
@ -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',
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
export const legacyDeckZhCn = {
|
|
||||||
operationsDeck: '运维控制台',
|
|
||||||
sandbox: '神经网络沙盒',
|
|
||||||
activeNodes: '活跃节点',
|
|
||||||
globalEvents: '全局事件',
|
|
||||||
listening: '等待指令',
|
|
||||||
standby: '所有单元待命中,等待部署...',
|
|
||||||
deckTitle: '数字员工工位',
|
|
||||||
deckSub: '实时观察机器人执行状态与数据流转',
|
|
||||||
idle: 'IDLE',
|
|
||||||
};
|
|
||||||
|
|
@ -5,3 +5,16 @@ body,
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: app-spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes app-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="w-full h-full relative animate-in fade-in zoom-in duration-700">
|
|
||||||
<div className="absolute inset-0">
|
|
||||||
<WorkingDeck />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute top-8 left-8 p-6 bg-black/40 backdrop-blur-xl border border-white/10 rounded-2xl pointer-events-none shadow-2xl">
|
|
||||||
<div className="flex items-center gap-4 mb-4">
|
|
||||||
<div className="w-12 h-12 bg-blue-600/20 rounded-xl flex items-center justify-center border border-blue-500/30">
|
|
||||||
<Radio className="text-blue-400 animate-pulse" size={24} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-black tracking-tighter text-white">
|
|
||||||
{t.operationsDeck}
|
|
||||||
</h1>
|
|
||||||
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-[0.2em]">{t.sandbox}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between gap-8 text-[10px] font-bold">
|
|
||||||
<span className="text-slate-500">{t.activeNodes}</span>
|
|
||||||
<span className="text-blue-400">
|
|
||||||
{runningBots.length} / {Object.keys(activeBots).length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full h-1 bg-white/5 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full bg-blue-500 transition-all duration-1000"
|
|
||||||
style={{ width: `${(runningBots.length / (Object.keys(activeBots).length || 1)) * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute top-8 right-8 space-y-4 pointer-events-none">
|
|
||||||
{runningBots.slice(0, 3).map((bot) => (
|
|
||||||
<div key={bot.id} className="p-4 bg-black/40 backdrop-blur-xl border border-white/10 rounded-xl flex items-center gap-4 min-w-[240px] animate-in slide-in-from-right-4">
|
|
||||||
<div className="p-2 bg-green-500/10 rounded-lg">
|
|
||||||
<Activity size={16} className="text-green-500" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex justify-between items-center mb-1">
|
|
||||||
<span className="text-xs font-bold text-white">{bot.name}</span>
|
|
||||||
<span className="text-[8px] font-mono text-green-500 uppercase">{bot.current_state || t.idle}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-[9px] text-slate-500 truncate max-w-[150px]">{bot.last_action || t.standby}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 w-[60%] p-4 bg-black/40 backdrop-blur-xl border border-white/10 rounded-2xl flex items-center gap-6">
|
|
||||||
<div className="flex items-center gap-2 text-slate-400 shrink-0">
|
|
||||||
<Terminal size={14} />
|
|
||||||
<span className="text-[10px] font-black uppercase tracking-widest">{t.globalEvents}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 overflow-hidden h-5">
|
|
||||||
<div className="animate-marquee whitespace-nowrap text-xs font-mono text-blue-400/80">
|
|
||||||
{runningBots.map((b) => `[${b.name}] ${b.last_action || t.listening}`).join(' • ') ||
|
|
||||||
t.standby}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -215,10 +215,12 @@ export function BotListPanel({
|
||||||
const controlState = controlStateByBot[bot.id];
|
const controlState = controlStateByBot[bot.id];
|
||||||
const isOperating = operatingBotId === bot.id;
|
const isOperating = operatingBotId === bot.id;
|
||||||
const isEnabled = bot.enabled !== false;
|
const isEnabled = bot.enabled !== false;
|
||||||
|
const isStarting = controlState === 'starting';
|
||||||
|
const isStopping = controlState === 'stopping';
|
||||||
const isEnabling = controlState === 'enabling';
|
const isEnabling = controlState === 'enabling';
|
||||||
const isDisabling = controlState === 'disabling';
|
const isDisabling = controlState === 'disabling';
|
||||||
const isRunning = String(bot.docker_status || '').toUpperCase() === 'RUNNING';
|
const isRunning = String(bot.docker_status || '').toUpperCase() === 'RUNNING';
|
||||||
const showActionPending = isOperating && !isEnabling && !isDisabling;
|
const showActionPending = isStarting || isStopping;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={bot.id}
|
key={bot.id}
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import { useRef } from 'react';
|
|
||||||
import { useFrame } from '@react-three/fiber';
|
|
||||||
import { Html } from '@react-three/drei';
|
|
||||||
import * as THREE from 'three';
|
|
||||||
|
|
||||||
interface VoxelBotProps {
|
|
||||||
name: string;
|
|
||||||
currentState?: string;
|
|
||||||
lastAction?: string;
|
|
||||||
position: [number, number, number];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VoxelBot({ name, currentState, lastAction, position }: VoxelBotProps) {
|
|
||||||
const meshRef = useRef<THREE.Mesh>(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 (
|
|
||||||
<group position={position}>
|
|
||||||
<mesh ref={meshRef} castShadow receiveShadow>
|
|
||||||
<boxGeometry args={[1, 1, 1]} />
|
|
||||||
<meshStandardMaterial color={stateColor} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
<Html position={[0, 1.2, 0]} center>
|
|
||||||
<div className="bg-black/80 text-white px-2 py-1 rounded text-xs whitespace-nowrap shadow-lg">
|
|
||||||
<p className="font-bold">{name}</p>
|
|
||||||
{lastAction && <p className="text-gray-400 italic text-[10px]">{lastAction}</p>}
|
|
||||||
</div>
|
|
||||||
</Html>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="w-full h-full bg-slate-900 rounded-xl overflow-hidden shadow-2xl relative">
|
|
||||||
<Canvas shadows camera={{ position: [5, 5, 5], fov: 40 }}>
|
|
||||||
<color attach="background" args={['#0f172a']} />
|
|
||||||
|
|
||||||
<ambientLight intensity={0.7} />
|
|
||||||
<directionalLight position={[10, 10, 5]} intensity={1.5} castShadow />
|
|
||||||
|
|
||||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]} receiveShadow>
|
|
||||||
<planeGeometry args={[20, 20]} />
|
|
||||||
<meshStandardMaterial color="#1e293b" roughness={0.8} />
|
|
||||||
</mesh>
|
|
||||||
<ContactShadows position={[0, -0.49, 0]} scale={20} blur={2} far={4.5} />
|
|
||||||
|
|
||||||
{Object.values(activeBots).map((bot, index) => {
|
|
||||||
const x = (index % 5) * 2 - 4;
|
|
||||||
const z = Math.floor(index / 5) * 2 - 2;
|
|
||||||
return (
|
|
||||||
<VoxelBot
|
|
||||||
key={bot.id}
|
|
||||||
name={bot.name}
|
|
||||||
currentState={bot.current_state}
|
|
||||||
lastAction={bot.last_action}
|
|
||||||
position={[x, 0, z]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2.2} />
|
|
||||||
</Canvas>
|
|
||||||
<div className="absolute top-4 left-4 bg-black/40 backdrop-blur-md p-3 rounded-lg border border-white/10 text-white">
|
|
||||||
<h2 className="text-lg font-bold flex items-center gap-2">
|
|
||||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
|
||||||
{t.deckTitle}
|
|
||||||
</h2>
|
|
||||||
<p className="text-xs text-gray-400">{t.deckSub}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import type { BotState } from '../../../types/bot';
|
import type { BotState } from '../../../types/bot';
|
||||||
import type { CompactPanelTab, RuntimeViewMode } from '../types';
|
import type { CompactPanelTab, RuntimeViewMode } from '../types';
|
||||||
import { parseBotTimestamp } from '../utils';
|
import { sortBotsByCreatedAtDesc } from '../utils';
|
||||||
|
|
||||||
interface UseDashboardShellStateOptions {
|
interface UseDashboardShellStateOptions {
|
||||||
activeBots: Record<string, BotState>;
|
activeBots: Record<string, BotState>;
|
||||||
|
|
@ -38,13 +38,7 @@ export function useDashboardShellState({
|
||||||
const controlCommandPanelRef = useRef<HTMLDivElement | null>(null);
|
const controlCommandPanelRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const bots = useMemo(
|
const bots = useMemo(
|
||||||
() =>
|
() => sortBotsByCreatedAtDesc(Object.values(activeBots)),
|
||||||
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 || ''));
|
|
||||||
}),
|
|
||||||
[activeBots],
|
[activeBots],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ export function normalizeRuntimeState(s?: string) {
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseBotTimestamp(raw?: string | number) {
|
export function parseBotTimestamp(raw?: string | number | null) {
|
||||||
if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
|
if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
|
||||||
const text = String(raw || '').trim();
|
const text = String(raw || '').trim();
|
||||||
if (!text) return 0;
|
if (!text) return 0;
|
||||||
|
|
@ -158,6 +158,17 @@ export function parseBotTimestamp(raw?: string | number) {
|
||||||
return Number.isFinite(ms) ? ms : 0;
|
return Number.isFinite(ms) ? ms : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sortBotsByCreatedAtDesc<T extends { id?: string | number | null; created_at?: string | number | null }>(
|
||||||
|
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 {
|
export function normalizeWorkspaceExtension(raw: unknown): string {
|
||||||
const value = String(raw ?? '').trim().toLowerCase();
|
const value = String(raw ?? '').trim().toLowerCase();
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<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">{management.t.botInstances}</h2>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button onClick={() => management.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={() => management.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} /> {management.t.newBot}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto space-y-2 pr-2 scrollbar-thin">
|
|
||||||
{Object.values(management.activeBots).map((bot) => (
|
|
||||||
<div
|
|
||||||
key={bot.id}
|
|
||||||
onClick={() => 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'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<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={management.operatingBotId === bot.id}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
void management.toggleBot(bot.id, bot.docker_status);
|
|
||||||
}}
|
|
||||||
className={`p-2 rounded-lg transition-all ${management.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'}`}
|
|
||||||
>
|
|
||||||
{management.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">
|
|
||||||
{management.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">{management.selectedBot.name}</h2>
|
|
||||||
<p className="text-xs text-slate-500 mt-1">{management.t.botOverview}</p>
|
|
||||||
</div>
|
|
||||||
<div className={`px-3 py-1 rounded-full text-[10px] font-black uppercase ${management.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'}`}>
|
|
||||||
{management.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">{management.t.kernel}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-mono text-blue-400 truncate">{management.selectedBot.image_tag || 'nanobot-base:v0.1.4'}</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">
|
|
||||||
<Cpu size={14} /> <span className="text-[10px] font-bold uppercase tracking-widest">{management.t.model}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-bold text-white">{management.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">{management.t.status}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-bold text-white">{management.selectedBot.current_state || management.t.idle}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid min-h-0 flex-1 gap-6 xl:grid-cols-[minmax(0,1.35fr)_minmax(320px,0.85fr)]">
|
|
||||||
<div className="bg-slate-900/50 border border-white/5 rounded-2xl p-6 flex min-h-0 flex-col">
|
|
||||||
<div className="mb-4 flex items-center justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<div className="text-[10px] font-black text-slate-500 uppercase tracking-widest">{management.t.workspace}</div>
|
|
||||||
<p className="mt-1 text-xs text-slate-500">{management.dashboardT.workspaceHint}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{workspaceError ? <div className="ops-empty-inline">{workspaceError}</div> : null}
|
|
||||||
|
|
||||||
<div className="workspace-toolbar">
|
|
||||||
<div className="workspace-path-wrap">
|
|
||||||
<div className="workspace-path mono" title={workspacePathDisplay}>
|
|
||||||
{workspacePathDisplay}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="workspace-toolbar-actions">
|
|
||||||
<LucentIconButton
|
|
||||||
className="workspace-refresh-icon-btn"
|
|
||||||
disabled={workspaceLoading}
|
|
||||||
onClick={() => {
|
|
||||||
if (!management.selectedBot) return;
|
|
||||||
void loadWorkspaceTree(management.selectedBot.id, workspaceCurrentPath);
|
|
||||||
}}
|
|
||||||
tooltip={management.isZh ? '刷新工作区' : 'Refresh workspace'}
|
|
||||||
aria-label={management.isZh ? '刷新工作区' : 'Refresh workspace'}
|
|
||||||
>
|
|
||||||
<RefreshCw size={14} className={workspaceLoading ? 'animate-spin' : ''} />
|
|
||||||
</LucentIconButton>
|
|
||||||
<label className="workspace-auto-switch" title={management.dashboardT.autoRefresh}>
|
|
||||||
<span className="workspace-auto-switch-label">{management.dashboardT.autoRefresh}</span>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={workspaceAutoRefresh}
|
|
||||||
onChange={() => setWorkspaceAutoRefresh((value) => !value)}
|
|
||||||
aria-label={management.dashboardT.autoRefresh}
|
|
||||||
/>
|
|
||||||
<span className="workspace-auto-switch-track" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="workspace-search-toolbar">
|
|
||||||
<ProtectedSearchInput
|
|
||||||
value={workspaceQuery}
|
|
||||||
onChange={setWorkspaceQuery}
|
|
||||||
onClear={() => 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}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="workspace-panel mt-3 flex-1">
|
|
||||||
<div className="workspace-list">
|
|
||||||
{workspaceLoading || workspaceSearchLoading ? (
|
|
||||||
<div className="ops-empty-inline">{management.dashboardT.loadingDir}</div>
|
|
||||||
) : filteredWorkspaceEntries.length === 0 && workspaceParentPath === null ? (
|
|
||||||
<div className="ops-empty-inline">
|
|
||||||
{workspaceQuery.trim() ? management.dashboardT.workspaceSearchNoResult : management.dashboardT.emptyDir}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<WorkspaceEntriesList
|
|
||||||
nodes={filteredWorkspaceEntries}
|
|
||||||
workspaceParentPath={workspaceParentPath}
|
|
||||||
selectedBotId={management.selectedBot.id}
|
|
||||||
workspaceFileLoading={workspaceFileLoading}
|
|
||||||
workspaceDownloadExtensionSet={workspaceDownloadExtensionSet}
|
|
||||||
labels={{
|
|
||||||
download: management.dashboardT.download,
|
|
||||||
fileNotPreviewable: management.dashboardT.fileNotPreviewable,
|
|
||||||
folder: management.dashboardT.folder,
|
|
||||||
goUp: management.dashboardT.goUp,
|
|
||||||
goUpTitle: management.dashboardT.goUpTitle,
|
|
||||||
openFolderTitle: management.dashboardT.openFolderTitle,
|
|
||||||
previewTitle: management.dashboardT.previewTitle,
|
|
||||||
}}
|
|
||||||
onLoadWorkspaceTree={loadWorkspaceTree}
|
|
||||||
onOpenWorkspaceFilePreview={openWorkspaceFilePreview}
|
|
||||||
onShowWorkspaceHoverCard={showWorkspaceHoverCard}
|
|
||||||
onHideWorkspaceHoverCard={hideWorkspaceHoverCard}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="workspace-hint">
|
|
||||||
{workspaceFileLoading ? management.dashboardT.openingPreview : management.dashboardT.workspaceHint}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!workspaceFiles.length ? (
|
|
||||||
<div className="ops-empty-inline mt-3">{management.dashboardT.noPreviewFile}</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-black rounded-2xl border border-white/5 flex min-h-0 flex-col overflow-hidden shadow-2xl">
|
|
||||||
<div className="px-6 py-3 bg-white/[0.02] border-b border-white/5 flex items-center justify-between gap-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Terminal size={14} className="text-blue-500" />
|
|
||||||
<span className="text-[10px] font-black text-slate-500 uppercase tracking-widest">{management.t.dockerLogs}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] text-slate-500">{management.t.dockerLogsHint}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 p-6 font-mono text-[11px] overflow-y-auto space-y-1 scrollbar-thin scrollbar-thumb-white/10">
|
|
||||||
{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 (
|
|
||||||
<div key={`${logNumber}-${i}`} className="flex gap-4 border-b border-white/[0.01] pb-1">
|
|
||||||
<span className="text-slate-700 shrink-0">{logNumber}</span>
|
|
||||||
<span className={log.includes('Executing') ? 'text-amber-400' : 'text-slate-400'}>{log}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}) : (
|
|
||||||
<div className="text-slate-500">{management.t.dockerLogsEmpty}</div>
|
|
||||||
)}
|
|
||||||
</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">{management.t.selectHint}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CreateBotModal isOpen={management.isModalOpen} onClose={() => management.setIsModalOpen(false)} onSuccess={management.fetchBots} />
|
|
||||||
<KernelManagerModal isOpen={management.isKernelModalOpen} onClose={() => management.setIsKernelModalOpen(false)} />
|
|
||||||
<WorkspacePreviewModal
|
|
||||||
isZh={management.isZh}
|
|
||||||
labels={{
|
|
||||||
cancel: management.dashboardT.cancel,
|
|
||||||
close: management.dashboardT.close,
|
|
||||||
copyAddress: management.dashboardT.copyAddress,
|
|
||||||
download: management.dashboardT.download,
|
|
||||||
editFile: management.dashboardT.editFile,
|
|
||||||
filePreview: management.dashboardT.filePreview,
|
|
||||||
fileTruncated: management.dashboardT.fileTruncated,
|
|
||||||
save: management.dashboardT.save,
|
|
||||||
}}
|
|
||||||
preview={workspacePreview}
|
|
||||||
previewFullscreen={workspacePreviewFullscreen}
|
|
||||||
previewEditorEnabled={workspacePreviewEditorEnabled}
|
|
||||||
previewCanEdit={workspacePreviewCanEdit}
|
|
||||||
previewDraft={workspacePreviewDraft}
|
|
||||||
previewSaving={workspacePreviewSaving}
|
|
||||||
markdownComponents={workspacePreviewMarkdownComponents}
|
|
||||||
onClose={closeWorkspacePreview}
|
|
||||||
onToggleFullscreen={() => setWorkspacePreviewFullscreen((value) => !value)}
|
|
||||||
onCopyPreviewPath={copyWorkspacePreviewPath}
|
|
||||||
onCopyPreviewUrl={copyWorkspacePreviewUrl}
|
|
||||||
onPreviewDraftChange={setWorkspacePreviewDraft}
|
|
||||||
onSavePreviewMarkdown={saveWorkspacePreviewMarkdown}
|
|
||||||
onEnterEditMode={() => setWorkspacePreviewMode('edit')}
|
|
||||||
onExitEditMode={() => {
|
|
||||||
setWorkspacePreviewMode('preview');
|
|
||||||
setWorkspacePreviewDraft(workspacePreview?.content || '');
|
|
||||||
}}
|
|
||||||
getWorkspaceDownloadHref={getWorkspaceDownloadHref}
|
|
||||||
getWorkspaceRawHref={getWorkspaceRawHref}
|
|
||||||
/>
|
|
||||||
<WorkspaceHoverCard
|
|
||||||
state={workspaceHoverCard}
|
|
||||||
isZh={management.isZh}
|
|
||||||
formatWorkspaceTime={formatWorkspaceTime}
|
|
||||||
formatBytes={formatBytes}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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<NanobotImage[]>([]);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
axios
|
|
||||||
.get<NanobotImage[]>(`${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 (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
|
||||||
<div className="w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200">
|
|
||||||
<div className="flex justify-between items-center p-6 border-b border-slate-800 bg-slate-800/50">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="p-2 bg-blue-600/20 rounded-lg text-white">
|
|
||||||
<Bot className="text-blue-400" size={24} />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-xl font-bold text-white">{t.title}</h2>
|
|
||||||
</div>
|
|
||||||
<button onClick={onClose} className="p-2 hover:bg-slate-700 rounded-full transition-colors text-white">
|
|
||||||
<X size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6 overflow-y-auto max-h-[80vh] scrollbar-thin scrollbar-thumb-white/10 pr-2">
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-400">{t.idLabel}</label>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-sm focus:border-blue-500 outline-none transition-all text-white"
|
|
||||||
placeholder={t.idPlaceholder}
|
|
||||||
onChange={(e) => setFormData({ ...formData, id: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-400">{t.nameLabel}</label>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-sm focus:border-blue-500 outline-none transition-all text-white"
|
|
||||||
placeholder={t.namePlaceholder}
|
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-400 flex items-center gap-2">
|
|
||||||
<Layers size={14} className="text-blue-500" /> {t.imageLabel}
|
|
||||||
</label>
|
|
||||||
<LucentSelect
|
|
||||||
onChange={(e) => setFormData({ ...formData, image_tag: e.target.value })}
|
|
||||||
value={formData.image_tag}
|
|
||||||
>
|
|
||||||
{availableImages.map((img) => (
|
|
||||||
<option key={img.tag} value={img.tag}>
|
|
||||||
{img.tag} ({img.source_dir ? `${t.source}: ${img.source_dir}` : t.pypi})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
{availableImages.length === 0 && <option disabled>{t.noImage}</option>}
|
|
||||||
</LucentSelect>
|
|
||||||
<p className="text-[10px] text-slate-600 italic">{t.imageHint}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-400 flex items-center gap-2 text-white">
|
|
||||||
<Cpu size={14} /> {t.providerLabel}
|
|
||||||
</label>
|
|
||||||
<LucentSelect
|
|
||||||
onChange={(e) => setFormData({ ...formData, llm_provider: e.target.value })}
|
|
||||||
>
|
|
||||||
<option value="openai">OpenAI</option>
|
|
||||||
<option value="vllm">vLLM (OpenAI-compatible)</option>
|
|
||||||
<option value="deepseek">DeepSeek</option>
|
|
||||||
<option value="kimi">Kimi (Moonshot)</option>
|
|
||||||
<option value="minimax">MiniMax</option>
|
|
||||||
<option value="ollama">Ollama (Local)</option>
|
|
||||||
</LucentSelect>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-400 text-white">{t.modelLabel}</label>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-sm focus:border-blue-500 outline-none transition-all text-white"
|
|
||||||
placeholder={t.modelPlaceholder}
|
|
||||||
defaultValue="gpt-4o"
|
|
||||||
onChange={(e) => setFormData({ ...formData, llm_model: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-400 flex items-center gap-2 text-white">
|
|
||||||
<Key size={14} /> API Key
|
|
||||||
</label>
|
|
||||||
<PasswordInput
|
|
||||||
required
|
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-sm focus:border-blue-500 outline-none transition-all text-white"
|
|
||||||
placeholder="sk-..."
|
|
||||||
onChange={(e) => setFormData({ ...formData, api_key: e.target.value })}
|
|
||||||
toggleLabels={passwordToggleLabels}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-400 flex items-center gap-2 text-white">
|
|
||||||
<FileText size={14} /> {t.soulLabel}
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-3 text-sm h-32 focus:border-blue-500 outline-none transition-all resize-none text-white"
|
|
||||||
defaultValue={formData.system_prompt}
|
|
||||||
onChange={(e) => setFormData({ ...formData, system_prompt: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 flex gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="flex-1 bg-slate-800 hover:bg-slate-700 py-3 rounded-xl font-bold transition-all text-white"
|
|
||||||
>
|
|
||||||
{t.cancel}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="flex-1 bg-blue-600 hover:bg-blue-500 py-3 rounded-xl font-bold flex items-center justify-center gap-2 transition-all shadow-lg shadow-blue-900/40 text-white"
|
|
||||||
>
|
|
||||||
<Save size={18} /> {isSubmitting ? t.creating : t.submit}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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<NanobotImage[]>([]);
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const imgRes = await axios.get<NanobotImage[]>(`${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 (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
|
||||||
<div className="w-full max-w-3xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200">
|
|
||||||
<div className="flex justify-between items-center p-6 border-b border-slate-800 bg-slate-800/50">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Cpu className="text-blue-400" size={24} />
|
|
||||||
<h2 className="text-xl font-bold text-white">{t.title}</h2>
|
|
||||||
</div>
|
|
||||||
<LucentIconButton onClick={onClose} className="p-2 hover:bg-slate-700 rounded-full transition-colors text-white" tooltip={locale === 'zh' ? '关闭' : 'Close'} aria-label={locale === 'zh' ? '关闭' : 'Close'}>
|
|
||||||
<X size={20} />
|
|
||||||
</LucentIconButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<h3 className="text-xs font-black text-slate-500 uppercase tracking-widest">{t.readySection}</h3>
|
|
||||||
<div className="space-y-2 h-[300px] overflow-y-auto pr-2">
|
|
||||||
{images.map((img) => (
|
|
||||||
<div key={img.tag} className="p-3 rounded-lg bg-white/[0.03] border border-white/5 flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-bold text-white">{img.tag}</p>
|
|
||||||
<p className="text-[10px] text-slate-500">{t.source}: {img.source_dir || t.pypi}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{img.status === 'READY' && <CheckCircle size={14} className="text-green-500" />}
|
|
||||||
{img.status === 'BUILDING' && <Loader2 size={14} className="text-blue-500 animate-spin" />}
|
|
||||||
{img.status === 'ERROR' && <AlertCircle size={14} className="text-red-500" />}
|
|
||||||
<span className={`text-[9px] font-bold ${img.status === 'READY' ? 'text-green-500' : 'text-slate-400'}`}>
|
|
||||||
{img.status}
|
|
||||||
</span>
|
|
||||||
<LucentIconButton
|
|
||||||
onClick={() => 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}
|
|
||||||
>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</LucentIconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-slate-800/30 border-t border-slate-800 flex items-center gap-2 text-[10px] text-slate-500">
|
|
||||||
<HardDrive size={12} />
|
|
||||||
<span>{t.footerHint}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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<string | null>(null);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [isKernelModalOpen, setIsKernelModalOpen] = useState(false);
|
|
||||||
const [operatingBotId, setOperatingBotId] = useState<string | null>(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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -3,6 +3,7 @@ import axios from 'axios';
|
||||||
|
|
||||||
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
|
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
|
||||||
import { APP_ENDPOINTS } from '../../../config/env';
|
import { APP_ENDPOINTS } from '../../../config/env';
|
||||||
|
import { sortBotsByCreatedAtDesc } from '../../dashboard/utils';
|
||||||
import { useAppStore } from '../../../store/appStore';
|
import { useAppStore } from '../../../store/appStore';
|
||||||
import type { BotState } from '../../../types/bot';
|
import type { BotState } from '../../../types/bot';
|
||||||
import {
|
import {
|
||||||
|
|
@ -62,15 +63,8 @@ export function usePlatformDashboard({ compactMode }: UsePlatformDashboardOption
|
||||||
const compactSheetTimerRef = useRef<number | null>(null);
|
const compactSheetTimerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const botList = useMemo(() => {
|
const botList = useMemo(() => {
|
||||||
const overviewBots = overview?.bots || [];
|
return sortBotsByCreatedAtDesc(Object.values(activeBots)) as BotState[];
|
||||||
if (overviewBots.length > 0) {
|
}, [activeBots]);
|
||||||
return overviewBots.map((bot) => ({
|
|
||||||
...(activeBots[bot.id] || { logs: [], messages: [], events: [] }),
|
|
||||||
...bot,
|
|
||||||
})) as BotState[];
|
|
||||||
}
|
|
||||||
return Object.values(activeBots);
|
|
||||||
}, [activeBots, overview?.bots]);
|
|
||||||
|
|
||||||
const filteredBots = useMemo(() => {
|
const filteredBots = useMemo(() => {
|
||||||
const keyword = search.trim().toLowerCase();
|
const keyword = search.trim().toLowerCase();
|
||||||
|
|
@ -173,8 +167,7 @@ export function usePlatformDashboard({ compactMode }: UsePlatformDashboardOption
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadOverview();
|
void loadOverview();
|
||||||
void loadBots();
|
}, [loadOverview]);
|
||||||
}, [loadBots, loadOverview]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pageSizeReady) return;
|
if (!pageSizeReady) return;
|
||||||
|
|
|
||||||
|
|
@ -131,24 +131,6 @@ export interface PlatformOverviewResponse {
|
||||||
source_dir?: string;
|
source_dir?: string;
|
||||||
created_at: 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;
|
settings: PlatformSettings;
|
||||||
usage: {
|
usage: {
|
||||||
summary: {
|
summary: {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ export const useAppStore = create<AppStore>((set) => ({
|
||||||
...prev,
|
...prev,
|
||||||
...bot,
|
...bot,
|
||||||
enabled: preferDefined(bot.enabled, prev?.enabled),
|
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),
|
image_tag: preferDefined(bot.image_tag, prev?.image_tag),
|
||||||
llm_provider: preferDefined(bot.llm_provider, prev?.llm_provider),
|
llm_provider: preferDefined(bot.llm_provider, prev?.llm_provider),
|
||||||
llm_model: preferDefined(bot.llm_model, prev?.llm_model),
|
llm_model: preferDefined(bot.llm_model, prev?.llm_model),
|
||||||
|
|
@ -105,6 +107,8 @@ export const useAppStore = create<AppStore>((set) => ({
|
||||||
...prev,
|
...prev,
|
||||||
...bot,
|
...bot,
|
||||||
enabled: preferDefined(bot.enabled, prev?.enabled),
|
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 ?? [],
|
logs: prev?.logs ?? bot.logs ?? [],
|
||||||
messages: prev?.messages ?? bot.messages ?? [],
|
messages: prev?.messages ?? bot.messages ?? [],
|
||||||
events: prev?.events ?? bot.events ?? [],
|
events: prev?.events ?? bot.events ?? [],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue