Skip to content

rtmg/web: named-slot prompt deck (kill A/B crossfade)#178

Open
hthillman wants to merge 3 commits into
mainfrom
hunter/prompt-deck
Open

rtmg/web: named-slot prompt deck (kill A/B crossfade)#178
hthillman wants to merge 3 commits into
mainfrom
hunter/prompt-deck

Conversation

@hthillman
Copy link
Copy Markdown
Collaborator

Summary

Three coordinated UX changes to the Tags feature in the realtime motion-graph web demo.

1. Kill the A/B crossfade. Replace with a named-slot deck.
The old two-textarea + blend-slider UI read as an abstract implementation detail. Operators rarely manually scrubbed the slider mid-performance; the more common gesture was "I want to switch to a different idea." So:

  • One "active prompt" textarea, bound to the current slot.
  • Strip of named slot chips underneath ("Dubstep" / "Daft Punk" by default, + to add, double-click to rename, × to delete).
  • Tap a non-active slot → the new `lib/promptDeck.ts` orchestrator loads the target text into whichever engine slot is currently inactive, sends the new A/B pair, and tweens `prompt_blend` toward it via the existing smoothing system. The lerp is still there — just no longer operator-visible.
  • Server protocol completely unchanged. The deck is a client-only construct mapped onto the existing two-slot engine via ping-pong.

2. Surface the Strength/Structure dependency.
Inline caption under the textarea: "Hits hardest at high Strength + low Structure." The relationship was tribal knowledge that the UI never communicated.

3. HeroMacros gets a Prompts zone.
Compact slot-chip strip in the bottom-center bay (mirrors the Styles fader zone next to it) + a `⋯` that opens Full Controls already routed to the Styles tab — new `dd:open-drawer-tab` event lets the chip address "open drawer + select tab" in one move. To make horizontal room without compressing Tools, the Record / Curve / Full / MIDI buttons are now a 2×2 grid instead of a vertical 4-stack.

State model

New store fields on `usePerformanceStore`:

  • `promptSlots: PromptSlot[]` — the deck.
  • `currentSlotId: string` — which slot is logically active.
  • `physicalSlot: 0 | 1` — which engine slot (A or B) currently holds the active text.

Switching: load target into the inactive engine slot, `sendPrompt(A, B)`, set `prompt_blend` target to the inactive slot, swap `physicalSlot`. The Smooth toggle's `smoothMs` controls the transition duration; with smoothing off the switch is instant.

Defaults seed two slots from the existing promptA/promptB defaults so the deck is non-empty from the first session. Saved-session restore continues to write `promptA/promptB` directly; the deck rebuilds on next interaction.

Test plan

  • Open Full Controls → Styles tab. Verify the new deck UI: one big textarea, slot strip, hint line, Send Tags button. The old A/B slider should be gone.
  • Type in the textarea, click Send Tags — engine adopts the new text (verify via `window.__demonPromptLog = true`).
  • Click a non-active slot — engine should fade smoothly to the new prompt (with Smooth on; instant with Smooth off).
  • Click "+" to add a new slot — drops into rename mode automatically, becomes the active slot.
  • Hover an existing slot — `×` button appears. Click it to delete (no-op on the last remaining slot).
  • Double-click a slot label to rename inline. Enter commits, Esc cancels.
  • On the HeroMacros bay (session active, drawer closed): the Prompts zone should show the slot chips. Tapping a chip switches slots the same as the in-tile click. The `⋯` button opens Full Controls already on the Styles tab.
  • Tools cluster on the right of the bay is a 2×2 grid (Record/Curve over Full/MIDI). When the drawer opens, it collapses to a horizontal row (same as before).
  • No regressions on existing prompt-blend MIDI bindings or `schedule_curves` automation of `prompt_blend`.

Follow-ups (out of scope for this PR)

  • Hold-two-fingers / shift-click on a slot to manually live-blend between two slots (the old slider gesture, now hidden by default).
  • Per-slot LoRA presets (today the LoRA strengths are global; would be a natural extension once the deck is settled).
  • Saved-session integration: roundtripping the entire deck instead of just A/B.

