Problem Synthesis Phase

A synthesis card. The caller supplies a prompt and an ordered list of components — each one a slot the student must articulate (the invariant, the recurrence, the base case). The student types into each slot, hits Synthesise, and the canonical phrasing reveals beneath each input. The card emits onSynthesize(values, blankIds) so the parent can score, advance phases, or persist the synthesis text.

Preview
Synthesise insertion sort. Articulate the three components a correctness argument needs.

What never changes across iterations?

Customize
Behaviour

Installation

npx shadcn@latest add https://craftbits.dev/r/problem-synthesis-phase.json

Usage

import { ProblemSynthesisPhase, type ProblemSynthesisComponent } from "@craft-bits/core";
 
const COMPONENTS: ReadonlyArray<ProblemSynthesisComponent> = [
  {
    id: "invariant",
    label: "Loop invariant",
    placeholder: "Describe the property that stays true on every iteration…",
    answer: "Every element in the prefix arr[0..i] is in its final sorted position.",
  },
  {
    id: "recurrence",
    label: "Recurrence",
    answer: "sorted(i) = insert(arr[i]) into sorted(i − 1).",
    multiline: true,
  },
  {
    id: "base",
    label: "Base case",
    answer: "A one-element prefix is trivially sorted.",
  },
];
 
<ProblemSynthesisPhase
  prompt="Synthesise insertion sort. Articulate the three components a correctness argument needs."
  components={COMPONENTS}
  onSynthesize={(values, blankIds) => {
    console.log("captured", values, "blanks", blankIds);
  }}
/>

Controlled — lift both the value map and the reveal flag so you can score, persist, or advance phases:

const [values, setValues] = useState<Record<string, string>>({});
const [revealed, setRevealed] = useState(false);
 
<ProblemSynthesisPhase
  prompt="Articulate the three components."
  components={COMPONENTS}
  values={values}
  onValuesChange={setValues}
  revealed={revealed}
  onSynthesize={(captured) => {
    setRevealed(true);
    persistSynthesis(captured);
  }}
  requireAll
/>

Read-only review — render the canonical answers without letting the student re-edit:

<ProblemSynthesisPhase
  prompt="Synthesis (review)"
  components={COMPONENTS}
  values={savedValues}
  revealed
  tone="success"
/>

Understanding the component

  1. Edit, then reveal. Each component renders as an editable slot. Hitting Synthesise locks the inputs and fades in the canonical answer beneath each one — coloured with the chosen tone ramp.
  2. Controlled + uncontrolled, twice. Both the value map (values + onValuesChange + defaultValues) and the reveal flag (revealed) follow the Radix pattern. Either may be omitted; the component keeps internal state for whichever you skip.
  3. Submit gating. requireAll keeps the submit button disabled until every slot has a non-empty value. With it off, the student can submit a partial synthesis and the second onSynthesize argument lists the ids they left blank.
  4. Multiline slots. Set multiline: true on any component to render a textarea instead of an input. Useful for recurrences, derivations, or anything longer than a phrase.
  5. Summary banner. Provide a summary ReactNode to land the lesson — restate the takeaway, link forward, or surface a code bridge. It fades in alongside the reveals on a success-tone banner.
  6. Reduced motion. Reveal banners, submit press feedback, and summary entrance collapse to instant under prefers-reduced-motion: reduce. The locking + reveal logic still runs.

Props

PropTypeDefaultDescription
promptReactNoderequiredSynthesis prompt rendered at the top of the card.
componentsReadonlyArray<ProblemSynthesisComponent>requiredOrdered list of slots the student must articulate.
valuesRecord<string, string>Controlled value map keyed by component id.
defaultValuesRecord<string, string>{}Uncontrolled initial values.
onValuesChange(next) => voidFires on every keystroke with the next value map.
onSynthesize(values, blankIds) => voidFires on submission with the captured values + ids left blank.
revealedbooleanControlled reveal flag. When true, slots lock and answers show.
submitLabelReactNode"Synthesise"Label for the primary submit button.
requireAllbooleanfalseKeep submit disabled until every slot has a value.
tone"default" | "accent" | "success" | "warning" | "error""accent"Tone for the reveal banner accent.
headerReactNodeContent rendered above the prompt.
summaryReactNodeReveal-time summary rendered below the slots.
transitionTransitionSPRINGS.smoothOverride reveal transitions. Reduced-motion users snap regardless.
classNamestringMerged onto the root via cn().

ProblemSynthesisComponent

FieldTypeDescription
idstringStable identifier — keyed in the values map.
labelReactNodeSlot label rendered above the input.
answerReactNodeCanonical phrasing revealed after submission.
placeholderstringPlaceholder text for the empty input.
helperTextReactNodeOptional muted helper line under the input.
multilinebooleanRender as textarea instead of input. Default false.

Accessibility

  • The slot list is wrapped in role="group" and labelled by the prompt id so screen readers navigate the slots as a single synthesis unit.
  • Every slot input is connected to its label via htmlFor + id, exposes a focus-visible accent ring, and enforces a 44 px minimum hit area regardless of label length.
  • Reveal banners use role="status" + aria-live="polite" so each canonical phrasing announces without stealing focus, and the same applies to the summary banner.
  • Tone is never the only signal — the canonical answer renders on a tinted background with a coloured left border so colour-blind users still perceive the reveal.
  • The submit button exposes aria-disabled whenever the requireAll gate is unmet, so assistive tech surfaces the unmet precondition.
  • Motion respects prefers-reduced-motion: reduce — reveal banner enter/exit and submit tap scale collapse to instant.

Credits

  • Extracted from: AlgoFlashcards (src/lessons/primitives/interaction/ProblemSynthesisPhase.tsx). The source bound the phase to useMultiQuiz, a LessonContext usePhaseGate, playSound, a trackHex confidence meter, project RichText, and a multi-question MCQ loop. The library extract recasts the phase as a construction interaction (editable slot per synthesis component, with a multiline toggle) instead of a recognition quiz, lifts both the value map and the reveal flag into Radix-style controlled / uncontrolled APIs, routes the per-track accent through the --cb-* token ramp, swaps the bespoke spring + confidence meter for SPRINGS.smooth + a single success-tone summary banner, and surfaces onSynthesize(values, blankIds) so consumers can score, persist, or advance phases on top of it.