diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index 94d3a564be..85171e4edb 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -5,6 +5,7 @@ import { Provider } from 'react-redux'
import { HashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
+import TabsContainer from './components/Tabs/TabsContainer'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider'
@@ -26,7 +27,9 @@ function App(): React.ReactElement {
-
+
+
+
diff --git a/src/renderer/src/Routes.tsx b/src/renderer/src/Routes.tsx
index 7990d4b7c8..489ac8c98f 100644
--- a/src/renderer/src/Routes.tsx
+++ b/src/renderer/src/Routes.tsx
@@ -1,111 +1,31 @@
-import { IpcChannel } from '@shared/IpcChannel'
-import { AnimatePresence, easeInOut, motion } from 'framer-motion'
-import { useEffect } from 'react'
-import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'
-import styled from 'styled-components'
+import { Route, Routes } from 'react-router-dom'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
+import LaunchpadPage from './pages/launchpad/LaunchpadPage'
import McpServersPage from './pages/mcp-servers'
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 navigate = useNavigate()
- const location = useLocation()
- const isHomePage = location.pathname === '/'
-
- // 获取当前路径的主路由部分
- const mainPath = WILDCARD_ROUTES.find((route) => location.pathname.startsWith(route))
-
- // 使用主路由作为 key,这样同一主路由下的切换不会触发动画
- const animationKey = mainPath || location.pathname
-
- useEffect(() => {
- window.api.navigation.url(location.pathname)
- }, [location.pathname])
-
- useEffect(() => {
- window.electron.ipcRenderer.on(IpcChannel.Navigation_Close, () => navigate('/'))
- }, [navigate])
-
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;
- will-change: transform;
- backface-visibility: hidden;
- transform-style: preserve-3d;
- perspective: 1000px;
- -webkit-font-smoothing: subpixel-antialiased;
-`
-
-const pageTransition = {
- type: 'tween' as const,
- duration: 0.25,
- ease: easeInOut
-}
-
-const pageVariants = {
- initial: { translateY: '100%' },
- animate: { translateY: '0%' },
- exit: { translateY: '100%' }
-}
-
export default RouteContainer
diff --git a/src/renderer/src/assets/styles/color.scss b/src/renderer/src/assets/styles/color.scss
index 2e90b5a1aa..e4bb90fea3 100644
--- a/src/renderer/src/assets/styles/color.scss
+++ b/src/renderer/src/assets/styles/color.scss
@@ -57,23 +57,10 @@
--navbar-background-win: rgba(20, 20, 20, 0.75);
--navbar-background: #1f1f1f;
- --navbar-height: 42px;
- --sidebar-width: 50px;
- --status-bar-height: 40px;
- --input-bar-height: 100px;
-
- --assistants-width: 275px;
- --topic-list-width: 275px;
- --settings-width: 250px;
- --scrollbar-width: 5px;
-
--chat-background: transparent;
--chat-background-user: rgba(255, 255, 255, 0.08);
--chat-background-assistant: transparent;
--chat-text-user: var(--color-black);
-
- --list-item-border-radius: 8px;
- --border-width: 0.5px;
}
[theme-mode='light'] {
@@ -139,17 +126,4 @@
--chat-background-user: rgba(0, 0, 0, 0.045);
--chat-background-assistant: transparent;
--chat-text-user: var(--color-text);
-
- --border-width: 0.5px;
-}
-
-[transparent-window='true'] {
- &[theme-mode='light'] {
- --color-list-item: rgba(255, 255, 255, 0.8);
- --color-list-item-hover: rgba(255, 255, 255, 0.4);
- }
- &[theme-mode='dark'] {
- --color-list-item: rgba(255, 255, 255, 0.1);
- --color-list-item-hover: rgba(255, 255, 255, 0.05);
- }
}
diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss
index bd02d9dba2..ce7d70e9cb 100644
--- a/src/renderer/src/assets/styles/index.scss
+++ b/src/renderer/src/assets/styles/index.scss
@@ -1,3 +1,4 @@
+@use './variables.scss';
@use './color.scss';
@use './font.scss';
@use './markdown.scss';
diff --git a/src/renderer/src/assets/styles/variables.scss b/src/renderer/src/assets/styles/variables.scss
new file mode 100644
index 0000000000..371590ff8a
--- /dev/null
+++ b/src/renderer/src/assets/styles/variables.scss
@@ -0,0 +1,18 @@
+:root {
+ --navbar-height: 42px;
+ --sidebar-width: 50px;
+ --status-bar-height: 40px;
+ --input-bar-height: 100px;
+
+ --assistants-width: 275px;
+ --topic-list-width: 275px;
+ --settings-width: 250px;
+ --scrollbar-width: 5px;
+
+ --list-item-border-radius: 15px;
+ --border-width: 0.5px;
+
+ --main-height: calc(100vh - var(--navbar-height) - 6px);
+
+ --border-width: 0.5px;
+}
diff --git a/src/renderer/src/components/Tabs/TabsContainer.tsx b/src/renderer/src/components/Tabs/TabsContainer.tsx
new file mode 100644
index 0000000000..6f25e340d8
--- /dev/null
+++ b/src/renderer/src/components/Tabs/TabsContainer.tsx
@@ -0,0 +1,237 @@
+import { PlusOutlined } from '@ant-design/icons'
+import { isMac } from '@renderer/config/constant'
+import { useFullscreen } from '@renderer/hooks/useFullscreen'
+import { useAppDispatch, useAppSelector } from '@renderer/store'
+import type { Tab } from '@renderer/store/tabs'
+import { addTab, removeTab, setActiveTab } from '@renderer/store/tabs'
+import {
+ FileSearch,
+ Folder,
+ Home,
+ Languages,
+ LayoutGrid,
+ Palette,
+ Settings,
+ Sparkle,
+ SquareTerminal,
+ X
+} from 'lucide-react'
+import { useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useLocation, useNavigate } from 'react-router-dom'
+import styled from 'styled-components'
+
+interface TabsContainerProps {
+ children: React.ReactNode
+}
+
+const getTabIcon = (tabId: string): React.ReactNode | undefined => {
+ switch (tabId) {
+ case 'home':
+ return
+ case 'agents':
+ return
+ case 'translate':
+ return
+ case 'paintings':
+ return
+ case 'apps':
+ return
+ case 'knowledge':
+ return
+ case 'mcp':
+ return
+ case 'files':
+ return
+ case 'settings':
+ return
+ default:
+ return null
+ }
+}
+
+const TabsContainer: React.FC = ({ children }) => {
+ const { t } = useTranslation()
+ const location = useLocation()
+ const navigate = useNavigate()
+ const dispatch = useAppDispatch()
+ const tabs = useAppSelector((state) => state.tabs.tabs)
+ const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
+ const isFullscreen = useFullscreen()
+
+ const getTabId = (path: string): string => {
+ if (path === '/') return 'home'
+ const segments = path.split('/')
+ return segments[1] // 获取第一个路径段作为 id
+ }
+
+ const shouldCreateTab = (path: string) => {
+ if (path === '/') return false
+ return !tabs.some((tab) => tab.id === getTabId(path))
+ }
+
+ useEffect(() => {
+ const tabId = getTabId(location.pathname)
+ const currentTab = tabs.find((tab) => tab.id === tabId)
+
+ if (!currentTab && shouldCreateTab(location.pathname)) {
+ dispatch(
+ addTab({
+ id: tabId,
+ path: location.pathname
+ })
+ )
+ } else if (currentTab) {
+ dispatch(setActiveTab(currentTab.id))
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [dispatch, location.pathname])
+
+ const closeTab = (tabId: string) => {
+ const tabToClose = tabs.find((tab) => tab.id === tabId)
+ if (!tabToClose) return
+
+ if (tabs.length === 1) return
+
+ if (tabId === activeTabId) {
+ const remainingTabs = tabs.filter((tab) => tab.id !== tabId)
+ const lastTab = remainingTabs[remainingTabs.length - 1]
+ navigate(lastTab.path)
+ }
+
+ dispatch(removeTab(tabId))
+ }
+
+ const handleAddTab = () => {
+ navigate('/launchpad')
+ }
+
+ return (
+
+
+ {tabs.map((tab) => (
+ navigate(tab.path)}>
+
+ {tab.id && {getTabIcon(tab.id)}}
+ {t(`title.${tab.id}`)}
+
+ {tab.id !== 'home' && (
+ {
+ e.stopPropagation()
+ closeTab(tab.id)
+ }}>
+
+
+ )}
+
+ ))}
+
+
+
+
+ {children}
+
+ )
+}
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+`
+
+const TabsBar = styled.div<{ $isFullscreen: boolean }>`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 5px;
+ padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? '80px' : '8px')};
+ -webkit-app-region: drag;
+ height: var(--navbar-height);
+`
+
+const Tab = styled.div<{ active?: boolean }>`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 4px 10px;
+ background: ${(props) => (props.active ? 'var(--color-background)' : 'transparent')};
+ border-radius: 8px;
+ cursor: pointer;
+ user-select: none;
+ -webkit-app-region: none;
+ min-width: 100px;
+ transition: background 0.2s;
+ .close-button {
+ opacity: 0;
+ transition: opacity 0.2s;
+ margin-right: -2px;
+ }
+
+ &:hover {
+ background: ${(props) => (props.active ? 'var(--color-background)' : 'var(--color-background-soft)')};
+ .close-button {
+ opacity: 1;
+ }
+ }
+`
+
+const TabHeader = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 6px;
+`
+
+const TabIcon = styled.span`
+ display: flex;
+ align-items: center;
+ margin-right: 6px;
+ color: var(--color-text-2);
+`
+
+const TabTitle = styled.span`
+ color: var(--color-text);
+ font-size: 13px;
+ margin-right: 8px;
+ display: flex;
+ align-items: center;
+`
+
+const CloseButton = styled.span`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+`
+
+const AddTabButton = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ cursor: pointer;
+ color: var(--color-text);
+ -webkit-app-region: none;
+
+ &:hover {
+ background: var(--color-background-soft);
+ border-radius: 8px;
+ }
+`
+
+const TabContent = styled.div`
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+ width: calc(100vw - 12px);
+ margin: 6px;
+ margin-top: 0;
+ border-radius: 8px;
+ overflow: hidden;
+`
+
+export default TabsContainer
diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx
index 74af3a2b3a..bf223d6146 100644
--- a/src/renderer/src/components/app/Navbar.tsx
+++ b/src/renderer/src/components/app/Navbar.tsx
@@ -1,10 +1,8 @@
-import { isLinux, isMac, isWindows } from '@renderer/config/constant'
+import { isLinux, isWindows } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { Button } from 'antd'
-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, { keyframes } from 'styled-components'
type Props = PropsWithChildren & HTMLAttributes
@@ -31,41 +29,11 @@ export const NavbarMain: FC = ({ children, ...props }) => {
return (
-
{children}
-
)
}
-const MacCloseIcon = () => {
- const navigate = useNavigate()
-
- if (!isMac) {
- return null
- }
-
- return } onClick={() => navigate('/')} className="nodrag" />
-}
-
-const CloseIcon = () => {
- const navigate = useNavigate()
-
- if (isMac) {
- return null
- }
-
- return (
-