Skip to content

Commit c4bf579

Browse files
committed
first commit
1 parent 365b5dc commit c4bf579

17 files changed

+1933
-155
lines changed

package-lock.json

+1,371-110
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"@auth/drizzle-adapter": "^0.4.0",
1313
"@hookform/resolvers": "^3.3.4",
14+
"@langchain/openai": "^0.0.14",
1415
"@radix-ui/react-dialog": "^1.0.5",
1516
"@radix-ui/react-icons": "^1.3.0",
1617
"@radix-ui/react-label": "^2.0.2",
@@ -23,15 +24,18 @@
2324
"class-variance-authority": "^0.7.0",
2425
"clsx": "^2.1.0",
2526
"drizzle-orm": "^0.29.3",
27+
"langchain": "^0.1.30",
2628
"lucide-react": "^0.309.0",
27-
"next": "14.0.4",
29+
"next": "14.0.1",
2830
"next-auth": "^5.0.0-beta.5",
2931
"next-plausible": "^3.12.0",
32+
"pdf-parse": "^1.1.1",
3033
"pg": "^8.11.3",
3134
"postgres": "^3.4.3",
3235
"react": "^18",
3336
"react-dom": "^18",
3437
"react-hook-form": "^7.49.3",
38+
"react-rewards": "^2.0.4",
3539
"stripe": "^14.14.0",
3640
"tailwind-merge": "^2.2.0",
3741
"tailwindcss-animate": "^1.0.7",

public/images/owl-smiling.png

1.48 MB
Loading

src/app/api/quizz/generate/route.ts

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
3+
import { ChatOpenAI } from "@langchain/openai";
4+
import { HumanMessage } from "@langchain/core/messages";
5+
6+
import { PDFLoader } from "langchain/document_loaders/fs/pdf";
7+
import { JsonOutputFunctionsParser } from "langchain/output_parsers";
8+
9+
export async function POST(req: NextRequest) {
10+
// const body = await req.json();
11+
const body = await req.formData();
12+
// const text = body.text;
13+
const document = body.get("pdf");
14+
15+
try {
16+
const pdfLoader = new PDFLoader(document as Blob, {
17+
parsedItemSeparator: " ",
18+
});
19+
const docs = await pdfLoader.load();
20+
21+
const selectedDocuments = docs.filter((doc) => doc.pageContent !== undefined);
22+
// console.log("selectedDocuments", selectedDocuments);
23+
const texts = selectedDocuments.map((doc) => doc.pageContent);
24+
// console.log("texts", texts);
25+
const metadatas = selectedDocuments.map((doc) => doc.metadata);
26+
// console.log("metadatas", metadatas);
27+
28+
const prompt =
29+
"given the text which is a summary of the document, generate a quiz based on the text. Return json only that contains a quizz object with fields: name, description and questions. The questions is an array of objects with fields: question_text, answers. The answers is an array of objects with fields: answer_text, is_correct.";
30+
31+
const model = new ChatOpenAI({
32+
apiKey: process.env.OPENAI_API_KEY,
33+
modelName: "gpt-4-1106-preview",
34+
});
35+
36+
// const message = new HumanMessage({
37+
// content: [
38+
// {
39+
// type: "text",
40+
// text: prompt + "\n" + texts.join("\n"),
41+
// },
42+
// ],
43+
// });
44+
45+
// const res = await model.invoke([message]);
46+
// console.log(res.lc_kwargs.content);
47+
// Instantiate the parser
48+
const parser = new JsonOutputFunctionsParser();
49+
// Define the function schema
50+
const extractionFunctionSchema = {
51+
name: "extractor",
52+
description: "Extracts fields from the input.",
53+
parameters: {
54+
type: "object",
55+
properties: {
56+
quizz: {
57+
type: "object",
58+
properties: {
59+
name: { type: "string" },
60+
description: { type: "string" },
61+
questions: {
62+
type: "array",
63+
items: {
64+
type: "object",
65+
properties: {
66+
question_text: { type: "string" },
67+
answers: {
68+
type: "array",
69+
items: {
70+
type: "object",
71+
properties: {
72+
answer_text: { type: "string" },
73+
is_correct: { type: "boolean" },
74+
},
75+
},
76+
},
77+
},
78+
},
79+
},
80+
},
81+
},
82+
},
83+
},
84+
};
85+
86+
// Create a new runnable, bind the function to the model, and pipe the output through the parser
87+
const runnable = model
88+
.bind({
89+
functions: [extractionFunctionSchema],
90+
function_call: { name: "extractor" },
91+
})
92+
.pipe(parser);
93+
94+
const message = new HumanMessage({
95+
content: [
96+
{
97+
type: "text",
98+
text: prompt + "\n" + texts.join("\n"),
99+
},
100+
],
101+
});
102+
103+
// Invoke the runnable with an input
104+
const result = await runnable.invoke([message]);
105+
106+
console.log({ result });
107+
108+
return NextResponse.json({ ok: true }, { status: 200 });
109+
} catch (e: any) {
110+
return NextResponse.json({ error: e.message }, { status: 500 });
111+
}
112+
}

