AI-Powered Flashcard Application With Next.js, Clerk, Firebase, Material UI, and LLaMA 3.1
Create flashcards: Next.js for the front end, Clerk for user authentication, Firebase for storage, Material UI for an interface, and LLaMA 3.1 for generation.
Join the DZone community and get the full member experience.
Join For FreeFlashcards have long been used as an effective tool for learning by providing quick, repeatable questions that help users memorize facts or concepts. Traditionally, flashcards contain a question on one side and the answer on the other. The concept is simple, yet powerful for retention, whether you're learning languages, mathematics, or any subject.
An AI-powered flashcard game takes this learning method to the next level. Rather than relying on static content, AI dynamically generates new questions and answers based on user input, learning patterns, and performance over time. This personalization makes the learning process more interactive and adaptive, providing questions that target specific areas where the user needs improvement.
In this tutorial, we'll use LLaMA 3.1, a powerful open-source large language model, to create dynamic flashcards. The AI engine will generate new questions and answers in real time based on the subject matter or keywords the user provides. This enhances the learning experience by making the flashcards more versatile, personalized, and efficient.
Setting Up the Environment for Development
We need to set up our working environment before we start writing code for our flashcard app.
1. Install Node.js and npm
The first step is to install Node.js and npm. Go to the Node.js website and get the Long-Term Support version for your computer's running system. Follow the steps given for installation.
2. Making a Project With Next.js
Start up your terminal and go to the location where you want to make your project. After that, run these commands:
npx create-next-app@latest flash-card-app
(With the@latest
flag, npm gets the most recent version of the Next.js starting setup.)cd flash-card-app
It will make a new Next.js project and take you to its path. You'll be given a number of configuration choices during the setup process, set them as given below:
- Would you like to use TypeScript? No
- Would you like to use ESLint? Yes
- Would you like to use Tailwind CSS? No
- Would you like to use the src/ directory? No
- Would you like to use App Router? Yes
- Would you like to customize the default import alias? No
3. Installing Firebase and Material-UI
In the directory of your project, execute the following command: npm install @mui/material @emotion/react @emotion/styled firebase
.
Setting Up Firebase
- Launch a new project on the Firebase Console.
- Click "Add app" after your project has been built, then choose the web platform (</>).
- Give your app a name when you register it, such as "flash-card-app”.
- Make a copy of the Firebase setup file. Afterwards, this will be useful.
4. Create a Firebase Configuration File
Make a new file called firebase.js in the root directory of your project and add the following code, replacing the placeholders with the real Firebase settings for your project:
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT_ID.appspot.com",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
export const auth = getAuth(app);
export const db = getFirestore(app);
How to Create an API Token in OpenRouter
We will use the free version of LLaMA 3.1 from OpenRouter and for that, we need to get the API token. Below are the steps to get one:
Step 1: Sign Up or Log In to OpenRouter
- Visit OpenRouter's official website.
- Create an account if you don’t have one. You can either sign up with your email or use an OAuth provider like Google, GitHub, or others.
- Log in to your OpenRouter account if you already have one.
Step 2: Navigate to API Key Settings
- Once you are logged in, go to the Dashboard.
- In the dashboard, look for the API or Developer Tools section.
- Click on the API Keys or Tokens option.
Step 3: Generate a New API Key
- In the API Keys section, you should see a button or link to Generate New API Key.
- Click on the Generate button to create a new API key.
- You may be asked to give your API key a name. This helps you organize your keys if you have multiple API keys for different projects (e.g., "Flashcard App Key").
Step 4: Copy the API Key
- Once the API key is generated, it will be displayed on the screen. Copy the API key immediately, as some services may not show it again after you leave the page.
- Store the API key securely in your environment configuration file (e.g.,
.env.local
).
Step 5: Add API Key to .env.local File
- In your Next.js project, open the
.env.local
file (if you don't have one, create it). - Add the following line:
OPENROUTER_API_KEY=your-generated-api-key-here
.
Make sure to replace your-generated-api-key-here
with the actual API key you copied.
Step 6: Use the API Key in Your Application
Building the Core Logic to Import LLaMa 3.1 for Creating Flashcards
Create a new file under the app folder with the name route.js
and follow the code given below:
import { NextResponse } from "next/server";
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
const systemPrompt = `
You are an AI flashcard creator. Your task is to generate concise and effective flashcards based on the given topic or content. Follow these guidelines:
1. Create clear and concise questions for the front of the flashcard.
2. Provide accurate and informative answers for the back of the flashcard, ensuring they do not exceed one or two sentences.
3. Ensure that each flashcard focuses on a single concept or piece of information.
4. Use simple language to make the flashcards accessible to a wide range of learners.
5. Include a variety of question types, such as definitions, examples, comparisons, and applications.
6. Avoid overly complex or ambiguous phrasing in both questions and answers.
7. When appropriate, use mnemonics or memory aids to help reinforce the information.
8. Tailor the difficulty level of the flashcards to the user's specified preferences.
9. If given a body of text, extract the most important and relevant information for the flashcards.
10. Aim to create a balanced set of flashcards that covers the topic comprehensively.
11. Only generate 10 flashcards.
Return in the following JSON format:
{
"flashcards": [{
"front": str,
"back": str
}]
}
Remember, the goal is to facilitate effective learning and retention of information through these flashcards.
`;
export async function POST(req) {
const data = await req.text(); // Get the raw text from the request
try {
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${OPENROUTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "meta-llama/llama-3.1-8b-instruct",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: data }
],
})
});
if (!response.ok) {
throw new Error(`Failed to fetch from OpenRouter AI: ${response.statusText}`);
}
const completion = await response.json();
// Extracting JSON from the response content
const rawJson = completion.choices[0].message.content;
const startIndex = rawJson.indexOf('{');
const endIndex = rawJson.lastIndexOf('}') + 1;
const jsonString = rawJson.substring(startIndex, endIndex);
const flashcardsData = JSON.parse(jsonString);
// Assuming flashcardsData contains the "flashcards" array directly
return NextResponse.json({ flashcards: flashcardsData.flashcards });
} catch (error) {
console.error("Error processing request:", error);
return new Response("Error processing request", { status: 500 });
}
}
The code works by receiving a POST
request from the client and extracting the raw text input using req.text()
. It then sends a POST
request to the OpenRouter API with a system prompt that outlines how LLaMA 3.1 should generate the flashcards. The response, containing the flashcards in JSON format, is parsed and returned to the client. In case of an error during the API call or processing, the error is logged, and a 500 response is returned to the client.
Building the Core Components for the Flash Card Application Sign In and Sign Up Using Clerk
Step 1: Set Up Your Clerk Account
- Sign up for Clerk: Go to Clerk.dev and create an account if you don’t already have one.
- Create an application:
- Once logged in, navigate to the Clerk Dashboard and create a new application.
- This application will be used for your flashcard app’s authentication system.
- Retrieve API keys: In your Clerk dashboard, you will find two keys: Frontend API Key and Secret Key. You will use these in your Next.js project for Clerk integration.
Step 2: Install Clerk SDK in Your Next.js Project
Run the following command to install Clerk's Next.js SDK: npm install @clerk/nextjs
.
Step 3: Set Up Environment Variables
To securely store your Clerk credentials, add them to your .env.local file. Create this file if it doesn't exist:
NEXT_PUBLIC_CLERK_FRONTEND_API=your-frontend-api-key
CLERK_API_KEY=your-secret-api-key
Replace your-frontend-api-key
and your-secret-api-key
with the actual values from the Clerk dashboard.
Step 4: Building Sign-In Components
"use client";
import { AppBar, Container, Typography, Box, Toolbar, Button } from "@mui/material";
import { useRouter } from 'next/navigation';
import { SignIn } from "@clerk/nextjs";
export default function LoginPage() {
const router = useRouter();
const handleHomeClick = () => {
router.push('/');
};
return (
<Container maxWidth="sm">
<AppBar position="static" sx={{ mb: 4 }}>
<Toolbar>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Learn in a Flash
</Typography>
<Button color="inherit" onClick={handleHomeClick}>
Home
</Button>
</Toolbar>
</AppBar>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
mt: 4,
p: 3,
border: 1,
borderColor: 'grey.300',
borderRadius: 2,
}}
>
<Typography variant="h4" gutterBottom>
Login to Your Account
</Typography>
<SignIn />
</Box>
</Container>
);
}
Step 5: Building Sign-Up Components
"use client";
import { AppBar, Container, Typography, TextField, Button, Box, Toolbar } from "@mui/material";
import { useRouter } from 'next/navigation';
export default function SignUpPage() {
const router = useRouter();
const handleHomeClick = () => {
router.push('/');
};
const handleLoginClick = () => {
router.push('/sign-in'); // Ensure the leading slash for routing
};
return (
<Container maxWidth="sm">
<AppBar position="static" sx={{ mb: 4 }}>
<Toolbar>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Learn in a Flash
</Typography>
<Button color="inherit" onClick={handleHomeClick}>
Home
</Button>
<Button color="inherit" onClick={handleLoginClick}>
Login
</Button>
</Toolbar>
</AppBar>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
mt: 4,
p: 3,
border: 1,
borderColor: 'grey.300',
borderRadius: 2,
}}
>
<Typography variant="h4" gutterBottom>
Create an Account
</Typography>
<form noValidate autoComplete="off">
<TextField
label="Email"
variant="outlined"
fullWidth
margin="normal"
type="email"
required
/>
<TextField
label="Password"
variant="outlined"
fullWidth
margin="normal"
type="password"
required
/>
<Button variant="contained" color="primary" fullWidth sx={{ mt: 2 }}>
Sign Up
</Button>
</form>
</Box>
</Container>
);
}
Creating Flashcard Generation Frontend Component
1. Setting Up Clerk for User Authentication
In this part, we utilize Clerk’s useUser()
hook to manage user authentication. This helps identify whether the user is logged in and provides access to the user’s data, which is crucial for associating flashcards with the correct user.
import { useUser } from "@clerk/nextjs";
export default function Generate() {
const { isLoaded, isSignedIn, user } = useUser();
// Other code will be placed below this
}
Notes:
isLoaded
: Checks if the user data is fully loadedisSignedIn
: Checks if the user is signed inuser
: Contains the user's data if they are authenticated
2. Managing Flashcard States
Here, we define the state variables using React’s useState
to handle the flashcards, their flipped state, user input, and dialog management for saving the flashcards.
const [flashcards, setFlashcards] = useState([]); // Stores the generated flashcards
const [flipped, setFlipped] = useState({}); // Keeps track of which flashcards are flipped
const [text, setText] = useState(""); // User input for generating flashcards
const [name, setName] = useState(""); // Name for the flashcard collection
const [open, setOpen] = useState(false); // Dialog state for saving flashcards
Notes:
flashcards
: Array to hold generated flashcardsflipped
: Object to track whether each flashcard is flippedtext
: Stores the text input from the user to generate flashcardsname
: Stores the name for the flashcard collectionopen
: Manages the dialog box visibility for saving flashcards
3. Submitting User Input to Generate Flashcards
This function handles sending the input text to an API to generate flashcards and updates the flashcards
state based on the API response.
const handleSubmit = async () => {
try {
const response = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }), // Sends the input text to the API
});
if (!response.ok) {
throw new Error("Failed to fetch flashcards");
}
const data = await response.json(); // Extracts the response data
if (data && data.flashcards) {
setFlashcards(data.flashcards); // Updates the flashcards state with the generated flashcards
}
} catch (error) {
console.error("Error generating flashcards:", error);
}
};
Notes:
- Sends a
POST
request to/api/generate
with the user’s input text - The server returns generated flashcards, which are then set in the
flashcards
state.
4. Handling Flashcard Flip on Click
This function allows users to click on a flashcard to "flip" it, revealing either the front or back of the card.
const handleCardClick = (index) => {
setFlipped((prev) => ({
...prev,
[index]: !prev[index], // Toggles the flipped state of the flashcard at the given index
}));
};
Notes:
- When a card is clicked, the
flipped
state is toggled for the respective card index, switching between showing the front and back.
5. Opening and Closing the Save Dialog
Here, the functions manage the dialog’s visibility. The user can open the dialog to save flashcards and close it when finished.
const handleOpen = () => {
setOpen(true); // Opens the dialog
};
const handleClose = () => {
setOpen(false); // Closes the dialog
};
Notes:
handleOpen
: Opens the save dialog boxhandleClose
: Closes the save dialog box
6. Saving Flashcards to Firebase
This function saves the generated flashcards into Firebase Firestore under the current user's collection, ensuring that each flashcard set is uniquely associated with the user.
const saveFlashcards = async () => {
if (!name) {
alert("Please enter a name");
return;
}
const batch = writeBatch(db); // Firestore batch for atomic writes
const userDocRef = doc(collection(db, "users"), user.id); // User document reference
const docSnap = await getDoc(userDocRef);
if (docSnap.exists()) {
const collectionData = docSnap.data().flashcards || [];
if (collectionData.find((f) => f.name === name)) {
alert("Flashcard with this name already exists.");
return;
} else {
collectionData.push({ name }); // Add the new flashcard collection name
batch.set(userDocRef, { flashcards: collectionData }, { merge: true });
}
} else {
batch.set(userDocRef, { flashcards: [{ name }] }); // Create a new user document if it doesn't exist
}
const colRef = collection(userDocRef, name); // Reference to the flashcard collection
flashcards.forEach((flashcard) => {
const cardDocRef = doc(colRef); // Create a document for each flashcard
batch.set(cardDocRef, flashcard); // Save each flashcard
});
await batch.commit(); // Commit the batch
handleClose();
router.push("/flashcards"); // Redirect to the flashcards page after saving
};
Notes:
- Checks if the user has entered a name for the flashcard collection
- Uses Firestore batch writes to ensure all flashcards are saved atomically
- Saves the flashcards under the user's document and collection in Firestore
7. Rendering the User Interface
This is the main part of the JSX, which handles the form for entering text, displays the flashcards, and renders the save dialog.
return (
<Container maxWidth="md">
<Box sx={{ mt: 4, mb: 6, display: "flex", flexDirection: "column", alignItems: "center" }}>
<TextField
label="Enter Text"
variant="outlined"
fullWidth
margin="normal"
value={text}
onChange={(e) => setText(e.target.value)} // Update the text state on input
/>
<Button variant="contained" onClick={handleSubmit}>
Generate Flashcards
</Button>
</Box>
{flashcards.length > 0 && (
<Box sx={{ mt: 4 }}>
<Typography variant="h5" align="center" gutterBottom>
Flashcard Preview
</Typography>
<Grid container spacing={3}>
{flashcards.map((flashcard, index) => (
<Grid item xs={12} sm={6} md={4} key={index}>
<Card onClick={() => handleCardClick(index)}>
<CardActionArea>
<CardContent>
<Typography variant="h6">
{flipped[index] ? flashcard.back : flashcard.front}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
<Box sx={{ mt: 4, display: "flex", justifyContent: "center" }}>
<Button variant="contained" color="secondary" onClick={handleOpen}>
Save
</Button>
</Box>
</Box>
)}
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Save the Flashcards</DialogTitle>
<DialogContent>
<DialogContentText>
Please enter a name for your Flashcard's Collection
</DialogContentText>
<TextField
autoFocus
margin="dense"
label="Collection Name"
type="text"
fullWidth
value={name}
onChange={(e) => setName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={saveFlashcards}>Save</Button>
</DialogActions>
</Dialog>
</Container>
);
Notes:
- This renders the form for entering text and generating flashcards.
- It also handles the rendering of generated flashcards with flip functionality and includes a dialog to save the flashcards to Firebase Firestore.
Sample Look of the Frontend Screen After Creation
Conclusion
This wraps up the creation of our flashcard application. In this example, I have utilized the LLaMA 3.1 language model, but feel free to experiment with any other model of your choice.
Happy coding!
Opinions expressed by DZone contributors are their own.
Comments