feat: new ui (#8322)

* feat: ui switch

* chore: update migration version to 122 and adjust settings topic position

* refactor: replace PinnedApps component with SidebarPinnedApps and SidebarOpenedMinappTabs for improved structure

* feat(i18n): add launchpad apps and minapps translations for multiple languages

* style: update MinAppIcon and IconContainer dimensions for improved UI consistency

* refactor: remove unused SidebarContainer component from AppsPage

* refactor: adjust Navbar padding and enhance search functionality in AgentsPage

* feat(minapps): implement MinApps page and enhance mini app management features

- Added MinAppsPage for managing mini applications.
- Introduced NewAppButton for adding custom mini apps.
- Created MiniAppSettings for configuring mini app settings.
- Enhanced mini app icon management with MiniAppIconsManager.
- Updated Router to include MinAppsPage and replaced AppsPage with MinAppsPage.
- Added translations for new mini app features in multiple languages.

* wip

* refactor: rename App component to MinApp and streamline LaunchpadPage logic

- Renamed App component to MinApp for clarity.
- Removed unnecessary state management in LaunchpadPage.
- Simplified minapp sorting logic by directly using openedKeepAliveMinapps.

* feat(i18n): update translations for multiple languages and restructure title entries

- Added missing title entries for various sections in English, Japanese, Russian, Chinese (Simplified and Traditional).
- Restructured the launchpad and minapp translations for better organization.
- Enhanced navbar display settings translations across all supported languages.

* feat: add header prop to DraggableVirtualList and implement Add Topic button in TopicsTab

- Introduced a new `header` prop in the DraggableVirtualList component to allow custom header content.
- Added an Add Topic button in the TopicsTab with a corresponding styled component and translation support for multiple languages.
- Updated styles in AssistantsTab and adjusted overflow behavior in Tabs index for better UI experience.

* style: adjust margins and max-width for improved layout in various components

- Updated margin-top in HtmlArtifactsCard for consistent spacing.
- Set max-width in MessageGroup to enhance responsiveness based on navbar position.
- Modified Add Topic button in TopicsTab to emit an event for better functionality.

* fix: correct state property name in migration for navbar position

- Updated the migration logic to set the correct state property from `topicPosition` to `navbarPosition` for proper configuration handling.

* fix: adjust traffic light position and navbar height for improved UI consistency

- Updated traffic light position in WindowService to enhance layout.
- Adjusted navbar height in color.scss for better alignment across components.
- Modified TabContainer to track last settings path and improve navigation handling.

* style: update AddTopicButton styling for improved hover effect in TopicsTab

- Changed AddTopicButton from a button to a div for better styling flexibility.
- Removed dashed border and added background color on hover for enhanced user experience.
- Retained border-radius for consistent design across components.

* feat: add TextBadge component and integrate into Display and Memory settings

- Introduced a new TextBadge component for displaying styled badges.
- Integrated TextBadge into DisplaySettings to highlight the navbar title as "New".
- Replaced inline badge implementation in MemorySettings with the new TextBadge component for consistency and improved maintainability.

* fix: adjust tab and navbar styling for improved UI consistency

- Increased height of title bar overlays for better visual balance.
- Updated tab creation logic to prevent duplicate tabs for specific paths.
- Modified tab icon and close button sizes for a more compact design.
- Enhanced tab spacing and padding for improved layout across components.

* style: update PinnedMinapps component for improved UI consistency

- Increased icon size and adjusted border radius for better visual appeal.
- Modified TopNavContainer padding and margin for enhanced layout.
- Reduced dimensions of TopNavIcon for a more compact design.

* refactor: enhance TabsContainer logic and styling for improved tab management

- Introduced a new `removeSpecialTabs` function to manage special tab removal more effectively.
- Updated tab filtering logic to utilize a dedicated `specialTabs` array for better maintainability.
- Adjusted styling in PinnedMinapps for consistent icon sizing and background color improvements.

* style: adjust layout and padding for improved UI consistency in Chat and Inputbar components

- Updated main height calculation in Chat component to account for additional spacing.
- Modified padding in Inputbar component for better alignment when navbar is positioned at the top.
- Ensured consistent minimum height in Tabs component to match updated navbar height calculations.

* refactor: update app menu item text keys for improved localization

- Changed text keys in the app menu items from specific titles to a more generalized 'title' namespace for better consistency and maintainability.
- Ensured that the visual representation of the menu items remains unchanged while enhancing the localization structure.

* refactor: simplify sidebar toggle logic in ChatNavbar and Navbar components

- Removed unnecessary cooldown logic when toggling the visibility of assistants and topics.
- Updated HomePage to conditionally render the Navbar based on the sidebar state for improved UI responsiveness.

* refactor: streamline Chat component and introduce useChatMaxWidth hook

- Consolidated max width calculation logic into a new `useChatMaxWidth` hook for better reusability.
- Removed unused variables and simplified state management in the Chat component.
- Updated MessageGroup to utilize the new `useChatMaxWidth` hook for consistent layout handling.

* refactor: remove FloatingSidebar component and integrate AssistantsDrawer for improved UI management

- Deleted the FloatingSidebar component to streamline the codebase.
- Introduced AssistantsDrawer for managing assistant interactions, enhancing user experience.
- Updated Navbar and ChatNavbar components to utilize AssistantsDrawer instead of FloatingSidebar for better responsiveness and maintainability.

* refactor: implement TabsService for improved tab management functionality

- Introduced TabsService to centralize tab operations, including closing and setting active tabs.
- Updated TabsContainer and LaunchpadPage components to utilize TabsService for closing tabs, enhancing code maintainability.
- Made minor UI adjustments in PinnedMinapps for consistent icon sizing and layout improvements.

* fix: prevent default event behavior when not in fullscreen mode

- Updated WindowService to conditionally call event.preventDefault() only when the main window is not in fullscreen, improving event handling logic.
This commit is contained in:
亢奋猫 2025-07-23 14:34:26 +08:00 committed by kangfenmao
parent c2086fdb15
commit 75b8a5a6a7
56 changed files with 2277 additions and 653 deletions

View File

@ -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'
}

View File

@ -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()

View File

@ -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 {
<CodeStyleProvider>
<PersistGate loading={null} persistor={persistor}>
<TopViewContainer>
<HashRouter>
<NavigationHandler />
<Sidebar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>
<Router />
</TopViewContainer>
</PersistGate>
</CodeStyleProvider>

View File

@ -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 (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<MinAppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} />
</Routes>
)
}, [])
if (navbarPosition === 'left') {
return (
<HashRouter>
<Sidebar />
{routes}
<NavigationHandler />
</HashRouter>
)
}
return (
<HashRouter>
<NavigationHandler />
<TabsContainer>{routes}</TabsContainer>
</HashRouter>
)
}
export default Router

View File

@ -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;

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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`

View File

@ -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<T> {
@ -43,6 +44,7 @@ interface DraggableVirtualListProps<T> {
list: T[]
itemKey?: (index: number) => Key
overscan?: number
header?: React.ReactNode
children: (item: T, index: number) => React.ReactNode
}
@ -66,6 +68,7 @@ function DraggableVirtualList<T>({
list,
itemKey,
overscan = 5,
header,
children
}: DraggableVirtualListProps<T>): React.ReactElement {
const _onDragEnd = (result: DropResult, provided: ResponderProvided) => {
@ -92,6 +95,7 @@ function DraggableVirtualList<T>({
return (
<div ref={ref} className={`${className} draggable-virtual-list`} style={{ height: '100%', ...style }}>
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
{header}
<Droppable
droppableId="droppable"
mode="virtual"

View File

@ -1,13 +1,18 @@
import { loggerService } from '@logger'
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
import IndicatorLight from '@renderer/components/IndicatorLight'
import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } from '@renderer/config/minapps'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useNavbarPosition } from '@renderer/hooks/useSettings'
import { setOpenedKeepAliveMinapps } from '@renderer/store/runtime'
import { MinAppType } from '@renderer/types'
import type { MenuProps } from 'antd'
import { Dropdown } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import styled from 'styled-components'
interface Props {
@ -19,12 +24,17 @@ interface Props {
const logger = loggerService.withContext('App')
const App: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
const MinApp: FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ app, onClick, size = 60, isLast }) => {
return (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Container onClick={handleClick}>
<MinAppIcon size={size} app={app} />
<IconContainer>
<MinAppIcon size={size} app={app} />
{isOpened && (
<StyledIndicator>
<IndicatorLight color="#22c55e" size={6} animation={!isActive} />
</StyledIndicator>
)}
</IconContainer>
<AppTitle>{isLast ? t('settings.miniapps.custom.title') : app.name}</AppTitle>
</Container>
</Dropdown>
@ -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

View File

@ -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 = () => {
</Tooltip>
{appInfo.canPinned && (
<Tooltip
title={appInfo.isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title')}
title={
appInfo.isPinned
? isTopNavbar
? t('minapp.remove_from_launchpad')
: t('minapp.remove_from_sidebar')
: isTopNavbar
? t('minapp.add_to_launchpad')
: t('minapp.add_to_sidebar')
}
mouseEnterDelay={0.8}
placement="bottom">
<TitleButton onClick={() => 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`

View File

@ -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<WebviewTag | null>(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 (
<webview
key={appid}
@ -88,11 +96,4 @@ const WebviewContainer = memo(
}
)
const WebviewStyle: React.CSSProperties = {
width: 'calc(100vw - var(--sidebar-width))',
height: 'calc(100vh - var(--navbar-height))',
backgroundColor: 'var(--color-background)',
display: 'inline-flex'
}
export default WebviewContainer

View File

@ -1,90 +0,0 @@
import HomeTabs from '@renderer/pages/home/Tabs/index'
import { Assistant, Topic } from '@renderer/types'
import { Popover } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import styled from 'styled-components'
interface Props {
children: React.ReactNode
activeAssistant: Assistant
setActiveAssistant: (assistant: Assistant) => void
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
position: 'left' | 'right'
}
const FloatingSidebar: FC<Props> = ({
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 = (
<PopoverContent maxHeight={maxHeight}>
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position={position}
forceToSeeAllTab={true}
style={{
background: 'transparent',
border: 'none',
height: '100%'
}}
/>
</PopoverContent>
)
return (
<Popover
open={open}
onOpenChange={setOpen}
content={content}
trigger={['hover', 'click', 'contextMenu']}
placement="bottomRight"
showArrow
mouseEnterDelay={0.8} // 800ms delay before showing
mouseLeaveDelay={20}
styles={{
body: {
padding: 0
}
}}>
{children}
</Popover>
)
}
const PopoverContent = styled.div<{ maxHeight: number }>`
max-height: ${(props) => props.maxHeight}px;
&.ant-popover-inner-content {
overflow-y: hidden;
}
`
export default FloatingSidebar

View File

@ -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 <Home size={14} />
case 'agents':
return <Sparkle size={14} />
case 'translate':
return <Languages size={14} />
case 'paintings':
return <Palette size={14} />
case 'apps':
return <LayoutGrid size={14} />
case 'knowledge':
return <FileSearch size={14} />
case 'mcp':
return <SquareTerminal size={14} />
case 'files':
return <Folder size={14} />
case 'settings':
return <Settings size={14} />
default:
return null
}
}
let lastSettingsPath = '/settings/provider'
const specialTabs = ['launchpad', 'settings']
const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
const { t } = useTranslation()
const location = useLocation()
const navigate = useNavigate()
const dispatch = useAppDispatch()
const tabs = useAppSelector((state) => state.tabs.tabs)
const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
const isFullscreen = useFullscreen()
const { 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 <Moon size={16} />
case ThemeMode.light:
return <Sun size={16} />
case ThemeMode.system:
return <SunMoon size={16} />
default:
return <SunMoon size={16} />
}
}
return (
<Container>
<TabsBar $isFullscreen={isFullscreen}>
{tabs
.filter((tab) => !specialTabs.includes(tab.id))
.map((tab) => {
return (
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => navigate(tab.path)}>
<TabHeader>
{tab.id && <TabIcon>{getTabIcon(tab.id)}</TabIcon>}
<TabTitle>{t(`title.${tab.id}`)}</TabTitle>
</TabHeader>
{tab.id !== 'home' && (
<CloseButton
className="close-button"
onClick={(e) => {
e.stopPropagation()
closeTab(tab.id)
}}>
<X size={12} />
</CloseButton>
)}
</Tab>
)
})}
<AddTabButton onClick={handleAddTab} className={classNames({ active: activeTabId === 'launchpad' })}>
<PlusOutlined />
</AddTabButton>
<RightButtonsContainer>
<TopNavbarOpenedMinappTabs />
<ThemeButton onClick={toggleTheme}>{getThemeIcon()}</ThemeButton>
<SettingsButton onClick={handleSettingsClick} $active={activeTabId === 'settings'}>
<Settings size={16} />
</SettingsButton>
</RightButtonsContainer>
</TabsBar>
<TabContent>{children}</TabContent>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`
const TabsBar = styled.div<{ $isFullscreen: boolean }>`
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? '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

View File

@ -0,0 +1,22 @@
import { FC } from 'react'
import styled from 'styled-components'
interface Props {
text: string
style?: React.CSSProperties
}
const TextBadge: FC<Props> = ({ text, style }) => {
return <Container style={style}>{text}</Container>
}
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

View File

@ -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<HTMLDivElement>
export const Navbar: FC<Props> = ({ children, ...props }) => {
const backgroundColor = useNavBackgroundColor()
const { isTopNavbar } = useNavbarPosition()
if (isTopNavbar) {
return null
}
return (
<NavbarContainer {...props} style={{ backgroundColor }}>
@ -43,6 +49,10 @@ export const NavbarMain: FC<Props> = ({ children, ...props }) => {
)
}
export const NavbarHeader: FC<Props> = ({ children, ...props }) => {
return <NavbarHeaderContent {...props}>{children}</NavbarHeaderContent>
}
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);
`

View File

@ -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 (
<TopNavContainer
style={{ backgroundColor: keepAliveMinapps.length > 1 ? 'var(--color-list-item)' : 'transparent' }}>
<TopNavMenus>
{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 (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="bottom">
<StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<TopNavIcon
theme={theme}
onClick={() => handleOnClick(app)}
className={`${isActive ? 'opened-active' : ''}`}>
<MinAppIcon size={22} app={app} style={{ border: 'none', padding: 0 }} />
</TopNavIcon>
</Dropdown>
</StyledLink>
</Tooltip>
)
})}
</TopNavMenus>
</TopNavContainer>
)
}
/** 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 <TabsContainer className="TabsContainer" />
return (
<TabsContainer className="TabsContainer">
{isLeftNavbar && <Divider />}
<TabsWrapper>
<Menus>
{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 (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<Icon
theme={theme}
onClick={() => handleOnClick(app)}
className={`${isActive ? 'opened-active' : ''}`}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} sidebar />
</Icon>
</Dropdown>
</StyledLink>
</Tooltip>
)
})}
</Menus>
</TabsWrapper>
</TabsContainer>
)
}
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 (
<DraggableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
{(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 (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<Icon
theme={theme}
onClick={() => openMinappKeepAlive(app)}
className={`${isActive ? 'active' : ''} ${openedKeepAliveMinapps.some((item) => item.id === app.id) ? 'opened-minapp' : ''}`}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} sidebar />
</Icon>
</Dropdown>
</StyledLink>
</Tooltip>
)
}}
</DraggableList>
)
}
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);
}
}
`

View File

@ -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 = () => {
<AppsContainer>
<Divider />
<Menus>
<PinnedApps />
<SidebarPinnedApps />
</Menus>
</AppsContainer>
)}
@ -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 <TabsContainer className="TabsContainer" />
return (
<TabsContainer className="TabsContainer">
<Divider />
<TabsWrapper>
<Menus>
{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 (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<Icon
theme={theme}
onClick={() => handleOnClick(app)}
className={`${isActive ? 'opened-active' : ''}`}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} sidebar />
</Icon>
</Dropdown>
</StyledLink>
</Tooltip>
)
})}
</Menus>
</TabsWrapper>
</TabsContainer>
)
}
const PinnedApps: FC = () => {
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
const { theme } = useTheme()
const { openMinappKeepAlive } = useMinappPopup()
return (
<DraggableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
{(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 (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<Icon
theme={theme}
onClick={() => openMinappKeepAlive(app)}
className={`${isActive ? 'active' : ''} ${openedKeepAliveMinapps.some((item) => item.id === app.id) ? 'opened-minapp' : ''}`}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} sidebar />
</Icon>
</Dropdown>
</StyledLink>
</Tooltip>
)
}}
</DraggableList>
)
}
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

View File

