Skip to content

Commit b021345

Browse files
authored
Merge pull request #68 from guillermoscript/64-free-chat-implement-ai-powered-conversational-interface
"Add chat functionality for students"
2 parents f867522 + bd3cc10 commit b021345

File tree

27 files changed

+769
-926
lines changed

27 files changed

+769
-926
lines changed

actions/dashboard/chatActions.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use server'
2+
3+
import { createResponse } from '@/utils/functions'
4+
import { createClient } from '@/utils/supabase/server'
5+
import { Tables } from '@/utils/supabase/supabase'
6+
7+
export async function studentCreateNewChat (state: {
8+
chatType: Tables<'chats'>['chat_type']
9+
title: string
10+
}) {
11+
const supabase = createClient()
12+
const userData = await supabase.auth.getUser()
13+
14+
if (userData.error) {
15+
return createResponse('error', 'Error no user found', null, 'Error no user found')
16+
}
17+
18+
const studentId = userData.data.user?.id
19+
20+
const chatInsert = await supabase.from('chats').insert({
21+
user_id: studentId,
22+
chat_type: state.chatType,
23+
created_at: new Date().toISOString(),
24+
title: state.title
25+
}).select('chat_id').single()
26+
27+
if (chatInsert.error) {
28+
return createResponse('error', 'Error creating chat', null, 'Error creating chat')
29+
}
30+
31+
// return chat id
32+
// revalidatePath('/dashboard/student/chat/', 'layout')
33+
return createResponse('success', 'Chat created successfully', chatInsert.data, null)
34+
}
35+
36+
export async function studentSubmitMessage (state: {
37+
chatId: number
38+
message: string
39+
}) {
40+
const supabase = createClient()
41+
const userData = await supabase.auth.getUser()
42+
43+
if (userData.error) {
44+
return createResponse('error', 'Error no user found', null, 'Error no user found')
45+
}
46+
47+
const studentId = userData.data.user?.id
48+
49+
const messageInsert = await supabase.from('messages').insert({
50+
user_id: studentId,
51+
chat_id: state.chatId,
52+
content: state.message,
53+
created_at: new Date().toISOString()
54+
})
55+
56+
if (messageInsert.error) {
57+
return createResponse('error', 'Error creating message', null, 'Error creating message')
58+
}
59+
60+
return createResponse('success', 'Message sent successfully', null, null)
61+
}

actions/dashboard/linkProductToCourse.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { createResponse } from '@/utils/functions'
44
export async function linkProductAction (data: courseSchemaType) {
55
console.log(data)
66

7-
if (!data.price || !data.status || !data.course_id) {
8-
return createResponse('error', 'Please fill in all fields', null, null)
9-
}
7+
// if (!data.price || !data.status || !data.course_id) {
8+
// return createResponse('error', 'Please fill in all fields', null, null)
9+
// }
1010

1111
return createResponse('success', 'Product linked successfully', null, null)
1212
}

app/api/chat/route.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,58 @@
11
import { google } from '@ai-sdk/google'
22
import { StreamingTextResponse, streamText } from 'ai'
33

