diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx index b695302965..fa76ed7b4d 100644 --- a/web/app/components/billing/plan/index.tsx +++ b/web/app/components/billing/plan/index.tsx @@ -1,8 +1,8 @@ 'use client' import type { FC } from 'react' -import React from 'react' +import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { useRouter } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { RiBook2Line, RiFileEditLine, @@ -25,6 +25,8 @@ import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/con import { useEducationVerify } from '@/service/use-education' import { useModalContextSelector } from '@/context/modal-context' import { Enterprise, Professional, Sandbox, Team } from './assets' +import { Loading } from '../../base/icons/src/public/thought' +import { useUnmountedRef } from 'ahooks' type Props = { loc: string @@ -35,6 +37,7 @@ const PlanComp: FC = ({ }) => { const { t } = useTranslation() const router = useRouter() + const path = usePathname() const { userProfile } = useAppContext() const { plan, enableEducationPlan, allowRefreshEducationVerify, isEducationAccount } = useProviderContext() const isAboutToExpire = allowRefreshEducationVerify @@ -61,17 +64,24 @@ const PlanComp: FC = ({ })() const [showModal, setShowModal] = React.useState(false) - const { mutateAsync } = useEducationVerify() + const { mutateAsync, isPending } = useEducationVerify() const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal) + const unmountedRef = useUnmountedRef() const handleVerify = () => { + if (isPending) return mutateAsync().then((res) => { localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM) + if (unmountedRef.current) return router.push(`/education-apply?token=${res.token}`) - setShowAccountSettingModal(null) }).catch(() => { setShowModal(true) }) } + useEffect(() => { + // setShowAccountSettingModal would prevent navigation + if (path.startsWith('/education-apply')) + setShowAccountSettingModal(null) + }, [path, setShowAccountSettingModal]) return (
@@ -96,9 +106,10 @@ const PlanComp: FC = ({
{enableEducationPlan && (!isEducationAccount || isAboutToExpire) && ( - )} {(plan.type as any) !== SelfHostedPlan.enterprise && ( diff --git a/web/app/components/header/account-setting/data-source-page-new/hooks/use-marketplace-all-plugins.ts b/web/app/components/header/account-setting/data-source-page-new/hooks/use-marketplace-all-plugins.ts index 01790d7002..0c2154210c 100644 --- a/web/app/components/header/account-setting/data-source-page-new/hooks/use-marketplace-all-plugins.ts +++ b/web/app/components/header/account-setting/data-source-page-new/hooks/use-marketplace-all-plugins.ts @@ -1,39 +1,28 @@ import { - useCallback, useEffect, useMemo, - useState, } from 'react' import { useMarketplacePlugins, + useMarketplacePluginsByCollectionId, } from '@/app/components/plugins/marketplace/hooks' -import type { Plugin } from '@/app/components/plugins/types' import { PluginCategoryEnum } from '@/app/components/plugins/types' -import { getMarketplacePluginsByCollectionId } from '@/app/components/plugins/marketplace/utils' export const useMarketplaceAllPlugins = (providers: any[], searchText: string) => { const exclude = useMemo(() => { return providers.map(provider => provider.plugin_id) }, [providers]) - const [collectionPlugins, setCollectionPlugins] = useState([]) - + const { + plugins: collectionPlugins = [], + isLoading: isCollectionLoading, + } = useMarketplacePluginsByCollectionId('__datasource-settings-pinned-datasources') const { plugins, queryPlugins, queryPluginsWithDebounced, - isLoading, + isLoading: isPluginsLoading, } = useMarketplacePlugins() - const getCollectionPlugins = useCallback(async () => { - const collectionPlugins = await getMarketplacePluginsByCollectionId('__datasource-settings-pinned-datasources') - - setCollectionPlugins(collectionPlugins) - }, []) - - useEffect(() => { - getCollectionPlugins() - }, [getCollectionPlugins]) - useEffect(() => { if (searchText) { queryPluginsWithDebounced({ @@ -75,6 +64,6 @@ export const useMarketplaceAllPlugins = (providers: any[], searchText: string) = return { plugins: allPlugins, - isLoading, + isLoading: isCollectionLoading || isPluginsLoading, } } diff --git a/web/app/components/header/account-setting/model-provider-page/hooks.ts b/web/app/components/header/account-setting/model-provider-page/hooks.ts index 8cfd144681..0ffd1df9de 100644 --- a/web/app/components/header/account-setting/model-provider-page/hooks.ts +++ b/web/app/components/header/account-setting/model-provider-page/hooks.ts @@ -33,10 +33,9 @@ import { import { useProviderContext } from '@/context/provider-context' import { useMarketplacePlugins, + useMarketplacePluginsByCollectionId, } from '@/app/components/plugins/marketplace/hooks' -import type { Plugin } from '@/app/components/plugins/types' import { PluginCategoryEnum } from '@/app/components/plugins/types' -import { getMarketplacePluginsByCollectionId } from '@/app/components/plugins/marketplace/utils' import { useModalContextSelector } from '@/context/modal-context' import { useEventEmitterContextContext } from '@/context/event-emitter' import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card' @@ -255,25 +254,17 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText: const exclude = useMemo(() => { return providers.map(provider => provider.provider.replace(/(.+)\/([^/]+)$/, '$1')) }, [providers]) - const [collectionPlugins, setCollectionPlugins] = useState([]) - + const { + plugins: collectionPlugins = [], + isLoading: isCollectionLoading, + } = useMarketplacePluginsByCollectionId('__model-settings-pinned-models') const { plugins, queryPlugins, queryPluginsWithDebounced, - isLoading, + isLoading: isPluginsLoading, } = useMarketplacePlugins() - const getCollectionPlugins = useCallback(async () => { - const collectionPlugins = await getMarketplacePluginsByCollectionId('__model-settings-pinned-models') - - setCollectionPlugins(collectionPlugins) - }, []) - - useEffect(() => { - getCollectionPlugins() - }, [getCollectionPlugins]) - useEffect(() => { if (searchText) { queryPluginsWithDebounced({ @@ -315,7 +306,7 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText: return { plugins: allPlugins, - isLoading, + isLoading: isCollectionLoading || isPluginsLoading, } } diff --git a/web/app/components/plugins/marketplace/constants.ts b/web/app/components/plugins/marketplace/constants.ts index 6bd4e29604..92c3e7278f 100644 --- a/web/app/components/plugins/marketplace/constants.ts +++ b/web/app/components/plugins/marketplace/constants.ts @@ -2,3 +2,5 @@ export const DEFAULT_SORT = { sortBy: 'install_count', sortOrder: 'DESC', } + +export const SCROLL_BOTTOM_THRESHOLD = 100 diff --git a/web/app/components/plugins/marketplace/context.tsx b/web/app/components/plugins/marketplace/context.tsx index 248e035c1b..fca8085271 100644 --- a/web/app/components/plugins/marketplace/context.tsx +++ b/web/app/components/plugins/marketplace/context.tsx @@ -50,7 +50,7 @@ export type MarketplaceContextValue = { activePluginType: string handleActivePluginTypeChange: (type: string) => void page: number - handlePageChange: (page: number) => void + handlePageChange: () => void plugins?: Plugin[] pluginsTotal?: number resetPlugins: () => void @@ -128,8 +128,6 @@ export const MarketplaceContextProvider = ({ const filterPluginTagsRef = useRef(filterPluginTags) const [activePluginType, setActivePluginType] = useState(categoryFromSearchParams) const activePluginTypeRef = useRef(activePluginType) - const [page, setPage] = useState(1) - const pageRef = useRef(page) const [sort, setSort] = useState(DEFAULT_SORT) const sortRef = useRef(sort) const { @@ -149,7 +147,11 @@ export const MarketplaceContextProvider = ({ queryPluginsWithDebounced, cancelQueryPluginsWithDebounced, isLoading: isPluginsLoading, + fetchNextPage: fetchNextPluginsPage, + hasNextPage: hasNextPluginsPage, + page: pluginsPage, } = useMarketplacePlugins() + const page = Math.max(pluginsPage || 0, 1) useEffect(() => { if (queryFromSearchParams || hasValidTags || hasValidCategory) { @@ -160,7 +162,6 @@ export const MarketplaceContextProvider = ({ sortBy: sortRef.current.sortBy, sortOrder: sortRef.current.sortOrder, type: getMarketplaceListFilterType(activePluginTypeRef.current), - page: pageRef.current, }) const url = new URL(window.location.href) if (searchParams?.language) @@ -221,7 +222,6 @@ export const MarketplaceContextProvider = ({ sortOrder: sortRef.current.sortOrder, exclude, type: getMarketplaceListFilterType(activePluginTypeRef.current), - page: pageRef.current, }) } else { @@ -233,7 +233,6 @@ export const MarketplaceContextProvider = ({ sortOrder: sortRef.current.sortOrder, exclude, type: getMarketplaceListFilterType(activePluginTypeRef.current), - page: pageRef.current, }) } }, [exclude, queryPluginsWithDebounced, queryPlugins, handleUpdateSearchParams]) @@ -252,8 +251,6 @@ export const MarketplaceContextProvider = ({ const handleSearchPluginTextChange = useCallback((text: string) => { setSearchPluginText(text) searchPluginTextRef.current = text - setPage(1) - pageRef.current = 1 handleQuery(true) }, [handleQuery]) @@ -261,8 +258,6 @@ export const MarketplaceContextProvider = ({ const handleFilterPluginTagsChange = useCallback((tags: string[]) => { setFilterPluginTags(tags) filterPluginTagsRef.current = tags - setPage(1) - pageRef.current = 1 handleQuery() }, [handleQuery]) @@ -270,8 +265,6 @@ export const MarketplaceContextProvider = ({ const handleActivePluginTypeChange = useCallback((type: string) => { setActivePluginType(type) activePluginTypeRef.current = type - setPage(1) - pageRef.current = 1 handleQuery() }, [handleQuery]) @@ -279,20 +272,14 @@ export const MarketplaceContextProvider = ({ const handleSortChange = useCallback((sort: PluginsSort) => { setSort(sort) sortRef.current = sort - setPage(1) - pageRef.current = 1 handleQueryPlugins() }, [handleQueryPlugins]) const handlePageChange = useCallback(() => { - if (pluginsTotal && plugins && pluginsTotal > plugins.length) { - setPage(pageRef.current + 1) - pageRef.current++ - - handleQueryPlugins() - } - }, [handleQueryPlugins, plugins, pluginsTotal]) + if (hasNextPluginsPage) + fetchNextPluginsPage() + }, [fetchNextPluginsPage, hasNextPluginsPage]) const handleMoreClick = useCallback((searchParams: SearchParamsFromCollection) => { setSearchPluginText(searchParams?.query || '') @@ -305,9 +292,6 @@ export const MarketplaceContextProvider = ({ sortBy: searchParams?.sort_by || DEFAULT_SORT.sortBy, sortOrder: searchParams?.sort_order || DEFAULT_SORT.sortOrder, } - setPage(1) - pageRef.current = 1 - handleQueryPlugins() }, [handleQueryPlugins]) diff --git a/web/app/components/plugins/marketplace/hooks.ts b/web/app/components/plugins/marketplace/hooks.ts index 5bc9263aaa..49b07d7af6 100644 --- a/web/app/components/plugins/marketplace/hooks.ts +++ b/web/app/components/plugins/marketplace/hooks.ts @@ -3,6 +3,11 @@ import { useEffect, useState, } from 'react' +import { + useInfiniteQuery, + useQuery, + useQueryClient, +} from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { useDebounceFn } from 'ahooks' import type { @@ -16,39 +21,41 @@ import type { import { getFormattedPlugin, getMarketplaceCollectionsAndPlugins, + getMarketplacePluginsByCollectionId, } from './utils' +import { SCROLL_BOTTOM_THRESHOLD } from './constants' import i18n from '@/i18n-config/i18next-config' -import { - useMutationPluginsFromMarketplace, -} from '@/service/use-plugins' +import { postMarketplace } from '@/service/base' +import type { PluginsFromMarketplaceResponse } from '@/app/components/plugins/types' export const useMarketplaceCollectionsAndPlugins = () => { - const [isLoading, setIsLoading] = useState(false) - const [isSuccess, setIsSuccess] = useState(false) - const [marketplaceCollections, setMarketplaceCollections] = useState() - const [marketplaceCollectionPluginsMap, setMarketplaceCollectionPluginsMap] = useState>() + const [queryParams, setQueryParams] = useState() + const [marketplaceCollectionsOverride, setMarketplaceCollections] = useState() + const [marketplaceCollectionPluginsMapOverride, setMarketplaceCollectionPluginsMap] = useState>() - const queryMarketplaceCollectionsAndPlugins = useCallback(async (query?: CollectionsAndPluginsSearchParams) => { - try { - setIsLoading(true) - setIsSuccess(false) - const { marketplaceCollections, marketplaceCollectionPluginsMap } = await getMarketplaceCollectionsAndPlugins(query) - setIsLoading(false) - setIsSuccess(true) - setMarketplaceCollections(marketplaceCollections) - setMarketplaceCollectionPluginsMap(marketplaceCollectionPluginsMap) - } - // eslint-disable-next-line unused-imports/no-unused-vars - catch (e) { - setIsLoading(false) - setIsSuccess(false) - } + const { + data, + isFetching, + isSuccess, + isPending, + } = useQuery({ + queryKey: ['marketplaceCollectionsAndPlugins', queryParams], + queryFn: ({ signal }) => getMarketplaceCollectionsAndPlugins(queryParams, { signal }), + enabled: queryParams !== undefined, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + retry: false, + }) + + const queryMarketplaceCollectionsAndPlugins = useCallback((query?: CollectionsAndPluginsSearchParams) => { + setQueryParams(query ? { ...query } : {}) }, []) + const isLoading = !!queryParams && (isFetching || isPending) return { - marketplaceCollections, + marketplaceCollections: marketplaceCollectionsOverride ?? data?.marketplaceCollections, setMarketplaceCollections, - marketplaceCollectionPluginsMap, + marketplaceCollectionPluginsMap: marketplaceCollectionPluginsMapOverride ?? data?.marketplaceCollectionPluginsMap, setMarketplaceCollectionPluginsMap, queryMarketplaceCollectionsAndPlugins, isLoading, @@ -56,37 +63,128 @@ export const useMarketplaceCollectionsAndPlugins = () => { } } -export const useMarketplacePlugins = () => { +export const useMarketplacePluginsByCollectionId = ( + collectionId?: string, + query?: CollectionsAndPluginsSearchParams, +) => { const { data, - mutateAsync, - reset, + isFetching, + isSuccess, isPending, - } = useMutationPluginsFromMarketplace() + } = useQuery({ + queryKey: ['marketplaceCollectionPlugins', collectionId, query], + queryFn: ({ signal }) => { + if (!collectionId) + return Promise.resolve([]) + return getMarketplacePluginsByCollectionId(collectionId, query, { signal }) + }, + enabled: !!collectionId, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + retry: false, + }) - const [prevPlugins, setPrevPlugins] = useState() + return { + plugins: data || [], + isLoading: !!collectionId && (isFetching || isPending), + isSuccess, + } +} + +export const useMarketplacePlugins = () => { + const queryClient = useQueryClient() + const [queryParams, setQueryParams] = useState() + + const normalizeParams = useCallback((pluginsSearchParams: PluginsSearchParams) => { + const pageSize = pluginsSearchParams.pageSize || 40 + + return { + ...pluginsSearchParams, + pageSize, + } + }, []) + + const marketplacePluginsQuery = useInfiniteQuery({ + queryKey: ['marketplacePlugins', queryParams], + queryFn: async ({ pageParam = 1, signal }) => { + if (!queryParams) { + return { + plugins: [] as Plugin[], + total: 0, + page: 1, + pageSize: 40, + } + } + + const params = normalizeParams(queryParams) + const { + query, + sortBy, + sortOrder, + category, + tags, + exclude, + type, + pageSize, + } = params + const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins' + + try { + const res = await postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, { + body: { + page: pageParam, + page_size: pageSize, + query, + sort_by: sortBy, + sort_order: sortOrder, + category: category !== 'all' ? category : '', + tags, + exclude, + type, + }, + signal, + }) + const resPlugins = res.data.bundles || res.data.plugins || [] + + return { + plugins: resPlugins.map(plugin => getFormattedPlugin(plugin)), + total: res.data.total, + page: pageParam, + pageSize, + } + } + catch { + return { + plugins: [], + total: 0, + page: pageParam, + pageSize, + } + } + }, + getNextPageParam: (lastPage) => { + const nextPage = lastPage.page + 1 + const loaded = lastPage.page * lastPage.pageSize + return loaded < (lastPage.total || 0) ? nextPage : undefined + }, + initialPageParam: 1, + enabled: !!queryParams, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + retry: false, + }) const resetPlugins = useCallback(() => { - reset() - setPrevPlugins(undefined) - }, [reset]) + setQueryParams(undefined) + queryClient.removeQueries({ + queryKey: ['marketplacePlugins'], + }) + }, [queryClient]) const handleUpdatePlugins = useCallback((pluginsSearchParams: PluginsSearchParams) => { - mutateAsync(pluginsSearchParams).then((res) => { - const currentPage = pluginsSearchParams.page || 1 - const resPlugins = res.data.bundles || res.data.plugins - if (currentPage > 1) { - setPrevPlugins(prevPlugins => [...(prevPlugins || []), ...resPlugins.map((plugin) => { - return getFormattedPlugin(plugin) - })]) - } - else { - setPrevPlugins(resPlugins.map((plugin) => { - return getFormattedPlugin(plugin) - })) - } - }) - }, [mutateAsync]) + setQueryParams(normalizeParams(pluginsSearchParams)) + }, [normalizeParams]) const { run: queryPluginsWithDebounced, cancel: cancelQueryPluginsWithDebounced } = useDebounceFn((pluginsSearchParams: PluginsSearchParams) => { handleUpdatePlugins(pluginsSearchParams) @@ -94,14 +192,29 @@ export const useMarketplacePlugins = () => { wait: 500, }) + const hasQuery = !!queryParams + const hasData = marketplacePluginsQuery.data !== undefined + const plugins = hasQuery && hasData + ? marketplacePluginsQuery.data.pages.flatMap(page => page.plugins) + : undefined + const total = hasQuery && hasData ? marketplacePluginsQuery.data.pages?.[0]?.total : undefined + const isPluginsLoading = hasQuery && ( + marketplacePluginsQuery.isPending + || (marketplacePluginsQuery.isFetching && !marketplacePluginsQuery.data) + ) + return { - plugins: prevPlugins, - total: data?.data?.total, + plugins, + total, resetPlugins, queryPlugins: handleUpdatePlugins, queryPluginsWithDebounced, cancelQueryPluginsWithDebounced, - isLoading: isPending, + isLoading: isPluginsLoading, + isFetchingNextPage: marketplacePluginsQuery.isFetchingNextPage, + hasNextPage: marketplacePluginsQuery.hasNextPage, + fetchNextPage: marketplacePluginsQuery.fetchNextPage, + page: marketplacePluginsQuery.data?.pages?.length || (marketplacePluginsQuery.isPending && hasQuery ? 1 : 0), } } @@ -131,7 +244,7 @@ export const useMarketplaceContainerScroll = ( scrollHeight, clientHeight, } = target - if (scrollTop + clientHeight >= scrollHeight - 5 && scrollTop > 0) + if (scrollTop + clientHeight >= scrollHeight - SCROLL_BOTTOM_THRESHOLD && scrollTop > 0) callback() }, [callback]) diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx index d6189a92a1..800c096639 100644 --- a/web/app/components/plugins/marketplace/index.tsx +++ b/web/app/components/plugins/marketplace/index.tsx @@ -4,7 +4,8 @@ import IntersectionLine from './intersection-line' import SearchBoxWrapper from './search-box/search-box-wrapper' import PluginTypeSwitch from './plugin-type-switch' import ListWrapper from './list/list-wrapper' -import type { SearchParams } from './types' +import type { MarketplaceCollection, SearchParams } from './types' +import type { Plugin } from '@/app/components/plugins/types' import { getMarketplaceCollectionsAndPlugins } from './utils' import { TanstackQueryInitializer } from '@/context/query-client' @@ -30,8 +31,8 @@ const Marketplace = async ({ scrollContainerId, showSearchParams = true, }: MarketplaceProps) => { - let marketplaceCollections: any = [] - let marketplaceCollectionPluginsMap = {} + let marketplaceCollections: MarketplaceCollection[] = [] + let marketplaceCollectionPluginsMap: Record = {} if (!shouldExclude) { const marketplaceCollectionsAndPluginsData = await getMarketplaceCollectionsAndPlugins() marketplaceCollections = marketplaceCollectionsAndPluginsData.marketplaceCollections diff --git a/web/app/components/plugins/marketplace/list/list-wrapper.tsx b/web/app/components/plugins/marketplace/list/list-wrapper.tsx index fa6521bcfd..908c9c4406 100644 --- a/web/app/components/plugins/marketplace/list/list-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/list-wrapper.tsx @@ -28,13 +28,20 @@ const ListWrapper = ({ const isLoading = useMarketplaceContext(v => v.isLoading) const isSuccessCollections = useMarketplaceContext(v => v.isSuccessCollections) const handleQueryPlugins = useMarketplaceContext(v => v.handleQueryPlugins) + const searchPluginText = useMarketplaceContext(v => v.searchPluginText) + const filterPluginTags = useMarketplaceContext(v => v.filterPluginTags) const page = useMarketplaceContext(v => v.page) const handleMoreClick = useMarketplaceContext(v => v.handleMoreClick) useEffect(() => { - if (!marketplaceCollectionsFromClient?.length && isSuccessCollections) + if ( + !marketplaceCollectionsFromClient?.length + && isSuccessCollections + && !searchPluginText + && !filterPluginTags.length + ) handleQueryPlugins() - }, [handleQueryPlugins, marketplaceCollections, marketplaceCollectionsFromClient, isSuccessCollections]) + }, [handleQueryPlugins, marketplaceCollections, marketplaceCollectionsFromClient, isSuccessCollections, searchPluginText, filterPluginTags]) return (
new Headers({ + 'X-Dify-Version': !IS_MARKETPLACE ? APP_VERSION : '999.0.0', +}) + export const getPluginIconInMarketplace = (plugin: Plugin) => { if (plugin.type === 'bundle') return `${MARKETPLACE_API_PREFIX}/bundles/${plugin.org}/${plugin.name}/icon` @@ -46,20 +54,23 @@ export const getPluginDetailLinkInMarketplace = (plugin: Plugin) => { return `/plugins/${plugin.org}/${plugin.name}` } -export const getMarketplacePluginsByCollectionId = async (collectionId: string, query?: CollectionsAndPluginsSearchParams) => { - let plugins: Plugin[] +export const getMarketplacePluginsByCollectionId = async ( + collectionId: string, + query?: CollectionsAndPluginsSearchParams, + options?: MarketplaceFetchOptions, +) => { + let plugins: Plugin[] = [] try { const url = `${MARKETPLACE_API_PREFIX}/collections/${collectionId}/plugins` - const headers = new Headers({ - 'X-Dify-Version': !IS_MARKETPLACE ? APP_VERSION : '999.0.0', - }) + const headers = getMarketplaceHeaders() const marketplaceCollectionPluginsData = await globalThis.fetch( url, { cache: 'no-store', method: 'POST', headers, + signal: options?.signal, body: JSON.stringify({ category: query?.category, exclude: query?.exclude, @@ -68,9 +79,7 @@ export const getMarketplacePluginsByCollectionId = async (collectionId: string, }, ) const marketplaceCollectionPluginsDataJson = await marketplaceCollectionPluginsData.json() - plugins = marketplaceCollectionPluginsDataJson.data.plugins.map((plugin: Plugin) => { - return getFormattedPlugin(plugin) - }) + plugins = (marketplaceCollectionPluginsDataJson.data.plugins || []).map((plugin: Plugin) => getFormattedPlugin(plugin)) } // eslint-disable-next-line unused-imports/no-unused-vars catch (e) { @@ -80,23 +89,31 @@ export const getMarketplacePluginsByCollectionId = async (collectionId: string, return plugins } -export const getMarketplaceCollectionsAndPlugins = async (query?: CollectionsAndPluginsSearchParams) => { - let marketplaceCollections = [] as MarketplaceCollection[] - let marketplaceCollectionPluginsMap = {} as Record +export const getMarketplaceCollectionsAndPlugins = async ( + query?: CollectionsAndPluginsSearchParams, + options?: MarketplaceFetchOptions, +) => { + let marketplaceCollections: MarketplaceCollection[] = [] + let marketplaceCollectionPluginsMap: Record = {} try { let marketplaceUrl = `${MARKETPLACE_API_PREFIX}/collections?page=1&page_size=100` if (query?.condition) marketplaceUrl += `&condition=${query.condition}` if (query?.type) marketplaceUrl += `&type=${query.type}` - const headers = new Headers({ - 'X-Dify-Version': !IS_MARKETPLACE ? APP_VERSION : '999.0.0', - }) - const marketplaceCollectionsData = await globalThis.fetch(marketplaceUrl, { headers, cache: 'no-store' }) + const headers = getMarketplaceHeaders() + const marketplaceCollectionsData = await globalThis.fetch( + marketplaceUrl, + { + headers, + cache: 'no-store', + signal: options?.signal, + }, + ) const marketplaceCollectionsDataJson = await marketplaceCollectionsData.json() - marketplaceCollections = marketplaceCollectionsDataJson.data.collections + marketplaceCollections = marketplaceCollectionsDataJson.data.collections || [] await Promise.all(marketplaceCollections.map(async (collection: MarketplaceCollection) => { - const plugins = await getMarketplacePluginsByCollectionId(collection.name, query) + const plugins = await getMarketplacePluginsByCollectionId(collection.name, query, options) marketplaceCollectionPluginsMap[collection.name] = plugins })) diff --git a/web/app/components/react-scan.tsx b/web/app/components/react-scan.tsx new file mode 100644 index 0000000000..03577b7d2b --- /dev/null +++ b/web/app/components/react-scan.tsx @@ -0,0 +1,22 @@ +'use client' + +import { scan } from 'react-scan' +import { useEffect } from 'react' +import { IS_DEV } from '@/config' + +export function ReactScan() { + useEffect(() => { + if (IS_DEV) { + scan({ + enabled: true, + // HACK: react-scan's getIsProduction() incorrectly detects Next.js dev as production + // because Next.js devtools overlay uses production React build + // Issue: https://github.com/aidenybai/react-scan/issues/402 + // TODO: remove this option after upstream fix + dangerouslyForceRunInProduction: true, + }) + } + }, []) + + return null +} diff --git a/web/app/components/tools/marketplace/hooks.ts b/web/app/components/tools/marketplace/hooks.ts index e3fad24710..904eeb95a8 100644 --- a/web/app/components/tools/marketplace/hooks.ts +++ b/web/app/components/tools/marketplace/hooks.ts @@ -3,12 +3,12 @@ import { useEffect, useMemo, useRef, - useState, } from 'react' import { useMarketplaceCollectionsAndPlugins, useMarketplacePlugins, } from '@/app/components/plugins/marketplace/hooks' +import { SCROLL_BOTTOM_THRESHOLD } from '@/app/components/plugins/marketplace/constants' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { getMarketplaceListCondition } from '@/app/components/plugins/marketplace/utils' import { useAllToolProviders } from '@/service/use-tools' @@ -31,10 +31,10 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin queryPlugins, queryPluginsWithDebounced, isLoading: isPluginsLoading, - total: pluginsTotal, + fetchNextPage, + hasNextPage, + page: pluginsPage, } = useMarketplacePlugins() - const [page, setPage] = useState(1) - const pageRef = useRef(page) const searchPluginTextRef = useRef(searchPluginText) const filterPluginTagsRef = useRef(filterPluginTags) @@ -44,9 +44,6 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin }, [searchPluginText, filterPluginTags]) useEffect(() => { if ((searchPluginText || filterPluginTags.length) && isSuccess) { - setPage(1) - pageRef.current = 1 - if (searchPluginText) { queryPluginsWithDebounced({ category: PluginCategoryEnum.tool, @@ -54,7 +51,6 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin tags: filterPluginTags, exclude, type: 'plugin', - page: pageRef.current, }) return } @@ -64,7 +60,6 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin tags: filterPluginTags, exclude, type: 'plugin', - page: pageRef.current, }) } else { @@ -87,24 +82,13 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin scrollHeight, clientHeight, } = target - if (scrollTop + clientHeight >= scrollHeight - 5 && scrollTop > 0) { + if (scrollTop + clientHeight >= scrollHeight - SCROLL_BOTTOM_THRESHOLD && scrollTop > 0) { const searchPluginText = searchPluginTextRef.current const filterPluginTags = filterPluginTagsRef.current - if (pluginsTotal && plugins && pluginsTotal > plugins.length && (!!searchPluginText || !!filterPluginTags.length)) { - setPage(pageRef.current + 1) - pageRef.current++ - - queryPlugins({ - category: PluginCategoryEnum.tool, - query: searchPluginText, - tags: filterPluginTags, - exclude, - type: 'plugin', - page: pageRef.current, - }) - } + if (hasNextPage && (!!searchPluginText || !!filterPluginTags.length)) + fetchNextPage() } - }, [exclude, plugins, pluginsTotal, queryPlugins]) + }, [exclude, fetchNextPage, hasNextPage, plugins, queryPlugins]) return { isLoading: isLoading || isPluginsLoading, @@ -112,6 +96,6 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin marketplaceCollectionPluginsMap, plugins, handleScroll, - page, + page: Math.max(pluginsPage || 0, 1), } } diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 011defe466..878f335b92 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,3 +1,4 @@ +import { ReactScan } from './components/react-scan' import RoutePrefixHandle from './routePrefixHandle' import type { Viewport } from 'next' import I18nServer from './components/i18n-server' @@ -86,6 +87,7 @@ const LocaleLayout = async ({ className='color-scheme h-full select-auto' {...datasetMap} > + - fetch(url).then(r => r.json()) - export const CompletionParams = ['temperature', 'top_p', 'presence_penalty', 'max_token', 'stop', 'frequency_penalty'] as const export type CompletionParamType = typeof CompletionParams[number] diff --git a/web/models/user.ts b/web/models/user.ts deleted file mode 100644 index 5451980902..0000000000 --- a/web/models/user.ts +++ /dev/null @@ -1,17 +0,0 @@ -export type User = { - id: string - firstName: string - lastName: string - name: string - phone: string - username: string - email: string - avatar: string -} - -export type UserResponse = { - users: User[] -} - -export const fetchUsers = (url: string) => - fetch(url).then(r => r.json()) diff --git a/web/package.json b/web/package.json index 11b2165ac7..07d1c97b3f 100644 --- a/web/package.json +++ b/web/package.json @@ -201,6 +201,7 @@ "lodash": "^4.17.21", "magicast": "^0.3.5", "postcss": "^8.5.6", + "react-scan": "^0.4.3", "sass": "^1.93.2", "storybook": "9.1.13", "tailwindcss": "^3.4.18", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 2d528ecdf2..a973996284 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -125,7 +125,7 @@ importers: version: 3.2.5 '@tailwindcss/typography': specifier: ^0.5.19 - version: 0.5.19(tailwindcss@3.4.18(yaml@2.8.2)) + version: 0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-form': specifier: ^1.23.7 version: 1.27.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -498,7 +498,7 @@ importers: version: 9.1.16(eslint@9.39.1(jiti@1.21.7))(storybook@9.1.13(@testing-library/dom@10.4.1))(typescript@5.9.3) eslint-plugin-tailwindcss: specifier: ^3.18.2 - version: 3.18.2(tailwindcss@3.4.18(yaml@2.8.2)) + version: 3.18.2(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)) globals: specifier: ^15.15.0 version: 15.15.0 @@ -523,6 +523,9 @@ importers: postcss: specifier: ^8.5.6 version: 8.5.6 + react-scan: + specifier: ^0.4.3 + version: 0.4.3(@types/react@19.1.17)(next@15.5.6(@babel/core@7.28.5)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.94.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(rollup@2.79.2) sass: specifier: ^1.93.2 version: 1.94.2 @@ -531,7 +534,7 @@ importers: version: 9.1.13(@testing-library/dom@10.4.1) tailwindcss: specifier: ^3.4.18 - version: 3.4.18(yaml@2.8.2) + version: 3.4.18(tsx@4.21.0)(yaml@2.8.2) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@18.15.0)(typescript@5.9.3) @@ -1349,12 +1352,18 @@ packages: peerDependencies: storybook: ^0.0.0-0 || ^9.0.0 || ^9.1.0-0 || ^9.2.0-0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 + '@clack/core@0.3.5': + resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} + '@clack/core@0.5.0': resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} '@clack/prompts@0.11.0': resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + '@clack/prompts@0.8.2': + resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} + '@code-inspector/core@1.2.9': resolution: {integrity: sha512-A1w+G73HlTB6S8X6sA6tT+ziWHTAcTyH+7FZ1Sgd3ZLXF/E/jT+hgRbKposjXMwxcbodRc6hBG6UyiV+VxwE6Q==} @@ -2591,6 +2600,12 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@pivanov/utils@0.0.2': + resolution: {integrity: sha512-q9CN0bFWxWgMY5hVVYyBgez1jGiLBa6I+LkG37ycylPhFvEGOOeaADGtUSu46CaZasPnlY8fCdVJZmrgKb1EPA==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2627,6 +2642,11 @@ packages: '@preact/signals-core@1.12.1': resolution: {integrity: sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==} + '@preact/signals@1.3.2': + resolution: {integrity: sha512-naxcJgUJ6BTOROJ7C3QML7KvwKwCXQJYTc5L/b0eEsdYgPB6SxwoQ1vDGcS0Q7GVjAenVq/tXrybVdFShHYZWg==} + peerDependencies: + preact: 10.x + '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} @@ -3457,6 +3477,11 @@ packages: peerDependencies: '@types/react': ~19.1.17 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': ~19.1.17 + '@types/react-slider@1.3.6': resolution: {integrity: sha512-RS8XN5O159YQ6tu3tGZIQz1/9StMLTg/FCIPxwqh2gwVixJnlfIodtVx+fpXVMZHe7A58lAX1Q4XTgAGOQaCQg==} @@ -3951,6 +3976,11 @@ packages: bing-translate-api@4.2.0: resolution: {integrity: sha512-7a9yo1NbGcHPS8zXTdz8tCOymHZp2pvCuYOChCaXKjOX8EIwdV3SLd4D7RGIqZt1UhffypYBUcAV2gDcTgK0rA==} + bippy@0.3.34: + resolution: {integrity: sha512-vmptmU/20UdIWHHhq7qCSHhHzK7Ro3YJ1utU0fBG7ujUc58LEfTtilKxcF0IOgSjT5XLcm7CBzDjbv4lcKApGQ==} + peerDependencies: + react: '>=17.0.1' + birecord@0.1.1: resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==} @@ -5374,6 +5404,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -6118,6 +6153,10 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + knip@5.71.0: resolution: {integrity: sha512-hwgdqEJ+7DNJ5jE8BCPu7b57TY7vUwP6MzWYgCgPpg6iPCee/jKPShDNIlFER2koti4oz5xF88VJbKCb4Wl71g==} engines: {node: '>=18.18.0'} @@ -6568,6 +6607,10 @@ packages: monaco-editor@0.55.1: resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -6920,6 +6963,16 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.57.0: + resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -7033,6 +7086,9 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + preact@10.28.0: + resolution: {integrity: sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -7271,6 +7327,26 @@ packages: react: '>=16.3.0' react-dom: '>=16.3.0' + react-scan@0.4.3: + resolution: {integrity: sha512-jhAQuQ1nja6HUYrSpbmNFHqZPsRCXk8Yqu0lHoRIw9eb8N96uTfXCpVyQhTTnJ/nWqnwuvxbpKVG/oWZT8+iTQ==} + hasBin: true + peerDependencies: + '@remix-run/react': '>=1.0.0' + next: '>=13.0.0' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-router: ^5.0.0 || ^6.0.0 || ^7.0.0 + react-router-dom: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@remix-run/react': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + react-slider@2.0.6: resolution: {integrity: sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA==} peerDependencies: @@ -8080,6 +8156,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + tty-browserify@0.0.1: resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} @@ -8188,6 +8269,10 @@ packages: resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} + unplugin@2.1.0: + resolution: {integrity: sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==} + engines: {node: '>=18.12.0'} + upath@1.2.0: resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} engines: {node: '>=4'} @@ -9663,6 +9748,11 @@ snapshots: - '@chromatic-com/cypress' - '@chromatic-com/playwright' + '@clack/core@0.3.5': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@clack/core@0.5.0': dependencies: picocolors: 1.1.1 @@ -9674,6 +9764,12 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@clack/prompts@0.8.2': + dependencies: + '@clack/core': 0.3.5 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@code-inspector/core@1.2.9': dependencies: '@vue/compiler-dom': 3.5.25 @@ -11063,6 +11159,11 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true + '@pivanov/utils@0.0.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + '@pkgr/core@0.2.9': {} '@pmmmwh/react-refresh-webpack-plugin@0.5.17(react-refresh@0.14.2)(type-fest@4.2.0)(webpack-hot-middleware@2.26.1)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3))': @@ -11084,6 +11185,11 @@ snapshots: '@preact/signals-core@1.12.1': {} + '@preact/signals@1.3.2(preact@10.28.0)': + dependencies: + '@preact/signals-core': 1.12.1 + preact: 10.28.0 + '@radix-ui/primitive@1.1.3': {} '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.17)(react@19.1.1)': @@ -11690,10 +11796,10 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(yaml@2.8.2))': + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))': dependencies: postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.18(yaml@2.8.2) + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2) '@tanstack/devtools-event-client@0.3.5': {} @@ -12065,6 +12171,10 @@ snapshots: dependencies: '@types/react': 19.1.17 + '@types/react-reconciler@0.28.9(@types/react@19.1.17)': + dependencies: + '@types/react': 19.1.17 + '@types/react-slider@1.3.6': dependencies: '@types/react': 19.1.17 @@ -12644,6 +12754,13 @@ snapshots: dependencies: got: 11.8.6 + bippy@0.3.34(@types/react@19.1.17)(react@19.1.1): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.1.17) + react: 19.1.1 + transitivePeerDependencies: + - '@types/react' + birecord@0.1.1: {} bl@4.1.0: @@ -13898,11 +14015,11 @@ snapshots: - supports-color - typescript - eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.18(yaml@2.8.2)): + eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)): dependencies: fast-glob: 3.3.3 postcss: 8.5.6 - tailwindcss: 3.4.18(yaml@2.8.2) + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2) eslint-plugin-toml@0.12.0(eslint@9.39.1(jiti@1.21.7)): dependencies: @@ -14307,6 +14424,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -15257,6 +15377,8 @@ snapshots: kleur@3.0.3: {} + kleur@4.1.5: {} + knip@5.71.0(@types/node@18.15.0)(typescript@5.9.3): dependencies: '@nodelib/fs.walk': 1.2.8 @@ -16029,6 +16151,8 @@ snapshots: dompurify: 3.2.7 marked: 14.0.0 + mri@1.2.0: {} + mrmime@2.0.1: {} ms@2.1.3: {} @@ -16414,6 +16538,14 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + playwright-core@1.57.0: {} + + playwright@1.57.0: + dependencies: + playwright-core: 1.57.0 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} pnpm-workspace-yaml@1.3.0: @@ -16446,12 +16578,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.6 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 postcss: 8.5.6 + tsx: 4.21.0 yaml: 2.8.2 postcss-loader@8.2.0(postcss@8.5.6)(typescript@5.9.3)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)): @@ -16520,6 +16653,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact@10.28.0: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -16782,6 +16917,35 @@ snapshots: react-draggable: 4.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) tslib: 2.6.2 + react-scan@0.4.3(@types/react@19.1.17)(next@15.5.6(@babel/core@7.28.5)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.94.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(rollup@2.79.2): + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/types': 7.28.5 + '@clack/core': 0.3.5 + '@clack/prompts': 0.8.2 + '@pivanov/utils': 0.0.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@preact/signals': 1.3.2(preact@10.28.0) + '@rollup/pluginutils': 5.3.0(rollup@2.79.2) + '@types/node': 20.19.25 + bippy: 0.3.34(@types/react@19.1.17)(react@19.1.1) + esbuild: 0.25.0 + estree-walker: 3.0.3 + kleur: 4.1.5 + mri: 1.2.0 + playwright: 1.57.0 + preact: 10.28.0 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + tsx: 4.21.0 + optionalDependencies: + next: 15.5.6(@babel/core@7.28.5)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.94.2) + unplugin: 2.1.0 + transitivePeerDependencies: + - '@types/react' + - rollup + - supports-color + react-slider@2.0.6(react@19.1.1): dependencies: prop-types: 15.8.1 @@ -17536,7 +17700,7 @@ snapshots: tailwind-merge@2.6.0: {} - tailwindcss@3.4.18(yaml@2.8.2): + tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -17555,7 +17719,7 @@ snapshots: postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) postcss-js: 4.1.0(postcss@8.5.6) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.11 @@ -17732,6 +17896,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.25.0 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + tty-browserify@0.0.1: {} tunnel-agent@0.6.0: @@ -17834,6 +18005,12 @@ snapshots: acorn: 8.15.0 webpack-virtual-modules: 0.6.2 + unplugin@2.1.0: + dependencies: + acorn: 8.15.0 + webpack-virtual-modules: 0.6.2 + optional: true + upath@1.2.0: {} update-browserslist-db@1.1.4(browserslist@4.28.0):