Skip to content

Commit

Permalink
Merge pull request #42 from it-at-m/1-chat-history
Browse files Browse the repository at this point in the history
Added Chat History
  • Loading branch information
pilitz authored Jul 31, 2024
2 parents 2dd953f + 4eabc0c commit 22da1dd
Show file tree
Hide file tree
Showing 14 changed files with 677 additions and 170 deletions.
1 change: 0 additions & 1 deletion app/frontend/src/components/Answer/Answer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export const Answer = ({
}: Props) => {
const parsedAnswer = useMemo(() => parseAnswerToHtml(answer.answer), [answer]);


const { t } = useTranslation();

const [copied, setCopied] = useState<boolean>(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import { useCallback, useState } from "react";
import { useTranslation } from 'react-i18next';
interface Props {
temperature: number;
setTemperature: (temp: number) => void;
setTemperature: (temp: number, id: number) => void;
max_tokens: number;
setMaxTokens: (maxTokens: number) => void;
setMaxTokens: (maxTokens: number, id: number) => void;
systemPrompt: string;
setSystemPrompt: (systemPrompt: string) => void;
setSystemPrompt: (systemPrompt: string, id: number) => void;
current_id: number;
}

export const ChatsettingsDrawer = ({ temperature, setTemperature, max_tokens, setMaxTokens, systemPrompt, setSystemPrompt }: Props) => {
export const ChatsettingsDrawer = ({ temperature, setTemperature, max_tokens, setMaxTokens, systemPrompt, setSystemPrompt, current_id }: Props) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const { t, i18n } = useTranslation();
const onClickRightButton = useCallback(() => {
Expand All @@ -49,19 +50,19 @@ export const ChatsettingsDrawer = ({ temperature, setTemperature, max_tokens, se
const isEmptySystemPrompt = systemPrompt.trim() === "";

const onTemperatureChange: SliderProps["onChange"] = (_, data) =>
setTemperature(data.value);
setTemperature(data.value, current_id);
const onMaxtokensChange: SliderProps["onChange"] = (_, data) =>
setMaxTokens(data.value);
setMaxTokens(data.value, current_id);

const onSytemPromptChange = (_ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: TextareaOnChangeData) => {
if (newValue?.value)
setSystemPrompt(newValue.value);
setSystemPrompt(newValue.value, current_id);
else
setSystemPrompt("");
setSystemPrompt("", current_id);
}

const onClearSystemPrompt = () => {
setSystemPrompt("");
setSystemPrompt("", current_id);
}
return (
<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Delete24Regular } from "@fluentui/react-icons";
import { ChatAdd24Regular } from "@fluentui/react-icons";
import { Button, Tooltip } from "@fluentui/react-components";

import styles from "./ClearChatButton.module.css";
Expand All @@ -14,7 +14,7 @@ export const ClearChatButton = ({ className, disabled, onClick }: Props) => {
return (
<div className={`${styles.container} ${className ?? ""}`}>
<Tooltip content={t('common.clear_chat')} relationship="description" positioning="below">
<Button appearance="secondary" aria-label={t('common.clear_chat')} icon={<Delete24Regular className={styles.iconRightMargin} />} disabled={disabled} onClick={onClick} size="large">
<Button appearance="secondary" aria-label={t('common.clear_chat')} icon={<ChatAdd24Regular className={styles.iconRightMargin} />} disabled={disabled} onClick={onClick} size="large">
</Button>
</Tooltip>
</div >
Expand Down
8 changes: 8 additions & 0 deletions app/frontend/src/components/History/History.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.button {
margin-left: 20px;
}

.savedChatButton {
width: 80%;
align-items: center;
}
184 changes: 184 additions & 0 deletions app/frontend/src/components/History/History.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@

import { Button, Drawer, DrawerBody, DrawerHeader, DrawerHeaderTitle, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from '@fluentui/react-components';
import { Options24Regular, Dismiss24Regular, History24Regular } from '@fluentui/react-icons';
import { MutableRefObject, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { deleteChatFromDB, indexedDBStorage, onError, onUpgrade, renameChat, changeFavouritesInDb, CURRENT_CHAT_IN_DB } from '../../service/storage';
import { AskResponse } from '../../api/models';
import styles from "./History.module.css";
import { MessageError } from '../../pages/chat/MessageError';

interface Props {
storage: indexedDBStorage
setAnswers: (answers: [user: string, response: AskResponse, user_tokens: number][]) => void
lastQuestionRef: MutableRefObject<string>
currentId: number
setCurrentId: (id: number) => void
onTemperatureChanged: (temp: number, id: number) => void
onMaxTokensChanged: (tokens: number, id: number) => void
onSystemPromptChanged: (prompt: string, id: number) => void
setError: (error: Error | undefined) => void
}
export const History = ({ storage, setAnswers, lastQuestionRef, currentId, setCurrentId, onTemperatureChanged, onMaxTokensChanged, onSystemPromptChanged, setError }: Props) => {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [chatButtons, setChatButtons] = useState<JSX.Element[]>([]);

const loadChat = (stored: any) => {
setError(undefined);
let storedAnswers = stored.Data.Answers;
lastQuestionRef.current = storedAnswers[storedAnswers.length - 1][0];
if (storedAnswers[storedAnswers.length - 1][1].answer == "") {
storedAnswers.pop()
setError(new MessageError(t('components.history.error')))
}
setAnswers(storedAnswers);
let id = stored.id;
setIsOpen(false);
setCurrentId(id);
onTemperatureChanged(stored.Options.temperature, id);
onMaxTokensChanged(stored.Options.maxTokens, id);
onSystemPromptChanged(stored.Options.system, id);
let openRequest = indexedDB.open(storage.db_name, storage.db_version);
openRequest.onupgradeneeded = () => onUpgrade(openRequest, storage);
openRequest.onerror = () => onError(openRequest)
openRequest.onsuccess = async function () {
stored["refID"] = id
stored["id"] = CURRENT_CHAT_IN_DB
let putRequest = openRequest.result.transaction(storage.objectStore_name, "readwrite").objectStore(storage.objectStore_name).put(stored);
putRequest.onerror = () => onError(putRequest)
}
}
const getCategory = (lastEdited: string, fav: boolean) => {
if (fav) return t('components.history.favourites');
const today = new Date();
const lastEditedDate = new Date(lastEdited);
today.setHours(0, 0, 0, 0);
lastEditedDate.setHours(0, 0, 0, 0);
const diffTime = Math.abs(today.getTime() - lastEditedDate.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) return t('components.history.today');
if (diffDays === 1) return t('components.history.yesterday');
if (diffDays <= 7) return t('components.history.sevendays');
return t('components.history.older');
};
const changeChatName = (chat: any) => {
const newName = prompt(t('components.history.newchat'), chat.Data.Name);
if (newName && newName.trim() !== "") {
renameChat(storage, newName.trim(), chat)
getAllChats();
}
};

const deleteChat = (storage: indexedDBStorage, id: number, setAnswers: (answers: []) => void, isCurrent: boolean, lastQuestionRef: MutableRefObject<string>) => {
deleteChatFromDB(storage, id, setAnswers, id === currentId, lastQuestionRef);
getAllChats();
}

const changeFavourites = (fav: boolean, id: number) => {
changeFavouritesInDb(fav, id, storage)
getAllChats()
}

const getAllChats = async () => {
let openRequest = indexedDB.open(storage.db_name, storage.db_version);
openRequest.onupgradeneeded = () => onUpgrade(openRequest, storage);
openRequest.onerror = () => onError(openRequest)
openRequest.onsuccess = async function () {
let stored = openRequest.result.transaction(storage.objectStore_name, "readonly").objectStore(storage.objectStore_name).getAll();
stored.onsuccess = function () {
const categorizedChats = stored.result
.filter((chat: any) => chat.id !== 0)
.sort((a: any, b: any) => { return new Date(b.Data.LastEdited).getTime() - new Date(a.Data.LastEdited).getTime(); })
.reduce((acc: any, chat: any) => {
const category = getCategory(chat.Data.LastEdited, chat.Options.favourites);
if (!acc[category]) acc[category] = [];
acc[category].push(chat);
return acc;
}, {});
const sortedChats = Object.entries(categorizedChats).sort(([categoryA], [categoryB]) => {
if (categoryA === t('components.history.favourites')) return -1;
if (categoryB === t('components.history.favourites')) return 1;
return 0;
});
const newChatButtons = sortedChats.map(([category, chats]: [string, any]) => (
<div key={category} >
<h2>{category}</h2>
{chats.map((chat: any, index: number) => (
<div key={index}>
<Tooltip content={t('components.history.lastEdited') + new Date(chat.Data.LastEdited).toString()} relationship="description" positioning="below">
<Button className={styles.savedChatButton} onClick={() => loadChat(chat)} size="large" >
{chat.Data.Name}
</Button>
</Tooltip>
<Menu >
<MenuTrigger disableButtonEnhancement>
<Tooltip content={t('components.history.options')} relationship="description" positioning="below">
<Button icon={<Options24Regular />} appearance="secondary" size="large" />
</Tooltip>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem onClick={() => deleteChat(storage, chat.id, setAnswers, chat.id === currentId, lastQuestionRef)}>{t('components.history.delete')}</MenuItem>
<MenuItem onClick={() => changeChatName(chat)}>{t('components.history.rename')}</MenuItem>
{chat.Options.favourites ? (
<MenuItem onClick={() => changeFavourites(false, chat.id)}>{t('components.history.unsave')}</MenuItem>
) : (
<MenuItem onClick={() => changeFavourites(true, chat.id)}>{t('components.history.save')}</MenuItem>
)}

</MenuList>
</MenuPopover>
</Menu>
{index != chats.length - 1 && <hr />}
</div>
))
}
</div >
));
setChatButtons(newChatButtons);
};
stored.onerror = () => onError(stored)
};
}
const open = () => {
getAllChats();
setIsOpen(true)
}

return (
<div>
<Drawer
type={"overlay"}
separator
open={isOpen}
onOpenChange={(_, { open }) => setIsOpen(open)}
>
<DrawerHeader>
<DrawerHeaderTitle
action={
<Button
appearance="subtle"
aria-label={t('components.history.close')}
icon={<Dismiss24Regular />}
onClick={() => setIsOpen(false)}
/>
}
>
{t('components.history.history')}:
</DrawerHeaderTitle>
</DrawerHeader>

<DrawerBody>
{chatButtons}
</DrawerBody>
</Drawer>
<div className={styles.button}>
<Tooltip content={t('components.history.button')} relationship="description" positioning="below">
<Button aria-label={t('components.history.button')} icon={<History24Regular />} appearance="secondary" onClick={() => open()} size="large">
</Button>
</Tooltip>
</div>
</div>
)
}
11 changes: 6 additions & 5 deletions app/frontend/src/components/UserChatMessage/RollbackMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button, Tooltip } from "@fluentui/react-components"
import { useTranslation } from "react-i18next";
import { deleteFromDB, indexedDBStorage, popLastInDB } from "../../service/storage";
import { deleteChatFromDB, indexedDBStorage, popLastMessageInDB } from "../../service/storage";
import { DeleteArrowBackRegular } from "@fluentui/react-icons";

import styles from "./UserChatMessage.module.css"
Expand All @@ -14,23 +14,24 @@ interface Props {
setAnswers: (answers: any[]) => void;
storage: indexedDBStorage;
lastQuestionRef: MutableRefObject<string>;
current_id: number;
}

export const RollBackMessage = ({ message, setQuestion, answers, setAnswers, storage, lastQuestionRef }: Props) => {
export const RollBackMessage = ({ message, setQuestion, answers, setAnswers, storage, lastQuestionRef, current_id }: Props) => {
const { t } = useTranslation();
const deleteMessageAndRollbackChat = () => {
let last;
while (answers.length) {
popLastInDB(storage);
popLastMessageInDB(storage, current_id);
last = answers.pop();
setAnswers(answers);
if (last && last[0] == message) {
break;
}
}
if (answers.length == 0) {
lastQuestionRef.current = ""
deleteFromDB(storage)
deleteChatFromDB(storage, current_id, setAnswers, true, lastQuestionRef)
deleteChatFromDB(storage, 0, setAnswers, false, lastQuestionRef)
} else {
lastQuestionRef.current = last[1]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ interface Props {
setAnswers: (answers: any[]) => void;
storage: indexedDBStorage;
lastQuestionRef: MutableRefObject<string>;
current_id: number;
}

export const UserChatMessage = ({ message, token, setQuestion, answers, setAnswers, storage, lastQuestionRef }: Props) => {
export const UserChatMessage = ({ message, token, setQuestion, answers, setAnswers, storage, lastQuestionRef, current_id }: Props) => {


return (
Expand All @@ -34,6 +35,7 @@ export const UserChatMessage = ({ message, token, setQuestion, answers, setAnswe
setAnswers={setAnswers}
storage={storage}
lastQuestionRef={lastQuestionRef}
current_id={current_id}
/>
</Stack>
<Markdown
Expand Down
Loading

0 comments on commit 22da1dd

Please sign in to comment.