🤖 Generated with Claude Code

@ryanontheinside ryanontheinside self-requested a review May 30, 2026 17:24
Copy link
Copy Markdown
Collaborator

@ryanontheinside ryanontheinside left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deck UI itself is genuinely great. The named-slot framing, the bay integration, and the Strength/Structure hint are all real wins.
My one hard blocker is that this removes controllable prompt blending as a capability.

It comes down to the philosophy we talked through yesterday: DEMON is an instrument, and for an instrument the answer is always more control, never less. People perform on MIDI controllers, and blending between two prompts on a knob is an obvious, expressive gesture. That's not an edge case, it's an example of the core that makes this an instrument.

The PR's reasoning is basically "users read the slider as an abstract implementation detail." Fair observation, but that's a communication problem, not a feature problem, and with a handful of user interviews behind it I don't think we have the signal to call it. Before removing something hard-won with explicit use cases because people don't immediately get it, I'd want to ask:

  • Is there a better way to present the gesture so it reads as musical? (e.g. blend lives on the slots: hold/drag between two chips, not a naked A/B slider.)
  • Who's actually confused, and how many? Excising a practical, hard-won feature is a high bar: I'd want a large sample and a supermajority before I'd believe removal beats redesign.

And these two behaviors aren't in tension. A clean tap-to-switch deck and controllable blend (drag/hold between slots) can live in the same design: the deck is the simple default, the blend is the depth underneath. The shift-click / two-finger live-blend you already noted as a follow-up is exactly the right seed.

So my ask: keep this PR's UI wins, but treat the simplification as configurable depth, not removal. Simplify the default surface; keep the full blend control reachable. If we simplify, it should be opt-in, not destructive.

Requesting changes on that basis, but I'm excited about where this goes. Happy to whiteboard the combined version anytime.

The more knobs we add, the more this becomes an instrument, which is what differentiates this from other AI music tools.

@hthillman
Copy link
Copy Markdown
Collaborator Author

Super valid @ryanontheinside! I’ll do another round with controllable blending restored.

@hthillman hthillman force-pushed the hunter/prompt-deck branch from 4733b7f to 730cd9e Compare June 1, 2026 17:15
@hthillman
Copy link
Copy Markdown
Collaborator Author

@ryanontheinside

crossfader is back with MORE CONTROLLABILITY THAN BEFORE. Both endpoints are popover-pickable, so you can use any pair (A→B, A→C, B→C, etc).

@hthillman hthillman requested a review from ryanontheinside June 2, 2026 00:04
hthillman and others added 3 commits June 2, 2026 08:52
The old A/B textareas + crossfade slider tested badly: operators read
the blend slider as a confusing implementation detail and the deck was
buried two levels deep in the drawer. Three changes here:

1. Prompt deck. PromptsTile now shows ONE active textarea and a strip
   of named slots ("+", rename, delete). Tapping a slot ping-pongs the
   new prompt through the engine's inactive A/B slot and tweens
   prompt_blend toward it via the existing smoothing system — the lerp
   is preserved, just no longer operator-visible. Slot list is client-
   only (lib/promptDeck.ts orchestrates); server protocol unchanged.

2. Strength/Structure hint. Inline caption under the textarea:
   "Hits hardest at high Strength + low Structure." The relationship
   was tribal knowledge; this surfaces it where operators actually
   read it.

3. HeroMacros gets a Prompts zone — compact slot chips + a "⋯" that
   opens Full Controls already routed to the Styles tab via a new
   dd:open-drawer-tab event. To free horizontal room without
   eating Tools, the Record / Curve / Full / MIDI buttons are now a
   2x2 grid instead of a vertical 4-stack.

Defaults seed two slots (Dubstep, Daft Punk) from the existing
promptA/promptB defaults so the deck is non-empty from the first
session. Saved-session restore still writes promptA/promptB directly
and the deck rebuilds on next interaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-2 iteration on the prompt-deck UX after dogfooding the first
pass. Three changes:

