AI Lesson Plan Assistant with Groq
Build a streaming AI lesson plan generator for higher education instructors.
Introduction
30 Second Summary
College instructors spend hours planning lessons that actually engage students and test real understanding. What if you could encode proven teaching strategies into an AI assistant that generates structured plans in seconds?
In this project, you will build a streaming AI lesson plan assistant using Next.js and Groq's free LLM API. An instructor fills out a form with their course details, clicks generate, and watches a pedagogically-sound lesson plan stream in real-time.
What You'll Build
A Next.js web app that uses the Vercel AI SDK and Groq's free inference API to generate structured lesson plans with real-time streaming.
By the end of this project, you'll have:
- A streaming API route that connects your Next.js app to Groq's free LLM inference for zero-cost lesson plan generation.
- A sophisticated system prompt encoding backward design, Bloom's taxonomy alignment, engagement scaffolding, and academic integrity-by-design principles.
- A responsive instructor input form with a real-time streaming output panel that displays the lesson plan as it generates.
- Secret Mission: Add structured output validation with Zod so the lesson plan returns as a typed JSON object with guaranteed sections.
Are there any prerequisites?
You should be familiar with JavaScript/TypeScript and React basics. You also need Node.js installed on your machine.
No prior experience with AI APIs or prompt engineering is required. This project walks you through everything from scratch.
Set Up the Project
Before you can build an AI-powered lesson plan assistant, you need a modern web app foundation. Next.js gives you server-side API routes (where your AI logic lives) and a React frontend (where instructors interact with the tool) in a single project.
In this step, you will scaffold a Next.js project, install the AI SDK packages that connect to Groq's free LLM API, and configure your API key.
In this step, get ready to:
- Create a new Next.js project with TypeScript and Tailwind CSS.
- Install the Vercel AI SDK and Groq provider packages.
- Obtain a free Groq API key and verify the dev server runs.
Create a new Next.js project
Next.js provides a scaffolding tool called create-next-app that generates the project structure, installs dependencies, and configures TypeScript and Tailwind CSS for you in one command.
- Open your terminal.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold your Next.js project by running:
npx create-next-app@latest lesson-plan-assistant
What will this command do?
This command downloads the latest Next.js project template and launches an interactive setup wizard. It will ask you a series of questions about how to configure your project.
- The first prompt asks whether to use recommended defaults. Select Yes, use recommended defaults by pressing Enter.
What do the recommended defaults include?
The recommended defaults enable TypeScript, ESLint, Tailwind CSS, and App Router. These are exactly the tools this project needs.
TypeScript adds type safety to your code. Tailwind CSS provides utility-first styling. The App Router is Next.js's modern routing system that supports server-side API routes.
Wait for the installation to finish. This may take a minute while npm downloads the packages.
- Move into your new project folder by running:
cd lesson-plan-assistant
- Confirm the project was created by running:
ls
You should see files like package.json, tsconfig.json, next.config.ts, and an app folder.
Command not found or download failed?
Make sure Node.js is installed by running node --version in your terminal. You need Node.js 18 or higher for Next.js. If npx hangs, check your internet connection and try again.
help me troubleshoot my create-next-app installation
Install the AI SDK dependencies
The Vercel AI SDK is a toolkit that handles streaming LLM responses, client-side chat state, and structured output validation. You will install the core SDK, the Groq provider, the React hooks package, and Zod for schema validation.
- Install the four packages by running:
npm i ai @ai-sdk/groq @ai-sdk/react zod
What are these packages?
- ai is the core Vercel AI SDK. It provides streamText and generateObject for calling LLMs.
- @ai-sdk/groq is the Groq provider. It connects the AI SDK to Groq's free, fast inference API.
- @ai-sdk/react provides the useChat hook for managing chat state in React components.
- zod is a schema validation library. You will use it in the Secret Mission for structured outputs.
- Confirm the install succeeded by checking that no errors appeared in your terminal output.
Seeing permission errors or EACCES?
On macOS, try prefixing the command with sudo. On Windows, open PowerShell as Administrator. If you see peer dependency warnings, those are safe to ignore for this project.
help me fix npm install errors
Get a Groq API key and start the dev server
Groq provides free LLM inference with generous rate limits. You will sign up, generate an API key, and store it in an environment file that Next.js reads automatically.
- Open your browser and go to console.groq.com.
- Sign up for a free account (or log in if you already have one).
- Navigate to the API Keys section in the left sidebar.
- Click Create API Key.
- Give it a name like lesson-plan-assistant and click Submit.
- Copy the API key immediately. You will not be able to see it again after closing this dialog.
Is this free?
Groq's free tier never charges you. If you exceed the rate limits (30 requests per minute, 1,000 requests per day), the API returns a 429 error instead of billing you. For a lesson plan tool generating one plan at a time, you will stay well within these limits.
- Back in your terminal, create the environment file by running:
touch .env.local
- Open your project in your code editor.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code (or your preferred editor) and press Enter to open it.
- Open the lesson-plan-assistant folder from your Desktop in the editor.
- Open the .env.local file and paste the following, replacing the placeholder with the key you just copied:
GROQ_API_KEY=your-api-key-here
What does .env.local do?
Next.js automatically loads environment variables from .env.local into your server-side code. The @ai-sdk/groq package reads the GROQ_API_KEY variable automatically. You never need to pass it manually in your code.
- Save .env.local.
- Start the development server by running:
npm run dev
- Open your browser and go to http://localhost:3000.
You should see the default Next.js welcome page. This confirms your project is scaffolded correctly and the dev server is running.
Page not loading at localhost:3000?
Make sure npm run dev is still running in your terminal. If you see a port-in-use error, another process is using port 3000. Close it or use a different port by running npx next dev -p 3001.
help me get my Next.js dev server running
✔️ Awesome, I've got everything!
Great. Double check you have saved .env.local and that your dev server is running at localhost:3000.
ⓧ I'd like to double check the full code
Your .env.local file should look like this (with your actual Groq API key in place of the placeholder):
GROQ_API_KEY=your-api-key-here
Your project foundation is ready. Next up, you will write a sophisticated system prompt that encodes pedagogical best practices into every lesson plan the AI generates.
Engineer the System Prompt
Your project is scaffolded and your dependencies are installed. Now it's time to write the brain of your application.
The system prompt is the most important piece of this project. It tells the AI exactly how to generate lesson plans, encoding pedagogical best practices so that every plan follows a research-backed structure. You'll build it in three layers: backward design, engagement scaffolding, and academic integrity.
In this step, get ready to:
- Write a system prompt encoding backward design and Bloom's taxonomy alignment.
- Add engagement scaffolding with formative assessment checkpoints every 15-20 minutes.
- Add academic integrity-by-design principles and an AI-use policy section.
Write the backward design foundation
Backward design is a curriculum framework by Wiggins and McTighe. Instead of starting with activities and hoping they lead somewhere, you start with the desired outcomes, then figure out how to measure them, then design activities that prepare students.
Your system prompt will instruct the AI to follow this same sequence when generating lesson plans.
- In your code editor, create a new folder called lib inside your project root (the same level as your app folder).
- Inside lib, create a new file called system-prompt.ts.
- Paste the following code into lib/system-prompt.ts:
export const systemPrompt = `You are an expert higher education instructional designer and lesson plan architect. You create rigorous, structured lesson plans for college and university instructors.
DESIGN PHILOSOPHY:
You follow Backward Design (Wiggins & McTighe):
1. Start with desired learning outcomes
2. Determine acceptable evidence (assessments)
3. Plan learning experiences and instruction
OUTPUT STRUCTURE:
Generate lesson plans in the following markdown format:
# [Lesson Title]
## Learning Objectives (Bloom's Taxonomy Aligned)
List 3-5 measurable objectives. Tag each with its Bloom's level in brackets.
Format: "Students will [verb] [content] [context]. [LEVEL: Remember|Understand|Apply|Analyze|Evaluate|Create]"
## Assessment Strategy (Backward Design)
Describe how you will measure whether students achieved each objective. Prioritize:
- Process-based assessments (scaffolded deliverables, drafts, reflective artifacts)
- Performative assessments (oral defense, live demonstration, peer teaching)
- Multi-stage assessments (proposal → draft → revision → final)
Avoid relying solely on output-based written assessments that can be easily outsourced to AI.
What does this code do?
This exported constant is the beginning of a multi-line template literal string. The AI receives this as its "system" instructions before every user message.
- The DESIGN PHILOSOPHY section tells the AI to use backward design: outcomes first, then assessments, then activities.
- The Learning Objectives section requires each objective to be tagged with a Bloom's taxonomy level (Remember, Understand, Apply, Analyze, Evaluate, Create).
- The Assessment Strategy section pushes the AI toward process-based and performative assessments rather than simple written outputs.
What is Bloom's taxonomy?
Bloom's taxonomy is a hierarchy of cognitive skills used in education. It ranks thinking from simple recall (Remember) up through understanding, applying, analyzing, evaluating, and creating.
By tagging each learning objective with its Bloom's level, instructors can see at a glance whether their lesson targets higher-order thinking or stays at surface-level recall.
Don't close the file yet. You'll continue adding to this same template literal string in the next substep.
Add engagement scaffolding instructions
Student attention drops significantly after 15-20 minutes of passive listening. Engagement checkpoints are brief formative assessment moments that break up the lecture and give instructors real-time feedback on comprehension.
- In lib/system-prompt.ts, add the following directly below the Assessment Strategy section (immediately after the line about output-based assessments):
## Timed Activity Sequence
Create a minute-by-minute breakdown. For each activity include:
- Time allocation (e.g., "0:00-0:10")
- Activity type (Lecture, Discussion, Active Learning, Assessment, Transition)
- Description of what instructor and students do
- ENGAGEMENT CHECKPOINT: Insert a formative assessment moment every 15-20 minutes. Use varied techniques:
* Think-Pair-Share
* One-Minute Paper
* Muddiest Point
* Polling/Show of Hands
* Exit Ticket
* Concept Map Snippet
## Materials & Preparation
List what the instructor needs to prepare before class.
## Engagement Tracking Indicators
List observable signals that students are engaged or disengaged, with suggested interventions.
What does this section do?
- The Timed Activity Sequence forces the AI to produce a minute-by-minute plan, not vague time blocks. This makes the lesson immediately usable.
- The ENGAGEMENT CHECKPOINT instruction ensures the AI inserts a formative assessment technique every 15-20 minutes. Think-pair-share, muddiest point, and exit tickets are all low-stakes ways to check comprehension without formal grading.
- The Engagement Tracking Indicators section gives instructors observable signals (like eye contact, note-taking, side conversations) so they can adapt in real time.
Why varied techniques matter
Using the same engagement technique every time leads to fatigue. By listing six different options in the prompt, the AI rotates through them, giving instructors variety across their lesson.
You still have more to add. Keep the template literal open for the final section.
Add academic integrity and AI-use policy instructions
Research consistently shows that AI-detection tools are unreliable and biased. The evidence-based alternative is integrity by design: structuring assessments so that the process itself demonstrates learning, making AI shortcuts counterproductive rather than prohibited.
- In lib/system-prompt.ts, add the following directly below the Engagement Tracking Indicators section, then close the template literal:
## Academic Integrity by Design
Explain how this lesson's assessments are designed to preserve integrity through structure rather than surveillance:
- What makes the assessments resistant to AI-generated shortcuts
- How scaffolding ensures students demonstrate their own thinking process
- What oral, interactive, or reflective components verify understanding
## AI-Use Policy Recommendation
Provide a specific, tailored AI-use policy for this lesson that addresses:
- What AI use is authorized (e.g., brainstorming, grammar checking)
- What AI use is not authorized (e.g., generating final deliverables)
- How students should transparently report AI usage
- How originality is maintained (work must contain original components reflective of learning)
GUIDELINES:
- Adapt complexity and vocabulary to the specified academic level
- Scale engagement strategies to class size
- Ensure activities directly support the stated learning objectives
- Every assessment must map back to at least one learning objective
- Use active verbs from Bloom's taxonomy for objectives
- Be specific and actionable, not generic
`;
What does this final section do?
- The Academic Integrity by Design section tells the AI to explain why the assessments resist shortcuts. Process-based assessments (drafts, reflections, oral components) make the learning journey visible, so copying a final output is not enough.
- The AI-Use Policy Recommendation section generates a tailored policy specifying what AI use is authorized, what is not, and how students report usage transparently.
- The GUIDELINES section provides general rules the AI follows across every generation: adapting to academic level, scaling to class size, and mapping assessments back to objectives.
Why integrity by design instead of detection?
AI-detection tools have high false-positive rates, especially for non-native English speakers. They create adversarial dynamics between instructors and students.
The approach encoded in this prompt is different. It designs assessments where the process is the proof: scaffolded deliverables, oral defenses, and reflective artifacts make it structurally difficult to skip the learning. Students can use AI as a tool without it replacing their thinking.
- Save lib/system-prompt.ts.
- Confirm the file is saved by checking that the lib folder now appears in your editor's sidebar with system-prompt.ts inside it.
Don't see the lib folder?
- Make sure you created lib at the project root level (next to the app folder), not inside the app folder.
- Check that the filename is exactly system-prompt.ts (with a hyphen, not an underscore).
help me find where to create the lib folder in my Next.js project
✔️ Awesome, I've got everything!
Great. Double check you've saved your file.
ⓧ I'd like to double check the full code
Here is the complete lib/system-prompt.ts file:
export const systemPrompt = `You are an expert higher education instructional designer and lesson plan architect. You create rigorous, structured lesson plans for college and university instructors.
DESIGN PHILOSOPHY:
You follow Backward Design (Wiggins & McTighe):
1. Start with desired learning outcomes
2. Determine acceptable evidence (assessments)
3. Plan learning experiences and instruction
OUTPUT STRUCTURE:
Generate lesson plans in the following markdown format:
# [Lesson Title]
## Learning Objectives (Bloom's Taxonomy Aligned)
List 3-5 measurable objectives. Tag each with its Bloom's level in brackets.
Format: "Students will [verb] [content] [context]. [LEVEL: Remember|Understand|Apply|Analyze|Evaluate|Create]"
## Assessment Strategy (Backward Design)
Describe how you will measure whether students achieved each objective. Prioritize:
- Process-based assessments (scaffolded deliverables, drafts, reflective artifacts)
- Performative assessments (oral defense, live demonstration, peer teaching)
- Multi-stage assessments (proposal → draft → revision → final)
Avoid relying solely on output-based written assessments that can be easily outsourced to AI.
## Timed Activity Sequence
Create a minute-by-minute breakdown. For each activity include:
- Time allocation (e.g., "0:00-0:10")
- Activity type (Lecture, Discussion, Active Learning, Assessment, Transition)
- Description of what instructor and students do
- ENGAGEMENT CHECKPOINT: Insert a formative assessment moment every 15-20 minutes. Use varied techniques:
* Think-Pair-Share
* One-Minute Paper
* Muddiest Point
* Polling/Show of Hands
* Exit Ticket
* Concept Map Snippet
## Materials & Preparation
List what the instructor needs to prepare before class.
## Engagement Tracking Indicators
List observable signals that students are engaged or disengaged, with suggested interventions.
## Academic Integrity by Design
Explain how this lesson's assessments are designed to preserve integrity through structure rather than surveillance:
- What makes the assessments resistant to AI-generated shortcuts
- How scaffolding ensures students demonstrate their own thinking process
- What oral, interactive, or reflective components verify understanding
## AI-Use Policy Recommendation
Provide a specific, tailored AI-use policy for this lesson that addresses:
- What AI use is authorized (e.g., brainstorming, grammar checking)
- What AI use is not authorized (e.g., generating final deliverables)
- How students should transparently report AI usage
- How originality is maintained (work must contain original components reflective of learning)
GUIDELINES:
- Adapt complexity and vocabulary to the specified academic level
- Scale engagement strategies to class size
- Ensure activities directly support the stated learning objectives
- Every assessment must map back to at least one learning objective
- Use active verbs from Bloom's taxonomy for objectives
- Be specific and actionable, not generic
`;
Your system prompt is complete. It encodes an entire instructional design philosophy into a single string. Next up, you'll build the API route that feeds this prompt to the Groq LLM and streams the response back in real time.
Build the Streaming API Route
Your system prompt is ready to instruct the AI. Now you need a way for your web app to send instructor details to the Groq API and receive a streaming lesson plan back.
In the Next.js App Router, you create API endpoints by placing a route.ts file inside the app/api/ folder. This file handles incoming HTTP requests on the server side. You will use the Vercel AI SDK to stream the model's response token by token, so the instructor sees the lesson plan appear in real time.
In this step, get ready to:
- Create a POST route handler that receives form data from the frontend.
- Call Groq's LLM with your system prompt and stream the response back.
Create the streaming API route handler
The Next.js App Router uses folder-based routing. A file at app/api/chat/route.ts automatically becomes a live endpoint at /api/chat.
- In your code editor, create the folder path app/api/chat/ inside your project.
- Create a new file called route.ts inside that chat folder.
- Add the following code to app/api/chat/route.ts:
import { streamText } from 'ai';
import { groq } from '@ai-sdk/groq';
import { systemPrompt } from '@/lib/system-prompt';
// Allow up to 30 seconds for the model to generate the full lesson plan
export const maxDuration = 30;
export async function POST(req: Request) {
// Extract the messages array from the incoming request body
const { messages } = await req.json();
// Call Groq's LLM with our system prompt and the user's messages
const result = streamText({
model: groq('llama-3.3-70b-versatile'),
system: systemPrompt,
messages,
});
// Return the response as a streaming data stream
return result.toDataStreamResponse();
}
What does this code do?
This file exports a POST function, which Next.js automatically maps to HTTP POST requests at /api/chat. When the frontend sends form data, this handler processes it.
- streamText from the ai package sends the prompt to Groq and returns tokens as they arrive, rather than waiting for the entire response.
- groq('llama-3.3-70b-versatile') tells the AI SDK which model to use. This model supports both text streaming and structured outputs.
- system: systemPrompt injects your backward design instructions so every response follows your pedagogical framework.
- result.toDataStreamResponse() converts the streaming result into a format the Vercel AI SDK's frontend hooks can consume directly.
- Save app/api/chat/route.ts.
Why maxDuration is set to 30
By default, serverless functions on many hosting platforms time out after 10 seconds. A detailed lesson plan with all its sections can take longer to generate. Setting export const maxDuration = 30 tells the runtime to allow up to 30 seconds for the response to finish streaming.
Seeing a module not found error?
If your editor or terminal shows an error about @/lib/system-prompt, confirm that your lib/system-prompt.ts file is in the project root's lib folder (not inside app/). The @/ alias points to your project root.
Also check that you have ai and @ai-sdk/groq installed from the previous step. If not, run npm install again.
help me debug a module not found error in my Next.js API route
Verify the route is live
Your dev server automatically picks up new files in the app/ directory. You can confirm the route exists by checking for compile errors.
- Switch back to the terminal where your dev server is running.
- Check the terminal output. You should see the page recompile with no errors.
Why not test the route directly yet?
The /api/chat endpoint expects a JSON body with a messages array in a specific format. You will build the frontend form in the next step, which sends data in exactly the right shape. For now, a clean compile confirms the route is wired up correctly.
✔️ Awesome, I've got everything!
Great. Double check you have saved app/api/chat/route.ts and that your terminal shows no compile errors.
ⓧ I'd like to double check the full code
Here is the complete app/api/chat/route.ts file:
import { streamText } from 'ai';
import { groq } from '@ai-sdk/groq';
import { systemPrompt } from '@/lib/system-prompt';
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: groq('llama-3.3-70b-versatile'),
system: systemPrompt,
messages,
});
return result.toDataStreamResponse();
}
Your API route is live and ready to accept requests. Next up, you will build the instructor input form that sends course details to this endpoint and displays the streaming lesson plan in real time.
Build the Instructor Input Form
Your API route is ready to receive messages and stream back lesson plans. Now you need the front end that instructors actually interact with.
In this step, you will build a responsive input form where instructors enter their course details, wire it up to the streaming API, and display the AI-generated lesson plan in real time.
In this step, get ready to:
- Create the page component with a six-field instructor form.
- Connect the form to your streaming API using the useChat hook.
- Build a two-panel layout that streams the lesson plan as it generates.
Create the page component and build the form
The main page component handles everything the instructor sees: the input form, the submission logic, and the streaming output display. It uses the useChat hook from the Vercel AI SDK to manage the connection between the form and your API route.
- In your code editor, open app/layout.tsx (the file the scaffold created).
- Replace the entire contents of the file with this code:
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'AI Lesson Plan Assistant',
description:
'Generate structured, pedagogically-sound lesson plans for higher education',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
What does this layout do?
The root layout wraps every page in your app with the <html> and <body> tags. The metadata export sets the browser tab title and page description. The globals.css import loads Tailwind CSS utility classes for the entire app.
- Now create the main page file. Open app/page.tsx and replace its contents with the following code:
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export default function Home() {
// useChat manages messages, streaming state, and API communication
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat();
// Form state holds the six instructor input fields
const [formData, setFormData] = useState({
courseName: '',
level: 'introductory',
topic: '',
duration: '50',
classSize: '30',
learningGoals: '',
});
// Build a formatted message from the form fields and send it to the API
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
const userMessage = `Please create a lesson plan with the following details:
- Course: ${formData.courseName}
- Academic Level: ${formData.level}
- Topic: ${formData.topic}
- Class Duration: ${formData.duration} minutes
- Class Size: ${formData.classSize} students
- Key Learning Goals: ${formData.learningGoals}`;
handleInputChange({
target: { value: userMessage },
} as React.ChangeEvent<HTMLInputElement>);
setTimeout(() => {
const form = document.createElement('form');
const event = new Event('submit', { bubbles: true, cancelable: true });
handleSubmit(event as unknown as React.FormEvent<HTMLFormElement>);
}, 0);
};
return (
<main className="min-h-screen bg-gray-50">
<header className="bg-white border-b border-gray-200 px-6 py-4">
<h1 className="text-2xl font-bold text-gray-900">
AI Lesson Plan Assistant
</h1>
<p className="text-sm text-gray-600">
Generate structured, pedagogically-sound lesson plans for higher
education
</p>
</header>
<div className="flex flex-col lg:flex-row gap-6 p-6 max-w-7xl mx-auto">
<aside className="lg:w-1/3 w-full">
<form
onSubmit={handleFormSubmit}
className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 space-y-4"
>
<h2 className="text-lg font-semibold text-gray-800">
Course Details
</h2>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Course Name
</label>
<input
type="text"
value={formData.courseName}
onChange={(e) =>
setFormData({ ...formData, courseName: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
placeholder="e.g., Introduction to Psychology"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Academic Level
</label>
<select
value={formData.level}
onChange={(e) =>
setFormData({ ...formData, level: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
>
<option value="introductory">Introductory (100-level)</option>
<option value="intermediate">Intermediate (200-300 level)</option>
<option value="advanced">Advanced (400-level)</option>
<option value="graduate">Graduate</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Lesson Topic
</label>
<input
type="text"
value={formData.topic}
onChange={(e) =>
setFormData({ ...formData, topic: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
placeholder="e.g., Classical Conditioning"
required
/>
</div>
<div className="flex gap-4">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Duration (min)
</label>
<input
type="number"
value={formData.duration}
onChange={(e) =>
setFormData({ ...formData, duration: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
min="15"
max="180"
required
/>
</div>
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Class Size
</label>
<input
type="number"
value={formData.classSize}
onChange={(e) =>
setFormData({ ...formData, classSize: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
min="1"
max="500"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Key Learning Goals
</label>
<textarea
value={formData.learningGoals}
onChange={(e) =>
setFormData({ ...formData, learningGoals: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
rows={3}
placeholder="What should students know or be able to do after this lesson?"
required
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full bg-blue-600 text-white rounded-md px-4 py-2 text-sm font-medium hover:bg-blue-700 disabled:bg-blue-300 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? 'Generating...' : 'Generate Lesson Plan'}
</button>
</form>
</aside>
<section className="lg:w-2/3 w-full">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 min-h-[600px]">
<h2 className="text-lg font-semibold text-gray-800 mb-4">
Generated Lesson Plan
</h2>
{messages.length === 0 ? (
<div className="text-gray-400 text-center mt-20">
<p className="text-lg">Your lesson plan will appear here</p>
<p className="text-sm mt-2">
Fill out the form and click Generate to get started
</p>
</div>
) : (
<div className="prose prose-sm max-w-none">
{messages
.filter((m) => m.role === 'assistant')
.map((m) => (
<div key={m.id} className="whitespace-pre-wrap">
{m.content}
</div>
))}
</div>
)}
{isLoading && (
<div className="mt-4 flex items-center gap-2 text-blue-600">
<div className="w-2 h-2 bg-blue-600 rounded-full animate-pulse" />
<span className="text-sm">Generating lesson plan...</span>
</div>
)}
</div>
</section>
</div>
</main>
);
}
What does this component do?
This is a large component, so here is a breakdown of the key sections:
- 'use client' at the top marks this as a Client Component. Hooks like useState and useChat only work on the client side.
- formData is a state object holding six fields: course name, academic level, topic, duration, class size, and learning goals.
- The form renders labeled inputs for each field, including a dropdown (<select>) for academic level with four options.
- The submit button disables itself while the AI is generating, preventing duplicate requests.
- Save both app/layout.tsx and app/page.tsx.
Seeing a compilation error?
- Make sure the very first line of app/page.tsx is 'use client'; with no whitespace above it.
- Check that @ai-sdk/react is installed. If not, run npm i @ai-sdk/react in your terminal.
- Confirm app/layout.tsx imports './globals.css' (with the leading dot). A missing import causes Tailwind styles to disappear.
Help me fix a compilation error in my Next.js page component
Understand the useChat hook and form submission
The useChat hook from @ai-sdk/react does the heavy lifting for connecting your form to the streaming API route you built in the previous step.
How does useChat connect to the API?
By default, useChat sends POST requests to /api/chat. It automatically handles streaming, buffering, and updating the messages array as new tokens arrive from the server.
The handleFormSubmit function converts the six form fields into a single formatted string. It then uses handleInputChange to set that string as the message, and handleSubmit to trigger the API call. The setTimeout ensures the state update completes before submission fires.
Why format the form fields into one message?
Your API route expects a messages array (the standard chat format). By combining all six fields into a single user message, you can use the simple useChat pattern without needing a custom fetch call. The system prompt on the server side knows how to interpret this structured input.
Test the streaming response display
The right panel of the layout renders the AI-generated lesson plan as it streams in. Let's verify everything works end to end.
How does the streaming display work?
The component filters messages to show only the assistant's responses. Each message renders inside a whitespace-pre-wrap div, which preserves the markdown formatting (headings, bullets, line breaks) from the AI.
While isLoading is true, a pulsing blue dot appears below the content. On desktop, the form sits on the left (one-third width) and the output fills the right (two-thirds width). On mobile, they stack vertically.
- Switch back to your browser and navigate to http://localhost:3000.
- You should see the header "AI Lesson Plan Assistant" with the input form on the left and a placeholder message on the right.
- Fill in the form with test data. For example, enter Introduction to Psychology for the course name, select **Introductory (100-level)** for the level, and enter Classical Conditioning for the topic.
- Enter a learning goal such as Students will understand the basic principles of classical conditioning.
- Click **Generate Lesson Plan**.
- You should see the pulsing blue indicator appear, then the lesson plan text streams in on the right side with section headings, objectives, and a timed activity sequence.
Not seeing any response?
- Open the browser DevTools console (F12) and check for errors. A 401 response means your GROQ_API_KEY in .env.local is incorrect or missing.
- Make sure your dev server is still running. If you stopped it, run npm run dev again.
- A 404 on /api/chat means your route file is not at the correct path. It must be app/api/chat/route.ts.
Help me debug why my lesson plan is not streaming
✔️ Awesome, I've got everything!
Great. Double check you have saved both app/layout.tsx and app/page.tsx.
ⓧ I'd like to double check the full code
Here is the complete app/layout.tsx:
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'AI Lesson Plan Assistant',
description:
'Generate structured, pedagogically-sound lesson plans for higher education',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
And here is the complete app/page.tsx:
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export default function Home() {
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat();
const [formData, setFormData] = useState({
courseName: '',
level: 'introductory',
topic: '',
duration: '50',
classSize: '30',
learningGoals: '',
});
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
const userMessage = `Please create a lesson plan with the following details:
- Course: ${formData.courseName}
- Academic Level: ${formData.level}
- Topic: ${formData.topic}
- Class Duration: ${formData.duration} minutes
- Class Size: ${formData.classSize} students
- Key Learning Goals: ${formData.learningGoals}`;
handleInputChange({
target: { value: userMessage },
} as React.ChangeEvent<HTMLInputElement>);
setTimeout(() => {
const form = document.createElement('form');
const event = new Event('submit', { bubbles: true, cancelable: true });
handleSubmit(event as unknown as React.FormEvent<HTMLFormElement>);
}, 0);
};
return (
<main className="min-h-screen bg-gray-50">
<header className="bg-white border-b border-gray-200 px-6 py-4">
<h1 className="text-2xl font-bold text-gray-900">
AI Lesson Plan Assistant
</h1>
<p className="text-sm text-gray-600">
Generate structured, pedagogically-sound lesson plans for higher
education
</p>
</header>
<div className="flex flex-col lg:flex-row gap-6 p-6 max-w-7xl mx-auto">
<aside className="lg:w-1/3 w-full">
<form
onSubmit={handleFormSubmit}
className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 space-y-4"
>
<h2 className="text-lg font-semibold text-gray-800">
Course Details
</h2>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Course Name
</label>
<input
type="text"
value={formData.courseName}
onChange={(e) =>
setFormData({ ...formData, courseName: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
placeholder="e.g., Introduction to Psychology"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Academic Level
</label>
<select
value={formData.level}
onChange={(e) =>
setFormData({ ...formData, level: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
>
<option value="introductory">Introductory (100-level)</option>
<option value="intermediate">Intermediate (200-300 level)</option>
<option value="advanced">Advanced (400-level)</option>
<option value="graduate">Graduate</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Lesson Topic
</label>
<input
type="text"
value={formData.topic}
onChange={(e) =>
setFormData({ ...formData, topic: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
placeholder="e.g., Classical Conditioning"
required
/>
</div>
<div className="flex gap-4">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Duration (min)
</label>
<input
type="number"
value={formData.duration}
onChange={(e) =>
setFormData({ ...formData, duration: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
min="15"
max="180"
required
/>
</div>
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Class Size
</label>
<input
type="number"
value={formData.classSize}
onChange={(e) =>
setFormData({ ...formData, classSize: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
min="1"
max="500"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Key Learning Goals
</label>
<textarea
value={formData.learningGoals}
onChange={(e) =>
setFormData({ ...formData, learningGoals: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
rows={3}
placeholder="What should students know or be able to do after this lesson?"
required
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full bg-blue-600 text-white rounded-md px-4 py-2 text-sm font-medium hover:bg-blue-700 disabled:bg-blue-300 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? 'Generating...' : 'Generate Lesson Plan'}
</button>
</form>
</aside>
<section className="lg:w-2/3 w-full">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 min-h-[600px]">
<h2 className="text-lg font-semibold text-gray-800 mb-4">
Generated Lesson Plan
</h2>
{messages.length === 0 ? (
<div className="text-gray-400 text-center mt-20">
<p className="text-lg">Your lesson plan will appear here</p>
<p className="text-sm mt-2">
Fill out the form and click Generate to get started
</p>
</div>
) : (
<div className="prose prose-sm max-w-none">
{messages
.filter((m) => m.role === 'assistant')
.map((m) => (
<div key={m.id} className="whitespace-pre-wrap">
{m.content}
</div>
))}
</div>
)}
{isLoading && (
<div className="mt-4 flex items-center gap-2 text-blue-600">
<div className="w-2 h-2 bg-blue-600 rounded-full animate-pulse" />
<span className="text-sm">Generating lesson plan...</span>
</div>
)}
</div>
</section>
</div>
</main>
);
}
Your lesson plan assistant is fully functional. You have a streaming AI app that takes instructor input and generates detailed, pedagogically-sound lesson plans in real time. In the Secret Mission, you will take this further by adding structured output validation with Zod so the AI returns a guaranteed typed JSON object.
Secret mission
Add Structured Output Validation with Zod
Your streaming lesson plans work great, but what if you need guaranteed structure? In this secret mission, you'll add a Zod schema that forces the AI to return a typed JSON object with exact fields for objectives, activities, assessments, and policies. Then you'll build a toggle that switches between free-form streaming and beautifully rendered structured cards.
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 with the Groq free tier, so there are no ongoing costs.
Resources you used:
- A local Next.js project folder on your Desktop.
- A Groq API key (free tier, never incurs charges).
Keep everything running
No action needed. Choose this if you are still actively building or want to keep generating lesson plans.
- Your project files stay on disk and the dev server can be restarted anytime with npm run dev.
- The Groq free tier has no time limit. Your API key remains active indefinitely.
Pause - I'll come back to this later
Shut down running processes to free up system resources. Your project files remain on disk for next time.
- In your terminal, press Ctrl+C to stop the Next.js dev server.
- Your project folder and .env.local file remain on your Desktop. Run npm run dev from the project folder whenever you want to resume.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- In your terminal, press Ctrl+C to stop the dev server if it is still running.
- Delete the project folder by navigating to your Desktop and running:
macOS
cd ~/Desktop
rm -rf lesson-plan-assistant
Windows
cd ~\Desktop
rmdir /s /q lesson-plan-assistant
If you named your project folder something different, replace lesson-plan-assistant with your folder's actual name.
- To delete your Groq API key, go to console.groq.com, open your API keys page, and click the delete button next to the key you created for this project.
Nice Work!
Nice Work!
You just built a fully functional AI lesson plan assistant that generates pedagogically-sound, structured lesson plans in real time. That is prompt engineering, full-stack development, and domain expertise all working together in one app.
Here's what you accomplished:
- Built a streaming AI lesson plan assistant using Next.js and the Groq API, delivering real-time output as the model generates each section.
- Engineered a sophisticated system prompt encoding backward design, Bloom's taxonomy alignment, engagement scaffolding, and academic integrity-by-design principles.
- Integrated the Vercel AI SDK with Groq's free tier for zero-cost LLM inference with streaming responses and a responsive two-panel UI.
- Secret Mission: Added structured output validation with Zod for guaranteed, typed lesson plan generation using generateObject and a custom schema.
Ready to quiz yourself?