Skip to main content
The live preview system provides real-time feedback by compiling user code and rendering it in the Remotion Player. It handles streaming updates, compilation errors, runtime errors, and frame capture for visual reference.

Architecture overview

The live preview system consists of four main components:

Code Editor

Monaco-based editor with TypeScript syntax highlighting, JSX support, and streaming overlays.

Compiler

Babel-based transpiler that converts code strings into executable React components.

Animation Player

Remotion Player wrapper with controls, error boundaries, and settings.

Animation State

React hook managing code, compilation status, errors, and component state.

Code compilation

The compiler transforms LLM-generated code into React components at runtime using Babel and dynamic function evaluation.

Compiler implementation

export function compileCode(code: string): CompilationResult {
  if (!code?.trim()) {
    return { Component: null, error: "No code provided" };
  }

  try {
    const componentBody = extractComponentBody(code);
    const wrappedSource = `const DynamicAnimation = () => {\n${componentBody}\n};`;

    const transpiled = Babel.transform(wrappedSource, {
      presets: ["react", "typescript"],
      filename: "dynamic-animation.tsx",
    });

    if (!transpiled.code) {
      return { Component: null, error: "Transpilation failed" };
    }

    // ... setup available imports

    const wrappedCode = `${transpiled.code}\nreturn DynamicAnimation;`;

    const createComponent = new Function(
      "React",
      "Remotion",
      "RemotionShapes",
      "Lottie",
      "ThreeCanvas",
      "THREE",
      // ... all whitelisted imports
      wrappedCode,
    );

    const Component = createComponent(
      React,
      Remotion,
      RemotionShapes,
      Lottie,
      ThreeCanvas,
      THREE,
      // ... inject actual modules
    );

    if (typeof Component !== "function") {
      return {
        Component: null,
        error: "Code must be a function that returns a React component",
      };
    }

    return { Component, error: null };
  } catch (error) {
    const errorMessage =
      error instanceof Error ? error.message : "Unknown compilation error";
    return { Component: null, error: errorMessage };
  }
}
From src/remotion/compiler.ts:80-214

Import extraction

The compiler strips import statements from LLM output and extracts the component body:
function extractComponentBody(code: string): string {
  // Strip all import statements (handles multi-line imports with newlines in braces)
  let cleaned = code;

  // Remove type imports: import type { ... } from "...";
  cleaned = cleaned.replace(
    /import\s+type\s*\{[\s\S]*?\}\s*from\s*["'][^"']+["'];?/g,
    "",
  );
  // Remove combined default + named imports: import X, { ... } from "...";
  cleaned = cleaned.replace(
    /import\s+\w+\s*,\s*\{[\s\S]*?\}\s*from\s*["'][^"']+["'];?/g,
    "",
  );
  // Remove multi-line named imports: import { ... } from "...";
  cleaned = cleaned.replace(
    /import\s*\{[\s\S]*?\}\s*from\s*["'][^"']+["'];?/g,
    "",
  );
  // Remove namespace imports: import * as X from "...";
  cleaned = cleaned.replace(
    /import\s+\*\s+as\s+\w+\s+from\s*["'][^"']+["'];?/g,
    "",
  );
  // Remove default imports: import X from "...";
  cleaned = cleaned.replace(/import\s+\w+\s+from\s*["'][^"']+["'];?/g, "");
  // Remove side-effect imports: import "...";
  cleaned = cleaned.replace(/import\s*["'][^"']+["'];?/g, "");

  cleaned = cleaned.trim();

  // Extract body from "export const MyAnimation = () => { ... };"
  const match = cleaned.match(
    /^([\s\S]*?)export\s+const\s+\w+\s*=\s*\(\s*\)\s*=>\s*\{([\s\S]*)\};?\s*$/,
  );

  if (match) {
    const helpers = match[1].trim();
    const body = match[2].trim();
    return helpers ? `${helpers}\n\n${body}` : body;
  }

  return cleaned;
}
From src/remotion/compiler.ts:34-77
The compiler uses new Function() to evaluate transpiled code in a controlled environment. All external dependencies (React, Remotion, Three.js, etc.) are explicitly injected as function parameters.

Whitelisted libraries

