jam-tracks / frontend /src /components /AnalysisDisplay.jsx
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