From f4ab5f4867c52635b2ad38b8049d5dfcd074591b Mon Sep 17 00:00:00 2001 From: Miayui <774713819@qq.com> Date: Tue, 22 Sep 2026 14:17:50 +0800 Subject: [PATCH] fix(web): unify mode previews and label window positions --- apps/web-console/assistant-chat.css | 11 +++ apps/web-console/assistant-chat.js | 98 +++++++++++++------ apps/web-console/index.html | 2 +- apps/web-console/service-worker.js | 8 +- apps/web-console/vue-ui.js | 23 ++++- tests/e2e/test_seat_web_api.py | 10 +- .../unit/test_assistant_frontend_contract.py | 11 ++- tests/unit/test_assistant_mode_draft_rows.py | 54 +++++++++- .../test_energy_monitor_frontend_contract.py | 4 +- .../test_frontend_performance_contract.py | 2 +- tests/unit/test_frontend_theme_contract.py | 2 +- tests/unit/test_vue_frontend_contract.py | 2 +- 12 files changed, 176 insertions(+), 51 deletions(-) diff --git a/apps/web-console/assistant-chat.css b/apps/web-console/assistant-chat.css index 6f71716..2fe2ad0 100644 --- a/apps/web-console/assistant-chat.css +++ b/apps/web-console/assistant-chat.css @@ -1760,6 +1760,17 @@ border-top: 1px solid color-mix(in oklch, var(--primary) 17%, var(--border)); } +.assistant-dispatched-controls-list[hidden] { display: none; } +.assistant-dispatched-controls-footer:empty { display: none; } +.assistant-dispatched-controls .assistant-mode-draft-inline { + width: 100%; + max-width: none; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + .assistant-dispatched-controls-list > li { min-height: 55px; display: grid; diff --git a/apps/web-console/assistant-chat.js b/apps/web-console/assistant-chat.js index cd372d4..56d2639 100644 --- a/apps/web-console/assistant-chat.js +++ b/apps/web-console/assistant-chat.js @@ -553,6 +553,17 @@ function assistantControlCategory(operation = {}, fallback = "控制指令") { const capability = String(operation.capability || ""); + if (capability.startsWith("window.")) { + const target = String(operation.target?.zone || operation.target?.instance + || operation.target_scope || (typeof operation.target === "string" ? operation.target : "")) + .replaceAll("-", "_"); + return { + front_left: "左前车窗(主驾)", driver: "左前车窗(主驾)", + front_right: "右前车窗(副驾)", passenger: "右前车窗(副驾)", + rear_left: "左后车窗", rear_right: "右后车窗", + all: "全部车窗", front: "前排车窗", rear: "后排车窗", + }[target] || "车窗(位置待确认)"; + } if (capability === "infrared.emit") { const deviceName = String( operation.target?.name || operation.target?.device || "红外外设", @@ -673,8 +684,30 @@ commands = []; } if (!commands.length) commands = fallbackAssistantControlCommands(result); + if (result.mode_profile_draft && Array.isArray(result.actions)) { + const operationSignature = (operation = {}) => { + const rawTarget = String(operation.target?.instance || operation.target?.zone + || operation.target?.device || operation.target_scope + || (typeof operation.target === "string" ? operation.target : "")) + .replaceAll("-", "_"); + const target = ({ passenger: "front_right", driver: "front_left" })[rawTarget] || rawTarget; + const parameters = operation.parameters || {}; + return JSON.stringify([operation.capability, target, + Object.keys(parameters).sort().map((key) => [key, parameters[key]])]); + }; + const presentationByAction = new Map(projections.map((presentation) => [ + operationSignature(presentation.operation), presentation, + ])); + commands = result.actions.filter((action) => action && typeof action === "object") + .map((operation, index) => ({ + operation, operationRef: `draft-action-${index}`, + semantic: String(operation.capability || ""), + command: String(presentationByAction.get(operationSignature(operation))?.actual_command || ""), + })); + } return commands.map((item, index) => { - const operation = projectionByRef.get(String(item.operationRef || "")) + const operation = item.operation + || projectionByRef.get(String(item.operationRef || "")) || operationByRef.get(String(item.operationRef || "")) || projections[index]?.operation || {}; @@ -737,7 +770,13 @@ const temperature = numericValue(parameters.temperature_c); if (temperature !== null) return `${temperature}°C`; const position = numericValue(parameters.position_pct ?? parameters.shade_position_pct); - if (position !== null) return `${position}%`; + if (position !== null) { + if (String(item.operation?.capability || "").startsWith("window.")) { + return position === 0 ? "0%(完全关闭)" + : position === 100 ? "100%(完全打开)" : `${position}%`; + } + return `${position}%`; + } const illumination = String(parameters.color_hex || "").trim(); const brightnessPercent = numericValue(parameters.brightness_pct); if (illumination && brightnessPercent !== null) return `${illumination} · ${brightnessPercent}%`; @@ -816,17 +855,20 @@ if (!commands.length) return; const previewOnly = String(mergedResult.execution_mode || "dry_run") !== "hardware" || mergedResult.orchestration_route === "mode_profile_create_preview"; - const headingText = previewOnly ? "控制指令预览(未下发)" : "下发控制指令"; + const headingText = mergedResult.mode_profile_draft?.name + ? `${mergedResult.mode_profile_draft.name}(未下发)` + : previewOnly ? "控制指令预览(未下发)" : "下发控制指令"; - const signature = JSON.stringify(commands.map((item) => [ + const signature = JSON.stringify([headingText, commands.map((item) => [ item.operationRef, item.category, item.command, item.semantic, + assistantControlValue(item), item.deliveryState, item.deliveryLabel, item.deliveryDetail, - ])); + ])]); const vueRenderer = window.CognitiveAgentVueUI?.renderDispatchedControls; if (typeof vueRenderer === "function") { let host = $(".assistant-dispatched-controls-vue-host", message.body); @@ -1114,8 +1156,8 @@ all: "全部车窗", front: "前排车窗", rear: "后排车窗", - front_left: "主驾车窗", - front_right: "副驾车窗", + front_left: "左前车窗(主驾)", + front_right: "右前车窗(副驾)", rear_left: "左后车窗", rear_right: "右后车窗", }; @@ -1383,27 +1425,15 @@ rememberModeDraftContext(draft); supersedeModeDraftCards(draft); const selector = `[data-assistant-mode-draft="${CSS.escape(String(draft.source_request_id))}"]`; - if ($(selector, message.body)) return; + if ($(selector, message.body) + || message.modeDraftCard?.dataset.assistantModeDraft === String(draft.source_request_id)) return; const card = document.createElement("section"); - card.className = "assistant-mode-draft"; + card.className = "assistant-mode-draft assistant-mode-draft-inline"; 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"; + message.modeDraftCard = card; 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"; @@ -1431,12 +1461,10 @@ hint.className = "assistant-mode-schedule-hint"; hint.textContent = scheduleView.stopHint; list.append(hint); - } else { - appendModeDraftRows(list, modeDraftRows(draft, actions)); } const previewNotice = document.createElement("p"); previewNotice.className = "assistant-mode-draft-preview-notice"; - previewNotice.textContent = "创建阶段仅生成并校验控制方案,不会向 CAN 或红外外设发送指令。"; + previewNotice.textContent = `${draft.existing_mode_id ? "已载入现有功能模式。" : ""}创建阶段仅生成并校验控制方案,不会向 CAN 或红外外设发送指令。`; list.append(previewNotice); const controls = document.createElement("div"); @@ -1464,8 +1492,22 @@ void tryModeProfileDraft(card, draft, actions, buttons, status); }); - card.append(heading, list, controls, status); - message.body.append(card); + card.append(list, controls, status); + appendDispatchedControls(message, { + mode_profile_draft: draft, actions, execution_mode: "dry_run", + orchestration_route: "mode_profile_create_preview", + }); + const host = $(".assistant-dispatched-controls-vue-host", message.body); + const attached = host && window.CognitiveAgentVueUI?.setDispatchedControlsFooter?.( + host, card, { hideCommands: Boolean(scheduleView) }, + ); + if (!attached) { + const panel = $(".assistant-dispatched-controls", message.body); + if (!panel) return; + const commandList = $(".assistant-dispatched-controls-list", panel); + if (commandList) commandList.hidden = Boolean(scheduleView); + panel.append(card); + } window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); followLatest(true); } diff --git a/apps/web-console/index.html b/apps/web-console/index.html index ce87cb3..1a8a1d7 100755 --- a/apps/web-console/index.html +++ b/apps/web-console/index.html @@ -45,7 +45,7 @@ - + diff --git a/apps/web-console/service-worker.js b/apps/web-console/service-worker.js index 5c8fce4..573bdfe 100644 --- a/apps/web-console/service-worker.js +++ b/apps/web-console/service-worker.js @@ -1,4 +1,4 @@ -const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"; +const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`; const APP_SHELL = [ @@ -16,13 +16,13 @@ const APP_SHELL = [ "/speech-input.css?v=sensevoice-7", "/speech-input.js?v=sensevoice-13", "/speech-audio-worklet.js?v=sensevoice-5", - "/assistant-chat.css?v=codex-assistant-30", - "/assistant-chat.js?v=codex-assistant-40-seat-rows", + "/assistant-chat.css?v=codex-assistant-31-unified-preview", + "/assistant-chat.js?v=codex-assistant-41-unified-preview", "/energy-monitor.css?v=energy-monitor-5-dialog-size", "/energy-monitor.js?v=energy-monitor-5-performance", "/ui-consistency.css?v=spatial-ui-8", "/codepen-glass-theme.css?v=creative-cloud-6-performance", - "/vue-ui.js?v=vue-presentation-4", + "/vue-ui.js?v=vue-presentation-5-draft-footer", "/firmware-workbench-entry.css?v=2", "/firmware-workbench-entry.js?v=3", "/firmware-workbench.html", diff --git a/apps/web-console/vue-ui.js b/apps/web-console/vue-ui.js index 1c12813..dfe20c1 100644 --- a/apps/web-console/vue-ui.js +++ b/apps/web-console/vue-ui.js @@ -111,7 +111,7 @@ import { Fragment, createApp, h, nextTick, reactive } from "vue"; } function createCommandMount(container) { - const state = reactive({ commands: [], open: true, heading: "下发控制指令" }); + const state = reactive({ commands: [], open: true, heading: "下发控制指令", footer: null, hideCommands: false }); const app = createApp({ name: "N7DispatchedControlsCard", setup() { @@ -138,7 +138,16 @@ import { Fragment, createApp, h, nextTick, reactive } from "vue"; "data-lucide": "chevron-down", }), ]), - h("ol", { class: "assistant-dispatched-controls-list" }, state.commands.map(renderCommandRow)), + h("ol", { class: "assistant-dispatched-controls-list", hidden: state.hideCommands }, state.commands.map(renderCommandRow)), + h("div", { + class: "assistant-dispatched-controls-footer", + hidden: !state.footer, + ref(element) { + if (element && state.footer && state.footer.parentNode !== element) { + element.replaceChildren(state.footer); + } + }, + }), ]); }, }); @@ -160,12 +169,22 @@ import { Fragment, createApp, h, nextTick, reactive } from "vue"; return true; } + function setDispatchedControlsFooter(container, footer, options = {}) { + const mount = commandMounts.get(container); + if (!mount || !(footer instanceof Element)) return false; + mount.state.footer = footer; + mount.state.hideCommands = Boolean(options.hideCommands); + refreshIcons(); + return true; + } + document.documentElement.classList.add("vue-presentation-enabled"); document.documentElement.dataset.vueVersion = VUE_VERSION; window.CognitiveAgentVueUI = Object.freeze({ available: true, version: VUE_VERSION, renderDispatchedControls, + setDispatchedControlsFooter, renderModeChips, }); }()); diff --git a/tests/e2e/test_seat_web_api.py b/tests/e2e/test_seat_web_api.py index a399233..01747fc 100755 --- a/tests/e2e/test_seat_web_api.py +++ b/tests/e2e/test_seat_web_api.py @@ -1071,7 +1071,7 @@ operations: ) as response: extension_styles = response.read().decode("utf-8").replace("\r\n", "\n") with self.opener.open( - f"{self.base_url}/assistant-chat.js?v=codex-assistant-40-seat-rows", timeout=3 + f"{self.base_url}/assistant-chat.js?v=codex-assistant-41-unified-preview", timeout=3 ) as response: assistant_script = response.read().decode("utf-8") with self.opener.open( @@ -1156,7 +1156,7 @@ operations: self.assertIn('id="assistantTranscript"', template) self.assertIn('import "../assistant-chat.css";', vite_entry) self.assertIn( - f'src="{runtime_url("assistant-chat.js", "/assistant-chat.js?v=codex-assistant-40-seat-rows")}"', + f'src="{runtime_url("assistant-chat.js", "/assistant-chat.js?v=codex-assistant-41-unified-preview")}"', html, ) self.assertIn('import "../energy-monitor.css";', vite_entry) @@ -1472,13 +1472,13 @@ operations: self.assertIn(f'/{runtime_assets[asset_name]}', service_worker) else: self.assertIn( - 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"', + 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"', service_worker, ) self.assertIn('/console-v2.css?v=delivery-state-69', service_worker) self.assertIn('/console-v2.js?v=local-first-79-cinema-status', service_worker) - self.assertIn('/assistant-chat.css?v=codex-assistant-30', service_worker) - self.assertIn('/assistant-chat.js?v=codex-assistant-40-seat-rows', service_worker) + self.assertIn('/assistant-chat.css?v=codex-assistant-31-unified-preview', service_worker) + self.assertIn('/assistant-chat.js?v=codex-assistant-41-unified-preview', service_worker) self.assertIn('/energy-monitor.css?v=energy-monitor-5-dialog-size', service_worker) self.assertIn('/energy-monitor.js?v=energy-monitor-5-performance', service_worker) self.assertIn('"/app.js?v=dual-seat-148-seat-preset"', service_worker) diff --git a/tests/unit/test_assistant_frontend_contract.py b/tests/unit/test_assistant_frontend_contract.py index 8566814..fccab6c 100644 --- a/tests/unit/test_assistant_frontend_contract.py +++ b/tests/unit/test_assistant_frontend_contract.py @@ -21,17 +21,17 @@ def test_console_contains_ai_assistant_surface(): assert 'placeholder="向 AI助手提问或交代工作"' in template assert 'aria-label="发送消息"' in template assert 'import "../assistant-chat.css";' in vite_entry - assert 'src="/assistant-chat.js?v=codex-assistant-40-seat-rows"' in html + assert 'src="/assistant-chat.js?v=codex-assistant-41-unified-preview"' in html def test_codex_assistant_assets_are_versioned_together_in_the_service_worker(): service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8") assert ( - 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"' + 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"' in service_worker ) - assert '"/assistant-chat.css?v=codex-assistant-30"' in service_worker - assert '"/assistant-chat.js?v=codex-assistant-40-seat-rows"' in service_worker + assert '"/assistant-chat.css?v=codex-assistant-31-unified-preview"' in service_worker + assert '"/assistant-chat.js?v=codex-assistant-41-unified-preview"' in service_worker def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events(): @@ -91,7 +91,8 @@ def test_validated_controls_are_mirrored_into_a_compact_assistant_card(): assert "window.CognitiveAgentConsoleV2?.getParsedCanCommands?.(result)" in script assert "fallbackAssistantControlCommands(result)" in script assert 'card.className = "assistant-dispatched-controls"' in script - assert 'const headingText = previewOnly ? "控制指令预览(未下发)" : "下发控制指令"' in script + assert 'const headingText = mergedResult.mode_profile_draft?.name' in script + assert 'previewOnly ? "控制指令预览(未下发)" : "下发控制指令"' in script assert 'count.className = "assistant-dispatched-controls-count"' in script assert 'list.className = "assistant-dispatched-controls-list"' in script assert "value.textContent = assistantControlValue(item)" in script diff --git a/tests/unit/test_assistant_mode_draft_rows.py b/tests/unit/test_assistant_mode_draft_rows.py index 9319ad2..ad042bc 100644 --- a/tests/unit/test_assistant_mode_draft_rows.py +++ b/tests/unit/test_assistant_mode_draft_rows.py @@ -204,7 +204,7 @@ def test_default_bundle_rows_show_each_passenger_action_separately(render_rows): assert [(row["label"], row["value"]) for row in rows] == [ ("空调电源", "开启"), - ("主驾车窗", "完全关闭(目标开度 0%)"), + ("左前车窗(主驾)", "完全关闭(目标开度 0%)"), ("副驾座椅通风", "开启 · 1档"), ("副驾靠背位置", "45%"), ("副驾腿托位置", "35%"), @@ -227,3 +227,55 @@ def test_cinema_draft_shows_all_four_axes_and_ventilation(render_rows): ("副驾腿托位置", "53%"), ("副驾座椅通风", "关闭"), ] + + +def test_collapsible_preview_keeps_all_actions_and_identifies_each_window(): + node = shutil.which("node") + assert node + source = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8") + start = source.index(" const assistantControlLabels =") + end = source.index(" function focusLatestDispatchedControls", start) + actions = [ + action("seat.set_longitudinal_position", {"position_pct": 51}, "passenger"), + action("seat.set_cushion_tilt", {"position_pct": 30}, "passenger"), + action("seat.set_backrest_recline", {"position_pct": 44}, "passenger"), + action("seat.set_legrest_height", {"position_pct": 53}, "passenger"), + action("seat.set_ventilation", {"state": "off"}, "passenger"), + *(action("window.set_position", {"position_pct": value}, zone) + for zone, value in [("front_left", 0), ("front_right", 50), + ("rear_left", 100), ("rear_right", 0)]), + action("climate.set_power", {"state": "on"}, "vehicle"), + action("ambient_light.set_power", {"state": "on"}, "vehicle"), + ] + payload = { + "execution_mode": "dry_run", "mode_profile_draft": {"name": "浪漫影院模式"}, + "actions": actions, + # IDs can collide across domains, and seat display projections may be absent. + "validated_control_commands": [{ + "operation": {**operation, "id": "shared-operation-id"}, + "actual_command": f"command-{index}", + } for index, operation in enumerate(actions[5:])], + } + script = "const window = {};\n" + source[start:end] + "\n" + r''' +const payload = JSON.parse(require('node:fs').readFileSync(0, 'utf8')); +const show = (value) => assistantDispatchedCommands(value).map(item => ({ + label: item.category, value: assistantControlValue(item), command: item.command, +})); +const preview = show(payload); +delete payload.validated_control_commands; +console.log(JSON.stringify({preview, withoutProjections: show(payload)})); +''' + output = subprocess.run([node, "-e", script], input=json.dumps(payload), + check=True, capture_output=True, text=True, encoding="utf-8") + results = json.loads(output.stdout) + for rows in (results["preview"], results["withoutProjections"]): + assert len(rows) == len(actions) + assert [row["value"] for row in rows[:5]] == ["51%", "30%", "44%", "53%", "关闭"] + assert [(row["label"], row["value"]) for row in rows[5:9]] == [ + ("左前车窗(主驾)", "0%(完全关闭)"), ("右前车窗(副驾)", "50%"), + ("左后车窗", "100%(完全打开)"), ("右后车窗", "0%(完全关闭)"), + ] + assert [row["label"] for row in rows[-2:]] == ["空调电源", "氛围灯"] + assert [row["command"] for row in results["preview"][5:]] == [ + f"command-{index}" for index in range(6) + ] diff --git a/tests/unit/test_energy_monitor_frontend_contract.py b/tests/unit/test_energy_monitor_frontend_contract.py index 3236d9d..3e1e717 100644 --- a/tests/unit/test_energy_monitor_frontend_contract.py +++ b/tests/unit/test_energy_monitor_frontend_contract.py @@ -37,11 +37,11 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None: assert f'"{asset}"' in service_worker assert 'import "../energy-monitor.css";' in vite_entry assert "/energy-monitor.js?v=energy-monitor-5-performance" in html - assert html.index("/assistant-chat.js?v=codex-assistant-40-seat-rows") < html.index( + assert html.index("/assistant-chat.js?v=codex-assistant-41-unified-preview") < html.index( "/energy-monitor.js?v=energy-monitor-5-performance" ) assert ( - 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"' + 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"' in service_worker ) diff --git a/tests/unit/test_frontend_performance_contract.py b/tests/unit/test_frontend_performance_contract.py index 39bfa5e..04fd6a1 100644 --- a/tests/unit/test_frontend_performance_contract.py +++ b/tests/unit/test_frontend_performance_contract.py @@ -99,5 +99,5 @@ def test_performance_assets_have_a_new_cache_generation() -> None: ): assert asset in html assert f'"{asset}"' in worker - assert 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"' in worker + assert 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"' in worker assert '"/codepen-glass-theme.css?v=creative-cloud-6-performance"' in worker diff --git a/tests/unit/test_frontend_theme_contract.py b/tests/unit/test_frontend_theme_contract.py index 09be8e6..5070234 100644 --- a/tests/unit/test_frontend_theme_contract.py +++ b/tests/unit/test_frontend_theme_contract.py @@ -54,7 +54,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None: assert source_asset in main assert f'"{cached_asset}"' in service_worker assert 'import "./styles/design-system.css"' in main - assert 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"' in service_worker + assert 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"' in service_worker def test_reference_glass_theme_covers_the_complete_workspace() -> None: diff --git a/tests/unit/test_vue_frontend_contract.py b/tests/unit/test_vue_frontend_contract.py index 1ef19fc..f64b3a2 100644 --- a/tests/unit/test_vue_frontend_contract.py +++ b/tests/unit/test_vue_frontend_contract.py @@ -31,7 +31,7 @@ def test_vue_vite_application_is_the_primary_frontend_entry() -> None: assert package["scripts"]["build"] == "vite build" assert 'import vue from "@vitejs/plugin-vue"' in vite assert 'outDir: outputRoot' in vite - assert 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"' in service_worker + assert 'const CACHE_VERSION = "cognitive-agent-pad-v338-unified-preview"' in service_worker assert 'createHash("sha256")' in vite assert 'runtime-assets.json' in vite assert 'rewriteRuntimeReferences' in vite