Merge branch 'feat/user-chat-history' of github.com:StanGirard/Quivr into feat/user-chat-history

This commit is contained in:
gozineb 2023-06-10 16:33:31 +02:00
commit c734202eca
17 changed files with 486 additions and 25 deletions

View File

@ -2,8 +2,7 @@ import os
import pypandoc
from auth.auth_bearer import JWTBearer
from fastapi import Depends, FastAPI
from llm.summarization import llm_evaluate_summaries
from fastapi import FastAPI
from logger import get_logger
from middlewares.cors import add_cors_middleware
from models.chats import ChatMessage

View File

@ -4,7 +4,7 @@ from tempfile import SpooledTemporaryFile
from auth.auth_bearer import JWTBearer
from crawl.crawler import CrawlWebsite
from fastapi import APIRouter, Depends, UploadFile
from fastapi import APIRouter, Depends, Request, UploadFile
from middlewares.cors import add_cors_middleware
from models.users import User
from parsers.github import process_github
@ -15,9 +15,11 @@ from utils.vectors import CommonsDep
crawl_router = APIRouter()
@crawl_router.post("/crawl/", dependencies=[Depends(JWTBearer())])
async def crawl_endpoint(commons: CommonsDep, crawl_website: CrawlWebsite, enable_summarization: bool = False, credentials: dict = Depends(JWTBearer())):
async def crawl_endpoint(request: Request,commons: CommonsDep, crawl_website: CrawlWebsite, enable_summarization: bool = False, credentials: dict = Depends(JWTBearer())):
max_brain_size = os.getenv("MAX_BRAIN_SIZE")
if request.headers.get('Openai-Api-Key'):
max_brain_size = os.getenv("MAX_BRAIN_SIZE_WITH_KEY",209715200)
user = User(email=credentials.get('email', 'none'))
user_vectors_response = commons['supabase'].table("vectors").select(
"name:metadata->>file_name, size:metadata->>file_size", count="exact") \

View File

@ -3,14 +3,14 @@ import os
import time
from auth.auth_bearer import JWTBearer
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Request
from models.users import User
from utils.vectors import CommonsDep
user_router = APIRouter()
max_brain_size_with_own_key = os.getenv("MAX_BRAIN_SIZE_WITH_KEY",209715200)
@user_router.get("/user", dependencies=[Depends(JWTBearer())])
async def get_user_endpoint(commons: CommonsDep, credentials: dict = Depends(JWTBearer())):
async def get_user_endpoint(request: Request,commons: CommonsDep, credentials: dict = Depends(JWTBearer())):
# Create a function that returns the unique documents out of the vectors
# Create a function that returns the list of documents that can take in what to put in the select + the filter
@ -27,11 +27,17 @@ async def get_user_endpoint(commons: CommonsDep, credentials: dict = Depends(JWT
current_brain_size = sum(float(doc['size']) for doc in user_unique_vectors)
max_brain_size = os.getenv("MAX_BRAIN_SIZE")
if request.headers.get('Openai-Api-Key'):
max_brain_size = max_brain_size_with_own_key
# Create function get user request stats -> nombre de requetes par jour + max number of requests -> svg to display the number of requests ? une fusee ?
user = User(email=credentials.get('email', 'none'))
date = time.strftime("%Y%m%d")
max_requests_number = os.getenv("MAX_REQUESTS_NUMBER")
if request.headers.get('Openai-Api-Key'):
max_brain_size = max_brain_size_with_own_key
requests_stats = commons['supabase'].from_('users').select(
'*').filter("email", "eq", user.email).execute()

View File

@ -0,0 +1,30 @@
"use client";
import { UUID } from "crypto";
import PageHeading from "../../components/ui/PageHeading";
import { ChatInput, ChatMessages } from "../components";
import useChats from "../hooks/useChats";
interface ChatPageProps {
params?: {
chatId?: UUID;
};
}
export default function ChatPage({ params }: ChatPageProps) {
const chatId: UUID | undefined = params?.chatId;
const { chat, ...others } = useChats(chatId);
return (
<main className="flex flex-col w-full">
<section className="flex flex-col items-center w-full overflow-auto">
<PageHeading
title="Chat with your brain"
subtitle="Talk to a language model about your uploaded data"
/>
{chat && <ChatMessages chat={chat} />}
<ChatInput chatId={chatId} {...others} />
</section>
</main>
);
}

View File

@ -0,0 +1,14 @@
"use client";
import Link from "next/link";
import { MdSettings } from "react-icons/md";
import Button from "../../../../components/ui/Button";
export function ConfigButton() {
return (
<Link href={"/config"}>
<Button className="px-3" variant={"tertiary"}>
<MdSettings className="text-2xl" />
</Button>
</Link>
);
}

View File

@ -0,0 +1,24 @@
"use client";
import { MdMic, MdMicOff } from "react-icons/md";
import Button from "../../../../components/ui/Button";
import { useSpeech } from "../../../hooks/useSpeech";
export function MicButton() {
const { isListening, speechSupported, startListening } = useSpeech();
return (
<Button
className="px-3"
variant={"tertiary"}
type="button"
onClick={startListening}
disabled={!speechSupported}
>
{isListening ? (
<MdMicOff className="text-2xl" />
) : (
<MdMic className="text-2xl" />
)}
</Button>
);
}

View File

@ -0,0 +1,52 @@
"use client";
import { ChatMessage } from "@/app/chat/types";
import { UUID } from "crypto";
import { Dispatch, SetStateAction } from "react";
import Button from "../../../../components/ui/Button";
import { ConfigButton } from "./ConfigButton";
import { MicButton } from "./MicButton";
interface ChatInputProps {
isSendingMessage: boolean;
sendMessage: (chatId?: UUID, msg?: ChatMessage) => Promise<void>;
setMessage: Dispatch<SetStateAction<ChatMessage>>;
message: ChatMessage;
chatId?: UUID;
}
export function ChatInput({
chatId,
isSendingMessage,
message,
sendMessage,
setMessage,
}: ChatInputProps) {
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (!isSendingMessage) sendMessage(chatId);
}}
className="fixed p-5 bg-white dark:bg-black rounded-t-md border border-black/10 dark:border-white/25 bottom-0 w-full max-w-3xl flex items-center justify-center gap-2 z-20"
>
<textarea
autoFocus
value={message[1]}
onChange={(e) => setMessage((msg) => [msg[0], e.target.value])}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); // Prevents the newline from being entered in the textarea
if (!isSendingMessage) sendMessage(chatId); // Call the submit function here
}
}}
className="w-full p-2 border border-gray-300 dark:border-gray-500 outline-none rounded dark:bg-gray-800"
placeholder="Begin conversation here..."
/>
<Button type="submit" isLoading={isSendingMessage}>
{isSendingMessage ? "Thinking..." : "Chat"}
</Button>
<MicButton />
<ConfigButton />
</form>
);
}

