diff --git a/public/config.json b/public/config.json index 91ea264..2c60bbc 100644 --- a/public/config.json +++ b/public/config.json @@ -66,5 +66,8 @@ }, "templates": { "custom_instructions": "" + }, + "chord_guide": { + "chord_definition": "" } } \ No newline at end of file diff --git a/src/App.css b/src/App.css index 271bcfd..6b00ca2 100644 --- a/src/App.css +++ b/src/App.css @@ -1751,6 +1751,50 @@ textarea { font-size: 12px; } +/* Settings Help Links */ +.settings-help-links { + display: flex; + gap: 16px; + margin-bottom: 8px; +} + +button.settings-help { + color: #5a9fd4; + text-decoration: underline; + cursor: pointer; + font-size: 14px; + background: none; + border: none; + padding: 0; + font-family: inherit; +} + +button.settings-help:hover { + color: #7bbfef; +} + +/* Settings Validation Errors */ +.settings-validation-errors { + margin-top: 8px; + padding: 8px; + background-color: rgba(211, 90, 90, 0.1); + border: 1px solid #d35a5a; + border-radius: 4px; + max-height: 200px; + overflow-y: auto; +} + +.settings-validation-error { + color: #ff6b6b; + font-size: 12px; + line-height: 1.4; + margin-bottom: 4px; +} + +.settings-validation-error:last-child { + margin-bottom: 0; +} + /* Templates List */ .templates-list { display: flex; diff --git a/src/App.tsx b/src/App.tsx index 9a037a0..feb3e0a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,8 @@ 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 @@ -40,6 +42,9 @@ function App() { // 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(); @@ -53,13 +58,31 @@ function App() { functionalChordsResponse.json() ]); - // Store functional chords data (includes name, steps, T/S/D groups, and chord notes for each mode) - KGCore.FUNCTIONAL_CHORDS_DATA = functionalChordsData; - console.log(`Loaded functional chords for ${Object.keys(functionalChordsData).length} modes`); + // 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 - KGCore.FUNCTIONAL_CHORDS_DATA = { + const fallbackData = { ionian: { name: 'Ionian', steps: [2, 2, 1, 2, 2, 2, 1], @@ -69,6 +92,8 @@ function App() { chords: {} } }; + KGCore.ORIGINAL_FUNCTIONAL_CHORDS_DATA = fallbackData; + KGCore.FUNCTIONAL_CHORDS_DATA = fallbackData; } // Log maxBars after initialization completes diff --git a/src/components/settings/SettingsPanel.tsx b/src/components/settings/SettingsPanel.tsx index ead5619..599a1ba 100644 --- a/src/components/settings/SettingsPanel.tsx +++ b/src/components/settings/SettingsPanel.tsx @@ -3,8 +3,9 @@ import SettingsSidebar from './SettingsSidebar'; import GeneralSettings from './sections/GeneralSettings'; import BehaviorSettings from './sections/BehaviorSettings'; import TemplatesSettings from './sections/TemplatesSettings'; +import ChordGuideSettings from './sections/ChordGuideSettings'; -export type SettingsSection = 'general' | 'behavior' | 'templates'; +export type SettingsSection = 'general' | 'behavior' | 'templates' | 'chord_guide'; interface SettingsPanelProps { onClose: () => void; @@ -21,6 +22,8 @@ const SettingsPanel: React.FC = ({ onClose }) => { return ; case 'templates': return ; + case 'chord_guide': + return ; default: return ; } diff --git a/src/components/settings/SettingsSidebar.tsx b/src/components/settings/SettingsSidebar.tsx index 3cfc896..d85f1c1 100644 --- a/src/components/settings/SettingsSidebar.tsx +++ b/src/components/settings/SettingsSidebar.tsx @@ -16,7 +16,8 @@ const SettingsSidebar: React.FC = ({ const sections = [ { id: 'general' as SettingsSection, label: 'General' }, { id: 'behavior' as SettingsSection, label: 'Behavior' }, - { id: 'templates' as SettingsSection, label: 'Templates' } + { id: 'templates' as SettingsSection, label: 'Templates' }, + { id: 'chord_guide' as SettingsSection, label: 'Chord Guide' } ]; return ( diff --git a/src/components/settings/sections/ChordGuideSettings.tsx b/src/components/settings/sections/ChordGuideSettings.tsx new file mode 100644 index 0000000..cb3eefb --- /dev/null +++ b/src/components/settings/sections/ChordGuideSettings.tsx @@ -0,0 +1,155 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { ConfigManager } from '../../../core/config/ConfigManager'; +import { validateFunctionalChordsJSON } from '../../../util/scaleUtil'; +import { KGCore } from '../../../core/KGCore'; + +const ChordGuideSettings: React.FC = () => { + const [chordDefinition, setChordDefinition] = useState(''); + const [validationErrors, setValidationErrors] = useState([]); + + const configManager = ConfigManager.instance(); + + // Load configuration values on component mount + useEffect(() => { + const loadConfig = async () => { + if (!configManager.getIsInitialized()) { + await configManager.initialize(); + } + + setChordDefinition((configManager.get('chord_guide.chord_definition') as string) || ''); + }; + + loadConfig(); + }, [configManager]); + + // Debounced save function for textarea + const debouncedSave = useCallback((value: string) => { + const timeoutId = setTimeout(async () => { + try { + await configManager.set('chord_guide.chord_definition', value); + console.log('Chord definition saved'); + } catch (error) { + console.error('Failed to save chord definition:', error); + } + }, 1000); // 1 second debounce for longer text + + return () => clearTimeout(timeoutId); + }, [configManager]); + + // Save configuration when value changes + const handleChordDefinitionChange = (value: string) => { + setChordDefinition(value); + + // Validate the JSON + if (value.trim()) { + const validationResult = validateFunctionalChordsJSON(value); + setValidationErrors(validationResult.valid ? [] : validationResult.errors); + + // Update FUNCTIONAL_CHORDS_DATA if valid and non-empty + if (validationResult.valid) { + try { + KGCore.FUNCTIONAL_CHORDS_DATA = JSON.parse(value); + console.log('Applied custom chord definition'); + } catch (error) { + console.error('Failed to parse chord definition:', error); + KGCore.FUNCTIONAL_CHORDS_DATA = KGCore.ORIGINAL_FUNCTIONAL_CHORDS_DATA; + } + } else { + // Revert to original if invalid + KGCore.FUNCTIONAL_CHORDS_DATA = KGCore.ORIGINAL_FUNCTIONAL_CHORDS_DATA; + console.log('Invalid chord definition, reverted to original'); + } + } else { + setValidationErrors([]); + // Revert to original if empty + KGCore.FUNCTIONAL_CHORDS_DATA = KGCore.ORIGINAL_FUNCTIONAL_CHORDS_DATA; + console.log('Chord definition cleared, reverted to original'); + } + + debouncedSave(value); + }; + + // Load default template from functional_chords.json (preserving original formatting) + const handleLoadDefaultTemplate = async () => { + try { + const response = await fetch(`${import.meta.env.BASE_URL}resources/modes/functional_chords.json`); + if (!response.ok) { + throw new Error(`Failed to fetch functional_chords.json: ${response.status}`); + } + // Get the raw text to preserve original formatting + const rawText = await response.text(); + setChordDefinition(rawText); + setValidationErrors([]); // Clear errors when loading valid template + await configManager.set('chord_guide.chord_definition', rawText); + + // Revert to original if empty + KGCore.FUNCTIONAL_CHORDS_DATA = KGCore.ORIGINAL_FUNCTIONAL_CHORDS_DATA; + + console.log('Loaded default chord template'); + } catch (error) { + console.error('Failed to load default template:', error); + alert('Failed to load default template. Please check the console for details.'); + } + }; + + // Clear the chord definition + const handleClear = async () => { + setChordDefinition(''); + setValidationErrors([]); // Clear errors when clearing + await configManager.set('chord_guide.chord_definition', ''); + + // Revert to original if empty + KGCore.FUNCTIONAL_CHORDS_DATA = KGCore.ORIGINAL_FUNCTIONAL_CHORDS_DATA; + + console.log('Chord definition cleared'); + }; + + return ( +
+
+

Chord Guide

+
+ +
+
+

Chord Definition

+
+ + +
+ +
+