Build an AI Short-Form Video Generator
Build a full-stack app that generates video scripts using Claude AI.
Introduction
30 Second Summary
Every day, millions of creators struggle to write engaging scripts, plan scenes, and craft hashtags for their short-form videos. Turning a single topic into a complete, ready-to-film content package can take hours.
In this project, you will build ReelForge AI, a full-stack web application that uses Claude AI to generate production-ready video content from a simple form submission.
What You'll Build
A React and Tailwind CSS frontend connected to a Python FastAPI backend that calls Claude AI to produce structured content packages for Instagram Reels, YouTube Shorts, and TikTok.
By the end of this project, you'll have:
- A modern React dashboard with dark/light mode toggle and sidebar navigation.
- A FastAPI backend that sends prompts to Claude AI and returns structured content including titles, scripts, scenes, hashtags, and SEO descriptions.
- A complete content generation pipeline from form input to exportable output with clipboard copy and JSON download.
- Secret Mission: Save generated content to a history panel using localStorage.
Are there any prerequisites?
This project assumes basic knowledge of React, Python, and REST APIs. You do not need prior experience with AI APIs or Tailwind CSS.
Set Up the Development Environment
ReelForge AI has two halves: a React frontend that collects user input and displays results, and a Python backend that talks to Claude AI. Before writing any application code, you need both halves scaffolded and wired to their dependencies.
In this step, you'll create a project folder with separate frontend and backend directories, scaffold a React app with Vite and Tailwind CSS, and set up a Python environment with FastAPI and the Anthropic SDK.
In this step, get ready to:
- Scaffold a React frontend with Vite and Tailwind CSS.
- Create a Python virtual environment with FastAPI and the Anthropic SDK installed.
- Set your Anthropic API key as an environment variable.
Confirm your tools are ready
With Node.js, Python 3.10+, and VS Code (or Cursor) already installed and verified on your machine, you're ready to create the project.
What if I haven't installed these yet?
Run node --version and python --version in any terminal. You should see a Node.js version number and Python 3.10 or higher. If either command fails, install them from their official websites before continuing.
Create the project structure and scaffold the React frontend
The project needs a root folder with two child directories. The frontend directory will hold the React app, and the backend directory will hold the Python API.
- Open a terminal in VS Code by pressing Ctrl+` (backtick) on both macOS and Windows.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir reelforge-ai
cd reelforge-ai
- Scaffold the React frontend using Vite by running this command:
npm create vite@latest frontend -- --template react
What does this command do?
This uses Vite's project scaffolding tool to create a new React project inside a folder called frontend. The -- --template react flag tells Vite to use the React starter template, skipping any interactive prompts.
- Move into the frontend folder and install the base dependencies by running:
cd frontend
npm install
- Install Tailwind CSS and its Vite plugin by running:
npm install tailwindcss @tailwindcss/vite
Why Tailwind CSS?
Tailwind CSS lets you style components using utility classes directly in your markup. Instead of writing separate CSS files with custom class names, you compose styles inline. This speeds up development and keeps your styling consistent.
Now you need to configure Vite to use the Tailwind CSS plugin and update the main stylesheet.
- Open frontend/vite.config.js in your editor.
- Replace the entire file contents with the following code:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})
What does this code do?
This configures Vite with two plugins. The react() plugin enables JSX and React Fast Refresh for instant updates as you code. The tailwindcss() plugin processes Tailwind utility classes during development and build.
- Open frontend/src/index.css in your editor.
- Delete everything in the file and replace it with this single line:
@import "tailwindcss";
What does this do?
This single import loads all of Tailwind's utility classes into your project. In Tailwind CSS v4, this replaces the older @tailwind base directives. No tailwind.config.js file is needed.
- Save both vite.config.js and index.css.
- Confirm the files exist by running:
ls src/index.css vite.config.js
You should see both files listed in the terminal output.
Not seeing the files?
Make sure you're inside the frontend folder. Run pwd to check your current directory. If you're in reelforge-ai instead, run cd frontend first.
Help me troubleshoot my project structure.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is what each file should look like:
frontend/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})
frontend/src/index.css
@import "tailwindcss";
frontend/src/main.jsx (generated by Vite, should already look like this):
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
Set up the Python backend
The backend needs its own isolated Python environment so its dependencies don't conflict with other Python projects on your machine.
- Navigate back to the project root and create the backend folder by running:
cd ..
mkdir backend
cd backend
- Create a Python virtual environment by running:
python -m venv venv
What is a virtual environment?
A virtual environment is an isolated Python workspace. Packages you install here stay separate from your system Python. This prevents version conflicts between different projects on your machine.
- Activate the virtual environment by running the command for your operating system:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
- Install FastAPI with its standard dependencies by running:
pip install "fastapi[standard]"
- Install the Anthropic SDK by running:
pip install anthropic
What are these packages?
FastAPI is a modern Python web framework for building APIs. The [standard] extra includes a development server with auto-reload.
The Anthropic SDK is the official Python library for communicating with Claude AI. It handles authentication, request formatting, and response parsing.
Finally, you need to set your Anthropic API key so the SDK can authenticate with Claude.
- Set the ANTHROPIC_API_KEY environment variable by running the command for your operating system:
macOS
export ANTHROPIC_API_KEY=your-api-key-here
Replace your-api-key-here with the API key from your Anthropic dashboard.
Windows
set ANTHROPIC_API_KEY=your-api-key-here
Replace your-api-key-here with the API key from your Anthropic dashboard.
Where do I get an API key?
Sign in at console.anthropic.com, navigate to API Keys, and create a new key. Copy it immediately. You will not be able to see it again after closing the dialog.
- Verify your backend is ready by running:
python -c "import fastapi; import anthropic; print('All packages installed!')"
You should see All packages installed! printed in your terminal.
Seeing a ModuleNotFoundError?
Make sure your virtual environment is active. Look for (venv) at the start of your terminal prompt. If it's missing, re-run the activation command for your OS.
If the environment is active but packages are still missing, re-run pip install "fastapi[standard]" anthropic and check for any error messages during installation.
Help me fix my Python package installation.
Your development environment is fully set up. You have a React frontend with Tailwind CSS ready to build, and a Python backend with FastAPI and the Anthropic SDK installed. Next up, you'll build the FastAPI backend and connect it to Claude AI.
Build the FastAPI Backend with Claude AI Integration
Your frontend is scaffolded and your Python environment is ready with all the dependencies installed. Now it's time to build the backend that powers everything: a FastAPI server that receives content requests and sends them to Claude AI for generation.
You'll define typed data models that describe what the frontend sends and what the backend returns. Then you'll build an endpoint that crafts a detailed prompt and calls the Claude API to produce structured video content.
In this step, get ready to:
- Create the FastAPI app with CORS middleware and data models.
- Build the content generation endpoint that calls Claude AI.
- Test the backend and verify it returns structured content.
Create the app structure and data models
Every FastAPI project starts with an app instance, middleware configuration, and data models. CORS middleware is essential here because your React frontend (on port 5173) needs to communicate with the backend (on port 8000).
- Create a file called main.py inside your backend folder.
- Add the following code to set up the app, CORS, and the AI client:
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from anthropic import Anthropic
# Create the FastAPI app instance
app = FastAPI()
# Allow the React frontend to make requests to this backend
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize the Anthropic client using the API key from your environment
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
What does this code do?
- app = FastAPI() creates the web server that will handle incoming requests.
- The CORS middleware tells the backend to accept requests from http://localhost:5173 (your Vite dev server). Without this, browsers block cross-origin requests.
- The Anthropic client reads your ANTHROPIC_API_KEY environment variable so it can authenticate with the Claude API.
- Confirm main.py exists by running:
ls
You should see main.py listed in the output.
Next, you need to define the shape of the data flowing in and out of your API. Pydantic models validate incoming requests and structure outgoing responses automatically.
- Add the following two classes below the client line in main.py:
class GenerateRequest(BaseModel):
topic: str
language: str
theme: str
voice_type: str
duration: str
platform: str
class GenerateResponse(BaseModel):
title: str
narration: str
scenes: list[str]
subtitles: str
thumbnail_text: str
hashtags: list[str]
seo_description: str
What do these models do?
GenerateRequest defines the six fields the frontend will send: topic, language, theme, voice type, duration, and platform. If a request arrives with missing or mistyped fields, FastAPI automatically returns an error.
GenerateResponse defines the structured output the endpoint returns: title, narration script, scene descriptions, subtitles, thumbnail text, hashtags, and an SEO description.
- Save main.py.
You won't see any visible output yet. The next chunk builds the endpoint that uses these models.
Build the content generation endpoint
This is the core of the app. The endpoint takes the user's form inputs, constructs a detailed prompt that guides Claude to produce structured video content, then parses the AI response into your defined format.
- Add the following endpoint below your GenerateResponse class in main.py:
@app.post("/api/generate", response_model=GenerateResponse)
async def generate_content(request: GenerateRequest):
# Build a detailed prompt using the user's form inputs
prompt = f"""You are a viral short-form video content expert. Generate complete content for a {request.duration} {request.platform} video.
Topic: {request.topic}
Language: {request.language}
Video Theme: {request.theme}
Voice Type: {request.voice_type}
Duration: {request.duration}
Platform: {request.platform}
Generate content optimized for high retention using this structure:
- Hook (first 3 seconds): Grab attention immediately
- Story build-up: Build curiosity
- Interesting fact: Deliver value
- Twist or surprise: Keep viewers engaged
- Strong ending: Create a memorable close
Respond in valid JSON format with these exact keys:
{{
"title": "A viral, clickable title",
"narration": "The complete narration script for the voice-over, written for {request.voice_type} voice",
"scenes": ["Scene 1 visual description", "Scene 2 visual description", ...],
"subtitles": "Complete subtitle text with timing markers",
"thumbnail_text": "Short punchy text for the video thumbnail",
"hashtags": ["hashtag1", "hashtag2", ...],
"seo_description": "SEO-optimized description for the video"
}}
Write the narration and all content in {request.language}. Make it engaging, dramatic, and optimized for {request.platform} audience retention."""
# Call the Claude API with the constructed prompt
message = client.messages.create(
model="claude-haiku-4-5",
max_tokens=2048,
messages=[
{"role": "user", "content": prompt}
],
)
# Parse the JSON from Claude's response text
import json
response_text = message.content[0].text
start = response_text.find("{")
end = response_text.rfind("}") + 1
parsed = json.loads(response_text[start:end])
return GenerateResponse(**parsed)
What does this endpoint do?
- The @app.post decorator registers this function at /api/generate and tells FastAPI the response shape is GenerateResponse.
- The f-string prompt injects the user's choices (topic, platform, duration, etc.) into a detailed instruction for Claude.
- client.messages.create sends the prompt to claude-haiku-4-5 with a 2048 token limit.
- The JSON parsing section extracts the first { to last } from Claude's response, handling cases where Claude adds text around the JSON.
Why Claude Haiku 4.5?
For structured text generation like video scripts, Haiku 4.5 delivers excellent quality at a fraction of the cost of larger models. Each generation costs roughly $0.01-0.05, making it ideal for a learning project where you might generate many times.
- Add a health check endpoint at the bottom of main.py:
@app.get("/api/health")
async def health_check():
return {"status": "ok"}
What is the health endpoint for?
A health check endpoint gives you a quick way to confirm the server is running without triggering an AI call. You'll use it to verify the backend starts correctly before testing the full generation flow.
- Save main.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and move on to testing.
ⓧ I'd like to double check the full code
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from anthropic import Anthropic
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
class GenerateRequest(BaseModel):
topic: str
language: str
theme: str
voice_type: str
duration: str
platform: str
class GenerateResponse(BaseModel):
title: str
narration: str
scenes: list[str]
subtitles: str
thumbnail_text: str
hashtags: list[str]
seo_description: str
@app.post("/api/generate", response_model=GenerateResponse)
async def generate_content(request: GenerateRequest):
prompt = f"""You are a viral short-form video content expert. Generate complete content for a {request.duration} {request.platform} video.
Topic: {request.topic}
Language: {request.language}
Video Theme: {request.theme}
Voice Type: {request.voice_type}
Duration: {request.duration}
Platform: {request.platform}
Generate content optimized for high retention using this structure:
- Hook (first 3 seconds): Grab attention immediately
- Story build-up: Build curiosity
- Interesting fact: Deliver value
- Twist or surprise: Keep viewers engaged
- Strong ending: Create a memorable close
Respond in valid JSON format with these exact keys:
{{
"title": "A viral, clickable title",
"narration": "The complete narration script for the voice-over, written for {request.voice_type} voice",
"scenes": ["Scene 1 visual description", "Scene 2 visual description", ...],
"subtitles": "Complete subtitle text with timing markers",
"thumbnail_text": "Short punchy text for the video thumbnail",
"hashtags": ["hashtag1", "hashtag2", ...],
"seo_description": "SEO-optimized description for the video"
}}
Write the narration and all content in {request.language}. Make it engaging, dramatic, and optimized for {request.platform} audience retention."""
message = client.messages.create(
model="claude-haiku-4-5",
max_tokens=2048,
messages=[
{"role": "user", "content": prompt}
],
)
import json
response_text = message.content[0].text
start = response_text.find("{")
end = response_text.rfind("}") + 1
parsed = json.loads(response_text[start:end])
return GenerateResponse(**parsed)
@app.get("/api/health")
async def health_check():
return {"status": "ok"}
Test the backend with a sample request
Your backend code is complete. Time to spin up the server and confirm Claude AI returns the structured content you expect.
- Make sure your terminal is in the backend directory with your virtual environment activated.
- Start the development server by running:
fastapi dev
What does this command do?
fastapi dev starts a local development server on http://localhost:8000 with auto-reload enabled. Any time you save main.py, the server restarts automatically.
- Open your browser and visit http://localhost:8000/api/health to confirm the server is running.
You should see {"status":"ok"} in your browser.
Server not starting?
- Make sure your virtual environment is activated. You should see (venv) or similar at the start of your terminal prompt.
- Check that you're inside the backend directory, not the project root.
- If you see an import error, confirm you installed FastAPI and the Anthropic SDK in your virtual environment in the previous step.
Help me debug why my FastAPI server won't start.
Now test the full AI generation. FastAPI automatically generates interactive API documentation.
- Visit http://localhost:8000/docs in your browser to open the Swagger UI.
- Click on the POST /api/generate endpoint to expand it.
- Click Try it out.
- Replace the request body with this test payload:
{
"topic": "Space",
"language": "English",
"theme": "Documentary",
"voice_type": "Male",
"duration": "30 seconds",
"platform": "YouTube Shorts"
}
- Click Execute.
After a few seconds, you should see a JSON response containing a title, narration script, scenes array, subtitles, thumbnail text, hashtags, and SEO description.
What should I see?
The response will be a structured JSON object with all seven fields populated by Claude AI. The narration will be written for a male documentary voice, the scenes will describe visuals for a 30-second space video, and the hashtags will be relevant to both space content and YouTube Shorts.
Seeing a 500 error?
- Check that your ANTHROPIC_API_KEY environment variable is set in the same terminal session where the server is running.
- If you see a JSON parsing error, Claude may have returned unexpected formatting. Try clicking Execute again.
- Verify your API key is valid and has available credits on your Anthropic account.
Help me fix a 500 error from my FastAPI generate endpoint.
Your backend is live and generating AI content on demand. Next up, you'll build the React frontend that sends requests to this endpoint and displays the results in a polished dashboard.
Build the Frontend Layout and Theme System
Your backend is ready to generate content. Now you need a frontend that looks and feels like a real SaaS product.
In this step, you'll build the app shell with a sidebar, top bar, and a dark/light mode theme toggle. These layout components give the app its professional structure before you wire up the content generation form.
In this step, get ready to:
- Create the App component with dark/light mode theming and layout structure.
- Build a sidebar navigation with active state highlighting.
- Create a top bar with a theme toggle and responsive mobile menu.
Set up the App component with dark/light mode
The Tailwind CSS plugin and CSS import are already configured from Step 1. Now you need the main App component that manages theme state and lays out the page structure.
- In your terminal, make sure you're inside the frontend directory. Start the Vite dev server by running:
npm run dev
What does this do?
This starts Vite's development server on http://localhost:5173. Any changes you save will instantly hot-reload in the browser.
- Open http://localhost:5173 in your browser. You should see the default Vite + React starter page.
- Replace the contents of frontend/src/App.jsx with the following code:
import { useState } from 'react'
import Sidebar from './components/Sidebar'
import TopBar from './components/TopBar'
import GeneratorForm from './components/GeneratorForm'
import ResultsPanel from './components/ResultsPanel'
function App() {
// State for theme, sidebar visibility, AI results, and navigation
const [darkMode, setDarkMode] = useState(true)
const [sidebarOpen, setSidebarOpen] = useState(false)
const [results, setResults] = useState(null)
const [loading, setLoading] = useState(false)
const [activeView, setActiveView] = useState('create')
const toggleDarkMode = () => setDarkMode(!darkMode)
const toggleSidebar = () => setSidebarOpen(!sidebarOpen)
// Send form data to the FastAPI backend and store the response
const handleGenerate = async (formData) => {
setLoading(true)
setResults(null)
try {
const response = await fetch('http://localhost:8000/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
})
if (!response.ok) throw new Error('Generation failed')
const data = await response.json()
setResults(data)
} catch (error) {
console.error('Error generating content:', error)
alert('Failed to generate content. Please check your API key and try again.')
} finally {
setLoading(false)
}
}
return (
<div className={darkMode ? 'dark' : ''}>
<div className="flex h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<Sidebar
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
activeView={activeView}
setActiveView={setActiveView}
/>
<div className="flex-1 flex flex-col overflow-hidden">
<TopBar
darkMode={darkMode}
toggleDarkMode={toggleDarkMode}
toggleSidebar={toggleSidebar}
/>
<main className="flex-1 overflow-y-auto p-4 md:p-6">
{activeView === 'create' && (
<div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-6">
<GeneratorForm onGenerate={handleGenerate} loading={loading} />
<ResultsPanel results={results} loading={loading} />
</div>
)}
{activeView === 'dashboard' && (
<div className="max-w-7xl mx-auto">
<h2 className="text-2xl font-bold mb-4">Dashboard</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<p className="text-sm text-gray-500 dark:text-gray-400">Total Projects</p>
<p className="text-3xl font-bold mt-1">0</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<p className="text-sm text-gray-500 dark:text-gray-400">This Week</p>
<p className="text-3xl font-bold mt-1">0</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<p className="text-sm text-gray-500 dark:text-gray-400">Favorites</p>
<p className="text-3xl font-bold mt-1">0</p>
</div>
</div>
</div>
)}
</main>
</div>
</div>
</div>
)
}
export default App
What does this code do?
This component manages the entire app layout. Here is what each piece handles:
- darkMode state (default true) controls whether the outer div gets a dark CSS class. This activates all the dark: variants in Tailwind.
- handleGenerate sends a POST request to your FastAPI backend at http://localhost:8000/api/generate and stores the AI response in results state.
- activeView controls which page is displayed (the create form or the dashboard stats) without needing a full router library.
- Save App.jsx.
Your browser will show an error overlay about failing to resolve the component imports. This is expected because Sidebar.jsx and the other component files don't exist yet.
Seeing a different error?
If you see an error about useState or a syntax error, double check that the file starts with import { useState } from 'react' and that all curly braces are balanced.
Help me fix a syntax error in my App.jsx file for a React + Vite project.
Build the sidebar navigation component
The sidebar gives your app the classic SaaS dashboard feel with navigation links, active state highlighting, and a mobile-friendly slide-in animation.
- Create a components folder inside frontend/src/ by running:
mkdir src/components
- Confirm the folder exists by running:
ls src
You should see components listed in the output alongside App.jsx and the other existing files.
- Create a new file frontend/src/components/Sidebar.jsx and add this code:
function Sidebar({ isOpen, onClose, activeView, setActiveView }) {
// Navigation items for the sidebar menu
const navItems = [
{ id: 'dashboard', label: 'Dashboard', icon: '📊' },
{ id: 'create', label: 'Create Video', icon: '🎬' },
{ id: 'history', label: 'History', icon: '📁' },
{ id: 'settings', label: 'Settings', icon: '⚙️' },
]
return (
<>
{/* Dark overlay on mobile when sidebar is open */}
{isOpen && (
<div
className="fixed inset-0 bg-black/50 z-20 lg:hidden"
onClick={onClose}
/>
)}
<aside
className={`fixed lg:static inset-y-0 left-0 z-30 w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transform transition-transform duration-200 ${
isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
}`}
>
<div className="p-6">
<h1 className="text-xl font-bold bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">
ReelForge AI
</h1>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">AI Content Generator</p>
</div>
<nav className="px-4 space-y-1">
{navItems.map((item) => (
<button
key={item.id}
onClick={() => {
setActiveView(item.id)
onClose()
}}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${
activeView === item.id
? 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400'
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}
>
<span>{item.icon}</span>
{item.label}
</button>
))}
</nav>
</aside>
</>
)
}
export default Sidebar
What does this code do?
The sidebar handles three key behaviors:
- On desktop (lg: breakpoint and above), the sidebar is always visible using lg:static and lg:translate-x-0.
- On mobile, it slides in from the left when isOpen is true. A dark overlay appears behind it so the user can tap outside to close.
- The active nav item gets a purple highlight via conditional class names, giving clear visual feedback for the current view.
- Save Sidebar.jsx.
The browser error should update. It now only complains about the remaining missing components.
Still seeing an error about Sidebar?
Make sure the file is saved at exactly src/components/Sidebar.jsx (capital S). The import path in App.jsx is case-sensitive.
Help me fix a missing component import in my Vite React project.
Create the top bar with theme toggle
The top bar sits at the top of the main content area. It holds the hamburger menu button for mobile, the dark/light mode toggle, and a user avatar placeholder.
- Create a new file frontend/src/components/TopBar.jsx and add this code:
function TopBar({ darkMode, toggleDarkMode, toggleSidebar }) {
return (
<header className="h-16 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between px-4 md:px-6">
{/* Hamburger menu - only visible on mobile */}
<button
onClick={toggleSidebar}
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
>
<span className="text-xl">☰</span>
</button>
<div className="flex-1" />
<div className="flex items-center gap-3">
{/* Dark/light mode toggle */}
<button
onClick={toggleDarkMode}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
title={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
>
{darkMode ? '☀️' : '🌙'}
</button>
{/* Placeholder user avatar */}
<div className="w-8 h-8 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center text-white text-sm font-bold">
U
</div>
</div>
</header>
)
}
export default TopBar
What does this code do?
The top bar adapts to the screen size:
- The hamburger button only shows on mobile because of the lg:hidden class. It calls toggleSidebar to slide the sidebar in.
- The theme toggle button swaps between a sun and moon icon depending on the current darkMode value.
- The purple gradient circle is a placeholder avatar. In a production app, this would show the user's profile image.
- Save TopBar.jsx.
Your browser error should now only mention GeneratorForm and ResultsPanel. You won't see the full UI yet because those components don't exist. You'll build them in the next steps.
Seeing more errors than expected?
If the error still mentions Sidebar or TopBar, confirm both files are inside src/components/ and the filenames match the import paths exactly (capital S and capital T).
Help me debug my component imports so my React app compiles.
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files.
ⓧ I'd like to double check the full code
Here is the full App.jsx:
import { useState } from 'react'
import Sidebar from './components/Sidebar'
import TopBar from './components/TopBar'
import GeneratorForm from './components/GeneratorForm'
import ResultsPanel from './components/ResultsPanel'
function App() {
const [darkMode, setDarkMode] = useState(true)
const [sidebarOpen, setSidebarOpen] = useState(false)
const [results, setResults] = useState(null)
const [loading, setLoading] = useState(false)
const [activeView, setActiveView] = useState('create')
const toggleDarkMode = () => setDarkMode(!darkMode)
const toggleSidebar = () => setSidebarOpen(!sidebarOpen)
const handleGenerate = async (formData) => {
setLoading(true)
setResults(null)
try {
const response = await fetch('http://localhost:8000/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
})
if (!response.ok) throw new Error('Generation failed')
const data = await response.json()
setResults(data)
} catch (error) {
console.error('Error generating content:', error)
alert('Failed to generate content. Please check your API key and try again.')
} finally {
setLoading(false)
}
}
return (
<div className={darkMode ? 'dark' : ''}>
<div className="flex h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<Sidebar
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
activeView={activeView}
setActiveView={setActiveView}
/>
<div className="flex-1 flex flex-col overflow-hidden">
<TopBar
darkMode={darkMode}
toggleDarkMode={toggleDarkMode}
toggleSidebar={toggleSidebar}
/>
<main className="flex-1 overflow-y-auto p-4 md:p-6">
{activeView === 'create' && (
<div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-6">
<GeneratorForm onGenerate={handleGenerate} loading={loading} />
<ResultsPanel results={results} loading={loading} />
</div>
)}
{activeView === 'dashboard' && (
<div className="max-w-7xl mx-auto">
<h2 className="text-2xl font-bold mb-4">Dashboard</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<p className="text-sm text-gray-500 dark:text-gray-400">Total Projects</p>
<p className="text-3xl font-bold mt-1">0</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<p className="text-sm text-gray-500 dark:text-gray-400">This Week</p>
<p className="text-3xl font-bold mt-1">0</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<p className="text-sm text-gray-500 dark:text-gray-400">Favorites</p>
<p className="text-3xl font-bold mt-1">0</p>
</div>
</div>
</div>
)}
</main>
</div>
</div>
</div>
)
}
export default App
Here is the full Sidebar.jsx:
function Sidebar({ isOpen, onClose, activeView, setActiveView }) {
const navItems = [
{ id: 'dashboard', label: 'Dashboard', icon: '📊' },
{ id: 'create', label: 'Create Video', icon: '🎬' },
{ id: 'history', label: 'History', icon: '📁' },
{ id: 'settings', label: 'Settings', icon: '⚙️' },
]
return (
<>
{isOpen && (
<div
className="fixed inset-0 bg-black/50 z-20 lg:hidden"
onClick={onClose}
/>
)}
<aside
className={`fixed lg:static inset-y-0 left-0 z-30 w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transform transition-transform duration-200 ${
isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
}`}
>
<div className="p-6">
<h1 className="text-xl font-bold bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">
ReelForge AI
</h1>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">AI Content Generator</p>
</div>
<nav className="px-4 space-y-1">
{navItems.map((item) => (
<button
key={item.id}
onClick={() => {
setActiveView(item.id)
onClose()
}}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${
activeView === item.id
? 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400'
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}
>
<span>{item.icon}</span>
{item.label}
</button>
))}
</nav>
</aside>
</>
)
}
export default Sidebar
Here is the full TopBar.jsx:
function TopBar({ darkMode, toggleDarkMode, toggleSidebar }) {
return (
<header className="h-16 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between px-4 md:px-6">
<button
onClick={toggleSidebar}
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
>
<span className="text-xl">☰</span>
</button>
<div className="flex-1" />
<div className="flex items-center gap-3">
<button
onClick={toggleDarkMode}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
title={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
>
{darkMode ? '☀️' : '🌙'}
</button>
<div className="w-8 h-8 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center text-white text-sm font-bold">
U
</div>
</div>
</header>
)
}
export default TopBar
Your app shell is taking shape. Next up, you'll build the content generation form that sends user inputs to your Claude AI backend.
Build the Content Generation Form
Your layout is looking sharp with a sidebar, top bar, and dark mode toggle all working together.
Now it's time to build the core of the app: the content generation form. This is where users will select their topic, language, platform, and other preferences before the AI generates a complete video content package.
In this step, get ready to:
- Define the dropdown option arrays that power the form fields.
- Add state management and form submission logic.
- Style the form with Tailwind CSS card layouts and a gradient submit button.
Create the GeneratorForm component with dropdown options
The form needs to offer users a curated set of choices for their video content. You'll define constant arrays for each field so the dropdowns are easy to maintain and extend later.
- Create a new file called GeneratorForm.jsx inside frontend/src/components/ and add the following option arrays:
import { useState } from 'react'
// Dropdown options for each form field
const TOPICS = ['History', 'Mystery', 'Space', 'Science', 'Technology', 'Animals', 'Business', 'Ancient Civilizations', 'Treasure Hunt', 'Motivation', 'AI News', 'Weird Facts']
const LANGUAGES = ['English', 'Telugu', 'Hindi', 'Tamil', 'Kannada']
const THEMES = ['Documentary', 'Cinematic', 'Dark Mystery', 'Historical', 'Sci-Fi', 'Adventure', 'Educational', 'News Style']
const VOICE_TYPES = ['Male', 'Female', 'Deep Documentary', 'Young Voice']
const DURATIONS = ['30 seconds', '45 seconds', '60 seconds']
const PLATFORMS = ['Instagram Reel', 'YouTube Shorts', 'TikTok']
What do these arrays do?
Each constant array defines the selectable options for one dropdown in the form.
- TOPICS has 12 content categories like History, Space, and AI News.
- LANGUAGES offers 5 language options for the generated script.
- PLATFORMS targets the three major short-form video platforms.
You won't see a change in the browser yet. The next chunk builds the component that uses these arrays.
Add state management and form submission logic
The component needs to track which option the user selects in each dropdown, then send all those selections to the backend when they click the generate button. React's useState hook handles this by storing the current form values in a single state object.
- Below the constant arrays in GeneratorForm.jsx, add the complete component function:
function GeneratorForm({ onGenerate, loading }) {
// Initialize form state with the first option from each array
const [formData, setFormData] = useState({
topic: TOPICS[0],
language: LANGUAGES[0],
theme: THEMES[0],
voice_type: VOICE_TYPES[0],
duration: DURATIONS[0],
platform: PLATFORMS[0],
})
// Update a single field in the form state
const handleChange = (field, value) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
// Prevent default form submit and pass data to the parent component
const handleSubmit = (e) => {
e.preventDefault()
onGenerate(formData)
}
// Configuration array that maps each field to its label and options
const fields = [
{ key: 'topic', label: 'Topic', options: TOPICS },
{ key: 'language', label: 'Language', options: LANGUAGES },
{ key: 'theme', label: 'Video Theme', options: THEMES },
{ key: 'voice_type', label: 'Voice Type', options: VOICE_TYPES },
{ key: 'duration', label: 'Duration', options: DURATIONS },
{ key: 'platform', label: 'Platform', options: PLATFORMS },
]
return (
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<h2 className="text-lg font-bold mb-4">Generate Content</h2>
<div className="space-y-4">
{fields.map(({ key, label, options }) => (
<div key={key}>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{label}
</label>
<select
value={formData[key]}
onChange={(e) => handleChange(key, e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-purple-500 focus:border-transparent outline-none transition-colors"
>
{options.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
))}
</div>
<button
type="submit"
disabled={loading}
className="w-full mt-6 px-6 py-3 bg-gradient-to-r from-purple-600 to-pink-600 text-white font-semibold rounded-lg hover:from-purple-700 hover:to-pink-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<span className="animate-spin">⌛</span> Generating...
</span>
) : (
'🚀 Generate Content'
)}
</button>
</form>
)
}
export default GeneratorForm
What does this component do?
- formData state holds the currently selected value for each field, initialized to the first option in each array.
- handleChange uses the spread operator to update just one field while keeping the rest unchanged.
- The fields array lets you render all six dropdowns with a single .map() loop instead of repeating JSX six times.
- onGenerate is a prop passed from App.jsx that triggers the API call to your FastAPI backend.
- The loading prop disables the button and swaps its text to a spinner animation while the API responds.
- Save GeneratorForm.jsx.
- Switch back to your browser at http://localhost:5173.
- You should see a white card (or dark card in dark mode) on the left side of the page with six dropdown fields and a purple-to-pink gradient button labeled "Generate Content".
Don't see the form?
- Check that the file is saved at frontend/src/components/GeneratorForm.jsx with the exact filename and capitalization.
- Look at your terminal running the Vite dev server for error messages. A missing export default or typo in the import path will show up there.
- Make sure App.jsx already has import GeneratorForm from './components/GeneratorForm' at the top. This was added in the previous step.
Help me debug why my GeneratorForm component isn't rendering.
Review the Tailwind styling patterns
The form is already styled using several key Tailwind CSS patterns that are worth understanding.
How does the Tailwind styling work?
- The outer <form> uses rounded-xl p-6 shadow-sm to create the card layout with rounded corners and subtle shadow.
- Dark mode classes like dark:bg-gray-800 automatically apply when the parent has the dark class (controlled by your toggle).
- The button uses bg-gradient-to-r from-purple-600 to-pink-600 for the purple-to-pink gradient.
- disabled:opacity-50 disabled:cursor-not-allowed dims the button and removes the pointer while the API request is in progress.
- Try clicking the dark mode toggle in the top bar. The form card and dropdowns should smoothly switch between light and dark themes.
- Try clicking Generate Content. You'll see an error alert because the ResultsPanel component doesn't exist yet. That's expected for now.
Why use a fields array instead of writing each dropdown manually?
The fields config array is a common React pattern called data-driven rendering. Instead of copying and pasting the same <select> markup six times, you describe what each field needs (key, label, options) and let .map() generate the JSX.
This means adding a 7th dropdown later is as simple as adding one object to the array. No JSX duplication needed.
✔️ Awesome, I've got everything!
Great. Double check you've saved GeneratorForm.jsx and your form is rendering in the browser.
ⓧ I'd like to double check the full code
Here is the complete GeneratorForm.jsx file:
import { useState } from 'react'
const TOPICS = ['History', 'Mystery', 'Space', 'Science', 'Technology', 'Animals', 'Business', 'Ancient Civilizations', 'Treasure Hunt', 'Motivation', 'AI News', 'Weird Facts']
const LANGUAGES = ['English', 'Telugu', 'Hindi', 'Tamil', 'Kannada']
const THEMES = ['Documentary', 'Cinematic', 'Dark Mystery', 'Historical', 'Sci-Fi', 'Adventure', 'Educational', 'News Style']
const VOICE_TYPES = ['Male', 'Female', 'Deep Documentary', 'Young Voice']
const DURATIONS = ['30 seconds', '45 seconds', '60 seconds']
const PLATFORMS = ['Instagram Reel', 'YouTube Shorts', 'TikTok']
function GeneratorForm({ onGenerate, loading }) {
const [formData, setFormData] = useState({
topic: TOPICS[0],
language: LANGUAGES[0],
theme: THEMES[0],
voice_type: VOICE_TYPES[0],
duration: DURATIONS[0],
platform: PLATFORMS[0],
})
const handleChange = (field, value) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
const handleSubmit = (e) => {
e.preventDefault()
onGenerate(formData)
}
const fields = [
{ key: 'topic', label: 'Topic', options: TOPICS },
{ key: 'language', label: 'Language', options: LANGUAGES },
{ key: 'theme', label: 'Video Theme', options: THEMES },
{ key: 'voice_type', label: 'Voice Type', options: VOICE_TYPES },
{ key: 'duration', label: 'Duration', options: DURATIONS },
{ key: 'platform', label: 'Platform', options: PLATFORMS },
]
return (
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<h2 className="text-lg font-bold mb-4">Generate Content</h2>
<div className="space-y-4">
{fields.map(({ key, label, options }) => (
<div key={key}>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{label}
</label>
<select
value={formData[key]}
onChange={(e) => handleChange(key, e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-purple-500 focus:border-transparent outline-none transition-colors"
>
{options.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
))}
</div>
<button
type="submit"
disabled={loading}
className="w-full mt-6 px-6 py-3 bg-gradient-to-r from-purple-600 to-pink-600 text-white font-semibold rounded-lg hover:from-purple-700 hover:to-pink-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<span className="animate-spin">⌛</span> Generating...
</span>
) : (
'🚀 Generate Content'
)}
</button>
</form>
)
}
export default GeneratorForm
Your content generation form is ready to accept user input. Next up, you'll build the ResultsPanel that displays the AI-generated content once it comes back from your backend.
Build the Results Display and Export
Your form is ready to collect user inputs and send them to the backend. Now the app needs somewhere to show the AI-generated content when it comes back.
In this step, you will build the ResultsPanel component. This component handles three states: a loading animation while the AI works, an empty placeholder before the first generation, and a full content display with export buttons once results arrive.
In this step, get ready to:
- Create the ResultsPanel component with organized content sections.
- Add Copy Script and Download JSON export buttons.
- Build loading and empty state transitions for smooth UX.
Create the ResultsPanel component with display sections
The ResultsPanel is the right half of your content generation view. It receives the AI response and renders each content section (title, narration, scenes, subtitles, thumbnail text, hashtags, and SEO description) in its own styled card.
- Create a new file called ResultsPanel.jsx inside frontend/src/components/.
- Copy the following code into the file. This is the main component function:
function ResultsPanel({ results, loading }) {
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text)
alert('Copied to clipboard!')
}
const downloadJSON = () => {
const blob = new Blob([JSON.stringify(results, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `reelforge-${Date.now()}.json`
a.click()
URL.revokeObjectURL(url)
}
if (loading) {
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="text-4xl animate-bounce mb-4">🎬</div>
<p className="text-gray-500 dark:text-gray-400">AI is crafting your content...</p>
</div>
</div>
)
}
if (!results) {
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="text-4xl mb-4">✨</div>
<p className="text-gray-500 dark:text-gray-400">Your generated content will appear here</p>
</div>
</div>
)
}
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm space-y-4 overflow-y-auto max-h-[calc(100vh-12rem)]">
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold">Generated Content</h2>
<div className="flex gap-2">
<button
onClick={() => copyToClipboard(results.narration)}
className="px-3 py-1.5 text-xs font-medium bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
📋 Copy Script
</button>
<button
onClick={downloadJSON}
className="px-3 py-1.5 text-xs font-medium bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
💾 Download JSON
</button>
</div>
</div>
<Section title="🎯 Title" content={results.title} />
<Section title="🎙️ Narration" content={results.narration} />
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<h3 className="text-sm font-semibold mb-2">🎥 Visual Scenes</h3>
<ol className="list-decimal list-inside space-y-1 text-sm text-gray-700 dark:text-gray-300">
{results.scenes.map((scene, i) => (
<li key={i}>{scene}</li>
))}
</ol>
</div>
<Section title="📝 Subtitles" content={results.subtitles} />
<Section title="🖼️ Thumbnail Text" content={results.thumbnail_text} />
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<h3 className="text-sm font-semibold mb-2"># Hashtags</h3>
<div className="flex flex-wrap gap-2">
{results.hashtags.map((tag, i) => (
<span key={i} className="px-2 py-1 text-xs bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded-full">
#{tag.replace('#', '')}
</span>
))}
</div>
</div>
<Section title="🔍 SEO Description" content={results.seo_description} />
</div>
)
}
What does this code do?
This component handles three visual states depending on the props it receives:
- When loading is true, it shows a bouncing animation with a status message.
- When results is null, it shows a placeholder telling the user where content will appear.
- When results exist, it renders each content section in styled cards with a header containing export buttons.
The copyToClipboard function uses the browser's clipboard API to copy the narration script. The downloadJSON function creates a downloadable file by building a Blob, generating a temporary URL, and programmatically clicking a hidden link.
Add the Section helper component
Several content sections (title, narration, subtitles, etc.) share the same card layout. A reusable helper component keeps the code clean.
- Below the ResultsPanel function's closing brace, add the Section helper and the export statement:
function Section({ title, content }) {
return (
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<h3 className="text-sm font-semibold mb-2">{title}</h3>
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{content}</p>
</div>
)
}
export default ResultsPanel
What does this helper do?
The Section component takes a title and content prop. It wraps the content in a rounded card with consistent padding and typography. The whitespace-pre-wrap class preserves line breaks in the AI-generated text.
- Save ResultsPanel.jsx.
- Switch back to your browser. You should see the empty state placeholder with a sparkle emoji and the text "Your generated content will appear here" on the right side of the page.
Don't see the results panel?
- Check that the file is saved as ResultsPanel.jsx (capital R, capital P) inside the components folder. The import in App.jsx expects this exact casing.
- If you see a blank screen or console errors, open your browser's DevTools console and check for a syntax error. A missing bracket or curly brace in the JSX will prevent the component from rendering.
Still stuck? Help me debug why my ResultsPanel component isn't showing up in the browser.
Test the full content generation flow
Your frontend and backend are both running. Time to generate your first AI content and see the full pipeline in action.
- In your browser at http://localhost:5173, make sure the Create Video view is active in the sidebar.
- Choose any options in the form dropdowns (or leave the defaults).
- Click the Generate Content button.
- Watch the right panel switch to the loading state with a bouncing movie emoji.
- After a few seconds, the AI-generated content appears in organized sections: Title, Narration, Visual Scenes, Subtitles, Thumbnail Text, Hashtags, and SEO Description.
What about the export buttons?
Once results appear, two buttons show in the top-right corner of the results panel:
- Copy Script copies the narration text to your clipboard so you can paste it into a teleprompter or script editor.
- Download JSON saves the entire content package as a .json file named reelforge-{timestamp}.json. This preserves all sections for later use.
- Click Copy Script and verify an alert says "Copied to clipboard!".
- Click Download JSON and verify a .json file downloads to your machine.
Generation failed or no results?
- If you see the alert "Failed to generate content", check that your backend is still running in its terminal. Look for any error messages there.
- Verify your ANTHROPIC_API_KEY environment variable is still set in the terminal where the backend runs. API keys do not persist across terminal sessions.
- If the backend shows a JSON parsing error, the AI response may have been malformed. Try generating again. This is rare but can happen.
Need more help? Help me debug why my ReelForge AI content generation is failing.
✔️ Awesome, I've got everything!
Great. Double check you have saved ResultsPanel.jsx and confirmed the end-to-end flow works.
ⓧ I'd like to double check the full code
Here is the complete ResultsPanel.jsx file:
function ResultsPanel({ results, loading }) {
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text)
alert('Copied to clipboard!')
}
const downloadJSON = () => {
const blob = new Blob([JSON.stringify(results, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `reelforge-${Date.now()}.json`
a.click()
URL.revokeObjectURL(url)
}
if (loading) {
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="text-4xl animate-bounce mb-4">🎬</div>
<p className="text-gray-500 dark:text-gray-400">AI is crafting your content...</p>
</div>
</div>
)
}
if (!results) {
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="text-4xl mb-4">✨</div>
<p className="text-gray-500 dark:text-gray-400">Your generated content will appear here</p>
</div>
</div>
)
}
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm space-y-4 overflow-y-auto max-h-[calc(100vh-12rem)]">
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold">Generated Content</h2>
<div className="flex gap-2">
<button
onClick={() => copyToClipboard(results.narration)}
className="px-3 py-1.5 text-xs font-medium bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
📋 Copy Script
</button>
<button
onClick={downloadJSON}
className="px-3 py-1.5 text-xs font-medium bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
💾 Download JSON
</button>
</div>
</div>
<Section title="🎯 Title" content={results.title} />
<Section title="🎙️ Narration" content={results.narration} />
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<h3 className="text-sm font-semibold mb-2">🎥 Visual Scenes</h3>
<ol className="list-decimal list-inside space-y-1 text-sm text-gray-700 dark:text-gray-300">
{results.scenes.map((scene, i) => (
<li key={i}>{scene}</li>
))}
</ol>
</div>
<Section title="📝 Subtitles" content={results.subtitles} />
<Section title="🖼️ Thumbnail Text" content={results.thumbnail_text} />
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<h3 className="text-sm font-semibold mb-2"># Hashtags</h3>
<div className="flex flex-wrap gap-2">
{results.hashtags.map((tag, i) => (
<span key={i} className="px-2 py-1 text-xs bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded-full">
#{tag.replace('#', '')}
</span>
))}
</div>
</div>
<Section title="🔍 SEO Description" content={results.seo_description} />
</div>
)
}
function Section({ title, content }) {
return (
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<h3 className="text-sm font-semibold mb-2">{title}</h3>
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{content}</p>
</div>
)
}
export default ResultsPanel
Your full-stack AI content generator is complete. The form collects inputs, the backend calls Claude AI, and the results panel displays everything in a polished layout with export options. Next up, you will tackle the Secret Mission and add persistent history.
Secret mission
Add Generation History with localStorage
Your app generates great content, but the moment you refresh the page it's gone forever. In this secret mission, you'll add a persistent history feature that saves every generation to localStorage and lets users revisit past creations from a dedicated History panel.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally, so there are no ongoing costs. The Anthropic API only charges when you actively make generation requests.
Resources you used:
- Frontend dev server (running on localhost:5173).
- Backend FastAPI server (running on localhost:8000).
- The frontend directory with React project and node_modules.
- The backend directory with Python virtual environment and FastAPI code.
- The ANTHROPIC_API_KEY environment variable in your terminal session.
- Browser localStorage data (reelforge-history).
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with ReelForge AI.
- Your frontend and backend servers will keep running until you close the terminal windows.
- Your generation history stays saved in your browser's localStorage.
- The Anthropic API only charges per request, so leaving the project idle costs nothing.
Pause - I'll come back to this later
Shut down running processes to free up memory, but keep all your files intact.
- Switch to the terminal running the frontend dev server and press Ctrl+C to stop it.
- Switch to the terminal running the backend server and press Ctrl+C to stop it.
- Your project files remain on disk. When you return, restart the servers with npm run dev (frontend) and fastapi dev (backend).
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Stop both running servers by pressing Ctrl+C in each terminal.
- Delete the project directories by running this command from your Desktop:
macOS
cd ~/Desktop
rm -rf frontend backend
Windows
cd ~\Desktop
Remove-Item -Recurse -Force frontend, backend
- Clear your browser's localStorage by opening DevTools (F12), going to the Application tab, selecting Local Storage, and deleting the reelforge-history entry.
- The ANTHROPIC_API_KEY environment variable only lives in your current terminal session. Closing the terminal removes it automatically.
- If you want to revoke the API key entirely, visit the Anthropic Console, go to API Keys, and delete the key you created for this project.
Nice Work!
Nice Work!
Congratulations! You just built ReelForge AI, a full-stack AI-powered content generation platform that turns a simple form submission into production-ready short-form video scripts.
Here's what you learned:
- Built a modern React frontend with Tailwind CSS featuring dark/light mode theming and responsive sidebar navigation.
- Created a FastAPI backend that integrates with Claude AI for intelligent, structured content generation.
- Implemented a complete content generation pipeline from form input to AI-structured output with export functionality (clipboard copy and JSON download).
- Added persistent generation history with localStorage so users can revisit past content across sessions.
Ready to quiz yourself?