View File

@ -1,7 +1,8 @@
import { cn } from "@/lib/utils"
import { motion } from "framer-motion"
import { forwardRef, Ref } from "react"
import ReactMarkdown from "react-markdown"
"use client";
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import { forwardRef, Ref } from "react";
import ReactMarkdown from "react-markdown";
const ChatMessage = forwardRef(
(
@ -9,8 +10,8 @@ const ChatMessage = forwardRef(
speaker,
text,
}: {
speaker: string
text: string
speaker: string;
text: string;
},
ref
) => {
@ -26,28 +27,32 @@ const ChatMessage = forwardRef(
exit={{ y: -24, opacity: 0 }}
className={cn(
"py-3 px-3 md:px-6 w-full dark:border-white/25 flex flex-col max-w-4xl overflow-hidden scroll-pt-32",
speaker === "user" ? "" : "bg-gray-200 dark:bg-gray-800 bg-opacity-60 py-8",
speaker === "user"
? ""
: "bg-gray-200 dark:bg-gray-800 bg-opacity-60 py-8"
)}
style={speaker === "user" ? { whiteSpace: "pre-line" } : {}} // Add this line to preserve line breaks
>
<span
className={cn(
"capitalize text-xs bg-sky-200 rounded-xl p-1 px-2 mb-2 w-fit dark:bg-sky-700"
)}>
)}
>
{speaker}
</span>
<>
<ReactMarkdown
// remarkRehypeOptions={{}}
className="prose dark:prose-invert ml-[6px] mt-1">
className="prose dark:prose-invert ml-[6px] mt-1"
>
{text}
</ReactMarkdown>
</>
</motion.div>
)
);
}
)
);
ChatMessage.displayName = "ChatMessage"
ChatMessage.displayName = "ChatMessage";
export default ChatMessage
export default ChatMessage;

View File

@ -0,0 +1,44 @@
"use client";
import Card from "@/app/components/ui/Card";
import { AnimatePresence } from "framer-motion";
import { useEffect, useRef } from "react";
import { Chat } from "../../types";
import ChatMessage from "./ChatMessage";
interface ChatMessagesProps {
chat: Chat;
}
export const ChatMessages = ({ chat }: ChatMessagesProps): JSX.Element => {
const lastChatRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
lastChatRef.current?.scrollIntoView({ behavior: "auto", block: "start" });
}, [chat]);
return (
<Card className="p-5 max-w-3xl w-full flex-1 flex flex-col mb-8">
<div className="">
{history.length === 0 ? (
<div className="text-center opacity-50">
Ask a question, or describe a task.
</div>
) : (
<AnimatePresence initial={false}>
{chat.history.map(([speaker, text], idx) => {
return (
<ChatMessage
ref={idx === history.length - 1 ? lastChatRef : null}
key={idx}
speaker={speaker}
text={text}
/>
);
})}
</AnimatePresence>
)}
</div>
</Card>
);
};
export default ChatMessages;

