Skip to content

Commit f7a73d6

Browse files
authored
Merge pull request #196 from CS3219-AY2425S1/feat/collab/ai-chat-enhancements
Enhance AI Chat
2 parents 28fbe4e + 7e398bf commit f7a73d6

File tree

7 files changed

+138
-63
lines changed

7 files changed

+138
-63
lines changed

collab-service/app/controller/ai-controller.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@ import { sendAiMessage } from "../model/repository.js";
22

33
// send ai message
44
export async function sendAiMessageController(req, res) {
5-
const { message } = req.body;
6-
if (!message) {
5+
const { messages } = req.body;
6+
if (!messages) {
77
return res.status(400).json({ error: "Message content is required" });
88
}
99

10-
const data = await sendAiMessage(message);
11-
const aiResponse =
12-
data.choices?.[0]?.message?.content || "No response from AI";
13-
14-
if (aiResponse) {
15-
res.status(200).json({ data });
10+
const returnMessage = await sendAiMessage(messages);
11+
if (returnMessage) {
12+
res.status(200).json({ data: returnMessage });
1613
} else {
1714
res.status(500).json({ error: "Failed to retrieve AI response" });
1815
}

collab-service/app/model/repository.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,21 +121,22 @@ export async function getQuestionIdByRoomId(roomId) {
121121
}
122122
}
123123

124-
export async function sendAiMessage(message) {
124+
export async function sendAiMessage(messages) {
125125
try {
126126
const response = await fetch("https://api.openai.com/v1/chat/completions", {
127127
method: "POST",
128128
headers: {
129-
'Content-Type': "application/json",
130-
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
129+
"Content-Type": "application/json",
130+
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
131131
},
132132
body: JSON.stringify({
133133
model: "gpt-3.5-turbo",
134-
messages: [{ role: "user", content: message }],
134+
messages: messages,
135135
}),
136136
});
137137
const data = await response.json();
138-
return data;
138+
const returnMessage = data.choices[0].message.content;
139+
return returnMessage;
139140
} catch (error) {
140141
console.error("Error in sending AI message:", error);
141142
}

frontend/components/collab/chat.tsx

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useToast } from "@/components/hooks/use-toast";
1212
import { useAuth } from "@/app/auth/auth-context";
1313
import LoadingScreen from "@/components/common/loading-screen";
1414
import { sendAiMessage } from "@/lib/api/openai/send-ai-message";
15+
import { Question } from "@/lib/schemas/question-schema";
1516
import { getChatHistory } from "@/lib/api/collab-service/get-chat-history";
1617
import { v4 as uuidv4 } from "uuid";
1718
import {
@@ -20,7 +21,7 @@ import {
2021
constructUriSuffix,
2122
} from "@/lib/api/api-uri";
2223

23-
interface Message {
24+
export interface Message {
2425
id: string;
2526
userId: string;
2627
text: string;
@@ -35,7 +36,15 @@ interface ChatHistoryMessage {
3536
timestamp: string;
3637
}
3738

38-
export default function Chat({ roomId }: { roomId: string }) {
39+
export default function Chat({
40+
roomId,
41+
question,
42+
code,
43+
}: {
44+
roomId: string;
45+
question: Question | null;
46+
code: string;
47+
}) {
3948
const auth = useAuth();
4049
const { toast } = useToast();
4150
const own_user_id = auth?.user?.id;
@@ -49,6 +58,32 @@ export default function Chat({ roomId }: { roomId: string }) {
4958
const lastMessageRef = useRef<HTMLDivElement | null>(null);
5059

5160
useEffect(() => {
61+
const greeting =
62+
"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.";
63+
const greetingMessage = {
64+
id: uuidv4(),
65+
userId: "assistant",
66+
text: greeting,
67+
timestamp: new Date(),
68+
};
69+
setAiMessages((prev) => [...prev, greetingMessage]);
70+
}, []);
71+
72+
useEffect(() => {
73+
if (question) {
74+
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.`;
75+
const systemMessage = {
76+
id: uuidv4(),
77+
userId: "system",
78+
text: context,
79+
timestamp: new Date(),
80+
};
81+
setAiMessages((prev) => [...prev, systemMessage]);
82+
}
83+
}, [question]);
84+
85+
useEffect(() => {
86+
if (!auth?.user?.id) return; // Avoid connecting if user is not authenticated
5287
const fetchChatHistory = async () => {
5388
try {
5489
if (!auth || !auth.token) {
@@ -176,17 +211,25 @@ export default function Chat({ roomId }: { roomId: string }) {
176211
timestamp: new Date(),
177212
};
178213
setAiMessages((prev) => [...prev, message]);
179-
const response = await sendAiMessage(auth?.token, newMessage);
214+
setNewMessage("");
215+
const attachedCode = {
216+
id: uuidv4(),
217+
userId: "system",
218+
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.`,
219+
timestamp: new Date(),
220+
};
221+
const response = await sendAiMessage(
222+
auth?.token,
223+
aiMessages.concat(attachedCode).concat(message)
224+
);
180225
const data = await response.json();
181226
const aiMessage = {
182227
id: uuidv4(),
183-
userId: "ai",
184-
text:
185-
data.data.choices && data.data.choices[0]?.message?.content
186-
? data.data.choices[0].message.content
187-
: "An error occurred. Please try again.",
228+
userId: "assistant",
229+
text: data.data ? data.data : "An error occurred. Please try again.",
188230
timestamp: new Date(),
189231
};
232+
setAiMessages((prev) => [...prev, attachedCode]);
190233
setAiMessages((prev) => [...prev, aiMessage]);
191234
}
192235

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

206-
const renderMessage = (message: Message, isOwnMessage: boolean) => (
207-
<div
208-
key={message.id}
209-
className={`p-2 rounded-lg mb-2 max-w-[80%] ${
210-
isOwnMessage
211-
? "ml-auto bg-blue-500 text-white"
212-
: "bg-gray-100 dark:bg-gray-800"
213-
}`}
214-
>
215-
<div className="text-sm">{message.text}</div>
216-
<div
217-
className={`text-xs ${isOwnMessage ? "text-blue-100" : "text-gray-500"}`}
218-
>
219-
{formatTimestamp(message.timestamp)}
220-
</div>
221-
</div>
222-
);
249+
const renderMessage = (
250+
message: Message,
251+
isOwnMessage: boolean,
252+
isSystem: boolean
253+
) => {
254+
if (isSystem) {
255+
return null;
256+
} else {
257+
return (
258+
<div
259+
key={message.id}
260+
className={`p-2 rounded-lg mb-2 max-w-[80%] ${
261+
isOwnMessage
262+
? "ml-auto bg-blue-500 text-white"
263+
: "bg-gray-100 dark:bg-gray-800"
264+
}`}
265+
>
266+
<div className="text-sm">{message.text}</div>
267+
<div
268+
className={`text-xs ${isOwnMessage ? "text-blue-100" : "text-gray-500"}`}
269+
>
270+
{formatTimestamp(message.timestamp)}
271+
</div>
272+
</div>
273+
);
274+
}
275+
};
223276

224277
if (!own_user_id || isLoading) {
225278
return <LoadingScreen />;
@@ -249,7 +302,7 @@ export default function Chat({ roomId }: { roomId: string }) {
249302
<ScrollArea className="h-[calc(70vh-280px)]">
250303
<div className="pr-4 space-y-2">
251304
{partnerMessages.map((msg) =>
252-
renderMessage(msg, msg.userId === own_user_id)
305+
renderMessage(msg, msg.userId === own_user_id, false)
253306
)}
254307
<div ref={lastMessageRef} />
255308
</div>
@@ -259,7 +312,11 @@ export default function Chat({ roomId }: { roomId: string }) {
259312
<ScrollArea className="h-[calc(70vh-280px)]">
260313
<div className="pr-4 space-y-2">
261314
{aiMessages.map((msg) =>
262-
renderMessage(msg, msg.userId === own_user_id)
315+
renderMessage(
316+
msg,
317+
msg.userId === own_user_id,
318+
msg.userId === "system"
319+
)
263320
)}
264321
<div ref={lastMessageRef} />
265322
</div>

frontend/components/collab/code-editor.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ const languages: Record<string, LanguageEntry> = {
4141
},
4242
};
4343

44-
export default function CodeEditor({ roomId }: { roomId: string }) {
44+
export default function CodeEditor({
45+
roomId,
46+
setCode,
47+
}: {
48+
roomId: string;
49+
setCode: (value: string) => void;
50+
}) {
4551
const monaco = useMonaco();
4652
const [language, setLanguage] = useState<string>("Javascript");
4753
const [theme, setTheme] = useState<string>("light");
@@ -140,6 +146,9 @@ export default function CodeEditor({ roomId }: { roomId: string }) {
140146
onMount={(editor) => {
141147
setEditor(editor);
142148
}}
149+
onChange={(value) => {
150+
setCode(value || "");
151+
}}
143152
theme={theme === "dark" ? "vs-dark" : "light"}
144153
/>
145154
</div>

frontend/components/collab/collab-room.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import React from "react";
1+
"use client";
2+
3+
import React, { useState } from "react";
24
import { Button } from "@/components/ui/button";
35
import { X } from "lucide-react";
46
import Chat from "./chat";
57
import QuestionDisplay from "./question-display";
68
import CodeEditor from "./code-editor";
79
import Link from "next/link";
10+
import { Question } from "@/lib/schemas/question-schema";
811

912
export default function CollabRoom({ roomId }: { roomId: string }) {
13+
const [code, setCode] = useState<string>("");
14+
const [exposedQuestion, setExposedQuestion] = useState<Question | null>(null);
1015
return (
1116
<div className="h-screen flex flex-col mx-4 p-4 overflow-hidden">
1217
<header className="flex justify-between border-b">
@@ -20,12 +25,13 @@ export default function CollabRoom({ roomId }: { roomId: string }) {
2025
</header>
2126
<div className="flex flex-1 overflow-hidden">
2227
<div className="w-2/5 p-4 flex flex-col space-y-4 overflow-hidden">
23-
<QuestionDisplay roomId={roomId} />
24-
<div className="flex-1 overflow-hidden">
25-
<Chat roomId={roomId} />
26-
</div>
28+
<QuestionDisplay
29+
roomId={roomId}
30+
setExposedQuestion={setExposedQuestion}
31+
/>
32+
<Chat roomId={roomId} question={exposedQuestion} code={code} />
2733
</div>
28-
<CodeEditor roomId={roomId} />
34+
<CodeEditor roomId={roomId} setCode={setCode} />
2935
</div>
3036
</div>
3137
);

frontend/components/collab/question-display.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import React, { useEffect, useState } from "react";
2+
import React, { useState, useEffect } from "react";
33
import clsx from "clsx";
44
import {
55
Card,
@@ -8,38 +8,35 @@ import {
88
CardHeader,
99
CardTitle,
1010
} from "@/components/ui/card";
11-
import { Badge } from "@/components/ui/badge";
12-
import LoadingScreen from "@/components/common/loading-screen";
1311
import { getQuestion } from "@/lib/api/question-service/get-question";
14-
import { useToast } from "@/components/hooks/use-toast";
1512
import { useAuth } from "@/app/auth/auth-context";
1613
import { getQuestionId } from "@/lib/api/collab-service/get-questionId";
14+
import { useToast } from "@/components/hooks/use-toast";
15+
import { Badge } from "@/components/ui/badge";
16+
import { Question } from "@/lib/schemas/question-schema";
17+
import LoadingScreen from "@/components/common/loading-screen";
1718

1819
const difficultyColors = {
1920
Easy: "bg-green-500",
2021
Medium: "bg-yellow-500",
2122
Hard: "bg-red-500",
2223
};
2324

24-
interface Question {
25-
title: string;
26-
categories: string;
27-
complexity: keyof typeof difficultyColors;
28-
description: string;
29-
}
30-
3125
export default function QuestionDisplay({
32-
roomId,
3326
className,
3427
date,
28+
roomId,
29+
setExposedQuestion,
3530
}: {
36-
roomId: string;
3731
className?: string;
3832
date?: Date;
33+
roomId: string;
34+
setExposedQuestion?: (question: Question) => void;
3935
}) {
4036
const auth = useAuth();
41-
const { toast } = useToast();
4237
const token = auth?.token;
38+
const { toast } = useToast();
39+
4340
const [question, setQuestion] = useState<Question | null>(null);
4441
const [loading, setLoading] = useState(true);
4542

@@ -65,6 +62,9 @@ export default function QuestionDisplay({
6562
const questionResponse = await getQuestion(token, data.questionId);
6663
const questionData = await questionResponse.json();
6764
setQuestion(questionData);
65+
if (setExposedQuestion) {
66+
setExposedQuestion(questionData);
67+
}
6868
} else {
6969
console.error("Token is not available");
7070
}

frontend/lib/api/openai/send-ai-message.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import { Message } from "@/components/collab/chat";
12
import { AuthType, collabServiceUri } from "@/lib/api/api-uri";
23

3-
export const sendAiMessage = async (jwtToken: string, message: string) => {
4+
export const sendAiMessage = async (jwtToken: string, messages: Message[]) => {
5+
const apiMessages = messages.map((msg) => ({
6+
role: `${msg.userId === "assistant" || msg.userId === "system" ? msg.userId : "user"}`,
7+
content: msg.text,
8+
}));
49
const response = await fetch(
510
`${collabServiceUri(window.location.hostname, AuthType.Private)}/collab/send-ai-message`,
611
{
@@ -9,7 +14,7 @@ export const sendAiMessage = async (jwtToken: string, message: string) => {
914
Authorization: `Bearer ${jwtToken}`,
1015
"Content-Type": "application/json",
1116
},
12-
body: JSON.stringify({ message: message }),
17+
body: JSON.stringify({ messages: apiMessages }),
1318
}
1419
);
1520
return response;

0 commit comments

Comments
 (0)