merge chord/key/progressions into banner, remove SafeNotes/ChordDisplay/ProgressionSuggestions panels

This commit is contained in:
vadimwit
2026-03-06 00:20:20 +00:00
parent ca6b901bc8
commit 0eb00784a6
2 changed files with 123 additions and 79 deletions
+4 -13
View File
@@ -1,11 +1,7 @@
import { useState, useCallback, useRef, useEffect } from 'react'
import AudioCapture from './components/AudioCapture'
import ProgressionBanner from './components/ProgressionBanner'
import KeyDisplay from './components/KeyDisplay'
import ChordDisplay from './components/ChordDisplay'
import SafeNotes from './components/SafeNotes'
import Fretboard from './components/Fretboard'
import ProgressionSuggestions from './components/ProgressionSuggestions'
import Tuner from './components/Tuner'
import Piano from './components/Piano'
import { NOTES, detectKey, detectTopKeys, matchChordFromChroma, detectRepeatingProgression, getChordTones } from './lib/theory'
@@ -295,17 +291,12 @@ export default function App() {
chordHistory={chordHistory}
keyInfo={effectiveKey}
detectedProgression={detectedProgression}
currentChord={currentChord}
/>
{/* ── Main grid ── */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<KeyDisplay keyInfo={effectiveKey} locked={!!lockedKey} />
<ChordDisplay history={chordHistory} />
<SafeNotes keyInfo={effectiveKey} currentChord={currentChord} />
<ProgressionSuggestions keyInfo={effectiveKey} />
{/* ── Instrument view — select in header ── */}
<div className="md:col-span-2">
<div className="flex flex-col gap-3">
<div>
<div className="flex items-center justify-between mb-1 px-1">
<span className="text-xs text-gray-500 uppercase tracking-widest">Instrument</span>
<select
@@ -324,7 +315,7 @@ export default function App() {
</div>
{/* ── Tuner — collapsible ── */}
<div className="md:col-span-2">
<div>
<button
onClick={() => setShowTuner(v => !v)}
className="w-full flex items-center justify-between px-4 py-2 bg-panel border border-border rounded-xl text-sm text-gray-400 hover:text-gray-200 hover:border-gray-500 transition-all"
+94 -41
View File
@@ -1,7 +1,7 @@
import { useRef, useEffect } from 'react'
import { toRomanNumeral } from '../lib/theory'
import { toRomanNumeral, getSuggestedProgressions } from '../lib/theory'
const HISTORY_SHOWN = 8 // ~2 bars at 4 chords/bar
const HISTORY_SHOWN = 12
function findLoopPosition(chordHistory, progression) {
if (!progression?.length || !chordHistory.length) return -1
@@ -17,67 +17,75 @@ function findLoopPosition(chordHistory, progression) {
return progression.indexOf(last)
}
export default function ProgressionBanner({ chordHistory, keyInfo, detectedProgression }) {
const { root, mode } = keyInfo ?? {}
export default function ProgressionBanner({ chordHistory, keyInfo, detectedProgression, currentChord }) {
const { root, mode, confidence } = keyInfo ?? {}
const progressions = root ? getSuggestedProgressions(root, mode) : []
// Newest chord is the last entry; we show the most recent HISTORY_SHOWN
const visible = chordHistory.slice(-HISTORY_SHOWN)
const current = visible[visible.length - 1]
const loopPos = findLoopPosition(chordHistory, detectedProgression)
const currentRN = root && current ? toRomanNumeral(current, root, mode) : ''
// Animate the current chord slot when it changes
const currentRef = useRef(null)
const prevChord = useRef(null)
useEffect(() => {
if (current && current !== prevChord.current && currentRef.current) {
currentRef.current.animate(
[{ opacity: 0, transform: 'scale(0.85)' },
{ opacity: 1, transform: 'scale(1)' }],
[{ opacity: 0, transform: 'scale(0.85)' }, { opacity: 1, transform: 'scale(1)' }],
{ duration: 200, easing: 'ease-out', fill: 'forwards' }
)
prevChord.current = current
}
}, [current])
const loopPos = findLoopPosition(chordHistory, detectedProgression)
if (!chordHistory.length) {
return (
<div className="bg-panel border border-border rounded-2xl p-5 mb-4 flex items-center justify-center h-28">
<p className="text-gray-600">Start listening to detect chords</p>
<div className="bg-panel border border-border rounded-2xl p-4 mb-3 flex gap-4 min-h-[160px]">
{/* ── Left: key + chord history + loop ── */}
<div className="flex-1 min-w-0 flex flex-col">
{/* Key */}
<div className="flex items-baseline gap-2 mb-2">
{root ? (
<>
<span className="text-2xl font-bold text-accent">{root}</span>
<span className="text-gray-400 text-sm">{mode}</span>
{confidence && (
<span className="text-xs text-gray-600">{Math.round(confidence * 100)}%</span>
)}
</>
) : (
<span className="text-gray-600 text-sm">Detecting key</span>
)}
</div>
)
}
return (
<div className="bg-panel border border-border rounded-2xl p-5 mb-4">
{/* ── Chord history strip: all HISTORY_SHOWN chords at consistent size ── */}
<div className="flex items-stretch gap-1 overflow-x-auto pb-1">
{/* Chord history strip */}
{!chordHistory.length ? (
<p className="text-gray-600 text-sm flex-1 flex items-center">Start listening to detect chords</p>
) : (
<div className="flex items-end gap-1 overflow-x-auto pb-1 flex-1">
{visible.map((chord, i) => {
const isCurrent = i === visible.length - 1
const age = visible.length - 1 - i // 0 = current, higher = older
const opacity = Math.max(0.2, 1 - age * 0.1) // fade but stay readable
const age = visible.length - 1 - i
const opacity = Math.max(0.2, 1 - age * 0.07)
const rn = root ? toRomanNumeral(chord, root, mode) : ''
return (
<div
key={i}
ref={isCurrent ? currentRef : null}
style={{ opacity }}
className={`
flex flex-col items-center justify-end shrink-0 px-3 py-2 rounded-xl
transition-colors duration-200
${isCurrent
className={`flex flex-col items-center shrink-0 px-2 py-1 rounded-xl transition-colors duration-200 ${
isCurrent
? 'bg-accent/10 border border-accent/40 ring-1 ring-accent/20'
: 'border border-transparent'}
`}
: 'border border-transparent'
}`}
>
<span className={`font-black leading-none tracking-tight ${
isCurrent ? 'text-5xl text-accent' : 'text-3xl text-gray-200'
isCurrent ? 'text-4xl text-accent' : 'text-2xl text-gray-200'
}`}>
{chord}
</span>
<span className={`text-xs font-semibold mt-1 ${
<span className={`text-xs font-semibold mt-0.5 ${
isCurrent ? 'text-amber-400' : 'text-gray-500'
}`}>
{rn || '\u00A0'}
@@ -86,37 +94,82 @@ export default function ProgressionBanner({ chordHistory, keyInfo, detectedProgr
)
})}
</div>
)}
{/* ── Detected loop ── */}
{/* Loop */}
{detectedProgression && (
<div className="mt-4 pt-3 border-t border-border">
<p className="text-xs text-gray-500 uppercase tracking-widest mb-2"> Detected loop</p>
<div className="flex gap-2 flex-wrap">
<div className="mt-2 flex items-center gap-1.5 flex-wrap">
<span className="text-xs text-gray-500"></span>
{detectedProgression.map((chord, i) => {
const isActive = i === loopPos
const rn = root ? toRomanNumeral(chord, root, mode) : chord
return (
<div
key={i}
className={`flex flex-col items-center px-4 py-2 rounded-xl border transition-all duration-200 ${
className={`flex flex-col items-center px-2 py-1 rounded-lg border transition-all duration-200 ${
isActive
? 'bg-accent/20 border-accent shadow-[0_0_14px_rgba(168,85,247,0.35)]'
? 'bg-accent/20 border-accent shadow-[0_0_10px_rgba(168,85,247,0.3)]'
: 'bg-border border-border'
}`}
>
<span className={`text-2xl font-bold leading-none ${isActive ? 'text-accent' : 'text-gray-200'}`}>
<span className={`text-sm font-bold leading-none ${isActive ? 'text-accent' : 'text-gray-300'}`}>
{chord}
</span>
<span className={`text-xs mt-1 font-semibold ${isActive ? 'text-amber-400' : 'text-gray-500'}`}>
<span className={`text-xs ${isActive ? 'text-amber-400' : 'text-gray-600'}`}>
{rn}
</span>
</div>
)
})}
<span className="self-center text-gray-600 text-sm pl-1"> loop</span>
</div>
<span className="text-gray-600 text-xs"> loop</span>
</div>
)}
</div>
{/* ── Divider ── */}
<div className="w-px bg-border shrink-0" />
{/* ── Right: current chord + genre progressions ── */}
<div className="w-64 shrink-0 flex flex-col">
{current ? (
<>
{/* Big chord */}
<div className="text-center mb-3">
<div className="text-6xl font-black text-amber-400 leading-none">{current}</div>
<div className="text-sm text-gray-500 mt-1">{currentRN}</div>
</div>
{/* Genre progressions */}
<div className="space-y-1.5 overflow-y-auto flex-1">
{progressions.map(prog => (
<div key={prog.genre} className="flex items-center gap-2">
<span className="text-xs text-gray-600 w-10 shrink-0">{prog.genre}</span>
<div className="flex gap-1 flex-wrap">
{prog.chords.map((chord, i) => (
<span
key={i}
className={`px-1.5 py-0.5 rounded text-xs font-medium transition-colors ${
chord === current
? 'bg-accent/30 text-accent border border-accent/50'
: 'bg-border text-gray-400'
}`}
>
{chord}
<span className="ml-0.5 text-gray-600 opacity-60">({prog.rn[i]})</span>
</span>
))}
</div>
</div>
))}
</div>
</>
) : (
<div className="flex items-center justify-center flex-1">
<p className="text-gray-600 text-sm text-center">Play a chord to see progressions</p>
</div>
)}
</div>
</div>
)
}