Skip to content

Commit e1670f8

Browse files
committed
finalise postgres schema and components
1 parent a434fee commit e1670f8

25 files changed

+2663
-7376
lines changed

bun.lock

Lines changed: 1117 additions & 0 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 0 additions & 6409 deletions
This file was deleted.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"@google/generative-ai": "^0.24.1",
1213
"@hookform/resolvers": "^3.3.4",
1314
"@next-auth/prisma-adapter": "^1.0.7",
1415
"@prisma/client": "^5.8.1",
@@ -35,6 +36,7 @@
3536
"react-hook-form": "^7.49.3",
3637
"stripe": "^14.14.0",
3738
"tailwind-merge": "^2.2.0",
39+
"tailwind-scrollbar": "^4.0.2",
3840
"tailwindcss-animate": "^1.0.7",
3941
"youtube-transcript": "^1.0.6",
4042
"zod": "^3.22.4"

prisma/schema.prisma

Lines changed: 66 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,104 @@
11
generator client {
2-
provider = "prisma-client-js"
2+
provider = "prisma-client-js"
33
}
44

55
datasource db {
6-
provider = "mysql"
7-
url = env("DATABASE_URL")
8-
relationMode = "prisma"
6+
provider = "postgresql"
7+
url = env("DATABASE_URL")
98
}
109

1110
model Account {
12-
id String @id @default(cuid())
13-
userId String
14-
type String
15-
provider String
16-
providerAccountId String
17-
refresh_token String? @db.Text
18-
access_token String? @db.Text
19-
expires_at Int?
20-
token_type String?
21-
scope String?
22-
id_token String? @db.Text
23-
session_state String?
24-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
25-
26-
@@unique([provider, providerAccountId])
27-
@@index([userId], name: "userId")
11+
id String @id @default(cuid())
12+
userId String
13+
type String
14+
provider String
15+
providerAccountId String
16+
refresh_token String?
17+
access_token String?
18+
expires_at Int?
19+
token_type String?
20+
scope String?
21+
id_token String?
22+
session_state String?
23+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
24+
25+
@@unique([provider, providerAccountId])
26+
@@index([userId])
2827
}
2928

3029
model Session {
31-
id String @id @default(cuid())
32-
sessionToken String @unique
33-
userId String
34-
expires DateTime
35-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
30+
id String @id @default(cuid())
31+
sessionToken String @unique
32+
userId String
33+
expires DateTime
34+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
3635
37-
@@index([userId], name: "userId")
36+
@@index([userId])
3837
}
3938

4039
model User {
41-
id String @id @default(cuid())
42-
name String?
43-
email String? @unique
44-
emailVerified DateTime?
45-
image String?
46-
credits Int @default(10)
47-
48-
accounts Account[]
49-
sessions Session[]
40+
id String @id @default(cuid())
41+
name String?
42+
email String? @unique
43+
emailVerified DateTime?
44+
image String?
45+
credits Int @default(10)
46+
47+
accounts Account[]
48+
sessions Session[]
5049
}
5150

5251
// course contains many units
5352
model Course {
54-
id String @id @default(cuid())
55-
name String
56-
image String
57-
units Unit[]
53+
id String @id @default(cuid())
54+
name String
55+
image String
56+
units Unit[]
5857
}
5958

6059
// unit contains many chapters
6160
model Unit {
62-
id String @id @default(cuid())
63-
courseId String
61+
id String @id @default(cuid())
62+
courseId String
6463
65-
name String
66-
course Course @relation(fields: [courseId], references: [id])
67-
chapters Chapter[]
64+
name String
65+
course Course @relation(fields: [courseId], references: [id])
66+
chapters Chapter[]
6867
69-
@@index([courseId], name: "courseId")
68+
@@index([courseId])
7069
}
7170

7271
model Chapter {
73-
id String @id @default(cuid())
74-
unitId String
72+
id String @id @default(cuid())
73+
unitId String
7574
76-
name String
77-
youtubeSearchQuery String
78-
videoId String?
79-
summary String? @db.VarChar(3000)
80-
unit Unit @relation(fields: [unitId], references: [id])
81-
questions Question[]
75+
name String
76+
youtubeSearchQuery String
77+
videoId String?
78+
summary String?
79+
unit Unit @relation(fields: [unitId], references: [id])
80+
questions Question[]
8281
83-
@@index([unitId], name: "unitId")
82+
@@index([unitId])
8483
}
8584

