Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
AmjedNazzal committed May 17, 2024
1 parent c91747b commit 441fa74
Show file tree
Hide file tree
Showing 25 changed files with 1,616 additions and 283 deletions.
965 changes: 799 additions & 166 deletions package-lock.json

Large diffs are not rendered by default.

22 changes: 18 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,32 @@
"lint": "next lint"
},
"dependencies": {
"@tailwindcss/forms": "^0.5.7",
"@tanstack/react-query": "^5.36.0",
"@upstash/ratelimit": "^1.1.3",
"@upstash/redis": "^1.31.1",
"clsx": "^2.1.1",
"eventsource-parser": "^1.1.2",
"googleapis": "^137.1.0",
"lucide-react": "^0.378.0",
"mongoose": "^8.3.4",
"nanoid": "^5.0.7",
"next": "14.2.3",
"react": "^18",
"react-dom": "^18",
"next": "14.2.3"
"react-hot-toast": "^2.4.1",
"react-textarea-autosize": "^8.5.3",
"tailwind-merge": "^2.3.0",
"zod": "^3.23.8"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "14.2.3"
"typescript": "^5"
}
}
1 change: 0 additions & 1 deletion public/next.svg

This file was deleted.

Binary file added public/steambot-icon.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/steambot-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion public/vercel.svg

This file was deleted.

38 changes: 38 additions & 0 deletions src/app/(models)/DB.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import mongoose, { Schema } from "mongoose";

mongoose.connect(process.env.MONGODB_URI);
mongoose.Promise = global.Promise;

const gamesSchema = new Schema({
AppID: String,
Name: String,
Releasedate: String,
Price: String,
Aboutthegame: { type: String, text: true },
Windows: String,
Mac: String,
Linux: String,
Userscore: String,
Positive: String,
Negative: String,
Developers: String,
Publishers: String,
Categories: String,
Genres: String,
Tags: String,
});

const gamesDescriptionsSchema = new Schema({
Name: String,
Description: { type: String, text: true },
Tags: String,
});

const allgames =
mongoose.models.allgames || mongoose.model("allgames", gamesSchema);

const gamesDescriptions =
mongoose.models.descriptions ||
mongoose.model("descriptions", gamesDescriptionsSchema);

export { allgames, gamesDescriptions };
127 changes: 127 additions & 0 deletions src/app/api/message/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { allgames, gamesDescriptions } from "@/app/(models)/DB";
import { initialPrompt, afterQueryPrompt } from "@/app/utils/prompts";
import { gameWithNameQuery, noNameQuery } from "@/app/utils/queries";
import { Message, MessageArraySchema } from "@/app/lib/validators/message";
import {
ChatGPTMessage,
OpenAIStream,
OpenAIStreamPayload,
} from "@/app/lib/openai-stream";

