feat(web-console): refine intelligent cockpit sync interface
This commit is contained in:
+952
-35
File diff suppressed because it is too large
Load Diff
+340
-62
@@ -112,7 +112,11 @@
|
||||
place();
|
||||
};
|
||||
reasoningPickerControl = { close, open, place, sync, trigger };
|
||||
const resetButton = $("#newConversation", originalForm);
|
||||
const runButton = $("#runButton", originalForm);
|
||||
if (resetButton) composer.append(resetButton);
|
||||
composer.append(trigger);
|
||||
if (runButton) composer.append(runButton);
|
||||
document.body.append(reasoningField);
|
||||
|
||||
trigger.addEventListener("click", (event) => {
|
||||
@@ -493,8 +497,14 @@
|
||||
requestId: null,
|
||||
pollTimer: null,
|
||||
deviceFrame: null,
|
||||
deviceFingerprint: "",
|
||||
activeDevice: null,
|
||||
pinnedDevice: null,
|
||||
previewClearTimer: null,
|
||||
hoverSuppressedUntil: 0,
|
||||
controlDevice: null,
|
||||
deviceDrafts: {},
|
||||
pendingControls: {},
|
||||
logs: [],
|
||||
logKeys: new Set(),
|
||||
extensionFingerprint: "",
|
||||
@@ -502,55 +512,172 @@
|
||||
|
||||
const readText = (selector, fallback = "--") => $(selector)?.textContent?.trim() || fallback;
|
||||
|
||||
function seatDeviceRows(seat) {
|
||||
const card = $(`.seat-card[data-seat="${seat}"]`);
|
||||
if (!card) return [["状态", "等待反馈"]];
|
||||
const rows = [
|
||||
["反馈通道", $("[data-seat-feedback]", card)?.textContent?.trim() || "等待反馈"],
|
||||
...$$('[data-function]', card).map((item) => [
|
||||
item.textContent.replace(item.querySelector("b")?.textContent || "", "").trim(),
|
||||
$("b", item)?.textContent?.trim() || "--",
|
||||
]),
|
||||
...$$('.motion-axis', card).map((item) => [
|
||||
$("span", item)?.textContent?.trim() || "姿态",
|
||||
$("b", item)?.textContent?.trim() || "--",
|
||||
]),
|
||||
];
|
||||
return rows.filter(([label]) => label);
|
||||
const clampControlNumber = (value, min, max, fallback = min) => {
|
||||
const numeric = Number.parseFloat(String(value ?? "").replace(/[^\d.-]/g, ""));
|
||||
return Number.isFinite(numeric) ? Math.max(min, Math.min(max, numeric)) : fallback;
|
||||
};
|
||||
|
||||
const splitReadout = (selector) => {
|
||||
const [front = "-", rear = "-"] = readText(selector, "- / -").split("/");
|
||||
return [front.trim() || "-", rear.trim() || "-"];
|
||||
};
|
||||
|
||||
function seatFunctionValue(seat, feature) {
|
||||
return $(`.seat-card[data-seat="${seat}"] [data-function="${feature}"] b`)?.textContent?.trim() || "--";
|
||||
}
|
||||
|
||||
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 = [
|
||||
{
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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) {
|
||||
const values = device.rows().map(([, value]) => value).filter(Boolean);
|
||||
return values.slice(0, 3).join(" · ") || "等待反馈";
|
||||
function renderDeviceControl(device, control) {
|
||||
const value = deviceControlValue(device.id, control);
|
||||
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() {
|
||||
@@ -558,25 +685,83 @@
|
||||
if (!grid) return;
|
||||
grid.innerHTML = syncedDevices.map((device) => {
|
||||
const rows = device.rows();
|
||||
const detail = deviceSummary(device);
|
||||
const waiting = /--|等待|同步中|未知/.test(detail);
|
||||
return `<button class="v2-synced-device" type="button" data-device="${device.id}" data-ready="${String(!waiting)}" aria-label="查看${device.name}完整状态">
|
||||
<span><i data-lucide="${device.icon}"></i></span>
|
||||
<div><strong>${device.name}</strong><small>${escapeHtml(detail)}</small></div>
|
||||
<i class="v2-device-signal"></i>
|
||||
<dl>${rows.slice(0, 6).map(([label, value]) => `<div><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`).join("")}</dl>
|
||||
</button>`;
|
||||
const waiting = rows.some(([, value]) => /--|等待|同步中|未知/.test(value));
|
||||
const controlOpen = displaySync.controlDevice === device.id;
|
||||
const hasDraft = Boolean(displaySync.deviceDrafts[device.id]);
|
||||
const pending = Boolean(displaySync.pendingControls[device.id]);
|
||||
const stateLabel = pending ? "已发送 · 等待同步" : hasDraft ? "参数待发送" : `${rows.length} 项关键状态`;
|
||||
return `<article class="v2-synced-device" data-device="${device.id}" data-ready="${String(!waiting)}" data-control-open="${String(controlOpen)}" data-has-draft="${String(hasDraft)}">
|
||||
<header class="v2-device-card-header">
|
||||
<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("");
|
||||
if (displaySync.activeDevice) {
|
||||
$(`.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" } });
|
||||
}
|
||||
|
||||
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() {
|
||||
if (displaySync.deviceFrame) return;
|
||||
displaySync.deviceFrame = requestAnimationFrame(() => {
|
||||
displaySync.deviceFrame = null;
|
||||
const fingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, device.rows()]));
|
||||
if (fingerprint === displaySync.deviceFingerprint && $("#v2DeviceSyncGrid")?.childElementCount) return;
|
||||
renderSyncedDevices();
|
||||
if (displaySync.activeDevice) presentTwinDevice(displaySync.activeDevice, {
|
||||
pinned: Boolean(displaySync.pinnedDevice),
|
||||
@@ -655,6 +840,10 @@
|
||||
const device = syncedDevices.find((item) => item.id === deviceId);
|
||||
const popover = $("#v2TwinDevicePopover");
|
||||
if (!device || !popover) return;
|
||||
if (displaySync.previewClearTimer) {
|
||||
window.clearTimeout(displaySync.previewClearTimer);
|
||||
displaySync.previewClearTimer = null;
|
||||
}
|
||||
displaySync.activeDevice = deviceId;
|
||||
if (pinned) displaySync.pinnedDevice = deviceId;
|
||||
const rows = device.rows();
|
||||
@@ -662,7 +851,7 @@
|
||||
popover.dataset.device = deviceId;
|
||||
popover.dataset.pinned = String(Boolean(displaySync.pinnedDevice));
|
||||
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>`;
|
||||
$$("[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));
|
||||
@@ -673,6 +862,10 @@
|
||||
}
|
||||
|
||||
function clearTwinDevicePreview({ force = false } = {}) {
|
||||
if (displaySync.previewClearTimer) {
|
||||
window.clearTimeout(displaySync.previewClearTimer);
|
||||
displaySync.previewClearTimer = null;
|
||||
}
|
||||
if (displaySync.pinnedDevice && !force) return;
|
||||
displaySync.activeDevice = null;
|
||||
if (force) displaySync.pinnedDevice = null;
|
||||
@@ -681,30 +874,42 @@
|
||||
$$("[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() {
|
||||
const stage = $("#seatTwin");
|
||||
if (!stage || $("#v2TwinHotspots")) return;
|
||||
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";
|
||||
const popover = make("aside", "v2-twin-device-popover", "");
|
||||
popover.id = "v2TwinDevicePopover";
|
||||
popover.hidden = true;
|
||||
popover.setAttribute("role", "dialog");
|
||||
popover.setAttribute("aria-label", "车端设备状态详情");
|
||||
popover.setAttribute("aria-live", "polite");
|
||||
stage.append(hotspots, popover);
|
||||
|
||||
hotspots.addEventListener("pointerdown", (event) => event.stopPropagation());
|
||||
hotspots.addEventListener("pointerover", (event) => {
|
||||
const button = event.target.closest("[data-twin-device]");
|
||||
if (button && !displaySync.pinnedDevice) presentTwinDevice(button.dataset.twinDevice);
|
||||
});
|
||||
hotspots.addEventListener("focusin", (event) => {
|
||||
const button = event.target.closest("[data-twin-device]");
|
||||
if (button) presentTwinDevice(button.dataset.twinDevice);
|
||||
});
|
||||
hotspots.addEventListener("pointerout", (event) => {
|
||||
if (event.relatedTarget?.closest?.("#v2TwinDevicePopover")) return;
|
||||
window.setTimeout(() => clearTwinDevicePreview(), 100);
|
||||
$$("[data-twin-device]", hotspots).forEach((button) => {
|
||||
button.addEventListener("pointerenter", () => {
|
||||
if (!displaySync.pinnedDevice && performance.now() >= displaySync.hoverSuppressedUntil) {
|
||||
presentTwinDevice(button.dataset.twinDevice);
|
||||
}
|
||||
});
|
||||
button.addEventListener("pointerleave", (event) => {
|
||||
if (event.relatedTarget?.closest?.("#v2TwinDevicePopover")) return;
|
||||
scheduleTwinDevicePreviewClear();
|
||||
});
|
||||
button.addEventListener("focus", () => presentTwinDevice(button.dataset.twinDevice));
|
||||
button.addEventListener("blur", scheduleTwinDevicePreviewClear);
|
||||
});
|
||||
hotspots.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-twin-device]");
|
||||
@@ -715,6 +920,23 @@
|
||||
else presentTwinDevice(button.dataset.twinDevice, { pinned: true });
|
||||
});
|
||||
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" } });
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
document.body.classList.add("v2-passive-display", "v2-interactive-sync");
|
||||
document.body.dataset.module = "self-orchestration";
|
||||
@@ -900,18 +1130,25 @@
|
||||
const sidebar = $(".v2-sidebar");
|
||||
const sidebarFooter = $(".v2-sidebar-footer", 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-link-stack">
|
||||
<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="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 class="v2-sync-sidebar-overview">
|
||||
<div class="v2-sync-sidebar-heading"><small>REMOTE CLIENT</small><h2>511工程-智能座舱</h2><p>网页可远程编排,车机任务、设备反馈与 USB 外设能力保持同步。</p></div>
|
||||
<div class="v2-sync-link-stack">
|
||||
<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="v2MirrorState" data-state="checking"><i data-lucide="refresh-cw"></i><div><strong>网页同步</strong><span>建立同步中</span></div><b></b></div>
|
||||
<div class="v2-sync-link" id="v2UsbState" data-state="checking"><i data-lucide="usb"></i><div><strong>USB 扩展</strong><span>检查外设中</span></div><b></b></div>
|
||||
</div>
|
||||
<div class="v2-sync-protocol"><i data-lucide="radio-tower"></i><div><small>扩展链路</small><strong id="v2SyncTransport">GSCU · 等待连接</strong></div></div>
|
||||
</div>
|
||||
<section class="v2-sync-log" aria-labelledby="v2SyncLogTitle">
|
||||
<header><div><small>RUNTIME / SYNC</small><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div><select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select></header>
|
||||
<ol id="v2SyncLogList" aria-live="polite"></ol>
|
||||
</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) sidebar.insertBefore(syncSidebar, sidebarFooter);
|
||||
</section>`);
|
||||
if (sidebar && 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 taskOverview = make("section", "v2-task-overview", `
|
||||
@@ -952,6 +1189,8 @@
|
||||
}
|
||||
setSidePanelVisibility("devices", panelVisible("devices"));
|
||||
setSidePanelVisibility("extensions", panelVisible("extensions"));
|
||||
window.addEventListener("resize", syncSidebarLogBaseline);
|
||||
requestAnimationFrame(syncSidebarLogBaseline);
|
||||
document.addEventListener("click", (event) => {
|
||||
const toggle = event.target.closest("[data-panel-toggle], [data-panel-close]");
|
||||
if (toggle) {
|
||||
@@ -959,10 +1198,34 @@
|
||||
setSidePanelVisibility(key, !panelVisible(key));
|
||||
}
|
||||
});
|
||||
$("#v2DeviceSyncGrid")?.addEventListener("click", (event) => {
|
||||
const card = event.target.closest("[data-device]");
|
||||
if (!card) return;
|
||||
presentTwinDevice(card.dataset.device, { pinned: true });
|
||||
const deviceGrid = $("#v2DeviceSyncGrid");
|
||||
deviceGrid?.addEventListener("click", (event) => {
|
||||
const controlToggle = event.target.closest("[data-device-control-toggle]");
|
||||
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);
|
||||
window.addEventListener("cognitive-agent:result", (event) => {
|
||||
@@ -976,8 +1239,22 @@
|
||||
});
|
||||
});
|
||||
window.addEventListener("cognitive-agent:request-complete", (event) => {
|
||||
if (!event.detail?.error) return;
|
||||
appendSyncLog({ category: "task", title: "编排任务执行失败", detail: event.detail.error.message || "请求失败", status: "error" });
|
||||
const pendingDevice = Object.entries(displaySync.pendingControls).find(([, utterance]) => utterance === event.detail?.utterance)?.[0];
|
||||
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) => {
|
||||
const online = event.detail?.status === "online";
|
||||
@@ -1152,7 +1429,7 @@
|
||||
"aria-label",
|
||||
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.setTimeout(() => window.dispatchEvent(new Event("resize")), 180);
|
||||
@@ -1273,6 +1550,7 @@
|
||||
if (event.key === "Escape") {
|
||||
closeCustomDropdowns();
|
||||
closeReasoningPicker();
|
||||
clearTwinDevicePreview({ force: true });
|
||||
}
|
||||
});
|
||||
window.addEventListener("resize", updateScrollCues);
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<link rel="icon" href="/icons/app-icon-192.png" />
|
||||
<title>511工程-智能座舱</title>
|
||||
<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="/self-extension.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-26" />
|
||||
<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/fflate.min.js" defer></script>
|
||||
@@ -20,8 +20,8 @@
|
||||
<script src="/app.js?v=dual-seat-85" defer></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="/console-v2.js?v=sync-layout-11" defer></script>
|
||||
<script src="/self-extension.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-26" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
|
||||
@@ -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 RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||
const APP_SHELL = [
|
||||
@@ -9,10 +9,10 @@ const APP_SHELL = [
|
||||
"/app.js?v=dual-seat-85",
|
||||
"/gesture-control.js?v=android-pad-8",
|
||||
"/pwa.js?v=android-pad-2",
|
||||
"/console-v2.css?v=sync-layout-11",
|
||||
"/console-v2.js?v=sync-layout-11",
|
||||
"/self-extension.css?v=sync-layout-11",
|
||||
"/self-extension.js?v=sync-layout-11",
|
||||
"/console-v2.css?v=sync-layout-26",
|
||||
"/console-v2.js?v=sync-layout-26",
|
||||
"/self-extension.css?v=sync-layout-26",
|
||||
"/self-extension.js?v=sync-layout-26",
|
||||
"/assets/pets/self-extension-cat.png?v=3",
|
||||
"/assets/pets/self-extension-cat-entrance.png?v=3",
|
||||
"/manifest.webmanifest",
|
||||
|
||||
Reference in New Issue
Block a user