feat(app): Jam Roulette — one-click genre + key + progression to jam over (task L-60, 2/2)
The 🎲 button opens a genre popover (Surprise me + the 10 KB styles); rollJam seeds a random key + an interesting round-trip-safe progression as a committed loop, so the whole dashboard fills exactly as if detection found it — banner chips, voicing rail, licks, related progressions. Three commit-layer guards keep the seed alive until the band takes over: the null-clear skips seeded loops (a seed would otherwise die one commit before the earliest possible confirmation), the agreement branch confirms it, replacement swaps once real playing is consistent, New Song clears it. An amber "rolled · style · name · bars — play it!" provenance chip flips to the normal loop marker on first agreeing detection. Audio contract grep: zero hits. Critic PASS (seed timeline hand-traced; §3.5 invariant verified at every write). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,9 +11,17 @@ import { findLoopPosition } from '../lib/match'
|
||||
// (p-4 → p-2, history pb-1 dropped) so the strip lands at ~76px.
|
||||
const HISTORY_SHOWN = 5
|
||||
|
||||
export default function ProgressionBanner({ chordHistory, keyInfo, detectedProgression, onChordClick }) {
|
||||
export default function ProgressionBanner({ chordHistory, keyInfo, detectedProgression, seedInfo, onChordClick }) {
|
||||
const { root, mode, confidence } = keyInfo ?? {}
|
||||
|
||||
// Jam-roulette provenance (task L-60, jam-roulette.md §1.3): while a rolled
|
||||
// loop is unconfirmed the loop row swaps its ♻ chrome for 🎲 + an amber chip
|
||||
// naming the source; it flips back to normal the moment live detection
|
||||
// confirms it (App clears seedInfo). The loop chips themselves are identical.
|
||||
const seedBars = Array.isArray(seedInfo?.bars)
|
||||
? seedInfo.bars.reduce((a, b) => a + (b ?? 0), 0)
|
||||
: null
|
||||
|
||||
const visible = chordHistory.slice(-HISTORY_SHOWN)
|
||||
const current = visible[visible.length - 1]
|
||||
const loopPos = findLoopPosition(chordHistory, detectedProgression)
|
||||
@@ -93,7 +101,7 @@ export default function ProgressionBanner({ chordHistory, keyInfo, detectedProgr
|
||||
<>
|
||||
<div className="w-px h-6 bg-border shrink-0 self-center" />
|
||||
<div className="flex items-center gap-1.5 flex-wrap self-center">
|
||||
<span className="text-xs text-gray-500">♻</span>
|
||||
<span className="text-xs text-gray-500">{seedInfo ? '🎲' : '♻'}</span>
|
||||
{detectedProgression.map((chord, i) => {
|
||||
const isActive = i === loopPos
|
||||
const rn = root ? toRomanNumeral(chord, root, mode) : chord
|
||||
@@ -114,7 +122,14 @@ export default function ProgressionBanner({ chordHistory, keyInfo, detectedProgr
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<span className="text-gray-600 text-xs">→ loop</span>
|
||||
{seedInfo ? (
|
||||
<span className="text-amber-400 text-xs">
|
||||
rolled · {seedInfo.styleLabel} · {seedInfo.name}
|
||||
{seedBars != null ? ` · ${seedBars} bars` : ''} — play it!
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-600 text-xs">→ loop</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user