feat: harden seat intent and expand HIL coverage
This commit is contained in:
@@ -17,6 +17,20 @@ UI 只展示可审计的“决策摘要”和“处理轨迹”,不展示模
|
||||
make dev
|
||||
```
|
||||
|
||||
Android PAD/模拟器联调使用:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\android_pad_dev.ps1
|
||||
```
|
||||
|
||||
脚本会启动已配置的 Pixel Tablet AVD、建立 `adb reverse tcp:8000 tcp:8000`、
|
||||
以真实硬件关闭/模拟台架开启的方式启动服务,并在 Android Chrome 中打开
|
||||
`http://127.0.0.1:8000`。停止联调环境:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\android_pad_dev.ps1 -Stop
|
||||
```
|
||||
|
||||
局域网演示使用:
|
||||
|
||||
```bash
|
||||
@@ -31,7 +45,7 @@ make dev-lan
|
||||
HTTPS。部署方式见
|
||||
[`deploy/server/README.md`](../../deploy/server/README.md)。
|
||||
|
||||
页面支持 GPT/本地解析切换、六阶段流水线、主副驾热舒适、五轴位置状态和 3D 座椅孪生、GPT 结构化能力请求、决策与校验轨迹、FunctionConfig、CommandPlan、虚拟帧和 ExecutionReport。硬件执行在服务端固定关闭,不是仅靠前端按钮限制。
|
||||
页面支持 GPT/本地解析切换、六阶段流水线、主副驾热舒适、五轴位置状态和 3D 座椅孪生、GPT 结构化能力请求、决策与校验轨迹、FunctionConfig、CommandPlan、虚拟帧和 ExecutionReport。真实硬件执行由服务端门禁控制,不是仅靠前端按钮限制;Android 联调脚本会显式关闭真实硬件执行。
|
||||
|
||||
## 手势资源与安全规则
|
||||
|
||||
@@ -42,7 +56,16 @@ assets/gesture/gesture_recognizer.task
|
||||
../../config/gesture/default.yaml
|
||||
```
|
||||
|
||||
后端提供 `/api/gesture/runtime` 和 `/api/gesture/model`,前端以二进制缓冲区加载模型,避免依赖外部 `seat_gesture_control` 目录。MediaPipe Web 运行时从 jsDelivr 按需加载;摄像头帧和手部关键点只在当前浏览器中处理。
|
||||
后端提供 `/api/gesture/runtime` 和 `/api/gesture/model`,前端以二进制缓冲区加载模型,避免依赖外部 `seat_gesture_control` 目录。MediaPipe Web、Lucide 和 Three.js 均按固定版本保存在 `vendor/`,运行时不访问外部 CDN;摄像头帧和手部关键点只在当前浏览器中处理。
|
||||
|
||||
## PAD、PWA 与离线行为
|
||||
|
||||
- 竖屏 PAD 在 900 CSS px 以下切换为上下布局,横屏保留指令栏与结果区;触控目标最小 48 dp,并处理安全区和动态视口。
|
||||
- 顶部连接徽标由 `/api/health` 周期探测驱动;GET 可有限重试,POST 控制请求不自动重试。
|
||||
- Service Worker 缓存页面壳、固定版本依赖和按需访问的 3D/手势资源;`/api/requests` 等控制 API 始终走网络且不会写入缓存。
|
||||
- 从 Android Chrome 的“安装应用”可进入 standalone PWA;首次完整在线加载后可离线打开控制台壳,实际编排仍要求 Windows 服务可达。
|
||||
|
||||
模拟台架需要 `COGNITIVE_AGENT_SIMULATED_HARDWARE_ENABLED=true`。选择“模拟台架”后可注入反馈超时、适配器断开、电机堵转、固件不匹配和部分反馈。结果会标记 `simulation=true`、`seat_feedback=simulated_hardware` 和 `physical_can_opened=false`,不会调用真实 CAN、ZLG 或 IAR。
|
||||
|
||||
安全规则:张掌保持 1 秒激活;动作保持 600 ms 且至少连续 8 帧才执行;换动作不重新锁止;握拳立即停止但不解除激活;3 秒无有效动作自动锁止;单次连续运动最多 1.5 秒;检测到多只手时不执行。自定义映射保存在浏览器 `localStorage`。
|
||||
|
||||
|
||||
+112
-25
@@ -22,10 +22,22 @@ const state = {
|
||||
seatActuatorState: {},
|
||||
seatFeedbackSource: "simulated",
|
||||
availableSeatAxes: null,
|
||||
connectionStatus: window.CognitiveAgentNetwork?.getState?.().status || "connecting",
|
||||
};
|
||||
|
||||
const $ = (selector, root = document) => root.querySelector(selector);
|
||||
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
const apiRequest = (input, options) => window.CognitiveAgentNetwork.request(input, options);
|
||||
// A four-axis hardware mode is executed serially. Each absolute move may wait
|
||||
// up to 33 seconds, and firmware recovery or known-range homing can extend the
|
||||
// request further. Keep the browser deadline above the backend's bounded HIL
|
||||
// workflow so the UI does not report failure while the seat is still moving.
|
||||
const HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000;
|
||||
const REQUEST_TIMEOUT_MS = 45 * 1000;
|
||||
|
||||
function requestTimeoutForExecutionMode(mode) {
|
||||
return mode === "hardware" ? HARDWARE_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
function refreshIcons() {
|
||||
if (window.lucide) {
|
||||
@@ -2519,6 +2531,10 @@ function updateSeatFeedbackModeLabel() {
|
||||
label.textContent = "PHYSICAL CAN · SYNC UNAVAILABLE";
|
||||
return;
|
||||
}
|
||||
if (state.seatFeedbackSource === "simulated_hardware") {
|
||||
label.textContent = "SIMULATED HIL · NO PHYSICAL CAN";
|
||||
return;
|
||||
}
|
||||
label.textContent = state.executionMode === "hardware"
|
||||
? "PHYSICAL CAN FEEDBACK"
|
||||
: "SIMULATED FEEDBACK";
|
||||
@@ -2526,30 +2542,41 @@ function updateSeatFeedbackModeLabel() {
|
||||
|
||||
function selectExecutionMode(mode) {
|
||||
if (mode === "hardware" && !state.runtime?.hardware?.configured) return;
|
||||
if (mode === "simulated_hardware" && !state.runtime?.simulated_hardware?.enabled) return;
|
||||
state.executionMode = mode;
|
||||
$$('[data-execution-mode]').forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.executionMode === mode);
|
||||
});
|
||||
const hardware = mode === "hardware";
|
||||
const simulatedHardware = mode === "simulated_hardware";
|
||||
const approvalRequired = Boolean(state.runtime?.hardware?.approval_required);
|
||||
$("#approvalField").hidden = !hardware || !approvalRequired;
|
||||
$("#executionBadge").className = `status-badge ${hardware ? "" : "neutral"}`.trim();
|
||||
$("#simulationFaultField").hidden = !simulatedHardware;
|
||||
$("#executionBadge").className = `status-badge ${hardware ? "" : simulatedHardware ? "warning" : "neutral"}`.trim();
|
||||
$("#executionBadge").innerHTML = hardware
|
||||
? '<i data-lucide="radio-tower"></i>HIL · 真实 CAN'
|
||||
: '<i data-lucide="flask-conical"></i>DRY-RUN';
|
||||
$("#safetyTitle").textContent = hardware ? "真实台架安全门禁" : "虚拟执行边界";
|
||||
: simulatedHardware
|
||||
? '<i data-lucide="bug-play"></i>SIM · 模拟台架'
|
||||
: '<i data-lucide="flask-conical"></i>DRY-RUN';
|
||||
$("#safetyTitle").textContent = hardware
|
||||
? "真实台架安全门禁"
|
||||
: simulatedHardware ? "模拟台架隔离边界" : "虚拟执行边界";
|
||||
$("#safetyText").textContent = hardware
|
||||
? "副驾座椅映射到统一实体执行器;绝对位置会实时检查板端标定,小幅相对移动可走受限定时/Hall 回读,异常会尝试 STOP_ALL。"
|
||||
: "四域 CAN 均为 dry-run,真实发送被强制关闭。";
|
||||
$("#executeStageLabel").textContent = hardware ? "台架执行" : "虚拟执行";
|
||||
: simulatedHardware
|
||||
? "故障只注入执行报告;物理 CAN、ZLG 适配器和 IAR 固件路径保持关闭。"
|
||||
: "四域 CAN 均为 dry-run,真实发送被强制关闭。";
|
||||
$("#executeStageLabel").textContent = hardware ? "台架执行" : simulatedHardware ? "故障仿真" : "虚拟执行";
|
||||
$("#feedbackModeLabel").textContent = hardware
|
||||
? "T2G BENCH · PHYSICAL CAN FEEDBACK"
|
||||
: "MATRIX-BACKED · SIMULATED FEEDBACK";
|
||||
: simulatedHardware
|
||||
? "SIMULATED HIL · FAULT INJECTION"
|
||||
: "MATRIX-BACKED · SIMULATED FEEDBACK";
|
||||
const runButtonLabel = $("#runButton span");
|
||||
if (runButtonLabel) {
|
||||
runButtonLabel.textContent = hardware
|
||||
? (approvalRequired ? "授权并真实执行" : "真实执行")
|
||||
: "生成并执行";
|
||||
: simulatedHardware ? "运行故障仿真" : "生成并执行";
|
||||
}
|
||||
updateSeatFeedbackModeLabel();
|
||||
refreshIcons();
|
||||
@@ -2730,7 +2757,7 @@ function saveCurrentModeProfile() {
|
||||
|
||||
async function loadRuntime() {
|
||||
try {
|
||||
const response = await fetch("/api/runtime", { cache: "no-store" });
|
||||
const response = await apiRequest("/api/runtime", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
state.runtime = await response.json();
|
||||
populateVehicleSelector();
|
||||
@@ -2741,7 +2768,8 @@ async function loadRuntime() {
|
||||
if (state.runtime.openai_available) {
|
||||
badge.classList.remove("danger");
|
||||
badge.innerHTML = `<i data-lucide="sparkles"></i>${state.runtime.openai_model}`;
|
||||
$("#providerNote").textContent = `${state.runtime.openai_model} · Responses API · 失败时自动降级本地规则`;
|
||||
const responsesEndpoint = state.runtime.openai_responses_endpoint || "Responses API";
|
||||
$("#providerNote").textContent = `${state.runtime.openai_model} · ${responsesEndpoint} · 失败时自动降级本地规则`;
|
||||
selectProvider("openai");
|
||||
} else {
|
||||
badge.classList.add("danger");
|
||||
@@ -2754,6 +2782,11 @@ async function loadRuntime() {
|
||||
$("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`;
|
||||
const hardwareButton = $("#hardwareMode");
|
||||
hardwareButton.disabled = !hardware.configured;
|
||||
const simulation = state.runtime.simulated_hardware || {};
|
||||
$("#simulationMode").disabled = !simulation.enabled;
|
||||
$("#simulationMode").title = simulation.enabled
|
||||
? "开发专用模拟台架,不连接物理 CAN"
|
||||
: "通过安卓联调脚本或环境变量启用模拟台架";
|
||||
const profileText = (hardware.firmware_profiles || []).join(" / ") || "未选择 profile";
|
||||
const flashText = hardware.auto_flash_enabled ? "自动烧录已启用" : "自动烧录未启用";
|
||||
const approvalText = hardware.approval_required ? "需授权令牌" : "调试免令牌";
|
||||
@@ -2772,7 +2805,12 @@ async function loadRuntime() {
|
||||
? `可选 · ${approvalText}`
|
||||
: "已阻断";
|
||||
$("#hardwareCoverage").className = hardware.configured ? "available" : "blocked";
|
||||
selectExecutionMode("dry_run");
|
||||
const modeAvailable = state.executionMode === "hardware"
|
||||
? hardware.configured
|
||||
: state.executionMode === "simulated_hardware"
|
||||
? simulation.enabled
|
||||
: true;
|
||||
selectExecutionMode(modeAvailable ? state.executionMode : "dry_run");
|
||||
refreshIcons();
|
||||
} catch (error) {
|
||||
$("#modelBadge").textContent = "运行时离线";
|
||||
@@ -2789,7 +2827,7 @@ async function synchronizePhysicalSeatState() {
|
||||
vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "",
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`/api/hardware/seat-state?${query}`, {
|
||||
const response = await apiRequest(`/api/hardware/seat-state?${query}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (response.status === 401) {
|
||||
@@ -2814,7 +2852,7 @@ async function synchronizePhysicalSeatState() {
|
||||
}
|
||||
|
||||
async function loadAuthStatus() {
|
||||
const response = await fetch("/api/auth/status", { cache: "no-store" });
|
||||
const response = await apiRequest("/api/auth/status", { cache: "no-store" });
|
||||
if (!response.ok) return;
|
||||
const auth = await response.json();
|
||||
if (auth.enabled && !auth.authenticated) {
|
||||
@@ -2825,7 +2863,7 @@ async function loadAuthStatus() {
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await fetch("/api/auth/logout", { method: "POST" });
|
||||
await apiRequest("/api/auth/logout", { method: "POST" });
|
||||
window.location.replace("/login");
|
||||
}
|
||||
|
||||
@@ -3423,7 +3461,10 @@ function renderCode(result) {
|
||||
const feedback = telemetry?.feedback_frame
|
||||
? `${telemetry.feedback_frame.frame_id} · ${telemetry.feedback_frame.data_hex}`
|
||||
: "NO DIRECT FEEDBACK";
|
||||
const mode = result.execution_report?.spec?.mode === "hil" ? "HIL · PHYSICAL" : "DRY-RUN";
|
||||
const simulatedHil = result.execution_report?.metadata?.labels?.seat_feedback === "simulated_hardware";
|
||||
const mode = simulatedHil
|
||||
? "HIL · SIMULATED"
|
||||
: result.execution_report?.spec?.mode === "hil" ? "HIL · PHYSICAL" : "DRY-RUN";
|
||||
summary.innerHTML = `<span>Frames</span><b>${frames.length}</b><span>First payload</span><code>${frames[0].frame_id} · ${frames[0].data_hex}</code><span>Feedback</span><code>${feedback}</code><span>Mode</span><b>${mode}</b>`;
|
||||
$("#planNavMeta").textContent = `${frames.length} 帧 · ${mode}`;
|
||||
} else if (telemetry?.operation === "save_posture_preset") {
|
||||
@@ -3459,7 +3500,8 @@ function renderExecution(result) {
|
||||
summary.className = `execution-summary ${reportSucceeded ? "succeeded" : "failed"}`;
|
||||
const presetSave = Boolean(result.preset_config);
|
||||
const modeSave = Boolean(result.mode_profile);
|
||||
const physical = report.spec.mode === "hil";
|
||||
const simulatedHil = report.metadata?.labels?.seat_feedback === "simulated_hardware";
|
||||
const physical = report.spec.mode === "hil" && !simulatedHil;
|
||||
const executedDomains = [...new Set(report.spec.results.map((item) => item.domain))];
|
||||
const motionCount = report.spec.results.filter((item) => item.telemetry?.motion_axis).length;
|
||||
const firmwareResult = report.spec.results.find((item) => item.telemetry?.firmware_verified);
|
||||
@@ -3468,6 +3510,8 @@ function renderExecution(result) {
|
||||
? `长期模式 ${result.mode_profile.name} 已写入配置库,未生成 CAN 命令`
|
||||
: presetSave
|
||||
? "当前五轴姿态已持久化到会话预设仓库,未生成 CAN 命令"
|
||||
: simulatedHil
|
||||
? `${executedDomains.join(" / ")} 已完成隔离 HIL 仿真,未访问物理 CAN`
|
||||
: physical
|
||||
? `${executedDomains.join(" / ")} 已下发真实 CAN,并以 IAR 固件实际反馈确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
|
||||
: `${executedDomains.join(" / ")} 共 ${report.spec.results.length} 个操作已完成 CAN 编码与虚拟执行`;
|
||||
@@ -3480,7 +3524,9 @@ function renderExecution(result) {
|
||||
if (!reportSucceeded) {
|
||||
const failure = report.spec.results.find((item) => item.status === "failed");
|
||||
const failureTelemetry = failure?.telemetry || {};
|
||||
executionDetail = failureTelemetry.error_type === "CanGatewayError"
|
||||
executionDetail = simulatedHil
|
||||
? `模拟故障 ${report.metadata?.labels?.fault_scenario || "unknown"}:${failure?.message || "故障已注入"};物理 CAN 未打开。`
|
||||
: failureTelemetry.error_type === "CanGatewayError"
|
||||
? `CAN 适配器或总线访问失败:${failure?.message || "无法打开或访问 CAN 适配器。"}`
|
||||
: failureTelemetry.error_type === "FirmwareProgrammingError"
|
||||
? `固件协议签名验证或自动烧录失败,座椅控制帧未发送:${failure?.message || "请检查 CMSIS-DAP 探针和 IAR 配置。"}`
|
||||
@@ -3496,9 +3542,15 @@ function renderExecution(result) {
|
||||
? "真实台架未返回满足目标的反馈,移动链路已尝试发送 STOP_ALL。"
|
||||
: failure?.message || "真实台架未返回满足目标的物理反馈。";
|
||||
}
|
||||
summary.innerHTML = `<i data-lucide="${reportSucceeded ? ((presetSave || modeSave) ? "bookmark-check" : "circle-check-big") : "circle-x"}"></i><div><strong>${reportSucceeded ? (modeSave ? "长期模式已保存" : presetSave ? "姿态预设已保存" : (physical ? "真实台架执行成功" : "虚拟执行成功")) : "真实台架执行失败"}</strong><span>${executionDetail}</span></div>`;
|
||||
const executionTitle = reportSucceeded
|
||||
? modeSave ? "长期模式已保存"
|
||||
: presetSave ? "姿态预设已保存"
|
||||
: simulatedHil ? "模拟台架执行成功"
|
||||
: physical ? "真实台架执行成功" : "虚拟执行成功"
|
||||
: simulatedHil ? "模拟故障已触发" : physical ? "真实台架执行失败" : "虚拟执行失败";
|
||||
summary.innerHTML = `<i data-lucide="${reportSucceeded ? ((presetSave || modeSave) ? "bookmark-check" : "circle-check-big") : "circle-x"}"></i><div><strong>${executionTitle}</strong><span>${executionDetail}</span></div>`;
|
||||
$("#resultNavMeta").textContent = reportSucceeded
|
||||
? `${report.spec.results.length} 项完成 · ${physical ? "HIL" : "DRY-RUN"}`
|
||||
? `${report.spec.results.length} 项完成 · ${simulatedHil ? "SIM-HIL" : physical ? "HIL" : "DRY-RUN"}`
|
||||
: "执行失败 · 查看回读";
|
||||
$("#resultPanelState").textContent = reportSucceeded ? "SUCCEEDED" : "FAILED";
|
||||
$("#resultPanelState").className = `workspace-panel-state ${reportSucceeded ? "succeeded" : "failed"}`;
|
||||
@@ -3513,10 +3565,19 @@ function renderExecution(result) {
|
||||
detail.textContent = item.message;
|
||||
text.append(title, detail);
|
||||
const frame = document.createElement("code");
|
||||
const firmwareTelemetry = item.telemetry?.firmware_verified
|
||||
? [
|
||||
item.telemetry.programmed_this_request ? "FLASHED" : "VERIFIED",
|
||||
item.telemetry.firmware_revision == null ? null : `REV ${item.telemetry.firmware_revision}`,
|
||||
item.telemetry.identity_frame_id && item.telemetry.identity_data_hex
|
||||
? `${item.telemetry.identity_frame_id} · ${item.telemetry.identity_data_hex}`
|
||||
: item.telemetry.protocol_signature,
|
||||
].filter(Boolean).join(" · ")
|
||||
: null;
|
||||
frame.textContent = item.telemetry?.command_frame
|
||||
? `${item.telemetry.command_frame.frame_id} ${item.telemetry.command_frame.data_hex}`
|
||||
: item.telemetry?.firmware_verified
|
||||
? `${item.telemetry.programmed_this_request ? "FLASHED" : "VERIFIED"} · ${item.telemetry.identity_frame_id} · ${item.telemetry.identity_data_hex}`
|
||||
: firmwareTelemetry
|
||||
? firmwareTelemetry
|
||||
: "CONTROL PLANE · NO CAN";
|
||||
row.append(text, frame);
|
||||
list.append(row);
|
||||
@@ -3527,7 +3588,10 @@ function renderExecution(result) {
|
||||
function renderResult(result) {
|
||||
state.result = result;
|
||||
const reportMode = result.execution_report?.spec?.mode;
|
||||
if (reportMode === "hil") {
|
||||
const feedbackSource = result.execution_report?.metadata?.labels?.seat_feedback;
|
||||
if (feedbackSource === "simulated_hardware") {
|
||||
state.seatFeedbackSource = "simulated_hardware";
|
||||
} else if (reportMode === "hil") {
|
||||
state.seatFeedbackSource = result.execution_report?.spec?.physical_state?.complete
|
||||
? "physical_can"
|
||||
: "physical_partial";
|
||||
@@ -3597,8 +3661,9 @@ async function submitRequest(event) {
|
||||
if (approvalTokenInput) approvalTokenInput.value = "";
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/requests", {
|
||||
const response = await apiRequest("/api/requests", {
|
||||
method: "POST",
|
||||
timeoutMs: requestTimeoutForExecutionMode(state.executionMode),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
utterance,
|
||||
@@ -3606,6 +3671,9 @@ async function submitRequest(event) {
|
||||
conversation_id: state.conversationId,
|
||||
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
|
||||
execution_mode: state.executionMode,
|
||||
...(state.executionMode === "simulated_hardware"
|
||||
? { simulation_fault: $("#simulationFault").value }
|
||||
: {}),
|
||||
...(approvalRequired
|
||||
? { approval_token: approvalToken }
|
||||
: {}),
|
||||
@@ -3622,9 +3690,14 @@ async function submitRequest(event) {
|
||||
await synchronizePhysicalSeatState();
|
||||
}
|
||||
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
|
||||
const simulated = payload.execution_report?.metadata?.labels?.seat_feedback === "simulated_hardware";
|
||||
showToast(completed
|
||||
? (payload.execution_report?.spec?.mode === "hil" ? "真实台架执行与反馈确认已完成" : "可执行动作的 dry-run 已完成")
|
||||
: "请求未执行");
|
||||
? (simulated
|
||||
? "模拟台架执行完成"
|
||||
: payload.execution_report?.spec?.mode === "hil"
|
||||
? "真实台架执行与反馈确认已完成"
|
||||
: "可执行动作的 dry-run 已完成")
|
||||
: simulated ? "模拟故障已触发,详情见执行结果" : "请求未执行");
|
||||
} catch (error) {
|
||||
window.clearInterval(state.progressTimer);
|
||||
resetPipeline();
|
||||
@@ -3637,7 +3710,10 @@ async function submitRequest(event) {
|
||||
const hardwareLabel = state.runtime?.hardware?.approval_required
|
||||
? "授权并真实执行"
|
||||
: "真实执行";
|
||||
button.innerHTML = `<i data-lucide="play"></i><span>${state.executionMode === "hardware" ? hardwareLabel : "生成并执行"}</span>`;
|
||||
const buttonLabel = state.executionMode === "hardware"
|
||||
? hardwareLabel
|
||||
: state.executionMode === "simulated_hardware" ? "运行故障仿真" : "生成并执行";
|
||||
button.innerHTML = `<i data-lucide="play"></i><span>${buttonLabel}</span>`;
|
||||
refreshIcons();
|
||||
}
|
||||
}
|
||||
@@ -3723,6 +3799,17 @@ window.CognitiveAgentConsole = {
|
||||
},
|
||||
};
|
||||
|
||||
window.addEventListener("cognitive-agent:connection", (event) => {
|
||||
const previousStatus = state.connectionStatus;
|
||||
state.connectionStatus = event.detail?.status || "offline";
|
||||
if (["offline", "degraded"].includes(previousStatus)
|
||||
&& state.connectionStatus === "online") {
|
||||
Promise.all([loadAuthStatus(), loadRuntime()])
|
||||
.then(() => showToast("服务连接已恢复"))
|
||||
.catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
initJointTwinLayout();
|
||||
initResultWorkspace();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const VISION_VERSION = "0.10.14";
|
||||
const VISION_MODULE = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${VISION_VERSION}/vision_bundle.mjs`;
|
||||
const VISION_WASM = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${VISION_VERSION}/wasm`;
|
||||
const VISION_MODULE = `/vendor/mediapipe/tasks-vision/${VISION_VERSION}/vision_bundle.mjs`;
|
||||
const VISION_WASM = `/vendor/mediapipe/tasks-vision/${VISION_VERSION}/wasm`;
|
||||
|
||||
const CONNECTIONS = [
|
||||
[0, 1], [1, 2], [2, 3], [3, 4],
|
||||
@@ -464,7 +464,7 @@ async function recognizeLoop() {
|
||||
}
|
||||
|
||||
async function loadGestureRuntime() {
|
||||
const response = await fetch("/api/gesture/runtime", { cache: "no-store" });
|
||||
const response = await window.CognitiveAgentNetwork.request("/api/gesture/runtime", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error("无法读取手势能力配置");
|
||||
state.runtime = await response.json();
|
||||
state.engine = new GestureEngine(state.runtime.safety, executeGestureAction);
|
||||
@@ -477,7 +477,10 @@ async function loadRecognizer() {
|
||||
$("#gestureSafetyText").textContent = "正在加载手势模型…";
|
||||
const { FilesetResolver, GestureRecognizer } = await import(VISION_MODULE);
|
||||
const fileset = await FilesetResolver.forVisionTasks(VISION_WASM);
|
||||
const modelResponse = await fetch("/api/gesture/model", { cache: "force-cache" });
|
||||
const modelResponse = await window.CognitiveAgentNetwork.request("/api/gesture/model", {
|
||||
cache: "force-cache",
|
||||
timeoutMs: 60000,
|
||||
});
|
||||
if (!modelResponse.ok) throw new Error("内置手势模型加载失败");
|
||||
const modelAssetBuffer = new Uint8Array(await modelResponse.arrayBuffer());
|
||||
state.recognizer = await GestureRecognizer.createFromOptions(fileset, {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
@@ -2,16 +2,22 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<meta name="theme-color" content="#146b65" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" href="/icons/app-icon-192.png" />
|
||||
<title>智舱随行 · 一句话与手势控制</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
<script src="https://unpkg.com/lucide@0.468.0/dist/umd/lucide.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/build/three.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/examples/js/libs/fflate.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/examples/js/loaders/FBXLoader.js" defer></script>
|
||||
<script src="/app.js?v=unified-cabin-23" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=gesture-core-3"></script>
|
||||
<link rel="stylesheet" href="/styles.css?v=android-pad-2" />
|
||||
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
|
||||
<script src="/network.js?v=android-pad-1" defer></script>
|
||||
<script src="/app.js?v=seat-hil-7" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=android-pad-1"></script>
|
||||
<script src="/pwa.js?v=android-pad-1" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
@@ -26,6 +32,8 @@
|
||||
<button class="gesture-entry" id="gestureToggleButton" type="button" aria-controls="gestureDrawer" aria-expanded="false">
|
||||
<span class="gesture-entry-dot"></span><i data-lucide="scan-face"></i>手势控制
|
||||
</button>
|
||||
<span class="status-badge neutral connection-badge" id="connectionBadge"><i data-lucide="loader-circle"></i><span>连接中</span></span>
|
||||
<button class="status-badge neutral pwa-badge" id="pwaBadge" type="button" hidden></button>
|
||||
<span class="status-badge neutral" id="executionBadge"><i data-lucide="flask-conical"></i>DRY-RUN</span>
|
||||
<span class="status-badge" id="modelBadge"><i data-lucide="sparkles"></i>模型检测中</span>
|
||||
<span class="status-badge danger" id="hardwareBadge"><i data-lucide="shield-off"></i>硬件检测中</span>
|
||||
@@ -83,18 +91,34 @@
|
||||
|
||||
<div class="field-group">
|
||||
<span class="field-label">执行目标</span>
|
||||
<div class="segmented" role="group" aria-label="执行目标">
|
||||
<div class="segmented execution-mode-options" role="group" aria-label="执行目标">
|
||||
<button type="button" class="mode-option active" data-execution-mode="dry_run">
|
||||
<i data-lucide="flask-conical"></i>虚拟 CAN
|
||||
</button>
|
||||
<button type="button" class="mode-option" id="hardwareMode" data-execution-mode="hardware" disabled>
|
||||
<i data-lucide="radio-tower"></i>真实台架
|
||||
</button>
|
||||
<button type="button" class="mode-option" id="simulationMode" data-execution-mode="simulated_hardware" disabled>
|
||||
<i data-lucide="bug-play"></i>模拟台架
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="hardwareNote">正在检查 CAN 适配器与 IAR 固件配置</p>
|
||||
<p class="provider-note" id="hardwareAliasNote">所有座椅能力仅控制副驾实体电机、加热与通风执行器</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group simulation-fault-field" id="simulationFaultField" hidden>
|
||||
<label class="field-label" for="simulationFault">故障注入场景</label>
|
||||
<select id="simulationFault" aria-label="选择模拟硬件故障">
|
||||
<option value="normal">正常反馈</option>
|
||||
<option value="feedback_timeout">反馈超时</option>
|
||||
<option value="adapter_disconnect">适配器断开</option>
|
||||
<option value="motor_stall">电机堵转</option>
|
||||
<option value="firmware_mismatch">固件不匹配</option>
|
||||
<option value="partial_feedback">部分反馈</option>
|
||||
</select>
|
||||
<p class="provider-note">仅生成模拟 HIL 结果,不会打开物理 CAN 适配器或调用 IAR。</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group vehicle-field">
|
||||
<label class="field-label" for="vehicleSelect">车型配置</label>
|
||||
<div class="vehicle-select-control">
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="color-scheme" content="light" />
|
||||
<title>登录 · Cognitive Agent</title>
|
||||
<link rel="stylesheet" href="/login.css" />
|
||||
<script src="/network.js?v=android-pad-1" defer></script>
|
||||
<script src="/login.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -4,7 +4,7 @@ function nextPath() {
|
||||
}
|
||||
|
||||
async function readAuthStatus() {
|
||||
const response = await fetch("/api/auth/status", { cache: "no-store" });
|
||||
const response = await window.CognitiveAgentNetwork.request("/api/auth/status", { cache: "no-store" });
|
||||
if (!response.ok) return;
|
||||
const status = await response.json();
|
||||
if (status.authenticated) window.location.replace(nextPath());
|
||||
@@ -18,7 +18,7 @@ async function submitLogin(event) {
|
||||
message.textContent = "";
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/login", {
|
||||
const response = await window.CognitiveAgentNetwork.request("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Cognitive Agent PAD Console",
|
||||
"short_name": "Cognitive Agent",
|
||||
"description": "车舱认知编排、模拟台架与手势控制 PAD 控制台",
|
||||
"lang": "zh-CN",
|
||||
"start_url": "/?source=pwa",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#eef3f1",
|
||||
"theme_color": "#146b65",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/app-icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/app-icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
(function initializeCognitiveAgentNetwork() {
|
||||
const nativeFetch = window.fetch.bind(window);
|
||||
const listeners = new Set();
|
||||
const state = {
|
||||
status: navigator.onLine === false ? "offline" : "connecting",
|
||||
lastSuccessAt: null,
|
||||
lastError: null,
|
||||
};
|
||||
let probeTimer = null;
|
||||
|
||||
function snapshot() {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
function renderBadge() {
|
||||
const badge = document.querySelector("#connectionBadge");
|
||||
if (!badge) return;
|
||||
const presentation = {
|
||||
connecting: ["neutral", "loader-circle", "连接中"],
|
||||
online: ["", "wifi", "服务已连接"],
|
||||
degraded: ["warning", "wifi-low", "连接不稳定"],
|
||||
offline: ["danger", "wifi-off", "服务已断开"],
|
||||
}[state.status];
|
||||
badge.className = `status-badge connection-badge ${presentation[0]}`.trim();
|
||||
badge.innerHTML = `<i data-lucide="${presentation[1]}"></i><span>${presentation[2]}</span>`;
|
||||
badge.title = state.lastError || presentation[2];
|
||||
window.lucide?.createIcons?.({
|
||||
attrs: { "aria-hidden": "true" },
|
||||
nodes: [badge],
|
||||
});
|
||||
}
|
||||
|
||||
function publish(status, error = null) {
|
||||
const changed = state.status !== status || state.lastError !== error;
|
||||
state.status = status;
|
||||
state.lastError = error;
|
||||
if (status === "online") state.lastSuccessAt = new Date().toISOString();
|
||||
renderBadge();
|
||||
if (!changed) return;
|
||||
const detail = snapshot();
|
||||
listeners.forEach((listener) => listener(detail));
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:connection", { detail }));
|
||||
}
|
||||
|
||||
function sleep(milliseconds) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
async function request(input, options = {}) {
|
||||
const {
|
||||
timeoutMs = 10000,
|
||||
retries: configuredRetries,
|
||||
signal: callerSignal,
|
||||
...fetchOptions
|
||||
} = options;
|
||||
const method = String(fetchOptions.method || "GET").toUpperCase();
|
||||
const retries = configuredRetries ?? (method === "GET" ? 1 : 0);
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
||||
const controller = new AbortController();
|
||||
const abortFromCaller = () => controller.abort(callerSignal?.reason);
|
||||
callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
||||
const timeout = window.setTimeout(() => controller.abort("timeout"), timeoutMs);
|
||||
try {
|
||||
const response = await nativeFetch(input, {
|
||||
...fetchOptions,
|
||||
signal: controller.signal,
|
||||
});
|
||||
publish(response.status >= 500 ? "degraded" : "online",
|
||||
response.status >= 500 ? `服务返回 HTTP ${response.status}` : null);
|
||||
return response;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const message = controller.signal.aborted && !callerSignal?.aborted
|
||||
? `请求超时(${Math.round(timeoutMs / 1000)} 秒)`
|
||||
: (error?.message || "网络请求失败");
|
||||
publish(attempt < retries ? "degraded" : "offline", message);
|
||||
if (callerSignal?.aborted || attempt >= retries || method !== "GET") {
|
||||
throw new Error(message, { cause: error });
|
||||
}
|
||||
await sleep(300 * (attempt + 1));
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
callerSignal?.removeEventListener("abort", abortFromCaller);
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function probe() {
|
||||
if (document.hidden) return;
|
||||
if (navigator.onLine === false) {
|
||||
publish("offline", "设备当前处于离线状态");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await request("/api/health", {
|
||||
cache: "no-store",
|
||||
timeoutMs: 4000,
|
||||
retries: 0,
|
||||
});
|
||||
if (!response.ok && response.status !== 401) {
|
||||
publish("degraded", `健康检查返回 HTTP ${response.status}`);
|
||||
}
|
||||
} catch (_error) {
|
||||
// request() already published the detailed state.
|
||||
}
|
||||
}
|
||||
|
||||
function startMonitoring() {
|
||||
if (probeTimer !== null) return;
|
||||
renderBadge();
|
||||
probe();
|
||||
probeTimer = window.setInterval(probe, 10000);
|
||||
window.addEventListener("online", () => {
|
||||
publish("connecting");
|
||||
probe();
|
||||
});
|
||||
window.addEventListener("offline", () => publish("offline", "设备当前处于离线状态"));
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden) probe();
|
||||
});
|
||||
}
|
||||
|
||||
window.CognitiveAgentNetwork = {
|
||||
request,
|
||||
probe,
|
||||
subscribe(listener) {
|
||||
listeners.add(listener);
|
||||
listener(snapshot());
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
getState: snapshot,
|
||||
nativeFetch,
|
||||
};
|
||||
document.addEventListener("DOMContentLoaded", startMonitoring, { once: true });
|
||||
})();
|
||||
@@ -0,0 +1,40 @@
|
||||
(function initializePwa() {
|
||||
function notify(message) {
|
||||
window.CognitiveAgentConsole?.notify?.(message);
|
||||
}
|
||||
|
||||
function showUpdate(registration) {
|
||||
const badge = document.querySelector("#pwaBadge");
|
||||
if (!badge) return;
|
||||
badge.hidden = false;
|
||||
badge.innerHTML = '<i data-lucide="refresh-cw"></i><span>发现更新</span>';
|
||||
badge.onclick = () => registration.waiting?.postMessage({ type: "SKIP_WAITING" });
|
||||
window.lucide?.createIcons?.({ attrs: { "aria-hidden": "true" }, nodes: [badge] });
|
||||
}
|
||||
|
||||
async function register() {
|
||||
if (!("serviceWorker" in navigator) || !window.isSecureContext) return;
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register("/service-worker.js", {
|
||||
scope: "/",
|
||||
});
|
||||
if (registration.waiting) showUpdate(registration);
|
||||
registration.addEventListener("updatefound", () => {
|
||||
const worker = registration.installing;
|
||||
worker?.addEventListener("statechange", () => {
|
||||
if (worker.state === "installed" && navigator.serviceWorker.controller) {
|
||||
showUpdate(registration);
|
||||
}
|
||||
});
|
||||
});
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => window.location.reload());
|
||||
navigator.serviceWorker.addEventListener("message", (event) => {
|
||||
if (event.data?.type === "OFFLINE_READY") notify("PAD 离线资源已准备完成");
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("PWA registration failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("load", register, { once: true });
|
||||
})();
|
||||
@@ -0,0 +1,84 @@
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v7";
|
||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||
const APP_SHELL = [
|
||||
"/",
|
||||
"/index.html",
|
||||
"/styles.css?v=android-pad-2",
|
||||
"/network.js?v=android-pad-1",
|
||||
"/app.js?v=seat-hil-7",
|
||||
"/gesture-control.js?v=android-pad-1",
|
||||
"/pwa.js?v=android-pad-1",
|
||||
"/manifest.webmanifest",
|
||||
"/icons/app-icon-192.png",
|
||||
"/icons/app-icon-512.png",
|
||||
"/vendor/lucide/0.468.0/lucide.min.js",
|
||||
"/vendor/three/0.147.0/three.min.js",
|
||||
"/vendor/three/0.147.0/fflate.min.js",
|
||||
"/vendor/three/0.147.0/FBXLoader.js",
|
||||
"/vendor/mediapipe/tasks-vision/0.10.14/vision_bundle.mjs",
|
||||
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_internal.js",
|
||||
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_internal.wasm",
|
||||
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_nosimd_internal.js",
|
||||
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_nosimd_internal.wasm"
|
||||
];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(SHELL_CACHE)
|
||||
.then((cache) => cache.addAll(APP_SHELL))
|
||||
.then(() => self.clients.matchAll({ includeUncontrolled: true }))
|
||||
.then((clients) => clients.forEach((client) => client.postMessage({ type: "OFFLINE_READY" })))
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((keys) => Promise.all(
|
||||
keys.filter((key) => key.startsWith("cognitive-agent-pad-")
|
||||
&& ![SHELL_CACHE, RUNTIME_CACHE].includes(key))
|
||||
.map((key) => caches.delete(key))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
async function cacheFirst(request) {
|
||||
const cached = await caches.match(request);
|
||||
if (cached) return cached;
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
const cache = await caches.open(RUNTIME_CACHE);
|
||||
await cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async function navigationResponse(request) {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
const cache = await caches.open(RUNTIME_CACHE);
|
||||
await cache.put("/index.html", response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch (_error) {
|
||||
return (await caches.match("/index.html")) || Response.error();
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
if (url.origin !== self.location.origin || event.request.method !== "GET") return;
|
||||
if (event.request.mode === "navigate") {
|
||||
event.respondWith(navigationResponse(event.request));
|
||||
return;
|
||||
}
|
||||
if (url.pathname.startsWith("/api/") && url.pathname !== "/api/gesture/model") return;
|
||||
event.respondWith(cacheFirst(event.request));
|
||||
});
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data?.type === "SKIP_WAITING") self.skipWaiting();
|
||||
});
|
||||
@@ -4356,3 +4356,172 @@ pre {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Android PAD, touch and installable-app refinements ---------------------- */
|
||||
.execution-mode-options {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.simulation-fault-field select {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
padding: 0 11px;
|
||||
color: var(--ink);
|
||||
background: var(--surface-muted);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 6px;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-badge.warning {
|
||||
color: #7a5416;
|
||||
background: var(--amber-soft);
|
||||
border-color: #e9cc96;
|
||||
}
|
||||
|
||||
.connection-badge svg[data-lucide="loader-circle"] {
|
||||
animation: spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
.pwa-badge {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pwa-badge[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@supports (min-height: 100dvh) {
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
min-height: calc(100dvh - 70px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
button,
|
||||
select,
|
||||
input,
|
||||
.segment,
|
||||
.mode-option,
|
||||
.tab,
|
||||
.example-command,
|
||||
.gesture-entry,
|
||||
.status-badge.auth-logout {
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.example-command:hover,
|
||||
.segment:hover,
|
||||
.mode-option:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.topbar {
|
||||
min-height: 64px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-top: max(10px, env(safe-area-inset-top));
|
||||
padding-right: max(14px, env(safe-area-inset-right));
|
||||
padding-left: max(14px, env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
.brand-block p,
|
||||
#hardwareBadge,
|
||||
#modelBadge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.runtime-badges {
|
||||
flex: 1;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-inline: contain;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.runtime-badges .status-badge#connectionBadge {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.runtime-badges::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.command-panel {
|
||||
width: auto;
|
||||
max-height: none;
|
||||
position: static;
|
||||
padding: 22px max(18px, env(safe-area-inset-right)) 24px max(18px, env(safe-area-inset-left));
|
||||
overflow: visible;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
padding-right: max(16px, env(safe-area-inset-right));
|
||||
padding-bottom: max(42px, env(safe-area-inset-bottom));
|
||||
padding-left: max(16px, env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
.pipeline {
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-inline: contain;
|
||||
}
|
||||
|
||||
.pipeline-step {
|
||||
min-width: 96px;
|
||||
}
|
||||
|
||||
.gesture-drawer {
|
||||
width: min(520px, 100vw);
|
||||
padding-right: max(18px, env(safe-area-inset-right));
|
||||
padding-bottom: max(22px, env(safe-area-inset-bottom));
|
||||
padding-left: max(18px, env(safe-area-inset-left));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.execution-mode-options {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.runtime-badges #executionBadge,
|
||||
.pwa-badge {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-height: 720px) and (min-width: 901px) {
|
||||
.topbar {
|
||||
min-height: 58px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
min-height: calc(100dvh - 58px);
|
||||
}
|
||||
|
||||
.command-panel,
|
||||
.main-panel {
|
||||
padding-top: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+20
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
+3681
File diff suppressed because it is too large
Load Diff
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright © 2010-2022 three.js authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user