feat: rebranding total UI of app

This commit is contained in:
canisminor1990
2025-12-20 21:51:47 +08:00
committed by arvinxx
parent 436d9e5e8d
commit 13ca81bafa
422 changed files with 9883 additions and 2334 deletions
@@ -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;
@@ -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>
);
@@ -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}
@@ -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}
@@ -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>
);
@@ -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>
@@ -25,7 +25,7 @@ const Related = memo(() => {
category,
source: marketSource,
},
url: '/discover/assistant',
url: '/community/assistant',
},
{ skipNull: true },
)}
@@ -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,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
@@ -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();
@@ -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>
@@ -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}
@@ -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 },
);
@@ -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';
@@ -31,7 +31,7 @@ const Sidebar = memo<{ mobile?: boolean }>(({ mobile }) => {
maxHeight: 'calc(100vh - 76px)',
paddingBottom: 24,
position: 'sticky',
top: 0,
top: 16,
}}
width={360}
>
@@ -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>
}
@@ -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;
@@ -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 }} />
@@ -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;
@@ -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>
);
@@ -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>
@@ -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';
@@ -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,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 {
@@ -19,7 +19,7 @@ const Related = memo(() => {
query: {
category,
},
url: '/discover/mcp',
url: '/community/mcp',
})}
>
{t('mcp.details.related.listTitle')}
@@ -62,6 +62,8 @@ const ActionButton = memo(() => {
const buttonLoading = isLoading || isAuthLoading;
console.log('installed', isLoading, isAuthLoading, installed);
return installed ? (
<Flexbox gap={8} horizontal>
<Button
@@ -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}
@@ -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} />
@@ -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';
@@ -35,7 +35,7 @@ const Sidebar = memo<{ mobile?: boolean }>(({ mobile }) => {
maxHeight: 'calc(100vh - 76px)',
paddingBottom: 24,
position: 'sticky',
top: 0,
top: 16,
}}
width={360}
>
@@ -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;
@@ -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}
@@ -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';
@@ -18,7 +18,7 @@ const Related = memo(() => {
query: {
category,
},
url: '/discover/plugin',
url: '/community/plugin',
})}
>
{t('assistants.details.related.listTitle')}
@@ -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';
@@ -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>
@@ -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>
@@ -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} />
@@ -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>
);
@@ -30,7 +30,7 @@ const Sidebar = memo<{ mobile?: boolean }>(({ mobile }) => {
maxHeight: 'calc(100vh - 76px)',
paddingBottom: 24,
position: 'sticky',
top: 0,
top: 16,
}}
width={360}
>
@@ -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;
@@ -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>
);
@@ -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}
@@ -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';
@@ -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,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>
),
@@ -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>
@@ -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} />
@@ -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>
);
@@ -30,7 +30,7 @@ const Sidebar = memo<{ mobile?: boolean }>(({ mobile }) => {
maxHeight: 'calc(100vh - 76px)',
paddingBottom: 24,
position: 'sticky',
top: 0,
top: 16,
}}
width={360}
>
@@ -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