feat(navigation): migrate to TanStack Router and enhance navigation structure

- Replaced react-router with TanStack Router for improved routing capabilities.
- Updated navigation logic across various components to utilize the new navigation API.
- Refactored App.tsx to integrate AppShell as the main routing component, removing the deprecated Router.
- Enhanced navigation methods to support search parameters, improving URL handling for settings and provider navigation.

These changes streamline the routing architecture, enhancing the overall user experience and maintainability of the application.
This commit is contained in:
MyPrototypeWhat 2025-12-09 17:30:04 +08:00
parent 5ad5bd537b
commit b9a56fcec1
46 changed files with 979 additions and 313 deletions

View File

@ -7,15 +7,13 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
// TODO: 新路由系统入口,迁移完成后启用
// import { AppShell } from './components/layout/AppShell'
import { AppShell } from './components/layout/AppShell'
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 Router from './Router'
const logger = loggerService.withContext('App.tsx')
@ -44,8 +42,7 @@ function App(): React.ReactElement {
<CodeStyleProvider>
<PersistGate loading={null} persistor={persistor}>
<TopViewContainer>
{/* TODO: 迁移完成后切换到 <AppShell /> */}
<Router />
<AppShell />
</TopViewContainer>
</PersistGate>
</CodeStyleProvider>

View File

@ -1,67 +0,0 @@
import '@renderer/databases'
import type { FC } from 'react'
import { useMemo } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom'
import Sidebar from './components/app/Sidebar'
import { ErrorBoundary } from './components/ErrorBoundary'
import TabsContainer from './components/Tab/TabContainer'
import NavigationHandler from './handler/NavigationHandler'
import { useNavbarPosition } from './hooks/useNavbar'
import CodeToolsPage from './pages/code/CodeToolsPage'
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 MinAppPage from './pages/minapps/MinAppPage'
import MinAppsPage from './pages/minapps/MinAppsPage'
import NotesPage from './pages/notes/NotesPage'
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
import SettingsPage from './pages/settings/SettingsPage'
import AssistantPresetsPage from './pages/store/assistants/presets/AssistantPresetsPage'
import TranslatePage from './pages/translate/TranslatePage'
const Router: FC = () => {
const { navbarPosition } = useNavbarPosition()
const routes = useMemo(() => {
return (
<ErrorBoundary>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/store" element={<AssistantPresetsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/notes" element={<NotesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps/:appId" element={<MinAppPage />} />
<Route path="/apps" element={<MinAppsPage />} />
<Route path="/code" element={<CodeToolsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} />
</Routes>
</ErrorBoundary>
)
}, [])
if (navbarPosition === 'left') {
return (
<HashRouter>
<Sidebar />
{routes}
<NavigationHandler />
</HashRouter>
)
}
return (
<HashRouter>
<NavigationHandler />
<TabsContainer>{routes}</TabsContainer>
</HashRouter>
)
}
export default Router

View File

@ -30,13 +30,13 @@ export const FreeTrialModelTag: FC<Props> = ({ model, showLabel = true }) => {
}
const onSelectProvider = () => {
NavigationService.navigate!(`/settings/provider?id=${providerId}`)
NavigationService.navigate!({ to: `/settings/provider`, search: { id: providerId } })
}
const onNavigateProvider = (e: MouseEvent) => {
e.stopPropagation()
SelectModelPopup.hide()
NavigationService.navigate!(`/settings/provider?id=${providerId}`)
NavigationService.navigate?.({ to: '/settings/provider', search: { id: providerId } })
}
if (!showLabel) {

View File

@ -6,11 +6,11 @@ import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import type { MinAppType } from '@renderer/types'
import { useNavigate } from '@tanstack/react-router'
import type { MenuProps } from 'antd'
import { Dropdown } from 'antd'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
interface Props {
@ -47,7 +47,7 @@ const MinApp: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
const handleClick = () => {
if (isTopNavbar) {
// 顶部导航栏:导航到小程序页面
navigate(`/apps/${app.id}`)
navigate({ to: `/apps/${app.id}` })
} else {
// 侧边导航栏:保持原有弹窗行为
openMinappKeepAlive(app)

View File

@ -3,9 +3,9 @@ import WebviewContainer from '@renderer/components/MinApp/WebviewContainer'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import { getWebviewLoaded, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
import { useLocation } from '@tanstack/react-router'
import type { WebviewTag } from 'electron'
import React, { useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components'
/**

View File

@ -193,7 +193,7 @@ const PopupContainer: React.FC<Props> = ({ model, filter: baseFilter, showTagFil
e.stopPropagation()
setOpen(false)
resolve(undefined)
window.navigate(`/settings/provider?id=${p.id}`)
window.navigate({ to: '/settings/provider', search: { id: p.id } })
}}
/>
</Tooltip>

View File

@ -17,6 +17,7 @@ import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs'
import type { MinAppType } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { useLocation, useNavigate } from '@tanstack/react-router'
import type { LRUCache } from 'lru-cache'
import {
FileSearch,
@ -37,7 +38,6 @@ import {
} from 'lucide-react'
import { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import MinAppIcon from '../Icons/MinAppIcon'
@ -203,17 +203,17 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
const handleAddTab = () => {
hideMinappPopup()
navigate('/launchpad')
navigate({ to: '/launchpad' })
}
const handleSettingsClick = () => {
hideMinappPopup()
navigate(lastSettingsPath)
navigate({ to: lastSettingsPath })
}
const handleTabClick = (tab: Tab) => {
hideMinappPopup()
navigate(tab.path)
navigate({ to: tab.path })
}
const visibleTabs = useMemo(() => tabs.filter((tab) => !specialTabs.includes(tab.id)), [tabs])

View File

@ -30,9 +30,9 @@ import {
} from 'lucide-react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { useTabs } from '../../hooks/useTabs'
import UserPopup from '../Popups/UserPopup'
import { SidebarOpenedMinappTabs, SidebarPinnedApps } from './PinnedMinapps'
@ -40,9 +40,11 @@ const Sidebar: FC = () => {
const { hideMinappPopup } = useMinappPopup()
const { pinned, minappShow } = useMinapps()
const [visibleSidebarIcons] = usePreference('ui.sidebar.icons.visible')
const { tabs, activeTabId, updateTab } = useTabs()
const { pathname } = useLocation()
const navigate = useNavigate()
// 获取当前 Tab 的 URL 作为 pathname
const activeTab = tabs.find((t) => t.id === activeTabId)
const pathname = activeTab?.url || '/'
const { theme, settedTheme, toggleTheme } = useTheme()
const avatar = useAvatar()
@ -54,9 +56,12 @@ const Sidebar: FC = () => {
const showPinnedApps = pinned.length > 0 && visibleSidebarIcons.includes('minapp')
// 在当前 Tab 内跳转
const to = async (path: string) => {
await modelGenerating()
navigate(path)
if (activeTabId) {
updateTab(activeTabId, { url: path })
}
}
const isFullscreen = useFullscreen()
@ -121,14 +126,16 @@ const Sidebar: FC = () => {
const MainMenus: FC = () => {
const { hideMinappPopup } = useMinappPopup()
const { minappShow } = useMinapps()
const { tabs, activeTabId, updateTab } = useTabs()
// 获取当前 Tab 的 URL 作为 pathname
const activeTab = tabs.find((t) => t.id === activeTabId)
const pathname = activeTab?.url || '/'
const { pathname } = useLocation()
const [visibleSidebarIcons] = usePreference('ui.sidebar.icons.visible')
const { defaultPaintingProvider } = useSettings()
const navigate = useNavigate()
const { theme } = useTheme()
const isRoute = (path: string): string => (pathname === path && !minappShow ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) && !minappShow ? 'active' : '')
const iconMap = {
@ -144,7 +151,7 @@ const MainMenus: FC = () => {
}
const pathMap = {
assistants: '/',
assistants: '/chat',
store: '/store',
paintings: `/paintings/${defaultPaintingProvider}`,
translate: '/translate',
@ -155,17 +162,24 @@ const MainMenus: FC = () => {
notes: '/notes'
}
// 在当前 Tab 内跳转
const to = async (path: string) => {
await modelGenerating()
if (activeTabId) {
updateTab(activeTabId, { url: path })
}
}
return visibleSidebarIcons.map((icon) => {
const path = pathMap[icon]
const isActive = path === '/' ? isRoute(path) : isRoutes(path)
const isActive = isRoutes(path)
return (
<Tooltip key={icon} placement="right" content={getSidebarIconLabel(icon)} delay={800}>
<StyledLink
onClick={async () => {
hideMinappPopup()
await modelGenerating()
navigate(path)
await to(path)
}}>
<Icon theme={theme} className={isActive}>
{iconMap[icon]}

View File

@ -1,3 +1,5 @@
import '@renderer/databases'
import { cn, Tabs, TabsList, TabsTrigger } from '@cherrystudio/ui'
import { Plus, X } from 'lucide-react'
import { Activity } from 'react'
@ -31,7 +33,7 @@ export const AppShell = () => {
id: uuid(),
type: 'route',
url: '/',
title: 'New Tab',
title: 'New Tab'
})
}

View File

@ -2,8 +2,8 @@
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
import type { ToastUtilities } from '@cherrystudio/ui'
import type { UseNavigateResult } from '@tanstack/react-router'
import type { HookAPI } from 'antd/es/modal/useModal'
import type { NavigateFunction } from 'react-router-dom'
interface ImportMetaEnv {
VITE_RENDERER_INTEGRATED_MODEL: string
@ -18,7 +18,7 @@ declare global {
root: HTMLElement
modal: HookAPI
store: any
navigate: NavigateFunction
navigate: UseNavigateResult<string>
toast: ToastUtilities
agentTools: {
respondToPermission: (payload: {

View File

@ -1,8 +1,8 @@
import { useAppSelector } from '@renderer/store'
import { IpcChannel } from '@shared/IpcChannel'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { useEffect } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useLocation, useNavigate } from 'react-router-dom'
const NavigationHandler: React.FC = () => {
const location = useLocation()
@ -17,7 +17,7 @@ const NavigationHandler: React.FC = () => {
if (location.pathname.startsWith('/settings')) {
return
}
navigate('/settings/provider')
navigate({ to: '/settings/provider' })
},
{
splitKey: '!',
@ -30,7 +30,7 @@ const NavigationHandler: React.FC = () => {
// Listen for navigate to About page event from macOS menu
useEffect(() => {
const handleNavigateToAbout = () => {
navigate('/settings/about')
navigate({ to: '/settings/about' })
}
const removeListener = window.electron.ipcRenderer.on(IpcChannel.Windows_NavigateToAbout, handleNavigateToAbout)

View File

@ -64,7 +64,7 @@ export function useAppInit() {
useEffect(() => {
window.api.getDataPathFromArgs().then((dataPath) => {
if (dataPath) {
window.navigate('/settings/data', { replace: true })
window.navigate({ to: '/settings/data', replace: true })
}
})
}, [])

View File

@ -13,8 +13,8 @@ window.electron.ipcRenderer.on(IpcChannel.Mcp_ServersChanged, (_event, servers)
window.electron.ipcRenderer.on(IpcChannel.Mcp_AddServer, (_event, server: MCPServer) => {
store.dispatch(addMCPServer(server))
NavigationService.navigate?.('/settings/mcp')
NavigationService.navigate?.(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)
NavigationService.navigate?.({ to: '/settings/mcp' })
NavigationService.navigate?.({ to: `/settings/mcp/settings/${encodeURIComponent(server.id)}` })
})
const selectMcpServers = (state: RootState) => state.mcp.servers

View File

@ -186,7 +186,7 @@ export const useMinappPopup = () => {
// Then navigate to the app tab using NavigationService
if (NavigationService.navigate) {
NavigationService.navigate(`/apps/${config.id}`)
NavigationService.navigate({ to: `/apps/${config.id}` })
}
} else {
// For side navbar, use the traditional popup system

View File

@ -19,12 +19,12 @@ import { getClaudeSupportedProviders } from '@renderer/utils/provider'
import type { TerminalConfig } from '@shared/config/constant'
import { codeTools, terminalApps } from '@shared/config/constant'
import { isSiliconAnthropicCompatibleModel } from '@shared/config/providers'
import { Link } from '@tanstack/react-router'
import { Alert, Checkbox, Input, Popover, Select, Space } from 'antd'
import { ArrowUpRight, Download, FolderOpen, HelpCircle, Terminal, X } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import {

View File

@ -11,9 +11,9 @@ import { getTopicById } from '@renderer/hooks/useTopic'
import { getAssistantById } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { locateToMessage } from '@renderer/services/MessagesService'
import NavigationService from '@renderer/services/NavigationService'
import type { Topic } from '@renderer/types'
import { classNames, runAsyncFunction } from '@renderer/utils'
import { useNavigate } from '@tanstack/react-router'
import { Divider, Empty } from 'antd'
import { t } from 'i18next'
import { Forward } from 'lucide-react'
@ -27,7 +27,8 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
}
const TopicMessages: FC<Props> = ({ topic: _topic, ...props }) => {
const navigate = NavigationService.navigate!
const navigate = useNavigate()
const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
const [messageStyle] = usePreference('chat.message.style')
const { setTimeoutTimer } = useTimer()
@ -53,7 +54,7 @@ const TopicMessages: FC<Props> = ({ topic: _topic, ...props }) => {
await modelGenerating()
SearchPopup.hide()
const assistant = getAssistantById(topic.assistantId)
navigate('/', { state: { assistant, topic } })
navigate({ to: '/chat', search: { assistantId: assistant?.id, topicId: topic.id } })
setTimeoutTimer('onContinueChat', () => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100)
}

View File

@ -10,11 +10,11 @@ import { newMessagesActions } from '@renderer/store/newMessage'
import { setActiveAgentId, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
import type { Assistant, Topic } from '@renderer/types'
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, SECOND_MIN_WINDOW_WIDTH } from '@shared/config/constant'
import { useNavigate, useSearch } from '@tanstack/react-router'
import { AnimatePresence, motion } from 'motion/react'
import type { FC } from 'react'
import { startTransition, useCallback, useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import Chat from './Chat'
@ -31,13 +31,23 @@ const HomePage: FC = () => {
// Initialize agent session hook
useAgentSessionInitializer()
const location = useLocation()
const state = location.state
const search = useSearch({ strict: false }) as { assistantId?: string; topicId?: string }
// 根据 search params 中的 ID 查找对应的 assistant
const assistantFromSearch = search.assistantId
? assistants.find((a) => a.id === search.assistantId)
: undefined
const [activeAssistant, _setActiveAssistant] = useState<Assistant>(
state?.assistant || _activeAssistant || assistants[0]
assistantFromSearch || _activeAssistant || assistants[0]
)
const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id ?? '', state?.topic)
// 根据 search params 中的 topicId 查找对应的 topic
const topicFromSearch = search.topicId
? activeAssistant?.topics?.find((t) => t.id === search.topicId)
: undefined
const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id ?? '', topicFromSearch)
const [showAssistants] = usePreference('assistant.tab.show')
const [showTopics] = usePreference('topic.tab.show')
const [topicPosition] = usePreference('topic.position')
@ -80,10 +90,10 @@ const HomePage: FC = () => {
}, [navigate])
useEffect(() => {
state?.assistant && setActiveAssistant(state?.assistant)
state?.topic && setActiveTopic(state?.topic)
assistantFromSearch && setActiveAssistant(assistantFromSearch)
topicFromSearch && setActiveTopic(topicFromSearch)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state])
}, [search.assistantId, search.topicId])
useEffect(() => {
const canMinimize = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics

View File

@ -5,11 +5,11 @@ import { QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/Qu
import type { ToolQuickPanelApi } from '@renderer/pages/home/Inputbar/types'
import { useAppSelector } from '@renderer/store'
import type { KnowledgeBase } from '@renderer/types'
import { useNavigate } from '@tanstack/react-router'
import { CircleX, FileSearch, Plus } from 'lucide-react'
import type { FC } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
interface Props {
quickPanel: ToolQuickPanelApi
@ -54,7 +54,7 @@ const KnowledgeBaseButton: FC<Props> = ({ quickPanel, selectedBases, onSelect, d
items.push({
label: t('knowledge.add.title') + '...',
icon: <Plus />,
action: () => navigate('/knowledge'),
action: () => navigate({ to: '/knowledge' }),
isSelected: false
})

View File

@ -12,12 +12,12 @@ import { EventEmitter } from '@renderer/services/EventService'
import type { MCPPrompt, MCPResource, MCPServer } from '@renderer/types'
import { isToolUseModeFunction } from '@renderer/utils/assistant'
import { isGeminiWebSearchProvider, isSupportUrlContextProvider } from '@renderer/utils/provider'
import { useNavigate } from '@tanstack/react-router'
import { Form, Input } from 'antd'
import { CircleX, Hammer, Plus } from 'lucide-react'
import type { FC } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
interface Props {
assistantId: string
@ -205,7 +205,7 @@ const MCPToolsButton: FC<Props> = ({ quickPanel, setInputValue, resizeTextArea,
newList.push({
label: t('settings.mcp.addServer.label') + '...',
icon: <Plus />,
action: () => navigate('/settings/mcp')
action: () => navigate({ to: '/settings/mcp' })
})
newList.unshift({

View File

@ -9,6 +9,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
import type { FileType, Model } from '@renderer/types'
import { FileTypes } from '@renderer/types'
import { getFancyProviderName } from '@renderer/utils'
import { useNavigate } from '@tanstack/react-router'
import { Avatar } from 'antd'
import { useLiveQuery } from 'dexie-react-hooks'
import { first, sortBy } from 'lodash'
@ -16,7 +17,6 @@ import { AtSign, CircleX, Plus } from 'lucide-react'
import type React from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
export type MentionTriggerInfo = { type: 'input' | 'button'; position?: number; originalText?: string }
@ -194,7 +194,7 @@ export const useMentionModelsPanel = (params: Params, role: 'button' | 'manager'
items.push({
label: t('settings.models.add.add_model') + '...',
icon: <Plus />,
action: () => navigate('/settings/provider'),
action: () => navigate({ to: '/settings/provider' }),
isSelected: false
})

View File

@ -3,7 +3,6 @@ import CodeViewer from '@renderer/components/CodeViewer'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useTimer } from '@renderer/hooks/useTimer'
import { getHttpMessageLabel, getProviderLabel } from '@renderer/i18n/label'
import { getProviderById } from '@renderer/services/ProviderService'
import { useAppDispatch } from '@renderer/store'
import { removeBlocksThunk } from '@renderer/store/thunk/messageThunk'
import type { SerializedAiSdkError, SerializedAiSdkErrorUnion, SerializedError } from '@renderer/types/error'
@ -33,10 +32,10 @@ import {
} from '@renderer/types/error'
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
import { Link } from '@tanstack/react-router'
import { Alert as AntdAlert, Modal } from 'antd'
import React, { useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
const HTTP_ERROR_CODES = [400, 401, 403, 404, 429, 500, 502, 503, 504]
@ -71,8 +70,8 @@ const ErrorMessage: React.FC<{ block: ErrorMessageBlock }> = ({ block }) => {
provider: (
<Link
style={{ color: 'var(--color-link)' }}
to={`/settings/provider`}
state={{ provider: getProviderById(providerId) }}
to="/settings/provider"
search={{ id: providerId }}
/>
)
}}

View File

@ -16,6 +16,7 @@ import styled from 'styled-components'
import AssistantsDrawer from './components/AssistantsDrawer'
import UpdateAppButton from './components/UpdateAppButton'
interface Props {
activeAssistant: Assistant
activeTopic: Topic

View File

@ -1,11 +1,11 @@
import App from '@renderer/components/MinApp/MinApp'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { useNavigate } from '@tanstack/react-router'
import { Code, FileSearch, Folder, Languages, LayoutGrid, NotepadText, Palette, Sparkle } from 'lucide-react'
import type { FC } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
const LaunchpadPage: FC = () => {
@ -87,7 +87,7 @@ const LaunchpadPage: FC = () => {
<SectionTitle>{t('launchpad.apps')}</SectionTitle>
<Grid>
{appMenuItems.map((item) => (
<AppIcon key={item.path} onClick={() => navigate(item.path)}>
<AppIcon key={item.path} onClick={() => navigate({ to: item.path })}>
<IconContainer>
<IconWrapper bgColor={item.bgColor}>{item.icon}</IconWrapper>
</IconContainer>

View File

@ -6,10 +6,10 @@ import { useMinapps } from '@renderer/hooks/useMinapps'
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import TabsService from '@renderer/services/TabsService'
import { getWebviewLoaded, onWebviewStateChange, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
import { useNavigate, useParams } from '@tanstack/react-router'
import type { WebviewTag } from 'electron'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import BeatLoader from 'react-spinners/BeatLoader'
import styled from 'styled-components'
@ -20,7 +20,7 @@ import WebviewSearch from './components/WebviewSearch'
const logger = loggerService.withContext('MinAppPage')
const MinAppPage: FC = () => {
const { appId } = useParams<{ appId: string }>()
const { appId } = useParams({ strict: false }) as { appId: string }
const { isTopNavbar } = useNavbarPosition()
const { openMinappKeepAlive, minAppsCache } = useMinappPopup()
const { minapps } = useMinapps()
@ -64,7 +64,7 @@ const MinAppPage: FC = () => {
useEffect(() => {
// If app not found, redirect to apps list
if (!app) {
navigate('/apps')
navigate({ to: '/apps' })
return
}
@ -72,7 +72,7 @@ const MinAppPage: FC = () => {
// Only check once and only if we haven't already redirected
if (!initialIsTopNavbar.current && !hasRedirected.current) {
hasRedirected.current = true
navigate('/apps')
navigate({ to: '/apps' })
// Open popup after navigation
setTimeout(() => {
openMinappKeepAlive(app)

View File

@ -15,11 +15,11 @@ import { isDev } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useMinapps } from '@renderer/hooks/useMinapps'
import type { MinAppType } from '@renderer/types'
import { useNavigate } from '@tanstack/react-router'
import type { WebviewTag } from 'electron'
import type { FC } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
const logger = loggerService.withContext('MinimalToolbar')
@ -213,7 +213,7 @@ const MinimalToolbar: FC<Props> = ({ app, webviewRef, currentUrl, onReload, onOp
}, [app.id, webviewRef, scheduleNavigationUpdate])
const handleMinimize = useCallback(() => {
navigate('/apps')
navigate({ to: '/apps' })
}, [navigate])
const handleTogglePin = useCallback(() => {

View File

@ -19,12 +19,12 @@ import { translateText } from '@renderer/services/TranslateService'
import type { FileMetadata } from '@renderer/types'
import type { PaintingAction, PaintingsState } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Input, InputNumber, Radio, Segmented, Select, Slider, Upload } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -667,7 +667,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -11,13 +11,13 @@ import { useAllProviders } from '@renderer/hooks/useProvider'
import FileManager from '@renderer/services/FileManager'
import type { FileMetadata } from '@renderer/types'
import { convertToBase64, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import type { DmxapiPainting } from '@types'
import { Input, InputNumber, Segmented, Select } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { generationModeType } from '../../types'
@ -640,7 +640,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -29,13 +29,13 @@ import type { PaintingAction, PaintingsState } from '@renderer/types'
import type { FileMetadata } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { isNewApiProvider } from '@renderer/utils/provider'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Empty, InputNumber, Segmented, Select, Upload } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -436,7 +436,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}
@ -463,7 +463,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
// 当 modelOptions 为空时,引导用户跳转到 Provider 设置页面,新增 image-generation 端点模型
const handleShowAddModelPopup = () => {
navigate(`/settings/provider?id=${newApiProvider.id}`)
navigate({ to: `/settings/provider?id=${newApiProvider.id}` })
}
useEffect(() => {

View File

@ -15,13 +15,13 @@ import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import type { FileMetadata, OvmsPainting } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Avatar, Input, InputNumber, Select, Slider } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -330,7 +330,7 @@ const OvmsPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -5,9 +5,9 @@ import { setDefaultPaintingProvider } from '@renderer/store/settings'
import { updateTab } from '@renderer/store/tabs'
import type { PaintingProvider, SystemProviderId } from '@renderer/types'
import { isNewApiProvider } from '@renderer/utils/provider'
import { useParams } from '@tanstack/react-router'
import type { FC } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { Route, Routes, useParams } from 'react-router-dom'
import AihubmixPage from './AihubmixPage'
import DmxapiPage from './DmxapiPage'
@ -22,8 +22,8 @@ const logger = loggerService.withContext('PaintingsRoutePage')
const BASE_OPTIONS: SystemProviderId[] = ['zhipu', 'aihubmix', 'silicon', 'dmxapi', 'tokenflux', 'ovms']
const PaintingsRoutePage: FC = () => {
const params = useParams()
const provider = params['*']
const params = useParams({ strict: false }) as { _splat?: string }
const provider = params._splat
const dispatch = useAppDispatch()
const providers = useAllProviders()
const [ovmsStatus, setOvmsStatus] = useState<'not-installed' | 'not-running' | 'running'>('not-running')
@ -49,22 +49,34 @@ const PaintingsRoutePage: FC = () => {
}
}, [provider, dispatch, validOptions])
return (
<Routes>
<Route path="*" element={<NewApiPage Options={validOptions} />} />
<Route path="/zhipu" element={<ZhipuPage Options={validOptions} />} />
<Route path="/aihubmix" element={<AihubmixPage Options={validOptions} />} />
<Route path="/silicon" element={<SiliconPage Options={validOptions} />} />
<Route path="/dmxapi" element={<DmxapiPage Options={validOptions} />} />
<Route path="/tokenflux" element={<TokenFluxPage Options={validOptions} />} />
<Route path="/ovms" element={<OvmsPage Options={validOptions} />} />
<Route path="/new-api" element={<NewApiPage Options={validOptions} />} />
{/* new-api family providers are mounted dynamically below */}
{newApiProviders.map((p) => (
<Route key={p.id} path={`/${p.id}`} element={<NewApiPage Options={validOptions} />} />
))}
</Routes>
)
// 根据 provider 渲染对应的页面
const renderPage = () => {
switch (provider) {
case 'zhipu':
return <ZhipuPage Options={validOptions} />
case 'aihubmix':
return <AihubmixPage Options={validOptions} />
case 'silicon':
return <SiliconPage Options={validOptions} />
case 'dmxapi':
return <DmxapiPage Options={validOptions} />
case 'tokenflux':
return <TokenFluxPage Options={validOptions} />
case 'ovms':
return <OvmsPage Options={validOptions} />
case 'new-api':
return <NewApiPage Options={validOptions} />
default:
// 检查是否是 new-api 家族的 provider
if (provider && newApiProviders.some((p) => p.id === provider)) {
return <NewApiPage Options={validOptions} />
}
// 默认页面
return <NewApiPage Options={validOptions} />
}
}
return renderPage()
}
export default PaintingsRoutePage

View File

@ -23,12 +23,12 @@ import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import type { FileMetadata, Painting } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Input, InputNumber, Radio, Select, Slider } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -333,7 +333,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -15,12 +15,12 @@ import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import type { TokenFluxPainting } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Select } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -268,7 +268,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -12,12 +12,12 @@ import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import FileManager from '@renderer/services/FileManager'
import { getErrorMessage, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { InputNumber, Radio, Select } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -260,7 +260,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -15,7 +15,7 @@ export function checkProviderEnabled(provider: Provider, t: TFunction): Promise<
closable: true,
okText: t('common.go_to_settings'),
onOk: () => {
window.navigate?.(`/settings/provider?id=${provider.id}`)
window.navigate?.({ to: `/settings/provider`, search: { id: provider.id } })
reject('Provider disabled')
},
onCancel: () => reject('Provider disabled')

View File

@ -13,6 +13,7 @@ import i18n from '@renderer/i18n'
// import { setUpdateState as setAppUpdateState } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils'
import { ThemeMode, UpgradeChannel } from '@shared/data/preference/preferenceTypes'
import { Link } from '@tanstack/react-router'
import { Avatar, Progress, Radio, Row, Tag } from 'antd'
import { debounce } from 'lodash'
import { Bug, Building2, Github, Globe, Mail, Rss } from 'lucide-react'
@ -21,7 +22,6 @@ import type { FC } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Markdown from 'react-markdown'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitle } from '.'

View File

@ -3,11 +3,11 @@ import { Center, ColFlex } from '@cherrystudio/ui'
import { Button } from '@cherrystudio/ui'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp'
import { useNavigate } from '@tanstack/react-router'
import { Alert } from 'antd'
import type { FC } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
@ -87,7 +87,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
<Button
className="nodrag rounded-full"
variant={installed ? 'default' : 'destructive'}
onClick={() => navigate('/settings/mcp/mcp-install')}
onClick={() => navigate({ to: '/settings/mcp/mcp-install' })}
size="icon">
{installed ? <CheckCircleOutlined /> : <WarningOutlined />}
</Button>

View File

@ -9,12 +9,12 @@ import { useMCPServerTrust } from '@renderer/hooks/useMCPServerTrust'
import type { MCPServer } from '@renderer/types'
import { formatMcpError } from '@renderer/utils/error'
import { matchKeywordsInString } from '@renderer/utils/match'
import { useNavigate } from '@tanstack/react-router'
import { Button, Dropdown, Empty } from 'antd'
import { Plus } from 'lucide-react'
import type { FC } from 'react'
import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingTitle } from '..'
@ -115,7 +115,7 @@ const McpServersList: FC = () => {
isActive: false
}
addMCPServer(newServer)
navigate(`/settings/mcp/settings/${encodeURIComponent(newServer.id)}`)
navigate({ to: `/settings/mcp/settings/${encodeURIComponent(newServer.id)}` })
window.toast.success(t('settings.mcp.addSuccess'))
}, [addMCPServer, navigate, t])
@ -260,7 +260,7 @@ const McpServersList: FC = () => {
isLoading={loadingServerIds.has(server.id)}
onToggle={async (active) => await handleToggleActive(server, active)}
onDelete={() => onDeleteMcpServer(server)}
onEdit={() => navigate(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)}
onEdit={() => navigate({ to: `/settings/mcp/settings/${encodeURIComponent(server.id)}` })}
onOpenUrl={(url) => window.open(url, '_blank')}
/>
)}

