feat(n7web): enhance control workflows and status feedback
This commit is contained in:
+48
-16
@@ -6136,6 +6136,7 @@ function ensureModeActionPopover() {
|
||||
const header = document.createElement("header");
|
||||
const headingCopy = document.createElement("span");
|
||||
const eyebrow = document.createElement("small");
|
||||
eyebrow.dataset.modeActionEyebrow = "true";
|
||||
eyebrow.textContent = "执行动作预览";
|
||||
const title = document.createElement("strong");
|
||||
title.dataset.modeActionTitle = "true";
|
||||
@@ -6153,7 +6154,12 @@ function ensureModeActionPopover() {
|
||||
|
||||
function renderModeActionPopover(mode) {
|
||||
const popover = ensureModeActionPopover();
|
||||
const actions = modeExecutionActionViews(mode);
|
||||
const commandFlow = modeEditorReadableCommands(mode?.command_flow);
|
||||
const usesLatestCommandFlow = commandFlow.length > 0;
|
||||
const actions = usesLatestCommandFlow ? commandFlow : modeExecutionActionViews(mode);
|
||||
$(`[data-mode-action-eyebrow]`, popover).textContent = usesLatestCommandFlow
|
||||
? "最新控制指令流"
|
||||
: "执行动作预览";
|
||||
$("[data-mode-action-title]", popover).textContent = mode?.name || mode?.id || "功能模式";
|
||||
$("[data-mode-action-count]", popover).textContent = actions.length
|
||||
? `${actions.length} 步`
|
||||
@@ -6163,7 +6169,7 @@ function renderModeActionPopover(mode) {
|
||||
if (!actions.length) {
|
||||
const empty = document.createElement("li");
|
||||
empty.className = "mode-chip-action-empty";
|
||||
empty.textContent = "执行动作明细暂未加载,请刷新后重试。";
|
||||
empty.textContent = "控制指令流暂未加载,请刷新后重试。";
|
||||
list.append(empty);
|
||||
return popover;
|
||||
}
|
||||
@@ -6175,18 +6181,28 @@ function renderModeActionPopover(mode) {
|
||||
const copy = document.createElement("span");
|
||||
copy.className = "mode-chip-action-copy";
|
||||
const actionName = document.createElement("strong");
|
||||
actionName.textContent = action.action;
|
||||
actionName.textContent = usesLatestCommandFlow
|
||||
? String(action.category || "控制指令")
|
||||
: 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);
|
||||
const appendMetadata = (label, value) => {
|
||||
const text = String(value || "").trim();
|
||||
if (!text) return;
|
||||
const item = document.createElement("span");
|
||||
const key = document.createElement("b");
|
||||
key.textContent = label;
|
||||
item.append(key, document.createTextNode(text));
|
||||
metadata.append(item);
|
||||
};
|
||||
if (usesLatestCommandFlow) {
|
||||
appendMetadata("语义", action.semantic || action.operation_ref);
|
||||
appendMetadata("实际", action.command);
|
||||
appendMetadata("通道", action.channel);
|
||||
} else {
|
||||
appendMetadata("设备", action.device);
|
||||
appendMetadata("目标", action.target);
|
||||
appendMetadata("参数", action.parameters);
|
||||
}
|
||||
copy.append(actionName, metadata);
|
||||
item.append(step, copy);
|
||||
list.append(item);
|
||||
@@ -6642,10 +6658,13 @@ async function executeSavedModeProfile(mode, { command = "" } = {}) {
|
||||
closeModeGallery({ restoreFocus: false });
|
||||
const runButton = $("#runButton");
|
||||
const input = $("#utterance");
|
||||
const assistantModeHandle = assistant?.beginModeExecution?.(mode, {
|
||||
command: modeCommand,
|
||||
}) || null;
|
||||
runButton.disabled = true;
|
||||
setModeChipsDisabled(true);
|
||||
setModeExecutionState(mode.id, "running");
|
||||
assistant?.open?.();
|
||||
if (!assistantModeHandle) assistant?.open?.();
|
||||
input.value = modeCommand;
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
setRunStatus("processing", "MODE COMMAND FLOW");
|
||||
@@ -6677,7 +6696,13 @@ async function executeSavedModeProfile(mode, { command = "" } = {}) {
|
||||
? `${mode.name}执行完成`
|
||||
: `${mode.name}执行未完成`;
|
||||
setModeExecutionState(mode.id, completed ? "succeeded" : "failed");
|
||||
assistant?.announceModeExecution?.(mode, payload, { command: modeCommand });
|
||||
if (assistantModeHandle && assistant?.completeModeExecution) {
|
||||
assistant.completeModeExecution(assistantModeHandle, mode, payload, {
|
||||
command: modeCommand,
|
||||
});
|
||||
} else {
|
||||
assistant?.announceModeExecution?.(mode, payload, { command: modeCommand });
|
||||
}
|
||||
if (state.executionMode === "hardware") await synchronizePhysicalSeatState();
|
||||
showToast(completed
|
||||
? `${mode.name}已直接执行保存的控制指令流`
|
||||
@@ -6686,7 +6711,14 @@ async function executeSavedModeProfile(mode, { command = "" } = {}) {
|
||||
setModeExecutionState(mode.id, "failed");
|
||||
setRunStatus("failed", "MODE FAILED");
|
||||
$("#resultTitle").textContent = `${mode.name}执行失败`;
|
||||
assistant?.announceModeExecution?.(mode, null, { command: modeCommand, error });
|
||||
if (assistantModeHandle && assistant?.completeModeExecution) {
|
||||
assistant.completeModeExecution(assistantModeHandle, mode, null, {
|
||||
command: modeCommand,
|
||||
error,
|
||||
});
|
||||
} else {
|
||||
assistant?.announceModeExecution?.(mode, null, { command: modeCommand, error });
|
||||
}
|
||||
showToast(error.message || "模式执行失败");
|
||||
} finally {
|
||||
if (input.value === modeCommand) {
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
bottom: auto;
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
margin-left: 10px;
|
||||
margin-left: 0;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 14px 38px rgba(25, 44, 82, 0.12);
|
||||
transform-origin: left center;
|
||||
|
||||
@@ -544,7 +544,27 @@
|
||||
if (capability.startsWith("seat.")) {
|
||||
const target = String(operation.target?.zone || operation.target?.instance || "")
|
||||
.replaceAll("_", "-");
|
||||
return `${["passenger", "front-right"].includes(target) ? "副驾" : "主驾"}座椅`;
|
||||
const seat = ["passenger", "front-right"].includes(target) ? "副驾座椅" : "主驾座椅";
|
||||
const component = {
|
||||
"seat.set_longitudinal_position": "前后位置",
|
||||
"seat.set_cushion_tilt": "坐盆",
|
||||
"seat.set_backrest_recline": "靠背",
|
||||
"seat.set_legrest_height": "腿托",
|
||||
"seat.set_legrest_extension": "腿托",
|
||||
}[capability];
|
||||
if (component) return `${seat}${component}`;
|
||||
if (capability === "seat.set_driver_axis_native") {
|
||||
const axis = String(operation.parameters?.axis || "").replaceAll("_", "-");
|
||||
const axisComponent = {
|
||||
horizontal: "前后位置",
|
||||
height: "坐盆",
|
||||
basin: "坐盆",
|
||||
backrest: "靠背",
|
||||
legrest: "腿托",
|
||||
}[axis];
|
||||
if (axisComponent) return `${seat}${axisComponent}`;
|
||||
}
|
||||
return seat;
|
||||
}
|
||||
if (capability.startsWith("outlet.")) return "空调风口";
|
||||
return String(fallback || capability || "控制指令");
|
||||
@@ -589,6 +609,24 @@
|
||||
String(presentation?.operation?.id || ""),
|
||||
presentation?.operation || {},
|
||||
]));
|
||||
const operationByRef = new Map();
|
||||
const configurations = result.function_configs && typeof result.function_configs === "object"
|
||||
? Object.values(result.function_configs)
|
||||
: [];
|
||||
if (result.function_config && typeof result.function_config === "object") {
|
||||
configurations.push(result.function_config);
|
||||
}
|
||||
configurations.forEach((configuration) => {
|
||||
const operations = Array.isArray(configuration?.spec?.operations)
|
||||
? configuration.spec.operations
|
||||
: [];
|
||||
operations.forEach((operation) => {
|
||||
const operationRef = String(operation?.id || "");
|
||||
if (operationRef && !operationByRef.has(operationRef)) {
|
||||
operationByRef.set(operationRef, operation);
|
||||
}
|
||||
});
|
||||
});
|
||||
let commands = [];
|
||||
try {
|
||||
const sharedCommands = window.CognitiveAgentConsoleV2?.getParsedCanCommands?.(result);
|
||||
@@ -599,10 +637,19 @@
|
||||
if (!commands.length) return fallbackAssistantControlCommands(result);
|
||||
return commands.map((item, index) => {
|
||||
const operation = projectionByRef.get(String(item.operationRef || ""))
|
||||
|| operationByRef.get(String(item.operationRef || ""))
|
||||
|| projections[index]?.operation
|
||||
|| {};
|
||||
const pendingDelivery = result.status === "processing"
|
||||
? {
|
||||
deliveryState: "submitting",
|
||||
deliveryLabel: "正在下发",
|
||||
deliveryDetail: "",
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
...item,
|
||||
...pendingDelivery,
|
||||
category: assistantControlCategory(operation, item.category),
|
||||
operation,
|
||||
};
|
||||
@@ -1459,14 +1506,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
function announceModeExecution(profile, result = null, { command = "", error = null } = {}) {
|
||||
function modeExecutionPreviewResult(profile) {
|
||||
const flow = profile?.command_flow && typeof profile.command_flow === "object"
|
||||
? profile.command_flow
|
||||
: {};
|
||||
return {
|
||||
status: "processing",
|
||||
function_configs: flow.function_configs && typeof flow.function_configs === "object"
|
||||
? flow.function_configs
|
||||
: {},
|
||||
command_plans: flow.plans && typeof flow.plans === "object" ? flow.plans : {},
|
||||
control_transport: String(flow.control_transport || ""),
|
||||
execution_report: null,
|
||||
operation_results: [],
|
||||
};
|
||||
}
|
||||
|
||||
function beginModeExecution(profile, { command = "" } = {}) {
|
||||
const modeName = String(profile?.name || profile?.id || "功能模式");
|
||||
const commandText = String(command || `进入${modeName}`).trim();
|
||||
setDrawerOpen(true);
|
||||
createMessage("user", commandText);
|
||||
const message = createMessage("assistant", `已加载${modeName}控制指令流,正在下发。`);
|
||||
setActionStatus(message, "模式控制指令正在下发", "working");
|
||||
appendDispatchedControls(message, modeExecutionPreviewResult(profile));
|
||||
persistMessages();
|
||||
return {
|
||||
message,
|
||||
modeName,
|
||||
commandText,
|
||||
profileId: String(profile?.id || ""),
|
||||
};
|
||||
}
|
||||
|
||||
function completeModeExecution(
|
||||
handle,
|
||||
profile,
|
||||
result = null,
|
||||
{ command = "", error = null } = {},
|
||||
) {
|
||||
const modeName = String(handle?.modeName || profile?.name || profile?.id || "功能模式");
|
||||
const commandText = String(handle?.commandText || command || `进入${modeName}`).trim();
|
||||
const activeHandle = handle?.message?.body
|
||||
? handle
|
||||
: beginModeExecution(profile, { command: commandText });
|
||||
const message = activeHandle.message;
|
||||
const status = String(result?.status || "");
|
||||
const dryRun = result?.execution_mode === "dry_run";
|
||||
const waitingForFeedback = result?.execution_outcome === "submitted_unverified";
|
||||
setDrawerOpen(true);
|
||||
createMessage("user", commandText);
|
||||
|
||||
let content = `${modeName}执行未完成,请查看控制指令状态。`;
|
||||
let statusText = "功能模式执行未完成";
|
||||
@@ -1491,10 +1578,16 @@
|
||||
statusText = "功能模式部分完成";
|
||||
}
|
||||
|
||||
const message = createMessage("assistant", content);
|
||||
setMessageContent(message, content, { final: true });
|
||||
setActionStatus(message, statusText, statusState);
|
||||
if (result) appendDispatchedControls(message, result);
|
||||
persistMessages();
|
||||
return activeHandle;
|
||||
}
|
||||
|
||||
function announceModeExecution(profile, result = null, { command = "", error = null } = {}) {
|
||||
const handle = beginModeExecution(profile, { command });
|
||||
return completeModeExecution(handle, profile, result, { command, error });
|
||||
}
|
||||
|
||||
function renderInitialMessages() {
|
||||
@@ -1584,6 +1677,8 @@
|
||||
close: () => setDrawerOpen(false),
|
||||
resetConversation,
|
||||
startModeCreation,
|
||||
beginModeExecution,
|
||||
completeModeExecution,
|
||||
announceModeExecution,
|
||||
announceModeCreated(profile) {
|
||||
if (!profile?.id) return;
|
||||
|
||||
@@ -85,6 +85,12 @@ body.console-v2 {
|
||||
--glass-highlight: oklch(1 0 0 / 0.14);
|
||||
--glass-shadow: 0 24px 70px oklch(0.06 0.04 285 / 0.42);
|
||||
--glass-panel-shadow: 0 12px 32px oklch(0.05 0.035 285 / 0.24);
|
||||
--glass-card-radius: 14px;
|
||||
--glass-card-lift: -3px;
|
||||
--glass-card-rest-shadow: inset 0 1px 0 var(--glass-highlight), 0 8px 22px color-mix(in oklch, var(--foreground) 10%, transparent);
|
||||
--glass-card-float-shadow: inset 0 1px 0 var(--glass-highlight), 0 16px 34px color-mix(in oklch, var(--foreground) 18%, transparent);
|
||||
--glass-dialog-scrim: oklch(0.035 0.025 286 / 0.82);
|
||||
--glass-dialog-scrim-filter: blur(18px) saturate(0.78) brightness(0.58);
|
||||
min-width: 1180px;
|
||||
min-height: 620px;
|
||||
overflow: hidden;
|
||||
@@ -126,6 +132,8 @@ html[data-glass-theme="light"] body.console-v2 {
|
||||
--sidebar-accent-foreground: var(--foreground);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--ring);
|
||||
--glass-dialog-scrim: oklch(0.26 0.035 252 / 0.34);
|
||||
--glass-dialog-scrim-filter: blur(14px) saturate(0.9) brightness(0.92);
|
||||
--heading-foreground: oklch(0.235 0.068 255);
|
||||
--body-foreground: var(--card-foreground);
|
||||
--subtle-foreground: var(--muted-foreground);
|
||||
@@ -438,14 +446,14 @@ body.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-s
|
||||
linear-gradient(160deg, oklch(0.18 0.05 285 / 0.2), oklch(0.09 0.03 286 / 0.32)) !important;
|
||||
}
|
||||
|
||||
body.console-v2 .joint-twin-shell :is(.twin-seat-switch, .twin-seat-switch button, .twin-reset, .twin-focus-badge) {
|
||||
body.console-v2 .joint-twin-shell :is(.twin-selector, .twin-selector button, .twin-reset, .twin-focus-badge) {
|
||||
color: var(--muted-foreground) !important;
|
||||
background: var(--glass-card) !important;
|
||||
border-color: var(--glass-line) !important;
|
||||
box-shadow: inset 0 1px 0 var(--glass-highlight) !important;
|
||||
}
|
||||
|
||||
body.console-v2 .joint-twin-shell .twin-seat-switch button.active,
|
||||
body.console-v2 .joint-twin-shell .twin-selector button.active,
|
||||
body.console-v2 .joint-twin-shell :is(.twin-reset, .twin-focus-badge):is(:hover, :focus-visible) {
|
||||
color: var(--primary-foreground) !important;
|
||||
background: color-mix(in oklch, var(--primary) 72%, transparent) !important;
|
||||
@@ -588,12 +596,18 @@ body.console-v2 :is(.v2-extension-linkage, .v2-sidebar-footer) {
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-backdrop,
|
||||
.mode-editor-backdrop,
|
||||
.v2-extension-dialog::backdrop,
|
||||
.energy-monitor-layer
|
||||
.energy-monitor-layer,
|
||||
.device-power-backdrop
|
||||
) {
|
||||
background: oklch(0.055 0.025 286 / 0.62) !important;
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.12);
|
||||
backdrop-filter: blur(14px) saturate(1.12);
|
||||
background: var(--glass-dialog-scrim) !important;
|
||||
-webkit-backdrop-filter: var(--glass-dialog-scrim-filter) !important;
|
||||
backdrop-filter: var(--glass-dialog-scrim-filter) !important;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-extension-dialog::backdrop {
|
||||
background: var(--glass-dialog-scrim) !important;
|
||||
-webkit-backdrop-filter: var(--glass-dialog-scrim-filter) !important;
|
||||
backdrop-filter: var(--glass-dialog-scrim-filter) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
@@ -631,6 +645,98 @@ body.console-v2 :is(
|
||||
box-shadow: inset 0 1px 0 var(--glass-highlight), 0 9px 24px oklch(0.04 0.03 286 / 0.2) !important;
|
||||
}
|
||||
|
||||
/* Unified floating cards -------------------------------------------------
|
||||
Card geometry never changes on interaction; transform-only elevation
|
||||
keeps the dashboard stable while matching the shared shadcn card states. */
|
||||
body.console-v2 :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-can-command-panel,
|
||||
.v2-sync-log,
|
||||
.v2-nfc-inline,
|
||||
.v2-extension-gateway,
|
||||
.v2-extension-synced-device,
|
||||
.v2-extension-empty,
|
||||
.v2-extension-config-card,
|
||||
.v2-extension-type-card,
|
||||
.v2-extension-vehicle-card,
|
||||
.v2-pet-card-region,
|
||||
.v2-pet-card,
|
||||
.v2-pet-presence-card,
|
||||
.v2-device-card,
|
||||
.v2-synced-device,
|
||||
.joint-seat-panel,
|
||||
.domain-panel,
|
||||
.coverage-card,
|
||||
.decision-list li,
|
||||
.frame-summary,
|
||||
.execution-summary,
|
||||
.mode-gallery-card,
|
||||
.mode-editor-flow,
|
||||
.mode-editor-command-card,
|
||||
.v2-nfc-body,
|
||||
.v2-nfc-existing,
|
||||
.v2-nfc-pet-binding,
|
||||
.energy-kpi,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card,
|
||||
.assistant-dispatched-controls,
|
||||
.assistant-mode-draft,
|
||||
.assistant-mode-created,
|
||||
.assistant-control-suggestion
|
||||
) {
|
||||
border-radius: var(--glass-card-radius) !important;
|
||||
box-shadow: var(--glass-card-rest-shadow) !important;
|
||||
transform: translateY(0);
|
||||
transition: transform 160ms ease, background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.v2-can-command-panel,
|
||||
.v2-sync-log,
|
||||
.v2-nfc-inline,
|
||||
.v2-extension-gateway,
|
||||
.v2-extension-synced-device,
|
||||
.v2-extension-empty,
|
||||
.v2-extension-config-card,
|
||||
.v2-extension-type-card,
|
||||
.v2-extension-vehicle-card,
|
||||
.v2-pet-card,
|
||||
.v2-pet-presence-card,
|
||||
.v2-device-card,
|
||||
.v2-synced-device,
|
||||
.joint-seat-panel,
|
||||
.domain-panel,
|
||||
.coverage-card,
|
||||
.decision-list li,
|
||||
.frame-summary,
|
||||
.execution-summary,
|
||||
.mode-gallery-card,
|
||||
.mode-editor-command-card,
|
||||
.v2-nfc-existing,
|
||||
.v2-nfc-pet-binding,
|
||||
.energy-kpi,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card,
|
||||
.assistant-dispatched-controls,
|
||||
.assistant-mode-draft,
|
||||
.assistant-mode-created,
|
||||
.assistant-control-suggestion
|
||||
):is(:hover, :focus-within) {
|
||||
background-color: var(--glass-card-hover) !important;
|
||||
border-color: var(--glass-line-strong) !important;
|
||||
box-shadow: var(--glass-card-float-shadow) !important;
|
||||
transform: translateY(var(--glass-card-lift));
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-card,
|
||||
.assistant-control-suggestion
|
||||
):active {
|
||||
box-shadow: var(--glass-card-rest-shadow) !important;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
body.console-v2 :is(.mode-gallery-close, .mode-editor-header > button, .v2-extension-dialog [data-dialog-close], #energyMonitorClose) {
|
||||
color: var(--muted-foreground) !important;
|
||||
background: var(--glass-card) !important;
|
||||
@@ -644,12 +750,12 @@ body.console-v2 :is(.mode-gallery-close, .mode-editor-header > button, .v2-exten
|
||||
|
||||
/* Persistent day / night appearance ------------------------------------- */
|
||||
.glass-theme-toggle {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
position: fixed;
|
||||
left: max(16px, env(safe-area-inset-left));
|
||||
bottom: max(16px, env(safe-area-inset-bottom));
|
||||
z-index: 1600;
|
||||
z-index: 1100;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
@@ -776,6 +882,206 @@ html[data-glass-theme="light"] body.console-v2 :is(
|
||||
box-shadow: 0 28px 84px color-mix(in oklch, var(--foreground) 20%, transparent), inset 0 1px 0 var(--glass-highlight) !important;
|
||||
}
|
||||
|
||||
/* Audited component hierarchy -------------------------------------------
|
||||
Keep shadcn-style roles visually distinct: panel > section > item >
|
||||
control. These rules intentionally live after the theme variants so both
|
||||
day and night modes consume the same geometry and typography contract. */
|
||||
body.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync :is(
|
||||
.main-panel > .v2-mode-rail,
|
||||
#assistantDrawer,
|
||||
.joint-twin-shell > .twin-band,
|
||||
#v2DevicePanel,
|
||||
#v2ExtensionPanel .v2-extension-sync-mount
|
||||
) {
|
||||
border-radius: var(--ui-radius-panel) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.v2-sync-log,
|
||||
.mode-gallery-group,
|
||||
.v2-pet-card-region,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card
|
||||
) {
|
||||
border-radius: var(--ui-radius-section) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-can-command-panel,
|
||||
.v2-nfc-inline,
|
||||
.v2-extension-gateway,
|
||||
.v2-extension-synced-device,
|
||||
.v2-extension-empty,
|
||||
.v2-extension-config-card,
|
||||
.v2-extension-type-card,
|
||||
.v2-extension-vehicle-card,
|
||||
.v2-pet-card,
|
||||
.v2-pet-presence-card,
|
||||
.v2-device-card,
|
||||
.v2-synced-device,
|
||||
.mode-gallery-card,
|
||||
.mode-editor-flow,
|
||||
.mode-editor-command-card,
|
||||
.energy-kpi,
|
||||
.assistant-dispatched-controls,
|
||||
.assistant-mode-draft,
|
||||
.assistant-mode-created,
|
||||
.assistant-control-suggestion
|
||||
) {
|
||||
border-radius: var(--ui-radius-item) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery,
|
||||
.mode-editor,
|
||||
.v2-nfc-dialog,
|
||||
.v2-gscu-tcp-dialog,
|
||||
.v2-pet-settings-dialog,
|
||||
.energy-monitor-page
|
||||
) {
|
||||
border-radius: var(--ui-radius-modal) !important;
|
||||
}
|
||||
|
||||
/* Read-only containers must not imitate clickable cards on hover. */
|
||||
body.console-v2 :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-sync-log,
|
||||
.v2-pet-card-region,
|
||||
.mode-editor-flow,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card
|
||||
):is(:hover, :focus-within) {
|
||||
background-color: var(--glass-card) !important;
|
||||
border-color: var(--glass-line) !important;
|
||||
box-shadow: var(--glass-card-rest-shadow) !important;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* One title scale across peer panels and dialogs. */
|
||||
body.console-v2 :is(
|
||||
#assistantDrawer .v2-operation-heading h2,
|
||||
.joint-twin-shell .twin-heading h3,
|
||||
.v2-device-sync-board > header h3,
|
||||
.v2-extension-sync-heading h3
|
||||
) {
|
||||
font-size: var(--ui-font-panel-title) !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.25 !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(.v2-sync-log h3, .v2-mode-rail .mode-rail-title) {
|
||||
font-size: 14px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.25 !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-header h2,
|
||||
.mode-editor-header h2,
|
||||
.v2-extension-dialog h2,
|
||||
.energy-monitor-heading h2
|
||||
) {
|
||||
font-size: 22px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.2 !important;
|
||||
letter-spacing: -0.025em !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-header p,
|
||||
.mode-gallery-footer,
|
||||
.mode-editor-header p,
|
||||
.mode-editor-status,
|
||||
.v2-pet-settings-footnote,
|
||||
.v2-nfc-footnote
|
||||
) {
|
||||
font-size: 12px !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-card-heading strong,
|
||||
.v2-pet-card > header > strong,
|
||||
.v2-gscu-actions > header strong,
|
||||
.v2-gscu-command-catalog > header strong
|
||||
) {
|
||||
font-size: var(--ui-font-card-title) !important;
|
||||
line-height: 1.35 !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-card-description,
|
||||
.v2-pet-card-fields > label > span,
|
||||
.v2-gscu-tcp-dialog label,
|
||||
.energy-kpi > small
|
||||
) {
|
||||
font-size: 12px !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(.mode-gallery-card-meta, .energy-kpi > em, .energy-devices-card > footer) {
|
||||
font-size: 11px !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
/* Consistent icon-button variants. */
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-close,
|
||||
.mode-editor-header > button,
|
||||
.v2-extension-dialog [data-dialog-close],
|
||||
.v2-extension-dialog [data-close-extension-dialog],
|
||||
.v2-extension-dialog [data-close-nfc-dialog],
|
||||
#energyMonitorClose,
|
||||
.twin-reset
|
||||
) {
|
||||
width: var(--ui-control-size-icon) !important;
|
||||
height: var(--ui-control-size-icon) !important;
|
||||
min-width: var(--ui-control-size-icon) !important;
|
||||
min-height: var(--ui-control-size-icon) !important;
|
||||
border-radius: var(--ui-radius-control) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(.mode-gallery-edit, .mode-gallery-delete, .v2-pet-card > header > button) {
|
||||
width: var(--ui-control-height-compact) !important;
|
||||
height: var(--ui-control-height-compact) !important;
|
||||
min-width: var(--ui-control-height-compact) !important;
|
||||
min-height: var(--ui-control-height-compact) !important;
|
||||
border-radius: var(--ui-radius-control-inner) !important;
|
||||
}
|
||||
|
||||
/* Make horizontal continuation discoverable without adding another control. */
|
||||
body.console-v2 .v2-mode-rail .mode-chips {
|
||||
scroll-padding-inline: 18px;
|
||||
-webkit-mask-image: linear-gradient(90deg, transparent, black 18px, black calc(100% - 26px), transparent);
|
||||
mask-image: linear-gradient(90deg, transparent, black 18px, black calc(100% - 26px), transparent);
|
||||
}
|
||||
|
||||
/* Reserve scroll space beneath the required floating theme switch. */
|
||||
body.console-v2 .v2-sync-log ol {
|
||||
padding-bottom: 58px !important;
|
||||
scroll-padding-bottom: 58px;
|
||||
}
|
||||
|
||||
/* Offline is a destructive state, not an active primary state. */
|
||||
body.console-v2 .v2-nfc-stage[data-state="offline"] > header {
|
||||
color: var(--status-error-foreground) !important;
|
||||
background: color-mix(in oklch, var(--destructive) 16%, var(--card)) !important;
|
||||
border-bottom: 1px solid color-mix(in oklch, var(--destructive) 30%, var(--border));
|
||||
}
|
||||
|
||||
body.console-v2 .v2-nfc-stage[data-state="offline"] > header :is(span, small, h3, p, b) {
|
||||
color: var(--status-error-foreground) !important;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-nfc-stage[data-state="offline"] > header > span,
|
||||
body.console-v2 .v2-nfc-stage[data-state="offline"] > header > b {
|
||||
background: var(--status-error-soft) !important;
|
||||
border-color: color-mix(in oklch, var(--destructive) 28%, var(--border)) !important;
|
||||
}
|
||||
|
||||
/* Scrollbars ------------------------------------------------------------- */
|
||||
body.console-v2 * {
|
||||
scrollbar-color: color-mix(in oklch, var(--primary) 70%, white 8%) color-mix(in oklch, var(--background) 66%, transparent);
|
||||
@@ -807,4 +1113,45 @@ body.console-v2 *::-webkit-scrollbar-thumb {
|
||||
body.console-v2::after {
|
||||
filter: blur(44px);
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-can-command-panel,
|
||||
.v2-sync-log,
|
||||
.v2-nfc-inline,
|
||||
.v2-extension-gateway,
|
||||
.v2-extension-synced-device,
|
||||
.v2-extension-empty,
|
||||
.v2-extension-config-card,
|
||||
.v2-extension-type-card,
|
||||
.v2-extension-vehicle-card,
|
||||
.v2-pet-card-region,
|
||||
.v2-pet-card,
|
||||
.v2-pet-presence-card,
|
||||
.v2-device-card,
|
||||
.v2-synced-device,
|
||||
.joint-seat-panel,
|
||||
.domain-panel,
|
||||
.coverage-card,
|
||||
.decision-list li,
|
||||
.frame-summary,
|
||||
.execution-summary,
|
||||
.mode-gallery-card,
|
||||
.mode-editor-flow,
|
||||
.mode-editor-command-card,
|
||||
.v2-nfc-body,
|
||||
.v2-nfc-existing,
|
||||
.v2-nfc-pet-binding,
|
||||
.energy-kpi,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card,
|
||||
.assistant-dispatched-controls,
|
||||
.assistant-mode-draft,
|
||||
.assistant-mode-created,
|
||||
.assistant-control-suggestion
|
||||
) {
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
+261
-17
@@ -266,7 +266,7 @@
|
||||
if (isPassengerSeat) {
|
||||
const passengerAxes = {
|
||||
"seat.set_longitudinal_position": ["副驾座椅前后调节", "horizontal", 0],
|
||||
"seat.set_cushion_tilt": ["副驾座椅高度调节", "basin", 1],
|
||||
"seat.set_cushion_tilt": ["副驾座椅坐盆调节", "basin", 1],
|
||||
"seat.set_backrest_recline": ["副驾座椅靠背调节", "backrest", 2],
|
||||
"seat.set_legrest_height": ["副驾座椅腿托调节", "legrest", 3],
|
||||
};
|
||||
@@ -447,11 +447,19 @@
|
||||
height: new Set(["up", "down", "stop"]),
|
||||
backrest: new Set(["forward", "backward", "stop"]),
|
||||
basin: new Set(["up", "down", "stop"]),
|
||||
legrest: new Set(["up", "down", "forward", "backward", "stop"]),
|
||||
};
|
||||
if (validDirections[axis]?.has(direction)) {
|
||||
if (direction === "stop") return command("主驾座椅调节", `seat driver ${axis} stop`);
|
||||
const axisCategory = {
|
||||
horizontal: "主驾座椅前后调节",
|
||||
height: "主驾座椅坐盆高度调节",
|
||||
basin: "主驾座椅坐盆调节",
|
||||
backrest: "主驾座椅靠背调节",
|
||||
legrest: "主驾座椅腿托调节",
|
||||
}[axis] || "主驾座椅调节";
|
||||
if (direction === "stop") return command(axisCategory, `seat driver ${axis} stop`);
|
||||
const duration = boundedInteger(parameters.duration_ms, 100, 2000);
|
||||
if (duration !== null) return command("主驾座椅调节", `seat driver ${axis} ${direction} ${duration}`);
|
||||
if (duration !== null) return command(axisCategory, `seat driver ${axis} ${direction} ${duration}`);
|
||||
}
|
||||
}
|
||||
if (capability === "seat.set_driver_position_native") {
|
||||
@@ -531,6 +539,7 @@
|
||||
}
|
||||
});
|
||||
const submittedStates = new Set(["submitted", "queued", "published", "sent", "delivered"]);
|
||||
const abortedStates = new Set(["aborted", "cancelled", "canceled", "skipped"]);
|
||||
const deliveryForOperation = (operation) => {
|
||||
const capability = String(operation?.capability || "");
|
||||
const operationId = String(operation?.id || "");
|
||||
@@ -555,30 +564,59 @@
|
||||
|| telemetry.feedback_observed === true
|
||||
|| telemetry.physical_effect_confirmed === true
|
||||
);
|
||||
const executionStatus = String(execution?.status || "").toLowerCase();
|
||||
const verificationStatus = String(telemetry.verification_status || "").toLowerCase();
|
||||
const failureMessage = String(execution?.message || "").trim();
|
||||
const executionFailed = execution?.status === "failed"
|
||||
|| telemetry.verification_status === "failed_or_blocked";
|
||||
const safetyBlocked = executionFailed && !submitted && (
|
||||
/no fresh valid 0x41a shade position/i.test(failureMessage)
|
||||
|| /refusing (?:blind )?motion/i.test(failureMessage)
|
||||
|| /thermal protection/i.test(failureMessage)
|
||||
const aborted = abortedStates.has(executionStatus);
|
||||
const executionFailed = executionStatus === "failed"
|
||||
|| verificationStatus === "failed_or_blocked";
|
||||
const missingWindowFeedback = /0x388/i.test(failureMessage) && (
|
||||
/no fresh valid/i.test(failureMessage)
|
||||
|| /未收到.*(?:有效|新鲜).*车窗位置/.test(failureMessage)
|
||||
);
|
||||
const deliveryState = safetyBlocked
|
||||
const missingShadeFeedback = /0x41a/i.test(failureMessage) && (
|
||||
/no fresh valid/i.test(failureMessage)
|
||||
|| /未收到.*(?:有效|新鲜).*遮阳帘.*位置/.test(failureMessage)
|
||||
);
|
||||
const safetyBlocked = executionFailed && !submitted && (
|
||||
missingWindowFeedback
|
||||
|| missingShadeFeedback
|
||||
|| /refusing (?:blind )?motion/i.test(failureMessage)
|
||||
|| /拒绝(?:启动机构|盲动|执行)/.test(failureMessage)
|
||||
|| /thermal protection|热保护/i.test(failureMessage)
|
||||
);
|
||||
const dispatchFailed = executionFailed && !submitted && !safetyBlocked;
|
||||
const verified = !executionFailed && !aborted && (
|
||||
verificationStatus === "verified_success"
|
||||
|| telemetry.physical_effect_confirmed === true
|
||||
|| telemetry.feedback_match === true
|
||||
);
|
||||
const deliveryState = aborted
|
||||
? "aborted"
|
||||
: safetyBlocked
|
||||
? "blocked"
|
||||
: executionFailed
|
||||
? "failed"
|
||||
: submitted
|
||||
? "submitted"
|
||||
: "not_submitted";
|
||||
const deliveryLabel = safetyBlocked
|
||||
const deliveryLabel = aborted
|
||||
? "已中止"
|
||||
: safetyBlocked
|
||||
? "安全阻止"
|
||||
: executionFailed
|
||||
: dispatchFailed
|
||||
? "下发失败"
|
||||
: executionFailed
|
||||
? "执行失败"
|
||||
: verified
|
||||
? "执行已确认"
|
||||
: submitted
|
||||
? "下发"
|
||||
? "已下发 · 待状态确认"
|
||||
: "未下发";
|
||||
const deliveryDetail = /no fresh valid 0x41a shade position/i.test(failureMessage)
|
||||
? "未收到遮阳帘 CAN1 / 0x41A 实时位置,无法判断移动方向,系统已阻止盲动"
|
||||
const deliveryDetail = missingWindowFeedback
|
||||
? "未收到车窗 CAN1 / 0x388 实时位置,系统已阻止车窗盲动"
|
||||
: missingShadeFeedback
|
||||
? "未收到遮阳帘 CAN1 / 0x41A 实时位置,无法判断移动方向,系统已阻止盲动"
|
||||
: failureMessage;
|
||||
const transportLabel = capability === "infrared.emit"
|
||||
? "红外 TCP"
|
||||
@@ -614,18 +652,32 @@
|
||||
.filter((frame) => frame.value);
|
||||
const command = String(presentation?.actual_command || fallback.command || "").trim();
|
||||
if (!command) return;
|
||||
const delivery = deliveryForOperation(operation);
|
||||
const explicitDeliveryState = String(presentation?.delivery_state || "").trim();
|
||||
const explicitDeliveryLabel = String(presentation?.delivery_label || "").trim();
|
||||
const explicitDeliveryDetail = String(presentation?.delivery_detail || "").trim();
|
||||
const explicitDeliveryTransport = String(presentation?.delivery_transport || "").trim();
|
||||
commands.push({
|
||||
...fallback,
|
||||
channel,
|
||||
command,
|
||||
category: String(presentation?.category || fallback.category || "控制指令"),
|
||||
...(frames.length ? {
|
||||
frame: frames[0].value,
|
||||
frameLabel: frames[0].label,
|
||||
frames,
|
||||
} : {}),
|
||||
semantic: semanticControlInstruction(operation),
|
||||
semantic: String(
|
||||
presentation?.semantic_instruction || semanticControlInstruction(operation),
|
||||
),
|
||||
operationRef: String(operation.id || ""),
|
||||
...deliveryForOperation(operation),
|
||||
...delivery,
|
||||
...(explicitDeliveryState ? { deliveryState: explicitDeliveryState } : {}),
|
||||
...(explicitDeliveryLabel ? { deliveryLabel: explicitDeliveryLabel } : {}),
|
||||
...(explicitDeliveryDetail ? { deliveryDetail: explicitDeliveryDetail } : {}),
|
||||
...(explicitDeliveryTransport ? {
|
||||
deliveryTransport: explicitDeliveryTransport,
|
||||
} : {}),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@@ -669,6 +721,7 @@
|
||||
saved_mode_command_flow: "模式卡片",
|
||||
local_rule: "本地规则",
|
||||
speech_semantic: "语音语义",
|
||||
vehicle_mqtt_extension: "车端 MQTT",
|
||||
}[source] || "云端模型";
|
||||
}
|
||||
|
||||
@@ -874,6 +927,10 @@
|
||||
<b class="v2-can-command-delivery" data-delivery-state="${escapeHtml(item.deliveryState)}" title="${escapeHtml([item.deliveryTransport, item.deliveryLabel, item.deliveryDetail].filter(Boolean).join(" · "))}">${escapeHtml(item.deliveryLabel)}</b>
|
||||
<i class="v2-can-command-category">${escapeHtml(item.category)}</i>
|
||||
</span>
|
||||
${item.deliveryDetail && ["blocked", "failed", "aborted"].includes(item.deliveryState) ? `
|
||||
<small class="v2-can-command-delivery-detail">
|
||||
<b>状态说明</b>${escapeHtml(item.deliveryDetail)}
|
||||
</small>` : ""}
|
||||
<div class="v2-can-command-layer v2-can-command-semantic">
|
||||
<small>语义控制指令</small>
|
||||
<code>${escapeHtml(item.semantic)}</code>
|
||||
@@ -1214,6 +1271,13 @@
|
||||
logKeys: new Set(),
|
||||
extensionFingerprint: "",
|
||||
};
|
||||
const extensionControlSync = {
|
||||
revision: -1,
|
||||
requestId: "",
|
||||
running: false,
|
||||
stopped: true,
|
||||
controller: null,
|
||||
};
|
||||
const nonDispatchTaskSources = new Set(["mode_editor_compile"]);
|
||||
|
||||
function isDispatchTask(task = {}) {
|
||||
@@ -2074,6 +2138,183 @@
|
||||
}
|
||||
}
|
||||
|
||||
function extensionControlDelivery(action = {}) {
|
||||
const state = String(action.state || "pending");
|
||||
const defaults = {
|
||||
pending: ["not_submitted", "等待下发"],
|
||||
executing: ["executing", "正在下发"],
|
||||
submitted_unverified: ["submitted", "已下发 · 物理效果未确认"],
|
||||
verified_success: ["submitted", "执行已确认"],
|
||||
failed: ["failed", "下发失败"],
|
||||
aborted: ["aborted", "已中止"],
|
||||
};
|
||||
const [deliveryState, deliveryLabel] = defaults[state] || defaults.pending;
|
||||
return {
|
||||
deliveryState,
|
||||
deliveryLabel: String(action.state_label || deliveryLabel),
|
||||
deliveryDetail: String(action.state_detail || ""),
|
||||
};
|
||||
}
|
||||
|
||||
function extensionControlResult(snapshot = {}) {
|
||||
const requestId = String(snapshot.last_request_id || "");
|
||||
const actions = Array.isArray(snapshot.action_statuses)
|
||||
? snapshot.action_statuses
|
||||
: [];
|
||||
return {
|
||||
request_id: requestId,
|
||||
utterance: "车端 MQTT 外设控制",
|
||||
status: snapshot.state === "failed" ? "failed" : "succeeded",
|
||||
execution_mode: "hardware",
|
||||
control_transport: "extension_mqtt",
|
||||
orchestration_route: "vehicle_mqtt_extension_control",
|
||||
validated_control_commands: actions.map((action, position) => {
|
||||
const index = Number.isInteger(Number(action.index))
|
||||
? Number(action.index)
|
||||
: position;
|
||||
const atMs = Math.max(0, Number(action.at_ms) || 0);
|
||||
const slot = Number(action.slot);
|
||||
const extensionName = String(action.extension_name || "红外外设");
|
||||
const actionName = String(action.action || "红外动作");
|
||||
const targetScope = String(action.target_scope || "external-device");
|
||||
const delivery = extensionControlDelivery(action);
|
||||
return {
|
||||
domain: "self_extension",
|
||||
channel: "IR",
|
||||
category: `${extensionName} · target_scope=${targetScope} · at_ms=${atMs}`,
|
||||
semantic_instruction: `${extensionName} · ${actionName}`,
|
||||
actual_command: `infrared emit ${slot}`,
|
||||
delivery_state: delivery.deliveryState,
|
||||
delivery_label: delivery.deliveryLabel,
|
||||
delivery_detail: [delivery.deliveryDetail, `at_ms=${atMs}`]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
delivery_transport: "车端 MQTT → 红外 TCP",
|
||||
frames: [{
|
||||
label: "GSCU1B TCP",
|
||||
value: JSON.stringify({ action: "emit", data: { no: slot }, type: "infrared" }),
|
||||
}],
|
||||
operation: {
|
||||
id: `${requestId || "extension-ctl"}:${index}`,
|
||||
domain: "self_extension",
|
||||
capability: "infrared.emit",
|
||||
target: { device: targetScope, name: extensionName },
|
||||
parameters: { action: actionName, slot },
|
||||
parse_source: "vehicle_mqtt_extension",
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function renderExtensionControlRejection(snapshot = {}) {
|
||||
const panel = $("#v2CanCommandPanel");
|
||||
const list = $("#v2CanCommandList");
|
||||
const count = $("#v2CanCommandCount");
|
||||
if (!panel || !list || !count) return;
|
||||
count.textContent = "0";
|
||||
panel.dataset.state = "error";
|
||||
panel.open = true;
|
||||
list.innerHTML = `<p>车端 MQTT 外设控制已拒绝:${escapeHtml(snapshot.last_error || "消息未通过校验")}</p>`;
|
||||
if (ui.sidebarCollapsed) setSidebarCollapsed(false);
|
||||
requestAnimationFrame(syncSidebarLogBaseline);
|
||||
}
|
||||
|
||||
function applyExtensionControlSnapshot(snapshot = {}) {
|
||||
const revision = Number(snapshot.revision);
|
||||
if (!Number.isInteger(revision)) return;
|
||||
if (revision < extensionControlSync.revision) {
|
||||
extensionControlSync.revision = -1;
|
||||
extensionControlSync.requestId = "";
|
||||
}
|
||||
if (revision <= extensionControlSync.revision) return;
|
||||
extensionControlSync.revision = revision;
|
||||
const requestId = String(snapshot.last_request_id || "");
|
||||
const state = String(snapshot.state || "");
|
||||
const actions = Array.isArray(snapshot.action_statuses)
|
||||
? snapshot.action_statuses
|
||||
: [];
|
||||
if (requestId) extensionControlSync.requestId = requestId;
|
||||
|
||||
if (actions.length) {
|
||||
renderParsedCanCommands(extensionControlResult(snapshot), { reveal: true });
|
||||
appendSyncLog({
|
||||
key: `extension-control:${requestId}:${state}:${snapshot.executed_action_count || 0}`,
|
||||
category: "extension",
|
||||
title: state === "completed"
|
||||
? "车端 MQTT 外设控制已下发"
|
||||
: state === "failed"
|
||||
? "车端 MQTT 外设控制下发失败"
|
||||
: "正在执行车端 MQTT 外设控制",
|
||||
detail: `${actions.length} 条红外指令 · ${snapshot.last_error || state}`,
|
||||
status: state === "failed" ? "error" : state === "completed" ? "success" : "info",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (requestId && ["rejected", "queue_full"].includes(state)) {
|
||||
renderExtensionControlRejection(snapshot);
|
||||
appendSyncLog({
|
||||
key: `extension-control:${requestId}:${state}`,
|
||||
category: "extension",
|
||||
title: "车端 MQTT 外设控制已拒绝",
|
||||
detail: String(snapshot.last_error || "消息未通过校验"),
|
||||
status: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const extensionControlRetryDelay = () => new Promise((resolve) => {
|
||||
window.setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
async function monitorExtensionControl() {
|
||||
if (extensionControlSync.running) return;
|
||||
extensionControlSync.running = true;
|
||||
extensionControlSync.stopped = false;
|
||||
try {
|
||||
while (!extensionControlSync.stopped) {
|
||||
const controller = new AbortController();
|
||||
extensionControlSync.controller = controller;
|
||||
const query = new URLSearchParams({
|
||||
after_revision: String(extensionControlSync.revision),
|
||||
timeout_ms: "25000",
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`/api/extension/control/status?${query}`, {
|
||||
credentials: "same-origin",
|
||||
cache: "no-store",
|
||||
headers: { Accept: "application/json" },
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
applyExtensionControlSnapshot(await response.json());
|
||||
} catch (error) {
|
||||
if (extensionControlSync.stopped || error?.name === "AbortError") break;
|
||||
console.warn("[extension-control-sync]", error);
|
||||
await extensionControlRetryDelay();
|
||||
} finally {
|
||||
if (extensionControlSync.controller === controller) {
|
||||
extensionControlSync.controller = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
extensionControlSync.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
function startExtensionControlMonitor() {
|
||||
if (extensionControlSync.running) return;
|
||||
extensionControlSync.stopped = false;
|
||||
void monitorExtensionControl();
|
||||
}
|
||||
|
||||
function stopExtensionControlMonitor() {
|
||||
extensionControlSync.stopped = true;
|
||||
extensionControlSync.controller?.abort();
|
||||
extensionControlSync.controller = null;
|
||||
}
|
||||
|
||||
function applyDisplaySnapshot(snapshot) {
|
||||
const synchronizedCatalog = Array.isArray(snapshot?.extension?.catalog)
|
||||
? snapshot.extension.catalog
|
||||
@@ -2461,6 +2702,9 @@
|
||||
appendSyncLog({ key: "client:ready", category: "sync", title: "同步控制台已就绪", detail: "正在连接车机任务与设备状态服务", status: "info" });
|
||||
pollVehicleDisplaySync();
|
||||
displaySync.pollTimer = window.setInterval(pollVehicleDisplaySync, 2000);
|
||||
startExtensionControlMonitor();
|
||||
window.addEventListener("pagehide", stopExtensionControlMonitor);
|
||||
window.addEventListener("pageshow", startExtensionControlMonitor);
|
||||
const updateClock = () => {
|
||||
if ($("#v2SyncClock")) $("#v2SyncClock").textContent = new Intl.DateTimeFormat("zh-CN", {
|
||||
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
|
||||
|
||||
@@ -566,7 +566,7 @@ async function loadGestureRuntime() {
|
||||
async function loadRecognizer() {
|
||||
if (state.recognizer) return;
|
||||
$("#gestureSafetyText").textContent = "正在加载手势模型…";
|
||||
const { FilesetResolver, GestureRecognizer } = await import(VISION_MODULE);
|
||||
const { FilesetResolver, GestureRecognizer } = await import(/* @vite-ignore */ VISION_MODULE);
|
||||
const fileset = await FilesetResolver.forVisionTasks(VISION_WASM);
|
||||
const modelResponse = await window.CognitiveAgentNetwork.request("/api/gesture/model", {
|
||||
cache: "force-cache",
|
||||
|
||||
+27
-612
@@ -11,630 +11,45 @@
|
||||
<title>511工程-智能座舱</title>
|
||||
<script>
|
||||
(() => {
|
||||
const key = "cognitive-agent.glass-theme";
|
||||
let theme = "dark";
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (stored === "light" || stored === "dark") theme = stored;
|
||||
} catch (_error) {
|
||||
// Storage can be unavailable in hardened or private browser contexts.
|
||||
}
|
||||
try {
|
||||
const cookieTheme = document.cookie
|
||||
.split(";")
|
||||
.map((part) => part.trim())
|
||||
.find((part) => part.startsWith(`${key}=`))
|
||||
?.slice(key.length + 1);
|
||||
if (cookieTheme === "light" || cookieTheme === "dark") theme = cookieTheme;
|
||||
} catch (_error) {
|
||||
// The default still applies when cookies are disabled.
|
||||
}
|
||||
document.documentElement.dataset.glassTheme = theme;
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
})();
|
||||
const key = "cognitive-agent.glass-theme";
|
||||
let theme = "dark";
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (stored === "light" || stored === "dark") theme = stored;
|
||||
} catch (_error) {
|
||||
// Storage can be unavailable in hardened or private browser contexts.
|
||||
}
|
||||
try {
|
||||
const cookieTheme = document.cookie
|
||||
.split(";")
|
||||
.map((part) => part.trim())
|
||||
.find((part) => part.startsWith(`${key}=`))
|
||||
?.slice(key.length + 1);
|
||||
if (cookieTheme === "light" || cookieTheme === "dark") theme = cookieTheme;
|
||||
} catch (_error) {
|
||||
// The default still applies when cookies are disabled.
|
||||
}
|
||||
document.documentElement.dataset.glassTheme = theme;
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="/styles.css?v=dual-seat-23" />
|
||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-69" />
|
||||
<link rel="stylesheet" href="/self-extension.css?v=pet-settings-52" />
|
||||
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-29" />
|
||||
<link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" />
|
||||
<link rel="stylesheet" href="/ui-consistency.css?v=spatial-ui-5" />
|
||||
<link rel="stylesheet" href="/codepen-glass-theme.css?v=creative-cloud-2" />
|
||||
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
||||
<script src="/vendor/vue/3.5.21/vue.runtime.global.prod.js" defer></script>
|
||||
<script src="/vue-ui.js?v=vue-presentation-3" defer></script>
|
||||
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
|
||||
<script src="/network.js?v=android-pad-2" defer></script>
|
||||
<script src="/app.js?v=dual-seat-140" defer></script>
|
||||
<script src="/app.js?v=dual-seat-141" defer></script>
|
||||
<script src="/speech-input.js?v=sensevoice-13" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
||||
<script src="/pwa.js?v=android-pad-3" defer></script>
|
||||
<script src="/console-v2.js?v=local-first-70" defer></script>
|
||||
<script src="/console-v2.js?v=local-first-76" defer></script>
|
||||
<script src="/self-extension.js?v=pet-settings-52" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-33" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-34" defer></script>
|
||||
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
||||
</head>
|
||||
<body class="console-v2">
|
||||
<div class="v2-boot-screen" id="v2BootScreen" role="status" aria-live="polite">
|
||||
<span class="v2-boot-mark" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
<strong>511工程-智能座舱</strong>
|
||||
<small>正在加载车机控制与实时状态</small>
|
||||
</div>
|
||||
<div class="workspace">
|
||||
<aside class="command-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>说一句,就调好了</h2>
|
||||
</div>
|
||||
|
||||
<div class="mode-rail" aria-label="功能模式">
|
||||
<div class="mode-rail-bar">
|
||||
<div class="mode-rail-identity">
|
||||
<span class="mode-rail-mark" aria-hidden="true"><i data-lucide="sparkles"></i></span>
|
||||
<span><strong class="mode-rail-title">功能模式</strong><small>预设与我的模式</small></span>
|
||||
</div>
|
||||
<div class="mode-rail-content" id="modeRailContent" role="region" aria-label="功能模式列表">
|
||||
<div class="mode-chips" tabindex="0" aria-label="快捷功能模式,可横向滚动" aria-live="polite">
|
||||
<button type="button" data-mode-profile-id="zero_gravity" data-mode-origin="preset" data-mode-command="进入零重力模式"><i data-lucide="orbit"></i><span>零重力</span></button>
|
||||
<button type="button" data-mode-profile-id="office" data-mode-origin="preset" data-mode-command="进入办公模式"><i data-lucide="briefcase"></i><span>办公</span></button>
|
||||
<button type="button" data-mode-profile-id="nap" data-mode-origin="preset" data-mode-command="进入小憩模式"><i data-lucide="moon-star"></i><span>小憩</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-rail-actions">
|
||||
<button class="mode-gallery-toggle" id="modeGalleryToggle" type="button" aria-expanded="false" aria-controls="modeGallery"><i data-lucide="settings-2" data-icon="inline-start"></i><span>管理</span><strong id="modeGalleryCount">0</strong></button>
|
||||
<button class="mode-create-toggle" id="saveModeToggle" type="button"><i data-lucide="sparkles" data-icon="inline-start"></i><span>AI 新建</span></button>
|
||||
<button class="mode-rail-toggle" id="modeRailToggle" type="button" aria-expanded="true" aria-controls="modeRailContent" aria-label="收起功能模式" title="收起功能模式">
|
||||
<span class="visually-hidden" id="modeRailSelection">静态模式</span><i data-lucide="chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="requestForm" autocomplete="off">
|
||||
<section class="assistant-transcript" id="assistantTranscript" role="log" aria-live="polite" aria-label="AI助手对话">
|
||||
<div class="assistant-messages" id="assistantMessages">
|
||||
<article class="assistant-message assistant-message-assistant">
|
||||
<span class="assistant-avatar" aria-hidden="true"><i data-lucide="waypoints"></i></span>
|
||||
<div class="assistant-message-body">
|
||||
<div class="assistant-message-content">你好,我是 AI助手。你可以向我提问,也可以直接交代需要完成的工作。</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<div class="field-label-row">
|
||||
<label for="utterance">向 AI助手提问或交代工作</label>
|
||||
<button class="context-reset" id="newConversation" type="button" title="开始新会话" aria-label="开始新会话">
|
||||
<i data-lucide="message-square-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="command-composer">
|
||||
<textarea id="utterance" name="utterance" maxlength="4000" autocomplete="off" placeholder="向 AI助手提问或交代工作"></textarea>
|
||||
<button class="speech-dictation" id="speechDictationButton" type="button" aria-label="开始语音输入" aria-pressed="false" title="开始语音输入">
|
||||
<i class="speech-mic-icon" data-lucide="mic" aria-hidden="true"></i>
|
||||
<span class="speech-waveform" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span>
|
||||
<span class="speech-visually-hidden">开始语音输入</span>
|
||||
</button>
|
||||
<span class="speech-status" id="speechDictationStatus" role="status" aria-live="polite"></span>
|
||||
<span class="composer-hint">Enter 发送 · Shift + Enter 换行</span>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<span class="field-label">意图解析器</span>
|
||||
<div class="segmented" role="group" aria-label="意图解析器">
|
||||
<button type="button" class="segment active" data-provider="openai">
|
||||
<i data-lucide="sparkles"></i>GPT
|
||||
</button>
|
||||
<button type="button" class="segment" data-provider="local">
|
||||
<i data-lucide="braces"></i>本地规则
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="providerNote">正在读取运行时配置</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group" id="reasoningField">
|
||||
<span class="field-label">模型推理强度</span>
|
||||
<div class="segmented reasoning-segmented" role="group" aria-label="模型推理强度">
|
||||
<button type="button" class="segment reasoning-option" data-reasoning-effort="low">
|
||||
<i data-lucide="zap"></i><span><strong>极速</strong><small>简单指令,优先响应速度</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
<button type="button" class="segment reasoning-option active" data-reasoning-effort="medium">
|
||||
<i data-lucide="scale"></i><span><strong>均衡</strong><small>日常控制,兼顾速度与理解</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
<button type="button" class="segment reasoning-option" data-reasoning-effort="high">
|
||||
<i data-lucide="brain"></i><span><strong>深度</strong><small>复杂联动、相对动作与约束</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="reasoningNote">均衡模式兼顾自然语言解析精度与响应速度</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<span class="field-label">执行目标</span>
|
||||
<div class="segmented execution-mode-options" role="group" aria-label="执行目标">
|
||||
<button type="button" class="mode-option" data-execution-mode="dry_run">
|
||||
<i data-lucide="flask-conical"></i>模拟下发
|
||||
</button>
|
||||
<button type="button" class="mode-option active" id="hardwareMode" data-execution-mode="hardware">
|
||||
<i data-lucide="radio-tower"></i>真实下发
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group" id="vehicleTransportField">
|
||||
<span class="field-label">车辆连接方式</span>
|
||||
<div class="segmented vehicle-transport-options" role="group" aria-label="车辆连接方式">
|
||||
<button type="button" class="mode-option active" data-control-transport="wired_can">
|
||||
<i data-lucide="cable"></i>有线 CAN
|
||||
</button>
|
||||
<button type="button" class="mode-option" data-control-transport="wireless_mqtt">
|
||||
<i data-lucide="wifi"></i>无线 CAN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group simulation-fault-field" id="simulationFaultField" hidden>
|
||||
<label class="field-label" for="simulationFault">故障注入场景</label>
|
||||
<select id="simulationFault" aria-label="选择模拟硬件故障">
|
||||
<option value="normal">正常反馈</option>
|
||||
<option value="feedback_timeout">反馈超时</option>
|
||||
<option value="adapter_disconnect">适配器断开</option>
|
||||
<option value="motor_stall">电机堵转</option>
|
||||
<option value="firmware_mismatch">固件不匹配</option>
|
||||
<option value="partial_feedback">部分反馈</option>
|
||||
</select>
|
||||
<p class="provider-note">仅生成模拟 HIL 结果,不会打开物理 CAN 适配器或调用 IAR。</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group vehicle-field">
|
||||
<label class="field-label" for="vehicleSelect">车型配置</label>
|
||||
<div class="vehicle-select-control">
|
||||
<i data-lucide="car-front"></i>
|
||||
<select id="vehicleSelect" name="vehicle_id" aria-label="选择车型配置">
|
||||
<option value="">加载车型配置</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="provider-note" id="vehicleNote">正在读取座椅量程与角度标定</p>
|
||||
</div>
|
||||
|
||||
<button class="primary-action" id="runButton" type="submit" aria-label="发送消息">
|
||||
<i data-lucide="send-horizontal"></i>
|
||||
<span>发送</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</aside>
|
||||
|
||||
<main class="main-panel">
|
||||
<section class="request-header">
|
||||
<div>
|
||||
<div class="request-title-row">
|
||||
<h2 id="resultTitle">等待请求</h2>
|
||||
<span class="run-status idle" id="runStatus">IDLE</span>
|
||||
</div>
|
||||
<p id="requestMeta">Cabin / reference_vehicle / dry-run</p>
|
||||
</div>
|
||||
<div class="matrix-stat">
|
||||
<i data-lucide="table-properties"></i>
|
||||
<div><strong id="matrixCount">10 / 57</strong><span>报文 / 信号</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cabin-band" aria-labelledby="cabinStateTitle">
|
||||
<div class="section-title-row cabin-heading">
|
||||
<div class="cabin-title-copy">
|
||||
<h3 id="cabinStateTitle">整车设备状态</h3>
|
||||
</div>
|
||||
<div class="cabin-channel-legend" aria-label="整车设备通道说明">
|
||||
<span data-channel="ch0"><i>CH0</i>副驾座椅</span>
|
||||
<span data-channel="ch1"><i>CH1</i>主驾 / 车身</span>
|
||||
<span class="simulation-label" id="feedbackModeLabel">MATRIX-BACKED · SIMULATED FEEDBACK</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="domain-grid">
|
||||
<article class="domain-panel climate-panel" data-domain-panel="climate" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="fan"></i></span>
|
||||
<div><strong>空调</strong><small>CMD 0x1DE · FB 0x404 · CH1</small></div>
|
||||
<span class="device-state" id="climatePower">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>电源 · 温度 · 风量/风向</p>
|
||||
<div class="climate-display">
|
||||
<div class="zone-temperature">
|
||||
<span>主驾</span><strong id="driverTemperature">--<sup>℃</sup></strong>
|
||||
<small>风向<b id="driverAirDirection">-</b></small>
|
||||
</div>
|
||||
<div class="airflow-visual" id="airflowVisual" data-level="0" aria-label="空调风量">
|
||||
<i></i><i></i><i></i><i></i><i></i><i></i><i></i>
|
||||
</div>
|
||||
<div class="zone-temperature passenger-temperature">
|
||||
<span>副驾</span><strong id="passengerTemperature">--<sup>℃</sup></strong>
|
||||
<small>风向<b id="passengerAirDirection">-</b></small>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>风量</dt><dd id="fanLevel">未知</dd></div>
|
||||
<div><dt>循环</dt><dd id="cycleMode">未知</dd></div>
|
||||
<div><dt>A/C</dt><dd id="acState">未知</dd></div>
|
||||
<div><dt>前除霜</dt><dd id="defrostState">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel vehicle-status-panel" data-domain-panel="vehicle_status" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="gauge"></i></span>
|
||||
<div><strong>车辆状态</strong><small>FB 0x342 · 0x4EB · 0x407 · CH1</small></div>
|
||||
<span class="device-state readonly-state" id="vehicleTelemetryState">只读</span>
|
||||
</header>
|
||||
<p class="domain-capability-line readonly-capability"><span>只读信号</span>档位 · 车速 · 四门锁状态</p>
|
||||
<div class="vehicle-primary-readout">
|
||||
<div><span>当前档位</span><strong id="vehicleGear">--</strong></div>
|
||||
<div><span>实时车速</span><strong id="vehicleSpeed">-- km/h</strong></div>
|
||||
</div>
|
||||
<div class="door-state-grid" aria-label="四门锁状态">
|
||||
<span data-door-state="front_left"><i></i>左前门<b id="frontLeftDoorState">未知</b></span>
|
||||
<span data-door-state="front_right"><i></i>右前门<b id="frontRightDoorState">未知</b></span>
|
||||
<span data-door-state="rear_left"><i></i>左后门<b id="rearLeftDoorState">未知</b></span>
|
||||
<span data-door-state="rear_right"><i></i>右后门<b id="rearRightDoorState">未知</b></span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel roof-panel" data-domain-panel="roof" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="panels-top-left"></i></span>
|
||||
<div><strong>遮阳帘</strong><small>CMD 0x431 · FB 0x41A · CH1</small></div>
|
||||
<span class="device-state" id="roofState">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>开关 / 停止 · 位置</p>
|
||||
<div class="domain-summary-visual shade-summary-visual" aria-hidden="true">
|
||||
<span>闭合</span><i><b id="shadePositionMeter"></b></i><span>打开</span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>遮阳帘</dt><dd id="shadePositionValue">未知</dd></div>
|
||||
<div><dt>动作</dt><dd id="shadeMotionState">未知</dd></div>
|
||||
<div><dt>初始化</dt><dd id="shadeInitializedState">未知</dd></div>
|
||||
<div><dt>热保护</dt><dd id="shadeThermalState">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel ambient-panel" data-domain-panel="ambient_light" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="lamp-ceiling"></i></span>
|
||||
<div><strong>车内灯光</strong><small>INTERIOR / AMBIENT · CH1</small></div>
|
||||
<span class="device-state" id="ambientPower">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>阅读灯 · 氛围亮度 · 色号</p>
|
||||
<div class="domain-summary-visual ambient-summary-visual" aria-hidden="true">
|
||||
<span data-ambient-zone="front"><i id="ambientFrontMeter"></i><b>前排</b></span>
|
||||
<span data-ambient-zone="rear"><i id="ambientRearMeter"></i><b>后排</b></span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>氛围灯</dt><dd id="ambientMode">未知</dd></div>
|
||||
<div><dt>室内/阅读灯</dt><dd id="interiorLightPower">未知</dd></div>
|
||||
<div><dt>氛围亮度</dt><dd id="ambientBrightness">- / -</dd></div>
|
||||
<div><dt>氛围色号</dt><dd id="ambientColorIndex">- / -</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel window-panel" data-domain-panel="window" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="gallery-vertical-end"></i></span>
|
||||
<div><strong>车窗</strong><small>CMD 0x431 · FB 0x388 · CH1</small></div>
|
||||
<span class="device-state" id="windowState">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>四窗独立 · 开/关/停 · 位置</p>
|
||||
<div class="domain-summary-visual window-summary-visual" aria-hidden="true">
|
||||
<span data-window-meter="front_left"><i></i><b>左前</b></span>
|
||||
<span data-window-meter="front_right"><i></i><b>右前</b></span>
|
||||
<span data-window-meter="rear_left"><i></i><b>左后</b></span>
|
||||
<span data-window-meter="rear_right"><i></i><b>右后</b></span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>左前窗</dt><dd id="frontLeftWindow">未知</dd></div>
|
||||
<div><dt>右前窗</dt><dd id="frontRightWindow">未知</dd></div>
|
||||
<div><dt>左后窗</dt><dd id="rearLeftWindow">未知</dd></div>
|
||||
<div><dt>右后窗</dt><dd id="rearRightWindow">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel lighting-panel" data-domain-panel="front_light" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="car-front"></i></span>
|
||||
<div><strong>前大灯</strong><small>FB 0x38A · CH1</small></div>
|
||||
<span class="device-state" id="frontLightState">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>近/远光 · 雾灯 · 转向灯</p>
|
||||
<div class="domain-summary-visual lighting-summary-visual" aria-hidden="true">
|
||||
<span data-light-meter="lowBeamState"><i></i><b>近光</b></span>
|
||||
<span data-light-meter="highBeamState"><i></i><b>远光</b></span>
|
||||
<span data-light-meter="fogLightState"><i></i><b>雾灯</b></span>
|
||||
<span data-light-meter="turnSignalState"><i></i><b>转向</b></span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>近光灯</dt><dd id="lowBeamState">未知</dd></div>
|
||||
<div><dt>远光灯</dt><dd id="highBeamState">未知</dd></div>
|
||||
<div><dt>雾灯</dt><dd id="fogLightState">未知</dd></div>
|
||||
<div><dt>转向灯</dt><dd id="turnSignalState">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="twin-band" aria-labelledby="twinTitle">
|
||||
<div class="twin-heading">
|
||||
<div class="twin-title-copy">
|
||||
<h3 id="twinTitle">整车孪生界面</h3>
|
||||
<div class="preset-indicator" id="presetIndicator" hidden>
|
||||
<i data-lucide="bookmark-check"></i><span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="twin-toolbar">
|
||||
<div class="twin-selector" role="tablist" aria-label="前排座椅孪生选择">
|
||||
<button class="twin-seat-button active" type="button" role="tab" aria-selected="true" data-twin-seat="passenger">副驾座椅 · CH0</button>
|
||||
<button class="twin-seat-button" type="button" role="tab" aria-selected="false" data-twin-seat="driver">主驾座椅 · CH1</button>
|
||||
</div>
|
||||
<button class="icon-button twin-reset" id="twinReset" type="button" title="复位 3D 视角" aria-label="复位 3D 视角">
|
||||
<i data-lucide="rotate-3d"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="twin-stage" id="seatTwin" data-selected-seat="passenger">
|
||||
<canvas id="seatTwinCanvas" data-vehicle-model="byd-tang-2024-reference" data-seat-layout="five-seat" data-active-seat="front-right" aria-label="通过模型动画展示 CH0 副驾座椅与 CH1 主驾座椅、空调气流、智能天幕、阅读灯、四门车窗与前大灯状态的整车数字孪生"></canvas>
|
||||
<div class="twin-seat-label passenger"><i></i><span>副驾座椅</span><small>CH0 · 右前排</small></div>
|
||||
<div class="twin-seat-label driver"><i></i><span>主驾座椅</span><small>CH1 · 左前排</small></div>
|
||||
<div class="twin-orientation"><i data-lucide="move-down"></i><span>车头方向</span></div>
|
||||
<aside class="twin-readout" aria-live="polite">
|
||||
<div class="twin-readout-heading">
|
||||
<div><strong id="twinSeatName">副驾座椅</strong><span id="twinSeatZone">CH0 · 右前排</span></div>
|
||||
<span class="twin-feedback"><i></i>FEEDBACK</span>
|
||||
</div>
|
||||
<div class="twin-axis-list">
|
||||
<div data-twin-axis="longitudinal"><span>水平位置</span><b>50%</b><i></i></div>
|
||||
<div data-twin-axis="cushion_tilt"><span>坐垫俯仰</span><b>50%</b><i></i></div>
|
||||
<div data-twin-axis="height"><span>座椅高度</span><b>50%</b><i></i></div>
|
||||
<div data-twin-axis="backrest_recline"><span>靠背夹角</span><b>20% · 104°</b><i></i></div>
|
||||
<div data-twin-axis="legrest_height"><span>腿托夹角</span><b>0% · 95°</b><i></i></div>
|
||||
<div data-twin-axis="legrest_extension"><span>腿托伸缩</span><b>0%</b><i></i></div>
|
||||
</div>
|
||||
<div class="twin-thermal-row" aria-label="座椅热舒适状态">
|
||||
<span data-twin-function="ventilation" data-level="0">
|
||||
<i data-lucide="wind"></i>
|
||||
<span class="twin-thermal-copy"><small>通风</small><b>关闭</b></span>
|
||||
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
<span data-twin-function="heating" data-level="0">
|
||||
<i data-lucide="flame"></i>
|
||||
<span class="twin-thermal-copy"><small>加热</small><b>关闭</b></span>
|
||||
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="twin-fallback" id="twinFallback"><i data-lucide="loader-circle"></i><span>正在加载整车参考模型</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="state-band">
|
||||
<div class="seat-state-section">
|
||||
<div class="section-title-row">
|
||||
<div><h3>前排座椅状态</h3></div>
|
||||
<span class="simulation-label" id="seatFeedbackModeLabel">CH0 / CH1 FEEDBACK</span>
|
||||
</div>
|
||||
<div class="seat-grid">
|
||||
<article class="seat-card" data-seat="passenger">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>副驾座椅</strong><span>右前排</span></div></div>
|
||||
<span class="seat-channel-badge" data-seat-feedback="passenger">CH0 · 等待反馈</span>
|
||||
<div class="dynamic-mode-state" data-dynamic-mode>
|
||||
<i data-lucide="waves"></i><span>动态模式</span><b>未运行</b>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
<article class="seat-card" data-seat="driver">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>主驾座椅</strong><span>左前排</span></div></div>
|
||||
<span class="seat-channel-badge" data-seat-feedback="driver">CH1 · 等待反馈</span>
|
||||
<div class="dynamic-mode-state" data-dynamic-mode>
|
||||
<i data-lucide="radio-tower"></i><span>车端 CAN FD</span><b>只读同步</b>
|
||||
</div>
|
||||
<div class="thermal-states">
|
||||
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
|
||||
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
||||
<span data-function="massage"><i data-lucide="waves"></i>按摩 <b>--</b></span>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coverage-section">
|
||||
<h3>当前控制边界</h3>
|
||||
<dl>
|
||||
<div><dt>空调 18–32℃ / 0–7 档</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>天幕 / 遮阳帘 0–100%</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>氛围灯 RGB / 亮度</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>副驾座椅统一执行器</dt><dd class="simulated">t2g_primary_seat</dd></div>
|
||||
<div><dt>主驾座椅车端执行器</dt><dd class="simulated">CH1 · 左前排</dd></div>
|
||||
<div><dt>真实台架 CAN 下发</dt><dd class="blocked" id="hardwareCoverage">未配置</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-section result-workspace" aria-labelledby="resultWorkspaceTitle">
|
||||
<header class="result-workspace-heading">
|
||||
<div><h3 id="resultWorkspaceTitle">编排结果工作台</h3></div>
|
||||
<span class="result-workspace-badge"><i data-lucide="route"></i>REQUEST → CONFIG → CAN → RESULT</span>
|
||||
</header>
|
||||
<nav class="tabs result-nav" aria-label="结果快速导航">
|
||||
<button class="tab active" type="button" data-tab="trace"><span class="tab-index">01</span><i data-lucide="list-tree"></i><span class="tab-copy"><strong>决策轨迹</strong><small id="traceNavMeta">等待请求</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="config"><span class="tab-index">02</span><i data-lucide="layout-grid"></i><span class="tab-copy"><strong>配置卡片</strong><small id="configNavMeta">等待生成</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="plan"><span class="tab-index">03</span><i data-lucide="binary"></i><span class="tab-copy"><strong>CAN 计划</strong><small id="planNavMeta">等待编译</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="result"><span class="tab-index">04</span><i data-lucide="circle-check-big"></i><span class="tab-copy"><strong>执行结果</strong><small id="resultNavMeta">尚未执行</small></span><i data-lucide="arrow-down"></i></button>
|
||||
</nav>
|
||||
|
||||
<div class="tab-panel active" id="panel-trace" role="tabpanel">
|
||||
<div class="workspace-panel-title"><div><h3>决策轨迹与校验</h3></div><span class="workspace-panel-state" id="tracePanelState">IDLE</span></div>
|
||||
<div class="trace-layout">
|
||||
<div>
|
||||
<div class="detail-heading"><h3>可审计决策摘要</h3><span id="providerUsed">--</span></div>
|
||||
<ol class="decision-list" id="decisionList"><li>提交请求后显示识别目标、能力选择和约束来源。</li></ol>
|
||||
</div>
|
||||
<div>
|
||||
<div class="detail-heading"><h3>阶段事件</h3><span id="traceDuration">0 ms</span></div>
|
||||
<div class="trace-list" id="traceList"><div class="empty-line">暂无执行事件</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="validation-block">
|
||||
<div class="detail-heading"><h3>校验结果</h3><span id="validationCount">0 条</span></div>
|
||||
<div id="validationList" class="validation-list"><div class="empty-line">等待配置生成</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel config-panel" id="panel-config" role="tabpanel">
|
||||
<div class="config-panel-heading">
|
||||
<div><h3>指令配置与硬件映射</h3></div>
|
||||
<div class="config-actions"><button class="config-action" type="button" data-copy="config"><i data-lucide="copy"></i>复制 YAML</button><button class="config-action" type="button" data-download="config"><i data-lucide="download"></i>下载配置</button></div>
|
||||
</div>
|
||||
<div class="config-card-grid" id="configCards">
|
||||
<div class="config-empty"><i data-lucide="panel-top-dashed"></i><strong>等待生成配置</strong><span>提交车舱控制指令后,这里会显示控制对象、目标参数和硬件信号。</span></div>
|
||||
</div>
|
||||
<details class="config-source-details">
|
||||
<summary><span><i data-lucide="braces"></i>查看原始配置</span><small id="configFileName">functions/seat.yaml</small></summary>
|
||||
<pre id="configCode"><code># 等待生成 FunctionConfig</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel plan-panel" id="panel-plan" role="tabpanel">
|
||||
<div class="workspace-panel-title plan-panel-heading"><div><h3>CAN 编译计划</h3></div><button class="config-action" type="button" data-copy="plan"><i data-lucide="copy"></i>复制计划</button></div>
|
||||
<div class="frame-summary" id="frameSummary"><span>Frame ID</span><b>--</b><span>Payload</span><code>--</code><span>Mode</span><b>DRY-RUN</b></div>
|
||||
<details class="config-source-details plan-source-details">
|
||||
<summary><span><i data-lucide="binary"></i>查看原始 CAN 计划</span><small id="planFileName">command-plans/seat.yaml</small></summary>
|
||||
<pre id="planCode"><code># 等待 CAN 编译</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel result-panel" id="panel-result" role="tabpanel">
|
||||
<div class="workspace-panel-title"><div><h3>执行结果与硬件回读</h3></div><span class="workspace-panel-state" id="resultPanelState">IDLE</span></div>
|
||||
<div class="execution-summary" id="executionSummary">
|
||||
<i data-lucide="circle-dashed"></i><div><strong>尚未执行</strong><span>运行结果将在此处显示</span></div>
|
||||
</div>
|
||||
<div class="result-list" id="resultList"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<aside class="gesture-drawer" id="gestureDrawer" aria-hidden="true" aria-labelledby="gestureDrawerTitle">
|
||||
<header class="gesture-drawer-header">
|
||||
<div>
|
||||
<h2 id="gestureDrawerTitle">手势控制</h2>
|
||||
</div>
|
||||
<button class="gesture-close" id="gestureCloseButton" type="button" aria-label="关闭手势控制"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
|
||||
<div class="gesture-status-strip">
|
||||
<span class="gesture-state-pill idle" id="gestureStatePill"><i></i><b>未启动</b></span>
|
||||
<span id="gestureSafetyText">摄像头尚未开启</span>
|
||||
</div>
|
||||
|
||||
<div class="gesture-camera-stage">
|
||||
<video id="gestureVideo" playsinline muted></video>
|
||||
<canvas id="gestureCanvas"></canvas>
|
||||
<div class="gesture-camera-empty" id="gestureCameraEmpty">
|
||||
<span><i data-lucide="scan-face"></i></span>
|
||||
<strong>用手势控制副驾驶座椅</strong>
|
||||
<p>画面只在本机处理,不上传视频。</p>
|
||||
</div>
|
||||
<div class="gesture-live-badge" id="gestureLiveBadge" hidden><i></i>LIVE</div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-now">
|
||||
<div><span>当前手势</span><strong id="gestureDetected">—</strong></div>
|
||||
<i data-lucide="arrow-right"></i>
|
||||
<div><span>期望动作</span><strong id="gestureExpected">等待启动</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-progress"><i id="gestureProgressBar"></i></div>
|
||||
|
||||
<div class="gesture-actions">
|
||||
<button class="gesture-primary" id="gestureStartButton" type="button"><i data-lucide="camera"></i><span>开启摄像头</span></button>
|
||||
<button id="gestureStopButton" type="button" disabled><i data-lucide="octagon"></i>停止</button>
|
||||
</div>
|
||||
|
||||
<section class="gesture-guide-card">
|
||||
<div class="gesture-card-heading"><div><span>手势速查</span><small>张掌激活 · 握拳只停止</small></div><button id="gestureCustomizeButton" type="button"><i data-lucide="sliders-horizontal"></i>自定义</button></div>
|
||||
<div class="gesture-map-grid" id="gestureMapGrid"></div>
|
||||
<div class="gesture-customize" id="gestureCustomizePanel" hidden>
|
||||
<div><span>识别到</span><strong id="gestureCustomizeToken">请先摆出手势</strong></div>
|
||||
<select id="gestureActionSelect" aria-label="选择座椅动作"></select>
|
||||
<button id="gestureBindButton" type="button">绑定当前手势</button>
|
||||
<button id="gestureResetMapping" type="button">恢复默认</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="gesture-footnote"><i data-lucide="shield-check"></i><span>换动作无需重新张掌;连续 3 秒无有效动作后自动锁止。</span></footer>
|
||||
</aside>
|
||||
|
||||
<div class="mode-gallery-backdrop" id="modeGalleryBackdrop" hidden>
|
||||
<section class="mode-gallery" id="modeGallery" role="dialog" aria-modal="true" aria-labelledby="modeGalleryTitle">
|
||||
<header class="mode-gallery-header">
|
||||
<div>
|
||||
<h2 id="modeGalleryTitle">管理功能模式</h2>
|
||||
<p>按来源与运行方式整理,快速定位并管理模式</p>
|
||||
</div>
|
||||
<button class="mode-gallery-close" id="modeGalleryClose" type="button" aria-label="关闭功能模式管理"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
<div class="mode-gallery-summary"><span><i data-lucide="layers-3"></i>自定义模式 · 静态模式 · 动态模式</span><strong id="modeGallerySummaryCount">0</strong></div>
|
||||
<div class="mode-gallery-groups" id="modeGalleryGroups" aria-live="polite"></div>
|
||||
<footer class="mode-gallery-footer"><i data-lucide="info"></i><span>自定义模式支持编辑与删除;静态模式执行后保持目标状态;动态模式持续变化,可随时停止。</span></footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="mode-editor-backdrop" id="modeEditorBackdrop" hidden>
|
||||
<section class="mode-editor" role="dialog" aria-modal="true" aria-labelledby="modeEditorTitle">
|
||||
<div class="mode-editor-setup">
|
||||
<header class="mode-editor-header">
|
||||
<div>
|
||||
<h2 id="modeEditorTitle">编辑功能模式</h2>
|
||||
</div>
|
||||
<button id="modeEditorClose" type="button" aria-label="关闭模式编辑器"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
<div class="mode-editor-inputs">
|
||||
<label class="mode-editor-field mode-editor-name-field" for="modeEditorName">
|
||||
<span>模式名称</span>
|
||||
<input id="modeEditorName" maxlength="64" autocomplete="off" placeholder="输入模式名称" />
|
||||
</label>
|
||||
<label class="mode-editor-field mode-editor-instruction-field" for="modeEditorInstruction">
|
||||
<span>自然语言指令</span>
|
||||
<textarea id="modeEditorInstruction" maxlength="1000" rows="3" placeholder="例如:空调24度、风量2档,打开遮阳帘"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<section class="mode-editor-flow" aria-live="polite">
|
||||
<header>
|
||||
<span><i data-lucide="workflow"></i>控制指令流</span>
|
||||
<div class="mode-editor-flow-tools">
|
||||
<b id="modeEditorFlowCount">尚未生成</b>
|
||||
<button id="modeEditorFlowExpand" type="button" aria-expanded="false" aria-label="展开控制指令区域" title="展开控制指令区域">
|
||||
<i data-lucide="maximize-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mode-editor-flow-preview empty" id="modeEditorFlowPreview">
|
||||
<p>点击“生成指令”后,这里会显示由后端解析、校验并编译的控制指令流。</p>
|
||||
</div>
|
||||
</section>
|
||||
<p class="mode-editor-status" id="modeEditorStatus">修改自然语言后需要重新生成指令,才能点击完成永久保存。</p>
|
||||
<footer class="mode-editor-actions">
|
||||
<button id="modeEditorCancel" type="button">取消</button>
|
||||
<button class="mode-editor-execute" id="modeEditorExecute" type="button"><i data-lucide="wand-sparkles" data-icon="inline-start"></i><span>生成指令</span></button>
|
||||
<button class="mode-editor-complete" id="modeEditorComplete" type="button" disabled><i data-lucide="save" data-icon="inline-start"></i><span>完成</span></button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<button class="glass-theme-toggle" id="glassThemeToggle" type="button" aria-label="切换到白日模式" aria-pressed="true" title="切换到白日模式">
|
||||
<i data-lucide="sun" data-theme-icon="sun" aria-hidden="true"></i>
|
||||
<i data-lucide="moon" data-theme-icon="moon" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">切换白日/夜间模式</span>
|
||||
</button>
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
<div id="app" aria-busy="true"></div>
|
||||
<noscript>需要启用 JavaScript 才能使用智能座舱控制台。</noscript>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Generated
+1394
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@n7/cognitive-agent-web-console",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1 --port 5173",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --host 127.0.0.1 --port 4173"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "3.5.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"vite": "7.3.6"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v305";
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v315-vue-vite";
|
||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||
const APP_SHELL = [
|
||||
@@ -6,22 +6,22 @@ const APP_SHELL = [
|
||||
"/index.html",
|
||||
"/styles.css?v=dual-seat-23",
|
||||
"/network.js?v=android-pad-2",
|
||||
"/app.js?v=dual-seat-140",
|
||||
"/app.js?v=dual-seat-141",
|
||||
"/gesture-control.js?v=android-pad-9",
|
||||
"/pwa.js?v=android-pad-3",
|
||||
"/console-v2.css?v=delivery-state-69",
|
||||
"/console-v2.js?v=local-first-70",
|
||||
"/console-v2.js?v=local-first-76",
|
||||
"/self-extension.css?v=pet-settings-52",
|
||||
"/self-extension.js?v=pet-settings-52",
|
||||
"/speech-input.css?v=sensevoice-7",
|
||||
"/speech-input.js?v=sensevoice-13",
|
||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||
"/assistant-chat.css?v=codex-assistant-29",
|
||||
"/assistant-chat.js?v=codex-assistant-33",
|
||||
"/assistant-chat.js?v=codex-assistant-34",
|
||||
"/energy-monitor.css?v=energy-monitor-4",
|
||||
"/energy-monitor.js?v=energy-monitor-4",
|
||||
"/ui-consistency.css?v=spatial-ui-5",
|
||||
"/codepen-glass-theme.css?v=creative-cloud-2",
|
||||
"/ui-consistency.css?v=spatial-ui-8",
|
||||
"/codepen-glass-theme.css?v=creative-cloud-5",
|
||||
"/vue-ui.js?v=vue-presentation-3",
|
||||
"/assets/pets/self-extension-cat.png?v=3",
|
||||
"/assets/pets/self-extension-cat-entrance.png?v=3",
|
||||
|
||||
@@ -0,0 +1,584 @@
|
||||
<script setup>
|
||||
defineOptions({ name: "N7ConsoleApp" });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="v2-boot-screen" id="v2BootScreen" role="status" aria-live="polite">
|
||||
<span class="v2-boot-mark" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
<strong>511工程-智能座舱</strong>
|
||||
<small>正在加载车机控制与实时状态</small>
|
||||
</div>
|
||||
<div class="workspace">
|
||||
<aside class="command-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>说一句,就调好了</h2>
|
||||
</div>
|
||||
|
||||
<div class="mode-rail" aria-label="功能模式">
|
||||
<div class="mode-rail-bar">
|
||||
<div class="mode-rail-identity">
|
||||
<span class="mode-rail-mark" aria-hidden="true"><i data-lucide="sparkles"></i></span>
|
||||
<span><strong class="mode-rail-title">功能模式</strong><small>预设与我的模式</small></span>
|
||||
</div>
|
||||
<div class="mode-rail-content" id="modeRailContent" role="region" aria-label="功能模式列表">
|
||||
<div class="mode-chips" tabindex="0" aria-label="快捷功能模式,可横向滚动" aria-live="polite">
|
||||
<button type="button" data-mode-profile-id="zero_gravity" data-mode-origin="preset" data-mode-command="进入零重力模式"><i data-lucide="orbit"></i><span>零重力</span></button>
|
||||
<button type="button" data-mode-profile-id="office" data-mode-origin="preset" data-mode-command="进入办公模式"><i data-lucide="briefcase"></i><span>办公</span></button>
|
||||
<button type="button" data-mode-profile-id="nap" data-mode-origin="preset" data-mode-command="进入小憩模式"><i data-lucide="moon-star"></i><span>小憩</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-rail-actions">
|
||||
<button class="mode-gallery-toggle" id="modeGalleryToggle" type="button" aria-expanded="false" aria-controls="modeGallery"><i data-lucide="settings-2" data-icon="inline-start"></i><span>管理</span><strong id="modeGalleryCount">0</strong></button>
|
||||
<button class="mode-create-toggle" id="saveModeToggle" type="button"><i data-lucide="sparkles" data-icon="inline-start"></i><span>AI 新建</span></button>
|
||||
<button class="mode-rail-toggle" id="modeRailToggle" type="button" aria-expanded="true" aria-controls="modeRailContent" aria-label="收起功能模式" title="收起功能模式">
|
||||
<span class="visually-hidden" id="modeRailSelection">静态模式</span><i data-lucide="chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="requestForm" autocomplete="off">
|
||||
<section class="assistant-transcript" id="assistantTranscript" role="log" aria-live="polite" aria-label="AI助手对话">
|
||||
<div class="assistant-messages" id="assistantMessages">
|
||||
<article class="assistant-message assistant-message-assistant">
|
||||
<span class="assistant-avatar" aria-hidden="true"><i data-lucide="waypoints"></i></span>
|
||||
<div class="assistant-message-body">
|
||||
<div class="assistant-message-content">你好,我是 AI助手。你可以向我提问,也可以直接交代需要完成的工作。</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<div class="field-label-row">
|
||||
<label for="utterance">向 AI助手提问或交代工作</label>
|
||||
<button class="context-reset" id="newConversation" type="button" title="开始新会话" aria-label="开始新会话">
|
||||
<i data-lucide="message-square-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="command-composer">
|
||||
<textarea id="utterance" name="utterance" maxlength="4000" autocomplete="off" placeholder="向 AI助手提问或交代工作"></textarea>
|
||||
<button class="speech-dictation" id="speechDictationButton" type="button" aria-label="开始语音输入" aria-pressed="false" title="开始语音输入">
|
||||
<i class="speech-mic-icon" data-lucide="mic" aria-hidden="true"></i>
|
||||
<span class="speech-waveform" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span>
|
||||
<span class="speech-visually-hidden">开始语音输入</span>
|
||||
</button>
|
||||
<span class="speech-status" id="speechDictationStatus" role="status" aria-live="polite"></span>
|
||||
<span class="composer-hint">Enter 发送 · Shift + Enter 换行</span>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<span class="field-label">意图解析器</span>
|
||||
<div class="segmented" role="group" aria-label="意图解析器">
|
||||
<button type="button" class="segment active" data-provider="openai">
|
||||
<i data-lucide="sparkles"></i>GPT
|
||||
</button>
|
||||
<button type="button" class="segment" data-provider="local">
|
||||
<i data-lucide="braces"></i>本地规则
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="providerNote">正在读取运行时配置</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group" id="reasoningField">
|
||||
<span class="field-label">模型推理强度</span>
|
||||
<div class="segmented reasoning-segmented" role="group" aria-label="模型推理强度">
|
||||
<button type="button" class="segment reasoning-option" data-reasoning-effort="low">
|
||||
<i data-lucide="zap"></i><span><strong>极速</strong><small>简单指令,优先响应速度</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
<button type="button" class="segment reasoning-option active" data-reasoning-effort="medium">
|
||||
<i data-lucide="scale"></i><span><strong>均衡</strong><small>日常控制,兼顾速度与理解</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
<button type="button" class="segment reasoning-option" data-reasoning-effort="high">
|
||||
<i data-lucide="brain"></i><span><strong>深度</strong><small>复杂联动、相对动作与约束</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="reasoningNote">均衡模式兼顾自然语言解析精度与响应速度</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<span class="field-label">执行目标</span>
|
||||
<div class="segmented execution-mode-options" role="group" aria-label="执行目标">
|
||||
<button type="button" class="mode-option" data-execution-mode="dry_run">
|
||||
<i data-lucide="flask-conical"></i>模拟下发
|
||||
</button>
|
||||
<button type="button" class="mode-option active" id="hardwareMode" data-execution-mode="hardware">
|
||||
<i data-lucide="radio-tower"></i>真实下发
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group" id="vehicleTransportField">
|
||||
<span class="field-label">车辆连接方式</span>
|
||||
<div class="segmented vehicle-transport-options" role="group" aria-label="车辆连接方式">
|
||||
<button type="button" class="mode-option active" data-control-transport="wired_can">
|
||||
<i data-lucide="cable"></i>有线 CAN
|
||||
</button>
|
||||
<button type="button" class="mode-option" data-control-transport="wireless_mqtt">
|
||||
<i data-lucide="wifi"></i>无线 CAN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group simulation-fault-field" id="simulationFaultField" hidden>
|
||||
<label class="field-label" for="simulationFault">故障注入场景</label>
|
||||
<select id="simulationFault" aria-label="选择模拟硬件故障">
|
||||
<option value="normal">正常反馈</option>
|
||||
<option value="feedback_timeout">反馈超时</option>
|
||||
<option value="adapter_disconnect">适配器断开</option>
|
||||
<option value="motor_stall">电机堵转</option>
|
||||
<option value="firmware_mismatch">固件不匹配</option>
|
||||
<option value="partial_feedback">部分反馈</option>
|
||||
</select>
|
||||
<p class="provider-note">仅生成模拟 HIL 结果,不会打开物理 CAN 适配器或调用 IAR。</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group vehicle-field">
|
||||
<label class="field-label" for="vehicleSelect">车型配置</label>
|
||||
<div class="vehicle-select-control">
|
||||
<i data-lucide="car-front"></i>
|
||||
<select id="vehicleSelect" name="vehicle_id" aria-label="选择车型配置">
|
||||
<option value="">加载车型配置</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="provider-note" id="vehicleNote">正在读取座椅量程与角度标定</p>
|
||||
</div>
|
||||
|
||||
<button class="primary-action" id="runButton" type="submit" aria-label="发送消息">
|
||||
<i data-lucide="send-horizontal"></i>
|
||||
<span>发送</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</aside>
|
||||
|
||||
<main class="main-panel">
|
||||
<section class="request-header">
|
||||
<div>
|
||||
<div class="request-title-row">
|
||||
<h2 id="resultTitle">等待请求</h2>
|
||||
<span class="run-status idle" id="runStatus">IDLE</span>
|
||||
</div>
|
||||
<p id="requestMeta">Cabin / reference_vehicle / dry-run</p>
|
||||
</div>
|
||||
<div class="matrix-stat">
|
||||
<i data-lucide="table-properties"></i>
|
||||
<div><strong id="matrixCount">10 / 57</strong><span>报文 / 信号</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cabin-band" aria-labelledby="cabinStateTitle">
|
||||
<div class="section-title-row cabin-heading">
|
||||
<div class="cabin-title-copy">
|
||||
<h3 id="cabinStateTitle">整车设备状态</h3>
|
||||
</div>
|
||||
<div class="cabin-channel-legend" aria-label="整车设备通道说明">
|
||||
<span data-channel="ch0"><i>CH0</i>副驾座椅</span>
|
||||
<span data-channel="ch1"><i>CH1</i>主驾 / 车身</span>
|
||||
<span class="simulation-label" id="feedbackModeLabel">MATRIX-BACKED · SIMULATED FEEDBACK</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="domain-grid">
|
||||
<article class="domain-panel climate-panel" data-domain-panel="climate" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="fan"></i></span>
|
||||
<div><strong>空调</strong><small>CMD 0x1DE · FB 0x404 · CH1</small></div>
|
||||
<span class="device-state" id="climatePower">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>电源 · 温度 · 风量/风向</p>
|
||||
<div class="climate-display">
|
||||
<div class="zone-temperature">
|
||||
<span>主驾</span><strong id="driverTemperature">--<sup>℃</sup></strong>
|
||||
<small>风向<b id="driverAirDirection">-</b></small>
|
||||
</div>
|
||||
<div class="airflow-visual" id="airflowVisual" data-level="0" aria-label="空调风量">
|
||||
<i></i><i></i><i></i><i></i><i></i><i></i><i></i>
|
||||
</div>
|
||||
<div class="zone-temperature passenger-temperature">
|
||||
<span>副驾</span><strong id="passengerTemperature">--<sup>℃</sup></strong>
|
||||
<small>风向<b id="passengerAirDirection">-</b></small>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>风量</dt><dd id="fanLevel">未知</dd></div>
|
||||
<div><dt>循环</dt><dd id="cycleMode">未知</dd></div>
|
||||
<div><dt>A/C</dt><dd id="acState">未知</dd></div>
|
||||
<div><dt>前除霜</dt><dd id="defrostState">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel vehicle-status-panel" data-domain-panel="vehicle_status" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="gauge"></i></span>
|
||||
<div><strong>车辆状态</strong><small>FB 0x342 · 0x4EB · 0x407 · CH1</small></div>
|
||||
<span class="device-state readonly-state" id="vehicleTelemetryState">只读</span>
|
||||
</header>
|
||||
<p class="domain-capability-line readonly-capability"><span>只读信号</span>档位 · 车速 · 四门锁状态</p>
|
||||
<div class="vehicle-primary-readout">
|
||||
<div><span>当前档位</span><strong id="vehicleGear">--</strong></div>
|
||||
<div><span>实时车速</span><strong id="vehicleSpeed">-- km/h</strong></div>
|
||||
</div>
|
||||
<div class="door-state-grid" aria-label="四门锁状态">
|
||||
<span data-door-state="front_left"><i></i>左前门<b id="frontLeftDoorState">未知</b></span>
|
||||
<span data-door-state="front_right"><i></i>右前门<b id="frontRightDoorState">未知</b></span>
|
||||
<span data-door-state="rear_left"><i></i>左后门<b id="rearLeftDoorState">未知</b></span>
|
||||
<span data-door-state="rear_right"><i></i>右后门<b id="rearRightDoorState">未知</b></span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel roof-panel" data-domain-panel="roof" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="panels-top-left"></i></span>
|
||||
<div><strong>遮阳帘</strong><small>CMD 0x431 · FB 0x41A · CH1</small></div>
|
||||
<span class="device-state" id="roofState">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>开关 / 停止 · 位置</p>
|
||||
<div class="domain-summary-visual shade-summary-visual" aria-hidden="true">
|
||||
<span>闭合</span><i><b id="shadePositionMeter"></b></i><span>打开</span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>遮阳帘</dt><dd id="shadePositionValue">未知</dd></div>
|
||||
<div><dt>动作</dt><dd id="shadeMotionState">未知</dd></div>
|
||||
<div><dt>初始化</dt><dd id="shadeInitializedState">未知</dd></div>
|
||||
<div><dt>热保护</dt><dd id="shadeThermalState">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel ambient-panel" data-domain-panel="ambient_light" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="lamp-ceiling"></i></span>
|
||||
<div><strong>车内灯光</strong><small>INTERIOR / AMBIENT · CH1</small></div>
|
||||
<span class="device-state" id="ambientPower">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>阅读灯 · 氛围亮度 · 色号</p>
|
||||
<div class="domain-summary-visual ambient-summary-visual" aria-hidden="true">
|
||||
<span data-ambient-zone="front"><i id="ambientFrontMeter"></i><b>前排</b></span>
|
||||
<span data-ambient-zone="rear"><i id="ambientRearMeter"></i><b>后排</b></span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>氛围灯</dt><dd id="ambientMode">未知</dd></div>
|
||||
<div><dt>室内/阅读灯</dt><dd id="interiorLightPower">未知</dd></div>
|
||||
<div><dt>氛围亮度</dt><dd id="ambientBrightness">- / -</dd></div>
|
||||
<div><dt>氛围色号</dt><dd id="ambientColorIndex">- / -</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel window-panel" data-domain-panel="window" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="gallery-vertical-end"></i></span>
|
||||
<div><strong>车窗</strong><small>CMD 0x431 · FB 0x388 · CH1</small></div>
|
||||
<span class="device-state" id="windowState">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>四窗独立 · 开/关/停 · 位置</p>
|
||||
<div class="domain-summary-visual window-summary-visual" aria-hidden="true">
|
||||
<span data-window-meter="front_left"><i></i><b>左前</b></span>
|
||||
<span data-window-meter="front_right"><i></i><b>右前</b></span>
|
||||
<span data-window-meter="rear_left"><i></i><b>左后</b></span>
|
||||
<span data-window-meter="rear_right"><i></i><b>右后</b></span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>左前窗</dt><dd id="frontLeftWindow">未知</dd></div>
|
||||
<div><dt>右前窗</dt><dd id="frontRightWindow">未知</dd></div>
|
||||
<div><dt>左后窗</dt><dd id="rearLeftWindow">未知</dd></div>
|
||||
<div><dt>右后窗</dt><dd id="rearRightWindow">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel lighting-panel" data-domain-panel="front_light" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="car-front"></i></span>
|
||||
<div><strong>前大灯</strong><small>FB 0x38A · CH1</small></div>
|
||||
<span class="device-state" id="frontLightState">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>近/远光 · 雾灯 · 转向灯</p>
|
||||
<div class="domain-summary-visual lighting-summary-visual" aria-hidden="true">
|
||||
<span data-light-meter="lowBeamState"><i></i><b>近光</b></span>
|
||||
<span data-light-meter="highBeamState"><i></i><b>远光</b></span>
|
||||
<span data-light-meter="fogLightState"><i></i><b>雾灯</b></span>
|
||||
<span data-light-meter="turnSignalState"><i></i><b>转向</b></span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>近光灯</dt><dd id="lowBeamState">未知</dd></div>
|
||||
<div><dt>远光灯</dt><dd id="highBeamState">未知</dd></div>
|
||||
<div><dt>雾灯</dt><dd id="fogLightState">未知</dd></div>
|
||||
<div><dt>转向灯</dt><dd id="turnSignalState">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="twin-band" aria-labelledby="twinTitle">
|
||||
<div class="twin-heading">
|
||||
<div class="twin-title-copy">
|
||||
<h3 id="twinTitle">整车孪生界面</h3>
|
||||
<div class="preset-indicator" id="presetIndicator" hidden>
|
||||
<i data-lucide="bookmark-check"></i><span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="twin-toolbar">
|
||||
<div class="twin-selector" role="tablist" aria-label="前排座椅孪生选择">
|
||||
<button class="twin-seat-button active" type="button" role="tab" aria-selected="true" data-twin-seat="passenger">副驾座椅 · CH0</button>
|
||||
<button class="twin-seat-button" type="button" role="tab" aria-selected="false" data-twin-seat="driver">主驾座椅 · CH1</button>
|
||||
</div>
|
||||
<button class="icon-button twin-reset" id="twinReset" type="button" title="复位 3D 视角" aria-label="复位 3D 视角">
|
||||
<i data-lucide="rotate-3d"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="twin-stage" id="seatTwin" data-selected-seat="passenger">
|
||||
<canvas id="seatTwinCanvas" data-vehicle-model="byd-tang-2024-reference" data-seat-layout="five-seat" data-active-seat="front-right" aria-label="通过模型动画展示 CH0 副驾座椅与 CH1 主驾座椅、空调气流、智能天幕、阅读灯、四门车窗与前大灯状态的整车数字孪生"></canvas>
|
||||
<div class="twin-seat-label passenger"><i></i><span>副驾座椅</span><small>CH0 · 右前排</small></div>
|
||||
<div class="twin-seat-label driver"><i></i><span>主驾座椅</span><small>CH1 · 左前排</small></div>
|
||||
<div class="twin-orientation"><i data-lucide="move-down"></i><span>车头方向</span></div>
|
||||
<aside class="twin-readout" aria-live="polite">
|
||||
<div class="twin-readout-heading">
|
||||
<div><strong id="twinSeatName">副驾座椅</strong><span id="twinSeatZone">CH0 · 右前排</span></div>
|
||||
<span class="twin-feedback"><i></i>FEEDBACK</span>
|
||||
</div>
|
||||
<div class="twin-axis-list">
|
||||
<div data-twin-axis="longitudinal"><span>水平位置</span><b>50%</b><i></i></div>
|
||||
<div data-twin-axis="cushion_tilt"><span>坐垫俯仰</span><b>50%</b><i></i></div>
|
||||
<div data-twin-axis="height"><span>座椅高度</span><b>50%</b><i></i></div>
|
||||
<div data-twin-axis="backrest_recline"><span>靠背夹角</span><b>20% · 104°</b><i></i></div>
|
||||
<div data-twin-axis="legrest_height"><span>腿托夹角</span><b>0% · 95°</b><i></i></div>
|
||||
<div data-twin-axis="legrest_extension"><span>腿托伸缩</span><b>0%</b><i></i></div>
|
||||
</div>
|
||||
<div class="twin-thermal-row" aria-label="座椅热舒适状态">
|
||||
<span data-twin-function="ventilation" data-level="0">
|
||||
<i data-lucide="wind"></i>
|
||||
<span class="twin-thermal-copy"><small>通风</small><b>关闭</b></span>
|
||||
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
<span data-twin-function="heating" data-level="0">
|
||||
<i data-lucide="flame"></i>
|
||||
<span class="twin-thermal-copy"><small>加热</small><b>关闭</b></span>
|
||||
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="twin-fallback" id="twinFallback"><i data-lucide="loader-circle"></i><span>正在加载整车参考模型</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="state-band">
|
||||
<div class="seat-state-section">
|
||||
<div class="section-title-row">
|
||||
<div><h3>前排座椅状态</h3></div>
|
||||
<span class="simulation-label" id="seatFeedbackModeLabel">CH0 / CH1 FEEDBACK</span>
|
||||
</div>
|
||||
<div class="seat-grid">
|
||||
<article class="seat-card" data-seat="passenger">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>副驾座椅</strong><span>右前排</span></div></div>
|
||||
<span class="seat-channel-badge" data-seat-feedback="passenger">CH0 · 等待反馈</span>
|
||||
<div class="dynamic-mode-state" data-dynamic-mode>
|
||||
<i data-lucide="waves"></i><span>动态模式</span><b>未运行</b>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
<article class="seat-card" data-seat="driver">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>主驾座椅</strong><span>左前排</span></div></div>
|
||||
<span class="seat-channel-badge" data-seat-feedback="driver">CH1 · 等待反馈</span>
|
||||
<div class="dynamic-mode-state" data-dynamic-mode>
|
||||
<i data-lucide="radio-tower"></i><span>车端 CAN FD</span><b>只读同步</b>
|
||||
</div>
|
||||
<div class="thermal-states">
|
||||
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
|
||||
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
||||
<span data-function="massage"><i data-lucide="waves"></i>按摩 <b>--</b></span>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coverage-section">
|
||||
<h3>当前控制边界</h3>
|
||||
<dl>
|
||||
<div><dt>空调 18–32℃ / 0–7 档</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>天幕 / 遮阳帘 0–100%</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>氛围灯 RGB / 亮度</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>副驾座椅统一执行器</dt><dd class="simulated">t2g_primary_seat</dd></div>
|
||||
<div><dt>主驾座椅车端执行器</dt><dd class="simulated">CH1 · 左前排</dd></div>
|
||||
<div><dt>真实台架 CAN 下发</dt><dd class="blocked" id="hardwareCoverage">未配置</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-section result-workspace" aria-labelledby="resultWorkspaceTitle">
|
||||
<header class="result-workspace-heading">
|
||||
<div><h3 id="resultWorkspaceTitle">编排结果工作台</h3></div>
|
||||
<span class="result-workspace-badge"><i data-lucide="route"></i>REQUEST → CONFIG → CAN → RESULT</span>
|
||||
</header>
|
||||
<nav class="tabs result-nav" aria-label="结果快速导航">
|
||||
<button class="tab active" type="button" data-tab="trace"><span class="tab-index">01</span><i data-lucide="list-tree"></i><span class="tab-copy"><strong>决策轨迹</strong><small id="traceNavMeta">等待请求</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="config"><span class="tab-index">02</span><i data-lucide="layout-grid"></i><span class="tab-copy"><strong>配置卡片</strong><small id="configNavMeta">等待生成</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="plan"><span class="tab-index">03</span><i data-lucide="binary"></i><span class="tab-copy"><strong>CAN 计划</strong><small id="planNavMeta">等待编译</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="result"><span class="tab-index">04</span><i data-lucide="circle-check-big"></i><span class="tab-copy"><strong>执行结果</strong><small id="resultNavMeta">尚未执行</small></span><i data-lucide="arrow-down"></i></button>
|
||||
</nav>
|
||||
|
||||
<div class="tab-panel active" id="panel-trace" role="tabpanel">
|
||||
<div class="workspace-panel-title"><div><h3>决策轨迹与校验</h3></div><span class="workspace-panel-state" id="tracePanelState">IDLE</span></div>
|
||||
<div class="trace-layout">
|
||||
<div>
|
||||
<div class="detail-heading"><h3>可审计决策摘要</h3><span id="providerUsed">--</span></div>
|
||||
<ol class="decision-list" id="decisionList"><li>提交请求后显示识别目标、能力选择和约束来源。</li></ol>
|
||||
</div>
|
||||
<div>
|
||||
<div class="detail-heading"><h3>阶段事件</h3><span id="traceDuration">0 ms</span></div>
|
||||
<div class="trace-list" id="traceList"><div class="empty-line">暂无执行事件</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="validation-block">
|
||||
<div class="detail-heading"><h3>校验结果</h3><span id="validationCount">0 条</span></div>
|
||||
<div id="validationList" class="validation-list"><div class="empty-line">等待配置生成</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel config-panel" id="panel-config" role="tabpanel">
|
||||
<div class="config-panel-heading">
|
||||
<div><h3>指令配置与硬件映射</h3></div>
|
||||
<div class="config-actions"><button class="config-action" type="button" data-copy="config"><i data-lucide="copy"></i>复制 YAML</button><button class="config-action" type="button" data-download="config"><i data-lucide="download"></i>下载配置</button></div>
|
||||
</div>
|
||||
<div class="config-card-grid" id="configCards">
|
||||
<div class="config-empty"><i data-lucide="panel-top-dashed"></i><strong>等待生成配置</strong><span>提交车舱控制指令后,这里会显示控制对象、目标参数和硬件信号。</span></div>
|
||||
</div>
|
||||
<details class="config-source-details">
|
||||
<summary><span><i data-lucide="braces"></i>查看原始配置</span><small id="configFileName">functions/seat.yaml</small></summary>
|
||||
<pre id="configCode"><code># 等待生成 FunctionConfig</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel plan-panel" id="panel-plan" role="tabpanel">
|
||||
<div class="workspace-panel-title plan-panel-heading"><div><h3>CAN 编译计划</h3></div><button class="config-action" type="button" data-copy="plan"><i data-lucide="copy"></i>复制计划</button></div>
|
||||
<div class="frame-summary" id="frameSummary"><span>Frame ID</span><b>--</b><span>Payload</span><code>--</code><span>Mode</span><b>DRY-RUN</b></div>
|
||||
<details class="config-source-details plan-source-details">
|
||||
<summary><span><i data-lucide="binary"></i>查看原始 CAN 计划</span><small id="planFileName">command-plans/seat.yaml</small></summary>
|
||||
<pre id="planCode"><code># 等待 CAN 编译</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel result-panel" id="panel-result" role="tabpanel">
|
||||
<div class="workspace-panel-title"><div><h3>执行结果与硬件回读</h3></div><span class="workspace-panel-state" id="resultPanelState">IDLE</span></div>
|
||||
<div class="execution-summary" id="executionSummary">
|
||||
<i data-lucide="circle-dashed"></i><div><strong>尚未执行</strong><span>运行结果将在此处显示</span></div>
|
||||
</div>
|
||||
<div class="result-list" id="resultList"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<aside class="gesture-drawer" id="gestureDrawer" aria-hidden="true" aria-labelledby="gestureDrawerTitle">
|
||||
<header class="gesture-drawer-header">
|
||||
<div>
|
||||
<h2 id="gestureDrawerTitle">手势控制</h2>
|
||||
</div>
|
||||
<button class="gesture-close" id="gestureCloseButton" type="button" aria-label="关闭手势控制"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
|
||||
<div class="gesture-status-strip">
|
||||
<span class="gesture-state-pill idle" id="gestureStatePill"><i></i><b>未启动</b></span>
|
||||
<span id="gestureSafetyText">摄像头尚未开启</span>
|
||||
</div>
|
||||
|
||||
<div class="gesture-camera-stage">
|
||||
<video id="gestureVideo" playsinline muted></video>
|
||||
<canvas id="gestureCanvas"></canvas>
|
||||
<div class="gesture-camera-empty" id="gestureCameraEmpty">
|
||||
<span><i data-lucide="scan-face"></i></span>
|
||||
<strong>用手势控制副驾驶座椅</strong>
|
||||
<p>画面只在本机处理,不上传视频。</p>
|
||||
</div>
|
||||
<div class="gesture-live-badge" id="gestureLiveBadge" hidden><i></i>LIVE</div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-now">
|
||||
<div><span>当前手势</span><strong id="gestureDetected">—</strong></div>
|
||||
<i data-lucide="arrow-right"></i>
|
||||
<div><span>期望动作</span><strong id="gestureExpected">等待启动</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-progress"><i id="gestureProgressBar"></i></div>
|
||||
|
||||
<div class="gesture-actions">
|
||||
<button class="gesture-primary" id="gestureStartButton" type="button"><i data-lucide="camera"></i><span>开启摄像头</span></button>
|
||||
<button id="gestureStopButton" type="button" disabled><i data-lucide="octagon"></i>停止</button>
|
||||
</div>
|
||||
|
||||
<section class="gesture-guide-card">
|
||||
<div class="gesture-card-heading"><div><span>手势速查</span><small>张掌激活 · 握拳只停止</small></div><button id="gestureCustomizeButton" type="button"><i data-lucide="sliders-horizontal"></i>自定义</button></div>
|
||||
<div class="gesture-map-grid" id="gestureMapGrid"></div>
|
||||
<div class="gesture-customize" id="gestureCustomizePanel" hidden>
|
||||
<div><span>识别到</span><strong id="gestureCustomizeToken">请先摆出手势</strong></div>
|
||||
<select id="gestureActionSelect" aria-label="选择座椅动作"></select>
|
||||
<button id="gestureBindButton" type="button">绑定当前手势</button>
|
||||
<button id="gestureResetMapping" type="button">恢复默认</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="gesture-footnote"><i data-lucide="shield-check"></i><span>换动作无需重新张掌;连续 3 秒无有效动作后自动锁止。</span></footer>
|
||||
</aside>
|
||||
|
||||
<div class="mode-gallery-backdrop" id="modeGalleryBackdrop" hidden>
|
||||
<section class="mode-gallery" id="modeGallery" role="dialog" aria-modal="true" aria-labelledby="modeGalleryTitle">
|
||||
<header class="mode-gallery-header">
|
||||
<div>
|
||||
<h2 id="modeGalleryTitle">管理功能模式</h2>
|
||||
<p>按来源与运行方式整理,快速定位并管理模式</p>
|
||||
</div>
|
||||
<button class="mode-gallery-close" id="modeGalleryClose" type="button" aria-label="关闭功能模式管理"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
<div class="mode-gallery-summary"><span><i data-lucide="layers-3"></i>自定义模式 · 静态模式 · 动态模式</span><strong id="modeGallerySummaryCount">0</strong></div>
|
||||
<div class="mode-gallery-groups" id="modeGalleryGroups" aria-live="polite"></div>
|
||||
<footer class="mode-gallery-footer"><i data-lucide="info"></i><span>自定义模式支持编辑与删除;静态模式执行后保持目标状态;动态模式持续变化,可随时停止。</span></footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="mode-editor-backdrop" id="modeEditorBackdrop" hidden>
|
||||
<section class="mode-editor" role="dialog" aria-modal="true" aria-labelledby="modeEditorTitle">
|
||||
<div class="mode-editor-setup">
|
||||
<header class="mode-editor-header">
|
||||
<div>
|
||||
<h2 id="modeEditorTitle">编辑功能模式</h2>
|
||||
</div>
|
||||
<button id="modeEditorClose" type="button" aria-label="关闭模式编辑器"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
<div class="mode-editor-inputs">
|
||||
<label class="mode-editor-field mode-editor-name-field" for="modeEditorName">
|
||||
<span>模式名称</span>
|
||||
<input id="modeEditorName" maxlength="64" autocomplete="off" placeholder="输入模式名称" />
|
||||
</label>
|
||||
<label class="mode-editor-field mode-editor-instruction-field" for="modeEditorInstruction">
|
||||
<span>自然语言指令</span>
|
||||
<textarea id="modeEditorInstruction" maxlength="1000" rows="3" placeholder="例如:空调24度、风量2档,打开遮阳帘"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<section class="mode-editor-flow" aria-live="polite">
|
||||
<header>
|
||||
<span><i data-lucide="workflow"></i>控制指令流</span>
|
||||
<div class="mode-editor-flow-tools">
|
||||
<b id="modeEditorFlowCount">尚未生成</b>
|
||||
<button id="modeEditorFlowExpand" type="button" aria-expanded="false" aria-label="展开控制指令区域" title="展开控制指令区域">
|
||||
<i data-lucide="maximize-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mode-editor-flow-preview empty" id="modeEditorFlowPreview">
|
||||
<p>点击“生成指令”后,这里会显示由后端解析、校验并编译的控制指令流。</p>
|
||||
</div>
|
||||
</section>
|
||||
<p class="mode-editor-status" id="modeEditorStatus">修改自然语言后需要重新生成指令,才能点击完成永久保存。</p>
|
||||
<footer class="mode-editor-actions">
|
||||
<button id="modeEditorCancel" type="button">取消</button>
|
||||
<button class="mode-editor-execute" id="modeEditorExecute" type="button"><i data-lucide="wand-sparkles" data-icon="inline-start"></i><span>生成指令</span></button>
|
||||
<button class="mode-editor-complete" id="modeEditorComplete" type="button" disabled><i data-lucide="save" data-icon="inline-start"></i><span>完成</span></button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<button class="glass-theme-toggle" id="glassThemeToggle" type="button" aria-label="切换到白日模式" aria-pressed="true" title="切换到白日模式">
|
||||
<i data-lucide="sun" data-theme-icon="sun" aria-hidden="true"></i>
|
||||
<i data-lucide="moon" data-theme-icon="moon" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">切换白日/夜间模式</span>
|
||||
</button>
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createApp, nextTick } from "vue";
|
||||
|
||||
import App from "./App.vue";
|
||||
import "../styles.css";
|
||||
import "../console-v2.css";
|
||||
import "../self-extension.css";
|
||||
import "../speech-input.css";
|
||||
import "../assistant-chat.css";
|
||||
import "../energy-monitor.css";
|
||||
import "../ui-consistency.css";
|
||||
import "../codepen-glass-theme.css";
|
||||
import "./styles/design-system.css";
|
||||
import "../vue-ui.js";
|
||||
|
||||
const application = createApp(App);
|
||||
application.config.errorHandler = (error, instance, info) => {
|
||||
console.error("N7 Vue application error", { error, instance, info });
|
||||
};
|
||||
application.mount("#app");
|
||||
|
||||
document.querySelector("#app")?.removeAttribute("aria-busy");
|
||||
document.documentElement.classList.add("vue-vite-application");
|
||||
document.documentElement.dataset.vueVersion = application.version || "3";
|
||||
|
||||
nextTick(() => {
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:vue-mounted"));
|
||||
});
|
||||
|
||||
window.CognitiveAgentVueApp = Object.freeze({
|
||||
application,
|
||||
version: application.version || "3"
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
/* N7 Web Console design-system contract.
|
||||
* Loaded last by Vite so every legacy surface consumes one geometry,
|
||||
* typography, state and elevation scale while the runtime is migrated to
|
||||
* Vue components incrementally. */
|
||||
|
||||
:root {
|
||||
--n7-control-height-sm: 32px;
|
||||
--n7-control-height: 38px;
|
||||
--n7-icon-button-size: 38px;
|
||||
--n7-radius-control: 10px;
|
||||
--n7-radius-card: 14px;
|
||||
--n7-radius-section: 16px;
|
||||
--n7-radius-panel: 18px;
|
||||
--n7-radius-dialog: 22px;
|
||||
--n7-font-caption: 12px;
|
||||
--n7-font-body: 13px;
|
||||
--n7-font-card-title: 14px;
|
||||
--n7-font-panel-title: 16px;
|
||||
--n7-font-dialog-title: 22px;
|
||||
--n7-shadow-card: inset 0 1px 0 var(--glass-highlight), 0 7px 20px color-mix(in oklch, var(--foreground) 8%, transparent);
|
||||
--n7-shadow-panel: inset 0 1px 0 var(--glass-highlight), 0 14px 34px color-mix(in oklch, var(--foreground) 13%, transparent);
|
||||
--n7-shadow-dialog: inset 0 1px 0 var(--glass-highlight), 0 28px 84px color-mix(in oklch, var(--foreground) 28%, transparent);
|
||||
}
|
||||
|
||||
#app {
|
||||
min-width: inherit;
|
||||
min-height: 100dvh;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* Workspace panels: one top-level geometry, distinct from their children. */
|
||||
body.console-v2.v2-passive-display.v2-interactive-sync :is(
|
||||
.main-panel > .v2-mode-rail,
|
||||
#assistantDrawer,
|
||||
.joint-twin-shell > .twin-band,
|
||||
#v2DevicePanel,
|
||||
#v2ExtensionPanel .v2-extension-sync-mount
|
||||
) {
|
||||
border-radius: var(--n7-radius-panel) !important;
|
||||
box-shadow: var(--n7-shadow-panel) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-can-command-panel,
|
||||
.v2-nfc-inline,
|
||||
.v2-extension-gateway,
|
||||
.v2-extension-synced-device,
|
||||
.v2-extension-empty,
|
||||
.v2-pet-card,
|
||||
.v2-pet-presence-card,
|
||||
.v2-device-card,
|
||||
.v2-synced-device,
|
||||
.mode-gallery-card,
|
||||
.mode-editor-command-card,
|
||||
.energy-kpi,
|
||||
.energy-device-row,
|
||||
.assistant-dispatched-controls,
|
||||
.assistant-mode-draft,
|
||||
.assistant-mode-created,
|
||||
.assistant-control-suggestion
|
||||
) {
|
||||
border-radius: var(--n7-radius-card) !important;
|
||||
box-shadow: var(--n7-shadow-card) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.v2-sync-log,
|
||||
.v2-pet-card-region,
|
||||
.mode-gallery-group,
|
||||
.mode-editor-flow,
|
||||
.v2-nfc-stage,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card
|
||||
) {
|
||||
border-radius: var(--n7-radius-section) !important;
|
||||
}
|
||||
|
||||
/* Only actionable cards float. Read-only panels remain spatially stable. */
|
||||
body.console-v2 :is(
|
||||
.v2-sync-log,
|
||||
.v2-pet-card-region,
|
||||
.mode-editor-flow,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card
|
||||
):is(:hover, :focus-within) {
|
||||
transform: none !important;
|
||||
box-shadow: var(--n7-shadow-card) !important;
|
||||
}
|
||||
|
||||
/* The current DOM uses .twin-selector; the previous glass selector targeted
|
||||
* the removed .twin-seat-switch class and produced white-on-white tabs. */
|
||||
body.console-v2 .joint-twin-shell :is(.twin-selector, .twin-selector button, .twin-reset, .twin-focus-badge) {
|
||||
color: var(--muted-foreground) !important;
|
||||
background: var(--glass-card) !important;
|
||||
border-color: var(--glass-line) !important;
|
||||
box-shadow: inset 0 1px 0 var(--glass-highlight) !important;
|
||||
}
|
||||
|
||||
body.console-v2 .joint-twin-shell .twin-selector button.active {
|
||||
color: var(--primary-foreground) !important;
|
||||
background: var(--primary) !important;
|
||||
border-color: color-mix(in oklch, var(--primary) 72%, white 18%) !important;
|
||||
}
|
||||
|
||||
/* Shared heading hierarchy. */
|
||||
body.console-v2 :is(
|
||||
#assistantDrawer h2,
|
||||
.joint-twin-shell .twin-heading h3,
|
||||
.v2-device-sync-board > header h3,
|
||||
.v2-extension-sync-heading h3,
|
||||
.v2-mode-rail .mode-rail-title
|
||||
) {
|
||||
font-size: var(--n7-font-panel-title) !important;
|
||||
font-weight: 750 !important;
|
||||
line-height: 1.25 !important;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* The log title shares a 217 px header with its filter, so it needs the
|
||||
* compact card-title scale instead of the full panel-title scale. */
|
||||
body.console-v2 .v2-sync-log h3 {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: var(--n7-font-card-title) !important;
|
||||
font-weight: 750 !important;
|
||||
line-height: 1.25 !important;
|
||||
letter-spacing: -0.02em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-header h2,
|
||||
.mode-editor-header h2,
|
||||
.v2-extension-dialog > header h2,
|
||||
.v2-extension-dialog form > header h2,
|
||||
.energy-monitor-heading h2
|
||||
) {
|
||||
font-size: var(--n7-font-dialog-title) !important;
|
||||
font-weight: 750 !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-card-heading strong,
|
||||
.v2-device-card-focus strong,
|
||||
.v2-nfc-inline strong,
|
||||
.v2-extension-gateway strong,
|
||||
.v2-pet-card > header > strong,
|
||||
.energy-trend-card h3,
|
||||
.energy-devices-card h3
|
||||
) {
|
||||
font-size: var(--n7-font-card-title) !important;
|
||||
line-height: 1.35 !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-editor-status,
|
||||
.mode-gallery-footer,
|
||||
.v2-pet-settings-footnote,
|
||||
.v2-pet-card-fields > label > span,
|
||||
.v2-pet-nfc-field output,
|
||||
.energy-kpi > small,
|
||||
.energy-kpi > em,
|
||||
.energy-devices-card > footer
|
||||
) {
|
||||
font-size: var(--n7-font-caption) !important;
|
||||
line-height: 1.45 !important;
|
||||
}
|
||||
|
||||
/* One dialog radius, elevation and close-button size. */
|
||||
body.console-v2 :is(
|
||||
.mode-gallery,
|
||||
.mode-editor,
|
||||
.v2-nfc-dialog,
|
||||
.v2-gscu-tcp-dialog,
|
||||
.v2-pet-settings-dialog,
|
||||
.energy-monitor-page
|
||||
) {
|
||||
border-radius: var(--n7-radius-dialog) !important;
|
||||
box-shadow: var(--n7-shadow-dialog) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-close,
|
||||
.mode-editor-header > button,
|
||||
.v2-extension-dialog [data-dialog-close],
|
||||
.v2-extension-dialog [data-close-extension-dialog],
|
||||
.v2-extension-dialog [data-close-nfc-dialog],
|
||||
#energyMonitorClose
|
||||
) {
|
||||
width: var(--n7-icon-button-size) !important;
|
||||
height: var(--n7-icon-button-size) !important;
|
||||
min-width: var(--n7-icon-button-size);
|
||||
min-height: var(--n7-icon-button-size);
|
||||
border-radius: var(--n7-radius-control) !important;
|
||||
}
|
||||
|
||||
/* Compact icon actions still meet a usable hit target without becoming a
|
||||
* second visual system. */
|
||||
body.console-v2 :is(
|
||||
.twin-reset,
|
||||
.mode-gallery-edit,
|
||||
.mode-gallery-delete,
|
||||
.v2-pet-card > header > button
|
||||
) {
|
||||
width: var(--n7-control-height-sm) !important;
|
||||
height: var(--n7-control-height-sm) !important;
|
||||
min-width: var(--n7-control-height-sm);
|
||||
min-height: var(--n7-control-height-sm);
|
||||
border-radius: var(--n7-radius-control) !important;
|
||||
}
|
||||
|
||||
/* Offline hardware is a destructive state, not a primary/active state. */
|
||||
body.console-v2 .v2-nfc-stage:has(.v2-nfc-waiting.offline) > header {
|
||||
color: var(--status-error-foreground) !important;
|
||||
background: color-mix(in oklch, var(--destructive) 16%, var(--card)) !important;
|
||||
border-bottom: 1px solid color-mix(in oklch, var(--destructive) 32%, var(--border));
|
||||
}
|
||||
|
||||
body.console-v2 .v2-nfc-stage:has(.v2-nfc-waiting.offline) > header :is(span, small, h3, p, b) {
|
||||
color: var(--status-error-foreground) !important;
|
||||
}
|
||||
|
||||
/* Keep the global appearance control out of content and below every modal. */
|
||||
.glass-theme-toggle {
|
||||
left: max(18px, env(safe-area-inset-left)) !important;
|
||||
bottom: max(18px, env(safe-area-inset-bottom)) !important;
|
||||
z-index: 1100 !important;
|
||||
}
|
||||
|
||||
body.mode-gallery-open .glass-theme-toggle,
|
||||
body.mode-editor-open .glass-theme-toggle,
|
||||
body:has(.v2-extension-dialog[open]) .glass-theme-toggle,
|
||||
body:has(.energy-monitor-layer:not([hidden])) .glass-theme-toggle {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* The sidebar reserves the floating control's footprint instead of allowing
|
||||
* it to cover the log timeline. */
|
||||
body.console-v2 .v2-sync-log {
|
||||
margin-bottom: 58px;
|
||||
}
|
||||
|
||||
/* Top mode rail communicates that more content is available. */
|
||||
body.console-v2 .v2-mode-rail .mode-rail-content {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-mode-rail .mode-rail-content::after {
|
||||
width: 28px;
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0 0 0 auto;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(90deg, transparent, color-mix(in oklch, var(--glass-panel) 94%, transparent));
|
||||
}
|
||||
|
||||
@media (max-width: 1360px) {
|
||||
.glass-theme-toggle {
|
||||
width: 42px !important;
|
||||
height: 42px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body.console-v2 * {
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -289,29 +289,96 @@
|
||||
/* Delivery states use the same semantic feedback language in the sidebar and
|
||||
in the compact assistant command card. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-delivery:is(
|
||||
[data-delivery-state="blocked"],
|
||||
[data-delivery-state="failed"]
|
||||
) {
|
||||
.v2-can-command-list
|
||||
li[data-can-channel]
|
||||
.v2-can-command-delivery[data-delivery-state="failed"] {
|
||||
color: var(--destructive);
|
||||
background: color-mix(in oklch, var(--destructive) 10%, var(--card));
|
||||
border-color: color-mix(in oklch, var(--destructive) 28%, var(--border));
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li:is(
|
||||
[data-delivery-state="blocked"],
|
||||
[data-delivery-state="failed"]
|
||||
) {
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list
|
||||
li[data-can-channel]
|
||||
.v2-can-command-delivery[data-delivery-state="blocked"] {
|
||||
color: var(--status-warning-foreground);
|
||||
background: var(--status-warning-soft);
|
||||
border-color: color-mix(in oklch, var(--status-warning) 38%, var(--border));
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list
|
||||
li[data-can-channel]
|
||||
.v2-can-command-delivery[data-delivery-state="aborted"] {
|
||||
color: var(--muted-foreground);
|
||||
background: var(--muted);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list
|
||||
li[data-can-channel]
|
||||
.v2-can-command-delivery-detail {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
margin: 1px 0 2px;
|
||||
padding: 5px 7px;
|
||||
color: var(--muted-foreground);
|
||||
background: color-mix(in oklch, var(--muted) 62%, transparent);
|
||||
border-left: 2px solid var(--border);
|
||||
border-radius: var(--ui-radius-control-inner);
|
||||
font-size: 9px;
|
||||
line-height: 1.45;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list
|
||||
li[data-delivery-state="failed"]
|
||||
.v2-can-command-delivery-detail {
|
||||
color: var(--status-error-foreground);
|
||||
border-left-color: var(--destructive);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list
|
||||
li[data-delivery-state="blocked"]
|
||||
.v2-can-command-delivery-detail {
|
||||
color: var(--status-warning-foreground);
|
||||
border-left-color: var(--status-warning);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li[data-delivery-state="failed"] {
|
||||
background: color-mix(in oklch, var(--destructive) 4%, var(--card));
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li:is(
|
||||
[data-delivery-state="blocked"],
|
||||
[data-delivery-state="failed"]
|
||||
) .assistant-dispatched-control-icon {
|
||||
.assistant-dispatched-controls-list
|
||||
> li[data-delivery-state="failed"]
|
||||
.assistant-dispatched-control-icon {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li[data-delivery-state="blocked"] {
|
||||
background: var(--status-warning-soft);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list
|
||||
> li[data-delivery-state="blocked"]
|
||||
.assistant-dispatched-control-icon {
|
||||
color: var(--status-warning-foreground);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li[data-delivery-state="aborted"] {
|
||||
background: color-mix(in oklch, var(--muted) 68%, transparent);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list
|
||||
> li[data-delivery-state="aborted"]
|
||||
.assistant-dispatched-control-icon {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.console-v2 :is(
|
||||
#v2PanelControlDock > .v2-panel-control-button,
|
||||
.v2-mode-rail .mode-rail-actions > button,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { cpSync, copyFileSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const webRoot = dirname(fileURLToPath(import.meta.url));
|
||||
const outputRoot = resolve(webRoot, "dist");
|
||||
const backendOrigin = "http://127.0.0.1:8000";
|
||||
|
||||
const runtimeDirectories = ["assets", "icons", "vendor"];
|
||||
const runtimeFiles = [
|
||||
"manifest.webmanifest",
|
||||
"service-worker.js",
|
||||
"speech-audio-worklet.js",
|
||||
"login.html",
|
||||
"login.css",
|
||||
"login.js",
|
||||
"hardware-control.html",
|
||||
"hardware-control.css",
|
||||
"hardware-control.js",
|
||||
"network.js",
|
||||
"app.js",
|
||||
"gesture-control.js",
|
||||
"pwa.js",
|
||||
"console-v2.js",
|
||||
"self-extension.js",
|
||||
"speech-input.js",
|
||||
"assistant-chat.js",
|
||||
"energy-monitor.js",
|
||||
"styles.css",
|
||||
"console-v2.css",
|
||||
"self-extension.css",
|
||||
"speech-input.css",
|
||||
"assistant-chat.css",
|
||||
"energy-monitor.css",
|
||||
"ui-consistency.css",
|
||||
"codepen-glass-theme.css",
|
||||
"vue-ui.js"
|
||||
];
|
||||
|
||||
function copyRuntimeAssets() {
|
||||
return {
|
||||
name: "copy-n7-runtime-assets",
|
||||
apply: "build",
|
||||
closeBundle() {
|
||||
for (const directory of runtimeDirectories) {
|
||||
const source = resolve(webRoot, directory);
|
||||
if (existsSync(source)) {
|
||||
cpSync(source, resolve(outputRoot, directory), { recursive: true });
|
||||
}
|
||||
}
|
||||
for (const file of runtimeFiles) {
|
||||
const source = resolve(webRoot, file);
|
||||
if (!existsSync(source)) continue;
|
||||
const target = resolve(outputRoot, file);
|
||||
mkdirSync(dirname(target), { recursive: true });
|
||||
copyFileSync(source, target);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function backendProxy() {
|
||||
return {
|
||||
target: backendOrigin,
|
||||
changeOrigin: true,
|
||||
configure(proxy) {
|
||||
proxy.on("proxyReq", (request) => request.setHeader("origin", backendOrigin));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
root: webRoot,
|
||||
publicDir: false,
|
||||
plugins: [vue(), copyRuntimeAssets()],
|
||||
server: {
|
||||
strictPort: true,
|
||||
fs: { allow: [webRoot] },
|
||||
proxy: {
|
||||
"/api": backendProxy(),
|
||||
"/artifacts": backendProxy()
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: outputRoot,
|
||||
emptyOutDir: true,
|
||||
assetsDir: "vite-assets",
|
||||
target: "es2022",
|
||||
sourcemap: true
|
||||
}
|
||||
});
|
||||
@@ -1,20 +1,9 @@
|
||||
import { Fragment, createApp, h, nextTick, reactive } from "vue";
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const VUE_VERSION = "3.5.21";
|
||||
const vue = window.Vue;
|
||||
const unavailable = {
|
||||
available: false,
|
||||
version: VUE_VERSION,
|
||||
renderDispatchedControls: () => false,
|
||||
renderModeChips: () => false,
|
||||
};
|
||||
if (!vue?.createApp || !vue?.h || !vue?.reactive) {
|
||||
window.CognitiveAgentVueUI = unavailable;
|
||||
return;
|
||||
}
|
||||
|
||||
const { Fragment, createApp, h, nextTick, reactive } = vue;
|
||||
const modeMounts = new WeakMap();
|
||||
const commandMounts = new WeakMap();
|
||||
const text = (value, fallback = "") => String(value ?? fallback);
|
||||
|
||||
Reference in New Issue
Block a user