feat: new app sidebar

This commit is contained in:
kangfenmao 2025-06-09 11:20:41 +08:00
parent e13b136484
commit 75b9e2f408
53 changed files with 751 additions and 383 deletions

View File

@ -63,7 +63,7 @@ export class WindowService {
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight, titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF', backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors, darkTheme: nativeTheme.shouldUseDarkColors,
trafficLightPosition: { x: 8, y: 12 }, trafficLightPosition: { x: 15, y: 12 },
...(isLinux ? { icon } : {}), ...(isLinux ? { icon } : {}),
webPreferences: { webPreferences: {
preload: join(__dirname, '../preload/index.js'), preload: join(__dirname, '../preload/index.js'),

View File

@ -65,7 +65,7 @@ export function handleMcpProtocolUrl(url: URL) {
const mainWindow = windowService.getMainWindow() const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')") mainWindow.webContents.executeJavaScript("window.navigate('/mcp-servers')")
} }
break break
} }

View File

@ -5,7 +5,7 @@ import { Provider } from 'react-redux'
import { HashRouter, Route, Routes } from 'react-router-dom' import { HashRouter, Route, Routes } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar' import MainSidebar from './components/app/MainSidebar'
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'
@ -18,6 +18,7 @@ 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 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'
@ -34,7 +35,7 @@ function App(): React.ReactElement {
<TopViewContainer> <TopViewContainer>
<HashRouter> <HashRouter>
<NavigationHandler /> <NavigationHandler />
<Sidebar /> <MainSidebar />
<Routes> <Routes>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} /> <Route path="/agents" element={<AgentsPage />} />
@ -43,6 +44,7 @@ function App(): React.ReactElement {
<Route path="/files" element={<FilesPage />} /> <Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} /> <Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} /> <Route path="/apps" element={<AppsPage />} />
<Route path="/mcp-servers/*" element={<McpServersPage />} />
<Route path="/settings/*" element={<SettingsPage />} /> <Route path="/settings/*" element={<SettingsPage />} />
</Routes> </Routes>
</HashRouter> </HashRouter>

View File

@ -25,7 +25,6 @@
} }
.minapp-drawer { .minapp-drawer {
max-width: calc(100vw - var(--sidebar-width));
.ant-drawer-content-wrapper { .ant-drawer-content-wrapper {
box-shadow: none; box-shadow: none;
} }
@ -33,7 +32,7 @@
position: absolute; position: absolute;
-webkit-app-region: drag; -webkit-app-region: drag;
min-height: calc(var(--navbar-height) + 0.5px); min-height: calc(var(--navbar-height) + 0.5px);
width: calc(100vw - var(--sidebar-width)); width: 100%;
margin-top: -0.5px; margin-top: -0.5px;
border-bottom: none; border-bottom: none;
} }

View File

@ -29,7 +29,7 @@
--color-text-secondary: rgba(235, 235, 245, 0.7); --color-text-secondary: rgba(235, 235, 245, 0.7);
--color-icon: #ffffff99; --color-icon: #ffffff99;
--color-icon-white: #ffffff; --color-icon-white: #ffffff;
--color-border: #ffffff19; --color-border: #000;
--color-border-soft: #ffffff10; --color-border-soft: #ffffff10;
--color-border-mute: #ffffff05; --color-border-mute: #ffffff05;
--color-error: #f44336; --color-error: #f44336;
@ -44,8 +44,8 @@
--color-reference-text: #ffffff; --color-reference-text: #ffffff;
--color-reference-background: #0b0e12; --color-reference-background: #0b0e12;
--color-list-item: #222; --color-list-item: rgba(255, 255, 255, 0.1);
--color-list-item-hover: #1e1e1e; --color-list-item-hover: rgba(255, 255, 255, 0.05);
--modal-background: #1f1f1f; --modal-background: #1f1f1f;
@ -56,7 +56,7 @@
--navbar-background-mac: rgba(20, 20, 20, 0.55); --navbar-background-mac: rgba(20, 20, 20, 0.55);
--navbar-background: #1f1f1f; --navbar-background: #1f1f1f;
--navbar-height: 40px; --navbar-height: 45px;
--sidebar-width: 50px; --sidebar-width: 50px;
--status-bar-height: 40px; --status-bar-height: 40px;
--input-bar-height: 100px; --input-bar-height: 100px;
@ -71,7 +71,8 @@
--chat-background-assistant: #2c2c2c; --chat-background-assistant: #2c2c2c;
--chat-text-user: var(--color-black); --chat-text-user: var(--color-black);
--list-item-border-radius: 20px; --list-item-border-radius: 8px;
--border-width: 0.5px;
} }
[theme-mode='light'] { [theme-mode='light'] {
@ -120,8 +121,8 @@
--color-reference-text: #000000; --color-reference-text: #000000;
--color-reference-background: #f1f7ff; --color-reference-background: #f1f7ff;
--color-list-item: #eee; --color-list-item: rgba(255, 255, 255, 0.9);
--color-list-item-hover: #f5f5f5; --color-list-item-hover: rgba(255, 255, 255, 0.5);
--modal-background: var(--color-white); --modal-background: var(--color-white);
@ -136,4 +137,6 @@
--chat-background-user: #95ec69; --chat-background-user: #95ec69;
--chat-background-assistant: #ffffff; --chat-background-assistant: #ffffff;
--chat-text-user: var(--color-text); --chat-text-user: var(--color-text);
--border-width: 0.5px;
} }

View File

