From c1f5ba192771932072aec989a5ef08d191a24629 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 15 May 2026 18:55:25 +0800 Subject: [PATCH] fix: align add extension marketplace ui --- .../plugin-market/PluginMarketComponent.tsx | 437 +++++++----------- .../PluginMarketCardComponent.tsx | 163 ++++--- web/src/app/infra/http/CloudServiceClient.ts | 110 +++++ web/src/i18n/locales/en-US.ts | 11 +- web/src/i18n/locales/zh-Hans.ts | 9 +- 5 files changed, 389 insertions(+), 341 deletions(-) diff --git a/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx b/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx index 09658bfb..bbdbb601 100644 --- a/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx @@ -19,7 +19,9 @@ import { Search, Wrench, AudioWaveform, + Hash, Book, + FileText, SlidersHorizontal, X, } from 'lucide-react'; @@ -35,8 +37,6 @@ import { LoadingSpinner } from '@/components/ui/loading-spinner'; import { Button } from '@/components/ui/button'; import { PluginTag } from '@/app/infra/http/CloudServiceClient'; -import { RecommendationLists, RecommendationList } from './RecommendationLists'; - interface SortOption { value: string; label: string; @@ -65,6 +65,7 @@ function MarketPageContent({ ]; const [searchQuery, setSearchQuery] = useState(''); + const [componentFilter, setComponentFilter] = useState('all'); const [typeFilter, setTypeFilter] = useState(() => { const type = searchParams.get('type'); if (type && validTypes.includes(type)) { @@ -72,7 +73,8 @@ function MarketPageContent({ } return 'all'; }); - const activeAdvancedFilters = 0; + const activeAdvancedFilters = + (typeFilter === 'all' ? 0 : 1) + (componentFilter === 'all' ? 0 : 1); const [selectedTags, setSelectedTags] = useState([]); const [availableTags, setAvailableTags] = useState([]); const [tagNames, setTagNames] = useState>({}); @@ -83,9 +85,6 @@ function MarketPageContent({ const [currentPage, setCurrentPage] = useState(1); const [total, setTotal] = useState(0); const [sortOption, setSortOption] = useState('install_count_desc'); - const [recommendationLists, setRecommendationLists] = useState< - RecommendationList[] - >([]); const pageSize = 12; // 每页12个 const searchTimeoutRef = useRef(null); @@ -120,6 +119,27 @@ function MarketPageContent({ }, ]; + const componentOptions = [ + { value: 'all', label: t('market.allComponents'), icon: null }, + { value: 'Tool', label: t('market.componentName.Tool'), icon: Wrench }, + { value: 'Command', label: t('market.componentName.Command'), icon: Hash }, + { + value: 'EventListener', + label: t('market.componentName.EventListener'), + icon: AudioWaveform, + }, + { + value: 'KnowledgeEngine', + label: t('market.componentName.KnowledgeEngine'), + icon: Book, + }, + { + value: 'Parser', + label: t('market.componentName.Parser'), + icon: FileText, + }, + ]; + // 获取当前排序参数 const getCurrentSort = useCallback(() => { const option = sortOptions.find((opt) => opt.value === sortOption); @@ -129,71 +149,30 @@ function MarketPageContent({ }, [sortOption]); // 将API响应转换为VO对象 - const transformToVO = useCallback((plugin: PluginV4): PluginMarketCardVO => { - return new PluginMarketCardVO({ - pluginId: plugin.author + ' / ' + plugin.name, - author: plugin.author, - pluginName: plugin.name, - label: extractI18nObject(plugin.label), - description: - extractI18nObject(plugin.description) || t('market.noDescription'), - installCount: plugin.install_count, - iconURL: getCloudServiceClientSync().getPluginIconURL( - plugin.author, - plugin.name, - ), - githubURL: plugin.repository, - version: plugin.latest_version, - components: plugin.components, - tags: plugin.tags || [], - type: plugin.type, - }); - }, []); + const transformToVO = useCallback( + (plugin: PluginV4): PluginMarketCardVO => { + const cloudClient = getCloudServiceClientSync(); + const iconURL = + plugin.type === 'mcp' + ? cloudClient.getMCPMarketplaceIconURL(plugin.author, plugin.name) + : plugin.type === 'skill' + ? cloudClient.getSkillMarketplaceIconURL(plugin.author, plugin.name) + : cloudClient.getPluginIconURL(plugin.author, plugin.name); - const transformMCPToVO = useCallback( - (mcp: any): PluginMarketCardVO => { return new PluginMarketCardVO({ - pluginId: mcp.author + ' / ' + mcp.name, - author: mcp.author, - pluginName: mcp.name, - label: extractI18nObject(mcp.label), + pluginId: plugin.author + ' / ' + plugin.name, + author: plugin.author, + pluginName: plugin.name, + label: extractI18nObject(plugin.label), description: - extractI18nObject(mcp.description) || t('market.noDescription'), - installCount: mcp.install_count || 0, - iconURL: - mcp.icon || - getCloudServiceClientSync().getPluginIconURL(mcp.author, mcp.name), - githubURL: mcp.repository, - version: mcp.latest_version, - components: mcp.components || {}, - tags: mcp.tags || [], - type: 'mcp', - }); - }, - [t], - ); - - const transformSkillToVO = useCallback( - (skill: any): PluginMarketCardVO => { - return new PluginMarketCardVO({ - pluginId: skill.author + ' / ' + skill.name, - author: skill.author, - pluginName: skill.name, - label: extractI18nObject(skill.label), - description: - extractI18nObject(skill.description) || t('market.noDescription'), - installCount: skill.install_count || 0, - iconURL: - skill.icon || - getCloudServiceClientSync().getPluginIconURL( - skill.author, - skill.name, - ), - githubURL: skill.repository, - version: skill.latest_version, - components: skill.components || {}, - tags: skill.tags || [], - type: 'skill', + extractI18nObject(plugin.description) || t('market.noDescription'), + installCount: plugin.install_count || 0, + iconURL, + githubURL: plugin.repository, + version: plugin.latest_version, + components: plugin.components || {}, + tags: plugin.tags || [], + type: plugin.type, }); }, [t], @@ -201,7 +180,12 @@ function MarketPageContent({ // 获取插件列表 const fetchPlugins = useCallback( - async (page: number, isSearch: boolean = false, reset: boolean = false) => { + async ( + page: number, + isSearch: boolean = false, + reset: boolean = false, + queryOverride?: string, + ) => { if (page === 1) { setIsLoading(true); } else { @@ -210,109 +194,24 @@ function MarketPageContent({ try { const { sortBy, sortOrder } = getCurrentSort(); - const query = isSearch && searchQuery.trim() ? searchQuery.trim() : ''; + const query = (queryOverride ?? searchQuery).trim(); - let newPlugins: PluginMarketCardVO[] = []; - let total = 0; + const response = + await getCloudServiceClientSync().searchMarketplaceExtensions({ + query: isSearch ? query : '', + page, + page_size: pageSize, + sort_by: sortBy, + sort_order: sortOrder, + type_filter: typeFilter === 'all' ? undefined : typeFilter, + component_filter: + componentFilter === 'all' ? undefined : componentFilter, + tags_filter: selectedTags.length > 0 ? selectedTags : undefined, + }); - if (typeFilter === 'all') { - let pluginsResult: PluginMarketCardVO[] = []; - let mcpsResult: PluginMarketCardVO[] = []; - let skillsResult: PluginMarketCardVO[] = []; - let pluginsTotal = 0; - let mcpsTotal = 0; - let skillsTotal = 0; - - try { - const pluginsResponse = - await getCloudServiceClientSync().searchMarketplacePlugins( - query, - page, - pageSize, - sortBy, - sortOrder, - undefined, - selectedTags.length > 0 ? selectedTags : undefined, - 'plugin', - ); - pluginsResult = pluginsResponse.plugins - .filter((plugin) => { - const keys = Object.keys(plugin.components || {}); - return !( - keys.length > 0 && - keys.every((k) => k === 'KnowledgeRetriever') - ); - }) - .map(transformToVO); - pluginsTotal = pluginsResponse.total || 0; - } catch (e) { - console.warn('Failed to fetch plugins:', e); - } - - try { - const mcpsResponse = - await getCloudServiceClientSync().searchMarketplacePlugins( - query, - page, - pageSize, - sortBy, - sortOrder, - undefined, - selectedTags.length > 0 ? selectedTags : undefined, - 'mcp', - ); - mcpsResult = (mcpsResponse.plugins || []).map(transformMCPToVO); - mcpsTotal = mcpsResponse.total || 0; - } catch (e) { - console.warn('Failed to fetch mcps:', e); - } - - try { - const skillsResponse = - await getCloudServiceClientSync().searchMarketplacePlugins( - query, - page, - pageSize, - sortBy, - sortOrder, - undefined, - selectedTags.length > 0 ? selectedTags : undefined, - 'skill', - ); - skillsResult = (skillsResponse.plugins || []).map( - transformSkillToVO, - ); - skillsTotal = skillsResponse.total || 0; - } catch (e) { - console.warn('Failed to fetch skills:', e); - } - - newPlugins = [...pluginsResult, ...mcpsResult, ...skillsResult]; - total = pluginsTotal + mcpsTotal + skillsTotal; - } else { - const response = - await getCloudServiceClientSync().searchMarketplacePlugins( - query, - page, - pageSize, - sortBy, - sortOrder, - undefined, - selectedTags.length > 0 ? selectedTags : undefined, - typeFilter === 'all' ? undefined : typeFilter, - ); - - const data: ApiRespMarketplacePlugins = response; - newPlugins = data.plugins - .filter((plugin) => { - const keys = Object.keys(plugin.components || {}); - return !( - keys.length > 0 && keys.every((k) => k === 'KnowledgeRetriever') - ); - }) - .map(transformToVO); - total = data.total; - } + const data: ApiRespMarketplacePlugins = response; + const newPlugins = data.plugins.map(transformToVO); + const total = data.total; if (reset || page === 1) { setPlugins(newPlugins); @@ -337,11 +236,10 @@ function MarketPageContent({ }, [ searchQuery, + componentFilter, selectedTags, pageSize, transformToVO, - transformMCPToVO, - transformSkillToVO, plugins.length, getCurrentSort, typeFilter, @@ -379,27 +277,13 @@ function MarketPageContent({ } }; - // Fetch recommendation lists - useEffect(() => { - async function fetchRecommendationLists() { - try { - const response = - await getCloudServiceClientSync().getRecommendationLists(); - setRecommendationLists(response.lists || []); - } catch (error) { - console.error('Failed to fetch recommendation lists:', error); - } - } - fetchRecommendationLists(); - }, []); - // 搜索功能 const handleSearch = useCallback( (query: string) => { setSearchQuery(query); setCurrentPage(1); setPlugins([]); - fetchPlugins(1, !!query.trim(), true); + fetchPlugins(1, !!query.trim(), true, query); }, [fetchPlugins], ); @@ -436,7 +320,11 @@ function MarketPageContent({ // Handle type filter change const handleTypeFilterChange = useCallback((value: string) => { setTypeFilter(value); + if (value !== 'plugin') { + setComponentFilter('all'); + } setCurrentPage(1); + setSelectedTags([]); setPlugins([]); // Update URL query param to keep it in sync @@ -452,10 +340,27 @@ function MarketPageContent({ window.history.replaceState({}, '', newUrl); }, []); - // 当排序选项或类型筛选变化时重新加载数据 + const handleComponentFilterChange = useCallback((value: string) => { + setComponentFilter(value); + setCurrentPage(1); + setPlugins([]); + + if (value !== 'all') { + setTypeFilter('plugin'); + + const params = new URLSearchParams(window.location.search); + params.set('type', 'plugin'); + const newUrl = params.toString() + ? `${window.location.pathname}?${params.toString()}` + : window.location.pathname; + window.history.replaceState({}, '', newUrl); + } + }, []); + + // 当排序选项或组件筛选或类型筛选变化时重新加载数据 useEffect(() => { fetchPlugins(1, !!searchQuery.trim(), true); - }, [sortOption, typeFilter]); + }, [sortOption, componentFilter, typeFilter]); // Tags 筛选变化时重新搜索 useEffect(() => { @@ -609,11 +514,11 @@ function MarketPageContent({ return (
- {/* Fixed header with search and sort controls */} -
- {/* Search box and actions */} -
-
+ {/* Fixed header section with search, sort, and status */} +
+ {/* 搜索、排序和筛选入口 */} +
+
- {headerActions && ( -
- {headerActions} -
- )} -
- {/* Sort, filters and tags in one row */} -
- {/* Sort dropdown */} -
- - {t('market.sortBy')}: - +