8685
model Question {
87-
id String @id @default(cuid())
88-
chapterId String
86+
id String @id @default(cuid())
87+
chapterId String
8988
90-
question String @db.VarChar(3000)
91-
answer String @db.VarChar(3000)
92-
options String @db.VarChar(3000)
93-
chapter Chapter @relation(fields: [chapterId], references: [id])
89+
question String
90+
answer String
91+
options String
92+
chapter Chapter @relation(fields: [chapterId], references: [id])
9493
95-
@@index([chapterId], name: "chapterId")
94+
@@index([chapterId])
9695
}
9796

9897
model UserSubscription {
99-
id String @id @default(cuid())
100-
userId String @unique
101-
stripeCustomerId String @unique @map(name: "stripe_customer_id")
102-
stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id")
103-
stripePriceId String? @map(name: "stripe_price_id")
104-
stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end")
98+
id String @id @default(cuid())
99+
userId String @unique
100+
stripeCustomerId String @unique @map("stripe_customer_id")
101+
stripeSubscriptionId String? @unique @map("stripe_subscription_id")
102+
stripePriceId String? @map("stripe_price_id")
103+
stripeCurrentPeriodEnd DateTime? @map("stripe_current_period_end")
105104
}
Lines changed: 92 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,121 @@
1-
import { db } from '@/lib/db';
2-
import { strict_output } from '@/lib/gpt';
31
import { getQuestionsFromTranscript, getTranscript, searchYoutube } from '@/lib/youtube';
2+
43
import { NextResponse } from 'next/server';
5-
import { resolve } from 'path';
4+
import { db } from '@/lib/db';
5+
import { strict_output } from '@/lib/gpt';
66
import { z } from 'zod';
77

8-
// api/chapter/getInfo
9-
10-
// sleep for 0-4 seconds to simulate async behavior of real world api calls to database etc.
11-
128
const bodyParser = z.object({
13-
// converting the body to a zod schema to validate it and get the types
149
chapterId: z.string(),
1510
});
16-
// const sleep = async () =>
17-
// new Promise((resolve) => {
18-
// setTimeout(resolve, Math.random() * 4000);
19-
// });
11+
12+
// Proper Fisher-Yates shuffle implementation
13+
const shuffleArray = (array: string[]) => {
14+
for (let i = array.length - 1; i > 0; i--) {
15+
const j = Math.floor(Math.random() * (i + 1));
16+
[array[i], array[j]] = [array[j], array[i]];
17+
}
18+
return array;
19+
};
2020

