Skip to content
Star on GitHub
Review and approveDecision prompt
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Decision prompt

The agent asks a few quick questions before it starts. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add decision-prompt
Question 1 of 3
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add decision-prompt

React

$npx designme add decision-prompt --react

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

02

About

What it is for and what it does.

Decision prompt lets an agent ask a few multiple-choice questions before it starts a task, one question at a time. When the last one is answered it shows a short summary of the choices and a line that says the agent is getting going.

  • When a request is vague and two or three answers would change what the agent does.
  • When you want quick answers by keyboard instead of a long reply in chat.
  • When some questions can be left to the agent and people should be able to skip them.
  • When the person should see a recap of their choices before the work begins.
03

Usage

Import it and drive it from your own state.

Import

import { DecisionPrompt } from '@/components/designme/decision-prompt/DecisionPrompt';

Example

The demo above as a React component, 19 lines you can copy.

'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { DecisionPrompt, type DecisionQuestion } from './DecisionPrompt';

/* ---------- Decision prompt: three quick questions before a redesign, then a summary ---------- */
const QUESTIONS: DecisionQuestion[] = [
  { question: 'Which frames should the agent redesign?', label: 'Frames', options: [{ label: 'Pricing only', detail: '1 frame' }, { label: 'Pricing and checkout', detail: '3 frames' }, { label: 'The whole flow', detail: '9 frames' }] },
  { question: 'How bold can the changes be?', label: 'Scope', options: [{ label: 'Polish spacing and type', detail: 'Safe' }, { label: 'Rework the layout', detail: 'Medium' }, { label: 'Fresh direction', detail: 'Bold' }] },
  { question: 'Where should the result go?', label: 'Output', options: [{ label: 'New Figma page', detail: 'Keeps the original' }, { label: 'Branch in the repo', detail: 'Opens a PR' }, { label: 'Both', detail: 'Recommended' }] }
];

export default function DecisionPromptDemo() {
  return (
    <div style={{ width: '100%' }}>
      <DecisionPrompt questions={QUESTIONS} startLabel="Starting the redesign" />
    </div>
  );
}
04

Props

Everything DecisionPrompt takes.

PropTypeWhat it does
questionsrequiredDecisionQuestion[]
answers(number | null)[]The answers so far, an option's index or null for a skipped question. Leave it out to let Continue and Skip fill it
defaultAnswers(number | null)[]
onAnswersChange(answers: (number | null)[]) => void
onComplete(answers: (number | null)[]) => voidCalled once every question is answered, with the picked option's index for each, or null where the person skipped
skippedLabelstringWhat the summary says for a skipped question
startLabelstringThe line under the summary while the agent gets going
startedbooleanThe agent has got going: the line under the summary settles with a check
startedLabelstringWhat that line says once started. Defaults to startLabel
barebooleanSits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own
classNamestring
05

Code

The full source, as the CLI installs it.

DecisionPrompt.tsxReact · 106 lines
'use client';
import { useState, type KeyboardEvent } from 'react';
import { cx } from '../lib/cx';
import { useControllable } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { enter, Think } from '../lib/motion';
import '../designme.css';
import './decision-prompt.css';

export interface DecisionOption {
  label: string;
  /** Quiet text at the end of the row, like "3 frames" or "Recommended" */
  detail?: string;
}
export interface DecisionQuestion {
  question: string;
  /** A short name for the question in the summary, like "Scope" */
  label: string;
  options: DecisionOption[];
}

export interface DecisionPromptProps {
  questions: DecisionQuestion[];
  /** The answers so far, an option's index or null for a skipped question. Leave it out to let Continue and Skip fill it */
  answers?: (number | null)[];
  defaultAnswers?: (number | null)[];
  onAnswersChange?: (answers: (number | null)[]) => void;
  /** Called once every question is answered, with the picked option's index for each, or null where the person skipped */
  onComplete?: (answers: (number | null)[]) => void;
  /** What the summary says for a skipped question */
  skippedLabel?: string;
  /** The line under the summary while the agent gets going */
  startLabel?: string;
  /** The agent has got going: the line under the summary settles with a check */
  started?: boolean;
  /** What that line says once started. Defaults to startLabel */
  startedLabel?: string;
  /** Sits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own */
  bare?: boolean;
  className?: string;
}