1. Hero-bay reverted to original 4-tool vertical stack. The 2x2 grid
   + PromptDeckChip from the previous commit are out — putting a
   slot-deck chip in HeroMacros felt cluttered, and turning Tools
   into a grid miscategorized "Prompt Mode" as a peer of Record /
   Curve / MIDI. The deck UX in PromptsTile (drawer Styles tab)
   stays as-is.

2. New "Prompt Mode" bay view — focused surface for tuning the
   active prompt. Layout: Strength + Structure knobs | prompt
   textarea + Send | Emphasize prompt / Disable lora auto-trigger /
   Full Controls. Mode toggle is a floating Dock/Prompt segmented
   control above the bay's top edge — sits in bay chrome, not in
   the tools cluster, so it reads as a view switcher rather than
   an action tool. Same position in both modes. Auto-snaps back to
   Dock when the drawer or curve overlay opens.

3. "Disable lora auto-trigger" checkbox (per-session, default off)
   gates both enabledLoraTriggerPrefix() and stripLeadingTriggers()
   at call time, so when on, the wire prompt is exactly the
   operator's text — no prepend, no strip. Applies to every send
   path (Send button, in-tile Send Tags, MCP-driven sends, key/sig
   re-sends) because the gate lives in the helpers themselves.

"Emphasize prompt" checkbox caches the current hint_strength
target, snaps to 0.15 while on, restores the cached value on
toggle-off or view exit (component unmount). Toggling Structure
manually while emphasize is on still works — the restore goes back
to the pre-emphasize value, not the post-touch one, so the toggle
remains a clean undo.

Drops the dd:open-drawer-tab event + AdvancedDrawer handler — only
consumer was the removed chip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses Ryan's PR-178 feedback: prompt blending should be a
first-class instrument gesture, not an opt-in. Round-3 rebuild of
the prompt deck UX:

1. Always-on crossfader in PromptsTile when slots.length >= 2.
   Generalizes the legacy A/B blender to any pair: both endpoints
   are popover-pickable, so A→B / A→C / B→C are all expressible by
   re-pointing either side. data-param="prompt_blend" carries
   through so right-click → MIDI-learn binds to a hardware knob
   exactly like before, and the "B + ▲▼" keyboard nudge is still
   wired (kbd hint restored).

2. Three concerns kept deliberately separate in the perf store:
     - focusedSlotId  → textarea binding (UI-only)
     - currentSlotId  → engine A endpoint
     - blendPartnerId → engine B endpoint
   Tap a chip = focuses it (★ badge); A and B don't move. The
   crossfader's labels stay stable mid-performance — they only
   change when the operator explicitly picks via the A or B
   popover. Editing the focused chip's text mirrors to engine
   promptA/promptB only when that chip is also loaded; otherwise
   edits stay in the slot until the operator loads it.

3. Endpoint badges (A / B) on the deck chips show "where is X
   loaded?" at a glance. Removed the dedicated "↹ Blend with…"
   button + the ✕ clear button — both rendered moot by the
   always-on crossfader with clickable endpoints.

4. lib/promptDeck.ts is now a thin orchestration layer:
     - focusPromptSlot(id)       → pure UI focus shift
     - setBlendEndpointA(id)     → load to engine A (+ focus)
     - setBlendPartner(id)       → load to engine B (+ focus)
     - addAndFocusPromptSlot()   → add + focus, no auto-load
     - removePromptSlot(id)      → re-pick endpoints / focus
   ensureValidPartner() runs after every mutation so the
   crossfader always has a valid non-self partner whenever there
   are 2+ slots — and clears to null (slider hidden) at 1 slot.

Hero Prompt Mode is unchanged — it binds to currentSlotId
(engine A, what's playing) which is the right semantic for the
performance surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hthillman hthillman force-pushed the hunter/prompt-deck branch from 730cd9e to 35dbcf4 Compare June 2, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants