mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 00:10:22 +08:00
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:
parent
c2086fdb15
commit
75b8a5a6a7
@ -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'
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
57
src/renderer/src/Router.tsx
Normal file
57
src/renderer/src/Router.tsx
Normal 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
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
322
src/renderer/src/components/Tab/TabContainer.tsx
Normal file
322
src/renderer/src/components/Tab/TabContainer.tsx
Normal 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
|
||||
22
src/renderer/src/components/TextBadge.tsx
Normal file
22
src/renderer/src/components/TextBadge.tsx
Normal 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
|
||||
@ -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);
|
||||
`
|
||||
|
||||
367
src/renderer/src/components/app/PinnedMinapps.tsx
Normal file
367
src/renderer/src/components/app/PinnedMinapps.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
`
|
||||
|
||||
183
src/renderer/src/pages/home/ChatNavbar.tsx
Normal file
183
src/renderer/src/pages/home/ChatNavbar.tsx
Normal 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
|
||||
@ -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`
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
92
src/renderer/src/pages/home/components/AssistantsDrawer.tsx
Normal file
92
src/renderer/src/pages/home/components/AssistantsDrawer.tsx
Normal 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
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
|
||||
217
src/renderer/src/pages/launchpad/LaunchpadPage.tsx
Normal file
217
src/renderer/src/pages/launchpad/LaunchpadPage.tsx
Normal 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
|
||||
@ -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`
|
||||
@ -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
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
`
|
||||
|
||||
// 修改和新增样式
|
||||
@ -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')}
|
||||
|
||||
@ -76,7 +76,6 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
return (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
variant="filled"
|
||||
shape="circle"
|
||||
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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
|
||||
|
||||
91
src/renderer/src/services/TabsService.ts
Normal file
91
src/renderer/src/services/TabsService.ts
Normal 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()
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
57
src/renderer/src/store/tabs.ts
Normal file
57
src/renderer/src/store/tabs.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export interface Tab {
|
||||
id: string
|
||||
path: string
|
||||
}
|
||||
|
||||
interface TabsState {
|
||||
tabs: Tab[]
|
||||
activeTabId: string
|
||||
}
|
||||
|
||||
const initialState: TabsState = {
|
||||
tabs: [
|
||||
{
|
||||
id: 'home',
|
||||
path: '/'
|
||||
}
|
||||
],
|
||||
activeTabId: 'home'
|
||||
}
|
||||
|
||||
const tabsSlice = createSlice({
|
||||
name: 'tabs',
|
||||
initialState,
|
||||
reducers: {
|
||||
addTab: (state, action: PayloadAction<Tab>) => {
|
||||
const existingTab = state.tabs.find((tab) => tab.path === action.payload.path)
|
||||
if (!existingTab) {
|
||||
state.tabs.push(action.payload)
|
||||
}
|
||||
state.activeTabId = action.payload.id
|
||||
},
|
||||
removeTab: (state, action: PayloadAction<string>) => {
|
||||
const index = state.tabs.findIndex((tab) => tab.id === action.payload)
|
||||
if (index !== -1) {
|
||||
state.tabs.splice(index, 1)
|
||||
// 如果关闭的是当前标签页,则切换到最后一个标签页
|
||||
if (action.payload === state.activeTabId) {
|
||||
state.activeTabId = state.tabs[state.tabs.length - 1].id
|
||||
}
|
||||
}
|
||||
},
|
||||
updateTab: (state, action: PayloadAction<{ id: string; updates: Partial<Tab> }>) => {
|
||||
const tab = state.tabs.find((tab) => tab.id === action.payload.id)
|
||||
if (tab) {
|
||||
Object.assign(tab, action.payload.updates)
|
||||
}
|
||||
},
|
||||
setActiveTab: (state, action: PayloadAction<string>) => {
|
||||
state.activeTabId = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { addTab, removeTab, setActiveTab, updateTab } = tabsSlice.actions
|
||||
export default tabsSlice.reducer
|
||||
Loading…
Reference in New Issue
Block a user