优化后端
This commit is contained in:
+178
-9
@@ -92,6 +92,24 @@ const REQUEST_TIMEOUT_MS = 45 * 1000;
|
||||
const DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS = 5000;
|
||||
const AUTHORITATIVE_CH1_SOURCES = new Set(["physical_can_ch1", "wireless_mqtt_ch1"]);
|
||||
|
||||
async function apiResponseError(response) {
|
||||
let payload = null;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (_error) {
|
||||
// Some proxies return an empty or non-JSON error page. Preserve the HTTP
|
||||
// status even when no structured reason is available.
|
||||
}
|
||||
const status = Number(response?.status) || 0;
|
||||
const serverMessage = String(payload?.message || payload?.error || "").trim();
|
||||
const fallback = status ? `HTTP ${status}` : "请求失败";
|
||||
const error = new Error(serverMessage ? `${fallback}:${serverMessage}` : fallback);
|
||||
error.status = status;
|
||||
error.code = String(payload?.error || "").trim();
|
||||
error.serverMessage = serverMessage;
|
||||
return error;
|
||||
}
|
||||
|
||||
function requestTimeoutForExecutionMode(mode) {
|
||||
return mode === "hardware" ? HARDWARE_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
|
||||
}
|
||||
@@ -111,6 +129,8 @@ function renderCabinLiveStatus(networkStatus = state.connectionStatus) {
|
||||
if (!indicator) return;
|
||||
const label = $("b", indicator);
|
||||
if (!label) return;
|
||||
indicator.removeAttribute("aria-label");
|
||||
indicator.removeAttribute("title");
|
||||
const summarySystem = $("#cabinSummarySystem");
|
||||
const setSummarySystem = (text, status) => {
|
||||
if (!summarySystem) return;
|
||||
@@ -144,6 +164,22 @@ function renderCabinLiveStatus(networkStatus = state.connectionStatus) {
|
||||
}
|
||||
|
||||
const snapshot = state.vehicleStateSnapshot;
|
||||
if (snapshot?.errorStatus === 403) {
|
||||
const reason = snapshot.errorReason || "当前终端没有车辆状态读取权限";
|
||||
const visibleReason = `${transportLabel} · 403:${reason}`;
|
||||
indicator.dataset.state = "offline";
|
||||
label.textContent = visibleReason;
|
||||
indicator.title = visibleReason;
|
||||
indicator.setAttribute("aria-label", visibleReason);
|
||||
const telemetryState = $("#vehicleTelemetryState");
|
||||
if (telemetryState) telemetryState.textContent = "403 拒绝";
|
||||
setSummarySystem("403 · 状态读取被拒绝", "warning");
|
||||
return;
|
||||
}
|
||||
const telemetryState = $("#vehicleTelemetryState");
|
||||
if (telemetryState?.textContent === "403 拒绝") {
|
||||
telemetryState.textContent = "只读";
|
||||
}
|
||||
const observedIds = Array.isArray(snapshot?.observedFrameIds)
|
||||
? snapshot.observedFrameIds
|
||||
: [];
|
||||
@@ -5884,19 +5920,24 @@ function installModeChipScroller() {
|
||||
let pointerStartScroll = 0;
|
||||
let dragged = false;
|
||||
let suppressClick = false;
|
||||
let pointerCaptured = false;
|
||||
group.addEventListener("pointerdown", (event) => {
|
||||
if (event.pointerType !== "mouse" || event.button !== 0 || maximumScroll() <= 1) return;
|
||||
pointerId = event.pointerId;
|
||||
pointerStartX = event.clientX;
|
||||
pointerStartScroll = group.scrollLeft;
|
||||
dragged = false;
|
||||
group.setPointerCapture?.(pointerId);
|
||||
pointerCaptured = false;
|
||||
});
|
||||
group.addEventListener("pointermove", (event) => {
|
||||
if (pointerId !== event.pointerId) return;
|
||||
const distance = event.clientX - pointerStartX;
|
||||
if (!dragged && Math.abs(distance) < 5) return;
|
||||
dragged = true;
|
||||
if (!pointerCaptured && typeof group.setPointerCapture === "function") {
|
||||
group.setPointerCapture(pointerId);
|
||||
pointerCaptured = true;
|
||||
}
|
||||
group.classList.add("is-dragging");
|
||||
group.scrollLeft = Math.max(0, Math.min(maximumScroll(), pointerStartScroll - distance));
|
||||
updateScrollState();
|
||||
@@ -5904,8 +5945,9 @@ function installModeChipScroller() {
|
||||
});
|
||||
const finishPointerDrag = (event) => {
|
||||
if (pointerId !== event.pointerId) return;
|
||||
group.releasePointerCapture?.(pointerId);
|
||||
if (pointerCaptured) group.releasePointerCapture?.(pointerId);
|
||||
pointerId = null;
|
||||
pointerCaptured = false;
|
||||
group.classList.remove("is-dragging");
|
||||
if (dragged) {
|
||||
suppressClick = true;
|
||||
@@ -6094,11 +6136,113 @@ function installModeActionPreview() {
|
||||
window.addEventListener("resize", () => hideModeActionPopover({ immediate: true }), { passive: true });
|
||||
}
|
||||
|
||||
function vueModeChipEntry(mode) {
|
||||
const timeline = mode?.timeline;
|
||||
const actionCount = modeExecutionActionViews(mode).length
|
||||
|| Number(mode?.operation_count || 0);
|
||||
const phaseSummary = Array.isArray(timeline?.phases)
|
||||
? timeline.phases.map((phase) => {
|
||||
if (Number(phase.duration_ms || 0) > 0) {
|
||||
return `${phase.name} ${modeSecondsLabel(phase.duration_ms)}s`;
|
||||
}
|
||||
const timeout = Number(phase.timeout_ms || 0);
|
||||
return timeout > 0
|
||||
? `${phase.name} 目标到位(保护上限${modeSecondsLabel(timeout)}s)`
|
||||
: `${phase.name} 目标到位`;
|
||||
}).join(" → ")
|
||||
: "";
|
||||
let cycleLabel = "";
|
||||
if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
|
||||
const runtimeMinutes = Number(timeline.max_runtime_ms || 0) / 60000;
|
||||
const runtimeLabel = Number.isInteger(runtimeMinutes)
|
||||
? String(runtimeMinutes)
|
||||
: runtimeMinutes.toFixed(1);
|
||||
const cycleDurationMs = Number(timeline.cycle_duration_ms || 0);
|
||||
const cycle = cycleDurationMs > 0
|
||||
? `${modeSecondsLabel(cycleDurationMs)}s循环`
|
||||
: "目标到位循环";
|
||||
cycleLabel = runtimeMinutes > 0 ? `${cycle} · 最长${runtimeLabel}min` : cycle;
|
||||
}
|
||||
return {
|
||||
type: "mode",
|
||||
key: `mode-${mode.id}`,
|
||||
id: mode.id,
|
||||
label: modeChipLabel(mode),
|
||||
summary: modeChipSummary(mode),
|
||||
icon: mode.icon || "bookmark",
|
||||
command: `进入${mode.name}`,
|
||||
origin: mode?.origin || "preset",
|
||||
behavior: cycleLabel ? "cyclic" : "",
|
||||
cycleLabel,
|
||||
ariaLabel: `${mode.name || mode.id},${actionCount} 项执行动作,点击执行`,
|
||||
title: [mode.description || mode.name, phaseSummary, "悬浮或聚焦查看全部执行动作"]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
function vueModeChipEntries(modes) {
|
||||
const preferredStaticOrder = [
|
||||
"zero_gravity",
|
||||
"office",
|
||||
"nap",
|
||||
"queen_passenger_driving",
|
||||
"leisure_lounger",
|
||||
];
|
||||
const userModes = modes.filter(isUserModeProfile);
|
||||
const dynamicOrder = new Map([["rocking_horse", 0], ["dynamic_cradle", 1]]);
|
||||
const dynamicModes = modes
|
||||
.filter((mode) => !isUserModeProfile(mode) && isDynamicModeProfile(mode))
|
||||
.sort((left, right) => (
|
||||
(dynamicOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER)
|
||||
- (dynamicOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER)
|
||||
));
|
||||
const staticModes = modes.filter(
|
||||
(mode) => !isUserModeProfile(mode) && !isDynamicModeProfile(mode),
|
||||
);
|
||||
const preferredStaticModes = preferredStaticOrder
|
||||
.map((modeId) => staticModes.find((mode) => mode.id === modeId))
|
||||
.filter(Boolean);
|
||||
const otherStaticModes = staticModes.filter((mode) => !preferredStaticModes.includes(mode));
|
||||
const entries = [];
|
||||
const appendLabel = (label, id) => entries.push({
|
||||
type: "label",
|
||||
key: `label-${id}`,
|
||||
id,
|
||||
label,
|
||||
});
|
||||
const appendDivider = (id) => entries.push({
|
||||
type: "divider",
|
||||
key: `divider-${id}`,
|
||||
id,
|
||||
});
|
||||
if (userModes.length) {
|
||||
appendLabel("自定义模式", "custom");
|
||||
entries.push(...userModes.map(vueModeChipEntry));
|
||||
}
|
||||
if (userModes.length && (staticModes.length || dynamicModes.length)) appendDivider("custom");
|
||||
if (staticModes.length) {
|
||||
appendLabel("静态模式", "static");
|
||||
entries.push(...preferredStaticModes.map(vueModeChipEntry));
|
||||
entries.push(...otherStaticModes.map(vueModeChipEntry));
|
||||
}
|
||||
if (dynamicModes.length) {
|
||||
if (userModes.length || staticModes.length) appendDivider("dynamic");
|
||||
appendLabel("动态模式", "dynamic");
|
||||
entries.push(...dynamicModes.map(vueModeChipEntry));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function populateModeChips() {
|
||||
const group = $(".mode-chips");
|
||||
if (!group) return;
|
||||
const buttons = () => $$("button", group);
|
||||
const modes = modeProfiles();
|
||||
if (window.CognitiveAgentVueUI?.renderModeChips?.(group, vueModeChipEntries(modes))) {
|
||||
populateModeGallery();
|
||||
return;
|
||||
}
|
||||
const runtimeModeIds = new Set(modes.map((mode) => mode.id));
|
||||
$$('button[data-mode-origin="user"]', group).forEach((button) => {
|
||||
if (!runtimeModeIds.has(button.dataset.modeProfileId)) button.remove();
|
||||
@@ -6155,6 +6299,8 @@ function populateModeChips() {
|
||||
runIcon.dataset.lucide = "play";
|
||||
button.append(runIcon);
|
||||
}
|
||||
runIcon.dataset.modeRun = "true";
|
||||
runIcon.title = `执行${mode.name || mode.id}`;
|
||||
const timeline = mode.timeline;
|
||||
let cycleBadge = $(".mode-cycle-badge", button);
|
||||
if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
|
||||
@@ -6330,8 +6476,10 @@ function setModeChipsDisabled(disabled) {
|
||||
});
|
||||
}
|
||||
|
||||
async function executeSavedModeProfile(mode) {
|
||||
async function executeSavedModeProfile(mode, { command = "" } = {}) {
|
||||
if (!mode?.id || !mode?.command_flow?.source_request_id) return false;
|
||||
const modeCommand = String(command || `进入${mode.name || mode.id}`).trim();
|
||||
const assistant = window.CognitiveAgentAssistant;
|
||||
const driverOnlyCh1 = isDriverOnlyCh1Runtime();
|
||||
if (state.executionMode === "hardware"
|
||||
&& state.controlTransport !== "wireless_mqtt"
|
||||
@@ -6346,10 +6494,13 @@ async function executeSavedModeProfile(mode) {
|
||||
|
||||
closeModeGallery({ restoreFocus: false });
|
||||
const runButton = $("#runButton");
|
||||
const input = $("#utterance");
|
||||
runButton.disabled = true;
|
||||
setModeChipsDisabled(true);
|
||||
setModeExecutionState(mode.id, "running");
|
||||
$("#utterance").value = mode.name || mode.id;
|
||||
assistant?.open?.();
|
||||
input.value = modeCommand;
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
setRunStatus("processing", "MODE COMMAND FLOW");
|
||||
$("#resultTitle").textContent = `正在直接执行${mode.name}`;
|
||||
try {
|
||||
@@ -6375,11 +6526,11 @@ async function executeSavedModeProfile(mode) {
|
||||
if (!payload) throw new Error("模式卡片未返回执行结果");
|
||||
renderResult(payload);
|
||||
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
|
||||
$("#utterance").value = mode.name || mode.id;
|
||||
$("#resultTitle").textContent = completed
|
||||
? `${mode.name}执行完成`
|
||||
: `${mode.name}执行未完成`;
|
||||
setModeExecutionState(mode.id, completed ? "succeeded" : "failed");
|
||||
assistant?.announceModeExecution?.(mode, payload, { command: modeCommand });
|
||||
if (state.executionMode === "hardware") await synchronizePhysicalSeatState();
|
||||
showToast(completed
|
||||
? `${mode.name}已直接执行保存的控制指令流`
|
||||
@@ -6388,8 +6539,13 @@ async function executeSavedModeProfile(mode) {
|
||||
setModeExecutionState(mode.id, "failed");
|
||||
setRunStatus("failed", "MODE FAILED");
|
||||
$("#resultTitle").textContent = `${mode.name}执行失败`;
|
||||
assistant?.announceModeExecution?.(mode, null, { command: modeCommand, error });
|
||||
showToast(error.message || "模式执行失败");
|
||||
} finally {
|
||||
if (input.value === modeCommand) {
|
||||
input.value = "";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
runButton.disabled = false;
|
||||
setModeChipsDisabled(false);
|
||||
}
|
||||
@@ -6412,6 +6568,10 @@ function applyModeCommand(command, modeProfileId = null) {
|
||||
setModeRailSelection(mode);
|
||||
closeModeGallery({ restoreFocus: false });
|
||||
assistant?.open?.();
|
||||
if (mode?.command_flow?.source_request_id) {
|
||||
void executeSavedModeProfile(mode, { command: modeCommand });
|
||||
return true;
|
||||
}
|
||||
const input = $("#utterance");
|
||||
const form = $("#requestForm");
|
||||
if (!input || !form) {
|
||||
@@ -6468,7 +6628,7 @@ function applyRuntimeTargetPolicy(runtime) {
|
||||
async function loadRuntime() {
|
||||
try {
|
||||
const response = await apiRequest("/api/runtime", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
if (!response.ok) throw await apiResponseError(response);
|
||||
state.runtime = await response.json();
|
||||
renderControlTransport();
|
||||
applyRuntimeTargetPolicy(state.runtime);
|
||||
@@ -6487,6 +6647,7 @@ async function loadRuntime() {
|
||||
mqttDualChannel ? vehicleCan.configured : hardware.configured,
|
||||
);
|
||||
const vehicleHardwareReady = Boolean(vehicleCan.configured);
|
||||
const linkedHardwareReady = seatHardwareReady && vehicleHardwareReady;
|
||||
const ch1DriverOnly = vehicleHardwareReady && !seatHardwareReady;
|
||||
const hardwareExecutionReady = Boolean(hardware.configured || vehicleCan.configured);
|
||||
const gptButton = $('[data-provider="openai"]');
|
||||
@@ -6563,7 +6724,7 @@ async function synchronizePassengerSeatState() {
|
||||
window.location.replace("/login");
|
||||
return false;
|
||||
}
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
if (!response.ok) throw await apiResponseError(response);
|
||||
const payload = await response.json();
|
||||
if (requestedTransport !== state.controlTransport) return false;
|
||||
updateWiredCanProbe("passenger", true, requestedTransport);
|
||||
@@ -6605,7 +6766,7 @@ async function synchronizeDriverSeatState() {
|
||||
window.location.replace("/login");
|
||||
return false;
|
||||
}
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
if (!response.ok) throw await apiResponseError(response);
|
||||
const payload = await response.json();
|
||||
if (requestedTransport !== state.controlTransport) return false;
|
||||
updateWiredCanProbe("vehicle", true, requestedTransport);
|
||||
@@ -6627,6 +6788,9 @@ async function synchronizeDriverSeatState() {
|
||||
state.vehicleStateSnapshot = {
|
||||
...(state.vehicleStateSnapshot || {}),
|
||||
error: error.message || String(error),
|
||||
errorStatus: Number(error.status) || null,
|
||||
errorCode: error.code || "",
|
||||
errorReason: error.serverMessage || error.message || String(error),
|
||||
};
|
||||
renderCabinLiveStatus();
|
||||
if (!["physical_can_ch1", "wireless_mqtt_ch1"].includes(state.seatFeedbackSources.driver)) {
|
||||
@@ -7950,6 +8114,10 @@ function recordVehicleStateTelemetry(payload) {
|
||||
: "physical_can_ch1";
|
||||
state.vehicleStateSnapshot = {
|
||||
...previous,
|
||||
error: null,
|
||||
errorStatus: null,
|
||||
errorCode: "",
|
||||
errorReason: "",
|
||||
source,
|
||||
capturedAt: payload.captured_at || null,
|
||||
receivedAt: Date.now(),
|
||||
@@ -8716,7 +8884,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
installModeActionPreview();
|
||||
$(".mode-chips").addEventListener("click", (event) => {
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
const button = target?.closest("button[data-mode-command]");
|
||||
const runControl = target?.closest("[data-mode-run]");
|
||||
const button = (runControl || target)?.closest("button[data-mode-command]");
|
||||
if (button) applyModeCommand(
|
||||
button.dataset.modeCommand,
|
||||
button.dataset.modeProfileId || null,
|
||||
|
||||
@@ -1506,3 +1506,220 @@
|
||||
min-height: 32px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Compact command receipt inside each AI assistant response. */
|
||||
.assistant-message-with-dispatched-controls {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.assistant-message-with-dispatched-controls .assistant-message-body {
|
||||
min-width: 0;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-vue-host {
|
||||
width: 100%;
|
||||
max-width: 336px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls {
|
||||
width: 100%;
|
||||
max-width: 336px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
color: var(--assistant-text, #233044);
|
||||
background: color-mix(in oklch, var(--card) 91%, var(--accent));
|
||||
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 24px color-mix(in oklch, var(--foreground) 7%, transparent);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls > summary {
|
||||
min-height: 46px;
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr) auto 18px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 11px;
|
||||
color: var(--heading-foreground, #1e2f46);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls > summary:focus-visible {
|
||||
outline: 2px solid color-mix(in oklch, var(--primary) 42%, transparent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-heading-icon,
|
||||
.assistant-dispatched-control-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
color: var(--primary, #2771e7);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-heading-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: color-mix(in oklch, var(--primary) 9%, var(--card));
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-heading-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke-width: 1.9;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls > summary > b {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-count {
|
||||
min-width: 25px;
|
||||
height: 22px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
padding: 0 7px;
|
||||
color: var(--primary, #2771e7);
|
||||
background: color-mix(in oklch, var(--primary) 10%, var(--card));
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-chevron {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
color: var(--muted-foreground, #67768b);
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls[open] > summary .assistant-dispatched-controls-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
border-top: 1px solid color-mix(in oklch, var(--primary) 17%, var(--border));
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li {
|
||||
min-height: 55px;
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li + li {
|
||||
border-top: 1px solid color-mix(in oklch, var(--primary) 17%, var(--border));
|
||||
}
|
||||
|
||||
.assistant-dispatched-control-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-control-icon svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
|
||||
.assistant-dispatched-control-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--muted-foreground, #5b687a);
|
||||
font-size: 12px;
|
||||
font-weight: 680;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.assistant-dispatched-control-value {
|
||||
max-width: 112px;
|
||||
overflow: hidden;
|
||||
color: var(--heading-foreground, #26364b);
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* The function-mode strip is now the only homepage header. */
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel,
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .main-panel {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-output-shell,
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .v2-output-shell {
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail .v2-module-switcher,
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .main-panel > .v2-mode-rail .v2-module-switcher {
|
||||
width: auto;
|
||||
min-width: max-content;
|
||||
min-height: 0;
|
||||
position: static;
|
||||
inset: auto;
|
||||
z-index: auto;
|
||||
display: block;
|
||||
grid-column: auto;
|
||||
grid-row: auto;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
@media (min-width: 901px) {
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .v2-operation-shell {
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-output-shell {
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-operation-shell {
|
||||
top: 108px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,6 +502,282 @@
|
||||
followLatest(true);
|
||||
}
|
||||
|
||||
const assistantControlLabels = {
|
||||
"climate.set_power": "空调电源",
|
||||
"climate.set_temperature": "空调温度",
|
||||
"climate.set_fan_level": "空调风量",
|
||||
"climate.set_outlet_mode": "出风模式",
|
||||
"climate.set_control_mode": "空调控制方式",
|
||||
"climate.set_cycle_mode": "空调循环",
|
||||
"interior_light.set_indoor_power": "室内灯",
|
||||
"interior_light.set_reading_power": "阅读灯",
|
||||
"ambient_light.set_power": "氛围灯",
|
||||
"ambient_light.set_color_index": "氛围灯颜色",
|
||||
"ambient_light.set_brightness_level": "氛围灯亮度",
|
||||
"ambient_light.set_illumination": "氛围灯颜色与亮度",
|
||||
"ambient_light.set_mode": "氛围灯模式",
|
||||
"roof.set_shade_motion": "遮阳帘",
|
||||
"roof.set_shade_position": "遮阳帘位置",
|
||||
"window.set_motion": "车窗",
|
||||
"window.set_position": "车窗位置",
|
||||
"door.set_rear_lock": "后排门锁",
|
||||
"door.set_vehicle_lock": "整车门锁",
|
||||
"infrared.emit": "红外外设",
|
||||
};
|
||||
|
||||
function assistantControlCategory(operation = {}, fallback = "控制指令") {
|
||||
const capability = String(operation.capability || "");
|
||||
if (capability === "infrared.emit") {
|
||||
const deviceName = String(
|
||||
operation.target?.name || operation.target?.device || "红外外设",
|
||||
).trim();
|
||||
return deviceName || "红外外设";
|
||||
}
|
||||
if (assistantControlLabels[capability]) return assistantControlLabels[capability];
|
||||
if (capability.startsWith("seat.")) {
|
||||
const target = String(operation.target?.zone || operation.target?.instance || "")
|
||||
.replaceAll("_", "-");
|
||||
return `${["passenger", "front-right"].includes(target) ? "副驾" : "主驾"}座椅`;
|
||||
}
|
||||
if (capability.startsWith("outlet.")) return "空调风口";
|
||||
return String(fallback || capability || "控制指令");
|
||||
}
|
||||
|
||||
function fallbackAssistantControlCommands(result = {}) {
|
||||
return (Array.isArray(result.validated_control_commands)
|
||||
? result.validated_control_commands
|
||||
: [])
|
||||
.map((presentation) => {
|
||||
const operation = presentation?.operation || {};
|
||||
const command = String(presentation?.actual_command || "").trim();
|
||||
if (!command) return null;
|
||||
const capability = String(operation.capability || "");
|
||||
const target = operation.target || {};
|
||||
const targetValue = target.zone || target.instance || target.device;
|
||||
const semanticArguments = [];
|
||||
if (targetValue != null && String(targetValue).trim()) {
|
||||
semanticArguments.push(`target=${String(targetValue).replaceAll("_", "-")}`);
|
||||
}
|
||||
Object.entries(operation.parameters || {})
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.forEach(([key, value]) => semanticArguments.push(`${key}=${String(value)}`));
|
||||
return {
|
||||
category: assistantControlCategory(operation, capability),
|
||||
channel: String(presentation?.channel || ""),
|
||||
command,
|
||||
deliveryState: "not_submitted",
|
||||
operation,
|
||||
operationRef: String(operation.id || ""),
|
||||
semantic: `${capability || "control"}(${semanticArguments.join(", ")})`,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function assistantDispatchedCommands(result = {}) {
|
||||
const projections = Array.isArray(result.validated_control_commands)
|
||||
? result.validated_control_commands
|
||||
: [];
|
||||
const projectionByRef = new Map(projections.map((presentation) => [
|
||||
String(presentation?.operation?.id || ""),
|
||||
presentation?.operation || {},
|
||||
]));
|
||||
let commands = [];
|
||||
try {
|
||||
const sharedCommands = window.CognitiveAgentConsoleV2?.getParsedCanCommands?.(result);
|
||||
if (Array.isArray(sharedCommands)) commands = sharedCommands;
|
||||
} catch (_error) {
|
||||
commands = [];
|
||||
}
|
||||
if (!commands.length) return fallbackAssistantControlCommands(result);
|
||||
return commands.map((item, index) => {
|
||||
const operation = projectionByRef.get(String(item.operationRef || ""))
|
||||
|| projections[index]?.operation
|
||||
|| {};
|
||||
return {
|
||||
...item,
|
||||
category: assistantControlCategory(operation, item.category),
|
||||
operation,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function localizedAssistantControlValue(value) {
|
||||
const normalized = String(value ?? "").trim().toLowerCase();
|
||||
return {
|
||||
on: "开启",
|
||||
open: "开启",
|
||||
true: "开启",
|
||||
off: "关闭",
|
||||
close: "关闭",
|
||||
false: "关闭",
|
||||
half: "一半",
|
||||
stop: "停止",
|
||||
lock: "上锁",
|
||||
unlock: "解锁",
|
||||
auto: "自动",
|
||||
manual: "手动",
|
||||
internal: "内循环",
|
||||
external: "外循环",
|
||||
}[normalized] || String(value ?? "").trim();
|
||||
}
|
||||
|
||||
function assistantControlValue(item = {}) {
|
||||
const parameters = item.operation?.parameters || {};
|
||||
const numericValue = (value) => (
|
||||
value !== null && value !== undefined && value !== "" && Number.isFinite(Number(value))
|
||||
? Number(value)
|
||||
: null
|
||||
);
|
||||
const temperature = numericValue(parameters.temperature_c);
|
||||
if (temperature !== null) return `${temperature}°C`;
|
||||
const position = numericValue(parameters.position_pct ?? parameters.shade_position_pct);
|
||||
if (position !== null) return `${position}%`;
|
||||
const illumination = String(parameters.color_hex || "").trim();
|
||||
const brightnessPercent = numericValue(parameters.brightness_pct);
|
||||
if (illumination && brightnessPercent !== null) return `${illumination} · ${brightnessPercent}%`;
|
||||
if (illumination) return illumination;
|
||||
if (brightnessPercent !== null) return `${brightnessPercent}%`;
|
||||
const brightness = numericValue(parameters.brightness_level);
|
||||
if (brightness !== null) return `${brightness}级`;
|
||||
const level = numericValue(parameters.level ?? parameters.fan_level);
|
||||
if (level !== null) return `${level}档`;
|
||||
const state = parameters.motion
|
||||
?? parameters.state
|
||||
?? parameters.power
|
||||
?? parameters.action
|
||||
?? parameters.mode;
|
||||
if (state !== null && state !== undefined && String(state).trim()) {
|
||||
return localizedAssistantControlValue(state);
|
||||
}
|
||||
const commandTail = String(item.command || "").trim().split(/\s+/).at(-1);
|
||||
return localizedAssistantControlValue(commandTail) || String(item.command || "控制");
|
||||
}
|
||||
|
||||
function assistantControlIcon(item = {}) {
|
||||
const capability = String(item.operation?.capability || "");
|
||||
const category = String(item.category || "");
|
||||
if (capability.startsWith("climate.") || category.includes("空调")) return "thermometer";
|
||||
if (capability.includes("reading") || category.includes("阅读灯")) return "book-open";
|
||||
if (capability.includes("light") || category.includes("灯")) return "lightbulb";
|
||||
if (capability.startsWith("roof.") || category.includes("遮阳帘")) return "panel-top";
|
||||
if (capability.startsWith("window.") || category.includes("车窗")) return "panels-top-left";
|
||||
if (capability.startsWith("seat.") || category.includes("座椅")) return "armchair";
|
||||
if (capability.startsWith("door.") || category.includes("门锁")) return "lock-keyhole";
|
||||
if (capability === "infrared.emit") return "radio-tower";
|
||||
return "binary";
|
||||
}
|
||||
|
||||
function appendDispatchedControls(message, result = {}) {
|
||||
if (!message?.body || !result || typeof result !== "object") return;
|
||||
const previous = message.dispatchedControlResult || {};
|
||||
const mergedResult = { ...previous, ...result };
|
||||
if (!Array.isArray(result.validated_control_commands)
|
||||
&& Array.isArray(previous.validated_control_commands)) {
|
||||
mergedResult.validated_control_commands = previous.validated_control_commands;
|
||||
}
|
||||
message.dispatchedControlResult = mergedResult;
|
||||
const commands = assistantDispatchedCommands(mergedResult);
|
||||
if (!commands.length) return;
|
||||
|
||||
const signature = JSON.stringify(commands.map((item) => [
|
||||
item.operationRef,
|
||||
item.category,
|
||||
item.command,
|
||||
item.semantic,
|
||||
item.deliveryState,
|
||||
item.deliveryLabel,
|
||||
item.deliveryDetail,
|
||||
]));
|
||||
const vueRenderer = window.CognitiveAgentVueUI?.renderDispatchedControls;
|
||||
if (typeof vueRenderer === "function") {
|
||||
let host = $(".assistant-dispatched-controls-vue-host", message.body);
|
||||
if (host?.dataset.commandSignature === signature) return;
|
||||
if (!host) {
|
||||
host = document.createElement("div");
|
||||
host.className = "assistant-dispatched-controls-vue-host";
|
||||
message.body.append(host);
|
||||
}
|
||||
const viewCommands = commands.map((item, index) => ({
|
||||
key: item.operationRef || `${item.semantic || item.command}-${index}`,
|
||||
category: String(item.category || "控制指令"),
|
||||
value: assistantControlValue(item),
|
||||
icon: assistantControlIcon(item),
|
||||
semantic: String(item.semantic || ""),
|
||||
command: String(item.command || ""),
|
||||
deliveryState: String(item.deliveryState || "not_submitted"),
|
||||
deliveryLabel: String(item.deliveryLabel || "未下发"),
|
||||
deliveryDetail: String(item.deliveryDetail || ""),
|
||||
}));
|
||||
if (vueRenderer(host, viewCommands)) {
|
||||
host.dataset.commandSignature = signature;
|
||||
message.article?.classList.add("assistant-message-with-dispatched-controls");
|
||||
followLatest(true);
|
||||
return;
|
||||
}
|
||||
host.remove();
|
||||
}
|
||||
let card = $(".assistant-dispatched-controls", message.body);
|
||||
if (card?.dataset.commandSignature === signature) return;
|
||||
if (!card) {
|
||||
card = document.createElement("details");
|
||||
card.className = "assistant-dispatched-controls";
|
||||
card.open = true;
|
||||
const summary = document.createElement("summary");
|
||||
const headingIcon = document.createElement("span");
|
||||
headingIcon.className = "assistant-dispatched-controls-heading-icon";
|
||||
headingIcon.innerHTML = '<i data-lucide="list-tree"></i>';
|
||||
const heading = document.createElement("b");
|
||||
heading.textContent = "下发控制指令";
|
||||
const count = document.createElement("strong");
|
||||
count.className = "assistant-dispatched-controls-count";
|
||||
const chevron = document.createElement("i");
|
||||
chevron.className = "assistant-dispatched-controls-chevron";
|
||||
chevron.setAttribute("data-lucide", "chevron-down");
|
||||
summary.append(headingIcon, heading, count, chevron);
|
||||
const list = document.createElement("ol");
|
||||
list.className = "assistant-dispatched-controls-list";
|
||||
card.append(summary, list);
|
||||
message.body.append(card);
|
||||
}
|
||||
|
||||
card.dataset.commandSignature = signature;
|
||||
const count = $(".assistant-dispatched-controls-count", card);
|
||||
const list = $(".assistant-dispatched-controls-list", card);
|
||||
if (count) count.textContent = String(commands.length);
|
||||
if (!list) return;
|
||||
list.replaceChildren();
|
||||
commands.forEach((item) => {
|
||||
const row = document.createElement("li");
|
||||
row.dataset.deliveryState = String(item.deliveryState || "not_submitted");
|
||||
const titleParts = [];
|
||||
if (item.semantic) titleParts.push(`语义控制指令:${item.semantic}`);
|
||||
if (item.command) titleParts.push(`实际控制指令:${item.command}`);
|
||||
if (item.deliveryLabel) {
|
||||
titleParts.push(`下发状态:${item.deliveryLabel}${item.deliveryDetail ? `(${item.deliveryDetail})` : ""}`);
|
||||
}
|
||||
row.title = titleParts.join("\n");
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "assistant-dispatched-control-icon";
|
||||
icon.innerHTML = `<i data-lucide="${assistantControlIcon(item)}"></i>`;
|
||||
const label = document.createElement("span");
|
||||
label.className = "assistant-dispatched-control-label";
|
||||
label.textContent = String(item.category || "控制指令");
|
||||
const value = document.createElement("strong");
|
||||
value.className = "assistant-dispatched-control-value";
|
||||
value.textContent = assistantControlValue(item);
|
||||
const accessibleCommand = document.createElement("small");
|
||||
accessibleCommand.className = "visually-hidden";
|
||||
accessibleCommand.textContent = titleParts.join(";");
|
||||
row.append(icon, label, value, accessibleCommand);
|
||||
list.append(row);
|
||||
});
|
||||
message.article?.classList.add("assistant-message-with-dispatched-controls");
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
followLatest(true);
|
||||
}
|
||||
|
||||
function appendModeCreatedState(message, profile, { updated = false } = {}) {
|
||||
if (!message?.body || !profile?.id) return;
|
||||
const selector = `[data-assistant-mode-created="${CSS.escape(String(profile.id))}"]`;
|
||||
@@ -921,6 +1197,7 @@
|
||||
"cognitive-agent:validated-can-commands",
|
||||
{ detail: event.payload },
|
||||
));
|
||||
appendDispatchedControls(message, event.payload);
|
||||
return;
|
||||
}
|
||||
if (event.type === "answer.delta") {
|
||||
@@ -952,6 +1229,7 @@
|
||||
);
|
||||
if (event.payload) {
|
||||
window.CognitiveAgentConsole?.applySyncedResult?.(event.payload);
|
||||
appendDispatchedControls(message, event.payload);
|
||||
if (
|
||||
event.payload.mode_profile_draft
|
||||
&& event.payload.orchestration_route === "mode_profile_create_preview"
|
||||
@@ -1165,6 +1443,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
function announceModeExecution(profile, result = null, { command = "", error = null } = {}) {
|
||||
const modeName = String(profile?.name || profile?.id || "功能模式");
|
||||
const commandText = String(command || `进入${modeName}`).trim();
|
||||
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 = "功能模式执行未完成";
|
||||
let statusState = "failed";
|
||||
if (error) {
|
||||
content = `${modeName}执行失败:${error.message || "请稍后重试。"}`;
|
||||
statusText = "功能模式执行失败";
|
||||
} else if (status === "succeeded") {
|
||||
content = dryRun
|
||||
? `已完成模拟编排:${commandText}。`
|
||||
: waitingForFeedback
|
||||
? `已提交执行:${commandText},正在等待车辆状态确认。`
|
||||
: `已完成执行:${commandText}。`;
|
||||
statusText = dryRun
|
||||
? "功能模式模拟编排已完成"
|
||||
: waitingForFeedback
|
||||
? "模式指令已提交,等待车辆状态确认"
|
||||
: "功能模式执行已完成";
|
||||
statusState = "success";
|
||||
} else if (status === "partially_succeeded") {
|
||||
content = `${modeName}仅部分完成,请查看未完成的控制指令。`;
|
||||
statusText = "功能模式部分完成";
|
||||
}
|
||||
|
||||
const message = createMessage("assistant", content);
|
||||
setActionStatus(message, statusText, statusState);
|
||||
if (result) appendDispatchedControls(message, result);
|
||||
persistMessages();
|
||||
}
|
||||
|
||||
function renderInitialMessages() {
|
||||
const container = $("#assistantMessages");
|
||||
if (!container) return;
|
||||
@@ -1221,6 +1537,7 @@
|
||||
open: () => setDrawerOpen(true),
|
||||
close: () => setDrawerOpen(false),
|
||||
startModeCreation,
|
||||
announceModeExecution,
|
||||
announceModeCreated(profile) {
|
||||
if (!profile?.id) return;
|
||||
setDrawerOpen(true);
|
||||
|
||||
@@ -9637,6 +9637,87 @@ body.device-power-drawer-open {
|
||||
border-left-color: color-mix(in oklch, var(--primary) 52%, var(--border));
|
||||
}
|
||||
|
||||
/* Unified homepage header: panel controls and function modes share one row. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .main-panel {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-output-shell {
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar {
|
||||
grid-template-columns: auto auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail .v2-module-switcher {
|
||||
width: auto;
|
||||
min-width: max-content;
|
||||
min-height: 0;
|
||||
position: static;
|
||||
inset: auto;
|
||||
z-index: auto;
|
||||
display: block;
|
||||
grid-column: auto;
|
||||
grid-row: auto;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail.is-collapsed > .mode-rail-bar {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar {
|
||||
grid-template-columns: auto auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail .mode-rail-actions {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar,
|
||||
.console-v2 .main-panel > .v2-mode-rail.is-collapsed > .mode-rail-bar {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail .v2-module-switcher {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail .mode-rail-identity {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar > .mode-rail-content {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail .mode-rail-actions {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.console-v2 .v2-mode-rail > .mode-rail-bar {
|
||||
grid-template-columns: auto minmax(150px, 1fr) auto;
|
||||
|
||||
@@ -373,6 +373,19 @@
|
||||
if (new Set(["driver", "passenger", "rear"]).has(target) && temperature !== null) {
|
||||
return command("空调温度", `ac temp ${target} ${temperature}`);
|
||||
}
|
||||
const vehicleZones = new Set([
|
||||
"vehicle",
|
||||
"front-row",
|
||||
"rear-row",
|
||||
"rear-left",
|
||||
"rear-right",
|
||||
]);
|
||||
if (vehicleZones.has(target) && temperature !== null) {
|
||||
return command(
|
||||
"空调温度",
|
||||
`Climate_Control_064_04(${target}=${temperature}°C)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (capability === "climate.set_control_mode") {
|
||||
const mode = String(parameters.mode ?? parameters.control_mode ?? "");
|
||||
@@ -466,6 +479,22 @@
|
||||
const level = boundedInteger(parameters.brightness_level, 1, 5);
|
||||
if (level !== null) return command("氛围灯", `light ambient brightness ${level}`);
|
||||
}
|
||||
if (capability === "ambient_light.set_illumination") {
|
||||
const color = String(parameters.color_hex || "").trim().toUpperCase();
|
||||
const brightness = boundedInteger(parameters.brightness_pct, 0, 100);
|
||||
if (/^#[0-9A-F]{6}$/.test(color) && brightness !== null) {
|
||||
return command(
|
||||
"氛围灯颜色与亮度",
|
||||
`Ambient_RGB_Control_32B_2B(color=${color}, brightness=${brightness}%)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (capability === "ambient_light.set_mode") {
|
||||
const mode = String(parameters.mode || "").trim();
|
||||
if (new Set(["camping", "rest", "cinema", "meditation", "romantic", "off", "on"]).has(mode)) {
|
||||
return command("氛围灯模式", `Ambient_Mode_Control_496_04(mode=${mode})`);
|
||||
}
|
||||
}
|
||||
if (capability === "roof.set_shade_motion") {
|
||||
const motion = String(parameters.motion || "");
|
||||
if (new Set(["open", "close", "half", "stop"]).has(motion)) {
|
||||
@@ -526,13 +555,39 @@
|
||||
|| telemetry.feedback_observed === true
|
||||
|| telemetry.physical_effect_confirmed === true
|
||||
);
|
||||
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 deliveryState = safetyBlocked
|
||||
? "blocked"
|
||||
: executionFailed
|
||||
? "failed"
|
||||
: submitted
|
||||
? "submitted"
|
||||
: "not_submitted";
|
||||
const deliveryLabel = safetyBlocked
|
||||
? "安全阻止"
|
||||
: executionFailed
|
||||
? "执行失败"
|
||||
: submitted
|
||||
? "下发"
|
||||
: "未下发";
|
||||
const deliveryDetail = /no fresh valid 0x41a shade position/i.test(failureMessage)
|
||||
? "未收到遮阳帘 CAN1 / 0x41A 实时位置,无法判断移动方向,系统已阻止盲动"
|
||||
: failureMessage;
|
||||
const transportLabel = capability === "infrared.emit"
|
||||
? "红外 TCP"
|
||||
: result.control_transport === "wireless_mqtt" ? "无线 CAN" : "有线 CAN";
|
||||
return {
|
||||
deliveryState: submitted ? "submitted" : "not_submitted",
|
||||
deliveryLabel: submitted ? "下发" : "未下发",
|
||||
deliveryState,
|
||||
deliveryLabel,
|
||||
deliveryTransport: transportLabel,
|
||||
...(deliveryDetail ? { deliveryDetail } : {}),
|
||||
};
|
||||
};
|
||||
const commands = [];
|
||||
@@ -816,7 +871,7 @@
|
||||
<span>
|
||||
<b class="v2-can-command-channel">${escapeHtml(item.channel)}</b>
|
||||
${item.parseSource ? `<b class="v2-can-command-source" data-parse-source="${escapeHtml(item.parseSource)}">${parsedCommandSourceLabel(item.parseSource)}</b>` : ""}
|
||||
<b class="v2-can-command-delivery" data-delivery-state="${escapeHtml(item.deliveryState)}" title="${escapeHtml(`${item.deliveryTransport} · ${item.deliveryLabel}`)}">${escapeHtml(item.deliveryLabel)}</b>
|
||||
<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>
|
||||
<div class="v2-can-command-layer v2-can-command-semantic">
|
||||
@@ -1083,20 +1138,16 @@
|
||||
return operation;
|
||||
}
|
||||
|
||||
function buildModuleSwitcher(mainPanel) {
|
||||
function buildModuleSwitcher(mainPanel, modeRail) {
|
||||
const switcher = make("nav", "v2-module-switcher", "");
|
||||
switcher.id = "v2ModuleSwitcher";
|
||||
switcher.dataset.unifiedSync = "true";
|
||||
switcher.setAttribute("aria-label", "智能座舱远程控制与同步状态");
|
||||
switcher.setAttribute("aria-label", "页面板块展开与收起");
|
||||
switcher.innerHTML = `
|
||||
<div class="v2-unified-brand">
|
||||
<span class="v2-unified-brand-mark"><i data-lucide="waypoints"></i></span>
|
||||
<div><h1>511工程-智能座舱</h1></div>
|
||||
</div>
|
||||
<div class="v2-remote-control-state"><i data-lucide="radio-tower"></i><span>网页控制链路</span><strong>已启用</strong></div>
|
||||
<div class="v2-unified-live"><i></i><span>LIVE</span><b id="v2SyncClock">--:--:--</b></div>
|
||||
<nav class="v2-panel-control-dock" id="v2PanelControlDock" aria-label="页面板块展开与收起"></nav>`;
|
||||
mainPanel.prepend(switcher);
|
||||
const modeRailBar = $(".mode-rail-bar", modeRail);
|
||||
if (modeRailBar) modeRailBar.prepend(switcher);
|
||||
else mainPanel.prepend(switcher);
|
||||
const panelDock = $("#v2PanelControlDock", switcher);
|
||||
const sidebarToggle = $("#v2SidebarToggle");
|
||||
if (panelDock && sidebarToggle) panelDock.append(sidebarToggle);
|
||||
@@ -1231,6 +1282,11 @@
|
||||
return text;
|
||||
}
|
||||
|
||||
function ambientPowerState() {
|
||||
const value = $('[data-domain-panel="ambient_light"]')?.dataset.ambientPower;
|
||||
return value === "on" || value === "off" ? value : "unknown";
|
||||
}
|
||||
|
||||
function selectedTransportHasConnection() {
|
||||
const selected = $('.vehicle-transport-options [data-control-transport][aria-pressed="true"]')
|
||||
|| $('.vehicle-transport-options [data-control-transport].active');
|
||||
@@ -1340,15 +1396,15 @@
|
||||
rows: () => {
|
||||
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
|
||||
const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
|
||||
const rawPower = normalizeDeviceReadout(readText("#ambientPower"));
|
||||
const power = rawPower === "-" ? "-" : /OFF|关闭/.test(rawPower) ? "关闭" : "开启";
|
||||
const powerState = ambientPowerState();
|
||||
const power = powerState === "on" ? "开启" : powerState === "off" ? "关闭" : "-";
|
||||
return [["开关状态", power], ["前排亮度", frontBrightness], ["前排颜色", frontColor], ["后排亮度", rearBrightness], ["后排颜色", rearColor]];
|
||||
},
|
||||
controls: () => {
|
||||
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
|
||||
const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
|
||||
return [
|
||||
selectControl("power", "开关状态", /OFF|关闭/.test(readText("#ambientPower")) ? "off" : "on", toggleOptions),
|
||||
selectControl("power", "开关状态", ambientPowerState() === "off" ? "off" : "on", toggleOptions),
|
||||
rangeControl("brightness", "全车亮度", frontBrightness === "-" ? rearBrightness : frontBrightness, 1, 5, 1, " 级"),
|
||||
selectControl("color", "全车颜色", String(clampControlNumber(frontColor === "-" ? rearColor : frontColor, 1, 127, 1)), colorIndexOptions),
|
||||
];
|
||||
@@ -2141,9 +2197,7 @@
|
||||
const sidebar = $(".v2-sidebar");
|
||||
const sidebarFooter = $(".v2-sidebar-footer", sidebar);
|
||||
const syncSidebar = make("section", "v2-sync-sidebar", `
|
||||
<div class="v2-sync-sidebar-overview">
|
||||
<div class="v2-sync-sidebar-heading"><h2>511工程-智能座舱</h2></div>
|
||||
</div>
|
||||
<div class="v2-sync-sidebar-overview"></div>
|
||||
<section class="v2-sync-log" aria-labelledby="v2SyncLogTitle">
|
||||
<header><div><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div><select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select></header>
|
||||
<ol id="v2SyncLogList" aria-live="polite"></ol>
|
||||
@@ -2679,7 +2733,7 @@
|
||||
requestHeader,
|
||||
gestureDrawer,
|
||||
);
|
||||
buildModuleSwitcher(mainPanel);
|
||||
buildModuleSwitcher(mainPanel, modeRail);
|
||||
buildOutput(mainPanel, modeRail);
|
||||
buildSyncControlLayer();
|
||||
bindEvents();
|
||||
|
||||
@@ -10,23 +10,26 @@
|
||||
<link rel="icon" href="/icons/app-icon-192.png" />
|
||||
<title>511工程-智能座舱</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=dual-seat-23" />
|
||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-68" />
|
||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-69" />
|
||||
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" />
|
||||
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-26" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-28" />
|
||||
<link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" />
|
||||
<link rel="stylesheet" href="/ui-consistency.css?v=spatial-ui-4" />
|
||||
<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-131" defer></script>
|
||||
<script src="/app.js?v=dual-seat-135" 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-66" defer></script>
|
||||
<script src="/self-extension.js?v=gscu-tcp-43" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-27" defer></script>
|
||||
<script src="/console-v2.js?v=local-first-70" defer></script>
|
||||
<script src="/self-extension.js?v=gscu-tcp-44" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-32" defer></script>
|
||||
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
||||
</head>
|
||||
<body class="console-v2">
|
||||
|
||||
@@ -666,7 +666,8 @@
|
||||
<span class="v2-extension-pet-mouth-closed" aria-hidden="true"></span>
|
||||
</button>
|
||||
</aside>`;
|
||||
moduleSwitcher.insertAdjacentElement("afterend", root);
|
||||
const homepageHeader = moduleSwitcher.closest(".v2-mode-rail") || moduleSwitcher;
|
||||
homepageHeader.insertAdjacentElement("afterend", root);
|
||||
|
||||
buildDialogs();
|
||||
return true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v282";
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v290";
|
||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||
const APP_SHELL = [
|
||||
@@ -6,26 +6,29 @@ const APP_SHELL = [
|
||||
"/index.html",
|
||||
"/styles.css?v=dual-seat-23",
|
||||
"/network.js?v=android-pad-2",
|
||||
"/app.js?v=dual-seat-131",
|
||||
"/app.js?v=dual-seat-135",
|
||||
"/gesture-control.js?v=android-pad-9",
|
||||
"/pwa.js?v=android-pad-3",
|
||||
"/console-v2.css?v=delivery-state-68",
|
||||
"/console-v2.js?v=local-first-66",
|
||||
"/console-v2.css?v=delivery-state-69",
|
||||
"/console-v2.js?v=local-first-70",
|
||||
"/self-extension.css?v=gscu-tcp-43",
|
||||
"/self-extension.js?v=gscu-tcp-43",
|
||||
"/self-extension.js?v=gscu-tcp-44",
|
||||
"/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-26",
|
||||
"/assistant-chat.js?v=codex-assistant-27",
|
||||
"/assistant-chat.css?v=codex-assistant-28",
|
||||
"/assistant-chat.js?v=codex-assistant-32",
|
||||
"/energy-monitor.css?v=energy-monitor-4",
|
||||
"/energy-monitor.js?v=energy-monitor-4",
|
||||
"/ui-consistency.css?v=spatial-ui-4",
|
||||
"/vue-ui.js?v=vue-presentation-3",
|
||||
"/assets/pets/self-extension-cat.png?v=3",
|
||||
"/assets/pets/self-extension-cat-entrance.png?v=3",
|
||||
"/manifest.webmanifest",
|
||||
"/icons/app-icon-192.png",
|
||||
"/icons/app-icon-512.png",
|
||||
"/vendor/lucide/0.468.0/lucide.min.js",
|
||||
"/vendor/vue/3.5.21/vue.runtime.global.prod.js",
|
||||
"/vendor/three/0.147.0/three.min.js",
|
||||
"/vendor/three/0.147.0/fflate.min.js",
|
||||
"/vendor/three/0.147.0/FBXLoader.js",
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
/* Shared spatial UI contract --------------------------------------------
|
||||
Keep geometry consistent across legacy modules without changing their
|
||||
domain-specific colors, content density, or interaction behavior.
|
||||
|
||||
Hierarchy: control 10px -> item 12px -> section 16px -> panel 18px ->
|
||||
modal 22px. All modules consume the same semantic color tokens. */
|
||||
:root {
|
||||
--ui-control-height-compact: 30px;
|
||||
--ui-control-height: 38px;
|
||||
--ui-control-size-icon: 36px;
|
||||
--ui-radius-control-inner: calc(var(--radius) - 2px);
|
||||
--ui-radius-control: var(--radius);
|
||||
--ui-radius-item: calc(var(--radius) + 2px);
|
||||
--ui-radius-section: calc(var(--radius) + 6px);
|
||||
--ui-radius-panel: calc(var(--radius) + 8px);
|
||||
--ui-radius-modal: calc(var(--radius) + 12px);
|
||||
--ui-space-card: 10px;
|
||||
--ui-border-subtle: 1px solid var(--border);
|
||||
--ui-shadow-item: 0 5px 14px color-mix(in oklch, var(--foreground) 6%, transparent);
|
||||
--ui-shadow-panel:
|
||||
0 14px 32px color-mix(in oklch, var(--foreground) 8%, transparent),
|
||||
inset 0 1px 0 color-mix(in oklch, var(--card) 78%, transparent);
|
||||
--ui-shadow-modal:
|
||||
0 28px 72px color-mix(in oklch, var(--foreground) 22%, transparent),
|
||||
0 4px 18px color-mix(in oklch, var(--primary) 8%, transparent),
|
||||
inset 0 1px 0 color-mix(in oklch, var(--card) 84%, transparent);
|
||||
--ui-focus-ring: 0 0 0 3px color-mix(in oklch, var(--ring) 22%, transparent);
|
||||
}
|
||||
|
||||
/* Top-level workspace surfaces share one frame. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync :is(
|
||||
.v2-operation-shell,
|
||||
.joint-twin-shell > .twin-band,
|
||||
#v2DevicePanel,
|
||||
#v2ExtensionPanel .v2-extension-sync-mount
|
||||
) {
|
||||
border: var(--ui-border-subtle);
|
||||
border-radius: var(--ui-radius-panel);
|
||||
box-shadow: var(--ui-shadow-panel);
|
||||
}
|
||||
|
||||
/* The operation shell owns the frame; its active view must not draw a
|
||||
second border directly inside the same spatial boundary. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-operation-shell > .v2-natural-panel {
|
||||
border: 0;
|
||||
border-radius: inherit;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Modal surfaces and their backdrops use one elevation level. */
|
||||
:is(
|
||||
.mode-gallery-backdrop .mode-gallery,
|
||||
.mode-editor-backdrop .mode-editor,
|
||||
.energy-monitor-layer .energy-monitor-page
|
||||
) {
|
||||
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
|
||||
border-radius: var(--ui-radius-modal);
|
||||
box-shadow: var(--ui-shadow-modal);
|
||||
}
|
||||
|
||||
.mode-gallery-backdrop,
|
||||
.mode-editor-backdrop,
|
||||
.energy-monitor-layer {
|
||||
background: color-mix(in oklch, var(--foreground) 42%, transparent);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
/* Section containers sit between panels and individual cards. */
|
||||
.v2-sync-log,
|
||||
.mode-gallery-group,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card {
|
||||
border-radius: var(--ui-radius-section);
|
||||
}
|
||||
|
||||
/* Repeated content cards share radius and elevation while retaining their
|
||||
own semantic state colors and specialized internal layouts. */
|
||||
.v2-synced-device,
|
||||
.v2-extension-gateway,
|
||||
.v2-nfc-inline,
|
||||
.mode-gallery-card,
|
||||
.mode-editor-flow,
|
||||
.mode-editor-command-card,
|
||||
.energy-kpi {
|
||||
border-radius: var(--ui-radius-item);
|
||||
box-shadow: var(--ui-shadow-item);
|
||||
}
|
||||
|
||||
.energy-kpi-primary {
|
||||
box-shadow: 0 8px 20px color-mix(in oklch, var(--primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.v2-extension-gateway,
|
||||
.v2-nfc-inline {
|
||||
padding: var(--ui-space-card);
|
||||
}
|
||||
|
||||
/* Sidebar cards and their nested segmented controls preserve a clear
|
||||
parent/child radius relationship. */
|
||||
.console-v2 :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-can-command-panel
|
||||
) {
|
||||
border-radius: var(--ui-radius-item);
|
||||
}
|
||||
|
||||
/* The compact-sidebar rules in console-v2.css intentionally use highly
|
||||
specific selectors. Keep all peer cards on the shared item radius even
|
||||
when that layout is active. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-can-command-panel
|
||||
) {
|
||||
border-radius: var(--ui-radius-item) !important;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log {
|
||||
border-radius: var(--ui-radius-section) !important;
|
||||
}
|
||||
|
||||
.console-v2 :is(.segmented, .twin-selector) {
|
||||
gap: 4px;
|
||||
padding: 3px;
|
||||
background: var(--muted);
|
||||
border: var(--ui-border-subtle);
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
.console-v2 .segmented > :is(.segment, .mode-option),
|
||||
.console-v2 .twin-selector > .twin-seat-button {
|
||||
border-radius: var(--ui-radius-control-inner);
|
||||
}
|
||||
|
||||
.console-v2 .segmented > .mode-option {
|
||||
min-height: var(--ui-control-height);
|
||||
height: var(--ui-control-height);
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.console-v2 .twin-selector > .twin-seat-button {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
/* Primary header controls use one medium size. */
|
||||
.console-v2 #v2PanelControlDock > .v2-panel-control-button,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-mode-rail .mode-rail-actions > :is(
|
||||
#modeGalleryToggle,
|
||||
#saveModeToggle,
|
||||
#modeRailToggle
|
||||
) {
|
||||
min-height: var(--ui-control-height);
|
||||
height: var(--ui-control-height);
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
#v2PanelControlDock > .v2-panel-control-button,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
#v2PanelControlDock > #v2SidebarToggle.v2-panel-control-button {
|
||||
border-radius: var(--ui-radius-control) !important;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-mode-rail #modeRailToggle {
|
||||
width: var(--ui-control-height);
|
||||
min-width: var(--ui-control-height);
|
||||
}
|
||||
|
||||
/* Repeated inline actions use one compact size. */
|
||||
.console-v2 :is(
|
||||
.v2-device-adjust-toggle,
|
||||
.v2-extension-nfc-launch,
|
||||
.v2-extension-gateway > button,
|
||||
.v2-nfc-inline button
|
||||
),
|
||||
.energy-trend-tabs button {
|
||||
min-height: var(--ui-control-height-compact);
|
||||
height: var(--ui-control-height-compact);
|
||||
border-radius: var(--ui-radius-control-inner);
|
||||
}
|
||||
|
||||
/* Header icon actions are distinct from circular record/send actions. */
|
||||
.assistant-drawer-actions .context-reset,
|
||||
.assistant-drawer-close,
|
||||
.mode-gallery-close,
|
||||
.mode-editor-header > button,
|
||||
#energyMonitorClose {
|
||||
width: var(--ui-control-size-icon);
|
||||
min-width: var(--ui-control-size-icon);
|
||||
height: var(--ui-control-size-icon);
|
||||
min-height: var(--ui-control-size-icon);
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
/* Text fields align with medium controls and use the same focus treatment. */
|
||||
.mode-editor-field input,
|
||||
.mode-editor-field textarea {
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
/* Reserve the right side for the microphone/send actions. Anchoring the hint
|
||||
from the content edge prevents the legacy right offsets from pushing it
|
||||
outside narrow assistant composers. */
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync
|
||||
.v2-request-form .composer-hint {
|
||||
left: 16px !important;
|
||||
right: auto !important;
|
||||
bottom: 8px;
|
||||
max-width: calc(100% - 132px);
|
||||
}
|
||||
|
||||
/* 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"]
|
||||
) {
|
||||
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"]
|
||||
) {
|
||||
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 {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
.console-v2 :is(
|
||||
#v2PanelControlDock > .v2-panel-control-button,
|
||||
.v2-mode-rail .mode-rail-actions > button,
|
||||
.segmented > button,
|
||||
.twin-selector > button,
|
||||
.v2-device-adjust-toggle,
|
||||
.v2-extension-nfc-launch,
|
||||
.v2-extension-gateway > button,
|
||||
.v2-nfc-inline button
|
||||
):focus-visible,
|
||||
.mode-gallery :is(button, input, textarea):focus-visible,
|
||||
.mode-editor :is(button, input, textarea):focus-visible,
|
||||
.energy-monitor-page button:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--ui-focus-ring);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
:root {
|
||||
--ui-control-height: 40px;
|
||||
--ui-control-height-compact: 32px;
|
||||
--ui-control-size-icon: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.v2-synced-device,
|
||||
.mode-gallery-card,
|
||||
.energy-kpi,
|
||||
.console-v2 button {
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-present, Yuxi (Evan) You
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,176 @@
|
||||
(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);
|
||||
|
||||
function refreshIcons() {
|
||||
nextTick(() => window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }));
|
||||
}
|
||||
|
||||
function replaceReactiveArray(target, values) {
|
||||
target.splice(0, target.length, ...values.map((value) => ({ ...value })));
|
||||
}
|
||||
|
||||
function renderModeEntry(entry) {
|
||||
const key = text(entry.key || `${entry.type}-${entry.id || entry.label}`);
|
||||
if (entry.type === "label") {
|
||||
return h("span", {
|
||||
key,
|
||||
class: "mode-group-label",
|
||||
"data-mode-group-label": text(entry.id),
|
||||
}, text(entry.label));
|
||||
}
|
||||
if (entry.type === "divider") {
|
||||
return h("span", {
|
||||
key,
|
||||
class: "mode-group-divider",
|
||||
"data-mode-divider": text(entry.id),
|
||||
"aria-hidden": "true",
|
||||
});
|
||||
}
|
||||
const children = [
|
||||
h("i", {
|
||||
class: "mode-chip-icon",
|
||||
"data-lucide": text(entry.icon, "bookmark"),
|
||||
}),
|
||||
h("span", { class: "mode-chip-label" }, text(entry.label, "模式")),
|
||||
h("small", { class: "mode-chip-summary" }, text(entry.summary, "整车联动")),
|
||||
];
|
||||
if (entry.cycleLabel) {
|
||||
children.push(h("small", { class: "mode-cycle-badge" }, text(entry.cycleLabel)));
|
||||
}
|
||||
children.push(h("i", {
|
||||
class: "mode-chip-run",
|
||||
"data-lucide": "play",
|
||||
"data-mode-run": "true",
|
||||
title: `执行${text(entry.label, "模式")}`,
|
||||
}));
|
||||
return h("button", {
|
||||
key,
|
||||
type: "button",
|
||||
title: text(entry.title),
|
||||
"data-mode-profile-id": text(entry.id),
|
||||
"data-mode-command": text(entry.command),
|
||||
"data-mode-origin": text(entry.origin, "preset"),
|
||||
...(entry.behavior ? { "data-mode-behavior": text(entry.behavior) } : {}),
|
||||
"aria-label": text(entry.ariaLabel, `${text(entry.label, "模式")},点击执行`),
|
||||
"aria-pressed": "false",
|
||||
}, children);
|
||||
}
|
||||
|
||||
function createModeMount(container) {
|
||||
const state = reactive({ entries: [] });
|
||||
const app = createApp({
|
||||
name: "N7ModeChipRail",
|
||||
setup() {
|
||||
return () => h(Fragment, null, state.entries.map(renderModeEntry));
|
||||
},
|
||||
});
|
||||
app.mount(container);
|
||||
const mount = { app, state };
|
||||
modeMounts.set(container, mount);
|
||||
return mount;
|
||||
}
|
||||
|
||||
function renderModeChips(container, entries) {
|
||||
if (!(container instanceof Element) || !Array.isArray(entries)) return false;
|
||||
const mount = modeMounts.get(container) || createModeMount(container);
|
||||
replaceReactiveArray(mount.state.entries, entries);
|
||||
container.dataset.vueSurface = "mode-chips";
|
||||
refreshIcons();
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderCommandRow(command, index) {
|
||||
const semantic = text(command.semantic);
|
||||
const actual = text(command.command);
|
||||
const accessibleText = [
|
||||
semantic ? `语义控制指令:${semantic}` : "",
|
||||
actual ? `实际控制指令:${actual}` : "",
|
||||
command.deliveryLabel
|
||||
? `下发状态:${text(command.deliveryLabel)}${command.deliveryDetail ? `(${text(command.deliveryDetail)})` : ""}`
|
||||
: "",
|
||||
].filter(Boolean).join(";");
|
||||
return h("li", {
|
||||
key: text(command.key, `command-${index}`),
|
||||
title: accessibleText,
|
||||
"data-delivery-state": text(command.deliveryState, "not_submitted"),
|
||||
}, [
|
||||
h("span", { class: "assistant-dispatched-control-icon" }, [
|
||||
h("i", { "data-lucide": text(command.icon, "binary") }),
|
||||
]),
|
||||
h("span", { class: "assistant-dispatched-control-label" }, text(command.category, "控制指令")),
|
||||
h("strong", { class: "assistant-dispatched-control-value" }, text(command.value, "控制")),
|
||||
h("small", { class: "visually-hidden" }, accessibleText),
|
||||
]);
|
||||
}
|
||||
|
||||
function createCommandMount(container) {
|
||||
const state = reactive({ commands: [], open: true });
|
||||
const app = createApp({
|
||||
name: "N7DispatchedControlsCard",
|
||||
setup() {
|
||||
return () => h("details", {
|
||||
class: "assistant-dispatched-controls",
|
||||
open: state.open,
|
||||
onToggle(event) {
|
||||
state.open = event.currentTarget.open;
|
||||
},
|
||||
}, [
|
||||
h("summary", null, [
|
||||
h("span", { class: "assistant-dispatched-controls-heading-icon" }, [
|
||||
h("i", { "data-lucide": "list-tree" }),
|
||||
]),
|
||||
h("b", null, "下发控制指令"),
|
||||
h("strong", { class: "assistant-dispatched-controls-count" }, String(state.commands.length)),
|
||||
h("i", {
|
||||
class: "assistant-dispatched-controls-chevron",
|
||||
"data-lucide": "chevron-down",
|
||||
}),
|
||||
]),
|
||||
h("ol", { class: "assistant-dispatched-controls-list" }, state.commands.map(renderCommandRow)),
|
||||
]);
|
||||
},
|
||||
});
|
||||
app.mount(container);
|
||||
const mount = { app, state };
|
||||
commandMounts.set(container, mount);
|
||||
return mount;
|
||||
}
|
||||
|
||||
function renderDispatchedControls(container, commands) {
|
||||
if (!(container instanceof Element) || !Array.isArray(commands) || !commands.length) {
|
||||
return false;
|
||||
}
|
||||
const mount = commandMounts.get(container) || createCommandMount(container);
|
||||
replaceReactiveArray(mount.state.commands, commands);
|
||||
container.dataset.vueSurface = "dispatched-controls";
|
||||
refreshIcons();
|
||||
return true;
|
||||
}
|
||||
|
||||
document.documentElement.classList.add("vue-presentation-enabled");
|
||||
document.documentElement.dataset.vueVersion = VUE_VERSION;
|
||||
window.CognitiveAgentVueUI = Object.freeze({
|
||||
available: true,
|
||||
version: VUE_VERSION,
|
||||
renderDispatchedControls,
|
||||
renderModeChips,
|
||||
});
|
||||
}());
|
||||
Reference in New Issue
Block a user