Skip to main content
The slide editor is the main interface in apps/web. It renders your slideshow, lets you navigate between slides and slideshows, and gives you access to the AI assistant, JSON editor, and save controls.

Architecture overview

The frontend is a React 19 application built with Vite using Feature-Sliced Design principles. All slideshow UI lives under apps/web/src/features/slideshow/.
LayerTechnology
RoutingTanStack Router (file-based, src/routes/)
Server stateTanStack Query (data fetching, caching, mutations)
Local stateCustom React hooks (useSlideshowStore)
API callsORPC typed client (src/infra/api/client.ts)
StylingTailwindCSS v4 + shadcn/ui (Radix UI)
Domain logicAdapter layer at src/domain/slideshow/

Loading slideshows

You can load slideshows in two ways:
Click Load from server in the input view. The frontend calls the slideshowLoad ORPC procedure, which reads JSON files from the server’s data directory and returns validated slideshow data with provenance metadata.
// src/features/slideshow/api/queries.ts
export const useSlideshowLoadQuery = (datasetId: string) =>
  useQuery(orpc.slideshowLoad.queryOptions({ input: { datasetId } }));
Slideshows loaded from the server include provenance — metadata about the source file’s location and whether it is writable. Only server-loaded slideshows can be saved back.
Once a slideshow is loaded, you navigate between slides using:
  • Arrow keys / to move between slides (keyboard navigation is handled by useKeyboardNav).
  • Slide list panel — click any slide thumbnail to jump directly.
  • Fullscreen mode — press the fullscreen button or use the keyboard shortcut; arrow key navigation remains active.
The current slide index is stored in useSlideshowStore and clamped automatically when the slideshow structure changes (e.g., after the AI assistant adds or removes slides).

Switching slideshows

If multiple slideshows are loaded, a slideshow selector lets you switch between them. Switching resets the current slide to 0 and clears any per-slideshow concept color overrides.
// Slideshow switch resets navigation state:
const handleSlideshowChange = (newIndex: number) => {
  const clampedIndex = Math.max(0, Math.min(newIndex, slideshowCount - 1));
  setCurrentSlideshowIndex(clampedIndex);
  setCurrentSlide(0);
  setConceptColors({});
};

How slides are organized

Each slide has two key organizational fields:
order
integer
1-based integer that determines the slide’s position in the presentation. The editor sorts and renders slides by order. When the AI assistant adds or moves slides, it updates this field.
concept
string
The key of the concept this slide belongs to (references an entry in the slideshow’s concepts map). Concepts are displayed as color-coded labels in the slide list. See Concepts for details.

Editing slides

The editor provides two editing modes:
The AI assistant panel (assistant-panel.tsx) gives you a chat interface to describe changes in natural language. The assistant returns a patch, which you can review in the pending transaction card and then apply.See AI assistant for the full workflow.

Saving changes

Only slideshows loaded from the server can be saved back. Manually pasted slideshows have no provenance and no save target. Use Load from server if you need persistence.
The save flow is manual — there is no auto-save. Click Save to persist changes.
1

Trigger save

Click the Save button. The onSaveCurrentSlideshow handler reads the active slideshow’s provenance to determine the targetId (the dataset ID for the server-side file).
2

Collect slideshows for that target

Multiple slideshows can share a single file. The frontend collects all slideshows whose provenance targetId matches the current one.
3

Call slideshowSave

The slideshowSave ORPC mutation is called with { datasetId, slideshows }. An optional expectedDigest can be passed to detect server-side changes since the last load.
4

Receive result

On success, the server returns { success: true, digest, path }. The digest can be used for subsequent staleness checks.
The save button shows a loading state while the mutation is in flight (isSaving: saveMutation.isPending).

Validation

The editor runs continuous validation against all loaded slideshows:
  • Schema validationvalidateSlideshowComplete checks the slideshow against the TypeBox schemas.
  • Runtime validationcollectRuntimeValidationErrors checks Mermaid diagram syntax (via the mermaid library) and table/CSV data (via papaparse).
Validation errors are stored in validation-error-store.ts and shown in a dismissible error card. You can copy all errors to the clipboard for debugging.

Domain adapter pattern

All imports of @slides/core in feature code go through the src/domain/slideshow/ adapter layer. This gives a single injection point for future cross-cutting concerns (analytics, retry logic, A/B testing) without touching individual component files.
// Correct — import from adapter:
import { validateSlideshow } from "@/domain/slideshow/validation";
import { buildAssistantMessages } from "@/domain/slideshow/ai";

// Incorrect — bypasses adapter:
import { validateSlideshow } from "@slides/core";

Feature directory layout

apps/web/src/features/slideshow/
├── api/           # TanStack Query hooks (queries.ts, mutations.ts)
├── components/    # UI components
│   ├── blocks/    # BlockRenderer + individual block components
│   ├── panels/    # Assistant panel, JSON editor, patch card
│   ├── viewer/    # Slide viewer and navigation
│   └── concepts/  # Concept color UI
├── hooks/         # Feature hooks (useAssistant, useSlideshowController, ...)
├── services/      # Frontend services (ai-assistant.ts: shouldUseFullContext)
├── state/         # Local state (slideshow-store.ts, validation-error-store.ts)
└── utils/         # Helpers (markdown renderer, normalization)

Build docs developers (and LLMs) love