mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-07-03 17:05:12 +08:00
Merge branch 'jzh' into deploy/dev
This commit is contained in:
commit
b264cc7f35
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppCard from '@/app/components/app/overview/appCard'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
fetchAppDetail,
|
||||
@ -117,6 +118,11 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
isInPanel={isInPanel}
|
||||
onChangeStatus={onChangeApiStatus}
|
||||
/>
|
||||
{isInPanel && appDetail.mode === 'workflow' && (
|
||||
<MCPServiceCard
|
||||
appInfo={appDetail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -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,14 +30,30 @@ import ConfigCredential from '@/app/components/tools/setting/build-in/config-cre
|
||||
import { updateBuiltInToolCredential } from '@/service/tools'
|
||||
import cn from '@/utils/classnames'
|
||||
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
|
||||
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
|
||||
type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } | null
|
||||
const AgentTools: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
|
||||
const { modelConfig, setModelConfig, collectionList } = useContext(ConfigContext)
|
||||
const { modelConfig, setModelConfig } = useContext(ConfigContext)
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const collectionList = useMemo(() => {
|
||||
const allTools = [
|
||||
...(buildInTools || []),
|
||||
...(customTools || []),
|
||||
...(workflowTools || []),
|
||||
...(mcpTools || []),
|
||||
]
|
||||
return allTools
|
||||
}, [buildInTools, customTools, workflowTools, mcpTools])
|
||||
|
||||
const formattingChangedDispatcher = useFormattingChangedDispatcher()
|
||||
|
||||
const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null)
|
||||
@ -85,23 +101,38 @@ const AgentTools: FC = () => {
|
||||
}
|
||||
|
||||
const [isDeleting, setIsDeleting] = useState<number>(-1)
|
||||
|
||||
const getToolValue = (tool: ToolDefaultValue) => {
|
||||
return {
|
||||
provider_id: tool.provider_id,
|
||||
provider_type: tool.provider_type as CollectionType,
|
||||
provider_name: tool.provider_name,
|
||||
tool_name: tool.tool_name,
|
||||
tool_label: tool.tool_label,
|
||||
tool_parameters: tool.params,
|
||||
notAuthor: !tool.is_team_authorization,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
const handleSelectTool = (tool: ToolDefaultValue) => {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
draft.agentConfig.tools.push({
|
||||
provider_id: tool.provider_id,
|
||||
provider_type: tool.provider_type as CollectionType,
|
||||
provider_name: tool.provider_name,
|
||||
tool_name: tool.tool_name,
|
||||
tool_label: tool.tool_label,
|
||||
tool_parameters: tool.params,
|
||||
notAuthor: !tool.is_team_authorization,
|
||||
enabled: true,
|
||||
})
|
||||
draft.agentConfig.tools.push(getToolValue(tool))
|
||||
})
|
||||
setModelConfig(newModelConfig)
|
||||
}
|
||||
|
||||
const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
draft.agentConfig.tools.push(...tool.map(getToolValue))
|
||||
})
|
||||
setModelConfig(newModelConfig)
|
||||
}
|
||||
const getProviderShowName = (item: AgentTool) => {
|
||||
const type = item.provider_type
|
||||
if(type === CollectionType.builtIn)
|
||||
return item.provider_name.split('/').pop()
|
||||
return item.provider_name
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel
|
||||
@ -132,7 +163,8 @@ const AgentTools: FC = () => {
|
||||
disabled={false}
|
||||
supportAddCustomTool
|
||||
onSelect={handleSelectTool}
|
||||
selectedTools={tools}
|
||||
onSelectMultiple={handleSelectMultipleTool}
|
||||
selectedTools={tools as unknown as ToolValue[]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@ -150,7 +182,7 @@ const AgentTools: FC = () => {
|
||||
<div className='flex w-0 grow items-center'>
|
||||
{item.isDeleted && <DefaultToolIcon className='h-5 w-5' />}
|
||||
{!item.isDeleted && (
|
||||
<div className={cn((item.notAuthor || !item.enabled) && 'opacity-50')}>
|
||||
<div className={cn((item.notAuthor || !item.enabled) && 'shrink-0 opacity-50')}>
|
||||
{typeof item.icon === 'string' && <div className='h-5 w-5 rounded-md bg-cover bg-center' style={{ backgroundImage: `url(${item.icon})` }} />}
|
||||
{typeof item.icon !== 'string' && <AppIcon className='rounded-md' size='xs' icon={item.icon?.content} background={item.icon?.background} />}
|
||||
</div>
|
||||
@ -161,7 +193,7 @@ const AgentTools: FC = () => {
|
||||
(item.isDeleted || item.notAuthor || !item.enabled) ? 'opacity-50' : '',
|
||||
)}
|
||||
>
|
||||
<span className='system-xs-medium pr-1.5 text-text-secondary'>{item.provider_type === CollectionType.builtIn ? item.provider_name.split('/').pop() : item.tool_label}</span>
|
||||
<span className='system-xs-medium pr-1.5 text-text-secondary'>{getProviderShowName(item)}</span>
|
||||
<span className='text-text-tertiary'>{item.tool_label}</span>
|
||||
{!item.isDeleted && (
|
||||
<Tooltip
|
||||
@ -274,8 +306,7 @@ const AgentTools: FC = () => {
|
||||
<SettingBuiltInTool
|
||||
toolName={currentTool?.tool_name as string}
|
||||
setting={currentTool?.tool_parameters}
|
||||
collection={currentTool?.collection as Collection}
|
||||
isBuiltIn={currentTool?.collection?.type === CollectionType.builtIn}
|
||||
collection={currentTool?.collection as ToolWithProvider}
|
||||
isModel={currentTool?.collection?.type === CollectionType.model}
|
||||
onSave={handleToolSettingChange}
|
||||
onHide={() => setIsShowSettingTool(false)}
|
||||
|
@ -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 () => {
|
||||
|
@ -178,6 +178,7 @@ function AppCard({
|
||||
icon={appInfo.icon}
|
||||
icon_background={appInfo.icon_background}
|
||||
name={basicName}
|
||||
hideType
|
||||
type={
|
||||
isApp
|
||||
? t('appOverview.overview.appInfo.explanation')
|
||||
|
@ -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'
|
||||
|
@ -19,6 +19,8 @@ export enum FormTypeEnum {
|
||||
toolSelector = 'tool-selector',
|
||||
multiToolSelector = 'array[tools]',
|
||||
appSelector = 'app-selector',
|
||||
object = 'object',
|
||||
array = 'array',
|
||||
}
|
||||
|
||||
export type FormOption = {
|
||||
|
@ -1,8 +1,9 @@
|
||||
'use client'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { RiCloseLine, RiSearchLine } from '@remixicon/react'
|
||||
import TagsFilter from './tags-filter'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
|
||||
type SearchBoxProps = {
|
||||
search: string
|
||||
@ -13,6 +14,9 @@ type SearchBoxProps = {
|
||||
size?: 'small' | 'large'
|
||||
placeholder?: string
|
||||
locale?: string
|
||||
supportAddCustomTool?: boolean
|
||||
onShowAddCustomCollectionModal?: () => void
|
||||
onAddedCustomTool?: () => void
|
||||
}
|
||||
const SearchBox = ({
|
||||
search,
|
||||
@ -23,46 +27,62 @@ const SearchBox = ({
|
||||
size = 'small',
|
||||
placeholder = '',
|
||||
locale,
|
||||
supportAddCustomTool,
|
||||
onShowAddCustomCollectionModal,
|
||||
}: SearchBoxProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'z-[11] flex items-center',
|
||||
size === 'large' && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md',
|
||||
size === 'small' && 'rounded-lg bg-components-input-bg-normal p-0.5',
|
||||
inputClassName,
|
||||
)}
|
||||
className='z-[11] flex items-center'
|
||||
>
|
||||
<TagsFilter
|
||||
tags={tags}
|
||||
onTagsChange={onTagsChange}
|
||||
size={size}
|
||||
locale={locale}
|
||||
/>
|
||||
<div className='mx-1 h-3.5 w-[1px] bg-divider-regular'></div>
|
||||
<div className='relative flex grow items-center p-1 pl-2'>
|
||||
<div className='mr-2 flex w-full items-center'>
|
||||
<input
|
||||
className={cn(
|
||||
'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none',
|
||||
)}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
onSearchChange(e.target.value)
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{
|
||||
search && (
|
||||
<div className='absolute right-2 top-1/2 -translate-y-1/2'>
|
||||
<ActionButton onClick={() => onSearchChange('')}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={
|
||||
cn('flex grow items-center',
|
||||
size === 'large' && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md',
|
||||
size === 'small' && 'rounded-lg bg-components-input-bg-normal p-0.5',
|
||||
inputClassName,
|
||||
)
|
||||
}>
|
||||
<div className='relative flex grow items-center p-1 pl-2'>
|
||||
<div className='mr-2 flex w-full items-center'>
|
||||
<RiSearchLine className='mr-1.5 size-4 text-text-placeholder' />
|
||||
<input
|
||||
className={cn(
|
||||
'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none',
|
||||
)}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
onSearchChange(e.target.value)
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{
|
||||
search && (
|
||||
<div className='absolute right-2 top-1/2 -translate-y-1/2'>
|
||||
<ActionButton onClick={() => onSearchChange('')}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-1 h-3.5 w-[1px] bg-divider-regular'></div>
|
||||
<TagsFilter
|
||||
tags={tags}
|
||||
onTagsChange={onTagsChange}
|
||||
size={size}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
{supportAddCustomTool && (
|
||||
<div className='flex shrink-0 items-center'>
|
||||
<ActionButton
|
||||
className='ml-2 rounded-full bg-components-button-primary-bg text-components-button-primary-text hover:bg-components-button-primary-bg hover:text-components-button-primary-text'
|
||||
onClick={onShowAddCustomCollectionModal}
|
||||
>
|
||||
<RiAddLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -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]'>
|
||||
|
@ -66,6 +66,19 @@ const MultipleToolSelector = ({
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleAddMultiple = (val: ToolValue[]) => {
|
||||
const newValue = [...value, ...val]
|
||||
// deduplication
|
||||
const deduplication = newValue.reduce((acc, cur) => {
|
||||
if (!acc.find(item => item.provider_name === cur.provider_name && item.tool_name === cur.tool_name))
|
||||
acc.push(cur)
|
||||
return acc
|
||||
}, [] as ToolValue[])
|
||||
// update value
|
||||
onChange(deduplication)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
// delete tool
|
||||
const handleDelete = (index: number) => {
|
||||
const newValue = [...value]
|
||||
@ -134,6 +147,7 @@ const MultipleToolSelector = ({
|
||||
value={undefined}
|
||||
selectedTools={value}
|
||||
onSelect={handleAdd}
|
||||
onSelectMultiple={handleAddMultiple}
|
||||
controlledState={open}
|
||||
onControlledStateChange={setOpen}
|
||||
trigger={
|
||||
@ -156,6 +170,7 @@ const MultipleToolSelector = ({
|
||||
value={item}
|
||||
selectedTools={value}
|
||||
onSelect={item => handleConfigure(item, index)}
|
||||
onSelectMultiple={handleAddMultiple}
|
||||
onDelete={() => handleDelete(index)}
|
||||
supportEnableSwitch
|
||||
isEdit
|
||||
|
@ -30,6 +30,7 @@ import { useAppContext } from '@/context/app-context'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
useInvalidateAllBuiltInTools,
|
||||
useUpdateProviderCredentials,
|
||||
@ -55,14 +56,8 @@ type Props = {
|
||||
value?: ToolValue
|
||||
selectedTools?: ToolValue[]
|
||||
isEdit?: boolean
|
||||
onSelect: (tool: {
|
||||
provider_name: string
|
||||
tool_name: string
|
||||
tool_label: string
|
||||
settings?: Record<string, any>
|
||||
parameters?: Record<string, any>
|
||||
extra?: Record<string, any>
|
||||
}) => void
|
||||
onSelect: (tool: ToolValue) => void
|
||||
onSelectMultiple: (tool: ToolValue[]) => void
|
||||
onDelete?: () => void
|
||||
supportEnableSwitch?: boolean
|
||||
supportAddCustomTool?: boolean
|
||||
@ -83,6 +78,7 @@ const ToolSelector: FC<Props> = ({
|
||||
placement = 'left',
|
||||
offset = 4,
|
||||
onSelect,
|
||||
onSelectMultiple,
|
||||
onDelete,
|
||||
scope,
|
||||
supportEnableSwitch,
|
||||
@ -105,6 +101,7 @@ const ToolSelector: FC<Props> = ({
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools()
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
|
||||
@ -112,18 +109,19 @@ const ToolSelector: FC<Props> = ({
|
||||
const { inMarketPlace, manifest } = usePluginInstalledCheck(value?.provider_name)
|
||||
|
||||
const currentProvider = useMemo(() => {
|
||||
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
|
||||
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])]
|
||||
return mergedTools.find((toolWithProvider) => {
|
||||
return toolWithProvider.id === value?.provider_name
|
||||
})
|
||||
}, [value, buildInTools, customTools, workflowTools])
|
||||
}, [value, buildInTools, customTools, workflowTools, mcpTools])
|
||||
|
||||
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
|
||||
const handleSelectTool = (tool: ToolDefaultValue) => {
|
||||
const getToolValue = (tool: ToolDefaultValue) => {
|
||||
const settingValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any))
|
||||
const paramValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any), true)
|
||||
const toolValue = {
|
||||
return {
|
||||
provider_name: tool.provider_id,
|
||||
provider_show_name: tool.provider_name,
|
||||
type: tool.provider_type,
|
||||
tool_name: tool.tool_name,
|
||||
tool_label: tool.tool_label,
|
||||
@ -136,9 +134,16 @@ const ToolSelector: FC<Props> = ({
|
||||
},
|
||||
schemas: tool.paramSchemas,
|
||||
}
|
||||
}
|
||||
const handleSelectTool = (tool: ToolDefaultValue) => {
|
||||
const toolValue = getToolValue(tool)
|
||||
onSelect(toolValue)
|
||||
// setIsShowChooseTool(false)
|
||||
}
|
||||
const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => {
|
||||
const toolValues = tool.map(item => getToolValue(item))
|
||||
onSelectMultiple(toolValues)
|
||||
}
|
||||
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onSelect({
|
||||
@ -250,7 +255,9 @@ const ToolSelector: FC<Props> = ({
|
||||
<ToolItem
|
||||
open={isShow}
|
||||
icon={currentProvider?.icon || manifestIcon}
|
||||
isMCPTool={currentProvider?.type === CollectionType.mcp}
|
||||
providerName={value.provider_name}
|
||||
providerShowName={value.provider_show_name}
|
||||
toolLabel={value.tool_label || value.tool_name}
|
||||
showSwitch={supportEnableSwitch}
|
||||
switchValue={value.enabled}
|
||||
@ -300,6 +307,7 @@ const ToolSelector: FC<Props> = ({
|
||||
disabled={false}
|
||||
supportAddCustomTool
|
||||
onSelect={handleSelectTool}
|
||||
onSelectMultiple={handleSelectMultipleTool}
|
||||
scope={scope}
|
||||
selectedTools={selectedTools}
|
||||
/>
|
||||
|
@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import {
|
||||
RiArrowRightUpLine,
|
||||
RiBracesLine,
|
||||
} from '@remixicon/react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
@ -22,6 +23,8 @@ import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import SchemaModal from './schema-modal'
|
||||
|
||||
type Props = {
|
||||
value: Record<string, any>
|
||||
@ -133,7 +136,12 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
}
|
||||
}, [onChange, value])
|
||||
|
||||
const renderField = (schema: any) => {
|
||||
const [isShowSchema, {
|
||||
setTrue: showSchema,
|
||||
setFalse: hideSchema,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const renderField = (schema: any, showSchema: () => void) => {
|
||||
const {
|
||||
variable,
|
||||
label,
|
||||
@ -149,26 +157,56 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
popupContent={<div className='w-[200px]'>
|
||||
{tooltip[language] || tooltip.en_US}
|
||||
</div>}
|
||||
triggerClassName='ml-1 w-4 h-4'
|
||||
triggerClassName='ml-0.5 w-4 h-4'
|
||||
asChild={false} />
|
||||
))
|
||||
const varInput = value[variable].value
|
||||
const isNumber = type === FormTypeEnum.textNumber
|
||||
const isSelect = type === FormTypeEnum.select
|
||||
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
|
||||
const isObject = type === FormTypeEnum.object
|
||||
const isArray = type === FormTypeEnum.array
|
||||
const isShowSchemaTooltip = isObject || isArray
|
||||
const isAppSelector = type === FormTypeEnum.appSelector
|
||||
const isModelSelector = type === FormTypeEnum.modelSelector
|
||||
// const isToolSelector = type === FormTypeEnum.toolSelector
|
||||
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector
|
||||
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector && !isObject && !isArray
|
||||
const valueType = (() => {
|
||||
if (isNumber) return VarType.number
|
||||
if (isSelect) return VarType.string
|
||||
if (isFile) return VarType.file
|
||||
if (isObject) return VarType.object
|
||||
if (isArray) return VarType.array
|
||||
|
||||
return VarType.string
|
||||
})()
|
||||
|
||||
return (
|
||||
<div key={variable} className='space-y-1'>
|
||||
<div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<span className={cn('code-sm-semibold text-text-secondary')}>{label[language] || label.en_US}</span>
|
||||
<div className='flex items-center'>
|
||||
<span className={cn('code-sm-semibold max-w-[140px] truncate text-text-secondary')} title={label[language] || label.en_US}>{label[language] || label.en_US}</span>
|
||||
{required && (
|
||||
<span className='ml-1 text-red-500'>*</span>
|
||||
)}
|
||||
{tooltipContent}
|
||||
<span className='system-xs-regular mx-1 text-text-quaternary'>·</span>
|
||||
<span className='system-xs-regular text-text-tertiary'>{valueType}</span>
|
||||
{!isShowSchemaTooltip && (
|
||||
<Tooltip
|
||||
popupContent={<div className='system-xs-medium text-text-secondary'>
|
||||
{t('workflow.nodes.agent.clickToViewParameterSchema')}
|
||||
</div>}
|
||||
asChild={false}>
|
||||
<div
|
||||
className='ml-0.5 cursor-pointer rounded-[4px] p-px text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
|
||||
onClick={showSchema}
|
||||
>
|
||||
<RiBracesLine className='size-3.5'/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className='flex cursor-pointer items-center gap-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter px-2 py-1 hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto)}>
|
||||
<span className='system-xs-medium text-text-secondary'>{t('plugin.detailPanel.toolSelector.auto')}</span>
|
||||
@ -220,7 +258,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
schema={schema}
|
||||
/>
|
||||
)}
|
||||
{isFile && (
|
||||
{(isFile || isObject || isArray) && (
|
||||
<VarReferencePicker
|
||||
zIndex={1001}
|
||||
readonly={false}
|
||||
@ -229,7 +267,15 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
value={varInput?.value || []}
|
||||
onChange={handleFileChange(variable)}
|
||||
defaultVarKindType={VarKindType.variable}
|
||||
filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile}
|
||||
filterVar={(varPayload: Var) => {
|
||||
if(isFile)
|
||||
return varPayload.type === VarType.file || varPayload.type === VarType.arrayFile
|
||||
if(isObject)
|
||||
return varPayload.type === VarType.object
|
||||
if(isArray)
|
||||
return [VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
|
||||
return true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isAppSelector && (
|
||||
@ -267,7 +313,13 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
}
|
||||
return (
|
||||
<div className='space-y-3 px-4 py-2'>
|
||||
{schemas.map(schema => renderField(schema))}
|
||||
{!isShowSchema && schemas.map(schema => renderField(schema, showSchema))}
|
||||
{isShowSchema && (
|
||||
<SchemaModal
|
||||
isShow={isShowSchema}
|
||||
onClose={hideSchema}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,125 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import VisualEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor'
|
||||
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { MittProvider, VisualEditorContextProvider } from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
|
||||
const testSchema: SchemaRoot = {
|
||||
type: Type.object,
|
||||
properties: {
|
||||
after: {
|
||||
type: Type.string,
|
||||
description: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
|
||||
},
|
||||
content_block: {
|
||||
type: Type.object,
|
||||
properties: {
|
||||
block_property: {
|
||||
type: Type.string,
|
||||
description: 'The block property of the block to be added. Possible property are `paragraph`,`heading_1`,`heading_2`,`heading_3`,`callout`,`todo`,`toggle`,`quote`, `bulleted_list_item`, `numbered_list_item`, other properties possible are `file`,`image`,`video` (link required).',
|
||||
},
|
||||
bold: {
|
||||
type: Type.boolean,
|
||||
description: 'Indicates if the text is bold.',
|
||||
},
|
||||
code: {
|
||||
type: Type.boolean,
|
||||
description: 'Indicates if the text is formatted as code.',
|
||||
},
|
||||
color: {
|
||||
type: Type.string,
|
||||
description: 'The color of the text background or text itself.',
|
||||
},
|
||||
content: {
|
||||
anyOf: [
|
||||
{
|
||||
type: Type.string,
|
||||
},
|
||||
{
|
||||
enum: [
|
||||
'null',
|
||||
],
|
||||
nullable: true,
|
||||
},
|
||||
],
|
||||
description: 'The textual content of the rich text object. Required for paragraph, heading_1, heading_2, heading_3, callout, todo, toggle, quote.',
|
||||
},
|
||||
italic: {
|
||||
type: Type.boolean,
|
||||
description: 'Indicates if the text is italic.',
|
||||
},
|
||||
link: {
|
||||
type: Type.string,
|
||||
description: 'The URL of the rich text object or the file to be uploaded or image/video link',
|
||||
},
|
||||
strikethrough: {
|
||||
type: Type.boolean,
|
||||
description: 'Indicates if the text has strikethrough.',
|
||||
},
|
||||
underline: {
|
||||
type: Type.boolean,
|
||||
description: 'Indicates if the text is underlined.',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
description: 'Child content to append to a page.',
|
||||
},
|
||||
parent_block_id: {
|
||||
type: Type.string,
|
||||
description: 'The ID of the page which the children will be added.',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'content_block',
|
||||
'parent_block_id',
|
||||
],
|
||||
additionalProperties: false,
|
||||
}
|
||||
type Props = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const SchemaModal: FC<Props> = ({
|
||||
isShow,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className='max-w-[960px] p-0'
|
||||
wrapperClassName='z-[9999]'
|
||||
>
|
||||
<div className='pb-6'>
|
||||
{/* Header */}
|
||||
<div className='relative flex p-6 pb-3 pr-14'>
|
||||
<div className='title-2xl-semi-bold grow truncate text-text-primary'>
|
||||
{t('workflow.nodes.agent.parameterSchema')}
|
||||
</div>
|
||||
<div className='absolute right-5 top-5 flex h-8 w-8 items-center justify-center p-1.5' onClick={onClose}>
|
||||
<RiCloseLine className='h-[18px] w-[18px] text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className='flex max-h-[700px] overflow-y-auto px-6 py-2'>
|
||||
<MittProvider>
|
||||
<VisualEditorContextProvider>
|
||||
<VisualEditor
|
||||
schema={testSchema}
|
||||
readOnly
|
||||
></VisualEditor>
|
||||
</VisualEditorContextProvider>
|
||||
</MittProvider>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(SchemaModal)
|
@ -21,6 +21,8 @@ import cn from '@/utils/classnames'
|
||||
type Props = {
|
||||
icon?: any
|
||||
providerName?: string
|
||||
isMCPTool?: boolean
|
||||
providerShowName?: string
|
||||
toolLabel?: string
|
||||
showSwitch?: boolean
|
||||
switchValue?: boolean
|
||||
@ -40,6 +42,8 @@ type Props = {
|
||||
const ToolItem = ({
|
||||
open,
|
||||
icon,
|
||||
isMCPTool,
|
||||
providerShowName,
|
||||
providerName,
|
||||
toolLabel,
|
||||
showSwitch,
|
||||
@ -56,7 +60,7 @@ const ToolItem = ({
|
||||
versionMismatch,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const providerNameText = providerName?.split('/').pop()
|
||||
const providerNameText = isMCPTool ? providerShowName : providerName?.split('/').pop()
|
||||
const isTransparent = uninstalled || versionMismatch || isError
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
|
@ -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'>
|
||||
|
74
web/app/components/tools/mcp/create-card.tsx
Normal file
74
web/app/components/tools/mcp/create-card.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
RiAddCircleFill,
|
||||
RiArrowRightUpLine,
|
||||
RiBookOpenLine,
|
||||
} from '@remixicon/react'
|
||||
import MCPModal from './modal'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useCreateMCP } from '@/service/use-tools'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
handleCreate: (provider: ToolWithProvider) => void
|
||||
}
|
||||
|
||||
const NewMCPCard = ({ handleCreate }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const { mutateAsync: createMCP } = useCreateMCP()
|
||||
|
||||
const create = async (info: any) => {
|
||||
const provider = await createMCP(info)
|
||||
handleCreate(provider)
|
||||
}
|
||||
|
||||
const linkUrl = useMemo(() => {
|
||||
// TODO help link
|
||||
if (language.startsWith('zh_'))
|
||||
return 'https://docs.dify.ai/zh-hans/guides/tools/integrate-tool/mcp'
|
||||
return 'https://docs.dify.ai/en/guides/tools/integrate-tool/mcp'
|
||||
}, [language])
|
||||
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCurrentWorkspaceManager && (
|
||||
<div className='col-span-1 flex min-h-[108px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out'>
|
||||
<div className='group grow rounded-t-xl' onClick={() => setShowModal(true)}>
|
||||
<div className='flex shrink-0 items-center p-4 pb-3'>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'>
|
||||
<RiAddCircleFill className='h-4 w-4 text-text-quaternary group-hover:text-text-accent'/>
|
||||
</div>
|
||||
<div className='system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent'>{t('tools.mcp.create.cardTitle')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent'>
|
||||
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
|
||||
<RiBookOpenLine className='h-3 w-3 shrink-0' />
|
||||
<div className='system-xs-regular grow truncate' title={t('tools.mcp.create.cardLink') || ''}>{t('tools.mcp.create.cardLink')}</div>
|
||||
<RiArrowRightUpLine className='h-3 w-3 shrink-0' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showModal && (
|
||||
<MCPModal
|
||||
show={showModal}
|
||||
onConfirm={create}
|
||||
onHide={() => setShowModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default NewMCPCard
|
266
web/app/components/tools/mcp/detail/content.tsx
Normal file
266
web/app/components/tools/mcp/detail/content.tsx
Normal file
@ -0,0 +1,266 @@
|
||||
'use client'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import type { FC } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiLoader2Line,
|
||||
RiLoopLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import type { ToolWithProvider } from '../../../workflow/types'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import MCPModal from '../modal'
|
||||
import OperationDropdown from './operation-dropdown'
|
||||
import ListLoading from './list-loading'
|
||||
import ToolItem from './tool-item'
|
||||
import {
|
||||
useAuthorizeMCP,
|
||||
useDeleteMCP,
|
||||
useInvalidateMCPTools,
|
||||
useMCPTools,
|
||||
useUpdateMCP,
|
||||
useUpdateMCPTools,
|
||||
} from '@/service/use-tools'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
detail: ToolWithProvider
|
||||
onUpdate: (isDelete?: boolean) => void
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const MCPDetailContent: FC<Props> = ({
|
||||
detail,
|
||||
onUpdate,
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const { data, isFetching: isGettingTools } = useMCPTools(detail.is_team_authorization ? detail.id : '')
|
||||
const invalidateMCPTools = useInvalidateMCPTools()
|
||||
const { mutateAsync: updateTools, isPending: isUpdating } = useUpdateMCPTools()
|
||||
const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useAuthorizeMCP()
|
||||
const toolList = data?.tools || []
|
||||
|
||||
const handleUpdateTools = useCallback(async () => {
|
||||
if (!detail)
|
||||
return
|
||||
await updateTools(detail.id)
|
||||
invalidateMCPTools(detail.id)
|
||||
onUpdate()
|
||||
}, [detail, updateTools])
|
||||
|
||||
const { mutate: updateMCP } = useUpdateMCP({
|
||||
onSuccess: onUpdate,
|
||||
})
|
||||
const { mutate: deleteMCP } = useDeleteMCP({
|
||||
onSuccess: onUpdate,
|
||||
})
|
||||
|
||||
const [isShowUpdateModal, {
|
||||
setTrue: showUpdateModal,
|
||||
setFalse: hideUpdateModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [isShowDeleteConfirm, {
|
||||
setTrue: showDeleteConfirm,
|
||||
setFalse: hideDeleteConfirm,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [deleting, {
|
||||
setTrue: showDeleting,
|
||||
setFalse: hideDeleting,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleAuthorize = useCallback(async () => {
|
||||
if (!detail)
|
||||
return
|
||||
const res = await authorizeMcp({
|
||||
provider_id: detail.id,
|
||||
server_url: detail.server_url!,
|
||||
})
|
||||
if (res.result === 'success')
|
||||
handleUpdateTools()
|
||||
|
||||
else if (res.authorization_url)
|
||||
router.push(res.authorization_url)
|
||||
}, [detail, updateMCP, hideUpdateModal, onUpdate])
|
||||
|
||||
const handleUpdate = useCallback(async (data: any) => {
|
||||
if (!detail)
|
||||
return
|
||||
const res = await updateMCP({
|
||||
...data,
|
||||
provider_id: detail.id,
|
||||
})
|
||||
if ((res as any)?.result === 'success') {
|
||||
hideUpdateModal()
|
||||
onUpdate()
|
||||
}
|
||||
}, [detail, updateMCP, hideUpdateModal, onUpdate])
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!detail)
|
||||
return
|
||||
showDeleting()
|
||||
const res = await deleteMCP(detail.id)
|
||||
hideDeleting()
|
||||
if ((res as any)?.result === 'success') {
|
||||
hideDeleteConfirm()
|
||||
onUpdate(true)
|
||||
}
|
||||
}, [detail, showDeleting, hideDeleting, hideDeleteConfirm, onUpdate])
|
||||
|
||||
if (!detail)
|
||||
return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3')}>
|
||||
<div className='flex'>
|
||||
<div className='shrink-0 overflow-hidden rounded-xl border border-components-panel-border-subtle'>
|
||||
<Icon src={detail.icon} />
|
||||
</div>
|
||||
<div className='ml-3 w-0 grow'>
|
||||
<div className='flex h-5 items-center'>
|
||||
<div className='system-md-semibold truncate text-text-primary' title={detail.name}>{detail.name}</div>
|
||||
</div>
|
||||
<div className='system-xs-regular mt-0.5 truncate text-text-tertiary' title={detail.server_url}>{detail.server_url}</div>
|
||||
</div>
|
||||
<div className='flex gap-1'>
|
||||
<OperationDropdown
|
||||
onEdit={showUpdateModal}
|
||||
onRemove={showDeleteConfirm}
|
||||
/>
|
||||
<ActionButton onClick={onHide}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-5'>
|
||||
{detail.is_team_authorization && (
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='w-full'
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className='mr-2' color={'green'} />
|
||||
{t('tools.auth.authorized')}
|
||||
</Button>
|
||||
)}
|
||||
{!detail.is_team_authorization && !isAuthorizing && (
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full'
|
||||
onClick={handleAuthorize}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('tools.mcp.authorize')}
|
||||
</Button>
|
||||
)}
|
||||
{isAuthorizing && (
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full'
|
||||
disabled
|
||||
>
|
||||
<RiLoader2Line className={cn('mr-1 h-4 w-4 animate-spin')} />
|
||||
{t('tools.mcp.authorizing')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='grow'>
|
||||
{((detail.is_team_authorization && isGettingTools) || isUpdating) && (
|
||||
<>
|
||||
<div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'>
|
||||
<div className='flex h-6 items-center'>
|
||||
{!isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.gettingTools')}</div>}
|
||||
{isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.updateTools')}</div>}
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div className='flex h-full w-full grow flex-col overflow-hidden px-4 pb-4'>
|
||||
<ListLoading />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{detail.is_team_authorization && !isGettingTools && !toolList.length && (
|
||||
<div className='flex h-full w-full flex-col items-center justify-center'>
|
||||
<div className='system-sm-regular mb-3 text-text-tertiary'>{t('tools.mcp.toolsEmpty')}</div>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleUpdateTools}
|
||||
>{t('tools.mcp.getTools')}</Button>
|
||||
</div>
|
||||
)}
|
||||
{!isGettingTools && toolList.length > 0 && (
|
||||
<>
|
||||
<div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'>
|
||||
<div className='flex h-6 items-center'>
|
||||
{toolList.length > 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.toolsNum', { count: toolList.length })}</div>}
|
||||
{toolList.length === 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.onlyTool')}</div>}
|
||||
</div>
|
||||
<div>
|
||||
<Button size='small' onClick={handleUpdateTools}>
|
||||
<RiLoopLeftLine className='mr-1 h-3.5 w-3.5' />
|
||||
{t('tools.mcp.update')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex w-full grow flex-col gap-2 overflow-y-auto px-4 pb-4'>
|
||||
{toolList.map(tool => (
|
||||
<ToolItem
|
||||
key={`${detail.id}${tool.name}`}
|
||||
tool={tool}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!detail.is_team_authorization && (
|
||||
<div className='flex h-full w-full flex-col items-center justify-center'>
|
||||
{!isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizingRequired')}</div>}
|
||||
{isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizing')}</div>}
|
||||
<div className='system-sm-regular text-text-tertiary'>{t('tools.mcp.authorizeTip')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isShowUpdateModal && (
|
||||
<MCPModal
|
||||
data={detail}
|
||||
show={isShowUpdateModal}
|
||||
onConfirm={handleUpdate}
|
||||
onHide={hideUpdateModal}
|
||||
/>
|
||||
)}
|
||||
{isShowDeleteConfirm && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('tools.mcp.delete')}
|
||||
content={
|
||||
<div>
|
||||
{t('tools.mcp.deleteConfirmTitle', { mcp: detail.name })}
|
||||
</div>
|
||||
}
|
||||
onCancel={hideDeleteConfirm}
|
||||
onConfirm={handleDelete}
|
||||
isLoading={deleting}
|
||||
isDisabled={deleting}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPDetailContent
|
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)
|
50
web/app/components/tools/mcp/detail/provider-detail.tsx
Normal file
50
web/app/components/tools/mcp/detail/provider-detail.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { FC } from 'react'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import MCPDetailContent from './content'
|
||||
import type { ToolWithProvider } from '../../../workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
detail?: ToolWithProvider
|
||||
onUpdate: () => void
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const MCPDetailPanel: FC<Props> = ({
|
||||
detail,
|
||||
onUpdate,
|
||||
onHide,
|
||||
}) => {
|
||||
const handleUpdate = (isDelete = false) => {
|
||||
if (isDelete)
|
||||
onHide()
|
||||
onUpdate()
|
||||
}
|
||||
|
||||
if (!detail)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={!!detail}
|
||||
clickOutsideNotOpen={false}
|
||||
onClose={onHide}
|
||||
footer={null}
|
||||
mask={false}
|
||||
positionCenter={false}
|
||||
panelClassName={cn('mb-2 mr-2 mt-[64px] !w-[420px] !max-w-[420px] justify-start rounded-2xl border-[0.5px] border-components-panel-border !bg-components-panel-bg !p-0 shadow-xl')}
|
||||
>
|
||||
{detail && (
|
||||
<MCPDetailContent
|
||||
detail={detail}
|
||||
onHide={onHide}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPDetailPanel
|
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 }
|
||||
}
|
132
web/app/components/tools/mcp/index.tsx
Normal file
132
web/app/components/tools/mcp/index.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import NewMCPCard from './create-card'
|
||||
import MCPCard from './provider-card'
|
||||
import MCPDetailPanel from './detail/provider-detail'
|
||||
import {
|
||||
useAllMCPTools,
|
||||
useAuthorizeMCP,
|
||||
useInvalidateMCPTools,
|
||||
useUpdateMCPAuthorizationToken,
|
||||
useUpdateMCPTools,
|
||||
} from '@/service/use-tools'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
searchText: string
|
||||
}
|
||||
|
||||
function renderDefaultCard() {
|
||||
const defaultCards = Array.from({ length: 36 }, (_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'inline-flex h-[111px] rounded-xl bg-background-default-lighter opacity-10',
|
||||
index < 4 && 'opacity-60',
|
||||
index >= 4 && index < 8 && 'opacity-50',
|
||||
index >= 8 && index < 12 && 'opacity-40',
|
||||
index >= 12 && index < 16 && 'opacity-30',
|
||||
index >= 16 && index < 20 && 'opacity-25',
|
||||
index >= 20 && index < 24 && 'opacity-20',
|
||||
)}
|
||||
></div>
|
||||
))
|
||||
return defaultCards
|
||||
}
|
||||
|
||||
const MCPList = ({
|
||||
searchText,
|
||||
}: Props) => {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const authCode = searchParams.get('code') || ''
|
||||
const providerID = searchParams.get('state') || ''
|
||||
|
||||
const { data: list = [], refetch } = useAllMCPTools()
|
||||
const { mutateAsync: authorizeMcp } = useAuthorizeMCP()
|
||||
const { mutateAsync: updateTools } = useUpdateMCPTools()
|
||||
const invalidateMCPTools = useInvalidateMCPTools()
|
||||
const { mutateAsync: updateMCPAuthorizationToken } = useUpdateMCPAuthorizationToken()
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
return list.filter((collection) => {
|
||||
if (searchText)
|
||||
return Object.values(collection.name).some(value => (value as string).toLowerCase().includes(searchText.toLowerCase()))
|
||||
return true
|
||||
})
|
||||
}, [list, searchText])
|
||||
|
||||
const [currentProviderID, setCurrentProviderID] = useState<string>()
|
||||
|
||||
const currentProvider = useMemo(() => {
|
||||
return list.find(provider => provider.id === currentProviderID)
|
||||
}, [list, currentProviderID])
|
||||
|
||||
const handleCreate = async (provider: ToolWithProvider) => {
|
||||
await refetch() // update list
|
||||
setCurrentProviderID(provider.id)
|
||||
await authorizeMcp({
|
||||
provider_id: provider.id,
|
||||
server_url: provider.server_url!,
|
||||
})
|
||||
await refetch() // update authorization in list
|
||||
await updateTools(provider.id)
|
||||
invalidateMCPTools(provider.id)
|
||||
await refetch() // update tool list in provider list
|
||||
}
|
||||
|
||||
const handleUpdateAuthorization = async (providerID: string, code: string) => {
|
||||
const targetProvider = list.find(provider => provider.id === providerID)
|
||||
router.replace(pathname)
|
||||
if (!targetProvider) return
|
||||
await updateMCPAuthorizationToken({
|
||||
provider_id: providerID,
|
||||
server_url: targetProvider.server_url!,
|
||||
authorization_code: code,
|
||||
})
|
||||
await refetch()
|
||||
setCurrentProviderID(providerID)
|
||||
await updateTools(providerID)
|
||||
invalidateMCPTools(providerID)
|
||||
await refetch()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (authCode && providerID && list.length > 0)
|
||||
handleUpdateAuthorization(providerID, authCode)
|
||||
}, [authCode, providerID, list])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6',
|
||||
!list.length && 'h-[calc(100vh_-_136px)] overflow-hidden',
|
||||
)}
|
||||
>
|
||||
<NewMCPCard handleCreate={handleCreate} />
|
||||
{filteredList.map(provider => (
|
||||
<MCPCard
|
||||
key={provider.id}
|
||||
data={provider}
|
||||
currentProvider={currentProvider}
|
||||
handleSelect={setCurrentProviderID}
|
||||
onUpdate={refetch}
|
||||
/>
|
||||
))}
|
||||
{!list.length && renderDefaultCard()}
|
||||
</div>
|
||||
{currentProvider && (
|
||||
<MCPDetailPanel
|
||||
detail={currentProvider}
|
||||
onHide={() => setCurrentProviderID(undefined)}
|
||||
onUpdate={refetch}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default MCPList
|
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
|
215
web/app/components/tools/mcp/mcp-service-card.tsx
Normal file
215
web/app/components/tools/mcp/mcp-service-card.tsx
Normal file
@ -0,0 +1,215 @@
|
||||
'use client'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiLoopLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
Mcp,
|
||||
} from '@/app/components/base/icons/src/vender/other'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import type { AppSSO } from '@/types/app'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal'
|
||||
import { useAppWorkflow } from '@/service/use-workflow'
|
||||
import {
|
||||
useInvalidateMCPServerDetail,
|
||||
useMCPServerDetail,
|
||||
useRefreshMCPServerCode,
|
||||
useUpdateMCPServer,
|
||||
} from '@/service/use-tools'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type IAppCardProps = {
|
||||
appInfo: AppDetailResponse & Partial<AppSSO>
|
||||
}
|
||||
|
||||
function MCPServiceCard({
|
||||
appInfo,
|
||||
}: IAppCardProps) {
|
||||
const { t } = useTranslation()
|
||||
const { mutateAsync: updateMCPServer } = useUpdateMCPServer()
|
||||
const { mutateAsync: refreshMCPServerCode, isPending: genLoading } = useRefreshMCPServerCode()
|
||||
const invalidateMCPServerDetail = useInvalidateMCPServerDetail()
|
||||
const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext()
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [showMCPServerModal, setShowMCPServerModal] = useState(false)
|
||||
|
||||
const { data: currentWorkflow } = useAppWorkflow(appInfo.id)
|
||||
const { data: detail } = useMCPServerDetail(appInfo.id)
|
||||
const { id, status, server_code } = detail ?? {}
|
||||
|
||||
const appUnpublished = !currentWorkflow?.graph
|
||||
const serverPublished = !!id
|
||||
const serverActivated = status === 'active'
|
||||
const serverURL = serverPublished ? `${globalThis.location.protocol}//${globalThis.location.host}/api/server/${server_code}/mcp` : '***********'
|
||||
const toggleDisabled = !isCurrentWorkspaceEditor || appUnpublished
|
||||
|
||||
const [activated, setActivated] = useState(serverActivated)
|
||||
|
||||
const latestParams = useMemo(() => {
|
||||
if (!currentWorkflow?.graph)
|
||||
return []
|
||||
const startNode = currentWorkflow?.graph.nodes.find(node => node.data.type === BlockEnum.Start) as any
|
||||
return startNode?.data.variables as any[] || []
|
||||
}, [currentWorkflow])
|
||||
|
||||
const onGenCode = async () => {
|
||||
await refreshMCPServerCode(detail?.id || '')
|
||||
invalidateMCPServerDetail(appInfo.id)
|
||||
}
|
||||
|
||||
const onChangeStatus = async (state: boolean) => {
|
||||
setActivated(state)
|
||||
if (state) {
|
||||
if (!serverPublished)
|
||||
setShowMCPServerModal(true)
|
||||
|
||||
await updateMCPServer({
|
||||
appID: appInfo.id,
|
||||
id: id || '',
|
||||
description: detail?.description || '',
|
||||
parameters: detail?.parameters || {},
|
||||
status: 'active',
|
||||
})
|
||||
invalidateMCPServerDetail(appInfo.id)
|
||||
}
|
||||
else {
|
||||
await updateMCPServer({
|
||||
appID: appInfo.id,
|
||||
id: id || '',
|
||||
description: detail?.description || '',
|
||||
parameters: detail?.parameters || {},
|
||||
status: 'inactive',
|
||||
})
|
||||
invalidateMCPServerDetail(appInfo.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleServerModalHide = () => {
|
||||
setShowMCPServerModal(false)
|
||||
if (!serverActivated)
|
||||
setActivated(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setActivated(serverActivated)
|
||||
}, [serverActivated])
|
||||
|
||||
if (!currentWorkflow)
|
||||
return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight')}>
|
||||
<div className='rounded-xl bg-background-default'>
|
||||
<div className='flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3'>
|
||||
<div className='flex w-full items-center gap-3 self-stretch'>
|
||||
<div className='flex grow items-center'>
|
||||
<div className='mr-3 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-indigo-indigo-500 p-1 shadow-md'>
|
||||
<Mcp className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<div className="group w-full">
|
||||
<div className="system-md-semibold min-w-0 overflow-hidden text-ellipsis break-normal text-text-secondary group-hover:text-text-primary">
|
||||
{t('tools.mcp.server.title')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Indicator color={serverActivated ? 'green' : 'yellow'} />
|
||||
<div className={`${serverActivated ? 'text-text-success' : 'text-text-warning'} system-xs-semibold-uppercase`}>
|
||||
{serverActivated
|
||||
? t('appOverview.overview.status.running')
|
||||
: t('appOverview.overview.status.disable')}
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
popupContent={appUnpublished ? t('tools.mcp.server.publishTip') : ''}
|
||||
>
|
||||
<div>
|
||||
<Switch defaultValue={activated} onChange={onChangeStatus} disabled={toggleDisabled} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='flex flex-col items-start justify-center self-stretch'>
|
||||
<div className="system-xs-medium pb-1 text-text-tertiary">
|
||||
{t('tools.mcp.server.url')}
|
||||
</div>
|
||||
<div className="inline-flex h-9 w-full items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 pl-2">
|
||||
<div className="flex h-4 min-w-0 flex-1 items-start justify-start gap-2 px-1">
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-xs font-medium text-text-secondary">
|
||||
{serverURL}
|
||||
</div>
|
||||
</div>
|
||||
{serverPublished && (
|
||||
<>
|
||||
<CopyFeedback
|
||||
content={serverURL}
|
||||
className={'!size-6'}
|
||||
/>
|
||||
<Divider type="vertical" className="!mx-0.5 !h-3.5 shrink-0" />
|
||||
{isCurrentWorkspaceManager && (
|
||||
<Tooltip
|
||||
popupContent={t('appOverview.overview.appInfo.regenerate') || ''}
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer rounded-md p-1 hover:bg-state-base-hover"
|
||||
onClick={() => setShowConfirmDelete(true)}
|
||||
>
|
||||
<RiLoopLeftLine className={cn('h-4 w-4 text-text-tertiary hover:text-text-secondary', genLoading && 'animate-spin')}/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-1 self-stretch p-3'>
|
||||
<Button
|
||||
disabled={toggleDisabled}
|
||||
size='small'
|
||||
variant='ghost'
|
||||
onClick={() => setShowMCPServerModal(true)}
|
||||
>
|
||||
{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showMCPServerModal && (
|
||||
<MCPServerModal
|
||||
show={showMCPServerModal}
|
||||
appID={appInfo.id}
|
||||
data={serverPublished ? detail : undefined}
|
||||
latestParams={latestParams}
|
||||
onHide={handleServerModalHide}
|
||||
/>
|
||||
)}
|
||||
{/* button copy link/ button regenerate */}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
type='warning'
|
||||
title={t('appOverview.overview.appInfo.regenerate')}
|
||||
content={t('tools.mcp.server.reGen')}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={() => {
|
||||
onGenCode()
|
||||
setShowConfirmDelete(false)
|
||||
}}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPServiceCard
|
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',
|
||||
},
|
||||
},
|
||||
]
|
135
web/app/components/tools/mcp/modal.tsx
Normal file
135
web/app/components/tools/mcp/modal.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import { noop } from 'lodash-es'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type DuplicateAppModalProps = {
|
||||
data?: ToolWithProvider
|
||||
show: boolean
|
||||
onConfirm: (info: {
|
||||
name: string
|
||||
server_url: string
|
||||
icon_type: AppIconType
|
||||
icon: string
|
||||
icon_background?: string | null
|
||||
}) => void
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_ICON = { type: 'emoji', icon: '🧿', background: '#EFF1F5' }
|
||||
const extractFileId = (url: string) => {
|
||||
const match = url.match(/files\/(.+?)\/file-preview/)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
const getIcon = (data?: ToolWithProvider) => {
|
||||
if (!data)
|
||||
return DEFAULT_ICON as AppIconSelection
|
||||
if (typeof data.icon === 'string')
|
||||
return { type: 'image', url: data.icon, fileId: extractFileId(data.icon) } as AppIconSelection
|
||||
return {
|
||||
...data.icon,
|
||||
icon: data.icon.content,
|
||||
type: 'emoji',
|
||||
} as unknown as AppIconSelection
|
||||
}
|
||||
|
||||
const MCPModal = ({
|
||||
data,
|
||||
show,
|
||||
onConfirm,
|
||||
onHide,
|
||||
}: DuplicateAppModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [name, setName] = React.useState(data?.name || '')
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>(getIcon(data))
|
||||
const [url, setUrl] = React.useState(data?.server_url || '')
|
||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||
|
||||
const submit = async () => {
|
||||
await onConfirm({
|
||||
name,
|
||||
server_url: url,
|
||||
icon_type: appIcon.type,
|
||||
icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
|
||||
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
|
||||
})
|
||||
onHide()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isShow={show}
|
||||
onClose={noop}
|
||||
className={cn('relative !max-w-[520px]', 'p-6')}
|
||||
>
|
||||
<div className='absolute right-5 top-5 z-10 cursor-pointer p-1.5' onClick={onHide}>
|
||||
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='title-2xl-semi-bold relative pb-3 text-xl text-text-primary'>{t('tools.mcp.modal.title')}</div>
|
||||
<div className='space-y-5 py-3'>
|
||||
<div className='flex space-x-3'>
|
||||
<div className='grow pb-1'>
|
||||
<div className='mb-1 flex h-6 items-center'>
|
||||
<span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.name')}</span>
|
||||
</div>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder={t('tools.mcp.modal.namePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className='pt-2'>
|
||||
<AppIcon
|
||||
iconType={appIcon.type}
|
||||
icon={appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId}
|
||||
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
|
||||
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
|
||||
size='xxl' className='cursor-pointer rounded-2xl'
|
||||
onClick={() => { setShowAppIconPicker(true) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='mb-1 flex h-6 items-center'>
|
||||
<span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.serverUrl')}</span>
|
||||
</div>
|
||||
<Input
|
||||
value={url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
placeholder={t('tools.mcp.modal.serverUrlPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row-reverse pt-5'>
|
||||
<Button disabled={!name || !url} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.modal.confirm')}</Button>
|
||||
<Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
{showAppIconPicker && <AppIconPicker
|
||||
onSelect={(payload) => {
|
||||
setAppIcon(payload)
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setAppIcon(getIcon(data))
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
/>}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default MCPModal
|
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='flex items-center gap-2'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<RiHammerFill className='h-3 w-3 shrink-0 text-text-quaternary' />
|
||||
{data.tools.length > 0 && (
|
||||
<div className='system-xs-regular grow text-text-tertiary'>{t('tools.mcp.toolsCount', { count: data.tools.length })}</div>
|
||||
)}
|
||||
{!data.tools.length && (
|
||||
<div className='system-xs-regular grow text-text-tertiary'>{t('tools.mcp.noTools')}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='system-xs-regular text-divider-deep'>/</div>
|
||||
<div className='system-xs-regular truncate text-text-tertiary'>{`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at!)}`}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-1 rounded-b-xl pb-2.5 pl-4 pr-2.5 pt-1.5'>
|
||||
<div className='system-xs-regular grow truncate text-text-tertiary' title={data.server_url}>{data.server_url}</div>
|
||||
{data.is_team_authorization && data.tools.length > 0 && <Indicator color='green' className='shrink-0' />}
|
||||
{(!data.is_team_authorization || !data.tools.length) && (
|
||||
<div className='system-xs-medium flex shrink-0 items-center gap-1 rounded-md border border-util-colors-red-red-500 bg-components-badge-bg-red-soft px-1.5 py-0.5 text-util-colors-red-red-500'>
|
||||
{t('tools.mcp.noConfigured')}
|
||||
<Indicator color='red' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCurrentWorkspaceManager && (
|
||||
<div className={cn('absolute right-2.5 top-2.5 hidden group-hover:block', isOperationShow && 'block')} onClick={e => e.stopPropagation()}>
|
||||
<OperationDropdown
|
||||
inCard
|
||||
onOpenChange={setIsOperationShow}
|
||||
onEdit={showUpdateModal}
|
||||
onRemove={showDeleteConfirm}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isShowUpdateModal && (
|
||||
<MCPModal
|
||||
data={data}
|
||||
show={isShowUpdateModal}
|
||||
onConfirm={handleUpdate}
|
||||
onHide={hideUpdateModal}
|
||||
/>
|
||||
)}
|
||||
{isShowDeleteConfirm && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('tools.mcp.delete')}
|
||||
content={
|
||||
<div>
|
||||
{t('tools.mcp.deleteConfirmTitle', { mcp: data.name })}
|
||||
</div>
|
||||
}
|
||||
onCancel={hideDeleteConfirm}
|
||||
onConfirm={handleDelete}
|
||||
isLoading={deleting}
|
||||
isDisabled={deleting}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default MCPCard
|
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import type { Collection } from './types'
|
||||
import Marketplace from './marketplace'
|
||||
import cn from '@/utils/classnames'
|
||||
@ -15,6 +16,7 @@ import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty'
|
||||
import Card from '@/app/components/plugins/card'
|
||||
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
import MCPList from './mcp'
|
||||
import { useAllToolProviders } from '@/service/use-tools'
|
||||
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
@ -24,13 +26,18 @@ const ProviderList = () => {
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const authCode = searchParams.get('code') || ''
|
||||
const providerID = searchParams.get('state') || ''
|
||||
|
||||
const [activeTab, setActiveTab] = useTabSearchParams({
|
||||
defaultTab: 'builtin',
|
||||
defaultTab: authCode && providerID ? 'mcp' : 'builtin',
|
||||
})
|
||||
const options = [
|
||||
{ value: 'builtin', text: t('tools.type.builtIn') },
|
||||
{ value: 'api', text: t('tools.type.custom') },
|
||||
{ value: 'workflow', text: t('tools.type.workflow') },
|
||||
{ value: 'mcp', text: 'MCP' },
|
||||
]
|
||||
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
|
||||
const handleTagsChange = (value: string[]) => {
|
||||
@ -85,7 +92,9 @@ const ProviderList = () => {
|
||||
options={options}
|
||||
/>
|
||||
<div className='flex items-center gap-2'>
|
||||
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
|
||||
{activeTab !== 'mcp' && (
|
||||
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
|
||||
)}
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
@ -96,7 +105,7 @@ const ProviderList = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(filteredCollectionList.length > 0 || activeTab !== 'builtin') && (
|
||||
{activeTab !== 'mcp' && (
|
||||
<div className={cn(
|
||||
'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
|
||||
!filteredCollectionList.length && activeTab === 'workflow' && 'grow',
|
||||
@ -133,19 +142,20 @@ const ProviderList = () => {
|
||||
{!filteredCollectionList.length && activeTab === 'builtin' && (
|
||||
<Empty lightCard text={t('tools.noTools')} className='h-[224px] px-12' />
|
||||
)}
|
||||
{
|
||||
enable_marketplace && activeTab === 'builtin' && (
|
||||
<Marketplace
|
||||
onMarketplaceScroll={() => {
|
||||
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
|
||||
}}
|
||||
searchPluginText={keywords}
|
||||
filterPluginTags={tagFilterValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
</div >
|
||||
{enable_marketplace && activeTab === 'builtin' && (
|
||||
<Marketplace
|
||||
onMarketplaceScroll={() => {
|
||||
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
|
||||
}}
|
||||
searchPluginText={keywords}
|
||||
filterPluginTags={tagFilterValue}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'mcp' && (
|
||||
<MCPList searchText={keywords} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{currentProvider && !currentProvider.plugin_id && (
|
||||
<ProviderDetail
|
||||
collection={currentProvider}
|
||||
|
@ -3,13 +3,13 @@ import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiAddCircleFill,
|
||||
RiArrowRightUpLine,
|
||||
RiBookOpenLine,
|
||||
} from '@remixicon/react'
|
||||
import type { CustomCollectionBackend } from '../types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
|
||||
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
|
||||
import { createCustomCollection } from '@/service/tools'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
@ -45,20 +45,20 @@ const Contribute = ({ onRefreshData }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{isCurrentWorkspaceManager && (
|
||||
<div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl border-[0.5px] border-divider-subtle bg-components-panel-on-panel-item-bg transition-all duration-200 ease-in-out hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg'>
|
||||
<div className='group grow rounded-t-xl hover:bg-background-body' onClick={() => setIsShowEditCustomCollectionModal(true)}>
|
||||
<div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out'>
|
||||
<div className='group grow rounded-t-xl' onClick={() => setIsShowEditCustomCollectionModal(true)}>
|
||||
<div className='flex shrink-0 items-center p-4 pb-3'>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg group-hover:border-components-option-card-option-border-hover group-hover:bg-components-option-card-option-bg-hover'>
|
||||
<RiAddLine className='h-4 w-4 text-text-tertiary group-hover:text-text-accent'/>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'>
|
||||
<RiAddCircleFill className='h-4 w-4 text-text-quaternary group-hover:text-text-accent'/>
|
||||
</div>
|
||||
<div className='ml-3 text-sm font-semibold leading-5 text-text-primary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div>
|
||||
<div className='system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-b-xl border-t-[0.5px] border-divider-regular px-4 py-3 text-text-tertiary hover:bg-background-body hover:text-text-accent'>
|
||||
<div className='rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent'>
|
||||
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
|
||||
<BookOpen01 className='h-3 w-3 shrink-0' />
|
||||
<div className='grow truncate text-xs font-normal leading-[18px]' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
|
||||
<ArrowUpRight className='h-3 w-3 shrink-0' />
|
||||
<RiBookOpenLine className='h-3 w-3 shrink-0' />
|
||||
<div className='system-xs-regular grow truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
|
||||
<RiArrowRightUpLine className='h-3 w-3 shrink-0' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,6 +29,7 @@ export enum CollectionType {
|
||||
custom = 'api',
|
||||
model = 'model',
|
||||
workflow = 'workflow',
|
||||
mcp = 'mcp',
|
||||
}
|
||||
|
||||
export type Emoji = {
|
||||
@ -50,6 +51,9 @@ export type Collection = {
|
||||
labels: string[]
|
||||
plugin_id?: string
|
||||
letter?: string
|
||||
// MCP Server
|
||||
server_url?: string
|
||||
updated_at?: number
|
||||
}
|
||||
|
||||
export type ToolParameter = {
|
||||
@ -168,3 +172,11 @@ export type WorkflowToolProviderResponse = {
|
||||
}
|
||||
privacy_policy: string
|
||||
}
|
||||
|
||||
export type MCPServerDetail = {
|
||||
id: string
|
||||
server_code: string
|
||||
description: string
|
||||
status: string
|
||||
parameters?: Record<string, string>
|
||||
}
|
||||
|
@ -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,8 +18,6 @@ import cn from '@/utils/classnames'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import PluginList, { type ListProps } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import ActionButton from '../../base/action-button'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { PluginType } from '../../plugins/types'
|
||||
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
@ -31,11 +30,12 @@ type AllToolsProps = {
|
||||
buildInTools: ToolWithProvider[]
|
||||
customTools: ToolWithProvider[]
|
||||
workflowTools: ToolWithProvider[]
|
||||
mcpTools: ToolWithProvider[]
|
||||
onSelect: OnSelectBlock
|
||||
supportAddCustomTool?: boolean
|
||||
onAddedCustomTool?: () => void
|
||||
onShowAddCustomCollectionModal?: () => void
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
isHideMCPTools?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_TAGS: AllToolsProps['tags'] = []
|
||||
@ -46,15 +46,17 @@ const AllTools = ({
|
||||
searchText,
|
||||
tags = DEFAULT_TAGS,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
buildInTools,
|
||||
workflowTools,
|
||||
customTools,
|
||||
supportAddCustomTool,
|
||||
onShowAddCustomCollectionModal,
|
||||
mcpTools = [],
|
||||
selectedTools,
|
||||
isHideMCPTools,
|
||||
}: AllToolsProps) => {
|
||||
const language = useGetLanguage()
|
||||
const tabs = useToolTabs()
|
||||
const tabs = useToolTabs(isHideMCPTools)
|
||||
const [activeTab, setActiveTab] = useState(ToolTypeEnum.All)
|
||||
const [activeView, setActiveView] = useState<ViewType>(ViewType.flat)
|
||||
const hasFilter = searchText || tags.length > 0
|
||||
@ -64,13 +66,15 @@ const AllTools = ({
|
||||
const tools = useMemo(() => {
|
||||
let mergedTools: ToolWithProvider[] = []
|
||||
if (activeTab === ToolTypeEnum.All)
|
||||
mergedTools = [...buildInTools, ...customTools, ...workflowTools]
|
||||
mergedTools = [...buildInTools, ...customTools, ...workflowTools, ...mcpTools]
|
||||
if (activeTab === ToolTypeEnum.BuiltIn)
|
||||
mergedTools = buildInTools
|
||||
if (activeTab === ToolTypeEnum.Custom)
|
||||
mergedTools = customTools
|
||||
if (activeTab === ToolTypeEnum.Workflow)
|
||||
mergedTools = workflowTools
|
||||
if (activeTab === ToolTypeEnum.MCP)
|
||||
mergedTools = mcpTools
|
||||
|
||||
if (!hasFilter)
|
||||
return mergedTools.filter(toolWithProvider => toolWithProvider.tools.length > 0)
|
||||
@ -80,7 +84,7 @@ const AllTools = ({
|
||||
return tool.label[language].toLowerCase().includes(searchText.toLowerCase()) || tool.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
})
|
||||
})
|
||||
}, [activeTab, buildInTools, customTools, workflowTools, searchText, language, hasFilter])
|
||||
}, [activeTab, buildInTools, customTools, workflowTools, mcpTools, searchText, language, hasFilter])
|
||||
|
||||
const {
|
||||
queryPluginsWithDebounced: fetchPlugins,
|
||||
@ -103,10 +107,11 @@ const AllTools = ({
|
||||
|
||||
const pluginRef = useRef<ListRef>(null)
|
||||
const wrapElemRef = useRef<HTMLDivElement>(null)
|
||||
const isSupportGroupView = [ToolTypeEnum.All, ToolTypeEnum.BuiltIn].includes(activeTab)
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<div className='flex items-center justify-between border-b-[0.5px] border-divider-subtle bg-background-default-hover px-3 shadow-xs'>
|
||||
<div className='flex items-center justify-between border-b border-divider-subtle px-3'>
|
||||
<div className='flex h-8 items-center space-x-1'>
|
||||
{
|
||||
tabs.map(tab => (
|
||||
@ -124,17 +129,8 @@ const AllTools = ({
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<ViewTypeSelect viewType={activeView} onChange={setActiveView} />
|
||||
{supportAddCustomTool && (
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-1.5 h-3.5 w-px bg-divider-regular'></div>
|
||||
<ActionButton
|
||||
className='bg-components-button-primary-bg text-components-button-primary-text hover:bg-components-button-primary-bg hover:text-components-button-primary-text'
|
||||
onClick={onShowAddCustomCollectionModal}
|
||||
>
|
||||
<RiAddLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
{isSupportGroupView && (
|
||||
<ViewTypeSelect viewType={activeView} onChange={setActiveView} />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
@ -147,7 +143,9 @@ const AllTools = ({
|
||||
showWorkflowEmpty={activeTab === ToolTypeEnum.Workflow}
|
||||
tools={tools}
|
||||
onSelect={onSelect}
|
||||
viewType={activeView}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
viewType={isSupportGroupView ? activeView : ViewType.flat}
|
||||
hasSearchText={!!searchText}
|
||||
selectedTools={selectedTools}
|
||||
/>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -129,33 +129,34 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}>
|
||||
<div className='px-2 pt-2' onClick={e => e.stopPropagation()}>
|
||||
{activeTab === TabsEnum.Blocks && (
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
autoFocus
|
||||
value={searchText}
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
onClear={() => setSearchText('')}
|
||||
/>
|
||||
)}
|
||||
{activeTab === TabsEnum.Tools && (
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
size='small'
|
||||
placeholder={t('plugin.searchTools')!}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<Tabs
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={handleActiveTabChange}
|
||||
filterElem={
|
||||
<div className='relative m-2' onClick={e => e.stopPropagation()}>
|
||||
{activeTab === TabsEnum.Blocks && (
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
autoFocus
|
||||
value={searchText}
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
onClear={() => setSearchText('')}
|
||||
/>
|
||||
)}
|
||||
{activeTab === TabsEnum.Tools && (
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
size='small'
|
||||
placeholder={t('plugin.searchTools')!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
onSelect={handleSelect}
|
||||
searchText={searchText}
|
||||
tags={tags}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
|
||||
import type { BlockEnum } from '../types'
|
||||
import { useTabs } from './hooks'
|
||||
import type { ToolDefaultValue } from './types'
|
||||
@ -16,6 +16,7 @@ export type TabsProps = {
|
||||
tags: string[]
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
filterElem: React.ReactNode
|
||||
noBlocks?: boolean
|
||||
}
|
||||
const Tabs: FC<TabsProps> = ({
|
||||
@ -25,26 +26,28 @@ const Tabs: FC<TabsProps> = ({
|
||||
searchText,
|
||||
onSelect,
|
||||
availableBlocksTypes,
|
||||
filterElem,
|
||||
noBlocks,
|
||||
}) => {
|
||||
const tabs = useTabs()
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
return (
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
{
|
||||
!noBlocks && (
|
||||
<div className='flex items-center border-b-[0.5px] border-divider-subtle px-3'>
|
||||
<div className='relative flex bg-background-section-burn pl-1 pt-1'>
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={cn(
|
||||
'system-sm-medium relative mr-4 cursor-pointer pb-2 pt-1',
|
||||
'system-sm-medium relative mr-0.5 flex h-8 cursor-pointer items-center rounded-t-lg px-3 ',
|
||||
activeTab === tab.key
|
||||
? 'text-text-primary after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-util-colors-blue-brand-blue-brand-600'
|
||||
? 'sm-no-bottom cursor-default bg-components-panel-bg text-text-accent'
|
||||
: 'text-text-tertiary',
|
||||
)}
|
||||
onClick={() => onActiveTabChange(tab.key)}
|
||||
@ -56,13 +59,16 @@ const Tabs: FC<TabsProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{filterElem}
|
||||
{
|
||||
activeTab === TabsEnum.Blocks && !noBlocks && (
|
||||
<Blocks
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
/>
|
||||
<div className='border-t border-divider-subtle'>
|
||||
<Blocks
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
@ -72,9 +78,12 @@ const Tabs: FC<TabsProps> = ({
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
tags={tags}
|
||||
canNotSelectMultiple
|
||||
buildInTools={buildInTools || []}
|
||||
customTools={customTools || []}
|
||||
workflowTools={workflowTools || []}
|
||||
mcpTools={mcpTools || []}
|
||||
isHideMCPTools
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
} from '@/service/tools'
|
||||
import type { CustomCollectionBackend } from '@/app/components/tools/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
@ -35,6 +35,7 @@ type Props = {
|
||||
isShow: boolean
|
||||
onShowChange: (isShow: boolean) => void
|
||||
onSelect: (tool: ToolDefaultValue) => void
|
||||
onSelectMultiple: (tools: ToolDefaultValue[]) => void
|
||||
supportAddCustomTool?: boolean
|
||||
scope?: string
|
||||
selectedTools?: ToolValue[]
|
||||
@ -48,6 +49,7 @@ const ToolPicker: FC<Props> = ({
|
||||
isShow,
|
||||
onShowChange,
|
||||
onSelect,
|
||||
onSelectMultiple,
|
||||
supportAddCustomTool,
|
||||
scope = 'all',
|
||||
selectedTools,
|
||||
@ -61,6 +63,7 @@ const ToolPicker: FC<Props> = ({
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const invalidateCustomTools = useInvalidateAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
const { builtinToolList, customToolList, workflowToolList } = useMemo(() => {
|
||||
if (scope === 'plugins') {
|
||||
@ -102,6 +105,10 @@ const ToolPicker: FC<Props> = ({
|
||||
onSelect(tool!)
|
||||
}
|
||||
|
||||
const handleSelectMultiple = (_type: BlockEnum, tools: ToolDefaultValue[]) => {
|
||||
onSelectMultiple(tools)
|
||||
}
|
||||
|
||||
const [isShowEditCollectionToolModal, {
|
||||
setFalse: hideEditCustomCollectionModal,
|
||||
setTrue: showEditCustomCollectionModal,
|
||||
@ -151,6 +158,10 @@ const ToolPicker: FC<Props> = ({
|
||||
onTagsChange={setTags}
|
||||
size='small'
|
||||
placeholder={t('plugin.searchTools')!}
|
||||
supportAddCustomTool={supportAddCustomTool}
|
||||
onAddedCustomTool={handleAddedCustomTool}
|
||||
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
|
||||
|
||||
/>
|
||||
</div>
|
||||
<AllTools
|
||||
@ -159,12 +170,12 @@ const ToolPicker: FC<Props> = ({
|
||||
tags={tags}
|
||||
searchText={searchText}
|
||||
onSelect={handleSelect}
|
||||
onSelectMultiple={handleSelectMultiple}
|
||||
buildInTools={builtinToolList || []}
|
||||
customTools={customToolList || []}
|
||||
workflowTools={workflowToolList || []}
|
||||
supportAddCustomTool={supportAddCustomTool}
|
||||
onAddedCustomTool={handleAddedCustomTool}
|
||||
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
|
||||
mcpTools={mcpTools || []}
|
||||
|
||||
selectedTools={selectedTools}
|
||||
/>
|
||||
</div>
|
||||
|
@ -10,8 +10,6 @@ import { useGetLanguage } from '@/context/i18n'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCheckLine } from '@remixicon/react'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
type Props = {
|
||||
provider: ToolWithProvider
|
||||
@ -74,15 +72,12 @@ const ToolItem: FC<Props> = ({
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary', disabled && 'opacity-30')}>{payload.label[language]}</div>
|
||||
{disabled && <Badge
|
||||
className='flex h-5 items-center space-x-0.5 text-text-tertiary'
|
||||
uppercase
|
||||
>
|
||||
<RiCheckLine className='h-3 w-3 ' />
|
||||
<div>{t('tools.addToolModal.added')}</div>
|
||||
</Badge>
|
||||
}
|
||||
<div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary')}>
|
||||
<span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span>
|
||||
</div>
|
||||
{disabled && (
|
||||
<div className='system-xs-regular mr-4 text-text-tertiary'>{t('tools.addToolModal.added')}</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip >
|
||||
)
|
||||
|
@ -13,6 +13,8 @@ type Props = {
|
||||
isShowLetterIndex: boolean
|
||||
hasSearchText: boolean
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
letters: string[]
|
||||
toolRefs: any
|
||||
selectedTools?: ToolValue[]
|
||||
@ -24,6 +26,8 @@ const ToolViewFlatView: FC<Props> = ({
|
||||
isShowLetterIndex,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
toolRefs,
|
||||
selectedTools,
|
||||
}) => {
|
||||
@ -53,6 +57,8 @@ const ToolViewFlatView: FC<Props> = ({
|
||||
isShowLetterIndex={isShowLetterIndex}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
/>
|
||||
</div>
|
||||
|
@ -12,6 +12,8 @@ type Props = {
|
||||
toolList: ToolWithProvider[]
|
||||
hasSearchText: boolean
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
}
|
||||
|
||||
@ -20,6 +22,8 @@ const Item: FC<Props> = ({
|
||||
toolList,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
}) => {
|
||||
return (
|
||||
@ -36,6 +40,8 @@ const Item: FC<Props> = ({
|
||||
isShowLetterIndex={false}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
/>
|
||||
))}
|
||||
|
@ -12,6 +12,8 @@ type Props = {
|
||||
payload: Record<string, ToolWithProvider[]>
|
||||
hasSearchText: boolean
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
}
|
||||
|
||||
@ -19,6 +21,8 @@ const ToolListTreeView: FC<Props> = ({
|
||||
payload,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@ -46,6 +50,8 @@ const ToolListTreeView: FC<Props> = ({
|
||||
toolList={payload[groupName]}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
/>
|
||||
))}
|
||||
|
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
@ -13,6 +13,7 @@ import { ViewType } from '../view-type-select'
|
||||
import ActonItem from './action-item'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHover } from 'ahooks'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
@ -21,6 +22,8 @@ type Props = {
|
||||
isShowLetterIndex: boolean
|
||||
hasSearchText: boolean
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
}
|
||||
|
||||
@ -31,18 +34,83 @@ const Tool: FC<Props> = ({
|
||||
isShowLetterIndex,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const isFlatView = viewType === ViewType.flat
|
||||
const notShowProvider = payload.type === CollectionType.workflow
|
||||
const actions = payload.tools
|
||||
const hasAction = true // Now always support actions
|
||||
const hasAction = !notShowProvider
|
||||
const [isFold, setFold] = React.useState<boolean>(true)
|
||||
const getIsDisabled = (tool: ToolType) => {
|
||||
const ref = useRef(null)
|
||||
const isHovering = useHover(ref)
|
||||
const getIsDisabled = useCallback((tool: ToolType) => {
|
||||
if (!selectedTools || !selectedTools.length) return false
|
||||
return selectedTools.some(selectedTool => selectedTool.provider_name === payload.name && selectedTool.tool_name === tool.name)
|
||||
}
|
||||
return selectedTools.some(selectedTool => (selectedTool.provider_name === payload.name || selectedTool.provider_name === payload.id) && selectedTool.tool_name === tool.name)
|
||||
}, [payload.id, payload.name, selectedTools])
|
||||
|
||||
const totalToolsNum = actions.length
|
||||
const selectedToolsNum = actions.filter(action => getIsDisabled(action)).length
|
||||
const isAllSelected = selectedToolsNum === totalToolsNum
|
||||
|
||||
const notShowProviderSelectInfo = useMemo(() => {
|
||||
if (isAllSelected) {
|
||||
return (
|
||||
<span className='system-xs-regular text-text-tertiary'>
|
||||
{t('tools.addToolModal.added')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}, [isAllSelected, t])
|
||||
const selectedInfo = useMemo(() => {
|
||||
if (isHovering && !isAllSelected) {
|
||||
return (
|
||||
<span className='system-xs-regular text-components-button-secondary-accent-text'
|
||||
onClick={(e) => {
|
||||
onSelectMultiple?.(BlockEnum.Tool, actions.filter(action => !getIsDisabled(action)).map((tool) => {
|
||||
const params: Record<string, string> = {}
|
||||
if (tool.parameters) {
|
||||
tool.parameters.forEach((item) => {
|
||||
params[item.name] = ''
|
||||
})
|
||||
}
|
||||
return {
|
||||
provider_id: payload.id,
|
||||
provider_type: payload.type,
|
||||
provider_name: payload.name,
|
||||
tool_name: tool.name,
|
||||
tool_label: tool.label[language],
|
||||
tool_description: tool.description[language],
|
||||
title: tool.label[language],
|
||||
is_team_authorization: payload.is_team_authorization,
|
||||
output_schema: tool.output_schema,
|
||||
paramSchemas: tool.parameters,
|
||||
params,
|
||||
}
|
||||
}))
|
||||
}}
|
||||
>
|
||||
{t('workflow.tabs.addAll')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedToolsNum === 0)
|
||||
return <></>
|
||||
|
||||
return (
|
||||
<span className='system-xs-regular text-text-tertiary'>
|
||||
{isAllSelected
|
||||
? t('workflow.tabs.allAdded')
|
||||
: `${selectedToolsNum} / ${totalToolsNum}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum])
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSearchText && isFold) {
|
||||
setFold(false)
|
||||
@ -72,29 +140,37 @@ const Tool: FC<Props> = ({
|
||||
<div
|
||||
key={payload.id}
|
||||
className={cn('mb-1 last-of-type:mb-0', isShowLetterIndex && 'mr-6')}
|
||||
ref={ref}
|
||||
>
|
||||
<div className={cn(className)}>
|
||||
<div
|
||||
className='flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
if (hasAction)
|
||||
if (hasAction) {
|
||||
setFold(!isFold)
|
||||
return
|
||||
}
|
||||
|
||||
// Now always support actions
|
||||
// if (payload.parameters) {
|
||||
// payload.parameters.forEach((item) => {
|
||||
// params[item.name] = ''
|
||||
// })
|
||||
// }
|
||||
// onSelect(BlockEnum.Tool, {
|
||||
// provider_id: payload.id,
|
||||
// provider_type: payload.type,
|
||||
// provider_name: payload.name,
|
||||
// tool_name: payload.name,
|
||||
// tool_label: payload.label[language],
|
||||
// title: payload.label[language],
|
||||
// params: {},
|
||||
// })
|
||||
const tool = actions[0]
|
||||
const params: Record<string, string> = {}
|
||||
if (tool.parameters) {
|
||||
tool.parameters.forEach((item) => {
|
||||
params[item.name] = ''
|
||||
})
|
||||
}
|
||||
onSelect(BlockEnum.Tool, {
|
||||
provider_id: payload.id,
|
||||
provider_type: payload.type,
|
||||
provider_name: payload.name,
|
||||
tool_name: tool.name,
|
||||
tool_label: tool.label[language],
|
||||
tool_description: tool.description[language],
|
||||
title: tool.label[language],
|
||||
is_team_authorization: payload.is_team_authorization,
|
||||
output_schema: tool.output_schema,
|
||||
paramSchemas: tool.parameters,
|
||||
params,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className='flex h-8 grow items-center'>
|
||||
@ -103,20 +179,23 @@ const Tool: FC<Props> = ({
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={payload.icon}
|
||||
/>
|
||||
<div className='ml-2 w-0 flex-1 grow truncate text-sm text-text-primary'>{payload.label[language]}</div>
|
||||
<div className='ml-2 w-0 grow truncate text-sm text-text-primary'>
|
||||
<span>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span>
|
||||
{isFlatView && (
|
||||
<span className='system-xs-regular ml-2 text-text-quaternary'>{groupName}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
{isFlatView && (
|
||||
<div className='system-xs-regular text-text-tertiary'>{groupName}</div>
|
||||
)}
|
||||
<div className='ml-2 flex items-center'>
|
||||
{!canNotSelectMultiple && (notShowProvider ? notShowProviderSelectInfo : selectedInfo)}
|
||||
{hasAction && (
|
||||
<FoldIcon className={cn('h-4 w-4 shrink-0 text-text-quaternary', isFold && 'text-text-tertiary')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasAction && !isFold && (
|
||||
{!notShowProvider && hasAction && !isFold && (
|
||||
actions.map(action => (
|
||||
<ActonItem
|
||||
key={action.name}
|
||||
|
@ -17,6 +17,8 @@ import classNames from '@/utils/classnames'
|
||||
type ToolsProps = {
|
||||
showWorkflowEmpty: boolean
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
tools: ToolWithProvider[]
|
||||
viewType: ViewType
|
||||
hasSearchText: boolean
|
||||
@ -27,6 +29,8 @@ type ToolsProps = {
|
||||
const Blocks = ({
|
||||
showWorkflowEmpty,
|
||||
onSelect,
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
tools,
|
||||
viewType,
|
||||
hasSearchText,
|
||||
@ -107,6 +111,8 @@ const Blocks = ({
|
||||
isShowLetterIndex={isShowLetterIndex}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
/>
|
||||
) : (
|
||||
@ -114,6 +120,8 @@ const Blocks = ({
|
||||
payload={treeViewToolsData}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
/>
|
||||
)
|
||||
|
@ -8,6 +8,7 @@ export enum ToolTypeEnum {
|
||||
BuiltIn = 'built-in',
|
||||
Custom = 'custom',
|
||||
Workflow = 'workflow',
|
||||
MCP = 'mcp',
|
||||
}
|
||||
|
||||
export enum BlockClassificationEnum {
|
||||
@ -34,6 +35,7 @@ export type ToolDefaultValue = {
|
||||
|
||||
export type ToolValue = {
|
||||
provider_name: string
|
||||
provider_show_name?: string
|
||||
tool_name: string
|
||||
tool_label: string
|
||||
tool_description: string
|
||||
|
@ -40,6 +40,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
fetchAllBuiltInTools,
|
||||
fetchAllCustomTools,
|
||||
fetchAllMCPTools,
|
||||
fetchAllWorkflowTools,
|
||||
} from '@/service/tools'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
@ -450,6 +451,13 @@ export const useFetchToolsData = () => {
|
||||
workflowTools: workflowTools || [],
|
||||
})
|
||||
}
|
||||
if(type === 'mcp') {
|
||||
const mcpTools = await fetchAllMCPTools()
|
||||
|
||||
workflowStore.setState({
|
||||
mcpTools: mcpTools || [],
|
||||
})
|
||||
}
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
@ -496,6 +504,8 @@ export const useToolIcon = (data: Node['data']) => {
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const mcpTools = useStore(s => s.mcpTools)
|
||||
|
||||
const toolIcon = useMemo(() => {
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
let targetTools = buildInTools
|
||||
@ -503,11 +513,13 @@ export const useToolIcon = (data: Node['data']) => {
|
||||
targetTools = buildInTools
|
||||
else if (data.provider_type === CollectionType.custom)
|
||||
targetTools = customTools
|
||||
else if (data.provider_type === CollectionType.mcp)
|
||||
targetTools = mcpTools
|
||||
else
|
||||
targetTools = workflowTools
|
||||
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon
|
||||
}
|
||||
}, [data, buildInTools, customTools, workflowTools])
|
||||
}, [data.type, data.provider_type, data.provider_id, buildInTools, customTools, mcpTools, workflowTools])
|
||||
|
||||
return toolIcon
|
||||
}
|
||||
|
@ -205,6 +205,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
handleFetchAllTools('builtin')
|
||||
handleFetchAllTools('custom')
|
||||
handleFetchAllTools('workflow')
|
||||
handleFetchAllTools('mcp')
|
||||
}, [handleFetchAllTools])
|
||||
|
||||
const {
|
||||
|
@ -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,11 +5,12 @@ import { useSchemaNodeOperations } from './hooks'
|
||||
|
||||
export type VisualEditorProps = {
|
||||
schema: SchemaRoot
|
||||
onChange: (schema: SchemaRoot) => void
|
||||
readOnly?: boolean
|
||||
onChange?: (schema: SchemaRoot) => void
|
||||
}
|
||||
|
||||
const VisualEditor: FC<VisualEditorProps> = (props) => {
|
||||
const { schema } = props
|
||||
const { schema, readOnly } = props
|
||||
useSchemaNodeOperations(props)
|
||||
|
||||
return (
|
||||
@ -20,6 +21,7 @@ const VisualEditor: FC<VisualEditorProps> = (props) => {
|
||||
required={false}
|
||||
path={[]}
|
||||
depth={0}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -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 />
|
||||
)
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const mcpTools = useStore(s => s.mcpTools)
|
||||
|
||||
const currentTools = (() => {
|
||||
switch (provider_type) {
|
||||
@ -46,6 +47,8 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
return customTools
|
||||
case CollectionType.workflow:
|
||||
return workflowTools
|
||||
case CollectionType.mcp:
|
||||
return mcpTools
|
||||
default:
|
||||
return []
|
||||
}
|
||||
|
@ -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 })),
|
||||
})
|
||||
|
@ -55,7 +55,7 @@ const translation = {
|
||||
unsupportedContent: 'Die installierte Plug-in-Version bietet diese Aktion nicht.',
|
||||
unsupportedTitle: 'Nicht unterstützte Aktion',
|
||||
descriptionPlaceholder: 'Kurze Beschreibung des Zwecks des Werkzeugs, z. B. um die Temperatur für einen bestimmten Ort zu ermitteln.',
|
||||
auto: 'Automatisch',
|
||||
auto: 'Auto',
|
||||
params: 'KONFIGURATION DER ARGUMENTATION',
|
||||
unsupportedContent2: 'Klicken Sie hier, um die Version zu wechseln.',
|
||||
placeholder: 'Wählen Sie ein Werkzeug aus...',
|
||||
|
@ -85,8 +85,8 @@ const translation = {
|
||||
settings: 'USER SETTINGS',
|
||||
params: 'REASONING CONFIG',
|
||||
paramsTip1: 'Controls LLM inference parameters.',
|
||||
paramsTip2: 'When \'Automatic\' is off, the default value is used.',
|
||||
auto: 'Automatic',
|
||||
paramsTip2: 'When \'Auto\' is off, the default value is used.',
|
||||
auto: 'Auto',
|
||||
empty: 'Click the \'+\' button to add tools. You can add multiple tools.',
|
||||
uninstalledTitle: 'Tool not installed',
|
||||
uninstalledContent: 'This plugin is installed from the local/GitHub repository. Please use after installation.',
|
||||
|
@ -152,6 +152,62 @@ const translation = {
|
||||
toolNameUsageTip: 'Tool call name for agent reasoning and prompting',
|
||||
copyToolName: 'Copy Name',
|
||||
noTools: 'No tools found',
|
||||
mcp: {
|
||||
create: {
|
||||
cardTitle: 'Add MCP Server (HTTP)',
|
||||
cardLink: 'Learn more about MCP server integration',
|
||||
},
|
||||
noConfigured: 'Unconfigured Server',
|
||||
updateTime: 'Updated',
|
||||
toolsCount: '{{count}} tools',
|
||||
noTools: 'No tools available',
|
||||
modal: {
|
||||
title: 'Add MCP Server (HTTP)',
|
||||
name: 'Name & Icon',
|
||||
namePlaceholder: 'Name your MCP server',
|
||||
serverUrl: 'Server URL',
|
||||
serverUrlPlaceholder: 'URL to server endpiont',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
confirm: 'Add & Authorize',
|
||||
},
|
||||
delete: 'Remove MCP Server',
|
||||
deleteConfirmTitle: 'Would you like to remove {{mcp}}?',
|
||||
operation: {
|
||||
edit: 'Edit',
|
||||
remove: 'Remove',
|
||||
},
|
||||
authorize: 'Authorize',
|
||||
authorizing: 'Authorizing...',
|
||||
authorizingRequired: 'Authorization is required',
|
||||
authorizeTip: 'After authorization, tools will be displayed here.',
|
||||
update: 'Update',
|
||||
updating: 'Updating',
|
||||
gettingTools: 'Getting Tools...',
|
||||
updateTools: 'Updating Tools...',
|
||||
toolsEmpty: 'Tools not loaded',
|
||||
getTools: 'Get tools',
|
||||
toolsNum: '{{count}} tools included',
|
||||
onlyTool: '1 tool included',
|
||||
server: {
|
||||
title: 'MCP Server',
|
||||
url: 'Server URL',
|
||||
reGen: 'Do you want to regenerator server URL?',
|
||||
addDescription: 'Add description',
|
||||
edit: 'Edit description',
|
||||
modal: {
|
||||
addTitle: 'Add description to enable MCP server',
|
||||
editTitle: 'Edit description',
|
||||
description: 'Description',
|
||||
descriptionPlaceholder: 'Explain what this tool does and how it should be used by the LLM',
|
||||
parameters: 'Parameters',
|
||||
parametersTip: 'Add descriptions for each parameter to help the LLM understand their purpose and constraints.',
|
||||
parametersPlaceholder: 'Parameter purpose and constraints',
|
||||
confirm: 'Enable MCP Server',
|
||||
},
|
||||
publishTip: 'App not published. Please publish the app first.',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
@ -229,6 +229,8 @@ const translation = {
|
||||
'utilities': 'Utilities',
|
||||
'noResult': 'No match found',
|
||||
'agent': 'Agent Strategy',
|
||||
'allAdded': 'All added',
|
||||
'addAll': 'Add all',
|
||||
},
|
||||
blocks: {
|
||||
'start': 'Start',
|
||||
@ -876,6 +878,8 @@ const translation = {
|
||||
install: 'Install',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
clickToViewParameterSchema: 'Click to view parameter schema',
|
||||
parameterSchema: 'Parameter Schema',
|
||||
},
|
||||
},
|
||||
tracing: {
|
||||
|
@ -51,11 +51,11 @@ const translation = {
|
||||
unsupportedContent2: 'Haga clic para cambiar de versión.',
|
||||
descriptionPlaceholder: 'Breve descripción del propósito de la herramienta, por ejemplo, obtener la temperatura para una ubicación específica.',
|
||||
empty: 'Haga clic en el botón \'+\' para agregar herramientas. Puede agregar varias herramientas.',
|
||||
paramsTip2: 'Cuando \'Automático\' está desactivado, se utiliza el valor predeterminado.',
|
||||
paramsTip2: 'Cuando \'Auto\' está desactivado, se utiliza el valor predeterminado.',
|
||||
uninstalledTitle: 'Herramienta no instalada',
|
||||
descriptionLabel: 'Descripción de la herramienta',
|
||||
unsupportedContent: 'La versión del plugin instalado no proporciona esta acción.',
|
||||
auto: 'Automático',
|
||||
auto: 'Auto',
|
||||
title: 'Agregar herramienta',
|
||||
placeholder: 'Seleccione una herramienta...',
|
||||
uninstalledContent: 'Este plugin se instala desde el repositorio local/GitHub. Úselo después de la instalación.',
|
||||
|
@ -53,14 +53,14 @@ const translation = {
|
||||
placeholder: 'Sélectionnez un outil...',
|
||||
params: 'CONFIGURATION DE RAISONNEMENT',
|
||||
unsupportedContent: 'La version du plugin installée ne fournit pas cette action.',
|
||||
auto: 'Automatique',
|
||||
auto: 'Auto',
|
||||
descriptionPlaceholder: 'Brève description de l’objectif de l’outil, par exemple, obtenir la température d’un endroit spécifique.',
|
||||
unsupportedContent2: 'Cliquez pour changer de version.',
|
||||
uninstalledTitle: 'Outil non installé',
|
||||
empty: 'Cliquez sur le bouton « + » pour ajouter des outils. Vous pouvez ajouter plusieurs outils.',
|
||||
toolLabel: 'Outil',
|
||||
settings: 'PARAMÈTRES UTILISATEUR',
|
||||
paramsTip2: 'Lorsque « Automatique » est désactivé, la valeur par défaut est utilisée.',
|
||||
paramsTip2: 'Lorsque « Auto » est désactivé, la valeur par défaut est utilisée.',
|
||||
paramsTip1: 'Contrôle les paramètres d’inférence LLM.',
|
||||
},
|
||||
modelNum: '{{num}} MODÈLES INCLUS',
|
||||
|
@ -60,8 +60,8 @@ const translation = {
|
||||
placeholder: 'Seleziona uno strumento...',
|
||||
unsupportedContent: 'La versione del plug-in installata non fornisce questa azione.',
|
||||
descriptionLabel: 'Descrizione dell\'utensile',
|
||||
auto: 'Automatico',
|
||||
paramsTip2: 'Quando \'Automatico\' è disattivato, viene utilizzato il valore predefinito.',
|
||||
auto: 'Auto',
|
||||
paramsTip2: 'Quando \'Auto\' è disattivato, viene utilizzato il valore predefinito.',
|
||||
},
|
||||
modelNum: '{{num}} MODELLI INCLUSI',
|
||||
endpointModalTitle: 'Endpoint di configurazione',
|
||||
|
@ -51,7 +51,7 @@ const translation = {
|
||||
paramsTip1: 'Steruje parametrami wnioskowania LLM.',
|
||||
unsupportedContent: 'Zainstalowana wersja wtyczki nie zapewnia tej akcji.',
|
||||
params: 'KONFIGURACJA ROZUMOWANIA',
|
||||
auto: 'Automatyczne',
|
||||
auto: 'Auto',
|
||||
empty: 'Kliknij przycisk "+", aby dodać narzędzia. Możesz dodać wiele narzędzi.',
|
||||
descriptionLabel: 'Opis narzędzia',
|
||||
title: 'Dodaj narzędzie',
|
||||
@ -60,7 +60,7 @@ const translation = {
|
||||
uninstalledContent: 'Ta wtyczka jest instalowana z repozytorium lokalnego/GitHub. Proszę użyć po instalacji.',
|
||||
unsupportedTitle: 'Nieobsługiwana akcja',
|
||||
uninstalledTitle: 'Narzędzie nie jest zainstalowane',
|
||||
paramsTip2: 'Gdy opcja "Automatycznie" jest wyłączona, używana jest wartość domyślna.',
|
||||
paramsTip2: 'Gdy opcja "Auto" jest wyłączona, używana jest wartość domyślna.',
|
||||
toolLabel: 'Narzędzie',
|
||||
},
|
||||
strategyNum: '{{liczba}} {{strategia}} ZAWARTE',
|
||||
|
@ -47,14 +47,14 @@ const translation = {
|
||||
toolSelector: {
|
||||
uninstalledLink: 'Gerenciar em plug-ins',
|
||||
unsupportedContent2: 'Clique para mudar de versão.',
|
||||
auto: 'Automático',
|
||||
auto: 'Auto',
|
||||
title: 'Adicionar ferramenta',
|
||||
params: 'CONFIGURAÇÃO DE RACIOCÍNIO',
|
||||
toolLabel: 'Ferramenta',
|
||||
paramsTip1: 'Controla os parâmetros de inferência do LLM.',
|
||||
descriptionLabel: 'Descrição da ferramenta',
|
||||
uninstalledContent: 'Este plug-in é instalado a partir do repositório local/GitHub. Por favor, use após a instalação.',
|
||||
paramsTip2: 'Quando \'Automático\' está desativado, o valor padrão é usado.',
|
||||
paramsTip2: 'Quando \'Auto\' está desativado, o valor padrão é usado.',
|
||||
placeholder: 'Selecione uma ferramenta...',
|
||||
empty: 'Clique no botão \'+\' para adicionar ferramentas. Você pode adicionar várias ferramentas.',
|
||||
settings: 'CONFIGURAÇÕES DO USUÁRIO',
|
||||
|
@ -46,7 +46,7 @@ const translation = {
|
||||
},
|
||||
toolSelector: {
|
||||
unsupportedContent: 'Versiunea de plugin instalată nu oferă această acțiune.',
|
||||
auto: 'Automat',
|
||||
auto: 'Auto',
|
||||
empty: 'Faceți clic pe butonul "+" pentru a adăuga instrumente. Puteți adăuga mai multe instrumente.',
|
||||
uninstalledContent: 'Acest plugin este instalat din depozitul local/GitHub. Vă rugăm să utilizați după instalare.',
|
||||
descriptionLabel: 'Descrierea instrumentului',
|
||||
@ -54,7 +54,7 @@ const translation = {
|
||||
uninstalledLink: 'Gestionați în pluginuri',
|
||||
paramsTip1: 'Controlează parametrii de inferență LLM.',
|
||||
params: 'CONFIGURAREA RAȚIONAMENTULUI',
|
||||
paramsTip2: 'Când "Automat" este dezactivat, se folosește valoarea implicită.',
|
||||
paramsTip2: 'Când "Auto" este dezactivat, se folosește valoarea implicită.',
|
||||
settings: 'SETĂRI UTILIZATOR',
|
||||
unsupportedTitle: 'Acțiune neacceptată',
|
||||
placeholder: 'Selectați un instrument...',
|
||||
|
@ -152,6 +152,62 @@ const translation = {
|
||||
toolNameUsageTip: '工具调用名称,用于 Agent 推理和提示词',
|
||||
copyToolName: '复制名称',
|
||||
noTools: '没有工具',
|
||||
mcp: {
|
||||
create: {
|
||||
cardTitle: '添加 MCP 服务 (HTTP)',
|
||||
cardLink: '了解更多关于 MCP 服务集成的信息',
|
||||
},
|
||||
noConfigured: '未配置',
|
||||
updateTime: '更新于',
|
||||
toolsCount: '{{count}} 个工具',
|
||||
noTools: '没有可用的工具',
|
||||
modal: {
|
||||
title: '添加 MCP 服务 (HTTP)',
|
||||
name: '名称和图标',
|
||||
namePlaceholder: '命名你的 MCP 服务',
|
||||
serverUrl: '服务端点 URL',
|
||||
serverUrlPlaceholder: '服务端点的 URL',
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
confirm: '添加并授权',
|
||||
},
|
||||
delete: '删除 MCP 服务',
|
||||
deleteConfirmTitle: '你想要删除 {{mcp}} 吗?',
|
||||
operation: {
|
||||
edit: '修改',
|
||||
remove: '删除',
|
||||
},
|
||||
authorize: '授权',
|
||||
authorizing: '授权中...',
|
||||
authorizingRequired: '需要授权',
|
||||
authorizeTip: '授权后,工具将显示在这里。',
|
||||
update: '更新',
|
||||
updating: '更新中',
|
||||
gettingTools: '获取工具中...',
|
||||
updateTools: '更新工具中...',
|
||||
toolsEmpty: '工具未加载',
|
||||
getTools: '获取工具',
|
||||
toolsNum: '包含 {{count}} 个工具',
|
||||
onlyTool: '包含 1 个工具',
|
||||
server: {
|
||||
title: 'MCP 服务',
|
||||
url: '服务端点 URL',
|
||||
reGen: '你想要重新生成服务端点 URL 吗?',
|
||||
addDescription: '添加描述',
|
||||
edit: '编辑描述',
|
||||
modal: {
|
||||
addTitle: '添加描述以启用 MCP 服务',
|
||||
editTitle: '编辑 MCP 服务描述',
|
||||
description: '描述',
|
||||
descriptionPlaceholder: '解释此工具的功能以及 LLM 应如何使用它',
|
||||
parameters: '参数',
|
||||
parametersTip: '为每个参数添加描述,以帮助 LLM 理解其目的和约束条件。',
|
||||
parametersPlaceholder: '参数的用途和约束条件',
|
||||
confirm: '启用 MCP 服务',
|
||||
},
|
||||
publishTip: '应用未发布。请先发布应用。',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
@ -230,6 +230,8 @@ const translation = {
|
||||
'utilities': '工具',
|
||||
'noResult': '未找到匹配项',
|
||||
'agent': 'Agent 策略',
|
||||
'allAdded': '已添加全部',
|
||||
'addAll': '添加全部',
|
||||
},
|
||||
blocks: {
|
||||
'start': '开始',
|
||||
@ -877,6 +879,8 @@ const translation = {
|
||||
install: '安装',
|
||||
cancel: '取消',
|
||||
},
|
||||
clickToViewParameterSchema: '点击查看参数 schema',
|
||||
parameterSchema: '参数 Schema',
|
||||
},
|
||||
},
|
||||
tracing: {
|
||||
|
@ -124,6 +124,10 @@ export const fetchAllWorkflowTools = () => {
|
||||
return get<ToolWithProvider[]>('/workspaces/current/tools/workflow')
|
||||
}
|
||||
|
||||
export const fetchAllMCPTools = () => {
|
||||
return get<ToolWithProvider[]>('/workspaces/current/tools/mcp')
|
||||
}
|
||||
|
||||
export const fetchLabelList = () => {
|
||||
return get<Label[]>('/workspaces/current/tool-labels')
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { get, post } from './base'
|
||||
import { del, get, post, put } from './base'
|
||||
import type {
|
||||
Collection,
|
||||
MCPServerDetail,
|
||||
Tool,
|
||||
} from '@/app/components/tools/types'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import { useInvalid } from './use-base'
|
||||
import {
|
||||
useMutation,
|
||||
@ -61,6 +63,191 @@ export const useInvalidateAllWorkflowTools = () => {
|
||||
return useInvalid(useAllWorkflowToolsKey)
|
||||
}
|
||||
|
||||
const useAllMCPToolsKey = [NAME_SPACE, 'MCPTools']
|
||||
export const useAllMCPTools = () => {
|
||||
return useQuery<ToolWithProvider[]>({
|
||||
queryKey: useAllMCPToolsKey,
|
||||
queryFn: () => get<ToolWithProvider[]>('/workspaces/current/tools/mcp'),
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidateAllMCPTools = () => {
|
||||
return useInvalid(useAllMCPToolsKey)
|
||||
}
|
||||
|
||||
export const useCreateMCP = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'create-mcp'],
|
||||
mutationFn: (payload: {
|
||||
name: string
|
||||
server_url: string
|
||||
icon_type: AppIconType
|
||||
icon: string
|
||||
icon_background?: string | null
|
||||
}) => {
|
||||
return post<ToolWithProvider>('workspaces/current/tool-provider/mcp', {
|
||||
body: {
|
||||
...payload,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateMCP = ({
|
||||
onSuccess,
|
||||
}: {
|
||||
onSuccess?: () => void
|
||||
}) => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'update-mcp'],
|
||||
mutationFn: (payload: {
|
||||
name: string
|
||||
server_url: string
|
||||
icon_type: AppIconType
|
||||
icon: string
|
||||
icon_background?: string | null
|
||||
provider_id: string
|
||||
}) => {
|
||||
return put('workspaces/current/tool-provider/mcp', {
|
||||
body: {
|
||||
...payload,
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteMCP = ({
|
||||
onSuccess,
|
||||
}: {
|
||||
onSuccess?: () => void
|
||||
}) => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'delete-mcp'],
|
||||
mutationFn: (id: string) => {
|
||||
return del('/workspaces/current/tool-provider/mcp', {
|
||||
body: {
|
||||
provider_id: id,
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAuthorizeMCP = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'authorize-mcp'],
|
||||
mutationFn: (payload: { provider_id: string; server_url: string }) => {
|
||||
return post<{ result?: string; authorization_url?: string }>('/workspaces/current/tool-provider/mcp/auth', {
|
||||
body: payload,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateMCPAuthorizationToken = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'],
|
||||
mutationFn: (payload: { provider_id: string; server_url: string; authorization_code: string }) => {
|
||||
return get<MCPServerDetail>('/workspaces/current/tool-provider/mcp/token', {
|
||||
params: {
|
||||
...payload,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useMCPTools = (providerID: string) => {
|
||||
return useQuery({
|
||||
enabled: !!providerID,
|
||||
queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID],
|
||||
queryFn: () => get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/tools/${providerID}`),
|
||||
})
|
||||
}
|
||||
export const useInvalidateMCPTools = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return (providerID: string) => {
|
||||
queryClient.invalidateQueries(
|
||||
{
|
||||
queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const useUpdateMCPTools = () => {
|
||||
return useMutation({
|
||||
mutationFn: (providerID: string) => get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/update/${providerID}`),
|
||||
})
|
||||
}
|
||||
|
||||
export const useMCPServerDetail = (appID: string) => {
|
||||
return useQuery<MCPServerDetail>({
|
||||
queryKey: [NAME_SPACE, 'MCPServerDetail', appID],
|
||||
queryFn: () => get<MCPServerDetail>(`/apps/${appID}/server`),
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidateMCPServerDetail = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return (appID: string) => {
|
||||
queryClient.invalidateQueries(
|
||||
{
|
||||
queryKey: [NAME_SPACE, 'MCPServerDetail', appID],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const useCreateMCPServer = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'create-mcp-server'],
|
||||
mutationFn: (payload: {
|
||||
appID: string
|
||||
description: string
|
||||
parameters?: Record<string, string>
|
||||
}) => {
|
||||
const { appID, ...rest } = payload
|
||||
return post(`apps/${appID}/server`, {
|
||||
body: {
|
||||
...rest,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateMCPServer = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'update-mcp-server'],
|
||||
mutationFn: (payload: {
|
||||
appID: string
|
||||
id: string
|
||||
description?: string
|
||||
status?: string
|
||||
parameters?: Record<string, string>
|
||||
}) => {
|
||||
const { appID, ...rest } = payload
|
||||
return put(`apps/${appID}/server`, {
|
||||
body: {
|
||||
...rest,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useRefreshMCPServerCode = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'],
|
||||
mutationFn: (appID: string) => {
|
||||
return get<MCPServerDetail>(`apps/${appID}/server/refresh`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useBuiltinProviderInfo = (providerName: string) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'builtin-provider-info', providerName],
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { del, get, patch, post } from './base'
|
||||
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import type {
|
||||
FetchWorkflowDraftPageParams,
|
||||
FetchWorkflowDraftPageResponse,
|
||||
@ -21,6 +21,16 @@ export const useAppWorkflow = (appID: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidateAppWorkflow = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return (appID: string) => {
|
||||
queryClient.invalidateQueries(
|
||||
{
|
||||
queryKey: [NAME_SPACE, 'publish', appID],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowConfig = (appId: string, onSuccess: (v: WorkflowConfigResponse) => void) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'config', appId],
|
||||
|
@ -71,6 +71,7 @@ const config = {
|
||||
boxShadow: {
|
||||
'xs': '0px 1px 2px 0px rgba(16, 24, 40, 0.05)',
|
||||
'sm': '0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10)',
|
||||
'sm-no-bottom': '0px -1px 2px 0px rgba(16, 24, 40, 0.06), 0px -1px 3px 0px rgba(16, 24, 40, 0.10)',
|
||||
'md': '0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(16, 24, 40, 0.10)',
|
||||
'lg': '0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08)',
|
||||
'xl': '0px 8px 8px -4px rgba(16, 24, 40, 0.03), 0px 20px 24px -4px rgba(16, 24, 40, 0.08)',
|
||||
|
Loading…
x
Reference in New Issue
Block a user