4+
import { createClient } from '@/utils/supabase/server'
5+
// Allow streaming responses up to 30 seconds
6+
export const dynamic = 'force-dynamic'
7+
export const maxDuration = 60
8+
49
export async function POST (req: Request) {
5-
const { messages } = await req.json()
10+
const { messages, chatId } = await req.json()
11+
const supabase = createClient()
12+
13+
const userData = await supabase.auth.getUser()
14+
15+
if (userData.error) {
16+
return Response.redirect('/login')
17+
}
618

719
const result = await streamText({
820
model: google('models/gemini-1.5-pro-latest'),
921
messages,
10-
temperature: 0
22+
temperature: 0,
23+
async onFinish (event) {
24+
const lastUserMessage = messages[messages.length - 1]
25+
26+
console.log(messages)
27+
28+
console.log(lastUserMessage)
29+
30+
const chat_id = chatId ?? (await supabase.from('chats').select('chat_id').eq('title', lastUserMessage.content).single()).data.chat_id
31+
32+
console.log(chat_id)
33+
34+
const messageInsert = await supabase.from('messages').insert([
35+
{
36+
chat_id,
37+
message: lastUserMessage.content,
38+
created_at: new Date().toISOString(),
39+
sender: 'user'
40+
},
41+
{
42+
chat_id,
43+
message: event.text,
44+
created_at: new Date().toISOString(),
45+
sender: 'assistant'
46+
}
47+
48+
])
49+
50+
if (messageInsert.error) {
51+
console.log('Error creating message', messageInsert.error)
52+
}
53+
54+
console.log('Message sent successfully')
55+
}
1156
})
1257

1358
return new StreamingTextResponse(result.toAIStream())
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use client' // Error components must be Client Components
2+
3+
import { useEffect } from 'react'
4+
5+
import GenericError from '@/components/GenericError'
6+
7+
export default function Error ({
8+
error,
9+
reset
10+
}: {
11+
error: Error & { digest?: string }
12+
reset: () => void
13+
}) {
14+
useEffect(() => {
15+
// Log the error to an error reporting service
16+
console.error(error)
17+
}, [error])
18+
19+
return (
20+
<GenericError
21+
retry={reset}
22+
title="Oh no! An error occurred loading the chat."
23+
description={error.message}
24+
/>
25+
)
26+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import FreeChat from '@/components/dashboards/student/chat/FreeChat'
2+
import { createClient } from '@/utils/supabase/server'
3+
4+
export const dynamic = 'force-dynamic'
5+
export const maxDuration = 30
6+
7+
export default async function FreeChatPage ({
8+
params
9+
}: {
10+
params: {
11+
chatId: string
12+
}
13+
}) {
14+
const supabase = createClient()
15+
16+
const messagesData = await supabase
17+
.from('messages')
18+
.select('*')
19+
.eq('chat_id', Number(params.chatId))
20+
.order('created_at', { ascending: true })
21+
22+
if (messagesData.error) {
23+
console.log(messagesData.error)
24+
throw new Error('Error fetching messages')
25+
}
26+
27+
return (
28+
29+
<>
30+
<div className='flex flex-col gap-4 overflow-y-auto h-[calc(100vh-4rem)]'>
31+
32+
<FreeChat
33+
chatId={Number(params.chatId)}
34+
initialMessages={
35+
messagesData.data.map(message => ({
36+
id: message.id.toString(),
37+
content: message.message,
38+
role: message.sender
39+
}))
40+
}
41+
/>
42+
</div>
43+
</>
44+
)
45+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createClient } from '@/utils/supabase/server'
2+
3+
export default async function CoursesLayout ({
4+
children,
5+
params
6+
}: {
7+
children: React.ReactNode
8+
params: {
9+
chatId: string
10+
}
11+
}) {
12+
const supabase = createClient()
13+
14+
const chatData = await supabase
15+
.from('chats')
16+
.select('*')
17+
.eq('chat_id', Number(params.chatId))
18+
.single()
19+
20+
if (chatData.error) {
21+
console.log(chatData.error)
22+
throw new Error('Error fetching chat')
23+
}
24+
25+
return (
26+
<div className='flex flex-col gap-4 p-4'>
27+
<h1 className="text-2xl font-semibold text-gray-800">
28+
{chatData.data.title}
29+
</h1>
30+
{children}
31+
</div>
32+
)
33+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* v0 by Vercel.
3+
* @see https://v0.dev/t/vkBhDMemo6s
4+
* Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app
5+
*/
6+
import { Skeleton } from '@/components/ui/skeleton'
7+
8+
export default function Component () {
9+
return (
10+
<div className="grid min-h-screen w-full">
11+
12+
<div className="hidden border-r bg-gray-100/40 lg:block dark:bg-gray-800/40">
13+
<div className="flex h-full flex-col gap-2 max-h-[calc(100vh-6rem)]">
14+
<div className="flex h-[60px] items-center border-b px-6">
15+
<Skeleton className="h-6 w-6 rounded-full" />
16+
<Skeleton className="h-5 w-24 ml-2" />
17+
</div>
18+
<div className="flex-1 overflow-auto py-2">
19+
<nav className="grid gap-2 px-4">
20+
<Skeleton className="h-8 w-full rounded-md" />
21+
<Skeleton className="h-8 w-full rounded-md" />
22+
<Skeleton className="h-8 w-full rounded-md" />
23+
<Skeleton className="h-8 w-full rounded-md" />
24+
<Skeleton className="h-8 w-full rounded-md" />
25+
</nav>
26+
</div>
27+
<div className="mt-auto p-4">
28+
<Skeleton className="h-[125px] w-full rounded-lg" />
29+
</div>
30+
</div>
31+
</div>
32+
</div>
33+
)
34+
}

app/dashboard/student/chat/layout.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import StudentChatSidebar from '@/components/dashboards/student/chat/StudentChatSidebar'
2+
import { createClient } from '@/utils/supabase/server'
3+
4+
export default async function CoursesLayout ({
5+
children
6+
}: {
7+
children: React.ReactNode
8+
}) {
9+
const supabase = createClient()
10+
const user = await supabase.auth.getUser()
11+
12+
if (user.error != null) {
13+
throw new Error(user.error.message)
14+
}
15+
16+
const userCourses = await supabase
17+
.from('enrollments')
18+
.select('enrollment_id')
19+
.eq('user_id', user.data.user.id)
20+
21+
const userSubscriptions = await supabase
22+
.from('subscriptions')
23+
.select('subscription_id')
24+
.eq('user_id', user.data.user.id)
25+
.eq('subscription_status', 'active')
26+
27+
if (userSubscriptions.error != null && userCourses.error != null) {
28+
throw new Error(
29+
'Something went wrong while fetching your courses and subscriptions.'
30+
)
31+
}
32+
33+
if (userSubscriptions.data.length === 0 && userCourses.data.length === 0) {
34+
throw new Error('You are not authorized to view this page.')
35+
}
36+
37+
return (
38+
<div className="grid min-h-screen w-full lg:grid-cols-[280px_1fr] gap-6">
39+
<StudentChatSidebar
40+
userRole='student'
41+
/>
42+
<div className="flex flex-col">
43+
{children}
44+
</div>
45+
</div>
46+
)
47+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* v0 by Vercel.
3+
* @see https://v0.dev/t/vkBhDMemo6s
4+
* Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app
5+
*/
6+
import { Skeleton } from '@/components/ui/skeleton'
7+
8+
export default function Component () {
9+
return (
10+
<div className="grid min-h-screen w-full">
11+
12+
<div className="hidden border-r bg-gray-100/40 lg:block dark:bg-gray-800/40">
13+
<div className="flex h-full flex-col gap-2 max-h-[calc(100vh-6rem)]">
14+
<div className="flex h-[60px] items-center border-b px-6">
15+
<Skeleton className="h-6 w-6 rounded-full" />
16+
<Skeleton className="h-5 w-24 ml-2" />
17+
</div>
18+
<div className="flex-1 overflow-auto py-2">
19+
<nav className="grid gap-2 px-4">
20+
<Skeleton className="h-8 w-full rounded-md" />
21+
<Skeleton className="h-8 w-full rounded-md" />
22+
<Skeleton className="h-8 w-full rounded-md" />
23+
<Skeleton className="h-8 w-full rounded-md" />
24+
<Skeleton className="h-8 w-full rounded-md" />
25+
</nav>
26+
</div>
27+
<div className="mt-auto p-4">
28+
<Skeleton className="h-[125px] w-full rounded-lg" />
29+
</div>
30+
</div>
31+
</div>
32+
</div>
33+
)
34+
}

0 commit comments

Comments
 (0)