mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat: update the discover page sort, add haveSkill、mostUsage params (#11807)
* fix: slove group member plugin is lost & not use the plugins * feat: add the agents list sort params useage/skilled * fix: slove the test lint error
This commit is contained in:
@@ -56,15 +56,10 @@
|
||||
"assistants.more": "More",
|
||||
"assistants.plugins": "Integrated Skills",
|
||||
"assistants.recentSubmits": "Recent Updates",
|
||||
"assistants.sorts.createdAt": "Recently Published",
|
||||
"assistants.sorts.identifier": "Agent ID",
|
||||
"assistants.sorts.knowledgeCount": "Libraries",
|
||||
"assistants.sorts.myown": "View My Agents",
|
||||
"assistants.sorts.pluginCount": "Skills",
|
||||
"assistants.sorts.recommended": "Recommended",
|
||||
"assistants.sorts.title": "Agent Name",
|
||||
"assistants.sorts.tokenUsage": "Token Usage",
|
||||
"assistants.status.archived.reasons.official": "The platform removed this Agent due to security, policy, or other concerns.",
|
||||
"assistants.sorts.haveSkills": "Skilled",
|
||||
"assistants.sorts.mostUsage": "Most Usage",
|
||||
"assistants.sorts.updatedAt": "Recently Updated", "assistants.status.archived.reasons.official": "The platform removed this Agent due to security, policy, or other concerns.",
|
||||
"assistants.status.archived.reasons.owner": "The creator archived or removed this Agent.",
|
||||
"assistants.status.archived.subtitle": "This Agent has been archived. Possible reasons:",
|
||||
"assistants.status.archived.title": "Agent archived",
|
||||
|
||||
@@ -56,15 +56,10 @@
|
||||
"assistants.more": "更多",
|
||||
"assistants.plugins": "集成技能",
|
||||
"assistants.recentSubmits": "最近更新",
|
||||
"assistants.sorts.createdAt": "最近发布",
|
||||
"assistants.sorts.identifier": "助理 ID",
|
||||
"assistants.sorts.knowledgeCount": "资源库数量",
|
||||
"assistants.sorts.myown": "查看我的",
|
||||
"assistants.sorts.pluginCount": "技能数量",
|
||||
"assistants.sorts.recommended": "推荐",
|
||||
"assistants.sorts.title": "助理名称",
|
||||
"assistants.sorts.tokenUsage": "Token 使用量",
|
||||
"assistants.status.archived.reasons.official": "助理有安全/政治等问题,被官方下架",
|
||||
"assistants.sorts.haveSkills": "技能筛选",
|
||||
"assistants.sorts.mostUsage": "最多使用",
|
||||
"assistants.sorts.updatedAt": "最近更新", "assistants.status.archived.reasons.official": "助理有安全/政治等问题,被官方下架",
|
||||
"assistants.status.archived.reasons.owner": "开发助理的 owner 主动下架/归档该助理",
|
||||
"assistants.status.archived.subtitle": "该助理已被归档,可能原因包括:",
|
||||
"assistants.status.archived.title": "该助理已归档",
|
||||
|
||||
@@ -22,14 +22,10 @@ export enum AssistantCategory {
|
||||
}
|
||||
|
||||
export enum AssistantSorts {
|
||||
CreatedAt = 'createdAt',
|
||||
Identifier = 'identifier',
|
||||
KnowledgeCount = 'knowledgeCount',
|
||||
MyOwn = 'myown',
|
||||
PluginCount = 'pluginCount',
|
||||
HaveSkills = 'haveSkills',
|
||||
MostUsage = 'mostUsage',
|
||||
Recommended = 'recommended',
|
||||
Title = 'title',
|
||||
TokenUsage = 'tokenUsage',
|
||||
UpdatedAt = 'updatedAt',
|
||||
}
|
||||
|
||||
export enum AssistantNavKey {
|
||||
@@ -72,6 +68,7 @@ export type AssistantMarketSource = 'legacy' | 'new';
|
||||
|
||||
export interface AssistantQueryParams {
|
||||
category?: string;
|
||||
haveSkills?: boolean;
|
||||
includeAgentGroup?: boolean;
|
||||
locale?: string;
|
||||
order?: 'asc' | 'desc';
|
||||
|
||||
@@ -65,7 +65,7 @@ const AssistantItem = memo<DiscoverAssistantItem>(
|
||||
tokenUsage,
|
||||
pluginCount,
|
||||
knowledgeCount,
|
||||
installCount,
|
||||
forkCount,
|
||||
backgroundColor,
|
||||
userName,
|
||||
type,
|
||||
@@ -202,7 +202,7 @@ const AssistantItem = memo<DiscoverAssistantItem>(
|
||||
{description}
|
||||
</Text>
|
||||
<TokenTag
|
||||
installCount={installCount}
|
||||
forkCount={forkCount}
|
||||
knowledgeCount={knowledgeCount}
|
||||
pluginCount={pluginCount}
|
||||
tokenUsage={tokenUsage}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MCP } from '@lobehub/icons';
|
||||
import { Flexbox, Icon, Tag, Tooltip } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { BookTextIcon, CoinsIcon, DownloadIcon } from 'lucide-react';
|
||||
import { BookTextIcon, CoinsIcon, GitForkIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -22,7 +22,7 @@ const styles = createStaticStyles(({ css, cssVar }) => {
|
||||
});
|
||||
|
||||
interface TokenTagProps {
|
||||
installCount?: number;
|
||||
forkCount?: number;
|
||||
knowledgeCount?: number;
|
||||
placement?: 'top' | 'right';
|
||||
pluginCount?: number;
|
||||
@@ -30,7 +30,7 @@ interface TokenTagProps {
|
||||
}
|
||||
|
||||
const TokenTag = memo<TokenTagProps>(
|
||||
({ tokenUsage, pluginCount, knowledgeCount, installCount, placement = 'right' }) => {
|
||||
({ tokenUsage, pluginCount, knowledgeCount, forkCount, placement = 'right' }) => {
|
||||
const { t } = useTranslation('discover');
|
||||
return (
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
@@ -43,14 +43,14 @@ const TokenTag = memo<TokenTagProps>(
|
||||
{formatIntergerNumber(tokenUsage)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
{Boolean(installCount && installCount > 0) && (
|
||||
{Boolean(forkCount && forkCount > 0) && (
|
||||
<Tooltip
|
||||
placement={placement}
|
||||
styles={{ root: { pointerEvents: 'none' } }}
|
||||
title={t('assistants.downloads')}
|
||||
title={t('fork.forksCount', { count: forkCount })}
|
||||
>
|
||||
<Tag className={styles.token} icon={<Icon icon={DownloadIcon} />}>
|
||||
{formatIntergerNumber(installCount)}
|
||||
<Tag className={styles.token} icon={<Icon icon={GitForkIcon} />}>
|
||||
{formatIntergerNumber(forkCount)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useQueryRoute } from '@/hooks/useQueryRoute';
|
||||
import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
|
||||
import { usePathname, useQuery } from '@/libs/router/navigation';
|
||||
import {
|
||||
AssistantSorts,
|
||||
@@ -26,7 +25,6 @@ const SortButton = memo(() => {
|
||||
const pathname = usePathname();
|
||||
const { sort } = useQuery();
|
||||
const router = useQueryRoute();
|
||||
const { isAuthenticated, getCurrentUserInfo } = useMarketAuth();
|
||||
const activeTab = useMemo(() => pathname.split('community/')[1] as DiscoverTab, [pathname]);
|
||||
type SortItem = Extract<DropdownItem, { type?: 'item' }> & {
|
||||
key: string;
|
||||
@@ -35,46 +33,24 @@ const SortButton = memo(() => {
|
||||
const items = useMemo<SortItem[]>(() => {
|
||||
switch (activeTab) {
|
||||
case DiscoverTab.Assistants: {
|
||||
const baseItems = [
|
||||
return [
|
||||
{
|
||||
key: AssistantSorts.Recommended,
|
||||
label: t('assistants.sorts.recommended'),
|
||||
},
|
||||
{
|
||||
key: AssistantSorts.CreatedAt,
|
||||
label: t('assistants.sorts.createdAt'),
|
||||
key: AssistantSorts.UpdatedAt,
|
||||
label: t('assistants.sorts.updatedAt'),
|
||||
},
|
||||
{
|
||||
key: AssistantSorts.Title,
|
||||
label: t('assistants.sorts.title'),
|
||||
key: AssistantSorts.MostUsage,
|
||||
label: t('assistants.sorts.mostUsage'),
|
||||
},
|
||||
{
|
||||
key: AssistantSorts.Identifier,
|
||||
label: t('assistants.sorts.identifier'),
|
||||
},
|
||||
{
|
||||
key: AssistantSorts.TokenUsage,
|
||||
label: t('assistants.sorts.tokenUsage'),
|
||||
},
|
||||
{
|
||||
key: AssistantSorts.PluginCount,
|
||||
label: t('assistants.sorts.pluginCount'),
|
||||
},
|
||||
{
|
||||
key: AssistantSorts.KnowledgeCount,
|
||||
label: t('assistants.sorts.knowledgeCount'),
|
||||
key: AssistantSorts.HaveSkills,
|
||||
label: t('assistants.sorts.haveSkills'),
|
||||
},
|
||||
];
|
||||
|
||||
// Only add "My Own" option if user is authenticated
|
||||
if (isAuthenticated) {
|
||||
baseItems.push({
|
||||
key: AssistantSorts.MyOwn,
|
||||
label: t('assistants.sorts.myown'),
|
||||
});
|
||||
}
|
||||
|
||||
return baseItems;
|
||||
}
|
||||
case DiscoverTab.Plugins: {
|
||||
return [
|
||||
@@ -172,7 +148,7 @@ const SortButton = memo(() => {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}, [t, activeTab, isAuthenticated]);
|
||||
}, [t, activeTab]);
|
||||
|
||||
const activeItem = useMemo<SortItem | undefined>(() => {
|
||||
if (sort) {
|
||||
@@ -183,18 +159,7 @@ const SortButton = memo(() => {
|
||||
}, [items, sort]);
|
||||
|
||||
const handleSort = (config: string) => {
|
||||
const query: any = { sort: config };
|
||||
|
||||
// If "My Own" is selected, add ownerId to query
|
||||
if (config === AssistantSorts.MyOwn) {
|
||||
const userInfo = getCurrentUserInfo();
|
||||
|
||||
if (userInfo?.accountId) {
|
||||
query.ownerId = userInfo.accountId;
|
||||
}
|
||||
}
|
||||
|
||||
router.push(pathname, { query });
|
||||
router.push(pathname, { query: { sort: config } });
|
||||
};
|
||||
|
||||
const menuItems = useMemo<DropdownMenuCheckboxItem[]>(
|
||||
|
||||
@@ -59,14 +59,10 @@ export default {
|
||||
'assistants.more': 'More',
|
||||
'assistants.plugins': 'Integrated Skills',
|
||||
'assistants.recentSubmits': 'Recent Updates',
|
||||
'assistants.sorts.createdAt': 'Recently Published',
|
||||
'assistants.sorts.identifier': 'Agent ID',
|
||||
'assistants.sorts.knowledgeCount': 'Libraries',
|
||||
'assistants.sorts.myown': 'View My Agents',
|
||||
'assistants.sorts.pluginCount': 'Skills',
|
||||
'assistants.sorts.haveSkills': 'Skilled',
|
||||
'assistants.sorts.mostUsage': 'Most Usage',
|
||||
'assistants.sorts.recommended': 'Recommended',
|
||||
'assistants.sorts.title': 'Agent Name',
|
||||
'assistants.sorts.tokenUsage': 'Token Usage',
|
||||
'assistants.sorts.updatedAt': 'Recently Updated',
|
||||
'assistants.status.archived.reasons.official':
|
||||
'The platform removed this Agent due to security, policy, or other concerns.',
|
||||
'assistants.status.archived.reasons.owner': 'The creator archived or removed this Agent.',
|
||||
|
||||
@@ -146,6 +146,7 @@ export const searchRouter = router({
|
||||
searchPromises.push(
|
||||
ctx.discoverService
|
||||
.getAssistantList({
|
||||
includeAgentGroup: true,
|
||||
locale,
|
||||
pageSize: limitPerType,
|
||||
q: query,
|
||||
|
||||
@@ -395,30 +395,6 @@ describe('DiscoverService', () => {
|
||||
expect(result.items.map((item) => item.identifier)).toContain('assistant-3');
|
||||
});
|
||||
|
||||
it('should sort by creation date descending', async () => {
|
||||
const result = await service.getAssistantList({
|
||||
sort: AssistantSorts.CreatedAt,
|
||||
order: 'desc',
|
||||
source: 'legacy',
|
||||
});
|
||||
|
||||
expect(result.items[0].identifier).toBe('assistant-3');
|
||||
expect(result.items[1].identifier).toBe('assistant-2');
|
||||
expect(result.items[2].identifier).toBe('assistant-1');
|
||||
});
|
||||
|
||||
it('should sort by title ascending', async () => {
|
||||
const result = await service.getAssistantList({
|
||||
sort: AssistantSorts.Title,
|
||||
order: 'asc',
|
||||
source: 'legacy',
|
||||
});
|
||||
|
||||
// Note: The service has reversed logic for title sorting
|
||||
expect(result.items[0].title).toBe('Test Assistant 3');
|
||||
expect(result.items[1].title).toBe('Test Assistant 2');
|
||||
});
|
||||
|
||||
it('should paginate results', async () => {
|
||||
const result = await service.getAssistantList({ page: 1, pageSize: 1, source: 'legacy' });
|
||||
|
||||
|
||||
@@ -375,6 +375,7 @@ export class DiscoverService {
|
||||
const assistant = merge(cloneDeep(DEFAULT_DISCOVER_ASSISTANT_ITEM), { ...item, ...meta });
|
||||
const list = await this.getAssistantList({
|
||||
category: assistant.category,
|
||||
includeAgentGroup: true,
|
||||
locale,
|
||||
page: 1,
|
||||
pageSize: 7,
|
||||
@@ -415,7 +416,7 @@ export class DiscoverService {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
q,
|
||||
sort = AssistantSorts.CreatedAt,
|
||||
sort = AssistantSorts.Recommended,
|
||||
ownerId,
|
||||
} = params;
|
||||
const currentPage = Number(page) || 1;
|
||||
@@ -466,7 +467,8 @@ export class DiscoverService {
|
||||
if (sort) {
|
||||
log('legacyGetAssistantList: sorting by %s %s', sort, order);
|
||||
switch (sort) {
|
||||
case AssistantSorts.CreatedAt: {
|
||||
case AssistantSorts.UpdatedAt: {
|
||||
// Legacy source doesn't have updatedAt, fallback to createdAt
|
||||
list = list.sort((a, b) => {
|
||||
if (order === 'asc') {
|
||||
return dayjs(a.createdAt).unix() - dayjs(b.createdAt).unix();
|
||||
@@ -476,57 +478,8 @@ export class DiscoverService {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case AssistantSorts.KnowledgeCount: {
|
||||
list = list.sort((a, b) => {
|
||||
if (order === 'asc') {
|
||||
return (a.knowledgeCount || 0) - (b.knowledgeCount || 0);
|
||||
} else {
|
||||
return (b.knowledgeCount || 0) - (a.knowledgeCount || 0);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case AssistantSorts.PluginCount: {
|
||||
list = list.sort((a, b) => {
|
||||
if (order === 'asc') {
|
||||
return (a.pluginCount || 0) - (b.pluginCount || 0);
|
||||
} else {
|
||||
return (b.pluginCount || 0) - (a.pluginCount || 0);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case AssistantSorts.TokenUsage: {
|
||||
list = list.sort((a, b) => {
|
||||
if (order === 'asc') {
|
||||
return (a.tokenUsage || 0) - (b.tokenUsage || 0);
|
||||
} else {
|
||||
return (b.tokenUsage || 0) - (a.tokenUsage || 0);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case AssistantSorts.Identifier: {
|
||||
list = list.sort((a, b) => {
|
||||
if (order !== 'desc') {
|
||||
return a.identifier.localeCompare(b.identifier);
|
||||
} else {
|
||||
return b.identifier.localeCompare(a.identifier);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case AssistantSorts.Title: {
|
||||
list = list.sort((a, b) => {
|
||||
if (order === 'desc') {
|
||||
return (a.title || a.identifier).localeCompare(b.title || b.identifier);
|
||||
} else {
|
||||
return (b.title || b.identifier).localeCompare(a.title || a.identifier);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Legacy source doesn't support these sorts (MostUsage, HaveSkills, Recommended), keep original order
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -620,9 +573,9 @@ export class DiscoverService {
|
||||
|
||||
examples: Array.isArray((data as any).examples)
|
||||
? (data as any).examples.map((example: any) => ({
|
||||
content: typeof example === 'string' ? example : example.content || '',
|
||||
role: example.role || 'user',
|
||||
}))
|
||||
content: typeof example === 'string' ? example : example.content || '',
|
||||
role: example.role || 'user',
|
||||
}))
|
||||
: [],
|
||||
homepage:
|
||||
(data as any).homepage ||
|
||||
@@ -654,6 +607,7 @@ export class DiscoverService {
|
||||
// Get related assistants
|
||||
const list = await this.getAssistantList({
|
||||
category: assistant.category,
|
||||
includeAgentGroup: true,
|
||||
locale,
|
||||
page: 1,
|
||||
pageSize: 7,
|
||||
@@ -711,7 +665,7 @@ export class DiscoverService {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
q,
|
||||
sort = AssistantSorts.CreatedAt,
|
||||
sort = AssistantSorts.Recommended,
|
||||
ownerId,
|
||||
includeAgentGroup,
|
||||
} = rest;
|
||||
@@ -719,25 +673,37 @@ export class DiscoverService {
|
||||
try {
|
||||
const normalizedLocale = normalizeLocale(locale);
|
||||
|
||||
let apiSort: 'createdAt' | 'updatedAt' | 'name' = 'createdAt';
|
||||
let apiSort: 'createdAt' | 'updatedAt' | 'name' | 'mostUsage' | 'recommended' =
|
||||
'recommended';
|
||||
let haveSkills: boolean | undefined = rest.haveSkills;
|
||||
|
||||
switch (sort) {
|
||||
case AssistantSorts.Identifier:
|
||||
case AssistantSorts.Title: {
|
||||
apiSort = 'name';
|
||||
case AssistantSorts.UpdatedAt: {
|
||||
apiSort = 'updatedAt';
|
||||
break;
|
||||
}
|
||||
case AssistantSorts.CreatedAt:
|
||||
case AssistantSorts.MyOwn: {
|
||||
apiSort = 'createdAt';
|
||||
case AssistantSorts.MostUsage: {
|
||||
apiSort = 'mostUsage';
|
||||
break;
|
||||
}
|
||||
case AssistantSorts.HaveSkills: {
|
||||
// When user selects "Skilled", set haveSkills=true and use recommended sort
|
||||
haveSkills = true;
|
||||
apiSort = 'updatedAt';
|
||||
break;
|
||||
}
|
||||
case AssistantSorts.Recommended: {
|
||||
apiSort = 'recommended';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
apiSort = 'createdAt';
|
||||
apiSort = 'recommended';
|
||||
}
|
||||
}
|
||||
|
||||
const data = await this.market.agents.getAgentList({
|
||||
category,
|
||||
haveSkills,
|
||||
// includeAgentGroup may not be in SDK type definition yet, using 'as any'
|
||||
includeAgentGroup,
|
||||
locale: normalizedLocale,
|
||||
@@ -761,6 +727,7 @@ export class DiscoverService {
|
||||
config: item.config || {},
|
||||
createdAt: item.createdAt || item.updatedAt || new Date().toISOString(),
|
||||
description: item.description || item.summary || '',
|
||||
forkCount: item.forkCount,
|
||||
homepage: item.homepage || `https://lobehub.com/discover/assistant/${item.identifier}`,
|
||||
identifier: item.identifier,
|
||||
installCount: item.installCount,
|
||||
|
||||
Reference in New Issue
Block a user