Skip to content

Commit

Permalink
Add communication with OpenAI GPT-3.5 Turbo and change naming to refl…
Browse files Browse the repository at this point in the history
…ect communication with user or AI
  • Loading branch information
kflim committed Oct 31, 2023
1 parent 0e18bab commit 92cca86
Show file tree
Hide file tree
Showing 12 changed files with 502 additions and 44 deletions.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ S3_BUCKET_NAME=s3

# Github OAuth Provider
GITHUB_ID=EXAMPLE_GITHUB_ID
GITHUB_SECRET=EXAMPLE_GITHUB_SECRET
GITHUB_SECRET=EXAMPLE_GITHUB_SECRET

# OpenAI
OPENAI_API_KEY=EXAMPLE_OPENAI_API_KEY
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"next": "^13.4.13",
"next-auth": "^4.22.4",
"npm-run-all": "^4.1.5",
"openai": "^4.14.1",
"react": "18.2.0",
"react-codemirror-merge": "^4.21.15",
"react-dom": "18.2.0",
Expand Down
45 changes: 29 additions & 16 deletions prisma/postgres/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,22 @@ model CodeSessionUserAuth {
}

model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
password String?
accounts Account[]
sessions Session[]
codeSpace CodeSpace[]
attempts QuestionAttempt[]
role Role @default(USER)
codeSessionAuthRecv CodeSessionUserAuth[] @relation(name: "auth_receiver")
matchRequest MatchRequest?
joinRequest JoinRequest[]
sessionMessage SessionMessage[]
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
password String?
accounts Account[]
sessions Session[]
codeSpace CodeSpace[]
attempts QuestionAttempt[]
role Role @default(USER)
codeSessionAuthRecv CodeSessionUserAuth[] @relation(name: "auth_receiver")
matchRequest MatchRequest?
joinRequest JoinRequest[]
sessionUserAndUserMessages SessionUserAndUserMessage[]
sessionUserAndAIMessages SessionUserAndAIMessage[]
}

