backend
This commit is contained in:
+151
-6
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user