mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-17 04:55:51 +00:00
✨ feat: rebranding total UI of app
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Center } from 'react-layout-kit';
|
||||
|
||||
import BrandWatermark from '@/components/BrandWatermark';
|
||||
|
||||
import Category from './features/Category';
|
||||
import UserBanner from './features/UserBanner';
|
||||
|
||||
const MeHomePage = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<UserBanner />
|
||||
<Category />
|
||||
<Center padding={16}>
|
||||
<BrandWatermark />
|
||||
</Center>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
MeHomePage.displayName = 'MeHomePage';
|
||||
|
||||
export default MeHomePage;
|
||||
@@ -1,15 +0,0 @@
|
||||
import MobileContentLayout from "@/components/server/MobileNavLayout";
|
||||
import Loading from "@/components/Loading/BrandTextLoading";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import Header from "./features/Header";
|
||||
import { Suspense } from "react";
|
||||
|
||||
const Layout = () => {
|
||||
return <MobileContentLayout header={<Header />} withNav>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</MobileContentLayout>
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -1,11 +0,0 @@
|
||||
import MobileContentLayout from "@/components/server/MobileNavLayout";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import Header from "./features/Header";
|
||||
|
||||
const Layout = () => {
|
||||
return <MobileContentLayout header={<Header />}>
|
||||
<Outlet />
|
||||
</MobileContentLayout>
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -1,15 +0,0 @@
|
||||
import MobileContentLayout from "@/components/server/MobileNavLayout";
|
||||
import Loading from "@/components/Loading/BrandTextLoading";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import Header from "./features/Header";
|
||||
import { Suspense } from "react";
|
||||
|
||||
const Layout = () => {
|
||||
return <MobileContentLayout header={<Header />} withNav>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</MobileContentLayout>
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import StoreSearchBar from '@/app/[variants]/(main)/community/features/Search';
|
||||
import UserAvatar from '@/app/[variants]/(main)/community/features/UserAvatar';
|
||||
import NavHeader from '@/features/NavHeader';
|
||||
|
||||
const Header = memo(() => {
|
||||
const theme = useTheme();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Extract the path segment (assistant, model, provider, mcp)
|
||||
const path = location.pathname.split('/').find(Boolean);
|
||||
|
||||
const handleGoBack = () => {
|
||||
navigate(`/${path}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<NavHeader
|
||||
left={
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
<ActionIcon icon={ArrowLeft} onClick={handleGoBack} size={'small'} />
|
||||
<StoreSearchBar />
|
||||
</Flexbox>
|
||||
}
|
||||
right={<UserAvatar />}
|
||||
style={{
|
||||
borderBottom: `1px solid ${theme.colorBorderSecondary}`,
|
||||
}}
|
||||
styles={{
|
||||
left: { flex: 1 },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import Footer from '@/features/Setting/Footer';
|
||||
import WideScreenContainer from '@/features/WideScreenContainer';
|
||||
|
||||
import { MAX_WIDTH, SCROLL_PARENT_ID } from '../../features/const';
|
||||
import Header from './Header';
|
||||
|
||||
/**
|
||||
* Desktop Discover Detail Layout
|
||||
* Layout for detail pages (assistant, model, provider, mcp details)
|
||||
*/
|
||||
const DesktopDiscoverDetailLayout = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Flexbox height={'100%'} id={SCROLL_PARENT_ID} style={{ overflowY: 'auto' }} width={'100%'}>
|
||||
<WideScreenContainer
|
||||
gap={32}
|
||||
minWidth={MAX_WIDTH}
|
||||
paddingBlock={16}
|
||||
style={{
|
||||
minHeight: '100%',
|
||||
}}
|
||||
wrapperStyle={{
|
||||
minHeight: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
<div style={{ flex: 1 }} />
|
||||
<Footer />
|
||||
</WideScreenContainer>
|
||||
</Flexbox>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
DesktopDiscoverDetailLayout.displayName = 'DesktopDiscoverDetailLayout';
|
||||
|
||||
export default DesktopDiscoverDetailLayout;
|
||||
+10
-3
@@ -1,18 +1,25 @@
|
||||
import { Block } from '@lobehub/ui';
|
||||
import { Empty } from 'antd';
|
||||
import { Block, Empty } from '@lobehub/ui';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
import KnowledgeItem from './KnowledgeItem';
|
||||
|
||||
const Knowledge = memo(() => {
|
||||
const { t } = useTranslation('discover');
|
||||
const { config } = useDetailContext();
|
||||
|
||||
if (!config?.knowledgeBases?.length)
|
||||
return (
|
||||
<Block variant={'outlined'}>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
<Empty
|
||||
description={t('assistants.details.capabilities.knowledge.desc')}
|
||||
descriptionProps={{ fontSize: 14 }}
|
||||
icon={BookOpen}
|
||||
style={{ maxWidth: 400 }}
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ const KnowledgeItem = memo<{ avatar?: string; description?: string; title: strin
|
||||
|
||||
return (
|
||||
<Block gap={12} horizontal padding={12} variant={'outlined'}>
|
||||
<Avatar avatar={avatar} size={40} style={{ flex: 'none' }} />
|
||||
<Avatar avatar={avatar} shape={'square'} size={40} style={{ flex: 'none' }} />
|
||||
<Flexbox
|
||||
flex={1}
|
||||
gap={6}
|
||||
+64
-8
@@ -1,7 +1,7 @@
|
||||
import { PluginSource } from '@lobechat/types';
|
||||
import { Avatar, Block, Tag, Text } from '@lobehub/ui';
|
||||
import { Skeleton } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { KLAVIS_SERVER_TYPES, KlavisServerType } from '@lobechat/const';
|
||||
import { DiscoverPluginDetail, PluginSource } from '@lobechat/types';
|
||||
import { Avatar, Block, Icon, Image, Skeleton, Tag, Text } from '@lobehub/ui';
|
||||
import { createStyles, useTheme } from 'antd-style';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
@@ -10,6 +10,24 @@ import urlJoin from 'url-join';
|
||||
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
|
||||
/**
|
||||
* Klavis icon component
|
||||
* For string type icon, use Image component to render
|
||||
* For IconType type icon, use Icon component to render with theme fill color
|
||||
*/
|
||||
const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (typeof icon === 'string') {
|
||||
return <Image alt={label} height={40} src={icon} style={{ flex: 'none' }} width={40} />;
|
||||
}
|
||||
|
||||
// Use theme color fill, automatically adapts in dark mode
|
||||
return <Icon fill={theme.colorText} icon={icon} size={40} />;
|
||||
});
|
||||
|
||||
KlavisIcon.displayName = 'KlavisIcon';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => {
|
||||
return {
|
||||
clickable: css`
|
||||
@@ -53,9 +71,36 @@ interface PluginItemProps {
|
||||
const PluginItem = memo<PluginItemProps>(({ identifier }) => {
|
||||
const { t } = useTranslation('discover');
|
||||
const usePluginDetail = useDiscoverStore((s) => s.usePluginDetail);
|
||||
const { data, isLoading } = usePluginDetail({ identifier, withManifest: false });
|
||||
const { data: apiData, isLoading } = usePluginDetail({ identifier, withManifest: false });
|
||||
const { styles, cx } = useStyles();
|
||||
|
||||
// Try to get Klavis tool info if API returns no data
|
||||
const klavisTool = useMemo(() => {
|
||||
return KLAVIS_SERVER_TYPES.find((tool) => tool.identifier === identifier);
|
||||
}, [identifier]);
|
||||
|
||||
// Convert Klavis tool to plugin detail format
|
||||
const data: DiscoverPluginDetail | undefined = useMemo(() => {
|
||||
if (apiData) return apiData;
|
||||
if (!klavisTool) return undefined;
|
||||
|
||||
return {
|
||||
author: 'Klavis',
|
||||
avatar: '', // Avatar will be rendered by KlavisIcon component
|
||||
category: undefined,
|
||||
createdAt: '',
|
||||
description: `LobeHub Mcp Server: ${klavisTool.label}`,
|
||||
homepage: 'https://klavis.ai',
|
||||
identifier: klavisTool.identifier,
|
||||
manifest: undefined,
|
||||
related: [],
|
||||
schemaVersion: 1,
|
||||
source: 'builtin' as const,
|
||||
tags: ['klavis', 'mcp'],
|
||||
title: klavisTool.label,
|
||||
};
|
||||
}, [apiData, klavisTool]);
|
||||
|
||||
const sourceConfig = useMemo(() => {
|
||||
const source: PluginSource = data?.source || 'market';
|
||||
|
||||
@@ -83,7 +128,7 @@ const PluginItem = memo<PluginItemProps>(({ identifier }) => {
|
||||
default: {
|
||||
return {
|
||||
clickable: true,
|
||||
href: urlJoin('/discover/plugin', identifier),
|
||||
href: urlJoin('/community/plugin', identifier),
|
||||
isExternal: false,
|
||||
tagColor: undefined,
|
||||
tagText: undefined,
|
||||
@@ -92,13 +137,24 @@ const PluginItem = memo<PluginItemProps>(({ identifier }) => {
|
||||
}
|
||||
}, [data?.source, data?.homepage, identifier, t]);
|
||||
|
||||
if (isLoading || !data)
|
||||
if (isLoading)
|
||||
return (
|
||||
<Block gap={12} horizontal key={identifier} padding={12} variant={'outlined'}>
|
||||
<Skeleton paragraph={{ rows: 1 }} title={false} />
|
||||
</Block>
|
||||
);
|
||||
|
||||
// If loading is complete but no data found, don't render anything
|
||||
if (!data) return null;
|
||||
|
||||
// Render avatar - use KlavisIcon for Klavis tools, Avatar for others
|
||||
const renderAvatar = () => {
|
||||
if (klavisTool) {
|
||||
return <KlavisIcon icon={klavisTool.icon} label={klavisTool.label} />;
|
||||
}
|
||||
return <Avatar avatar={data.avatar} shape={'square'} size={40} style={{ flex: 'none' }} />;
|
||||
};
|
||||
|
||||
const content = (
|
||||
<Block
|
||||
className={cx(sourceConfig.clickable ? styles.clickable : styles.noLink)}
|
||||
@@ -108,7 +164,7 @@ const PluginItem = memo<PluginItemProps>(({ identifier }) => {
|
||||
padding={12}
|
||||
variant={'outlined'}
|
||||
>
|
||||
<Avatar avatar={data.avatar} size={40} style={{ flex: 'none' }} />
|
||||
{renderAvatar()}
|
||||
<Flexbox
|
||||
flex={1}
|
||||
gap={6}
|
||||
+10
-3
@@ -1,18 +1,25 @@
|
||||
import { Block } from '@lobehub/ui';
|
||||
import { Empty } from 'antd';
|
||||
import { Block, Empty } from '@lobehub/ui';
|
||||
import { Plug2 } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
import PluginItem from './PluginItem';
|
||||
|
||||
const Plugin = memo(() => {
|
||||
const { t } = useTranslation('discover');
|
||||
const { config } = useDetailContext();
|
||||
|
||||
if (!config?.plugins?.length)
|
||||
return (
|
||||
<Block variant={'outlined'}>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
<Empty
|
||||
description={t('assistants.details.capabilities.plugin.desc')}
|
||||
descriptionProps={{ fontSize: 14 }}
|
||||
icon={Plug2}
|
||||
style={{ maxWidth: 400 }}
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
|
||||
+4
-4
@@ -2,10 +2,10 @@
|
||||
|
||||
import { Tag } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import Link from 'next/link';
|
||||
import qs from 'query-string';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useQuery } from '@/hooks/useQuery';
|
||||
import { AssistantMarketSource } from '@/types/discover';
|
||||
@@ -33,17 +33,17 @@ const TagList = memo<{ tags: string[] }>(({ tags }) => {
|
||||
<Flexbox gap={8} horizontal wrap={'wrap'}>
|
||||
{tags.map((tag) => (
|
||||
<Link
|
||||
href={qs.stringifyUrl(
|
||||
key={tag}
|
||||
to={qs.stringifyUrl(
|
||||
{
|
||||
query: {
|
||||
q: tag,
|
||||
source: marketSource,
|
||||
},
|
||||
url: '/discover/assistant',
|
||||
url: '/community/assistant',
|
||||
},
|
||||
{ skipNull: true },
|
||||
)}
|
||||
key={tag}
|
||||
>
|
||||
<Tag className={styles.tag}>{tag}</Tag>
|
||||
</Link>
|
||||
+1
-1
@@ -25,7 +25,7 @@ const Related = memo(() => {
|
||||
category,
|
||||
source: marketSource,
|
||||
},
|
||||
url: '/discover/assistant',
|
||||
url: '/community/assistant',
|
||||
},
|
||||
{ skipNull: true },
|
||||
)}
|
||||
+4
-4
@@ -2,10 +2,10 @@
|
||||
|
||||
import { Tag } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import Link from 'next/link';
|
||||
import qs from 'query-string';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useQuery } from '@/hooks/useQuery';
|
||||
import { AssistantMarketSource } from '@/types/discover';
|
||||
@@ -33,17 +33,17 @@ const TagList = memo<{ tags: string[] }>(({ tags }) => {
|
||||
<Flexbox gap={8} horizontal wrap={'wrap'}>
|
||||
{tags.map((tag) => (
|
||||
<Link
|
||||
href={qs.stringifyUrl(
|
||||
key={tag}
|
||||
to={qs.stringifyUrl(
|
||||
{
|
||||
query: {
|
||||
q: tag,
|
||||
source: marketSource,
|
||||
},
|
||||
url: '/discover/assistant',
|
||||
url: '/community/assistant',
|
||||
},
|
||||
{ skipNull: true },
|
||||
)}
|
||||
key={tag}
|
||||
>
|
||||
<Tag className={styles.tag}>{tag}</Tag>
|
||||
</Link>
|
||||
+11
-20
@@ -1,14 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { Github, MCP } from '@lobehub/icons';
|
||||
import { ActionIcon, Avatar, Button, Icon, Text, Tooltip } from '@lobehub/ui';
|
||||
import { MCP } from '@lobehub/icons';
|
||||
import { 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 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 { formatIntergerNumber } from '@/utils/format';
|
||||
@@ -45,6 +45,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
||||
tokenUsage,
|
||||
pluginCount,
|
||||
knowledgeCount,
|
||||
userName,
|
||||
} = useDetailContext();
|
||||
const { styles, theme } = useStyles();
|
||||
const { mobile = isMobile } = useResponsive();
|
||||
@@ -53,9 +54,9 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
||||
|
||||
const cateButton = (
|
||||
<Link
|
||||
href={qs.stringifyUrl({
|
||||
to={qs.stringifyUrl({
|
||||
query: { category: cate?.key },
|
||||
url: '/discover/assistant',
|
||||
url: '/community/assistant',
|
||||
})}
|
||||
>
|
||||
<Button icon={cate?.icon} size={'middle'} variant={'outlined'}>
|
||||
@@ -67,7 +68,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
||||
return (
|
||||
<Flexbox gap={12}>
|
||||
<Flexbox align={'flex-start'} gap={16} horizontal width={'100%'}>
|
||||
<Avatar avatar={avatar} size={mobile ? 48 : 64} />
|
||||
<Avatar avatar={avatar} shape={'square'} size={mobile ? 48 : 64} />
|
||||
<Flexbox
|
||||
flex={1}
|
||||
gap={4}
|
||||
@@ -104,24 +105,14 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
||||
{title}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
<Flexbox align={'center'} gap={6} horizontal>
|
||||
<Link
|
||||
href={urlJoin(
|
||||
'https://github.com/lobehub/lobe-chat-agents/tree/main/locales',
|
||||
identifier as string,
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
target={'_blank'}
|
||||
>
|
||||
<ActionIcon fill={theme.colorTextDescription} icon={Github} />
|
||||
</Link>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
{author && (
|
||||
<Link href={urlJoin('https://github.com', author)} target={'_blank'}>
|
||||
{author && userName ? (
|
||||
<Link style={{ color: 'inherit' }} to={urlJoin('/community/user', userName)}>
|
||||
{author}
|
||||
</Link>
|
||||
) : (
|
||||
author
|
||||
)}
|
||||
<Icon icon={DotIcon} />
|
||||
<PublishedTime
|
||||
+37
-58
@@ -9,8 +9,10 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { SESSION_CHAT_URL } from '@/const/url';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import type { LobeAgentConfig } from '@/types/agent';
|
||||
import { agentService } from '@/services/agent';
|
||||
import { discoverService } from '@/services/discover';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { useHomeStore } from '@/store/home';
|
||||
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
|
||||
@@ -22,53 +24,14 @@ const useStyles = createStyles(({ css }) => ({
|
||||
`,
|
||||
}));
|
||||
|
||||
type MarketAgentModel =
|
||||
| LobeAgentConfig['model']
|
||||
| {
|
||||
model: LobeAgentConfig['model'];
|
||||
parameters?: Partial<LobeAgentConfig['params']>;
|
||||
provider?: LobeAgentConfig['provider'];
|
||||
};
|
||||
|
||||
type MarketAgentConfig = Partial<Omit<LobeAgentConfig, 'model' | 'params'>> & {
|
||||
model?: MarketAgentModel;
|
||||
params?: Partial<LobeAgentConfig['params']>;
|
||||
};
|
||||
|
||||
const normalizeMarketAgentConfig = (
|
||||
config?: MarketAgentConfig,
|
||||
): Partial<LobeAgentConfig> | undefined => {
|
||||
if (!config) return undefined;
|
||||
|
||||
const { model, params, ...rest } = config;
|
||||
const normalized: Partial<LobeAgentConfig> = { ...rest };
|
||||
|
||||
const modelInfo = model;
|
||||
|
||||
const mergedParams: Partial<LobeAgentConfig['params']> = {};
|
||||
if (typeof modelInfo === 'object' && modelInfo) {
|
||||
Object.assign(mergedParams, modelInfo.parameters ?? {});
|
||||
normalized.provider = normalized.provider ?? modelInfo.provider;
|
||||
normalized.model = modelInfo.model;
|
||||
} else {
|
||||
normalized.model = modelInfo;
|
||||
}
|
||||
Object.assign(mergedParams, params ?? {});
|
||||
|
||||
normalized.params = Object.keys(mergedParams).length > 0 ? mergedParams : undefined;
|
||||
|
||||
normalized.plugins = normalized.plugins ?? [];
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
const { avatar, description, tags, title, config, backgroundColor, identifier } =
|
||||
const { avatar, description, tags, title, config, backgroundColor, identifier, editorData } =
|
||||
useDetailContext();
|
||||
|
||||
const { styles } = useStyles();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const createSession = useSessionStore((s) => s.createSession);
|
||||
const sessions = useSessionStore((s) => s.sessions);
|
||||
const createAgent = useAgentStore((s) => s.createAgent);
|
||||
const refreshAgentList = useHomeStore((s) => s.refreshAgentList);
|
||||
const { message, modal } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('discover');
|
||||
@@ -82,9 +45,9 @@ const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
title,
|
||||
};
|
||||
|
||||
const checkDuplicateAgent = () => {
|
||||
const checkDuplicateAgent = async () => {
|
||||
if (!identifier) return false;
|
||||
return sessions.some((session) => session.meta?.marketIdentifier === identifier);
|
||||
return agentService.checkByMarketIdentifier(identifier);
|
||||
};
|
||||
|
||||
const showDuplicateConfirmation = (callback: () => void) => {
|
||||
@@ -97,24 +60,38 @@ const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const createSessionWithMarketIdentifier = async (isSwitchSession = true) => {
|
||||
const createAgentWithMarketIdentifier = async (shouldNavigate = true) => {
|
||||
if (!config) return;
|
||||
|
||||
const sessionData = {
|
||||
config: normalizeMarketAgentConfig(config),
|
||||
meta,
|
||||
// Note: agentService.createAgent automatically normalizes market config (handles model as object)
|
||||
const agentData = {
|
||||
config: {
|
||||
...config,
|
||||
editorData,
|
||||
...meta,
|
||||
},
|
||||
};
|
||||
|
||||
const session = await createSession(sessionData, isSwitchSession);
|
||||
return session;
|
||||
const result = await createAgent(agentData);
|
||||
await refreshAgentList();
|
||||
|
||||
// Report agent installation to marketplace if it has a market identifier
|
||||
if (identifier) {
|
||||
discoverService.reportAgentInstall(identifier);
|
||||
}
|
||||
|
||||
if (shouldNavigate) {
|
||||
console.log(shouldNavigate);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleCreateAndConverse = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const session = await createSessionWithMarketIdentifier(true);
|
||||
const result = await createAgentWithMarketIdentifier(true);
|
||||
message.success(t('assistants.addAgentSuccess'));
|
||||
navigate(SESSION_CHAT_URL(session, mobile));
|
||||
navigate(SESSION_CHAT_URL(result!.agentId || result!.sessionId, mobile));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -123,7 +100,7 @@ const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
const handleCreate = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await createSessionWithMarketIdentifier(false);
|
||||
await createAgentWithMarketIdentifier(false);
|
||||
message.success(t('assistants.addAgentSuccess'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -133,7 +110,8 @@ const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
const handleAddAgentAndConverse = async () => {
|
||||
if (!config) return;
|
||||
|
||||
if (checkDuplicateAgent()) {
|
||||
const isDuplicate = await checkDuplicateAgent();
|
||||
if (isDuplicate) {
|
||||
showDuplicateConfirmation(handleCreateAndConverse);
|
||||
} else {
|
||||
await handleCreateAndConverse();
|
||||
@@ -143,7 +121,8 @@ const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
const handleAddAgent = async () => {
|
||||
if (!config) return;
|
||||
|
||||
if (checkDuplicateAgent()) {
|
||||
const isDuplicate = await checkDuplicateAgent();
|
||||
if (isDuplicate) {
|
||||
showDuplicateConfirmation(handleCreate);
|
||||
} else {
|
||||
await handleCreate();
|
||||
+1
-1
@@ -21,7 +21,7 @@ const ActionButton = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
desc: description,
|
||||
hashtags: tags,
|
||||
title: title,
|
||||
url: urlJoin(OFFICIAL_URL, '/discover/assistant', identifier as string),
|
||||
url: urlJoin(OFFICIAL_URL, '/community/assistant', identifier as string),
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
+1
-1
@@ -29,7 +29,7 @@ const RelatedItem = memo<DiscoverAssistantItem>(({ avatar, title, description, i
|
||||
const { styles } = useStyles();
|
||||
return (
|
||||
<Block gap={12} horizontal key={identifier} padding={12} variant={'outlined'}>
|
||||
<Avatar avatar={avatar} size={40} style={{ flex: 'none' }} />
|
||||
<Avatar avatar={avatar} shape={'square'} size={40} style={{ flex: 'none' }} />
|
||||
<Flexbox
|
||||
flex={1}
|
||||
gap={6}
|
||||
+2
-2
@@ -27,7 +27,7 @@ const Related = memo(() => {
|
||||
category,
|
||||
source: marketSource,
|
||||
},
|
||||
url: '/discover/assistant',
|
||||
url: '/community/assistant',
|
||||
},
|
||||
{ skipNull: true },
|
||||
)}
|
||||
@@ -39,7 +39,7 @@ const Related = memo(() => {
|
||||
const link = qs.stringifyUrl(
|
||||
{
|
||||
query: marketSource ? { source: marketSource } : undefined,
|
||||
url: urlJoin('/discover/assistant', item.identifier),
|
||||
url: urlJoin('/community/assistant', item.identifier),
|
||||
},
|
||||
{ skipNull: true },
|
||||
);
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useToc } from '@/app/[variants]/(main)/discover/(detail)/features/Toc/useToc';
|
||||
import { useToc } from '@/app/[variants]/(main)/community/(detail)/features/Toc/useToc';
|
||||
import { useQuery } from '@/hooks/useQuery';
|
||||
import { AssistantNavKey } from '@/types/discover';
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ const Sidebar = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
maxHeight: 'calc(100vh - 76px)',
|
||||
paddingBottom: 24,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
top: 16,
|
||||
}}
|
||||
width={360}
|
||||
>
|
||||
+26
-25
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
FolderOpenOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { ExclamationCircleOutlined, FolderOpenOutlined } from '@ant-design/icons';
|
||||
import { FluentEmoji, Text } from '@lobehub/ui';
|
||||
import { Button, Result } from 'antd';
|
||||
import { memo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
@@ -19,7 +16,7 @@ const StatusPage = memo<StatusPageProps>(({ status }) => {
|
||||
const { t } = useTranslation('discover');
|
||||
|
||||
const handleBackToMarket = () => {
|
||||
navigate('/discover/assistant');
|
||||
navigate('/community/assistant');
|
||||
};
|
||||
|
||||
// 审核中状态
|
||||
@@ -37,23 +34,27 @@ const StatusPage = memo<StatusPageProps>(({ status }) => {
|
||||
>
|
||||
<Result
|
||||
extra={
|
||||
<Button onClick={handleBackToMarket} type="primary">
|
||||
<Button onClick={handleBackToMarket} size={'large'} type="primary">
|
||||
{t('assistants.status.backToMarket')}
|
||||
</Button>
|
||||
}
|
||||
icon={<ClockCircleOutlined style={{ color: '#faad14' }} />}
|
||||
icon={<FluentEmoji emoji={'⌛'} size={96} type={'anim'} />}
|
||||
subTitle={
|
||||
<div style={{ color: '#666', lineHeight: 1.6 }}>
|
||||
<Trans i18nKey="assistants.status.unpublished.subtitle" ns="discover">
|
||||
当前访问的助手正在进行版本审核中,如果有疑问复制链接发送问题到{' '}
|
||||
<a href="mailto:support@lobehub.com" style={{ color: '#1890ff' }}>
|
||||
support@lobehub.com
|
||||
</a>{' '}
|
||||
进行咨询。
|
||||
</Trans>
|
||||
</div>
|
||||
<Text fontSize={16} type={'secondary'}>
|
||||
<Trans
|
||||
components={{
|
||||
email: <a href="mailto:support@lobehub.com">support@lobehub.com</a>,
|
||||
}}
|
||||
i18nKey="assistants.status.unpublished.subtitle"
|
||||
ns="discover"
|
||||
/>
|
||||
</Text>
|
||||
}
|
||||
title={
|
||||
<Text fontSize={28} weight={'bold'}>
|
||||
{t('assistants.status.unpublished.title')}
|
||||
</Text>
|
||||
}
|
||||
title={t('assistants.status.unpublished.title')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -94,13 +95,13 @@ const StatusPage = memo<StatusPageProps>(({ status }) => {
|
||||
<li>{t(`assistants.status.${statusKey}.reasons.official`)}</li>
|
||||
</ul>
|
||||
<p>
|
||||
<Trans i18nKey="assistants.status.support" ns="discover">
|
||||
有各种问题请复制链接发送到{' '}
|
||||
<a href="mailto:support@lobehub.com" style={{ color: '#1890ff' }}>
|
||||
support@lobehub.com
|
||||
</a>{' '}
|
||||
进行咨询。
|
||||
</Trans>
|
||||
<Trans
|
||||
components={{
|
||||
email: <a href="mailto:support@lobehub.com">support@lobehub.com</a>,
|
||||
}}
|
||||
i18nKey="assistants.status.support"
|
||||
ns="discover"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
+6
-13
@@ -2,15 +2,13 @@
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useLoaderData } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import type { SlugParams } from '@/app/[variants]/loaders/routeParams';
|
||||
import { useQuery } from '@/hooks/useQuery';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
import { AssistantMarketSource, DiscoverTab } from '@/types/discover';
|
||||
import { AssistantMarketSource } from '@/types/discover';
|
||||
|
||||
import NotFound from '../components/NotFound';
|
||||
import Breadcrumb from '../features/Breadcrumb';
|
||||
import { TocProvider } from '../features/Toc/useToc';
|
||||
import { DetailProvider } from './features/DetailProvider';
|
||||
import Details from './features/Details';
|
||||
@@ -23,8 +21,8 @@ interface AssistantDetailPageProps {
|
||||
}
|
||||
|
||||
const AssistantDetailPage = memo<AssistantDetailPageProps>(({ mobile }) => {
|
||||
const { slug } = useLoaderData() as SlugParams;
|
||||
const identifier = decodeURIComponent(slug);
|
||||
const params = useParams<{ slug: string }>();
|
||||
const identifier = decodeURIComponent(params.slug ?? '');
|
||||
const { version, source } = useQuery() as { source?: AssistantMarketSource; version?: string };
|
||||
|
||||
const useAssistantDetail = useDiscoverStore((s) => s.useAssistantDetail);
|
||||
@@ -42,7 +40,6 @@ const AssistantDetailPage = memo<AssistantDetailPageProps>(({ mobile }) => {
|
||||
return (
|
||||
<TocProvider>
|
||||
<DetailProvider config={data}>
|
||||
{!mobile && <Breadcrumb identifier={identifier} tab={DiscoverTab.Assistants} />}
|
||||
<Flexbox gap={16}>
|
||||
<Header mobile={mobile} />
|
||||
<Details mobile={mobile} />
|
||||
@@ -52,12 +49,8 @@ const AssistantDetailPage = memo<AssistantDetailPageProps>(({ mobile }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const DesktopDiscoverAssistantDetailPage = memo<{ mobile?: boolean }>(() => {
|
||||
return <AssistantDetailPage mobile={false} />;
|
||||
});
|
||||
|
||||
const MobileDiscoverAssistantDetailPage = memo<{ mobile?: boolean }>(() => {
|
||||
export const MobileDiscoverAssistantDetailPage = memo<{ mobile?: boolean }>(() => {
|
||||
return <AssistantDetailPage mobile={true} />;
|
||||
});
|
||||
|
||||
export { DesktopDiscoverAssistantDetailPage, MobileDiscoverAssistantDetailPage };
|
||||
export default AssistantDetailPage;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Skeleton } from 'antd';
|
||||
import { Skeleton } from '@lobehub/ui';
|
||||
import { useResponsive } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
@@ -13,7 +13,7 @@ const Loading = memo(() => {
|
||||
<Flexbox gap={24}>
|
||||
<Flexbox gap={12}>
|
||||
<Flexbox align={'center'} gap={16} horizontal width={'100%'}>
|
||||
<Skeleton.Avatar active size={mobile ? 48 : 64} />
|
||||
<Skeleton.Avatar active shape={'square'} size={mobile ? 48 : 64} />
|
||||
<Skeleton.Button active style={{ height: 36, width: 200 }} />
|
||||
</Flexbox>
|
||||
<Skeleton.Button size={'small'} style={{ width: 200 }} />
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { Button, Icon } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox, FlexboxProps } from 'react-layout-kit';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
more: css`
|
||||
@@ -33,7 +33,7 @@ const Block = memo<BlockProps>(({ title, more, moreLink, children, ...rest }) =>
|
||||
<Flexbox align={'center'} gap={16} horizontal justify={'space-between'} width={'100%'}>
|
||||
<h2 className={styles.title}>{title}</h2>
|
||||
{moreLink && (
|
||||
<Link href={moreLink} target={moreLink.startsWith('http') ? '_blank' : undefined}>
|
||||
<Link target={moreLink.startsWith('http') ? '_blank' : undefined} to={moreLink}>
|
||||
<Button className={styles.more} type={'text'}>
|
||||
<span>{more}</span>
|
||||
<Icon icon={ChevronRight} />
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import { CopyButton } from '@lobehub/ui';
|
||||
import { Breadcrumb as AntdBreadcrumb, type BreadcrumbProps } from 'antd';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { DiscoverTab } from '@/types/discover';
|
||||
|
||||
const Breadcrumb = memo<{ identifier: string; tab: DiscoverTab }>(({ tab, identifier }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation('discover');
|
||||
|
||||
const tabLabel = useMemo(() => {
|
||||
if (tab === DiscoverTab.Mcp) return 'MCP Servers';
|
||||
if (tab === DiscoverTab.User) return t('tab.user');
|
||||
return t(`tab.${tab}` as any);
|
||||
}, [tab, t]);
|
||||
|
||||
// For user tab, we don't show the middle breadcrumb as there's no user list page
|
||||
const items: BreadcrumbProps['items'] = useMemo(() => {
|
||||
if (tab === DiscoverTab.User) {
|
||||
return [
|
||||
{
|
||||
title: <Link to="/community">Community</Link>,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Flexbox
|
||||
align="center"
|
||||
gap={4}
|
||||
horizontal
|
||||
style={{
|
||||
color: theme.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
@{identifier}
|
||||
</Flexbox>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
title: <Link to="/community">Community</Link>,
|
||||
},
|
||||
{
|
||||
title: <Link to={`/community/${tab}`}>{tabLabel}</Link>,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Flexbox
|
||||
align="center"
|
||||
gap={4}
|
||||
horizontal
|
||||
style={{
|
||||
color: theme.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
{identifier}
|
||||
<CopyButton
|
||||
content={identifier}
|
||||
size={{
|
||||
blockSize: 22,
|
||||
size: 14,
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [tab, identifier, tabLabel, theme.colorTextSecondary]);
|
||||
|
||||
return <AntdBreadcrumb items={items} />;
|
||||
});
|
||||
|
||||
export default Breadcrumb;
|
||||
+8
-3
@@ -1,5 +1,5 @@
|
||||
import { Markdown } from '@lobehub/ui';
|
||||
import { Empty } from 'antd';
|
||||
import { Empty, Markdown } from '@lobehub/ui';
|
||||
import { FileText } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { ReactNode, memo } from 'react';
|
||||
import { Center } from 'react-layout-kit';
|
||||
@@ -10,7 +10,12 @@ const MarkdownRender = memo<{ children?: string }>(({ children }) => {
|
||||
if (!children)
|
||||
return (
|
||||
<Center paddingBlock={32} width={'100%'}>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
<Empty
|
||||
description="暂无内容"
|
||||
descriptionProps={{ fontSize: 14 }}
|
||||
icon={FileText}
|
||||
style={{ maxWidth: 400 }}
|
||||
/>
|
||||
</Center>
|
||||
);
|
||||
|
||||
+3
-3
@@ -6,12 +6,12 @@ import {
|
||||
CopyButton,
|
||||
Input,
|
||||
Modal,
|
||||
Skeleton,
|
||||
Tag,
|
||||
Text,
|
||||
} from '@lobehub/ui';
|
||||
import { Skeleton } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { startCase } from 'lodash-es';
|
||||
import { startCase } from 'es-toolkit/compat';
|
||||
import { LinkIcon, Share2Icon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { ReactNode, memo, useState } from 'react';
|
||||
@@ -100,7 +100,7 @@ const ShareButton = memo<ShareButtonProps>(({ meta, ...rest }) => {
|
||||
}}
|
||||
width={72}
|
||||
>
|
||||
<Avatar animation avatar={meta.avatar} shape={'circle'} size={64} />
|
||||
<Avatar animation avatar={meta.avatar} shape={'square'} size={64} />
|
||||
</Center>
|
||||
<Center padding={12} width={'100%'}>
|
||||
<h3 style={{ fontWeight: 'bold', textAlign: 'center' }}>{meta.title}</h3>
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { createStyles, useTheme } from 'antd-style';
|
||||
import { kebabCase } from 'lodash-es';
|
||||
import { kebabCase } from 'es-toolkit/compat';
|
||||
import { Heading2, Heading3, Heading4, Heading5 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Children, ComponentProps, FC, ReactNode, isValidElement, useEffect, useMemo } from 'react';
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { Anchor, AnchorProps } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/discover/features/const';
|
||||
import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/community/features/const';
|
||||
import { isOnServerSide } from '@/utils/env';
|
||||
|
||||
import { createTOCTree } from './useToc';
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { AnchorProps } from 'antd';
|
||||
import { unionBy } from 'lodash-es';
|
||||
import { unionBy } from 'es-toolkit/compat';
|
||||
import { FC, PropsWithChildren, createContext, useContext, useState } from 'react';
|
||||
|
||||
interface TocState {
|
||||
+1
-1
@@ -19,7 +19,7 @@ const Related = memo(() => {
|
||||
query: {
|
||||
category,
|
||||
},
|
||||
url: '/discover/mcp',
|
||||
url: '/community/mcp',
|
||||
})}
|
||||
>
|
||||
{t('mcp.details.related.listTitle')}
|
||||
+2
@@ -62,6 +62,8 @@ const ActionButton = memo(() => {
|
||||
|
||||
const buttonLoading = isLoading || isAuthLoading;
|
||||
|
||||
console.log('installed', isLoading, isAuthLoading, installed);
|
||||
|
||||
return installed ? (
|
||||
<Flexbox gap={8} horizontal>
|
||||
<Button
|
||||
+1
-1
@@ -29,7 +29,7 @@ const RelatedItem = memo<DiscoverMcpItem>(({ name, icon, description, identifier
|
||||
const { styles } = useStyles();
|
||||
return (
|
||||
<Block gap={12} horizontal key={identifier} padding={12} variant={'outlined'}>
|
||||
<Avatar avatar={icon} size={40} style={{ flex: 'none' }} />
|
||||
<Avatar avatar={icon} shape={'square'} size={40} style={{ flex: 'none' }} />
|
||||
<Flexbox
|
||||
flex={1}
|
||||
gap={6}
|
||||
+2
-2
@@ -22,14 +22,14 @@ const Related = memo(() => {
|
||||
query: {
|
||||
category,
|
||||
},
|
||||
url: '/discover/mcp',
|
||||
url: '/community/mcp',
|
||||
})}
|
||||
>
|
||||
{t('mcp.details.related.listTitle')}
|
||||
</Title>
|
||||
<Flexbox gap={8}>
|
||||
{related?.map((item, index) => {
|
||||
const link = urlJoin('/discover/mcp', item.identifier);
|
||||
const link = urlJoin('/community/mcp', item.identifier);
|
||||
return (
|
||||
<Link key={index} style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
|
||||
<Item {...item} />
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { AnchorProps } from 'antd';
|
||||
import { startCase } from 'lodash-es';
|
||||
import { startCase } from 'es-toolkit/compat';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useToc } from '@/app/[variants]/(main)/discover/(detail)/features/Toc/useToc';
|
||||
import { useToc } from '@/app/[variants]/(main)/community/(detail)/features/Toc/useToc';
|
||||
import { useDetailContext } from '@/features/MCPPluginDetail/DetailProvider';
|
||||
import { useQuery } from '@/hooks/useQuery';
|
||||
import { McpNavKey } from '@/types/discover';
|
||||
+1
-1
@@ -35,7 +35,7 @@ const Sidebar = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
maxHeight: 'calc(100vh - 76px)',
|
||||
paddingBottom: 24,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
top: 16,
|
||||
}}
|
||||
width={360}
|
||||
>
|
||||
+5
-13
@@ -2,18 +2,15 @@
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useLoaderData } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import type { SlugParams } from '@/app/[variants]/loaders/routeParams';
|
||||
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 NotFound from '../components/NotFound';
|
||||
import Breadcrumb from '../features/Breadcrumb';
|
||||
import { TocProvider } from '../features/Toc/useToc';
|
||||
import Details from './features/Details';
|
||||
import Loading from './loading';
|
||||
@@ -23,8 +20,8 @@ interface McpDetailPageProps {
|
||||
}
|
||||
|
||||
const McpDetailPage = memo<McpDetailPageProps>(({ mobile }) => {
|
||||
const { slug } = useLoaderData() as SlugParams;
|
||||
const identifier = slug;
|
||||
const params = useParams<{ slug: string }>();
|
||||
const identifier = params.slug ?? '';
|
||||
|
||||
const { version } = useQuery() as { version?: string };
|
||||
const useMcpDetail = useDiscoverStore((s) => s.useFetchMcpDetail);
|
||||
@@ -38,7 +35,6 @@ const McpDetailPage = memo<McpDetailPageProps>(({ mobile }) => {
|
||||
return (
|
||||
<TocProvider>
|
||||
<DetailProvider config={data}>
|
||||
{!mobile && <Breadcrumb identifier={identifier} tab={DiscoverTab.Mcp} />}
|
||||
<Flexbox gap={16}>
|
||||
<Header mobile={mobile} />
|
||||
<Details mobile={mobile} />
|
||||
@@ -48,12 +44,8 @@ const McpDetailPage = memo<McpDetailPageProps>(({ mobile }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const DesktopMcpPage = memo<{ mobile?: boolean }>(() => {
|
||||
return <McpDetailPage mobile={false} />;
|
||||
});
|
||||
|
||||
const MobileMcpPage = memo<{ mobile?: boolean }>(() => {
|
||||
export const MobileMcpPage = memo<{ mobile?: boolean }>(() => {
|
||||
return <McpDetailPage mobile={true} />;
|
||||
});
|
||||
|
||||
export { DesktopMcpPage, MobileMcpPage };
|
||||
export default McpDetailPage;
|
||||
+5
-8
@@ -4,10 +4,10 @@ import { ProviderIcon } from '@lobehub/icons';
|
||||
import { ActionIcon, Block, Icon, Tooltip } from '@lobehub/ui';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { BadgeCheck, BookIcon, ChevronRightIcon, KeyIcon } from 'lucide-react';
|
||||
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 InlineTable from '@/components/InlineTable';
|
||||
@@ -32,7 +32,7 @@ const ProviderList = memo(() => {
|
||||
key: 'provider',
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Link href={urlJoin('/discover/provider', record.id)} style={{ color: 'inherit' }}>
|
||||
<Link style={{ color: 'inherit' }} to={urlJoin('/community/provider', record.id)}>
|
||||
<Flexbox align="center" gap={8} horizontal>
|
||||
<ProviderIcon provider={record.id} size={24} type={'avatar'} />
|
||||
<div style={{ fontWeight: 500 }}>{record.name}</div>
|
||||
@@ -157,14 +157,11 @@ const ProviderList = memo(() => {
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('models.guide')}>
|
||||
<Link href={urlJoin(BASE_PROVIDER_DOC_URL, record.id)} target={'_blank'}>
|
||||
<a href={urlJoin(BASE_PROVIDER_DOC_URL, record.id)} rel="noreferrer" target={'_blank'}>
|
||||
<ActionIcon icon={BookIcon} size={'small'} variant={'filled'} />
|
||||
</Link>
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Link
|
||||
href={urlJoin('/discover/provider', record.id)}
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
<Link style={{ color: 'inherit' }} to={urlJoin('/community/provider', record.id)}>
|
||||
<ActionIcon
|
||||
color={theme.colorTextDescription}
|
||||
icon={ChevronRightIcon}
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import Title from '@/app/[variants]/(main)/discover/features/Title';
|
||||
import Title from '@/app/[variants]/(main)/community/features/Title';
|
||||
import { formatTokenNumber } from '@/utils/format';
|
||||
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
+1
-1
@@ -18,7 +18,7 @@ const Related = memo(() => {
|
||||
query: {
|
||||
category,
|
||||
},
|
||||
url: '/discover/plugin',
|
||||
url: '/community/plugin',
|
||||
})}
|
||||
>
|
||||
{t('assistants.details.related.listTitle')}
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import ModelTypeIcon from '@/app/[variants]/(main)/discover/(list)/model/features/List/ModelTypeIcon';
|
||||
import ModelTypeIcon from '@/app/[variants]/(main)/community/(list)/model/features/List/ModelTypeIcon';
|
||||
import { ModelInfoTags } from '@/components/ModelSelect';
|
||||
|
||||
import PublishedTime from '../../../../../../../components/PublishedTime';
|
||||
+4
-5
@@ -5,10 +5,9 @@ import { Button, Icon } from '@lobehub/ui';
|
||||
import { Dropdown } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Link , useNavigate } from 'react-router-dom';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
@@ -33,14 +32,14 @@ const ChatWithModel = memo(() => {
|
||||
icon: <ProviderIcon provider={item.id} size={20} type={'avatar'} />,
|
||||
key: item.id,
|
||||
label: (
|
||||
<Link href={urlJoin('/discover/provider', item.id)}>
|
||||
<Link to={urlJoin('/community/provider', item.id)}>
|
||||
{[item.name, t('models.guide')].join(' ')}
|
||||
</Link>
|
||||
),
|
||||
}));
|
||||
|
||||
const handleLobeHubChat = () => {
|
||||
navigate('/chat');
|
||||
navigate('/agent');
|
||||
};
|
||||
|
||||
if (includeLobeHub)
|
||||
@@ -63,7 +62,7 @@ const ChatWithModel = memo(() => {
|
||||
|
||||
if (items.length === 1)
|
||||
return (
|
||||
<Link href={urlJoin('/discover/provider', items[0].key)} style={{ flex: 1 }}>
|
||||
<Link style={{ flex: 1 }} to={urlJoin('/community/provider', items[0].key)}>
|
||||
<Button block className={styles.button} size={'large'} type={'primary'}>
|
||||
{t('models.guide')}
|
||||
</Button>
|
||||
+1
-1
@@ -22,7 +22,7 @@ const ActionButton = memo(() => {
|
||||
desc: description,
|
||||
hashtags: providers?.map((item) => item.name) || [],
|
||||
title: displayName || identifier,
|
||||
url: urlJoin(OFFICIAL_URL, '/discover/model', identifier as string),
|
||||
url: urlJoin(OFFICIAL_URL, '/community/model', identifier as string),
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
+2
-2
@@ -21,14 +21,14 @@ const Related = memo(() => {
|
||||
query: {
|
||||
category,
|
||||
},
|
||||
url: '/discover/model',
|
||||
url: '/community/model',
|
||||
})}
|
||||
>
|
||||
{t('models.details.related.listTitle')}
|
||||
</Title>
|
||||
<Flexbox gap={8}>
|
||||
{related?.map((item, index) => {
|
||||
const link = urlJoin('/discover/model', item.identifier);
|
||||
const link = urlJoin('/community/model', item.identifier);
|
||||
return (
|
||||
<Link key={index} style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
|
||||
<Item {...item} />
|
||||
+4
-4
@@ -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';
|
||||
@@ -14,14 +14,14 @@ const Related = memo(() => {
|
||||
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
<Title more={t('providers.details.related.more')} moreLink={'/discover/provider'}>
|
||||
<Title more={t('providers.details.related.more')} moreLink={'/community/provider'}>
|
||||
{t('providers.details.related.listTitle')}
|
||||
</Title>
|
||||
<Flexbox gap={8}>
|
||||
{providers.slice(0, 6).map((item, index) => {
|
||||
const link = urlJoin('/discover/provider', item.id);
|
||||
const link = urlJoin('/community/provider', item.id);
|
||||
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
-1
@@ -30,7 +30,7 @@ const Sidebar = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
maxHeight: 'calc(100vh - 76px)',
|
||||
paddingBottom: 24,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
top: 16,
|
||||
}}
|
||||
width={360}
|
||||
>
|
||||
+5
-13
@@ -2,14 +2,11 @@
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useLoaderData } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import type { SlugParams } from '@/app/[variants]/loaders/routeParams';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
import { DiscoverTab } from '@/types/discover';
|
||||
|
||||
import NotFound from '../components/NotFound';
|
||||
import Breadcrumb from '../features/Breadcrumb';
|
||||
import { DetailProvider } from './features/DetailProvider';
|
||||
import Details from './features/Details';
|
||||
import Header from './features/Header';
|
||||
@@ -20,8 +17,8 @@ interface ModelDetailPageProps {
|
||||
}
|
||||
|
||||
const ModelDetailPage = memo<ModelDetailPageProps>(({ mobile }) => {
|
||||
const { slug } = useLoaderData() as SlugParams;
|
||||
const identifier = decodeURIComponent(slug);
|
||||
const params = useParams<{ slug: string }>();
|
||||
const identifier = decodeURIComponent(params.slug ?? '');
|
||||
|
||||
const useModelDetail = useDiscoverStore((s) => s.useModelDetail);
|
||||
const { data, isLoading } = useModelDetail({ identifier });
|
||||
@@ -31,7 +28,6 @@ const ModelDetailPage = memo<ModelDetailPageProps>(({ mobile }) => {
|
||||
|
||||
return (
|
||||
<DetailProvider config={data}>
|
||||
{!mobile && <Breadcrumb identifier={identifier} tab={DiscoverTab.Models} />}
|
||||
<Flexbox gap={16}>
|
||||
<Header mobile={mobile} />
|
||||
<Details mobile={mobile} />
|
||||
@@ -40,12 +36,8 @@ const ModelDetailPage = memo<ModelDetailPageProps>(({ mobile }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const DesktopModelPage = memo<{ mobile?: boolean }>(() => {
|
||||
return <ModelDetailPage mobile={false} />;
|
||||
});
|
||||
|
||||
const MobileModelPage = memo<{ mobile?: boolean }>(() => {
|
||||
export const MobileModelPage = memo<{ mobile?: boolean }>(() => {
|
||||
return <ModelDetailPage mobile={true} />;
|
||||
});
|
||||
|
||||
export { DesktopModelPage, MobileModelPage };
|
||||
export default ModelDetailPage;
|
||||
+10
-3
@@ -1,17 +1,24 @@
|
||||
import { Block } from '@lobehub/ui';
|
||||
import { Block, Empty } from '@lobehub/ui';
|
||||
import { Mdx } from '@lobehub/ui/mdx';
|
||||
import { Empty } from 'antd';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
|
||||
const Guide = memo(() => {
|
||||
const { t } = useTranslation('discover');
|
||||
const { readme = '' } = useDetailContext();
|
||||
|
||||
if (!readme)
|
||||
return (
|
||||
<Block variant={'outlined'}>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
<Empty
|
||||
description={t('providers.details.guide.title')}
|
||||
descriptionProps={{ fontSize: 14 }}
|
||||
icon={BookOpen}
|
||||
style={{ maxWidth: 400 }}
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
|
||||
+3
-3
@@ -4,10 +4,10 @@ import { ModelIcon } from '@lobehub/icons';
|
||||
import { ActionIcon, Block, Tooltip } from '@lobehub/ui';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { ChevronRightIcon } from 'lucide-react';
|
||||
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 InlineTable from '@/components/InlineTable';
|
||||
@@ -31,7 +31,7 @@ const ModelList = memo(() => {
|
||||
key: 'model',
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Link href={urlJoin('/discover/model', record.id)} style={{ color: 'inherit' }}>
|
||||
<Link style={{ color: 'inherit' }} to={urlJoin('/community/model', record.id)}>
|
||||
<Flexbox align="center" gap={8} horizontal>
|
||||
<ModelIcon model={record.id} size={24} type={'avatar'} />
|
||||
<Flexbox style={{ overflow: 'hidden' }}>
|
||||
@@ -129,7 +129,7 @@ const ModelList = memo(() => {
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Flexbox align="center" gap={4} horizontal justify={'flex-end'}>
|
||||
<Link href={urlJoin('/discover/model', record.id)} style={{ color: 'inherit' }}>
|
||||
<Link style={{ color: 'inherit' }} to={urlJoin('/community/model', record.id)}>
|
||||
<ActionIcon
|
||||
color={theme.colorTextDescription}
|
||||
icon={ChevronRightIcon}
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import Title from '@/app/[variants]/(main)/discover/features/Title';
|
||||
import Title from '@/app/[variants]/(main)/community/features/Title';
|
||||
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
import ModelList from './ModelList';
|
||||
+1
-1
@@ -11,7 +11,7 @@ const Related = memo(() => {
|
||||
const { related } = useDetailContext();
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
<Title more={t('assistants.details.related.more')} moreLink={'/discover/provider'}>
|
||||
<Title more={t('assistants.details.related.more')} moreLink={'/community/provider'}>
|
||||
{t('assistants.details.related.listTitle')}
|
||||
</Title>
|
||||
<List data={related} rows={2} />
|
||||
+5
-12
@@ -5,10 +5,9 @@ import { Button, Icon } from '@lobehub/ui';
|
||||
import { Dropdown } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { ChevronDownIcon, SquareArrowOutUpRight } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
|
||||
@@ -26,20 +25,14 @@ const ProviderConfig = memo(() => {
|
||||
const { url, modelsUrl, identifier } = useDetailContext();
|
||||
const navigate = useNavigate();
|
||||
const openSettings = async () => {
|
||||
const searchParams = { active: 'provider', provider: identifier };
|
||||
const tab = 'provider';
|
||||
|
||||
if (isDesktop) {
|
||||
const { ensureElectronIpc } = await import('@/utils/electron/ipc');
|
||||
await ensureElectronIpc().windows.openSettingsWindow({
|
||||
searchParams,
|
||||
tab,
|
||||
path: `/settings/provider/${identifier}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
navigate(
|
||||
`/settings?active=provider&provider=${identifier}`
|
||||
)
|
||||
navigate(`/settings/provider/${identifier}`);
|
||||
};
|
||||
|
||||
const icon = <Icon icon={SquareArrowOutUpRight} size={16} />;
|
||||
@@ -49,7 +42,7 @@ const ProviderConfig = memo(() => {
|
||||
icon,
|
||||
key: 'officialSite',
|
||||
label: (
|
||||
<Link href={url} target={'_blank'}>
|
||||
<Link target={'_blank'} to={url}>
|
||||
{t('providers.officialSite')}
|
||||
</Link>
|
||||
),
|
||||
@@ -58,7 +51,7 @@ const ProviderConfig = memo(() => {
|
||||
icon,
|
||||
key: 'modelSite',
|
||||
label: (
|
||||
<Link href={modelsUrl} target={'_blank'}>
|
||||
<Link target={'_blank'} to={modelsUrl}>
|
||||
{t('providers.modelSite')}
|
||||
</Link>
|
||||
),
|
||||
+1
-1
@@ -35,7 +35,7 @@ const ActionButton = memo(() => {
|
||||
</Flexbox>
|
||||
),
|
||||
title: name,
|
||||
url: urlJoin(OFFICIAL_URL, '/discover/provider', identifier as string),
|
||||
url: urlJoin(OFFICIAL_URL, '/community/provider', identifier as string),
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
+2
-2
@@ -14,12 +14,12 @@ const Related = memo(() => {
|
||||
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
<Title more={t('providers.details.related.more')} moreLink={'/discover/provider'}>
|
||||
<Title more={t('providers.details.related.more')} moreLink={'/community/provider'}>
|
||||
{t('providers.details.related.listTitle')}
|
||||
</Title>
|
||||
<Flexbox gap={8}>
|
||||
{related?.map((item, index) => {
|
||||
const link = urlJoin('/discover/provider', item.identifier);
|
||||
const link = urlJoin('/community/provider', item.identifier);
|
||||
return (
|
||||
<Link key={index} style={{ color: 'inherit', overflow: 'hidden' }} to={link}>
|
||||
<Item {...item} />
|
||||
+4
-4
@@ -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';
|
||||
@@ -21,16 +21,16 @@ const Related = memo(() => {
|
||||
query: {
|
||||
category: identifier,
|
||||
},
|
||||
url: '/discover/model',
|
||||
url: '/community/model',
|
||||
})}
|
||||
>
|
||||
{t('models.details.related.listTitle')}
|
||||
</Title>
|
||||
<Flexbox gap={8}>
|
||||
{models?.slice(0, 6)?.map((item, index) => {
|
||||
const link = urlJoin('/discover/model', item.id);
|
||||
const link = urlJoin('/community/model', item.id);
|
||||
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
-1
@@ -30,7 +30,7 @@ const Sidebar = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
maxHeight: 'calc(100vh - 76px)',
|
||||
paddingBottom: 24,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
top: 16,
|
||||
}}
|
||||
width={360}
|
||||
>
|
||||
+5
-13
@@ -2,14 +2,11 @@
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useLoaderData } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import type { SlugParams } from '@/app/[variants]/loaders/routeParams';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
import { DiscoverTab } from '@/types/discover';
|
||||
|
||||
import NotFound from '../components/NotFound';
|
||||
import Breadcrumb from '../features/Breadcrumb';
|
||||
import { DetailProvider } from './features/DetailProvider';
|
||||
import Details from './features/Details';
|
||||
import Header from './features/Header';
|
||||
@@ -20,8 +17,8 @@ interface ProviderDetailPageProps {
|
||||
}
|
||||
|
||||
const ProviderDetailPage = memo<ProviderDetailPageProps>(({ mobile }) => {
|
||||
const { slug } = useLoaderData() as SlugParams;
|
||||
const identifier = decodeURIComponent(slug);
|
||||
const params = useParams<{ slug: string }>();
|
||||
const identifier = decodeURIComponent(params.slug ?? '');
|
||||
|
||||
const useProviderDetail = useDiscoverStore((s) => s.useProviderDetail);
|
||||
const { data, isLoading } = useProviderDetail({ identifier, withReadme: true });
|
||||
@@ -31,7 +28,6 @@ const ProviderDetailPage = memo<ProviderDetailPageProps>(({ mobile }) => {
|
||||
|
||||
return (
|
||||
<DetailProvider config={data}>
|
||||
{!mobile && <Breadcrumb identifier={identifier} tab={DiscoverTab.Providers} />}
|
||||
<Flexbox gap={16}>
|
||||
<Header mobile={mobile} />
|
||||
<Details mobile={mobile} />
|
||||
@@ -40,12 +36,8 @@ const ProviderDetailPage = memo<ProviderDetailPageProps>(({ mobile }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const DesktopProviderPage = memo<{ mobile?: boolean }>(() => {
|
||||
return <ProviderDetailPage mobile={false} />;
|
||||
});
|
||||
|
||||
const MobileProviderPage = memo<{ mobile?: boolean }>(() => {
|
||||
export const MobileProviderPage = memo<{ mobile?: boolean }>(() => {
|
||||
return <ProviderDetailPage mobile={true} />;
|
||||
});
|
||||
|
||||
export { DesktopProviderPage, MobileProviderPage };
|
||||
export default ProviderDetailPage;
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { type ReactNode, createContext, memo, use } from 'react';
|
||||
|
||||
import { MarketUserProfile } from '@/layout/AuthProvider/MarketAuth/types';
|
||||
import { DiscoverAssistantItem, DiscoverUserInfo } from '@/types/discover';
|
||||
|
||||
export interface UserDetailContextConfig {
|
||||
agentCount: number;
|
||||
agents: DiscoverAssistantItem[];
|
||||
isOwner: boolean;
|
||||
mobile?: boolean;
|
||||
onEditProfile?: (onSuccess?: (profile: MarketUserProfile) => void) => void;
|
||||
onStatusChange?: (identifier: string, action: 'publish' | 'unpublish' | 'deprecate') => void;
|
||||
totalInstalls: number;
|
||||
user: DiscoverUserInfo;
|
||||
}
|
||||
|
||||
export const UserDetailContext = createContext<UserDetailContextConfig | null>(null);
|
||||
|
||||
export const UserDetailProvider = memo<{ children: ReactNode; config: UserDetailContextConfig }>(
|
||||
({ children, config }) => {
|
||||
return <UserDetailContext value={config}>{children}</UserDetailContext>;
|
||||
},
|
||||
);
|
||||
|
||||
export const useUserDetailContext = () => {
|
||||
const context = use(UserDetailContext);
|
||||
if (!context) {
|
||||
throw new Error('useUserDetailContext must be used within UserDetailProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { Center } from 'react-layout-kit';
|
||||
|
||||
const useStyles = createStyles(({ css, token, responsive }) => ({
|
||||
banner: css`
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
padding: 16px;
|
||||
|
||||
${responsive.mobile} {
|
||||
position: relative;
|
||||
|
||||
width: calc(100% + 32px);
|
||||
height: 120px;
|
||||
margin-block: -16px 0;
|
||||
margin-inline: -16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1720px) {
|
||||
height: 144px;
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
bannerAvatar: css`
|
||||
filter: blur(100px);
|
||||
`,
|
||||
bannerInner: css`
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
|
||||
background: ${token.colorFillTertiary};
|
||||
|
||||
@media (max-width: 1720px) {
|
||||
border-radius: 0;
|
||||
}
|
||||
`,
|
||||
button: css`
|
||||
position: absolute;
|
||||
inset-block-end: -16px;
|
||||
inset-inline-end: 16px;
|
||||
`,
|
||||
placeholder: css`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
min-height: 64px;
|
||||
|
||||
${responsive.mobile} {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
interface BannerProps {
|
||||
avatar?: string | null;
|
||||
bannerUrl?: string | null;
|
||||
}
|
||||
|
||||
const Banner = memo<BannerProps>(({ avatar, bannerUrl }) => {
|
||||
const { styles } = useStyles();
|
||||
// Use bannerUrl if available, otherwise fall back to blurred avatar
|
||||
const backgroundImage = bannerUrl || avatar;
|
||||
const shouldBlur = !bannerUrl && !!avatar;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.banner}>
|
||||
<Center className={styles.bannerInner}>
|
||||
{backgroundImage && (
|
||||
<div
|
||||
className={shouldBlur ? styles.bannerAvatar : undefined}
|
||||
style={{
|
||||
backgroundImage: `url(${backgroundImage})`,
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: 'cover',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Center>
|
||||
</div>
|
||||
<div className={styles.placeholder} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Banner;
|
||||
@@ -0,0 +1,92 @@
|
||||
'use client';
|
||||
|
||||
import { SiGithub, SiX } from '@icons-pack/react-simple-icons';
|
||||
import { ActionIcon, Avatar, Text, Tooltip } from '@lobehub/ui';
|
||||
import { Button } from 'antd';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { Globe } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useUserDetailContext } from '../DetailProvider';
|
||||
import Banner from './Banner';
|
||||
|
||||
const UserHeader = memo(() => {
|
||||
const { t } = useTranslation('discover');
|
||||
const theme = useTheme();
|
||||
const { user, isOwner, onEditProfile } = useUserDetailContext();
|
||||
|
||||
const displayName = user.displayName || user.userName || user.namespace;
|
||||
const username = user.userName || user.namespace;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Banner avatar={user?.avatarUrl} bannerUrl={user?.bannerUrl} />
|
||||
<Flexbox gap={16}>
|
||||
<Avatar
|
||||
avatar={user.avatarUrl || undefined}
|
||||
shape={'square'}
|
||||
size={64}
|
||||
style={{ boxShadow: `0 0 0 4px ${theme.colorBgContainer}`, flexShrink: 0 }}
|
||||
/>
|
||||
<Flexbox align={'flex-start'} gap={16} horizontal justify={'space-between'}>
|
||||
<Flexbox
|
||||
gap={4}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Text as={'h1'} ellipsis fontSize={24} style={{ margin: 0 }} weight={'bold'}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text ellipsis fontSize={12} type={'secondary'}>
|
||||
@{username}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
{isOwner && onEditProfile && (
|
||||
<Button onClick={() => onEditProfile()} shape={'round'}>
|
||||
{t('user.editProfile')}
|
||||
</Button>
|
||||
)}
|
||||
</Flexbox>
|
||||
|
||||
{user.description && <Text as={'p'}>{user.description}</Text>}
|
||||
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
{user.socialLinks?.github && (
|
||||
<Tooltip title="GitHub">
|
||||
<a
|
||||
href={`https://github.com/${user?.socialLinks?.github}`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<ActionIcon icon={<SiGithub size={16} />} size={20} variant={'outlined'} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
{user.socialLinks?.twitter && (
|
||||
<Tooltip title="Twitter">
|
||||
<a
|
||||
href={`https://twitter.com/${user?.socialLinks?.twitter}`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<ActionIcon icon={<SiX size={16} />} size={20} variant={'outlined'} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
{user.socialLinks?.website && (
|
||||
<Tooltip title={t('user.website')}>
|
||||
<a href={user?.socialLinks?.website} rel="noopener noreferrer" target="_blank">
|
||||
<ActionIcon icon={Globe} size={20} variant={'outlined'} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default UserHeader;
|
||||
@@ -0,0 +1,372 @@
|
||||
'use client';
|
||||
|
||||
import { Avatar, Block, Icon, Tag, Text, Tooltip } from '@lobehub/ui';
|
||||
import { Tag as AntTag, App, Dropdown } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import {
|
||||
AlertTriangle,
|
||||
ClockIcon,
|
||||
CoinsIcon,
|
||||
DownloadIcon,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
EyeOff,
|
||||
MoreVerticalIcon,
|
||||
Pencil,
|
||||
} from 'lucide-react';
|
||||
import qs from 'query-string';
|
||||
import { memo, useCallback, useState } 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';
|
||||
import { agentService } from '@/services/agent';
|
||||
import { discoverService } from '@/services/discover';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { useHomeStore } from '@/store/home';
|
||||
import { AgentStatus, DiscoverAssistantItem } from '@/types/discover';
|
||||
import { formatIntergerNumber } from '@/utils/format';
|
||||
|
||||
import { useUserDetailContext } from './DetailProvider';
|
||||
|
||||
const getStatusTagColor = (status?: AgentStatus) => {
|
||||
switch (status) {
|
||||
case 'published': {
|
||||
return 'green';
|
||||
}
|
||||
case 'unpublished': {
|
||||
return 'orange';
|
||||
}
|
||||
case 'deprecated': {
|
||||
return 'red';
|
||||
}
|
||||
case 'archived': {
|
||||
return 'default';
|
||||
}
|
||||
default: {
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => {
|
||||
return {
|
||||
author: css`
|
||||
color: ${token.colorTextDescription};
|
||||
`,
|
||||
desc: css`
|
||||
flex: 1;
|
||||
margin: 0 !important;
|
||||
color: ${token.colorTextSecondary};
|
||||
`,
|
||||
footer: css`
|
||||
margin-block-start: 16px;
|
||||
border-block-start: 1px dashed ${token.colorBorder};
|
||||
background: ${token.colorBgContainerSecondary};
|
||||
`,
|
||||
moreButton: css`
|
||||
position: absolute;
|
||||
inset-block-start: 12px;
|
||||
inset-inline-end: 12px;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 0.2s;
|
||||
`,
|
||||
secondaryDesc: css`
|
||||
font-size: 12px;
|
||||
color: ${token.colorTextDescription};
|
||||
`,
|
||||
statTag: css`
|
||||
border-radius: 4px;
|
||||
|
||||
font-family: ${token.fontFamilyCode};
|
||||
font-size: 11px;
|
||||
color: ${token.colorTextSecondary};
|
||||
|
||||
background: ${token.colorFillTertiary};
|
||||
`,
|
||||
title: css`
|
||||
margin: 0 !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 500 !important;
|
||||
|
||||
&:hover {
|
||||
color: ${token.colorLink};
|
||||
}
|
||||
`,
|
||||
wrapper: css`
|
||||
&:hover .more-button {
|
||||
opacity: 1;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
type UserAgentCardProps = DiscoverAssistantItem;
|
||||
|
||||
const UserAgentCard = memo<UserAgentCardProps>(
|
||||
({
|
||||
avatar,
|
||||
backgroundColor,
|
||||
title,
|
||||
description,
|
||||
createdAt,
|
||||
category,
|
||||
tokenUsage,
|
||||
installCount,
|
||||
status,
|
||||
identifier,
|
||||
}) => {
|
||||
const { styles } = useStyles();
|
||||
const { t } = useTranslation(['discover', 'setting']);
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const { isOwner, onStatusChange } = useUserDetailContext();
|
||||
|
||||
const [, setIsEditLoading] = useState(false);
|
||||
const createAgent = useAgentStore((s) => s.createAgent);
|
||||
const refreshAgentList = useHomeStore((s) => s.refreshAgentList);
|
||||
|
||||
const link = qs.stringifyUrl(
|
||||
{
|
||||
query: { source: 'new' },
|
||||
url: urlJoin('/community/assistant', identifier),
|
||||
},
|
||||
{ skipNull: true },
|
||||
);
|
||||
|
||||
const isPublished = status === 'published';
|
||||
|
||||
const handleViewDetail = useCallback(() => {
|
||||
window.open(urlJoin('/community/assistant', identifier), '_blank');
|
||||
}, [identifier]);
|
||||
|
||||
const handleEdit = useCallback(async () => {
|
||||
setIsEditLoading(true);
|
||||
try {
|
||||
// First, try to find the local agent by market identifier
|
||||
const localAgentId = await agentService.getAgentByMarketIdentifier(identifier);
|
||||
|
||||
if (localAgentId) {
|
||||
// Agent exists locally, navigate to edit
|
||||
navigate(urlJoin('/agent', localAgentId, 'profile'));
|
||||
} else {
|
||||
// Agent doesn't exist locally, fetch from market and create
|
||||
const marketAgent = await discoverService.getAssistantDetail({
|
||||
identifier,
|
||||
source: 'new',
|
||||
});
|
||||
|
||||
if (!marketAgent) {
|
||||
message.error(t('setting:myAgents.errors.fetchFailed'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create local agent with market data
|
||||
const result = await createAgent({
|
||||
config: {
|
||||
...marketAgent.config,
|
||||
avatar: marketAgent.avatar,
|
||||
backgroundColor: marketAgent.backgroundColor,
|
||||
description: marketAgent.description,
|
||||
editorData: marketAgent.editorData,
|
||||
marketIdentifier: identifier,
|
||||
tags: marketAgent.tags,
|
||||
title: marketAgent.title,
|
||||
},
|
||||
});
|
||||
|
||||
await refreshAgentList();
|
||||
|
||||
if (result.agentId) {
|
||||
navigate(urlJoin('/agent', result.agentId, 'profile'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[UserAgentCard] handleEdit error:', error);
|
||||
message.error(t('setting:myAgents.errors.editFailed'));
|
||||
} finally {
|
||||
setIsEditLoading(false);
|
||||
}
|
||||
}, [identifier, navigate, createAgent, refreshAgentList, message, t]);
|
||||
|
||||
const handleStatusAction = useCallback(
|
||||
(action: 'publish' | 'unpublish' | 'deprecate') => {
|
||||
onStatusChange?.(identifier, action);
|
||||
},
|
||||
[identifier, onStatusChange],
|
||||
);
|
||||
|
||||
const menuItems = isOwner
|
||||
? [
|
||||
{
|
||||
icon: <Icon icon={ExternalLink} />,
|
||||
key: 'viewDetail',
|
||||
label: t('setting:myAgents.actions.viewDetail'),
|
||||
onClick: handleViewDetail,
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={Pencil} />,
|
||||
key: 'edit',
|
||||
label: t('setting:myAgents.actions.edit'),
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={isPublished ? EyeOff : Eye} />,
|
||||
key: 'togglePublish',
|
||||
label: isPublished
|
||||
? t('setting:myAgents.actions.unpublish')
|
||||
: t('setting:myAgents.actions.publish'),
|
||||
onClick: () => handleStatusAction(isPublished ? 'unpublish' : 'publish'),
|
||||
},
|
||||
{
|
||||
danger: true,
|
||||
icon: <Icon icon={AlertTriangle} />,
|
||||
key: 'deprecate',
|
||||
label: t('setting:myAgents.actions.deprecate'),
|
||||
onClick: () => handleStatusAction('deprecate'),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Block
|
||||
className={styles.wrapper}
|
||||
clickable
|
||||
height={'100%'}
|
||||
onClick={() => navigate(link)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
variant={'outlined'}
|
||||
width={'100%'}
|
||||
>
|
||||
{isOwner && (
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['click']}>
|
||||
<div
|
||||
className={`more-button ${styles.moreButton}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Icon icon={MoreVerticalIcon} size={16} style={{ cursor: 'pointer' }} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
)}
|
||||
<Flexbox
|
||||
align={'flex-start'}
|
||||
gap={16}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
padding={16}
|
||||
width={'100%'}
|
||||
>
|
||||
<Flexbox
|
||||
gap={12}
|
||||
horizontal
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
avatar={avatar}
|
||||
background={backgroundColor || 'transparent'}
|
||||
shape={'square'}
|
||||
size={40}
|
||||
style={{ flex: 'none' }}
|
||||
/>
|
||||
<Flexbox
|
||||
flex={1}
|
||||
gap={2}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
<Link
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ color: 'inherit', flex: 1, overflow: 'hidden' }}
|
||||
to={link}
|
||||
>
|
||||
<Text as={'h3'} className={styles.title} ellipsis style={{ flex: 1 }}>
|
||||
{title}
|
||||
</Text>
|
||||
</Link>
|
||||
{isOwner && status && (
|
||||
<AntTag color={getStatusTagColor(status)} style={{ flexShrink: 0, margin: 0 }}>
|
||||
{t(`setting:myAgents.status.${status}`)}
|
||||
</AntTag>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Flexbox flex={1} gap={12} paddingInline={16}>
|
||||
<Text
|
||||
as={'p'}
|
||||
className={styles.desc}
|
||||
ellipsis={{
|
||||
rows: 3,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
<Tooltip
|
||||
placement={'top'}
|
||||
styles={{ root: { pointerEvents: 'none' } }}
|
||||
title={t('assistants.tokenUsage')}
|
||||
>
|
||||
<Tag className={styles.statTag} icon={<Icon icon={CoinsIcon} />}>
|
||||
{formatIntergerNumber(tokenUsage)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
{installCount !== undefined && (
|
||||
<Tooltip
|
||||
placement={'top'}
|
||||
styles={{ root: { pointerEvents: 'none' } }}
|
||||
title={t('assistants.downloads')}
|
||||
>
|
||||
<Tag className={styles.statTag} icon={<Icon icon={DownloadIcon} />}>
|
||||
{formatIntergerNumber(installCount)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.footer}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
padding={16}
|
||||
>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.secondaryDesc}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
<Icon icon={ClockIcon} size={14} />
|
||||
<PublishedTime
|
||||
className={styles.secondaryDesc}
|
||||
date={createdAt}
|
||||
template={'MMM DD, YYYY'}
|
||||
/>
|
||||
</Flexbox>
|
||||
{category && t(`category.assistant.${category}` as any)}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Block>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default UserAgentCard;
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import { Grid, Tag, Text } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import AssistantEmpty from '../../../features/AssistantEmpty';
|
||||
import { useUserDetailContext } from './DetailProvider';
|
||||
import UserAgentCard from './UserAgentCard';
|
||||
|
||||
interface UserAgentListProps {
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
const UserAgentList = memo<UserAgentListProps>(({ rows = 4 }) => {
|
||||
const { t } = useTranslation('discover');
|
||||
const { agents, agentCount } = useUserDetailContext();
|
||||
|
||||
if (agents.length === 0) return <AssistantEmpty />;
|
||||
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
<Text fontSize={16} weight={500}>
|
||||
{t('user.publishedAgents')}
|
||||
</Text>
|
||||
{agentCount > 0 && <Tag>{agentCount}</Tag>}
|
||||
</Flexbox>
|
||||
<Grid rows={rows} width={'100%'}>
|
||||
{agents.map((item, index) => (
|
||||
<UserAgentCard key={index} {...item} />
|
||||
))}
|
||||
</Grid>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default UserAgentList;
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { App } from 'antd';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
|
||||
import { marketApiService } from '@/services/marketApi';
|
||||
|
||||
export type AgentStatusAction = 'publish' | 'unpublish' | 'deprecate';
|
||||
|
||||
interface UseUserDetailOptions {
|
||||
onMutate?: () => void;
|
||||
}
|
||||
|
||||
export const useUserDetail = ({ onMutate }: UseUserDetailOptions = {}) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const { message, modal } = App.useApp();
|
||||
const { session } = useMarketAuth();
|
||||
|
||||
const handleStatusChange = useCallback(
|
||||
async (identifier: string, action: AgentStatusAction) => {
|
||||
if (!session?.accessToken) {
|
||||
message.error(t('myAgents.errors.notAuthenticated'));
|
||||
return;
|
||||
}
|
||||
|
||||
const messageKey = `agent-status-${action}`;
|
||||
const loadingText = t(`myAgents.actions.${action}Loading` as any);
|
||||
const successText = t(`myAgents.actions.${action}Success` as any);
|
||||
const errorText = t(`myAgents.actions.${action}Error` as any);
|
||||
|
||||
async function executeStatusChange(identifier: string, action: AgentStatusAction) {
|
||||
try {
|
||||
message.loading({ content: loadingText, key: messageKey });
|
||||
marketApiService.setAccessToken(session!.accessToken);
|
||||
|
||||
switch (action) {
|
||||
case 'publish': {
|
||||
await marketApiService.publishAgent(identifier);
|
||||
break;
|
||||
}
|
||||
case 'unpublish': {
|
||||
await marketApiService.unpublishAgent(identifier);
|
||||
break;
|
||||
}
|
||||
case 'deprecate': {
|
||||
await marketApiService.deprecateAgent(identifier);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
message.success({ content: successText, key: messageKey });
|
||||
onMutate?.();
|
||||
} catch (error) {
|
||||
console.error(`[useUserDetail] ${action} agent error:`, error);
|
||||
message.error({
|
||||
content: `${errorText}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
key: messageKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// For deprecate action, show confirmation dialog first
|
||||
if (action === 'deprecate') {
|
||||
modal.confirm({
|
||||
cancelText: t('myAgents.actions.cancel'),
|
||||
content: t('myAgents.actions.deprecateConfirmContent'),
|
||||
okButtonProps: { danger: true },
|
||||
okText: t('myAgents.actions.confirmDeprecate'),
|
||||
onOk: async () => {
|
||||
await executeStatusChange(identifier, action);
|
||||
},
|
||||
title: t('myAgents.actions.deprecateConfirmTitle'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await executeStatusChange(identifier, action);
|
||||
},
|
||||
[session?.accessToken, message, modal, t, onMutate],
|
||||
);
|
||||
|
||||
return {
|
||||
handleStatusChange,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { useMarketAuth, useMarketUserProfile } from '@/layout/AuthProvider/MarketAuth';
|
||||
import { MarketUserProfile } from '@/layout/AuthProvider/MarketAuth/types';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
|
||||
import NotFound from '../components/NotFound';
|
||||
import { UserDetailProvider } from './features/DetailProvider';
|
||||
import UserHeader from './features/Header';
|
||||
import UserAgentList from './features/UserAgentList';
|
||||
import { useUserDetail } from './features/useUserDetail';
|
||||
import Loading from './loading';
|
||||
|
||||
interface UserDetailPageProps {
|
||||
mobile?: boolean;
|
||||
}
|
||||
|
||||
const UserDetailPage = memo<UserDetailPageProps>(({ mobile }) => {
|
||||
const params = useParams<{ slug: string }>();
|
||||
const username = decodeURIComponent(params.slug ?? '');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { getCurrentUserInfo, isAuthenticated, openProfileSetup } = useMarketAuth();
|
||||
|
||||
const useUserProfile = useDiscoverStore((s) => s.useUserProfile);
|
||||
const { data, isLoading, mutate } = useUserProfile({ username });
|
||||
|
||||
// Get current user's profile to check ownership by userName
|
||||
const currentUser = getCurrentUserInfo();
|
||||
const { data: currentUserProfile } = useMarketUserProfile(currentUser?.sub);
|
||||
|
||||
// Check if the current user is viewing their own profile
|
||||
const isOwner =
|
||||
isAuthenticated && !!currentUser && data?.user?.namespace === currentUserProfile?.namespace;
|
||||
|
||||
const { handleStatusChange } = useUserDetail({ onMutate: mutate });
|
||||
|
||||
// Handle profile edit with navigation on userName change
|
||||
const handleEditProfile = useCallback(
|
||||
(onSuccess?: (profile: MarketUserProfile) => void) => {
|
||||
const currentUserName = data?.user?.userName || data?.user?.namespace;
|
||||
openProfileSetup((profile) => {
|
||||
// Call the original onSuccess callback if provided
|
||||
onSuccess?.(profile);
|
||||
|
||||
// Navigate to new URL if userName changed
|
||||
const newUserName = profile.userName || profile.namespace;
|
||||
if (newUserName && newUserName !== currentUserName) {
|
||||
navigate(`/community/user/${newUserName}`, { replace: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
[data?.user?.userName, data?.user?.namespace, openProfileSetup, navigate],
|
||||
);
|
||||
|
||||
const contextConfig = useMemo(() => {
|
||||
if (!data || !data.user) return null;
|
||||
const { user, agents } = data;
|
||||
const totalInstalls = agents.reduce((sum, agent) => sum + (agent.installCount || 0), 0);
|
||||
return {
|
||||
agentCount: agents.length,
|
||||
agents,
|
||||
isOwner,
|
||||
mobile,
|
||||
onEditProfile: handleEditProfile,
|
||||
onStatusChange: isOwner ? handleStatusChange : undefined,
|
||||
totalInstalls,
|
||||
user,
|
||||
};
|
||||
}, [data, isOwner, mobile, handleEditProfile, handleStatusChange]);
|
||||
|
||||
if (isLoading) return <Loading />;
|
||||
if (!contextConfig) return <NotFound />;
|
||||
|
||||
return (
|
||||
<UserDetailProvider config={contextConfig}>
|
||||
<UserHeader />
|
||||
<UserAgentList />
|
||||
</UserDetailProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export const MobileUserDetailPage = memo(() => {
|
||||
return <UserDetailPage mobile={true} />;
|
||||
});
|
||||
|
||||
export default UserDetailPage;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user