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:
vadimwit
2026-07-11 19:48:14 +01:00
parent 8d74e54f5a
commit 1df6fe8e9e
2 changed files with 195 additions and 5 deletions
+18 -3
View File
@@ -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>
</>
)}