diff --git a/README.md b/README.md index d056edf..56a6dcb 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,7 @@ Feature priorities might change. - [ ] Support WAV audio tracks - [ ] Filters and effects - [ ] MCP Support -- [ ] Add support for OpenAI's open source models (`gpt-oss-20b` and `gpt-oss-120b`) +- [X] Add support for OpenAI's open source models (`gpt-oss-20b` and `gpt-oss-120b`) - [ ] Automatically compact conversations when the context window runs low on space ## Help Needed diff --git a/src/App.css b/src/App.css index 5bbb13e..c0d216c 100644 --- a/src/App.css +++ b/src/App.css @@ -325,24 +325,27 @@ body { cursor: crosshair; } -.track-control { - padding: 5px; - background-color: #2d2d2d; - border-top: 1px solid #3a3a3a; -} - -.track-control button { +.add-track-btn { background: transparent; border: none; + outline: none; color: #999; cursor: pointer; - padding: 5px; - width: 100%; - text-align: left; font-size: 12px; + padding: 0; + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; } -.track-control button:hover { +.add-track-btn:focus, +.add-track-btn:focus-visible { + outline: none; +} + +.add-track-btn:hover { color: #e0e0e0; } diff --git a/src/App.tsx b/src/App.tsx index feb3e0a..f219f79 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,6 @@ import { useProjectStore } from './stores/projectStore'; import { useGlobalKeyboardHandler } from './hooks/useGlobalKeyboardHandler'; import Toolbar from './components/Toolbar'; import StatusBar from './components/StatusBar'; -import TrackControl from './components/TrackControl'; import MainContent from './components/MainContent'; import InstrumentSelection from './components/InstrumentSelection'; import ChatBox from './components/ChatBox'; @@ -135,8 +134,6 @@ function App() { - {/* Track Control */} - {/* Status Bar */} diff --git a/src/components/MainContent.tsx b/src/components/MainContent.tsx index 76e742c..d8fb397 100644 --- a/src/components/MainContent.tsx +++ b/src/components/MainContent.tsx @@ -18,7 +18,7 @@ interface MainContentProps { } const MainContent: React.FC = ({ - onTrackClick = () => {} // Default to empty function if not provided + onTrackClick = () => { } // Default to empty function if not provided }) => { const { tracks, @@ -33,18 +33,19 @@ const MainContent: React.FC = ({ showPianoRoll, activeRegionId, setShowPianoRoll, - setActiveRegionId + setActiveRegionId, + addTrack } = useProjectStore(); - + // State to store regions const [regions, setRegions] = useState([]); - + // Drag state for track grid highlighting const [draggedTrackIndex, setDraggedTrackIndex] = useState(null); const [dragOverTrackIndex, setDragOverTrackIndex] = useState(null); - + // Piano roll state is now managed by the store - removed local state - + // Region selection state const [selectedRegionId, setSelectedRegionId] = useState(null); @@ -64,16 +65,16 @@ const MainContent: React.FC = ({ // Register the delete function with the global manager useEffect(() => { regionDeleteManager.registerDeleteCallback(deleteSelectedRegions); - + // Cleanup on unmount return () => { regionDeleteManager.unregisterDeleteCallback(); }; }, [deleteSelectedRegions]); - + // Refs to track pending updates for verification const pendingUpdates = useRef>(new Map()); - + // Refs for bar numbers and loop range drag functionality const barNumbersRef = useRef(null); const isLoopDraggingRef = useRef(false); @@ -87,26 +88,26 @@ const MainContent: React.FC = ({ if (pendingUpdates.current.size > 0) { // Create a copy of the pending updates const updates = new Map(pendingUpdates.current); - + // Clear pending updates pendingUpdates.current.clear(); - + // Check each update updates.forEach((update, key) => { const { trackId, regionId, startBeat, length } = update; - + // Find the track const track = tracks.find(t => t.getId().toString() === trackId); if (track) { // Find the region const regions = track.getRegions(); const region = regions.find(r => r.getId() === regionId); - + if (region && DEBUG_MODE.MAIN_CONTENT) { console.log(`Verification - Region ${regionId} in track ${trackId}:`); console.log(` Expected: startBeat=${startBeat}, length=${length}`); console.log(` Actual: startBeat=${region.getStartFromBeat()}, length=${region.getLength()}, trackId=${region.getTrackId()}, trackIndex=${region.getTrackIndex()}`); - + // Check if the update was successful const success = region.getStartFromBeat() === startBeat && region.getLength() === length && region.getTrackId() === trackId; console.log(` Update successful: ${success}`); @@ -120,12 +121,12 @@ const MainContent: React.FC = ({ useEffect(() => { // Create a new array of RegionUI objects based on the current tracks const updatedRegions: RegionUI[] = []; - + // Iterate through all tracks tracks.forEach(track => { const trackId = track.getId().toString(); const trackIndex = track.getTrackIndex(); - + // Iterate through all regions in the track track.getRegions().forEach(region => { if (region instanceof KGMidiRegion) { @@ -133,7 +134,7 @@ const MainContent: React.FC = ({ const beatsPerBar = timeSignature.numerator; const barNumber = Math.floor(region.getStartFromBeat() / beatsPerBar) + 1; const length = region.getLength() / beatsPerBar; - + // Create a RegionUI object updatedRegions.push({ id: region.getId(), @@ -146,7 +147,7 @@ const MainContent: React.FC = ({ } }); }); - + // Update the regions state setRegions(updatedRegions); }, [tracks, timeSignature]); @@ -161,7 +162,7 @@ const MainContent: React.FC = ({ const handleTracksReordered = (fromIndex: number, toIndex: number) => { // Reorder tracks in the store - this will also update trackIndex in each KGTrack reorderTracks(fromIndex, toIndex); - + // Update regions to match the new track order setRegions(prevRegions => { return prevRegions.map(region => { @@ -171,17 +172,17 @@ const MainContent: React.FC = ({ } // If the region belongs to a track that was shifted due to the drag operation else if ( - (fromIndex < toIndex && - region.trackIndex > fromIndex && - region.trackIndex <= toIndex) + (fromIndex < toIndex && + region.trackIndex > fromIndex && + region.trackIndex <= toIndex) ) { // Shift up by 1 return { ...region, trackIndex: region.trackIndex - 1 }; } else if ( - (fromIndex > toIndex && - region.trackIndex < fromIndex && - region.trackIndex >= toIndex) + (fromIndex > toIndex && + region.trackIndex < fromIndex && + region.trackIndex >= toIndex) ) { // Shift down by 1 return { ...region, trackIndex: region.trackIndex + 1 }; @@ -190,7 +191,7 @@ const MainContent: React.FC = ({ return region; }); }); - + // Update the grid drag state to match setDraggedTrackIndex(null); setDragOverTrackIndex(null); @@ -200,53 +201,53 @@ const MainContent: React.FC = ({ const handleRegionCreated = (trackIndex: number, regionUI: RegionUI, midiRegion: KGMidiRegion) => { // Note: The region model is already created by the CreateRegionCommand // We just need to update the UI state and handle selection - + // Get the track for store updates const track = tracks[trackIndex]; - + // Update the track in the store to reflect the command changes updateTrack(track); - + // Select the track that contains the new region setSelectedTrack(track.getId().toString()); - + // Add the new region to the UI state and select it immediately setRegions(prevRegions => { const updatedRegions = [...prevRegions, regionUI]; - + // Select the region using the updated regions array selectRegion(regionUI.id, updatedRegions); - + // Manually trigger selection sync to ensure UI updates immediately const { syncSelectionFromCore } = useProjectStore.getState(); syncSelectionFromCore(); - + // If piano roll is visible, set this region as the active region if (showPianoRoll) { setActiveRegionId(regionUI.id); - + if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Newly created region ${regionUI.id} set as active region in piano roll`); } } - + return updatedRegions; }); }; - + // Handle region updates (resize, move, etc.) const handleRegionUpdated = ( - regionId: string, - updates: Partial, + regionId: string, + updates: Partial, expectedModelUpdates?: { startBeat: number, length: number } ) => { if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Updating region ${regionId} with:`, updates); } - + // Select the region when it's being updated (resize or move) selectRegion(regionId); - + // Find the region to determine which track to select const updatedRegion = regions.find(r => r.id === regionId); if (updatedRegion) { @@ -257,7 +258,7 @@ const MainContent: React.FC = ({ setSelectedTrack(track.getId().toString()); } } - + // Update the region in the UI state setRegions(prevRegions => { return prevRegions.map(region => { @@ -267,31 +268,31 @@ const MainContent: React.FC = ({ return region; }); }); - + // Find the region that was updated const region = regions.find(r => r.id === regionId); if (!region) return; - + // Check if the track ID is being updated (region moved to different track) if (updates.trackId && updates.trackId !== region.trackId) { if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Region ${regionId} moved from track ${region.trackId} to track ${updates.trackId}`); } - + // Get the original track const originalTrack = tracks.find(t => t.getId().toString() === region.trackId); - + // Get the target track const targetTrack = tracks.find(t => t.getId().toString() === updates.trackId); - + if (originalTrack && targetTrack) { // Select the target track that now contains the region setSelectedTrack(targetTrack.getId().toString()); - + // Update both tracks in the store updateTrack(originalTrack); updateTrack(targetTrack); - + // Add to pending updates for verification if (expectedModelUpdates) { const key = `${updates.trackId}-${regionId}-${Date.now()}`; @@ -310,14 +311,14 @@ const MainContent: React.FC = ({ // Log the track's regions before updating the store const trackRegions = track.getRegions(); const midiRegion = trackRegions.find(r => r.getId() === regionId) as KGMidiRegion | undefined; - + if (midiRegion) { // If we have expected model updates, use those if (expectedModelUpdates) { if (DEBUG_MODE.MAIN_CONTENT) { console.log(`MainContent - Expected model updates: startBeat=${expectedModelUpdates.startBeat}, length=${expectedModelUpdates.length}`); } - + // Add to pending updates for verification const key = `${track.getId()}-${regionId}-${Date.now()}`; pendingUpdates.current.set(key, { @@ -330,11 +331,11 @@ const MainContent: React.FC = ({ // Otherwise use the current values (for backward compatibility) const startBeat = midiRegion.getStartFromBeat(); const length = midiRegion.getLength(); - + if (DEBUG_MODE.MAIN_CONTENT) { console.log(`MainContent - Region before store update: startBeat=${startBeat}, length=${length}`); } - + // Add to pending updates for verification const key = `${track.getId()}-${regionId}-${Date.now()}`; pendingUpdates.current.set(key, { @@ -345,16 +346,16 @@ const MainContent: React.FC = ({ }); } } - + // Update the track in the store to persist changes updateTrack(track); } } - + // If piano roll is visible, set this region as the active region if (showPianoRoll) { setActiveRegionId(regionId); - + if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Updated region ${regionId} set as active region in piano roll`); } @@ -365,7 +366,7 @@ const MainContent: React.FC = ({ const selectRegion = (regionId: string, regionsToSearch?: RegionUI[]) => { // Clear any existing selections using store method clearAllSelections(); - + // Find the region in the UI state (use provided regions or current state) const regionsToUse = regionsToSearch || regions; const region = regionsToUse.find(r => r.id === regionId); @@ -375,7 +376,7 @@ const MainContent: React.FC = ({ } return; } - + // Find the track that contains this region const track = tracks.find(t => t.getId().toString() === region.trackId); if (!track) { @@ -384,28 +385,28 @@ const MainContent: React.FC = ({ } return; } - + // Find the region in the track's model const trackRegions = track.getRegions(); const midiRegion = trackRegions.find(r => r.getId() === regionId) as KGMidiRegion | undefined; - + if (!midiRegion) { if (DEBUG_MODE.MAIN_CONTENT) { console.log(`MIDI region not found in track model: ${regionId}`); } return; } - + // Add the region to KGCore's selection const core = KGCore.instance(); core.addSelectedItem(midiRegion); - + // Update the region's internal selection state midiRegion.select(); - + // Set the selected region (this might be redundant now, but keeping for compatibility) setSelectedRegionId(regionId); - + if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Selected region: ${regionId} (added to KGCore selection)`); } @@ -416,10 +417,10 @@ const MainContent: React.FC = ({ if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Region clicked in MainContent (selection only): ${regionId}`); } - + // Select the region selectRegion(regionId); - + // Also select the containing track const region = regions.find(r => r.id === regionId); if (!region) return; @@ -433,10 +434,10 @@ const MainContent: React.FC = ({ if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Open piano roll via pencil for region: ${regionId}`); } - + // Reuse selection logic handleRegionClick(regionId); - + // Activate and show piano roll setActiveRegionId(regionId); setShowPianoRoll(true); @@ -458,8 +459,8 @@ const MainContent: React.FC = ({ // Skip if user is typing in an input field (including ChatBox) const target = event.target as HTMLElement; if (target && ( - target.tagName === 'INPUT' || - target.tagName === 'TEXTAREA' || + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || target.contentEditable === 'true' || target.hasAttribute('data-chatbox-input') || target.closest('.chatbox-input') @@ -472,7 +473,7 @@ const MainContent: React.FC = ({ // Only handle if we're not in the piano roll (piano roll has its own delete handler) const isInPianoRoll = document.querySelector('.piano-roll')?.contains(event.target as Node); const isPianoRollOpen = showPianoRoll; - + if (!isInPianoRoll && !isPianoRollOpen) { const deleted = deleteSelectedRegions(); if (deleted) { @@ -482,10 +483,10 @@ const MainContent: React.FC = ({ } } }; - + // Add event listener window.addEventListener('keydown', handleKeyDown); - + // Remove event listener on cleanup return () => { window.removeEventListener('keydown', handleKeyDown); @@ -495,25 +496,25 @@ const MainContent: React.FC = ({ // Utility function to calculate playhead position from mouse coordinates (bar-level snapping) const calculatePlayheadFromMouse = useCallback((clientX: number): number | null => { if (!barNumbersRef.current) return null; - + const rect = barNumbersRef.current.getBoundingClientRect(); const relativeX = clientX - rect.left; - + // Calculate the width of each bar const barWidth = parseInt( getComputedStyle(document.documentElement).getPropertyValue('--track-grid-bar-width') ) || 40; - + // Find the closest bar start (using Math.round for nearest bar) const barIndex = Math.round(relativeX / barWidth); - + // Ensure we don't go below 0 const clampedBarIndex = Math.max(0, barIndex); - + // Calculate destination beat position (start of the bar) const beatsPerBar = timeSignature.numerator; const destinationBeatPosition = clampedBarIndex * beatsPerBar; - + return destinationBeatPosition; }, [timeSignature]); @@ -688,7 +689,9 @@ const MainContent: React.FC = ({
{/* Top-left spacer */} -
+
+ +
{/* Bar numbers at the top */}
= ({
))}
- +
{/* Fixed left panel with track info */} = ({ onTrackNameEdit={handleTrackNameEdit} onTracksReordered={handleTracksReordered} /> - + {/* Scrollable grid area */} = ({ />
- + {/* Piano Roll - render using portal */} {showPianoRoll && createPortal( - , diff --git a/src/components/TrackControl.tsx b/src/components/TrackControl.tsx deleted file mode 100644 index 3f7136c..0000000 --- a/src/components/TrackControl.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { useProjectStore } from '../stores/projectStore'; - -const TrackControl: React.FC = () => { - const { addTrack } = useProjectStore(); - - const handleAddTrack = () => { - addTrack(); - }; - - return ( -
- -
- ); -}; - -export default TrackControl; \ No newline at end of file