mvp
This commit is contained in:
parent
e87136c4ec
commit
758a1215c2
|
@ -0,0 +1,39 @@
|
||||||
|
import { Conversation } from "@/types";
|
||||||
|
import { IconMessage, IconTrash } from "@tabler/icons-react";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
conversations: Conversation[];
|
||||||
|
selectedConversation: Conversation;
|
||||||
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
|
onDeleteConversation: (conversation: Conversation) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Conversations: FC<Props> = ({ conversations, selectedConversation, onSelectConversation, onDeleteConversation }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
{conversations.map((conversation, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`flex items-center justify-start w-[240px] h-[40px] px-2 text-sm rounded-lg hover:bg-neutral-700 cursor-pointer ${selectedConversation.id === conversation.id ? "bg-slate-600" : ""}`}
|
||||||
|
onClick={() => onSelectConversation(conversation)}
|
||||||
|
>
|
||||||
|
<IconMessage
|
||||||
|
className="mr-2 min-w-[20px]"
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
<div className="overflow-hidden whitespace-nowrap overflow-ellipsis pr-1">{conversation.messages[0] ? conversation.messages[0].content : "Empty conversation"}</div>
|
||||||
|
|
||||||
|
<IconTrash
|
||||||
|
className="ml-auto min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteConversation(conversation);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -11,7 +11,7 @@ export const ModelSelect: FC<Props> = ({ model, onSelect }) => {
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className="text-left mb-2 dark:text-neutral-400 text-neutral-700">Model</label>
|
<label className="text-left mb-2 dark:text-neutral-400 text-neutral-700">Model</label>
|
||||||
<select
|
<select
|
||||||
className="w-[300px] p-3 dark:text-white dark:bg-[#343541] border border-neutral-500 rounded-lg appearance-none focus:shadow-outline text-neutral-900"
|
className="w-[300px] p-3 dark:text-white dark:bg-[#343541] border border-neutral-500 rounded-lg appearance-none focus:shadow-outline text-neutral-900 cursor-pointer"
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
value={model}
|
value={model}
|
||||||
onChange={(e) => onSelect(e.target.value as OpenAIModel)}
|
onChange={(e) => onSelect(e.target.value as OpenAIModel)}
|
||||||
|
|
|
@ -1,17 +1,27 @@
|
||||||
|
import { Conversation } from "@/types";
|
||||||
import { IconPlus } from "@tabler/icons-react";
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
import { Conversations } from "../Conversations";
|
||||||
import { SidebarSettings } from "./SidebarSettings";
|
import { SidebarSettings } from "./SidebarSettings";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
conversations: Conversation[];
|
||||||
lightMode: "light" | "dark";
|
lightMode: "light" | "dark";
|
||||||
|
selectedConversation: Conversation;
|
||||||
|
onNewConversation: () => void;
|
||||||
onToggleLightMode: (mode: "light" | "dark") => void;
|
onToggleLightMode: (mode: "light" | "dark") => void;
|
||||||
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
|
onDeleteConversation: (conversation: Conversation) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar: FC<Props> = ({ lightMode, onToggleLightMode }) => {
|
export const Sidebar: FC<Props> = ({ conversations, lightMode, selectedConversation, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-[#202123] min-w-[260px]">
|
<div className="flex flex-col bg-[#202123] min-w-[260px]">
|
||||||
<div className="flex items-center justify-center h-[60px]">
|
<div className="flex items-center justify-center h-[60px]">
|
||||||
<button className="flex items-center w-[240px] h-[40px] rounded-lg bg-[#202123] border border-neutral-600 text-sm hover:bg-neutral-700">
|
<button
|
||||||
|
className="flex items-center w-[240px] h-[40px] rounded-lg bg-[#202123] border border-neutral-600 text-sm hover:bg-neutral-700"
|
||||||
|
onClick={onNewConversation}
|
||||||
|
>
|
||||||
<IconPlus
|
<IconPlus
|
||||||
className="ml-4 mr-3"
|
className="ml-4 mr-3"
|
||||||
size={16}
|
size={16}
|
||||||
|
@ -20,7 +30,14 @@ export const Sidebar: FC<Props> = ({ lightMode, onToggleLightMode }) => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1"></div>
|
<div className="flex-1 mx-auto pb-2 overflow-auto">
|
||||||
|
<Conversations
|
||||||
|
conversations={conversations}
|
||||||
|
selectedConversation={selectedConversation}
|
||||||
|
onSelectConversation={onSelectConversation}
|
||||||
|
onDeleteConversation={onDeleteConversation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SidebarSettings
|
<SidebarSettings
|
||||||
lightMode={lightMode}
|
lightMode={lightMode}
|
||||||
|
|
243
pages/index.tsx
243
pages/index.tsx
|
@ -1,80 +1,112 @@
|
||||||
import { Chat } from "@/components/Chat/Chat";
|
import { Chat } from "@/components/Chat/Chat";
|
||||||
import { Sidebar } from "@/components/Sidebar/Sidebar";
|
import { Sidebar } from "@/components/Sidebar/Sidebar";
|
||||||
import { Message, OpenAIModel } from "@/types";
|
import { Conversation, Message, OpenAIModel } from "@/types";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||||
|
const [selectedConversation, setSelectedConversation] = useState<Conversation>();
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [model, setModel] = useState<OpenAIModel>(OpenAIModel.GPT_3_5);
|
const [model, setModel] = useState<OpenAIModel>(OpenAIModel.GPT_3_5);
|
||||||
const [lightMode, setLightMode] = useState<"dark" | "light">("dark");
|
const [lightMode, setLightMode] = useState<"dark" | "light">("dark");
|
||||||
|
|
||||||
const handleSend = async (message: Message) => {
|
const handleSend = async (message: Message) => {
|
||||||
const updatedMessages = [...messages, message];
|
if (selectedConversation) {
|
||||||
|
let updatedConversation: Conversation = {
|
||||||
|
...selectedConversation,
|
||||||
|
messages: [...selectedConversation.messages, message]
|
||||||
|
};
|
||||||
|
|
||||||
setMessages(updatedMessages);
|
setSelectedConversation(updatedConversation);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const response = await fetch("/api/chat", {
|
const response = await fetch("/api/chat", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model,
|
model,
|
||||||
messages: updatedMessages
|
messages: updatedConversation.messages
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
}
|
|
||||||
|
|
||||||
const data = response.body;
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
const reader = data.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let done = false;
|
|
||||||
let isFirst = true;
|
|
||||||
let text = "";
|
|
||||||
|
|
||||||
while (!done) {
|
|
||||||
const { value, done: doneReading } = await reader.read();
|
|
||||||
done = doneReading;
|
|
||||||
const chunkValue = decoder.decode(value);
|
|
||||||
|
|
||||||
text += chunkValue;
|
|
||||||
|
|
||||||
if (isFirst) {
|
|
||||||
isFirst = false;
|
|
||||||
setMessages((messages) => [
|
|
||||||
...messages,
|
|
||||||
{
|
|
||||||
role: "assistant",
|
|
||||||
content: chunkValue
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
setMessages((messages) => {
|
|
||||||
const lastMessage = messages[messages.length - 1];
|
|
||||||
const updatedMessage = {
|
|
||||||
...lastMessage,
|
|
||||||
content: lastMessage.content + chunkValue
|
|
||||||
};
|
|
||||||
return [...messages.slice(0, -1), updatedMessage];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem("messageHistory", JSON.stringify([...updatedMessages, { role: "assistant", content: text }]));
|
const data = response.body;
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
const reader = data.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let done = false;
|
||||||
|
let isFirst = true;
|
||||||
|
let text = "";
|
||||||
|
|
||||||
|
while (!done) {
|
||||||
|
const { value, done: doneReading } = await reader.read();
|
||||||
|
done = doneReading;
|
||||||
|
const chunkValue = decoder.decode(value);
|
||||||
|
|
||||||
|
text += chunkValue;
|
||||||
|
|
||||||
|
if (isFirst) {
|
||||||
|
isFirst = false;
|
||||||
|
const updatedMessages: Message[] = [...updatedConversation.messages, { role: "assistant", content: chunkValue }];
|
||||||
|
|
||||||
|
updatedConversation = {
|
||||||
|
...updatedConversation,
|
||||||
|
messages: updatedMessages
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedConversation(updatedConversation);
|
||||||
|
} else {
|
||||||
|
const updatedMessages: Message[] = updatedConversation.messages.map((message, index) => {
|
||||||
|
if (index === updatedConversation.messages.length - 1) {
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
content: text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
});
|
||||||
|
|
||||||
|
updatedConversation = {
|
||||||
|
...updatedConversation,
|
||||||
|
messages: updatedMessages
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedConversation(updatedConversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("selectedConversation", JSON.stringify(updatedConversation));
|
||||||
|
|
||||||
|
const updatedConversations: Conversation[] = conversations.map((conversation) => {
|
||||||
|
if (conversation.id === selectedConversation.id) {
|
||||||
|
return updatedConversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversation;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updatedConversations.length === 0) {
|
||||||
|
updatedConversations.push(updatedConversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
setConversations(updatedConversations);
|
||||||
|
|
||||||
|
localStorage.setItem("conversationHistory", JSON.stringify(updatedConversations));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLightMode = (mode: "dark" | "light") => {
|
const handleLightMode = (mode: "dark" | "light") => {
|
||||||
|
@ -82,18 +114,61 @@ export default function Home() {
|
||||||
localStorage.setItem("theme", mode);
|
localStorage.setItem("theme", mode);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNewConversation = () => {
|
||||||
|
const newConversation: Conversation = {
|
||||||
|
id: conversations.length + 1,
|
||||||
|
name: "",
|
||||||
|
messages: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedConversations = [...conversations, newConversation];
|
||||||
|
setConversations(updatedConversations);
|
||||||
|
localStorage.setItem("conversationHistory", JSON.stringify(updatedConversations));
|
||||||
|
|
||||||
|
setSelectedConversation(newConversation);
|
||||||
|
localStorage.setItem("selectedConversation", JSON.stringify(newConversation));
|
||||||
|
|
||||||
|
setModel(OpenAIModel.GPT_3_5);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectConversation = (conversation: Conversation) => {
|
||||||
|
setSelectedConversation(conversation);
|
||||||
|
localStorage.setItem("selectedConversation", JSON.stringify(conversation));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConversation = (conversation: Conversation) => {
|
||||||
|
const updatedConversations = conversations.filter((c) => c.id !== conversation.id);
|
||||||
|
setConversations(updatedConversations);
|
||||||
|
localStorage.setItem("conversationHistory", JSON.stringify(updatedConversations));
|
||||||
|
|
||||||
|
if (selectedConversation && selectedConversation.id === conversation.id) {
|
||||||
|
setSelectedConversation(undefined);
|
||||||
|
localStorage.removeItem("selectedConversation");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const theme = localStorage.getItem("theme");
|
const theme = localStorage.getItem("theme");
|
||||||
|
|
||||||
if (theme) {
|
if (theme) {
|
||||||
setLightMode(theme as "dark" | "light");
|
setLightMode(theme as "dark" | "light");
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageHistory = localStorage.getItem("messageHistory");
|
const conversationHistory = localStorage.getItem("conversationHistory");
|
||||||
console.log(messageHistory);
|
|
||||||
|
|
||||||
if (messageHistory) {
|
if (conversationHistory) {
|
||||||
setMessages(JSON.parse(messageHistory));
|
setConversations(JSON.parse(conversationHistory));
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedConversation = localStorage.getItem("selectedConversation");
|
||||||
|
if (selectedConversation) {
|
||||||
|
setSelectedConversation(JSON.parse(selectedConversation));
|
||||||
|
} else {
|
||||||
|
setSelectedConversation({
|
||||||
|
id: 1,
|
||||||
|
name: "",
|
||||||
|
messages: []
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -114,23 +189,29 @@ export default function Home() {
|
||||||
href="/favicon.ico"
|
href="/favicon.ico"
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
{selectedConversation && (
|
||||||
<div className={`flex h-screen text-white ${lightMode}`}>
|
<div className={`flex h-screen text-white ${lightMode}`}>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
lightMode={lightMode}
|
conversations={conversations}
|
||||||
onToggleLightMode={handleLightMode}
|
lightMode={lightMode}
|
||||||
/>
|
selectedConversation={selectedConversation}
|
||||||
|
onToggleLightMode={handleLightMode}
|
||||||
<div className="flex flex-col w-full h-full dark:bg-[#343541]">
|
onNewConversation={handleNewConversation}
|
||||||
<Chat
|
onSelectConversation={handleSelectConversation}
|
||||||
model={model}
|
onDeleteConversation={handleDeleteConversation}
|
||||||
messages={messages}
|
|
||||||
loading={loading}
|
|
||||||
onSend={handleSend}
|
|
||||||
onSelect={setModel}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-full h-full dark:bg-[#343541]">
|
||||||
|
<Chat
|
||||||
|
model={model}
|
||||||
|
messages={selectedConversation.messages}
|
||||||
|
loading={loading}
|
||||||
|
onSend={handleSend}
|
||||||
|
onSelect={setModel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,3 +16,9 @@ export interface Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Role = "assistant" | "user";
|
export type Role = "assistant" | "user";
|
||||||
|
|
||||||
|
export interface Conversation {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
messages: Message[];
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue