This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import {
usePluginManifestInfo,
} from '@/service/use-plugins'
export const usePluginInstalledCheck = (providerName = '') => {
const pluginID = providerName?.split('/').splice(0, 2).join('/')
const { data: manifest } = usePluginManifestInfo(pluginID)
return {
inMarketPlace: !!manifest,
manifest: manifest?.data.plugin,
}
}

View File

@@ -0,0 +1,406 @@
'use client'
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
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 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 { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import {
useAllBuiltInTools,
useAllCustomTools,
useAllMCPTools,
useAllWorkflowTools,
useInvalidateAllBuiltInTools,
} from '@/service/use-tools'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks'
import { CollectionType } from '@/app/components/tools/types'
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import { MARKETPLACE_API_PREFIX } from '@/config'
import type { Node } from 'reactflow'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import {
AuthCategory,
PluginAuthInAgent,
} from '@/app/components/plugins/plugin-auth'
import { ReadmeEntrance } from '../../readme-panel/entrance'
type Props = {
disabled?: boolean
placement?: Placement
offset?: OffsetOptions
scope?: string
value?: ToolValue
selectedTools?: ToolValue[]
onSelect: (tool: ToolValue) => void
onSelectMultiple?: (tool: ToolValue[]) => void
isEdit?: boolean
onDelete?: () => void
supportEnableSwitch?: boolean
supportAddCustomTool?: boolean
trigger?: React.ReactNode
controlledState?: boolean
onControlledStateChange?: (state: boolean) => void
panelShowState?: boolean
onPanelShowStateChange?: (state: boolean) => void
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId?: string,
canChooseMCPTool?: boolean,
}
const ToolSelector: FC<Props> = ({
value,
selectedTools,
isEdit,
disabled,
placement = 'left',
offset = 4,
onSelect,
onSelectMultiple,
onDelete,
scope,
supportEnableSwitch,
trigger,
controlledState,
onControlledStateChange,
panelShowState,
onPanelShowStateChange,
nodeOutputVars,
availableNodes,
nodeId = '',
canChooseMCPTool,
}) => {
const { t } = useTranslation()
const [isShow, onShowChange] = useState(false)
const handleTriggerClick = () => {
if (disabled) return
onShowChange(true)
}
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools()
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
// plugin info check
const { inMarketPlace, manifest } = usePluginInstalledCheck(value?.provider_name)
const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.id === value?.provider_name
})
}, [value, buildInTools, customTools, workflowTools, mcpTools])
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
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)
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,
tool_description: tool.tool_description,
settings: settingValues,
parameters: paramValues,
enabled: tool.is_team_authorization,
extra: {
description: tool.tool_description,
},
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({
...value,
extra: {
...value?.extra,
description: e.target.value || '',
},
} as any)
}
// tool settings & params
const currentToolSettings = useMemo(() => {
if (!currentProvider) return []
return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form !== 'llm') || []
}, [currentProvider, value])
const currentToolParams = useMemo(() => {
if (!currentProvider) return []
return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form === 'llm') || []
}, [currentProvider, value])
const [currType, setCurrType] = useState('settings')
const showTabSlider = currentToolSettings.length > 0 && currentToolParams.length > 0
const userSettingsOnly = currentToolSettings.length > 0 && !currentToolParams.length
const reasoningConfigOnly = currentToolParams.length > 0 && !currentToolSettings.length
const settingsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolSettings), [currentToolSettings])
const paramsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams])
const handleSettingsFormChange = (v: Record<string, any>) => {
const newValue = getStructureValue(v)
const toolValue = {
...value,
settings: newValue,
}
onSelect(toolValue as any)
}
const handleParamsFormChange = (v: Record<string, any>) => {
const toolValue = {
...value,
parameters: v,
}
onSelect(toolValue as any)
}
const handleEnabledChange = (state: boolean) => {
onSelect({
...value,
enabled: state,
} as any)
}
// install from marketplace
const currentTool = useMemo(() => {
return currentProvider?.tools.find(tool => tool.name === value?.tool_name)
}, [currentProvider?.tools, value?.tool_name])
const manifestIcon = useMemo(() => {
if (!manifest)
return ''
return `${MARKETPLACE_API_PREFIX}/plugins/${(manifest as any).plugin_id}/icon`
}, [manifest])
const handleInstall = async () => {
invalidateAllBuiltinTools()
invalidateInstalledPluginList()
}
const handleAuthorizationItemClick = (id: string) => {
onSelect({
...value,
credential_id: id,
} as any)
}
return (
<>
<PortalToFollowElem
placement={placement}
offset={offset}
open={trigger ? controlledState : isShow}
onOpenChange={trigger ? onControlledStateChange : onShowChange}
>
<PortalToFollowElemTrigger
className='w-full'
onClick={() => {
if (!currentProvider || !currentTool) return
handleTriggerClick()
}}
>
{trigger}
{!trigger && !value?.provider_name && (
<ToolTrigger
isConfigure
open={isShow}
value={value}
provider={currentProvider}
/>
)}
{!trigger && value?.provider_name && (
<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}
onSwitchChange={handleEnabledChange}
onDelete={onDelete}
noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization}
uninstalled={!currentProvider && inMarketPlace}
versionMismatch={currentProvider && inMarketPlace && !currentTool}
installInfo={manifest?.latest_package_identifier}
onInstall={() => handleInstall()}
isError={(!currentProvider || !currentTool) && !inMarketPlace}
errorTip={
<div className='max-w-[240px] space-y-1 text-xs'>
<h3 className='font-semibold text-text-primary'>{currentTool ? t('plugin.detailPanel.toolSelector.uninstalledTitle') : t('plugin.detailPanel.toolSelector.unsupportedTitle')}</h3>
<p className='tracking-tight text-text-secondary'>{currentTool ? t('plugin.detailPanel.toolSelector.uninstalledContent') : t('plugin.detailPanel.toolSelector.unsupportedContent')}</p>
<p>
<Link href={'/plugins'} className='tracking-tight text-text-accent'>{t('plugin.detailPanel.toolSelector.uninstalledLink')}</Link>
</p>
</div>
}
canChooseMCPTool={canChooseMCPTool}
/>
)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', 'overflow-y-auto pb-2')}>
<>
<div className='system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary'>{t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}</div>
{/* base form */}
<div className='flex flex-col gap-3 px-4 py-2'>
<div className='flex flex-col gap-1'>
<div className='system-sm-semibold flex h-6 items-center justify-between text-text-secondary'>
{t('plugin.detailPanel.toolSelector.toolLabel')}
<ReadmeEntrance pluginDetail={currentProvider as any} showShortTip className='pb-0' />
</div>
<ToolPicker
placement='bottom'
offset={offset}
trigger={
<ToolTrigger
open={panelShowState || isShowChooseTool}
value={value}
provider={currentProvider}
/>
}
isShow={panelShowState || isShowChooseTool}
onShowChange={trigger ? onPanelShowStateChange as any : setIsShowChooseTool}
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
onSelectMultiple={handleSelectMultipleTool}
scope={scope}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
</div>
<div className='flex flex-col gap-1'>
<div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.descriptionLabel')}</div>
<Textarea
className='resize-none'
placeholder={t('plugin.detailPanel.toolSelector.descriptionPlaceholder')}
value={value?.extra?.description || ''}
onChange={handleDescriptionChange}
disabled={!value?.provider_name}
/>
</div>
</div>
{/* authorization */}
{currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && (
<>
<Divider className='my-1 w-full' />
<div className='px-4 py-2'>
<PluginAuthInAgent
pluginPayload={{
provider: currentProvider.name,
category: AuthCategory.tool,
providerType: currentProvider.type,
detail: currentProvider as any,
}}
credentialId={value?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/>
</div>
</>
)}
{/* tool settings */}
{(currentToolSettings.length > 0 || currentToolParams.length > 0) && currentProvider?.is_team_authorization && (
<>
<Divider className='my-1 w-full' />
{/* tabs */}
{nodeId && showTabSlider && (
<TabSlider
className='mt-1 shrink-0 px-4'
itemClassName='py-3'
noBorderBottom
smallItem
value={currType}
onChange={(value) => {
setCurrType(value)
}}
options={[
{ value: 'settings', text: t('plugin.detailPanel.toolSelector.settings')! },
{ value: 'params', text: t('plugin.detailPanel.toolSelector.params')! },
]}
/>
)}
{nodeId && showTabSlider && currType === 'params' && (
<div className='px-4 py-2'>
<div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
<div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
</div>
)}
{/* user settings only */}
{userSettingsOnly && (
<div className='p-4 pb-1'>
<div className='system-sm-semibold-uppercase text-text-primary'>{t('plugin.detailPanel.toolSelector.settings')}</div>
</div>
)}
{/* reasoning config only */}
{nodeId && reasoningConfigOnly && (
<div className='mb-1 p-4 pb-1'>
<div className='system-sm-semibold-uppercase text-text-primary'>{t('plugin.detailPanel.toolSelector.params')}</div>
<div className='pb-1'>
<div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
<div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
</div>
</div>
)}
{/* user settings form */}
{(currType === 'settings' || userSettingsOnly) && (
<div className='px-4 py-2'>
<ToolForm
inPanel
readOnly={false}
nodeId={nodeId}
schema={settingsFormSchemas as any}
value={getPlainValue(value?.settings || {})}
onChange={handleSettingsFormChange}
/>
</div>
)}
{/* reasoning config form */}
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
<ReasoningConfigForm
value={value?.parameters || {}}
onChange={handleParamsFormChange}
schemas={paramsFormSchemas as any}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
nodeId={nodeId}
/>
)}
</>
)}
</>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</>
)
}
export default React.memo(ToolSelector)

