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

Commit

Permalink
[web] Group conversations by relative date (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxijonson authored May 23, 2023
1 parent 7a82b17 commit 76b915a
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 25 deletions.
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

0 comments on commit 76b915a

Please sign in to comment.