@ -1,8 +1,6 @@
#content-container { #content-container {
background-color: var(--color-background); background-color: var(--color-background);
border-top: 0.5px solid var(--color-border); border-top: 0.5px solid var(--color-border);
border-top-left-radius: 10px;
border-left: 0.5px solid var(--color-border);
} }
.group-container { .group-container {

View File

@ -395,10 +395,7 @@ const MinappPopupContainer: React.FC = () => {
height={'100%'} height={'100%'}
maskClosable={false} maskClosable={false}
closeIcon={null} closeIcon={null}
style={{ style={{ backgroundColor: window.root.style.background }}>
marginLeft: 'var(--sidebar-width)',
backgroundColor: window.root.style.background
}}>
{!isReady && ( {!isReady && (
<EmptyView> <EmptyView>
<Avatar <Avatar
@ -418,7 +415,7 @@ const TitleContainer = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding-left: ${isMac ? '20px' : '10px'}; padding-left: ${isMac ? '80px' : '10px'};
padding-right: 10px; padding-right: 10px;
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -78,7 +78,7 @@ const WebviewContainer = memo(
) )
const WebviewStyle: React.CSSProperties = { const WebviewStyle: React.CSSProperties = {
width: 'calc(100vw - var(--sidebar-width))', width: '100vw',
height: 'calc(100vh - var(--navbar-height))', height: 'calc(100vh - var(--navbar-height))',
backgroundColor: 'var(--color-background)', backgroundColor: 'var(--color-background)',
display: 'inline-flex' display: 'inline-flex'

View File

@ -0,0 +1,72 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { MessageSquareDiff } from 'lucide-react'
import { FC } from 'react'
import styled from 'styled-components'
interface Props {}
const HeaderNavbar: FC<Props> = () => {
return (
<Container>
<div></div>
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
<MessageSquareDiff size={18} />
</NavbarIcon>
</Tooltip>
</Container>
)
}
const Container = styled.div`
display: flex;
width: var(--assistant-width);
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0;
padding-left: var(--sidebar-width);
height: var(--navbar-height);
min-height: var(--navbar-height);
background-color: transparent;
-webkit-app-region: drag;
`
export const NavbarIcon = styled.div`
-webkit-app-region: none;
border-radius: 8px;
height: 30px;
padding: 0 7px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
-webkit-app-region: no-drag;
cursor: pointer;
.iconfont {
font-size: 18px;
color: var(--color-icon);
&.icon-a-addchat {
font-size: 20px;
}
&.icon-a-darkmode {
font-size: 20px;
}
&.icon-appstore {
font-size: 20px;
}
}
.anticon {
color: var(--color-icon);
font-size: 16px;
}
&:hover {
background-color: var(--color-background-mute);
color: var(--color-icon-white);
}
`
export default HeaderNavbar

View File

@ -0,0 +1,378 @@
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistants } from '@renderer/hooks/useAssistant'
import useAvatar from '@renderer/hooks/useAvatar'
import { useChat } from '@renderer/hooks/useChat'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import NavigationService from '@renderer/services/NavigationService'
import { ThemeMode } from '@renderer/types'
import { isEmoji } from '@renderer/utils'
import { Avatar, Tooltip } from 'antd'
import {
Blocks,
Bot,
ChevronDown,
ChevronRight,
FileSearch,
Folder,
Languages,
LayoutGrid,
MessageSquare,
Moon,
Palette,
Sparkle,
SquareTerminal,
Sun,
SunMoon
} from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import Tabs from '../../pages/home/Tabs'
import MainNavbar from './MainNavbar'
type Tab = 'assistants' | 'topic' | 'settings'
const MainSidebar: FC = () => {
const { assistants } = useAssistants()
const navigate = useNavigate()
const [tab, setTab] = useState<Tab>('assistants')
const avatar = useAvatar()
const { userName, defaultPaintingProvider } = useSettings()
const { t } = useTranslation()
const { theme, settedTheme, toggleTheme } = useTheme()
const [isAppMenuExpanded, setIsAppMenuExpanded] = useState(false)
const location = useLocation()
const state = location.state
const { pathname } = location
const { activeAssistant, activeTopic, setActiveAssistant, setActiveTopic } = useChat()
const { showAssistants, showTopics, topicPosition } = useSettings()
useEffect(() => {
NavigationService.setNavigate(navigate)
}, [navigate])
useEffect(() => {
state?.assistant && setActiveAssistant(state?.assistant)
state?.topic && setActiveTopic(state?.topic)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state])
useEffect(() => {
const unsubscribe = EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => {
const newAssistant = assistants.find((a) => a.id === assistantId)
if (newAssistant) {
setActiveAssistant(newAssistant)
}
})
return () => {
unsubscribe()
}
}, [assistants, setActiveAssistant])
useEffect(() => {
const canMinimize = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
return () => {
window.api.window.resetMinimumSize()
}
}, [showAssistants, showTopics, topicPosition])
const onAvatarClick = () => {
navigate('/settings/provider')
}
const onChageTheme = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
toggleTheme()
}
const appMenuItems = [
{ icon: <Sparkle size={18} className="icon" />, text: t('agents.title'), path: '/agents' },
{ icon: <Languages size={18} className="icon" />, text: t('translate.title'), path: '/translate' },
{
icon: <Palette size={18} className="icon" />,
text: t('paintings.title'),
path: `/paintings/${defaultPaintingProvider}`
},
{ icon: <LayoutGrid size={18} className="icon" />, text: t('minapp.title'), path: '/apps' },
{ icon: <FileSearch size={18} className="icon" />, text: t('knowledge.title'), path: '/knowledge' },
{ icon: <SquareTerminal size={18} className="icon" />, text: t('common.mcp'), path: '/mcp-servers' },
{ icon: <Folder size={18} className="icon" />, text: t('files.title'), path: '/files' }
]
const isRoutes = (path: string): boolean => pathname.startsWith(path)
if (location.pathname !== '/') {
return null
}
const onChageTab = (tab: Tab) => {
setTab(tab)
setIsAppMenuExpanded(false)
}
if (!showAssistants) {
return null
}
return (
<Container id="main-sidebar">
<MainNavbar />
<MainMenu>
<MainMenuItem
active={tab === 'assistants' && location.pathname === '/'}
onClick={() => onChageTab('assistants')}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<Bot size={18} />
</MainMenuItemIcon>
<MainMenuItemText>{t('assistants.title')}</MainMenuItemText>
</MainMenuItemLeft>
{tab === 'topic' && (
<MainMenuItemRight>
<MainMenuItemRightText>{activeAssistant.name}</MainMenuItemRightText>
</MainMenuItemRight>
)}
</MainMenuItem>
<MainMenuItem active={tab === 'topic' && location.pathname === '/'} onClick={() => onChageTab('topic')}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<MessageSquare size={18} />
</MainMenuItemIcon>
<MainMenuItemText>{t('common.topics')}</MainMenuItemText>
</MainMenuItemLeft>
</MainMenuItem>
<MainMenuItem
style={{ opacity: isAppMenuExpanded ? 0.5 : 1 }}
onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<Blocks size={19} className="icon" />
</MainMenuItemIcon>
<MainMenuItemText>{t('common.apps')}</MainMenuItemText>
</MainMenuItemLeft>
<MainMenuItemRight>
{isAppMenuExpanded ? (
<ChevronDown size={18} color="var(--color-text-3)" />
) : (
<ChevronRight size={18} color="var(--color-text-3)" />
)}
</MainMenuItemRight>
</MainMenuItem>
{isAppMenuExpanded && (
<SubMenu>
{appMenuItems.map((item) => (
<MainMenuItem key={item.path} active={isRoutes(item.path)} onClick={() => navigate(item.path)}>
<MainMenuItemLeft>
<MainMenuItemIcon>{item.icon}</MainMenuItemIcon>
<MainMenuItemText>{item.text}</MainMenuItemText>
</MainMenuItemLeft>
</MainMenuItem>
))}
</SubMenu>
)}
</MainMenu>
<Tabs
tab={tab}
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position="left"
/>
<UserMenu onClick={onAvatarClick}>
<UserMenuLeft>
{isEmoji(avatar) ? (
<EmojiAvatar className="sidebar-avatar" size={31} fontSize={18}>
{avatar}
</EmojiAvatar>
) : (
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" />
)}
<UserMenuText>{userName}</UserMenuText>
</UserMenuLeft>
<Tooltip
title={t('settings.theme.title') + ': ' + t(`settings.theme.${settedTheme}`)}
mouseEnterDelay={0.8}
placement="right">
<Icon theme={theme} onClick={onChageTheme}>
{settedTheme === ThemeMode.dark ? (
<Moon size={18} className="icon" />
) : settedTheme === ThemeMode.light ? (
<Sun size={18} className="icon" />
) : (
<SunMoon size={18} className="icon" />
)}
</Icon>
</Tooltip>
</UserMenu>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
width: var(--assistant-width);
max-width: var(--assistant-width);
border-right: 0.5px solid var(--color-border);
`
const MainMenu = styled.div`
display: flex;
flex-direction: column;
padding: 10px;
padding-top: 0;
gap: 8px;
`
const MainMenuItem = styled.div<{ active?: boolean }>`
display: flex;
justify-content: space-between;
align-items: center;
gap: 5px;
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'transparent')};
padding: 5px 10px;
border-radius: 5px;
border-radius: 8px;
&:hover {
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'var(--color-list-item-hover)')};
}
`
const MainMenuItemLeft = styled.div`
display: flex;
align-items: center;
gap: 5px;
`
const MainMenuItemRight = styled.div`
display: flex;
align-items: center;
gap: 5px;
`
const MainMenuItemRightText = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const MainMenuItemIcon = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
`
const MainMenuItemText = styled.div`
font-size: 14px;
font-weight: 500;
`
const UserMenu = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 10px;
margin-bottom: 10px;
padding: 4px 8px;
gap: 5px;
border-radius: 8px;
&:hover {
background-color: var(--color-list-item);
}
`
const UserMenuLeft = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
`
const AvatarImg = styled(Avatar)`
width: 28px;
height: 28px;
background-color: var(--color-background-soft);
border: none;
cursor: pointer;
`
const UserMenuText = styled.div`
font-size: 14px;
font-weight: 500;
`
const Icon = styled.div<{ theme: string }>`
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
box-sizing: border-box;
-webkit-app-region: none;
border: 0.5px solid transparent;
&:hover {
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
opacity: 0.8;
cursor: pointer;
.icon {
color: var(--color-icon-white);
}
}
&.active {
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
border: 0.5px solid var(--color-border);
.icon {
color: var(--color-primary);
}
}
@keyframes borderBreath {
0% {
opacity: 0.1;
}
50% {
opacity: 1;
}
100% {
opacity: 0.1;
}
}
&.opened-minapp {
position: relative;
}
&.opened-minapp::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
border-radius: inherit;
opacity: 0.3;
border: 0.5px solid var(--color-primary);
}
`
const SubMenu = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
`
export default MainSidebar

View File

@ -1,24 +1,16 @@
import { isLinux, isMac, isWindows } from '@renderer/config/constant' import { isLinux, isMac, isWindows } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen' import { useFullscreen } from '@renderer/hooks/useFullscreen'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor' import { Button } from 'antd'
import { 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 from 'styled-components' import styled from 'styled-components'
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement> type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
export const Navbar: FC<Props> = ({ children, ...props }) => { export const Navbar: FC<Props> = ({ children, ...props }) => {
const backgroundColor = useNavBackgroundColor() return <NavbarContainer {...props}>{children}</NavbarContainer>
return (
<NavbarContainer {...props} style={{ backgroundColor }}>
{children}
</NavbarContainer>
)
}
export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
return <NavbarLeftContainer {...props}>{children}</NavbarLeftContainer>
} }
export const NavbarCenter: FC<Props> = ({ children, ...props }) => { export const NavbarCenter: FC<Props> = ({ children, ...props }) => {
@ -36,9 +28,12 @@ export const NavbarRight: FC<Props> = ({ children, ...props }) => {
export const NavbarMain: FC<Props> = ({ children, ...props }) => { export const NavbarMain: FC<Props> = ({ children, ...props }) => {
const isFullscreen = useFullscreen() const isFullscreen = useFullscreen()
const navigate = useNavigate()
return ( return (
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}> <NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
{children} {children}
<Button type="text" icon={<X size={18} />} onClick={() => navigate('/')} className="nodrag"></Button>
</NavbarMainContainer> </NavbarMainContainer>
) )
} }
@ -49,28 +44,8 @@ const NavbarContainer = styled.div`
flex-direction: row; flex-direction: row;
min-height: var(--navbar-height); min-height: var(--navbar-height);
max-height: var(--navbar-height); max-height: var(--navbar-height);
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
-webkit-app-region: drag; -webkit-app-region: drag;
` background-color: var(--color-background);
const NavbarLeftContainer = styled.div`
min-width: var(--assistants-width);
padding: 0 10px;
display: flex;
flex-direction: row;
align-items: center;
font-weight: bold;
color: var(--color-text-1);
`
const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
padding: 0 ${isMac ? '20px' : 0};
font-weight: bold;
color: var(--color-text-1);
` `
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>` const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
@ -87,9 +62,26 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
justify-content: space-between; justify-content: space-between;
padding: 0 ${isMac ? '20px' : 0}; padding-left: ${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 ? '140px' : isLinux ? '120px' : '12px')}; padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
-webkit-app-region: drag;
`
const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
padding: 0 8px;
font-weight: bold;
justify-content: space-between;
color: var(--color-text-1);
` `

View File

@ -0,0 +1,42 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setActiveAssistant, setActiveTopic } from '@renderer/store/runtime'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant } from '@renderer/types'
import { Topic } from '@renderer/types'
import { find } from 'lodash'
import { useEffect } from 'react'
import { useAssistants } from './useAssistant'
export const useChat = () => {
const { assistants } = useAssistants()
const activeAssistant = useAppSelector((state) => state.runtime.chat.activeAssistant) || assistants[0]
const activeTopic = useAppSelector((state) => state.runtime.chat.activeTopic) || activeAssistant?.topics[0]!
const dispatch = useAppDispatch()
useEffect(() => {
if (activeTopic) {
dispatch(loadTopicMessagesThunk(activeTopic.id))
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
}
}, [activeTopic, dispatch])
useEffect(() => {
// activeTopic not in assistant.topics
if (activeAssistant && !find(activeAssistant.topics, { id: activeTopic?.id })) {
dispatch(setActiveTopic(activeAssistant.topics[0]))
}
}, [activeTopic?.id, activeAssistant, dispatch])
return {
activeAssistant,
activeTopic,
setActiveAssistant: (assistant: Assistant) => {
dispatch(setActiveAssistant(assistant))
},
setActiveTopic: (topic: Topic) => {
dispatch(setActiveTopic(topic))
}
}
}

View File

@ -1,47 +1,16 @@
import db from '@renderer/databases' import db from '@renderer/databases'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { deleteMessageFiles } from '@renderer/services/MessagesService' import { deleteMessageFiles } from '@renderer/services/MessagesService'
import store from '@renderer/store' import store from '@renderer/store'
import { updateTopic } from '@renderer/store/assistants' import { updateTopic } from '@renderer/store/assistants'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find' import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
import { find, isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { useEffect, useState } from 'react'
import { useAssistant } from './useAssistant'
import { getStoreSetting } from './useSettings' import { getStoreSetting } from './useSettings'
const renamingTopics = new Set<string>() const renamingTopics = new Set<string>()
let _activeTopic: Topic
let _setActiveTopic: (topic: Topic) => void
export function useActiveTopic(_assistant: Assistant, topic?: Topic) {
const { assistant } = useAssistant(_assistant.id)
const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0])
_activeTopic = activeTopic
_setActiveTopic = setActiveTopic
useEffect(() => {
if (activeTopic) {
store.dispatch(loadTopicMessagesThunk(activeTopic.id))
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
}
}, [activeTopic])
useEffect(() => {
// activeTopic not in assistant.topics
if (assistant && !find(assistant.topics, { id: activeTopic?.id })) {
setActiveTopic(assistant.topics[0])
}
}, [activeTopic?.id, assistant])
return { activeTopic, setActiveTopic }
}
export function useTopic(assistant: Assistant, topicId?: string) { export function useTopic(assistant: Assistant, topicId?: string) {
return assistant?.topics.find((topic) => topic.id === topicId) return assistant?.topics.find((topic) => topic.id === topicId)
} }
@ -86,7 +55,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
.substring(0, 50) .substring(0, 50)
if (topicName) { if (topicName) {
const data = { ...topic, name: topicName } as Topic const data = { ...topic, name: topicName } as Topic
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data })) store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} }
return return
@ -97,7 +65,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant }) const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
if (summaryText) { if (summaryText) {
const data = { ...topic, name: summaryText } const data = { ...topic, name: summaryText }
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data })) store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} else { } else {
window.message?.error(i18n.t('message.error.fetchTopicName')) window.message?.error(i18n.t('message.error.fetchTopicName'))

View File

@ -421,7 +421,9 @@
"pinyin.asc": "Sort by Pinyin (A-Z)", "pinyin.asc": "Sort by Pinyin (A-Z)",
"pinyin.desc": "Sort by Pinyin (Z-A)" "pinyin.desc": "Sort by Pinyin (Z-A)"
}, },
"no_results": "No results" "no_results": "No results",
"apps": "Apps",
"mcp": "Tools"
}, },
"docs": { "docs": {
"title": "Docs" "title": "Docs"

View File

@ -421,7 +421,9 @@
"pinyin.asc": "ピンインで昇順ソート", "pinyin.asc": "ピンインで昇順ソート",
"pinyin.desc": "ピンインで降順ソート" "pinyin.desc": "ピンインで降順ソート"
}, },
"no_results": "検索結果なし" "no_results": "検索結果なし",
"apps": "アプリ",
"mcp": "ツール"
}, },
"docs": { "docs": {
"title": "ドキュメント" "title": "ドキュメント"

View File

@ -421,7 +421,9 @@
"pinyin.asc": "Сортировать по пиньинь (А-Я)", "pinyin.asc": "Сортировать по пиньинь (А-Я)",
"pinyin.desc": "Сортировать по пиньинь (Я-А)" "pinyin.desc": "Сортировать по пиньинь (Я-А)"
}, },
"no_results": "Результатов не найдено" "no_results": "Результатов не найдено",
"apps": "Приложения",
"mcp": "Инструменты"
}, },
"docs": { "docs": {
"title": "Документация" "title": "Документация"

View File

@ -421,7 +421,9 @@
"pinyin.asc": "按拼音升序", "pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序" "pinyin.desc": "按拼音降序"
}, },
"no_results": "无结果" "no_results": "无结果",
"apps": "应用",
"mcp": "工具"
}, },
"docs": { "docs": {
"title": "帮助文档" "title": "帮助文档"

View File

@ -421,7 +421,9 @@
"pinyin.asc": "按拼音升序", "pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序" "pinyin.desc": "按拼音降序"
}, },
"no_results": "沒有結果" "no_results": "沒有結果",
"apps": "應用",
"mcp": "工具"
}, },
"docs": { "docs": {
"title": "說明文件" "title": "說明文件"

View File

@ -1,5 +1,5 @@
import { ImportOutlined, PlusOutlined } from '@ant-design/icons' import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import CustomTag from '@renderer/components/CustomTag' import CustomTag from '@renderer/components/CustomTag'
import ListItem from '@renderer/components/ListItem' import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
@ -152,7 +152,7 @@ const AgentsPage: FC = () => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}> <NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('agents.title')} {t('agents.title')}
<Input <Input
@ -171,7 +171,7 @@ const AgentsPage: FC = () => {
/> />
<div style={{ width: 80 }} /> <div style={{ width: 80 }} />
</NavbarCenter> </NavbarCenter>
</Navbar> </NavbarMain>
<Main id="content-container"> <Main id="content-container">
<AgentsGroupList> <AgentsGroupList>

View File

@ -1,7 +1,7 @@
import { Navbar, NavbarMain } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import { useMinapps } from '@renderer/hooks/useMinapps' import { useMinapps } from '@renderer/hooks/useMinapps'
import { Button, Input } from 'antd' import { Button, Input } from 'antd'
import { Search, SettingsIcon, X } from 'lucide-react' import { Search, SettingsIcon } from 'lucide-react'
import React, { FC, useEffect, useState } from 'react' import React, { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router' import { useLocation } from 'react-router'
@ -41,8 +41,8 @@ const AppsPage: FC = () => {
return ( return (
<Container onContextMenu={handleContextMenu}> <Container onContextMenu={handleContextMenu}>
<Navbar> <NavbarMain>
<NavbarMain> <NavbarCenter>
{t('minapp.title')} {t('minapp.title')}
<Input <Input
placeholder={t('common.search')} placeholder={t('common.search')}
@ -50,10 +50,7 @@ const AppsPage: FC = () => {
style={{ style={{
width: '30%', width: '30%',
height: 28, height: 28,
borderRadius: 15, borderRadius: 15
position: 'absolute',
left: '50vw',
transform: 'translateX(-50%)'
}} }}
size="small" size="small"
variant="filled" variant="filled"
@ -65,11 +62,11 @@ const AppsPage: FC = () => {
<Button <Button
type="text" type="text"
className="nodrag" className="nodrag"
icon={isSettingsOpen ? <X size={18} /> : <SettingsIcon size={18} color="var(--color-text-2)" />} icon={<SettingsIcon size={18} color={isSettingsOpen ? 'var(--color-primary)' : 'var(--color-text-2)'} />}
onClick={() => setIsSettingsOpen(!isSettingsOpen)} onClick={() => setIsSettingsOpen(!isSettingsOpen)}
/> />
</NavbarMain> </NavbarCenter>
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
{isSettingsOpen && <MiniAppSettings />} {isSettingsOpen && <MiniAppSettings />}
{!isSettingsOpen && ( {!isSettingsOpen && (

View File

@ -5,7 +5,7 @@ import {
SortAscendingOutlined, SortAscendingOutlined,
SortDescendingOutlined SortDescendingOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import ListItem from '@renderer/components/ListItem' import ListItem from '@renderer/components/ListItem'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup' import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import Logger from '@renderer/config/logger' import Logger from '@renderer/config/logger'
@ -207,9 +207,9 @@ const FilesPage: FC = () => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<SideNav> <SideNav>
{menuItems.map((item) => ( {menuItems.map((item) => (

View File

@ -15,7 +15,6 @@ import styled from 'styled-components'
import Inputbar from './Inputbar/Inputbar' import Inputbar from './Inputbar/Inputbar'
import Messages from './Messages/Messages' import Messages from './Messages/Messages'
import Tabs from './Tabs'
interface Props { interface Props {
assistant: Assistant assistant: Assistant
@ -38,7 +37,7 @@ const Chat: FC<Props> = (props) => {
const showRightTopics = showTopics && topicPosition === 'right' const showRightTopics = showTopics && topicPosition === 'right'
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : '' const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : '' const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})` return `calc(100vw - ${minusAssistantsWidth} ${minusRightTopicsWidth})`
}, [showAssistants, showTopics, topicPosition]) }, [showAssistants, showTopics, topicPosition])
useHotkeys('esc', () => { useHotkeys('esc', () => {
@ -128,15 +127,6 @@ const Chat: FC<Props> = (props) => {
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />} {isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
</QuickPanelProvider> </QuickPanelProvider>
</Main> </Main>
{topicPosition === 'right' && showTopics && (
<Tabs
activeAssistant={assistant}
activeTopic={props.activeTopic}
setActiveAssistant={props.setActiveAssistant}
setActiveTopic={props.setActiveTopic}
position="right"
/>
)}
</Container> </Container>
) )
} }

View File

@ -1,18 +1,14 @@
import { useAssistants } from '@renderer/hooks/useAssistant' import { useAssistants } from '@renderer/hooks/useAssistant'
import { useChat } from '@renderer/hooks/useChat'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import NavigationService from '@renderer/services/NavigationService' import NavigationService from '@renderer/services/NavigationService'
import { Assistant } from '@renderer/types' import { FC, useEffect } from 'react'
import { FC, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import Chat from './Chat' import Chat from './Chat'
import Navbar from './Navbar' import Navbar from './Navbar'
import HomeTabs from './Tabs'
let _activeAssistant: Assistant
const HomePage: FC = () => { const HomePage: FC = () => {
const { assistants } = useAssistants() const { assistants } = useAssistants()
@ -21,12 +17,9 @@ const HomePage: FC = () => {
const location = useLocation() const location = useLocation()
const state = location.state const state = location.state
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0]) const { activeAssistant, activeTopic, setActiveAssistant, setActiveTopic } = useChat()
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
const { showAssistants, showTopics, topicPosition } = useSettings() const { showAssistants, showTopics, topicPosition } = useSettings()
_activeAssistant = activeAssistant
useEffect(() => { useEffect(() => {
NavigationService.setNavigate(navigate) NavigationService.setNavigate(navigate)
}, [navigate]) }, [navigate])
@ -69,15 +62,6 @@ const HomePage: FC = () => {
position="left" position="left"
/> />
<ContentContainer id="content-container"> <ContentContainer id="content-container">
{showAssistants && (
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position="left"
/>
)}
<Chat <Chat
assistant={activeAssistant} assistant={activeAssistant}
activeTopic={activeTopic} activeTopic={activeTopic}
@ -93,7 +77,6 @@ const Container = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
max-width: calc(100vw - var(--sidebar-width));
` `
const ContentContainer = styled.div` const ContentContainer = styled.div`

View File

@ -176,7 +176,7 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
newList.push({ newList.push({
label: t('settings.mcp.addServer') + '...', label: t('settings.mcp.addServer') + '...',
icon: <Plus />, icon: <Plus />,
action: () => navigate('/settings/mcp') action: () => navigate('/mcp-servers')
}) })
newList.unshift({ newList.unshift({

View File

@ -257,7 +257,7 @@ const MessageFooter = styled.div`
align-items: center; align-items: center;
padding: 2px 0; padding: 2px 0;
margin-top: 2px; margin-top: 2px;
border-top: 1px dotted var(--color-border); border-top: 0.5px dotted var(--color-border);
gap: 20px; gap: 20px;
` `

View File

@ -1,9 +1,9 @@
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' import { Navbar } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import FloatingSidebar from '@renderer/components/Popups/FloatingSidebar' import FloatingSidebar from '@renderer/components/Popups/FloatingSidebar'
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover' import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
import SearchPopup from '@renderer/components/Popups/SearchPopup' import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isMac } from '@renderer/config/constant' import { isLinux, isMac, isWindows } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useFullscreen } from '@renderer/hooks/useFullscreen' import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { modelGenerating } from '@renderer/hooks/useRuntime' import { modelGenerating } from '@renderer/hooks/useRuntime'
@ -16,7 +16,7 @@ import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import { t } from 'i18next' import { t } from 'i18next'
import { LayoutGrid, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react' import { LayoutGrid, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { FC, useCallback, useState } from 'react' import { FC, useCallback, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -54,6 +54,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
toggleShowAssistants() toggleShowAssistants()
} }
}, [showAssistants, toggleShowAssistants]) }, [showAssistants, toggleShowAssistants])
const handleToggleShowTopics = useCallback(() => { const handleToggleShowTopics = useCallback(() => {
if (showTopics) { if (showTopics) {
// When hiding sidebar, set cooldown // When hiding sidebar, set cooldown
@ -89,48 +90,13 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
return ( return (
<Navbar className="home-navbar"> <Navbar className="home-navbar">
{showAssistants && ( <NavbarContainer $isFullscreen={isFullscreen} $showSidebar={showAssistants} className="home-navbar-right">
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: 0 }}>
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={handleToggleShowAssistants} style={{ marginLeft: isMac && !isFullscreen ? 16 : 0 }}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
<MessageSquareDiff size={18} />
</NavbarIcon>
</Tooltip>
</NavbarLeft>
)}
<NavbarRight style={{ justifyContent: 'space-between', flex: 1 }} className="home-navbar-right">
<HStack alignItems="center"> <HStack alignItems="center">
{!showAssistants && !sidebarHideCooldown && ( <NavbarIcon
<FloatingSidebar onClick={() => toggleShowAssistants()}
activeAssistant={assistant} style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
setActiveAssistant={setActiveAssistant} {showAssistants ? <PanelLeftClose size={18} /> : <PanelRightClose size={18} />}
activeTopic={activeTopic} </NavbarIcon>
setActiveTopic={setActiveTopic}
position={'left'}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
</FloatingSidebar>
)}
{!showAssistants && sidebarHideCooldown && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}
onMouseOut={() => setSidebarHideCooldown(false)}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
)}
<SelectModelButton assistant={assistant} /> <SelectModelButton assistant={assistant} />
</HStack> </HStack>
<HStack alignItems="center" gap={8}> <HStack alignItems="center" gap={8}>
@ -183,11 +149,27 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
</Tooltip> </Tooltip>
)} )}
</HStack> </HStack>
</NavbarRight> </NavbarContainer>
</Navbar> </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;
padding-left: ${({ $showSidebar }) => (isMac && !$showSidebar ? '70px' : '10px')};
font-weight: bold;
color: var(--color-text-1);
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
-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;

View File

@ -6,7 +6,7 @@ import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistantsTabSortType } from '@renderer/hooks/useStore' import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
import { useTags } from '@renderer/hooks/useTags' import { useTags } from '@renderer/hooks/useTags'
import { Assistant, AssistantsSortType } from '@renderer/types' import { Assistant, AssistantsSortType } from '@renderer/types'
import { Divider, Tooltip } from 'antd' import { Tooltip } from 'antd'
import { FC, useCallback, useRef, useState } from 'react' import { FC, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -80,7 +80,7 @@ const Assistants: FC<AssistantsTabProps> = ({
if (assistantsTabSortType === 'tags') { if (assistantsTabSortType === 'tags') {
return ( return (
<Container className="assistants-tab" ref={containerRef}> <Container className="assistants-tab" ref={containerRef}>
<div style={{ marginBottom: '8px' }}> <div style={{ marginBottom: 4 }}>
{getGroupedAssistants.map((group) => ( {getGroupedAssistants.map((group) => (
<TagsContainer key={group.tag}> <TagsContainer key={group.tag}>
{group.tag !== t('assistants.tags.untagged') && ( {group.tag !== t('assistants.tags.untagged') && (
@ -95,7 +95,7 @@ const Assistants: FC<AssistantsTabProps> = ({
{group.tag} {group.tag}
</GroupTitleName> </GroupTitleName>
</Tooltip> </Tooltip>
<Divider style={{ margin: '12px 0' }}></Divider> <GroupTitleDivider />
</GroupTitle> </GroupTitle>
)} )}
{!collapsedTags[group.tag] && ( {!collapsedTags[group.tag] && (
@ -207,13 +207,15 @@ const AssistantAddItem = styled.div`
` `
const GroupTitle = styled.div` const GroupTitle = styled.div`
padding: 8px 0;
position: relative;
color: var(--color-text-2); color: var(--color-text-2);
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
margin-bottom: -8px;
cursor: pointer; cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 24px;
` `
const GroupTitleName = styled.div` const GroupTitleName = styled.div`
@ -221,13 +223,18 @@ const GroupTitleName = styled.div`
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
background-color: var(--color-background);
box-sizing: border-box; box-sizing: border-box;
padding: 0 4px; padding: 0 4px;
color: var(--color-text); color: var(--color-text);
position: absolute;
transform: translateY(2px);
font-size: 13px; font-size: 13px;
line-height: 24px;
margin-right: 2px;
display: flex;
`
const GroupTitleDivider = styled.div`
flex: 1;
border-top: 1px solid var(--color-border);
` `
const AssistantName = styled.div` const AssistantName = styled.div`

View File

@ -20,11 +20,11 @@ import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { getDefaultModel, getDefaultTopic } from '@renderer/services/AssistantService' import { getDefaultModel, getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, AssistantsSortType } from '@renderer/types' import { Assistant, AssistantsSortType } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils' import { classNames, getLeadingEmoji, uuid } from '@renderer/utils'
import { hasTopicPendingRequests } from '@renderer/utils/queue' import { hasTopicPendingRequests } from '@renderer/utils/queue'
import { Dropdown, MenuProps } from 'antd' import { Button, Dropdown, MenuProps } from 'antd'
import { omit } from 'lodash' import { omit } from 'lodash'
import { AlignJustify, Plus, Settings2, Tag, Tags } from 'lucide-react' import { AlignJustify, EllipsisVertical, Plus, Settings2, Tag, Tags } from 'lucide-react'
import { FC, memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react' import { FC, memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -63,6 +63,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
const { assistants, updateAssistants } = useAssistants() const { assistants, updateAssistants } = useAssistants()
const [isPending, setIsPending] = useState(false) const [isPending, setIsPending] = useState(false)
const [isMenuOpen, setIsMenuOpen] = useState(false)
useEffect(() => { useEffect(() => {
if (isActive) { if (isActive) {
@ -141,7 +142,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
return ( return (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}> <Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Container onClick={handleSwitch} className={isActive ? 'active' : ''}> <Container onClick={handleSwitch} className={classNames({ active: isActive, 'is-menu-open': isMenuOpen })}>
<AssistantNameRow className="name" title={fullAssistantName}> <AssistantNameRow className="name" title={fullAssistantName}>
{assistantIconType === 'model' ? ( {assistantIconType === 'model' ? (
<ModelAvatar <ModelAvatar
@ -159,11 +160,15 @@ const AssistantItem: FC<AssistantItemProps> = ({
)} )}
<AssistantName className="text-nowrap">{assistantName}</AssistantName> <AssistantName className="text-nowrap">{assistantName}</AssistantName>
</AssistantNameRow> </AssistantNameRow>
{isActive && ( <Dropdown menu={{ items: menuItems }} trigger={['click']} onOpenChange={setIsMenuOpen}>
<MenuButton onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}> <Button
<TopicCount className="topics-count">{assistant.topics.length}</TopicCount> className="item-menu-button"
</MenuButton> type="text"
)} size="small"
icon={<EllipsisVertical size={16} color="var(--color-text-3)" />}
onClick={(e) => e.stopPropagation()}
/>
</Dropdown>
</Container> </Container>
</Dropdown> </Dropdown>
) )
@ -382,6 +387,7 @@ const Container = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center;
padding: 0 8px; padding: 0 8px;
height: 37px; height: 37px;
position: relative; position: relative;
@ -389,12 +395,23 @@ const Container = styled.div`
border: 0.5px solid transparent; border: 0.5px solid transparent;
width: calc(var(--assistants-width) - 20px); width: calc(var(--assistants-width) - 20px);
cursor: pointer; cursor: pointer;
&.is-menu-open {
.item-menu-button {
display: block;
}
}
&:hover { &:hover {
background-color: var(--color-list-item-hover); background-color: var(--color-list-item-hover);
.item-menu-button {
display: block;
}
} }
&.active { &.active {
background-color: var(--color-list-item); background-color: var(--color-list-item);
} }
.item-menu-button {
display: none;
}
` `
const AssistantNameRow = styled.div` const AssistantNameRow = styled.div`
@ -410,31 +427,4 @@ const AssistantName = styled.div`
font-size: 13px; font-size: 13px;
` `
const MenuButton = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
min-width: 22px;
height: 22px;
min-height: 22px;
border-radius: 11px;
position: absolute;
background-color: var(--color-background);
right: 9px;
top: 6px;
padding: 0 5px;
border: 0.5px solid var(--color-border);
`
const TopicCount = styled.div`
color: var(--color-text);
font-size: 10px;
border-radius: 10px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
`
export default memo(AssistantItem) export default memo(AssistantItem)

View File

@ -2,11 +2,10 @@ import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant' import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useShowTopics } from '@renderer/hooks/useStore' import { useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { Segmented as AntSegmented, SegmentedProps } from 'antd' import { Segmented as AntSegmented } from 'antd'
import { FC, useEffect, useState } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -15,6 +14,7 @@ import Settings from './SettingsTab'
import Topics from './TopicsTab' import Topics from './TopicsTab'
interface Props { interface Props {
tab: Tab
activeAssistant: Assistant activeAssistant: Assistant
activeTopic: Topic activeTopic: Topic
setActiveAssistant: (assistant: Assistant) => void setActiveAssistant: (assistant: Assistant) => void
@ -26,9 +26,8 @@ interface Props {
type Tab = 'assistants' | 'topic' | 'settings' type Tab = 'assistants' | 'topic' | 'settings'
let _tab: any = ''
const HomeTabs: FC<Props> = ({ const HomeTabs: FC<Props> = ({
tab,
activeAssistant, activeAssistant,
activeTopic, activeTopic,
setActiveAssistant, setActiveAssistant,
@ -38,21 +37,12 @@ const HomeTabs: FC<Props> = ({
style style
}) => { }) => {
const { addAssistant } = useAssistants() const { addAssistant } = useAssistants()
const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic')
const { topicPosition } = useSettings() const { topicPosition } = useSettings()
const { defaultAssistant } = useDefaultAssistant() const { defaultAssistant } = useDefaultAssistant()
const { showTopics, toggleShowTopics } = useShowTopics() const { showTopics, toggleShowTopics } = useShowTopics()
const { t } = useTranslation() const { t } = useTranslation()
const borderStyle = '0.5px solid var(--color-border)'
const border =
position === 'left' ? { borderRight: borderStyle } : { borderLeft: borderStyle, borderTopLeftRadius: 0 }
if (position === 'left' && topicPosition === 'left') {
_tab = tab
}
const showTab = !(position === 'left' && topicPosition === 'right') const showTab = !(position === 'left' && topicPosition === 'right')
const assistantTab = { const assistantTab = {
@ -72,68 +62,38 @@ const HomeTabs: FC<Props> = ({
setActiveAssistant(assistant) setActiveAssistant(assistant)
} }
useEffect(() => { // useEffect(() => {
const unsubscribes = [ // const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SHOW_ASSISTANTS, (): any => { // EventEmitter.on(EVENT_NAMES.SHOW_ASSISTANTS, (): any => {
showTab && setTab('assistants') // showTab && setTab('assistants')
}), // }),
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => { // EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => {
showTab && setTab('topic') // showTab && setTab('topic')
}), // }),
EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => { // EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => {
showTab && setTab('settings') // showTab && setTab('settings')
}), // }),
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => { // EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => {
showTab && setTab('topic') // showTab && setTab('topic')
if (position === 'left' && topicPosition === 'right') { // if (position === 'left' && topicPosition === 'right') {
toggleShowTopics() // toggleShowTopics()
} // }
}) // })
] // ]
return () => unsubscribes.forEach((unsub) => unsub()) // return () => unsubscribes.forEach((unsub) => unsub())
}, [position, showTab, tab, toggleShowTopics, topicPosition]) // }, [position, showTab, tab, toggleShowTopics, topicPosition])
useEffect(() => { // useEffect(() => {
if (position === 'right' && topicPosition === 'right' && tab === 'assistants') { // if (position === 'right' && topicPosition === 'right' && tab === 'assistants') {
setTab('topic') // setTab('topic')
} // }
if (position === 'left' && topicPosition === 'right' && forceToSeeAllTab != true && tab !== 'assistants') { // if (position === 'left' && topicPosition === 'right' && forceToSeeAllTab != true && tab !== 'assistants') {
setTab('assistants') // setTab('assistants')
} // }
}, [position, tab, topicPosition, forceToSeeAllTab]) // }, [position, tab, topicPosition, forceToSeeAllTab])
return ( return (
<Container style={{ ...border, ...style }} className="home-tabs"> <Container style={{ ...style }} className="home-tabs">
{(showTab || (forceToSeeAllTab == true && !showTopics)) && (
<>
<Segmented
value={tab}
style={{ borderRadius: 50 }}
shape="round"
options={
[
(position === 'left' && topicPosition === 'left') || (forceToSeeAllTab == true && position === 'left')
? assistantTab
: undefined,
{
label: t('common.topics'),
value: 'topic'
// icon: <MessageSquareQuote size={16} />
},
{
label: t('settings.title'),
value: 'settings'
// icon: <SettingsIcon size={16} />
}
].filter(Boolean) as SegmentedProps['options']
}
onChange={(value) => setTab(value as 'topic' | 'settings')}
block
/>
<Divider />
</>
)}
<TabContent className="home-tabs-content"> <TabContent className="home-tabs-content">
{tab === 'assistants' && ( {tab === 'assistants' && (
<Assistants <Assistants
@ -154,6 +114,7 @@ const HomeTabs: FC<Props> = ({
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
max-width: var(--assistants-width); max-width: var(--assistants-width);
min-width: var(--assistants-width); min-width: var(--assistants-width);

View File

@ -1,4 +1,5 @@
import { CopyOutlined, DeleteOutlined, EditOutlined, RedoOutlined } from '@ant-design/icons' import { CopyOutlined, DeleteOutlined, EditOutlined, RedoOutlined } from '@ant-design/icons'
import { NavbarIcon } from '@renderer/components/app/MainNavbar'
import CustomTag from '@renderer/components/CustomTag' import CustomTag from '@renderer/components/CustomTag'
import Ellipsis from '@renderer/components/Ellipsis' import Ellipsis from '@renderer/components/Ellipsis'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
@ -22,7 +23,6 @@ import styled from 'styled-components'
import CustomCollapse from '../../components/CustomCollapse' import CustomCollapse from '../../components/CustomCollapse'
import FileItem from '../files/FileItem' import FileItem from '../files/FileItem'
import { NavbarIcon } from '../home/Navbar'
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup' import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup' import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup'
import StatusIcon from './components/StatusIcon' import StatusIcon from './components/StatusIcon'

View File

@ -1,5 +1,5 @@
import { DeleteOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons' import { DeleteOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import DragableList from '@renderer/components/DragableList' import DragableList from '@renderer/components/DragableList'
import ListItem from '@renderer/components/ListItem' import ListItem from '@renderer/components/ListItem'
import PromptPopup from '@renderer/components/Popups/PromptPopup' import PromptPopup from '@renderer/components/Popups/PromptPopup'
@ -92,9 +92,9 @@ const KnowledgePage: FC = () => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter>
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<SideNav> <SideNav>
<ScrollContainer> <ScrollContainer>

View File

@ -1,5 +1,6 @@
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons' import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
import { Center, VStack } from '@renderer/components/Layout' import { Center, VStack } from '@renderer/components/Layout'
import { SettingDescription, SettingRow, SettingSubtitle } from '@renderer/pages/settings'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp' import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp'
import { Alert, Button } from 'antd' import { Alert, Button } from 'antd'
@ -8,8 +9,6 @@ import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
interface Props { interface Props {
mini?: boolean mini?: boolean
} }
@ -82,7 +81,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />} icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
className="nodrag" className="nodrag"
color={installed ? 'green' : 'danger'} color={installed ? 'green' : 'danger'}
onClick={() => navigate('/settings/mcp/mcp-install')} onClick={() => navigate('/mcp-servers/mcp-install')}
/> />
) )
} }

View File

@ -3,6 +3,7 @@ import { nanoid } from '@reduxjs/toolkit'
import DragableList from '@renderer/components/DragableList' import DragableList from '@renderer/components/DragableList'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { SettingTitle } from '@renderer/pages/settings'
import { MCPServer } from '@renderer/types' import { MCPServer } from '@renderer/types'
import { formatMcpError } from '@renderer/utils/error' import { formatMcpError } from '@renderer/utils/error'
import { Button, Dropdown, Empty, Switch, Tag } from 'antd' import { Button, Dropdown, Empty, Switch, Tag } from 'antd'
@ -12,7 +13,6 @@ import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingTitle } from '..'
import AddMcpServerModal from './AddMcpServerModal' import AddMcpServerModal from './AddMcpServerModal'
import EditMcpJsonPopup from './EditMcpJsonPopup' import EditMcpJsonPopup from './EditMcpJsonPopup'
import SyncServersPopup from './SyncServersPopup' import SyncServersPopup from './SyncServersPopup'
@ -36,7 +36,7 @@ const McpServersList: FC = () => {
isActive: false isActive: false
} }
addMCPServer(newServer) addMCPServer(newServer)
navigate(`/settings/mcp/settings`, { state: { server: newServer } }) navigate(`/mcp-servers/settings`, { state: { server: newServer } })
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' }) window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
}, [addMCPServer, navigate, t]) }, [addMCPServer, navigate, t])
@ -50,7 +50,7 @@ const McpServersList: FC = () => {
setIsAddModalVisible(false) setIsAddModalVisible(false)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-quick-add' }) window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-quick-add' })
// Optionally navigate to the new server's settings page // Optionally navigate to the new server's settings page
// navigate(`/settings/mcp/settings`, { state: { server } }) // navigate(`/mcp-servers/settings`, { state: { server } })
}, },
[addMCPServer, t] [addMCPServer, t]
) )
@ -119,7 +119,7 @@ const McpServersList: FC = () => {
</ListHeader> </ListHeader>
<DragableList style={{ width: '100%' }} list={mcpServers} onUpdate={updateMcpServers}> <DragableList style={{ width: '100%' }} list={mcpServers} onUpdate={updateMcpServers}>
{(server: MCPServer) => ( {(server: MCPServer) => (
<ServerCard key={server.id} onClick={() => navigate(`/settings/mcp/settings`, { state: { server } })}> <ServerCard key={server.id} onClick={() => navigate(`/mcp-servers/settings`, { state: { server } })}>
<ServerHeader> <ServerHeader>
<ServerName> <ServerName>
{server.logoUrl && <ServerLogo src={server.logoUrl} alt={`${server.name} logo`} />} {server.logoUrl && <ServerLogo src={server.logoUrl} alt={`${server.name} logo`} />}
@ -148,7 +148,7 @@ const McpServersList: FC = () => {
<Button <Button
icon={<Settings2 size={16} />} icon={<Settings2 size={16} />}
type="text" type="text"
onClick={() => navigate(`/settings/mcp/settings`, { state: { server } })} onClick={() => navigate(`/mcp-servers/settings`, { state: { server } })}
/> />
</StatusIndicator> </StatusIndicator>
</ServerHeader> </ServerHeader>

View File

@ -1,7 +1,8 @@
import { DeleteOutlined, SaveOutlined } from '@ant-design/icons' import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers'
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription' import MCPDescription from '@renderer/pages/mcp-servers/McpDescription'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '@renderer/pages/settings'
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types' import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
import { formatMcpError } from '@renderer/utils/error' import { formatMcpError } from '@renderer/utils/error'
import { Button, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd' import { Button, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd'
@ -12,7 +13,6 @@ import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router' import { useLocation, useNavigate } from 'react-router'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
import MCPPromptsSection from './McpPrompt' import MCPPromptsSection from './McpPrompt'
import MCPResourcesSection from './McpResource' import MCPResourcesSection from './McpResource'
import MCPToolsSection from './McpTool' import MCPToolsSection from './McpTool'
@ -319,7 +319,7 @@ const McpSettings: React.FC = () => {
await window.api.mcp.removeServer(server) await window.api.mcp.removeServer(server)
deleteMCPServer(server.id) deleteMCPServer(server.id)
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' }) window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
navigate('/settings/mcp') navigate('/mcp-servers')
} }
}) })
} catch (error: any) { } catch (error: any) {
@ -608,7 +608,7 @@ const McpSettings: React.FC = () => {
} }
return ( return (
<SettingContainer theme={theme} style={{ width: '100%', paddingTop: 55, backgroundColor: 'transparent' }}> <SettingContainer theme={theme} style={{ width: '100%', paddingTop: 20, backgroundColor: 'transparent' }}>
<SettingGroup style={{ marginBottom: 0, borderRadius: 'var(--list-item-border-radius)' }}> <SettingGroup style={{ marginBottom: 0, borderRadius: 'var(--list-item-border-radius)' }}>
<SettingTitle> <SettingTitle>
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}> <Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>

View File

@ -79,7 +79,7 @@ export const McpSettingsNavbar = () => {
<Button <Button
size="small" size="small"
type="text" type="text"
onClick={() => navigate('/settings/mcp/npx-search')} onClick={() => navigate('/mcp-servers/npx-search')}
icon={<Search size={14} />} icon={<Search size={14} />}
className="nodrag" className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20 }}> style={{ fontSize: 13, height: 28, borderRadius: 20 }}>

View File

@ -1,35 +1,46 @@
import { ArrowLeftOutlined } from '@ant-design/icons' import { ArrowLeftOutlined } from '@ant-design/icons'
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { SettingContainer } from '@renderer/pages/settings'
import { Button } from 'antd' import { Button } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Route, Routes, useLocation } from 'react-router' import { Route, Routes, useLocation } from 'react-router'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer } from '..'
import InstallNpxUv from './InstallNpxUv' import InstallNpxUv from './InstallNpxUv'
import McpServersList from './McpServersList' import McpServersList from './McpServersList'
import McpSettings from './McpSettings' import McpSettings from './McpSettings'
import { McpSettingsNavbar } from './McpSettingsNavbar'
import NpxSearch from './NpxSearch' import NpxSearch from './NpxSearch'
const MCPSettings: FC = () => { const McpServersPage: FC = () => {
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation()
const location = useLocation() const location = useLocation()
const pathname = location.pathname const pathname = location.pathname
const isHome = pathname === '/settings/mcp' const isHome = pathname === '/mcp-servers'
return ( return (
<SettingContainer theme={theme} style={{ padding: 0, position: 'relative' }}> <Container theme={theme} style={{ padding: 0, position: 'relative' }}>
{!isHome && ( <NavbarMain>
<BackButtonContainer> <NavbarCenter style={{ borderRight: 'none' }}>
<Link to="/settings/mcp"> <HStack alignItems="center" gap={10}>
<Button type="default" icon={<ArrowLeftOutlined />} shape="circle" /> {t('common.mcp')}
</Link> {!isHome && (
</BackButtonContainer> <Link to="/mcp-servers">
)} <Button type="default" icon={<ArrowLeftOutlined />} shape="circle" size="small" className="nodrag" />
<MainContainer> </Link>
)}
</HStack>
</NavbarCenter>
{pathname.includes('/mcp-servers') && <McpSettingsNavbar />}
</NavbarMain>
<MainContainer id="content-container">
<Routes> <Routes>
<Route path="/" element={<McpServersList />} /> <Route path="/" element={<McpServersList />} />
<Route path="settings" element={<McpSettings />} /> <Route path="settings" element={<McpSettings />} />
@ -51,20 +62,14 @@ const MCPSettings: FC = () => {
/> />
</Routes> </Routes>
</MainContainer> </MainContainer>
</SettingContainer> </Container>
) )
} }
const BackButtonContainer = styled.div` const Container = styled.div`
display: flex; display: flex;
align-items: center; flex-direction: column;
padding: 10px 20px; flex: 1;
background-color: transparent;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1000;
` `
const MainContainer = styled.div` const MainContainer = styled.div`
@ -73,4 +78,4 @@ const MainContainer = styled.div`
width: 100%; width: 100%;
` `
export default MCPSettings export default McpServersPage

View File

@ -1,6 +1,6 @@
import { PlusOutlined, RedoOutlined } from '@ant-design/icons' import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg' import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import TranslateButton from '@renderer/components/TranslateButton' import TranslateButton from '@renderer/components/TranslateButton'
@ -785,7 +785,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
{isMac && ( {isMac && (
<NavbarRight style={{ justifyContent: 'flex-end' }}> <NavbarRight style={{ justifyContent: 'flex-end' }}>
@ -794,7 +794,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
</Button> </Button>
</NavbarRight> </NavbarRight>
)} )}
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<LeftContainer> <LeftContainer>
<ProviderTitleContainer> <ProviderTitleContainer>

View File

@ -1,6 +1,6 @@
import { PlusOutlined, RedoOutlined } from '@ant-design/icons' import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
import DMXAPIToImg from '@renderer/assets/images/providers/DMXAPI-to-img.webp' import DMXAPIToImg from '@renderer/assets/images/providers/DMXAPI-to-img.webp'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain, NavbarRight } from '@renderer/components/app/Navbar'
import { VStack } from '@renderer/components/Layout' import { VStack } from '@renderer/components/Layout'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
@ -645,7 +645,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
{isMac && ( {isMac && (
<NavbarRight style={{ justifyContent: 'flex-end' }}> <NavbarRight style={{ justifyContent: 'flex-end' }}>
@ -658,7 +658,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
</Button> </Button>
</NavbarRight> </NavbarRight>
)} )}
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<LeftContainer> <LeftContainer>
<ProviderTitleContainer> <ProviderTitleContainer>

View File

@ -5,7 +5,7 @@ import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg'
import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg' import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg'
import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg' import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg'
import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg' import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack, VStack } from '@renderer/components/Layout' import { HStack, VStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import TranslateButton from '@renderer/components/TranslateButton' import TranslateButton from '@renderer/components/TranslateButton'
@ -354,7 +354,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
{isMac && ( {isMac && (
<NavbarRight style={{ justifyContent: 'flex-end' }}> <NavbarRight style={{ justifyContent: 'flex-end' }}>
@ -367,7 +367,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
</Button> </Button>
</NavbarRight> </NavbarRight>
)} )}
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<LeftContainer> <LeftContainer>
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle> <SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>

View File

@ -1,5 +1,5 @@
import { PlusOutlined } from '@ant-design/icons' import { PlusOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain, NavbarRight } from '@renderer/components/app/Navbar'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import TranslateButton from '@renderer/components/TranslateButton' import TranslateButton from '@renderer/components/TranslateButton'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
@ -367,7 +367,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
{isMac && ( {isMac && (
<NavbarRight style={{ justifyContent: 'flex-end' }}> <NavbarRight style={{ justifyContent: 'flex-end' }}>
@ -376,7 +376,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
</Button> </Button>
</NavbarRight> </NavbarRight>
)} )}
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<LeftContainer> <LeftContainer>
{/* Provider Section */} {/* Provider Section */}

View File

@ -1,4 +1,4 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' 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 { import {
Cloud, Cloud,
@ -10,11 +10,9 @@ import {
Package, Package,
Rocket, Rocket,
Settings2, Settings2,
SquareTerminal,
TextCursorInput, TextCursorInput,
Zap Zap
} from 'lucide-react' } from 'lucide-react'
// 导入useAppSelector
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link, Route, Routes, useLocation } from 'react-router-dom' import { Link, Route, Routes, useLocation } from 'react-router-dom'
@ -24,8 +22,6 @@ import AboutSettings from './AboutSettings'
import DataSettings from './DataSettings/DataSettings' import DataSettings from './DataSettings/DataSettings'
import DisplaySettings from './DisplaySettings/DisplaySettings' import DisplaySettings from './DisplaySettings/DisplaySettings'
import GeneralSettings from './GeneralSettings' import GeneralSettings from './GeneralSettings'
import MCPSettings from './MCPSettings'
import { McpSettingsNavbar } from './MCPSettings/McpSettingsNavbar'
import ProvidersList from './ProviderSettings' import ProvidersList from './ProviderSettings'
import QuickAssistantSettings from './QuickAssistantSettings' import QuickAssistantSettings from './QuickAssistantSettings'
import QuickPhraseSettings from './QuickPhraseSettings' import QuickPhraseSettings from './QuickPhraseSettings'
@ -41,10 +37,9 @@ const SettingsPage: FC = () => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
{pathname.includes('/settings/mcp') && <McpSettingsNavbar />} </NavbarMain>
</Navbar>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<SettingMenus> <SettingMenus>
<MenuItemLink to="/settings/provider"> <MenuItemLink to="/settings/provider">
@ -65,12 +60,6 @@ const SettingsPage: FC = () => {
{t('settings.websearch.title')} {t('settings.websearch.title')}
</MenuItem> </MenuItem>
</MenuItemLink> </MenuItemLink>
<MenuItemLink to="/settings/mcp">
<MenuItem className={isRoute('/settings/mcp')}>
<SquareTerminal size={18} />
{t('settings.mcp.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/general"> <MenuItemLink to="/settings/general">
<MenuItem className={isRoute('/settings/general')}> <MenuItem className={isRoute('/settings/general')}>
<Settings2 size={18} /> <Settings2 size={18} />
@ -125,7 +114,6 @@ const SettingsPage: FC = () => {
<Route path="provider" element={<ProvidersList />} /> <Route path="provider" element={<ProvidersList />} />
<Route path="model" element={<ModelSettings />} /> <Route path="model" element={<ModelSettings />} />
<Route path="web-search" element={<WebSearchSettings />} /> <Route path="web-search" element={<WebSearchSettings />} />
<Route path="mcp/*" element={<MCPSettings />} />
<Route path="general" element={<GeneralSettings />} /> <Route path="general" element={<GeneralSettings />} />
<Route path="display" element={<DisplaySettings />} /> <Route path="display" element={<DisplaySettings />} />
<Route path="shortcut" element={<ShortcutSettings />} /> <Route path="shortcut" element={<ShortcutSettings />} />

View File

@ -1,5 +1,5 @@
import { CheckOutlined, DeleteOutlined, HistoryOutlined, SendOutlined } from '@ant-design/icons' import { CheckOutlined, DeleteOutlined, HistoryOutlined, SendOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import CopyIcon from '@renderer/components/Icons/CopyIcon' import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { isEmbeddingModel } from '@renderer/config/models' import { isEmbeddingModel } from '@renderer/config/models'
@ -431,8 +431,8 @@ const TranslatePage: FC = () => {
return ( return (
<Container id="translate-page"> <Container id="translate-page">
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none', gap: 10 }}> <NavbarCenter>
{t('translate.title')} {t('translate.title')}
<Button <Button
className="nodrag" className="nodrag"
@ -443,7 +443,7 @@ const TranslatePage: FC = () => {
onClick={() => setHistoryDrawerVisible(!historyDrawerVisible)} onClick={() => setHistoryDrawerVisible(!historyDrawerVisible)}
/> />
</NavbarCenter> </NavbarCenter>
</Navbar> </NavbarMain>
<ContentContainer id="content-container" ref={contentContainerRef} $historyDrawerVisible={historyDrawerVisible}> <ContentContainer id="content-container" ref={contentContainerRef} $historyDrawerVisible={historyDrawerVisible}>
<HistoryContainner $historyDrawerVisible={historyDrawerVisible}> <HistoryContainner $historyDrawerVisible={historyDrawerVisible}>
<OperationBar> <OperationBar>
@ -606,7 +606,7 @@ const InputContainer = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
border: 1px solid var(--color-border-soft); border: 0.5px solid var(--color-border);
border-radius: 10px; border-radius: 10px;
padding-bottom: 5px; padding-bottom: 5px;
padding-right: 2px; padding-right: 2px;
@ -675,7 +675,7 @@ const HistoryContainner = styled.div<{ $historyDrawerVisible: boolean }>`
transition: transition:
width 0.2s, width 0.2s,
opacity 0.2s; opacity 0.2s;
border: 1px solid var(--color-border-soft); border: 0.5px solid var(--color-border);
border-radius: 10px; border-radius: 10px;
margin-right: 15px; margin-right: 15px;
display: flex; display: flex;

View File

@ -1,12 +1,13 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { AppLogo, UserAvatar } from '@renderer/config/env' import { AppLogo, UserAvatar } from '@renderer/config/env'
import type { MinAppType, Topic } from '@renderer/types' import type { Assistant, MinAppType, Topic } from '@renderer/types'
import type { UpdateInfo } from 'builder-util-runtime' import type { UpdateInfo } from 'builder-util-runtime'
export interface ChatState { export interface ChatState {
isMultiSelectMode: boolean isMultiSelectMode: boolean
selectedMessageIds: string[] selectedMessageIds: string[]
activeTopic: Topic | null activeTopic: Topic | null
activeAssistant: Assistant | null
} }
export interface UpdateState { export interface UpdateState {
@ -65,7 +66,8 @@ const initialState: RuntimeState = {
chat: { chat: {
isMultiSelectMode: false, isMultiSelectMode: false,
selectedMessageIds: [], selectedMessageIds: [],
activeTopic: null activeTopic: null,
activeAssistant: null
} }
} }
@ -118,6 +120,9 @@ const runtimeSlice = createSlice({
}, },
setActiveTopic: (state, action: PayloadAction<Topic>) => { setActiveTopic: (state, action: PayloadAction<Topic>) => {
state.chat.activeTopic = action.payload state.chat.activeTopic = action.payload
},
setActiveAssistant: (state, action: PayloadAction<Assistant>) => {
state.chat.activeAssistant = action.payload
} }
} }
}) })
@ -137,7 +142,8 @@ export const {
// Chat related actions // Chat related actions
toggleMultiSelectMode, toggleMultiSelectMode,
setSelectedMessageIds, setSelectedMessageIds,
setActiveTopic setActiveTopic,
setActiveAssistant
} = runtimeSlice.actions } = runtimeSlice.actions
export default runtimeSlice.reducer export default runtimeSlice.reducer