feat(web-console): refine intelligent cockpit sync interface

This commit is contained in:
vee
2026-08-19 08:36:38 +08:00
parent 9b24d5c8c9
commit 772ab685d6
6 changed files with 1420 additions and 115 deletions
File diff suppressed because it is too large Load Diff
+340 -62
View File
@@ -112,7 +112,11 @@
place(); place();
}; };
reasoningPickerControl = { close, open, place, sync, trigger }; reasoningPickerControl = { close, open, place, sync, trigger };
const resetButton = $("#newConversation", originalForm);
const runButton = $("#runButton", originalForm);
if (resetButton) composer.append(resetButton);
composer.append(trigger); composer.append(trigger);
if (runButton) composer.append(runButton);
document.body.append(reasoningField); document.body.append(reasoningField);
trigger.addEventListener("click", (event) => { trigger.addEventListener("click", (event) => {
@@ -493,8 +497,14 @@
requestId: null, requestId: null,
pollTimer: null, pollTimer: null,
deviceFrame: null, deviceFrame: null,
deviceFingerprint: "",
activeDevice: null, activeDevice: null,
pinnedDevice: null, pinnedDevice: null,
previewClearTimer: null,
hoverSuppressedUntil: 0,
controlDevice: null,
deviceDrafts: {},
pendingControls: {},
logs: [], logs: [],
logKeys: new Set(), logKeys: new Set(),
extensionFingerprint: "", extensionFingerprint: "",
@@ -502,55 +512,172 @@
const readText = (selector, fallback = "--") => $(selector)?.textContent?.trim() || fallback; const readText = (selector, fallback = "--") => $(selector)?.textContent?.trim() || fallback;
function seatDeviceRows(seat) { const clampControlNumber = (value, min, max, fallback = min) => {
const card = $(`.seat-card[data-seat="${seat}"]`); const numeric = Number.parseFloat(String(value ?? "").replace(/[^\d.-]/g, ""));
if (!card) return [["状态", "等待反馈"]]; return Number.isFinite(numeric) ? Math.max(min, Math.min(max, numeric)) : fallback;
const rows = [ };
["反馈通道", $("[data-seat-feedback]", card)?.textContent?.trim() || "等待反馈"],
...$$('[data-function]', card).map((item) => [ const splitReadout = (selector) => {
item.textContent.replace(item.querySelector("b")?.textContent || "", "").trim(), const [front = "-", rear = "-"] = readText(selector, "- / -").split("/");
$("b", item)?.textContent?.trim() || "--", return [front.trim() || "-", rear.trim() || "-"];
]), };
...$$('.motion-axis', card).map((item) => [
$("span", item)?.textContent?.trim() || "姿态", function seatFunctionValue(seat, feature) {
$("b", item)?.textContent?.trim() || "--", return $(`.seat-card[data-seat="${seat}"] [data-function="${feature}"] b`)?.textContent?.trim() || "--";
]), }
];
return rows.filter(([label]) => label); function seatAxisValue(seat, axis) {
return $(`.seat-card[data-seat="${seat}"] [data-axis="${axis}"] b`)?.textContent?.trim() || "--";
}
function seatLevelControlValue(seat, feature) {
const text = seatFunctionValue(seat, feature);
const level = text.match(/L(\d+)/i)?.[1];
return /OFF|关闭|--/.test(text) ? "off" : level || "1";
}
function massageReadout() {
const text = seatFunctionValue("driver", "massage");
if (text === "--") return ["--", "--"];
if (/OFF|关闭/.test(text)) return ["关闭", "关闭"];
const [mode = "开启", strength = "默认"] = text.split("·").map((item) => item.trim());
return [mode, strength];
}
const levelOptions = [
["off", "关闭"], ["1", "1 档"], ["2", "2 档"], ["3", "3 档"],
];
const toggleOptions = [["off", "关闭"], ["on", "开启"]];
const massageModeOptions = [
["off", "关闭"], ["relax", "舒缓"], ["stretch", "伸展"], ["waist", "腰部"], ["wave", "波浪"],
];
const massageStrengthOptions = [
["off", "关闭"], ["low", "低"], ["medium", "中"], ["high", "高"],
];
const colorIndexNames = ["皓月白", "冰川蓝", "松石绿", "星云紫", "琥珀橙", "玫瑰红"];
const colorIndexOptions = Array.from({ length: 16 }, (_item, index) => [
String(index), colorIndexNames[index] ? `${index} · ${colorIndexNames[index]}` : `色号 ${index}`,
]);
const rangeControl = (key, label, value, min = 0, max = 100, step = 1, unit = "%", hint = "") => ({
key, label, type: "range", value: clampControlNumber(value, min, max), min, max, step, unit, hint,
});
const selectControl = (key, label, value, options) => ({ key, label, type: "select", value, options });
function deviceControlValue(deviceId, control) {
const draft = displaySync.deviceDrafts[deviceId];
return draft && Object.hasOwn(draft, control.key) ? draft[control.key] : control.value;
}
function optionLabel(options, value) {
return options.find(([optionValue]) => String(optionValue) === String(value))?.[1] || String(value);
} }
const syncedDevices = [ const syncedDevices = [
{
id: "passenger", name: "副驾座椅", icon: "armchair",
rows: () => [["通风", seatFunctionValue("passenger", "ventilation")], ["加热", seatFunctionValue("passenger", "heating")], ["水平", seatAxisValue("passenger", "longitudinal")], ["坐垫", seatAxisValue("passenger", "cushion_tilt")], ["靠背", seatAxisValue("passenger", "backrest_recline")], ["腿托", seatAxisValue("passenger", "legrest_height")]],
controls: () => [
selectControl("ventilation", "通风", seatLevelControlValue("passenger", "ventilation"), levelOptions),
selectControl("heating", "加热", seatLevelControlValue("passenger", "heating"), levelOptions),
rangeControl("longitudinal", "水平", seatAxisValue("passenger", "longitudinal")),
rangeControl("cushion_tilt", "坐垫", seatAxisValue("passenger", "cushion_tilt")),
rangeControl("backrest_recline", "靠背", seatAxisValue("passenger", "backrest_recline")),
rangeControl("legrest_height", "腿托", seatAxisValue("passenger", "legrest_height")),
],
command: (values) => `调节副驾座椅:通风${optionLabel(levelOptions, values.ventilation)},加热${optionLabel(levelOptions, values.heating)},水平${values.longitudinal}%,坐垫${values.cushion_tilt}%,靠背${values.backrest_recline}%,腿托${values.legrest_height}%`,
},
{
id: "driver", name: "主驾座椅", icon: "armchair",
rows: () => {
const [massageMode, massageStrength] = massageReadout();
return [["通风", seatFunctionValue("driver", "ventilation")], ["加热", seatFunctionValue("driver", "heating")], ["按摩模式", massageMode], ["按摩强度", massageStrength]];
},
controls: () => {
const [massageMode, massageStrength] = massageReadout();
return [
selectControl("ventilation", "通风", seatLevelControlValue("driver", "ventilation"), levelOptions),
selectControl("heating", "加热", seatLevelControlValue("driver", "heating"), levelOptions),
selectControl("massage_mode", "按摩模式", /关闭|--/.test(massageMode) ? "off" : massageMode.toLowerCase(), massageModeOptions),
selectControl("massage_strength", "按摩强度", /关闭|--/.test(massageStrength) ? "off" : massageStrength.toLowerCase(), massageStrengthOptions),
];
},
command: (values) => `调节主驾座椅:通风${optionLabel(levelOptions, values.ventilation)},加热${optionLabel(levelOptions, values.heating)},按摩模式${optionLabel(massageModeOptions, values.massage_mode)},按摩强度${optionLabel(massageStrengthOptions, values.massage_strength)}`,
},
{ {
id: "windows", name: "车窗", icon: "gallery-vertical-end", id: "windows", name: "车窗", icon: "gallery-vertical-end",
rows: () => [["设备状态", readText("#windowState", "等待反馈")], ["左前窗", readText("#frontLeftWindow")], ["右前窗", readText("#frontRightWindow")], ["左后窗", readText("#rearLeftWindow")], ["右后窗", readText("#rearRightWindow")]], rows: () => [["左前窗", readText("#frontLeftWindow")], ["右前窗", readText("#frontRightWindow")], ["左后窗", readText("#rearLeftWindow")], ["右后窗", readText("#rearRightWindow")]],
controls: () => [
rangeControl("front_left", "左前窗", readText("#frontLeftWindow"), 0, 100, 1, "%", "0% 关闭 · 100% 打开"),
rangeControl("front_right", "右前窗", readText("#frontRightWindow")),
rangeControl("rear_left", "左后窗", readText("#rearLeftWindow")),
rangeControl("rear_right", "右后窗", readText("#rearRightWindow")),
],
command: (values) => `调节车窗开合比例:左前窗${values.front_left}%,右前窗${values.front_right}%,左后窗${values.rear_left}%,右后窗${values.rear_right}%`,
}, },
{ {
id: "shade", name: "遮阳帘", icon: "panels-top-left", id: "shade", name: "遮阳帘", icon: "panels-top-left",
rows: () => [["设备状态", readText("#roofState", "等待反馈")], ["当前位置", readText("#shadePositionValue")], ["动作", readText("#shadeMotionState")], ["初始化", readText("#shadeInitializedState")], ["热保护", readText("#shadeThermalState")]], rows: () => [["当前位置", readText("#shadePositionValue")]],
controls: () => [rangeControl("position", "当前位置", readText("#shadePositionValue"), 0, 100, 1, "%", "0% 关闭 · 100% 打开")],
command: (values) => `将遮阳帘开合位置调整为${values.position}%`,
}, },
{ id: "driver", name: "主驾座椅", icon: "armchair", rows: () => seatDeviceRows("driver") },
{ id: "passenger", name: "副驾座椅", icon: "armchair", rows: () => seatDeviceRows("passenger") },
{ {
id: "reading", name: "阅读灯", icon: "book-open", id: "reading", name: "阅读灯", icon: "book-open",
rows: () => [["开关状态", readText("#interiorLightPower", "等待反馈")], ["控制通道", "CH1 · INTERIOR"], ["控制能力", "开 / 关"]], rows: () => [["开关状态", readText("#interiorLightPower", "等待反馈")]],
controls: () => [selectControl("power", "开关状态", /开启|ON/.test(readText("#interiorLightPower")) ? "on" : "off", toggleOptions)],
command: (values) => `${values.power === "on" ? "打开" : "关闭"}阅读灯`,
}, },
{ {
id: "ambient", name: "氛围灯", icon: "sparkles", id: "ambient", name: "氛围灯", icon: "sparkles",
rows: () => [["电源", readText("#ambientPower", "等待反馈")], ["模式", readText("#ambientMode")], ["前/后排亮度", readText("#ambientBrightness")], ["前/后排色号", readText("#ambientColorIndex")]], rows: () => {
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
return [["开关状态", /OFF|关闭/.test(readText("#ambientPower")) ? "关闭" : "开启"], ["前排亮度", 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),
rangeControl("front_brightness", "前排亮度", frontBrightness, 0, 5, 1, " 级"),
selectControl("front_color", "前排颜色", String(clampControlNumber(frontColor, 0, 15, 0)), colorIndexOptions),
rangeControl("rear_brightness", "后排亮度", rearBrightness, 0, 5, 1, " 级"),
selectControl("rear_color", "后排颜色", String(clampControlNumber(rearColor, 0, 15, 0)), colorIndexOptions),
];
},
command: (values) => `调节氛围灯:${values.power === "on" ? "开启" : "关闭"},前排亮度${values.front_brightness}级、颜色${optionLabel(colorIndexOptions, values.front_color)},后排亮度${values.rear_brightness}级、颜色${optionLabel(colorIndexOptions, values.rear_color)}`,
}, },
{ {
id: "headlight", name: "前大灯", icon: "car-front", id: "headlight", name: "前大灯", icon: "car-front",
rows: () => [["设备状态", readText("#frontLightState", "等待反馈")], ["近光灯", readText("#lowBeamState")], ["远光灯", readText("#highBeamState")], ["雾灯", readText("#fogLightState")], ["转向灯", readText("#turnSignalState")]], rows: () => [["近光灯", readText("#lowBeamState")], ["远光灯", readText("#highBeamState")], ["雾灯", readText("#fogLightState")], ["转向灯", readText("#turnSignalState")]],
controls: () => [["low_beam", "近光灯", "#lowBeamState"], ["high_beam", "远光灯", "#highBeamState"], ["fog_light", "雾灯", "#fogLightState"], ["turn_signal", "转向灯", "#turnSignalState"]].map(([key, label, selector]) => selectControl(key, label, /开启|ON/.test(readText(selector)) ? "on" : "off", toggleOptions)),
command: (values) => `调节前大灯:近光灯${optionLabel(toggleOptions, values.low_beam)},远光灯${optionLabel(toggleOptions, values.high_beam)},雾灯${optionLabel(toggleOptions, values.fog_light)},转向灯${optionLabel(toggleOptions, values.turn_signal)}`,
}, },
{ {
id: "climate", name: "空调", icon: "fan", id: "climate", name: "空调", icon: "fan",
rows: () => [["电源", readText("#climatePower", "等待反馈")], ["主驾温度", readText("#driverTemperature")], ["副驾温度", readText("#passengerTemperature")], ["风量", readText("#fanLevel")], ["主驾风向", readText("#driverAirDirection")], ["副驾风向", readText("#passengerAirDirection")], ["循环", readText("#cycleMode")], ["A/C", readText("#acState")], ["前除霜", readText("#defrostState")]], rows: () => [["开关状态", readText("#climatePower", "等待反馈")], ["主驾温度", readText("#driverTemperature")], ["副驾温度", readText("#passengerTemperature")], ["风量", readText("#fanLevel")], ["循环模式", readText("#cycleMode")]],
controls: () => [
selectControl("power", "开关状态", /ON|ACTIVE|开启/.test(readText("#climatePower")) ? "on" : "off", toggleOptions),
rangeControl("driver_temperature", "主驾温度", readText("#driverTemperature"), 16, 32, 0.5, "℃"),
rangeControl("passenger_temperature", "副驾温度", readText("#passengerTemperature"), 16, 32, 0.5, "℃"),
rangeControl("fan_level", "风量", readText("#fanLevel"), 0, 7, 1, " 档"),
selectControl("cycle_mode", "循环模式", readText("#cycleMode").includes("内") ? "internal" : "external", [["internal", "内循环"], ["external", "外循环"]]),
],
command: (values) => `调节空调:${values.power === "on" ? "开启" : "关闭"},主驾${values.driver_temperature}℃,副驾${values.passenger_temperature}℃,风量${values.fan_level}档,${values.cycle_mode === "internal" ? "内循环" : "外循环"}`,
}, },
]; ];
function deviceSummary(device) { function renderDeviceControl(device, control) {
const values = device.rows().map(([, value]) => value).filter(Boolean); const value = deviceControlValue(device.id, control);
return values.slice(0, 3).join(" · ") || "等待反馈"; if (control.type === "select") {
const options = control.options.map(([optionValue, label]) => `<option value="${escapeHtml(optionValue)}"${String(optionValue) === String(value) ? " selected" : ""}>${escapeHtml(label)}</option>`).join("");
return `<label class="v2-device-control-row v2-device-control-select"><span>${escapeHtml(control.label)}</span><select data-device-control="${device.id}" data-control-key="${control.key}" aria-label="${escapeHtml(device.name + control.label)}">${options}</select></label>`;
}
return `<label class="v2-device-control-row v2-device-control-range">
<span>${escapeHtml(control.label)}</span><output data-control-output="${control.key}">${escapeHtml(value)}${escapeHtml(control.unit)}</output>
<input type="range" min="${control.min}" max="${control.max}" step="${control.step}" value="${escapeHtml(value)}" data-device-control="${device.id}" data-control-key="${control.key}" data-control-unit="${escapeHtml(control.unit)}" aria-label="${escapeHtml(device.name + control.label)}" />
${control.hint ? `<small>${escapeHtml(control.hint)}</small>` : ""}
</label>`;
} }
function renderSyncedDevices() { function renderSyncedDevices() {
@@ -558,25 +685,83 @@
if (!grid) return; if (!grid) return;
grid.innerHTML = syncedDevices.map((device) => { grid.innerHTML = syncedDevices.map((device) => {
const rows = device.rows(); const rows = device.rows();
const detail = deviceSummary(device); const waiting = rows.some(([, value]) => /--|等待|同步中|未知/.test(value));
const waiting = /--|等待|同步中|未知/.test(detail); const controlOpen = displaySync.controlDevice === device.id;
return `<button class="v2-synced-device" type="button" data-device="${device.id}" data-ready="${String(!waiting)}" aria-label="查看${device.name}完整状态"> const hasDraft = Boolean(displaySync.deviceDrafts[device.id]);
<span><i data-lucide="${device.icon}"></i></span> const pending = Boolean(displaySync.pendingControls[device.id]);
<div><strong>${device.name}</strong><small>${escapeHtml(detail)}</small></div> const stateLabel = pending ? "已发送 · 等待同步" : hasDraft ? "参数待发送" : `${rows.length} 项关键状态`;
<i class="v2-device-signal"></i> return `<article class="v2-synced-device" data-device="${device.id}" data-ready="${String(!waiting)}" data-control-open="${String(controlOpen)}" data-has-draft="${String(hasDraft)}">
<dl>${rows.slice(0, 6).map(([label, value]) => `<div><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`).join("")}</dl> <header class="v2-device-card-header">
</button>`; <button class="v2-device-card-focus" type="button" data-device-focus="${device.id}" aria-label="在孪生界面查看${device.name}">
<span><i data-lucide="${device.icon}"></i></span><span><strong>${device.name}</strong><small data-device-card-state>${stateLabel}</small></span><i class="v2-device-signal"></i>
</button>
<button class="v2-device-adjust-toggle" type="button" data-device-control-toggle="${device.id}" aria-expanded="${String(controlOpen)}"><i data-lucide="sliders-horizontal"></i><span>${controlOpen ? "收起" : "调节"}</span></button>
</header>
<dl>${rows.map(([label, value]) => `<div><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`).join("")}</dl>
<div class="v2-device-controls" data-device-controls="${device.id}"${controlOpen ? "" : " hidden"}>
<div class="v2-device-controls-intro"><span>手动调节</span><small>参数确认后通过编排链路发送至车机</small></div>
${device.controls().map((control) => renderDeviceControl(device, control)).join("")}
<button class="v2-device-control-apply" type="button" data-device-control-apply="${device.id}"${pending ? " disabled" : ""}><i data-lucide="send"></i><span>${pending ? "等待车机同步" : "发送控制"}</span></button>
</div>
</article>`;
}).join(""); }).join("");
if (displaySync.activeDevice) { if (displaySync.activeDevice) {
$(`.v2-synced-device[data-device="${displaySync.activeDevice}"]`)?.classList.add("active"); $(`.v2-synced-device[data-device="${displaySync.activeDevice}"]`)?.classList.add("active");
} }
displaySync.deviceFingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, device.rows()]));
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
} }
function storeDeviceControlDraft(input) {
const deviceId = input.dataset.deviceControl;
const key = input.dataset.controlKey;
if (!deviceId || !key) return;
displaySync.deviceDrafts[deviceId] ||= {};
displaySync.deviceDrafts[deviceId][key] = input.value;
const controls = input.closest("[data-device-controls]");
const output = controls?.querySelector(`[data-control-output="${key}"]`);
if (output) output.textContent = `${input.value}${input.dataset.controlUnit || ""}`;
const card = input.closest("[data-device]");
if (card) {
card.dataset.hasDraft = "true";
const state = $("[data-device-card-state]", card);
if (state) state.textContent = "参数待发送";
}
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-control-draft", {
detail: { device_id: deviceId, control: key, value: input.value, source: "web_console" },
}));
}
function sendDeviceControl(deviceId) {
const device = syncedDevices.find((item) => item.id === deviceId);
if (!device) return;
const values = Object.fromEntries(device.controls().map((control) => [control.key, String(deviceControlValue(device.id, control))]));
const utterance = device.command(values);
const detail = {
device_id: device.id,
device_name: device.name,
controls: values,
utterance,
source: "web_console",
requested_at: new Date().toISOString(),
};
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-control-request", { detail }));
const accepted = window.CognitiveAgentConsole?.executeUtterance?.(utterance, { autoSubmit: true }) || false;
if (!accepted) {
window.CognitiveAgentConsole?.notify?.("上一条指令仍在执行,请稍候");
return;
}
displaySync.pendingControls[device.id] = utterance;
appendSyncLog({ category: "device", title: `网页端调节${device.name}`, detail: utterance, status: "info" });
renderSyncedDevices();
}
function scheduleSyncedDeviceRender() { function scheduleSyncedDeviceRender() {
if (displaySync.deviceFrame) return; if (displaySync.deviceFrame) return;
displaySync.deviceFrame = requestAnimationFrame(() => { displaySync.deviceFrame = requestAnimationFrame(() => {
displaySync.deviceFrame = null; displaySync.deviceFrame = null;
const fingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, device.rows()]));
if (fingerprint === displaySync.deviceFingerprint && $("#v2DeviceSyncGrid")?.childElementCount) return;
renderSyncedDevices(); renderSyncedDevices();
if (displaySync.activeDevice) presentTwinDevice(displaySync.activeDevice, { if (displaySync.activeDevice) presentTwinDevice(displaySync.activeDevice, {
pinned: Boolean(displaySync.pinnedDevice), pinned: Boolean(displaySync.pinnedDevice),
@@ -655,6 +840,10 @@
const device = syncedDevices.find((item) => item.id === deviceId); const device = syncedDevices.find((item) => item.id === deviceId);
const popover = $("#v2TwinDevicePopover"); const popover = $("#v2TwinDevicePopover");
if (!device || !popover) return; if (!device || !popover) return;
if (displaySync.previewClearTimer) {
window.clearTimeout(displaySync.previewClearTimer);
displaySync.previewClearTimer = null;
}
displaySync.activeDevice = deviceId; displaySync.activeDevice = deviceId;
if (pinned) displaySync.pinnedDevice = deviceId; if (pinned) displaySync.pinnedDevice = deviceId;
const rows = device.rows(); const rows = device.rows();
@@ -662,7 +851,7 @@
popover.dataset.device = deviceId; popover.dataset.device = deviceId;
popover.dataset.pinned = String(Boolean(displaySync.pinnedDevice)); popover.dataset.pinned = String(Boolean(displaySync.pinnedDevice));
popover.innerHTML = ` popover.innerHTML = `
<header><span><i data-lucide="${device.icon}"></i></span><div><small>LIVE DEVICE FEEDBACK</small><strong>${escapeHtml(device.name)}</strong></div><b>${pinned ? "已固定" : "靠近查看"}</b></header> <header><span><i data-lucide="${device.icon}"></i></span><div><small>LIVE DEVICE FEEDBACK</small><strong>${escapeHtml(device.name)}</strong></div><b>${pinned ? "已固定" : "靠近查看"}</b><button class="v2-twin-popover-close" type="button" data-twin-popover-close aria-label="关闭${escapeHtml(device.name)}状态详情" title="关闭"><i data-lucide="x"></i></button></header>
<dl>${rows.map(([label, value]) => `<div><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`).join("")}</dl>`; <dl>${rows.map(([label, value]) => `<div><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`).join("")}</dl>`;
$$("[data-twin-device]").forEach((button) => button.classList.toggle("active", button.dataset.twinDevice === deviceId)); $$("[data-twin-device]").forEach((button) => button.classList.toggle("active", button.dataset.twinDevice === deviceId));
$$(".v2-synced-device").forEach((button) => button.classList.toggle("active", button.dataset.device === deviceId)); $$(".v2-synced-device").forEach((button) => button.classList.toggle("active", button.dataset.device === deviceId));
@@ -673,6 +862,10 @@
} }
function clearTwinDevicePreview({ force = false } = {}) { function clearTwinDevicePreview({ force = false } = {}) {
if (displaySync.previewClearTimer) {
window.clearTimeout(displaySync.previewClearTimer);
displaySync.previewClearTimer = null;
}
if (displaySync.pinnedDevice && !force) return; if (displaySync.pinnedDevice && !force) return;
displaySync.activeDevice = null; displaySync.activeDevice = null;
if (force) displaySync.pinnedDevice = null; if (force) displaySync.pinnedDevice = null;
@@ -681,30 +874,42 @@
$$("[data-twin-device], .v2-synced-device").forEach((button) => button.classList.remove("active")); $$("[data-twin-device], .v2-synced-device").forEach((button) => button.classList.remove("active"));
} }
function scheduleTwinDevicePreviewClear() {
if (displaySync.pinnedDevice) return;
if (displaySync.previewClearTimer) window.clearTimeout(displaySync.previewClearTimer);
displaySync.previewClearTimer = window.setTimeout(() => {
displaySync.previewClearTimer = null;
clearTwinDevicePreview();
}, 45);
}
function buildTwinDeviceInteractions() { function buildTwinDeviceInteractions() {
const stage = $("#seatTwin"); const stage = $("#seatTwin");
if (!stage || $("#v2TwinHotspots")) return; if (!stage || $("#v2TwinHotspots")) return;
const hotspots = make("div", "v2-twin-hotspots", syncedDevices.map((device) => ` const hotspots = make("div", "v2-twin-hotspots", syncedDevices.map((device) => `
<button type="button" data-twin-device="${device.id}" aria-label="查看${device.name}状态"><i data-lucide="${device.icon}"></i><span>${device.name}</span></button>`).join("")); <button type="button" data-twin-device="${device.id}" aria-label="查看${device.name}状态"><span class="v2-twin-hotspot-label"><i data-lucide="${device.icon}"></i><b>${device.name}</b><small>实时状态</small></span></button>`).join(""));
hotspots.id = "v2TwinHotspots"; hotspots.id = "v2TwinHotspots";
const popover = make("aside", "v2-twin-device-popover", ""); const popover = make("aside", "v2-twin-device-popover", "");
popover.id = "v2TwinDevicePopover"; popover.id = "v2TwinDevicePopover";
popover.hidden = true; popover.hidden = true;
popover.setAttribute("role", "dialog");
popover.setAttribute("aria-label", "车端设备状态详情");
popover.setAttribute("aria-live", "polite"); popover.setAttribute("aria-live", "polite");
stage.append(hotspots, popover); stage.append(hotspots, popover);
hotspots.addEventListener("pointerdown", (event) => event.stopPropagation()); hotspots.addEventListener("pointerdown", (event) => event.stopPropagation());
hotspots.addEventListener("pointerover", (event) => { $$("[data-twin-device]", hotspots).forEach((button) => {
const button = event.target.closest("[data-twin-device]"); button.addEventListener("pointerenter", () => {
if (button && !displaySync.pinnedDevice) presentTwinDevice(button.dataset.twinDevice); if (!displaySync.pinnedDevice && performance.now() >= displaySync.hoverSuppressedUntil) {
}); presentTwinDevice(button.dataset.twinDevice);
hotspots.addEventListener("focusin", (event) => { }
const button = event.target.closest("[data-twin-device]"); });
if (button) presentTwinDevice(button.dataset.twinDevice); button.addEventListener("pointerleave", (event) => {
}); if (event.relatedTarget?.closest?.("#v2TwinDevicePopover")) return;
hotspots.addEventListener("pointerout", (event) => { scheduleTwinDevicePreviewClear();
if (event.relatedTarget?.closest?.("#v2TwinDevicePopover")) return; });
window.setTimeout(() => clearTwinDevicePreview(), 100); button.addEventListener("focus", () => presentTwinDevice(button.dataset.twinDevice));
button.addEventListener("blur", scheduleTwinDevicePreviewClear);
}); });
hotspots.addEventListener("click", (event) => { hotspots.addEventListener("click", (event) => {
const button = event.target.closest("[data-twin-device]"); const button = event.target.closest("[data-twin-device]");
@@ -715,6 +920,23 @@
else presentTwinDevice(button.dataset.twinDevice, { pinned: true }); else presentTwinDevice(button.dataset.twinDevice, { pinned: true });
}); });
popover.addEventListener("pointerdown", (event) => event.stopPropagation()); popover.addEventListener("pointerdown", (event) => event.stopPropagation());
popover.addEventListener("pointerenter", () => {
if (displaySync.previewClearTimer) {
window.clearTimeout(displaySync.previewClearTimer);
displaySync.previewClearTimer = null;
}
});
popover.addEventListener("pointerleave", scheduleTwinDevicePreviewClear);
popover.addEventListener("click", (event) => {
if (!event.target.closest("[data-twin-popover-close]")) return;
event.stopPropagation();
displaySync.hoverSuppressedUntil = performance.now() + 160;
clearTwinDevicePreview({ force: true });
});
stage.addEventListener("click", (event) => {
if (event.target.closest("[data-twin-device], #v2TwinDevicePopover")) return;
clearTwinDevicePreview({ force: true });
});
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
} }
@@ -892,6 +1114,14 @@
}); });
} }
function syncSidebarLogBaseline() {
const sidebar = $(".v2-sidebar");
const lowerPanel = $("#v2DevicePanel") || $(".joint-twin-shell > .twin-band");
if (!sidebar || !lowerPanel) return;
const offset = Math.max(0, lowerPanel.getBoundingClientRect().top - sidebar.getBoundingClientRect().top);
sidebar.style.setProperty("--v2-sync-log-top", `${offset}px`);
}
function buildSyncControlLayer() { function buildSyncControlLayer() {
document.body.classList.add("v2-passive-display", "v2-interactive-sync"); document.body.classList.add("v2-passive-display", "v2-interactive-sync");
document.body.dataset.module = "self-orchestration"; document.body.dataset.module = "self-orchestration";
@@ -900,18 +1130,25 @@
const sidebar = $(".v2-sidebar"); const sidebar = $(".v2-sidebar");
const sidebarFooter = $(".v2-sidebar-footer", sidebar); const sidebarFooter = $(".v2-sidebar-footer", sidebar);
const syncSidebar = make("section", "v2-sync-sidebar", ` const syncSidebar = make("section", "v2-sync-sidebar", `
<div class="v2-sync-sidebar-heading"><small>REMOTE CLIENT</small><h2>511工程-智能座舱</h2><p>网页可远程编排,车机任务、设备反馈与 USB 外设能力保持同步。</p></div> <div class="v2-sync-sidebar-overview">
<div class="v2-sync-link-stack"> <div class="v2-sync-sidebar-heading"><small>REMOTE CLIENT</small><h2>511工程-智能座舱</h2><p>网页可远程编排,车机任务、设备反馈与 USB 外设能力保持同步。</p></div>
<div class="v2-sync-link" id="v2HeadUnitState" data-state="checking"><i data-lucide="monitor-smartphone"></i><div><strong>车机链路</strong><span>连接检查中</span></div><b></b></div> <div class="v2-sync-link-stack">
<div class="v2-sync-link" id="v2MirrorState" data-state="checking"><i data-lucide="refresh-cw"></i><div><strong>网页同步</strong><span>建立同步中</span></div><b></b></div> <div class="v2-sync-link" id="v2HeadUnitState" data-state="checking"><i data-lucide="monitor-smartphone"></i><div><strong>车机链路</strong><span>连接检查中</span></div><b></b></div>
<div class="v2-sync-link" id="v2UsbState" data-state="checking"><i data-lucide="usb"></i><div><strong>USB 扩展</strong><span>检查外设中</span></div><b></b></div> <div class="v2-sync-link" id="v2MirrorState" data-state="checking"><i data-lucide="refresh-cw"></i><div><strong>网页同步</strong><span>建立同步中</span></div><b></b></div>
<div class="v2-sync-link" id="v2UsbState" data-state="checking"><i data-lucide="usb"></i><div><strong>USB 扩展</strong><span>检查外设中</span></div><b></b></div>
</div>
<div class="v2-sync-protocol"><i data-lucide="radio-tower"></i><div><small>扩展链路</small><strong id="v2SyncTransport">GSCU · 等待连接</strong></div></div>
</div> </div>
<section class="v2-sync-log" aria-labelledby="v2SyncLogTitle"> <section class="v2-sync-log" aria-labelledby="v2SyncLogTitle">
<header><div><small>RUNTIME / SYNC</small><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div><select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select></header> <header><div><small>RUNTIME / SYNC</small><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div><select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select></header>
<ol id="v2SyncLogList" aria-live="polite"></ol> <ol id="v2SyncLogList" aria-live="polite"></ol>
</section> </section>`);
<div class="v2-sync-protocol"><i data-lucide="radio-tower"></i><div><small>扩展链路</small><strong id="v2SyncTransport">GSCU · 等待连接</strong></div></div>`); if (sidebar && sidebarFooter) {
if (sidebar && sidebarFooter) sidebar.insertBefore(syncSidebar, sidebarFooter); sidebar.insertBefore(syncSidebar, sidebarFooter);
$(".v2-sync-sidebar-overview", syncSidebar)?.append(sidebarFooter);
const sidebarToggle = $("#v2SidebarToggle", sidebar);
if (sidebarToggle) $(".v2-sync-sidebar-heading", syncSidebar)?.append(sidebarToggle);
}
const naturalPanel = $("#v2NaturalPanel"); const naturalPanel = $("#v2NaturalPanel");
const taskOverview = make("section", "v2-task-overview", ` const taskOverview = make("section", "v2-task-overview", `
@@ -952,6 +1189,8 @@
} }
setSidePanelVisibility("devices", panelVisible("devices")); setSidePanelVisibility("devices", panelVisible("devices"));
setSidePanelVisibility("extensions", panelVisible("extensions")); setSidePanelVisibility("extensions", panelVisible("extensions"));
window.addEventListener("resize", syncSidebarLogBaseline);
requestAnimationFrame(syncSidebarLogBaseline);
document.addEventListener("click", (event) => { document.addEventListener("click", (event) => {
const toggle = event.target.closest("[data-panel-toggle], [data-panel-close]"); const toggle = event.target.closest("[data-panel-toggle], [data-panel-close]");
if (toggle) { if (toggle) {
@@ -959,10 +1198,34 @@
setSidePanelVisibility(key, !panelVisible(key)); setSidePanelVisibility(key, !panelVisible(key));
} }
}); });
$("#v2DeviceSyncGrid")?.addEventListener("click", (event) => { const deviceGrid = $("#v2DeviceSyncGrid");
const card = event.target.closest("[data-device]"); deviceGrid?.addEventListener("click", (event) => {
if (!card) return; const controlToggle = event.target.closest("[data-device-control-toggle]");
presentTwinDevice(card.dataset.device, { pinned: true }); if (controlToggle) {
const deviceId = controlToggle.dataset.deviceControlToggle;
displaySync.controlDevice = displaySync.controlDevice === deviceId ? null : deviceId;
if (displaySync.controlDevice) presentTwinDevice(deviceId, { pinned: true });
else if (displaySync.pinnedDevice === deviceId) clearTwinDevicePreview({ force: true });
renderSyncedDevices();
return;
}
const apply = event.target.closest("[data-device-control-apply]");
if (apply) {
sendDeviceControl(apply.dataset.deviceControlApply);
return;
}
const focus = event.target.closest("[data-device-focus]");
if (focus) {
const samePinned = displaySync.pinnedDevice === focus.dataset.deviceFocus;
if (samePinned) clearTwinDevicePreview({ force: true });
else presentTwinDevice(focus.dataset.deviceFocus, { pinned: true });
}
});
deviceGrid?.addEventListener("input", (event) => {
if (event.target.matches("[data-device-control]")) storeDeviceControlDraft(event.target);
});
deviceGrid?.addEventListener("change", (event) => {
if (event.target.matches("select[data-device-control]")) storeDeviceControlDraft(event.target);
}); });
$("#v2LogFilter")?.addEventListener("change", renderSyncLogs); $("#v2LogFilter")?.addEventListener("change", renderSyncLogs);
window.addEventListener("cognitive-agent:result", (event) => { window.addEventListener("cognitive-agent:result", (event) => {
@@ -976,8 +1239,22 @@
}); });
}); });
window.addEventListener("cognitive-agent:request-complete", (event) => { window.addEventListener("cognitive-agent:request-complete", (event) => {
if (!event.detail?.error) return; const pendingDevice = Object.entries(displaySync.pendingControls).find(([, utterance]) => utterance === event.detail?.utterance)?.[0];
appendSyncLog({ category: "task", title: "编排任务执行失败", detail: event.detail.error.message || "请求失败", status: "error" }); if (pendingDevice) {
const device = syncedDevices.find((item) => item.id === pendingDevice);
delete displaySync.pendingControls[pendingDevice];
if (!event.detail?.error) delete displaySync.deviceDrafts[pendingDevice];
appendSyncLog({
category: "device",
title: event.detail?.error ? `${device?.name || "设备"}控制发送失败` : `${device?.name || "设备"}控制已执行`,
detail: event.detail?.error?.message || "控制结果已进入车机状态同步链路",
status: event.detail?.error ? "error" : "success",
});
renderSyncedDevices();
}
if (event.detail?.error) {
appendSyncLog({ category: "task", title: "编排任务执行失败", detail: event.detail.error.message || "请求失败", status: "error" });
}
}); });
window.addEventListener("cognitive-agent:connection", (event) => { window.addEventListener("cognitive-agent:connection", (event) => {
const online = event.detail?.status === "online"; const online = event.detail?.status === "online";
@@ -1152,7 +1429,7 @@
"aria-label", "aria-label",
ui.sidebarCollapsed ? "展开左侧控制台" : "折叠左侧控制台", ui.sidebarCollapsed ? "展开左侧控制台" : "折叠左侧控制台",
); );
button.innerHTML = `<i data-lucide="${ui.sidebarCollapsed ? "panel-left-open" : "panel-left-close"}"></i>`; button.innerHTML = `<i data-lucide="${ui.sidebarCollapsed ? "panel-left-open" : "panel-left-close"}"></i><span>${ui.sidebarCollapsed ? "展开" : "收起"}</span>`;
} }
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
window.setTimeout(() => window.dispatchEvent(new Event("resize")), 180); window.setTimeout(() => window.dispatchEvent(new Event("resize")), 180);
@@ -1273,6 +1550,7 @@
if (event.key === "Escape") { if (event.key === "Escape") {
closeCustomDropdowns(); closeCustomDropdowns();
closeReasoningPicker(); closeReasoningPicker();
clearTwinDevicePreview({ force: true });
} }
}); });
window.addEventListener("resize", updateScrollCues); window.addEventListener("resize", updateScrollCues);
+4 -4
View File
@@ -10,8 +10,8 @@
<link rel="icon" href="/icons/app-icon-192.png" /> <link rel="icon" href="/icons/app-icon-192.png" />
<title>511工程-智能座舱</title> <title>511工程-智能座舱</title>
<link rel="stylesheet" href="/styles.css?v=dual-seat-9" /> <link rel="stylesheet" href="/styles.css?v=dual-seat-9" />
<link rel="stylesheet" href="/console-v2.css?v=sync-layout-11" /> <link rel="stylesheet" href="/console-v2.css?v=sync-layout-26" />
<link rel="stylesheet" href="/self-extension.css?v=sync-layout-11" /> <link rel="stylesheet" href="/self-extension.css?v=sync-layout-26" />
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script> <script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
<script src="/vendor/three/0.147.0/three.min.js" 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/fflate.min.js" defer></script>
@@ -20,8 +20,8 @@
<script src="/app.js?v=dual-seat-85" defer></script> <script src="/app.js?v=dual-seat-85" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-8"></script> <script type="module" src="/gesture-control.js?v=android-pad-8"></script>
<script src="/pwa.js?v=android-pad-2" defer></script> <script src="/pwa.js?v=android-pad-2" defer></script>
<script src="/console-v2.js?v=sync-layout-11" defer></script> <script src="/console-v2.js?v=sync-layout-26" defer></script>
<script src="/self-extension.js?v=sync-layout-11" defer></script> <script src="/self-extension.js?v=sync-layout-26" defer></script>
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
+5 -5
View File
@@ -1,4 +1,4 @@
const CACHE_VERSION = "cognitive-agent-pad-v163"; const CACHE_VERSION = "cognitive-agent-pad-v178";
const SHELL_CACHE = `${CACHE_VERSION}-shell`; const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`; const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [ const APP_SHELL = [
@@ -9,10 +9,10 @@ const APP_SHELL = [
"/app.js?v=dual-seat-85", "/app.js?v=dual-seat-85",
"/gesture-control.js?v=android-pad-8", "/gesture-control.js?v=android-pad-8",
"/pwa.js?v=android-pad-2", "/pwa.js?v=android-pad-2",
"/console-v2.css?v=sync-layout-11", "/console-v2.css?v=sync-layout-26",
"/console-v2.js?v=sync-layout-11", "/console-v2.js?v=sync-layout-26",
"/self-extension.css?v=sync-layout-11", "/self-extension.css?v=sync-layout-26",
"/self-extension.js?v=sync-layout-11", "/self-extension.js?v=sync-layout-26",
"/assets/pets/self-extension-cat.png?v=3", "/assets/pets/self-extension-cat.png?v=3",
"/assets/pets/self-extension-cat-entrance.png?v=3", "/assets/pets/self-extension-cat-entrance.png?v=3",
"/manifest.webmanifest", "/manifest.webmanifest",
+9 -9
View File
@@ -236,19 +236,19 @@ operations:
) as response: ) as response:
gesture_script = response.read().decode("utf-8") gesture_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
f"{self.base_url}/console-v2.js?v=sync-layout-11", timeout=3 f"{self.base_url}/console-v2.js?v=sync-layout-26", timeout=3
) as response: ) as response:
console_script = response.read().decode("utf-8") console_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
f"{self.base_url}/console-v2.css?v=sync-layout-11", timeout=3 f"{self.base_url}/console-v2.css?v=sync-layout-26", timeout=3
) as response: ) as response:
console_styles = response.read().decode("utf-8") console_styles = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
f"{self.base_url}/self-extension.js?v=sync-layout-11", timeout=3 f"{self.base_url}/self-extension.js?v=sync-layout-26", timeout=3
) as response: ) as response:
extension_script = response.read().decode("utf-8") extension_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
f"{self.base_url}/self-extension.css?v=sync-layout-11", timeout=3 f"{self.base_url}/self-extension.css?v=sync-layout-26", timeout=3
) as response: ) as response:
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n") extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
with self.opener.open( with self.opener.open(
@@ -279,10 +279,10 @@ operations:
self.assertIn('href="/styles.css?v=dual-seat-9"', html) self.assertIn('href="/styles.css?v=dual-seat-9"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-8"', html) self.assertIn('src="/gesture-control.js?v=android-pad-8"', html)
self.assertNotIn('class="vehicle-system-hud"', html) self.assertNotIn('class="vehicle-system-hud"', html)
self.assertIn('src="/console-v2.js?v=sync-layout-11"', html) self.assertIn('src="/console-v2.js?v=sync-layout-26"', html)
self.assertIn('href="/console-v2.css?v=sync-layout-11"', html) self.assertIn('href="/console-v2.css?v=sync-layout-26"', html)
self.assertIn('src="/self-extension.js?v=sync-layout-11"', html) self.assertIn('src="/self-extension.js?v=sync-layout-26"', html)
self.assertIn('href="/self-extension.css?v=sync-layout-11"', html) self.assertIn('href="/self-extension.css?v=sync-layout-26"', html)
self.assertNotIn("self-calibration", html) self.assertNotIn("self-calibration", html)
self.assertNotIn("self-repair", html) self.assertNotIn("self-repair", html)
self.assertNotIn("self-evolution", html) self.assertNotIn("self-evolution", html)
@@ -484,7 +484,7 @@ operations:
self.assertIn('client_type: "web_console"', app_script) self.assertIn('client_type: "web_console"', app_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script) self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script) self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v163"', service_worker) self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v178"', service_worker)
self.assertIn('"/assets/pets/self-extension-cat.png?v=3"', service_worker) self.assertIn('"/assets/pets/self-extension-cat.png?v=3"', service_worker)
self.assertIn(".then(() => self.skipWaiting())", service_worker) self.assertIn(".then(() => self.skipWaiting())", service_worker)
self.assertNotIn("const cached = await caches.match(request)", service_worker) self.assertNotIn("const cached = await caches.match(request)", service_worker)
@@ -103,3 +103,113 @@ def test_orchestration_session_reset_remains_available() -> None:
orchestration = source("app.js") orchestration = source("app.js")
assert 'sessionStorage.removeItem("cognitive-agent.conversation-id")' in orchestration assert 'sessionStorage.removeItem("cognitive-agent.conversation-id")' in orchestration
def test_vehicle_device_status_cards_prioritize_seats_and_expose_manual_controls() -> None:
console = source("console-v2.js")
styles = source("console-v2.css")
passenger = console.index('id: "passenger", name: "副驾座椅"')
driver = console.index('id: "driver", name: "主驾座椅"')
windows = console.index('id: "windows", name: "车窗"')
assert passenger < driver < windows
for key_status in (
'["左前窗", readText("#frontLeftWindow")]',
'["右前窗", readText("#frontRightWindow")]',
'["左后窗", readText("#rearLeftWindow")]',
'["右后窗", readText("#rearRightWindow")]',
'["当前位置", readText("#shadePositionValue")]',
'["按摩模式", massageMode]',
'["按摩强度", massageStrength]',
'["前排亮度", frontBrightness]',
'["前排颜色", frontColor]',
'["后排亮度", rearBrightness]',
'["后排颜色", rearColor]',
):
assert key_status in console
assert 'data-device-control-toggle="${device.id}"' in console
assert 'data-device-control-apply="${device.id}"' in console
assert 'new CustomEvent("cognitive-agent:vehicle-control-request"' in console
assert "window.CognitiveAgentConsole?.executeUtterance?.(utterance" in console
assert '.v2-device-control-row input[type="range"]' in styles
assert ".v2-device-control-select" in styles
assert ".v2-device-control-apply" in styles
def test_interactive_console_keeps_actions_inside_composer_and_collapsed_rails_aligned() -> None:
console = source("console-v2.js")
styles = source("console-v2.css")
assert 'const resetButton = $("#newConversation", originalForm);' in console
assert 'const runButton = $("#runButton", originalForm);' in console
assert "if (resetButton) composer.append(resetButton);" in console
assert "if (runButton) composer.append(runButton);" in console
assert ".v2-request-form .command-composer" in styles
assert "overflow: hidden;" in styles
assert "transform: translateY(-50%);" in styles
assert '.joint-twin-shell[data-open-panels="none"]' in styles
assert "grid-template-columns: 48px minmax(520px, 1fr) 48px;" in styles
assert "writing-mode: horizontal-tb;" in styles
assert "writing-mode: vertical-rl;" in styles
def test_left_sync_rail_can_collapse_and_log_aligns_with_lower_workspace() -> None:
console = source("console-v2.js")
styles = source("console-v2.css")
assert 'make("section", "v2-sync-sidebar"' in console
assert '<div class="v2-sync-sidebar-overview">' in console
assert '$(".v2-sync-sidebar-overview", syncSidebar)?.append(sidebarFooter);' in console
assert '$(".v2-sync-sidebar-heading", syncSidebar)?.append(sidebarToggle);' in console
assert "function syncSidebarLogBaseline()" in console
assert 'sidebar.style.setProperty("--v2-sync-log-top"' in console
assert 'window.addEventListener("resize", syncSidebarLogBaseline);' in console
assert "requestAnimationFrame(syncSidebarLogBaseline);" in console
assert "--v2-sync-log-top: 291px;" in styles
assert "grid-template-rows: calc(var(--v2-sync-log-top) - 10px) minmax(0, 1fr);" in styles
assert ".v2-sync-sidebar > .v2-sync-log" in styles
assert "grid-template-columns: 64px minmax(0, 1fr);" in styles
assert ".v2-sidebar-collapsed #v2SidebarToggle" in styles
def test_twin_device_status_popover_has_clear_close_paths() -> None:
console = source("console-v2.js")
styles = source("console-v2.css")
assert 'data-twin-popover-close aria-label="关闭${escapeHtml(device.name)}状态详情"' in console
assert 'popover.setAttribute("role", "dialog");' in console
assert 'popover.setAttribute("aria-label", "车端设备状态详情");' in console
assert 'event.target.closest("[data-twin-popover-close]")' in console
assert 'event.target.closest("[data-twin-device], #v2TwinDevicePopover")' in console
assert "const samePinned = displaySync.pinnedDevice === focus.dataset.deviceFocus;" in console
assert "clearTwinDevicePreview({ force: true });" in console
assert ".v2-twin-popover-close" in styles
assert ".v2-twin-popover-close:focus-visible" in styles
def test_twin_device_hotspots_use_responsive_hover_and_leader_labels() -> None:
console = source("console-v2.js")
styles = source("console-v2.css")
assert "previewClearTimer: null" in console
assert "hoverSuppressedUntil: 0" in console
assert 'class="v2-twin-hotspot-label"' in console
assert 'button.addEventListener("pointerenter"' in console
assert 'button.addEventListener("pointerleave"' in console
assert "scheduleTwinDevicePreviewClear();" in console
assert "}, 45);" in console
assert "performance.now() + 160" in console
assert 'hotspots.addEventListener("pointerover"' not in console
assert 'hotspots.addEventListener("pointerout"' not in console
assert ".v2-twin-hotspots button::before" in styles
assert ".v2-twin-hotspots button::after" in styles
assert ".v2-twin-hotspot-label" in styles
assert "--v2-hotspot-label-x: -115px" in styles
assert "--v2-hotspot-line-angle: -150deg" in styles
assert '.v2-twin-device-popover[data-pinned="false"]' in styles
assert "pointer-events: none;" in styles