import {useEffect,useState,type ReactNode} from 'react'; import {Download,Link,Play,Server,Square} from 'lucide-react'; import {Badge,Button,ProgressBar,PropertyRow,Select} from '../../components/ui'; import {LocalTrainingClient} from '../../training/LocalTrainingClient'; import type {TrainingDevice,TrainingJob,TrainingServerInfo,WandbMode} from '../../training/types'; const ENDPOINT_KEY='mujoco-local-training-endpoint',JOB_KEY='mujoco-local-training-job'; const DEFAULT_ENDPOINT='http://127.0.0.1:8765'; const ACTIVE_STATES=new Set(['queued','running']); function stored(key:string,fallback=''):string{try{return localStorage.getItem(key)??fallback;}catch{return fallback;}} function errorText(error:unknown):string{return error instanceof Error?error.message:String(error);} function stateLabel(state:TrainingJob['state']):string{return {queued:'排队中',running:'训练中',succeeded:'已完成',failed:'失败',cancelled:'已取消'}[state];} export function LocalTrainingPanel({onPolicyReady}:{onPolicyReady(file:File):void}){ const [endpoint,setEndpoint]=useState(()=>stored(ENDPOINT_KEY,DEFAULT_ENDPOINT)); const [server,setServer]=useState(); const [job,setJob]=useState(); const [busy,setBusy]=useState(false),[error,setError]=useState(); const [taskId,setTaskId]=useState('Unitree-Go2-Flat'),[numEnvs,setNumEnvs]=useState(4096),[maxIterations,setMaxIterations]=useState(2000),[seed,setSeed]=useState(42),[runName,setRunName]=useState('web'),[device,setDevice]=useState('gpu'),[gpuIds,setGpuIds]=useState('0'),[wandbMode,setWandbMode]=useState('offline'); const connect=async()=>{ setBusy(true);setError(undefined); try{ const client=new LocalTrainingClient(endpoint),info=await client.health(); setServer(info);try{localStorage.setItem(ENDPOINT_KEY,client.endpoint);}catch{/* 当前会话仍可连接 */} if(info.tasks.length&&!info.tasks.includes(taskId))setTaskId(info.tasks[0]); const remembered=info.activeJobId??stored(JOB_KEY); if(remembered){try{setJob(await client.job(remembered));}catch{try{localStorage.removeItem(JOB_KEY);}catch{/* ignore */}}} if(!info.ready)setError(info.error??'训练服务尚未就绪'); }catch(value){setServer(undefined);setError(errorText(value));} finally{setBusy(false);} }; const jobId=job?.id,jobState=job?.state; useEffect(()=>{ if(!jobId||!jobState||!ACTIVE_STATES.has(jobState))return; let disposed=false; const refresh=async()=>{try{const next=await new LocalTrainingClient(endpoint).job(jobId);if(!disposed)setJob(next);}catch(value){if(!disposed)setError(errorText(value));}}; const timer=window.setInterval(()=>void refresh(),1500);return()=>{disposed=true;window.clearInterval(timer);}; },[endpoint,jobId,jobState]); const start=async()=>{ setBusy(true);setError(undefined); try{ const ids=device==='gpu'?gpuIds.split(/[\s,]+/).filter(Boolean).map(Number):[]; if(ids.some(id=>!Number.isInteger(id)||id<0))throw new Error('GPU 编号必须是非负整数'); const next=await new LocalTrainingClient(endpoint).start({taskId,numEnvs,maxIterations,seed,runName,device,gpuIds:ids,wandbMode}); setJob(next);try{localStorage.setItem(JOB_KEY,next.id);}catch{/* ignore */} }catch(value){setError(errorText(value));}finally{setBusy(false);} }; const cancel=async()=>{if(!job)return;setBusy(true);setError(undefined);try{setJob(await new LocalTrainingClient(endpoint).cancel(job.id));}catch(value){setError(errorText(value));}finally{setBusy(false);}}; const importResult=async()=>{if(!job)return;setBusy(true);setError(undefined);try{onPolicyReady(await new LocalTrainingClient(endpoint).downloadPolicy(job.id));}catch(value){setError(errorText(value));}finally{setBusy(false);}}; const active=Boolean(job&&ACTIVE_STATES.has(job.state)); return
{server?.trainerRoot??'请先启动本地训练服务'}{server?.ready?'可用':'离线'}
{server?.ready&&!job&&
setRunName(event.target.value)}/>
setGpuIds(event.target.value)}/>

训练使用本地 mjlab 任务资产,不会把浏览器中的模型上传到网络。服务一次只运行一个训练任务。

} {job&&
{job.taskId}{stateLabel(job.state)}
{job.logs.length>0&&
最近日志
{job.logs.slice(-40).join('\n')}
}
{active?:<>}
} {error&&

{error}

}
; } function Field({label,children}:{label:string;children:ReactNode}){return ;} function NumberField({label,value,min,max,onChange}:{label:string;value:number;min:number;max:number;onChange(value:number):void}){return onChange(Number(event.target.value))}/>;}