src/app/globals.css

+36-33
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,53 @@
33
@tailwind utilities;
44

55

6+
67
@layer base {
78
:root {
89
--background: 0 0% 100%;
9-
--foreground: 224 71.4% 4.1%;
10+
--foreground: 222.2 84% 4.9%;
1011
--card: 0 0% 100%;
11-
--card-foreground: 224 71.4% 4.1%;
12+
--card-foreground: 222.2 84% 4.9%;
1213
--popover: 0 0% 100%;
13-
--popover-foreground: 224 71.4% 4.1%;
14-
--primary: 262.1 83.3% 57.8%;
15-
--primary-foreground: 210 20% 98%;
16-
--secondary: 220 14.3% 95.9%;
17-
--secondary-foreground: 220.9 39.3% 11%;
18-
--muted: 220 14.3% 95.9%;
19-
--muted-foreground: 220 8.9% 46.1%;
20-
--accent: 220 14.3% 95.9%;
21-
--accent-foreground: 220.9 39.3% 11%;
14+
--popover-foreground: 222.2 84% 4.9%;
15+
--primary: 221.2 83.2% 53.3%;
16+
--primary-shadow: 221.2 83.2% 33.3%;
17+
--primary-foreground: 210 40% 98%;
18+
--secondary: 210 40% 96.1%;
19+
--secondary-foreground: 222.2 47.4% 11.2%;
20+
--muted: 210 40% 96.1%;
21+
--muted-foreground: 215.4 16.3% 46.9%;
22+
--accent: 210 40% 96.1%;
23+
--accent-foreground: 222.2 47.4% 11.2%;
2224
--destructive: 0 84.2% 60.2%;
23-
--destructive-foreground: 210 20% 98%;
24-
--border: 220 13% 91%;
25-
--input: 220 13% 91%;
26-
--ring: 262.1 83.3% 57.8%;
25+
--destructive-foreground: 210 40% 98%;
26+
--border: 214.3 31.8% 91.4%;
27+
--input: 214.3 31.8% 91.4%;
28+
--ring: 221.2 83.2% 53.3%;
2729
--radius: 0.5rem;
2830
}
2931

3032
.dark {
31-
--background: 224 71.4% 4.1%;
32-
--foreground: 210 20% 98%;
33-
--card: 224 71.4% 4.1%;
34-
--card-foreground: 210 20% 98%;
35-
--popover: 224 71.4% 4.1%;
36-
--popover-foreground: 210 20% 98%;
37-
--primary: 263.4 70% 50.4%;
38-
--primary-foreground: 210 20% 98%;
39-
--secondary: 215 27.9% 16.9%;
40-
--secondary-foreground: 210 20% 98%;
41-
--muted: 215 27.9% 16.9%;
42-
--muted-foreground: 217.9 10.6% 64.9%;
43-
--accent: 215 27.9% 16.9%;
44-
--accent-foreground: 210 20% 98%;
33+
--background: 222.2 84% 4.9%;
34+
--foreground: 210 40% 98%;
35+
--card: 222.2 84% 4.9%;
36+
--card-foreground: 210 40% 98%;
37+
--popover: 222.2 84% 4.9%;
38+
--popover-foreground: 210 40% 98%;
39+
--primary: 217.2 91.2% 59.8%;
40+
--primary-shadow: 217.2 91.2% 39.8%;
41+
--primary-foreground: 222.2 47.4% 11.2%;
42+
--secondary: 217.2 32.6% 17.5%;
43+
--secondary-foreground: 210 40% 98%;
44+
--muted: 217.2 32.6% 17.5%;
45+
--muted-foreground: 215 20.2% 65.1%;
46+
--accent: 217.2 32.6% 17.5%;
47+
--accent-foreground: 210 40% 98%;
4548
--destructive: 0 62.8% 30.6%;
46-
--destructive-foreground: 210 20% 98%;
47-
--border: 215 27.9% 16.9%;
48-
--input: 215 27.9% 16.9%;
49-
--ring: 263.4 70% 50.4%;
49+
--destructive-foreground: 210 40% 98%;
50+
--border: 217.2 32.6% 17.5%;
51+
--input: 217.2 32.6% 17.5%;
52+
--ring: 224.3 76.3% 48%;
5053
}
5154
}
5255

