feat: integrate production cockpit AI assistant

This commit is contained in:
vee
2026-09-08 17:46:07 +08:00
parent 5e228e12ae
commit 93103c2579
66 changed files with 11046 additions and 374 deletions
+174 -10
View File
@@ -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;