Flashcards have lengthy been used as an efficient instrument for studying by offering fast, repeatable questions that assist customers memorize information or ideas. Historically, flashcards include a query on one aspect and the reply on the opposite. The idea is straightforward, but highly effective for retention, whether or not you are studying languages, arithmetic, or any topic.
An AI-powered flashcard sport takes this studying technique to the following degree. Moderately than counting on static content material, AI dynamically generates new questions and solutions based mostly on person enter, studying patterns, and efficiency over time. This personalization makes the educational course of extra interactive and adaptive, offering questions that focus on particular areas the place the person wants enchancment.
On this tutorial, we’ll use LLaMA 3.1, a strong open-source large language model, to create dynamic flashcards. The AI engine will generate new questions and solutions in actual time based mostly on the subject material or key phrases the person supplies. This enhances the educational expertise by making the flashcards extra versatile, customized, and environment friendly.
Setting Up the Surroundings for Growth
We have to arrange our working setting earlier than we begin writing code for our flashcard app.
1. Set up Node.js and npm
Step one is to put in Node.js and npm. Go to the Node.js website and get the Lengthy-Time period Assist model on your pc’s operating system. Observe the steps given for set up.
2. Making a Undertaking With Subsequent.js
Begin up your terminal and go to the placement the place you need to make your mission. After that, run these instructions:
npx create-next-app@newest flash-card-app
(With the@newest
flag, npm will get the newest model of the Subsequent.js beginning setup.)cd flash-card-app
It’s going to make a brand new Subsequent.js mission and take you to its path. You may be given a lot of configuration decisions throughout the setup course of, set them as given beneath:
- Would you want to make use of TypeScript? No
- Would you want to make use of ESLint? Sure
- Would you want to make use of Tailwind CSS? No
- Would you want to make use of the src/ listing? No
- Would you want to make use of App Router? Sure
- Would you wish to customise the default import alias? No
3. Putting in Firebase and Materials-UI
Within the listing of your mission, execute the next command: npm set up @mui/materials @emotion/react @emotion/styled firebase
.
Setting Up Firebase
- Launch a brand new mission on the Firebase Console.
- Click on “Add app” after your mission has been constructed, then select the online platform (</>).
- Give your app a reputation if you register it, akin to “flash-card-app”.
- Make a replica of the Firebase setup file. Afterwards, this shall be helpful.
4. Create a Firebase Configuration File
Make a brand new file referred to as firebase.js within the root listing of your mission and add the next code, changing the placeholders with the actual Firebase settings on your mission:
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);
The right way to Create an API Token in OpenRouter
We are going to use the free model of LLaMA 3.1 from OpenRouter and for that, we have to get the API token. Beneath are the steps to get one:
Step 1: Signal Up or Log In to OpenRouter
- Go to OpenRouter’s official website.
- Create an account should you don’t have one. You may both enroll together with your electronic mail or use an OAuth supplier like Google, GitHub, or others.
- Log in to your OpenRouter account if you have already got one.
Step 2: Navigate to API Key Settings
- As soon as you might be logged in, go to the Dashboard.
- Within the dashboard, search for the API or Developer Instruments part.
- Click on on the API Keys or Tokens choice.
Step 3: Generate a New API Key
- Within the API Keys part, you need to see a button or hyperlink to Generate New API Key.
- Click on on the Generate button to create a brand new API key.
- You might be requested to offer your API key a reputation. This helps you arrange your keys in case you have a number of API keys for various initiatives (e.g., “Flashcard App Key”).
Step 4: Copy the API Key
- As soon as the API secret is generated, will probably be displayed on the display screen. Copy the API key instantly, as some providers might not present it once more after you allow the web page.
- Retailer the API key securely in your setting configuration file (e.g.,
.env.native
).
Step 5: Add API Key to .env.native File
- In your Subsequent.js mission, open the
.env.native
file (if you do not have one, create it). - Add the next line:
OPENROUTER_API_KEY=your-generated-api-key-here
.
Make sure that to switch your-generated-api-key-here
with the precise API key you copied.
Step 6: Use the API Key in Your Software
Constructing the Core Logic to Import LLaMa 3.1 for Creating Flashcards
Create a brand new file beneath the app folder with the title route.js
and observe the code given beneath:
import { NextResponse } from "subsequent/server";
const OPENROUTER_API_KEY = course of.env.OPENROUTER_API_KEY;
const systemPrompt = `
You might be an AI flashcard creator. Your job is to generate concise and efficient flashcards based mostly on the given subject or content material. Observe these tips:
1. Create clear and concise questions for the entrance of the flashcard.
2. Present correct and informative solutions for the again of the flashcard, guaranteeing they don't exceed one or two sentences.
3. Be certain that every flashcard focuses on a single idea or piece of knowledge.
4. Use easy language to make the flashcards accessible to a variety of learners.
5. Embrace quite a lot of query sorts, akin to definitions, examples, comparisons, and purposes.
6. Keep away from overly complicated or ambiguous phrasing in each questions and solutions.
7. When acceptable, use mnemonics or reminiscence aids to assist reinforce the knowledge.
8. Tailor the issue degree of the flashcards to the person's specified preferences.
9. If given a physique of textual content, extract crucial and related data for the flashcards.
10. Goal to create a balanced set of flashcards that covers the subject comprehensively.
11. Solely generate 10 flashcards.
Return within the following JSON format:
{
"flashcards": [{
"front": str,
"back": str
}]
}
Keep in mind, the purpose is to facilitate efficient studying and retention of knowledge by way of these flashcards.
`;
export async perform POST(req) {
const knowledge = await req.textual content(); // Get the uncooked textual content from the request
strive {
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
technique: "POST",
headers: {
"Authorization": `Bearer ${OPENROUTER_API_KEY}`,
"Content material-Kind": "utility/json",
},
physique: JSON.stringify({
mannequin: "meta-llama/llama-3.1-8b-instruct",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: data }
],
})
});
if (!response.okay) {
throw new Error(`Did not fetch from OpenRouter AI: ${response.statusText}`);
}
const completion = await response.json();
// Extracting JSON from the response content material
const rawJson = completion.decisions[0].message.content material;
const startIndex = rawJson.indexOf('{');
const endIndex = rawJson.lastIndexOf('}') + 1;
const jsonString = rawJson.substring(startIndex, endIndex);
const flashcardsData = JSON.parse(jsonString);
// Assuming flashcardsData accommodates the "flashcards" array instantly
return NextResponse.json({ flashcards: flashcardsData.flashcards });
} catch (error) {
console.error("Error processing request:", error);
return new Response("Error processing request", { standing: 500 });
}
}
The code works by receiving a POST
request from the consumer and extracting the uncooked textual content enter utilizing req.textual content()
. It then sends a POST
request to the OpenRouter API with a system immediate that outlines how LLaMA 3.1 ought to generate the flashcards. The response, containing the flashcards in JSON format, is parsed and returned to the consumer. In case of an error throughout the API name or processing, the error is logged, and a 500 response is returned to the consumer.
Constructing the Core Parts for the Flash Card Software Signal In and Signal Up Utilizing Clerk
Step 1: Set Up Your Clerk Account
- Join Clerk: Go to Clerk.dev and create an account should you don’t have already got one.
- Create an utility:
- As soon as logged in, navigate to the Clerk Dashboard and create a brand new utility.
- This utility shall be used on your flashcard app’s authentication system.
- Retrieve API keys: In your Clerk dashboard, you can see two keys: Frontend API Key and Secret Key. You’ll use these in your Subsequent.js mission for Clerk integration.
Step 2: Set up Clerk SDK in Your Subsequent.js Undertaking
Run the next command to put in Clerk’s Subsequent.js SDK: npm set up @clerk/nextjs
.
Step 3: Set Up Surroundings Variables
To securely retailer your Clerk credentials, add them to your .env.native file. Create this file if it does not exist:
NEXT_PUBLIC_CLERK_FRONTEND_API=your-frontend-api-key
CLERK_API_KEY=your-secret-api-key
Exchange your-frontend-api-key
and your-secret-api-key
with the precise values from the Clerk dashboard.
Step 4: Constructing Signal-In Parts
Study in a Flash
Login to Your Account
);
}” data-lang=”textual content/javascript”>
"use consumer";
import { AppBar, Container, Typography, Field, Toolbar, Button } from "@mui/materials";
import { useRouter } from 'subsequent/navigation';
import { SignIn } from "@clerk/nextjs";
export default perform LoginPage() {
const router = useRouter();
const handleHomeClick = () => {
router.push("https://dzone.com/");
};
return (
<Container maxWidth="sm">
<AppBar place="static" sx={{ mb: 4 }}>
<Toolbar>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Study in a Flash
</Typography>
<Button coloration="inherit" onClick={handleHomeClick}>
Dwelling
</Button>
</Toolbar>
</AppBar>
<Field
sx={{
show: 'flex',
flexDirection: 'column',
alignItems: 'middle',
justifyContent: 'middle',
mt: 4,
p: 3,
border: 1,
borderColor: 'gray.300',
borderRadius: 2,
}}
>
<Typography variant="h4" gutterBottom>
Login to Your Account
</Typography>
<SignIn />
</Field>
</Container>
);
}
Step 5: Constructing Signal-Up Parts
router.push(‘/sign-in’); // Make sure the main slash for routing
};
return (
Study in a Flash
Create an Account
);
}
” data-lang=”textual content/javascript”>
"use consumer";
import { AppBar, Container, Typography, TextField, Button, Field, Toolbar } from "@mui/materials";
import { useRouter } from 'subsequent/navigation';
export default perform SignUpPage() {
const router = useRouter();
const handleHomeClick = () => {
router.push("https://dzone.com/");
};
const handleLoginClick = () => {
router.push('/sign-in'); // Make sure the main slash for routing
};
return (
<Container maxWidth="sm">
<AppBar place="static" sx={{ mb: 4 }}>
<Toolbar>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Study in a Flash
</Typography>
<Button coloration="inherit" onClick={handleHomeClick}>
Dwelling
</Button>
<Button coloration="inherit" onClick={handleLoginClick}>
Login
</Button>
</Toolbar>
</AppBar>
<Field
sx={{
show: 'flex',
flexDirection: 'column',
alignItems: 'middle',
justifyContent: 'middle',
mt: 4,
p: 3,
border: 1,
borderColor: 'gray.300',
borderRadius: 2,
}}
>
<Typography variant="h4" gutterBottom>
Create an Account
</Typography>
<kind noValidate autoComplete="off">
<TextField
label="Electronic mail"
variant="outlined"
fullWidth
margin="regular"
kind="electronic mail"
required
/>
<TextField
label="Password"
variant="outlined"
fullWidth
margin="regular"
kind="password"
required
/>
<Button variant="contained" coloration="main" fullWidth sx={{ mt: 2 }}>
Signal Up
</Button>
</kind>
</Field>
</Container>
);
}
Creating Flashcard Technology Frontend Element
1. Setting Up Clerk for Consumer Authentication
On this half, we make the most of Clerk’s useUser()
hook to handle person authentication. This helps determine whether or not the person is logged in and supplies entry to the person’s knowledge, which is essential for associating flashcards with the right person.
import { useUser } from "@clerk/nextjs";
export default perform Generate() {
const { isLoaded, isSignedIn, person } = useUser();
// Different code shall be positioned beneath this
}
Notes:
isLoaded
: Checks if the person knowledge is totally loadedisSignedIn
: Checks if the person is signed inperson
: Accommodates the person’s knowledge if they’re authenticated
2. Managing Flashcard States
Right here, we outline the state variables utilizing React’s useState
to deal with the flashcards, their flipped state, person enter, and dialog administration for saving the flashcards.
const [flashcards, setFlashcards] = useState([]); // Shops the generated flashcards
const [flipped, setFlipped] = useState({}); // Retains observe of which flashcards are flipped
const [text, setText] = useState(""); // Consumer enter for producing flashcards
const [name, setName] = useState(""); // Title for the flashcard assortment
const [open, setOpen] = useState(false); // Dialog state for saving flashcards
Notes:
flashcards
: Array to carry generated flashcardsflipped
: Object to trace whether or not every flashcard is flippedtextual content
: Shops the textual content enter from the person to generate flashcardstitle
: Shops the title for the flashcard assortmentopen
: Manages the dialog field visibility for saving flashcards
3. Submitting Consumer Enter to Generate Flashcards
This perform handles sending the enter textual content to an API to generate flashcards and updates the flashcards
state based mostly on the API response.
const handleSubmit = async () => {
strive {
const response = await fetch("/api/generate", {
technique: "POST",
headers: { "Content material-Kind": "utility/json" },
physique: JSON.stringify({ textual content }), // Sends the enter textual content to the API
});
if (!response.okay) {
throw new Error("Did not fetch flashcards");
}
const knowledge = await response.json(); // Extracts the response knowledge
if (knowledge && knowledge.flashcards) {
setFlashcards(knowledge.flashcards); // Updates the flashcards state with the generated flashcards
}
} catch (error) {
console.error("Error producing flashcards:", error);
}
};
Notes:
- Sends a
POST
request to/api/generate
with the person’s enter textual content - The server returns generated flashcards, that are then set within the
flashcards
state.
4. Dealing with Flashcard Flip on Click on
This perform permits customers to click on on a flashcard to “flip” it, revealing both the entrance or again of the cardboard.
const handleCardClick = (index) => {
setFlipped((prev) => ({
...prev,
[index]: !prev[index], // Toggles the flipped state of the flashcard on the given index
}));
};
Notes:
- When a card is clicked, the
flipped
state is toggled for the respective card index, switching between exhibiting the back and front.
5. Opening and Closing the Save Dialog
Right here, the features handle the dialog’s visibility. The person can open the dialog to avoid wasting flashcards and shut it when completed.
const handleOpen = () => {
setOpen(true); // Opens the dialog
};
const handleClose = () => {
setOpen(false); // Closes the dialog
};
Notes:
handleOpen
: Opens the save dialog fieldhandleClose
: Closes the save dialog field
6. Saving Flashcards to Firebase
This perform saves the generated flashcards into Firebase Firestore beneath the present person’s assortment, guaranteeing that every flashcard set is uniquely related to the person.
const saveFlashcards = async () => {
if (!title) {
alert("Please enter a reputation");
return;
}
const batch = writeBatch(db); // Firestore batch for atomic writes
const userDocRef = doc(assortment(db, "customers"), person.id); // Consumer doc reference
const docSnap = await getDoc(userDocRef);
if (docSnap.exists()) {
const collectionData = docSnap.knowledge().flashcards || [];
if (collectionData.discover((f) => f.title === title)) {
alert("Flashcard with this title already exists.");
return;
} else {
collectionData.push({ title }); // Add the brand new flashcard assortment title
batch.set(userDocRef, { flashcards: collectionData }, { merge: true });
}
} else {
batch.set(userDocRef, { flashcards: [{ name }] }); // Create a brand new person doc if it does not exist
}
const colRef = assortment(userDocRef, title); // Reference to the flashcard assortment
flashcards.forEach((flashcard) => {
const cardDocRef = doc(colRef); // Create a doc for every flashcard
batch.set(cardDocRef, flashcard); // Save every flashcard
});
await batch.commit(); // Commit the batch
handleClose();
router.push("/flashcards"); // Redirect to the flashcards web page after saving
};
Notes:
- Checks if the person has entered a reputation for the flashcard assortment
- Makes use of Firestore batch writes to make sure all flashcards are saved atomically
- Saves the flashcards beneath the person’s doc and assortment in Firestore
7. Rendering the Consumer Interface
That is the principle a part of the JSX, which handles the shape for getting into textual content, shows the flashcards, and renders the save dialog.
return (
<Container maxWidth="md">
<Field sx={{ mt: 4, mb: 6, show: "flex", flexDirection: "column", alignItems: "middle" }}>
<TextField
label="Enter Textual content"
variant="outlined"
fullWidth
margin="regular"
worth={textual content}
onChange={(e) => setText(e.goal.worth)} // Replace the textual content state on enter
/>
<Button variant="contained" onClick={handleSubmit}>
Generate Flashcards
</Button>
</Field>
{flashcards.size > 0 && (
<Field sx={{ mt: 4 }}>
<Typography variant="h5" align="middle" gutterBottom>
Flashcard Preview
</Typography>
<Grid container spacing={3}>
{flashcards.map((flashcard, index) => (
<Grid merchandise xs={12} sm={6} md={4} key={index}>
<Card onClick={() => handleCardClick(index)}>
<CardActionArea>
<CardContent>
<Typography variant="h6">
{flipped[index] ? flashcard.again : flashcard.entrance}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
<Field sx={{ mt: 4, show: "flex", justifyContent: "middle" }}>
<Button variant="contained" coloration="secondary" onClick={handleOpen}>
Save
</Button>
</Field>
</Field>
)}
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Save the Flashcards</DialogTitle>
<DialogContent>
<DialogContentText>
Please enter a reputation on your Flashcard's Assortment
</DialogContentText>
<TextField
autoFocus
margin="dense"
label="Assortment Title"
kind="textual content"
fullWidth
worth={title}
onChange={(e) => setName(e.goal.worth)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={saveFlashcards}>Save</Button>
</DialogActions>
</Dialog>
</Container>
);
Notes:
- This renders the shape for getting into textual content and producing flashcards.
- It additionally handles the rendering of generated flashcards with flip performance and features a dialog to avoid wasting the flashcards to Firebase Firestore.
Pattern Look of the Frontend Display After Creation
Conclusion
This wraps up the creation of our flashcard utility. On this instance, I’ve utilized the LLaMA 3.1 language mannequin, however be at liberty to experiment with every other mannequin of your alternative.
Blissful coding!