diff --git a/locales/ar/common.json b/locales/ar/common.json index 1b92d10b81bd..b85550f8a7b1 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -28,6 +28,7 @@ }, "feedback": "تقديم ملاحظات", "follow": "تابعنا على {{name}}", + "fullscreen": "وضع كامل الشاشة", "historyRange": "نطاق التاريخ", "import": "استيراد الإعدادات", "importModal": { diff --git a/locales/bg-BG/common.json b/locales/bg-BG/common.json index 7f7c82e50430..f3e4d73478af 100644 --- a/locales/bg-BG/common.json +++ b/locales/bg-BG/common.json @@ -28,6 +28,7 @@ }, "feedback": "Обратна връзка", "follow": "Следете ни на {{name}}", + "fullscreen": "Цял екран", "historyRange": "Диапазон на историята", "import": "Импортирай конфигурация", "importModal": { diff --git a/locales/de-DE/common.json b/locales/de-DE/common.json index 48390954049c..596d92bea87d 100644 --- a/locales/de-DE/common.json +++ b/locales/de-DE/common.json @@ -28,6 +28,7 @@ }, "feedback": "Feedback und Vorschläge", "follow": "Folge uns auf {{name}}", + "fullscreen": "Vollbildmodus", "historyRange": "Verlaufsbereich", "import": "Importieren", "importModal": { diff --git a/locales/en-US/common.json b/locales/en-US/common.json index d7e51b71334f..2ca18ea61ac9 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -28,6 +28,7 @@ }, "feedback": "Feedback", "follow": "Follow us on {{name}}", + "fullscreen": "Full Screen Mode", "historyRange": "History Range", "import": "Import Configuration", "importModal": { diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index f874678f453e..380d0f631a26 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -28,6 +28,7 @@ }, "feedback": "Comentarios y sugerencias", "follow": "Síguenos en {{name}}", + "fullscreen": "Pantalla completa", "historyRange": "Rango de historial", "import": "Importar configuración", "importModal": { diff --git a/locales/fr-FR/common.json b/locales/fr-FR/common.json index a76bf2cab449..bf5b2dc8b214 100644 --- a/locales/fr-FR/common.json +++ b/locales/fr-FR/common.json @@ -28,6 +28,7 @@ }, "feedback": "Retour d'information et suggestions", "follow": "Suivez-nous sur {{name}}", + "fullscreen": "Mode plein écran", "historyRange": "Plage d'historique", "import": "Importer", "importModal": { diff --git a/locales/it-IT/common.json b/locales/it-IT/common.json index 87235355377f..88924b5b0758 100644 --- a/locales/it-IT/common.json +++ b/locales/it-IT/common.json @@ -28,6 +28,7 @@ }, "feedback": "Feedback e suggerimenti", "follow": "Seguici su {{name}}", + "fullscreen": "Modalità a schermo intero", "historyRange": "Intervallo cronologico", "import": "Importa configurazione", "importModal": { diff --git a/locales/ja-JP/common.json b/locales/ja-JP/common.json index d1b5833772e7..71908363750b 100644 --- a/locales/ja-JP/common.json +++ b/locales/ja-JP/common.json @@ -28,6 +28,7 @@ }, "feedback": "フィードバック", "follow": " {{name}} で私たちをフォローする", + "fullscreen": "フルスクリーンモード", "historyRange": "履歴範囲", "import": "インポート", "importModal": { diff --git a/locales/ko-KR/common.json b/locales/ko-KR/common.json index 547207cd809d..93ed239849f3 100644 --- a/locales/ko-KR/common.json +++ b/locales/ko-KR/common.json @@ -28,6 +28,7 @@ }, "feedback": "피드백 및 제안", "follow": "{{name}}에서 우리를 팔로우하세요", + "fullscreen": "전체 화면", "historyRange": "기록 범위", "import": "가져오기", "importModal": { diff --git a/locales/nl-NL/common.json b/locales/nl-NL/common.json index 5b1af2e26850..919eab14119f 100644 --- a/locales/nl-NL/common.json +++ b/locales/nl-NL/common.json @@ -28,6 +28,7 @@ }, "feedback": "Feedback en suggesties", "follow": "Volg ons op {{name}}", + "fullscreen": "Volledig scherm", "historyRange": "Geschiedenisbereik", "import": "Importeren", "importModal": { diff --git a/locales/pl-PL/common.json b/locales/pl-PL/common.json index f98ee02b1cce..b3e22ce5adf8 100644 --- a/locales/pl-PL/common.json +++ b/locales/pl-PL/common.json @@ -28,6 +28,7 @@ }, "feedback": "Opinie i sugestie", "follow": "Zaobserwuj nas na {{name}}", + "fullscreen": "Tryb pełnoekranowy", "historyRange": "Zakres historii", "import": "Importuj ustawienia", "importModal": { diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index b0d93098e026..e71d4d7b4dee 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -28,6 +28,7 @@ }, "feedback": "Feedback e sugestões", "follow": "Siga-nos no {{name}}", + "fullscreen": "Modo de Tela Cheia", "historyRange": "Intervalo de histórico", "import": "Importar configuração", "importModal": { diff --git a/locales/ru-RU/common.json b/locales/ru-RU/common.json index e11a78bc4507..af75634182cd 100644 --- a/locales/ru-RU/common.json +++ b/locales/ru-RU/common.json @@ -28,6 +28,7 @@ }, "feedback": "Обратная связь и предложения", "follow": "Подпишитесь на нас на {{name}}", + "fullscreen": "Полноэкранный режим", "historyRange": "История", "import": "Импорт настроек", "importModal": { diff --git a/locales/tr-TR/common.json b/locales/tr-TR/common.json index 8dd54d7bacb0..660896789079 100644 --- a/locales/tr-TR/common.json +++ b/locales/tr-TR/common.json @@ -28,6 +28,7 @@ }, "feedback": "Feedback", "follow": "Bizi {{name}} üzerinde takip edin", + "fullscreen": "Tam Ekran Modu", "historyRange": "Geçmiş Aralığı", "import": "İçe Aktar", "importModal": { diff --git a/locales/vi-VN/common.json b/locales/vi-VN/common.json index 1b34a99f6de8..ea90f9ea7a63 100644 --- a/locales/vi-VN/common.json +++ b/locales/vi-VN/common.json @@ -28,6 +28,7 @@ }, "feedback": "Phản hồi và đề xuất", "follow": "Theo dõi chúng tôi trên {{name}}", + "fullscreen": "Chế độ toàn màn hình", "historyRange": "Phạm vi lịch sử", "import": "Nhập cấu hình", "importModal": { diff --git a/locales/zh-CN/common.json b/locales/zh-CN/common.json index b8c23af1e92d..db15b4488158 100644 --- a/locales/zh-CN/common.json +++ b/locales/zh-CN/common.json @@ -28,6 +28,7 @@ }, "feedback": "反馈与建议", "follow": "在 {{name}} 上关注我们", + "fullscreen": "全屏模式", "historyRange": "历史范围", "import": "导入配置", "importModal": { diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 94b5972d3aa7..7529d58bceb1 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -28,6 +28,7 @@ }, "feedback": "回饋與建議", "follow": "在 {{name}} 上關注我們", + "fullscreen": "全螢幕模式", "historyRange": "歷史範圍", "import": "匯入設定", "importModal": { diff --git a/src/app/(main)/chat/(desktop)/features/ChatHeader/Main.tsx b/src/app/(main)/chat/(desktop)/features/ChatHeader/Main.tsx index 56d80c380ba8..7fd29e4fd5a7 100644 --- a/src/app/(main)/chat/(desktop)/features/ChatHeader/Main.tsx +++ b/src/app/(main)/chat/(desktop)/features/ChatHeader/Main.tsx @@ -1,21 +1,18 @@ import { Avatar, ChatHeaderTitle } from '@lobehub/ui'; import { Skeleton } from 'antd'; -import { useRouter } from 'next/navigation'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes'; import { useSessionStore } from '@/store/session'; import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors'; -import { pathString } from '@/utils/url'; import Tags from './Tags'; const Main = memo(() => { const { t } = useTranslation('chat'); - const router = useRouter(); - const [init, isInbox, title, description, avatar, backgroundColor] = useSessionStore((s) => [ sessionSelectors.isSomeSessionActive(s), sessionSelectors.isInboxSession(s), @@ -25,6 +22,8 @@ const Main = memo(() => { sessionMetaSelectors.currentAgentBackgroundColor(s), ]); + const openChatSettings = useOpenChatSettings(); + const displayTitle = isInbox ? t('inbox.title') : title; const displayDesc = isInbox ? t('inbox.desc') : description; @@ -42,11 +41,7 @@ const Main = memo(() => { - isInbox - ? router.push('/settings/agent') - : router.push(pathString('/chat/settings', { search: location.search })) - } + onClick={openChatSettings} size={40} title={title} /> diff --git a/src/app/(main)/chat/(desktop)/features/SideBar/SystemRole/index.tsx b/src/app/(main)/chat/(desktop)/features/SideBar/SystemRole/index.tsx index 73033631d7e3..05959b9dc6ad 100644 --- a/src/app/(main)/chat/(desktop)/features/SideBar/SystemRole/index.tsx +++ b/src/app/(main)/chat/(desktop)/features/SideBar/SystemRole/index.tsx @@ -1,28 +1,27 @@ import { ActionIcon, EditableMessage } from '@lobehub/ui'; import { Skeleton } from 'antd'; import { Edit } from 'lucide-react'; -import { useRouter } from 'next/navigation'; import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; import useMergeState from 'use-merge-value'; +import SidebarHeader from '@/components/SidebarHeader'; import AgentInfo from '@/features/AgentInfo'; +import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes'; import { useAgentStore } from '@/store/agent'; import { agentSelectors } from '@/store/agent/selectors'; import { useGlobalStore } from '@/store/global'; +import { ChatSettingsTabs } from '@/store/global/initialState'; import { useSessionStore } from '@/store/session'; import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors'; -import { pathString } from '@/utils/url'; -import SidebarHeader from '../../../../components/SidebarHeader'; import { useStyles } from './style'; const SystemRole = memo(() => { - const router = useRouter(); const [editing, setEditing] = useState(false); const { styles } = useStyles(); - + const openChatSettings = useOpenChatSettings(ChatSettingsTabs.Prompt); const [init, meta] = useSessionStore((s) => [ sessionSelectors.isSomeSessionActive(s), sessionMetaSelectors.currentAgentMeta(s), @@ -93,7 +92,7 @@ const SystemRole = memo(() => { onAvatarClick={() => { setOpen(false); setEditing(false); - router.push(pathString('/chat/settings', { search: location.search })); + openChatSettings(); }} style={{ marginBottom: 16 }} /> diff --git a/src/app/(main)/chat/features/SettingButton.tsx b/src/app/(main)/chat/features/SettingButton.tsx index d7d384604142..c82560540ff9 100644 --- a/src/app/(main)/chat/features/SettingButton.tsx +++ b/src/app/(main)/chat/features/SettingButton.tsx @@ -4,30 +4,16 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { DESKTOP_HEADER_ICON_SIZE, MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens'; -import { useQueryRoute } from '@/hooks/useQueryRoute'; -import { useGlobalStore } from '@/store/global'; -import { SidebarTabKey } from '@/store/global/initialState'; -import { useSessionStore } from '@/store/session'; -import { sessionSelectors } from '@/store/session/selectors'; +import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes'; const SettingButton = memo<{ mobile?: boolean }>(({ mobile }) => { - const isInbox = useSessionStore(sessionSelectors.isInboxSession); const { t } = useTranslation('common'); - const router = useQueryRoute(); + const openChatSettings = useOpenChatSettings(); return ( { - if (isInbox) { - useGlobalStore.setState({ - sidebarKey: SidebarTabKey.Setting, - }); - router.push('/settings/agent'); - } else { - router.push('/chat/settings'); - } - }} + onClick={openChatSettings} size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE} title={t('header.session', { ns: 'setting' })} /> diff --git a/src/app/(main)/chat/features/TopicListContent/Header.tsx b/src/app/(main)/chat/features/TopicListContent/Header.tsx index d1a8c26bb995..5816fb182326 100644 --- a/src/app/(main)/chat/features/TopicListContent/Header.tsx +++ b/src/app/(main)/chat/features/TopicListContent/Header.tsx @@ -5,10 +5,10 @@ import { memo, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import SidebarHeader from '@/components/SidebarHeader'; import { useChatStore } from '@/store/chat'; import { topicSelectors } from '@/store/chat/selectors'; -import SidebarHeader from '../../components/SidebarHeader'; import TopicSearchBar from './TopicSearchBar'; const Header = memo(() => { diff --git a/src/app/(main)/chat/settings/_layout/Desktop/Header.tsx b/src/app/(main)/chat/settings/_layout/Desktop/Header.tsx index b0fd25b68e0d..a5ccd67eef97 100644 --- a/src/app/(main)/chat/settings/_layout/Desktop/Header.tsx +++ b/src/app/(main)/chat/settings/_layout/Desktop/Header.tsx @@ -5,9 +5,10 @@ import { useRouter } from 'next/navigation'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import HeaderContent from '@/app/(main)/chat/settings/features/HeaderContent'; import { pathString } from '@/utils/url'; +import HeaderContent from '../../features/HeaderContent'; + const Header = memo(() => { const { t } = useTranslation('setting'); const router = useRouter(); diff --git a/src/app/(main)/chat/settings/_layout/Mobile/Header.tsx b/src/app/(main)/chat/settings/_layout/Mobile/Header.tsx index 455a6dfa26ad..beb425acdaf0 100644 --- a/src/app/(main)/chat/settings/_layout/Mobile/Header.tsx +++ b/src/app/(main)/chat/settings/_layout/Mobile/Header.tsx @@ -5,10 +5,11 @@ import { useRouter } from 'next/navigation'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import HeaderContent from '@/app/(main)/chat/settings/features/HeaderContent'; import { mobileHeaderSticky } from '@/styles/mobileHeader'; import { pathString } from '@/utils/url'; +import HeaderContent from '../../features/HeaderContent'; + const Header = memo(() => { const { t } = useTranslation('setting'); const router = useRouter(); diff --git a/src/app/(main)/chat/settings/modal/page.tsx b/src/app/(main)/chat/settings/modal/page.tsx new file mode 100644 index 000000000000..8863499c1fab --- /dev/null +++ b/src/app/(main)/chat/settings/modal/page.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { useLayoutEffect } from 'react'; + +import { useQueryRoute } from '@/hooks/useQueryRoute'; + +/** + * @description: Chat Settings Modal (intercepting routes fallback when hard refresh) + * @example: /chat/settings/modal?tab=prompt => /chat/settings + * @refs: https://github.com/lobehub/lobe-chat/discussions/2295#discussioncomment-9290942 + */ + +const ChatSettingsModalFallback = () => { + const router = useQueryRoute(); + + useLayoutEffect(() => { + router.replace('/chat/settings', { query: { tab: '' } }); + }, []); + + return null; +}; + +export default ChatSettingsModalFallback; diff --git a/src/app/(main)/settings/@category/features/CategoryContent.tsx b/src/app/(main)/settings/@category/features/CategoryContent.tsx index 012dce65e795..06cea712edee 100644 --- a/src/app/(main)/settings/@category/features/CategoryContent.tsx +++ b/src/app/(main)/settings/@category/features/CategoryContent.tsx @@ -21,7 +21,11 @@ const CategoryContent = memo<{ modal?: boolean }>(({ modal }) => { { - router.push(urlJoin('/settings', key)); + if (modal) { + router.replace('/settings/modal', { query: { tab: key } }); + } else { + router.push(urlJoin('/settings', key)); + } }} selectable selectedKeys={[modal ? tab : (activeTab as any)]} diff --git a/src/app/(main)/settings/modal/page.tsx b/src/app/(main)/settings/modal/page.tsx new file mode 100644 index 000000000000..fe9214f96dda --- /dev/null +++ b/src/app/(main)/settings/modal/page.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { useLayoutEffect } from 'react'; +import urlJoin from 'url-join'; + +import { useQuery } from '@/hooks/useQuery'; +import { useQueryRoute } from '@/hooks/useQueryRoute'; +import { SettingsTabs } from '@/store/global/initialState'; + +/** + * @description: Settings Modal (intercepting routes fallback when hard refresh) + * @example: /settings/modal?tab=common => /settings/common + * @refs: https://github.com/lobehub/lobe-chat/discussions/2295#discussioncomment-9290942 + */ + +const SettingsModalFallback = () => { + const { tab = SettingsTabs.Common } = useQuery(); + const router = useQueryRoute(); + + useLayoutEffect(() => { + router.replace(urlJoin('/settings', tab as SettingsTabs), { query: { tab: '' } }); + }, []); + + return null; +}; + +export default SettingsModalFallback; diff --git a/src/app/@modal/(.)settings/modal/index.tsx b/src/app/@modal/(.)settings/modal/index.tsx new file mode 100644 index 000000000000..9423d005865e --- /dev/null +++ b/src/app/@modal/(.)settings/modal/index.tsx @@ -0,0 +1,40 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import { memo } from 'react'; + +import { useQuery } from '@/hooks/useQuery'; +import { SettingsTabs } from '@/store/global/initialState'; + +import Skeleton from './loading'; + +const loading = () => ; + +const Common = dynamic(() => import('@/app/(main)/settings/common'), { loading, ssr: false }); +const About = dynamic(() => import('@/app/(main)/settings/about'), { loading, ssr: false }); +const LLM = dynamic(() => import('@/app/(main)/settings/llm'), { loading, ssr: false }); +const TTS = dynamic(() => import('@/app/(main)/settings/tts'), { loading, ssr: false }); +const Agent = dynamic(() => import('@/app/(main)/settings/agent'), { loading, ssr: false }); +const Sync = dynamic(() => import('@/app/(main)/settings/sync'), { loading, ssr: false }); + +interface SettingsModalProps { + browser?: string; + mobile?: boolean; + os?: string; +} + +const SettingsModal = memo(({ browser, os, mobile }) => { + const { tab = SettingsTabs.Common } = useQuery(); + return ( + <> + {tab === SettingsTabs.Common && } + {tab === SettingsTabs.Sync && } + {tab === SettingsTabs.LLM && } + {tab === SettingsTabs.TTS && } + {tab === SettingsTabs.Agent && } + {tab === SettingsTabs.About && } + + ); +}); + +export default SettingsModal; diff --git a/src/app/@modal/(.)settings/modal/layout.tsx b/src/app/@modal/(.)settings/modal/layout.tsx new file mode 100644 index 000000000000..604b2a791fd3 --- /dev/null +++ b/src/app/@modal/(.)settings/modal/layout.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { Skeleton } from 'antd'; +import dynamic from 'next/dynamic'; +import { PropsWithChildren, memo } from 'react'; + +import SettingModalLayout from '../../_layout/SettingModalLayout'; + +const CategoryContent = dynamic( + () => import('@/app/(main)/settings/@category/features/CategoryContent'), + { loading: () => , ssr: false }, +); +const UpgradeAlert = dynamic(() => import('@/app/(main)/settings/features/UpgradeAlert'), { + ssr: false, +}); + +const Layout = memo(({ children }) => { + return ( + + + + + } + > + {children} + + ); +}); + +export default Layout; diff --git a/src/app/@modal/(.)settings/modal/loading.tsx b/src/app/@modal/(.)settings/modal/loading.tsx new file mode 100644 index 000000000000..f99251a28e34 --- /dev/null +++ b/src/app/@modal/(.)settings/modal/loading.tsx @@ -0,0 +1,5 @@ +import { Skeleton } from 'antd'; + +export default () => { + return ; +}; diff --git a/src/app/@modal/(.)settings/modal/page.tsx b/src/app/@modal/(.)settings/modal/page.tsx new file mode 100644 index 000000000000..a269e1ba3136 --- /dev/null +++ b/src/app/@modal/(.)settings/modal/page.tsx @@ -0,0 +1,19 @@ +import { gerServerDeviceInfo, isMobileDevice } from '@/utils/responsive'; + +import SettingsModal from './index'; + +/** + * @description: Settings Modal (intercepting route: /settings/modal ) + * @refs: https://github.com/lobehub/lobe-chat/discussions/2295#discussioncomment-9290942 + */ + +const Page = () => { + const isMobile = isMobileDevice(); + const { os, browser } = gerServerDeviceInfo(); + + return ; +}; + +Page.displayName = 'SettingModal'; + +export default Page; diff --git a/src/app/@modal/_layout/SettingModalLayout.tsx b/src/app/@modal/_layout/SettingModalLayout.tsx new file mode 100644 index 000000000000..7e07e07745b6 --- /dev/null +++ b/src/app/@modal/_layout/SettingModalLayout.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { useResponsive, useTheme, useThemeMode } from 'antd-style'; +import { ReactNode, memo, useRef } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import Header from '@/app/(main)/settings/_layout/Desktop/Header'; +import SideBar from '@/app/(main)/settings/_layout/Desktop/SideBar'; + +interface SettingLayoutProps { + category: ReactNode; + children: ReactNode; + desc?: string; + title?: string; +} + +const SettingModalLayout = memo(({ children, category, desc, title }) => { + const ref = useRef(null); + const theme = useTheme(); + const { isDarkMode } = useThemeMode(); + const { md = true } = useResponsive(); + + return ( + <> + {md ? ( + + {category} + + ) : ( +
ref.current}>{category}
+ )} + + {children} + + + ); +}); + +SettingModalLayout.displayName = 'SettingModalLayout'; + +export default SettingModalLayout; diff --git a/src/app/@modal/chat/(.)settings/modal/features/CategoryContent.tsx b/src/app/@modal/chat/(.)settings/modal/features/CategoryContent.tsx new file mode 100644 index 000000000000..32b76ade5cde --- /dev/null +++ b/src/app/@modal/chat/(.)settings/modal/features/CategoryContent.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { memo } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import HeaderContent from '@/app/(main)/chat/settings/features/HeaderContent'; +import Menu from '@/components/Menu'; +import { useQuery } from '@/hooks/useQuery'; +import { useQueryRoute } from '@/hooks/useQueryRoute'; +import { ChatSettingsTabs } from '@/store/global/initialState'; + +import { useCategory } from './useCategory'; + +const CategoryContent = memo(() => { + const cateItems = useCategory(); + const router = useQueryRoute(); + const { tab = ChatSettingsTabs.Meta } = useQuery(); + + return ( + <> + { + router.replace('/chat/settings/modal', { query: { tab: key } }); + }} + selectable + selectedKeys={[tab as any]} + variant={'compact'} + /> + + + + + ); +}); + +export default CategoryContent; diff --git a/src/app/@modal/chat/(.)settings/modal/features/useCategory.tsx b/src/app/@modal/chat/(.)settings/modal/features/useCategory.tsx new file mode 100644 index 000000000000..dd0d87a39233 --- /dev/null +++ b/src/app/@modal/chat/(.)settings/modal/features/useCategory.tsx @@ -0,0 +1,54 @@ +import { Icon } from '@lobehub/ui'; +import { Blocks, Bot, BrainCog, MessagesSquare, Mic2, UserCircle } from 'lucide-react'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { MenuProps } from '@/components/Menu'; +import { ChatSettingsTabs } from '@/store/global/initialState'; + +interface UseCategoryOptions { + mobile?: boolean; +} + +export const useCategory = ({ mobile }: UseCategoryOptions = {}) => { + const { t } = useTranslation('setting'); + const iconSize = mobile ? { fontSize: 20 } : undefined; + + const cateItems: MenuProps['items'] = useMemo( + () => [ + { + icon: , + key: ChatSettingsTabs.Meta, + label: t('settingAgent.title'), + }, + { + icon: , + key: ChatSettingsTabs.Prompt, + label: t('settingAgent.prompt.title'), + }, + { + icon: , + key: ChatSettingsTabs.Chat, + label: t('settingChat.title'), + }, + { + icon: , + key: ChatSettingsTabs.Modal, + label: t('settingModel.title'), + }, + { + icon: , + key: ChatSettingsTabs.TTS, + label: t('settingTTS.title'), + }, + { + icon: , + key: ChatSettingsTabs.Plugin, + label: t('settingPlugin.title'), + }, + ], + [t], + ); + + return cateItems; +}; diff --git a/src/app/@modal/chat/(.)settings/modal/layout.tsx b/src/app/@modal/chat/(.)settings/modal/layout.tsx new file mode 100644 index 000000000000..e3c409105a56 --- /dev/null +++ b/src/app/@modal/chat/(.)settings/modal/layout.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { Skeleton } from 'antd'; +import isEqual from 'fast-deep-equal'; +import dynamic from 'next/dynamic'; +import { PropsWithChildren, memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import StoreUpdater from '@/features/AgentSetting/StoreUpdater'; +import { Provider, createStore } from '@/features/AgentSetting/store'; +import { useAgentStore } from '@/store/agent'; +import { agentSelectors } from '@/store/agent/slices/chat'; +import { useSessionStore } from '@/store/session'; +import { sessionMetaSelectors } from '@/store/session/selectors'; + +import SettingModalLayout from '../../../_layout/SettingModalLayout'; + +const CategoryContent = dynamic(() => import('./features/CategoryContent'), { + loading: () => , + ssr: false, +}); + +const Layout = memo(({ children }) => { + const { t } = useTranslation('setting'); + const id = useSessionStore((s) => s.activeId); + const config = useAgentStore(agentSelectors.currentAgentConfig, isEqual); + const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual); + const [updateAgentConfig] = useAgentStore((s) => [s.updateAgentConfig]); + + const [updateAgentMeta] = useSessionStore((s) => [ + s.updateSessionMeta, + sessionMetaSelectors.currentAgentTitle(s), + ]); + + return ( + } + desc={t('header.sessionDesc')} + title={t('header.session')} + > + + + {children} + + + ); +}); + +export default Layout; diff --git a/src/app/@modal/chat/(.)settings/modal/loading.tsx b/src/app/@modal/chat/(.)settings/modal/loading.tsx new file mode 100644 index 000000000000..f99251a28e34 --- /dev/null +++ b/src/app/@modal/chat/(.)settings/modal/loading.tsx @@ -0,0 +1,5 @@ +import { Skeleton } from 'antd'; + +export default () => { + return ; +}; diff --git a/src/app/@modal/chat/(.)settings/modal/page.tsx b/src/app/@modal/chat/(.)settings/modal/page.tsx new file mode 100644 index 000000000000..13d213a754ba --- /dev/null +++ b/src/app/@modal/chat/(.)settings/modal/page.tsx @@ -0,0 +1,55 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +import { useQuery } from '@/hooks/useQuery'; +import { ChatSettingsTabs } from '@/store/global/initialState'; + +import Skeleton from './loading'; + +const loading = () => ; + +const AgentMeta = dynamic(() => import('@/features/AgentSetting/AgentMeta'), { + loading, + ssr: false, +}); +const AgentChat = dynamic(() => import('@/features/AgentSetting/AgentChat'), { + loading, + ssr: false, +}); +const AgentPrompt = dynamic(() => import('@/features/AgentSetting/AgentPrompt'), { + loading, + ssr: false, +}); +const AgentPlugin = dynamic(() => import('@/features/AgentSetting/AgentPlugin'), { + loading, + ssr: false, +}); +const AgentModal = dynamic(() => import('@/features/AgentSetting/AgentModal'), { + loading, + ssr: false, +}); +const AgentTTS = dynamic(() => import('@/features/AgentSetting/AgentTTS'), { loading, ssr: false }); + +/** + * @description: Agent Settings Modal (intercepting route: /chat/settings/modal ) + * @refs: https://github.com/lobehub/lobe-chat/discussions/2295#discussioncomment-9290942 + */ + +const Page = () => { + const { tab = ChatSettingsTabs.Meta } = useQuery(); + return ( + <> + {tab === ChatSettingsTabs.Meta && } + {tab === ChatSettingsTabs.Prompt && } + {tab === ChatSettingsTabs.Chat && } + {tab === ChatSettingsTabs.Modal && } + {tab === ChatSettingsTabs.TTS && } + {tab === ChatSettingsTabs.Plugin && } + + ); +}; + +Page.displayName = 'AgentSettingModal'; + +export default Page; diff --git a/src/app/@modal/default.tsx b/src/app/@modal/default.tsx new file mode 100644 index 000000000000..56c849e56170 --- /dev/null +++ b/src/app/@modal/default.tsx @@ -0,0 +1,3 @@ +// This ensures that the modal is not rendered when it's not active. + +export default () => null; diff --git a/src/app/@modal/error.tsx b/src/app/@modal/error.tsx new file mode 100644 index 000000000000..071491038c70 --- /dev/null +++ b/src/app/@modal/error.tsx @@ -0,0 +1,5 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +export default dynamic(() => import('@/components/Error')); diff --git a/src/app/@modal/layout.tsx b/src/app/@modal/layout.tsx new file mode 100644 index 000000000000..6ebb7e566fed --- /dev/null +++ b/src/app/@modal/layout.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { Modal } from '@lobehub/ui'; +import { useRouter } from 'next/navigation'; +import { PropsWithChildren, memo, useState } from 'react'; + +const SessionSettingsModal = memo(({ children }) => { + const [open, setOpen] = useState(true); + const router = useRouter(); + + return ( + { + router.back(); + }} + footer={null} + onCancel={() => setOpen(false)} + open={open} + styles={{ + body: { display: 'flex', minHeight: 'min(75vh, 750px)', overflow: 'hidden', padding: 0 }, + }} + title={false} + width={1024} + > + {children} + + ); +}); + +export default SessionSettingsModal; diff --git a/src/app/@modal/loading.tsx b/src/app/@modal/loading.tsx new file mode 100644 index 000000000000..f3b20d84370d --- /dev/null +++ b/src/app/@modal/loading.tsx @@ -0,0 +1,5 @@ +import { Skeleton } from 'antd'; + +export default () => { + return ; +}; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 191b8c5f35bc..cbec8f14f561 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -12,9 +12,10 @@ import { isMobileDevice } from '@/utils/responsive'; type RootLayoutProps = { children: ReactNode; + modal: ReactNode; }; -const RootLayout = async ({ children }: RootLayoutProps) => { +const RootLayout = async ({ children, modal }: RootLayoutProps) => { const cookieStore = cookies(); const lang = cookieStore.get(LOBE_LOCALE_COOKIE); @@ -24,7 +25,10 @@ const RootLayout = async ({ children }: RootLayoutProps) => { - {children} + + {children} + {modal} + diff --git a/src/app/(main)/chat/components/SidebarHeader/index.tsx b/src/components/SidebarHeader/index.tsx similarity index 100% rename from src/app/(main)/chat/components/SidebarHeader/index.tsx rename to src/components/SidebarHeader/index.tsx diff --git a/src/features/Conversation/Messages/index.ts b/src/features/Conversation/Messages/index.ts index 3ba722cb8349..273223593c66 100644 --- a/src/features/Conversation/Messages/index.ts +++ b/src/features/Conversation/Messages/index.ts @@ -1,10 +1,7 @@ -import { useResponsive } from 'antd-style'; -import { useRouter } from 'next/navigation'; - +import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes'; import { useGlobalStore } from '@/store/global'; import { useSessionStore } from '@/store/session'; import { sessionSelectors } from '@/store/session/selectors'; -import { pathString } from '@/utils/url'; import { OnAvatarsClick, RenderMessage } from '../types'; import { AssistantMessage } from './Assistant'; @@ -22,18 +19,18 @@ export const renderMessages: Record = { export const useAvatarsClick = (): OnAvatarsClick => { const [isInbox] = useSessionStore((s) => [sessionSelectors.isInboxSession(s)]); const [toggleSystemRole] = useGlobalStore((s) => [s.toggleSystemRole]); - const { mobile } = useResponsive(); - const router = useRouter(); + const openChatSettings = useOpenChatSettings(); return (role) => { switch (role) { case 'assistant': { - return () => - isInbox - ? router.push('/settings/agent') - : mobile - ? router.push(pathString('/chat/settings', { search: location.search })) - : toggleSystemRole(true); + return () => { + if (!isInbox) { + toggleSystemRole(true); + } else { + openChatSettings(); + } + }; } } }; diff --git a/src/features/MobileTabBar/index.tsx b/src/features/MobileTabBar/index.tsx index 54afa8b47429..c7e88bbcd47c 100644 --- a/src/features/MobileTabBar/index.tsx +++ b/src/features/MobileTabBar/index.tsx @@ -6,6 +6,7 @@ import { rgba } from 'polished'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useOpenSettings } from '@/hooks/useInterceptingRoutes'; import { SidebarTabKey } from '@/store/global/initialState'; const useStyles = createStyles(({ css, token }) => ({ @@ -24,6 +25,7 @@ interface Props { export default memo(({ className, tabBarKey }) => { const { t } = useTranslation('common'); const { styles } = useStyles(); + const openSettings = useOpenSettings(); const router = useRouter(); const items: MobileTabBarProps['items'] = useMemo( () => [ @@ -48,9 +50,7 @@ export default memo(({ className, tabBarKey }) => { { icon: (active) => , key: SidebarTabKey.Setting, - onClick: () => { - router.push('/settings'); - }, + onClick: openSettings, title: t('tab.setting'), }, ], diff --git a/src/features/User/UserPanel/UserInfo.tsx b/src/features/User/UserPanel/UserInfo.tsx deleted file mode 100644 index 6f423769da4e..000000000000 --- a/src/features/User/UserPanel/UserInfo.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { createStyles } from 'antd-style'; -import { memo } from 'react'; -import { Flexbox } from 'react-layout-kit'; - -import UserAvatar from '@/features/User/UserAvatar'; - -const useStyles = createStyles(({ css, token }) => ({ - nickname: css` - font-size: 16px; - font-weight: bold; - line-height: 1; - `, - username: css` - line-height: 1; - color: ${token.colorTextDescription}; - `, -})); - -// TODO - -const UserInfo = memo<{ onClick?: () => void }>(({ onClick }) => { - const { styles, theme } = useStyles(); - - return ( - - - -
{'社区版用户'}
-
{'Community Edition'}
-
-
- ); -}); - -export default UserInfo; diff --git a/src/features/User/UserPanel/useMenu.tsx b/src/features/User/UserPanel/useMenu.tsx index 4ba047f657bd..2e3251e30c7a 100644 --- a/src/features/User/UserPanel/useMenu.tsx +++ b/src/features/User/UserPanel/useMenu.tsx @@ -1,4 +1,4 @@ -import { DiscordIcon, Icon } from '@lobehub/ui'; +import { ActionIcon, DiscordIcon, Icon } from '@lobehub/ui'; import { Badge } from 'antd'; import { Book, @@ -7,22 +7,29 @@ import { HardDriveUpload, LifeBuoy, Mail, + Maximize, Settings2, } from 'lucide-react'; import Link from 'next/link'; import { PropsWithChildren, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import urlJoin from 'url-join'; import { type MenuProps } from '@/components/Menu'; import { DISCORD, DOCUMENTS, EMAIL_SUPPORT, GITHUB_ISSUES } from '@/const/url'; import DataImporter from '@/features/DataImporter'; +import { useOpenSettings } from '@/hooks/useInterceptingRoutes'; +import { useQueryRoute } from '@/hooks/useQueryRoute'; import { configService } from '@/services/config'; +import { SettingsTabs } from '@/store/global/initialState'; import { useNewVersion } from './useNewVersion'; export const useMenu = () => { + const router = useQueryRoute(); const hasNewVersion = useNewVersion(); + const openSettings = useOpenSettings(); const { t } = useTranslation(['common', 'setting']); const NewVersionBadge = useCallback( @@ -38,6 +45,29 @@ export const useMenu = () => { [t], ); + const settings: MenuProps['items'] = [ + { + icon: , + key: 'setting', + label: ( + + + {t('userPanel.setting')} + + router.push(urlJoin('/settings', SettingsTabs.Common))} + size={'small'} + title={t('fullscreen')} + /> + + ), + }, + { + type: 'divider', + }, + ]; + const exports: MenuProps['items'] = [ { icon: , @@ -79,21 +109,6 @@ export const useMenu = () => { }, ]; - const settings: MenuProps['items'] = [ - { - icon: , - key: 'setting', - label: ( - - {t('userPanel.setting')} - - ), - }, - { - type: 'divider', - }, - ]; - const helps: MenuProps['items'] = [ { icon: , diff --git a/src/hooks/useInterceptingRoutes.test.ts b/src/hooks/useInterceptingRoutes.test.ts new file mode 100644 index 000000000000..8cb3f9ebbcd8 --- /dev/null +++ b/src/hooks/useInterceptingRoutes.test.ts @@ -0,0 +1,70 @@ +import { renderHook } from '@testing-library/react'; +import urlJoin from 'url-join'; +import { describe, expect, it, vi } from 'vitest'; + +import { INBOX_SESSION_ID } from '@/const/session'; +import { useIsMobile } from '@/hooks/useIsMobile'; +import { useGlobalStore } from '@/store/global'; +import { ChatSettingsTabs, SettingsTabs, SidebarTabKey } from '@/store/global/initialState'; +import { useSessionStore } from '@/store/session'; + +import { useOpenChatSettings, useOpenSettings } from './useInterceptingRoutes'; + +// Mocks +vi.mock('next/navigation', () => ({ + useRouter: vi.fn(() => ({ + push: vi.fn((href) => href), + replace: vi.fn((href) => href), + })), +})); +vi.mock('@/hooks/useQuery', () => ({ + useQuery: vi.fn(() => ({})), +})); +vi.mock('@/hooks/useIsMobile', () => ({ + useIsMobile: vi.fn(), +})); +vi.mock('@/store/session', () => ({ + useSessionStore: vi.fn(), +})); +vi.mock('@/store/global', () => ({ + useGlobalStore: { + setState: vi.fn(), + }, +})); + +describe('useOpenSettings', () => { + it('should handle mobile route correctly', () => { + vi.mocked(useIsMobile).mockReturnValue(true); + const { result } = renderHook(() => useOpenSettings(SettingsTabs.Common)); + expect(result.current()).toBe('/settings/common'); + }); + + it('should handle desktop route correctly', () => { + vi.mocked(useIsMobile).mockReturnValue(false); + const { result } = renderHook(() => useOpenSettings(SettingsTabs.Agent)); + expect(result.current()).toBe('/settings/modal?tab=agent'); + }); +}); + +describe('useOpenChatSettings', () => { + it('should handle inbox session id correctly', () => { + vi.mocked(useSessionStore).mockReturnValue(INBOX_SESSION_ID); + const { result } = renderHook(() => useOpenChatSettings()); + + expect(result.current()).toBe('/settings/modal?session=inbox&tab=agent'); // Assuming openSettings returns a function + }); + + it('should handle mobile route for chat settings', () => { + vi.mocked(useSessionStore).mockReturnValue('123'); + vi.mocked(useIsMobile).mockReturnValue(true); + const { result } = renderHook(() => useOpenChatSettings(ChatSettingsTabs.Meta)); + expect(result.current()).toBe('/chat/settings'); + }); + + it('should handle desktop route for chat settings with session and tab', () => { + vi.mocked(useSessionStore).mockReturnValue('456'); + vi.mocked(useIsMobile).mockReturnValue(false); + const { result } = renderHook(() => useOpenChatSettings(ChatSettingsTabs.Meta)); + expect(result.current()).toBe('/chat/settings/modal?session=456&tab=meta'); + }); +}); diff --git a/src/hooks/useInterceptingRoutes.ts b/src/hooks/useInterceptingRoutes.ts new file mode 100644 index 000000000000..253519ac9ffb --- /dev/null +++ b/src/hooks/useInterceptingRoutes.ts @@ -0,0 +1,46 @@ +import { useMemo } from 'react'; +import urlJoin from 'url-join'; + +import { INBOX_SESSION_ID } from '@/const/session'; +import { useIsMobile } from '@/hooks/useIsMobile'; +import { useQueryRoute } from '@/hooks/useQueryRoute'; +import { useGlobalStore } from '@/store/global'; +import { ChatSettingsTabs, SettingsTabs, SidebarTabKey } from '@/store/global/initialState'; +import { useSessionStore } from '@/store/session'; + +export const useOpenSettings = (tab: SettingsTabs = SettingsTabs.Common) => { + const activeId = useSessionStore((s) => s.activeId); + const router = useQueryRoute(); + const mobile = useIsMobile(); + + return useMemo(() => { + if (mobile) { + return () => router.push(urlJoin('/settings', tab)); + } else { + // use Intercepting Routes on Desktop + return () => router.push('/settings/modal', { query: { session: activeId, tab } }); + } + }, [mobile, tab, activeId, router]); +}; + +export const useOpenChatSettings = (tab: ChatSettingsTabs = ChatSettingsTabs.Meta) => { + const activeId = useSessionStore((s) => s.activeId); + const openSettings = useOpenSettings(SettingsTabs.Agent); + const router = useQueryRoute(); + const mobile = useIsMobile(); + + return useMemo(() => { + if (activeId === INBOX_SESSION_ID) { + useGlobalStore.setState({ + sidebarKey: SidebarTabKey.Setting, + }); + return openSettings; + } + if (mobile) { + return () => router.push('/chat/settings'); + } else { + // use Intercepting Routes on Desktop + return () => router.push('/chat/settings/modal', { query: { session: activeId, tab } }); + } + }, [openSettings, mobile, activeId, router, tab]); +}; diff --git a/src/hooks/useQuery.test.ts b/src/hooks/useQuery.test.ts index 9996a7818cde..d8c2f7ffc869 100644 --- a/src/hooks/useQuery.test.ts +++ b/src/hooks/useQuery.test.ts @@ -1,5 +1,4 @@ import { renderHook } from '@testing-library/react'; -import { useSearchParams } from 'next/navigation'; import { describe, expect, it, vi } from 'vitest'; import { useQuery } from './useQuery'; diff --git a/src/hooks/useQuery.ts b/src/hooks/useQuery.ts index e27bca2f1288..f79f78f1369c 100644 --- a/src/hooks/useQuery.ts +++ b/src/hooks/useQuery.ts @@ -1,7 +1,8 @@ import { useSearchParams } from 'next/navigation'; import qs from 'query-string'; +import { useMemo } from 'react'; export const useQuery = () => { const rawQuery = useSearchParams(); - return qs.parse(rawQuery.toString()); + return useMemo(() => qs.parse(rawQuery.toString()), [rawQuery]); }; diff --git a/src/layout/GlobalProvider/StoreInitialization.tsx b/src/layout/GlobalProvider/StoreInitialization.tsx index b84631abc1d4..0959b2d22154 100644 --- a/src/layout/GlobalProvider/StoreInitialization.tsx +++ b/src/layout/GlobalProvider/StoreInitialization.tsx @@ -49,12 +49,19 @@ const StoreInitialization = memo(() => { useEffect(() => { router.prefetch('/chat'); - router.prefetch('/chat/settings'); router.prefetch('/market'); - router.prefetch('/settings/common'); - router.prefetch('/settings/agent'); - router.prefetch('/settings/sync'); - }, [router]); + + if (mobile) { + router.prefetch('/me'); + router.prefetch('/chat/settings'); + router.prefetch('/settings/common'); + router.prefetch('/settings/agent'); + router.prefetch('/settings/sync'); + } else { + router.prefetch('/chat/settings/modal'); + router.prefetch('/settings/modal'); + } + }, [router, mobile]); return null; }); diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index 67f45f997217..b2d5bc26aa80 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -28,8 +28,9 @@ export default { }, feedback: '反馈与建议', follow: '在 {{name}} 上关注我们', - historyRange: '历史范围', + fullscreen: '全屏模式', + historyRange: '历史范围', import: '导入配置', importModal: { finish: { diff --git a/src/store/global/initialState.ts b/src/store/global/initialState.ts index 3297a1a3fecb..3db05d9b2207 100644 --- a/src/store/global/initialState.ts +++ b/src/store/global/initialState.ts @@ -10,6 +10,15 @@ export enum SidebarTabKey { Setting = 'settings', } +export enum ChatSettingsTabs { + Chat = 'chat', + Meta = 'meta', + Modal = 'modal', + Plugin = 'plugin', + Prompt = 'prompt', + TTS = 'tts', +} + export enum SettingsTabs { About = 'about', Agent = 'agent',