/** The agent asks a few quick questions before it starts. Number keys pick, Enter continues. */
export function DecisionPrompt({
  questions, answers: value, defaultAnswers = [], onAnswersChange, onComplete, skippedLabel = 'Agent decides', startLabel = 'Starting',
  started = false, startedLabel, bare = true, className
}: DecisionPromptProps) {
  const [answers, setAnswers] = useControllable(value, defaultAnswers, onAnswersChange);
  const [sel, setSel] = useState(0);
  const n = questions.length, at = answers.length, done = at >= n, q = questions[Math.min(at, n - 1)];

  const next = (skipped = false) => {
    if (done) return;
    const all = [...answers, skipped ? null : sel];
    setAnswers(all); setSel(0);
    if (all.length >= n) onComplete?.(all);
  };
  const key = (e: KeyboardEvent) => {
    if (done) return;
    const count = q.options.length;
    if (/^[1-9]$/.test(e.key) && +e.key <= count) { setSel(+e.key - 1); e.preventDefault(); }
    else if (e.key === 'Enter') { next(); e.preventDefault(); }
    else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { setSel((sel + (e.key === 'ArrowDown' ? 1 : count - 1)) % count); e.preventDefault(); }
  };

  return (
    <div data-dm="" className={cx('dm-dpx', !bare && 'dm-panel', className)} tabIndex={0} onKeyDown={key}>
      <div className="dm-dpx-h">
        <span className="dm-dpx-k dm-muted dm-num">{done ? 'All set' : `Question ${at + 1} of ${n}`}</span>
        <div className="dm-dpx-dots" aria-hidden="true">{questions.map((_, i) => <i key={i} className={cx(!done && i === at && 'dm-on', (done || i < at) && 'dm-done')} />)}</div>
      </div>
      {/* keyed by question, so each one plays its entrance */}
      <div className="dm-dpx-body" key={done ? 'sum' : at} aria-live="polite">
        {done ? (
          <div className="dm-dpx-sum">
            {questions.map((x, i) => (
              <div key={i} className={cx('dm-dpx-sr', enter().className)} style={enter(i * 70).style}>
                <span>{x.label}</span>{answers[i] === null ? skippedLabel : x.options[answers[i]!]?.label}
              </div>
            ))}
            <div className={cx('dm-dpx-run', enter().className)} style={enter(n * 70).style}>{started
              ? <><span style={{ color: 'var(--dm-ok)' }}><Icon name="check" size="xs" /></span><span>{startedLabel ?? startLabel}</span></>
              : <><Think /><span className="dm-shim">{startLabel}</span></>}</div>
          </div>
        ) : (
          <>
            <div className="dm-dpx-q dm-enter">{q.question}</div>
            <div role="radiogroup" aria-label={q.question}>
              {q.options.map((o, i) => (
                <button key={i} type="button" role="radio" aria-checked={i === sel} className={cx('dm-dpx-o', i === sel && 'dm-on', enter().className)} style={enter(60 + i * 50).style}
                  onClick={() => setSel(i)} onDoubleClick={() => next()}>
                  <span className="dm-kbd">{i + 1}</span>{o.label}{o.detail && <span className="dm-d">{o.detail}</span>}
                </button>
              ))}
            </div>
          </>
        )}
      </div>
      <div className={cx('dm-dpx-f', done && 'dm-leave')} inert={done}>
        <button type="button" className="dm-pill dm-ghost dm-sm dm-dpx-skip" onClick={() => next(true)}>Skip</button>
        <button type="button" className="dm-pill dm-primary dm-sm dm-dpx-go" onClick={() => next()}>Continue <span className="dm-kbd">↵</span></button>
      </div>
    </div>
  );
}
decision-prompt.cssReact · 27 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Decision prompt ---------- */
/* the agent asks in the chat: no container; with .panel it becomes a card */
.dm-dpx{padding:var(--dm-s-4) 0;display:flex;flex-direction:column;gap:var(--dm-s-3);outline:0;border-radius:var(--dm-r-in)}
.dm-dpx.dm-panel{padding:var(--dm-s-4);border-radius:var(--dm-r-card)}
.dm-dpx:focus-visible{outline:2px solid var(--dm-focus);outline-offset:6px}
.dm-dpx-h{display:flex;align-items:center;justify-content:space-between}
.dm-dpx-k{font-size:12px}
.dm-dpx-dots{display:flex;gap:var(--dm-s-1)}
.dm-dpx-dots i{width:6px;height:6px;border-radius:3px;background:var(--dm-line);transition:width .45s var(--dm-ease),background .35s}
.dm-dpx-dots i.dm-on{width:16px;background:var(--dm-accent-mark,var(--dm-accent))}
.dm-dpx-dots i.dm-done{background:var(--dm-accent-mark,var(--dm-accent));opacity:.4}
.dm-dpx-body{min-height:168px}
.dm-dpx-q{font-size:14px;font-weight:500;margin-bottom:10px;text-wrap:balance}
.dm-dpx-o{display:flex;align-items:center;gap:10px;width:calc(100% + 20px);margin-inline:-10px;padding:9px 10px;border-radius:var(--dm-r-in);font-size:13.5px;text-align:left;color:var(--dm-fg2);transition:background .25s,color .25s}
@media (hover:hover) and (pointer:fine){.dm-dpx-o:hover{background:var(--dm-row);color:var(--dm-fg)}}
.dm-dpx-o.dm-on{background:var(--dm-sel);color:var(--dm-fg)}
.dm-dpx-o .dm-kbd{width:20px;text-align:center;transition:background .25s,color .25s}
.dm-dpx-o.dm-on .dm-kbd{background:var(--dm-accent);color:var(--dm-on-accent)}
.dm-dpx-o .dm-d{margin-left:auto;font-size:12px;color:var(--dm-muted)}
.dm-dpx-f{display:flex;justify-content:flex-end;gap:6px}
.dm-dpx-sum{display:flex;flex-direction:column;gap:6px}
.dm-dpx-sr{display:flex;gap:10px;font-size:13px;padding:7px 0;border-bottom:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-dpx-sr:last-child{border:0}
.dm-dpx-sr span{color:var(--dm-muted);width:92px;flex:none}
.dm-dpx-run{display:flex;align-items:center;gap:var(--dm-s-2);font-size:13px;margin-top:6px}
decision-prompt.htmlHTML · 10 lines
<div class="card bare" data-c="decision-prompt">
  <div class="stage-in">
    <div class="dpx" tabindex="0">
      <div class="dpx-h"><span class="dpx-k muted num">Question 1 of 3</span><div class="dpx-dots"><i class="on"></i><i></i><i></i></div></div>
      <div class="dpx-body"></div>
      <div class="dpx-f"><button class="pill ghost sm dpx-skip">Skip</button><button class="pill primary sm dpx-go">Continue <span class="kbd">↵</span></button></div>
    </div>
  </div>
</div>
decision-prompt.cssHTML · 26 lines
/* ---------- Decision prompt ---------- */
/* the agent asks in the chat: no container; with .panel it becomes a card */
.dpx{padding:var(--s-4) 0;display:flex;flex-direction:column;gap:var(--s-3);outline:0;border-radius:var(--r-in)}
.dpx.panel{padding:var(--s-4);border-radius:var(--r-card)}
.dpx:focus-visible{outline:2px solid var(--focus);outline-offset:6px}
.dpx-h{display:flex;align-items:center;justify-content:space-between}
.dpx-k{font-size:12px}
.dpx-dots{display:flex;gap:var(--s-1)}
.dpx-dots i{width:6px;height:6px;border-radius:3px;background:var(--line);transition:width .45s var(--ease),background .35s}
.dpx-dots i.on{width:16px;background:var(--accent-mark,var(--accent))}
.dpx-dots i.done{background:var(--accent-mark,var(--accent));opacity:.4}
.dpx-body{min-height:168px}
.dpx-q{font-size:14px;font-weight:500;margin-bottom:10px;text-wrap:balance}
.dpx-o{display:flex;align-items:center;gap:10px;width:calc(100% + 20px);margin-inline:-10px;padding:9px 10px;border-radius:var(--r-in);font-size:13.5px;text-align:left;color:var(--fg2);transition:background .25s,color .25s}
@media (hover:hover) and (pointer:fine){.dpx-o:hover{background:var(--row);color:var(--fg)}}
.dpx-o.on{background:var(--sel);color:var(--fg)}
.dpx-o .kbd{width:20px;text-align:center;transition:background .25s,color .25s}
.dpx-o.on .kbd{background:var(--accent);color:var(--on-accent)}
.dpx-o .d{margin-left:auto;font-size:12px;color:var(--muted)}
.dpx-f{display:flex;justify-content:flex-end;gap:6px}
.dpx-sum{display:flex;flex-direction:column;gap:6px}
.dpx-sr{display:flex;gap:10px;font-size:13px;padding:7px 0;border-bottom:1px solid color-mix(in srgb,var(--line) 50%,transparent)}
.dpx-sr:last-child{border:0}
.dpx-sr span{color:var(--muted);width:92px;flex:none}
.dpx-run{display:flex;align-items:center;gap:var(--s-2);font-size:13px;margin-top:6px}
decision-prompt.jsHTML · 30 lines
DM.def('decision-prompt', (r, c) => {
  const Q = [
    ['Which frames should the agent redesign?', 'Frames', [['Pricing only','1 frame'],['Pricing and checkout','3 frames'],['The whole flow','9 frames']]],
    ['How bold can the changes be?', 'Scope', [['Polish spacing and type','Safe'],['Rework the layout','Medium'],['Fresh direction','Bold']]],
    ['Where should the result go?', 'Output', [['New Figma page','Keeps the original'],['Branch in the repo','Opens a PR'],['Both','Recommended']]]
  ];
  const ans = []; let qi = 0, sel = 0, done = false;
  const body = $('.dpx-body', r), k = $('.dpx-k', r), dots = $$('.dpx-dots i', r), go = $('.dpx-go', r), skip = $('.dpx-skip', r), card = $('.dpx', r);
  function paint(){
    k.textContent = 'Question ' + (qi + 1) + ' of 3';
    dots.forEach((d, i) => { d.classList.toggle('on', i === qi); d.classList.toggle('done', i < qi); });
    const q = Q[qi];
    body.innerHTML = `<div class="dpx-q">${q[0]}</div>` + q[2].map((o, i) => `<button class="dpx-o${i === sel ? ' on' : ''}" data-i="${i}"><span class="kbd">${i + 1}</span>${o[0]}<span class="d">${o[1]}</span></button>`).join('');
    enter($('.dpx-q', body)); stagger($$('.dpx-o', body), 50, 60);
    $$('.dpx-o', body).forEach(b => { b.onclick = () => choose(+b.dataset.i); b.ondblclick = next; });
  }
  function choose(i){ sel = i; $$('.dpx-o', body).forEach((b, j) => b.classList.toggle('on', j === i)); }
  function next(skipped){
    if (done) return; ans.push(skipped === true ? 'Agent decides' : Q[qi][2][sel][0]); qi++; sel = 0;
    if (qi < Q.length) return paint();
    done = true; dots.forEach(d => { d.classList.remove('on'); d.classList.add('done'); }); k.textContent = 'All set';
    go.parentElement.classList.add('leave');
    body.innerHTML = `<div class="dpx-sum">${Q.map((q, i) => `<div class="dpx-sr"><span>${q[1]}</span>${ans[i]}</div>`).join('')}<div class="dpx-run"><span class="think on"><i></i><i></i><i></i><i></i></span><span class="shim">Starting the redesign</span></div></div>`;
    stagger($$('.dpx-sr, .dpx-run', body), 70);
  }
  go.onclick = () => next(); skip.onclick = () => next(true);
  c.on(card, 'keydown', e => { if (done) return; if (/^[1-3]$/.test(e.key)){ choose(+e.key - 1); e.preventDefault(); } else if (e.key === 'Enter'){ next(); e.preventDefault(); } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){ choose((sel + (e.key === 'ArrowDown' ? 1 : 2)) % 3); e.preventDefault(); } });
  paint();
});
06

Questions

What people ask about it.

How do I get the answers to my agent?
Use onComplete. It receives an array with the chosen option's index for each question, or null where the person skipped.
Does the keyboard work right away?
The whole prompt is focusable, so the keys work once it has focus, for example after clicking an option. Number keys pick, arrows move and Enter continues.
How do I show that the agent has actually started?
Set started to true. The shimmering startLabel line turns into a check with startedLabel, or the same text if you leave that out.
07

More in Review and approve

Moments where a person checks, steers or signs off on the agent's work.

Change reviewAccept or reject each change the agent made, file by file.
Plan reviewCheck the agent's plan, edit steps, then let it run.
Permission gateThe agent asks before it touches something new.
Hand-off to a personThe agent hands a conversation to a person, with what they need to pick it up.
Proposal cardA suggested change with confidence, alternatives and undo.
Design tunerTweak a component by hand or let the agent tune it.
Rewrite toolbarSelect a sentence and let the agent rewrite it in place.
Launch checklistThe agent checks a design before it ships, and fixes what fails.
All 14 in Review and approve →