refactor(App): replace TabsContainer with AppLayout for improved layout structure; integrate ChatProvider for chat context management; update routing logic in NavigationHandler to utilize SettingsPopup; adjust main height variable in styles for consistency; enhance FilesPage layout by repositioning SideNav; implement new SettingsPopup component for settings management.

This commit is contained in:
suyao 2025-07-02 11:50:41 +08:00
parent bedea8aaaa
commit db1c03f9fa
No known key found for this signature in database
12 changed files with 320 additions and 79 deletions

View File

@ -5,7 +5,7 @@ import { Provider } from 'react-redux'
import { HashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import TabsContainer from './components/Tabs/TabsContainer'
import AppLayout from './components/Layout/AppLayout'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider'
@ -13,6 +13,7 @@ import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider'
import NavigationHandler from './handler/NavigationHandler'
import { ChatProvider } from './hooks/useChat'
import Routes from './Routes'
function App(): React.ReactElement {
@ -24,14 +25,16 @@ function App(): React.ReactElement {
<NotificationProvider>
<CodeStyleProvider>
<PersistGate loading={null} persistor={persistor}>
<TopViewContainer>
<HashRouter>
<HashRouter>
<TopViewContainer>
<NavigationHandler />
<TabsContainer>
<Routes />
</TabsContainer>
</HashRouter>
</TopViewContainer>
<ChatProvider>
<AppLayout>
<Routes />
</AppLayout>
</ChatProvider>
</TopViewContainer>
</HashRouter>
</PersistGate>
</CodeStyleProvider>
</NotificationProvider>

View File

@ -5,10 +5,8 @@ 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 LaunchpadPage from './pages/launchpad/LaunchpadPage'
import McpServersPage from './pages/mcp-servers'
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
const RouteContainer = () => {
@ -22,8 +20,8 @@ const RouteContainer = () => {
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/mcp-servers/*" element={<McpServersPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} />
{/* <Route path="/settings/*" element={<SettingsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} /> */}
</Routes>
)
}

View File

@ -12,7 +12,7 @@
--list-item-border-radius: 15px;
--border-width: 0.5px;
--main-height: calc(100vh - var(--navbar-height) - 6px);
--main-height: 100vh;
--border-width: 0.5px;
}

View File

@ -0,0 +1,27 @@
import { HStack } from '@renderer/components/Layout'
import MainSidebar from '@renderer/pages/home/MainSidebar/MainSidebar'
import { FC } from 'react'
import styled from 'styled-components'
interface AppLayoutProps {
children: React.ReactNode
}
const AppLayout: FC<AppLayoutProps> = ({ children }) => {
return (
<HStack style={{ display: 'flex', flex: 1 }} id="app-layout">
<MainSidebar />
<ContentArea>{children}</ContentArea>
</HStack>
)
}
const ContentArea = styled.div`
min-width: 0;
display: flex;
flex: 1;
flex-direction: column;
height: 100vh;
`
export default AppLayout

View File

@ -0,0 +1,230 @@
import AboutSettings from '@renderer/pages/settings/AboutSettings'
import DataSettings from '@renderer/pages/settings/DataSettings/DataSettings'
import DisplaySettings from '@renderer/pages/settings/DisplaySettings/DisplaySettings'
import GeneralSettings from '@renderer/pages/settings/GeneralSettings'
import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings'
import ProvidersList from '@renderer/pages/settings/ProviderSettings'
import QuickAssistantSettings from '@renderer/pages/settings/QuickAssistantSettings'
import QuickPhraseSettings from '@renderer/pages/settings/QuickPhraseSettings'
import SelectionAssistantSettings from '@renderer/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings'
import ShortcutSettings from '@renderer/pages/settings/ShortcutSettings'
import WebSearchSettings from '@renderer/pages/settings/WebSearchSettings'
import { Modal, Spin } from 'antd'
import {
Cloud,
Command,
Globe,
HardDrive,
Info,
MonitorCog,
Package,
Rocket,
Settings2,
TextCursorInput,
Zap
} from 'lucide-react'
import React, { Suspense, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { TopView } from '../TopView'
type SettingsTab =
| 'provider'
| 'model'
| 'web-search'
| 'general'
| 'display'
| 'shortcut'
| 'quickAssistant'
| 'selectionAssistant'
| 'data'
| 'about'
| 'quickPhrase'
interface SettingsPopupShowParams {
defaultTab?: SettingsTab
}
interface Props extends SettingsPopupShowParams {
resolve?: (value: any) => void
}
const SettingsPopupContainer: React.FC<Props> = ({ defaultTab = 'provider', resolve }) => {
const { t } = useTranslation()
const [activeTab, setActiveTab] = useState<SettingsTab>(defaultTab)
const [open, setOpen] = useState(true)
const menuItems = [
{ key: 'provider', icon: <Cloud size={18} />, label: t('settings.provider.title') },
{ key: 'model', icon: <Package size={18} />, label: t('settings.model') },
{ key: 'web-search', icon: <Globe size={18} />, label: t('settings.websearch.title') },
{ key: 'general', icon: <Settings2 size={18} />, label: t('settings.general') },
{ key: 'display', icon: <MonitorCog size={18} />, label: t('settings.display.title') },
{ key: 'shortcut', icon: <Command size={18} />, label: t('settings.shortcuts.title') },
{ key: 'quickAssistant', icon: <Rocket size={18} />, label: t('settings.quickAssistant.title') },
{ key: 'selectionAssistant', icon: <TextCursorInput size={18} />, label: t('selection.name') },
{ key: 'quickPhrase', icon: <Zap size={18} />, label: t('settings.quickPhrase.title') },
{ key: 'data', icon: <HardDrive size={18} />, label: t('settings.data.title') },
{ key: 'about', icon: <Info size={18} />, label: t('settings.about') }
] as const
const renderContent = () => {
switch (activeTab) {
case 'provider':
return (
<Suspense fallback={<Spin />}>
<ProvidersList />
</Suspense>
)
case 'model':
return <ModelSettings />
case 'web-search':
return <WebSearchSettings />
case 'general':
return <GeneralSettings />
case 'display':
return <DisplaySettings />
case 'shortcut':
return <ShortcutSettings />
case 'quickAssistant':
return <QuickAssistantSettings />
case 'selectionAssistant':
return <SelectionAssistantSettings />
case 'data':
return <DataSettings />
case 'about':
return <AboutSettings />
case 'quickPhrase':
return <QuickPhraseSettings />
default:
return <ProvidersList />
}
}
const onCancel = () => {
setOpen(false)
}
const onAfterClose = () => {
resolve && resolve(null)
TopView.hide(TopViewKey)
}
// 设置全局隐藏方法
SettingsPopup.hide = onCancel
return (
<StyledModal
title={t('settings.title')}
open={open}
onCancel={onCancel}
afterClose={onAfterClose}
footer={null}
width={1000}
centered
destroyOnClose>
<ContentContainer>
<SettingMenus>
{menuItems.map((item) => (
<MenuItem
key={item.key}
className={activeTab === item.key ? 'active' : ''}
onClick={() => setActiveTab(item.key as SettingsTab)}>
{item.icon}
{item.label}
</MenuItem>
))}
</SettingMenus>
<SettingContent>{renderContent()}</SettingContent>
</ContentContainer>
</StyledModal>
)
}
const TopViewKey = 'SettingsPopup'
export default class SettingsPopup {
static hide() {
TopView.hide(TopViewKey)
}
static show(props: SettingsPopupShowParams = {}) {
return new Promise<any>((resolve) => {
TopView.show(<SettingsPopupContainer {...props} resolve={resolve} />, TopViewKey)
})
}
}
const StyledModal = styled(Modal)`
.ant-modal-content {
height: 80vh;
display: flex;
flex-direction: column;
}
.ant-modal-body {
flex: 1;
padding: 0;
overflow: hidden;
}
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
height: 100%;
`
const SettingMenus = styled.div`
display: flex;
flex-direction: column;
min-width: var(--settings-width);
border-right: 0.5px solid var(--color-border);
padding: 10px;
user-select: none;
background: var(--color-background);
`
const MenuItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 6px 10px;
width: 100%;
cursor: pointer;
border-radius: var(--list-item-border-radius);
font-weight: 500;
transition: all 0.2s ease-in-out;
border: 0.5px solid transparent;
margin-bottom: 5px;
.anticon {
font-size: 16px;
opacity: 0.8;
}
.iconfont {
font-size: 18px;
line-height: 18px;
opacity: 0.7;
margin-left: -1px;
}
&:hover {
background: var(--color-background-soft);
}
&.active {
background: var(--color-background-soft);
border: 0.5px solid var(--color-border);
}
`
const SettingContent = styled.div`
display: flex;
height: 100%;
flex: 1;
overflow: auto;
`

View File

@ -1,15 +1,12 @@
import SettingsPopup from '@renderer/components/Popups/SettingsPopup'
import NavigationService from '@renderer/services/NavigationService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { addTab, Tab } from '@renderer/store/tabs'
import { useAppSelector } from '@renderer/store'
import { useEffect } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useLocation, useNavigate } from 'react-router-dom'
import { useNavigate } from 'react-router-dom'
const NavigationHandler: React.FC = () => {
const dispatch = useAppDispatch()
const location = useLocation()
const navigate = useNavigate()
const tabs = useAppSelector((state) => state.tabs.tabs)
const showSettingsShortcutEnabled = useAppSelector(
(state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled
@ -22,10 +19,7 @@ const NavigationHandler: React.FC = () => {
useHotkeys(
'meta+, ! ctrl+,',
function () {
if (location.pathname.startsWith('/settings')) {
return
}
navigate('/settings/provider')
SettingsPopup.show({ defaultTab: 'provider' })
},
{
splitKey: '!',
@ -35,20 +29,6 @@ const NavigationHandler: React.FC = () => {
}
)
// 初始化 home tab
useEffect(() => {
if (tabs.length === 0) {
const homeTab: Tab = {
id: 'home',
titleKey: 'title.home',
title: '',
path: '/',
iconType: 'home'
}
dispatch(addTab(homeTab))
}
}, [dispatch, tabs.length])
return null
}

View File

@ -1,10 +1,9 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant } from '@renderer/types'
import { Topic } from '@renderer/types'
import { use, useEffect, useMemo, useState } from 'react'
import { createContext } from 'react'
import { Assistant, Topic } from '@renderer/types'
import { createContext, use, useCallback, useEffect, useMemo, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useTopicsForAssistant } from './useAssistant'
import { useSettings } from './useSettings'
@ -20,14 +19,25 @@ const ChatContext = createContext<ChatContextType | null>(null)
export const ChatProvider = ({ children }) => {
const assistants = useAppSelector((state) => state.assistants.assistants)
const [activeAssistant, setActiveAssistant] = useState<Assistant>(assistants[0])
const [activeAssistant, setActiveAssistantBase] = useState<Assistant>(assistants[0])
const topics = useTopicsForAssistant(activeAssistant.id)
const [activeTopic, setActiveTopic] = useState<Topic>(topics[0])
const { clickAssistantToShowTopic } = useSettings()
const dispatch = useAppDispatch()
const navigate = useNavigate()
const location = useLocation()
console.log('activeAssistant', activeAssistant)
console.log('activeTopic', activeTopic)
// 包装setActiveAssistant以添加导航逻辑
const setActiveAssistant = useCallback(
(assistant: Assistant) => {
setActiveAssistantBase(assistant)
// 如果当前不在聊天页面,导航到聊天页面
if (location.pathname !== '/') {
navigate('/')
}
},
[setActiveAssistantBase, location.pathname, navigate]
)
// 当 topics 变化时,如果当前 activeTopic 不在 topics 中,设置第一个 topic
useEffect(() => {
@ -58,7 +68,7 @@ export const ChatProvider = ({ children }) => {
EventEmitter.on(EVENT_NAMES.SET_TOPIC, setActiveTopic)
]
return () => subscriptions.forEach((subscription) => subscription())
}, [])
}, [setActiveAssistant])
const value = useMemo(
() => ({
@ -67,7 +77,7 @@ export const ChatProvider = ({ children }) => {
setActiveAssistant,
setActiveTopic
}),
[activeAssistant, activeTopic]
[activeAssistant, activeTopic, setActiveAssistant]
)
return <ChatContext value={value}>{children}</ChatContext>

View File

@ -134,17 +134,6 @@ const FilesPage: FC = () => {
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
</NavbarMain>
<ContentContainer id="content-container">
<SideNav>
{menuItems.map((item) => (
<ListItem
key={item.key}
icon={item.icon}
title={item.label}
active={fileType === item.key}
onClick={() => setFileType(item.key as FileTypes)}
/>
))}
</SideNav>
<MainContent>
<SortContainer>
<Flex gap={8} align="center">
@ -207,6 +196,17 @@ const FilesPage: FC = () => {
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</MainContent>
<SideNav>
{menuItems.map((item) => (
<ListItem
key={item.key}
icon={item.icon}
title={item.label}
active={fileType === item.key}
onClick={() => setFileType(item.key as FileTypes)}
/>
))}
</SideNav>
</ContentContainer>
</Container>
)

View File

@ -122,7 +122,7 @@ const Chat: FC = () => {
}
const Main = styled(Flex)`
height: calc(100vh - var(--navbar-height) - 50px);
height: calc(100vh - var(--navbar-height));
transform: translateZ(0);
position: relative;
`

View File

@ -1,12 +1,9 @@
import { HStack } from '@renderer/components/Layout'
import { ChatProvider } from '@renderer/hooks/useChat'
import { useSettings } from '@renderer/hooks/useSettings'
import { FC, useEffect } from 'react'
import styled from 'styled-components'
import Chat from './Chat'
import ChatNavbar from './ChatNavbar'
import MainSidebar from './MainSidebar/MainSidebar'
const HomePage: FC<{ style?: React.CSSProperties }> = ({ style }) => {
const { showAssistants, showTopics, topicPosition } = useSettings()
@ -20,17 +17,12 @@ const HomePage: FC<{ style?: React.CSSProperties }> = ({ style }) => {
}, [showAssistants, showTopics, topicPosition])
return (
<ChatProvider>
<HStack style={{ display: 'flex', flex: 1 }} id="home-page">
<MainSidebar />
<Container style={style}>
<ChatNavbar />
<ContentContainer id="content-container">
<Chat />
</ContentContainer>
</Container>
</HStack>
</ChatProvider>
<Container style={style}>
<ChatNavbar />
<ContentContainer id="content-container">
<Chat />
</ContentContainer>
</Container>
)
}

View File

@ -56,6 +56,7 @@ export const Container = styled.div<{ transparent?: boolean }>`
min-height: var(--main-height);
background: var(--color-background);
padding-top: 10px;
margin-top: 50px;
`
export const MainMenu = styled.div`

View File

@ -96,6 +96,13 @@ const KnowledgePage: FC = () => {
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter>
</NavbarMain>
<ContentContainer id="content-container">
{bases.length === 0 ? (
<MainContent>
<Empty description={t('knowledge.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
</MainContent>
) : selectedBase ? (
<KnowledgeContent selectedBase={selectedBase} />
) : null}
<SideNav>
<ScrollContainer>
<DragableList
@ -128,13 +135,6 @@ const KnowledgePage: FC = () => {
<div style={{ minHeight: '10px' }}></div>
</ScrollContainer>
</SideNav>
{bases.length === 0 ? (
<MainContent>
<Empty description={t('knowledge.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
</MainContent>
) : selectedBase ? (
<KnowledgeContent selectedBase={selectedBase} />
) : null}
</ContentContainer>
</Container>
)