Add frontend debug mode gate
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 在线但页面提示摄像头不可用”或“能看到画面但动作不推进”的卡点。
|
||||
|
||||
@@ -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 调试面板只在全局调试模式下渲染。面板默认折叠,只保留一行连接状态,展开后才显示动作、手势、解析告警和原始包预览,避免开发诊断信息遮挡拼图棋盘和底部操作。
|
||||
|
||||
## 接入规则
|
||||
|
||||
新玩法或新设备接入时遵循以下边界:
|
||||
|
||||
@@ -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(
|
||||
<PuzzleRuntimeShell
|
||||
run={{
|
||||
@@ -176,12 +192,42 @@ test('拼图界面显示 mocap 连接状态和最近动作调试信息', () => {
|
||||
|
||||
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(
|
||||
<PuzzleRuntimeShell
|
||||
run={{
|
||||
...clearedRun,
|
||||
currentLevel: {
|
||||
...clearedRun.currentLevel!,
|
||||
status: 'playing',
|
||||
},
|
||||
}}
|
||||
onBack={vi.fn()}
|
||||
onSwapPieces={vi.fn()}
|
||||
onDragPiece={vi.fn()}
|
||||
onAdvanceNextLevel={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('puzzle-mocap-debug')).toBeNull();
|
||||
});
|
||||
|
||||
test('拼图界面在 mocap open_palm 时显示体感光标', () => {
|
||||
mocapMock.state = 'open_palm';
|
||||
mocapMock.x = 0.42;
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [isMocapDebugExpanded, setIsMocapDebugExpanded] = useState(false);
|
||||
const [hintDemo, setHintDemo] = useState<PuzzleHintDemoState | null>(null);
|
||||
const [mergeFlash, setMergeFlash] = useState<PuzzleMergeFlashState | null>(
|
||||
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,11 +1749,34 @@ export function PuzzleRuntimeShell({
|
||||
已选择
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
{shouldShowMocapDebugPanel ? (
|
||||
<section
|
||||
data-testid="puzzle-mocap-debug"
|
||||
className="w-[min(92vw,34rem)] rounded-[0.9rem] border border-white/20 bg-slate-950/70 px-3 py-2 font-mono text-[10px] leading-4 text-white shadow-[0_12px_32px_rgba(15,23,42,0.25)] backdrop-blur"
|
||||
className="w-[min(92vw,34rem)] overflow-hidden rounded-[0.9rem] border border-white/20 bg-slate-950/70 font-mono text-[10px] leading-4 text-white shadow-[0_12px_32px_rgba(15,23,42,0.25)] backdrop-blur"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isMocapDebugExpanded}
|
||||
aria-controls="puzzle-mocap-debug-content"
|
||||
onClick={() => {
|
||||
setIsMocapDebugExpanded((current) => !current);
|
||||
}}
|
||||
className="flex min-h-9 w-full items-center justify-between gap-3 px-3 py-2 text-left transition hover:bg-white/10"
|
||||
>
|
||||
<span className="min-w-0 truncate">
|
||||
mocap: {mocapInput.status}
|
||||
</span>
|
||||
{isMocapDebugExpanded ? (
|
||||
<ChevronDown className="h-3.5 w-3.5 shrink-0" />
|
||||
) : (
|
||||
<ChevronUp className="h-3.5 w-3.5 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
{isMocapDebugExpanded ? (
|
||||
<div
|
||||
id="puzzle-mocap-debug-content"
|
||||
className="border-t border-white/10 px-3 pb-2 pt-2"
|
||||
>
|
||||
<div>mocap: {mocapInput.status}</div>
|
||||
<div>动作: {mocapActionsLabel}</div>
|
||||
<div>手势: {mocapHandLabel}</div>
|
||||
<div>解析: {mocapParseWarningLabel}</div>
|
||||
@@ -1757,6 +1785,9 @@ export function PuzzleRuntimeShell({
|
||||
</div>
|
||||
{mocapInput.error ? <div>错误: {mocapInput.error}</div> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
{canShowNextAction ? (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
23
src/config/debugMode.ts
Normal file
23
src/config/debugMode.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
const DEBUG_MODE_TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
||||
const DEBUG_MODE_FALSE_VALUES = new Set(['0', 'false', 'no', 'off']);
|
||||
|
||||
function parseOptionalBoolean(value: string | undefined) {
|
||||
const normalizedValue = value?.trim().toLowerCase();
|
||||
if (!normalizedValue) {
|
||||
return null;
|
||||
}
|
||||
if (DEBUG_MODE_TRUE_VALUES.has(normalizedValue)) {
|
||||
return true;
|
||||
}
|
||||
if (DEBUG_MODE_FALSE_VALUES.has(normalizedValue)) {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const IS_DEBUG_MODE =
|
||||
parseOptionalBoolean(import.meta.env.VITE_DEBUG_MODE) ?? import.meta.env.DEV;
|
||||
|
||||
export function isDebugMode() {
|
||||
return IS_DEBUG_MODE;
|
||||
}
|
||||
4
src/vite-env.d.ts
vendored
4
src/vite-env.d.ts
vendored
@@ -1 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_DEBUG_MODE?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user