View File

@@ -0,0 +1,367 @@
import { useCallback, useState } from 'react'
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 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,
} 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>
onChange: (val: Record<string, any>) => void
schemas: any[]
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId: string
}
const ReasoningConfigForm: React.FC<Props> = ({
value,
onChange,
schemas,
nodeOutputVars,
availableNodes,
nodeId,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const getVarKindType = (type: FormTypeEnum) => {
if (type === FormTypeEnum.file || type === FormTypeEnum.files)
return VarKindType.variable
if (type === FormTypeEnum.select || type === FormTypeEnum.checkbox || 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 : { type: getVarKindType(type), value: null },
auto: val ? 1 : 0,
},
})
}
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(res)
}
}, [onChange, value])
const handleValueChange = useCallback((variable: string, varType: FormTypeEnum) => {
return (newValue: any) => {
const res = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
type: getVarKindType(varType),
value: newValue,
}
})
onChange(res)
}
}, [onChange, value])
const handleAppChange = useCallback((variable: string) => {
return (app: {
app_id: string
inputs: Record<string, any>
files?: any[]
}) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = app as any
})
onChange(newValue)
}
}, [onChange, value])
const handleModelChange = useCallback((variable: string) => {
return (model: any) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
...draft[variable].value,
...model,
} as any
})
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 [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,
tooltip,
type,
scope,
url,
input_schema,
placeholder,
options,
} = schema
const auto = value[variable]?.auto
const tooltipContent = (tooltip && (
<Tooltip
popupContent={<div className='w-[200px]'>
{tooltip[language] || tooltip.en_US}
</div>}
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 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.checkbox
const isSelect = type === FormTypeEnum.select
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 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-0.5'>
<div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'>
<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, 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, 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 && (
<MixedVariableTextInput
value={varInput?.value as string || ''}
onChange={handleValueChange(variable, type)}
nodesOutputVars={nodeOutputVars}
availableNodes={availableNodes}
/>
)}
{isNumber && isConstant && (
<Input
className='h-8 grow'
type='number'
value={varInput?.value || ''}
onChange={e => handleValueChange(variable, type)(e.target.value)}
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
)}
{isBoolean && (
<FormInputBoolean
value={varInput?.value as boolean}
onChange={handleValueChange(variable, type)}
/>
)}
{isSelect && (
<SimpleSelect
wrapperClassName='h-8 grow'
defaultValue={varInput?.value}
items={options.filter((option: { show_on: any[] }) => {
if (option.show_on.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
onSelect={item => handleValueChange(variable, type)(item.value as string)}
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
)}
{isShowJSONEditor && isConstant && (
<div className='mt-1 w-full'>
<CodeEditor
title='JSON'
value={varInput?.value as any}
isExpand
isInNode
height={100}
language={CodeLanguage.json}
onChange={handleValueChange(variable, type)}
className='w-full'
placeholder={<div className='whitespace-pre'>{placeholder?.[language] || placeholder?.en_US}</div>}
/>
</div>
)}
{isAppSelector && (
<AppSelector
disabled={false}
scope={scope || 'all'}
value={varInput as any}
onSelect={handleAppChange(variable)}
/>
)}
{isModelSelector && (
<ModelParameterModal
popupClassName='!w-[387px]'
isAdvancedMode
isInWorkflow
value={varInput}
setModel={handleModelChange(variable)}
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
href={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>
)}
</div>
)
}
return (
<div className='space-y-3 px-4 py-2'>
{!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>
)
}
export default ReasoningConfigForm

View File

@@ -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)

View File

@@ -0,0 +1,97 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowRightUpLine,
} from '@remixicon/react'
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import type { Collection } from '@/app/components/tools/types'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import { fetchBuiltInToolCredential, fetchBuiltInToolCredentialSchema } from '@/service/tools'
import Loading from '@/app/components/base/loading'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import cn from '@/utils/classnames'
type Props = {
collection: Collection
onCancel: () => void
onSaved: (value: Record<string, any>) => void
}
const ToolCredentialForm: FC<Props> = ({
collection,
onCancel,
onSaved,
}) => {
const getValueFromI18nObject = useRenderI18nObject()
const { t } = useTranslation()
const [credentialSchema, setCredentialSchema] = useState<any>(null)
const { name: collectionName } = collection
const [tempCredential, setTempCredential] = React.useState<any>({})
useEffect(() => {
fetchBuiltInToolCredentialSchema(collectionName).then(async (res) => {
const toolCredentialSchemas = toolCredentialToFormSchemas(res)
const credentialValue = await fetchBuiltInToolCredential(collectionName)
setTempCredential(credentialValue)
const defaultCredentials = addDefaultValue(credentialValue, toolCredentialSchemas)
setCredentialSchema(toolCredentialSchemas)
setTempCredential(defaultCredentials)
})
}, [])
const handleSave = () => {
for (const field of credentialSchema) {
if (field.required && !tempCredential[field.name]) {
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: getValueFromI18nObject(field.label) }) })
return
}
}
onSaved(tempCredential)
}
return (
<>
{!credentialSchema
? <div className='pt-3'><Loading type='app' /></div>
: (
<>
<div className='max-h-[464px] overflow-y-auto px-4'>
<Form
value={tempCredential}
onChange={(v) => {
setTempCredential(v)
}}
formSchemas={credentialSchema}
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>
<div className={cn('mt-1 flex justify-end px-4')} >
<div className='flex space-x-2'>
<Button onClick={onCancel}>{t('common.operation.cancel')}</Button>
<Button variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
</div>
</div>
</>
)
}
</>
)
}
export default React.memo(ToolCredentialForm)

