Skip to content
This repository has been archived by the owner on Jun 2, 2024. It is now read-only.

[web] Group conversations by relative date #22

Merged
merged 2 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 2 additions & 23 deletions packages/web/src/components/AppNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
Group,
MediaQuery,
Navbar,
ScrollArea,
Stack,
Text,
createStyles,
Expand All @@ -24,18 +23,13 @@ import {
import TippedActionIcon from "./TippedActionIcon";
import { openModal } from "@mantine/modals";
import SettingsForm from "./SettingsForm";
import NavbarConversation from "./NavbarConversation";
import React from "react";
import Usage from "./Usage";
import { useMediaQuery } from "@mantine/hooks";
import { BsDiscord, BsGithub } from "react-icons/bs";
import NavbarConversations from "./NavbarConversations";

const useStyles = createStyles(() => ({
scrollArea: {
"& > div": {
display: "block !important",
},
},
burger: {
position: "absolute",
top: 0,
Expand Down Expand Up @@ -165,22 +159,7 @@ export default () => {
<Divider my="xs" />
</Navbar.Section>
<Navbar.Section grow h={0}>
<ScrollArea
h="100%"
classNames={{
viewport: classes.scrollArea,
}}
>
<Stack spacing="xs">
{conversations.map((conversation) => (
<NavbarConversation
key={conversation.id}
conversation={conversation}
onClick={closeNavbar}
/>
))}
</Stack>
</ScrollArea>
<NavbarConversations onConversationSelect={closeNavbar} />
</Navbar.Section>
{activeConversation && showUsage && (
<Navbar.Section>
Expand Down
184 changes: 184 additions & 0 deletions packages/web/src/components/NavbarConversations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {
Divider,
Group,
ScrollArea,
Stack,
Text,
createStyles,
} from "@mantine/core";
import NavbarConversation from "./NavbarConversation";
import useConversationManager from "../hooks/useConversationManager";
import React from "react";
import TippedActionIcon from "./TippedActionIcon";
import { BiTrash } from "react-icons/bi";
import { useTimeout } from "@mantine/hooks";

interface NavbarConversationsProps {
onConversationSelect?: () => void;
}

const useStyles = createStyles(() => ({
scrollArea: {
"& > div": {
display: "block !important",
},
},
}));

const getRelativeDate = (target: number) => {
const currentDate = new Date();
const targetDate = new Date(target);

// Get the start of today
const todayStart = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
currentDate.getDate()
);

// Get the start of yesterday
const yesterdayStart = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
currentDate.getDate() - 1
);

// Get the start of this week
const thisWeekStart = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
currentDate.getDate() - currentDate.getDay()
);

// Get the start of this month
const thisMonthStart = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
1
);

if (targetDate >= todayStart) {
return "Today";
} else if (targetDate >= yesterdayStart) {
return "Yesterday";
} else if (targetDate >= thisWeekStart) {
return "This Week";
} else if (targetDate >= thisMonthStart) {
return "This Month";
} else {
return "Older";
}
};

export default ({
onConversationSelect = () => {},
}: NavbarConversationsProps) => {
const { classes } = useStyles();
const {
conversations: allConversations,
getConversationLastEdit,
removeConversation,
} = useConversationManager();
const [deleteConfirmation, setdeleteConfirmation] = React.useState<
string | null
>(null);
const { start: startUnconfirmDelete, clear: clearUnconfirmDelete } =
useTimeout(() => setdeleteConfirmation(null), 3000);

const conversationGroups = React.useMemo(() => {
return allConversations
.map((c) => ({
conversation: c,
lastEdit: getConversationLastEdit(c.id),
}))
.sort((a, b) => b.lastEdit - a.lastEdit)
.reduce((acc, { conversation, lastEdit }) => {
const relativeDate = getRelativeDate(lastEdit);
if (!acc[relativeDate]) {
acc[relativeDate] = [];
}
acc[relativeDate].push(conversation);
return acc;
}, {} as Record<string, typeof allConversations>);
}, [allConversations, getConversationLastEdit]);

const makeDeleteGroup = React.useCallback(
(group: string) => () => {
if (deleteConfirmation === group) {
conversationGroups[group].forEach((c) =>
removeConversation(c.id)
);
} else {
clearUnconfirmDelete();
setdeleteConfirmation(group);
startUnconfirmDelete();
}
},
[
clearUnconfirmDelete,
conversationGroups,
deleteConfirmation,
removeConversation,
startUnconfirmDelete,
]
);

return (
<ScrollArea
h="100%"
classNames={{
viewport: classes.scrollArea,
}}
>
<Stack spacing="xs">
{Object.entries(conversationGroups).map(
([relativeDate, conversations]) => (
<Stack key={relativeDate} spacing="xs">
<Group>
<Divider
sx={{ flexGrow: 1 }}
label={
<Text
size="xs"
weight={700}
opacity={0.7}
>
{relativeDate}
</Text>
}
/>
<TippedActionIcon
onClick={makeDeleteGroup(relativeDate)}
variant={
deleteConfirmation === relativeDate
? "filled"
: undefined
}
color={
deleteConfirmation === relativeDate
? "red"
: undefined
}
tip={
relativeDate === "Older"
? "Delete all older conversations"
: `Delete all conversations from ${relativeDate.toLowerCase()}`
}
>
<BiTrash />
</TippedActionIcon>
</Group>
{conversations.map((conversation) => (
<NavbarConversation
key={conversation.id}
conversation={conversation}
onClick={onConversationSelect}
/>
))}
</Stack>
)
)}
</Stack>
</ScrollArea>
);
};
8 changes: 6 additions & 2 deletions packages/web/src/components/Prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ import SavedPromptsModalBody from "./SavedPromptsModalBody";
import usePersistence from "../hooks/usePersistence";

export default () => {
const { activeConversation: conversation, showUsage } =
useConversationManager();
const {
activeConversation: conversation,
showUsage,
setConversationLastEdit,
} = useConversationManager();
const form = useForm({
initialValues: {
prompt: "",
Expand Down Expand Up @@ -71,6 +74,7 @@ export default () => {
form.reset();
try {
const message = await conversation.prompt(values.prompt);
setConversationLastEdit(conversation.id);
if (message) {
message.onMessageStreamingUpdate((isStreaming) => {
setIsStreaming(isStreaming);
Expand Down
4 changes: 4 additions & 0 deletions packages/web/src/contexts/ConversationManagerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface ConversationManagerContextValue {
setActiveConversation: (id: string | null, force?: boolean) => void;
getConversationName: (id: string) => string;
setConversationName: (id: string, name: string) => void;
getConversationLastEdit: (id: string) => number;
setConversationLastEdit: (id: string, lastEdit?: number) => void;
setShowUsage: React.Dispatch<React.SetStateAction<boolean>>;
}

Expand All @@ -36,5 +38,7 @@ export const ConversationManagerContext =
setActiveConversation: notImplemented,
getConversationName: notImplemented,
setConversationName: notImplemented,
getConversationLastEdit: notImplemented,
setConversationLastEdit: notImplemented,
setShowUsage: notImplemented,
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ export default ({ children }: ConversationManagerProviderProps) => {
[]
);
const [activeId, setActiveId] = React.useState<string | null>(null);

const [conversationNames, setConversationNames] = React.useState<
Map<string, string>
>(new Map());
const [conversationLastEdits, setconversationLastEdits] = React.useState<
Map<string, number>
>(new Map());

const { value: showUsage, setValue: setShowUsage } = useStorage(
"gpt-turbo-showusage",
true,
Expand All @@ -39,6 +44,11 @@ export default ({ children }: ConversationManagerProviderProps) => {
? conversation
: new Conversation(conversation, requestOptions);
setConversations((c) => [...c, newConversation]);
setconversationLastEdits((c) => {
const newMap = new Map(c);
newMap.set(newConversation.id, Date.now());
return newMap;
});
return newConversation;
},
[]
Expand Down Expand Up @@ -86,6 +96,13 @@ export default ({ children }: ConversationManagerProviderProps) => {
[conversationNames]
);

const getConversationLastEdit = React.useCallback(
(id: string) => {
return conversationLastEdits.get(id) ?? Date.now();
},
[conversationLastEdits]
);

const setConversationName = React.useCallback(
(id: string, name: string) => {
setConversationNames((c) => {
Expand All @@ -97,6 +114,17 @@ export default ({ children }: ConversationManagerProviderProps) => {
[]
);

const setConversationLastEdit = React.useCallback(
(id: string, lastEdit = Date.now()) => {
setconversationLastEdits((c) => {
const newMap = new Map(c);
newMap.set(id, lastEdit);
return newMap;
});
},
[]
);

const providerValue = React.useMemo<ConversationManagerContextValue>(
() => ({
conversations: Array.from(conversations.values()),
Expand All @@ -110,16 +138,20 @@ export default ({ children }: ConversationManagerProviderProps) => {
setActiveConversation,
getConversationName,
setConversationName,
getConversationLastEdit,
setConversationLastEdit,
setShowUsage,
}),
[
activeId,
addConversation,
conversations,
getConversationLastEdit,
getConversationName,
removeAllConversations,
removeConversation,
setActiveConversation,
setConversationLastEdit,
setConversationName,
setShowUsage,
showUsage,
Expand Down
Loading