refactor(web-platform): release V0.7.2 项目常规优化
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import { useRef, type ChangeEvent } from 'react';
|
||||
import { BrainCircuit, FileUp, Power, RotateCw, Trash2 } from 'lucide-react';
|
||||
import type { RLCommand, RLPolicyStatus } from './types';
|
||||
import { Badge, Button, PropertyRow, Select } from '../components/ui';
|
||||
|
||||
export interface RLPolicyPanelProps {
|
||||
paths: string[];
|
||||
selectedPath?: string;
|
||||
status?: RLPolicyStatus;
|
||||
loading: boolean;
|
||||
onSelectPath(path: string): void;
|
||||
onLoadPath(path: string): void;
|
||||
onImport(file: File): void;
|
||||
onToggle(enabled: boolean): void;
|
||||
onCommand(command: RLCommand): void;
|
||||
onRemove(): void;
|
||||
}
|
||||
|
||||
export function RLPolicyPanel({
|
||||
paths,
|
||||
selectedPath,
|
||||
status,
|
||||
loading,
|
||||
onSelectPath,
|
||||
onLoadPath,
|
||||
onImport,
|
||||
onToggle,
|
||||
onCommand,
|
||||
onRemove,
|
||||
}: RLPolicyPanelProps) {
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const importFile = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) onImport(file);
|
||||
event.target.value = '';
|
||||
};
|
||||
const command = status?.command ?? { linearX: 0, linearY: 0, angularZ: 0 };
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
ref={input}
|
||||
className="hidden"
|
||||
type="file"
|
||||
accept=".onnx,application/octet-stream"
|
||||
onChange={importFile}
|
||||
/>
|
||||
{paths.length > 0 && (
|
||||
<label className="mb-3 block text-xs text-text-secondary">
|
||||
<span className="mb-1 block">工程内策略</span>
|
||||
<Select
|
||||
aria-label="ONNX 策略"
|
||||
className="w-full"
|
||||
value={selectedPath ?? ''}
|
||||
disabled={loading}
|
||||
onChange={(event) => onSelectPath(event.target.value)}
|
||||
>
|
||||
<option value="">选择 .onnx 文件</option>
|
||||
{paths.map((path) => (
|
||||
<option key={path} value={path}>
|
||||
{path}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
icon={<FileUp className="h-3.5 w-3.5" />}
|
||||
disabled={loading}
|
||||
onClick={() => input.current?.click()}
|
||||
>
|
||||
导入 ONNX
|
||||
</Button>
|
||||
<Button
|
||||
icon={<RotateCw className="h-3.5 w-3.5" />}
|
||||
disabled={loading || !selectedPath}
|
||||
onClick={() => selectedPath && onLoadPath(selectedPath)}
|
||||
>
|
||||
加载策略
|
||||
</Button>
|
||||
</div>
|
||||
{status ? (
|
||||
<div className="mt-3 rounded-lg border border-border bg-surface p-2.5">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<span
|
||||
className="flex min-w-0 items-center gap-1.5 truncate text-xs font-medium text-text-primary"
|
||||
title={status.path}
|
||||
>
|
||||
<BrainCircuit className="h-3.5 w-3.5 shrink-0 text-accent" />
|
||||
{status.taskName}
|
||||
</span>
|
||||
<Badge>{status.enabled ? '推理中' : '已停止'}</Badge>
|
||||
</div>
|
||||
<PropertyRow label="控制频率" value={`${status.controlHz} Hz`} />
|
||||
<PropertyRow
|
||||
label="观测 / 动作"
|
||||
value={`${status.observationSize} / ${status.actionSize}`}
|
||||
/>
|
||||
<PropertyRow label="推理次数" value={status.inferenceCount} />
|
||||
<PropertyRow label="上次推理" value={`${status.lastInferenceMs.toFixed(2)} ms`} />
|
||||
<div className="mt-3 border-t border-border pt-3">
|
||||
<p className="mb-2 text-[10px] text-text-tertiary">速度指令(机身坐标系)</p>
|
||||
<CommandInput
|
||||
label="前向 m/s"
|
||||
value={command.linearX}
|
||||
min={-0.5}
|
||||
max={1}
|
||||
onChange={(linearX) => onCommand({ ...command, linearX })}
|
||||
/>
|
||||
<CommandInput
|
||||
label="侧向 m/s"
|
||||
value={command.linearY}
|
||||
min={-0.5}
|
||||
max={0.5}
|
||||
onChange={(linearY) => onCommand({ ...command, linearY })}
|
||||
/>
|
||||
<CommandInput
|
||||
label="偏航 rad/s"
|
||||
value={command.angularZ}
|
||||
min={-1}
|
||||
max={1}
|
||||
onChange={(angularZ) => onCommand({ ...command, angularZ })}
|
||||
/>
|
||||
<Button
|
||||
className="mt-1 w-full"
|
||||
onClick={() => onCommand({ linearX: 0, linearY: 0, angularZ: 0 })}
|
||||
>
|
||||
停止移动
|
||||
</Button>
|
||||
</div>
|
||||
{status.error && (
|
||||
<p
|
||||
role="alert"
|
||||
className="mt-2 break-words rounded bg-danger/10 p-2 text-[10px] leading-4 text-danger"
|
||||
>
|
||||
{status.error}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant={status.enabled ? 'secondary' : 'primary'}
|
||||
icon={<Power className="h-3.5 w-3.5" />}
|
||||
disabled={Boolean(status.error)}
|
||||
onClick={() => onToggle(!status.enabled)}
|
||||
>
|
||||
{status.enabled ? '停止' : '启用'}
|
||||
</Button>
|
||||
<Button variant="danger" icon={<Trash2 className="h-3.5 w-3.5" />} onClick={onRemove}>
|
||||
卸载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 text-xs leading-5 text-text-tertiary">
|
||||
加载 mjlab 导出的单输入、单动作输出 policy.onnx。首个内置任务使用 47 维 Go2 actor 观测和
|
||||
12 维腿部关节位置动作;Go2-W 轮电机保持零力矩。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
label,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
onChange(value: number): void;
|
||||
}) {
|
||||
return (
|
||||
<label className="mb-2 grid grid-cols-[1fr_72px] items-center gap-2 text-[10px] text-text-tertiary">
|
||||
<span>{label}</span>
|
||||
<input
|
||||
className="field h-7 w-full px-2 text-right text-xs text-text-primary"
|
||||
type="number"
|
||||
step="0.05"
|
||||
min={min}
|
||||
max={max}
|
||||
value={value}
|
||||
onChange={(event) => onChange(Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user