View File

@@ -0,0 +1,180 @@
'use client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiDeleteBinLine,
RiEqualizer2Line,
RiErrorWarningFill,
} from '@remixicon/react'
import { Group } from '@/app/components/base/icons/src/vender/other'
import AppIcon from '@/app/components/base/app-icon'
import Switch from '@/app/components/base/switch'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
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
onSwitchChange?: (value: boolean) => void
onDelete?: () => void
noAuth?: boolean
isError?: boolean
errorTip?: any
uninstalled?: boolean
installInfo?: string
onInstall?: () => void
versionMismatch?: boolean
open: boolean
authRemoved?: boolean
canChooseMCPTool?: boolean,
}
const ToolItem = ({
open,
icon,
isMCPTool,
providerShowName,
providerName,
toolLabel,
showSwitch,
switchValue,
onSwitchChange,
onDelete,
noAuth,
uninstalled,
installInfo,
onInstall,
isError,
errorTip,
versionMismatch,
authRemoved,
canChooseMCPTool,
}: Props) => {
const { t } = useTranslation()
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(
'group flex cursor-default items-center gap-1 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg p-1.5 pr-2 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm',
open && 'bg-components-panel-on-panel-item-bg-hover shadow-sm',
isDeleting && 'border-state-destructive-border shadow-xs hover:bg-state-destructive-hover',
)}>
{icon && (
<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>
)}
{!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', 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 && !isShowCanNotChooseMCPTip && (
<ActionButton>
<RiEqualizer2Line className='h-4 w-4' />
</ActionButton>
)}
<div
className='cursor-pointer rounded-md p-1 text-text-tertiary hover:text-text-destructive'
onClick={(e) => {
e.stopPropagation()
onDelete?.()
}}
onMouseOver={() => setIsDeleting(true)}
onMouseLeave={() => setIsDeleting(false)}
>
<RiDeleteBinLine className='h-4 w-4' />
</div>
</div>
{!isError && !uninstalled && !noAuth && !versionMismatch && !isShowCanNotChooseMCPTip && showSwitch && (
<div className='mr-1' onClick={e => e.stopPropagation()}>
<Switch
size='md'
defaultValue={switchValue}
onChange={onSwitchChange}
/>
</div>
)}
{isShowCanNotChooseMCPTip && (
<McpToolNotSupportTooltip />
)}
{!isError && !uninstalled && !versionMismatch && noAuth && (
<Button variant='secondary' size='small'>
{t('tools.notAuthorized')}
<Indicator className='ml-2' color='orange' />
</Button>
)}
{!isError && !uninstalled && !versionMismatch && authRemoved && (
<Button variant='secondary' size='small'>
{t('plugin.auth.authRemoved')}
<Indicator className='ml-2' color='red' />
</Button>
)}
{!isError && !uninstalled && versionMismatch && installInfo && (
<div onClick={e => e.stopPropagation()}>
<SwitchPluginVersion
className='-mt-1'
uniqueIdentifier={installInfo}
tooltip={
<ToolTipContent
title={t('plugin.detailPanel.toolSelector.unsupportedTitle')}
>
{`${t('plugin.detailPanel.toolSelector.unsupportedContent')} ${t('plugin.detailPanel.toolSelector.unsupportedContent2')}`}
</ToolTipContent>
}
onChange={() => {
onInstall?.()
}}
/>
</div>
)}
{!isError && uninstalled && installInfo && (
<InstallPluginButton
onClick={e => e.stopPropagation()}
size={'small'}
uniqueIdentifier={installInfo}
onSuccess={() => {
onInstall?.()
}}
/>
)}
{isError && (
<Tooltip
popupContent={errorTip}
>
<div>
<RiErrorWarningFill className='h-4 w-4 text-text-destructive' />
</div>
</Tooltip>
)}
</div>
)
}
export default ToolItem

