1192 lines
50 KiB
JavaScript
1192 lines
50 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const pageDefinitions = [
|
|
{ index: 0, label: "整车联合数字孪生", shortLabel: "数字孪生", target: ".joint-twin-shell" },
|
|
{ index: 1, label: "决策轨迹与校验", shortLabel: "决策校验", target: "#panel-trace" },
|
|
{ index: 2, label: "指令配置与硬件映射", shortLabel: "配置映射", target: "#panel-config" },
|
|
{ index: 3, label: "CAN 编译计划", shortLabel: "CAN 计划", target: "#panel-plan" },
|
|
{ index: 4, label: "执行结果与硬件回读", shortLabel: "执行回读", target: "#panel-result" },
|
|
];
|
|
|
|
const $ = (selector, root = document) => root.querySelector(selector);
|
|
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
|
const initialRole = sessionStorage.getItem("cognitive-agent.demo-role") === "admin"
|
|
? "admin"
|
|
: "user";
|
|
const storedModule = sessionStorage.getItem("cognitive-agent.active-module");
|
|
|
|
const ui = {
|
|
role: initialRole,
|
|
controlMode: sessionStorage.getItem("cognitive-agent.control-mode") === "gesture"
|
|
? "gesture"
|
|
: "natural",
|
|
activeModule: storedModule === "self-evolution"
|
|
? "self-evolution"
|
|
: storedModule === "self-extension"
|
|
? "self-extension"
|
|
: storedModule === "self-calibration"
|
|
? "self-calibration"
|
|
: storedModule === "self-repair"
|
|
? "self-repair"
|
|
: "self-orchestration",
|
|
activePage: 0,
|
|
sidebarCollapsed: sessionStorage.getItem("cognitive-agent.sidebar-collapsed") === "true",
|
|
};
|
|
|
|
const dropdownControls = new Map();
|
|
let reasoningPickerControl = null;
|
|
|
|
function make(tag, className, html) {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (html != null) node.innerHTML = html;
|
|
return node;
|
|
}
|
|
|
|
function closeCustomDropdowns(except = null) {
|
|
dropdownControls.forEach((control) => {
|
|
if (control !== except) control.close();
|
|
});
|
|
}
|
|
|
|
function closeReasoningPicker() {
|
|
reasoningPickerControl?.close();
|
|
}
|
|
|
|
function installReasoningPicker(reasoningField, originalForm) {
|
|
const composer = $(".command-composer", originalForm);
|
|
if (!composer || !reasoningField) return;
|
|
|
|
reasoningField.classList.add("v2-reasoning-field");
|
|
reasoningField.hidden = true;
|
|
const trigger = make("button", "v2-reasoning-trigger", `
|
|
<i data-lucide="sparkles"></i>
|
|
<span>模型智能</span>
|
|
<strong id="v2ReasoningValue">均衡</strong>
|
|
<i class="v2-reasoning-caret" data-lucide="chevron-up"></i>`);
|
|
trigger.id = "v2ReasoningTrigger";
|
|
trigger.type = "button";
|
|
trigger.setAttribute("aria-haspopup", "listbox");
|
|
trigger.setAttribute("aria-expanded", "false");
|
|
trigger.setAttribute("aria-controls", "reasoningField");
|
|
trigger.setAttribute("aria-label", "选择模型智能程度,当前均衡");
|
|
$(".reasoning-segmented", reasoningField)?.setAttribute("role", "listbox");
|
|
|
|
const options = () => $$('[data-reasoning-effort]', reasoningField);
|
|
const place = () => {
|
|
if (reasoningField.hidden) return;
|
|
const triggerBox = trigger.getBoundingClientRect();
|
|
const width = Math.min(320, Math.max(250, window.innerWidth - 24));
|
|
const left = Math.min(
|
|
Math.max(12, triggerBox.left),
|
|
Math.max(12, window.innerWidth - width - 12),
|
|
);
|
|
reasoningField.style.width = `${width}px`;
|
|
reasoningField.style.left = `${left}px`;
|
|
const height = reasoningField.offsetHeight;
|
|
const above = triggerBox.top - height - 8;
|
|
const top = above >= 12
|
|
? above
|
|
: Math.min(triggerBox.bottom + 8, window.innerHeight - height - 12);
|
|
reasoningField.style.top = `${Math.max(12, top)}px`;
|
|
};
|
|
const sync = () => {
|
|
const selected = options().find((button) => button.classList.contains("active"));
|
|
const label = selected?.querySelector("strong")?.textContent?.trim() || "均衡";
|
|
const unavailable = options().length > 0 && options().every((button) => button.disabled);
|
|
$("#v2ReasoningValue", trigger).textContent = label;
|
|
trigger.classList.toggle("unavailable", unavailable);
|
|
trigger.setAttribute("aria-disabled", String(unavailable));
|
|
trigger.setAttribute("aria-label", `选择模型智能程度,当前${label}`);
|
|
options().forEach((button) => {
|
|
button.setAttribute("role", "option");
|
|
button.setAttribute("aria-selected", String(button === selected));
|
|
});
|
|
};
|
|
const close = () => {
|
|
reasoningField.hidden = true;
|
|
trigger.classList.remove("open");
|
|
trigger.setAttribute("aria-expanded", "false");
|
|
};
|
|
const open = () => {
|
|
closeCustomDropdowns();
|
|
reasoningField.hidden = false;
|
|
trigger.classList.add("open");
|
|
trigger.setAttribute("aria-expanded", "true");
|
|
place();
|
|
};
|
|
reasoningPickerControl = { close, open, place, sync, trigger };
|
|
composer.append(trigger);
|
|
document.body.append(reasoningField);
|
|
|
|
trigger.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
if (reasoningField.hidden) open();
|
|
else close();
|
|
});
|
|
reasoningField.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
if (!event.target.closest("[data-reasoning-effort]")) return;
|
|
requestAnimationFrame(() => {
|
|
sync();
|
|
close();
|
|
trigger.focus({ preventScroll: true });
|
|
});
|
|
});
|
|
new MutationObserver(sync).observe(reasoningField, {
|
|
attributes: true,
|
|
attributeFilter: ["class", "disabled"],
|
|
subtree: true,
|
|
});
|
|
window.addEventListener("resize", place);
|
|
sync();
|
|
}
|
|
|
|
function enhanceSelect(select) {
|
|
if (!select || dropdownControls.has(select)) return;
|
|
const shell = select.parentElement;
|
|
if (!shell) return;
|
|
|
|
shell.classList.add("v2-custom-select");
|
|
select.classList.add("v2-native-select");
|
|
|
|
const trigger = make("button", "v2-dropdown-trigger", `
|
|
<span class="v2-dropdown-value"></span>
|
|
<span class="v2-dropdown-caret" aria-hidden="true"></span>`);
|
|
trigger.type = "button";
|
|
trigger.setAttribute("aria-haspopup", "listbox");
|
|
trigger.setAttribute("aria-expanded", "false");
|
|
trigger.setAttribute("aria-label", select.getAttribute("aria-label") || "打开下拉菜单");
|
|
|
|
const menu = make("div", "v2-dropdown-menu", "");
|
|
menu.id = `${select.id}Menu`;
|
|
menu.hidden = true;
|
|
menu.setAttribute("role", "listbox");
|
|
menu.setAttribute("aria-label", select.getAttribute("aria-label") || "选项");
|
|
trigger.setAttribute("aria-controls", menu.id);
|
|
shell.append(trigger, menu);
|
|
|
|
const valueNode = $(".v2-dropdown-value", trigger);
|
|
const render = () => {
|
|
const selected = select.selectedOptions[0] || select.options[0];
|
|
valueNode.textContent = selected?.textContent?.trim() || "请选择";
|
|
trigger.disabled = select.disabled;
|
|
menu.innerHTML = "";
|
|
[...select.options].forEach((option) => {
|
|
const item = make("button", "v2-dropdown-option", "");
|
|
item.type = "button";
|
|
item.dataset.value = option.value;
|
|
item.textContent = option.textContent.trim();
|
|
item.disabled = option.disabled;
|
|
item.setAttribute("role", "option");
|
|
item.setAttribute("aria-selected", String(option.value === select.value));
|
|
menu.append(item);
|
|
});
|
|
};
|
|
|
|
const close = () => {
|
|
menu.hidden = true;
|
|
shell.classList.remove("v2-dropdown-open");
|
|
trigger.setAttribute("aria-expanded", "false");
|
|
};
|
|
|
|
const open = () => {
|
|
if (trigger.disabled) return;
|
|
closeCustomDropdowns();
|
|
render();
|
|
menu.hidden = false;
|
|
shell.classList.add("v2-dropdown-open");
|
|
trigger.setAttribute("aria-expanded", "true");
|
|
};
|
|
|
|
const control = { close, menu, open, select, shell, sync: render, trigger };
|
|
dropdownControls.set(select, control);
|
|
|
|
trigger.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
if (menu.hidden) open();
|
|
else close();
|
|
});
|
|
trigger.addEventListener("keydown", (event) => {
|
|
if (event.key === "ArrowDown") {
|
|
event.preventDefault();
|
|
open();
|
|
requestAnimationFrame(() => {
|
|
$(".v2-dropdown-option[aria-selected=\"true\"]:not(:disabled)", menu)?.focus();
|
|
});
|
|
} else if (event.key === "Escape") {
|
|
close();
|
|
}
|
|
});
|
|
menu.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
const item = event.target.closest(".v2-dropdown-option");
|
|
if (!item || item.disabled) return;
|
|
select.value = item.dataset.value;
|
|
select.dispatchEvent(new Event("change", { bubbles: true }));
|
|
render();
|
|
close();
|
|
trigger.focus();
|
|
});
|
|
menu.addEventListener("keydown", (event) => {
|
|
if (event.key !== "Escape") return;
|
|
event.preventDefault();
|
|
close();
|
|
trigger.focus();
|
|
});
|
|
select.addEventListener("change", render);
|
|
new MutationObserver(render).observe(select, {
|
|
attributes: true,
|
|
characterData: true,
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
render();
|
|
}
|
|
|
|
function enhanceConfiguredDropdowns() {
|
|
[$("#v2RoleSelect"), $("#vehicleSelect"), $("#v2ExecutionSelect")]
|
|
.filter(Boolean)
|
|
.forEach(enhanceSelect);
|
|
}
|
|
|
|
function buildSidebar(commandPanel) {
|
|
commandPanel.classList.add("v2-sidebar");
|
|
commandPanel.innerHTML = "";
|
|
|
|
const profile = make("section", "v2-profile", `
|
|
<div class="v2-avatar" aria-hidden="true"><span></span></div>
|
|
<div class="v2-profile-copy">
|
|
<small>WELCOME TO THE CABIN</small>
|
|
<div class="v2-role-select">
|
|
<span>Hi,</span>
|
|
<select id="v2RoleSelect" aria-label="切换演示权限">
|
|
<option value="user">user</option>
|
|
<option value="admin">admin</option>
|
|
</select>
|
|
</div>
|
|
<div class="v2-extension-profile-title" aria-label="自扩展面向所有登录用户">
|
|
<span>Hi,</span><strong>user</strong>
|
|
</div>
|
|
</div>
|
|
<button class="v2-sidebar-toggle" id="v2SidebarToggle" type="button" aria-label="折叠左侧控制台" aria-expanded="true">
|
|
<i data-lucide="panel-left-close"></i>
|
|
</button>`);
|
|
|
|
const control = make("section", "v2-sidebar-section v2-control-section", `
|
|
<div class="v2-section-heading">
|
|
<span>CONTROL DESK</span>
|
|
<h2>控制台</h2>
|
|
</div>
|
|
<span class="v2-field-caption">控制方式</span>
|
|
<div class="v2-control-switch" role="group" aria-label="控制方式">
|
|
<button type="button" data-v2-control="natural"><i data-lucide="message-square-text"></i><span>自然语言控制</span></button>
|
|
<button type="button" data-v2-control="gesture"><i data-lucide="scan-face"></i><span>手势控制</span></button>
|
|
</div>`);
|
|
|
|
const config = make("section", "v2-sidebar-section v2-config-section", `
|
|
<div class="v2-divider"></div>
|
|
<span class="v2-field-caption">配置信息</span>
|
|
<div class="v2-config-stack" id="v2ConfigStack"></div>`);
|
|
|
|
const footer = make("footer", "v2-sidebar-footer", `
|
|
<span class="v2-live-dot"></span>
|
|
<div><strong>智舱控制链路</strong><small id="v2ConnectionText">正在连接服务</small></div>
|
|
<button type="button" id="v2LogoutButton" title="退出登录" aria-label="退出登录"><i data-lucide="log-out"></i></button>`);
|
|
|
|
commandPanel.append(profile, control, config, footer);
|
|
$("#v2RoleSelect").value = ui.role;
|
|
return $("#v2ConfigStack");
|
|
}
|
|
|
|
function buildConfigFields(configStack, originalForm) {
|
|
const vehicleField = $(".vehicle-field", originalForm);
|
|
const providerField = $(".field-group:has(.segmented [data-provider])", originalForm);
|
|
const reasoningField = $("#reasoningField", originalForm);
|
|
const executionField = $(".field-group:has(.execution-mode-options)", originalForm);
|
|
const simulationFaultField = $("#simulationFaultField", originalForm);
|
|
|
|
if (vehicleField) {
|
|
vehicleField.classList.add("v2-config-field", "v2-vehicle-field");
|
|
const vehicleControl = $(".vehicle-select-control", vehicleField);
|
|
const vehicleSelect = $("#vehicleSelect", vehicleField);
|
|
const vehicleIcon = $("svg, [data-lucide]", vehicleControl);
|
|
if (vehicleControl && vehicleSelect && vehicleIcon) {
|
|
const copy = make("div", "v2-config-card-copy", "<small>车型配置</small>");
|
|
const selectSlot = make("div", "v2-config-card-select", "");
|
|
const state = make("span", "v2-field-state", "CHECK");
|
|
state.id = "v2VehicleState";
|
|
selectSlot.append(vehicleSelect);
|
|
copy.append(selectSlot);
|
|
vehicleControl.classList.add("v2-config-card", "v2-config-select-card");
|
|
vehicleControl.id = "v2VehicleCard";
|
|
vehicleControl.dataset.state = "checking";
|
|
vehicleControl.replaceChildren(vehicleIcon, copy, state);
|
|
$(".field-label", vehicleField)?.remove();
|
|
}
|
|
configStack.append(vehicleField);
|
|
}
|
|
|
|
const modelField = make("div", "v2-config-field v2-config-card v2-model-field", `
|
|
<i data-lucide="sparkles"></i>
|
|
<div><small>推理模型</small><strong id="v2ModelValue">模型检测中</strong></div>
|
|
<span class="v2-field-state" id="v2ModelState">AUTO</span>
|
|
<p class="provider-note" id="v2ModelNote">模型由服务端安全配置</p>`);
|
|
modelField.id = "v2ModelField";
|
|
configStack.append(modelField);
|
|
installReasoningPicker(reasoningField, originalForm);
|
|
|
|
const gestureModelField = make("div", "v2-config-field v2-config-card v2-gesture-model-field", `
|
|
<i data-lucide="scan-line"></i>
|
|
<div><small>手势模型</small><strong id="v2GestureModelValue">MediaPipe GR</strong></div>
|
|
<span class="v2-field-state">VISION</span>
|
|
<small class="v2-gesture-model-note" id="v2GestureModelNote">Tasks Vision · 0.10.14</small>`);
|
|
gestureModelField.id = "v2GestureModelField";
|
|
configStack.append(gestureModelField);
|
|
|
|
const userHardwareState = make("div", "v2-user-target-status", `
|
|
<i></i><span id="v2UserTargetText">正在准备虚拟 CAN</span>`);
|
|
configStack.append(userHardwareState);
|
|
|
|
if (providerField) {
|
|
providerField.classList.add("v2-provider-field");
|
|
providerField.hidden = true;
|
|
configStack.append(providerField);
|
|
}
|
|
|
|
const executionSelect = make("div", "v2-config-field v2-execution-field", `
|
|
<div class="v2-config-card v2-config-select-card">
|
|
<i data-lucide="workflow"></i>
|
|
<div class="v2-config-card-copy"><small>执行目标</small><div class="v2-config-card-select">
|
|
<select id="v2ExecutionSelect" aria-label="选择执行目标">
|
|
<option value="dry_run">虚拟 CAN</option>
|
|
<option value="simulated_hardware">模拟台架</option>
|
|
<option value="hardware">真实台架</option>
|
|
</select>
|
|
</div></div>
|
|
<span class="v2-field-state">ADMIN</span>
|
|
</div>
|
|
<p class="provider-note" id="v2ExecutionNote">管理员可切换执行环境</p>`);
|
|
configStack.append(executionSelect);
|
|
|
|
if (executionField) {
|
|
executionField.classList.add("v2-native-execution-field");
|
|
executionField.hidden = true;
|
|
configStack.append(executionField);
|
|
}
|
|
|
|
const advanced = make("div", "v2-advanced-stack", "");
|
|
[simulationFaultField].forEach((field) => {
|
|
if (!field) return;
|
|
field.classList.add("v2-config-field", "v2-advanced-field");
|
|
advanced.append(field);
|
|
});
|
|
configStack.append(advanced);
|
|
}
|
|
|
|
function buildOperation(mainPanel, originalForm, panelHeading, modeRail, requestHeader, gestureDrawer) {
|
|
const operation = make("section", "v2-operation-shell", `
|
|
<div class="v2-operation-glow" aria-hidden="true"></div>
|
|
<div class="v2-natural-panel" id="v2NaturalPanel"></div>
|
|
<div class="v2-gesture-panel" id="v2GesturePanel"></div>`);
|
|
mainPanel.prepend(operation);
|
|
|
|
const naturalPanel = $("#v2NaturalPanel");
|
|
if (panelHeading) {
|
|
panelHeading.classList.add("v2-operation-heading", "v2-module-heading");
|
|
const kicker = $(".section-kicker", panelHeading);
|
|
const title = $("h2", panelHeading);
|
|
const intro = $(".panel-intro", panelHeading);
|
|
if (kicker) kicker.textContent = "SELF-ORCHESTRATION";
|
|
if (title) title.textContent = "一句话,调好整车";
|
|
if (intro) intro.textContent = "座椅、空调、天幕与氛围灯,一句话联动完成";
|
|
naturalPanel.append(panelHeading);
|
|
}
|
|
|
|
const naturalGrid = make("div", "v2-natural-grid", "");
|
|
naturalGrid.id = "v2NaturalGrid";
|
|
originalForm.classList.add("v2-request-form");
|
|
naturalGrid.append(originalForm);
|
|
if (requestHeader) {
|
|
requestHeader.classList.add("v2-request-status");
|
|
naturalGrid.append(requestHeader);
|
|
}
|
|
naturalPanel.append(naturalGrid);
|
|
if (modeRail) {
|
|
modeRail.classList.add("v2-mode-rail");
|
|
naturalPanel.append(modeRail);
|
|
}
|
|
|
|
const gesturePanel = $("#v2GesturePanel");
|
|
const gestureHeading = make("header", "v2-operation-heading v2-module-heading v2-gesture-operation-heading", `
|
|
<span class="section-kicker">SELF-ORCHESTRATION</span>
|
|
<h2>抬抬手,控制整车</h2>
|
|
<p class="panel-intro">通过摄像头识别手势,直观控制车舱功能</p>`);
|
|
gesturePanel.append(gestureHeading);
|
|
if (gestureDrawer) {
|
|
gestureDrawer.classList.add("v2-inline-gesture");
|
|
const cameraStage = $(".gesture-camera-stage", gestureDrawer);
|
|
const cameraToggle = $("#gestureStartButton", gestureDrawer);
|
|
const debugToggle = $("#gestureArmButton", gestureDrawer);
|
|
const actionRail = $(".gesture-actions", gestureDrawer);
|
|
const statusStrip = $(".gesture-status-strip", gestureDrawer);
|
|
const gestureNow = $(".gesture-now", gestureDrawer);
|
|
const guideCard = $(".gesture-guide-card", gestureDrawer);
|
|
if (cameraToggle) {
|
|
cameraToggle.classList.add("v2-camera-toggle");
|
|
cameraToggle.setAttribute("aria-label", "开启摄像头");
|
|
cameraToggle.setAttribute("title", "开启摄像头");
|
|
}
|
|
if (actionRail && cameraToggle && debugToggle) {
|
|
actionRail.classList.add("v2-gesture-action-rail");
|
|
actionRail.insertBefore(debugToggle, cameraToggle);
|
|
actionRail.append(cameraToggle);
|
|
}
|
|
|
|
const resultStack = make("div", "v2-gesture-result-stack", "");
|
|
resultStack.id = "v2GestureResultStack";
|
|
const recognitionCard = make("section", "v2-gesture-recognition-card", `
|
|
<span class="v2-gesture-result-label">手势识别结果</span>`);
|
|
const executionSlot = make("section", "v2-gesture-execution-slot", `
|
|
<span class="v2-gesture-result-label">执行结果</span>`);
|
|
executionSlot.id = "v2GestureExecutionSlot";
|
|
if (statusStrip) recognitionCard.append(statusStrip);
|
|
if (gestureNow) recognitionCard.append(gestureNow);
|
|
resultStack.append(recognitionCard, executionSlot);
|
|
|
|
if (cameraStage) gestureDrawer.append(cameraStage);
|
|
if (actionRail) gestureDrawer.append(actionRail);
|
|
gestureDrawer.append(resultStack);
|
|
if (guideCard) gestureDrawer.append(guideCard);
|
|
gesturePanel.append(gestureDrawer);
|
|
}
|
|
return operation;
|
|
}
|
|
|
|
function buildModuleSwitcher(mainPanel) {
|
|
const modules = [
|
|
{
|
|
id: "self-calibration",
|
|
label: "自标定",
|
|
available: true,
|
|
adminOnly: true,
|
|
},
|
|
{ id: "self-orchestration", label: "自编排", available: true },
|
|
{ id: "self-extension", label: "自扩展", available: true },
|
|
{
|
|
id: "self-repair",
|
|
label: "自修复",
|
|
available: true,
|
|
adminOnly: true,
|
|
},
|
|
{ id: "self-evolution", label: "自进化", available: true },
|
|
];
|
|
const switcher = make("nav", "v2-module-switcher", "");
|
|
switcher.id = "v2ModuleSwitcher";
|
|
switcher.setAttribute("role", "tablist");
|
|
switcher.setAttribute("aria-label", "能力模块切换");
|
|
modules.forEach((module, index) => {
|
|
const button = make("button", "v2-module-tab", `<span>${module.label}</span>`);
|
|
button.type = "button";
|
|
button.id = `v2ModuleTab${index}`;
|
|
button.dataset.module = module.id;
|
|
button.setAttribute("role", "tab");
|
|
button.setAttribute("aria-selected", "false");
|
|
if (module.adminOnly) button.dataset.adminOnly = "true";
|
|
if (!module.available) {
|
|
button.disabled = true;
|
|
button.setAttribute("aria-disabled", "true");
|
|
button.title = module.adminOnly ? `${module.label}仅对 admin 开放` : `${module.label}模块待接入`;
|
|
}
|
|
switcher.append(button);
|
|
});
|
|
mainPanel.prepend(switcher);
|
|
}
|
|
|
|
function buildEvolutionJournal(mainPanel) {
|
|
const evolutionEntries = [
|
|
{
|
|
version: "v0.5",
|
|
date: "2026.08.12",
|
|
status: "当前版本",
|
|
title: "统一主题与权限体验",
|
|
summary: "把权限、主题与核心交互收束到同一套体验规则,让不同模块看起来、用起来都属于同一个系统。",
|
|
changes: [
|
|
["主题统一", "自扩展固定使用 user 绿色主题,避免权限语义混用。"],
|
|
["孪生同步", "3D 模型渲染、玩具动作和车端动作统一跟随当前主题。"],
|
|
["能力扩充", "新增自进化入口,让 user 与 admin 共同查看演进过程。"],
|
|
],
|
|
modules: ["自扩展", "数字孪生", "权限体系"],
|
|
metrics: [
|
|
["主题一致性", "72%", "100%", 100],
|
|
["可见能力模块", "4 个", "5 个", 92],
|
|
["待修正视觉项", "4 项", "0 项", 100],
|
|
],
|
|
userValidation: "核心板块已完成主题复核,权限色与交互反馈保持一致。",
|
|
adminValidation: "前端契约 11/11 · 自标定/自修复检查 8/8",
|
|
},
|
|
{
|
|
version: "v0.4",
|
|
date: "2026.08.11",
|
|
status: "稳定版本",
|
|
title: "自修复闭环增强",
|
|
summary: "从发现异常走向诊断、修复与验证闭环,同时补齐离线场景的默认内容,确保功能状态始终可见。",
|
|
changes: [
|
|
["故障覆盖", "补充六类量产故障用例,覆盖更真实的车端异常。"],
|
|
["修复闭环", "形成发现、定位、决策、执行、验证五阶段流程。"],
|
|
["离线韧性", "服务暂时离线时仍保留默认案例和演示路径。"],
|
|
],
|
|
modules: ["自修复", "故障诊断", "离线体验"],
|
|
metrics: [
|
|
["故障用例", "2 类", "6 类", 86],
|
|
["修复阶段", "3 阶段", "5 阶段", 92],
|
|
["离线可用性", "空白", "可演示", 100],
|
|
],
|
|
userValidation: "断开服务后仍可理解自修复能力,并可继续浏览默认案例。",
|
|
adminValidation: "默认数据回退、流程状态与接口异常路径均已验证。",
|
|
},
|
|
{
|
|
version: "v0.3",
|
|
date: "2026.08.10",
|
|
status: "能力升级",
|
|
title: "外设与车端统一编排",
|
|
summary: "将玩具动作、车端设备动作与数字孪生放进同一个控制流程,减少跨区域理解和操作成本。",
|
|
changes: [
|
|
["动作融合", "玩具动作与车端设备动作进入统一动作界面。"],
|
|
["布局优化", "孪生界面位于左侧,动作界面固定在右侧。"],
|
|
["映射精简", "移除重复外框,让实时映射关系更直接。"],
|
|
],
|
|
modules: ["自扩展", "外设学习", "整车控制"],
|
|
metrics: [
|
|
["动作入口", "2 处", "1 处", 100],
|
|
["映射层级", "3 层", "2 层", 88],
|
|
["协同对象", "玩具", "玩具 + 车端", 94],
|
|
],
|
|
userValidation: "动作与孪生左右分区清晰,学习结果可以直接用于统一控制。",
|
|
adminValidation: "外设连接状态、动作映射与车端执行链路保持独立可追踪。",
|
|
},
|
|
{
|
|
version: "v0.2",
|
|
date: "2026.08.09",
|
|
status: "体系建立",
|
|
title: "多权限主题体系",
|
|
summary: "建立 user 与 admin 的视觉身份,使权限差异不只体现在可用功能,也能被用户直观感知。",
|
|
changes: [
|
|
["角色主题", "user 使用绿色,admin 使用蓝色,形成稳定的权限认知。"],
|
|
["能力边界", "按角色控制模块入口,同时保留清晰的切换反馈。"],
|
|
["模型联动", "数字孪生模型与界面主题保持同色语义。"],
|
|
],
|
|
modules: ["权限体系", "主题系统", "数字孪生"],
|
|
metrics: [
|
|
["角色主题", "1 套", "2 套", 100],
|
|
["权限反馈", "文字", "视觉 + 文字", 90],
|
|
["主题联动", "界面", "界面 + 模型", 96],
|
|
],
|
|
userValidation: "绿色主题贯穿 user 可用能力,角色身份更加明确。",
|
|
adminValidation: "角色切换事件可同步驱动界面变量与模型渲染参数。",
|
|
},
|
|
{
|
|
version: "v0.1",
|
|
date: "2026.08.08",
|
|
status: "起点版本",
|
|
title: "整车认知控制台诞生",
|
|
summary: "完成从自然语言意图到整车状态回读的第一条闭环,建立后续能力持续进化的共同底座。",
|
|
changes: [
|
|
["自然语言控制", "用贴近日常表达的指令控制座舱功能。"],
|
|
["整车孪生", "把座椅、空调、车窗等状态集中映射到一个界面。"],
|
|
["执行回读", "串联 CAN 计划、设备执行与状态反馈。"],
|
|
],
|
|
modules: ["自编排", "数字孪生", "CAN 执行"],
|
|
metrics: [
|
|
["能力模块", "0 个", "1 个", 100],
|
|
["控制链路", "分散", "闭环", 92],
|
|
["状态视图", "单设备", "整车", 96],
|
|
],
|
|
userValidation: "自然语言、执行过程与结果反馈首次形成连续体验。",
|
|
adminValidation: "意图解析、CAN 编译计划与硬件回读具备完整追踪链路。",
|
|
},
|
|
];
|
|
|
|
const versionButtons = evolutionEntries.map((entry, index) => `
|
|
<button type="button" class="v2-evolution-version${index === 0 ? " active" : ""}"
|
|
data-evolution-index="${index}" aria-pressed="${index === 0 ? "true" : "false"}">
|
|
<span><strong>${entry.version}</strong><time datetime="${entry.date.replaceAll(".", "-")}">${entry.date.slice(5)}</time></span>
|
|
<b>${entry.title}</b>
|
|
<small>${entry.status}</small>
|
|
</button>`).join("");
|
|
|
|
const evolution = make("section", "v2-self-evolution", `
|
|
<div class="v2-evolution-shell">
|
|
<header class="v2-evolution-overview">
|
|
<div class="v2-evolution-heading v2-module-heading">
|
|
<span class="section-kicker">SELF-EVOLUTION</span>
|
|
<h1>能力进化日志</h1>
|
|
<p>记录每次问题发现、能力升级与验证结果,见证系统持续进化。</p>
|
|
</div>
|
|
<dl class="v2-evolution-summary" aria-label="当前进化概览">
|
|
<div class="v2-evolution-current"><dt>当前版本</dt><dd>v0.5</dd><small>能力融合期</small></div>
|
|
<div><dt>累计迭代</dt><dd>5</dd><small>个版本</small></div>
|
|
<div><dt>能力改进</dt><dd>12</dd><small>项记录</small></div>
|
|
<div><dt>最近更新</dt><dd>08.12</dd><small>2026</small></div>
|
|
</dl>
|
|
</header>
|
|
|
|
<div class="v2-evolution-workspace">
|
|
<aside class="v2-evolution-timeline" aria-label="版本进化时间轴">
|
|
<div class="v2-evolution-timeline-heading">
|
|
<span>EVOLUTION PATH</span>
|
|
<strong>从起点到现在</strong>
|
|
</div>
|
|
<div class="v2-evolution-version-list">${versionButtons}</div>
|
|
</aside>
|
|
|
|
<article class="v2-evolution-detail" id="v2EvolutionDetail" aria-live="polite">
|
|
<header class="v2-evolution-detail-heading">
|
|
<div>
|
|
<span class="v2-evolution-status" id="v2EvolutionStatus"></span>
|
|
<span class="v2-evolution-detail-version" id="v2EvolutionVersion"></span>
|
|
</div>
|
|
<time id="v2EvolutionDate"></time>
|
|
</header>
|
|
<h2 id="v2EvolutionTitle"></h2>
|
|
<p class="v2-evolution-lead" id="v2EvolutionLead"></p>
|
|
<div class="v2-evolution-detail-grid">
|
|
<section class="v2-evolution-changes" aria-labelledby="v2EvolutionChangesTitle">
|
|
<h3 id="v2EvolutionChangesTitle"><i data-lucide="git-commit-horizontal"></i> 本次进化</h3>
|
|
<div id="v2EvolutionChangeList"></div>
|
|
</section>
|
|
<section class="v2-evolution-evidence" aria-labelledby="v2EvolutionEvidenceTitle">
|
|
<h3 id="v2EvolutionEvidenceTitle"><i data-lucide="trending-up"></i> 变好的证据</h3>
|
|
<div id="v2EvolutionMetrics"></div>
|
|
</section>
|
|
</div>
|
|
<footer class="v2-evolution-validation">
|
|
<div class="v2-evolution-module-tags" id="v2EvolutionModules" aria-label="关联模块"></div>
|
|
<p><i data-lucide="badge-check"></i><span data-evolution-scope="user" id="v2EvolutionUserValidation"></span><span data-evolution-scope="admin" id="v2EvolutionAdminValidation"></span></p>
|
|
</footer>
|
|
</article>
|
|
</div>
|
|
|
|
<footer class="v2-evolution-next">
|
|
<span class="v2-evolution-next-icon"><i data-lucide="flag"></i></span>
|
|
<div><small>NEXT EVOLUTION · v0.6</small><strong>自主评估闭环</strong><p>让系统主动发现能力退化,对比历史基线,并形成下一轮优化建议。</p></div>
|
|
<div class="v2-evolution-next-steps" aria-label="下一版本计划"><span>自动复盘</span><i data-lucide="arrow-right"></i><span>对比基线</span><i data-lucide="arrow-right"></i><span>形成建议</span></div>
|
|
</footer>
|
|
</div>`);
|
|
evolution.id = "v2SelfEvolution";
|
|
evolution.setAttribute("aria-label", "自进化能力日志");
|
|
mainPanel.append(evolution);
|
|
|
|
const renderEntry = (index) => {
|
|
const entry = evolutionEntries[index];
|
|
if (!entry) return;
|
|
$("#v2EvolutionStatus", evolution).textContent = entry.status;
|
|
$("#v2EvolutionVersion", evolution).textContent = entry.version;
|
|
const date = $("#v2EvolutionDate", evolution);
|
|
date.textContent = entry.date;
|
|
date.dateTime = entry.date.replaceAll(".", "-");
|
|
$("#v2EvolutionTitle", evolution).textContent = entry.title;
|
|
$("#v2EvolutionLead", evolution).textContent = entry.summary;
|
|
$("#v2EvolutionChangeList", evolution).innerHTML = entry.changes.map(([label, copy]) => `
|
|
<div><i data-lucide="check"></i><p><strong>${label}</strong><span>${copy}</span></p></div>`).join("");
|
|
$("#v2EvolutionMetrics", evolution).innerHTML = entry.metrics.map(([label, before, after, progress]) => `
|
|
<div class="v2-evolution-metric">
|
|
<div><span>${label}</span><p><del>${before}</del><i data-lucide="arrow-right"></i><strong>${after}</strong></p></div>
|
|
<span class="v2-evolution-progress" style="--evolution-progress: ${progress}%"><i></i></span>
|
|
</div>`).join("");
|
|
$("#v2EvolutionModules", evolution).innerHTML = entry.modules
|
|
.map((module) => `<span>${module}</span>`).join("");
|
|
$("#v2EvolutionUserValidation", evolution).textContent = entry.userValidation;
|
|
$("#v2EvolutionAdminValidation", evolution).textContent = entry.adminValidation;
|
|
$$("[data-evolution-index]", evolution).forEach((button, buttonIndex) => {
|
|
const active = buttonIndex === index;
|
|
button.classList.toggle("active", active);
|
|
button.setAttribute("aria-pressed", String(active));
|
|
});
|
|
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
|
};
|
|
|
|
$(".v2-evolution-version-list", evolution)?.addEventListener("click", (event) => {
|
|
const button = event.target.closest("[data-evolution-index]");
|
|
if (button) renderEntry(Number(button.dataset.evolutionIndex));
|
|
});
|
|
renderEntry(0);
|
|
}
|
|
|
|
function buildOutput(mainPanel) {
|
|
const output = make("section", "v2-output-shell", `
|
|
<header class="v2-output-heading">
|
|
<div>
|
|
<span class="v2-page-index" id="v2PageIndex">00</span>
|
|
<div><small>INFORMATION OUTPUT</small><h2 id="v2PageTitle">整车联合数字孪生</h2></div>
|
|
</div>
|
|
<span class="v2-output-state"><i></i><span id="v2OutputState">LIVE STATE</span></span>
|
|
</header>
|
|
<div class="v2-output-content" id="v2OutputContent"></div>
|
|
<nav class="v2-page-rail" id="v2PageRail" aria-label="管理员信息分页"></nav>`);
|
|
mainPanel.append(output);
|
|
|
|
const content = $("#v2OutputContent");
|
|
const twin = $(".joint-twin-shell", mainPanel);
|
|
const resultWorkspace = $(".result-workspace", mainPanel);
|
|
if (twin) content.append(twin);
|
|
if (resultWorkspace) content.append(resultWorkspace);
|
|
|
|
pageDefinitions.forEach((page) => {
|
|
const target = $(page.target, content);
|
|
if (!target) return;
|
|
target.dataset.v2Page = String(page.index);
|
|
target.classList.add("v2-output-page");
|
|
});
|
|
|
|
pageDefinitions.forEach((page) => {
|
|
const button = make("button", "v2-page-button", `
|
|
<span>${String(page.index).padStart(2, "0")}</span>
|
|
<i></i>
|
|
<strong>${page.shortLabel}</strong>`);
|
|
button.type = "button";
|
|
button.dataset.v2PageTarget = String(page.index);
|
|
button.setAttribute("aria-label", `${String(page.index).padStart(2, "0")} ${page.label}`);
|
|
$("#v2PageRail").append(button);
|
|
});
|
|
}
|
|
|
|
function setPage(pageIndex) {
|
|
const maxPage = ui.role === "admin" ? pageDefinitions.length - 1 : 0;
|
|
ui.activePage = Math.max(0, Math.min(Number(pageIndex) || 0, maxPage));
|
|
const definition = pageDefinitions[ui.activePage];
|
|
$(".v2-output-shell")?.classList.toggle("v2-twin-active", definition.index === 0);
|
|
$$(".v2-output-page").forEach((page) => {
|
|
const active = Number(page.dataset.v2Page) === ui.activePage;
|
|
page.classList.toggle("v2-active-output", active);
|
|
page.setAttribute("aria-hidden", active ? "false" : "true");
|
|
});
|
|
$$(".v2-page-button").forEach((button) => {
|
|
const active = Number(button.dataset.v2PageTarget) === ui.activePage;
|
|
button.classList.toggle("active", active);
|
|
button.setAttribute("aria-current", active ? "page" : "false");
|
|
});
|
|
$("#v2PageIndex").textContent = String(definition.index).padStart(2, "0");
|
|
$("#v2PageTitle").textContent = definition.label;
|
|
$("#v2OutputState").textContent = definition.index === 0 ? "LIVE STATE" : "AUDIT VIEW";
|
|
requestAnimationFrame(updateScrollCues);
|
|
}
|
|
|
|
function updateScrollCues() {
|
|
$$(".result-workspace .tab-panel").forEach((panel) => {
|
|
const active = panel.classList.contains("v2-active-output");
|
|
panel.classList.toggle(
|
|
"v2-scrollable",
|
|
active && panel.scrollHeight > panel.clientHeight + 6,
|
|
);
|
|
});
|
|
}
|
|
|
|
function updateModeEdgeHints() {
|
|
const chips = $(".v2-mode-rail .mode-chips");
|
|
if (!chips) return;
|
|
const buttons = $$('button', chips);
|
|
buttons.forEach((button) => {
|
|
button.classList.remove("v2-edge-fade-left", "v2-edge-fade-right");
|
|
button.style.removeProperty("--v2-edge-fade-start");
|
|
button.style.removeProperty("--v2-edge-fade-end");
|
|
});
|
|
if (buttons.length < 2 || chips.clientWidth <= 0) return;
|
|
|
|
const viewport = chips.getBoundingClientRect();
|
|
const visible = buttons.map((button) => ({
|
|
button,
|
|
rect: button.getBoundingClientRect(),
|
|
})).filter(({ rect }) => {
|
|
return rect.right > viewport.left + 1 && rect.left < viewport.right - 1;
|
|
});
|
|
if (!visible.length) return;
|
|
|
|
const fadeSpan = 30;
|
|
const firstVisible = visible[0];
|
|
const lastVisible = visible[visible.length - 1];
|
|
if (firstVisible.rect.left < viewport.left - 1) {
|
|
const clippedAt = Math.min(100, Math.max(
|
|
0,
|
|
((viewport.left - firstVisible.rect.left) / firstVisible.rect.width) * 100,
|
|
));
|
|
firstVisible.button.style.setProperty("--v2-edge-fade-start", `${clippedAt}%`);
|
|
firstVisible.button.style.setProperty(
|
|
"--v2-edge-fade-end",
|
|
`${Math.min(100, clippedAt + fadeSpan)}%`,
|
|
);
|
|
firstVisible.button.classList.add("v2-edge-fade-left");
|
|
}
|
|
if (lastVisible.rect.right > viewport.right + 1) {
|
|
const clippedAt = Math.min(100, Math.max(
|
|
0,
|
|
((viewport.right - lastVisible.rect.left) / lastVisible.rect.width) * 100,
|
|
));
|
|
lastVisible.button.style.setProperty(
|
|
"--v2-edge-fade-start",
|
|
`${Math.max(0, clippedAt - fadeSpan)}%`,
|
|
);
|
|
lastVisible.button.style.setProperty("--v2-edge-fade-end", `${clippedAt}%`);
|
|
lastVisible.button.classList.add("v2-edge-fade-right");
|
|
}
|
|
}
|
|
|
|
function setControlMode(mode, { triggerGesture = true } = {}) {
|
|
ui.controlMode = mode === "gesture" ? "gesture" : "natural";
|
|
sessionStorage.setItem("cognitive-agent.control-mode", ui.controlMode);
|
|
document.body.dataset.controlMode = ui.controlMode;
|
|
const requestStatus = $(".v2-request-status");
|
|
const statusTarget = ui.controlMode === "gesture"
|
|
? $("#v2GestureExecutionSlot")
|
|
: $("#v2NaturalGrid");
|
|
if (requestStatus && statusTarget && requestStatus.parentElement !== statusTarget) {
|
|
statusTarget.append(requestStatus);
|
|
}
|
|
$$('[data-v2-control]').forEach((button) => {
|
|
const active = button.dataset.v2Control === ui.controlMode;
|
|
button.classList.toggle("active", active);
|
|
button.setAttribute("aria-pressed", String(active));
|
|
});
|
|
requestAnimationFrame(updateModeEdgeHints);
|
|
const drawer = $("#gestureDrawer");
|
|
if (!triggerGesture) {
|
|
const gestureVisible = ui.controlMode === "gesture";
|
|
drawer?.setAttribute("aria-hidden", String(!gestureVisible));
|
|
$("#gestureToggleButton")?.setAttribute("aria-expanded", String(gestureVisible));
|
|
return;
|
|
}
|
|
if (ui.controlMode === "gesture") {
|
|
if (!drawer?.classList.contains("open")) $("#gestureToggleButton")?.click();
|
|
} else if (drawer?.classList.contains("open")) {
|
|
$("#gestureCloseButton")?.click();
|
|
}
|
|
}
|
|
|
|
function setModule(module, { persist = true } = {}) {
|
|
if (["self-calibration", "self-repair"].includes(module) && ui.role !== "admin") {
|
|
setRole("admin", { allowModuleTransition: true });
|
|
}
|
|
const allowed = new Set(["self-orchestration", "self-extension", "self-evolution"]);
|
|
if (ui.role === "admin") {
|
|
allowed.add("self-calibration");
|
|
allowed.add("self-repair");
|
|
}
|
|
ui.activeModule = allowed.has(module) ? module : "self-orchestration";
|
|
if (ui.activeModule === "self-extension" && ui.role !== "user") {
|
|
setRole("user");
|
|
}
|
|
if (persist) {
|
|
sessionStorage.setItem("cognitive-agent.active-module", ui.activeModule);
|
|
}
|
|
document.body.dataset.module = ui.activeModule;
|
|
const roleSelect = $("#v2RoleSelect");
|
|
if (roleSelect) {
|
|
roleSelect.disabled = ui.activeModule === "self-repair";
|
|
dropdownControls.get(roleSelect)?.sync();
|
|
}
|
|
$$('.v2-module-tab').forEach((button) => {
|
|
const active = button.dataset.module === ui.activeModule;
|
|
button.classList.toggle("active", active);
|
|
button.setAttribute("aria-selected", String(active));
|
|
button.tabIndex = active ? 0 : -1;
|
|
});
|
|
if (["self-extension", "self-calibration", "self-repair", "self-evolution"].includes(ui.activeModule)) {
|
|
const drawer = $("#gestureDrawer");
|
|
if (drawer?.classList.contains("open")) $("#gestureCloseButton")?.click();
|
|
} else {
|
|
setControlMode(ui.controlMode, { triggerGesture: false });
|
|
}
|
|
window.dispatchEvent(new CustomEvent("cognitive-agent:module-change", {
|
|
detail: { module: ui.activeModule },
|
|
}));
|
|
window.setTimeout(() => window.dispatchEvent(new Event("resize")), 120);
|
|
}
|
|
|
|
function setRole(role, { allowModuleTransition = false } = {}) {
|
|
if (ui.activeModule === "self-extension" && role !== "user" && !allowModuleTransition) {
|
|
role = "user";
|
|
}
|
|
if (ui.activeModule === "self-repair" && role !== "admin") {
|
|
const roleSelect = $("#v2RoleSelect");
|
|
if (roleSelect) {
|
|
roleSelect.value = "admin";
|
|
dropdownControls.get(roleSelect)?.sync();
|
|
}
|
|
return;
|
|
}
|
|
ui.role = role === "admin" ? "admin" : "user";
|
|
sessionStorage.setItem("cognitive-agent.demo-role", ui.role);
|
|
document.body.dataset.demoRole = ui.role;
|
|
window.dispatchEvent(new CustomEvent("cognitive-agent:demo-role-change", {
|
|
detail: { role: ui.role },
|
|
}));
|
|
$("#v2RoleSelect").value = ui.role;
|
|
dropdownControls.get($("#v2RoleSelect"))?.sync();
|
|
$("#v2PageRail").hidden = ui.role !== "admin";
|
|
const calibrationTab = $('.v2-module-tab[data-module="self-calibration"]');
|
|
if (calibrationTab) {
|
|
calibrationTab.disabled = false;
|
|
calibrationTab.setAttribute("aria-disabled", "false");
|
|
calibrationTab.title = ui.role === "admin"
|
|
? ""
|
|
: "进入自标定时将自动切换为 admin";
|
|
}
|
|
const repairTab = $('.v2-module-tab[data-module="self-repair"]');
|
|
if (repairTab) {
|
|
repairTab.hidden = false;
|
|
repairTab.disabled = false;
|
|
repairTab.setAttribute("aria-disabled", "false");
|
|
repairTab.title = ui.role === "admin"
|
|
? ""
|
|
: "进入自修复时将自动切换为 admin";
|
|
}
|
|
if (ui.role !== "admin") {
|
|
if (["self-calibration", "self-repair"].includes(ui.activeModule)) {
|
|
setModule("self-orchestration");
|
|
}
|
|
setPage(0);
|
|
requestUserSafeExecutionMode();
|
|
} else {
|
|
const note = $("#v2ExecutionNote");
|
|
if (note) note.textContent = "管理员可切换虚拟 CAN、模拟台架与真实台架";
|
|
const runButton = $("#runButton");
|
|
if (runButton && !runButton.classList.contains("loading")) runButton.disabled = false;
|
|
syncExecutionSelector();
|
|
}
|
|
}
|
|
|
|
function setSidebarCollapsed(collapsed) {
|
|
ui.sidebarCollapsed = Boolean(collapsed);
|
|
sessionStorage.setItem(
|
|
"cognitive-agent.sidebar-collapsed",
|
|
String(ui.sidebarCollapsed),
|
|
);
|
|
document.body.classList.toggle("v2-sidebar-collapsed", ui.sidebarCollapsed);
|
|
const button = $("#v2SidebarToggle");
|
|
if (button) {
|
|
button.setAttribute("aria-expanded", String(!ui.sidebarCollapsed));
|
|
button.setAttribute(
|
|
"aria-label",
|
|
ui.sidebarCollapsed ? "展开左侧控制台" : "折叠左侧控制台",
|
|
);
|
|
button.innerHTML = `<i data-lucide="${ui.sidebarCollapsed ? "panel-left-open" : "panel-left-close"}"></i>`;
|
|
}
|
|
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
|
window.setTimeout(() => window.dispatchEvent(new Event("resize")), 180);
|
|
}
|
|
|
|
function requestUserSafeExecutionMode() {
|
|
const dryRunButton = $('[data-execution-mode="dry_run"]');
|
|
const note = $("#v2ExecutionNote");
|
|
const userTarget = $(".v2-user-target-status");
|
|
const userTargetText = $("#v2UserTargetText");
|
|
const runButton = $("#runButton");
|
|
if (!dryRunButton) return;
|
|
if (!dryRunButton.classList.contains("active")) dryRunButton.click();
|
|
if (note) note.textContent = "测试用户使用虚拟 CAN;真实台架仅由管理员显式启用";
|
|
if (userTarget) userTarget.dataset.state = "ready";
|
|
if (userTargetText) userTargetText.textContent = "执行目标:虚拟 CAN";
|
|
if (runButton && !runButton.classList.contains("loading")) runButton.disabled = false;
|
|
syncExecutionSelector();
|
|
}
|
|
|
|
function syncExecutionSelector() {
|
|
const select = $("#v2ExecutionSelect");
|
|
if (!select) return;
|
|
const active = $('[data-execution-mode].active');
|
|
if (active) select.value = active.dataset.executionMode;
|
|
["dry_run", "simulated_hardware", "hardware"].forEach((value) => {
|
|
const option = $(`option[value="${value}"]`, select);
|
|
const button = $(`[data-execution-mode="${value}"]`);
|
|
if (option && button) option.disabled = button.disabled;
|
|
});
|
|
dropdownControls.get(select)?.sync();
|
|
}
|
|
|
|
function syncRuntimePresentation() {
|
|
const modelBadge = $("#modelBadge");
|
|
const connectionBadge = $("#connectionBadge");
|
|
const modelValue = $("#v2ModelValue");
|
|
const modelNote = $("#v2ModelNote");
|
|
const connectionText = $("#v2ConnectionText");
|
|
if (modelBadge && modelValue) {
|
|
const text = modelBadge.textContent.trim() || "模型检测中";
|
|
modelValue.textContent = text;
|
|
const unavailable = modelBadge.classList.contains("danger");
|
|
$("#v2ModelField")?.classList.toggle("unavailable", unavailable);
|
|
$("#v2ModelState").textContent = unavailable ? "LOCAL" : "READY";
|
|
if (modelNote) modelNote.textContent = unavailable
|
|
? "GPT 不可用,自动使用本地规则"
|
|
: "服务端安全托管 · 失败自动降级";
|
|
}
|
|
if (connectionBadge && connectionText) {
|
|
connectionText.textContent = connectionBadge.textContent.trim() || "正在连接服务";
|
|
const online = !connectionBadge.classList.contains("danger")
|
|
&& !connectionBadge.classList.contains("warning");
|
|
$(".v2-sidebar-footer")?.classList.toggle("offline", !online);
|
|
}
|
|
syncExecutionSelector();
|
|
if (ui.role === "user") requestUserSafeExecutionMode();
|
|
}
|
|
|
|
function setVehicleLinkState(connected) {
|
|
const card = $("#v2VehicleCard");
|
|
const state = $("#v2VehicleState");
|
|
if (!card || !state) return;
|
|
card.dataset.state = connected ? "online" : "offline";
|
|
state.textContent = connected ? "READY" : "OFFLINE";
|
|
}
|
|
|
|
async function refreshVehicleLinkState() {
|
|
const card = $("#v2VehicleCard");
|
|
const state = $("#v2VehicleState");
|
|
if (card) card.dataset.state = "checking";
|
|
if (state) state.textContent = "CHECK";
|
|
try {
|
|
const response = await fetch("/api/extension/health", {
|
|
credentials: "same-origin",
|
|
headers: { Accept: "application/json" },
|
|
});
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const health = await response.json();
|
|
setVehicleLinkState(Boolean(health.vehicle?.connected));
|
|
} catch (_error) {
|
|
setVehicleLinkState(false);
|
|
}
|
|
}
|
|
|
|
function bindEvents() {
|
|
$("#v2RoleSelect")?.addEventListener("change", (event) => setRole(event.target.value));
|
|
$$('[data-v2-control]').forEach((button) => {
|
|
button.addEventListener("click", () => setControlMode(button.dataset.v2Control));
|
|
});
|
|
$$('.v2-module-tab').forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
if (!button.disabled) setModule(button.dataset.module);
|
|
});
|
|
});
|
|
$("#v2ExecutionSelect")?.addEventListener("change", (event) => {
|
|
$(`[data-execution-mode="${event.target.value}"]`)?.click();
|
|
syncExecutionSelector();
|
|
});
|
|
$$(".v2-page-button").forEach((button) => {
|
|
button.addEventListener("click", () => setPage(button.dataset.v2PageTarget));
|
|
});
|
|
$("#v2LogoutButton")?.addEventListener("click", () => $("#logoutButton")?.click());
|
|
window.addEventListener("cognitive-agent:connection", (event) => {
|
|
if (event.detail?.status === "online") refreshVehicleLinkState();
|
|
else if (["offline", "degraded"].includes(event.detail?.status)) setVehicleLinkState(false);
|
|
});
|
|
$("#v2SidebarToggle")?.addEventListener(
|
|
"click",
|
|
() => setSidebarCollapsed(!ui.sidebarCollapsed),
|
|
);
|
|
document.addEventListener("click", () => {
|
|
closeCustomDropdowns();
|
|
closeReasoningPicker();
|
|
});
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") {
|
|
closeCustomDropdowns();
|
|
closeReasoningPicker();
|
|
}
|
|
});
|
|
window.addEventListener("resize", updateScrollCues);
|
|
window.addEventListener("resize", updateModeEdgeHints);
|
|
window.addEventListener("cognitive-agent:result", () => requestAnimationFrame(updateScrollCues));
|
|
const modeChips = $(".v2-mode-rail .mode-chips");
|
|
modeChips?.addEventListener(
|
|
"scroll",
|
|
() => requestAnimationFrame(updateModeEdgeHints),
|
|
{ passive: true },
|
|
);
|
|
if (modeChips) {
|
|
new MutationObserver(() => requestAnimationFrame(updateModeEdgeHints))
|
|
.observe(modeChips, { childList: true });
|
|
}
|
|
|
|
const observer = new MutationObserver(syncRuntimePresentation);
|
|
[$("#modelBadge"), $("#connectionBadge"), $("#hardwareMode"), $("#simulationMode")]
|
|
.filter(Boolean)
|
|
.forEach((node) => observer.observe(node, {
|
|
attributes: true,
|
|
childList: true,
|
|
subtree: true,
|
|
characterData: true,
|
|
}));
|
|
}
|
|
|
|
function init() {
|
|
const workspace = $(".workspace");
|
|
const commandPanel = $(".command-panel");
|
|
const mainPanel = $(".main-panel");
|
|
const originalForm = $("#requestForm");
|
|
if (!workspace || !commandPanel || !mainPanel || !originalForm) return;
|
|
|
|
document.body.classList.add("console-v2");
|
|
const panelHeading = $(".panel-heading", commandPanel);
|
|
const modeRail = $(".mode-rail", commandPanel);
|
|
const exampleGroup = $(".example-group", commandPanel);
|
|
const safetyStrip = $("#safetyStrip", commandPanel);
|
|
const requestHeader = $(".request-header", mainPanel);
|
|
const gestureDrawer = $("#gestureDrawer");
|
|
|
|
const detached = {
|
|
panelHeading,
|
|
modeRail,
|
|
requestHeader,
|
|
gestureDrawer,
|
|
originalForm,
|
|
exampleGroup,
|
|
safetyStrip,
|
|
};
|
|
Object.values(detached).filter(Boolean).forEach((node) => node.remove());
|
|
|
|
const legacyParking = make("div", "v2-legacy-parking v2-legacy-hidden", "");
|
|
[exampleGroup, safetyStrip].filter(Boolean).forEach((node) => legacyParking.append(node));
|
|
document.body.append(legacyParking);
|
|
|
|
const configStack = buildSidebar(commandPanel);
|
|
buildConfigFields(configStack, originalForm);
|
|
enhanceConfiguredDropdowns();
|
|
buildOperation(
|
|
mainPanel,
|
|
originalForm,
|
|
panelHeading,
|
|
modeRail,
|
|
requestHeader,
|
|
gestureDrawer,
|
|
);
|
|
buildModuleSwitcher(mainPanel);
|
|
buildEvolutionJournal(mainPanel);
|
|
|
|
$(".pipeline", mainPanel)?.classList.add("v2-legacy-hidden");
|
|
$(".topbar")?.classList.add("v2-legacy-hidden");
|
|
$(".gesture-backdrop")?.classList.add("v2-legacy-hidden");
|
|
buildOutput(mainPanel);
|
|
bindEvents();
|
|
setControlMode(ui.controlMode, { triggerGesture: false });
|
|
setModule(ui.activeModule, { persist: false });
|
|
setRole(ui.role);
|
|
setSidebarCollapsed(ui.sidebarCollapsed);
|
|
setPage(0);
|
|
syncRuntimePresentation();
|
|
refreshVehicleLinkState();
|
|
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
|
requestAnimationFrame(() => {
|
|
updateModeEdgeHints();
|
|
document.body.classList.add("v2-ready");
|
|
});
|
|
}
|
|
|
|
window.CognitiveAgentConsoleV2 = {
|
|
enhanceSelect,
|
|
getModule: () => ui.activeModule,
|
|
getRole: () => ui.role,
|
|
setModule,
|
|
};
|
|
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
}());
|