diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts
index 9fdfa69c0e..7ed15a721a 100644
--- a/packages/shared/IpcChannel.ts
+++ b/packages/shared/IpcChannel.ts
@@ -246,5 +246,8 @@ export enum IpcChannel {
// Navigation
Navigation_Url = 'navigation:url',
- Navigation_Close = 'navigation:close'
+ Navigation_Close = 'navigation:close',
+
+ // Settings Window
+ SettingsWindow_Show = 'settings-window:show'
}
diff --git a/src/main/ipc.ts b/src/main/ipc.ts
index 86e9cc0fe3..b0c2bfb572 100644
--- a/src/main/ipc.ts
+++ b/src/main/ipc.ts
@@ -31,6 +31,7 @@ import { pythonService } from './services/PythonService'
import { FileServiceManager } from './services/remotefile/FileServiceManager'
import { searchService } from './services/SearchService'
import { SelectionService } from './services/SelectionService'
+import { SettingsWindowService } from './services/SettingsWindowService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import storeSyncService from './services/StoreSyncService'
import { themeService } from './services/ThemeService'
@@ -583,4 +584,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Navigation_Url, (_, url: string) => {
CacheService.set('navigation-url', url)
})
+
+ // Settings Window
+ SettingsWindowService.registerIpcHandler()
}
diff --git a/src/main/services/SettingsWindowService.ts b/src/main/services/SettingsWindowService.ts
new file mode 100644
index 0000000000..8b2c39b6f4
--- /dev/null
+++ b/src/main/services/SettingsWindowService.ts
@@ -0,0 +1,149 @@
+import { is } from '@electron-toolkit/utils'
+import { isLinux, isMac } from '@main/constant'
+import { IpcChannel } from '@shared/IpcChannel'
+import { BrowserWindow, nativeTheme } from 'electron'
+import { join } from 'path'
+
+import icon from '../../../build/icon.png?asset'
+import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
+
+export class SettingsWindowService {
+ private static instance: SettingsWindowService | null = null
+ private settingsWindow: BrowserWindow | null = null
+
+ public static getInstance(): SettingsWindowService {
+ if (!SettingsWindowService.instance) {
+ SettingsWindowService.instance = new SettingsWindowService()
+ }
+ return SettingsWindowService.instance
+ }
+
+ public createSettingsWindow(defaultTab?: string): BrowserWindow {
+ if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
+ this.settingsWindow.show()
+ this.settingsWindow.focus()
+ return this.settingsWindow
+ }
+
+ this.settingsWindow = new BrowserWindow({
+ width: 1000,
+ height: 700,
+ minWidth: 900,
+ minHeight: 600,
+ show: false,
+ autoHideMenuBar: true,
+ transparent: false,
+ vibrancy: 'sidebar',
+ visualEffectState: 'active',
+ titleBarStyle: 'hidden',
+ titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
+ backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
+ darkTheme: nativeTheme.shouldUseDarkColors,
+ trafficLightPosition: { x: 12, y: 12 },
+ ...(isLinux ? { icon } : {}),
+ webPreferences: {
+ preload: join(__dirname, '../preload/index.js'),
+ sandbox: false,
+ webSecurity: false,
+ webviewTag: true,
+ allowRunningInsecureContent: true,
+ backgroundThrottling: false
+ }
+ })
+
+ this.setupSettingsWindow()
+ this.loadSettingsWindowContent(defaultTab)
+
+ return this.settingsWindow
+ }
+
+ private setupSettingsWindow() {
+ if (!this.settingsWindow) return
+
+ this.settingsWindow.on('ready-to-show', () => {
+ this.settingsWindow?.show()
+ })
+
+ this.settingsWindow.on('closed', () => {
+ this.settingsWindow = null
+ })
+
+ this.settingsWindow.on('close', () => {
+ // Clean up when window is closed
+ })
+
+ // Handle theme changes
+ nativeTheme.on('updated', () => {
+ if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
+ this.settingsWindow.setTitleBarOverlay(
+ nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight
+ )
+ }
+ })
+ }
+
+ private loadSettingsWindowContent(defaultTab?: string) {
+ if (!this.settingsWindow) return
+
+ const queryParam = defaultTab ? `?tab=${defaultTab}` : ''
+
+ if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
+ this.settingsWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/settingsWindow.html' + queryParam)
+ } else {
+ this.settingsWindow.loadFile(join(__dirname, '../renderer/settingsWindow.html'))
+ if (defaultTab) {
+ this.settingsWindow.webContents.once('did-finish-load', () => {
+ this.settingsWindow?.webContents.send(IpcChannel.SettingsWindow_Show, { defaultTab })
+ })
+ }
+ }
+ }
+
+ public showSettingsWindow(defaultTab?: string) {
+ if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
+ if (this.settingsWindow.isMinimized()) {
+ this.settingsWindow.restore()
+ }
+
+ if (!isLinux) {
+ this.settingsWindow.setVisibleOnAllWorkspaces(true)
+ }
+
+ this.settingsWindow.show()
+ this.settingsWindow.focus()
+
+ if (!isLinux) {
+ this.settingsWindow.setVisibleOnAllWorkspaces(false)
+ }
+ } else {
+ this.createSettingsWindow(defaultTab)
+ }
+ }
+
+ public hideSettingsWindow() {
+ if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
+ this.settingsWindow.hide()
+ }
+ }
+
+ public closeSettingsWindow() {
+ if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
+ this.settingsWindow.close()
+ }
+ }
+
+ public getSettingsWindow(): BrowserWindow | null {
+ return this.settingsWindow
+ }
+
+ public static registerIpcHandler() {
+ const { ipcMain } = require('electron')
+ const service = SettingsWindowService.getInstance()
+
+ ipcMain.handle(IpcChannel.SettingsWindow_Show, (_, options?: { defaultTab?: string }) => {
+ service.showSettingsWindow(options?.defaultTab)
+ })
+ }
+}
+
+export const settingsWindowService = SettingsWindowService.getInstance()
diff --git a/src/main/services/ShortcutService.ts b/src/main/services/ShortcutService.ts
index 24ea2324fd..d944119859 100644
--- a/src/main/services/ShortcutService.ts
+++ b/src/main/services/ShortcutService.ts
@@ -5,10 +5,12 @@ import Logger from 'electron-log'
import { configManager } from './ConfigManager'
import selectionService from './SelectionService'
+import { settingsWindowService } from './SettingsWindowService'
import { windowService } from './WindowService'
let showAppAccelerator: string | null = null
let showMiniWindowAccelerator: string | null = null
+let showSettingsAccelerator: string | null = null
let selectionAssistantToggleAccelerator: string | null = null
let selectionAssistantSelectTextAccelerator: string | null = null
@@ -26,6 +28,10 @@ function getShortcutHandler(shortcut: Shortcut) {
return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
case 'zoom_reset':
return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
+ case 'show_settings':
+ return () => {
+ settingsWindowService.showSettingsWindow()
+ }
case 'show_app':
return () => {
windowService.toggleMainWindow()
@@ -146,9 +152,13 @@ export function registerShortcuts(window: BrowserWindow) {
// only register universal shortcuts when needed
if (
onlyUniversalShortcuts &&
- !['show_app', 'mini_window', 'selection_assistant_toggle', 'selection_assistant_select_text'].includes(
- shortcut.key
- )
+ ![
+ 'show_app',
+ 'mini_window',
+ 'show_settings',
+ 'selection_assistant_toggle',
+ 'selection_assistant_select_text'
+ ].includes(shortcut.key)
) {
return
}
@@ -171,6 +181,10 @@ export function registerShortcuts(window: BrowserWindow) {
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
break
+ case 'show_settings':
+ showSettingsAccelerator = formatShortcutKey(shortcut.shortcut)
+ break
+
case 'selection_assistant_toggle':
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
break
@@ -222,6 +236,12 @@ export function registerShortcuts(window: BrowserWindow) {
handler && globalShortcut.register(accelerator, () => handler(window))
}
+ if (showSettingsAccelerator) {
+ const handler = getShortcutHandler({ key: 'show_settings' } as Shortcut)
+ const accelerator = convertShortcutFormat(showSettingsAccelerator)
+ handler && globalShortcut.register(accelerator, () => handler(window))
+ }
+
if (selectionAssistantToggleAccelerator) {
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
@@ -258,6 +278,7 @@ export function unregisterAllShortcuts() {
try {
showAppAccelerator = null
showMiniWindowAccelerator = null
+ showSettingsAccelerator = null
selectionAssistantToggleAccelerator = null
selectionAssistantSelectTextAccelerator = null
windowOnHandlers.forEach((handlers, window) => {
diff --git a/src/preload/index.ts b/src/preload/index.ts
index d420e693dd..62505b8d4b 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -322,7 +322,21 @@ const api = {
url: (url: string) => ipcRenderer.invoke(IpcChannel.Navigation_Url, url)
},
setDisableHardwareAcceleration: (isDisable: boolean) =>
- ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable)
+ ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable),
+
+ // Settings Window
+ showSettingsWindow: (options?: { defaultTab?: string }) =>
+ ipcRenderer.invoke(IpcChannel.SettingsWindow_Show, options),
+
+ on: (channel: string, func: any) => {
+ const listener = (_event: Electron.IpcRendererEvent, ...args: any[]) => {
+ func(...args)
+ }
+ ipcRenderer.on(channel, listener)
+ return () => {
+ ipcRenderer.off(channel, listener)
+ }
+ }
}
// Use `contextBridge` APIs to expose Electron APIs to
diff --git a/src/renderer/settingsWindow.html b/src/renderer/settingsWindow.html
new file mode 100644
index 0000000000..2974290358
--- /dev/null
+++ b/src/renderer/settingsWindow.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Cherry Studio
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/renderer/src/components/Popups/SettingsPopup.tsx b/src/renderer/src/components/Popups/SettingsPopup.tsx
index a4bd356999..6f16c0d51b 100644
--- a/src/renderer/src/components/Popups/SettingsPopup.tsx
+++ b/src/renderer/src/components/Popups/SettingsPopup.tsx
@@ -1,230 +1,24 @@
-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
+export interface SettingsPopupShowParams {
+ defaultTab?:
+ | 'provider'
+ | 'model'
+ | 'tool'
+ | 'general'
+ | 'display'
+ | 'shortcut'
+ | 'quickAssistant'
+ | 'selectionAssistant'
+ | 'data'
+ | 'about'
+ | 'quickPhrase'
}
-interface Props extends SettingsPopupShowParams {
- resolve?: (value: any) => void
-}
-
-const SettingsPopupContainer: React.FC = ({ defaultTab = 'provider', resolve }) => {
- const { t } = useTranslation()
- const [activeTab, setActiveTab] = useState(defaultTab)
- const [open, setOpen] = useState(true)
-
- const menuItems = [
- { key: 'provider', icon: , label: t('settings.provider.title') },
- { key: 'model', icon: , label: t('settings.model') },
- { key: 'web-search', icon: , label: t('settings.websearch.title') },
- { key: 'general', icon: , label: t('settings.general') },
- { key: 'display', icon: , label: t('settings.display.title') },
- { key: 'shortcut', icon: , label: t('settings.shortcuts.title') },
- { key: 'quickAssistant', icon: , label: t('settings.quickAssistant.title') },
- { key: 'selectionAssistant', icon: , label: t('selection.name') },
- { key: 'quickPhrase', icon: , label: t('settings.quickPhrase.title') },
- { key: 'data', icon: , label: t('settings.data.title') },
- { key: 'about', icon: , label: t('settings.about') }
- ] as const
-
- const renderContent = () => {
- switch (activeTab) {
- case 'provider':
- return (
- }>
-
-
- )
- case 'model':
- return
- case 'web-search':
- return
- case 'general':
- return
- case 'display':
- return
- case 'shortcut':
- return
- case 'quickAssistant':
- return
- case 'selectionAssistant':
- return
- case 'data':
- return
- case 'about':
- return
- case 'quickPhrase':
- return
- default:
- return
- }
- }
-
- const onCancel = () => {
- setOpen(false)
- }
-
- const onAfterClose = () => {
- resolve && resolve(null)
- TopView.hide(TopViewKey)
- }
-
- // 设置全局隐藏方法
- SettingsPopup.hide = onCancel
-
- return (
-
-
-
- {menuItems.map((item) => (
-
- ))}
-
- {renderContent()}
-
-
- )
-}
-
-const TopViewKey = 'SettingsPopup'
-
export default class SettingsPopup {
static hide() {
- TopView.hide(TopViewKey)
+ // Settings window is now independent, user can close it manually
}
static show(props: SettingsPopupShowParams = {}) {
- return new Promise((resolve) => {
- TopView.show(, TopViewKey)
- })
+ return window.api.showSettingsWindow({ defaultTab: props.defaultTab })
}
}
-
-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;
-`
diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx
index 73f6cbcfa1..d47431b073 100644
--- a/src/renderer/src/components/app/Navbar.tsx
+++ b/src/renderer/src/components/app/Navbar.tsx
@@ -1,5 +1,6 @@
-import { isLinux, isWin } from '@renderer/config/constant'
+import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
+import { useShowAssistants } from '@renderer/hooks/useStore'
import type { FC, HTMLAttributes, PropsWithChildren } from 'react'
import styled from 'styled-components'
@@ -24,9 +25,10 @@ export const NavbarRight: FC = ({ children, ...props }) => {
export const NavbarMain: FC = ({ children, ...props }) => {
const isFullscreen = useFullscreen()
+ const { showAssistants } = useShowAssistants()
return (
-
+
{children}
)
@@ -51,7 +53,7 @@ const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
justify-content: flex-end;
`
-const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
+const NavbarMainContainer = styled.div<{ $isFullscreen: boolean; $showAssistants: boolean }>`
flex: 1;
display: flex;
flex-direction: row;
@@ -65,6 +67,7 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
color: var(--color-text-1);
-webkit-app-region: drag;
padding: 0 12px;
+ padding-left: ${({ $showAssistants }) => (isMac && !$showAssistants ? '70px' : '10px')};
`
const NavbarCenterContainer = styled.div`
diff --git a/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx
index d52f586df5..cb0aa01ff6 100644
--- a/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx
+++ b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx
@@ -266,7 +266,7 @@ const MainSidebar: FC = () => {
key: 'settings',
label: t('settings.title'),
icon: ,
- onClick: () => navigate('/settings/provider')
+ onClick: () => window.api.showSettingsWindow({ defaultTab: 'provider' })
}
]
}}>
diff --git a/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx b/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx
index 6b42f4120f..d3a4eeebba 100644
--- a/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx
+++ b/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx
@@ -52,8 +52,8 @@ export const Container = styled.div<{ transparent?: boolean }>`
width: var(--assistants-width);
max-width: var(--assistants-width);
border-right: 0.5px solid var(--color-border);
- height: var(--main-height);
- min-height: var(--main-height);
+ height: calc(var(--main-height) - 50px);
+ min-height: calc(var(--main-height) - 50px);
background: var(--color-background);
padding-top: 10px;
margin-top: 50px;
diff --git a/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx b/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx
index 8aa94d5455..c26c610246 100644
--- a/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx
+++ b/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx
@@ -1,4 +1,4 @@
-import DragableList from '@renderer/components/DragableList'
+import { DraggableList } from '@renderer/components/DraggableList'
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
import IndicatorLight from '@renderer/components/IndicatorLight'
import { Center } from '@renderer/components/Layout'
@@ -84,7 +84,7 @@ const OpenedMinapps: FC = () => {
- {
// 只更新固定应用的顺序
@@ -144,7 +144,7 @@ const OpenedMinapps: FC = () => {
)
}}
-
+
{isEmpty(sortedApps) && (
diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx
index d524a7bcba..c9609341e3 100644
--- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx
+++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx
@@ -42,6 +42,7 @@ import { findIndex } from 'lodash'
import { FC, startTransition, useCallback, useDeferredValue, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
+import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
interface TopicsTabProps {
@@ -49,12 +50,14 @@ interface TopicsTabProps {
style?: React.CSSProperties
}
-const Topics: FC = ({ searchValue, style }) => {
+const Topics: FC = ({ searchValue }) => {
const { activeAssistant, activeTopic, setActiveTopic } = useChat()
const { assistants } = useAssistants()
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(activeAssistant.id)
const { t } = useTranslation()
- const { showTopicTime, pinTopicsToTop, topicPosition } = useSettings()
+ const { showTopicTime, pinTopicsToTop } = useSettings()
+ const navigate = useNavigate()
+ const location = useLocation()
const topics = useTopicsForAssistant(activeAssistant.id)
@@ -173,9 +176,13 @@ const Topics: FC = ({ searchValue, style }) => {
// await modelGenerating()
startTransition(() => {
setActiveTopic(topic)
+ // 如果当前不在聊天页面,导航到聊天页面
+ if (location.pathname !== '/') {
+ navigate('/')
+ }
})
},
- [setActiveTopic]
+ [setActiveTopic, location.pathname, navigate]
)
const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions)
diff --git a/src/renderer/src/windows/settings/SettingsWindowApp.tsx b/src/renderer/src/windows/settings/SettingsWindowApp.tsx
new file mode 100644
index 0000000000..802efc36e6
--- /dev/null
+++ b/src/renderer/src/windows/settings/SettingsWindowApp.tsx
@@ -0,0 +1,262 @@
+import '@renderer/i18n'
+import '@renderer/databases'
+
+import StyleSheetManager from '@renderer/context/StyleSheetManager'
+import { useSettings } from '@renderer/hooks/useSettings'
+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 ToolSettings from '@renderer/pages/settings/ToolSettings'
+import { Spin } from 'antd'
+import {
+ Cloud,
+ Command,
+ HardDrive,
+ Info,
+ MonitorCog,
+ Package,
+ PencilRuler,
+ Rocket,
+ Settings2,
+ TextCursorInput,
+ Zap
+} from 'lucide-react'
+import React, { Suspense, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import styled from 'styled-components'
+
+type SettingsTab =
+ | 'provider'
+ | 'model'
+ | 'tool'
+ | 'general'
+ | 'display'
+ | 'shortcut'
+ | 'quickAssistant'
+ | 'selectionAssistant'
+ | 'data'
+ | 'about'
+ | 'quickPhrase'
+
+// Inner component that uses hooks after Redux is initialized
+function SettingsWindowContent(): React.ReactElement {
+ const { t } = useTranslation()
+ const { customCss } = useSettings()
+ const [activeTab, setActiveTab] = useState('provider')
+
+ // Remove spinner after component mounts
+ useEffect(() => {
+ document.getElementById('spinner')?.remove()
+ console.timeEnd('settings-init')
+ }, [])
+
+ // Handle custom CSS injection
+ useEffect(() => {
+ let customCssElement = document.getElementById('user-defined-custom-css') as HTMLStyleElement
+ if (customCssElement) {
+ customCssElement.remove()
+ }
+
+ if (customCss) {
+ customCssElement = document.createElement('style')
+ customCssElement.id = 'user-defined-custom-css'
+ customCssElement.textContent = customCss
+ document.head.appendChild(customCssElement)
+ }
+ }, [customCss])
+
+ useEffect(() => {
+ // Parse URL parameters for initial tab
+ const urlParams = new URLSearchParams(window.location.search)
+ const tab = urlParams.get('tab')
+ if (tab && isValidTab(tab)) {
+ setActiveTab(tab as SettingsTab)
+ }
+ }, [])
+
+ const isValidTab = (tab: string): boolean => {
+ const validTabs = [
+ 'provider',
+ 'model',
+ 'tool',
+ 'general',
+ 'display',
+ 'shortcut',
+ 'quickAssistant',
+ 'selectionAssistant',
+ 'data',
+ 'about',
+ 'quickPhrase'
+ ]
+ return validTabs.includes(tab)
+ }
+
+ const menuItems = [
+ { key: 'provider', icon: , label: t('settings.provider.title') },
+ { key: 'model', icon: , label: t('settings.model') },
+ { key: 'tool', icon: , label: t('settings.tool.title') },
+ { key: 'general', icon: , label: t('settings.general') },
+ { key: 'display', icon: , label: t('settings.display.title') },
+ { key: 'shortcut', icon: , label: t('settings.shortcuts.title') },
+ { key: 'quickAssistant', icon: , label: t('settings.quickAssistant.title') },
+ { key: 'selectionAssistant', icon: , label: t('selection.name') },
+ { key: 'quickPhrase', icon: , label: t('settings.quickPhrase.title') },
+ { key: 'data', icon: , label: t('settings.data.title') },
+ { key: 'about', icon: , label: t('settings.about') }
+ ] as const
+
+ const renderContent = () => {
+ switch (activeTab) {
+ case 'provider':
+ return (
+ }>
+
+
+ )
+ case 'model':
+ return
+ case 'tool':
+ return
+ case 'general':
+ return
+ case 'display':
+ return
+ case 'shortcut':
+ return
+ case 'quickAssistant':
+ return
+ case 'selectionAssistant':
+ return
+ case 'data':
+ return
+ case 'about':
+ return
+ case 'quickPhrase':
+ return
+ default:
+ return
+ }
+ }
+
+ return (
+
+
+
+ {t('settings.title')}
+
+
+
+ {menuItems.map((item) => (
+
+ ))}
+
+ {renderContent()}
+
+
+
+ )
+}
+
+const SettingsWindowApp: React.FC = () => {
+ return
+}
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ width: 100vw;
+ background: var(--color-background);
+`
+
+const TitleBar = styled.div`
+ display: flex;
+ align-items: center;
+ height: 40px;
+ padding: 0 20px;
+ background: var(--color-background);
+ border-bottom: 0.5px solid var(--color-border);
+ -webkit-app-region: drag;
+ user-select: none;
+`
+
+const Title = styled.h1`
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-text);
+`
+
+const ContentContainer = styled.div`
+ display: flex;
+ flex: 1;
+ height: calc(100vh - 40px);
+`
+
+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;
+`
+
+export default SettingsWindowApp
diff --git a/src/renderer/src/windows/settings/entryPoint.tsx b/src/renderer/src/windows/settings/entryPoint.tsx
new file mode 100644
index 0000000000..1fc7ea73c6
--- /dev/null
+++ b/src/renderer/src/windows/settings/entryPoint.tsx
@@ -0,0 +1,79 @@
+import '@renderer/assets/styles/index.scss'
+import '@ant-design/v5-patch-for-react-19'
+
+import KeyvStorage from '@kangfenmao/keyv-storage'
+import AntdProvider from '@renderer/context/AntdProvider'
+import { CodeStyleProvider } from '@renderer/context/CodeStyleProvider'
+import { ThemeProvider } from '@renderer/context/ThemeProvider'
+import NavigationService from '@renderer/services/NavigationService'
+import storeSyncService from '@renderer/services/StoreSyncService'
+import store, { persistor } from '@renderer/store'
+import { message, Modal } from 'antd'
+import { FC } from 'react'
+import { createRoot } from 'react-dom/client'
+import { Provider } from 'react-redux'
+import { MemoryRouter, useNavigate } from 'react-router-dom'
+import { PersistGate } from 'redux-persist/integration/react'
+
+import SettingsWindowApp from './SettingsWindowApp'
+
+/**
+ * This function is required for model API
+ * eg. BaseProviders.ts
+ * Although the coupling is too strong, we have no choice but to load it
+ * In multi-window handling, decoupling is needed
+ */
+function initKeyv() {
+ window.keyv = new KeyvStorage()
+ window.keyv.init()
+}
+
+initKeyv()
+
+//subscribe to store sync
+storeSyncService.subscribe()
+
+// Navigation wrapper component to set up navigation service
+const NavigationWrapper: FC<{ children: React.ReactNode }> = ({ children }) => {
+ const navigate = useNavigate()
+
+ // Set up navigation service for the settings window
+ NavigationService.setNavigate(navigate)
+
+ return <>{children}>
+}
+
+const App: FC = () => {
+ // 设置窗口需要显示各种操作的提示消息(如API连接测试、保存成功等)
+ // messageContextHolder 为 Ant Design 的全局消息提示提供上下文容器
+ const [messageApi, messageContextHolder] = message.useMessage()
+ const [modal, modalContextHolder] = Modal.useModal()
+
+ // Set up global APIs for the settings window
+ window.message = messageApi
+ window.modal = modal
+
+ return (
+
+
+
+
+
+
+
+ {/* 消息提示容器,用于显示设置操作的反馈信息 */}
+ {messageContextHolder}
+ {modalContextHolder}
+
+
+
+
+
+
+
+
+ )
+}
+
+const root = createRoot(document.getElementById('root') as HTMLElement)
+root.render()