(function initializeUnifiedAssistant() { const STORAGE_KEY = "cognitive-agent.assistant-messages"; const CONVERSATION_STORAGE_KEY = "cognitive-agent.conversation-id"; const ACTIVE_MODE_DRAFT_STORAGE_KEY = "cognitive-agent.active-mode-draft"; 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; let latestControlResult = null; let activeModeDraftContext = null; let activeSchedulePoll = null; let initialized = false; const $ = (selector, root = document) => root.querySelector(selector); function setDrawerOpen(open, { restoreFocus = false } = {}) { const drawer = $("#assistantDrawer"); const trigger = $("#assistantDrawerToggle"); if (!drawer || !trigger) return; document.body.classList.toggle("assistant-drawer-open", open); trigger.classList.toggle("active", open); trigger.setAttribute("aria-pressed", String(open)); trigger.setAttribute("aria-expanded", String(open)); const triggerLabel = open ? "收起AI助手" : "展开AI助手"; trigger.setAttribute("aria-label", triggerLabel); trigger.title = triggerLabel; drawer.setAttribute("aria-hidden", String(!open)); drawer.toggleAttribute("inert", !open); const speech = window.CognitiveAgentSpeech; if (typeof speech?.setAssistantOpen === "function") { void speech.setAssistantOpen(open); } if (open) { requestAnimationFrame(() => $("#utterance")?.focus({ preventScroll: true })); } else { // A collapsed assistant must never leave an invisible microphone or // preview request running in the background. if (typeof speech?.setAssistantOpen !== "function") { void speech?.stop?.("AI助手已收起,语音输入已停止"); } if (restoreFocus) trigger.focus({ preventScroll: true }); } } function installAssistantDrawer() { const drawer = $(".v2-operation-shell"); const heading = $(".v2-operation-heading", drawer); if (!drawer || !heading || $("#assistantDrawerToggle")) return; drawer.id = "assistantDrawer"; drawer.setAttribute("role", "dialog"); drawer.setAttribute("aria-label", "AI助手"); const actions = document.createElement("div"); actions.className = "assistant-drawer-actions"; const reset = $("#newConversation"); if (reset) { reset.classList.add("assistant-drawer-new"); actions.append(reset); } heading.append(actions); const trigger = document.createElement("button"); trigger.id = "assistantDrawerToggle"; trigger.className = "assistant-drawer-toggle v2-panel-control-button"; trigger.type = "button"; trigger.setAttribute("aria-controls", drawer.id); trigger.setAttribute("aria-pressed", "false"); trigger.setAttribute("aria-expanded", "false"); trigger.setAttribute("aria-label", "展开AI助手"); trigger.title = "展开AI助手"; trigger.innerHTML = 'AI助手'; ($("#v2PanelControlDock") || document.body).append(trigger); trigger.addEventListener("click", () => { setDrawerOpen(!document.body.classList.contains("assistant-drawer-open")); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && document.body.classList.contains("assistant-drawer-open")) { event.preventDefault(); setDrawerOpen(false, { restoreFocus: true }); } }); window.addEventListener("cognitive-agent:module-change", (event) => { if (event.detail?.module === "self-extension") setDrawerOpen(false); }); setDrawerOpen(true); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); } function createMessageId() { if (globalThis.crypto?.randomUUID) return `ui-${globalThis.crypto.randomUUID()}`; return `ui-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; } function createConversationId() { if (globalThis.crypto?.randomUUID) return `conv-${globalThis.crypto.randomUUID()}`; return `conv-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; } function conversationId() { const existing = sessionStorage.getItem(CONVERSATION_STORAGE_KEY) || window.CognitiveAgentConsole?.getConversationId?.(); if (existing) return existing; const created = createConversationId(); sessionStorage.setItem(CONVERSATION_STORAGE_KEY, created); return created; } function normalizeModeName(value) { return String(value || "").trim().replace(/\s+/g, "").toLowerCase(); } function sanitizeModeDraftContext(value) { if (!value || typeof value !== "object") return null; const name = String(value.name || "").trim(); const sourceRequestId = String(value.source_request_id || "").trim(); const vehicleId = String(value.vehicle_id || "").trim(); const draftConversationId = String(value.conversation_id || "").trim(); if ( !name || name.length > 64 || !/^req-[a-z0-9-]{6,100}$/.test(sourceRequestId) ) return null; if (!vehicleId || vehicleId.length > 80 || draftConversationId !== conversationId()) return null; return { name, source_request_id: sourceRequestId, vehicle_id: vehicleId, conversation_id: draftConversationId, }; } function loadActiveModeDraftContext() { try { return sanitizeModeDraftContext( JSON.parse(sessionStorage.getItem(ACTIVE_MODE_DRAFT_STORAGE_KEY) || "null"), ); } catch (_error) { return null; } } function rememberModeDraftContext(draft) { const candidate = sanitizeModeDraftContext({ name: draft?.name, source_request_id: draft?.source_request_id, vehicle_id: draft?.vehicle_id || $("#vehicleSelect")?.value || null, conversation_id: conversationId(), }); if (!candidate) return; activeModeDraftContext = candidate; sessionStorage.setItem(ACTIVE_MODE_DRAFT_STORAGE_KEY, JSON.stringify(candidate)); } function clearActiveModeDraftContext(sourceRequestId = null) { if ( sourceRequestId && activeModeDraftContext?.source_request_id !== String(sourceRequestId) ) return; activeModeDraftContext = null; sessionStorage.removeItem(ACTIVE_MODE_DRAFT_STORAGE_KEY); } function isModeDraftUpdateRequest(message, context) { const compact = String(message || "").trim().replace(/\s+/g, ""); if (!compact || !context?.name) return false; const politePrefix = "(?:(?:请|麻烦)(?:帮我|帮忙|给我)?|帮我|帮忙|给我)?"; const prefixUpdateVerb = "(?:修改一下|修改|调整|更改|更新|改一下|变更)"; const changePredicate = "(?:修改|改成|改为|更改|更新|变更|变为|变成|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)"; const modeName = "([^,。!?!?、;;::]{1,64}?模式)"; const modePossessiveBoundary = "(?:中的|里的|内的|之中的|的|中|里|内)"; const modeMemberBoundary = `(?:${modePossessiveBoundary}|[,,::])`; const question = /为什么|什么|怎么|如何|介绍|解释|作用|区别|原理|哪些|多少|几(?:度|档|级|个|项)?|是否|能否|可不可以|合适吗|合理吗|会怎样|列出|告诉我|[??]|(?:吗|么|呢)[。.!!]?$/; const candidates = [ { pattern: new RegExp(`^${politePrefix}${prefixUpdateVerb}${modeName}${modeMemberBoundary}[,,::]?(.*)$`), predicateRequired: false, }, { pattern: new RegExp(`^${politePrefix}(?:把|将)${modeName}${modeMemberBoundary}[,,::]?(.+)$`), predicateRequired: true, }, { pattern: new RegExp(`^${politePrefix}${modeName}${modeMemberBoundary}[,,::]?(.+)$`), predicateRequired: true, }, ]; return candidates.some(({ pattern, predicateRequired }) => { const matched = compact.match(pattern); const change = String(matched?.[2] || ""); return Boolean( matched && normalizeModeName(matched[1]) === normalizeModeName(context.name) && !question.test(change) && (!predicateRequired || new RegExp(changePredicate).test(change)) ); }); } function requestModeDraftContext(message) { const context = activeModeDraftContext || loadActiveModeDraftContext(); if (!context || !isModeDraftUpdateRequest(message, context)) return null; activeModeDraftContext = context; return { name: context.name, source_request_id: context.source_request_id, vehicle_id: context.vehicle_id, }; } function loadStoredMessages() { try { const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "[]"); return Array.isArray(stored) ? stored.filter((item) => item && ["user", "assistant"].includes(item.role)) : []; } catch (_error) { return []; } } function persistMessages() { const messages = [...document.querySelectorAll("#assistantMessages [data-message-role]")] .map((node) => ({ role: node.dataset.messageRole, content: node.dataset.messageContent || "", })) .filter((item) => item.content && item.content !== WELCOME_TEXT) .slice(-MAX_STORED_MESSAGES); sessionStorage.setItem(STORAGE_KEY, JSON.stringify(messages)); } function appendInlineText(container, text) { const parts = String(text).split(/(`[^`\n]+`)/g); parts.forEach((part) => { if (part.startsWith("`") && part.endsWith("`") && part.length > 2) { const code = document.createElement("code"); code.textContent = part.slice(1, -1); container.append(code); } else { container.append(document.createTextNode(part)); } }); } function renderRichText(container, value) { container.replaceChildren(); const sections = String(value).split(/```(?:[\w+-]+)?\n?([\s\S]*?)```/g); sections.forEach((section, index) => { if (index % 2 === 1) { const pre = document.createElement("pre"); const code = document.createElement("code"); code.textContent = section.replace(/\n$/, ""); pre.append(code); container.append(pre); return; } const lines = section.split("\n"); let list = null; lines.forEach((line) => { const bullet = line.match(/^\s*[-*]\s+(.+)$/); if (bullet) { if (!list) { list = document.createElement("ul"); container.append(list); } const item = document.createElement("li"); appendInlineText(item, bullet[1]); list.append(item); return; } list = null; if (!line.trim()) return; const heading = line.match(/^#{1,3}\s+(.+)$/); const block = document.createElement(heading ? "strong" : "p"); if (heading) block.className = "assistant-inline-heading"; appendInlineText(block, heading ? heading[1] : line); container.append(block); }); }); } function shouldFollowLatest(transcript) { return transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 90; } function followLatest(force = false) { const transcript = $("#assistantTranscript"); if (!transcript || (!force && !shouldFollowLatest(transcript))) return; requestAnimationFrame(() => { transcript.scrollTop = transcript.scrollHeight; }); } function resizeComposerInput(input = $("#utterance")) { if (!input) return; input.style.height = "auto"; const contentHeight = input.scrollHeight; const nextHeight = Math.max( COMPOSER_MIN_HEIGHT, Math.min(contentHeight, COMPOSER_MAX_HEIGHT), ); input.style.height = `${nextHeight}px`; const scrollable = contentHeight > COMPOSER_MAX_HEIGHT + 1; input.classList.toggle("is-scrollable", scrollable); if (!scrollable) input.scrollTop = 0; } function createMessage(role, content = "", { pending = false } = {}) { const article = document.createElement("article"); article.className = `assistant-message assistant-message-${role}`; article.dataset.messageId = createMessageId(); article.dataset.messageRole = role; article.dataset.messageContent = content; if (role === "assistant") { const avatar = document.createElement("span"); avatar.className = "assistant-avatar"; avatar.setAttribute("aria-hidden", "true"); avatar.innerHTML = ''; article.append(avatar); } const body = document.createElement("div"); body.className = "assistant-message-body"; const contentNode = document.createElement("div"); contentNode.className = "assistant-message-content"; if (pending && !content) { contentNode.classList.add("assistant-thinking"); contentNode.innerHTML = '正在理解你的需求'; } else if (role === "assistant") { renderRichText(contentNode, content); } else { contentNode.textContent = content; } body.append(contentNode); const action = document.createElement("div"); action.className = "assistant-action-status"; action.hidden = true; action.setAttribute("role", "status"); body.append(action); article.append(body); $("#assistantMessages").append(article); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); followLatest(true); return { article, body, contentNode, action, content }; } function setMessageContent(message, content, { final = false } = {}) { message.content = content; message.article.dataset.messageContent = content; message.contentNode.classList.remove("assistant-thinking"); if (final) renderRichText(message.contentNode, content); else message.contentNode.textContent = content; followLatest(); } function setActionStatus(message, text, state = "working") { message.action.hidden = !text; message.action.className = `assistant-action-status ${state}`; message.action.replaceChildren(); if (!text) return; const icon = document.createElement("i"); icon.setAttribute( "data-lucide", state === "success" ? "circle-check" : state === "failed" ? "circle-x" : "loader-circle", ); const label = document.createElement("span"); label.textContent = text; message.action.append(icon, label); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); 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 = ``; 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))}"]`; if ($(selector, message.body)) return; const card = document.createElement("div"); card.className = "assistant-mode-created"; card.dataset.assistantModeCreated = String(profile.id); const icon = document.createElement("span"); icon.className = "assistant-mode-created-icon"; icon.innerHTML = ''; const copy = document.createElement("span"); copy.className = "assistant-mode-created-copy"; const label = document.createElement("small"); label.textContent = updated ? "已更新「我的模式」" : "已添加到「我的模式」"; const name = document.createElement("strong"); name.textContent = String(profile.name || "新功能模式"); copy.append(label, name); const reveal = document.createElement("button"); reveal.type = "button"; reveal.textContent = "查看"; reveal.addEventListener("click", () => { window.CognitiveAgentConsole?.revealModeProfile?.(String(profile.id), { focus: true }); }); card.append(icon, copy, reveal); message.body.append(card); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); followLatest(true); } function modeDraftRows(draft, rawActions) { const instruction = String(draft?.instruction || ""); const actions = Array.isArray(rawActions) ? rawActions.filter((item) => item && typeof item === "object") : []; const consumed = new Set(); const rows = []; const targetZone = (action) => String( action?.target?.instance || action?.target?.zone || action?.target_scope || action?.target || "", ); const parameters = (action) => ( action?.parameters && typeof action.parameters === "object" ? action.parameters : {} ); const instructionOrder = (...terms) => { const indexes = terms.map((term) => instruction.indexOf(term)).filter((index) => index >= 0); return indexes.length ? Math.min(...indexes) : 10_000 + rows.length; }; const addRow = (label, value, icon, order, indexes = []) => { indexes.forEach((index) => consumed.add(index)); rows.push({ label, value, icon, order }); }; const zeroGravityIndexes = actions .map((action, index) => ({ action, index })) .filter(({ action }) => String(action.capability || "").startsWith("seat.set_") && ["passenger", "front_right"].includes(targetZone(action))) .map(({ index }) => index); if (/零重力/.test(instruction) && zeroGravityIndexes.length >= 2) { addRow( "副驾座椅", "零重力模式", "armchair", instructionOrder("副驾", "零重力"), zeroGravityIndexes, ); } const lightIndexes = actions .map((action, index) => ({ action, index })) .filter(({ action }) => ["interior_light", "ambient_light"].includes(String(action.domain || ""))) .map(({ index }) => index); if ( lightIndexes.length >= 2 && /(?:灯光.*(?:全部|所有|全车).*(?:关闭|关掉|熄灭)|(?:全部|所有|全车).*灯光.*(?:关闭|关掉|熄灭))/.test(instruction) ) { addRow( "座舱灯光", "全部关闭", "lightbulb-off", instructionOrder("灯光"), lightIndexes, ); } const motionLabel = (value) => ({ open: "打开", close: "关闭", stop: "停止", half: "半开", on: "开启", off: "关闭", }[String(value)] || String(value || "已配置")); const shadePositionLabel = (value) => { if (value == null || String(value).trim() === "") return "目标开度待确认"; const position = Number(value); if (!Number.isFinite(position) || position < 0 || position > 100) { return "目标开度待确认"; } const target = `目标开度 ${position}%`; if (position === 0) return `完全关闭(${target})`; if (position === 50) return `半开(${target})`; if (position === 100) return `完全打开(${target})`; return target; }; actions.forEach((action, index) => { if (consumed.has(index)) return; const capability = String(action.capability || ""); const domain = String(action.domain || ""); const zone = targetZone(action); const zoneLabel = { driver: "主驾", passenger: "副驾", front_left: "主驾", front_right: "副驾", front_row: "前排", rear_row: "后排", rear_left: "左后", rear_right: "右后", }[zone] || ""; const params = parameters(action); let label = capability || domain || "座舱设备"; let value = "已配置"; let icon = "sliders-horizontal"; let order = 10_000 + index; if (capability === "roof.set_shade_motion" || capability === "roof.set_shade_position") { label = "遮阳帘"; value = params.position_pct != null ? shadePositionLabel(params.position_pct) : motionLabel(params.motion); icon = "blinds"; order = instructionOrder("遮阳帘", "天幕帘"); } else if (domain === "window") { const windowLabels = { all: "全部车窗", front: "前排车窗", rear: "后排车窗", front_left: "主驾车窗", front_right: "副驾车窗", rear_left: "左后车窗", rear_right: "右后车窗", }; label = windowLabels[zone] || "车窗"; value = params.position_pct != null ? `${params.position_pct}%` : motionLabel(params.motion); icon = "panels-top-left"; order = instructionOrder("车窗"); } else if (domain === "seat") { const seatLabel = ["passenger", "front_right"].includes(zone) ? "副驾" : "主驾"; const seatCapabilityLabels = { "seat.set_longitudinal_position": "座椅前后位置", "seat.set_cushion_tilt": "坐垫角度", "seat.set_backrest_recline": "靠背角度", "seat.set_legrest_height": "腿托高度", "seat.set_ventilation": "座椅通风", "seat.set_heating": "座椅加热", "seat.set_driver_ventilation_native": "座椅通风", "seat.set_driver_heating_native": "座椅加热", "seat.set_driver_massage_native": "座椅按摩", }; label = `${seatLabel}${seatCapabilityLabels[capability] || "座椅"}`; value = params.position_pct != null ? `${params.position_pct}%${params.angle_deg != null ? `(${params.angle_deg}°)` : ""}` : motionLabel(params.state || params.mode_id); if (["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"].includes(capability)) { // CH1 driver controls encode high as 1 and low as 2; passenger // comfort levels use a different scale and must not reuse this map. const level = { 1: "高档", 2: "低档" }[params.level ?? 2] || "档位待确认"; value = params.state === "off" ? "关闭" : params.state === "on" ? `开启 · ${level}` : "开关状态待确认"; } else if (["seat.set_ventilation", "seat.set_heating"].includes(capability)) { value = params.state === "off" ? "关闭" : params.state === "on" ? `开启${params.level != null ? ` · ${params.level}档` : ""}` : "开关状态待确认"; } else if (capability === "seat.set_driver_massage_native") { const massageModes = { wave: "波浪", pulse: "脉冲", stretch: "舒展", slack: "舒缓", relax: "放松", }; const massageStrengths = { 1: "轻柔", 2: "中等", 3: "较强" }; value = params.action === "off" ? "关闭" : params.action === "mode" ? (massageModes[params.mode] ? `${massageModes[params.mode]}模式` : "按摩模式待确认") : params.action === "strength" ? (massageStrengths[params.strength] ? `${massageStrengths[params.strength]}力度` : "按摩力度待确认") : "按摩动作待确认"; } icon = "armchair"; order = instructionOrder("座椅", "副驾", "主驾"); } else if (capability === "climate.set_temperature") { label = `${zoneLabel}空调温度`; value = `${params.temperature_c ?? "--"}℃`; icon = "thermometer"; order = instructionOrder("空调", "温度"); } else if (domain === "climate") { const climateModeLabels = { "climate.set_cycle_mode": { internal: "内循环", external: "外循环" }, "climate.set_control_mode": { manual: "手动控制", auto: "自动控制" }, "climate.set_outlet_mode": { face: "吹面", face_feet: "吹面 / 吹脚", feet: "吹脚", feet_defrost: "吹脚 / 除霜", defrost: "除霜", face_feet_defrost: "吹面 / 吹脚 / 除霜", face_defrost: "吹面 / 除霜", }, }; label = capability === "climate.set_fan_level" ? "空调风量" : capability === "climate.set_cycle_mode" ? "空调循环" : capability === "climate.set_control_mode" ? "空调控制模式" : capability === "climate.set_outlet_mode" ? "空调出风模式" : "空调"; label = `${zoneLabel}${label}`; value = params.level != null ? `${params.level}档` : params.mode != null ? (climateModeLabels[capability]?.[String(params.mode)] || String(params.mode)) : motionLabel(params.state); icon = "fan"; order = instructionOrder("空调", "风量"); } else if (capability === "interior_light.set_reading_power") { label = `${zoneLabel}阅读灯`; value = motionLabel(params.state); icon = "book-open"; order = instructionOrder("阅读灯", "灯光"); } else if (["interior_light", "ambient_light"].includes(domain)) { label = domain === "ambient_light" ? "氛围灯" : "室内灯"; value = motionLabel(params.state); if (capability === "ambient_light.set_brightness_level") { label = "氛围灯亮度"; value = params.brightness_level != null ? `${params.brightness_level}档` : "亮度档位待确认"; } else if (capability === "ambient_light.set_color_index") { label = "氛围灯颜色"; value = params.color_index != null ? `色号 ${params.color_index}` : "颜色待确认"; } else if (capability === "ambient_light.set_illumination") { value = [ params.color_hex != null ? `颜色 ${params.color_hex}` : "颜色待确认", params.brightness_pct != null ? `亮度 ${params.brightness_pct}%` : "亮度待确认", ].join(" · "); } icon = "lightbulb"; order = instructionOrder(label, "灯光"); } addRow(label, value, icon, order, [index]); }); return rows.sort((left, right) => left.order - right.order); } function modeDraftScheduleView(draft) { const schedule = draft?.schedule; if ( schedule?.schema !== "cognitive-agent-mode-schedule/v1" || !Array.isArray(schedule.phases) || !schedule.phases.length || !Number.isInteger(schedule.repeat_count) || schedule.repeat_count < 1 || schedule.repeat_count > 20 || schedule.phases.some((phase) => !Number.isFinite(phase?.duration_ms) || phase.duration_ms <= 0 || !Array.isArray(phase.operations)) ) return null; const durationLabel = (durationMs) => { const minutes = Math.floor(durationMs / 60000); const seconds = Number(((durationMs % 60000) / 1000).toFixed(3)); return minutes ? `${minutes}分${seconds ? `${seconds}秒` : ""}` : `${seconds}秒`; }; let offsetMs = 0; const phases = schedule.phases.map((phase, index) => { // Each phase is rendered separately, without instruction-based grouping: // the same seat or temperature can have a different target later on. const rows = modeDraftRows({ instruction: "" }, phase.operations.map((operation) => ({ ...operation, target: operation.target || (operation.domain === "seat" ? { instance: operation.target_scope } : { zone: operation.target_scope }), }))); const view = { id: String(phase.id || `phase-${index + 1}`), name: String(phase.name || `阶段 ${index + 1}`), timing: `+${durationLabel(offsetMs)} 开始 · 持续 ${durationLabel(phase.duration_ms)}`, rows, }; offsetMs += phase.duration_ms; return view; }); return { phases, operationCount: schedule.phases.reduce((count, phase) => count + phase.operations.length, 0), summary: `${phases.length} 个阶段 · 重复 ${schedule.repeat_count} 次 · 总时长 ${durationLabel(offsetMs * schedule.repeat_count)}`, stopHint: "执行后可输入“停止动态模式”提前结束,已执行状态不自动回滚。各阶段按顺序执行,时间为每轮的相对时间。", }; } function appendModeDraftRows(container, rows) { rows.forEach((item) => { const row = document.createElement("div"); row.className = "assistant-mode-draft-row"; const icon = document.createElement("i"); icon.dataset.lucide = item.icon; const label = document.createElement("span"); label.textContent = item.label; const value = document.createElement("strong"); value.textContent = item.value; row.append(icon, label, value); container.append(row); }); } async function saveModeProfileDraft(message, card, draft, buttons, status) { buttons.forEach((button) => { button.disabled = true; }); card.classList.add("is-busy"); status.hidden = false; status.className = "assistant-mode-draft-status working"; const existingModeId = String(draft.existing_mode_id || "").trim(); status.textContent = existingModeId ? "正在将验证结果保存为模式更新…" : "正在保存到「我的模式」…"; try { const endpoint = existingModeId ? `/api/mode-profiles/${encodeURIComponent(existingModeId)}` : "/api/mode-profiles"; const response = await window.CognitiveAgentNetwork.request(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: draft.name, instruction: draft.instruction, source_request_id: draft.source_request_id, vehicle_id: draft.vehicle_id || $("#vehicleSelect")?.value || null, }), timeoutMs: 45 * 1000, retries: 0, }); const payload = await response.json(); if (response.status === 401) { window.location.replace("/login"); return; } if (!response.ok) throw new Error(payload.message || payload.error || `HTTP ${response.status}`); card.classList.remove("is-busy"); card.classList.add("is-saved"); status.className = "assistant-mode-draft-status success"; status.textContent = existingModeId ? "模式已更新" : "模式已保存"; card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", ""); clearActiveModeDraftContext(draft.source_request_id); await window.CognitiveAgentConsole?.refreshModeProfiles?.(payload.mode_profile?.id); appendModeCreatedState(message, payload.mode_profile, { updated: Boolean(existingModeId) }); persistMessages(); } catch (error) { card.classList.remove("is-busy"); status.className = "assistant-mode-draft-status failed"; status.textContent = error.message || "保存失败,请重试。"; buttons.forEach((button) => { button.disabled = false; }); } } async function tryModeProfileDraft(card, draft, actions, buttons, status) { buttons.forEach((button) => { button.disabled = true; }); card.classList.add("is-busy"); status.hidden = false; status.className = "assistant-mode-draft-status working"; status.textContent = "正在进行模拟试运行…"; try { const result = await window.CognitiveAgentConsole?.tryModeDraft?.({ ...draft, actions }); if (!result || result.status !== "succeeded") throw new Error("试运行未通过,请检查动作设置。"); status.className = "assistant-mode-draft-status success"; status.textContent = "试运行通过 · 尚未保存"; } catch (error) { status.className = "assistant-mode-draft-status failed"; status.textContent = error.message || "试运行失败,请重试。"; } finally { card.classList.remove("is-busy"); buttons.forEach((button) => { button.disabled = false; }); } } function supersedeModeDraftCards(draft) { const nextName = normalizeModeName(draft?.name); const nextRequestId = String(draft?.source_request_id || ""); if (!nextName || !nextRequestId) return; document.querySelectorAll("[data-assistant-mode-draft]").forEach((card) => { if ( card.dataset.assistantModeName !== nextName || card.dataset.assistantModeDraft === nextRequestId ) return; card.classList.remove("is-busy"); card.classList.add("is-superseded"); card.querySelectorAll("button").forEach((button) => { button.disabled = true; }); card.querySelector(".assistant-mode-draft-actions")?.setAttribute("hidden", ""); const priorStatus = card.querySelector(".assistant-mode-draft-status"); if (priorStatus) { priorStatus.hidden = false; priorStatus.className = "assistant-mode-draft-status superseded"; priorStatus.textContent = "已由下方最新草稿替换"; } }); } function appendModeDraftState(message, draft, actions) { if (!message?.body || !draft?.name || !draft?.source_request_id) return; rememberModeDraftContext(draft); supersedeModeDraftCards(draft); const selector = `[data-assistant-mode-draft="${CSS.escape(String(draft.source_request_id))}"]`; if ($(selector, message.body)) return; const card = document.createElement("section"); card.className = "assistant-mode-draft"; card.dataset.assistantModeDraft = String(draft.source_request_id); card.dataset.assistantModeName = normalizeModeName(draft.name); message.article?.classList.add("assistant-message-mode-draft"); const heading = document.createElement("header"); const headingCopy = document.createElement("span"); const eyebrow = document.createElement("small"); eyebrow.textContent = draft.existing_mode_id ? "已载入现有功能模式" : "已为你生成功能模式"; const title = document.createElement("strong"); title.textContent = `「${draft.name}」`; headingCopy.append(eyebrow, title); const badge = document.createElement("span"); badge.className = "assistant-mode-draft-badge"; const scheduleView = modeDraftScheduleView(draft); badge.textContent = `${scheduleView?.operationCount ?? (Number(draft.operation_count) || 0)} 项动作`; heading.append(headingCopy, badge); const list = document.createElement("div"); list.className = "assistant-mode-draft-list"; if (scheduleView) { card.classList.add("assistant-mode-draft-scheduled"); const summary = document.createElement("p"); summary.className = "assistant-mode-schedule-summary"; summary.textContent = scheduleView.summary; list.append(summary); scheduleView.phases.forEach((phase, index) => { const section = document.createElement("section"); section.className = "assistant-mode-schedule-phase"; section.dataset.modeSchedulePhase = phase.id; const phaseHeading = document.createElement("header"); const name = document.createElement("strong"); name.textContent = `${index + 1}. ${phase.name}`; const timing = document.createElement("small"); timing.textContent = phase.timing; phaseHeading.append(name, timing); section.append(phaseHeading); appendModeDraftRows(section, phase.rows); list.append(section); }); const hint = document.createElement("p"); hint.className = "assistant-mode-schedule-hint"; hint.textContent = scheduleView.stopHint; list.append(hint); } else { appendModeDraftRows(list, modeDraftRows(draft, actions)); } const controls = document.createElement("div"); controls.className = "assistant-mode-draft-actions"; const save = document.createElement("button"); save.type = "button"; save.className = "assistant-mode-draft-save"; save.innerHTML = draft.existing_mode_id ? '确认更新此模式' : '保存为功能模式'; const trial = document.createElement("button"); trial.type = "button"; trial.className = "assistant-mode-draft-trial"; trial.textContent = "先试运行"; controls.append(save, trial); const status = document.createElement("p"); status.className = "assistant-mode-draft-status"; status.hidden = true; status.setAttribute("role", "status"); const buttons = [save, trial]; save.addEventListener("click", () => { void saveModeProfileDraft(message, card, draft, buttons, status); }); trial.addEventListener("click", () => { void tryModeProfileDraft(card, draft, actions, buttons, status); }); card.append(heading, list, controls, status); message.body.append(card); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); followLatest(true); } function startModeCreation() { setDrawerOpen(true); const input = $("#utterance"); if (!input) return; const helper = createMessage( "assistant", "告诉我模式名称和希望联动的座舱状态。比如:创建宝宝睡眠模式,副驾半躺、空调 24℃、关闭阅读灯。我会先生成动作预览,确认后再加入「我的模式」。", ); helper.article.classList.add("assistant-mode-guide"); input.value = "请帮我创建一个"; input.dispatchEvent(new Event("input", { bubbles: true })); input.focus({ preventScroll: true }); input.setSelectionRange(input.value.length, input.value.length); } function setComposerPending(pending) { const button = $("#runButton"); const input = $("#utterance"); if (!button || !input) return; document.body.classList.toggle("assistant-drawer-busy", pending); button.classList.toggle("assistant-stop", pending); button.setAttribute("aria-label", pending ? "停止生成" : "发送消息"); button.title = pending ? "停止生成" : "发送消息"; // General questions must stay available even when no physical CAN adapter // is connected; hardware eligibility is evaluated only for control routes. button.disabled = false; button.innerHTML = pending ? '停止' : '发送'; input.setAttribute("aria-busy", String(pending)); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); } function requestBody(message) { const executionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run"; const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can"; const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id"); const vehicleId = $("#vehicleSelect")?.value || null; const modeProfileDraftContext = requestModeDraftContext(message); return { message, conversation_id: conversationId(), client_type: "web_console_codex_assistant", execution_mode: executionMode, control_transport: controlTransport, ...(vehicleId ? { vehicle_id: vehicleId } : {}), ...(modeProfileDraftContext ? { mode_profile_draft_context: modeProfileDraftContext } : {}), ...(executionMode === "hardware" && controlTransport !== "wireless_mqtt" && hardwareClientId ? { hardware_client_id: hardwareClientId } : {}), ...(executionMode === "simulated_hardware" ? { simulation_fault: $("#simulationFault")?.value || "normal" } : {}), }; } async function readErrorResponse(response) { try { const payload = await response.json(); return payload.message || payload.error || `HTTP ${response.status}`; } catch (_error) { return `HTTP ${response.status}`; } } function stopScheduleStatusPoll() { if (!activeSchedulePoll) return; window.clearTimeout(activeSchedulePoll.timer); activeSchedulePoll.controller?.abort(); activeSchedulePoll = null; } function startScheduleStatusPoll(payload, message) { stopScheduleStatusPoll(); const vehicleId = String(payload?.vehicle_id || payload?.vehicle?.id || payload?.mode_profile?.vehicle_id || $("#vehicleSelect")?.value || ""); if (!vehicleId) return; const poll = { vehicleId, conversationId: conversationId(), message, sessionId: String(payload?.schedule_execution?.session_id || ""), runId: String(payload?.schedule_execution?.run_id || ""), modeProfileId: String(payload?.mode_profile?.id || ""), timer: null, controller: null, }; activeSchedulePoll = poll; const isCurrent = () => activeSchedulePoll === poll && conversationId() === poll.conversationId && (!$("#vehicleSelect")?.value || $("#vehicleSelect").value === poll.vehicleId); const update = async () => { if (!isCurrent()) { if (activeSchedulePoll === poll) stopScheduleStatusPoll(); return; } poll.controller = new AbortController(); try { const response = await window.CognitiveAgentNetwork.request( `/api/modes/schedule/status?vehicle_id=${encodeURIComponent(poll.vehicleId)}`, { method: "GET", signal: poll.controller.signal, timeoutMs: 8000, retries: 0 }, ); if (!isCurrent()) return; if (response.status === 401) { setActionStatus(message, "登录状态已失效,无法继续查询时间轴状态;请重新登录查看。", "failed"); stopScheduleStatusPoll(); return; } if (!response.ok) throw new Error(`HTTP ${response.status}`); const status = await response.json(); if (!isCurrent()) return; if (status.vehicle_id && String(status.vehicle_id) !== poll.vehicleId) { throw new Error("时间轴状态车型不匹配"); } if ((poll.runId && status.run_id && String(status.run_id) !== poll.runId) || (poll.sessionId && status.session_id && String(status.session_id) !== poll.sessionId)) { setActionStatus(message, "当前时间轴已被新的运行任务替换,请查看最新模式消息。", "working"); stopScheduleStatusPoll(); return; } const terminal = ["completed", "failed", "cancelled", "stopped", "idle"].includes(status.status); if (status.status === "running") { const phase = Number.isInteger(status.phase_index) && status.phase_index >= 0 ? ` · 阶段 ${status.phase_index + 1}` : ""; const repeat = Number.isInteger(status.repeat_index) && status.repeat_index >= 0 ? ` · 第 ${status.repeat_index + 1} 轮` : ""; setActionStatus(message, `动态时间轴执行中${repeat}${phase}。可输入“停止动态模式”;已执行状态不自动回滚。`, "working"); } else if (terminal) { const stopped = ["cancelled", "stopped"].includes(status.status); const failed = status.status === "failed"; const copy = failed ? `动态时间轴执行失败:${status.error || "某个阶段未通过执行校验"}。后续阶段不再执行,已执行状态不自动回滚。` : stopped ? "动态时间轴已停止,后续阶段不再执行;已执行状态不自动回滚。" : status.status === "completed" ? "动态时间轴已结束,全部计划阶段已处理;请以车辆反馈确认设备实际状态。" : "当前没有正在运行的动态时间轴。"; setActionStatus(message, copy, failed ? "failed" : "success"); window.CognitiveAgentConsole?.updateScheduleExecutionStatus?.(poll.modeProfileId, status.status); stopScheduleStatusPoll(); return; } } catch (error) { if (!isCurrent() || poll.controller?.signal.aborted) return; setActionStatus(message, "暂时无法获取时间轴状态,正在重新查询;需要停止时可输入“停止动态模式”。", "working"); } finally { if (isCurrent()) poll.timer = window.setTimeout(update, 2000); } }; poll.timer = window.setTimeout(update, 2000); } function applyAssistantEvent(event, message) { if (event.type === "validated_control_commands" && event.payload) { window.dispatchEvent(new CustomEvent( "cognitive-agent:validated-can-commands", { detail: event.payload }, )); return; } if (event.type === "answer.delta") { setMessageContent(message, `${message.content}${event.delta || ""}`); return; } if (["action.started", "action.progress", "work.started", "work.progress"].includes(event.type)) { const defaultText = event.type.startsWith("work.") ? "AI助手正在处理工作……" : "正在执行座舱控制……"; setActionStatus(message, event.display_text || defaultText, "working"); if (event.type === "action.progress" && event.payload) { window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-control-activity", { detail: event.payload, })); } return; } if (event.type === "action.completed" || event.type === "work.completed") { latestControlResult = event.payload || latestControlResult; const completed = event.success !== false; const scheduleRunning = completed && event.payload?.orchestration_route === "mode_schedule_execute" && (event.payload?.status === "running" || event.payload?.schedule_execution?.status === "running"); const defaultText = event.type === "work.completed" ? (completed ? "AI助手已完成工作" : "AI助手未能完成工作") : (completed ? "座舱控制已完成" : "座舱控制未完成"); setActionStatus( message, scheduleRunning ? "已启动动态时间轴;后续动作将分阶段执行。可输入“停止动态模式”提前结束,已执行的设备状态不会自动回滚。" : event.display_text || defaultText, scheduleRunning ? "working" : completed ? "success" : "failed", ); if (scheduleRunning) startScheduleStatusPoll(event.payload, message); if (event.payload) { window.CognitiveAgentConsole?.applySyncedResult?.(event.payload); if ( event.payload.mode_profile_draft && event.payload.orchestration_route === "mode_profile_create_preview" ) { appendModeDraftState( message, event.payload.mode_profile_draft, event.payload.actions, ); } else if ( event.payload.mode_profile && ["mode_profile_create", "mode_profile_update"].includes( event.payload.orchestration_route, ) ) { 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") { const error = new Error(event.message || "请求处理失败"); error.code = event.error; throw error; } } async function streamAssistant(messageText, assistantMessage, signal) { const response = await window.CognitiveAgentNetwork.request("/api/assistant/stream", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/x-ndjson", }, body: JSON.stringify(requestBody(messageText)), timeoutMs: 15 * 60 * 1000, retries: 0, signal, }); if (!response.ok) throw new Error(await readErrorResponse(response)); if (!response.body?.getReader) throw new Error("当前浏览器不支持流式问答"); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffered = ""; let answerCompleted = false; const abortError = () => { const error = new Error("助手响应已取消"); error.name = "AbortError"; return error; }; const cancelReader = () => { // CognitiveAgentNetwork resolves once response headers arrive. Cancelling // the body reader keeps the Stop button effective for the remaining // long-lived NDJSON stream as well. reader.cancel(signal.reason).catch(() => {}); }; signal.addEventListener("abort", cancelReader, { once: true }); try { if (signal.aborted) { await reader.cancel(signal.reason).catch(() => {}); throw abortError(); } while (true) { const { done, value } = await reader.read(); if (signal.aborted) throw abortError(); buffered += decoder.decode(value || new Uint8Array(), { stream: !done }); const lines = buffered.split("\n"); buffered = done ? "" : lines.pop() || ""; for (const line of lines) { if (!line.trim()) continue; const event = JSON.parse(line); applyAssistantEvent(event, assistantMessage); if (event.type === "answer.completed") answerCompleted = true; } if (done) break; } if (buffered.trim()) { const event = JSON.parse(buffered); applyAssistantEvent(event, assistantMessage); if (event.type === "answer.completed") answerCompleted = true; } if (!answerCompleted) throw new Error("助手响应意外中断,请重试。"); } finally { signal.removeEventListener("abort", cancelReader); reader.releaseLock(); } } async function submitAssistant(messageOverride = null) { if (pendingController) { if (typeof messageOverride === "string") return; pendingController.abort("user_stop"); return; } const input = $("#utterance"); const speechPhase = window.CognitiveAgentSpeech?.phase; const speechInputMode = window.CognitiveAgentSpeech?.inputMode; if (["capturing", "finalizing"].includes(speechPhase) || (speechPhase === "arming" && speechInputMode === "manual")) { window.CognitiveAgentConsole?.notify?.("请等待语音整理完成后再发送"); return; } const suggestedMessage = typeof messageOverride === "string" ? messageOverride.trim() : ""; const messageText = suggestedMessage || input?.value.trim() || ""; if (!messageText) { window.CognitiveAgentConsole?.notify?.("请输入问题或工作需求"); return; } window.dispatchEvent(new CustomEvent( "cognitive-agent:control-request-start", { detail: { utterance: messageText, speech_utterance_id: suggestedMessage ? "" : String(input?.dataset.speechUtteranceId || ""), speech_value: suggestedMessage ? "" : String(input?.dataset.speechValue || ""), }, }, )); createMessage("user", messageText); 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; pendingController = new AbortController(); setComposerPending(true); let completedError = null; try { await streamAssistant(messageText, assistantMessage, pendingController.signal); setMessageContent(assistantMessage, assistantMessage.content, { final: true }); persistMessages(); } catch (error) { completedError = error; if (pendingController.signal.aborted) { const stopped = assistantMessage.content ? `${assistantMessage.content}\n\n已停止继续生成。已下发的车辆操作仍会由安全执行链路完成。` : "已停止生成。已下发的车辆操作仍会由安全执行链路完成。"; setMessageContent(assistantMessage, stopped, { final: true }); setActionStatus(assistantMessage, "已停止显示", "failed"); } else { setMessageContent(assistantMessage, error.message || "请求处理失败,请稍后再试。", { final: true, }); setActionStatus(assistantMessage, "本次请求未完成", "failed"); } persistMessages(); } finally { pendingController = null; pendingMessage = null; setComposerPending(false); input.focus({ preventScroll: true }); window.dispatchEvent(new CustomEvent("cognitive-agent:request-complete", { detail: { utterance: messageText, result: latestControlResult, error: completedError, }, })); } } function renderInitialMessages() { const container = $("#assistantMessages"); if (!container) return; const stored = loadStoredMessages(); container.replaceChildren(); createMessage("assistant", WELCOME_TEXT); stored.forEach((message) => createMessage(message.role, String(message.content || ""))); } function initialize() { if (initialized || !$("#requestForm") || !$("#assistantMessages")) return; initialized = true; document.body.classList.add("assistant-chat-enabled"); const title = $(".v2-operation-heading h2"); if (title) title.textContent = "AI助手"; const input = $("#utterance"); input.value = ""; input.placeholder = "向 AI助手提问或交代工作"; input.setAttribute("aria-label", "向 AI助手提问或交代工作"); installAssistantDrawer(); input.addEventListener("input", () => resizeComposerInput(input)); resizeComposerInput(input); // Some browsers restore the last submitted textarea value after // DOMContentLoaded. Clear that restoration once the initial page settles so // a reload cannot accidentally resend an already handled vehicle command. window.setTimeout(() => { if (pendingController) return; input.value = ""; resizeComposerInput(input); }, 300); renderInitialMessages(); activeModeDraftContext = loadActiveModeDraftContext(); setComposerPending(false); } document.addEventListener("submit", (event) => { if (!(event.target instanceof HTMLFormElement) || event.target.id !== "requestForm") return; event.preventDefault(); event.stopImmediatePropagation(); submitAssistant(); }, true); document.addEventListener("click", (event) => { const target = event.target instanceof Element ? event.target : null; if (!target?.closest("#newConversation")) return; stopScheduleStatusPoll(); sessionStorage.removeItem(STORAGE_KEY); clearActiveModeDraftContext(); sessionStorage.setItem(CONVERSATION_STORAGE_KEY, createConversationId()); }, true); document.addEventListener("change", (event) => { if (event.target?.id === "vehicleSelect") stopScheduleStatusPoll(); }); window.addEventListener("pagehide", stopScheduleStatusPoll); document.addEventListener("DOMContentLoaded", initialize); window.CognitiveAgentAssistant = { submit: submitAssistant, open: () => setDrawerOpen(true), close: () => setDrawerOpen(false), startModeCreation, announceModeCreated(profile) { if (!profile?.id) return; setDrawerOpen(true); const message = createMessage("assistant", `已完成「${profile.name || "新功能模式"}」的配置。`); appendModeCreatedState(message, profile); persistMessages(); }, stop() { pendingController?.abort("user_stop"); }, isPending: () => Boolean(pendingMessage), }; if (window.__N7WEB_ASSISTANT_TEST__ === true) { window.CognitiveAgentAssistant.__test = { setDrawerOpen }; } }());