tr]:last:border-b-0",
48 | className
49 | )}
50 | {...props}
51 | />
52 | )
53 | }
54 |
55 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
56 | return (
57 |
65 | )
66 | }
67 |
68 | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69 | return (
70 | [role=checkbox]]:translate-y-[2px]",
74 | className
75 | )}
76 | {...props}
77 | />
78 | )
79 | }
80 |
81 | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82 | return (
83 | [role=checkbox]]:translate-y-[2px]",
87 | className
88 | )}
89 | {...props}
90 | />
91 | )
92 | }
93 |
94 | function TableCaption({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"caption">) {
98 | return (
99 |
104 | )
105 | }
106 |
107 | export {
108 | Table,
109 | TableHeader,
110 | TableBody,
111 | TableFooter,
112 | TableHead,
113 | TableRow,
114 | TableCell,
115 | TableCaption,
116 | }
117 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "deploy:db": "prisma migrate deploy",
10 | "lint": "next lint"
11 | },
12 | "dependencies": {
13 | "@clerk/nextjs": "^6.31.3",
14 | "@contentful/rich-text-react-renderer": "^16.1.0",
15 | "@fontsource/outfit": "^5.2.5",
16 | "@google/generative-ai": "^0.24.0",
17 | "@langchain/community": "^1.0.7",
18 | "@langchain/core": "^1.1.4",
19 | "@langchain/google-genai": "^2.0.4",
20 | "@langchain/openai": "^1.1.3",
21 | "@langchain/textsplitters": "^1.0.1",
22 | "@mdxeditor/editor": "^3.50.0",
23 | "@prisma/client": "^6.14.0",
24 | "@radix-ui/react-alert-dialog": "^1.1.7",
25 | "@radix-ui/react-checkbox": "^1.1.5",
26 | "@radix-ui/react-dialog": "^1.1.7",
27 | "@radix-ui/react-dropdown-menu": "^2.1.7",
28 | "@radix-ui/react-label": "^2.1.3",
29 | "@radix-ui/react-popover": "^1.1.15",
30 | "@radix-ui/react-progress": "^1.1.3",
31 | "@radix-ui/react-radio-group": "^1.3.8",
32 | "@radix-ui/react-scroll-area": "^1.2.3",
33 | "@radix-ui/react-select": "^2.2.6",
34 | "@radix-ui/react-slot": "^1.2.0",
35 | "@radix-ui/react-switch": "^1.2.6",
36 | "@radix-ui/react-tabs": "^1.1.4",
37 | "@radix-ui/react-tooltip": "^1.2.8",
38 | "@supabase/supabase-js": "^2.87.0",
39 | "@tanstack/react-table": "^8.21.2",
40 | "@types/axios": "^0.9.36",
41 | "@types/canvas-confetti": "^1.9.0",
42 | "anki-apkg-export": "^4.0.3",
43 | "autoprefixer": "^10.4.21",
44 | "axios": "^1.12.2",
45 | "canvas-confetti": "^1.9.3",
46 | "chart.js": "^4.5.0",
47 | "class-variance-authority": "^0.7.1",
48 | "cloudinary": "^2.6.0",
49 | "clsx": "^2.1.1",
50 | "contentful": "^11.7.15",
51 | "framer-motion": "^12.6.3",
52 | "langchain": "^1.1.5",
53 | "libphonenumber-js": "^1.12.10",
54 | "lucide-react": "^0.487.0",
55 | "mermaid": "^11.10.0",
56 | "motion": "^12.23.12",
57 | "next": "^15.5.9",
58 | "next-themes": "^0.4.6",
59 | "openai": "4.62.1",
60 | "postcss": "^8.5.3",
61 | "react": "^19.1.1",
62 | "react-chartjs-2": "^5.3.0",
63 | "react-circular-progressbar": "^2.2.0",
64 | "react-dom": "^19.1.1",
65 | "react-markdown": "^10.1.0",
66 | "react-phone-input-2": "^2.15.1",
67 | "recharts": "^2.15.2",
68 | "sonner": "^2.0.3",
69 | "swr": "^2.3.6",
70 | "tailwind-merge": "^3.2.0",
71 | "tw-animate-css": "^1.2.5",
72 | "twilio": "^5.5.2",
73 | "typed.js": "^2.1.0",
74 | "youtube-transcript": "^1.2.1"
75 | },
76 | "devDependencies": {
77 | "@eslint/eslintrc": "^3",
78 | "@tailwindcss/postcss": "^4",
79 | "@types/node": "^20",
80 | "@types/react": "^19",
81 | "@types/react-dom": "^19",
82 | "@types/typed.js": "^2.0.0",
83 | "eslint": "^9",
84 | "eslint-config-next": "15.2.4",
85 | "prisma": "^6.14.0",
86 | "tailwindcss": "^4.1.3",
87 | "typescript": "^5"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/(public)/privacy-policy/page.tsx:
--------------------------------------------------------------------------------
1 | import { generateMetadataUtil } from "@/utils/generateMetadata";
2 | export const metadata = generateMetadataUtil({
3 | title: "Privacy Policy",
4 | description: "Read the Privacy Policy of Smriti AI to learn how we collect, use, and protect your personal information. Your data privacy and security are our top priority.",
5 | keywords: [
6 | "Smriti AI Privacy Policy",
7 | "AI data protection",
8 | "Smriti AI security",
9 | "user privacy",
10 | "data collection policy",
11 | "GDPR compliance",
12 | "AI privacy guidelines",
13 | "data protection",
14 | "privacy rights",
15 | "secure AI platform"
16 | ],
17 | url: "https://www.smriti.live/privacy-policy",
18 | });
19 |
20 | export default function PrivacyPolicy() {
21 | return (
22 |
23 |
24 | Privacy Policy
25 |
26 |
27 | This Privacy Policy explains how Smriti AI collects, uses, and protects
28 | your personal information. By using our services, you agree to the terms
29 | of this policy.
30 |
31 |
32 |
33 | 1. Information We Collect
34 |
35 |
36 | We may collect information such as your name, email address, and usage
37 | data to improve your experience and provide our services.
38 |
39 |
40 |
41 | 2. How We Use Your Information
42 |
43 |
44 | Your data helps us personalize your experience, analyze usage trends,
45 | and improve our features. We do not sell your personal data to third
46 | parties.
47 |
48 |
49 |
50 | 3. Data Security
51 |
52 |
53 | We implement appropriate security measures to protect your data from
54 | unauthorized access, disclosure, or loss.
55 |
56 |
57 |
58 | 4. Your Rights
59 |
60 |
61 | You have the right to access, correct, or delete your personal
62 | information. You can contact us for any data-related requests.
63 |
64 |
65 |
66 | 5. Contact Us
67 |
68 |
69 | If you have any questions or concerns about this Privacy Policy, please
70 | reach out to us through our support page.
71 |
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/Contributors data.md:
--------------------------------------------------------------------------------
1 | # 🚫 DO NOT CONTRIBUTE TO THIS FILE
2 |
3 | ## 🏆 **Contributor Points Table**
4 |
5 | This document records **all contributions** to the project and assigns points based on the **difficulty, complexity, and impact** of the contribution.
6 |
7 | | 🏅 Contribution Level | 🎯 Points Awarded |
8 | | -------------------- | ----------------- |
9 | | **Level 1** | 3 Points |
10 | | **Level 2** | 7 Points |
11 | | **Level 3** | 10 Points |
12 |
13 | > 📌 **Note:** Points are awarded **only for merged Pull Requests (PRs)**.
14 | > 🛠 **Evaluation Criteria:** Complexity, quality, and importance of the work.
15 | > ✅ **Approval:** Points updated **only** by the Mentor or Project Admin (PA).
16 |
17 |
18 | Contributors:
19 | | #️⃣ S.No | 👤 GitHub Profile Link | 🎯 Points | 🔗 PR No. |
20 | | --------- | ----------------------------------------- | --------- | --------------------------------- |
21 | | 1 | https://github.com/Sujal-Raj | 3 | #7 |
22 | | 2 | https://github.com/Rashidaga18 | 3 | #16 |
23 | | 3 | https://github.com/SidoJain | 3 | #17 |
24 | | 4 | https://github.com/Harshdev10 | 3 | #18 |
25 | | 5 | https://github.com/nilaychugh | 17 | #19 #41 |
26 | | 6 | https://github.com/o000SAI000o | 3 | #29 |
27 | | 7 | https://github.com/shubhranshu-sahu | 3 | #36 |
28 | | 8 | https://github.com/revanthsaich | 7 | #40 |
29 | | 9 | https://github.com/Indhuvanguru | 3 | #42 |
30 | | 10 | https://github.com/medss19 | 25 | #47 #48 #49 #52 #60 #62 |
31 | | 11 | https://github.com/farazmirzax | 3 | #54 |
32 | | 12 | https://github.com/Abhash-Chakraborty | 3 | #56 |
33 | | 13 | https://github.com/BhaveshMulchandani | 3 | #58 |
34 | | 14 | https://github.com/aakash0101200 | 7 | #59 |
35 | | 15 | https://github.com/Aaditya-0701 | 10 | #63 #75 |
36 | | 16 | https://github.com/git-lakshy | 24 | #70 #73 #87 |
37 | | 17 | https://github.com/Ankit-Matth | 3 | #76 |
38 | | 18 | https://github.com/DivyaJain-DataAnalyst | 3 | #80 |
39 | | 19 | https://github.com/ei-sanu | 3 | #83 |
40 | | 20 | https://github.com/nallarahul | 7 | #85 |
41 |
42 |
--------------------------------------------------------------------------------
/components/notes/SimpleNoteEditor.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import axios from "axios";
5 | import { Button } from "@/components/ui/button";
6 | import { Textarea } from "@/components/ui/textarea";
7 | import { LoaderCircle, AlertTriangle, Edit2 } from "lucide-react";
8 | import { Note } from "@/types/note"; // Import the Note type
9 |
10 | export function SimpleNoteEditor({ topicId }: { topicId: string }) {
11 | const [note, setNote] = useState("");
12 | const [isEditing, setIsEditing] = useState(false);
13 | const [status, setStatus] = useState<"loading" | "error" | "idle">("loading");
14 |
15 | useEffect(() => {
16 | const fetchNote = async () => {
17 | try {
18 | // FIX: Tell axios what type of data to expect
19 | const response = await axios.get(`/api/notes/${topicId}`);
20 | if (response.data?.content) {
21 | setNote(response.data.content);
22 | }
23 | } catch (error) {
24 | console.error("Failed to fetch note:", error);
25 | setStatus("error");
26 | } finally {
27 | setStatus("idle");
28 | }
29 | };
30 | fetchNote();
31 | }, [topicId]);
32 |
33 | const handleSave = async () => {
34 | try {
35 | await axios.patch(`/api/notes/${topicId}`, { content: note });
36 | setIsEditing(false);
37 | } catch (error) {
38 | console.error("Failed to save note:", error);
39 | setStatus("error");
40 | }
41 | };
42 |
43 | if (status === "loading") {
44 | return
;
45 | }
46 |
47 | if (status === "error") {
48 | return (
49 |
50 |
51 |
Failed to load or save note. Please refresh.
52 |
53 | );
54 | }
55 |
56 | return (
57 |
58 | {isEditing ? (
59 |
71 | ) : (
72 |
73 |
setIsEditing(true)} className="absolute top-0 right-0">
74 |
75 |
76 | {note ? (
77 |
{note}
78 | ) : (
79 |
No notes yet. Click the edit icon to start writing.
80 | )}
81 |
82 | )}
83 |
84 | );
85 | }
--------------------------------------------------------------------------------
/hooks/useTopic.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useRouter } from "next/navigation";
3 | import axios from "axios";
4 | import { toast } from "sonner";
5 |
6 | const topicAPI = "/api/topic";
7 |
8 | export function useTopic(id: string | null) {
9 | const router = useRouter();
10 | const [topicId, setTopicId] = useState(id || null);
11 | const [topicModalOpen, setTopicModalOpen] = useState(id ? false : true);
12 | const [editMode, setEditMode] = useState(!!id);
13 | const [topicName, setTopicName] = useState("");
14 | const [isLoading, setIsLoading] = useState(false);
15 |
16 | useEffect(() => {
17 | async function fetchTopic() {
18 | if (!id) return;
19 |
20 | try {
21 | setIsLoading(true);
22 | console.log("Fetching topic for ID:", id);
23 |
24 | // Fetch the topic title
25 | const topicRes = await axios.get<{ topic: { title: string } }>(
26 | topicAPI,
27 | {
28 | params: { id },
29 | }
30 | );
31 | setTopicName(topicRes.data.topic.title);
32 | } catch (error) {
33 | console.error("Error fetching topic:", error);
34 | toast.error("Failed to fetch topic information.");
35 | } finally {
36 | setIsLoading(false);
37 | }
38 | }
39 |
40 | fetchTopic();
41 | }, [id]);
42 |
43 | const handleSaveTopic = async () => {
44 | const title = topicName.trim();
45 | if (!title) return;
46 |
47 | try {
48 | let res;
49 |
50 | if (editMode && topicId) {
51 | // Edit mode: update topic
52 | res = await axios.put(topicAPI, {
53 | id: topicId,
54 | title,
55 | });
56 | } else {
57 | // Create mode: add topic
58 | res = await axios.post(topicAPI, {
59 | title,
60 | });
61 | }
62 |
63 | if (res.status === 201 || res.status === 200) {
64 | const topic = (res.data as { topic: { id: string } }).topic;
65 | const responseData = res.data as { message: string };
66 | toast.success(responseData.message);
67 |
68 | // Update topicId if we just created a new topic
69 | if (!editMode) {
70 | const newTopicId = topic.id;
71 | setTopicId(newTopicId);
72 |
73 | // Update the URL to include the new topic ID without reloading the page
74 | router.replace(`/dashboard/topic/${newTopicId}`, { scroll: false });
75 | }
76 | } else {
77 | const responseData = res.data as { message: string };
78 | toast.error(responseData.message || "Something went wrong");
79 | }
80 | } catch (error: any) {
81 | toast.error(error?.response?.data?.message || "Request failed");
82 | console.error("Error saving topic:", error);
83 | }
84 |
85 | setTopicModalOpen(false);
86 | setEditMode(true);
87 | };
88 |
89 | return {
90 | topicId,
91 | setTopicId,
92 | topicName,
93 | setTopicName,
94 | topicModalOpen,
95 | setTopicModalOpen,
96 | editMode,
97 | setEditMode,
98 | handleSaveTopic,
99 | isLoading,
100 | };
101 | }
102 |
--------------------------------------------------------------------------------
/.github/workflows/update-contributors.yml:
--------------------------------------------------------------------------------
1 | name: Update Contributors Data
2 |
3 | on:
4 | schedule:
5 | # Runs once a day at 6:00 AM UTC
6 | # Change to '0 0 * * 0' for weekly (Sunday at midnight)
7 | # Change to '0 */6 * * *' for every 6 hours
8 | - cron: '0 6 * * *'
9 | workflow_dispatch: # Allow manual triggering
10 |
11 | jobs:
12 | update-contributors:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v4
18 | with:
19 | token: ${{ secrets.GITHUB_TOKEN }}
20 |
21 | - name: Setup Node.js
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: '18'
25 |
26 | - name: Fetch contributors data
27 | run: |
28 | echo "Fetching contributors from GitHub API..."
29 |
30 | # Create public directory if it doesn't exist
31 | mkdir -p public
32 |
33 | # Fetch contributors data
34 | curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
35 | -H "Accept: application/vnd.github.v3+json" \
36 | "https://api.github.com/repos/vatsal-bhakodia/smriti-ai/contributors" \
37 | > public/contributors.json
38 |
39 | # Check if the API call was successful
40 | if [ $? -ne 0 ]; then
41 | echo "Failed to fetch contributors data"
42 | exit 1
43 | fi
44 |
45 | # Verify the response is valid JSON and not an error
46 | if ! jq empty public/contributors.json 2>/dev/null; then
47 | echo "Invalid JSON response"
48 | cat public/contributors.json
49 | exit 1
50 | fi
51 |
52 | # Check if it's an error response
53 | if jq -e '.message' public/contributors.json > /dev/null 2>&1; then
54 | echo "GitHub API error:"
55 | cat public/contributors.json
56 | exit 1
57 | fi
58 |
59 | echo "Successfully fetched contributors data"
60 |
61 | # Create lastUpdated.json with current timestamp
62 | echo "{\"lastUpdated\":\"$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")\"}" > public/lastUpdated.json
63 |
64 | echo "Created timestamp file"
65 |
66 | - name: Check for changes
67 | id: check_changes
68 | run: |
69 | if git diff --quiet public/contributors.json public/lastUpdated.json; then
70 | echo "No changes detected"
71 | echo "changes=false" >> $GITHUB_OUTPUT
72 | else
73 | echo "Changes detected"
74 | echo "changes=true" >> $GITHUB_OUTPUT
75 | fi
76 |
77 | - name: Commit and push changes
78 | if: steps.check_changes.outputs.changes == 'true'
79 | run: |
80 | git config --local user.email "action@github.com"
81 | git config --local user.name "GitHub Action"
82 | git add public/contributors.json public/lastUpdated.json
83 | git commit -m "🤖 Update contributors data - $(date -u +"%Y-%m-%d %H:%M:%S UTC")"
84 | git push
85 |
86 | - name: Summary
87 | run: |
88 | echo "Contributors update completed!"
89 | echo "Total contributors: $(jq length public/contributors.json)"
90 | echo "Last updated: $(jq -r '.lastUpdated' public/lastUpdated.json)"
--------------------------------------------------------------------------------
/components/quiz/QuizResult.tsx:
--------------------------------------------------------------------------------
1 | // components/quiz/QuizResult.tsx
2 |
3 | "use client";
4 | import { useEffect, useMemo } from "react";
5 | import { CircularProgressbar, buildStyles } from "react-circular-progressbar";
6 | import "react-circular-progressbar/dist/styles.css";
7 | import confetti from "canvas-confetti";
8 |
9 | export type QuizQA = {
10 | id: string;
11 | question: string;
12 | options: string[];
13 | correctAnswer: string;
14 | explanation: string;
15 | difficulty: string;
16 | };
17 |
18 | export type QuizFinalResultProps = {
19 | userAnswers: { quizQAId: string; selectedOption: string; isCorrect: boolean }[];
20 | quizData: QuizQA[];
21 | resetQuiz: () => void;
22 | startReview: () => void;
23 | };
24 |
25 | const getColorForScore = (percentage: number): string => {
26 | if (percentage > 70) return "#84cc16"; // green
27 | if (percentage > 30) return "#eab308"; // yellow
28 | return "#dc2626"; // red
29 | };
30 |
31 | const QuizFinalResult = ({
32 | userAnswers,
33 | quizData,
34 | resetQuiz,
35 | startReview,
36 | }: QuizFinalResultProps) => {
37 | const score = useMemo(() => userAnswers.filter((a) => a.isCorrect).length, [userAnswers]);
38 | const total = useMemo(() => userAnswers.length, [userAnswers]);
39 | const percentage = total > 0 ? Math.round((score / total) * 100) : 0;
40 | const color = getColorForScore(percentage);
41 |
42 | useEffect(() => {
43 | if (percentage > 70) {
44 | confetti({
45 | particleCount: 120,
46 | spread: 80,
47 | origin: { y: 0.6 },
48 | });
49 | }
50 | }, [percentage]);
51 |
52 | const wrongAnswersCount = useMemo(() => {
53 | return userAnswers.filter(
54 | (answer) => !answer.isCorrect
55 | ).length;
56 | }, [userAnswers]);
57 |
58 | return (
59 |
60 |
Your Quiz Summary
61 |
62 |
63 |
74 |
75 |
76 |
77 | You answered {score} out
78 | of {total} questions
79 | correctly.
80 |
81 |
82 |
83 |
87 | Retry Quiz
88 |
89 |
90 | {wrongAnswersCount > 0 && (
91 |
95 | Revisit Incorrect Questions ({wrongAnswersCount})
96 |
97 | )}
98 |
99 |
100 | );
101 | };
102 |
103 | export default QuizFinalResult;
--------------------------------------------------------------------------------
/utils/youtube.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | // import { YoutubeTranscript } from "youtube-transcript";
3 |
4 | export function getYouTubeVideoId(url: string) {
5 | const regex = /(?:youtube\.com\/.*v=|youtu\.be\/)([^?/]+)/;
6 | const match = url.match(regex);
7 | return match ? match[1] : null;
8 | }
9 |
10 | export const getYouTubeThumbnail = (url: string) => {
11 | const match = url.match(/(?:v=|\.be\/)([\w-]{11})/);
12 | return match ? `https://img.youtube.com/vi/${match[1]}/mqdefault.jpg` : "";
13 | };
14 |
15 | interface TranscriptItem {
16 | text: string;
17 | start: number;
18 | duration: number;
19 | }
20 |
21 | interface Transcripts {
22 | [key: string]: {
23 | custom?: TranscriptItem[];
24 | default?: TranscriptItem[];
25 | auto?: TranscriptItem[];
26 | [key: string]: TranscriptItem[] | undefined;
27 | };
28 | }
29 |
30 | interface ApiResponse {
31 | code: number;
32 | message: string;
33 | data: {
34 | videoId: string;
35 | videoInfo: {
36 | name: string;
37 | thumbnailUrl: object;
38 | embedUrl: string;
39 | duration: string;
40 | description: string;
41 | upload_date: string;
42 | genre: string;
43 | author: string;
44 | };
45 | language_code: string[][];
46 | transcripts: Transcripts;
47 | };
48 | }
49 |
50 | export async function getYoutubeTranscript(url: string): Promise {
51 | const videoId = getYouTubeVideoId(url);
52 | const options = {
53 | method: "GET",
54 | url: process.env.RAPIDAPI_URL || "",
55 | params: {
56 | video_id: videoId,
57 | platform: "youtube",
58 | },
59 | headers: {
60 | "x-rapidapi-host": process.env.RAPIDAPI_HOST || "",
61 | "x-rapidapi-key": process.env.RAPIDAPI_KEY || "",
62 | },
63 | };
64 |
65 | try {
66 | const response = await axios.request(options);
67 | // console.log("Response:", response.data);
68 |
69 | const transcriptsByLang = response.data.data.transcripts;
70 | const availableLangKeys = Object.keys(transcriptsByLang);
71 |
72 | if (availableLangKeys.length === 0) {
73 | throw new Error("No transcripts available.");
74 | }
75 |
76 | // Pick the first available language (or prioritize "en_auto" if present)
77 | const preferredLang = availableLangKeys.includes("en_auto")
78 | ? "en_auto"
79 | : availableLangKeys[0];
80 |
81 | const transcripts = transcriptsByLang[preferredLang];
82 |
83 | const fallbackOrder = ["custom", "default", "auto"] as const;
84 |
85 | // Fixed the type error by properly checking each property
86 | for (const type of fallbackOrder) {
87 | const transcriptArray = transcripts[type];
88 | if (Array.isArray(transcriptArray) && transcriptArray.length > 0) {
89 | return transcriptArray
90 | .map((item: TranscriptItem) => item.text)
91 | .join(" ");
92 | }
93 | }
94 |
95 | throw new Error("No valid transcript data found.");
96 | } catch (error) {
97 | console.error("Error fetching transcript:", error);
98 | // This will trigger the fallback prompt in your main handler
99 | throw new Error("Transcript fetch failed.");
100 | }
101 |
102 | // const transcript = await YoutubeTranscript.fetchTranscript(url);
103 | // return transcript.map((item) => item.text).join(" ");
104 | }
105 |
--------------------------------------------------------------------------------
/components/dashboard/performanceCard.tsx:
--------------------------------------------------------------------------------
1 | // components/dashboard/performanceCard.tsx
2 |
3 | "use client";
4 |
5 | import {
6 | ChartConfig,
7 | ChartContainer,
8 | ChartTooltip,
9 | ChartTooltipContent,
10 | } from "@/components/ui/chart";
11 | import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
12 | import { Card, CardContent } from "@/components/ui/card";
13 | import { ChartColumnBig, LoaderCircle } from "lucide-react";
14 | import useSWR from 'swr';
15 |
16 | // A simple fetcher function for SWR
17 | const fetcher = (url: string) => fetch(url).then((res) => res.json());
18 |
19 | const chartConfig = {
20 | marks: {
21 | label: "marks",
22 | color: "hsl(90, 60%, 50%)", // vibrant yellow-green
23 | },
24 | } satisfies ChartConfig;
25 |
26 | // The chart component now accepts 'data' as a prop
27 | function PerformanceChart({ data }: { data: any[] }) {
28 | return (
29 |
33 |
41 |
42 | value.slice(0, 3)}
48 | />
49 | }
52 | />
53 |
60 |
61 |
62 | );
63 | }
64 |
65 | export default function PerformanceCard() {
66 | // SWR to fetch data from API endpoint
67 | const { data: chartData, error, isLoading } = useSWR('/api/performance-data', fetcher);
68 |
69 | return (
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
Performance
78 | {/* This part can be dynamic later */}
79 |
80 | Last 6 Months
81 |
82 |
83 |
84 |
85 |
86 | {isLoading && (
87 |
88 |
89 |
90 | )}
91 | {error && (
92 |
93 | Failed to load chart data.
94 |
95 | )}
96 | {chartData && }
97 |
98 |
99 | );
100 | }
--------------------------------------------------------------------------------
/app/api/user/login/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { getAuth } from "@clerk/nextjs/server";
3 | import prisma from "@/lib/prisma";
4 |
5 | // POST: Log daily user login and update streak
6 | export async function POST(req: NextRequest) {
7 | const { userId } = getAuth(req);
8 | if (!userId) {
9 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
10 | }
11 |
12 | try {
13 | const today = new Date();
14 | today.setHours(0, 0, 0, 0); // Set to start of day
15 |
16 | // Get current user data
17 | const user = await prisma.user.findUnique({
18 | where: { id: userId },
19 | select: { lastLogin: true, currentStreak: true },
20 | });
21 |
22 | if (!user) {
23 | return NextResponse.json({ message: "User not found" }, { status: 404 });
24 | }
25 |
26 | const lastLoginDate = new Date(user.lastLogin);
27 | lastLoginDate.setHours(0, 0, 0, 0);
28 |
29 | // Check if user already logged in today
30 | if (lastLoginDate.getTime() === today.getTime()) {
31 | return NextResponse.json({
32 | message: "Login already recorded for today",
33 | alreadyLogged: true,
34 | currentStreak: user.currentStreak,
35 | });
36 | }
37 |
38 | // Calculate new streak
39 | let newStreak = user.currentStreak;
40 | const yesterday = new Date(today);
41 | yesterday.setDate(today.getDate() - 1);
42 |
43 | if (lastLoginDate.getTime() === yesterday.getTime()) {
44 | // Consecutive day - increment streak
45 | newStreak += 1;
46 | } else if (lastLoginDate.getTime() < yesterday.getTime()) {
47 | // Gap in login - reset streak to 1
48 | newStreak = 1;
49 | }
50 | // If lastLoginDate > yesterday, it's a future date (shouldn't happen), keep current streak
51 |
52 | // Update user with new login time and streak
53 | const updatedUser = await prisma.user.update({
54 | where: { id: userId },
55 | data: {
56 | lastLogin: today,
57 | currentStreak: newStreak,
58 | },
59 | select: { lastLogin: true, currentStreak: true },
60 | });
61 |
62 | return NextResponse.json({
63 | message: "Daily login recorded successfully",
64 | currentStreak: updatedUser.currentStreak,
65 | alreadyLogged: false,
66 | });
67 | } catch (error) {
68 | console.error("Error logging daily login:", error);
69 | return NextResponse.json(
70 | { message: "Internal server error" },
71 | { status: 500 }
72 | );
73 | }
74 | }
75 |
76 | // GET: Retrieve current streak score
77 | export async function GET(req: NextRequest) {
78 | const { userId } = getAuth(req);
79 | if (!userId) {
80 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
81 | }
82 |
83 | try {
84 | const user = await prisma.user.findUnique({
85 | where: { id: userId },
86 | select: { currentStreak: true },
87 | });
88 |
89 | if (!user) {
90 | return NextResponse.json({ message: "User not found" }, { status: 404 });
91 | }
92 |
93 | return NextResponse.json({
94 | currentStreak: user.currentStreak,
95 | });
96 | } catch (error) {
97 | console.error("Error fetching streak:", error);
98 | return NextResponse.json(
99 | { message: "Internal server error" },
100 | { status: 500 }
101 | );
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/prisma/migrations/20250414072937_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "ResourceType" AS ENUM ('ARTICLE', 'VIDEO', 'PDF');
3 |
4 | -- CreateTable
5 | CREATE TABLE "User" (
6 | "id" TEXT NOT NULL,
7 | "username" TEXT NOT NULL,
8 | "email" TEXT NOT NULL,
9 | "mobile" TEXT NOT NULL,
10 | "dob" TEXT NOT NULL,
11 |
12 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
13 | );
14 |
15 | -- CreateTable
16 | CREATE TABLE "Topic" (
17 | "id" TEXT NOT NULL,
18 | "userId" TEXT NOT NULL,
19 | "title" TEXT NOT NULL,
20 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
21 | "updatedAt" TIMESTAMP(3) NOT NULL,
22 |
23 | CONSTRAINT "Topic_pkey" PRIMARY KEY ("id")
24 | );
25 |
26 | -- CreateTable
27 | CREATE TABLE "Resource" (
28 | "id" TEXT NOT NULL,
29 | "topicId" TEXT NOT NULL,
30 | "title" TEXT NOT NULL,
31 | "type" "ResourceType" NOT NULL,
32 | "url" TEXT NOT NULL,
33 | "summary" TEXT NOT NULL,
34 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
35 | "updatedAt" TIMESTAMP(3) NOT NULL,
36 |
37 | CONSTRAINT "Resource_pkey" PRIMARY KEY ("id")
38 | );
39 |
40 | -- CreateTable
41 | CREATE TABLE "Quiz" (
42 | "id" TEXT NOT NULL,
43 | "resourceId" TEXT NOT NULL,
44 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
45 | "updatedAt" TIMESTAMP(3) NOT NULL,
46 |
47 | CONSTRAINT "Quiz_pkey" PRIMARY KEY ("id")
48 | );
49 |
50 | -- CreateTable
51 | CREATE TABLE "QuizQA" (
52 | "id" TEXT NOT NULL,
53 | "quizId" TEXT NOT NULL,
54 | "question" TEXT NOT NULL,
55 | "options" TEXT[],
56 | "correctAnswer" TEXT NOT NULL,
57 | "explanation" TEXT,
58 |
59 | CONSTRAINT "QuizQA_pkey" PRIMARY KEY ("id")
60 | );
61 |
62 | -- CreateTable
63 | CREATE TABLE "QuizResult" (
64 | "id" TEXT NOT NULL,
65 | "userId" TEXT NOT NULL,
66 | "quizId" TEXT NOT NULL,
67 | "score" INTEGER NOT NULL,
68 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
69 | "updatedAt" TIMESTAMP(3) NOT NULL,
70 |
71 | CONSTRAINT "QuizResult_pkey" PRIMARY KEY ("id")
72 | );
73 |
74 | -- CreateIndex
75 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
76 |
77 | -- CreateIndex
78 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
79 |
80 | -- CreateIndex
81 | CREATE UNIQUE INDEX "Quiz_resourceId_key" ON "Quiz"("resourceId");
82 |
83 | -- AddForeignKey
84 | ALTER TABLE "Topic" ADD CONSTRAINT "Topic_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
85 |
86 | -- AddForeignKey
87 | ALTER TABLE "Resource" ADD CONSTRAINT "Resource_topicId_fkey" FOREIGN KEY ("topicId") REFERENCES "Topic"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
88 |
89 | -- AddForeignKey
90 | ALTER TABLE "Quiz" ADD CONSTRAINT "Quiz_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
91 |
92 | -- AddForeignKey
93 | ALTER TABLE "QuizQA" ADD CONSTRAINT "QuizQA_quizId_fkey" FOREIGN KEY ("quizId") REFERENCES "Quiz"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
94 |
95 | -- AddForeignKey
96 | ALTER TABLE "QuizResult" ADD CONSTRAINT "QuizResult_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
97 |
98 | -- AddForeignKey
99 | ALTER TABLE "QuizResult" ADD CONSTRAINT "QuizResult_quizId_fkey" FOREIGN KEY ("quizId") REFERENCES "Quiz"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
100 |
--------------------------------------------------------------------------------
/lib/processPrompt.ts:
--------------------------------------------------------------------------------
1 | import OpenAI from "openai";
2 | import { GoogleGenerativeAI } from "@google/generative-ai";
3 |
4 | const provider = process.env.AI_PROVIDER || "gemini";
5 |
6 | function getAIClient() {
7 | //
8 | // --- OpenAI ---
9 | //
10 | if (provider === "openai") {
11 | const apiKey = process.env.OPENAI_API_KEY;
12 | const model = process.env.OPENAI_MODEL;
13 |
14 | if (!apiKey) throw new Error("OPENAI_API_KEY is required");
15 | if (!model) throw new Error("OPENAI_MODEL is required");
16 |
17 | const client = new OpenAI({ apiKey });
18 |
19 | return {
20 | provider: "openai" as const,
21 | model,
22 | instance: client,
23 | };
24 | }
25 |
26 | //
27 | // --- DeepSeek ---
28 | //
29 | if (provider === "deepseek") {
30 | const apiKey = process.env.DEEPSEEK_API_KEY;
31 | const model = process.env.DEEPSEEK_MODEL;
32 |
33 | if (!apiKey) throw new Error("DEEPSEEK_API_KEY is required");
34 | if (!model) throw new Error("DEEPSEEK_MODEL is required");
35 |
36 | // Uses OpenAI-compatible API
37 | const client = new OpenAI({
38 | apiKey,
39 | baseURL: process.env.DEEPSEEK_BASE_URL || "https://api.deepseek.com",
40 | });
41 |
42 | return {
43 | provider: "deepseek" as const,
44 | model,
45 | instance: client,
46 | };
47 | }
48 |
49 | //
50 | // --- Gemini (default) ---
51 | //
52 | const geminiKey = process.env.GEMINI_API_KEY;
53 | const geminiModel = process.env.GEMINI_MODEL;
54 |
55 | if (!geminiKey) throw new Error("GEMINI_API_KEY is required");
56 | if (!geminiModel) throw new Error("GEMINI_MODEL is required");
57 |
58 | const gemini = new GoogleGenerativeAI(geminiKey);
59 |
60 | return {
61 | provider: "gemini" as const,
62 | model: geminiModel,
63 | instance: gemini.getGenerativeModel({ model: geminiModel }),
64 | };
65 | }
66 |
67 | /**
68 | * Process a prompt with the configured AI provider
69 | * @param systemPrompt - The system instruction for the AI
70 | * @param userPrompt - The user's actual prompt/content
71 | * @returns The AI's response as a string
72 | */
73 | export async function processPrompt(
74 | systemPrompt: string,
75 | userPrompt: string = ""
76 | ): Promise {
77 | const ai = getAIClient();
78 |
79 | try {
80 | //
81 | // --- Gemini ---
82 | //
83 | if (ai.provider === "gemini") {
84 | // Gemini doesn't have separate system/user roles in the same way
85 | // We combine them with clear separation
86 | const combinedPrompt = `${systemPrompt}
87 |
88 | ---
89 |
90 | ${userPrompt}`;
91 |
92 | const result = await ai.instance.generateContent(combinedPrompt);
93 | return result.response.text();
94 | }
95 |
96 | //
97 | // --- OpenAI & DeepSeek ---
98 | //
99 | const completion = await ai.instance.chat.completions.create({
100 | model: ai.model,
101 | messages: [
102 | { role: "system", content: systemPrompt },
103 | { role: "user", content: userPrompt },
104 | ],
105 | temperature: 0.7,
106 | max_tokens: 4096,
107 | });
108 |
109 | return completion.choices[0].message?.content || "";
110 | } catch (error) {
111 | console.error(`${ai.provider} API Error:`, error);
112 | throw new Error(
113 | `AI service request failed: ${
114 | error instanceof Error ? error.message : String(error)
115 | }`
116 | );
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/app/api/contact/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import prisma from "@/lib/prisma";
3 | import { checkRateLimit } from "@/lib/rateLimiter";
4 |
5 |
6 | interface ContactRequestBody {
7 | name: string;
8 | email: string;
9 | subject?: string;
10 | message: string;
11 | userAnswer: number;
12 | answer: number;
13 | }
14 |
15 | export async function POST(request: NextRequest) {
16 | try {
17 | const body: ContactRequestBody = await request.json();
18 | // CAPTCHA check
19 | if (body.userAnswer !== body.answer) {
20 | return NextResponse.json(
21 | { error: "Incorrect CAPTCHA answer." },
22 | { status: 400 }
23 | );
24 | }
25 |
26 | // Rate limiting by IP
27 | const ip = request.headers.get("x-forwarded-for") ?? "unknown";
28 | if (!ip || !checkRateLimit(ip)) {
29 | return NextResponse.json(
30 | { error: "Too many submissions. Try again later." },
31 | { status: 429 }
32 | );
33 | }
34 | // Validate required fields
35 | if (!body.name || !body.email || !body.message) {
36 | return NextResponse.json(
37 | { message: "Name, email, and message are required" },
38 | { status: 400 }
39 | );
40 | }
41 |
42 | // Basic email validation
43 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
44 | if (!emailRegex.test(body.email)) {
45 | return NextResponse.json(
46 | { message: "Please provide a valid email address" },
47 | { status: 400 }
48 | );
49 | }
50 |
51 | // Validate message length
52 | if (body.message.trim().length < 10) {
53 | return NextResponse.json(
54 | { message: "Message must be at least 10 characters long" },
55 | { status: 400 }
56 | );
57 | }
58 |
59 | // Validate name length
60 | if (body.name.trim().length < 2) {
61 | return NextResponse.json(
62 | { message: "Name must be at least 2 characters long" },
63 | { status: 400 }
64 | );
65 | }
66 |
67 | // Save to database
68 | const contactQuery = await prisma.contactQuery.create({
69 | data: {
70 | name: body.name.trim(),
71 | email: body.email.trim(),
72 | subject: body.subject?.trim() || null,
73 | message: body.message.trim(),
74 | },
75 | });
76 |
77 | console.log("Contact query saved:", contactQuery.id);
78 |
79 | return NextResponse.json(
80 | {
81 | message: "Your message has been sent successfully! We'll get back to you soon.",
82 | id: contactQuery.id
83 | },
84 | { status: 201 }
85 | );
86 |
87 | } catch (error) {
88 | console.error("Error processing contact form:", error);
89 |
90 | // Handle specific Prisma errors
91 | if (error instanceof Error) {
92 | if (error.message.includes("unique constraint")) {
93 | return NextResponse.json(
94 | { message: "There was an issue processing your request. Please try again." },
95 | { status: 400 }
96 | );
97 | }
98 | }
99 |
100 | return NextResponse.json(
101 | { message: "Internal server error. Please try again later." },
102 | { status: 500 }
103 | );
104 | }
105 | }
106 |
107 | // Handle GET requests (optional - for health check or documentation)
108 | export async function GET() {
109 | return NextResponse.json(
110 | {
111 | message: "Contact API endpoint",
112 | methods: ["POST"],
113 | fields: ["name", "email", "subject (optional)", "message"]
114 | },
115 | { status: 200 }
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/app/(public)/blogs/page.tsx:
--------------------------------------------------------------------------------
1 | import BlogCard from "@/components/blog/BlogCard";
2 | import Link from "next/link";
3 | import { getAllBlogPosts } from "@/lib/blog";
4 | import Footer from "@/components/Footer";
5 | import { generateMetadataUtil } from "@/utils/generateMetadata";
6 |
7 | export const metadata = generateMetadataUtil({
8 | title: "Blog",
9 | description: "Discover the latest insights, tips, and strategies for smarter learning with AI. Read our blog for study techniques, productivity hacks, and educational technology updates.",
10 | keywords: [
11 | "Smriti AI Blog",
12 | "learning blog",
13 | "study tips",
14 | "AI education",
15 | "productivity blog",
16 | "learning strategies",
17 | "educational technology",
18 | "study techniques",
19 | "memory retention tips",
20 | "AI learning insights"
21 | ],
22 | url: "https://www.smriti.live/blogs",
23 | });
24 |
25 | export default async function BlogsPage() {
26 | //the below line is commented out to use dummy data for layout testing
27 | //when using real data, uncomment it
28 |
29 | const posts = await getAllBlogPosts();
30 |
31 | //dummy posts for layout testing
32 |
33 | // const posts = [
34 | // {
35 | // title: "Sample Blog Post",
36 | // slug: "sample-blog",
37 | // featureImage: { url: "https://via.placeholder.com/600x400" },
38 | // author: "John Doe",
39 | // publishDate: new Date().toISOString(),
40 | // summary: "This is a test excerpt for the blog layout.",
41 | // },
42 | // {
43 | // title: "Another Post",
44 | // slug: "another-post",
45 | // featureImage: { url: "https://via.placeholder.com/600x400" },
46 | // author: "Jane Smith",
47 | // publishDate: new Date().toISOString(),
48 | // summary: "Second test excerpt, to check grid responsiveness.",
49 | // },
50 | // ];
51 |
52 | //above data is dummy data for layout testing
53 |
54 | return (
55 |
56 | Blogs
57 | {/* 1 col on mobile, 2 on md, 3 on lg+ */}
58 |
59 | {posts.map((post) => (
60 |
69 | ))}
70 |
71 |
72 | {/*
73 | {posts.map((post) => (
74 |
78 |
79 |
80 | {post.title}
81 |
82 |
83 | By {post.author} •{" "}
84 | {new Date(post.publishDate).toLocaleDateString()}
85 |
86 |
{post.summary}
87 |
91 | Read more →
92 |
93 |
94 |
95 | ))}
96 |
*/}
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/components/ui/radialcharttext.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { TrendingUp } from "lucide-react"
4 | import {
5 | Label,
6 | PolarGrid,
7 | PolarRadiusAxis,
8 | RadialBar,
9 | RadialBarChart,
10 | } from "recharts"
11 |
12 | import {
13 | Card,
14 | CardContent,
15 | CardDescription,
16 | CardFooter,
17 | CardHeader,
18 | CardTitle,
19 | } from "@/components/ui/card"
20 | import { ChartConfig, ChartContainer } from "@/components/ui/chart"
21 | const chartData = [
22 | { browser: "safari", visitors: 200, fill: "var(--color-safari)" },
23 | ]
24 |
25 | const chartConfig = {
26 | visitors: {
27 | label: "Visitors",
28 | },
29 | safari: {
30 | label: "Safari",
31 | color: "hsl(var(--chart-2))",
32 | },
33 | } satisfies ChartConfig
34 |
35 | export function Component() {
36 | return (
37 |
38 |
39 | Radial Chart - Text
40 | January - June 2024
41 |
42 |
43 |
47 |
54 |
61 |
62 |
63 | {
65 | if (viewBox && "cx" in viewBox && "cy" in viewBox) {
66 | return (
67 |
73 |
78 | {chartData[0].visitors.toLocaleString()}
79 |
80 |
85 | Visitors
86 |
87 |
88 | )
89 | }
90 | }}
91 | />
92 |
93 |
94 |
95 |
96 |
97 |
98 | Trending up by 5.2% this month
99 |
100 |
101 | Showing total visitors for the last 6 months
102 |
103 |
104 |
105 | )
106 | }
107 |
--------------------------------------------------------------------------------
/app/api/interview/generate/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { getAuth } from "@clerk/nextjs/server";
3 | import prisma from "@/lib/prisma";
4 | import { processPrompt } from "@/lib/processPrompt";
5 |
6 | type GeneratedMCQ = {
7 | question: string;
8 | options: string[];
9 | correctAnswer: string;
10 | explanation?: string;
11 | difficulty?: string;
12 | };
13 |
14 | async function askGeminiForMCQs(
15 | language: string,
16 | domain: string,
17 | count: number
18 | ): Promise {
19 | const prompt = [
20 | "You are an expert interviewer.",
21 | `Create ${count} high-quality multiple-choice questions for ${language} in the domain of ${domain}.`,
22 | "Return STRICT JSON array only. Each item must have keys: question, options (exactly 4), correctAnswer (exact string from options), explanation (<=200 chars), difficulty (Easy|Medium|Hard).",
23 | 'Example: [ { "question": "...", "options": ["A","B","C","D"], "correctAnswer": "A", "explanation": "...", "difficulty": "Medium" } ]',
24 | ].join("\n");
25 |
26 | const text = await processPrompt(prompt);
27 | const match = text.match(/\[[\s\S]*\]/);
28 | const json = JSON.parse(match ? match[0] : text);
29 | if (!Array.isArray(json)) return [];
30 | return json.slice(0, count) as GeneratedMCQ[];
31 | }
32 |
33 | export async function POST(req: NextRequest) {
34 | const { userId } = getAuth(req);
35 | if (!userId)
36 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
37 |
38 | try {
39 | const { language, domain, domains, count = 10 } = await req.json();
40 | if (
41 | !language ||
42 | (!domain && (!domains || !Array.isArray(domains) || domains.length === 0))
43 | ) {
44 | return NextResponse.json(
45 | { message: "language and domain(s) are required" },
46 | { status: 400 }
47 | );
48 | }
49 | const selectedDomains: string[] = (
50 | Array.isArray(domains) ? domains : [domain]
51 | ).filter(Boolean);
52 |
53 | const joinedDomain = selectedDomains.join(", ");
54 | const items = await askGeminiForMCQs(
55 | String(language),
56 | joinedDomain,
57 | Number(count)
58 | );
59 | if (!items.length)
60 | return NextResponse.json(
61 | { message: "Failed to generate questions" },
62 | { status: 502 }
63 | );
64 |
65 | const quiz = await (prisma as any).interviewQuiz.create({
66 | data: {
67 | userId,
68 | language: String(language),
69 | domain: joinedDomain || selectedDomains[0] || "",
70 | questions: {
71 | create: items.map((q: GeneratedMCQ) => ({
72 | question: q.question,
73 | options: q.options,
74 | correctAnswer: q.correctAnswer,
75 | explanation: q.explanation ?? null,
76 | difficulty: q.difficulty ?? "Medium",
77 | })),
78 | },
79 | },
80 | include: { questions: true },
81 | });
82 |
83 | const sanitizedQuestions = (
84 | quiz.questions as Array<{
85 | id: string;
86 | question: string;
87 | options: string[];
88 | difficulty?: string;
89 | }>
90 | ).map((q) => ({
91 | id: q.id,
92 | question: q.question,
93 | options: q.options,
94 | difficulty: q.difficulty,
95 | }));
96 |
97 | return NextResponse.json({
98 | quizId: quiz.id,
99 | language: quiz.language,
100 | domain: quiz.domain,
101 | createdAt: quiz.createdAt,
102 | questions: sanitizedQuestions,
103 | });
104 | } catch (e) {
105 | console.error("[INTERVIEW_GENERATE]", e);
106 | return NextResponse.json(
107 | { message: "Internal server error" },
108 | { status: 500 }
109 | );
110 | }
111 | }
112 |
--------------------------------------------------------------------------------