src/app/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default function RootLayout({
2121
<head>
2222
<PlausibleProvider domain={process.env.PLAUSIBLE_DOMAIN || ""} />
2323
</head>
24-
<body className={inter.className}>{children}</body>
24+
<body className={"dark"}>{children}</body>
2525
</html>
2626
)
2727
}

src/app/quizz/Bar.tsx

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react'
2+
import clsx from 'clsx'
3+
4+
type Props = {
5+
percentage: number,
6+
color: string
7+
}
8+
9+
const Bar = (props: Props) => {
10+
const { percentage, color } = props;
11+
12+
const barStyle = {
13+
height: `${percentage}%`,
14+
}
15+
16+
const barBgClasses: Record<string, string> = {
17+
'green': 'bg-green-500',
18+
'red': 'bg-red-500',
19+
'blue': 'bg-blue-500',
20+
}
21+
22+
return (
23+
<div className="h-40 flex items-end justify-end">
24+
<div className={clsx(barBgClasses[color], "w-14 rounded-xl border-2 border-black")} style={barStyle}>
25+
</div>
26+
</div>
27+
)
28+
}
29+
30+
export default Bar

src/app/quizz/QuizzSubmission.tsx

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { useEffect } from 'react'
2+
import Bar from './Bar'
3+
import { useReward } from 'react-rewards';
4+
import Image from 'next/image';
5+
6+
type Props = {
7+
scorePercentage: number,
8+
score: number,
9+
totalQuestions: number
10+
}
11+
12+
const QuizzSubmission = (props: Props) => {
13+
const { scorePercentage, score, totalQuestions } = props;
14+
const { reward, isAnimating } = useReward('rewardId', 'confetti');
15+
16+
useEffect(() => {
17+
if (scorePercentage === 100) {
18+
reward();
19+
}
20+
}, [scorePercentage, reward])
21+
22+
return (
23+
<div className="flex flex-col flex-1">
24+
<main className='py-11 flex flex-col gap-4 items-center flex-1 mt-24'>
25+
<h2 className='text-3xl font-bold'>Quizz Complete!</h2>
26+
<p>You scored: {scorePercentage}%</p>
27+
{
28+
scorePercentage === 100 ?
29+
<div>
30+
<p>Congratulations! 🎉</p>
31+
<div className="flex justify-center">
32+
<Image src="/images/owl-smiling.png" alt="Smiling Owl Image" width={100} height={100} />
33+
</div>
34+
<span id="rewardId" />
35+
</div> :
36+
<>
37+
<div className="flex flex-row gap-8 mt-6">
38+
<Bar percentage={scorePercentage} color="green" />
39+
<Bar percentage={100 - scorePercentage} color="red" />
40+
</div>
41+
<div className="flex flex-row gap-8">
42+
<p>{score} Correct</p>
43+
<p>{totalQuestions - score} Incorrect</p>
44+
</div>
45+
</>
46+
}
47+
</main>
48+
</div>
49+
)
50+
}
51+
52+
export default QuizzSubmission

src/app/quizz/ResultCard.tsx

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react'
2+
import { clsx } from 'clsx'
3+
import { cn } from '@/lib/utils'
4+
5+
type Props = {
6+
isCorrect: boolean | null,
7+
correctAnswer: string,
8+
}
9+
10+
const ResultCard = (props: Props) => {
11+
const { isCorrect } = props;
12+
13+
if (isCorrect === null) {
14+
return null
15+
};
16+
17+
const text = isCorrect ? 'Correct!' : 'Incorrect! The correct answer is: ' + props.correctAnswer;
18+
19+
const borderClasses = clsx({
20+
"border-green-500": isCorrect,
21+
"border-red-500": !isCorrect,
22+
});
23+
24+
return (
25+
<div className={cn(borderClasses,
26+
"border-2",
27+
"rounded-lg",
28+
"p-4",
29+
"text-center",
30+
"text-lg",
31+
"font-semibold",
32+
"mt-4",
33+
"bg-secondary",
34+
)}>
35+
{text}
36+
</div>
37+
)
38+
}
39+
40+
export default ResultCard

0 commit comments

Comments
 (0)