import { useState } from 'react'
import { toRomanNumeral, CHORD_TYPES, NOTES } from '../lib/theory'
import {
findSimilarProgressions,
getChordSubstitutions,
progressionInKey,
styleVariationInKey,
parseChord,
} from '../lib/education'
// ─── Session Snapshot ─────────────────────────────────────────────────────────
function SessionSnapshot({ keyInfo, detectedProgression, chordHistory }) {
const { root, mode, confidence } = keyInfo ?? {}
const uniqueChords = [...new Set(chordHistory)]
const totalPlayed = chordHistory.length
return (
Key
{root ? (
{root}
{mode}
{confidence && {Math.round(confidence * 100)}% }
) : (
Detecting…
)}
Detected Loop
{detectedProgression?.length ? (
{detectedProgression.map((chord, i) => (
{chord}
{root &&
{toRomanNumeral(chord, root, mode)}
}
))}
) : (
None yet — keep playing!
)}
Session
{totalPlayed} chords ·
{uniqueChords.length} unique
{uniqueChords.length > 0 && (
{uniqueChords.slice(0, 10).map(c => (
{c}
))}
{uniqueChords.length > 10 && +{uniqueChords.length - 10} }
)}
)
}
// ─── Similar Progressions ─────────────────────────────────────────────────────
function SimilarProgressions({ similar, keyInfo, onChordClick }) {
const [expanded, setExpanded] = useState(null)
if (!similar.length) return (
Play more chords and lock a key to find similar famous progressions.
)
return (
{similar.map(prog => {
const isOpen = expanded === prog.id
const chordsInKey = keyInfo?.root ? progressionInKey(prog, keyInfo.root) : []
return (
{/* Header row */}
setExpanded(isOpen ? null : prog.id)}
className="w-full flex items-start justify-between gap-3 px-4 py-3 text-left"
>
{prog.name}
{prog.genre.map(g => (
{g}
))}
{prog.pattern}
{Math.round(prog.score * 100)}% match
{/* Chords in current key */}
{chordsInKey.length > 0 && (
{chordsInKey.map((c, i) => (
{ e.stopPropagation(); onChordClick?.(c) }}
className="px-2 py-0.5 bg-surface border border-border hover:border-accent/50 rounded text-xs font-bold text-gray-200 hover:text-accent transition-colors"
title={`See voicings for ${c}`}
>
{c}
))}
in {keyInfo.root} {keyInfo.mode}
)}
{isOpen ? '▲' : '▼'}
{/* Expanded detail */}
{isOpen && (
{prog.description}
{prog.tip && (
Insight: {prog.tip}
)}
{/* Song examples */}
Famous examples
{prog.songs.map(s => (
{s}
))}
{/* Style variations */}
{prog.styleVariations.length > 0 && (
Style variations
{prog.styleVariations.map(sv => {
const svChords = keyInfo?.root ? styleVariationInKey(sv, prog, keyInfo.root) : []
return (
{sv.label}
{sv.pattern}
{svChords.length > 0 && (
{svChords.map((c, i) => (
onChordClick?.(c)}
className="px-1.5 py-0.5 bg-accent/10 border border-accent/20 hover:border-accent rounded text-[11px] font-bold text-accent/90 hover:text-accent transition-colors"
title={`See voicings for ${c}`}
>
{c}
))}
in {keyInfo.root}
)}
)
})}
)}
)}
)
})}
)
}
// ─── Play It Differently ──────────────────────────────────────────────────────
function PlayDifferently({ progression, onChordClick }) {
if (!progression?.length) return (
Keep playing — a repeating progression will appear here with substitution ideas.
)
return (
Tap any substitution to see how to play it. These are harmonic replacements — same role, different colour.
{progression.map(chord => {
const subs = getChordSubstitutions(chord)
return (
{/* Original chord */}
onChordClick?.(chord)}
className="px-3 py-1.5 bg-accent text-white font-black rounded-lg text-sm shrink-0 hover:bg-purple-600 transition-colors"
title="See voicings"
>
{chord}
→
{/* Substitutions */}
{subs.map(sub => (
onChordClick?.(sub.chord)}
className="px-2.5 py-1.5 bg-panel border border-border hover:border-accent/50 hover:text-accent rounded-lg text-sm font-bold text-gray-300 transition-all"
>
{sub.chord}
{/* Tooltip */}
{sub.tip}
))}
)
})}
Hover substitutions to see what they change · click to see voicings
)
}
// ─── Chord Variation Ideas ────────────────────────────────────────────────────
const VARIATION_ROWS = [
{
label: '7th Upgrade',
desc: 'Add 7ths throughout — jazz and soul texture',
typeMap: { maj: 'maj7', min: 'min7', dom7: 'dom7', maj7: 'maj7', min7: 'min7', dim: 'dim7', add9: 'maj7', sus2: 'sus2', sus4: 'sus4', aug: 'aug', half_dim: 'half_dim', maj6: 'maj6', min6: 'min6' },
},
{
label: 'Sus2 Wash',
desc: 'Replace triads with sus2 — ambient and spacious',
typeMap: { maj: 'sus2', min: 'sus2', dom7: 'sus4', maj7: 'sus2', min7: 'sus2', add9: 'sus2', dim: 'dim', aug: 'aug', sus4: 'sus4', sus2: 'sus2', half_dim: 'half_dim', maj6: 'sus2', min6: 'sus2' },
},
{
label: 'Add9 Modern',
desc: 'Add9 on majors, m7 on minors — indie and neo-soul',
typeMap: { maj: 'add9', min: 'min7', dom7: 'dom7', maj7: 'add9', min7: 'min7', add9: 'add9', sus2: 'sus2', sus4: 'sus4', dim: 'dim', aug: 'aug', half_dim: 'half_dim', maj6: 'add9', min6: 'min7' },
},
{
label: 'Blues Dominant',
desc: 'All chords → dom7 — instant 12-bar blues energy',
typeMap: { maj: 'dom7', min: 'dom7', dom7: 'dom7', maj7: 'dom7', min7: 'dom7', add9: 'dom7', sus2: 'dom7', sus4: 'dom7', dim: 'dim7', aug: 'aug', half_dim: 'dom7', maj6: 'dom7', min6: 'dom7' },
},
]
function ProgressionVariationIdeas({ progression, onChordClick }) {
if (!progression?.length) return (
Keep playing — your progression will appear here.
)
return (
Your progression re-harmonised four ways. Click any chord to open its voicing explorer.
{VARIATION_ROWS.map(row => {
const transformed = progression.map(chord => {
const p = parseChord(chord)
if (!p) return chord
const newType = row.typeMap[p.type] ?? p.type
return NOTES[p.rootPc] + (CHORD_TYPES[newType]?.suffix ?? '')
})
return (
{row.label}
{row.desc}
{progression.map((orig, i) => (
{orig}
→
onChordClick?.(transformed[i])}
className="px-2.5 py-1 bg-panel border border-border hover:border-accent/50 hover:text-accent rounded-lg font-bold text-sm text-gray-200 transition-all"
>
{transformed[i]}
{i < progression.length - 1 && · }
))}
)
})}
)
}
// ─── Main ─────────────────────────────────────────────────────────────────────
export default function EducationPanel({ chordHistory, keyInfo, detectedProgression, onChordClick }) {
const [open, setOpen] = useState(false)
const [section, setSection] = useState('similar')
// Use detected progression if available, else last 4 unique chords from history
const workingProgression = detectedProgression?.length
? detectedProgression
: [...new Set([...chordHistory].reverse())].reverse().slice(-4)
const similar = findSimilarProgressions(workingProgression, keyInfo)
return (
setOpen(v => !v)}
className="w-full flex items-center justify-between px-4 py-2 text-sm text-gray-400 hover:text-gray-200 transition-all"
>
EDUCATION
{similar.length > 0 && (
{similar.length} match{similar.length !== 1 ? 'es' : ''}
)}
{open ? '▲' : '▼'}
{open && (
{/* Section nav */}
{[
{ key: 'snapshot', label: '📊 Session' },
{ key: 'similar', label: `🎵 Similar Progressions${similar.length ? ` (${similar.length})` : ''}` },
{ key: 'play', label: '🎨 Play Differently' },
{ key: 'variations',label: '🔀 Progression Variations' },
].map(s => (
setSection(s.key)}
className={`px-3 py-2 text-xs font-semibold whitespace-nowrap border-b-2 transition-all shrink-0 ${
section === s.key
? 'border-accent text-accent'
: 'border-transparent text-gray-500 hover:text-gray-300'
}`}>
{s.label}
))}
{section === 'snapshot' && (
)}
{section === 'similar' && (
)}
{section === 'play' && (
)}
{section === 'variations' && (
)}
)}
)
}