View File

@ -0,0 +1,61 @@
import { cn } from "@/lib/utils";
import { UUID } from "crypto";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { FC } from "react";
import { FiTrash2 } from "react-icons/fi";
import { MdChatBubbleOutline } from "react-icons/md";
import { Chat } from "../../types";
interface ChatsListItemProps {
chat: Chat;
deleteChat: (id: UUID) => void;
}
const ChatsListItem: FC<ChatsListItemProps> = ({ chat, deleteChat }) => {
const pathname = usePathname()?.split("/").at(-1);
const selected = chat.chatId === pathname;
return (
<div
className={cn(
"w-full border-b border-black/10 dark:border-white/25 last:border-none relative group flex overflow-x-hidden hover:bg-gray-100 dark:hover:bg-gray-800",
selected ? "bg-gray-100 text-primary" : ""
)}
>
<Link
className="flex flex-col flex-1 min-w-0 p-4"
href={`/chat/${chat.chatId}`}
key={chat.chatId}
>
<div className="flex items-center gap-2">
<MdChatBubbleOutline className="text-xl" />
<p className="min-w-0 flex-1 whitespace-nowrap">
{chat.history[chat.history.length - 1][1]}
</p>
</div>
<div className="grid-cols-2 text-xs opacity-50 whitespace-nowrap">
{chat.chatId}
</div>
</Link>
<div className="opacity-0 group-hover:opacity-100 flex items-center justify-center hover:text-red-700 bg-gradient-to-l from-white dark:from-black to-transparent z-10 transition-opacity">
<button
className="p-5"
type="button"
onClick={() => deleteChat(chat.chatId)}
>
<FiTrash2 />
</button>
</div>
{/* Fade to white */}
<div
aria-hidden
className="not-sr-only absolute left-1/2 top-0 bottom-0 right-0 bg-gradient-to-r from-transparent to-white dark:to-black pointer-events-none"
></div>
</div>
);
};
export default ChatsListItem;

View File

@ -0,0 +1,11 @@
import Link from "next/link";
import { BsPlusSquare } from "react-icons/bs";
export const NewChatButton = () => (
<Link
href="/chat"
className="px-4 py-2 mx-4 my-2 border border-primary hover:text-white hover:bg-primary shadow-lg rounded-lg flex items-center justify-center"
>
<BsPlusSquare className="h-6 w-6 mr-2" /> New Chat
</Link>
);

View File

@ -0,0 +1,21 @@
"use client";
import useChats from "../../hooks/useChats";
import ChatsListItem from "./ChatsListItem";
import { NewChatButton } from "./NewChatButton";
export function ChatsList() {
const { allChats, deleteChat } = useChats();
return (
<aside className="h-screen bg-white dark:bg-black max-w-xs w-full border-r border-black/10 dark:border-white/25 ">
<NewChatButton />
<div className="flex flex-col gap-0">
{allChats.map((chat) => (
<ChatsListItem
key={chat.chatId}
chat={chat}
deleteChat={deleteChat}
/>
))}
</div>
</aside>
);
}

View File

@ -1,4 +1,4 @@
export * from "./ChatInput";
export * from "./ChatMessage";
export * from "./ChatMessages";
export * from "./ChatMessages/ChatInput";
export * from "./ChatMessages/ChatMessage";
export * from "./ChatsList";

View File

