@aime.ai/renderer-react
v1.6.0
Published
React component library for rendering AIME lesson packs with BlockSuite whiteboard support
Maintainers
Readme
@aime.ai/renderer-react
A composable React component library for rendering AIME lesson packs — structured educational content with slide-based presentation, a BlockSuite whiteboard, speaker notes, and full RTL / i18n support.
Table of Contents
- Installation
- Quick Start
- Core Concepts
- Editing Feature
- Components
- Contexts & Hooks
- i18n / Labels
- Canvas Persistence
- Types Reference
- Peer Dependencies
Installation
pnpm add @aime.ai/renderer-react
# or
npm install @aime.ai/renderer-reactImport the stylesheet once at your app root:
import "@aime.ai/renderer-react/style.css";Quick Start
import "@aime.ai/renderer-react/style.css";
import {
LessonProvider,
LessonWindow,
SlideNavigation,
PresentButton,
ExportButton,
CompleteLessonButton,
SlideSidebar,
SlideViewer,
FloatingNavigation,
NotesPanel,
BlockSuiteCanvas,
} from "@aime.ai/renderer-react";
import type { LessonPackOutput } from "@aime.ai/renderer-react";
function LessonPage({ pack }: { pack: LessonPackOutput }) {
return (
<LessonProvider pack={pack}>
<SlideNavigation>
<PresentButton />
<ExportButton onExport={() => downloadPdf(pack)} />
<CompleteLessonButton onComplete={() => router.back()} />
</SlideNavigation>
<LessonWindow>
<SlideSidebar onBack={() => router.back()} />
<SlideViewer />
<BlockSuiteCanvas />
</LessonWindow>
<FloatingNavigation />
<NotesPanel />
</LessonProvider>
);
}Core Concepts
Composability — Every component is optional. Omit BlockSuiteCanvas if you don't need whiteboard annotations. Omit NotesPanel for a minimal viewer. Compose only what you need.
Shared state via context — LessonProvider wraps two React contexts:
LessonPackContext— holds the currentLessonPackOutput, the active slide index, and setters.PresentationContext— holds UI state: presentation mode, sidebar collapse, whiteboard mode, zoom level, text direction, and i18n labels.
All built-in components read from and write to these contexts automatically.
Editing Feature
The renderer ships a full inline-editing system. When editing is enabled, every slide component swaps its static display for live-editable primitives — teachers or content creators can update text, markdown content, lists, and images without leaving the lesson view.
Enabling Edit Mode
Pass editable and editCallbacks to LessonProvider:
import {
LessonProvider,
LessonWindow,
SlideNavigation,
EditButton,
EditView,
SlideViewer,
} from "@aime.ai/renderer-react";
import type { EditCallbacks } from "@aime.ai/renderer-react";
const editCallbacks: EditCallbacks = {
onSave: (updatedPack) => {
// Persist to your API whenever any field changes
api.patch(`/lesson-packs/${updatedPack.id}`, updatedPack);
},
onSelectImage: (resolve) => {
// Open your own image picker, then call resolve() with the URL
openMyImagePicker().then((url) => resolve(url));
},
onReorderSlides: (slides) => console.log("Slides reordered", slides),
onDeleteSlide: (index) => console.log("Slide deleted at", index),
};
function LessonEditor({ pack }) {
return (
<LessonProvider pack={pack} editCallbacks={editCallbacks}>
<SlideNavigation>
<EditButton />
</SlideNavigation>
<LessonWindow>
<EditView /> {/* Full-page editor — only renders when editable */}
<SlideViewer /> {/* Presentation viewer — hides when EditView is active */}
</LessonWindow>
</LessonProvider>
);
}Note:
editabledefaults tofalse. It can also be toggled at runtime viauseLessonPack().toggleEditable()or the built-inEditButtoncomponent.
EditView
A full-page slide management view that replaces the SlideViewer when editing is active. It renders null when editable is false, so it is safe to always include in the tree.
Features:
- Horizontal thumbnail grid — every slide is rendered as a live mini-preview.
- Drag-to-reorder — grab the grip handle on any card and drag it to a new position. Slide numbers are re-assigned automatically and
onReorderSlides+onSaveare called. - Delete — the trash icon on each card removes the slide;
onDeleteSlide+onSaveare called. - Inline editor — clicking a card opens a single-slide editing view where the active slide's primitive fields become editable. A breadcrumb header and Prev / Next arrows allow navigation between slides without returning to the grid.
<EditView />No props — reads all state from LessonPackContext.
EditButton
A toggle button that switches editable on and off via useLessonPack().toggleEditable(). Place it inside SlideNavigation.
<SlideNavigation>
<EditButton />
<PresentButton />
</SlideNavigation>No props.
EditCallbacks
All callbacks are optional. Pass only the ones you need.
interface EditCallbacks {
/**
* Called (debounced) whenever any slide field changes or slides are
* reordered / deleted. Receives the full updated pack.
*/
onSave?: (pack: LessonPackOutput) => void;
/**
* Invoked when the user clicks an image placeholder or "Change image".
* Implement your own file picker and call `resolve(url)` with the chosen
* URL, or `resolve(null)` to cancel.
*/
onSelectImage?: (resolve: (url: string | null) => void) => void;
/**
* Called after a drag-to-reorder operation completes.
* `slides` is the full new ordered array with updated `slide_number` values.
*/
onReorderSlides?: (slides: AnySlide[]) => void;
/**
* Called after a slide is deleted.
* `slideIndex` is the zero-based position of the deleted slide.
*/
onDeleteSlide?: (slideIndex: number) => void;
}useLessonPack — editing API
The following editing-specific members are available on useLessonPack():
const {
editable, // boolean — whether edit mode is active
toggleEditable, // () => void — toggle edit mode on/off
editCallbacks, // EditCallbacks — the callbacks passed to LessonProvider
updateSlideField, // <K extends keyof AnySlide>(key: K, value: AnySlide[K]) => void
} = useLessonPack();updateSlideField patches a single field on the currently active slide, merges it into the pack, and fires editCallbacks.onSave automatically.
// Update the title of the current slide
updateSlideField("title", "New Title");
// Update a slide-type-specific field (cast required for non-BaseSlide keys)
updateSlideField("content" as any, "# Updated content");Primitive editing components
All primitive components read editable from context automatically — they render their non-interactive version when editable is false, so you can render them unconditionally.
EditableText
Inline plain-text editor. Renders as a contentEditable element. Commits on blur or Enter; reverts on Escape.
import { EditableText } from "@aime.ai/renderer-react";
<EditableText
value={slide.title}
onChange={(v) => updateSlideField("title", v)}
className="font-black text-3xl text-zinc-900"
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | — | Current text value. |
| onChange | (next: string) => void | — | Called on commit. |
| className | string | "" | Applied to the element. |
| as | "span" \| "p" \| "h1" \| "h2" \| "h3" \| "div" | "span" | Rendered tag. |
| placeholder | string | "Click to edit…" | Shown when empty. |
| multiline | boolean | false | Allow Shift+Enter for new lines. |
EditableMarkdown
Displays rendered markdown. In edit mode, clicking opens a raw-markdown <textarea> that auto-resizes. Commits on blur; reverts on Escape.
import { EditableMarkdown } from "@aime.ai/renderer-react";
<EditableMarkdown
value={slide.content}
onChange={(v) => updateSlideField("content" as any, v)}
className="text-zinc-800 text-base leading-relaxed"
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | — | Raw markdown string. |
| onChange | (next: string) => void | — | Called on commit. |
| className | string | "" | Applied to both rendered and textarea views. |
EditableList
Edits a string[] array. Renders each item with an EditableText and shows Add / Remove controls. Supports a custom renderWrapper for per-item card styling.
import { EditableList } from "@aime.ai/renderer-react";
<EditableList
items={slide.objectives}
onChange={(v) => updateSlideField("objectives" as any, v)}
placeholder="Add an objective…"
renderWrapper={(child, i) => (
<div key={i} className="flex gap-3 p-4 bg-white border rounded-xl">
<span className="font-black text-blue-200 text-2xl">{i + 1}</span>
<div className="flex-1">{child}</div>
</div>
)}
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| items | string[] | — | Current array of strings. |
| onChange | (items: string[]) => void | — | Called with the entire updated array. |
| itemClassName | string | "" | className passed to each item's EditableText. |
| as | "span" \| "p" \| "div" | "span" | Tag for each item text. |
| placeholder | string | "Click to edit…" | Placeholder for empty items. |
| renderWrapper | (child: ReactNode, index: number) => ReactNode | — | Optional custom wrapper per item (e.g. icon + number badge). |
EditableImage
Displays an image with a hover overlay to change it in edit mode. When src is null in edit mode, renders an "Add image" placeholder button. Calls editCallbacks.onSelectImage to let the host app provide an image URL.
import { EditableImage } from "@aime.ai/renderer-react";
<EditableImage
src={slide.image_url}
alt="Slide image"
className="w-full h-48 rounded-2xl object-cover"
onChanged={(url) => updateSlideField("image_url", url)}
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| src | string \| null | — | Image URL. null shows the Add placeholder in edit mode. |
| alt | string | "" | Alt text. |
| className | string | "" | Applied to the wrapper. |
| onChanged | (url: string \| null) => void | — | Called with the new URL after the user picks an image. |
EditableImagerequireseditCallbacks.onSelectImageto be set onLessonProvider, otherwise the click does nothing.
Components
LessonProvider
Root provider and layout shell. Must wrap all other components from this library.
<LessonProvider
pack={lessonPack} // LessonPackOutput — pre-loads data into context
dir="rtl" // "ltr" | "rtl" — text direction (default: "ltr")
labels={myLabels} // LessonLabels — override button/label strings (i18n)
>
{/* your composed UI */}
</LessonProvider>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| pack | LessonPackOutput | — | Lesson data to render. Can be omitted and loaded later via useLessonPack().setCurrentPack. |
| dir | "ltr" \| "rtl" | "ltr" | Document text direction. Applied to the root <div>. |
| labels | LessonLabels | English defaults | UI string overrides for i18n. |
LessonWindow
Flex row container for the main content area (sidebar + viewer). Place inside LessonProvider, after any top navigation.
<LessonWindow>
<SlideSidebar onBack={() => router.back()} />
<SlideViewer />
<BlockSuiteCanvas />
</LessonWindow>SlideNavigation
Top navigation bar. Accepts any combination of action buttons as children.
<SlideNavigation>
<PresentButton />
<ExportButton onExport={handleExport} />
<CompleteLessonButton onComplete={handleComplete} />
</SlideNavigation>PresentButton / ExitButton
Toggles presentation (full-screen) mode. Renders as Present when inactive and Exit when in presentation mode — labels are i18n-aware.
<PresentButton />ExportButton
Calls a user-supplied handler when clicked. Label is i18n-aware.
<ExportButton onExport={() => generateAndDownloadPdf(pack)} />| Prop | Type | Description |
|------|------|-------------|
| onExport | () => void | Required. Called when the button is pressed. |
CompleteLessonButton
Signals lesson completion. Label is i18n-aware.
<CompleteLessonButton onComplete={() => router.push("/dashboard")} />| Prop | Type | Description |
|------|------|-------------|
| onComplete | () => void | Required. Called when the button is pressed. |
SlideSidebar
Collapsible slide-strip sidebar. Shows a SlideThumbnail for every slide in the pack. Collapses automatically in presentation mode.
<SlideSidebar onBack={() => router.back()} />| Prop | Type | Description |
|------|------|-------------|
| onBack | () => void | Required. Called when the Back button is pressed. |
SlideViewer
Renders the currently active slide, scaled to fit the available space. Handles zoom via PresentationContext.
<SlideViewer />No props — reads active slide and zoom from context.
SlideContainer
Lower-level wrapper used by SlideViewer. Use this if you need to render an individual slide without the full viewer infrastructure.
<SlideContainer slide={mySlide} scale={1} />| Prop | Type | Description |
|------|------|-------------|
| slide | AnySlide | The slide data to render. |
| scale | number | Transform scale applied to the 1280 × 720 fixed canvas. |
SlideThumbnail
Renders a single slide at thumbnail size. Used inside SlideSidebar; also useful for custom slide pickers.
<SlideThumbnail slide={slide} index={0} isActive={currentIndex === 0} />| Prop | Type | Description |
|------|------|-------------|
| slide | AnySlide | Slide to render. |
| index | number | Zero-based position in the pack. |
| isActive | boolean | Whether this thumbnail is the active/selected slide. |
FloatingNavigation
Previous / Next arrow buttons that float over the viewer. Only visible in presentation mode.
<FloatingNavigation />No props.
NotesPanel
Slide-out panel showing teacher_notes for the current slide. Toggled via PresentationContext.toggleNotesPanel().
<NotesPanel />No props.
BlockSuiteCanvas
An edgeless BlockSuite whiteboard that overlays the slide content. Activates when isWhiteboardMode is true in PresentationContext.
<BlockSuiteCanvas
initialData={savedData}
onSave={(data) => persistCanvas(data)}
/>| Prop | Type | Description |
|------|------|-------------|
| initialData | Uint8Array | Optional. Restores a previously saved Yjs snapshot. When provided, user annotations are re-applied on top of the slides. |
| onSave | (data: Uint8Array) => void | Optional. Called (debounced 500 ms) whenever the canvas changes. Serialize to your backend or localStorage. Convert to number[] for JSON storage. |
See Canvas Persistence for a full example.
Contexts & Hooks
useLessonPack
Access and mutate the lesson pack state from any descendant of LessonProvider.
import { useLessonPack } from "@aime.ai/renderer-react";
function MyComponent() {
const {
currentPack, // LessonPackOutput | null
setCurrentPack, // (pack: LessonPackOutput | null) => void
currentSlideIndex, // number
setCurrentSlideIndex, // (index: number) => void
currentSlide, // AnySlide | null
} = useLessonPack();
}Use useLessonPackOptional() if the component might render outside a provider — returns null instead of throwing.
usePresentation
Access and mutate presentation UI state.
import { usePresentation } from "@aime.ai/renderer-react";
function MyComponent() {
const {
isPresentationMode, // boolean
isSidebarCollapsed, // boolean
isNotesPanelVisible, // boolean
isWhiteboardMode, // boolean
zoom, // number (50–200, default 90)
dir, // "ltr" | "rtl"
labels, // Required<LessonLabels>
// Setters
setIsPresentationMode,
setIsSidebarCollapsed,
setIsNotesPanelVisible,
setIsWhiteboardMode,
setZoom,
setDir,
// Convenience toggles
togglePresentationMode,
toggleSidebar,
toggleNotesPanel,
toggleWhiteboardMode,
zoomIn, // +10, max 200
zoomOut, // -10, min 50
resetZoom, // back to 100
} = usePresentation();
}Use usePresentationOptional() if the component might render outside a provider — returns undefined instead of throwing.
i18n / Labels
Override any built-in button label by passing a labels prop to LessonProvider. Only specify the strings you want to change — others fall back to English defaults.
<LessonProvider
pack={pack}
dir="rtl"
labels={{
present: "عرض",
exit: "خروج",
exportLesson: "تصدير الدرس",
completeLesson: "إتمام الدرس",
back: "رجوع",
}}
>
{/* ... */}
</LessonProvider>LessonLabels interface
interface LessonLabels {
present?: string; // default: "Present"
exit?: string; // default: "Exit"
exportLesson?: string; // default: "Export Lesson"
completeLesson?: string; // default: "Complete Lesson"
back?: string; // default: "Back"
}Canvas Persistence
BlockSuiteCanvas exposes a raw Yjs binary API so you can store annotations wherever you like.
import { useState, useCallback } from "react";
import {
LessonProvider,
LessonWindow,
SlideViewer,
BlockSuiteCanvas,
} from "@aime.ai/renderer-react";
import type { LessonPackOutput } from "@aime.ai/renderer-react";
function LessonPage({ pack }: { pack: LessonPackOutput }) {
// Restore from JSON field on the lesson pack
const initialData = pack.canvasData
? new Uint8Array(pack.canvasData)
: undefined;
const handleSave = useCallback(
(data: Uint8Array) => {
// Convert to number[] for JSON serialisation, then PATCH your API
saveLessonCanvas(pack.id, Array.from(data));
},
[pack.id],
);
return (
<LessonProvider pack={pack}>
<LessonWindow>
<SlideViewer />
<BlockSuiteCanvas initialData={initialData} onSave={handleSave} />
</LessonWindow>
</LessonProvider>
);
}LessonPackOutput includes a canvasData?: number[] field as a convenience slot — populate it from your API response and the component handles the rest.
If you need to serialize a doc manually (e.g. for an export flow), use the serializeDoc utility:
import { createSlideDoc, serializeDoc } from "@aime.ai/renderer-react";
const doc = createSlideDoc(pack.slides);
const bytes = serializeDoc(doc); // Uint8ArrayTypes Reference
LessonPackOutput
interface LessonPackOutput {
id: string;
lesson_intent_id: string;
version: number;
is_active: boolean;
status: LessonPackStatus; // "completed" | "generating" | "starting" | "failed"
lesson_type: LessonType;
title: string;
subject: string;
grade_level: string;
total_duration_minutes: number;
ai_rationale: string;
file_url: string | null;
created_at: string;
updated_at: string;
slides: AnySlide[];
resources: string[];
canvasData?: number[]; // Persisted Yjs state for the whiteboard
}LessonType enum
enum LessonType {
INTRODUCTION = "INTRODUCTION",
PRACTICE = "PRACTICE",
DEEP_DIVE = "DEEP_DIVE",
REVIEW = "REVIEW",
ASSESSMENT = "ASSESSMENT",
CONSOLIDATION = "CONSOLIDATION",
}Slide types
Every slide extends BaseSlide and has a discriminating slide_type field:
| slide_type | Type |
|---|---|
| "TITLE" | TitleSlide |
| "LEARNING_OBJECTIVES" | LearningObjectivesSlide |
| "HOOK" | HookSlide |
| "PRIOR_KNOWLEDGE" | PriorKnowledgeSlide |
| "TEACH" | TeachSlide |
| "WORKED_EXAMPLE" | WorkedExampleSlide |
| "GUIDED_PRACTICE" | GuidedPracticeSlide |
| "STUDENT_TASK" | StudentTaskSlide |
| "WARM_UP" | WarmUpSlide |
| "DISCUSSION" | DiscussionSlide |
| "WORKED_SOLUTION" | WorkedSolutionSlide |
| "MISCONCEPTION" | MisconceptionSlide |
| "SELF_ASSESSMENT" | SelfAssessmentSlide |
| "CONCEPT_MAP" | ConceptMapSlide |
| "SYNTHESIS_TASK" | SynthesisTaskSlide |
| "SUMMARY" | SummarySlide |
| "EXIT_TICKET" | ExitTicketSlide |
| "ASSESSMENT_INSTRUCTIONS" | AssessmentInstructionsSlide |
| "ASSESSMENT_QUESTION" | AssessmentQuestionSlide |
| "MARK_SCHEME" | MarkSchemeSlide |
The union type AnySlide covers all of the above. Use a switch on slide_type for exhaustive handling.
BaseSlide
interface BaseSlide {
slide_number: number;
title: string;
teacher_notes: string;
duration_minutes: number;
teacher_only: boolean;
micro_objective_ids: number[];
sen_support: string | null;
image_url: string | null;
}Peer Dependencies
The following packages must be installed alongside this library:
{
"@blocksuite/block-std": "0.19.5",
"@blocksuite/blocks": "0.19.5",
"@blocksuite/icons": "2.1.75",
"@blocksuite/presets": "0.19.5",
"@blocksuite/store": "0.19.5",
"lit": "^3.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"yjs": "^13.0.0"
}Install them all at once:
pnpm add @blocksuite/[email protected] @blocksuite/[email protected] @blocksuite/[email protected] @blocksuite/[email protected] @blocksuite/[email protected] lit yjsCurrently, two official plugins are available:
React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see this documentation.
Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
export default defineConfig([
globalIgnores(["dist"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
]);You can also install eslint-plugin-react-x and eslint-plugin-react-dom for React-specific lint rules:
// eslint.config.js
import reactX from "eslint-plugin-react-x";
import reactDom from "eslint-plugin-react-dom";
export default defineConfig([
globalIgnores(["dist"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs["recommended-typescript"],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
]);