diff --git a/components/ChatView/MessageView.tsx b/components/ChatView/MessageView.tsx
new file mode 100644
index 0000000..1d52385
--- /dev/null
+++ b/components/ChatView/MessageView.tsx
@@ -0,0 +1,13 @@
+import { Message } from "../../types";
+
+interface Props {
+ message: Message;
+}
+
+const Message = (props: Props) => {
+ const message = props.message;
+
+ return
{message.content}
;
+};
+
+export default Message;
diff --git a/components/ChatView/Sidebar.tsx b/components/ChatView/Sidebar.tsx
new file mode 100644
index 0000000..72ca290
--- /dev/null
+++ b/components/ChatView/Sidebar.tsx
@@ -0,0 +1,32 @@
+import { useChatStore, useUserStore } from "../../store";
+import { User } from "../../types";
+
+const Sidebar = () => {
+ const userStore = useUserStore();
+ const chatStore = useChatStore();
+
+ const handleAssistantClick = (user: User) => {
+ for (const chat of chatStore.chatList) {
+ if (chat.userId === user.id) {
+ chatStore.setCurrentChat(chat);
+ return;
+ }
+ }
+ chatStore.createChat(user);
+ };
+
+ return (
+
+
Assistant list
+
+ {userStore.assistantList.map((assistant) => (
+
handleAssistantClick(assistant)} key={assistant.id}>
+ {assistant.name}
+
+ ))}
+
+
+ );
+};
+
+export default Sidebar;
diff --git a/components/ChatView/Textarea.tsx b/components/ChatView/Textarea.tsx
new file mode 100644
index 0000000..cb50e13
--- /dev/null
+++ b/components/ChatView/Textarea.tsx
@@ -0,0 +1,59 @@
+import axios from "axios";
+import { useRef, useState } from "react";
+import { useChatStore, useMessageStore, useUserStore } from "../../store";
+import { UserRole } from "../../types";
+import { generateUUID } from "../../utils";
+import Icon from "../Icon";
+
+const Textarea = () => {
+ const userStore = useUserStore();
+ const chatStore = useChatStore();
+ const messageStore = useMessageStore();
+ const [value, setValue] = useState("");
+ const textareaRef = useRef(null);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setValue(e.target.value);
+ };
+
+ const handleSend = async () => {
+ if (!chatStore.currentChat) {
+ return;
+ }
+
+ const currentChat = chatStore.currentChat;
+ messageStore.addMessage({
+ id: generateUUID(),
+ chatId: currentChat.id,
+ creatorId: userStore.currentUser.id,
+ createdAt: Date.now(),
+ content: value,
+ });
+ setValue("");
+ textareaRef.current!.value = "";
+
+ const messageList = messageStore.getState().messageList.filter((message) => message.chatId === currentChat.id);
+ const { data } = await axios.post("/api/chat", {
+ messages: messageList.map((message) => ({
+ role: message.creatorId === userStore.currentUser.id ? UserRole.User : UserRole.Assistant,
+ content: message.content,
+ })),
+ });
+ messageStore.addMessage({
+ id: generateUUID(),
+ chatId: currentChat.id,
+ creatorId: currentChat.userId,
+ createdAt: Date.now(),
+ content: data,
+ });
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default Textarea;
diff --git a/components/ChatView/index.tsx b/components/ChatView/index.tsx
new file mode 100644
index 0000000..74e96e9
--- /dev/null
+++ b/components/ChatView/index.tsx
@@ -0,0 +1,28 @@
+import { useChatStore, useMessageStore, useUserStore } from "../../store";
+import MessageView from "./MessageView";
+import Sidebar from "./Sidebar";
+import Textarea from "./Textarea";
+
+const ChatView = () => {
+ const chatStore = useChatStore();
+ const userStore = useUserStore();
+ const messageStore = useMessageStore();
+ const currentChat = chatStore.currentChat;
+ const chatTitle = currentChat ? userStore.getAssistantById(currentChat.userId)?.name : "No chat";
+ const messageList = messageStore.messageList.filter((message) => message.chatId === currentChat?.id);
+
+ return (
+
+
+
+ {chatTitle}
+
+ {messageList.length === 0 ?
no message
: messageList.map((message) =>
)}
+
+
+
+
+ );
+};
+
+export default ChatView;
diff --git a/components/Icon.tsx b/components/Icon.tsx
new file mode 100644
index 0000000..56882fe
--- /dev/null
+++ b/components/Icon.tsx
@@ -0,0 +1,3 @@
+import * as Icon from "lucide-react";
+
+export default Icon;
diff --git a/package.json b/package.json
index 82055e1..9334cf0 100644
--- a/package.json
+++ b/package.json
@@ -8,15 +8,22 @@
},
"dependencies": {
"@vercel/analytics": "^0.1.11",
+ "axios": "^1.3.4",
+ "csstype": "^3.1.1",
"highlight.js": "^11.7.0",
+ "lucide-react": "^0.125.0",
"next": "^13.2.4",
"openai": "^3.0.0",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "react-hot-toast": "^2.4.0",
+ "uuid": "^9.0.0",
+ "zustand": "^4.3.6"
},
"devDependencies": {
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
+ "@types/uuid": "^9.0.1",
"autoprefixer": "^10.4.13",
"eslint": "8.20.0",
"eslint-config-next": "12.2.3",
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 90defae..5a8e3d8 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -1,7 +1,7 @@
import { AppProps } from "next/app";
import React from "react";
import { Analytics } from "@vercel/analytics/react";
-import "./styles/globals.css";
+import "../styles/global.css";
function MyApp({ Component, pageProps }: AppProps) {
return (
diff --git a/pages/api/chat.ts b/pages/api/chat.ts
index 11da40c..7000a66 100644
--- a/pages/api/chat.ts
+++ b/pages/api/chat.ts
@@ -1,18 +1,17 @@
import { NextApiRequest, NextApiResponse } from "next";
-import openai, { ChatCompletionResponse } from "./openai-api";
+import openai from "../../utils/openai-api";
-const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const completionResponse = await openai.createChatCompletion({
model: "gpt-3.5-turbo",
messages: req.body.messages,
max_tokens: 2000,
temperature: 0,
- top_p: 1.0,
frequency_penalty: 0.0,
presence_penalty: 0.0,
});
- res.status(200).json({ message: completionResponse.data.choices[0].message! });
+ res.status(200).json(completionResponse.data.choices[0].message?.content || "");
};
// TODO(steven): Implement a generic getChatPrompt function that takes in a
diff --git a/pages/chat.tsx b/pages/chat.tsx
index 11fe0db..eb6584e 100644
--- a/pages/chat.tsx
+++ b/pages/chat.tsx
@@ -1,12 +1,9 @@
import { NextPage } from "next";
import Head from "next/head";
-import React, { useEffect } from "react";
+import React from "react";
+import ChatView from "../components/ChatView";
const ChatPage: NextPage = () => {
- useEffect(() => {
- // todo
- }, []);
-
return (
@@ -15,7 +12,9 @@ const ChatPage: NextPage = () => {
- WIP
+
+
+
);
};
diff --git a/pages/index.tsx b/pages/index.tsx
index ca261a8..4b459bc 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -12,12 +12,12 @@ const HomePage: NextPage = () => {
-
-
-
ChatDBA
-
+
+
+
ChatDBA
+
-
Chat →
+ Chat →
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dd6f3e7..26f3819 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3,30 +3,44 @@ lockfileVersion: 5.4
specifiers:
'@types/node': ^18.11.18
'@types/react': ^18.0.26
+ '@types/uuid': ^9.0.1
'@vercel/analytics': ^0.1.11
autoprefixer: ^10.4.13
+ axios: ^1.3.4
+ csstype: ^3.1.1
eslint: 8.20.0
eslint-config-next: 12.2.3
highlight.js: ^11.7.0
+ lucide-react: ^0.125.0
next: ^13.2.4
openai: ^3.0.0
postcss: ^8.4.20
react: ^18.2.0
react-dom: ^18.2.0
+ react-hot-toast: ^2.4.0
tailwindcss: ^3.2.4
typescript: ^4.9.4
+ uuid: ^9.0.0
+ zustand: ^4.3.6
dependencies:
'@vercel/analytics': 0.1.11_react@18.2.0
+ axios: 1.3.4
+ csstype: 3.1.1
highlight.js: 11.7.0
+ lucide-react: 0.125.0_react@18.2.0
next: 13.2.4_biqbaboplfbrettd7655fr4n2y
openai: 3.2.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
+ react-hot-toast: 2.4.0_owo25xnefcwdq3zjgtohz6dbju
+ uuid: 9.0.0
+ zustand: 4.3.6_react@18.2.0
devDependencies:
'@types/node': 18.15.3
'@types/react': 18.0.28
+ '@types/uuid': 9.0.1
autoprefixer: 10.4.14_postcss@8.4.21
eslint: 8.20.0
eslint-config-next: 12.2.3_bqegqxcnsisudkhpmmezgt6uoa
@@ -257,6 +271,10 @@ packages:
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
dev: true
+ /@types/uuid/9.0.1:
+ resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
+ dev: true
+
/@typescript-eslint/parser/5.55.0_bqegqxcnsisudkhpmmezgt6uoa:
resolution: {integrity: sha512-ppvmeF7hvdhUUZWSd2EEWfzcFkjJzgNQzVST22nzg958CR+sphy8A6K7LXQZd6V75m1VKjp+J4g/PCEfSCmzhw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -498,6 +516,16 @@ packages:
- debug
dev: false
+ /axios/1.3.4:
+ resolution: {integrity: sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==}
+ dependencies:
+ follow-redirects: 1.15.2
+ form-data: 4.0.0
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+ dev: false
+
/axobject-query/3.1.1:
resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==}
dependencies:
@@ -624,7 +652,6 @@ packages:
/csstype/3.1.1:
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
- dev: true
/damerau-levenshtein/1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -1306,6 +1333,14 @@ packages:
slash: 3.0.0
dev: true
+ /goober/2.1.12_csstype@3.1.1:
+ resolution: {integrity: sha512-yXHAvO08FU1JgTXX6Zn6sYCUFfB/OJSX8HHjDSgerZHZmFKAb08cykp5LBw5QnmyMcZyPRMqkdyHUSSzge788Q==}
+ peerDependencies:
+ csstype: ^3.0.10
+ dependencies:
+ csstype: 3.1.1
+ dev: false
+
/gopd/1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
dependencies:
@@ -1623,6 +1658,14 @@ packages:
yallist: 4.0.0
dev: true
+ /lucide-react/0.125.0_react@18.2.0:
+ resolution: {integrity: sha512-tadphtB6TPytEitR9vX75hqu9PQT/uz5RcvXMq976nC190eukAM9+cHMgBxfvfEGDXwIhIT9aFxTUGdAjxw9uQ==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.2.0
+ dev: false
+
/merge2/1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -1965,6 +2008,10 @@ packages:
react-is: 16.13.1
dev: true
+ /proxy-from-env/1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+ dev: false
+
/punycode/2.3.0:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'}
@@ -1989,6 +2036,20 @@ packages:
scheduler: 0.23.0
dev: false
+ /react-hot-toast/2.4.0_owo25xnefcwdq3zjgtohz6dbju:
+ resolution: {integrity: sha512-qnnVbXropKuwUpriVVosgo8QrB+IaPJCpL8oBI6Ov84uvHZ5QQcTp2qg6ku2wNfgJl6rlQXJIQU5q+5lmPOutA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ react: '>=16'
+ react-dom: '>=16'
+ dependencies:
+ goober: 2.1.12_csstype@3.1.1
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+ transitivePeerDependencies:
+ - csstype
+ dev: false
+
/react-is/16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
@@ -2343,10 +2404,23 @@ packages:
punycode: 2.3.0
dev: true
+ /use-sync-external-store/1.2.0_react@18.2.0:
+ resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.2.0
+ dev: false
+
/util-deprecate/1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
+ /uuid/9.0.0:
+ resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
+ hasBin: true
+ dev: false
+
/v8-compile-cache/2.3.0:
resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
dev: true
@@ -2412,3 +2486,19 @@ packages:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
dev: true
+
+ /zustand/4.3.6_react@18.2.0:
+ resolution: {integrity: sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==}
+ engines: {node: '>=12.7.0'}
+ peerDependencies:
+ immer: '>=9.0'
+ react: '>=16.8'
+ peerDependenciesMeta:
+ immer:
+ optional: true
+ react:
+ optional: true
+ dependencies:
+ react: 18.2.0
+ use-sync-external-store: 1.2.0_react@18.2.0
+ dev: false
diff --git a/store/chat.ts b/store/chat.ts
new file mode 100644
index 0000000..650bfb2
--- /dev/null
+++ b/store/chat.ts
@@ -0,0 +1,31 @@
+import { create } from "zustand";
+import { Chat, User } from "../types";
+import { generateUUID } from "../utils";
+
+const defaultChat: Chat = {
+ id: generateUUID(),
+ userId: "assistant-chatgpt",
+};
+
+interface ChatState {
+ chatList: Chat[];
+ currentChat: Chat;
+ createChat: (user: User) => void;
+ setCurrentChat: (chat: Chat) => void;
+}
+
+export const useChatStore = create((set) => ({
+ chatList: [],
+ currentChat: defaultChat,
+ createChat: (user: User) => {
+ const chat = {
+ id: generateUUID(),
+ userId: user.id,
+ };
+ set((state) => ({
+ chatList: [...state.chatList, chat],
+ currentChat: chat,
+ }));
+ },
+ setCurrentChat: (chat: Chat) => set(() => ({ currentChat: chat })),
+}));
diff --git a/store/index.ts b/store/index.ts
new file mode 100644
index 0000000..5e5bd31
--- /dev/null
+++ b/store/index.ts
@@ -0,0 +1,3 @@
+export * from "./user";
+export * from "./chat";
+export * from "./message";
diff --git a/store/message.ts b/store/message.ts
new file mode 100644
index 0000000..1820d7e
--- /dev/null
+++ b/store/message.ts
@@ -0,0 +1,14 @@
+import { create } from "zustand";
+import { Message } from "../types";
+
+interface MessageState {
+ messageList: Message[];
+ getState: () => MessageState;
+ addMessage: (message: Message) => void;
+}
+
+export const useMessageStore = create((set, get) => ({
+ messageList: [],
+ getState: () => get(),
+ addMessage: (message: Message) => set((state) => ({ messageList: [...state.messageList, message] })),
+}));
diff --git a/store/user.ts b/store/user.ts
new file mode 100644
index 0000000..46fcda7
--- /dev/null
+++ b/store/user.ts
@@ -0,0 +1,36 @@
+import { create } from "zustand";
+import { Id, User, UserRole } from "../types";
+
+const assistantList: User[] = [
+ {
+ id: "assistant-chatgpt",
+ name: "Origin ChatGPT",
+ description: "",
+ avatar: "",
+ role: UserRole.Assistant,
+ },
+];
+
+const localUser: User = {
+ id: "local-user",
+ name: "Local user",
+ description: "",
+ avatar: "",
+ role: UserRole.User,
+};
+
+interface UserState {
+ // We can think assistants are special users.
+ assistantList: User[];
+ currentUser: User;
+ getAssistantById: (id: string) => User | undefined;
+}
+
+export const useUserStore = create((set) => ({
+ assistantList: assistantList,
+ currentUser: localUser,
+ getAssistantById: (id: Id) => {
+ const user = assistantList.find((user) => user.id === id);
+ return user || undefined;
+ },
+}));
diff --git a/pages/styles/globals.css b/styles/global.css
similarity index 100%
rename from pages/styles/globals.css
rename to styles/global.css
diff --git a/types/chat.ts b/types/chat.ts
new file mode 100644
index 0000000..dc4c99e
--- /dev/null
+++ b/types/chat.ts
@@ -0,0 +1,6 @@
+import { Id } from "./common";
+
+export interface Chat {
+ id: string;
+ userId: Id;
+}
diff --git a/types/common.ts b/types/common.ts
new file mode 100644
index 0000000..e5bada3
--- /dev/null
+++ b/types/common.ts
@@ -0,0 +1,2 @@
+export type Id = string;
+export type Timestamp = number;
diff --git a/types/index.ts b/types/index.ts
new file mode 100644
index 0000000..ff6f2e3
--- /dev/null
+++ b/types/index.ts
@@ -0,0 +1,4 @@
+export * from "./common";
+export * from "./user";
+export * from "./chat";
+export * from "./message";
diff --git a/types/message.ts b/types/message.ts
new file mode 100644
index 0000000..1cfe635
--- /dev/null
+++ b/types/message.ts
@@ -0,0 +1,9 @@
+import { Id, Timestamp } from "./";
+
+export interface Message {
+ id: Id;
+ chatId: string;
+ creatorId: Id;
+ createdAt: Timestamp;
+ content: string;
+}
diff --git a/types/user.ts b/types/user.ts
new file mode 100644
index 0000000..85b3a35
--- /dev/null
+++ b/types/user.ts
@@ -0,0 +1,13 @@
+export enum UserRole {
+ System = "system",
+ User = "user",
+ Assistant = "assistant",
+}
+
+export interface User {
+ id: string;
+ name: string;
+ description: string;
+ avatar: string;
+ role: UserRole;
+}
diff --git a/utils/index.ts b/utils/index.ts
new file mode 100644
index 0000000..5e43db9
--- /dev/null
+++ b/utils/index.ts
@@ -0,0 +1,5 @@
+import { v4 as uuidv4 } from "uuid";
+
+export const generateUUID = () => {
+ return uuidv4();
+};
diff --git a/pages/api/openai-api.ts b/utils/openai-api.ts
similarity index 99%
rename from pages/api/openai-api.ts
rename to utils/openai-api.ts
index 008ba2f..97c21b8 100644
--- a/pages/api/openai-api.ts
+++ b/utils/openai-api.ts
@@ -7,6 +7,7 @@ export interface ChatCompletionResponse {
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
+
const openai = new OpenAIApi(configuration);
export default openai;