Back to Blog

Halving Reviewer Clicks: Feedback Navigator, Keyboard Navigation, and Read-Only Context Patterns

By · 7 min read
UX Engineering React TypeScript Accessibility Admin UI Keyboard Navigation

The review workflow on the platform I work on asks human reviewers to inspect AI-generated artifacts, read LLM judge outputs, and record approve/reject decisions with structured feedback. The interface was technically complete — every piece of information was reachable — but the path to reach it demanded too many clicks. Accordions enforced single-section viewing, so opening File Artifacts closed LLM Judges and vice versa. Navigation between feedback items was mouse-only. And model outputs sat in the review section, demanding feedback even though they were reference material, not reviewable artifacts.

I shipped three PRs in the same sprint to fix this. Together they touched roughly 1,800 lines across navigation, layout, and gate-review derive logic. The headline result: reviewers cut their click count in half. This post walks through the click analysis, the FeedbackNavigator component, keyboard shortcuts, auto-advance after decisions, and the read-only context pattern that removed phantom feedback items from the workflow entirely.

Measuring the click problem

Before writing code, I mapped the reviewer journey click-by-click. A single artifact with one file and three LLM judges required eight clicks to review: open the file accordion, scroll to each judge, open the judge accordion, click into each feedback field, and navigate back. A full session — eight artifacts, three judges each — ballooned to roughly 71 clicks of pure navigation overhead, before any actual judgment.

After the sprint, the same paths dropped to four clicks per artifact and roughly 35 clicks per session. That is a 50–51% reduction — not from hiding information, but from showing more of it at once and giving reviewers faster ways to move through it.

Scenario Before After Improvement
Review single artifact (1 file + 3 judges) 8 clicks 4 clicks 50%
Full session (8 artifacts, 3 judges each) ~71 clicks ~35 clicks 51%
Clicks are a proxy for cognitive load. Every accordion toggle and scroll hunt is a context switch that pulls attention away from the actual review decision.

Independent accordion sections

The first fix was embarrassingly simple. The artifact detail panel used a single-open accordion: opening File Artifacts closed LLM Judges, and opening LLM Judges closed File Artifacts. Reviewers constantly toggled back and forth to cross-reference a file against judge feedback — two clicks per comparison, repeated dozens of times per session.

I changed both sections to open by default and toggle independently. Reviewers can now see file artifacts and judge feedback simultaneously. No architectural change was required — just removing the singleOpen constraint and defaulting both panels to expanded. The impact was immediate: cross-referencing went from a toggle dance to a glance.

FeedbackNavigator: progress at a glance

Accordion independence solved visibility, but reviewers still needed a way to jump between feedback items without hunting through scroll position. I built a FeedbackNavigator component — 196 lines — that renders a compact chip-based navigator showing every feedback item in the current artifact.

Each chip displays a status dot: red for required, yellow for in-progress, green for complete. Click a chip and the view scrolls to that item and focuses its input. A progress label — "5 of 11 reviewed" — gives reviewers session-level awareness without opening a separate panel.

interface FeedbackNavigatorProps {
  items: FeedbackItem[];
  activeId: string | null;
  onSelect: (id: string) => void;
}

function FeedbackNavigator({ items, activeId, onSelect }: FeedbackNavigatorProps) {
  const reviewable = items.filter((item) => !isContextOnlyStep(item.stepId));
  const completed = reviewable.filter((item) => item.status === 'complete').length;

  return (
    <nav aria-label="Feedback navigation" className="feedback-navigator">
      <span className="feedback-navigator__progress">
        {completed} of {reviewable.length} reviewed
      </span>
      <div role="listbox" className="feedback-navigator__chips">
        {reviewable.map((item) => (
          <button
            key={item.id}
            role="option"
            aria-selected={item.id === activeId}
            className={cn('feedback-chip', item.id === activeId && 'active')}
            onClick={() => onSelect(item.id)}
          >
            <StatusIndicator status={item.status} />
            {item.label}
          </button>
        ))}
      </div>
    </nav>
  );
}

The navigator reuses the existing StatusIndicator component from the reviewer-flow module, keeping visual language consistent across the platform. Chips are keyboard-focusable and wired into the same selection state that drives scroll-and-focus behavior in the feedback column.

Keyboard navigation for power users

Mouse-driven chip clicking helps casual reviewers. Power users — who process dozens of artifacts per shift — needed something faster. I added a keyboard navigation layer scoped per artifact, so navigation state resets cleanly when the reviewer moves to the next artifact.

  • j / k — next / previous feedback item
  • n — next incomplete item (skips already-reviewed)
  • Home / End — first / last item

The hook listens on the artifact panel container, ignores keystrokes when focus is inside a text input, and wraps at list boundaries only for j/kn always searches forward from the current position.

function useFeedbackKeyboardNav(
  items: FeedbackItem[],
  activeId: string | null,
  onSelect: (id: string) => void,
) {
  const reviewable = useMemo(
    () => items.filter((item) => !isContextOnlyStep(item.stepId)),
    [items],
  );

  useEffect(() => {
    function handleKeyDown(e: KeyboardEvent) {
      if (isEditableTarget(e.target)) return;

      const idx = reviewable.findIndex((item) => item.id === activeId);

      switch (e.key) {
        case 'j':
          onSelect(reviewable[Math.min(idx + 1, reviewable.length - 1)].id);
          break;
        case 'k':
          onSelect(reviewable[Math.max(idx - 1, 0)].id);
          break;
        case 'n':
          onSelect(findNextIncomplete(reviewable, idx)?.id ?? activeId!);
          break;
        case 'Home':
          onSelect(reviewable[0].id);
          break;
        case 'End':
          onSelect(reviewable[reviewable.length - 1].id);
          break;
      }
    }

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [reviewable, activeId, onSelect]);
}

Reviewers who learn the shortcuts can complete an entire artifact without touching the mouse — open artifact, tab to first field, approve/reject, auto-advance to next item, repeat.

Auto-advance after decisions

Even with keyboard navigation, reviewers hit a recurring friction point: after approving or rejecting an item, they paused to figure out what came next. That "what's next?" moment added seconds per decision across hundreds of decisions per day.

I added an onDecisionComplete callback to the DecisionToggle component. When a reviewer records a decision, the callback fires, maps the decision to a feedback status, and automatically navigates to the next incomplete item.

function mapDecisionToFeedbackStatus(
  decision: ReviewDecision,
): FeedbackStatus {
  if (decision === 'approved') return 'complete';
  if (decision === 'rejected') return 'complete';
  return 'in_progress';
}

function DecisionToggle({ itemId, onDecisionComplete }: DecisionToggleProps) {
  const handleDecision = (decision: ReviewDecision) => {
    submitDecision(itemId, decision);
    onDecisionComplete?.(itemId, mapDecisionToFeedbackStatus(decision));
  };

  return (
    <div className="decision-toggle">
      <button onClick={() => handleDecision('approved')}>Approve</button>
      <button onClick={() => handleDecision('rejected')}>Reject</button>
    </div>
  );
}

The auto-advance reuses the same findNextIncomplete helper from keyboard navigation, so chip selection, keyboard shortcuts, and post-decision flow all converge on one navigation model.

Model outputs as read-only context

The navigation improvements addressed how reviewers moved through feedback items. A separate problem was which items counted as feedback at all. Model outputs — the raw LLM generations that reviewers read for context — were rendered in the section: "review" group. That placement put them in the feedback column with approve/reject controls, even though they are reference material, not reviewable artifacts.

Reviewers had to acknowledge phantom feedback slots. Progress counts included them. Submit readiness checks blocked on them. The UI treated context and reviewable content identically because they shared the same section type.

I moved model output groups from section: "review" to section: "context" and introduced a derive-layer helper to identify context-only steps:

const CONTEXT_ONLY_STEP_IDS = new Set([
  'model_output',
  'generation_preview',
  'intermediate_reasoning',
]);

function isContextOnlyStep(stepId: string): boolean {
  return CONTEXT_ONLY_STEP_IDS.has(stepId);
}

Context-only items are excluded from progress counts, keyboard navigation, and submit readiness checks. In the explorer panel, they render with a "Read-only" badge, a lock icon, and muted styling so reviewers understand at a glance that no decision is expected.

The layout change was equally important. When renderFeedback returns null for a context-only step, the ReviewerFlowContent three-pane shell auto-collapses to two panes — explorer plus preview, with no feedback column. ResizableColumns switches to TWO_PANE_WEIGHTS when the feedback column is dropped, giving the preview pane more horizontal space for reading model output.

function OutputReviewStep({ step }: StepRendererProps) {
  if (isContextOnlyStep(step.id)) {
    return <ContextPreview content={step.output} />;
  }

  return (
    <ReviewerFlowContent
      explorer={<ArtifactExplorer step={step} />}
      preview={<ArtifactPreview step={step} />}
      feedback={renderFeedback(step)}
    />
  );
}

function renderFeedback(step: ReviewStep): React.ReactNode | null {
  if (isContextOnlyStep(step.id)) return null;
  return <FeedbackForm step={step} />;
}

Simplifying OutputReviewStep to a pure context viewer — no local state management, no decision handlers — removed an entire class of bugs where context steps accidentally participated in review lifecycle events.

Reusable layout patterns

These changes compose through existing abstractions rather than one-off conditionals. The StatusIndicator component carries over from reviewer-flow. ReviewerFlowContent already supported a three-pane layout; the new behavior is that it auto-collapses when feedback is null. ResizableColumns already had weight presets — adding TWO_PANE_WEIGHTS for the no-feedback case was a single configuration branch.

The pattern generalizes: any step that is informational rather than actionable should declare itself context-only at the derive layer, skip feedback machinery, and let the layout shell adapt. Future step types — pipeline logs, intermediate embeddings, debug traces — can follow the same path without new layout code.

Testing

I wrote 14 unit tests for FeedbackNavigator covering chip rendering, progress counts, active selection, and context-only exclusion. Separate tests cover mapDecisionToFeedbackStatus for each decision type. The gate-review derive logic — including isContextOnlyStep filtering in progress and submit readiness — has 118 tests that guard against regressions where context items creep back into reviewable counts.

The keyboard navigation hook is tested indirectly through integration tests on the artifact review panel, verifying that keystrokes advance selection and that input fields do not intercept navigation when unfocused.

What I learned

Admin review UIs accumulate friction in ways that are invisible in demos but brutal in production. Accordions that save vertical space cost cross-reference clicks. Feedback columns that treat every rendered output as reviewable inflate progress counts and block submission. The fix is not one big redesign — it is a set of targeted patterns: show more by default, navigate with chips and keyboard shortcuts, auto-advance after decisions, and separate context from review at the data layer.

Halving clicks sounds like a UX metric. For reviewers processing hundreds of artifacts per week, it is closer to an hour reclaimed per shift — time spent judging quality instead of hunting through accordions. The patterns shipped in three PRs, but they compose: the FeedbackNavigator respects context-only filtering, keyboard navigation shares the incomplete-item finder with auto-advance, and the two-pane layout activates automatically when feedback is not needed. That composability is what makes the sprint durable rather than a one-off polish pass.

Related Articles