feat: group chats by date and fixes

This commit is contained in:
BetaHuhn 2023-08-31 12:29:00 +02:00
parent d07b23cdbf
commit cfc23d1909
17 changed files with 178 additions and 90 deletions

26
package-lock.json generated
View file

@ -43,6 +43,7 @@
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^4.0.4",
"buffer": "^5.7.1",
"date-fns": "^2.30.0",
"deta": "^2.0.0-rc.1",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
@ -2545,6 +2546,22 @@
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -9245,6 +9262,15 @@
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
},
"date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dev": true,
"requires": {
"@babel/runtime": "^7.21.0"
}
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",

View file

@ -15,6 +15,7 @@
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^4.0.4",
"buffer": "^5.7.1",
"date-fns": "^2.30.0",
"deta": "^2.0.0-rc.1",
"path-browserify": "^1.0.1",
"process": "^0.11.10",

View file

@ -49,7 +49,7 @@ export function App() {
withCSSVariables
theme={{
colorScheme,
primaryColor: "teal",
primaryColor: "orange",
defaultRadius: "md",
globalStyles: (theme) => ({
body: {

View file

@ -33,6 +33,7 @@ export function ChatHeader() {
notifications.show({
title: "Saved",
color: "green",
message: "Chat name updated.",
});
}

View file

@ -0,0 +1,56 @@
import { ActionIcon, Flex, Menu, useMantineTheme } from "@mantine/core";
import { IconDotsVertical, IconMessages } from "@tabler/icons-react";
import { Link } from "@tanstack/react-location";
import { DeleteChatModal } from "./DeleteChatModal";
import { EditChatModal } from "./EditChatModal";
import { MainLink } from "./MainLink";
import { useHover } from "@mantine/hooks";
import { Chat } from "../db";
export function ChatItem({ chat, active = false }: { chat: Chat, active?: boolean }) {
const { hovered, ref } = useHover();
const theme = useMantineTheme();
return (
<Flex
ref={ref}
key={chat.key}
className={active ? "active" : undefined}
sx={(theme) => ({
marginTop: 1,
"&:hover, &.active": {
backgroundColor:
theme.colorScheme === "dark"
? theme.colors.dark[6]
: theme.colors.gray[1],
},
})}
>
<Link to={`/chats/${chat.key}`} style={{ flex: 1 }}>
<MainLink
icon={<IconMessages size="1rem" />}
color={theme.primaryColor}
label={chat.description}
/>
</Link>
<Menu shadow="md" width={200} keepMounted>
{hovered && (
<Menu.Target>
<ActionIcon sx={{ height: "auto", display: hovered ? 'block' : 'none' }}>
<IconDotsVertical size={20} />
</ActionIcon>
</Menu.Target>
)}
<Menu.Dropdown>
<EditChatModal chat={chat}>
<Menu.Item>Edit</Menu.Item>
</EditChatModal>
<DeleteChatModal chat={chat}>
<Menu.Item>Delete</Menu.Item>
</DeleteChatModal>
</Menu.Dropdown>
</Menu>
</Flex>
);
}

View file

@ -1,85 +1,80 @@
import { ActionIcon, Flex, Menu, useMantineTheme } from "@mantine/core";
import { IconDotsVertical, IconMessages } from "@tabler/icons-react";
import { Link } from "@tanstack/react-location";
import { useMemo } from "react";
import { Text } from '@mantine/core';
import { format, isToday, isYesterday, isThisWeek, subWeeks, isSameYear, isSameWeek } from 'date-fns';
import { useChatId } from "../hooks/useChatId";
import { DeleteChatModal } from "./DeleteChatModal";
import { EditChatModal } from "./EditChatModal";
import { MainLink } from "./MainLink";
import { useChats } from "../hooks/contexts";
import { ChatItem } from "./ChatItem";
export function Chats({ search }: { search: string }) {
const chatId = useChatId();
const theme = useMantineTheme();
const { chats } = useChats()
// const [chats, setChats] = useState<Chat[]>();
// useEffect(() => {
// // fetch data
// const dataFetch = async () => {
// const { items } = await detaDB.chats.fetch();
// setChats(items as unknown as Chat[]);
// };
// dataFetch();
// }, []);
// const chats = useLiveQuery(() =>
// db.chats.orderBy("createdAt").reverse().toArray()
// );
const filteredChats = useMemo(
() =>
(chats ?? []).filter((chat) => {
if (!search) return true;
return chat.description.toLowerCase().includes(search);
}),
(chats ?? [])
.filter((chat) => {
if (!search) return true;
return chat.description.toLowerCase().includes(search);
})
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
[chats, search]
);
const groupedChats = useMemo(() => filteredChats.reduce((acc, chat) => {
const chatDate = new Date(chat.createdAt);
if (isToday(chatDate)) {
if (!acc['Today']) {
acc['Today'] = [chat];
} else {
acc['Today'].push(chat);
}
} else if (isYesterday(chatDate)) {
if (!acc['Yesterday']) {
acc['Yesterday'] = [chat];
} else {
acc['Yesterday'].push(chat);
}
} else if (isThisWeek(chatDate, { weekStartsOn: 1 })) {
const chatWeekday = format(chatDate, 'EEEE');
if (!acc[chatWeekday]) {
acc[chatWeekday] = [chat];
} else {
acc[chatWeekday].push(chat);
}
} else if (isSameWeek(chatDate, subWeeks(new Date(), 1), { weekStartsOn: 1 })) {
if (!acc['Last Week']) {
acc['Last Week'] = [chat];
} else {
acc['Last Week'].push(chat);
}
} else if (isSameYear(chatDate, new Date())) {
const chatMonth = format(chatDate, 'MMMM');
if (!acc[chatMonth]) {
acc[chatMonth] = [chat];
} else {
acc[chatMonth].push(chat);
}
} else {
const chatYear = ` ${format(chatDate, 'yyyy')}`; // space to avoid js object key sorting
if (!acc[chatYear]) {
acc[chatYear] = [chat];
} else {
acc[chatYear].push(chat);
}
}
return acc;
}, {} as Record<string, typeof filteredChats>), [filteredChats]);
return (
<>
{filteredChats.map((chat) => (
<Flex
key={chat.key}
className={chatId === chat.key ? "active" : undefined}
sx={(theme) => ({
marginTop: 1,
"&:hover, &.active": {
backgroundColor:
theme.colorScheme === "dark"
? theme.colors.dark[6]
: theme.colors.gray[1],
},
})}
>
<Link to={`/chats/${chat.key}`} style={{ flex: 1 }}>
<MainLink
icon={<IconMessages size="1rem" />}
color={theme.primaryColor}
label={chat.description}
/>
</Link>
<Menu shadow="md" width={200} keepMounted>
<Menu.Target>
<ActionIcon sx={{ height: "auto" }}>
<IconDotsVertical size={20} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<EditChatModal chat={chat}>
<Menu.Item>Edit</Menu.Item>
</EditChatModal>
<DeleteChatModal chat={chat}>
<Menu.Item>Delete</Menu.Item>
</DeleteChatModal>
</Menu.Dropdown>
</Menu>
</Flex>
{Object.entries(groupedChats).map(([date, chats]) => (
<div key={date}>
<Text fz="sm" py="sm" px="xs" color="dimmed">{date}</Text>
{chats.map((chat) => (
<ChatItem key={chat.key} chat={chat} active={chatId === chat.key} />
))}
</div>
))}
</>
);

View file

@ -77,6 +77,7 @@ export function CreatePromptModal({ content, title: titleProp, open: openProp }:
notifications.show({
title: "Saved",
color: "green",
message: "Prompt created",
});

View file

@ -58,6 +58,7 @@ export function DeleteChatModal({
notifications.show({
title: "Deleted",
color: "green",
message: "Chat deleted.",
});
} catch (error: any) {

View file

@ -36,6 +36,7 @@ export function DeletePromptModal({ prompt }: { prompt: Prompt }) {
notifications.show({
title: "Deleted",
color: "green",
message: "Prompt deleted.",
});
} catch (error: any) {

View file

@ -110,7 +110,8 @@ export function EditChatModal({
notifications.show({
title: "Saved",
message: "",
color: "green",
message: "Chat updated.",
});
close();
} catch (error: any) {

View file

@ -70,6 +70,7 @@ export function EditPromptModal({ prompt }: { prompt: Prompt }) {
notifications.show({
title: "Saved",
color: "green",
message: "Prompt updated",
});

View file

@ -14,7 +14,7 @@ export function LogoText(props: JSX.IntrinsicElements["svg"]) {
);
}
export function Logo(props: JSX.IntrinsicElements["svg"]) {
export function Logo(props: JSX.IntrinsicElements["svg"] & { color1: string, color2: string }) {
return (
<svg
fill="none"
@ -41,8 +41,8 @@ export function Logo(props: JSX.IntrinsicElements["svg"]) {
y2={116.46}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41E094" />
<stop offset={1} stopColor="#27B882" />
<stop stopColor={props.color1} />
<stop offset={1} stopColor={props.color2} />
</linearGradient>
</defs>
</svg>

View file

@ -46,6 +46,7 @@ export function MessageItem({ message, onDeleted }: { message: Message, onDelete
notifications.show({
title: "Deleted",
color: "green",
message: "Message deleted.",
});
}

View file

@ -89,6 +89,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
// });
notifications.show({
title: "Saved",
color: "green",
message: "Your OpenAI Key has been saved.",
});
} catch (error: any) {
@ -159,6 +160,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
notifications.show({
title: "Saved",
color: "green",
message: "Your OpenAI Type has been saved.",
});
} catch (error: any) {
@ -198,6 +200,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
notifications.show({
title: "Saved",
color: "green",
message: "Your OpenAI Model has been saved.",
});
} catch (error: any) {
@ -241,6 +244,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
notifications.show({
title: "Saved",
color: "green",
message: "Your OpenAI Auth has been saved.",
});
} catch (error: any) {
@ -280,6 +284,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
notifications.show({
title: "Saved",
color: "green",
message: "Your OpenAI Base has been saved.",
});
} catch (error: any) {
@ -331,6 +336,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
notifications.show({
title: "Saved",
color: "green",
message: "Your OpenAI Version has been saved.",
});
} catch (error: any) {

View file

@ -14,7 +14,7 @@ import { notifications } from "@mantine/notifications";
import { KeyboardEvent, useState, type ChangeEvent, useEffect } from "react";
import { AiOutlineSend } from "react-icons/ai";
import { MessageItem } from "../components/MessageItem";
import { Chat, Message, Prompt, detaDB, generateKey } from "../db";
import { Message, Prompt, detaDB, generateKey } from "../db";
import { useChatId } from "../hooks/useChatId";
import {
createChatCompletion,
@ -28,7 +28,6 @@ export function ChatRoute() {
const chatId = useChatId();
const { settings } = useSettings()
const { prompts } = usePrompts()
const [messages, setMessages] = useState<Message[]>([]);
@ -63,21 +62,10 @@ export function ChatRoute() {
const { chat, setChat } = useChat()
useEffect(() => {
const dataFetch = async () => {
const item = await detaDB.chats.get(chatId!);
const fetchedChat = item as unknown as Chat
setChat(fetchedChat);
if (fetchedChat.prompt) {
setPromptKey(fetchedChat.prompt)
}
};
if (!chat) {
dataFetch();
if (chat?.prompt) {
setPromptKey(chat.prompt)
}
}, [chatId]);
}, [chat]);
// const chat = useLiveQuery(async () => {
// if (!chatId) return null;

View file

@ -7,6 +7,7 @@ import {
SimpleGrid,
Text,
ThemeIcon,
useMantineTheme,
} from "@mantine/core";
import {
IconCloudDownload,
@ -22,6 +23,7 @@ import { useSettings } from "../hooks/contexts";
export function IndexRoute() {
const { settings } = useSettings()
const theme = useMantineTheme()
return (
<>
@ -29,7 +31,7 @@ export function IndexRoute() {
<Container size="sm">
<Badge mb="lg">GPT-4 Ready</Badge>
<Text>
<Logo style={{ maxWidth: 240 }} />
<Logo style={{ maxWidth: 240 }} color1={theme.colors[theme.primaryColor][3]} color2={theme.colors[theme.primaryColor][7]} />
</Text>
<Text mt={4} size="xl">
Not just another ChatGPT user-interface!

View file

@ -174,7 +174,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7":
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7":
"integrity" "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw=="
"resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz"
"version" "7.21.0"
@ -1188,6 +1188,13 @@
"resolved" "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz"
"version" "1.0.8"
"date-fns@^2.30.0":
"integrity" "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="
"resolved" "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz"
"version" "2.30.0"
dependencies:
"@babel/runtime" "^7.21.0"
"debug@^3.2.7":
"integrity" "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="
"resolved" "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"