优化语音输入/普通文本输入

This commit is contained in:
minghanchen
2026-09-03 13:36:40 +08:00
parent 009d8ca73e
commit 32d179143f
26 changed files with 2318 additions and 53 deletions
@@ -0,0 +1,288 @@
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();