feat: integrate production cockpit AI assistant

This commit is contained in:
vee
2026-09-08 17:46:07 +08:00
parent 5e228e12ae
commit 93103c2579
66 changed files with 11046 additions and 374 deletions
+337 -20
View File
@@ -4779,10 +4779,17 @@ function modeSecondsLabel(durationMs) {
}
const MODE_GALLERY_GROUPS = [
{
id: "custom",
title: "自定义模式",
description: "由用户创建或调整,可继续编辑与删除",
icon: "wand-sparkles",
preferredIds: [],
},
{
id: "static",
title: "静态模式",
description: "一次执行并保持目标状态,包含预设与自定义模式",
description: "系统预设,一次执行并保持目标状态",
icon: "layout-grid",
preferredIds: [
"zero_gravity",
@@ -4820,6 +4827,126 @@ const MODE_DOMAIN_LABELS = {
vehicle_status: "车辆状态",
};
const MODE_TARGET_LABELS = {
driver: "主驾",
passenger: "副驾",
front_left: "左前",
front_right: "右前 / 副驾",
rear_left: "左后",
rear_right: "右后",
front_row: "前排",
rear_row: "后排",
vehicle: "全车",
cabin: "全舱",
all: "全部",
};
const MODE_TIMELINE_TARGET_LABELS = {
longitudinal: "前后位置",
cushion_tilt: "坐垫角度",
backrest_recline: "靠背角度",
legrest_height: "腿托高度",
legrest_extension: "腿托伸缩",
};
function modeOperationParameters(operation) {
const parameters = operation?.parameters;
if (Array.isArray(parameters)) {
return parameters
.filter((item) => item && typeof item === "object" && item.name)
.map((item) => [String(item.name), item.value]);
}
if (!parameters || typeof parameters !== "object") return [];
return Object.entries(parameters);
}
function formatModeOperationValue(name, value) {
const valueLabels = {
on: "开启",
off: "关闭",
open: "打开",
close: "关闭",
stop: "停止",
internal: "内循环",
external: "外循环",
};
if (typeof value === "string" && valueLabels[value]) return valueLabels[value];
return formatConfigValue(name, value);
}
function modeOperationView(operation) {
const domain = String(operation?.domain || "");
const capability = String(operation?.capability || operation?.operation || "执行动作");
const scope = String(
operation?.target_scope
|| operation?.target?.instance
|| operation?.target?.zone
|| operation?.target
|| "vehicle",
);
const parameters = modeOperationParameters(operation)
.map(([name, value]) => {
const label = CONFIG_PARAMETER_LABELS[name] || name;
return `${label}:${formatModeOperationValue(name, value)}`;
})
.join(",");
return {
device: MODE_DOMAIN_LABELS[domain] || domain || "车端设备",
target: MODE_TARGET_LABELS[scope] || scope || "全车",
action: CONFIG_CAPABILITY_LABELS[capability] || capability,
parameters: parameters || "无附加参数",
};
}
function modeTimelineTargetSummary(targets) {
if (!targets || typeof targets !== "object") return "按模式目标运行";
return Object.entries(targets).map(([motor, target]) => {
const label = MODE_TIMELINE_TARGET_LABELS[motor] || motor;
if (!target || typeof target !== "object") return `${label}:${target}`;
if (target.position_pct != null) return `${label}:${target.position_pct}%`;
if (target.angle_deg != null) return `${label}:${target.angle_deg}°`;
if (target.hall_pulse != null) return `${label}:${target.hall_pulse} Hall`;
return `${label}:${Object.values(target).join(" / ")}`;
}).join(",");
}
function modeTimelineStepView(step, action) {
const timeout = Number(step?.timeout_ms || 0);
const duration = Number(step?.duration_ms || 0);
const timing = duration > 0
? `持续 ${modeSecondsLabel(duration)}s`
: timeout > 0
? `目标到位,保护上限 ${modeSecondsLabel(timeout)}s`
: "目标到位";
return {
device: "座椅",
target: "副驾",
action,
parameters: `${modeTimelineTargetSummary(step?.targets)};${timing}`,
};
}
function modeExecutionActionViews(mode) {
const operations = Array.isArray(mode?.operations)
? mode.operations.map(modeOperationView)
: [];
const timeline = mode?.timeline;
if (!timeline || typeof timeline !== "object") return operations;
const timelineSteps = [];
if (timeline.entry?.targets) {
timelineSteps.push(modeTimelineStepView(timeline.entry, "进入动态模式初始位置"));
}
if (Array.isArray(timeline.phases)) {
timeline.phases.forEach((phase, index) => {
timelineSteps.push(modeTimelineStepView(
phase,
`循环阶段 ${index + 1}:${phase?.name || phase?.id || "动态调节"}`,
));
});
}
return [...operations, ...timelineSteps];
}
function isUserModeProfile(mode) {
return mode?.origin === "user" || mode?.deletable === true;
}
@@ -4853,9 +4980,11 @@ function modeGalleryBuckets() {
...definition,
modes: orderModeGalleryProfiles(
modes.filter((mode) => (
definition.id === "dynamic"
? isDynamicModeProfile(mode)
: !isDynamicModeProfile(mode)
definition.id === "custom"
? isUserModeProfile(mode)
: definition.id === "dynamic"
? !isUserModeProfile(mode) && isDynamicModeProfile(mode)
: !isUserModeProfile(mode) && !isDynamicModeProfile(mode)
)),
definition.preferredIds,
),
@@ -4978,14 +5107,20 @@ function createModeGalleryEmpty(group) {
empty.dataset.modeGroupEmpty = group.id;
empty.setAttribute("role", "status");
const icon = document.createElement("i");
icon.dataset.lucide = group.id === "dynamic" ? "activity" : "layout-grid";
icon.dataset.lucide = group.id === "custom"
? "wand-sparkles"
: group.id === "dynamic"
? "activity"
: "layout-grid";
const copy = document.createElement("span");
const title = document.createElement("strong");
title.textContent = `暂无${group.title}`;
const description = document.createElement("small");
description.textContent = group.id === "dynamic"
? "支持持续变化的模式会显示在这里"
: "可通过 AI助手创建新的静态模式";
description.textContent = group.id === "custom"
? "可通过 AI助手创建新的功能模式"
: group.id === "dynamic"
? "支持持续变化的系统模式会显示在这里"
: "系统静态预设会显示在这里";
copy.append(title, description);
empty.append(icon, copy);
return empty;
@@ -5020,7 +5155,7 @@ function populateModeGallery() {
const grid = document.createElement("div");
grid.className = "mode-gallery-grid";
if (group.id === "static") grid.append(createModeGalleryAddCard());
if (group.id === "custom") grid.append(createModeGalleryAddCard());
group.modes.forEach((mode) => grid.append(createModeGalleryCard(mode)));
if (!group.modes.length) grid.append(createModeGalleryEmpty(group));
section.append(grid);
@@ -5042,6 +5177,7 @@ function setModeRailExpanded(expanded) {
toggle.title = open ? "收起功能模式" : "展开功能模式";
content.hidden = !open;
$(".mode-rail")?.classList.toggle("is-collapsed", !open);
if (!open) hideModeActionPopover({ immediate: true });
const icon = $("i", toggle);
if (icon) icon.dataset.lucide = open ? "chevron-up" : "chevron-down";
refreshIcons();
@@ -5816,6 +5952,168 @@ function installModeChipScroller() {
window.requestAnimationFrame(updateScrollState);
}
let modeActionPopoverHideTimer = null;
let modeActionPopoverAnchor = null;
function ensureModeActionPopover() {
let popover = $("#modeChipActionPopover");
if (popover) return popover;
popover = document.createElement("aside");
popover.id = "modeChipActionPopover";
popover.className = "mode-chip-action-popover";
popover.setAttribute("role", "tooltip");
popover.hidden = true;
const header = document.createElement("header");
const headingCopy = document.createElement("span");
const eyebrow = document.createElement("small");
eyebrow.textContent = "执行动作预览";
const title = document.createElement("strong");
title.dataset.modeActionTitle = "true";
headingCopy.append(eyebrow, title);
const count = document.createElement("b");
count.dataset.modeActionCount = "true";
header.append(headingCopy, count);
const list = document.createElement("ol");
list.dataset.modeActionList = "true";
popover.append(header, list);
document.body.append(popover);
return popover;
}
function renderModeActionPopover(mode) {
const popover = ensureModeActionPopover();
const actions = modeExecutionActionViews(mode);
$("[data-mode-action-title]", popover).textContent = mode?.name || mode?.id || "功能模式";
$("[data-mode-action-count]", popover).textContent = actions.length
? `${actions.length} 步`
: `${Number(mode?.operation_count || 0)} 项`;
const list = $("[data-mode-action-list]", popover);
list.replaceChildren();
if (!actions.length) {
const empty = document.createElement("li");
empty.className = "mode-chip-action-empty";
empty.textContent = "执行动作明细暂未加载,请刷新后重试。";
list.append(empty);
return popover;
}
actions.forEach((action, index) => {
const item = document.createElement("li");
const step = document.createElement("span");
step.className = "mode-chip-action-step";
step.textContent = String(index + 1).padStart(2, "0");
const copy = document.createElement("span");
copy.className = "mode-chip-action-copy";
const actionName = document.createElement("strong");
actionName.textContent = action.action;
const metadata = document.createElement("small");
const device = document.createElement("span");
device.innerHTML = "<b>设备</b>";
device.append(document.createTextNode(action.device));
const target = document.createElement("span");
target.innerHTML = "<b>目标</b>";
target.append(document.createTextNode(action.target));
const parameters = document.createElement("span");
parameters.innerHTML = "<b>参数</b>";
parameters.append(document.createTextNode(action.parameters));
metadata.append(device, target, parameters);
copy.append(actionName, metadata);
item.append(step, copy);
list.append(item);
});
return popover;
}
function positionModeActionPopover(anchor, popover) {
const margin = 12;
const gap = 8;
const anchorRect = anchor.getBoundingClientRect();
const popoverRect = popover.getBoundingClientRect();
const maximumLeft = Math.max(margin, window.innerWidth - popoverRect.width - margin);
const left = Math.max(
margin,
Math.min(maximumLeft, anchorRect.left + (anchorRect.width - popoverRect.width) / 2),
);
const availableBelow = window.innerHeight - anchorRect.bottom - margin;
const availableAbove = anchorRect.top - margin;
const placeAbove = availableBelow < Math.min(popoverRect.height, 320)
&& availableAbove > availableBelow;
const desiredTop = placeAbove
? anchorRect.top - popoverRect.height - gap
: anchorRect.bottom + gap;
const maximumTop = Math.max(margin, window.innerHeight - popoverRect.height - margin);
popover.style.left = `${Math.max(margin, Math.min(maximumLeft, left))}px`;
popover.style.top = `${Math.max(margin, Math.min(maximumTop, desiredTop))}px`;
popover.classList.toggle("is-above", placeAbove);
}
function showModeActionPopover(button) {
const mode = modeProfiles().find((item) => item.id === button?.dataset.modeProfileId);
if (!mode) return;
window.clearTimeout(modeActionPopoverHideTimer);
modeActionPopoverHideTimer = null;
const popover = renderModeActionPopover(mode);
modeActionPopoverAnchor = button;
button.setAttribute("aria-describedby", popover.id);
popover.hidden = false;
window.requestAnimationFrame(() => {
if (!popover.hidden && modeActionPopoverAnchor === button) {
positionModeActionPopover(button, popover);
}
});
}
function hideModeActionPopover({ immediate = false } = {}) {
window.clearTimeout(modeActionPopoverHideTimer);
const hide = () => {
const popover = $("#modeChipActionPopover");
if (popover) popover.hidden = true;
modeActionPopoverAnchor = null;
modeActionPopoverHideTimer = null;
};
if (immediate) hide();
else modeActionPopoverHideTimer = window.setTimeout(hide, 90);
}
function installModeActionPreview() {
const group = $(".mode-chips");
if (!group || group.dataset.modeActionPreviewBound === "true") return;
group.dataset.modeActionPreviewBound = "true";
const resolveButton = (event) => {
const target = event.target instanceof Element ? event.target : null;
return target?.closest("button[data-mode-profile-id]");
};
group.addEventListener("pointerover", (event) => {
const button = resolveButton(event);
if (button && !button.contains(event.relatedTarget)) showModeActionPopover(button);
});
group.addEventListener("pointerout", (event) => {
const button = resolveButton(event);
if (button && !button.contains(event.relatedTarget)) hideModeActionPopover();
});
group.addEventListener("focusin", (event) => {
const button = resolveButton(event);
if (button) showModeActionPopover(button);
});
group.addEventListener("focusout", (event) => {
const button = resolveButton(event);
if (button && !button.contains(event.relatedTarget)) hideModeActionPopover();
});
group.addEventListener("scroll", () => hideModeActionPopover({ immediate: true }), { passive: true });
group.addEventListener("pointerdown", () => hideModeActionPopover({ immediate: true }));
group.addEventListener("keydown", (event) => {
if (event.key === "Escape") hideModeActionPopover({ immediate: true });
});
const popover = ensureModeActionPopover();
popover.addEventListener("pointerenter", () => {
window.clearTimeout(modeActionPopoverHideTimer);
modeActionPopoverHideTimer = null;
});
popover.addEventListener("pointerleave", () => hideModeActionPopover());
window.addEventListener("resize", () => hideModeActionPopover({ immediate: true }), { passive: true });
}
function populateModeChips() {
const group = $(".mode-chips");
if (!group) return;
@@ -5857,6 +6155,12 @@ function populateModeChips() {
button.dataset.modeCommand = `进入${mode.name}`;
button.dataset.modeOrigin = mode?.origin || "preset";
button.setAttribute("aria-pressed", "false");
const actionCount = modeExecutionActionViews(mode).length
|| Number(mode.operation_count || 0);
button.setAttribute(
"aria-label",
`${mode.name || mode.id},${actionCount} 项执行动作,点击执行`,
);
let summary = $(".mode-chip-summary", button);
if (!summary) {
summary = document.createElement("small");
@@ -5905,7 +6209,9 @@ function populateModeChips() {
: `${phase.name} 目标到位`;
}).join(" → ")
: "";
button.title = [mode.description || mode.name, phaseSummary].filter(Boolean).join("\n");
button.title = [mode.description || mode.name, phaseSummary, "悬浮或聚焦查看全部执行动作"]
.filter(Boolean)
.join("\n");
});
const preferredStaticOrder = [
@@ -5915,12 +6221,20 @@ function populateModeChips() {
"queen_passenger_driving",
"leisure_lounger",
];
const dynamicModeOrder = ["rocking_horse", "dynamic_cradle"];
const allButtons = buttons();
const userButtons = allButtons.filter((button) => button.dataset.modeOrigin === "user");
const dynamicButtons = dynamicModeOrder
.map((modeId) => allButtons.find((button) => button.dataset.modeProfileId === modeId))
.filter(Boolean);
const profilesById = new Map(modes.map((mode) => [String(mode.id), mode]));
const userButtons = allButtons.filter((button) => {
const mode = profilesById.get(String(button.dataset.modeProfileId || ""));
return mode ? isUserModeProfile(mode) : button.dataset.modeOrigin === "user";
});
const dynamicOrder = new Map([["rocking_horse", 0], ["dynamic_cradle", 1]]);
const dynamicButtons = allButtons.filter((button) => {
const mode = profilesById.get(String(button.dataset.modeProfileId || ""));
return mode && !isUserModeProfile(mode) && isDynamicModeProfile(mode);
}).sort((left, right) => (
(dynamicOrder.get(left.dataset.modeProfileId) ?? Number.MAX_SAFE_INTEGER)
- (dynamicOrder.get(right.dataset.modeProfileId) ?? Number.MAX_SAFE_INTEGER)
));
const staticButtons = allButtons.filter(
(button) => !userButtons.includes(button) && !dynamicButtons.includes(button),
);
@@ -5946,15 +6260,17 @@ function populateModeChips() {
group.append(divider);
};
if (userButtons.length) {
appendGroupLabel("我的模式", "user");
appendGroupLabel("自定义模式", "custom");
userButtons.forEach((button) => group.append(button));
}
if (userButtons.length && staticButtons.length) appendDivider("user");
if (staticButtons.length) appendGroupLabel("预设模式", "preset");
if (userButtons.length && (staticButtons.length || dynamicButtons.length)) {
appendDivider("custom");
}
if (staticButtons.length) appendGroupLabel("静态模式", "static");
preferredStaticButtons.forEach((button) => group.append(button));
otherStaticButtons.forEach((button) => group.append(button));
if (dynamicButtons.length && staticButtons.length) {
appendDivider("dynamic");
if (dynamicButtons.length) {
if (userButtons.length || staticButtons.length) appendDivider("dynamic");
appendGroupLabel("动态模式", "dynamic");
}
dynamicButtons.forEach((button) => group.append(button));
@@ -8423,6 +8739,7 @@ document.addEventListener("DOMContentLoaded", async () => {
selectVehicle(event.target.value);
});
installModeChipScroller();
installModeActionPreview();
$(".mode-chips").addEventListener("click", (event) => {
const target = event.target instanceof Element ? event.target : null;
const button = target?.closest("button[data-mode-command]");