diff --git a/web/app/components/plugins/marketplace/context.tsx b/web/app/components/plugins/marketplace/context.tsx index bbadb4bf3a..8754b2c551 100644 --- a/web/app/components/plugins/marketplace/context.tsx +++ b/web/app/components/plugins/marketplace/context.tsx @@ -2,21 +2,42 @@ import type { ReactNode } from 'react' import { + useCallback, useState, } from 'react' import { createContext, useContextSelector, } from 'use-context-selector' +import { useDebounceFn } from 'ahooks' +import { PLUGIN_TYPE_SEARCH_MAP } from './plugin-type-switch' +import type { Plugin } from '../types' +import type { PluginsSearchParams } from './types' export type MarketplaceContextValue = { intersected: boolean setIntersected: (intersected: boolean) => void + searchPluginText: string + handleSearchPluginTextChange: (text: string) => void + filterPluginTags: string[] + handleFilterPluginTagsChange: (tags: string[]) => void + activePluginType: string + handleActivePluginTypeChange: (type: string) => void + plugins?: Plugin[] + setPlugins?: (plugins: Plugin[]) => void } export const MarketplaceContext = createContext({ intersected: true, setIntersected: () => {}, + searchPluginText: '', + handleSearchPluginTextChange: () => {}, + filterPluginTags: [], + handleFilterPluginTagsChange: () => {}, + activePluginType: PLUGIN_TYPE_SEARCH_MAP.all, + handleActivePluginTypeChange: () => {}, + plugins: undefined, + setPlugins: () => {}, }) type MarketplaceContextProviderProps = { @@ -31,12 +52,69 @@ export const MarketplaceContextProvider = ({ children, }: MarketplaceContextProviderProps) => { const [intersected, setIntersected] = useState(true) + const [searchPluginText, setSearchPluginText] = useState('') + const [filterPluginTags, setFilterPluginTags] = useState([]) + const [activePluginType, setActivePluginType] = useState(PLUGIN_TYPE_SEARCH_MAP.all) + const [plugins, setPlugins] = useState() + + const handleUpdatePlugins = useCallback((query: PluginsSearchParams) => { + const fetchPlugins = async () => { + const response = await fetch( + 'https://marketplace.dify.dev/api/v1/plugins/search/basic', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: query.query, + page: 1, + page_size: 10, + sort_by: query.sortBy, + sort_order: query.sortOrder, + category: query.category, + tag: query.tag, + }), + }, + ) + const data = await response.json() + setPlugins(data.data.plugins) + } + + fetchPlugins() + }, []) + + const { run: handleUpdatePluginsWithDebounced } = useDebounceFn(handleUpdatePlugins, { + wait: 500, + }) + + const handleSearchPluginTextChange = useCallback((text: string) => { + setSearchPluginText(text) + + handleUpdatePluginsWithDebounced({ query: text }) + }, [handleUpdatePluginsWithDebounced]) + + const handleFilterPluginTagsChange = useCallback((tags: string[]) => { + setFilterPluginTags(tags) + }, []) + + const handleActivePluginTypeChange = useCallback((type: string) => { + setActivePluginType(type) + }, []) return ( {children} diff --git a/web/app/components/plugins/marketplace/list/list-with-collection.tsx b/web/app/components/plugins/marketplace/list/list-with-collection.tsx index 9009b7a799..944b72a0e0 100644 --- a/web/app/components/plugins/marketplace/list/list-with-collection.tsx +++ b/web/app/components/plugins/marketplace/list/list-with-collection.tsx @@ -4,7 +4,7 @@ import type { Plugin } from '@/app/components/plugins/types' import Card from '@/app/components/plugins/card' import CardMoreInfo from '@/app/components/plugins/card/card-more-info' -interface ListWithCollectionProps { +type ListWithCollectionProps = { marketplaceCollections: MarketplaceCollection[] marketplaceCollectionPluginsMap: Record } diff --git a/web/app/components/plugins/marketplace/list/list-wrapper.tsx b/web/app/components/plugins/marketplace/list/list-wrapper.tsx index 0de2fa2c06..8272edf480 100644 --- a/web/app/components/plugins/marketplace/list/list-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/list-wrapper.tsx @@ -1,9 +1,10 @@ 'use client' import type { Plugin } from '../../types' import type { MarketplaceCollection } from '../types' +import { useMarketplaceContext } from '../context' import List from './index' -interface ListWrapperProps { +type ListWrapperProps = { marketplaceCollections: MarketplaceCollection[] marketplaceCollectionPluginsMap: Record } @@ -11,10 +12,13 @@ const ListWrapper = ({ marketplaceCollections, marketplaceCollectionPluginsMap, }: ListWrapperProps) => { + const plugins = useMarketplaceContext(s => s.plugins) + return ( ) } diff --git a/web/app/components/plugins/marketplace/plugin-type-switch.tsx b/web/app/components/plugins/marketplace/plugin-type-switch.tsx index 8f3c478c57..35f5349343 100644 --- a/web/app/components/plugins/marketplace/plugin-type-switch.tsx +++ b/web/app/components/plugins/marketplace/plugin-type-switch.tsx @@ -1,25 +1,22 @@ 'use client' -import { useState } from 'react' -import { PluginType } from '../types' import { RiArchive2Line, RiBrain2Line, RiHammerLine, RiPuzzle2Line, } from '@remixicon/react' +import { PluginType } from '../types' +import { useMarketplaceContext } from './context' import cn from '@/utils/classnames' -const PLUGIN_TYPE_SEARCH_MAP = { +export const PLUGIN_TYPE_SEARCH_MAP = { all: 'all', model: PluginType.model, tool: PluginType.tool, extension: PluginType.extension, bundle: 'bundle', } -type PluginTypeSwitchProps = { - onChange?: (type: string) => void -} const options = [ { value: PLUGIN_TYPE_SEARCH_MAP.all, @@ -47,10 +44,9 @@ const options = [ icon: , }, ] -const PluginTypeSwitch = ({ - onChange, -}: PluginTypeSwitchProps) => { - const [activeType, setActiveType] = useState(PLUGIN_TYPE_SEARCH_MAP.all) +const PluginTypeSwitch = () => { + const activePluginType = useMarketplaceContext(s => s.activePluginType) + const handleActivePluginTypeChange = useMarketplaceContext(s => s.handleActivePluginTypeChange) return (
{ - setActiveType(option.value) - onChange?.(option.value) + handleActivePluginTypeChange(option.value) }} > {option.icon} diff --git a/web/app/components/plugins/marketplace/search-box/index.tsx b/web/app/components/plugins/marketplace/search-box/index.tsx index 3115b87738..6878efdfa1 100644 --- a/web/app/components/plugins/marketplace/search-box/index.tsx +++ b/web/app/components/plugins/marketplace/search-box/index.tsx @@ -1,29 +1,14 @@ 'use client' - -import { - useCallback, - useState, -} from 'react' import { RiCloseLine } from '@remixicon/react' import { useMarketplaceContext } from '../context' import TagsFilter from './tags-filter' import ActionButton from '@/app/components/base/action-button' import cn from '@/utils/classnames' -type SearchBoxProps = { - onChange?: (searchText: string, tags: string[]) => void -} -const SearchBox = ({ - onChange, -}: SearchBoxProps) => { +const SearchBox = () => { const intersected = useMarketplaceContext(v => v.intersected) - const [searchText, setSearchText] = useState('') - const [selectedTags, setSelectedTags] = useState([]) - - const handleTagsChange = useCallback((tags: string[]) => { - setSelectedTags(tags) - onChange?.(searchText, tags) - }, [searchText, onChange]) + const searchPluginText = useMarketplaceContext(v => v.searchPluginText) + const handleSearchPluginTextChange = useMarketplaceContext(v => v.handleSearchPluginTextChange) return (
- +
{ - setSearchText(e.target.value) - onChange?.(e.target.value, selectedTags) + handleSearchPluginTextChange(e.target.value) }} /> { - searchText && ( - setSearchText('')}> + searchPluginText && ( + handleSearchPluginTextChange('')}> ) diff --git a/web/app/components/plugins/marketplace/search-box/tags-filter.tsx b/web/app/components/plugins/marketplace/search-box/tags-filter.tsx index 30337567b3..95d806c43b 100644 --- a/web/app/components/plugins/marketplace/search-box/tags-filter.tsx +++ b/web/app/components/plugins/marketplace/search-box/tags-filter.tsx @@ -6,6 +6,7 @@ import { RiCloseCircleFill, RiFilter3Line, } from '@remixicon/react' +import { useMarketplaceContext } from '../context' import { PortalToFollowElem, PortalToFollowElemContent, @@ -15,14 +16,9 @@ import Checkbox from '@/app/components/base/checkbox' import cn from '@/utils/classnames' import Input from '@/app/components/base/input' -type TagsFilterProps = { - value: string[] - onChange: (tags: string[]) => void -} -const TagsFilter = ({ - value, - onChange, -}: TagsFilterProps) => { +const TagsFilter = () => { + const filterPluginTags = useMarketplaceContext(v => v.filterPluginTags) + const handleFilterPluginTagsChange = useMarketplaceContext(v => v.handleFilterPluginTagsChange) const [open, setOpen] = useState(false) const [searchText, setSearchText] = useState('') const options = [ @@ -37,12 +33,12 @@ const TagsFilter = ({ ] const filteredOptions = options.filter(option => option.text.toLowerCase().includes(searchText.toLowerCase())) const handleCheck = (id: string) => { - if (value.includes(id)) - onChange(value.filter(tag => tag !== id)) + if (filterPluginTags.includes(id)) + handleFilterPluginTagsChange(filterPluginTags.filter((tag: string) => tag !== id)) else - onChange([...value, id]) + handleFilterPluginTagsChange([...filterPluginTags, id]) } - const selectedTagsLength = value.length + const selectedTagsLength = filterPluginTags.length return ( 2 && ( @@ -84,7 +80,7 @@ const TagsFilter = ({ !!selectedTagsLength && ( onChange([])} + onClick={() => handleFilterPluginTagsChange([])} /> ) } @@ -115,7 +111,7 @@ const TagsFilter = ({ >
{option.text} diff --git a/web/app/components/plugins/marketplace/types.ts b/web/app/components/plugins/marketplace/types.ts index 6af481cfb8..eea19b374c 100644 --- a/web/app/components/plugins/marketplace/types.ts +++ b/web/app/components/plugins/marketplace/types.ts @@ -17,3 +17,13 @@ export type MarketplaceCollectionPluginsResponse = { plugins: Plugin[] total: number } + +export type PluginsSearchParams = { + query: string + page?: number + pageSize?: number + sortBy?: string + sortOrder?: string + category?: string + tag?: string +} diff --git a/web/app/components/tools/marketplace.tsx b/web/app/components/tools/marketplace.tsx deleted file mode 100644 index 9608f4ac69..0000000000 --- a/web/app/components/tools/marketplace.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { RiArrowUpDoubleLine } from '@remixicon/react' -import Card from '@/app/components/plugins/card' -import CardMoreInfo from '@/app/components/plugins/card/card-more-info' -import { toolNotion } from '@/app/components/plugins/card/card-mock' -import { useGetLanguage } from '@/context/i18n' - -type MarketplaceProps = { - onMarketplaceScroll: () => void -} -const Marketplace = ({ - onMarketplaceScroll, -}: MarketplaceProps) => { - const locale = useGetLanguage() - - return ( -
- onMarketplaceScroll()} - /> -
-
More from Marketplace
-
- Discover - - models - - , - - tools - - , - - extensions - - and - - bundles - - in Dify Marketplace -
-
-
-
Featured
-
Our top picks to get you started
-
- - } - /> - - } - /> - - } - /> - - } - /> - - } - /> -
-
-
-
Popular
-
Explore the library and discover the incredible work of our community
-
- - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> -
-
-
- ) -} - -export default Marketplace diff --git a/web/app/components/tools/marketplace/hooks.ts b/web/app/components/tools/marketplace/hooks.ts new file mode 100644 index 0000000000..8f89aadfda --- /dev/null +++ b/web/app/components/tools/marketplace/hooks.ts @@ -0,0 +1,35 @@ +import { + useCallback, + useEffect, + useState, +} from 'react' +import type { Plugin } from '@/app/components/plugins/types' +import type { MarketplaceCollection } from '@/app/components/plugins/marketplace/types' + +export const useMarketplace = () => { + const [marketplaceCollections, setMarketplaceCollections] = useState([]) + const [marketplaceCollectionPluginsMap, setMarketplaceCollectionPluginsMap] = useState>({}) + const getMarketplaceCollections = useCallback(async () => { + const marketplaceCollectionsData = await globalThis.fetch('https://marketplace.dify.dev/api/v1/collections') + const marketplaceCollectionsDataJson = await marketplaceCollectionsData.json() + const marketplaceCollections = marketplaceCollectionsDataJson.data.collections + const marketplaceCollectionPluginsMap = {} as Record + await Promise.all(marketplaceCollections.map(async (collection: MarketplaceCollection) => { + const marketplaceCollectionPluginsData = await globalThis.fetch(`https://marketplace.dify.dev/api/v1/collections/${collection.name}/plugins`) + const marketplaceCollectionPluginsDataJson = await marketplaceCollectionPluginsData.json() + const plugins = marketplaceCollectionPluginsDataJson.data.plugins + + marketplaceCollectionPluginsMap[collection.name] = plugins + })) + setMarketplaceCollections(marketplaceCollections) + setMarketplaceCollectionPluginsMap(marketplaceCollectionPluginsMap) + }, []) + useEffect(() => { + getMarketplaceCollections() + }, [getMarketplaceCollections]) + + return { + marketplaceCollections, + marketplaceCollectionPluginsMap, + } +} diff --git a/web/app/components/tools/marketplace/index.tsx b/web/app/components/tools/marketplace/index.tsx new file mode 100644 index 0000000000..1d1a4c3c88 --- /dev/null +++ b/web/app/components/tools/marketplace/index.tsx @@ -0,0 +1,48 @@ +import { RiArrowUpDoubleLine } from '@remixicon/react' +import { useMarketplace } from './hooks' +import List from '@/app/components/plugins/marketplace/list' + +type MarketplaceProps = { + onMarketplaceScroll: () => void +} +const Marketplace = ({ + onMarketplaceScroll, +}: MarketplaceProps) => { + const { marketplaceCollections, marketplaceCollectionPluginsMap } = useMarketplace() + return ( +
+ onMarketplaceScroll()} + /> +
+
More from Marketplace
+
+ Discover + + models + + , + + tools + + , + + extensions + + and + + bundles + + in Dify Marketplace +
+
+ +
+ ) +} + +export default Marketplace