feat: integrate safer vehicle CAN reasoning
This commit is contained in:
@@ -36,6 +36,7 @@ function loadHardwareClientId() {
|
||||
|
||||
const state = {
|
||||
provider: "openai",
|
||||
reasoningEffort: localStorage.getItem("cognitive-agent.reasoning-effort") || "medium",
|
||||
executionMode: "dry_run",
|
||||
conversationId: loadConversationId(),
|
||||
vehicleId: null,
|
||||
@@ -3929,6 +3930,35 @@ function openModeGallery() {
|
||||
backdrop.classList.add("open");
|
||||
$("#modeGalleryClose").focus();
|
||||
});
|
||||
syncReasoningControls();
|
||||
}
|
||||
|
||||
function availableReasoningEfforts() {
|
||||
const configured = state.runtime?.openai_reasoning_efforts;
|
||||
return Array.isArray(configured) && configured.length ? configured : ["low", "medium", "high"];
|
||||
}
|
||||
|
||||
function selectReasoningEffort(effort, { persist = true } = {}) {
|
||||
if (!availableReasoningEfforts().includes(effort)) return false;
|
||||
state.reasoningEffort = effort;
|
||||
if (persist) localStorage.setItem("cognitive-agent.reasoning-effort", effort);
|
||||
syncReasoningControls();
|
||||
return true;
|
||||
}
|
||||
|
||||
function syncReasoningControls() {
|
||||
const enabled = state.provider === "openai" && state.runtime?.openai_available === true;
|
||||
const descriptions = {
|
||||
low: "极速模式优先降低指令解析延迟",
|
||||
medium: "均衡模式兼顾自然语言解析精度与响应速度",
|
||||
high: "深度模式适合多设备、相对动作与复杂约束指令",
|
||||
};
|
||||
$$('[data-reasoning-effort]').forEach((button) => {
|
||||
button.disabled = !enabled || !availableReasoningEfforts().includes(button.dataset.reasoningEffort);
|
||||
button.classList.toggle("active", button.dataset.reasoningEffort === state.reasoningEffort);
|
||||
});
|
||||
const note = $("#reasoningNote");
|
||||
if (note) note.textContent = enabled ? descriptions[state.reasoningEffort] : "本地规则解析不调用模型推理";
|
||||
}
|
||||
|
||||
function closeModeGallery({ restoreFocus = true } = {}) {
|
||||
@@ -4292,6 +4322,10 @@ async function loadRuntime() {
|
||||
applyRuntimeTargetPolicy(state.runtime);
|
||||
populateVehicleSelector();
|
||||
populateModeChips();
|
||||
const configuredEffort = state.runtime.openai_reasoning_effort_default || "medium";
|
||||
if (!availableReasoningEfforts().includes(state.reasoningEffort)) {
|
||||
state.reasoningEffort = configuredEffort;
|
||||
}
|
||||
const hardware = state.runtime.hardware || {};
|
||||
const vehicleCan = state.runtime.vehicle_can || {};
|
||||
const linkedHardwareReady = Boolean(hardware.configured && vehicleCan.configured);
|
||||
@@ -4315,6 +4349,7 @@ async function loadRuntime() {
|
||||
$("#providerNote").textContent = "未设置 OPENAI_API_KEY,已切换为本地规则解析";
|
||||
selectProvider("local");
|
||||
}
|
||||
syncReasoningControls();
|
||||
$("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`;
|
||||
const hardwareButton = $("#hardwareMode");
|
||||
hardwareButton.disabled = !hardwareExecutionReady;
|
||||
@@ -5575,6 +5610,9 @@ function renderResult(result) {
|
||||
const conversationLabel = result.conversation_id?.slice(-8) || "no-context";
|
||||
$("#requestMeta").textContent = `${result.request_id} / context:${conversationLabel} / turn:${result.conversation_turn} / ${result.provider_used || result.provider_requested}`;
|
||||
$("#requestMeta").textContent += ` / ${result.execution_report?.spec?.mode || result.execution_mode || "dry_run"}`;
|
||||
if (result.reasoning_effort_used) {
|
||||
$("#requestMeta").textContent += ` / reasoning:${result.reasoning_effort_used}`;
|
||||
}
|
||||
setRunStatus(
|
||||
partiallySucceeded ? "partial" : executionSucceeded ? "succeeded" : "failed",
|
||||
result.status.toUpperCase(),
|
||||
@@ -5647,6 +5685,7 @@ async function submitRequest(event) {
|
||||
conversation_id: state.conversationId,
|
||||
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
|
||||
execution_mode: state.executionMode,
|
||||
...(state.provider === "openai" ? { reasoning_effort: state.reasoningEffort } : {}),
|
||||
...(state.executionMode === "hardware" && !driverOnlyCh1
|
||||
? { hardware_client_id: state.hardwareClientId }
|
||||
: {}),
|
||||
@@ -5892,6 +5931,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
}
|
||||
});
|
||||
$$(".segment[data-provider]").forEach((button) => button.addEventListener("click", () => selectProvider(button.dataset.provider)));
|
||||
$$('[data-reasoning-effort]').forEach((button) => button.addEventListener("click", () => selectReasoningEffort(button.dataset.reasoningEffort)));
|
||||
$$('[data-execution-mode]').forEach((button) => button.addEventListener("click", () => selectExecutionMode(button.dataset.executionMode)));
|
||||
$$(".example-command").forEach((button) => button.addEventListener("click", () => {
|
||||
$("#utterance").value = button.dataset.example;
|
||||
|
||||
@@ -3283,3 +3283,209 @@
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
.reasoning-segmented {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.reasoning-option > span > strong {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.reasoning-option > span > small,
|
||||
.reasoning-option > .reasoning-check {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.reasoning-option:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* Market-style model intelligence picker: a compact composer pill expands
|
||||
into an overlay only when the user needs to change the reasoning level. */
|
||||
.console-v2 .v2-reasoning-field {
|
||||
width: 320px;
|
||||
min-width: 250px;
|
||||
position: fixed;
|
||||
z-index: 120;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border: 1px solid color-mix(in srgb, var(--v2-accent) 18%, #e6eceb);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 16px 38px rgba(28, 61, 62, 0.2);
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field > .field-label {
|
||||
margin: 0 0 8px;
|
||||
color: #263b3d;
|
||||
font-size: 11px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field .reasoning-segmented {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 5px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field .reasoning-option {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 42px;
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr) 18px;
|
||||
align-items: center;
|
||||
justify-content: initial;
|
||||
gap: 8px;
|
||||
padding: 6px 9px;
|
||||
color: #536466;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 9px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field .reasoning-option:hover {
|
||||
color: var(--v2-accent-strong);
|
||||
background: color-mix(in srgb, var(--v2-accent-soft) 38%, white);
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field .reasoning-option.active {
|
||||
color: var(--v2-accent-strong);
|
||||
background: color-mix(in srgb, var(--v2-accent-soft) 58%, white);
|
||||
border-color: color-mix(in srgb, var(--v2-accent) 16%, transparent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field .reasoning-option > svg:first-child {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field .reasoning-option > span {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field .reasoning-option > span > strong,
|
||||
.console-v2 .v2-reasoning-field .reasoning-option > span > small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field .reasoning-option > span > strong {
|
||||
color: inherit;
|
||||
font-size: 10px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field .reasoning-option > span > small {
|
||||
color: #899697;
|
||||
font-size: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field .reasoning-option > .reasoning-check {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field .reasoning-option.active > .reasoning-check {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-field > .provider-note {
|
||||
min-height: 0;
|
||||
margin: 8px 2px 0;
|
||||
color: #829091;
|
||||
font-size: 8px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .command-composer {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form #utterance {
|
||||
padding-bottom: 42px;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-trigger {
|
||||
max-width: 180px;
|
||||
min-height: 26px;
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
bottom: 9px;
|
||||
z-index: 12;
|
||||
display: inline-grid;
|
||||
grid-template-columns: 14px auto auto 12px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 0 9px;
|
||||
color: #526264;
|
||||
background: rgba(241, 244, 243, 0.96);
|
||||
border: 1px solid rgba(98, 127, 126, 0.11);
|
||||
border-radius: 999px;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-trigger:hover,
|
||||
.console-v2 .v2-reasoning-trigger.open {
|
||||
color: var(--v2-accent-strong);
|
||||
background: color-mix(in srgb, var(--v2-accent-soft) 52%, white);
|
||||
border-color: color-mix(in srgb, var(--v2-accent) 18%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-trigger.unavailable {
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-trigger > svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-trigger > strong {
|
||||
color: var(--v2-accent-strong);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-trigger > .v2-reasoning-caret {
|
||||
transition: transform 150ms ease;
|
||||
}
|
||||
|
||||
.console-v2 .v2-reasoning-trigger.open > .v2-reasoning-caret {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .composer-hint {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-height: 820px) {
|
||||
.console-v2 .v2-reasoning-trigger {
|
||||
bottom: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
};
|
||||
|
||||
const dropdownControls = new Map();
|
||||
let reasoningPickerControl = null;
|
||||
|
||||
function make(tag, className, html) {
|
||||
const node = document.createElement(tag);
|
||||
@@ -47,6 +48,99 @@
|
||||
});
|
||||
}
|
||||
|
||||
function closeReasoningPicker() {
|
||||
reasoningPickerControl?.close();
|
||||
}
|
||||
|
||||
function installReasoningPicker(reasoningField, originalForm) {
|
||||
const composer = $(".command-composer", originalForm);
|
||||
if (!composer || !reasoningField) return;
|
||||
|
||||
reasoningField.classList.add("v2-reasoning-field");
|
||||
reasoningField.hidden = true;
|
||||
const trigger = make("button", "v2-reasoning-trigger", `
|
||||
<i data-lucide="sparkles"></i>
|
||||
<span>模型智能</span>
|
||||
<strong id="v2ReasoningValue">均衡</strong>
|
||||
<i class="v2-reasoning-caret" data-lucide="chevron-up"></i>`);
|
||||
trigger.id = "v2ReasoningTrigger";
|
||||
trigger.type = "button";
|
||||
trigger.setAttribute("aria-haspopup", "listbox");
|
||||
trigger.setAttribute("aria-expanded", "false");
|
||||
trigger.setAttribute("aria-controls", "reasoningField");
|
||||
trigger.setAttribute("aria-label", "选择模型智能程度,当前均衡");
|
||||
$(".reasoning-segmented", reasoningField)?.setAttribute("role", "listbox");
|
||||
|
||||
const options = () => $$('[data-reasoning-effort]', reasoningField);
|
||||
const place = () => {
|
||||
if (reasoningField.hidden) return;
|
||||
const triggerBox = trigger.getBoundingClientRect();
|
||||
const width = Math.min(320, Math.max(250, window.innerWidth - 24));
|
||||
const left = Math.min(
|
||||
Math.max(12, triggerBox.left),
|
||||
Math.max(12, window.innerWidth - width - 12),
|
||||
);
|
||||
reasoningField.style.width = `${width}px`;
|
||||
reasoningField.style.left = `${left}px`;
|
||||
const height = reasoningField.offsetHeight;
|
||||
const above = triggerBox.top - height - 8;
|
||||
const top = above >= 12
|
||||
? above
|
||||
: Math.min(triggerBox.bottom + 8, window.innerHeight - height - 12);
|
||||
reasoningField.style.top = `${Math.max(12, top)}px`;
|
||||
};
|
||||
const sync = () => {
|
||||
const selected = options().find((button) => button.classList.contains("active"));
|
||||
const label = selected?.querySelector("strong")?.textContent?.trim() || "均衡";
|
||||
const unavailable = options().length > 0 && options().every((button) => button.disabled);
|
||||
$("#v2ReasoningValue", trigger).textContent = label;
|
||||
trigger.classList.toggle("unavailable", unavailable);
|
||||
trigger.setAttribute("aria-disabled", String(unavailable));
|
||||
trigger.setAttribute("aria-label", `选择模型智能程度,当前${label}`);
|
||||
options().forEach((button) => {
|
||||
button.setAttribute("role", "option");
|
||||
button.setAttribute("aria-selected", String(button === selected));
|
||||
});
|
||||
};
|
||||
const close = () => {
|
||||
reasoningField.hidden = true;
|
||||
trigger.classList.remove("open");
|
||||
trigger.setAttribute("aria-expanded", "false");
|
||||
};
|
||||
const open = () => {
|
||||
closeCustomDropdowns();
|
||||
reasoningField.hidden = false;
|
||||
trigger.classList.add("open");
|
||||
trigger.setAttribute("aria-expanded", "true");
|
||||
place();
|
||||
};
|
||||
reasoningPickerControl = { close, open, place, sync, trigger };
|
||||
composer.append(trigger);
|
||||
document.body.append(reasoningField);
|
||||
|
||||
trigger.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
if (reasoningField.hidden) open();
|
||||
else close();
|
||||
});
|
||||
reasoningField.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
if (!event.target.closest("[data-reasoning-effort]")) return;
|
||||
requestAnimationFrame(() => {
|
||||
sync();
|
||||
close();
|
||||
trigger.focus({ preventScroll: true });
|
||||
});
|
||||
});
|
||||
new MutationObserver(sync).observe(reasoningField, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class", "disabled"],
|
||||
subtree: true,
|
||||
});
|
||||
window.addEventListener("resize", place);
|
||||
sync();
|
||||
}
|
||||
|
||||
function enhanceSelect(select) {
|
||||
if (!select || dropdownControls.has(select)) return;
|
||||
const shell = select.parentElement;
|
||||
@@ -207,6 +301,7 @@
|
||||
function buildConfigFields(configStack, originalForm) {
|
||||
const vehicleField = $(".vehicle-field", originalForm);
|
||||
const providerField = $(".field-group:has(.segmented [data-provider])", originalForm);
|
||||
const reasoningField = $("#reasoningField", originalForm);
|
||||
const executionField = $(".field-group:has(.execution-mode-options)", originalForm);
|
||||
const simulationFaultField = $("#simulationFaultField", originalForm);
|
||||
|
||||
@@ -238,6 +333,7 @@
|
||||
<p class="provider-note" id="v2ModelNote">模型由服务端安全配置</p>`);
|
||||
modelField.id = "v2ModelField";
|
||||
configStack.append(modelField);
|
||||
installReasoningPicker(reasoningField, originalForm);
|
||||
|
||||
const gestureModelField = make("div", "v2-config-field v2-config-card v2-gesture-model-field", `
|
||||
<i data-lucide="scan-line"></i>
|
||||
@@ -773,9 +869,15 @@
|
||||
"click",
|
||||
() => setSidebarCollapsed(!ui.sidebarCollapsed),
|
||||
);
|
||||
document.addEventListener("click", () => closeCustomDropdowns());
|
||||
document.addEventListener("click", () => {
|
||||
closeCustomDropdowns();
|
||||
closeReasoningPicker();
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") closeCustomDropdowns();
|
||||
if (event.key === "Escape") {
|
||||
closeCustomDropdowns();
|
||||
closeReasoningPicker();
|
||||
}
|
||||
});
|
||||
window.addEventListener("resize", updateScrollCues);
|
||||
window.addEventListener("resize", updateModeEdgeHints);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<link rel="icon" href="/icons/app-icon-192.png" />
|
||||
<title>智舱随行 · 整车智能控制台</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=dual-seat-9" />
|
||||
<link rel="stylesheet" href="/console-v2.css?v=desktop-refactor-87" />
|
||||
<link rel="stylesheet" href="/console-v2.css?v=desktop-refactor-89" />
|
||||
<link rel="stylesheet" href="/self-extension.css?v=extension-12" />
|
||||
<link rel="stylesheet" href="/self-calibration.css?v=calibration-6" />
|
||||
<link rel="stylesheet" href="/self-repair.css?v=repair-17" />
|
||||
@@ -22,7 +22,7 @@
|
||||
<script src="/app.js?v=dual-seat-67" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=android-pad-7"></script>
|
||||
<script src="/pwa.js?v=android-pad-1" defer></script>
|
||||
<script src="/console-v2.js?v=desktop-refactor-28" defer></script>
|
||||
<script src="/console-v2.js?v=desktop-refactor-30" defer></script>
|
||||
<script src="/self-extension.js?v=extension-18" defer></script>
|
||||
<script src="/self-calibration.js?v=calibration-3" defer></script>
|
||||
<script src="/self-repair.js?v=repair-17" defer></script>
|
||||
@@ -112,6 +112,22 @@
|
||||
<p class="provider-note" id="providerNote">正在读取运行时配置</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group" id="reasoningField">
|
||||
<span class="field-label">模型推理强度</span>
|
||||
<div class="segmented reasoning-segmented" role="group" aria-label="模型推理强度">
|
||||
<button type="button" class="segment reasoning-option" data-reasoning-effort="low">
|
||||
<i data-lucide="zap"></i><span><strong>极速</strong><small>简单指令,优先响应速度</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
<button type="button" class="segment reasoning-option active" data-reasoning-effort="medium">
|
||||
<i data-lucide="scale"></i><span><strong>均衡</strong><small>日常控制,兼顾速度与理解</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
<button type="button" class="segment reasoning-option" data-reasoning-effort="high">
|
||||
<i data-lucide="brain"></i><span><strong>深度</strong><small>复杂联动、相对动作与约束</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="reasoningNote">均衡模式兼顾自然语言解析精度与响应速度</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<span class="field-label">执行目标</span>
|
||||
<div class="segmented execution-mode-options" role="group" aria-label="执行目标">
|
||||
|
||||
Reference in New Issue
Block a user