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 && ( + + ) + )} 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} /> @@ -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 (