From 1ec1353c0bf9e23f29f5fec0938438217bf7ce5f Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Tue, 15 Sep 2026 21:11:21 +0000 Subject: [PATCH] Add Assist mode: primitive note display for practicing from paper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New game mode 'assist' — no trainer, no scoring: - Large display of the currently mic-detected note - Live notation staff (same pipeline as the pitch monitor) - Live fretboard showing alternative positions for the played note (respects the fret-window practice filter) - Tuner and Metronome remain the shared footer tools, identical in all modes; user can practice own-pace from paper with a free metronome Header mode toggle gains 'Assist'; 'm' shortcut cycles back to sight reading from assist/phrase. Export PitchData type. --- src/App.css | 46 ++++++++++++++++++ src/App.tsx | 39 +++++++++++++-- src/components/AssistMode.tsx | 89 +++++++++++++++++++++++++++++++++++ src/hooks/usePitchDetector.ts | 2 +- src/types/SettingsTypes.ts | 2 +- 5 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 src/components/AssistMode.tsx diff --git a/src/App.css b/src/App.css index cc43cf1..6dbdcf0 100644 --- a/src/App.css +++ b/src/App.css @@ -1126,3 +1126,49 @@ button { justify-content: safe center; justify-content: center; } + +/* ---- Assist Mode ------------------------------------------------------- */ +/* Primitive practice companion: big detected note + live staff + optional + fretboard with alternative positions. No trainer actions. */ + +.assist-card { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-lg); +} + +.assist-note-area { + display: flex; + align-items: center; + justify-content: center; + min-height: 110px; +} + +.assist-note-name { + font-size: 5rem; + font-weight: 800; + line-height: 1; + color: var(--color-text-main); + font-variant-emoji: text; +} + +.assist-note-name.placeholder { + color: var(--color-text-muted); + opacity: 0.5; +} + +.assist-fretboard { + width: 100%; + display: flex; + justify-content: center; + overflow-x: auto; +} + +.assist-hint { + font-size: 12px; + color: var(--color-text-muted); + margin: 0; + text-align: center; +} diff --git a/src/App.tsx b/src/App.tsx index ec5f151..4174bc7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { SingleNoteTrainer, type SingleNoteHandle } from './components/SingleNot import { LiveNoteStaff } from './components/LiveNoteStaff'; import { resolveClefTranspose } from './music/InstrumentConfigs'; import { PhraseTrainer, type PhraseHandle } from './components/PhraseTrainer'; +import { AssistMode } from './components/AssistMode'; import { usePitchDetector } from './hooks/usePitchDetector'; import { useSettings } from './context/useSettings'; import { audioEngine } from './audio/AudioEngine'; @@ -26,6 +27,7 @@ function App() { const setSettings = updateSettings; const isPhrase = settings.gameMode === 'phrase'; + const isAssist = settings.gameMode === 'assist'; const currentTuning = TUNINGS[settings.tuningId]; const currentInstrumentDef = INSTRUMENT_DEFINITIONS[settings.instrument]; @@ -175,6 +177,25 @@ function App() { return; } + if (isAssist) { + // Assist mode: only display shortcuts — no trainer actions. + switch (e.key.toLowerCase()) { + case 'h': + setSettings(s => ({ ...s, showHint: !s.showHint })); + break; + case 'z': + setSettings(s => ({ ...s, zenMode: !s.zenMode })); + break; + case 'v': + setSettings(s => ({ ...s, showFretboard: !s.showFretboard })); + break; + case 'l': + setListening(l => !l); + break; + } + return; + } + switch (e.key.toLowerCase()) { case 'h': setSettings(s => ({ ...s, showHint: !s.showHint })); @@ -207,16 +228,16 @@ function App() { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [settings, showHelp, isPhrase, setSettings]); + }, [settings, showHelp, isPhrase, isAssist, setSettings]); - // 'm' cycles all three modes from phrase mode too. + // 'm' cycles back to sight reading from phrase/assist modes. useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key.toLowerCase() !== 'm') return; const target = e.target as HTMLElement | null; if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLButtonElement || target instanceof HTMLSelectElement || target?.isContentEditable) return; - if (settings.gameMode === 'phrase') { + if (settings.gameMode === 'phrase' || settings.gameMode === 'assist') { setSettings(s => ({ ...s, gameMode: 'sight_reading' })); } }; @@ -263,6 +284,13 @@ function App() { > Phrases +