Co-authored-by: jZonG <jzongcode@gmail.com> Co-authored-by: Novice <novice12185727@gmail.com> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: Hanqing Zhao <sherry9277@gmail.com>tags/1.6.0
| @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' | |||
| import { useContext } from 'use-context-selector' | |||
| import AppCard from '@/app/components/app/overview/appCard' | |||
| import Loading from '@/app/components/base/loading' | |||
| import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card' | |||
| import { ToastContext } from '@/app/components/base/toast' | |||
| import { | |||
| fetchAppDetail, | |||
| @@ -31,6 +32,8 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => { | |||
| const appDetail = useAppStore(state => state.appDetail) | |||
| const setAppDetail = useAppStore(state => state.setAppDetail) | |||
| const showMCPCard = isInPanel | |||
| const updateAppDetail = async () => { | |||
| try { | |||
| const res = await fetchAppDetail({ url: '/apps', id: appId }) | |||
| @@ -117,6 +120,11 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => { | |||
| isInPanel={isInPanel} | |||
| onChangeStatus={onChangeApiStatus} | |||
| /> | |||
| {showMCPCard && ( | |||
| <MCPServiceCard | |||
| appInfo={appDetail} | |||
| /> | |||
| )} | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -2,6 +2,10 @@ import React from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import AppIcon from '../base/app-icon' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| import { | |||
| Code, | |||
| WindowCursor, | |||
| } from '@/app/components/base/icons/src/vender/workflow' | |||
| export type IAppBasicProps = { | |||
| iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion' | |||
| @@ -14,25 +18,13 @@ export type IAppBasicProps = { | |||
| textStyle?: { main?: string; extra?: string } | |||
| isExtraInLine?: boolean | |||
| mode?: string | |||
| hideType?: boolean | |||
| } | |||
| const ApiSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path d="M8.5 3.5C8.5 4.60457 9.39543 5.5 10.5 5.5C11.6046 5.5 12.5 4.60457 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.39543 1.5 8.5 2.39543 8.5 3.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> | |||
| <path d="M12.5 9C12.5 10.1046 13.3954 11 14.5 11C15.6046 11 16.5 10.1046 16.5 9C16.5 7.89543 15.6046 7 14.5 7C13.3954 7 12.5 7.89543 12.5 9Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> | |||
| <path d="M8.5 3.5H5.5L3.5 6.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> | |||
| <path d="M8.5 14.5C8.5 15.6046 9.39543 16.5 10.5 16.5C11.6046 16.5 12.5 15.6046 12.5 14.5C12.5 13.3954 11.6046 12.5 10.5 12.5C9.39543 12.5 8.5 13.3954 8.5 14.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> | |||
| <path d="M8.5 14.5H5.5L3.5 11.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> | |||
| <path d="M12.5 9H1.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> | |||
| </svg> | |||
| const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path fillRule="evenodd" clipRule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" /> | |||
| </svg> | |||
| const WebappSvg = <svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path d="M14.375 5.45825L7.99998 8.99992M7.99998 8.99992L1.62498 5.45825M7.99998 8.99992L8 16.1249M14.75 12.0439V5.95603C14.75 5.69904 14.75 5.57055 14.7121 5.45595C14.6786 5.35457 14.6239 5.26151 14.5515 5.18299C14.4697 5.09424 14.3574 5.03184 14.1328 4.90704L8.58277 1.8237C8.37007 1.70553 8.26372 1.64645 8.15109 1.62329C8.05141 1.60278 7.9486 1.60278 7.84891 1.62329C7.73628 1.64645 7.62993 1.70553 7.41723 1.8237L1.86723 4.90704C1.64259 5.03184 1.53026 5.09424 1.44847 5.18299C1.37612 5.26151 1.32136 5.35457 1.28786 5.45595C1.25 5.57055 1.25 5.69904 1.25 5.95603V12.0439C1.25 12.3008 1.25 12.4293 1.28786 12.5439C1.32136 12.6453 1.37612 12.7384 1.44847 12.8169C1.53026 12.9056 1.64259 12.968 1.86723 13.0928L7.41723 16.1762C7.62993 16.2943 7.73628 16.3534 7.84891 16.3766C7.9486 16.3971 8.05141 16.3971 8.15109 16.3766C8.26372 16.3534 8.37007 16.2943 8.58277 16.1762L14.1328 13.0928C14.3574 12.968 14.4697 12.9056 14.5515 12.8169C14.6239 12.7384 14.6786 12.6453 14.7121 12.5439C14.75 12.4293 14.75 12.3008 14.75 12.0439Z" stroke="#155EEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> | |||
| </svg> | |||
| const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <g clipPath="url(#clip0_6294_13848)"> | |||
| <path fill-rule="evenodd" clip-rule="evenodd" d="M4.287 21.9133L1.70748 18.6999C1.08685 17.9267 0.75 16.976 0.75 15.9974V4.36124C0.75 2.89548 1.92269 1.67923 3.43553 1.57594L15.3991 0.759137C16.2682 0.699797 17.1321 0.930818 17.8461 1.41353L22.0494 4.25543C22.8018 4.76414 23.25 5.59574 23.25 6.48319V19.7124C23.25 21.1468 22.0969 22.3345 20.6157 22.4256L7.3375 23.243C6.1555 23.3158 5.01299 22.8178 4.287 21.9133Z" fill="white" /> | |||
| @@ -48,13 +40,17 @@ const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xm | |||
| const ICON_MAP = { | |||
| app: <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />, | |||
| api: <AppIcon innerIcon={ApiSvg} className='border !border-purple-200 !bg-purple-50' />, | |||
| api: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-violet-violet-500 p-1 shadow-md'> | |||
| <Code className='h-4 w-4 text-text-primary-on-surface' /> | |||
| </div>, | |||
| dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />, | |||
| webapp: <AppIcon innerIcon={WebappSvg} className='border !border-primary-200 !bg-primary-100' />, | |||
| webapp: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md'> | |||
| <WindowCursor className='h-4 w-4 text-text-primary-on-surface' /> | |||
| </div>, | |||
| notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />, | |||
| } | |||
| export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, isExtraInLine, mode = 'expand', iconType = 'app' }: IAppBasicProps) { | |||
| export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, isExtraInLine, mode = 'expand', iconType = 'app', hideType }: IAppBasicProps) { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| @@ -88,9 +84,10 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type | |||
| /> | |||
| } | |||
| </div> | |||
| {isExtraInLine ? ( | |||
| {!hideType && isExtraInLine && ( | |||
| <div className="system-2xs-medium-uppercase flex text-text-tertiary">{type}</div> | |||
| ) : ( | |||
| )} | |||
| {!hideType && !isExtraInLine && ( | |||
| <div className='system-2xs-medium-uppercase text-text-tertiary'>{isExternal ? t('dataset.externalTag') : type}</div> | |||
| )} | |||
| </div>} | |||
| @@ -30,15 +30,31 @@ import ConfigCredential from '@/app/components/tools/setting/build-in/config-cre | |||
| import { updateBuiltInToolCredential } from '@/service/tools' | |||
| import cn from '@/utils/classnames' | |||
| import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' | |||
| import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types' | |||
| import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types' | |||
| import { canFindTool } from '@/utils' | |||
| import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools' | |||
| import type { ToolWithProvider } from '@/app/components/workflow/types' | |||
| import { useMittContextSelector } from '@/context/mitt-context' | |||
| type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } | null | |||
| const AgentTools: FC = () => { | |||
| const { t } = useTranslation() | |||
| const [isShowChooseTool, setIsShowChooseTool] = useState(false) | |||
| const { modelConfig, setModelConfig, collectionList } = useContext(ConfigContext) | |||
| const { modelConfig, setModelConfig } = useContext(ConfigContext) | |||
| const { data: buildInTools } = useAllBuiltInTools() | |||
| const { data: customTools } = useAllCustomTools() | |||
| const { data: workflowTools } = useAllWorkflowTools() | |||
| const { data: mcpTools } = useAllMCPTools() | |||
| const collectionList = useMemo(() => { | |||
| const allTools = [ | |||
| ...(buildInTools || []), | |||
| ...(customTools || []), | |||
| ...(workflowTools || []), | |||
| ...(mcpTools || []), | |||
| ] | |||
| return allTools | |||
| }, [buildInTools, customTools, workflowTools, mcpTools]) | |||
| const formattingChangedDispatcher = useFormattingChangedDispatcher() | |||
| const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null) | |||
| const currentCollection = useMemo(() => { | |||
| @@ -96,23 +112,38 @@ const AgentTools: FC = () => { | |||
| } | |||
| const [isDeleting, setIsDeleting] = useState<number>(-1) | |||
| const getToolValue = (tool: ToolDefaultValue) => { | |||
| return { | |||
| provider_id: tool.provider_id, | |||
| provider_type: tool.provider_type as CollectionType, | |||
| provider_name: tool.provider_name, | |||
| tool_name: tool.tool_name, | |||
| tool_label: tool.tool_label, | |||
| tool_parameters: tool.params, | |||
| notAuthor: !tool.is_team_authorization, | |||
| enabled: true, | |||
| } | |||
| } | |||
| const handleSelectTool = (tool: ToolDefaultValue) => { | |||
| const newModelConfig = produce(modelConfig, (draft) => { | |||
| draft.agentConfig.tools.push({ | |||
| provider_id: tool.provider_id, | |||
| provider_type: tool.provider_type as CollectionType, | |||
| provider_name: tool.provider_name, | |||
| tool_name: tool.tool_name, | |||
| tool_label: tool.tool_label, | |||
| tool_parameters: tool.params, | |||
| notAuthor: !tool.is_team_authorization, | |||
| enabled: true, | |||
| }) | |||
| draft.agentConfig.tools.push(getToolValue(tool)) | |||
| }) | |||
| setModelConfig(newModelConfig) | |||
| } | |||
| const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => { | |||
| const newModelConfig = produce(modelConfig, (draft) => { | |||
| draft.agentConfig.tools.push(...tool.map(getToolValue)) | |||
| }) | |||
| setModelConfig(newModelConfig) | |||
| } | |||
| const getProviderShowName = (item: AgentTool) => { | |||
| const type = item.provider_type | |||
| if(type === CollectionType.builtIn) | |||
| return item.provider_name.split('/').pop() | |||
| return item.provider_name | |||
| } | |||
| return ( | |||
| <> | |||
| <Panel | |||
| @@ -143,7 +174,9 @@ const AgentTools: FC = () => { | |||
| disabled={false} | |||
| supportAddCustomTool | |||
| onSelect={handleSelectTool} | |||
| selectedTools={tools as any} | |||
| onSelectMultiple={handleSelectMultipleTool} | |||
| selectedTools={tools as unknown as ToolValue[]} | |||
| canChooseMCPTool | |||
| /> | |||
| </> | |||
| )} | |||
| @@ -161,7 +194,7 @@ const AgentTools: FC = () => { | |||
| <div className='flex w-0 grow items-center'> | |||
| {item.isDeleted && <DefaultToolIcon className='h-5 w-5' />} | |||
| {!item.isDeleted && ( | |||
| <div className={cn((item.notAuthor || !item.enabled) && 'opacity-50')}> | |||
| <div className={cn((item.notAuthor || !item.enabled) && 'shrink-0 opacity-50')}> | |||
| {typeof item.icon === 'string' && <div className='h-5 w-5 rounded-md bg-cover bg-center' style={{ backgroundImage: `url(${item.icon})` }} />} | |||
| {typeof item.icon !== 'string' && <AppIcon className='rounded-md' size='xs' icon={item.icon?.content} background={item.icon?.background} />} | |||
| </div> | |||
| @@ -172,7 +205,7 @@ const AgentTools: FC = () => { | |||
| (item.isDeleted || item.notAuthor || !item.enabled) ? 'opacity-50' : '', | |||
| )} | |||
| > | |||
| <span className='system-xs-medium pr-1.5 text-text-secondary'>{item.provider_type === CollectionType.builtIn ? item.provider_name.split('/').pop() : item.tool_label}</span> | |||
| <span className='system-xs-medium pr-1.5 text-text-secondary'>{getProviderShowName(item)}</span> | |||
| <span className='text-text-tertiary'>{item.tool_label}</span> | |||
| {!item.isDeleted && ( | |||
| <Tooltip | |||
| @@ -285,8 +318,7 @@ const AgentTools: FC = () => { | |||
| <SettingBuiltInTool | |||
| toolName={currentTool?.tool_name as string} | |||
| setting={currentTool?.tool_parameters} | |||
| collection={currentTool?.collection as Collection} | |||
| isBuiltIn={currentTool?.collection?.type === CollectionType.builtIn} | |||
| collection={currentTool?.collection as ToolWithProvider} | |||
| isModel={currentTool?.collection?.type === CollectionType.model} | |||
| onSave={handleToolSettingChange} | |||
| onHide={() => setIsShowSettingTool(false)} | |||
| @@ -24,10 +24,11 @@ import { fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList, fetchWor | |||
| import I18n from '@/context/i18n' | |||
| import { getLanguage } from '@/i18n/language' | |||
| import cn from '@/utils/classnames' | |||
| import type { ToolWithProvider } from '@/app/components/workflow/types' | |||
| type Props = { | |||
| showBackButton?: boolean | |||
| collection: Collection | |||
| collection: Collection | ToolWithProvider | |||
| isBuiltIn?: boolean | |||
| isModel?: boolean | |||
| toolName: string | |||
| @@ -51,9 +52,10 @@ const SettingBuiltInTool: FC<Props> = ({ | |||
| const { locale } = useContext(I18n) | |||
| const language = getLanguage(locale) | |||
| const { t } = useTranslation() | |||
| const [isLoading, setIsLoading] = useState(true) | |||
| const [tools, setTools] = useState<Tool[]>([]) | |||
| const passedTools = (collection as ToolWithProvider).tools | |||
| const hasPassedTools = passedTools?.length > 0 | |||
| const [isLoading, setIsLoading] = useState(!hasPassedTools) | |||
| const [tools, setTools] = useState<Tool[]>(hasPassedTools ? passedTools : []) | |||
| const currTool = tools.find(tool => tool.name === toolName) | |||
| const formSchemas = currTool ? toolParametersToFormSchemas(currTool.parameters) : [] | |||
| const infoSchemas = formSchemas.filter(item => item.form === 'llm') | |||
| @@ -63,7 +65,7 @@ const SettingBuiltInTool: FC<Props> = ({ | |||
| const [currType, setCurrType] = useState('info') | |||
| const isInfoActive = currType === 'info' | |||
| useEffect(() => { | |||
| if (!collection) | |||
| if (!collection || hasPassedTools) | |||
| return | |||
| (async () => { | |||
| @@ -181,6 +181,7 @@ function AppCard({ | |||
| icon={appInfo.icon} | |||
| icon_background={appInfo.icon_background} | |||
| name={basicName} | |||
| hideType | |||
| type={ | |||
| isApp | |||
| ? t('appOverview.overview.appInfo.explanation') | |||
| @@ -18,6 +18,7 @@ export type AppIconProps = { | |||
| imageUrl?: string | null | |||
| className?: string | |||
| innerIcon?: React.ReactNode | |||
| coverElement?: React.ReactNode | |||
| onClick?: () => void | |||
| } | |||
| const appIconVariants = cva( | |||
| @@ -51,6 +52,7 @@ const AppIcon: FC<AppIconProps> = ({ | |||
| imageUrl, | |||
| className, | |||
| innerIcon, | |||
| coverElement, | |||
| onClick, | |||
| }) => { | |||
| const isValidImageIcon = iconType === 'image' && imageUrl | |||
| @@ -65,6 +67,7 @@ const AppIcon: FC<AppIconProps> = ({ | |||
| ? <img src={imageUrl} className="h-full w-full" alt="app icon" /> | |||
| : (innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />)) | |||
| } | |||
| {coverElement} | |||
| </span> | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path d="M28.0049 16C28.0049 20.4183 24.4231 24 20.0049 24C15.5866 24 12.0049 20.4183 12.0049 16C12.0049 11.5817 15.5866 8 20.0049 8C24.4231 8 28.0049 11.5817 28.0049 16Z" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M4.00488 16H6.67155" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M4.00488 9.33334H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M4.00488 22.6667H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M26 22L29.3333 25.3333" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |||
| </svg> | |||
| @@ -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> | |||
| @@ -0,0 +1,36 @@ | |||
| <svg width="204" height="36" viewBox="0 0 204 36" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <g opacity="0.1"> | |||
| <path d="M3.33333 18.3333C3.33333 18.5423 3.64067 18.9056 4.35365 19.2621C5.27603 19.7233 6.58451 20 8 20C9.41547 20 10.7239 19.7233 11.6463 19.2621C12.3593 18.9056 12.6667 18.5423 12.6667 18.3333V16.8858C11.5667 17.5655 9.88487 18 8 18C6.11515 18 4.43331 17.5655 3.33333 16.8858V18.3333ZM12.6667 20.2191C11.5667 20.8988 9.88487 21.3333 8 21.3333C6.11515 21.3333 4.43331 20.8988 3.33333 20.2191V21.6667C3.33333 21.8756 3.64067 22.2389 4.35365 22.5954C5.27603 23.0566 6.58451 23.3333 8 23.3333C9.41547 23.3333 10.7239 23.0566 11.6463 22.5954C12.3593 22.2389 12.6667 21.8756 12.6667 21.6667V20.2191ZM2 21.6667V15C2 13.3431 4.68629 12 8 12C11.3137 12 14 13.3431 14 15V21.6667C14 23.3235 11.3137 24.6667 8 24.6667C4.68629 24.6667 2 23.3235 2 21.6667ZM8 16.6667C9.41547 16.6667 10.7239 16.3899 11.6463 15.9288C12.3593 15.5723 12.6667 15.2089 12.6667 15C12.6667 14.7911 12.3593 14.4277 11.6463 14.0712C10.7239 13.6101 9.41547 13.3333 8 13.3333C6.58451 13.3333 5.27603 13.6101 4.35365 14.0712C3.64067 14.4277 3.33333 14.7911 3.33333 15C3.33333 15.2089 3.64067 15.5723 4.35365 15.9288C5.27603 16.3899 6.58451 16.6667 8 16.6667Z" fill="#101828" fill-opacity="0.3"/> | |||
| </g> | |||
| <g opacity="0.3"> | |||
| <path d="M41.3337 11.3333C41.7019 11.3333 42.0003 11.6318 42.0003 12V15.3333C42.0003 15.7015 41.7019 16 41.3337 16H38.0003V24.6667C38.0003 25.0349 37.7019 25.3333 37.3337 25.3333H34.667C34.2988 25.3333 34.0003 25.0349 34.0003 24.6667V16H30.3337C29.9655 16 29.667 15.7015 29.667 15.3333V13.7454C29.667 13.4929 29.8097 13.262 30.0355 13.1491L33.667 11.3333H41.3337ZM38.0003 12.6667H33.9818L31.0003 14.1574V14.6667H35.3337V24H36.667V14.6667H38.0003V12.6667ZM40.667 12.6667H39.3337V14.6667H40.667V12.6667Z" fill="#101828" fill-opacity="0.3"/> | |||
| </g> | |||
| <g opacity="0.6"> | |||
| <path d="M60.6667 13.3333C60.6667 11.8606 61.8606 10.6667 63.3333 10.6667C64.8061 10.6667 66 11.8606 66 13.3333H69.3333C69.7015 13.3333 70 13.6318 70 14V16.7805C70 16.9969 69.8949 17.1998 69.7183 17.3248C69.5415 17.4497 69.3152 17.4811 69.1112 17.409C68.973 17.3602 68.8237 17.3333 68.6667 17.3333C67.9303 17.3333 67.3333 17.9303 67.3333 18.6667C67.3333 19.4031 67.9303 20 68.6667 20C68.8237 20 68.973 19.9731 69.1112 19.9243C69.3152 19.8522 69.5415 19.8836 69.7183 20.0085C69.8949 20.1335 70 20.3365 70 20.5529V23.3333C70 23.7015 69.7015 24 69.3333 24H58.6667C58.2985 24 58 23.7015 58 23.3333V14C58 13.6318 58.2985 13.3333 58.6667 13.3333H60.6667ZM63.3333 12C62.597 12 62 12.5969 62 13.3333C62 13.4903 62.0269 13.6397 62.0757 13.7778C62.1478 13.9819 62.1164 14.2082 61.9915 14.3849C61.8665 14.5616 61.6635 14.6667 61.4471 14.6667H59.3333V22.6667H68.6667V21.3333C67.1939 21.3333 66 20.1394 66 18.6667C66 17.1939 67.1939 16 68.6667 16V14.6667H65.2195C65.0031 14.6667 64.8002 14.5616 64.6752 14.3849C64.5503 14.2082 64.5189 13.9819 64.591 13.7778C64.6398 13.6397 64.6667 13.4904 64.6667 13.3333C64.6667 12.5969 64.0697 12 63.3333 12Z" fill="#101828" fill-opacity="0.3"/> | |||
| </g> | |||
| <rect x="84.5" y="0.5" width="35" height="35" rx="9.5" stroke="#101828" stroke-opacity="0.04"/> | |||
| <path d="M96.167 16.3333H107.834V25.5H96.167V16.3333Z" stroke="#676F83" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M100.333 19.6667H103.666" stroke="#676F83" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M107.833 12.1667L105.583 15.9167" stroke="#676F83" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |||
| <path d="M97.4888 11.9517C97.5447 11.9238 97.5901 11.8784 97.6181 11.8224L97.9911 11.0765C98.0976 10.8634 98.4017 10.8634 98.5083 11.0765L98.8813 11.8224C98.9092 11.8784 98.9546 11.9238 99.0106 11.9517L99.7565 12.3247C99.9696 12.4313 99.9696 12.7354 99.7565 12.842L99.0106 13.2149C98.9546 13.2429 98.9092 13.2883 98.8813 13.3442L98.5083 14.0902C98.4017 14.3033 98.0976 14.3033 97.9911 14.0902L97.6181 13.3442C97.5901 13.2883 97.5447 13.2429 97.4888 13.2149L96.7429 12.842C96.5297 12.7354 96.5297 12.4313 96.7429 12.3247L97.4888 11.9517Z" fill="#676F83"/> | |||
| <path d="M101.882 10.5438C101.952 10.5089 102.009 10.4521 102.044 10.3822L102.51 9.4498C102.643 9.1834 103.023 9.1834 103.157 9.4498L103.623 10.3822C103.658 10.4521 103.714 10.5089 103.784 10.5438L104.717 11.0101C104.983 11.1432 104.983 11.5234 104.717 11.6566L103.784 12.1228C103.714 12.1578 103.658 12.2145 103.623 12.2845L103.157 13.2169C103.023 13.4833 102.643 13.4833 102.51 13.2169L102.044 12.2845C102.009 12.2145 101.952 12.1578 101.882 12.1228L100.95 11.6566C100.683 11.5234 100.683 11.1432 100.95 11.0101L101.882 10.5438Z" fill="#676F83"/> | |||
| <g opacity="0.6" clip-path="url(#clip0_9296_51042)"> | |||
| <path d="M145.809 14.7521L145.645 15.1292C145.525 15.4053 145.143 15.4053 145.022 15.1292L144.858 14.7521C144.565 14.0796 144.037 13.5443 143.379 13.2514L142.872 13.0261C142.599 12.9044 142.599 12.5059 142.872 12.3841L143.35 12.1714C144.026 11.871 144.563 11.3158 144.851 10.6206L145.02 10.213C145.138 9.92899 145.53 9.92899 145.647 10.213L145.816 10.6206C146.104 11.3158 146.641 11.871 147.317 12.1714L147.795 12.3841C148.069 12.5059 148.069 12.9044 147.795 13.0261L147.289 13.2514C146.63 13.5443 146.102 14.0796 145.809 14.7521ZM138 11.3333C140.712 11.3333 142.951 13.3571 143.289 15.9766L144.79 18.3358C144.889 18.4911 144.869 18.7231 144.64 18.8211L143.334 19.3807V21.3333C143.334 22.0697 142.737 22.6667 142 22.6667H140.668L140.667 24.6667H134.667L134.667 22.2041C134.667 21.4168 134.376 20.6725 133.837 20.0007C133.105 19.0875 132.667 17.9283 132.667 16.6667C132.667 13.7211 135.055 11.3333 138 11.3333ZM138 12.6667C135.791 12.6667 134 14.4575 134 16.6667C134 17.5899 134.312 18.4619 134.878 19.1666C135.607 20.076 136.001 21.1115 136 22.2042L136 23.3333H139.334L139.335 21.3333H142V18.5013L143.033 18.0587L142.005 16.4417L141.967 16.1475C141.711 14.1676 140.017 12.6667 138 12.6667ZM144.993 21.3286L146.103 22.0683C146.88 20.9041 147.334 19.5051 147.334 18.0001C147.334 17.5447 147.292 17.0991 147.213 16.6667L145.917 17C145.972 17.3252 146 17.6593 146 18.0001C146 19.2314 145.629 20.3761 144.993 21.3286Z" fill="#101828" fill-opacity="0.3"/> | |||
| </g> | |||
| <g opacity="0.3"> | |||
| <path fill-rule="evenodd" clip-rule="evenodd" d="M167.149 16.3522L167.251 16.3822L174.224 19.0391L174.317 19.082C174.722 19.308 174.777 19.8792 174.424 20.179L174.341 20.2389L171.817 21.8171L170.239 24.3418C169.962 24.784 169.324 24.7511 169.082 24.3171L169.039 24.2246L166.382 17.2513C166.188 16.742 166.644 16.2421 167.149 16.3522ZM169.812 22.5085L170.767 20.9811L170.811 20.9186C170.858 20.859 170.916 20.8076 170.981 20.7669L172.508 19.8119L168.152 18.1524L169.812 22.5085Z" fill="#101828" fill-opacity="0.3"/> | |||
| <path d="M165.212 20.3978L163.562 22.0475L162.619 21.1048L164.269 19.4551L165.212 20.3978Z" fill="#101828" fill-opacity="0.3"/> | |||
| <path d="M163.666 18H161.333V16.6667H163.666V18Z" fill="#101828" fill-opacity="0.3"/> | |||
| <path d="M165.212 14.2689L164.269 15.2116L162.619 13.5619L163.562 12.6192L165.212 14.2689Z" fill="#101828" fill-opacity="0.3"/> | |||
| <path d="M172.047 13.5619L170.397 15.2116L169.455 14.2689L171.104 12.6192L172.047 13.5619Z" fill="#101828" fill-opacity="0.3"/> | |||
| <path d="M168 13.6667H166.666V11.3333H168V13.6667Z" fill="#101828" fill-opacity="0.3"/> | |||
| </g> | |||
| <g opacity="0.1"> | |||
| <path d="M202.666 23.3333V14.6667L201.333 12H190.666L189.333 14.669V23.3333C189.333 23.7015 189.631 24 190 24H202C202.368 24 202.666 23.7015 202.666 23.3333ZM190.666 16H201.333V22.6667H190.666V16ZM191.49 13.3333H200.509L201.176 14.6667H190.824L191.49 13.3333ZM198 17.3333H194V18.6667H198V17.3333Z" fill="#101828" fill-opacity="0.3"/> | |||
| </g> | |||
| <defs> | |||
| <clipPath id="clip0_9296_51042"> | |||
| <rect width="16" height="16" fill="white" transform="translate(132 10)"/> | |||
| </clipPath> | |||
| </defs> | |||
| </svg> | |||
| @@ -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> | |||
| @@ -0,0 +1,77 @@ | |||
| { | |||
| "icon": { | |||
| "type": "element", | |||
| "isRootNode": true, | |||
| "name": "svg", | |||
| "attributes": { | |||
| "width": "32", | |||
| "height": "32", | |||
| "viewBox": "0 0 32 32", | |||
| "fill": "none", | |||
| "xmlns": "http://www.w3.org/2000/svg" | |||
| }, | |||
| "children": [ | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M28.0049 16C28.0049 20.4183 24.4231 24 20.0049 24C15.5866 24 12.0049 20.4183 12.0049 16C12.0049 11.5817 15.5866 8 20.0049 8C24.4231 8 28.0049 11.5817 28.0049 16Z", | |||
| "stroke": "currentColor", | |||
| "stroke-width": "2", | |||
| "stroke-linecap": "round", | |||
| "stroke-linejoin": "round" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M4.00488 16H6.67155", | |||
| "stroke": "currentColor", | |||
| "stroke-width": "2", | |||
| "stroke-linecap": "round", | |||
| "stroke-linejoin": "round" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M4.00488 9.33334H8.00488", | |||
| "stroke": "currentColor", | |||
| "stroke-width": "2", | |||
| "stroke-linecap": "round", | |||
| "stroke-linejoin": "round" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M4.00488 22.6667H8.00488", | |||
| "stroke": "currentColor", | |||
| "stroke-width": "2", | |||
| "stroke-linecap": "round", | |||
| "stroke-linejoin": "round" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M26 22L29.3333 25.3333", | |||
| "stroke": "currentColor", | |||
| "stroke-width": "2", | |||
| "stroke-linecap": "round", | |||
| "stroke-linejoin": "round" | |||
| }, | |||
| "children": [] | |||
| } | |||
| ] | |||
| }, | |||
| "name": "SearchMenu" | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| // GENERATE BY script | |||
| // DON NOT EDIT IT MANUALLY | |||
| import * as React from 'react' | |||
| import data from './SearchMenu.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 = 'SearchMenu' | |||
| export default Icon | |||
| @@ -9,4 +9,5 @@ export { default as GlobalVariable } from './GlobalVariable' | |||
| export { default as Icon3Dots } from './Icon3Dots' | |||
| export { default as LongArrowLeft } from './LongArrowLeft' | |||
| export { default as LongArrowRight } from './LongArrowRight' | |||
| export { default as SearchMenu } from './SearchMenu' | |||
| export { default as Tools } from './Tools' | |||
| @@ -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" | |||
| } | |||
| @@ -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 | |||
| @@ -0,0 +1,279 @@ | |||
| { | |||
| "icon": { | |||
| "type": "element", | |||
| "isRootNode": true, | |||
| "name": "svg", | |||
| "attributes": { | |||
| "width": "204", | |||
| "height": "36", | |||
| "viewBox": "0 0 204 36", | |||
| "fill": "none", | |||
| "xmlns": "http://www.w3.org/2000/svg" | |||
| }, | |||
| "children": [ | |||
| { | |||
| "type": "element", | |||
| "name": "g", | |||
| "attributes": { | |||
| "opacity": "0.1" | |||
| }, | |||
| "children": [ | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M3.33333 18.3333C3.33333 18.5423 3.64067 18.9056 4.35365 19.2621C5.27603 19.7233 6.58451 20 8 20C9.41547 20 10.7239 19.7233 11.6463 19.2621C12.3593 18.9056 12.6667 18.5423 12.6667 18.3333V16.8858C11.5667 17.5655 9.88487 18 8 18C6.11515 18 4.43331 17.5655 3.33333 16.8858V18.3333ZM12.6667 20.2191C11.5667 20.8988 9.88487 21.3333 8 21.3333C6.11515 21.3333 4.43331 20.8988 3.33333 20.2191V21.6667C3.33333 21.8756 3.64067 22.2389 4.35365 22.5954C5.27603 23.0566 6.58451 23.3333 8 23.3333C9.41547 23.3333 10.7239 23.0566 11.6463 22.5954C12.3593 22.2389 12.6667 21.8756 12.6667 21.6667V20.2191ZM2 21.6667V15C2 13.3431 4.68629 12 8 12C11.3137 12 14 13.3431 14 15V21.6667C14 23.3235 11.3137 24.6667 8 24.6667C4.68629 24.6667 2 23.3235 2 21.6667ZM8 16.6667C9.41547 16.6667 10.7239 16.3899 11.6463 15.9288C12.3593 15.5723 12.6667 15.2089 12.6667 15C12.6667 14.7911 12.3593 14.4277 11.6463 14.0712C10.7239 13.6101 9.41547 13.3333 8 13.3333C6.58451 13.3333 5.27603 13.6101 4.35365 14.0712C3.64067 14.4277 3.33333 14.7911 3.33333 15C3.33333 15.2089 3.64067 15.5723 4.35365 15.9288C5.27603 16.3899 6.58451 16.6667 8 16.6667Z", | |||
| "fill": "currentColor", | |||
| "fill-opacity": "0.3" | |||
| }, | |||
| "children": [] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "g", | |||
| "attributes": { | |||
| "opacity": "0.3" | |||
| }, | |||
| "children": [ | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M41.3337 11.3333C41.7019 11.3333 42.0003 11.6318 42.0003 12V15.3333C42.0003 15.7015 41.7019 16 41.3337 16H38.0003V24.6667C38.0003 25.0349 37.7019 25.3333 37.3337 25.3333H34.667C34.2988 25.3333 34.0003 25.0349 34.0003 24.6667V16H30.3337C29.9655 16 29.667 15.7015 29.667 15.3333V13.7454C29.667 13.4929 29.8097 13.262 30.0355 13.1491L33.667 11.3333H41.3337ZM38.0003 12.6667H33.9818L31.0003 14.1574V14.6667H35.3337V24H36.667V14.6667H38.0003V12.6667ZM40.667 12.6667H39.3337V14.6667H40.667V12.6667Z", | |||
| "fill": "currentColor", | |||
| "fill-opacity": "0.3" | |||
| }, | |||
| "children": [] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "g", | |||
| "attributes": { | |||
| "opacity": "0.6" | |||
| }, | |||
| "children": [ | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M60.6667 13.3333C60.6667 11.8606 61.8606 10.6667 63.3333 10.6667C64.8061 10.6667 66 11.8606 66 13.3333H69.3333C69.7015 13.3333 70 13.6318 70 14V16.7805C70 16.9969 69.8949 17.1998 69.7183 17.3248C69.5415 17.4497 69.3152 17.4811 69.1112 17.409C68.973 17.3602 68.8237 17.3333 68.6667 17.3333C67.9303 17.3333 67.3333 17.9303 67.3333 18.6667C67.3333 19.4031 67.9303 20 68.6667 20C68.8237 20 68.973 19.9731 69.1112 19.9243C69.3152 19.8522 69.5415 19.8836 69.7183 20.0085C69.8949 20.1335 70 20.3365 70 20.5529V23.3333C70 23.7015 69.7015 24 69.3333 24H58.6667C58.2985 24 58 23.7015 58 23.3333V14C58 13.6318 58.2985 13.3333 58.6667 13.3333H60.6667ZM63.3333 12C62.597 12 62 12.5969 62 13.3333C62 13.4903 62.0269 13.6397 62.0757 13.7778C62.1478 13.9819 62.1164 14.2082 61.9915 14.3849C61.8665 14.5616 61.6635 14.6667 61.4471 14.6667H59.3333V22.6667H68.6667V21.3333C67.1939 21.3333 66 20.1394 66 18.6667C66 17.1939 67.1939 16 68.6667 16V14.6667H65.2195C65.0031 14.6667 64.8002 14.5616 64.6752 14.3849C64.5503 14.2082 64.5189 13.9819 64.591 13.7778C64.6398 13.6397 64.6667 13.4904 64.6667 13.3333C64.6667 12.5969 64.0697 12 63.3333 12Z", | |||
| "fill": "currentColor", | |||
| "fill-opacity": "0.3" | |||
| }, | |||
| "children": [] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "rect", | |||
| "attributes": { | |||
| "x": "84.5", | |||
| "y": "0.5", | |||
| "width": "35", | |||
| "height": "35", | |||
| "rx": "9.5", | |||
| "stroke": "#101828", | |||
| "stroke-opacity": "0.04" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M96.167 16.3333H107.834V25.5H96.167V16.3333Z", | |||
| "stroke": "currentColor", | |||
| "stroke-width": "1.5", | |||
| "stroke-linecap": "round", | |||
| "stroke-linejoin": "round" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M100.333 19.6667H103.666", | |||
| "stroke": "currentColor", | |||
| "stroke-width": "1.5", | |||
| "stroke-linecap": "round", | |||
| "stroke-linejoin": "round" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M107.833 12.1667L105.583 15.9167", | |||
| "stroke": "currentColor", | |||
| "stroke-width": "1.5", | |||
| "stroke-linecap": "round", | |||
| "stroke-linejoin": "round" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M97.4888 11.9517C97.5447 11.9238 97.5901 11.8784 97.6181 11.8224L97.9911 11.0765C98.0976 10.8634 98.4017 10.8634 98.5083 11.0765L98.8813 11.8224C98.9092 11.8784 98.9546 11.9238 99.0106 11.9517L99.7565 12.3247C99.9696 12.4313 99.9696 12.7354 99.7565 12.842L99.0106 13.2149C98.9546 13.2429 98.9092 13.2883 98.8813 13.3442L98.5083 14.0902C98.4017 14.3033 98.0976 14.3033 97.9911 14.0902L97.6181 13.3442C97.5901 13.2883 97.5447 13.2429 97.4888 13.2149L96.7429 12.842C96.5297 12.7354 96.5297 12.4313 96.7429 12.3247L97.4888 11.9517Z", | |||
| "fill": "currentColor" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M101.882 10.5438C101.952 10.5089 102.009 10.4521 102.044 10.3822L102.51 9.4498C102.643 9.1834 103.023 9.1834 103.157 9.4498L103.623 10.3822C103.658 10.4521 103.714 10.5089 103.784 10.5438L104.717 11.0101C104.983 11.1432 104.983 11.5234 104.717 11.6566L103.784 12.1228C103.714 12.1578 103.658 12.2145 103.623 12.2845L103.157 13.2169C103.023 13.4833 102.643 13.4833 102.51 13.2169L102.044 12.2845C102.009 12.2145 101.952 12.1578 101.882 12.1228L100.95 11.6566C100.683 11.5234 100.683 11.1432 100.95 11.0101L101.882 10.5438Z", | |||
| "fill": "currentColor" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "g", | |||
| "attributes": { | |||
| "opacity": "0.6", | |||
| "clip-path": "url(#clip0_9296_51042)" | |||
| }, | |||
| "children": [ | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M145.809 14.7521L145.645 15.1292C145.525 15.4053 145.143 15.4053 145.022 15.1292L144.858 14.7521C144.565 14.0796 144.037 13.5443 143.379 13.2514L142.872 13.0261C142.599 12.9044 142.599 12.5059 142.872 12.3841L143.35 12.1714C144.026 11.871 144.563 11.3158 144.851 10.6206L145.02 10.213C145.138 9.92899 145.53 9.92899 145.647 10.213L145.816 10.6206C146.104 11.3158 146.641 11.871 147.317 12.1714L147.795 12.3841C148.069 12.5059 148.069 12.9044 147.795 13.0261L147.289 13.2514C146.63 13.5443 146.102 14.0796 145.809 14.7521ZM138 11.3333C140.712 11.3333 142.951 13.3571 143.289 15.9766L144.79 18.3358C144.889 18.4911 144.869 18.7231 144.64 18.8211L143.334 19.3807V21.3333C143.334 22.0697 142.737 22.6667 142 22.6667H140.668L140.667 24.6667H134.667L134.667 22.2041C134.667 21.4168 134.376 20.6725 133.837 20.0007C133.105 19.0875 132.667 17.9283 132.667 16.6667C132.667 13.7211 135.055 11.3333 138 11.3333ZM138 12.6667C135.791 12.6667 134 14.4575 134 16.6667C134 17.5899 134.312 18.4619 134.878 19.1666C135.607 20.076 136.001 21.1115 136 22.2042L136 23.3333H139.334L139.335 21.3333H142V18.5013L143.033 18.0587L142.005 16.4417L141.967 16.1475C141.711 14.1676 140.017 12.6667 138 12.6667ZM144.993 21.3286L146.103 22.0683C146.88 20.9041 147.334 19.5051 147.334 18.0001C147.334 17.5447 147.292 17.0991 147.213 16.6667L145.917 17C145.972 17.3252 146 17.6593 146 18.0001C146 19.2314 145.629 20.3761 144.993 21.3286Z", | |||
| "fill": "currentColor", | |||
| "fill-opacity": "0.3" | |||
| }, | |||
| "children": [] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "g", | |||
| "attributes": { | |||
| "opacity": "0.3" | |||
| }, | |||
| "children": [ | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "fill-rule": "evenodd", | |||
| "clip-rule": "evenodd", | |||
| "d": "M167.149 16.3522L167.251 16.3822L174.224 19.0391L174.317 19.082C174.722 19.308 174.777 19.8792 174.424 20.179L174.341 20.2389L171.817 21.8171L170.239 24.3418C169.962 24.784 169.324 24.7511 169.082 24.3171L169.039 24.2246L166.382 17.2513C166.188 16.742 166.644 16.2421 167.149 16.3522ZM169.812 22.5085L170.767 20.9811L170.811 20.9186C170.858 20.859 170.916 20.8076 170.981 20.7669L172.508 19.8119L168.152 18.1524L169.812 22.5085Z", | |||
| "fill": "currentColor", | |||
| "fill-opacity": "0.3" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M165.212 20.3978L163.562 22.0475L162.619 21.1048L164.269 19.4551L165.212 20.3978Z", | |||
| "fill": "currentColor", | |||
| "fill-opacity": "0.3" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M163.666 18H161.333V16.6667H163.666V18Z", | |||
| "fill": "currentColor", | |||
| "fill-opacity": "0.3" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M165.212 14.2689L164.269 15.2116L162.619 13.5619L163.562 12.6192L165.212 14.2689Z", | |||
| "fill": "currentColor", | |||
| "fill-opacity": "0.3" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M172.047 13.5619L170.397 15.2116L169.455 14.2689L171.104 12.6192L172.047 13.5619Z", | |||
| "fill": "currentColor", | |||
| "fill-opacity": "0.3" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M168 13.6667H166.666V11.3333H168V13.6667Z", | |||
| "fill": "currentColor", | |||
| "fill-opacity": "0.3" | |||
| }, | |||
| "children": [] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "g", | |||
| "attributes": { | |||
| "opacity": "0.1" | |||
| }, | |||
| "children": [ | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M202.666 23.3333V14.6667L201.333 12H190.666L189.333 14.669V23.3333C189.333 23.7015 189.631 24 190 24H202C202.368 24 202.666 23.7015 202.666 23.3333ZM190.666 16H201.333V22.6667H190.666V16ZM191.49 13.3333H200.509L201.176 14.6667H190.824L191.49 13.3333ZM198 17.3333H194V18.6667H198V17.3333Z", | |||
| "fill": "currentColor", | |||
| "fill-opacity": "0.3" | |||
| }, | |||
| "children": [] | |||
| } | |||
| ] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "defs", | |||
| "attributes": {}, | |||
| "children": [ | |||
| { | |||
| "type": "element", | |||
| "name": "clipPath", | |||
| "attributes": { | |||
| "id": "clip0_9296_51042" | |||
| }, | |||
| "children": [ | |||
| { | |||
| "type": "element", | |||
| "name": "rect", | |||
| "attributes": { | |||
| "width": "16", | |||
| "height": "16", | |||
| "fill": "white", | |||
| "transform": "translate(132 10)" | |||
| }, | |||
| "children": [] | |||
| } | |||
| ] | |||
| } | |||
| ] | |||
| } | |||
| ] | |||
| }, | |||
| "name": "NoToolPlaceholder" | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| // GENERATE BY script | |||
| // DON NOT EDIT IT MANUALLY | |||
| import * as React from 'react' | |||
| import data from './NoToolPlaceholder.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 = 'NoToolPlaceholder' | |||
| export default Icon | |||
| @@ -1,5 +1,7 @@ | |||
| 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 NoToolPlaceholder } from './NoToolPlaceholder' | |||
| export { default as Openai } from './Openai' | |||
| export { default as ReplayLine } from './ReplayLine' | |||
| @@ -0,0 +1,62 @@ | |||
| { | |||
| "icon": { | |||
| "type": "element", | |||
| "isRootNode": true, | |||
| "name": "svg", | |||
| "attributes": { | |||
| "width": "16", | |||
| "height": "16", | |||
| "viewBox": "0 0 16 16", | |||
| "fill": "none", | |||
| "xmlns": "http://www.w3.org/2000/svg" | |||
| }, | |||
| "children": [ | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M1.33325 4.66663C1.33325 3.56206 2.22869 2.66663 3.33325 2.66663H12.6666C13.7712 2.66663 14.6666 3.56206 14.6666 4.66663V8.16663C14.6666 8.53483 14.3681 8.83329 13.9999 8.83329C13.6317 8.83329 13.3333 8.53483 13.3333 8.16663V4.66663C13.3333 4.29844 13.0348 3.99996 12.6666 3.99996H3.33325C2.96507 3.99996 2.66659 4.29844 2.66659 4.66663V12C2.66659 12.3682 2.96507 12.6666 3.33325 12.6666H7.99992C8.36812 12.6666 8.66658 12.9651 8.66658 13.3333C8.66658 13.7015 8.36812 14 7.99992 14H3.33325C2.22869 14 1.33325 13.1046 1.33325 12V4.66663Z", | |||
| "fill": "currentColor" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M3.66659 5.83329C3.66659 6.29353 4.03968 6.66663 4.49992 6.66663C4.96016 6.66663 5.33325 6.29353 5.33325 5.83329C5.33325 5.37305 4.96016 4.99996 4.49992 4.99996C4.03968 4.99996 3.66659 5.37305 3.66659 5.83329Z", | |||
| "fill": "currentColor" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M5.99992 5.83329C5.99992 6.29353 6.37301 6.66663 6.83325 6.66663C7.29352 6.66663 7.66658 6.29353 7.66658 5.83329C7.66658 5.37305 7.29352 4.99996 6.83325 4.99996C6.37301 4.99996 5.99992 5.37305 5.99992 5.83329Z", | |||
| "fill": "currentColor" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M8.33325 5.83329C8.33325 6.29353 8.70632 6.66663 9.16658 6.66663C9.62685 6.66663 9.99992 6.29353 9.99992 5.83329C9.99992 5.37305 9.62685 4.99996 9.16658 4.99996C8.70632 4.99996 8.33325 5.37305 8.33325 5.83329Z", | |||
| "fill": "currentColor" | |||
| }, | |||
| "children": [] | |||
| }, | |||
| { | |||
| "type": "element", | |||
| "name": "path", | |||
| "attributes": { | |||
| "d": "M10.5293 9.69609C10.2933 9.62349 10.0365 9.68729 9.86185 9.86189C9.68725 10.0365 9.62345 10.2934 9.69605 10.5294L11.0294 14.8627C11.1095 15.1231 11.3401 15.3086 11.6116 15.331C11.8832 15.3535 12.1411 15.2085 12.2629 14.9648L13.1635 13.1636L14.9647 12.263C15.2085 12.1411 15.3535 11.8832 15.331 11.6116C15.3085 11.3401 15.1231 11.1096 14.8627 11.0294L10.5293 9.69609Z", | |||
| "fill": "currentColor" | |||
| }, | |||
| "children": [] | |||
| } | |||
| ] | |||
| }, | |||
| "name": "WindowCursor" | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| // GENERATE BY script | |||
| // DON NOT EDIT IT MANUALLY | |||
| import * as React from 'react' | |||
| import data from './WindowCursor.json' | |||
| import IconBase from '@/app/components/base/icons/IconBase' | |||
| import type { IconData } from '@/app/components/base/icons/IconBase' | |||
| const Icon = ( | |||
| { | |||
| ref, | |||
| ...props | |||
| }: React.SVGProps<SVGSVGElement> & { | |||
| ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>; | |||
| }, | |||
| ) => <IconBase {...props} ref={ref} data={data as IconData} /> | |||
| Icon.displayName = 'WindowCursor' | |||
| export default Icon | |||
| @@ -19,3 +19,4 @@ export { default as ParameterExtractor } from './ParameterExtractor' | |||
| export { default as QuestionClassifier } from './QuestionClassifier' | |||
| export { default as TemplatingTransform } from './TemplatingTransform' | |||
| export { default as VariableX } from './VariableX' | |||
| export { default as WindowCursor } from './WindowCursor' | |||
| @@ -64,8 +64,9 @@ import cn from '@/utils/classnames' | |||
| export type PromptEditorProps = { | |||
| instanceId?: string | |||
| compact?: boolean | |||
| wrapperClassName?: string | |||
| className?: string | |||
| placeholder?: string | |||
| placeholder?: string | JSX.Element | |||
| placeholderClassName?: string | |||
| style?: React.CSSProperties | |||
| value?: string | |||
| @@ -85,6 +86,7 @@ export type PromptEditorProps = { | |||
| const PromptEditor: FC<PromptEditorProps> = ({ | |||
| instanceId, | |||
| compact, | |||
| wrapperClassName, | |||
| className, | |||
| placeholder, | |||
| placeholderClassName, | |||
| @@ -147,10 +149,25 @@ const PromptEditor: FC<PromptEditorProps> = ({ | |||
| return ( | |||
| <LexicalComposer initialConfig={{ ...initialConfig, editable }}> | |||
| <div className='relative min-h-5'> | |||
| <div className={cn('relative', wrapperClassName)}> | |||
| <RichTextPlugin | |||
| contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'text-[13px] leading-5' : 'text-sm leading-6'} text-text-secondary`} style={style || {}} />} | |||
| placeholder={<Placeholder value={placeholder} className={cn('truncate', placeholderClassName)} compact={compact} />} | |||
| contentEditable={ | |||
| <ContentEditable | |||
| className={cn( | |||
| 'text-text-secondary outline-none', | |||
| compact ? 'text-[13px] leading-5' : 'text-sm leading-6', | |||
| className, | |||
| )} | |||
| style={style || {}} | |||
| /> | |||
| } | |||
| placeholder={ | |||
| <Placeholder | |||
| value={placeholder} | |||
| className={cn('truncate', placeholderClassName)} | |||
| compact={compact} | |||
| /> | |||
| } | |||
| ErrorBoundary={LexicalErrorBoundary} | |||
| /> | |||
| <ComponentPickerBlock | |||
| @@ -16,7 +16,6 @@ export class CustomTextNode extends TextNode { | |||
| createDOM(config: EditorConfig) { | |||
| const dom = super.createDOM(config) | |||
| dom.classList.add('align-middle') | |||
| return dom | |||
| } | |||
| @@ -8,7 +8,7 @@ const Placeholder = ({ | |||
| className, | |||
| }: { | |||
| compact?: boolean | |||
| value?: string | |||
| value?: string | JSX.Element | |||
| className?: string | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| @@ -1,3 +1,5 @@ | |||
| import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types' | |||
| export type FormValue = Record<string, any> | |||
| export type TypeWithI18N<T = string> = { | |||
| @@ -19,6 +21,8 @@ export enum FormTypeEnum { | |||
| toolSelector = 'tool-selector', | |||
| multiToolSelector = 'array[tools]', | |||
| appSelector = 'app-selector', | |||
| object = 'object', | |||
| array = 'array', | |||
| dynamicSelect = 'dynamic-select', | |||
| } | |||
| @@ -109,6 +113,7 @@ export type FormShowOnObject = { | |||
| } | |||
| export type CredentialFormSchemaBase = { | |||
| name: string | |||
| variable: string | |||
| label: TypeWithI18N | |||
| type: FormTypeEnum | |||
| @@ -118,6 +123,7 @@ export type CredentialFormSchemaBase = { | |||
| show_on: FormShowOnObject[] | |||
| url?: string | |||
| scope?: string | |||
| input_schema?: SchemaRoot | |||
| } | |||
| export type CredentialFormSchemaTextInput = CredentialFormSchemaBase & { | |||
| @@ -54,6 +54,7 @@ type FormProps< | |||
| nodeId?: string | |||
| nodeOutputVars?: NodeOutPutVar[], | |||
| availableNodes?: Node[], | |||
| canChooseMCPTool?: boolean | |||
| } | |||
| function Form< | |||
| @@ -79,6 +80,7 @@ function Form< | |||
| nodeId, | |||
| nodeOutputVars, | |||
| availableNodes, | |||
| canChooseMCPTool, | |||
| }: FormProps<CustomFormSchema>) { | |||
| const language = useLanguage() | |||
| const [changeKey, setChangeKey] = useState('') | |||
| @@ -377,6 +379,7 @@ function Form< | |||
| value={value[variable] || []} | |||
| onChange={item => handleFormChange(variable, item as any)} | |||
| supportCollapse | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| {fieldMoreInfo?.(formSchema)} | |||
| {validating && changeKey === variable && <ValidatingTip />} | |||
| @@ -1,8 +1,9 @@ | |||
| 'use client' | |||
| import { RiCloseLine } from '@remixicon/react' | |||
| import { RiCloseLine, RiSearchLine } from '@remixicon/react' | |||
| import TagsFilter from './tags-filter' | |||
| import ActionButton from '@/app/components/base/action-button' | |||
| import cn from '@/utils/classnames' | |||
| import { RiAddLine } from '@remixicon/react' | |||
| type SearchBoxProps = { | |||
| search: string | |||
| @@ -13,6 +14,9 @@ type SearchBoxProps = { | |||
| size?: 'small' | 'large' | |||
| placeholder?: string | |||
| locale?: string | |||
| supportAddCustomTool?: boolean | |||
| onShowAddCustomCollectionModal?: () => void | |||
| onAddedCustomTool?: () => void | |||
| } | |||
| const SearchBox = ({ | |||
| search, | |||
| @@ -23,46 +27,62 @@ const SearchBox = ({ | |||
| size = 'small', | |||
| placeholder = '', | |||
| locale, | |||
| supportAddCustomTool, | |||
| onShowAddCustomCollectionModal, | |||
| }: SearchBoxProps) => { | |||
| return ( | |||
| <div | |||
| className={cn( | |||
| 'z-[11] flex items-center', | |||
| size === 'large' && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md', | |||
| size === 'small' && 'rounded-lg bg-components-input-bg-normal p-0.5', | |||
| inputClassName, | |||
| )} | |||
| className='z-[11] flex items-center' | |||
| > | |||
| <TagsFilter | |||
| tags={tags} | |||
| onTagsChange={onTagsChange} | |||
| size={size} | |||
| locale={locale} | |||
| /> | |||
| <div className='mx-1 h-3.5 w-[1px] bg-divider-regular'></div> | |||
| <div className='relative flex grow items-center p-1 pl-2'> | |||
| <div className='mr-2 flex w-full items-center'> | |||
| <input | |||
| className={cn( | |||
| 'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none', | |||
| )} | |||
| value={search} | |||
| onChange={(e) => { | |||
| onSearchChange(e.target.value) | |||
| }} | |||
| placeholder={placeholder} | |||
| /> | |||
| { | |||
| search && ( | |||
| <div className='absolute right-2 top-1/2 -translate-y-1/2'> | |||
| <ActionButton onClick={() => onSearchChange('')}> | |||
| <RiCloseLine className='h-4 w-4' /> | |||
| </ActionButton> | |||
| </div> | |||
| ) | |||
| } | |||
| <div className={ | |||
| cn('flex items-center', | |||
| size === 'large' && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md', | |||
| size === 'small' && 'rounded-lg bg-components-input-bg-normal p-0.5', | |||
| inputClassName, | |||
| ) | |||
| }> | |||
| <div className='relative flex grow items-center p-1 pl-2'> | |||
| <div className='mr-2 flex w-full items-center'> | |||
| <RiSearchLine className='mr-1.5 size-4 text-text-placeholder' /> | |||
| <input | |||
| className={cn( | |||
| 'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none', | |||
| )} | |||
| value={search} | |||
| onChange={(e) => { | |||
| onSearchChange(e.target.value) | |||
| }} | |||
| placeholder={placeholder} | |||
| /> | |||
| { | |||
| search && ( | |||
| <div className='absolute right-2 top-1/2 -translate-y-1/2'> | |||
| <ActionButton onClick={() => onSearchChange('')}> | |||
| <RiCloseLine className='h-4 w-4' /> | |||
| </ActionButton> | |||
| </div> | |||
| ) | |||
| } | |||
| </div> | |||
| </div> | |||
| <div className='mx-1 h-3.5 w-[1px] bg-divider-regular'></div> | |||
| <TagsFilter | |||
| tags={tags} | |||
| onTagsChange={onTagsChange} | |||
| size={size} | |||
| locale={locale} | |||
| /> | |||
| </div> | |||
| {supportAddCustomTool && ( | |||
| <div className='flex shrink-0 items-center'> | |||
| <ActionButton | |||
| className='ml-2 rounded-full bg-components-button-primary-bg text-components-button-primary-text hover:bg-components-button-primary-bg hover:text-components-button-primary-text' | |||
| onClick={onShowAddCustomCollectionModal} | |||
| > | |||
| <RiAddLine className='h-4 w-4' /> | |||
| </ActionButton> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -2,9 +2,7 @@ | |||
| import { useState } from 'react' | |||
| import { | |||
| RiArrowDownSLine, | |||
| RiCloseCircleFill, | |||
| RiFilter3Line, | |||
| RiPriceTag3Line, | |||
| } from '@remixicon/react' | |||
| import { | |||
| PortalToFollowElem, | |||
| @@ -57,47 +55,15 @@ const TagsFilter = ({ | |||
| onClick={() => setOpen(v => !v)} | |||
| > | |||
| <div className={cn( | |||
| 'flex cursor-pointer items-center rounded-lg text-text-tertiary hover:bg-state-base-hover', | |||
| size === 'large' && 'h-8 px-2 py-1', | |||
| size === 'small' && 'h-7 py-0.5 pl-1 pr-1.5 ', | |||
| selectedTagsLength && 'text-text-secondary', | |||
| open && 'bg-state-base-hover', | |||
| 'ml-0.5 mr-1.5 flex items-center text-text-tertiary ', | |||
| size === 'large' && 'h-8 py-1', | |||
| size === 'small' && 'h-7 py-0.5 ', | |||
| // selectedTagsLength && 'text-text-secondary', | |||
| // open && 'bg-state-base-hover', | |||
| )}> | |||
| <div className='p-0.5'> | |||
| <RiFilter3Line className='h-4 w-4' /> | |||
| <div className='cursor-pointer rounded-md p-0.5 hover:bg-state-base-hover'> | |||
| <RiPriceTag3Line className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| <div className={cn( | |||
| 'system-sm-medium flex items-center p-1', | |||
| size === 'large' && 'p-1', | |||
| size === 'small' && 'px-0.5 py-1', | |||
| )}> | |||
| { | |||
| !selectedTagsLength && t('pluginTags.allTags') | |||
| } | |||
| { | |||
| !!selectedTagsLength && tags.map(tag => tagsMap[tag].label).slice(0, 2).join(',') | |||
| } | |||
| { | |||
| selectedTagsLength > 2 && ( | |||
| <div className='system-xs-medium ml-1 text-text-tertiary'> | |||
| +{selectedTagsLength - 2} | |||
| </div> | |||
| ) | |||
| } | |||
| </div> | |||
| { | |||
| !!selectedTagsLength && ( | |||
| <RiCloseCircleFill | |||
| className='h-4 w-4 cursor-pointer text-text-quaternary' | |||
| onClick={() => onTagsChange([])} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| !selectedTagsLength && ( | |||
| <RiArrowDownSLine className='h-4 w-4' /> | |||
| ) | |||
| } | |||
| </div> | |||
| </PortalToFollowElemTrigger> | |||
| <PortalToFollowElemContent className='z-[1000]'> | |||
| @@ -13,6 +13,7 @@ import type { Node } from 'reactflow' | |||
| import type { NodeOutPutVar } from '@/app/components/workflow/types' | |||
| import cn from '@/utils/classnames' | |||
| import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general' | |||
| import { useAllMCPTools } from '@/service/use-tools' | |||
| type Props = { | |||
| disabled?: boolean | |||
| @@ -26,6 +27,7 @@ type Props = { | |||
| nodeOutputVars: NodeOutPutVar[], | |||
| availableNodes: Node[], | |||
| nodeId?: string | |||
| canChooseMCPTool?: boolean | |||
| } | |||
| const MultipleToolSelector = ({ | |||
| @@ -40,9 +42,16 @@ const MultipleToolSelector = ({ | |||
| nodeOutputVars, | |||
| availableNodes, | |||
| nodeId, | |||
| canChooseMCPTool, | |||
| }: Props) => { | |||
| const { t } = useTranslation() | |||
| const enabledCount = value.filter(item => item.enabled).length | |||
| const { data: mcpTools } = useAllMCPTools() | |||
| const enabledCount = value.filter((item) => { | |||
| const isMCPTool = mcpTools?.find(tool => tool.id === item.provider_name) | |||
| if(isMCPTool) | |||
| return item.enabled && canChooseMCPTool | |||
| return item.enabled | |||
| }).length | |||
| // collapse control | |||
| const [collapse, setCollapse] = React.useState(false) | |||
| const handleCollapse = () => { | |||
| @@ -66,6 +75,19 @@ const MultipleToolSelector = ({ | |||
| setOpen(false) | |||
| } | |||
| const handleAddMultiple = (val: ToolValue[]) => { | |||
| const newValue = [...value, ...val] | |||
| // deduplication | |||
| const deduplication = newValue.reduce((acc, cur) => { | |||
| if (!acc.find(item => item.provider_name === cur.provider_name && item.tool_name === cur.tool_name)) | |||
| acc.push(cur) | |||
| return acc | |||
| }, [] as ToolValue[]) | |||
| // update value | |||
| onChange(deduplication) | |||
| setOpen(false) | |||
| } | |||
| // delete tool | |||
| const handleDelete = (index: number) => { | |||
| const newValue = [...value] | |||
| @@ -140,8 +162,10 @@ const MultipleToolSelector = ({ | |||
| value={item} | |||
| selectedTools={value} | |||
| onSelect={item => handleConfigure(item, index)} | |||
| onSelectMultiple={handleAddMultiple} | |||
| onDelete={() => handleDelete(index)} | |||
| supportEnableSwitch | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| isEdit | |||
| /> | |||
| </div> | |||
| @@ -164,6 +188,8 @@ const MultipleToolSelector = ({ | |||
| panelShowState={panelShowState} | |||
| onPanelShowStateChange={setPanelShowState} | |||
| isEdit={false} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| onSelectMultiple={handleAddMultiple} | |||
| /> | |||
| </> | |||
| ) | |||
| @@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next' | |||
| import Link from 'next/link' | |||
| import { | |||
| RiArrowLeftLine, | |||
| RiArrowRightUpLine, | |||
| } from '@remixicon/react' | |||
| import { | |||
| PortalToFollowElem, | |||
| @@ -15,6 +14,7 @@ import { | |||
| import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-trigger' | |||
| import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item' | |||
| import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' | |||
| import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form' | |||
| import Button from '@/app/components/base/button' | |||
| import Indicator from '@/app/components/header/indicator' | |||
| import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form' | |||
| @@ -23,13 +23,13 @@ import Textarea from '@/app/components/base/textarea' | |||
| import Divider from '@/app/components/base/divider' | |||
| import TabSlider from '@/app/components/base/tab-slider-plain' | |||
| import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form' | |||
| import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' | |||
| import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { | |||
| useAllBuiltInTools, | |||
| useAllCustomTools, | |||
| useAllMCPTools, | |||
| useAllWorkflowTools, | |||
| useInvalidateAllBuiltInTools, | |||
| useUpdateProviderCredentials, | |||
| @@ -54,15 +54,9 @@ type Props = { | |||
| scope?: string | |||
| value?: ToolValue | |||
| selectedTools?: ToolValue[] | |||
| onSelect: (tool: ToolValue) => void | |||
| onSelectMultiple: (tool: ToolValue[]) => void | |||
| isEdit?: boolean | |||
| onSelect: (tool: { | |||
| provider_name: string | |||
| tool_name: string | |||
| tool_label: string | |||
| settings?: Record<string, any> | |||
| parameters?: Record<string, any> | |||
| extra?: Record<string, any> | |||
| }) => void | |||
| onDelete?: () => void | |||
| supportEnableSwitch?: boolean | |||
| supportAddCustomTool?: boolean | |||
| @@ -74,6 +68,7 @@ type Props = { | |||
| nodeOutputVars: NodeOutPutVar[], | |||
| availableNodes: Node[], | |||
| nodeId?: string, | |||
| canChooseMCPTool?: boolean, | |||
| } | |||
| const ToolSelector: FC<Props> = ({ | |||
| value, | |||
| @@ -83,6 +78,7 @@ const ToolSelector: FC<Props> = ({ | |||
| placement = 'left', | |||
| offset = 4, | |||
| onSelect, | |||
| onSelectMultiple, | |||
| onDelete, | |||
| scope, | |||
| supportEnableSwitch, | |||
| @@ -94,6 +90,7 @@ const ToolSelector: FC<Props> = ({ | |||
| nodeOutputVars, | |||
| availableNodes, | |||
| nodeId = '', | |||
| canChooseMCPTool, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const [isShow, onShowChange] = useState(false) | |||
| @@ -105,6 +102,7 @@ const ToolSelector: FC<Props> = ({ | |||
| const { data: buildInTools } = useAllBuiltInTools() | |||
| const { data: customTools } = useAllCustomTools() | |||
| const { data: workflowTools } = useAllWorkflowTools() | |||
| const { data: mcpTools } = useAllMCPTools() | |||
| const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools() | |||
| const invalidateInstalledPluginList = useInvalidateInstalledPluginList() | |||
| @@ -112,18 +110,19 @@ const ToolSelector: FC<Props> = ({ | |||
| const { inMarketPlace, manifest } = usePluginInstalledCheck(value?.provider_name) | |||
| const currentProvider = useMemo(() => { | |||
| const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])] | |||
| const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])] | |||
| return mergedTools.find((toolWithProvider) => { | |||
| return toolWithProvider.id === value?.provider_name | |||
| }) | |||
| }, [value, buildInTools, customTools, workflowTools]) | |||
| }, [value, buildInTools, customTools, workflowTools, mcpTools]) | |||
| const [isShowChooseTool, setIsShowChooseTool] = useState(false) | |||
| const handleSelectTool = (tool: ToolDefaultValue) => { | |||
| const getToolValue = (tool: ToolDefaultValue) => { | |||
| const settingValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any)) | |||
| const paramValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any), true) | |||
| const toolValue = { | |||
| return { | |||
| provider_name: tool.provider_id, | |||
| provider_show_name: tool.provider_name, | |||
| type: tool.provider_type, | |||
| tool_name: tool.tool_name, | |||
| tool_label: tool.tool_label, | |||
| @@ -136,9 +135,16 @@ const ToolSelector: FC<Props> = ({ | |||
| }, | |||
| schemas: tool.paramSchemas, | |||
| } | |||
| } | |||
| const handleSelectTool = (tool: ToolDefaultValue) => { | |||
| const toolValue = getToolValue(tool) | |||
| onSelect(toolValue) | |||
| // setIsShowChooseTool(false) | |||
| } | |||
| const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => { | |||
| const toolValues = tool.map(item => getToolValue(item)) | |||
| onSelectMultiple(toolValues) | |||
| } | |||
| const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { | |||
| onSelect({ | |||
| @@ -169,7 +175,6 @@ const ToolSelector: FC<Props> = ({ | |||
| const handleSettingsFormChange = (v: Record<string, any>) => { | |||
| const newValue = getStructureValue(v) | |||
| const toolValue = { | |||
| ...value, | |||
| settings: newValue, | |||
| @@ -250,7 +255,9 @@ const ToolSelector: FC<Props> = ({ | |||
| <ToolItem | |||
| open={isShow} | |||
| icon={currentProvider?.icon || manifestIcon} | |||
| isMCPTool={currentProvider?.type === CollectionType.mcp} | |||
| providerName={value.provider_name} | |||
| providerShowName={value.provider_show_name} | |||
| toolLabel={value.tool_label || value.tool_name} | |||
| showSwitch={supportEnableSwitch} | |||
| switchValue={value.enabled} | |||
| @@ -272,6 +279,7 @@ const ToolSelector: FC<Props> = ({ | |||
| </p> | |||
| </div> | |||
| } | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| )} | |||
| </PortalToFollowElemTrigger> | |||
| @@ -285,7 +293,6 @@ const ToolSelector: FC<Props> = ({ | |||
| <div className='flex flex-col gap-1'> | |||
| <div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div> | |||
| <ToolPicker | |||
| panelClassName='w-[328px]' | |||
| placement='bottom' | |||
| offset={offset} | |||
| trigger={ | |||
| @@ -300,8 +307,10 @@ const ToolSelector: FC<Props> = ({ | |||
| disabled={false} | |||
| supportAddCustomTool | |||
| onSelect={handleSelectTool} | |||
| onSelectMultiple={handleSelectMultipleTool} | |||
| scope={scope} | |||
| selectedTools={selectedTools} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| </div> | |||
| <div className='flex flex-col gap-1'> | |||
| @@ -390,24 +399,13 @@ const ToolSelector: FC<Props> = ({ | |||
| {/* user settings form */} | |||
| {(currType === 'settings' || userSettingsOnly) && ( | |||
| <div className='px-4 py-2'> | |||
| <Form | |||
| <ToolForm | |||
| inPanel | |||
| readOnly={false} | |||
| nodeId={nodeId} | |||
| schema={settingsFormSchemas as any} | |||
| value={getPlainValue(value?.settings || {})} | |||
| onChange={handleSettingsFormChange} | |||
| formSchemas={settingsFormSchemas as any} | |||
| isEditMode={true} | |||
| showOnVariableMap={{}} | |||
| validating={false} | |||
| inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover' | |||
| fieldMoreInfo={item => item.url | |||
| ? (<a | |||
| href={item.url} | |||
| target='_blank' rel='noopener noreferrer' | |||
| className='inline-flex items-center text-xs text-text-accent' | |||
| > | |||
| {t('tools.howToGet')} | |||
| <RiArrowRightUpLine className='ml-1 h-3 w-3' /> | |||
| </a>) | |||
| : null} | |||
| /> | |||
| </div> | |||
| )} | |||
| @@ -3,25 +3,34 @@ import { useTranslation } from 'react-i18next' | |||
| import produce from 'immer' | |||
| import { | |||
| RiArrowRightUpLine, | |||
| RiBracesLine, | |||
| } from '@remixicon/react' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| import Switch from '@/app/components/base/switch' | |||
| import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var' | |||
| import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input' | |||
| import Input from '@/app/components/base/input' | |||
| import FormInputTypeSwitch from '@/app/components/workflow/nodes/_base/components/form-input-type-switch' | |||
| import FormInputBoolean from '@/app/components/workflow/nodes/_base/components/form-input-boolean' | |||
| import { SimpleSelect } from '@/app/components/base/select' | |||
| import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' | |||
| import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker' | |||
| import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' | |||
| import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' | |||
| import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' | |||
| import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' | |||
| import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' | |||
| import type { Node } from 'reactflow' | |||
| import type { | |||
| NodeOutPutVar, | |||
| ValueSelector, | |||
| Var, | |||
| } from '@/app/components/workflow/types' | |||
| import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types' | |||
| import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' | |||
| import { VarType } from '@/app/components/workflow/types' | |||
| import cn from '@/utils/classnames' | |||
| import { useBoolean } from 'ahooks' | |||
| import SchemaModal from './schema-modal' | |||
| import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types' | |||
| type Props = { | |||
| value: Record<string, any> | |||
| @@ -42,73 +51,46 @@ const ReasoningConfigForm: React.FC<Props> = ({ | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const language = useLanguage() | |||
| const handleAutomatic = (key: string, val: any) => { | |||
| const getVarKindType = (type: FormTypeEnum) => { | |||
| if (type === FormTypeEnum.file || type === FormTypeEnum.files) | |||
| return VarKindType.variable | |||
| if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber || type === FormTypeEnum.array || type === FormTypeEnum.object) | |||
| return VarKindType.constant | |||
| if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput) | |||
| return VarKindType.mixed | |||
| } | |||
| const handleAutomatic = (key: string, val: any, type: FormTypeEnum) => { | |||
| onChange({ | |||
| ...value, | |||
| [key]: { | |||
| value: val ? null : value[key]?.value, | |||
| value: val ? null : { type: getVarKindType(type), value: null }, | |||
| auto: val ? 1 : 0, | |||
| }, | |||
| }) | |||
| } | |||
| const [inputsIsFocus, setInputsIsFocus] = useState<Record<string, boolean>>({}) | |||
| const handleInputFocus = useCallback((variable: string) => { | |||
| return (value: boolean) => { | |||
| setInputsIsFocus((prev) => { | |||
| return { | |||
| ...prev, | |||
| [variable]: value, | |||
| } | |||
| }) | |||
| } | |||
| }, []) | |||
| const handleNotMixedTypeChange = useCallback((variable: string) => { | |||
| return (varValue: ValueSelector | string, varKindType: VarKindType) => { | |||
| const newValue = produce(value, (draft: ToolVarInputs) => { | |||
| const target = draft[variable].value | |||
| if (target) { | |||
| target.type = varKindType | |||
| target.value = varValue | |||
| } | |||
| else { | |||
| draft[variable].value = { | |||
| type: varKindType, | |||
| value: varValue, | |||
| } | |||
| } | |||
| }) | |||
| onChange(newValue) | |||
| } | |||
| }, [value, onChange]) | |||
| const handleMixedTypeChange = useCallback((variable: string) => { | |||
| return (itemValue: string) => { | |||
| const newValue = produce(value, (draft: ToolVarInputs) => { | |||
| const target = draft[variable].value | |||
| if (target) { | |||
| target.value = itemValue | |||
| } | |||
| else { | |||
| draft[variable].value = { | |||
| type: VarKindType.mixed, | |||
| value: itemValue, | |||
| } | |||
| const handleTypeChange = useCallback((variable: string, defaultValue: any) => { | |||
| return (newType: VarKindType) => { | |||
| const res = produce(value, (draft: ToolVarInputs) => { | |||
| draft[variable].value = { | |||
| type: newType, | |||
| value: newType === VarKindType.variable ? '' : defaultValue, | |||
| } | |||
| }) | |||
| onChange(newValue) | |||
| onChange(res) | |||
| } | |||
| }, [value, onChange]) | |||
| const handleFileChange = useCallback((variable: string) => { | |||
| return (varValue: ValueSelector | string) => { | |||
| const newValue = produce(value, (draft: ToolVarInputs) => { | |||
| }, [onChange, value]) | |||
| const handleValueChange = useCallback((variable: string, varType: FormTypeEnum) => { | |||
| return (newValue: any) => { | |||
| const res = produce(value, (draft: ToolVarInputs) => { | |||
| draft[variable].value = { | |||
| type: VarKindType.variable, | |||
| value: varValue, | |||
| type: getVarKindType(varType), | |||
| value: newValue, | |||
| } | |||
| }) | |||
| onChange(newValue) | |||
| onChange(res) | |||
| } | |||
| }, [value, onChange]) | |||
| }, [onChange, value]) | |||
| const handleAppChange = useCallback((variable: string) => { | |||
| return (app: { | |||
| app_id: string | |||
| @@ -132,9 +114,29 @@ const ReasoningConfigForm: React.FC<Props> = ({ | |||
| onChange(newValue) | |||
| } | |||
| }, [onChange, value]) | |||
| const handleVariableSelectorChange = useCallback((variable: string) => { | |||
| return (newValue: ValueSelector | string) => { | |||
| const res = produce(value, (draft: ToolVarInputs) => { | |||
| draft[variable].value = { | |||
| type: VarKindType.variable, | |||
| value: newValue, | |||
| } | |||
| }) | |||
| onChange(res) | |||
| } | |||
| }, [onChange, value]) | |||
| const renderField = (schema: any) => { | |||
| const [isShowSchema, { | |||
| setTrue: showSchema, | |||
| setFalse: hideSchema, | |||
| }] = useBoolean(false) | |||
| const [schema, setSchema] = useState<SchemaRoot | null>(null) | |||
| const [schemaRootName, setSchemaRootName] = useState<string>('') | |||
| const renderField = (schema: any, showSchema: (schema: SchemaRoot, rootName: string) => void) => { | |||
| const { | |||
| default: defaultValue, | |||
| variable, | |||
| label, | |||
| required, | |||
| @@ -142,6 +144,9 @@ const ReasoningConfigForm: React.FC<Props> = ({ | |||
| type, | |||
| scope, | |||
| url, | |||
| input_schema, | |||
| placeholder, | |||
| options, | |||
| } = schema | |||
| const auto = value[variable]?.auto | |||
| const tooltipContent = (tooltip && ( | |||
| @@ -149,89 +154,150 @@ const ReasoningConfigForm: React.FC<Props> = ({ | |||
| popupContent={<div className='w-[200px]'> | |||
| {tooltip[language] || tooltip.en_US} | |||
| </div>} | |||
| triggerClassName='ml-1 w-4 h-4' | |||
| triggerClassName='ml-0.5 w-4 h-4' | |||
| asChild={false} /> | |||
| )) | |||
| const varInput = value[variable].value | |||
| const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput | |||
| const isNumber = type === FormTypeEnum.textNumber | |||
| const isSelect = type === FormTypeEnum.select | |||
| const isObject = type === FormTypeEnum.object | |||
| const isArray = type === FormTypeEnum.array | |||
| const isShowJSONEditor = isObject || isArray | |||
| const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files | |||
| const isBoolean = type === FormTypeEnum.boolean | |||
| const isSelect = type === FormTypeEnum.select | |||
| const isAppSelector = type === FormTypeEnum.appSelector | |||
| const isModelSelector = type === FormTypeEnum.modelSelector | |||
| // const isToolSelector = type === FormTypeEnum.toolSelector | |||
| const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector | |||
| const showTypeSwitch = isNumber || isObject || isArray | |||
| const isConstant = varInput?.type === VarKindType.constant || !varInput?.type | |||
| const showVariableSelector = isFile || varInput?.type === VarKindType.variable | |||
| const targetVarType = () => { | |||
| if (isString) | |||
| return VarType.string | |||
| else if (isNumber) | |||
| return VarType.number | |||
| else if (type === FormTypeEnum.files) | |||
| return VarType.arrayFile | |||
| else if (type === FormTypeEnum.file) | |||
| return VarType.file | |||
| else if (isBoolean) | |||
| return VarType.boolean | |||
| else if (isObject) | |||
| return VarType.object | |||
| else if (isArray) | |||
| return VarType.arrayObject | |||
| else | |||
| return VarType.string | |||
| } | |||
| const getFilterVar = () => { | |||
| if (isNumber) | |||
| return (varPayload: any) => varPayload.type === VarType.number | |||
| else if (isString) | |||
| return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type) | |||
| else if (isFile) | |||
| return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type) | |||
| else if (isBoolean) | |||
| return (varPayload: any) => varPayload.type === VarType.boolean | |||
| else if (isObject) | |||
| return (varPayload: any) => varPayload.type === VarType.object | |||
| else if (isArray) | |||
| return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type) | |||
| return undefined | |||
| } | |||
| return ( | |||
| <div key={variable} className='space-y-1'> | |||
| <div key={variable} className='space-y-0.5'> | |||
| <div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'> | |||
| <div className='flex items-center space-x-2'> | |||
| <span className={cn('code-sm-semibold text-text-secondary')}>{label[language] || label.en_US}</span> | |||
| <div className='flex items-center'> | |||
| <span className={cn('code-sm-semibold max-w-[140px] truncate text-text-secondary')} title={label[language] || label.en_US}>{label[language] || label.en_US}</span> | |||
| {required && ( | |||
| <span className='ml-1 text-red-500'>*</span> | |||
| )} | |||
| {tooltipContent} | |||
| <span className='system-xs-regular mx-1 text-text-quaternary'>·</span> | |||
| <span className='system-xs-regular text-text-tertiary'>{targetVarType()}</span> | |||
| {isShowJSONEditor && ( | |||
| <Tooltip | |||
| popupContent={<div className='system-xs-medium text-text-secondary'> | |||
| {t('workflow.nodes.agent.clickToViewParameterSchema')} | |||
| </div>} | |||
| asChild={false}> | |||
| <div | |||
| className='ml-0.5 cursor-pointer rounded-[4px] p-px text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary' | |||
| onClick={() => showSchema(input_schema as SchemaRoot, label[language] || label.en_US)} | |||
| > | |||
| <RiBracesLine className='size-3.5'/> | |||
| </div> | |||
| </Tooltip> | |||
| )} | |||
| </div> | |||
| <div className='flex cursor-pointer items-center gap-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter px-2 py-1 hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto)}> | |||
| <div className='flex cursor-pointer items-center gap-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter px-2 py-1 hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto, type)}> | |||
| <span className='system-xs-medium text-text-secondary'>{t('plugin.detailPanel.toolSelector.auto')}</span> | |||
| <Switch | |||
| size='xs' | |||
| defaultValue={!!auto} | |||
| onChange={val => handleAutomatic(variable, val)} | |||
| onChange={val => handleAutomatic(variable, val, type)} | |||
| /> | |||
| </div> | |||
| </div> | |||
| {auto === 0 && ( | |||
| <> | |||
| <div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}> | |||
| {showTypeSwitch && ( | |||
| <FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange(variable, defaultValue)}/> | |||
| )} | |||
| {isString && ( | |||
| <Input | |||
| className={cn(inputsIsFocus[variable] ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'rounded-lg border px-3 py-[6px]')} | |||
| <MixedVariableTextInput | |||
| value={varInput?.value as string || ''} | |||
| onChange={handleMixedTypeChange(variable)} | |||
| onChange={handleValueChange(variable, type)} | |||
| nodesOutputVars={nodeOutputVars} | |||
| availableNodes={availableNodes} | |||
| onFocusChange={handleInputFocus(variable)} | |||
| placeholder={t('workflow.nodes.http.insertVarPlaceholder')!} | |||
| placeholderClassName='!leading-[21px]' | |||
| /> | |||
| )} | |||
| {/* {isString && ( | |||
| <VarReferencePicker | |||
| zIndex={1001} | |||
| readonly={false} | |||
| isShowNodeName | |||
| nodeId={nodeId} | |||
| {isNumber && isConstant && ( | |||
| <Input | |||
| className='h-8 grow' | |||
| type='number' | |||
| value={varInput?.value || ''} | |||
| onChange={handleNotMixedTypeChange(variable)} | |||
| defaultVarKindType={VarKindType.variable} | |||
| filterVar={(varPayload: Var) => varPayload.type === VarType.number || varPayload.type === VarType.secret || varPayload.type === VarType.string} | |||
| onChange={handleValueChange(variable, type)} | |||
| placeholder={placeholder?.[language] || placeholder?.en_US} | |||
| /> | |||
| )} */} | |||
| {(isNumber || isSelect) && ( | |||
| <VarReferencePicker | |||
| zIndex={1001} | |||
| readonly={false} | |||
| isShowNodeName | |||
| nodeId={nodeId} | |||
| value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])} | |||
| onChange={handleNotMixedTypeChange(variable)} | |||
| defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)} | |||
| isSupportConstantValue | |||
| filterVar={isNumber ? (varPayload: Var) => varPayload.type === schema._type : undefined} | |||
| availableVars={isSelect ? nodeOutputVars : undefined} | |||
| schema={schema} | |||
| )} | |||
| {isBoolean && ( | |||
| <FormInputBoolean | |||
| value={varInput?.value as boolean} | |||
| onChange={handleValueChange(variable, type)} | |||
| /> | |||
| )} | |||
| {isFile && ( | |||
| <VarReferencePicker | |||
| zIndex={1001} | |||
| readonly={false} | |||
| isShowNodeName | |||
| nodeId={nodeId} | |||
| value={varInput?.value || []} | |||
| onChange={handleFileChange(variable)} | |||
| defaultVarKindType={VarKindType.variable} | |||
| filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile} | |||
| {isSelect && ( | |||
| <SimpleSelect | |||
| wrapperClassName='h-8 grow' | |||
| defaultValue={varInput?.value} | |||
| items={options.filter((option: { show_on: any[] }) => { | |||
| if (option.show_on.length) | |||
| return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value) | |||
| return true | |||
| }).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))} | |||
| onSelect={item => handleValueChange(variable, type)(item.value as string)} | |||
| placeholder={placeholder?.[language] || placeholder?.en_US} | |||
| /> | |||
| )} | |||
| {isShowJSONEditor && isConstant && ( | |||
| <div className='mt-1 w-full'> | |||
| <CodeEditor | |||
| title='JSON' | |||
| value={varInput?.value as any} | |||
| isExpand | |||
| isInNode | |||
| height={100} | |||
| language={CodeLanguage.json} | |||
| onChange={handleValueChange(variable, type)} | |||
| className='w-full' | |||
| placeholder={<div className='whitespace-pre'>{placeholder?.[language] || placeholder?.en_US}</div>} | |||
| /> | |||
| </div> | |||
| )} | |||
| {isAppSelector && ( | |||
| <AppSelector | |||
| disabled={false} | |||
| @@ -250,7 +316,21 @@ const ReasoningConfigForm: React.FC<Props> = ({ | |||
| scope={scope} | |||
| /> | |||
| )} | |||
| </> | |||
| {showVariableSelector && ( | |||
| <VarReferencePicker | |||
| zIndex={1001} | |||
| className='h-8 grow' | |||
| readonly={false} | |||
| isShowNodeName | |||
| nodeId={nodeId} | |||
| value={varInput?.value || []} | |||
| onChange={handleVariableSelectorChange(variable)} | |||
| filterVar={getFilterVar()} | |||
| schema={schema} | |||
| valueTypePlaceHolder={targetVarType()} | |||
| /> | |||
| )} | |||
| </div> | |||
| )} | |||
| {url && ( | |||
| <a | |||
| @@ -267,7 +347,19 @@ const ReasoningConfigForm: React.FC<Props> = ({ | |||
| } | |||
| return ( | |||
| <div className='space-y-3 px-4 py-2'> | |||
| {schemas.map(schema => renderField(schema))} | |||
| {!isShowSchema && schemas.map(schema => renderField(schema, (s: SchemaRoot, rootName: string) => { | |||
| setSchema(s) | |||
| setSchemaRootName(rootName) | |||
| showSchema() | |||
| }))} | |||
| {isShowSchema && ( | |||
| <SchemaModal | |||
| isShow={isShowSchema} | |||
| schema={schema!} | |||
| rootName={schemaRootName} | |||
| onClose={hideSchema} | |||
| /> | |||
| )} | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -0,0 +1,59 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import Modal from '@/app/components/base/modal' | |||
| import VisualEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor' | |||
| import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types' | |||
| import { MittProvider, VisualEditorContextProvider } from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { RiCloseLine } from '@remixicon/react' | |||
| type Props = { | |||
| isShow: boolean | |||
| schema: SchemaRoot | |||
| rootName: string | |||
| onClose: () => void | |||
| } | |||
| const SchemaModal: FC<Props> = ({ | |||
| isShow, | |||
| schema, | |||
| rootName, | |||
| onClose, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <Modal | |||
| isShow={isShow} | |||
| onClose={onClose} | |||
| className='max-w-[960px] p-0' | |||
| wrapperClassName='z-[9999]' | |||
| > | |||
| <div className='pb-6'> | |||
| {/* Header */} | |||
| <div className='relative flex p-6 pb-3 pr-14'> | |||
| <div className='title-2xl-semi-bold grow truncate text-text-primary'> | |||
| {t('workflow.nodes.agent.parameterSchema')} | |||
| </div> | |||
| <div className='absolute right-5 top-5 flex h-8 w-8 items-center justify-center p-1.5' onClick={onClose}> | |||
| <RiCloseLine className='h-[18px] w-[18px] text-text-tertiary' /> | |||
| </div> | |||
| </div> | |||
| {/* Content */} | |||
| <div className='flex max-h-[700px] overflow-y-auto px-6 py-2'> | |||
| <MittProvider> | |||
| <VisualEditorContextProvider> | |||
| <VisualEditor | |||
| className='w-full' | |||
| schema={schema} | |||
| rootName={rootName} | |||
| readOnly | |||
| ></VisualEditor> | |||
| </VisualEditorContextProvider> | |||
| </MittProvider> | |||
| </div> | |||
| </div> | |||
| </Modal> | |||
| ) | |||
| } | |||
| export default React.memo(SchemaModal) | |||
| @@ -17,10 +17,13 @@ import { ToolTipContent } from '@/app/components/base/tooltip/content' | |||
| import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' | |||
| import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/components/switch-plugin-version' | |||
| import cn from '@/utils/classnames' | |||
| import McpToolNotSupportTooltip from '@/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip' | |||
| type Props = { | |||
| icon?: any | |||
| providerName?: string | |||
| isMCPTool?: boolean | |||
| providerShowName?: string | |||
| toolLabel?: string | |||
| showSwitch?: boolean | |||
| switchValue?: boolean | |||
| @@ -35,11 +38,14 @@ type Props = { | |||
| onInstall?: () => void | |||
| versionMismatch?: boolean | |||
| open: boolean | |||
| canChooseMCPTool?: boolean, | |||
| } | |||
| const ToolItem = ({ | |||
| open, | |||
| icon, | |||
| isMCPTool, | |||
| providerShowName, | |||
| providerName, | |||
| toolLabel, | |||
| showSwitch, | |||
| @@ -54,11 +60,13 @@ const ToolItem = ({ | |||
| isError, | |||
| errorTip, | |||
| versionMismatch, | |||
| canChooseMCPTool, | |||
| }: Props) => { | |||
| const { t } = useTranslation() | |||
| const providerNameText = providerName?.split('/').pop() | |||
| const providerNameText = isMCPTool ? providerShowName : providerName?.split('/').pop() | |||
| const isTransparent = uninstalled || versionMismatch || isError | |||
| const [isDeleting, setIsDeleting] = useState(false) | |||
| const isShowCanNotChooseMCPTip = isMCPTool && !canChooseMCPTool | |||
| return ( | |||
| <div className={cn( | |||
| @@ -67,7 +75,7 @@ const ToolItem = ({ | |||
| isDeleting && 'border-state-destructive-border shadow-xs hover:bg-state-destructive-hover', | |||
| )}> | |||
| {icon && ( | |||
| <div className={cn('shrink-0', isTransparent && 'opacity-50')}> | |||
| <div className={cn('shrink-0', isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30')}> | |||
| {typeof icon === 'string' && <div className='h-7 w-7 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge bg-cover bg-center' style={{ backgroundImage: `url(${icon})` }} />} | |||
| {typeof icon !== 'string' && <AppIcon className='h-7 w-7 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge' size='xs' icon={icon?.content} background={icon?.background} />} | |||
| </div> | |||
| @@ -75,18 +83,19 @@ const ToolItem = ({ | |||
| {!icon && ( | |||
| <div className={cn( | |||
| 'flex h-7 w-7 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle', | |||
| isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30', | |||
| )}> | |||
| <div className='flex h-5 w-5 items-center justify-center opacity-35'> | |||
| <Group className='text-text-tertiary' /> | |||
| </div> | |||
| </div> | |||
| )} | |||
| <div className={cn('grow truncate pl-0.5', isTransparent && 'opacity-50')}> | |||
| <div className={cn('grow truncate pl-0.5', isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30')}> | |||
| <div className='system-2xs-medium-uppercase text-text-tertiary'>{providerNameText}</div> | |||
| <div className='system-xs-medium text-text-secondary'>{toolLabel}</div> | |||
| </div> | |||
| <div className='hidden items-center gap-1 group-hover:flex'> | |||
| {!noAuth && !isError && !uninstalled && !versionMismatch && ( | |||
| {!noAuth && !isError && !uninstalled && !versionMismatch && !isShowCanNotChooseMCPTip && ( | |||
| <ActionButton> | |||
| <RiEqualizer2Line className='h-4 w-4' /> | |||
| </ActionButton> | |||
| @@ -103,7 +112,7 @@ const ToolItem = ({ | |||
| <RiDeleteBinLine className='h-4 w-4' /> | |||
| </div> | |||
| </div> | |||
| {!isError && !uninstalled && !noAuth && !versionMismatch && showSwitch && ( | |||
| {!isError && !uninstalled && !noAuth && !versionMismatch && !isShowCanNotChooseMCPTip && showSwitch && ( | |||
| <div className='mr-1' onClick={e => e.stopPropagation()}> | |||
| <Switch | |||
| size='md' | |||
| @@ -112,6 +121,9 @@ const ToolItem = ({ | |||
| /> | |||
| </div> | |||
| )} | |||
| {isShowCanNotChooseMCPTip && ( | |||
| <McpToolNotSupportTooltip /> | |||
| )} | |||
| {!isError && !uninstalled && !versionMismatch && noAuth && ( | |||
| <Button variant='secondary' size='small' onClick={onAuth}> | |||
| {t('tools.notAuthorized')} | |||
| @@ -462,9 +462,14 @@ export type StrategyDeclaration = { | |||
| strategies: StrategyDetail[] | |||
| } | |||
| export type PluginMeta = { | |||
| version: string // the version of dify sdk | |||
| } | |||
| export type StrategyPluginDetail = { | |||
| provider: string | |||
| plugin_unique_identifier: string | |||
| plugin_id: string | |||
| declaration: StrategyDeclaration | |||
| meta: PluginMeta | |||
| } | |||
| @@ -1,19 +1,48 @@ | |||
| 'use client' | |||
| import { useSearchParams } from 'next/navigation' | |||
| import { useTranslation } from 'react-i18next' | |||
| const Empty = () => { | |||
| import { ToolTypeEnum } from '../../workflow/block-selector/types' | |||
| import { RiArrowRightUpLine } from '@remixicon/react' | |||
| import Link from 'next/link' | |||
| import cn from '@/utils/classnames' | |||
| import { NoToolPlaceholder } from '../../base/icons/src/vender/other' | |||
| type Props = { | |||
| type?: ToolTypeEnum | |||
| isAgent?: boolean | |||
| } | |||
| const getLink = (type?: ToolTypeEnum) => { | |||
| switch (type) { | |||
| case ToolTypeEnum.Custom: | |||
| return '/tools?category=api' | |||
| case ToolTypeEnum.MCP: | |||
| return '/tools?category=mcp' | |||
| default: | |||
| return '/tools?category=api' | |||
| } | |||
| } | |||
| const Empty = ({ | |||
| type, | |||
| isAgent, | |||
| }: Props) => { | |||
| const { t } = useTranslation() | |||
| const searchParams = useSearchParams() | |||
| const hasLink = type && [ToolTypeEnum.Custom, ToolTypeEnum.MCP].includes(type) | |||
| const Comp = (hasLink ? Link : 'div') as any | |||
| const linkProps = hasLink ? { href: getLink(type), target: '_blank' } : {} | |||
| const renderType = isAgent ? 'agent' : type | |||
| const hasTitle = t(`tools.addToolModal.${renderType}.title`) !== `tools.addToolModal.${renderType}.title` | |||
| return ( | |||
| <div className='flex flex-col items-center'> | |||
| <div className="h-[149px] w-[163px] shrink-0 bg-[url('~@/app/components/tools/add-tool-modal/empty.png')] bg-cover bg-no-repeat"></div> | |||
| <div className='mb-1 text-[13px] font-medium leading-[18px] text-text-primary'> | |||
| {t(`tools.addToolModal.${searchParams.get('category') === 'workflow' ? 'emptyTitle' : 'emptyTitleCustom'}`)} | |||
| </div> | |||
| <div className='text-[13px] leading-[18px] text-text-tertiary'> | |||
| {t(`tools.addToolModal.${searchParams.get('category') === 'workflow' ? 'emptyTip' : 'emptyTipCustom'}`)} | |||
| <div className='flex h-[336px] flex-col items-center justify-center'> | |||
| <NoToolPlaceholder /> | |||
| <div className='mb-1 mt-2 text-[13px] font-medium leading-[18px] text-text-primary'> | |||
| {hasTitle ? t(`tools.addToolModal.${renderType}.title`) : 'No tools available'} | |||
| </div> | |||
| {(!isAgent && hasTitle) && ( | |||
| <Comp className={cn('flex items-center text-[13px] leading-[18px] text-text-tertiary', hasLink && 'cursor-pointer hover:text-text-accent')} {...linkProps}> | |||
| {t(`tools.addToolModal.${renderType}.tip`)} {hasLink && <RiArrowRightUpLine className='ml-0.5 h-3 w-3' />} | |||
| </Comp> | |||
| )} | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -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'> | |||
| @@ -0,0 +1,75 @@ | |||
| 'use client' | |||
| import { useMemo, useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useContext } from 'use-context-selector' | |||
| import { | |||
| RiAddCircleFill, | |||
| RiArrowRightUpLine, | |||
| RiBookOpenLine, | |||
| } from '@remixicon/react' | |||
| import MCPModal from './modal' | |||
| import I18n from '@/context/i18n' | |||
| import { getLanguage } from '@/i18n/language' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { useCreateMCP } from '@/service/use-tools' | |||
| import type { ToolWithProvider } from '@/app/components/workflow/types' | |||
| type Props = { | |||
| handleCreate: (provider: ToolWithProvider) => void | |||
| } | |||
| const NewMCPCard = ({ handleCreate }: Props) => { | |||
| const { t } = useTranslation() | |||
| const { locale } = useContext(I18n) | |||
| const language = getLanguage(locale) | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { mutateAsync: createMCP } = useCreateMCP() | |||
| const create = async (info: any) => { | |||
| const provider = await createMCP(info) | |||
| handleCreate(provider) | |||
| } | |||
| const linkUrl = useMemo(() => { | |||
| if (language.startsWith('zh_')) | |||
| return 'https://docs.dify.ai/zh-hans/guides/tools/mcp' | |||
| if (language.startsWith('ja_jp')) | |||
| return 'https://docs.dify.ai/ja_jp/guides/tools/mcp' | |||
| return 'https://docs.dify.ai/en/guides/tools/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 | |||
| @@ -0,0 +1,308 @@ | |||
| 'use client' | |||
| import React, { useCallback, useEffect } from 'react' | |||
| import type { FC } from 'react' | |||
| import { useBoolean } from 'ahooks' | |||
| import copy from 'copy-to-clipboard' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { | |||
| RiCloseLine, | |||
| RiLoader2Line, | |||
| RiLoopLeftLine, | |||
| } from '@remixicon/react' | |||
| import type { ToolWithProvider } from '../../../workflow/types' | |||
| import Icon from '@/app/components/plugins/card/base/card-icon' | |||
| import ActionButton from '@/app/components/base/action-button' | |||
| import Button from '@/app/components/base/button' | |||
| import Confirm from '@/app/components/base/confirm' | |||
| import Indicator from '@/app/components/header/indicator' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| import MCPModal from '../modal' | |||
| import OperationDropdown from './operation-dropdown' | |||
| import ListLoading from './list-loading' | |||
| import ToolItem from './tool-item' | |||
| import { | |||
| useAuthorizeMCP, | |||
| useDeleteMCP, | |||
| useInvalidateMCPTools, | |||
| useMCPTools, | |||
| useUpdateMCP, | |||
| useUpdateMCPTools, | |||
| } from '@/service/use-tools' | |||
| import { openOAuthPopup } from '@/hooks/use-oauth' | |||
| import cn from '@/utils/classnames' | |||
| type Props = { | |||
| detail: ToolWithProvider | |||
| onUpdate: (isDelete?: boolean) => void | |||
| onHide: () => void | |||
| isTriggerAuthorize: boolean | |||
| onFirstCreate: () => void | |||
| } | |||
| const MCPDetailContent: FC<Props> = ({ | |||
| detail, | |||
| onUpdate, | |||
| onHide, | |||
| isTriggerAuthorize, | |||
| onFirstCreate, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { data, isFetching: isGettingTools } = useMCPTools(detail.is_team_authorization ? detail.id : '') | |||
| const invalidateMCPTools = useInvalidateMCPTools() | |||
| const { mutateAsync: updateTools, isPending: isUpdating } = useUpdateMCPTools() | |||
| const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useAuthorizeMCP() | |||
| const toolList = data?.tools || [] | |||
| const [isShowUpdateConfirm, { | |||
| setTrue: showUpdateConfirm, | |||
| setFalse: hideUpdateConfirm, | |||
| }] = useBoolean(false) | |||
| const handleUpdateTools = useCallback(async () => { | |||
| hideUpdateConfirm() | |||
| if (!detail) | |||
| return | |||
| await updateTools(detail.id) | |||
| invalidateMCPTools(detail.id) | |||
| onUpdate() | |||
| }, [detail, hideUpdateConfirm, invalidateMCPTools, onUpdate, updateTools]) | |||
| const { mutateAsync: updateMCP } = useUpdateMCP({}) | |||
| const { mutateAsync: deleteMCP } = useDeleteMCP({}) | |||
| const [isShowUpdateModal, { | |||
| setTrue: showUpdateModal, | |||
| setFalse: hideUpdateModal, | |||
| }] = useBoolean(false) | |||
| const [isShowDeleteConfirm, { | |||
| setTrue: showDeleteConfirm, | |||
| setFalse: hideDeleteConfirm, | |||
| }] = useBoolean(false) | |||
| const [deleting, { | |||
| setTrue: showDeleting, | |||
| setFalse: hideDeleting, | |||
| }] = useBoolean(false) | |||
| const handleOAuthCallback = useCallback(() => { | |||
| if (!isCurrentWorkspaceManager) | |||
| return | |||
| if (!detail.id) | |||
| return | |||
| handleUpdateTools() | |||
| }, [detail.id, handleUpdateTools, isCurrentWorkspaceManager]) | |||
| const handleAuthorize = useCallback(async () => { | |||
| onFirstCreate() | |||
| if (!isCurrentWorkspaceManager) | |||
| return | |||
| if (!detail) | |||
| return | |||
| const res = await authorizeMcp({ | |||
| provider_id: detail.id, | |||
| }) | |||
| if (res.result === 'success') | |||
| handleUpdateTools() | |||
| else if (res.authorization_url) | |||
| openOAuthPopup(res.authorization_url, handleOAuthCallback) | |||
| }, [onFirstCreate, isCurrentWorkspaceManager, detail, authorizeMcp, handleUpdateTools, handleOAuthCallback]) | |||
| const handleUpdate = useCallback(async (data: any) => { | |||
| if (!detail) | |||
| return | |||
| const res = await updateMCP({ | |||
| ...data, | |||
| provider_id: detail.id, | |||
| }) | |||
| if ((res as any)?.result === 'success') { | |||
| hideUpdateModal() | |||
| onUpdate() | |||
| handleAuthorize() | |||
| } | |||
| }, [detail, updateMCP, hideUpdateModal, onUpdate, handleAuthorize]) | |||
| const handleDelete = useCallback(async () => { | |||
| if (!detail) | |||
| return | |||
| showDeleting() | |||
| const res = await deleteMCP(detail.id) | |||
| hideDeleting() | |||
| if ((res as any)?.result === 'success') { | |||
| hideDeleteConfirm() | |||
| onUpdate(true) | |||
| } | |||
| }, [detail, showDeleting, deleteMCP, hideDeleting, hideDeleteConfirm, onUpdate]) | |||
| useEffect(() => { | |||
| if (isTriggerAuthorize) | |||
| handleAuthorize() | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, []) | |||
| if (!detail) | |||
| return null | |||
| return ( | |||
| <> | |||
| <div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3')}> | |||
| <div className='flex'> | |||
| <div className='shrink-0 overflow-hidden rounded-xl border border-components-panel-border-subtle'> | |||
| <Icon src={detail.icon} /> | |||
| </div> | |||
| <div className='ml-3 w-0 grow'> | |||
| <div className='flex h-5 items-center'> | |||
| <div className='system-md-semibold truncate text-text-primary' title={detail.name}>{detail.name}</div> | |||
| </div> | |||
| <div className='mt-0.5 flex items-center gap-1'> | |||
| <Tooltip popupContent={t('tools.mcp.identifier')}> | |||
| <div className='system-xs-regular shrink-0 cursor-pointer text-text-secondary' onClick={() => copy(detail.server_identifier || '')}>{detail.server_identifier}</div> | |||
| </Tooltip> | |||
| <div className='system-xs-regular shrink-0 text-text-quaternary'>·</div> | |||
| <Tooltip popupContent={t('tools.mcp.modal.serverUrl')}> | |||
| <div className='system-xs-regular truncate text-text-secondary'>{detail.server_url}</div> | |||
| </Tooltip> | |||
| </div> | |||
| </div> | |||
| <div className='flex gap-1'> | |||
| <OperationDropdown | |||
| onEdit={showUpdateModal} | |||
| onRemove={showDeleteConfirm} | |||
| /> | |||
| <ActionButton onClick={onHide}> | |||
| <RiCloseLine className='h-4 w-4' /> | |||
| </ActionButton> | |||
| </div> | |||
| </div> | |||
| <div className='mt-5'> | |||
| {!isAuthorizing && detail.is_team_authorization && ( | |||
| <Button | |||
| variant='secondary' | |||
| className='w-full' | |||
| onClick={handleAuthorize} | |||
| disabled={!isCurrentWorkspaceManager} | |||
| > | |||
| <Indicator className='mr-2' color={'green'} /> | |||
| {t('tools.auth.authorized')} | |||
| </Button> | |||
| )} | |||
| {!detail.is_team_authorization && !isAuthorizing && ( | |||
| <Button | |||
| variant='primary' | |||
| className='w-full' | |||
| onClick={handleAuthorize} | |||
| disabled={!isCurrentWorkspaceManager} | |||
| > | |||
| {t('tools.mcp.authorize')} | |||
| </Button> | |||
| )} | |||
| {isAuthorizing && ( | |||
| <Button | |||
| variant='primary' | |||
| className='w-full' | |||
| disabled | |||
| > | |||
| <RiLoader2Line className={cn('mr-1 h-4 w-4 animate-spin')} /> | |||
| {t('tools.mcp.authorizing')} | |||
| </Button> | |||
| )} | |||
| </div> | |||
| </div> | |||
| <div className='flex grow flex-col'> | |||
| {((detail.is_team_authorization && isGettingTools) || isUpdating) && ( | |||
| <> | |||
| <div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'> | |||
| <div className='flex h-6 items-center'> | |||
| {!isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.gettingTools')}</div>} | |||
| {isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.updateTools')}</div>} | |||
| </div> | |||
| <div></div> | |||
| </div> | |||
| <div className='flex h-full w-full grow flex-col overflow-hidden px-4 pb-4'> | |||
| <ListLoading /> | |||
| </div> | |||
| </> | |||
| )} | |||
| {!isUpdating && detail.is_team_authorization && !isGettingTools && !toolList.length && ( | |||
| <div className='flex h-full w-full flex-col items-center justify-center'> | |||
| <div className='system-sm-regular mb-3 text-text-tertiary'>{t('tools.mcp.toolsEmpty')}</div> | |||
| <Button | |||
| variant='primary' | |||
| onClick={handleUpdateTools} | |||
| >{t('tools.mcp.getTools')}</Button> | |||
| </div> | |||
| )} | |||
| {!isUpdating && !isGettingTools && toolList.length > 0 && ( | |||
| <> | |||
| <div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'> | |||
| <div className='flex h-6 items-center'> | |||
| {toolList.length > 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.toolsNum', { count: toolList.length })}</div>} | |||
| {toolList.length === 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.onlyTool')}</div>} | |||
| </div> | |||
| <div> | |||
| <Button size='small' onClick={showUpdateConfirm}> | |||
| <RiLoopLeftLine className='mr-1 h-3.5 w-3.5' /> | |||
| {t('tools.mcp.update')} | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| <div className='flex h-0 w-full grow flex-col gap-2 overflow-y-auto px-4 pb-4'> | |||
| {toolList.map(tool => ( | |||
| <ToolItem | |||
| key={`${detail.id}${tool.name}`} | |||
| tool={tool} | |||
| /> | |||
| ))} | |||
| </div> | |||
| </> | |||
| )} | |||
| {!isUpdating && !detail.is_team_authorization && ( | |||
| <div className='flex h-full w-full flex-col items-center justify-center'> | |||
| {!isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizingRequired')}</div>} | |||
| {isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizing')}</div>} | |||
| <div className='system-sm-regular text-text-tertiary'>{t('tools.mcp.authorizeTip')}</div> | |||
| </div> | |||
| )} | |||
| </div> | |||
| {isShowUpdateModal && ( | |||
| <MCPModal | |||
| data={detail} | |||
| show={isShowUpdateModal} | |||
| onConfirm={handleUpdate} | |||
| onHide={hideUpdateModal} | |||
| /> | |||
| )} | |||
| {isShowDeleteConfirm && ( | |||
| <Confirm | |||
| isShow | |||
| title={t('tools.mcp.delete')} | |||
| content={ | |||
| <div> | |||
| {t('tools.mcp.deleteConfirmTitle', { mcp: detail.name })} | |||
| </div> | |||
| } | |||
| onCancel={hideDeleteConfirm} | |||
| onConfirm={handleDelete} | |||
| isLoading={deleting} | |||
| isDisabled={deleting} | |||
| /> | |||
| )} | |||
| {isShowUpdateConfirm && ( | |||
| <Confirm | |||
| isShow | |||
| title={t('tools.mcp.toolUpdateConfirmTitle')} | |||
| content={t('tools.mcp.toolUpdateConfirmContent')} | |||
| onCancel={hideUpdateConfirm} | |||
| onConfirm={handleUpdateTools} | |||
| /> | |||
| )} | |||
| </> | |||
| ) | |||
| } | |||
| export default MCPDetailContent | |||
| @@ -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 | |||
| @@ -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) | |||
| @@ -0,0 +1,56 @@ | |||
| 'use client' | |||
| import React from 'react' | |||
| import type { FC } from 'react' | |||
| import Drawer from '@/app/components/base/drawer' | |||
| import MCPDetailContent from './content' | |||
| import type { ToolWithProvider } from '../../../workflow/types' | |||
| import cn from '@/utils/classnames' | |||
| type Props = { | |||
| detail?: ToolWithProvider | |||
| onUpdate: () => void | |||
| onHide: () => void | |||
| isTriggerAuthorize: boolean | |||
| onFirstCreate: () => void | |||
| } | |||
| const MCPDetailPanel: FC<Props> = ({ | |||
| detail, | |||
| onUpdate, | |||
| onHide, | |||
| isTriggerAuthorize, | |||
| onFirstCreate, | |||
| }) => { | |||
| const handleUpdate = (isDelete = false) => { | |||
| if (isDelete) | |||
| onHide() | |||
| onUpdate() | |||
| } | |||
| if (!detail) | |||
| return null | |||
| return ( | |||
| <Drawer | |||
| isOpen={!!detail} | |||
| clickOutsideNotOpen={false} | |||
| onClose={onHide} | |||
| footer={null} | |||
| mask={false} | |||
| positionCenter={false} | |||
| panelClassName={cn('mb-2 mr-2 mt-[64px] !w-[420px] !max-w-[420px] justify-start rounded-2xl border-[0.5px] border-components-panel-border !bg-components-panel-bg !p-0 shadow-xl')} | |||
| > | |||
| {detail && ( | |||
| <MCPDetailContent | |||
| detail={detail} | |||
| onHide={onHide} | |||
| onUpdate={handleUpdate} | |||
| isTriggerAuthorize={isTriggerAuthorize} | |||
| onFirstCreate={onFirstCreate} | |||
| /> | |||
| )} | |||
| </Drawer> | |||
| ) | |||
| } | |||
| export default MCPDetailPanel | |||
| @@ -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 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 | |||
| @@ -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 } | |||
| } | |||
| @@ -0,0 +1,98 @@ | |||
| 'use client' | |||
| import { useMemo, useState } from 'react' | |||
| import NewMCPCard from './create-card' | |||
| import MCPCard from './provider-card' | |||
| import MCPDetailPanel from './detail/provider-detail' | |||
| import { | |||
| useAllToolProviders, | |||
| } from '@/service/use-tools' | |||
| import type { ToolWithProvider } from '@/app/components/workflow/types' | |||
| import cn from '@/utils/classnames' | |||
| type Props = { | |||
| searchText: string | |||
| } | |||
| function renderDefaultCard() { | |||
| const defaultCards = Array.from({ length: 36 }, (_, index) => ( | |||
| <div | |||
| key={index} | |||
| className={cn( | |||
| 'inline-flex h-[111px] rounded-xl bg-background-default-lighter opacity-10', | |||
| index < 4 && 'opacity-60', | |||
| index >= 4 && index < 8 && 'opacity-50', | |||
| index >= 8 && index < 12 && 'opacity-40', | |||
| index >= 12 && index < 16 && 'opacity-30', | |||
| index >= 16 && index < 20 && 'opacity-25', | |||
| index >= 20 && index < 24 && 'opacity-20', | |||
| )} | |||
| ></div> | |||
| )) | |||
| return defaultCards | |||
| } | |||
| const MCPList = ({ | |||
| searchText, | |||
| }: Props) => { | |||
| const { data: list = [] as ToolWithProvider[], refetch } = useAllToolProviders() | |||
| const [isTriggerAuthorize, setIsTriggerAuthorize] = useState<boolean>(false) | |||
| const filteredList = useMemo(() => { | |||
| return list.filter((collection) => { | |||
| if (searchText) | |||
| return Object.values(collection.name).some(value => (value as string).toLowerCase().includes(searchText.toLowerCase())) | |||
| return collection.type === 'mcp' | |||
| }) as ToolWithProvider[] | |||
| }, [list, searchText]) | |||
| const [currentProviderID, setCurrentProviderID] = useState<string>() | |||
| const currentProvider = useMemo(() => { | |||
| return list.find(provider => provider.id === currentProviderID) | |||
| }, [list, currentProviderID]) | |||
| const handleCreate = async (provider: ToolWithProvider) => { | |||
| await refetch() // update list | |||
| setCurrentProviderID(provider.id) | |||
| setIsTriggerAuthorize(true) | |||
| } | |||
| const handleUpdate = async (providerID: string) => { | |||
| await refetch() // update list | |||
| setCurrentProviderID(providerID) | |||
| setIsTriggerAuthorize(true) | |||
| } | |||
| return ( | |||
| <> | |||
| <div | |||
| className={cn( | |||
| 'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6', | |||
| !list.length && 'h-[calc(100vh_-_136px)] overflow-hidden', | |||
| )} | |||
| > | |||
| <NewMCPCard handleCreate={handleCreate} /> | |||
| {filteredList.map(provider => ( | |||
| <MCPCard | |||
| key={provider.id} | |||
| data={provider} | |||
| currentProvider={currentProvider as ToolWithProvider} | |||
| handleSelect={setCurrentProviderID} | |||
| onUpdate={handleUpdate} | |||
| onDeleted={refetch} | |||
| /> | |||
| ))} | |||
| {!list.length && renderDefaultCard()} | |||
| </div> | |||
| {currentProvider && ( | |||
| <MCPDetailPanel | |||
| detail={currentProvider as ToolWithProvider} | |||
| onHide={() => setCurrentProviderID(undefined)} | |||
| onUpdate={refetch} | |||
| isTriggerAuthorize={isTriggerAuthorize} | |||
| onFirstCreate={() => setIsTriggerAuthorize(false)} | |||
| /> | |||
| )} | |||
| </> | |||
| ) | |||
| } | |||
| export default MCPList | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -0,0 +1,244 @@ | |||
| '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' | |||
| import { fetchAppDetail } from '@/service/apps' | |||
| export type IAppCardProps = { | |||
| appInfo: AppDetailResponse & Partial<AppSSO> | |||
| } | |||
| function MCPServiceCard({ | |||
| appInfo, | |||
| }: IAppCardProps) { | |||
| const { t } = useTranslation() | |||
| const appId = appInfo.id | |||
| 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 isAdvancedApp = appInfo?.mode === 'advanced-chat' || appInfo?.mode === 'workflow' | |||
| const isBasicApp = !isAdvancedApp | |||
| const { data: currentWorkflow } = useAppWorkflow(isAdvancedApp ? appId : '') | |||
| const [basicAppConfig, setBasicAppConfig] = useState<any>({}) | |||
| const basicAppInputForm = useMemo(() => { | |||
| if(!isBasicApp || !basicAppConfig?.user_input_form) | |||
| return [] | |||
| return basicAppConfig.user_input_form.map((item: any) => { | |||
| const type = Object.keys(item)[0] | |||
| return { | |||
| ...item[type], | |||
| type: type || 'text-input', | |||
| } | |||
| }) | |||
| }, [basicAppConfig.user_input_form, isBasicApp]) | |||
| useEffect(() => { | |||
| if(isBasicApp && appId) { | |||
| (async () => { | |||
| const res = await fetchAppDetail({ url: '/apps', id: appId }) | |||
| setBasicAppConfig(res?.model_config || {}) | |||
| })() | |||
| } | |||
| }, [appId, isBasicApp]) | |||
| const { data: detail } = useMCPServerDetail(appId) | |||
| const { id, status, server_code } = detail ?? {} | |||
| const appUnpublished = isAdvancedApp ? !currentWorkflow?.graph : !basicAppConfig.updated_at | |||
| const serverPublished = !!id | |||
| const serverActivated = status === 'active' | |||
| const serverURL = serverPublished ? `${appInfo.api_base_url.replace('/v1', '')}/mcp/server/${server_code}/mcp` : '***********' | |||
| const toggleDisabled = !isCurrentWorkspaceEditor || appUnpublished | |||
| const [activated, setActivated] = useState(serverActivated) | |||
| const latestParams = useMemo(() => { | |||
| if(isAdvancedApp) { | |||
| if (!currentWorkflow?.graph) | |||
| return [] | |||
| const startNode = currentWorkflow?.graph.nodes.find(node => node.data.type === BlockEnum.Start) as any | |||
| return startNode?.data.variables as any[] || [] | |||
| } | |||
| return basicAppInputForm | |||
| }, [currentWorkflow, basicAppInputForm, isAdvancedApp]) | |||
| const onGenCode = async () => { | |||
| await refreshMCPServerCode(detail?.id || '') | |||
| invalidateMCPServerDetail(appId) | |||
| } | |||
| const onChangeStatus = async (state: boolean) => { | |||
| setActivated(state) | |||
| if (state) { | |||
| if (!serverPublished) { | |||
| setShowMCPServerModal(true) | |||
| return | |||
| } | |||
| await updateMCPServer({ | |||
| appID: appId, | |||
| id: id || '', | |||
| description: detail?.description || '', | |||
| parameters: detail?.parameters || {}, | |||
| status: 'active', | |||
| }) | |||
| invalidateMCPServerDetail(appId) | |||
| } | |||
| else { | |||
| await updateMCPServer({ | |||
| appID: appId, | |||
| id: id || '', | |||
| description: detail?.description || '', | |||
| parameters: detail?.parameters || {}, | |||
| status: 'inactive', | |||
| }) | |||
| invalidateMCPServerDetail(appId) | |||
| } | |||
| } | |||
| const handleServerModalHide = () => { | |||
| setShowMCPServerModal(false) | |||
| if (!serverActivated) | |||
| setActivated(false) | |||
| } | |||
| useEffect(() => { | |||
| setActivated(serverActivated) | |||
| }, [serverActivated]) | |||
| if (!currentWorkflow && isAdvancedApp) | |||
| 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={appId} | |||
| 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 | |||
| @@ -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', | |||
| }, | |||
| }, | |||
| ] | |||
| @@ -0,0 +1,221 @@ | |||
| 'use client' | |||
| import React, { useRef, useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { getDomain } from 'tldts' | |||
| import { RiCloseLine, RiEditLine } from '@remixicon/react' | |||
| import AppIconPicker from '@/app/components/base/app-icon-picker' | |||
| import type { AppIconSelection } from '@/app/components/base/app-icon-picker' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| import Modal from '@/app/components/base/modal' | |||
| import Button from '@/app/components/base/button' | |||
| import Input from '@/app/components/base/input' | |||
| import type { AppIconType } from '@/types/app' | |||
| import type { ToolWithProvider } from '@/app/components/workflow/types' | |||
| import { noop } from 'lodash-es' | |||
| import Toast from '@/app/components/base/toast' | |||
| import { uploadRemoteFileInfo } from '@/service/common' | |||
| import cn from '@/utils/classnames' | |||
| import { useHover } from 'ahooks' | |||
| export type DuplicateAppModalProps = { | |||
| data?: ToolWithProvider | |||
| show: boolean | |||
| onConfirm: (info: { | |||
| name: string | |||
| server_url: string | |||
| icon_type: AppIconType | |||
| icon: string | |||
| icon_background?: string | null | |||
| server_identifier: string | |||
| }) => void | |||
| onHide: () => void | |||
| } | |||
| const DEFAULT_ICON = { type: 'emoji', icon: '🧿', background: '#EFF1F5' } | |||
| const extractFileId = (url: string) => { | |||
| const match = url.match(/files\/(.+?)\/file-preview/) | |||
| return match ? match[1] : null | |||
| } | |||
| const getIcon = (data?: ToolWithProvider) => { | |||
| if (!data) | |||
| return DEFAULT_ICON as AppIconSelection | |||
| if (typeof data.icon === 'string') | |||
| return { type: 'image', url: data.icon, fileId: extractFileId(data.icon) } as AppIconSelection | |||
| return { | |||
| ...data.icon, | |||
| icon: data.icon.content, | |||
| type: 'emoji', | |||
| } as unknown as AppIconSelection | |||
| } | |||
| const MCPModal = ({ | |||
| data, | |||
| show, | |||
| onConfirm, | |||
| onHide, | |||
| }: DuplicateAppModalProps) => { | |||
| const { t } = useTranslation() | |||
| const isCreate = !data | |||
| const originalServerUrl = data?.server_url | |||
| const originalServerID = data?.server_identifier | |||
| const [url, setUrl] = React.useState(data?.server_url || '') | |||
| const [name, setName] = React.useState(data?.name || '') | |||
| const [appIcon, setAppIcon] = useState<AppIconSelection>(getIcon(data)) | |||
| const [showAppIconPicker, setShowAppIconPicker] = useState(false) | |||
| const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '') | |||
| const [isFetchingIcon, setIsFetchingIcon] = useState(false) | |||
| const appIconRef = useRef<HTMLDivElement>(null) | |||
| const isHovering = useHover(appIconRef) | |||
| const isValidUrl = (string: string) => { | |||
| try { | |||
| const urlPattern = /^(https?:\/\/)((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3}))(\:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?/i | |||
| return urlPattern.test(string) | |||
| } | |||
| catch (e) { | |||
| return false | |||
| } | |||
| } | |||
| const isValidServerID = (str: string) => { | |||
| return /^[a-z0-9_-]{1,24}$/.test(str) | |||
| } | |||
| const handleBlur = async (url: string) => { | |||
| if (data) | |||
| return | |||
| if (!isValidUrl(url)) | |||
| return | |||
| const domain = getDomain(url) | |||
| const remoteIcon = `https://www.google.com/s2/favicons?domain=${domain}&sz=128` | |||
| setIsFetchingIcon(true) | |||
| try { | |||
| const res = await uploadRemoteFileInfo(remoteIcon, undefined, true) | |||
| setAppIcon({ type: 'image', url: res.url, fileId: extractFileId(res.url) || '' }) | |||
| } | |||
| catch (e) { | |||
| console.error('Failed to fetch remote icon:', e) | |||
| Toast.notify({ type: 'warning', message: 'Failed to fetch remote icon' }) | |||
| } | |||
| finally { | |||
| setIsFetchingIcon(false) | |||
| } | |||
| } | |||
| const submit = async () => { | |||
| if (!isValidUrl(url)) { | |||
| Toast.notify({ type: 'error', message: 'invalid server url' }) | |||
| return | |||
| } | |||
| if (!isValidServerID(serverIdentifier.trim())) { | |||
| Toast.notify({ type: 'error', message: 'invalid server identifier' }) | |||
| return | |||
| } | |||
| await onConfirm({ | |||
| server_url: originalServerUrl === url ? '[__HIDDEN__]' : url.trim(), | |||
| name, | |||
| icon_type: appIcon.type, | |||
| icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, | |||
| icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, | |||
| server_identifier: serverIdentifier.trim(), | |||
| }) | |||
| if(isCreate) | |||
| 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'>{!isCreate ? t('tools.mcp.modal.editTitle') : t('tools.mcp.modal.title')}</div> | |||
| <div className='space-y-5 py-3'> | |||
| <div> | |||
| <div className='mb-1 flex h-6 items-center'> | |||
| <span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.serverUrl')}</span> | |||
| </div> | |||
| <Input | |||
| value={url} | |||
| onChange={e => setUrl(e.target.value)} | |||
| onBlur={e => handleBlur(e.target.value.trim())} | |||
| placeholder={t('tools.mcp.modal.serverUrlPlaceholder')} | |||
| /> | |||
| {originalServerUrl && originalServerUrl !== url && ( | |||
| <div className='mt-1 flex h-5 items-center'> | |||
| <span className='body-xs-regular text-text-warning'>{t('tools.mcp.modal.serverUrlWarning')}</span> | |||
| </div> | |||
| )} | |||
| </div> | |||
| <div className='flex space-x-3'> | |||
| <div className='grow pb-1'> | |||
| <div className='mb-1 flex h-6 items-center'> | |||
| <span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.name')}</span> | |||
| </div> | |||
| <Input | |||
| value={name} | |||
| onChange={e => setName(e.target.value)} | |||
| placeholder={t('tools.mcp.modal.namePlaceholder')} | |||
| /> | |||
| </div> | |||
| <div className='pt-2' ref={appIconRef}> | |||
| <AppIcon | |||
| iconType={appIcon.type} | |||
| icon={appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId} | |||
| background={appIcon.type === 'emoji' ? appIcon.background : undefined} | |||
| imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} | |||
| size='xxl' | |||
| className='relative cursor-pointer rounded-2xl' | |||
| coverElement={ | |||
| isHovering | |||
| ? (<div className='absolute inset-0 flex items-center justify-center overflow-hidden rounded-2xl bg-background-overlay-alt'> | |||
| <RiEditLine className='size-6 text-text-primary-on-surface' /> | |||
| </div>) : null | |||
| } | |||
| onClick={() => { setShowAppIconPicker(true) }} | |||
| /> | |||
| </div> | |||
| </div> | |||
| <div> | |||
| <div className='flex h-6 items-center'> | |||
| <span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.serverIdentifier')}</span> | |||
| </div> | |||
| <div className='body-xs-regular mb-1 text-text-tertiary'>{t('tools.mcp.modal.serverIdentifierTip')}</div> | |||
| <Input | |||
| value={serverIdentifier} | |||
| onChange={e => setServerIdentifier(e.target.value)} | |||
| placeholder={t('tools.mcp.modal.serverIdentifierPlaceholder')} | |||
| /> | |||
| {originalServerID && originalServerID !== serverIdentifier && ( | |||
| <div className='mt-1 flex h-5 items-center'> | |||
| <span className='body-xs-regular text-text-warning'>{t('tools.mcp.modal.serverIdentifierWarning')}</span> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </div> | |||
| <div className='flex flex-row-reverse pt-5'> | |||
| <Button disabled={!name || !url || !serverIdentifier || isFetchingIcon} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.modal.confirm')}</Button> | |||
| <Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button> | |||
| </div> | |||
| </Modal> | |||
| {showAppIconPicker && <AppIconPicker | |||
| onSelect={(payload) => { | |||
| setAppIcon(payload) | |||
| setShowAppIconPicker(false) | |||
| }} | |||
| onClose={() => { | |||
| setAppIcon(getIcon(data)) | |||
| setShowAppIconPicker(false) | |||
| }} | |||
| />} | |||
| </> | |||
| ) | |||
| } | |||
| export default MCPModal | |||
| @@ -0,0 +1,152 @@ | |||
| '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: (providerID: string) => void | |||
| onDeleted: () => void | |||
| } | |||
| const MCPCard = ({ | |||
| currentProvider, | |||
| data, | |||
| onUpdate, | |||
| handleSelect, | |||
| onDeleted, | |||
| }: Props) => { | |||
| const { t } = useTranslation() | |||
| const { formatTimeFromNow } = useFormatTimeFromNow() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { mutateAsync: updateMCP } = useUpdateMCP({}) | |||
| const { mutateAsync: deleteMCP } = useDeleteMCP({}) | |||
| 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.id) | |||
| } | |||
| }, [data, updateMCP, hideUpdateModal, onUpdate]) | |||
| const handleDelete = useCallback(async () => { | |||
| showDeleting() | |||
| const res = await deleteMCP(data.id) | |||
| hideDeleting() | |||
| if ((res as any)?.result === 'success') { | |||
| hideDeleteConfirm() | |||
| onDeleted() | |||
| } | |||
| }, [showDeleting, deleteMCP, data.id, hideDeleting, hideDeleteConfirm, onDeleted]) | |||
| return ( | |||
| <div | |||
| onClick={() => handleSelect(data.id)} | |||
| className={cn( | |||
| 'group relative flex cursor-pointer flex-col rounded-xl border-[1.5px] border-transparent bg-components-card-bg shadow-xs hover:bg-components-card-bg-alt hover:shadow-md', | |||
| currentProvider?.id === data.id && 'border-components-option-card-option-selected-border bg-components-card-bg-alt', | |||
| )} | |||
| > | |||
| <div className='flex grow items-center gap-3 rounded-t-xl p-4'> | |||
| <div className='shrink-0 overflow-hidden rounded-xl border border-components-panel-border-subtle'> | |||
| <Icon src={data.icon} /> | |||
| </div> | |||
| <div className='grow'> | |||
| <div className='system-md-semibold mb-1 truncate text-text-secondary' title={data.name}>{data.name}</div> | |||
| <div className='system-xs-regular text-text-tertiary'>{data.server_identifier}</div> | |||
| </div> | |||
| </div> | |||
| <div className='flex items-center gap-1 rounded-b-xl pb-2.5 pl-4 pr-2.5 pt-1.5'> | |||
| <div className='flex w-0 grow items-center gap-2'> | |||
| <div className='flex items-center gap-1'> | |||
| <RiHammerFill className='h-3 w-3 shrink-0 text-text-quaternary' /> | |||
| {data.tools.length > 0 && ( | |||
| <div className='system-xs-regular shrink-0 text-text-tertiary'>{t('tools.mcp.toolsCount', { count: data.tools.length })}</div> | |||
| )} | |||
| {!data.tools.length && ( | |||
| <div className='system-xs-regular shrink-0 text-text-tertiary'>{t('tools.mcp.noTools')}</div> | |||
| )} | |||
| </div> | |||
| <div className={cn('system-xs-regular text-divider-deep', (!data.is_team_authorization || !data.tools.length) && 'sm:hidden')}>/</div> | |||
| <div className={cn('system-xs-regular truncate text-text-tertiary', (!data.is_team_authorization || !data.tools.length) && ' sm:hidden')} title={`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at! * 1000)}`}>{`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at! * 1000)}`}</div> | |||
| </div> | |||
| {data.is_team_authorization && data.tools.length > 0 && <Indicator color='green' className='shrink-0' />} | |||
| {(!data.is_team_authorization || !data.tools.length) && ( | |||
| <div className='system-xs-medium flex shrink-0 items-center gap-1 rounded-md border border-util-colors-red-red-500 bg-components-badge-bg-red-soft px-1.5 py-0.5 text-util-colors-red-red-500'> | |||
| {t('tools.mcp.noConfigured')} | |||
| <Indicator color='red' /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| {isCurrentWorkspaceManager && ( | |||
| <div className={cn('absolute right-2.5 top-2.5 hidden group-hover:block', isOperationShow && 'block')} onClick={e => e.stopPropagation()}> | |||
| <OperationDropdown | |||
| inCard | |||
| onOpenChange={setIsOperationShow} | |||
| onEdit={showUpdateModal} | |||
| onRemove={showDeleteConfirm} | |||
| /> | |||
| </div> | |||
| )} | |||
| {isShowUpdateModal && ( | |||
| <MCPModal | |||
| data={data} | |||
| show={isShowUpdateModal} | |||
| onConfirm={handleUpdate} | |||
| onHide={hideUpdateModal} | |||
| /> | |||
| )} | |||
| {isShowDeleteConfirm && ( | |||
| <Confirm | |||
| isShow | |||
| title={t('tools.mcp.delete')} | |||
| content={ | |||
| <div> | |||
| {t('tools.mcp.deleteConfirmTitle', { mcp: data.name })} | |||
| </div> | |||
| } | |||
| onCancel={hideDeleteConfirm} | |||
| onConfirm={handleDelete} | |||
| isLoading={deleting} | |||
| isDisabled={deleting} | |||
| /> | |||
| )} | |||
| </div> | |||
| ) | |||
| } | |||
| export default MCPCard | |||
| @@ -15,11 +15,29 @@ 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' | |||
| import { ToolTypeEnum } from '../workflow/block-selector/types' | |||
| const getToolType = (type: string) => { | |||
| switch (type) { | |||
| case 'builtin': | |||
| return ToolTypeEnum.BuiltIn | |||
| case 'api': | |||
| return ToolTypeEnum.Custom | |||
| case 'workflow': | |||
| return ToolTypeEnum.Workflow | |||
| case 'mcp': | |||
| return ToolTypeEnum.MCP | |||
| default: | |||
| return ToolTypeEnum.BuiltIn | |||
| } | |||
| } | |||
| const ProviderList = () => { | |||
| // const searchParams = useSearchParams() | |||
| // searchParams.get('category') === 'workflow' | |||
| const { t } = useTranslation() | |||
| const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) | |||
| const containerRef = useRef<HTMLDivElement>(null) | |||
| @@ -31,6 +49,7 @@ const ProviderList = () => { | |||
| { value: 'builtin', text: t('tools.type.builtIn') }, | |||
| { value: 'api', text: t('tools.type.custom') }, | |||
| { value: 'workflow', text: t('tools.type.workflow') }, | |||
| { value: 'mcp', text: 'MCP' }, | |||
| ] | |||
| const [tagFilterValue, setTagFilterValue] = useState<string[]>([]) | |||
| const handleTagsChange = (value: string[]) => { | |||
| @@ -85,7 +104,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 +117,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', | |||
| @@ -127,25 +148,26 @@ const ProviderList = () => { | |||
| /> | |||
| </div> | |||
| ))} | |||
| {!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty /></div>} | |||
| {!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty type={getToolType(activeTab)} /></div>} | |||
| </div> | |||
| )} | |||
| {!filteredCollectionList.length && activeTab === 'builtin' && ( | |||
| <Empty lightCard text={t('tools.noTools')} className='h-[224px] px-12' /> | |||
| )} | |||
| { | |||
| enable_marketplace && activeTab === 'builtin' && ( | |||
| <Marketplace | |||
| onMarketplaceScroll={() => { | |||
| containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' }) | |||
| }} | |||
| searchPluginText={keywords} | |||
| filterPluginTags={tagFilterValue} | |||
| /> | |||
| ) | |||
| } | |||
| </div > | |||
| </div > | |||
| {enable_marketplace && activeTab === 'builtin' && ( | |||
| <Marketplace | |||
| onMarketplaceScroll={() => { | |||
| containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' }) | |||
| }} | |||
| searchPluginText={keywords} | |||
| filterPluginTags={tagFilterValue} | |||
| /> | |||
| )} | |||
| {activeTab === 'mcp' && ( | |||
| <MCPList searchText={keywords} /> | |||
| )} | |||
| </div> | |||
| </div> | |||
| {currentProvider && !currentProvider.plugin_id && ( | |||
| <ProviderDetail | |||
| collection={currentProvider} | |||
| @@ -3,13 +3,13 @@ import { useMemo, useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useContext } from 'use-context-selector' | |||
| import { | |||
| RiAddLine, | |||
| RiAddCircleFill, | |||
| RiArrowRightUpLine, | |||
| RiBookOpenLine, | |||
| } from '@remixicon/react' | |||
| import type { CustomCollectionBackend } from '../types' | |||
| import I18n from '@/context/i18n' | |||
| import { getLanguage } from '@/i18n/language' | |||
| import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education' | |||
| import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows' | |||
| import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' | |||
| import { createCustomCollection } from '@/service/tools' | |||
| import Toast from '@/app/components/base/toast' | |||
| @@ -47,20 +47,20 @@ const Contribute = ({ onRefreshData }: Props) => { | |||
| return ( | |||
| <> | |||
| {isCurrentWorkspaceManager && ( | |||
| <div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl border-[0.5px] border-divider-subtle bg-components-panel-on-panel-item-bg transition-all duration-200 ease-in-out hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg'> | |||
| <div className='group grow rounded-t-xl hover:bg-background-body' onClick={() => setIsShowEditCustomCollectionModal(true)}> | |||
| <div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out'> | |||
| <div className='group grow rounded-t-xl' onClick={() => setIsShowEditCustomCollectionModal(true)}> | |||
| <div className='flex shrink-0 items-center p-4 pb-3'> | |||
| <div className='flex h-10 w-10 items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg group-hover:border-components-option-card-option-border-hover group-hover:bg-components-option-card-option-bg-hover'> | |||
| <RiAddLine className='h-4 w-4 text-text-tertiary group-hover:text-text-accent'/> | |||
| <div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'> | |||
| <RiAddCircleFill className='h-4 w-4 text-text-quaternary group-hover:text-text-accent'/> | |||
| </div> | |||
| <div className='ml-3 text-sm font-semibold leading-5 text-text-primary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div> | |||
| <div className='system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div> | |||
| </div> | |||
| </div> | |||
| <div className='rounded-b-xl border-t-[0.5px] border-divider-regular px-4 py-3 text-text-tertiary hover:bg-background-body hover:text-text-accent'> | |||
| <div className='rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent'> | |||
| <a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'> | |||
| <BookOpen01 className='h-3 w-3 shrink-0' /> | |||
| <div className='grow truncate text-xs font-normal leading-[18px]' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div> | |||
| <ArrowUpRight className='h-3 w-3 shrink-0' /> | |||
| <RiBookOpenLine className='h-3 w-3 shrink-0' /> | |||
| <div className='system-xs-regular grow truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div> | |||
| <RiArrowRightUpLine className='h-3 w-3 shrink-0' /> | |||
| </a> | |||
| </div> | |||
| </div> | |||
| @@ -29,7 +29,7 @@ const ToolItem = ({ | |||
| return ( | |||
| <> | |||
| <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', disabled && '!cursor-not-allowed opacity-50')} | |||
| className={cn('bg-components-panel-item-bg 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', disabled && '!cursor-not-allowed opacity-50')} | |||
| onClick={() => !disabled && setShowDetail(true)} | |||
| > | |||
| <div className='system-md-semibold pb-0.5 text-text-secondary'>{tool.label[language]}</div> | |||
| @@ -29,6 +29,7 @@ export enum CollectionType { | |||
| custom = 'api', | |||
| model = 'model', | |||
| workflow = 'workflow', | |||
| mcp = 'mcp', | |||
| } | |||
| export type Emoji = { | |||
| @@ -50,6 +51,10 @@ export type Collection = { | |||
| labels: string[] | |||
| plugin_id?: string | |||
| letter?: string | |||
| // MCP Server | |||
| server_url?: string | |||
| updated_at?: number | |||
| server_identifier?: string | |||
| } | |||
| export type ToolParameter = { | |||
| @@ -168,3 +173,11 @@ export type WorkflowToolProviderResponse = { | |||
| } | |||
| privacy_policy: string | |||
| } | |||
| export type MCPServerDetail = { | |||
| id: string | |||
| server_code: string | |||
| description: string | |||
| status: string | |||
| parameters?: Record<string, string> | |||
| } | |||
| @@ -1,4 +1,7 @@ | |||
| import type { ToolCredential, ToolParameter } from '../types' | |||
| import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' | |||
| import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' | |||
| export const toType = (type: string) => { | |||
| switch (type) { | |||
| case 'string': | |||
| @@ -54,7 +57,7 @@ export const toolCredentialToFormSchemas = (parameters: ToolCredential[]) => { | |||
| return formSchemas | |||
| } | |||
| export const addDefaultValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[]) => { | |||
| export const addDefaultValue = (value: Record<string, any>, formSchemas: { variable: string; type: string; default?: any }[]) => { | |||
| const newValues = { ...value } | |||
| formSchemas.forEach((formSchema) => { | |||
| const itemValue = value[formSchema.variable] | |||
| @@ -64,14 +67,47 @@ export const addDefaultValue = (value: Record<string, any>, formSchemas: { varia | |||
| return newValues | |||
| } | |||
| export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[], isReasoning = false) => { | |||
| const correctInitialData = (type: string, target: any, defaultValue: any) => { | |||
| if (type === 'text-input' || type === 'secret-input') | |||
| target.type = 'mixed' | |||
| if (type === 'boolean') { | |||
| if (typeof defaultValue === 'string') | |||
| target.value = defaultValue === 'true' || defaultValue === '1' | |||
| if (typeof defaultValue === 'boolean') | |||
| target.value = defaultValue | |||
| if (typeof defaultValue === 'number') | |||
| target.value = defaultValue === 1 | |||
| } | |||
| if (type === 'number-input') { | |||
| if (typeof defaultValue === 'string' && defaultValue !== '') | |||
| target.value = Number.parseFloat(defaultValue) | |||
| } | |||
| if (type === 'app-selector' || type === 'model-selector') | |||
| target.value = defaultValue | |||
| return target | |||
| } | |||
| export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => { | |||
| const newValues = {} as any | |||
| formSchemas.forEach((formSchema) => { | |||
| const itemValue = value[formSchema.variable] | |||
| if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) { | |||
| const value = formSchema.default | |||
| newValues[formSchema.variable] = { | |||
| ...(isReasoning ? { value: null, auto: 1 } : { value: formSchema.default }), | |||
| value: { | |||
| type: 'constant', | |||
| value: formSchema.default, | |||
| }, | |||
| ...(isReasoning ? { auto: 1, value: null } : {}), | |||
| } | |||
| if (!isReasoning) | |||
| newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, value) | |||
| } | |||
| }) | |||
| return newValues | |||
| @@ -80,7 +116,9 @@ export const generateFormValue = (value: Record<string, any>, formSchemas: { var | |||
| export const getPlainValue = (value: Record<string, any>) => { | |||
| const plainValue = { ...value } | |||
| Object.keys(plainValue).forEach((key) => { | |||
| plainValue[key] = value[key].value | |||
| plainValue[key] = { | |||
| ...value[key].value, | |||
| } | |||
| }) | |||
| return plainValue | |||
| } | |||
| @@ -94,3 +132,65 @@ export const getStructureValue = (value: Record<string, any>) => { | |||
| }) | |||
| return newValue | |||
| } | |||
| export const getConfiguredValue = (value: Record<string, any>, formSchemas: { variable: string; type: string; default?: any }[]) => { | |||
| const newValues = { ...value } | |||
| formSchemas.forEach((formSchema) => { | |||
| const itemValue = value[formSchema.variable] | |||
| if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) { | |||
| const value = formSchema.default | |||
| newValues[formSchema.variable] = { | |||
| type: 'constant', | |||
| value: formSchema.default, | |||
| } | |||
| newValues[formSchema.variable] = correctInitialData(formSchema.type, newValues[formSchema.variable], value) | |||
| } | |||
| }) | |||
| return newValues | |||
| } | |||
| const getVarKindType = (type: FormTypeEnum) => { | |||
| if (type === FormTypeEnum.file || type === FormTypeEnum.files) | |||
| return VarKindType.variable | |||
| if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber) | |||
| return VarKindType.constant | |||
| if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput) | |||
| return VarKindType.mixed | |||
| } | |||
| export const generateAgentToolValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => { | |||
| const newValues = {} as any | |||
| if (!isReasoning) { | |||
| formSchemas.forEach((formSchema) => { | |||
| const itemValue = value[formSchema.variable] | |||
| newValues[formSchema.variable] = { | |||
| value: { | |||
| type: 'constant', | |||
| value: itemValue.value, | |||
| }, | |||
| } | |||
| newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, itemValue.value) | |||
| }) | |||
| } | |||
| else { | |||
| formSchemas.forEach((formSchema) => { | |||
| const itemValue = value[formSchema.variable] | |||
| if (itemValue.auto === 1) { | |||
| newValues[formSchema.variable] = { | |||
| auto: 1, | |||
| value: null, | |||
| } | |||
| } | |||
| else { | |||
| newValues[formSchema.variable] = { | |||
| auto: 0, | |||
| value: itemValue.value || { | |||
| type: getVarKindType(formSchema.type as FormTypeEnum), | |||
| value: null, | |||
| }, | |||
| } | |||
| } | |||
| }) | |||
| } | |||
| return newValues | |||
| } | |||
| @@ -23,7 +23,7 @@ import { | |||
| InputVarType, | |||
| } from '@/app/components/workflow/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' | |||
| @@ -89,6 +89,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({ | |||
| @@ -98,6 +99,7 @@ const FeaturesTrigger = () => { | |||
| if (res) { | |||
| notify({ type: 'success', message: t('common.api.actionSuccess') }) | |||
| updatePublishedWorkflow(appID!) | |||
| updateAppDetail() | |||
| workflowStore.getState().setPublishedAt(res.created_at) | |||
| resetWorkflowVersionHistory() | |||
| @@ -106,7 +108,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[] | |||
| canChooseMCPTool?: boolean | |||
| } | |||
| const DEFAULT_TAGS: AllToolsProps['tags'] = [] | |||
| @@ -46,12 +46,14 @@ const AllTools = ({ | |||
| searchText, | |||
| tags = DEFAULT_TAGS, | |||
| onSelect, | |||
| canNotSelectMultiple, | |||
| onSelectMultiple, | |||
| buildInTools, | |||
| workflowTools, | |||
| customTools, | |||
| supportAddCustomTool, | |||
| onShowAddCustomCollectionModal, | |||
| mcpTools = [], | |||
| selectedTools, | |||
| canChooseMCPTool, | |||
| }: AllToolsProps) => { | |||
| const language = useGetLanguage() | |||
| const tabs = useToolTabs() | |||
| @@ -64,13 +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, | |||
| @@ -88,7 +92,6 @@ const AllTools = ({ | |||
| } = useMarketplacePlugins() | |||
| const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) | |||
| useEffect(() => { | |||
| if (!enable_marketplace) return | |||
| if (searchText || tags.length > 0) { | |||
| @@ -103,10 +106,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={cn('min-w-[400px] max-w-[500px]', className)}> | |||
| <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 +128,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 | |||
| @@ -144,12 +139,15 @@ const AllTools = ({ | |||
| > | |||
| <Tools | |||
| className={toolContentClassName} | |||
| showWorkflowEmpty={activeTab === ToolTypeEnum.Workflow} | |||
| tools={tools} | |||
| onSelect={onSelect} | |||
| viewType={activeView} | |||
| canNotSelectMultiple={canNotSelectMultiple} | |||
| onSelectMultiple={onSelectMultiple} | |||
| toolType={activeTab} | |||
| viewType={isSupportGroupView ? activeView : ViewType.flat} | |||
| hasSearchText={!!searchText} | |||
| selectedTools={selectedTools} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| {/* Plugins from marketplace */} | |||
| {enable_marketplace && <PluginList | |||
| @@ -31,10 +31,9 @@ export const useTabs = () => { | |||
| ] | |||
| } | |||
| export const useToolTabs = () => { | |||
| export const useToolTabs = (isHideMCPTools?: boolean) => { | |||
| const { t } = useTranslation() | |||
| return [ | |||
| const tabs = [ | |||
| { | |||
| key: ToolTypeEnum.All, | |||
| name: t('workflow.tabs.allTool'), | |||
| @@ -52,4 +51,12 @@ export const useToolTabs = () => { | |||
| name: t('workflow.tabs.workflowTool'), | |||
| }, | |||
| ] | |||
| if(!isHideMCPTools) { | |||
| tabs.push({ | |||
| key: ToolTypeEnum.MCP, | |||
| name: 'MCP', | |||
| }) | |||
| } | |||
| return tabs | |||
| } | |||
| @@ -83,8 +83,8 @@ const IndexBar: FC<IndexBarProps> = ({ letters, itemRefs, className }) => { | |||
| element.scrollIntoView({ behavior: 'smooth' }) | |||
| } | |||
| return ( | |||
| <div className={classNames('index-bar absolute right-0 top-36 flex flex-col items-center w-6 justify-center text-xs font-medium text-text-quaternary', className)}> | |||
| <div className='absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]'></div> | |||
| <div className={classNames('index-bar sticky top-[20px] flex h-full w-6 flex-col items-center justify-center text-xs font-medium text-text-quaternary', className)}> | |||
| <div className={classNames('absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]')}></div> | |||
| {letters.map(letter => ( | |||
| <div className="cursor-pointer hover:text-text-secondary" key={letter} onClick={() => handleIndexClick(letter)}> | |||
| {letter} | |||
| @@ -129,33 +129,35 @@ const NodeSelector: FC<NodeSelectorProps> = ({ | |||
| </PortalToFollowElemTrigger> | |||
| <PortalToFollowElemContent className='z-[1000]'> | |||
| <div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}> | |||
| <div className='px-2 pt-2' onClick={e => e.stopPropagation()}> | |||
| {activeTab === TabsEnum.Blocks && ( | |||
| <Input | |||
| showLeftIcon | |||
| showClearIcon | |||
| autoFocus | |||
| value={searchText} | |||
| placeholder={searchPlaceholder} | |||
| onChange={e => setSearchText(e.target.value)} | |||
| onClear={() => setSearchText('')} | |||
| /> | |||
| )} | |||
| {activeTab === TabsEnum.Tools && ( | |||
| <SearchBox | |||
| search={searchText} | |||
| onSearchChange={setSearchText} | |||
| tags={tags} | |||
| onTagsChange={setTags} | |||
| size='small' | |||
| placeholder={t('plugin.searchTools')!} | |||
| /> | |||
| )} | |||
| </div> | |||
| <Tabs | |||
| activeTab={activeTab} | |||
| onActiveTabChange={handleActiveTabChange} | |||
| filterElem={ | |||
| <div className='relative m-2' onClick={e => e.stopPropagation()}> | |||
| {activeTab === TabsEnum.Blocks && ( | |||
| <Input | |||
| showLeftIcon | |||
| showClearIcon | |||
| autoFocus | |||
| value={searchText} | |||
| placeholder={searchPlaceholder} | |||
| onChange={e => setSearchText(e.target.value)} | |||
| onClear={() => setSearchText('')} | |||
| /> | |||
| )} | |||
| {activeTab === TabsEnum.Tools && ( | |||
| <SearchBox | |||
| search={searchText} | |||
| onSearchChange={setSearchText} | |||
| tags={tags} | |||
| onTagsChange={setTags} | |||
| size='small' | |||
| placeholder={t('plugin.searchTools')!} | |||
| inputClassName='grow' | |||
| /> | |||
| )} | |||
| </div> | |||
| } | |||
| onSelect={handleSelect} | |||
| searchText={searchText} | |||
| tags={tags} | |||
| @@ -80,7 +80,7 @@ const List = forwardRef<ListRef, ListProps>(({ | |||
| ) | |||
| } | |||
| const maxWidthClassName = toolContentClassName || 'max-w-[300px]' | |||
| const maxWidthClassName = toolContentClassName || 'max-w-[100%]' | |||
| return ( | |||
| <> | |||
| @@ -109,18 +109,20 @@ const List = forwardRef<ListRef, ListProps>(({ | |||
| onAction={noop} | |||
| /> | |||
| ))} | |||
| <div className='mb-3 mt-2 flex items-center justify-center space-x-2'> | |||
| <div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(16,24,40,0.08)] to-[rgba(255,255,255,0.01)]"></div> | |||
| <Link | |||
| href={urlWithSearchText} | |||
| target='_blank' | |||
| className='system-sm-medium flex h-4 shrink-0 items-center text-text-accent-light-mode-only' | |||
| > | |||
| <RiSearchLine className='mr-0.5 h-3 w-3' /> | |||
| <span>{t('plugin.searchInMarketplace')}</span> | |||
| </Link> | |||
| <div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(255,255,255,0.01)] to-[rgba(16,24,40,0.08)]"></div> | |||
| </div> | |||
| {list.length > 0 && ( | |||
| <div className='mb-3 mt-2 flex items-center justify-center space-x-2'> | |||
| <div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(16,24,40,0.08)] to-[rgba(255,255,255,0.01)]"></div> | |||
| <Link | |||
| href={urlWithSearchText} | |||
| target='_blank' | |||
| className='system-sm-medium flex h-4 shrink-0 items-center text-text-accent-light-mode-only' | |||
| > | |||
| <RiSearchLine className='mr-0.5 h-3 w-3' /> | |||
| <span>{t('plugin.searchInMarketplace')}</span> | |||
| </Link> | |||
| <div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(255,255,255,0.01)] to-[rgba(16,24,40,0.08)]"></div> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </> | |||
| ) | |||
| @@ -1,6 +1,6 @@ | |||
| import type { FC } from 'react' | |||
| import { memo } from 'react' | |||
| import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' | |||
| import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools' | |||
| import type { BlockEnum } from '../types' | |||
| import { useTabs } from './hooks' | |||
| import type { ToolDefaultValue } from './types' | |||
| @@ -16,6 +16,7 @@ export type TabsProps = { | |||
| tags: string[] | |||
| onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void | |||
| availableBlocksTypes?: BlockEnum[] | |||
| filterElem: React.ReactNode | |||
| noBlocks?: boolean | |||
| } | |||
| const Tabs: FC<TabsProps> = ({ | |||
| @@ -25,26 +26,28 @@ const Tabs: FC<TabsProps> = ({ | |||
| searchText, | |||
| onSelect, | |||
| availableBlocksTypes, | |||
| filterElem, | |||
| noBlocks, | |||
| }) => { | |||
| const tabs = useTabs() | |||
| const { data: buildInTools } = useAllBuiltInTools() | |||
| const { data: customTools } = useAllCustomTools() | |||
| const { data: workflowTools } = useAllWorkflowTools() | |||
| const { data: mcpTools } = useAllMCPTools() | |||
| return ( | |||
| <div onClick={e => e.stopPropagation()}> | |||
| { | |||
| !noBlocks && ( | |||
| <div className='flex items-center border-b-[0.5px] border-divider-subtle px-3'> | |||
| <div className='relative flex bg-background-section-burn pl-1 pt-1'> | |||
| { | |||
| tabs.map(tab => ( | |||
| <div | |||
| key={tab.key} | |||
| className={cn( | |||
| 'system-sm-medium relative mr-4 cursor-pointer pb-2 pt-1', | |||
| 'system-sm-medium relative mr-0.5 flex h-8 cursor-pointer items-center rounded-t-lg px-3 ', | |||
| activeTab === tab.key | |||
| ? 'text-text-primary after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-util-colors-blue-brand-blue-brand-600' | |||
| ? 'sm-no-bottom cursor-default bg-components-panel-bg text-text-accent' | |||
| : 'text-text-tertiary', | |||
| )} | |||
| onClick={() => onActiveTabChange(tab.key)} | |||
| @@ -56,25 +59,30 @@ const Tabs: FC<TabsProps> = ({ | |||
| </div> | |||
| ) | |||
| } | |||
| {filterElem} | |||
| { | |||
| activeTab === TabsEnum.Blocks && !noBlocks && ( | |||
| <Blocks | |||
| searchText={searchText} | |||
| onSelect={onSelect} | |||
| availableBlocksTypes={availableBlocksTypes} | |||
| /> | |||
| <div className='border-t border-divider-subtle'> | |||
| <Blocks | |||
| searchText={searchText} | |||
| onSelect={onSelect} | |||
| availableBlocksTypes={availableBlocksTypes} | |||
| /> | |||
| </div> | |||
| ) | |||
| } | |||
| { | |||
| activeTab === TabsEnum.Tools && ( | |||
| <AllTools | |||
| className='w-[315px]' | |||
| searchText={searchText} | |||
| onSelect={onSelect} | |||
| tags={tags} | |||
| canNotSelectMultiple | |||
| buildInTools={buildInTools || []} | |||
| customTools={customTools || []} | |||
| workflowTools={workflowTools || []} | |||
| mcpTools={mcpTools || []} | |||
| canChooseMCPTool | |||
| /> | |||
| ) | |||
| } | |||
| @@ -23,7 +23,7 @@ import { | |||
| } from '@/service/tools' | |||
| import type { CustomCollectionBackend } from '@/app/components/tools/types' | |||
| import Toast from '@/app/components/base/toast' | |||
| import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools' | |||
| import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools' | |||
| import cn from '@/utils/classnames' | |||
| type Props = { | |||
| @@ -35,9 +35,11 @@ type Props = { | |||
| isShow: boolean | |||
| onShowChange: (isShow: boolean) => void | |||
| onSelect: (tool: ToolDefaultValue) => void | |||
| onSelectMultiple: (tools: ToolDefaultValue[]) => void | |||
| supportAddCustomTool?: boolean | |||
| scope?: string | |||
| selectedTools?: ToolValue[] | |||
| canChooseMCPTool?: boolean | |||
| } | |||
| const ToolPicker: FC<Props> = ({ | |||
| @@ -48,10 +50,12 @@ const ToolPicker: FC<Props> = ({ | |||
| isShow, | |||
| onShowChange, | |||
| onSelect, | |||
| onSelectMultiple, | |||
| supportAddCustomTool, | |||
| scope = 'all', | |||
| selectedTools, | |||
| panelClassName, | |||
| canChooseMCPTool, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const [searchText, setSearchText] = useState('') | |||
| @@ -61,6 +65,7 @@ const ToolPicker: FC<Props> = ({ | |||
| const { data: customTools } = useAllCustomTools() | |||
| const invalidateCustomTools = useInvalidateAllCustomTools() | |||
| const { data: workflowTools } = useAllWorkflowTools() | |||
| const { data: mcpTools } = useAllMCPTools() | |||
| const { builtinToolList, customToolList, workflowToolList } = useMemo(() => { | |||
| if (scope === 'plugins') { | |||
| @@ -102,6 +107,10 @@ const ToolPicker: FC<Props> = ({ | |||
| onSelect(tool!) | |||
| } | |||
| const handleSelectMultiple = (_type: BlockEnum, tools: ToolDefaultValue[]) => { | |||
| onSelectMultiple(tools) | |||
| } | |||
| const [isShowEditCollectionToolModal, { | |||
| setFalse: hideEditCustomCollectionModal, | |||
| setTrue: showEditCustomCollectionModal, | |||
| @@ -142,7 +151,7 @@ const ToolPicker: FC<Props> = ({ | |||
| </PortalToFollowElemTrigger> | |||
| <PortalToFollowElemContent className='z-[1000]'> | |||
| <div className={cn('relative min-h-20 w-[356px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', panelClassName)}> | |||
| <div className={cn('relative min-h-20 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', panelClassName)}> | |||
| <div className='p-2 pb-1'> | |||
| <SearchBox | |||
| search={searchText} | |||
| @@ -151,21 +160,26 @@ const ToolPicker: FC<Props> = ({ | |||
| onTagsChange={setTags} | |||
| size='small' | |||
| placeholder={t('plugin.searchTools')!} | |||
| supportAddCustomTool={supportAddCustomTool} | |||
| onAddedCustomTool={handleAddedCustomTool} | |||
| onShowAddCustomCollectionModal={showEditCustomCollectionModal} | |||
| inputClassName='grow' | |||
| /> | |||
| </div> | |||
| <AllTools | |||
| className='mt-1' | |||
| toolContentClassName='max-w-[360px]' | |||
| toolContentClassName='max-w-[100%]' | |||
| tags={tags} | |||
| searchText={searchText} | |||
| onSelect={handleSelect} | |||
| onSelectMultiple={handleSelectMultiple} | |||
| buildInTools={builtinToolList || []} | |||
| customTools={customToolList || []} | |||
| workflowTools={workflowToolList || []} | |||
| supportAddCustomTool={supportAddCustomTool} | |||
| onAddedCustomTool={handleAddedCustomTool} | |||
| onShowAddCustomCollectionModal={showEditCustomCollectionModal} | |||
| mcpTools={mcpTools || []} | |||
| selectedTools={selectedTools} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| </div> | |||
| </PortalToFollowElemContent> | |||
| @@ -10,13 +10,12 @@ import { useGetLanguage } from '@/context/i18n' | |||
| import BlockIcon from '../../block-icon' | |||
| import cn from '@/utils/classnames' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { RiCheckLine } from '@remixicon/react' | |||
| import Badge from '@/app/components/base/badge' | |||
| type Props = { | |||
| provider: ToolWithProvider | |||
| payload: Tool | |||
| disabled?: boolean | |||
| isAdded?: boolean | |||
| onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void | |||
| } | |||
| @@ -25,6 +24,7 @@ const ToolItem: FC<Props> = ({ | |||
| payload, | |||
| onSelect, | |||
| disabled, | |||
| isAdded, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| @@ -71,18 +71,16 @@ const ToolItem: FC<Props> = ({ | |||
| output_schema: payload.output_schema, | |||
| paramSchemas: payload.parameters, | |||
| params, | |||
| meta: provider.meta, | |||
| }) | |||
| }} | |||
| > | |||
| <div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary', disabled && 'opacity-30')}>{payload.label[language]}</div> | |||
| {disabled && <Badge | |||
| className='flex h-5 items-center space-x-0.5 text-text-tertiary' | |||
| uppercase | |||
| > | |||
| <RiCheckLine className='h-3 w-3 ' /> | |||
| <div>{t('tools.addToolModal.added')}</div> | |||
| </Badge> | |||
| } | |||
| <div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary')}> | |||
| <span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span> | |||
| </div> | |||
| {isAdded && ( | |||
| <div className='system-xs-regular mr-4 text-text-tertiary'>{t('tools.addToolModal.added')}</div> | |||
| )} | |||
| </div> | |||
| </Tooltip > | |||
| ) | |||
| @@ -11,21 +11,29 @@ import { useMemo } from 'react' | |||
| type Props = { | |||
| payload: ToolWithProvider[] | |||
| isShowLetterIndex: boolean | |||
| indexBar: React.ReactNode | |||
| hasSearchText: boolean | |||
| onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void | |||
| canNotSelectMultiple?: boolean | |||
| onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void | |||
| letters: string[] | |||
| toolRefs: any | |||
| selectedTools?: ToolValue[] | |||
| canChooseMCPTool?: boolean | |||
| } | |||
| const ToolViewFlatView: FC<Props> = ({ | |||
| letters, | |||
| payload, | |||
| isShowLetterIndex, | |||
| indexBar, | |||
| hasSearchText, | |||
| onSelect, | |||
| canNotSelectMultiple, | |||
| onSelectMultiple, | |||
| toolRefs, | |||
| selectedTools, | |||
| canChooseMCPTool, | |||
| }) => { | |||
| const firstLetterToolIds = useMemo(() => { | |||
| const res: Record<string, string> = {} | |||
| @@ -37,26 +45,31 @@ const ToolViewFlatView: FC<Props> = ({ | |||
| return res | |||
| }, [payload, letters]) | |||
| return ( | |||
| <div> | |||
| {payload.map(tool => ( | |||
| <div | |||
| key={tool.id} | |||
| ref={(el) => { | |||
| const letter = firstLetterToolIds[tool.id] | |||
| if (letter) | |||
| toolRefs.current[letter] = el | |||
| }} | |||
| > | |||
| <Tool | |||
| payload={tool} | |||
| viewType={ViewType.flat} | |||
| isShowLetterIndex={isShowLetterIndex} | |||
| hasSearchText={hasSearchText} | |||
| onSelect={onSelect} | |||
| selectedTools={selectedTools} | |||
| /> | |||
| </div> | |||
| ))} | |||
| <div className='flex w-full'> | |||
| <div className='mr-1 grow'> | |||
| {payload.map(tool => ( | |||
| <div | |||
| key={tool.id} | |||
| ref={(el) => { | |||
| const letter = firstLetterToolIds[tool.id] | |||
| if (letter) | |||
| toolRefs.current[letter] = el | |||
| }} | |||
| > | |||
| <Tool | |||
| payload={tool} | |||
| viewType={ViewType.flat} | |||
| hasSearchText={hasSearchText} | |||
| onSelect={onSelect} | |||
| canNotSelectMultiple={canNotSelectMultiple} | |||
| onSelectMultiple={onSelectMultiple} | |||
| selectedTools={selectedTools} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| </div> | |||
| ))} | |||
| </div> | |||
| {isShowLetterIndex && indexBar} | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -12,7 +12,10 @@ type Props = { | |||
| toolList: ToolWithProvider[] | |||
| hasSearchText: boolean | |||
| onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void | |||
| canNotSelectMultiple?: boolean | |||
| onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void | |||
| selectedTools?: ToolValue[] | |||
| canChooseMCPTool?: boolean | |||
| } | |||
| const Item: FC<Props> = ({ | |||
| @@ -20,7 +23,10 @@ const Item: FC<Props> = ({ | |||
| toolList, | |||
| hasSearchText, | |||
| onSelect, | |||
| canNotSelectMultiple, | |||
| onSelectMultiple, | |||
| selectedTools, | |||
| canChooseMCPTool, | |||
| }) => { | |||
| return ( | |||
| <div> | |||
| @@ -36,7 +42,10 @@ const Item: FC<Props> = ({ | |||
| isShowLetterIndex={false} | |||
| hasSearchText={hasSearchText} | |||
| onSelect={onSelect} | |||
| canNotSelectMultiple={canNotSelectMultiple} | |||
| onSelectMultiple={onSelectMultiple} | |||
| selectedTools={selectedTools} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| ))} | |||
| </div> | |||
| @@ -12,14 +12,20 @@ type Props = { | |||
| payload: Record<string, ToolWithProvider[]> | |||
| hasSearchText: boolean | |||
| onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void | |||
| canNotSelectMultiple?: boolean | |||
| onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void | |||
| selectedTools?: ToolValue[] | |||
| canChooseMCPTool?: boolean | |||
| } | |||
| const ToolListTreeView: FC<Props> = ({ | |||
| payload, | |||
| hasSearchText, | |||
| onSelect, | |||
| canNotSelectMultiple, | |||
| onSelectMultiple, | |||
| selectedTools, | |||
| canChooseMCPTool, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const getI18nGroupName = useCallback((name: string) => { | |||
| @@ -46,7 +52,10 @@ const ToolListTreeView: FC<Props> = ({ | |||
| toolList={payload[groupName]} | |||
| hasSearchText={hasSearchText} | |||
| onSelect={onSelect} | |||
| canNotSelectMultiple={canNotSelectMultiple} | |||
| onSelectMultiple={onSelectMultiple} | |||
| selectedTools={selectedTools} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| ))} | |||
| </div> | |||
| @@ -1,6 +1,6 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React, { useEffect, useMemo } from 'react' | |||
| import React, { useCallback, useEffect, useMemo, useRef } from 'react' | |||
| import cn from '@/utils/classnames' | |||
| import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' | |||
| import { useGetLanguage } from '@/context/i18n' | |||
| @@ -13,36 +13,108 @@ import { ViewType } from '../view-type-select' | |||
| import ActionItem from './action-item' | |||
| import BlockIcon from '../../block-icon' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useHover } from 'ahooks' | |||
| import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip' | |||
| import { Mcp } from '@/app/components/base/icons/src/vender/other' | |||
| type Props = { | |||
| className?: string | |||
| payload: ToolWithProvider | |||
| viewType: ViewType | |||
| isShowLetterIndex: boolean | |||
| hasSearchText: boolean | |||
| onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void | |||
| canNotSelectMultiple?: boolean | |||
| onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void | |||
| selectedTools?: ToolValue[] | |||
| canChooseMCPTool?: boolean | |||
| } | |||
| const Tool: FC<Props> = ({ | |||
| className, | |||
| payload, | |||
| viewType, | |||
| isShowLetterIndex, | |||
| hasSearchText, | |||
| onSelect, | |||
| canNotSelectMultiple, | |||
| onSelectMultiple, | |||
| selectedTools, | |||
| canChooseMCPTool, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const language = useGetLanguage() | |||
| const isFlatView = viewType === ViewType.flat | |||
| const notShowProvider = payload.type === CollectionType.workflow | |||
| const actions = payload.tools | |||
| const hasAction = true // Now always support actions | |||
| const hasAction = !notShowProvider | |||
| const [isFold, setFold] = React.useState<boolean>(true) | |||
| const getIsDisabled = (tool: ToolType) => { | |||
| const ref = useRef(null) | |||
| const isHovering = useHover(ref) | |||
| const isMCPTool = payload.type === CollectionType.mcp | |||
| const isShowCanNotChooseMCPTip = !canChooseMCPTool && isMCPTool | |||
| const getIsDisabled = useCallback((tool: ToolType) => { | |||
| if (!selectedTools || !selectedTools.length) return false | |||
| return selectedTools.some(selectedTool => selectedTool.provider_name === payload.name && selectedTool.tool_name === tool.name) | |||
| } | |||
| return selectedTools.some(selectedTool => (selectedTool.provider_name === payload.name || selectedTool.provider_name === payload.id) && selectedTool.tool_name === tool.name) | |||
| }, [payload.id, payload.name, selectedTools]) | |||
| const totalToolsNum = actions.length | |||
| const selectedToolsNum = actions.filter(action => getIsDisabled(action)).length | |||
| const isAllSelected = selectedToolsNum === totalToolsNum | |||
| const notShowProviderSelectInfo = useMemo(() => { | |||
| if (isAllSelected) { | |||
| return ( | |||
| <span className='system-xs-regular text-text-tertiary'> | |||
| {t('tools.addToolModal.added')} | |||
| </span> | |||
| ) | |||
| } | |||
| }, [isAllSelected, t]) | |||
| const selectedInfo = useMemo(() => { | |||
| if (isHovering && !isAllSelected) { | |||
| return ( | |||
| <span className='system-xs-regular text-components-button-secondary-accent-text' | |||
| onClick={(e) => { | |||
| onSelectMultiple?.(BlockEnum.Tool, actions.filter(action => !getIsDisabled(action)).map((tool) => { | |||
| const params: Record<string, string> = {} | |||
| if (tool.parameters) { | |||
| tool.parameters.forEach((item) => { | |||
| params[item.name] = '' | |||
| }) | |||
| } | |||
| return { | |||
| provider_id: payload.id, | |||
| provider_type: payload.type, | |||
| provider_name: payload.name, | |||
| tool_name: tool.name, | |||
| tool_label: tool.label[language], | |||
| tool_description: tool.description[language], | |||
| title: tool.label[language], | |||
| is_team_authorization: payload.is_team_authorization, | |||
| output_schema: tool.output_schema, | |||
| paramSchemas: tool.parameters, | |||
| params, | |||
| } | |||
| })) | |||
| }} | |||
| > | |||
| {t('workflow.tabs.addAll')} | |||
| </span> | |||
| ) | |||
| } | |||
| if (selectedToolsNum === 0) | |||
| return <></> | |||
| return ( | |||
| <span className='system-xs-regular text-text-tertiary'> | |||
| {isAllSelected | |||
| ? t('workflow.tabs.allAdded') | |||
| : `${selectedToolsNum} / ${totalToolsNum}` | |||
| } | |||
| </span> | |||
| ) | |||
| }, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum]) | |||
| useEffect(() => { | |||
| if (hasSearchText && isFold) { | |||
| setFold(false) | |||
| @@ -71,59 +143,73 @@ const Tool: FC<Props> = ({ | |||
| return ( | |||
| <div | |||
| key={payload.id} | |||
| className={cn('mb-1 last-of-type:mb-0', isShowLetterIndex && 'mr-6')} | |||
| className={cn('mb-1 last-of-type:mb-0')} | |||
| ref={ref} | |||
| > | |||
| <div className={cn(className)}> | |||
| <div | |||
| className='flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover' | |||
| className='group/item flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover' | |||
| onClick={() => { | |||
| if (hasAction) | |||
| if (hasAction) { | |||
| setFold(!isFold) | |||
| return | |||
| } | |||
| // Now always support actions | |||
| // if (payload.parameters) { | |||
| // payload.parameters.forEach((item) => { | |||
| // params[item.name] = '' | |||
| // }) | |||
| // } | |||
| // onSelect(BlockEnum.Tool, { | |||
| // provider_id: payload.id, | |||
| // provider_type: payload.type, | |||
| // provider_name: payload.name, | |||
| // tool_name: payload.name, | |||
| // tool_label: payload.label[language], | |||
| // title: payload.label[language], | |||
| // params: {}, | |||
| // }) | |||
| const tool = actions[0] | |||
| const params: Record<string, string> = {} | |||
| if (tool.parameters) { | |||
| tool.parameters.forEach((item) => { | |||
| params[item.name] = '' | |||
| }) | |||
| } | |||
| onSelect(BlockEnum.Tool, { | |||
| provider_id: payload.id, | |||
| provider_type: payload.type, | |||
| provider_name: payload.name, | |||
| tool_name: tool.name, | |||
| tool_label: tool.label[language], | |||
| tool_description: tool.description[language], | |||
| title: tool.label[language], | |||
| is_team_authorization: payload.is_team_authorization, | |||
| output_schema: tool.output_schema, | |||
| paramSchemas: tool.parameters, | |||
| params, | |||
| }) | |||
| }} | |||
| > | |||
| <div className='flex h-8 grow items-center'> | |||
| <div className={cn('flex h-8 grow items-center', isShowCanNotChooseMCPTip && 'opacity-30')}> | |||
| <BlockIcon | |||
| className='shrink-0' | |||
| type={BlockEnum.Tool} | |||
| toolIcon={payload.icon} | |||
| /> | |||
| <div className='ml-2 w-0 flex-1 grow truncate text-sm text-text-primary'>{payload.label[language]}</div> | |||
| <div className='ml-2 flex w-0 grow items-center text-sm text-text-primary'> | |||
| <span className='max-w-[250px] truncate'>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span> | |||
| {isFlatView && groupName && ( | |||
| <span className='system-xs-regular ml-2 shrink-0 text-text-quaternary'>{groupName}</span> | |||
| )} | |||
| {isMCPTool && <Mcp className='ml-2 size-3.5 shrink-0 text-text-quaternary' />} | |||
| </div> | |||
| </div> | |||
| <div className='flex items-center'> | |||
| {isFlatView && ( | |||
| <div className='system-xs-regular text-text-tertiary'>{groupName}</div> | |||
| )} | |||
| <div className='ml-2 flex items-center'> | |||
| {!isShowCanNotChooseMCPTip && !canNotSelectMultiple && (notShowProvider ? notShowProviderSelectInfo : selectedInfo)} | |||
| {isShowCanNotChooseMCPTip && <McpToolNotSupportTooltip />} | |||
| {hasAction && ( | |||
| <FoldIcon className={cn('h-4 w-4 shrink-0 text-text-quaternary', isFold && 'text-text-tertiary')} /> | |||
| <FoldIcon className={cn('h-4 w-4 shrink-0 text-text-tertiary group-hover/item:text-text-tertiary', isFold && 'text-text-quaternary')} /> | |||
| )} | |||
| </div> | |||
| </div> | |||
| {hasAction && !isFold && ( | |||
| {!notShowProvider && hasAction && !isFold && ( | |||
| actions.map(action => ( | |||
| <ActionItem | |||
| key={action.name} | |||
| provider={payload} | |||
| payload={action} | |||
| onSelect={onSelect} | |||
| disabled={getIsDisabled(action)} | |||
| disabled={getIsDisabled(action) || isShowCanNotChooseMCPTip} | |||
| isAdded={getIsDisabled(action)} | |||
| /> | |||
| )) | |||
| )} | |||
| @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' | |||
| import type { BlockEnum, ToolWithProvider } from '../types' | |||
| import IndexBar, { groupItems } from './index-bar' | |||
| import type { ToolDefaultValue, ToolValue } from './types' | |||
| import type { ToolTypeEnum } from './types' | |||
| import { ViewType } from './view-type-select' | |||
| import Empty from '@/app/components/tools/add-tool-modal/empty' | |||
| import { useGetLanguage } from '@/context/i18n' | |||
| @@ -15,25 +16,34 @@ import ToolListFlatView from './tool/tool-list-flat-view/list' | |||
| 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 | |||
| toolType?: ToolTypeEnum | |||
| isAgent?: boolean | |||
| className?: string | |||
| indexBarClassName?: string | |||
| selectedTools?: ToolValue[] | |||
| canChooseMCPTool?: boolean | |||
| } | |||
| const Blocks = ({ | |||
| showWorkflowEmpty, | |||
| onSelect, | |||
| canNotSelectMultiple, | |||
| onSelectMultiple, | |||
| tools, | |||
| viewType, | |||
| hasSearchText, | |||
| toolType, | |||
| isAgent, | |||
| className, | |||
| indexBarClassName, | |||
| selectedTools, | |||
| canChooseMCPTool, | |||
| }: ToolsProps) => { | |||
| // const tools: any = [] | |||
| const { t } = useTranslation() | |||
| const language = useGetLanguage() | |||
| const isFlatView = viewType === ViewType.flat | |||
| @@ -87,15 +97,15 @@ const Blocks = ({ | |||
| const toolRefs = useRef({}) | |||
| return ( | |||
| <div className={classNames('p-1 max-w-[320px]', className)}> | |||
| <div className={classNames('max-w-[100%] p-1', className)}> | |||
| { | |||
| !tools.length && !showWorkflowEmpty && ( | |||
| <div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>{t('workflow.tabs.noResult')}</div> | |||
| !tools.length && hasSearchText && ( | |||
| <div className='mt-2 flex h-[22px] items-center px-3 text-xs font-medium text-text-secondary'>{t('workflow.tabs.noResult')}</div> | |||
| ) | |||
| } | |||
| {!tools.length && showWorkflowEmpty && ( | |||
| {!tools.length && !hasSearchText && ( | |||
| <div className='py-10'> | |||
| <Empty /> | |||
| <Empty type={toolType!} isAgent={isAgent}/> | |||
| </div> | |||
| )} | |||
| {!!tools.length && ( | |||
| @@ -107,19 +117,24 @@ const Blocks = ({ | |||
| isShowLetterIndex={isShowLetterIndex} | |||
| hasSearchText={hasSearchText} | |||
| onSelect={onSelect} | |||
| canNotSelectMultiple={canNotSelectMultiple} | |||
| onSelectMultiple={onSelectMultiple} | |||
| selectedTools={selectedTools} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| indexBar={<IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />} | |||
| /> | |||
| ) : ( | |||
| <ToolListTreeView | |||
| payload={treeViewToolsData} | |||
| hasSearchText={hasSearchText} | |||
| onSelect={onSelect} | |||
| canNotSelectMultiple={canNotSelectMultiple} | |||
| onSelectMultiple={onSelectMultiple} | |||
| selectedTools={selectedTools} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| ) | |||
| )} | |||
| {isShowLetterIndex && <IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />} | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -1,3 +1,5 @@ | |||
| import type { PluginMeta } from '../../plugins/types' | |||
| export enum TabsEnum { | |||
| Blocks = 'blocks', | |||
| Tools = 'tools', | |||
| @@ -8,6 +10,7 @@ export enum ToolTypeEnum { | |||
| BuiltIn = 'built-in', | |||
| Custom = 'custom', | |||
| Workflow = 'workflow', | |||
| MCP = 'mcp', | |||
| } | |||
| export enum BlockClassificationEnum { | |||
| @@ -30,10 +33,12 @@ export type ToolDefaultValue = { | |||
| params: Record<string, any> | |||
| paramSchemas: Record<string, any>[] | |||
| output_schema: Record<string, any> | |||
| meta?: PluginMeta | |||
| } | |||
| export type ToolValue = { | |||
| provider_name: string | |||
| provider_show_name?: string | |||
| tool_name: string | |||
| tool_label: string | |||
| tool_description?: string | |||
| @@ -0,0 +1,31 @@ | |||
| import { useEffect, useState } from 'react' | |||
| const useCheckVerticalScrollbar = (ref: React.RefObject<HTMLElement>) => { | |||
| const [hasVerticalScrollbar, setHasVerticalScrollbar] = useState(false) | |||
| useEffect(() => { | |||
| const elem = ref.current | |||
| if (!elem) return | |||
| const checkScrollbar = () => { | |||
| setHasVerticalScrollbar(elem.scrollHeight > elem.clientHeight) | |||
| } | |||
| checkScrollbar() | |||
| const resizeObserver = new ResizeObserver(checkScrollbar) | |||
| resizeObserver.observe(elem) | |||
| const mutationObserver = new MutationObserver(checkScrollbar) | |||
| mutationObserver.observe(elem, { childList: true, subtree: true, characterData: true }) | |||
| return () => { | |||
| resizeObserver.disconnect() | |||
| mutationObserver.disconnect() | |||
| } | |||
| }, [ref]) | |||
| return hasVerticalScrollbar | |||
| } | |||
| export default useCheckVerticalScrollbar | |||
| @@ -40,6 +40,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' | |||
| import { | |||
| fetchAllBuiltInTools, | |||
| fetchAllCustomTools, | |||
| fetchAllMCPTools, | |||
| fetchAllWorkflowTools, | |||
| } from '@/service/tools' | |||
| import { CollectionType } from '@/app/components/tools/types' | |||
| @@ -445,6 +446,13 @@ export const useFetchToolsData = () => { | |||
| workflowTools: workflowTools || [], | |||
| }) | |||
| } | |||
| if(type === 'mcp') { | |||
| const mcpTools = await fetchAllMCPTools() | |||
| workflowStore.setState({ | |||
| mcpTools: mcpTools || [], | |||
| }) | |||
| } | |||
| }, [workflowStore]) | |||
| return { | |||
| @@ -491,6 +499,8 @@ export const useToolIcon = (data: Node['data']) => { | |||
| const buildInTools = useStore(s => s.buildInTools) | |||
| const customTools = useStore(s => s.customTools) | |||
| const workflowTools = useStore(s => s.workflowTools) | |||
| const mcpTools = useStore(s => s.mcpTools) | |||
| const toolIcon = useMemo(() => { | |||
| if(!data) | |||
| return '' | |||
| @@ -500,11 +510,13 @@ export const useToolIcon = (data: Node['data']) => { | |||
| targetTools = buildInTools | |||
| else if (data.provider_type === CollectionType.custom) | |||
| targetTools = customTools | |||
| else if (data.provider_type === CollectionType.mcp) | |||
| targetTools = mcpTools | |||
| else | |||
| targetTools = workflowTools | |||
| return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon | |||
| } | |||
| }, [data, buildInTools, customTools, workflowTools]) | |||
| }, [data, buildInTools, customTools, mcpTools, workflowTools]) | |||
| return toolIcon | |||
| } | |||
| @@ -234,6 +234,7 @@ export const Workflow: FC<WorkflowProps> = memo(({ | |||
| handleFetchAllTools('builtin') | |||
| handleFetchAllTools('custom') | |||
| handleFetchAllTools('workflow') | |||
| handleFetchAllTools('mcp') | |||
| }, [handleFetchAllTools]) | |||
| const { | |||
| @@ -68,6 +68,7 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s | |||
| icon: getIcon(item.declaration.identity.icon), | |||
| label: item.declaration.identity.label as any, | |||
| type: CollectionType.all, | |||
| meta: item.meta, | |||
| tools: item.declaration.strategies.map(strategy => ({ | |||
| name: strategy.identity.name, | |||
| author: strategy.identity.author, | |||
| @@ -89,10 +90,13 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s | |||
| export type AgentStrategySelectorProps = { | |||
| value?: Strategy, | |||
| onChange: (value?: Strategy) => void, | |||
| canChooseMCPTool: boolean, | |||
| } | |||
| export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => { | |||
| const { value, onChange } = props | |||
| const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) | |||
| const { value, onChange, canChooseMCPTool } = props | |||
| const [open, setOpen] = useState(false) | |||
| const [viewType, setViewType] = useState<ViewType>(ViewType.flat) | |||
| const [query, setQuery] = useState('') | |||
| @@ -132,8 +136,6 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => | |||
| plugins: notInstalledPlugins = [], | |||
| } = useMarketplacePlugins() | |||
| const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) | |||
| useEffect(() => { | |||
| if (!enable_marketplace) return | |||
| if (query) { | |||
| @@ -214,21 +216,25 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => | |||
| agent_strategy_label: tool!.tool_label, | |||
| agent_output_schema: tool!.output_schema, | |||
| plugin_unique_identifier: tool!.provider_id, | |||
| meta: tool!.meta, | |||
| }) | |||
| setOpen(false) | |||
| }} | |||
| className='h-full max-h-full max-w-none overflow-y-auto' | |||
| indexBarClassName='top-0 xl:top-36' showWorkflowEmpty={false} hasSearchText={false} /> | |||
| {enable_marketplace | |||
| && <PluginList | |||
| indexBarClassName='top-0 xl:top-36' | |||
| hasSearchText={false} | |||
| canNotSelectMultiple | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| isAgent | |||
| /> | |||
| {enable_marketplace && <PluginList | |||
| ref={pluginRef} | |||
| wrapElemRef={wrapElemRef} | |||
| list={notInstalledPlugins} | |||
| searchText={query} | |||
| tags={DEFAULT_TAGS} | |||
| disableMaxWidth | |||
| /> | |||
| } | |||
| />} | |||
| </main> | |||
| </div> | |||
| </PortalToFollowElemContent> | |||
| @@ -19,6 +19,8 @@ import { useWorkflowStore } from '../../../store' | |||
| import { useRenderI18nObject } from '@/hooks/use-i18n' | |||
| import type { NodeOutPutVar } from '../../../types' | |||
| import type { Node } from 'reactflow' | |||
| import type { PluginMeta } from '@/app/components/plugins/types' | |||
| import { noop } from 'lodash' | |||
| import { useDocLink } from '@/context/i18n' | |||
| export type Strategy = { | |||
| @@ -27,6 +29,7 @@ export type Strategy = { | |||
| agent_strategy_label: string | |||
| agent_output_schema: Record<string, any> | |||
| plugin_unique_identifier: string | |||
| meta?: PluginMeta | |||
| } | |||
| export type AgentStrategyProps = { | |||
| @@ -38,6 +41,7 @@ export type AgentStrategyProps = { | |||
| nodeOutputVars?: NodeOutPutVar[], | |||
| availableNodes?: Node[], | |||
| nodeId?: string | |||
| canChooseMCPTool: boolean | |||
| } | |||
| type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field | |||
| @@ -48,7 +52,7 @@ type MultipleToolSelectorSchema = CustomSchema<'array[tools]'> | |||
| type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema | |||
| export const AgentStrategy = memo((props: AgentStrategyProps) => { | |||
| const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props | |||
| const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId, canChooseMCPTool } = props | |||
| const { t } = useTranslation() | |||
| const docLink = useDocLink() | |||
| const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration) | |||
| @@ -57,6 +61,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { | |||
| const { | |||
| setControlPromptEditorRerenderKey, | |||
| } = workflowStore.getState() | |||
| const override: ComponentProps<typeof Form<CustomField>>['override'] = [ | |||
| [FormTypeEnum.textNumber, FormTypeEnum.textInput], | |||
| (schema, props) => { | |||
| @@ -168,6 +173,8 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { | |||
| value={value} | |||
| onSelect={item => onChange(item)} | |||
| onDelete={() => onChange(null)} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| onSelectMultiple={noop} | |||
| /> | |||
| </Field> | |||
| ) | |||
| @@ -189,13 +196,14 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { | |||
| onChange={onChange} | |||
| supportCollapse | |||
| required={schema.required} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| return <div className='space-y-2'> | |||
| <AgentStrategySelector value={strategy} onChange={onStrategyChange} /> | |||
| <AgentStrategySelector value={strategy} onChange={onStrategyChange} canChooseMCPTool={canChooseMCPTool} /> | |||
| { | |||
| strategy | |||
| ? <div> | |||
| @@ -215,6 +223,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { | |||
| nodeId={nodeId} | |||
| nodeOutputVars={nodeOutputVars || []} | |||
| availableNodes={availableNodes || []} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| </div> | |||
| : <ListEmpty | |||
| @@ -76,7 +76,7 @@ const Base: FC<Props> = ({ | |||
| return ( | |||
| <Wrap className={cn(wrapClassName)} style={wrapStyle} isInNode={isInNode} isExpand={isExpand}> | |||
| <div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', isFocus ? 'border-transparent bg-components-input-bg-normal' : 'overflow-hidden border-components-input-border-hover bg-components-input-bg-hover')}> | |||
| <div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', !isFocus ? 'border-transparent bg-components-input-bg-normal' : 'overflow-hidden border-components-input-border-hover bg-components-input-bg-hover')}> | |||
| <div className='flex h-7 items-center justify-between pl-3 pr-2 pt-1'> | |||
| <div className='system-xs-semibold-uppercase text-text-secondary'>{title}</div> | |||
| <div className='flex items-center' onClick={(e) => { | |||
| @@ -23,7 +23,7 @@ export type Props = { | |||
| value?: string | object | |||
| placeholder?: React.JSX.Element | string | |||
| onChange?: (value: string) => void | |||
| title?: React.JSX.Element | |||
| title?: string | React.JSX.Element | |||
| language: CodeLanguage | |||
| headerRight?: React.JSX.Element | |||
| readOnly?: boolean | |||
| @@ -0,0 +1,35 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import cn from '@/utils/classnames' | |||
| type Props = { | |||
| value: boolean | |||
| onChange: (value: boolean) => void | |||
| } | |||
| const FormInputBoolean: FC<Props> = ({ | |||
| value, | |||
| onChange, | |||
| }) => { | |||
| return ( | |||
| <div className='flex w-full space-x-1'> | |||
| <div | |||
| className={cn( | |||
| 'system-sm-regular flex h-8 grow cursor-default items-center justify-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary', | |||
| !value && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs', | |||
| value && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs', | |||
| )} | |||
| onClick={() => onChange(true)} | |||
| >True</div> | |||
| <div | |||
| className={cn( | |||
| 'system-sm-regular flex h-8 grow cursor-default items-center justify-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary', | |||
| value && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs', | |||
| !value && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs', | |||
| )} | |||
| onClick={() => onChange(false)} | |||
| >False</div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default FormInputBoolean | |||
| @@ -0,0 +1,279 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types' | |||
| import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' | |||
| import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' | |||
| import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' | |||
| import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' | |||
| import { VarType } from '@/app/components/workflow/types' | |||
| import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types' | |||
| import FormInputTypeSwitch from './form-input-type-switch' | |||
| import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' | |||
| import Input from '@/app/components/base/input' | |||
| import { SimpleSelect } from '@/app/components/base/select' | |||
| import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input' | |||
| import FormInputBoolean from './form-input-boolean' | |||
| import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' | |||
| import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' | |||
| import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker' | |||
| import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' | |||
| import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' | |||
| import cn from '@/utils/classnames' | |||
| import type { Tool } from '@/app/components/tools/types' | |||
| type Props = { | |||
| readOnly: boolean | |||
| nodeId: string | |||
| schema: CredentialFormSchema | |||
| value: ToolVarInputs | |||
| onChange: (value: any) => void | |||
| inPanel?: boolean | |||
| currentTool?: Tool | |||
| currentProvider?: ToolWithProvider | |||
| } | |||
| const FormInputItem: FC<Props> = ({ | |||
| readOnly, | |||
| nodeId, | |||
| schema, | |||
| value, | |||
| onChange, | |||
| inPanel, | |||
| currentTool, | |||
| currentProvider, | |||
| }) => { | |||
| const language = useLanguage() | |||
| const { | |||
| placeholder, | |||
| variable, | |||
| type, | |||
| default: defaultValue, | |||
| options, | |||
| scope, | |||
| } = schema as any | |||
| const varInput = value[variable] | |||
| const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput | |||
| const isNumber = type === FormTypeEnum.textNumber | |||
| const isObject = type === FormTypeEnum.object | |||
| const isArray = type === FormTypeEnum.array | |||
| const isShowJSONEditor = isObject || isArray | |||
| const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files | |||
| const isBoolean = type === FormTypeEnum.boolean | |||
| const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect | |||
| const isAppSelector = type === FormTypeEnum.appSelector | |||
| const isModelSelector = type === FormTypeEnum.modelSelector | |||
| const showTypeSwitch = isNumber || isObject || isArray | |||
| const isConstant = varInput?.type === VarKindType.constant || !varInput?.type | |||
| const showVariableSelector = isFile || varInput?.type === VarKindType.variable | |||
| const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, { | |||
| onlyLeafNodeVar: false, | |||
| filterVar: (varPayload: Var) => { | |||
| return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type) | |||
| }, | |||
| }) | |||
| const targetVarType = () => { | |||
| if (isString) | |||
| return VarType.string | |||
| else if (isNumber) | |||
| return VarType.number | |||
| else if (type === FormTypeEnum.files) | |||
| return VarType.arrayFile | |||
| else if (type === FormTypeEnum.file) | |||
| return VarType.file | |||
| // else if (isSelect) | |||
| // return VarType.select | |||
| // else if (isAppSelector) | |||
| // return VarType.appSelector | |||
| // else if (isModelSelector) | |||
| // return VarType.modelSelector | |||
| // else if (isBoolean) | |||
| // return VarType.boolean | |||
| else if (isObject) | |||
| return VarType.object | |||
| else if (isArray) | |||
| return VarType.arrayObject | |||
| else | |||
| return VarType.string | |||
| } | |||
| const getFilterVar = () => { | |||
| if (isNumber) | |||
| return (varPayload: any) => varPayload.type === VarType.number | |||
| else if (isString) | |||
| return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type) | |||
| else if (isFile) | |||
| return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type) | |||
| else if (isBoolean) | |||
| return (varPayload: any) => varPayload.type === VarType.boolean | |||
| else if (isObject) | |||
| return (varPayload: any) => varPayload.type === VarType.object | |||
| else if (isArray) | |||
| return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type) | |||
| return undefined | |||
| } | |||
| const getVarKindType = () => { | |||
| if (isFile) | |||
| return VarKindType.variable | |||
| if (isSelect || isBoolean || isNumber || isArray || isObject) | |||
| return VarKindType.constant | |||
| if (isString) | |||
| return VarKindType.mixed | |||
| } | |||
| const handleTypeChange = (newType: string) => { | |||
| if (newType === VarKindType.variable) { | |||
| onChange({ | |||
| ...value, | |||
| [variable]: { | |||
| ...varInput, | |||
| type: VarKindType.variable, | |||
| value: '', | |||
| }, | |||
| }) | |||
| } | |||
| else { | |||
| onChange({ | |||
| ...value, | |||
| [variable]: { | |||
| ...varInput, | |||
| type: VarKindType.constant, | |||
| value: defaultValue, | |||
| }, | |||
| }) | |||
| } | |||
| } | |||
| const handleValueChange = (newValue: any) => { | |||
| onChange({ | |||
| ...value, | |||
| [variable]: { | |||
| ...varInput, | |||
| type: getVarKindType(), | |||
| value: isNumber ? Number.parseFloat(newValue) : newValue, | |||
| }, | |||
| }) | |||
| } | |||
| const handleAppOrModelSelect = (newValue: any) => { | |||
| onChange({ | |||
| ...value, | |||
| [variable]: { | |||
| ...varInput, | |||
| ...newValue, | |||
| }, | |||
| }) | |||
| } | |||
| const handleVariableSelectorChange = (newValue: ValueSelector | string, variable: string) => { | |||
| onChange({ | |||
| ...value, | |||
| [variable]: { | |||
| ...varInput, | |||
| type: VarKindType.variable, | |||
| value: newValue || '', | |||
| }, | |||
| }) | |||
| } | |||
| return ( | |||
| <div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}> | |||
| {showTypeSwitch && ( | |||
| <FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange}/> | |||
| )} | |||
| {isString && ( | |||
| <MixedVariableTextInput | |||
| readOnly={readOnly} | |||
| value={varInput?.value as string || ''} | |||
| onChange={handleValueChange} | |||
| nodesOutputVars={availableVars} | |||
| availableNodes={availableNodesWithParent} | |||
| /> | |||
| )} | |||
| {isNumber && isConstant && ( | |||
| <Input | |||
| className='h-8 grow' | |||
| type='number' | |||
| value={varInput?.value || ''} | |||
| onChange={e => handleValueChange(e.target.value)} | |||
| placeholder={placeholder?.[language] || placeholder?.en_US} | |||
| /> | |||
| )} | |||
| {isBoolean && ( | |||
| <FormInputBoolean | |||
| value={varInput?.value as boolean} | |||
| onChange={handleValueChange} | |||
| /> | |||
| )} | |||
| {isSelect && ( | |||
| <SimpleSelect | |||
| wrapperClassName='h-8 grow' | |||
| disabled={readOnly} | |||
| defaultValue={varInput?.value} | |||
| items={options.filter((option: { show_on: any[] }) => { | |||
| if (option.show_on.length) | |||
| return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value) | |||
| return true | |||
| }).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))} | |||
| onSelect={item => handleValueChange(item.value as string)} | |||
| placeholder={placeholder?.[language] || placeholder?.en_US} | |||
| /> | |||
| )} | |||
| {isShowJSONEditor && isConstant && ( | |||
| <div className='mt-1 w-full'> | |||
| <CodeEditor | |||
| title='JSON' | |||
| value={varInput?.value as any} | |||
| isExpand | |||
| isInNode | |||
| language={CodeLanguage.json} | |||
| onChange={handleValueChange} | |||
| className='w-full' | |||
| placeholder={<div className='whitespace-pre'>{placeholder?.[language] || placeholder?.en_US}</div>} | |||
| /> | |||
| </div> | |||
| )} | |||
| {isAppSelector && ( | |||
| <AppSelector | |||
| disabled={readOnly} | |||
| scope={scope || 'all'} | |||
| value={varInput?.value as any} | |||
| onSelect={handleAppOrModelSelect} | |||
| /> | |||
| )} | |||
| {isModelSelector && isConstant && ( | |||
| <ModelParameterModal | |||
| popupClassName='!w-[387px]' | |||
| isAdvancedMode | |||
| isInWorkflow | |||
| value={varInput?.value as any} | |||
| setModel={handleAppOrModelSelect} | |||
| readonly={readOnly} | |||
| scope={scope} | |||
| /> | |||
| )} | |||
| {showVariableSelector && ( | |||
| <VarReferencePicker | |||
| zIndex={inPanel ? 1000 : undefined} | |||
| className='h-8 grow' | |||
| readonly={readOnly} | |||
| isShowNodeName | |||
| nodeId={nodeId} | |||
| value={varInput?.value || []} | |||
| onChange={value => handleVariableSelectorChange(value, variable)} | |||
| filterVar={getFilterVar()} | |||
| schema={schema} | |||
| valueTypePlaceHolder={targetVarType()} | |||
| currentTool={currentTool} | |||
| currentProvider={currentProvider} | |||
| /> | |||
| )} | |||
| </div> | |||
| ) | |||
| } | |||
| export default FormInputItem | |||
| @@ -0,0 +1,47 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { | |||
| RiEditLine, | |||
| } from '@remixicon/react' | |||
| import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| import { VarType } from '@/app/components/workflow/nodes/tool/types' | |||
| import cn from '@/utils/classnames' | |||
| type Props = { | |||
| value: VarType | |||
| onChange: (value: VarType) => void | |||
| } | |||
| const FormInputTypeSwitch: FC<Props> = ({ | |||
| value, | |||
| onChange, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <div className='inline-flex h-8 shrink-0 gap-px rounded-[10px] bg-components-segmented-control-bg-normal p-0.5'> | |||
| <Tooltip | |||
| popupContent={value === VarType.variable ? '' : t('workflow.nodes.common.typeSwitch.variable')} | |||
| > | |||
| <div | |||
| className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.variable && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')} | |||
| onClick={() => onChange(VarType.variable)} | |||
| > | |||
| <Variable02 className='h-4 w-4' /> | |||
| </div> | |||
| </Tooltip> | |||
| <Tooltip | |||
| popupContent={value === VarType.constant ? '' : t('workflow.nodes.common.typeSwitch.input')} | |||
| > | |||
| <div | |||
| className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.constant && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')} | |||
| onClick={() => onChange(VarType.constant)} | |||
| > | |||
| <RiEditLine className='h-4 w-4' /> | |||
| </div> | |||
| </Tooltip> | |||
| </div> | |||
| ) | |||
| } | |||
| export default FormInputTypeSwitch | |||
| @@ -0,0 +1,22 @@ | |||
| 'use client' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| import { RiAlertFill } from '@remixicon/react' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| const McpToolNotSupportTooltip: FC = () => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <Tooltip | |||
| popupContent={ | |||
| <div className='w-[256px]'> | |||
| {t('plugin.detailPanel.toolSelector.unsupportedMCPTool')} | |||
| </div> | |||
| } | |||
| > | |||
| <RiAlertFill className='size-4 text-text-warning-secondary' /> | |||
| </Tooltip> | |||
| ) | |||
| } | |||
| export default React.memo(McpToolNotSupportTooltip) | |||
| @@ -13,7 +13,7 @@ export const SettingItem = memo(({ label, children, status, tooltip }: SettingIt | |||
| const indicator: ComponentProps<typeof Indicator>['color'] = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined | |||
| const needTooltip = ['error', 'warning'].includes(status as any) | |||
| return <div className='relative flex items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1.5 py-1 text-xs font-normal'> | |||
| <div className={classNames('shrink-0 truncate text-text-tertiary system-xs-medium-uppercase', !!children && 'max-w-[100px]')}> | |||
| <div className={classNames('system-xs-medium-uppercase max-w-full shrink-0 truncate text-text-tertiary', !!children && 'max-w-[100px]')}> | |||
| {label} | |||
| </div> | |||
| <Tooltip popupContent={tooltip} disabled={!needTooltip}> | |||
| @@ -528,6 +528,7 @@ const VarReferencePicker: FC<Props> = ({ | |||
| onChange={handleVarReferenceChange} | |||
| itemWidth={isAddBtnTrigger ? 260 : (minWidth || triggerWidth)} | |||
| isSupportFileVar={isSupportFileVar} | |||
| zIndex={zIndex} | |||
| /> | |||
| )} | |||
| </PortalToFollowElemContent> | |||
| @@ -13,6 +13,7 @@ type Props = { | |||
| onChange: (value: ValueSelector, varDetail: Var) => void | |||
| itemWidth?: number | |||
| isSupportFileVar?: boolean | |||
| zIndex?: number | |||
| } | |||
| const VarReferencePopup: FC<Props> = ({ | |||
| vars, | |||
| @@ -20,6 +21,7 @@ const VarReferencePopup: FC<Props> = ({ | |||
| onChange, | |||
| itemWidth, | |||
| isSupportFileVar = true, | |||
| zIndex, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const docLink = useDocLink() | |||
| @@ -60,6 +62,7 @@ const VarReferencePopup: FC<Props> = ({ | |||
| onChange={onChange} | |||
| itemWidth={itemWidth} | |||
| isSupportFileVar={isSupportFileVar} | |||
| zIndex={zIndex} | |||
| /> | |||
| } | |||
| </div > | |||
| @@ -46,6 +46,7 @@ type ItemProps = { | |||
| isSupportFileVar?: boolean | |||
| isException?: boolean | |||
| isLoopVar?: boolean | |||
| zIndex?: number | |||
| } | |||
| const objVarTypes = [VarType.object, VarType.file] | |||
| @@ -60,6 +61,7 @@ const Item: FC<ItemProps> = ({ | |||
| isSupportFileVar, | |||
| isException, | |||
| isLoopVar, | |||
| zIndex, | |||
| }) => { | |||
| const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties | |||
| const isFile = itemData.type === VarType.file && !isStructureOutput | |||
| @@ -171,7 +173,7 @@ const Item: FC<ItemProps> = ({ | |||
| </div > | |||
| </PortalToFollowElemTrigger > | |||
| <PortalToFollowElemContent style={{ | |||
| zIndex: 100, | |||
| zIndex: zIndex || 100, | |||
| }}> | |||
| {(isStructureOutput || isObj) && ( | |||
| <PickerStructurePanel | |||
| @@ -260,6 +262,7 @@ type Props = { | |||
| maxHeightClass?: string | |||
| onClose?: () => void | |||
| onBlur?: () => void | |||
| zIndex?: number | |||
| autoFocus?: boolean | |||
| } | |||
| const VarReferenceVars: FC<Props> = ({ | |||
| @@ -272,6 +275,7 @@ const VarReferenceVars: FC<Props> = ({ | |||
| maxHeightClass, | |||
| onClose, | |||
| onBlur, | |||
| zIndex, | |||
| autoFocus = true, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| @@ -357,6 +361,7 @@ const VarReferenceVars: FC<Props> = ({ | |||
| isSupportFileVar={isSupportFileVar} | |||
| isException={v.isException} | |||
| isLoopVar={item.isLoop} | |||
| zIndex={zIndex} | |||
| /> | |||
| ))} | |||
| </div>)) | |||
| @@ -32,6 +32,7 @@ import { | |||
| import { useNodeIterationInteractions } from '../iteration/use-interactions' | |||
| import { useNodeLoopInteractions } from '../loop/use-interactions' | |||
| import type { IterationNodeType } from '../iteration/types' | |||
| import CopyID from '../tool/components/copy-id' | |||
| import { | |||
| NodeSourceHandle, | |||
| NodeTargetHandle, | |||
| @@ -321,6 +322,11 @@ const BaseNode: FC<BaseNodeProps> = ({ | |||
| </div> | |||
| ) | |||
| } | |||
| {data.type === BlockEnum.Tool && ( | |||
| <div className='px-3 pb-2'> | |||
| <CopyID content={data.provider_id || ''} /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </div> | |||
| ) | |||
| @@ -2,10 +2,11 @@ import Tooltip from '@/app/components/base/tooltip' | |||
| import Indicator from '@/app/components/header/indicator' | |||
| import classNames from '@/utils/classnames' | |||
| import { memo, useMemo, useRef, useState } from 'react' | |||
| import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' | |||
| import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools' | |||
| import { getIconFromMarketPlace } from '@/utils/get-icon' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { Group } from '@/app/components/base/icons/src/vender/other' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| type Status = 'not-installed' | 'not-authorized' | undefined | |||
| @@ -19,19 +20,21 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => { | |||
| const { data: buildInTools } = useAllBuiltInTools() | |||
| const { data: customTools } = useAllCustomTools() | |||
| const { data: workflowTools } = useAllWorkflowTools() | |||
| const isDataReady = !!buildInTools && !!customTools && !!workflowTools | |||
| const { data: mcpTools } = useAllMCPTools() | |||
| const isDataReady = !!buildInTools && !!customTools && !!workflowTools && !!mcpTools | |||
| const currentProvider = useMemo(() => { | |||
| const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])] | |||
| const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])] | |||
| return mergedTools.find((toolWithProvider) => { | |||
| return toolWithProvider.name === providerName | |||
| return toolWithProvider.name === providerName || toolWithProvider.id === providerName | |||
| }) | |||
| }, [buildInTools, customTools, providerName, workflowTools]) | |||
| }, [buildInTools, customTools, providerName, workflowTools, mcpTools]) | |||
| const providerNameParts = providerName.split('/') | |||
| const author = providerNameParts[0] | |||
| const name = providerNameParts[1] | |||
| const icon = useMemo(() => { | |||
| if (!isDataReady) return '' | |||
| if (currentProvider) return currentProvider.icon as string | |||
| if (currentProvider) return currentProvider.icon | |||
| const iconFromMarketPlace = getIconFromMarketPlace(`${author}/${name}`) | |||
| return iconFromMarketPlace | |||
| }, [author, currentProvider, name, isDataReady]) | |||
| @@ -62,19 +65,32 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => { | |||
| )} | |||
| ref={containerRef} | |||
| > | |||
| {(!iconFetchError && isDataReady) | |||
| ? <img | |||
| src={icon} | |||
| alt='tool icon' | |||
| className={classNames( | |||
| 'w-full h-full size-3.5 object-cover', | |||
| notSuccess && 'opacity-50', | |||
| )} | |||
| onError={() => setIconFetchError(true)} | |||
| /> | |||
| : <Group className="h-3 w-3 opacity-35" /> | |||
| } | |||
| {(() => { | |||
| if (iconFetchError || !icon) | |||
| return <Group className="h-3 w-3 opacity-35" /> | |||
| if (typeof icon === 'string') { | |||
| return <img | |||
| src={icon} | |||
| alt='tool icon' | |||
| className={classNames( | |||
| 'w-full h-full size-3.5 object-cover', | |||
| notSuccess && 'opacity-50', | |||
| )} | |||
| onError={() => setIconFetchError(true)} | |||
| /> | |||
| } | |||
| if (typeof icon === 'object') { | |||
| return <AppIcon | |||
| className={classNames( | |||
| 'w-full h-full size-3.5 object-cover', | |||
| notSuccess && 'opacity-50', | |||
| )} | |||
| icon={icon?.content} | |||
| background={icon?.background} | |||
| /> | |||
| } | |||
| return <Group className="h-3 w-3 opacity-35" /> | |||
| })()} | |||
| {indicator && <Indicator color={indicator} className="absolute right-[-1px] top-[-1px]" />} | |||
| </div> | |||
| </Tooltip> | |||
| @@ -7,6 +7,7 @@ import { renderI18nObject } from '@/i18n' | |||
| const nodeDefault: NodeDefault<AgentNodeType> = { | |||
| defaultValue: { | |||
| version: '2', | |||
| }, | |||
| getAvailablePrevNodes(isChatMode) { | |||
| return isChatMode | |||
| @@ -60,15 +61,28 @@ const nodeDefault: NodeDefault<AgentNodeType> = { | |||
| const schemas = toolValue.schemas || [] | |||
| const userSettings = toolValue.settings | |||
| const reasoningConfig = toolValue.parameters | |||
| const version = payload.version | |||
| schemas.forEach((schema: any) => { | |||
| if (schema?.required) { | |||
| if (schema.form === 'form' && !userSettings[schema.name]?.value) { | |||
| if (schema.form === 'form' && !version && !userSettings[schema.name]?.value) { | |||
| return { | |||
| isValid: false, | |||
| errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), | |||
| } | |||
| } | |||
| if (schema.form === 'llm' && reasoningConfig[schema.name].auto === 0 && !userSettings[schema.name]?.value) { | |||
| if (schema.form === 'form' && version && !userSettings[schema.name]?.value.value) { | |||
| return { | |||
| isValid: false, | |||
| errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), | |||
| } | |||
| } | |||
| if (schema.form === 'llm' && !version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) { | |||
| return { | |||
| isValid: false, | |||
| errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), | |||
| } | |||
| } | |||
| if (schema.form === 'llm' && version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) { | |||
| return { | |||
| isValid: false, | |||
| errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), | |||
| @@ -104,7 +104,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => { | |||
| {t('workflow.nodes.agent.toolbox')} | |||
| </GroupLabel>}> | |||
| <div className='grid grid-cols-10 gap-0.5'> | |||
| {tools.map(tool => <ToolIcon {...tool} key={tool.id} />)} | |||
| {tools.map((tool, i) => <ToolIcon {...tool} key={tool.id + i} />)} | |||
| </div> | |||
| </Group>} | |||
| </div> | |||
| @@ -38,11 +38,11 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { | |||
| readOnly, | |||
| outputSchema, | |||
| handleMemoryChange, | |||
| canChooseMCPTool, | |||
| } = useConfig(props.id, props.data) | |||
| const { t } = useTranslation() | |||
| const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey) | |||
| return <div className='my-2'> | |||
| <Field | |||
| required | |||
| @@ -56,6 +56,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { | |||
| agent_strategy_label: inputs.agent_strategy_label!, | |||
| agent_output_schema: inputs.output_schema, | |||
| plugin_unique_identifier: inputs.plugin_unique_identifier!, | |||
| meta: inputs.meta, | |||
| } : undefined} | |||
| onStrategyChange={(strategy) => { | |||
| setInputs({ | |||
| @@ -65,6 +66,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { | |||
| agent_strategy_label: strategy?.agent_strategy_label, | |||
| output_schema: strategy!.agent_output_schema, | |||
| plugin_unique_identifier: strategy!.plugin_unique_identifier, | |||
| meta: strategy?.meta, | |||
| }) | |||
| resetEditor(Date.now()) | |||
| }} | |||
| @@ -74,6 +76,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { | |||
| nodeOutputVars={availableVars} | |||
| availableNodes={availableNodesWithParent} | |||
| nodeId={props.id} | |||
| canChooseMCPTool={canChooseMCPTool} | |||
| /> | |||
| </Field> | |||
| <div className='px-4 py-2'> | |||
| @@ -1,14 +1,17 @@ | |||
| import type { CommonNodeType, Memory } from '@/app/components/workflow/types' | |||
| import type { ToolVarInputs } from '../tool/types' | |||
| import type { PluginMeta } from '@/app/components/plugins/types' | |||
| export type AgentNodeType = CommonNodeType & { | |||
| agent_strategy_provider_name?: string | |||
| agent_strategy_name?: string | |||
| agent_strategy_label?: string | |||
| agent_parameters?: ToolVarInputs | |||
| meta?: PluginMeta | |||
| output_schema: Record<string, any> | |||
| plugin_unique_identifier?: string | |||
| memory?: Memory | |||
| version?: string | |||
| } | |||
| export enum AgentFeature { | |||
| @@ -6,13 +6,16 @@ import { | |||
| useIsChatMode, | |||
| useNodesReadOnly, | |||
| } from '@/app/components/workflow/hooks' | |||
| import { useCallback, useMemo } from 'react' | |||
| import { useCallback, useEffect, useMemo } from 'react' | |||
| import { type ToolVarInputs, VarType } from '../tool/types' | |||
| import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins' | |||
| import type { Memory, Var } from '../../types' | |||
| import { VarType as VarKindType } from '../../types' | |||
| import useAvailableVarList from '../_base/hooks/use-available-var-list' | |||
| import produce from 'immer' | |||
| import { isSupportMCP } from '@/utils/plugin-version-feature' | |||
| import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' | |||
| import { generateAgentToolValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' | |||
| export type StrategyStatus = { | |||
| plugin: { | |||
| @@ -85,11 +88,12 @@ const useConfig = (id: string, payload: AgentNodeType) => { | |||
| }) | |||
| const formData = useMemo(() => { | |||
| const paramNameList = (currentStrategy?.parameters || []).map(item => item.name) | |||
| return Object.fromEntries( | |||
| const res = Object.fromEntries( | |||
| Object.entries(inputs.agent_parameters || {}).filter(([name]) => paramNameList.includes(name)).map(([key, value]) => { | |||
| return [key, value.value] | |||
| }), | |||
| ) | |||
| return res | |||
| }, [inputs.agent_parameters, currentStrategy?.parameters]) | |||
| const onFormChange = (value: Record<string, any>) => { | |||
| const res: ToolVarInputs = {} | |||
| @@ -105,6 +109,42 @@ const useConfig = (id: string, payload: AgentNodeType) => { | |||
| }) | |||
| } | |||
| const formattingToolData = (data: any) => { | |||
| const settingValues = generateAgentToolValue(data.settings, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form !== 'llm') as any)) | |||
| const paramValues = generateAgentToolValue(data.parameters, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form === 'llm') as any), true) | |||
| const res = produce(data, (draft: any) => { | |||
| draft.settings = settingValues | |||
| draft.parameters = paramValues | |||
| }) | |||
| return res | |||
| } | |||
| const formattingLegacyData = () => { | |||
| if (inputs.version) | |||
| return inputs | |||
| const newData = produce(inputs, (draft) => { | |||
| const schemas = currentStrategy?.parameters || [] | |||
| Object.keys(draft.agent_parameters || {}).forEach((key) => { | |||
| const targetSchema = schemas.find(schema => schema.name === key) | |||
| if (targetSchema?.type === FormTypeEnum.toolSelector) | |||
| draft.agent_parameters![key].value = formattingToolData(draft.agent_parameters![key].value) | |||
| if (targetSchema?.type === FormTypeEnum.multiToolSelector) | |||
| draft.agent_parameters![key].value = draft.agent_parameters![key].value.map((tool: any) => formattingToolData(tool)) | |||
| }) | |||
| draft.version = '2' | |||
| }) | |||
| return newData | |||
| } | |||
| // formatting legacy data | |||
| useEffect(() => { | |||
| if (!currentStrategy) | |||
| return | |||
| const newData = formattingLegacyData() | |||
| setInputs(newData) | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, [currentStrategy]) | |||
| // vars | |||
| const filterMemoryPromptVar = useCallback((varPayload: Var) => { | |||
| @@ -172,6 +212,7 @@ const useConfig = (id: string, payload: AgentNodeType) => { | |||
| outputSchema, | |||
| handleMemoryChange, | |||
| isChatMode, | |||
| canChooseMCPTool: isSupportMCP(inputs.meta?.version), | |||
| } | |||
| } | |||
| @@ -6,6 +6,7 @@ import type { EditData } from './edit-card' | |||
| import { ArrayType, type Field, Type } from '../../../types' | |||
| import Toast from '@/app/components/base/toast' | |||
| import { findPropertyWithPath } from '../../../utils' | |||
| import _ from 'lodash' | |||
| type ChangeEventParams = { | |||
| path: string[], | |||
| @@ -19,7 +20,8 @@ type AddEventParams = { | |||
| } | |||
| export const useSchemaNodeOperations = (props: VisualEditorProps) => { | |||
| const { schema: jsonSchema, onChange } = props | |||
| const { schema: jsonSchema, onChange: doOnChange } = props | |||
| const onChange = doOnChange || _.noop | |||
| const backupSchema = useVisualEditorStore(state => state.backupSchema) | |||
| const setBackupSchema = useVisualEditorStore(state => state.setBackupSchema) | |||
| const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField) | |||
| @@ -2,24 +2,29 @@ import type { FC } from 'react' | |||
| import type { SchemaRoot } from '../../../types' | |||
| import SchemaNode from './schema-node' | |||
| import { useSchemaNodeOperations } from './hooks' | |||
| import cn from '@/utils/classnames' | |||
| export type VisualEditorProps = { | |||
| className?: string | |||
| schema: SchemaRoot | |||
| onChange: (schema: SchemaRoot) => void | |||
| rootName?: string | |||
| readOnly?: boolean | |||
| onChange?: (schema: SchemaRoot) => void | |||
| } | |||
| const VisualEditor: FC<VisualEditorProps> = (props) => { | |||
| const { schema } = props | |||
| const { className, schema, readOnly } = props | |||
| useSchemaNodeOperations(props) | |||
| return ( | |||
| <div className='h-full overflow-auto rounded-xl bg-background-section-burn p-1 pl-2'> | |||
| <div className={cn('h-full overflow-auto rounded-xl bg-background-section-burn p-1 pl-2', className)}> | |||
| <SchemaNode | |||
| name='structured_output' | |||
| name={props.rootName || 'structured_output'} | |||
| schema={schema} | |||
| required={false} | |||
| path={[]} | |||
| depth={0} | |||
| readOnly={readOnly} | |||
| /> | |||
| </div> | |||
| ) | |||
| @@ -19,6 +19,7 @@ type SchemaNodeProps = { | |||
| path: string[] | |||
| parentPath?: string[] | |||
| depth: number | |||
| readOnly?: boolean | |||
| } | |||
| // Support 10 levels of indentation | |||
| @@ -57,6 +58,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({ | |||
| path, | |||
| parentPath, | |||
| depth, | |||
| readOnly, | |||
| }) => { | |||
| const [isExpanded, setIsExpanded] = useState(true) | |||
| const hoveringProperty = useVisualEditorStore(state => state.hoveringProperty) | |||
| @@ -77,11 +79,13 @@ const SchemaNode: FC<SchemaNodeProps> = ({ | |||
| } | |||
| const handleMouseEnter = () => { | |||
| if(!readOnly) return | |||
| if (advancedEditing || isAddingNewField) return | |||
| setHoveringPropertyDebounced(path.join('.')) | |||
| } | |||
| const handleMouseLeave = () => { | |||
| if(!readOnly) return | |||
| if (advancedEditing || isAddingNewField) return | |||
| setHoveringPropertyDebounced(null) | |||
| } | |||
| @@ -183,7 +187,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({ | |||
| )} | |||
| { | |||
| depth === 0 && !isAddingNewField && ( | |||
| !readOnly && depth === 0 && !isAddingNewField && ( | |||
| <AddField /> | |||
| ) | |||
| } | |||
| @@ -0,0 +1,51 @@ | |||
| 'use client' | |||
| import React, { useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { RiFileCopyLine } from '@remixicon/react' | |||
| import copy from 'copy-to-clipboard' | |||
| import { debounce } from 'lodash-es' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| type Props = { | |||
| content: string | |||
| } | |||
| const prefixEmbedded = 'appOverview.overview.appInfo.embedded' | |||
| const CopyFeedbackNew = ({ content }: Props) => { | |||
| const { t } = useTranslation() | |||
| const [isCopied, setIsCopied] = useState<boolean>(false) | |||
| const onClickCopy = debounce(() => { | |||
| copy(content) | |||
| setIsCopied(true) | |||
| }, 100) | |||
| const onMouseLeave = debounce(() => { | |||
| setIsCopied(false) | |||
| }, 100) | |||
| return ( | |||
| <div className='inline-flex pb-0.5' onClick={e => e.stopPropagation()} onMouseLeave={onMouseLeave}> | |||
| <Tooltip | |||
| popupContent={ | |||
| (isCopied | |||
| ? t(`${prefixEmbedded}.copied`) | |||
| : t(`${prefixEmbedded}.copy`)) || '' | |||
| } | |||
| > | |||
| <div | |||
| className='group/copy flex items-center gap-0.5 ' | |||
| onClick={onClickCopy} | |||
| > | |||
| <div | |||
| className='system-2xs-regular cursor-pointer text-text-quaternary group-hover:text-text-tertiary' | |||
| >{content}</div> | |||
| <RiFileCopyLine className='h-3 w-3 text-text-tertiary opacity-0 group-hover/copy:opacity-100' /> | |||
| </div> | |||
| </Tooltip> | |||
| </div> | |||
| ) | |||
| } | |||
| export default CopyFeedbackNew | |||
| @@ -0,0 +1,62 @@ | |||
| import { | |||
| memo, | |||
| } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import PromptEditor from '@/app/components/base/prompt-editor' | |||
| import Placeholder from './placeholder' | |||
| import type { | |||
| Node, | |||
| NodeOutPutVar, | |||
| } from '@/app/components/workflow/types' | |||
| import { BlockEnum } from '@/app/components/workflow/types' | |||
| import cn from '@/utils/classnames' | |||
| type MixedVariableTextInputProps = { | |||
| readOnly?: boolean | |||
| nodesOutputVars?: NodeOutPutVar[] | |||
| availableNodes?: Node[] | |||
| value?: string | |||
| onChange?: (text: string) => void | |||
| } | |||
| const MixedVariableTextInput = ({ | |||
| readOnly = false, | |||
| nodesOutputVars, | |||
| availableNodes = [], | |||
| value = '', | |||
| onChange, | |||
| }: MixedVariableTextInputProps) => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <PromptEditor | |||
| wrapperClassName={cn( | |||
| 'w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1', | |||
| 'hover:border-components-input-border-hover hover:bg-components-input-bg-hover', | |||
| 'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs', | |||
| )} | |||
| className='caret:text-text-accent' | |||
| editable={!readOnly} | |||
| value={value} | |||
| workflowVariableBlock={{ | |||
| show: true, | |||
| variables: nodesOutputVars || [], | |||
| workflowNodesMap: availableNodes.reduce((acc, node) => { | |||
| acc[node.id] = { | |||
| title: node.data.title, | |||
| type: node.data.type, | |||
| } | |||
| if (node.data.type === BlockEnum.Start) { | |||
| acc.sys = { | |||
| title: t('workflow.blocks.start'), | |||
| type: BlockEnum.Start, | |||
| } | |||
| } | |||
| return acc | |||
| }, {} as any), | |||
| }} | |||
| placeholder={<Placeholder />} | |||
| onChange={onChange} | |||
| /> | |||
| ) | |||
| } | |||
| export default memo(MixedVariableTextInput) | |||
| @@ -0,0 +1,51 @@ | |||
| import { useCallback } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' | |||
| import { FOCUS_COMMAND } from 'lexical' | |||
| import { $insertNodes } from 'lexical' | |||
| import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node' | |||
| import Badge from '@/app/components/base/badge' | |||
| const Placeholder = () => { | |||
| const { t } = useTranslation() | |||
| const [editor] = useLexicalComposerContext() | |||
| const handleInsert = useCallback((text: string) => { | |||
| editor.update(() => { | |||
| const textNode = new CustomTextNode(text) | |||
| $insertNodes([textNode]) | |||
| }) | |||
| editor.dispatchCommand(FOCUS_COMMAND, undefined as any) | |||
| }, [editor]) | |||
| return ( | |||
| <div | |||
| className='pointer-events-auto flex h-full w-full cursor-text items-center px-2' | |||
| onClick={(e) => { | |||
| e.stopPropagation() | |||
| handleInsert('') | |||
| }} | |||
| > | |||
| <div className='flex grow items-center'> | |||
| {t('workflow.nodes.tool.insertPlaceholder1')} | |||
| <div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div> | |||
| <div | |||
| className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary' | |||
| onClick={((e) => { | |||
| e.stopPropagation() | |||
| handleInsert('/') | |||
| })} | |||
| > | |||
| {t('workflow.nodes.tool.insertPlaceholder2')} | |||
| </div> | |||
| </div> | |||
| <Badge | |||
| className='shrink-0' | |||
| text='String' | |||
| uppercase={false} | |||
| /> | |||
| </div> | |||
| ) | |||
| } | |||
| export default Placeholder | |||
| @@ -0,0 +1,51 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import type { ToolVarInputs } from '../../types' | |||
| import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' | |||
| import ToolFormItem from './item' | |||
| import type { ToolWithProvider } from '@/app/components/workflow/types' | |||
| import type { Tool } from '@/app/components/tools/types' | |||
| type Props = { | |||
| readOnly: boolean | |||
| nodeId: string | |||
| schema: CredentialFormSchema[] | |||
| value: ToolVarInputs | |||
| onChange: (value: ToolVarInputs) => void | |||
| onOpen?: (index: number) => void | |||
| inPanel?: boolean | |||
| currentTool?: Tool | |||
| currentProvider?: ToolWithProvider | |||
| } | |||
| const ToolForm: FC<Props> = ({ | |||
| readOnly, | |||
| nodeId, | |||
| schema, | |||
| value, | |||
| onChange, | |||
| inPanel, | |||
| currentTool, | |||
| currentProvider, | |||
| }) => { | |||
| return ( | |||
| <div className='space-y-1'> | |||
| { | |||
| schema.map((schema, index) => ( | |||
| <ToolFormItem | |||
| key={index} | |||
| readOnly={readOnly} | |||
| nodeId={nodeId} | |||
| schema={schema} | |||
| value={value} | |||
| onChange={onChange} | |||
| inPanel={inPanel} | |||
| currentTool={currentTool} | |||
| currentProvider={currentProvider} | |||
| /> | |||
| )) | |||
| } | |||
| </div> | |||
| ) | |||
| } | |||
| export default ToolForm | |||