feat: implemented chord guide customization feature

This commit is contained in:
Xiaohan-Tian
2025-12-17 23:07:21 -08:00
parent 610b81e4cc
commit 5cceec0c01
10 changed files with 696 additions and 8 deletions
+4 -1
View File
@@ -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<SettingsPanelProps> = ({ onClose }) => {
return <BehaviorSettings />;
case 'templates':
return <TemplatesSettings />;
case 'chord_guide':
return <ChordGuideSettings />;
default:
return <GeneralSettings />;
}
+2 -1
View File
@@ -16,7 +16,8 @@ const SettingsSidebar: React.FC<SettingsSidebarProps> = ({
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 (
@@ -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<string>('');
const [validationErrors, setValidationErrors] = useState<string[]>([]);
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 (
<div className="settings-section">
<div className="settings-section-header">
<h3>Chord Guide</h3>
</div>
<div className="settings-section-content">
<div className="settings-group">
<h4>Chord Definition</h4>
<div className="settings-help-links">
<button
className="settings-help"
onClick={handleLoadDefaultTemplate}
>
Load Default Template
</button>
<button
className="settings-help"
onClick={handleClear}
>
Clear
</button>
</div>
<div className="settings-item">
<textarea
className="settings-textarea"
placeholder="Please input your chord definitions"
rows={8}
value={chordDefinition}
onChange={(e) => handleChordDefinitionChange(e.target.value)}
/>
{validationErrors.length > 0 && (
<div className="settings-validation-errors">
{validationErrors.map((error, index) => (
<div key={index} className="settings-validation-error">
{error}
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default ChordGuideSettings;