优化语音输入/普通文本输入
This commit is contained in:
@@ -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("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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();
|
||||
Reference in New Issue
Block a user