mirror of
https://github.com/deiucanta/chatpad.git
synced 2026-03-11 09:04:31 +00:00
feat: group chats by date and fixes
This commit is contained in:
parent
d07b23cdbf
commit
cfc23d1909
17 changed files with 178 additions and 90 deletions
26
package-lock.json
generated
26
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export function App() {
|
|||
withCSSVariables
|
||||
theme={{
|
||||
colorScheme,
|
||||
primaryColor: "teal",
|
||||
primaryColor: "orange",
|
||||
defaultRadius: "md",
|
||||
globalStyles: (theme) => ({
|
||||
body: {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export function ChatHeader() {
|
|||
|
||||
notifications.show({
|
||||
title: "Saved",
|
||||
color: "green",
|
||||
message: "Chat name updated.",
|
||||
});
|
||||
}
|
||||
|
|
|
|||
56
src/components/ChatItem.tsx
Normal file
56
src/components/ChatItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ export function CreatePromptModal({ content, title: titleProp, open: openProp }:
|
|||
|
||||
notifications.show({
|
||||
title: "Saved",
|
||||
color: "green",
|
||||
message: "Prompt created",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export function DeleteChatModal({
|
|||
|
||||
notifications.show({
|
||||
title: "Deleted",
|
||||
color: "green",
|
||||
message: "Chat deleted.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export function DeletePromptModal({ prompt }: { prompt: Prompt }) {
|
|||
|
||||
notifications.show({
|
||||
title: "Deleted",
|
||||
color: "green",
|
||||
message: "Prompt deleted.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@ export function EditChatModal({
|
|||
|
||||
notifications.show({
|
||||
title: "Saved",
|
||||
message: "",
|
||||
color: "green",
|
||||
message: "Chat updated.",
|
||||
});
|
||||
close();
|
||||
} catch (error: any) {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export function EditPromptModal({ prompt }: { prompt: Prompt }) {
|
|||
|
||||
notifications.show({
|
||||
title: "Saved",
|
||||
color: "green",
|
||||
message: "Prompt updated",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export function MessageItem({ message, onDeleted }: { message: Message, onDelete
|
|||
|
||||
notifications.show({
|
||||
title: "Deleted",
|
||||
color: "green",
|
||||
message: "Message deleted.",
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue