> ## Documentation Index
> Fetch the complete documentation index at: https://docs.edpire.com/llms.txt
> Use this file to discover all available pages before exploring further.

# React Components

> EdpireAssessmentPlayer, AssessmentShell, and EdpireQuestion — the React components in @edpire/sdk/react.

The `@edpire/sdk/react` entry exports three React components:

| Component                    | Use when                                                                                            |
| ---------------------------- | --------------------------------------------------------------------------------------------------- |
| **`EdpireAssessmentPlayer`** | You want the simplest integration — handles token fetch, mount, and cleanup. **Start here.**        |
| **`AssessmentShell`**        | You fetch the assessment yourself and want full control over data flow + submission.                |
| **`EdpireQuestion`**         | You're building a [Custom Flow](/developer/sdk/custom-flow) (question-by-question, Duolingo-style). |

```tsx theme={null}
import { EdpireAssessmentPlayer, AssessmentShell, EdpireQuestion } from "@edpire/sdk/react"
```

<Info>
  All components require React 19. CSS is bundled and injected automatically — no stylesheet imports needed.
</Info>

***

## `EdpireAssessmentPlayer`

The recommended way to embed a player in React. Wraps `EdpireAssessment.mount()` and handles token fetching, mounting, unmounting, and StrictMode double-invoke cleanup — the boilerplate you'd otherwise write with `useRef` + `useEffect`.

```tsx theme={null}
import { EdpireAssessmentPlayer } from "@edpire/sdk/react"

function LessonPage({ assessmentId }: { assessmentId: string }) {
  return (
    <EdpireAssessmentPlayer
      tokenEndpoint="/api/edpire/token"
      assessmentId={assessmentId}
      onComplete={(r) => console.log("Score:", r.score, "/", r.max_score)}
      onError={(e) => console.error(e.code, e.message)}
      style={{ width: "100%", height: "100vh" }}
    />
  )
}
```

Pair this with `createEdpireTokenHandler()` on the server — see [Embedded Player → Quickstart](/developer/sdk/embedded-player#quickstart-react-recommended).

### `EdpireAssessmentPlayerProps`

| Prop            | Type                                   | Required | Description                                                                                                 |
| --------------- | -------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `tokenEndpoint` | `string`                               | **Yes**  | URL of your server endpoint that returns `{ token }`. The component POSTs `{ assessmentId }` here.          |
| `assessmentId`  | `string`                               | **Yes**  | Assessment UUID to embed. Sent to `tokenEndpoint` in the POST body.                                         |
| `tokenBody`     | `Record<string, unknown>`              | No       | Extra fields merged into the POST body (e.g. course ID for server-side validation).                         |
| `onComplete`    | `(result: EmbedResult) => void`        | No       | Called on successful submission.                                                                            |
| `onError`       | `(error: EmbedError) => void`          | No       | Called on non-recoverable errors (token failure, origin blocked, etc.).                                     |
| `locale`        | `"en" \| "fr" \| "ar"`                 | No       | UI language. RTL applied automatically for `"ar"`.                                                          |
| `branding`      | `TakeBranding`                         | No       | Override org branding (white-labelling).                                                                    |
| `returnUrl`     | `string`                               | No       | Post-submit redirect URL with score params appended.                                                        |
| `reportLabel`   | `string`                               | No       | Custom label for the report button.                                                                         |
| `onBack`        | `() => void`                           | No       | Back button handler (shown before submission).                                                              |
| `backLabel`     | `string`                               | No       | Custom back button label.                                                                                   |
| `overlayConfig` | `OverlayConfig`                        | No       | Customise the grading overlay.                                                                              |
| `mediaHandler`  | `QuestionRuntimeProps["mediaHandler"]` | No       | **Required** for file-upload/recording OpenResponse questions.                                              |
| `baseUrl`       | `string`                               | No       | Edpire instance URL. Defaults to `https://edpire.com`.                                                      |
| `className`     | `string`                               | No       | CSS class for the container `<div>`. Use this to size the player (e.g. `"h-screen"`).                       |
| `style`         | `React.CSSProperties`                  | No       | Inline style for the container. Defaults to `{ width: "100%", height: "100%" }` when `className` is absent. |

The player fills its container — you control the size via `className` or `style`.

***

<Info>
  These are React 19 components. CSS is bundled and injected automatically — no stylesheet imports needed.
</Info>

***

## `AssessmentShell`

The complete assessment-taking UI: sticky top bar with logo + countdown, exercise sections with score badges, submit confirmation, grading overlay, post-submit score banner, and per-question feedback. You supply the assessment data and an `onSubmit` handler that does the grading.

```tsx theme={null}
import { AssessmentShell } from "@edpire/sdk/react"
import type { SubmitResult } from "@edpire/sdk/react"

function Player({ assessment, meta }) {
  async function handleSubmit(answers): Promise<SubmitResult> {
    const res = await fetch("/api/grade", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ answers }),
    })
    if (!res.ok) throw new Error("Grading failed")
    return res.json()
  }

  return (
    <AssessmentShell
      assessment={assessment}
      meta={{ title: "Unit 3 Quiz", category: "Grammar" }}
      locale="en"
      onSubmit={handleSubmit}
    />
  )
}
```

**What your `/api/grade` server endpoint must return.** `client.submit()` returns `GradeResult` (snake\_case); `AssessmentShell` expects `SubmitResult` (camelCase). Map between them on the server:

```typescript theme={null}
// POST /api/grade/route.ts
import { EdpireClient } from "@edpire/sdk/client"

const client = new EdpireClient({ apiKey: process.env.EDPIRE_API_KEY! })

export async function POST(req: Request) {
  const { assessmentId, answers } = await req.json()
  const result = await client.submit(assessmentId, {
    learner_ref: session.userId,
    answers,
  })

  // Map GradeResult (snake_case) → SubmitResult (camelCase)
  return Response.json({
    submissionId: result.submission_id,
    totalScore: result.score,
    maxScore: result.max_score,
    percentage: result.percentage,
    passed: result.passed,
    exerciseFeedback: [],   // GradeResult doesn't include per-node feedback;
                            // the score banner shows, but inline corrections won't.
    awaitingManualGrading: false,
  })
}
```

<Note>
  `client.submit()` returns per-question scores (`exercise_results`) but not the per-node visual feedback (`exerciseFeedback`) that `AssessmentShell` uses to highlight incorrect answers inline. If you need both, use the [Embedded Player](/developer/sdk/embedded-player) instead — it gets the full response from Edpire automatically.
</Note>

<Note>
  **When to use `AssessmentShell` vs `EdpireAssessmentPlayer`**: `EdpireAssessmentPlayer` fetches assessment data client-side after mount and handles everything for you — start there. Drop down to `AssessmentShell` only when you need to control data loading yourself: server-rendering the assessment for a faster initial paint, pre-fetching it before the learner navigates, or plugging into your own caching layer. `AssessmentShell` expects data in the runtime shape (`AssessmentContent`), not the raw REST `Assessment` — you're responsible for that conversion.
</Note>

### `AssessmentShellProps`

| Prop               | Type                                          | Required | Description                                                                                                                                                                                              |
| ------------------ | --------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `assessment`       | `AssessmentContent`                           | **Yes**  | The assessment content (runtime shape) to render.                                                                                                                                                        |
| `meta`             | `{ title: string; category: string \| null }` | **Yes**  | Title + category shown in the top bar.                                                                                                                                                                   |
| `onSubmit`         | `(answers) => Promise<SubmitResult>`          | **Yes**  | Grades the attempt. Should **throw** on failure (the shell shows an error banner).                                                                                                                       |
| `locale`           | `"en" \| "fr" \| "ar"`                        | No       | Shell string language. Default `"en"`. RTL auto-applied for `"ar"`.                                                                                                                                      |
| `branding`         | `TakeBranding`                                | No       | Logo, colors, fonts. See [Branding](/developer/sdk/branding-theming).                                                                                                                                    |
| `timeLimitMinutes` | `number \| null`                              | No       | Shows a countdown chip; auto-submits at `0:00`.                                                                                                                                                          |
| `startedAt`        | `string` (ISO)                                | No       | ISO timestamp when the attempt started. If provided, the countdown picks up from where it left off after a page refresh instead of restarting from the full limit.                                       |
| `onViewReport`     | `(result: SubmitResult) => void`              | No       | Called when the learner clicks "View Full Report". Omit to hide the button.                                                                                                                              |
| `reportLabel`      | `string`                                      | No       | Custom label for the report button.                                                                                                                                                                      |
| `onBack`           | `() => void`                                  | No       | Called when the learner clicks the Back button in the top bar (visible only before submission). Use it to navigate back in your app or close a modal.                                                    |
| `backLabel`        | `string`                                      | No       | Custom back-button label (default: localized "Back").                                                                                                                                                    |
| `allowReset`       | `boolean`                                     | No       | Show a Reset button in the top bar.                                                                                                                                                                      |
| `onReset`          | `() => void`                                  | No       | Reset handler.                                                                                                                                                                                           |
| `onClose`          | `() => void`                                  | No       | Show a close (✕) button and handle it.                                                                                                                                                                   |
| `overlayConfig`    | `OverlayConfig`                               | No       | Customise the grading overlay. See [Branding](/developer/sdk/branding-theming#grading-overlay).                                                                                                          |
| `contained`        | `boolean`                                     | No       | Scope the grading and time-up overlays to the shell's bounding box instead of the full viewport. Use when the player is embedded in a page alongside other content, rather than filling the full screen. |
| `mediaHandler`     | `QuestionRuntimeProps["mediaHandler"]`        | No       | **Required** for file-upload/recording OpenResponse questions. See [mediaHandler example](/developer/sdk/embedded-player#mediahandler).                                                                  |
| `fallbackLogoUrl`  | `string`                                      | No       | Logo shown when `branding.logoUrl` is absent. Defaults to an inline Edpire SVG.                                                                                                                          |

### `SubmitResult` (returned by `onSubmit`)

```typescript theme={null}
interface SubmitResult {
  submissionId: string
  totalScore: number
  maxScore: number
  percentage: number
  passed: boolean
  /** If null/undefined, the "View Full Report" button is hidden. */
  returnUrl?: string | null
  exerciseFeedback: ExerciseFeedbackData[]
  /** When true, score banners hide the score (still under review). */
  awaitingManualGrading?: boolean
}
```

`exerciseFeedback` drives the inline per-question feedback. See the [type catalog](/developer/sdk/types#feedback-types) for `ExerciseFeedbackData`, `QuestionFeedback`, and `NodeFeedback`.

***

## `EdpireQuestion`

Renders **one question** and reports answer changes. The core building block for [Custom Flow](/developer/sdk/custom-flow).

```tsx theme={null}
import { EdpireQuestion } from "@edpire/sdk/react"
import type { RuntimeAnswer } from "@edpire/sdk/react"

function QuestionStep({ step }) {
  const [answers, setAnswers] = useState<RuntimeAnswer[]>([])
  const [feedback, setFeedback] = useState<Record<string, unknown> | null>(null)

  return (
    <EdpireQuestion
      content={step.content}        // ContentAST from flattenAssessment()
      onAnswersChange={setAnswers}
      feedback={feedback}           // null = no feedback; pass /check result to show it
      dir="ltr"                     // "rtl" for Arabic
    />
  )
}
```

### `EdpireQuestionProps`

| Prop              | Type                                 | Required | Description                                                                                                                           |
| ----------------- | ------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `content`         | `unknown`                            | **Yes**  | The question's `content_ast` (a `FlatStep.content`).                                                                                  |
| `onAnswersChange` | `(answers: RuntimeAnswer[]) => void` | No       | Fires on every answer change. Collect these for `/check` and submit.                                                                  |
| `feedback`        | `Record<string, unknown> \| null`    | No       | Pass the `feedback` field from `POST /check` **directly** — the component converts it internally. `null`/`undefined` clears feedback. |
| `initialAnswers`  | `RuntimeAnswer[]`                    | No       | Pre-fill answers (e.g. restoring state when navigating back).                                                                         |
| `className`       | `string`                             | No       | Class added to the question root `<div>`.                                                                                             |
| `dir`             | `"ltr" \| "rtl"`                     | No       | Text direction. Default `"ltr"`.                                                                                                      |

<Tip>
  `feedback` takes the **raw JSON** from the `/check` endpoint — no manual mapping. Internally the component converts it to the `Map` the runtime expects.
</Tip>

***

## `GradingOverlay`

The branded spinner overlay shown while grading. You don't usually render it directly — `AssessmentShell` and the Embedded Player show it for you and accept an `overlayConfig`. The `OverlayConfig` / `SpinnerTheme` types and theming options are documented under [Branding → Grading overlay](/developer/sdk/branding-theming#grading-overlay).

***

## Working examples

See the starter templates for complete, runnable integrations:

```bash theme={null}
npx --package=@edpire/sdk create-edpire-app nextjs my-app       # EdpireAssessmentPlayer + token handler
npx --package=@edpire/sdk create-edpire-app vite-express my-app  # same, with Vite + Express
```

For a Custom Flow (question-by-question), see the [Custom Flow guide](/developer/sdk/custom-flow).
