From ec9f3d66610bdaa1cf8d64019cd9c21308b0091e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 2 Jun 2026 23:17:05 -0400 Subject: [PATCH] fix: refine project navigation affordances --- .../src/components/Conversations/Header.tsx | 22 ++- .../Conversations/ProjectConversations.tsx | 129 ++++++++++++++---- .../components/Projects/ProjectWorkspace.tsx | 22 ++- .../src/components/Projects/ProjectsView.tsx | 84 +++++++----- .../UnifiedSidebar/ConversationsSection.tsx | 7 + client/src/locales/en/translation.json | 2 + 6 files changed, 196 insertions(+), 70 deletions(-) diff --git a/client/src/components/Conversations/Header.tsx b/client/src/components/Conversations/Header.tsx index d65dd20e06..fcfa7c927b 100644 --- a/client/src/components/Conversations/Header.tsx +++ b/client/src/components/Conversations/Header.tsx @@ -13,6 +13,7 @@ export interface ChatsHeaderControls { onOrganizationModeChange: (mode: SidebarOrganizationMode) => void; onChatSortByChange: (sortBy: SidebarChatSort) => void; onNewProject: () => void; + onOpenProjects: () => void; } interface ChatsHeaderProps extends ChatsHeaderControls { @@ -78,6 +79,7 @@ const ChatsHeader: FC = ({ onOrganizationModeChange, onChatSortByChange, onNewProject, + onOpenProjects, }) => { const localize = useLocalize(); const menuId = useId(); @@ -144,6 +146,10 @@ const ChatsHeader: FC = ({ ], [chatSortBy, localize, onChatSortByChange, onOrganizationModeChange, organizationMode], ); + const headerLabel = + organizationMode === 'chronological' + ? localize('com_ui_all_chats') + : localize('com_ui_projects'); return (
@@ -153,7 +159,7 @@ const ChatsHeader: FC = ({ type="button" aria-expanded={isExpanded} > - {localize('com_ui_all_chats')} + {headerLabel} = ({ items={dropdownItems} /> + +
); }, ); @@ -165,6 +190,22 @@ const EmptyRow = memo(({ title }: { title: string }) => { EmptyRow.displayName = 'ProjectEmptyRow'; +const ProjectsOverviewRow = memo(({ onOpenProjects }: { onOpenProjects: () => void }) => { + const localize = useLocalize(); + return ( + + ); +}); + +ProjectsOverviewRow.displayName = 'ProjectsOverviewRow'; + const ProjectConversations: FC = ({ mode, chatSortBy, @@ -177,8 +218,11 @@ const ProjectConversations: FC = ({ chatsHeaderControls, }) => { const localize = useLocalize(); + const queryClient = useQueryClient(); const search = useRecoilValue(store.search); + const conversation = useRecoilValue(store.conversationByIndex(0)); const isSmallScreen = useMediaQuery('(max-width: 768px)'); + const { newConversation } = useNewConvo(); const { favorites, isLoading: isFavoritesLoading } = useFavorites(); const showAgentMarketplace = useShowMarketplace(); const favoritesContentKeyRef = useRef(''); @@ -403,6 +447,10 @@ const ProjectConversations: FC = ({ }; } + if (!isSearching) { + items.push({ type: 'projects-overview' }); + } + if (isSearching) { if (isSearchConversationsLoading) { items.push({ type: 'loading', key: 'loading-search-conversations' }); @@ -557,6 +605,9 @@ const ProjectConversations: FC = ({ if (item.type === 'chats-header') { return 'project-chats-header'; } + if (item.type === 'projects-overview') { + return 'project-overview'; + } if (item.type === 'date') { return `project-date-${item.sectionId ?? 'search'}-${item.groupName}-${ item.isFirst ? 'first' : 'next' @@ -601,6 +652,22 @@ const ProjectConversations: FC = ({ containerRef.current?.scrollToPosition(0); }, [containerRef]); + const startProjectChat = useCallback( + (project: TChatProject, event: React.MouseEvent) => { + event.stopPropagation(); + const path = `/c/new?projectId=${encodeURIComponent(project._id)}`; + if (event.ctrlKey || event.metaKey) { + window.open(path, '_blank'); + return; + } + clearMessagesCache(queryClient, conversation?.conversationId); + queryClient.invalidateQueries([QueryKeys.messages]); + newConversation({ template: { chatProjectId: project._id } }); + toggleNav(); + }, + [conversation?.conversationId, newConversation, queryClient, toggleNav], + ); + const toggleSection = useCallback((sectionId: string) => { setExpandedSectionIds((current) => { const next = new Set(current); @@ -746,6 +813,14 @@ const ProjectConversations: FC = ({ ); } + if (item.type === 'projects-overview') { + return ( + + + + ); + } + if (item.type === 'section') { return ( @@ -753,6 +828,7 @@ const ProjectConversations: FC = ({ section={item.section} isExpanded={item.isExpanded} onToggle={() => toggleSection(item.section.id)} + onStartProjectChat={startProjectChat} /> ); @@ -800,6 +876,7 @@ const ProjectConversations: FC = ({ isSmallScreen, setIsChatsExpanded, chatsHeaderControls, + startProjectChat, ], ); diff --git a/client/src/components/Projects/ProjectWorkspace.tsx b/client/src/components/Projects/ProjectWorkspace.tsx index b8d01b60b6..79d43c33c6 100644 --- a/client/src/components/Projects/ProjectWorkspace.tsx +++ b/client/src/components/Projects/ProjectWorkspace.tsx @@ -1,8 +1,10 @@ import { useCallback, useId, useMemo, useState } from 'react'; import * as Ariakit from '@ariakit/react'; import { useRecoilValue } from 'recoil'; +import { useQueryClient } from '@tanstack/react-query'; import { ArrowLeft, ArrowUpDown, Check, Folder, Plus, SlidersHorizontal } from 'lucide-react'; import { useNavigate, useParams } from 'react-router-dom'; +import { QueryKeys } from 'librechat-data-provider'; import type { ConversationListResponse } from 'librechat-data-provider'; import { Button, Spinner, DropdownPopup } from '@librechat/client'; import type { MenuItemProps, RenderProp } from '~/common'; @@ -11,8 +13,14 @@ import { useGetStartupConfig, useProjectQuery, } from '~/data-provider'; -import { useAgentsMap, useLocalize } from '~/hooks'; -import { cn, getDefaultModelSpec, getLocalStorageItems, getModelSpec } from '~/utils'; +import { useAgentsMap, useLocalize, useNewConvo } from '~/hooks'; +import { + cn, + clearMessagesCache, + getDefaultModelSpec, + getLocalStorageItems, + getModelSpec, +} from '~/utils'; import ProjectChatList from './ProjectChatList'; import store from '~/store'; @@ -36,6 +44,7 @@ function renderSortMenuItem(label: string, isSelected: boolean): RenderProp { export default function ProjectWorkspace() { const localize = useLocalize(); const navigate = useNavigate(); + const queryClient = useQueryClient(); const { projectId = '' } = useParams(); const [sortBy, setSortBy] = useState('updatedAt'); const sortMenuId = useId(); @@ -43,6 +52,7 @@ export default function ProjectWorkspace() { const { data: startupConfig } = useGetStartupConfig(); const { data: project, isLoading: isProjectLoading } = useProjectQuery(projectId); const conversation = useRecoilValue(store.conversationByIndex(0)); + const { newConversation } = useNewConvo(); const agentsMap = useAgentsMap({ isAuthenticated: true }); const activeProjectId = project?._id; const activeSettingsLabel = useMemo(() => { @@ -145,10 +155,10 @@ export default function ProjectWorkspace() { if (!activeProjectId) { return; } - navigate(`/c/new?projectId=${encodeURIComponent(activeProjectId)}`, { - state: { focusChat: true }, - }); - }, [activeProjectId, navigate]); + clearMessagesCache(queryClient, conversation?.conversationId); + queryClient.invalidateQueries([QueryKeys.messages]); + newConversation({ template: { chatProjectId: activeProjectId } }); + }, [activeProjectId, conversation?.conversationId, newConversation, queryClient]); if (isProjectLoading) { return ( diff --git a/client/src/components/Projects/ProjectsView.tsx b/client/src/components/Projects/ProjectsView.tsx index 7dacb7ed48..76baaff2df 100644 --- a/client/src/components/Projects/ProjectsView.tsx +++ b/client/src/components/Projects/ProjectsView.tsx @@ -94,18 +94,50 @@ export default function ProjectsView() { return (
-
+

{localize('com_ui_projects')}

- +
+ + {localize('com_ui_sort_by')} + + + + + + } + items={sortMenuItems} + /> + +
-
+
- - - - - } - items={sortMenuItems} - /> +
+ + {localize('com_ui_your_projects')} + +
) : ( -
+
{projects.map((project) => (