Only specific libraries are available during compilation:
  • React: Core hooks (useState, useEffect, useRef, useMemo)
  • Remotion: Animation primitives (useCurrentFrame, useVideoConfig, interpolate, spring, AbsoluteFill, Sequence, Img)
  • Remotion Shapes: SVG primitives (Circle, Rect, Triangle, Star, Polygon, Ellipse, Heart, Pie)
  • Remotion Transitions: Transition components and effects (TransitionSeries, fade, slide, wipe, flip, clockWipe)
  • Remotion Three: 3D canvas (ThreeCanvas)
  • Three.js: 3D primitives (Vector3, Color, Mesh, geometries, materials)
  • Remotion Lottie: Lottie animations (Lottie)
Any imports not in this whitelist will cause compilation errors.

Animation state management

The useAnimationState hook manages the compilation lifecycle:
export interface AnimationState {
  code: string;
  Component: React.ComponentType | null;
  error: string | null;
  isCompiling: boolean;
}

export function useAnimationState(initialCode: string = "") {
  const [state, setState] = useState<AnimationState>({
    code: initialCode,
    Component: null,
    error: null,
    isCompiling: false,
  });

  // Compile code when it changes (with debouncing handled by caller)
  const compileCode = useCallback((code: string) => {
    setState((prev) => ({ ...prev, isCompiling: true }));

    const result: CompilationResult = compile(code);

    setState((prev) => ({
      ...prev,
      Component: result.Component,
      error: result.error,
      isCompiling: false,
    }));
  }, []);

  // Update code and trigger compilation
  const setCode = useCallback((newCode: string) => {
    setState((prev) => ({ ...prev, code: newCode }));
  }, []);

  // Auto-compile when component mounts with initial code
  useEffect(() => {
    if (initialCode) {
      compileCode(initialCode);
    }
  }, []);

  return {
    ...state,
    setCode,
    compileCode,
  };
}
From src/hooks/useAnimationState.ts:9-56

Compilation triggers

Compilation is triggered in different scenarios:
  1. User edits (debounced 500ms):
const handleCodeChange = useCallback(
  (newCode: string) => {
    setCode(newCode);

    // Clear existing debounce
    if (debounceRef.current) {
      clearTimeout(debounceRef.current);
    }

    // Skip compilation while streaming - will compile when streaming ends
    if (isStreamingRef.current) {
      return;
    }

    // Set new debounce
    debounceRef.current = setTimeout(() => {
      compileCode(newCode);
    }, 500);
  },
  [setCode, compileCode],
);
From src/app/generate/page.tsx:144-171
  1. Streaming ends:
useEffect(() => {
  const wasStreaming = isStreamingRef.current;
  isStreamingRef.current = isStreaming;

  // Compile when streaming ends - mark as AI change
  if (wasStreaming && !isStreaming) {
    markAsAiGenerated();
    compileCode(codeRef.current);
  }
}, [isStreaming, compileCode, markAsAiGenerated]);
From src/app/generate/page.tsx:134-142
Compilation is skipped during streaming to avoid syntax errors from incomplete code. The final compilation happens when streaming ends.

Monaco editor integration

The code editor uses Monaco with custom TypeScript configurations for JSX support:

TypeScript configuration

const handleEditorMount = (
  editorInstance: editor.IStandaloneCodeEditor,
  monaco: Monaco,
) => {
  monacoRef.current = monaco;
  editorRef.current = editorInstance;

  const ts = (monaco.languages as any).typescript;

  // Configure TypeScript compiler options for JSX support
  ts?.typescriptDefaults?.setCompilerOptions({
    target: ts.ScriptTarget?.ESNext,
    module: ts.ModuleKind?.ESNext,
    jsx: ts.JsxEmit?.Preserve,
    allowNonTsExtensions: true,
    strict: false,
    noEmit: true,
    esModuleInterop: true,
    moduleResolution: ts.ModuleResolutionKind?.NodeJs,
    skipLibCheck: true,
  });

  // Enable semantic validation for import checking
  ts?.typescriptDefaults?.setDiagnosticsOptions({
    noSemanticValidation: false,
    noSyntaxValidation: false,
  });

  // Add React types so TS understands JSX
  ts?.typescriptDefaults?.addExtraLib(
    `declare namespace React { ... }`,
    "react.d.ts",
  );
  // ... more type definitions
};
From src/components/CodeEditor/CodeEditor.tsx:70-98

Streaming mode

