feat: integrate production cockpit AI assistant
This commit is contained in:
@@ -8,7 +8,7 @@ Web 控制台是车机端的远程自编排与同步显示界面。网页保留
|
||||
- AI助手默认展开;左侧控制台、车端设备状态、外设与扩展能力默认收起。四个板块统一通过页面右上角的图标按钮展开或收回,主工作区随面板状态自适应。
|
||||
- AI助手支持普通问答、工程能力核对和车控工作。能力问答读取运行时权威目录,高置信度冷热请求走确定性快速控制链,其他问题和复杂车控由 Codex App Server 流式处理;车控结果仍以自然语言和可审计的动作进度呈现。
|
||||
- 网页端可输入一句话指令或点击预设模式,车机端同样可发起任务;两端任务与结果统一同步。
|
||||
- AI助手语音入口支持双模式:手动点击麦克风按钮会立即以 `manual` 模式录音,不要求唤醒词;浏览器已授权麦克风、助手展开且页面可见时自动进入 `wake` 待机,只有听到“你好,双十一”(兼容无标点与“双11”)后才展示并继续转录。AudioWorklet 采集后,服务端 SenseVoiceSmall 以节流预览返回未语义清洗的实时文字;静音端点立即停止采音,再由 Silero VAD、幂等 TranscriptAssembler 和理解层生成最终 `assistant_text`。普通问答、未知能力、否定和撤销表达均可回填,`commands` 仅作为受信任车控旁路,不能反向过滤助手输入。
|
||||
- AI助手语音入口支持双模式:手动点击麦克风按钮会立即以 `manual` 模式录音,不要求唤醒词;浏览器已授权麦克风、助手展开且页面可见时自动进入 `wake` 待机,只有听到“你好,小迪”(兼容无标点与字间停顿)后才展示并继续转录。AudioWorklet 采集后,服务端 SenseVoiceSmall 以节流预览返回未语义清洗的实时文字;静音端点立即停止采音,再由 Silero VAD、幂等 TranscriptAssembler 和理解层生成最终 `assistant_text`。普通问答、未知能力、否定和撤销表达均可回填,`commands` 仅作为受信任车控旁路,不能反向过滤助手输入。
|
||||
- 整车数字孪生位于工作区中央,点击或悬停热点可查看车窗、遮阳帘、主/副驾座椅、阅读灯、氛围灯、前大灯和空调的完整状态。
|
||||
- 车端设备状态与外设能力板块均可独立隐藏或恢复,面板状态在当前浏览器会话中保留。
|
||||
- USB 外设接入后同步显示 GSCU 链路、外设名称、类型、已学习能力和最近一次跨设备联动。
|
||||
@@ -22,7 +22,7 @@ UI 只展示可审计的“决策摘要”和“处理轨迹”,不展示模
|
||||
|
||||
- `GET /api/assistant/status`:返回 App Server 状态、可用性和当前会话数。
|
||||
- `GET /api/assistant/capabilities`:返回由运行时注册表、模式目录、CAN 绑定和已学习外设共同生成的权威能力快照。`service_surfaces` 只提供语音、手势、NFC、自标定和自修复的状态信息,不代表助手可调用;执行资格以 `assistant_callable` 和服务端校验为准。
|
||||
- `POST /api/assistant/stream`:提交 `message`、`conversation_id`、执行模式和当前车型等上下文,返回 `application/x-ndjson`。前端消费 `message.accepted`、`work.*`、`action.*`、`answer.delta`、`answer.completed` 与 `error` 事件,以增量方式展示回复和控制进度。
|
||||
- `POST /api/assistant/stream`:提交 `message`、`conversation_id`、执行模式和当前车型等上下文,返回 `application/x-ndjson`。前端消费 `message.accepted`、`work.*`、`action.*`、`answer.delta`、`control.suggestions`、`answer.completed` 与 `error` 事件,以增量方式展示回复和控制进度。不支持的车控请求可随 `control.suggestions` 返回当前车型可执行的替代指令;前端去重后渲染为按钮卡片,点击会作为新指令沿同一受控链路提交。
|
||||
|
||||
`POST /api/internal/codex/vehicle-control` 是 App Server MCP 子进程专用的回环接口,要求一次性内部令牌且与原始用户文本绑定,不是浏览器或第三方集成接口。浏览器也不能通过响应内容扩大执行目标、切换执行模式或提交原始 CAN。
|
||||
|
||||
|
||||
+337
-20
@@ -4779,10 +4779,17 @@ function modeSecondsLabel(durationMs) {
|
||||
}
|
||||
|
||||
const MODE_GALLERY_GROUPS = [
|
||||
{
|
||||
id: "custom",
|
||||
title: "自定义模式",
|
||||
description: "由用户创建或调整,可继续编辑与删除",
|
||||
icon: "wand-sparkles",
|
||||
preferredIds: [],
|
||||
},
|
||||
{
|
||||
id: "static",
|
||||
title: "静态模式",
|
||||
description: "一次执行并保持目标状态,包含预设与自定义模式",
|
||||
description: "系统预设,一次执行并保持目标状态",
|
||||
icon: "layout-grid",
|
||||
preferredIds: [
|
||||
"zero_gravity",
|
||||
@@ -4820,6 +4827,126 @@ const MODE_DOMAIN_LABELS = {
|
||||
vehicle_status: "车辆状态",
|
||||
};
|
||||
|
||||
const MODE_TARGET_LABELS = {
|
||||
driver: "主驾",
|
||||
passenger: "副驾",
|
||||
front_left: "左前",
|
||||
front_right: "右前 / 副驾",
|
||||
rear_left: "左后",
|
||||
rear_right: "右后",
|
||||
front_row: "前排",
|
||||
rear_row: "后排",
|
||||
vehicle: "全车",
|
||||
cabin: "全舱",
|
||||
all: "全部",
|
||||
};
|
||||
|
||||
const MODE_TIMELINE_TARGET_LABELS = {
|
||||
longitudinal: "前后位置",
|
||||
cushion_tilt: "坐垫角度",
|
||||
backrest_recline: "靠背角度",
|
||||
legrest_height: "腿托高度",
|
||||
legrest_extension: "腿托伸缩",
|
||||
};
|
||||
|
||||
function modeOperationParameters(operation) {
|
||||
const parameters = operation?.parameters;
|
||||
if (Array.isArray(parameters)) {
|
||||
return parameters
|
||||
.filter((item) => item && typeof item === "object" && item.name)
|
||||
.map((item) => [String(item.name), item.value]);
|
||||
}
|
||||
if (!parameters || typeof parameters !== "object") return [];
|
||||
return Object.entries(parameters);
|
||||
}
|
||||
|
||||
function formatModeOperationValue(name, value) {
|
||||
const valueLabels = {
|
||||
on: "开启",
|
||||
off: "关闭",
|
||||
open: "打开",
|
||||
close: "关闭",
|
||||
stop: "停止",
|
||||
internal: "内循环",
|
||||
external: "外循环",
|
||||
};
|
||||
if (typeof value === "string" && valueLabels[value]) return valueLabels[value];
|
||||
return formatConfigValue(name, value);
|
||||
}
|
||||
|
||||
function modeOperationView(operation) {
|
||||
const domain = String(operation?.domain || "");
|
||||
const capability = String(operation?.capability || operation?.operation || "执行动作");
|
||||
const scope = String(
|
||||
operation?.target_scope
|
||||
|| operation?.target?.instance
|
||||
|| operation?.target?.zone
|
||||
|| operation?.target
|
||||
|| "vehicle",
|
||||
);
|
||||
const parameters = modeOperationParameters(operation)
|
||||
.map(([name, value]) => {
|
||||
const label = CONFIG_PARAMETER_LABELS[name] || name;
|
||||
return `${label}:${formatModeOperationValue(name, value)}`;
|
||||
})
|
||||
.join(",");
|
||||
return {
|
||||
device: MODE_DOMAIN_LABELS[domain] || domain || "车端设备",
|
||||
target: MODE_TARGET_LABELS[scope] || scope || "全车",
|
||||
action: CONFIG_CAPABILITY_LABELS[capability] || capability,
|
||||
parameters: parameters || "无附加参数",
|
||||
};
|
||||
}
|
||||
|
||||
function modeTimelineTargetSummary(targets) {
|
||||
if (!targets || typeof targets !== "object") return "按模式目标运行";
|
||||
return Object.entries(targets).map(([motor, target]) => {
|
||||
const label = MODE_TIMELINE_TARGET_LABELS[motor] || motor;
|
||||
if (!target || typeof target !== "object") return `${label}:${target}`;
|
||||
if (target.position_pct != null) return `${label}:${target.position_pct}%`;
|
||||
if (target.angle_deg != null) return `${label}:${target.angle_deg}°`;
|
||||
if (target.hall_pulse != null) return `${label}:${target.hall_pulse} Hall`;
|
||||
return `${label}:${Object.values(target).join(" / ")}`;
|
||||
}).join(",");
|
||||
}
|
||||
|
||||
function modeTimelineStepView(step, action) {
|
||||
const timeout = Number(step?.timeout_ms || 0);
|
||||
const duration = Number(step?.duration_ms || 0);
|
||||
const timing = duration > 0
|
||||
? `持续 ${modeSecondsLabel(duration)}s`
|
||||
: timeout > 0
|
||||
? `目标到位,保护上限 ${modeSecondsLabel(timeout)}s`
|
||||
: "目标到位";
|
||||
return {
|
||||
device: "座椅",
|
||||
target: "副驾",
|
||||
action,
|
||||
parameters: `${modeTimelineTargetSummary(step?.targets)};${timing}`,
|
||||
};
|
||||
}
|
||||
|
||||
function modeExecutionActionViews(mode) {
|
||||
const operations = Array.isArray(mode?.operations)
|
||||
? mode.operations.map(modeOperationView)
|
||||
: [];
|
||||
const timeline = mode?.timeline;
|
||||
if (!timeline || typeof timeline !== "object") return operations;
|
||||
const timelineSteps = [];
|
||||
if (timeline.entry?.targets) {
|
||||
timelineSteps.push(modeTimelineStepView(timeline.entry, "进入动态模式初始位置"));
|
||||
}
|
||||
if (Array.isArray(timeline.phases)) {
|
||||
timeline.phases.forEach((phase, index) => {
|
||||
timelineSteps.push(modeTimelineStepView(
|
||||
phase,
|
||||
`循环阶段 ${index + 1}:${phase?.name || phase?.id || "动态调节"}`,
|
||||
));
|
||||
});
|
||||
}
|
||||
return [...operations, ...timelineSteps];
|
||||
}
|
||||
|
||||
function isUserModeProfile(mode) {
|
||||
return mode?.origin === "user" || mode?.deletable === true;
|
||||
}
|
||||
@@ -4853,9 +4980,11 @@ function modeGalleryBuckets() {
|
||||
...definition,
|
||||
modes: orderModeGalleryProfiles(
|
||||
modes.filter((mode) => (
|
||||
definition.id === "dynamic"
|
||||
? isDynamicModeProfile(mode)
|
||||
: !isDynamicModeProfile(mode)
|
||||
definition.id === "custom"
|
||||
? isUserModeProfile(mode)
|
||||
: definition.id === "dynamic"
|
||||
? !isUserModeProfile(mode) && isDynamicModeProfile(mode)
|
||||
: !isUserModeProfile(mode) && !isDynamicModeProfile(mode)
|
||||
)),
|
||||
definition.preferredIds,
|
||||
),
|
||||
@@ -4978,14 +5107,20 @@ function createModeGalleryEmpty(group) {
|
||||
empty.dataset.modeGroupEmpty = group.id;
|
||||
empty.setAttribute("role", "status");
|
||||
const icon = document.createElement("i");
|
||||
icon.dataset.lucide = group.id === "dynamic" ? "activity" : "layout-grid";
|
||||
icon.dataset.lucide = group.id === "custom"
|
||||
? "wand-sparkles"
|
||||
: group.id === "dynamic"
|
||||
? "activity"
|
||||
: "layout-grid";
|
||||
const copy = document.createElement("span");
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = `暂无${group.title}`;
|
||||
const description = document.createElement("small");
|
||||
description.textContent = group.id === "dynamic"
|
||||
? "支持持续变化的模式会显示在这里"
|
||||
: "可通过 AI助手创建新的静态模式";
|
||||
description.textContent = group.id === "custom"
|
||||
? "可通过 AI助手创建新的功能模式"
|
||||
: group.id === "dynamic"
|
||||
? "支持持续变化的系统模式会显示在这里"
|
||||
: "系统静态预设会显示在这里";
|
||||
copy.append(title, description);
|
||||
empty.append(icon, copy);
|
||||
return empty;
|
||||
@@ -5020,7 +5155,7 @@ function populateModeGallery() {
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "mode-gallery-grid";
|
||||
if (group.id === "static") grid.append(createModeGalleryAddCard());
|
||||
if (group.id === "custom") grid.append(createModeGalleryAddCard());
|
||||
group.modes.forEach((mode) => grid.append(createModeGalleryCard(mode)));
|
||||
if (!group.modes.length) grid.append(createModeGalleryEmpty(group));
|
||||
section.append(grid);
|
||||
@@ -5042,6 +5177,7 @@ function setModeRailExpanded(expanded) {
|
||||
toggle.title = open ? "收起功能模式" : "展开功能模式";
|
||||
content.hidden = !open;
|
||||
$(".mode-rail")?.classList.toggle("is-collapsed", !open);
|
||||
if (!open) hideModeActionPopover({ immediate: true });
|
||||
const icon = $("i", toggle);
|
||||
if (icon) icon.dataset.lucide = open ? "chevron-up" : "chevron-down";
|
||||
refreshIcons();
|
||||
@@ -5816,6 +5952,168 @@ function installModeChipScroller() {
|
||||
window.requestAnimationFrame(updateScrollState);
|
||||
}
|
||||
|
||||
let modeActionPopoverHideTimer = null;
|
||||
let modeActionPopoverAnchor = null;
|
||||
|
||||
function ensureModeActionPopover() {
|
||||
let popover = $("#modeChipActionPopover");
|
||||
if (popover) return popover;
|
||||
popover = document.createElement("aside");
|
||||
popover.id = "modeChipActionPopover";
|
||||
popover.className = "mode-chip-action-popover";
|
||||
popover.setAttribute("role", "tooltip");
|
||||
popover.hidden = true;
|
||||
|
||||
const header = document.createElement("header");
|
||||
const headingCopy = document.createElement("span");
|
||||
const eyebrow = document.createElement("small");
|
||||
eyebrow.textContent = "执行动作预览";
|
||||
const title = document.createElement("strong");
|
||||
title.dataset.modeActionTitle = "true";
|
||||
headingCopy.append(eyebrow, title);
|
||||
const count = document.createElement("b");
|
||||
count.dataset.modeActionCount = "true";
|
||||
header.append(headingCopy, count);
|
||||
|
||||
const list = document.createElement("ol");
|
||||
list.dataset.modeActionList = "true";
|
||||
popover.append(header, list);
|
||||
document.body.append(popover);
|
||||
return popover;
|
||||
}
|
||||
|
||||
function renderModeActionPopover(mode) {
|
||||
const popover = ensureModeActionPopover();
|
||||
const actions = modeExecutionActionViews(mode);
|
||||
$("[data-mode-action-title]", popover).textContent = mode?.name || mode?.id || "功能模式";
|
||||
$("[data-mode-action-count]", popover).textContent = actions.length
|
||||
? `${actions.length} 步`
|
||||
: `${Number(mode?.operation_count || 0)} 项`;
|
||||
const list = $("[data-mode-action-list]", popover);
|
||||
list.replaceChildren();
|
||||
if (!actions.length) {
|
||||
const empty = document.createElement("li");
|
||||
empty.className = "mode-chip-action-empty";
|
||||
empty.textContent = "执行动作明细暂未加载,请刷新后重试。";
|
||||
list.append(empty);
|
||||
return popover;
|
||||
}
|
||||
actions.forEach((action, index) => {
|
||||
const item = document.createElement("li");
|
||||
const step = document.createElement("span");
|
||||
step.className = "mode-chip-action-step";
|
||||
step.textContent = String(index + 1).padStart(2, "0");
|
||||
const copy = document.createElement("span");
|
||||
copy.className = "mode-chip-action-copy";
|
||||
const actionName = document.createElement("strong");
|
||||
actionName.textContent = action.action;
|
||||
const metadata = document.createElement("small");
|
||||
const device = document.createElement("span");
|
||||
device.innerHTML = "<b>设备</b>";
|
||||
device.append(document.createTextNode(action.device));
|
||||
const target = document.createElement("span");
|
||||
target.innerHTML = "<b>目标</b>";
|
||||
target.append(document.createTextNode(action.target));
|
||||
const parameters = document.createElement("span");
|
||||
parameters.innerHTML = "<b>参数</b>";
|
||||
parameters.append(document.createTextNode(action.parameters));
|
||||
metadata.append(device, target, parameters);
|
||||
copy.append(actionName, metadata);
|
||||
item.append(step, copy);
|
||||
list.append(item);
|
||||
});
|
||||
return popover;
|
||||
}
|
||||
|
||||
function positionModeActionPopover(anchor, popover) {
|
||||
const margin = 12;
|
||||
const gap = 8;
|
||||
const anchorRect = anchor.getBoundingClientRect();
|
||||
const popoverRect = popover.getBoundingClientRect();
|
||||
const maximumLeft = Math.max(margin, window.innerWidth - popoverRect.width - margin);
|
||||
const left = Math.max(
|
||||
margin,
|
||||
Math.min(maximumLeft, anchorRect.left + (anchorRect.width - popoverRect.width) / 2),
|
||||
);
|
||||
const availableBelow = window.innerHeight - anchorRect.bottom - margin;
|
||||
const availableAbove = anchorRect.top - margin;
|
||||
const placeAbove = availableBelow < Math.min(popoverRect.height, 320)
|
||||
&& availableAbove > availableBelow;
|
||||
const desiredTop = placeAbove
|
||||
? anchorRect.top - popoverRect.height - gap
|
||||
: anchorRect.bottom + gap;
|
||||
const maximumTop = Math.max(margin, window.innerHeight - popoverRect.height - margin);
|
||||
popover.style.left = `${Math.max(margin, Math.min(maximumLeft, left))}px`;
|
||||
popover.style.top = `${Math.max(margin, Math.min(maximumTop, desiredTop))}px`;
|
||||
popover.classList.toggle("is-above", placeAbove);
|
||||
}
|
||||
|
||||
function showModeActionPopover(button) {
|
||||
const mode = modeProfiles().find((item) => item.id === button?.dataset.modeProfileId);
|
||||
if (!mode) return;
|
||||
window.clearTimeout(modeActionPopoverHideTimer);
|
||||
modeActionPopoverHideTimer = null;
|
||||
const popover = renderModeActionPopover(mode);
|
||||
modeActionPopoverAnchor = button;
|
||||
button.setAttribute("aria-describedby", popover.id);
|
||||
popover.hidden = false;
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!popover.hidden && modeActionPopoverAnchor === button) {
|
||||
positionModeActionPopover(button, popover);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function hideModeActionPopover({ immediate = false } = {}) {
|
||||
window.clearTimeout(modeActionPopoverHideTimer);
|
||||
const hide = () => {
|
||||
const popover = $("#modeChipActionPopover");
|
||||
if (popover) popover.hidden = true;
|
||||
modeActionPopoverAnchor = null;
|
||||
modeActionPopoverHideTimer = null;
|
||||
};
|
||||
if (immediate) hide();
|
||||
else modeActionPopoverHideTimer = window.setTimeout(hide, 90);
|
||||
}
|
||||
|
||||
function installModeActionPreview() {
|
||||
const group = $(".mode-chips");
|
||||
if (!group || group.dataset.modeActionPreviewBound === "true") return;
|
||||
group.dataset.modeActionPreviewBound = "true";
|
||||
const resolveButton = (event) => {
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
return target?.closest("button[data-mode-profile-id]");
|
||||
};
|
||||
group.addEventListener("pointerover", (event) => {
|
||||
const button = resolveButton(event);
|
||||
if (button && !button.contains(event.relatedTarget)) showModeActionPopover(button);
|
||||
});
|
||||
group.addEventListener("pointerout", (event) => {
|
||||
const button = resolveButton(event);
|
||||
if (button && !button.contains(event.relatedTarget)) hideModeActionPopover();
|
||||
});
|
||||
group.addEventListener("focusin", (event) => {
|
||||
const button = resolveButton(event);
|
||||
if (button) showModeActionPopover(button);
|
||||
});
|
||||
group.addEventListener("focusout", (event) => {
|
||||
const button = resolveButton(event);
|
||||
if (button && !button.contains(event.relatedTarget)) hideModeActionPopover();
|
||||
});
|
||||
group.addEventListener("scroll", () => hideModeActionPopover({ immediate: true }), { passive: true });
|
||||
group.addEventListener("pointerdown", () => hideModeActionPopover({ immediate: true }));
|
||||
group.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") hideModeActionPopover({ immediate: true });
|
||||
});
|
||||
const popover = ensureModeActionPopover();
|
||||
popover.addEventListener("pointerenter", () => {
|
||||
window.clearTimeout(modeActionPopoverHideTimer);
|
||||
modeActionPopoverHideTimer = null;
|
||||
});
|
||||
popover.addEventListener("pointerleave", () => hideModeActionPopover());
|
||||
window.addEventListener("resize", () => hideModeActionPopover({ immediate: true }), { passive: true });
|
||||
}
|
||||
|
||||
function populateModeChips() {
|
||||
const group = $(".mode-chips");
|
||||
if (!group) return;
|
||||
@@ -5857,6 +6155,12 @@ function populateModeChips() {
|
||||
button.dataset.modeCommand = `进入${mode.name}`;
|
||||
button.dataset.modeOrigin = mode?.origin || "preset";
|
||||
button.setAttribute("aria-pressed", "false");
|
||||
const actionCount = modeExecutionActionViews(mode).length
|
||||
|| Number(mode.operation_count || 0);
|
||||
button.setAttribute(
|
||||
"aria-label",
|
||||
`${mode.name || mode.id},${actionCount} 项执行动作,点击执行`,
|
||||
);
|
||||
let summary = $(".mode-chip-summary", button);
|
||||
if (!summary) {
|
||||
summary = document.createElement("small");
|
||||
@@ -5905,7 +6209,9 @@ function populateModeChips() {
|
||||
: `${phase.name} 目标到位`;
|
||||
}).join(" → ")
|
||||
: "";
|
||||
button.title = [mode.description || mode.name, phaseSummary].filter(Boolean).join("\n");
|
||||
button.title = [mode.description || mode.name, phaseSummary, "悬浮或聚焦查看全部执行动作"]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
});
|
||||
|
||||
const preferredStaticOrder = [
|
||||
@@ -5915,12 +6221,20 @@ function populateModeChips() {
|
||||
"queen_passenger_driving",
|
||||
"leisure_lounger",
|
||||
];
|
||||
const dynamicModeOrder = ["rocking_horse", "dynamic_cradle"];
|
||||
const allButtons = buttons();
|
||||
const userButtons = allButtons.filter((button) => button.dataset.modeOrigin === "user");
|
||||
const dynamicButtons = dynamicModeOrder
|
||||
.map((modeId) => allButtons.find((button) => button.dataset.modeProfileId === modeId))
|
||||
.filter(Boolean);
|
||||
const profilesById = new Map(modes.map((mode) => [String(mode.id), mode]));
|
||||
const userButtons = allButtons.filter((button) => {
|
||||
const mode = profilesById.get(String(button.dataset.modeProfileId || ""));
|
||||
return mode ? isUserModeProfile(mode) : button.dataset.modeOrigin === "user";
|
||||
});
|
||||
const dynamicOrder = new Map([["rocking_horse", 0], ["dynamic_cradle", 1]]);
|
||||
const dynamicButtons = allButtons.filter((button) => {
|
||||
const mode = profilesById.get(String(button.dataset.modeProfileId || ""));
|
||||
return mode && !isUserModeProfile(mode) && isDynamicModeProfile(mode);
|
||||
}).sort((left, right) => (
|
||||
(dynamicOrder.get(left.dataset.modeProfileId) ?? Number.MAX_SAFE_INTEGER)
|
||||
- (dynamicOrder.get(right.dataset.modeProfileId) ?? Number.MAX_SAFE_INTEGER)
|
||||
));
|
||||
const staticButtons = allButtons.filter(
|
||||
(button) => !userButtons.includes(button) && !dynamicButtons.includes(button),
|
||||
);
|
||||
@@ -5946,15 +6260,17 @@ function populateModeChips() {
|
||||
group.append(divider);
|
||||
};
|
||||
if (userButtons.length) {
|
||||
appendGroupLabel("我的模式", "user");
|
||||
appendGroupLabel("自定义模式", "custom");
|
||||
userButtons.forEach((button) => group.append(button));
|
||||
}
|
||||
if (userButtons.length && staticButtons.length) appendDivider("user");
|
||||
if (staticButtons.length) appendGroupLabel("预设模式", "preset");
|
||||
if (userButtons.length && (staticButtons.length || dynamicButtons.length)) {
|
||||
appendDivider("custom");
|
||||
}
|
||||
if (staticButtons.length) appendGroupLabel("静态模式", "static");
|
||||
preferredStaticButtons.forEach((button) => group.append(button));
|
||||
otherStaticButtons.forEach((button) => group.append(button));
|
||||
if (dynamicButtons.length && staticButtons.length) {
|
||||
appendDivider("dynamic");
|
||||
if (dynamicButtons.length) {
|
||||
if (userButtons.length || staticButtons.length) appendDivider("dynamic");
|
||||
appendGroupLabel("动态模式", "dynamic");
|
||||
}
|
||||
dynamicButtons.forEach((button) => group.append(button));
|
||||
@@ -8423,6 +8739,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
selectVehicle(event.target.value);
|
||||
});
|
||||
installModeChipScroller();
|
||||
installModeActionPreview();
|
||||
$(".mode-chips").addEventListener("click", (event) => {
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
const button = target?.closest("button[data-mode-command]");
|
||||
|
||||
@@ -723,6 +723,140 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.assistant-control-suggestions {
|
||||
width: 100%;
|
||||
max-width: 314px;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
padding: 10px;
|
||||
background: color-mix(in oklch, var(--card) 92%, var(--secondary));
|
||||
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 24px color-mix(in oklch, var(--foreground) 6%, transparent);
|
||||
}
|
||||
|
||||
.assistant-message-control-suggestions {
|
||||
width: auto;
|
||||
max-width: none;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.assistant-message-control-suggestions .assistant-message-body {
|
||||
min-width: 0;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.assistant-control-suggestions-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
color: var(--assistant-muted, #64748b);
|
||||
font-size: 10px;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.assistant-control-suggestions-title svg {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
color: var(--primary, #275df5);
|
||||
}
|
||||
|
||||
.assistant-control-suggestion-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.assistant-control-suggestion {
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr) 16px;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 7px 9px;
|
||||
color: var(--assistant-text, #233044);
|
||||
text-align: left;
|
||||
background: var(--card, #fff);
|
||||
border: 1px solid var(--border, #dce3ed);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: border-color 150ms ease, background 150ms ease, box-shadow 150ms ease,
|
||||
transform 150ms ease, opacity 150ms ease;
|
||||
}
|
||||
|
||||
.assistant-control-suggestion:hover:not(:disabled) {
|
||||
background: color-mix(in oklch, var(--primary) 5%, var(--card));
|
||||
border-color: color-mix(in oklch, var(--primary) 38%, var(--border));
|
||||
box-shadow: 0 5px 14px color-mix(in oklch, var(--primary) 10%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.assistant-control-suggestion:focus-visible {
|
||||
border-color: var(--ring, #275df5);
|
||||
outline: 3px solid color-mix(in oklch, var(--ring, #275df5) 18%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.assistant-control-suggestion:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.assistant-control-suggestion.is-sending {
|
||||
color: var(--status-info-foreground, #1741ad);
|
||||
background: var(--status-info-soft, #edf2ff);
|
||||
border-color: color-mix(in oklch, var(--status-info, #275df5) 36%, var(--border));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.assistant-control-suggestion-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--primary, #275df5);
|
||||
background: color-mix(in oklch, var(--primary) 9%, transparent);
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.assistant-control-suggestion-icon svg,
|
||||
.assistant-control-suggestion-arrow {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.assistant-control-suggestion-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.assistant-control-suggestion-copy strong,
|
||||
.assistant-control-suggestion-copy small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.assistant-control-suggestion-copy strong {
|
||||
color: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
.assistant-control-suggestion-copy small {
|
||||
color: var(--assistant-muted, #64748b);
|
||||
font-size: 8px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.assistant-control-suggestion-arrow {
|
||||
color: var(--assistant-muted, #64748b);
|
||||
}
|
||||
|
||||
.assistant-thinking {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const MAX_STORED_MESSAGES = 24;
|
||||
const COMPOSER_MIN_HEIGHT = 64;
|
||||
const COMPOSER_MAX_HEIGHT = 116;
|
||||
const MAX_CONTROL_SUGGESTIONS = 4;
|
||||
const WELCOME_TEXT = "你好,我是 AI助手。你可以向我提问,也可以直接交代需要完成的工作。";
|
||||
let pendingController = null;
|
||||
let pendingMessage = null;
|
||||
@@ -375,6 +376,132 @@
|
||||
followLatest();
|
||||
}
|
||||
|
||||
function normalizeControlSuggestions(rawSuggestions) {
|
||||
if (!Array.isArray(rawSuggestions)) return [];
|
||||
const seenCommands = new Set();
|
||||
const suggestions = [];
|
||||
rawSuggestions.forEach((rawSuggestion, index) => {
|
||||
const source = typeof rawSuggestion === "string"
|
||||
? { command: rawSuggestion, label: rawSuggestion }
|
||||
: rawSuggestion;
|
||||
if (!source || typeof source !== "object") return;
|
||||
const command = String(source.command || "").trim().replace(/\s+/g, " ");
|
||||
if (!command || command.length > 160) return;
|
||||
const commandKey = command.toLocaleLowerCase("zh-CN").replace(/\s+/g, "");
|
||||
if (seenCommands.has(commandKey)) return;
|
||||
seenCommands.add(commandKey);
|
||||
suggestions.push({
|
||||
id: String(source.id || `suggestion-${index + 1}`).trim(),
|
||||
label: String(source.label || command).trim().slice(0, 64) || command,
|
||||
command,
|
||||
domain: String(source.domain || "").trim().toLowerCase(),
|
||||
});
|
||||
});
|
||||
return suggestions.slice(0, MAX_CONTROL_SUGGESTIONS);
|
||||
}
|
||||
|
||||
function controlSuggestionIcon(domain) {
|
||||
if (["climate", "hvac"].includes(domain)) return "wind";
|
||||
if (domain.includes("seat")) return "armchair";
|
||||
if (["roof", "shade", "sunshade", "window"].includes(domain)) return "panel-top";
|
||||
if (["light", "lighting", "ambient_light", "interior_light"].includes(domain)) {
|
||||
return "lightbulb";
|
||||
}
|
||||
return "circle-play";
|
||||
}
|
||||
|
||||
function setSuggestionGroupPending(group, activeButton, pending) {
|
||||
group.classList.toggle("is-sending", pending);
|
||||
group.setAttribute("aria-busy", String(pending));
|
||||
group.querySelectorAll("button").forEach((button) => {
|
||||
button.disabled = pending;
|
||||
const label = button.querySelector("strong");
|
||||
if (label && button === activeButton) {
|
||||
label.textContent = pending ? "正在发送…" : button.dataset.suggestionLabel;
|
||||
}
|
||||
button.classList.toggle("is-sending", pending && button === activeButton);
|
||||
button.setAttribute("aria-busy", String(pending && button === activeButton));
|
||||
});
|
||||
}
|
||||
|
||||
async function submitSuggestedCommand(command, group, button) {
|
||||
if (button.disabled) return;
|
||||
if (pendingController) {
|
||||
window.CognitiveAgentConsole?.notify?.("请等待当前回答完成后再执行其他指令");
|
||||
return;
|
||||
}
|
||||
setSuggestionGroupPending(group, button, true);
|
||||
try {
|
||||
await submitAssistant(command);
|
||||
} finally {
|
||||
setSuggestionGroupPending(group, button, false);
|
||||
}
|
||||
}
|
||||
|
||||
function appendControlSuggestions(message, rawSuggestions) {
|
||||
if (!message?.body) return;
|
||||
const suggestions = normalizeControlSuggestions(rawSuggestions);
|
||||
if (!suggestions.length) return;
|
||||
message.article?.classList.add("assistant-message-control-suggestions");
|
||||
const signature = JSON.stringify(suggestions.map(({ id, command }) => [id, command]));
|
||||
let group = $(".assistant-control-suggestions", message.body);
|
||||
if (group?.dataset.suggestionSignature === signature) return;
|
||||
if (!group) {
|
||||
group = document.createElement("section");
|
||||
group.className = "assistant-control-suggestions";
|
||||
group.setAttribute("role", "group");
|
||||
group.setAttribute("aria-label", "可以尝试的其他控制指令");
|
||||
message.body.append(group);
|
||||
}
|
||||
group.dataset.suggestionSignature = signature;
|
||||
group.replaceChildren();
|
||||
|
||||
const heading = document.createElement("p");
|
||||
heading.className = "assistant-control-suggestions-title";
|
||||
const headingIcon = document.createElement("i");
|
||||
headingIcon.setAttribute("data-lucide", "sparkles");
|
||||
const headingText = document.createElement("span");
|
||||
headingText.textContent = "你还可以试试";
|
||||
heading.append(headingIcon, headingText);
|
||||
group.append(heading);
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "assistant-control-suggestion-list";
|
||||
suggestions.forEach((suggestion) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "assistant-control-suggestion";
|
||||
button.dataset.suggestionId = suggestion.id;
|
||||
button.dataset.suggestionCommand = suggestion.command;
|
||||
button.dataset.suggestionLabel = suggestion.label;
|
||||
button.setAttribute("aria-label", `执行控制指令:${suggestion.label}`);
|
||||
button.setAttribute("aria-busy", "false");
|
||||
button.title = suggestion.command;
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "assistant-control-suggestion-icon";
|
||||
icon.innerHTML = `<i data-lucide="${controlSuggestionIcon(suggestion.domain)}"></i>`;
|
||||
const copy = document.createElement("span");
|
||||
copy.className = "assistant-control-suggestion-copy";
|
||||
const hint = document.createElement("small");
|
||||
hint.textContent = "点击直接执行";
|
||||
const label = document.createElement("strong");
|
||||
label.textContent = suggestion.label;
|
||||
copy.append(label, hint);
|
||||
const arrow = document.createElement("i");
|
||||
arrow.setAttribute("data-lucide", "arrow-up-right");
|
||||
arrow.className = "assistant-control-suggestion-arrow";
|
||||
button.append(icon, copy, arrow);
|
||||
button.addEventListener("click", () => {
|
||||
void submitSuggestedCommand(suggestion.command, group, button);
|
||||
});
|
||||
list.append(button);
|
||||
});
|
||||
group.append(list);
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
followLatest(true);
|
||||
}
|
||||
|
||||
function appendModeCreatedState(message, profile, { updated = false } = {}) {
|
||||
if (!message?.body || !profile?.id) return;
|
||||
const selector = `[data-assistant-mode-created="${CSS.escape(String(profile.id))}"]`;
|
||||
@@ -829,19 +956,52 @@
|
||||
);
|
||||
} else if (
|
||||
event.payload.mode_profile
|
||||
&& event.payload.orchestration_route === "mode_profile_create"
|
||||
&& ["mode_profile_create", "mode_profile_update"].includes(
|
||||
event.payload.orchestration_route,
|
||||
)
|
||||
) {
|
||||
clearActiveModeDraftContext();
|
||||
appendModeCreatedState(message, event.payload.mode_profile);
|
||||
if (
|
||||
event.payload.orchestration_route === "mode_profile_create"
|
||||
|| normalizeModeName(activeModeDraftContext?.name)
|
||||
=== normalizeModeName(event.payload.mode_profile.name)
|
||||
) {
|
||||
clearActiveModeDraftContext();
|
||||
}
|
||||
void window.CognitiveAgentConsole?.refreshModeProfiles?.(
|
||||
event.payload.mode_profile.id,
|
||||
);
|
||||
appendModeCreatedState(message, event.payload.mode_profile, {
|
||||
updated: event.payload.orchestration_route === "mode_profile_update",
|
||||
});
|
||||
} else if (
|
||||
event.payload.deleted_mode_profile
|
||||
&& event.payload.orchestration_route === "mode_profile_delete"
|
||||
) {
|
||||
if (
|
||||
normalizeModeName(activeModeDraftContext?.name)
|
||||
=== normalizeModeName(event.payload.deleted_mode_profile.name)
|
||||
) {
|
||||
clearActiveModeDraftContext();
|
||||
}
|
||||
void window.CognitiveAgentConsole?.refreshModeProfiles?.();
|
||||
}
|
||||
appendControlSuggestions(
|
||||
message,
|
||||
event.payload.suggestions || event.payload.recommended_commands,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.type === "control.suggestions") {
|
||||
appendControlSuggestions(message, event.suggestions || event.recommended_commands);
|
||||
return;
|
||||
}
|
||||
if (event.type === "answer.completed") {
|
||||
const authoritativeAnswer = Object.prototype.hasOwnProperty.call(event, "answer")
|
||||
? String(event.answer || "")
|
||||
: message.content;
|
||||
setMessageContent(message, authoritativeAnswer, { final: true });
|
||||
appendControlSuggestions(message, event.suggestions || event.recommended_commands);
|
||||
return;
|
||||
}
|
||||
if (event.type === "error") {
|
||||
@@ -913,8 +1073,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAssistant() {
|
||||
async function submitAssistant(messageOverride = null) {
|
||||
if (pendingController) {
|
||||
if (typeof messageOverride === "string") return;
|
||||
pendingController.abort("user_stop");
|
||||
return;
|
||||
}
|
||||
@@ -926,18 +1087,21 @@
|
||||
window.CognitiveAgentConsole?.notify?.("请等待语音整理完成后再发送");
|
||||
return;
|
||||
}
|
||||
const messageText = input?.value.trim() || "";
|
||||
const suggestedMessage = typeof messageOverride === "string" ? messageOverride.trim() : "";
|
||||
const messageText = suggestedMessage || input?.value.trim() || "";
|
||||
if (!messageText) {
|
||||
window.CognitiveAgentConsole?.notify?.("请输入问题或工作需求");
|
||||
return;
|
||||
}
|
||||
|
||||
createMessage("user", messageText);
|
||||
input.value = "";
|
||||
delete input.dataset.speechStage;
|
||||
delete input.dataset.speechUtteranceId;
|
||||
delete input.dataset.speechValue;
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
if (!suggestedMessage) {
|
||||
input.value = "";
|
||||
delete input.dataset.speechStage;
|
||||
delete input.dataset.speechUtteranceId;
|
||||
delete input.dataset.speechValue;
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
const assistantMessage = createMessage("assistant", "", { pending: true });
|
||||
pendingMessage = assistantMessage;
|
||||
latestControlResult = null;
|
||||
|
||||
@@ -7101,6 +7101,195 @@ body.device-power-drawer-open {
|
||||
linear-gradient(165deg, var(--card), var(--secondary));
|
||||
}
|
||||
|
||||
/* Body-level portal: the horizontal mode rail clips its own overflow. */
|
||||
.mode-chip-action-popover {
|
||||
width: min(430px, calc(100vw - 24px));
|
||||
max-height: min(68vh, 520px);
|
||||
position: fixed;
|
||||
z-index: 1900;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 9px;
|
||||
padding: 11px;
|
||||
overflow: hidden;
|
||||
color: var(--popover-foreground);
|
||||
background: color-mix(in oklch, var(--popover) 96%, var(--primary));
|
||||
border: 1px solid color-mix(in oklch, var(--primary) 24%, var(--border));
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 16px 42px color-mix(in oklch, var(--foreground) 18%, transparent),
|
||||
0 0 0 1px color-mix(in oklch, var(--card) 65%, transparent) inset;
|
||||
backdrop-filter: blur(16px);
|
||||
transform-origin: top center;
|
||||
animation: mode-action-popover-in 140ms ease-out;
|
||||
}
|
||||
|
||||
.mode-chip-action-popover[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mode-chip-action-popover.is-above {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
.mode-chip-action-popover > header,
|
||||
.mode-chip-action-popover > header > span {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mode-chip-action-popover > header {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.mode-chip-action-popover > header > span {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.mode-chip-action-popover > header small {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 8px;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.mode-chip-action-popover > header strong {
|
||||
max-width: 330px;
|
||||
overflow: hidden;
|
||||
color: var(--heading-foreground);
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mode-chip-action-popover > header b {
|
||||
min-width: 38px;
|
||||
min-height: 22px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
padding: 3px 7px;
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklch, var(--primary) 10%, var(--card));
|
||||
border-radius: 999px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.mode-chip-action-popover ol {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
margin: 0;
|
||||
padding: 0 3px 0 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mode-chip-action-popover li {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
padding: 7px 8px;
|
||||
color: var(--body-foreground);
|
||||
background: color-mix(in oklch, var(--secondary) 66%, var(--card));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.mode-chip-action-step {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklch, var(--primary) 9%, var(--card));
|
||||
border-radius: 7px;
|
||||
font-size: 8px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.mode-chip-action-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mode-chip-action-copy > strong {
|
||||
overflow: hidden;
|
||||
color: var(--heading-foreground);
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mode-chip-action-copy > small {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px 10px;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 8px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.mode-chip-action-copy > small > span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.mode-chip-action-copy > small b {
|
||||
margin-right: 3px;
|
||||
color: var(--primary);
|
||||
font-size: 7px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.mode-chip-action-empty {
|
||||
display: block !important;
|
||||
padding: 13px !important;
|
||||
color: var(--muted-foreground) !important;
|
||||
font-size: 9px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes mode-action-popover-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-3px) scale(0.985);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.mode-chip-action-popover {
|
||||
max-height: min(58vh, 430px);
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.mode-chip-action-copy > small {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mode-chip-action-popover {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-gateway,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device {
|
||||
|
||||
@@ -10,23 +10,23 @@
|
||||
<link rel="icon" href="/icons/app-icon-192.png" />
|
||||
<title>511工程-智能座舱</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=dual-seat-23" />
|
||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-65" />
|
||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-66" />
|
||||
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" />
|
||||
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-24" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-26" />
|
||||
<link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" />
|
||||
<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>
|
||||
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
|
||||
<script src="/network.js?v=android-pad-2" defer></script>
|
||||
<script src="/app.js?v=dual-seat-127" defer></script>
|
||||
<script src="/speech-input.js?v=sensevoice-12" defer></script>
|
||||
<script src="/app.js?v=dual-seat-128" defer></script>
|
||||
<script src="/speech-input.js?v=sensevoice-13" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
||||
<script src="/pwa.js?v=android-pad-3" defer></script>
|
||||
<script src="/console-v2.js?v=delivery-state-62" defer></script>
|
||||
<script src="/self-extension.js?v=gscu-tcp-43" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-24" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-26" defer></script>
|
||||
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
||||
</head>
|
||||
<body class="console-v2">
|
||||
@@ -552,13 +552,13 @@
|
||||
<header class="mode-gallery-header">
|
||||
<div>
|
||||
<h2 id="modeGalleryTitle">管理功能模式</h2>
|
||||
<p>按运行方式整理,预设与自定义模式统一归类</p>
|
||||
<p>按来源与运行方式整理,快速定位并管理模式</p>
|
||||
</div>
|
||||
<button class="mode-gallery-close" id="modeGalleryClose" type="button" aria-label="关闭功能模式管理"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
<div class="mode-gallery-summary"><span><i data-lucide="layers-3"></i>静态模式 · 动态模式</span><strong id="modeGallerySummaryCount">0</strong></div>
|
||||
<div class="mode-gallery-summary"><span><i data-lucide="layers-3"></i>自定义模式 · 静态模式 · 动态模式</span><strong id="modeGallerySummaryCount">0</strong></div>
|
||||
<div class="mode-gallery-groups" id="modeGalleryGroups" aria-live="polite"></div>
|
||||
<footer class="mode-gallery-footer"><i data-lucide="info"></i><span>静态模式执行后保持目标状态;动态模式持续变化,可随时停止。执行状态与主界面同步。</span></footer>
|
||||
<footer class="mode-gallery-footer"><i data-lucide="info"></i><span>自定义模式支持编辑与删除;静态模式执行后保持目标状态;动态模式持续变化,可随时停止。</span></footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v272";
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v275";
|
||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||
const APP_SHELL = [
|
||||
@@ -6,18 +6,18 @@ const APP_SHELL = [
|
||||
"/index.html",
|
||||
"/styles.css?v=dual-seat-23",
|
||||
"/network.js?v=android-pad-2",
|
||||
"/app.js?v=dual-seat-127",
|
||||
"/app.js?v=dual-seat-128",
|
||||
"/gesture-control.js?v=android-pad-9",
|
||||
"/pwa.js?v=android-pad-3",
|
||||
"/console-v2.css?v=delivery-state-65",
|
||||
"/console-v2.css?v=delivery-state-66",
|
||||
"/console-v2.js?v=delivery-state-62",
|
||||
"/self-extension.css?v=gscu-tcp-43",
|
||||
"/self-extension.js?v=gscu-tcp-43",
|
||||
"/speech-input.css?v=sensevoice-7",
|
||||
"/speech-input.js?v=sensevoice-12",
|
||||
"/speech-input.js?v=sensevoice-13",
|
||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||
"/assistant-chat.css?v=codex-assistant-24",
|
||||
"/assistant-chat.js?v=codex-assistant-24",
|
||||
"/assistant-chat.css?v=codex-assistant-26",
|
||||
"/assistant-chat.js?v=codex-assistant-26",
|
||||
"/energy-monitor.css?v=energy-monitor-4",
|
||||
"/energy-monitor.js?v=energy-monitor-4",
|
||||
"/assets/pets/self-extension-cat.png?v=3",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
const MIN_PREVIEW_AUDIO_MS = 320;
|
||||
const STATUS_RESET_MS = 5000;
|
||||
const MICROPHONE_CONNECT_TIMEOUT_MS = 12000;
|
||||
const DEFAULT_WAKE_WORD = "你好,双十一";
|
||||
const DEFAULT_WAKE_WORD = "你好,小迪";
|
||||
const PREFERRED_MICROPHONE = /TAKSTAR\s*V2/i;
|
||||
const ACTIVE_PHASES = new Set(["arming", "armed", "capturing", "finalizing"]);
|
||||
const DEBUG = new URLSearchParams(window.location.search).get("speechDebug") === "1"
|
||||
|
||||
Reference in New Issue
Block a user