Files
Mujoco_WASM/web_platform/src/training/TrainingJobSection.tsx
T
chenlin 26bb5634bd
web-platform-ci / Standalone decision service (no cloud credentials) (push) Waiting to run
web-platform-ci / TypeScript, lint, unit, build (push) Waiting to run
web-platform-ci / Playwright E2E (push) Waiting to run
lekiwi-compatibility / cpu-compatibility (push) Waiting to run
refactor: release v1.0.5 安全精简与网站发布
2026-09-29 09:55:08 +08:00

137 lines
4.8 KiB
TypeScript

import { Download, Square } from 'lucide-react';
import { Badge, Button, ProgressBar, PropertyRow } from '../components/ui';
import type { MobileDeployment } from '../mobile/training';
import { PretrainedIdentity } from './PretrainedSourceSelect';
import { TrainingMetricsPanel } from './TrainingMetricsPanel';
import { trainingLosses } from './trainingLosses';
import { ACTIVE_STATES, stateLabel } from './trainingPresentation';
import type { TrainingJob } from './types';
export function TrainingJobSection({
job,
mobileDeployment,
busy,
onContinue,
onCancel,
onImport,
onNew,
}: {
job: TrainingJob;
mobileDeployment?: MobileDeployment;
busy: boolean;
onContinue(): void;
onCancel(): void | Promise<void>;
onImport(): void | Promise<void>;
onNew(): void;
}) {
const active = ACTIVE_STATES.has(job.state);
return (
<div className="mt-3 border-t border-border pt-3">
<div className="mb-2 flex items-center justify-between gap-2">
<span className="truncate text-xs font-medium text-text-primary" title={job.id}>
{job.taskId}
</span>
<Badge
tone={
job.state === 'succeeded'
? 'success'
: job.state === 'failed' || job.state === 'cancelled'
? 'warning'
: 'accent'
}
>
{stateLabel(job.state)}
</Badge>
</div>
<PretrainedIdentity source={job.pretrained} />
{mobileDeployment?.evaluation && (
<div className="mt-2 text-xs">
<PropertyRow label="阶段" value={mobileDeployment.trainingStage} />
<PropertyRow
label="独立评估成功率"
value={`${(mobileDeployment.evaluation.successRate * 100).toFixed(1)}% / ${mobileDeployment.evaluation.episodes} 回合`}
/>
<PropertyRow
label="实测关节峰值"
value={`${mobileDeployment.evaluation.maxJointVelocity.toFixed(3)} rad/s`}
/>
<PropertyRow label="安全终止次数" value={mobileDeployment.evaluation.safetyStops} />
<p className="text-warning">
导出成功不代表策略达标;未达标策略导入仅用于调试,请先同阶段续训。
</p>
</div>
)}
{job.taskId === 'Unitree-Go2-Rough' && (
<div>
<p className="text-xs text-warning">仅后端评测:234 维 Rough 策略无法在浏览器加载。</p>
</div>
)}
{job.deployment?.terrain && (
<div>
<p className="text-xs text-warning">
导入会替换当前物理地图并启动配套策略,请先保存场景。
</p>
<Badge tone="warning">
{job.deployment.terrain.approximation ? '近似碰撞布局' : '配套碰撞布局'}
</Badge>
</div>
)}
<ProgressBar value={job.progress} label="训练进度" />
<div className="mt-2">
<PropertyRow label="迭代" value={`${job.iteration} / ${job.maxIterations}`} />
<p role="status" className="break-words text-xs text-text-secondary">
{job.message}
</p>
{trainingLosses(job.logs).map(({ label, value }) => (
<PropertyRow key={label} label={label} value={value} />
))}
</div>
<TrainingMetricsPanel key={job.id} jobId={job.id} logs={job.logs} />
{job.logs.length > 0 && (
<details className="mt-2">
<summary className="cursor-pointer text-xs text-text-secondary">最近日志</summary>
<pre className="mt-1 max-h-36 overflow-auto whitespace-pre-wrap break-all rounded bg-app p-2 text-xs leading-4 text-text-tertiary">
{job.logs.slice(-40).join('\n')}
</pre>
</details>
)}
{mobileDeployment && job.state === 'succeeded' && (
<Button disabled={busy} onClick={onContinue}>
接续此作业(保留权重)
</Button>
)}
<div className="mt-3 grid grid-cols-2 gap-2">
{active ? (
<Button
variant="danger"
className="col-span-2"
icon={<Square className="h-3.5 w-3.5" />}
disabled={busy}
onClick={() => void onCancel()}
>
停止训练
</Button>
) : (
<>
<Button
disabled={
busy ||
!job.artifactReady ||
job.taskId === 'Unitree-Go2-Rough' ||
(job.deployment && !job.deployment.browserCompatible)
}
icon={<Download className="h-3.5 w-3.5" />}
onClick={() => void onImport()}
>
导入策略
</Button>
<Button disabled={busy} onClick={onNew}>
新建任务
</Button>
</>
)}
</div>
</div>
);
}