DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • How to Build an OpenAI Custom GPT With a Third-Party API
  • Build an AI Chatroom With ChatGPT and ZK by Asking It How!
  • Why Mocking Sucks
  • GenAI: From Prompt to Production

Trending

  • Using Python Libraries in Java
  • Teradata Performance and Skew Prevention Tips
  • Beyond Linguistics: Real-Time Domain Event Mapping with WebSocket and Spring Boot
  • How AI Agents Are Transforming Enterprise Automation Architecture
  1. DZone
  2. Data Engineering
  3. AI/ML
  4. AI-Powered Flashcard Application With Next.js, Clerk, Firebase, Material UI, and LLaMA 3.1

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.

By 
Vaibhavi Tiwari user avatar
Vaibhavi Tiwari
·
Nov. 04, 24 · Tutorial
Likes (7)
Comment
Save
Tweet
Share
14.0K Views

Join the DZone community and get the full member experience.

Join For Free

Flashcards 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:

JavaScript
 
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

  1. Visit OpenRouter's official website.
  2. 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.
  3. Log in to your OpenRouter account if you already have one.

Step 2: Navigate to API Key Settings

  1. Once you are logged in, go to the Dashboard.
  2. In the dashboard, look for the API or Developer Tools section.
  3. Click on the API Keys or Tokens option.

Step 3: Generate a New API Key

  1. In the API Keys section, you should see a button or link to Generate New API Key.
  2. Click on the Generate button to create a new API key.
  3. 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

  1. 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.
  2. Store the API key securely in your environment configuration file (e.g., .env.local).

Step 5: Add API Key to .env.local File

  1. In your Next.js project, open the .env.local file (if you don't have one, create it).
  2. 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:

JavaScript
 
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

  1. Sign up for Clerk: Go to Clerk.dev and create an account if you don’t already have one.
  2. 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.
  3. 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:

Plain Text
 
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

JavaScript
 
"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

JavaScript
 
"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.

TypeScript
 
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 loaded
  • isSignedIn: Checks if the user is signed in
  • user: 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.

TypeScript
 
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 flashcards
  • flipped: Object to track whether each flashcard is flipped
  • text: Stores the text input from the user to generate flashcards
  • name: Stores the name for the flashcard collection
  • open: 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.

TypeScript
 
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.

TypeScript
 
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.

TypeScript
 
const handleOpen = () => {
  setOpen(true);  // Opens the dialog
};

const handleClose = () => {
  setOpen(false);  // Closes the dialog
};


Notes:

  • handleOpen: Opens the save dialog box
  • handleClose: 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.

TypeScript
 
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.

TypeScript
 
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

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!

AI API Firebase Next.js UI

Opinions expressed by DZone contributors are their own.

Related

  • How to Build an OpenAI Custom GPT With a Third-Party API
  • Build an AI Chatroom With ChatGPT and ZK by Asking It How!
  • Why Mocking Sucks
  • GenAI: From Prompt to Production

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • [email protected]

Let's be friends: