feat(training): release V0.8 自调参 Agent
This commit is contained in:
@@ -1,28 +1,25 @@
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import { Download, Link, Play, Server, Square } from 'lucide-react';
|
||||
import { Download, ExternalLink, Link, Play, Server, Square } from 'lucide-react';
|
||||
import { Badge, Button, ProgressBar, PropertyRow, Select } from '../components/ui';
|
||||
import { LocalTrainingClient } from './LocalTrainingClient';
|
||||
import type { TrainingDevice, TrainingJob, TrainingServerInfo, WandbMode } from './types';
|
||||
import type {
|
||||
RewardPreset,
|
||||
TrainingDevice,
|
||||
TrainingJob,
|
||||
TrainingServerInfo,
|
||||
WandbMode,
|
||||
} from './types';
|
||||
import {
|
||||
DEFAULT_TRAINING_ENDPOINT,
|
||||
localStored,
|
||||
rememberTrainingConnection,
|
||||
sessionStored,
|
||||
TRAINING_ENDPOINT_KEY,
|
||||
TRAINING_JOB_KEY,
|
||||
TRAINING_TOKEN_KEY,
|
||||
} from './storage';
|
||||
|
||||
const ENDPOINT_KEY = 'mujoco-local-training-endpoint',
|
||||
JOB_KEY = 'mujoco-local-training-job',
|
||||
TOKEN_KEY = 'mujoco-local-training-token';
|
||||
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 sessionStored(key: string): string {
|
||||
try {
|
||||
return sessionStorage.getItem(key) ?? '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
function errorText(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
@@ -37,10 +34,14 @@ function stateLabel(state: TrainingJob['state']): string {
|
||||
}
|
||||
|
||||
export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File): void }) {
|
||||
const [endpoint, setEndpoint] = useState(() => stored(ENDPOINT_KEY, DEFAULT_ENDPOINT));
|
||||
const [token, setToken] = useState(() => sessionStored(TOKEN_KEY));
|
||||
const [endpoint, setEndpoint] = useState(() =>
|
||||
localStored(TRAINING_ENDPOINT_KEY, DEFAULT_TRAINING_ENDPOINT),
|
||||
);
|
||||
const [token, setToken] = useState(() => sessionStored(TRAINING_TOKEN_KEY));
|
||||
const [server, setServer] = useState<TrainingServerInfo>();
|
||||
const [job, setJob] = useState<TrainingJob>();
|
||||
const [presets, setPresets] = useState<RewardPreset[]>([]);
|
||||
const [rewardPresetId, setRewardPresetId] = useState('');
|
||||
const [busy, setBusy] = useState(false),
|
||||
[error, setError] = useState<string>();
|
||||
const [taskId, setTaskId] = useState('Unitree-Go2-Flat'),
|
||||
@@ -60,26 +61,30 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
|
||||
info = await client.health();
|
||||
setServer(info);
|
||||
try {
|
||||
localStorage.setItem(ENDPOINT_KEY, client.endpoint);
|
||||
sessionStorage.setItem(TOKEN_KEY, client.token);
|
||||
setPresets(await client.presets());
|
||||
} catch {
|
||||
setPresets([]);
|
||||
}
|
||||
try {
|
||||
rememberTrainingConnection(client.endpoint, client.token);
|
||||
} catch {
|
||||
/* 当前会话仍可连接 */
|
||||
}
|
||||
if (info.tasks.length && !info.tasks.includes(taskId)) setTaskId(info.tasks[0]);
|
||||
const remembered = info.activeJobId ?? stored(JOB_KEY);
|
||||
const remembered = info.activeJobId ?? localStored(TRAINING_JOB_KEY);
|
||||
if (remembered) {
|
||||
try {
|
||||
const recovered = await client.job(remembered);
|
||||
setJob(recovered);
|
||||
try {
|
||||
localStorage.setItem(JOB_KEY, recovered.id);
|
||||
localStorage.setItem(TRAINING_JOB_KEY, recovered.id);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
} catch {
|
||||
setJob(undefined);
|
||||
try {
|
||||
localStorage.removeItem(JOB_KEY);
|
||||
localStorage.removeItem(TRAINING_JOB_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -116,6 +121,37 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
|
||||
};
|
||||
}, [endpoint, jobId, jobState, token]);
|
||||
|
||||
useEffect(() => {
|
||||
const receive = (event: MessageEvent) => {
|
||||
if (
|
||||
event.origin !== window.location.origin ||
|
||||
!event.source ||
|
||||
typeof event.data !== 'object'
|
||||
)
|
||||
return;
|
||||
const data = event.data as { type?: string; sessionId?: string };
|
||||
if (data.type === 'mujoco-tuning-ready') {
|
||||
(event.source as Window).postMessage(
|
||||
{ type: 'mujoco-tuning-credentials', endpoint, token },
|
||||
event.origin,
|
||||
);
|
||||
}
|
||||
if (data.type === 'mujoco-tuning-import-policy' && data.sessionId) {
|
||||
void new LocalTrainingClient(endpoint, token)
|
||||
.downloadBestPolicy(data.sessionId)
|
||||
.then(onPolicyReady)
|
||||
.catch((value: unknown) => setError(errorText(value)));
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', receive);
|
||||
return () => window.removeEventListener('message', receive);
|
||||
}, [endpoint, onPolicyReady, token]);
|
||||
|
||||
const openTuningDashboard = () => {
|
||||
rememberTrainingConnection(endpoint, token);
|
||||
window.open(new URL('tuning.html', document.baseURI), '_blank');
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
setBusy(true);
|
||||
setError(undefined);
|
||||
@@ -138,10 +174,11 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
|
||||
device,
|
||||
gpuIds: ids,
|
||||
wandbMode,
|
||||
rewardPresetId: rewardPresetId || undefined,
|
||||
});
|
||||
setJob(next);
|
||||
try {
|
||||
localStorage.setItem(JOB_KEY, next.id);
|
||||
localStorage.setItem(TRAINING_JOB_KEY, next.id);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -217,6 +254,15 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
|
||||
{server?.ready ? '可用' : '离线'}
|
||||
</Badge>
|
||||
</div>
|
||||
{server?.ready && (
|
||||
<Button
|
||||
className="mt-2 w-full"
|
||||
icon={<ExternalLink className="h-3.5 w-3.5" />}
|
||||
onClick={openTuningDashboard}
|
||||
>
|
||||
打开自调参 Agent 工作台
|
||||
</Button>
|
||||
)}
|
||||
{server?.ready && !job && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<Field label="训练任务">
|
||||
@@ -286,6 +332,21 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Field label="奖励配置">
|
||||
<Select
|
||||
aria-label="奖励配置 preset"
|
||||
className="w-full"
|
||||
value={rewardPresetId}
|
||||
onChange={(event) => setRewardPresetId(event.target.value)}
|
||||
>
|
||||
<option value="">仓库默认奖励</option>
|
||||
{presets.map((preset) => (
|
||||
<option key={preset.id} value={preset.id}>
|
||||
{preset.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="实验记录">
|
||||
<Select
|
||||
aria-label="W&B 模式"
|
||||
@@ -368,7 +429,7 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
|
||||
onClick={() => {
|
||||
setJob(undefined);
|
||||
try {
|
||||
localStorage.removeItem(JOB_KEY);
|
||||
localStorage.removeItem(TRAINING_JOB_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user