Skip to content

Instantly share code, notes, and snippets.

@StijnZanders
Created April 26, 2026 12:46
Show Gist options
  • Select an option

  • Save StijnZanders/aceb0bb5c4453dd897558a214900f013 to your computer and use it in GitHub Desktop.

Select an option

Save StijnZanders/aceb0bb5c4453dd897558a214900f013 to your computer and use it in GitHub Desktop.
AI Triage UI - a spec for building keyboard-driven human-in-the-loop decision interfaces

AI Triage UI

A spec for building keyboard-driven human-in-the-loop decision UIs. Give this to any AI coding tool and it should produce a working prototype.

The idea: AI prepares decisions, humans make the call. Every override teaches the system something.

What you're building

A card stack. Each card is one case that needs a human decision. The AI has already done the investigation: scored the case, surfaced the key signals, written a draft action (email, routing decision, approval, whatever fits your domain). The human reviews and either accepts or overrides.

The whole thing runs on arrow keys. No mouse needed.

Card anatomy

Each card contains, top to bottom:

Header — case title, metadata line (tier, value, tenure), score badge, severity label.

Signal bar — exactly 4 signals in a horizontal row. Each signal is a value + label. These are the numbers the human scans first. Pick the 4 most decision-relevant metrics for your domain. The signal bar must not scroll or resize.

Draft action — the thing AI is proposing. In outreach this is an email draft (subject + body). In support routing this is a suggested queue + priority. In approvals this is a recommendation + rationale. The draft should be editable inline.

Evidence panel (collapsed by default) — the "why." Expands on press of ↓. Each row: a metric value on the left, a headline + detail paragraph on the right. One row per signal. This is where the human goes when something feels off.

Override menu (hidden by default) — appears on press of ←. Four domain-specific options in a 2x2 grid. Each option has a letter key (A/B/C/D) and a label. Selecting one swipes the card left.

Action bar — three buttons: Override (←), Why (↓), Accept/Send (→). Each button shows its keyboard shortcut. These are visual affordances for the arrow keys, not the primary interaction method.

Footer — queue position ("3 of 14").

Keyboard interaction

This is the core of the pattern. Everything is operable without a mouse.

DEFAULT:
  →  Accept the AI recommendation. Card swipes right.
  ←  Open override menu.
  ↓  Expand evidence panel. If already expanded, scroll down.
  ↑  Scroll evidence panel up. If at top, collapse it.

OVERRIDE MENU OPEN:
  A/B/C/D  Select override option. Card swipes left.
  Escape   Close menu, back to default.

EDIT MODE (when editing the draft inline):
  Shift+Enter  Send the edited version. Card swipes right.
  Escape       Cancel edit, discard changes.

Important: disable arrow key navigation during card exit animations to prevent double-fires. Skip keyboard events when focus is in an input/textarea (except edit mode shortcuts).

Card stack

3 cards visible at a time. The stack communicates "there's more" without showing a count.

  • Position 0 (current): full size, fully interactive.
  • Position 1 (next): slightly narrower (scaleX ~0.96), dimmed, shifted down ~8px, no pointer events.
  • Position 2 (third): narrower still (scaleX ~0.92), more dimmed, shifted down ~16px, no pointer events.

Use transform-origin: top center so the scaling pulls the sides in symmetrically while the top edge stays aligned.

Cards beyond position 2 are hidden entirely (display: none).

Transitions

  • Accept: card slides to 120% right, rotates ~8deg, fades out. ~350ms ease-out.
  • Override: card slides to 120% left, rotates ~-8deg, fades out. ~350ms ease-out.
  • Promotion: remaining cards animate up to their new positions. Use CSS class swapping, not DOM destruction/recreation.

The exiting card must have a higher z-index than the stack (z-index: 50) so it animates above, not behind.

DOM strategy

Build all cards once on init. Assign CSS classes (stack-0, stack-1, stack-2, stack-hidden) to control visibility and position. On each decision, add an exit class to the current card, then promote the remaining cards by reassigning classes. Never destroy and recreate card DOM nodes mid-session.

Progressive disclosure

The default card view should be scannable in under 5 seconds: header, signals, draft, action buttons. That's enough for ~80% of decisions.

The evidence panel is for the other 20%. It expands with a height/opacity animation on ↓. It should be scrollable if content overflows.

Layout constraint: header, signal bar, action buttons, and footer must never scroll or shrink. Only the middle section (draft + evidence panel) scrolls. Use flex layout with flex-shrink: 0 on the fixed sections.

Data shape

Case {
  id: string
  title: string
  meta: string
  score: number (0-100)
  label: string ("HOT LEAD", "WARM", "NEUTRAL", etc.)
  signals: [{ value: string, label: string }]  // exactly 4
  action: string  // recommended action label
  draft: {
    subject: string
    body: string  // can contain HTML for highlights
  }
  findings: [{
    headline: string
    detail: string
  }]  // one per signal
}

Override {
  key: string  // "a", "b", "c", "d"
  label: string  // domain-specific action name
}

State

Minimal. No state management library needed.

currentIndex: number
isAnimating: boolean
deepDiveOpen: boolean
overrideMenuOpen: boolean
editMode: boolean
totalAccepted: number
totalOverridden: number

Feedback

Every decision triggers:

  • A toast notification (bottom-right, auto-dismiss after ~2s).
  • A running counter in the top bar that increments on accept.
  • Queue position updates in the card footer.

When the queue is empty, show a completion screen with summary stats: total reviewed, total accepted, total overridden.

The override as training signal

This is what makes the pattern compound. Every human decision is a labeled data point.

Track per decision:

  • Case ID + all signals at time of decision
  • Accepted or overridden
  • If overridden: which option was selected
  • Time spent on the card before deciding
  • If draft was edited: before/after text

What this gives you over time:

  • Override rate by signal pattern (which combinations does the human disagree with?)
  • Override rate by case type (are borderline cases getting overridden more?)
  • Decision velocity (which cases take longer? those are where the AI summary isn't sufficient)
  • Edited drafts as fine-tuning data for the draft generator

The override rate should decline over time as the system learns from disagreements.

Adapting to your domain

The interaction pattern is the same everywhere. What changes:

Component Outreach Support routing Invoice approval Content moderation
Signals Usage growth, plan cap, login freq, feature requests Sentiment, topic, urgency, customer tier Amount, vendor, GL code, policy match Toxicity score, user history, report count, context
Draft Email with subject + body Suggested queue + priority + response template Approve/reject recommendation + rationale Keep/remove recommendation + explanation
Override options Edit draft, Skip, Reassign to AE, Snooze Re-route, Escalate, Merge, Defer Flag for review, Request docs, Split, Reject Warn user, Reduce visibility, Escalate, Ignore

Swap the case data, signals, draft format, and override options. Keep the card layout, keyboard nav, and stack behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment