diff --git a/src/app/(main)/chat/(workspace)/@topic/_layout/Desktop.tsx b/src/app/(main)/chat/(workspace)/@topic/_layout/Desktop.tsx new file mode 100644 index 000000000000..522f38621584 --- /dev/null +++ b/src/app/(main)/chat/(workspace)/@topic/_layout/Desktop.tsx @@ -0,0 +1,17 @@ +import { PropsWithChildren } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import Header from '../features/Header'; + +const Layout = ({ children }: PropsWithChildren) => { + return ( + <> +
+ + {children} + + + ); +}; + +export default Layout; diff --git a/src/app/(main)/chat/(workspace)/@topic/_layout/Mobile.tsx b/src/app/(main)/chat/(workspace)/@topic/_layout/Mobile.tsx new file mode 100644 index 000000000000..296f57178070 --- /dev/null +++ b/src/app/(main)/chat/(workspace)/@topic/_layout/Mobile.tsx @@ -0,0 +1,21 @@ +import { PropsWithChildren } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import TopicSearchBar from '../features/TopicSearchBar'; + +const Layout = ({ children }: PropsWithChildren) => { + return ( + + + + {children} + + + ); +}; + +export default Layout; diff --git a/src/app/(main)/chat/(workspace)/@topic/default.tsx b/src/app/(main)/chat/(workspace)/@topic/default.tsx index 6e99bfc39ed4..b776c5f97b4f 100644 --- a/src/app/(main)/chat/(workspace)/@topic/default.tsx +++ b/src/app/(main)/chat/(workspace)/@topic/default.tsx @@ -1,15 +1,21 @@ import { isMobileDevice } from '@/utils/responsive'; +import Desktop from './_layout/Desktop'; +import Mobile from './_layout/Mobile'; import SystemRole from './features/SystemRole'; import TopicListContent from './features/TopicListContent'; const Topic = () => { const mobile = isMobileDevice(); + const Layout = mobile ? Mobile : Desktop; + return ( <> {!mobile && } - + + + ); }; diff --git a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Header.tsx b/src/app/(main)/chat/(workspace)/@topic/features/Header.tsx similarity index 100% rename from src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Header.tsx rename to src/app/(main)/chat/(workspace)/@topic/features/Header.tsx diff --git a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/DefaultContent.tsx b/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/DefaultContent.tsx similarity index 100% rename from src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/DefaultContent.tsx rename to src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/DefaultContent.tsx diff --git a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/SkeletonList.tsx b/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/SkeletonList.tsx similarity index 96% rename from src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/SkeletonList.tsx rename to src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/SkeletonList.tsx index 1b18f4df84b8..41d2b776d1eb 100644 --- a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/SkeletonList.tsx +++ b/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/SkeletonList.tsx @@ -45,7 +45,7 @@ export const Placeholder = memo(() => { }); export const SkeletonList = memo(() => ( - + {Array.from({ length: 8 }).map((_, i) => ( ))} diff --git a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/index.tsx b/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/index.tsx deleted file mode 100644 index 89f221c3d743..000000000000 --- a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -'use client'; - -import { EmptyCard } from '@lobehub/ui'; -import { useThemeMode } from 'antd-style'; -import isEqual from 'fast-deep-equal'; -import React, { memo, useCallback, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Flexbox } from 'react-layout-kit'; -import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; - -import { imageUrl } from '@/const/url'; -import { useChatStore } from '@/store/chat'; -import { topicSelectors } from '@/store/chat/selectors'; -import { useUserStore } from '@/store/user'; -import { ChatTopic } from '@/types/topic'; - -import { Placeholder, SkeletonList } from './SkeletonList'; -import TopicItem from './TopicItem'; - -export const Topic = memo<{ mobile?: boolean }>(({ mobile }) => { - const { t } = useTranslation('chat'); - const virtuosoRef = useRef(null); - const { isDarkMode } = useThemeMode(); - const [topicsInit, activeTopicId, topicLength] = useChatStore((s) => [ - s.topicsInit, - s.activeTopicId, - topicSelectors.currentTopicLength(s), - ]); - const [visible, updateGuideState] = useUserStore((s) => [ - s.preference.guide?.topic, - s.updateGuideState, - ]); - - const topics = useChatStore( - (s) => [ - { - favorite: false, - id: 'default', - title: t('topic.defaultTitle'), - } as ChatTopic, - ...topicSelectors.displayTopics(s), - ], - isEqual, - ); - - const itemContent = useCallback( - (index: number, { id, favorite, title }: ChatTopic) => - index === 0 ? ( - - ) : ( - - ), - [activeTopicId], - ); - - const activeIndex = topics.findIndex((topic) => topic.id === activeTopicId); - - return !topicsInit ? ( - - ) : ( - - {topicLength === 0 && ( - { - updateGuideState({ topic: visible }); - }} - style={{ flex: 'none', marginBottom: 12 }} - title={t('topic.guide.title')} - visible={visible} - width={200} - /> - )} - item.id} - data={topics} - fixedItemHeight={44} - initialTopMostItemIndex={Math.max(activeIndex, 0)} - itemContent={itemContent} - overscan={44 * 10} - ref={virtuosoRef} - scrollSeekConfiguration={{ - enter: (velocity) => Math.abs(velocity) > 350, - exit: (velocity) => Math.abs(velocity) < 10, - }} - /> - - ); -}); diff --git a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/TopicContent.tsx b/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicContent.tsx similarity index 100% rename from src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/TopicContent.tsx rename to src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicContent.tsx diff --git a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/TopicItem.tsx b/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicItem.tsx similarity index 95% rename from src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/TopicItem.tsx rename to src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicItem.tsx index 66779df79b34..42252183f25f 100644 --- a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/Topic/TopicItem.tsx +++ b/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicItem.tsx @@ -19,7 +19,12 @@ const useStyles = createStyles(({ css, token, isDarkMode }) => ({ `, container: css` cursor: pointer; + + width: calc(100% - 16px); + margin-block: 2px; + margin-inline: 8px; padding: 8px; + border-radius: ${token.borderRadius}px; &:hover { diff --git a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/index.tsx b/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/index.tsx index 381aae2203da..de2a4a911f76 100644 --- a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/index.tsx +++ b/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/index.tsx @@ -1,18 +1,100 @@ +'use client'; + +import { EmptyCard } from '@lobehub/ui'; +import { useThemeMode } from 'antd-style'; +import isEqual from 'fast-deep-equal'; +import React, { memo, useCallback, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; + +import { imageUrl } from '@/const/url'; +import { useChatStore } from '@/store/chat'; +import { topicSelectors } from '@/store/chat/selectors'; +import { useUserStore } from '@/store/user'; +import { ChatTopic } from '@/types/topic'; + +import { Placeholder, SkeletonList } from './SkeletonList'; +import TopicItem from './TopicItem'; + +const TopicListContent = memo(() => { + const { t } = useTranslation('chat'); + const virtuosoRef = useRef(null); + const { isDarkMode } = useThemeMode(); + const [topicsInit, activeTopicId, topicLength] = useChatStore((s) => [ + s.topicsInit, + s.activeTopicId, + topicSelectors.currentTopicLength(s), + ]); + const [visible, updateGuideState] = useUserStore((s) => [ + s.preference.guide?.topic, + s.updateGuideState, + ]); + + const topics = useChatStore( + (s) => [ + { + favorite: false, + id: 'default', + title: t('topic.defaultTitle'), + } as ChatTopic, + ...topicSelectors.displayTopics(s), + ], + isEqual, + ); + + const itemContent = useCallback( + (index: number, { id, favorite, title }: ChatTopic) => + index === 0 ? ( + + ) : ( + + ), + [activeTopicId], + ); + + const activeIndex = topics.findIndex((topic) => topic.id === activeTopicId); -import Header from './Header'; -import { Topic } from './Topic'; -import TopicSearchBar from './TopicSearchBar'; - -const TopicListContent = ({ mobile }: { mobile?: boolean }) => { - return ( - - {mobile ? :
} - - - - + return !topicsInit ? ( + + ) : ( + <> + {topicLength === 0 && visible && ( + + { + updateGuideState({ topic: visible }); + }} + style={{ flex: 'none', marginBottom: 12 }} + title={t('topic.guide.title')} + visible={visible} + width={200} + /> + + )} + item.id} + data={topics} + fixedItemHeight={44} + initialTopMostItemIndex={Math.max(activeIndex, 0)} + itemContent={itemContent} + overscan={44 * 10} + ref={virtuosoRef} + scrollSeekConfiguration={{ + enter: (velocity) => Math.abs(velocity) > 350, + exit: (velocity) => Math.abs(velocity) < 10, + }} + /> + ); -}; +}); export default TopicListContent; diff --git a/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicSearchBar/index.tsx b/src/app/(main)/chat/(workspace)/@topic/features/TopicSearchBar/index.tsx similarity index 100% rename from src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicSearchBar/index.tsx rename to src/app/(main)/chat/(workspace)/@topic/features/TopicSearchBar/index.tsx diff --git a/src/app/(main)/chat/(workspace)/_layout/Desktop/TopicPanel.tsx b/src/app/(main)/chat/(workspace)/_layout/Desktop/TopicPanel.tsx index 5b4559883e84..119fa57f0576 100644 --- a/src/app/(main)/chat/(workspace)/_layout/Desktop/TopicPanel.tsx +++ b/src/app/(main)/chat/(workspace)/_layout/Desktop/TopicPanel.tsx @@ -2,7 +2,8 @@ import { DraggablePanel, DraggablePanelContainer } from '@lobehub/ui'; import { createStyles, useResponsive } from 'antd-style'; -import { PropsWithChildren, memo, useEffect, useLayoutEffect, useState } from 'react'; +import isEqual from 'fast-deep-equal'; +import { PropsWithChildren, memo, useEffect, useState } from 'react'; import SafeSpacing from '@/components/SafeSpacing'; import { CHAT_SIDEBAR_WIDTH } from '@/const/layoutTokens'; @@ -26,27 +27,23 @@ const useStyles = createStyles(({ css, token }) => ({ const TopicPanel = memo(({ children }: PropsWithChildren) => { const { styles } = useStyles(); const { md = true, lg = true } = useResponsive(); - const [showAgentSettings, toggleConfig, isPreferenceInit] = useGlobalStore((s) => [ + const [showAgentSettings, toggleConfig] = useGlobalStore((s) => [ s.preference.showChatSideBar, s.toggleChatSideBar, s.isPreferenceInit, ]); - const [expand, setExpand] = useState(showAgentSettings); + const [cacheExpand, setCacheExpand] = useState(Boolean(showAgentSettings)); - const handleExpand = (e: boolean) => { - toggleConfig(e); - setExpand(e); + const handleExpand = (expand: boolean) => { + if (isEqual(expand, Boolean(showAgentSettings))) return; + toggleConfig(expand); + setCacheExpand(expand); }; - useLayoutEffect(() => { - if (!isPreferenceInit) return; - setExpand(showAgentSettings); - }, [isPreferenceInit, showAgentSettings]); - useEffect(() => { - if (lg && showAgentSettings) setExpand(true); - if (!lg) setExpand(false); - }, [lg, showAgentSettings]); + if (lg && cacheExpand) toggleConfig(true); + if (!lg) toggleConfig(false); + }, [lg, cacheExpand]); return ( { classNames={{ content: styles.content, }} - expand={expand} + expand={showAgentSettings} minWidth={CHAT_SIDEBAR_WIDTH} mode={md ? 'fixed' : 'float'} onExpandChange={handleExpand} diff --git a/src/app/(main)/chat/(workspace)/_layout/Mobile/TopicModal.tsx b/src/app/(main)/chat/(workspace)/_layout/Mobile/TopicModal.tsx index a569f83d3eaf..e95ac732dc35 100644 --- a/src/app/(main)/chat/(workspace)/_layout/Mobile/TopicModal.tsx +++ b/src/app/(main)/chat/(workspace)/_layout/Mobile/TopicModal.tsx @@ -17,7 +17,15 @@ const Topics = memo(({ children }: PropsWithChildren) => { const { t } = useTranslation('chat'); return ( - setOpen(false)} open={open} title={t('topic.title')}> + setOpen(false)} + open={open} + styles={{ + body: { padding: 0 }, + }} + title={t('topic.title')} + > {children} ); diff --git a/src/app/(main)/chat/_layout/Desktop/SessionPanel.tsx b/src/app/(main)/chat/_layout/Desktop/SessionPanel.tsx index e066e3145ade..bf5cff3f02e1 100644 --- a/src/app/(main)/chat/_layout/Desktop/SessionPanel.tsx +++ b/src/app/(main)/chat/_layout/Desktop/SessionPanel.tsx @@ -3,7 +3,7 @@ import { DraggablePanel, DraggablePanelContainer, type DraggablePanelProps } from '@lobehub/ui'; import { createStyles, useResponsive } from 'antd-style'; import isEqual from 'fast-deep-equal'; -import { PropsWithChildren, memo, useEffect, useLayoutEffect, useState } from 'react'; +import { PropsWithChildren, memo, useEffect, useState } from 'react'; import { FOLDER_WIDTH } from '@/const/layoutTokens'; import { useGlobalStore } from '@/store/global'; @@ -18,25 +18,21 @@ export const useStyles = createStyles(({ css, token }) => ({ const SessionPanel = memo(({ children }) => { const { md = true } = useResponsive(); + const { styles } = useStyles(); - const [sessionsWidth, sessionExpandable, updatePreference, isPreferenceInit] = useGlobalStore( - (s) => [ - s.preference.sessionsWidth, - s.preference.showSessionPanel, - s.updatePreference, - s.isPreferenceInit, - ], - ); - const [expand, setExpand] = useState(sessionExpandable); + const [sessionsWidth, sessionExpandable, updatePreference] = useGlobalStore((s) => [ + s.preference.sessionsWidth, + s.preference.showSessionPanel, + s.updatePreference, + ]); + const [cacheExpand, setCacheExpand] = useState(Boolean(sessionExpandable)); const [tmpWidth, setWidth] = useState(sessionsWidth); if (tmpWidth !== sessionsWidth) setWidth(sessionsWidth); - const handleExpand: DraggablePanelProps['onExpandChange'] = (e) => { - updatePreference({ - sessionsWidth: e ? 320 : 0, - showSessionPanel: e, - }); - setExpand(e); + const handleExpand = (expand: boolean) => { + if (isEqual(expand, sessionExpandable)) return; + updatePreference({ showSessionPanel: expand }); + setCacheExpand(expand); }; const handleSizeChange: DraggablePanelProps['onSizeChange'] = (_, size) => { @@ -47,21 +43,16 @@ const SessionPanel = memo(({ children }) => { updatePreference({ sessionsWidth: nextWidth }); }; - useLayoutEffect(() => { - if (!isPreferenceInit) return; - setExpand(sessionExpandable); - }, [isPreferenceInit, sessionExpandable]); - useEffect(() => { - if (md && sessionExpandable) setExpand(true); - if (!md) setExpand(false); - }, [md, sessionExpandable]); + if (md && cacheExpand) updatePreference({ showSessionPanel: true }); + if (!md) updatePreference({ showSessionPanel: false }); + }, [md, cacheExpand]); return ( ({ +const useStyles = createStyles(({ css }) => ({ header: css` z-index: 10; - box-shadow: 0 2px 6px ${token.colorBgLayout}; `, })); diff --git a/src/components/server/MobileNavLayout.tsx b/src/components/server/MobileNavLayout.tsx index 81e0c4d6272f..35adde475adc 100644 --- a/src/components/server/MobileNavLayout.tsx +++ b/src/components/server/MobileNavLayout.tsx @@ -16,6 +16,7 @@ const MobileContentLayout = ({ const content = ( (({ mobile }) => { )); - const cards = useMemo( - () => - agentList.slice(sliceStart, sliceStart + agentLength).map((agent) => ( - - - - - - {agent.meta.title} - - - {agent.meta.description} - - - - - )), - [agentList, sliceStart], - ); - const handleRefresh = () => { if (!agentList) return; setSliceStart(Math.floor((Math.random() * agentList.length) / 2)); @@ -108,7 +88,23 @@ const AgentsSuggest = memo<{ mobile?: boolean }>(({ mobile }) => { /> - {isLoading ? loadingCards : cards} + {isLoading + ? loadingCards + : agentList.slice(sliceStart, sliceStart + agentLength).map((agent) => ( + + + + + + {agent.meta.title} + + + {agent.meta.description} + + + + + ))} ); diff --git a/src/layout/GlobalProvider/AppTheme.tsx b/src/layout/GlobalProvider/AppTheme.tsx index 4a5199641f4d..ff32f123bc5a 100644 --- a/src/layout/GlobalProvider/AppTheme.tsx +++ b/src/layout/GlobalProvider/AppTheme.tsx @@ -30,12 +30,25 @@ const useStyles = createStyles(({ css, token }) => ({ height: 100%; min-height: 100dvh; max-height: 100dvh; + + @media (min-device-width: 576px) { + overflow: hidden; + } `, // scrollbar-width and scrollbar-color are supported from Chrome 121 // https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color scrollbar: css` scrollbar-color: ${token.colorFill} transparent; scrollbar-width: thin; + + #lobe-mobile-scroll-container { + scrollbar-width: none; + + ::-webkit-scrollbar { + width: 0; + height: 0; + } + } `, // so this is a polyfill for older browsers diff --git a/src/store/user/slices/preference/initialState.ts b/src/store/user/slices/preference/initialState.ts index 8691b36e692b..5ff02ee7c976 100644 --- a/src/store/user/slices/preference/initialState.ts +++ b/src/store/user/slices/preference/initialState.ts @@ -32,6 +32,7 @@ export interface UserPreferenceState { export const DEFAULT_PREFERENCE: UserPreference = { guide: { moveSettingsToAvatar: true, + topic: true, }, telemetry: null, useCmdEnterToSend: false, diff --git a/src/styles/global.ts b/src/styles/global.ts index 5b2739506242..0dc17cf1fbbc 100644 --- a/src/styles/global.ts +++ b/src/styles/global.ts @@ -16,10 +16,33 @@ export default ({ token }: { prefixCls: string; token: Theme }) => css` max-height: 100dvh; background: ${token.colorBgLayout}; + + @media (min-device-width: 576px) { + overflow: hidden; + } } * { scrollbar-color: ${token.colorFill} transparent; scrollbar-width: thin; + + ::-webkit-scrollbar { + width: 0.75em; + height: 0.75em; + } + + ::-webkit-scrollbar-thumb { + border-radius: 10px; + } + + :hover::-webkit-scrollbar-thumb { + background-color: ${token.colorText}; + background-clip: content-box; + border: 3px solid transparent; + } + + ::-webkit-scrollbar-track { + background-color: transparent; + } } `;