import { useEffect } from 'react'; import './App.css'; import './styles/shared.css'; import { useProjectStore } from './stores/projectStore'; import { useGlobalKeyboardHandler } from './hooks/useGlobalKeyboardHandler'; import Toolbar from './components/Toolbar'; import StatusBar from './components/StatusBar'; import MainContent from './components/MainContent'; import InstrumentSelection from './components/InstrumentSelection'; import ChatBox from './components/ChatBox'; import { SettingsPanel } from './components/settings'; import LoadingOverlay from './components/common/LoadingOverlay'; import { useEffect as useEffectReact, useState, useRef } from 'react'; import { KGToneBuffersPool } from './core/audio-interface/KGToneBuffersPool'; import { KGCore } from './core/KGCore'; import { ConfigManager } from './core/config/ConfigManager'; import { validateFunctionalChordsJSON } from './util/scaleUtil'; function App() { // Enable global keyboard handler for copy/paste and undo/redo useGlobalKeyboardHandler(); // Use project store instead of local state for project name and tracks const { refreshStatus, loadProject, showChatBox, showSettings, setShowSettings, initializeFromConfig, showInstrumentSelection } = useProjectStore(); // Track if app has been initialized to prevent multiple initializations const hasInitialized = useRef(false); // Load project when component mounts useEffect(() => { // Guard against multiple initializations (can happen due to React Strict Mode or rerenders) if (hasInitialized.current) { return; } hasInitialized.current = true; const initializeApp = async () => { // Load the current project from KGCore loadProject(null); // Initialize ConfigManager first to load config.json and user settings await ConfigManager.instance().initialize(); // Initialize store from config after ConfigManager is ready await initializeFromConfig(); // Load all mode and chord data files in parallel try { const [functionalChordsResponse] = await Promise.all([ fetch(`${import.meta.env.BASE_URL}resources/modes/functional_chords.json`) ]); const [functionalChordsData] = await Promise.all([ functionalChordsResponse.json() ]); // Store original functional chords data KGCore.ORIGINAL_FUNCTIONAL_CHORDS_DATA = functionalChordsData; console.log(`Loaded original functional chords for ${Object.keys(functionalChordsData).length} modes`); // Check if custom chord definition exists and is valid const configManager = ConfigManager.instance(); const customDefinition = configManager.get('chord_guide.chord_definition') as string; if (customDefinition && customDefinition.trim()) { const validationResult = validateFunctionalChordsJSON(customDefinition); if (validationResult.valid) { KGCore.FUNCTIONAL_CHORDS_DATA = JSON.parse(customDefinition); console.log('Using custom chord definition from settings'); } else { KGCore.FUNCTIONAL_CHORDS_DATA = functionalChordsData; console.log('Custom chord definition invalid, using original'); } } else { KGCore.FUNCTIONAL_CHORDS_DATA = functionalChordsData; console.log('No custom chord definition, using original'); } } catch (error) { console.error('Failed to load mode/chord data:', error); // Fallback to defaults const fallbackData = { ionian: { name: 'Ionian', steps: [2, 2, 1, 2, 2, 2, 1], T: [], S: [], D: [], chords: {} } }; KGCore.ORIGINAL_FUNCTIONAL_CHORDS_DATA = fallbackData; KGCore.FUNCTIONAL_CHORDS_DATA = fallbackData; } // Log maxBars after initialization completes const currentMaxBars = useProjectStore.getState().maxBars; console.log(`Project max bars: ${currentMaxBars}`); }; initializeApp(); }, [loadProject, initializeFromConfig]); // Refresh status periodically to ensure UI is in sync with KGCore useEffect(() => { // Initial refresh refreshStatus(); // Set up interval to refresh status every second const intervalId = setInterval(() => { refreshStatus(); }, 1000); // Clean up interval on unmount return () => clearInterval(intervalId); }, [refreshStatus]); return (
{/* Top Toolbar */} {/* Main Display Area containing MainContent, ChatBox, and Settings */}
{showSettings && setShowSettings(false)} />} {!showSettings && ( <> {showInstrumentSelection && } )}
{/* Status Bar */} {/* Global Loading Overlay for instrument buffer loading */}
); } export default App; // Local component to subscribe to pool events and manage a counter const GlobalLoadingOverlayContainer: React.FC = () => { const [loadingCount, setLoadingCount] = useState(() => KGToneBuffersPool.instance().getActiveLoadCount()); const [overdue, setOverdue] = useState(false); const timeoutRef = useRef(null); useEffectReact(() => { const pool = KGToneBuffersPool.instance(); const listener = (evt: { type: 'start' | 'end'; instrument: string }) => { setLoadingCount(prev => { if (evt.type === 'start') return prev + 1; return Math.max(0, prev - 1); }); }; pool.addLoadingListener(listener); return () => { pool.removeLoadingListener(listener); }; }, []); // Handle long-running overlay: after 10s, hide and alert the user, but allow future loads to show overlay again useEffectReact(() => { // When loading starts, start a 30s timer if not already overdue/timed if (loadingCount > 0 && !overdue && timeoutRef.current === null) { timeoutRef.current = window.setTimeout(() => { // Only trigger if still loading if (loadingCount > 0) { setOverdue(true); // Friendly alert to the user window.alert( 'Loading resources is taking longer than expected and may have partially failed. If you notice any playback issues, please refresh the page to retry downloading the audio files.' ); } // Clear the timeout handle timeoutRef.current = null; }, 10000); } // When loading finishes, clear timer and reset overdue state if (loadingCount === 0) { if (timeoutRef.current !== null) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } if (overdue) setOverdue(false); } // Cleanup on unmount: clear any pending timer return () => { if (timeoutRef.current !== null) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }; }, [loadingCount, overdue]); return ( 0 && !overdue} message={loadingCount > 1 ? `Loading ... (${loadingCount})` : 'Loading ...'} /> ); };