From ffd62f7f42f5a2613477d3413dc09f5d6751e375 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Thu, 21 Dec 2023 22:26:47 +0100 Subject: [PATCH 01/26] Add and Show Folders --- .../components/applayout/ChatDrawerItems.tsx | 25 ++++++ .../chat/components/applayout/Folders.tsx | 72 ++++++++++++++++++ src/common/state/store-folders.ts | 76 +++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 src/apps/chat/components/applayout/Folders.tsx create mode 100644 src/common/state/store-folders.ts diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 2c21b20e7..17c698934 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -13,6 +13,8 @@ import { useUIPreferencesStore } from '~/common/state/store-ui'; import { useUXLabsStore } from '~/common/state/store-ux-labs'; import { ChatNavigationItemMemo } from './ChatNavigationItem'; +import { Folders } from './Folders'; +import { useFolderStore } from '~/common/state/store-folders'; // type ListGrouping = 'off' | 'persona'; @@ -37,6 +39,9 @@ function ChatDrawerItems(props: { const conversations = useChatStore(state => state.conversations, shallow); const showSymbols = useUIPreferencesStore(state => state.zenMode !== 'cleaner'); const labsEnhancedUI = useUXLabsStore(state => state.labsEnhancedUI); + const createFolder = useFolderStore((state) => state.createFolder); + const addConversationToFolder = useFolderStore((state) => state.addConversationToFolder); + // derived state const maxChatMessages = conversations.reduce((longest, _c) => Math.max(longest, _c.messages.length), 1); @@ -46,6 +51,22 @@ function ChatDrawerItems(props: { const softMaxReached = totalConversations >= 50; + const handleFolderSelect = (folderId: string | null) => { + // Logic to handle folder selection + + // Optionally, you could automatically activate the first conversation in the folder + console.log('Folder selected: ', folderId); + + }; + + const handleFolderCreate = (folderTitle: string) => { + const newFolderId = createFolder(folderTitle); + // Optionally, you could automatically add the current conversation to the new folder + // addConversationToFolder(newFolderId, props.activeConversationId); + }; + + + const handleButtonNew = React.useCallback(() => { onConversationNew(); closeLayoutDrawer(); @@ -90,6 +111,9 @@ function ChatDrawerItems(props: { {/* */} {/**/} + {/* Include the Folders component */} + + @@ -100,6 +124,7 @@ function ChatDrawerItems(props: { + {/**/} {/* */} diff --git a/src/apps/chat/components/applayout/Folders.tsx b/src/apps/chat/components/applayout/Folders.tsx new file mode 100644 index 000000000..0c9da5358 --- /dev/null +++ b/src/apps/chat/components/applayout/Folders.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useFolderStore } from '~/common/state/store-folders'; +import { Box, List, ListItem, ListItemButton, ListItemDecorator, ListItemContent } from '@mui/joy'; +import FolderIcon from '@mui/icons-material/Folder'; +import AddBoxIcon from '@mui/icons-material/AddBox'; +import DeleteIcon from '@mui/icons-material/Delete'; + +export const Folders = ({ onFolderSelect, onFolderCreate }: { onFolderSelect: (folderId: string | null) => void, onFolderCreate: (folderTitle: string) => void }) => { + + // Internal state + const [selectedFolderId, setSelectedFolderId] = React.useState(null); + + // External state + const folders = useFolderStore((state) => state.folders); + const deleteFolder = useFolderStore((state) => state.deleteFolder); + + const handleFolderSelect = (folderId: string) => { + onFolderSelect(folderId); + setSelectedFolderId(folderId); + }; + + const handleFolderCreate = () => { + const folderTitle = prompt('Enter folder name:'); + if (folderTitle) { + onFolderCreate(folderTitle); + } + }; + + const handleFolderDelete = (folderId: string, event: React.MouseEvent) => { + event.stopPropagation(); // Prevent ListItemButton onClick from being triggered + if (window.confirm('Are you sure you want to delete this folder?')) { + deleteFolder(folderId); + if (selectedFolderId === folderId) { + setSelectedFolderId(null); // Reset selection if the deleted folder was selected + onFolderSelect(''); // Notify parent component about the deletion + } + } + }; + + return ( + + + {folders.map((folder) => ( + + handleFolderSelect(folder.id)} + selected={folder.id === selectedFolderId} + sx={{ justifyContent: 'space-between', + '&:hover .delete-icon': { + visibility: 'visible', // Show delete icon on hover + } }} + > + + + {folder.title} + + + handleFolderDelete(folder.id, event)} /> + + + + ))} + + + + Create New Folder + + + + + ); +}; \ No newline at end of file diff --git a/src/common/state/store-folders.ts b/src/common/state/store-folders.ts new file mode 100644 index 000000000..85ecf6c0b --- /dev/null +++ b/src/common/state/store-folders.ts @@ -0,0 +1,76 @@ +// store-folders.ts + +import { create } from 'zustand'; +import { devtools, persist } from 'zustand/middleware'; +import { v4 as uuidv4 } from 'uuid'; +import { DConversationId } from './store-chats'; // Assuming this is the path to your chats store + +export interface DFolder { + id: string; + title: string; + conversationIds: DConversationId[]; +} + +interface FolderState { + folders: DFolder[]; +} + +interface FolderActions { + createFolder: (title: string) => string; + deleteFolder: (folderId: string) => void; + addConversationToFolder: (folderId: string, conversationId: DConversationId) => void; + removeConversationFromFolder: (folderId: string, conversationId: DConversationId) => void; +} + +type FolderStore = FolderState & FolderActions; + +export const useFolderStore = create()(devtools( + persist( + (set, get) => ({ + folders: [], // Initial state + + createFolder: (title: string): string => { + const newFolder: DFolder = { + id: uuidv4(), + title, + conversationIds: [], + }; + + set(state => ({ + folders: [...state.folders, newFolder], + })); + + return newFolder.id; + }, + + deleteFolder: (folderId: string): void => { + set(state => ({ + folders: state.folders.filter(folder => folder.id !== folderId), + })); + }, + + addConversationToFolder: (folderId: string, conversationId: DConversationId): void => { + set(state => ({ + folders: state.folders.map(folder => + folder.id === folderId + ? { ...folder, conversationIds: [...folder.conversationIds, conversationId] } + : folder + ), + })); + }, + + removeConversationFromFolder: (folderId: string, conversationId: DConversationId): void => { + set(state => ({ + folders: state.folders.map(folder => + folder.id === folderId + ? { ...folder, conversationIds: folder.conversationIds.filter(id => id !== conversationId) } + : folder + ), + })); + }, + }), + { + name: 'app-folders', + } + ) +)); \ No newline at end of file From e4a45fa0084705ab056e1781937bd0c7cf33d409 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Fri, 22 Dec 2023 08:41:01 +0100 Subject: [PATCH 02/26] first folder structure --- src/apps/chat/AppChat.tsx | 35 ++++-- .../components/applayout/ChatDrawerItems.tsx | 72 ++++++------ .../applayout/ChatNavigationItem.tsx | 4 +- .../chat/components/applayout/Folders.tsx | 107 ++++++++++++++++-- src/common/state/store-chats.ts | 12 ++ src/common/state/store-folders.ts | 4 +- 6 files changed, 170 insertions(+), 64 deletions(-) diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index 36da9e41d..04a76cb62 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -35,6 +35,7 @@ import { runAssistantUpdatingState } from './editors/chat-stream'; import { runBrowseUpdatingState } from './editors/browse-load'; import { runImageGenerationUpdatingState } from './editors/image-generate'; import { runReActUpdatingState } from './editors/react-tangent'; +import { useFolderStore } from '~/common/state/store-folders'; /** @@ -56,9 +57,13 @@ export function AppChat() { const [flattenConversationId, setFlattenConversationId] = React.useState(null); const showNextTitle = React.useRef(false); const composerTextAreaRef = React.useRef(null); + const [selectedFolderId, setSelectedFolderId] = React.useState(null); + // external state const { chatLLM } = useChatLLM(); + const addConversationToFolder = useFolderStore((state) => state.addConversationToFolder); + const { chatPanes, @@ -242,17 +247,24 @@ export function AppChat() { }; - // Chat actions +const handleConversationNew = React.useCallback(() => { + // Create a new conversation + const newConversationId = prependNewConversation(focusedSystemPurposeId ?? undefined); - const handleConversationNew = React.useCallback(() => { - // activate an existing new conversation if present, or create another - setFocusedConversationId(newConversationId - ? newConversationId - : prependNewConversation(focusedSystemPurposeId ?? undefined), - ); - composerTextAreaRef.current?.focus(); - }, [focusedSystemPurposeId, newConversationId, prependNewConversation, setFocusedConversationId]); + // If a folder is selected, add the new conversation to the folder + if (selectedFolderId) { + addConversationToFolder(selectedFolderId, newConversationId); + } + // Focus on the new conversation + setFocusedConversationId(newConversationId); + composerTextAreaRef.current?.focus(); + + // Return the new conversation ID + return newConversationId; +}, [focusedSystemPurposeId, prependNewConversation, setFocusedConversationId, selectedFolderId]); + + const handleConversationImportDialog = () => setTradeConfig({ dir: 'import' }); const handleConversationExport = (conversationId: DConversationId | null) => setTradeConfig({ dir: 'export', conversationId }); @@ -349,8 +361,10 @@ export function AppChat() { onConversationImportDialog={handleConversationImportDialog} onConversationNew={handleConversationNew} onConversationsDeleteAll={handleConversationsDeleteAll} + selectedFolderId={selectedFolderId} + setSelectedFolderId={setSelectedFolderId} />, - [focusedConversationId, handleConversationDelete, handleConversationNew, isFocusedChatEmpty, setFocusedConversationId], + [focusedConversationId, handleConversationDelete, handleConversationNew, isFocusedChatEmpty, setFocusedConversationId, selectedFolderId], ); const menuItems = React.useMemo(() => @@ -475,3 +489,4 @@ export function AppChat() { ; } + diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 17c698934..209f0fa0e 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -6,7 +6,7 @@ import AddIcon from '@mui/icons-material/Add'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import FileUploadIcon from '@mui/icons-material/FileUpload'; -import { DConversationId, useChatStore } from '~/common/state/store-chats'; +import { DConversationId, useChatStore, useConversationsByFolder } from '~/common/state/store-chats'; import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon'; import { closeLayoutDrawer } from '~/common/layout/store-applayout'; import { useUIPreferencesStore } from '~/common/state/store-ui'; @@ -27,16 +27,23 @@ function ChatDrawerItems(props: { onConversationActivate: (conversationId: DConversationId) => void, onConversationDelete: (conversationId: DConversationId, bypassConfirmation: boolean) => void, onConversationImportDialog: () => void, - onConversationNew: () => void, + onConversationNew: () => DConversationId, onConversationsDeleteAll: () => void, + selectedFolderId: string | null, + setSelectedFolderId: (folderId: string | null) => void, }) { + const { selectedFolderId, setSelectedFolderId } = props; + // local state const { onConversationDelete, onConversationNew, onConversationActivate } = props; // const [grouping] = React.useState('off'); + // Add state to track the selected folder // external state - const conversations = useChatStore(state => state.conversations, shallow); + //const conversations = useChatStore(state => state.conversations, shallow); + const conversations = useConversationsByFolder(selectedFolderId); + const showSymbols = useUIPreferencesStore(state => state.zenMode !== 'cleaner'); const labsEnhancedUI = useUXLabsStore(state => state.labsEnhancedUI); const createFolder = useFolderStore((state) => state.createFolder); @@ -53,6 +60,7 @@ function ChatDrawerItems(props: { const handleFolderSelect = (folderId: string | null) => { // Logic to handle folder selection + setSelectedFolderId(folderId); // Optionally, you could automatically activate the first conversation in the folder console.log('Folder selected: ', folderId); @@ -65,12 +73,17 @@ function ChatDrawerItems(props: { // addConversationToFolder(newFolderId, props.activeConversationId); }; - - const handleButtonNew = React.useCallback(() => { - onConversationNew(); + const newConversationId = onConversationNew(); + console.log('New conversation created: ', newConversationId); + if (selectedFolderId) { + console.log('Adding conversation to folder: ', selectedFolderId); + addConversationToFolder(selectedFolderId, newConversationId); + } closeLayoutDrawer(); - }, [onConversationNew]); + }, [onConversationNew, selectedFolderId, addConversationToFolder]); + + const handleConversationActivate = React.useCallback((conversationId: DConversationId, closeMenu: boolean) => { onConversationActivate(conversationId); @@ -112,7 +125,19 @@ function ChatDrawerItems(props: { {/**/} {/* Include the Folders component */} - + state.folders)} + selectedFolderId={selectedFolderId} + activeConversationId={props.activeConversationId} + isLonely={singleChat} + maxChatMessages={maxChatMessages} + showSymbols={showSymbols} + onConversationActivate={handleConversationActivate} + onConversationDelete={handleConversationDelete} + conversationsByFolder={conversations} + /> @@ -122,37 +147,6 @@ function ChatDrawerItems(props: { - - - - - {/**/} - {/* */} - {/* Conversations*/} - {/* */} - {/* newValue && setGrouping(newValue)}>*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/**/} - - {conversations.map(conversation => - )} - - diff --git a/src/apps/chat/components/applayout/ChatNavigationItem.tsx b/src/apps/chat/components/applayout/ChatNavigationItem.tsx index 9dec2a3b7..769c162ca 100644 --- a/src/apps/chat/components/applayout/ChatNavigationItem.tsx +++ b/src/apps/chat/components/applayout/ChatNavigationItem.tsx @@ -89,7 +89,7 @@ function ChatNavigationItem(props: { return ( button': { opacity: 1 }, - ...(isActive ? { bgcolor: 'red' } : {}), + ...(isActive ? { bgcolor: 'red' } : { }), }} > diff --git a/src/apps/chat/components/applayout/Folders.tsx b/src/apps/chat/components/applayout/Folders.tsx index 0c9da5358..e8176e981 100644 --- a/src/apps/chat/components/applayout/Folders.tsx +++ b/src/apps/chat/components/applayout/Folders.tsx @@ -1,22 +1,46 @@ import React from 'react'; -import { useFolderStore } from '~/common/state/store-folders'; +import { DFolder, useFolderStore } from '~/common/state/store-folders'; import { Box, List, ListItem, ListItemButton, ListItemDecorator, ListItemContent } from '@mui/joy'; import FolderIcon from '@mui/icons-material/Folder'; import AddBoxIcon from '@mui/icons-material/AddBox'; import DeleteIcon from '@mui/icons-material/Delete'; +import { DConversation, DConversationId } from '~/common/state/store-chats'; +import { ChatNavigationItemMemo } from './ChatNavigationItem'; -export const Folders = ({ onFolderSelect, onFolderCreate }: { onFolderSelect: (folderId: string | null) => void, onFolderCreate: (folderTitle: string) => void }) => { +export const Folders = ({ + onFolderSelect, + onFolderCreate, + folders, + selectedFolderId, + activeConversationId, + isLonely, + maxChatMessages, + showSymbols, + onConversationActivate, + onConversationDelete, + conversationsByFolder, + +}: { + onFolderSelect: (folderId: string | null) => void, + onFolderCreate: (folderTitle: string) => void, + folders: DFolder[], // Add this prop + selectedFolderId: string | null, // Add this prop + activeConversationId: string | null, + isLonely: boolean, + maxChatMessages: number, + showSymbols: boolean, + onConversationActivate: (conversationId: DConversationId, closeMenu: boolean) => void, + onConversationDelete: (conversationId: DConversationId) => void, + conversationsByFolder: DConversation[], // Add this prop +}) => { - // Internal state - const [selectedFolderId, setSelectedFolderId] = React.useState(null); + // Add a constant for the default folder title + const DEFAULT_FOLDER_TITLE = 'General'; - // External state - const folders = useFolderStore((state) => state.folders); const deleteFolder = useFolderStore((state) => state.deleteFolder); - const handleFolderSelect = (folderId: string) => { + const handleFolderSelect = (folderId: string | null) => { onFolderSelect(folderId); - setSelectedFolderId(folderId); }; const handleFolderCreate = () => { @@ -31,8 +55,7 @@ export const Folders = ({ onFolderSelect, onFolderCreate }: { onFolderSelect: (f if (window.confirm('Are you sure you want to delete this folder?')) { deleteFolder(folderId); if (selectedFolderId === folderId) { - setSelectedFolderId(null); // Reset selection if the deleted folder was selected - onFolderSelect(''); // Notify parent component about the deletion + onFolderSelect(null); // Notify parent component about the deletion } } }; @@ -40,7 +63,49 @@ export const Folders = ({ onFolderSelect, onFolderCreate }: { onFolderSelect: (f return ( + {/* Add a ListItem for the default folder */} + + handleFolderSelect(null)} + selected={selectedFolderId === null} + sx={{ + justifyContent: 'space-between', + '&:hover .delete-icon': { + visibility: 'hidden', // Hide delete icon for default folder + }, + }} + > + + + {DEFAULT_FOLDER_TITLE} + + + + + {/* Render the default folder's conversations when selected */} + {selectedFolderId === null && ( + + {conversationsByFolder.map((conversation) => ( + + ))} + + )} + {folders.map((folder) => ( + <> handleFolderSelect(folder.id)} @@ -59,6 +124,28 @@ export const Folders = ({ onFolderSelect, onFolderCreate }: { onFolderSelect: (f + {/* now if selected show conversations */} + {selectedFolderId === folder.id && ( + + {conversationsByFolder.map((conversation) => ( + + ))} + + )} + + ))} diff --git a/src/common/state/store-chats.ts b/src/common/state/store-chats.ts index 5f6c50cb8..c7f8a7e07 100644 --- a/src/common/state/store-chats.ts +++ b/src/common/state/store-chats.ts @@ -4,6 +4,7 @@ import { shallow } from 'zustand/shallow'; import { v4 as uuidv4 } from 'uuid'; import { DLLMId, useModelsStore } from '~/modules/llms/store-llms'; +import { useFolderStore } from '~/common/state/store-folders'; import { countModelTokens } from '../util/token-counter'; import { defaultSystemPurposeId, SystemPurposeId } from '../../data'; @@ -562,3 +563,14 @@ export const useConversation = (conversationId: DConversationId | null) => useCh setMessages: state.setMessages, }; }, shallow); + +export const useConversationsByFolder = (folderId: string | null) => useChatStore(state => { + if (folderId) { + const { conversations } = state; + const folder = useFolderStore.getState().folders.find(_f => _f.id === folderId); + if (folder) + return conversations.filter(_c => folder.conversationIds.includes(_c.id)); + } + // return only the conversations that are not in any folder + return state.conversations.filter(_c => !useFolderStore.getState().folders.find(_f => _f.conversationIds.includes(_c.id))); +}, shallow); diff --git a/src/common/state/store-folders.ts b/src/common/state/store-folders.ts index 85ecf6c0b..7a0dc96f1 100644 --- a/src/common/state/store-folders.ts +++ b/src/common/state/store-folders.ts @@ -1,5 +1,3 @@ -// store-folders.ts - import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; import { v4 as uuidv4 } from 'uuid'; @@ -57,7 +55,7 @@ export const useFolderStore = create()(devtools( : folder ), })); - }, + }, removeConversationFromFolder: (folderId: string, conversationId: DConversationId): void => { set(state => ({ From d9afbed543cd5b5bc9766de82f8a7a970e579d4a Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Fri, 22 Dec 2023 08:46:06 +0100 Subject: [PATCH 03/26] Renamed components to reflect existing structure --- src/apps/chat/components/applayout/ChatDrawerItems.tsx | 4 ++-- .../applayout/{Folders.tsx => ChatNavigationFolders.tsx} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/apps/chat/components/applayout/{Folders.tsx => ChatNavigationFolders.tsx} (99%) diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 209f0fa0e..2ce7c667f 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -13,7 +13,7 @@ import { useUIPreferencesStore } from '~/common/state/store-ui'; import { useUXLabsStore } from '~/common/state/store-ux-labs'; import { ChatNavigationItemMemo } from './ChatNavigationItem'; -import { Folders } from './Folders'; +import { ChatNavigationFolders } from './ChatNavigationFolders'; import { useFolderStore } from '~/common/state/store-folders'; @@ -125,7 +125,7 @@ function ChatDrawerItems(props: { {/**/} {/* Include the Folders component */} - state.folders)} diff --git a/src/apps/chat/components/applayout/Folders.tsx b/src/apps/chat/components/applayout/ChatNavigationFolders.tsx similarity index 99% rename from src/apps/chat/components/applayout/Folders.tsx rename to src/apps/chat/components/applayout/ChatNavigationFolders.tsx index e8176e981..c4c8d6bb4 100644 --- a/src/apps/chat/components/applayout/Folders.tsx +++ b/src/apps/chat/components/applayout/ChatNavigationFolders.tsx @@ -7,7 +7,7 @@ import DeleteIcon from '@mui/icons-material/Delete'; import { DConversation, DConversationId } from '~/common/state/store-chats'; import { ChatNavigationItemMemo } from './ChatNavigationItem'; -export const Folders = ({ +export const ChatNavigationFolders = ({ onFolderSelect, onFolderCreate, folders, From 37dbfb6ee3571f5dba50d1226fa519796da70c47 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Fri, 22 Dec 2023 09:09:10 +0100 Subject: [PATCH 04/26] Delete confirmation buttons --- .../applayout/ChatNavigationFolders.tsx | 122 +++++++++++------- 1 file changed, 74 insertions(+), 48 deletions(-) diff --git a/src/apps/chat/components/applayout/ChatNavigationFolders.tsx b/src/apps/chat/components/applayout/ChatNavigationFolders.tsx index c4c8d6bb4..2aedbc8f1 100644 --- a/src/apps/chat/components/applayout/ChatNavigationFolders.tsx +++ b/src/apps/chat/components/applayout/ChatNavigationFolders.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { DFolder, useFolderStore } from '~/common/state/store-folders'; -import { Box, List, ListItem, ListItemButton, ListItemDecorator, ListItemContent } from '@mui/joy'; +import { Box, List, IconButton, ListItem, ListItemButton, ListItemDecorator, ListItemContent } from '@mui/joy'; import FolderIcon from '@mui/icons-material/Folder'; import AddBoxIcon from '@mui/icons-material/AddBox'; -import DeleteIcon from '@mui/icons-material/Delete'; +import CloseIcon from '@mui/icons-material/Close'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import { DConversation, DConversationId } from '~/common/state/store-chats'; import { ChatNavigationItemMemo } from './ChatNavigationItem'; @@ -38,6 +39,7 @@ export const ChatNavigationFolders = ({ const DEFAULT_FOLDER_TITLE = 'General'; const deleteFolder = useFolderStore((state) => state.deleteFolder); + const [deleteArmedFolderId, setDeleteArmedFolderId] = React.useState(null); const handleFolderSelect = (folderId: string | null) => { onFolderSelect(folderId); @@ -50,16 +52,25 @@ export const ChatNavigationFolders = ({ } }; + const handleFolderDelete = (folderId: string, event: React.MouseEvent) => { - event.stopPropagation(); // Prevent ListItemButton onClick from being triggered - if (window.confirm('Are you sure you want to delete this folder?')) { - deleteFolder(folderId); - if (selectedFolderId === folderId) { - onFolderSelect(null); // Notify parent component about the deletion - } + event.stopPropagation(); + setDeleteArmedFolderId(folderId); // Arm the deletion instead of confirming immediately + }; + + const handleConfirmDelete = (folderId: string) => { + deleteFolder(folderId); + if (selectedFolderId === folderId) { + onFolderSelect(null); } + setDeleteArmedFolderId(null); // Disarm the deletion + }; + + const handleCancelDelete = () => { + setDeleteArmedFolderId(null); // Disarm the deletion }; + return ( @@ -105,46 +116,61 @@ export const ChatNavigationFolders = ({ )} {folders.map((folder) => ( - <> - - handleFolderSelect(folder.id)} - selected={folder.id === selectedFolderId} - sx={{ justifyContent: 'space-between', - '&:hover .delete-icon': { - visibility: 'visible', // Show delete icon on hover - } }} - > - - - {folder.title} - - - handleFolderDelete(folder.id, event)} /> - - - - {/* now if selected show conversations */} - {selectedFolderId === folder.id && ( - - {conversationsByFolder.map((conversation) => ( - - ))} - - )} - + + + handleFolderSelect(folder.id)} + selected={folder.id === selectedFolderId} + sx={{ justifyContent: 'space-between', + '&:hover .delete-icon': { + visibility: 'visible', // Show delete icon on hover + } }} + > + + + {folder.title} + + {deleteArmedFolderId !== folder.id && ( + + handleFolderDelete(folder.id, event)}> + + + + )} + {/* Confirm/Cancel delete buttons */} + {deleteArmedFolderId === folder.id && ( + + handleConfirmDelete(folder.id)}> + + + + + + + )} + + + {/* now if selected show conversations */} + {selectedFolderId === folder.id && ( + + {conversationsByFolder.map((conversation) => ( + + ))} + + )} + ))} From ddc056616a11f34d6d70972c41ebe3808474db42 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Fri, 22 Dec 2023 09:13:26 +0100 Subject: [PATCH 05/26] formatting --- .../applayout/ChatNavigationFolders.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/apps/chat/components/applayout/ChatNavigationFolders.tsx b/src/apps/chat/components/applayout/ChatNavigationFolders.tsx index 2aedbc8f1..aaaad5035 100644 --- a/src/apps/chat/components/applayout/ChatNavigationFolders.tsx +++ b/src/apps/chat/components/applayout/ChatNavigationFolders.tsx @@ -137,17 +137,17 @@ export const ChatNavigationFolders = ({ )} - {/* Confirm/Cancel delete buttons */} - {deleteArmedFolderId === folder.id && ( - - handleConfirmDelete(folder.id)}> - - - - - - - )} + {/* Confirm/Cancel delete buttons */} + {deleteArmedFolderId === folder.id && ( + + handleConfirmDelete(folder.id)}> + + + + + + + )} {/* now if selected show conversations */} From 3f843b7a9909270b9aa73209b5a5277dda896e68 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Fri, 22 Dec 2023 09:16:41 +0100 Subject: [PATCH 06/26] select newly created folder --- src/apps/chat/components/applayout/ChatDrawerItems.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 2ce7c667f..46af9b753 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -69,8 +69,9 @@ function ChatDrawerItems(props: { const handleFolderCreate = (folderTitle: string) => { const newFolderId = createFolder(folderTitle); - // Optionally, you could automatically add the current conversation to the new folder - // addConversationToFolder(newFolderId, props.activeConversationId); + + // select the new folder + setSelectedFolderId(newFolderId); }; const handleButtonNew = React.useCallback(() => { From bcc6cf021118bd7c0dea844295701b2469d9b239 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Fri, 22 Dec 2023 10:13:16 +0100 Subject: [PATCH 07/26] allow creations of new chats for empty folders --- src/apps/chat/AppChat.tsx | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index 04a76cb62..bfd9e4672 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -17,7 +17,7 @@ import { useChatLLM, useModelsStore } from '~/modules/llms/store-llms'; import { ConfirmationModal } from '~/common/components/ConfirmationModal'; import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut'; import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore'; -import { createDMessage, DConversationId, DMessage, getConversation, useConversation } from '~/common/state/store-chats'; +import { createDMessage, DConversationId, DMessage, getConversation, useChatStore, useConversation } from '~/common/state/store-chats'; import { openLayoutLLMOptions, useLayoutPluggable } from '~/common/layout/store-applayout'; import { useUXLabsStore } from '~/common/state/store-ux-labs'; @@ -64,6 +64,9 @@ export function AppChat() { const { chatLLM } = useChatLLM(); const addConversationToFolder = useFolderStore((state) => state.addConversationToFolder); + // Get the list of conversations from the store + const conversations = useChatStore(state => state.conversations); + const { chatPanes, @@ -246,6 +249,27 @@ export function AppChat() { await speakText(text); }; + // Function to check if the selected folder has an empty chat + const doesSelectedFolderHaveEmptyChat = (selectedFolderId: string | null, conversations: any[]) => { + // If no folder is selected (default folder), check if there is an empty chat globally + if (selectedFolderId === null) { + return conversations.some(convo => convo.messages.length === 0); + } + + // Retrieve the folder's conversations + const folderConversations = useFolderStore.getState().folders.find(folder => folder.id === selectedFolderId)?.conversationIds || []; + + // Check if any of the folder's conversations are empty + return folderConversations.some(convoId => { + const convo = conversations.find(conversation => conversation.id === convoId); + return convo && convo.messages.length === 0; + }); + }; + + + // Determine if the "New" button should be disabled + const disableNewButton = doesSelectedFolderHaveEmptyChat(selectedFolderId, conversations); + const handleConversationNew = React.useCallback(() => { // Create a new conversation @@ -262,7 +286,7 @@ const handleConversationNew = React.useCallback(() => { // Return the new conversation ID return newConversationId; -}, [focusedSystemPurposeId, prependNewConversation, setFocusedConversationId, selectedFolderId]); +}, [focusedSystemPurposeId, prependNewConversation, setFocusedConversationId, selectedFolderId, addConversationToFolder]); const handleConversationImportDialog = () => setTradeConfig({ dir: 'import' }); @@ -355,7 +379,7 @@ const handleConversationNew = React.useCallback(() => { const drawerItems = React.useMemo(() => { selectedFolderId={selectedFolderId} setSelectedFolderId={setSelectedFolderId} />, - [focusedConversationId, handleConversationDelete, handleConversationNew, isFocusedChatEmpty, setFocusedConversationId, selectedFolderId], + [focusedConversationId, handleConversationDelete, handleConversationNew, setFocusedConversationId, selectedFolderId, disableNewButton], ); const menuItems = React.useMemo(() => From e5adfb18873d0c2580c9aa7a89142be037ce79ec Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Fri, 22 Dec 2023 10:17:47 +0100 Subject: [PATCH 08/26] Cleaning things up --- src/apps/chat/components/applayout/ChatDrawerItems.tsx | 8 +------- .../chat/components/applayout/ChatNavigationFolders.tsx | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 46af9b753..42e819ddc 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -61,10 +61,6 @@ function ChatDrawerItems(props: { const handleFolderSelect = (folderId: string | null) => { // Logic to handle folder selection setSelectedFolderId(folderId); - - // Optionally, you could automatically activate the first conversation in the folder - console.log('Folder selected: ', folderId); - }; const handleFolderCreate = (folderTitle: string) => { @@ -76,9 +72,7 @@ function ChatDrawerItems(props: { const handleButtonNew = React.useCallback(() => { const newConversationId = onConversationNew(); - console.log('New conversation created: ', newConversationId); if (selectedFolderId) { - console.log('Adding conversation to folder: ', selectedFolderId); addConversationToFolder(selectedFolderId, newConversationId); } closeLayoutDrawer(); @@ -143,7 +137,7 @@ function ChatDrawerItems(props: { - New + New chat {/**/} diff --git a/src/apps/chat/components/applayout/ChatNavigationFolders.tsx b/src/apps/chat/components/applayout/ChatNavigationFolders.tsx index aaaad5035..9a73ba1ca 100644 --- a/src/apps/chat/components/applayout/ChatNavigationFolders.tsx +++ b/src/apps/chat/components/applayout/ChatNavigationFolders.tsx @@ -176,7 +176,7 @@ export const ChatNavigationFolders = ({ - Create New Folder + New Folder From a9c1cb786d656c551fd889e3a7745448102b761a Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Fri, 22 Dec 2023 16:53:37 +0100 Subject: [PATCH 09/26] Enable editing of folder names --- .../applayout/ChatNavigationFolders.tsx | 103 ++++++++++++++---- src/common/state/store-folders.ts | 12 ++ 2 files changed, 96 insertions(+), 19 deletions(-) diff --git a/src/apps/chat/components/applayout/ChatNavigationFolders.tsx b/src/apps/chat/components/applayout/ChatNavigationFolders.tsx index 9a73ba1ca..f69e30efc 100644 --- a/src/apps/chat/components/applayout/ChatNavigationFolders.tsx +++ b/src/apps/chat/components/applayout/ChatNavigationFolders.tsx @@ -4,6 +4,7 @@ import { Box, List, IconButton, ListItem, ListItemButton, ListItemDecorator, Lis import FolderIcon from '@mui/icons-material/Folder'; import AddBoxIcon from '@mui/icons-material/AddBox'; import CloseIcon from '@mui/icons-material/Close'; +import EditIcon from '@mui/icons-material/Edit'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import { DConversation, DConversationId } from '~/common/state/store-chats'; import { ChatNavigationItemMemo } from './ChatNavigationItem'; @@ -39,8 +40,11 @@ export const ChatNavigationFolders = ({ const DEFAULT_FOLDER_TITLE = 'General'; const deleteFolder = useFolderStore((state) => state.deleteFolder); + const updateFolderName = useFolderStore((state) => state.updateFolderName); const [deleteArmedFolderId, setDeleteArmedFolderId] = React.useState(null); - + const [editingFolderId, setEditingFolderId] = React.useState(null); + const [editingFolderName, setEditingFolderName] = React.useState(''); + const handleFolderSelect = (folderId: string | null) => { onFolderSelect(folderId); }; @@ -70,6 +74,29 @@ export const ChatNavigationFolders = ({ setDeleteArmedFolderId(null); // Disarm the deletion }; + const handleSaveFolder = (folderId: string) => { + if (editingFolderName.trim() !== '') { + // Call a function to update the folder name in your state management + updateFolderName(folderId, editingFolderName.trim()); + } + setEditingFolderId(null); // Exit edit mode + }; + + const handleCancelEdit = () => { + setEditingFolderId(null); // Exit edit mode without saving + setEditingFolderName(''); // Reset editing name + }; + + const handleEdit = (event: React.MouseEvent, folderId: string, folderTitle: string) => { + event.stopPropagation(); // Prevent the ListItemButton's onClick from firing + + setEditingFolderId(folderId); + setEditingFolderName(folderTitle); + }; + + + + return ( @@ -122,32 +149,70 @@ export const ChatNavigationFolders = ({ onClick={() => handleFolderSelect(folder.id)} selected={folder.id === selectedFolderId} sx={{ justifyContent: 'space-between', - '&:hover .delete-icon': { + '&:hover .edit-icon': { visibility: 'visible', // Show delete icon on hover } }} > - {folder.title} + {editingFolderId !== folder.id ? ( + {folder.title} + ) : ( + handleSaveFolder(folder.id)} + onChange={(e) => { + e.stopPropagation(); // Prevent the ListItemButton's onClick from firing + setEditingFolderName(e.target.value); + }} + onKeyUp={(e) => { + e.stopPropagation(); // Prevent the ListItemButton's onClick from firing + if (e.key === 'Enter') { + handleSaveFolder(folder.id); + } + }} + autoFocus + /> + )} + + {editingFolderId === folder.id && ( + <> + handleSaveFolder(folder.id)}> + + + + )} - {deleteArmedFolderId !== folder.id && ( - - handleFolderDelete(folder.id, event)}> - - - - )} - {/* Confirm/Cancel delete buttons */} - {deleteArmedFolderId === folder.id && ( - - handleConfirmDelete(folder.id)}> - + + + {editingFolderId !== folder.id && ( + + handleEdit(event, folder.id, folder.title)}> + - - + + )} + + {deleteArmedFolderId !== folder.id && !editingFolderId && ( + + handleFolderDelete(folder.id, event)}> + - - )} + + )} + {/* Confirm/Cancel delete buttons */} + {deleteArmedFolderId === folder.id && ( + + handleConfirmDelete(folder.id)}> + + + + + + + )} + {/* now if selected show conversations */} diff --git a/src/common/state/store-folders.ts b/src/common/state/store-folders.ts index 7a0dc96f1..27a88488a 100644 --- a/src/common/state/store-folders.ts +++ b/src/common/state/store-folders.ts @@ -15,6 +15,7 @@ interface FolderState { interface FolderActions { createFolder: (title: string) => string; + updateFolderName: (folderId: string, title: string) => void; deleteFolder: (folderId: string) => void; addConversationToFolder: (folderId: string, conversationId: DConversationId) => void; removeConversationFromFolder: (folderId: string, conversationId: DConversationId) => void; @@ -41,6 +42,17 @@ export const useFolderStore = create()(devtools( return newFolder.id; }, + updateFolderName: (folderId: string, title: string): void => { + set(state => ({ + folders: state.folders.map(folder => + folder.id === folderId + ? { ...folder, title } + : folder + ), + })); + }, + + deleteFolder: (folderId: string): void => { set(state => ({ folders: state.folders.filter(folder => folder.id !== folderId), From 6f1504fe852d250794b9ecba1517997aaa06fc13 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Sun, 24 Dec 2023 22:39:50 +0100 Subject: [PATCH 10/26] Initial new idea --- package-lock.json | 117 +++++++ package.json | 2 + .../components/applayout/ChatDrawerItems.tsx | 178 +++++++++-- .../chat/components/applayout/ChatSidebar.tsx | 143 +++++++++ .../components/applayout/FolderListItem.tsx | 291 ++++++++++++++++++ src/common/state/store-chats.ts | 1 + src/common/state/store-folders.ts | 32 +- 7 files changed, 741 insertions(+), 23 deletions(-) create mode 100644 src/apps/chat/components/applayout/ChatSidebar.tsx create mode 100644 src/apps/chat/components/applayout/FolderListItem.tsx diff --git a/package-lock.json b/package-lock.json index aa488b564..c37f053d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "plantuml-encoder": "^1.4.0", "prismjs": "^1.29.0", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-katex": "^3.0.1", "react-markdown": "^9.0.1", @@ -51,6 +52,7 @@ "@types/plantuml-encoder": "^1.4.2", "@types/prismjs": "^1.26.3", "@types/react": "^18.2.45", + "@types/react-beautiful-dnd": "^13.1.7", "@types/react-dom": "^18.2.17", "@types/react-katex": "^3.0.4", "@types/react-timeago": "^4.1.6", @@ -1323,6 +1325,15 @@ "@types/unist": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1386,6 +1397,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-beautiful-dnd": { + "version": "13.1.7", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.7.tgz", + "integrity": "sha512-jQZLov9OkD0xRQkqz8/lx66bHYAYv+g4+POBqnH5Jtt/xo4MygzM879Q9sxAiosPBdNj1JYTdbPxDn3dNRYgow==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.2.17", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", @@ -1404,6 +1424,17 @@ "@types/react": "*" } }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/react-timeago": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.6.tgz", @@ -2192,6 +2223,14 @@ "node": ">= 8" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -4551,6 +4590,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5796,6 +5840,11 @@ } ] }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -5807,6 +5856,24 @@ "node": ">=0.10.0" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -5861,6 +5928,35 @@ "react": ">=18" } }, + "node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, "node_modules/react-ssr-prepass": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz", @@ -5904,6 +6000,14 @@ "string_decoder": "~0.10.x" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -6624,6 +6728,11 @@ "xtend": "~2.1.1" } }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -6946,6 +7055,14 @@ "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", "dev": true }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index a68a319bd..ae58aa807 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "plantuml-encoder": "^1.4.0", "prismjs": "^1.29.0", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-katex": "^3.0.1", "react-markdown": "^9.0.1", @@ -55,6 +56,7 @@ "@types/plantuml-encoder": "^1.4.2", "@types/prismjs": "^1.26.3", "@types/react": "^18.2.45", + "@types/react-beautiful-dnd": "^13.1.7", "@types/react-dom": "^18.2.17", "@types/react-katex": "^3.0.4", "@types/react-timeago": "^4.1.6", diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 42e819ddc..2f772465d 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -1,12 +1,15 @@ import * as React from 'react'; import { shallow } from 'zustand/shallow'; -import { Box, ListDivider, ListItemDecorator, MenuItem, Typography } from '@mui/joy'; +import { Box, IconButton, Input, List, ListDivider, ListItem, ListItemButton, ListItemDecorator, MenuItem, Sheet, Typography, useTheme } from '@mui/joy'; import AddIcon from '@mui/icons-material/Add'; +import SearchIcon from '@mui/icons-material/Search'; + import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import FileUploadIcon from '@mui/icons-material/FileUpload'; +import ExpandCircleDownIcon from '@mui/icons-material/ExpandCircleDown'; -import { DConversationId, useChatStore, useConversationsByFolder } from '~/common/state/store-chats'; +import { DConversation, DConversationId, useChatStore, useConversationsByFolder } from '~/common/state/store-chats'; import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon'; import { closeLayoutDrawer } from '~/common/layout/store-applayout'; import { useUIPreferencesStore } from '~/common/state/store-ui'; @@ -14,7 +17,12 @@ import { useUXLabsStore } from '~/common/state/store-ux-labs'; import { ChatNavigationItemMemo } from './ChatNavigationItem'; import { ChatNavigationFolders } from './ChatNavigationFolders'; + +import ChatSidebar from './ChatSidebar'; + import { useFolderStore } from '~/common/state/store-folders'; +import { NoSSR } from '~/common/components/NoSSR'; +import { CSSProperties, useEffect, useRef, useState } from 'react'; // type ListGrouping = 'off' | 'persona'; @@ -90,7 +98,137 @@ function ChatDrawerItems(props: { !singleChat && conversationId && onConversationDelete(conversationId, true); }, [onConversationDelete, singleChat]); + const ConversationList = ({ conversations }: { conversations: DConversation[] }) => { + const listRef = useRef(null); + const [showDownArrow, setShowDownArrow] = useState(false); + const [showUpArrow, setShowUpArrow] = useState(false); + + const theme = useTheme(); + + const checkForOverflow = () => { + const currentList = listRef.current; + if (currentList) { + const isOverflowing = currentList.scrollHeight > currentList.clientHeight; + setShowDownArrow(isOverflowing); + setShowUpArrow(false); // Initially, we don't want to show the up arrow + } + }; + + const checkScrollPosition = () => { + const currentList = listRef.current; + if (currentList) { + const isAtBottom = currentList.scrollHeight - currentList.scrollTop === currentList.clientHeight; + const isAtTop = currentList.scrollTop === 0; + setShowDownArrow(!isAtBottom); + setShowUpArrow(!isAtTop); + } + }; + + useEffect(() => { + checkForOverflow(); + window.addEventListener('resize', checkForOverflow); + + // Add scroll event listener + const currentList = listRef.current; + if (currentList) { + currentList.addEventListener('scroll', checkScrollPosition); + } + + return () => { + window.removeEventListener('resize', checkForOverflow); + // Remove scroll event listener + if (currentList) { + currentList.removeEventListener('scroll', checkScrollPosition); + } + }; + }, [conversations]); + + const styles: { container: CSSProperties; listContainer: CSSProperties; arrow: CSSProperties, arrowDown: CSSProperties, arrowUp: CSSProperties } = { + container: { + position: 'relative', // Container for both list and arrows + maxHeight: '200px', + }, + listContainer: { + maxHeight: '200px', + overflow: 'auto', + }, + arrow: { + position: 'absolute', + left: '50%', + transform: 'translateX(-50%)', + color: theme.palette.text.secondary, + opacity: 0.7, + fontSize: '30px', + }, + arrowDown: { + bottom: 0, + }, + arrowUp: { + top: 10, + // rotate arrow 180 degrees + transform: 'translateX(-50%) rotate(180deg)', + }, + }; + + return ( +
+
+ + {conversations.map((conversation, index) => ( + + handleConversationActivate(conversation.id, false)} + sx={{ + // py: 0, + position: 'relative', + border: 'none', // note, there's a default border of 1px and invisible.. hmm + '&:hover > button': { opacity: 1 }, + ...(conversation.id === props.activeConversationId ? { bgcolor: 'red' } : { }), + }} + > + { conversation.autoTitle && ( + + {conversation.autoTitle} + + )} + + + + + ))} + +
+ {showUpArrow && ( + { + listRef.current?.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }} + /> + )} + {showDownArrow && ( + { + listRef.current?.scrollTo({ + top: listRef.current?.scrollHeight, + behavior: 'smooth', + }); + }} + /> + )} +
+ ); + }; + + + // grouping /*let sortedIds = conversationIDs; if (grouping === 'persona') { @@ -113,26 +251,22 @@ function ChatDrawerItems(props: { return <> - {/**/} - {/* */} - {/* Active chats*/} - {/* */} - {/**/} - - {/* Include the Folders component */} - state.folders)} - selectedFolderId={selectedFolderId} - activeConversationId={props.activeConversationId} - isLonely={singleChat} - maxChatMessages={maxChatMessages} - showSymbols={showSymbols} - onConversationActivate={handleConversationActivate} - onConversationDelete={handleConversationDelete} - conversationsByFolder={conversations} - /> + state.folders)} + selectedFolderId={selectedFolderId} + conversationsByFolder={conversations} + /> + + {/* show all ChatNavigation items */} + + {/* Search input for conversations */} + + }> + + + + diff --git a/src/apps/chat/components/applayout/ChatSidebar.tsx b/src/apps/chat/components/applayout/ChatSidebar.tsx new file mode 100644 index 000000000..11cdc671f --- /dev/null +++ b/src/apps/chat/components/applayout/ChatSidebar.tsx @@ -0,0 +1,143 @@ +import * as React from 'react'; +import { useEffect, useState } from 'react'; +import List from '@mui/joy/List'; +import ListItem from '@mui/joy/ListItem'; +import ListItemButton from '@mui/joy/ListItemButton'; +import ListItemContent from '@mui/joy/ListItemContent'; +import ListItemDecorator from '@mui/joy/ListItemDecorator'; +import IconButton from '@mui/joy/IconButton'; +import Box from '@mui/joy/Box'; +import Sheet, { sheetClasses } from '@mui/joy/Sheet'; +import Typography from '@mui/joy/Typography'; +import Radio, { radioClasses } from '@mui/joy/Radio'; + +import { DFolder, useFolderStore } from '~/common/state/store-folders'; +import { + DragDropContext, + Droppable, + DroppableProps, + Draggable, + DropResult, + DraggingStyle, + NotDraggingStyle, + DraggableProvided, + DraggableStateSnapshot, +} from 'react-beautiful-dnd'; +import OutlineFolderIcon from '@mui/icons-material/Folder'; +import { Dropdown, FormLabel, Menu, MenuButton, MenuItem, RadioGroup } from '@mui/joy'; +import MoreVert from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; + +import Done from '@mui/icons-material/Done'; +import FolderListItem from './FolderListItem'; +import { DConversation } from '~/common/state/store-chats'; + +export default function ChatSidebar( + { + onFolderSelect, + folders, + selectedFolderId, + conversationsByFolder, + + }: { + onFolderSelect: (folderId: string | null) => void, + folders: DFolder[], + selectedFolderId: string | null, + conversationsByFolder: DConversation[], + } +) { + const { moveFolder, updateFolderName } = useFolderStore((state) => ({ + folders: state.folders, + moveFolder: state.moveFolder, + updateFolderName: state.updateFolderName, + })); + + const onDragEnd = (result: DropResult) => { + if (!result.destination) return; + moveFolder(result.source.index, result.destination.index); + }; + + return ( + + + Folders + + + ({ + '& ul': { + '--List-gap': '0px', + bgcolor: 'background.surface', + '& > li:first-of-type > [role="button"]': { + borderTopRightRadius: 'var(--List-radius)', + borderTopLeftRadius: 'var(--List-radius)', + }, + '& > li:last-child > [role="button"]': { + borderBottomRightRadius: 'var(--List-radius)', + borderBottomLeftRadius: 'var(--List-radius)', + }, + }, + '--List-radius': '8px', + '--List-gap': '1rem', + '--ListDivider-gap': '0px', + '--ListItem-paddingY': '0.5rem', + // override global variant tokens + '--joy-palette-neutral-plainHoverBg': 'rgba(0 0 0 / 0.08)', + '--joy-palette-neutral-plainActiveBg': 'rgba(0 0 0 / 0.12)', + [theme.getColorSchemeSelector('light')]: { + '--joy-palette-divider': 'rgba(0 0 0 / 0.08)', + }, + [theme.getColorSchemeSelector('dark')]: { + '--joy-palette-neutral-plainHoverBg': 'rgba(255 255 255 / 0.1)', + '--joy-palette-neutral-plainActiveBg': 'rgba(255 255 255 / 0.16)', + }, + })} + > + + + } + > + {(provided) => ( + + {folders.map((folder, index) => ( + + {(provided, snapshot) => ( + + + + )} + + ))} + {provided.placeholder} + + )} + + + + + + ); +} + +export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => { + const [enabled, setEnabled] = useState(false); + + useEffect(() => { + const animation = requestAnimationFrame(() => setEnabled(true)); + + return () => { + cancelAnimationFrame(animation); + setEnabled(false); + }; + }, []); + + if (!enabled) { + return null; + } + + return {children}; +}; diff --git a/src/apps/chat/components/applayout/FolderListItem.tsx b/src/apps/chat/components/applayout/FolderListItem.tsx new file mode 100644 index 000000000..3c5fa1807 --- /dev/null +++ b/src/apps/chat/components/applayout/FolderListItem.tsx @@ -0,0 +1,291 @@ +import React, { useState } from 'react'; +import { DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd'; +import { + ListItem, + ListItemButton, + ListItemDecorator, + ListItemContent, + Typography, + IconButton, + Dropdown, + Menu, + MenuButton, + MenuItem, + FormLabel, + RadioGroup, + Sheet, + Radio, + radioClasses, +} from '@mui/joy'; +import OutlineFolderIcon from '@mui/icons-material/Folder'; +import MoreVert from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import Done from '@mui/icons-material/Done'; +import { DFolder, useFolderStore } from '~/common/state/store-folders'; +import { DraggingStyle, NotDraggingStyle } from 'react-beautiful-dnd'; + +// Define the type for your props if you're using TypeScript +type RenderItemProps = { + folder: DFolder; + provided: DraggableProvided; + snapshot: DraggableStateSnapshot; + onFolderSelect: (folderId: string | null) => void; + selectedFolderId: string | null; + // Include any other props that RenderItem needs +}; + +const FolderListItem: React.FC = ({ folder, provided, snapshot, onFolderSelect, selectedFolderId }) => { + // internal state + const [deleteArmedFolderId, setDeleteArmedFolderId] = useState(null); + const [editingFolderId, setEditingFolderId] = useState(null); + const [editingFolderName, setEditingFolderName] = useState(''); + + // external state + const { folders, moveFolder, updateFolderName } = useFolderStore((state) => ({ + folders: state.folders, + moveFolder: state.moveFolder, + updateFolderName: state.updateFolderName, + })); + + const { setFolderColor } = useFolderStore((state) => ({ + setFolderColor: state.setFolderColor, + })); + + const handleColorChange = (event: React.ChangeEvent) => { + setFolderColor(folder.id, event.target.value); + console.log(event.target.value); + }; + + // Handlers for editing and deleting + const handleEdit = (event: React.MouseEvent, folderId: string, folderTitle: string) => { + event.stopPropagation(); // Prevent the ListItemButton's onClick from firing + setEditingFolderId(folderId); + setEditingFolderName(folderTitle); + }; + + const handleSaveFolder = (folderId: string) => { + if (editingFolderName.trim() !== '') { + updateFolderName(folderId, editingFolderName.trim()); + } + setEditingFolderId(null); // Exit edit mode + }; + + const handleInputChange = (event: React.ChangeEvent) => { + setEditingFolderName(event.target.value); + }; + + const handleInputKeyUp = (event: React.KeyboardEvent, folderId: string) => { + if (event.key === 'Enter') { + handleSaveFolder(folderId); + } else if (event.key === 'Escape') { + handleCancelEdit(); + } + }; + + const handleCancelEdit = () => { + setEditingFolderId(null); // Exit edit mode without saving + setEditingFolderName(''); // Reset editing name + }; + + const getItemStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({ + userSelect: 'none', + borderRadius: '8px', + backgroundColor: isDragging ? 'rgba(0, 80, 80, 0.18)' : 'transparent', + + ...draggableStyle, + + // Any additional styles you want to apply during dragging + ...(isDragging && + { + // Apply any drag-specific styles here + // marginLeft: '12px', + }), + }); + + const getListItemContentStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({ + ...(isDragging && { + // Apply any drag-specific styles here + marginLeft: '20px', + }), + }); + + const getListItemDecoratorStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({ + ...(isDragging && { + // Apply any drag-specific styles here + marginLeft: '12px', + }), + }); + + + const handleFolderSelect = (folderId: string | null) => { + onFolderSelect(folderId); + }; + + + return ( + + { + event.stopPropagation(); // Prevent the ListItemButton's onClick from firing + console.log('folder.id', folder.id); + handleFolderSelect(folder.id); + }} + selected={folder.id === selectedFolderId} + + sx={{ + justifyContent: 'space-between', + '&:hover .menu-icon': { + visibility: 'visible', // Hide delete icon for default folder + }, + }} + > + + + + + {editingFolderId === folder.id ? ( + handleInputKeyUp(event, folder.id)} + onBlur={() => handleSaveFolder(folder.id)} + autoFocus + style={{ + // Add styles for the input field + fontSize: 'inherit', + fontWeight: 'inherit', + color: 'inherit', + background: 'none', + border: 'none', + outline: 'none', + width: '100%', // Ensure the input field expands as needed + }} + /> + ) : ( + + {folder.title} + + )} + + + + + + + { + event.stopPropagation(); // Prevent the ListItemButton's onClick from firing + setEditingFolderId(folder.id); + }} + > + + Edit + + + + Delete + + + + Color + + + {(['#01B80A', '#F0CB00', '#C001D7', '#0093E8', '#E03C00'] as const).map((color, index) => ( + + } + value={color} + color="neutral" + slotProps={{ + input: { 'aria-label': color }, + radio: { + sx: { + display: 'contents', + '--variant-borderWidth': '2px', + }, + }, + }} + sx={{ + '--joy-focus-outlineOffset': '4px', + '--joy-palette-focusVisible': color, + [`& .${radioClasses.action}.${radioClasses.focusVisible}`]: { + outlineWidth: '2px', + }, + }} + /> + + ))} + + + + + + + ); +}; + +export default FolderListItem; diff --git a/src/common/state/store-chats.ts b/src/common/state/store-chats.ts index c7f8a7e07..dad2fddac 100644 --- a/src/common/state/store-chats.ts +++ b/src/common/state/store-chats.ts @@ -565,6 +565,7 @@ export const useConversation = (conversationId: DConversationId | null) => useCh }, shallow); export const useConversationsByFolder = (folderId: string | null) => useChatStore(state => { + console.log('useConversationsByFolder', folderId); if (folderId) { const { conversations } = state; const folder = useFolderStore.getState().folders.find(_f => _f.id === folderId); diff --git a/src/common/state/store-folders.ts b/src/common/state/store-folders.ts index 27a88488a..326a3bbe6 100644 --- a/src/common/state/store-folders.ts +++ b/src/common/state/store-folders.ts @@ -7,6 +7,7 @@ export interface DFolder { id: string; title: string; conversationIds: DConversationId[]; + color?: string; // Optional color property } interface FolderState { @@ -19,6 +20,10 @@ interface FolderActions { deleteFolder: (folderId: string) => void; addConversationToFolder: (folderId: string, conversationId: DConversationId) => void; removeConversationFromFolder: (folderId: string, conversationId: DConversationId) => void; + moveFolder: (fromIndex: number, toIndex: number) => void; // New action to reorder folders + setFolders: (folders: DFolder[]) => void; // New action to set folders state directly + setFolderColor: (folderId: string, color: string) => void; // New action to set folder color + } type FolderStore = FolderState & FolderActions; @@ -35,6 +40,7 @@ export const useFolderStore = create()(devtools( conversationIds: [], }; + set(state => ({ folders: [...state.folders, newFolder], })); @@ -51,7 +57,6 @@ export const useFolderStore = create()(devtools( ), })); }, - deleteFolder: (folderId: string): void => { set(state => ({ @@ -78,6 +83,31 @@ export const useFolderStore = create()(devtools( ), })); }, + + moveFolder: (fromIndex: number, toIndex: number): void => { + set(state => { + const newFolders = Array.from(state.folders); + const [movedFolder] = newFolders.splice(fromIndex, 1); + newFolders.splice(toIndex, 0, movedFolder); + return { folders: newFolders }; + }); + }, + + setFolders: (folders: DFolder[]): void => { + set({ folders }); + }, + + setFolderColor: (folderId: string, color: string): void => { + set(state => ({ + folders: state.folders.map(folder => + folder.id === folderId + ? { ...folder, color } + : folder + ), + })); + }, + + }), { name: 'app-folders', From a93d252efde46635dfd60de8546f269af8f9de70 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Mon, 25 Dec 2023 00:27:20 +0100 Subject: [PATCH 11/26] Added ChatNavigationItemMemo again --- .../components/applayout/ChatDrawerItems.tsx | 46 ++-- .../applayout/ChatNavigationFolders.tsx | 250 ------------------ .../chat/components/applayout/ChatSidebar.tsx | 70 +++-- src/common/state/store-chats.ts | 4 +- 4 files changed, 75 insertions(+), 295 deletions(-) delete mode 100644 src/apps/chat/components/applayout/ChatNavigationFolders.tsx diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 2f772465d..543d6cb42 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { shallow } from 'zustand/shallow'; -import { Box, IconButton, Input, List, ListDivider, ListItem, ListItemButton, ListItemDecorator, MenuItem, Sheet, Typography, useTheme } from '@mui/joy'; +import { Avatar, Box, IconButton, Input, InputProps, List, ListDivider, ListItem, ListItemButton, ListItemDecorator, MenuItem, Sheet, Typography, useTheme } from '@mui/joy'; import AddIcon from '@mui/icons-material/Add'; import SearchIcon from '@mui/icons-material/Search'; @@ -16,12 +16,10 @@ import { useUIPreferencesStore } from '~/common/state/store-ui'; import { useUXLabsStore } from '~/common/state/store-ux-labs'; import { ChatNavigationItemMemo } from './ChatNavigationItem'; -import { ChatNavigationFolders } from './ChatNavigationFolders'; import ChatSidebar from './ChatSidebar'; import { useFolderStore } from '~/common/state/store-folders'; -import { NoSSR } from '~/common/components/NoSSR'; import { CSSProperties, useEffect, useRef, useState } from 'react'; @@ -102,6 +100,8 @@ function ChatDrawerItems(props: { const listRef = useRef(null); const [showDownArrow, setShowDownArrow] = useState(false); const [showUpArrow, setShowUpArrow] = useState(false); + + const theme = useTheme(); @@ -175,28 +175,16 @@ function ChatDrawerItems(props: {
{conversations.map((conversation, index) => ( - - handleConversationActivate(conversation.id, false)} - sx={{ - // py: 0, - position: 'relative', - border: 'none', // note, there's a default border of 1px and invisible.. hmm - '&:hover > button': { opacity: 1 }, - ...(conversation.id === props.activeConversationId ? { bgcolor: 'red' } : { }), - }} - > - { conversation.autoTitle && ( - - {conversation.autoTitle} - - )} - - - - + ))}
@@ -262,7 +250,13 @@ function ChatDrawerItems(props: { {/* Search input for conversations */} - }> + + + } + sx={{ mb: 2 }} + placeholder='Filter by title' + /> diff --git a/src/apps/chat/components/applayout/ChatNavigationFolders.tsx b/src/apps/chat/components/applayout/ChatNavigationFolders.tsx deleted file mode 100644 index f69e30efc..000000000 --- a/src/apps/chat/components/applayout/ChatNavigationFolders.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import React from 'react'; -import { DFolder, useFolderStore } from '~/common/state/store-folders'; -import { Box, List, IconButton, ListItem, ListItemButton, ListItemDecorator, ListItemContent } from '@mui/joy'; -import FolderIcon from '@mui/icons-material/Folder'; -import AddBoxIcon from '@mui/icons-material/AddBox'; -import CloseIcon from '@mui/icons-material/Close'; -import EditIcon from '@mui/icons-material/Edit'; -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import { DConversation, DConversationId } from '~/common/state/store-chats'; -import { ChatNavigationItemMemo } from './ChatNavigationItem'; - -export const ChatNavigationFolders = ({ - onFolderSelect, - onFolderCreate, - folders, - selectedFolderId, - activeConversationId, - isLonely, - maxChatMessages, - showSymbols, - onConversationActivate, - onConversationDelete, - conversationsByFolder, - -}: { - onFolderSelect: (folderId: string | null) => void, - onFolderCreate: (folderTitle: string) => void, - folders: DFolder[], // Add this prop - selectedFolderId: string | null, // Add this prop - activeConversationId: string | null, - isLonely: boolean, - maxChatMessages: number, - showSymbols: boolean, - onConversationActivate: (conversationId: DConversationId, closeMenu: boolean) => void, - onConversationDelete: (conversationId: DConversationId) => void, - conversationsByFolder: DConversation[], // Add this prop -}) => { - - // Add a constant for the default folder title - const DEFAULT_FOLDER_TITLE = 'General'; - - const deleteFolder = useFolderStore((state) => state.deleteFolder); - const updateFolderName = useFolderStore((state) => state.updateFolderName); - const [deleteArmedFolderId, setDeleteArmedFolderId] = React.useState(null); - const [editingFolderId, setEditingFolderId] = React.useState(null); - const [editingFolderName, setEditingFolderName] = React.useState(''); - - const handleFolderSelect = (folderId: string | null) => { - onFolderSelect(folderId); - }; - - const handleFolderCreate = () => { - const folderTitle = prompt('Enter folder name:'); - if (folderTitle) { - onFolderCreate(folderTitle); - } - }; - - - const handleFolderDelete = (folderId: string, event: React.MouseEvent) => { - event.stopPropagation(); - setDeleteArmedFolderId(folderId); // Arm the deletion instead of confirming immediately - }; - - const handleConfirmDelete = (folderId: string) => { - deleteFolder(folderId); - if (selectedFolderId === folderId) { - onFolderSelect(null); - } - setDeleteArmedFolderId(null); // Disarm the deletion - }; - - const handleCancelDelete = () => { - setDeleteArmedFolderId(null); // Disarm the deletion - }; - - const handleSaveFolder = (folderId: string) => { - if (editingFolderName.trim() !== '') { - // Call a function to update the folder name in your state management - updateFolderName(folderId, editingFolderName.trim()); - } - setEditingFolderId(null); // Exit edit mode - }; - - const handleCancelEdit = () => { - setEditingFolderId(null); // Exit edit mode without saving - setEditingFolderName(''); // Reset editing name - }; - - const handleEdit = (event: React.MouseEvent, folderId: string, folderTitle: string) => { - event.stopPropagation(); // Prevent the ListItemButton's onClick from firing - - setEditingFolderId(folderId); - setEditingFolderName(folderTitle); - }; - - - - - - return ( - - - {/* Add a ListItem for the default folder */} - - handleFolderSelect(null)} - selected={selectedFolderId === null} - sx={{ - justifyContent: 'space-between', - '&:hover .delete-icon': { - visibility: 'hidden', // Hide delete icon for default folder - }, - }} - > - - - {DEFAULT_FOLDER_TITLE} - - - - - {/* Render the default folder's conversations when selected */} - {selectedFolderId === null && ( - - {conversationsByFolder.map((conversation) => ( - - ))} - - )} - - {folders.map((folder) => ( - - - handleFolderSelect(folder.id)} - selected={folder.id === selectedFolderId} - sx={{ justifyContent: 'space-between', - '&:hover .edit-icon': { - visibility: 'visible', // Show delete icon on hover - } }} - > - - - {editingFolderId !== folder.id ? ( - {folder.title} - ) : ( - handleSaveFolder(folder.id)} - onChange={(e) => { - e.stopPropagation(); // Prevent the ListItemButton's onClick from firing - setEditingFolderName(e.target.value); - }} - onKeyUp={(e) => { - e.stopPropagation(); // Prevent the ListItemButton's onClick from firing - if (e.key === 'Enter') { - handleSaveFolder(folder.id); - } - }} - autoFocus - /> - )} - - {editingFolderId === folder.id && ( - <> - handleSaveFolder(folder.id)}> - - - - )} - - - - {editingFolderId !== folder.id && ( - - handleEdit(event, folder.id, folder.title)}> - - - - )} - - {deleteArmedFolderId !== folder.id && !editingFolderId && ( - - handleFolderDelete(folder.id, event)}> - - - - )} - {/* Confirm/Cancel delete buttons */} - {deleteArmedFolderId === folder.id && ( - - handleConfirmDelete(folder.id)}> - - - - - - - )} - - - - {/* now if selected show conversations */} - {selectedFolderId === folder.id && ( - - {conversationsByFolder.map((conversation) => ( - - ))} - - )} - - - ))} - - - - New Folder - - - - - ); -}; \ No newline at end of file diff --git a/src/apps/chat/components/applayout/ChatSidebar.tsx b/src/apps/chat/components/applayout/ChatSidebar.tsx index 11cdc671f..f864f9b3e 100644 --- a/src/apps/chat/components/applayout/ChatSidebar.tsx +++ b/src/apps/chat/components/applayout/ChatSidebar.tsx @@ -33,20 +33,17 @@ import Done from '@mui/icons-material/Done'; import FolderListItem from './FolderListItem'; import { DConversation } from '~/common/state/store-chats'; -export default function ChatSidebar( - { - onFolderSelect, - folders, - selectedFolderId, - conversationsByFolder, - - }: { - onFolderSelect: (folderId: string | null) => void, - folders: DFolder[], - selectedFolderId: string | null, - conversationsByFolder: DConversation[], - } -) { +export default function ChatSidebar({ + onFolderSelect, + folders, + selectedFolderId, + conversationsByFolder, +}: { + onFolderSelect: (folderId: string | null) => void; + folders: DFolder[]; + selectedFolderId: string | null; + conversationsByFolder: DConversation[]; +}) { const { moveFolder, updateFolderName } = useFolderStore((state) => ({ folders: state.folders, moveFolder: state.moveFolder, @@ -99,20 +96,59 @@ export default function ChatSidebar( } + renderClone={(provided, snapshot, rubric) => ( + + )} > {(provided) => ( + + { + event.stopPropagation(); // Prevent the ListItemButton's onClick from firing + onFolderSelect(null); + }} + selected={selectedFolderId === null} + sx={{ + justifyContent: 'space-between', + '&:hover .menu-icon': { + visibility: 'visible', // Hide delete icon for default folder + }, + }} + > + + + + + + All + + + + {folders.map((folder, index) => ( {(provided, snapshot) => ( - + )} ))} - {provided.placeholder} + {provided.placeholder} )} diff --git a/src/common/state/store-chats.ts b/src/common/state/store-chats.ts index dad2fddac..f08678970 100644 --- a/src/common/state/store-chats.ts +++ b/src/common/state/store-chats.ts @@ -572,6 +572,6 @@ export const useConversationsByFolder = (folderId: string | null) => useChatStor if (folder) return conversations.filter(_c => folder.conversationIds.includes(_c.id)); } - // return only the conversations that are not in any folder - return state.conversations.filter(_c => !useFolderStore.getState().folders.find(_f => _f.conversationIds.includes(_c.id))); + // return all conversations if all folder is selected + return state.conversations; }, shallow); From 401f9c75e165721540896213736902e9dc1ae085 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Mon, 25 Dec 2023 00:48:17 +0100 Subject: [PATCH 12/26] We need more colors --- src/apps/chat/components/applayout/FolderListItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/chat/components/applayout/FolderListItem.tsx b/src/apps/chat/components/applayout/FolderListItem.tsx index 3c5fa1807..fea6615c2 100644 --- a/src/apps/chat/components/applayout/FolderListItem.tsx +++ b/src/apps/chat/components/applayout/FolderListItem.tsx @@ -237,9 +237,9 @@ const FolderListItem: React.FC = ({ folder, provided, snapshot, aria-labelledby="product-color-attribute" defaultValue={folder.color || 'warning'} onChange={handleColorChange} - sx={{ gap: 2, flexWrap: 'wrap', flexDirection: 'row' }} + sx={{ gap: 2, flexWrap: 'wrap', flexDirection: 'row', maxWidth: 180 }} > - {(['#01B80A', '#F0CB00', '#C001D7', '#0093E8', '#E03C00'] as const).map((color, index) => ( + {(["#ff0000","#ff8700","#ffd300","#deff0a","#a1ff0a", "#8A0000", "#8A3700", "#8A5700", "#7C6A05", "#626906","#0aff99","#0aefff","#147df5","#580aff","#be0aff", "#226D40","#22656D","#25346A","#440669","#6E0569"] as const).map((color, index) => ( Date: Mon, 25 Dec 2023 15:06:39 +0100 Subject: [PATCH 13/26] ConversationList as a new file --- .../components/applayout/ChatDrawerItems.tsx | 151 ++++-------------- .../components/applayout/ConversationList.tsx | 147 +++++++++++++++++ 2 files changed, 179 insertions(+), 119 deletions(-) create mode 100644 src/apps/chat/components/applayout/ConversationList.tsx diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 543d6cb42..83bb54fe8 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -19,8 +19,10 @@ import { ChatNavigationItemMemo } from './ChatNavigationItem'; import ChatSidebar from './ChatSidebar'; +import { ConversationList } from './ConversationList'; + import { useFolderStore } from '~/common/state/store-folders'; -import { CSSProperties, useEffect, useRef, useState } from 'react'; +import { CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from 'react'; // type ListGrouping = 'off' | 'persona'; @@ -43,6 +45,8 @@ function ChatDrawerItems(props: { // local state const { onConversationDelete, onConversationNew, onConversationActivate } = props; + const [searchTerm, setSearchTerm] = useState(''); + // const [grouping] = React.useState('off'); // Add state to track the selected folder @@ -96,124 +100,18 @@ function ChatDrawerItems(props: { !singleChat && conversationId && onConversationDelete(conversationId, true); }, [onConversationDelete, singleChat]); - const ConversationList = ({ conversations }: { conversations: DConversation[] }) => { - const listRef = useRef(null); - const [showDownArrow, setShowDownArrow] = useState(false); - const [showUpArrow, setShowUpArrow] = useState(false); - + // Memoized event handler for the search input change + const handleSearchChange = useCallback((event: React.ChangeEvent) => { + setSearchTerm(event.target.value.toLowerCase()); + }, []); + // Filter conversations based on the search term + const filteredConversations = useMemo(() => conversations.filter((conversation) => + conversation.userTitle?.toLowerCase().includes(searchTerm) || + conversation.autoTitle?.toLowerCase().includes(searchTerm) + ), [conversations, searchTerm]); - const theme = useTheme(); - - const checkForOverflow = () => { - const currentList = listRef.current; - if (currentList) { - const isOverflowing = currentList.scrollHeight > currentList.clientHeight; - setShowDownArrow(isOverflowing); - setShowUpArrow(false); // Initially, we don't want to show the up arrow - } - }; - - const checkScrollPosition = () => { - const currentList = listRef.current; - if (currentList) { - const isAtBottom = currentList.scrollHeight - currentList.scrollTop === currentList.clientHeight; - const isAtTop = currentList.scrollTop === 0; - setShowDownArrow(!isAtBottom); - setShowUpArrow(!isAtTop); - } - }; - - useEffect(() => { - checkForOverflow(); - window.addEventListener('resize', checkForOverflow); - - // Add scroll event listener - const currentList = listRef.current; - if (currentList) { - currentList.addEventListener('scroll', checkScrollPosition); - } - - return () => { - window.removeEventListener('resize', checkForOverflow); - // Remove scroll event listener - if (currentList) { - currentList.removeEventListener('scroll', checkScrollPosition); - } - }; - }, [conversations]); - - const styles: { container: CSSProperties; listContainer: CSSProperties; arrow: CSSProperties, arrowDown: CSSProperties, arrowUp: CSSProperties } = { - container: { - position: 'relative', // Container for both list and arrows - maxHeight: '200px', - }, - listContainer: { - maxHeight: '200px', - overflow: 'auto', - }, - arrow: { - position: 'absolute', - left: '50%', - transform: 'translateX(-50%)', - color: theme.palette.text.secondary, - opacity: 0.7, - fontSize: '30px', - }, - arrowDown: { - bottom: 0, - }, - arrowUp: { - top: 10, - // rotate arrow 180 degrees - transform: 'translateX(-50%) rotate(180deg)', - }, - }; - - return ( -
-
- - {conversations.map((conversation, index) => ( - - ))} - -
- {showUpArrow && ( - { - listRef.current?.scrollTo({ - top: 0, - behavior: 'smooth', - }); - }} - /> - )} - {showDownArrow && ( - { - listRef.current?.scrollTo({ - top: listRef.current?.scrollHeight, - behavior: 'smooth', - }); - }} - /> - )} -
- ); - }; - + @@ -253,13 +151,28 @@ function ChatDrawerItems(props: { } sx={{ mb: 2 }} placeholder='Filter by title' + value={searchTerm} + onChange={handleSearchChange} /> - - +
diff --git a/src/apps/chat/components/applayout/ConversationList.tsx b/src/apps/chat/components/applayout/ConversationList.tsx new file mode 100644 index 000000000..25ac3d35a --- /dev/null +++ b/src/apps/chat/components/applayout/ConversationList.tsx @@ -0,0 +1,147 @@ +import React, { CSSProperties, useEffect, useRef, useState } from 'react'; +import { List, useTheme } from '@mui/joy'; +import { DConversation, DConversationId } from '~/common/state/store-chats'; +import { ChatNavigationItemMemo } from './ChatNavigationItem'; +import ExpandCircleDownIcon from '@mui/icons-material/ExpandCircleDown'; + + +// Define the ConversationList component with its own props +export const ConversationList = (props: { + conversations: DConversation[], + activeConversationId: DConversationId | null, + disableNewButton: boolean, + onConversationActivate: (conversationId: DConversationId, closeMenu: boolean) => void, + onConversationDelete: (conversationId: DConversationId) => void, + onConversationImportDialog: () => void, + onConversationNew: () => DConversationId, + onConversationsDeleteAll: () => void, + selectedFolderId: string | null, + setSelectedFolderId: (folderId: string | null) => void, + labsEnhancedUI: boolean, + showSymbols: boolean, + }) => { + const listRef = useRef(null); + const [showDownArrow, setShowDownArrow] = useState(false); + const [showUpArrow, setShowUpArrow] = useState(false); + + const theme = useTheme(); + + + // derived state + const maxChatMessages = props.conversations.reduce((longest, _c) => Math.max(longest, _c.messages.length), 1); + const totalConversations = props.conversations.length; + const hasChats = totalConversations > 0; + const singleChat = totalConversations === 1; + const softMaxReached = totalConversations >= 50; + + const checkForOverflow = () => { + const currentList = listRef.current; + if (currentList) { + const isOverflowing = currentList.scrollHeight > currentList.clientHeight; + setShowDownArrow(isOverflowing); + setShowUpArrow(false); // Initially, we don't want to show the up arrow + } + }; + + const checkScrollPosition = () => { + const currentList = listRef.current; + if (currentList) { + const isAtBottom = currentList.scrollHeight - currentList.scrollTop === currentList.clientHeight; + const isAtTop = currentList.scrollTop === 0; + setShowDownArrow(!isAtBottom); + setShowUpArrow(!isAtTop); + } + }; + + useEffect(() => { + checkForOverflow(); + window.addEventListener('resize', checkForOverflow); + + // Add scroll event listener + const currentList = listRef.current; + if (currentList) { + currentList.addEventListener('scroll', checkScrollPosition); + } + + return () => { + window.removeEventListener('resize', checkForOverflow); + // Remove scroll event listener + if (currentList) { + currentList.removeEventListener('scroll', checkScrollPosition); + } + }; + }, [props.conversations]); + + const styles: { container: CSSProperties; listContainer: CSSProperties; arrow: CSSProperties, arrowDown: CSSProperties, arrowUp: CSSProperties } = { + container: { + position: 'relative', // Container for both list and arrows + maxHeight: '200px', + }, + listContainer: { + maxHeight: '200px', + overflow: 'auto', + }, + arrow: { + position: 'absolute', + left: '50%', + transform: 'translateX(-50%)', + color: theme.palette.text.secondary, + opacity: 0.7, + fontSize: '30px', + }, + arrowDown: { + bottom: 0, + }, + arrowUp: { + top: 10, + // rotate arrow 180 degrees + transform: 'translateX(-50%) rotate(180deg)', + }, + }; + + return ( +
+
+ + {props.conversations.map((conversation, index) => ( + + ))} + +
+ {showUpArrow && ( + { + listRef.current?.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }} + /> + )} + {showDownArrow && ( + { + listRef.current?.scrollTo({ + top: listRef.current?.scrollHeight, + behavior: 'smooth', + }); + }} + /> + )} +
+ ); + +}; + +export default React.memo(ConversationList); From 8dffdd5b349eb81aee05782a706d562dfec82f27 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Mon, 25 Dec 2023 15:34:07 +0100 Subject: [PATCH 14/26] Add New Folder button --- .../chat/components/applayout/ChatSidebar.tsx | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/apps/chat/components/applayout/ChatSidebar.tsx b/src/apps/chat/components/applayout/ChatSidebar.tsx index f864f9b3e..7b3e92788 100644 --- a/src/apps/chat/components/applayout/ChatSidebar.tsx +++ b/src/apps/chat/components/applayout/ChatSidebar.tsx @@ -5,6 +5,7 @@ import ListItem from '@mui/joy/ListItem'; import ListItemButton from '@mui/joy/ListItemButton'; import ListItemContent from '@mui/joy/ListItemContent'; import ListItemDecorator from '@mui/joy/ListItemDecorator'; +import AddIcon from '@mui/icons-material/Add'; import IconButton from '@mui/joy/IconButton'; import Box from '@mui/joy/Box'; import Sheet, { sheetClasses } from '@mui/joy/Sheet'; @@ -24,7 +25,7 @@ import { DraggableStateSnapshot, } from 'react-beautiful-dnd'; import OutlineFolderIcon from '@mui/icons-material/Folder'; -import { Dropdown, FormLabel, Menu, MenuButton, MenuItem, RadioGroup } from '@mui/joy'; +import { Button, Dropdown, FormLabel, Menu, MenuButton, MenuItem, RadioGroup } from '@mui/joy'; import MoreVert from '@mui/icons-material/MoreVert'; import EditIcon from '@mui/icons-material/Edit'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; @@ -37,13 +38,30 @@ export default function ChatSidebar({ onFolderSelect, folders, selectedFolderId, - conversationsByFolder, }: { onFolderSelect: (folderId: string | null) => void; folders: DFolder[]; selectedFolderId: string | null; conversationsByFolder: DConversation[]; }) { + + + // external state + // Get the createFolder action from the store + const { createFolder } = useFolderStore((state) => ({ + createFolder: state.createFolder, + })); + + // The function to call when the "Add Folder" button is clicked + const handleAddFolderClick = () => { + // Prompt the user for a folder name + const title = prompt('Enter folder name:'); + if (title) { + createFolder(title); + } + }; + + const { moveFolder, updateFolderName } = useFolderStore((state) => ({ folders: state.folders, moveFolder: state.moveFolder, @@ -60,7 +78,6 @@ export default function ChatSidebar({ Folders - ({ @@ -155,6 +172,12 @@ export default function ChatSidebar({
+ {/* right aligned button */} + ); } From 43579efa216a3a7a31136cab7ae673a3285504eb Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Mon, 25 Dec 2023 15:57:45 +0100 Subject: [PATCH 15/26] Enable Deletion of Folders --- .../components/applayout/FolderListItem.tsx | 99 ++++++++++++++++--- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/src/apps/chat/components/applayout/FolderListItem.tsx b/src/apps/chat/components/applayout/FolderListItem.tsx index fea6615c2..10faf1620 100644 --- a/src/apps/chat/components/applayout/FolderListItem.tsx +++ b/src/apps/chat/components/applayout/FolderListItem.tsx @@ -21,6 +21,7 @@ import OutlineFolderIcon from '@mui/icons-material/Folder'; import MoreVert from '@mui/icons-material/MoreVert'; import EditIcon from '@mui/icons-material/Edit'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import CloseIcon from '@mui/icons-material/Close'; import Done from '@mui/icons-material/Done'; import { DFolder, useFolderStore } from '~/common/state/store-folders'; import { DraggingStyle, NotDraggingStyle } from 'react-beautiful-dnd'; @@ -37,15 +38,20 @@ type RenderItemProps = { const FolderListItem: React.FC = ({ folder, provided, snapshot, onFolderSelect, selectedFolderId }) => { // internal state + const [deleteArmed, setDeleteArmed] = useState(false); const [deleteArmedFolderId, setDeleteArmedFolderId] = useState(null); const [editingFolderId, setEditingFolderId] = useState(null); const [editingFolderName, setEditingFolderName] = useState(''); + // State to control the open state of the Menu + const [menuOpen, setMenuOpen] = useState(false); + // external state - const { folders, moveFolder, updateFolderName } = useFolderStore((state) => ({ + const { folders, moveFolder, updateFolderName, deleteFolder } = useFolderStore((state) => ({ folders: state.folders, moveFolder: state.moveFolder, updateFolderName: state.updateFolderName, + deleteFolder: state.deleteFolder, })); const { setFolderColor } = useFolderStore((state) => ({ @@ -88,6 +94,36 @@ const FolderListItem: React.FC = ({ folder, provided, snapshot, setEditingFolderName(''); // Reset editing name }; + // Modified handler to arm the delete action and keep the menu open + const handleDeleteButtonShow = (event: React.MouseEvent) => { + event.stopPropagation(); + setDeleteArmed(true); + setMenuOpen(true); // Keep the menu open + }; + + // Handler to close the menu + const handleCloseMenu = () => { + setMenuOpen(false); + setDeleteArmed(false); // Reset delete armed state + }; + + // Handler to disarm the delete action + const handleDeleteButtonHide = () => setDeleteArmed(false); + + // Handler to delete the folder + const handleDeleteConfirmed = (event: React.MouseEvent) => { + if (deleteArmed) { + setDeleteArmed(false); + event.stopPropagation(); + deleteFolder(folder.id); + } + }; + + // Toggle the menu's open state + const toggleMenu = () => { + setMenuOpen(!menuOpen); + }; + const getItemStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({ userSelect: 'none', borderRadius: '8px', @@ -117,12 +153,10 @@ const FolderListItem: React.FC = ({ folder, provided, snapshot, }), }); - const handleFolderSelect = (folderId: string | null) => { onFolderSelect(folderId); }; - return ( = ({ folder, provided, snapshot, { - event.stopPropagation(); // Prevent the ListItemButton's onClick from firing - console.log('folder.id', folder.id); - handleFolderSelect(folder.id); + event.stopPropagation(); // Prevent the ListItemButton's onClick from firing + console.log('folder.id', folder.id); + handleFolderSelect(folder.id); }} selected={folder.id === selectedFolderId} - sx={{ justifyContent: 'space-between', '&:hover .menu-icon': { @@ -194,24 +227,39 @@ const FolderListItem: React.FC = ({ folder, provided, snapshot, sx={{ visibility: 'hidden' }} slots={{ root: IconButton }} slotProps={{ root: { variant: 'outlined', color: 'neutral' } }} - + onClick={toggleMenu} > - + { event.stopPropagation(); // Prevent the ListItemButton's onClick from firing + handleCloseMenu(); setEditingFolderId(folder.id); }} > Edit - - - Delete - + {!deleteArmed ? ( + + + Delete + + ) : ( + <> + + + Confirm Delete + + + + Cancel + + + )} + = ({ folder, provided, snapshot, onChange={handleColorChange} sx={{ gap: 2, flexWrap: 'wrap', flexDirection: 'row', maxWidth: 180 }} > - {(["#ff0000","#ff8700","#ffd300","#deff0a","#a1ff0a", "#8A0000", "#8A3700", "#8A5700", "#7C6A05", "#626906","#0aff99","#0aefff","#147df5","#580aff","#be0aff", "#226D40","#22656D","#25346A","#440669","#6E0569"] as const).map((color, index) => ( + {( + [ + '#ff0000', + '#ff8700', + '#ffd300', + '#deff0a', + '#a1ff0a', + '#8A0000', + '#8A3700', + '#8A5700', + '#7C6A05', + '#626906', + '#0aff99', + '#0aefff', + '#147df5', + '#580aff', + '#be0aff', + '#226D40', + '#22656D', + '#25346A', + '#440669', + '#6E0569', + ] as const + ).map((color, index) => ( Date: Mon, 25 Dec 2023 21:37:18 +0100 Subject: [PATCH 16/26] aider: Modified delete all conversations function to only delete conversations based on selected folder filter. # Aider chat conversation: USER: in ChatDrawerItems.tsx in the menu item props.onConversationsDeleteAll is called to delete all conversations. It finally calls wipeAllConversations. However with the new Folder filter (const conversations = useConversationsByFolder(selectedFolderId)) it shows the exact number (Delete {totalConversations >= 2 ? `all ${totalConversations} chats` : 'chat'}), but in the delete function it does not take care about the selected filter. Please change it to only delete the one based on the selectedFolderId. ASSISTANT: To make this change, we need to modify `src/apps/chat/components/applayout/ChatDrawerItems.tsx` and `src/common/state/store-chats.ts`. Here are the steps: 1. In `src/apps/chat/components/applayout/ChatDrawerItems.tsx`, we need to pass `selectedFolderId` to `onConversationsDeleteAll`. 2. In `src/common/state/store-chats.ts`, we need to modify `wipeAllConversations` to delete only the conversations in the selected folder. Here are the *SEARCH/REPLACE* blocks: src/apps/chat/components/applayout/ChatDrawerItems.tsx ```python <<<<<<< SEARCH ======= props.onConversationsDeleteAll(selectedFolderId)}> >>>>>>> REPLACE ``` src/common/state/store-chats.ts ```python <<<<<<< SEARCH wipeAllConversations: (personaId: SystemPurposeId | undefined): DConversationId => { const { conversations } = _get(); // abort any pending requests on all conversations conversations.forEach(conversation => conversation.abortController?.abort()); const conversation = createDConversation(personaId); _set({ conversations: [conversation], }); return conversation.id; }, ======= wipeAllConversations: (personaId: SystemPurposeId | undefined, folderId: string | null): DConversationId => { let { conversations } = _get(); // If a folder is selected, only delete conversations in that folder if (folderId) { const folderConversations = useFolderStore.getState().folders.find(folder => folder.id === folderId)?.conversationIds || []; conversations = conversations.filter(conversation => folderConversations.includes(conversation.id)); } // abort any pending requests on all conversations conversations.forEach(conversation => conversation.abortController?.abort()); const conversation = createDConversation(personaId); _set({ conversations: folderId ? conversations : [conversation], }); return conversation.id; }, >>>>>>> REPLACE ``` Please confirm these changes. --- .../chat/components/applayout/ChatDrawerItems.tsx | 4 ++-- src/common/state/store-chats.ts | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 83bb54fe8..a07abe993 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -193,7 +193,7 @@ function ChatDrawerItems(props: { - + props.onConversationsDeleteAll(selectedFolderId)}> Delete {totalConversations >= 2 ? `all ${totalConversations} chats` : 'chat'} @@ -201,4 +201,4 @@ function ChatDrawerItems(props: { ; -} \ No newline at end of file +} diff --git a/src/common/state/store-chats.ts b/src/common/state/store-chats.ts index f08678970..635e6864a 100644 --- a/src/common/state/store-chats.ts +++ b/src/common/state/store-chats.ts @@ -255,8 +255,14 @@ export const useChatStore = create()(devtools( : null; }, - wipeAllConversations: (personaId: SystemPurposeId | undefined): DConversationId => { - const { conversations } = _get(); + wipeAllConversations: (personaId: SystemPurposeId | undefined, folderId: string | null): DConversationId => { + let { conversations } = _get(); + + // If a folder is selected, only delete conversations in that folder + if (folderId) { + const folderConversations = useFolderStore.getState().folders.find(folder => folder.id === folderId)?.conversationIds || []; + conversations = conversations.filter(conversation => folderConversations.includes(conversation.id)); + } // abort any pending requests on all conversations conversations.forEach(conversation => conversation.abortController?.abort()); @@ -264,7 +270,7 @@ export const useChatStore = create()(devtools( const conversation = createDConversation(personaId); _set({ - conversations: [conversation], + conversations: folderId ? conversations : [conversation], }); return conversation.id; From d2192abb05bebcfe102d2563863e180c7db978fa Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Mon, 25 Dec 2023 21:41:07 +0100 Subject: [PATCH 17/26] Modified delete all conversations --- src/apps/chat/components/applayout/ChatDrawerItems.tsx | 2 +- src/apps/chat/components/applayout/ConversationList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index a07abe993..31c6d49c8 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -36,7 +36,7 @@ function ChatDrawerItems(props: { onConversationDelete: (conversationId: DConversationId, bypassConfirmation: boolean) => void, onConversationImportDialog: () => void, onConversationNew: () => DConversationId, - onConversationsDeleteAll: () => void, + onConversationsDeleteAll: (folderId: string | null) => void, selectedFolderId: string | null, setSelectedFolderId: (folderId: string | null) => void, }) { diff --git a/src/apps/chat/components/applayout/ConversationList.tsx b/src/apps/chat/components/applayout/ConversationList.tsx index 25ac3d35a..45eacbc33 100644 --- a/src/apps/chat/components/applayout/ConversationList.tsx +++ b/src/apps/chat/components/applayout/ConversationList.tsx @@ -14,7 +14,7 @@ export const ConversationList = (props: { onConversationDelete: (conversationId: DConversationId) => void, onConversationImportDialog: () => void, onConversationNew: () => DConversationId, - onConversationsDeleteAll: () => void, + onConversationsDeleteAll: (folderId: string | null) => void, selectedFolderId: string | null, setSelectedFolderId: (folderId: string | null) => void, labsEnhancedUI: boolean, From d133d04f773ac1ef57aaf8cf1ff4d4d5e18e09dc Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Tue, 26 Dec 2023 08:28:30 +0100 Subject: [PATCH 18/26] Delete All dialog folder aware --- src/apps/chat/AppChat.tsx | 14 ++++++++++---- src/common/state/store-chats.ts | 27 +++++++++++++++++---------- src/common/state/store-folders.ts | 23 ++++++++++++++--------- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index ea058d24b..370c733a3 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -335,15 +335,16 @@ const handleConversationNew = React.useCallback(() => { if (deleteConversationId) { let nextConversationId: DConversationId | null; if (deleteConversationId === SPECIAL_ID_WIPE_ALL) - nextConversationId = wipeAllConversations(focusedSystemPurposeId ?? undefined); + nextConversationId = wipeAllConversations(focusedSystemPurposeId ?? undefined, selectedFolderId); else nextConversationId = deleteConversation(deleteConversationId); setFocusedConversationId(nextConversationId); setDeleteConversationId(null); } }; + - const handleConversationsDeleteAll = () => setDeleteConversationId(SPECIAL_ID_WIPE_ALL); + const handleConversationsDeleteAll = (folderId: string | null) => setDeleteConversationId(SPECIAL_ID_WIPE_ALL); const handleConversationDelete = React.useCallback((conversationId: DConversationId, bypassConfirmation: boolean) => { if (bypassConfirmation) @@ -411,6 +412,11 @@ const handleConversationNew = React.useCallback(() => { [areChatsEmpty, focusedConversationId, handleConversationBranch, isFocusedChatEmpty, isMessageSelectionMode], ); + const conversationsToDeleteCount = selectedFolderId + ? useFolderStore.getState().folders.find(folder => folder.id === selectedFolderId)?.conversationIds.length || 0 + : conversations.length; + + useLayoutPluggable(centerItems, drawerItems, menuItems); return <> @@ -510,10 +516,10 @@ const handleConversationNew = React.useCallback(() => { {!!deleteConversationId && setDeleteConversationId(null)} onPositive={handleConfirmedDeleteConversation} confirmationText={deleteConversationId === SPECIAL_ID_WIPE_ALL - ? 'Are you absolutely sure you want to delete ALL conversations? This action cannot be undone.' + ? `Are you absolutely sure you want to delete ${selectedFolderId ? 'ALL conversations in this folder' : 'ALL conversations'}? This action cannot be undone.` : 'Are you sure you want to delete this conversation?'} positiveActionText={deleteConversationId === SPECIAL_ID_WIPE_ALL - ? 'Yes, delete all' + ? `Yes, delete all ${conversationsToDeleteCount} conversations` : 'Delete conversation'} />} diff --git a/src/common/state/store-chats.ts b/src/common/state/store-chats.ts index 635e6864a..63aa444c0 100644 --- a/src/common/state/store-chats.ts +++ b/src/common/state/store-chats.ts @@ -120,7 +120,7 @@ interface ChatActions { importConversation: (conversation: DConversation, preventClash: boolean) => DConversationId; branchConversation: (conversationId: DConversationId, messageId: string | null) => DConversationId | null; deleteConversation: (conversationId: DConversationId) => DConversationId | null; - wipeAllConversations: (personaId: SystemPurposeId | undefined) => DConversationId; + wipeAllConversations: (personaId: SystemPurposeId | undefined, folderId: string | null) => DConversationId; // within a conversation startTyping: (conversationId: string, abortController: AbortController | null) => void; @@ -257,24 +257,31 @@ export const useChatStore = create()(devtools( wipeAllConversations: (personaId: SystemPurposeId | undefined, folderId: string | null): DConversationId => { let { conversations } = _get(); - + // If a folder is selected, only delete conversations in that folder if (folderId) { - const folderConversations = useFolderStore.getState().folders.find(folder => folder.id === folderId)?.conversationIds || []; - conversations = conversations.filter(conversation => folderConversations.includes(conversation.id)); + const folderStore = useFolderStore.getState(); + const folderConversations = folderStore.folders.find(folder => folder.id === folderId)?.conversationIds || []; + conversations = conversations.filter(conversation => !folderConversations.includes(conversation.id)); + + // Update the folder to remove the deleted conversation IDs + // for each conversation in the folder call folderStore.removeConversationFromFolder + folderConversations.forEach(conversationId => folderStore.removeConversationFromFolder(folderId, conversationId)); + + } else { + // abort any pending requests on all conversations + conversations.forEach(conversation => conversation.abortController?.abort()); } - - // abort any pending requests on all conversations - conversations.forEach(conversation => conversation.abortController?.abort()); - + const conversation = createDConversation(personaId); - + _set({ conversations: folderId ? conversations : [conversation], }); - + return conversation.id; }, + // within a conversation diff --git a/src/common/state/store-folders.ts b/src/common/state/store-folders.ts index 326a3bbe6..c99807a74 100644 --- a/src/common/state/store-folders.ts +++ b/src/common/state/store-folders.ts @@ -64,15 +64,20 @@ export const useFolderStore = create()(devtools( })); }, - addConversationToFolder: (folderId: string, conversationId: DConversationId): void => { - set(state => ({ - folders: state.folders.map(folder => - folder.id === folderId - ? { ...folder, conversationIds: [...folder.conversationIds, conversationId] } - : folder - ), - })); - }, + addConversationToFolder: (folderId: string, conversationId: string) => { + set(state => { + const folders = state.folders.map(folder => { + // Check if this is the correct folder and if the conversationId is not already present + if (folder.id === folderId && !folder.conversationIds.includes(conversationId)) { + // Use the spread operator to create a new array with the conversationId added + return { ...folder, conversationIds: [...folder.conversationIds, conversationId] }; + } + return folder; + }); + return { folders }; + }); + }, + removeConversationFromFolder: (folderId: string, conversationId: DConversationId): void => { set(state => ({ From 2f723f12991de1e600bcbf371e971e5ca37295cb Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Tue, 26 Dec 2023 08:41:24 +0100 Subject: [PATCH 19/26] reduced required parameters --- src/apps/chat/components/applayout/ChatDrawerItems.tsx | 4 ---- src/apps/chat/components/applayout/ConversationList.tsx | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 31c6d49c8..32b8b2216 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -162,12 +162,8 @@ function ChatDrawerItems(props: { void, onConversationDelete: (conversationId: DConversationId) => void, - onConversationImportDialog: () => void, - onConversationNew: () => DConversationId, - onConversationsDeleteAll: (folderId: string | null) => void, selectedFolderId: string | null, setSelectedFolderId: (folderId: string | null) => void, labsEnhancedUI: boolean, From 768b2a158ca8ee4d4fd982b76147043b5d94f2b5 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Tue, 26 Dec 2023 08:50:12 +0100 Subject: [PATCH 20/26] Enable new button --- src/apps/chat/AppChat.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index 370c733a3..6ae069a9e 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -258,9 +258,12 @@ export function AppChat() { const doesSelectedFolderHaveEmptyChat = (selectedFolderId: string | null, conversations: any[]) => { // If no folder is selected (default folder), check if there is an empty chat globally if (selectedFolderId === null) { - return conversations.some(convo => convo.messages.length === 0); + // get all conversations that are not in a folder + const conversationsNotInFolder = conversations.filter(convo => convo.folderId === null); + return conversationsNotInFolder.some(convo => convo.messages.length === 0); } + // Retrieve the folder's conversations const folderConversations = useFolderStore.getState().folders.find(folder => folder.id === selectedFolderId)?.conversationIds || []; From e7b09e0dac68adafa6533e74d76ab11491f8eca1 Mon Sep 17 00:00:00 2001 From: Joris Kalz Date: Tue, 26 Dec 2023 20:46:01 +0100 Subject: [PATCH 21/26] Add Folder Drop Down to the AppBar --- .../components/applayout/ChatDropdowns.tsx | 6 +++ .../applayout/useFolderDropdown.tsx | 50 +++++++++++++++++++ src/common/layout/AppBarDropdown.tsx | 2 +- 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/apps/chat/components/applayout/useFolderDropdown.tsx diff --git a/src/apps/chat/components/applayout/ChatDropdowns.tsx b/src/apps/chat/components/applayout/ChatDropdowns.tsx index 3d7d0bb6b..760ba8f54 100644 --- a/src/apps/chat/components/applayout/ChatDropdowns.tsx +++ b/src/apps/chat/components/applayout/ChatDropdowns.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; import type { DConversationId } from '~/common/state/store-chats'; +import { DFolder } from '~/common/state/store-folders'; import { useChatLLMDropdown } from './useLLMDropdown'; import { usePersonaIdDropdown } from './usePersonaDropdown'; +import { useFolderDropdown } from './useFolderDropdown'; export function ChatDropdowns(props: { @@ -13,6 +15,7 @@ export function ChatDropdowns(props: { // state const { chatLLMDropdown } = useChatLLMDropdown(); const { personaDropdown } = usePersonaIdDropdown(props.conversationId); + const { folderDropdown } = useFolderDropdown(props.conversationId); return <> @@ -22,5 +25,8 @@ export function ChatDropdowns(props: { {/* Persona selector */} {personaDropdown} + {/* Folder selector */} + {folderDropdown} + ; } diff --git a/src/apps/chat/components/applayout/useFolderDropdown.tsx b/src/apps/chat/components/applayout/useFolderDropdown.tsx new file mode 100644 index 000000000..b0f97fb3d --- /dev/null +++ b/src/apps/chat/components/applayout/useFolderDropdown.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { shallow } from 'zustand/shallow'; +import FolderIcon from '@mui/icons-material/Folder'; +import { useFolderStore } from '~/common/state/store-folders'; +import { AppBarDropdown, DropdownItems } from '~/common/layout/AppBarDropdown'; +import { DConversationId } from '~/common/state/store-chats'; + +export function useFolderDropdown(conversationId: DConversationId | null) { + // Get folders from the store + const folders = useFolderStore(state => state.folders); + + // Prepare items for the dropdown + const folderItems: DropdownItems = folders.reduce((items, folder) => { + items[folder.id] = { + title: folder.title, + icon: , + }; + return items; + }, {} as DropdownItems); + + // Handle folder change + const handleFolderChange = (_event: any, folderId: string | null) => { + if (conversationId && folderId) { + // Remove conversation from all folders + folders.forEach(folder => { + if (folder.conversationIds.includes(conversationId)) { + useFolderStore.getState().removeConversationFromFolder(folder.id, conversationId); + } + }); + // Add conversation to the selected folder + useFolderStore.getState().addConversationToFolder(folderId, conversationId); + } + }; + + // Get the current folder ID for the selected conversation + const currentFolderId = folders.find(folder => folder.conversationIds.includes(conversationId || ''))?.id || null; + + // Create the dropdown component + const folderDropdown = ( + + ); + + return { folderDropdown }; +} \ No newline at end of file diff --git a/src/common/layout/AppBarDropdown.tsx b/src/common/layout/AppBarDropdown.tsx index d0205d9e9..94a66a90d 100644 --- a/src/common/layout/AppBarDropdown.tsx +++ b/src/common/layout/AppBarDropdown.tsx @@ -80,7 +80,7 @@ export function AppBarDropdown(props: { return (