From b59ee0aabe44603459f4e6b2726ff209b226bb4d Mon Sep 17 00:00:00 2001 From: Shinji-Li Date: Wed, 22 Oct 2025 23:35:49 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20change=20d?= =?UTF-8?q?iscover=20page=20from=20RSC=20to=20SPA=20to=20improve=20perform?= =?UTF-8?q?ance=20(#9828)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: change discord page to spa * fix: change locals * feat: update router change * fix: revert some files * feat: add model provider detail page use link * fix: add trpc back * feat: update e2e timeout time * feat: change discord page to spa * fix: change locals * feat: update router change * fix: revert some files * feat: add model provider detail page use link * fix: add trpc back * feat: update e2e timeout time * fix: use reactrouter-dom link replace next link --- .../src/main/controllers/BrowserWindowsCtr.ts | 51 ++++-- .../__tests__/BrowserWindowsCtr.test.ts | 21 ++- .../src/main/core/browser/BrowserManager.ts | 74 ++++++-- package.json | 2 + .../electron-client-ipc/src/events/index.ts | 2 + .../electron-client-ipc/src/events/windows.ts | 51 ++++-- packages/utils/src/server/responsive.ts | 2 +- playwright.config.ts | 2 +- .../(backend)/trpc/desktop/[trpc]/route.ts | 5 + .../(detail)/_layout/DetailLayout.tsx | 22 +++ .../(detail)/_layout/Mobile/Header.tsx | 13 +- .../assistant/AssistantDetailPage.tsx | 47 +++++ .../assistant/[...slugs]/features/Header.tsx | 19 +- .../features/Sidebar/Related/index.tsx | 6 +- .../(detail)/assistant/[...slugs]/page.tsx | 110 ------------ .../discover/(detail)/components/NotFound.tsx | 23 +++ .../discover/(detail)/features/Back.tsx | 4 +- .../discover/(detail)/features/Breadcrumb.tsx | 7 +- .../(main)/discover/(detail)/layout.tsx | 12 -- .../discover/(detail)/mcp/McpDetailPage.tsx | 51 ++++++ .../[slug]/features/Sidebar/Related/index.tsx | 6 +- .../discover/(detail)/mcp/[slug]/page.tsx | 103 ----------- .../(detail)/model/ModelDetailPage.tsx | 44 +++++ .../features/Sidebar/Related/index.tsx | 6 +- .../(detail)/model/[...slugs]/page.tsx | 104 ----------- .../(detail)/provider/ProviderDetailPage.tsx | 44 +++++ .../Sidebar/ActionButton/ProviderConfig.tsx | 18 +- .../features/Sidebar/Related/index.tsx | 6 +- .../(detail)/provider/[...slugs]/page.tsx | 103 ----------- .../discover/(list)/(home)/HomePage.tsx | 45 +++++ .../(main)/discover/(list)/(home)/page.tsx | 44 ----- .../discover/(list)/_layout/Desktop/Nav.tsx | 15 +- .../discover/(list)/_layout/ListLayout.tsx | 22 +++ .../discover/(list)/_layout/Mobile/Nav.tsx | 9 +- .../(list)/assistant/AssistantLayout.tsx | 21 +++ .../(list)/assistant/AssistantPage.tsx | 44 +++++ .../assistant/features/Category/index.tsx | 11 +- .../(list)/assistant/features/List/Item.tsx | 16 +- .../discover/(list)/assistant/layout.tsx | 12 -- .../(main)/discover/(list)/assistant/page.tsx | 46 ----- .../discover/(list)/features/Pagination.tsx | 15 +- .../(main)/discover/(list)/layout.tsx | 12 -- .../(main)/discover/(list)/mcp/McpLayout.tsx | 21 +++ .../(main)/discover/(list)/mcp/McpPage.tsx | 44 +++++ .../(list)/mcp/features/Category/index.tsx | 11 +- .../(list)/mcp/features/List/Item.tsx | 20 ++- .../(main)/discover/(list)/mcp/layout.tsx | 12 -- .../(main)/discover/(list)/mcp/page.tsx | 46 ----- .../discover/(list)/model/ModelLayout.tsx | 21 +++ .../discover/(list)/model/ModelPage.tsx | 44 +++++ .../discover/(list)/model/_layout/Desktop.tsx | 2 +- .../(list)/model/features/Category/index.tsx | 11 +- .../(list)/model/features/List/Item.tsx | 11 +- .../(main)/discover/(list)/model/layout.tsx | 12 -- .../(main)/discover/(list)/model/page.tsx | 44 ----- .../discover/(list)/provider/ProviderPage.tsx | 43 +++++ .../(list)/provider/features/List/Item.tsx | 27 +-- .../(main)/discover/(list)/provider/page.tsx | 44 ----- .../(main)/discover/DiscoverRouter.tsx | 167 ++++++++++++++++++ .../(main)/discover/[[...path]]/page.tsx | 11 ++ .../discover/_layout/Desktop/Header.tsx | 4 +- .../discover/_layout/DiscoverLayout.tsx | 22 +++ .../(main)/discover/components/Title.tsx | 7 +- .../(main)/discover/features/Title.tsx | 47 +++-- .../(main)/discover/features/useNav.tsx | 23 ++- src/app/[variants]/(main)/discover/layout.tsx | 12 -- .../(main)/labs/components/LabCard.tsx | 13 +- src/layout/GlobalProvider/Query.tsx | 9 +- 68 files changed, 1080 insertions(+), 918 deletions(-) create mode 100644 src/app/[variants]/(main)/discover/(detail)/_layout/DetailLayout.tsx create mode 100644 src/app/[variants]/(main)/discover/(detail)/assistant/AssistantDetailPage.tsx delete mode 100644 src/app/[variants]/(main)/discover/(detail)/assistant/[...slugs]/page.tsx create mode 100644 src/app/[variants]/(main)/discover/(detail)/components/NotFound.tsx delete mode 100644 src/app/[variants]/(main)/discover/(detail)/layout.tsx create mode 100644 src/app/[variants]/(main)/discover/(detail)/mcp/McpDetailPage.tsx delete mode 100644 src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/page.tsx create mode 100644 src/app/[variants]/(main)/discover/(detail)/model/ModelDetailPage.tsx delete mode 100644 src/app/[variants]/(main)/discover/(detail)/model/[...slugs]/page.tsx create mode 100644 src/app/[variants]/(main)/discover/(detail)/provider/ProviderDetailPage.tsx delete mode 100644 src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/page.tsx create mode 100644 src/app/[variants]/(main)/discover/(list)/(home)/HomePage.tsx delete mode 100644 src/app/[variants]/(main)/discover/(list)/(home)/page.tsx create mode 100644 src/app/[variants]/(main)/discover/(list)/_layout/ListLayout.tsx create mode 100644 src/app/[variants]/(main)/discover/(list)/assistant/AssistantLayout.tsx create mode 100644 src/app/[variants]/(main)/discover/(list)/assistant/AssistantPage.tsx delete mode 100644 src/app/[variants]/(main)/discover/(list)/assistant/layout.tsx delete mode 100644 src/app/[variants]/(main)/discover/(list)/assistant/page.tsx delete mode 100644 src/app/[variants]/(main)/discover/(list)/layout.tsx create mode 100644 src/app/[variants]/(main)/discover/(list)/mcp/McpLayout.tsx create mode 100644 src/app/[variants]/(main)/discover/(list)/mcp/McpPage.tsx delete mode 100644 src/app/[variants]/(main)/discover/(list)/mcp/layout.tsx delete mode 100644 src/app/[variants]/(main)/discover/(list)/mcp/page.tsx create mode 100644 src/app/[variants]/(main)/discover/(list)/model/ModelLayout.tsx create mode 100644 src/app/[variants]/(main)/discover/(list)/model/ModelPage.tsx delete mode 100644 src/app/[variants]/(main)/discover/(list)/model/layout.tsx delete mode 100644 src/app/[variants]/(main)/discover/(list)/model/page.tsx create mode 100644 src/app/[variants]/(main)/discover/(list)/provider/ProviderPage.tsx delete mode 100644 src/app/[variants]/(main)/discover/(list)/provider/page.tsx create mode 100644 src/app/[variants]/(main)/discover/DiscoverRouter.tsx create mode 100644 src/app/[variants]/(main)/discover/[[...path]]/page.tsx create mode 100644 src/app/[variants]/(main)/discover/_layout/DiscoverLayout.tsx delete mode 100644 src/app/[variants]/(main)/discover/layout.tsx diff --git a/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts b/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts index 96e7affe015..3671d858e48 100644 --- a/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +++ b/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts @@ -1,7 +1,11 @@ -import { InterceptRouteParams } from '@lobechat/electron-client-ipc'; +import { InterceptRouteParams, OpenSettingsWindowOptions } from '@lobechat/electron-client-ipc'; import { extractSubPath, findMatchingRoute } from '~common/routes'; -import { AppBrowsersIdentifiers, BrowsersIdentifiers, WindowTemplateIdentifiers } from '@/appBrowsers'; +import { + AppBrowsersIdentifiers, + BrowsersIdentifiers, + WindowTemplateIdentifiers, +} from '@/appBrowsers'; import { IpcClientEventSender } from '@/types/ipcClientEvent'; import { ControllerModule, ipcClientEvent, shortcut } from './index'; @@ -14,11 +18,16 @@ export default class BrowserWindowsCtr extends ControllerModule { } @ipcClientEvent('openSettingsWindow') - async openSettingsWindow(tab?: string) { - console.log('[BrowserWindowsCtr] Received request to open settings window', tab); + async openSettingsWindow(options?: string | OpenSettingsWindowOptions) { + const normalizedOptions: OpenSettingsWindowOptions = + typeof options === 'string' || options === undefined + ? { tab: typeof options === 'string' ? options : undefined } + : options; + + console.log('[BrowserWindowsCtr] Received request to open settings window', normalizedOptions); try { - await this.app.browserManager.showSettingsWindowWithTab(tab); + await this.app.browserManager.showSettingsWindowWithTab(normalizedOptions); return { success: true }; } catch (error) { @@ -68,15 +77,37 @@ export default class BrowserWindowsCtr extends ControllerModule { try { if (matchedRoute.targetWindow === BrowsersIdentifiers.settings) { - const subPath = extractSubPath(path, matchedRoute.pathPrefix); - - await this.app.browserManager.showSettingsWindowWithTab(subPath); + const extractedSubPath = extractSubPath(path, matchedRoute.pathPrefix); + const sanitizedSubPath = + extractedSubPath && !extractedSubPath.startsWith('?') ? extractedSubPath : undefined; + let searchParams: Record | undefined; + try { + const url = new URL(params.url); + const entries = Array.from(url.searchParams.entries()); + if (entries.length > 0) { + searchParams = entries.reduce>((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}); + } + } catch (error) { + console.warn( + '[BrowserWindowsCtr] Failed to parse URL for settings route interception:', + params.url, + error, + ); + } + + await this.app.browserManager.showSettingsWindowWithTab({ + searchParams, + tab: sanitizedSubPath, + }); return { intercepted: true, path, source, - subPath, + subPath: sanitizedSubPath, targetWindow: matchedRoute.targetWindow, }; } else { @@ -105,8 +136,8 @@ export default class BrowserWindowsCtr extends ControllerModule { */ @ipcClientEvent('createMultiInstanceWindow') async createMultiInstanceWindow(params: { - templateId: WindowTemplateIdentifiers; path: string; + templateId: WindowTemplateIdentifiers; uniqueId?: string; }) { try { diff --git a/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts b/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts index 9b67768f2e8..6665519f322 100644 --- a/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts +++ b/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts @@ -64,7 +64,7 @@ describe('BrowserWindowsCtr', () => { it('should show the settings window with the specified tab', async () => { const tab = 'appearance'; const result = await browserWindowsCtr.openSettingsWindow(tab); - expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith(tab); + expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({ tab }); expect(result).toEqual({ success: true }); }); @@ -120,11 +120,11 @@ describe('BrowserWindowsCtr', () => { it('should show settings window if matched route target is settings', async () => { const params: InterceptRouteParams = { ...baseParams, - path: '/settings?active=common', - url: 'app://host/settings?active=common', + path: '/settings/provider', + url: 'app://host/settings/provider?active=provider&provider=ollama', }; const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' }; - const subPath = 'common'; + const subPath = 'provider'; (findMatchingRoute as Mock).mockReturnValue(matchedRoute); (extractSubPath as Mock).mockReturnValue(subPath); @@ -132,7 +132,10 @@ describe('BrowserWindowsCtr', () => { expect(findMatchingRoute).toHaveBeenCalledWith(params.path); expect(extractSubPath).toHaveBeenCalledWith(params.path, matchedRoute.pathPrefix); - expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith(subPath); + expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({ + searchParams: { active: 'provider', provider: 'ollama' }, + tab: subPath, + }); expect(result).toEqual({ intercepted: true, path: params.path, @@ -170,11 +173,11 @@ describe('BrowserWindowsCtr', () => { it('should return error if processing route interception fails for settings', async () => { const params: InterceptRouteParams = { ...baseParams, - path: '/settings?active=general', + path: '/settings', url: 'app://host/settings?active=general', }; const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' }; - const subPath = 'general'; + const subPath = undefined; const errorMessage = 'Processing error for settings'; (findMatchingRoute as Mock).mockReturnValue(matchedRoute); (extractSubPath as Mock).mockReturnValue(subPath); @@ -182,6 +185,10 @@ describe('BrowserWindowsCtr', () => { const result = await browserWindowsCtr.interceptRoute(params); + expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith({ + searchParams: { active: 'general' }, + tab: subPath, + }); expect(result).toEqual({ error: errorMessage, intercepted: false, diff --git a/apps/desktop/src/main/core/browser/BrowserManager.ts b/apps/desktop/src/main/core/browser/BrowserManager.ts index 11323a6cd29..a9759e9c7f4 100644 --- a/apps/desktop/src/main/core/browser/BrowserManager.ts +++ b/apps/desktop/src/main/core/browser/BrowserManager.ts @@ -1,9 +1,18 @@ -import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc'; +import { + MainBroadcastEventKey, + MainBroadcastParams, + OpenSettingsWindowOptions, +} from '@lobechat/electron-client-ipc'; import { WebContents } from 'electron'; import { createLogger } from '@/utils/logger'; -import { AppBrowsersIdentifiers, appBrowsers, WindowTemplate, WindowTemplateIdentifiers, windowTemplates } from '../../appBrowsers'; +import { + AppBrowsersIdentifiers, + WindowTemplateIdentifiers, + appBrowsers, + windowTemplates, +} from '../../appBrowsers'; import type { App } from '../App'; import type { BrowserWindowOpts } from './Browser'; import Browser from './Browser'; @@ -63,14 +72,35 @@ export class BrowserManager { * Display the settings window and navigate to a specific tab * @param tab Settings window sub-path tab */ - async showSettingsWindowWithTab(tab?: string) { - logger.debug(`Showing settings window with tab: ${tab || 'default'}`); - // common is the main path for settings route - if (tab && tab !== 'common') { - const browser = await this.redirectToPage('settings', tab); + async showSettingsWindowWithTab(options?: OpenSettingsWindowOptions) { + const tab = options?.tab; + const searchParams = options?.searchParams; + + const query = new URLSearchParams(); + if (searchParams) { + Object.entries(searchParams).forEach(([key, value]) => { + if (value !== undefined) query.set(key, value); + }); + } + + if (tab && tab !== 'common' && !query.has('active')) { + query.set('active', tab); + } + + const queryString = query.toString(); + const activeTab = query.get('active') ?? tab; + + logger.debug( + `Showing settings window with navigation: active=${activeTab || 'default'}, query=${ + queryString || 'none' + }`, + ); + + if (queryString) { + const browser = await this.redirectToPage('settings', undefined, queryString); // make provider page more large - if (tab.startsWith('provider/')) { + if (activeTab?.startsWith('provider')) { logger.debug('Resizing window for provider settings'); browser.setWindowSize({ height: 1000, width: 1400 }); browser.moveToCenter(); @@ -87,7 +117,7 @@ export class BrowserManager { * @param identifier Window identifier * @param subPath Sub-path, such as 'agent', 'about', etc. */ - async redirectToPage(identifier: string, subPath?: string) { + async redirectToPage(identifier: string, subPath?: string, search?: string) { try { // Ensure window is retrieved or created const browser = this.retrieveByIdentifier(identifier); @@ -105,11 +135,14 @@ export class BrowserManager { // Build complete URL path const fullPath = subPath ? `${baseRoute}/${subPath}` : baseRoute; + const normalizedSearch = + search && search.length > 0 ? (search.startsWith('?') ? search : `?${search}`) : ''; + const fullUrl = `${fullPath}${normalizedSearch}`; - logger.debug(`Redirecting to: ${fullPath}`); + logger.debug(`Redirecting to: ${fullUrl}`); // Load URL and show window - await browser.loadUrl(fullPath); + await browser.loadUrl(fullUrl); browser.show(); return browser; @@ -143,14 +176,20 @@ export class BrowserManager { * @param uniqueId Optional unique identifier, will be generated if not provided * @returns The window identifier and Browser instance */ - createMultiInstanceWindow(templateId: WindowTemplateIdentifiers, path: string, uniqueId?: string) { + createMultiInstanceWindow( + templateId: WindowTemplateIdentifiers, + path: string, + uniqueId?: string, + ) { const template = windowTemplates[templateId]; if (!template) { throw new Error(`Window template ${templateId} not found`); } // Generate unique identifier - const windowId = uniqueId || `${template.baseIdentifier}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const windowId = + uniqueId || + `${template.baseIdentifier}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`; // Create browser options from template const browserOpts: BrowserWindowOpts = { @@ -164,8 +203,8 @@ export class BrowserManager { const browser = this.retrieveOrInitialize(browserOpts); return { - identifier: windowId, browser: browser, + identifier: windowId, }; } @@ -176,7 +215,7 @@ export class BrowserManager { */ getWindowsByTemplate(templateId: string): string[] { const prefix = `${templateId}_`; - return Array.from(this.browsers.keys()).filter(id => id.startsWith(prefix)); + return Array.from(this.browsers.keys()).filter((id) => id.startsWith(prefix)); } /** @@ -185,7 +224,7 @@ export class BrowserManager { */ closeWindowsByTemplate(templateId: string): void { const windowIds = this.getWindowsByTemplate(templateId); - windowIds.forEach(id => { + windowIds.forEach((id) => { const browser = this.browsers.get(id); if (browser) { browser.close(); @@ -235,8 +274,7 @@ export class BrowserManager { }); browser.browserWindow.on('show', () => { - if (browser.webContents) - this.webContentsMap.set(browser.webContents, browser.identifier); + if (browser.webContents) this.webContentsMap.set(browser.webContents, browser.identifier); }); return browser; diff --git a/package.json b/package.json index 8774f05520c..ef8b3f328e8 100644 --- a/package.json +++ b/package.json @@ -266,7 +266,9 @@ "react-layout-kit": "^2.0.0", "react-lazy-load": "^4.0.1", "react-pdf": "^9.2.1", + "react-responsive": "^10.0.1", "react-rnd": "^10.5.2", + "react-router-dom": "^7.9.4", "react-scan": "^0.4.3", "react-virtuoso": "^4.14.1", "react-wrap-balancer": "^1.1.1", diff --git a/packages/electron-client-ipc/src/events/index.ts b/packages/electron-client-ipc/src/events/index.ts index 6de3a54e290..b9788e7d219 100644 --- a/packages/electron-client-ipc/src/events/index.ts +++ b/packages/electron-client-ipc/src/events/index.ts @@ -50,3 +50,5 @@ export type MainBroadcastEventKey = keyof MainBroadcastEvents; export type MainBroadcastParams = Parameters< MainBroadcastEvents[T] >[0]; + +export type { OpenSettingsWindowOptions } from './windows'; diff --git a/packages/electron-client-ipc/src/events/windows.ts b/packages/electron-client-ipc/src/events/windows.ts index 14191590a99..093b127fec6 100644 --- a/packages/electron-client-ipc/src/events/windows.ts +++ b/packages/electron-client-ipc/src/events/windows.ts @@ -1,44 +1,50 @@ import { InterceptRouteParams, InterceptRouteResponse } from '../types/route'; +export interface OpenSettingsWindowOptions { + /** + * Query parameters that should be appended to the settings URL. + */ + searchParams?: Record; + /** + * Settings page tab path or identifier. + */ + tab?: string; +} + export interface CreateMultiInstanceWindowParams { - templateId: string; path: string; + templateId: string; uniqueId?: string; } export interface CreateMultiInstanceWindowResponse { + error?: string; success: boolean; windowId?: string; - error?: string; } export interface GetWindowsByTemplateResponse { + error?: string; success: boolean; windowIds?: string[]; - error?: string; } export interface WindowsDispatchEvents { /** - * 拦截客户端路由导航请求 - * @param params 包含路径和来源信息的参数对象 - * @returns 路由拦截结果 - */ - interceptRoute: (params: InterceptRouteParams) => InterceptRouteResponse; - - /** - * open the LobeHub Devtools + * Close all windows by template + * @param templateId Template identifier + * @returns Operation result */ - openDevtools: () => void; - - openSettingsWindow: (tab?: string) => void; + closeWindowsByTemplate: (templateId: string) => { error?: string, success: boolean; }; /** * Create a new multi-instance window * @param params Window creation parameters * @returns Creation result */ - createMultiInstanceWindow: (params: CreateMultiInstanceWindowParams) => CreateMultiInstanceWindowResponse; + createMultiInstanceWindow: ( + params: CreateMultiInstanceWindowParams, + ) => CreateMultiInstanceWindowResponse; /** * Get all windows by template @@ -48,9 +54,16 @@ export interface WindowsDispatchEvents { getWindowsByTemplate: (templateId: string) => GetWindowsByTemplateResponse; /** - * Close all windows by template - * @param templateId Template identifier - * @returns Operation result + * 拦截客户端路由导航请求 + * @param params 包含路径和来源信息的参数对象 + * @returns 路由拦截结果 */ - closeWindowsByTemplate: (templateId: string) => { success: boolean; error?: string }; + interceptRoute: (params: InterceptRouteParams) => InterceptRouteResponse; + + /** + * open the LobeHub Devtools + */ + openDevtools: () => void; + + openSettingsWindow: (options?: OpenSettingsWindowOptions | string) => void; } diff --git a/packages/utils/src/server/responsive.ts b/packages/utils/src/server/responsive.ts index ec7e51eb4fa..7f6d5c929ed 100644 --- a/packages/utils/src/server/responsive.ts +++ b/packages/utils/src/server/responsive.ts @@ -4,7 +4,7 @@ import { UAParser } from 'ua-parser-js'; /** * check mobile device in server */ -const isMobileDevice = async () => { +export const isMobileDevice = async () => { if (typeof process === 'undefined') { throw new Error('[Server method] you are importing a server-only module outside of server'); } diff --git a/playwright.config.ts b/playwright.config.ts index 017fa29ced3..4b8d08ba350 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ reporter: 'list', retries: 0, testDir: './e2e', - timeout: 60_000, + timeout: 1_200_000, use: { baseURL: `http://localhost:${PORT}`, trace: 'on-first-retry', diff --git a/src/app/(backend)/trpc/desktop/[trpc]/route.ts b/src/app/(backend)/trpc/desktop/[trpc]/route.ts index 8f82dc50953..987ca4b8650 100644 --- a/src/app/(backend)/trpc/desktop/[trpc]/route.ts +++ b/src/app/(backend)/trpc/desktop/[trpc]/route.ts @@ -20,6 +20,11 @@ const handler = (req: NextRequest) => }, req, + responseMeta({ ctx }) { + const headers = ctx?.resHeaders; + + return { headers }; + }, router: desktopRouter, }); diff --git a/src/app/[variants]/(main)/discover/(detail)/_layout/DetailLayout.tsx b/src/app/[variants]/(main)/discover/(detail)/_layout/DetailLayout.tsx new file mode 100644 index 00000000000..bfeafa61e45 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(detail)/_layout/DetailLayout.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { PropsWithChildren, memo } from 'react'; + +import Desktop from './Desktop'; +import Mobile from './Mobile'; + +interface DetailLayoutProps extends PropsWithChildren { + mobile?: boolean; +} + +const DetailLayout = memo(({ children, mobile }) => { + if (mobile) { + return {children}; + } + + return {children}; +}); + +DetailLayout.displayName = 'DetailLayout'; + +export default DetailLayout; diff --git a/src/app/[variants]/(main)/discover/(detail)/_layout/Mobile/Header.tsx b/src/app/[variants]/(main)/discover/(detail)/_layout/Mobile/Header.tsx index 9cec918152f..2aa9c144231 100644 --- a/src/app/[variants]/(main)/discover/(detail)/_layout/Mobile/Header.tsx +++ b/src/app/[variants]/(main)/discover/(detail)/_layout/Mobile/Header.tsx @@ -1,22 +1,21 @@ 'use client'; import { ChatHeader } from '@lobehub/ui/mobile'; -import { usePathname } from 'next/navigation'; -import { useRouter } from 'nextjs-toploader/app'; import { memo } from 'react'; -import urlJoin from 'url-join'; +import { useLocation, useNavigate } from 'react-router-dom'; import { mobileHeaderSticky } from '@/styles/mobileHeader'; const Header = memo(() => { - const pathname = usePathname(); - const router = useRouter(); + const location = useLocation(); + const navigate = useNavigate(); - const path = pathname.split('/').filter(Boolean)[1]; + // Extract the path segment (assistant, model, provider, mcp) + const path = location.pathname.split('/').find(Boolean); return ( router.push(urlJoin('/discover', path))} + onBackClick={() => navigate(`/${path}`)} showBackButton style={mobileHeaderSticky} /> diff --git a/src/app/[variants]/(main)/discover/(detail)/assistant/AssistantDetailPage.tsx b/src/app/[variants]/(main)/discover/(detail)/assistant/AssistantDetailPage.tsx new file mode 100644 index 00000000000..bb24a0a34d8 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(detail)/assistant/AssistantDetailPage.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { memo } from 'react'; +import { useParams } from 'react-router-dom'; +import { Flexbox } from 'react-layout-kit'; + +import { withSuspense } from '@/components/withSuspense'; +import { useDiscoverStore } from '@/store/discover'; +import { DiscoverTab } from '@/types/discover'; + +import Breadcrumb from '../features/Breadcrumb'; +import { TocProvider } from '../features/Toc/useToc'; +import NotFound from '../components/NotFound'; +import { DetailProvider } from './[...slugs]/features/DetailProvider'; +import Details from './[...slugs]/features/Details'; +import Header from './[...slugs]/features/Header'; +import Loading from './[...slugs]/loading'; + +interface AssistantDetailPageProps { + mobile?: boolean; +} + +const AssistantDetailPage = memo(({ mobile }) => { + const params = useParams(); + const slugs = params['*']?.split('/') || []; + const identifier = decodeURIComponent(slugs.join('/')); + + const useAssistantDetail = useDiscoverStore((s) => s.useAssistantDetail); + const { data, isLoading } = useAssistantDetail({ identifier }); + + if (isLoading) return ; + if (!data) return ; + + return ( + + + {!mobile && } + +
+
+ + + + ); +}); + +export default withSuspense(AssistantDetailPage); diff --git a/src/app/[variants]/(main)/discover/(detail)/assistant/[...slugs]/features/Header.tsx b/src/app/[variants]/(main)/discover/(detail)/assistant/[...slugs]/features/Header.tsx index 98dc963793b..a0dc252f72c 100644 --- a/src/app/[variants]/(main)/discover/(detail)/assistant/[...slugs]/features/Header.tsx +++ b/src/app/[variants]/(main)/discover/(detail)/assistant/[...slugs]/features/Header.tsx @@ -4,11 +4,12 @@ import { Github, MCP } from '@lobehub/icons'; import { ActionIcon, Avatar, Button, Icon, Text, Tooltip } from '@lobehub/ui'; import { createStyles, useResponsive } from 'antd-style'; import { BookTextIcon, CoinsIcon, DotIcon } from 'lucide-react'; -import Link from 'next/link'; +import NextLink from 'next/link'; import qs from 'query-string'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { Link as RouterLink } from 'react-router-dom'; import urlJoin from 'url-join'; import { formatIntergerNumber } from '@/utils/format'; @@ -52,16 +53,16 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => { const cate = categories.find((c) => c.key === category); const cateButton = ( - - + ); return ( @@ -105,7 +106,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => { - (({ mobile: isMobile }) => { target={'_blank'} > - + {author && ( - + {author} - + )} { {related?.map((item, index) => { - const link = urlJoin('/discover/assistant', item.identifier); + const link = urlJoin('/assistant', item.identifier); return ( - + ); diff --git a/src/app/[variants]/(main)/discover/(detail)/assistant/[...slugs]/page.tsx b/src/app/[variants]/(main)/discover/(detail)/assistant/[...slugs]/page.tsx deleted file mode 100644 index 292b558345e..00000000000 --- a/src/app/[variants]/(main)/discover/(detail)/assistant/[...slugs]/page.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { notFound } from 'next/navigation'; -import urlJoin from 'url-join'; - -import StructuredData from '@/components/StructuredData'; -import { Locales } from '@/locales/resources'; -import { ldModule } from '@/server/ld'; -import { metadataModule } from '@/server/metadata'; -import { DiscoverService } from '@/server/services/discover'; -import { translation } from '@/server/translation'; -import { DiscoverTab } from '@/types/discover'; -import { PageProps } from '@/types/next'; -import { RouteVariants } from '@/utils/server/routeVariants'; - -import Breadcrumb from '../../features/Breadcrumb'; -import Client from './Client'; - -type DiscoverPageProps = PageProps< - { slugs: string[]; variants: string }, - { hl?: Locales; version?: string } ->; - -const getSharedProps = async (props: DiscoverPageProps) => { - const params = await props.params; - const { slugs } = params; - const identifier = decodeURIComponent(slugs.join('/')); - const { isMobile, locale: hl } = await RouteVariants.getVariantsFromProps(props); - const discoverService = new DiscoverService(); - const [{ t, locale }, data] = await Promise.all([ - translation('metadata', hl), - discoverService.getAssistantDetail({ identifier, locale: hl }), - ]); - return { - data, - identifier, - isMobile, - locale, - t, - }; -}; - -export const generateMetadata = async (props: DiscoverPageProps) => { - const { data, t, locale, identifier } = await getSharedProps(props); - if (!data) return; - - const { tags, createdAt, homepage, author, description, title } = data; - - return { - authors: [ - { name: author, url: homepage }, - { name: 'LobeHub', url: 'https://github.com/lobehub' }, - { name: 'LobeHub Cloud', url: 'https://lobehub.com' }, - ], - keywords: tags, - ...metadataModule.generate({ - alternate: true, - canonical: urlJoin('https://lobehub.com/agent', identifier), - description: description, - locale, - tags: tags, - title: [title, t('discover.assistants.title')].join(' · '), - url: urlJoin('/discover/assistant', identifier), - }), - other: { - 'article:author': author, - 'article:published_time': createdAt - ? new Date(createdAt).toISOString() - : new Date().toISOString(), - 'robots': 'index,follow,max-image-preview:large', - }, - }; -}; - -const Page = async (props: DiscoverPageProps) => { - const { data, t, locale, identifier, isMobile } = await getSharedProps(props); - if (!data) return notFound(); - - const { tags, title, description, createdAt, author } = data; - - const ld = ldModule.generate({ - article: { - author: [author], - enable: true, - identifier, - tags: tags, - }, - date: createdAt ? new Date(createdAt).toISOString() : new Date().toISOString(), - description: description || t('discover.assistants.description'), - locale, - title: [title, t('discover.assistants.title')].join(' · '), - url: urlJoin('/discover/assistant', identifier), - webpage: { - enable: true, - search: '/discover/assistant', - }, - }); - - return ( - <> - - {!isMobile && } - - - ); -}; - -export const generateStaticParams = async () => []; - -Page.DisplayName = 'DiscoverAssistantsDetail'; - -export default Page; diff --git a/src/app/[variants]/(main)/discover/(detail)/components/NotFound.tsx b/src/app/[variants]/(main)/discover/(detail)/components/NotFound.tsx new file mode 100644 index 00000000000..7f0ce59e411 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(detail)/components/NotFound.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +const NotFound = memo(() => { + const { t } = useTranslation('error', { keyPrefix: 'notFound' }); + + return ( + +

{t('title')}

+
+ ); +}); + +export default NotFound; diff --git a/src/app/[variants]/(main)/discover/(detail)/features/Back.tsx b/src/app/[variants]/(main)/discover/(detail)/features/Back.tsx index 330a520b82a..0b997608b55 100644 --- a/src/app/[variants]/(main)/discover/(detail)/features/Back.tsx +++ b/src/app/[variants]/(main)/discover/(detail)/features/Back.tsx @@ -3,10 +3,10 @@ import { Icon } from '@lobehub/ui'; import { createStyles } from 'antd-style'; import { ArrowLeft } from 'lucide-react'; -import Link from 'next/link'; import { CSSProperties, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { Link } from 'react-router-dom'; const useStyles = createStyles(({ css, token }) => { return { @@ -25,7 +25,7 @@ const Back = memo<{ href: string; style?: CSSProperties }>(({ href, style }) => const { styles } = useStyles(); return ( - + {t(`back`)} diff --git a/src/app/[variants]/(main)/discover/(detail)/features/Breadcrumb.tsx b/src/app/[variants]/(main)/discover/(detail)/features/Breadcrumb.tsx index ecb30f44f4d..92327a6a1c5 100644 --- a/src/app/[variants]/(main)/discover/(detail)/features/Breadcrumb.tsx +++ b/src/app/[variants]/(main)/discover/(detail)/features/Breadcrumb.tsx @@ -3,11 +3,10 @@ import { CopyButton } from '@lobehub/ui'; import { Breadcrumb as AntdBreadcrumb } from 'antd'; import { useTheme } from 'antd-style'; -import Link from 'next/link'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; -import urlJoin from 'url-join'; +import { Link } from 'react-router-dom'; import { DiscoverTab } from '@/types/discover'; @@ -18,11 +17,11 @@ const Breadcrumb = memo<{ identifier: string; tab: DiscoverTab }>(({ tab, identi Discover, + title: Discover, }, { title: ( - + {tab === DiscoverTab.Mcp ? 'MCP Servers' : t(`tab.${tab}` as any)} ), diff --git a/src/app/[variants]/(main)/discover/(detail)/layout.tsx b/src/app/[variants]/(main)/discover/(detail)/layout.tsx deleted file mode 100644 index 9e846916552..00000000000 --- a/src/app/[variants]/(main)/discover/(detail)/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { PropsWithChildren } from 'react'; - -import ServerLayout from '@/components/server/ServerLayout'; - -import Desktop from './_layout/Desktop'; -import Mobile from './_layout/Mobile'; - -const MainLayout = ServerLayout({ Desktop, Mobile }); - -MainLayout.displayName = 'DiscoverAssistantsDetailLayout'; - -export default MainLayout; diff --git a/src/app/[variants]/(main)/discover/(detail)/mcp/McpDetailPage.tsx b/src/app/[variants]/(main)/discover/(detail)/mcp/McpDetailPage.tsx new file mode 100644 index 00000000000..0017295f6f9 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(detail)/mcp/McpDetailPage.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { memo } from 'react'; +import { useParams } from 'react-router-dom'; +import { Flexbox } from 'react-layout-kit'; + +import { withSuspense } from '@/components/withSuspense'; +import { DetailProvider } from '@/features/MCPPluginDetail/DetailProvider'; +import Header from '@/features/MCPPluginDetail/Header'; +import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins'; +import { useQuery } from '@/hooks/useQuery'; +import { useDiscoverStore } from '@/store/discover'; +import { DiscoverTab } from '@/types/discover'; + +import Breadcrumb from '../features/Breadcrumb'; +import { TocProvider } from '../features/Toc/useToc'; +import NotFound from '../components/NotFound'; +import Details from './[slug]/features/Details'; +import Loading from './[slug]/loading'; + +interface McpDetailPageProps { + mobile?: boolean; +} + +const McpDetailPage = memo(({ mobile }) => { + const params = useParams(); + const identifier = params['*'] || params.slug || ''; + + const { version } = useQuery() as { version?: string }; + const useMcpDetail = useDiscoverStore((s) => s.useFetchMcpDetail); + const { data, isLoading } = useMcpDetail({ identifier, version }); + + useFetchInstalledPlugins(); + + if (isLoading) return ; + if (!data) return ; + + return ( + + + {!mobile && } + +
+
+ + + + ); +}); + +export default withSuspense(McpDetailPage); diff --git a/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/features/Sidebar/Related/index.tsx b/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/features/Sidebar/Related/index.tsx index 798e997b2cd..e265fb85084 100644 --- a/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/features/Sidebar/Related/index.tsx +++ b/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/features/Sidebar/Related/index.tsx @@ -1,8 +1,8 @@ -import Link from 'next/link'; import qs from 'query-string'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { Link } from 'react-router-dom'; import urlJoin from 'url-join'; import { useDetailContext } from '@/features/MCPPluginDetail/DetailProvider'; @@ -29,9 +29,9 @@ const Related = memo(() => { {related?.map((item, index) => { - const link = urlJoin('/discover/mcp', item.identifier); + const link = urlJoin('/mcp', item.identifier); return ( - + ); diff --git a/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/page.tsx b/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/page.tsx deleted file mode 100644 index 0c4da01f9ee..00000000000 --- a/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { notFound } from 'next/navigation'; -import urlJoin from 'url-join'; - -import StructuredData from '@/components/StructuredData'; -import { isDesktop } from '@/const/version'; -import { ldModule } from '@/server/ld'; -import { metadataModule } from '@/server/metadata'; -import { DiscoverService } from '@/server/services/discover'; -import { translation } from '@/server/translation'; -import { PageProps } from '@/types/next'; -import { RouteVariants } from '@/utils/server/routeVariants'; - -import Client from './Client'; - -type DiscoverPageProps = PageProps<{ slug: string; variants: string }>; - -const getSharedProps = async (props: DiscoverPageProps) => { - const params = await props.params; - const { slug: identifier } = params; - const { isMobile, locale: hl } = await RouteVariants.getVariantsFromProps(props); - const discoverService = new DiscoverService(); - const [{ t, locale }, data] = await Promise.all([ - translation('metadata', hl), - discoverService.getMcpDetail({ identifier, locale: hl }), - ]); - return { - data, - identifier, - isMobile, - locale, - t, - }; -}; - -export const generateMetadata = async (props: DiscoverPageProps) => { - const { data, t, locale, identifier } = await getSharedProps(props); - if (!data) return notFound(); - - const { tags, createdAt, homepage, author, description, name } = data; - - return { - authors: [ - { name: author, url: homepage }, - { name: 'LobeHub', url: 'https://github.com/lobehub' }, - { name: 'LobeHub Cloud', url: 'https://lobehub,com' }, - ], - keywords: tags, - ...metadataModule.generate({ - alternate: true, - canonical: urlJoin('https://lobehub.com/mcp', identifier), - description: description, - locale, - tags: tags, - title: [name, t('discover.mcp.title')].join(' · '), - url: urlJoin('/discover/mcp', identifier), - }), - other: { - 'article:author': author, - 'article:published_time': createdAt - ? new Date(createdAt).toISOString() - : new Date().toISOString(), - 'robots': 'index,follow,max-image-preview:large', - }, - }; -}; - -export const generateStaticParams = async () => []; - -const Page = async (props: DiscoverPageProps) => { - const { data, identifier, isMobile, locale, t } = await getSharedProps(props); - if (!data) return notFound(); - - const { tags, name, description, createdAt, author } = data; - - const ld = ldModule.generate({ - article: { - author: [author?.name || 'LobeHub'], - enable: true, - identifier, - tags: tags, - }, - date: createdAt ? new Date(createdAt).toISOString() : new Date().toISOString(), - description: description || t('discover.mcp.description'), - locale, - title: [name, t('discover.mcp.title')].join(' · '), - url: urlJoin('/discover/mcp', identifier), - webpage: { - enable: true, - search: '/discover/mcp', - }, - }); - - return ( - <> - {!isDesktop && } - - - ); -}; - -Page.displayName = 'DiscoverMCPDetail'; - -export default Page; diff --git a/src/app/[variants]/(main)/discover/(detail)/model/ModelDetailPage.tsx b/src/app/[variants]/(main)/discover/(detail)/model/ModelDetailPage.tsx new file mode 100644 index 00000000000..2d8c45ffa60 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(detail)/model/ModelDetailPage.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { memo } from 'react'; +import { useParams } from 'react-router-dom'; +import { Flexbox } from 'react-layout-kit'; + +import { withSuspense } from '@/components/withSuspense'; +import { useDiscoverStore } from '@/store/discover'; +import { DiscoverTab } from '@/types/discover'; + +import Breadcrumb from '../features/Breadcrumb'; +import NotFound from '../components/NotFound'; +import { DetailProvider } from './[...slugs]/features/DetailProvider'; +import Details from './[...slugs]/features/Details'; +import Header from './[...slugs]/features/Header'; +import Loading from './[...slugs]/loading'; + +interface ModelDetailPageProps { + mobile?: boolean; +} + +const ModelDetailPage = memo(({ mobile }) => { + const params = useParams(); + const slugs = params['*']?.split('/') || []; + const identifier = decodeURIComponent(slugs.join('/')); + + const useModelDetail = useDiscoverStore((s) => s.useModelDetail); + const { data, isLoading } = useModelDetail({ identifier }); + + if (isLoading) return ; + if (!data) return ; + + return ( + + {!mobile && } + +
+
+ + + ); +}); + +export default withSuspense(ModelDetailPage); diff --git a/src/app/[variants]/(main)/discover/(detail)/model/[...slugs]/features/Sidebar/Related/index.tsx b/src/app/[variants]/(main)/discover/(detail)/model/[...slugs]/features/Sidebar/Related/index.tsx index 2cd51e72750..cce17369a67 100644 --- a/src/app/[variants]/(main)/discover/(detail)/model/[...slugs]/features/Sidebar/Related/index.tsx +++ b/src/app/[variants]/(main)/discover/(detail)/model/[...slugs]/features/Sidebar/Related/index.tsx @@ -1,8 +1,8 @@ -import Link from 'next/link'; import qs from 'query-string'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { Link } from 'react-router-dom'; import urlJoin from 'url-join'; import Title from '../../../../../../features/Title'; @@ -28,9 +28,9 @@ const Related = memo(() => { {related?.map((item, index) => { - const link = urlJoin('/discover/model', item.identifier); + const link = urlJoin('/model', item.identifier); return ( - + ); diff --git a/src/app/[variants]/(main)/discover/(detail)/model/[...slugs]/page.tsx b/src/app/[variants]/(main)/discover/(detail)/model/[...slugs]/page.tsx deleted file mode 100644 index 5f0fe4d7459..00000000000 --- a/src/app/[variants]/(main)/discover/(detail)/model/[...slugs]/page.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { notFound } from 'next/navigation'; -import urlJoin from 'url-join'; - -import StructuredData from '@/components/StructuredData'; -import { ldModule } from '@/server/ld'; -import { metadataModule } from '@/server/metadata'; -import { DiscoverService } from '@/server/services/discover'; -import { translation } from '@/server/translation'; -import { PageProps } from '@/types/next'; -import { RouteVariants } from '@/utils/server/routeVariants'; - -import Client from './Client'; - -type DiscoverPageProps = PageProps<{ slugs: string[]; variants: string }>; - -const getSharedProps = async (props: DiscoverPageProps) => { - const params = await props.params; - const { isMobile, locale: hl } = await RouteVariants.getVariantsFromProps(props); - - const { slugs } = params; - const identifier = decodeURIComponent(slugs.join('/')); - const { t, locale } = await translation('metadata', hl); - const { t: td } = await translation('models', hl); - - const discoverService = new DiscoverService(); - const data = await discoverService.getModelDetail({ identifier }); - return { - data, - discoverService, - identifier, - isMobile, - locale, - t, - td, - }; -}; - -export const generateMetadata = async (props: DiscoverPageProps) => { - const { data, locale, identifier, t, td } = await getSharedProps(props); - if (!data) return; - - const { displayName, releasedAt, providers } = data; - - return { - authors: [ - { name: displayName || identifier }, - { name: 'LobeHub', url: 'https://github.com/lobehub' }, - { name: 'LobeChat', url: 'https://github.com/lobehub/lobe-chat' }, - ], - webpage: { - enable: true, - search: true, - }, - ...metadataModule.generate({ - alternate: true, - description: td(`${identifier}.description`) || t('discover.models.description'), - locale, - tags: providers.map((item) => item.name) || [], - title: [displayName || identifier, t('discover.models.title')].join(' · '), - url: urlJoin('/discover/model', identifier), - }), - other: { - 'article:author': displayName || identifier, - 'article:published_time': releasedAt - ? new Date(releasedAt).toISOString() - : new Date().toISOString(), - 'robots': 'index,follow,max-image-preview:large', - }, - }; -}; - -export const generateStaticParams = async () => []; - -const Page = async (props: DiscoverPageProps) => { - const { data, locale, identifier, t, td, isMobile } = await getSharedProps(props); - if (!data) return notFound(); - - const { displayName, releasedAt, providers } = data; - - const ld = ldModule.generate({ - article: { - author: [displayName || identifier], - enable: true, - identifier, - tags: providers.map((item) => item.name) || [], - }, - date: releasedAt ? new Date(releasedAt).toISOString() : new Date().toISOString(), - description: td(`${identifier}.description`) || t('discover.models.description'), - locale, - title: [displayName || identifier, t('discover.models.title')].join(' · '), - url: urlJoin('/discover/model', identifier), - }); - - return ( - <> - - - - ); -}; - -Page.DisplayName = 'DiscoverModelDetail'; - -export default Page; diff --git a/src/app/[variants]/(main)/discover/(detail)/provider/ProviderDetailPage.tsx b/src/app/[variants]/(main)/discover/(detail)/provider/ProviderDetailPage.tsx new file mode 100644 index 00000000000..0201923a0d3 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(detail)/provider/ProviderDetailPage.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { memo } from 'react'; +import { useParams } from 'react-router-dom'; +import { Flexbox } from 'react-layout-kit'; + +import { withSuspense } from '@/components/withSuspense'; +import { useDiscoverStore } from '@/store/discover'; +import { DiscoverTab } from '@/types/discover'; + +import Breadcrumb from '../features/Breadcrumb'; +import NotFound from '../components/NotFound'; +import { DetailProvider } from './[...slugs]/features/DetailProvider'; +import Details from './[...slugs]/features/Details'; +import Header from './[...slugs]/features/Header'; +import Loading from './[...slugs]/loading'; + +interface ProviderDetailPageProps { + mobile?: boolean; +} + +const ProviderDetailPage = memo(({ mobile }) => { + const params = useParams(); + const slugs = params['*']?.split('/') || []; + const identifier = decodeURIComponent(slugs.join('/')); + + const useProviderDetail = useDiscoverStore((s) => s.useProviderDetail); + const { data, isLoading } = useProviderDetail({ identifier, withReadme: true }); + + if (isLoading) return ; + if (!data) return ; + + return ( + + {!mobile && } + +
+
+ + + ); +}); + +export default withSuspense(ProviderDetailPage); diff --git a/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/features/Sidebar/ActionButton/ProviderConfig.tsx b/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/features/Sidebar/ActionButton/ProviderConfig.tsx index 08f34e05df0..81a3144591b 100644 --- a/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/features/Sidebar/ActionButton/ProviderConfig.tsx +++ b/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/features/Sidebar/ActionButton/ProviderConfig.tsx @@ -9,7 +9,7 @@ import { useRouter } from 'nextjs-toploader/app'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { isDeprecatedEdition } from '@/const/version'; +import { isDeprecatedEdition, isDesktop } from '@/const/version'; import { useDetailContext } from '../../DetailProvider'; @@ -26,7 +26,21 @@ const ProviderConfig = memo(() => { const { t } = useTranslation('discover'); const { url, modelsUrl, identifier } = useDetailContext(); const router = useRouter(); - const openSettings = () => { + const openSettings = async () => { + const searchParams = isDeprecatedEdition + ? { active: 'llm' } + : { active: 'provider', provider: identifier }; + const tab = isDeprecatedEdition ? 'llm' : 'provider'; + + if (isDesktop) { + const { dispatch } = await import('@lobechat/electron-client-ipc'); + await dispatch('openSettingsWindow', { + searchParams, + tab, + }); + return; + } + router.push( isDeprecatedEdition ? '/settings?active=llm' diff --git a/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/features/Sidebar/Related/index.tsx b/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/features/Sidebar/Related/index.tsx index 7e18682fdcc..b6e883b9316 100644 --- a/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/features/Sidebar/Related/index.tsx +++ b/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/features/Sidebar/Related/index.tsx @@ -1,7 +1,7 @@ -import Link from 'next/link'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { Link } from 'react-router-dom'; import urlJoin from 'url-join'; import Title from '../../../../../../features/Title'; @@ -19,9 +19,9 @@ const Related = memo(() => { {related?.map((item, index) => { - const link = urlJoin('/discover/provider', item.identifier); + const link = urlJoin('/provider', item.identifier); return ( - + ); diff --git a/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/page.tsx b/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/page.tsx deleted file mode 100644 index cea26686ccd..00000000000 --- a/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { notFound } from 'next/navigation'; -import urlJoin from 'url-join'; - -import StructuredData from '@/components/StructuredData'; -import { ldModule } from '@/server/ld'; -import { metadataModule } from '@/server/metadata'; -import { DiscoverService } from '@/server/services/discover'; -import { translation } from '@/server/translation'; -import { PageProps } from '@/types/next'; -import { RouteVariants } from '@/utils/server/routeVariants'; - -import Client from './Client'; - -type DiscoverPageProps = PageProps<{ slugs: string[]; variants: string }, { version?: string }>; - -const getSharedProps = async (props: DiscoverPageProps) => { - const [params, { isMobile, locale: hl }] = await Promise.all([ - props.params, - RouteVariants.getVariantsFromProps(props), - ]); - const { slugs } = params; - const identifier = decodeURIComponent(slugs.join('/')); - const discoverService = new DiscoverService(); - const [{ t, locale }, { t: td }, data] = await Promise.all([ - translation('metadata', hl), - translation('providers', hl), - discoverService.getProviderDetail({ identifier }), - ]); - return { - data, - identifier, - isMobile, - locale, - t, - td, - }; -}; - -export const generateMetadata = async (props: DiscoverPageProps) => { - const { data, t, td, locale, identifier } = await getSharedProps(props); - if (!data) return; - - const { name, models = [] } = data; - - return { - authors: [ - { name: name }, - { name: 'LobeHub', url: 'https://github.com/lobehub' }, - { name: 'LobeChat', url: 'https://github.com/lobehub/lobe-chat' }, - ], - ...metadataModule.generate({ - alternate: true, - description: td(`${identifier}.description`) || t('discover.providers.description'), - locale, - tags: models.map((item) => item.displayName || item.id) || [], - title: [name, t('discover.providers.title')].join(' · '), - url: urlJoin('/discover/provider', identifier), - }), - other: { - 'article:author': name, - 'article:published_time': new Date().toISOString(), - 'robots': 'index,follow,max-image-preview:large', - }, - }; -}; - -export const generateStaticParams = async () => []; - -const Page = async (props: DiscoverPageProps) => { - const { data, t, td, locale, identifier, isMobile } = await getSharedProps(props); - if (!data) return notFound(); - - const { models, name } = data; - - const ld = ldModule.generate({ - article: { - author: [name], - enable: true, - identifier, - tags: models.map((item) => item.displayName || item.id) || [], - }, - date: new Date().toISOString(), - description: td(`${identifier}.description`) || t('discover.providers.description'), - locale, - title: [name, t('discover.providers.title')].join(' · '), - url: urlJoin('/discover/provider', identifier), - webpage: { - enable: true, - search: true, - }, - }); - - return ( - <> - - - - ); -}; - -Page.DisplayName = 'DiscoverProviderDetail'; - -export default Page; diff --git a/src/app/[variants]/(main)/discover/(list)/(home)/HomePage.tsx b/src/app/[variants]/(main)/discover/(list)/(home)/HomePage.tsx new file mode 100644 index 00000000000..3dd3ac68a26 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(list)/(home)/HomePage.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useDiscoverStore } from '@/store/discover'; + +import Title from '../../components/Title'; +import AssistantList from '../assistant/features/List'; +import McpList from '../mcp/features/List'; +import Loading from './loading'; + +const HomePage = memo<{ mobile?: boolean }>(() => { + const { t } = useTranslation('discover'); + const useAssistantList = useDiscoverStore((s) => s.useAssistantList); + const useMcpList = useDiscoverStore((s) => s.useFetchMcpList); + + const { data: assistantList, isLoading: assistantLoading } = useAssistantList({ + page: 1, + pageSize: 12, + }); + + const { data: mcpList, isLoading: pluginLoading } = useMcpList({ + page: 1, + pageSize: 12, + }); + + if (assistantLoading || pluginLoading || !assistantList || !mcpList) return ; + + return ( + <> + + {t('home.featuredAssistants')} + + +
+ + {t('home.featuredTools')} + + + + ); +}); + +export default HomePage; diff --git a/src/app/[variants]/(main)/discover/(list)/(home)/page.tsx b/src/app/[variants]/(main)/discover/(list)/(home)/page.tsx deleted file mode 100644 index 41674ea818e..00000000000 --- a/src/app/[variants]/(main)/discover/(list)/(home)/page.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import StructuredData from '@/components/StructuredData'; -import { ldModule } from '@/server/ld'; -import { metadataModule } from '@/server/metadata'; -import { DynamicLayoutProps } from '@/types/next'; -import { parsePageMetaProps } from '@/utils/server/pageProps'; - -import Client from './Client'; - -export const generateMetadata = async (props: DynamicLayoutProps) => { - const { locale, t } = await parsePageMetaProps(props); - return metadataModule.generate({ - alternate: true, - description: t('discover.description'), - locale, - title: t('discover.title'), - url: '/discover', - }); -}; - -const Page = async (props: DynamicLayoutProps) => { - const { locale, t, isMobile } = await parsePageMetaProps(props); - - const ld = ldModule.generate({ - description: t('discover.description'), - locale, - title: t('discover.title'), - url: '/discover', - webpage: { - enable: true, - search: true, - }, - }); - - return ( - <> - - - - ); -}; - -Page.DisplayName = 'DiscoverHome'; - -export default Page; diff --git a/src/app/[variants]/(main)/discover/(list)/_layout/Desktop/Nav.tsx b/src/app/[variants]/(main)/discover/(list)/_layout/Desktop/Nav.tsx index 1f54d6c6b56..1886f9a5d75 100644 --- a/src/app/[variants]/(main)/discover/(list)/_layout/Desktop/Nav.tsx +++ b/src/app/[variants]/(main)/discover/(list)/_layout/Desktop/Nav.tsx @@ -2,15 +2,13 @@ import { Tabs } from '@lobehub/ui'; import { createStyles } from 'antd-style'; -import { usePathname } from 'next/navigation'; import { rgba } from 'polished'; import { memo, useState } from 'react'; import { Center, Flexbox } from 'react-layout-kit'; -import urlJoin from 'url-join'; +import { useLocation, useNavigate } from 'react-router-dom'; import { withSuspense } from '@/components/withSuspense'; import { useQuery } from '@/hooks/useQuery'; -import { useQueryRoute } from '@/hooks/useQueryRoute'; import { DiscoverTab } from '@/types/discover'; import { MAX_WIDTH, SCROLL_PARENT_ID } from '../../../features/const'; @@ -42,11 +40,11 @@ export const useStyles = createStyles(({ cx, stylish, css, token }) => ({ const Nav = memo(() => { const [hide, setHide] = useState(false); - const pathname = usePathname(); + const location = useLocation(); + const navigate = useNavigate(); const { cx, styles } = useStyles(); const { items, activeKey } = useNav(); const { q } = useQuery() as { q?: string }; - const router = useQueryRoute(); useScroll((scroll, delta) => { if (delta < 0) { @@ -58,7 +56,7 @@ const Nav = memo(() => { } }); - const isHome = pathname === '/discover'; + const isHome = location.pathname === '/'; return (
@@ -77,8 +75,9 @@ const Nav = memo(() => { compact items={items as any} onChange={(key) => { - const href = key === DiscoverTab.Home ? '/discover' : urlJoin('/discover', key); - router.push(href, { query: q ? { q } : {}, replace: true }); + const path = key === DiscoverTab.Home ? '/' : `/${key}`; + const search = q ? `?q=${encodeURIComponent(q)}` : ''; + navigate(path + search, { replace: true }); const scrollableElement = document?.querySelector(`#${SCROLL_PARENT_ID}`); if (!scrollableElement) return; scrollableElement.scrollTo({ behavior: 'smooth', top: 0 }); diff --git a/src/app/[variants]/(main)/discover/(list)/_layout/ListLayout.tsx b/src/app/[variants]/(main)/discover/(list)/_layout/ListLayout.tsx new file mode 100644 index 00000000000..fde7ad32a62 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(list)/_layout/ListLayout.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { PropsWithChildren, memo } from 'react'; + +import Desktop from './Desktop'; +import Mobile from './Mobile'; + +interface ListLayoutProps extends PropsWithChildren { + mobile?: boolean; +} + +const ListLayout = memo(({ children, mobile }) => { + if (mobile) { + return {children}; + } + + return {children}; +}); + +ListLayout.displayName = 'ListLayout'; + +export default ListLayout; diff --git a/src/app/[variants]/(main)/discover/(list)/_layout/Mobile/Nav.tsx b/src/app/[variants]/(main)/discover/(list)/_layout/Mobile/Nav.tsx index ce02ed01dac..20bb8110920 100644 --- a/src/app/[variants]/(main)/discover/(list)/_layout/Mobile/Nav.tsx +++ b/src/app/[variants]/(main)/discover/(list)/_layout/Mobile/Nav.tsx @@ -6,11 +6,10 @@ import { createStyles } from 'antd-style'; import { MenuIcon } from 'lucide-react'; import { memo, useState } from 'react'; import { Flexbox } from 'react-layout-kit'; -import urlJoin from 'url-join'; +import { useNavigate } from 'react-router-dom'; import Menu from '@/components/Menu'; import { withSuspense } from '@/components/withSuspense'; -import { useQueryRoute } from '@/hooks/useQueryRoute'; import { DiscoverTab } from '@/types/discover'; import { useNav } from '../../../features/useNav'; @@ -38,7 +37,7 @@ const Nav = memo(() => { const [open, setOpen] = useState(false); const { styles, theme } = useStyles(); const { items, activeKey, activeItem } = useNav(); - const router = useQueryRoute(); + const navigate = useNavigate(); return ( <> @@ -79,9 +78,9 @@ const Nav = memo(() => { items={items} onClick={({ key }) => { if (key === DiscoverTab.Home) { - router.push('/discover'); + navigate('/'); } else { - router.push(urlJoin('/discover', key)); + navigate(`/${key}`); } }} selectable diff --git a/src/app/[variants]/(main)/discover/(list)/assistant/AssistantLayout.tsx b/src/app/[variants]/(main)/discover/(list)/assistant/AssistantLayout.tsx new file mode 100644 index 00000000000..3a66b31ce3a --- /dev/null +++ b/src/app/[variants]/(main)/discover/(list)/assistant/AssistantLayout.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { memo, PropsWithChildren } from 'react'; + +import Desktop from './_layout/Desktop'; +import Mobile from './_layout/Mobile'; + +interface AssistantLayoutProps extends PropsWithChildren { + mobile?: boolean; +} + +const AssistantLayout = memo(({ children, mobile }) => { + if (mobile) { + return {children}; + } + return {children}; +}); + +AssistantLayout.displayName = 'AssistantLayout'; + +export default AssistantLayout; diff --git a/src/app/[variants]/(main)/discover/(list)/assistant/AssistantPage.tsx b/src/app/[variants]/(main)/discover/(list)/assistant/AssistantPage.tsx new file mode 100644 index 00000000000..161c29c1d40 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(list)/assistant/AssistantPage.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { memo } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import { withSuspense } from '@/components/withSuspense'; +import { useQuery } from '@/hooks/useQuery'; +import { useDiscoverStore } from '@/store/discover'; +import { AssistantQueryParams, DiscoverTab } from '@/types/discover'; + +import Pagination from '../features/Pagination'; +import List from './features/List'; +import Loading from './loading'; + +const AssistantPage = memo<{ mobile?: boolean }>(() => { + const { q, page, category, sort, order } = useQuery() as AssistantQueryParams; + const useAssistantList = useDiscoverStore((s) => s.useAssistantList); + const { data, isLoading } = useAssistantList({ + category, + order, + page, + pageSize: 21, + q, + sort, + }); + + if (isLoading || !data) return ; + + const { items, currentPage, pageSize, totalCount } = data; + + return ( + + + + + ); +}); + +export default withSuspense(AssistantPage); diff --git a/src/app/[variants]/(main)/discover/(list)/assistant/features/Category/index.tsx b/src/app/[variants]/(main)/discover/(list)/assistant/features/Category/index.tsx index daf8aa73660..cfc4ee83eac 100644 --- a/src/app/[variants]/(main)/discover/(list)/assistant/features/Category/index.tsx +++ b/src/app/[variants]/(main)/discover/(list)/assistant/features/Category/index.tsx @@ -1,8 +1,7 @@ 'use client'; import { Icon, Tag } from '@lobehub/ui'; -import Link from 'next/link'; -import { useRouter } from 'nextjs-toploader/app'; +import { Link, useNavigate } from 'react-router-dom'; import qs from 'query-string'; import { memo, useMemo } from 'react'; @@ -19,20 +18,20 @@ const Category = memo(() => { const useAssistantCategories = useDiscoverStore((s) => s.useAssistantCategories); const { category = 'all', q } = useQuery() as { category?: AssistantCategory; q?: string }; const { data: items = [] } = useAssistantCategories({ q }); - const route = useRouter(); + const navigate = useNavigate(); const cates = useCategory(); const genUrl = (key: AssistantCategory) => qs.stringifyUrl( { query: { category: key === AssistantCategory.All ? null : key, q }, - url: '/discover/assistant', + url: '/assistant', }, { skipNull: true }, ); const handleClick = (key: AssistantCategory) => { - route.push(genUrl(key)); + navigate(genUrl(key)); const scrollableElement = document?.querySelector(`#${SCROLL_PARENT_ID}`); if (!scrollableElement) return; scrollableElement.scrollTo({ behavior: 'smooth', top: 0 }); @@ -71,7 +70,7 @@ const Category = memo(() => { ), ...item, icon: , - label: {item.label}, + label: {item.label}, }; })} mode={'inline'} diff --git a/src/app/[variants]/(main)/discover/(list)/assistant/features/List/Item.tsx b/src/app/[variants]/(main)/discover/(list)/assistant/features/List/Item.tsx index fbabbaf096f..3e414295c77 100644 --- a/src/app/[variants]/(main)/discover/(list)/assistant/features/List/Item.tsx +++ b/src/app/[variants]/(main)/discover/(list)/assistant/features/List/Item.tsx @@ -2,11 +2,10 @@ import { Github } from '@lobehub/icons'; import { ActionIcon, Avatar, Block, Icon, Text } from '@lobehub/ui'; import { createStyles } from 'antd-style'; import { ClockIcon } from 'lucide-react'; -import Link from 'next/link'; -import { useRouter } from 'nextjs-toploader/app'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { Link, useNavigate } from 'react-router-dom'; import urlJoin from 'url-join'; import PublishedTime from '@/components/PublishedTime'; @@ -63,8 +62,8 @@ const AssistantItem = memo( backgroundColor, }) => { const { styles, theme } = useStyles(); - const router = useRouter(); - const link = urlJoin('/discover/assistant', identifier); + const navigate = useNavigate(); + const link = urlJoin('/assistant', identifier); const { t } = useTranslation('discover'); return ( @@ -72,7 +71,7 @@ const AssistantItem = memo( clickable height={'100%'} onClick={() => { - router.push(link); + navigate(link); }} style={{ overflow: 'hidden', @@ -119,7 +118,7 @@ const AssistantItem = memo( overflow: 'hidden', }} > - + {title} @@ -129,16 +128,17 @@ const AssistantItem = memo( - e.stopPropagation()} + rel="noopener noreferrer" target={'_blank'} > - + diff --git a/src/app/[variants]/(main)/discover/(list)/assistant/layout.tsx b/src/app/[variants]/(main)/discover/(list)/assistant/layout.tsx deleted file mode 100644 index 98e75a6debc..00000000000 --- a/src/app/[variants]/(main)/discover/(list)/assistant/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { PropsWithChildren } from 'react'; - -import ServerLayout from '@/components/server/ServerLayout'; - -import Desktop from './_layout/Desktop'; -import Mobile from './_layout/Mobile'; - -const MainLayout = ServerLayout({ Desktop, Mobile }); - -MainLayout.displayName = 'DiscoverAssistantsLayout'; - -export default MainLayout; diff --git a/src/app/[variants]/(main)/discover/(list)/assistant/page.tsx b/src/app/[variants]/(main)/discover/(list)/assistant/page.tsx deleted file mode 100644 index 77b130ecb4b..00000000000 --- a/src/app/[variants]/(main)/discover/(list)/assistant/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import StructuredData from '@/components/StructuredData'; -import { ldModule } from '@/server/ld'; -import { metadataModule } from '@/server/metadata'; -import { DynamicLayoutProps } from '@/types/next'; -import { parsePageMetaProps } from '@/utils/server/pageProps'; - -import Client from './Client'; - -export const generateMetadata = async (props: DynamicLayoutProps) => { - const { locale, t } = await parsePageMetaProps(props); - - return metadataModule.generate({ - alternate: true, - canonical: 'https://lobehub.com/agent', - description: t('discover.assistants.description'), - locale, - title: t('discover.assistants.title'), - url: '/discover/assistant', - }); -}; - -const Page = async (props: DynamicLayoutProps) => { - const { locale, t, isMobile } = await parsePageMetaProps(props); - - const ld = ldModule.generate({ - description: t('discover.assistants.description'), - locale, - title: t('discover.assistants.title'), - url: '/discover/assistant', - webpage: { - enable: true, - search: '/discover/assistant', - }, - }); - - return ( - <> - - - - ); -}; - -Page.DisplayName = 'DiscoverAssistants'; - -export default Page; diff --git a/src/app/[variants]/(main)/discover/(list)/features/Pagination.tsx b/src/app/[variants]/(main)/discover/(list)/features/Pagination.tsx index 986f43f883f..c31bb79fab0 100644 --- a/src/app/[variants]/(main)/discover/(list)/features/Pagination.tsx +++ b/src/app/[variants]/(main)/discover/(list)/features/Pagination.tsx @@ -3,11 +3,10 @@ import { Pagination as Page } from 'antd'; import { createStyles } from 'antd-style'; import { memo } from 'react'; -import urlJoin from 'url-join'; +import { useLocation, useNavigate } from 'react-router-dom'; import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/discover/features/const'; import { useQuery } from '@/hooks/useQuery'; -import { useQueryRoute } from '@/hooks/useQueryRoute'; import { DiscoverTab } from '@/types/discover'; const useStyles = createStyles(({ css, token, prefixCls }) => { @@ -36,14 +35,14 @@ interface PaginationProps { const Pagination = memo(({ tab, currentPage, total, pageSize }) => { const { styles } = useStyles(); const { page } = useQuery(); - const router = useQueryRoute(); + const navigate = useNavigate(); + const location = useLocation(); const handlePageChange = (newPage: number) => { - router.push(urlJoin('/discover', tab), { - query: { - page: String(newPage), - }, - }); + const searchParams = new URLSearchParams(location.search); + searchParams.set('page', String(newPage)); + navigate(`/${tab}?${searchParams.toString()}`); + const scrollableElement = document?.querySelector(`#${SCROLL_PARENT_ID}`); if (!scrollableElement) return; scrollableElement.scrollTo({ behavior: 'smooth', top: 0 }); diff --git a/src/app/[variants]/(main)/discover/(list)/layout.tsx b/src/app/[variants]/(main)/discover/(list)/layout.tsx deleted file mode 100644 index 2cb096e758a..00000000000 --- a/src/app/[variants]/(main)/discover/(list)/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { PropsWithChildren } from 'react'; - -import ServerLayout from '@/components/server/ServerLayout'; - -import Desktop from './_layout/Desktop'; -import Mobile from './_layout/Mobile'; - -const MainLayout = ServerLayout({ Desktop, Mobile }); - -MainLayout.displayName = 'DiscoverLayout'; - -export default MainLayout; diff --git a/src/app/[variants]/(main)/discover/(list)/mcp/McpLayout.tsx b/src/app/[variants]/(main)/discover/(list)/mcp/McpLayout.tsx new file mode 100644 index 00000000000..b26f6badcc7 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(list)/mcp/McpLayout.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { memo, PropsWithChildren } from 'react'; + +import Desktop from './_layout/Desktop'; +import Mobile from './_layout/Mobile'; + +interface McpLayoutProps extends PropsWithChildren { + mobile?: boolean; +} + +const McpLayout = memo(({ children, mobile }) => { + if (mobile) { + return {children}; + } + return {children}; +}); + +McpLayout.displayName = 'McpLayout'; + +export default McpLayout; diff --git a/src/app/[variants]/(main)/discover/(list)/mcp/McpPage.tsx b/src/app/[variants]/(main)/discover/(list)/mcp/McpPage.tsx new file mode 100644 index 00000000000..f1377b9d5ce --- /dev/null +++ b/src/app/[variants]/(main)/discover/(list)/mcp/McpPage.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { memo } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import { withSuspense } from '@/components/withSuspense'; +import { useQuery } from '@/hooks/useQuery'; +import { useDiscoverStore } from '@/store/discover'; +import { DiscoverTab, McpQueryParams } from '@/types/discover'; + +import Pagination from '../features/Pagination'; +import List from './features/List'; +import Loading from './loading'; + +const McpPage = memo<{ mobile?: boolean }>(() => { + const { q, page, category, sort, order } = useQuery() as McpQueryParams; + const useMcpList = useDiscoverStore((s) => s.useFetchMcpList); + const { data, isLoading } = useMcpList({ + category, + order, + page, + pageSize: 21, + q, + sort, + }); + + if (isLoading || !data) return ; + + const { items, currentPage, pageSize, totalCount } = data; + + return ( + + + + + ); +}); + +export default withSuspense(McpPage); diff --git a/src/app/[variants]/(main)/discover/(list)/mcp/features/Category/index.tsx b/src/app/[variants]/(main)/discover/(list)/mcp/features/Category/index.tsx index c2bdb58a474..9da1fc47dd6 100644 --- a/src/app/[variants]/(main)/discover/(list)/mcp/features/Category/index.tsx +++ b/src/app/[variants]/(main)/discover/(list)/mcp/features/Category/index.tsx @@ -1,8 +1,7 @@ 'use client'; import { Icon, Tag } from '@lobehub/ui'; -import Link from 'next/link'; -import { useRouter } from 'nextjs-toploader/app'; +import { Link, useNavigate } from 'react-router-dom'; import qs from 'query-string'; import { memo, useMemo } from 'react'; @@ -19,20 +18,20 @@ const Category = memo(() => { const useMcpCategories = useDiscoverStore((s) => s.useMcpCategories); const { category = 'all', q } = useQuery() as { category?: McpCategory; q?: string }; const { data: items = [] } = useMcpCategories({ q }); - const route = useRouter(); + const navigate = useNavigate(); const cates = useCategory(); const genUrl = (key: McpCategory) => qs.stringifyUrl( { query: { category: key === McpCategory.All ? null : key, q }, - url: '/discover/mcp', + url: '/mcp', }, { skipNull: true }, ); const handleClick = (key: McpCategory) => { - route.push(genUrl(key)); + navigate(genUrl(key)); const scrollableElement = document?.querySelector(`#${SCROLL_PARENT_ID}`); if (!scrollableElement) return; scrollableElement.scrollTo({ behavior: 'smooth', top: 0 }); @@ -70,7 +69,7 @@ const Category = memo(() => { ), ...item, icon: , - label: {item.label}, + label: {item.label}, }; })} mode={'inline'} diff --git a/src/app/[variants]/(main)/discover/(list)/mcp/features/List/Item.tsx b/src/app/[variants]/(main)/discover/(list)/mcp/features/List/Item.tsx index 5c640108c07..4b61e5c8684 100644 --- a/src/app/[variants]/(main)/discover/(list)/mcp/features/List/Item.tsx +++ b/src/app/[variants]/(main)/discover/(list)/mcp/features/List/Item.tsx @@ -5,11 +5,10 @@ import { ActionIcon, Avatar, Block, Icon, Tag, Text, Tooltip } from '@lobehub/ui import { Spotlight } from '@lobehub/ui/awesome'; import { createStyles } from 'antd-style'; import { ClockIcon } from 'lucide-react'; -import Link from 'next/link'; -import { useRouter } from 'nextjs-toploader/app'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { Link, useNavigate } from 'react-router-dom'; import urlJoin from 'url-join'; import InstallationIcon from '@/components/MCPDepsIcon'; @@ -78,14 +77,14 @@ const McpItem = memo( }) => { const { t } = useTranslation('discover'); const { styles, theme } = useStyles(); - const router = useRouter(); - const link = urlJoin('/discover/mcp', identifier); + const navigate = useNavigate(); + const link = urlJoin('/mcp', identifier); return ( { - router.push(link); + navigate(link); }} style={{ overflow: 'hidden', @@ -128,7 +127,7 @@ const McpItem = memo( overflow: 'hidden', }} > - + {name} @@ -145,9 +144,14 @@ const McpItem = memo( {installationMethods && } {github && ( - e.stopPropagation()} target={'_blank'}> + e.stopPropagation()} + rel="noopener noreferrer" + target={'_blank'} + > - + )} diff --git a/src/app/[variants]/(main)/discover/(list)/mcp/layout.tsx b/src/app/[variants]/(main)/discover/(list)/mcp/layout.tsx deleted file mode 100644 index d5482207f79..00000000000 --- a/src/app/[variants]/(main)/discover/(list)/mcp/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { PropsWithChildren } from 'react'; - -import ServerLayout from '@/components/server/ServerLayout'; - -import Desktop from './_layout/Desktop'; -import Mobile from './_layout/Mobile'; - -const MainLayout = ServerLayout({ Desktop, Mobile }); - -MainLayout.displayName = 'DiscoverToolsLayout'; - -export default MainLayout; diff --git a/src/app/[variants]/(main)/discover/(list)/mcp/page.tsx b/src/app/[variants]/(main)/discover/(list)/mcp/page.tsx deleted file mode 100644 index 9075063a676..00000000000 --- a/src/app/[variants]/(main)/discover/(list)/mcp/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import StructuredData from '@/components/StructuredData'; -import { ldModule } from '@/server/ld'; -import { metadataModule } from '@/server/metadata'; -import { DynamicLayoutProps } from '@/types/next'; -import { parsePageMetaProps } from '@/utils/server/pageProps'; - -import Client from './Client'; - -export const generateMetadata = async (props: DynamicLayoutProps) => { - const { locale, t } = await parsePageMetaProps(props); - - return metadataModule.generate({ - alternate: true, - canonical: 'https://lobehub.com/mcp', - description: t('discover.plugins.description'), - locale, - title: t('discover.plugins.title'), - url: '/discover/mcp', - }); -}; - -const Page = async (props: DynamicLayoutProps) => { - const { locale, t, isMobile } = await parsePageMetaProps(props); - - const ld = ldModule.generate({ - description: t('discover.plugins.description'), - locale, - title: t('discover.plugins.title'), - url: '/discover/mcp', - webpage: { - enable: true, - search: '/discover/mcp', - }, - }); - - return ( - <> - - - - ); -}; - -Page.DisplayName = 'DiscoverMCP'; - -export default Page; diff --git a/src/app/[variants]/(main)/discover/(list)/model/ModelLayout.tsx b/src/app/[variants]/(main)/discover/(list)/model/ModelLayout.tsx new file mode 100644 index 00000000000..7a87c027a89 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(list)/model/ModelLayout.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { memo, PropsWithChildren } from 'react'; + +import Desktop from './_layout/Desktop'; +import Mobile from './_layout/Mobile'; + +interface ModelLayoutProps extends PropsWithChildren { + mobile?: boolean; +} + +const ModelLayout = memo(({ children, mobile }) => { + if (mobile) { + return {children}; + } + return {children}; +}); + +ModelLayout.displayName = 'ModelLayout'; + +export default ModelLayout; diff --git a/src/app/[variants]/(main)/discover/(list)/model/ModelPage.tsx b/src/app/[variants]/(main)/discover/(list)/model/ModelPage.tsx new file mode 100644 index 00000000000..98e434fab76 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(list)/model/ModelPage.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { memo } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import { withSuspense } from '@/components/withSuspense'; +import { useQuery } from '@/hooks/useQuery'; +import { useDiscoverStore } from '@/store/discover'; +import { DiscoverTab, ModelQueryParams } from '@/types/discover'; + +import Pagination from '../features/Pagination'; +import List from './features/List'; +import Loading from './loading'; + +const ModelPage = memo<{ mobile?: boolean }>(() => { + const { q, page, category, sort, order } = useQuery() as ModelQueryParams; + const useModelList = useDiscoverStore((s) => s.useModelList); + const { data, isLoading } = useModelList({ + category, + order, + page, + pageSize: 21, + q, + sort, + }); + + if (isLoading || !data) return ; + + const { items, currentPage, pageSize, totalCount } = data; + + return ( + + + + + ); +}); + +export default withSuspense(ModelPage); diff --git a/src/app/[variants]/(main)/discover/(list)/model/_layout/Desktop.tsx b/src/app/[variants]/(main)/discover/(list)/model/_layout/Desktop.tsx index a13845e1ac6..2cc6cc013b8 100644 --- a/src/app/[variants]/(main)/discover/(list)/model/_layout/Desktop.tsx +++ b/src/app/[variants]/(main)/discover/(list)/model/_layout/Desktop.tsx @@ -4,7 +4,7 @@ import { Flexbox } from 'react-layout-kit'; import CategoryContainer from '../../../components/CategoryContainer'; import Category from '../features/Category'; -const Layout = async ({ children }: PropsWithChildren) => { +const Layout = ({ children }: PropsWithChildren) => { return ( diff --git a/src/app/[variants]/(main)/discover/(list)/model/features/Category/index.tsx b/src/app/[variants]/(main)/discover/(list)/model/features/Category/index.tsx index e5788bc1e16..7869f4693a3 100644 --- a/src/app/[variants]/(main)/discover/(list)/model/features/Category/index.tsx +++ b/src/app/[variants]/(main)/discover/(list)/model/features/Category/index.tsx @@ -1,8 +1,7 @@ 'use client'; import { Icon, Tag } from '@lobehub/ui'; -import Link from 'next/link'; -import { useRouter } from 'nextjs-toploader/app'; +import { Link, useNavigate } from 'react-router-dom'; import qs from 'query-string'; import { memo, useMemo } from 'react'; @@ -18,20 +17,20 @@ const Category = memo(() => { const useModelCategories = useDiscoverStore((s) => s.useModelCategories); const { category = 'all', q } = useQuery() as { category?: string; q?: string }; const { data: items = [] } = useModelCategories({ q }); - const route = useRouter(); + const navigate = useNavigate(); const cates = useCategory(); const genUrl = (key: string) => qs.stringifyUrl( { query: { category: key === 'all' ? null : key, q }, - url: '/discover/model', + url: '/model', }, { skipNull: true }, ); const handleClick = (key: string) => { - route.push(genUrl(key)); + navigate(genUrl(key)); const scrollableElement = document?.querySelector(`#${SCROLL_PARENT_ID}`); if (!scrollableElement) return; scrollableElement.scrollTo({ behavior: 'smooth', top: 0 }); @@ -69,7 +68,7 @@ const Category = memo(() => { ), ...item, icon: , - label: {item.label}, + label: {item.label}, }; })} mode={'inline'} diff --git a/src/app/[variants]/(main)/discover/(list)/model/features/List/Item.tsx b/src/app/[variants]/(main)/discover/(list)/model/features/List/Item.tsx index 47cfe002140..a46c4c86ab9 100644 --- a/src/app/[variants]/(main)/discover/(list)/model/features/List/Item.tsx +++ b/src/app/[variants]/(main)/discover/(list)/model/features/List/Item.tsx @@ -6,11 +6,10 @@ import { Popover } from 'antd'; import { createStyles } from 'antd-style'; import dayjs from 'dayjs'; import { ClockIcon } from 'lucide-react'; -import Link from 'next/link'; -import { useRouter } from 'nextjs-toploader/app'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { Link, useNavigate } from 'react-router-dom'; import urlJoin from 'url-join'; import { ModelInfoTags } from '@/components/ModelSelect'; @@ -57,14 +56,14 @@ const ModelItem = memo( ({ identifier, displayName, contextWindowTokens, releasedAt, type, abilities, providers }) => { const { t } = useTranslation(['models', 'discover']); const { styles } = useStyles(); - const router = useRouter(); - const link = urlJoin('/discover/model', identifier); + const navigate = useNavigate(); + const link = urlJoin('/model', identifier); return ( { - router.push(link); + navigate(link); }} style={{ overflow: 'hidden', @@ -106,7 +105,7 @@ const ModelItem = memo( overflow: 'hidden', }} > - + {displayName} diff --git a/src/app/[variants]/(main)/discover/(list)/model/layout.tsx b/src/app/[variants]/(main)/discover/(list)/model/layout.tsx deleted file mode 100644 index b41b524924d..00000000000 --- a/src/app/[variants]/(main)/discover/(list)/model/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { PropsWithChildren } from 'react'; - -import ServerLayout from '@/components/server/ServerLayout'; - -import Desktop from './_layout/Desktop'; -import Mobile from './_layout/Mobile'; - -const MainLayout = ServerLayout({ Desktop, Mobile }); - -MainLayout.displayName = 'DiscoverModelsLayout'; - -export default MainLayout; diff --git a/src/app/[variants]/(main)/discover/(list)/model/page.tsx b/src/app/[variants]/(main)/discover/(list)/model/page.tsx deleted file mode 100644 index 730f413fae3..00000000000 --- a/src/app/[variants]/(main)/discover/(list)/model/page.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import StructuredData from '@/components/StructuredData'; -import { ldModule } from '@/server/ld'; -import { metadataModule } from '@/server/metadata'; -import { DynamicLayoutProps } from '@/types/next'; -import { parsePageMetaProps } from '@/utils/server/pageProps'; - -import Client from './Client'; - -export const generateMetadata = async (props: DynamicLayoutProps) => { - const { locale, t } = await parsePageMetaProps(props); - return metadataModule.generate({ - alternate: true, - description: t('discover.models.description'), - locale, - title: t('discover.models.title'), - url: '/discover/model', - }); -}; - -const Page = async (props: DynamicLayoutProps) => { - const { locale, t } = await parsePageMetaProps(props); - - const ld = ldModule.generate({ - description: t('discover.models.description'), - locale, - title: t('discover.models.title'), - url: '/discover/model', - webpage: { - enable: true, - search: '/discover/model', - }, - }); - - return ( - <> - - - - ); -}; - -Page.DisplayName = 'DiscoverModels'; - -export default Page; diff --git a/src/app/[variants]/(main)/discover/(list)/provider/ProviderPage.tsx b/src/app/[variants]/(main)/discover/(list)/provider/ProviderPage.tsx new file mode 100644 index 00000000000..89301d73600 --- /dev/null +++ b/src/app/[variants]/(main)/discover/(list)/provider/ProviderPage.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { memo } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import { withSuspense } from '@/components/withSuspense'; +import { useQuery } from '@/hooks/useQuery'; +import { useDiscoverStore } from '@/store/discover'; +import { DiscoverTab, ProviderQueryParams } from '@/types/discover'; + +import Pagination from '../features/Pagination'; +import List from './features/List'; +import Loading from './loading'; + +const ProviderPage = memo<{ mobile?: boolean }>(() => { + const { q, page, sort, order } = useQuery() as ProviderQueryParams; + const useProviderList = useDiscoverStore((s) => s.useProviderList); + const { data, isLoading } = useProviderList({ + order, + page, + pageSize: 21, + q, + sort, + }); + + if (isLoading || !data) return ; + + const { items, currentPage, pageSize, totalCount } = data; + + return ( + + + + + ); +}); + +export default withSuspense(ProviderPage); diff --git a/src/app/[variants]/(main)/discover/(list)/provider/features/List/Item.tsx b/src/app/[variants]/(main)/discover/(list)/provider/features/List/Item.tsx index 01b3477e8ea..a157feafbf7 100644 --- a/src/app/[variants]/(main)/discover/(list)/provider/features/List/Item.tsx +++ b/src/app/[variants]/(main)/discover/(list)/provider/features/List/Item.tsx @@ -2,11 +2,10 @@ import { Github, ModelTag, ProviderCombine } from '@lobehub/icons'; import { ActionIcon, Block, MaskShadow, Text } from '@lobehub/ui'; import { createStyles } from 'antd-style'; import { GlobeIcon } from 'lucide-react'; -import Link from 'next/link'; -import { useRouter } from 'nextjs-toploader/app'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { Link, useNavigate } from 'react-router-dom'; import urlJoin from 'url-join'; import { DiscoverProviderItem } from '@/types/discover'; @@ -48,8 +47,8 @@ const useStyles = createStyles(({ css, token }) => { const ProviderItem = memo( ({ url, name, description, identifier, models }) => { const { styles, theme } = useStyles(); - const router = useRouter(); - const link = urlJoin('/discover/provider', identifier); + const navigate = useNavigate(); + const link = urlJoin('/provider', identifier); const { t } = useTranslation(['discover', 'providers']); return ( @@ -57,7 +56,7 @@ const ProviderItem = memo( clickable height={'100%'} onClick={() => { - router.push(link); + navigate(link); }} style={{ overflow: 'hidden', @@ -80,22 +79,28 @@ const ProviderItem = memo( }} title={identifier} > - +
@{name}
- e.stopPropagation()} target={'_blank'}> + e.stopPropagation()} + rel="noopener noreferrer" + target={'_blank'} + > - - + e.stopPropagation()} + rel="noopener noreferrer" target={'_blank'} > - + @@ -122,7 +127,7 @@ const ProviderItem = memo( .slice(0, 6) .filter(Boolean) .map((tag: string) => ( - + ))} diff --git a/src/app/[variants]/(main)/discover/(list)/provider/page.tsx b/src/app/[variants]/(main)/discover/(list)/provider/page.tsx deleted file mode 100644 index 1a4b22642e9..00000000000 --- a/src/app/[variants]/(main)/discover/(list)/provider/page.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import StructuredData from '@/components/StructuredData'; -import { ldModule } from '@/server/ld'; -import { metadataModule } from '@/server/metadata'; -import { DynamicLayoutProps } from '@/types/next'; -import { parsePageMetaProps } from '@/utils/server/pageProps'; - -import Client from './Client'; - -export const generateMetadata = async (props: DynamicLayoutProps) => { - const { locale, t } = await parsePageMetaProps(props); - return metadataModule.generate({ - alternate: true, - description: t('discover.providers.description'), - locale, - title: t('discover.providers.title'), - url: '/discover/provider', - }); -}; - -const Page = async (props: DynamicLayoutProps) => { - const { locale, t, isMobile } = await parsePageMetaProps(props); - - const ld = ldModule.generate({ - description: t('discover.providers.description'), - locale, - title: t('discover.providers.title'), - url: '/discover/provider', - webpage: { - enable: true, - search: '/discover/provider', - }, - }); - - return ( - <> - - - - ); -}; - -Page.DisplayName = 'DiscoverProviders'; - -export default Page; diff --git a/src/app/[variants]/(main)/discover/DiscoverRouter.tsx b/src/app/[variants]/(main)/discover/DiscoverRouter.tsx new file mode 100644 index 00000000000..13a64aed456 --- /dev/null +++ b/src/app/[variants]/(main)/discover/DiscoverRouter.tsx @@ -0,0 +1,167 @@ +'use client'; + +import { memo, useEffect } from 'react'; +import { MemoryRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; +import { useMediaQuery } from 'react-responsive'; + +import DiscoverLayout from './_layout/DiscoverLayout'; +import ListLayout from './(list)/_layout/ListLayout'; +import DetailLayout from './(detail)/_layout/DetailLayout'; +import HomePage from './(list)/(home)/HomePage'; +import AssistantPage from './(list)/assistant/AssistantPage'; +import AssistantLayout from './(list)/assistant/AssistantLayout'; +import McpPage from './(list)/mcp/McpPage'; +import McpLayout from './(list)/mcp/McpLayout'; +import ModelPage from './(list)/model/ModelPage'; +import ModelLayout from './(list)/model/ModelLayout'; +import ProviderPage from './(list)/provider/ProviderPage'; +import AssistantDetailPage from './(detail)/assistant/AssistantDetailPage'; +import McpDetailPage from './(detail)/mcp/McpDetailPage'; +import ModelDetailPage from './(detail)/model/ModelDetailPage'; +import ProviderDetailPage from './(detail)/provider/ProviderDetailPage'; + +// Get initial path from URL +const getInitialPath = () => { + if (typeof window === 'undefined') return '/'; + const fullPath = window.location.pathname; + const searchParams = window.location.search; + const discoverIndex = fullPath.indexOf('/discover'); + + if (discoverIndex !== -1) { + const pathAfterDiscover = fullPath.slice(discoverIndex + '/discover'.length) || '/'; + return pathAfterDiscover + searchParams; + } + return '/'; +}; + +// Helper component to sync URL with MemoryRouter +const UrlSynchronizer = () => { + const location = useLocation(); + const navigate = useNavigate(); + + // Sync initial URL + useEffect(() => { + const fullPath = window.location.pathname; + const searchParams = window.location.search; + const discoverIndex = fullPath.indexOf('/discover'); + + if (discoverIndex !== -1) { + const pathAfterDiscover = fullPath.slice(discoverIndex + '/discover'.length) || '/'; + const targetPath = pathAfterDiscover + searchParams; + + if (location.pathname + location.search !== targetPath) { + navigate(targetPath, { replace: true }); + } + } + }, []); + + // Update browser URL when location changes + useEffect(() => { + const newUrl = `/discover${location.pathname}${location.search}`; + if (window.location.pathname + window.location.search !== newUrl) { + window.history.replaceState({}, '', newUrl); + } + }, [location.pathname, location.search]); + + return null; +}; + +const DiscoverRouter = memo(() => { + const mobile = useMediaQuery({ maxWidth: 768 }); + + return ( + + + + + {/* List routes with ListLayout */} + + + + } + path="/" + /> + + + + + + } + path="/assistant" + /> + + + + + + } + path="/model" + /> + + + + } + path="/provider" + /> + + + + + + } + path="/mcp" + /> + + {/* Detail routes with DetailLayout */} + + + + } + path="/assistant/*" + /> + + + + } + path="/model/*" + /> + + + + } + path="/provider/*" + /> + + + + } + path="/mcp/*" + /> + + {/* Fallback */} + } path="*" /> + + + + ); +}); + +export default DiscoverRouter; diff --git a/src/app/[variants]/(main)/discover/[[...path]]/page.tsx b/src/app/[variants]/(main)/discover/[[...path]]/page.tsx new file mode 100644 index 00000000000..a76c0ea6738 --- /dev/null +++ b/src/app/[variants]/(main)/discover/[[...path]]/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import DiscoverRouter from '../DiscoverRouter'; + +const DiscoverPage = () => { + return ; +}; + +DiscoverPage.displayName = 'DiscoverPage'; + +export default DiscoverPage; diff --git a/src/app/[variants]/(main)/discover/_layout/Desktop/Header.tsx b/src/app/[variants]/(main)/discover/_layout/Desktop/Header.tsx index 305d389ca48..e5f0f352578 100644 --- a/src/app/[variants]/(main)/discover/_layout/Desktop/Header.tsx +++ b/src/app/[variants]/(main)/discover/_layout/Desktop/Header.tsx @@ -1,8 +1,8 @@ 'use client'; import { ChatHeader } from '@lobehub/ui/chat'; -import Link from 'next/link'; import { memo } from 'react'; +import { Link } from 'react-router-dom'; import { ProductLogo } from '@/components/Branding'; import { isCustomBranding } from '@/const/version'; @@ -14,7 +14,7 @@ const Header = memo(() => { return ( + } diff --git a/src/app/[variants]/(main)/discover/_layout/DiscoverLayout.tsx b/src/app/[variants]/(main)/discover/_layout/DiscoverLayout.tsx new file mode 100644 index 00000000000..5cdbcb11228 --- /dev/null +++ b/src/app/[variants]/(main)/discover/_layout/DiscoverLayout.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { PropsWithChildren, memo } from 'react'; + +import Desktop from './Desktop'; +import Mobile from './Mobile'; + +interface DiscoverLayoutProps extends PropsWithChildren { + mobile?: boolean; +} + +const DiscoverLayout = memo(({ children, mobile }) => { + if (mobile) { + return {children}; + } + + return {children}; +}); + +DiscoverLayout.displayName = 'DiscoverLayout'; + +export default DiscoverLayout; diff --git a/src/app/[variants]/(main)/discover/components/Title.tsx b/src/app/[variants]/(main)/discover/components/Title.tsx index dd97edca317..bfee007558b 100644 --- a/src/app/[variants]/(main)/discover/components/Title.tsx +++ b/src/app/[variants]/(main)/discover/components/Title.tsx @@ -3,9 +3,9 @@ import { Button, Icon, Tag } from '@lobehub/ui'; import { createStyles } from 'antd-style'; import { ChevronRight } from 'lucide-react'; -import Link from 'next/link'; import { ReactNode, memo } from 'react'; import { Flexbox, FlexboxProps } from 'react-layout-kit'; +import { Link } from 'react-router-dom'; const useStyles = createStyles(({ css, responsive, token }) => ({ more: css` @@ -59,7 +59,10 @@ const Title = memo(({ tag, children, moreLink, more }) => { title )} {moreLink && ( - +