Mina Emadi
applied the comments regarding chord progression, scale suggestions and UI/UX changes
25d51c9 | import React, { useState, useEffect, useMemo, useCallback } from 'react' | |
| const ALL_KEYS_WITH_MODES = [ | |
| 'C major', 'C minor', 'C# major', 'C# minor', | |
| 'D major', 'D minor', 'D# major', 'D# minor', | |
| 'E major', 'E minor', 'F major', 'F minor', | |
| 'F# major', 'F# minor', 'G major', 'G minor', | |
| 'G# major', 'G# minor', 'A major', 'A minor', | |
| 'A# major', 'A# minor', 'B major', 'B minor' | |
| ] | |
| const KEY_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] | |
| function AnalysisDisplay({ | |
| detection, | |
| loading, | |
| onReady, | |
| appliedSemitones = 0, | |
| onProcess, | |
| isProcessing, | |
| hasRegion | |
| }) { | |
| const [targetKeyWithMode, setTargetKeyWithMode] = useState('C major') | |
| const [targetBpm, setTargetBpm] = useState(120) | |
| useEffect(() => { | |
| if (detection && onReady) { | |
| onReady() | |
| } | |
| }, [detection, onReady]) | |
| useEffect(() => { | |
| if (detection) { | |
| setTargetKeyWithMode(`${detection.key} ${detection.mode}`) | |
| setTargetBpm(detection.bpm) | |
| } | |
| }, [detection]) | |
| const targetKey = targetKeyWithMode.split(' ')[0] | |
| const semitones = useMemo(() => { | |
| if (!detection || !targetKey) return 0 | |
| const fromIdx = KEY_NAMES.indexOf(detection.key) | |
| const toIdx = KEY_NAMES.indexOf(targetKey) | |
| let diff = toIdx - fromIdx | |
| if (diff > 6) diff -= 12 | |
| if (diff < -6) diff += 12 | |
| return diff | |
| }, [detection, targetKey]) | |
| const handleKeyShift = useCallback((shift) => { | |
| const currentIdx = KEY_NAMES.indexOf(targetKey) | |
| const newIdx = (currentIdx + shift + 12) % 12 | |
| const mode = targetKeyWithMode.split(' ')[1] | |
| setTargetKeyWithMode(`${KEY_NAMES[newIdx]} ${mode}`) | |
| }, [targetKey, targetKeyWithMode]) | |
| const handleApply = useCallback(() => { | |
| if (!detection || !onProcess) return | |
| const newBpm = Math.abs(targetBpm - detection.bpm) > 0.1 ? targetBpm : null | |
| onProcess(semitones, newBpm) | |
| }, [onProcess, semitones, targetBpm, detection]) | |
| const hasChanges = detection && (semitones !== 0 || Math.abs(targetBpm - detection.bpm) > 0.1) | |
| if (loading) { | |
| return ( | |
| <div className="card rounded-xl p-5 animate-pulse"> | |
| <div className="flex gap-6"> | |
| <div className="h-12 bg-white/5 rounded-lg w-40"></div> | |
| <div className="h-12 bg-white/5 rounded-lg w-40"></div> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| if (!detection) return null | |
| const btnClass = 'w-8 h-8 rounded-lg font-bold text-sm transition-all bg-white/[0.06] hover:bg-white/[0.12] text-gray-300 border border-white/[0.08] flex items-center justify-center flex-shrink-0' | |
| const inputClass = 'bg-surface-elevated border border-white/[0.08] rounded-lg text-white text-sm text-center focus:outline-none focus:border-accent-500 transition-colors' | |
| return ( | |
| <div className="card rounded-xl p-5 animate-fade-in"> | |
| <div className="flex items-center gap-6 flex-wrap"> | |
| {/* BPM Control: - [editable input] + */} | |
| <div className="flex items-center gap-1.5"> | |
| <span className="text-[10px] uppercase tracking-widest text-accent-500 font-medium mr-2">BPM</span> | |
| <button onClick={() => setTargetBpm(v => Math.max(20, v - 1))} className={btnClass}>-</button> | |
| <input | |
| type="number" | |
| value={Math.round(targetBpm)} | |
| onChange={(e) => { | |
| const val = parseFloat(e.target.value) | |
| if (!isNaN(val) && val >= 20 && val <= 400) setTargetBpm(val) | |
| }} | |
| min={20} | |
| max={400} | |
| className={`${inputClass} w-20 px-2 py-1.5 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none`} | |
| /> | |
| <button onClick={() => setTargetBpm(v => Math.min(400, v + 1))} className={btnClass}>+</button> | |
| {Math.abs(targetBpm - detection.bpm) > 0.1 && ( | |
| <button | |
| onClick={() => setTargetBpm(detection.bpm)} | |
| className="text-[10px] text-gray-500 hover:text-gray-300 transition-colors ml-1" | |
| > | |
| Reset | |
| </button> | |
| )} | |
| </div> | |
| {/* Divider */} | |
| <div className="w-px h-10 bg-white/[0.08]"></div> | |
| {/* Key Control: - [dropdown] + */} | |
| <div className="flex items-center gap-1.5"> | |
| <span className="text-[10px] uppercase tracking-widest text-accent-500 font-medium mr-2">Key</span> | |
| <button onClick={() => handleKeyShift(-1)} className={btnClass}>-</button> | |
| <select | |
| value={targetKeyWithMode} | |
| onChange={(e) => setTargetKeyWithMode(e.target.value)} | |
| className={`${inputClass} w-28 px-2 py-1.5 cursor-pointer`} | |
| > | |
| {ALL_KEYS_WITH_MODES.map(k => ( | |
| <option key={k} value={k}>{k}</option> | |
| ))} | |
| </select> | |
| <button onClick={() => handleKeyShift(1)} className={btnClass}>+</button> | |
| {semitones !== 0 && ( | |
| <span className="text-xs text-gray-500 font-mono ml-1"> | |
| {semitones > 0 ? '+' : ''}{semitones}st | |
| </span> | |
| )} | |
| {semitones !== 0 && ( | |
| <button | |
| onClick={() => setTargetKeyWithMode(`${detection.key} ${detection.mode}`)} | |
| className="text-[10px] text-gray-500 hover:text-gray-300 transition-colors ml-1" | |
| > | |
| Reset | |
| </button> | |
| )} | |
| </div> | |
| {/* Divider */} | |
| <div className="w-px h-10 bg-white/[0.08]"></div> | |
| {/* Apply Button */} | |
| <button | |
| onClick={handleApply} | |
| disabled={!hasChanges || isProcessing} | |
| className={`px-5 py-2 rounded-lg text-sm font-medium transition-all ${ | |
| !hasChanges || isProcessing | |
| ? 'bg-white/[0.04] text-gray-600 cursor-not-allowed' | |
| : 'bg-accent-500 hover:bg-accent-400 text-white' | |
| }`} | |
| > | |
| {isProcessing ? 'Processing...' : hasChanges ? (hasRegion ? 'Apply to Selection' : 'Apply') : 'No Changes'} | |
| </button> | |
| {/* Original values */} | |
| {(semitones !== 0 || Math.abs(targetBpm - detection.bpm) > 0.1) && ( | |
| <span className="text-[10px] text-gray-600 ml-auto"> | |
| Original: {detection.key} {detection.mode} / {detection.bpm.toFixed(1)} BPM | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| export default AnalysisDisplay | |