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 KGOnePanel from './components/KGOnePanel'; import { useEffect as useEffectReact, useState, useRef } from 'react'; import { KGToneBuffersPool } from './core/audio-interface/KGToneBuffersPool'; import { KGOfflineRenderer } from './core/audio-interface/KGOfflineRenderer'; import type { RenderingEvent } from './core/audio-interface/KGOfflineRenderer'; import { KGCore } from './core/KGCore'; import { ConfigManager } from './core/config/ConfigManager'; import { validateFunctionalChordsJSON } from './util/scaleUtil'; import { showAlert } from './components/common/DialogProvider'; import { KGProjectStorage } from './core/io/KGProjectStorage'; import { RESERVED_PROJECT_NAME } from './util/projectNameUtil'; 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, showKGOnePanel } = 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 () => { // Wipe the reserved "Untitled Project" OPFS folder on every startup so it stays ephemeral try { const storage = KGProjectStorage.getInstance(); if (await storage.exists(RESERVED_PROJECT_NAME)) { await storage.delete(RESERVED_PROJECT_NAME); } } catch (error) { console.warn('Could not clear Untitled Project folder on startup:', error); } // Load the current project from KGCore loadProject(null); // Initialize ConfigManager first to load config.json and user settings await ConfigManager.instance().initialize(); // Check for kgone-server.json (managed deployment override) try { const kgoneServerResponse = await fetch(`${import.meta.env.BASE_URL}kgone-server.json?ts=${Date.now()}`); const contentType = kgoneServerResponse.headers.get('Content-Type') ?? ''; if (kgoneServerResponse.ok && contentType.includes('application/json')) { const data = await kgoneServerResponse.json(); if (data.base_url) { ConfigManager.instance().setKGOneManagedByServer(data.base_url); console.log('K.G.One: server-managed config loaded from kgone-server.json, base URL:', data.base_url); } if (data.soundfont) { ConfigManager.instance().setSoundfontManagedByServer(data.soundfont); console.log('Soundfont: server-managed config loaded from kgone-server.json, base URL:', data.soundfont); } } } catch { // File not present — user configures manually } // 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 */} {/* Migration Loading Overlay */} {/* Bounce/Render Overlay */}
); } 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(async () => { // Only trigger if still loading if (loadingCount > 0) { setOverdue(true); await showAlert( '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 ...'} /> ); }; // Migration overlay — shown during one-time IndexedDB -> OPFS migration const MigrationOverlayContainer: React.FC = () => { const [isMigrating, setIsMigrating] = useState(() => KGCore.instance().getIsMigrating()); useEffectReact(() => { KGCore.instance().setMigrationStateChangeCallback(setIsMigrating); return () => { KGCore.instance().setMigrationStateChangeCallback(() => {}); }; }, []); return ( ); }; // Bounce/render overlay — shown during offline WAV/MP3 rendering const BounceOverlayContainer: React.FC = () => { const [renderMessage, setRenderMessage] = useState(null); useEffectReact(() => { const renderer = KGOfflineRenderer.instance(); const listener = (evt: RenderingEvent) => { setRenderMessage(evt.type === 'start' ? (evt.message ?? 'Rendering...') : null); }; renderer.addRenderingListener(listener); return () => { renderer.removeRenderingListener(listener); }; }, []); return ( ); };