2121
export async function POST(req: Request, res: Response) {
2222
try {
23+
console.log('[CHAPTER PROCESSING] Starting processing');
24+
2325
const body = await req.json();
2426
const { chapterId } = bodyParser.parse(body);
2527

26-
// Get Chapter Info from Database using chapterId
27-
28+
// Get Chapter Info
2829
const chapter = await db.chapter.findUnique({
29-
where: {
30-
id: chapterId,
31-
},
30+
where: { id: chapterId },
3231
});
3332

3433
if (!chapter) {
35-
return NextResponse.json(
36-
{
37-
success: false,
38-
error: 'Chapter not found',
39-
},
40-
{
41-
status: 404,
42-
},
43-
);
34+
console.error(`[CHAPTER ${chapterId}] Not found`);
35+
return NextResponse.json({ success: false, error: 'Chapter not found' }, { status: 404 });
4436
}
4537

46-
// Get Youtube Video Id from Youtube Search Query
38+
// Validate YouTube search query
39+
if (!chapter.youtubeSearchQuery?.trim()) {
40+
console.error(`[CHAPTER ${chapterId}] Invalid search query`);
41+
return NextResponse.json({ success: false, error: 'Invalid YouTube search query' }, { status: 400 });
42+
}
43+
44+
// Get YouTube Video ID
4745
const videoId = await searchYoutube(chapter.youtubeSearchQuery);
46+
if (!videoId) {
47+
console.error(`[CHAPTER ${chapterId}] No YouTube video found`);
48+
return NextResponse.json({ success: false, error: 'No YouTube video found' }, { status: 404 });
49+
}
50+
console.log(`[CHAPTER ${chapterId}] Found video ID: ${videoId}`);
4851

49-
// Get Transcript from Youtube Video Id
50-
let videoTranscript = await getTranscript(videoId);
51-
52-
// Getting summary from transcript using GPT-3 API (OpenAI) and returning it in the response (strict_output is a helper function to make the API call to GPT-3) (see lib/gpt.ts)
53-
let maxLength = 500;
54-
videoTranscript = videoTranscript.split(' ').slice(0, maxLength).join(' ');
55-
const { summary }: { summary: string } = await strict_output(
56-
'You are an AI capable of summarising a youtube transcript',
57-
'summarise in 250 words or less and do not talk of the sponsors or anything unrelated to the main topic, also do not introduce what the summary is about.\n' +
58-
videoTranscript,
59-
{ summary: 'summary of the transcript' },
60-
);
61-
62-
const questions = await getQuestionsFromTranscript(videoTranscript, chapter.name);
63-
64-
// create questions in database
65-
await db.question.createMany({
66-
data: questions.map((question) => {
67-
let options = [question.answer, question.option1, question.option2, question.option3];
68-
options = options.sort(() => Math.random() - 0.5); // shuffle the options array randomly (see https://stackoverflow.com/a/2450976/13697995)
69-
return {
70-
question: question.question,
71-
answer: question.answer,
72-
options: JSON.stringify(options),
73-
chapterId: chapterId,
74-
};
75-
}),
76-
});
52+
// Get Transcript
53+
const videoTranscript = await getTranscript(videoId);
54+
if (!videoTranscript) {
55+
console.error(`[CHAPTER ${chapterId}] No transcript available`);
56+
return NextResponse.json({ success: false, error: 'No transcript available' }, { status: 404 });
57+
}
58+
console.log(`[CHAPTER ${chapterId}] Transcript length: ${videoTranscript.length}`);
7759

78-
// Update Chapter in Database with videoId and summary
60+
// Truncate transcript intelligently
61+
const truncatedTranscript = videoTranscript.split(' ').slice(0, 500).join(' ');
7962

80-
await db.chapter.update({
81-
where: {
82-
id: chapterId,
83-
},
84-
data: {
85-
videoId,
86-
summary,
87-
},
88-
});
63+
// Generate Summary
64+
let summary;
65+
try {
66+
const result = await strict_output(
67+
'You are an AI capable of summarising YouTube transcripts',
68+
`Summarize this transcript in 250 words or less, focusing on core concepts:\n${truncatedTranscript}`,
69+
{ summary: 'summary of the transcript' },
70+
);
71+
summary = result.summary;
72+
} catch (error) {
73+
console.error(`[CHAPTER ${chapterId}] Summary generation failed:`, error);
74+
return NextResponse.json({ success: false, error: 'Failed to generate summary' }, { status: 500 });
75+
}
8976

90-
return NextResponse.json({
91-
success: true,
92-
});
77+
// Generate Questions
78+
let questions;
79+
try {
80+
questions = await getQuestionsFromTranscript(truncatedTranscript, chapter.name);
81+
} catch (error) {
82+
console.error(`[CHAPTER ${chapterId}] Question generation failed:`, error);
83+
return NextResponse.json({ success: false, error: 'Failed to generate questions' }, { status: 500 });
84+
}
85+
86+
// Database Transaction
87+
try {
88+
await db.$transaction([
89+
db.chapter.update({
90+
where: { id: chapterId },
91+
data: { videoId, summary },
92+
}),
93+
db.question.createMany({
94+
data: questions.map((question) => {
95+
const options = shuffleArray([question.answer, question.option1, question.option2, question.option3]);
96+
return {
97+
question: question.question,
98+
answer: question.answer,
99+
options: JSON.stringify(options),
100+
chapterId: chapterId,
101+
};
102+
}),
103+
}),
104+
]);
105+
console.log(`[CHAPTER ${chapterId}] Successfully updated database`);
106+
} catch (dbError) {
107+
console.error(`[CHAPTER ${chapterId}] Database transaction failed:`, dbError);
108+
return NextResponse.json({ success: false, error: 'Database update failed' }, { status: 500 });
109+
}
110+
111+
return NextResponse.json({ success: true });
93112
} catch (error) {
113+
console.error('[CHAPTER PROCESSING] General error:', error);
114+
94115
if (error instanceof z.ZodError) {
95-
return NextResponse.json(
96-
{
97-
success: false,
98-
error: error.issues,
99-
},
100-
{
101-
status: 400,
102-
},
103-
);
104-
} else {
105-
return NextResponse.json(
106-
{
107-
success: false,
108-
error: 'Something went wrong',
109-
},
110-
{
111-
status: 500,
112-
},
113-
);
116+
return NextResponse.json({ success: false, error: error.errors }, { status: 400 });
114117
}
118+
119+
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 });
115120
}
116121
}

0 commit comments

Comments
 (0)