- ${rows.map(([label, value]) => `
- ${escapeHtml(label)}
- ${escapeHtml(value)}
diff --git a/apps/web-console/console-v2.css b/apps/web-console/console-v2.css
index f419a0c..caad520 100644
--- a/apps/web-console/console-v2.css
+++ b/apps/web-console/console-v2.css
@@ -3178,46 +3178,140 @@
}
.console-v2 .v2-twin-hotspots button {
+ --v2-hotspot-label-x: 0px;
+ --v2-hotspot-label-y: -52px;
+ --v2-hotspot-line-length: 52px;
+ --v2-hotspot-line-angle: -90deg;
+ --v2-hotspot-label-scale: 1;
+ width: 14px;
+ height: 14px;
position: absolute;
- display: flex;
- align-items: center;
- gap: 4px;
- padding: 5px 7px;
- color: #164846;
- background: rgba(242, 252, 250, 0.92);
- border: 1px solid rgba(12, 119, 111, 0.2);
- border-radius: 999px;
- box-shadow: 0 7px 18px rgba(22, 82, 78, 0.13);
- font-size: 8px;
- font-weight: 850;
+ display: block;
+ padding: 0;
+ color: #ffffff;
+ background: transparent;
+ border: 0;
+ border-radius: 50%;
+ box-shadow: none;
cursor: pointer;
pointer-events: auto;
transform: translate(-50%, -50%);
- transition: transform 150ms ease, background 150ms ease, color 150ms ease;
+ overflow: visible;
+}
+
+.console-v2 .v2-twin-hotspots button::before {
+ content: "";
+ width: var(--v2-hotspot-line-length);
+ height: 1.5px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: 0;
+ background: linear-gradient(90deg, #12a69b, rgba(18, 166, 155, 0.55));
+ box-shadow: 0 0 7px rgba(18, 166, 155, 0.24);
+ transform: rotate(var(--v2-hotspot-line-angle));
+ transform-origin: 0 50%;
+ pointer-events: auto;
+ transition: height 120ms ease, background 120ms ease, box-shadow 120ms ease;
+}
+
+.console-v2 .v2-twin-hotspots button::after {
+ content: "";
+ width: 8px;
+ height: 8px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: 3;
+ background: #11b5aa;
+ border: 2px solid rgba(255, 255, 255, 0.94);
+ border-radius: 50%;
+ box-shadow: 0 0 0 3px rgba(17, 181, 170, 0.17), 0 2px 8px rgba(17, 105, 99, 0.28);
+ transform: translate(-50%, -50%);
+ transition: transform 120ms ease, background 120ms ease, box-shadow 120ms ease;
+}
+
+.console-v2 .v2-twin-hotspot-label {
+ min-width: 78px;
+ min-height: 34px;
+ position: absolute;
+ top: calc(50% + var(--v2-hotspot-label-y));
+ left: calc(50% + var(--v2-hotspot-label-x));
+ z-index: 2;
+ display: grid;
+ grid-template-columns: 22px minmax(0, 1fr);
+ grid-template-rows: auto auto;
+ align-items: center;
+ gap: 1px 6px;
+ padding: 6px 8px;
+ color: #f4fffd;
+ background: linear-gradient(145deg, rgba(19, 66, 72, 0.96), rgba(12, 48, 54, 0.94));
+ border: 1px solid rgba(151, 232, 222, 0.24);
+ border-radius: 9px;
+ box-shadow: 0 8px 20px rgba(21, 70, 69, 0.2);
+ text-align: left;
+ white-space: nowrap;
+ transform: translate(-50%, -50%) scale(var(--v2-hotspot-label-scale));
+ transform-origin: center;
+ backdrop-filter: blur(8px);
+ transition: transform 120ms ease, background 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
+}
+
+.console-v2 .v2-twin-hotspot-label svg {
+ width: 16px;
+ height: 16px;
+ grid-column: 1;
+ grid-row: 1 / 3;
+ color: #77e1d6;
+}
+
+.console-v2 .v2-twin-hotspot-label b {
+ grid-column: 2;
+ grid-row: 1;
+ font-size: 9px;
+ line-height: 1.15;
+}
+
+.console-v2 .v2-twin-hotspot-label small {
+ grid-column: 2;
+ grid-row: 2;
+ color: rgba(221, 246, 242, 0.7);
+ font-size: 7px;
+ font-weight: 700;
+ line-height: 1.1;
}
.console-v2 .v2-twin-hotspots button:hover,
.console-v2 .v2-twin-hotspots button:focus-visible,
.console-v2 .v2-twin-hotspots button.active {
z-index: 2;
- color: #fff;
- background: #0b7f78;
outline: none;
- transform: translate(-50%, -50%) scale(1.08);
+ --v2-hotspot-label-scale: 1.05;
}
-.console-v2 .v2-twin-hotspots button svg {
- width: 11px;
+.console-v2 .v2-twin-hotspots button:hover::before,
+.console-v2 .v2-twin-hotspots button:focus-visible::before,
+.console-v2 .v2-twin-hotspots button.active::before {
+ height: 2px;
+ background: #0d968d;
+ box-shadow: 0 0 9px rgba(13, 150, 141, 0.42);
}
-.console-v2 .v2-twin-hotspots [data-twin-device="windows"] { top: 48%; left: 8%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="shade"] { top: 16%; left: 45%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="driver"] { top: 46%; left: 63%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="passenger"] { top: 58%; left: 44%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="reading"] { top: 30%; left: 34%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="ambient"] { top: 78%; left: 47%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="headlight"] { top: 62%; left: 88%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="climate"] { top: 80%; left: 12%; }
+.console-v2 .v2-twin-hotspots button:hover::after,
+.console-v2 .v2-twin-hotspots button:focus-visible::after,
+.console-v2 .v2-twin-hotspots button.active::after {
+ background: #087a73;
+ box-shadow: 0 0 0 5px rgba(8, 122, 115, 0.18), 0 2px 9px rgba(8, 93, 88, 0.34);
+ transform: translate(-50%, -50%) scale(1.18);
+}
+
+.console-v2 .v2-twin-hotspots button:hover .v2-twin-hotspot-label,
+.console-v2 .v2-twin-hotspots button:focus-visible .v2-twin-hotspot-label,
+.console-v2 .v2-twin-hotspots button.active .v2-twin-hotspot-label {
+ background: linear-gradient(145deg, #0c8b83, #08716b);
+ border-color: rgba(196, 255, 246, 0.48);
+ box-shadow: 0 11px 24px rgba(8, 103, 96, 0.28);
+}
.console-v2 .v2-twin-device-popover {
width: min(360px, calc(100% - 34px));
@@ -3241,9 +3335,21 @@
display: none;
}
+.console-v2 .v2-twin-device-popover[data-pinned="false"] {
+ pointer-events: none;
+}
+
+.console-v2 .v2-twin-device-popover[data-pinned="false"] header {
+ grid-template-columns: 30px minmax(0, 1fr) auto;
+}
+
+.console-v2 .v2-twin-device-popover[data-pinned="false"] .v2-twin-popover-close {
+ display: none;
+}
+
.console-v2 .v2-twin-device-popover header {
display: grid;
- grid-template-columns: 30px minmax(0, 1fr) auto;
+ grid-template-columns: 30px minmax(0, 1fr) auto 28px;
align-items: center;
gap: 8px;
}
@@ -3282,6 +3388,34 @@
font-size: 8px;
}
+.console-v2 .v2-twin-popover-close {
+ width: 28px;
+ height: 28px;
+ display: inline-grid;
+ place-items: center;
+ padding: 0;
+ color: #557471;
+ background: #f1f7f6;
+ border: 1px solid rgba(16, 111, 104, 0.12);
+ border-radius: 9px;
+ cursor: pointer;
+ transition: color 150ms ease, background 150ms ease, border-color 150ms ease, transform 150ms ease;
+}
+
+.console-v2 .v2-twin-popover-close:hover,
+.console-v2 .v2-twin-popover-close:focus-visible {
+ color: #ffffff;
+ background: #118078;
+ border-color: #118078;
+ outline: none;
+ transform: scale(1.04);
+}
+
+.console-v2 .v2-twin-popover-close svg {
+ width: 14px;
+ height: 14px;
+}
+
.console-v2 .v2-twin-device-popover dl {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -6348,15 +6482,15 @@ body.device-power-drawer-open {
display: none !important;
}
-/* Hotspots follow the actual rendered vehicle: rear at left, front at right. */
-.console-v2 .v2-twin-hotspots [data-twin-device="windows"] { top: 50%; left: 42%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="shade"] { top: 25%; left: 53%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="driver"] { top: 45%; left: 62%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="passenger"] { top: 57%; left: 52%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="reading"] { top: 36%; left: 55%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="ambient"] { top: 62%; left: 66%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="headlight"] { top: 66%; left: 86%; }
-.console-v2 .v2-twin-hotspots [data-twin-device="climate"] { top: 48%; left: 73%; }
+/* Anchors follow the rendered vehicle; labels fan out along readable leader lines. */
+.console-v2 .v2-twin-hotspots [data-twin-device="windows"] { top: 50%; left: 42%; --v2-hotspot-label-x: -115px; --v2-hotspot-label-y: 0px; --v2-hotspot-line-length: 115px; --v2-hotspot-line-angle: 180deg; }
+.console-v2 .v2-twin-hotspots [data-twin-device="shade"] { top: 25%; left: 53%; --v2-hotspot-label-x: 0px; --v2-hotspot-label-y: -65px; --v2-hotspot-line-length: 65px; --v2-hotspot-line-angle: -90deg; }
+.console-v2 .v2-twin-hotspots [data-twin-device="driver"] { top: 45%; left: 62%; --v2-hotspot-label-x: 110px; --v2-hotspot-label-y: -70px; --v2-hotspot-line-length: 130px; --v2-hotspot-line-angle: -32deg; }
+.console-v2 .v2-twin-hotspots [data-twin-device="passenger"] { top: 57%; left: 52%; --v2-hotspot-label-x: -105px; --v2-hotspot-label-y: 55px; --v2-hotspot-line-length: 119px; --v2-hotspot-line-angle: 152deg; }
+.console-v2 .v2-twin-hotspots [data-twin-device="reading"] { top: 36%; left: 55%; --v2-hotspot-label-x: -120px; --v2-hotspot-label-y: -70px; --v2-hotspot-line-length: 139px; --v2-hotspot-line-angle: -150deg; }
+.console-v2 .v2-twin-hotspots [data-twin-device="ambient"] { top: 62%; left: 66%; --v2-hotspot-label-x: -30px; --v2-hotspot-label-y: 85px; --v2-hotspot-line-length: 90px; --v2-hotspot-line-angle: 109deg; }
+.console-v2 .v2-twin-hotspots [data-twin-device="headlight"] { top: 66%; left: 86%; --v2-hotspot-label-x: 15px; --v2-hotspot-label-y: 70px; --v2-hotspot-line-length: 72px; --v2-hotspot-line-angle: 78deg; }
+.console-v2 .v2-twin-hotspots [data-twin-device="climate"] { top: 48%; left: 73%; --v2-hotspot-label-x: 94px; --v2-hotspot-label-y: 5px; --v2-hotspot-line-length: 94px; --v2-hotspot-line-angle: 3deg; }
@media (max-width: 1200px) {
.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell.v2-output-page {
@@ -6633,3 +6767,786 @@ body.device-power-drawer-open {
transition: none;
}
}
+
+/* Actionable vehicle device status cards -------------------------------- */
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-sync-grid {
+ display: flex;
+ flex-direction: column;
+ align-content: start;
+ padding-right: 3px;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device {
+ flex: 0 0 auto;
+ min-height: 0;
+ display: block;
+ padding: 8px;
+ text-align: left;
+ cursor: default;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device:hover,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device.active {
+ transform: none;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device[data-control-open="true"] {
+ background: linear-gradient(155deg, #eff9f6, #fbfdfc);
+ border-color: rgba(13, 126, 117, 0.24);
+ box-shadow: 0 9px 22px rgba(29, 100, 94, 0.1);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device[data-has-draft="true"] {
+ border-color: rgba(209, 143, 44, 0.3);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-header {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 5px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus {
+ min-width: 0;
+ display: grid;
+ grid-template-columns: 28px minmax(0, 1fr) 6px;
+ align-items: center;
+ gap: 7px;
+ padding: 0;
+ color: inherit;
+ background: transparent;
+ border: 0;
+ text-align: left;
+ cursor: pointer;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus > span:first-child {
+ width: 28px;
+ height: 28px;
+ display: grid;
+ place-items: center;
+ color: #16877f;
+ background: #dff2ee;
+ border-radius: 9px;
+ transition: color 150ms ease, background 150ms ease;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus:hover > span:first-child,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus:focus-visible > span:first-child {
+ color: #ffffff;
+ background: #12857d;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus svg {
+ width: 14px;
+ height: 14px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus > span:nth-child(2) {
+ min-width: 0;
+ display: grid;
+ gap: 2px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus strong {
+ color: #1d3a3b;
+ font-size: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus small {
+ overflow: hidden;
+ color: #7a908e;
+ font-size: 7px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device[data-has-draft="true"] .v2-device-card-focus small {
+ color: #a16b1b;
+ font-weight: 800;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus .v2-device-signal {
+ width: 6px;
+ height: 6px;
+ background: #aebcba;
+ border-radius: 50%;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device[data-ready="true"] .v2-device-card-focus .v2-device-signal {
+ background: #18a783;
+ box-shadow: 0 0 0 3px rgba(24, 167, 131, 0.1);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-adjust-toggle {
+ min-width: 43px;
+ height: 25px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 3px;
+ padding: 0 6px;
+ color: #537572;
+ background: #f4f9f8;
+ border: 1px solid rgba(17, 108, 101, 0.12);
+ border-radius: 8px;
+ font-size: 7px;
+ font-weight: 850;
+ cursor: pointer;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-adjust-toggle:hover,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-adjust-toggle[aria-expanded="true"] {
+ color: #ffffff;
+ background: #117e77;
+ border-color: #117e77;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-adjust-toggle svg {
+ width: 11px;
+ height: 11px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dl {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 4px;
+ margin: 7px 0 0;
+ padding: 7px 0 0;
+ border-top: 1px solid rgba(35, 105, 99, 0.08);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dl div {
+ min-width: 0;
+ display: grid;
+ gap: 2px;
+ padding: 5px 6px;
+ background: rgba(239, 247, 245, 0.72);
+ border-radius: 7px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dl div:only-child {
+ grid-column: 1 / -1;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dt,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dd {
+ min-width: 0;
+ overflow: hidden;
+ margin: 0;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dt {
+ color: #80928f;
+ font-size: 6.5px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dd {
+ color: #294e4b;
+ font-size: 8px;
+ font-weight: 850;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-controls {
+ display: grid;
+ gap: 7px;
+ margin-top: 8px;
+ padding: 8px;
+ background: linear-gradient(155deg, rgba(224, 243, 239, 0.9), rgba(242, 249, 247, 0.96));
+ border: 1px solid rgba(20, 117, 109, 0.12);
+ border-radius: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-controls[hidden] {
+ display: none !important;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-controls-intro {
+ display: grid;
+ gap: 2px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid rgba(22, 111, 104, 0.1);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-controls-intro span {
+ color: #166f69;
+ font-size: 8px;
+ font-weight: 900;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-controls-intro small {
+ color: #718783;
+ font-size: 6.5px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-row {
+ min-width: 0;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 4px 7px;
+ color: #456764;
+ font-size: 7px;
+ font-weight: 800;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-row output {
+ color: #0e756f;
+ font: 850 7px/1 "SFMono-Regular", Consolas, monospace;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-row input[type="range"] {
+ width: 100%;
+ height: 15px;
+ grid-column: 1 / -1;
+ margin: 0;
+ accent-color: #0f8179;
+ cursor: pointer;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-row > small {
+ grid-column: 1 / -1;
+ color: #899a97;
+ font-size: 6px;
+ font-weight: 650;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-select select {
+ width: 82px;
+ height: 25px;
+ padding: 0 5px;
+ color: #2f5753;
+ background: rgba(255, 255, 255, 0.8);
+ border: 1px solid rgba(21, 108, 101, 0.14);
+ border-radius: 7px;
+ outline: none;
+ font-size: 7px;
+ font-weight: 800;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-select select:focus {
+ border-color: #16877f;
+ box-shadow: 0 0 0 2px rgba(22, 135, 127, 0.1);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-apply {
+ width: 100%;
+ min-height: 29px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ margin-top: 1px;
+ color: #ffffff;
+ background: linear-gradient(135deg, #138b82, #0a706a);
+ border: 0;
+ border-radius: 8px;
+ box-shadow: 0 6px 14px rgba(14, 119, 112, 0.18);
+ font-size: 8px;
+ font-weight: 900;
+ cursor: pointer;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-apply:hover:not(:disabled) {
+ background: linear-gradient(135deg, #15988e, #0b7770);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-apply:disabled {
+ color: #6e8985;
+ background: #d8e7e4;
+ box-shadow: none;
+ cursor: wait;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-apply svg {
+ width: 12px;
+ height: 12px;
+}
+
+/* Readability and final layout alignment -------------------------------- */
+.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .command-composer {
+ height: 88px;
+ min-height: 88px;
+ position: relative;
+ overflow: hidden;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form #utterance {
+ height: 88px;
+ min-height: 88px;
+ padding: 11px 72px 36px 14px;
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .primary-action {
+ width: 36px;
+ height: 36px;
+ min-height: 36px;
+ top: 50%;
+ right: 10px;
+ bottom: auto;
+ transform: translateY(-50%);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .primary-action:hover:not(:disabled),
+.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .primary-action:focus-visible {
+ transform: translateY(-50%) scale(1.04);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .context-reset {
+ top: 7px;
+ right: 52px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .composer-hint {
+ right: 56px;
+ bottom: 8px;
+ font-size: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-reasoning-trigger {
+ min-height: 28px;
+ font-size: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-reasoning-trigger > strong {
+ font-size: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-natural-panel {
+ grid-template-rows: auto minmax(88px, 1fr) auto;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-link span {
+ font-size: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log header small,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log time {
+ font-size: 8.5px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log select {
+ font-size: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log li strong {
+ font-size: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log li small {
+ font-size: 9px;
+ line-height: 1.4;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-heading .section-kicker,
+.console-v2.v2-passive-display.v2-interactive-sync .twin-orientation,
+.console-v2.v2-passive-display.v2-interactive-sync .twin-seat-switch button {
+ font-size: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell.v2-output-page,
+.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell[data-open-panels="devices extensions"] {
+ grid-template-columns: minmax(234px, 0.56fr) minmax(460px, 1.5fr) minmax(234px, 0.56fr);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell[data-open-panels="devices"] {
+ grid-template-columns: minmax(224px, 0.58fr) minmax(460px, 1.42fr) 48px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell[data-open-panels="extensions"] {
+ grid-template-columns: 48px minmax(460px, 1.42fr) minmax(224px, 0.58fr);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell[data-open-panels="none"] {
+ grid-template-columns: 48px minmax(520px, 1fr) 48px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device {
+ padding: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-header {
+ gap: 7px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus {
+ grid-template-columns: 30px minmax(0, 1fr) 6px;
+ gap: 8px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus > span:first-child {
+ width: 30px;
+ height: 30px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus strong {
+ font-size: 11px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus small {
+ font-size: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-adjust-toggle {
+ min-width: 48px;
+ height: 29px;
+ padding: 0 7px;
+ font-size: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-adjust-toggle svg {
+ width: 12px;
+ height: 12px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dl {
+ gap: 6px;
+ margin-top: 9px;
+ padding-top: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dl div {
+ gap: 3px;
+ min-height: 38px;
+ padding: 7px 8px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dt {
+ font-size: 9px;
+ line-height: 1.2;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dd {
+ font-size: 10px;
+ line-height: 1.25;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-controls {
+ gap: 9px;
+ margin-top: 10px;
+ padding: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-controls-intro span {
+ font-size: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-controls-intro small,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-row,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-row output {
+ font-size: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-row > small {
+ font-size: 8.5px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-select select {
+ width: 92px;
+ height: 30px;
+ font-size: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-apply {
+ min-height: 34px;
+ font-size: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-sync-heading small,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-gateway > b,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device header small,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device header b,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device > div span,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-linkage small,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-linkage > span {
+ font-size: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-gateway strong,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device header strong {
+ font-size: 11px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-gateway small,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-empty span {
+ font-size: 9px;
+ line-height: 1.4;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-empty strong,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-linkage strong {
+ font-size: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-side-status-panel.is-collapsed {
+ padding: 0;
+ overflow: hidden;
+ background: rgba(250, 253, 252, 0.96);
+ border: 1px solid rgba(17, 112, 105, 0.14);
+ border-radius: 16px;
+ box-shadow: 0 8px 20px rgba(35, 78, 75, 0.06);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel.is-collapsed,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel.is-collapsed {
+ border-radius: 16px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel.is-collapsed > header,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel.is-collapsed .v2-extension-sync-mount,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel.is-collapsed .v2-extension-sync-heading {
+ width: 100%;
+ height: 100%;
+ min-height: 0;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ background: transparent;
+ border: 0;
+ border-radius: 15px;
+ box-shadow: none;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel.is-collapsed > header {
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-rows: minmax(0, 1fr);
+ place-items: stretch;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel.is-collapsed > header > button,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel.is-collapsed .v2-extension-sync-heading > button {
+ width: 100%;
+ height: 100%;
+ min-width: 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ margin: 0;
+ padding: 12px 0;
+ color: #216f6a;
+ background: transparent;
+ border: 0;
+ border-radius: 15px;
+ writing-mode: horizontal-tb;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel.is-collapsed > header > button {
+ grid-column: 1;
+ grid-row: 1;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel.is-collapsed > header > button span,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel.is-collapsed .v2-extension-sync-heading > button span {
+ font-size: 10px;
+ font-weight: 850;
+ letter-spacing: 0.16em;
+ writing-mode: vertical-rl;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel.is-collapsed > header > button:hover,
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel.is-collapsed .v2-extension-sync-heading > button:hover {
+ color: #0b756f;
+ background: #e6f5f2;
+}
+
+@media (max-width: 1120px) {
+ .console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell.v2-output-page,
+ .console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell[data-open-panels="devices extensions"] {
+ grid-template-columns: minmax(210px, 0.58fr) minmax(420px, 1.35fr) minmax(210px, 0.58fr);
+ }
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-gallery-toggle strong,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-task-timeline i,
+.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-seat-label small,
+.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-callout span,
+.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-callout small,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-twin-hotspots button,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-twin-device-popover header > b,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-twin-device-popover dt {
+ font-size: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-twin-device-popover header small {
+ font-size: 8.5px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-twin-device-popover dd {
+ font-size: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sidebar-footer small,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-device-sync-board > header > button,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-extension-sync-heading > button {
+ font-size: 9px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-gateway small {
+ overflow: visible;
+ text-overflow: clip;
+ white-space: normal;
+}
+
+/* Collapsible left sync rail aligned with the lower workspace ------------ */
+.console-v2.v2-passive-display.v2-interactive-sync .workspace {
+ transition: grid-template-columns 180ms ease;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .command-panel.v2-sidebar {
+ --v2-sync-log-top: 291px;
+ position: relative;
+ display: block;
+ padding: 0 15px 13px;
+ overflow: hidden;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar {
+ width: 100%;
+ height: 100%;
+ display: grid;
+ grid-template-rows: calc(var(--v2-sync-log-top) - 10px) minmax(0, 1fr);
+ gap: 10px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-overview {
+ min-height: 0;
+ display: grid;
+ grid-template-rows: auto auto minmax(0, 1fr) auto;
+ gap: 10px;
+ padding-top: 26px;
+ overflow: hidden;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-heading {
+ min-width: 0;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 32px;
+ grid-template-rows: auto auto auto;
+ column-gap: 7px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-heading > small,
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-heading > h2 {
+ min-width: 0;
+ grid-column: 1;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-heading > p {
+ grid-column: 1 / -1;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-heading h2 {
+ font-size: 19px;
+ white-space: nowrap;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2SidebarToggle {
+ width: 32px;
+ height: 28px;
+ min-width: 32px;
+ grid-column: 2;
+ grid-row: 1 / 3;
+ align-self: start;
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+ gap: 3px;
+ padding: 0;
+ color: #3d716e;
+ background: rgba(255, 255, 255, 0.78);
+ border: 1px solid rgba(19, 105, 99, 0.12);
+ border-radius: 9px;
+ box-shadow: 0 3px 9px rgba(37, 84, 80, 0.055);
+ cursor: pointer;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2SidebarToggle:hover,
+.console-v2.v2-passive-display.v2-interactive-sync #v2SidebarToggle:focus-visible {
+ color: #0b756f;
+ background: #e3f4f1;
+ border-color: rgba(13, 126, 117, 0.21);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2SidebarToggle svg {
+ width: 14px;
+ height: 14px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync #v2SidebarToggle span {
+ display: none;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-overview > .v2-sync-protocol {
+ align-self: end;
+ margin: 0;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar-overview > .v2-sidebar-footer {
+ width: 100%;
+ margin: 0;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-sidebar > .v2-sync-log {
+ min-height: 0;
+ grid-row: 2;
+ flex: none;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .workspace {
+ grid-template-columns: 64px minmax(0, 1fr);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .command-panel.v2-sidebar {
+ padding: 3px 8px 13px;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-sidebar,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-sidebar-overview,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-sidebar-heading {
+ width: 100%;
+ height: 100%;
+ display: block;
+ margin: 0;
+ padding: 0;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-sidebar-heading > small,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-sidebar-heading > h2,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-sidebar-heading > p,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-link-stack,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-protocol,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sidebar-footer,
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-log {
+ display: none !important;
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed #v2SidebarToggle {
+ width: 100%;
+ height: 100%;
+ min-width: 0;
+ display: flex !important;
+ flex-direction: column;
+ gap: 10px;
+ color: #216f6a;
+ background: rgba(250, 253, 252, 0.92);
+ border-radius: 16px;
+ box-shadow: 0 8px 20px rgba(35, 78, 75, 0.06);
+}
+
+.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed #v2SidebarToggle span {
+ display: block;
+ font-size: 10px;
+ font-weight: 850;
+ letter-spacing: 0.16em;
+ writing-mode: vertical-rl;
+}
diff --git a/apps/web-console/console-v2.js b/apps/web-console/console-v2.js
index 8083f9c..ba4b9ba 100644
--- a/apps/web-console/console-v2.js
+++ b/apps/web-console/console-v2.js
@@ -112,7 +112,11 @@
place();
};
reasoningPickerControl = { close, open, place, sync, trigger };
+ const resetButton = $("#newConversation", originalForm);
+ const runButton = $("#runButton", originalForm);
+ if (resetButton) composer.append(resetButton);
composer.append(trigger);
+ if (runButton) composer.append(runButton);
document.body.append(reasoningField);
trigger.addEventListener("click", (event) => {
@@ -493,8 +497,14 @@
requestId: null,
pollTimer: null,
deviceFrame: null,
+ deviceFingerprint: "",
activeDevice: null,
pinnedDevice: null,
+ previewClearTimer: null,
+ hoverSuppressedUntil: 0,
+ controlDevice: null,
+ deviceDrafts: {},
+ pendingControls: {},
logs: [],
logKeys: new Set(),
extensionFingerprint: "",
@@ -502,55 +512,172 @@
const readText = (selector, fallback = "--") => $(selector)?.textContent?.trim() || fallback;
- function seatDeviceRows(seat) {
- const card = $(`.seat-card[data-seat="${seat}"]`);
- if (!card) return [["状态", "等待反馈"]];
- const rows = [
- ["反馈通道", $("[data-seat-feedback]", card)?.textContent?.trim() || "等待反馈"],
- ...$$('[data-function]', card).map((item) => [
- item.textContent.replace(item.querySelector("b")?.textContent || "", "").trim(),
- $("b", item)?.textContent?.trim() || "--",
- ]),
- ...$$('.motion-axis', card).map((item) => [
- $("span", item)?.textContent?.trim() || "姿态",
- $("b", item)?.textContent?.trim() || "--",
- ]),
- ];
- return rows.filter(([label]) => label);
+ const clampControlNumber = (value, min, max, fallback = min) => {
+ const numeric = Number.parseFloat(String(value ?? "").replace(/[^\d.-]/g, ""));
+ return Number.isFinite(numeric) ? Math.max(min, Math.min(max, numeric)) : fallback;
+ };
+
+ const splitReadout = (selector) => {
+ const [front = "-", rear = "-"] = readText(selector, "- / -").split("/");
+ return [front.trim() || "-", rear.trim() || "-"];
+ };
+
+ function seatFunctionValue(seat, feature) {
+ return $(`.seat-card[data-seat="${seat}"] [data-function="${feature}"] b`)?.textContent?.trim() || "--";
+ }
+
+ function seatAxisValue(seat, axis) {
+ return $(`.seat-card[data-seat="${seat}"] [data-axis="${axis}"] b`)?.textContent?.trim() || "--";
+ }
+
+ function seatLevelControlValue(seat, feature) {
+ const text = seatFunctionValue(seat, feature);
+ const level = text.match(/L(\d+)/i)?.[1];
+ return /OFF|关闭|--/.test(text) ? "off" : level || "1";
+ }
+
+ function massageReadout() {
+ const text = seatFunctionValue("driver", "massage");
+ if (text === "--") return ["--", "--"];
+ if (/OFF|关闭/.test(text)) return ["关闭", "关闭"];
+ const [mode = "开启", strength = "默认"] = text.split("·").map((item) => item.trim());
+ return [mode, strength];
+ }
+
+ const levelOptions = [
+ ["off", "关闭"], ["1", "1 档"], ["2", "2 档"], ["3", "3 档"],
+ ];
+ const toggleOptions = [["off", "关闭"], ["on", "开启"]];
+ const massageModeOptions = [
+ ["off", "关闭"], ["relax", "舒缓"], ["stretch", "伸展"], ["waist", "腰部"], ["wave", "波浪"],
+ ];
+ const massageStrengthOptions = [
+ ["off", "关闭"], ["low", "低"], ["medium", "中"], ["high", "高"],
+ ];
+ const colorIndexNames = ["皓月白", "冰川蓝", "松石绿", "星云紫", "琥珀橙", "玫瑰红"];
+ const colorIndexOptions = Array.from({ length: 16 }, (_item, index) => [
+ String(index), colorIndexNames[index] ? `${index} · ${colorIndexNames[index]}` : `色号 ${index}`,
+ ]);
+
+ const rangeControl = (key, label, value, min = 0, max = 100, step = 1, unit = "%", hint = "") => ({
+ key, label, type: "range", value: clampControlNumber(value, min, max), min, max, step, unit, hint,
+ });
+ const selectControl = (key, label, value, options) => ({ key, label, type: "select", value, options });
+
+ function deviceControlValue(deviceId, control) {
+ const draft = displaySync.deviceDrafts[deviceId];
+ return draft && Object.hasOwn(draft, control.key) ? draft[control.key] : control.value;
+ }
+
+ function optionLabel(options, value) {
+ return options.find(([optionValue]) => String(optionValue) === String(value))?.[1] || String(value);
}
const syncedDevices = [
+ {
+ id: "passenger", name: "副驾座椅", icon: "armchair",
+ rows: () => [["通风", seatFunctionValue("passenger", "ventilation")], ["加热", seatFunctionValue("passenger", "heating")], ["水平", seatAxisValue("passenger", "longitudinal")], ["坐垫", seatAxisValue("passenger", "cushion_tilt")], ["靠背", seatAxisValue("passenger", "backrest_recline")], ["腿托", seatAxisValue("passenger", "legrest_height")]],
+ controls: () => [
+ selectControl("ventilation", "通风", seatLevelControlValue("passenger", "ventilation"), levelOptions),
+ selectControl("heating", "加热", seatLevelControlValue("passenger", "heating"), levelOptions),
+ rangeControl("longitudinal", "水平", seatAxisValue("passenger", "longitudinal")),
+ rangeControl("cushion_tilt", "坐垫", seatAxisValue("passenger", "cushion_tilt")),
+ rangeControl("backrest_recline", "靠背", seatAxisValue("passenger", "backrest_recline")),
+ rangeControl("legrest_height", "腿托", seatAxisValue("passenger", "legrest_height")),
+ ],
+ command: (values) => `调节副驾座椅:通风${optionLabel(levelOptions, values.ventilation)},加热${optionLabel(levelOptions, values.heating)},水平${values.longitudinal}%,坐垫${values.cushion_tilt}%,靠背${values.backrest_recline}%,腿托${values.legrest_height}%`,
+ },
+ {
+ id: "driver", name: "主驾座椅", icon: "armchair",
+ rows: () => {
+ const [massageMode, massageStrength] = massageReadout();
+ return [["通风", seatFunctionValue("driver", "ventilation")], ["加热", seatFunctionValue("driver", "heating")], ["按摩模式", massageMode], ["按摩强度", massageStrength]];
+ },
+ controls: () => {
+ const [massageMode, massageStrength] = massageReadout();
+ return [
+ selectControl("ventilation", "通风", seatLevelControlValue("driver", "ventilation"), levelOptions),
+ selectControl("heating", "加热", seatLevelControlValue("driver", "heating"), levelOptions),
+ selectControl("massage_mode", "按摩模式", /关闭|--/.test(massageMode) ? "off" : massageMode.toLowerCase(), massageModeOptions),
+ selectControl("massage_strength", "按摩强度", /关闭|--/.test(massageStrength) ? "off" : massageStrength.toLowerCase(), massageStrengthOptions),
+ ];
+ },
+ command: (values) => `调节主驾座椅:通风${optionLabel(levelOptions, values.ventilation)},加热${optionLabel(levelOptions, values.heating)},按摩模式${optionLabel(massageModeOptions, values.massage_mode)},按摩强度${optionLabel(massageStrengthOptions, values.massage_strength)}`,
+ },
{
id: "windows", name: "车窗", icon: "gallery-vertical-end",
- rows: () => [["设备状态", readText("#windowState", "等待反馈")], ["左前窗", readText("#frontLeftWindow")], ["右前窗", readText("#frontRightWindow")], ["左后窗", readText("#rearLeftWindow")], ["右后窗", readText("#rearRightWindow")]],
+ rows: () => [["左前窗", readText("#frontLeftWindow")], ["右前窗", readText("#frontRightWindow")], ["左后窗", readText("#rearLeftWindow")], ["右后窗", readText("#rearRightWindow")]],
+ controls: () => [
+ rangeControl("front_left", "左前窗", readText("#frontLeftWindow"), 0, 100, 1, "%", "0% 关闭 · 100% 打开"),
+ rangeControl("front_right", "右前窗", readText("#frontRightWindow")),
+ rangeControl("rear_left", "左后窗", readText("#rearLeftWindow")),
+ rangeControl("rear_right", "右后窗", readText("#rearRightWindow")),
+ ],
+ command: (values) => `调节车窗开合比例:左前窗${values.front_left}%,右前窗${values.front_right}%,左后窗${values.rear_left}%,右后窗${values.rear_right}%`,
},
{
id: "shade", name: "遮阳帘", icon: "panels-top-left",
- rows: () => [["设备状态", readText("#roofState", "等待反馈")], ["当前位置", readText("#shadePositionValue")], ["动作", readText("#shadeMotionState")], ["初始化", readText("#shadeInitializedState")], ["热保护", readText("#shadeThermalState")]],
+ rows: () => [["当前位置", readText("#shadePositionValue")]],
+ controls: () => [rangeControl("position", "当前位置", readText("#shadePositionValue"), 0, 100, 1, "%", "0% 关闭 · 100% 打开")],
+ command: (values) => `将遮阳帘开合位置调整为${values.position}%`,
},
- { id: "driver", name: "主驾座椅", icon: "armchair", rows: () => seatDeviceRows("driver") },
- { id: "passenger", name: "副驾座椅", icon: "armchair", rows: () => seatDeviceRows("passenger") },
{
id: "reading", name: "阅读灯", icon: "book-open",
- rows: () => [["开关状态", readText("#interiorLightPower", "等待反馈")], ["控制通道", "CH1 · INTERIOR"], ["控制能力", "开 / 关"]],
+ rows: () => [["开关状态", readText("#interiorLightPower", "等待反馈")]],
+ controls: () => [selectControl("power", "开关状态", /开启|ON/.test(readText("#interiorLightPower")) ? "on" : "off", toggleOptions)],
+ command: (values) => `${values.power === "on" ? "打开" : "关闭"}阅读灯`,
},
{
id: "ambient", name: "氛围灯", icon: "sparkles",
- rows: () => [["电源", readText("#ambientPower", "等待反馈")], ["模式", readText("#ambientMode")], ["前/后排亮度", readText("#ambientBrightness")], ["前/后排色号", readText("#ambientColorIndex")]],
+ rows: () => {
+ const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
+ const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
+ return [["开关状态", /OFF|关闭/.test(readText("#ambientPower")) ? "关闭" : "开启"], ["前排亮度", frontBrightness], ["前排颜色", frontColor], ["后排亮度", rearBrightness], ["后排颜色", rearColor]];
+ },
+ controls: () => {
+ const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
+ const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
+ return [
+ selectControl("power", "开关状态", /OFF|关闭/.test(readText("#ambientPower")) ? "off" : "on", toggleOptions),
+ rangeControl("front_brightness", "前排亮度", frontBrightness, 0, 5, 1, " 级"),
+ selectControl("front_color", "前排颜色", String(clampControlNumber(frontColor, 0, 15, 0)), colorIndexOptions),
+ rangeControl("rear_brightness", "后排亮度", rearBrightness, 0, 5, 1, " 级"),
+ selectControl("rear_color", "后排颜色", String(clampControlNumber(rearColor, 0, 15, 0)), colorIndexOptions),
+ ];
+ },
+ command: (values) => `调节氛围灯:${values.power === "on" ? "开启" : "关闭"},前排亮度${values.front_brightness}级、颜色${optionLabel(colorIndexOptions, values.front_color)},后排亮度${values.rear_brightness}级、颜色${optionLabel(colorIndexOptions, values.rear_color)}`,
},
{
id: "headlight", name: "前大灯", icon: "car-front",
- rows: () => [["设备状态", readText("#frontLightState", "等待反馈")], ["近光灯", readText("#lowBeamState")], ["远光灯", readText("#highBeamState")], ["雾灯", readText("#fogLightState")], ["转向灯", readText("#turnSignalState")]],
+ rows: () => [["近光灯", readText("#lowBeamState")], ["远光灯", readText("#highBeamState")], ["雾灯", readText("#fogLightState")], ["转向灯", readText("#turnSignalState")]],
+ controls: () => [["low_beam", "近光灯", "#lowBeamState"], ["high_beam", "远光灯", "#highBeamState"], ["fog_light", "雾灯", "#fogLightState"], ["turn_signal", "转向灯", "#turnSignalState"]].map(([key, label, selector]) => selectControl(key, label, /开启|ON/.test(readText(selector)) ? "on" : "off", toggleOptions)),
+ command: (values) => `调节前大灯:近光灯${optionLabel(toggleOptions, values.low_beam)},远光灯${optionLabel(toggleOptions, values.high_beam)},雾灯${optionLabel(toggleOptions, values.fog_light)},转向灯${optionLabel(toggleOptions, values.turn_signal)}`,
},
{
id: "climate", name: "空调", icon: "fan",
- rows: () => [["电源", readText("#climatePower", "等待反馈")], ["主驾温度", readText("#driverTemperature")], ["副驾温度", readText("#passengerTemperature")], ["风量", readText("#fanLevel")], ["主驾风向", readText("#driverAirDirection")], ["副驾风向", readText("#passengerAirDirection")], ["循环", readText("#cycleMode")], ["A/C", readText("#acState")], ["前除霜", readText("#defrostState")]],
+ rows: () => [["开关状态", readText("#climatePower", "等待反馈")], ["主驾温度", readText("#driverTemperature")], ["副驾温度", readText("#passengerTemperature")], ["风量", readText("#fanLevel")], ["循环模式", readText("#cycleMode")]],
+ controls: () => [
+ selectControl("power", "开关状态", /ON|ACTIVE|开启/.test(readText("#climatePower")) ? "on" : "off", toggleOptions),
+ rangeControl("driver_temperature", "主驾温度", readText("#driverTemperature"), 16, 32, 0.5, "℃"),
+ rangeControl("passenger_temperature", "副驾温度", readText("#passengerTemperature"), 16, 32, 0.5, "℃"),
+ rangeControl("fan_level", "风量", readText("#fanLevel"), 0, 7, 1, " 档"),
+ selectControl("cycle_mode", "循环模式", readText("#cycleMode").includes("内") ? "internal" : "external", [["internal", "内循环"], ["external", "外循环"]]),
+ ],
+ command: (values) => `调节空调:${values.power === "on" ? "开启" : "关闭"},主驾${values.driver_temperature}℃,副驾${values.passenger_temperature}℃,风量${values.fan_level}档,${values.cycle_mode === "internal" ? "内循环" : "外循环"}`,
},
];
- function deviceSummary(device) {
- const values = device.rows().map(([, value]) => value).filter(Boolean);
- return values.slice(0, 3).join(" · ") || "等待反馈";
+ function renderDeviceControl(device, control) {
+ const value = deviceControlValue(device.id, control);
+ if (control.type === "select") {
+ const options = control.options.map(([optionValue, label]) => ``).join("");
+ return ``;
+ }
+ return ``;
}
function renderSyncedDevices() {
@@ -558,25 +685,83 @@
if (!grid) return;
grid.innerHTML = syncedDevices.map((device) => {
const rows = device.rows();
- const detail = deviceSummary(device);
- const waiting = /--|等待|同步中|未知/.test(detail);
- return ``;
+ const waiting = rows.some(([, value]) => /--|等待|同步中|未知/.test(value));
+ const controlOpen = displaySync.controlDevice === device.id;
+ const hasDraft = Boolean(displaySync.deviceDrafts[device.id]);
+ const pending = Boolean(displaySync.pendingControls[device.id]);
+ const stateLabel = pending ? "已发送 · 等待同步" : hasDraft ? "参数待发送" : `${rows.length} 项关键状态`;
+ return `${rows.map(([label, value]) => `
+