mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
♻️ refactor: change discover page from RSC to SPA to improve performance (#9828)
* 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
This commit is contained in:
@@ -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);
|
||||
const extractedSubPath = extractSubPath(path, matchedRoute.pathPrefix);
|
||||
const sanitizedSubPath =
|
||||
extractedSubPath && !extractedSubPath.startsWith('?') ? extractedSubPath : undefined;
|
||||
let searchParams: Record<string, string> | undefined;
|
||||
try {
|
||||
const url = new URL(params.url);
|
||||
const entries = Array.from(url.searchParams.entries());
|
||||
if (entries.length > 0) {
|
||||
searchParams = entries.reduce<Record<string, string>>((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(subPath);
|
||||
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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -50,3 +50,5 @@ export type MainBroadcastEventKey = keyof MainBroadcastEvents;
|
||||
export type MainBroadcastParams<T extends MainBroadcastEventKey> = Parameters<
|
||||
MainBroadcastEvents[T]
|
||||
>[0];
|
||||
|
||||
export type { OpenSettingsWindowOptions } from './windows';
|
||||
|
||||
@@ -1,24 +1,58 @@
|
||||
import { InterceptRouteParams, InterceptRouteResponse } from '../types/route';
|
||||
|
||||
export interface OpenSettingsWindowOptions {
|
||||
/**
|
||||
* Query parameters that should be appended to the settings URL.
|
||||
*/
|
||||
searchParams?: Record<string, string | undefined>;
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* Close all windows by template
|
||||
* @param templateId Template identifier
|
||||
* @returns Operation result
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Get all windows by template
|
||||
* @param templateId Template identifier
|
||||
* @returns List of window identifiers
|
||||
*/
|
||||
getWindowsByTemplate: (templateId: string) => GetWindowsByTemplateResponse;
|
||||
|
||||
/**
|
||||
* 拦截客户端路由导航请求
|
||||
* @param params 包含路径和来源信息的参数对象
|
||||
@@ -31,26 +65,5 @@ export interface WindowsDispatchEvents {
|
||||
*/
|
||||
openDevtools: () => void;
|
||||
|
||||
openSettingsWindow: (tab?: string) => void;
|
||||
|
||||
/**
|
||||
* Create a new multi-instance window
|
||||
* @param params Window creation parameters
|
||||
* @returns Creation result
|
||||
*/
|
||||
createMultiInstanceWindow: (params: CreateMultiInstanceWindowParams) => CreateMultiInstanceWindowResponse;
|
||||
|
||||
/**
|
||||
* Get all windows by template
|
||||
* @param templateId Template identifier
|
||||
* @returns List of window identifiers
|
||||
*/
|
||||
getWindowsByTemplate: (templateId: string) => GetWindowsByTemplateResponse;
|
||||
|
||||
/**
|
||||
* Close all windows by template
|
||||
* @param templateId Template identifier
|
||||
* @returns Operation result
|
||||
*/
|
||||
closeWindowsByTemplate: (templateId: string) => { success: boolean; error?: string };
|
||||
openSettingsWindow: (options?: OpenSettingsWindowOptions | string) => void;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -20,6 +20,11 @@ const handler = (req: NextRequest) =>
|
||||
},
|
||||
|
||||
req,
|
||||
responseMeta({ ctx }) {
|
||||
const headers = ctx?.resHeaders;
|
||||
|
||||
return { headers };
|
||||
},
|
||||
router: desktopRouter,
|
||||
});
|
||||
|
||||
|
||||
@@ -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<DetailLayoutProps>(({ children, mobile }) => {
|
||||
if (mobile) {
|
||||
return <Mobile>{children}</Mobile>;
|
||||
}
|
||||
|
||||
return <Desktop>{children}</Desktop>;
|
||||
});
|
||||
|
||||
DetailLayout.displayName = 'DetailLayout';
|
||||
|
||||
export default DetailLayout;
|
||||
@@ -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 (
|
||||
<ChatHeader
|
||||
onBackClick={() => router.push(urlJoin('/discover', path))}
|
||||
onBackClick={() => navigate(`/${path}`)}
|
||||
showBackButton
|
||||
style={mobileHeaderSticky}
|
||||
/>
|
||||
|
||||
@@ -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<AssistantDetailPageProps>(({ 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 <Loading />;
|
||||
if (!data) return <NotFound />;
|
||||
|
||||
return (
|
||||
<TocProvider>
|
||||
<DetailProvider config={data}>
|
||||
{!mobile && <Breadcrumb identifier={identifier} tab={DiscoverTab.Assistants} />}
|
||||
<Flexbox gap={16}>
|
||||
<Header mobile={mobile} />
|
||||
<Details mobile={mobile} />
|
||||
</Flexbox>
|
||||
</DetailProvider>
|
||||
</TocProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export default withSuspense(AssistantDetailPage);
|
||||
@@ -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 = (
|
||||
<Link
|
||||
href={qs.stringifyUrl({
|
||||
<RouterLink
|
||||
to={qs.stringifyUrl({
|
||||
query: { category: cate?.key },
|
||||
url: '/discover/assistant',
|
||||
url: '/assistant',
|
||||
})}
|
||||
>
|
||||
<Button icon={cate?.icon} size={'middle'} variant={'outlined'}>
|
||||
{cate?.label}
|
||||
</Button>
|
||||
</Link>
|
||||
</RouterLink>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -105,7 +106,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
||||
</Text>
|
||||
</Flexbox>
|
||||
<Flexbox align={'center'} gap={6} horizontal>
|
||||
<Link
|
||||
<NextLink
|
||||
href={urlJoin(
|
||||
'https://github.com/lobehub/lobe-chat-agents/tree/main/locales',
|
||||
identifier as string,
|
||||
@@ -114,14 +115,14 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
||||
target={'_blank'}
|
||||
>
|
||||
<ActionIcon fill={theme.colorTextDescription} icon={Github} />
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
{author && (
|
||||
<Link href={urlJoin('https://github.com', author)} target={'_blank'}>
|
||||
<NextLink href={urlJoin('https://github.com', author)} target={'_blank'}>
|
||||
{author}
|
||||
</Link>
|
||||
</NextLink>
|
||||
)}
|
||||
<Icon icon={DotIcon} />
|
||||
<PublishedTime
|
||||
|
||||
@@ -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(() => {
|
||||
</Title>
|
||||
<Flexbox gap={8}>
|
||||
{related?.map((item, index) => {
|
||||
const link = urlJoin('/discover/assistant', item.identifier);
|
||||
const link = urlJoin('/assistant', item.identifier);
|
||||
return (
|
||||
<Link href={link} key={index} style={{ color: 'inherit', overflow: 'hidden' }}>
|
||||
<Link key={index} style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
|
||||
<Item {...item} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<StructuredData ld={ld} />
|
||||
{!isMobile && <Breadcrumb identifier={identifier} tab={DiscoverTab.Assistants} />}
|
||||
<Client identifier={identifier} mobile={isMobile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => [];
|
||||
|
||||
Page.DisplayName = 'DiscoverAssistantsDetail';
|
||||
|
||||
export default Page;
|
||||
@@ -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 (
|
||||
<Flexbox
|
||||
align="center"
|
||||
height="100%"
|
||||
justify="center"
|
||||
style={{ minHeight: 400 }}
|
||||
width="100%"
|
||||
>
|
||||
<h2>{t('title')}</h2>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default NotFound;
|
||||
@@ -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 (
|
||||
<Link className={styles.back} href={href} style={{ marginBottom: 8, ...style }}>
|
||||
<Link className={styles.back} style={{ marginBottom: 8, ...style }} to={href}>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
<Icon icon={ArrowLeft} />
|
||||
{t(`back`)}
|
||||
|
||||
@@ -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
|
||||
<AntdBreadcrumb
|
||||
items={[
|
||||
{
|
||||
title: <Link href={'/discover'}>Discover</Link>,
|
||||
title: <Link to={'/'}>Discover</Link>,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Link href={urlJoin('/discover', tab)}>
|
||||
<Link to={`/${tab}`}>
|
||||
{tab === DiscoverTab.Mcp ? 'MCP Servers' : t(`tab.${tab}` as any)}
|
||||
</Link>
|
||||
),
|
||||
|
||||
@@ -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<PropsWithChildren>({ Desktop, Mobile });
|
||||
|
||||
MainLayout.displayName = 'DiscoverAssistantsDetailLayout';
|
||||
|
||||
export default MainLayout;
|
||||
@@ -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<McpDetailPageProps>(({ 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 <Loading />;
|
||||
if (!data) return <NotFound />;
|
||||
|
||||
return (
|
||||
<TocProvider>
|
||||
<DetailProvider config={data}>
|
||||
{!mobile && <Breadcrumb identifier={identifier} tab={DiscoverTab.Mcp} />}
|
||||
<Flexbox gap={16}>
|
||||
<Header mobile={mobile} />
|
||||
<Details mobile={mobile} />
|
||||
</Flexbox>
|
||||
</DetailProvider>
|
||||
</TocProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export default withSuspense(McpDetailPage);
|
||||
@@ -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(() => {
|
||||
</Title>
|
||||
<Flexbox gap={8}>
|
||||
{related?.map((item, index) => {
|
||||
const link = urlJoin('/discover/mcp', item.identifier);
|
||||
const link = urlJoin('/mcp', item.identifier);
|
||||
return (
|
||||
<Link href={link} key={index} style={{ color: 'inherit', overflow: 'hidden' }}>
|
||||
<Link key={index} style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
|
||||
<Item {...item} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -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 && <StructuredData ld={ld} />}
|
||||
<Client identifier={identifier} mobile={isMobile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Page.displayName = 'DiscoverMCPDetail';
|
||||
|
||||
export default Page;
|
||||
@@ -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<ModelDetailPageProps>(({ 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 <Loading />;
|
||||
if (!data) return <NotFound />;
|
||||
|
||||
return (
|
||||
<DetailProvider config={data}>
|
||||
{!mobile && <Breadcrumb identifier={identifier} tab={DiscoverTab.Models} />}
|
||||
<Flexbox gap={16}>
|
||||
<Header mobile={mobile} />
|
||||
<Details mobile={mobile} />
|
||||
</Flexbox>
|
||||
</DetailProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export default withSuspense(ModelDetailPage);
|
||||
@@ -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(() => {
|
||||
</Title>
|
||||
<Flexbox gap={8}>
|
||||
{related?.map((item, index) => {
|
||||
const link = urlJoin('/discover/model', item.identifier);
|
||||
const link = urlJoin('/model', item.identifier);
|
||||
return (
|
||||
<Link href={link} key={index} style={{ color: 'inherit', overflow: 'hidden' }}>
|
||||
<Link key={index} style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
|
||||
<Item {...item} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<StructuredData ld={ld} />
|
||||
<Client identifier={identifier} mobile={isMobile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Page.DisplayName = 'DiscoverModelDetail';
|
||||
|
||||
export default Page;
|
||||
@@ -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<ProviderDetailPageProps>(({ 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 <Loading />;
|
||||
if (!data) return <NotFound />;
|
||||
|
||||
return (
|
||||
<DetailProvider config={data}>
|
||||
{!mobile && <Breadcrumb identifier={identifier} tab={DiscoverTab.Providers} />}
|
||||
<Flexbox gap={16}>
|
||||
<Header mobile={mobile} />
|
||||
<Details mobile={mobile} />
|
||||
</Flexbox>
|
||||
</DetailProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export default withSuspense(ProviderDetailPage);
|
||||
@@ -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'
|
||||
|
||||
@@ -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(() => {
|
||||
</Title>
|
||||
<Flexbox gap={8}>
|
||||
{related?.map((item, index) => {
|
||||
const link = urlJoin('/discover/provider', item.identifier);
|
||||
const link = urlJoin('/provider', item.identifier);
|
||||
return (
|
||||
<Link href={link} key={index} style={{ color: 'inherit', overflow: 'hidden' }}>
|
||||
<Link key={index} style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
|
||||
<Item {...item} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<StructuredData ld={ld} />
|
||||
<Client identifier={identifier} mobile={isMobile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Page.DisplayName = 'DiscoverProviderDetail';
|
||||
|
||||
export default Page;
|
||||
@@ -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 <Loading />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title more={t('home.more')} moreLink={'/discover/assistant'}>
|
||||
{t('home.featuredAssistants')}
|
||||
</Title>
|
||||
<AssistantList data={assistantList.items} rows={4} />
|
||||
<div />
|
||||
<Title more={t('home.more')} moreLink={'/discover/mcp'}>
|
||||
{t('home.featuredTools')}
|
||||
</Title>
|
||||
<McpList data={mcpList.items} rows={4} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default HomePage;
|
||||
@@ -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 (
|
||||
<>
|
||||
<StructuredData ld={ld} />
|
||||
<Client mobile={isMobile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Page.DisplayName = 'DiscoverHome';
|
||||
|
||||
export default Page;
|
||||
@@ -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 (
|
||||
<Center className={cx(styles.container, hide && styles.hide)} height={46}>
|
||||
@@ -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 });
|
||||
|
||||
@@ -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<ListLayoutProps>(({ children, mobile }) => {
|
||||
if (mobile) {
|
||||
return <Mobile>{children}</Mobile>;
|
||||
}
|
||||
|
||||
return <Desktop>{children}</Desktop>;
|
||||
});
|
||||
|
||||
ListLayout.displayName = 'ListLayout';
|
||||
|
||||
export default ListLayout;
|
||||
@@ -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
|
||||
|
||||
@@ -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<AssistantLayoutProps>(({ children, mobile }) => {
|
||||
if (mobile) {
|
||||
return <Mobile>{children}</Mobile>;
|
||||
}
|
||||
return <Desktop>{children}</Desktop>;
|
||||
});
|
||||
|
||||
AssistantLayout.displayName = 'AssistantLayout';
|
||||
|
||||
export default AssistantLayout;
|
||||
@@ -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 <Loading />;
|
||||
|
||||
const { items, currentPage, pageSize, totalCount } = data;
|
||||
|
||||
return (
|
||||
<Flexbox gap={32} width={'100%'}>
|
||||
<List data={items} />
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
tab={DiscoverTab.Assistants}
|
||||
total={totalCount}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default withSuspense(AssistantPage);
|
||||
@@ -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: <Icon icon={item.icon} size={18} />,
|
||||
label: <Link href={genUrl(item.key)}>{item.label}</Link>,
|
||||
label: <Link to={genUrl(item.key)}>{item.label}</Link>,
|
||||
};
|
||||
})}
|
||||
mode={'inline'}
|
||||
|
||||
@@ -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<DiscoverAssistantItem>(
|
||||
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<DiscoverAssistantItem>(
|
||||
clickable
|
||||
height={'100%'}
|
||||
onClick={() => {
|
||||
router.push(link);
|
||||
navigate(link);
|
||||
}}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
@@ -119,7 +118,7 @@ const AssistantItem = memo<DiscoverAssistantItem>(
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Link href={link} style={{ color: 'inherit', overflow: 'hidden' }}>
|
||||
<Link style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
|
||||
<Text as={'h2'} className={styles.title} ellipsis>
|
||||
{title}
|
||||
</Text>
|
||||
@@ -129,16 +128,17 @@ const AssistantItem = memo<DiscoverAssistantItem>(
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
<Link
|
||||
<a
|
||||
href={urlJoin(
|
||||
'https://github.com/lobehub/lobe-chat-agents/tree/main/locales',
|
||||
identifier,
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
rel="noopener noreferrer"
|
||||
target={'_blank'}
|
||||
>
|
||||
<ActionIcon fill={theme.colorTextDescription} icon={Github} />
|
||||
</Link>
|
||||
</a>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Flexbox flex={1} gap={12} paddingInline={16}>
|
||||
|
||||
@@ -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<PropsWithChildren>({ Desktop, Mobile });
|
||||
|
||||
MainLayout.displayName = 'DiscoverAssistantsLayout';
|
||||
|
||||
export default MainLayout;
|
||||
@@ -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 (
|
||||
<>
|
||||
<StructuredData ld={ld} />
|
||||
<Client mobile={isMobile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Page.DisplayName = 'DiscoverAssistants';
|
||||
|
||||
export default Page;
|
||||
@@ -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<PaginationProps>(({ 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 });
|
||||
|
||||
@@ -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<PropsWithChildren>({ Desktop, Mobile });
|
||||
|
||||
MainLayout.displayName = 'DiscoverLayout';
|
||||
|
||||
export default MainLayout;
|
||||
21
src/app/[variants]/(main)/discover/(list)/mcp/McpLayout.tsx
Normal file
21
src/app/[variants]/(main)/discover/(list)/mcp/McpLayout.tsx
Normal file
@@ -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<McpLayoutProps>(({ children, mobile }) => {
|
||||
if (mobile) {
|
||||
return <Mobile>{children}</Mobile>;
|
||||
}
|
||||
return <Desktop>{children}</Desktop>;
|
||||
});
|
||||
|
||||
McpLayout.displayName = 'McpLayout';
|
||||
|
||||
export default McpLayout;
|
||||
44
src/app/[variants]/(main)/discover/(list)/mcp/McpPage.tsx
Normal file
44
src/app/[variants]/(main)/discover/(list)/mcp/McpPage.tsx
Normal file
@@ -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 <Loading />;
|
||||
|
||||
const { items, currentPage, pageSize, totalCount } = data;
|
||||
|
||||
return (
|
||||
<Flexbox gap={32} width={'100%'}>
|
||||
<List data={items} />
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
tab={DiscoverTab.Mcp}
|
||||
total={totalCount}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default withSuspense(McpPage);
|
||||
@@ -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: <Icon icon={item.icon} size={18} />,
|
||||
label: <Link href={genUrl(item.key)}>{item.label}</Link>,
|
||||
label: <Link to={genUrl(item.key)}>{item.label}</Link>,
|
||||
};
|
||||
})}
|
||||
mode={'inline'}
|
||||
|
||||
@@ -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<DiscoverMcpItem>(
|
||||
}) => {
|
||||
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 (
|
||||
<Block
|
||||
clickable
|
||||
height={'100%'}
|
||||
onClick={() => {
|
||||
router.push(link);
|
||||
navigate(link);
|
||||
}}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
@@ -128,7 +127,7 @@ const McpItem = memo<DiscoverMcpItem>(
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Link href={link} style={{ color: 'inherit', overflow: 'hidden' }}>
|
||||
<Link style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
|
||||
<Text as={'h2'} className={styles.title} ellipsis>
|
||||
{name}
|
||||
</Text>
|
||||
@@ -145,9 +144,14 @@ const McpItem = memo<DiscoverMcpItem>(
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
{installationMethods && <InstallationIcon type={installationMethods} />}
|
||||
{github && (
|
||||
<Link href={github.url} onClick={(e) => e.stopPropagation()} target={'_blank'}>
|
||||
<a
|
||||
href={github.url}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
rel="noopener noreferrer"
|
||||
target={'_blank'}
|
||||
>
|
||||
<ActionIcon fill={theme.colorTextDescription} icon={Github} />
|
||||
</Link>
|
||||
</a>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
|
||||
@@ -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<PropsWithChildren>({ Desktop, Mobile });
|
||||
|
||||
MainLayout.displayName = 'DiscoverToolsLayout';
|
||||
|
||||
export default MainLayout;
|
||||
@@ -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 (
|
||||
<>
|
||||
<StructuredData ld={ld} />
|
||||
<Client mobile={isMobile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Page.DisplayName = 'DiscoverMCP';
|
||||
|
||||
export default Page;
|
||||
@@ -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<ModelLayoutProps>(({ children, mobile }) => {
|
||||
if (mobile) {
|
||||
return <Mobile>{children}</Mobile>;
|
||||
}
|
||||
return <Desktop>{children}</Desktop>;
|
||||
});
|
||||
|
||||
ModelLayout.displayName = 'ModelLayout';
|
||||
|
||||
export default ModelLayout;
|
||||
@@ -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 <Loading />;
|
||||
|
||||
const { items, currentPage, pageSize, totalCount } = data;
|
||||
|
||||
return (
|
||||
<Flexbox gap={32} width={'100%'}>
|
||||
<List data={items} />
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
tab={DiscoverTab.Models}
|
||||
total={totalCount}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default withSuspense(ModelPage);
|
||||
@@ -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 (
|
||||
<Flexbox gap={24} horizontal style={{ position: 'relative' }} width={'100%'}>
|
||||
<CategoryContainer>
|
||||
|
||||
@@ -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: <Icon icon={item.icon} size={18} />,
|
||||
label: <Link href={genUrl(item.key)}>{item.label}</Link>,
|
||||
label: <Link to={genUrl(item.key)}>{item.label}</Link>,
|
||||
};
|
||||
})}
|
||||
mode={'inline'}
|
||||
|
||||
@@ -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<DiscoverModelItem>(
|
||||
({ 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 (
|
||||
<Block
|
||||
clickable
|
||||
height={'100%'}
|
||||
onClick={() => {
|
||||
router.push(link);
|
||||
navigate(link);
|
||||
}}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
@@ -106,7 +105,7 @@ const ModelItem = memo<DiscoverModelItem>(
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Link href={link} style={{ color: 'inherit', overflow: 'hidden' }}>
|
||||
<Link style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
|
||||
<Text as={'h2'} className={styles.title} ellipsis>
|
||||
{displayName}
|
||||
</Text>
|
||||
|
||||
@@ -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<PropsWithChildren>({ Desktop, Mobile });
|
||||
|
||||
MainLayout.displayName = 'DiscoverModelsLayout';
|
||||
|
||||
export default MainLayout;
|
||||
@@ -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 (
|
||||
<>
|
||||
<StructuredData ld={ld} />
|
||||
<Client />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Page.DisplayName = 'DiscoverModels';
|
||||
|
||||
export default Page;
|
||||
@@ -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 <Loading />;
|
||||
|
||||
const { items, currentPage, pageSize, totalCount } = data;
|
||||
|
||||
return (
|
||||
<Flexbox gap={32} width={'100%'}>
|
||||
<List data={items} />
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
tab={DiscoverTab.Providers}
|
||||
total={totalCount}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default withSuspense(ProviderPage);
|
||||
@@ -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<DiscoverProviderItem>(
|
||||
({ 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<DiscoverProviderItem>(
|
||||
clickable
|
||||
height={'100%'}
|
||||
onClick={() => {
|
||||
router.push(link);
|
||||
navigate(link);
|
||||
}}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
@@ -80,22 +79,28 @@ const ProviderItem = memo<DiscoverProviderItem>(
|
||||
}}
|
||||
title={identifier}
|
||||
>
|
||||
<Link href={link} style={{ color: 'inherit', overflow: 'hidden' }}>
|
||||
<Link style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
|
||||
<ProviderCombine provider={identifier} size={28} style={{ flex: 'none' }} />
|
||||
</Link>
|
||||
<div className={styles.author}>@{name}</div>
|
||||
</Flexbox>
|
||||
<Flexbox align={'center'} horizontal>
|
||||
<Link href={url} onClick={(e) => e.stopPropagation()} target={'_blank'}>
|
||||
<a
|
||||
href={url}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
rel="noopener noreferrer"
|
||||
target={'_blank'}
|
||||
>
|
||||
<ActionIcon color={theme.colorTextDescription} icon={GlobeIcon} />
|
||||
</Link>
|
||||
<Link
|
||||
</a>
|
||||
<a
|
||||
href={`https://github.com/lobehub/lobe-chat/blob/main/src/config/modelProviders/${identifier}.ts`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
rel="noopener noreferrer"
|
||||
target={'_blank'}
|
||||
>
|
||||
<ActionIcon fill={theme.colorTextDescription} icon={Github} />
|
||||
</Link>
|
||||
</a>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Flexbox flex={1} gap={12} paddingInline={16}>
|
||||
@@ -122,7 +127,7 @@ const ProviderItem = memo<DiscoverProviderItem>(
|
||||
.slice(0, 6)
|
||||
.filter(Boolean)
|
||||
.map((tag: string) => (
|
||||
<Link href={urlJoin('/discover/model', tag)} key={tag}>
|
||||
<Link key={tag} to={urlJoin('/model', tag)}>
|
||||
<ModelTag model={tag} style={{ margin: 0 }} />
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<StructuredData ld={ld} />
|
||||
<Client mobile={isMobile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Page.DisplayName = 'DiscoverProviders';
|
||||
|
||||
export default Page;
|
||||
167
src/app/[variants]/(main)/discover/DiscoverRouter.tsx
Normal file
167
src/app/[variants]/(main)/discover/DiscoverRouter.tsx
Normal file
@@ -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 (
|
||||
<MemoryRouter initialEntries={[getInitialPath()]} initialIndex={0}>
|
||||
<UrlSynchronizer />
|
||||
<DiscoverLayout mobile={mobile}>
|
||||
<Routes>
|
||||
{/* List routes with ListLayout */}
|
||||
<Route
|
||||
element={
|
||||
<ListLayout mobile={mobile}>
|
||||
<HomePage mobile={mobile} />
|
||||
</ListLayout>
|
||||
}
|
||||
path="/"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<ListLayout mobile={mobile}>
|
||||
<AssistantLayout mobile={mobile}>
|
||||
<AssistantPage mobile={mobile} />
|
||||
</AssistantLayout>
|
||||
</ListLayout>
|
||||
}
|
||||
path="/assistant"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<ListLayout mobile={mobile}>
|
||||
<ModelLayout mobile={mobile}>
|
||||
<ModelPage mobile={mobile} />
|
||||
</ModelLayout>
|
||||
</ListLayout>
|
||||
}
|
||||
path="/model"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<ListLayout mobile={mobile}>
|
||||
<ProviderPage mobile={mobile} />
|
||||
</ListLayout>
|
||||
}
|
||||
path="/provider"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<ListLayout mobile={mobile}>
|
||||
<McpLayout mobile={mobile}>
|
||||
<McpPage mobile={mobile} />
|
||||
</McpLayout>
|
||||
</ListLayout>
|
||||
}
|
||||
path="/mcp"
|
||||
/>
|
||||
|
||||
{/* Detail routes with DetailLayout */}
|
||||
<Route
|
||||
element={
|
||||
<DetailLayout mobile={mobile}>
|
||||
<AssistantDetailPage mobile={mobile} />
|
||||
</DetailLayout>
|
||||
}
|
||||
path="/assistant/*"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DetailLayout mobile={mobile}>
|
||||
<ModelDetailPage mobile={mobile} />
|
||||
</DetailLayout>
|
||||
}
|
||||
path="/model/*"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DetailLayout mobile={mobile}>
|
||||
<ProviderDetailPage mobile={mobile} />
|
||||
</DetailLayout>
|
||||
}
|
||||
path="/provider/*"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DetailLayout mobile={mobile}>
|
||||
<McpDetailPage mobile={mobile} />
|
||||
</DetailLayout>
|
||||
}
|
||||
path="/mcp/*"
|
||||
/>
|
||||
|
||||
{/* Fallback */}
|
||||
<Route element={<Navigate replace to="/" />} path="*" />
|
||||
</Routes>
|
||||
</DiscoverLayout>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
export default DiscoverRouter;
|
||||
11
src/app/[variants]/(main)/discover/[[...path]]/page.tsx
Normal file
11
src/app/[variants]/(main)/discover/[[...path]]/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import DiscoverRouter from '../DiscoverRouter';
|
||||
|
||||
const DiscoverPage = () => {
|
||||
return <DiscoverRouter />;
|
||||
};
|
||||
|
||||
DiscoverPage.displayName = 'DiscoverPage';
|
||||
|
||||
export default DiscoverPage;
|
||||
@@ -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 (
|
||||
<ChatHeader
|
||||
left={
|
||||
<Link href={'/discover'} style={{ color: 'inherit' }}>
|
||||
<Link style={{ color: 'inherit' }} to={'/'}>
|
||||
<ProductLogo extra={'Discover'} size={36} type={'text'} />
|
||||
</Link>
|
||||
}
|
||||
|
||||
@@ -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<DiscoverLayoutProps>(({ children, mobile }) => {
|
||||
if (mobile) {
|
||||
return <Mobile>{children}</Mobile>;
|
||||
}
|
||||
|
||||
return <Desktop>{children}</Desktop>;
|
||||
});
|
||||
|
||||
DiscoverLayout.displayName = 'DiscoverLayout';
|
||||
|
||||
export default DiscoverLayout;
|
||||
@@ -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<TitleProps>(({ tag, children, moreLink, more }) => {
|
||||
title
|
||||
)}
|
||||
{moreLink && (
|
||||
<Link href={moreLink} target={moreLink.startsWith('http') ? '_blank' : undefined}>
|
||||
<Link
|
||||
target={moreLink.startsWith('http') ? '_blank' : undefined}
|
||||
to={moreLink}
|
||||
>
|
||||
<Button className={styles.more} style={{ paddingInline: 6 }} type={'text'}>
|
||||
<span>{more}</span>
|
||||
<Icon icon={ChevronRight} />
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import NextLink from 'next/link';
|
||||
import { ReactNode, memo } from 'react';
|
||||
import { Flexbox, FlexboxProps } from 'react-layout-kit';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
more: css`
|
||||
@@ -39,10 +40,41 @@ const Title = memo<TitleProps>(
|
||||
({ id, tag, children, moreLink, more, level = 2, icon, ...rest }) => {
|
||||
const { cx, styles } = useStyles();
|
||||
const title = (
|
||||
<h2 className={cx(styles.title, styles[`title${level}`])} id={id}>
|
||||
<h2 className={cx(styles.title, styles[`title${level}` as 'title2' | 'title3'])} id={id}>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
|
||||
// Check if it's an external link or internal discover route
|
||||
const isExternalLink = moreLink?.startsWith('http') ?? false;
|
||||
const isDiscoverRoute = moreLink?.startsWith('/discover') ?? false;
|
||||
|
||||
let moreLinkElement = null;
|
||||
if (moreLink) {
|
||||
if (isExternalLink) {
|
||||
moreLinkElement = (
|
||||
<NextLink className={styles.more} href={moreLink} target="_blank">
|
||||
<span style={{ marginRight: 4 }}>{more}</span>
|
||||
<Icon icon={ChevronRight} />
|
||||
</NextLink>
|
||||
);
|
||||
} else if (isDiscoverRoute) {
|
||||
moreLinkElement = (
|
||||
<RouterLink className={styles.more} to={moreLink.replace('/discover', '')}>
|
||||
<span style={{ marginRight: 4 }}>{more}</span>
|
||||
<Icon icon={ChevronRight} />
|
||||
</RouterLink>
|
||||
);
|
||||
} else {
|
||||
moreLinkElement = (
|
||||
<NextLink className={styles.more} href={moreLink}>
|
||||
<span style={{ marginRight: 4 }}>{more}</span>
|
||||
<Icon icon={ChevronRight} />
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
@@ -65,16 +97,7 @@ const Title = memo<TitleProps>(
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
{moreLink && (
|
||||
<Link
|
||||
className={styles.more}
|
||||
href={moreLink}
|
||||
target={moreLink.startsWith('http') ? '_blank' : undefined}
|
||||
>
|
||||
<span style={{ marginRight: 4 }}>{more}</span>
|
||||
<Icon icon={ChevronRight} />
|
||||
</Link>
|
||||
)}
|
||||
{moreLinkElement}
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { MCP } from '@lobehub/icons';
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { Bot, Brain, BrainCircuit, House } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import urlJoin from 'url-join';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import type { MenuProps } from '@/components/Menu';
|
||||
import { DiscoverTab } from '@/types/discover';
|
||||
@@ -13,19 +11,20 @@ import { DiscoverTab } from '@/types/discover';
|
||||
const ICON_SIZE = 16;
|
||||
|
||||
export const useNav = () => {
|
||||
const pathname = usePathname();
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation('discover');
|
||||
|
||||
const activeKey = useMemo(() => {
|
||||
const pathname = location.pathname;
|
||||
for (const value of Object.values(DiscoverTab)) {
|
||||
if (pathname.includes(urlJoin('/discover', DiscoverTab.Plugins))) {
|
||||
if (pathname.includes(`/${DiscoverTab.Plugins}`)) {
|
||||
return DiscoverTab.Mcp;
|
||||
} else if (pathname.includes(urlJoin('/discover', value))) {
|
||||
} else if (pathname.includes(`/${value}`)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return DiscoverTab.Home;
|
||||
}, [pathname]);
|
||||
}, [location.pathname]);
|
||||
|
||||
const items: MenuProps['items'] = useMemo(
|
||||
() => [
|
||||
@@ -33,7 +32,7 @@ export const useNav = () => {
|
||||
icon: <Icon icon={House} size={ICON_SIZE} />,
|
||||
key: DiscoverTab.Home,
|
||||
label: (
|
||||
<Link href={'/discover'} style={{ color: 'inherit' }}>
|
||||
<Link style={{ color: 'inherit' }} to={'/'}>
|
||||
{t('tab.home')}
|
||||
</Link>
|
||||
),
|
||||
@@ -42,7 +41,7 @@ export const useNav = () => {
|
||||
icon: <Icon icon={Bot} size={ICON_SIZE} />,
|
||||
key: DiscoverTab.Assistants,
|
||||
label: (
|
||||
<Link href={urlJoin('/discover', DiscoverTab.Assistants)} style={{ color: 'inherit' }}>
|
||||
<Link style={{ color: 'inherit' }} to={`/${DiscoverTab.Assistants}`}>
|
||||
{t('tab.assistant')}
|
||||
</Link>
|
||||
),
|
||||
@@ -51,7 +50,7 @@ export const useNav = () => {
|
||||
icon: <MCP className={'anticon'} size={ICON_SIZE} />,
|
||||
key: DiscoverTab.Mcp,
|
||||
label: (
|
||||
<Link href={urlJoin('/discover', DiscoverTab.Mcp)} style={{ color: 'inherit' }}>
|
||||
<Link style={{ color: 'inherit' }} to={`/${DiscoverTab.Mcp}`}>
|
||||
{`MCP ${t('tab.plugin')}`}
|
||||
</Link>
|
||||
),
|
||||
@@ -60,7 +59,7 @@ export const useNav = () => {
|
||||
icon: <Icon icon={Brain} size={ICON_SIZE} />,
|
||||
key: DiscoverTab.Models,
|
||||
label: (
|
||||
<Link href={urlJoin('/discover', DiscoverTab.Models)} style={{ color: 'inherit' }}>
|
||||
<Link style={{ color: 'inherit' }} to={`/${DiscoverTab.Models}`}>
|
||||
{t('tab.model')}
|
||||
</Link>
|
||||
),
|
||||
@@ -69,7 +68,7 @@ export const useNav = () => {
|
||||
icon: <Icon icon={BrainCircuit} size={ICON_SIZE} />,
|
||||
key: DiscoverTab.Providers,
|
||||
label: (
|
||||
<Link href={urlJoin('/discover', DiscoverTab.Providers)} style={{ color: 'inherit' }}>
|
||||
<Link style={{ color: 'inherit' }} to={`/${DiscoverTab.Providers}`}>
|
||||
{t('tab.provider')}
|
||||
</Link>
|
||||
),
|
||||
|
||||
@@ -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<PropsWithChildren>({ Desktop, Mobile });
|
||||
|
||||
MainLayout.displayName = 'DiscoverStoreLayout';
|
||||
|
||||
export default MainLayout;
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Switch } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import Image from 'next/image';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
@@ -41,6 +42,8 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
align-items: center;
|
||||
`,
|
||||
thumb: css`
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 250px;
|
||||
@@ -48,12 +51,6 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
|
||||
background: linear-gradient(135deg, ${token.colorFillTertiary}, ${token.colorFillQuaternary});
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
`,
|
||||
title: css`
|
||||
font-size: 16px;
|
||||
@@ -73,7 +70,9 @@ const LabCard = memo<PropsWithChildren<LabCardProps>>(
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.thumb}>{cover && <img alt={title} src={cover} />}</div>
|
||||
<div className={styles.thumb}>
|
||||
{cover && <Image alt={title} fill src={cover} style={{ objectFit: 'cover' }} />}
|
||||
</div>
|
||||
<Flexbox gap={6}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.desc}>{desc}</div>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { QueryClient } from '@tanstack/query-core';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import React, { PropsWithChildren, useState } from 'react';
|
||||
|
||||
import { lambdaQuery, lambdaQueryClient } from '@/libs/trpc/client';
|
||||
|
||||
const QueryProvider = ({ children }: PropsWithChildren) => {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
// Cast required because pnpm installs separate QueryClient type instances for trpc and app
|
||||
const providerQueryClient = queryClient as unknown as React.ComponentProps<
|
||||
typeof lambdaQuery.Provider
|
||||
>['queryClient'];
|
||||
|
||||
return (
|
||||
<lambdaQuery.Provider client={lambdaQueryClient} queryClient={queryClient}>
|
||||
<lambdaQuery.Provider client={lambdaQueryClient} queryClient={providerQueryClient}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</lambdaQuery.Provider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user