From 7cea41c911d856905922ee2f041fb04bd1fc8ded Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 11 May 2026 18:00:36 +0800 Subject: [PATCH] Add frontend debug mode gate --- .env.example | 4 ++ .hermes/shared-memory/decision-log.md | 8 +++ ...IME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md | 6 ++ .../PuzzleRuntimeShell.test.tsx | 50 +++++++++++++++- .../puzzle-runtime/PuzzleRuntimeShell.tsx | 57 ++++++++++++++----- src/config/debugMode.ts | 23 ++++++++ src/vite-env.d.ts | 4 ++ 7 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 src/config/debugMode.ts diff --git a/.env.example b/.env.example index e8b61440..74c656dc 100644 --- a/.env.example +++ b/.env.example @@ -173,6 +173,10 @@ VITE_SCENE_IMAGE_REQUEST_TIMEOUT_MS="150000" # Keep this off by default for cleaner logs. VITE_LLM_DEBUG_LOG="false" +# Optional: global frontend debug mode. When empty, it follows Vite dev mode. +# Set to "true" to expose local diagnostic panels, or "false" to hide them. +VITE_DEBUG_MODE="" + # Optional: official VikingDB credentials for regenerating build-tag similarities # with the Python embedding script. The script auto-loads `.env.local` and uses # the fixed `bge-large-zh` embedding model. diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index ce5249ab..2d0c0c6d 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -32,6 +32,14 @@ - 验证方式:执行 `npm run test -- src\services\input-devices\runtimeDragInputController.test.ts`、`npm run test -- src\components\puzzle-runtime\PuzzleRuntimeShell.test.tsx`、`npm run typecheck` 和编码检查。 - 关联文档:`docs/technical/RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md`、`docs/technical/PUZZLE_RUNTIME_FRONTEND_LOGIC_REHOME_2026-05-02.md`。 +## 2026-05-11 前端调试模式统一判断 + +- 背景:拼图 mocap 调试面板此前在运行态常驻展示,生产构建和正式体验里容易遮挡棋盘内容;后续其它局部诊断 UI 也需要统一的调试模式入口。 +- 决策:前端新增 `src/config/debugMode.ts` 作为全局调试模式判断,默认跟随 Vite 开发态,允许 `VITE_DEBUG_MODE=true/false` 显式覆盖。拼图运行态 mocap 调试面板只在调试模式下渲染,并默认折叠,只保留连接状态行。 +- 影响范围:前端局部调试 UI、拼图运行态 mocap 诊断面板、`.env.example` 和运行态输入技术文档。 +- 验证方式:执行 `npm run test -- src\components\puzzle-runtime\PuzzleRuntimeShell.test.tsx`、`npm run typecheck` 和编码检查。 +- 关联文档:`docs/technical/RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md`。 + ## 2026-05-10 儿童动作热身关直接消费 mocap 数据源 - 背景:儿童动作 Demo 不能只依赖浏览器摄像头状态和键鼠调试输入,否则真实硬件接入后会出现“mocap 在线但页面提示摄像头不可用”或“能看到画面但动作不推进”的卡点。 diff --git a/docs/technical/RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md b/docs/technical/RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md index d7e00cfc..fc03df58 100644 --- a/docs/technical/RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md +++ b/docs/technical/RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md @@ -26,6 +26,12 @@ - mocap 光标按 60Hz 插值更新 UI 位置,并在拖拽中用插值后的当前点持续驱动输入层,避免输入包帧率低或抖动时出现明显跳变。 - 合并大块由拼图运行态把手部坐标命中到任一成员拼块;本地拼图运行时再按 `mergedGroupId` 执行整组平移。 +## 调试模式 + +前端全局调试模式统一通过 `src/config/debugMode.ts` 判断。默认跟随 Vite 开发态:`import.meta.env.DEV` 为真时开启,生产构建默认关闭;如需显式覆盖,可设置 `VITE_DEBUG_MODE=true` 或 `VITE_DEBUG_MODE=false`。 + +拼图运行态的 mocap 调试面板只在全局调试模式下渲染。面板默认折叠,只保留一行连接状态,展开后才显示动作、手势、解析告警和原始包预览,避免开发诊断信息遮挡拼图棋盘和底部操作。 + ## 接入规则 新玩法或新设备接入时遵循以下边界: diff --git a/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx b/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx index 283c874b..51043905 100644 --- a/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx +++ b/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx @@ -1,7 +1,7 @@ /* @vitest-environment jsdom */ import { act, fireEvent, render, screen, within } from '@testing-library/react'; -import { expect, test, vi } from 'vitest'; +import { beforeEach, expect, test, vi } from 'vitest'; import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession'; import { AuthUiContext } from '../auth/AuthUiContext'; @@ -31,6 +31,15 @@ const mocapMock = vi.hoisted(() => ({ y: 0.58, })); +const debugModeMock = vi.hoisted(() => ({ + enabled: true, +})); + +vi.mock('../../config/debugMode', () => ({ + IS_DEBUG_MODE: debugModeMock.enabled, + isDebugMode: () => debugModeMock.enabled, +})); + vi.mock('../../services/useMocapInput', () => ({ useMocapInput: () => ({ status: 'connected', @@ -44,6 +53,13 @@ vi.mock('../../services/useMocapInput', () => ({ }), })); +beforeEach(() => { + debugModeMock.enabled = true; + mocapMock.state = 'grab'; + mocapMock.x = 0.42; + mocapMock.y = 0.58; +}); + function createAuthValue() { return { user: null, @@ -157,7 +173,7 @@ const clearedRun: PuzzleRunSnapshot = { }, }; -test('拼图界面显示 mocap 连接状态和最近动作调试信息', () => { +test('调试模式下拼图界面折叠展示 mocap 连接状态,展开后显示最近动作调试信息', () => { renderPuzzleRuntime( { const debugPanel = screen.getByTestId('puzzle-mocap-debug'); expect(within(debugPanel).getByText('mocap: connected')).toBeTruthy(); + const toggleButton = within(debugPanel).getByRole('button', { + name: 'mocap: connected', + }); + expect(toggleButton.getAttribute('aria-expanded')).toBe('false'); + expect(within(debugPanel).queryByText('动作: grab')).toBeNull(); + + fireEvent.click(toggleButton); + + expect(toggleButton.getAttribute('aria-expanded')).toBe('true'); expect(within(debugPanel).getByText('动作: grab')).toBeTruthy(); expect(within(debugPanel).getByText('手势: grab @ 0.42, 0.58')).toBeTruthy(); expect(within(debugPanel).getByText('解析: 无')).toBeTruthy(); expect(within(debugPanel).getByText(/原始:/)).toBeTruthy(); }); +test('非调试模式下拼图界面不渲染 mocap 调试面板', () => { + debugModeMock.enabled = false; + renderPuzzleRuntime( + , + ); + + expect(screen.queryByTestId('puzzle-mocap-debug')).toBeNull(); +}); + test('拼图界面在 mocap open_palm 时显示体感光标', () => { mocapMock.state = 'open_palm'; mocapMock.x = 0.42; diff --git a/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx b/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx index 3125ae89..f6f082d9 100644 --- a/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx +++ b/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx @@ -1,6 +1,8 @@ import { ArrowLeft, ArrowRight, + ChevronDown, + ChevronUp, Clock, Eye, Lightbulb, @@ -22,6 +24,7 @@ import type { PuzzleRuntimePropKind, SwapPuzzlePiecesRequest, } from '../../../packages/shared/src/contracts/puzzleRuntimeSession'; +import { isDebugMode } from '../../config/debugMode'; import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl'; import { createRuntimeDragInputController, @@ -361,6 +364,7 @@ export function PuzzleRuntimeShell({ const [isFreezeEffectVisible, setIsFreezeEffectVisible] = useState(false); const [isPropConfirming, setIsPropConfirming] = useState(false); const [propConfirmError, setPropConfirmError] = useState(null); + const [isMocapDebugExpanded, setIsMocapDebugExpanded] = useState(false); const [hintDemo, setHintDemo] = useState(null); const [mergeFlash, setMergeFlash] = useState( null, @@ -462,6 +466,7 @@ export function PuzzleRuntimeShell({ ? mocapInput.latestCommand.parseWarnings.join(';') : '无'; const mocapRawPacketLabel = mocapInput.rawPacketPreview?.text ?? '未收到'; + const shouldShowMocapDebugPanel = isDebugMode(); useEffect(() => { currentLevelRef.current = currentLevel; @@ -1744,19 +1749,45 @@ export function PuzzleRuntimeShell({ 已选择 ) : null} -
-
mocap: {mocapInput.status}
-
动作: {mocapActionsLabel}
-
手势: {mocapHandLabel}
-
解析: {mocapParseWarningLabel}
-
- 原始: {mocapRawPacketLabel} -
- {mocapInput.error ?
错误: {mocapInput.error}
: null} -
+ {shouldShowMocapDebugPanel ? ( +
+ + {isMocapDebugExpanded ? ( +
+
动作: {mocapActionsLabel}
+
手势: {mocapHandLabel}
+
解析: {mocapParseWarningLabel}
+
+ 原始: {mocapRawPacketLabel} +
+ {mocapInput.error ?
错误: {mocapInput.error}
: null} +
+ ) : null} +
+ ) : null} {canShowNextAction ? (