优化前端,优化前端指令,优化自定义控制
This commit is contained in:
+367
-330
@@ -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 <SEQ> ${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 = `<ol>${commands.map((item) => `
|
||||
<li data-can-channel="${escapeHtml(item.channel)}">
|
||||
<span><b class="v2-can-command-channel">${escapeHtml(item.channel)}</b>${escapeHtml(item.category)}</span>
|
||||
<code>${escapeHtml(item.command)}</code>
|
||||
${item.frame ? `<small class="v2-can-command-frame">seat-ota · ${escapeHtml(item.frame)}</small>` : ""}
|
||||
</li>`).join("")}</ol>`;
|
||||
} else {
|
||||
list.innerHTML = `<p>${escapeHtml(message || "本次请求未生成已验证的 CAN 控制指令")}</p>`;
|
||||
}
|
||||
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", `
|
||||
<span class="v2-dropdown-value"></span>
|
||||
<span class="v2-dropdown-caret" aria-hidden="true"></span>`);
|
||||
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", `
|
||||
<div class="v2-avatar" aria-hidden="true"><span></span></div>
|
||||
<div class="v2-profile-copy">
|
||||
<small>WELCOME TO THE CABIN</small>
|
||||
<div class="v2-role-select">
|
||||
<span>Hi,</span><strong>user</strong>
|
||||
</div>
|
||||
</div>
|
||||
<button class="v2-sidebar-toggle" id="v2SidebarToggle" type="button" aria-label="折叠左侧控制台" aria-expanded="true">
|
||||
<i data-lucide="panel-left-close"></i>
|
||||
</button>`);
|
||||
|
||||
const control = make("section", "v2-sidebar-section v2-control-section", `
|
||||
<div class="v2-section-heading">
|
||||
<span>CONTROL DESK</span>
|
||||
<h2>控制台</h2>
|
||||
</div>
|
||||
<span class="v2-field-caption">控制方式</span>
|
||||
<div class="v2-control-switch" role="group" aria-label="控制方式">
|
||||
<button type="button" data-v2-control="natural"><i data-lucide="message-square-text"></i><span>自然语言控制</span></button>
|
||||
<button type="button" data-v2-control="gesture"><i data-lucide="scan-face"></i><span>手势控制</span></button>
|
||||
</div>`);
|
||||
|
||||
const config = make("section", "v2-sidebar-section v2-config-section", `
|
||||
<div class="v2-divider"></div>
|
||||
<span class="v2-field-caption">配置信息</span>
|
||||
<div class="v2-config-stack" id="v2ConfigStack"></div>`);
|
||||
const sidebarToggle = make("button", "v2-sidebar-toggle", '<i data-lucide="panel-left-close"></i>');
|
||||
sidebarToggle.id = "v2SidebarToggle";
|
||||
sidebarToggle.type = "button";
|
||||
sidebarToggle.setAttribute("aria-label", "折叠左侧控制台");
|
||||
sidebarToggle.setAttribute("aria-expanded", "true");
|
||||
|
||||
const footer = make("footer", "v2-sidebar-footer", `
|
||||
<span class="v2-live-dot"></span>
|
||||
<div><strong>智舱控制链路</strong><small id="v2ConnectionText">正在连接服务</small></div>
|
||||
<button type="button" id="v2LogoutButton" title="退出登录" aria-label="退出登录"><i data-lucide="log-out"></i></button>`);
|
||||
|
||||
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", "<small>车型配置</small>");
|
||||
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", `
|
||||
<i data-lucide="sparkles"></i>
|
||||
<div><small>推理模型</small><strong id="v2ModelValue">模型检测中</strong></div>
|
||||
<span class="v2-field-state" id="v2ModelState">AUTO</span>
|
||||
<p class="provider-note" id="v2ModelNote">模型由服务端安全配置</p>`);
|
||||
modelField.id = "v2ModelField";
|
||||
configStack.append(modelField);
|
||||
installReasoningPicker(reasoningField, originalForm);
|
||||
|
||||
const gestureModelField = make("div", "v2-config-field v2-config-card v2-gesture-model-field", `
|
||||
<i data-lucide="scan-line"></i>
|
||||
<div><small>手势模型</small><strong id="v2GestureModelValue">MediaPipe GR</strong></div>
|
||||
<span class="v2-field-state">VISION</span>
|
||||
<small class="v2-gesture-model-note" id="v2GestureModelNote">Tasks Vision · 0.10.14</small>`);
|
||||
gestureModelField.id = "v2GestureModelField";
|
||||
configStack.append(gestureModelField);
|
||||
|
||||
const userHardwareState = make("div", "v2-user-target-status", `
|
||||
<i></i><span id="v2UserTargetText">正在准备虚拟 CAN</span>`);
|
||||
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 @@
|
||||
<div class="v2-sync-link" id="v2MirrorState" data-state="checking"><i data-lucide="refresh-cw"></i><div><strong>网页同步</strong><span>建立同步中</span></div><b></b></div>
|
||||
<div class="v2-sync-link" id="v2UsbState" data-state="checking"><i data-lucide="usb"></i><div><strong>USB 扩展</strong><span>检查外设中</span></div><b></b></div>
|
||||
</div>
|
||||
<div class="v2-sync-protocol"><i data-lucide="radio-tower"></i><div><small>扩展链路</small><strong id="v2SyncTransport">GSCU · 等待连接</strong></div></div>
|
||||
</div>
|
||||
<section class="v2-sync-log" aria-labelledby="v2SyncLogTitle">
|
||||
<header><div><small>RUNTIME / SYNC</small><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div><select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select></header>
|
||||
@@ -1151,7 +1232,39 @@
|
||||
</section>`);
|
||||
if (sidebar && sidebarFooter) {
|
||||
sidebar.insertBefore(syncSidebar, sidebarFooter);
|
||||
$(".v2-sync-sidebar-overview", syncSidebar)?.append(sidebarFooter);
|
||||
const syncOverview = $(".v2-sync-sidebar-overview", syncSidebar);
|
||||
const transportField = $("#vehicleTransportField");
|
||||
const executionField = $(".field-group:has(.execution-mode-options)");
|
||||
const simulationFaultField = $("#simulationFaultField");
|
||||
if (syncOverview) {
|
||||
const linkStack = $(".v2-sync-link-stack", syncOverview);
|
||||
const insertVisibleControl = (field, className) => {
|
||||
if (!field) return;
|
||||
field.classList.add(className);
|
||||
if (linkStack) syncOverview.insertBefore(field, linkStack);
|
||||
else syncOverview.append(field);
|
||||
};
|
||||
insertVisibleControl(transportField, "v2-sync-transport-field");
|
||||
if (executionField) executionField.hidden = false;
|
||||
insertVisibleControl(executionField, "v2-sync-execution-field");
|
||||
insertVisibleControl(simulationFaultField, "v2-sync-simulation-fault-field");
|
||||
const canCommandPanel = make("details", "v2-can-command-panel", `
|
||||
<summary>
|
||||
<span><i data-lucide="binary"></i><b>已解析 CAN 控制指令</b></span>
|
||||
<strong id="v2CanCommandCount">0</strong>
|
||||
<i data-lucide="chevron-down"></i>
|
||||
</summary>
|
||||
<div class="v2-can-command-list" id="v2CanCommandList">
|
||||
<p>等待自然语言解析结果</p>
|
||||
</div>`);
|
||||
canCommandPanel.id = "v2CanCommandPanel";
|
||||
if (linkStack) syncOverview.insertBefore(canCommandPanel, linkStack);
|
||||
else syncOverview.append(canCommandPanel);
|
||||
canCommandPanel.addEventListener("toggle", () => {
|
||||
requestAnimationFrame(syncSidebarLogBaseline);
|
||||
});
|
||||
}
|
||||
syncOverview?.append(sidebarFooter);
|
||||
const sidebarToggle = $("#v2SidebarToggle", sidebar);
|
||||
if (sidebarToggle) $(".v2-sync-sidebar-heading", syncSidebar)?.append(sidebarToggle);
|
||||
}
|
||||
@@ -1234,8 +1347,12 @@
|
||||
if (event.target.matches("select[data-device-control]")) storeDeviceControlDraft(event.target);
|
||||
});
|
||||
$("#v2LogFilter")?.addEventListener("change", renderSyncLogs);
|
||||
$("#requestForm")?.addEventListener("submit", () => {
|
||||
renderParsedCanCommands(null, { message: "正在解析自然语言并生成 CAN 控制指令…" });
|
||||
});
|
||||
window.addEventListener("cognitive-agent:result", (event) => {
|
||||
const result = event.detail || {};
|
||||
renderParsedCanCommands(result);
|
||||
appendSyncLog({
|
||||
key: result.request_id ? `result:${result.request_id}` : "",
|
||||
category: "task",
|
||||
@@ -1259,6 +1376,7 @@
|
||||
renderSyncedDevices();
|
||||
}
|
||||
if (event.detail?.error) {
|
||||
renderParsedCanCommands(null, { message: "本次请求未生成 CAN 控制指令" });
|
||||
appendSyncLog({ category: "task", title: "编排任务执行失败", detail: event.detail.error.message || "请求失败", status: "error" });
|
||||
}
|
||||
});
|
||||
@@ -1376,23 +1494,18 @@
|
||||
if (requestStatus && statusTarget && requestStatus.parentElement !== statusTarget) {
|
||||
statusTarget.append(requestStatus);
|
||||
}
|
||||
$$('[data-v2-control]').forEach((button) => {
|
||||
const active = button.dataset.v2Control === ui.controlMode;
|
||||
button.classList.toggle("active", active);
|
||||
button.setAttribute("aria-pressed", String(active));
|
||||
});
|
||||
requestAnimationFrame(updateModeEdgeHints);
|
||||
const drawer = $("#gestureDrawer");
|
||||
if (!triggerGesture) {
|
||||
const gestureVisible = ui.controlMode === "gesture";
|
||||
drawer?.setAttribute("aria-hidden", String(!gestureVisible));
|
||||
$("#gestureToggleButton")?.setAttribute("aria-expanded", String(gestureVisible));
|
||||
drawer?.classList.toggle("open", gestureVisible);
|
||||
return;
|
||||
}
|
||||
if (ui.controlMode === "gesture") {
|
||||
if (!drawer?.classList.contains("open")) $("#gestureToggleButton")?.click();
|
||||
window.CognitiveAgentGesture?.open?.();
|
||||
} else if (drawer?.classList.contains("open")) {
|
||||
$("#gestureCloseButton")?.click();
|
||||
window.CognitiveAgentGesture?.close?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1441,120 +1554,53 @@
|
||||
window.setTimeout(() => window.dispatchEvent(new Event("resize")), 180);
|
||||
}
|
||||
|
||||
function requestUserSafeExecutionMode() {
|
||||
const dryRunButton = $('[data-execution-mode="dry_run"]');
|
||||
const note = $("#v2ExecutionNote");
|
||||
const userTarget = $(".v2-user-target-status");
|
||||
const userTargetText = $("#v2UserTargetText");
|
||||
function keepRunButtonAvailable() {
|
||||
const runButton = $("#runButton");
|
||||
if (!dryRunButton) return;
|
||||
if (!dryRunButton.classList.contains("active")) dryRunButton.click();
|
||||
if (note) note.textContent = "user 固定使用虚拟 CAN 安全执行环境";
|
||||
if (userTarget) userTarget.dataset.state = "ready";
|
||||
if (userTargetText) userTargetText.textContent = "执行目标:虚拟 CAN";
|
||||
if (runButton && !runButton.classList.contains("loading")) runButton.disabled = false;
|
||||
syncExecutionSelector();
|
||||
}
|
||||
|
||||
function syncExecutionSelector() {
|
||||
const select = $("#v2ExecutionSelect");
|
||||
if (!select) return;
|
||||
const active = $('[data-execution-mode].active');
|
||||
if (active) select.value = active.dataset.executionMode;
|
||||
["dry_run", "simulated_hardware", "hardware"].forEach((value) => {
|
||||
const option = $(`option[value="${value}"]`, select);
|
||||
const button = $(`[data-execution-mode="${value}"]`);
|
||||
if (option && button) option.disabled = button.disabled;
|
||||
});
|
||||
dropdownControls.get(select)?.sync();
|
||||
}
|
||||
|
||||
function syncRuntimePresentation() {
|
||||
const modelBadge = $("#modelBadge");
|
||||
const connectionBadge = $("#connectionBadge");
|
||||
const modelValue = $("#v2ModelValue");
|
||||
const modelNote = $("#v2ModelNote");
|
||||
function syncRuntimePresentation(connectionState = window.CognitiveAgentNetwork?.getState?.()) {
|
||||
const connectionText = $("#v2ConnectionText");
|
||||
if (modelBadge && modelValue) {
|
||||
const text = modelBadge.textContent.trim() || "模型检测中";
|
||||
modelValue.textContent = text;
|
||||
const unavailable = modelBadge.classList.contains("danger");
|
||||
$("#v2ModelField")?.classList.toggle("unavailable", unavailable);
|
||||
$("#v2ModelState").textContent = unavailable ? "LOCAL" : "READY";
|
||||
if (modelNote) modelNote.textContent = unavailable
|
||||
? "GPT 不可用,自动使用本地规则"
|
||||
: "服务端安全托管 · 失败自动降级";
|
||||
}
|
||||
if (connectionBadge && connectionText) {
|
||||
connectionText.textContent = connectionBadge.textContent.trim() || "正在连接服务";
|
||||
const online = !connectionBadge.classList.contains("danger")
|
||||
&& !connectionBadge.classList.contains("warning");
|
||||
$(".v2-sidebar-footer")?.classList.toggle("offline", !online);
|
||||
}
|
||||
syncExecutionSelector();
|
||||
requestUserSafeExecutionMode();
|
||||
}
|
||||
|
||||
function setVehicleLinkState(connected) {
|
||||
const card = $("#v2VehicleCard");
|
||||
const state = $("#v2VehicleState");
|
||||
if (!card || !state) return;
|
||||
card.dataset.state = connected ? "online" : "offline";
|
||||
state.textContent = connected ? "READY" : "OFFLINE";
|
||||
}
|
||||
|
||||
async function refreshVehicleLinkState() {
|
||||
const card = $("#v2VehicleCard");
|
||||
const state = $("#v2VehicleState");
|
||||
if (card) card.dataset.state = "checking";
|
||||
if (state) state.textContent = "CHECK";
|
||||
try {
|
||||
const response = await fetch("/api/extension/health", {
|
||||
credentials: "same-origin",
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const health = await response.json();
|
||||
setVehicleLinkState(Boolean(health.vehicle?.connected));
|
||||
} catch (_error) {
|
||||
setVehicleLinkState(false);
|
||||
if (connectionText) {
|
||||
const status = connectionState?.status || "connecting";
|
||||
const labels = {
|
||||
connecting: "正在连接服务",
|
||||
online: "服务已连接",
|
||||
degraded: "连接不稳定",
|
||||
offline: "服务已断开",
|
||||
};
|
||||
connectionText.textContent = labels[status] || labels.connecting;
|
||||
connectionText.title = connectionState?.lastError || connectionText.textContent;
|
||||
$(".v2-sidebar-footer")?.classList.toggle(
|
||||
"offline",
|
||||
status === "offline" || status === "degraded",
|
||||
);
|
||||
}
|
||||
keepRunButtonAvailable();
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
$$('[data-v2-control]').forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
setControlMode(button.dataset.v2Control);
|
||||
});
|
||||
});
|
||||
$$('.v2-module-tab').forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
if (!button.disabled) setModule(button.dataset.module);
|
||||
});
|
||||
});
|
||||
$("#v2ExecutionSelect")?.addEventListener("change", (event) => {
|
||||
$(`[data-execution-mode="${event.target.value}"]`)?.click();
|
||||
syncExecutionSelector();
|
||||
});
|
||||
$$(".v2-page-button").forEach((button) => {
|
||||
button.addEventListener("click", () => setPage(button.dataset.v2PageTarget));
|
||||
});
|
||||
$("#v2LogoutButton")?.addEventListener("click", () => $("#logoutButton")?.click());
|
||||
window.addEventListener("cognitive-agent:connection", (event) => {
|
||||
if (event.detail?.status === "online") refreshVehicleLinkState();
|
||||
else if (["offline", "degraded"].includes(event.detail?.status)) setVehicleLinkState(false);
|
||||
});
|
||||
$("#v2LogoutButton")?.addEventListener(
|
||||
"click",
|
||||
() => window.CognitiveAgentConsole?.logout?.(),
|
||||
);
|
||||
$("#v2SidebarToggle")?.addEventListener(
|
||||
"click",
|
||||
() => setSidebarCollapsed(!ui.sidebarCollapsed),
|
||||
);
|
||||
document.addEventListener("click", () => {
|
||||
closeCustomDropdowns();
|
||||
closeReasoningPicker();
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
closeCustomDropdowns();
|
||||
closeReasoningPicker();
|
||||
clearTwinDevicePreview({ force: true });
|
||||
}
|
||||
@@ -1583,8 +1629,11 @@
|
||||
.observe(modeChips, { childList: true });
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(syncRuntimePresentation);
|
||||
[$("#modelBadge"), $("#connectionBadge"), $("#hardwareMode"), $("#simulationMode")]
|
||||
window.addEventListener("cognitive-agent:connection", (event) => {
|
||||
syncRuntimePresentation(event.detail);
|
||||
});
|
||||
const observer = new MutationObserver(() => keepRunButtonAvailable());
|
||||
[$("#hardwareMode"), $("#simulationMode")]
|
||||
.filter(Boolean)
|
||||
.forEach((node) => observer.observe(node, {
|
||||
attributes: true,
|
||||
@@ -1604,8 +1653,6 @@
|
||||
document.body.classList.add("console-v2");
|
||||
const panelHeading = $(".panel-heading", commandPanel);
|
||||
const modeRail = $(".mode-rail", commandPanel);
|
||||
const exampleGroup = $(".example-group", commandPanel);
|
||||
const safetyStrip = $("#safetyStrip", commandPanel);
|
||||
const requestHeader = $(".request-header", mainPanel);
|
||||
const gestureDrawer = $("#gestureDrawer");
|
||||
|
||||
@@ -1615,18 +1662,11 @@
|
||||
requestHeader,
|
||||
gestureDrawer,
|
||||
originalForm,
|
||||
exampleGroup,
|
||||
safetyStrip,
|
||||
};
|
||||
Object.values(detached).filter(Boolean).forEach((node) => node.remove());
|
||||
|
||||
const legacyParking = make("div", "v2-legacy-parking v2-legacy-hidden", "");
|
||||
[exampleGroup, safetyStrip].filter(Boolean).forEach((node) => legacyParking.append(node));
|
||||
document.body.append(legacyParking);
|
||||
|
||||
const configStack = buildSidebar(commandPanel);
|
||||
buildConfigFields(configStack, originalForm);
|
||||
enhanceConfiguredDropdowns();
|
||||
buildSidebar(commandPanel);
|
||||
prepareRuntimeControls(originalForm);
|
||||
buildOperation(
|
||||
mainPanel,
|
||||
originalForm,
|
||||
@@ -1636,9 +1676,6 @@
|
||||
gestureDrawer,
|
||||
);
|
||||
buildModuleSwitcher(mainPanel);
|
||||
$(".pipeline", mainPanel)?.classList.add("v2-legacy-hidden");
|
||||
$(".topbar")?.classList.add("v2-legacy-hidden");
|
||||
$(".gesture-backdrop")?.classList.add("v2-legacy-hidden");
|
||||
buildOutput(mainPanel);
|
||||
buildSyncControlLayer();
|
||||
bindEvents();
|
||||
@@ -1647,18 +1684,18 @@
|
||||
setSidebarCollapsed(ui.sidebarCollapsed);
|
||||
setPage(0);
|
||||
syncRuntimePresentation();
|
||||
refreshVehicleLinkState();
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
requestAnimationFrame(() => {
|
||||
updateModeEdgeHints();
|
||||
document.body.classList.add("v2-ready");
|
||||
window.setTimeout(() => $("#v2BootScreen")?.remove(), 220);
|
||||
});
|
||||
}
|
||||
|
||||
window.CognitiveAgentConsoleV2 = {
|
||||
enhanceSelect,
|
||||
getModule: () => ui.activeModule,
|
||||
getRole: () => "user",
|
||||
getParsedCanCommands: (result) => parsedCanCommands(result).map((item) => ({ ...item })),
|
||||
setModule,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user