mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-01 01:30:51 +08:00
feat: use tabs
wip wip wip wip
This commit is contained in:
parent
202504fd17
commit
4317f4b672
@ -5,6 +5,7 @@ import { Provider } from 'react-redux'
|
|||||||
import { HashRouter } from 'react-router-dom'
|
import { HashRouter } from 'react-router-dom'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
import { PersistGate } from 'redux-persist/integration/react'
|
||||||
|
|
||||||
|
import TabsContainer from './components/Tabs/TabsContainer'
|
||||||
import TopViewContainer from './components/TopView'
|
import TopViewContainer from './components/TopView'
|
||||||
import AntdProvider from './context/AntdProvider'
|
import AntdProvider from './context/AntdProvider'
|
||||||
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
||||||
@ -26,7 +27,9 @@ function App(): React.ReactElement {
|
|||||||
<TopViewContainer>
|
<TopViewContainer>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<NavigationHandler />
|
<NavigationHandler />
|
||||||
<Routes />
|
<TabsContainer>
|
||||||
|
<Routes />
|
||||||
|
</TabsContainer>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</TopViewContainer>
|
</TopViewContainer>
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
|
|||||||
@ -1,111 +1,31 @@
|
|||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { Route, Routes } from 'react-router-dom'
|
||||||
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 AgentsPage from './pages/agents/AgentsPage'
|
import AgentsPage from './pages/agents/AgentsPage'
|
||||||
import AppsPage from './pages/apps/AppsPage'
|
import AppsPage from './pages/apps/AppsPage'
|
||||||
import FilesPage from './pages/files/FilesPage'
|
import FilesPage from './pages/files/FilesPage'
|
||||||
import HomePage from './pages/home/HomePage'
|
import HomePage from './pages/home/HomePage'
|
||||||
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||||
|
import LaunchpadPage from './pages/launchpad/LaunchpadPage'
|
||||||
import McpServersPage from './pages/mcp-servers'
|
import McpServersPage from './pages/mcp-servers'
|
||||||
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
||||||
import SettingsPage from './pages/settings/SettingsPage'
|
import SettingsPage from './pages/settings/SettingsPage'
|
||||||
import TranslatePage from './pages/translate/TranslatePage'
|
import TranslatePage from './pages/translate/TranslatePage'
|
||||||
|
|
||||||
const WILDCARD_ROUTES = ['/settings', '/paintings', '/mcp-servers']
|
|
||||||
|
|
||||||
const RouteContainer = () => {
|
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 (
|
return (
|
||||||
<Container>
|
<Routes>
|
||||||
<HomePageWrapper />
|
<Route path="/" element={<HomePage />} />
|
||||||
<AnimatePresence mode="wait">
|
<Route path="/agents" element={<AgentsPage />} />
|
||||||
{!isHomePage && (
|
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
|
||||||
<PageContainer
|
<Route path="/translate" element={<TranslatePage />} />
|
||||||
key={animationKey}
|
<Route path="/files" element={<FilesPage />} />
|
||||||
initial="initial"
|
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||||
animate="animate"
|
<Route path="/apps" element={<AppsPage />} />
|
||||||
exit="exit"
|
<Route path="/mcp-servers/*" element={<McpServersPage />} />
|
||||||
variants={pageVariants}
|
<Route path="/settings/*" element={<SettingsPage />} />
|
||||||
transition={pageTransition}>
|
<Route path="/launchpad" element={<LaunchpadPage />} />
|
||||||
<Routes location={location}>
|
</Routes>
|
||||||
<Route path="/agents" element={<AgentsPage />} />
|
|
||||||
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
|
|
||||||
<Route path="/translate" element={<TranslatePage />} />
|
|
||||||
<Route path="/files" element={<FilesPage />} />
|
|
||||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
|
||||||
<Route path="/apps" element={<AppsPage />} />
|
|
||||||
<Route path="/mcp-servers/*" element={<McpServersPage />} />
|
|
||||||
<Route path="/settings/*" element={<SettingsPage />} />
|
|
||||||
</Routes>
|
|
||||||
</PageContainer>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</Container>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
export default RouteContainer
|
||||||
|
|||||||
@ -57,23 +57,10 @@
|
|||||||
--navbar-background-win: rgba(20, 20, 20, 0.75);
|
--navbar-background-win: rgba(20, 20, 20, 0.75);
|
||||||
--navbar-background: #1f1f1f;
|
--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: transparent;
|
||||||
--chat-background-user: rgba(255, 255, 255, 0.08);
|
--chat-background-user: rgba(255, 255, 255, 0.08);
|
||||||
--chat-background-assistant: transparent;
|
--chat-background-assistant: transparent;
|
||||||
--chat-text-user: var(--color-black);
|
--chat-text-user: var(--color-black);
|
||||||
|
|
||||||
--list-item-border-radius: 8px;
|
|
||||||
--border-width: 0.5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[theme-mode='light'] {
|
[theme-mode='light'] {
|
||||||
@ -139,17 +126,4 @@
|
|||||||
--chat-background-user: rgba(0, 0, 0, 0.045);
|
--chat-background-user: rgba(0, 0, 0, 0.045);
|
||||||
--chat-background-assistant: transparent;
|
--chat-background-assistant: transparent;
|
||||||
--chat-text-user: var(--color-text);
|
--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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
@use './variables.scss';
|
||||||
@use './color.scss';
|
@use './color.scss';
|
||||||
@use './font.scss';
|
@use './font.scss';
|
||||||
@use './markdown.scss';
|
@use './markdown.scss';
|
||||||
|
|||||||
18
src/renderer/src/assets/styles/variables.scss
Normal file
18
src/renderer/src/assets/styles/variables.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
237
src/renderer/src/components/Tabs/TabsContainer.tsx
Normal file
237
src/renderer/src/components/Tabs/TabsContainer.tsx
Normal file
@ -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 <Home size={14} />
|
||||||
|
case 'agents':
|
||||||
|
return <Sparkle size={14} />
|
||||||
|
case 'translate':
|
||||||
|
return <Languages size={14} />
|
||||||
|
case 'paintings':
|
||||||
|
return <Palette size={14} />
|
||||||
|
case 'apps':
|
||||||
|
return <LayoutGrid size={14} />
|
||||||
|
case 'knowledge':
|
||||||
|
return <FileSearch size={14} />
|
||||||
|
case 'mcp':
|
||||||
|
return <SquareTerminal size={14} />
|
||||||
|
case 'files':
|
||||||
|
return <Folder size={14} />
|
||||||
|
case 'settings':
|
||||||
|
return <Settings size={14} />
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabsContainer: React.FC<TabsContainerProps> = ({ 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 (
|
||||||
|
<Container>
|
||||||
|
<TabsBar $isFullscreen={isFullscreen}>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => navigate(tab.path)}>
|
||||||
|
<TabHeader>
|
||||||
|
{tab.id && <TabIcon>{getTabIcon(tab.id)}</TabIcon>}
|
||||||
|
<TabTitle>{t(`title.${tab.id}`)}</TabTitle>
|
||||||
|
</TabHeader>
|
||||||
|
{tab.id !== 'home' && (
|
||||||
|
<CloseButton
|
||||||
|
className="close-button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
closeTab(tab.id)
|
||||||
|
}}>
|
||||||
|
<X size={12} />
|
||||||
|
</CloseButton>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
<AddTabButton onClick={handleAddTab}>
|
||||||
|
<PlusOutlined />
|
||||||
|
</AddTabButton>
|
||||||
|
</TabsBar>
|
||||||
|
<TabContent>{children}</TabContent>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@ -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 { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
import { CircleArrowLeft, X } from 'lucide-react'
|
|
||||||
import type { FC, PropsWithChildren } from 'react'
|
import type { FC, PropsWithChildren } from 'react'
|
||||||
import type { HTMLAttributes } from 'react'
|
import type { HTMLAttributes } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import styled, { keyframes } from 'styled-components'
|
import styled, { keyframes } from 'styled-components'
|
||||||
|
|
||||||
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
|
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
|
||||||
@ -31,41 +29,11 @@ export const NavbarMain: FC<Props> = ({ children, ...props }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
|
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
|
||||||
<CloseIcon />
|
|
||||||
{children}
|
{children}
|
||||||
<MacCloseIcon />
|
|
||||||
</NavbarMainContainer>
|
</NavbarMainContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const MacCloseIcon = () => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
if (!isMac) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return <AnimatedButton type="text" icon={<X size={18} />} onClick={() => navigate('/')} className="nodrag" />
|
|
||||||
}
|
|
||||||
|
|
||||||
const CloseIcon = () => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
if (isMac) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
onClick={() => navigate('/')}
|
|
||||||
className="nodrag"
|
|
||||||
style={{ marginRight: 2, marginLeft: 5 }}
|
|
||||||
icon={<CircleArrowLeft size={20} color="var(--color-icon)" style={{ marginTop: 2 }} />}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const NavbarContainer = styled.div`
|
const NavbarContainer = styled.div`
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -95,11 +63,10 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
|
|||||||
max-height: var(--navbar-height);
|
max-height: var(--navbar-height);
|
||||||
min-height: var(--navbar-height);
|
min-height: var(--navbar-height);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding-left: ${({ $isFullscreen }) => ($isFullscreen ? '10px' : isMac ? '70px' : '10px')};
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '135px' : isLinux ? '120px' : '12px')};
|
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
|
padding: 0 12px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const NavbarCenterContainer = styled.div`
|
const NavbarCenterContainer = styled.div`
|
||||||
|
|||||||
@ -42,7 +42,6 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
|||||||
// Set initial theme and OS attributes on body
|
// Set initial theme and OS attributes on body
|
||||||
document.body.setAttribute('os', isMac ? 'mac' : 'windows')
|
document.body.setAttribute('os', isMac ? 'mac' : 'windows')
|
||||||
document.body.setAttribute('theme-mode', actualTheme)
|
document.body.setAttribute('theme-mode', actualTheme)
|
||||||
document.body.setAttribute('transparent-window', transparentWindow ? 'true' : 'false')
|
|
||||||
|
|
||||||
// if theme is old auto, then set theme to system
|
// if theme is old auto, then set theme to system
|
||||||
// we can delete this after next big release
|
// we can delete this after next big release
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import NavigationService from '@renderer/services/NavigationService'
|
import NavigationService from '@renderer/services/NavigationService'
|
||||||
import { useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import { addTab, Tab } from '@renderer/store/tabs'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
const NavigationHandler: React.FC = () => {
|
const NavigationHandler: React.FC = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const tabs = useAppSelector((state) => state.tabs.tabs)
|
||||||
|
|
||||||
const showSettingsShortcutEnabled = useAppSelector(
|
const showSettingsShortcutEnabled = useAppSelector(
|
||||||
(state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled
|
(state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled
|
||||||
@ -32,6 +35,20 @@ const NavigationHandler: React.FC = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 初始化 home tab
|
||||||
|
useEffect(() => {
|
||||||
|
if (tabs.length === 0) {
|
||||||
|
const homeTab: Tab = {
|
||||||
|
id: 'home',
|
||||||
|
titleKey: 'title.home',
|
||||||
|
title: '',
|
||||||
|
path: '/',
|
||||||
|
iconType: 'home'
|
||||||
|
}
|
||||||
|
dispatch(addTab(homeTab))
|
||||||
|
}
|
||||||
|
}, [dispatch, tabs.length])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { isLocalAi } from '@renderer/config/env'
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import KnowledgeQueue from '@renderer/queue/KnowledgeQueue'
|
import KnowledgeQueue from '@renderer/queue/KnowledgeQueue'
|
||||||
@ -12,17 +11,14 @@ import { useEffect } from 'react'
|
|||||||
|
|
||||||
import { useDefaultModel } from './useAssistant'
|
import { useDefaultModel } from './useAssistant'
|
||||||
import useFullScreenNotice from './useFullScreenNotice'
|
import useFullScreenNotice from './useFullScreenNotice'
|
||||||
import { useRuntime } from './useRuntime'
|
|
||||||
import { useSettings } from './useSettings'
|
import { useSettings } from './useSettings'
|
||||||
import useUpdateHandler from './useUpdateHandler'
|
import useUpdateHandler from './useUpdateHandler'
|
||||||
|
|
||||||
export function useAppInit() {
|
export function useAppInit() {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { proxyUrl, language, autoCheckUpdate, proxyMode, customCss, enableDataCollection } = useSettings()
|
const { proxyUrl, language, autoCheckUpdate, proxyMode, customCss, enableDataCollection } = useSettings()
|
||||||
const { minappShow } = useRuntime()
|
|
||||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||||
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||||
const { theme } = useTheme()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.getElementById('spinner')?.remove()
|
document.getElementById('spinner')?.remove()
|
||||||
@ -61,15 +57,6 @@ export function useAppInit() {
|
|||||||
i18n.changeLanguage(language || navigator.language || defaultLanguage)
|
i18n.changeLanguage(language || navigator.language || defaultLanguage)
|
||||||
}, [language])
|
}, [language])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (minappShow) {
|
|
||||||
window.root.style.background = 'var(--color-background)'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
window.root.style.background = !minappShow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
|
||||||
}, [minappShow, theme])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLocalAi) {
|
if (isLocalAi) {
|
||||||
const model = JSON.parse(import.meta.env.VITE_RENDERER_INTEGRATED_MODEL)
|
const model = JSON.parse(import.meta.env.VITE_RENDERER_INTEGRATED_MODEL)
|
||||||
|
|||||||
64
src/renderer/src/hooks/useTabs.ts
Normal file
64
src/renderer/src/hooks/useTabs.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import type { Tab } from '@renderer/store/tabs'
|
||||||
|
import { addTab, removeTab, setActiveTab, updateTab } from '@renderer/store/tabs'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
export function useTabs() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const tabs = useAppSelector((state) => state.tabs.tabs)
|
||||||
|
const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
|
||||||
|
const activeTab = useAppSelector((state) => state.tabs.tabs.find((tab) => tab.id === activeTabId))
|
||||||
|
|
||||||
|
const getTabId = (path: string): string => {
|
||||||
|
if (path === '/') return 'home'
|
||||||
|
const segments = path.split('/')
|
||||||
|
return segments[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldCreateTab = (path: string) => {
|
||||||
|
if (path === '/') return false
|
||||||
|
return !tabs.some((tab) => tab.id === getTabId(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNewTab = (tab: Tab) => {
|
||||||
|
dispatch(addTab(tab))
|
||||||
|
navigate(tab.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeTab = (tabId: string) => {
|
||||||
|
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 switchTab = (tabId: string) => {
|
||||||
|
const tab = tabs.find((tab) => tab.id === tabId)
|
||||||
|
if (tab) {
|
||||||
|
dispatch(setActiveTab(tabId))
|
||||||
|
navigate(tab.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCurrentTab = (updates: Partial<Tab>) => {
|
||||||
|
dispatch(updateTab({ id: activeTabId, updates }))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tabs,
|
||||||
|
activeTab,
|
||||||
|
activeTabId,
|
||||||
|
addNewTab,
|
||||||
|
closeTab,
|
||||||
|
switchTab,
|
||||||
|
getTabId,
|
||||||
|
shouldCreateTab,
|
||||||
|
updateCurrentTab
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,17 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"translation": {
|
||||||
|
"title": {
|
||||||
|
"home": "Home",
|
||||||
|
"agents": "Agents",
|
||||||
|
"paintings": "Paintings",
|
||||||
|
"translate": "Translate",
|
||||||
|
"files": "Files",
|
||||||
|
"knowledge": "Knowledge Base",
|
||||||
|
"apps": "Apps",
|
||||||
|
"mcp-servers": "MCP Servers",
|
||||||
|
"settings": "Settings",
|
||||||
|
"launchpad": "Launchpad"
|
||||||
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"add.button": "Add to Assistant",
|
"add.button": "Add to Assistant",
|
||||||
"add.knowledge_base": "Knowledge Base",
|
"add.knowledge_base": "Knowledge Base",
|
||||||
|
|||||||
@ -1,5 +1,17 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"translation": {
|
||||||
|
"title": {
|
||||||
|
"home": "ホーム",
|
||||||
|
"agents": "エージェント",
|
||||||
|
"paintings": "ペインティング",
|
||||||
|
"translate": "翻訳",
|
||||||
|
"files": "ファイル",
|
||||||
|
"knowledge": "ナレッジベース",
|
||||||
|
"apps": "アプリ",
|
||||||
|
"mcp-servers": "MCP サーバー",
|
||||||
|
"settings": "設定",
|
||||||
|
"launchpad": "ランチパッド"
|
||||||
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"add.button": "アシスタントに追加",
|
"add.button": "アシスタントに追加",
|
||||||
"add.knowledge_base": "ナレッジベース",
|
"add.knowledge_base": "ナレッジベース",
|
||||||
|
|||||||
@ -1,5 +1,17 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"translation": {
|
||||||
|
"title": {
|
||||||
|
"home": "Главная",
|
||||||
|
"agents": "Агенты",
|
||||||
|
"paintings": "Рисунки",
|
||||||
|
"translate": "Перевод",
|
||||||
|
"files": "Файлы",
|
||||||
|
"knowledge": "База знаний",
|
||||||
|
"apps": "Приложения",
|
||||||
|
"mcp-servers": "MCP серверы",
|
||||||
|
"settings": "Настройки",
|
||||||
|
"launchpad": "Запуск"
|
||||||
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"add.button": "Добавить в ассистента",
|
"add.button": "Добавить в ассистента",
|
||||||
"add.knowledge_base": "База знаний",
|
"add.knowledge_base": "База знаний",
|
||||||
|
|||||||
@ -1,5 +1,17 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"translation": {
|
||||||
|
"title": {
|
||||||
|
"home": "首页",
|
||||||
|
"agents": "智能体",
|
||||||
|
"paintings": "绘画",
|
||||||
|
"translate": "翻译",
|
||||||
|
"files": "文件",
|
||||||
|
"knowledge": "知识库",
|
||||||
|
"apps": "小程序",
|
||||||
|
"mcp-servers": "MCP 服务器",
|
||||||
|
"settings": "设置",
|
||||||
|
"launchpad": "启动台"
|
||||||
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"add.button": "添加到助手",
|
"add.button": "添加到助手",
|
||||||
"add.knowledge_base": "知识库",
|
"add.knowledge_base": "知识库",
|
||||||
|
|||||||
@ -1,5 +1,17 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"translation": {
|
||||||
|
"title": {
|
||||||
|
"home": "主頁",
|
||||||
|
"agents": "智能體",
|
||||||
|
"paintings": "繪畫",
|
||||||
|
"translate": "翻譯",
|
||||||
|
"files": "文件",
|
||||||
|
"knowledge": "知識庫",
|
||||||
|
"apps": "小程序",
|
||||||
|
"mcp-servers": "MCP 伺服器",
|
||||||
|
"settings": "設定",
|
||||||
|
"launchpad": "啟動台"
|
||||||
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"add.button": "新增到助手",
|
"add.button": "新增到助手",
|
||||||
"add.knowledge_base": "知識庫",
|
"add.knowledge_base": "知識庫",
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import type { MenuProps } from 'antd'
|
|||||||
import { Dropdown, message } from 'antd'
|
import { Dropdown, message } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useLocation, useNavigate } from 'react-router'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -23,15 +22,8 @@ const App: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
|
|||||||
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
|
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
|
||||||
const isPinned = pinned.some((p) => p.id === app.id)
|
const isPinned = pinned.some((p) => p.id === app.id)
|
||||||
const isVisible = minapps.some((m) => m.id === app.id)
|
const isVisible = minapps.some((m) => m.id === app.id)
|
||||||
const location = useLocation()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const isHome = location.pathname === '/'
|
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!isHome) {
|
|
||||||
setTimeout(() => navigate('/'), 300)
|
|
||||||
}
|
|
||||||
|
|
||||||
openMinappKeepAlive(app)
|
openMinappKeepAlive(app)
|
||||||
onClick?.()
|
onClick?.()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,7 +113,7 @@ const Chat: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Main = styled(Flex)`
|
const Main = styled(Flex)`
|
||||||
height: calc(100vh - var(--navbar-height));
|
height: calc(100vh - var(--navbar-height) - 50px);
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
position: relative;
|
position: relative;
|
||||||
`
|
`
|
||||||
|
|||||||
@ -1,23 +1,15 @@
|
|||||||
import { Navbar } from '@renderer/components/app/Navbar'
|
import { NavbarMain } from '@renderer/components/app/Navbar'
|
||||||
import NarrowModeIcon from '@renderer/components/Icons/NarrowModeIcon'
|
|
||||||
import { PanelLeftIcon } from '@renderer/components/Icons/PanelIcons'
|
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||||
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useChat } from '@renderer/hooks/useChat'
|
import { useChat } from '@renderer/hooks/useChat'
|
||||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
|
||||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
|
||||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||||
import { useShowAssistants } from '@renderer/hooks/useStore'
|
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
|
||||||
import { setNarrowMode } from '@renderer/store/settings'
|
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { LayoutGrid, Search } from 'lucide-react'
|
import { PanelLeft, PanelRight, Search } from 'lucide-react'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useNavigate } from 'react-router'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import SelectModelButton from './components/SelectModelButton'
|
import SelectModelButton from './components/SelectModelButton'
|
||||||
@ -27,70 +19,31 @@ const ChatNavbar: FC = () => {
|
|||||||
const { activeAssistant } = useChat()
|
const { activeAssistant } = useChat()
|
||||||
const { assistant } = useAssistant(activeAssistant.id)
|
const { assistant } = useAssistant(activeAssistant.id)
|
||||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||||
const isFullscreen = useFullscreen()
|
|
||||||
const { sidebarIcons, narrowMode } = useSettings()
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
useShortcut('search_message', SearchPopup.show)
|
useShortcut('search_message', SearchPopup.show)
|
||||||
|
|
||||||
const handleNarrowModeToggle = async () => {
|
|
||||||
await modelGenerating()
|
|
||||||
dispatch(setNarrowMode(!narrowMode))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar className="home-navbar">
|
<NavbarMain className="home-navbar" style={{ minHeight: 50 }}>
|
||||||
<NavbarContainer $isFullscreen={isFullscreen} $showSidebar={showAssistants} className="home-navbar-right">
|
<HStack alignItems="center" gap={8}>
|
||||||
<HStack alignItems="center" gap={8}>
|
<NavbarIcon onClick={() => toggleShowAssistants()}>
|
||||||
{!showAssistants && (
|
{showAssistants ? <PanelLeft size={18} /> : <PanelRight size={18} />}
|
||||||
<NavbarIcon onClick={() => toggleShowAssistants()}>
|
</NavbarIcon>
|
||||||
<PanelLeftIcon size={18} expanded={false} />
|
<SelectModelButton assistant={assistant} />
|
||||||
</NavbarIcon>
|
</HStack>
|
||||||
)}
|
<HStack alignItems="center" gap={8}>
|
||||||
<SelectModelButton assistant={assistant} />
|
<UpdateAppButton />
|
||||||
</HStack>
|
{isMac && (
|
||||||
<HStack alignItems="center" gap={8}>
|
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||||
<UpdateAppButton />
|
|
||||||
<Tooltip title={t('history.title')} mouseEnterDelay={0.8}>
|
|
||||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||||
<Search size={18} />
|
<Search size={18} />
|
||||||
</NarrowIcon>
|
</NarrowIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
|
)}
|
||||||
<NarrowIcon onClick={handleNarrowModeToggle}>
|
</HStack>
|
||||||
<NarrowModeIcon isNarrowMode={narrowMode} />
|
</NavbarMain>
|
||||||
</NarrowIcon>
|
|
||||||
</Tooltip>
|
|
||||||
{sidebarIcons.visible.includes('minapp') && (
|
|
||||||
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8}>
|
|
||||||
<NarrowIcon onClick={() => navigate('/apps')}>
|
|
||||||
<LayoutGrid size={18} />
|
|
||||||
</NarrowIcon>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</NavbarContainer>
|
|
||||||
</Navbar>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavbarContainer = styled.div<{ $isFullscreen: boolean; $showSidebar: boolean }>`
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
height: var(--navbar-height);
|
|
||||||
max-height: var(--navbar-height);
|
|
||||||
min-height: var(--navbar-height);
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-left: ${({ $showSidebar }) => ($showSidebar ? '15px' : isMac ? '75px' : '15px')};
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '15px' : isWindows ? '140px' : isLinux ? '120px' : '15px')};
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const NavbarIcon = styled.div`
|
export const NavbarIcon = styled.div`
|
||||||
-webkit-app-region: none;
|
-webkit-app-region: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@ -40,7 +40,6 @@ import styled from 'styled-components'
|
|||||||
import AssistantsTab from '../Tabs/AssistantsTab'
|
import AssistantsTab from '../Tabs/AssistantsTab'
|
||||||
import AssistantItem from '../Tabs/components/AssistantItem'
|
import AssistantItem from '../Tabs/components/AssistantItem'
|
||||||
import TopicsTab from '../Tabs/TopicsTab'
|
import TopicsTab from '../Tabs/TopicsTab'
|
||||||
import MainNavbar from './MainNavbar'
|
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
MainMenu,
|
MainMenu,
|
||||||
@ -177,7 +176,6 @@ const MainSidebar: FC = () => {
|
|||||||
opacity: showAssistants ? 1 : 0,
|
opacity: showAssistants ? 1 : 0,
|
||||||
overflow: showAssistants ? 'initial' : 'hidden'
|
overflow: showAssistants ? 'initial' : 'hidden'
|
||||||
}}>
|
}}>
|
||||||
<MainNavbar />
|
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
<SidebarSearch onSearch={setSearchValue} />
|
<SidebarSearch onSearch={setSearchValue} />
|
||||||
<MainMenuItem active={isAppMenuExpanded} onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
|
<MainMenuItem active={isAppMenuExpanded} onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
|
||||||
@ -305,6 +303,7 @@ const MainContainer = styled.div`
|
|||||||
const AssistantContainer = styled.div`
|
const AssistantContainer = styled.div`
|
||||||
margin: 4px 10px;
|
margin: 4px 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-top: 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
const UserMenu = styled.div`
|
const UserMenu = styled.div`
|
||||||
|
|||||||
@ -52,7 +52,10 @@ export const Container = styled.div<{ transparent?: boolean }>`
|
|||||||
width: var(--assistants-width);
|
width: var(--assistants-width);
|
||||||
max-width: var(--assistants-width);
|
max-width: var(--assistants-width);
|
||||||
border-right: 0.5px solid var(--color-border);
|
border-right: 0.5px solid var(--color-border);
|
||||||
height: 100vh;
|
height: var(--main-height);
|
||||||
|
min-height: var(--main-height);
|
||||||
|
background: var(--color-background);
|
||||||
|
padding-top: 10px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const MainMenu = styled.div`
|
export const MainMenu = styled.div`
|
||||||
|
|||||||
118
src/renderer/src/pages/launchpad/LaunchpadPage.tsx
Normal file
118
src/renderer/src/pages/launchpad/LaunchpadPage.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { FileSearch, Folder, Languages, LayoutGrid, Palette, Sparkle, SquareTerminal } from 'lucide-react'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const LaunchpadPage: FC = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { defaultPaintingProvider } = useSettings()
|
||||||
|
|
||||||
|
const appMenuItems = [
|
||||||
|
{
|
||||||
|
icon: <Sparkle size={32} className="icon" />,
|
||||||
|
text: t('agents.title'),
|
||||||
|
path: '/agents',
|
||||||
|
bgColor: 'linear-gradient(135deg, #6366F1, #4F46E5)' // AI助手:靛蓝渐变,代表智能和科技
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Languages size={32} className="icon" />,
|
||||||
|
text: t('translate.title'),
|
||||||
|
path: '/translate',
|
||||||
|
bgColor: 'linear-gradient(135deg, #06B6D4, #0EA5E9)' // 翻译:明亮的青蓝色,代表沟通和流畅
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Palette size={32} className="icon" />,
|
||||||
|
text: t('paintings.title'),
|
||||||
|
path: `/paintings/${defaultPaintingProvider}`,
|
||||||
|
bgColor: 'linear-gradient(135deg, #EC4899, #F472B6)' // 绘画:活力粉色,代表创造力和艺术
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <LayoutGrid size={32} className="icon" />,
|
||||||
|
text: t('minapp.title'),
|
||||||
|
path: '/apps',
|
||||||
|
bgColor: 'linear-gradient(135deg, #8B5CF6, #A855F7)' // 小程序:紫色,代表多功能和灵活性
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FileSearch size={32} className="icon" />,
|
||||||
|
text: t('knowledge.title'),
|
||||||
|
path: '/knowledge',
|
||||||
|
bgColor: 'linear-gradient(135deg, #10B981, #34D399)' // 知识库:翠绿色,代表生长和知识
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <SquareTerminal size={32} className="icon" />,
|
||||||
|
text: t('settings.mcp.title'),
|
||||||
|
path: '/mcp-servers',
|
||||||
|
bgColor: 'linear-gradient(135deg, #3B82F6, #60A5FA)' // MCP服务器:科技蓝,代表专业和稳定
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Folder size={32} className="icon" />,
|
||||||
|
text: t('files.title'),
|
||||||
|
path: '/files',
|
||||||
|
bgColor: 'linear-gradient(135deg, #F59E0B, #FBBF24)' // 文件:金色,代表资源和重要性
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Grid>
|
||||||
|
{appMenuItems.map((item) => (
|
||||||
|
<AppIcon key={item.path} onClick={() => navigate(item.path)}>
|
||||||
|
<IconWrapper bgColor={item.bgColor}>{item.icon}</IconWrapper>
|
||||||
|
<AppName>{item.text}</AppName>
|
||||||
|
</AppIcon>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
`
|
||||||
|
|
||||||
|
const Grid = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 40px;
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 800px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const AppIcon = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 12px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const IconWrapper = styled.div<{ bgColor: string }>`
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: ${(props) => props.bgColor};
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const AppName = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text);
|
||||||
|
text-align: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default LaunchpadPage
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
|
|
||||||
import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings'
|
import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings'
|
||||||
import { Spin } from 'antd'
|
import { Spin } from 'antd'
|
||||||
import {
|
import {
|
||||||
@ -38,9 +37,6 @@ const SettingsPage: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<NavbarMain>
|
|
||||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
|
|
||||||
</NavbarMain>
|
|
||||||
<ContentContainer id="content-container">
|
<ContentContainer id="content-container">
|
||||||
<SettingMenus>
|
<SettingMenus>
|
||||||
<MenuItemLink to="/settings/provider">
|
<MenuItemLink to="/settings/provider">
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import runtime from './runtime'
|
|||||||
import selectionStore from './selectionStore'
|
import selectionStore from './selectionStore'
|
||||||
import settings from './settings'
|
import settings from './settings'
|
||||||
import shortcuts from './shortcuts'
|
import shortcuts from './shortcuts'
|
||||||
|
import tabs from './tabs'
|
||||||
import topics from './topics'
|
import topics from './topics'
|
||||||
import websearch from './websearch'
|
import websearch from './websearch'
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ const rootReducer = combineReducers({
|
|||||||
copilot,
|
copilot,
|
||||||
selectionStore,
|
selectionStore,
|
||||||
topics,
|
topics,
|
||||||
|
tabs,
|
||||||
// messages: messagesReducer,
|
// messages: messagesReducer,
|
||||||
messages: newMessagesReducer,
|
messages: newMessagesReducer,
|
||||||
messageBlocks: messageBlocksReducer,
|
messageBlocks: messageBlocksReducer,
|
||||||
@ -53,7 +55,7 @@ const persistedReducer = persistReducer(
|
|||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 114,
|
version: 114,
|
||||||
blacklist: ['runtime', 'messages', 'messageBlocks'],
|
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
rootReducer
|
rootReducer
|
||||||
|
|||||||
57
src/renderer/src/store/tabs.ts
Normal file
57
src/renderer/src/store/tabs.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
export interface Tab {
|
||||||
|
id: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabsState {
|
||||||
|
tabs: Tab[]
|
||||||
|
activeTabId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: TabsState = {
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
id: 'home',
|
||||||
|
path: '/'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
activeTabId: 'home'
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabsSlice = createSlice({
|
||||||
|
name: 'tabs',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
addTab: (state, action: PayloadAction<Tab>) => {
|
||||||
|
const existingTab = state.tabs.find((tab) => tab.path === action.payload.path)
|
||||||
|
if (!existingTab) {
|
||||||
|
state.tabs.push(action.payload)
|
||||||
|
}
|
||||||
|
state.activeTabId = action.payload.id
|
||||||
|
},
|
||||||
|
removeTab: (state, action: PayloadAction<string>) => {
|
||||||
|
const index = state.tabs.findIndex((tab) => tab.id === action.payload)
|
||||||
|
if (index !== -1) {
|
||||||
|
state.tabs.splice(index, 1)
|
||||||
|
// 如果关闭的是当前标签页,则切换到最后一个标签页
|
||||||
|
if (action.payload === state.activeTabId) {
|
||||||
|
state.activeTabId = state.tabs[state.tabs.length - 1].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateTab: (state, action: PayloadAction<{ id: string; updates: Partial<Tab> }>) => {
|
||||||
|
const tab = state.tabs.find((tab) => tab.id === action.payload.id)
|
||||||
|
if (tab) {
|
||||||
|
Object.assign(tab, action.payload.updates)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setActiveTab: (state, action: PayloadAction<string>) => {
|
||||||
|
state.activeTabId = action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { addTab, removeTab, setActiveTab, updateTab } = tabsSlice.actions
|
||||||
|
export default tabsSlice.reducer
|
||||||
Loading…
Reference in New Issue
Block a user