This commit is contained in:
orin
2026-07-30 14:09:04 +08:00
parent 25b24ac4cd
commit 49f5c49c53
35 changed files with 4078 additions and 48 deletions
+151 -6
View File
@@ -14,6 +14,7 @@ const state = {
provider: "openai",
executionMode: "dry_run",
conversationId: loadConversationId(),
vehicleId: null,
runtime: null,
result: null,
progressTimer: null,
@@ -37,8 +38,8 @@ const TWIN_DEFAULT_POSITIONS = {
legrest_extension: 0,
};
const TWIN_ANGLE_RANGES = {
backrest_recline: [95, 140],
legrest_height: [95, 160],
backrest_recline: [95, 150],
legrest_height: [95, 163],
};
function initJointTwinLayout() {
@@ -2436,11 +2437,132 @@ function selectExecutionMode(mode) {
refreshIcons();
}
function vehicleProfiles() {
return Array.isArray(state.runtime?.vehicles) ? state.runtime.vehicles : [];
}
function findVehicleProfile(vehicleId) {
return vehicleProfiles().find((vehicle) => vehicle.id === vehicleId)
|| state.runtime?.vehicle
|| null;
}
function applyTwinAngleRanges(vehicle) {
Object.keys(TWIN_ANGLE_RANGES).forEach((axis) => delete TWIN_ANGLE_RANGES[axis]);
const motors = vehicle?.seat?.motors || {};
Object.entries(motors).forEach(([axis, motor]) => {
const display = motor?.display_angle;
if (!display) return;
const zero = Number(display.zero_deg);
const maximum = Number(display.max_deg);
if (Number.isFinite(zero) && Number.isFinite(maximum) && maximum > zero) {
TWIN_ANGLE_RANGES[axis] = [zero, maximum];
}
});
}
function selectVehicle(vehicleId, { silent = false } = {}) {
const vehicle = findVehicleProfile(vehicleId);
if (!vehicle) return;
state.vehicleId = vehicle.id;
const select = $("#vehicleSelect");
if (select) select.value = vehicle.id;
applyTwinAngleRanges(vehicle);
const longitudinal = vehicle.seat?.motors?.longitudinal;
const backrest = vehicle.seat?.motors?.backrest_recline;
$("#vehicleNote").textContent = longitudinal && backrest
? `${vehicle.name} · 水平 ${longitudinal.range}${longitudinal.unit} · 靠背 ${backrest.range}${backrest.unit}`
: `${vehicle.name} · 已加载车型配置`;
if (state.result?.actuator_state) {
renderSeatState(state.result.actuator_state);
} else if (state.seatTwin?.actuatorState) {
updateSeatTwin(state.seatTwin.actuatorState);
}
if (!silent) showToast(`已切换车型:${vehicle.name}`);
}
function populateVehicleSelector() {
const select = $("#vehicleSelect");
if (!select) return;
const vehicles = vehicleProfiles();
select.innerHTML = vehicles.length
? vehicles.map((vehicle) => `<option value="${vehicle.id}">${vehicle.name}</option>`).join("")
: '<option value="">未找到车型配置</option>';
const defaultVehicleId = state.runtime?.default_vehicle_id
|| state.runtime?.vehicle?.id
|| vehicles[0]?.id
|| "";
select.disabled = vehicles.length === 0;
if (defaultVehicleId) selectVehicle(defaultVehicleId, { silent: true });
}
function modeProfiles() {
return Array.isArray(state.runtime?.modes) ? state.runtime.modes : [];
}
function modeChipLabel(mode) {
const name = String(mode?.name || mode?.id || "模式");
return name.endsWith("模式") && name.length > 2
? name.slice(0, -2)
: name;
}
function populateModeChips() {
const group = $(".mode-chips");
if (!group) return;
const buttons = () => $$("button", group);
modeProfiles().forEach((mode) => {
const label = modeChipLabel(mode);
let button = buttons().find((item) => item.dataset.modeProfileId === mode.id)
|| buttons().find((item) => item.textContent.trim() === label);
if (!button) {
button = document.createElement("button");
button.type = "button";
const icon = document.createElement("i");
icon.dataset.lucide = mode.icon || "bookmark";
button.append(icon, document.createTextNode(label));
group.append(button);
}
button.dataset.modeProfileId = mode.id;
button.dataset.modeCommand = `进入${mode.name}`;
button.title = mode.description || mode.name;
});
refreshIcons();
}
function applyModeCommand(command) {
if (!command) return;
$("#utterance").value = command;
$("#requestForm").requestSubmit();
}
function toggleSaveModeComposer() {
const composer = $("#saveModeComposer");
composer.hidden = !composer.hidden;
if (!composer.hidden) $("#saveModeName").focus();
}
function saveCurrentModeProfile() {
const input = $("#saveModeName");
const rawName = input.value.trim();
if (!rawName) {
showToast("请输入模式名称");
return;
}
const modeName = rawName.endsWith("模式") ? rawName : `${rawName}模式`;
$("#utterance").value = `这个模式不错,固化为${modeName}`;
$("#saveModeComposer").hidden = true;
input.value = "";
$("#requestForm").requestSubmit();
}
async function loadRuntime() {
try {
const response = await fetch("/api/runtime", { cache: "no-store" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
state.runtime = await response.json();
populateVehicleSelector();
populateModeChips();
const hardware = state.runtime.hardware || {};
const badge = $("#modelBadge");
const gptButton = $('[data-provider="openai"]');
@@ -3099,13 +3221,16 @@ function renderExecution(result) {
const reportSucceeded = report.spec.status === "succeeded";
summary.className = `execution-summary ${reportSucceeded ? "succeeded" : "failed"}`;
const presetSave = Boolean(result.preset_config);
const modeSave = Boolean(result.mode_profile);
const physical = report.spec.mode === "hil";
const executedDomains = [...new Set(report.spec.results.map((item) => item.domain))];
const motionCount = report.spec.results.filter((item) => item.telemetry?.motion_axis).length;
const firmwareResult = report.spec.results.find((item) => item.telemetry?.firmware_verified);
const operationResults = report.spec.results.filter((item) => item.ref !== "seat-controller-firmware");
let executionDetail = presetSave
? "当前五轴姿态已持久化到会话预设仓库,未生成 CAN 命令"
let executionDetail = modeSave
? `长期模式 ${result.mode_profile.name} 已写入配置库,未生成 CAN 命令`
: presetSave
? "当前五轴姿态已持久化到会话预设仓库,未生成 CAN 命令"
: physical
? `${executedDomains.join(" / ")} 已下发真实 CAN,并以 IAR 固件实际反馈确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
: `${executedDomains.join(" / ")} 共 ${report.spec.results.length} 个操作已完成 CAN 编码与虚拟执行`;
@@ -3132,7 +3257,7 @@ function renderExecution(result) {
? "真实台架未返回满足目标的反馈,移动链路已尝试发送 STOP_ALL。"
: "真实台架未返回满足目标的物理反馈。";
}
summary.innerHTML = `<i data-lucide="${reportSucceeded ? (presetSave ? "bookmark-check" : "circle-check-big") : "circle-x"}"></i><div><strong>${reportSucceeded ? (presetSave ? "姿态预设已保存" : (physical ? "真实台架执行成功" : "虚拟执行成功")) : "真实台架执行失败"}</strong><span>${executionDetail}</span></div>`;
summary.innerHTML = `<i data-lucide="${reportSucceeded ? ((presetSave || modeSave) ? "bookmark-check" : "circle-check-big") : "circle-x"}"></i><div><strong>${reportSucceeded ? (modeSave ? "长期模式已保存" : presetSave ? "姿态预设已保存" : (physical ? "真实台架执行成功" : "虚拟执行成功")) : "真实台架执行失败"}</strong><span>${executionDetail}</span></div>`;
$("#resultNavMeta").textContent = reportSucceeded
? `${report.spec.results.length} 项完成 · ${physical ? "HIL" : "DRY-RUN"}`
: "执行失败 · 查看回读";
@@ -3171,7 +3296,9 @@ function renderResult(result) {
ambient_light: "氛围灯",
};
$("#resultTitle").textContent = executionSucceeded
? result.preset_config
? result.mode_profile
? "长期模式配置已保存"
: result.preset_config
? "座椅姿态预设已保存"
: result.scene_config
? `${domains.length} 域场景闭环完成`
@@ -3192,6 +3319,7 @@ function renderResult(result) {
renderDomainState(result.domain_state);
updateTwinExplicitAngles(result);
renderSavedPresets(result);
if (result.mode_profile) loadRuntime();
renderCode(result);
renderExecution(result);
window.dispatchEvent(new CustomEvent("cognitive-agent:result", { detail: result }));
@@ -3226,6 +3354,7 @@ async function submitRequest(event) {
utterance,
provider: state.provider,
conversation_id: state.conversationId,
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
execution_mode: state.executionMode,
...(state.executionMode === "hardware"
? { approval_token: approvalToken }
@@ -3352,6 +3481,22 @@ document.addEventListener("DOMContentLoaded", async () => {
});
$("#newConversation").addEventListener("click", startNewConversation);
$("#logoutButton").addEventListener("click", logout);
$("#vehicleSelect").addEventListener("change", (event) => {
selectVehicle(event.target.value);
});
$(".mode-chips").addEventListener("click", (event) => {
const target = event.target instanceof Element ? event.target : null;
const button = target?.closest("button[data-mode-command]");
if (button) applyModeCommand(button.dataset.modeCommand);
});
$("#saveModeToggle").addEventListener("click", toggleSaveModeComposer);
$("#saveModeConfirm").addEventListener("click", saveCurrentModeProfile);
$("#saveModeName").addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.isComposing) {
event.preventDefault();
saveCurrentModeProfile();
}
});
$$(".segment[data-provider]").forEach((button) => button.addEventListener("click", () => selectProvider(button.dataset.provider)));
$$('[data-execution-mode]').forEach((button) => button.addEventListener("click", () => selectExecutionMode(button.dataset.executionMode)));
$$(".example-command").forEach((button) => button.addEventListener("click", () => {
+13 -2
View File
@@ -10,7 +10,7 @@
<script src="https://unpkg.com/three@0.147.0/build/three.min.js" defer></script>
<script src="https://unpkg.com/three@0.147.0/examples/js/libs/fflate.min.js" defer></script>
<script src="https://unpkg.com/three@0.147.0/examples/js/loaders/FBXLoader.js" defer></script>
<script src="/app.js?v=unified-cabin-12" defer></script>
<script src="/app.js?v=unified-cabin-13" defer></script>
<script type="module" src="/gesture-control.js?v=gesture-core-3"></script>
</head>
<body>
@@ -51,7 +51,7 @@
<button type="button" data-mode-command="副驾驶调整为零重力模式"><i data-lucide="orbit"></i>零重力</button>
</div>
<div class="save-mode-composer" id="saveModeComposer" hidden>
<input id="saveModeName" maxlength="24" placeholder="给当前座椅姿态起个名字" />
<input id="saveModeName" maxlength="24" placeholder="给当前模式起个名字" />
<button id="saveModeConfirm" type="button">保存</button>
</div>
</div>
@@ -95,6 +95,17 @@
<p class="provider-note" id="hardwareAliasNote">主驾和副驾暂时共享同一组实体电机、加热与通风执行器</p>
</div>
<div class="field-group vehicle-field">
<label class="field-label" for="vehicleSelect">车型配置</label>
<div class="vehicle-select-control">
<i data-lucide="car-front"></i>
<select id="vehicleSelect" name="vehicle_id" aria-label="选择车型配置">
<option value="">加载车型配置</option>
</select>
</div>
<p class="provider-note" id="vehicleNote">正在读取座椅量程与角度标定</p>
</div>
<div class="field-group approval-field" id="approvalField" hidden>
<label class="field-label" for="approvalToken">硬件执行授权令牌</label>
<input id="approvalToken" name="approval_token" type="password" inputmode="numeric" maxlength="6" autocomplete="off" spellcheck="false" placeholder="请输入 123456" />
+31
View File
@@ -1086,6 +1086,37 @@ textarea:focus {
margin-top: 18px;
}
.vehicle-select-control {
min-height: 40px;
display: grid;
grid-template-columns: 18px minmax(0, 1fr);
align-items: center;
gap: 9px;
padding: 0 11px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface-muted);
}
.vehicle-select-control svg {
width: 17px;
height: 17px;
color: var(--teal);
}
.vehicle-select-control select {
min-width: 0;
width: 100%;
height: 38px;
border: 0;
outline: 0;
background: transparent;
color: var(--ink);
font: inherit;
font-size: 13px;
font-weight: 750;
}
.segmented {
display: grid;
grid-template-columns: 1fr 1fr;