Compare commits

...

12 Commits

35 changed files with 219 additions and 294 deletions
+50 -7
View File
@@ -1,11 +1,20 @@
'use client';
import { ActionIcon, Flexbox, Text } from '@lobehub/ui';
import { useClickAway } from 'ahooks';
import { Drawer } from 'antd';
import { cssVar } from 'antd-style';
import { XIcon } from 'lucide-react';
import type { ReactNode, Ref } from 'react';
import { cloneElement, isValidElement, memo, Suspense, useCallback, useState } from 'react';
import {
cloneElement,
isValidElement,
memo,
Suspense,
useCallback,
useImperativeHandle,
useState,
} from 'react';
import { DESKTOP_HEADER_ICON_SMALL_SIZE } from '@/const/layoutTokens';
@@ -14,11 +23,16 @@ import SkeletonList from './components/SkeletonList';
import { OverlayContainerContext } from './OverlayContainer';
import SideBarHeaderLayout from './SideBarHeaderLayout';
export interface SideBarDrawerHandle {
close: () => void;
open: () => void;
}
interface SideBarDrawerProps {
action?: ReactNode;
children?: ReactNode;
onClose: () => void;
open: boolean;
onOpenChange?: (open: boolean) => void;
ref?: Ref<SideBarDrawerHandle>;
subHeader?: ReactNode;
title?: ReactNode;
}
@@ -39,10 +53,39 @@ const setRef = <T,>(ref: Ref<T> | undefined, value: T | null) => {
};
const SideBarDrawer = memo<SideBarDrawerProps>(
({ subHeader, open, onClose, children, title, action }) => {
({ subHeader, onOpenChange, children, title, action, ref }) => {
const size = 280;
const [overlayContainer, setOverlayContainer] = useState<HTMLDivElement | null>(null);
const [internalOpen, setInternalOpen] = useState(false);
const handleOpen = useCallback(() => {
setInternalOpen((prev) => {
if (!prev) onOpenChange?.(true);
return true;
});
}, [onOpenChange]);
const handleClose = useCallback(() => {
setInternalOpen((prev) => {
if (prev) onOpenChange?.(false);
return false;
});
}, [onOpenChange]);
useImperativeHandle(
ref,
() => ({
close: handleClose,
open: handleOpen,
}),
[handleClose, handleOpen],
);
useClickAway(() => {
if (!internalOpen) return;
handleClose();
}, overlayContainer);
const renderDrawerContent = useCallback((node: ReactNode) => {
if (!isValidElement<DrawerRenderNodeProps>(node)) return node;
@@ -67,7 +110,7 @@ const SideBarDrawer = memo<SideBarDrawerProps>(
drawerRender={renderDrawerContent}
getContainer={() => document.querySelector(`#${NAV_PANEL_RIGHT_DRAWER_ID}`)!}
mask={false}
open={open}
open={internalOpen}
placement="left"
size={size}
rootStyle={{
@@ -120,7 +163,7 @@ const SideBarDrawer = memo<SideBarDrawerProps>(
icon={XIcon}
size={DESKTOP_HEADER_ICON_SMALL_SIZE}
style={{ marginInlineEnd: -2 }}
onClick={onClose}
onClick={handleClose}
/>
</>
}
@@ -128,7 +171,7 @@ const SideBarDrawer = memo<SideBarDrawerProps>(
{subHeader}
</>
}
onClose={onClose}
onClose={handleClose}
>
<Suspense
fallback={
@@ -1,11 +1,11 @@
'use client';
import { Flexbox, SearchBar } from '@lobehub/ui';
import { memo, useState } from 'react';
import { memo, type Ref, useState } from 'react';
import { useTranslation } from 'react-i18next';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import SideBarDrawer from '@/features/NavPanel/SideBarDrawer';
import SideBarDrawer, { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import dynamic from '@/libs/next/dynamic';
const Content = dynamic(() => import('./Content'), {
@@ -18,17 +18,16 @@ const Content = dynamic(() => import('./Content'), {
});
interface AllPagesDrawerProps {
onClose: () => void;
open: boolean;
ref?: Ref<SideBarDrawerHandle>;
}
const AllPagesDrawer = memo<AllPagesDrawerProps>(({ open, onClose }) => {
const AllPagesDrawer = memo<AllPagesDrawerProps>(({ ref }) => {
const { t } = useTranslation('file');
const [searchKeyword, setSearchKeyword] = useState('');
return (
<SideBarDrawer
open={open}
ref={ref}
title={t('pageList.title')}
subHeader={
<Flexbox paddingBlock={'0 8px'} paddingInline={8}>
@@ -43,7 +42,6 @@ const AllPagesDrawer = memo<AllPagesDrawerProps>(({ open, onClose }) => {
/>
</Flexbox>
}
onClose={onClose}
>
<Content searchKeyword={searchKeyword} />
</SideBarDrawer>
@@ -10,17 +10,20 @@ import { pageSelectors, usePageStore } from '@/store/page';
import Item from './Item';
interface PageListProps {
onOpenDrawer: () => void;
}
/**
* Show pages filtered by library
*/
const PageList = () => {
const PageList = ({ onOpenDrawer }: PageListProps) => {
const { t } = useTranslation(['file', 'common']);
const [filteredDocuments, hasMore, isLoadingMore, openAllPagesDrawer] = usePageStore((s) => [
const [filteredDocuments, hasMore, isLoadingMore] = usePageStore((s) => [
pageSelectors.getFilteredDocumentsLimited(s),
pageSelectors.hasMoreFilteredDocuments(s),
pageSelectors.isLoadingMoreDocuments(s),
s.openAllPagesDrawer,
]);
return (
@@ -30,11 +33,7 @@ const PageList = () => {
))}
{isLoadingMore && <SkeletonList rows={3} />}
{hasMore && !isLoadingMore && (
<NavItem
icon={MoreHorizontal}
title={t('more', { ns: 'common' })}
onClick={openAllPagesDrawer}
/>
<NavItem icon={MoreHorizontal} title={t('more', { ns: 'common' })} onClick={onOpenDrawer} />
)}
</Flexbox>
);
+6 -7
View File
@@ -1,10 +1,11 @@
'use client';
import { Accordion, AccordionItem, ContextMenuTrigger, Flexbox, Text } from '@lobehub/ui';
import React, { memo, Suspense } from 'react';
import React, { memo, Suspense, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import PageEmpty from '@/features/PageEmpty';
import { pageSelectors, usePageStore } from '@/store/page';
@@ -33,10 +34,8 @@ const Body = memo(() => {
const filteredDocuments = usePageStore(pageSelectors.getFilteredDocumentsLimited);
const searchKeywords = usePageStore((s) => s.searchKeywords);
const dropdownMenu = useDropdownMenu();
const [allPagesDrawerOpen, closeAllPagesDrawer] = usePageStore((s) => [
s.allPagesDrawerOpen,
s.closeAllPagesDrawer,
]);
const drawerRef = useRef<SideBarDrawerHandle>(null);
const openDrawer = useCallback(() => drawerRef.current?.open(), []);
return (
<Flexbox gap={1} paddingInline={4}>
@@ -64,14 +63,14 @@ const Body = memo(() => {
{filteredDocuments.length === 0 ? (
<PageEmpty search={Boolean(searchKeywords.trim())} />
) : (
<List />
<List onOpenDrawer={openDrawer} />
)}
</Flexbox>
)}
</Suspense>
</AccordionItem>
</Accordion>
<AllPagesDrawer open={allPagesDrawerOpen} onClose={closeAllPagesDrawer} />
<AllPagesDrawer ref={drawerRef} />
</Flexbox>
);
});
@@ -1,11 +1,11 @@
'use client';
import { Flexbox, SearchBar } from '@lobehub/ui';
import { memo, useState } from 'react';
import { memo, type Ref, useState } from 'react';
import { useTranslation } from 'react-i18next';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import SideBarDrawer from '@/features/NavPanel/SideBarDrawer';
import SideBarDrawer, { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import dynamic from '@/libs/next/dynamic';
const Content = dynamic(() => import('./Content'), {
@@ -18,17 +18,17 @@ const Content = dynamic(() => import('./Content'), {
});
interface AllTopicsDrawerProps {
onClose: () => void;
open: boolean;
ref?: Ref<SideBarDrawerHandle>;
}
const AllTopicsDrawer = memo<AllTopicsDrawerProps>(({ open, onClose }) => {
const AllTopicsDrawer = memo<AllTopicsDrawerProps>(({ ref }) => {
const { t } = useTranslation('topic');
const [searchKeyword, setSearchKeyword] = useState('');
const [isOpen, setIsOpen] = useState(false);
return (
<SideBarDrawer
open={open}
ref={ref}
title={t('title')}
subHeader={
<Flexbox paddingBlock={'0 8px'} paddingInline={8}>
@@ -43,9 +43,9 @@ const AllTopicsDrawer = memo<AllTopicsDrawerProps>(({ open, onClose }) => {
/>
</Flexbox>
}
onClose={onClose}
onOpenChange={setIsOpen}
>
<Content open={open} searchKeyword={searchKeyword} />
<Content open={isOpen} searchKeyword={searchKeyword} />
</SideBarDrawer>
);
});
@@ -1,11 +1,12 @@
'use client';
import React, { memo } from 'react';
import React, { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import urlJoin from 'url-join';
import EmptyNavItem from '@/features/NavPanel/components/EmptyNavItem';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import { useFetchChatTopics } from '@/hooks/useFetchChatTopics';
import { useQueryRoute } from '@/hooks/useQueryRoute';
import { useChatStore } from '@/store/chat';
@@ -23,17 +24,15 @@ const TopicList = memo(() => {
const topicLength = useChatStore((s) => topicSelectors.currentTopicLength(s));
const isUndefinedTopics = useChatStore((s) => topicSelectors.isUndefinedTopics(s));
const [agentId, allTopicsDrawerOpen, closeAllTopicsDrawer] = useChatStore((s) => [
s.activeAgentId,
s.allTopicsDrawerOpen,
s.closeAllTopicsDrawer,
]);
const agentId = useChatStore((s) => s.activeAgentId);
const { topicGroupMode } = useAgentTopicGroupMode();
const drawerRef = useRef<SideBarDrawerHandle>(null);
const openDrawer = useCallback(() => drawerRef.current?.open(), []);
useFetchChatTopics();
// Show skeleton when current session's topic data is not yet loaded
if (isUndefinedTopics) return <SkeletonList />;
return (
@@ -47,13 +46,13 @@ const TopicList = memo(() => {
/>
)}
{topicGroupMode === 'flat' ? (
<FlatMode />
<FlatMode onOpenDrawer={openDrawer} />
) : topicGroupMode === 'byProject' ? (
<ByProjectMode />
<ByProjectMode onOpenDrawer={openDrawer} />
) : (
<ByTimeMode />
<ByTimeMode onOpenDrawer={openDrawer} />
)}
<AllTopicsDrawer open={allTopicsDrawerOpen} onClose={closeAllTopicsDrawer} />
<AllTopicsDrawer ref={drawerRef} />
</>
);
});
@@ -5,7 +5,13 @@ import { memo } from 'react';
import GroupedAccordion from '../GroupedAccordion';
import GroupItem from './GroupItem';
const ByProjectMode = memo(() => <GroupedAccordion GroupItem={GroupItem} />);
interface ByProjectModeProps {
onOpenDrawer?: () => void;
}
const ByProjectMode = memo<ByProjectModeProps>(({ onOpenDrawer }) => (
<GroupedAccordion GroupItem={GroupItem} onOpenDrawer={onOpenDrawer} />
));
ByProjectMode.displayName = 'ByProjectMode';
@@ -5,7 +5,13 @@ import { memo } from 'react';
import GroupedAccordion from '../GroupedAccordion';
import GroupItem from './GroupItem';
const ByTimeMode = memo(() => <GroupedAccordion GroupItem={GroupItem} />);
interface ByTimeModeProps {
onOpenDrawer?: () => void;
}
const ByTimeMode = memo<ByTimeModeProps>(({ onOpenDrawer }) => (
<GroupedAccordion GroupItem={GroupItem} onOpenDrawer={onOpenDrawer} />
));
ByTimeMode.displayName = 'ByTimeMode';
@@ -17,19 +17,21 @@ import { preferenceSelectors } from '@/store/user/selectors';
import TopicItem from '../../List/Item';
const FlatMode = memo(() => {
interface FlatModeProps {
onOpenDrawer?: () => void;
}
const FlatMode = memo<FlatModeProps>(({ onOpenDrawer }) => {
const { t } = useTranslation('topic');
const topicPageSize = useGlobalStore(systemStatusSelectors.topicPageSize);
const topicSortBy = useUserStore(preferenceSelectors.topicSortBy);
const [activeTopicId, activeThreadId, hasMore, isExpandingPageSize, openAllTopicsDrawer] =
useChatStore((s) => [
s.activeTopicId,
s.activeThreadId,
topicSelectors.hasMoreTopics(s),
topicSelectors.isExpandingPageSize(s),
s.openAllTopicsDrawer,
]);
const [activeTopicId, activeThreadId, hasMore, isExpandingPageSize] = useChatStore((s) => [
s.activeTopicId,
s.activeThreadId,
topicSelectors.hasMoreTopics(s),
topicSelectors.isExpandingPageSize(s),
]);
const activeTopicList = useChatStore(
topicSelectors.displayTopicsForSidebar(topicPageSize, topicSortBy),
@@ -51,8 +53,8 @@ const FlatMode = memo(() => {
/>
))}
{isExpandingPageSize && <SkeletonList rows={3} />}
{hasMore && !isExpandingPageSize && (
<NavItem icon={MoreHorizontal} title={t('loadMore')} onClick={openAllTopicsDrawer} />
{onOpenDrawer && hasMore && !isExpandingPageSize && (
<NavItem icon={MoreHorizontal} title={t('loadMore')} onClick={onOpenDrawer} />
)}
</Flexbox>
);
@@ -26,18 +26,18 @@ export interface GroupItemComponentProps {
interface GroupedAccordionProps {
GroupItem: ComponentType<GroupItemComponentProps>;
onOpenDrawer?: () => void;
}
const GroupedAccordion = memo<GroupedAccordionProps>(({ GroupItem }) => {
const GroupedAccordion = memo<GroupedAccordionProps>(({ GroupItem, onOpenDrawer }) => {
const { t } = useTranslation('topic');
const topicPageSize = useGlobalStore(systemStatusSelectors.topicPageSize);
const topicSortBy = useUserStore(preferenceSelectors.topicSortBy);
const { topicGroupMode } = useAgentTopicGroupMode();
const [hasMore, isExpandingPageSize, openAllTopicsDrawer] = useChatStore((s) => [
const [hasMore, isExpandingPageSize] = useChatStore((s) => [
topicSelectors.hasMoreTopics(s),
topicSelectors.isExpandingPageSize(s),
s.openAllTopicsDrawer,
]);
const [activeTopicId, activeThreadId] = useChatStore((s) => [s.activeTopicId, s.activeThreadId]);
@@ -78,8 +78,8 @@ const GroupedAccordion = memo<GroupedAccordionProps>(({ GroupItem }) => {
))}
</Accordion>
{isExpandingPageSize && <SkeletonList rows={3} />}
{hasMore && !isExpandingPageSize && (
<NavItem icon={MoreHorizontal} title={t('loadMore')} onClick={openAllTopicsDrawer} />
{onOpenDrawer && hasMore && !isExpandingPageSize && (
<NavItem icon={MoreHorizontal} title={t('loadMore')} onClick={onOpenDrawer} />
)}
</Flexbox>
);
@@ -33,7 +33,6 @@ const TopicListContent = memo(() => {
if (isInSearchMode) return <SearchResult />;
// Show skeleton when current session's topic data is not yet loaded
if (isUndefinedTopics) return <SkeletonList />;
return (
@@ -1,11 +1,11 @@
'use client';
import { Flexbox, SearchBar } from '@lobehub/ui';
import { memo, useState } from 'react';
import { memo, type Ref, useState } from 'react';
import { useTranslation } from 'react-i18next';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import SideBarDrawer from '@/features/NavPanel/SideBarDrawer';
import SideBarDrawer, { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import dynamic from '@/libs/next/dynamic';
const Content = dynamic(() => import('./Content'), {
@@ -18,17 +18,17 @@ const Content = dynamic(() => import('./Content'), {
});
interface AllTopicsDrawerProps {
onClose: () => void;
open: boolean;
ref?: Ref<SideBarDrawerHandle>;
}
const AllTopicsDrawer = memo<AllTopicsDrawerProps>(({ open, onClose }) => {
const AllTopicsDrawer = memo<AllTopicsDrawerProps>(({ ref }) => {
const { t } = useTranslation('topic');
const [searchKeyword, setSearchKeyword] = useState('');
const [isOpen, setIsOpen] = useState(false);
return (
<SideBarDrawer
open={open}
ref={ref}
title={t('title')}
subHeader={
<Flexbox paddingBlock={'0 8px'} paddingInline={8}>
@@ -43,9 +43,9 @@ const AllTopicsDrawer = memo<AllTopicsDrawerProps>(({ open, onClose }) => {
/>
</Flexbox>
}
onClose={onClose}
onOpenChange={setIsOpen}
>
<Content open={open} searchKeyword={searchKeyword} />
<Content open={isOpen} searchKeyword={searchKeyword} />
</SideBarDrawer>
);
});
@@ -1,11 +1,12 @@
'use client';
import React, { memo } from 'react';
import React, { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import urlJoin from 'url-join';
import EmptyNavItem from '@/features/NavPanel/components/EmptyNavItem';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import { useFetchChatTopics } from '@/hooks/useFetchChatTopics';
import { useQueryRoute } from '@/hooks/useQueryRoute';
import { useAgentGroupStore } from '@/store/agentGroup';
@@ -24,16 +25,14 @@ const TopicList = memo(() => {
const topicLength = useChatStore((s) => topicSelectors.currentTopicLength(s));
const isUndefinedTopics = useChatStore((s) => topicSelectors.isUndefinedTopics(s));
const activeGroupId = useAgentGroupStore((s) => s.activeGroupId);
const [allTopicsDrawerOpen, closeAllTopicsDrawer] = useChatStore((s) => [
s.allTopicsDrawerOpen,
s.closeAllTopicsDrawer,
]);
const topicGroupMode = useUserStore(preferenceSelectors.topicGroupMode);
const drawerRef = useRef<SideBarDrawerHandle>(null);
const openDrawer = useCallback(() => drawerRef.current?.open(), []);
useFetchChatTopics();
// Show skeleton when current session's topic data is not yet loaded
if (isUndefinedTopics) return <SkeletonList />;
return (
@@ -46,8 +45,12 @@ const TopicList = memo(() => {
}}
/>
)}
{topicGroupMode === 'flat' ? <FlatMode /> : <ByTimeMode />}
<AllTopicsDrawer open={allTopicsDrawerOpen} onClose={closeAllTopicsDrawer} />
{topicGroupMode === 'flat' ? (
<FlatMode onOpenDrawer={openDrawer} />
) : (
<ByTimeMode onOpenDrawer={openDrawer} />
)}
<AllTopicsDrawer ref={drawerRef} />
</>
);
});
@@ -17,16 +17,19 @@ import { preferenceSelectors } from '@/store/user/selectors';
import GroupItem from './GroupItem';
const ByTimeMode = memo(() => {
interface ByTimeModeProps {
onOpenDrawer?: () => void;
}
const ByTimeMode = memo<ByTimeModeProps>(({ onOpenDrawer }) => {
const { t } = useTranslation('topic');
const topicPageSize = useGlobalStore(systemStatusSelectors.topicPageSize);
const topicSortBy = useUserStore(preferenceSelectors.topicSortBy);
const topicGroupMode = useUserStore(preferenceSelectors.topicGroupMode);
const [hasMore, isExpandingPageSize, openAllTopicsDrawer] = useChatStore((s) => [
const [hasMore, isExpandingPageSize] = useChatStore((s) => [
topicSelectors.hasMoreTopics(s),
topicSelectors.isExpandingPageSize(s),
s.openAllTopicsDrawer,
]);
const [activeTopicId, activeThreadId] = useChatStore((s) => [s.activeTopicId, s.activeThreadId]);
@@ -68,8 +71,8 @@ const ByTimeMode = memo(() => {
))}
</Accordion>
{isExpandingPageSize && <SkeletonList rows={3} />}
{hasMore && !isExpandingPageSize && (
<NavItem icon={MoreHorizontal} title={t('loadMore')} onClick={openAllTopicsDrawer} />
{onOpenDrawer && hasMore && !isExpandingPageSize && (
<NavItem icon={MoreHorizontal} title={t('loadMore')} onClick={onOpenDrawer} />
)}
</Flexbox>
);
@@ -17,19 +17,21 @@ import { preferenceSelectors } from '@/store/user/selectors';
import TopicItem from '../../List/Item';
const FlatMode = memo(() => {
interface FlatModeProps {
onOpenDrawer?: () => void;
}
const FlatMode = memo<FlatModeProps>(({ onOpenDrawer }) => {
const { t } = useTranslation('topic');
const topicPageSize = useGlobalStore(systemStatusSelectors.topicPageSize);
const topicSortBy = useUserStore(preferenceSelectors.topicSortBy);
const [activeTopicId, activeThreadId, hasMore, isExpandingPageSize, openAllTopicsDrawer] =
useChatStore((s) => [
s.activeTopicId,
s.activeThreadId,
topicSelectors.hasMoreTopics(s),
topicSelectors.isExpandingPageSize(s),
s.openAllTopicsDrawer,
]);
const [activeTopicId, activeThreadId, hasMore, isExpandingPageSize] = useChatStore((s) => [
s.activeTopicId,
s.activeThreadId,
topicSelectors.hasMoreTopics(s),
topicSelectors.isExpandingPageSize(s),
]);
const activeTopicList = useChatStore(
topicSelectors.displayTopicsForSidebar(topicPageSize, topicSortBy),
@@ -50,8 +52,8 @@ const FlatMode = memo(() => {
/>
))}
{isExpandingPageSize && <SkeletonList rows={3} />}
{hasMore && !isExpandingPageSize && (
<NavItem icon={MoreHorizontal} title={t('loadMore')} onClick={openAllTopicsDrawer} />
{onOpenDrawer && hasMore && !isExpandingPageSize && (
<NavItem icon={MoreHorizontal} title={t('loadMore')} onClick={onOpenDrawer} />
)}
</Flexbox>
);
@@ -1,56 +0,0 @@
'use client';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import urlJoin from 'url-join';
import EmptyNavItem from '@/features/NavPanel/components/EmptyNavItem';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import { useFetchChatTopics } from '@/hooks/useFetchChatTopics';
import { useQueryRoute } from '@/hooks/useQueryRoute';
import { useAgentGroupStore } from '@/store/agentGroup';
import { useChatStore } from '@/store/chat';
import { topicSelectors } from '@/store/chat/selectors';
import { useUserStore } from '@/store/user';
import { preferenceSelectors } from '@/store/user/selectors';
import ByTimeMode from './ByTimeMode';
import FlatMode from './FlatMode';
import SearchResult from './SearchResult';
const TopicListContent = memo(() => {
const { t } = useTranslation('topic');
const router = useQueryRoute();
const topicLength = useChatStore((s) => topicSelectors.currentTopicLength(s));
const [isUndefinedTopics, isInSearchMode] = useChatStore((s) => [
topicSelectors.isUndefinedTopics(s),
topicSelectors.isInSearchMode(s),
]);
const activeGroupId = useAgentGroupStore((s) => s.activeGroupId);
const topicGroupMode = useUserStore(preferenceSelectors.topicGroupMode);
useFetchChatTopics();
if (isInSearchMode) return <SearchResult />;
// Show skeleton when current session's topic data is not yet loaded
if (isUndefinedTopics) return <SkeletonList />;
return (
<>
{topicLength === 0 && activeGroupId && (
<EmptyNavItem
title={t('actions.addNewTopic')}
onClick={() => {
router.push(urlJoin('/group', activeGroupId));
}}
/>
)}
{topicGroupMode === 'flat' ? <FlatMode /> : <ByTimeMode />}
</>
);
});
TopicListContent.displayName = 'TopicListContent';
export default TopicListContent;
@@ -14,36 +14,28 @@ import GroupItem from '../List/AgentGroupItem';
import AgentItem from '../List/AgentItem';
interface ContentProps {
open: boolean;
onNavigate: () => void;
searchKeyword: string;
}
const Content = memo<ContentProps>(({ searchKeyword }) => {
// Use server-side search if there's a keyword
const Content = memo<ContentProps>(({ searchKeyword, onNavigate }) => {
const trimmedKeyword = searchKeyword.trim();
const isSearching = trimmedKeyword.length > 0;
// Search agents using homeStore
const [closeAllAgentsDrawer, useSearchAgents] = useHomeStore((s) => [
s.closeAllAgentsDrawer,
s.useSearchAgents,
]);
const useSearchAgents = useHomeStore((s) => s.useSearchAgents);
const { data: searchResults, isLoading: isSearchLoading } = useSearchAgents(
isSearching ? trimmedKeyword : undefined,
);
// Get all agents from homeStore (ungrouped agents for default view)
const allUngroupedAgents = useHomeStore(homeAgentListSelectors.ungroupedAgents, isEqual);
// Filter and display - searchResults already returns SidebarAgentItem[]
const displayItems = isSearching ? searchResults || [] : allUngroupedAgents;
const count = displayItems.length;
// Close on navigation because the Home layout stays mounted offscreen across route changes.
const handleNavigate = closeAllAgentsDrawer;
const handleNavigate = onNavigate;
// Show loading skeleton when searching
if (isSearching && (isSearchLoading || !searchResults)) {
return (
<Flexbox gap={1} paddingBlock={1} paddingInline={4}>
@@ -52,7 +44,6 @@ const Content = memo<ContentProps>(({ searchKeyword }) => {
);
}
// Show empty state when no agents
if (count === 0) {
return <AgentSelectionEmpty search={isSearching} />;
}
@@ -1,11 +1,11 @@
'use client';
import { Flexbox, SearchBar } from '@lobehub/ui';
import { memo, useState } from 'react';
import { memo, type Ref, useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import SideBarDrawer from '@/features/NavPanel/SideBarDrawer';
import SideBarDrawer, { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import dynamic from '@/libs/next/dynamic';
const Content = dynamic(() => import('./Content'), {
@@ -18,17 +18,31 @@ const Content = dynamic(() => import('./Content'), {
});
interface AllAgentsDrawerProps {
onClose: () => void;
open: boolean;
ref?: Ref<SideBarDrawerHandle>;
}
const AllAgentsDrawer = memo<AllAgentsDrawerProps>(({ open, onClose }) => {
const AllAgentsDrawer = memo<AllAgentsDrawerProps>(({ ref: externalRef }) => {
const { t } = useTranslation('common');
const [searchKeyword, setSearchKeyword] = useState('');
const handleRef = useRef<SideBarDrawerHandle | null>(null);
const setHandle = useCallback(
(handle: SideBarDrawerHandle | null) => {
handleRef.current = handle;
if (typeof externalRef === 'function') {
externalRef(handle);
} else if (externalRef) {
externalRef.current = handle;
}
},
[externalRef],
);
const handleNavigate = useCallback(() => handleRef.current?.close(), []);
return (
<SideBarDrawer
open={open}
ref={setHandle}
title={t('navPanel.agent')}
subHeader={
<Flexbox paddingBlock={'0 8px'} paddingInline={8}>
@@ -43,9 +57,8 @@ const AllAgentsDrawer = memo<AllAgentsDrawerProps>(({ open, onClose }) => {
/>
</Flexbox>
}
onClose={onClose}
>
<Content open={open} searchKeyword={searchKeyword} />
<Content searchKeyword={searchKeyword} onNavigate={handleNavigate} />
</SideBarDrawer>
);
});
@@ -35,7 +35,6 @@ const List = memo<SessionListProps>(
const isDefaultList = groupId === SessionDefaultGroup.Default;
const ungroupedAgentsCount = useHomeStore(homeAgentListSelectors.ungroupedAgentsCount);
const agentPageSize = useGlobalStore(systemStatusSelectors.agentPageSize);
const openAllAgentsDrawer = useHomeStore((s) => s.openAllAgentsDrawer);
const hasMore = isDefaultList && ungroupedAgentsCount > agentPageSize;
@@ -59,12 +58,8 @@ const List = memo<SessionListProps>(
<AgentItem className={itemClassName} item={item} key={item.id} style={itemStyle} />
),
)}
{hasMore && (
<NavItem
icon={MoreHorizontal}
title={t('input.more')}
onClick={onMoreClick || openAllAgentsDrawer}
/>
{hasMore && onMoreClick && (
<NavItem icon={MoreHorizontal} title={t('input.more')} onClick={onMoreClick} />
)}
{showCreateButton && <CreateAgentButton className={itemClassName} groupId={groupId} />}
</Flexbox>
@@ -1,23 +1,21 @@
'use client';
import { memo } from 'react';
import { memo, useCallback, useRef } from 'react';
import { useHomeStore } from '@/store/home';
import { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import AllAgentsDrawer from '../AllAgentsDrawer';
import AgentListContent from './AgentListContent';
// The Home sidebar owns the all-agents drawer; other surfaces should import AgentListContent directly.
const AgentList = memo<{ onMoreClick?: () => void }>(({ onMoreClick }) => {
const [allAgentsDrawerOpen, closeAllAgentsDrawer] = useHomeStore((s) => [
s.allAgentsDrawerOpen,
s.closeAllAgentsDrawer,
]);
const drawerRef = useRef<SideBarDrawerHandle>(null);
const openDrawer = useCallback(() => drawerRef.current?.open(), []);
return (
<>
<AgentListContent onMoreClick={onMoreClick} />
<AllAgentsDrawer open={allAgentsDrawerOpen} onClose={closeAllAgentsDrawer} />
<AgentListContent onMoreClick={onMoreClick ?? openDrawer} />
<AllAgentsDrawer ref={drawerRef} />
</>
);
});
@@ -3,10 +3,11 @@
import { ActionIcon } from '@lobehub/ui';
import { Badge } from 'antd';
import { BellIcon } from 'lucide-react';
import { memo, useCallback, useState } from 'react';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { DESKTOP_HEADER_ICON_SMALL_SIZE } from '@/const/layoutTokens';
import { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import { useClientDataSWR } from '@/libs/swr';
import { notificationService } from '@/services/notification';
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
@@ -16,7 +17,6 @@ import { UNREAD_COUNT_KEY } from './InboxDrawer/constants';
const InboxButton = memo(() => {
const { t } = useTranslation('notification');
const [open, setOpen] = useState(false);
const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
const { data: unreadCount = 0 } = useClientDataSWR<number>(
@@ -25,13 +25,8 @@ const InboxButton = memo(() => {
{ refreshInterval: 10_000 },
);
const handleToggle = useCallback(() => {
setOpen((prev) => !prev);
}, []);
const handleClose = useCallback(() => {
setOpen(false);
}, []);
const drawerRef = useRef<SideBarDrawerHandle>(null);
const handleOpen = useCallback(() => drawerRef.current?.open(), []);
if (!enableBusinessFeatures) return null;
@@ -42,10 +37,10 @@ const InboxButton = memo(() => {
icon={BellIcon}
size={DESKTOP_HEADER_ICON_SMALL_SIZE}
title={t('inbox.title')}
onClick={handleToggle}
onClick={handleOpen}
/>
</Badge>
<InboxDrawer open={open} onClose={handleClose} />
<InboxDrawer ref={drawerRef} />
</>
);
});
@@ -2,12 +2,12 @@
import { ActionIcon, Flexbox } from '@lobehub/ui';
import { ArchiveIcon, CheckCheckIcon, ListFilterIcon } from 'lucide-react';
import { memo, useCallback, useState } from 'react';
import { memo, type Ref, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import SideBarDrawer from '@/features/NavPanel/SideBarDrawer';
import SideBarDrawer, { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import dynamic from '@/libs/next/dynamic';
import { mutate } from '@/libs/swr';
import { notificationService } from '@/services/notification';
@@ -24,13 +24,13 @@ const Content = dynamic(() => import('./Content'), {
});
interface InboxDrawerProps {
onClose: () => void;
open: boolean;
ref?: Ref<SideBarDrawerHandle>;
}
const InboxDrawer = memo<InboxDrawerProps>(({ open, onClose }) => {
const InboxDrawer = memo<InboxDrawerProps>(({ ref }) => {
const { t } = useTranslation('notification');
const [unreadOnly, setUnreadOnly] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const refreshList = useCallback(() => {
mutate((key: unknown) => Array.isArray(key) && key[0] === FETCH_KEY);
@@ -69,7 +69,7 @@ const InboxDrawer = memo<InboxDrawerProps>(({ open, onClose }) => {
return (
<SideBarDrawer
open={open}
ref={ref}
title={t('inbox.title')}
action={
<>
@@ -94,10 +94,10 @@ const InboxDrawer = memo<InboxDrawerProps>(({ open, onClose }) => {
/>
</>
}
onClose={onClose}
onOpenChange={setIsOpen}
>
<Content
open={open}
open={isOpen}
unreadOnly={unreadOnly}
onArchive={handleArchive}
onMarkAsRead={handleMarkAsRead}
@@ -2,12 +2,12 @@
import { Empty, Flexbox, SearchBar } from '@lobehub/ui';
import { SearchIcon } from 'lucide-react';
import { memo, useCallback, useMemo, useState } from 'react';
import { memo, type Ref, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import SideBarDrawer from '@/features/NavPanel/SideBarDrawer';
import SideBarDrawer, { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import { useClientDataSWR } from '@/libs/swr';
import { recentService } from '@/services/recent';
import { ALL_RECENTS_DRAWER_SWR_PREFIX } from '@/store/home/slices/recent/action';
@@ -15,16 +15,16 @@ import { ALL_RECENTS_DRAWER_SWR_PREFIX } from '@/store/home/slices/recent/action
import RecentListItem from './Item';
interface AllRecentsDrawerProps {
onClose: () => void;
open: boolean;
ref?: Ref<SideBarDrawerHandle>;
}
const AllRecentsDrawer = memo<AllRecentsDrawerProps>(({ open, onClose }) => {
const AllRecentsDrawer = memo<AllRecentsDrawerProps>(({ ref }) => {
const { t } = useTranslation('common');
const [searchKeyword, setSearchKeyword] = useState('');
const [isOpen, setIsOpen] = useState(false);
const { data: recents, isLoading } = useClientDataSWR(
open ? [ALL_RECENTS_DRAWER_SWR_PREFIX, open] : null,
isOpen ? [ALL_RECENTS_DRAWER_SWR_PREFIX, isOpen] : null,
() => recentService.getAll(50),
);
@@ -39,13 +39,12 @@ const AllRecentsDrawer = memo<AllRecentsDrawerProps>(({ open, onClose }) => {
if (item.type !== 'task') return item.routePath;
const taskId = item.id;
if (!taskId) return item.routePath;
return `/task/${taskId}`;
}, []);
return (
<SideBarDrawer
open={open}
ref={ref}
title={t('recents')}
subHeader={
<Flexbox paddingBlock={'0 8px'} paddingInline={8}>
@@ -60,7 +59,7 @@ const AllRecentsDrawer = memo<AllRecentsDrawerProps>(({ open, onClose }) => {
/>
</Flexbox>
}
onClose={onClose}
onOpenChange={setIsOpen}
>
<Flexbox gap={1} paddingBlock={1} paddingInline={4}>
{isLoading || !recents ? (
@@ -1,11 +1,12 @@
import { Flexbox } from '@lobehub/ui';
import { MoreHorizontalIcon } from 'lucide-react';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import NavItem from '@/features/NavPanel/components/NavItem';
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import { type SideBarDrawerHandle } from '@/features/NavPanel/SideBarDrawer';
import { useGlobalStore } from '@/store/global';
import { systemStatusSelectors } from '@/store/global/selectors';
import { useHomeStore } from '@/store/home';
@@ -19,11 +20,9 @@ const RecentsList = memo(() => {
const recents = useHomeStore(homeRecentSelectors.recents);
const isInit = useHomeStore(homeRecentSelectors.isRecentsInit);
const recentPageSize = useGlobalStore(systemStatusSelectors.recentPageSize);
const [drawerOpen, openDrawer, closeDrawer] = useHomeStore((s) => [
s.allRecentsDrawerOpen,
s.openAllRecentsDrawer,
s.closeAllRecentsDrawer,
]);
const drawerRef = useRef<SideBarDrawerHandle>(null);
const openDrawer = useCallback(() => drawerRef.current?.open(), []);
const displayItems = useMemo(() => recents.slice(0, recentPageSize), [recents, recentPageSize]);
const hasMore = recents.length > recentPageSize;
@@ -32,7 +31,6 @@ const RecentsList = memo(() => {
if (item.type !== 'task') return item.routePath;
const taskId = item.id;
if (!taskId) return item.routePath;
return `/task/${taskId}`;
}, []);
@@ -54,7 +52,7 @@ const RecentsList = memo(() => {
{hasMore && (
<NavItem icon={MoreHorizontalIcon} title={t('input.more')} onClick={openDrawer} />
)}
<AllRecentsDrawer open={drawerOpen} onClose={closeDrawer} />
<AllRecentsDrawer ref={drawerRef} />
</Flexbox>
);
});
-8
View File
@@ -82,14 +82,6 @@ export class ChatTopicActionImpl {
this.#get = get;
}
closeAllTopicsDrawer = (): void => {
this.#set({ allTopicsDrawerOpen: false }, false, n('closeAllTopicsDrawer'));
};
openAllTopicsDrawer = (): void => {
this.#set({ allTopicsDrawerOpen: true }, false, n('openAllTopicsDrawer'));
};
openNewTopicOrSaveTopic = async (): Promise<void> => {
const { switchTopic, saveToTopic, refreshMessages, activeTopicId } = this.#get();
const hasTopic = !!activeTopicId;
@@ -23,10 +23,6 @@ export interface TopicData {
export interface ChatTopicState {
// TODO: need to add the null to the type
activeTopicId?: string;
/**
* whether all topics drawer is open
*/
allTopicsDrawerOpen: boolean;
creatingTopic: boolean;
inSearchingMode?: boolean;
isSearchingTopic: boolean;
@@ -43,7 +39,6 @@ export interface ChatTopicState {
export const initialTopicState: ChatTopicState = {
activeTopicId: null as any,
allTopicsDrawerOpen: false,
creatingTopic: false,
isSearchingTopic: false,
searchTopics: [],
@@ -29,14 +29,6 @@ export class AgentListActionImpl {
this.#get = get;
}
closeAllAgentsDrawer = (): void => {
this.#set({ allAgentsDrawerOpen: false }, false, n('closeAllAgentsDrawer'));
};
openAllAgentsDrawer = (): void => {
this.#set({ allAgentsDrawerOpen: true }, false, n('openAllAgentsDrawer'));
};
refreshAgentList = async (): Promise<void> => {
await mutate([FETCH_AGENT_LIST_KEY, true]);
};
@@ -9,10 +9,6 @@ export interface AgentListState {
* Agent groups (user-defined folders)
*/
agentGroups: SidebarGroup[];
/**
* Whether all agents drawer is open
*/
allAgentsDrawerOpen: boolean;
/**
* Whether the agent list has been initialized
*/
@@ -29,7 +25,6 @@ export interface AgentListState {
export const initialAgentListState: AgentListState = {
agentGroups: [],
allAgentsDrawerOpen: false,
isAgentListInit: false,
pinnedAgents: [],
ungroupedAgents: [],
-8
View File
@@ -33,14 +33,6 @@ export class RecentActionImpl {
this.#get = get;
}
closeAllRecentsDrawer = (): void => {
this.#set({ allRecentsDrawerOpen: false }, false, n('closeAllRecentsDrawer'));
};
openAllRecentsDrawer = (): void => {
this.#set({ allRecentsDrawerOpen: true }, false, n('openAllRecentsDrawer'));
};
updateRecentTitle = (id: string, title: string): void => {
const recents = this.#get().recents.map((item) => (item.id === id ? { ...item, title } : item));
this.#set({ recents }, false, n('updateRecentTitle'));
@@ -1,13 +1,11 @@
import { type RecentItem } from '@/server/routers/lambda/recent';
export interface RecentState {
allRecentsDrawerOpen: boolean;
isRecentsInit: boolean;
recents: RecentItem[];
}
export const initialRecentState: RecentState = {
allRecentsDrawerOpen: false,
isRecentsInit: false,
recents: [],
};
-8
View File
@@ -8,11 +8,6 @@ export interface PageQueryFilter {
}
export interface PageState {
// ===== Selection & Navigation =====
/**
* Whether all pages drawer is open
*/
allPagesDrawerOpen: boolean;
// ===== List Management =====
/**
* Current page number (0-based) for pagination
@@ -66,9 +61,6 @@ export interface PageState {
}
export const initialState: PageState = {
// Selection & Navigation
allPagesDrawerOpen: false,
// List Management
currentPage: 0,
@@ -19,14 +19,6 @@ export class SelectionActionImpl {
this.#get = get;
}
closeAllPagesDrawer = (): void => {
this.#set({ allPagesDrawerOpen: false }, false, n('closeAllPagesDrawer'));
};
openAllPagesDrawer = (): void => {
this.#set({ allPagesDrawerOpen: true }, false, n('openAllPagesDrawer'));
};
selectPage = (pageId: string): void => {
const { selectedPageId } = this.#get();
@@ -1,11 +1,9 @@
export interface SelectionState {
allPagesDrawerOpen: boolean;
renamingPageId: string | null;
selectedPageId: string | null;
}
export const initialSelectionState: SelectionState = {
allPagesDrawerOpen: false,
renamingPageId: null,
selectedPageId: null,
};
@@ -55,10 +55,6 @@ export class SessionActionImpl {
await this.#get().refreshSessions();
};
closeAllAgentsDrawer = (): void => {
this.#set({ allAgentsDrawerOpen: false }, false, n('closeAllAgentsDrawer'));
};
/** @deprecated Use agentStore.createAgent instead */
createSession = async (
agent?: PartialDeep<LobeAgentSession>,
@@ -133,10 +129,6 @@ export class SessionActionImpl {
switchSession(newId);
};
openAllAgentsDrawer = (): void => {
this.#set({ allAgentsDrawerOpen: true }, false, n('openAllAgentsDrawer'));
};
pinSession = async (id: string, pinned: boolean): Promise<void> => {
await this.#get().internal_updateSession(id, { pinned });
};
@@ -7,10 +7,6 @@ export interface SessionState {
* @description The session currently being edited or viewed
*/
activeId: string;
/**
* whether all agents drawer is open
*/
allAgentsDrawerOpen: boolean;
defaultSessions: LobeSessions;
/**
* @title Whether the agent panel is pinned
@@ -40,7 +36,6 @@ export interface SessionState {
export const initialSessionState: SessionState = {
activeId: 'inbox',
allAgentsDrawerOpen: false,
defaultSessions: [],
isAgentPinned: false,
isSearching: false,