Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance AI Chat #196

Merged
merged 13 commits into from
Nov 10, 2024
13 changes: 5 additions & 8 deletions collab-service/app/controller/ai-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ import { sendAiMessage } from "../model/repository.js";

// send ai message
export async function sendAiMessageController(req, res) {
const { message } = req.body;
if (!message) {
const { messages } = req.body;
if (!messages) {
return res.status(400).json({ error: "Message content is required" });
}

const data = await sendAiMessage(message);
const aiResponse =
data.choices?.[0]?.message?.content || "No response from AI";

if (aiResponse) {
res.status(200).json({ data });
const returnMessage = await sendAiMessage(messages);
if (returnMessage) {
res.status(200).json({ data: returnMessage });
} else {
res.status(500).json({ error: "Failed to retrieve AI response" });
}
Expand Down
11 changes: 6 additions & 5 deletions collab-service/app/model/repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,21 +121,22 @@ export async function getQuestionIdByRoomId(roomId) {
}
}

export async function sendAiMessage(message) {
export async function sendAiMessage(messages) {
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
'Content-Type': "application/json",
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages: [{ role: "user", content: message }],
messages: messages,
}),
});
const data = await response.json();
return data;
const returnMessage = data.choices[0].message.content;
return returnMessage;
} catch (error) {
console.error("Error in sending AI message:", error);
}
Expand Down
111 changes: 84 additions & 27 deletions frontend/components/collab/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useToast } from "@/components/hooks/use-toast";
import { useAuth } from "@/app/auth/auth-context";
import LoadingScreen from "@/components/common/loading-screen";
import { sendAiMessage } from "@/lib/api/openai/send-ai-message";
import { Question } from "@/lib/schemas/question-schema";
import { getChatHistory } from "@/lib/api/collab-service/get-chat-history";
import { v4 as uuidv4 } from "uuid";
import {
Expand All @@ -20,7 +21,7 @@ import {
constructUriSuffix,
} from "@/lib/api/api-uri";

interface Message {
export interface Message {
id: string;
userId: string;
text: string;
Expand All @@ -35,7 +36,15 @@ interface ChatHistoryMessage {
timestamp: string;
}

export default function Chat({ roomId }: { roomId: string }) {
export default function Chat({
roomId,
question,
code,
}: {
roomId: string;
question: Question | null;
code: string;
}) {
const auth = useAuth();
const { toast } = useToast();
const own_user_id = auth?.user?.id;
Expand All @@ -49,6 +58,32 @@ export default function Chat({ roomId }: { roomId: string }) {
const lastMessageRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const greeting =
"Hello! I am your AI assistant! You can ask me for help with the question or any other programming related queries while you are coding.";
const greetingMessage = {
id: uuidv4(),
userId: "assistant",
text: greeting,
timestamp: new Date(),
};
setAiMessages((prev) => [...prev, greetingMessage]);
}, []);

useEffect(() => {
if (question) {
const context = `${question.title}: ${question.description}. Your job is to assist a student who is solving this problem. Provide hints and guide them through the problem solving process if they ask for it. Do not answer irrelevant questions, try to keep the student focussed on the task.`;
const systemMessage = {
id: uuidv4(),
userId: "system",
text: context,
timestamp: new Date(),
};
setAiMessages((prev) => [...prev, systemMessage]);
}
}, [question]);

useEffect(() => {
if (!auth?.user?.id) return; // Avoid connecting if user is not authenticated
const fetchChatHistory = async () => {
try {
if (!auth || !auth.token) {
Expand Down Expand Up @@ -176,17 +211,25 @@ export default function Chat({ roomId }: { roomId: string }) {
timestamp: new Date(),
};
setAiMessages((prev) => [...prev, message]);
const response = await sendAiMessage(auth?.token, newMessage);
setNewMessage("");
const attachedCode = {
id: uuidv4(),
userId: "system",
text: `This is the student's current code now: ${code}. Take note of any changes and be prepared to explain, correct or fix any issues in the code if the student asks.`,
timestamp: new Date(),
};
const response = await sendAiMessage(
auth?.token,
aiMessages.concat(attachedCode).concat(message)
);
const data = await response.json();
const aiMessage = {
id: uuidv4(),
userId: "ai",
text:
data.data.choices && data.data.choices[0]?.message?.content
? data.data.choices[0].message.content
: "An error occurred. Please try again.",
userId: "assistant",
text: data.data ? data.data : "An error occurred. Please try again.",
timestamp: new Date(),
};
setAiMessages((prev) => [...prev, attachedCode]);
setAiMessages((prev) => [...prev, aiMessage]);
}

Expand All @@ -203,23 +246,33 @@ export default function Chat({ roomId }: { roomId: string }) {
});
};

const renderMessage = (message: Message, isOwnMessage: boolean) => (
<div
key={message.id}
className={`p-2 rounded-lg mb-2 max-w-[80%] ${
isOwnMessage
? "ml-auto bg-blue-500 text-white"
: "bg-gray-100 dark:bg-gray-800"
}`}
>
<div className="text-sm">{message.text}</div>
<div
className={`text-xs ${isOwnMessage ? "text-blue-100" : "text-gray-500"}`}
>
{formatTimestamp(message.timestamp)}
</div>
</div>
);
const renderMessage = (
message: Message,
isOwnMessage: boolean,
isSystem: boolean
) => {
if (isSystem) {
return null;
} else {
return (
<div
key={message.id}
className={`p-2 rounded-lg mb-2 max-w-[80%] ${
isOwnMessage
? "ml-auto bg-blue-500 text-white"
: "bg-gray-100 dark:bg-gray-800"
}`}
>
<div className="text-sm">{message.text}</div>
<div
className={`text-xs ${isOwnMessage ? "text-blue-100" : "text-gray-500"}`}
>
{formatTimestamp(message.timestamp)}
</div>
</div>
);
}
};

if (!own_user_id || isLoading) {
return <LoadingScreen />;
Expand Down Expand Up @@ -249,7 +302,7 @@ export default function Chat({ roomId }: { roomId: string }) {
<ScrollArea className="h-[calc(70vh-280px)]">
<div className="pr-4 space-y-2">
{partnerMessages.map((msg) =>
renderMessage(msg, msg.userId === own_user_id)
renderMessage(msg, msg.userId === own_user_id, false)
)}
<div ref={lastMessageRef} />
</div>
Expand All @@ -259,7 +312,11 @@ export default function Chat({ roomId }: { roomId: string }) {
<ScrollArea className="h-[calc(70vh-280px)]">
<div className="pr-4 space-y-2">
{aiMessages.map((msg) =>
renderMessage(msg, msg.userId === own_user_id)
renderMessage(
msg,
msg.userId === own_user_id,
msg.userId === "system"
)
)}
<div ref={lastMessageRef} />
</div>
Expand Down
11 changes: 10 additions & 1 deletion frontend/components/collab/code-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ const languages: Record<string, LanguageEntry> = {
},
};

export default function CodeEditor({ roomId }: { roomId: string }) {
export default function CodeEditor({
roomId,
setCode,
}: {
roomId: string;
setCode: (value: string) => void;
}) {
const monaco = useMonaco();
const [language, setLanguage] = useState<string>("Javascript");
const [theme, setTheme] = useState<string>("light");
Expand Down Expand Up @@ -140,6 +146,9 @@ export default function CodeEditor({ roomId }: { roomId: string }) {
onMount={(editor) => {
setEditor(editor);
}}
onChange={(value) => {
setCode(value || "");
}}
theme={theme === "dark" ? "vs-dark" : "light"}
/>
</div>
Expand Down
18 changes: 12 additions & 6 deletions frontend/components/collab/collab-room.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import React from "react";
"use client";

import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import Chat from "./chat";
import QuestionDisplay from "./question-display";
import CodeEditor from "./code-editor";
import Link from "next/link";
import { Question } from "@/lib/schemas/question-schema";

export default function CollabRoom({ roomId }: { roomId: string }) {
const [code, setCode] = useState<string>("");
const [exposedQuestion, setExposedQuestion] = useState<Question | null>(null);
return (
<div className="h-screen flex flex-col mx-4 p-4 overflow-hidden">
<header className="flex justify-between border-b">
Expand All @@ -20,12 +25,13 @@ export default function CollabRoom({ roomId }: { roomId: string }) {
</header>
<div className="flex flex-1 overflow-hidden">
<div className="w-2/5 p-4 flex flex-col space-y-4 overflow-hidden">
<QuestionDisplay roomId={roomId} />
<div className="flex-1 overflow-hidden">
<Chat roomId={roomId} />
</div>
<QuestionDisplay
roomId={roomId}
setExposedQuestion={setExposedQuestion}
/>
<Chat roomId={roomId} question={exposedQuestion} code={code} />
</div>
<CodeEditor roomId={roomId} />
<CodeEditor roomId={roomId} setCode={setCode} />
</div>
</div>
);
Expand Down
28 changes: 14 additions & 14 deletions frontend/components/collab/question-display.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import React, { useEffect, useState } from "react";
import React, { useState, useEffect } from "react";
import clsx from "clsx";
import {
Card,
Expand All @@ -8,38 +8,35 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import LoadingScreen from "@/components/common/loading-screen";
import { getQuestion } from "@/lib/api/question-service/get-question";
import { useToast } from "@/components/hooks/use-toast";
import { useAuth } from "@/app/auth/auth-context";
import { getQuestionId } from "@/lib/api/collab-service/get-questionId";
import { useToast } from "@/components/hooks/use-toast";
import { Badge } from "@/components/ui/badge";
import { Question } from "@/lib/schemas/question-schema";
import LoadingScreen from "@/components/common/loading-screen";

const difficultyColors = {
Easy: "bg-green-500",
Medium: "bg-yellow-500",
Hard: "bg-red-500",
};

interface Question {
title: string;
categories: string;
complexity: keyof typeof difficultyColors;
description: string;
}

export default function QuestionDisplay({
roomId,
className,
date,
roomId,
setExposedQuestion,
}: {
roomId: string;
className?: string;
date?: Date;
roomId: string;
setExposedQuestion?: (question: Question) => void;
}) {
const auth = useAuth();
const { toast } = useToast();
const token = auth?.token;
const { toast } = useToast();

const [question, setQuestion] = useState<Question | null>(null);
const [loading, setLoading] = useState(true);

Expand All @@ -65,6 +62,9 @@ export default function QuestionDisplay({
const questionResponse = await getQuestion(token, data.questionId);
const questionData = await questionResponse.json();
setQuestion(questionData);
if (setExposedQuestion) {
setExposedQuestion(questionData);
}
} else {
console.error("Token is not available");
}
Expand Down
9 changes: 7 additions & 2 deletions frontend/lib/api/openai/send-ai-message.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Message } from "@/components/collab/chat";
import { AuthType, collabServiceUri } from "@/lib/api/api-uri";

export const sendAiMessage = async (jwtToken: string, message: string) => {
export const sendAiMessage = async (jwtToken: string, messages: Message[]) => {
const apiMessages = messages.map((msg) => ({
role: `${msg.userId === "assistant" || msg.userId === "system" ? msg.userId : "user"}`,
content: msg.text,
}));
const response = await fetch(
`${collabServiceUri(window.location.hostname, AuthType.Private)}/collab/send-ai-message`,
{
Expand All @@ -9,7 +14,7 @@ export const sendAiMessage = async (jwtToken: string, message: string) => {
Authorization: `Bearer ${jwtToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ message: message }),
body: JSON.stringify({ messages: apiMessages }),
}
);
return response;
Expand Down