mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:28:10 +08:00
Merge branch 'feat/mcp-frontend' into deploy/dev
This commit is contained in:
@ -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,
|
||||
@ -31,6 +32,8 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
|
||||
const showMCPCard = isInPanel && (appDetail?.mode === 'advanced-chat' || appDetail?.mode === 'workflow')
|
||||
|
||||
const updateAppDetail = async () => {
|
||||
try {
|
||||
const res = await fetchAppDetail({ url: '/apps', id: appId })
|
||||
@ -117,6 +120,11 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
isInPanel={isInPanel}
|
||||
onChangeStatus={onChangeApiStatus}
|
||||
/>
|
||||
{showMCPCard && (
|
||||
<MCPServiceCard
|
||||
appInfo={appDetail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>}
|
||||
|
||||
@ -30,15 +30,31 @@ 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'
|
||||
import { useMittContextSelector } from '@/context/mitt-context'
|
||||
|
||||
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)
|
||||
const currentCollection = useMemo(() => {
|
||||
@ -96,23 +112,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
|
||||
@ -143,7 +174,9 @@ const AgentTools: FC = () => {
|
||||
disabled={false}
|
||||
supportAddCustomTool
|
||||
onSelect={handleSelectTool}
|
||||
selectedTools={tools as any}
|
||||
onSelectMultiple={handleSelectMultipleTool}
|
||||
selectedTools={tools as unknown as ToolValue[]}
|
||||
canChooseMCPTool
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@ -161,7 +194,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>
|
||||
@ -172,7 +205,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
|
||||
@ -285,8 +318,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)}
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -181,6 +181,7 @@ function AppCard({
|
||||
icon={appInfo.icon}
|
||||
icon_background={appInfo.icon_background}
|
||||
name={basicName}
|
||||
hideType
|
||||
type={
|
||||
isApp
|
||||
? t('appOverview.overview.appInfo.explanation')
|
||||
|
||||
@ -18,6 +18,7 @@ export type AppIconProps = {
|
||||
imageUrl?: string | null
|
||||
className?: string
|
||||
innerIcon?: React.ReactNode
|
||||
coverElement?: React.ReactNode
|
||||
onClick?: () => void
|
||||
}
|
||||
const appIconVariants = cva(
|
||||
@ -51,6 +52,7 @@ const AppIcon: FC<AppIconProps> = ({
|
||||
imageUrl,
|
||||
className,
|
||||
innerIcon,
|
||||
coverElement,
|
||||
onClick,
|
||||
}) => {
|
||||
const isValidImageIcon = iconType === 'image' && imageUrl
|
||||
@ -65,6 +67,7 @@ const AppIcon: FC<AppIconProps> = ({
|
||||
? <img src={imageUrl} className="h-full w-full" alt="app icon" />
|
||||
: (innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />))
|
||||
}
|
||||
{coverElement}
|
||||
</span>
|
||||
}
|
||||
|
||||
|
||||
@ -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 |
@ -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 |
35
web/app/components/base/icons/src/vender/other/Mcp.json
Normal file
35
web/app/components/base/icons/src/vender/other/Mcp.json
Normal 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"
|
||||
}
|
||||
20
web/app/components/base/icons/src/vender/other/Mcp.tsx
Normal file
20
web/app/components/base/icons/src/vender/other/Mcp.tsx
Normal 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
|
||||
@ -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'
|
||||
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
@ -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'
|
||||
|
||||
@ -64,8 +64,9 @@ import cn from '@/utils/classnames'
|
||||
export type PromptEditorProps = {
|
||||
instanceId?: string
|
||||
compact?: boolean
|
||||
wrapperClassName?: string
|
||||
className?: string
|
||||
placeholder?: string
|
||||
placeholder?: string | JSX.Element
|
||||
placeholderClassName?: string
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
@ -85,6 +86,7 @@ export type PromptEditorProps = {
|
||||
const PromptEditor: FC<PromptEditorProps> = ({
|
||||
instanceId,
|
||||
compact,
|
||||
wrapperClassName,
|
||||
className,
|
||||
placeholder,
|
||||
placeholderClassName,
|
||||
@ -147,10 +149,25 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
||||
<div className='relative min-h-5'>
|
||||
<div className={cn('relative', wrapperClassName)}>
|
||||
<RichTextPlugin
|
||||
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'text-[13px] leading-5' : 'text-sm leading-6'} text-text-secondary`} style={style || {}} />}
|
||||
placeholder={<Placeholder value={placeholder} className={cn('truncate', placeholderClassName)} compact={compact} />}
|
||||
contentEditable={
|
||||
<ContentEditable
|
||||
className={cn(
|
||||
'text-text-secondary outline-none',
|
||||
compact ? 'text-[13px] leading-5' : 'text-sm leading-6',
|
||||
className,
|
||||
)}
|
||||
style={style || {}}
|
||||
/>
|
||||
}
|
||||
placeholder={
|
||||
<Placeholder
|
||||
value={placeholder}
|
||||
className={cn('truncate', placeholderClassName)}
|
||||
compact={compact}
|
||||
/>
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<ComponentPickerBlock
|
||||
|
||||
@ -16,7 +16,6 @@ export class CustomTextNode extends TextNode {
|
||||
|
||||
createDOM(config: EditorConfig) {
|
||||
const dom = super.createDOM(config)
|
||||
dom.classList.add('align-middle')
|
||||
return dom
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ const Placeholder = ({
|
||||
className,
|
||||
}: {
|
||||
compact?: boolean
|
||||
value?: string
|
||||
value?: string | JSX.Element
|
||||
className?: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
|
||||
|
||||
export type FormValue = Record<string, any>
|
||||
|
||||
export type TypeWithI18N<T = string> = {
|
||||
@ -19,6 +21,8 @@ export enum FormTypeEnum {
|
||||
toolSelector = 'tool-selector',
|
||||
multiToolSelector = 'array[tools]',
|
||||
appSelector = 'app-selector',
|
||||
object = 'object',
|
||||
array = 'array',
|
||||
dynamicSelect = 'dynamic-select',
|
||||
}
|
||||
|
||||
@ -109,6 +113,7 @@ export type FormShowOnObject = {
|
||||
}
|
||||
|
||||
export type CredentialFormSchemaBase = {
|
||||
name: string
|
||||
variable: string
|
||||
label: TypeWithI18N
|
||||
type: FormTypeEnum
|
||||
@ -118,6 +123,7 @@ export type CredentialFormSchemaBase = {
|
||||
show_on: FormShowOnObject[]
|
||||
url?: string
|
||||
scope?: string
|
||||
input_schema?: SchemaRoot
|
||||
}
|
||||
|
||||
export type CredentialFormSchemaTextInput = CredentialFormSchemaBase & {
|
||||
|
||||
@ -54,6 +54,7 @@ type FormProps<
|
||||
nodeId?: string
|
||||
nodeOutputVars?: NodeOutPutVar[],
|
||||
availableNodes?: Node[],
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
function Form<
|
||||
@ -79,6 +80,7 @@ function Form<
|
||||
nodeId,
|
||||
nodeOutputVars,
|
||||
availableNodes,
|
||||
canChooseMCPTool,
|
||||
}: FormProps<CustomFormSchema>) {
|
||||
const language = useLanguage()
|
||||
const [changeKey, setChangeKey] = useState('')
|
||||
@ -377,6 +379,7 @@ function Form<
|
||||
value={value[variable] || []}
|
||||
onChange={item => handleFormChange(variable, item as any)}
|
||||
supportCollapse
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
|
||||
@ -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 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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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]'>
|
||||
|
||||
@ -13,6 +13,7 @@ import type { Node } from 'reactflow'
|
||||
import type { NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { useAllMCPTools } from '@/service/use-tools'
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean
|
||||
@ -26,6 +27,7 @@ type Props = {
|
||||
nodeOutputVars: NodeOutPutVar[],
|
||||
availableNodes: Node[],
|
||||
nodeId?: string
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const MultipleToolSelector = ({
|
||||
@ -40,9 +42,16 @@ const MultipleToolSelector = ({
|
||||
nodeOutputVars,
|
||||
availableNodes,
|
||||
nodeId,
|
||||
canChooseMCPTool,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const enabledCount = value.filter(item => item.enabled).length
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const enabledCount = value.filter((item) => {
|
||||
const isMCPTool = mcpTools?.find(tool => tool.id === item.provider_name)
|
||||
if(isMCPTool)
|
||||
return item.enabled && canChooseMCPTool
|
||||
return item.enabled
|
||||
}).length
|
||||
// collapse control
|
||||
const [collapse, setCollapse] = React.useState(false)
|
||||
const handleCollapse = () => {
|
||||
@ -66,6 +75,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]
|
||||
@ -140,8 +162,10 @@ const MultipleToolSelector = ({
|
||||
value={item}
|
||||
selectedTools={value}
|
||||
onSelect={item => handleConfigure(item, index)}
|
||||
onSelectMultiple={handleAddMultiple}
|
||||
onDelete={() => handleDelete(index)}
|
||||
supportEnableSwitch
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
isEdit
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
RiArrowRightUpLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
@ -15,6 +14,7 @@ import {
|
||||
import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-trigger'
|
||||
import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item'
|
||||
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
|
||||
import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form'
|
||||
@ -23,13 +23,13 @@ import Textarea from '@/app/components/base/textarea'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import TabSlider from '@/app/components/base/tab-slider-plain'
|
||||
import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
|
||||
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
||||
import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
useInvalidateAllBuiltInTools,
|
||||
useUpdateProviderCredentials,
|
||||
@ -54,15 +54,9 @@ type Props = {
|
||||
scope?: string
|
||||
value?: ToolValue
|
||||
selectedTools?: ToolValue[]
|
||||
onSelect: (tool: ToolValue) => void
|
||||
onSelectMultiple: (tool: ToolValue[]) => void
|
||||
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
|
||||
onDelete?: () => void
|
||||
supportEnableSwitch?: boolean
|
||||
supportAddCustomTool?: boolean
|
||||
@ -74,6 +68,7 @@ type Props = {
|
||||
nodeOutputVars: NodeOutPutVar[],
|
||||
availableNodes: Node[],
|
||||
nodeId?: string,
|
||||
canChooseMCPTool?: boolean,
|
||||
}
|
||||
const ToolSelector: FC<Props> = ({
|
||||
value,
|
||||
@ -83,6 +78,7 @@ const ToolSelector: FC<Props> = ({
|
||||
placement = 'left',
|
||||
offset = 4,
|
||||
onSelect,
|
||||
onSelectMultiple,
|
||||
onDelete,
|
||||
scope,
|
||||
supportEnableSwitch,
|
||||
@ -94,6 +90,7 @@ const ToolSelector: FC<Props> = ({
|
||||
nodeOutputVars,
|
||||
availableNodes,
|
||||
nodeId = '',
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isShow, onShowChange] = useState(false)
|
||||
@ -105,6 +102,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 +110,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 +135,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({
|
||||
@ -169,7 +175,6 @@ const ToolSelector: FC<Props> = ({
|
||||
|
||||
const handleSettingsFormChange = (v: Record<string, any>) => {
|
||||
const newValue = getStructureValue(v)
|
||||
|
||||
const toolValue = {
|
||||
...value,
|
||||
settings: newValue,
|
||||
@ -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}
|
||||
@ -272,6 +279,7 @@ const ToolSelector: FC<Props> = ({
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
)}
|
||||
</PortalToFollowElemTrigger>
|
||||
@ -285,7 +293,7 @@ const ToolSelector: FC<Props> = ({
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div>
|
||||
<ToolPicker
|
||||
panelClassName='w-[328px]'
|
||||
panelClassName='w-[400px]'
|
||||
placement='bottom'
|
||||
offset={offset}
|
||||
trigger={
|
||||
@ -300,8 +308,10 @@ const ToolSelector: FC<Props> = ({
|
||||
disabled={false}
|
||||
supportAddCustomTool
|
||||
onSelect={handleSelectTool}
|
||||
onSelectMultiple={handleSelectMultipleTool}
|
||||
scope={scope}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
@ -390,24 +400,13 @@ const ToolSelector: FC<Props> = ({
|
||||
{/* user settings form */}
|
||||
{(currType === 'settings' || userSettingsOnly) && (
|
||||
<div className='px-4 py-2'>
|
||||
<Form
|
||||
<ToolForm
|
||||
inPanel
|
||||
readOnly={false}
|
||||
nodeId={nodeId}
|
||||
schema={settingsFormSchemas as any}
|
||||
value={getPlainValue(value?.settings || {})}
|
||||
onChange={handleSettingsFormChange}
|
||||
formSchemas={settingsFormSchemas as any}
|
||||
isEditMode={true}
|
||||
showOnVariableMap={{}}
|
||||
validating={false}
|
||||
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
|
||||
fieldMoreInfo={item => item.url
|
||||
? (<a
|
||||
href={item.url}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='inline-flex items-center text-xs text-text-accent'
|
||||
>
|
||||
{t('tools.howToGet')}
|
||||
<RiArrowRightUpLine className='ml-1 h-3 w-3' />
|
||||
</a>)
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -3,25 +3,34 @@ 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'
|
||||
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
|
||||
import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input'
|
||||
import Input from '@/app/components/base/input'
|
||||
import FormInputTypeSwitch from '@/app/components/workflow/nodes/_base/components/form-input-type-switch'
|
||||
import FormInputBoolean from '@/app/components/workflow/nodes/_base/components/form-input-boolean'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { Node } from 'reactflow'
|
||||
import type {
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
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'
|
||||
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
|
||||
|
||||
type Props = {
|
||||
value: Record<string, any>
|
||||
@ -42,73 +51,46 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const handleAutomatic = (key: string, val: any) => {
|
||||
const getVarKindType = (type: FormTypeEnum) => {
|
||||
if (type === FormTypeEnum.file || type === FormTypeEnum.files)
|
||||
return VarKindType.variable
|
||||
if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber || type === FormTypeEnum.array || type === FormTypeEnum.object)
|
||||
return VarKindType.constant
|
||||
if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput)
|
||||
return VarKindType.mixed
|
||||
}
|
||||
|
||||
const handleAutomatic = (key: string, val: any, type: FormTypeEnum) => {
|
||||
onChange({
|
||||
...value,
|
||||
[key]: {
|
||||
value: val ? null : value[key]?.value,
|
||||
value: val ? null : { type: getVarKindType(type), value: null },
|
||||
auto: val ? 1 : 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const [inputsIsFocus, setInputsIsFocus] = useState<Record<string, boolean>>({})
|
||||
const handleInputFocus = useCallback((variable: string) => {
|
||||
return (value: boolean) => {
|
||||
setInputsIsFocus((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[variable]: value,
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
const handleNotMixedTypeChange = useCallback((variable: string) => {
|
||||
return (varValue: ValueSelector | string, varKindType: VarKindType) => {
|
||||
const newValue = produce(value, (draft: ToolVarInputs) => {
|
||||
const target = draft[variable].value
|
||||
if (target) {
|
||||
target.type = varKindType
|
||||
target.value = varValue
|
||||
}
|
||||
else {
|
||||
draft[variable].value = {
|
||||
type: varKindType,
|
||||
value: varValue,
|
||||
}
|
||||
}
|
||||
})
|
||||
onChange(newValue)
|
||||
}
|
||||
}, [value, onChange])
|
||||
const handleMixedTypeChange = useCallback((variable: string) => {
|
||||
return (itemValue: string) => {
|
||||
const newValue = produce(value, (draft: ToolVarInputs) => {
|
||||
const target = draft[variable].value
|
||||
if (target) {
|
||||
target.value = itemValue
|
||||
}
|
||||
else {
|
||||
draft[variable].value = {
|
||||
type: VarKindType.mixed,
|
||||
value: itemValue,
|
||||
}
|
||||
}
|
||||
})
|
||||
onChange(newValue)
|
||||
}
|
||||
}, [value, onChange])
|
||||
const handleFileChange = useCallback((variable: string) => {
|
||||
return (varValue: ValueSelector | string) => {
|
||||
const newValue = produce(value, (draft: ToolVarInputs) => {
|
||||
const handleTypeChange = useCallback((variable: string, defaultValue: any) => {
|
||||
return (newType: VarKindType) => {
|
||||
const res = produce(value, (draft: ToolVarInputs) => {
|
||||
draft[variable].value = {
|
||||
type: VarKindType.variable,
|
||||
value: varValue,
|
||||
type: newType,
|
||||
value: newType === VarKindType.variable ? '' : defaultValue,
|
||||
}
|
||||
})
|
||||
onChange(newValue)
|
||||
onChange(res)
|
||||
}
|
||||
}, [value, onChange])
|
||||
}, [onChange, value])
|
||||
const handleValueChange = useCallback((variable: string, varType: FormTypeEnum) => {
|
||||
return (newValue: any) => {
|
||||
const res = produce(value, (draft: ToolVarInputs) => {
|
||||
draft[variable].value = {
|
||||
type: getVarKindType(varType),
|
||||
value: newValue,
|
||||
}
|
||||
})
|
||||
onChange(res)
|
||||
}
|
||||
}, [onChange, value])
|
||||
const handleAppChange = useCallback((variable: string) => {
|
||||
return (app: {
|
||||
app_id: string
|
||||
@ -132,9 +114,29 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
onChange(newValue)
|
||||
}
|
||||
}, [onChange, value])
|
||||
const handleVariableSelectorChange = useCallback((variable: string) => {
|
||||
return (newValue: ValueSelector | string) => {
|
||||
const res = produce(value, (draft: ToolVarInputs) => {
|
||||
draft[variable].value = {
|
||||
type: VarKindType.variable,
|
||||
value: newValue,
|
||||
}
|
||||
})
|
||||
onChange(res)
|
||||
}
|
||||
}, [onChange, value])
|
||||
|
||||
const renderField = (schema: any) => {
|
||||
const [isShowSchema, {
|
||||
setTrue: showSchema,
|
||||
setFalse: hideSchema,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [schema, setSchema] = useState<SchemaRoot | null>(null)
|
||||
const [schemaRootName, setSchemaRootName] = useState<string>('')
|
||||
|
||||
const renderField = (schema: any, showSchema: (schema: SchemaRoot, rootName: string) => void) => {
|
||||
const {
|
||||
default: defaultValue,
|
||||
variable,
|
||||
label,
|
||||
required,
|
||||
@ -142,6 +144,9 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
type,
|
||||
scope,
|
||||
url,
|
||||
input_schema,
|
||||
placeholder,
|
||||
options,
|
||||
} = schema
|
||||
const auto = value[variable]?.auto
|
||||
const tooltipContent = (tooltip && (
|
||||
@ -149,89 +154,150 @@ 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 isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
|
||||
const isNumber = type === FormTypeEnum.textNumber
|
||||
const isSelect = type === FormTypeEnum.select
|
||||
const isObject = type === FormTypeEnum.object
|
||||
const isArray = type === FormTypeEnum.array
|
||||
const isShowJSONEditor = isObject || isArray
|
||||
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
|
||||
const isBoolean = type === FormTypeEnum.boolean
|
||||
const isSelect = type === FormTypeEnum.select
|
||||
const isAppSelector = type === FormTypeEnum.appSelector
|
||||
const isModelSelector = type === FormTypeEnum.modelSelector
|
||||
// const isToolSelector = type === FormTypeEnum.toolSelector
|
||||
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector
|
||||
const showTypeSwitch = isNumber || isObject || isArray
|
||||
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
|
||||
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
|
||||
const targetVarType = () => {
|
||||
if (isString)
|
||||
return VarType.string
|
||||
else if (isNumber)
|
||||
return VarType.number
|
||||
else if (type === FormTypeEnum.files)
|
||||
return VarType.arrayFile
|
||||
else if (type === FormTypeEnum.file)
|
||||
return VarType.file
|
||||
else if (isBoolean)
|
||||
return VarType.boolean
|
||||
else if (isObject)
|
||||
return VarType.object
|
||||
else if (isArray)
|
||||
return VarType.arrayObject
|
||||
else
|
||||
return VarType.string
|
||||
}
|
||||
const getFilterVar = () => {
|
||||
if (isNumber)
|
||||
return (varPayload: any) => varPayload.type === VarType.number
|
||||
else if (isString)
|
||||
return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
else if (isFile)
|
||||
return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||
else if (isBoolean)
|
||||
return (varPayload: any) => varPayload.type === VarType.boolean
|
||||
else if (isObject)
|
||||
return (varPayload: any) => varPayload.type === VarType.object
|
||||
else if (isArray)
|
||||
return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={variable} className='space-y-1'>
|
||||
<div key={variable} className='space-y-0.5'>
|
||||
<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'>{targetVarType()}</span>
|
||||
{isShowJSONEditor && (
|
||||
<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(input_schema as SchemaRoot, label[language] || label.en_US)}
|
||||
>
|
||||
<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)}>
|
||||
<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, type)}>
|
||||
<span className='system-xs-medium text-text-secondary'>{t('plugin.detailPanel.toolSelector.auto')}</span>
|
||||
<Switch
|
||||
size='xs'
|
||||
defaultValue={!!auto}
|
||||
onChange={val => handleAutomatic(variable, val)}
|
||||
onChange={val => handleAutomatic(variable, val, type)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{auto === 0 && (
|
||||
<>
|
||||
<div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}>
|
||||
{showTypeSwitch && (
|
||||
<FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange(variable, defaultValue)}/>
|
||||
)}
|
||||
{isString && (
|
||||
<Input
|
||||
className={cn(inputsIsFocus[variable] ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'rounded-lg border px-3 py-[6px]')}
|
||||
<MixedVariableTextInput
|
||||
value={varInput?.value as string || ''}
|
||||
onChange={handleMixedTypeChange(variable)}
|
||||
onChange={handleValueChange(variable, type)}
|
||||
nodesOutputVars={nodeOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
onFocusChange={handleInputFocus(variable)}
|
||||
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>
|
||||
)}
|
||||
{/* {isString && (
|
||||
<VarReferencePicker
|
||||
zIndex={1001}
|
||||
readonly={false}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
{isNumber && isConstant && (
|
||||
<Input
|
||||
className='h-8 grow'
|
||||
type='number'
|
||||
value={varInput?.value || ''}
|
||||
onChange={handleNotMixedTypeChange(variable)}
|
||||
defaultVarKindType={VarKindType.variable}
|
||||
filterVar={(varPayload: Var) => varPayload.type === VarType.number || varPayload.type === VarType.secret || varPayload.type === VarType.string}
|
||||
/>
|
||||
)} */}
|
||||
{(isNumber || isSelect) && (
|
||||
<VarReferencePicker
|
||||
zIndex={1001}
|
||||
readonly={false}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])}
|
||||
onChange={handleNotMixedTypeChange(variable)}
|
||||
defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)}
|
||||
isSupportConstantValue
|
||||
filterVar={isNumber ? (varPayload: Var) => varPayload.type === schema._type : undefined}
|
||||
availableVars={isSelect ? nodeOutputVars : undefined}
|
||||
schema={schema}
|
||||
onChange={handleValueChange(variable, type)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
)}
|
||||
{isFile && (
|
||||
<VarReferencePicker
|
||||
zIndex={1001}
|
||||
readonly={false}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.value || []}
|
||||
onChange={handleFileChange(variable)}
|
||||
defaultVarKindType={VarKindType.variable}
|
||||
filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile}
|
||||
{isBoolean && (
|
||||
<FormInputBoolean
|
||||
value={varInput?.value as boolean}
|
||||
onChange={handleValueChange(variable, type)}
|
||||
/>
|
||||
)}
|
||||
{isSelect && (
|
||||
<SimpleSelect
|
||||
wrapperClassName='h-8 grow'
|
||||
defaultValue={varInput?.value}
|
||||
items={options.filter((option: { show_on: any[] }) => {
|
||||
if (option.show_on.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
|
||||
return true
|
||||
}).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
|
||||
onSelect={item => handleValueChange(variable, type)(item.value as string)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
)}
|
||||
{isShowJSONEditor && isConstant && (
|
||||
<div className='mt-1 w-full'>
|
||||
<CodeEditor
|
||||
title='JSON'
|
||||
value={varInput?.value as any}
|
||||
isExpand
|
||||
isInNode
|
||||
height={100}
|
||||
language={CodeLanguage.json}
|
||||
onChange={handleValueChange(variable, type)}
|
||||
className='w-full'
|
||||
placeholder={<div className='whitespace-pre'>{placeholder?.[language] || placeholder?.en_US}</div>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isAppSelector && (
|
||||
<AppSelector
|
||||
disabled={false}
|
||||
@ -250,7 +316,21 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
scope={scope}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{showVariableSelector && (
|
||||
<VarReferencePicker
|
||||
zIndex={1001}
|
||||
className='h-8 grow'
|
||||
readonly={false}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.value || []}
|
||||
onChange={handleVariableSelectorChange(variable)}
|
||||
filterVar={getFilterVar()}
|
||||
schema={schema}
|
||||
valueTypePlaceHolder={targetVarType()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{url && (
|
||||
<a
|
||||
@ -267,7 +347,19 @@ 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, (s: SchemaRoot, rootName: string) => {
|
||||
setSchema(s)
|
||||
setSchemaRootName(rootName)
|
||||
showSchema()
|
||||
}))}
|
||||
{isShowSchema && (
|
||||
<SchemaModal
|
||||
isShow={isShowSchema}
|
||||
schema={schema!}
|
||||
rootName={schemaRootName}
|
||||
onClose={hideSchema}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
'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 { 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'
|
||||
|
||||
type Props = {
|
||||
isShow: boolean
|
||||
schema: SchemaRoot
|
||||
rootName: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const SchemaModal: FC<Props> = ({
|
||||
isShow,
|
||||
schema,
|
||||
rootName,
|
||||
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={schema}
|
||||
rootName={rootName}
|
||||
readOnly
|
||||
></VisualEditor>
|
||||
</VisualEditorContextProvider>
|
||||
</MittProvider>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(SchemaModal)
|
||||
@ -17,10 +17,13 @@ import { ToolTipContent } from '@/app/components/base/tooltip/content'
|
||||
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
|
||||
import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/components/switch-plugin-version'
|
||||
import cn from '@/utils/classnames'
|
||||
import McpToolNotSupportTooltip from '@/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip'
|
||||
|
||||
type Props = {
|
||||
icon?: any
|
||||
providerName?: string
|
||||
isMCPTool?: boolean
|
||||
providerShowName?: string
|
||||
toolLabel?: string
|
||||
showSwitch?: boolean
|
||||
switchValue?: boolean
|
||||
@ -35,11 +38,14 @@ type Props = {
|
||||
onInstall?: () => void
|
||||
versionMismatch?: boolean
|
||||
open: boolean
|
||||
canChooseMCPTool?: boolean,
|
||||
}
|
||||
|
||||
const ToolItem = ({
|
||||
open,
|
||||
icon,
|
||||
isMCPTool,
|
||||
providerShowName,
|
||||
providerName,
|
||||
toolLabel,
|
||||
showSwitch,
|
||||
@ -54,11 +60,13 @@ const ToolItem = ({
|
||||
isError,
|
||||
errorTip,
|
||||
versionMismatch,
|
||||
canChooseMCPTool,
|
||||
}: 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)
|
||||
const isShowCanNotChooseMCPTip = isMCPTool && !canChooseMCPTool
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
@ -67,7 +75,7 @@ const ToolItem = ({
|
||||
isDeleting && 'border-state-destructive-border shadow-xs hover:bg-state-destructive-hover',
|
||||
)}>
|
||||
{icon && (
|
||||
<div className={cn('shrink-0', isTransparent && 'opacity-50')}>
|
||||
<div className={cn('shrink-0', isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30')}>
|
||||
{typeof icon === 'string' && <div className='h-7 w-7 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge bg-cover bg-center' style={{ backgroundImage: `url(${icon})` }} />}
|
||||
{typeof icon !== 'string' && <AppIcon className='h-7 w-7 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge' size='xs' icon={icon?.content} background={icon?.background} />}
|
||||
</div>
|
||||
@ -75,18 +83,19 @@ const ToolItem = ({
|
||||
{!icon && (
|
||||
<div className={cn(
|
||||
'flex h-7 w-7 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle',
|
||||
isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30',
|
||||
)}>
|
||||
<div className='flex h-5 w-5 items-center justify-center opacity-35'>
|
||||
<Group className='text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('grow truncate pl-0.5', isTransparent && 'opacity-50')}>
|
||||
<div className={cn('grow truncate pl-0.5', isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30')}>
|
||||
<div className='system-2xs-medium-uppercase text-text-tertiary'>{providerNameText}</div>
|
||||
<div className='system-xs-medium text-text-secondary'>{toolLabel}</div>
|
||||
</div>
|
||||
<div className='hidden items-center gap-1 group-hover:flex'>
|
||||
{!noAuth && !isError && !uninstalled && !versionMismatch && (
|
||||
{!noAuth && !isError && !uninstalled && !versionMismatch && !isShowCanNotChooseMCPTip && (
|
||||
<ActionButton>
|
||||
<RiEqualizer2Line className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
@ -103,7 +112,7 @@ const ToolItem = ({
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</div>
|
||||
</div>
|
||||
{!isError && !uninstalled && !noAuth && !versionMismatch && showSwitch && (
|
||||
{!isError && !uninstalled && !noAuth && !versionMismatch && !isShowCanNotChooseMCPTip && showSwitch && (
|
||||
<div className='mr-1' onClick={e => e.stopPropagation()}>
|
||||
<Switch
|
||||
size='md'
|
||||
@ -112,6 +121,9 @@ const ToolItem = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isShowCanNotChooseMCPTip && (
|
||||
<McpToolNotSupportTooltip />
|
||||
)}
|
||||
{!isError && !uninstalled && !versionMismatch && noAuth && (
|
||||
<Button variant='secondary' size='small' onClick={onAuth}>
|
||||
{t('tools.notAuthorized')}
|
||||
|
||||
@ -462,9 +462,14 @@ export type StrategyDeclaration = {
|
||||
strategies: StrategyDetail[]
|
||||
}
|
||||
|
||||
export type PluginMeta = {
|
||||
version: string // the version of dify sdk
|
||||
}
|
||||
|
||||
export type StrategyPluginDetail = {
|
||||
provider: string
|
||||
plugin_unique_identifier: string
|
||||
plugin_id: string
|
||||
declaration: StrategyDeclaration
|
||||
meta: PluginMeta
|
||||
}
|
||||
|
||||
@ -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'>
|
||||
|
||||
75
web/app/components/tools/mcp/create-card.tsx
Normal file
75
web/app/components/tools/mcp/create-card.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
'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(() => {
|
||||
if (language.startsWith('zh_'))
|
||||
return 'https://docs.dify.ai/zh-hans/guides/tools/integrate-tool/mcp'
|
||||
if (language.startsWith('ja_jp'))
|
||||
return 'https://docs.dify.ai/ja_jp/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
|
||||
310
web/app/components/tools/mcp/detail/content.tsx
Normal file
310
web/app/components/tools/mcp/detail/content.tsx
Normal file
@ -0,0 +1,310 @@
|
||||
'use client'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import copy from 'copy-to-clipboard'
|
||||
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 Tooltip from '@/app/components/base/tooltip'
|
||||
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 { openOAuthPopup } from '@/hooks/use-oauth'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
detail: ToolWithProvider
|
||||
onUpdate: (isDelete?: boolean) => void
|
||||
onHide: () => void
|
||||
isCreation: boolean
|
||||
onFirstCreate: () => void
|
||||
}
|
||||
|
||||
const MCPDetailContent: FC<Props> = ({
|
||||
detail,
|
||||
onUpdate,
|
||||
onHide,
|
||||
isCreation,
|
||||
onFirstCreate,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
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 [isShowUpdateConfirm, {
|
||||
setTrue: showUpdateConfirm,
|
||||
setFalse: hideUpdateConfirm,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleUpdateTools = useCallback(async () => {
|
||||
hideUpdateConfirm()
|
||||
if (!detail)
|
||||
return
|
||||
await updateTools(detail.id)
|
||||
invalidateMCPTools(detail.id)
|
||||
onUpdate()
|
||||
}, [detail, hideUpdateConfirm, invalidateMCPTools, onUpdate, 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 handleOAuthCallback = useCallback(() => {
|
||||
if (!isCurrentWorkspaceManager)
|
||||
return
|
||||
if (!detail.id)
|
||||
return
|
||||
handleUpdateTools()
|
||||
}, [detail.id, handleUpdateTools, isCurrentWorkspaceManager])
|
||||
|
||||
const handleAuthorize = useCallback(async () => {
|
||||
onFirstCreate()
|
||||
if (!isCurrentWorkspaceManager)
|
||||
return
|
||||
if (!detail)
|
||||
return
|
||||
const res = await authorizeMcp({
|
||||
provider_id: detail.id,
|
||||
})
|
||||
if (res.result === 'success')
|
||||
handleUpdateTools()
|
||||
|
||||
else if (res.authorization_url)
|
||||
openOAuthPopup(res.authorization_url, handleOAuthCallback)
|
||||
}, [onFirstCreate, isCurrentWorkspaceManager, detail, authorizeMcp, handleUpdateTools, handleOAuthCallback])
|
||||
|
||||
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, deleteMCP, hideDeleting, hideDeleteConfirm, onUpdate])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreation)
|
||||
handleAuthorize()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
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='mt-0.5 flex items-center gap-1'>
|
||||
<Tooltip popupContent={t('tools.mcp.identifier')}>
|
||||
<div className='system-xs-regular shrink-0 cursor-pointer text-text-secondary' onClick={() => copy(detail.server_identifier || '')}>{detail.server_identifier}</div>
|
||||
</Tooltip>
|
||||
<div className='system-xs-regular shrink-0 text-text-quaternary'>·</div>
|
||||
<Tooltip popupContent={t('tools.mcp.modal.serverUrl')}>
|
||||
<div className='system-xs-regular truncate text-text-secondary'>{detail.server_url}</div>
|
||||
</Tooltip>
|
||||
</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'>
|
||||
{!isAuthorizing && detail.is_team_authorization && (
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='w-full'
|
||||
onClick={handleAuthorize}
|
||||
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='flex grow flex-col'>
|
||||
{((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>
|
||||
</>
|
||||
)}
|
||||
{!isUpdating && 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>
|
||||
)}
|
||||
{!isUpdating && !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={showUpdateConfirm}>
|
||||
<RiLoopLeftLine className='mr-1 h-3.5 w-3.5' />
|
||||
{t('tools.mcp.update')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-0 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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isUpdating && !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}
|
||||
/>
|
||||
)}
|
||||
{isShowUpdateConfirm && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('tools.mcp.update')}
|
||||
onCancel={hideUpdateConfirm}
|
||||
onConfirm={handleUpdateTools}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPDetailContent
|
||||
37
web/app/components/tools/mcp/detail/list-loading.tsx
Normal file
37
web/app/components/tools/mcp/detail/list-loading.tsx
Normal 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
|
||||
88
web/app/components/tools/mcp/detail/operation-dropdown.tsx
Normal file
88
web/app/components/tools/mcp/detail/operation-dropdown.tsx
Normal 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)
|
||||
56
web/app/components/tools/mcp/detail/provider-detail.tsx
Normal file
56
web/app/components/tools/mcp/detail/provider-detail.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
'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
|
||||
isCreation: boolean
|
||||
onFirstCreate: () => void
|
||||
}
|
||||
|
||||
const MCPDetailPanel: FC<Props> = ({
|
||||
detail,
|
||||
onUpdate,
|
||||
onHide,
|
||||
isCreation,
|
||||
onFirstCreate,
|
||||
}) => {
|
||||
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}
|
||||
isCreation={isCreation}
|
||||
onFirstCreate={onFirstCreate}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPDetailPanel
|
||||
41
web/app/components/tools/mcp/detail/tool-item.tsx
Normal file
41
web/app/components/tools/mcp/detail/tool-item.tsx
Normal 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
|
||||
12
web/app/components/tools/mcp/hooks.ts
Normal file
12
web/app/components/tools/mcp/hooks.ts
Normal 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 }
|
||||
}
|
||||
92
web/app/components/tools/mcp/index.tsx
Normal file
92
web/app/components/tools/mcp/index.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
'use client'
|
||||
import { useMemo, useState } from 'react'
|
||||
import NewMCPCard from './create-card'
|
||||
import MCPCard from './provider-card'
|
||||
import MCPDetailPanel from './detail/provider-detail'
|
||||
import {
|
||||
useAllToolProviders,
|
||||
} 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 { data: list = [] as ToolWithProvider[], refetch } = useAllToolProviders()
|
||||
const [isCreation, setIsCreation] = useState<boolean>(false)
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
return list.filter((collection) => {
|
||||
if (searchText)
|
||||
return Object.values(collection.name).some(value => (value as string).toLowerCase().includes(searchText.toLowerCase()))
|
||||
return collection.type === 'mcp'
|
||||
}) as ToolWithProvider[]
|
||||
}, [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)
|
||||
setIsCreation(true)
|
||||
}
|
||||
|
||||
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 as ToolWithProvider}
|
||||
handleSelect={setCurrentProviderID}
|
||||
onUpdate={refetch}
|
||||
/>
|
||||
))}
|
||||
{!list.length && renderDefaultCard()}
|
||||
</div>
|
||||
{currentProvider && (
|
||||
<MCPDetailPanel
|
||||
detail={currentProvider as ToolWithProvider}
|
||||
onHide={() => setCurrentProviderID(undefined)}
|
||||
onUpdate={refetch}
|
||||
isCreation={isCreation}
|
||||
onFirstCreate={() => setIsCreation(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default MCPList
|
||||
134
web/app/components/tools/mcp/mcp-server-modal.tsx
Normal file
134
web/app/components/tools/mcp/mcp-server-modal.tsx
Normal 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
|
||||
37
web/app/components/tools/mcp/mcp-server-param-item.tsx
Normal file
37
web/app/components/tools/mcp/mcp-server-param-item.tsx
Normal 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
|
||||
217
web/app/components/tools/mcp/mcp-service-card.tsx
Normal file
217
web/app/components/tools/mcp/mcp-service-card.tsx
Normal file
@ -0,0 +1,217 @@
|
||||
'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 ? `${appInfo.api_base_url.replace('/v1', '')}/mcp/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)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
154
web/app/components/tools/mcp/mock.ts
Normal file
154
web/app/components/tools/mcp/mock.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
]
|
||||
213
web/app/components/tools/mcp/modal.tsx
Normal file
213
web/app/components/tools/mcp/modal.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
'use client'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getDomain } from 'tldts'
|
||||
import { RiCloseLine, RiEditLine } 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 Toast from '@/app/components/base/toast'
|
||||
import { uploadRemoteFileInfo } from '@/service/common'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useHover } from 'ahooks'
|
||||
|
||||
export type DuplicateAppModalProps = {
|
||||
data?: ToolWithProvider
|
||||
show: boolean
|
||||
onConfirm: (info: {
|
||||
name: string
|
||||
server_url: string
|
||||
icon_type: AppIconType
|
||||
icon: string
|
||||
icon_background?: string | null
|
||||
server_identifier: string
|
||||
}) => 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 originalServerUrl = data?.server_url
|
||||
const [url, setUrl] = React.useState(data?.server_url || '')
|
||||
const [name, setName] = React.useState(data?.name || '')
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>(getIcon(data))
|
||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||
const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '')
|
||||
const [isFetchingIcon, setIsFetchingIcon] = useState(false)
|
||||
const appIconRef = useRef<HTMLDivElement>(null)
|
||||
const isHovering = useHover(appIconRef)
|
||||
|
||||
const isValidUrl = (string: string) => {
|
||||
try {
|
||||
const urlPattern = /^(https?:\/\/)((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3}))(\:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?/i
|
||||
return urlPattern.test(string)
|
||||
}
|
||||
catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isValidServerID = (str: string) => {
|
||||
return /^[a-z0-9_-]{1,24}$/.test(str)
|
||||
}
|
||||
|
||||
const handleBlur = async (url: string) => {
|
||||
if (data)
|
||||
return
|
||||
if (!isValidUrl(url))
|
||||
return
|
||||
const domain = getDomain(url)
|
||||
const remoteIcon = `https://www.google.com/s2/favicons?domain=${domain}&sz=128`
|
||||
setIsFetchingIcon(true)
|
||||
try {
|
||||
const res = await uploadRemoteFileInfo(remoteIcon, undefined, true)
|
||||
setAppIcon({ type: 'image', url: res.url, fileId: extractFileId(res.url) || '' })
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to fetch remote icon:', e)
|
||||
Toast.notify({ type: 'error', message: 'Failed to fetch remote icon' })
|
||||
}
|
||||
finally {
|
||||
setIsFetchingIcon(false)
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (!isValidUrl(url)) {
|
||||
Toast.notify({ type: 'error', message: 'invalid server url' })
|
||||
return
|
||||
}
|
||||
if (!isValidServerID(serverIdentifier.trim())) {
|
||||
Toast.notify({ type: 'error', message: 'invalid server identifier' })
|
||||
return
|
||||
}
|
||||
await onConfirm({
|
||||
server_url: originalServerUrl === url ? '[__HIDDEN__]' : url.trim(),
|
||||
name,
|
||||
icon_type: appIcon.type,
|
||||
icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
|
||||
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
|
||||
server_identifier: serverIdentifier.trim(),
|
||||
})
|
||||
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'>{data ? t('tools.mcp.modal.editTitle') : t('tools.mcp.modal.title')}</div>
|
||||
<div className='space-y-5 py-3'>
|
||||
<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)}
|
||||
onBlur={e => handleBlur(e.target.value.trim())}
|
||||
placeholder={t('tools.mcp.modal.serverUrlPlaceholder')}
|
||||
/>
|
||||
{originalServerUrl && originalServerUrl !== url && (
|
||||
<div className='mt-1 flex h-5 items-center'>
|
||||
<span className='body-xs-regular text-text-warning'>{t('tools.mcp.modal.warning')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<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' ref={appIconRef}>
|
||||
<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='relative cursor-pointer rounded-2xl'
|
||||
coverElement={
|
||||
isHovering
|
||||
? (<div className='absolute inset-0 flex items-center justify-center overflow-hidden rounded-2xl bg-background-overlay-alt'>
|
||||
<RiEditLine className='size-6 text-text-primary-on-surface' />
|
||||
</div>) : null
|
||||
}
|
||||
onClick={() => { setShowAppIconPicker(true) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='flex h-6 items-center'>
|
||||
<span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.serverIdentifier')}</span>
|
||||
</div>
|
||||
<div className='body-xs-regular mb-1 text-text-tertiary'>{t('tools.mcp.modal.serverIdentifierTip')}</div>
|
||||
<Input
|
||||
value={serverIdentifier}
|
||||
onChange={e => setServerIdentifier(e.target.value)}
|
||||
placeholder={t('tools.mcp.modal.serverIdentifierPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row-reverse pt-5'>
|
||||
<Button disabled={!name || !url || !serverIdentifier || isFetchingIcon} 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
|
||||
154
web/app/components/tools/mcp/provider-card.tsx
Normal file
154
web/app/components/tools/mcp/provider-card.tsx
Normal 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='system-xs-regular text-text-tertiary'>{data.server_identifier}</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='flex w-0 grow 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 shrink-0 text-text-tertiary'>{t('tools.mcp.toolsCount', { count: data.tools.length })}</div>
|
||||
)}
|
||||
{!data.tools.length && (
|
||||
<div className='system-xs-regular shrink-0 text-text-tertiary'>{t('tools.mcp.noTools')}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn('system-xs-regular text-divider-deep', (!data.is_team_authorization || !data.tools.length) && 'sm:hidden')}>/</div>
|
||||
<div className={cn('system-xs-regular truncate text-text-tertiary', (!data.is_team_authorization || !data.tools.length) && ' sm:hidden')} title={`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at! * 1000)}`}>{`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at! * 1000)}`}</div>
|
||||
</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
|
||||
@ -15,6 +15,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'
|
||||
@ -31,6 +32,7 @@ const ProviderList = () => {
|
||||
{ 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 +87,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 +100,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 +137,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}
|
||||
|
||||
@ -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'
|
||||
@ -47,20 +47,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>
|
||||
|
||||
@ -29,6 +29,7 @@ export enum CollectionType {
|
||||
custom = 'api',
|
||||
model = 'model',
|
||||
workflow = 'workflow',
|
||||
mcp = 'mcp',
|
||||
}
|
||||
|
||||
export type Emoji = {
|
||||
@ -50,6 +51,10 @@ export type Collection = {
|
||||
labels: string[]
|
||||
plugin_id?: string
|
||||
letter?: string
|
||||
// MCP Server
|
||||
server_url?: string
|
||||
updated_at?: number
|
||||
server_identifier?: string
|
||||
}
|
||||
|
||||
export type ToolParameter = {
|
||||
@ -168,3 +173,11 @@ export type WorkflowToolProviderResponse = {
|
||||
}
|
||||
privacy_policy: string
|
||||
}
|
||||
|
||||
export type MCPServerDetail = {
|
||||
id: string
|
||||
server_code: string
|
||||
description: string
|
||||
status: string
|
||||
parameters?: Record<string, string>
|
||||
}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import type { ToolCredential, ToolParameter } from '../types'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
|
||||
export const toType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
@ -54,7 +57,7 @@ export const toolCredentialToFormSchemas = (parameters: ToolCredential[]) => {
|
||||
return formSchemas
|
||||
}
|
||||
|
||||
export const addDefaultValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[]) => {
|
||||
export const addDefaultValue = (value: Record<string, any>, formSchemas: { variable: string; type: string; default?: any }[]) => {
|
||||
const newValues = { ...value }
|
||||
formSchemas.forEach((formSchema) => {
|
||||
const itemValue = value[formSchema.variable]
|
||||
@ -64,14 +67,47 @@ export const addDefaultValue = (value: Record<string, any>, formSchemas: { varia
|
||||
return newValues
|
||||
}
|
||||
|
||||
export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[], isReasoning = false) => {
|
||||
const correctInitialData = (type: string, target: any, defaultValue: any) => {
|
||||
if (type === 'text-input' || type === 'secret-input')
|
||||
target.type = 'mixed'
|
||||
|
||||
if (type === 'boolean') {
|
||||
if (typeof defaultValue === 'string')
|
||||
target.value = defaultValue === 'true' || defaultValue === '1'
|
||||
|
||||
if (typeof defaultValue === 'boolean')
|
||||
target.value = defaultValue
|
||||
|
||||
if (typeof defaultValue === 'number')
|
||||
target.value = defaultValue === 1
|
||||
}
|
||||
|
||||
if (type === 'number-input') {
|
||||
if (typeof defaultValue === 'string' && defaultValue !== '')
|
||||
target.value = Number.parseFloat(defaultValue)
|
||||
}
|
||||
|
||||
if (type === 'app-selector' || type === 'model-selector')
|
||||
target.value = defaultValue
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => {
|
||||
const newValues = {} as any
|
||||
formSchemas.forEach((formSchema) => {
|
||||
const itemValue = value[formSchema.variable]
|
||||
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
|
||||
const value = formSchema.default
|
||||
newValues[formSchema.variable] = {
|
||||
...(isReasoning ? { value: null, auto: 1 } : { value: formSchema.default }),
|
||||
value: {
|
||||
type: 'constant',
|
||||
value: formSchema.default,
|
||||
},
|
||||
...(isReasoning ? { auto: 1, value: null } : {}),
|
||||
}
|
||||
if (!isReasoning)
|
||||
newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, value)
|
||||
}
|
||||
})
|
||||
return newValues
|
||||
@ -80,7 +116,9 @@ export const generateFormValue = (value: Record<string, any>, formSchemas: { var
|
||||
export const getPlainValue = (value: Record<string, any>) => {
|
||||
const plainValue = { ...value }
|
||||
Object.keys(plainValue).forEach((key) => {
|
||||
plainValue[key] = value[key].value
|
||||
plainValue[key] = {
|
||||
...value[key].value,
|
||||
}
|
||||
})
|
||||
return plainValue
|
||||
}
|
||||
@ -94,3 +132,65 @@ export const getStructureValue = (value: Record<string, any>) => {
|
||||
})
|
||||
return newValue
|
||||
}
|
||||
|
||||
export const getConfiguredValue = (value: Record<string, any>, formSchemas: { variable: string; type: string; default?: any }[]) => {
|
||||
const newValues = { ...value }
|
||||
formSchemas.forEach((formSchema) => {
|
||||
const itemValue = value[formSchema.variable]
|
||||
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
|
||||
const value = formSchema.default
|
||||
newValues[formSchema.variable] = {
|
||||
type: 'constant',
|
||||
value: formSchema.default,
|
||||
}
|
||||
newValues[formSchema.variable] = correctInitialData(formSchema.type, newValues[formSchema.variable], value)
|
||||
}
|
||||
})
|
||||
return newValues
|
||||
}
|
||||
|
||||
const getVarKindType = (type: FormTypeEnum) => {
|
||||
if (type === FormTypeEnum.file || type === FormTypeEnum.files)
|
||||
return VarKindType.variable
|
||||
if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber)
|
||||
return VarKindType.constant
|
||||
if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput)
|
||||
return VarKindType.mixed
|
||||
}
|
||||
|
||||
export const generateAgentToolValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => {
|
||||
const newValues = {} as any
|
||||
if (!isReasoning) {
|
||||
formSchemas.forEach((formSchema) => {
|
||||
const itemValue = value[formSchema.variable]
|
||||
newValues[formSchema.variable] = {
|
||||
value: {
|
||||
type: 'constant',
|
||||
value: itemValue.value,
|
||||
},
|
||||
}
|
||||
newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, itemValue.value)
|
||||
})
|
||||
}
|
||||
else {
|
||||
formSchemas.forEach((formSchema) => {
|
||||
const itemValue = value[formSchema.variable]
|
||||
if (itemValue.auto === 1) {
|
||||
newValues[formSchema.variable] = {
|
||||
auto: 1,
|
||||
value: null,
|
||||
}
|
||||
}
|
||||
else {
|
||||
newValues[formSchema.variable] = {
|
||||
auto: 0,
|
||||
value: itemValue.value || {
|
||||
type: getVarKindType(formSchema.type as FormTypeEnum),
|
||||
value: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return newValues
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,11 +18,10 @@ 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'
|
||||
import useCheckVerticalScrollbar from './use-check-vertical-scrollbar'
|
||||
|
||||
type AllToolsProps = {
|
||||
className?: string
|
||||
@ -31,11 +31,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[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_TAGS: AllToolsProps['tags'] = []
|
||||
@ -46,12 +47,14 @@ const AllTools = ({
|
||||
searchText,
|
||||
tags = DEFAULT_TAGS,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
buildInTools,
|
||||
workflowTools,
|
||||
customTools,
|
||||
supportAddCustomTool,
|
||||
onShowAddCustomCollectionModal,
|
||||
mcpTools = [],
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}: AllToolsProps) => {
|
||||
const language = useGetLanguage()
|
||||
const tabs = useToolTabs()
|
||||
@ -64,13 +67,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 +85,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 +108,12 @@ const AllTools = ({
|
||||
|
||||
const pluginRef = useRef<ListRef>(null)
|
||||
const wrapElemRef = useRef<HTMLDivElement>(null)
|
||||
const hasVerticalScrollbar = useCheckVerticalScrollbar(wrapElemRef)
|
||||
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 +131,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,9 +145,13 @@ 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}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
hasScrollBar={hasVerticalScrollbar}
|
||||
/>
|
||||
{/* Plugins from marketplace */}
|
||||
{enable_marketplace && <PluginList
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -74,17 +74,18 @@ type IndexBarProps = {
|
||||
letters: string[]
|
||||
itemRefs: RefObject<{ [key: string]: HTMLElement | null }>
|
||||
className?: string
|
||||
hasScrollBar: boolean
|
||||
}
|
||||
|
||||
const IndexBar: FC<IndexBarProps> = ({ letters, itemRefs, className }) => {
|
||||
const IndexBar: FC<IndexBarProps> = ({ letters, itemRefs, className, hasScrollBar }) => {
|
||||
const handleIndexClick = (letter: string) => {
|
||||
const element = itemRefs.current?.[letter]
|
||||
if (element)
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
return (
|
||||
<div className={classNames('index-bar absolute right-0 top-36 flex flex-col items-center w-6 justify-center text-xs font-medium text-text-quaternary', className)}>
|
||||
<div className='absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]'></div>
|
||||
<div className={classNames('index-bar absolute right-0 top-36 flex flex-col items-center w-6 justify-center text-xs font-medium text-text-quaternary', hasScrollBar && 'right-3', className)}>
|
||||
<div className={classNames('absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]')}></div>
|
||||
{letters.map(letter => (
|
||||
<div className="cursor-pointer hover:text-text-secondary" key={letter} onClick={() => handleIndexClick(letter)}>
|
||||
{letter}
|
||||
|
||||
@ -129,33 +129,35 @@ 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')!}
|
||||
inputClassName='grow'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
onSelect={handleSelect}
|
||||
searchText={searchText}
|
||||
tags={tags}
|
||||
|
||||
@ -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,25 +59,31 @@ 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>
|
||||
)
|
||||
}
|
||||
{
|
||||
activeTab === TabsEnum.Tools && (
|
||||
<AllTools
|
||||
className='w-[315px]'
|
||||
className='w-[400px]'
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
tags={tags}
|
||||
canNotSelectMultiple
|
||||
buildInTools={buildInTools || []}
|
||||
customTools={customTools || []}
|
||||
workflowTools={workflowTools || []}
|
||||
mcpTools={mcpTools || []}
|
||||
canChooseMCPTool
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,9 +35,11 @@ type Props = {
|
||||
isShow: boolean
|
||||
onShowChange: (isShow: boolean) => void
|
||||
onSelect: (tool: ToolDefaultValue) => void
|
||||
onSelectMultiple: (tools: ToolDefaultValue[]) => void
|
||||
supportAddCustomTool?: boolean
|
||||
scope?: string
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const ToolPicker: FC<Props> = ({
|
||||
@ -48,10 +50,12 @@ const ToolPicker: FC<Props> = ({
|
||||
isShow,
|
||||
onShowChange,
|
||||
onSelect,
|
||||
onSelectMultiple,
|
||||
supportAddCustomTool,
|
||||
scope = 'all',
|
||||
selectedTools,
|
||||
panelClassName,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
@ -61,6 +65,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 +107,10 @@ const ToolPicker: FC<Props> = ({
|
||||
onSelect(tool!)
|
||||
}
|
||||
|
||||
const handleSelectMultiple = (_type: BlockEnum, tools: ToolDefaultValue[]) => {
|
||||
onSelectMultiple(tools)
|
||||
}
|
||||
|
||||
const [isShowEditCollectionToolModal, {
|
||||
setFalse: hideEditCustomCollectionModal,
|
||||
setTrue: showEditCustomCollectionModal,
|
||||
@ -142,7 +151,7 @@ const ToolPicker: FC<Props> = ({
|
||||
</PortalToFollowElemTrigger>
|
||||
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className={cn('relative min-h-20 w-[356px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', panelClassName)}>
|
||||
<div className={cn('relative min-h-20 w-[400px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', panelClassName)}>
|
||||
<div className='p-2 pb-1'>
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
@ -151,21 +160,26 @@ const ToolPicker: FC<Props> = ({
|
||||
onTagsChange={setTags}
|
||||
size='small'
|
||||
placeholder={t('plugin.searchTools')!}
|
||||
supportAddCustomTool={supportAddCustomTool}
|
||||
onAddedCustomTool={handleAddedCustomTool}
|
||||
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
|
||||
inputClassName='grow'
|
||||
|
||||
/>
|
||||
</div>
|
||||
<AllTools
|
||||
className='mt-1'
|
||||
toolContentClassName='max-w-[360px]'
|
||||
toolContentClassName='max-w-[100%]'
|
||||
tags={tags}
|
||||
searchText={searchText}
|
||||
onSelect={handleSelect}
|
||||
onSelectMultiple={handleSelectMultiple}
|
||||
buildInTools={builtinToolList || []}
|
||||
customTools={customToolList || []}
|
||||
workflowTools={workflowToolList || []}
|
||||
supportAddCustomTool={supportAddCustomTool}
|
||||
onAddedCustomTool={handleAddedCustomTool}
|
||||
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
|
||||
mcpTools={mcpTools || []}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
|
||||
@ -10,13 +10,12 @@ 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
|
||||
payload: Tool
|
||||
disabled?: boolean
|
||||
isAdded?: boolean
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
}
|
||||
|
||||
@ -25,6 +24,7 @@ const ToolItem: FC<Props> = ({
|
||||
payload,
|
||||
onSelect,
|
||||
disabled,
|
||||
isAdded,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -71,18 +71,16 @@ const ToolItem: FC<Props> = ({
|
||||
output_schema: payload.output_schema,
|
||||
paramSchemas: payload.parameters,
|
||||
params,
|
||||
meta: provider.meta,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{isAdded && (
|
||||
<div className='system-xs-regular mr-4 text-text-tertiary'>{t('tools.addToolModal.added')}</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip >
|
||||
)
|
||||
|
||||
@ -13,9 +13,12 @@ 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[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const ToolViewFlatView: FC<Props> = ({
|
||||
@ -24,8 +27,11 @@ const ToolViewFlatView: FC<Props> = ({
|
||||
isShowLetterIndex,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
toolRefs,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const firstLetterToolIds = useMemo(() => {
|
||||
const res: Record<string, string> = {}
|
||||
@ -53,7 +59,10 @@ const ToolViewFlatView: FC<Props> = ({
|
||||
isShowLetterIndex={isShowLetterIndex}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -12,7 +12,10 @@ type Props = {
|
||||
toolList: ToolWithProvider[]
|
||||
hasSearchText: boolean
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const Item: FC<Props> = ({
|
||||
@ -20,7 +23,10 @@ const Item: FC<Props> = ({
|
||||
toolList,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
@ -36,7 +42,10 @@ const Item: FC<Props> = ({
|
||||
isShowLetterIndex={false}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -12,14 +12,20 @@ type Props = {
|
||||
payload: Record<string, ToolWithProvider[]>
|
||||
hasSearchText: boolean
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const ToolListTreeView: FC<Props> = ({
|
||||
payload,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const getI18nGroupName = useCallback((name: string) => {
|
||||
@ -46,7 +52,10 @@ const ToolListTreeView: FC<Props> = ({
|
||||
toolList={payload[groupName]}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -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,9 @@ import { ViewType } from '../view-type-select'
|
||||
import ActionItem from './action-item'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHover } from 'ahooks'
|
||||
import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip'
|
||||
import { Mcp } from '@/app/components/base/icons/src/vender/other'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
@ -21,7 +24,10 @@ type Props = {
|
||||
isShowLetterIndex: boolean
|
||||
hasSearchText: boolean
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const Tool: FC<Props> = ({
|
||||
@ -31,18 +37,86 @@ const Tool: FC<Props> = ({
|
||||
isShowLetterIndex,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
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 isMCPTool = payload.type === CollectionType.mcp
|
||||
const isShowCanNotChooseMCPTip = !canChooseMCPTool && isMCPTool
|
||||
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,58 +146,72 @@ 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'
|
||||
className='group/item 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'>
|
||||
<div className={cn('flex h-8 grow items-center', isShowCanNotChooseMCPTip && 'opacity-30')}>
|
||||
<BlockIcon
|
||||
className='shrink-0'
|
||||
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 flex w-0 grow items-center text-sm text-text-primary'>
|
||||
<span className='max-w-[250px] truncate'>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span>
|
||||
{isFlatView && groupName && (
|
||||
<span className='system-xs-regular ml-2 shrink-0 text-text-quaternary'>{groupName}</span>
|
||||
)}
|
||||
{isMCPTool && <Mcp className='ml-2 size-3.5 shrink-0 text-text-quaternary' />}
|
||||
</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'>
|
||||
{!isShowCanNotChooseMCPTip && !canNotSelectMultiple && (notShowProvider ? notShowProviderSelectInfo : selectedInfo)}
|
||||
{isShowCanNotChooseMCPTip && <McpToolNotSupportTooltip />}
|
||||
{hasAction && (
|
||||
<FoldIcon className={cn('h-4 w-4 shrink-0 text-text-quaternary', isFold && 'text-text-tertiary')} />
|
||||
<FoldIcon className={cn('h-4 w-4 shrink-0 text-text-tertiary group-hover/item:text-text-tertiary', isFold && 'text-text-quaternary')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasAction && !isFold && (
|
||||
{!notShowProvider && hasAction && !isFold && (
|
||||
actions.map(action => (
|
||||
<ActionItem
|
||||
key={action.name}
|
||||
provider={payload}
|
||||
payload={action}
|
||||
onSelect={onSelect}
|
||||
disabled={getIsDisabled(action)}
|
||||
disabled={getIsDisabled(action) || isShowCanNotChooseMCPTip}
|
||||
isAdded={getIsDisabled(action)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@ -17,22 +17,30 @@ 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
|
||||
className?: string
|
||||
indexBarClassName?: string
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
hasScrollBar: boolean
|
||||
}
|
||||
const Blocks = ({
|
||||
showWorkflowEmpty,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
tools,
|
||||
viewType,
|
||||
hasSearchText,
|
||||
className,
|
||||
indexBarClassName,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
hasScrollBar,
|
||||
}: ToolsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
@ -87,7 +95,7 @@ const Blocks = ({
|
||||
const toolRefs = useRef({})
|
||||
|
||||
return (
|
||||
<div className={classNames('p-1 max-w-[320px]', className)}>
|
||||
<div className={classNames('p-1 max-w-[100%]', className)}>
|
||||
{
|
||||
!tools.length && !showWorkflowEmpty && (
|
||||
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>{t('workflow.tabs.noResult')}</div>
|
||||
@ -107,19 +115,25 @@ const Blocks = ({
|
||||
isShowLetterIndex={isShowLetterIndex}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
) : (
|
||||
<ToolListTreeView
|
||||
payload={treeViewToolsData}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{isShowLetterIndex && <IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />}
|
||||
{isShowLetterIndex && <IndexBar hasScrollBar={hasScrollBar} letters={letters} itemRefs={toolRefs} className={indexBarClassName} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import type { PluginMeta } from '../../plugins/types'
|
||||
|
||||
export enum TabsEnum {
|
||||
Blocks = 'blocks',
|
||||
Tools = 'tools',
|
||||
@ -8,6 +10,7 @@ export enum ToolTypeEnum {
|
||||
BuiltIn = 'built-in',
|
||||
Custom = 'custom',
|
||||
Workflow = 'workflow',
|
||||
MCP = 'mcp',
|
||||
}
|
||||
|
||||
export enum BlockClassificationEnum {
|
||||
@ -30,10 +33,12 @@ export type ToolDefaultValue = {
|
||||
params: Record<string, any>
|
||||
paramSchemas: Record<string, any>[]
|
||||
output_schema: Record<string, any>
|
||||
meta?: PluginMeta
|
||||
}
|
||||
|
||||
export type ToolValue = {
|
||||
provider_name: string
|
||||
provider_show_name?: string
|
||||
tool_name: string
|
||||
tool_label: string
|
||||
tool_description?: string
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const useCheckVerticalScrollbar = (ref: React.RefObject<HTMLElement>) => {
|
||||
const [hasVerticalScrollbar, setHasVerticalScrollbar] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const elem = ref.current
|
||||
if (!elem) return
|
||||
|
||||
const checkScrollbar = () => {
|
||||
setHasVerticalScrollbar(elem.scrollHeight > elem.clientHeight)
|
||||
}
|
||||
|
||||
checkScrollbar()
|
||||
|
||||
const resizeObserver = new ResizeObserver(checkScrollbar)
|
||||
resizeObserver.observe(elem)
|
||||
|
||||
const mutationObserver = new MutationObserver(checkScrollbar)
|
||||
mutationObserver.observe(elem, { childList: true, subtree: true, characterData: true })
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
mutationObserver.disconnect()
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
return hasVerticalScrollbar
|
||||
}
|
||||
|
||||
export default useCheckVerticalScrollbar
|
||||
@ -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'
|
||||
@ -445,6 +446,13 @@ export const useFetchToolsData = () => {
|
||||
workflowTools: workflowTools || [],
|
||||
})
|
||||
}
|
||||
if(type === 'mcp') {
|
||||
const mcpTools = await fetchAllMCPTools()
|
||||
|
||||
workflowStore.setState({
|
||||
mcpTools: mcpTools || [],
|
||||
})
|
||||
}
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
@ -491,6 +499,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)
|
||||
return ''
|
||||
@ -500,11 +510,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, buildInTools, customTools, mcpTools, workflowTools])
|
||||
|
||||
return toolIcon
|
||||
}
|
||||
|
||||
@ -234,6 +234,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
handleFetchAllTools('builtin')
|
||||
handleFetchAllTools('custom')
|
||||
handleFetchAllTools('workflow')
|
||||
handleFetchAllTools('mcp')
|
||||
}, [handleFetchAllTools])
|
||||
|
||||
const {
|
||||
|
||||
@ -67,6 +67,7 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s
|
||||
icon: getIcon(item.declaration.identity.icon),
|
||||
label: item.declaration.identity.label as any,
|
||||
type: CollectionType.all,
|
||||
meta: item.meta,
|
||||
tools: item.declaration.strategies.map(strategy => ({
|
||||
name: strategy.identity.name,
|
||||
author: strategy.identity.author,
|
||||
@ -88,10 +89,11 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s
|
||||
export type AgentStrategySelectorProps = {
|
||||
value?: Strategy,
|
||||
onChange: (value?: Strategy) => void,
|
||||
canChooseMCPTool: boolean,
|
||||
}
|
||||
|
||||
export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => {
|
||||
const { value, onChange } = props
|
||||
const { value, onChange, canChooseMCPTool } = props
|
||||
const [open, setOpen] = useState(false)
|
||||
const [viewType, setViewType] = useState<ViewType>(ViewType.flat)
|
||||
const [query, setQuery] = useState('')
|
||||
@ -210,11 +212,17 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
|
||||
agent_strategy_label: tool!.tool_label,
|
||||
agent_output_schema: tool!.output_schema,
|
||||
plugin_unique_identifier: tool!.provider_id,
|
||||
meta: tool!.meta,
|
||||
})
|
||||
setOpen(false)
|
||||
}}
|
||||
className='h-full max-h-full max-w-none overflow-y-auto'
|
||||
indexBarClassName='top-0 xl:top-36' showWorkflowEmpty={false} hasSearchText={false} />
|
||||
indexBarClassName='top-0 xl:top-36'
|
||||
showWorkflowEmpty={false}
|
||||
hasSearchText={false}
|
||||
canNotSelectMultiple
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
<PluginList
|
||||
ref={pluginRef}
|
||||
wrapElemRef={wrapElemRef}
|
||||
|
||||
@ -19,6 +19,8 @@ import { useWorkflowStore } from '../../../store'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import type { NodeOutPutVar } from '../../../types'
|
||||
import type { Node } from 'reactflow'
|
||||
import type { PluginMeta } from '@/app/components/plugins/types'
|
||||
import { noop } from 'lodash'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
export type Strategy = {
|
||||
@ -27,6 +29,7 @@ export type Strategy = {
|
||||
agent_strategy_label: string
|
||||
agent_output_schema: Record<string, any>
|
||||
plugin_unique_identifier: string
|
||||
meta?: PluginMeta
|
||||
}
|
||||
|
||||
export type AgentStrategyProps = {
|
||||
@ -38,6 +41,7 @@ export type AgentStrategyProps = {
|
||||
nodeOutputVars?: NodeOutPutVar[],
|
||||
availableNodes?: Node[],
|
||||
nodeId?: string
|
||||
canChooseMCPTool: boolean
|
||||
}
|
||||
|
||||
type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field
|
||||
@ -48,7 +52,7 @@ type MultipleToolSelectorSchema = CustomSchema<'array[tools]'>
|
||||
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema
|
||||
|
||||
export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props
|
||||
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId, canChooseMCPTool } = props
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
|
||||
@ -57,6 +61,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
const {
|
||||
setControlPromptEditorRerenderKey,
|
||||
} = workflowStore.getState()
|
||||
|
||||
const override: ComponentProps<typeof Form<CustomField>>['override'] = [
|
||||
[FormTypeEnum.textNumber, FormTypeEnum.textInput],
|
||||
(schema, props) => {
|
||||
@ -168,6 +173,8 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
value={value}
|
||||
onSelect={item => onChange(item)}
|
||||
onDelete={() => onChange(null)}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
onSelectMultiple={noop}
|
||||
/>
|
||||
</Field>
|
||||
)
|
||||
@ -189,13 +196,14 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
onChange={onChange}
|
||||
supportCollapse
|
||||
required={schema.required}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return <div className='space-y-2'>
|
||||
<AgentStrategySelector value={strategy} onChange={onStrategyChange} />
|
||||
<AgentStrategySelector value={strategy} onChange={onStrategyChange} canChooseMCPTool={canChooseMCPTool} />
|
||||
{
|
||||
strategy
|
||||
? <div>
|
||||
@ -215,6 +223,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
nodeId={nodeId}
|
||||
nodeOutputVars={nodeOutputVars || []}
|
||||
availableNodes={availableNodes || []}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</div>
|
||||
: <ListEmpty
|
||||
|
||||
@ -76,7 +76,7 @@ const Base: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<Wrap className={cn(wrapClassName)} style={wrapStyle} isInNode={isInNode} isExpand={isExpand}>
|
||||
<div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', isFocus ? 'border-transparent bg-components-input-bg-normal' : 'overflow-hidden border-components-input-border-hover bg-components-input-bg-hover')}>
|
||||
<div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', !isFocus ? 'border-transparent bg-components-input-bg-normal' : 'overflow-hidden border-components-input-border-hover bg-components-input-bg-hover')}>
|
||||
<div className='flex h-7 items-center justify-between pl-3 pr-2 pt-1'>
|
||||
<div className='system-xs-semibold-uppercase text-text-secondary'>{title}</div>
|
||||
<div className='flex items-center' onClick={(e) => {
|
||||
|
||||
@ -23,7 +23,7 @@ export type Props = {
|
||||
value?: string | object
|
||||
placeholder?: React.JSX.Element | string
|
||||
onChange?: (value: string) => void
|
||||
title?: React.JSX.Element
|
||||
title?: string | React.JSX.Element
|
||||
language: CodeLanguage
|
||||
headerRight?: React.JSX.Element
|
||||
readOnly?: boolean
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
value: boolean
|
||||
onChange: (value: boolean) => void
|
||||
}
|
||||
|
||||
const FormInputBoolean: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex w-full space-x-1'>
|
||||
<div
|
||||
className={cn(
|
||||
'system-sm-regular flex h-8 grow cursor-default items-center justify-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary',
|
||||
!value && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||
value && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
|
||||
)}
|
||||
onClick={() => onChange(true)}
|
||||
>True</div>
|
||||
<div
|
||||
className={cn(
|
||||
'system-sm-regular flex h-8 grow cursor-default items-center justify-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary',
|
||||
value && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||
!value && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
|
||||
)}
|
||||
onClick={() => onChange(false)}
|
||||
>False</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default FormInputBoolean
|
||||
@ -0,0 +1,280 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
|
||||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import FormInputTypeSwitch from './form-input-type-switch'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input'
|
||||
import FormInputBoolean from './form-input-boolean'
|
||||
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
schema: CredentialFormSchema
|
||||
value: ToolVarInputs
|
||||
onChange: (value: any) => void
|
||||
inPanel?: boolean
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
}
|
||||
|
||||
const FormInputItem: FC<Props> = ({
|
||||
readOnly,
|
||||
nodeId,
|
||||
schema,
|
||||
value,
|
||||
onChange,
|
||||
inPanel,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
|
||||
const {
|
||||
placeholder,
|
||||
variable,
|
||||
type,
|
||||
default: defaultValue,
|
||||
options,
|
||||
scope,
|
||||
} = schema as any
|
||||
const varInput = value[variable]
|
||||
const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
|
||||
const isNumber = type === FormTypeEnum.textNumber
|
||||
const isObject = type === FormTypeEnum.object
|
||||
const isArray = type === FormTypeEnum.array
|
||||
const isShowJSONEditor = isObject || isArray
|
||||
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
|
||||
const isBoolean = type === FormTypeEnum.boolean
|
||||
const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
|
||||
const isAppSelector = type === FormTypeEnum.appSelector
|
||||
const isModelSelector = type === FormTypeEnum.modelSelector
|
||||
const showTypeSwitch = isNumber || isObject || isArray
|
||||
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
|
||||
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
|
||||
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
const targetVarType = () => {
|
||||
if (isString)
|
||||
return VarType.string
|
||||
else if (isNumber)
|
||||
return VarType.number
|
||||
else if (type === FormTypeEnum.files)
|
||||
return VarType.arrayFile
|
||||
else if (type === FormTypeEnum.file)
|
||||
return VarType.file
|
||||
// else if (isSelect)
|
||||
// return VarType.select
|
||||
// else if (isAppSelector)
|
||||
// return VarType.appSelector
|
||||
// else if (isModelSelector)
|
||||
// return VarType.modelSelector
|
||||
// else if (isBoolean)
|
||||
// return VarType.boolean
|
||||
else if (isObject)
|
||||
return VarType.object
|
||||
else if (isArray)
|
||||
return VarType.arrayObject
|
||||
else
|
||||
return VarType.string
|
||||
}
|
||||
|
||||
const getFilterVar = () => {
|
||||
if (isNumber)
|
||||
return (varPayload: any) => varPayload.type === VarType.number
|
||||
else if (isString)
|
||||
return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
else if (isFile)
|
||||
return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||
else if (isBoolean)
|
||||
return (varPayload: any) => varPayload.type === VarType.boolean
|
||||
else if (isObject)
|
||||
return (varPayload: any) => varPayload.type === VarType.object
|
||||
else if (isArray)
|
||||
return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getVarKindType = () => {
|
||||
if (isFile)
|
||||
return VarKindType.variable
|
||||
if (isSelect || isBoolean || isNumber || isArray || isObject)
|
||||
return VarKindType.constant
|
||||
if (isString)
|
||||
return VarKindType.mixed
|
||||
}
|
||||
|
||||
const handleTypeChange = (newType: string) => {
|
||||
if (newType === VarKindType.variable) {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: VarKindType.variable,
|
||||
value: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: VarKindType.constant,
|
||||
value: defaultValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleValueChange = (newValue: any) => {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: getVarKindType(),
|
||||
value: isNumber ? Number.parseFloat(newValue) : newValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleAppOrModelSelect = (newValue: any) => {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
...newValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleVariableSelectorChange = (newValue: ValueSelector | string, variable: string) => {
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: VarKindType.variable,
|
||||
value: newValue || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}>
|
||||
{showTypeSwitch && (
|
||||
<FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange}/>
|
||||
)}
|
||||
{isString && (
|
||||
<MixedVariableTextInput
|
||||
readOnly={readOnly}
|
||||
value={varInput?.value as string || ''}
|
||||
onChange={handleValueChange}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
/>
|
||||
)}
|
||||
{isNumber && isConstant && (
|
||||
<Input
|
||||
className='h-8 grow'
|
||||
type='number'
|
||||
value={varInput?.value || ''}
|
||||
onChange={e => handleValueChange(e.target.value)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
)}
|
||||
{isBoolean && (
|
||||
<FormInputBoolean
|
||||
value={varInput?.value as boolean}
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
)}
|
||||
{isSelect && (
|
||||
<SimpleSelect
|
||||
wrapperClassName='h-8 grow'
|
||||
disabled={readOnly}
|
||||
defaultValue={varInput?.value}
|
||||
items={options.filter((option: { show_on: any[] }) => {
|
||||
if (option.show_on.length)
|
||||
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
|
||||
|
||||
return true
|
||||
}).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
|
||||
onSelect={item => handleValueChange(item.value as string)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
)}
|
||||
{isShowJSONEditor && isConstant && (
|
||||
<div className='mt-1 w-full'>
|
||||
<CodeEditor
|
||||
title='JSON'
|
||||
value={varInput?.value as any}
|
||||
isExpand
|
||||
isInNode
|
||||
height={100}
|
||||
language={CodeLanguage.json}
|
||||
onChange={handleValueChange}
|
||||
className='w-full'
|
||||
placeholder={<div className='whitespace-pre'>{placeholder?.[language] || placeholder?.en_US}</div>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isAppSelector && (
|
||||
<AppSelector
|
||||
disabled={readOnly}
|
||||
scope={scope || 'all'}
|
||||
value={varInput?.value as any}
|
||||
onSelect={handleAppOrModelSelect}
|
||||
/>
|
||||
)}
|
||||
{isModelSelector && isConstant && (
|
||||
<ModelParameterModal
|
||||
popupClassName='!w-[387px]'
|
||||
isAdvancedMode
|
||||
isInWorkflow
|
||||
value={varInput?.value as any}
|
||||
setModel={handleAppOrModelSelect}
|
||||
readonly={readOnly}
|
||||
scope={scope}
|
||||
/>
|
||||
)}
|
||||
{showVariableSelector && (
|
||||
<VarReferencePicker
|
||||
zIndex={inPanel ? 1000 : undefined}
|
||||
className='h-8 grow'
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.value || []}
|
||||
onChange={value => handleVariableSelectorChange(value, variable)}
|
||||
filterVar={getFilterVar()}
|
||||
schema={schema}
|
||||
valueTypePlaceHolder={targetVarType()}
|
||||
currentTool={currentTool}
|
||||
currentProvider={currentProvider}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default FormInputItem
|
||||
@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiEditLine,
|
||||
} from '@remixicon/react'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { VarType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
value: VarType
|
||||
onChange: (value: VarType) => void
|
||||
}
|
||||
|
||||
const FormInputTypeSwitch: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='inline-flex h-8 shrink-0 gap-px rounded-[10px] bg-components-segmented-control-bg-normal p-0.5'>
|
||||
<Tooltip
|
||||
popupContent={value === VarType.variable ? '' : t('workflow.nodes.common.typeSwitch.variable')}
|
||||
>
|
||||
<div
|
||||
className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.variable && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')}
|
||||
onClick={() => onChange(VarType.variable)}
|
||||
>
|
||||
<Variable02 className='h-4 w-4' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent={value === VarType.constant ? '' : t('workflow.nodes.common.typeSwitch.input')}
|
||||
>
|
||||
<div
|
||||
className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.constant && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')}
|
||||
onClick={() => onChange(VarType.constant)}
|
||||
>
|
||||
<RiEditLine className='h-4 w-4' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default FormInputTypeSwitch
|
||||
@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { RiAlertFill } from '@remixicon/react'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const McpToolNotSupportTooltip: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[256px]'>
|
||||
{t('plugin.detailPanel.toolSelector.unsupportedMCPTool')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<RiAlertFill className='size-4 text-text-warning-secondary' />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
export default React.memo(McpToolNotSupportTooltip)
|
||||
@ -526,6 +526,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
onChange={handleVarReferenceChange}
|
||||
itemWidth={isAddBtnTrigger ? 260 : (minWidth || triggerWidth)}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
zIndex={zIndex}
|
||||
/>
|
||||
)}
|
||||
</PortalToFollowElemContent>
|
||||
|
||||
@ -13,6 +13,7 @@ type Props = {
|
||||
onChange: (value: ValueSelector, varDetail: Var) => void
|
||||
itemWidth?: number
|
||||
isSupportFileVar?: boolean
|
||||
zIndex?: number
|
||||
}
|
||||
const VarReferencePopup: FC<Props> = ({
|
||||
vars,
|
||||
@ -20,6 +21,7 @@ const VarReferencePopup: FC<Props> = ({
|
||||
onChange,
|
||||
itemWidth,
|
||||
isSupportFileVar = true,
|
||||
zIndex,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
@ -60,6 +62,7 @@ const VarReferencePopup: FC<Props> = ({
|
||||
onChange={onChange}
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
zIndex={zIndex}
|
||||
/>
|
||||
}
|
||||
</div >
|
||||
|
||||
@ -46,6 +46,7 @@ type ItemProps = {
|
||||
isSupportFileVar?: boolean
|
||||
isException?: boolean
|
||||
isLoopVar?: boolean
|
||||
zIndex?: number
|
||||
}
|
||||
|
||||
const objVarTypes = [VarType.object, VarType.file]
|
||||
@ -60,6 +61,7 @@ const Item: FC<ItemProps> = ({
|
||||
isSupportFileVar,
|
||||
isException,
|
||||
isLoopVar,
|
||||
zIndex,
|
||||
}) => {
|
||||
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
|
||||
const isFile = itemData.type === VarType.file && !isStructureOutput
|
||||
@ -171,7 +173,7 @@ const Item: FC<ItemProps> = ({
|
||||
</div >
|
||||
</PortalToFollowElemTrigger >
|
||||
<PortalToFollowElemContent style={{
|
||||
zIndex: 100,
|
||||
zIndex: zIndex || 100,
|
||||
}}>
|
||||
{(isStructureOutput || isObj) && (
|
||||
<PickerStructurePanel
|
||||
@ -260,6 +262,7 @@ type Props = {
|
||||
maxHeightClass?: string
|
||||
onClose?: () => void
|
||||
onBlur?: () => void
|
||||
zIndex?: number
|
||||
autoFocus?: boolean
|
||||
}
|
||||
const VarReferenceVars: FC<Props> = ({
|
||||
@ -272,6 +275,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
maxHeightClass,
|
||||
onClose,
|
||||
onBlur,
|
||||
zIndex,
|
||||
autoFocus = true,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@ -357,6 +361,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isException={v.isException}
|
||||
isLoopVar={item.isLoop}
|
||||
zIndex={zIndex}
|
||||
/>
|
||||
))}
|
||||
</div>))
|
||||
|
||||
@ -32,6 +32,7 @@ import {
|
||||
import { useNodeIterationInteractions } from '../iteration/use-interactions'
|
||||
import { useNodeLoopInteractions } from '../loop/use-interactions'
|
||||
import type { IterationNodeType } from '../iteration/types'
|
||||
import CopyID from '../tool/components/copy-id'
|
||||
import {
|
||||
NodeSourceHandle,
|
||||
NodeTargetHandle,
|
||||
@ -319,6 +320,11 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{data.type === BlockEnum.Tool && (
|
||||
<div className='px-3 pb-2'>
|
||||
<CopyID content={data.provider_id || ''} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -2,10 +2,11 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { memo, useMemo, useRef, useState } from 'react'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
|
||||
import { getIconFromMarketPlace } from '@/utils/get-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
||||
type Status = 'not-installed' | 'not-authorized' | undefined
|
||||
|
||||
@ -19,19 +20,21 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const isDataReady = !!buildInTools && !!customTools && !!workflowTools
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const isDataReady = !!buildInTools && !!customTools && !!workflowTools && !!mcpTools
|
||||
const currentProvider = useMemo(() => {
|
||||
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
|
||||
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])]
|
||||
return mergedTools.find((toolWithProvider) => {
|
||||
return toolWithProvider.name === providerName
|
||||
return toolWithProvider.name === providerName || toolWithProvider.id === providerName
|
||||
})
|
||||
}, [buildInTools, customTools, providerName, workflowTools])
|
||||
}, [buildInTools, customTools, providerName, workflowTools, mcpTools])
|
||||
|
||||
const providerNameParts = providerName.split('/')
|
||||
const author = providerNameParts[0]
|
||||
const name = providerNameParts[1]
|
||||
const icon = useMemo(() => {
|
||||
if (!isDataReady) return ''
|
||||
if (currentProvider) return currentProvider.icon as string
|
||||
if (currentProvider) return currentProvider.icon
|
||||
const iconFromMarketPlace = getIconFromMarketPlace(`${author}/${name}`)
|
||||
return iconFromMarketPlace
|
||||
}, [author, currentProvider, name, isDataReady])
|
||||
@ -62,19 +65,32 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
|
||||
)}
|
||||
ref={containerRef}
|
||||
>
|
||||
{(!iconFetchError && isDataReady)
|
||||
|
||||
? <img
|
||||
src={icon}
|
||||
alt='tool icon'
|
||||
className={classNames(
|
||||
'w-full h-full size-3.5 object-cover',
|
||||
notSuccess && 'opacity-50',
|
||||
)}
|
||||
onError={() => setIconFetchError(true)}
|
||||
/>
|
||||
: <Group className="h-3 w-3 opacity-35" />
|
||||
}
|
||||
{(() => {
|
||||
if (iconFetchError || !icon)
|
||||
return <Group className="h-3 w-3 opacity-35" />
|
||||
if (typeof icon === 'string') {
|
||||
return <img
|
||||
src={icon}
|
||||
alt='tool icon'
|
||||
className={classNames(
|
||||
'w-full h-full size-3.5 object-cover',
|
||||
notSuccess && 'opacity-50',
|
||||
)}
|
||||
onError={() => setIconFetchError(true)}
|
||||
/>
|
||||
}
|
||||
if (typeof icon === 'object') {
|
||||
return <AppIcon
|
||||
className={classNames(
|
||||
'w-full h-full size-3.5 object-cover',
|
||||
notSuccess && 'opacity-50',
|
||||
)}
|
||||
icon={icon?.content}
|
||||
background={icon?.background}
|
||||
/>
|
||||
}
|
||||
return <Group className="h-3 w-3 opacity-35" />
|
||||
})()}
|
||||
{indicator && <Indicator color={indicator} className="absolute right-[-1px] top-[-1px]" />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@ -7,6 +7,7 @@ import { renderI18nObject } from '@/i18n'
|
||||
|
||||
const nodeDefault: NodeDefault<AgentNodeType> = {
|
||||
defaultValue: {
|
||||
version: '2',
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode) {
|
||||
return isChatMode
|
||||
@ -60,15 +61,28 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
|
||||
const schemas = toolValue.schemas || []
|
||||
const userSettings = toolValue.settings
|
||||
const reasoningConfig = toolValue.parameters
|
||||
const version = payload.version
|
||||
schemas.forEach((schema: any) => {
|
||||
if (schema?.required) {
|
||||
if (schema.form === 'form' && !userSettings[schema.name]?.value) {
|
||||
if (schema.form === 'form' && !version && !userSettings[schema.name]?.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
|
||||
}
|
||||
}
|
||||
if (schema.form === 'llm' && reasoningConfig[schema.name].auto === 0 && !userSettings[schema.name]?.value) {
|
||||
if (schema.form === 'form' && version && !userSettings[schema.name]?.value.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
|
||||
}
|
||||
}
|
||||
if (schema.form === 'llm' && !version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
|
||||
}
|
||||
}
|
||||
if (schema.form === 'llm' && version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
|
||||
|
||||
@ -104,7 +104,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
|
||||
{t('workflow.nodes.agent.toolbox')}
|
||||
</GroupLabel>}>
|
||||
<div className='grid grid-cols-10 gap-0.5'>
|
||||
{tools.map(tool => <ToolIcon {...tool} key={tool.id} />)}
|
||||
{tools.map((tool, i) => <ToolIcon {...tool} key={tool.id + i} />)}
|
||||
</div>
|
||||
</Group>}
|
||||
</div>
|
||||
|
||||
@ -38,11 +38,11 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
|
||||
readOnly,
|
||||
outputSchema,
|
||||
handleMemoryChange,
|
||||
canChooseMCPTool,
|
||||
} = useConfig(props.id, props.data)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey)
|
||||
|
||||
return <div className='my-2'>
|
||||
<Field
|
||||
required
|
||||
@ -56,6 +56,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
|
||||
agent_strategy_label: inputs.agent_strategy_label!,
|
||||
agent_output_schema: inputs.output_schema,
|
||||
plugin_unique_identifier: inputs.plugin_unique_identifier!,
|
||||
meta: inputs.meta,
|
||||
} : undefined}
|
||||
onStrategyChange={(strategy) => {
|
||||
setInputs({
|
||||
@ -65,6 +66,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
|
||||
agent_strategy_label: strategy?.agent_strategy_label,
|
||||
output_schema: strategy!.agent_output_schema,
|
||||
plugin_unique_identifier: strategy!.plugin_unique_identifier,
|
||||
meta: strategy?.meta,
|
||||
})
|
||||
resetEditor(Date.now())
|
||||
}}
|
||||
@ -74,6 +76,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
|
||||
nodeOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
nodeId={props.id}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</Field>
|
||||
<div className='px-4 py-2'>
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import type { CommonNodeType, Memory } from '@/app/components/workflow/types'
|
||||
import type { ToolVarInputs } from '../tool/types'
|
||||
import type { PluginMeta } from '@/app/components/plugins/types'
|
||||
|
||||
export type AgentNodeType = CommonNodeType & {
|
||||
agent_strategy_provider_name?: string
|
||||
agent_strategy_name?: string
|
||||
agent_strategy_label?: string
|
||||
agent_parameters?: ToolVarInputs
|
||||
meta?: PluginMeta
|
||||
output_schema: Record<string, any>
|
||||
plugin_unique_identifier?: string
|
||||
memory?: Memory
|
||||
version?: string
|
||||
}
|
||||
|
||||
export enum AgentFeature {
|
||||
|
||||
@ -6,13 +6,16 @@ import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { type ToolVarInputs, VarType } from '../tool/types'
|
||||
import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins'
|
||||
import type { Memory, Var } from '../../types'
|
||||
import { VarType as VarKindType } from '../../types'
|
||||
import useAvailableVarList from '../_base/hooks/use-available-var-list'
|
||||
import produce from 'immer'
|
||||
import { isSupportMCP } from '@/utils/plugin-version-feature'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { generateAgentToolValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
|
||||
export type StrategyStatus = {
|
||||
plugin: {
|
||||
@ -85,11 +88,12 @@ const useConfig = (id: string, payload: AgentNodeType) => {
|
||||
})
|
||||
const formData = useMemo(() => {
|
||||
const paramNameList = (currentStrategy?.parameters || []).map(item => item.name)
|
||||
return Object.fromEntries(
|
||||
const res = Object.fromEntries(
|
||||
Object.entries(inputs.agent_parameters || {}).filter(([name]) => paramNameList.includes(name)).map(([key, value]) => {
|
||||
return [key, value.value]
|
||||
}),
|
||||
)
|
||||
return res
|
||||
}, [inputs.agent_parameters, currentStrategy?.parameters])
|
||||
const onFormChange = (value: Record<string, any>) => {
|
||||
const res: ToolVarInputs = {}
|
||||
@ -105,6 +109,42 @@ const useConfig = (id: string, payload: AgentNodeType) => {
|
||||
})
|
||||
}
|
||||
|
||||
const formattingToolData = (data: any) => {
|
||||
const settingValues = generateAgentToolValue(data.settings, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form !== 'llm') as any))
|
||||
const paramValues = generateAgentToolValue(data.parameters, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form === 'llm') as any), true)
|
||||
const res = produce(data, (draft: any) => {
|
||||
draft.settings = settingValues
|
||||
draft.parameters = paramValues
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const formattingLegacyData = () => {
|
||||
if (inputs.version)
|
||||
return inputs
|
||||
const newData = produce(inputs, (draft) => {
|
||||
const schemas = currentStrategy?.parameters || []
|
||||
Object.keys(draft.agent_parameters || {}).forEach((key) => {
|
||||
const targetSchema = schemas.find(schema => schema.name === key)
|
||||
if (targetSchema?.type === FormTypeEnum.toolSelector)
|
||||
draft.agent_parameters![key].value = formattingToolData(draft.agent_parameters![key].value)
|
||||
if (targetSchema?.type === FormTypeEnum.multiToolSelector)
|
||||
draft.agent_parameters![key].value = draft.agent_parameters![key].value.map((tool: any) => formattingToolData(tool))
|
||||
})
|
||||
draft.version = '2'
|
||||
})
|
||||
return newData
|
||||
}
|
||||
|
||||
// formatting legacy data
|
||||
useEffect(() => {
|
||||
if (!currentStrategy)
|
||||
return
|
||||
const newData = formattingLegacyData()
|
||||
setInputs(newData)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentStrategy])
|
||||
|
||||
// vars
|
||||
|
||||
const filterMemoryPromptVar = useCallback((varPayload: Var) => {
|
||||
@ -172,6 +212,7 @@ const useConfig = (id: string, payload: AgentNodeType) => {
|
||||
outputSchema,
|
||||
handleMemoryChange,
|
||||
isChatMode,
|
||||
canChooseMCPTool: isSupportMCP(inputs.meta?.version),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -5,21 +5,24 @@ import { useSchemaNodeOperations } from './hooks'
|
||||
|
||||
export type VisualEditorProps = {
|
||||
schema: SchemaRoot
|
||||
onChange: (schema: SchemaRoot) => void
|
||||
rootName?: string
|
||||
readOnly?: boolean
|
||||
onChange?: (schema: SchemaRoot) => void
|
||||
}
|
||||
|
||||
const VisualEditor: FC<VisualEditorProps> = (props) => {
|
||||
const { schema } = props
|
||||
const { schema, readOnly } = props
|
||||
useSchemaNodeOperations(props)
|
||||
|
||||
return (
|
||||
<div className='h-full overflow-auto rounded-xl bg-background-section-burn p-1 pl-2'>
|
||||
<SchemaNode
|
||||
name='structured_output'
|
||||
name={props.rootName || 'structured_output'}
|
||||
schema={schema}
|
||||
required={false}
|
||||
path={[]}
|
||||
depth={0}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -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 />
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiFileCopyLine } from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { debounce } from 'lodash-es'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type Props = {
|
||||
content: string
|
||||
}
|
||||
|
||||
const prefixEmbedded = 'appOverview.overview.appInfo.embedded'
|
||||
|
||||
const CopyFeedbackNew = ({ content }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
||||
|
||||
const onClickCopy = debounce(() => {
|
||||
copy(content)
|
||||
setIsCopied(true)
|
||||
}, 100)
|
||||
|
||||
const onMouseLeave = debounce(() => {
|
||||
setIsCopied(false)
|
||||
}, 100)
|
||||
|
||||
return (
|
||||
<div className='group flex items-center gap-0.5 pb-0.5' onClick={e => e.stopPropagation()}>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
(isCopied
|
||||
? t(`${prefixEmbedded}.copied`)
|
||||
: t(`${prefixEmbedded}.copy`)) || ''
|
||||
}
|
||||
>
|
||||
<div
|
||||
className='system-2xs-regular cursor-pointer text-text-quaternary group-hover:text-text-tertiary'
|
||||
onClick={onClickCopy}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>{content}</div>
|
||||
</Tooltip>
|
||||
<RiFileCopyLine className='hidden h-3 w-3 text-text-tertiary group-hover:block' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyFeedbackNew
|
||||
@ -0,0 +1,62 @@
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import Placeholder from './placeholder'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type MixedVariableTextInputProps = {
|
||||
readOnly?: boolean
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
value?: string
|
||||
onChange?: (text: string) => void
|
||||
}
|
||||
const MixedVariableTextInput = ({
|
||||
readOnly = false,
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
value = '',
|
||||
onChange,
|
||||
}: MixedVariableTextInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<PromptEditor
|
||||
wrapperClassName={cn(
|
||||
'w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1',
|
||||
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
||||
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
|
||||
)}
|
||||
className='caret:text-text-accent'
|
||||
editable={!readOnly}
|
||||
value={value}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
}}
|
||||
placeholder={<Placeholder />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(MixedVariableTextInput)
|
||||
@ -0,0 +1,51 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { FOCUS_COMMAND } from 'lexical'
|
||||
import { $insertNodes } from 'lexical'
|
||||
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
const Placeholder = () => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const handleInsert = useCallback((text: string) => {
|
||||
editor.update(() => {
|
||||
const textNode = new CustomTextNode(text)
|
||||
$insertNodes([textNode])
|
||||
})
|
||||
editor.dispatchCommand(FOCUS_COMMAND, undefined as any)
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='pointer-events-auto flex h-full w-full cursor-text items-center px-2'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleInsert('')
|
||||
}}
|
||||
>
|
||||
<div className='flex grow items-center'>
|
||||
{t('workflow.nodes.tool.insertPlaceholder1')}
|
||||
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
|
||||
<div
|
||||
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
|
||||
onClick={((e) => {
|
||||
e.stopPropagation()
|
||||
handleInsert('/')
|
||||
})}
|
||||
>
|
||||
{t('workflow.nodes.tool.insertPlaceholder2')}
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
className='shrink-0'
|
||||
text='String'
|
||||
uppercase={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Placeholder
|
||||
@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ToolVarInputs } from '../../types'
|
||||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import ToolFormItem from './item'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
schema: CredentialFormSchema[]
|
||||
value: ToolVarInputs
|
||||
onChange: (value: ToolVarInputs) => void
|
||||
onOpen?: (index: number) => void
|
||||
inPanel?: boolean
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
}
|
||||
|
||||
const ToolForm: FC<Props> = ({
|
||||
readOnly,
|
||||
nodeId,
|
||||
schema,
|
||||
value,
|
||||
onChange,
|
||||
inPanel,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
}) => {
|
||||
return (
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
schema.map((schema, index) => (
|
||||
<ToolFormItem
|
||||
key={index}
|
||||
readOnly={readOnly}
|
||||
nodeId={nodeId}
|
||||
schema={schema}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
inPanel={inPanel}
|
||||
currentTool={currentTool}
|
||||
currentProvider={currentProvider}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ToolForm
|
||||
@ -0,0 +1,105 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
RiBracesLine,
|
||||
} from '@remixicon/react'
|
||||
import type { ToolVarInputs } from '../../types'
|
||||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import FormInputItem from '@/app/components/workflow/nodes/_base/components/form-input-item'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import SchemaModal from '@/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
schema: CredentialFormSchema
|
||||
value: ToolVarInputs
|
||||
onChange: (value: ToolVarInputs) => void
|
||||
inPanel?: boolean
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
}
|
||||
|
||||
const ToolFormItem: FC<Props> = ({
|
||||
readOnly,
|
||||
nodeId,
|
||||
schema,
|
||||
value,
|
||||
onChange,
|
||||
inPanel,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const { name, label, type, required, tooltip, input_schema } = schema
|
||||
const showSchemaButton = type === FormTypeEnum.object || type === FormTypeEnum.array
|
||||
const showDescription = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
|
||||
const [isShowSchema, {
|
||||
setTrue: showSchema,
|
||||
setFalse: hideSchema,
|
||||
}] = useBoolean(false)
|
||||
return (
|
||||
<div className='space-y-0.5 py-1'>
|
||||
<div>
|
||||
<div className='flex h-6 items-center'>
|
||||
<div className='system-sm-medium text-text-secondary'>{label[language] || label.en_US}</div>
|
||||
{required && (
|
||||
<div className='system-xs-regular ml-1 text-text-destructive-secondary'>*</div>
|
||||
)}
|
||||
{!showDescription && tooltip && (
|
||||
<Tooltip
|
||||
popupContent={<div className='w-[200px]'>
|
||||
{tooltip[language] || tooltip.en_US}
|
||||
</div>}
|
||||
triggerClassName='ml-1 w-4 h-4'
|
||||
asChild={false}
|
||||
/>
|
||||
)}
|
||||
{showSchemaButton && (
|
||||
<>
|
||||
<div className='system-xs-regular ml-1 mr-0.5 text-text-quaternary'>·</div>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='small'
|
||||
onClick={showSchema}
|
||||
className='system-xs-regular px-1 text-text-tertiary'
|
||||
>
|
||||
<RiBracesLine className='mr-1 size-3.5' />
|
||||
<span>JSON Schema</span>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showDescription && tooltip && (
|
||||
<div className='body-xs-regular pb-0.5 text-text-tertiary'>{tooltip[language] || tooltip.en_US}</div>
|
||||
)}
|
||||
</div>
|
||||
<FormInputItem
|
||||
readOnly={readOnly}
|
||||
nodeId={nodeId}
|
||||
schema={schema}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
inPanel={inPanel}
|
||||
currentTool={currentTool}
|
||||
currentProvider={currentProvider}
|
||||
/>
|
||||
|
||||
{isShowSchema && (
|
||||
<SchemaModal
|
||||
isShow
|
||||
onClose={hideSchema}
|
||||
rootName={name}
|
||||
schema={input_schema!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ToolFormItem
|
||||
@ -10,6 +10,7 @@ const nodeDefault: NodeDefault<ToolNodeType> = {
|
||||
defaultValue: {
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
version: '2',
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
@ -55,6 +56,8 @@ const nodeDefault: NodeDefault<ToolNodeType> = {
|
||||
const value = payload.tool_configurations[field.variable]
|
||||
if (!errorMessages && (value === undefined || value === null || value === ''))
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label[language] })
|
||||
if (!errorMessages && typeof value === 'object' && !!value.type && (value.value === undefined || value.value === null || value.value === '' || (Array.isArray(value.value) && value.value.length === 0)))
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label[language] })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -21,14 +21,14 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
|
||||
<div title={key} className='max-w-[100px] shrink-0 truncate text-xs font-medium uppercase text-text-tertiary'>
|
||||
{key}
|
||||
</div>
|
||||
{typeof tool_configurations[key] === 'string' && (
|
||||
{typeof tool_configurations[key].value === 'string' && (
|
||||
<div title={tool_configurations[key]} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
|
||||
{paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key]}
|
||||
{paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value}
|
||||
</div>
|
||||
)}
|
||||
{typeof tool_configurations[key] === 'number' && (
|
||||
{typeof tool_configurations[key].value === 'number' && (
|
||||
<div title={tool_configurations[key].toString()} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
|
||||
{tool_configurations[key]}
|
||||
{tool_configurations[key].value}
|
||||
</div>
|
||||
)}
|
||||
{typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && (
|
||||
@ -36,11 +36,6 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
|
||||
{tool_configurations[key].model}
|
||||
</div>
|
||||
)}
|
||||
{/* {typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.appSelector && (
|
||||
<div title={tool_configurations[key].app_id} className='grow w-0 shrink-0 truncate text-right text-xs font-normal text-gray-700'>
|
||||
{tool_configurations[key].app_id}
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
@ -4,11 +4,10 @@ import { useTranslation } from 'react-i18next'
|
||||
import Split from '../_base/components/split'
|
||||
import type { ToolNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
import InputVarList from './components/input-var-list'
|
||||
import ToolForm from './components/tool-form'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
||||
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
@ -28,8 +27,6 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
|
||||
inputs,
|
||||
toolInputVarSchema,
|
||||
setInputVar,
|
||||
handleOnVarOpen,
|
||||
filterVar,
|
||||
toolSettingSchema,
|
||||
toolSettingValue,
|
||||
setToolSettingValue,
|
||||
@ -45,6 +42,8 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
|
||||
currTool,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const [collapsed, setCollapsed] = React.useState(false)
|
||||
|
||||
if (isLoading) {
|
||||
return <div className='flex h-[200px] items-center justify-center'>
|
||||
<Loading />
|
||||
@ -66,21 +65,19 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isShowAuthBtn && <>
|
||||
<div className='space-y-4 px-4'>
|
||||
{!isShowAuthBtn && (
|
||||
<div className='relative'>
|
||||
{toolInputVarSchema.length > 0 && (
|
||||
<Field
|
||||
className='px-4'
|
||||
title={t(`${i18nPrefix}.inputVars`)}
|
||||
>
|
||||
<InputVarList
|
||||
<ToolForm
|
||||
readOnly={readOnly}
|
||||
nodeId={id}
|
||||
schema={toolInputVarSchema as any}
|
||||
value={inputs.tool_parameters}
|
||||
onChange={setInputVar}
|
||||
filterVar={filterVar}
|
||||
isSupportConstantValue
|
||||
onOpen={handleOnVarOpen}
|
||||
currentProvider={currCollection}
|
||||
currentTool={currTool}
|
||||
/>
|
||||
@ -88,24 +85,29 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
|
||||
)}
|
||||
|
||||
{toolInputVarSchema.length > 0 && toolSettingSchema.length > 0 && (
|
||||
<Split />
|
||||
<Split className='mt-1' />
|
||||
)}
|
||||
|
||||
<Form
|
||||
className='space-y-4'
|
||||
itemClassName='!py-0'
|
||||
fieldLabelClassName='!text-[13px] !font-semibold !text-text-secondary uppercase'
|
||||
value={toolSettingValue}
|
||||
onChange={setToolSettingValue}
|
||||
formSchemas={toolSettingSchema as any}
|
||||
isEditMode={false}
|
||||
showOnVariableMap={{}}
|
||||
validating={false}
|
||||
// inputClassName='!bg-gray-50'
|
||||
readonly={readOnly}
|
||||
/>
|
||||
{toolSettingSchema.length > 0 && (
|
||||
<>
|
||||
<OutputVars
|
||||
title={t(`${i18nPrefix}.settings`)}
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
>
|
||||
<ToolForm
|
||||
readOnly={readOnly}
|
||||
nodeId={id}
|
||||
schema={toolSettingSchema as any}
|
||||
value={toolSettingValue}
|
||||
onChange={setToolSettingValue}
|
||||
/>
|
||||
</OutputVars>
|
||||
<Split />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>}
|
||||
)}
|
||||
|
||||
{showSetAuth && (
|
||||
<ConfigCredential
|
||||
|
||||
@ -22,4 +22,5 @@ export type ToolNodeType = CommonNodeType & {
|
||||
tool_configurations: Record<string, any>
|
||||
output_schema: Record<string, any>
|
||||
paramSchemas?: Record<string, any>[]
|
||||
version?: string
|
||||
}
|
||||
|
||||
@ -8,9 +8,11 @@ import { useLanguage } from '@/app/components/header/account-setting/model-provi
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { updateBuiltInToolCredential } from '@/service/tools'
|
||||
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import {
|
||||
getConfiguredValue,
|
||||
toolParametersToFormSchemas,
|
||||
} from '@/app/components/tools/utils/to-form-schema'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { VarType as VarVarType } from '@/app/components/workflow/types'
|
||||
import type { InputVar, Var } from '@/app/components/workflow/types'
|
||||
import {
|
||||
useFetchToolsData,
|
||||
@ -26,17 +28,18 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
const language = useLanguage()
|
||||
const { inputs, setInputs: doSetInputs } = useNodeCrud<ToolNodeType>(id, payload)
|
||||
/*
|
||||
* tool_configurations: tool setting, not dynamic setting
|
||||
* tool_parameters: tool dynamic setting(by user)
|
||||
* tool_configurations: tool setting, not dynamic setting (form type = form)
|
||||
* tool_parameters: tool dynamic setting(form type = llm)
|
||||
* output_schema: tool dynamic output
|
||||
*/
|
||||
const { provider_id, provider_type, tool_name, tool_configurations, output_schema } = inputs
|
||||
const { provider_id, provider_type, tool_name, tool_configurations, output_schema, tool_parameters } = inputs
|
||||
const isBuiltIn = provider_type === CollectionType.builtIn
|
||||
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 = (() => {
|
||||
const currentTools = useMemo(() => {
|
||||
switch (provider_type) {
|
||||
case CollectionType.builtIn:
|
||||
return buildInTools
|
||||
@ -44,10 +47,12 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
return customTools
|
||||
case CollectionType.workflow:
|
||||
return workflowTools
|
||||
case CollectionType.mcp:
|
||||
return mcpTools
|
||||
default:
|
||||
return []
|
||||
}
|
||||
})()
|
||||
}, [buildInTools, customTools, mcpTools, provider_type, workflowTools])
|
||||
const currCollection = currentTools.find(item => canFindTool(item.id, provider_id))
|
||||
|
||||
// Auth
|
||||
@ -91,10 +96,10 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
const value = newConfig[key]
|
||||
if (schema?.type === 'boolean') {
|
||||
if (typeof value === 'string')
|
||||
newConfig[key] = Number.parseInt(value, 10)
|
||||
newConfig[key] = value === 'true' || value === '1'
|
||||
|
||||
if (typeof value === 'boolean')
|
||||
newConfig[key] = value ? 1 : 0
|
||||
if (typeof value === 'number')
|
||||
newConfig[key] = value === 1
|
||||
}
|
||||
|
||||
if (schema?.type === 'number-input') {
|
||||
@ -107,12 +112,11 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
doSetInputs(newInputs)
|
||||
}, [doSetInputs, formSchemas, hasShouldTransferTypeSettingInput])
|
||||
const [notSetDefaultValue, setNotSetDefaultValue] = useState(false)
|
||||
const toolSettingValue = (() => {
|
||||
const toolSettingValue = useMemo(() => {
|
||||
if (notSetDefaultValue)
|
||||
return tool_configurations
|
||||
|
||||
return addDefaultValue(tool_configurations, toolSettingSchema)
|
||||
})()
|
||||
return getConfiguredValue(tool_configurations, toolSettingSchema)
|
||||
}, [notSetDefaultValue, toolSettingSchema, tool_configurations])
|
||||
const setToolSettingValue = useCallback((value: Record<string, any>) => {
|
||||
setNotSetDefaultValue(true)
|
||||
setInputs({
|
||||
@ -121,16 +125,20 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
})
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const formattingParameters = () => {
|
||||
const inputsWithDefaultValue = produce(inputs, (draft) => {
|
||||
if (!draft.tool_configurations || Object.keys(draft.tool_configurations).length === 0)
|
||||
draft.tool_configurations = getConfiguredValue(tool_configurations, toolSettingSchema)
|
||||
if (!draft.tool_parameters || Object.keys(draft.tool_parameters).length === 0)
|
||||
draft.tool_parameters = getConfiguredValue(tool_parameters, toolInputVarSchema)
|
||||
})
|
||||
return inputsWithDefaultValue
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!currTool)
|
||||
return
|
||||
const inputsWithDefaultValue = produce(inputs, (draft) => {
|
||||
if (!draft.tool_configurations || Object.keys(draft.tool_configurations).length === 0)
|
||||
draft.tool_configurations = addDefaultValue(tool_configurations, toolSettingSchema)
|
||||
|
||||
if (!draft.tool_parameters)
|
||||
draft.tool_parameters = {}
|
||||
})
|
||||
const inputsWithDefaultValue = formattingParameters()
|
||||
setInputs(inputsWithDefaultValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currTool])
|
||||
@ -143,19 +151,6 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
})
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const [currVarIndex, setCurrVarIndex] = useState(-1)
|
||||
const currVarType = toolInputVarSchema[currVarIndex]?._type
|
||||
const handleOnVarOpen = useCallback((index: number) => {
|
||||
setCurrVarIndex(index)
|
||||
}, [])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
if (currVarType)
|
||||
return varPayload.type === currVarType
|
||||
|
||||
return varPayload.type !== VarVarType.arrayFile
|
||||
}, [currVarType])
|
||||
|
||||
const isLoading = currTool && (isBuiltIn ? !currCollection : false)
|
||||
|
||||
const getMoreDataForCheckValid = () => {
|
||||
@ -220,8 +215,6 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
setToolSettingValue,
|
||||
toolInputVarSchema,
|
||||
setInputVar,
|
||||
handleOnVarOpen,
|
||||
filterVar,
|
||||
currCollection,
|
||||
isShowAuthBtn,
|
||||
showSetAuth,
|
||||
|
||||
@ -34,7 +34,12 @@ const useSingleRunFormParams = ({
|
||||
const hadVarParams = Object.keys(inputs.tool_parameters)
|
||||
.filter(key => inputs.tool_parameters[key].type !== VarType.constant)
|
||||
.map(k => inputs.tool_parameters[k])
|
||||
const varInputs = getInputVars(hadVarParams.map((p) => {
|
||||
|
||||
const hadVarSettings = Object.keys(inputs.tool_configurations)
|
||||
.filter(key => typeof inputs.tool_configurations[key] === 'object' && inputs.tool_configurations[key].type && inputs.tool_configurations[key].type !== VarType.constant)
|
||||
.map(k => inputs.tool_configurations[k])
|
||||
|
||||
const varInputs = getInputVars([...hadVarParams, ...hadVarSettings].map((p) => {
|
||||
if (p.type === VarType.variable) {
|
||||
// handle the old wrong value not crash the page
|
||||
if (!(p.value as any).join)
|
||||
@ -55,8 +60,11 @@ const useSingleRunFormParams = ({
|
||||
const res = produce(inputVarValues, (draft) => {
|
||||
Object.keys(inputs.tool_parameters).forEach((key: string) => {
|
||||
const { type, value } = inputs.tool_parameters[key]
|
||||
if (type === VarType.constant && (value === undefined || value === null))
|
||||
if (type === VarType.constant && (value === undefined || value === null)) {
|
||||
if(!draft.tool_parameters || !draft.tool_parameters[key])
|
||||
return
|
||||
draft[key] = value
|
||||
}
|
||||
})
|
||||
})
|
||||
return res
|
||||
|
||||
@ -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 })),
|
||||
})
|
||||
|
||||
@ -16,6 +16,7 @@ import type {
|
||||
} from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import type { WorkflowRetryConfig } from '@/app/components/workflow/nodes/_base/components/retry/types'
|
||||
import type { StructuredOutput } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { PluginMeta } from '../plugins/types'
|
||||
|
||||
export enum BlockEnum {
|
||||
Start = 'start',
|
||||
@ -408,6 +409,7 @@ export type MoreInfo = {
|
||||
|
||||
export type ToolWithProvider = Collection & {
|
||||
tools: Tool[]
|
||||
meta: PluginMeta
|
||||
}
|
||||
|
||||
export enum SupportUploadFileTypes {
|
||||
|
||||
@ -28,6 +28,7 @@ import type { IfElseNodeType } from '../nodes/if-else/types'
|
||||
import { branchNameCorrect } from '../nodes/if-else/utils'
|
||||
import type { IterationNodeType } from '../nodes/iteration/types'
|
||||
import type { LoopNodeType } from '../nodes/loop/types'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import {
|
||||
getIterationStartNode,
|
||||
getLoopStartNode,
|
||||
@ -276,6 +277,7 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
|
||||
if (node.data.type === BlockEnum.ParameterExtractor)
|
||||
(node as any).data.model.provider = correctModelProvider((node as any).data.model.provider)
|
||||
|
||||
if (node.data.type === BlockEnum.HttpRequest && !node.data.retry_config) {
|
||||
node.data.retry_config = {
|
||||
retry_enabled: true,
|
||||
@ -284,6 +286,24 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (node.data.type === BlockEnum.Tool && !(node as Node<ToolNodeType>).data.version) {
|
||||
(node as Node<ToolNodeType>).data.version = '2'
|
||||
|
||||
const toolConfigurations = (node as Node<ToolNodeType>).data.tool_configurations
|
||||
if (toolConfigurations && Object.keys(toolConfigurations).length > 0) {
|
||||
const newValues = { ...toolConfigurations }
|
||||
Object.keys(toolConfigurations).forEach((key) => {
|
||||
if (typeof toolConfigurations[key] !== 'object' || toolConfigurations[key] === null) {
|
||||
newValues[key] = {
|
||||
type: 'constant',
|
||||
value: toolConfigurations[key],
|
||||
}
|
||||
}
|
||||
});
|
||||
(node as Node<ToolNodeType>).data.tool_configurations = newValues
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
})
|
||||
}
|
||||
|
||||
10
web/app/oauth-callback/page.tsx
Normal file
10
web/app/oauth-callback/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
'use client'
|
||||
import { useOAuthCallback } from '@/hooks/use-oauth'
|
||||
|
||||
const OAuthCallback = () => {
|
||||
useOAuthCallback()
|
||||
|
||||
return <div />
|
||||
}
|
||||
|
||||
export default OAuthCallback
|
||||
Reference in New Issue
Block a user