View File

@ -11,13 +11,13 @@ import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
import type { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
import { parseKeyValueString } from '@renderer/utils/env'
import { formatMcpError } from '@renderer/utils/error'
import { useNavigate, useParams } from '@tanstack/react-router'
import type { TabsProps } from 'antd'
import { Badge, Form, Input, Radio, Select, Tabs } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { ChevronDown, SaveIcon } from 'lucide-react'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate, useParams } from 'react-router'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
@ -68,7 +68,8 @@ type TabKey = 'settings' | 'description' | 'tools' | 'prompts' | 'resources'
const McpSettings: React.FC = () => {
const { t } = useTranslation()
const { serverId } = useParams<{ serverId: string }>()
const params = useParams({ strict: false }) as { serverId?: string }
const serverId = params.serverId
const decodedServerId = serverId ? decodeURIComponent(serverId) : ''
const server = useMCPServer(decodedServerId).server as MCPServer
const { deleteMCPServer, updateMCPServer } = useMCPServers()
@ -376,7 +377,7 @@ const McpSettings: React.FC = () => {
await window.api.mcp.removeServer(server)
deleteMCPServer(server.id)
window.toast.success(t('settings.mcp.deleteSuccess'))
navigate('/settings/mcp')
navigate({ to: '/settings/mcp' })
}
})
} catch (error: any) {

View File

@ -8,30 +8,17 @@ import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.p
import DividerWithText from '@renderer/components/DividerWithText'
import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { Link, Outlet, useLocation, useNavigate } from '@tanstack/react-router'
import { Button, Flex } from 'antd'
import { FolderCog, Package, ShoppingBag } from 'lucide-react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { SettingContainer } from '..'
import BuiltinMCPServerList from './BuiltinMCPServerList'
import InstallNpxUv from './InstallNpxUv'
import McpMarketList from './McpMarketList'
import ProviderDetail from './McpProviderSettings'
import McpServersList from './McpServersList'
import McpSettings from './McpSettings'
import NpxSearch from './NpxSearch'
import { providers } from './providers/config'
const MCPSettings: FC = () => {
const { theme } = useTheme()
const { t } = useTranslation()
const { mcpServers } = useMCPServers()
const navigate = useNavigate()
const location = useLocation()
@ -84,7 +71,7 @@ const MCPSettings: FC = () => {
<ListItem
title={t('settings.mcp.servers', 'MCP Servers')}
active={activeView === 'servers'}
onClick={() => navigate('/settings/mcp/servers')}
onClick={() => navigate({ to: '/settings/mcp/servers' })}
icon={<FolderCog size={18} />}
titleStyle={{ fontWeight: 500 }}
/>
@ -92,14 +79,14 @@ const MCPSettings: FC = () => {
<ListItem
title={t('settings.mcp.builtinServers', 'Built-in Servers')}
active={activeView === 'builtin'}
onClick={() => navigate('/settings/mcp/builtin')}
onClick={() => navigate({ to: '/settings/mcp/builtin' })}
icon={<Package size={18} />}
titleStyle={{ fontWeight: 500 }}
/>
<ListItem
title={t('settings.mcp.marketplaces', 'Marketplaces')}
active={activeView === 'marketplaces'}
onClick={() => navigate('/settings/mcp/marketplaces')}
onClick={() => navigate({ to: '/settings/mcp/marketplaces' })}
icon={<ShoppingBag size={18} />}
titleStyle={{ fontWeight: 500 }}
/>
@ -109,7 +96,7 @@ const MCPSettings: FC = () => {
key={provider.key}
title={provider.name}
active={activeView === provider.key}
onClick={() => navigate(`/settings/mcp/${provider.key}`)}
onClick={() => navigate({ to: `/settings/mcp/${provider.key}` })}
icon={providerIcons[provider.key] || <FolderCog size={16} />}
titleStyle={{ fontWeight: 500 }}
/>
@ -125,50 +112,7 @@ const MCPSettings: FC = () => {
</Link>
</BackButtonContainer>
)}
<Routes>
<Route index element={<Navigate to="servers" replace />} />
<Route path="servers" element={<McpServersList />} />
<Route path="settings/:serverId" element={<McpSettings />} />
<Route
path="npx-search"
element={
<SettingContainer theme={theme}>
<NpxSearch />
</SettingContainer>
}
/>
<Route
path="mcp-install"
element={
<SettingContainer style={{ backgroundColor: 'inherit' }}>
<InstallNpxUv />
</SettingContainer>
}
/>
<Route
path="builtin"
element={
<ContentWrapper>
<BuiltinMCPServerList />
</ContentWrapper>
}
/>
<Route
path="marketplaces"
element={
<ContentWrapper>
<McpMarketList />
</ContentWrapper>
}
/>
{providers.map((provider) => (
<Route
key={provider.key}
path={provider.key}
element={<ProviderDetail provider={provider} existingServers={mcpServers} />}
/>
))}
</Routes>
<Outlet />
</RightContainer>
</MainContainer>
</Container>
@ -212,12 +156,6 @@ const ProviderIcon = styled.img`
background-color: var(--color-background-soft);
`
const ContentWrapper = styled.div`
padding: 20px;
overflow-y: auto;
height: 100%;
`
const BackButtonContainer = styled.div`
display: flex;
align-items: center;

View File

@ -14,13 +14,13 @@ import ImageStorage from '@renderer/services/ImageStorage'
import type { Provider, ProviderType } from '@renderer/types'
import { isSystemProvider } from '@renderer/types'
import { getFancyProviderName, matchKeywordsInModel, matchKeywordsInProvider, uuid } from '@renderer/utils'
import { useLocation, useNavigate, useSearch } from '@tanstack/react-router'
import type { MenuProps } from 'antd'
import { Dropdown, Input, Tag } from 'antd'
import { GripVertical, PlusIcon, Search, UserPen } from 'lucide-react'
import type { FC } from 'react'
import { startTransition, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSearchParams } from 'react-router-dom'
import styled from 'styled-components'
import AddProviderPopup from './AddProviderPopup'
@ -35,7 +35,9 @@ const systemType = await window.api.system.getDeviceType()
const cpuName = await window.api.system.getCpuName()
const ProviderList: FC = () => {
const [searchParams, setSearchParams] = useSearchParams()
const search = useSearch({ strict: false }) as Record<string, string | undefined>
const navigate = useNavigate()
const location = useLocation()
const providers = useAllProviders()
const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders()
const { setTimeoutTimer } = useTimer()
@ -72,8 +74,8 @@ const ProviderList: FC = () => {
}, [providers])
useEffect(() => {
if (searchParams.get('id')) {
const providerId = searchParams.get('id')
if (search.id) {
const providerId = search.id
const provider = providers.find((p) => p.id === providerId)
if (provider) {
setSelectedProvider(provider)
@ -89,10 +91,17 @@ const ProviderList: FC = () => {
} else {
setSelectedProvider(providers[0])
}
searchParams.delete('id')
setSearchParams(searchParams)
// 清除 id 参数
navigate({
to: location.pathname,
search: (prev) => {
const { id: _, ...rest } = prev as Record<string, unknown>
return rest
},
replace: true
})
}
}, [providers, searchParams, setSearchParams, setSelectedProvider, setTimeoutTimer])
}, [providers, search.id, navigate, location.pathname, setSelectedProvider, setTimeoutTimer])
// Handle provider add key from URL schema
useEffect(() => {
@ -106,7 +115,7 @@ const ProviderList: FC = () => {
const { id } = data
const { updatedProvider, isNew, displayName } = await UrlSchemaInfoPopup.show(data)
window.navigate(`/settings/provider?id=${id}`)
navigate({ to: '/settings/provider', search: { id } })
if (!updatedProvider) {
return
@ -123,7 +132,7 @@ const ProviderList: FC = () => {
}
// 检查 URL 参数
const addProviderData = searchParams.get('addProviderData')
const addProviderData = search.addProviderData
if (!addProviderData) {
return
}
@ -132,17 +141,17 @@ const ProviderList: FC = () => {
const { id, apiKey: newApiKey, baseUrl, type, name } = JSON.parse(addProviderData)
if (!id || !newApiKey || !baseUrl) {
window.toast.error(t('settings.models.provider_key_add_failed_by_invalid_data'))
window.navigate('/settings/provider')
navigate({ to: '/settings/provider' })
return
}
handleProviderAddKey({ id, apiKey: newApiKey, baseUrl, type, name })
} catch (error) {
window.toast.error(t('settings.models.provider_key_add_failed_by_invalid_data'))
window.navigate('/settings/provider')
navigate({ to: '/settings/provider' })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams])
}, [search.addProviderData])
const onAddProvider = async () => {
const { name: providerName, type, logo } = await AddProviderPopup.show()

View File

@ -6,12 +6,12 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { getSelectionDescriptionLabel } from '@renderer/i18n/label'
import SelectionToolbar from '@renderer/windows/selection/toolbar/SelectionToolbar'
import type { SelectionFilterMode, SelectionTriggerMode } from '@shared/data/preference/preferenceTypes'
import { Link } from '@tanstack/react-router'
import { Row, Slider } from 'antd'
import { CircleHelp, Edit2 } from 'lucide-react'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import {

View File

@ -1,7 +1,7 @@
import { GlobalOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import Scrollbar from '@renderer/components/Scrollbar'
import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings'
import { Link, Outlet, useLocation } from '@tanstack/react-router'
import { Divider as AntDivider } from 'antd'
import {
Brain,
@ -22,27 +22,11 @@ import {
} from 'lucide-react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, Route, Routes, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import AboutSettings from './AboutSettings'
import DataSettings from './DataSettings/DataSettings'
import DisplaySettings from './DisplaySettings/DisplaySettings'
import DocProcessSettings from './DocProcessSettings'
import GeneralSettings from './GeneralSettings'
import MCPSettings from './MCPSettings'
import MemorySettings from './MemorySettings'
import NotesSettings from './NotesSettings'
import { ProviderList } from './ProviderSettings'
import QuickAssistantSettings from './QuickAssistantSettings'
import QuickPhraseSettings from './QuickPhraseSettings'
import SelectionAssistantSettings from './SelectionAssistantSettings/SelectionAssistantSettings'
import ShortcutSettings from './ShortcutSettings'
import { ApiServerSettings } from './ToolSettings/ApiServerSettings'
import WebSearchSettings from './WebSearchSettings'
const SettingsPage: FC = () => {
const { pathname } = useLocation()
const location = useLocation()
const { pathname } = location
const { t } = useTranslation()
const isRoute = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
@ -156,24 +140,7 @@ const SettingsPage: FC = () => {
</MenuItemLink>
</SettingMenus>
<SettingContent>
<Routes>
<Route path="provider" element={<ProviderList />} />
<Route path="model" element={<ModelSettings />} />
<Route path="websearch" element={<WebSearchSettings />} />
<Route path="api-server" element={<ApiServerSettings />} />
<Route path="docprocess" element={<DocProcessSettings />} />
<Route path="quickphrase" element={<QuickPhraseSettings />} />
<Route path="mcp/*" element={<MCPSettings />} />
<Route path="memory" element={<MemorySettings />} />
<Route path="general/*" element={<GeneralSettings />} />
<Route path="display" element={<DisplaySettings />} />
<Route path="shortcut" element={<ShortcutSettings />} />
<Route path="quickAssistant" element={<QuickAssistantSettings />} />
<Route path="selectionAssistant" element={<SelectionAssistantSettings />} />
<Route path="data" element={<DataSettings />} />
<Route path="notes" element={<NotesSettings />} />
<Route path="about" element={<AboutSettings />} />
</Routes>
<Outlet />
</SettingContent>
</ContentContainer>
</Container>

View File

@ -9,48 +9,510 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as TranslateRouteImport } from './routes/translate'
import { Route as StoreRouteImport } from './routes/store'
import { Route as SettingsRouteImport } from './routes/settings'
import { Route as NotesRouteImport } from './routes/notes'
import { Route as KnowledgeRouteImport } from './routes/knowledge'
import { Route as FilesRouteImport } from './routes/files'
import { Route as CodeRouteImport } from './routes/code'
import { Route as ChatRouteImport } from './routes/chat'
import { Route as IndexRouteImport } from './routes/index'
import { Route as SettingsIndexRouteImport } from './routes/settings/index'
import { Route as PaintingsIndexRouteImport } from './routes/paintings/index'
import { Route as AppsIndexRouteImport } from './routes/apps/index'
import { Route as SettingsWebsearchRouteImport } from './routes/settings/websearch'
import { Route as SettingsShortcutRouteImport } from './routes/settings/shortcut'
import { Route as SettingsSelectionAssistantRouteImport } from './routes/settings/selectionAssistant'
import { Route as SettingsQuickphraseRouteImport } from './routes/settings/quickphrase'
import { Route as SettingsQuickAssistantRouteImport } from './routes/settings/quickAssistant'
import { Route as SettingsProviderRouteImport } from './routes/settings/provider'
import { Route as SettingsNotesRouteImport } from './routes/settings/notes'
import { Route as SettingsModelRouteImport } from './routes/settings/model'
import { Route as SettingsMemoryRouteImport } from './routes/settings/memory'
import { Route as SettingsMcpRouteImport } from './routes/settings/mcp'
import { Route as SettingsGeneralRouteImport } from './routes/settings/general'
import { Route as SettingsDocprocessRouteImport } from './routes/settings/docprocess'
import { Route as SettingsDisplayRouteImport } from './routes/settings/display'
import { Route as SettingsDataRouteImport } from './routes/settings/data'
import { Route as SettingsApiServerRouteImport } from './routes/settings/api-server'
import { Route as SettingsAboutRouteImport } from './routes/settings/about'
import { Route as PaintingsSplatRouteImport } from './routes/paintings/$'
import { Route as AppsAppIdRouteImport } from './routes/apps/$appId'
import { Route as SettingsMcpIndexRouteImport } from './routes/settings/mcp/index'
import { Route as SettingsMcpServersRouteImport } from './routes/settings/mcp/servers'
import { Route as SettingsMcpNpxSearchRouteImport } from './routes/settings/mcp/npx-search'
import { Route as SettingsMcpMcpInstallRouteImport } from './routes/settings/mcp/mcp-install'
import { Route as SettingsMcpMarketplacesRouteImport } from './routes/settings/mcp/marketplaces'
import { Route as SettingsMcpBuiltinRouteImport } from './routes/settings/mcp/builtin'
import { Route as SettingsMcpSplatRouteImport } from './routes/settings/mcp/$'
import { Route as SettingsMcpSettingsServerIdRouteImport } from './routes/settings/mcp/settings.$serverId'
const TranslateRoute = TranslateRouteImport.update({
id: '/translate',
path: '/translate',
getParentRoute: () => rootRouteImport,
} as any)
const StoreRoute = StoreRouteImport.update({
id: '/store',
path: '/store',
getParentRoute: () => rootRouteImport,
} as any)
const SettingsRoute = SettingsRouteImport.update({
id: '/settings',
path: '/settings',
getParentRoute: () => rootRouteImport,
} as any)
const NotesRoute = NotesRouteImport.update({
id: '/notes',
path: '/notes',
getParentRoute: () => rootRouteImport,
} as any)
const KnowledgeRoute = KnowledgeRouteImport.update({
id: '/knowledge',
path: '/knowledge',
getParentRoute: () => rootRouteImport,
} as any)
const FilesRoute = FilesRouteImport.update({
id: '/files',
path: '/files',
getParentRoute: () => rootRouteImport,
} as any)
const CodeRoute = CodeRouteImport.update({
id: '/code',
path: '/code',
getParentRoute: () => rootRouteImport,
} as any)
const ChatRoute = ChatRouteImport.update({
id: '/chat',
path: '/chat',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const SettingsIndexRoute = SettingsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => SettingsRoute,
} as any)
const PaintingsIndexRoute = PaintingsIndexRouteImport.update({
id: '/paintings/',
path: '/paintings/',
getParentRoute: () => rootRouteImport,
} as any)
const AppsIndexRoute = AppsIndexRouteImport.update({
id: '/apps/',
path: '/apps/',
getParentRoute: () => rootRouteImport,
} as any)
const SettingsWebsearchRoute = SettingsWebsearchRouteImport.update({
id: '/websearch',
path: '/websearch',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsShortcutRoute = SettingsShortcutRouteImport.update({
id: '/shortcut',
path: '/shortcut',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsSelectionAssistantRoute =
SettingsSelectionAssistantRouteImport.update({
id: '/selectionAssistant',
path: '/selectionAssistant',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsQuickphraseRoute = SettingsQuickphraseRouteImport.update({
id: '/quickphrase',
path: '/quickphrase',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsQuickAssistantRoute = SettingsQuickAssistantRouteImport.update({
id: '/quickAssistant',
path: '/quickAssistant',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsProviderRoute = SettingsProviderRouteImport.update({
id: '/provider',
path: '/provider',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsNotesRoute = SettingsNotesRouteImport.update({
id: '/notes',
path: '/notes',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsModelRoute = SettingsModelRouteImport.update({
id: '/model',
path: '/model',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsMemoryRoute = SettingsMemoryRouteImport.update({
id: '/memory',
path: '/memory',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsMcpRoute = SettingsMcpRouteImport.update({
id: '/mcp',
path: '/mcp',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsGeneralRoute = SettingsGeneralRouteImport.update({
id: '/general',
path: '/general',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsDocprocessRoute = SettingsDocprocessRouteImport.update({
id: '/docprocess',
path: '/docprocess',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsDisplayRoute = SettingsDisplayRouteImport.update({
id: '/display',
path: '/display',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsDataRoute = SettingsDataRouteImport.update({
id: '/data',
path: '/data',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsApiServerRoute = SettingsApiServerRouteImport.update({
id: '/api-server',
path: '/api-server',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsAboutRoute = SettingsAboutRouteImport.update({
id: '/about',
path: '/about',
getParentRoute: () => SettingsRoute,
} as any)
const PaintingsSplatRoute = PaintingsSplatRouteImport.update({
id: '/paintings/$',
path: '/paintings/$',
getParentRoute: () => rootRouteImport,
} as any)
const AppsAppIdRoute = AppsAppIdRouteImport.update({
id: '/apps/$appId',
path: '/apps/$appId',
getParentRoute: () => rootRouteImport,
} as any)
const SettingsMcpIndexRoute = SettingsMcpIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpServersRoute = SettingsMcpServersRouteImport.update({
id: '/servers',
path: '/servers',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpNpxSearchRoute = SettingsMcpNpxSearchRouteImport.update({
id: '/npx-search',
path: '/npx-search',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpMcpInstallRoute = SettingsMcpMcpInstallRouteImport.update({
id: '/mcp-install',
path: '/mcp-install',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpMarketplacesRoute = SettingsMcpMarketplacesRouteImport.update({
id: '/marketplaces',
path: '/marketplaces',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpBuiltinRoute = SettingsMcpBuiltinRouteImport.update({
id: '/builtin',
path: '/builtin',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpSplatRoute = SettingsMcpSplatRouteImport.update({
id: '/$',
path: '/$',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpSettingsServerIdRoute =
SettingsMcpSettingsServerIdRouteImport.update({
id: '/settings/$serverId',
path: '/settings/$serverId',
getParentRoute: () => SettingsMcpRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/settings': typeof SettingsRoute
'/chat': typeof ChatRoute
'/code': typeof CodeRoute
'/files': typeof FilesRoute
'/knowledge': typeof KnowledgeRoute
'/notes': typeof NotesRoute
'/settings': typeof SettingsRouteWithChildren
'/store': typeof StoreRoute
'/translate': typeof TranslateRoute
'/apps/$appId': typeof AppsAppIdRoute
'/paintings/$': typeof PaintingsSplatRoute
'/settings/about': typeof SettingsAboutRoute
'/settings/api-server': typeof SettingsApiServerRoute
'/settings/data': typeof SettingsDataRoute
'/settings/display': typeof SettingsDisplayRoute
'/settings/docprocess': typeof SettingsDocprocessRoute
'/settings/general': typeof SettingsGeneralRoute
'/settings/mcp': typeof SettingsMcpRouteWithChildren
'/settings/memory': typeof SettingsMemoryRoute
'/settings/model': typeof SettingsModelRoute
'/settings/notes': typeof SettingsNotesRoute
'/settings/provider': typeof SettingsProviderRoute
'/settings/quickAssistant': typeof SettingsQuickAssistantRoute
'/settings/quickphrase': typeof SettingsQuickphraseRoute
'/settings/selectionAssistant': typeof SettingsSelectionAssistantRoute
'/settings/shortcut': typeof SettingsShortcutRoute
'/settings/websearch': typeof SettingsWebsearchRoute
'/apps': typeof AppsIndexRoute
'/paintings': typeof PaintingsIndexRoute
'/settings/': typeof SettingsIndexRoute
'/settings/mcp/$': typeof SettingsMcpSplatRoute
'/settings/mcp/builtin': typeof SettingsMcpBuiltinRoute
'/settings/mcp/marketplaces': typeof SettingsMcpMarketplacesRoute
'/settings/mcp/mcp-install': typeof SettingsMcpMcpInstallRoute
'/settings/mcp/npx-search': typeof SettingsMcpNpxSearchRoute
'/settings/mcp/servers': typeof SettingsMcpServersRoute
'/settings/mcp/': typeof SettingsMcpIndexRoute
'/settings/mcp/settings/$serverId': typeof SettingsMcpSettingsServerIdRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/settings': typeof SettingsRoute
'/chat': typeof ChatRoute
'/code': typeof CodeRoute
'/files': typeof FilesRoute
'/knowledge': typeof KnowledgeRoute
'/notes': typeof NotesRoute
'/store': typeof StoreRoute
'/translate': typeof TranslateRoute
'/apps/$appId': typeof AppsAppIdRoute
'/paintings/$': typeof PaintingsSplatRoute
'/settings/about': typeof SettingsAboutRoute
'/settings/api-server': typeof SettingsApiServerRoute
'/settings/data': typeof SettingsDataRoute
'/settings/display': typeof SettingsDisplayRoute
'/settings/docprocess': typeof SettingsDocprocessRoute
'/settings/general': typeof SettingsGeneralRoute
'/settings/memory': typeof SettingsMemoryRoute
'/settings/model': typeof SettingsModelRoute
'/settings/notes': typeof SettingsNotesRoute
'/settings/provider': typeof SettingsProviderRoute
'/settings/quickAssistant': typeof SettingsQuickAssistantRoute
'/settings/quickphrase': typeof SettingsQuickphraseRoute
'/settings/selectionAssistant': typeof SettingsSelectionAssistantRoute
'/settings/shortcut': typeof SettingsShortcutRoute
'/settings/websearch': typeof SettingsWebsearchRoute
'/apps': typeof AppsIndexRoute
'/paintings': typeof PaintingsIndexRoute
'/settings': typeof SettingsIndexRoute
'/settings/mcp/$': typeof SettingsMcpSplatRoute
'/settings/mcp/builtin': typeof SettingsMcpBuiltinRoute
'/settings/mcp/marketplaces': typeof SettingsMcpMarketplacesRoute
'/settings/mcp/mcp-install': typeof SettingsMcpMcpInstallRoute
'/settings/mcp/npx-search': typeof SettingsMcpNpxSearchRoute
'/settings/mcp/servers': typeof SettingsMcpServersRoute
'/settings/mcp': typeof SettingsMcpIndexRoute
'/settings/mcp/settings/$serverId': typeof SettingsMcpSettingsServerIdRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/settings': typeof SettingsRoute
'/chat': typeof ChatRoute
'/code': typeof CodeRoute
'/files': typeof FilesRoute
'/knowledge': typeof KnowledgeRoute
'/notes': typeof NotesRoute
'/settings': typeof SettingsRouteWithChildren
'/store': typeof StoreRoute
'/translate': typeof TranslateRoute
'/apps/$appId': typeof AppsAppIdRoute
'/paintings/$': typeof PaintingsSplatRoute
'/settings/about': typeof SettingsAboutRoute
'/settings/api-server': typeof SettingsApiServerRoute
'/settings/data': typeof SettingsDataRoute
'/settings/display': typeof SettingsDisplayRoute
'/settings/docprocess': typeof SettingsDocprocessRoute
'/settings/general': typeof SettingsGeneralRoute
'/settings/mcp': typeof SettingsMcpRouteWithChildren
'/settings/memory': typeof SettingsMemoryRoute
'/settings/model': typeof SettingsModelRoute
'/settings/notes': typeof SettingsNotesRoute
'/settings/provider': typeof SettingsProviderRoute
'/settings/quickAssistant': typeof SettingsQuickAssistantRoute
'/settings/quickphrase': typeof SettingsQuickphraseRoute
'/settings/selectionAssistant': typeof SettingsSelectionAssistantRoute
'/settings/shortcut': typeof SettingsShortcutRoute
'/settings/websearch': typeof SettingsWebsearchRoute
'/apps/': typeof AppsIndexRoute
'/paintings/': typeof PaintingsIndexRoute
'/settings/': typeof SettingsIndexRoute
'/settings/mcp/$': typeof SettingsMcpSplatRoute
'/settings/mcp/builtin': typeof SettingsMcpBuiltinRoute
'/settings/mcp/marketplaces': typeof SettingsMcpMarketplacesRoute
'/settings/mcp/mcp-install': typeof SettingsMcpMcpInstallRoute
'/settings/mcp/npx-search': typeof SettingsMcpNpxSearchRoute
'/settings/mcp/servers': typeof SettingsMcpServersRoute
'/settings/mcp/': typeof SettingsMcpIndexRoute
'/settings/mcp/settings/$serverId': typeof SettingsMcpSettingsServerIdRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/settings'
fullPaths:
| '/'
| '/chat'
| '/code'
| '/files'
| '/knowledge'
| '/notes'
| '/settings'
| '/store'
| '/translate'
| '/apps/$appId'
| '/paintings/$'
| '/settings/about'
| '/settings/api-server'
| '/settings/data'
| '/settings/display'
| '/settings/docprocess'
| '/settings/general'
| '/settings/mcp'
| '/settings/memory'
| '/settings/model'
| '/settings/notes'
| '/settings/provider'
| '/settings/quickAssistant'
| '/settings/quickphrase'
| '/settings/selectionAssistant'
| '/settings/shortcut'
| '/settings/websearch'
| '/apps'
| '/paintings'
| '/settings/'
| '/settings/mcp/$'
| '/settings/mcp/builtin'
| '/settings/mcp/marketplaces'
| '/settings/mcp/mcp-install'
| '/settings/mcp/npx-search'
| '/settings/mcp/servers'
| '/settings/mcp/'
| '/settings/mcp/settings/$serverId'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/settings'
id: '__root__' | '/' | '/settings'
to:
| '/'
| '/chat'
| '/code'
| '/files'
| '/knowledge'
| '/notes'
| '/store'
| '/translate'
| '/apps/$appId'
| '/paintings/$'
| '/settings/about'
| '/settings/api-server'
| '/settings/data'
| '/settings/display'
| '/settings/docprocess'
| '/settings/general'
| '/settings/memory'
| '/settings/model'
| '/settings/notes'
| '/settings/provider'
| '/settings/quickAssistant'
| '/settings/quickphrase'
| '/settings/selectionAssistant'
| '/settings/shortcut'
| '/settings/websearch'
| '/apps'
| '/paintings'
| '/settings'
| '/settings/mcp/$'
| '/settings/mcp/builtin'
| '/settings/mcp/marketplaces'
| '/settings/mcp/mcp-install'
| '/settings/mcp/npx-search'
| '/settings/mcp/servers'
| '/settings/mcp'
| '/settings/mcp/settings/$serverId'
id:
| '__root__'
| '/'
| '/chat'
| '/code'
| '/files'
| '/knowledge'
| '/notes'
| '/settings'
| '/store'
| '/translate'
| '/apps/$appId'
| '/paintings/$'
| '/settings/about'
| '/settings/api-server'
| '/settings/data'
| '/settings/display'
| '/settings/docprocess'
| '/settings/general'
| '/settings/mcp'
| '/settings/memory'
| '/settings/model'
| '/settings/notes'
| '/settings/provider'
| '/settings/quickAssistant'
| '/settings/quickphrase'
| '/settings/selectionAssistant'
| '/settings/shortcut'
| '/settings/websearch'
| '/apps/'
| '/paintings/'
| '/settings/'
| '/settings/mcp/$'
| '/settings/mcp/builtin'
| '/settings/mcp/marketplaces'
| '/settings/mcp/mcp-install'
| '/settings/mcp/npx-search'
| '/settings/mcp/servers'
| '/settings/mcp/'
| '/settings/mcp/settings/$serverId'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
SettingsRoute: typeof SettingsRoute
ChatRoute: typeof ChatRoute
CodeRoute: typeof CodeRoute
FilesRoute: typeof FilesRoute
KnowledgeRoute: typeof KnowledgeRoute
NotesRoute: typeof NotesRoute
SettingsRoute: typeof SettingsRouteWithChildren
StoreRoute: typeof StoreRoute
TranslateRoute: typeof TranslateRoute
AppsAppIdRoute: typeof AppsAppIdRoute
PaintingsSplatRoute: typeof PaintingsSplatRoute
AppsIndexRoute: typeof AppsIndexRoute
PaintingsIndexRoute: typeof PaintingsIndexRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/translate': {
id: '/translate'
path: '/translate'
fullPath: '/translate'
preLoaderRoute: typeof TranslateRouteImport
parentRoute: typeof rootRouteImport
}
'/store': {
id: '/store'
path: '/store'
fullPath: '/store'
preLoaderRoute: typeof StoreRouteImport
parentRoute: typeof rootRouteImport
}
'/settings': {
id: '/settings'
path: '/settings'
@ -58,6 +520,41 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SettingsRouteImport
parentRoute: typeof rootRouteImport
}
'/notes': {
id: '/notes'
path: '/notes'
fullPath: '/notes'
preLoaderRoute: typeof NotesRouteImport
parentRoute: typeof rootRouteImport
}
'/knowledge': {
id: '/knowledge'
path: '/knowledge'
fullPath: '/knowledge'
preLoaderRoute: typeof KnowledgeRouteImport
parentRoute: typeof rootRouteImport
}
'/files': {
id: '/files'
path: '/files'
fullPath: '/files'
preLoaderRoute: typeof FilesRouteImport
parentRoute: typeof rootRouteImport
}
'/code': {
id: '/code'
path: '/code'
fullPath: '/code'
preLoaderRoute: typeof CodeRouteImport
parentRoute: typeof rootRouteImport
}
'/chat': {
id: '/chat'
path: '/chat'
fullPath: '/chat'
preLoaderRoute: typeof ChatRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
@ -65,12 +562,296 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/settings/': {
id: '/settings/'
path: '/'
fullPath: '/settings/'
preLoaderRoute: typeof SettingsIndexRouteImport
parentRoute: typeof SettingsRoute
}
'/paintings/': {
id: '/paintings/'
path: '/paintings'
fullPath: '/paintings'
preLoaderRoute: typeof PaintingsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/apps/': {
id: '/apps/'
path: '/apps'
fullPath: '/apps'
preLoaderRoute: typeof AppsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/settings/websearch': {
id: '/settings/websearch'
path: '/websearch'
fullPath: '/settings/websearch'
preLoaderRoute: typeof SettingsWebsearchRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/shortcut': {
id: '/settings/shortcut'
path: '/shortcut'
fullPath: '/settings/shortcut'
preLoaderRoute: typeof SettingsShortcutRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/selectionAssistant': {
id: '/settings/selectionAssistant'
path: '/selectionAssistant'
fullPath: '/settings/selectionAssistant'
preLoaderRoute: typeof SettingsSelectionAssistantRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/quickphrase': {
id: '/settings/quickphrase'
path: '/quickphrase'
fullPath: '/settings/quickphrase'
preLoaderRoute: typeof SettingsQuickphraseRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/quickAssistant': {
id: '/settings/quickAssistant'
path: '/quickAssistant'
fullPath: '/settings/quickAssistant'
preLoaderRoute: typeof SettingsQuickAssistantRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/provider': {
id: '/settings/provider'
path: '/provider'
fullPath: '/settings/provider'
preLoaderRoute: typeof SettingsProviderRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/notes': {
id: '/settings/notes'
path: '/notes'
fullPath: '/settings/notes'
preLoaderRoute: typeof SettingsNotesRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/model': {
id: '/settings/model'
path: '/model'
fullPath: '/settings/model'
preLoaderRoute: typeof SettingsModelRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/memory': {
id: '/settings/memory'
path: '/memory'
fullPath: '/settings/memory'
preLoaderRoute: typeof SettingsMemoryRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/mcp': {
id: '/settings/mcp'
path: '/mcp'
fullPath: '/settings/mcp'
preLoaderRoute: typeof SettingsMcpRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/general': {
id: '/settings/general'
path: '/general'
fullPath: '/settings/general'
preLoaderRoute: typeof SettingsGeneralRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/docprocess': {
id: '/settings/docprocess'
path: '/docprocess'
fullPath: '/settings/docprocess'
preLoaderRoute: typeof SettingsDocprocessRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/display': {
id: '/settings/display'
path: '/display'
fullPath: '/settings/display'
preLoaderRoute: typeof SettingsDisplayRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/data': {
id: '/settings/data'
path: '/data'
fullPath: '/settings/data'
preLoaderRoute: typeof SettingsDataRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/api-server': {
id: '/settings/api-server'
path: '/api-server'
fullPath: '/settings/api-server'
preLoaderRoute: typeof SettingsApiServerRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/about': {
id: '/settings/about'
path: '/about'
fullPath: '/settings/about'
preLoaderRoute: typeof SettingsAboutRouteImport
parentRoute: typeof SettingsRoute
}
'/paintings/$': {
id: '/paintings/$'
path: '/paintings/$'
fullPath: '/paintings/$'
preLoaderRoute: typeof PaintingsSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/apps/$appId': {
id: '/apps/$appId'
path: '/apps/$appId'
fullPath: '/apps/$appId'
preLoaderRoute: typeof AppsAppIdRouteImport
parentRoute: typeof rootRouteImport
}
'/settings/mcp/': {
id: '/settings/mcp/'
path: '/'
fullPath: '/settings/mcp/'
preLoaderRoute: typeof SettingsMcpIndexRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/servers': {
id: '/settings/mcp/servers'
path: '/servers'
fullPath: '/settings/mcp/servers'
preLoaderRoute: typeof SettingsMcpServersRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/npx-search': {
id: '/settings/mcp/npx-search'
path: '/npx-search'
fullPath: '/settings/mcp/npx-search'
preLoaderRoute: typeof SettingsMcpNpxSearchRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/mcp-install': {
id: '/settings/mcp/mcp-install'
path: '/mcp-install'
fullPath: '/settings/mcp/mcp-install'
preLoaderRoute: typeof SettingsMcpMcpInstallRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/marketplaces': {
id: '/settings/mcp/marketplaces'
path: '/marketplaces'
fullPath: '/settings/mcp/marketplaces'
preLoaderRoute: typeof SettingsMcpMarketplacesRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/builtin': {
id: '/settings/mcp/builtin'
path: '/builtin'
fullPath: '/settings/mcp/builtin'
preLoaderRoute: typeof SettingsMcpBuiltinRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/$': {
id: '/settings/mcp/$'
path: '/$'
fullPath: '/settings/mcp/$'
preLoaderRoute: typeof SettingsMcpSplatRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/settings/$serverId': {
id: '/settings/mcp/settings/$serverId'
path: '/settings/$serverId'
fullPath: '/settings/mcp/settings/$serverId'
preLoaderRoute: typeof SettingsMcpSettingsServerIdRouteImport
parentRoute: typeof SettingsMcpRoute
}
}
}
interface SettingsMcpRouteChildren {
SettingsMcpSplatRoute: typeof SettingsMcpSplatRoute
SettingsMcpBuiltinRoute: typeof SettingsMcpBuiltinRoute
SettingsMcpMarketplacesRoute: typeof SettingsMcpMarketplacesRoute
SettingsMcpMcpInstallRoute: typeof SettingsMcpMcpInstallRoute
SettingsMcpNpxSearchRoute: typeof SettingsMcpNpxSearchRoute
SettingsMcpServersRoute: typeof SettingsMcpServersRoute
SettingsMcpIndexRoute: typeof SettingsMcpIndexRoute
SettingsMcpSettingsServerIdRoute: typeof SettingsMcpSettingsServerIdRoute
}
const SettingsMcpRouteChildren: SettingsMcpRouteChildren = {
SettingsMcpSplatRoute: SettingsMcpSplatRoute,
SettingsMcpBuiltinRoute: SettingsMcpBuiltinRoute,
SettingsMcpMarketplacesRoute: SettingsMcpMarketplacesRoute,
SettingsMcpMcpInstallRoute: SettingsMcpMcpInstallRoute,
SettingsMcpNpxSearchRoute: SettingsMcpNpxSearchRoute,
SettingsMcpServersRoute: SettingsMcpServersRoute,
SettingsMcpIndexRoute: SettingsMcpIndexRoute,
SettingsMcpSettingsServerIdRoute: SettingsMcpSettingsServerIdRoute,
}
const SettingsMcpRouteWithChildren = SettingsMcpRoute._addFileChildren(
SettingsMcpRouteChildren,
)
interface SettingsRouteChildren {
SettingsAboutRoute: typeof SettingsAboutRoute
SettingsApiServerRoute: typeof SettingsApiServerRoute
SettingsDataRoute: typeof SettingsDataRoute
SettingsDisplayRoute: typeof SettingsDisplayRoute
SettingsDocprocessRoute: typeof SettingsDocprocessRoute
SettingsGeneralRoute: typeof SettingsGeneralRoute
SettingsMcpRoute: typeof SettingsMcpRouteWithChildren
SettingsMemoryRoute: typeof SettingsMemoryRoute
SettingsModelRoute: typeof SettingsModelRoute
SettingsNotesRoute: typeof SettingsNotesRoute
SettingsProviderRoute: typeof SettingsProviderRoute
SettingsQuickAssistantRoute: typeof SettingsQuickAssistantRoute
SettingsQuickphraseRoute: typeof SettingsQuickphraseRoute
SettingsSelectionAssistantRoute: typeof SettingsSelectionAssistantRoute
SettingsShortcutRoute: typeof SettingsShortcutRoute
SettingsWebsearchRoute: typeof SettingsWebsearchRoute
SettingsIndexRoute: typeof SettingsIndexRoute
}
const SettingsRouteChildren: SettingsRouteChildren = {
SettingsAboutRoute: SettingsAboutRoute,
SettingsApiServerRoute: SettingsApiServerRoute,
SettingsDataRoute: SettingsDataRoute,
SettingsDisplayRoute: SettingsDisplayRoute,
SettingsDocprocessRoute: SettingsDocprocessRoute,
SettingsGeneralRoute: SettingsGeneralRoute,
SettingsMcpRoute: SettingsMcpRouteWithChildren,
SettingsMemoryRoute: SettingsMemoryRoute,
SettingsModelRoute: SettingsModelRoute,
SettingsNotesRoute: SettingsNotesRoute,
SettingsProviderRoute: SettingsProviderRoute,
SettingsQuickAssistantRoute: SettingsQuickAssistantRoute,
SettingsQuickphraseRoute: SettingsQuickphraseRoute,
SettingsSelectionAssistantRoute: SettingsSelectionAssistantRoute,
SettingsShortcutRoute: SettingsShortcutRoute,
SettingsWebsearchRoute: SettingsWebsearchRoute,
SettingsIndexRoute: SettingsIndexRoute,
}
const SettingsRouteWithChildren = SettingsRoute._addFileChildren(
SettingsRouteChildren,
)
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
SettingsRoute: SettingsRoute,
ChatRoute: ChatRoute,
CodeRoute: CodeRoute,
FilesRoute: FilesRoute,
KnowledgeRoute: KnowledgeRoute,
NotesRoute: NotesRoute,
SettingsRoute: SettingsRouteWithChildren,
StoreRoute: StoreRoute,
TranslateRoute: TranslateRoute,
AppsAppIdRoute: AppsAppIdRoute,
PaintingsSplatRoute: PaintingsSplatRoute,
AppsIndexRoute: AppsIndexRoute,
PaintingsIndexRoute: PaintingsIndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View File

@ -23,9 +23,9 @@ import {
} from '@renderer/utils/messageUtils/create'
import { filterContextMessages } from '@renderer/utils/messageUtils/filters'
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import type { UseNavigateResult } from '@tanstack/react-router'
import dayjs from 'dayjs'
import { t } from 'i18next'
import type { NavigateFunction } from 'react-router'
import { getAssistantById, getAssistantProvider, getDefaultModel } from './AssistantService'
import { EVENT_NAMES, EventEmitter } from './EventService'
@ -68,14 +68,14 @@ export function deleteMessageFiles(message: Message) {
})
}
export async function locateToMessage(navigate: NavigateFunction, message: Message) {
export async function locateToMessage(navigate: UseNavigateResult<string>, message: Message) {
await modelGenerating()
SearchPopup.hide()
const assistant = getAssistantById(message.assistantId)
const topic = await getTopicById(message.topicId)
navigate('/', { state: { assistant, topic } })
navigate({ to: '/chat', search: { assistantId: assistant?.id, topicId: topic?.id } })
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id), 300)

View File

@ -1,14 +1,15 @@
import type { NavigateFunction } from 'react-router-dom'
import type { UseNavigateResult } from '@tanstack/react-router'
// Tab 导航服务 - 用于在非 React 组件中进行路由导航
interface INavigationService {
navigate: NavigateFunction | null
setNavigate: (navigateFunc: NavigateFunction) => void
navigate: UseNavigateResult<string> | null
setNavigate: (navigateFunc: UseNavigateResult<string>) => void
}
const NavigationService: INavigationService = {
navigate: null,
setNavigate: (navigateFunc: NavigateFunction): void => {
setNavigate: (navigateFunc: UseNavigateResult<string>): void => {
NavigationService.navigate = navigateFunc
window.navigate = NavigationService.navigate
}

View File

@ -55,12 +55,12 @@ class TabsService {
// 使用 NavigationService 导航到新的标签页
if (NavigationService.navigate) {
NavigationService.navigate(lastTab.path)
NavigationService.navigate({ to: lastTab.path })
} else {
logger.warn('Navigation service not ready, will navigate on next render')
setTimeout(() => {
if (NavigationService.navigate) {
NavigationService.navigate(lastTab.path)
NavigationService.navigate({ to: lastTab.path })
}
}, 100)
}
@ -133,7 +133,7 @@ class TabsService {
// 导航到对应页面
if (NavigationService.navigate) {
NavigationService.navigate(tab.path)
NavigationService.navigate({ to: tab.path })
}
return true