diff --git a/.gitignore b/.gitignore
index 1e0b874..304a00e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
__pycache__/
*.py[cod]
artifacts/runs/
+artifacts/mode-profiles/
artifacts/server/
artifacts/tmp/
artifacts/self-repair/
diff --git a/apps/web-console/app.js b/apps/web-console/app.js
index ea31ef8..72e5c71 100755
--- a/apps/web-console/app.js
+++ b/apps/web-console/app.js
@@ -44,10 +44,13 @@ function loadControlTransport() {
return ["wired_can", "wireless_mqtt"].includes(stored) ? stored : "wired_can";
}
+const DEFAULT_EXECUTION_MODE = "hardware";
+
const state = {
provider: "openai",
reasoningEffort: localStorage.getItem("cognitive-agent.reasoning-effort") || "medium",
- executionMode: "dry_run",
+ executionMode: DEFAULT_EXECUTION_MODE,
+ preferredExecutionMode: DEFAULT_EXECUTION_MODE,
controlTransport: loadControlTransport(),
conversationId: loadConversationId(),
vehicleId: null,
@@ -55,7 +58,6 @@ const state = {
result: null,
vehicleStateSnapshot: null,
vehicleStateTurn: -1,
- progressTimer: null,
seatTwin: null,
seatActuatorState: {},
seatFeedbackSource: "simulated",
@@ -72,6 +74,7 @@ const state = {
modeRunTimer: null,
modeDraftCommand: "",
lastSuccessfulUtterance: "",
+ modeEditor: null,
};
const $ = (selector, root = document) => root.querySelector(selector);
@@ -84,6 +87,7 @@ const apiRequest = (input, options) => window.CognitiveAgentNetwork.request(inpu
const HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000;
const REQUEST_TIMEOUT_MS = 45 * 1000;
const DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS = 5000;
+const AUTHORITATIVE_CH1_SOURCES = new Set(["physical_can_ch1", "wireless_mqtt_ch1"]);
function requestTimeoutForExecutionMode(mode) {
return mode === "hardware" ? HARDWARE_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
@@ -95,6 +99,66 @@ function physicalStatePollIntervalMs() {
return Math.max(250, Math.min(DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS, configured));
}
+function isAuthoritativeCh1Source(value) {
+ return AUTHORITATIVE_CH1_SOURCES.has(String(value || ""));
+}
+
+function renderCabinLiveStatus(networkStatus = state.connectionStatus) {
+ const indicator = $("#cabinLiveStatus");
+ if (!indicator) return;
+ const label = $("b", indicator);
+ if (!label) return;
+ const summarySystem = $("#cabinSummarySystem");
+ const setSummarySystem = (text, status) => {
+ if (!summarySystem) return;
+ summarySystem.textContent = text;
+ summarySystem.dataset.state = status;
+ };
+ if (networkStatus !== "online") {
+ indicator.dataset.state = networkStatus === "connecting" ? "connecting" : "offline";
+ label.textContent = networkStatus === "connecting"
+ ? "正在连接车辆服务"
+ : "车辆服务通信离线";
+ setSummarySystem(networkStatus === "connecting" ? "连接中" : "服务离线", "waiting");
+ return;
+ }
+
+ const selected = selectedVehicleTransport();
+ const wireless = state.controlTransport === "wireless_mqtt";
+ const transportLabel = wireless ? "无线 CAN" : "有线 CAN";
+ if (selected?.state_configured !== true) {
+ indicator.dataset.state = "offline";
+ label.textContent = `${transportLabel} 未配置`;
+ setSummarySystem("链路未配置", "waiting");
+ return;
+ }
+
+ const snapshot = state.vehicleStateSnapshot;
+ const observedIds = Array.isArray(snapshot?.observedFrameIds)
+ ? snapshot.observedFrameIds
+ : [];
+ const maxAgeMs = Math.max(3000, physicalStatePollIntervalMs() * 4);
+ const responseAgeMs = Date.now() - Number(snapshot?.receivedAt || 0);
+ if (!snapshot || responseAgeMs > maxAgeMs || observedIds.length === 0) {
+ indicator.dataset.state = "connecting";
+ label.textContent = `${transportLabel} · 等待 CAN1 状态`;
+ indicator.title = snapshot?.error || "尚未收到可按矩阵解析的 CAN1 状态帧";
+ setSummarySystem("等待状态", "waiting");
+ return;
+ }
+
+ const sourceLabel = snapshot.source === "wireless_mqtt_ch1" ? "无线 CAN" : "有线 CAN";
+ indicator.dataset.state = "online";
+ label.textContent = `${sourceLabel} 实时 · ${observedIds.length} 类报文`;
+ indicator.title = [
+ snapshot.capturedAt ? `采集时间 ${snapshot.capturedAt}` : "",
+ observedIds.length ? `报文 ${observedIds.join("、")}` : "",
+ ].filter(Boolean).join(" · ");
+ if (summarySystem?.dataset.state !== "warning") {
+ setSummarySystem("CAN 实时", "healthy");
+ }
+}
+
function refreshIcons() {
if (window.lucide) {
window.lucide.createIcons({ attrs: { "aria-hidden": "true" } });
@@ -169,7 +233,7 @@ function initJointTwinLayout() {
const cabinHeadingActions = document.createElement("div");
cabinHeadingActions.className = "cabin-heading-actions";
cabinHeadingActions.innerHTML = `
- 通信正常 · 7 个设备在线
+ 正在读取车辆 CAN 状态
`;
@@ -182,7 +246,7 @@ function initJointTwinLayout() {
1–3 / 7
@@ -599,14 +663,7 @@ function initJointTwinLayout() {
});
const updateCabinConnection = (status = state.connectionStatus) => {
- const indicator = $("#cabinLiveStatus", cabinBand);
- const label = $("b", indicator);
- if (!indicator || !label) return;
- const online = status === "online";
- indicator.dataset.state = online ? "online" : status === "connecting" ? "connecting" : "offline";
- label.textContent = online
- ? "通信正常 · 7 个设备在线"
- : status === "connecting" ? "正在连接车辆设备" : "车辆设备通信离线";
+ renderCabinLiveStatus(status);
};
window.addEventListener("cognitive-agent:connection", (event) => {
updateCabinConnection(event.detail?.status || "offline");
@@ -2413,8 +2470,21 @@ function renderTwinCalloutValues(device) {
function twinDeviceForSeat(seat, actuatorState = state.seatTwin?.actuatorState || {}) {
const device = actuatorState?.[seat] || {};
- if (seat !== "driver") return device;
const position = device.position || {};
+ if (seat !== "driver") {
+ const seatHeight = position.height || position.cushion_tilt;
+ return {
+ ...device,
+ position: {
+ ...position,
+ // The seat-domain compatibility key is cushion_tilt, but seat-ota
+ // board axis 1 is physical height. Keep the 3D cushion neutral and
+ // animate/display the firmware feedback as height.
+ cushion_tilt: { position_pct: 0, position_valid: true },
+ height: seatHeight,
+ },
+ };
+ }
return {
...device,
position: {
@@ -2429,7 +2499,7 @@ function twinDeviceForSeat(seat, actuatorState = state.seatTwin?.actuatorState |
function twinAxesForSeat(seat) {
return seat === "driver"
? new Set(["longitudinal", "height", "cushion_tilt", "backrest_recline"])
- : new Set(["longitudinal", "cushion_tilt", "backrest_recline", "legrest_height", "legrest_extension"]);
+ : new Set(["longitudinal", "height", "backrest_recline", "legrest_height"]);
}
function renderTwinReadout() {
@@ -2444,6 +2514,8 @@ function renderTwinReadout() {
};
$("#twinSeatName").textContent = seat === "driver" ? "主驾座椅" : "副驾座椅";
$("#twinSeatZone").textContent = seat === "driver" ? "CH1 · 左前排" : "CH0 · 右前排";
+ const thermalRow = $(".twin-thermal-row");
+ if (thermalRow) thermalRow.hidden = seat === "passenger";
const postureAngles = twinPostureAngles(device);
Object.entries(TWIN_DEFAULT_POSITIONS).forEach(([axis, fallback]) => {
const motion = device?.position?.[axis];
@@ -2519,7 +2591,7 @@ function updateSeatTwin(actuatorState) {
});
["ventilation", "heating"].forEach((feature) => {
const value = device?.[feature];
- const enabled = value?.state === "on";
+ const enabled = seat === "driver" && value?.state === "on";
const level = enabled ? Math.max(1, Math.min(3, Number(value.level) || 1)) : 0;
model.thermalTargets[feature] = level / 3;
});
@@ -4277,6 +4349,7 @@ function renderControlTransport() {
});
const selected = selectedVehicleTransport();
const note = $("#vehicleTransportNote");
+ renderCabinLiveStatus();
if (!note) return;
if (selected?.state_configured !== true) {
note.textContent = state.controlTransport === "wireless_mqtt"
@@ -4286,7 +4359,7 @@ function renderControlTransport() {
}
if (state.controlTransport === "wireless_mqtt") {
const connection = selected?.state_sync?.state === "online" ? "已连接" : "未连接";
- note.textContent = `MQTT ${connection} · 状态 ${selected?.condition_topic || "n7/n7-headunit/can/state"} · 控制 ${selected?.control_topic || "n7/can_control"}`;
+ note.textContent = `MQTT ${connection} · 状态 ${selected?.condition_topic || "n7/n7-headunit/can/state"} · 控制 ${selected?.control_topic || "n7/n7-headunit/can/command"}`;
} else {
const connection = selected?.state_sync?.state === "online" ? "已连接" : "未连接";
note.textContent = `ZLG 有线 ${connection} · CH${selected?.channel ?? 1} · CAN FD 500k/2M`;
@@ -4303,10 +4376,12 @@ function selectControlTransport(transport) {
sessionStorage.setItem("cognitive-agent.conversation-id", state.conversationId);
state.seatFeedbackSources.passenger = "unavailable";
state.seatFeedbackSources.driver = "unavailable";
+ resetVehicleStatusDisplay();
}
state.vehicleStateSnapshot = null;
state.vehicleStateTurn = -1;
renderControlTransport();
+ renderCabinLiveStatus();
applyRuntimeTargetPolicy(state.runtime);
}
@@ -4368,42 +4443,23 @@ function updateSeatFeedbackModeLabel() {
if (label) label.textContent = `CH0 副驾 · ${descriptions[state.seatFeedbackSources.passenger]} / CH1 主驾 · ${descriptions[state.seatFeedbackSources.driver]}`;
}
-function selectExecutionMode(mode) {
+function selectExecutionMode(mode, { remember = true } = {}) {
const selectedTransport = selectedVehicleTransport();
const hardwareExecutionReady = Boolean(
state.runtime?.hardware?.configured || selectedTransport?.configured,
);
- if (mode === "hardware" && !hardwareExecutionReady) return;
- if (mode === "simulated_hardware" && !state.runtime?.simulated_hardware?.enabled) return;
+ if (mode === "hardware" && !hardwareExecutionReady) return false;
+ if (mode === "simulated_hardware" && !state.runtime?.simulated_hardware?.enabled) return false;
+ if (remember) state.preferredExecutionMode = mode;
state.executionMode = mode;
$$('[data-execution-mode]').forEach((button) => {
button.classList.toggle("active", button.dataset.executionMode === mode);
});
const hardware = mode === "hardware";
const simulatedHardware = mode === "simulated_hardware";
- const vehicleCanReady = Boolean(selectedTransport?.configured);
const mqttSelected = state.controlTransport === "wireless_mqtt";
const ch1DriverOnly = isDriverOnlyCh1Runtime();
$("#simulationFaultField").hidden = !simulatedHardware;
- $("#executionBadge").className = `status-badge ${hardware ? "" : simulatedHardware ? "warning" : "neutral"}`.trim();
- $("#executionBadge").innerHTML = hardware
- ? '
HIL · 真实 CAN'
- : simulatedHardware
- ? '
SIM · 模拟台架'
- : '
DRY-RUN';
- $("#safetyTitle").textContent = hardware
- ? (ch1DriverOnly ? "CH1 实车安全门禁" : "真实台架安全门禁")
- : simulatedHardware ? "模拟台架隔离边界" : "虚拟执行边界";
- $("#safetyText").textContent = hardware
- ? ch1DriverOnly && vehicleCanReady
- ? `仅使用${mqttSelected ? "本地 MQTT 无线链路" : "ZLG 有线 CH1"}控制主驾座椅与已验证车身设备;每次动作前必须确认 P 挡和 0 km/h,并以 CH1 状态帧回读为准。CH0、副驾 T2G 与 IAR 路径均不参与。`
- : vehicleCanReady
- ? "同一 CAN 盒由单进程持有:CH0 控制副驾四电机并校验板端反馈;CH1 控制主驾四电机与车身设备,每次动作前校验 P 挡、零车速和对应状态帧。"
- : "当前仅通道 0 连接副驾座椅台架并校验板端反馈;车端 CAN 未启用,模式中的其他车舱领域保持虚拟执行。"
- : simulatedHardware
- ? "故障只注入执行报告;物理 CAN、ZLG 适配器和 IAR 固件路径保持关闭。"
- : "四域 CAN 均为 dry-run,真实发送被强制关闭。";
- $("#executeStageLabel").textContent = hardware ? "台架执行" : simulatedHardware ? "故障仿真" : "虚拟执行";
$("#feedbackModeLabel").textContent = hardware
? (ch1DriverOnly
? (mqttSelected
@@ -4421,6 +4477,7 @@ function selectExecutionMode(mode) {
}
updateSeatFeedbackModeLabel();
refreshIcons();
+ return true;
}
function vehicleProfiles() {
@@ -4485,17 +4542,24 @@ function renderSeatMotorRows(vehicle) {
["backrest_recline", { name: "靠背" }],
["cushion_tilt", { name: "盆倾" }],
]
- : Object.entries(vehicle?.seat?.motors || {});
+ : [
+ ["longitudinal", { name: "前后" }],
+ ["cushion_tilt", { name: "高度" }],
+ ["backrest_recline", { name: "靠背" }],
+ ["legrest_height", { name: "腿托" }],
+ ];
const fragment = document.createDocumentFragment();
motors.forEach(([axis, motor]) => {
const item = document.createElement("div");
item.className = "motion-axis";
item.dataset.axis = axis;
const label = document.createElement("span");
- label.textContent = {
+ label.textContent = seat === "passenger" && axis === "cushion_tilt"
+ ? "高度"
+ : {
longitudinal: "水平", cushion_tilt: "坐垫", height: "高度",
backrest_recline: "靠背", legrest_height: "腿托", legrest_extension: "伸缩",
- }[axis] || motor?.name || axis;
+ }[axis] || motor?.name || axis;
const value = document.createElement("b");
value.textContent = "--";
item.append(label, value, document.createElement("i"));
@@ -4621,7 +4685,7 @@ function modeGalleryBuckets() {
return MODE_GALLERY_GROUPS.map((definition) => {
let groupModes;
if (definition.userOnly) {
- groupModes = modes.filter(isUserModeProfile);
+ groupModes = modes.filter((mode) => mode?.origin === "user");
} else if (definition.id === "comfort") {
groupModes = definition.modeIds
.map((modeId) => modes.find((mode) => mode.id === modeId))
@@ -4640,13 +4704,13 @@ function modeGalleryBuckets() {
function createModeGalleryCard(mode) {
const shell = document.createElement("article");
shell.className = "mode-gallery-card-shell";
- shell.dataset.modeOrigin = isUserModeProfile(mode) ? "user" : "preset";
+ shell.dataset.modeOrigin = mode?.origin || "preset";
const button = document.createElement("button");
button.className = "mode-gallery-card";
button.type = "button";
button.dataset.modeProfileId = mode.id;
button.dataset.modeCommand = `进入${mode.name}`;
- button.dataset.modeOrigin = isUserModeProfile(mode) ? "user" : "preset";
+ button.dataset.modeOrigin = mode?.origin || "preset";
button.dataset.modeState = "idle";
button.setAttribute("aria-pressed", "false");
@@ -4679,13 +4743,29 @@ function createModeGalleryCard(mode) {
copy.append(heading, description, meta);
button.append(iconBox, copy);
shell.append(button);
+
+ const editButton = document.createElement("button");
+ editButton.className = "mode-gallery-edit";
+ editButton.type = "button";
+ editButton.dataset.modeEdit = mode.id;
+ editButton.title = `编辑${mode.name}`;
+ editButton.setAttribute("aria-label", `编辑功能模式${mode.name}`);
+ const editIcon = document.createElement("i");
+ editIcon.dataset.lucide = "pencil";
+ editButton.append(editIcon);
+ shell.append(editButton);
+
if (isUserModeProfile(mode)) {
const deleteButton = document.createElement("button");
deleteButton.className = "mode-gallery-delete";
deleteButton.type = "button";
deleteButton.dataset.modeDelete = mode.id;
- deleteButton.title = `删除${mode.name}`;
- deleteButton.setAttribute("aria-label", `删除自定义功能模式${mode.name}`);
+ const presetOverride = mode.preset_override === true;
+ deleteButton.title = presetOverride ? `恢复${mode.name}的预设配置` : `删除${mode.name}`;
+ deleteButton.setAttribute(
+ "aria-label",
+ presetOverride ? `恢复功能模式${mode.name}的预设配置` : `删除自定义功能模式${mode.name}`,
+ );
const deleteIcon = document.createElement("i");
deleteIcon.dataset.lucide = "trash-2";
deleteButton.append(deleteIcon);
@@ -4817,6 +4897,535 @@ function closeModeGallery({ restoreFocus = true } = {}) {
if (restoreFocus) $("#modeGalleryToggle").focus();
}
+function modeEditorCommandCount(flow) {
+ const plans = flow?.plans && typeof flow.plans === "object" ? flow.plans : {};
+ return Object.values(plans).reduce(
+ (count, plan) => count + (Array.isArray(plan?.spec?.commands) ? plan.spec.commands.length : 0),
+ 0,
+ );
+}
+
+function modeEditorFlowFromResult(result) {
+ const plans = result?.command_plans && typeof result.command_plans === "object"
+ ? result.command_plans
+ : {};
+ const functionConfigs = result?.function_configs && typeof result.function_configs === "object"
+ ? result.function_configs
+ : {};
+ const executionSteps = Array.isArray(result?.execution_report?.spec?.results)
+ ? result.execution_report.spec.results
+ : [];
+ return {
+ schema: "cognitive-agent-mode-command-flow/v1",
+ source_request_id: result?.request_id || "",
+ execution_mode: result?.execution_mode || "",
+ control_transport: result?.control_transport || "",
+ function_configs: functionConfigs,
+ plans,
+ execution_steps: executionSteps,
+ };
+}
+
+function modeEditorCommandCategory(command) {
+ const value = String(command || "").trim();
+ if (/^ac temp\b/.test(value)) return "空调温度";
+ if (/^ac fan\b/.test(value)) return "空调风量";
+ if (/^ac (?:on|off)$/.test(value)) return "空调电源";
+ if (/^ac mode\b/.test(value)) return "出风模式";
+ if (/^ac control\b/.test(value)) return "空调控制方式";
+ if (/^ac cycle\b/.test(value)) return "空调循环";
+ if (/^window\b/.test(value)) return value.includes(" position ") ? "单窗位置" : "车窗动作";
+ if (/^shade\b/.test(value)) return "遮阳帘";
+ if (/^light ambient\b/.test(value)) return "氛围灯";
+ if (/^light reading\b/.test(value)) return "全部阅读灯";
+ if (/^light indoor\b/.test(value)) return "室内灯总开关";
+ if (/^seat passenger\b/.test(value)) return "副驾座椅控制";
+ if (/^seat driver\b/.test(value)) return "主驾座椅控制";
+ if (/^outlet\b/.test(value)) return "电动风口";
+ if (/^door rear\b/.test(value)) return "后排门锁";
+ if (/^door vehicle\b/.test(value)) return "整车门锁";
+ return "CAN 控制";
+}
+
+function modeEditorPlanCommands(plan, operationRef = "") {
+ const commands = Array.isArray(plan?.spec?.commands) ? plan.spec.commands : [];
+ if (!operationRef) return commands;
+ return commands.filter((item) => String(item?.operation_ref || "")
+ .split(",")
+ .map((value) => value.trim())
+ .includes(operationRef));
+}
+
+function modeEditorCanId(value) {
+ const normalized = String(value || "").trim().replace(/^0x/i, "").toUpperCase();
+ return normalized ? `0x${normalized}` : "";
+}
+
+function modeEditorFrameFromPlan(command, readable) {
+ const signals = command?.signals || {};
+ const can0Frame = String(readable?.frame || "").match(/^(0x[0-9A-F]+)\s*·\s*(.+)$/i);
+ const frameId = modeEditorCanId(signals.frame_id || can0Frame?.[1]);
+ const dataHex = String(signals.data_hex || can0Frame?.[2] || "").toUpperCase();
+ if (!frameId || !dataHex) return null;
+ const channelIndex = readable?.channel === "CAN0" ? 0 : 1;
+ const isFd = Boolean(signals.is_fd);
+ return {
+ frame_id: frameId,
+ data_hex: dataHex,
+ channel: channelIndex,
+ is_fd: isFd,
+ bitrate_switch: Boolean(signals.bitrate_switch),
+ };
+}
+
+function modeEditorNormalizeFrame(frame, readable) {
+ if (!frame || typeof frame !== "object") return null;
+ const frameId = modeEditorCanId(frame.frame_id || frame.canId);
+ const dataHex = String(frame.data_hex || frame.data || "").toUpperCase();
+ if (!frameId || !dataHex) return null;
+ const rawChannel = frame.channel;
+ const channel = typeof rawChannel === "string"
+ ? Number(rawChannel.replace(/^can/i, ""))
+ : Number(rawChannel ?? (readable?.channel === "CAN0" ? 0 : 1));
+ const isFd = frame.is_fd === true || frame.frameType === "fd" || frame.frame_format === "can_fd";
+ return {
+ frame_id: frameId,
+ data_hex: dataHex,
+ channel: Number.isInteger(channel) ? channel : readable?.channel === "CAN0" ? 0 : 1,
+ is_fd: isFd,
+ bitrate_switch: Boolean(frame.bitrate_switch ?? frame.brs),
+ };
+}
+
+function modeEditorMqttEnvelope(frame, command, requestId = "<运行时生成>") {
+ const topic = state.runtime?.vehicle_mqtt?.control_topic || "n7/n7-headunit/can/command";
+ return {
+ topic,
+ qos: Number(state.runtime?.vehicle_mqtt?.qos ?? 1),
+ retain: false,
+ payload: {
+ requestId,
+ command,
+ channel: `can${frame.channel}`,
+ canId: frame.frame_id,
+ frameType: frame.is_fd ? "fd" : "classic",
+ brs: frame.bitrate_switch,
+ extended: false,
+ remote: false,
+ data: frame.data_hex,
+ repeat: 1,
+ intervalMs: 0,
+ responseTimeoutMs: Number(state.runtime?.vehicle_mqtt?.feedback_timeout_ms ?? 1500),
+ },
+ };
+}
+
+function modeEditorMessagesForOperation(flow, readable, operationRef, plan) {
+ const steps = Array.isArray(flow?.execution_steps) ? flow.execution_steps : [];
+ const matchingSteps = steps.filter((item) => String(item?.ref || "") === operationRef);
+ const actualMessages = matchingSteps.flatMap((item) => {
+ const telemetry = item?.telemetry && typeof item.telemetry === "object" ? item.telemetry : item;
+ return Array.isArray(telemetry?.mqtt_messages) ? telemetry.mqtt_messages : [];
+ });
+ if (actualMessages.length) {
+ return actualMessages.map((message, index) => ({
+ sequence: index + 1,
+ offset_ms: null,
+ source: "published",
+ ...message,
+ }));
+ }
+
+ let frames = matchingSteps.flatMap((item) => {
+ const telemetry = item?.telemetry && typeof item.telemetry === "object" ? item.telemetry : item;
+ const sentFrames = Array.isArray(telemetry?.sent_frames) ? telemetry.sent_frames : [];
+ if (sentFrames.length) return sentFrames;
+ return telemetry?.command_frame ? [telemetry.command_frame] : [];
+ }).map((frame) => modeEditorNormalizeFrame(frame, readable)).filter(Boolean);
+
+ const planMessages = [];
+ if (!frames.length) {
+ modeEditorPlanCommands(plan, operationRef).forEach((command) => {
+ const frame = modeEditorFrameFromPlan(command, readable);
+ if (!frame) return;
+ let special = null;
+ try {
+ special = JSON.parse(String(command?.signals?.special_parameters || "null"));
+ } catch (_error) {
+ special = null;
+ }
+ if (command?.signals?.special_handler === "driver_seat_memory_recall"
+ && Array.isArray(special?.frame_sequence)) {
+ const baseOffset = Math.max(0, Number(command?.send_offset_ms || 0));
+ const period = Math.max(0, Number(special.period_ms || 0));
+ special.frame_sequence.forEach((dataHex, index) => {
+ planMessages.push({
+ frame: { ...frame, data_hex: String(dataHex).toUpperCase() },
+ offset_ms: baseOffset + (index * period),
+ });
+ });
+ return;
+ }
+ const repeat = Math.max(1, Number(command?.repeat?.count || 1));
+ const period = Math.max(0, Number(command?.repeat?.period_ms || 0));
+ const baseOffset = Math.max(0, Number(command?.send_offset_ms || 0));
+ for (let index = 0; index < repeat; index += 1) {
+ planMessages.push({ frame, offset_ms: baseOffset + (index * period) });
+ }
+ });
+ } else {
+ frames = frames.map((frame, index) => ({ frame, offset_ms: index === 0 ? 0 : null }));
+ }
+
+ const candidates = frames.length ? frames : planMessages;
+ return candidates.map((item, index) => {
+ const base = {
+ sequence: index + 1,
+ offset_ms: item.offset_ms,
+ source: flow?.control_transport === "wireless_mqtt" ? "projected" : "can",
+ };
+ return flow?.control_transport === "wireless_mqtt"
+ ? { ...base, ...modeEditorMqttEnvelope(item.frame, readable.command) }
+ : { ...base, frame: item.frame };
+ });
+}
+
+function modeEditorReadableCommands(flow) {
+ if (!flow) return [];
+ const plans = flow.plans && typeof flow.plans === "object" ? flow.plans : {};
+ const configurations = flow.function_configs && typeof flow.function_configs === "object"
+ ? flow.function_configs
+ : {};
+ const converter = window.CognitiveAgentConsoleV2?.getParsedCanCommands;
+ const commands = [];
+
+ if (typeof converter === "function") {
+ Object.entries(configurations).forEach(([key, configuration]) => {
+ const plan = plans[key];
+ if (!plan) return;
+ (configuration?.spec?.operations || []).forEach((operation) => {
+ const singleConfig = {
+ ...configuration,
+ spec: { ...configuration.spec, operations: [operation] },
+ };
+ const readable = converter({
+ function_configs: { [key]: singleConfig },
+ command_plans: { [key]: plan },
+ })[0];
+ if (!readable) return;
+ commands.push({
+ ...readable,
+ operation_ref: String(operation.id || ""),
+ messages: modeEditorMessagesForOperation(
+ flow,
+ readable,
+ String(operation.id || ""),
+ plan,
+ ),
+ });
+ });
+ });
+ }
+
+ if (!commands.length) {
+ Object.values(plans).forEach((plan) => {
+ modeEditorPlanCommands(plan).forEach((item) => {
+ const debugCommand = String(item?.signals?.debug_command || item?.message || "CAN frame");
+ const channel = modeEditorCanId(item?.signals?.frame_id) === "0x6A0"
+ ? "CAN0"
+ : "CAN1";
+ const readable = { channel, category: modeEditorCommandCategory(debugCommand), command: debugCommand };
+ commands.push({
+ ...readable,
+ operation_ref: String(item?.operation_ref || ""),
+ messages: modeEditorMessagesForOperation(
+ flow,
+ readable,
+ String(item?.operation_ref || ""),
+ plan,
+ ),
+ });
+ });
+ });
+ }
+
+ const unique = new Map();
+ commands.forEach((item) => {
+ const key = `${item.channel}:${item.command}`;
+ if (!unique.has(key)) unique.set(key, item);
+ else unique.get(key).messages.push(...item.messages);
+ });
+ return [...unique.values()];
+}
+
+function modeEditorAppendCommandCard(preview, item, flow) {
+ const card = document.createElement("details");
+ card.className = "mode-editor-command-card";
+ card.dataset.canChannel = item.channel;
+ const summary = document.createElement("summary");
+ const meta = document.createElement("span");
+ meta.className = "mode-editor-command-meta";
+ const channel = document.createElement("b");
+ channel.textContent = item.channel;
+ const category = document.createElement("strong");
+ category.textContent = item.category;
+ meta.append(channel, category);
+ const command = document.createElement("code");
+ command.textContent = item.command;
+ const chevron = document.createElement("i");
+ chevron.dataset.lucide = "chevron-down";
+ summary.append(meta, command, chevron);
+ card.append(summary);
+
+ const sequence = document.createElement("section");
+ sequence.className = "mode-editor-message-sequence";
+ const header = document.createElement("header");
+ const title = document.createElement("span");
+ title.textContent = flow?.control_transport === "wireless_mqtt" ? "MQTT 消息序列" : "CAN 帧序列(未经过 MQTT)";
+ const messageCount = document.createElement("b");
+ messageCount.textContent = `${item.messages.length} 条`;
+ header.append(title, messageCount);
+ sequence.append(header);
+
+ if (!item.messages.length) {
+ const empty = document.createElement("p");
+ empty.textContent = "该指令当前没有可展示的已编译帧。";
+ sequence.append(empty);
+ } else {
+ const list = document.createElement("ol");
+ item.messages.forEach((message) => {
+ const row = document.createElement("li");
+ const rowHeader = document.createElement("div");
+ const sequenceLabel = document.createElement("span");
+ const offset = Number.isFinite(message.offset_ms) ? ` · +${message.offset_ms}ms` : "";
+ sequenceLabel.textContent = `#${message.sequence}${offset}`;
+ const topic = document.createElement("code");
+ topic.textContent = message.topic || `can${message.frame?.channel ?? "-"}`;
+ rowHeader.append(sequenceLabel, topic);
+ const payload = document.createElement("pre");
+ payload.textContent = JSON.stringify(
+ message.payload
+ ? { topic: message.topic, qos: message.qos, retain: message.retain, payload: message.payload }
+ : message.frame,
+ null,
+ 2,
+ );
+ row.append(rowHeader, payload);
+ list.append(row);
+ });
+ sequence.append(list);
+ }
+ card.append(sequence);
+ preview.append(card);
+}
+
+function renderModeEditorFlow() {
+ const editor = state.modeEditor;
+ const preview = $("#modeEditorFlowPreview");
+ const count = $("#modeEditorFlowCount");
+ const complete = $("#modeEditorComplete");
+ if (!preview || !count || !complete) return;
+ const commandCount = modeEditorCommandCount(editor?.flow);
+ const readableCommands = modeEditorReadableCommands(editor?.flow);
+ const instructionMatches = Boolean(
+ editor?.executedInstruction
+ && editor.executedInstruction === $("#modeEditorInstruction").value.trim(),
+ );
+ complete.disabled = !(commandCount > 0 && instructionMatches && editor?.sourceRequestId);
+ count.textContent = commandCount > 0
+ ? `${readableCommands.length || commandCount} 条指令`
+ : "尚未生成";
+ preview.classList.toggle("empty", commandCount === 0);
+ preview.replaceChildren();
+ if (commandCount > 0 && readableCommands.length) {
+ readableCommands.forEach((item) => modeEditorAppendCommandCard(preview, item, editor.flow));
+ refreshIcons();
+ } else {
+ const empty = document.createElement("p");
+ empty.textContent = commandCount > 0
+ ? "已生成 CAN 控制流,但当前指令没有可读映射。"
+ : "点击“执行”后,这里会显示由后端生成的 CAN 控制指令流。";
+ preview.append(empty);
+ }
+}
+
+function setModeEditorFlowExpanded(expanded) {
+ const backdrop = $("#modeEditorBackdrop");
+ const dialog = backdrop?.querySelector(".mode-editor");
+ const button = $("#modeEditorFlowExpand");
+ if (!backdrop || !dialog || !button) return;
+ const nextExpanded = Boolean(expanded);
+ backdrop.classList.toggle("flow-expanded", nextExpanded);
+ dialog.classList.toggle("flow-expanded", nextExpanded);
+ button.setAttribute("aria-expanded", String(nextExpanded));
+ button.setAttribute(
+ "aria-label",
+ nextExpanded ? "还原 CAN 控制指令区域" : "展开 CAN 控制指令区域",
+ );
+ button.title = nextExpanded ? "还原 CAN 控制指令区域" : "展开 CAN 控制指令区域";
+ button.innerHTML = `
`;
+ refreshIcons();
+}
+
+function toggleModeEditorFlowExpanded() {
+ const button = $("#modeEditorFlowExpand");
+ setModeEditorFlowExpanded(button?.getAttribute("aria-expanded") !== "true");
+}
+
+function openModeEditor(modeProfileId) {
+ const mode = modeProfiles().find((item) => item.id === modeProfileId);
+ const backdrop = $("#modeEditorBackdrop");
+ if (!mode || !backdrop) return;
+ const instruction = String(mode.instruction || `进入${mode.name}`).trim();
+ const flow = mode.command_flow && typeof mode.command_flow === "object"
+ ? mode.command_flow
+ : null;
+ state.modeEditor = {
+ modeId: mode.id,
+ sourceRequestId: String(flow?.source_request_id || ""),
+ executedInstruction: flow ? instruction : "",
+ flow,
+ };
+ $("#modeEditorName").value = mode.name || "";
+ $("#modeEditorInstruction").value = instruction;
+ $("#modeEditorStatus").textContent = flow
+ ? "已载入永久保存的自然语言和 CAN 指令流,可修改后重新执行。"
+ : "修改自然语言后需要重新执行,才能点击完成永久保存。";
+ setModeEditorFlowExpanded(false);
+ backdrop.hidden = false;
+ document.body.classList.add("mode-editor-open");
+ renderModeEditorFlow();
+ window.requestAnimationFrame(() => {
+ backdrop.classList.add("open");
+ $("#modeEditorName").focus();
+ });
+ refreshIcons();
+}
+
+function closeModeEditor({ restoreFocus = true } = {}) {
+ const backdrop = $("#modeEditorBackdrop");
+ const modeId = state.modeEditor?.modeId;
+ if (!backdrop || backdrop.hidden) return;
+ backdrop.classList.remove("open");
+ backdrop.hidden = true;
+ setModeEditorFlowExpanded(false);
+ document.body.classList.remove("mode-editor-open");
+ state.modeEditor = null;
+ if (restoreFocus && modeId) {
+ $(`[data-mode-edit="${CSS.escape(modeId)}"]`)?.focus();
+ }
+}
+
+function invalidateModeEditorFlow() {
+ if (!state.modeEditor) return;
+ const instruction = $("#modeEditorInstruction").value.trim();
+ const current = instruction === state.modeEditor.executedInstruction;
+ $("#modeEditorStatus").textContent = current && modeEditorCommandCount(state.modeEditor.flow) > 0
+ ? "当前自然语言已有 CAN 指令流,可点击完成永久保存。"
+ : "自然语言已修改,请重新点击执行生成 CAN 控制指令流。";
+ renderModeEditorFlow();
+}
+
+async function executeModeEditorInstruction() {
+ const editor = state.modeEditor;
+ const instruction = $("#modeEditorInstruction").value.trim();
+ if (!editor || !instruction) {
+ showToast("请输入自然语言指令");
+ $("#modeEditorInstruction").focus();
+ return;
+ }
+ const execute = $("#modeEditorExecute");
+ execute.disabled = true;
+ execute.classList.add("loading");
+ execute.innerHTML = '
执行中…';
+ $("#modeEditorComplete").disabled = true;
+ $("#modeEditorStatus").textContent = "正在解析自然语言并生成、执行 CAN 控制指令流…";
+ refreshIcons();
+ try {
+ const execution = await requestCommandAndWait(instruction);
+ const result = execution.result;
+ const flow = modeEditorFlowFromResult(result);
+ const commandCount = modeEditorCommandCount(flow);
+ if (!result || commandCount === 0) {
+ throw new Error(execution.error?.message || "未生成可保存的 CAN 控制指令流");
+ }
+ editor.flow = flow;
+ editor.sourceRequestId = result.request_id;
+ editor.executedInstruction = instruction;
+ $("#modeEditorStatus").textContent = `已生成 ${commandCount} 条 CAN 控制指令;执行状态:${result.status}。`;
+ setModeExecutionState(
+ editor.modeId,
+ ["succeeded", "partially_succeeded"].includes(result.status) ? "succeeded" : "failed",
+ );
+ renderModeEditorFlow();
+ } catch (error) {
+ editor.flow = null;
+ editor.sourceRequestId = "";
+ editor.executedInstruction = "";
+ $("#modeEditorStatus").textContent = error.message || "CAN 控制指令流生成失败。";
+ renderModeEditorFlow();
+ showToast(error.message || "CAN 控制指令流生成失败");
+ } finally {
+ execute.disabled = false;
+ execute.classList.remove("loading");
+ execute.innerHTML = '
执行';
+ refreshIcons();
+ }
+}
+
+async function completeModeEditor() {
+ const editor = state.modeEditor;
+ const name = $("#modeEditorName").value.trim();
+ const instruction = $("#modeEditorInstruction").value.trim();
+ if (!editor || !name) {
+ showToast("请输入模式名称");
+ $("#modeEditorName").focus();
+ return;
+ }
+ if (
+ !editor.sourceRequestId
+ || editor.executedInstruction !== instruction
+ || modeEditorCommandCount(editor.flow) === 0
+ ) {
+ showToast("请先执行当前自然语言并生成 CAN 控制指令流");
+ return;
+ }
+ const complete = $("#modeEditorComplete");
+ complete.disabled = true;
+ complete.classList.add("loading");
+ complete.innerHTML = '
保存中…';
+ refreshIcons();
+ try {
+ const response = await apiRequest(`/api/mode-profiles/${encodeURIComponent(editor.modeId)}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ name,
+ instruction,
+ source_request_id: editor.sourceRequestId,
+ vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
+ }),
+ });
+ const payload = await response.json();
+ if (response.status === 401) {
+ window.location.replace("/login");
+ return;
+ }
+ if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
+ await loadRuntime();
+ closeModeEditor({ restoreFocus: false });
+ showToast(`${payload.mode_profile?.name || name}已永久保存到模式卡片`);
+ } catch (error) {
+ $("#modeEditorStatus").textContent = error.message || "模式保存失败。";
+ showToast(error.message || "模式保存失败");
+ renderModeEditorFlow();
+ } finally {
+ complete.classList.remove("loading");
+ complete.innerHTML = '
完成';
+ refreshIcons();
+ }
+}
+
function populateModeChips() {
const group = $(".mode-chips");
if (!group) return;
@@ -4850,9 +5459,7 @@ function populateModeChips() {
labelNode.textContent = label;
button.dataset.modeProfileId = mode.id;
button.dataset.modeCommand = `进入${mode.name}`;
- button.dataset.modeOrigin = isUserModeProfile(mode)
- ? "user"
- : "preset";
+ button.dataset.modeOrigin = mode?.origin || "preset";
button.setAttribute("aria-pressed", "false");
const timeline = mode.timeline;
let cycleBadge = $(".mode-cycle-badge", button);
@@ -4940,7 +5547,11 @@ function populateModeChips() {
async function deleteUserModeProfile(modeProfileId) {
const mode = modeProfiles().find((item) => item.id === modeProfileId);
if (!mode || !isUserModeProfile(mode)) return;
- if (!window.confirm(`删除自定义功能模式“${mode.name}”?删除后无法恢复。`)) return;
+ const presetOverride = mode.preset_override === true;
+ const confirmation = presetOverride
+ ? `恢复“${mode.name}”的初始预设配置?当前保存的名称、自然语言和 CAN 指令流将被删除。`
+ : `删除自定义功能模式“${mode.name}”?删除后无法恢复。`;
+ if (!window.confirm(confirmation)) return;
const deleteButtons = $$(`[data-mode-delete="${CSS.escape(modeProfileId)}"]`);
deleteButtons.forEach((button) => {
@@ -4962,7 +5573,7 @@ async function deleteUserModeProfile(modeProfileId) {
}
if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`);
await loadRuntime();
- showToast(`${mode.name}已删除`);
+ showToast(presetOverride ? `${mode.name}已恢复初始预设` : `${mode.name}已删除`);
} catch (error) {
showToast(`删除失败:${error.message || "服务不可用"}`);
deleteButtons.forEach((button) => {
@@ -5141,27 +5752,9 @@ function applyRuntimeTargetPolicy(runtime) {
if (input && driverOnly) {
input.placeholder = "例如:打开主驾通风,并把空调温度调到24度";
}
- const safeExamples = [
- ["seat driver vent low", "主驾通风低档"],
- ["seat driver heat low", "主驾加热低档"],
- ["ac temp driver 24", "主驾空调 24℃"],
- ["window front-left position 0", "关闭左前车窗"],
- ["shade position 0", "关闭遮阳帘"],
- ["light reading all off", "关闭全部阅读灯"],
- ];
- if (driverOnly) $$(".example-command").forEach((button, index) => {
- const example = safeExamples[index];
- button.hidden = !example;
- if (!example) return;
- button.dataset.example = example[0];
- const label = $("span", button);
- if (label) label.textContent = example[1];
+ window.CognitiveAgentGesture?.setEnabled?.(!driverOnly, {
+ reason: driverOnly ? "CH1 主驾实车测试不启用无闭环的座椅位置手势" : "",
});
- const gestureButton = $("#gestureToggleButton");
- if (gestureButton && driverOnly) {
- gestureButton.disabled = true;
- gestureButton.title = "CH1 主驾实车测试不启用无闭环的座椅位置手势";
- }
refreshIcons();
}
@@ -5190,17 +5783,12 @@ async function loadRuntime() {
const linkedHardwareReady = seatHardwareReady && vehicleHardwareReady;
const ch1DriverOnly = vehicleHardwareReady && !seatHardwareReady;
const hardwareExecutionReady = Boolean(hardware.configured || vehicleCan.configured);
- const badge = $("#modelBadge");
const gptButton = $('[data-provider="openai"]');
if (state.runtime.openai_available) {
- badge.classList.remove("danger");
- badge.innerHTML = `
${state.runtime.openai_model}`;
const responsesEndpoint = state.runtime.openai_responses_endpoint || "Responses API";
$("#providerNote").textContent = `${state.runtime.openai_model} · ${responsesEndpoint} · 失败时自动降级本地规则`;
selectProvider("openai");
} else {
- badge.classList.add("danger");
- badge.innerHTML = '
GPT 未配置';
gptButton.disabled = true;
gptButton.title = "设置 OPENAI_API_KEY 后启用";
$("#providerNote").textContent = "未设置 OPENAI_API_KEY,已切换为本地规则解析";
@@ -5220,17 +5808,17 @@ async function loadRuntime() {
const concurrencyText = hardware.concurrency_policy === "single_flight_fail_fast"
? " · 动作防冲突"
: "";
- const accessText = !ch1DriverOnly
+ const accessText = !mqttDualChannel && !ch1DriverOnly
&& hardware.access_policy === "local_approval_idle_lease"
? ` · 本地审批 · 空闲 ${hardware.access_inactivity_timeout_seconds || 180} 秒自动停止`
: "";
const vehicleRouteText = mqttSelected
- ? `MQTT ${vehicleCan.host || "127.0.0.1"}:${vehicleCan.port || 1883} · ${vehicleCan.condition_topic || "n7/n7-headunit/can/state"} / ${vehicleCan.control_topic || "n7/can_control"}`
+ ? `MQTT ${vehicleCan.host || "127.0.0.1"}:${vehicleCan.port || 1883} · ${vehicleCan.condition_topic || "n7/n7-headunit/can/state"} / ${vehicleCan.control_topic || "n7/n7-headunit/can/command"}`
: `${vehicleCan.backend || "zlgcan"} · CH${vehicleCan.channel ?? 1} 车身 CAN FD ${Number(vehicleCan.arbitration?.bitrate || 500000) / 1000}k/${Number(vehicleCan.data?.bitrate || 2000000) / 1000000}M`;
$("#hardwareNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
? `${vehicleRouteText} · 仅主驾/车端白名单 · CH0/T2G/IAR 已禁用${accessText}${concurrencyText}`
: linkedHardwareReady
- ? `${mqttDualChannel ? "MQTT CAN0 副驾座椅 · MQTT CAN1 其余车辆" : `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s`} · ${vehicleRouteText} · ${profileText}${mqttDualChannel ? " · 远端自动烧录禁用" : ` · ${flashText}`}${accessText}${concurrencyText}`
+ ? `${mqttDualChannel ? "MQTT CAN0 副驾座椅 · MQTT CAN1 其余车辆" : `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s`} · ${vehicleRouteText}${mqttDualChannel ? " · 远端自动烧录禁用" : ` · ${profileText} · ${flashText}`}${accessText}${concurrencyText}`
: seatHardwareReady
? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · 车端 CAN 未启用,其他领域保持虚拟执行${accessText}${concurrencyText}`
: `真实执行已锁定:${profileText};当前${mqttSelected ? "无线 MQTT" : "有线 CAN"}控制未通过后端审批配置`;
@@ -5241,14 +5829,6 @@ async function loadRuntime() {
: seatHardwareReady
? `CH${hardware.channel} → 副驾座椅控制器 · 车身控制暂用虚拟 CAN`
: "尚未配置可执行的物理 CAN 通道";
- $("#hardwareBadge").className = `status-badge ${hardwareExecutionReady ? "" : "danger"}`.trim();
- $("#hardwareBadge").innerHTML = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
- ? `
${mqttSelected ? "MQTT" : "CH1"} 车端已配置`
- : linkedHardwareReady
- ? '
双通道已配置'
- : seatHardwareReady
- ? '
座椅台架已配置'
- : '
硬件锁定';
$("#hardwareCoverage").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
? `${mqttSelected ? "MQTT" : "CH1"} 主驾 + 车端设备 · CAN 字段回读${accessText}${concurrencyText}`
: linkedHardwareReady
@@ -5257,16 +5837,15 @@ async function loadRuntime() {
? `CH${hardware.channel} 座椅 · 物理反馈${accessText}${concurrencyText}`
: "已阻断";
$("#hardwareCoverage").className = hardwareExecutionReady ? "available" : "blocked";
- const modeAvailable = state.executionMode === "hardware"
+ const preferredMode = state.preferredExecutionMode || DEFAULT_EXECUTION_MODE;
+ const modeAvailable = preferredMode === "hardware"
? hardwareExecutionReady
- : state.executionMode === "simulated_hardware"
+ : preferredMode === "simulated_hardware"
? simulation.enabled
: true;
- selectExecutionMode(modeAvailable ? state.executionMode : "dry_run");
+ selectExecutionMode(modeAvailable ? preferredMode : "dry_run", { remember: false });
refreshIcons();
} catch (error) {
- $("#modelBadge").textContent = "运行时离线";
- $("#modelBadge").classList.add("danger");
$("#providerNote").textContent = "无法读取后端运行状态";
showToast(`运行时连接失败:${error.message}`);
}
@@ -5337,7 +5916,12 @@ async function synchronizeDriverSeatState() {
state.seatFeedbackSource = state.seatFeedbackSources.driver;
updateSeatFeedbackModeLabel();
return true;
- } catch (_error) {
+ } catch (error) {
+ state.vehicleStateSnapshot = {
+ ...(state.vehicleStateSnapshot || {}),
+ error: error.message || String(error),
+ };
+ renderCabinLiveStatus();
if (!["physical_can_ch1", "wireless_mqtt_ch1"].includes(state.seatFeedbackSources.driver)) {
state.seatFeedbackSources.driver = "unavailable";
updateSeatFeedbackModeLabel();
@@ -5394,7 +5978,8 @@ async function loadAuthStatus() {
window.location.replace("/login");
return;
}
- $("#logoutButton").hidden = !auth.enabled;
+ const logoutButton = $("#v2LogoutButton");
+ if (logoutButton) logoutButton.hidden = !auth.enabled;
}
async function logout() {
@@ -5408,39 +5993,6 @@ function setRunStatus(status, text) {
element.textContent = text;
}
-function resetPipeline() {
- $$(".pipeline-step").forEach((step) => step.classList.remove("active", "succeeded", "failed"));
-}
-
-function startProgress() {
- resetPipeline();
- const steps = $$(".pipeline-step");
- let index = 0;
- steps[0].classList.add("active");
- state.progressTimer = window.setInterval(() => {
- steps[index]?.classList.remove("active");
- steps[index]?.classList.add("succeeded");
- index = Math.min(index + 1, steps.length - 1);
- steps[index]?.classList.add("active");
- }, 360);
-}
-
-function renderPipeline(events) {
- window.clearInterval(state.progressTimer);
- resetPipeline();
- const byStage = new Map(events.map((event) => [event.stage, event]));
- const schemaEvent = byStage.get("schema_validate");
- const domainEvent = byStage.get("domain_validate");
- $$(".pipeline-step").forEach((step) => {
- let event = byStage.get(step.dataset.stage);
- if (step.dataset.stage === "domain_validate") {
- event = schemaEvent?.status === "failed" ? schemaEvent : domainEvent;
- }
- if (!event) return;
- step.classList.add(event.status === "failed" || event.status === "waiting_confirmation" ? "failed" : "succeeded");
- });
-}
-
function yamlScalar(value) {
if (value === null) return "null";
if (typeof value === "boolean" || typeof value === "number") return String(value);
@@ -5546,6 +6098,80 @@ function resetSeatState() {
});
}
+function resetVehicleStatusDisplay() {
+ state.seatActuatorState = {};
+ resetSeatState();
+ const setText = (selector, value) => {
+ const element = $(selector);
+ if (element) element.textContent = value;
+ };
+ $$('[data-domain-panel]').forEach((panel) => panel.classList.remove("active"));
+
+ setText("#climatePower", "UNKNOWN");
+ setText("#driverTemperature", "-- ℃");
+ setText("#passengerTemperature", "-- ℃");
+ setText("#driverAirDirection", "-");
+ setText("#passengerAirDirection", "-");
+ const airflow = $("#airflowVisual");
+ if (airflow) airflow.dataset.level = "0";
+ setText("#fanLevel", "未知");
+ setText("#cycleMode", "未知");
+ setText("#acState", "未知");
+ setText("#defrostState", "未知");
+
+ setText("#vehicleTelemetryState", "只读");
+ setText("#vehicleGear", "--");
+ setText("#vehicleSpeed", "-- km/h");
+ [
+ "frontLeftDoorState",
+ "frontRightDoorState",
+ "rearLeftDoorState",
+ "rearRightDoorState",
+ ].forEach((id) => setText(`#${id}`, "未知"));
+ $$('[data-door-state]').forEach((row) => { row.dataset.state = "unknown"; });
+
+ setText("#roofState", "UNKNOWN");
+ setText("#shadePositionValue", "未知");
+ setText("#shadeMotionState", "未知");
+ setText("#shadeInitializedState", "未知");
+ setText("#shadeThermalState", "未知");
+ const shadeMeter = $("#shadePositionMeter");
+ if (shadeMeter) shadeMeter.style.removeProperty("width");
+
+ setText("#ambientPower", "UNKNOWN");
+ setText("#ambientMode", "未知");
+ setText("#interiorLightPower", "未知");
+ setText("#ambientBrightness", "- / -");
+ setText("#ambientColorIndex", "- / -");
+ const ambientPanel = $('[data-domain-panel="ambient_light"]');
+ if (ambientPanel) {
+ ambientPanel.dataset.ambientPower = "unknown";
+ ambientPanel.dataset.interiorPower = "unknown";
+ }
+
+ setText("#windowState", "UNKNOWN");
+ ["frontLeftWindow", "frontRightWindow", "rearLeftWindow", "rearRightWindow"]
+ .forEach((id) => setText(`#${id}`, "未知"));
+ $$('[data-window-meter]').forEach((meter) => {
+ meter.dataset.state = "unknown";
+ meter.style.removeProperty("--window-open");
+ });
+
+ setText("#frontLightState", "UNKNOWN");
+ ["lowBeamState", "highBeamState", "fogLightState", "turnSignalState"]
+ .forEach((id) => setText(`#${id}`, "未知"));
+ $$('[data-light-meter]').forEach((meter) => { meter.dataset.state = "unknown"; });
+
+ setText("#cabinSummaryGear", "--");
+ setText("#cabinSummarySpeed", "-- km/h");
+ setText("#cabinSummaryDoors", "状态未知");
+ const summarySystem = $("#cabinSummarySystem");
+ if (summarySystem) {
+ summarySystem.textContent = "等待状态";
+ summarySystem.dataset.state = "waiting";
+ }
+}
+
function mergeSeatActuatorState(previousState, patchState) {
const mergedState = { ...(previousState || {}) };
Object.entries(patchState || {}).forEach(([seat, functions]) => {
@@ -5646,6 +6272,16 @@ function renderDomainState(domainState) {
if (!domainState) return;
const outletModeLabel = (value) => {
const normalized = String(value || "").toLowerCase().replaceAll("-", "_");
+ const matrixLabels = {
+ face: "吹面",
+ face_feet: "吹面 / 吹脚",
+ feet: "吹脚",
+ feet_defrost: "吹脚 / 除霜",
+ defrost: "除霜",
+ face_feet_defrost: "吹面 / 吹脚 / 除霜",
+ face_defrost: "吹面 / 除霜",
+ };
+ if (matrixLabels[normalized]) return matrixLabels[normalized];
if (["avoid_people", "avoid_person", "avoid_person_blow", "avoid_occupant"].includes(normalized)) {
return "避人吹";
}
@@ -5656,7 +6292,8 @@ function renderDomainState(domainState) {
};
const onOffLabel = (value) => {
if ([true, 1, "1", "on", "open", "opened", "active"].includes(value)) return "开启";
- return "关闭";
+ if ([false, 0, "0", "off", "close", "closed", "inactive"].includes(value)) return "关闭";
+ return "未知";
};
const percentageValue = (value) => {
const numeric = Number(value);
@@ -5664,32 +6301,57 @@ function renderDomainState(domainState) {
};
const climate = domainState.climate;
if (climate) {
- const active = climate.power === "on" || climate.ac === "on" || Number(climate.fan_level) > 0;
+ const powerLabel = onOffLabel(climate.power);
+ const acLabel = onOffLabel(climate.ac);
+ const defrostLabel = onOffLabel(climate.front_defrost);
+ const fanValue = Number(climate.fan_level);
+ const hasFanLevel = climate.fan_level !== null
+ && climate.fan_level !== undefined
+ && climate.fan_level !== ""
+ && Number.isFinite(fanValue);
+ const active = powerLabel === "开启" || acLabel === "开启" || (hasFanLevel && fanValue > 0);
const driverTemperature = climate.zones?.driver?.temperature_c
- ?? climate.zones?.vehicle?.temperature_c
- ?? 24;
+ ?? climate.zones?.vehicle?.temperature_c;
const passengerTemperature = climate.zones?.passenger?.temperature_c
- ?? climate.zones?.vehicle?.temperature_c
- ?? 24;
+ ?? climate.zones?.vehicle?.temperature_c;
+ const hasDriverTemperature = Number.isFinite(Number(driverTemperature));
+ const hasPassengerTemperature = Number.isFinite(Number(passengerTemperature));
$('[data-domain-panel="climate"]').classList.toggle("active", active);
- $("#climatePower").textContent = climate.power === "on" ? "ON" : (active ? "ACTIVE" : "OFF");
- $("#driverTemperature").innerHTML = `${driverTemperature}
℃`;
- $("#passengerTemperature").innerHTML = `${passengerTemperature}
℃`;
+ $("#climatePower").textContent = powerLabel === "开启"
+ ? "ON"
+ : powerLabel === "关闭" ? (active ? "ACTIVE" : "OFF") : active ? "ACTIVE" : "UNKNOWN";
+ $("#driverTemperature").innerHTML = hasDriverTemperature
+ ? `${driverTemperature}
℃`
+ : "--
℃";
+ $("#passengerTemperature").innerHTML = hasPassengerTemperature
+ ? `${passengerTemperature}
℃`
+ : "--
℃";
$("#driverAirDirection").textContent = outletModeLabel(climate.zones?.driver?.outlet_mode);
$("#passengerAirDirection").textContent = outletModeLabel(climate.zones?.passenger?.outlet_mode);
- $("#airflowVisual").dataset.level = String(Math.max(0, Math.min(7, Number(climate.fan_level) || 0)));
- $("#fanLevel").textContent = `${climate.fan_level ?? 0} 档`;
- $("#cycleMode").textContent = climate.cycle_mode === "internal" ? "内循环" : "外循环";
- $("#acState").textContent = climate.ac === "on" ? "开启" : "关闭";
- $("#defrostState").textContent = climate.front_defrost === "on" ? "开启" : "关闭";
+ $("#airflowVisual").dataset.level = String(
+ hasFanLevel ? Math.max(0, Math.min(7, fanValue)) : 0,
+ );
+ $("#fanLevel").textContent = hasFanLevel ? `${fanValue} 档` : "未知";
+ $("#cycleMode").textContent = {
+ internal: "内循环",
+ external: "外循环",
+ auto: "自动循环",
+ }[climate.cycle_mode] || "未知";
+ $("#acState").textContent = acLabel;
+ $("#defrostState").textContent = defrostLabel;
const vehicleSystems = state.seatTwin?.vehicleSystems;
if (vehicleSystems) {
- vehicleSystems.targets.climateLevel = Math.max(0, Math.min(7, Number(climate.fan_level) || 0));
- vehicleSystems.targets.climateTemperature = Number(
+ if (hasFanLevel) {
+ vehicleSystems.targets.climateLevel = Math.max(0, Math.min(7, fanValue));
+ }
+ const activeTemperature = Number(
selectedVehicleTransport()?.target_policy === "ch1_body_can_driver_only"
? driverTemperature
: passengerTemperature,
- ) || 24;
+ );
+ if (Number.isFinite(activeTemperature)) {
+ vehicleSystems.targets.climateTemperature = activeTemperature;
+ }
}
}
@@ -5762,11 +6424,14 @@ function renderDomainState(domainState) {
const row = panel?.querySelector(`[data-door-state="${name}"]`);
if (row) row.dataset.state = lockState;
});
- const hasPhysicalState = vehicleStatus?.feedback_source === "physical_can_ch1"
- || door?.feedback_source === "physical_can_ch1";
- panel?.classList.toggle("active", hasPhysicalState);
- $("#vehicleTelemetryState").textContent = hasPhysicalState
- ? (hasGear && hasSpeed && knownDoorCount === 4 ? "实时" : "部分")
+ const feedbackSource = vehicleStatus?.feedback_source || door?.feedback_source;
+ const hasAuthoritativeState = isAuthoritativeCh1Source(feedbackSource);
+ const wirelessState = feedbackSource === "wireless_mqtt_ch1";
+ panel?.classList.toggle("active", hasAuthoritativeState);
+ $("#vehicleTelemetryState").textContent = hasAuthoritativeState
+ ? (hasGear && hasSpeed && knownDoorCount === 4
+ ? (wirelessState ? "MQTT 实时" : "实时")
+ : (wirelessState ? "MQTT 部分" : "部分"))
: "只读";
const summaryGear = $("#cabinSummaryGear");
const summarySpeed = $("#cabinSummarySpeed");
@@ -5781,24 +6446,30 @@ function renderDomainState(domainState) {
: lockedDoorCount === 4 ? "四门已锁" : `${lockedDoorCount} 门已锁`;
if (summarySystem) {
const faulted = Boolean(vehicleStatus?.fault || door?.fault);
- summarySystem.textContent = faulted ? "需检查" : "无故障";
- summarySystem.dataset.state = faulted ? "warning" : "healthy";
+ summarySystem.textContent = faulted
+ ? "需检查"
+ : hasAuthoritativeState ? "CAN 实时" : "等待状态";
+ summarySystem.dataset.state = faulted
+ ? "warning"
+ : hasAuthoritativeState ? "healthy" : "waiting";
}
}
const ambient = domainState.ambient_light;
if (ambient) {
const active = ambient.power === "on";
- const indexedPhysicalColor = ambient.feedback_source === "physical_can_ch1"
+ const authoritativeAmbient = isAuthoritativeCh1Source(ambient.feedback_source);
+ const indexedPhysicalColor = authoritativeAmbient
&& Number.isInteger(ambient.front_color_index);
const color = indexedPhysicalColor
? "#D6DCE0"
+ : authoritativeAmbient ? "#B8C2C5"
: /^#[0-9A-Fa-f]{6}$/.test(ambient.color_hex || "")
? ambient.color_hex.toUpperCase()
: "#74D9FF";
const physicalBrightness = Number(ambient.front_brightness_level);
const brightness = Math.max(0, Math.min(100,
- Number.isFinite(physicalBrightness) && ambient.feedback_source === "physical_can_ch1"
+ Number.isFinite(physicalBrightness) && isAuthoritativeCh1Source(ambient.feedback_source)
? physicalBrightness * 20
: Number(ambient.brightness_pct) || 0,
));
@@ -5822,6 +6493,8 @@ function renderDomainState(domainState) {
});
$("#ambientMode").textContent = indexedPhysicalColor
? (active ? "开启(车端色号)" : "关闭")
+ : authoritativeAmbient
+ ? (active ? "开启(色号无效)" : ambient.power === "off" ? "关闭" : "状态未知")
: modeLabels[ambient.mode] || ambient.mode || "自定义";
$("#ambientBrightness").textContent = `${ambient.front_brightness_level ?? "-"} / ${ambient.rear_brightness_level ?? "-"}`;
$("#ambientColorIndex").textContent = `${ambient.front_color_index ?? "-"} / ${ambient.rear_color_index ?? "-"}`;
@@ -5850,34 +6523,48 @@ function renderDomainState(domainState) {
if (interiorLight) {
const lightPanel = $('[data-domain-panel="ambient_light"]');
lightPanel.dataset.interiorPower = interiorLight.power || "unknown";
- $("#interiorLightPower").textContent = interiorLight.power === "on" ? "开启" : "关闭";
+ $("#interiorLightPower").textContent = onOffLabel(interiorLight.power);
}
if (ambient || interiorLight) {
const lightPanel = $('[data-domain-panel="ambient_light"]');
const ambientOn = lightPanel.dataset.ambientPower === "on";
const interiorOn = lightPanel.dataset.interiorPower === "on";
lightPanel.classList.toggle("active", ambientOn || interiorOn);
+ const ambientKnown = ["on", "off"].includes(lightPanel.dataset.ambientPower);
+ const interiorKnown = ["on", "off"].includes(lightPanel.dataset.interiorPower);
$("#ambientPower").textContent = ambientOn
? "AMBIENT"
- : interiorOn ? "INTERIOR" : "OFF";
+ : interiorOn ? "INTERIOR" : ambientKnown && interiorKnown ? "OFF" : "UNKNOWN";
}
const windowState = domainState.window || domainState.windows;
if (windowState) {
- const readWindowPosition = (key) => {
+ const readWindowFeedback = (key) => {
const source = windowState.positions?.[key]
?? windowState.windows?.[key]
?? windowState[key];
- return percentageValue(source?.position_pct ?? source?.opening_pct ?? source?.position ?? source);
+ const rawPosition = source?.position_pct ?? source?.opening_pct ?? source?.position;
+ const numericPosition = Number(rawPosition);
+ return {
+ position: rawPosition !== null
+ && rawPosition !== undefined
+ && rawPosition !== ""
+ && Number.isFinite(numericPosition)
+ ? percentageValue(numericPosition)
+ : null,
+ closed: typeof source?.closed === "boolean" ? source.closed : null,
+ };
};
- const windowPositions = {
- frontLeftWindow: readWindowPosition("front_left"),
- frontRightWindow: readWindowPosition("front_right"),
- rearLeftWindow: readWindowPosition("rear_left"),
- rearRightWindow: readWindowPosition("rear_right"),
+ const windowFeedback = {
+ frontLeftWindow: readWindowFeedback("front_left"),
+ frontRightWindow: readWindowFeedback("front_right"),
+ rearLeftWindow: readWindowFeedback("rear_left"),
+ rearRightWindow: readWindowFeedback("rear_right"),
};
- Object.entries(windowPositions).forEach(([id, value]) => {
- $(`#${id}`).textContent = `${value}%`;
+ Object.entries(windowFeedback).forEach(([id, feedback]) => {
+ $(`#${id}`).textContent = feedback.position !== null
+ ? `${feedback.position}%`
+ : feedback.closed === true ? "已关闭" : feedback.closed === false ? "未关闭" : "未知";
const windowKey = {
frontLeftWindow: "front_left",
frontRightWindow: "front_right",
@@ -5885,21 +6572,44 @@ function renderDomainState(domainState) {
rearRightWindow: "rear_right",
}[id];
const meter = $(`[data-window-meter="${windowKey}"]`);
- if (meter) meter.style.setProperty("--window-open", `${value}%`);
+ if (meter) {
+ meter.dataset.state = feedback.closed === true
+ ? "closed"
+ : feedback.closed === false ? "open" : "unknown";
+ if (feedback.position !== null) {
+ meter.style.setProperty("--window-open", `${feedback.position}%`);
+ } else {
+ meter.style.removeProperty("--window-open");
+ }
+ }
});
- const anyWindowOpen = Object.values(windowPositions).some((value) => value > 0);
+ const knownWindowFeedback = Object.values(windowFeedback).filter(
+ (feedback) => feedback.position !== null || feedback.closed !== null,
+ );
+ const anyWindowOpen = knownWindowFeedback.some(
+ (feedback) => feedback.closed === false
+ || (feedback.position !== null && feedback.position > 0),
+ );
+ const allWindowsClosed = knownWindowFeedback.length === 4
+ && knownWindowFeedback.every(
+ (feedback) => feedback.closed === true || feedback.position === 0,
+ );
$('[data-domain-panel="window"]').classList.toggle("active", anyWindowOpen);
- $("#windowState").textContent = anyWindowOpen ? "OPEN" : "CLOSED";
+ $("#windowState").textContent = anyWindowOpen ? "OPEN" : allWindowsClosed ? "CLOSED" : "UNKNOWN";
const vehicleSystems = state.seatTwin?.vehicleSystems;
if (vehicleSystems) {
- Object.entries(windowPositions).forEach(([id, value]) => {
+ Object.entries(windowFeedback).forEach(([id, feedback]) => {
const windowKey = {
frontLeftWindow: "front_left",
frontRightWindow: "front_right",
rearLeftWindow: "rear_left",
rearRightWindow: "rear_right",
}[id];
- vehicleSystems.targets.windowPositions[windowKey] = value;
+ if (feedback.position !== null) {
+ vehicleSystems.targets.windowPositions[windowKey] = feedback.position;
+ } else if (feedback.closed === true) {
+ vehicleSystems.targets.windowPositions[windowKey] = 0;
+ }
});
}
}
@@ -5921,19 +6631,27 @@ function renderDomainState(domainState) {
turnSignalState: readLightState("turn_signal", "indicator"),
};
Object.entries(lightStates).forEach(([id, value]) => {
- $(`#${id}`).textContent = onOffLabel(value);
+ const label = onOffLabel(value);
+ $(`#${id}`).textContent = label;
const meter = $(`[data-light-meter="${id}"]`);
- if (meter) meter.dataset.state = onOffLabel(value) === "开启" ? "on" : "off";
+ if (meter) meter.dataset.state = label === "开启" ? "on" : label === "关闭" ? "off" : "unknown";
});
- const anyLightOn = Object.values(lightStates).some((value) => onOffLabel(value) === "开启");
+ const lightLabels = Object.values(lightStates).map(onOffLabel);
+ const anyLightOn = lightLabels.includes("开启");
+ const hasKnownLight = lightLabels.some((value) => value !== "未知");
$('[data-domain-panel="front_light"]').classList.toggle("active", anyLightOn);
- $("#frontLightState").textContent = anyLightOn ? "ON" : "OFF";
+ $("#frontLightState").textContent = anyLightOn ? "ON" : hasKnownLight ? "OFF" : "UNKNOWN";
const vehicleSystems = state.seatTwin?.vehicleSystems;
if (vehicleSystems) {
- vehicleSystems.targets.lowBeam = onOffLabel(lightStates.lowBeamState) === "开启" ? 1 : 0;
- vehicleSystems.targets.highBeam = onOffLabel(lightStates.highBeamState) === "开启" ? 1 : 0;
- vehicleSystems.targets.fogLight = onOffLabel(lightStates.fogLightState) === "开启" ? 1 : 0;
- vehicleSystems.targets.turnSignal = onOffLabel(lightStates.turnSignalState) === "开启" ? 1 : 0;
+ [
+ ["lowBeam", lightStates.lowBeamState],
+ ["highBeam", lightStates.highBeamState],
+ ["fogLight", lightStates.fogLightState],
+ ["turnSignal", lightStates.turnSignalState],
+ ].forEach(([target, value]) => {
+ const label = onOffLabel(value);
+ if (label !== "未知") vehicleSystems.targets[target] = label === "开启" ? 1 : 0;
+ });
}
}
}
@@ -5987,7 +6705,7 @@ const CONFIG_CAPABILITY_LABELS = {
"seat.set_heating": "设置座椅加热",
"seat.set_longitudinal": "调节座椅前后位置",
"seat.set_longitudinal_position": "调节座椅前后位置",
- "seat.set_cushion_tilt": "调节坐垫俯仰",
+ "seat.set_cushion_tilt": "调节座椅高度",
"seat.set_backrest_recline": "调节靠背角度",
"seat.set_legrest_height": "调节腿托角度",
"seat.set_legrest_extension": "调节腿托伸缩",
@@ -6305,6 +7023,14 @@ function renderExecution(result) {
const operationResults = report.spec.results.filter((item) => item.ref !== "seat-controller-firmware");
const succeededOperations = operationResults.filter((item) => item.status === "succeeded");
const failedOperations = operationResults.filter((item) => item.status === "failed");
+ const mqttPublishedOperations = operationResults.filter(
+ (item) => item.telemetry?.delivery_status === "published",
+ );
+ const physicallyConfirmedOperations = operationResults.filter(
+ (item) => item.telemetry?.physical_effect_confirmed === true,
+ );
+ const mqttPublishedUnconfirmed = result.control_transport === "wireless_mqtt"
+ && mqttPublishedOperations.length > physicallyConfirmedOperations.length;
const succeededDomains = [...new Set(succeededOperations.map((item) => item.domain))];
const failedDomains = [...new Set(failedOperations.map((item) => item.domain))];
let executionDetail = modeSave
@@ -6314,8 +7040,10 @@ function renderExecution(result) {
: simulatedHil
? `${executedDomains.join(" / ")} 已完成隔离 HIL 仿真,未访问物理 CAN`
: physical
- ? selectedVehicleTransport()?.target_policy === "ch1_body_can_driver_only"
- ? `${executedDomains.join(" / ")} 已通过${result.control_transport === "wireless_mqtt" ? " MQTT" : " CH1"}下发,并以车端状态字段确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
+ ? result.control_transport === "wireless_mqtt"
+ ? `${executedDomains.join(" / ")} 已通过 MQTT 发布 ${mqttPublishedOperations.length || succeededOperations.length} 个白名单控制步骤${mqttPublishedUnconfirmed ? ";车辆效果尚未确认,以状态话题回读为准" : `,并确认 ${physicallyConfirmedOperations.length || succeededOperations.length} 个车辆状态`}`
+ : selectedVehicleTransport()?.target_policy === "ch1_body_can_driver_only"
+ ? `${executedDomains.join(" / ")} 已通过 CH1 下发,并以车端状态字段确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
: `${executedDomains.join(" / ")} 已下发真实 CAN,并以 IAR 固件实际反馈确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
: `${executedDomains.join(" / ")} 共 ${report.spec.results.length} 个操作已完成 CAN 编码与虚拟执行`;
if (partiallySucceeded) {
@@ -6352,7 +7080,9 @@ function renderExecution(result) {
? modeSave ? "长期模式已保存"
: presetSave ? "姿态预设已保存"
: simulatedHil ? "模拟台架执行成功"
- : physical ? "真实台架执行成功" : "虚拟执行成功"
+ : physical
+ ? mqttPublishedUnconfirmed ? "MQTT 指令已发布" : "真实台架执行成功"
+ : "虚拟执行成功"
: partiallySucceeded ? (physical ? "真实台架部分执行成功" : "部分执行成功")
: simulatedHil ? "模拟故障已触发" : physical ? "真实台架执行失败" : "虚拟执行失败";
const executionIcon = reportSucceeded
@@ -6360,7 +7090,9 @@ function renderExecution(result) {
: partiallySucceeded ? "circle-alert" : "circle-x";
summary.innerHTML = `
${executionTitle}${executionDetail}
`;
$("#resultNavMeta").textContent = reportSucceeded
- ? `${report.spec.results.length} 项完成 · ${simulatedHil ? "SIM-HIL" : physical ? "HIL" : "DRY-RUN"}`
+ ? mqttPublishedUnconfirmed
+ ? `${mqttPublishedOperations.length} 项已发布 · MQTT`
+ : `${report.spec.results.length} 项完成 · ${simulatedHil ? "SIM-HIL" : physical ? "HIL" : "DRY-RUN"}`
: partiallySucceeded ? `${succeededOperations.length} 项成功 / ${failedOperations.length} 项失败 · 查看明细`
: "执行失败 · 查看回读";
$("#resultPanelState").textContent = reportSucceeded ? "SUCCEEDED" : partiallySucceeded ? "PARTIAL" : "FAILED";
@@ -6396,14 +7128,14 @@ function renderExecution(result) {
refreshIcons();
}
-function physicalCh1StatePatch(value) {
+function authoritativeCh1StatePatch(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
- if (value.source === "physical_can_ch1" || value.feedback_source === "physical_can_ch1") {
+ if (isAuthoritativeCh1Source(value.source || value.feedback_source)) {
return value;
}
const patch = {};
Object.entries(value).forEach(([key, item]) => {
- const child = physicalCh1StatePatch(item);
+ const child = authoritativeCh1StatePatch(item);
if (child) patch[key] = child;
});
return Object.keys(patch).length ? patch : null;
@@ -6427,20 +7159,49 @@ function isAuthoritativeVehicleStatePayload(payload) {
if (!payload || typeof payload !== "object") return false;
if (payload.conversation_id !== state.conversationId) return false;
return Boolean(
- physicalCh1StatePatch(payload.actuator_state)
- || physicalCh1StatePatch(payload.domain_state),
+ authoritativeCh1StatePatch(payload.actuator_state)
+ || authoritativeCh1StatePatch(payload.domain_state),
);
}
+function recordVehicleStateTelemetry(payload) {
+ if (!payload || payload.conversation_id !== state.conversationId) return;
+ const previous = state.vehicleStateSnapshot || {};
+ const source = payload.source === "wireless_mqtt_ch1"
+ || payload.control_transport === "wireless_mqtt"
+ ? "wireless_mqtt_ch1"
+ : "physical_can_ch1";
+ state.vehicleStateSnapshot = {
+ ...previous,
+ source,
+ capturedAt: payload.captured_at || null,
+ receivedAt: Date.now(),
+ captureSequence: Number.isFinite(Number(payload.capture_sequence))
+ ? Number(payload.capture_sequence)
+ : null,
+ observedFrameIds: Array.isArray(payload.observed_frame_ids)
+ ? payload.observed_frame_ids
+ : [],
+ bodyStateComplete: payload.body_state_complete === true,
+ bodyStateMissing: Array.isArray(payload.body_state_missing)
+ ? payload.body_state_missing
+ : [],
+ };
+ renderCabinLiveStatus();
+}
+
function applyAuthoritativeVehicleState(payload) {
- if (!isAuthoritativeVehicleStatePayload(payload)) return false;
+ if (!payload || payload.conversation_id !== state.conversationId) return false;
const turn = Number(payload.conversation_turn);
if (Number.isFinite(turn) && turn < state.vehicleStateTurn) return false;
+ recordVehicleStateTelemetry(payload);
+ if (!isAuthoritativeVehicleStatePayload(payload)) return false;
if (Number.isFinite(turn)) state.vehicleStateTurn = turn;
- const actuatorPatch = physicalCh1StatePatch(payload.actuator_state);
- const domainPatch = physicalCh1StatePatch(payload.domain_state);
+ const actuatorPatch = authoritativeCh1StatePatch(payload.actuator_state);
+ const domainPatch = authoritativeCh1StatePatch(payload.domain_state);
const previousActuatorState = state.vehicleStateSnapshot?.actuatorState || null;
state.vehicleStateSnapshot = {
+ ...state.vehicleStateSnapshot,
conversationId: payload.conversation_id,
conversationTurn: Number.isFinite(turn) ? turn : null,
requestId: payload.request_id || null,
@@ -6488,15 +7249,18 @@ function renderResult(result) {
state.result = result;
const reportMode = result.execution_report?.spec?.mode;
const feedbackSource = result.execution_report?.metadata?.labels?.seat_feedback;
+ const mqttPublishedUnconfirmed = result.control_transport === "wireless_mqtt"
+ && (result.execution_report?.spec?.results || []).some(
+ (item) => item.telemetry?.delivery_status === "published"
+ && item.telemetry?.physical_effect_confirmed !== true,
+ );
if (feedbackSource === "simulated_hardware") {
state.seatFeedbackSource = "simulated_hardware";
if (result.actuator_state?.passenger) state.seatFeedbackSources.passenger = "simulated_hardware";
if (result.actuator_state?.driver) state.seatFeedbackSources.driver = "simulated_hardware";
- } else if (feedbackSource === "physical_can_ch1"
+ } else if (isAuthoritativeCh1Source(feedbackSource)
&& isAuthoritativeVehicleStatePayload(result)) {
- state.seatFeedbackSource = result.control_transport === "wireless_mqtt"
- ? "wireless_mqtt_ch1"
- : "physical_can_ch1";
+ state.seatFeedbackSource = feedbackSource;
state.seatFeedbackSources.driver = state.seatFeedbackSource;
} else if (reportMode === "hil") {
state.seatFeedbackSource = result.execution_report?.spec?.physical_state?.complete
@@ -6526,7 +7290,9 @@ function renderResult(result) {
? "座椅姿态预设已保存"
: result.scene_config
? `${domains.length} 域场景闭环完成`
- : `${domainLabels[domains[0]] || "车舱"}单域闭环完成`
+ : mqttPublishedUnconfirmed
+ ? `${domainLabels[domains[0]] || "车舱"}控制指令已发布`
+ : `${domainLabels[domains[0]] || "车舱"}单域闭环完成`
: "请求未执行";
const conversationLabel = result.conversation_id?.slice(-8) || "no-context";
$("#requestMeta").textContent = `${result.request_id} / context:${conversationLabel} / turn:${result.conversation_turn} / ${result.provider_used || result.provider_requested}`;
@@ -6538,7 +7304,6 @@ function renderResult(result) {
partiallySucceeded ? "partial" : executionSucceeded ? "succeeded" : "failed",
result.status.toUpperCase(),
);
- renderPipeline(result.trace_events || []);
renderDecision(result);
renderTrace(result.trace_events || []);
renderValidation(result.validation || []);
@@ -6594,7 +7359,6 @@ async function submitRequest(event) {
refreshIcons();
setRunStatus("processing", "PROCESSING");
$("#resultTitle").textContent = "正在执行车舱编排";
- startProgress();
try {
const response = await apiRequest("/api/requests", {
method: "POST",
@@ -6655,8 +7419,6 @@ async function submitRequest(event) {
} catch (error) {
completedError = error;
if (submittedModeProfileId) setModeExecutionState(submittedModeProfileId, "failed");
- window.clearInterval(state.progressTimer);
- resetPipeline();
if (error.code === "hardware_access_required") {
setRunStatus("failed", "ACCESS REQUIRED");
$("#resultTitle").textContent = "真实台架使用权无效";
@@ -6780,6 +7542,7 @@ window.CognitiveAgentConsole = {
notify(message) {
showToast(String(message));
},
+ logout,
};
window.addEventListener("cognitive-agent:connection", (event) => {
@@ -6795,6 +7558,7 @@ window.addEventListener("cognitive-agent:connection", (event) => {
document.addEventListener("DOMContentLoaded", async () => {
initJointTwinLayout();
+ resetVehicleStatusDisplay();
initResultWorkspace();
refreshIcons();
window.requestAnimationFrame(initSeatTwin);
@@ -6806,7 +7570,6 @@ document.addEventListener("DOMContentLoaded", async () => {
}
});
$("#newConversation").addEventListener("click", startNewConversation);
- $("#logoutButton").addEventListener("click", logout);
$("#vehicleSelect").addEventListener("change", (event) => {
selectVehicle(event.target.value);
});
@@ -6832,6 +7595,13 @@ document.addEventListener("DOMContentLoaded", async () => {
});
$("#modeGalleryGroups").addEventListener("click", (event) => {
const target = event.target instanceof Element ? event.target : null;
+ const editButton = target?.closest("button[data-mode-edit]");
+ if (editButton) {
+ event.preventDefault();
+ event.stopPropagation();
+ openModeEditor(editButton.dataset.modeEdit);
+ return;
+ }
const deleteButton = target?.closest("button[data-mode-delete]");
if (deleteButton) {
event.preventDefault();
@@ -6845,8 +7615,24 @@ document.addEventListener("DOMContentLoaded", async () => {
button.dataset.modeProfileId || null,
);
});
+ $("#modeEditorClose").addEventListener("click", () => closeModeEditor());
+ $("#modeEditorCancel").addEventListener("click", () => closeModeEditor());
+ $("#modeEditorExecute").addEventListener("click", executeModeEditorInstruction);
+ $("#modeEditorComplete").addEventListener("click", completeModeEditor);
+ $("#modeEditorFlowExpand").addEventListener("click", toggleModeEditorFlowExpanded);
+ $("#modeEditorInstruction").addEventListener("input", invalidateModeEditorFlow);
+ $("#modeEditorBackdrop").addEventListener("click", (event) => {
+ if (event.target === event.currentTarget) closeModeEditor();
+ });
document.addEventListener("keydown", (event) => {
- if (event.key === "Escape" && !$("#modeGalleryBackdrop").hidden) {
+ if (event.key === "Escape" && !$("#modeEditorBackdrop").hidden) {
+ event.preventDefault();
+ if ($("#modeEditorFlowExpand").getAttribute("aria-expanded") === "true") {
+ setModeEditorFlowExpanded(false);
+ } else {
+ closeModeEditor();
+ }
+ } else if (event.key === "Escape" && !$("#modeGalleryBackdrop").hidden) {
event.preventDefault();
closeModeGallery();
}
@@ -6873,9 +7659,6 @@ document.addEventListener("DOMContentLoaded", async () => {
restartPhysicalStatePolling();
await synchronizePhysicalSeatState();
}));
- $$(".example-command").forEach((button) => button.addEventListener("click", () => {
- $("#utterance").value = button.dataset.example;
- }));
$$(".tab").forEach((tab) => tab.addEventListener("click", () => activateTab(tab.dataset.tab)));
$$('[data-copy]').forEach((button) => button.addEventListener("click", () => copyArtifact(button.dataset.copy)));
$$('[data-download]').forEach((button) => button.addEventListener("click", () => downloadArtifact(button.dataset.download)));
diff --git a/apps/web-console/console-v2.css b/apps/web-console/console-v2.css
index aa56e4d..f9727d2 100644
--- a/apps/web-console/console-v2.css
+++ b/apps/web-console/console-v2.css
@@ -1,6 +1,5 @@
-/* Desktop console refactor --------------------------------------------------
- This layer deliberately overrides the original console without changing the
- tested control, gesture, hardware-safety, and digital-twin DOM contracts. */
+/* N7 web console ------------------------------------------------------------
+ Only the active remote-control layout lives here. */
.console-v2 {
--v2-accent: #0e7773;
@@ -16,10 +15,6 @@
--v2-shadow: 0 18px 50px rgba(35, 71, 74, 0.1);
--v2-module-title-block: clamp(12px, 2vh, 22px);
--v2-module-title-inline: clamp(20px, 2.3vw, 34px);
- --v2-sidebar-item-height: 52px;
- --v2-sidebar-card-height: 43px;
- --v2-sidebar-item-radius: 13px;
- --v2-sidebar-card-radius: 10px;
min-width: 1180px;
min-height: 620px;
overflow: hidden;
@@ -28,6 +23,77 @@
font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
}
+.console-v2 .v2-boot-screen {
+ position: fixed;
+ inset: 0;
+ z-index: 10000;
+ display: grid;
+ place-content: center;
+ justify-items: center;
+ gap: 10px;
+ color: #16413f;
+ background:
+ radial-gradient(circle at 50% 42%, rgba(80, 190, 179, 0.18), transparent 18rem),
+ #f4f7f6;
+ opacity: 1;
+ visibility: visible;
+ transition: opacity 160ms ease, visibility 160ms ease;
+}
+
+.console-v2.v2-ready .v2-boot-screen {
+ pointer-events: none;
+ opacity: 0;
+ visibility: hidden;
+}
+
+.console-v2 .v2-boot-screen strong {
+ font-size: 20px;
+ font-weight: 900;
+ letter-spacing: -0.03em;
+}
+
+.console-v2 .v2-boot-screen small {
+ color: #748886;
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.console-v2 .v2-boot-mark {
+ width: 48px;
+ height: 48px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ margin-bottom: 4px;
+ border-radius: 15px;
+ background: linear-gradient(145deg, var(--v2-accent), var(--v2-accent-strong));
+ box-shadow: 0 12px 28px rgba(14, 119, 115, 0.2);
+}
+
+.console-v2 .v2-boot-mark i {
+ width: 4px;
+ height: 4px;
+ border-radius: 999px;
+ background: #fff;
+ animation: v2-boot-pulse 900ms ease-in-out infinite alternate;
+}
+
+.console-v2 .v2-boot-mark i:nth-child(2) {
+ animation-delay: 150ms;
+}
+
+.console-v2 .v2-boot-mark i:nth-child(3) {
+ animation-delay: 300ms;
+}
+
+@keyframes v2-boot-pulse {
+ to {
+ height: 18px;
+ border-radius: 4px;
+ }
+}
+
.console-v2 * {
scrollbar-color: var(--v2-accent) rgba(133, 151, 151, 0.14);
scrollbar-width: thin;
@@ -50,15 +116,6 @@
background-clip: padding-box;
}
-.console-v2 .v2-legacy-hidden,
-.console-v2 .topbar,
-.console-v2 .example-group,
-.console-v2 #safetyStrip,
-.console-v2 .gesture-backdrop,
-.console-v2 .pipeline {
- display: none !important;
-}
-
.console-v2 .workspace {
width: 100vw;
height: 100dvh;
@@ -69,10 +126,12 @@
opacity: 0;
background: var(--v2-canvas);
transition: opacity 180ms ease, grid-template-columns 180ms ease;
+ visibility: hidden;
}
.console-v2.v2-ready .workspace {
opacity: 1;
+ visibility: visible;
}
.console-v2.v2-sidebar-collapsed .workspace {
@@ -96,88 +155,6 @@
border-right: 1px solid var(--v2-line);
}
-.v2-profile {
- display: grid;
- grid-template-columns: 56px minmax(0, 1fr) 30px;
- align-items: center;
- gap: 14px;
- min-height: 66px;
-}
-
-.v2-avatar {
- position: relative;
- width: 54px;
- height: 54px;
- display: grid;
- place-items: center;
- overflow: hidden;
- background:
- radial-gradient(circle at 28% 25%, rgba(255, 255, 255, 0.95), transparent 18%),
- linear-gradient(135deg, #0b4d4b 3%, #70b7b1 49%, #d1efeb 52%, #276d69 100%);
- border: 2px solid color-mix(in srgb, var(--v2-accent) 82%, #051617);
- border-radius: 50%;
- box-shadow: inset -10px -8px 18px rgba(0, 32, 33, 0.24), 0 8px 18px rgba(19, 74, 74, 0.14);
-}
-
-.v2-avatar::after {
- content: "";
- width: 74px;
- height: 13px;
- position: absolute;
- transform: rotate(-42deg);
- background: rgba(255, 255, 255, 0.26);
- filter: blur(4px);
-}
-
-.v2-profile-copy {
- min-width: 0;
-}
-
-.v2-profile-copy > small {
- display: block;
- margin-bottom: 4px;
- color: color-mix(in srgb, var(--v2-accent) 62%, #506366);
- font-size: 8px;
- font-weight: 900;
- letter-spacing: 0.14em;
-}
-
-.v2-role-select {
- min-width: 0;
- display: flex;
- align-items: center;
- gap: 5px;
- cursor: pointer;
-}
-
-.v2-role-select > span,
-.v2-role-select > strong,
-.v2-role-select select,
-.v2-role-select .v2-dropdown-trigger {
- color: #091c1e;
- font-size: clamp(20px, 1.6vw, 26px);
- font-weight: 900;
- letter-spacing: -0.04em;
-}
-
-.v2-role-select select {
- max-width: 118px;
- padding: 0 17px 0 0;
- appearance: none;
- background: transparent;
- border: 0;
- outline: 0;
- cursor: pointer;
-}
-
-.v2-role-select > svg {
- width: 16px;
- height: 16px;
- margin-left: -20px;
- color: var(--v2-accent);
- pointer-events: none;
-}
-
.v2-sidebar-toggle {
width: 29px;
height: 29px;
@@ -207,21 +184,6 @@
padding: 15px 9px;
}
-.console-v2.v2-sidebar-collapsed .v2-profile {
- width: 100%;
- min-height: 104px;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 12px;
-}
-
-.console-v2.v2-sidebar-collapsed .v2-avatar {
- width: 44px;
- height: 44px;
-}
-
-.console-v2.v2-sidebar-collapsed .v2-profile-copy,
.console-v2.v2-sidebar-collapsed .v2-sidebar-section,
.console-v2.v2-sidebar-collapsed .v2-sidebar-footer {
display: none;
@@ -231,10 +193,6 @@
min-width: 0;
}
-.v2-control-section {
- margin-top: clamp(22px, 3.5vh, 42px);
-}
-
.v2-section-heading span,
.v2-operation-heading .section-kicker,
.v2-output-heading small {
@@ -252,75 +210,6 @@
letter-spacing: -0.04em;
}
-.v2-control-section > .v2-field-caption,
-.v2-config-section > .v2-field-caption {
- display: block;
- margin: 0 0 8px;
- color: #788888;
- font-size: 13px;
- font-weight: 750;
-}
-
-.console-v2 .v2-config-field > .field-label,
-.console-v2 .v2-config-field > label.field-label {
- display: block;
- margin: 0 0 6px;
- color: #8a9999;
- font-size: 10px;
- font-weight: 700;
- letter-spacing: 0.02em;
-}
-
-.v2-control-switch {
- display: grid;
- grid-template-columns: 1fr;
- gap: 7px;
-}
-
-.v2-control-switch button {
- min-height: var(--v2-sidebar-item-height);
- display: grid;
- grid-template-columns: 30px 1fr;
- align-items: center;
- gap: 8px;
- padding: 0 15px;
- color: #637677;
- background: transparent;
- border: 1px solid transparent;
- border-radius: 13px;
- text-align: left;
- font-size: 15px;
- font-weight: 700;
- cursor: pointer;
- transition: 160ms ease;
-}
-
-.v2-control-switch button svg {
- width: 19px;
- height: 19px;
-}
-
-.v2-control-switch button:hover {
- color: var(--v2-accent-strong);
- background: color-mix(in srgb, var(--v2-accent-soft) 34%, white);
- border-color: color-mix(in srgb, var(--v2-accent) 10%, white);
-}
-
-.v2-control-switch button.active {
- color: #111f20;
- background: rgba(255, 255, 255, 0.94);
- border-color: color-mix(in srgb, var(--v2-accent) 18%, white);
- box-shadow: 0 10px 28px color-mix(in srgb, var(--v2-accent) 10%, transparent);
-}
-
-.v2-control-switch button.active svg {
- color: var(--v2-accent);
-}
-
-.v2-config-section {
- margin-top: clamp(18px, 2.5vh, 28px);
-}
-
.v2-divider {
height: 1px;
margin-bottom: 15px;
@@ -328,25 +217,13 @@
opacity: 0.32;
}
-.v2-config-stack {
- display: grid;
- gap: 8px;
- padding: 0 0 0 9px;
- border-left: 1px solid color-mix(in srgb, var(--v2-accent) 14%, transparent);
-}
-
-.console-v2 .v2-config-field {
- min-width: 0;
- margin: 0;
-}
-
-.console-v2 .v2-config-field > .provider-note,
-.console-v2 .v2-config-card > .provider-note,
-.console-v2 .v2-config-card > .v2-gesture-model-note {
- display: none;
-}
-
.console-v2 .v2-transport-field {
+ width: 100%;
+ min-width: 0;
+ max-width: 100%;
+ margin: 0;
+ overflow: hidden;
+ box-sizing: border-box;
padding: 8px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(255, 255, 255, 0.78);
@@ -354,10 +231,18 @@
}
.console-v2 .v2-transport-field > .field-label {
+ display: block;
+ margin-top: 0;
margin-bottom: 6px;
+ color: #8a9999;
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.02em;
}
.console-v2 .v2-transport-field .vehicle-transport-options {
+ width: 100%;
+ min-width: 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 5px;
@@ -377,12 +262,16 @@
border-radius: 8px;
font-size: 10px;
font-weight: 850;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
cursor: pointer;
}
.console-v2 .v2-transport-field .mode-option svg {
width: 14px;
height: 14px;
+ flex: 0 0 auto;
}
.console-v2 .v2-transport-field .mode-option.active {
@@ -393,332 +282,16 @@
}
.console-v2 .v2-transport-field > #vehicleTransportNote {
- display: block;
+ display: -webkit-box;
margin: 6px 1px 0;
overflow: hidden;
color: #809091;
font-size: 8px;
line-height: 1.35;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.console-v2 .v2-config-card {
- min-width: 0;
- min-height: var(--v2-sidebar-card-height);
- display: grid;
- grid-template-columns: 28px minmax(0, 1fr) auto;
- align-items: center;
- gap: 8px;
- padding: 6px 8px;
- background: rgba(255, 255, 255, 0.72);
- border: 1px solid rgba(255, 255, 255, 0.78);
- border-radius: 10px;
-}
-
-.console-v2 .v2-config-card > svg {
- width: 17px;
- height: 17px;
- color: var(--v2-accent);
-}
-
-.v2-config-card-copy,
-.v2-config-card > div {
- min-width: 0;
-}
-
-.v2-config-card small,
-.v2-config-card strong {
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.v2-config-card small {
- color: #819091;
- font-size: 8px;
-}
-
-.v2-config-card strong {
- margin-top: 2px;
- color: #263d3f;
- font-size: 11px;
-}
-
-.v2-config-card-select {
- min-width: 0;
- position: relative;
-}
-
-.v2-config-card-select select {
- width: 100%;
- height: 18px;
- padding: 0;
- appearance: none;
- color: #263d3f;
- background: transparent;
- border: 0;
- outline: 0;
- font-size: 11px;
- font-weight: 850;
- cursor: pointer;
-}
-
-/* Custom popover menus keep the original closed-field styling intact. */
-
-.v2-custom-select {
- position: relative;
- overflow: visible;
-}
-
-.v2-custom-select.v2-dropdown-open {
- z-index: 80;
-}
-
-.v2-native-select {
- width: 1px !important;
- height: 1px !important;
- position: absolute !important;
- inset: auto !important;
- overflow: hidden !important;
- padding: 0 !important;
- clip-path: inset(50%) !important;
- opacity: 0 !important;
- pointer-events: none !important;
-}
-
-.v2-dropdown-trigger {
- min-width: 0;
- display: inline-flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- padding: 0;
- color: inherit;
- background: transparent;
- border: 0;
- outline: 0;
- cursor: pointer;
-}
-
-.v2-config-card-select > .v2-dropdown-trigger {
- width: 100%;
- height: 18px;
- font-size: 11px;
- font-weight: 850;
- text-align: left;
-}
-
-.v2-role-select > .v2-dropdown-trigger {
- width: auto;
- max-width: 118px;
-}
-
-.v2-role-select > .v2-dropdown-trigger:focus-visible {
- box-shadow: none;
-}
-
-.v2-dropdown-value {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.v2-dropdown-caret {
- width: 0;
- height: 0;
- flex: 0 0 auto;
- border-right: 4px solid transparent;
- border-left: 4px solid transparent;
- border-top: 6px solid var(--v2-accent-strong);
- transition: transform 150ms ease;
-}
-
-.v2-dropdown-open .v2-dropdown-caret {
- transform: rotate(180deg);
-}
-
-.v2-dropdown-trigger:focus-visible {
- border-radius: 6px;
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--v2-accent) 16%, transparent);
-}
-
-.v2-dropdown-menu {
- min-width: 100%;
- position: absolute;
- top: calc(100% + 7px);
- right: 0;
- left: 0;
- z-index: 100;
- display: grid;
- gap: 3px;
- padding: 6px;
- overflow: hidden;
- background: rgba(255, 255, 255, 0.98);
- border: 1px solid color-mix(in srgb, var(--v2-accent) 12%, #eef3f2);
- border-radius: 13px;
- box-shadow: 0 16px 34px rgba(27, 67, 69, 0.18);
- backdrop-filter: blur(14px);
-}
-
-.v2-dropdown-menu[hidden] {
- display: none;
-}
-
-.v2-role-select .v2-dropdown-menu {
- width: 126px;
- min-width: 126px;
- right: auto;
- left: 24px;
-}
-
-.v2-dropdown-option {
- width: 100%;
- min-height: 34px;
- display: grid;
- grid-template-columns: minmax(0, 1fr) 14px;
- align-items: center;
- gap: 8px;
- padding: 7px 10px;
- color: #405456;
- background: transparent;
- border: 0;
- border-radius: 8px;
- text-align: left;
- font-size: 11px;
- font-weight: 800;
- cursor: pointer;
- transition: 130ms ease;
-}
-
-.v2-role-select .v2-dropdown-option {
- font-size: 13px;
-}
-
-.v2-dropdown-option:hover,
-.v2-dropdown-option:focus-visible {
- color: var(--v2-accent-strong);
- background: color-mix(in srgb, var(--v2-accent-soft) 66%, white);
- outline: 0;
-}
-
-.v2-dropdown-option[aria-selected="true"] {
- color: var(--v2-accent-strong);
- background: color-mix(in srgb, var(--v2-accent-soft) 78%, white);
-}
-
-.v2-dropdown-option[aria-selected="true"]::after {
- content: "✓";
- justify-self: end;
- color: var(--v2-accent);
- font-size: 12px;
- font-weight: 950;
-}
-
-.v2-dropdown-option:disabled {
- color: #a9b3b3;
- background: transparent;
- cursor: not-allowed;
- opacity: 0.72;
-}
-
-.v2-field-state {
- padding: 3px 5px;
- color: var(--v2-accent-strong);
- background: color-mix(in srgb, var(--v2-accent-soft) 68%, white);
- border-radius: 5px;
- font: 800 7px "SFMono-Regular", Consolas, monospace;
-}
-
-.v2-config-card.unavailable {
- color: #9b5f32;
-}
-
-.v2-config-card.unavailable > svg,
-.v2-config-card.unavailable > .v2-field-state {
- color: #9b5f32;
-}
-
-.v2-config-card[data-state="offline"] > svg,
-.v2-config-card[data-state="offline"] > .v2-field-state {
- color: #a85f43;
-}
-
-.v2-config-card[data-state="offline"] > .v2-field-state {
- background: #fff0e9;
-}
-
-.console-v2 .v2-execution-field {
- display: none;
-}
-
-.v2-user-target-status {
- min-height: 33px;
- display: none;
- grid-template-columns: 8px minmax(0, 1fr);
- align-items: center;
- gap: 8px;
- padding: 7px 11px;
- color: #516f70;
- background: rgba(255, 255, 255, 0.48);
- border: 1px solid rgba(255, 255, 255, 0.72);
- border-radius: 9px;
- font-size: 9px;
- font-weight: 800;
-}
-
-.console-v2 .v2-user-target-status {
- display: grid;
-}
-
-.v2-user-target-status > i {
- width: 7px;
- height: 7px;
- background: #d69549;
- border-radius: 50%;
- box-shadow: 0 0 0 4px rgba(214, 149, 73, 0.12);
-}
-
-.v2-user-target-status[data-state="ready"] > i {
- background: #18a493;
- box-shadow: 0 0 0 4px rgba(24, 164, 147, 0.12);
-}
-
-.console-v2 .v2-provider-field,
-.console-v2 .v2-native-execution-field {
- display: none !important;
-}
-
-.v2-advanced-stack {
- display: grid;
- gap: 8px;
-}
-
-.console-v2 .v2-advanced-field:not([hidden]) {
- display: block;
- padding: 10px;
- background: rgba(255, 255, 255, 0.58);
- border: 1px solid rgba(255, 255, 255, 0.74);
- border-radius: 10px;
-}
-
-.console-v2 .v2-advanced-field input,
-.console-v2 .v2-advanced-field select {
- width: 100%;
- min-height: 36px;
- margin-top: 4px;
- padding: 0 9px;
- background: rgba(255, 255, 255, 0.94);
- border: 1px solid var(--v2-line);
- border-radius: 7px;
- font-size: 10px;
-}
-
-.console-v2 .v2-advanced-field button {
- min-height: 33px;
- border-radius: 7px;
+ overflow-wrap: anywhere;
+ white-space: normal;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
}
.v2-sidebar-footer {
@@ -1264,8 +837,7 @@
}
.console-v2[data-control-mode="gesture"] .v2-natural-panel,
-.console-v2:not([data-control-mode="gesture"]) .v2-gesture-panel,
-.console-v2:not([data-control-mode="gesture"]) .v2-gesture-model-field {
+.console-v2:not([data-control-mode="gesture"]) .v2-gesture-panel {
display: none;
}
@@ -2460,33 +2032,10 @@
padding-bottom: 10px;
}
- .v2-profile {
- grid-template-columns: 50px minmax(0, 1fr) 28px;
- gap: 10px;
- min-height: 52px;
- }
-
- .v2-role-select > span,
- .v2-role-select > strong,
- .v2-role-select select,
- .v2-role-select .v2-dropdown-trigger {
- font-size: 20px;
- }
-
- .v2-avatar {
- width: 48px;
- height: 48px;
- }
-
- .v2-profile-copy > small,
.v2-section-heading span {
display: none;
}
- .v2-control-section {
- margin-top: 14px;
- }
-
.console-v2[data-module="self-extension"] .v2-extension-sidebar {
margin-top: 14px;
}
@@ -2496,81 +2045,32 @@
font-size: 22px;
}
- .v2-control-section > .v2-field-caption,
- .v2-config-section > .v2-field-caption,
- .v2-calibration-sidebar > .v2-field-caption,
- .v2-extension-sidebar > .v2-field-caption,
- .v2-repair-sidebar > .v2-field-caption {
+ .v2-extension-sidebar > .v2-field-caption {
margin-bottom: 5px;
font-size: 11px;
}
- .console-v2 .v2-config-field > .field-label,
- .console-v2 .v2-config-field > label.field-label {
- margin-bottom: 4px;
- font-size: 9px;
- }
-
- .v2-control-switch {
- grid-template-columns: 1fr;
- }
-
- .v2-control-switch button,
- .v2-calibration-mode-switch button,
- .v2-extension-mode-switch button,
- .v2-repair-view-switch button {
+ .v2-extension-mode-switch button {
min-height: 41px;
grid-template-columns: 21px minmax(0, 1fr);
padding: 0 9px;
font-size: 10px;
}
- .v2-control-switch button svg,
- .v2-calibration-mode-switch button svg,
- .v2-extension-mode-switch button svg,
- .v2-repair-view-switch button > svg {
+ .v2-extension-mode-switch button svg {
width: 17px;
height: 17px;
}
- .v2-repair-view-switch button {
- min-height: 48px;
- padding-block: 4px;
- }
-
- .v2-repair-view-switch strong {
- font-size: 10px;
- }
-
- .v2-repair-view-switch small {
- font-size: 8px;
- }
-
- .v2-config-section {
- margin-top: 12px;
- }
-
.v2-divider {
margin-bottom: 9px;
}
- .v2-config-stack {
- gap: 8px;
- padding-left: 8px;
- }
-
- .console-v2 .v2-config-card,
- .v2-calibration-config-card,
.v2-extension-config-card,
- .v2-repair-config-card,
.v2-extension-metrics > div {
min-height: 38px;
}
- .console-v2 .v2-config-field .provider-note {
- display: none;
- }
-
.console-v2 .main-panel {
--v2-main-pad: 9px;
grid-template-rows: 36px clamp(236px, 33dvh, 246px) minmax(0, 1fr);
@@ -2667,10 +2167,6 @@
padding-top: 7px;
}
- .console-v2 .v2-advanced-field:not([hidden]) {
- max-height: 92px;
- overflow: auto;
- }
}
/* Interactive vehicle sync control layout -------------------------------- */
@@ -3033,12 +2529,6 @@
text-align: center;
}
-.console-v2.v2-interactive-sync .v2-sync-protocol {
- flex: 0 0 auto;
- margin-top: 0;
- padding: 8px 10px;
-}
-
.console-v2.v2-interactive-sync .v2-sidebar-footer {
min-height: 34px;
margin-top: 7px;
@@ -3572,15 +3062,14 @@
linear-gradient(160deg, #e6f7f4 0%, #d5efec 64%, #bfe4df 145%);
}
-.console-v2.v2-passive-display .v2-profile,
-.console-v2.v2-passive-display .v2-control-section,
-.console-v2.v2-passive-display .v2-config-section,
.console-v2.v2-passive-display .v2-sidebar-toggle,
.console-v2.v2-passive-display .v2-sidebar-footer button {
display: none !important;
}
.console-v2 .v2-sync-sidebar {
+ min-width: 0;
+ max-width: 100%;
min-height: 0;
display: flex;
flex: 1;
@@ -3613,11 +3102,14 @@
}
.console-v2 .v2-sync-link-stack {
+ width: 100%;
+ min-width: 0;
display: grid;
gap: 9px;
}
.console-v2 .v2-sync-link {
+ min-width: 0;
min-height: 58px;
display: grid;
grid-template-columns: 34px minmax(0, 1fr) 8px;
@@ -3689,41 +3181,6 @@
line-height: 1.8;
}
-.console-v2 .v2-sync-protocol {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-top: auto;
- padding: 13px;
- background: rgba(255, 255, 255, 0.48);
- border: 1px solid rgba(25, 111, 107, 0.1);
- border-radius: 14px;
-}
-
-.console-v2 .v2-sync-protocol > svg {
- width: 18px;
- color: var(--v2-accent);
-}
-
-.console-v2 .v2-sync-protocol div {
- min-width: 0;
- display: grid;
- gap: 3px;
-}
-
-.console-v2 .v2-sync-protocol small {
- color: #7d918f;
- font-size: 9px;
-}
-
-.console-v2 .v2-sync-protocol strong {
- overflow: hidden;
- color: #214342;
- font-size: 10px;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
.console-v2.v2-passive-display .v2-sidebar-footer {
min-height: 45px;
margin-top: 16px;
@@ -4549,6 +4006,19 @@
transition: height 220ms ease;
}
+.console-v2 .joint-twin-shell .window-summary-visual [data-state="closed"] i {
+ border-color: color-mix(in srgb, var(--v2-accent) 55%, #9db2af);
+}
+
+.console-v2 .joint-twin-shell .window-summary-visual [data-state="open"] i {
+ border-color: #d29a42;
+ box-shadow: 0 0 0 2px rgba(210, 154, 66, 0.12);
+}
+
+.console-v2 .joint-twin-shell .window-summary-visual [data-state="unknown"] i {
+ opacity: 0.48;
+}
+
.console-v2 .joint-twin-shell .lighting-summary-visual i {
width: 8px;
height: 8px;
@@ -4961,6 +4431,7 @@
}
.console-v2 .cabin-live-status {
+ min-width: 0;
display: inline-flex;
align-items: center;
gap: 6px;
@@ -4969,6 +4440,11 @@
white-space: nowrap;
}
+.console-v2 .cabin-live-status > b {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
.console-v2 .cabin-live-status > i {
width: 7px;
height: 7px;
@@ -7476,14 +6952,289 @@ body.device-power-drawer-open {
}
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-overview {
+ width: 100%;
+ min-width: 0;
+ max-width: 100%;
min-height: 0;
display: grid;
- grid-template-rows: auto auto minmax(0, 1fr) auto;
+ grid-auto-rows: max-content;
+ align-content: start;
gap: 10px;
padding-top: 26px;
overflow: hidden;
}
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-transport-field {
+ width: 100%;
+ min-width: 0;
+ min-height: max-content;
+ max-width: 100%;
+ align-self: start;
+ overflow: visible;
+ padding: 7px;
+ background: rgba(255, 255, 255, 0.86);
+ border-color: rgba(19, 105, 99, 0.14);
+ box-shadow: 0 5px 14px rgba(37, 84, 80, 0.06);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-simulation-fault-field {
+ width: 100%;
+ min-width: 0;
+ max-width: 100%;
+ margin: 0;
+ padding: 7px;
+ overflow: visible;
+ align-self: start;
+ background: rgba(255, 255, 255, 0.86);
+ border: 1px solid rgba(19, 105, 99, 0.14);
+ border-radius: 10px;
+ box-shadow: 0 5px 14px rgba(37, 84, 80, 0.06);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field > .field-label,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-simulation-fault-field > .field-label {
+ display: block;
+ margin: 0 0 6px;
+ color: #466664;
+ font-size: 10px;
+ font-weight: 850;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .execution-mode-options {
+ width: 100%;
+ min-width: 0;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 4px;
+ padding: 3px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .mode-option {
+ min-width: 0;
+ min-height: 34px;
+ display: grid;
+ grid-template-columns: 13px minmax(0, auto);
+ align-items: center;
+ justify-content: center;
+ gap: 3px;
+ padding: 4px;
+ overflow: hidden;
+ font-size: 9px;
+ line-height: 1.15;
+ white-space: nowrap;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .mode-option svg {
+ width: 13px;
+ height: 13px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .mode-option.active {
+ color: #fff;
+ background: linear-gradient(145deg, var(--v2-accent), var(--v2-accent-strong));
+ border-color: transparent;
+ box-shadow: 0 4px 11px color-mix(in srgb, var(--v2-accent) 18%, transparent);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .mode-option:disabled {
+ color: #7c8d8d;
+ background: rgba(235, 240, 239, 0.92);
+ opacity: 0.72;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .provider-note {
+ min-height: 0;
+ margin: 5px 1px 0;
+ overflow: hidden;
+ color: #809091;
+ font-size: 8px;
+ line-height: 1.35;
+ overflow-wrap: anywhere;
+ white-space: normal;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field #hardwareNote {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field #hardwareAliasNote {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-simulation-fault-field select {
+ min-height: 34px;
+ padding: 0 8px;
+ border-radius: 8px;
+ font-size: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel {
+ width: 100%;
+ min-width: 0;
+ align-self: start;
+ overflow: hidden;
+ background: rgba(255, 255, 255, 0.86);
+ border: 1px solid rgba(19, 105, 99, 0.14);
+ border-radius: 10px;
+ box-shadow: 0 5px 14px rgba(37, 84, 80, 0.06);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel summary {
+ min-height: 38px;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto 14px;
+ align-items: center;
+ gap: 7px;
+ padding: 7px 9px;
+ color: #315e5b;
+ cursor: pointer;
+ list-style: none;
+ user-select: none;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel summary::-webkit-details-marker {
+ display: none;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel summary > span {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel summary > span svg {
+ width: 14px;
+ height: 14px;
+ color: var(--v2-accent);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel summary b {
+ overflow: hidden;
+ font-size: 10px;
+ font-weight: 850;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel summary > strong {
+ min-width: 22px;
+ padding: 2px 6px;
+ color: #687f7d;
+ background: #edf4f3;
+ border-radius: 999px;
+ font: 850 9px/1.5 "SFMono-Regular", Consolas, monospace;
+ text-align: center;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel[data-state="ready"] summary > strong {
+ color: #08766f;
+ background: #d9f3ef;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel summary > svg {
+ width: 14px;
+ height: 14px;
+ transition: transform 160ms ease;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel[open] summary > svg {
+ transform: rotate(180deg);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list {
+ max-height: min(24dvh, 190px);
+ padding: 7px;
+ overflow: auto;
+ border-top: 1px solid rgba(19, 105, 99, 0.1);
+ scrollbar-width: thin;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list p {
+ margin: 0;
+ padding: 8px 6px;
+ color: #819190;
+ font-size: 9px;
+ line-height: 1.45;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list ol {
+ display: grid;
+ gap: 6px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li {
+ min-width: 0;
+ display: grid;
+ gap: 3px;
+ padding: 6px 7px;
+ background: #f3f8f7;
+ border-radius: 7px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li > span {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ color: #718684;
+ font-size: 8px;
+ font-weight: 800;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-channel {
+ flex: 0 0 auto;
+ min-width: 33px;
+ padding: 1px 5px;
+ color: #0b6f68;
+ background: #d9f1ed;
+ border: 1px solid rgba(11, 111, 104, 0.12);
+ border-radius: 999px;
+ font: 850 7px/1.5 "SFMono-Regular", Consolas, monospace;
+ letter-spacing: 0.02em;
+ text-align: center;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel="CAN0"] {
+ background: #f7f4ec;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel="CAN0"] .v2-can-command-channel {
+ color: #8a5a10;
+ background: #f8e9c9;
+ border-color: rgba(138, 90, 16, 0.13);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li code {
+ color: #075f5a;
+ font: 750 9px/1.45 "SFMono-Regular", Consolas, monospace;
+ overflow-wrap: anywhere;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-frame {
+ display: block;
+ color: #8a6b36;
+ font: 700 7px/1.45 "SFMono-Regular", Consolas, monospace;
+ overflow-wrap: anywhere;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-transport-field > .field-label {
+ color: #466664;
+ font-size: 10px;
+ font-weight: 850;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-transport-field .mode-option {
+ min-height: 34px;
+ font-size: 11px;
+}
+
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-heading {
min-width: 0;
display: grid;
@@ -7543,11 +7294,6 @@ body.device-power-drawer-open {
display: none;
}
-.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-overview > .v2-sync-protocol {
- align-self: end;
- margin: 0;
-}
-
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-overview > .v2-sidebar-footer {
width: 100%;
margin: 0;
@@ -7580,8 +7326,11 @@ body.device-power-drawer-open {
.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-sidebar-heading > small,
.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-sidebar-heading > h2,
.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-sidebar-heading > p,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-transport-field,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-execution-field,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-simulation-fault-field,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-can-command-panel,
.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-link-stack,
-.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-protocol,
.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sidebar-footer,
.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-log {
display: none !important;
diff --git a/apps/web-console/console-v2.js b/apps/web-console/console-v2.js
index b942e89..83af78d 100644
--- a/apps/web-console/console-v2.js
+++ b/apps/web-console/console-v2.js
@@ -20,7 +20,6 @@
sidebarCollapsed: sessionStorage.getItem("cognitive-agent.sidebar-collapsed") === "true",
};
- const dropdownControls = new Map();
let reasoningPickerControl = null;
function make(tag, className, html) {
@@ -39,10 +38,261 @@
.replaceAll("'", "'");
}
- function closeCustomDropdowns(except = null) {
- dropdownControls.forEach((control) => {
- if (control !== except) control.close();
+ const normalizedCanTarget = (target = {}) => String(
+ target.zone || target.instance || target.device || "vehicle",
+ ).replaceAll("_", "-");
+
+ const normalizedOnOff = (value) => {
+ if ([true, 1, "1", "on", "open", "enabled"].includes(value)) return "on";
+ if ([false, 0, "0", "off", "close", "disabled"].includes(value)) return "off";
+ return null;
+ };
+
+ const boundedInteger = (value, minimum, maximum) => {
+ const numeric = Number(value);
+ return Number.isInteger(numeric) && numeric >= minimum && numeric <= maximum
+ ? numeric
+ : null;
+ };
+
+ function parsedCanCommandForOperation(operation = {}) {
+ const capability = String(operation.capability || "");
+ const parameters = operation.parameters || {};
+ const target = normalizedCanTarget(operation.target);
+ const onOff = normalizedOnOff(parameters.state ?? parameters.power);
+ const hyphenated = (value) => String(value || "").replaceAll("_", "-");
+ const command = (category, text, channel = "CAN1", frame = "") => ({
+ channel, category, command: text, ...(frame ? { frame } : {}),
});
+ const hexByte = (value) => Number(value).toString(16).padStart(2, "0").toUpperCase();
+ const otaCan0Frame = (opcode, selector, argument) => {
+ const numeric = Number(argument) >>> 0;
+ const argumentBytes = [0, 8, 16, 24].map((shift) => hexByte((numeric >>> shift) & 0xFF));
+ return `0x6A0 · C7
${hexByte(opcode)} ${hexByte(selector)} ${argumentBytes.join(" ")}`;
+ };
+
+ const isPassengerSeat = new Set(["passenger", "front-right"]).has(target);
+ if (isPassengerSeat) {
+ const passengerAxes = {
+ "seat.set_longitudinal_position": ["副驾座椅前后调节", "longitudinal", 0],
+ "seat.set_cushion_tilt": ["副驾座椅高度调节", "height", 1],
+ "seat.set_backrest_recline": ["副驾座椅靠背调节", "backrest", 2],
+ "seat.set_legrest_height": ["副驾座椅腿托调节", "legrest", 3],
+ };
+ const axis = passengerAxes[capability];
+ const position = boundedInteger(parameters.position_pct, 0, 100);
+ if (axis && position !== null) {
+ const targetPermille = position * 10;
+ return command(
+ axis[0],
+ `seat passenger ${axis[1]} position ${position}`,
+ "CAN0",
+ otaCan0Frame(0x64, axis[2], targetPermille),
+ );
+ }
+ if (capability === "seat.start_dynamic_mode") {
+ const mode = boundedInteger(parameters.mode_id, 1, 2);
+ if (mode !== null) {
+ const dynamic = mode === 1
+ ? { name: "rocking-horse", selector: 2 }
+ : { name: "cradle", selector: 1 };
+ return command(
+ "副驾座椅动态模式",
+ `seat passenger dynamic start ${dynamic.name}`,
+ "CAN0",
+ otaCan0Frame(0x66, dynamic.selector, 300),
+ );
+ }
+ }
+ if (capability === "seat.stop_dynamic_mode") {
+ return command(
+ "副驾座椅动态模式",
+ "seat passenger dynamic stop",
+ "CAN0",
+ otaCan0Frame(0x63, 0xFF, 0),
+ );
+ }
+ }
+
+ if (capability === "door.set_rear_lock") {
+ const lockState = String(parameters.state || "");
+ const action = new Set(["lock", "unlock"]).has(lockState)
+ ? lockState
+ : onOff === "on" ? "lock" : onOff === "off" ? "unlock" : null;
+ if (action) return command("后排门锁", `door rear ${action}`);
+ }
+ if (capability === "door.set_vehicle_lock") {
+ const lockState = String(parameters.state || "");
+ if (new Set(["lock", "unlock"]).has(lockState)) {
+ return command("整车门锁", `door vehicle ${lockState}`);
+ }
+ }
+ if (capability === "window.set_motion") {
+ const scope = new Set(["all", "front", "rear", "front-left", "front-right", "rear-left", "rear-right"]);
+ const motion = String(parameters.motion || parameters.action || "");
+ if (scope.has(target) && new Set(["open", "close", "stop"]).has(motion)) {
+ return command("车窗动作", `window ${target} ${motion}`);
+ }
+ }
+ if (capability === "window.set_position") {
+ const position = boundedInteger(parameters.position_pct, 0, 100);
+ if (new Set(["front-left", "front-right", "rear-left", "rear-right"]).has(target)
+ && position !== null) {
+ return command("单窗位置", `window ${target} position ${position}`);
+ }
+ }
+ if (capability === "climate.set_power" && onOff) {
+ return command("空调电源", `ac ${onOff}`);
+ }
+ if (capability === "climate.set_fan_level") {
+ const level = boundedInteger(parameters.level ?? parameters.fan_level, 1, 7);
+ if (level !== null) return command("空调风量", `ac fan ${level}`);
+ }
+ if (capability === "climate.set_outlet_mode") {
+ const mode = hyphenated(parameters.mode ?? parameters.outlet_mode);
+ const modes = new Set(["face", "face-feet", "feet", "feet-defrost", "defrost", "face-feet-defrost", "face-defrost"]);
+ if (modes.has(mode)) return command("出风模式", `ac mode ${mode}`);
+ }
+ if (capability === "climate.set_temperature") {
+ const temperature = boundedInteger(parameters.temperature_c, 18, 32);
+ if (new Set(["driver", "passenger", "rear"]).has(target) && temperature !== null) {
+ return command("空调温度", `ac temp ${target} ${temperature}`);
+ }
+ }
+ if (capability === "climate.set_control_mode") {
+ const mode = String(parameters.mode ?? parameters.control_mode ?? "");
+ if (new Set(["manual", "auto"]).has(mode)) return command("空调控制方式", `ac control ${mode}`);
+ }
+ if (capability === "climate.set_cycle_mode") {
+ const mode = String(parameters.mode ?? parameters.cycle_mode ?? "");
+ if (new Set(["internal", "external"]).has(mode)) return command("空调循环", `ac cycle ${mode}`);
+ }
+ if (capability === "outlet.set_position_percent") {
+ const horizontal = boundedInteger(parameters.horizontal_pct, 0, 100);
+ const vertical = boundedInteger(parameters.vertical_pct, 0, 100);
+ if (new Set(["driver-left", "driver-right", "passenger-left", "passenger-right"]).has(target)
+ && horizontal !== null && vertical !== null) {
+ return command("风口百分比", `outlet ${target} percent ${horizontal} ${vertical}`);
+ }
+ }
+ if (capability === "outlet.set_calibrated_position") {
+ const horizontal = boundedInteger(parameters.horizontal_value, -128, 126);
+ const vertical = boundedInteger(parameters.vertical_value, -128, 126);
+ if (new Set(["driver-left", "driver-right", "passenger-left", "passenger-right"]).has(target)
+ && horizontal !== null && vertical !== null) {
+ return command("风口标定坐标", `outlet ${target} position ${horizontal} ${vertical}`);
+ }
+ }
+ if (new Set(["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"]).has(capability)
+ && new Set(["driver", "front-left"]).has(target)) {
+ const feature = capability.includes("heating") ? "heat" : "vent";
+ const level = boundedInteger(parameters.level, 1, 2);
+ const setting = onOff === "off" ? "off" : level === 1 ? "high" : level === 2 ? "low" : null;
+ if (setting) return command("主驾座椅通风/加热", `seat driver ${feature} ${setting}`);
+ }
+ if (capability === "seat.set_driver_massage_native"
+ && new Set(["driver", "front-left"]).has(target)) {
+ const action = String(parameters.action || "");
+ if (action === "off") return command("主驾座椅按摩", "seat driver massage off");
+ if (action === "strength") {
+ const strength = { 1: "weak", 2: "medium", 3: "strong" }[Number(parameters.strength)]
+ || (new Set(["weak", "medium", "strong"]).has(parameters.strength) ? parameters.strength : null);
+ if (strength) return command("主驾座椅按摩", `seat driver massage strength ${strength}`);
+ }
+ if (action === "mode" && new Set(["wave", "pulse", "stretch", "slack", "relax"]).has(parameters.mode)) {
+ return command("主驾座椅按摩", `seat driver massage mode ${parameters.mode}`);
+ }
+ }
+ if (capability === "seat.set_driver_axis_native"
+ && new Set(["driver", "front-left"]).has(target)) {
+ const axis = String(parameters.axis || "");
+ const direction = String(parameters.direction || "");
+ const validDirections = {
+ horizontal: new Set(["forward", "backward", "stop"]),
+ height: new Set(["up", "down", "stop"]),
+ backrest: new Set(["forward", "backward", "stop"]),
+ basin: new Set(["up", "down", "stop"]),
+ };
+ if (validDirections[axis]?.has(direction)) {
+ if (direction === "stop") return command("主驾座椅调节", `seat driver ${axis} stop`);
+ const duration = boundedInteger(parameters.duration_ms, 100, 2000);
+ if (duration !== null) return command("主驾座椅调节", `seat driver ${axis} ${direction} ${duration}`);
+ }
+ }
+ if (capability === "seat.set_driver_position_native") {
+ const position = boundedInteger(parameters.position ?? parameters.slot, 1, 3);
+ if (position !== null) return command("主驾座椅记忆", `seat driver position ${position}`);
+ }
+ if (capability === "seat.recall_driver_memory_native") {
+ const position = boundedInteger(parameters.position ?? parameters.slot, 1, 3);
+ if (position !== null) return command("主驾座椅记忆", `seat driver memory recall ${position}`);
+ }
+ if (capability === "interior_light.set_indoor_power" && onOff) {
+ return command("室内灯总开关", `light indoor ${onOff}`);
+ }
+ if (capability === "interior_light.set_reading_power" && target === "all" && onOff) {
+ return command("全部阅读灯", `light reading all ${onOff}`);
+ }
+ if (capability === "ambient_light.set_power" && onOff) {
+ return command("氛围灯", `light ambient ${onOff}`);
+ }
+ if (capability === "ambient_light.set_color_index") {
+ const color = boundedInteger(parameters.color_index, 1, 127);
+ if (color !== null) return command("氛围灯", `light ambient color ${color}`);
+ }
+ if (capability === "ambient_light.set_brightness_level") {
+ const level = boundedInteger(parameters.brightness_level, 1, 5);
+ if (level !== null) return command("氛围灯", `light ambient brightness ${level}`);
+ }
+ if (capability === "roof.set_shade_motion") {
+ const motion = String(parameters.motion || "");
+ if (new Set(["open", "close", "half", "stop"]).has(motion)) {
+ return command("遮阳帘", `shade ${motion}`);
+ }
+ }
+ if (capability === "roof.set_shade_position") {
+ const position = boundedInteger(parameters.position_pct ?? parameters.shade_position_pct, 0, 100);
+ if (position !== null) return command("遮阳帘", `shade position ${position}`);
+ }
+ return null;
+ }
+
+ function parsedCanCommands(result = {}) {
+ const configurations = result.function_configs || {};
+ const plans = result.command_plans || {};
+ const planKeys = new Set(Object.keys(plans));
+ const hasCompiledPlan = planKeys.size > 0 || Boolean(result.command_plan);
+ if (!hasCompiledPlan) return [];
+ const commands = [];
+ Object.entries(configurations).forEach(([configKey, configuration]) => {
+ if (planKeys.size > 0 && !planKeys.has(configKey)) return;
+ (configuration?.spec?.operations || []).forEach((operation) => {
+ const item = parsedCanCommandForOperation(operation);
+ if (item) commands.push(item);
+ });
+ });
+ return [...new Map(commands.map((item) => [`${item.channel}:${item.command}`, item])).values()];
+ }
+
+ function renderParsedCanCommands(result, { message = "" } = {}) {
+ const panel = $("#v2CanCommandPanel");
+ const list = $("#v2CanCommandList");
+ const count = $("#v2CanCommandCount");
+ if (!panel || !list || !count) return;
+ const commands = result ? parsedCanCommands(result) : [];
+ count.textContent = String(commands.length);
+ panel.dataset.state = commands.length ? "ready" : message ? "pending" : "empty";
+ if (commands.length) {
+ list.innerHTML = `${commands.map((item) => `
+ -
+ ${escapeHtml(item.channel)}${escapeHtml(item.category)}
+
${escapeHtml(item.command)}
+ ${item.frame ? `seat-ota · ${escapeHtml(item.frame)}` : ""}
+ `).join("")}
`;
+ } else {
+ list.innerHTML = `${escapeHtml(message || "本次请求未生成已验证的 CAN 控制指令")}
`;
+ }
+ requestAnimationFrame(syncSidebarLogBaseline);
}
function closeReasoningPicker() {
@@ -105,7 +355,6 @@
trigger.setAttribute("aria-expanded", "false");
};
const open = () => {
- closeCustomDropdowns();
reasoningField.hidden = false;
trigger.classList.add("open");
trigger.setAttribute("aria-expanded", "true");
@@ -142,156 +391,25 @@
sync();
}
- function enhanceSelect(select) {
- if (!select || dropdownControls.has(select)) return;
- const shell = select.parentElement;
- if (!shell) return;
-
- shell.classList.add("v2-custom-select");
- select.classList.add("v2-native-select");
-
- const trigger = make("button", "v2-dropdown-trigger", `
-
- `);
- trigger.type = "button";
- trigger.setAttribute("aria-haspopup", "listbox");
- trigger.setAttribute("aria-expanded", "false");
- trigger.setAttribute("aria-label", select.getAttribute("aria-label") || "打开下拉菜单");
-
- const menu = make("div", "v2-dropdown-menu", "");
- menu.id = `${select.id}Menu`;
- menu.hidden = true;
- menu.setAttribute("role", "listbox");
- menu.setAttribute("aria-label", select.getAttribute("aria-label") || "选项");
- trigger.setAttribute("aria-controls", menu.id);
- shell.append(trigger, menu);
-
- const valueNode = $(".v2-dropdown-value", trigger);
- const render = () => {
- const selected = select.selectedOptions[0] || select.options[0];
- valueNode.textContent = selected?.textContent?.trim() || "请选择";
- trigger.disabled = select.disabled;
- menu.innerHTML = "";
- [...select.options].forEach((option) => {
- const item = make("button", "v2-dropdown-option", "");
- item.type = "button";
- item.dataset.value = option.value;
- item.textContent = option.textContent.trim();
- item.disabled = option.disabled;
- item.setAttribute("role", "option");
- item.setAttribute("aria-selected", String(option.value === select.value));
- menu.append(item);
- });
- };
-
- const close = () => {
- menu.hidden = true;
- shell.classList.remove("v2-dropdown-open");
- trigger.setAttribute("aria-expanded", "false");
- };
-
- const open = () => {
- if (trigger.disabled) return;
- closeCustomDropdowns();
- render();
- menu.hidden = false;
- shell.classList.add("v2-dropdown-open");
- trigger.setAttribute("aria-expanded", "true");
- };
-
- const control = { close, menu, open, select, shell, sync: render, trigger };
- dropdownControls.set(select, control);
-
- trigger.addEventListener("click", (event) => {
- event.stopPropagation();
- if (menu.hidden) open();
- else close();
- });
- trigger.addEventListener("keydown", (event) => {
- if (event.key === "ArrowDown") {
- event.preventDefault();
- open();
- requestAnimationFrame(() => {
- $(".v2-dropdown-option[aria-selected=\"true\"]:not(:disabled)", menu)?.focus();
- });
- } else if (event.key === "Escape") {
- close();
- }
- });
- menu.addEventListener("click", (event) => {
- event.stopPropagation();
- const item = event.target.closest(".v2-dropdown-option");
- if (!item || item.disabled) return;
- select.value = item.dataset.value;
- select.dispatchEvent(new Event("change", { bubbles: true }));
- render();
- close();
- trigger.focus();
- });
- menu.addEventListener("keydown", (event) => {
- if (event.key !== "Escape") return;
- event.preventDefault();
- close();
- trigger.focus();
- });
- select.addEventListener("change", render);
- new MutationObserver(render).observe(select, {
- attributes: true,
- characterData: true,
- childList: true,
- subtree: true,
- });
- render();
- }
-
- function enhanceConfiguredDropdowns() {
- [$("#vehicleSelect")]
- .filter(Boolean)
- .forEach(enhanceSelect);
- }
-
function buildSidebar(commandPanel) {
commandPanel.classList.add("v2-sidebar");
commandPanel.innerHTML = "";
- const profile = make("section", "v2-profile", `
-
-
-
WELCOME TO THE CABIN
-
- Hi,user
-
-
- `);
-
- const control = make("section", "v2-sidebar-section v2-control-section", `
-
- CONTROL DESK
-
控制台
-
- 控制方式
-
-
-
-
`);
-
- const config = make("section", "v2-sidebar-section v2-config-section", `
-
- 配置信息
- `);
+ const sidebarToggle = make("button", "v2-sidebar-toggle", '');
+ sidebarToggle.id = "v2SidebarToggle";
+ sidebarToggle.type = "button";
+ sidebarToggle.setAttribute("aria-label", "折叠左侧控制台");
+ sidebarToggle.setAttribute("aria-expanded", "true");
const footer = make("footer", "v2-sidebar-footer", `
智舱控制链路正在连接服务
`);
- commandPanel.append(profile, control, config, footer);
- return $("#v2ConfigStack");
+ commandPanel.append(sidebarToggle, footer);
}
- function buildConfigFields(configStack, originalForm) {
+ function prepareRuntimeControls(originalForm) {
const vehicleField = $(".vehicle-field", originalForm);
const transportField = $("#vehicleTransportField", originalForm);
const providerField = $(".field-group:has(.segmented [data-provider])", originalForm);
@@ -299,72 +417,17 @@
const executionField = $(".field-group:has(.execution-mode-options)", originalForm);
const simulationFaultField = $("#simulationFaultField", originalForm);
- if (vehicleField) {
- vehicleField.classList.add("v2-config-field", "v2-vehicle-field");
- const vehicleControl = $(".vehicle-select-control", vehicleField);
- const vehicleSelect = $("#vehicleSelect", vehicleField);
- const vehicleIcon = $("svg, [data-lucide]", vehicleControl);
- if (vehicleControl && vehicleSelect && vehicleIcon) {
- const copy = make("div", "v2-config-card-copy", "车型配置");
- const selectSlot = make("div", "v2-config-card-select", "");
- const state = make("span", "v2-field-state", "CHECK");
- state.id = "v2VehicleState";
- selectSlot.append(vehicleSelect);
- copy.append(selectSlot);
- vehicleControl.classList.add("v2-config-card", "v2-config-select-card");
- vehicleControl.id = "v2VehicleCard";
- vehicleControl.dataset.state = "checking";
- vehicleControl.replaceChildren(vehicleIcon, copy, state);
- $(".field-label", vehicleField)?.remove();
- }
- configStack.append(vehicleField);
- }
+ transportField?.classList.add("v2-transport-field");
+ executionField?.classList.add("v2-execution-field");
+ simulationFaultField?.classList.add("v2-simulation-fault-field");
- if (transportField) {
- transportField.classList.add("v2-config-field", "v2-transport-field");
- configStack.append(transportField);
- }
-
- const modelField = make("div", "v2-config-field v2-config-card v2-model-field", `
-
- 推理模型模型检测中
- AUTO
- 模型由服务端安全配置
`);
- modelField.id = "v2ModelField";
- configStack.append(modelField);
installReasoningPicker(reasoningField, originalForm);
- const gestureModelField = make("div", "v2-config-field v2-config-card v2-gesture-model-field", `
-
- 手势模型MediaPipe GR
- VISION
- Tasks Vision · 0.10.14`);
- gestureModelField.id = "v2GestureModelField";
- configStack.append(gestureModelField);
-
- const userHardwareState = make("div", "v2-user-target-status", `
- 正在准备虚拟 CAN`);
- configStack.append(userHardwareState);
-
- if (providerField) {
- providerField.classList.add("v2-provider-field");
- providerField.hidden = true;
- configStack.append(providerField);
- }
-
- if (executionField) {
- executionField.classList.add("v2-native-execution-field");
- executionField.hidden = true;
- configStack.append(executionField);
- }
-
- const advanced = make("div", "v2-advanced-stack", "");
- [simulationFaultField].forEach((field) => {
+ [vehicleField, providerField].forEach((field) => {
if (!field) return;
- field.classList.add("v2-config-field", "v2-advanced-field");
- advanced.append(field);
+ field.classList.add("v2-runtime-data-field");
+ field.hidden = true;
});
- configStack.append(advanced);
}
function buildOperation(mainPanel, originalForm, panelHeading, modeRail, requestHeader, gestureDrawer) {
@@ -582,16 +645,14 @@
const syncedDevices = [
{
id: "passenger", name: "副驾座椅", icon: "armchair",
- rows: () => [["通风", seatFunctionValue("passenger", "ventilation")], ["加热", seatFunctionValue("passenger", "heating")], ["水平", seatAxisValue("passenger", "longitudinal")], ["坐垫", seatAxisValue("passenger", "cushion_tilt")], ["靠背", seatAxisValue("passenger", "backrest_recline")], ["腿托", seatAxisValue("passenger", "legrest_height")]],
+ rows: () => [["前后", seatAxisValue("passenger", "longitudinal")], ["高度", seatAxisValue("passenger", "cushion_tilt")], ["靠背", seatAxisValue("passenger", "backrest_recline")], ["腿托", seatAxisValue("passenger", "legrest_height")]],
controls: () => [
- selectControl("ventilation", "通风", seatLevelControlValue("passenger", "ventilation"), levelOptions),
- selectControl("heating", "加热", seatLevelControlValue("passenger", "heating"), levelOptions),
- rangeControl("longitudinal", "水平", seatAxisValue("passenger", "longitudinal")),
- rangeControl("cushion_tilt", "坐垫", seatAxisValue("passenger", "cushion_tilt")),
+ rangeControl("longitudinal", "前后", seatAxisValue("passenger", "longitudinal")),
+ rangeControl("cushion_tilt", "高度", seatAxisValue("passenger", "cushion_tilt")),
rangeControl("backrest_recline", "靠背", seatAxisValue("passenger", "backrest_recline")),
rangeControl("legrest_height", "腿托", seatAxisValue("passenger", "legrest_height")),
],
- command: (values) => `调节副驾座椅:通风${optionLabel(levelOptions, values.ventilation)},加热${optionLabel(levelOptions, values.heating)},水平${values.longitudinal}%,坐垫${values.cushion_tilt}%,靠背${values.backrest_recline}%,腿托${values.legrest_height}%`,
+ command: (values) => `调节副驾座椅:前后${values.longitudinal}%,座椅高度${values.cushion_tilt}%,靠背${values.backrest_recline}%,腿托${values.legrest_height}%`,
},
{
id: "driver", name: "主驾座椅", icon: "armchair",
@@ -1006,6 +1067,11 @@
button.classList.toggle("v2-sync-active", active);
});
renderTaskTimeline(task);
+ if (normalizedStatus === "running") {
+ renderParsedCanCommands(null, { message: "正在解析自然语言并生成 CAN 控制指令…" });
+ } else if (result) {
+ renderParsedCanCommands(result);
+ }
}
function applyDisplaySnapshot(snapshot) {
@@ -1027,9 +1093,6 @@
: listening ? "等待 USB 外设" : "扩展链路检查中";
usbState.dataset.state = connected ? "online" : listening ? "listening" : "offline";
}
- if ($("#v2SyncTransport")) {
- $("#v2SyncTransport").textContent = `${String(health.transport || "GSCU").toUpperCase()} · ${health.peer || health.listen || "等待连接"}`;
- }
const vehicleOnline = health.vehicle?.connected !== false;
const vehicleBadge = $("#v2HeadUnitState");
if (vehicleBadge) {
@@ -1124,7 +1187,26 @@
const sidebar = $(".v2-sidebar");
const lowerPanel = $("#v2DevicePanel") || $(".joint-twin-shell > .twin-band");
if (!sidebar || !lowerPanel) return;
- const offset = Math.max(0, lowerPanel.getBoundingClientRect().top - sidebar.getBoundingClientRect().top);
+ const sidebarRect = sidebar.getBoundingClientRect();
+ const overview = $(".v2-sync-sidebar-overview", sidebar);
+ const overviewRect = overview?.getBoundingClientRect();
+ const overviewBottom = overview && overviewRect
+ ? Array.from(overview.children).reduce(
+ (bottom, child) => Math.max(bottom, child.getBoundingClientRect().bottom),
+ overviewRect.top,
+ )
+ : 0;
+ const overviewPaddingBottom = overview
+ ? Number.parseFloat(getComputedStyle(overview).paddingBottom) || 0
+ : 0;
+ const requiredOverviewHeight = overviewRect
+ ? Math.ceil(overviewBottom - overviewRect.top + overviewPaddingBottom + 10)
+ : 0;
+ const offset = Math.max(
+ 0,
+ lowerPanel.getBoundingClientRect().top - sidebarRect.top,
+ requiredOverviewHeight,
+ );
sidebar.style.setProperty("--v2-sync-log-top", `${offset}px`);
}
@@ -1143,7 +1225,6 @@
-