Files
Cognitive_Agent/experiments/device-capability-card-demo/app.js
T
2026-09-03 13:36:40 +08:00

289 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
DEFAULT_DESCRIPTOR,
buildManualCommand,
cloneDescriptor,
parseInstruction,
splitAliases,
} from "./parser-engine.js";
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
let descriptor = cloneDescriptor();
let latestPayload = null;
const categoryLabels = {
vehicle_electrical: "车端电器",
seat: "座椅设备",
peripheral: "扩展外设",
};
const transportLabels = {
native_can: "原生 CAN",
domain_runtime: "领域执行器",
gscu_tcp: "红外网关",
mqtt: "MQTT",
};
const exampleTemplates = [
(name) => `开启${name}`,
(name) => `把${name}温度调到4度`,
(name) => `将${name}设为速冷模式`,
(name) => `关闭${name}`,
(name) => `把${name}调到零下10度`,
];
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function fillIdentityForm() {
$("#deviceName").value = descriptor.device.name;
$("#deviceId").value = descriptor.device.id;
$("#deviceDomain").value = descriptor.device.domain;
$("#deviceCategory").value = descriptor.device.category;
$("#deviceAliases").value = descriptor.device.aliases.join("、");
$("#deviceScope").value = descriptor.device.defaultScope;
$("#deviceTransport").value = descriptor.device.transport;
}
function syncIdentityFromForm() {
descriptor.device.name = $("#deviceName").value.trim() || "未命名设备";
descriptor.device.id = $("#deviceId").value.trim() || "unnamed_device";
descriptor.device.domain = $("#deviceDomain").value.trim() || "custom";
descriptor.device.category = $("#deviceCategory").value;
descriptor.device.aliases = splitAliases($("#deviceAliases").value);
descriptor.device.defaultScope = $("#deviceScope").value;
descriptor.device.targetScopes = [descriptor.device.defaultScope];
descriptor.device.transport = $("#deviceTransport").value;
descriptor.device.targetDevice = descriptor.device.id;
renderAll({ preserveResult: true });
}
function renderCapabilityEditor() {
$("#capabilityEditor").innerHTML = descriptor.capabilities.map((capability, capabilityIndex) => `
<section class="capability-definition" data-capability-index="${capabilityIndex}">
<div class="capability-title">
<div>
<strong>${escapeHtml(capability.label)}</strong>
<code>${escapeHtml(capability.id)}</code>
</div>
<label class="toggle-label">
<input type="checkbox" data-capability-enabled ${capability.enabled !== false ? "checked" : ""} />
启用
</label>
</div>
<div class="intent-list">
${capability.intents.map((intent, intentIndex) => `
<label class="intent-alias-field">
<span>${escapeHtml(intent.label)}</span>
<input
data-intent-index="${intentIndex}"
value="${escapeHtml(intent.phrases.join("、"))}"
aria-label="${escapeHtml(capability.label + intent.label)}的自然语言说法"
/>
</label>
`).join("")}
</div>
</section>
`).join("");
}
function renderExamples() {
$("#exampleCommands").innerHTML = exampleTemplates.map((makeExample) => {
const utterance = makeExample(descriptor.device.name);
return `<button type="button" data-example="${escapeHtml(utterance)}">${escapeHtml(utterance)}</button>`;
}).join("");
}
function controlMarkup(capability) {
const ui = capability.ui;
if (ui.type === "select") {
return `
<label class="card-control">
<span>${escapeHtml(capability.label)}</span>
<select data-manual-capability="${escapeHtml(capability.id)}">
${ui.options.map(([value, label]) => `<option value="${escapeHtml(value)}"${String(value) === String(ui.value) ? " selected" : ""}>${escapeHtml(label)}</option>`).join("")}
</select>
</label>
`;
}
return `
<label class="card-control">
<span>${escapeHtml(capability.label)} <output>${escapeHtml(ui.value)}${escapeHtml(ui.unit || "")}</output></span>
<input
type="range"
min="${ui.minimum}"
max="${ui.maximum}"
step="${ui.step}"
value="${ui.value}"
data-manual-capability="${escapeHtml(capability.id)}"
data-unit="${escapeHtml(ui.unit || "")}"
/>
</label>
`;
}
function renderDeviceCard() {
const enabledCapabilities = descriptor.capabilities.filter((item) => item.enabled !== false);
const category = categoryLabels[descriptor.device.category] || "自定义设备";
const transport = transportLabels[descriptor.device.transport] || descriptor.device.transport;
const icon = descriptor.device.category === "seat" ? "◫" : descriptor.device.category === "peripheral" ? "⌁" : "❄";
$("#deviceCardPreview").innerHTML = `
<header class="device-card-header">
<div class="device-identity">
<span class="device-icon" aria-hidden="true">${icon}</span>
<div>
<h3>${escapeHtml(descriptor.device.name)}</h3>
<p>${escapeHtml(category)} · ${escapeHtml(transport)} · ${escapeHtml(descriptor.device.defaultScope)}</p>
</div>
</div>
<span class="connection-state"><i></i>实验就绪</span>
</header>
<div class="readout-grid">
${descriptor.readouts.map((readout) => `<div><span>${escapeHtml(readout.label)}</span><strong>${escapeHtml(readout.value)}</strong></div>`).join("")}
</div>
<div class="card-controls">
${enabledCapabilities.length ? enabledCapabilities.map(controlMarkup).join("") : "<p>没有已启用功能。</p>"}
</div>
<p class="card-footnote">功能控件只生成结构化演示数据;执行映射为虚拟引用,不连接现有服务。</p>
`;
}
function compactDescriptor() {
return {
version: descriptor.version,
device: descriptor.device,
capabilities: descriptor.capabilities.map((capability) => ({
id: capability.id,
property: capability.property,
target_scopes: descriptor.device.targetScopes,
parameters: capability.parameters,
intents: capability.intents,
risk_level: capability.risk,
confirmation_policy: capability.confirmationPolicy,
executor: capability.executor,
enabled: capability.enabled,
})),
};
}
function renderDescriptor() {
$("#descriptorPreview").textContent = JSON.stringify(compactDescriptor(), null, 2);
}
function renderAll({ preserveResult = false } = {}) {
renderCapabilityEditor();
renderExamples();
renderDeviceCard();
renderDescriptor();
if (!preserveResult) renderResult(null);
}
function resultPresentation(result) {
if (!result) return { status: "idle", title: "等待解析", message: "输入一句话,查看设备、功能和参数的匹配过程。" };
if (result.source === "card_control") {
return {
status: "success",
title: "已生成卡片控制载荷",
message: `已生成 ${result.commands.length} 条结构化演示指令;实验页面不会发送到车辆。`,
};
}
if (result.semantic_status === "recognized_executable") {
return { status: "success", title: "已匹配可执行功能", message: `${result.clean_text} 当前仅生成演示载荷。` };
}
if (result.semantic_status === "ambiguous" || result.semantic_status === "incomplete") {
return { status: "warning", title: "需要补充信息", message: result.issues.join(";") };
}
return { status: "error", title: "未生成控制指令", message: result.issues.join(";") || "没有匹配到设备能力。" };
}
function renderResult(result) {
const presentation = resultPresentation(result);
$("#resultBanner").dataset.status = presentation.status;
$("#resultTitle").textContent = presentation.title;
$("#resultMessage").textContent = presentation.message;
$("#traceList").innerHTML = result?.trace?.length
? result.trace.map((item) => `<li data-state="${escapeHtml(item.state)}"><strong>${escapeHtml(item.stage)}</strong>${escapeHtml(item.detail)}</li>`).join("")
: "<li><strong>尚无轨迹</strong>解析后将在这里显示每一步决策。</li>";
latestPayload = result;
$("#payloadOutput").textContent = JSON.stringify(result || {}, null, 2);
$("#copyPayload").disabled = !result;
}
function parseCurrentUtterance() {
renderResult(parseInstruction($("#utterance").value, descriptor));
}
function bindEvents() {
["deviceName", "deviceId", "deviceDomain", "deviceCategory", "deviceAliases", "deviceScope", "deviceTransport"]
.forEach((id) => $("#" + id).addEventListener("input", syncIdentityFromForm));
$("#capabilityEditor").addEventListener("input", (event) => {
const section = event.target.closest("[data-capability-index]");
if (!section) return;
const capability = descriptor.capabilities[Number(section.dataset.capabilityIndex)];
if (event.target.matches("[data-capability-enabled]")) {
capability.enabled = event.target.checked;
} else if (event.target.matches("[data-intent-index]")) {
capability.intents[Number(event.target.dataset.intentIndex)].phrases = splitAliases(event.target.value);
}
renderDeviceCard();
renderDescriptor();
});
$("#exampleCommands").addEventListener("click", (event) => {
const button = event.target.closest("[data-example]");
if (!button) return;
$("#utterance").value = button.dataset.example;
parseCurrentUtterance();
});
$("#parseCommand").addEventListener("click", parseCurrentUtterance);
$("#utterance").addEventListener("keydown", (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") parseCurrentUtterance();
});
$("#deviceCardPreview").addEventListener("input", (event) => {
if (!event.target.matches("[data-manual-capability]")) return;
const output = event.target.closest(".card-control")?.querySelector("output");
if (output) output.textContent = `${event.target.value}${event.target.dataset.unit || ""}`;
const values = Object.fromEntries($$("[data-manual-capability]", $("#deviceCardPreview"))
.map((input) => [input.dataset.manualCapability, input.value]));
const result = {
source: "card_control",
semantic_status: "recognized_executable",
execution_eligible: false,
note: "实验页面不允许真实执行",
commands: buildManualCommand(descriptor, values),
};
renderResult(result);
});
$("#copyPayload").addEventListener("click", async () => {
if (!latestPayload) return;
await navigator.clipboard.writeText(JSON.stringify(latestPayload, null, 2));
const button = $("#copyPayload");
button.textContent = "已复制";
window.setTimeout(() => { button.textContent = "复制"; }, 1200);
});
$("#resetDemo").addEventListener("click", () => {
descriptor = cloneDescriptor(DEFAULT_DESCRIPTOR);
fillIdentityForm();
$("#utterance").value = "把冰箱温度调到4度";
renderAll();
parseCurrentUtterance();
});
}
fillIdentityForm();
renderAll();
bindEvents();
parseCurrentUtterance();