Merge branch 'jzh' into deploy/dev

This commit is contained in:
jZonG 2025-05-28 16:51:27 +08:00
commit b264cc7f35
75 changed files with 2704 additions and 293 deletions

View File

@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import AppCard from '@/app/components/app/overview/appCard'
import Loading from '@/app/components/base/loading'
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
import { ToastContext } from '@/app/components/base/toast'
import {
fetchAppDetail,
@ -117,6 +118,11 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
isInPanel={isInPanel}
onChangeStatus={onChangeApiStatus}
/>
{isInPanel && appDetail.mode === 'workflow' && (
<MCPServiceCard
appInfo={appDetail}
/>
)}
</div>
)
}

View File

@ -2,6 +2,10 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '../base/app-icon'
import Tooltip from '@/app/components/base/tooltip'
import {
Code,
WindowCursor,
} from '@/app/components/base/icons/src/vender/workflow'
export type IAppBasicProps = {
iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion'
@ -14,25 +18,13 @@ export type IAppBasicProps = {
textStyle?: { main?: string; extra?: string }
isExtraInLine?: boolean
mode?: string
hideType?: boolean
}
const ApiSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3.5C8.5 4.60457 9.39543 5.5 10.5 5.5C11.6046 5.5 12.5 4.60457 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.39543 1.5 8.5 2.39543 8.5 3.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.5 9C12.5 10.1046 13.3954 11 14.5 11C15.6046 11 16.5 10.1046 16.5 9C16.5 7.89543 15.6046 7 14.5 7C13.3954 7 12.5 7.89543 12.5 9Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 3.5H5.5L3.5 6.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 14.5C8.5 15.6046 9.39543 16.5 10.5 16.5C11.6046 16.5 12.5 15.6046 12.5 14.5C12.5 13.3954 11.6046 12.5 10.5 12.5C9.39543 12.5 8.5 13.3954 8.5 14.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 14.5H5.5L3.5 11.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.5 9H1.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" />
</svg>
const WebappSvg = <svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.375 5.45825L7.99998 8.99992M7.99998 8.99992L1.62498 5.45825M7.99998 8.99992L8 16.1249M14.75 12.0439V5.95603C14.75 5.69904 14.75 5.57055 14.7121 5.45595C14.6786 5.35457 14.6239 5.26151 14.5515 5.18299C14.4697 5.09424 14.3574 5.03184 14.1328 4.90704L8.58277 1.8237C8.37007 1.70553 8.26372 1.64645 8.15109 1.62329C8.05141 1.60278 7.9486 1.60278 7.84891 1.62329C7.73628 1.64645 7.62993 1.70553 7.41723 1.8237L1.86723 4.90704C1.64259 5.03184 1.53026 5.09424 1.44847 5.18299C1.37612 5.26151 1.32136 5.35457 1.28786 5.45595C1.25 5.57055 1.25 5.69904 1.25 5.95603V12.0439C1.25 12.3008 1.25 12.4293 1.28786 12.5439C1.32136 12.6453 1.37612 12.7384 1.44847 12.8169C1.53026 12.9056 1.64259 12.968 1.86723 13.0928L7.41723 16.1762C7.62993 16.2943 7.73628 16.3534 7.84891 16.3766C7.9486 16.3971 8.05141 16.3971 8.15109 16.3766C8.26372 16.3534 8.37007 16.2943 8.58277 16.1762L14.1328 13.0928C14.3574 12.968 14.4697 12.9056 14.5515 12.8169C14.6239 12.7384 14.6786 12.6453 14.7121 12.5439C14.75 12.4293 14.75 12.3008 14.75 12.0439Z" stroke="#155EEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_6294_13848)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.287 21.9133L1.70748 18.6999C1.08685 17.9267 0.75 16.976 0.75 15.9974V4.36124C0.75 2.89548 1.92269 1.67923 3.43553 1.57594L15.3991 0.759137C16.2682 0.699797 17.1321 0.930818 17.8461 1.41353L22.0494 4.25543C22.8018 4.76414 23.25 5.59574 23.25 6.48319V19.7124C23.25 21.1468 22.0969 22.3345 20.6157 22.4256L7.3375 23.243C6.1555 23.3158 5.01299 22.8178 4.287 21.9133Z" fill="white" />
@ -48,13 +40,17 @@ const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xm
const ICON_MAP = {
app: <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />,
api: <AppIcon innerIcon={ApiSvg} className='border !border-purple-200 !bg-purple-50' />,
api: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-violet-violet-500 p-1 shadow-md'>
<Code className='h-4 w-4 text-text-primary-on-surface' />
</div>,
dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />,
webapp: <AppIcon innerIcon={WebappSvg} className='border !border-primary-200 !bg-primary-100' />,
webapp: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md'>
<WindowCursor className='h-4 w-4 text-text-primary-on-surface' />
</div>,
notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />,
}
export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, isExtraInLine, mode = 'expand', iconType = 'app' }: IAppBasicProps) {
export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, isExtraInLine, mode = 'expand', iconType = 'app', hideType }: IAppBasicProps) {
const { t } = useTranslation()
return (
@ -88,9 +84,10 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
/>
}
</div>
{isExtraInLine ? (
{!hideType && isExtraInLine && (
<div className="system-2xs-medium-uppercase flex text-text-tertiary">{type}</div>
) : (
)}
{!hideType && !isExtraInLine && (
<div className='system-2xs-medium-uppercase text-text-tertiary'>{isExternal ? t('dataset.externalTag') : type}</div>
)}
</div>}

View File

@ -30,14 +30,30 @@ import ConfigCredential from '@/app/components/tools/setting/build-in/config-cre
import { updateBuiltInToolCredential } from '@/service/tools'
import cn from '@/utils/classnames'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
import { canFindTool } from '@/utils'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
import type { ToolWithProvider } from '@/app/components/workflow/types'
type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } | null
const AgentTools: FC = () => {
const { t } = useTranslation()
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const { modelConfig, setModelConfig, collectionList } = useContext(ConfigContext)
const { modelConfig, setModelConfig } = useContext(ConfigContext)
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const collectionList = useMemo(() => {
const allTools = [
...(buildInTools || []),
...(customTools || []),
...(workflowTools || []),
...(mcpTools || []),
]
return allTools
}, [buildInTools, customTools, workflowTools, mcpTools])
const formattingChangedDispatcher = useFormattingChangedDispatcher()
const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null)
@ -85,23 +101,38 @@ const AgentTools: FC = () => {
}
const [isDeleting, setIsDeleting] = useState<number>(-1)
const getToolValue = (tool: ToolDefaultValue) => {
return {
provider_id: tool.provider_id,
provider_type: tool.provider_type as CollectionType,
provider_name: tool.provider_name,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
tool_parameters: tool.params,
notAuthor: !tool.is_team_authorization,
enabled: true,
}
}
const handleSelectTool = (tool: ToolDefaultValue) => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.agentConfig.tools.push({
provider_id: tool.provider_id,
provider_type: tool.provider_type as CollectionType,
provider_name: tool.provider_name,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
tool_parameters: tool.params,
notAuthor: !tool.is_team_authorization,
enabled: true,
})
draft.agentConfig.tools.push(getToolValue(tool))
})
setModelConfig(newModelConfig)
}
const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.agentConfig.tools.push(...tool.map(getToolValue))
})
setModelConfig(newModelConfig)
}
const getProviderShowName = (item: AgentTool) => {
const type = item.provider_type
if(type === CollectionType.builtIn)
return item.provider_name.split('/').pop()
return item.provider_name
}
return (
<>
<Panel
@ -132,7 +163,8 @@ const AgentTools: FC = () => {
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
selectedTools={tools}
onSelectMultiple={handleSelectMultipleTool}
selectedTools={tools as unknown as ToolValue[]}
/>
</>
)}
@ -150,7 +182,7 @@ const AgentTools: FC = () => {
<div className='flex w-0 grow items-center'>
{item.isDeleted && <DefaultToolIcon className='h-5 w-5' />}
{!item.isDeleted && (
<div className={cn((item.notAuthor || !item.enabled) && 'opacity-50')}>
<div className={cn((item.notAuthor || !item.enabled) && 'shrink-0 opacity-50')}>
{typeof item.icon === 'string' && <div className='h-5 w-5 rounded-md bg-cover bg-center' style={{ backgroundImage: `url(${item.icon})` }} />}
{typeof item.icon !== 'string' && <AppIcon className='rounded-md' size='xs' icon={item.icon?.content} background={item.icon?.background} />}
</div>
@ -161,7 +193,7 @@ const AgentTools: FC = () => {
(item.isDeleted || item.notAuthor || !item.enabled) ? 'opacity-50' : '',
)}
>
<span className='system-xs-medium pr-1.5 text-text-secondary'>{item.provider_type === CollectionType.builtIn ? item.provider_name.split('/').pop() : item.tool_label}</span>
<span className='system-xs-medium pr-1.5 text-text-secondary'>{getProviderShowName(item)}</span>
<span className='text-text-tertiary'>{item.tool_label}</span>
{!item.isDeleted && (
<Tooltip
@ -274,8 +306,7 @@ const AgentTools: FC = () => {
<SettingBuiltInTool
toolName={currentTool?.tool_name as string}
setting={currentTool?.tool_parameters}
collection={currentTool?.collection as Collection}
isBuiltIn={currentTool?.collection?.type === CollectionType.builtIn}
collection={currentTool?.collection as ToolWithProvider}
isModel={currentTool?.collection?.type === CollectionType.model}
onSave={handleToolSettingChange}
onHide={() => setIsShowSettingTool(false)}

View File

@ -24,10 +24,11 @@ import { fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList, fetchWor
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import cn from '@/utils/classnames'
import type { ToolWithProvider } from '@/app/components/workflow/types'
type Props = {
showBackButton?: boolean
collection: Collection
collection: Collection | ToolWithProvider
isBuiltIn?: boolean
isModel?: boolean
toolName: string
@ -51,9 +52,10 @@ const SettingBuiltInTool: FC<Props> = ({
const { locale } = useContext(I18n)
const language = getLanguage(locale)
const { t } = useTranslation()
const [isLoading, setIsLoading] = useState(true)
const [tools, setTools] = useState<Tool[]>([])
const passedTools = (collection as ToolWithProvider).tools
const hasPassedTools = passedTools?.length > 0
const [isLoading, setIsLoading] = useState(!hasPassedTools)
const [tools, setTools] = useState<Tool[]>(hasPassedTools ? passedTools : [])
const currTool = tools.find(tool => tool.name === toolName)
const formSchemas = currTool ? toolParametersToFormSchemas(currTool.parameters) : []
const infoSchemas = formSchemas.filter(item => item.form === 'llm')
@ -63,7 +65,7 @@ const SettingBuiltInTool: FC<Props> = ({
const [currType, setCurrType] = useState('info')
const isInfoActive = currType === 'info'
useEffect(() => {
if (!collection)
if (!collection || hasPassedTools)
return
(async () => {

View File

@ -178,6 +178,7 @@ function AppCard({
icon={appInfo.icon}
icon_background={appInfo.icon_background}
name={basicName}
hideType
type={
isApp
? t('appOverview.overview.appInfo.explanation')

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.20626 1.68651C9.61828 1.68651 10.014 1.8473 10.3093 2.13466C10.4536 2.27516 10.5684 2.44313 10.6468 2.62868C10.7252 2.81422 10.7657 3.01358 10.7659 3.21501C10.7661 3.41645 10.7259 3.61588 10.6478 3.80156C10.5697 3.98723 10.4552 4.1554 10.3111 4.29614L5.86656 8.65516C5.81837 8.70203 5.78006 8.75808 5.7539 8.82001C5.72775 8.88194 5.71427 8.94848 5.71427 9.01571C5.71427 9.08294 5.72775 9.14948 5.7539 9.21141C5.78006 9.27334 5.81837 9.32939 5.86656 9.37626C5.96503 9.47212 6.09703 9.52576 6.23445 9.52576C6.37187 9.52576 6.50387 9.47212 6.60234 9.37626L6.66222 9.31698L6.66345 9.31576L11.0463 5.01725C11.3417 4.73067 11.7372 4.57056 12.1488 4.5709C12.5604 4.57124 12.9556 4.73202 13.2506 5.01908L13.2811 5.04902C13.4256 5.18967 13.5405 5.35786 13.6189 5.54363C13.6973 5.72941 13.7377 5.92903 13.7377 6.13068C13.7377 6.33233 13.6973 6.53195 13.6189 6.71773C13.5405 6.9035 13.4256 7.07169 13.2811 7.21234L7.96082 12.43C7.84828 12.5393 7.75882 12.6701 7.69773 12.8147C7.63664 12.9592 7.60517 13.1145 7.60517 13.2714C7.60517 13.4284 7.63664 13.5837 7.69773 13.7282C7.75882 13.8728 7.84828 14.0036 7.96082 14.1129L9.05348 15.1842C9.15192 15.2799 9.28378 15.3334 9.42106 15.3334C9.55834 15.3334 9.6902 15.2799 9.78864 15.1842C9.83683 15.1373 9.87514 15.0813 9.9013 15.0194C9.92746 14.9574 9.94094 14.8909 9.94094 14.8237C9.94094 14.7564 9.92746 14.6899 9.9013 14.628C9.87514 14.566 9.83683 14.51 9.78864 14.4631L8.69598 13.3912C8.67992 13.3756 8.66716 13.357 8.65844 13.3363C8.64973 13.3157 8.64523 13.2935 8.64523 13.2711C8.64523 13.2488 8.64973 13.2266 8.65844 13.206C8.66716 13.1853 8.67992 13.1667 8.69598 13.1511L14.0163 7.93405C14.2572 7.69971 14.4488 7.41943 14.5796 7.10979C14.7104 6.80014 14.7778 6.46742 14.7778 6.13129C14.7778 5.79516 14.7104 5.46244 14.5796 5.1528C14.4488 4.84315 14.2572 4.56288 14.0163 4.32853L13.9857 4.29797C13.6978 4.01697 13.3493 3.80582 12.9669 3.6808C12.5845 3.55578 12.1785 3.52022 11.7802 3.57687C11.8371 3.1838 11.8001 2.78285 11.6722 2.40684C11.5443 2.03083 11.3292 1.69045 11.0445 1.41356C10.5524 0.93469 9.89288 0.666748 9.20626 0.666748C8.51964 0.666748 7.86012 0.93469 7.36805 1.41356L1.48555 7.18239C1.43735 7.22926 1.39905 7.28532 1.37289 7.34725C1.34673 7.40917 1.33325 7.47572 1.33325 7.54294C1.33325 7.61017 1.34673 7.67672 1.37289 7.73864C1.39905 7.80057 1.43735 7.85663 1.48555 7.9035C1.58399 7.99918 1.71585 8.0527 1.85313 8.0527C1.9904 8.0527 2.12227 7.99918 2.22071 7.9035L8.10321 2.13466C8.39848 1.8473 8.79424 1.68651 9.20626 1.68651Z" fill="white"/>
<path d="M9.68688 3.41201C9.66072 3.47394 9.62241 3.52999 9.57422 3.57686L5.22314 7.8436C5.07864 7.98425 4.96378 8.15243 4.88535 8.33821C4.80693 8.52399 4.76652 8.7236 4.76652 8.92526C4.76652 9.12691 4.80693 9.32652 4.88535 9.5123C4.96378 9.69808 5.07864 9.86626 5.22314 10.0069C5.51841 10.2943 5.91417 10.4551 6.32619 10.4551C6.73821 10.4551 7.13397 10.2943 7.42924 10.0069L11.7797 5.74017C11.8782 5.64431 12.0102 5.59067 12.1476 5.59067C12.285 5.59067 12.417 5.64431 12.5155 5.74017C12.5637 5.78704 12.602 5.8431 12.6281 5.90503C12.6543 5.96696 12.6678 6.0335 12.6678 6.10073C12.6678 6.16795 12.6543 6.2345 12.6281 6.29643C12.602 6.35835 12.5637 6.41441 12.5155 6.46128L8.1644 10.728C7.67225 11.2067 7.01276 11.4746 6.32619 11.4746C5.63962 11.4746 4.98013 11.2067 4.48798 10.728C4.24701 10.4937 4.05547 10.2134 3.92468 9.90375C3.79389 9.59411 3.7265 9.26139 3.7265 8.92526C3.7265 8.58912 3.79389 8.2564 3.92468 7.94676C4.05547 7.63712 4.24701 7.35684 4.48798 7.1225L8.83845 2.85576C8.93691 2.75989 9.06891 2.70625 9.20633 2.70625C9.34375 2.70625 9.47575 2.75989 9.57422 2.85576C9.62241 2.90263 9.66072 2.95868 9.68688 3.02061C9.71304 3.08254 9.72651 3.14908 9.72651 3.21631C9.72651 3.28353 9.71304 3.35008 9.68688 3.41201Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.33325 4.66663C1.33325 3.56206 2.22869 2.66663 3.33325 2.66663H12.6666C13.7712 2.66663 14.6666 3.56206 14.6666 4.66663V8.16663C14.6666 8.53483 14.3681 8.83329 13.9999 8.83329C13.6317 8.83329 13.3333 8.53483 13.3333 8.16663V4.66663C13.3333 4.29844 13.0348 3.99996 12.6666 3.99996H3.33325C2.96507 3.99996 2.66659 4.29844 2.66659 4.66663V12C2.66659 12.3682 2.96507 12.6666 3.33325 12.6666H7.99992C8.36812 12.6666 8.66658 12.9651 8.66658 13.3333C8.66658 13.7015 8.36812 14 7.99992 14H3.33325C2.22869 14 1.33325 13.1046 1.33325 12V4.66663Z" fill="white"/>
<path d="M3.66659 5.83329C3.66659 6.29353 4.03968 6.66663 4.49992 6.66663C4.96016 6.66663 5.33325 6.29353 5.33325 5.83329C5.33325 5.37305 4.96016 4.99996 4.49992 4.99996C4.03968 4.99996 3.66659 5.37305 3.66659 5.83329Z" fill="white"/>
<path d="M5.99992 5.83329C5.99992 6.29353 6.37301 6.66663 6.83325 6.66663C7.29352 6.66663 7.66658 6.29353 7.66658 5.83329C7.66658 5.37305 7.29352 4.99996 6.83325 4.99996C6.37301 4.99996 5.99992 5.37305 5.99992 5.83329Z" fill="white"/>
<path d="M8.33325 5.83329C8.33325 6.29353 8.70632 6.66663 9.16658 6.66663C9.62685 6.66663 9.99992 6.29353 9.99992 5.83329C9.99992 5.37305 9.62685 4.99996 9.16658 4.99996C8.70632 4.99996 8.33325 5.37305 8.33325 5.83329Z" fill="white"/>
<path d="M10.5293 9.69609C10.2933 9.62349 10.0365 9.68729 9.86185 9.86189C9.68725 10.0365 9.62345 10.2934 9.69605 10.5294L11.0294 14.8627C11.1095 15.1231 11.3401 15.3086 11.6116 15.331C11.8832 15.3535 12.1411 15.2085 12.2629 14.9648L13.1635 13.1636L14.9647 12.263C15.2085 12.1411 15.3535 11.8832 15.331 11.6116C15.3085 11.3401 15.1231 11.1096 14.8627 11.0294L10.5293 9.69609Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,35 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.20626 1.68651C9.61828 1.68651 10.014 1.8473 10.3093 2.13466C10.4536 2.27516 10.5684 2.44313 10.6468 2.62868C10.7252 2.81422 10.7657 3.01358 10.7659 3.21501C10.7661 3.41645 10.7259 3.61588 10.6478 3.80156C10.5697 3.98723 10.4552 4.1554 10.3111 4.29614L5.86656 8.65516C5.81837 8.70203 5.78006 8.75808 5.7539 8.82001C5.72775 8.88194 5.71427 8.94848 5.71427 9.01571C5.71427 9.08294 5.72775 9.14948 5.7539 9.21141C5.78006 9.27334 5.81837 9.32939 5.86656 9.37626C5.96503 9.47212 6.09703 9.52576 6.23445 9.52576C6.37187 9.52576 6.50387 9.47212 6.60234 9.37626L6.66222 9.31698L6.66345 9.31576L11.0463 5.01725C11.3417 4.73067 11.7372 4.57056 12.1488 4.5709C12.5604 4.57124 12.9556 4.73202 13.2506 5.01908L13.2811 5.04902C13.4256 5.18967 13.5405 5.35786 13.6189 5.54363C13.6973 5.72941 13.7377 5.92903 13.7377 6.13068C13.7377 6.33233 13.6973 6.53195 13.6189 6.71773C13.5405 6.9035 13.4256 7.07169 13.2811 7.21234L7.96082 12.43C7.84828 12.5393 7.75882 12.6701 7.69773 12.8147C7.63664 12.9592 7.60517 13.1145 7.60517 13.2714C7.60517 13.4284 7.63664 13.5837 7.69773 13.7282C7.75882 13.8728 7.84828 14.0036 7.96082 14.1129L9.05348 15.1842C9.15192 15.2799 9.28378 15.3334 9.42106 15.3334C9.55834 15.3334 9.6902 15.2799 9.78864 15.1842C9.83683 15.1373 9.87514 15.0813 9.9013 15.0194C9.92746 14.9574 9.94094 14.8909 9.94094 14.8237C9.94094 14.7564 9.92746 14.6899 9.9013 14.628C9.87514 14.566 9.83683 14.51 9.78864 14.4631L8.69598 13.3912C8.67992 13.3756 8.66716 13.357 8.65844 13.3363C8.64973 13.3157 8.64523 13.2935 8.64523 13.2711C8.64523 13.2488 8.64973 13.2266 8.65844 13.206C8.66716 13.1853 8.67992 13.1667 8.69598 13.1511L14.0163 7.93405C14.2572 7.69971 14.4488 7.41943 14.5796 7.10979C14.7104 6.80014 14.7778 6.46742 14.7778 6.13129C14.7778 5.79516 14.7104 5.46244 14.5796 5.1528C14.4488 4.84315 14.2572 4.56288 14.0163 4.32853L13.9857 4.29797C13.6978 4.01697 13.3493 3.80582 12.9669 3.6808C12.5845 3.55578 12.1785 3.52022 11.7802 3.57687C11.8371 3.1838 11.8001 2.78285 11.6722 2.40684C11.5443 2.03083 11.3292 1.69045 11.0445 1.41356C10.5524 0.93469 9.89288 0.666748 9.20626 0.666748C8.51964 0.666748 7.86012 0.93469 7.36805 1.41356L1.48555 7.18239C1.43735 7.22926 1.39905 7.28532 1.37289 7.34725C1.34673 7.40917 1.33325 7.47572 1.33325 7.54294C1.33325 7.61017 1.34673 7.67672 1.37289 7.73864C1.39905 7.80057 1.43735 7.85663 1.48555 7.9035C1.58399 7.99918 1.71585 8.0527 1.85313 8.0527C1.9904 8.0527 2.12227 7.99918 2.22071 7.9035L8.10321 2.13466C8.39848 1.8473 8.79424 1.68651 9.20626 1.68651Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.68688 3.41201C9.66072 3.47394 9.62241 3.52999 9.57422 3.57686L5.22314 7.8436C5.07864 7.98425 4.96378 8.15243 4.88535 8.33821C4.80693 8.52399 4.76652 8.7236 4.76652 8.92526C4.76652 9.12691 4.80693 9.32652 4.88535 9.5123C4.96378 9.69808 5.07864 9.86626 5.22314 10.0069C5.51841 10.2943 5.91417 10.4551 6.32619 10.4551C6.73821 10.4551 7.13397 10.2943 7.42924 10.0069L11.7797 5.74017C11.8782 5.64431 12.0102 5.59067 12.1476 5.59067C12.285 5.59067 12.417 5.64431 12.5155 5.74017C12.5637 5.78704 12.602 5.8431 12.6281 5.90503C12.6543 5.96696 12.6678 6.0335 12.6678 6.10073C12.6678 6.16795 12.6543 6.2345 12.6281 6.29643C12.602 6.35835 12.5637 6.41441 12.5155 6.46128L8.1644 10.728C7.67225 11.2067 7.01276 11.4746 6.32619 11.4746C5.63962 11.4746 4.98013 11.2067 4.48798 10.728C4.24701 10.4937 4.05547 10.2134 3.92468 9.90375C3.79389 9.59411 3.7265 9.26139 3.7265 8.92526C3.7265 8.58912 3.79389 8.2564 3.92468 7.94676C4.05547 7.63712 4.24701 7.35684 4.48798 7.1225L8.83845 2.85576C8.93691 2.75989 9.06891 2.70625 9.20633 2.70625C9.34375 2.70625 9.47575 2.75989 9.57422 2.85576C9.62241 2.90263 9.66072 2.95868 9.68688 3.02061C9.71304 3.08254 9.72651 3.14908 9.72651 3.21631C9.72651 3.28353 9.71304 3.35008 9.68688 3.41201Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "Mcp"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Mcp.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Mcp'
export default Icon

View File

@ -1,5 +1,6 @@
export { default as AnthropicText } from './AnthropicText'
export { default as Generator } from './Generator'
export { default as Group } from './Group'
export { default as Mcp } from './Mcp'
export { default as Openai } from './Openai'
export { default as ReplayLine } from './ReplayLine'

View File

@ -0,0 +1,62 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M1.33325 4.66663C1.33325 3.56206 2.22869 2.66663 3.33325 2.66663H12.6666C13.7712 2.66663 14.6666 3.56206 14.6666 4.66663V8.16663C14.6666 8.53483 14.3681 8.83329 13.9999 8.83329C13.6317 8.83329 13.3333 8.53483 13.3333 8.16663V4.66663C13.3333 4.29844 13.0348 3.99996 12.6666 3.99996H3.33325C2.96507 3.99996 2.66659 4.29844 2.66659 4.66663V12C2.66659 12.3682 2.96507 12.6666 3.33325 12.6666H7.99992C8.36812 12.6666 8.66658 12.9651 8.66658 13.3333C8.66658 13.7015 8.36812 14 7.99992 14H3.33325C2.22869 14 1.33325 13.1046 1.33325 12V4.66663Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.66659 5.83329C3.66659 6.29353 4.03968 6.66663 4.49992 6.66663C4.96016 6.66663 5.33325 6.29353 5.33325 5.83329C5.33325 5.37305 4.96016 4.99996 4.49992 4.99996C4.03968 4.99996 3.66659 5.37305 3.66659 5.83329Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.99992 5.83329C5.99992 6.29353 6.37301 6.66663 6.83325 6.66663C7.29352 6.66663 7.66658 6.29353 7.66658 5.83329C7.66658 5.37305 7.29352 4.99996 6.83325 4.99996C6.37301 4.99996 5.99992 5.37305 5.99992 5.83329Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M8.33325 5.83329C8.33325 6.29353 8.70632 6.66663 9.16658 6.66663C9.62685 6.66663 9.99992 6.29353 9.99992 5.83329C9.99992 5.37305 9.62685 4.99996 9.16658 4.99996C8.70632 4.99996 8.33325 5.37305 8.33325 5.83329Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M10.5293 9.69609C10.2933 9.62349 10.0365 9.68729 9.86185 9.86189C9.68725 10.0365 9.62345 10.2934 9.69605 10.5294L11.0294 14.8627C11.1095 15.1231 11.3401 15.3086 11.6116 15.331C11.8832 15.3535 12.1411 15.2085 12.2629 14.9648L13.1635 13.1636L14.9647 12.263C15.2085 12.1411 15.3535 11.8832 15.331 11.6116C15.3085 11.3401 15.1231 11.1096 14.8627 11.0294L10.5293 9.69609Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "WindowCursor"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './WindowCursor.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'WindowCursor'
export default Icon

View File

@ -19,3 +19,4 @@ export { default as ParameterExtractor } from './ParameterExtractor'
export { default as QuestionClassifier } from './QuestionClassifier'
export { default as TemplatingTransform } from './TemplatingTransform'
export { default as VariableX } from './VariableX'
export { default as WindowCursor } from './WindowCursor'

View File

@ -19,6 +19,8 @@ export enum FormTypeEnum {
toolSelector = 'tool-selector',
multiToolSelector = 'array[tools]',
appSelector = 'app-selector',
object = 'object',
array = 'array',
}
export type FormOption = {

View File

@ -1,8 +1,9 @@
'use client'
import { RiCloseLine } from '@remixicon/react'
import { RiCloseLine, RiSearchLine } from '@remixicon/react'
import TagsFilter from './tags-filter'
import ActionButton from '@/app/components/base/action-button'
import cn from '@/utils/classnames'
import { RiAddLine } from '@remixicon/react'
type SearchBoxProps = {
search: string
@ -13,6 +14,9 @@ type SearchBoxProps = {
size?: 'small' | 'large'
placeholder?: string
locale?: string
supportAddCustomTool?: boolean
onShowAddCustomCollectionModal?: () => void
onAddedCustomTool?: () => void
}
const SearchBox = ({
search,
@ -23,46 +27,62 @@ const SearchBox = ({
size = 'small',
placeholder = '',
locale,
supportAddCustomTool,
onShowAddCustomCollectionModal,
}: SearchBoxProps) => {
return (
<div
className={cn(
'z-[11] flex items-center',
size === 'large' && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md',
size === 'small' && 'rounded-lg bg-components-input-bg-normal p-0.5',
inputClassName,
)}
className='z-[11] flex items-center'
>
<TagsFilter
tags={tags}
onTagsChange={onTagsChange}
size={size}
locale={locale}
/>
<div className='mx-1 h-3.5 w-[1px] bg-divider-regular'></div>
<div className='relative flex grow items-center p-1 pl-2'>
<div className='mr-2 flex w-full items-center'>
<input
className={cn(
'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none',
)}
value={search}
onChange={(e) => {
onSearchChange(e.target.value)
}}
placeholder={placeholder}
/>
{
search && (
<div className='absolute right-2 top-1/2 -translate-y-1/2'>
<ActionButton onClick={() => onSearchChange('')}>
<RiCloseLine className='h-4 w-4' />
</ActionButton>
</div>
)
}
<div className={
cn('flex grow items-center',
size === 'large' && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md',
size === 'small' && 'rounded-lg bg-components-input-bg-normal p-0.5',
inputClassName,
)
}>
<div className='relative flex grow items-center p-1 pl-2'>
<div className='mr-2 flex w-full items-center'>
<RiSearchLine className='mr-1.5 size-4 text-text-placeholder' />
<input
className={cn(
'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none',
)}
value={search}
onChange={(e) => {
onSearchChange(e.target.value)
}}
placeholder={placeholder}
/>
{
search && (
<div className='absolute right-2 top-1/2 -translate-y-1/2'>
<ActionButton onClick={() => onSearchChange('')}>
<RiCloseLine className='h-4 w-4' />
</ActionButton>
</div>
)
}
</div>
</div>
<div className='mx-1 h-3.5 w-[1px] bg-divider-regular'></div>
<TagsFilter
tags={tags}
onTagsChange={onTagsChange}
size={size}
locale={locale}
/>
</div>
{supportAddCustomTool && (
<div className='flex shrink-0 items-center'>
<ActionButton
className='ml-2 rounded-full bg-components-button-primary-bg text-components-button-primary-text hover:bg-components-button-primary-bg hover:text-components-button-primary-text'
onClick={onShowAddCustomCollectionModal}
>
<RiAddLine className='h-4 w-4' />
</ActionButton>
</div>
)}
</div>
)
}

View File

@ -2,9 +2,7 @@
import { useState } from 'react'
import {
RiArrowDownSLine,
RiCloseCircleFill,
RiFilter3Line,
RiPriceTag3Line,
} from '@remixicon/react'
import {
PortalToFollowElem,
@ -57,47 +55,15 @@ const TagsFilter = ({
onClick={() => setOpen(v => !v)}
>
<div className={cn(
'flex cursor-pointer items-center rounded-lg text-text-tertiary hover:bg-state-base-hover',
size === 'large' && 'h-8 px-2 py-1',
size === 'small' && 'h-7 py-0.5 pl-1 pr-1.5 ',
selectedTagsLength && 'text-text-secondary',
open && 'bg-state-base-hover',
'ml-0.5 mr-1.5 flex items-center text-text-tertiary ',
size === 'large' && 'h-8 py-1',
size === 'small' && 'h-7 py-0.5 ',
// selectedTagsLength && 'text-text-secondary',
// open && 'bg-state-base-hover',
)}>
<div className='p-0.5'>
<RiFilter3Line className='h-4 w-4' />
<div className='cursor-pointer rounded-md p-0.5 hover:bg-state-base-hover'>
<RiPriceTag3Line className='h-4 w-4 text-text-tertiary' />
</div>
<div className={cn(
'system-sm-medium flex items-center p-1',
size === 'large' && 'p-1',
size === 'small' && 'px-0.5 py-1',
)}>
{
!selectedTagsLength && t('pluginTags.allTags')
}
{
!!selectedTagsLength && tags.map(tag => tagsMap[tag].label).slice(0, 2).join(',')
}
{
selectedTagsLength > 2 && (
<div className='system-xs-medium ml-1 text-text-tertiary'>
+{selectedTagsLength - 2}
</div>
)
}
</div>
{
!!selectedTagsLength && (
<RiCloseCircleFill
className='h-4 w-4 cursor-pointer text-text-quaternary'
onClick={() => onTagsChange([])}
/>
)
}
{
!selectedTagsLength && (
<RiArrowDownSLine className='h-4 w-4' />
)
}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>

View File

@ -66,6 +66,19 @@ const MultipleToolSelector = ({
setOpen(false)
}
const handleAddMultiple = (val: ToolValue[]) => {
const newValue = [...value, ...val]
// deduplication
const deduplication = newValue.reduce((acc, cur) => {
if (!acc.find(item => item.provider_name === cur.provider_name && item.tool_name === cur.tool_name))
acc.push(cur)
return acc
}, [] as ToolValue[])
// update value
onChange(deduplication)
setOpen(false)
}
// delete tool
const handleDelete = (index: number) => {
const newValue = [...value]
@ -134,6 +147,7 @@ const MultipleToolSelector = ({
value={undefined}
selectedTools={value}
onSelect={handleAdd}
onSelectMultiple={handleAddMultiple}
controlledState={open}
onControlledStateChange={setOpen}
trigger={
@ -156,6 +170,7 @@ const MultipleToolSelector = ({
value={item}
selectedTools={value}
onSelect={item => handleConfigure(item, index)}
onSelectMultiple={handleAddMultiple}
onDelete={() => handleDelete(index)}
supportEnableSwitch
isEdit

View File

@ -30,6 +30,7 @@ import { useAppContext } from '@/context/app-context'
import {
useAllBuiltInTools,
useAllCustomTools,
useAllMCPTools,
useAllWorkflowTools,
useInvalidateAllBuiltInTools,
useUpdateProviderCredentials,
@ -55,14 +56,8 @@ type Props = {
value?: ToolValue
selectedTools?: ToolValue[]
isEdit?: boolean
onSelect: (tool: {
provider_name: string
tool_name: string
tool_label: string
settings?: Record<string, any>
parameters?: Record<string, any>
extra?: Record<string, any>
}) => void
onSelect: (tool: ToolValue) => void
onSelectMultiple: (tool: ToolValue[]) => void
onDelete?: () => void
supportEnableSwitch?: boolean
supportAddCustomTool?: boolean
@ -83,6 +78,7 @@ const ToolSelector: FC<Props> = ({
placement = 'left',
offset = 4,
onSelect,
onSelectMultiple,
onDelete,
scope,
supportEnableSwitch,
@ -105,6 +101,7 @@ const ToolSelector: FC<Props> = ({
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools()
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
@ -112,18 +109,19 @@ const ToolSelector: FC<Props> = ({
const { inMarketPlace, manifest } = usePluginInstalledCheck(value?.provider_name)
const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.id === value?.provider_name
})
}, [value, buildInTools, customTools, workflowTools])
}, [value, buildInTools, customTools, workflowTools, mcpTools])
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const handleSelectTool = (tool: ToolDefaultValue) => {
const getToolValue = (tool: ToolDefaultValue) => {
const settingValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any))
const paramValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any), true)
const toolValue = {
return {
provider_name: tool.provider_id,
provider_show_name: tool.provider_name,
type: tool.provider_type,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
@ -136,9 +134,16 @@ const ToolSelector: FC<Props> = ({
},
schemas: tool.paramSchemas,
}
}
const handleSelectTool = (tool: ToolDefaultValue) => {
const toolValue = getToolValue(tool)
onSelect(toolValue)
// setIsShowChooseTool(false)
}
const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => {
const toolValues = tool.map(item => getToolValue(item))
onSelectMultiple(toolValues)
}
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onSelect({
@ -250,7 +255,9 @@ const ToolSelector: FC<Props> = ({
<ToolItem
open={isShow}
icon={currentProvider?.icon || manifestIcon}
isMCPTool={currentProvider?.type === CollectionType.mcp}
providerName={value.provider_name}
providerShowName={value.provider_show_name}
toolLabel={value.tool_label || value.tool_name}
showSwitch={supportEnableSwitch}
switchValue={value.enabled}
@ -300,6 +307,7 @@ const ToolSelector: FC<Props> = ({
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
onSelectMultiple={handleSelectMultipleTool}
scope={scope}
selectedTools={selectedTools}
/>

View File

@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import produce from 'immer'
import {
RiArrowRightUpLine,
RiBracesLine,
} from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
@ -22,6 +23,8 @@ import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { VarType } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import { useBoolean } from 'ahooks'
import SchemaModal from './schema-modal'
type Props = {
value: Record<string, any>
@ -133,7 +136,12 @@ const ReasoningConfigForm: React.FC<Props> = ({
}
}, [onChange, value])
const renderField = (schema: any) => {
const [isShowSchema, {
setTrue: showSchema,
setFalse: hideSchema,
}] = useBoolean(false)
const renderField = (schema: any, showSchema: () => void) => {
const {
variable,
label,
@ -149,26 +157,56 @@ const ReasoningConfigForm: React.FC<Props> = ({
popupContent={<div className='w-[200px]'>
{tooltip[language] || tooltip.en_US}
</div>}
triggerClassName='ml-1 w-4 h-4'
triggerClassName='ml-0.5 w-4 h-4'
asChild={false} />
))
const varInput = value[variable].value
const isNumber = type === FormTypeEnum.textNumber
const isSelect = type === FormTypeEnum.select
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isObject = type === FormTypeEnum.object
const isArray = type === FormTypeEnum.array
const isShowSchemaTooltip = isObject || isArray
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
// const isToolSelector = type === FormTypeEnum.toolSelector
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector && !isObject && !isArray
const valueType = (() => {
if (isNumber) return VarType.number
if (isSelect) return VarType.string
if (isFile) return VarType.file
if (isObject) return VarType.object
if (isArray) return VarType.array
return VarType.string
})()
return (
<div key={variable} className='space-y-1'>
<div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'>
<div className='flex items-center space-x-2'>
<span className={cn('code-sm-semibold text-text-secondary')}>{label[language] || label.en_US}</span>
<div className='flex items-center'>
<span className={cn('code-sm-semibold max-w-[140px] truncate text-text-secondary')} title={label[language] || label.en_US}>{label[language] || label.en_US}</span>
{required && (
<span className='ml-1 text-red-500'>*</span>
)}
{tooltipContent}
<span className='system-xs-regular mx-1 text-text-quaternary'>·</span>
<span className='system-xs-regular text-text-tertiary'>{valueType}</span>
{!isShowSchemaTooltip && (
<Tooltip
popupContent={<div className='system-xs-medium text-text-secondary'>
{t('workflow.nodes.agent.clickToViewParameterSchema')}
</div>}
asChild={false}>
<div
className='ml-0.5 cursor-pointer rounded-[4px] p-px text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
onClick={showSchema}
>
<RiBracesLine className='size-3.5'/>
</div>
</Tooltip>
)}
</div>
<div className='flex cursor-pointer items-center gap-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter px-2 py-1 hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto)}>
<span className='system-xs-medium text-text-secondary'>{t('plugin.detailPanel.toolSelector.auto')}</span>
@ -220,7 +258,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
schema={schema}
/>
)}
{isFile && (
{(isFile || isObject || isArray) && (
<VarReferencePicker
zIndex={1001}
readonly={false}
@ -229,7 +267,15 @@ const ReasoningConfigForm: React.FC<Props> = ({
value={varInput?.value || []}
onChange={handleFileChange(variable)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile}
filterVar={(varPayload: Var) => {
if(isFile)
return varPayload.type === VarType.file || varPayload.type === VarType.arrayFile
if(isObject)
return varPayload.type === VarType.object
if(isArray)
return [VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
return true
}}
/>
)}
{isAppSelector && (
@ -267,7 +313,13 @@ const ReasoningConfigForm: React.FC<Props> = ({
}
return (
<div className='space-y-3 px-4 py-2'>
{schemas.map(schema => renderField(schema))}
{!isShowSchema && schemas.map(schema => renderField(schema, showSchema))}
{isShowSchema && (
<SchemaModal
isShow={isShowSchema}
onClose={hideSchema}
/>
)}
</div>
)
}

View File

@ -0,0 +1,125 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import Modal from '@/app/components/base/modal'
import VisualEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor'
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import { MittProvider, VisualEditorContextProvider } from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
const testSchema: SchemaRoot = {
type: Type.object,
properties: {
after: {
type: Type.string,
description: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
},
content_block: {
type: Type.object,
properties: {
block_property: {
type: Type.string,
description: 'The block property of the block to be added. Possible property are `paragraph`,`heading_1`,`heading_2`,`heading_3`,`callout`,`todo`,`toggle`,`quote`, `bulleted_list_item`, `numbered_list_item`, other properties possible are `file`,`image`,`video` (link required).',
},
bold: {
type: Type.boolean,
description: 'Indicates if the text is bold.',
},
code: {
type: Type.boolean,
description: 'Indicates if the text is formatted as code.',
},
color: {
type: Type.string,
description: 'The color of the text background or text itself.',
},
content: {
anyOf: [
{
type: Type.string,
},
{
enum: [
'null',
],
nullable: true,
},
],
description: 'The textual content of the rich text object. Required for paragraph, heading_1, heading_2, heading_3, callout, todo, toggle, quote.',
},
italic: {
type: Type.boolean,
description: 'Indicates if the text is italic.',
},
link: {
type: Type.string,
description: 'The URL of the rich text object or the file to be uploaded or image/video link',
},
strikethrough: {
type: Type.boolean,
description: 'Indicates if the text has strikethrough.',
},
underline: {
type: Type.boolean,
description: 'Indicates if the text is underlined.',
},
},
additionalProperties: false,
description: 'Child content to append to a page.',
},
parent_block_id: {
type: Type.string,
description: 'The ID of the page which the children will be added.',
},
},
required: [
'content_block',
'parent_block_id',
],
additionalProperties: false,
}
type Props = {
isShow: boolean
onClose: () => void
}
const SchemaModal: FC<Props> = ({
isShow,
onClose,
}) => {
const { t } = useTranslation()
return (
<Modal
isShow={isShow}
onClose={onClose}
className='max-w-[960px] p-0'
wrapperClassName='z-[9999]'
>
<div className='pb-6'>
{/* Header */}
<div className='relative flex p-6 pb-3 pr-14'>
<div className='title-2xl-semi-bold grow truncate text-text-primary'>
{t('workflow.nodes.agent.parameterSchema')}
</div>
<div className='absolute right-5 top-5 flex h-8 w-8 items-center justify-center p-1.5' onClick={onClose}>
<RiCloseLine className='h-[18px] w-[18px] text-text-tertiary' />
</div>
</div>
{/* Content */}
<div className='flex max-h-[700px] overflow-y-auto px-6 py-2'>
<MittProvider>
<VisualEditorContextProvider>
<VisualEditor
schema={testSchema}
readOnly
></VisualEditor>
</VisualEditorContextProvider>
</MittProvider>
</div>
</div>
</Modal>
)
}
export default React.memo(SchemaModal)

View File

@ -21,6 +21,8 @@ import cn from '@/utils/classnames'
type Props = {
icon?: any
providerName?: string
isMCPTool?: boolean
providerShowName?: string
toolLabel?: string
showSwitch?: boolean
switchValue?: boolean
@ -40,6 +42,8 @@ type Props = {
const ToolItem = ({
open,
icon,
isMCPTool,
providerShowName,
providerName,
toolLabel,
showSwitch,
@ -56,7 +60,7 @@ const ToolItem = ({
versionMismatch,
}: Props) => {
const { t } = useTranslation()
const providerNameText = providerName?.split('/').pop()
const providerNameText = isMCPTool ? providerShowName : providerName?.split('/').pop()
const isTransparent = uninstalled || versionMismatch || isError
const [isDeleting, setIsDeleting] = useState(false)

View File

@ -184,6 +184,7 @@ const EditCustomCollectionModal: FC<Props> = ({
onClose={onHide}
closable
className='!h-[calc(100vh-16px)] !max-w-[630px] !p-0'
wrapperClassName='z-[1000]'
>
<div className='flex h-full flex-col'>
<div className='ml-6 mt-6 text-base font-semibold text-text-primary'>

View File

@ -0,0 +1,74 @@
'use client'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import {
RiAddCircleFill,
RiArrowRightUpLine,
RiBookOpenLine,
} from '@remixicon/react'
import MCPModal from './modal'
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import { useAppContext } from '@/context/app-context'
import { useCreateMCP } from '@/service/use-tools'
import type { ToolWithProvider } from '@/app/components/workflow/types'
type Props = {
handleCreate: (provider: ToolWithProvider) => void
}
const NewMCPCard = ({ handleCreate }: Props) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const language = getLanguage(locale)
const { isCurrentWorkspaceManager } = useAppContext()
const { mutateAsync: createMCP } = useCreateMCP()
const create = async (info: any) => {
const provider = await createMCP(info)
handleCreate(provider)
}
const linkUrl = useMemo(() => {
// TODO help link
if (language.startsWith('zh_'))
return 'https://docs.dify.ai/zh-hans/guides/tools/integrate-tool/mcp'
return 'https://docs.dify.ai/en/guides/tools/integrate-tool/mcp'
}, [language])
const [showModal, setShowModal] = useState(false)
return (
<>
{isCurrentWorkspaceManager && (
<div className='col-span-1 flex min-h-[108px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out'>
<div className='group grow rounded-t-xl' onClick={() => setShowModal(true)}>
<div className='flex shrink-0 items-center p-4 pb-3'>
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'>
<RiAddCircleFill className='h-4 w-4 text-text-quaternary group-hover:text-text-accent'/>
</div>
<div className='system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent'>{t('tools.mcp.create.cardTitle')}</div>
</div>
</div>
<div className='rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent'>
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
<RiBookOpenLine className='h-3 w-3 shrink-0' />
<div className='system-xs-regular grow truncate' title={t('tools.mcp.create.cardLink') || ''}>{t('tools.mcp.create.cardLink')}</div>
<RiArrowRightUpLine className='h-3 w-3 shrink-0' />
</a>
</div>
</div>
)}
{showModal && (
<MCPModal
show={showModal}
onConfirm={create}
onHide={() => setShowModal(false)}
/>
)}
</>
)
}
export default NewMCPCard

View File

@ -0,0 +1,266 @@
'use client'
import React, { useCallback } from 'react'
import { useRouter } from 'next/navigation'
import type { FC } from 'react'
import { useBoolean } from 'ahooks'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import {
RiCloseLine,
RiLoader2Line,
RiLoopLeftLine,
} from '@remixicon/react'
import type { ToolWithProvider } from '../../../workflow/types'
import Icon from '@/app/components/plugins/card/base/card-icon'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Indicator from '@/app/components/header/indicator'
import MCPModal from '../modal'
import OperationDropdown from './operation-dropdown'
import ListLoading from './list-loading'
import ToolItem from './tool-item'
import {
useAuthorizeMCP,
useDeleteMCP,
useInvalidateMCPTools,
useMCPTools,
useUpdateMCP,
useUpdateMCPTools,
} from '@/service/use-tools'
import cn from '@/utils/classnames'
type Props = {
detail: ToolWithProvider
onUpdate: (isDelete?: boolean) => void
onHide: () => void
}
const MCPDetailContent: FC<Props> = ({
detail,
onUpdate,
onHide,
}) => {
const { t } = useTranslation()
const router = useRouter()
const { isCurrentWorkspaceManager } = useAppContext()
const { data, isFetching: isGettingTools } = useMCPTools(detail.is_team_authorization ? detail.id : '')
const invalidateMCPTools = useInvalidateMCPTools()
const { mutateAsync: updateTools, isPending: isUpdating } = useUpdateMCPTools()
const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useAuthorizeMCP()
const toolList = data?.tools || []
const handleUpdateTools = useCallback(async () => {
if (!detail)
return
await updateTools(detail.id)
invalidateMCPTools(detail.id)
onUpdate()
}, [detail, updateTools])
const { mutate: updateMCP } = useUpdateMCP({
onSuccess: onUpdate,
})
const { mutate: deleteMCP } = useDeleteMCP({
onSuccess: onUpdate,
})
const [isShowUpdateModal, {
setTrue: showUpdateModal,
setFalse: hideUpdateModal,
}] = useBoolean(false)
const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)
const [deleting, {
setTrue: showDeleting,
setFalse: hideDeleting,
}] = useBoolean(false)
const handleAuthorize = useCallback(async () => {
if (!detail)
return
const res = await authorizeMcp({
provider_id: detail.id,
server_url: detail.server_url!,
})
if (res.result === 'success')
handleUpdateTools()
else if (res.authorization_url)
router.push(res.authorization_url)
}, [detail, updateMCP, hideUpdateModal, onUpdate])
const handleUpdate = useCallback(async (data: any) => {
if (!detail)
return
const res = await updateMCP({
...data,
provider_id: detail.id,
})
if ((res as any)?.result === 'success') {
hideUpdateModal()
onUpdate()
}
}, [detail, updateMCP, hideUpdateModal, onUpdate])
const handleDelete = useCallback(async () => {
if (!detail)
return
showDeleting()
const res = await deleteMCP(detail.id)
hideDeleting()
if ((res as any)?.result === 'success') {
hideDeleteConfirm()
onUpdate(true)
}
}, [detail, showDeleting, hideDeleting, hideDeleteConfirm, onUpdate])
if (!detail)
return null
return (
<>
<div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3')}>
<div className='flex'>
<div className='shrink-0 overflow-hidden rounded-xl border border-components-panel-border-subtle'>
<Icon src={detail.icon} />
</div>
<div className='ml-3 w-0 grow'>
<div className='flex h-5 items-center'>
<div className='system-md-semibold truncate text-text-primary' title={detail.name}>{detail.name}</div>
</div>
<div className='system-xs-regular mt-0.5 truncate text-text-tertiary' title={detail.server_url}>{detail.server_url}</div>
</div>
<div className='flex gap-1'>
<OperationDropdown
onEdit={showUpdateModal}
onRemove={showDeleteConfirm}
/>
<ActionButton onClick={onHide}>
<RiCloseLine className='h-4 w-4' />
</ActionButton>
</div>
</div>
<div className='mt-5'>
{detail.is_team_authorization && (
<Button
variant='secondary'
className='w-full'
disabled={!isCurrentWorkspaceManager}
>
<Indicator className='mr-2' color={'green'} />
{t('tools.auth.authorized')}
</Button>
)}
{!detail.is_team_authorization && !isAuthorizing && (
<Button
variant='primary'
className='w-full'
onClick={handleAuthorize}
disabled={!isCurrentWorkspaceManager}
>
{t('tools.mcp.authorize')}
</Button>
)}
{isAuthorizing && (
<Button
variant='primary'
className='w-full'
disabled
>
<RiLoader2Line className={cn('mr-1 h-4 w-4 animate-spin')} />
{t('tools.mcp.authorizing')}
</Button>
)}
</div>
</div>
<div className='grow'>
{((detail.is_team_authorization && isGettingTools) || isUpdating) && (
<>
<div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'>
<div className='flex h-6 items-center'>
{!isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.gettingTools')}</div>}
{isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.updateTools')}</div>}
</div>
<div></div>
</div>
<div className='flex h-full w-full grow flex-col overflow-hidden px-4 pb-4'>
<ListLoading />
</div>
</>
)}
{detail.is_team_authorization && !isGettingTools && !toolList.length && (
<div className='flex h-full w-full flex-col items-center justify-center'>
<div className='system-sm-regular mb-3 text-text-tertiary'>{t('tools.mcp.toolsEmpty')}</div>
<Button
variant='primary'
onClick={handleUpdateTools}
>{t('tools.mcp.getTools')}</Button>
</div>
)}
{!isGettingTools && toolList.length > 0 && (
<>
<div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'>
<div className='flex h-6 items-center'>
{toolList.length > 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.toolsNum', { count: toolList.length })}</div>}
{toolList.length === 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.onlyTool')}</div>}
</div>
<div>
<Button size='small' onClick={handleUpdateTools}>
<RiLoopLeftLine className='mr-1 h-3.5 w-3.5' />
{t('tools.mcp.update')}
</Button>
</div>
</div>
<div className='flex w-full grow flex-col gap-2 overflow-y-auto px-4 pb-4'>
{toolList.map(tool => (
<ToolItem
key={`${detail.id}${tool.name}`}
tool={tool}
/>
))}
</div>
</>
)}
{!detail.is_team_authorization && (
<div className='flex h-full w-full flex-col items-center justify-center'>
{!isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizingRequired')}</div>}
{isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizing')}</div>}
<div className='system-sm-regular text-text-tertiary'>{t('tools.mcp.authorizeTip')}</div>
</div>
)}
</div>
{isShowUpdateModal && (
<MCPModal
data={detail}
show={isShowUpdateModal}
onConfirm={handleUpdate}
onHide={hideUpdateModal}
/>
)}
{isShowDeleteConfirm && (
<Confirm
isShow
title={t('tools.mcp.delete')}
content={
<div>
{t('tools.mcp.deleteConfirmTitle', { mcp: detail.name })}
</div>
}
onCancel={hideDeleteConfirm}
onConfirm={handleDelete}
isLoading={deleting}
isDisabled={deleting}
/>
)}
</>
)
}
export default MCPDetailContent

View File

@ -0,0 +1,37 @@
'use client'
import React from 'react'
import cn from '@/utils/classnames'
const ListLoading = () => {
return (
<div className={cn('space-y-2')}>
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
<div className='h-2 w-[180px] rounded-sm bg-text-quaternary opacity-20'></div>
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
</div>
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
<div className='h-2 w-[148px] rounded-sm bg-text-quaternary opacity-20'></div>
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
</div>
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
<div className='h-2 w-[196px] rounded-sm bg-text-quaternary opacity-20'></div>
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
</div>
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
<div className='h-2 w-[148px] rounded-sm bg-text-quaternary opacity-20'></div>
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
</div>
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
<div className='h-2 w-[180px] rounded-sm bg-text-quaternary opacity-20'></div>
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
</div>
</div>
)
}
export default ListLoading

View File

@ -0,0 +1,88 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiDeleteBinLine,
RiEditLine,
RiMoreFill,
} from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames'
type Props = {
inCard?: boolean
onOpenChange?: (open: boolean) => void
onEdit: () => void
onRemove: () => void
}
const OperationDropdown: FC<Props> = ({
inCard,
onOpenChange,
onEdit,
onRemove,
}) => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const openRef = useRef(open)
const setOpen = useCallback((v: boolean) => {
doSetOpen(v)
openRef.current = v
onOpenChange?.(v)
}, [doSetOpen])
const handleTrigger = useCallback(() => {
setOpen(!openRef.current)
}, [setOpen])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: !inCard ? -12 : 0,
crossAxis: !inCard ? 36 : 0,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div>
<ActionButton size={inCard ? 'l' : 'm'} className={cn(open && 'bg-state-base-hover')}>
<RiMoreFill className={cn('h-4 w-4', inCard && 'h-5 w-5')} />
</ActionButton>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='w-[160px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-sm'>
<div
className='flex cursor-pointer items-center rounded-lg px-3 py-1.5 hover:bg-state-base-hover'
onClick={() => {
onEdit()
handleTrigger()
}}
>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
<div className='system-md-regular ml-2 text-text-secondary'>{t('tools.mcp.operation.edit')}</div>
</div>
<div
className='group flex cursor-pointer items-center rounded-lg px-3 py-1.5 hover:bg-state-destructive-hover'
onClick={() => {
onRemove()
handleTrigger()
}}
>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary group-hover:text-text-destructive-secondary' />
<div className='system-md-regular ml-2 text-text-secondary group-hover:text-text-destructive'>{t('tools.mcp.operation.remove')}</div>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(OperationDropdown)

View File

@ -0,0 +1,50 @@
'use client'
import React from 'react'
import type { FC } from 'react'
import Drawer from '@/app/components/base/drawer'
import MCPDetailContent from './content'
import type { ToolWithProvider } from '../../../workflow/types'
import cn from '@/utils/classnames'
type Props = {
detail?: ToolWithProvider
onUpdate: () => void
onHide: () => void
}
const MCPDetailPanel: FC<Props> = ({
detail,
onUpdate,
onHide,
}) => {
const handleUpdate = (isDelete = false) => {
if (isDelete)
onHide()
onUpdate()
}
if (!detail)
return null
return (
<Drawer
isOpen={!!detail}
clickOutsideNotOpen={false}
onClose={onHide}
footer={null}
mask={false}
positionCenter={false}
panelClassName={cn('mb-2 mr-2 mt-[64px] !w-[420px] !max-w-[420px] justify-start rounded-2xl border-[0.5px] border-components-panel-border !bg-components-panel-bg !p-0 shadow-xl')}
>
{detail && (
<MCPDetailContent
detail={detail}
onHide={onHide}
onUpdate={handleUpdate}
/>
)}
</Drawer>
)
}
export default MCPDetailPanel

View File

@ -0,0 +1,41 @@
'use client'
import React from 'react'
import { useContext } from 'use-context-selector'
import type { Tool } from '@/app/components/tools/types'
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
type Props = {
tool: Tool
}
const MCPToolItem = ({
tool,
}: Props) => {
const { locale } = useContext(I18n)
const language = getLanguage(locale)
return (
<Tooltip
key={tool.name}
position='left'
popupClassName='!p-0 !px-4 !py-3.5 !w-[360px] !border-[0.5px] !border-components-panel-border !rounded-xl !shadow-lg'
popupContent={(
<div>
<div className='title-xs-semi-bold mb-1 text-text-primary'>{tool.label[language]}</div>
<div className='body-xs-regular text-text-secondary'>{tool.description[language]}</div>
</div>
)}
>
<div
className={cn('bg-components-panel-item-bg mb-2 cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover')}
>
<div className='system-md-semibold pb-0.5 text-text-secondary'>{tool.label[language]}</div>
<div className='system-xs-regular line-clamp-2 text-text-tertiary' title={tool.description[language]}>{tool.description[language]}</div>
</div>
</Tooltip>
)
}
export default MCPToolItem

View File

@ -0,0 +1,12 @@
import dayjs from 'dayjs'
import { useCallback } from 'react'
import { useI18N } from '@/context/i18n'
export const useFormatTimeFromNow = () => {
const { locale } = useI18N()
const formatTimeFromNow = useCallback((time: number) => {
return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
}, [locale])
return { formatTimeFromNow }
}

View File

@ -0,0 +1,132 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import NewMCPCard from './create-card'
import MCPCard from './provider-card'
import MCPDetailPanel from './detail/provider-detail'
import {
useAllMCPTools,
useAuthorizeMCP,
useInvalidateMCPTools,
useUpdateMCPAuthorizationToken,
useUpdateMCPTools,
} from '@/service/use-tools'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
searchText: string
}
function renderDefaultCard() {
const defaultCards = Array.from({ length: 36 }, (_, index) => (
<div
key={index}
className={cn(
'inline-flex h-[111px] rounded-xl bg-background-default-lighter opacity-10',
index < 4 && 'opacity-60',
index >= 4 && index < 8 && 'opacity-50',
index >= 8 && index < 12 && 'opacity-40',
index >= 12 && index < 16 && 'opacity-30',
index >= 16 && index < 20 && 'opacity-25',
index >= 20 && index < 24 && 'opacity-20',
)}
></div>
))
return defaultCards
}
const MCPList = ({
searchText,
}: Props) => {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const authCode = searchParams.get('code') || ''
const providerID = searchParams.get('state') || ''
const { data: list = [], refetch } = useAllMCPTools()
const { mutateAsync: authorizeMcp } = useAuthorizeMCP()
const { mutateAsync: updateTools } = useUpdateMCPTools()
const invalidateMCPTools = useInvalidateMCPTools()
const { mutateAsync: updateMCPAuthorizationToken } = useUpdateMCPAuthorizationToken()
const filteredList = useMemo(() => {
return list.filter((collection) => {
if (searchText)
return Object.values(collection.name).some(value => (value as string).toLowerCase().includes(searchText.toLowerCase()))
return true
})
}, [list, searchText])
const [currentProviderID, setCurrentProviderID] = useState<string>()
const currentProvider = useMemo(() => {
return list.find(provider => provider.id === currentProviderID)
}, [list, currentProviderID])
const handleCreate = async (provider: ToolWithProvider) => {
await refetch() // update list
setCurrentProviderID(provider.id)
await authorizeMcp({
provider_id: provider.id,
server_url: provider.server_url!,
})
await refetch() // update authorization in list
await updateTools(provider.id)
invalidateMCPTools(provider.id)
await refetch() // update tool list in provider list
}
const handleUpdateAuthorization = async (providerID: string, code: string) => {
const targetProvider = list.find(provider => provider.id === providerID)
router.replace(pathname)
if (!targetProvider) return
await updateMCPAuthorizationToken({
provider_id: providerID,
server_url: targetProvider.server_url!,
authorization_code: code,
})
await refetch()
setCurrentProviderID(providerID)
await updateTools(providerID)
invalidateMCPTools(providerID)
await refetch()
}
useEffect(() => {
if (authCode && providerID && list.length > 0)
handleUpdateAuthorization(providerID, authCode)
}, [authCode, providerID, list])
return (
<>
<div
className={cn(
'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6',
!list.length && 'h-[calc(100vh_-_136px)] overflow-hidden',
)}
>
<NewMCPCard handleCreate={handleCreate} />
{filteredList.map(provider => (
<MCPCard
key={provider.id}
data={provider}
currentProvider={currentProvider}
handleSelect={setCurrentProviderID}
onUpdate={refetch}
/>
))}
{!list.length && renderDefaultCard()}
</div>
{currentProvider && (
<MCPDetailPanel
detail={currentProvider}
onHide={() => setCurrentProviderID(undefined)}
onUpdate={refetch}
/>
)}
</>
)
}
export default MCPList

View File

@ -0,0 +1,134 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
import Divider from '@/app/components/base/divider'
import MCPServerParamItem from '@/app/components/tools/mcp/mcp-server-param-item'
import type {
MCPServerDetail,
} from '@/app/components/tools/types'
import {
useCreateMCPServer,
useInvalidateMCPServerDetail,
useUpdateMCPServer,
} from '@/service/use-tools'
import cn from '@/utils/classnames'
export type ModalProps = {
appID: string
latestParams?: any[]
data?: MCPServerDetail
show: boolean
onHide: () => void
}
const MCPServerModal = ({
appID,
latestParams = [],
data,
show,
onHide,
}: ModalProps) => {
const { t } = useTranslation()
const { mutateAsync: createMCPServer, isPending: creating } = useCreateMCPServer()
const { mutateAsync: updateMCPServer, isPending: updating } = useUpdateMCPServer()
const invalidateMCPServerDetail = useInvalidateMCPServerDetail()
const [description, setDescription] = React.useState(data?.description || '')
const [params, setParams] = React.useState(data?.parameters || {})
const handleParamChange = (variable: string, value: string) => {
setParams(prev => ({
...prev,
[variable]: value,
}))
}
const getParamValue = () => {
const res = {} as any
latestParams.map((param) => {
res[param.variable] = params[param.variable]
return param
})
return res
}
const submit = async () => {
if (!data) {
await createMCPServer({
appID,
description,
parameters: getParamValue(),
})
invalidateMCPServerDetail(appID)
onHide()
}
else {
await updateMCPServer({
appID,
id: data.id,
description,
parameters: getParamValue(),
})
invalidateMCPServerDetail(appID)
onHide()
}
}
return (
<Modal
isShow={show}
onClose={onHide}
className={cn('relative !max-w-[520px] !p-0')}
>
<div className='absolute right-5 top-5 z-10 cursor-pointer p-1.5' onClick={onHide}>
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
</div>
<div className='title-2xl-semi-bold relative p-6 pb-3 text-xl text-text-primary'>
{!data ? t('tools.mcp.server.modal.addTitle') : t('tools.mcp.server.modal.editTitle')}
</div>
<div className='space-y-5 px-6 py-3'>
<div className='space-y-0.5'>
<div className='flex h-6 items-center gap-1'>
<div className='system-sm-medium text-text-secondary'>{t('tools.mcp.server.modal.description')}</div>
<div className='system-xs-regular text-text-destructive-secondary'>*</div>
</div>
<Textarea
className='h-[96px] resize-none'
value={description}
placeholder={t('tools.mcp.server.modal.descriptionPlaceholder')}
onChange={e => setDescription(e.target.value)}
></Textarea>
</div>
{latestParams.length > 0 && (
<div>
<div className='mb-1 flex items-center gap-2'>
<div className='system-xs-medium-uppercase shrink-0 text-text-primary'>{t('tools.mcp.server.modal.parameters')}</div>
<Divider type='horizontal' className='!m-0 !h-px grow bg-divider-subtle' />
</div>
<div className='body-xs-regular mb-2 text-text-tertiary'>{t('tools.mcp.server.modal.parametersTip')}</div>
<div className='space-y-3'>
{latestParams.map(paramItem => (
<MCPServerParamItem
key={paramItem.variable}
data={paramItem}
value={params[paramItem.variable] || ''}
onChange={value => handleParamChange(paramItem.variable, value)}
/>
))}
</div>
</div>
)}
</div>
<div className='flex flex-row-reverse p-6 pt-5'>
<Button disabled={!description || creating || updating} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.server.modal.confirm')}</Button>
<Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button>
</div>
</Modal>
)
}
export default MCPServerModal

View File

@ -0,0 +1,37 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Textarea from '@/app/components/base/textarea'
type Props = {
data?: any
value: string
onChange: (value: string) => void
}
const MCPServerParamItem = ({
data,
value,
onChange,
}: Props) => {
const { t } = useTranslation()
return (
<div className='space-y-0.5'>
<div className='flex h-6 items-center gap-2'>
<div className='system-xs-medium text-text-secondary'>{data.label}</div>
<div className='system-xs-medium text-text-quaternary'>·</div>
<div className='system-xs-medium text-text-secondary'>{data.variable}</div>
<div className='system-xs-medium text-text-tertiary'>{data.type}</div>
</div>
<Textarea
className='h-8 resize-none'
value={value}
placeholder={t('tools.mcp.server.modal.parametersPlaceholder')}
onChange={e => onChange(e.target.value)}
></Textarea>
</div>
)
}
export default MCPServerParamItem

View File

@ -0,0 +1,215 @@
'use client'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiLoopLeftLine,
} from '@remixicon/react'
import {
Mcp,
} from '@/app/components/base/icons/src/vender/other'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import Divider from '@/app/components/base/divider'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Confirm from '@/app/components/base/confirm'
import type { AppDetailResponse } from '@/models/app'
import { useAppContext } from '@/context/app-context'
import type { AppSSO } from '@/types/app'
import Indicator from '@/app/components/header/indicator'
import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal'
import { useAppWorkflow } from '@/service/use-workflow'
import {
useInvalidateMCPServerDetail,
useMCPServerDetail,
useRefreshMCPServerCode,
useUpdateMCPServer,
} from '@/service/use-tools'
import { BlockEnum } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
export type IAppCardProps = {
appInfo: AppDetailResponse & Partial<AppSSO>
}
function MCPServiceCard({
appInfo,
}: IAppCardProps) {
const { t } = useTranslation()
const { mutateAsync: updateMCPServer } = useUpdateMCPServer()
const { mutateAsync: refreshMCPServerCode, isPending: genLoading } = useRefreshMCPServerCode()
const invalidateMCPServerDetail = useInvalidateMCPServerDetail()
const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext()
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showMCPServerModal, setShowMCPServerModal] = useState(false)
const { data: currentWorkflow } = useAppWorkflow(appInfo.id)
const { data: detail } = useMCPServerDetail(appInfo.id)
const { id, status, server_code } = detail ?? {}
const appUnpublished = !currentWorkflow?.graph
const serverPublished = !!id
const serverActivated = status === 'active'
const serverURL = serverPublished ? `${globalThis.location.protocol}//${globalThis.location.host}/api/server/${server_code}/mcp` : '***********'
const toggleDisabled = !isCurrentWorkspaceEditor || appUnpublished
const [activated, setActivated] = useState(serverActivated)
const latestParams = useMemo(() => {
if (!currentWorkflow?.graph)
return []
const startNode = currentWorkflow?.graph.nodes.find(node => node.data.type === BlockEnum.Start) as any
return startNode?.data.variables as any[] || []
}, [currentWorkflow])
const onGenCode = async () => {
await refreshMCPServerCode(detail?.id || '')
invalidateMCPServerDetail(appInfo.id)
}
const onChangeStatus = async (state: boolean) => {
setActivated(state)
if (state) {
if (!serverPublished)
setShowMCPServerModal(true)
await updateMCPServer({
appID: appInfo.id,
id: id || '',
description: detail?.description || '',
parameters: detail?.parameters || {},
status: 'active',
})
invalidateMCPServerDetail(appInfo.id)
}
else {
await updateMCPServer({
appID: appInfo.id,
id: id || '',
description: detail?.description || '',
parameters: detail?.parameters || {},
status: 'inactive',
})
invalidateMCPServerDetail(appInfo.id)
}
}
const handleServerModalHide = () => {
setShowMCPServerModal(false)
if (!serverActivated)
setActivated(false)
}
useEffect(() => {
setActivated(serverActivated)
}, [serverActivated])
if (!currentWorkflow)
return null
return (
<>
<div className={cn('w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight')}>
<div className='rounded-xl bg-background-default'>
<div className='flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3'>
<div className='flex w-full items-center gap-3 self-stretch'>
<div className='flex grow items-center'>
<div className='mr-3 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-indigo-indigo-500 p-1 shadow-md'>
<Mcp className='h-4 w-4 text-text-primary-on-surface' />
</div>
<div className="group w-full">
<div className="system-md-semibold min-w-0 overflow-hidden text-ellipsis break-normal text-text-secondary group-hover:text-text-primary">
{t('tools.mcp.server.title')}
</div>
</div>
</div>
<div className='flex items-center gap-1'>
<Indicator color={serverActivated ? 'green' : 'yellow'} />
<div className={`${serverActivated ? 'text-text-success' : 'text-text-warning'} system-xs-semibold-uppercase`}>
{serverActivated
? t('appOverview.overview.status.running')
: t('appOverview.overview.status.disable')}
</div>
</div>
<Tooltip
popupContent={appUnpublished ? t('tools.mcp.server.publishTip') : ''}
>
<div>
<Switch defaultValue={activated} onChange={onChangeStatus} disabled={toggleDisabled} />
</div>
</Tooltip>
</div>
<div className='flex flex-col items-start justify-center self-stretch'>
<div className="system-xs-medium pb-1 text-text-tertiary">
{t('tools.mcp.server.url')}
</div>
<div className="inline-flex h-9 w-full items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 pl-2">
<div className="flex h-4 min-w-0 flex-1 items-start justify-start gap-2 px-1">
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-xs font-medium text-text-secondary">
{serverURL}
</div>
</div>
{serverPublished && (
<>
<CopyFeedback
content={serverURL}
className={'!size-6'}
/>
<Divider type="vertical" className="!mx-0.5 !h-3.5 shrink-0" />
{isCurrentWorkspaceManager && (
<Tooltip
popupContent={t('appOverview.overview.appInfo.regenerate') || ''}
>
<div
className="cursor-pointer rounded-md p-1 hover:bg-state-base-hover"
onClick={() => setShowConfirmDelete(true)}
>
<RiLoopLeftLine className={cn('h-4 w-4 text-text-tertiary hover:text-text-secondary', genLoading && 'animate-spin')}/>
</div>
</Tooltip>
)}
</>
)}
</div>
</div>
</div>
<div className='flex items-center gap-1 self-stretch p-3'>
<Button
disabled={toggleDisabled}
size='small'
variant='ghost'
onClick={() => setShowMCPServerModal(true)}
>
{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}
</Button>
</div>
</div>
</div>
{showMCPServerModal && (
<MCPServerModal
show={showMCPServerModal}
appID={appInfo.id}
data={serverPublished ? detail : undefined}
latestParams={latestParams}
onHide={handleServerModalHide}
/>
)}
{/* button copy link/ button regenerate */}
{showConfirmDelete && (
<Confirm
type='warning'
title={t('appOverview.overview.appInfo.regenerate')}
content={t('tools.mcp.server.reGen')}
isShow={showConfirmDelete}
onConfirm={() => {
onGenCode()
setShowConfirmDelete(false)
}}
onCancel={() => setShowConfirmDelete(false)}
/>
)}
</>
)
}
export default MCPServiceCard

View File

@ -0,0 +1,154 @@
const tools = [
{
author: 'Novice',
name: 'NOTION_ADD_PAGE_CONTENT',
label: {
en_US: 'NOTION_ADD_PAGE_CONTENT',
zh_Hans: 'NOTION_ADD_PAGE_CONTENT',
pt_BR: 'NOTION_ADD_PAGE_CONTENT',
ja_JP: 'NOTION_ADD_PAGE_CONTENT',
},
description: {
en_US: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
zh_Hans: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
pt_BR: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
ja_JP: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
},
parameters: [
{
name: 'after',
label: {
en_US: 'after',
zh_Hans: 'after',
pt_BR: 'after',
ja_JP: 'after',
},
placeholder: null,
scope: null,
auto_generate: null,
template: null,
required: false,
default: null,
min: null,
max: null,
precision: null,
options: [],
type: 'string',
human_description: {
en_US: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
zh_Hans: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
pt_BR: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
ja_JP: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
},
form: 'llm',
llm_description: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
},
{
name: 'content_block',
label: {
en_US: 'content_block',
zh_Hans: 'content_block',
pt_BR: 'content_block',
ja_JP: 'content_block',
},
placeholder: null,
scope: null,
auto_generate: null,
template: null,
required: false,
default: null,
min: null,
max: null,
precision: null,
options: [],
type: 'string',
human_description: {
en_US: 'Child content to append to a page.',
zh_Hans: 'Child content to append to a page.',
pt_BR: 'Child content to append to a page.',
ja_JP: 'Child content to append to a page.',
},
form: 'llm',
llm_description: 'Child content to append to a page.',
},
{
name: 'parent_block_id',
label: {
en_US: 'parent_block_id',
zh_Hans: 'parent_block_id',
pt_BR: 'parent_block_id',
ja_JP: 'parent_block_id',
},
placeholder: null,
scope: null,
auto_generate: null,
template: null,
required: false,
default: null,
min: null,
max: null,
precision: null,
options: [],
type: 'string',
human_description: {
en_US: 'The ID of the page which the children will be added.',
zh_Hans: 'The ID of the page which the children will be added.',
pt_BR: 'The ID of the page which the children will be added.',
ja_JP: 'The ID of the page which the children will be added.',
},
form: 'llm',
llm_description: 'The ID of the page which the children will be added.',
},
],
labels: [],
output_schema: null,
},
]
export const listData = [
{
id: 'fdjklajfkljadslf111',
author: 'KVOJJJin',
name: 'GOGOGO',
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
server_url: 'https://mcp.composio.dev/notion/****/abc',
type: 'mcp',
is_team_authorization: true,
tools,
update_elapsed_time: 1744793369,
label: {
en_US: 'GOGOGO',
zh_Hans: 'GOGOGO',
},
},
{
id: 'fdjklajfkljadslf222',
author: 'KVOJJJin',
name: 'GOGOGO2',
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
server_url: 'https://mcp.composio.dev/notion/****/abc',
type: 'mcp',
is_team_authorization: false,
tools: [],
update_elapsed_time: 1744793369,
label: {
en_US: 'GOGOGO2',
zh_Hans: 'GOGOGO2',
},
},
{
id: 'fdjklajfkljadslf333',
author: 'KVOJJJin',
name: 'GOGOGO3',
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
server_url: 'https://mcp.composio.dev/notion/****/abc',
type: 'mcp',
is_team_authorization: true,
tools,
update_elapsed_time: 1744793369,
label: {
en_US: 'GOGOGO3',
zh_Hans: 'GOGOGO3',
},
},
]

View File

@ -0,0 +1,135 @@
'use client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIcon from '@/app/components/base/app-icon'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import type { AppIconType } from '@/types/app'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { noop } from 'lodash-es'
import cn from '@/utils/classnames'
export type DuplicateAppModalProps = {
data?: ToolWithProvider
show: boolean
onConfirm: (info: {
name: string
server_url: string
icon_type: AppIconType
icon: string
icon_background?: string | null
}) => void
onHide: () => void
}
const DEFAULT_ICON = { type: 'emoji', icon: '🧿', background: '#EFF1F5' }
const extractFileId = (url: string) => {
const match = url.match(/files\/(.+?)\/file-preview/)
return match ? match[1] : null
}
const getIcon = (data?: ToolWithProvider) => {
if (!data)
return DEFAULT_ICON as AppIconSelection
if (typeof data.icon === 'string')
return { type: 'image', url: data.icon, fileId: extractFileId(data.icon) } as AppIconSelection
return {
...data.icon,
icon: data.icon.content,
type: 'emoji',
} as unknown as AppIconSelection
}
const MCPModal = ({
data,
show,
onConfirm,
onHide,
}: DuplicateAppModalProps) => {
const { t } = useTranslation()
const [name, setName] = React.useState(data?.name || '')
const [appIcon, setAppIcon] = useState<AppIconSelection>(getIcon(data))
const [url, setUrl] = React.useState(data?.server_url || '')
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const submit = async () => {
await onConfirm({
name,
server_url: url,
icon_type: appIcon.type,
icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
})
onHide()
}
return (
<>
<Modal
isShow={show}
onClose={noop}
className={cn('relative !max-w-[520px]', 'p-6')}
>
<div className='absolute right-5 top-5 z-10 cursor-pointer p-1.5' onClick={onHide}>
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
</div>
<div className='title-2xl-semi-bold relative pb-3 text-xl text-text-primary'>{t('tools.mcp.modal.title')}</div>
<div className='space-y-5 py-3'>
<div className='flex space-x-3'>
<div className='grow pb-1'>
<div className='mb-1 flex h-6 items-center'>
<span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.name')}</span>
</div>
<Input
value={name}
onChange={e => setName(e.target.value)}
placeholder={t('tools.mcp.modal.namePlaceholder')}
/>
</div>
<div className='pt-2'>
<AppIcon
iconType={appIcon.type}
icon={appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId}
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
size='xxl' className='cursor-pointer rounded-2xl'
onClick={() => { setShowAppIconPicker(true) }}
/>
</div>
</div>
<div>
<div className='mb-1 flex h-6 items-center'>
<span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.serverUrl')}</span>
</div>
<Input
value={url}
onChange={e => setUrl(e.target.value)}
placeholder={t('tools.mcp.modal.serverUrlPlaceholder')}
/>
</div>
</div>
<div className='flex flex-row-reverse pt-5'>
<Button disabled={!name || !url} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.modal.confirm')}</Button>
<Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button>
</div>
</Modal>
{showAppIconPicker && <AppIconPicker
onSelect={(payload) => {
setAppIcon(payload)
setShowAppIconPicker(false)
}}
onClose={() => {
setAppIcon(getIcon(data))
setShowAppIconPicker(false)
}}
/>}
</>
)
}
export default MCPModal

View File

@ -0,0 +1,154 @@
'use client'
import { useCallback, useState } from 'react'
import { useBoolean } from 'ahooks'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import { RiHammerFill } from '@remixicon/react'
import Indicator from '@/app/components/header/indicator'
import Icon from '@/app/components/plugins/card/base/card-icon'
import { useFormatTimeFromNow } from './hooks'
import type { ToolWithProvider } from '../../workflow/types'
import Confirm from '@/app/components/base/confirm'
import MCPModal from './modal'
import OperationDropdown from './detail/operation-dropdown'
import { useDeleteMCP, useUpdateMCP } from '@/service/use-tools'
import cn from '@/utils/classnames'
type Props = {
currentProvider?: ToolWithProvider
data: ToolWithProvider
handleSelect: (providerID: string) => void
onUpdate: () => void
}
const MCPCard = ({
currentProvider,
data,
onUpdate,
handleSelect,
}: Props) => {
const { t } = useTranslation()
const { formatTimeFromNow } = useFormatTimeFromNow()
const { isCurrentWorkspaceManager } = useAppContext()
const { mutate: updateMCP } = useUpdateMCP({
onSuccess: onUpdate,
})
const { mutate: deleteMCP } = useDeleteMCP({
onSuccess: onUpdate,
})
const [isOperationShow, setIsOperationShow] = useState(false)
const [isShowUpdateModal, {
setTrue: showUpdateModal,
setFalse: hideUpdateModal,
}] = useBoolean(false)
const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)
const [deleting, {
setTrue: showDeleting,
setFalse: hideDeleting,
}] = useBoolean(false)
const handleUpdate = useCallback(async (form: any) => {
const res = await updateMCP({
...form,
provider_id: data.id,
})
if ((res as any)?.result === 'success') {
hideUpdateModal()
onUpdate()
}
}, [data, updateMCP, hideUpdateModal, onUpdate])
const handleDelete = useCallback(async () => {
showDeleting()
const res = await deleteMCP(data.id)
hideDeleting()
if ((res as any)?.result === 'success') {
hideDeleteConfirm()
onUpdate()
}
}, [showDeleting, deleteMCP, data.id, hideDeleting, hideDeleteConfirm, onUpdate])
return (
<div
onClick={() => handleSelect(data.id)}
className={cn(
'group relative flex cursor-pointer flex-col rounded-xl border-[1.5px] border-transparent bg-components-card-bg shadow-xs hover:bg-components-card-bg-alt hover:shadow-md',
currentProvider?.id === data.id && 'border-components-option-card-option-selected-border bg-components-card-bg-alt',
)}
>
<div className='flex grow items-center gap-3 rounded-t-xl p-4'>
<div className='shrink-0 overflow-hidden rounded-xl border border-components-panel-border-subtle'>
<Icon src={data.icon} />
</div>
<div className='grow'>
<div className='system-md-semibold mb-1 truncate text-text-secondary' title={data.name}>{data.name}</div>
<div className='flex items-center gap-2'>
<div className='flex items-center gap-1'>
<RiHammerFill className='h-3 w-3 shrink-0 text-text-quaternary' />
{data.tools.length > 0 && (
<div className='system-xs-regular grow text-text-tertiary'>{t('tools.mcp.toolsCount', { count: data.tools.length })}</div>
)}
{!data.tools.length && (
<div className='system-xs-regular grow text-text-tertiary'>{t('tools.mcp.noTools')}</div>
)}
</div>
<div className='system-xs-regular text-divider-deep'>/</div>
<div className='system-xs-regular truncate text-text-tertiary'>{`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at!)}`}</div>
</div>
</div>
</div>
<div className='flex items-center gap-1 rounded-b-xl pb-2.5 pl-4 pr-2.5 pt-1.5'>
<div className='system-xs-regular grow truncate text-text-tertiary' title={data.server_url}>{data.server_url}</div>
{data.is_team_authorization && data.tools.length > 0 && <Indicator color='green' className='shrink-0' />}
{(!data.is_team_authorization || !data.tools.length) && (
<div className='system-xs-medium flex shrink-0 items-center gap-1 rounded-md border border-util-colors-red-red-500 bg-components-badge-bg-red-soft px-1.5 py-0.5 text-util-colors-red-red-500'>
{t('tools.mcp.noConfigured')}
<Indicator color='red' />
</div>
)}
</div>
{isCurrentWorkspaceManager && (
<div className={cn('absolute right-2.5 top-2.5 hidden group-hover:block', isOperationShow && 'block')} onClick={e => e.stopPropagation()}>
<OperationDropdown
inCard
onOpenChange={setIsOperationShow}
onEdit={showUpdateModal}
onRemove={showDeleteConfirm}
/>
</div>
)}
{isShowUpdateModal && (
<MCPModal
data={data}
show={isShowUpdateModal}
onConfirm={handleUpdate}
onHide={hideUpdateModal}
/>
)}
{isShowDeleteConfirm && (
<Confirm
isShow
title={t('tools.mcp.delete')}
content={
<div>
{t('tools.mcp.deleteConfirmTitle', { mcp: data.name })}
</div>
}
onCancel={hideDeleteConfirm}
onConfirm={handleDelete}
isLoading={deleting}
isDisabled={deleting}
/>
)}
</div>
)
}
export default MCPCard

View File

@ -1,6 +1,7 @@
'use client'
import { useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSearchParams } from 'next/navigation'
import type { Collection } from './types'
import Marketplace from './marketplace'
import cn from '@/utils/classnames'
@ -15,6 +16,7 @@ import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty'
import Card from '@/app/components/plugins/card'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
import MCPList from './mcp'
import { useAllToolProviders } from '@/service/use-tools'
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useGlobalPublicStore } from '@/context/global-public-context'
@ -24,13 +26,18 @@ const ProviderList = () => {
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const containerRef = useRef<HTMLDivElement>(null)
const searchParams = useSearchParams()
const authCode = searchParams.get('code') || ''
const providerID = searchParams.get('state') || ''
const [activeTab, setActiveTab] = useTabSearchParams({
defaultTab: 'builtin',
defaultTab: authCode && providerID ? 'mcp' : 'builtin',
})
const options = [
{ value: 'builtin', text: t('tools.type.builtIn') },
{ value: 'api', text: t('tools.type.custom') },
{ value: 'workflow', text: t('tools.type.workflow') },
{ value: 'mcp', text: 'MCP' },
]
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
const handleTagsChange = (value: string[]) => {
@ -85,7 +92,9 @@ const ProviderList = () => {
options={options}
/>
<div className='flex items-center gap-2'>
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
{activeTab !== 'mcp' && (
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
)}
<Input
showLeftIcon
showClearIcon
@ -96,7 +105,7 @@ const ProviderList = () => {
/>
</div>
</div>
{(filteredCollectionList.length > 0 || activeTab !== 'builtin') && (
{activeTab !== 'mcp' && (
<div className={cn(
'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
!filteredCollectionList.length && activeTab === 'workflow' && 'grow',
@ -133,19 +142,20 @@ const ProviderList = () => {
{!filteredCollectionList.length && activeTab === 'builtin' && (
<Empty lightCard text={t('tools.noTools')} className='h-[224px] px-12' />
)}
{
enable_marketplace && activeTab === 'builtin' && (
<Marketplace
onMarketplaceScroll={() => {
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
}}
searchPluginText={keywords}
filterPluginTags={tagFilterValue}
/>
)
}
</div >
</div >
{enable_marketplace && activeTab === 'builtin' && (
<Marketplace
onMarketplaceScroll={() => {
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
}}
searchPluginText={keywords}
filterPluginTags={tagFilterValue}
/>
)}
{activeTab === 'mcp' && (
<MCPList searchText={keywords} />
)}
</div>
</div>
{currentProvider && !currentProvider.plugin_id && (
<ProviderDetail
collection={currentProvider}

View File

@ -3,13 +3,13 @@ import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import {
RiAddLine,
RiAddCircleFill,
RiArrowRightUpLine,
RiBookOpenLine,
} from '@remixicon/react'
import type { CustomCollectionBackend } from '../types'
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
import { createCustomCollection } from '@/service/tools'
import Toast from '@/app/components/base/toast'
@ -45,20 +45,20 @@ const Contribute = ({ onRefreshData }: Props) => {
return (
<>
{isCurrentWorkspaceManager && (
<div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl border-[0.5px] border-divider-subtle bg-components-panel-on-panel-item-bg transition-all duration-200 ease-in-out hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg'>
<div className='group grow rounded-t-xl hover:bg-background-body' onClick={() => setIsShowEditCustomCollectionModal(true)}>
<div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out'>
<div className='group grow rounded-t-xl' onClick={() => setIsShowEditCustomCollectionModal(true)}>
<div className='flex shrink-0 items-center p-4 pb-3'>
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg group-hover:border-components-option-card-option-border-hover group-hover:bg-components-option-card-option-bg-hover'>
<RiAddLine className='h-4 w-4 text-text-tertiary group-hover:text-text-accent'/>
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'>
<RiAddCircleFill className='h-4 w-4 text-text-quaternary group-hover:text-text-accent'/>
</div>
<div className='ml-3 text-sm font-semibold leading-5 text-text-primary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div>
<div className='system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div>
</div>
</div>
<div className='rounded-b-xl border-t-[0.5px] border-divider-regular px-4 py-3 text-text-tertiary hover:bg-background-body hover:text-text-accent'>
<div className='rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent'>
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
<BookOpen01 className='h-3 w-3 shrink-0' />
<div className='grow truncate text-xs font-normal leading-[18px]' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
<ArrowUpRight className='h-3 w-3 shrink-0' />
<RiBookOpenLine className='h-3 w-3 shrink-0' />
<div className='system-xs-regular grow truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
<RiArrowRightUpLine className='h-3 w-3 shrink-0' />
</a>
</div>
</div>

View File

@ -29,6 +29,7 @@ export enum CollectionType {
custom = 'api',
model = 'model',
workflow = 'workflow',
mcp = 'mcp',
}
export type Emoji = {
@ -50,6 +51,9 @@ export type Collection = {
labels: string[]
plugin_id?: string
letter?: string
// MCP Server
server_url?: string
updated_at?: number
}
export type ToolParameter = {
@ -168,3 +172,11 @@ export type WorkflowToolProviderResponse = {
}
privacy_policy: string
}
export type MCPServerDetail = {
id: string
server_code: string
description: string
status: string
parameters?: Record<string, string>
}

View File

@ -24,7 +24,7 @@ import {
} from '@/app/components/workflow/types'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import { useToastContext } from '@/app/components/base/toast'
import { usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
import { useInvalidateAppWorkflow, usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
import type { PublishWorkflowParams } from '@/types/workflow'
import { fetchAppDetail } from '@/service/apps'
import { useStore as useAppStore } from '@/app/components/app/store'
@ -90,6 +90,7 @@ const FeaturesTrigger = () => {
}
}, [appID, setAppDetail])
const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!)
const updatePublishedWorkflow = useInvalidateAppWorkflow()
const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
if (await handleCheckBeforePublish()) {
const res = await publishWorkflow({
@ -99,6 +100,7 @@ const FeaturesTrigger = () => {
if (res) {
notify({ type: 'success', message: t('common.api.actionSuccess') })
updatePublishedWorkflow(appID!)
updateAppDetail()
workflowStore.getState().setPublishedAt(res.created_at)
resetWorkflowVersionHistory()
@ -107,7 +109,7 @@ const FeaturesTrigger = () => {
else {
throw new Error('Checklist failed')
}
}, [handleCheckBeforePublish, notify, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail])
}, [handleCheckBeforePublish, publishWorkflow, notify, t, updatePublishedWorkflow, appID, updateAppDetail, workflowStore, resetWorkflowVersionHistory])
const onPublisherToggle = useCallback((state: boolean) => {
if (state)

View File

@ -5,10 +5,11 @@ import {
useState,
} from 'react'
import type {
BlockEnum,
OnSelectBlock,
ToolWithProvider,
} from '../types'
import type { ToolValue } from './types'
import type { ToolDefaultValue, ToolValue } from './types'
import { ToolTypeEnum } from './types'
import Tools from './tools'
import { useToolTabs } from './hooks'
@ -17,8 +18,6 @@ import cn from '@/utils/classnames'
import { useGetLanguage } from '@/context/i18n'
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
import PluginList, { type ListProps } from '@/app/components/workflow/block-selector/market-place-plugin/list'
import ActionButton from '../../base/action-button'
import { RiAddLine } from '@remixicon/react'
import { PluginType } from '../../plugins/types'
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
import { useGlobalPublicStore } from '@/context/global-public-context'
@ -31,11 +30,12 @@ type AllToolsProps = {
buildInTools: ToolWithProvider[]
customTools: ToolWithProvider[]
workflowTools: ToolWithProvider[]
mcpTools: ToolWithProvider[]
onSelect: OnSelectBlock
supportAddCustomTool?: boolean
onAddedCustomTool?: () => void
onShowAddCustomCollectionModal?: () => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
isHideMCPTools?: boolean
}
const DEFAULT_TAGS: AllToolsProps['tags'] = []
@ -46,15 +46,17 @@ const AllTools = ({
searchText,
tags = DEFAULT_TAGS,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
buildInTools,
workflowTools,
customTools,
supportAddCustomTool,
onShowAddCustomCollectionModal,
mcpTools = [],
selectedTools,
isHideMCPTools,
}: AllToolsProps) => {
const language = useGetLanguage()
const tabs = useToolTabs()
const tabs = useToolTabs(isHideMCPTools)
const [activeTab, setActiveTab] = useState(ToolTypeEnum.All)
const [activeView, setActiveView] = useState<ViewType>(ViewType.flat)
const hasFilter = searchText || tags.length > 0
@ -64,13 +66,15 @@ const AllTools = ({
const tools = useMemo(() => {
let mergedTools: ToolWithProvider[] = []
if (activeTab === ToolTypeEnum.All)
mergedTools = [...buildInTools, ...customTools, ...workflowTools]
mergedTools = [...buildInTools, ...customTools, ...workflowTools, ...mcpTools]
if (activeTab === ToolTypeEnum.BuiltIn)
mergedTools = buildInTools
if (activeTab === ToolTypeEnum.Custom)
mergedTools = customTools
if (activeTab === ToolTypeEnum.Workflow)
mergedTools = workflowTools
if (activeTab === ToolTypeEnum.MCP)
mergedTools = mcpTools
if (!hasFilter)
return mergedTools.filter(toolWithProvider => toolWithProvider.tools.length > 0)
@ -80,7 +84,7 @@ const AllTools = ({
return tool.label[language].toLowerCase().includes(searchText.toLowerCase()) || tool.name.toLowerCase().includes(searchText.toLowerCase())
})
})
}, [activeTab, buildInTools, customTools, workflowTools, searchText, language, hasFilter])
}, [activeTab, buildInTools, customTools, workflowTools, mcpTools, searchText, language, hasFilter])
const {
queryPluginsWithDebounced: fetchPlugins,
@ -103,10 +107,11 @@ const AllTools = ({
const pluginRef = useRef<ListRef>(null)
const wrapElemRef = useRef<HTMLDivElement>(null)
const isSupportGroupView = [ToolTypeEnum.All, ToolTypeEnum.BuiltIn].includes(activeTab)
return (
<div className={cn(className)}>
<div className='flex items-center justify-between border-b-[0.5px] border-divider-subtle bg-background-default-hover px-3 shadow-xs'>
<div className='flex items-center justify-between border-b border-divider-subtle px-3'>
<div className='flex h-8 items-center space-x-1'>
{
tabs.map(tab => (
@ -124,17 +129,8 @@ const AllTools = ({
))
}
</div>
<ViewTypeSelect viewType={activeView} onChange={setActiveView} />
{supportAddCustomTool && (
<div className='flex items-center'>
<div className='mr-1.5 h-3.5 w-px bg-divider-regular'></div>
<ActionButton
className='bg-components-button-primary-bg text-components-button-primary-text hover:bg-components-button-primary-bg hover:text-components-button-primary-text'
onClick={onShowAddCustomCollectionModal}
>
<RiAddLine className='h-4 w-4' />
</ActionButton>
</div>
{isSupportGroupView && (
<ViewTypeSelect viewType={activeView} onChange={setActiveView} />
)}
</div>
<div
@ -147,7 +143,9 @@ const AllTools = ({
showWorkflowEmpty={activeTab === ToolTypeEnum.Workflow}
tools={tools}
onSelect={onSelect}
viewType={activeView}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
viewType={isSupportGroupView ? activeView : ViewType.flat}
hasSearchText={!!searchText}
selectedTools={selectedTools}
/>

View File

@ -31,10 +31,9 @@ export const useTabs = () => {
]
}
export const useToolTabs = () => {
export const useToolTabs = (isHideMCPTools?: boolean) => {
const { t } = useTranslation()
return [
const tabs = [
{
key: ToolTypeEnum.All,
name: t('workflow.tabs.allTool'),
@ -52,4 +51,12 @@ export const useToolTabs = () => {
name: t('workflow.tabs.workflowTool'),
},
]
if(!isHideMCPTools) {
tabs.push({
key: ToolTypeEnum.MCP,
name: 'MCP',
})
}
return tabs
}

View File

@ -129,33 +129,34 @@ const NodeSelector: FC<NodeSelectorProps> = ({
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}>
<div className='px-2 pt-2' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
<Input
showLeftIcon
showClearIcon
autoFocus
value={searchText}
placeholder={searchPlaceholder}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
)}
{activeTab === TabsEnum.Tools && (
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
/>
)}
</div>
<Tabs
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
filterElem={
<div className='relative m-2' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
<Input
showLeftIcon
showClearIcon
autoFocus
value={searchText}
placeholder={searchPlaceholder}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
)}
{activeTab === TabsEnum.Tools && (
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
/>
)}
</div>
}
onSelect={handleSelect}
searchText={searchText}
tags={tags}

View File

@ -1,6 +1,6 @@
import type { FC } from 'react'
import { memo } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
import type { BlockEnum } from '../types'
import { useTabs } from './hooks'
import type { ToolDefaultValue } from './types'
@ -16,6 +16,7 @@ export type TabsProps = {
tags: string[]
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
filterElem: React.ReactNode
noBlocks?: boolean
}
const Tabs: FC<TabsProps> = ({
@ -25,26 +26,28 @@ const Tabs: FC<TabsProps> = ({
searchText,
onSelect,
availableBlocksTypes,
filterElem,
noBlocks,
}) => {
const tabs = useTabs()
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
return (
<div onClick={e => e.stopPropagation()}>
{
!noBlocks && (
<div className='flex items-center border-b-[0.5px] border-divider-subtle px-3'>
<div className='relative flex bg-background-section-burn pl-1 pt-1'>
{
tabs.map(tab => (
<div
key={tab.key}
className={cn(
'system-sm-medium relative mr-4 cursor-pointer pb-2 pt-1',
'system-sm-medium relative mr-0.5 flex h-8 cursor-pointer items-center rounded-t-lg px-3 ',
activeTab === tab.key
? 'text-text-primary after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-util-colors-blue-brand-blue-brand-600'
? 'sm-no-bottom cursor-default bg-components-panel-bg text-text-accent'
: 'text-text-tertiary',
)}
onClick={() => onActiveTabChange(tab.key)}
@ -56,13 +59,16 @@ const Tabs: FC<TabsProps> = ({
</div>
)
}
{filterElem}
{
activeTab === TabsEnum.Blocks && !noBlocks && (
<Blocks
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={availableBlocksTypes}
/>
<div className='border-t border-divider-subtle'>
<Blocks
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={availableBlocksTypes}
/>
</div>
)
}
{
@ -72,9 +78,12 @@ const Tabs: FC<TabsProps> = ({
searchText={searchText}
onSelect={onSelect}
tags={tags}
canNotSelectMultiple
buildInTools={buildInTools || []}
customTools={customTools || []}
workflowTools={workflowTools || []}
mcpTools={mcpTools || []}
isHideMCPTools
/>
)
}

View File

@ -23,7 +23,7 @@ import {
} from '@/service/tools'
import type { CustomCollectionBackend } from '@/app/components/tools/types'
import Toast from '@/app/components/base/toast'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools'
import cn from '@/utils/classnames'
type Props = {
@ -35,6 +35,7 @@ type Props = {
isShow: boolean
onShowChange: (isShow: boolean) => void
onSelect: (tool: ToolDefaultValue) => void
onSelectMultiple: (tools: ToolDefaultValue[]) => void
supportAddCustomTool?: boolean
scope?: string
selectedTools?: ToolValue[]
@ -48,6 +49,7 @@ const ToolPicker: FC<Props> = ({
isShow,
onShowChange,
onSelect,
onSelectMultiple,
supportAddCustomTool,
scope = 'all',
selectedTools,
@ -61,6 +63,7 @@ const ToolPicker: FC<Props> = ({
const { data: customTools } = useAllCustomTools()
const invalidateCustomTools = useInvalidateAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const { builtinToolList, customToolList, workflowToolList } = useMemo(() => {
if (scope === 'plugins') {
@ -102,6 +105,10 @@ const ToolPicker: FC<Props> = ({
onSelect(tool!)
}
const handleSelectMultiple = (_type: BlockEnum, tools: ToolDefaultValue[]) => {
onSelectMultiple(tools)
}
const [isShowEditCollectionToolModal, {
setFalse: hideEditCustomCollectionModal,
setTrue: showEditCustomCollectionModal,
@ -151,6 +158,10 @@ const ToolPicker: FC<Props> = ({
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
supportAddCustomTool={supportAddCustomTool}
onAddedCustomTool={handleAddedCustomTool}
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
/>
</div>
<AllTools
@ -159,12 +170,12 @@ const ToolPicker: FC<Props> = ({
tags={tags}
searchText={searchText}
onSelect={handleSelect}
onSelectMultiple={handleSelectMultiple}
buildInTools={builtinToolList || []}
customTools={customToolList || []}
workflowTools={workflowToolList || []}
supportAddCustomTool={supportAddCustomTool}
onAddedCustomTool={handleAddedCustomTool}
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
mcpTools={mcpTools || []}
selectedTools={selectedTools}
/>
</div>

View File

@ -10,8 +10,6 @@ import { useGetLanguage } from '@/context/i18n'
import BlockIcon from '../../block-icon'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import { RiCheckLine } from '@remixicon/react'
import Badge from '@/app/components/base/badge'
type Props = {
provider: ToolWithProvider
@ -74,15 +72,12 @@ const ToolItem: FC<Props> = ({
})
}}
>
<div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary', disabled && 'opacity-30')}>{payload.label[language]}</div>
{disabled && <Badge
className='flex h-5 items-center space-x-0.5 text-text-tertiary'
uppercase
>
<RiCheckLine className='h-3 w-3 ' />
<div>{t('tools.addToolModal.added')}</div>
</Badge>
}
<div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary')}>
<span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span>
</div>
{disabled && (
<div className='system-xs-regular mr-4 text-text-tertiary'>{t('tools.addToolModal.added')}</div>
)}
</div>
</Tooltip >
)

View File

@ -13,6 +13,8 @@ type Props = {
isShowLetterIndex: boolean
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
letters: string[]
toolRefs: any
selectedTools?: ToolValue[]
@ -24,6 +26,8 @@ const ToolViewFlatView: FC<Props> = ({
isShowLetterIndex,
hasSearchText,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
toolRefs,
selectedTools,
}) => {
@ -53,6 +57,8 @@ const ToolViewFlatView: FC<Props> = ({
isShowLetterIndex={isShowLetterIndex}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
/>
</div>

View File

@ -12,6 +12,8 @@ type Props = {
toolList: ToolWithProvider[]
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
}
@ -20,6 +22,8 @@ const Item: FC<Props> = ({
toolList,
hasSearchText,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
selectedTools,
}) => {
return (
@ -36,6 +40,8 @@ const Item: FC<Props> = ({
isShowLetterIndex={false}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
/>
))}

View File

@ -12,6 +12,8 @@ type Props = {
payload: Record<string, ToolWithProvider[]>
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
}
@ -19,6 +21,8 @@ const ToolListTreeView: FC<Props> = ({
payload,
hasSearchText,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
selectedTools,
}) => {
const { t } = useTranslation()
@ -46,6 +50,8 @@ const ToolListTreeView: FC<Props> = ({
toolList={payload[groupName]}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
/>
))}

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useMemo } from 'react'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import cn from '@/utils/classnames'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useGetLanguage } from '@/context/i18n'
@ -13,6 +13,7 @@ import { ViewType } from '../view-type-select'
import ActonItem from './action-item'
import BlockIcon from '../../block-icon'
import { useTranslation } from 'react-i18next'
import { useHover } from 'ahooks'
type Props = {
className?: string
@ -21,6 +22,8 @@ type Props = {
isShowLetterIndex: boolean
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
}
@ -31,18 +34,83 @@ const Tool: FC<Props> = ({
isShowLetterIndex,
hasSearchText,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
selectedTools,
}) => {
const { t } = useTranslation()
const language = useGetLanguage()
const isFlatView = viewType === ViewType.flat
const notShowProvider = payload.type === CollectionType.workflow
const actions = payload.tools
const hasAction = true // Now always support actions
const hasAction = !notShowProvider
const [isFold, setFold] = React.useState<boolean>(true)
const getIsDisabled = (tool: ToolType) => {
const ref = useRef(null)
const isHovering = useHover(ref)
const getIsDisabled = useCallback((tool: ToolType) => {
if (!selectedTools || !selectedTools.length) return false
return selectedTools.some(selectedTool => selectedTool.provider_name === payload.name && selectedTool.tool_name === tool.name)
}
return selectedTools.some(selectedTool => (selectedTool.provider_name === payload.name || selectedTool.provider_name === payload.id) && selectedTool.tool_name === tool.name)
}, [payload.id, payload.name, selectedTools])
const totalToolsNum = actions.length
const selectedToolsNum = actions.filter(action => getIsDisabled(action)).length
const isAllSelected = selectedToolsNum === totalToolsNum
const notShowProviderSelectInfo = useMemo(() => {
if (isAllSelected) {
return (
<span className='system-xs-regular text-text-tertiary'>
{t('tools.addToolModal.added')}
</span>
)
}
}, [isAllSelected, t])
const selectedInfo = useMemo(() => {
if (isHovering && !isAllSelected) {
return (
<span className='system-xs-regular text-components-button-secondary-accent-text'
onClick={(e) => {
onSelectMultiple?.(BlockEnum.Tool, actions.filter(action => !getIsDisabled(action)).map((tool) => {
const params: Record<string, string> = {}
if (tool.parameters) {
tool.parameters.forEach((item) => {
params[item.name] = ''
})
}
return {
provider_id: payload.id,
provider_type: payload.type,
provider_name: payload.name,
tool_name: tool.name,
tool_label: tool.label[language],
tool_description: tool.description[language],
title: tool.label[language],
is_team_authorization: payload.is_team_authorization,
output_schema: tool.output_schema,
paramSchemas: tool.parameters,
params,
}
}))
}}
>
{t('workflow.tabs.addAll')}
</span>
)
}
if (selectedToolsNum === 0)
return <></>
return (
<span className='system-xs-regular text-text-tertiary'>
{isAllSelected
? t('workflow.tabs.allAdded')
: `${selectedToolsNum} / ${totalToolsNum}`
}
</span>
)
}, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum])
useEffect(() => {
if (hasSearchText && isFold) {
setFold(false)
@ -72,29 +140,37 @@ const Tool: FC<Props> = ({
<div
key={payload.id}
className={cn('mb-1 last-of-type:mb-0', isShowLetterIndex && 'mr-6')}
ref={ref}
>
<div className={cn(className)}>
<div
className='flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover'
onClick={() => {
if (hasAction)
if (hasAction) {
setFold(!isFold)
return
}
// Now always support actions
// if (payload.parameters) {
// payload.parameters.forEach((item) => {
// params[item.name] = ''
// })
// }
// onSelect(BlockEnum.Tool, {
// provider_id: payload.id,
// provider_type: payload.type,
// provider_name: payload.name,
// tool_name: payload.name,
// tool_label: payload.label[language],
// title: payload.label[language],
// params: {},
// })
const tool = actions[0]
const params: Record<string, string> = {}
if (tool.parameters) {
tool.parameters.forEach((item) => {
params[item.name] = ''
})
}
onSelect(BlockEnum.Tool, {
provider_id: payload.id,
provider_type: payload.type,
provider_name: payload.name,
tool_name: tool.name,
tool_label: tool.label[language],
tool_description: tool.description[language],
title: tool.label[language],
is_team_authorization: payload.is_team_authorization,
output_schema: tool.output_schema,
paramSchemas: tool.parameters,
params,
})
}}
>
<div className='flex h-8 grow items-center'>
@ -103,20 +179,23 @@ const Tool: FC<Props> = ({
type={BlockEnum.Tool}
toolIcon={payload.icon}
/>
<div className='ml-2 w-0 flex-1 grow truncate text-sm text-text-primary'>{payload.label[language]}</div>
<div className='ml-2 w-0 grow truncate text-sm text-text-primary'>
<span>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span>
{isFlatView && (
<span className='system-xs-regular ml-2 text-text-quaternary'>{groupName}</span>
)}
</div>
</div>
<div className='flex items-center'>
{isFlatView && (
<div className='system-xs-regular text-text-tertiary'>{groupName}</div>
)}
<div className='ml-2 flex items-center'>
{!canNotSelectMultiple && (notShowProvider ? notShowProviderSelectInfo : selectedInfo)}
{hasAction && (
<FoldIcon className={cn('h-4 w-4 shrink-0 text-text-quaternary', isFold && 'text-text-tertiary')} />
)}
</div>
</div>
{hasAction && !isFold && (
{!notShowProvider && hasAction && !isFold && (
actions.map(action => (
<ActonItem
key={action.name}

View File

@ -17,6 +17,8 @@ import classNames from '@/utils/classnames'
type ToolsProps = {
showWorkflowEmpty: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
tools: ToolWithProvider[]
viewType: ViewType
hasSearchText: boolean
@ -27,6 +29,8 @@ type ToolsProps = {
const Blocks = ({
showWorkflowEmpty,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
tools,
viewType,
hasSearchText,
@ -107,6 +111,8 @@ const Blocks = ({
isShowLetterIndex={isShowLetterIndex}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
/>
) : (
@ -114,6 +120,8 @@ const Blocks = ({
payload={treeViewToolsData}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
/>
)

View File

@ -8,6 +8,7 @@ export enum ToolTypeEnum {
BuiltIn = 'built-in',
Custom = 'custom',
Workflow = 'workflow',
MCP = 'mcp',
}
export enum BlockClassificationEnum {
@ -34,6 +35,7 @@ export type ToolDefaultValue = {
export type ToolValue = {
provider_name: string
provider_show_name?: string
tool_name: string
tool_label: string
tool_description: string

View File

@ -40,6 +40,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import {
fetchAllBuiltInTools,
fetchAllCustomTools,
fetchAllMCPTools,
fetchAllWorkflowTools,
} from '@/service/tools'
import { CollectionType } from '@/app/components/tools/types'
@ -450,6 +451,13 @@ export const useFetchToolsData = () => {
workflowTools: workflowTools || [],
})
}
if(type === 'mcp') {
const mcpTools = await fetchAllMCPTools()
workflowStore.setState({
mcpTools: mcpTools || [],
})
}
}, [workflowStore])
return {
@ -496,6 +504,8 @@ export const useToolIcon = (data: Node['data']) => {
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const mcpTools = useStore(s => s.mcpTools)
const toolIcon = useMemo(() => {
if (data.type === BlockEnum.Tool) {
let targetTools = buildInTools
@ -503,11 +513,13 @@ export const useToolIcon = (data: Node['data']) => {
targetTools = buildInTools
else if (data.provider_type === CollectionType.custom)
targetTools = customTools
else if (data.provider_type === CollectionType.mcp)
targetTools = mcpTools
else
targetTools = workflowTools
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon
}
}, [data, buildInTools, customTools, workflowTools])
}, [data.type, data.provider_type, data.provider_id, buildInTools, customTools, mcpTools, workflowTools])
return toolIcon
}

View File

@ -205,6 +205,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
handleFetchAllTools('builtin')
handleFetchAllTools('custom')
handleFetchAllTools('workflow')
handleFetchAllTools('mcp')
}, [handleFetchAllTools])
const {

View File

@ -6,6 +6,7 @@ import type { EditData } from './edit-card'
import { ArrayType, type Field, Type } from '../../../types'
import Toast from '@/app/components/base/toast'
import { findPropertyWithPath } from '../../../utils'
import _ from 'lodash'
type ChangeEventParams = {
path: string[],
@ -19,7 +20,8 @@ type AddEventParams = {
}
export const useSchemaNodeOperations = (props: VisualEditorProps) => {
const { schema: jsonSchema, onChange } = props
const { schema: jsonSchema, onChange: doOnChange } = props
const onChange = doOnChange || _.noop
const backupSchema = useVisualEditorStore(state => state.backupSchema)
const setBackupSchema = useVisualEditorStore(state => state.setBackupSchema)
const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField)

View File

@ -5,11 +5,12 @@ import { useSchemaNodeOperations } from './hooks'
export type VisualEditorProps = {
schema: SchemaRoot
onChange: (schema: SchemaRoot) => void
readOnly?: boolean
onChange?: (schema: SchemaRoot) => void
}
const VisualEditor: FC<VisualEditorProps> = (props) => {
const { schema } = props
const { schema, readOnly } = props
useSchemaNodeOperations(props)
return (
@ -20,6 +21,7 @@ const VisualEditor: FC<VisualEditorProps> = (props) => {
required={false}
path={[]}
depth={0}
readOnly={readOnly}
/>
</div>
)

View File

@ -19,6 +19,7 @@ type SchemaNodeProps = {
path: string[]
parentPath?: string[]
depth: number
readOnly?: boolean
}
// Support 10 levels of indentation
@ -57,6 +58,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
path,
parentPath,
depth,
readOnly,
}) => {
const [isExpanded, setIsExpanded] = useState(true)
const hoveringProperty = useVisualEditorStore(state => state.hoveringProperty)
@ -77,11 +79,13 @@ const SchemaNode: FC<SchemaNodeProps> = ({
}
const handleMouseEnter = () => {
if(!readOnly) return
if (advancedEditing || isAddingNewField) return
setHoveringPropertyDebounced(path.join('.'))
}
const handleMouseLeave = () => {
if(!readOnly) return
if (advancedEditing || isAddingNewField) return
setHoveringPropertyDebounced(null)
}
@ -183,7 +187,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
)}
{
depth === 0 && !isAddingNewField && (
!readOnly && depth === 0 && !isAddingNewField && (
<AddField />
)
}

View File

@ -37,6 +37,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const mcpTools = useStore(s => s.mcpTools)
const currentTools = (() => {
switch (provider_type) {
@ -46,6 +47,8 @@ const useConfig = (id: string, payload: ToolNodeType) => {
return customTools
case CollectionType.workflow:
return workflowTools
case CollectionType.mcp:
return mcpTools
default:
return []
}

View File

@ -10,6 +10,8 @@ export type ToolSliceShape = {
setCustomTools: (tools: ToolWithProvider[]) => void
workflowTools: ToolWithProvider[]
setWorkflowTools: (tools: ToolWithProvider[]) => void
mcpTools: ToolWithProvider[]
setMcpTools: (tools: ToolWithProvider[]) => void
toolPublished: boolean
setToolPublished: (toolPublished: boolean) => void
}
@ -21,6 +23,8 @@ export const createToolSlice: StateCreator<ToolSliceShape> = set => ({
setCustomTools: customTools => set(() => ({ customTools })),
workflowTools: [],
setWorkflowTools: workflowTools => set(() => ({ workflowTools })),
mcpTools: [],
setMcpTools: mcpTools => set(() => ({ mcpTools })),
toolPublished: false,
setToolPublished: toolPublished => set(() => ({ toolPublished })),
})

View File

@ -55,7 +55,7 @@ const translation = {
unsupportedContent: 'Die installierte Plug-in-Version bietet diese Aktion nicht.',
unsupportedTitle: 'Nicht unterstützte Aktion',
descriptionPlaceholder: 'Kurze Beschreibung des Zwecks des Werkzeugs, z. B. um die Temperatur für einen bestimmten Ort zu ermitteln.',
auto: 'Automatisch',
auto: 'Auto',
params: 'KONFIGURATION DER ARGUMENTATION',
unsupportedContent2: 'Klicken Sie hier, um die Version zu wechseln.',
placeholder: 'Wählen Sie ein Werkzeug aus...',

View File

@ -85,8 +85,8 @@ const translation = {
settings: 'USER SETTINGS',
params: 'REASONING CONFIG',
paramsTip1: 'Controls LLM inference parameters.',
paramsTip2: 'When \'Automatic\' is off, the default value is used.',
auto: 'Automatic',
paramsTip2: 'When \'Auto\' is off, the default value is used.',
auto: 'Auto',
empty: 'Click the \'+\' button to add tools. You can add multiple tools.',
uninstalledTitle: 'Tool not installed',
uninstalledContent: 'This plugin is installed from the local/GitHub repository. Please use after installation.',

View File

@ -152,6 +152,62 @@ const translation = {
toolNameUsageTip: 'Tool call name for agent reasoning and prompting',
copyToolName: 'Copy Name',
noTools: 'No tools found',
mcp: {
create: {
cardTitle: 'Add MCP Server (HTTP)',
cardLink: 'Learn more about MCP server integration',
},
noConfigured: 'Unconfigured Server',
updateTime: 'Updated',
toolsCount: '{{count}} tools',
noTools: 'No tools available',
modal: {
title: 'Add MCP Server (HTTP)',
name: 'Name & Icon',
namePlaceholder: 'Name your MCP server',
serverUrl: 'Server URL',
serverUrlPlaceholder: 'URL to server endpiont',
cancel: 'Cancel',
save: 'Save',
confirm: 'Add & Authorize',
},
delete: 'Remove MCP Server',
deleteConfirmTitle: 'Would you like to remove {{mcp}}?',
operation: {
edit: 'Edit',
remove: 'Remove',
},
authorize: 'Authorize',
authorizing: 'Authorizing...',
authorizingRequired: 'Authorization is required',
authorizeTip: 'After authorization, tools will be displayed here.',
update: 'Update',
updating: 'Updating',
gettingTools: 'Getting Tools...',
updateTools: 'Updating Tools...',
toolsEmpty: 'Tools not loaded',
getTools: 'Get tools',
toolsNum: '{{count}} tools included',
onlyTool: '1 tool included',
server: {
title: 'MCP Server',
url: 'Server URL',
reGen: 'Do you want to regenerator server URL?',
addDescription: 'Add description',
edit: 'Edit description',
modal: {
addTitle: 'Add description to enable MCP server',
editTitle: 'Edit description',
description: 'Description',
descriptionPlaceholder: 'Explain what this tool does and how it should be used by the LLM',
parameters: 'Parameters',
parametersTip: 'Add descriptions for each parameter to help the LLM understand their purpose and constraints.',
parametersPlaceholder: 'Parameter purpose and constraints',
confirm: 'Enable MCP Server',
},
publishTip: 'App not published. Please publish the app first.',
},
},
}
export default translation

View File

@ -229,6 +229,8 @@ const translation = {
'utilities': 'Utilities',
'noResult': 'No match found',
'agent': 'Agent Strategy',
'allAdded': 'All added',
'addAll': 'Add all',
},
blocks: {
'start': 'Start',
@ -876,6 +878,8 @@ const translation = {
install: 'Install',
cancel: 'Cancel',
},
clickToViewParameterSchema: 'Click to view parameter schema',
parameterSchema: 'Parameter Schema',
},
},
tracing: {

View File

@ -51,11 +51,11 @@ const translation = {
unsupportedContent2: 'Haga clic para cambiar de versión.',
descriptionPlaceholder: 'Breve descripción del propósito de la herramienta, por ejemplo, obtener la temperatura para una ubicación específica.',
empty: 'Haga clic en el botón \'+\' para agregar herramientas. Puede agregar varias herramientas.',
paramsTip2: 'Cuando \'Automático\' está desactivado, se utiliza el valor predeterminado.',
paramsTip2: 'Cuando \'Auto\' está desactivado, se utiliza el valor predeterminado.',
uninstalledTitle: 'Herramienta no instalada',
descriptionLabel: 'Descripción de la herramienta',
unsupportedContent: 'La versión del plugin instalado no proporciona esta acción.',
auto: 'Automático',
auto: 'Auto',
title: 'Agregar herramienta',
placeholder: 'Seleccione una herramienta...',
uninstalledContent: 'Este plugin se instala desde el repositorio local/GitHub. Úselo después de la instalación.',

View File

@ -53,14 +53,14 @@ const translation = {
placeholder: 'Sélectionnez un outil...',
params: 'CONFIGURATION DE RAISONNEMENT',
unsupportedContent: 'La version du plugin installée ne fournit pas cette action.',
auto: 'Automatique',
auto: 'Auto',
descriptionPlaceholder: 'Brève description de lobjectif de loutil, par exemple, obtenir la température dun endroit spécifique.',
unsupportedContent2: 'Cliquez pour changer de version.',
uninstalledTitle: 'Outil non installé',
empty: 'Cliquez sur le bouton « + » pour ajouter des outils. Vous pouvez ajouter plusieurs outils.',
toolLabel: 'Outil',
settings: 'PARAMÈTRES UTILISATEUR',
paramsTip2: 'Lorsque « Automatique » est désactivé, la valeur par défaut est utilisée.',
paramsTip2: 'Lorsque « Auto » est désactivé, la valeur par défaut est utilisée.',
paramsTip1: 'Contrôle les paramètres dinférence LLM.',
},
modelNum: '{{num}} MODÈLES INCLUS',

View File

@ -60,8 +60,8 @@ const translation = {
placeholder: 'Seleziona uno strumento...',
unsupportedContent: 'La versione del plug-in installata non fornisce questa azione.',
descriptionLabel: 'Descrizione dell\'utensile',
auto: 'Automatico',
paramsTip2: 'Quando \'Automatico\' è disattivato, viene utilizzato il valore predefinito.',
auto: 'Auto',
paramsTip2: 'Quando \'Auto\' è disattivato, viene utilizzato il valore predefinito.',
},
modelNum: '{{num}} MODELLI INCLUSI',
endpointModalTitle: 'Endpoint di configurazione',

View File

@ -51,7 +51,7 @@ const translation = {
paramsTip1: 'Steruje parametrami wnioskowania LLM.',
unsupportedContent: 'Zainstalowana wersja wtyczki nie zapewnia tej akcji.',
params: 'KONFIGURACJA ROZUMOWANIA',
auto: 'Automatyczne',
auto: 'Auto',
empty: 'Kliknij przycisk "+", aby dodać narzędzia. Możesz dodać wiele narzędzi.',
descriptionLabel: 'Opis narzędzia',
title: 'Dodaj narzędzie',
@ -60,7 +60,7 @@ const translation = {
uninstalledContent: 'Ta wtyczka jest instalowana z repozytorium lokalnego/GitHub. Proszę użyć po instalacji.',
unsupportedTitle: 'Nieobsługiwana akcja',
uninstalledTitle: 'Narzędzie nie jest zainstalowane',
paramsTip2: 'Gdy opcja "Automatycznie" jest wyłączona, używana jest wartość domyślna.',
paramsTip2: 'Gdy opcja "Auto" jest wyłączona, używana jest wartość domyślna.',
toolLabel: 'Narzędzie',
},
strategyNum: '{{liczba}} {{strategia}} ZAWARTE',

View File

@ -47,14 +47,14 @@ const translation = {
toolSelector: {
uninstalledLink: 'Gerenciar em plug-ins',
unsupportedContent2: 'Clique para mudar de versão.',
auto: 'Automático',
auto: 'Auto',
title: 'Adicionar ferramenta',
params: 'CONFIGURAÇÃO DE RACIOCÍNIO',
toolLabel: 'Ferramenta',
paramsTip1: 'Controla os parâmetros de inferência do LLM.',
descriptionLabel: 'Descrição da ferramenta',
uninstalledContent: 'Este plug-in é instalado a partir do repositório local/GitHub. Por favor, use após a instalação.',
paramsTip2: 'Quando \'Automático\' está desativado, o valor padrão é usado.',
paramsTip2: 'Quando \'Auto\' está desativado, o valor padrão é usado.',
placeholder: 'Selecione uma ferramenta...',
empty: 'Clique no botão \'+\' para adicionar ferramentas. Você pode adicionar várias ferramentas.',
settings: 'CONFIGURAÇÕES DO USUÁRIO',

View File

@ -46,7 +46,7 @@ const translation = {
},
toolSelector: {
unsupportedContent: 'Versiunea de plugin instalată nu oferă această acțiune.',
auto: 'Automat',
auto: 'Auto',
empty: 'Faceți clic pe butonul "+" pentru a adăuga instrumente. Puteți adăuga mai multe instrumente.',
uninstalledContent: 'Acest plugin este instalat din depozitul local/GitHub. Vă rugăm să utilizați după instalare.',
descriptionLabel: 'Descrierea instrumentului',
@ -54,7 +54,7 @@ const translation = {
uninstalledLink: 'Gestionați în pluginuri',
paramsTip1: 'Controlează parametrii de inferență LLM.',
params: 'CONFIGURAREA RAȚIONAMENTULUI',
paramsTip2: 'Când "Automat" este dezactivat, se folosește valoarea implicită.',
paramsTip2: 'Când "Auto" este dezactivat, se folosește valoarea implicită.',
settings: 'SETĂRI UTILIZATOR',
unsupportedTitle: 'Acțiune neacceptată',
placeholder: 'Selectați un instrument...',

View File

@ -152,6 +152,62 @@ const translation = {
toolNameUsageTip: '工具调用名称,用于 Agent 推理和提示词',
copyToolName: '复制名称',
noTools: '没有工具',
mcp: {
create: {
cardTitle: '添加 MCP 服务 (HTTP)',
cardLink: '了解更多关于 MCP 服务集成的信息',
},
noConfigured: '未配置',
updateTime: '更新于',
toolsCount: '{{count}} 个工具',
noTools: '没有可用的工具',
modal: {
title: '添加 MCP 服务 (HTTP)',
name: '名称和图标',
namePlaceholder: '命名你的 MCP 服务',
serverUrl: '服务端点 URL',
serverUrlPlaceholder: '服务端点的 URL',
cancel: '取消',
save: '保存',
confirm: '添加并授权',
},
delete: '删除 MCP 服务',
deleteConfirmTitle: '你想要删除 {{mcp}} 吗?',
operation: {
edit: '修改',
remove: '删除',
},
authorize: '授权',
authorizing: '授权中...',
authorizingRequired: '需要授权',
authorizeTip: '授权后,工具将显示在这里。',
update: '更新',
updating: '更新中',
gettingTools: '获取工具中...',
updateTools: '更新工具中...',
toolsEmpty: '工具未加载',
getTools: '获取工具',
toolsNum: '包含 {{count}} 个工具',
onlyTool: '包含 1 个工具',
server: {
title: 'MCP 服务',
url: '服务端点 URL',
reGen: '你想要重新生成服务端点 URL 吗?',
addDescription: '添加描述',
edit: '编辑描述',
modal: {
addTitle: '添加描述以启用 MCP 服务',
editTitle: '编辑 MCP 服务描述',
description: '描述',
descriptionPlaceholder: '解释此工具的功能以及 LLM 应如何使用它',
parameters: '参数',
parametersTip: '为每个参数添加描述,以帮助 LLM 理解其目的和约束条件。',
parametersPlaceholder: '参数的用途和约束条件',
confirm: '启用 MCP 服务',
},
publishTip: '应用未发布。请先发布应用。',
},
},
}
export default translation

View File

@ -230,6 +230,8 @@ const translation = {
'utilities': '工具',
'noResult': '未找到匹配项',
'agent': 'Agent 策略',
'allAdded': '已添加全部',
'addAll': '添加全部',
},
blocks: {
'start': '开始',
@ -877,6 +879,8 @@ const translation = {
install: '安装',
cancel: '取消',
},
clickToViewParameterSchema: '点击查看参数 schema',
parameterSchema: '参数 Schema',
},
},
tracing: {

View File

@ -124,6 +124,10 @@ export const fetchAllWorkflowTools = () => {
return get<ToolWithProvider[]>('/workspaces/current/tools/workflow')
}
export const fetchAllMCPTools = () => {
return get<ToolWithProvider[]>('/workspaces/current/tools/mcp')
}
export const fetchLabelList = () => {
return get<Label[]>('/workspaces/current/tool-labels')
}

View File

@ -1,9 +1,11 @@
import { get, post } from './base'
import { del, get, post, put } from './base'
import type {
Collection,
MCPServerDetail,
Tool,
} from '@/app/components/tools/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import type { AppIconType } from '@/types/app'
import { useInvalid } from './use-base'
import {
useMutation,
@ -61,6 +63,191 @@ export const useInvalidateAllWorkflowTools = () => {
return useInvalid(useAllWorkflowToolsKey)
}
const useAllMCPToolsKey = [NAME_SPACE, 'MCPTools']
export const useAllMCPTools = () => {
return useQuery<ToolWithProvider[]>({
queryKey: useAllMCPToolsKey,
queryFn: () => get<ToolWithProvider[]>('/workspaces/current/tools/mcp'),
})
}
export const useInvalidateAllMCPTools = () => {
return useInvalid(useAllMCPToolsKey)
}
export const useCreateMCP = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'create-mcp'],
mutationFn: (payload: {
name: string
server_url: string
icon_type: AppIconType
icon: string
icon_background?: string | null
}) => {
return post<ToolWithProvider>('workspaces/current/tool-provider/mcp', {
body: {
...payload,
},
})
},
})
}
export const useUpdateMCP = ({
onSuccess,
}: {
onSuccess?: () => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'update-mcp'],
mutationFn: (payload: {
name: string
server_url: string
icon_type: AppIconType
icon: string
icon_background?: string | null
provider_id: string
}) => {
return put('workspaces/current/tool-provider/mcp', {
body: {
...payload,
},
})
},
onSuccess,
})
}
export const useDeleteMCP = ({
onSuccess,
}: {
onSuccess?: () => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'delete-mcp'],
mutationFn: (id: string) => {
return del('/workspaces/current/tool-provider/mcp', {
body: {
provider_id: id,
},
})
},
onSuccess,
})
}
export const useAuthorizeMCP = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'authorize-mcp'],
mutationFn: (payload: { provider_id: string; server_url: string }) => {
return post<{ result?: string; authorization_url?: string }>('/workspaces/current/tool-provider/mcp/auth', {
body: payload,
})
},
})
}
export const useUpdateMCPAuthorizationToken = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'],
mutationFn: (payload: { provider_id: string; server_url: string; authorization_code: string }) => {
return get<MCPServerDetail>('/workspaces/current/tool-provider/mcp/token', {
params: {
...payload,
},
})
},
})
}
export const useMCPTools = (providerID: string) => {
return useQuery({
enabled: !!providerID,
queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID],
queryFn: () => get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/tools/${providerID}`),
})
}
export const useInvalidateMCPTools = () => {
const queryClient = useQueryClient()
return (providerID: string) => {
queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID],
})
}
}
export const useUpdateMCPTools = () => {
return useMutation({
mutationFn: (providerID: string) => get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/update/${providerID}`),
})
}
export const useMCPServerDetail = (appID: string) => {
return useQuery<MCPServerDetail>({
queryKey: [NAME_SPACE, 'MCPServerDetail', appID],
queryFn: () => get<MCPServerDetail>(`/apps/${appID}/server`),
})
}
export const useInvalidateMCPServerDetail = () => {
const queryClient = useQueryClient()
return (appID: string) => {
queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'MCPServerDetail', appID],
})
}
}
export const useCreateMCPServer = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'create-mcp-server'],
mutationFn: (payload: {
appID: string
description: string
parameters?: Record<string, string>
}) => {
const { appID, ...rest } = payload
return post(`apps/${appID}/server`, {
body: {
...rest,
},
})
},
})
}
export const useUpdateMCPServer = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'update-mcp-server'],
mutationFn: (payload: {
appID: string
id: string
description?: string
status?: string
parameters?: Record<string, string>
}) => {
const { appID, ...rest } = payload
return put(`apps/${appID}/server`, {
body: {
...rest,
},
})
},
})
}
export const useRefreshMCPServerCode = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'],
mutationFn: (appID: string) => {
return get<MCPServerDetail>(`apps/${appID}/server/refresh`)
},
})
}
export const useBuiltinProviderInfo = (providerName: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'builtin-provider-info', providerName],

View File

@ -1,5 +1,5 @@
import { del, get, patch, post } from './base'
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import type {
FetchWorkflowDraftPageParams,
FetchWorkflowDraftPageResponse,
@ -21,6 +21,16 @@ export const useAppWorkflow = (appID: string) => {
})
}
export const useInvalidateAppWorkflow = () => {
const queryClient = useQueryClient()
return (appID: string) => {
queryClient.invalidateQueries(
{
queryKey: [NAME_SPACE, 'publish', appID],
})
}
}
export const useWorkflowConfig = (appId: string, onSuccess: (v: WorkflowConfigResponse) => void) => {
return useQuery({
queryKey: [NAME_SPACE, 'config', appId],

View File

@ -71,6 +71,7 @@ const config = {
boxShadow: {
'xs': '0px 1px 2px 0px rgba(16, 24, 40, 0.05)',
'sm': '0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10)',
'sm-no-bottom': '0px -1px 2px 0px rgba(16, 24, 40, 0.06), 0px -1px 3px 0px rgba(16, 24, 40, 0.10)',
'md': '0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(16, 24, 40, 0.10)',
'lg': '0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08)',
'xl': '0px 8px 8px -4px rgba(16, 24, 40, 0.03), 0px 20px 24px -4px rgba(16, 24, 40, 0.08)',