During streaming, the editor switches to plaintext mode to disable syntax highlighting and error markers:
// Use typescript for semantic checking, plaintext during streaming
const editorLanguage = isStreaming ? "plaintext" : "typescript";

// Continuously clear markers while streaming
useEffect(() => {
  if (!isStreaming || !monacoRef.current) return;

  const clearAllMarkers = () => {
    monacoRef.current?.editor.getModels().forEach((model) => {
      monacoRef.current?.editor.setModelMarkers(model, "javascript", []);
      monacoRef.current?.editor.setModelMarkers(model, "typescript", []);
      monacoRef.current?.editor.setModelMarkers(model, "owner", []);
    });
  };

  clearAllMarkers();
  const interval = setInterval(clearAllMarkers, 100);

  return () => clearInterval(interval);
}, [isStreaming, code]);
From src/components/CodeEditor/CodeEditor.tsx:50-68

Remotion Player integration

The AnimationPlayer component wraps the Remotion Player with error handling and controls:
return (
  <>
    <div className="w-full aspect-video max-h-[calc(100%-80px)] rounded-lg overflow-hidden shadow-[0_0_60px_rgba(0,0,0,0.5)]">
      <Player
        ref={playerRef}
        key={Component.toString()}
        component={Component}
        durationInFrames={durationInFrames}
        fps={fps}
        compositionHeight={1080}
        compositionWidth={1920}
        style={{
          width: "100%",
          height: "100%",
          backgroundColor: "transparent",
        }}
        controls
        autoPlay
        loop
        errorFallback={renderErrorFallback}
        spaceKeyToPlayOrPause={false}
        clickToPlay={false}
      />
    </div>
    <div className="flex items-center justify-between gap-6 mt-4">
      <RenderControls
        code={code}
        durationInFrames={durationInFrames}
        fps={fps}
      />
      <SettingsModal
        durationInFrames={durationInFrames}
        onDurationChange={onDurationChange}
        fps={fps}
        onFpsChange={onFpsChange}
      />
    </div>
  </>
);
From src/components/AnimationPlayer/index.tsx:129-166

Error boundaries

Runtime errors are caught by the Player’s error boundary:
const renderErrorFallback: ErrorFallback = ({ error }) => {
  return (
    <ErrorDisplay
      error={error.message || "An error occurred while rendering"}
      title="Runtime Error"
      variant="fullscreen"
      size="lg"
    />
  );
};
From src/components/AnimationPlayer/index.tsx:15-24 Runtime errors are also propagated to the parent for auto-correction:
useEffect(() => {
  const player = playerRef.current;
  if (!player || !onRuntimeError) return;

  const handleError = (e: { detail: { error: Error } }) => {
    onRuntimeError(e.detail.error.message);
  };

  player.addEventListener("error", handleError);
  return () => {
    player.removeEventListener("error", handleError);
  };
}, [onRuntimeError, Component]);
From src/components/AnimationPlayer/index.tsx:58-71

Frame capture

Users can capture the current frame as a visual reference for follow-up prompts:
const handleCapture = async () => {
  if (!Component || isCapturing || !canAddMore) return;

  setIsCapturing(true);
  try {
    const base64 = await captureFrame(Component, currentFrame, {
      width: 1920,
      height: 1080,
      fps,
      durationInFrames,
    });
    addImages([base64]);
  } catch (error) {
    console.error("Failed to capture frame:", error);
  } finally {
    setIsCapturing(false);
  }
};
From src/components/ChatSidebar/ChatInput.tsx:85-102 Captured frames are sent to the LLM as base64-encoded images in follow-up requests, enabling visual context for prompts like “make the text match the color of the background in this frame.”

State synchronization

The preview system maintains several pieces of state:
  • Code: Current editor content
  • Component: Compiled React component
  • Compilation error: Syntax/type errors from Babel
  • Runtime error: Errors from executing the component
  • Frame number: Current playback position
  • Settings: FPS and duration
These states are synchronized across components:
const {
  code,
  Component,
  error: compilationError,
  isCompiling,
  setCode,
  compileCode,
} = useAnimationState(examples[0]?.code || "");

const [runtimeError, setRuntimeError] = useState<string | null>(null);
const codeError = compilationError || runtimeError;
From src/app/generate/page.tsx:76-88
The system combines compilation and runtime errors into a single codeError state for unified error handling and auto-correction.

Build docs developers (and LLMs) love