@ -0,0 +1,164 @@
import { useBrainConfig } from "@/lib/context/BrainConfigProvider/hooks/useBrainConfig";
import { useToast } from "@/lib/hooks/useToast";
import { useAxios } from "@/lib/useAxios";
import { UUID } from "crypto";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { Chat, ChatMessage } from "../types";
export default function useChats(chatId?: UUID) {
const [allChats, setAllChats] = useState<Chat[]>([]);
const [chat, setChat] = useState<Chat | null>(null);
const [currentChatId, setCurrentChatId] = useState<UUID | undefined>(chatId);
const [isSendingMessage, setIsSendingMessage] = useState(false);
const [message, setMessage] = useState<ChatMessage>(["", ""]); // for optimistic updates
const { axiosInstance } = useAxios();
const {
config: { maxTokens, model, temperature },
} = useBrainConfig();
const router = useRouter();
const { publish } = useToast();
const fetchAllChats = useCallback(async () => {
try {
console.log("Fetching all chats");
const response = await axiosInstance.get<{
chats: Chat[];
}>(`/chat`);
setAllChats(response.data.chats);
console.log("Fetched all chats");
} catch (error) {
console.error(error);
publish({
variant: "danger",
text: "Error occured while fetching your chats",
});
}
}, []);
const fetchChat = useCallback(async (chatId?: UUID) => {
console.log(currentChatId, chatId);
if (!currentChatId && !chatId) throw new Error("No ID provided");
setCurrentChatId(chatId);
try {
console.log(`Fetching chat ${chatId ?? currentChatId}`);
const response = await axiosInstance.get<Chat>(
`/chat/${chatId ?? currentChatId}`
);
setChat(response.data);
} catch (error) {
console.error(error);
publish({
variant: "danger",
text: `Error occured while fetching ${chatId}`,
});
}
}, []);
const sendMessage = async (chatId?: UUID, msg?: ChatMessage) => {
setIsSendingMessage(true);
// const chat_id = {
// ...((chatId || currentChatId) && {
// chat_id: chatId ?? currentChatId,
// }),
// };
if (msg) setMessage(msg);
const options = {
// ...(chat_id && { chat_id }),
// chat_id gets set only if either chatId or currentChatId exists, by the priority of chatId
chat_id: chatId
? chatId[0]
: currentChatId
? currentChatId[0]
: undefined,
model,
question: msg ? msg[1] : message[1],
history: chat ? chat.history : [],
temperature,
max_tokens: maxTokens,
use_summarization: false,
};
console.log({ options });
const response = await axiosInstance.post<
// response.data.chatId can be undefined when the max number of requests has reached
Omit<Chat, "chatId"> & { chatId: UUID | undefined }
>(`/chat`, options);
// response.data.chatId can be undefined when the max number of requests has reached
if (!response.data.chatId) {
publish({
text: "You have reached max number of requests.",
variant: "danger",
});
setMessage(["", ""]);
setIsSendingMessage(false);
return;
}
const newChat = {
chatId: response.data.chatId,
history: response.data.history,
};
if (!chatId) {
// Creating a new chat
// setAllChats((chats) => {
// console.log({ chats });
// return [...chats, newChat];
// });
fetchAllChats();
setCurrentChatId(response.data.chatId);
setChat(newChat);
router.push(`/chat/${response.data.chatId}`);
}
setChat(newChat);
setMessage(["", ""]);
setIsSendingMessage(false);
};
const deleteChat = async (chatId: UUID) => {
try {
await axiosInstance.delete(`/chat/${chatId}`);
setAllChats((chats) => chats.filter((chat) => chat.chatId !== chatId));
// TODO: Change route only when the current chat is being deleted
console.log({ chatIdsaldkfj: chat?.chatId, currentChatId, chatId });
router.push("/chat");
publish({
text: `Chat sucessfully deleted. Id: ${chatId}`,
variant: "success",
});
} catch (error) {
console.error("Error deleting chat:", error);
publish({ text: `Error deleting chat: ${error}`, variant: "danger" });
}
};
useEffect(() => {
fetchAllChats();
console.log(chatId);
if (chatId) {
setCurrentChatId(chatId);
fetchChat(chatId);
}
}, [fetchAllChats, fetchChat, chatId]);
return {
allChats,
chat,
currentChatId,
isSendingMessage,
message,
setMessage,
fetchAllChats,
fetchChat,
deleteChat,
sendMessage,
};
}

View File

@ -0,0 +1,25 @@
"use client";
import { redirect } from "next/navigation";
import { FC, ReactNode } from "react";
import { useSupabase } from "../supabase-provider";
import { ChatsList } from "./components";
interface LayoutProps {
children?: ReactNode;
}
const Layout: FC<LayoutProps> = ({ children }) => {
const { session } = useSupabase();
if (!session) redirect("/login");
return (
<div className="relative h-full w-full flex pt-20">
<div className="h-full">
<ChatsList />
</div>
{children}
</div>
);
};
export default Layout;

View File

@ -3,5 +3,8 @@ import { UUID } from "crypto";
export interface Chat {
chatId: UUID;
chatName: string;
history: Array<[string, string]>;
history: ChatHistory;
}
export type ChatMessage = [string, string];
export type ChatHistory = ChatMessage[];

View File

@ -55,7 +55,7 @@ export const NavItems: FC<NavItemsProps> = ({
{isUserLoggedIn && (
<>
<Link aria-label="account" className="" href={"/user"}>
<MdPerson />
<MdPerson className="text-2xl" />
</Link>
<Link href={"/config"}>
<Button