@ -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<ThemeProviderProps> = ({ 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<ThemeProviderProps> = ({ 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<ThemeProviderProps> = ({ children }) => {
document.body.setAttribute('theme-mode', actualTheme)
setActualTheme(actualTheme)
})
}, [actualTheme, initUserTheme, setSettedTheme, settedTheme])
}, [actualTheme, initUserTheme, navbarPosition, setSettedTheme, settedTheme])
useEffect(() => {
window.api.setTheme(settedTheme)

View File

@ -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))
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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<Record<string, Agent[]>>({})
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<HTMLInputElement>) => {
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={<Search size={14} color="var(--color-icon)" onClick={handleSearch} />}
value={searchInput}
maxLength={50}
onChange={(e) => setSearchInput(e.target.value)}
onChange={handleSearchInputChange}
onPressEnter={handleSearch}
onBlur={handleSearchInputBlur}
/>
<div style={{ width: 80 }} />
</NavbarCenter>
@ -221,6 +253,33 @@ const AgentsPage: FC = () => {
}
</AgentsListTitle>
<Flex gap={8}>
{isSearchExpanded ? (
<Input
placeholder={t('common.search')}
className="nodrag"
style={{ width: 300, height: 28, borderRadius: 15, paddingLeft: 12 }}
size="small"
variant="filled"
allowClear
onClear={handleSearchClear}
suffix={<Search size={14} color="var(--color-icon)" onClick={handleSearchIconClick} />}
value={searchInput}
maxLength={50}
onChange={handleSearchInputChange}
onPressEnter={handleSearch}
onBlur={handleSearchInputBlur}
autoFocus
/>
) : (
isTopNavbar && (
<Button
type="text"
onClick={handleSearchIconClick}
icon={<Search size={18} color="var(--color-icon)" />}>
{t('common.search')}
</Button>
)
)}
<Button type="text" onClick={handleImportAgent} icon={<ImportOutlined />}>
{t('agents.import.title')}
</Button>

View File

@ -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> = (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<HTMLDivElement>(null)
const contentSearchRef = React.useRef<ContentSearchRef>(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> = (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 (
<Container id="chat" className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
<Main ref={mainRef} id="chat-main" vertical flex={1} justify="space-between" style={{ maxWidth }}>
<Messages
key={props.activeTopic.id}
assistant={assistant}
topic={props.activeTopic}
setActiveTopic={props.setActiveTopic}
onComponentUpdate={messagesComponentUpdateHandler}
onFirstUpdate={messagesComponentFirstUpdateHandler}
/>
<ContentSearch
ref={contentSearchRef}
searchTarget={mainRef as React.RefObject<HTMLElement>}
filter={contentSearchFilter}
includeUser={filterIncludeUser}
onIncludeUserChange={userOutlinedItemClickHandler}
/>
<QuickPanelProvider>
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
</QuickPanelProvider>
</Main>
{topicPosition === 'right' && showTopics && (
<Tabs
activeAssistant={assistant}
{isTopNavbar && (
<ChatNavbar
activeAssistant={props.assistant}
activeTopic={props.activeTopic}
setActiveAssistant={props.setActiveAssistant}
setActiveTopic={props.setActiveTopic}
position="right"
setActiveAssistant={props.setActiveAssistant}
position="left"
/>
)}
<HStack>
<Main
ref={mainRef}
id="chat-main"
vertical
flex={1}
justify="space-between"
style={{ maxWidth, height: mainHeight }}>
<Messages
key={props.activeTopic.id}
assistant={assistant}
topic={props.activeTopic}
setActiveTopic={props.setActiveTopic}
onComponentUpdate={messagesComponentUpdateHandler}
onFirstUpdate={messagesComponentFirstUpdateHandler}
/>
<ContentSearch
ref={contentSearchRef}
searchTarget={mainRef as React.RefObject<HTMLElement>}
filter={contentSearchFilter}
includeUser={filterIncludeUser}
onIncludeUserChange={userOutlinedItemClickHandler}
/>
<QuickPanelProvider>
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
</QuickPanelProvider>
</Main>
{topicPosition === 'right' && showTopics && (
<Tabs
activeAssistant={assistant}
activeTopic={props.activeTopic}
setActiveAssistant={props.setActiveAssistant}
setActiveTopic={props.setActiveTopic}
position="right"
/>
)}
</HStack>
</Container>
)
}
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;
`

View File

@ -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<Props> = ({ 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 (
<NavbarHeader className="home-navbar">
<HStack alignItems="center">
{showAssistants && (
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={handleToggleShowAssistants}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
)}
{!showAssistants && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
)}
{!showAssistants && (
<NavbarIcon onClick={onShowAssistantsDrawer} style={{ marginRight: 8 }}>
<Menu size={18} />
</NavbarIcon>
)}
<SelectModelButton assistant={assistant} />
</HStack>
<HStack alignItems="center" gap={8}>
<UpdateAppButton />
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NarrowIcon>
</Tooltip>
{topicPosition === 'right' && !showTopics && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => toggleShowTopics()}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
)}
{topicPosition === 'right' && showTopics && (
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => handleToggleShowTopics()}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
)}
</HStack>
</NavbarHeader>
)
}
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

View File

@ -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 (
<Container id="home-page">
<Navbar
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
position="left"
/>
<ContentContainer id="content-container">
{isLeftNavbar && (
<Navbar
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
position="left"
/>
)}
<ContentContainer id={isLeftNavbar ? 'content-container' : undefined}>
{showAssistants && (
<HomeTabs
activeAssistant={activeAssistant}
@ -113,7 +116,12 @@ const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
max-width: calc(100vw - var(--sidebar-width));
[navbar-position='left'] & {
max-width: calc(100vw - var(--sidebar-width));
}
[navbar-position='top'] & {
max-width: 100vw;
}
`
const ContentContainer = styled.div`

View File

@ -962,7 +962,10 @@ const Container = styled.div`
flex-direction: column;
position: relative;
z-index: 2;
padding: 0 24px 18px 24px;
padding: 0 18px 18px 18px;
[navbar-position='top'] & {
padding: 0 18px 10px 18px;
}
`
const InputBarContainer = styled.div`

View File

@ -12,6 +12,7 @@ import { Popover } from 'antd'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components'
import { useChatMaxWidth } from '../Chat'
import MessageItem from './Message'
import MessageGroupMenuBar from './MessageGroupMenuBar'
@ -219,11 +220,14 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
[isGrid, isGrouped, topic, multiModelMessageStyle, messages.length, selectedMessageId, gridPopoverTrigger]
)
const maxWidth = useChatMaxWidth()
return (
<MessageEditingProvider>
<GroupContainer
id={messages[0].askId ? `message-group-${messages[0].askId}` : undefined}
className={classNames([multiModelMessageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
className={classNames([multiModelMessageStyle, { 'multi-select-mode': isMultiSelectMode }])}
style={{ maxWidth }}>
<GridContainer
$count={messageLength}
$gridColumns={gridColumns}
@ -251,6 +255,9 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
}
const GroupContainer = styled.div`
[navbar-position='left'] & {
max-width: calc(100vw - var(--sidebar-width) - var(--assistants-width) - 20px);
}
&.horizontal,
&.grid {
padding: 4px 10px;

View File

@ -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;
}

View File

@ -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<Props> = ({ 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<Props> = ({ activeAssistant, setActiveAssistant, activeTo
dispatch(setNarrowMode(!narrowMode))
}
const onShowAssistantsDrawer = () => {
AssistantsDrawer.show({
activeAssistant,
setActiveAssistant,
activeTopic,
setActiveTopic
})
}
return (
<Navbar className="home-navbar">
{showAssistants && (
@ -104,32 +101,20 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
)}
<NavbarRight style={{ justifyContent: 'space-between', flex: 1 }} className="home-navbar-right">
<HStack alignItems="center">
{!showAssistants && !sidebarHideCooldown && (
<FloatingSidebar
activeAssistant={assistant}
setActiveAssistant={setActiveAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
position={'left'}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
</FloatingSidebar>
)}
{!showAssistants && sidebarHideCooldown && (
{!showAssistants && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}
onMouseOut={() => setSidebarHideCooldown(false)}>
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
)}
{!showAssistants && (
<NavbarIcon onClick={onShowAssistantsDrawer} style={{ marginRight: 8 }}>
<Menu size={18} />
</NavbarIcon>
)}
<SelectModelButton assistant={assistant} />
</HStack>
<HStack alignItems="center" gap={8}>
@ -144,23 +129,9 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
<i className="iconfont icon-icon-adaptive-width"></i>
</NarrowIcon>
</Tooltip>
{topicPosition === 'right' && !showTopics && !sidebarHideCooldown && (
<FloatingSidebar
activeAssistant={assistant}
setActiveAssistant={setActiveAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
position={'right'}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => toggleShowTopics()}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
</FloatingSidebar>
)}
{topicPosition === 'right' && !showTopics && sidebarHideCooldown && (
{topicPosition === 'right' && !showTopics && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => toggleShowTopics()} onMouseOut={() => setSidebarHideCooldown(false)}>
<NavbarIcon onClick={() => toggleShowTopics()}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>

View File

@ -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);
}
`

View File

@ -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<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
const Topics: FC<Props> = ({ 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<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
return assistant.topics
}, [assistant.topics, pinTopicsToTop])
const singlealone = topicPosition === 'right' && position === 'right'
return (
<DraggableList
className="topics-tab"
list={sortedTopics}
onUpdate={updateTopics}
style={{ padding: '13px 0 10px 10px' }}
itemContainerStyle={{ paddingBottom: '8px' }}>
itemContainerStyle={{ paddingBottom: '8px' }}
header={
<AddTopicButton onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
<PlusOutlined />
{t('chat.add.topic.title')}
</AddTopicButton>
}>
{(topic) => {
const isActive = topic.id === activeTopic?.id
const topicName = topic.name.replace('`', '')
@ -466,7 +476,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}>
<TopicListItem
onContextMenu={() => setTargetTopic(topic)}
className={isActive ? 'active' : ''}
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
onClick={() => onSwitchTopic(topic)}
style={{ borderRadius }}>
{isPending(topic.id) && !isActive && <PendingIndicator />}
@ -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;

View File

@ -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);
}
`

View File

@ -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<Props> = ({
const [tab, setTab] = useState<Tab>(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: <BotIcon size={16} />
}
const showTab = position === 'left' && topicPosition === 'left'
const onCreateAssistant = async () => {
const assistant = await AddAssistantPopup.show()
@ -97,41 +93,36 @@ const HomeTabs: FC<Props> = ({
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 (
<Container style={{ ...border, ...style }} className="home-tabs">
{(showTab || (forceToSeeAllTab == true && !showTopics)) && (
<>
<Segmented
value={tab}
style={{ borderRadius: 50 }}
shape="round"
options={
[
(position === 'left' && topicPosition === 'left') || (forceToSeeAllTab == true && position === 'left')
? assistantTab
: undefined,
{
label: t('common.topics'),
value: 'topic'
// icon: <MessageSquareQuote size={16} />
},
{
label: t('settings.title'),
value: 'settings'
// icon: <SettingsIcon size={16} />
}
].filter(Boolean) as SegmentedProps['options']
}
onChange={(value) => setTab(value as 'topic' | 'settings')}
block
/>
<Divider />
</>
{position === 'left' && topicPosition === 'left' && (
<CustomTabs>
<TabItem active={tab === 'assistants'} onClick={() => setTab('assistants')}>
{t('assistants.abbr')}
</TabItem>
<TabItem active={tab === 'topic'} onClick={() => setTab('topic')}>
{t('common.topics')}
</TabItem>
<TabItem active={tab === 'settings'} onClick={() => setTab('settings')}>
{t('settings.title')}
</TabItem>
</CustomTabs>
)}
{position === 'left' && topicPosition === 'right' && (
<CustomTabs>
<TabItem active={tab === 'assistants'} onClick={() => setTab('assistants')}>
{t('assistants.abbr')}
</TabItem>
<TabItem active={tab === 'settings'} onClick={() => setTab('settings')}>
{t('settings.title')}
</TabItem>
</CustomTabs>
)}
<TabContent className="home-tabs-content">
@ -144,7 +135,12 @@ const HomeTabs: FC<Props> = ({
/>
)}
{tab === 'topic' && (
<Topics assistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
<Topics
assistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
position={position}
/>
)}
{tab === 'settings' && <Settings assistant={activeAssistant} />}
</TabContent>
@ -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

View File

@ -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<Props> = ({
activeAssistant,
setActiveAssistant,
activeTopic,
setActiveTopic,
resolve
}) => {
const [open, setOpen] = useState(true)
const onClose = () => {
setOpen(false)
setTimeout(resolve, 300)
}
AssistantsDrawer.hide = onClose
return (
<Drawer
title={null}
height="100vh"
placement="left"
open={open}
onClose={onClose}
style={{ width: 'var(--assistants-width)' }}
styles={{
header: { display: 'none' },
body: {
display: 'flex',
padding: 0,
paddingTop: isMac ? 'var(--navbar-height)' : 0,
height: 'calc(100vh - var(--navbar-height))',
overflow: 'hidden'
}
}}>
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={(assistant) => {
setActiveAssistant(assistant)
onClose()
}}
setActiveTopic={(topic) => {
setActiveTopic(topic)
onClose()
}}
position="left"
/>
</Drawer>
)
}
const TopViewKey = 'AssistantsDrawer'
export default class AssistantsDrawer {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@ -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'

View File

@ -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: <LayoutGrid size={32} className="icon" />,
text: t('title.apps'),
path: '/apps',
bgColor: 'linear-gradient(135deg, #8B5CF6, #A855F7)' // 小程序:紫色,代表多功能和灵活性
},
{
icon: <FileSearch size={32} className="icon" />,
text: t('title.knowledge'),
path: '/knowledge',
bgColor: 'linear-gradient(135deg, #10B981, #34D399)' // 知识库:翠绿色,代表生长和知识
},
{
icon: <Palette size={32} className="icon" />,
text: t('title.paintings'),
path: `/paintings/${defaultPaintingProvider}`,
bgColor: 'linear-gradient(135deg, #EC4899, #F472B6)' // 绘画:活力粉色,代表创造力和艺术
},
{
icon: <Sparkle size={32} className="icon" />,
text: t('title.agents'),
path: '/agents',
bgColor: 'linear-gradient(135deg, #6366F1, #4F46E5)' // AI助手靛蓝渐变代表智能和科技
},
{
icon: <Languages size={32} className="icon" />,
text: t('title.translate'),
path: '/translate',
bgColor: 'linear-gradient(135deg, #06B6D4, #0EA5E9)' // 翻译:明亮的青蓝色,代表沟通和流畅
},
{
icon: <Folder size={32} className="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 (
<Container>
<Content>
<Section>
<SectionTitle>{t('launchpad.apps')}</SectionTitle>
<Grid>
{appMenuItems.map((item) => (
<AppIcon key={item.path} onClick={() => navigate(item.path)}>
<IconContainer>
<IconWrapper bgColor={item.bgColor}>{item.icon}</IconWrapper>
</IconContainer>
<AppName>{item.text}</AppName>
</AppIcon>
))}
</Grid>
</Section>
{sortedMinapps.length > 0 && (
<Section>
<SectionTitle>{t('launchpad.minapps')}</SectionTitle>
<Grid>
{sortedMinapps.map((app) => (
<AppWrapper key={app.id} onClick={() => setTimeout(() => tabsService.closeTab('launchpad'), 350)}>
<App app={app} size={56} />
</AppWrapper>
))}
</Grid>
</Section>
)}
</Content>
</Container>
)
}
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

View File

@ -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 (
<Container onContextMenu={handleContextMenu}>
<Navbar>
@ -60,26 +56,47 @@ const AppsPage: FC = () => {
suffix={<Search size={18} />}
value={search}
onChange={(e) => setSearch(e.target.value)}
disabled={isSettingsOpen}
/>
<Button
type="text"
className="nodrag"
icon={isSettingsOpen ? <X size={18} /> : <SettingsIcon size={18} color="var(--color-text-2)" />}
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
icon={<SettingsIcon size={18} color="var(--color-text-2)" />}
onClick={MinappSettingsPopup.show}
/>
</NavbarMain>
</Navbar>
<ContentContainer id="content-container">
{isSettingsOpen && <MiniAppSettings />}
{!isSettingsOpen && (
<AppsContainer style={{ height: containerHeight }}>
{filteredApps.map((app) => (
<App key={app.id} app={app} />
))}
<NewAppButton />
</AppsContainer>
)}
<MainContainer>
<RightContainer>
{isTopNavbar && (
<HeaderContainer>
<Input
placeholder={t('common.search')}
className="nodrag"
style={{ width: '30%', borderRadius: 15 }}
variant="filled"
suffix={<Search size={18} />}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<Button
type="text"
className="nodrag"
icon={<SettingsIcon size={18} color="var(--color-text-2)" />}
onClick={() => MinappSettingsPopup.show()}
/>
</HeaderContainer>
)}
<AppsContainerWrapper>
<AppsContainer style={{ height: containerHeight }}>
{filteredApps.map((app) => (
<App key={app.id} app={app} />
))}
<NewAppButton />
</AppsContainer>
</AppsContainerWrapper>
</RightContainer>
</MainContainer>
</ContentContainer>
</Container>
)
@ -98,8 +115,41 @@ const ContentContainer = styled.div`
flex-direction: row;
justify-content: center;
height: 100%;
overflow-y: auto;
padding: 50px;
`
const HeaderContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 60px;
width: 100%;
gap: 10px;
`
const MainContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
height: calc(100vh - var(--navbar-height));
`
const RightContainer = styled(Scrollbar)`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
align-items: center;
height: calc(100vh - var(--navbar-height));
`
const AppsContainerWrapper = styled(Scrollbar)`
display: flex;
flex: 1;
flex-direction: row;
justify-content: center;
padding: 20px 0;
width: 100%;
`
const AppsContainer = styled.div`

View File

@ -0,0 +1,66 @@
import { TopView } from '@renderer/components/TopView'
import { Modal } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import MiniAppSettings from './MiniAppSettings'
interface Props {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const onOk = () => {
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
MinappSettingsPopup.hide = onCancel
return (
<Modal
open={open}
onOk={onOk}
width="80vw"
title={t('settings.miniapps.display_title')}
onCancel={onCancel}
afterClose={onClose}
footer={null}
transitionName="animation-move-down"
centered>
<MiniAppSettings />
</Modal>
)
}
const TopViewKey = 'MinappSettingsPopup'
export default class MinappSettingsPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show() {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@ -12,7 +12,6 @@ import {
import { Button, message, Slider, Switch, Tooltip } from 'antd'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import MiniAppIconsManager from './MiniAppIconsManager'
@ -25,7 +24,6 @@ const MiniAppSettings: FC = () => {
const dispatch = useAppDispatch()
const { maxKeepAliveMinapps, showOpenedMinappsInSidebar, minappsOpenLinkExternal } = useSettings()
const { minapps, disabled, updateMinapps, updateDisabledMinapps } = useMinapps()
const navigate = useNavigate()
const [visibleMiniApps, setVisibleMiniApps] = useState(minapps)
const [disabledMiniApps, setDisabledMiniApps] = useState(disabled || [])
@ -80,9 +78,7 @@ const MiniAppSettings: FC = () => {
return (
<Container>
{contextHolder} {/* 添加消息上下文 */}
<SettingTitle
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{t('settings.miniapps.display_title')}</span>
<SettingTitle style={{ display: 'flex', flexDirection: 'row', justifyContent: 'flex-end', alignItems: 'center' }}>
<ButtonWrapper>
<Button onClick={handleSwapMinApps}>{t('common.swap')}</Button>
<Button onClick={handleResetMinApps}>{t('common.reset')}</Button>
@ -146,10 +142,6 @@ const MiniAppSettings: FC = () => {
onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ justifyContent: 'flex-end' }}>
<Button onClick={() => navigate('/apps')}>{t('common.close')}</Button>
</SettingRow>
</Container>
)
}
@ -158,6 +150,7 @@ const Container = styled.div`
display: flex;
flex-direction: column;
flex: 1;
padding-top: 10px;
`
// 修改和新增样式

View File

@ -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 = () => {
</>
)}
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle style={{ justifyContent: 'flex-start', gap: 5 }}>
{t('settings.display.navbar.title')} <TextBadge text="New" />
</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.display.navbar.position')}</SettingRowTitle>
<Segmented
value={navbarPosition}
shape="round"
onChange={setNavbarPosition}
options={[
{ label: t('settings.display.navbar.position.left'), value: 'left' },
{ label: t('settings.display.navbar.position.top'), value: 'top' }
]}
/>
</SettingRow>
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.display.zoom.title')}</SettingTitle>
<SettingDivider />
@ -286,22 +306,24 @@ const DisplaySettings: FC = () => {
/>
</SettingRow>
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{t('settings.display.sidebar.title')}</span>
<ResetButtonWrapper>
<Button onClick={handleReset}>{t('common.reset')}</Button>
</ResetButtonWrapper>
</SettingTitle>
<SettingDivider />
<SidebarIconsManager
visibleIcons={visibleIcons}
disabledIcons={disabledIcons}
setVisibleIcons={setVisibleIcons}
setDisabledIcons={setDisabledIcons}
/>
</SettingGroup>
{navbarPosition === 'left' && (
<SettingGroup theme={theme}>
<SettingTitle
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{t('settings.display.sidebar.title')}</span>
<ResetButtonWrapper>
<Button onClick={handleReset}>{t('common.reset')}</Button>
</ResetButtonWrapper>
</SettingTitle>
<SettingDivider />
<SidebarIconsManager
visibleIcons={visibleIcons}
disabledIcons={disabledIcons}
setVisibleIcons={setVisibleIcons}
setDisabledIcons={setDisabledIcons}
/>
</SettingGroup>
)}
<SettingGroup theme={theme}>
<SettingTitle>
{t('settings.display.custom.css')}

View File

@ -76,7 +76,6 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
return (
<Button
type="primary"
size="small"
variant="filled"
shape="circle"
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}

View File

@ -16,6 +16,7 @@ import { SettingTitle } from '..'
import AddMcpServerModal from './AddMcpServerModal'
import BuiltinMCPServersSection from './BuiltinMCPServersSection'
import EditMcpJsonPopup from './EditMcpJsonPopup'
import InstallNpxUv from './InstallNpxUv'
import McpResourcesSection from './McpResourcesSection'
import SyncServersPopup from './SyncServersPopup'
@ -118,6 +119,7 @@ const McpServersList: FC = () => {
<Button icon={<EditOutlined />} type="text" onClick={() => EditMcpJsonPopup.show()} shape="circle" />
</SettingTitle>
<ButtonGroup>
<InstallNpxUv mini />
<Dropdown
menu={{
items: [

View File

@ -12,6 +12,7 @@ import {
} from '@ant-design/icons'
import { loggerService } from '@logger'
import { HStack } from '@renderer/components/Layout'
import TextBadge from '@renderer/components/TextBadge'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useModel } from '@renderer/hooks/useModel'
import MemoryService from '@renderer/services/MemoryService'
@ -611,17 +612,7 @@ const MemorySettings = () => {
<HStack style={{ justifyContent: 'space-between', alignItems: 'center' }}>
<HStack style={{ alignItems: 'center', gap: '2px' }}>
<SettingRowTitle>{t('memory.global_memory')}</SettingRowTitle>
<span
style={{
fontSize: '12px',
color: 'var(--color-primary)',
background: 'var(--color-primary-bg)',
padding: '2px 6px',
borderRadius: '4px',
fontWeight: '500'
}}>
Beta
</span>
<TextBadge text="Beta" />
</HStack>
<HStack style={{ alignItems: 'center', gap: 10 }}>
<Switch checked={globalMemoryEnabled} onChange={handleGlobalMemoryToggle} />

View File

@ -26,7 +26,6 @@ import DataSettings from './DataSettings/DataSettings'
import DisplaySettings from './DisplaySettings/DisplaySettings'
import GeneralSettings from './GeneralSettings'
import MCPSettings from './MCPSettings'
import { McpSettingsNavbar } from './MCPSettings/McpSettingsNavbar'
import MemorySettings from './MemorySettings'
import ProvidersList from './ProviderSettings'
import QuickAssistantSettings from './QuickAssistantSettings'
@ -45,7 +44,6 @@ const SettingsPage: FC = () => {
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
{pathname.includes('/settings/mcp') && <McpSettingsNavbar />}
</Navbar>
<ContentContainer id="content-container">
<SettingMenus>
@ -79,18 +77,18 @@ const SettingsPage: FC = () => {
{t('settings.mcp.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/tool">
<MenuItem className={isRoute('/settings/tool')}>
<PencilRuler size={18} />
{t('settings.tool.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/memory">
<MenuItem className={isRoute('/settings/memory')}>
<Brain size={18} />
{t('memory.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/tool">
<MenuItem className={isRoute('/settings/tool')}>
<PencilRuler size={18} />
{t('settings.tool.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/shortcut">
<MenuItem className={isRoute('/settings/shortcut')}>
<Command size={18} />

View File

@ -555,17 +555,7 @@ const TranslatePage: FC = () => {
return (
<Container id="translate-page">
<Navbar>
<NavbarCenter style={{ borderRight: 'none', gap: 10 }}>
{t('translate.title')}
<Button
className="nodrag"
color="default"
variant={historyDrawerVisible ? 'filled' : 'text'}
type="text"
icon={<HistoryOutlined />}
onClick={() => setHistoryDrawerVisible(!historyDrawerVisible)}
/>
</NavbarCenter>
<NavbarCenter style={{ borderRight: 'none', gap: 10 }}>{t('translate.title')}</NavbarCenter>
</Navbar>
<ContentContainer id="content-container" ref={contentContainerRef} $historyDrawerVisible={historyDrawerVisible}>
<HistoryContainer $historyDrawerVisible={historyDrawerVisible}>
@ -626,7 +616,7 @@ const TranslatePage: FC = () => {
<InputContainer>
<OperationBar>
<Flex align="center" gap={20}>
<Flex align="center" gap={8}>
<Select
showSearch
value={sourceLanguage !== 'auto' ? sourceLanguage.langCode : 'auto'}
@ -663,6 +653,14 @@ const TranslatePage: FC = () => {
onClick={() => setSettingsVisible(true)}
style={{ color: 'var(--color-text-2)', display: 'flex' }}
/>
<Button
className="nodrag"
color="default"
variant={historyDrawerVisible ? 'filled' : 'text'}
type="text"
icon={<HistoryOutlined />}
onClick={() => setHistoryDrawerVisible(!historyDrawerVisible)}
/>
</Flex>
<Tooltip

View File

@ -0,0 +1,91 @@
import { loggerService } from '@logger'
import store from '@renderer/store'
import { removeTab, setActiveTab } from '@renderer/store/tabs'
import NavigationService from './NavigationService'
const logger = loggerService.withContext('TabsService')
class TabsService {
/**
*
* @param tabId ID
* @returns
*/
public closeTab(tabId: string): boolean {
const state = store.getState()
const tabs = state.tabs.tabs
const activeTabId = state.tabs.activeTabId
const tabToClose = tabs.find((tab) => tab.id === tabId)
if (!tabToClose) {
logger.warn(`Tab with id ${tabId} not found`)
return false
}
// 如果只有一个标签页,不允许关闭
if (tabs.length === 1) {
logger.warn('Cannot close the last tab')
return false
}
// 如果关闭的是当前激活的标签页,需要切换到其他标签页
if (tabId === activeTabId) {
const remainingTabs = tabs.filter((tab) => tab.id !== tabId)
const lastTab = remainingTabs[remainingTabs.length - 1]
// 使用 NavigationService 导航到新的标签页
if (NavigationService.navigate) {
NavigationService.navigate(lastTab.path)
} else {
logger.error('Navigation service is not initialized')
return false
}
}
// 使用 Redux action 移除标签页
store.dispatch(removeTab(tabId))
logger.info(`Tab ${tabId} closed successfully`)
return true
}
/**
*
*/
public getTabs() {
return store.getState().tabs.tabs
}
/**
* ID
*/
public getActiveTabId() {
return store.getState().tabs.activeTabId
}
/**
*
* @param tabId ID
*/
public setActiveTab(tabId: string): boolean {
const tabs = store.getState().tabs.tabs
const tab = tabs.find((t) => t.id === tabId)
if (!tab) {
logger.warn(`Tab with id ${tabId} not found`)
return false
}
store.dispatch(setActiveTab(tabId))
// 导航到对应页面
if (NavigationService.navigate) {
NavigationService.navigate(tab.path)
}
return true
}
}
export default new TabsService()

View File

@ -25,6 +25,7 @@ import runtime from './runtime'
import selectionStore from './selectionStore'
import settings from './settings'
import shortcuts from './shortcuts'
import tabs from './tabs'
import websearch from './websearch'
const rootReducer = combineReducers({
@ -45,6 +46,7 @@ const rootReducer = combineReducers({
memory,
copilot,
selectionStore,
tabs,
// messages: messagesReducer,
preprocess,
messages: newMessagesReducer,
@ -56,8 +58,8 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 121,
blacklist: ['runtime', 'messages', 'messageBlocks'],
version: 122,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate
},
rootReducer

View File

@ -1835,6 +1835,15 @@ const migrateConfig = {
logger.error('migrate 121 error', error as Error)
return state
}
},
'122': (state: RootState) => {
try {
state.settings.navbarPosition = 'left'
return state
} catch (error) {
logger.error('migrate 122 error', error as Error)
return state
}
}
}

View File

@ -200,6 +200,8 @@ export interface SettingsState {
s3: S3Config
// Developer mode
enableDeveloperMode: boolean
// UI
navbarPosition: 'left' | 'top'
}
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
@ -291,7 +293,7 @@ export const initialState: SettingsState = {
enableQuickAssistant: false,
clickTrayToShowQuickAssistant: false,
readClipboardAtStartup: true,
multiModelMessageStyle: 'fold',
multiModelMessageStyle: 'horizontal',
notionDatabaseID: '',
notionApiKey: '',
notionPageNameKey: 'Name',
@ -368,7 +370,9 @@ export const initialState: SettingsState = {
skipBackupFile: false
},
// Developer mode
enableDeveloperMode: false
enableDeveloperMode: false,
// UI
navbarPosition: 'left'
}
const settingsSlice = createSlice({
@ -763,6 +767,9 @@ const settingsSlice = createSlice({
},
setEnableDeveloperMode: (state, action: PayloadAction<boolean>) => {
state.enableDeveloperMode = action.payload
},
setNavbarPosition: (state, action: PayloadAction<'left' | 'top'>) => {
state.navbarPosition = action.payload
}
}
})
@ -882,7 +889,8 @@ export const {
setDefaultPaintingProvider,
setS3,
setS3Partial,
setEnableDeveloperMode
setEnableDeveloperMode,
setNavbarPosition
} = settingsSlice.actions
export default settingsSlice.reducer

View File

@ -0,0 +1,57 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
export interface Tab {
id: string
path: string
}
interface TabsState {
tabs: Tab[]
activeTabId: string
}
const initialState: TabsState = {
tabs: [
{
id: 'home',
path: '/'
}
],
activeTabId: 'home'
}
const tabsSlice = createSlice({
name: 'tabs',
initialState,
reducers: {
addTab: (state, action: PayloadAction<Tab>) => {
const existingTab = state.tabs.find((tab) => tab.path === action.payload.path)
if (!existingTab) {
state.tabs.push(action.payload)
}
state.activeTabId = action.payload.id
},
removeTab: (state, action: PayloadAction<string>) => {
const index = state.tabs.findIndex((tab) => tab.id === action.payload)
if (index !== -1) {
state.tabs.splice(index, 1)
// 如果关闭的是当前标签页,则切换到最后一个标签页
if (action.payload === state.activeTabId) {
state.activeTabId = state.tabs[state.tabs.length - 1].id
}
}
},
updateTab: (state, action: PayloadAction<{ id: string; updates: Partial<Tab> }>) => {
const tab = state.tabs.find((tab) => tab.id === action.payload.id)
if (tab) {
Object.assign(tab, action.payload.updates)
}
},
setActiveTab: (state, action: PayloadAction<string>) => {
state.activeTabId = action.payload
}
}
})
export const { addTab, removeTab, setActiveTab, updateTab } = tabsSlice.actions
export default tabsSlice.reducer