View File

@@ -0,0 +1,63 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowDownSLine,
RiEqualizer2Line,
} from '@remixicon/react'
import BlockIcon from '@/app/components/workflow/block-icon'
import { BlockEnum } from '@/app/components/workflow/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
open: boolean
provider?: ToolWithProvider
value?: {
provider_name: string
tool_name: string
}
isConfigure?: boolean
}
const ToolTrigger = ({
open,
provider,
value,
isConfigure,
}: Props) => {
const { t } = useTranslation()
return (
<div className={cn(
'group flex cursor-pointer items-center rounded-lg bg-components-input-bg-normal p-2 pl-3 hover:bg-state-base-hover-alt',
open && 'bg-state-base-hover-alt',
value?.provider_name && 'py-1.5 pl-1.5',
)}>
{value?.provider_name && provider && (
<div className='mr-1 shrink-0 rounded-lg border border-components-panel-border bg-components-panel-bg p-px'>
<BlockIcon
className='!h-4 !w-4'
type={BlockEnum.Tool}
toolIcon={provider.icon}
/>
</div>
)}
{value?.tool_name && (
<div className='system-sm-medium grow text-components-input-text-filled'>{value.tool_name}</div>
)}
{!value?.provider_name && (
<div className='system-sm-regular grow text-components-input-text-placeholder'>
{!isConfigure ? t('plugin.detailPanel.toolSelector.placeholder') : t('plugin.detailPanel.configureTool')}
</div>
)}
{isConfigure && (
<RiEqualizer2Line className={cn('ml-0.5 h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary', open && 'text-text-secondary')} />
)}
{!isConfigure && (
<RiArrowDownSLine className={cn('ml-0.5 h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary', open && 'text-text-secondary')} />
)}
</div>
)
}
export default ToolTrigger