diff --git a/package.json b/package.json
index adb3764281..050d52de02 100644
--- a/package.json
+++ b/package.json
@@ -86,6 +86,7 @@
"electron-window-state": "^5.0.3",
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
"fast-xml-parser": "^5.2.0",
+ "framer-motion": "^12.17.0",
"franc-min": "^6.2.0",
"fs-extra": "^11.2.0",
"jsdom": "^26.0.0",
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index a327cef59d..94d3a564be 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -12,7 +12,6 @@ import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider'
import NavigationHandler from './handler/NavigationHandler'
-import MainSidebar from './pages/home/MainSidebar/MainSidebar'
import Routes from './Routes'
function App(): React.ReactElement {
@@ -27,7 +26,6 @@ function App(): React.ReactElement {
-
diff --git a/src/renderer/src/Routes.tsx b/src/renderer/src/Routes.tsx
index 410ce8d036..805cee726b 100644
--- a/src/renderer/src/Routes.tsx
+++ b/src/renderer/src/Routes.tsx
@@ -1,4 +1,7 @@
+import { AnimatePresence, motion } from 'framer-motion'
+import { useEffect, useState } from 'react'
import { Route, Routes, useLocation } from 'react-router-dom'
+import styled from 'styled-components'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
@@ -10,27 +13,93 @@ import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
+const WILDCARD_ROUTES = ['/settings', '/paintings', '/mcp-servers']
+
const RouteContainer = () => {
const location = useLocation()
const isHomePage = location.pathname === '/'
+ const [isReady, setIsReady] = useState(false)
+
+ // 获取当前路径的主路由部分
+ const mainPath = WILDCARD_ROUTES.find((route) => location.pathname.startsWith(route))
+
+ // 使用主路由作为 key,这样同一主路由下的切换不会触发动画
+ const animationKey = mainPath || location.pathname
+
+ // 路由变化时重置状态
+ useEffect(() => {
+ setIsReady(false)
+ // 给一个很短的延迟,确保组件已经渲染
+ const timer = setTimeout(() => setIsReady(true), 300)
+ return () => clearTimeout(timer)
+ }, [location.pathname])
return (
-
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
+
+
+
+ {!isHomePage && (
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ )}
+
+
)
}
+const Container = styled.div`
+ position: relative;
+ width: 100%;
+ height: 100%;
+ min-width: 0;
+ overflow: hidden;
+`
+
+const HomePageWrapper = styled(HomePage)`
+ display: flex;
+ width: 100%;
+ height: 100%;
+`
+
+const PageContainer = styled(motion.div)`
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: var(--color-base);
+ display: flex;
+ width: 100%;
+ height: 100%;
+ z-index: 10;
+`
+
+const pageTransition = {
+ type: 'tween',
+ duration: 0.25,
+ ease: 'easeInOut'
+}
+
+const pageVariants = {
+ initial: { y: '100%' },
+ animate: { y: 0 },
+ exit: { y: '100%' }
+}
+
export default RouteContainer
diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx
index 384622c96f..d78b347194 100644
--- a/src/renderer/src/components/app/Navbar.tsx
+++ b/src/renderer/src/components/app/Navbar.tsx
@@ -5,7 +5,7 @@ import { CircleArrowLeft, X } from 'lucide-react'
import type { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react'
import { useNavigate } from 'react-router-dom'
-import styled from 'styled-components'
+import styled, { keyframes } from 'styled-components'
type Props = PropsWithChildren & HTMLAttributes
@@ -38,6 +38,19 @@ export const NavbarMain: FC = ({ children, ...props }) => {
)
}
+const rotateAnimation = keyframes`
+ from {
+ transform: rotate(-180deg);
+ }
+ to {
+ transform: rotate(0);
+ }
+`
+
+const AnimatedButton = styled(Button)`
+ animation: ${rotateAnimation} 0.4s ease-out;
+`
+
const MacCloseIcon = () => {
const navigate = useNavigate()
@@ -45,7 +58,7 @@ const MacCloseIcon = () => {
return null
}
- return } onClick={() => navigate('/')} className="nodrag" />
+ return } onClick={() => navigate('/')} className="nodrag" />
}
const CloseIcon = () => {
@@ -90,6 +103,7 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
display: flex;
flex-direction: row;
align-items: center;
+ background-color: var(--color-background);
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
diff --git a/src/renderer/src/pages/apps/App.tsx b/src/renderer/src/pages/apps/App.tsx
index d8e751dee7..52aae0120e 100644
--- a/src/renderer/src/pages/apps/App.tsx
+++ b/src/renderer/src/pages/apps/App.tsx
@@ -7,6 +7,7 @@ import type { MenuProps } from 'antd'
import { Dropdown, message } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
+import { useLocation, useNavigate } from 'react-router'
import styled from 'styled-components'
interface Props {
@@ -22,8 +23,15 @@ const App: FC = ({ app, onClick, size = 60, isLast }) => {
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
const isPinned = pinned.some((p) => p.id === app.id)
const isVisible = minapps.some((m) => m.id === app.id)
+ const location = useLocation()
+ const navigate = useNavigate()
+ const isHome = location.pathname === '/'
const handleClick = () => {
+ if (!isHome) {
+ navigate('/')
+ }
+
openMinappKeepAlive(app)
onClick?.()
}
diff --git a/src/renderer/src/pages/home/ChatNavbar.tsx b/src/renderer/src/pages/home/ChatNavbar.tsx
index 9c81b82d54..b3e2de673c 100644
--- a/src/renderer/src/pages/home/ChatNavbar.tsx
+++ b/src/renderer/src/pages/home/ChatNavbar.tsx
@@ -1,7 +1,6 @@
import { Navbar } from '@renderer/components/app/Navbar'
import NarrowModeIcon from '@renderer/components/Icons/NarrowModeIcon'
import { HStack } from '@renderer/components/Layout'
-import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
@@ -17,6 +16,7 @@ import { Tooltip } from 'antd'
import { t } from 'i18next'
import { LayoutGrid, PanelLeft, PanelRight, Search } from 'lucide-react'
import { FC } from 'react'
+import { useNavigate } from 'react-router'
import styled from 'styled-components'
import SelectModelButton from './components/SelectModelButton'
@@ -29,6 +29,7 @@ const ChatNavbar: FC = () => {
const isFullscreen = useFullscreen()
const { sidebarIcons, narrowMode } = useSettings()
const dispatch = useAppDispatch()
+ const navigate = useNavigate()
useShortcut('search_message', SearchPopup.show)
@@ -63,13 +64,11 @@ const ChatNavbar: FC = () => {
{sidebarIcons.visible.includes('minapp') && (
-
-
-
-
-
-
-
+
+ navigate('/apps')}>
+
+
+
)}
diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx
index 487ca20596..8202317e49 100644
--- a/src/renderer/src/pages/home/HomePage.tsx
+++ b/src/renderer/src/pages/home/HomePage.tsx
@@ -1,9 +1,11 @@
+import { HStack } from '@renderer/components/Layout'
import { useSettings } from '@renderer/hooks/useSettings'
import { FC, useEffect } from 'react'
import styled from 'styled-components'
import Chat from './Chat'
import ChatNavbar from './ChatNavbar'
+import MainSidebar from './MainSidebar/MainSidebar'
const HomePage: FC<{ style?: React.CSSProperties }> = ({ style }) => {
const { showAssistants, showTopics, topicPosition } = useSettings()
@@ -17,12 +19,15 @@ const HomePage: FC<{ style?: React.CSSProperties }> = ({ style }) => {
}, [showAssistants, showTopics, topicPosition])
return (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx
index 65b5753a62..e7fbedcb77 100644
--- a/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx
+++ b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx
@@ -133,10 +133,6 @@ const MainSidebar: FC = () => {
return null
}
- if (location.pathname !== '/') {
- return null
- }
-
return (
@@ -165,13 +161,7 @@ const MainSidebar: FC = () => {
transition={{ duration: 0.2 }}>
{appMenuItems.map((item) => (
- {
- navigate(item.path)
- setIsAppMenuExpanded(false)
- }}>
+ navigate(item.path)}>
{item.icon}
{item.text}
diff --git a/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx b/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx
index f0ae3f7f74..6e7fe37c0e 100644
--- a/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx
+++ b/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx
@@ -51,6 +51,7 @@ export const Container = styled.div`
width: var(--assistant-width);
max-width: var(--assistant-width);
border-right: 0.5px solid var(--color-border);
+ height: 100vh;
`
export const MainMenu = styled.div`
diff --git a/src/renderer/src/pages/settings/ProviderSettings/index.tsx b/src/renderer/src/pages/settings/ProviderSettings/index.tsx
index bb06dfe995..928f491523 100644
--- a/src/renderer/src/pages/settings/ProviderSettings/index.tsx
+++ b/src/renderer/src/pages/settings/ProviderSettings/index.tsx
@@ -7,7 +7,7 @@ import ImageStorage from '@renderer/services/ImageStorage'
import { INITIAL_PROVIDERS } from '@renderer/store/llm'
import { Provider } from '@renderer/types'
import { droppableReorder, generateColorFromChar, getFirstCharacter, uuid } from '@renderer/utils'
-import { Avatar, Button, Dropdown, Input, MenuProps, Tag } from 'antd'
+import { Avatar, Button, Dropdown, Input, MenuProps, Spin, Tag } from 'antd'
import { Search, UserPen } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -27,6 +27,7 @@ const ProvidersList: FC = () => {
const [searchText, setSearchText] = useState('')
const [dragging, setDragging] = useState(false)
const [providerLogos, setProviderLogos] = useState>({})
+ const [ready, setReady] = useState(false)
useEffect(() => {
const loadAllLogos = async () => {
@@ -241,6 +242,18 @@ const ProvidersList: FC = () => {
return isProviderMatch || isModelMatch
})
+ useEffect(() => {
+ setTimeout(() => setReady(true), 250)
+ }, [])
+
+ if (!ready) {
+ return (
+
+
+
+ )
+ }
+
return (
diff --git a/yarn.lock b/yarn.lock
index b83cc248b2..b4313593e5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5656,6 +5656,7 @@ __metadata:
eslint-plugin-unused-imports: "npm:^4.1.4"
fast-diff: "npm:^1.3.0"
fast-xml-parser: "npm:^5.2.0"
+ framer-motion: "npm:^12.17.0"
franc-min: "npm:^6.2.0"
fs-extra: "npm:^11.2.0"
html-to-image: "npm:^1.11.13"
@@ -9863,6 +9864,28 @@ __metadata:
languageName: node
linkType: hard
+"framer-motion@npm:^12.17.0":
+ version: 12.17.0
+ resolution: "framer-motion@npm:12.17.0"
+ dependencies:
+ motion-dom: "npm:^12.17.0"
+ motion-utils: "npm:^12.12.1"
+ tslib: "npm:^2.4.0"
+ peerDependencies:
+ "@emotion/is-prop-valid": "*"
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/is-prop-valid":
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ checksum: 10c0/3262ab125650d71cd13eb9f4838da70550ea383d68a2fbd2664b05bac88b7420fe7db25911fbd30cbc237327d98a4567df34e675c8261dde559a9375e580103c
+ languageName: node
+ linkType: hard
+
"franc-min@npm:^6.2.0":
version: 6.2.0
resolution: "franc-min@npm:6.2.0"
@@ -13552,6 +13575,22 @@ __metadata:
languageName: node
linkType: hard
+"motion-dom@npm:^12.17.0":
+ version: 12.17.0
+ resolution: "motion-dom@npm:12.17.0"
+ dependencies:
+ motion-utils: "npm:^12.12.1"
+ checksum: 10c0/1ec428e113f334193dcd52293c94bca21fcca97f3825521d1dafe41f6b999e8dda5013b48de2c09e2f32204f80d1d7281079ba3a142c71b8d6923a0ddb056513
+ languageName: node
+ linkType: hard
+
+"motion-utils@npm:^12.12.1":
+ version: 12.12.1
+ resolution: "motion-utils@npm:12.12.1"
+ checksum: 10c0/880a174769d1be42b46cfb34af81b4a629c068d30d5cf7e07d249fbf2f5121d577482d3ea5bdc1db549c0288733e1e987efecb195fae350995270651559c6697
+ languageName: node
+ linkType: hard
+
"motion-utils@npm:^12.9.4":
version: 12.9.4
resolution: "motion-utils@npm:12.9.4"