Skip to content

Commit

Permalink
Merge pull request #73 from kristianka/chat-page
Browse files Browse the repository at this point in the history
Chat page
  • Loading branch information
kristianka authored Nov 4, 2024
2 parents 20de2db + 928ffcc commit 6d95c9d
Show file tree
Hide file tree
Showing 11 changed files with 574 additions and 223 deletions.
126 changes: 95 additions & 31 deletions nextjs/src/app/api/openai/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ async function getAssistant(supabase: ReturnType<typeof createServiceRoleClient>
async function getOrCreateThread(
supabase: ReturnType<typeof createServiceRoleClient>,
supabaseUserId: string,
assistantId: string
assistantId: string,
threadId?: string
) {
if (threadId) {
return threadId;
}

const { data: profile, error: profileError } = await supabase
.from("profiles")
.select("id, thread_id")
Expand Down Expand Up @@ -92,16 +97,6 @@ async function getOrCreateThread(
throw new Error("Failed to create new thread");
}

const { error: updateError } = await supabase
.from("profiles")
.update({ thread_id: newThread.id })
.eq("id", profile.id);

if (updateError) {
console.error("Error updating profile's thread_id:", updateError);
throw new Error("Failed to associate profile with new thread");
}

return newThread.id;
}

Expand All @@ -113,10 +108,6 @@ async function postMessage(
messageContent: string,
messageType: string
) {
// console.log(
// `Inserting message into thread ${threadId || ""} from ${sender}: ${messageContent}`
// );

const { data: thread, error: threadError } = await supabase
.from("threads")
.select("id")
Expand Down Expand Up @@ -156,7 +147,7 @@ async function getThreadHistory(
) {
const { data, error } = await supabase
.from("messages")
.select("*")
.select("sender, message_content")
.eq("thread_id", threadId)
.eq("message_type", "chat")
.order("created_at", { ascending: true });
Expand All @@ -166,28 +157,102 @@ async function getThreadHistory(
throw new Error("Failed to retrieve thread history");
}

return data.map((message) => ({
role: message.sender,
content: message.message_content
return data.map((msg) => ({
role: msg.sender,
content: msg.message_content
}));
}

async function createNewThread(
supabase: ReturnType<typeof createServiceRoleClient>,
supabaseUserId: string,
assistantId: string
) {
const { data: profile, error: profileError } = await supabase
.from("profiles")
.select("id")
.eq("id", supabaseUserId)
.single();

if (profileError) {
console.error("Error fetching profile:", profileError);
throw new Error("Error fetching profile data");
}

if (!profile) {
console.error("Profile not found with Supabase User ID:", supabaseUserId);
throw new Error("Profile not found");
}

const { data: newThread, error: threadError } = await supabase
.from("threads")
.insert([{ user_id: profile.id, assistant_id: assistantId }])
.select()
.single();

if (threadError) {
console.error("Error creating thread:", threadError);
throw new Error("Failed to create new thread");
}

return newThread.id;
}

async function softDeleteThread(
supabase: ReturnType<typeof createServiceRoleClient>,
threadId: string
) {
const { error } = await supabase.from("threads").update({ deleted: true }).eq("id", threadId);

if (error) {
console.error("Error soft deleting thread:", error);
throw new Error("Failed to soft delete thread");
}

return { success: true };
}

export async function POST(req: Request) {
const { userId, message } = await req.json();
const { userId, message, action, threadId } = await req.json();

if (!userId) {
return NextResponse.json({ error: "Missing userId" }, { status: 400 });
}

if (!userId || !message) {
return NextResponse.json({ error: "Missing userId or message" }, { status: 400 });
if (action === "delete-thread" && !threadId) {
return NextResponse.json({ error: "Missing threadId" }, { status: 400 });
}

try {
const { supabase, openai } = await initClients();
const assistant = await getAssistant(supabase);

const threadId = await getOrCreateThread(supabase, userId, assistant.id);
if (action === "get-history") {
const threadIdOrNew =
threadId || (await getOrCreateThread(supabase, userId, assistant.id, threadId));
const previousMessages = await getThreadHistory(supabase, threadIdOrNew);
return NextResponse.json(previousMessages);
}

const previousMessages = await getThreadHistory(supabase, threadId);
if (action === "create-thread") {
const newThreadId = await createNewThread(supabase, userId, assistant.id);
return NextResponse.json({ threadId: newThreadId });
}

if (action === "delete-thread") {
await softDeleteThread(supabase, threadId);
return NextResponse.json({ success: true });
}

await postMessage(supabase, threadId, "user", message, "chat");
if (!message) {
return NextResponse.json({ error: "Missing message content" }, { status: 400 });
}

const threadIdOrNew =
threadId || (await getOrCreateThread(supabase, userId, assistant.id, threadId));
const previousMessages = await getThreadHistory(supabase, threadIdOrNew);

await postMessage(supabase, threadIdOrNew, "user", message, "chat");

const assistantResponse = await openai.chat.completions.create({
model: "gpt-4o-mini",
Expand All @@ -196,16 +261,15 @@ export async function POST(req: Request) {

const assistantMessage = assistantResponse.choices[0].message.content as string;

const serviceSupabase = createServiceRoleClient();
const cardData = await postMessage(
serviceSupabase,
threadId,
const insertedMessage = await postMessage(
supabase,
threadIdOrNew,
"assistant",
assistantMessage,
"chat"
);
const cardId = cardData.id;
return NextResponse.json({ assistantMessage, cardId });

return NextResponse.json({ assistantMessage, cardId: insertedMessage.id });
} catch (error) {
console.error("Error processing request:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
Expand Down
7 changes: 3 additions & 4 deletions nextjs/src/app/chat/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Metadata } from "next";
import { Container } from "@/components/landing_page/Container";

export const metadata: Metadata = {
title: "Chat - Study Tutor"
};

export default function ChatLayout({ children }: { children: React.ReactNode }) {
return (
<Container className="h-200px flex max-w-6xl flex-col rounded-lg p-2 sm:p-4">
{children}
</Container>
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-100">
<div className="w-full max-w-6xl rounded-lg bg-white p-4 shadow-lg">{children}</div>
</div>
);
}
20 changes: 17 additions & 3 deletions nextjs/src/app/chat/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import ChatContainer from "@/components/chat/ChatContainer";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import Chat from "@/components/chat/Chat";

export default async function ChatPage() {
const supabase = createClient();
const { data, error } = await supabase.auth.getUser();

if (error || !data?.user) {
redirect("/login");
}

const userId = data.user.id;

const { data: threads, error: threadsError } = await supabase
.from("threads")
.select("id, created_at")
.eq("user_id", userId)
.eq("deleted", false)
.order("created_at", { ascending: false });

if (threadsError) {
console.error("Error fetching threads:", threadsError);
}

return (
<div className="my-1">
<Chat userId={data.user.id} />
<div className="flex min-h-screen items-center justify-center bg-gray-100">
<ChatContainer userId={userId} initialThreads={threads || []} />
</div>
);
}
84 changes: 75 additions & 9 deletions nextjs/src/components/chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,78 @@
"use client";
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import { ChatBubble, ChatBubbleMessage } from "@/components/ui/chat/chat-bubble";
import { ChatMessageList } from "@/components/ui/chat/chat-message-list";
import { ChatInput } from "@/components/ui/chat/chat-input";

interface Message {
sender: string;
message_content: string;
}

interface ChatProps {
userId: string;
threadId: string | null;
onNewThread: () => void;
onSelectThread: (id: string) => void;
}

export default function Chat({ userId }: ChatProps) {
const [messages, setMessages] = useState<{ sender: string; content: string }[]>([]);
export default function Chat({
userId,
threadId,
onNewThread: _onNewThread,
onSelectThread: _onSelectThread
}: ChatProps) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const messagesEndRef = useRef<HTMLDivElement | null>(null);
const messageListRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const fetchChatHistory = async () => {
if (!threadId) return;

try {
const response = await fetch("/api/openai", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
userId,
threadId,
action: "get-history"
})
});

if (!response.ok) {
throw new Error("Failed to fetch chat history");
}

const data = await response.json();
const formattedMessages = data.map((msg: { role: string; content: string }) => ({
sender: msg.role,
message_content: msg.content
}));

setMessages(formattedMessages);
} catch (error) {
console.error("Error fetching chat history:", error);
}
};

void fetchChatHistory();
}, [userId, threadId]);

useEffect(() => {
if (messageListRef.current) {
messageListRef.current.scrollTop = messageListRef.current.scrollHeight;
}
}, [messages]);

const sendMessage = async () => {
if (!input.trim()) return;
if (!input.trim() || !threadId) return;

const userMessage = { sender: "user", content: input };
const userMessage = { sender: "user", message_content: input };
setMessages((prev) => [...prev, userMessage]);
setInput("");

Expand All @@ -28,6 +84,7 @@ export default function Chat({ userId }: ChatProps) {
},
body: JSON.stringify({
userId,
threadId,
message: input
})
});
Expand All @@ -37,19 +94,27 @@ export default function Chat({ userId }: ChatProps) {
}

const data = await response.json();
const assistantMessage = { sender: "assistant", content: data.assistantMessage };
//console.log("Assistant response:", data);

const assistantMessage = {
sender: "assistant",
message_content: data.assistantMessage
};
setMessages((prev) => [...prev, assistantMessage]);
} catch (error) {
console.error("Error sending message:", error);
}
};

return (
<div className="chat-container mx-auto my-auto max-w-[1200px] overflow-hidden rounded-lg border-2 border-black p-4 shadow-md sm:p-6">
<ChatMessageList className="max-h-[600px] min-h-[600px] overflow-y-auto">
<div className="chat-container mx-auto my-auto max-w-[1200px] overflow-hidden rounded-lg border-2 border-gray-300 bg-white p-4 shadow-md sm:p-6">
<ChatMessageList
ref={messageListRef}
className="max-h-[600px] min-h-[600px] overflow-y-auto"
>
{messages.map((msg, index) => (
<ChatBubble key={index} variant={msg.sender === "user" ? "sent" : "received"}>
<ChatBubbleMessage>{msg.content}</ChatBubbleMessage>
<ChatBubbleMessage>{msg.message_content}</ChatBubbleMessage>
</ChatBubble>
))}
<div ref={messagesEndRef} />
Expand All @@ -64,6 +129,7 @@ export default function Chat({ userId }: ChatProps) {
void sendMessage();
}
}}
onSend={sendMessage}
className="w-full"
/>
</div>
Expand Down
Loading

0 comments on commit 6d95c9d

Please sign in to comment.