enum Difficulty {
Expand Down Expand Up @@ -177,7 +178,7 @@ model QuestionAttempt {
}

model SessionMessage {
model SessionUserAndUserMessage {
id String @id @default(cuid())
sessionId String
userId String
Expand All @@ -188,3 +189,15 @@ model SessionMessage {
@@index([sessionId])
}

model SessionUserAndAIMessage {
id String @id @default(cuid())
sessionId String
userId String
message String
role String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([sessionId])
}
67 changes: 67 additions & 0 deletions src/components/AIBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import useAIComm from "~/hooks/useAIComm";

const AIBox = ({
sessionId,
userId,
userName,
className,
}: {
sessionId: string;
userId: string;
userName: string;
className?: string | undefined;
}) => {
const [
allSessionMessages,
sendMessage,
onTyping,
currentMessage,
isAIResponding,
] = useAIComm(sessionId, userId);

return (
<div className={className}>
<div className="messages-container overflow-y-auto flex flex-col h-5/6">
{allSessionMessages?.map((message) => {
return (
<div
key={message.id}
className={`w-3/4 rounded-md ${
message.role === "user"
? "dark:bg-gray-900 ml-auto"
: "dark:bg-gray-500"
} text-white p-2 my-2`}
>
<div className="flex justify-between">
<span>{message.role === "user" ? userName : "GPT-3.5"}</span>
</div>
<p>{message.message}</p>
</div>
);
})}
</div>
<div className="flex flex-col mt-2">
<div className="flex">
{!isAIResponding && (
<input
className="w-full rounded-md p-2 focus:ring-primary-600 focus:border-primary-600"
type="text"
value={currentMessage}
onChange={(e) => onTyping(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
/>
)}
{isAIResponding && (
<input
className="w-full rounded-md p-2"
value="GPT-3.5 is responding..."
disabled
/>
)}
</div>
</div>
</div>
);
};

export default AIBox;
2 changes: 2 additions & 0 deletions src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const env = createEnv({
// Add `.min(1) on ID and SECRET if you want to make sure they're not empty
GITHUB_ID: z.string().min(1),
GITHUB_SECRET: z.string().min(1),
OPENAI_API_KEY: z.string().min(1),
},

/**
Expand Down Expand Up @@ -59,6 +60,7 @@ export const env = createEnv({
S3_BUCKET_NAME: process.env.S3_BUCKET_NAME,
GITHUB_ID: process.env.GITHUB_ID,
GITHUB_SECRET: process.env.GITHUB_SECRET,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
Expand Down
102 changes: 102 additions & 0 deletions src/hooks/useAIComm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { useState } from "react";
import type { UserAndAIMessage } from "~/server/api/routers/userAndAIComm";
import { api } from "~/utils/api";

export type AICommResult = [
UserAndAIMessage[],
() => void,
(value: string) => void,
currentMessage: string,
isAIResponding: boolean,
];

export default function useAIComm(
sessionId: string,
userId: string,
): AICommResult {
const [chatState, setChatState] = useState({
currentMessage: "",
isAIResponding: false,
});

const utils = api.useContext();

const allSessionMessages =
api.userAndAIMessages.getAllSessionUserAndAIMessages.useQuery({
sessionId,
userId,
}).data;

const addMessageMutation =
api.userAndAIMessages.addUserAndAIMessage.useMutation();

api.userAndAIMessages.subscribeToSessionUserAndAIMessages.useSubscription(
{ sessionId, userId },
{
onData: (data: UserAndAIMessage) => {
if (allSessionMessages)
allSessionMessages.push({
...data,
id: data.id!,
message: data.message,
createdAt: data.createdAt!,
});

setChatState((state) => ({
...state,
isAIResponding: false,
}));
},
onError(err) {
console.log("Subscription error: ", err);
void Promise.resolve(utils.userAndUserMessages.invalidate());
},
},
);

const sendMessage = () => {
// Don't send empty messages, or messages with only whitespace. Could change this later
if (chatState.currentMessage.trim().length === 0) return;

allSessionMessages?.push({
id: "",
sessionId,
userId,
message: chatState.currentMessage,
role: "user",
createdAt: new Date(),
});

addMessageMutation.mutate({
sessionId,
userId,
message: chatState.currentMessage,
});

setChatState((state) => ({
...state,
currentMessage: "",
isAIResponding: true,
}));
};

const onTyping = (value: string) => {
setChatState((prev) => {
return {
...prev,
currentMessage: value,
};
});
};

return [
allSessionMessages as UserAndAIMessage[],
sendMessage,
onTyping,
chatState.currentMessage,
chatState.isAIResponding,
];
}
28 changes: 17 additions & 11 deletions src/hooks/useSessionComm.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { useState } from "react";
import { type Message } from "~/server/api/routers/communication";
import { type UserAndUserMessage } from "~/server/api/routers/userAndUserComm";
import { api } from "~/utils/api";

export type SessionCommResult = [
Message[],
UserAndUserMessage[],
() => void,
(value: string) => void,
currentMessage: string,
Expand All @@ -23,16 +26,18 @@ export default function useSessionComm(

const utils = api.useContext();

const allSessionMessages = api.messages.getAllSessionMessages.useQuery({
sessionId,
}).data;
const allSessionMessages =
api.userAndUserMessages.getAllSessionUserAndUserMessages.useQuery({
sessionId,
}).data;

const addMessageMutation = api.messages.addMessage.useMutation();
const addMessageMutation =
api.userAndUserMessages.addUserAndUserMessage.useMutation();

api.messages.subscribeToSessionMessages.useSubscription(
api.userAndUserMessages.subscribeToSessionUserAndUserMessages.useSubscription(
{ sessionId, userId },
{
onData: (data: Message) => {
onData: (data: UserAndUserMessage) => {
if (data.message) {
if (data.userId !== userId)
setChatState((state) => ({
Expand All @@ -58,12 +63,13 @@ export default function useSessionComm(
},
onError(err) {
console.log("Subscription error: ", err);
void Promise.resolve(utils.messages.invalidate());
void Promise.resolve(utils.userAndUserMessages.invalidate());
},
},
);

const addWhoIsTypingMutation = api.messages.addWhoIsTyping.useMutation();
const addWhoIsTypingMutation =
api.userAndUserMessages.addWhoIsTyping.useMutation();

const sendMessage = () => {
// Don't send empty messages, or messages with only whitespace. Could change this later
Expand Down Expand Up @@ -101,7 +107,7 @@ export default function useSessionComm(
: "";

return [
allSessionMessages as Message[],
allSessionMessages as UserAndUserMessage[],
sendMessage,
onTyping,
chatState.currentMessage,
Expand Down
10 changes: 10 additions & 0 deletions src/pages/collab/rooms/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { getLanguage } from "~/utils/utils";
import { Tabs, TabList, Tab, TabPanel } from "react-tabs";
import { useSession } from "next-auth/react";
import Chatbox from "~/components/ChatBox";
import AIBox from "~/components/AIBox";

const SharedEditor = ({
onSave,
Expand Down Expand Up @@ -267,6 +268,7 @@ const Room = () => {
<TabList>
<Tab>Output</Tab>
<Tab>Chat</Tab>
<Tab>GPT-3.5</Tab>
</TabList>
<TabPanel>
<Output
Expand All @@ -282,6 +284,14 @@ const Room = () => {
className="row-span-2 w-full h-full p-3 flex flex-col text-black"
/>
</TabPanel>
<TabPanel>
<AIBox
sessionId={roomId as string}
userId={session?.user.id ?? ""}
userName={session?.user.name ?? ""}
className="row-span-2 w-full h-full p-3 flex flex-col text-black"
/>
</TabPanel>
</Tabs>
</div>
<div className="room-editor-wrapper bg-slate-600">
Expand Down
6 changes: 4 additions & 2 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { formRouter } from "~/server/api/routers/form";
import answerRouter from "./routers/answer";
import { codeSessionRouter } from "./routers/codeSession";
import { judgeRouter } from "~/server/api/routers/judge";
import { messagesRouter } from "./routers/communication";
import { userAndUserMessagesRouter } from "./routers/userAndUserComm";
import { sharedCodeSessionRouter } from "./routers/sharedCodeSession";
import { userAndAIMessagesRouter } from "./routers/userAndAIComm";

/**
* This is the primary router for your server.
Expand All @@ -22,7 +23,8 @@ export const appRouter = createTRPCRouter({
answer: answerRouter,
codeSession: codeSessionRouter,
judge: judgeRouter,
messages: messagesRouter,
userAndUserMessages: userAndUserMessagesRouter,
userAndAIMessages: userAndAIMessagesRouter,
sharedSession: sharedCodeSessionRouter,
});

Expand Down
Loading

0 comments on commit 92cca86

Please sign in to comment.