async function getQuery(messages: ChatGPTMessage[]) {
try {
messages.unshift({
role: "system",
content: initialPrompt,
});

const res = await fetch(`${process.env.OPENAI_URL}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages: messages,
temperature: 0.1,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
n: 1,
}),
});

if (res.ok) {
const data = await res.json();
const responseBody = data.choices[0].message.content;
const parsedBody = JSON.parse(responseBody);
if (!parsedBody) {
return "No games found";
}
let query = {};
let aggregationPipeline: any[] = [];
if (parsedBody && parsedBody[0].name) {
query = { Name: parsedBody[0].name };
const gameWithName = await gamesDescriptions.findOne({
Name: { $regex: new RegExp("^" + parsedBody[0].name + "$", "i") },
});
if (gameWithName && gameWithName.Tags) {
const gameWithNameGenres = gameWithName.Tags.split(",");
const gameWithNameAggregation = gameWithNameQuery(gameWithNameGenres);

aggregationPipeline = gameWithNameAggregation;
} else return "No games found";
} else {
const noNameQueryArr = noNameQuery(parsedBody[0]);
aggregationPipeline = noNameQueryArr;
}

const games = await gamesDescriptions.aggregate(aggregationPipeline);

if (games) {
return games;
} else return "No games found";
}
} catch (error) {
return null;
}
}

export async function POST(req: Request) {
const { messages } = await req.json();

const parsedMessages = MessageArraySchema.parse(messages);

const filteredMessages = parsedMessages.filter((message) => {
if (message.text === "..." && !message.isUserMessage) {
return false;
}

return true;
});

const outboundMessages: ChatGPTMessage[] = filteredMessages.map((message) => {
return {
role: message.isUserMessage ? "user" : "system",
content: message.text,
};
});

const initialMessageList = [...outboundMessages];

initialMessageList.reverse();

const initialRes = await getQuery([initialMessageList[0]]);

if (!initialRes) {
throw new Error("Something went wrong, please try again later");
}

for (let i = outboundMessages.length - 1; i >= 0; i--) {
if (outboundMessages[i].role === "system") {
outboundMessages.splice(i, 1);
}
}

const chatbotPrompt = afterQueryPrompt(initialRes);
outboundMessages.unshift({
role: "system",
content: chatbotPrompt,
});

const payload: OpenAIStreamPayload = {
model: "gpt-3.5-turbo",
messages: outboundMessages,
temperature: 0.4,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
stream: true,
n: 1,
};

const stream = await OpenAIStream(payload);

return new Response(stream);
}
17 changes: 17 additions & 0 deletions src/app/components/chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { FC } from "react";
import ChatInput from "./chatInput";
import ChatMessages from "./chatMessages";

const Chat: FC = () => {
return (
<div
style={{ backgroundColor: "rgba(0, 0, 0, 0.2)" }}
className="flex flex-grow flex-col w-[70%] h-full p-5 rounded-lg"
>
<ChatMessages className="px-2 py-3 flex-1" />
<ChatInput className="px-4" />
</div>
);
};

export default Chat;
129 changes: 129 additions & 0 deletions src/app/components/chatInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"use client";
import React, { FC, HTMLAttributes, useContext, useRef, useState } from "react";
import { cn } from "../utils/utils";
import TextareaAutosize from "react-textarea-autosize";
import { useMutation } from "@tanstack/react-query";
import { nanoid } from "nanoid";
import { Message } from "../lib/validators/message";
import { MessagesContext } from "../context/messages";
import { Loader2, CornerDownLeft } from "lucide-react";
import { toast } from "react-hot-toast";

interface ChatInputProps extends HTMLAttributes<HTMLDivElement> {}

const ChatInput: FC<ChatInputProps> = ({ className, ...props }) => {
const [input, setInput] = useState<string>("");
const [inputFallback, setInputFallback] = useState<string>("");
const {
messages,
addMessage,
removeMessage,
updateMessage,
isMessageUpdating,
setIsMessageUpdating,
} = useContext(MessagesContext);
const textareaAutoRef = useRef<null | HTMLTextAreaElement>(null);

const { mutate: sendMessage, isPending } = useMutation({
mutationFn: async (message: Message) => {
const response = await fetch("/api/message", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ messages }),
});

if (!response.ok) {
throw new Error();
}

return response.body;
},
onMutate(message) {
addMessage(message);
setInputFallback(input);
setInput("");
},
onSuccess: async (stream) => {
if (!stream) throw new Error("Something went wrong");
const id = nanoid();
const responseMessage: Message = {
id,
isUserMessage: false,
text: "",
};
addMessage(responseMessage);
setIsMessageUpdating(true);

const reader = stream.getReader();
const decoder = new TextDecoder();
let done = false;

while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunkValue = decoder.decode(value);
updateMessage(id, (prev) => prev + chunkValue);
}

setIsMessageUpdating(false);
setTimeout(() => {
textareaAutoRef.current?.focus();
}, 10);
},
onError: (_, message) => {
toast.error("Something went wrong. Please try again.");
removeMessage(message.id);
setInput(inputFallback);
setIsMessageUpdating(false);
setTimeout(() => {
textareaAutoRef.current?.focus();
}, 10);
},
});

return (
<div {...props} className={cn("border-t border-gray-500", className)}>
<div className="relative mt-4 flex-1 overflow-hidden rounded-lg border-none outline-none">
<TextareaAutosize
style={{ background: "rgba( 0, 0, 0, 0.2 )" }}
ref={textareaAutoRef}
rows={2}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();

const message: Message = {
id: nanoid(),
isUserMessage: true,
text: input,
};

sendMessage(message);
}
}}
disabled={isPending}
maxRows={4}
value={input}
onChange={(e) => setInput(e.target.value)}
autoFocus
placeholder="Write a message..."
className="peer disabled:opacity-50 resize-none block w-full border-0 py-3 px-5 text-gray-200 focus:ring-0 text-sm sm:leading-6"
/>

<div className="absolute inset-y-0 right-0 flex py-1.5 pr-1.5">
<kbd className="inline-flex items-center rounded border bg-[#2e4969] border-none px-1 font-sans text-xs text-gray-200">
{isPending || isMessageUpdating ? (
<Loader2 className="h-3 animate-spin" />
) : (
<CornerDownLeft className="h-3" />
)}
</kbd>
</div>
</div>
</div>
);
};

export default ChatInput;
Loading

0 comments on commit 441fa74

Please sign in to comment.