Merge branch 'feat/plugins' into dev/plugin-deploy

pull/12372/head
JzoNg 1 year ago
commit f51336df08

@ -1,20 +0,0 @@
'use client'
import { useBoolean } from 'ahooks'
import UpdatePlugin from '@/app/components/plugins/update-plugin'
const Page = () => {
const [isShowUpdateModal, {
setTrue: showUpdateModal,
setFalse: hideUpdateModal,
}] = useBoolean(false)
return (
<div>
<div onClick={showUpdateModal}>Show Upgrade</div>
{isShowUpdateModal && (
<UpdatePlugin onHide={hideUpdateModal} />
)}
</div>
)
}
export default Page

@ -2,12 +2,19 @@ import { RiCheckLine, RiCloseLine } from '@remixicon/react'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
const iconSizeMap = {
xs: 'w-4 h-4 text-base',
tiny: 'w-6 h-6 text-base',
small: 'w-8 h-8',
medium: 'w-9 h-9',
large: 'w-10 h-10',
}
const Icon = ({ const Icon = ({
className, className,
src, src,
installed = false, installed = false,
installFailed = false, installFailed = false,
size, size = 'large',
}: { }: {
className?: string className?: string
src: string | { src: string | {
@ -23,7 +30,7 @@ const Icon = ({
return ( return (
<div className={cn('relative', className)}> <div className={cn('relative', className)}>
<AppIcon <AppIcon
size={size || 'large'} size={size}
iconType={'emoji'} iconType={'emoji'}
icon={src.content} icon={src.content}
background={src.background} background={src.background}
@ -32,9 +39,10 @@ const Icon = ({
</div> </div>
) )
} }
return ( return (
<div <div
className={cn('shrink-0 relative w-10 h-10 rounded-md bg-center bg-no-repeat bg-contain', className)} className={cn('shrink-0 relative rounded-md bg-center bg-no-repeat bg-contain', iconSizeMap[size], className)}
style={{ style={{
backgroundImage: `url(${src})`, backgroundImage: `url(${src})`,
}} }}

@ -4,8 +4,6 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Card from '../../card' import Card from '../../card'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { PluginType } from '../../types'
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types' import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types'
import { pluginManifestInMarketToPluginProps, pluginManifestToCardPluginProps } from '../utils' import { pluginManifestInMarketToPluginProps, pluginManifestToCardPluginProps } from '../utils'
import Badge, { BadgeState } from '@/app/components/base/badge/index' import Badge, { BadgeState } from '@/app/components/base/badge/index'
@ -26,12 +24,9 @@ const Installed: FC<Props> = ({
onCancel, onCancel,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const updateModelProviders = useUpdateModelProviders()
const handleClose = () => { const handleClose = () => {
onCancel() onCancel()
if (payload?.category === PluginType.model)
updateModelProviders()
} }
return ( return (
<> <>

@ -34,9 +34,7 @@ const InstallBundle: FC<Props> = ({
if (step === InstallStep.uploadFailed) if (step === InstallStep.uploadFailed)
return t(`${i18nPrefix}.uploadFailed`) return t(`${i18nPrefix}.uploadFailed`)
if (step === InstallStep.installed) if (step === InstallStep.installed)
return t(`${i18nPrefix}.installedSuccessfully`) return t(`${i18nPrefix}.installComplete`)
if (step === InstallStep.installFailed)
return t(`${i18nPrefix}.installFailed`)
return t(`${i18nPrefix}.installPlugin`) return t(`${i18nPrefix}.installPlugin`)
}, [step, t]) }, [step, t])

@ -6,7 +6,6 @@ import MarketplaceItem from '../item/marketplace-item'
import GithubItem from '../item/github-item' import GithubItem from '../item/github-item'
import { useFetchPluginsInMarketPlaceByIds, useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins' import { useFetchPluginsInMarketPlaceByIds, useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
import produce from 'immer' import produce from 'immer'
import { useGetState } from 'ahooks'
import PackageItem from '../item/package-item' import PackageItem from '../item/package-item'
import LoadingError from '../../base/loading-error' import LoadingError from '../../base/loading-error'
@ -25,7 +24,7 @@ const InstallByDSLList: FC<Props> = ({
}) => { }) => {
const { isLoading: isFetchingMarketplaceDataFromDSL, data: marketplaceFromDSLRes } = useFetchPluginsInMarketPlaceByIds(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value.plugin_unique_identifier!)) const { isLoading: isFetchingMarketplaceDataFromDSL, data: marketplaceFromDSLRes } = useFetchPluginsInMarketPlaceByIds(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value.plugin_unique_identifier!))
const { isLoading: isFetchingMarketplaceDataFromLocal, data: marketplaceResFromLocalRes } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value!)) const { isLoading: isFetchingMarketplaceDataFromLocal, data: marketplaceResFromLocalRes } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value!))
const [plugins, setPlugins, getPlugins] = useGetState<(Plugin | undefined)[]>((() => { const [plugins, doSetPlugins] = useState<(Plugin | undefined)[]>((() => {
const hasLocalPackage = allPlugins.some(d => d.type === 'package') const hasLocalPackage = allPlugins.some(d => d.type === 'package')
if (!hasLocalPackage) if (!hasLocalPackage)
return [] return []
@ -42,17 +41,23 @@ const InstallByDSLList: FC<Props> = ({
}) })
return _plugins return _plugins
})()) })())
const pluginsRef = React.useRef<(Plugin | undefined)[]>(plugins)
const setPlugins = useCallback((p: (Plugin | undefined)[]) => {
doSetPlugins(p)
pluginsRef.current = p
}, [])
const [errorIndexes, setErrorIndexes] = useState<number[]>([]) const [errorIndexes, setErrorIndexes] = useState<number[]>([])
const handleGitHubPluginFetched = useCallback((index: number) => { const handleGitHubPluginFetched = useCallback((index: number) => {
return (p: Plugin) => { return (p: Plugin) => {
const nextPlugins = produce(getPlugins(), (draft) => { const nextPlugins = produce(pluginsRef.current, (draft) => {
draft[index] = p draft[index] = p
}) })
setPlugins(nextPlugins) setPlugins(nextPlugins)
} }
}, [getPlugins, setPlugins]) }, [setPlugins])
const handleGitHubPluginFetchError = useCallback((index: number) => { const handleGitHubPluginFetchError = useCallback((index: number) => {
return () => { return () => {
@ -73,7 +78,7 @@ const InstallByDSLList: FC<Props> = ({
if (!isFetchingMarketplaceDataFromDSL && marketplaceFromDSLRes?.data.plugins) { if (!isFetchingMarketplaceDataFromDSL && marketplaceFromDSLRes?.data.plugins) {
const payloads = marketplaceFromDSLRes?.data.plugins const payloads = marketplaceFromDSLRes?.data.plugins
const failedIndex: number[] = [] const failedIndex: number[] = []
const nextPlugins = produce(getPlugins(), (draft) => { const nextPlugins = produce(pluginsRef.current, (draft) => {
marketPlaceInDSLIndex.forEach((index, i) => { marketPlaceInDSLIndex.forEach((index, i) => {
if (payloads[i]) if (payloads[i])
draft[index] = payloads[i] draft[index] = payloads[i]
@ -82,6 +87,7 @@ const InstallByDSLList: FC<Props> = ({
}) })
}) })
setPlugins(nextPlugins) setPlugins(nextPlugins)
if (failedIndex.length > 0) if (failedIndex.length > 0)
setErrorIndexes([...errorIndexes, ...failedIndex]) setErrorIndexes([...errorIndexes, ...failedIndex])
} }
@ -92,7 +98,7 @@ const InstallByDSLList: FC<Props> = ({
if (!isFetchingMarketplaceDataFromLocal && marketplaceResFromLocalRes?.data.list) { if (!isFetchingMarketplaceDataFromLocal && marketplaceResFromLocalRes?.data.list) {
const payloads = marketplaceResFromLocalRes?.data.list const payloads = marketplaceResFromLocalRes?.data.list
const failedIndex: number[] = [] const failedIndex: number[] = []
const nextPlugins = produce(getPlugins(), (draft) => { const nextPlugins = produce(pluginsRef.current, (draft) => {
marketPlaceInDSLIndex.forEach((index, i) => { marketPlaceInDSLIndex.forEach((index, i) => {
if (payloads[i]) { if (payloads[i]) {
const item = payloads[i] const item = payloads[i]

@ -7,7 +7,7 @@ import type { InstallState } from '@/app/components/plugins/types'
import { useGitHubReleases } from '../hooks' import { useGitHubReleases } from '../hooks'
import { convertRepoToUrl, parseGitHubUrl } from '../utils' import { convertRepoToUrl, parseGitHubUrl } from '../utils'
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types' import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
import { InstallStepFromGitHub } from '../../types' import { InstallStepFromGitHub, PluginType } from '../../types'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import SetURL from './steps/setURL' import SetURL from './steps/setURL'
import SelectPackage from './steps/selectPackage' import SelectPackage from './steps/selectPackage'
@ -15,6 +15,8 @@ import Installed from '../base/installed'
import Loaded from './steps/loaded' import Loaded from './steps/loaded'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useInvalidateAllToolProviders } from '@/service/use-tools'
const i18nPrefix = 'plugin.installFromGitHub' const i18nPrefix = 'plugin.installFromGitHub'
@ -28,6 +30,8 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
const { t } = useTranslation() const { t } = useTranslation()
const { getIconUrl } = useGetIcon() const { getIconUrl } = useGetIcon()
const { fetchReleases } = useGitHubReleases() const { fetchReleases } = useGitHubReleases()
const updateModelProviders = useUpdateModelProviders()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const [state, setState] = useState<InstallState>({ const [state, setState] = useState<InstallState>({
step: updatePayload ? InstallStepFromGitHub.selectPackage : InstallStepFromGitHub.setUrl, step: updatePayload ? InstallStepFromGitHub.selectPackage : InstallStepFromGitHub.setUrl,
repoUrl: updatePayload?.originalPackageInfo?.repo repoUrl: updatePayload?.originalPackageInfo?.repo
@ -63,7 +67,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
return t(`${i18nPrefix}.installFailed`) return t(`${i18nPrefix}.installFailed`)
return updatePayload ? t(`${i18nPrefix}.updatePlugin`) : t(`${i18nPrefix}.installPlugin`) return updatePayload ? t(`${i18nPrefix}.updatePlugin`) : t(`${i18nPrefix}.installPlugin`)
}, [state.step]) }, [state.step, t, updatePayload])
const handleUrlSubmit = async () => { const handleUrlSubmit = async () => {
const { isValid, owner, repo } = parseGitHubUrl(state.repoUrl) const { isValid, owner, repo } = parseGitHubUrl(state.repoUrl)
@ -111,8 +115,14 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
const handleInstalled = useCallback(() => { const handleInstalled = useCallback(() => {
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed })) setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed }))
if (!manifest)
return
if (PluginType.model.includes(manifest.category))
updateModelProviders()
if (PluginType.tool.includes(manifest.category))
invalidateAllToolProviders()
onSuccess() onSuccess()
}, [onSuccess]) }, [invalidateAllToolProviders, manifest, onSuccess, updateModelProviders])
const handleFailed = useCallback((errorMsg?: string) => { const handleFailed = useCallback((errorMsg?: string) => {
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installFailed })) setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installFailed }))
@ -142,7 +152,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
closable closable
> >
<div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'> <div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
<div className='flex flex-col items-start gap-1 flex-grow'> <div className='flex flex-col items-start gap-1 grow'>
<div className='self-stretch text-text-primary title-2xl-semi-bold'> <div className='self-stretch text-text-primary title-2xl-semi-bold'>
{getTitle()} {getTitle()}
</div> </div>

@ -34,13 +34,15 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
const getTitle = useCallback(() => { const getTitle = useCallback(() => {
if (step === InstallStep.uploadFailed) if (step === InstallStep.uploadFailed)
return t(`${i18nPrefix}.uploadFailed`) return t(`${i18nPrefix}.uploadFailed`)
if (isBundle && step === InstallStep.installed)
return t(`${i18nPrefix}.installComplete`)
if (step === InstallStep.installed) if (step === InstallStep.installed)
return t(`${i18nPrefix}.installedSuccessfully`) return t(`${i18nPrefix}.installedSuccessfully`)
if (step === InstallStep.installFailed) if (step === InstallStep.installFailed)
return t(`${i18nPrefix}.installFailed`) return t(`${i18nPrefix}.installFailed`)
return t(`${i18nPrefix}.installPlugin`) return t(`${i18nPrefix}.installPlugin`)
}, [step, t]) }, [isBundle, step, t])
const { getIconUrl } = useGetIcon() const { getIconUrl } = useGetIcon()

@ -2,11 +2,12 @@
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import type { PluginDeclaration } from '../../types' import type { PluginDeclaration } from '../../types'
import { InstallStep } from '../../types' import { InstallStep, PluginType } from '../../types'
import Install from './steps/install' import Install from './steps/install'
import Installed from '../base/installed' import Installed from '../base/installed'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useInvalidateAllToolProviders } from '@/service/use-tools'
type Props = { type Props = {
step: InstallStep step: InstallStep
onStepChange: (step: InstallStep) => void, onStepChange: (step: InstallStep) => void,
@ -27,11 +28,19 @@ const ReadyToInstall: FC<Props> = ({
onError, onError,
}) => { }) => {
const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const updateModelProviders = useUpdateModelProviders()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const handleInstalled = useCallback(() => { const handleInstalled = useCallback(() => {
invalidateInstalledPluginList()
onStepChange(InstallStep.installed) onStepChange(InstallStep.installed)
}, [invalidateInstalledPluginList, onStepChange]) invalidateInstalledPluginList()
if (!manifest)
return
if (PluginType.model.includes(manifest.category))
updateModelProviders()
if (PluginType.tool.includes(manifest.category))
invalidateAllToolProviders()
}, [invalidateAllToolProviders, invalidateInstalledPluginList, manifest, onStepChange, updateModelProviders])
const handleFailed = useCallback((errorMsg?: string) => { const handleFailed = useCallback((errorMsg?: string) => {
onStepChange(InstallStep.installFailed) onStepChange(InstallStep.installFailed)

@ -3,10 +3,13 @@
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import type { Plugin, PluginManifestInMarket } from '../../types' import type { Plugin, PluginManifestInMarket } from '../../types'
import { InstallStep } from '../../types' import { InstallStep, PluginType } from '../../types'
import Install from './steps/install' import Install from './steps/install'
import Installed from '../base/installed' import Installed from '../base/installed'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInvalidateAllToolProviders } from '@/service/use-tools'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
@ -27,7 +30,9 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
// readyToInstall -> check installed -> installed/failed // readyToInstall -> check installed -> installed/failed
const [step, setStep] = useState<InstallStep>(InstallStep.readyToInstall) const [step, setStep] = useState<InstallStep>(InstallStep.readyToInstall)
const [errorMsg, setErrorMsg] = useState<string | null>(null) const [errorMsg, setErrorMsg] = useState<string | null>(null)
const updateModelProviders = useUpdateModelProviders()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
// TODO: check installed in beta version. // TODO: check installed in beta version.
const getTitle = useCallback(() => { const getTitle = useCallback(() => {
@ -40,7 +45,12 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
const handleInstalled = useCallback(() => { const handleInstalled = useCallback(() => {
setStep(InstallStep.installed) setStep(InstallStep.installed)
}, []) invalidateInstalledPluginList()
if (PluginType.model.includes(manifest.category))
updateModelProviders()
if (PluginType.tool.includes(manifest.category))
invalidateAllToolProviders()
}, [invalidateAllToolProviders, invalidateInstalledPluginList, manifest.category, updateModelProviders])
const handleFailed = useCallback((errorMsg?: string) => { const handleFailed = useCallback((errorMsg?: string) => {
setStep(InstallStep.installFailed) setStep(InstallStep.installFailed)

@ -80,7 +80,7 @@ const Installed: FC<Props> = ({
return (<>{ return (<>{
payload.latest_version === toInstallVersion || !supportCheckInstalled payload.latest_version === toInstallVersion || !supportCheckInstalled
? ( ? (
<Badge className='mx-1' size="s" state={BadgeState.Default}>{payload.latest_version}</Badge> <Badge className='mx-1' size="s" state={BadgeState.Default}>{payload.version || payload.latest_version}</Badge>
) )
: ( : (
<> <>

@ -1,6 +1,5 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
@ -14,19 +13,25 @@ import {
useRemoveProviderCredentials, useRemoveProviderCredentials,
useUpdateProviderCredentials, useUpdateProviderCredentials,
} from '@/service/use-tools' } from '@/service/use-tools'
import type { PluginDetail } from '@/app/components/plugins/types'
const ActionList = () => { type Props = {
detail: PluginDetail
}
const ActionList = ({
detail,
}: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext() const { isCurrentWorkspaceManager } = useAppContext()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail) const { data: provider } = useBuiltinProviderInfo(`${detail.plugin_id}/${detail.name}`)
const { data: provider } = useBuiltinProviderInfo(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
const invalidateProviderInfo = useInvalidateBuiltinProviderInfo() const invalidateProviderInfo = useInvalidateBuiltinProviderInfo()
const { data } = useBuiltinTools(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`) const { data } = useBuiltinTools(`${detail.plugin_id}/${detail.name}`)
const [showSettingAuth, setShowSettingAuth] = useState(false) const [showSettingAuth, setShowSettingAuth] = useState(false)
const handleCredentialSettingUpdate = () => { const handleCredentialSettingUpdate = () => {
invalidateProviderInfo(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`) invalidateProviderInfo(`${detail.plugin_id}/${detail.name}`)
Toast.notify({ Toast.notify({
type: 'success', type: 'success',
message: t('common.api.actionSuccess'), message: t('common.api.actionSuccess'),
@ -74,7 +79,7 @@ const ActionList = () => {
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
{data.map(tool => ( {data.map(tool => (
<ToolItem <ToolItem
key={`${currentPluginDetail.plugin_id}${tool.name}`} key={`${detail.plugin_id}${tool.name}`}
disabled={false} disabled={false}
collection={provider} collection={provider}
tool={tool} tool={tool}

@ -31,6 +31,7 @@ import { uninstallPlugin } from '@/service/plugins'
import { useGetLanguage } from '@/context/i18n' import { useGetLanguage } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { useInvalidateAllToolProviders } from '@/service/use-tools'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -52,6 +53,7 @@ const DetailHeader = ({
const { checkForUpdates, fetchReleases } = useGitHubReleases() const { checkForUpdates, fetchReleases } = useGitHubReleases()
const { setShowUpdatePluginModal } = useModalContext() const { setShowUpdatePluginModal } = useModalContext()
const { refreshModelProviders } = useProviderContext() const { refreshModelProviders } = useProviderContext()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const { const {
installation_id, installation_id,
@ -150,10 +152,12 @@ const DetailHeader = ({
if (res.success) { if (res.success) {
hideDeleteConfirm() hideDeleteConfirm()
onUpdate(true) onUpdate(true)
if (category === PluginType.model) if (PluginType.model.includes(category))
refreshModelProviders() refreshModelProviders()
if (PluginType.tool.includes(category))
invalidateAllToolProviders()
} }
}, [showDeleting, installation_id, hideDeleting, hideDeleteConfirm, onUpdate, category, refreshModelProviders]) }, [showDeleting, installation_id, hideDeleting, hideDeleteConfirm, onUpdate, category, refreshModelProviders, invalidateAllToolProviders])
// #plugin TODO# used in apps // #plugin TODO# used in apps
// const usedInApps = 3 // const usedInApps = 3
@ -169,7 +173,7 @@ const DetailHeader = ({
<Title title={label[locale]} /> <Title title={label[locale]} />
{verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />} {verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />}
<PluginVersionPicker <PluginVersionPicker
disabled={!isFromMarketplace || !hasNewVersion} disabled={!isFromMarketplace}
isShow={isShow} isShow={isShow}
onShowChange={setIsShow} onShowChange={setIsShow}
pluginID={plugin_id} pluginID={plugin_id}

@ -13,23 +13,23 @@ import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-for
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import { import {
useCreateEndpoint, useCreateEndpoint,
useEndpointList, useEndpointList,
useInvalidateEndpointList, useInvalidateEndpointList,
} from '@/service/use-endpoints' } from '@/service/use-endpoints'
import type { PluginDetail } from '@/app/components/plugins/types'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type Props = { type Props = {
showTopBorder?: boolean detail: PluginDetail
} }
const EndpointList = ({ showTopBorder }: Props) => { const EndpointList = ({ detail }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const pluginDetail = usePluginPageContext(v => v.currentPluginDetail) const pluginUniqueID = detail.plugin_unique_identifier
const pluginUniqueID = pluginDetail.plugin_unique_identifier const declaration = detail.declaration.endpoint
const declaration = pluginDetail.declaration.endpoint const showTopBorder = detail.declaration.tool
const { data } = useEndpointList(pluginDetail.plugin_id) const { data } = useEndpointList(detail.plugin_id)
const invalidateEndpointList = useInvalidateEndpointList() const invalidateEndpointList = useInvalidateEndpointList()
const [isShowEndpointModal, { const [isShowEndpointModal, {
@ -43,7 +43,7 @@ const EndpointList = ({ showTopBorder }: Props) => {
const { mutate: createEndpoint } = useCreateEndpoint({ const { mutate: createEndpoint } = useCreateEndpoint({
onSuccess: async () => { onSuccess: async () => {
await invalidateEndpointList(pluginDetail.plugin_id) await invalidateEndpointList(detail.plugin_id)
hideEndpointModal() hideEndpointModal()
}, },
onError: () => { onError: () => {
@ -101,7 +101,7 @@ const EndpointList = ({ showTopBorder }: Props) => {
<EndpointCard <EndpointCard
key={index} key={index}
data={item} data={item}
handleChange={() => invalidateEndpointList(pluginDetail.plugin_id)} handleChange={() => invalidateEndpointList(detail.plugin_id)}
/> />
))} ))}
</div> </div>

@ -6,51 +6,50 @@ import EndpointList from './endpoint-list'
import ActionList from './action-list' import ActionList from './action-list'
import ModelList from './model-list' import ModelList from './model-list'
import Drawer from '@/app/components/base/drawer' import Drawer from '@/app/components/base/drawer'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' import type { PluginDetail } from '@/app/components/plugins/types'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type Props = { type Props = {
detail?: PluginDetail
onUpdate: () => void onUpdate: () => void
onHide: () => void
} }
const PluginDetailPanel: FC<Props> = ({ const PluginDetailPanel: FC<Props> = ({
detail,
onUpdate, onUpdate,
onHide,
}) => { }) => {
const pluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const setCurrentPluginDetail = usePluginPageContext(v => v.setCurrentPluginDetail)
const handleHide = () => setCurrentPluginDetail(undefined)
const handleUpdate = (isDelete = false) => { const handleUpdate = (isDelete = false) => {
if (isDelete) if (isDelete)
handleHide() onHide()
onUpdate() onUpdate()
} }
if (!pluginDetail) if (!detail)
return null return null
return ( return (
<Drawer <Drawer
isOpen={!!pluginDetail} isOpen={!!detail}
clickOutsideNotOpen={false} clickOutsideNotOpen={false}
onClose={handleHide} onClose={onHide}
footer={null} footer={null}
mask={false} mask={false}
positionCenter={false} positionCenter={false}
panelClassname={cn('justify-start mt-[64px] mr-2 mb-2 !w-[420px] !max-w-[420px] !p-0 !bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-xl')} panelClassname={cn('justify-start mt-[64px] mr-2 mb-2 !w-[420px] !max-w-[420px] !p-0 !bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-xl')}
> >
{pluginDetail && ( {detail && (
<> <>
<DetailHeader <DetailHeader
detail={pluginDetail} detail={detail}
onHide={handleHide} onHide={onHide}
onUpdate={handleUpdate} onUpdate={handleUpdate}
/> />
<div className='grow overflow-y-auto'> <div className='grow overflow-y-auto'>
{!!pluginDetail.declaration.tool && <ActionList />} {!!detail.declaration.tool && <ActionList detail={detail} />}
{!!pluginDetail.declaration.endpoint && <EndpointList showTopBorder={!!pluginDetail.declaration.tool} />} {!!detail.declaration.endpoint && <EndpointList detail={detail} />}
{!!pluginDetail.declaration.model && <ModelList />} {!!detail.declaration.model && <ModelList detail={detail} />}
</div> </div>
</> </>
)} )}

@ -1,14 +1,19 @@
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name' import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
import { useModelProviderModelList } from '@/service/use-models' import { useModelProviderModelList } from '@/service/use-models'
import type { PluginDetail } from '@/app/components/plugins/types'
const ModelList = () => { type Props = {
detail: PluginDetail
}
const ModelList = ({
detail,
}: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail) const { data: res } = useModelProviderModelList(`${detail.plugin_id}/${detail.name}`)
const { data: res } = useModelProviderModelList(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
if (!res) if (!res)
return null return null

@ -22,6 +22,7 @@ import cn from '@/utils/classnames'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import { useLanguage } from '../../header/account-setting/model-provider-page/hooks' import { useLanguage } from '../../header/account-setting/model-provider-page/hooks'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInvalidateAllToolProviders } from '@/service/use-tools'
import { useCategories } from '../hooks' import { useCategories } from '../hooks'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
@ -37,9 +38,10 @@ const PluginItem: FC<Props> = ({
const locale = useLanguage() const locale = useLanguage()
const { t } = useTranslation() const { t } = useTranslation()
const { categoriesMap } = useCategories() const { categoriesMap } = useCategories()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail) const currentPluginID = usePluginPageContext(v => v.currentPluginID)
const setCurrentPluginDetail = usePluginPageContext(v => v.setCurrentPluginDetail) const setCurrentPluginID = usePluginPageContext(v => v.setCurrentPluginID)
const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const { refreshModelProviders } = useProviderContext() const { refreshModelProviders } = useProviderContext()
const { const {
@ -59,20 +61,22 @@ const PluginItem: FC<Props> = ({
const handleDelete = () => { const handleDelete = () => {
invalidateInstalledPluginList() invalidateInstalledPluginList()
if (category === PluginType.model) if (PluginType.model.includes(category))
refreshModelProviders() refreshModelProviders()
if (PluginType.tool.includes(category))
invalidateAllToolProviders()
} }
return ( return (
<div <div
className={cn( className={cn(
'p-1 rounded-xl border-[1.5px] border-background-section-burn', 'p-1 rounded-xl border-[1.5px] border-background-section-burn',
currentPluginDetail?.plugin_id === plugin_id && 'border-components-option-card-option-selected-border', currentPluginID === plugin_id && 'border-components-option-card-option-selected-border',
source === PluginSource.debugging source === PluginSource.debugging
? 'bg-[repeating-linear-gradient(-45deg,rgba(16,24,40,0.04),rgba(16,24,40,0.04)_5px,rgba(0,0,0,0.02)_5px,rgba(0,0,0,0.02)_10px)]' ? 'bg-[repeating-linear-gradient(-45deg,rgba(16,24,40,0.04),rgba(16,24,40,0.04)_5px,rgba(0,0,0,0.02)_5px,rgba(0,0,0,0.02)_10px)]'
: 'bg-background-section-burn', : 'bg-background-section-burn',
)} )}
onClick={() => { onClick={() => {
setCurrentPluginDetail(plugin) setCurrentPluginID(plugin.plugin_id)
}} }}
> >
<div className={cn('relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs', className)}> <div className={cn('relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs', className)}>

@ -11,15 +11,14 @@ import {
useContextSelector, useContextSelector,
} from 'use-context-selector' } from 'use-context-selector'
import { useSelector as useAppContextSelector } from '@/context/app-context' import { useSelector as useAppContextSelector } from '@/context/app-context'
import type { PluginDetail } from '../types'
import type { FilterState } from './filter-management' import type { FilterState } from './filter-management'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
export type PluginPageContextValue = { export type PluginPageContextValue = {
containerRef: React.RefObject<HTMLDivElement> containerRef: React.RefObject<HTMLDivElement>
currentPluginDetail: PluginDetail | undefined currentPluginID: string | undefined
setCurrentPluginDetail: (plugin: PluginDetail) => void setCurrentPluginID: (pluginID?: string) => void
filters: FilterState filters: FilterState
setFilters: (filter: FilterState) => void setFilters: (filter: FilterState) => void
activeTab: string activeTab: string
@ -29,8 +28,8 @@ export type PluginPageContextValue = {
export const PluginPageContext = createContext<PluginPageContextValue>({ export const PluginPageContext = createContext<PluginPageContextValue>({
containerRef: { current: null }, containerRef: { current: null },
currentPluginDetail: undefined, currentPluginID: undefined,
setCurrentPluginDetail: () => { }, setCurrentPluginID: () => { },
filters: { filters: {
categories: [], categories: [],
tags: [], tags: [],
@ -60,7 +59,7 @@ export const PluginPageContextProvider = ({
tags: [], tags: [],
searchQuery: '', searchQuery: '',
}) })
const [currentPluginDetail, setCurrentPluginDetail] = useState<PluginDetail | undefined>() const [currentPluginID, setCurrentPluginID] = useState<string | undefined>()
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const options = useMemo(() => { const options = useMemo(() => {
@ -81,8 +80,8 @@ export const PluginPageContextProvider = ({
<PluginPageContext.Provider <PluginPageContext.Provider
value={{ value={{
containerRef, containerRef,
currentPluginDetail, currentPluginID,
setCurrentPluginDetail, setCurrentPluginID,
filters, filters,
setFilters, setFilters,
activeTab, activeTab,

@ -84,6 +84,7 @@ const PluginPage = ({
showInstallFromMarketplace() showInstallFromMarketplace()
} }
})() })()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [packageId]) }, [packageId])
const { const {

@ -14,6 +14,8 @@ const PluginsPanel = () => {
const [filters, setFilters] = usePluginPageContext(v => [v.filters, v.setFilters]) as [FilterState, (filter: FilterState) => void] const [filters, setFilters] = usePluginPageContext(v => [v.filters, v.setFilters]) as [FilterState, (filter: FilterState) => void]
const { data: pluginList, isLoading: isPluginListLoading } = useInstalledPluginList() const { data: pluginList, isLoading: isPluginListLoading } = useInstalledPluginList()
const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const currentPluginID = usePluginPageContext(v => v.currentPluginID)
const setCurrentPluginID = usePluginPageContext(v => v.setCurrentPluginID)
const { run: handleFilterChange } = useDebounceFn((filters: FilterState) => { const { run: handleFilterChange } = useDebounceFn((filters: FilterState) => {
setFilters(filters) setFilters(filters)
@ -31,6 +33,13 @@ const PluginsPanel = () => {
return filteredList return filteredList
}, [pluginList, filters]) }, [pluginList, filters])
const currentPluginDetail = useMemo(() => {
const detail = pluginList?.plugins.find(plugin => plugin.plugin_id === currentPluginID)
return detail
}, [currentPluginID, pluginList?.plugins])
const handleHide = () => setCurrentPluginID(undefined)
return ( return (
<> <>
<div className='flex flex-col pt-1 pb-3 px-12 justify-center items-start gap-3 self-stretch'> <div className='flex flex-col pt-1 pb-3 px-12 justify-center items-start gap-3 self-stretch'>
@ -40,7 +49,7 @@ const PluginsPanel = () => {
/> />
</div> </div>
{isPluginListLoading ? <Loading type='app' /> : (filteredList?.length ?? 0) > 0 ? ( {isPluginListLoading ? <Loading type='app' /> : (filteredList?.length ?? 0) > 0 ? (
<div className='flex px-12 items-start content-start gap-2 flex-grow self-stretch flex-wrap'> <div className='flex px-12 items-start content-start gap-2 grow self-stretch flex-wrap'>
<div className='w-full'> <div className='w-full'>
<List pluginList={filteredList || []} /> <List pluginList={filteredList || []} />
</div> </div>
@ -48,7 +57,11 @@ const PluginsPanel = () => {
) : ( ) : (
<Empty /> <Empty />
)} )}
<PluginDetailPanel onUpdate={() => invalidateInstalledPluginList()}/> <PluginDetailPanel
detail={currentPluginDetail}
onUpdate={() => invalidateInstalledPluginList()}
onHide={handleHide}
/>
</> </>
) )
} }

@ -79,6 +79,7 @@ export type PluginManifestInMarket = {
icon: string icon: string
label: Record<Locale, string> label: Record<Locale, string>
category: PluginType category: PluginType
version: string // TODO: wait api return current plugin version
latest_version: string latest_version: string
brief: Record<Locale, string> brief: Record<Locale, string>
introduction: string introduction: string

@ -94,11 +94,9 @@ const UpdatePluginModal: FC<Props> = ({
} }
return return
} }
if (uploadStep === UploadStep.installed) { if (uploadStep === UploadStep.installed)
onSave() onSave()
onCancel() }, [onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, targetPackageInfo.id])
}
}, [onCancel, onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, targetPackageInfo.id])
const usedInAppInfo = useMemo(() => { const usedInAppInfo = useMemo(() => {
return ( return (
<div className='flex px-0.5 justify-center items-center gap-0.5'> <div className='flex px-0.5 justify-center items-center gap-0.5'>

@ -67,7 +67,7 @@ const PluginVersionPicker: FC<Props> = ({
return return
onSelect({ version, unique_identifier }) onSelect({ version, unique_identifier })
onShowChange(false) onShowChange(false)
}, [currentVersion, onSelect]) }, [currentVersion, onSelect, onShowChange])
return ( return (
<PortalToFollowElem <PortalToFollowElem

@ -10,10 +10,14 @@ import LabelFilter from '@/app/components/tools/labels/filter'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import ProviderDetail from '@/app/components/tools/provider/detail' import ProviderDetail from '@/app/components/tools/provider/detail'
import Empty from '@/app/components/plugins/marketplace/empty' import Empty from '@/app/components/plugins/marketplace/empty'
import CustomCreateCard from '@/app/components/tools/provider/custom-create-card'
import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty'
import Card from '@/app/components/plugins/card' import Card from '@/app/components/plugins/card'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info' import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
import { useSelector as useAppContextSelector } from '@/context/app-context' import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useAllToolProviders } from '@/service/use-tools' import { useAllToolProviders } from '@/service/use-tools'
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
const ProviderList = () => { const ProviderList = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -50,90 +54,105 @@ const ProviderList = () => {
}, [activeTab, tagFilterValue, keywords, collectionList]) }, [activeTab, tagFilterValue, keywords, collectionList])
const [currentProvider, setCurrentProvider] = useState<Collection | undefined>() const [currentProvider, setCurrentProvider] = useState<Collection | undefined>()
const { data: pluginList } = useInstalledPluginList()
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const currentPluginDetail = useMemo(() => {
const detail = pluginList?.plugins.find(plugin => plugin.plugin_id === currentProvider?.plugin_id)
return detail
}, [currentProvider?.plugin_id, pluginList?.plugins])
return ( return (
<div className='relative flex overflow-hidden bg-gray-100 shrink-0 h-0 grow'> <>
<div <div className='relative flex overflow-hidden bg-gray-100 shrink-0 h-0 grow'>
ref={containerRef} <div
className='relative flex flex-col overflow-y-auto bg-gray-100 grow' ref={containerRef}
> className='relative flex flex-col overflow-y-auto bg-gray-100 grow'
<div className={cn( >
'sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-20 flex-wrap gap-y-2',
currentProvider && 'pr-6',
)}>
<TabSliderNew
value={activeTab}
onChange={(state) => {
setActiveTab(state)
if (state !== activeTab)
setCurrentProvider(undefined)
}}
options={options}
/>
<div className='flex items-center gap-2'>
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
<Input
showLeftIcon
showClearIcon
wrapperClassName='w-[200px]'
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
/>
</div>
</div>
{filteredCollectionList.length > 0 && (
<div className={cn( <div className={cn(
'relative grid content-start grid-cols-1 gap-4 px-12 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0', 'sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-20 flex-wrap gap-y-2',
currentProvider && 'pr-6',
)}> )}>
{filteredCollectionList.map(collection => ( <TabSliderNew
<div value={activeTab}
key={collection.id} onChange={(state) => {
onClick={() => setCurrentProvider(collection)} setActiveTab(state)
> if (state !== activeTab)
<Card setCurrentProvider(undefined)
className={cn(
'border-[1.5px] border-transparent cursor-pointer',
currentProvider?.id === collection.id && 'border-components-option-card-option-selected-border',
)}
hideCornerMark
payload={{
...collection,
brief: collection.description,
} as any}
footer={
<CardMoreInfo
tags={collection.labels}
/>
}
/>
</div>
))}
</div>
)}
{!filteredCollectionList.length && (
<Empty lightCard text={t('tools.noTools')} className='px-12' />
)}
{
enable_marketplace && (
<Marketplace
onMarketplaceScroll={() => {
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
}} }}
searchPluginText={keywords} options={options}
filterPluginTags={tagFilterValue}
/> />
) <div className='flex items-center gap-2'>
} <LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
<Input
showLeftIcon
showClearIcon
wrapperClassName='w-[200px]'
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
/>
</div>
</div>
{(filteredCollectionList.length > 0 || activeTab !== 'builtin') && (
<div className={cn(
'relative grid content-start grid-cols-1 gap-4 px-12 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0',
)}>
{activeTab === 'api' && <CustomCreateCard onRefreshData={refetch} />}
{filteredCollectionList.map(collection => (
<div
key={collection.id}
onClick={() => setCurrentProvider(collection)}
>
<Card
className={cn(
'border-[1.5px] border-transparent cursor-pointer',
currentProvider?.id === collection.id && 'border-components-option-card-option-selected-border',
)}
hideCornerMark
payload={{
...collection,
brief: collection.description,
} as any}
footer={
<CardMoreInfo
tags={collection.labels}
/>
}
/>
</div>
))}
{!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty /></div>}
</div>
)}
{!filteredCollectionList.length && activeTab === 'builtin' && (
<Empty lightCard text={t('tools.noTools')} className='px-12' />
)}
{
enable_marketplace && activeTab === 'builtin' && (
<Marketplace
onMarketplaceScroll={() => {
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
}}
searchPluginText={keywords}
filterPluginTags={tagFilterValue}
/>
)
}
</div>
</div> </div>
{currentProvider && ( {currentProvider && !currentProvider.plugin_id && (
<ProviderDetail <ProviderDetail
collection={currentProvider} collection={currentProvider}
onHide={() => setCurrentProvider(undefined)} onHide={() => setCurrentProvider(undefined)}
onRefreshData={refetch} onRefreshData={refetch}
/> />
)} )}
</div> <PluginDetailPanel
detail={currentPluginDetail}
onUpdate={() => invalidateInstalledPluginList()}
onHide={() => setCurrentProvider(undefined)}
/>
</>
) )
} }
ProviderList.displayName = 'ToolProviderList' ProviderList.displayName = 'ToolProviderList'

@ -45,7 +45,7 @@ const Contribute = ({ onRefreshData }: Props) => {
return ( return (
<> <>
{isCurrentWorkspaceManager && ( {isCurrentWorkspaceManager && (
<div className='flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[160px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-gray-50 hover:shadow-lg'> <div className='flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[135px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-gray-50 hover:shadow-lg'>
<div className='group grow rounded-t-xl hover:bg-white' onClick={() => setIsShowEditCustomCollectionModal(true)}> <div className='group grow rounded-t-xl hover:bg-white' onClick={() => setIsShowEditCustomCollectionModal(true)}>
<div className='shrink-0 flex items-center p-4 pb-3'> <div className='shrink-0 flex items-center p-4 pb-3'>
<div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg group-hover:border-primary-100 group-hover:bg-primary-50'> <div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg group-hover:border-primary-100 group-hover:bg-primary-50'>

@ -48,6 +48,7 @@ export type Collection = {
is_team_authorization: boolean is_team_authorization: boolean
allow_delete: boolean allow_delete: boolean
labels: string[] labels: string[]
plugin_id?: string
} }
export type ToolParameter = { export type ToolParameter = {

@ -1,6 +1,6 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback, useRef } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiMoreFill } from '@remixicon/react' import { RiMoreFill } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
@ -12,12 +12,15 @@ import {
} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { MARKETPLACE_URL_PREFIX } from '@/config' import { MARKETPLACE_URL_PREFIX } from '@/config'
import { useDownloadPlugin } from '@/service/use-plugins'
import { downloadFile } from '@/utils/format'
type Props = { type Props = {
open: boolean open: boolean
onOpenChange: (v: boolean) => void onOpenChange: (v: boolean) => void
author: string author: string
name: string name: string
version: string
} }
const OperationDropdown: FC<Props> = ({ const OperationDropdown: FC<Props> = ({
@ -25,6 +28,7 @@ const OperationDropdown: FC<Props> = ({
onOpenChange, onOpenChange,
author, author,
name, name,
version,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const openRef = useRef(open) const openRef = useRef(open)
@ -37,6 +41,25 @@ const OperationDropdown: FC<Props> = ({
setOpen(!openRef.current) setOpen(!openRef.current)
}, [setOpen]) }, [setOpen])
const [needDownload, setNeedDownload] = useState(false)
const { data: blob, isLoading } = useDownloadPlugin({
organization: author,
pluginName: name,
version,
}, needDownload)
const handleDownload = useCallback(() => {
if (isLoading) return
setNeedDownload(true)
}, [isLoading])
useEffect(() => {
if (blob) {
const fileName = `${author}-${name}_${version}.zip`
downloadFile({ data: blob, fileName })
setNeedDownload(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [blob])
return ( return (
<PortalToFollowElem <PortalToFollowElem
open={open} open={open}
@ -54,7 +77,7 @@ const OperationDropdown: FC<Props> = ({
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[9999]'> <PortalToFollowElemContent className='z-[9999]'>
<div className='w-[112px] p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'> <div className='w-[112px] p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
<div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('common.operation.download')}</div> <div onClick={handleDownload} className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('common.operation.download')}</div>
<a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}`} target='_blank' className='block px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a> <a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}`} target='_blank' className='block px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a>
</div> </div>
</PortalToFollowElemContent> </PortalToFollowElemContent>

@ -59,6 +59,7 @@ const Item: FC<Props> = ({
onOpenChange={setOpen} onOpenChange={setOpen}
author={payload.org} author={payload.org}
name={payload.name} name={payload.name}
version={payload.latest_version}
/> />
</div> </div>
{isShowInstallModal && ( {isShowInstallModal && (

@ -94,6 +94,7 @@ const translation = {
}, },
installModal: { installModal: {
installPlugin: 'Install Plugin', installPlugin: 'Install Plugin',
installComplete: 'Installation complete',
installedSuccessfully: 'Installation successful', installedSuccessfully: 'Installation successful',
installedSuccessfullyDesc: 'The plugin has been installed successfully.', installedSuccessfullyDesc: 'The plugin has been installed successfully.',
uploadFailed: 'Upload failed', uploadFailed: 'Upload failed',

@ -94,6 +94,7 @@ const translation = {
}, },
installModal: { installModal: {
installPlugin: '安装插件', installPlugin: '安装插件',
installComplete: '安装完成',
installedSuccessfully: '安装成功', installedSuccessfully: '安装成功',
installedSuccessfullyDesc: '插件已成功安装。', installedSuccessfullyDesc: '插件已成功安装。',
uploadFailed: '上传失败', uploadFailed: '上传失败',

@ -12,6 +12,7 @@ export const ContentType = {
audio: 'audio/mpeg', audio: 'audio/mpeg',
form: 'application/x-www-form-urlencoded; charset=UTF-8', form: 'application/x-www-form-urlencoded; charset=UTF-8',
download: 'application/octet-stream', // for download download: 'application/octet-stream', // for download
downloadZip: 'application/zip', // for download
upload: 'multipart/form-data', // for upload upload: 'multipart/form-data', // for upload
} }
@ -193,7 +194,7 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
const contentType = res.headers.get('content-type') const contentType = res.headers.get('content-type')
if ( if (
contentType contentType
&& [ContentType.download, ContentType.audio].includes(contentType) && [ContentType.download, ContentType.audio, ContentType.downloadZip].includes(contentType)
) )
return await res.blob() as T return await res.blob() as T

@ -344,3 +344,12 @@ export const useMutationCheckDependenciesBeforeImportDSL = () => {
return mutation return mutation
} }
export const useDownloadPlugin = (info: { organization: string; pluginName: string; version: string }, needDownload: boolean) => {
return useQuery({
queryKey: [NAME_SPACE, 'downloadPlugin', info],
queryFn: () => getMarketplace<Blob>(`/plugins/${info.organization}/${info.pluginName}/${info.version}/download`),
enabled: needDownload,
retry: 0,
})
}

@ -21,6 +21,10 @@ export const useAllToolProviders = () => {
}) })
} }
export const useInvalidateAllToolProviders = () => {
return useInvalid(useAllToolProvidersKey)
}
const useAllBuiltInToolsKey = [NAME_SPACE, 'builtIn'] const useAllBuiltInToolsKey = [NAME_SPACE, 'builtIn']
export const useAllBuiltInTools = () => { export const useAllBuiltInTools = () => {
return useQuery<ToolWithProvider[]>({ return useQuery<ToolWithProvider[]>({

@ -34,3 +34,14 @@ export const formatTime = (num: number) => {
} }
return `${num.toFixed(2)} ${units[index]}` return `${num.toFixed(2)} ${units[index]}`
} }
export const downloadFile = ({ data, fileName }: { data: Blob; fileName: string }) => {
const url = window.URL.createObjectURL(data)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(url)
}

Loading…
Cancel
Save