feat: integrate production cockpit AI assistant
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user