Mina Emadi
implemented some UI changes including adding a knob for pan and reverb, changing the UI of the key and bpm modification and changing the appearance of the detected key and bpm and removed some dev
e631a15 | import React, { useState, useCallback, useEffect } from 'react' | |
| /* Upload slots — hidden | |
| const STEM_TYPES = [ | |
| { name: 'guitar', label: 'Guitar', icon: '🎸' }, | |
| { name: 'drums', label: 'Drums', icon: '🥁' }, | |
| { name: 'bass', label: 'Bass', icon: '🎸' }, | |
| { name: 'synth', label: 'Synth', icon: '🎹' }, | |
| { name: 'click_record', label: 'Click Record', icon: '⏱️' } | |
| ] | |
| */ | |
| function FileUpload({ onUpload, onLoadPreset, loading, error, onClearError, cacheStatus = {} }) { | |
| const [presets, setPresets] = useState([]) | |
| const [selectedPreset, setSelectedPreset] = useState('') | |
| useEffect(() => { | |
| fetch('/api/presets') | |
| .then(r => r.json()) | |
| .then(data => setPresets(data.presets || [])) | |
| .catch(() => {}) | |
| }, []) | |
| /* Upload slots — hidden | |
| const [stems, setStems] = useState({ | |
| guitar: null, | |
| drums: null, | |
| bass: null, | |
| synth: null, | |
| click_record: null | |
| }) | |
| const [midiFiles, setMidiFiles] = useState([]) | |
| const [midiDragActive, setMidiDragActive] = useState(false) | |
| const handleStemSelect = useCallback((stemType, file) => { | |
| setStems(prev => ({ ...prev, [stemType]: file })) | |
| }, []) | |
| const handleStemRemove = useCallback((stemType) => { | |
| setStems(prev => ({ ...prev, [stemType]: null })) | |
| }, []) | |
| const handleMidiSelect = useCallback((e) => { | |
| const selectedFiles = Array.from(e.target.files).filter(f => | |
| f.name.toLowerCase().endsWith('.mid') || f.name.toLowerCase().endsWith('.midi') | |
| ) | |
| setMidiFiles(prev => [...prev, ...selectedFiles]) | |
| }, []) | |
| const handleMidiRemove = useCallback((index) => { | |
| setMidiFiles(prev => prev.filter((_, i) => i !== index)) | |
| }, []) | |
| const handleMidiDrag = useCallback((e) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| if (e.type === 'dragenter' || e.type === 'dragover') { | |
| setMidiDragActive(true) | |
| } else if (e.type === 'dragleave') { | |
| setMidiDragActive(false) | |
| } | |
| }, []) | |
| const handleMidiDrop = useCallback((e) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| setMidiDragActive(false) | |
| const droppedFiles = Array.from(e.dataTransfer.files).filter(f => | |
| f.name.toLowerCase().endsWith('.mid') || f.name.toLowerCase().endsWith('.midi') | |
| ) | |
| setMidiFiles(prev => [...prev, ...droppedFiles]) | |
| }, []) | |
| const handleSubmit = useCallback(async () => { | |
| const formData = new FormData() | |
| Object.entries(stems).forEach(([name, file]) => { | |
| if (file) formData.append(name, file) | |
| }) | |
| midiFiles.forEach(file => { | |
| formData.append('midi', file) | |
| }) | |
| await onUpload(formData) | |
| }, [stems, midiFiles, onUpload]) | |
| const stemCount = Object.values(stems).filter(f => f !== null).length | |
| const hasFiles = stemCount > 0 | |
| */ | |
| return ( | |
| <div className="glass rounded-2xl p-6 animate-fade-in max-w-4xl mx-auto"> | |
| <h2 className="text-xl font-semibold mb-6 text-center">Load a Demo Track</h2> | |
| {/* Demo presets dropdown */} | |
| {presets.length > 0 && ( | |
| <div className="p-4 bg-gray-800/50 rounded-xl border border-gray-600"> | |
| <div className="flex gap-3"> | |
| <select | |
| value={selectedPreset} | |
| onChange={(e) => setSelectedPreset(e.target.value)} | |
| disabled={loading} | |
| className="flex-1 bg-gray-800 border border-gray-600 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-primary-500" | |
| > | |
| <option value="">Select a demo...</option> | |
| {presets.map(p => { | |
| const status = cacheStatus[p] | |
| const label = status === 'cached' | |
| ? `${p} ✓` | |
| : status === 'loading' | |
| ? `${p} …` | |
| : p | |
| return <option key={p} value={p}>{label}</option> | |
| })} | |
| </select> | |
| <button | |
| onClick={() => selectedPreset && onLoadPreset(selectedPreset)} | |
| disabled={loading || !selectedPreset} | |
| className="px-4 py-2 rounded-lg text-sm font-medium transition-all disabled:bg-gray-700 disabled:text-gray-500 disabled:cursor-not-allowed bg-gradient-to-r from-primary-600 to-accent-600 hover:from-primary-500 hover:to-accent-500" | |
| > | |
| {loading ? 'Loading...' : 'Load'} | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Upload slots — hidden | |
| <div className="grid grid-cols-2 gap-4 mb-6"> | |
| {STEM_TYPES.map(({ name, label, icon }) => ( | |
| <StemUploadCard | |
| key={name} | |
| name={name} | |
| label={label} | |
| icon={icon} | |
| file={stems[name]} | |
| onSelect={(file) => handleStemSelect(name, file)} | |
| onRemove={() => handleStemRemove(name)} | |
| disabled={loading} | |
| /> | |
| ))} | |
| </div> | |
| <div | |
| onDragEnter={handleMidiDrag} | |
| onDragLeave={handleMidiDrag} | |
| onDragOver={handleMidiDrag} | |
| onDrop={handleMidiDrop} | |
| className={` | |
| mt-6 p-4 border-2 border-dashed rounded-xl transition-all | |
| ${midiDragActive | |
| ? 'border-blue-500 bg-blue-500/10' | |
| : 'border-gray-600' | |
| } | |
| `} | |
| > | |
| <div className="flex items-center justify-between mb-3"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-2xl">🎹</span> | |
| <span className="font-medium text-gray-300">MIDI Files (Optional)</span> | |
| </div> | |
| <label | |
| htmlFor="midi-input" | |
| className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg cursor-pointer transition-colors text-sm" | |
| > | |
| + Add MIDI | |
| </label> | |
| <input | |
| type="file" | |
| multiple | |
| accept=".mid,.midi" | |
| onChange={handleMidiSelect} | |
| className="hidden" | |
| id="midi-input" | |
| disabled={loading} | |
| /> | |
| </div> | |
| {midiFiles.length > 0 && ( | |
| <div className="space-y-2"> | |
| {midiFiles.map((file, index) => ( | |
| <div | |
| key={index} | |
| className="flex items-center justify-between bg-gray-800/50 rounded-lg px-3 py-2" | |
| > | |
| <div className="flex items-center gap-2"> | |
| <span className="text-sm truncate">{file.name}</span> | |
| <span className="text-xs text-gray-500"> | |
| {(file.size / 1024).toFixed(1)} KB | |
| </span> | |
| </div> | |
| <button | |
| onClick={() => handleMidiRemove(index)} | |
| className="text-gray-400 hover:text-red-400 transition-colors" | |
| disabled={loading} | |
| > | |
| ✕ | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| <button | |
| onClick={handleSubmit} | |
| disabled={loading || !hasFiles} | |
| className={` | |
| w-full mt-6 py-3 rounded-xl font-medium transition-all | |
| ${loading || !hasFiles | |
| ? 'bg-gray-600 cursor-not-allowed' | |
| : 'bg-gradient-to-r from-primary-600 to-accent-600 hover:from-primary-500 hover:to-accent-500' | |
| } | |
| `} | |
| > | |
| {loading ? ( | |
| <span className="flex items-center justify-center gap-2"> | |
| <span className="animate-spin">⏳</span> | |
| Analyzing... | |
| </span> | |
| ) : ( | |
| `Upload ${stemCount} Stem${stemCount !== 1 ? 's' : ''}${midiFiles.length > 0 ? ` + ${midiFiles.length} MIDI` : ''}` | |
| )} | |
| </button> | |
| */} | |
| {/* Error display */} | |
| {error && ( | |
| <div className="mt-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300 text-sm flex items-center justify-between"> | |
| <span>{error}</span> | |
| <button | |
| onClick={onClearError} | |
| className="text-red-400 hover:text-red-200" | |
| > | |
| ✕ | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |
| /* StemUploadCard — hidden | |
| function StemUploadCard({ name, label, icon, file, onSelect, onRemove, disabled }) { | |
| const [dragActive, setDragActive] = React.useState(false) | |
| const handleFileChange = (e) => { | |
| const selectedFile = e.target.files[0] | |
| if (selectedFile && selectedFile.name.toLowerCase().endsWith('.wav')) { | |
| onSelect(selectedFile) | |
| } | |
| } | |
| const handleDrag = (e) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| if (e.type === 'dragenter' || e.type === 'dragover') { | |
| setDragActive(true) | |
| } else if (e.type === 'dragleave') { | |
| setDragActive(false) | |
| } | |
| } | |
| const handleDrop = (e) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| setDragActive(false) | |
| const droppedFile = e.dataTransfer.files[0] | |
| if (droppedFile && droppedFile.name.toLowerCase().endsWith('.wav')) { | |
| onSelect(droppedFile) | |
| } | |
| } | |
| return ( | |
| <div | |
| onDragEnter={handleDrag} | |
| onDragLeave={handleDrag} | |
| onDragOver={handleDrag} | |
| onDrop={handleDrop} | |
| className={` | |
| p-4 rounded-xl border-2 transition-all | |
| ${file | |
| ? 'border-green-500 bg-green-500/10' | |
| : dragActive | |
| ? 'border-blue-500 bg-blue-500/10' | |
| : 'border-gray-600 hover:border-primary-500 bg-gray-800/30' | |
| } | |
| `} | |
| > | |
| <div className="flex items-center justify-between mb-3"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-2xl">{icon}</span> | |
| <span className="font-medium text-gray-300">{label}</span> | |
| </div> | |
| {file && ( | |
| <button | |
| onClick={onRemove} | |
| className="text-gray-400 hover:text-red-400 transition-colors" | |
| disabled={disabled} | |
| > | |
| ✕ | |
| </button> | |
| )} | |
| </div> | |
| {file ? ( | |
| <div className="text-sm"> | |
| <p className="text-gray-300 truncate">{file.name}</p> | |
| <p className="text-gray-500 text-xs mt-1"> | |
| {(file.size / 1024 / 1024).toFixed(1)} MB | |
| </p> | |
| </div> | |
| ) : ( | |
| <label | |
| htmlFor={`${name}-input`} | |
| className="block w-full py-2 text-center bg-gray-700 hover:bg-gray-600 rounded-lg cursor-pointer transition-colors text-sm" | |
| > | |
| Select File | |
| </label> | |
| )} | |
| <input | |
| type="file" | |
| accept=".wav" | |
| onChange={handleFileChange} | |
| className="hidden" | |
| id={`${name}-input`} | |
| disabled={disabled} | |
| /> | |
| </div> | |
| ) | |
| } | |
| */ | |
| export default FileUpload | |