diff --git a/src/main/config.ts b/src/main/config.ts
index 40e4ac2e90..c676823b89 100644
--- a/src/main/config.ts
+++ b/src/main/config.ts
@@ -10,13 +10,13 @@ if (isDev) {
export const DATA_PATH = getDataPath()
export const titleBarOverlayDark = {
- height: 40,
+ height: 42,
color: 'rgba(255,255,255,0)',
symbolColor: '#fff'
}
export const titleBarOverlayLight = {
- height: 40,
+ height: 42,
color: 'rgba(255,255,255,0)',
symbolColor: '#000'
}
diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts
index 2b81bc8050..c9912b9d04 100644
--- a/src/main/services/WindowService.ts
+++ b/src/main/services/WindowService.ts
@@ -74,7 +74,7 @@ export class WindowService {
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors,
- trafficLightPosition: { x: 8, y: 12 },
+ trafficLightPosition: { x: 8, y: 13 },
...(isLinux ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
@@ -343,7 +343,9 @@ export class WindowService {
* mac: 任何情况都会到这里,因此需要单独处理mac
*/
- event.preventDefault()
+ if (!mainWindow.isFullScreen()) {
+ event.preventDefault()
+ }
mainWindow.hide()
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index 4c02234305..ad18a9b193 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -3,25 +3,15 @@ import '@renderer/databases'
import { loggerService } from '@logger'
import store, { persistor } from '@renderer/store'
import { Provider } from 'react-redux'
-import { HashRouter, Route, Routes } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
-import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider'
import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider'
-import NavigationHandler from './handler/NavigationHandler'
-import AgentsPage from './pages/agents/AgentsPage'
-import AppsPage from './pages/apps/AppsPage'
-import FilesPage from './pages/files/FilesPage'
-import HomePage from './pages/home/HomePage'
-import KnowledgePage from './pages/knowledge/KnowledgePage'
-import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
-import SettingsPage from './pages/settings/SettingsPage'
-import TranslatePage from './pages/translate/TranslatePage'
+import Router from './Router'
const logger = loggerService.withContext('App.tsx')
@@ -37,20 +27,7 @@ function App(): React.ReactElement {
-
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
+
diff --git a/src/renderer/src/Router.tsx b/src/renderer/src/Router.tsx
new file mode 100644
index 0000000000..624c6ccc47
--- /dev/null
+++ b/src/renderer/src/Router.tsx
@@ -0,0 +1,57 @@
+import '@renderer/databases'
+
+import { FC, useMemo } from 'react'
+import { HashRouter, Route, Routes } from 'react-router-dom'
+
+import Sidebar from './components/app/Sidebar'
+import TabsContainer from './components/Tab/TabContainer'
+import NavigationHandler from './handler/NavigationHandler'
+import { useNavbarPosition } from './hooks/useSettings'
+import AgentsPage from './pages/agents/AgentsPage'
+import FilesPage from './pages/files/FilesPage'
+import HomePage from './pages/home/HomePage'
+import KnowledgePage from './pages/knowledge/KnowledgePage'
+import LaunchpadPage from './pages/launchpad/LaunchpadPage'
+import MinAppsPage from './pages/minapps/MinAppsPage'
+import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
+import SettingsPage from './pages/settings/SettingsPage'
+import TranslatePage from './pages/translate/TranslatePage'
+
+const Router: FC = () => {
+ const { navbarPosition } = useNavbarPosition()
+
+ const routes = useMemo(() => {
+ return (
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ )
+ }, [])
+
+ if (navbarPosition === 'left') {
+ return (
+
+
+ {routes}
+
+
+ )
+ }
+
+ return (
+
+
+ {routes}
+
+ )
+}
+
+export default Router
diff --git a/src/renderer/src/assets/styles/ant.scss b/src/renderer/src/assets/styles/ant.scss
index efd8fe3de2..d89e3d1d39 100644
--- a/src/renderer/src/assets/styles/ant.scss
+++ b/src/renderer/src/assets/styles/ant.scss
@@ -25,7 +25,18 @@
}
.minapp-drawer {
- max-width: calc(100vw - var(--sidebar-width));
+ [navbar-position='left'] & {
+ max-width: calc(100vw - var(--sidebar-width));
+ .ant-drawer-header {
+ width: calc(100vw - var(--sidebar-width));
+ }
+ }
+ [navbar-position='top'] & {
+ max-width: 100vw;
+ .ant-drawer-header {
+ width: 100vw;
+ }
+ }
.ant-drawer-content-wrapper {
box-shadow: none;
}
@@ -33,7 +44,6 @@
position: absolute;
-webkit-app-region: drag;
min-height: calc(var(--navbar-height) + 0.5px);
- width: calc(100vw - var(--sidebar-width));
margin-top: -0.5px;
border-bottom: none;
}
@@ -69,6 +79,7 @@
background-color: var(--ant-color-bg-elevated);
overflow: hidden;
border-radius: var(--ant-border-radius-lg);
+ user-select: none;
.ant-dropdown-menu {
max-height: 50vh;
overflow-y: auto;
diff --git a/src/renderer/src/assets/styles/color.scss b/src/renderer/src/assets/styles/color.scss
index 224566e199..b0549dd8e6 100644
--- a/src/renderer/src/assets/styles/color.scss
+++ b/src/renderer/src/assets/styles/color.scss
@@ -44,8 +44,8 @@
--color-reference-text: #ffffff;
--color-reference-background: #0b0e12;
- --color-list-item: #252525;
- --color-list-item-hover: #1e1e1e;
+ --color-list-item: rgba(255, 255, 255, 0.1);
+ --color-list-item-hover: rgba(255, 255, 255, 0.05);
--modal-background: #111111;
@@ -56,7 +56,7 @@
--navbar-background-mac: rgba(20, 20, 20, 0.55);
--navbar-background: #1f1f1f;
- --navbar-height: 40px;
+ --navbar-height: 44px;
--sidebar-width: 50px;
--status-bar-height: 40px;
--input-bar-height: 100px;
@@ -71,7 +71,7 @@
--chat-background-assistant: transparent;
--chat-text-user: var(--color-black);
- --list-item-border-radius: 20px;
+ --list-item-border-radius: 10px;
--color-status-success: #52c41a;
--color-status-error: #ff4d4f;
@@ -98,7 +98,7 @@
--color-background: var(--color-white);
--color-background-soft: var(--color-white-soft);
--color-background-mute: var(--color-white-mute);
- --color-background-opacity: rgba(235, 235, 235, 0.7);
+ --color-background-opacity: rgba(243, 243, 243, 1);
--inner-glow-opacity: 0.1;
--color-primary: #00b96b;
@@ -124,8 +124,8 @@
--color-reference-text: #000000;
--color-reference-background: #f1f7ff;
- --color-list-item: #eee;
- --color-list-item-hover: #f5f5f5;
+ --color-list-item: #fff;
+ --color-list-item-hover: #fafafa;
--modal-background: var(--color-white);
@@ -141,3 +141,18 @@
--chat-background-assistant: transparent;
--chat-text-user: var(--color-text);
}
+
+[navbar-position='left'] {
+ --navbar-height: 42px;
+ --list-item-border-radius: 20px;
+}
+
+[navbar-position='left'][theme-mode='light'] {
+ --color-list-item: #eee;
+ --color-list-item-hover: #f5f5f5;
+}
+
+[navbar-position='left'][theme-mode='dark'] {
+ --color-list-item: #252525;
+ --color-list-item-hover: #1e1e1e;
+}
diff --git a/src/renderer/src/assets/styles/container.scss b/src/renderer/src/assets/styles/container.scss
index 8be4027981..fd2d7f9aec 100644
--- a/src/renderer/src/assets/styles/container.scss
+++ b/src/renderer/src/assets/styles/container.scss
@@ -1,6 +1,11 @@
#content-container {
background-color: var(--color-background);
- border-top: 0.5px solid var(--color-border);
- border-top-left-radius: 10px;
- border-left: 0.5px solid var(--color-border);
+}
+
+[navbar-position='left'] {
+ #content-container {
+ border-top: 0.5px solid var(--color-border);
+ border-top-left-radius: 10px;
+ border-left: 0.5px solid var(--color-border);
+ }
}
diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx
index c7a5a99d52..b1a0d3c422 100644
--- a/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx
+++ b/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx
@@ -203,6 +203,7 @@ const Container = styled.div<{ $isStreaming: boolean }>`
border-radius: 8px;
overflow: hidden;
margin: 10px 0;
+ margin-top: 0;
`
const GeneratingContainer = styled.div`
@@ -233,8 +234,8 @@ const IconWrapper = styled.div<{ $isStreaming: boolean }>`
display: flex;
align-items: center;
justify-content: center;
- width: 40px;
- height: 40px;
+ width: 44px;
+ height: 44px;
background: ${(props) =>
props.$isStreaming
? 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'
@@ -255,10 +256,11 @@ const TitleSection = styled.div`
const Title = styled.h3`
margin: 0 !important;
- font-size: 16px;
+ font-size: 14px !important;
font-weight: 600;
color: var(--color-text);
line-height: 1.4;
+ font-family: 'Ubuntu';
`
const TypeBadge = styled.div`
diff --git a/src/renderer/src/components/DraggableList/virtual-list.tsx b/src/renderer/src/components/DraggableList/virtual-list.tsx
index 78b4c4a697..08234dd96d 100644
--- a/src/renderer/src/components/DraggableList/virtual-list.tsx
+++ b/src/renderer/src/components/DraggableList/virtual-list.tsx
@@ -28,6 +28,7 @@ import { type Key, memo, useCallback, useRef } from 'react'
* @property {T[]} list 渲染的数据源
* @property {(index: number) => Key} [itemKey] 提供给虚拟列表的行 key,若不提供默认使用 index
* @property {number} [overscan=5] 前后额外渲染的行数,提升快速滚动时的体验
+ * @property {React.ReactNode} [header] 列表头部内容
* @property {(item: T, index: number) => React.ReactNode} children 列表项渲染函数
*/
interface DraggableVirtualListProps {
@@ -43,6 +44,7 @@ interface DraggableVirtualListProps {
list: T[]
itemKey?: (index: number) => Key
overscan?: number
+ header?: React.ReactNode
children: (item: T, index: number) => React.ReactNode
}
@@ -66,6 +68,7 @@ function DraggableVirtualList({
list,
itemKey,
overscan = 5,
+ header,
children
}: DraggableVirtualListProps): React.ReactElement {
const _onDragEnd = (result: DropResult, provided: ResponderProvided) => {
@@ -92,6 +95,7 @@ function DraggableVirtualList({
return (
+ {header}
= ({ app, onClick, size = 60, isLast }) => {
+const MinApp: FC = ({ app, onClick, size = 60, isLast }) => {
const { openMinappKeepAlive } = useMinappPopup()
const { t } = useTranslation()
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
+ const { openedKeepAliveMinapps, currentMinappId, minappShow } = useRuntime()
+ const dispatch = useDispatch()
const isPinned = pinned.some((p) => p.id === app.id)
const isVisible = minapps.some((m) => m.id === app.id)
+ const isActive = minappShow && currentMinappId === app.id
+ const isOpened = openedKeepAliveMinapps.some((item) => item.id === app.id)
+ const { isTopNavbar } = useNavbarPosition()
const handleClick = () => {
openMinappKeepAlive(app)
@@ -34,7 +44,13 @@ const App: FC = ({ app, onClick, size = 60, isLast }) => {
const menuItems: MenuProps['items'] = [
{
key: 'togglePin',
- label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'),
+ label: isPinned
+ ? isTopNavbar
+ ? t('minapp.remove_from_launchpad')
+ : t('minapp.remove_from_sidebar')
+ : isTopNavbar
+ ? t('minapp.add_to_launchpad')
+ : t('minapp.add_to_sidebar'),
onClick: () => {
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app]
updatePinnedMinapps(newPinned)
@@ -50,6 +66,9 @@ const App: FC = ({ app, onClick, size = 60, isLast }) => {
updateDisabledMinapps(newDisabled)
const newPinned = pinned.filter((item) => item.id !== app.id)
updatePinnedMinapps(newPinned)
+ // 更新 openedKeepAliveMinapps
+ const newOpenedKeepAliveMinapps = openedKeepAliveMinapps.filter((item) => item.id !== app.id)
+ dispatch(setOpenedKeepAliveMinapps(newOpenedKeepAliveMinapps))
}
},
...(app.type === 'Custom'
@@ -87,7 +106,14 @@ const App: FC = ({ app, onClick, size = 60, isLast }) => {
return (
-
+
+
+ {isOpened && (
+
+
+
+ )}
+
{isLast ? t('settings.miniapps.custom.title') : app.name}
@@ -103,6 +129,22 @@ const Container = styled.div`
overflow: hidden;
`
+const IconContainer = styled.div`
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`
+
+const StyledIndicator = styled.div`
+ position: absolute;
+ bottom: -2px;
+ right: -2px;
+ padding: 2px;
+ background: var(--color-background);
+ border-radius: 50%;
+`
+
const AppTitle = styled.div`
font-size: 12px;
margin-top: 5px;
@@ -112,4 +154,4 @@ const AppTitle = styled.div`
white-space: nowrap;
`
-export default App
+export default MinApp
diff --git a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx
index b39f4964ba..45cb25c488 100644
--- a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx
+++ b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx
@@ -18,7 +18,7 @@ import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { useRuntime } from '@renderer/hooks/useRuntime'
-import { useSettings } from '@renderer/hooks/useSettings'
+import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setMinappsOpenLinkExternal } from '@renderer/store/settings'
import { MinAppType } from '@renderer/types'
@@ -143,6 +143,7 @@ const MinappPopupContainer: React.FC = () => {
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
const backgroundColor = useNavBackgroundColor()
+ const { isTopNavbar } = useNavbarPosition()
const dispatch = useAppDispatch()
/** control the drawer open or close */
@@ -164,6 +165,8 @@ const MinappPopupContainer: React.FC = () => {
/** whether the minapps open link external is enabled */
const { minappsOpenLinkExternal } = useSettings()
+ const { isLeftNavbar } = useNavbarPosition()
+
const isInDevelopment = process.env.NODE_ENV === 'development'
useBridge()
@@ -420,7 +423,15 @@ const MinappPopupContainer: React.FC = () => {
{appInfo.canPinned && (
handleTogglePin(appInfo.id)} className={appInfo.isPinned ? 'pinned' : ''}>
@@ -495,7 +506,7 @@ const MinappPopupContainer: React.FC = () => {
maskClosable={false}
closeIcon={null}
style={{
- marginLeft: 'var(--sidebar-width)',
+ marginLeft: isLeftNavbar ? 'var(--sidebar-width)' : 0,
backgroundColor: window.root.style.background
}}>
{/* 在所有小程序中显示GoogleLoginTip */}
@@ -519,7 +530,6 @@ const TitleContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
- padding-left: ${isMac ? '20px' : '10px'};
padding-right: 10px;
position: absolute;
top: 0;
@@ -527,6 +537,13 @@ const TitleContainer = styled.div`
right: 0;
bottom: 0;
background-color: transparent;
+ [navbar-position='left'] & {
+ padding-left: ${isMac ? '20px' : '10px'};
+ }
+ [navbar-position='top'] & {
+ padding-left: ${isMac ? '80px' : '10px'};
+ border-bottom: 0.5px solid var(--color-border);
+ }
`
const TitleText = styled.div`
diff --git a/src/renderer/src/components/MinApp/WebviewContainer.tsx b/src/renderer/src/components/MinApp/WebviewContainer.tsx
index 507de765af..844f5c8e43 100644
--- a/src/renderer/src/components/MinApp/WebviewContainer.tsx
+++ b/src/renderer/src/components/MinApp/WebviewContainer.tsx
@@ -1,4 +1,4 @@
-import { useSettings } from '@renderer/hooks/useSettings'
+import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import { WebviewTag } from 'electron'
import { memo, useEffect, useRef } from 'react'
@@ -23,6 +23,7 @@ const WebviewContainer = memo(
}) => {
const webviewRef = useRef(null)
const { enableSpellCheck } = useSettings()
+ const { isLeftNavbar } = useNavbarPosition()
const setRef = (appid: string) => {
onSetRefCallback(appid, null)
@@ -71,6 +72,13 @@ const WebviewContainer = memo(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appid, url])
+ const WebviewStyle: React.CSSProperties = {
+ width: isLeftNavbar ? 'calc(100vw - var(--sidebar-width))' : '100vw',
+ height: 'calc(100vh - var(--navbar-height))',
+ backgroundColor: 'var(--color-background)',
+ display: 'inline-flex'
+ }
+
return (
void
- activeTopic: Topic
- setActiveTopic: (topic: Topic) => void
- position: 'left' | 'right'
-}
-
-const FloatingSidebar: FC = ({
- children,
- activeAssistant,
- setActiveAssistant,
- activeTopic,
- setActiveTopic,
- position = 'left'
-}) => {
- const [open, setOpen] = useState(false)
-
- useHotkeys('esc', () => {
- setOpen(false)
- })
-
- const [maxHeight, setMaxHeight] = useState(Math.floor(window.innerHeight * 0.75))
-
- useEffect(() => {
- const handleResize = () => {
- setMaxHeight(Math.floor(window.innerHeight * 0.75))
- }
-
- window.addEventListener('resize', handleResize)
-
- return () => {
- window.removeEventListener('resize', handleResize)
- }
- }, [])
-
- const content = (
-
-
-
- )
-
- return (
-
- {children}
-
- )
-}
-
-const PopoverContent = styled.div<{ maxHeight: number }>`
- max-height: ${(props) => props.maxHeight}px;
- &.ant-popover-inner-content {
- overflow-y: hidden;
- }
-`
-
-export default FloatingSidebar
diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx
new file mode 100644
index 0000000000..768391af58
--- /dev/null
+++ b/src/renderer/src/components/Tab/TabContainer.tsx
@@ -0,0 +1,322 @@
+import { PlusOutlined } from '@ant-design/icons'
+import { isLinux, isMac, isWin } from '@renderer/config/constant'
+import { useTheme } from '@renderer/context/ThemeProvider'
+import { useFullscreen } from '@renderer/hooks/useFullscreen'
+import tabsService from '@renderer/services/TabsService'
+import { useAppDispatch, useAppSelector } from '@renderer/store'
+import type { Tab } from '@renderer/store/tabs'
+import { addTab, removeTab, setActiveTab } from '@renderer/store/tabs'
+import { ThemeMode } from '@renderer/types'
+import { classNames } from '@renderer/utils'
+import {
+ FileSearch,
+ Folder,
+ Home,
+ Languages,
+ LayoutGrid,
+ Moon,
+ Palette,
+ Settings,
+ Sparkle,
+ SquareTerminal,
+ Sun,
+ SunMoon,
+ X
+} from 'lucide-react'
+import { useCallback, useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useLocation, useNavigate } from 'react-router-dom'
+import styled from 'styled-components'
+
+import { TopNavbarOpenedMinappTabs } from '../app/PinnedMinapps'
+
+interface TabsContainerProps {
+ children: React.ReactNode
+}
+
+const getTabIcon = (tabId: string): React.ReactNode | undefined => {
+ switch (tabId) {
+ case 'home':
+ return
+ case 'agents':
+ return
+ case 'translate':
+ return
+ case 'paintings':
+ return
+ case 'apps':
+ return
+ case 'knowledge':
+ return
+ case 'mcp':
+ return
+ case 'files':
+ return
+ case 'settings':
+ return
+ default:
+ return null
+ }
+}
+
+let lastSettingsPath = '/settings/provider'
+const specialTabs = ['launchpad', 'settings']
+
+const TabsContainer: React.FC = ({ children }) => {
+ const { t } = useTranslation()
+ const location = useLocation()
+ const navigate = useNavigate()
+ const dispatch = useAppDispatch()
+ const tabs = useAppSelector((state) => state.tabs.tabs)
+ const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
+ const isFullscreen = useFullscreen()
+ const { settedTheme, toggleTheme } = useTheme()
+
+ 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
+ if (path === '/settings') return false
+ return !tabs.some((tab) => tab.id === getTabId(path))
+ }
+
+ const removeSpecialTabs = useCallback(() => {
+ specialTabs.forEach((tabId) => {
+ if (activeTabId !== tabId) {
+ dispatch(removeTab(tabId))
+ }
+ })
+ }, [activeTabId, dispatch])
+
+ 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))
+ }
+
+ // 当访问设置页面时,记录路径
+ if (location.pathname.startsWith('/settings/')) {
+ lastSettingsPath = location.pathname
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [dispatch, location.pathname])
+
+ useEffect(() => {
+ removeSpecialTabs()
+ }, [removeSpecialTabs])
+
+ const closeTab = (tabId: string) => {
+ tabsService.closeTab(tabId)
+ }
+
+ const handleAddTab = () => {
+ navigate('/launchpad')
+ }
+
+ const handleSettingsClick = () => {
+ navigate(lastSettingsPath)
+ }
+
+ const getThemeIcon = () => {
+ switch (settedTheme) {
+ case ThemeMode.dark:
+ return
+ case ThemeMode.light:
+ return
+ case ThemeMode.system:
+ return
+ default:
+ return
+ }
+ }
+
+ return (
+
+
+ {tabs
+ .filter((tab) => !specialTabs.includes(tab.id))
+ .map((tab) => {
+ return (
+ navigate(tab.path)}>
+
+ {tab.id && {getTabIcon(tab.id)}}
+ {t(`title.${tab.id}`)}
+
+ {tab.id !== 'home' && (
+ {
+ e.stopPropagation()
+ closeTab(tab.id)
+ }}>
+
+
+ )}
+
+ )
+ })}
+
+
+
+
+
+ {getThemeIcon()}
+
+
+
+
+
+ {children}
+
+ )
+}
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+`
+
+const TabsBar = styled.div<{ $isFullscreen: boolean }>`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 5px;
+ padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? '75px' : '15px')};
+ padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWin ? '140px' : isLinux ? '120px' : '12px')};
+ -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;
+ padding-right: 8px;
+ background: ${(props) => (props.active ? 'var(--color-list-item)' : 'transparent')};
+ border-radius: var(--list-item-border-radius);
+ cursor: pointer;
+ user-select: none;
+ -webkit-app-region: none;
+ height: 30px;
+ min-width: 90px;
+ transition: background 0.2s;
+ .close-button {
+ opacity: 0;
+ transition: opacity 0.2s;
+ }
+
+ &:hover {
+ background: ${(props) => (props.active ? 'var(--color-list-item)' : 'var(--color-list-item)')};
+ .close-button {
+ opacity: 1;
+ }
+ }
+`
+
+const TabHeader = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 6px;
+`
+
+const TabIcon = styled.span`
+ display: flex;
+ align-items: center;
+ color: var(--color-text-2);
+`
+
+const TabTitle = styled.span`
+ color: var(--color-text);
+ font-size: 13px;
+ display: flex;
+ align-items: center;
+ margin-right: 4px;
+`
+
+const CloseButton = styled.span`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+`
+
+const AddTabButton = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ cursor: pointer;
+ color: var(--color-text-2);
+ -webkit-app-region: none;
+ border-radius: var(--list-item-border-radius);
+ &.active {
+ background: var(--color-list-item);
+ }
+ &:hover {
+ background: var(--color-list-item);
+ }
+`
+
+const RightButtonsContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-left: auto;
+`
+
+const ThemeButton = 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-list-item);
+ border-radius: 8px;
+ }
+`
+
+const SettingsButton = styled.div<{ $active: boolean }>`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ cursor: pointer;
+ color: var(--color-text);
+ -webkit-app-region: none;
+ border-radius: 8px;
+ background: ${(props) => (props.$active ? 'var(--color-list-item)' : 'transparent')};
+ &:hover {
+ background: var(--color-list-item);
+ }
+`
+
+const TabContent = styled.div`
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+ width: calc(100vw - 12px);
+ margin: 6px;
+ margin-top: 0;
+ border-radius: 8px;
+ overflow: hidden;
+`
+
+export default TabsContainer
diff --git a/src/renderer/src/components/TextBadge.tsx b/src/renderer/src/components/TextBadge.tsx
new file mode 100644
index 0000000000..1945810b25
--- /dev/null
+++ b/src/renderer/src/components/TextBadge.tsx
@@ -0,0 +1,22 @@
+import { FC } from 'react'
+import styled from 'styled-components'
+
+interface Props {
+ text: string
+ style?: React.CSSProperties
+}
+
+const TextBadge: FC = ({ text, style }) => {
+ return {text}
+}
+
+const Container = styled.span`
+ font-size: 12px;
+ color: var(--color-primary);
+ background: var(--color-primary-bg);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-weight: 500;
+`
+
+export default TextBadge
diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx
index 9514f200b8..0d0204eb59 100644
--- a/src/renderer/src/components/app/Navbar.tsx
+++ b/src/renderer/src/components/app/Navbar.tsx
@@ -1,6 +1,7 @@
import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
+import { useNavbarPosition } from '@renderer/hooks/useSettings'
import type { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react'
import styled from 'styled-components'
@@ -9,6 +10,11 @@ type Props = PropsWithChildren & HTMLAttributes
export const Navbar: FC = ({ children, ...props }) => {
const backgroundColor = useNavBackgroundColor()
+ const { isTopNavbar } = useNavbarPosition()
+
+ if (isTopNavbar) {
+ return null
+ }
return (
@@ -43,6 +49,10 @@ export const NavbarMain: FC = ({ children, ...props }) => {
)
}
+export const NavbarHeader: FC = ({ children, ...props }) => {
+ return {children}
+}
+
const NavbarContainer = styled.div`
min-width: 100%;
display: flex;
@@ -93,3 +103,14 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
color: var(--color-text-1);
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWin ? '140px' : isLinux ? '120px' : '12px')};
`
+
+const NavbarHeaderContent = styled.div`
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 12px;
+ min-height: var(--navbar-height);
+ max-height: var(--navbar-height);
+`
diff --git a/src/renderer/src/components/app/PinnedMinapps.tsx b/src/renderer/src/components/app/PinnedMinapps.tsx
new file mode 100644
index 0000000000..bd13c4b8ac
--- /dev/null
+++ b/src/renderer/src/components/app/PinnedMinapps.tsx
@@ -0,0 +1,367 @@
+import { useTheme } from '@renderer/context/ThemeProvider'
+import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
+import { useMinapps } from '@renderer/hooks/useMinapps'
+import { useRuntime } from '@renderer/hooks/useRuntime'
+import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
+import type { MenuProps } from 'antd'
+import { Dropdown, Tooltip } from 'antd'
+import { FC, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import styled from 'styled-components'
+
+import { DraggableList } from '../DraggableList'
+import MinAppIcon from '../Icons/MinAppIcon'
+
+/** Tabs of opened minapps in top navbar */
+export const TopNavbarOpenedMinappTabs: FC = () => {
+ const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
+ const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
+ const { showOpenedMinappsInSidebar } = useSettings()
+ const { theme } = useTheme()
+ const { t } = useTranslation()
+ const [keepAliveMinapps, setKeepAliveMinapps] = useState(openedKeepAliveMinapps)
+
+ useEffect(() => {
+ setTimeout(() => setKeepAliveMinapps(openedKeepAliveMinapps), 300)
+ }, [openedKeepAliveMinapps])
+
+ const handleOnClick = (app) => {
+ if (minappShow && currentMinappId === app.id) {
+ hideMinappPopup()
+ } else {
+ openMinappKeepAlive(app)
+ }
+ }
+
+ // 检查是否需要显示已打开小程序组件
+ const isShowOpened = showOpenedMinappsInSidebar && keepAliveMinapps.length > 0
+
+ // 如果不需要显示,返回空容器
+ if (!isShowOpened) return null
+
+ return (
+ 1 ? 'var(--color-list-item)' : 'transparent' }}>
+
+ {keepAliveMinapps.map((app) => {
+ const menuItems: MenuProps['items'] = [
+ {
+ key: 'closeApp',
+ label: t('minapp.sidebar.close.title'),
+ onClick: () => {
+ closeMinapp(app.id)
+ }
+ },
+ {
+ key: 'closeAllApp',
+ label: t('minapp.sidebar.closeall.title'),
+ onClick: () => {
+ closeAllMinapps()
+ }
+ }
+ ]
+ const isActive = minappShow && currentMinappId === app.id
+
+ return (
+
+
+
+ handleOnClick(app)}
+ className={`${isActive ? 'opened-active' : ''}`}>
+
+
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+/** Tabs of opened minapps in sidebar */
+export const SidebarOpenedMinappTabs: FC = () => {
+ const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
+ const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
+ const { showOpenedMinappsInSidebar } = useSettings() // 获取控制显示的设置
+ const { theme } = useTheme()
+ const { t } = useTranslation()
+ const { isLeftNavbar } = useNavbarPosition()
+
+ const handleOnClick = (app) => {
+ if (minappShow && currentMinappId === app.id) {
+ hideMinappPopup()
+ } else {
+ openMinappKeepAlive(app)
+ }
+ }
+
+ // animation for minapp switch indicator
+ useEffect(() => {
+ //hacky way to get the height of the icon
+ const iconDefaultHeight = 40
+ const iconDefaultOffset = 17
+ const container = document.querySelector('.TabsContainer') as HTMLElement
+ const activeIcon = document.querySelector('.TabsContainer .opened-active') as HTMLElement
+
+ let indicatorTop = 0,
+ indicatorRight = 0
+ if (minappShow && activeIcon && container) {
+ indicatorTop = activeIcon.offsetTop + activeIcon.offsetHeight / 2 - 4 // 4 is half of the indicator's height (8px)
+ indicatorRight = 0
+ } else {
+ indicatorTop =
+ ((openedKeepAliveMinapps.length > 0 ? openedKeepAliveMinapps.length : 1) / 2) * iconDefaultHeight +
+ iconDefaultOffset -
+ 4
+ indicatorRight = -50
+ }
+ container.style.setProperty('--indicator-top', `${indicatorTop}px`)
+ container.style.setProperty('--indicator-right', `${indicatorRight}px`)
+ }, [currentMinappId, openedKeepAliveMinapps, minappShow])
+
+ // 检查是否需要显示已打开小程序组件
+ const isShowOpened = showOpenedMinappsInSidebar && openedKeepAliveMinapps.length > 0
+
+ // 如果不需要显示,返回空容器保持动画效果但不显示内容
+ if (!isShowOpened) return
+
+ return (
+
+ {isLeftNavbar && }
+
+
+ {openedKeepAliveMinapps.map((app) => {
+ const menuItems: MenuProps['items'] = [
+ {
+ key: 'closeApp',
+ label: t('minapp.sidebar.close.title'),
+ onClick: () => {
+ closeMinapp(app.id)
+ }
+ },
+ {
+ key: 'closeAllApp',
+ label: t('minapp.sidebar.closeall.title'),
+ onClick: () => {
+ closeAllMinapps()
+ }
+ }
+ ]
+ const isActive = minappShow && currentMinappId === app.id
+
+ return (
+
+
+
+ handleOnClick(app)}
+ className={`${isActive ? 'opened-active' : ''}`}>
+
+
+
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+export const SidebarPinnedApps: FC = () => {
+ const { pinned, updatePinnedMinapps } = useMinapps()
+ const { t } = useTranslation()
+ const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
+ const { theme } = useTheme()
+ const { openMinappKeepAlive } = useMinappPopup()
+ const { isTopNavbar } = useNavbarPosition()
+
+ return (
+
+ {(app) => {
+ const menuItems: MenuProps['items'] = [
+ {
+ key: 'togglePin',
+ label: isTopNavbar ? t('minapp.remove_from_launchpad') : t('minapp.remove_from_sidebar'),
+ onClick: () => {
+ const newPinned = pinned.filter((item) => item.id !== app.id)
+ updatePinnedMinapps(newPinned)
+ }
+ }
+ ]
+ const isActive = minappShow && currentMinappId === app.id
+ return (
+
+
+
+ openMinappKeepAlive(app)}
+ className={`${isActive ? 'active' : ''} ${openedKeepAliveMinapps.some((item) => item.id === app.id) ? 'opened-minapp' : ''}`}>
+
+
+
+
+
+ )
+ }}
+
+ )
+}
+
+const Menus = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 5px;
+`
+
+const Icon = styled.div<{ theme: string }>`
+ width: 35px;
+ height: 35px;
+ 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 StyledLink = styled.div`
+ text-decoration: none;
+ -webkit-app-region: none;
+ &* {
+ user-select: none;
+ }
+`
+
+const Divider = styled.div`
+ width: 50%;
+ margin: 8px 0;
+ border-bottom: 0.5px solid var(--color-border);
+`
+
+const TabsContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ -webkit-app-region: none;
+ position: relative;
+ width: 100%;
+
+ &::after {
+ content: '';
+ position: absolute;
+ right: var(--indicator-right, 0);
+ top: var(--indicator-top, 0);
+ width: 4px;
+ height: 8px;
+ background-color: var(--color-primary);
+ transition:
+ top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
+ right 0.3s ease-in-out;
+ border-radius: 2px;
+ }
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`
+
+const TabsWrapper = styled.div`
+ background-color: rgba(128, 128, 128, 0.1);
+ border-radius: 20px;
+ overflow: hidden;
+`
+
+const TopNavContainer = styled.div`
+ display: flex;
+ align-items: center;
+ padding: 4px 2px;
+ gap: 6px;
+ background-color: var(--color-list-item);
+ border-radius: 20px;
+ margin: 0 5px;
+`
+
+const TopNavMenus = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0 2px;
+ height: 100%;
+`
+
+const TopNavIcon = styled(Icon)`
+ width: 22px;
+ height: 22px;
+
+ .icon {
+ width: 22px;
+ height: 22px;
+ }
+
+ &:hover {
+ background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
+ opacity: 0.8;
+ border-radius: 50%;
+ }
+
+ &.opened-active {
+ background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
+ border: 0.5px solid var(--color-border);
+ border-radius: 50%;
+ .icon {
+ color: var(--color-primary);
+ }
+ }
+`
diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx
index 4e03cee5ff..0119cc094b 100644
--- a/src/renderer/src/components/app/Sidebar.tsx
+++ b/src/renderer/src/components/app/Sidebar.tsx
@@ -12,8 +12,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { ThemeMode } from '@renderer/types'
import { isEmoji } from '@renderer/utils'
-import type { MenuProps } from 'antd'
-import { Avatar, Dropdown, Tooltip } from 'antd'
+import { Avatar, Tooltip } from 'antd'
import {
CircleHelp,
FileSearch,
@@ -28,14 +27,13 @@ import {
Sun,
SunMoon
} from 'lucide-react'
-import { FC, useEffect } from 'react'
+import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
-import { DraggableList } from '../DraggableList'
-import MinAppIcon from '../Icons/MinAppIcon'
import UserPopup from '../Popups/UserPopup'
+import { SidebarOpenedMinappTabs, SidebarPinnedApps } from './PinnedMinapps'
const Sidebar: FC = () => {
const { hideMinappPopup, openMinapp } = useMinappPopup()
@@ -95,7 +93,7 @@ const Sidebar: FC = () => {
-
+
)}
@@ -189,137 +187,6 @@ const MainMenus: FC = () => {
})
}
-/** Tabs of opened minapps in sidebar */
-const SidebarOpenedMinappTabs: FC = () => {
- const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
- const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
- const { showOpenedMinappsInSidebar } = useSettings() // 获取控制显示的设置
- const { theme } = useTheme()
- const { t } = useTranslation()
-
- const handleOnClick = (app) => {
- if (minappShow && currentMinappId === app.id) {
- hideMinappPopup()
- } else {
- openMinappKeepAlive(app)
- }
- }
-
- // animation for minapp switch indicator
- useEffect(() => {
- //hacky way to get the height of the icon
- const iconDefaultHeight = 40
- const iconDefaultOffset = 17
- const container = document.querySelector('.TabsContainer') as HTMLElement
- const activeIcon = document.querySelector('.TabsContainer .opened-active') as HTMLElement
-
- let indicatorTop = 0,
- indicatorRight = 0
- if (minappShow && activeIcon && container) {
- indicatorTop = activeIcon.offsetTop + activeIcon.offsetHeight / 2 - 4 // 4 is half of the indicator's height (8px)
- indicatorRight = 0
- } else {
- indicatorTop =
- ((openedKeepAliveMinapps.length > 0 ? openedKeepAliveMinapps.length : 1) / 2) * iconDefaultHeight +
- iconDefaultOffset -
- 4
- indicatorRight = -50
- }
- container.style.setProperty('--indicator-top', `${indicatorTop}px`)
- container.style.setProperty('--indicator-right', `${indicatorRight}px`)
- }, [currentMinappId, openedKeepAliveMinapps, minappShow])
-
- // 检查是否需要显示已打开小程序组件
- const isShowOpened = showOpenedMinappsInSidebar && openedKeepAliveMinapps.length > 0
-
- // 如果不需要显示,返回空容器保持动画效果但不显示内容
- if (!isShowOpened) return
-
- return (
-
-
-
-
- {openedKeepAliveMinapps.map((app) => {
- const menuItems: MenuProps['items'] = [
- {
- key: 'closeApp',
- label: t('minapp.sidebar.close.title'),
- onClick: () => {
- closeMinapp(app.id)
- }
- },
- {
- key: 'closeAllApp',
- label: t('minapp.sidebar.closeall.title'),
- onClick: () => {
- closeAllMinapps()
- }
- }
- ]
- const isActive = minappShow && currentMinappId === app.id
-
- return (
-
-
-
- handleOnClick(app)}
- className={`${isActive ? 'opened-active' : ''}`}>
-
-
-
-
-
- )
- })}
-
-
-
- )
-}
-
-const PinnedApps: FC = () => {
- const { pinned, updatePinnedMinapps } = useMinapps()
- const { t } = useTranslation()
- const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
- const { theme } = useTheme()
- const { openMinappKeepAlive } = useMinappPopup()
-
- return (
-
- {(app) => {
- const menuItems: MenuProps['items'] = [
- {
- key: 'togglePin',
- label: t('minapp.sidebar.remove.title'),
- onClick: () => {
- const newPinned = pinned.filter((item) => item.id !== app.id)
- updatePinnedMinapps(newPinned)
- }
- }
- ]
- const isActive = minappShow && currentMinappId === app.id
- return (
-
-
-
- openMinappKeepAlive(app)}
- className={`${isActive ? 'active' : ''} ${openedKeepAliveMinapps.some((item) => item.id === app.id) ? 'opened-minapp' : ''}`}>
-
-
-
-
-
- )
- }}
-
- )
-}
-
const Container = styled.div<{ $isFullscreen: boolean }>`
display: flex;
flex-direction: column;
@@ -445,37 +312,4 @@ const Divider = styled.div`
border-bottom: 0.5px solid var(--color-border);
`
-const TabsContainer = styled.div`
- display: flex;
- flex-direction: column;
- align-items: center;
- -webkit-app-region: none;
- position: relative;
- width: 100%;
-
- &::after {
- content: '';
- position: absolute;
- right: var(--indicator-right, 0);
- top: var(--indicator-top, 0);
- width: 4px;
- height: 8px;
- background-color: var(--color-primary);
- transition:
- top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
- right 0.3s ease-in-out;
- border-radius: 2px;
- }
-
- &::-webkit-scrollbar {
- display: none;
- }
-`
-
-const TabsWrapper = styled.div`
- background-color: rgba(128, 128, 128, 0.1);
- border-radius: 20px;
- overflow: hidden;
-`
-
export default Sidebar
diff --git a/src/renderer/src/context/ThemeProvider.tsx b/src/renderer/src/context/ThemeProvider.tsx
index b818472076..3b9f6c2f0d 100644
--- a/src/renderer/src/context/ThemeProvider.tsx
+++ b/src/renderer/src/context/ThemeProvider.tsx
@@ -1,5 +1,5 @@
import { isMac, isWin } from '@renderer/config/constant'
-import { useSettings } from '@renderer/hooks/useSettings'
+import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import useUserTheme from '@renderer/hooks/useUserTheme'
import { ThemeMode } from '@renderer/types'
import { IpcChannel } from '@shared/IpcChannel'
@@ -28,6 +28,7 @@ export const ThemeProvider: React.FC = ({ children }) => {
window.matchMedia('(prefers-color-scheme: dark)').matches ? ThemeMode.dark : ThemeMode.light
)
const { initUserTheme } = useUserTheme()
+ const { navbarPosition } = useNavbarPosition()
const toggleTheme = () => {
const nextTheme = {
@@ -42,6 +43,7 @@ export const ThemeProvider: React.FC = ({ children }) => {
// Set initial theme and OS attributes on body
document.body.setAttribute('os', isMac ? 'mac' : isWin ? 'windows' : 'linux')
document.body.setAttribute('theme-mode', actualTheme)
+ document.body.setAttribute('navbar-position', navbarPosition)
// if theme is old auto, then set theme to system
// we can delete this after next big release
@@ -56,7 +58,7 @@ export const ThemeProvider: React.FC = ({ children }) => {
document.body.setAttribute('theme-mode', actualTheme)
setActualTheme(actualTheme)
})
- }, [actualTheme, initUserTheme, setSettedTheme, settedTheme])
+ }, [actualTheme, initUserTheme, navbarPosition, setSettedTheme, settedTheme])
useEffect(() => {
window.api.setTheme(settedTheme)
diff --git a/src/renderer/src/hooks/useSettings.ts b/src/renderer/src/hooks/useSettings.ts
index c44eff419e..388e1b1ea9 100644
--- a/src/renderer/src/hooks/useSettings.ts
+++ b/src/renderer/src/hooks/useSettings.ts
@@ -8,6 +8,7 @@ import {
setEnableDeveloperMode,
setLaunchOnBoot,
setLaunchToTray,
+ setNavbarPosition,
setPinTopicsToTop,
setSendMessageShortcut as _setSendMessageShortcut,
setShowTokens,
@@ -139,3 +140,15 @@ export const useEnableDeveloperMode = () => {
export const getEnableDeveloperMode = () => {
return store.getState().settings.enableDeveloperMode
}
+
+export const useNavbarPosition = () => {
+ const navbarPosition = useAppSelector((state) => state.settings.navbarPosition)
+ const dispatch = useAppDispatch()
+
+ return {
+ navbarPosition,
+ isLeftNavbar: navbarPosition === 'left',
+ isTopNavbar: navbarPosition === 'top',
+ setNavbarPosition: (position: 'left' | 'top') => dispatch(setNavbarPosition(position))
+ }
+}
diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json
index 3e90bae4ce..57a1e9ba3c 100644
--- a/src/renderer/src/i18n/locales/en-us.json
+++ b/src/renderer/src/i18n/locales/en-us.json
@@ -149,6 +149,7 @@
},
"chat": {
"add.assistant.title": "Add Assistant",
+ "add.topic.title": "New Topic",
"artifacts.button.download": "Download",
"artifacts.button.openExternal": "Open in external browser",
"artifacts.button.preview": "Preview",
@@ -655,6 +656,10 @@
"urdu": "Urdu",
"vietnamese": "Vietnamese"
},
+ "launchpad": {
+ "apps": "Apps",
+ "minapps": "Minapps"
+ },
"lmstudio": {
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
"keep_alive_time.placeholder": "Minutes",
@@ -885,6 +890,8 @@
}
},
"minapp": {
+ "add_to_launchpad": "Add to Launchpad",
+ "add_to_sidebar": "Add to Sidebar",
"popup": {
"close": "Close MinApp",
"devtools": "Developer Tools",
@@ -897,10 +904,9 @@
"refresh": "Refresh",
"rightclick_copyurl": "Right-click to copy URL"
},
+ "remove_from_launchpad": "Remove from Launchpad",
+ "remove_from_sidebar": "Remove from Sidebar",
"sidebar": {
- "add": {
- "title": "Add to Sidebar"
- },
"close": {
"title": "Close"
},
@@ -910,9 +916,6 @@
"hide": {
"title": "Hide"
},
- "remove": {
- "title": "Remove from Sidebar"
- },
"remove_custom": {
"title": "Delete Custom App"
}
@@ -1810,6 +1813,10 @@
"display.custom.css": "Custom CSS",
"display.custom.css.cherrycss": "Get from cherrycss.com",
"display.custom.css.placeholder": "/* Put custom CSS here */",
+ "display.navbar.position": "Navbar Position",
+ "display.navbar.position.left": "Left",
+ "display.navbar.position.top": "Top",
+ "display.navbar.title": "Navbar Settings",
"display.sidebar.chat.hiddenMessage": "Assistants are basic functions, not supported for hiding",
"display.sidebar.disabled": "Hide icons",
"display.sidebar.empty": "Drag the hidden feature from the left side here",
@@ -2509,6 +2516,19 @@
"title": "Page Zoom"
}
},
+ "title": {
+ "agents": "Agents",
+ "apps": "Apps",
+ "files": "Files",
+ "home": "Home",
+ "knowledge": "Knowledge Base",
+ "launchpad": "Launchpad",
+ "mcp-servers": "MCP Servers",
+ "memories": "Memories",
+ "paintings": "Paintings",
+ "settings": "Settings",
+ "translate": "Translate"
+ },
"trace": {
"backList": "Back To List",
"edasSupport": "Powered by Alibaba Cloud EDAS",
diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json
index a1d8a5fb4a..5fd2e4adfb 100644
--- a/src/renderer/src/i18n/locales/ja-jp.json
+++ b/src/renderer/src/i18n/locales/ja-jp.json
@@ -149,6 +149,7 @@
},
"chat": {
"add.assistant.title": "アシスタントを追加",
+ "add.topic.title": "新しいトピック",
"artifacts.button.download": "ダウンロード",
"artifacts.button.openExternal": "外部ブラウザで開く",
"artifacts.button.preview": "プレビュー",
@@ -655,6 +656,10 @@
"urdu": "ウルドゥー語",
"vietnamese": "ベトナム語"
},
+ "launchpad": {
+ "apps": "アプリ",
+ "minapps": "アプリ"
+ },
"lmstudio": {
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
"keep_alive_time.placeholder": "分",
@@ -885,6 +890,8 @@
}
},
"minapp": {
+ "add_to_launchpad": "スタート画面に追加",
+ "add_to_sidebar": "サイドバーに追加",
"popup": {
"close": "ミニアプリを閉じる",
"devtools": "開発者ツール",
@@ -897,10 +904,9 @@
"refresh": "更新",
"rightclick_copyurl": "右クリックでURLをコピー"
},
+ "remove_from_launchpad": "スタート画面から削除",
+ "remove_from_sidebar": "サイドバーから削除",
"sidebar": {
- "add": {
- "title": "サイドバーに追加"
- },
"close": {
"title": "閉じる"
},
@@ -910,9 +916,6 @@
"hide": {
"title": "非表示"
},
- "remove": {
- "title": "サイドバーから削除"
- },
"remove_custom": {
"title": "カスタムアプリを削除"
}
@@ -1810,6 +1813,10 @@
"display.custom.css": "カスタムCSS",
"display.custom.css.cherrycss": "cherrycss.comから取得",
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
+ "display.navbar.position": "ナビゲーションバー位置",
+ "display.navbar.position.left": "左",
+ "display.navbar.position.top": "上",
+ "display.navbar.title": "ナビゲーションバー設定",
"display.sidebar.chat.hiddenMessage": "アシスタントは基本的な機能であり、非表示はサポートされていません",
"display.sidebar.disabled": "アイコンを非表示",
"display.sidebar.empty": "非表示にする機能を左側からここにドラッグ",
@@ -2509,6 +2516,19 @@
"title": "ページズーム"
}
},
+ "title": {
+ "agents": "エージェント",
+ "apps": "アプリ",
+ "files": "ファイル",
+ "home": "ホーム",
+ "knowledge": "ナレッジベース",
+ "launchpad": "ランチパッド",
+ "mcp-servers": "MCP サーバー",
+ "memories": "メモリ",
+ "paintings": "ペインティング",
+ "settings": "設定",
+ "translate": "翻訳"
+ },
"trace": {
"backList": "リストに戻る",
"edasSupport": "Powered by Alibaba Cloud EDAS",
diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json
index bcb35dce23..099de0f93a 100644
--- a/src/renderer/src/i18n/locales/ru-ru.json
+++ b/src/renderer/src/i18n/locales/ru-ru.json
@@ -149,6 +149,7 @@
},
"chat": {
"add.assistant.title": "Добавить ассистента",
+ "add.topic.title": "Новый топик",
"artifacts.button.download": "Скачать",
"artifacts.button.openExternal": "Открыть во внешнем браузере",
"artifacts.button.preview": "Предпросмотр",
@@ -655,6 +656,10 @@
"urdu": "Урду",
"vietnamese": "Вьетнамский"
},
+ "launchpad": {
+ "apps": "Приложения",
+ "minapps": "Приложения"
+ },
"lmstudio": {
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
"keep_alive_time.placeholder": "Минуты",
@@ -885,6 +890,8 @@
}
},
"minapp": {
+ "add_to_launchpad": "Добавить в стартовый экран",
+ "add_to_sidebar": "Добавить в боковую панель",
"popup": {
"close": "Закрыть встроенное приложение",
"devtools": "Инструменты разработчика",
@@ -897,10 +904,9 @@
"refresh": "Обновить",
"rightclick_copyurl": "ПКМ → Копировать URL"
},
+ "remove_from_launchpad": "Удалить из стартового экрана",
+ "remove_from_sidebar": "Удалить из боковой панели",
"sidebar": {
- "add": {
- "title": "Добавить в боковую панель"
- },
"close": {
"title": "Закрыть"
},
@@ -910,9 +916,6 @@
"hide": {
"title": "Скрыть"
},
- "remove": {
- "title": "Удалить из боковой панели"
- },
"remove_custom": {
"title": "Удалить пользовательское приложение"
}
@@ -1810,6 +1813,10 @@
"display.custom.css": "Пользовательский CSS",
"display.custom.css.cherrycss": "Получить из cherrycss.com",
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
+ "display.navbar.position": "Положение навигации",
+ "display.navbar.position.left": "Слева",
+ "display.navbar.position.top": "Сверху",
+ "display.navbar.title": "Настройки навигации",
"display.sidebar.chat.hiddenMessage": "Помощник является базовой функцией и не поддерживает скрытие",
"display.sidebar.disabled": "Скрыть иконки",
"display.sidebar.empty": "Перетащите скрываемую функцию с левой стороны сюда",
@@ -2509,6 +2516,19 @@
"title": "Масштаб страницы"
}
},
+ "title": {
+ "agents": "Агенты",
+ "apps": "Приложения",
+ "files": "Файлы",
+ "home": "Главная",
+ "knowledge": "База знаний",
+ "launchpad": "Запуск",
+ "mcp-servers": "MCP серверы",
+ "memories": "Память",
+ "paintings": "Рисунки",
+ "settings": "Настройки",
+ "translate": "Перевод"
+ },
"trace": {
"backList": "Вернуться к списку",
"edasSupport": "Powered by Alibaba Cloud EDAS",
diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json
index 18f9593cc9..fc4ae307c5 100644
--- a/src/renderer/src/i18n/locales/zh-cn.json
+++ b/src/renderer/src/i18n/locales/zh-cn.json
@@ -149,6 +149,7 @@
},
"chat": {
"add.assistant.title": "添加助手",
+ "add.topic.title": "新建话题",
"artifacts.button.download": "下载",
"artifacts.button.openExternal": "外部浏览器打开",
"artifacts.button.preview": "预览",
@@ -655,6 +656,10 @@
"urdu": "乌尔都文",
"vietnamese": "越南文"
},
+ "launchpad": {
+ "apps": "应用",
+ "minapps": "小程序"
+ },
"lmstudio": {
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5 分钟)",
"keep_alive_time.placeholder": "分钟",
@@ -885,6 +890,8 @@
}
},
"minapp": {
+ "add_to_launchpad": "添加到启动台",
+ "add_to_sidebar": "添加到侧边栏",
"popup": {
"close": "关闭小程序",
"devtools": "开发者工具",
@@ -897,10 +904,9 @@
"refresh": "刷新",
"rightclick_copyurl": "右键复制 URL"
},
+ "remove_from_launchpad": "从启动台移除",
+ "remove_from_sidebar": "从侧边栏移除",
"sidebar": {
- "add": {
- "title": "添加到侧边栏"
- },
"close": {
"title": "关闭"
},
@@ -910,9 +916,6 @@
"hide": {
"title": "隐藏"
},
- "remove": {
- "title": "从侧边栏移除"
- },
"remove_custom": {
"title": "删除自定义应用"
}
@@ -1810,6 +1813,10 @@
"display.custom.css": "自定义 CSS",
"display.custom.css.cherrycss": "从 cherrycss.com 获取",
"display.custom.css.placeholder": "/* 这里写自定义 CSS */",
+ "display.navbar.position": "导航栏位置",
+ "display.navbar.position.left": "左侧",
+ "display.navbar.position.top": "顶部",
+ "display.navbar.title": "导航栏设置",
"display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏",
"display.sidebar.disabled": "隐藏的图标",
"display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里",
@@ -2509,6 +2516,19 @@
"title": "缩放"
}
},
+ "title": {
+ "agents": "智能体",
+ "apps": "小程序",
+ "files": "文件",
+ "home": "首页",
+ "knowledge": "知识库",
+ "launchpad": "启动台",
+ "mcp-servers": "MCP 服务器",
+ "memories": "记忆",
+ "paintings": "绘画",
+ "settings": "设置",
+ "translate": "翻译"
+ },
"trace": {
"backList": "返回列表",
"edasSupport": "Powered by Alibaba Cloud EDAS",
diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json
index a99c50ddf8..07fa397ce3 100644
--- a/src/renderer/src/i18n/locales/zh-tw.json
+++ b/src/renderer/src/i18n/locales/zh-tw.json
@@ -149,6 +149,7 @@
},
"chat": {
"add.assistant.title": "新增助手",
+ "add.topic.title": "新增話題",
"artifacts.button.download": "下載",
"artifacts.button.openExternal": "外部瀏覽器開啟",
"artifacts.button.preview": "預覽",
@@ -655,6 +656,10 @@
"urdu": "烏爾都文",
"vietnamese": "越南文"
},
+ "launchpad": {
+ "apps": "應用",
+ "minapps": "小程序"
+ },
"lmstudio": {
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)",
"keep_alive_time.placeholder": "分鐘",
@@ -885,6 +890,8 @@
}
},
"minapp": {
+ "add_to_launchpad": "添加到启动台",
+ "add_to_sidebar": "添加到侧边栏",
"popup": {
"close": "關閉小工具",
"devtools": "開發者工具",
@@ -897,10 +904,9 @@
"refresh": "重新整理",
"rightclick_copyurl": "右鍵複製 URL"
},
+ "remove_from_launchpad": "从启动台移除",
+ "remove_from_sidebar": "从侧边栏移除",
"sidebar": {
- "add": {
- "title": "添加到側邊欄"
- },
"close": {
"title": "關閉"
},
@@ -910,9 +916,6 @@
"hide": {
"title": "隱藏"
},
- "remove": {
- "title": "從側邊欄移除"
- },
"remove_custom": {
"title": "刪除自定義應用"
}
@@ -1810,6 +1813,10 @@
"display.custom.css": "自訂 CSS",
"display.custom.css.cherrycss": "從 cherrycss.com 取得",
"display.custom.css.placeholder": "/* 這裡寫自訂 CSS */",
+ "display.navbar.position": "導航欄位置",
+ "display.navbar.position.left": "左側",
+ "display.navbar.position.top": "頂部",
+ "display.navbar.title": "導航欄設定",
"display.sidebar.chat.hiddenMessage": "助手是基礎功能,不支援隱藏",
"display.sidebar.disabled": "隱藏的圖示",
"display.sidebar.empty": "把要隱藏的功能從左側拖拽到這裡",
@@ -2509,6 +2516,19 @@
"title": "縮放"
}
},
+ "title": {
+ "agents": "智能體",
+ "apps": "小程序",
+ "files": "文件",
+ "home": "主頁",
+ "knowledge": "知識庫",
+ "launchpad": "啟動台",
+ "mcp-servers": "MCP 伺服器",
+ "memories": "記憶",
+ "paintings": "繪畫",
+ "settings": "設定",
+ "translate": "翻譯"
+ },
"trace": {
"backList": "返回清單",
"edasSupport": "Powered by Alibaba Cloud EDAS",
diff --git a/src/renderer/src/pages/agents/AgentsPage.tsx b/src/renderer/src/pages/agents/AgentsPage.tsx
index dbcb7c5e57..45f4846430 100644
--- a/src/renderer/src/pages/agents/AgentsPage.tsx
+++ b/src/renderer/src/pages/agents/AgentsPage.tsx
@@ -4,6 +4,7 @@ import CustomTag from '@renderer/components/CustomTag'
import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents'
+import { useNavbarPosition } from '@renderer/hooks/useSettings'
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { Agent } from '@renderer/types'
import { uuid } from '@renderer/utils'
@@ -27,8 +28,10 @@ const AgentsPage: FC = () => {
const [searchInput, setSearchInput] = useState('')
const [activeGroup, setActiveGroup] = useState('我的')
const [agentGroups, setAgentGroups] = useState>({})
+ const [isSearchExpanded, setIsSearchExpanded] = useState(false)
const systemAgents = useSystemAgents()
const { agents: userAgents } = useAgents()
+ const { isTopNavbar } = useNavbarPosition()
useEffect(() => {
const systemAgentsGroupList = groupByCategories(systemAgents)
@@ -124,7 +127,35 @@ const AgentsPage: FC = () => {
const handleSearchClear = () => {
setSearch('')
+ setSearchInput('')
setActiveGroup('我的')
+ setIsSearchExpanded(false)
+ }
+
+ const handleSearchIconClick = () => {
+ if (!isSearchExpanded) {
+ setIsSearchExpanded(true)
+ } else {
+ handleSearch()
+ }
+ }
+
+ const handleSearchInputChange = (e: React.ChangeEvent) => {
+ const value = e.target.value
+ setSearchInput(value)
+ // 如果输入内容为空,折叠搜索框
+ if (value.trim() === '') {
+ setIsSearchExpanded(false)
+ setSearch('')
+ setActiveGroup('我的')
+ }
+ }
+
+ const handleSearchInputBlur = () => {
+ // 如果输入内容为空,失焦时折叠搜索框
+ if (searchInput.trim() === '') {
+ setIsSearchExpanded(false)
+ }
}
const handleGroupClick = (group: string) => () => {
@@ -166,8 +197,9 @@ const AgentsPage: FC = () => {
suffix={}
value={searchInput}
maxLength={50}
- onChange={(e) => setSearchInput(e.target.value)}
+ onChange={handleSearchInputChange}
onPressEnter={handleSearch}
+ onBlur={handleSearchInputBlur}
/>
@@ -221,6 +253,33 @@ const AgentsPage: FC = () => {
}
+ {isSearchExpanded ? (
+ }
+ value={searchInput}
+ maxLength={50}
+ onChange={handleSearchInputChange}
+ onPressEnter={handleSearch}
+ onBlur={handleSearchInputBlur}
+ autoFocus
+ />
+ ) : (
+ isTopNavbar && (
+ }>
+ {t('common.search')}
+
+ )
+ )}
}>
{t('agents.import.title')}
diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx
index 0860440b69..d3aec9df98 100644
--- a/src/renderer/src/pages/home/Chat.tsx
+++ b/src/renderer/src/pages/home/Chat.tsx
@@ -1,20 +1,22 @@
import { loggerService } from '@logger'
import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch'
+import { HStack } from '@renderer/components/Layout'
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChatContext } from '@renderer/hooks/useChatContext'
-import { useSettings } from '@renderer/hooks/useSettings'
+import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
-import { useShowTopics } from '@renderer/hooks/useStore'
+import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { Assistant, Topic } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { Flex } from 'antd'
import { debounce } from 'lodash'
-import React, { FC, useMemo, useState } from 'react'
+import React, { FC, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import styled from 'styled-components'
+import ChatNavbar from './ChatNavbar'
import Inputbar from './Inputbar/Inputbar'
import Messages from './Messages/Messages'
import Tabs from './Tabs'
@@ -30,20 +32,16 @@ interface Props {
const Chat: FC = (props) => {
const { assistant } = useAssistant(props.assistant.id)
- const { topicPosition, messageStyle, showAssistants } = useSettings()
+ const { topicPosition, messageStyle } = useSettings()
const { showTopics } = useShowTopics()
const { isMultiSelectMode } = useChatContext(props.activeTopic)
+ const { isTopNavbar } = useNavbarPosition()
const mainRef = React.useRef(null)
const contentSearchRef = React.useRef(null)
const [filterIncludeUser, setFilterIncludeUser] = useState(false)
- const maxWidth = useMemo(() => {
- const showRightTopics = showTopics && topicPosition === 'right'
- const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
- const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
- return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})`
- }, [showAssistants, showTopics, topicPosition])
+ const maxWidth = useChatMaxWidth()
useHotkeys('esc', () => {
contentSearchRef.current?.disable()
@@ -92,61 +90,103 @@ const Chat: FC = (props) => {
const firstUpdateOrNoFirstUpdateHandler = debounce(() => {
contentSearchRef.current?.silentSearch()
}, 10)
+
const messagesComponentUpdateHandler = () => {
if (firstUpdateCompleted) {
firstUpdateOrNoFirstUpdateHandler()
}
}
+
const messagesComponentFirstUpdateHandler = () => {
setTimeout(() => (firstUpdateCompleted = true), 300)
firstUpdateOrNoFirstUpdateHandler()
}
+ const mainHeight = isTopNavbar
+ ? 'calc(100vh - var(--navbar-height) - var(--navbar-height) - 12px)'
+ : 'calc(100vh - var(--navbar-height))'
+
return (
-
-
- }
- filter={contentSearchFilter}
- includeUser={filterIncludeUser}
- onIncludeUserChange={userOutlinedItemClickHandler}
- />
-
-
- {isMultiSelectMode && }
-
-
- {topicPosition === 'right' && showTopics && (
-
)}
+
+
+
+ }
+ filter={contentSearchFilter}
+ includeUser={filterIncludeUser}
+ onIncludeUserChange={userOutlinedItemClickHandler}
+ />
+
+
+ {isMultiSelectMode && }
+
+
+ {topicPosition === 'right' && showTopics && (
+
+ )}
+
)
}
+export const useChatMaxWidth = () => {
+ const { showTopics, topicPosition } = useSettings()
+ const { isLeftNavbar } = useNavbarPosition()
+ const { showAssistants } = useShowAssistants()
+ const showRightTopics = showTopics && topicPosition === 'right'
+ const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
+ const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
+ return `calc(100vw - ${isLeftNavbar ? 'var(--sidebar-width)' : '0'} ${minusAssistantsWidth} ${minusRightTopicsWidth})`
+}
+
const Container = styled.div`
display: flex;
- flex-direction: row;
- height: 100%;
+ flex-direction: column;
+ height: calc(100vh - var(--navbar-height));
flex: 1;
+ [navbar-position='top'] & {
+ height: calc(100vh - var(--navbar-height) -6px);
+ background-color: var(--color-background);
+ border-top-left-radius: 10px;
+ border-bottom-left-radius: 10px;
+ overflow: hidden;
+ }
`
const Main = styled(Flex)`
- height: calc(100vh - var(--navbar-height));
+ [navbar-position='left'] & {
+ height: calc(100vh - var(--navbar-height));
+ }
transform: translateZ(0);
position: relative;
`
diff --git a/src/renderer/src/pages/home/ChatNavbar.tsx b/src/renderer/src/pages/home/ChatNavbar.tsx
new file mode 100644
index 0000000000..0172153e35
--- /dev/null
+++ b/src/renderer/src/pages/home/ChatNavbar.tsx
@@ -0,0 +1,183 @@
+import { NavbarHeader } from '@renderer/components/app/Navbar'
+import { HStack } from '@renderer/components/Layout'
+import SearchPopup from '@renderer/components/Popups/SearchPopup'
+import { isMac } from '@renderer/config/constant'
+import { useAssistant } from '@renderer/hooks/useAssistant'
+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 { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
+import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
+import { useAppDispatch } from '@renderer/store'
+import { setNarrowMode } from '@renderer/store/settings'
+import { Assistant, Topic } from '@renderer/types'
+import { Tooltip } from 'antd'
+import { t } from 'i18next'
+import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
+import { FC, useCallback } from 'react'
+import styled from 'styled-components'
+
+import AssistantsDrawer from './components/AssistantsDrawer'
+import SelectModelButton from './components/SelectModelButton'
+import UpdateAppButton from './components/UpdateAppButton'
+
+interface Props {
+ activeAssistant: Assistant
+ activeTopic: Topic
+ setActiveTopic: (topic: Topic) => void
+ setActiveAssistant: (assistant: Assistant) => void
+ position: 'left' | 'right'
+}
+
+const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
+ const { assistant } = useAssistant(activeAssistant.id)
+ const { showAssistants, toggleShowAssistants } = useShowAssistants()
+ const isFullscreen = useFullscreen()
+ const { topicPosition, narrowMode } = useSettings()
+ const { showTopics, toggleShowTopics } = useShowTopics()
+ const dispatch = useAppDispatch()
+
+ // Function to toggle assistants with cooldown
+ const handleToggleShowAssistants = useCallback(() => {
+ if (showAssistants) {
+ toggleShowAssistants()
+ } else {
+ toggleShowAssistants()
+ }
+ }, [showAssistants, toggleShowAssistants])
+
+ const handleToggleShowTopics = useCallback(() => {
+ if (showTopics) {
+ toggleShowTopics()
+ } else {
+ toggleShowTopics()
+ }
+ }, [showTopics, toggleShowTopics])
+
+ useShortcut('toggle_show_assistants', handleToggleShowAssistants)
+
+ useShortcut('toggle_show_topics', () => {
+ if (topicPosition === 'right') {
+ toggleShowTopics()
+ } else {
+ EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
+ }
+ })
+
+ useShortcut('search_message', () => {
+ SearchPopup.show()
+ })
+
+ const handleNarrowModeToggle = async () => {
+ await modelGenerating()
+ dispatch(setNarrowMode(!narrowMode))
+ }
+
+ const onShowAssistantsDrawer = () => {
+ AssistantsDrawer.show({
+ activeAssistant,
+ setActiveAssistant,
+ activeTopic,
+ setActiveTopic
+ })
+ }
+
+ return (
+
+
+ {showAssistants && (
+
+
+
+
+
+ )}
+ {!showAssistants && (
+
+ toggleShowAssistants()}
+ style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
+
+
+
+ )}
+ {!showAssistants && (
+
+
+
+ )}
+
+
+
+
+
+ SearchPopup.show()}>
+
+
+
+
+
+
+
+
+ {topicPosition === 'right' && !showTopics && (
+
+ toggleShowTopics()}>
+
+
+
+ )}
+ {topicPosition === 'right' && showTopics && (
+
+ handleToggleShowTopics()}>
+
+
+
+ )}
+
+
+ )
+}
+
+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;
+ 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);
+ }
+`
+
+const NarrowIcon = styled(NavbarIcon)`
+ @media (max-width: 1000px) {
+ display: none;
+ }
+`
+
+export default HeaderNavbar
diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx
index 78ac37bde3..66aa73c852 100644
--- a/src/renderer/src/pages/home/HomePage.tsx
+++ b/src/renderer/src/pages/home/HomePage.tsx
@@ -1,5 +1,5 @@
import { useAssistants } from '@renderer/hooks/useAssistant'
-import { useSettings } from '@renderer/hooks/useSettings'
+import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import NavigationService from '@renderer/services/NavigationService'
@@ -17,6 +17,7 @@ let _activeAssistant: Assistant
const HomePage: FC = () => {
const { assistants } = useAssistants()
const navigate = useNavigate()
+ const { isLeftNavbar } = useNavbarPosition()
const location = useLocation()
const state = location.state
@@ -81,14 +82,16 @@ const HomePage: FC = () => {
return (
-
-
+ {isLeftNavbar && (
+
+ )}
+
{showAssistants && (
{
[isGrid, isGrouped, topic, multiModelMessageStyle, messages.length, selectedMessageId, gridPopoverTrigger]
)
+ const maxWidth = useChatMaxWidth()
+
return (
+ className={classNames([multiModelMessageStyle, { 'multi-select-mode': isMultiSelectMode }])}
+ style={{ maxWidth }}>
{
}
const GroupContainer = styled.div`
+ [navbar-position='left'] & {
+ max-width: calc(100vw - var(--sidebar-width) - var(--assistants-width) - 20px);
+ }
&.horizontal,
&.grid {
padding: 4px 10px;
diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx
index 968c4152d5..8e0532d7ae 100644
--- a/src/renderer/src/pages/home/Messages/Messages.tsx
+++ b/src/renderer/src/pages/home/Messages/Messages.tsx
@@ -379,7 +379,7 @@ const LoaderContainer = styled.div`
const ScrollContainer = styled.div`
display: flex;
flex-direction: column-reverse;
- padding: 10px 16px 20px;
+ padding: 10px 10px 20px;
.multi-select-mode & {
padding-bottom: 60px;
}
diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx
index 4ef2c7e673..7e65c25cfa 100644
--- a/src/renderer/src/pages/home/Navbar.tsx
+++ b/src/renderer/src/pages/home/Navbar.tsx
@@ -1,6 +1,5 @@
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
-import FloatingSidebar from '@renderer/components/Popups/FloatingSidebar'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isMac } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
@@ -15,10 +14,11 @@ import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types'
import { Tooltip } from 'antd'
import { t } from 'i18next'
-import { MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
-import { FC, useCallback, useState } from 'react'
+import { Menu, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
+import { FC, useCallback } from 'react'
import styled from 'styled-components'
+import AssistantsDrawer from './components/AssistantsDrawer'
import SelectModelButton from './components/SelectModelButton'
import UpdateAppButton from './components/UpdateAppButton'
@@ -37,32 +37,20 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo
const { topicPosition, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
- const [sidebarHideCooldown, setSidebarHideCooldown] = useState(false)
// Function to toggle assistants with cooldown
const handleToggleShowAssistants = useCallback(() => {
if (showAssistants) {
- // When hiding sidebar, set cooldown
toggleShowAssistants()
- setSidebarHideCooldown(true)
- // setTimeout(() => {
- // setSidebarHideCooldown(false)
- // }, 10000) // 10 seconds cooldown
} else {
- // When showing sidebar, no cooldown needed
toggleShowAssistants()
}
}, [showAssistants, toggleShowAssistants])
+
const handleToggleShowTopics = useCallback(() => {
if (showTopics) {
- // When hiding sidebar, set cooldown
toggleShowTopics()
- setSidebarHideCooldown(true)
- // setTimeout(() => {
- // setSidebarHideCooldown(false)
- // }, 10000) // 10 seconds cooldown
} else {
- // When showing sidebar, no cooldown needed
toggleShowTopics()
}
}, [showTopics, toggleShowTopics])
@@ -86,6 +74,15 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo
dispatch(setNarrowMode(!narrowMode))
}
+ const onShowAssistantsDrawer = () => {
+ AssistantsDrawer.show({
+ activeAssistant,
+ setActiveAssistant,
+ activeTopic,
+ setActiveTopic
+ })
+ }
+
return (
{showAssistants && (
@@ -104,32 +101,20 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo
)}
- {!showAssistants && !sidebarHideCooldown && (
-
-
- toggleShowAssistants()}
- style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
-
-
-
-
- )}
- {!showAssistants && sidebarHideCooldown && (
+ {!showAssistants && (
toggleShowAssistants()}
- style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}
- onMouseOut={() => setSidebarHideCooldown(false)}>
+ style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
)}
+ {!showAssistants && (
+
+
+
+ )}
@@ -144,23 +129,9 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo
- {topicPosition === 'right' && !showTopics && !sidebarHideCooldown && (
-
-
- toggleShowTopics()}>
-
-
-
-
- )}
- {topicPosition === 'right' && !showTopics && sidebarHideCooldown && (
+ {topicPosition === 'right' && !showTopics && (
- toggleShowTopics()} onMouseOut={() => setSidebarHideCooldown(false)}>
+ toggleShowTopics()}>
diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx
index 20e3456be6..e4e25a1311 100644
--- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx
+++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx
@@ -185,15 +185,11 @@ const AssistantAddItem = styled.div`
padding-right: 35px;
border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent;
+ margin-top: -8px;
cursor: pointer;
&:hover {
- background-color: var(--color-background-soft);
- }
-
- &.active {
- background-color: var(--color-background-soft);
- border: 0.5px solid var(--color-border);
+ background-color: var(--color-list-item-hover);
}
`
diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx
index d296ccdcf9..1f3997ff8a 100644
--- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx
+++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx
@@ -5,6 +5,7 @@ import {
EditOutlined,
FolderOutlined,
MenuOutlined,
+ PlusOutlined,
PushpinOutlined,
QuestionCircleOutlined,
UploadOutlined
@@ -24,7 +25,7 @@ import store from '@renderer/store'
import { RootState } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Topic } from '@renderer/types'
-import { removeSpecialCharactersForFileName } from '@renderer/utils'
+import { classNames, removeSpecialCharactersForFileName } from '@renderer/utils'
import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy'
import {
exportMarkdownToJoplin,
@@ -48,13 +49,14 @@ interface Props {
assistant: Assistant
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
+ position: 'left' | 'right'
}
-const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
+const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, position }) => {
const { assistants } = useAssistants()
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
const { t } = useTranslation()
- const { showTopicTime, pinTopicsToTop, setTopicPosition } = useSettings()
+ const { showTopicTime, pinTopicsToTop, setTopicPosition, topicPosition } = useSettings()
const renamingTopics = useSelector((state: RootState) => state.runtime.chat.renamingTopics)
const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics)
@@ -443,13 +445,21 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic
return assistant.topics
}, [assistant.topics, pinTopicsToTop])
+ const singlealone = topicPosition === 'right' && position === 'right'
+
return (
+ itemContainerStyle={{ paddingBottom: '8px' }}
+ header={
+ EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
+
+ {t('chat.add.topic.title')}
+
+ }>
{(topic) => {
const isActive = topic.id === activeTopic?.id
const topicName = topic.name.replace('`', '')
@@ -466,7 +476,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic
setTargetTopic(topic)}
- className={isActive ? 'active' : ''}
+ className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
onClick={() => onSwitchTopic(topic)}
style={{ borderRadius }}>
{isPending(topic.id) && !isActive && }
@@ -548,6 +558,7 @@ const TopicListItem = styled.div`
}
&.active {
background-color: var(--color-list-item);
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
.menu {
opacity: 1;
&:hover {
@@ -555,6 +566,16 @@ const TopicListItem = styled.div`
}
}
}
+ &.singlealone {
+ border-radius: 0 !important;
+ &:hover {
+ background-color: var(--color-background-soft);
+ }
+ &.active {
+ border-left: 2px solid var(--color-primary);
+ box-shadow: none;
+ }
+ }
`
const TopicNameContainer = styled.div`
@@ -626,6 +647,31 @@ const PendingIndicator = styled.div.attrs({
background-color: var(--color-primary);
`
+const AddTopicButton = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ width: calc(100% - 10px);
+ padding: 7px 12px;
+ margin-bottom: 8px;
+ background: transparent;
+ color: var(--color-text-2);
+ font-size: 13px;
+ border-radius: var(--list-item-border-radius);
+ cursor: pointer;
+ transition: all 0.2s;
+ margin-top: -5px;
+
+ &:hover {
+ background-color: var(--color-list-item-hover);
+ color: var(--color-text-1);
+ }
+
+ .anticon {
+ font-size: 12px;
+ }
+`
+
const TopicPromptText = styled.div`
color: var(--color-text-2);
font-size: 12px;
diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx
index f0ae3e8883..0c169a7c24 100644
--- a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx
+++ b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx
@@ -390,6 +390,7 @@ const Container = styled.div`
}
&.active {
background-color: var(--color-list-item);
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
`
diff --git a/src/renderer/src/pages/home/Tabs/index.tsx b/src/renderer/src/pages/home/Tabs/index.tsx
index 21f3a21e43..5a8c6b67d5 100644
--- a/src/renderer/src/pages/home/Tabs/index.tsx
+++ b/src/renderer/src/pages/home/Tabs/index.tsx
@@ -1,11 +1,10 @@
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
-import { useSettings } from '@renderer/hooks/useSettings'
+import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import { useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
-import { Segmented as AntSegmented, SegmentedProps } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -41,25 +40,22 @@ const HomeTabs: FC = ({
const [tab, setTab] = useState(position === 'left' ? _tab || 'assistants' : 'topic')
const { topicPosition } = useSettings()
const { defaultAssistant } = useDefaultAssistant()
- const { showTopics, toggleShowTopics } = useShowTopics()
+ const { toggleShowTopics } = useShowTopics()
+ const { isLeftNavbar } = useNavbarPosition()
const { t } = useTranslation()
const borderStyle = '0.5px solid var(--color-border)'
const border =
- position === 'left' ? { borderRight: borderStyle } : { borderLeft: borderStyle, borderTopLeftRadius: 0 }
+ position === 'left'
+ ? { borderRight: isLeftNavbar ? borderStyle : 'none' }
+ : { borderLeft: isLeftNavbar ? borderStyle : 'none', borderTopLeftRadius: 0 }
if (position === 'left' && topicPosition === 'left') {
_tab = tab
}
- const showTab = !(position === 'left' && topicPosition === 'right')
-
- const assistantTab = {
- label: t('assistants.abbr'),
- value: 'assistants'
- // icon:
- }
+ const showTab = position === 'left' && topicPosition === 'left'
const onCreateAssistant = async () => {
const assistant = await AddAssistantPopup.show()
@@ -97,41 +93,36 @@ const HomeTabs: FC = ({
if (position === 'right' && topicPosition === 'right' && tab === 'assistants') {
setTab('topic')
}
- if (position === 'left' && topicPosition === 'right' && forceToSeeAllTab != true && tab !== 'assistants') {
+ if (position === 'left' && topicPosition === 'right' && tab === 'topic') {
setTab('assistants')
}
}, [position, tab, topicPosition, forceToSeeAllTab])
return (
- {(showTab || (forceToSeeAllTab == true && !showTopics)) && (
- <>
-
- },
- {
- label: t('settings.title'),
- value: 'settings'
- // icon:
- }
- ].filter(Boolean) as SegmentedProps['options']
- }
- onChange={(value) => setTab(value as 'topic' | 'settings')}
- block
- />
-
- >
+ {position === 'left' && topicPosition === 'left' && (
+
+ setTab('assistants')}>
+ {t('assistants.abbr')}
+
+ setTab('topic')}>
+ {t('common.topics')}
+
+ setTab('settings')}>
+ {t('settings.title')}
+
+
+ )}
+
+ {position === 'left' && topicPosition === 'right' && (
+
+ setTab('assistants')}>
+ {t('assistants.abbr')}
+
+ setTab('settings')}>
+ {t('settings.title')}
+
+
)}
@@ -144,7 +135,12 @@ const HomeTabs: FC = ({
/>
)}
{tab === 'topic' && (
-
+
)}
{tab === 'settings' && }
@@ -157,7 +153,12 @@ const Container = styled.div`
flex-direction: column;
max-width: var(--assistants-width);
min-width: var(--assistants-width);
- background-color: var(--color-background);
+ [navbar-position='left'] & {
+ background-color: var(--color-background);
+ }
+ [navbar-position='top'] & {
+ min-height: calc(100vh - var(--navbar-height) - var(--navbar-height) - 12px);
+ }
overflow: hidden;
.collapsed {
width: 0;
@@ -169,72 +170,62 @@ const TabContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
- overflow-y: auto;
+ overflow-y: hidden;
overflow-x: hidden;
`
-const Divider = styled.div`
- border-top: 0.5px solid var(--color-border);
- margin-top: 10px;
- margin-left: 10px;
- margin-right: 10px;
+const CustomTabs = styled.div`
+ display: flex;
+ margin: 0 12px;
+ padding: 6px 0;
+ border-bottom: 1px solid var(--color-border);
+ background: transparent;
+ [navbar-position='top'] & {
+ padding-top: 2px;
+ }
`
-const Segmented = styled(AntSegmented)`
- font-family: var(--font-family);
+const TabItem = styled.button<{ active: boolean }>`
+ flex: 1;
+ height: 32px;
+ border: none;
+ background: transparent;
+ color: ${(props) => (props.active ? 'var(--color-text)' : 'var(--color-text-secondary)')};
+ font-size: 13px;
+ font-weight: ${(props) => (props.active ? '600' : '400')};
+ cursor: pointer;
+ border-radius: 8px;
+ margin: 0 2px;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
- &.ant-segmented {
- background-color: transparent;
- margin: 0 10px;
- margin-top: 10px;
- padding: 0;
- }
- .ant-segmented-item {
- overflow: hidden;
- transition: none !important;
- height: 34px;
- line-height: 34px;
- background-color: transparent;
- user-select: none;
- border-radius: var(--list-item-border-radius);
- box-shadow: none;
- }
- .ant-segmented-item-selected,
- .ant-segmented-item-selected:active {
- transition: none !important;
- background-color: var(--color-list-item);
- }
- .ant-segmented-item-label {
- align-items: center;
- display: flex;
- flex-direction: row;
- justify-content: center;
- font-size: 13px;
- height: 100%;
- }
- .ant-segmented-item-label[aria-selected='true'] {
+ &:hover {
color: var(--color-text);
}
- .icon-business-smart-assistant {
- margin-right: -2px;
+
+ &:active {
+ transform: scale(0.98);
}
- .ant-segmented-thumb {
- transition: none !important;
- background-color: var(--color-list-item);
- border-radius: var(--list-item-border-radius);
- box-shadow: none;
- &:hover {
- background-color: transparent;
- }
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: -9px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: ${(props) => (props.active ? '30px' : '0')};
+ height: 3px;
+ background: var(--color-primary);
+ border-radius: 1px;
+ transition: all 0.2s ease;
}
- .ant-segmented-item-label,
- .ant-segmented-item-icon {
- display: flex;
- align-items: center;
+
+ &:hover::after {
+ width: ${(props) => (props.active ? '30px' : '16px')};
+ background: ${(props) => (props.active ? 'var(--color-primary)' : 'var(--color-primary-soft)')};
}
- /* These styles ensure the same appearance as before */
- border-radius: 0;
- box-shadow: none;
`
export default HomeTabs
diff --git a/src/renderer/src/pages/home/components/AssistantsDrawer.tsx b/src/renderer/src/pages/home/components/AssistantsDrawer.tsx
new file mode 100644
index 0000000000..58eed840c4
--- /dev/null
+++ b/src/renderer/src/pages/home/components/AssistantsDrawer.tsx
@@ -0,0 +1,92 @@
+import { TopView } from '@renderer/components/TopView'
+import { isMac } from '@renderer/config/constant'
+import { Assistant, Topic } from '@renderer/types'
+import { Drawer } from 'antd'
+import { useState } from 'react'
+
+import HomeTabs from '../Tabs'
+
+interface ShowParams {
+ activeAssistant: Assistant
+ setActiveAssistant: (assistant: Assistant) => void
+ activeTopic: Topic
+ setActiveTopic: (topic: Topic) => void
+}
+
+interface Props extends ShowParams {
+ resolve: (data: any) => void
+}
+
+const PopupContainer: React.FC = ({
+ activeAssistant,
+ setActiveAssistant,
+ activeTopic,
+ setActiveTopic,
+ resolve
+}) => {
+ const [open, setOpen] = useState(true)
+
+ const onClose = () => {
+ setOpen(false)
+ setTimeout(resolve, 300)
+ }
+
+ AssistantsDrawer.hide = onClose
+
+ return (
+
+ {
+ setActiveAssistant(assistant)
+ onClose()
+ }}
+ setActiveTopic={(topic) => {
+ setActiveTopic(topic)
+ onClose()
+ }}
+ position="left"
+ />
+
+ )
+}
+
+const TopViewKey = 'AssistantsDrawer'
+
+export default class AssistantsDrawer {
+ static topviewId = 0
+ static hide() {
+ TopView.hide(TopViewKey)
+ }
+ static show(props: ShowParams) {
+ return new Promise((resolve) => {
+ TopView.show(
+ {
+ resolve(v)
+ TopView.hide(TopViewKey)
+ }}
+ />,
+ TopViewKey
+ )
+ })
+ }
+}
diff --git a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx
index e1b5464781..e8948508e3 100644
--- a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx
+++ b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx
@@ -3,7 +3,7 @@ import { loggerService } from '@logger'
import CustomTag from '@renderer/components/CustomTag'
import { HStack } from '@renderer/components/Layout'
import { useKnowledge } from '@renderer/hooks/useKnowledge'
-import { NavbarIcon } from '@renderer/pages/home/Navbar'
+import { NavbarIcon } from '@renderer/pages/home/ChatNavbar'
import { getProviderName } from '@renderer/services/ProviderService'
import { KnowledgeBase } from '@renderer/types'
import { Button, Empty, Tabs, Tag, Tooltip } from 'antd'
diff --git a/src/renderer/src/pages/launchpad/LaunchpadPage.tsx b/src/renderer/src/pages/launchpad/LaunchpadPage.tsx
new file mode 100644
index 0000000000..c189ae0864
--- /dev/null
+++ b/src/renderer/src/pages/launchpad/LaunchpadPage.tsx
@@ -0,0 +1,217 @@
+import App from '@renderer/components/MinApp/MinApp'
+import { useMinapps } from '@renderer/hooks/useMinapps'
+import { useRuntime } from '@renderer/hooks/useRuntime'
+import { useSettings } from '@renderer/hooks/useSettings'
+import tabsService from '@renderer/services/TabsService'
+import { FileSearch, Folder, Languages, LayoutGrid, Palette, Sparkle } from 'lucide-react'
+import { FC, useMemo } 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 { pinned } = useMinapps()
+ const { openedKeepAliveMinapps } = useRuntime()
+
+ const appMenuItems = [
+ {
+ icon: ,
+ text: t('title.apps'),
+ path: '/apps',
+ bgColor: 'linear-gradient(135deg, #8B5CF6, #A855F7)' // 小程序:紫色,代表多功能和灵活性
+ },
+ {
+ icon: ,
+ text: t('title.knowledge'),
+ path: '/knowledge',
+ bgColor: 'linear-gradient(135deg, #10B981, #34D399)' // 知识库:翠绿色,代表生长和知识
+ },
+ {
+ icon: ,
+ text: t('title.paintings'),
+ path: `/paintings/${defaultPaintingProvider}`,
+ bgColor: 'linear-gradient(135deg, #EC4899, #F472B6)' // 绘画:活力粉色,代表创造力和艺术
+ },
+ {
+ icon: ,
+ text: t('title.agents'),
+ path: '/agents',
+ bgColor: 'linear-gradient(135deg, #6366F1, #4F46E5)' // AI助手:靛蓝渐变,代表智能和科技
+ },
+ {
+ icon: ,
+ text: t('title.translate'),
+ path: '/translate',
+ bgColor: 'linear-gradient(135deg, #06B6D4, #0EA5E9)' // 翻译:明亮的青蓝色,代表沟通和流畅
+ },
+ {
+ icon: ,
+ text: t('title.files'),
+ path: '/files',
+ bgColor: 'linear-gradient(135deg, #F59E0B, #FBBF24)' // 文件:金色,代表资源和重要性
+ }
+ ]
+
+ // 合并并排序小程序列表
+ const sortedMinapps = useMemo(() => {
+ // 先添加固定的小程序,保持原有顺序
+ const result = [...pinned]
+
+ // 再添加其他已打开但未固定的小程序
+ openedKeepAliveMinapps.forEach((app) => {
+ if (!result.some((pinnedApp) => pinnedApp.id === app.id)) {
+ result.push(app)
+ }
+ })
+
+ return result
+ }, [openedKeepAliveMinapps, pinned])
+
+ return (
+
+
+
+ {t('launchpad.apps')}
+
+ {appMenuItems.map((item) => (
+ navigate(item.path)}>
+
+ {item.icon}
+
+ {item.text}
+
+ ))}
+
+
+
+ {sortedMinapps.length > 0 && (
+
+ {t('launchpad.minapps')}
+
+ {sortedMinapps.map((app) => (
+ setTimeout(() => tabsService.closeTab('launchpad'), 350)}>
+
+
+ ))}
+
+
+ )}
+
+
+ )
+}
+
+const Container = styled.div`
+ width: 100%;
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ background-color: var(--color-background);
+ overflow-y: auto;
+ padding: 50px 0;
+`
+
+const Content = styled.div`
+ max-width: 720px;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+`
+
+const Section = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+`
+
+const SectionTitle = styled.h2`
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-text);
+ opacity: 0.8;
+ margin: 0;
+ padding: 0 36px;
+`
+
+const Grid = styled.div`
+ display: grid;
+ grid-template-columns: repeat(6, 1fr);
+ gap: 8px;
+ padding: 0 8px;
+`
+
+const AppIcon = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: pointer;
+ gap: 4px;
+ padding: 8px 4px;
+ border-radius: 16px;
+ transition: transform 0.2s ease;
+
+ &:hover {
+ transform: scale(1.05);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+`
+
+const IconContainer = styled.div`
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 56px;
+ height: 56px;
+`
+
+const IconWrapper = styled.div<{ bgColor: string }>`
+ width: 56px;
+ height: 56px;
+ border-radius: 16px;
+ background: ${(props) => props.bgColor};
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+ .icon {
+ color: white;
+ width: 28px;
+ height: 28px;
+ }
+`
+
+const AppName = styled.div`
+ font-size: 12px;
+ color: var(--color-text);
+ text-align: center;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`
+
+const AppWrapper = styled.div`
+ padding: 8px 4px;
+ border-radius: 8px;
+ transition: transform 0.2s ease;
+
+ &:hover {
+ transform: scale(1.05);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+`
+
+export default LaunchpadPage
diff --git a/src/renderer/src/pages/apps/AppsPage.tsx b/src/renderer/src/pages/minapps/MinAppsPage.tsx
similarity index 51%
rename from src/renderer/src/pages/apps/AppsPage.tsx
rename to src/renderer/src/pages/minapps/MinAppsPage.tsx
index 31cb3f2392..f1c052ca73 100644
--- a/src/renderer/src/pages/apps/AppsPage.tsx
+++ b/src/renderer/src/pages/minapps/MinAppsPage.tsx
@@ -1,22 +1,22 @@
import { Navbar, NavbarMain } from '@renderer/components/app/Navbar'
+import App from '@renderer/components/MinApp/MinApp'
+import Scrollbar from '@renderer/components/Scrollbar'
import { useMinapps } from '@renderer/hooks/useMinapps'
+import { useNavbarPosition } from '@renderer/hooks/useSettings'
import { Button, Input } from 'antd'
-import { Search, SettingsIcon, X } from 'lucide-react'
-import React, { FC, useEffect, useState } from 'react'
+import { Search, SettingsIcon } from 'lucide-react'
+import React, { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { useLocation } from 'react-router'
import styled from 'styled-components'
-import App from './App'
-import MiniAppSettings from './MiniappSettings/MiniAppSettings'
+import MinappSettingsPopup from './MiniappSettings/MinappSettingsPopup'
import NewAppButton from './NewAppButton'
const AppsPage: FC = () => {
const { t } = useTranslation()
const [search, setSearch] = useState('')
const { minapps } = useMinapps()
- const [isSettingsOpen, setIsSettingsOpen] = useState(false)
- const location = useLocation()
+ const { isTopNavbar } = useNavbarPosition()
const filteredApps = search
? minapps.filter(
@@ -35,10 +35,6 @@ const AppsPage: FC = () => {
e.preventDefault()
}
- useEffect(() => {
- setIsSettingsOpen(false)
- }, [location.key])
-
return (
@@ -60,26 +56,47 @@ const AppsPage: FC = () => {
suffix={}
value={search}
onChange={(e) => setSearch(e.target.value)}
- disabled={isSettingsOpen}
/>
: }
- onClick={() => setIsSettingsOpen(!isSettingsOpen)}
+ icon={}
+ onClick={MinappSettingsPopup.show}
/>
- {isSettingsOpen && }
- {!isSettingsOpen && (
-
- {filteredApps.map((app) => (
-
- ))}
-
-
- )}
+
+
+ {isTopNavbar && (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.target.value)}
+ />
+ }
+ onClick={() => MinappSettingsPopup.show()}
+ />
+
+ )}
+
+
+ {filteredApps.map((app) => (
+
+ ))}
+
+
+
+
+
)
@@ -98,8 +115,41 @@ const ContentContainer = styled.div`
flex-direction: row;
justify-content: center;
height: 100%;
- overflow-y: auto;
- padding: 50px;
+`
+
+const HeaderContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ height: 60px;
+ width: 100%;
+ gap: 10px;
+`
+
+const MainContainer = styled.div`
+ display: flex;
+ flex: 1;
+ flex-direction: row;
+ height: calc(100vh - var(--navbar-height));
+`
+
+const RightContainer = styled(Scrollbar)`
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ height: 100%;
+ align-items: center;
+ height: calc(100vh - var(--navbar-height));
+`
+
+const AppsContainerWrapper = styled(Scrollbar)`
+ display: flex;
+ flex: 1;
+ flex-direction: row;
+ justify-content: center;
+ padding: 20px 0;
+ width: 100%;
`
const AppsContainer = styled.div`
diff --git a/src/renderer/src/pages/minapps/MiniappSettings/MinappSettingsPopup.tsx b/src/renderer/src/pages/minapps/MiniappSettings/MinappSettingsPopup.tsx
new file mode 100644
index 0000000000..8b3d48b7f3
--- /dev/null
+++ b/src/renderer/src/pages/minapps/MiniappSettings/MinappSettingsPopup.tsx
@@ -0,0 +1,66 @@
+import { TopView } from '@renderer/components/TopView'
+import { Modal } from 'antd'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+
+import MiniAppSettings from './MiniAppSettings'
+
+interface Props {
+ resolve: (data: any) => void
+}
+
+const PopupContainer: React.FC = ({ resolve }) => {
+ const [open, setOpen] = useState(true)
+ const { t } = useTranslation()
+
+ const onOk = () => {
+ setOpen(false)
+ }
+
+ const onCancel = () => {
+ setOpen(false)
+ }
+
+ const onClose = () => {
+ resolve({})
+ }
+
+ MinappSettingsPopup.hide = onCancel
+
+ return (
+
+
+
+ )
+}
+
+const TopViewKey = 'MinappSettingsPopup'
+
+export default class MinappSettingsPopup {
+ static topviewId = 0
+ static hide() {
+ TopView.hide(TopViewKey)
+ }
+ static show() {
+ return new Promise((resolve) => {
+ TopView.show(
+ {
+ resolve(v)
+ TopView.hide(TopViewKey)
+ }}
+ />,
+ TopViewKey
+ )
+ })
+ }
+}
diff --git a/src/renderer/src/pages/apps/MiniappSettings/MiniAppIconsManager.tsx b/src/renderer/src/pages/minapps/MiniappSettings/MiniAppIconsManager.tsx
similarity index 100%
rename from src/renderer/src/pages/apps/MiniappSettings/MiniAppIconsManager.tsx
rename to src/renderer/src/pages/minapps/MiniappSettings/MiniAppIconsManager.tsx
diff --git a/src/renderer/src/pages/apps/MiniappSettings/MiniAppSettings.tsx b/src/renderer/src/pages/minapps/MiniappSettings/MiniAppSettings.tsx
similarity index 93%
rename from src/renderer/src/pages/apps/MiniappSettings/MiniAppSettings.tsx
rename to src/renderer/src/pages/minapps/MiniappSettings/MiniAppSettings.tsx
index e322f4fd9b..506d1092fb 100644
--- a/src/renderer/src/pages/apps/MiniappSettings/MiniAppSettings.tsx
+++ b/src/renderer/src/pages/minapps/MiniappSettings/MiniAppSettings.tsx
@@ -12,7 +12,6 @@ import {
import { Button, message, Slider, Switch, Tooltip } from 'antd'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { useNavigate } from 'react-router'
import styled from 'styled-components'
import MiniAppIconsManager from './MiniAppIconsManager'
@@ -25,7 +24,6 @@ const MiniAppSettings: FC = () => {
const dispatch = useAppDispatch()
const { maxKeepAliveMinapps, showOpenedMinappsInSidebar, minappsOpenLinkExternal } = useSettings()
const { minapps, disabled, updateMinapps, updateDisabledMinapps } = useMinapps()
- const navigate = useNavigate()
const [visibleMiniApps, setVisibleMiniApps] = useState(minapps)
const [disabledMiniApps, setDisabledMiniApps] = useState(disabled || [])
@@ -80,9 +78,7 @@ const MiniAppSettings: FC = () => {
return (
{contextHolder} {/* 添加消息上下文 */}
-
- {t('settings.miniapps.display_title')}
+
@@ -146,10 +142,6 @@ const MiniAppSettings: FC = () => {
onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))}
/>
-
-
-
-
)
}
@@ -158,6 +150,7 @@ const Container = styled.div`
display: flex;
flex-direction: column;
flex: 1;
+ padding-top: 10px;
`
// 修改和新增样式
diff --git a/src/renderer/src/pages/apps/NewAppButton.tsx b/src/renderer/src/pages/minapps/NewAppButton.tsx
similarity index 100%
rename from src/renderer/src/pages/apps/NewAppButton.tsx
rename to src/renderer/src/pages/minapps/NewAppButton.tsx
diff --git a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
index 27453ef1cd..276447d72e 100644
--- a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
+++ b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx
@@ -1,9 +1,10 @@
import { SyncOutlined } from '@ant-design/icons'
import CodeEditor from '@renderer/components/CodeEditor'
import { HStack } from '@renderer/components/Layout'
+import TextBadge from '@renderer/components/TextBadge'
import { isMac, THEME_COLOR_PRESETS } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
-import { useSettings } from '@renderer/hooks/useSettings'
+import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import useUserTheme from '@renderer/hooks/useUserTheme'
import { useAppDispatch } from '@renderer/store'
import {
@@ -68,6 +69,7 @@ const DisplaySettings: FC = () => {
assistantIconType,
userTheme
} = useSettings()
+ const { navbarPosition, setNavbarPosition } = useNavbarPosition()
const { theme, settedTheme } = useTheme()
const { t } = useTranslation()
const dispatch = useAppDispatch()
@@ -216,6 +218,24 @@ const DisplaySettings: FC = () => {
>
)}
+
+
+ {t('settings.display.navbar.title')}
+
+
+
+ {t('settings.display.navbar.position')}
+
+
+
{t('settings.display.zoom.title')}
@@ -286,22 +306,24 @@ const DisplaySettings: FC = () => {
/>
-
-
- {t('settings.display.sidebar.title')}
-
-
-
-
-
-
-
+ {navbarPosition === 'left' && (
+
+
+ {t('settings.display.sidebar.title')}
+
+
+
+
+
+
+
+ )}
{t('settings.display.custom.css')}
diff --git a/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx
index ef1bbfad3e..2f7e64bf34 100644
--- a/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx
+++ b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx
@@ -76,7 +76,6 @@ const InstallNpxUv: FC = ({ mini = false }) => {
return (
: }
diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx
index 9f8b709575..a6de15b806 100644
--- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx
+++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx
@@ -16,6 +16,7 @@ import { SettingTitle } from '..'
import AddMcpServerModal from './AddMcpServerModal'
import BuiltinMCPServersSection from './BuiltinMCPServersSection'
import EditMcpJsonPopup from './EditMcpJsonPopup'
+import InstallNpxUv from './InstallNpxUv'
import McpResourcesSection from './McpResourcesSection'
import SyncServersPopup from './SyncServersPopup'
@@ -118,6 +119,7 @@ const McpServersList: FC = () => {
} type="text" onClick={() => EditMcpJsonPopup.show()} shape="circle" />
+
{
{t('memory.global_memory')}
-
- Beta
-
+
diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx
index fe5c575bff..4e21ff54eb 100644
--- a/src/renderer/src/pages/settings/SettingsPage.tsx
+++ b/src/renderer/src/pages/settings/SettingsPage.tsx
@@ -26,7 +26,6 @@ import DataSettings from './DataSettings/DataSettings'
import DisplaySettings from './DisplaySettings/DisplaySettings'
import GeneralSettings from './GeneralSettings'
import MCPSettings from './MCPSettings'
-import { McpSettingsNavbar } from './MCPSettings/McpSettingsNavbar'
import MemorySettings from './MemorySettings'
import ProvidersList from './ProviderSettings'
import QuickAssistantSettings from './QuickAssistantSettings'
@@ -45,7 +44,6 @@ const SettingsPage: FC = () => {
{t('settings.title')}
- {pathname.includes('/settings/mcp') && }
@@ -79,18 +77,18 @@ const SettingsPage: FC = () => {
{t('settings.mcp.title')}
-
-
-
+
+
+