feat: workflow new nodes (#4683)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Patryk Garstecki <patryk20120@yahoo.pl> Co-authored-by: Sebastian.W <thiner@gmail.com> Co-authored-by: 呆萌闷油瓶 <253605712@qq.com> Co-authored-by: takatost <takatost@users.noreply.github.com> Co-authored-by: rechardwang <wh_goodjob@163.com> Co-authored-by: Nite Knite <nkCoding@gmail.com> Co-authored-by: Chenhe Gu <guchenhe@gmail.com> Co-authored-by: Joshua <138381132+joshua20231026@users.noreply.github.com> Co-authored-by: Weaxs <459312872@qq.com> Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com> Co-authored-by: leejoo0 <81673835+leejoo0@users.noreply.github.com> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: sino <sino2322@gmail.com> Co-authored-by: Vikey Chen <vikeytk@gmail.com> Co-authored-by: wanghl <Wang-HL@users.noreply.github.com> Co-authored-by: Haolin Wang-汪皓临 <haolin.wang@atlaslovestravel.com> Co-authored-by: Zixuan Cheng <61724187+Theysua@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: Bowen Liang <bowenliang@apache.org> Co-authored-by: Bowen Liang <liangbowen@gf.com.cn> Co-authored-by: fanghongtai <42790567+fanghongtai@users.noreply.github.com> Co-authored-by: wxfanghongtai <wxfanghongtai@gf.com.cn> Co-authored-by: Matri <qjp@bithuman.io> Co-authored-by: Benjamin <benjaminx@gmail.com>
This commit is contained in:
BIN
web/app/components/tools/add-tool-modal/D.png
Normal file
BIN
web/app/components/tools/add-tool-modal/D.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
70
web/app/components/tools/add-tool-modal/category.tsx
Normal file
70
web/app/components/tools/add-tool-modal/category.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
import { useRef } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useMount } from 'ahooks'
|
||||
import { Apps02 } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import { useStore as useLabelStore } from '@/app/components/tools/labels/store'
|
||||
import { fetchLabelList } from '@/service/tools'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onSelect: (type: string) => void
|
||||
}
|
||||
|
||||
const Icon = ({ svgString, active }: { svgString: string; active: boolean }) => {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null)
|
||||
const SVGParsor = (svg: string) => {
|
||||
if (!svg)
|
||||
return null
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(svg, 'image/svg+xml')
|
||||
console.log(doc.documentElement)
|
||||
return doc.documentElement
|
||||
}
|
||||
useMount(() => {
|
||||
const svgElement = SVGParsor(svgString)
|
||||
if (svgRef.current && svgElement)
|
||||
svgRef.current.appendChild(svgElement)
|
||||
})
|
||||
return <svg className={cn('w-4 h-4 text-gray-700', active && '!text-primary-600')} ref={svgRef} />
|
||||
}
|
||||
|
||||
const Category = ({
|
||||
value,
|
||||
onSelect,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const labelList = useLabelStore(s => s.labelList)
|
||||
const setLabelList = useLabelStore(s => s.setLabelList)
|
||||
|
||||
useMount(() => {
|
||||
fetchLabelList().then((res) => {
|
||||
setLabelList(res)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='mb-3'>
|
||||
<div className='px-3 py-0.5 text-gray-500 text-xs leading-[18px] font-medium'>{t('tools.addToolModal.category').toLocaleUpperCase()}</div>
|
||||
<div className={cn('mb-0.5 p-1 pl-3 flex items-center cursor-pointer text-gray-700 text-sm leading-5 rounded-lg hover:bg-white', value === '' && '!bg-white !text-primary-600 font-medium')} onClick={() => onSelect('')}>
|
||||
<Apps02 className='shrink-0 w-4 h-4 mr-2' />
|
||||
{t('tools.type.all')}
|
||||
</div>
|
||||
{labelList.map(label => (
|
||||
<div key={label.name} title={label.label[language]} className={cn('mb-0.5 p-1 pl-3 flex items-center cursor-pointer text-gray-700 text-sm leading-5 rounded-lg hover:bg-white truncate overflow-hidden', value === label.name && '!bg-white !text-primary-600 font-medium')} onClick={() => onSelect(label.name)}>
|
||||
<div className='shrink-0 w-4 h-4 mr-2'>
|
||||
<Icon active={value === label.name} svgString={label.icon} />
|
||||
</div>
|
||||
{label.label[language]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Category
|
||||
BIN
web/app/components/tools/add-tool-modal/empty.png
Normal file
BIN
web/app/components/tools/add-tool-modal/empty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
15
web/app/components/tools/add-tool-modal/empty.tsx
Normal file
15
web/app/components/tools/add-tool-modal/empty.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Empty = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<div className="shrink-0 w-[163px] h-[149px] bg-cover bg-no-repeat bg-[url('~@/app/components/tools/add-tool-modal/empty.png')]"></div>
|
||||
<div className='mb-1 text-[13px] font-medium text-gray-700 leading-[18px]'>{t('tools.addToolModal.emptyTitle')}</div>
|
||||
<div className='text-[13px] text-gray-500 leading-[18px]'>{t('tools.addToolModal.emptyTip')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Empty
|
||||
235
web/app/components/tools/add-tool-modal/index.tsx
Normal file
235
web/app/components/tools/add-tool-modal/index.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import cn from 'classnames'
|
||||
import { useMount } from 'ahooks'
|
||||
import type { Collection, CustomCollectionBackend, Tool } from '../types'
|
||||
import Type from './type'
|
||||
import Category from './category'
|
||||
import Tools from './tools'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import { Plus, XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
|
||||
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
|
||||
import {
|
||||
createCustomCollection,
|
||||
fetchAllBuiltInTools,
|
||||
fetchAllCustomTools,
|
||||
fetchAllWorkflowTools,
|
||||
removeBuiltInToolCredential,
|
||||
updateBuiltInToolCredential,
|
||||
} from '@/service/tools'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import type { ModelConfig } from '@/models/debug'
|
||||
|
||||
type Props = {
|
||||
onHide: () => void
|
||||
}
|
||||
// Add and Edit
|
||||
const AddToolModal: FC<Props> = ({
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const [currentType, setCurrentType] = useState('builtin')
|
||||
const [currentCategory, setCurrentCategory] = useState('')
|
||||
const [keywords, setKeywords] = useState<string>('')
|
||||
const handleKeywordsChange = (value: string) => {
|
||||
setKeywords(value)
|
||||
}
|
||||
const [toolList, setToolList] = useState<ToolWithProvider[]>([])
|
||||
const [listLoading, setListLoading] = useState(true)
|
||||
const getAllTools = async () => {
|
||||
setListLoading(true)
|
||||
const buildInTools = await fetchAllBuiltInTools()
|
||||
const customTools = await fetchAllCustomTools()
|
||||
const workflowTools = await fetchAllWorkflowTools()
|
||||
const mergedToolList = [
|
||||
...buildInTools,
|
||||
...customTools,
|
||||
...workflowTools.filter((toolWithProvider) => {
|
||||
return !toolWithProvider.tools.some((tool) => {
|
||||
return !!tool.parameters.find(item => item.name === '__image')
|
||||
})
|
||||
}),
|
||||
]
|
||||
setToolList(mergedToolList)
|
||||
setListLoading(false)
|
||||
}
|
||||
const filteredList = useMemo(() => {
|
||||
return toolList.filter((toolWithProvider) => {
|
||||
if (currentType === 'all')
|
||||
return true
|
||||
else
|
||||
return toolWithProvider.type === currentType
|
||||
}).filter((toolWithProvider) => {
|
||||
if (!currentCategory)
|
||||
return true
|
||||
else
|
||||
return toolWithProvider.labels.includes(currentCategory)
|
||||
}).filter((toolWithProvider) => {
|
||||
return toolWithProvider.tools.some((tool) => {
|
||||
return tool.label[language].toLowerCase().includes(keywords.toLowerCase())
|
||||
})
|
||||
})
|
||||
}, [currentType, currentCategory, toolList, keywords, language])
|
||||
|
||||
const {
|
||||
modelConfig,
|
||||
setModelConfig,
|
||||
} = useContext(ConfigContext)
|
||||
|
||||
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
|
||||
const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => {
|
||||
await createCustomCollection(data)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
setIsShowEditCustomCollectionModal(false)
|
||||
getAllTools()
|
||||
}
|
||||
const [showSettingAuth, setShowSettingAuth] = useState(false)
|
||||
const [collection, setCollection] = useState<Collection>()
|
||||
const toolSelectHandle = (collection: Collection, tool: Tool) => {
|
||||
const parameters: Record<string, string> = {}
|
||||
if (tool.parameters) {
|
||||
tool.parameters.forEach((item) => {
|
||||
parameters[item.name] = ''
|
||||
})
|
||||
}
|
||||
|
||||
const nexModelConfig = produce(modelConfig, (draft: ModelConfig) => {
|
||||
draft.agentConfig.tools.push({
|
||||
provider_id: collection.id || collection.name,
|
||||
provider_type: collection.type,
|
||||
provider_name: collection.name,
|
||||
tool_name: tool.name,
|
||||
tool_label: tool.label[locale] || tool.label[locale.replaceAll('-', '_')],
|
||||
tool_parameters: parameters,
|
||||
enabled: true,
|
||||
})
|
||||
})
|
||||
setModelConfig(nexModelConfig)
|
||||
}
|
||||
const authSelectHandle = (provider: Collection) => {
|
||||
setCollection(provider)
|
||||
setShowSettingAuth(true)
|
||||
}
|
||||
const updateBuiltinAuth = async (value: Record<string, any>) => {
|
||||
if (!collection)
|
||||
return
|
||||
await updateBuiltInToolCredential(collection.name, value)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
await getAllTools()
|
||||
setShowSettingAuth(false)
|
||||
}
|
||||
const removeBuiltinAuth = async () => {
|
||||
if (!collection)
|
||||
return
|
||||
await removeBuiltInToolCredential(collection.name)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
await getAllTools()
|
||||
setShowSettingAuth(false)
|
||||
}
|
||||
|
||||
useMount(() => {
|
||||
getAllTools()
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
isOpen
|
||||
mask
|
||||
clickOutsideNotOpen
|
||||
onClose={onHide}
|
||||
footer={null}
|
||||
panelClassname={cn('mt-16 mx-2 sm:mr-2 mb-3 !p-0 rounded-xl', 'mt-2 !w-[640px]', '!max-w-[640px]')}
|
||||
>
|
||||
<div
|
||||
className='w-full flex bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl'
|
||||
style={{
|
||||
height: 'calc(100vh - 16px)',
|
||||
}}
|
||||
>
|
||||
<div className='relative shrink-0 w-[200px] pb-3 bg-gray-100 rounded-l-xl border-r-[0.5px] border-black/2 overflow-y-auto'>
|
||||
<div className='sticky top-0 left-0 right-0'>
|
||||
<div className='sticky top-0 left-0 right-0 px-5 py-3 text-md font-semibold text-gray-900'>{t('tools.addTool')}</div>
|
||||
<div className='px-3 pt-2 pb-4'>
|
||||
<Button type='primary' className='w-[176px] text-[13px] leading-[18px] font-medium' onClick={() => setIsShowEditCustomCollectionModal(true)}>
|
||||
<Plus className='w-4 h-4 mr-1'/>
|
||||
{t('tools.createCustomTool')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-2 py-1'>
|
||||
<Type value={currentType} onSelect={setCurrentType}/>
|
||||
<Category value={currentCategory} onSelect={setCurrentCategory}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='relative grow bg-white rounded-r-xl overflow-y-auto'>
|
||||
<div className='z-10 sticky top-0 left-0 right-0 p-2 flex items-center gap-1 bg-white'>
|
||||
<div className='grow'>
|
||||
<SearchInput className='w-full' value={keywords} onChange={handleKeywordsChange} />
|
||||
</div>
|
||||
<div className='ml-2 mr-1 w-[1px] h-4 bg-gray-200'></div>
|
||||
<div className='p-2 cursor-pointer' onClick={onHide}>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
{listLoading && (
|
||||
<div className='flex h-[200px] items-center justify-center bg-white'>
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
{!listLoading && (
|
||||
<Tools
|
||||
showWorkflowEmpty={currentType === 'workflow'}
|
||||
tools={filteredList}
|
||||
addedTools={(modelConfig?.agentConfig?.tools as any) || []}
|
||||
onSelect={toolSelectHandle}
|
||||
onAuthSetup={authSelectHandle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
{isShowEditCollectionToolModal && (
|
||||
<EditCustomToolModal
|
||||
positionLeft
|
||||
payload={null}
|
||||
onHide={() => setIsShowEditCustomCollectionModal(false)}
|
||||
onAdd={doCreateCustomToolCollection}
|
||||
/>
|
||||
)}
|
||||
{showSettingAuth && collection && (
|
||||
<ConfigCredential
|
||||
collection={collection}
|
||||
onCancel={() => setShowSettingAuth(false)}
|
||||
onSaved={updateBuiltinAuth}
|
||||
onRemove={removeBuiltinAuth}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(AddToolModal)
|
||||
146
web/app/components/tools/add-tool-modal/tools.tsx
Normal file
146
web/app/components/tools/add-tool-modal/tools.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Check, Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Tag01 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { useStore as useLabelStore } from '@/app/components/tools/labels/store'
|
||||
import Empty from '@/app/components/tools/add-tool-modal/empty'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import type { AgentTool } from '@/types/app'
|
||||
import { MAX_TOOLS_NUM } from '@/config'
|
||||
|
||||
type ToolsProps = {
|
||||
showWorkflowEmpty: boolean
|
||||
tools: ToolWithProvider[]
|
||||
addedTools: AgentTool[]
|
||||
onSelect: (provider: ToolWithProvider, tool: Tool) => void
|
||||
onAuthSetup: (provider: ToolWithProvider) => void
|
||||
}
|
||||
const Blocks = ({
|
||||
showWorkflowEmpty,
|
||||
tools,
|
||||
addedTools,
|
||||
onSelect,
|
||||
onAuthSetup,
|
||||
}: ToolsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const labelList = useLabelStore(s => s.labelList)
|
||||
const addable = addedTools.length < MAX_TOOLS_NUM
|
||||
|
||||
const renderGroup = useCallback((toolWithProvider: ToolWithProvider) => {
|
||||
const list = toolWithProvider.tools
|
||||
const needAuth = toolWithProvider.allow_delete && !toolWithProvider.is_team_authorization && toolWithProvider.type === CollectionType.builtIn
|
||||
|
||||
return (
|
||||
<div
|
||||
key={toolWithProvider.id}
|
||||
className='group mb-1 last-of-type:mb-0'
|
||||
>
|
||||
<div className='flex items-center justify-between w-full pl-3 pr-1 h-[22px] text-xs font-medium text-gray-500'>
|
||||
{toolWithProvider.label[language]}
|
||||
<a className='hidden cursor-pointer items-center group-hover:flex' href={`/tools?category=${toolWithProvider.type}`} target='_blank'>{t('tools.addToolModal.manageInTools')}<ArrowUpRight className='ml-0.5 w-3 h-3' /></a>
|
||||
</div>
|
||||
{list.map((tool) => {
|
||||
const labelContent = (() => {
|
||||
if (!tool.labels)
|
||||
return ''
|
||||
return tool.labels.map((name) => {
|
||||
const label = labelList.find(item => item.name === name)
|
||||
return label?.label[language]
|
||||
}).filter(Boolean).join(', ')
|
||||
})()
|
||||
const added = !!addedTools?.find(v => v.provider_id === toolWithProvider.id && v.provider_type === toolWithProvider.type && v.tool_name === tool.name)
|
||||
return (
|
||||
<Tooltip
|
||||
key={tool.name}
|
||||
selector={`workflow-block-tool-${tool.name}`}
|
||||
position='bottom'
|
||||
className='!p-0 !px-3 !py-2.5 !w-[210px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !bg-transparent !rounded-xl !shadow-lg translate-x-[108px]'
|
||||
htmlContent={(
|
||||
<div>
|
||||
<BlockIcon
|
||||
size='md'
|
||||
className='mb-2'
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={toolWithProvider.icon}
|
||||
/>
|
||||
<div className='mb-1 text-sm leading-5 text-gray-900'>{tool.label[language]}</div>
|
||||
<div className='text-xs text-gray-700 leading-[18px]'>{tool.description[language]}</div>
|
||||
{tool.labels?.length > 0 && (
|
||||
<div className='flex items-center shrink-0 mt-1'>
|
||||
<div className='relative w-full flex items-center gap-1 py-1 rounded-md text-gray-500' title={labelContent}>
|
||||
<Tag01 className='shrink-0 w-3 h-3 text-gray-500' />
|
||||
<div className='grow text-xs text-start leading-[18px] font-normal truncate'>{labelContent}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
noArrow
|
||||
>
|
||||
<div className='group/item flex items-center w-full pl-3 pr-1 h-8 rounded-lg hover:bg-gray-50 cursor-pointer'>
|
||||
<BlockIcon
|
||||
className={cn('mr-2 shrink-0', needAuth && 'opacity-30')}
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={toolWithProvider.icon}
|
||||
/>
|
||||
<div className={cn('grow text-sm text-gray-900 truncate', needAuth && 'opacity-30')}>{tool.label[language]}</div>
|
||||
{!needAuth && added && (
|
||||
<div className='flex items-center gap-1 rounded-[6px] border border-gray-100 px-2 py-[3px] bg-white text-gray-300 text-xs font-medium leading-[18px]'>
|
||||
<Check className='w-3 h-3'/>
|
||||
{t('tools.addToolModal.added').toLocaleUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
{!needAuth && !added && addable && (
|
||||
<Button
|
||||
type='default'
|
||||
className={cn('hidden shrink-0 items-center !h-6 px-2 py-1 bg-white text-xs font-medium leading-[18px] text-primary-600 group-hover/item:flex')}
|
||||
onClick={() => onSelect(toolWithProvider, tool)}
|
||||
>
|
||||
<Plus className='w-3 h-3'/>
|
||||
{t('tools.addToolModal.add').toLocaleUpperCase()}
|
||||
</Button>
|
||||
)}
|
||||
{needAuth && (
|
||||
<Button
|
||||
type='default'
|
||||
className={cn('hidden shrink-0 items-center !h-6 px-2 py-1 bg-white text-xs font-medium leading-[18px] text-primary-600 group-hover/item:flex')}
|
||||
onClick={() => onAuthSetup(toolWithProvider)}
|
||||
>{t('tools.auth.setup')}</Button>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}, [addable, language, t, labelList, addedTools, onAuthSetup, onSelect])
|
||||
|
||||
return (
|
||||
<div className='p-1 pb-6 max-w-[440px]'>
|
||||
{!tools.length && !showWorkflowEmpty && (
|
||||
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>{t('workflow.tabs.noResult')}</div>
|
||||
)}
|
||||
{!tools.length && showWorkflowEmpty && (
|
||||
<div className='pt-[280px]'>
|
||||
<Empty/>
|
||||
</div>
|
||||
)}
|
||||
{!!tools.length && tools.map(renderGroup)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Blocks)
|
||||
34
web/app/components/tools/add-tool-modal/type.tsx
Normal file
34
web/app/components/tools/add-tool-modal/type.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Exchange02, FileCode } from '@/app/components/base/icons/src/vender/line/others'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onSelect: (type: string) => void
|
||||
}
|
||||
|
||||
const Types = ({
|
||||
value,
|
||||
onSelect,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='mb-3'>
|
||||
<div className={cn('mb-0.5 p-1 pl-3 flex items-center cursor-pointer text-sm leading-5 rounded-lg hover:bg-white', value === 'builtin' && '!bg-white font-medium')} onClick={() => onSelect('builtin')}>
|
||||
<div className="shrink-0 w-4 h-4 mr-2 bg-cover bg-no-repeat bg-[url('~@/app/components/tools/add-tool-modal/D.png')]" />
|
||||
<span className={cn('text-gray-700', value === 'builtin' && '!text-primary-600')}>{t('tools.type.builtIn')}</span>
|
||||
</div>
|
||||
<div className={cn('mb-0.5 p-1 pl-3 flex items-center cursor-pointer text-gray-700 text-sm leading-5 rounded-lg hover:bg-white', value === 'api' && '!bg-white !text-primary-600 font-medium')} onClick={() => onSelect('api')}>
|
||||
<FileCode className='shrink-0 w-4 h-4 mr-2' />
|
||||
{t('tools.type.custom')}
|
||||
</div>
|
||||
<div className={cn('mb-0.5 p-1 pl-3 flex items-center cursor-pointer text-gray-700 text-sm leading-5 rounded-lg hover:bg-white', value === 'workflow' && '!bg-white !text-primary-600 font-medium')} onClick={() => onSelect('workflow')}>
|
||||
<Exchange02 className='shrink-0 w-4 h-4 mr-2' />
|
||||
{t('tools.type.workflow')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Types
|
||||
@@ -1,31 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Heart02 } from '../base/icons/src/vender/solid/education'
|
||||
import { BookOpen01 } from '../base/icons/src/vender/line/education'
|
||||
|
||||
const Contribute: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='shrink-0 p-2'>
|
||||
<div className='inline-block p-2 bg-white shadow-lg rounded-lg'>
|
||||
<Heart02 className='w-3 h-3 text-[#EE46BC]' />
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<div className='text-gradient'>
|
||||
{t('tools.contribute.line1')}
|
||||
</div>
|
||||
<div className='text-gradient'>
|
||||
{t('tools.contribute.line2')}
|
||||
</div>
|
||||
</div>
|
||||
<a href='https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md' target='_blank' rel='noopener noreferrer' className='mt-1 flex items-center space-x-1 text-[#155EEF]'>
|
||||
<BookOpen01 className='w-3 h-3' />
|
||||
<div className='leading-[18px] text-xs font-normal'>{t('tools.contribute.viewGuide')}</div>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Contribute)
|
||||
@@ -12,6 +12,7 @@ import Radio from '@/app/components/base/radio/ui'
|
||||
import { AuthHeaderPrefix, AuthType } from '@/app/components/tools/types'
|
||||
|
||||
type Props = {
|
||||
positionCenter?: boolean
|
||||
credential: Credential
|
||||
onChange: (credential: Credential) => void
|
||||
onHide: () => void
|
||||
@@ -38,6 +39,7 @@ const SelectItem: FC<ItemProps> = ({ text, value, isChecked, onClick }) => {
|
||||
}
|
||||
|
||||
const ConfigCredential: FC<Props> = ({
|
||||
positionCenter,
|
||||
credential,
|
||||
onChange,
|
||||
onHide,
|
||||
@@ -48,6 +50,7 @@ const ConfigCredential: FC<Props> = ({
|
||||
return (
|
||||
<Drawer
|
||||
isShow
|
||||
positionCenter={positionCenter}
|
||||
onHide={onHide}
|
||||
title={t('tools.createTool.authMethod.title')!}
|
||||
panelClassName='mt-2 !w-[520px]'
|
||||
|
||||
@@ -16,9 +16,11 @@ import Button from '@/app/components/base/button'
|
||||
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { parseParamsSchema } from '@/service/tools'
|
||||
import LabelSelector from '@/app/components/tools/labels/selector'
|
||||
|
||||
const fieldNameClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
|
||||
type Props = {
|
||||
positionLeft?: boolean
|
||||
payload: any
|
||||
onHide: () => void
|
||||
onAdd?: (payload: CustomCollectionBackend) => void
|
||||
@@ -27,6 +29,7 @@ type Props = {
|
||||
}
|
||||
// Add and Edit
|
||||
const EditCustomCollectionModal: FC<Props> = ({
|
||||
positionLeft,
|
||||
payload,
|
||||
onHide,
|
||||
onAdd,
|
||||
@@ -114,6 +117,11 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
const [currTool, setCurrTool] = useState<CustomParamSchema | null>(null)
|
||||
const [isShowTestApi, setIsShowTestApi] = useState(false)
|
||||
|
||||
const [labels, setLabels] = useState<string[]>(payload?.labels || [])
|
||||
const handleLabelSelect = (value: string[]) => {
|
||||
setLabels(value)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
// const postData = clone(customCollection)
|
||||
const postData = produce(customCollection, (draft) => {
|
||||
@@ -124,6 +132,8 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
delete draft.credentials.api_key_header_prefix
|
||||
delete draft.credentials.api_key_value
|
||||
}
|
||||
|
||||
draft.labels = labels
|
||||
})
|
||||
|
||||
if (isAdd) {
|
||||
@@ -154,10 +164,11 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
<>
|
||||
<Drawer
|
||||
isShow
|
||||
positionCenter={isAdd && !positionLeft}
|
||||
onHide={onHide}
|
||||
title={t(`tools.createTool.${isAdd ? 'title' : 'editTitle'}`)!}
|
||||
panelClassName='mt-2 !w-[640px]'
|
||||
maxWidthClassName='!max-w-[640px]'
|
||||
panelClassName='mt-2 !w-[630px]'
|
||||
maxWidthClassName='!max-w-[630px]'
|
||||
height='calc(100vh - 16px)'
|
||||
headerClassName='!border-b-black/5'
|
||||
body={
|
||||
@@ -254,6 +265,13 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
<div>
|
||||
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div>
|
||||
<LabelSelector value={labels} onChange={handleLabelSelect} />
|
||||
</div>
|
||||
|
||||
{/* Privacy Policy */}
|
||||
<div>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.privacyPolicy')}</div>
|
||||
<input
|
||||
@@ -288,7 +306,7 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
)
|
||||
}
|
||||
<div className='flex space-x-2 '>
|
||||
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700 bg-white' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium' type='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -308,6 +326,7 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
/>}
|
||||
{credentialsModalShow && (
|
||||
<ConfigCredentials
|
||||
positionCenter={isAdd}
|
||||
credential={credential}
|
||||
onChange={setCredential}
|
||||
onHide={() => setCredentialsModalShow(false)}
|
||||
@@ -315,6 +334,7 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
}
|
||||
{isShowTestApi && (
|
||||
<TestApi
|
||||
positionCenter={isAdd}
|
||||
tool={currTool as CustomParamSchema}
|
||||
customCollection={customCollection}
|
||||
onHide={() => setIsShowTestApi(false)}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { testAPIAvailable } from '@/service/tools'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
|
||||
type Props = {
|
||||
positionCenter?: boolean
|
||||
customCollection: CustomCollectionBackend
|
||||
tool: CustomParamSchema
|
||||
onHide: () => void
|
||||
@@ -21,6 +22,7 @@ type Props = {
|
||||
const keyClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
|
||||
|
||||
const TestApi: FC<Props> = ({
|
||||
positionCenter,
|
||||
customCollection,
|
||||
tool,
|
||||
onHide,
|
||||
@@ -57,6 +59,7 @@ const TestApi: FC<Props> = ({
|
||||
<>
|
||||
<Drawer
|
||||
isShow
|
||||
positionCenter={positionCenter}
|
||||
onHide={onHide}
|
||||
title={`${t('tools.test.title')} ${toolName}`}
|
||||
panelClassName='mt-2 !w-[600px]'
|
||||
@@ -119,6 +122,7 @@ const TestApi: FC<Props> = ({
|
||||
/>
|
||||
{credentialsModalShow && (
|
||||
<ConfigCredentials
|
||||
positionCenter={positionCenter}
|
||||
credential={tempCredential}
|
||||
onChange={setTempCredential}
|
||||
onHide={() => setCredentialsModalShow(false)}
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import Button from '../base/button'
|
||||
import { Plus } from '../base/icons/src/vender/line/general'
|
||||
import Toast from '../base/toast'
|
||||
import type { Collection, CustomCollectionBackend, Tool } from './types'
|
||||
import { CollectionType, LOC } from './types'
|
||||
import ToolNavList from './tool-nav-list'
|
||||
import Search from './search'
|
||||
import Contribute from './contribute'
|
||||
import ToolList from './tool-list'
|
||||
import EditCustomToolModal from './edit-custom-collection-modal'
|
||||
import NoCustomTool from './info/no-custom-tool'
|
||||
import NoSearchRes from './info/no-search-res'
|
||||
import NoCustomToolPlaceholder from './no-custom-tool-placeholder'
|
||||
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||
import TabSlider from '@/app/components/base/tab-slider'
|
||||
import { createCustomCollection, fetchCollectionList as doFetchCollectionList, fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList } from '@/service/tools'
|
||||
import type { AgentTool } from '@/types/app'
|
||||
|
||||
type Props = {
|
||||
loc: LOC
|
||||
addedTools?: AgentTool[]
|
||||
onAddTool?: (collection: Collection, payload: Tool) => void
|
||||
selectedProviderId?: string
|
||||
}
|
||||
|
||||
const Tools: FC<Props> = ({
|
||||
loc,
|
||||
addedTools,
|
||||
onAddTool,
|
||||
selectedProviderId,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isInToolsPage = loc === LOC.tools
|
||||
const isInDebugPage = !isInToolsPage
|
||||
|
||||
const [collectionList, setCollectionList] = useState<Collection[]>([])
|
||||
const [currCollectionIndex, setCurrCollectionIndex] = useState<number | null>(null)
|
||||
|
||||
const [isDetailLoading, setIsDetailLoading] = useState(false)
|
||||
|
||||
const fetchCollectionList = async () => {
|
||||
const list = await doFetchCollectionList()
|
||||
setCollectionList(list)
|
||||
if (list.length > 0 && currCollectionIndex === null) {
|
||||
let index = 0
|
||||
if (selectedProviderId)
|
||||
index = list.findIndex(item => item.id === selectedProviderId)
|
||||
|
||||
setCurrCollectionIndex(index || 0)
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchCollectionList()
|
||||
}, [])
|
||||
|
||||
const collectionTypeOptions = (() => {
|
||||
const res = [
|
||||
{ value: CollectionType.builtIn, text: t('tools.type.builtIn') },
|
||||
{ value: CollectionType.custom, text: t('tools.type.custom') },
|
||||
]
|
||||
if (!isInToolsPage)
|
||||
res.unshift({ value: CollectionType.all, text: t('tools.type.all') })
|
||||
return res
|
||||
})()
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
const [toolPageCollectionType, setToolPageCollectionType] = useTabSearchParams({
|
||||
defaultTab: collectionTypeOptions[0].value,
|
||||
})
|
||||
const [appPageCollectionType, setAppPageCollectionType] = useState(collectionTypeOptions[0].value)
|
||||
const { collectionType, setCollectionType } = (() => {
|
||||
if (isInToolsPage) {
|
||||
return {
|
||||
collectionType: toolPageCollectionType,
|
||||
setCollectionType: setToolPageCollectionType,
|
||||
}
|
||||
}
|
||||
return {
|
||||
collectionType: appPageCollectionType,
|
||||
setCollectionType: setAppPageCollectionType,
|
||||
}
|
||||
})()
|
||||
|
||||
const showCollectionList = (() => {
|
||||
let typeFilteredList: Collection[] = []
|
||||
if (collectionType === CollectionType.all)
|
||||
typeFilteredList = collectionList.filter(item => item.type !== CollectionType.model)
|
||||
else if (collectionType === CollectionType.builtIn)
|
||||
typeFilteredList = collectionList.filter(item => item.type === CollectionType.builtIn)
|
||||
else if (collectionType === CollectionType.custom)
|
||||
typeFilteredList = collectionList.filter(item => item.type === CollectionType.custom)
|
||||
if (query)
|
||||
return typeFilteredList.filter(item => item.name.includes(query))
|
||||
|
||||
return typeFilteredList
|
||||
})()
|
||||
|
||||
const hasNoCustomCollection = !collectionList.find(item => item.type === CollectionType.custom)
|
||||
|
||||
useEffect(() => {
|
||||
setCurrCollectionIndex(0)
|
||||
}, [collectionType])
|
||||
|
||||
const currCollection = (() => {
|
||||
if (currCollectionIndex === null)
|
||||
return null
|
||||
return showCollectionList[currCollectionIndex]
|
||||
})()
|
||||
|
||||
const [currTools, setCurrentTools] = useState<Tool[]>([])
|
||||
useEffect(() => {
|
||||
if (!currCollection)
|
||||
return
|
||||
|
||||
(async () => {
|
||||
setIsDetailLoading(true)
|
||||
try {
|
||||
if (currCollection.type === CollectionType.builtIn) {
|
||||
const list = await fetchBuiltInToolList(currCollection.name)
|
||||
setCurrentTools(list)
|
||||
}
|
||||
else if (currCollection.type === CollectionType.model) {
|
||||
const list = await fetchModelToolList(currCollection.name)
|
||||
setCurrentTools(list)
|
||||
}
|
||||
else {
|
||||
const list = await fetchCustomToolList(currCollection.name)
|
||||
setCurrentTools(list)
|
||||
}
|
||||
}
|
||||
catch (e) { }
|
||||
setIsDetailLoading(false)
|
||||
})()
|
||||
}, [currCollection?.name, currCollection?.type])
|
||||
|
||||
const [isShowEditCollectionToolModal, setIsShowEditCollectionToolModal] = useState(false)
|
||||
const handleCreateToolCollection = () => {
|
||||
setIsShowEditCollectionToolModal(true)
|
||||
}
|
||||
|
||||
const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => {
|
||||
await createCustomCollection(data)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
await fetchCollectionList()
|
||||
setIsShowEditCollectionToolModal(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex h-full'>
|
||||
{/* sidebar */}
|
||||
<div className={cn(isInToolsPage ? 'sm:w-[216px] px-4' : 'sm:w-[256px] px-3', 'flex flex-col w-16 shrink-0 pb-2')}>
|
||||
{isInToolsPage && (
|
||||
<Button className='mt-6 flex items-center !h-8 pl-4' type='primary' onClick={handleCreateToolCollection}>
|
||||
<Plus className='w-4 h-4 mr-1' />
|
||||
<div className='leading-[18px] text-[13px] font-medium truncate'>{t('tools.createCustomTool')}</div>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isInDebugPage && (
|
||||
<div className='mt-6 flex space-x-1 items-center'>
|
||||
<Search
|
||||
className='grow'
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
/>
|
||||
<Button className='flex items-center justify-center !w-8 !h-8 !p-0' type='primary'>
|
||||
<Plus className='w-4 h-4' onClick={handleCreateToolCollection} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabSlider
|
||||
className='mt-3'
|
||||
itemWidth={isInToolsPage ? 89 : 75}
|
||||
value={collectionType}
|
||||
onChange={v => setCollectionType(v as CollectionType)}
|
||||
options={collectionTypeOptions}
|
||||
/>
|
||||
{isInToolsPage && (
|
||||
<Search
|
||||
className='mt-5'
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(collectionType === CollectionType.custom && hasNoCustomCollection)
|
||||
? (
|
||||
<div className='grow h-0 p-2 pt-8'>
|
||||
<NoCustomTool onCreateTool={handleCreateToolCollection} />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
(showCollectionList.length > 0 || !query)
|
||||
? <ToolNavList
|
||||
className='mt-2 grow height-0 overflow-y-auto'
|
||||
currentIndex={currCollectionIndex || 0}
|
||||
list={showCollectionList}
|
||||
onChosen={setCurrCollectionIndex}
|
||||
/>
|
||||
: (
|
||||
<div className='grow h-0 p-2 pt-8'>
|
||||
<NoSearchRes
|
||||
onReset={() => { setQuery('') }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{loc === LOC.tools && (
|
||||
<Contribute />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* tools */}
|
||||
<div className={cn('grow h-full overflow-hidden p-2')}>
|
||||
<div className='h-full bg-white rounded-2xl'>
|
||||
{!(collectionType === CollectionType.custom && hasNoCustomCollection) && showCollectionList.length > 0 && (
|
||||
<ToolList
|
||||
collection={currCollection}
|
||||
list={currTools}
|
||||
loc={loc}
|
||||
addedTools={addedTools}
|
||||
onAddTool={onAddTool}
|
||||
onRefreshData={fetchCollectionList}
|
||||
onCollectionRemoved={() => {
|
||||
setCurrCollectionIndex(0)
|
||||
fetchCollectionList()
|
||||
}}
|
||||
isLoading={isDetailLoading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{collectionType === CollectionType.custom && hasNoCustomCollection && (
|
||||
<NoCustomToolPlaceholder />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isShowEditCollectionToolModal && (
|
||||
<EditCustomToolModal
|
||||
payload={null}
|
||||
onHide={() => setIsShowEditCollectionToolModal(false)}
|
||||
onAdd={doCreateCustomToolCollection}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Tools)
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Icon3Dots } from '../../base/icons/src/public/other'
|
||||
import { Tools } from '@/app/components/base/icons/src/public/header-nav/tools'
|
||||
type Props = {
|
||||
onCreateTool: () => void
|
||||
}
|
||||
|
||||
const NoCustomTool: FC<Props> = ({
|
||||
onCreateTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='inline-flex p-3 rounded-lg bg-gray-50 border border-[#EAECF5]'>
|
||||
<Tools className='w-5 h-5 text-gray-500' />
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<div className='leading-5 text-sm font-medium text-gray-500'>
|
||||
{t('tools.noCustomTool.title')}<Icon3Dots className='inline relative -top-3 -left-1.5' />
|
||||
</div>
|
||||
<div className='mt-1 leading-[18px] text-xs font-normal text-gray-500'>
|
||||
{t('tools.noCustomTool.content')}
|
||||
</div>
|
||||
<div
|
||||
className='mt-2 leading-[18px] text-xs font-medium text-[#155EEF] uppercase cursor-pointer'
|
||||
onClick={onCreateTool}
|
||||
>
|
||||
{t('tools.noCustomTool.createTool')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(NoCustomTool)
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SearchMd } from '../../base/icons/src/vender/solid/general'
|
||||
|
||||
type Props = {
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
const NoSearchRes: FC<Props> = ({
|
||||
onReset,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='inline-flex p-3 rounded-lg bg-gray-50 border border-[#EAECF5]'>
|
||||
<SearchMd className='w-5 h-5 text-gray-500' />
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<div className='leading-5 text-sm font-medium text-gray-500'>
|
||||
{t('tools.noSearchRes.title')}
|
||||
</div>
|
||||
<div className='mt-1 leading-[18px] text-xs font-normal text-gray-500'>
|
||||
{t('tools.noSearchRes.content')}
|
||||
</div>
|
||||
<div
|
||||
className='mt-2 leading-[18px] text-xs font-medium text-[#155EEF] uppercase cursor-pointer'
|
||||
onClick={onReset}
|
||||
>
|
||||
{t('tools.noSearchRes.reset')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(NoSearchRes)
|
||||
6
web/app/components/tools/labels/constant.ts
Normal file
6
web/app/components/tools/labels/constant.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
export type Label = {
|
||||
name: string
|
||||
icon: string
|
||||
label: TypeWithI18N
|
||||
}
|
||||
144
web/app/components/tools/labels/filter.tsx
Normal file
144
web/app/components/tools/labels/filter.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import type { FC } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useDebounceFn, useMount } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import { useStore as useLabelStore } from './store'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import type { Label } from '@/app/components/tools/labels/constant'
|
||||
import { fetchLabelList } from '@/service/tools'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
|
||||
type LabelFilterProps = {
|
||||
value: string[]
|
||||
onChange: (v: string[]) => void
|
||||
}
|
||||
const LabelFilter: FC<LabelFilterProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const labelList = useLabelStore(s => s.labelList)
|
||||
const setLabelList = useLabelStore(s => s.setLabelList)
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchKeywords(keywords)
|
||||
}, { wait: 500 })
|
||||
const handleKeywordsChange = (value: string) => {
|
||||
setKeywords(value)
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const filteredLabelList = useMemo(() => {
|
||||
return labelList.filter(label => label.name.includes(searchKeywords))
|
||||
}, [labelList, searchKeywords])
|
||||
|
||||
const currentLabel = useMemo(() => {
|
||||
return labelList.find(label => label.name === value[0])
|
||||
}, [value, labelList])
|
||||
|
||||
const selectLabel = (label: Label) => {
|
||||
if (value.includes(label.name))
|
||||
onChange(value.filter(v => v !== label.name))
|
||||
else
|
||||
onChange([...value, label.name])
|
||||
}
|
||||
|
||||
useMount(() => {
|
||||
fetchLabelList().then((res) => {
|
||||
setLabelList(res)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<div className='relative'>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className='block'
|
||||
>
|
||||
<div className={cn(
|
||||
'flex items-center gap-1 px-2 h-8 rounded-lg border-[0.5px] border-transparent bg-gray-200 cursor-pointer hover:bg-gray-300',
|
||||
open && !value.length && '!bg-gray-300 hover:bg-gray-300',
|
||||
!open && !!value.length && '!bg-white/80 shadow-xs !border-black/5 hover:!bg-gray-200',
|
||||
open && !!value.length && '!bg-gray-200 !border-black/5 shadow-xs hover:!bg-gray-200',
|
||||
)}>
|
||||
<div className='p-[1px]'>
|
||||
<Tag01 className='h-3.5 w-3.5 text-gray-700' />
|
||||
</div>
|
||||
<div className='text-[13px] leading-[18px] text-gray-700'>
|
||||
{!value.length && t('common.tag.placeholder')}
|
||||
{!!value.length && currentLabel?.label[language]}
|
||||
</div>
|
||||
{value.length > 1 && (
|
||||
<div className='text-xs font-medium leading-[18px] text-gray-500'>{`+${value.length - 1}`}</div>
|
||||
)}
|
||||
{!value.length && (
|
||||
<div className='p-[1px]'>
|
||||
<ChevronDown className='h-3.5 w-3.5 text-gray-700'/>
|
||||
</div>
|
||||
)}
|
||||
{!!value.length && (
|
||||
<div className='p-[1px] cursor-pointer group/clear' onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange([])
|
||||
}}>
|
||||
<XCircle className='h-3.5 w-3.5 text-gray-400 group-hover/clear:text-gray-600'/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1002]'>
|
||||
<div className='relative w-[240px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
|
||||
<div className='p-2 border-b-[0.5px] border-black/5'>
|
||||
<SearchInput white value={keywords} onChange={handleKeywordsChange} />
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
{filteredLabelList.map(label => (
|
||||
<div
|
||||
key={label.name}
|
||||
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
|
||||
onClick={() => selectLabel(label)}
|
||||
>
|
||||
<div title={label.label[language]} className='grow text-sm text-gray-700 leading-5 truncate'>{label.label[language]}</div>
|
||||
{value.includes(label.name) && <Check className='shrink-0 w-4 h-4 text-primary-600'/>}
|
||||
</div>
|
||||
))}
|
||||
{!filteredLabelList.length && (
|
||||
<div className='p-3 flex flex-col items-center gap-1'>
|
||||
<Tag03 className='h-6 w-6 text-gray-300' />
|
||||
<div className='text-gray-500 text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default LabelFilter
|
||||
128
web/app/components/tools/labels/selector.tsx
Normal file
128
web/app/components/tools/labels/selector.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { FC } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useDebounceFn, useMount } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import { useStore as useLabelStore } from './store'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import type { Label } from '@/app/components/tools/labels/constant'
|
||||
import { fetchLabelList } from '@/service/tools'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
|
||||
type LabelSelectorProps = {
|
||||
value: string[]
|
||||
onChange: (v: string[]) => void
|
||||
}
|
||||
const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const labelList = useLabelStore(s => s.labelList)
|
||||
const setLabelList = useLabelStore(s => s.setLabelList)
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchKeywords(keywords)
|
||||
}, { wait: 500 })
|
||||
const handleKeywordsChange = (value: string) => {
|
||||
setKeywords(value)
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const filteredLabelList = useMemo(() => {
|
||||
return labelList.filter(label => label.name.includes(searchKeywords))
|
||||
}, [labelList, searchKeywords])
|
||||
|
||||
const selectedLabels = useMemo(() => {
|
||||
return value.map(v => labelList.find(l => l.name === v)?.label[language]).join(', ')
|
||||
}, [value, labelList, language])
|
||||
|
||||
const selectLabel = (label: Label) => {
|
||||
if (value.includes(label.name))
|
||||
onChange(value.filter(v => v !== label.name))
|
||||
else
|
||||
onChange([...value, label.name])
|
||||
}
|
||||
|
||||
useMount(() => {
|
||||
fetchLabelList().then((res) => {
|
||||
setLabelList(res)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<div className='relative'>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className='block'
|
||||
>
|
||||
<div className={cn(
|
||||
'flex items-center gap-1 px-3 h-9 rounded-lg border-[0.5px] border-transparent bg-gray-100 cursor-pointer hover:bg-gray-200',
|
||||
open && '!bg-gray-200 hover:bg-gray-200',
|
||||
)}>
|
||||
<div title={value.length > 0 ? selectedLabels : ''} className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate', !value.length && '!text-gray-400')}>
|
||||
{!value.length && t('tools.createTool.toolInput.labelPlaceholder')}
|
||||
{!!value.length && selectedLabels}
|
||||
</div>
|
||||
<div className='shrink-0 ml-1 text-gray-700 opacity-60'>
|
||||
<ChevronDown className='h-4 w-4'/>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1040]'>
|
||||
<div className='relative w-[591px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
|
||||
<div className='p-2 border-b-[0.5px] border-black/5'>
|
||||
<SearchInput white value={keywords} onChange={handleKeywordsChange} />
|
||||
</div>
|
||||
<div className='p-1 max-h-[264px] overflow-y-auto'>
|
||||
{filteredLabelList.map(label => (
|
||||
<div
|
||||
key={label.name}
|
||||
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
|
||||
onClick={() => selectLabel(label)}
|
||||
>
|
||||
<Checkbox
|
||||
className='shrink-0'
|
||||
checked={value.includes(label.name)}
|
||||
onCheck={() => {}}
|
||||
/>
|
||||
<div title={label.label[language]} className='grow text-sm text-gray-700 leading-5 truncate'>{label.label[language]}</div>
|
||||
</div>
|
||||
))}
|
||||
{!filteredLabelList.length && (
|
||||
<div className='p-3 flex flex-col items-center gap-1'>
|
||||
<Tag03 className='h-6 w-6 text-gray-300' />
|
||||
<div className='text-gray-500 text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default LabelSelector
|
||||
15
web/app/components/tools/labels/store.ts
Normal file
15
web/app/components/tools/labels/store.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { create } from 'zustand'
|
||||
import type { Label } from './constant'
|
||||
|
||||
type State = {
|
||||
labelList: Label[]
|
||||
}
|
||||
|
||||
type Action = {
|
||||
setLabelList: (labelList?: Label[]) => void
|
||||
}
|
||||
|
||||
export const useStore = create<State & Action>(set => ({
|
||||
labelList: [],
|
||||
setLabelList: labelList => set(() => ({ labelList })),
|
||||
}))
|
||||
@@ -1,26 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BookOpen01 } from '../base/icons/src/vender/line/education'
|
||||
import { Icon3Dots } from '../base/icons/src/public/other'
|
||||
|
||||
const NoCustomToolPlaceHolder: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='h-full flex items-center justify-center'>
|
||||
<div className='p-6 rounded-xl bg-gray-50'>
|
||||
<div className='inline-flex p-2 border border-gray-200 rounded-md'>
|
||||
<BookOpen01 className='w-4 h-4 text-primary-600' />
|
||||
</div>
|
||||
<div className='mt-3 leading-6 text-base font-medium text-gray-700'>
|
||||
{t('tools.noCustomTool.title')}
|
||||
<Icon3Dots className='inline relative -top-3 -left-1.5' />
|
||||
</div>
|
||||
<div className='mt-2 leading-5 text-sm font-normal text-gray-700'>{t('tools.noCustomTool.content')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(NoCustomToolPlaceHolder)
|
||||
117
web/app/components/tools/provider-list.tsx
Normal file
117
web/app/components/tools/provider-list.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Collection } from './types'
|
||||
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
||||
import LabelFilter from '@/app/components/tools/labels/filter'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import { DotsGrid, XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Colors } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { Route } from '@/app/components/base/icons/src/vender/line/mapsAndTravel'
|
||||
import CustomCreateCard from '@/app/components/tools/provider/custom-create-card'
|
||||
import ContributeCard from '@/app/components/tools/provider/contribute'
|
||||
import ProviderCard from '@/app/components/tools/provider/card'
|
||||
import ProviderDetail from '@/app/components/tools/provider/detail'
|
||||
import Empty from '@/app/components/tools/add-tool-modal/empty'
|
||||
import { fetchCollectionList } from '@/service/tools'
|
||||
|
||||
const ProviderList = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [activeTab, setActiveTab] = useTabSearchParams({
|
||||
defaultTab: 'builtin',
|
||||
})
|
||||
const options = [
|
||||
{ value: 'builtin', text: t('tools.type.builtIn'), icon: <DotsGrid className='w-[14px] h-[14px] mr-1'/> },
|
||||
{ value: 'api', text: t('tools.type.custom'), icon: <Colors className='w-[14px] h-[14px] mr-1'/> },
|
||||
{ value: 'workflow', text: t('tools.type.workflow'), icon: <Route className='w-[14px] h-[14px] mr-1'/> },
|
||||
]
|
||||
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
|
||||
const handleTagsChange = (value: string[]) => {
|
||||
setTagFilterValue(value)
|
||||
}
|
||||
const [keywords, setKeywords] = useState<string>('')
|
||||
const handleKeywordsChange = (value: string) => {
|
||||
setKeywords(value)
|
||||
}
|
||||
|
||||
const [collectionList, setCollectionList] = useState<Collection[]>([])
|
||||
const filteredCollectionList = useMemo(() => {
|
||||
return collectionList.filter((collection) => {
|
||||
if (collection.type !== activeTab)
|
||||
return false
|
||||
if (tagFilterValue.length > 0 && (!collection.labels || collection.labels.every(label => !tagFilterValue.includes(label))))
|
||||
return false
|
||||
if (keywords)
|
||||
return collection.name.toLowerCase().includes(keywords.toLowerCase())
|
||||
return true
|
||||
})
|
||||
}, [activeTab, tagFilterValue, keywords, collectionList])
|
||||
const getProviderList = async () => {
|
||||
const list = await fetchCollectionList()
|
||||
setCollectionList([...list])
|
||||
}
|
||||
useEffect(() => {
|
||||
getProviderList()
|
||||
}, [])
|
||||
|
||||
const [currentProvider, setCurrentProvider] = useState<Collection | undefined>()
|
||||
useEffect(() => {
|
||||
if (currentProvider && collectionList.length > 0) {
|
||||
const newCurrentProvider = collectionList.find(collection => collection.id === currentProvider.id)
|
||||
setCurrentProvider(newCurrentProvider)
|
||||
}
|
||||
}, [collectionList, currentProvider])
|
||||
|
||||
return (
|
||||
<div className='relative flex overflow-hidden bg-gray-100 shrink-0 h-0 grow'>
|
||||
<div className='relative flex flex-col overflow-y-auto bg-gray-100 grow'>
|
||||
<div className={cn(
|
||||
'z-20 sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 flex-wrap gap-y-2',
|
||||
currentProvider && 'pr-6',
|
||||
)}>
|
||||
<TabSliderNew
|
||||
value={activeTab}
|
||||
onChange={(state) => {
|
||||
setActiveTab(state)
|
||||
if (state !== activeTab)
|
||||
setCurrentProvider(undefined)
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
<div className='flex items-center gap-2'>
|
||||
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
|
||||
<SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(
|
||||
'relative grid content-start grid-cols-1 gap-4 px-12 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0',
|
||||
currentProvider && 'pr-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
)}>
|
||||
{activeTab === 'builtin' && <ContributeCard />}
|
||||
{activeTab === 'api' && <CustomCreateCard onRefreshData={getProviderList}/>}
|
||||
{filteredCollectionList.map(collection => (
|
||||
<ProviderCard
|
||||
active={currentProvider?.id === collection.id}
|
||||
onSelect={() => setCurrentProvider(collection)}
|
||||
key={collection.id}
|
||||
collection={collection}
|
||||
/>
|
||||
))}
|
||||
{!filteredCollectionList.length && <div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'><Empty/></div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(
|
||||
'shrink-0 w-0 border-l-[0.5px] border-black/8 overflow-y-auto transition-all duration-200 ease-in-out',
|
||||
currentProvider && 'w-[420px]',
|
||||
)}>
|
||||
{currentProvider && <ProviderDetail collection={currentProvider} onRefreshData={getProviderList} />}
|
||||
</div>
|
||||
<div className='absolute top-5 right-5 p-1 cursor-pointer' onClick={() => setCurrentProvider(undefined)}><XClose className='w-4 h-4'/></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
ProviderList.displayName = 'ToolProviderList'
|
||||
export default ProviderList
|
||||
83
web/app/components/tools/provider/card.tsx
Normal file
83
web/app/components/tools/provider/card.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
import { useMemo } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Collection } from '../types'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { Tag01 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import { useStore as useLabelStore } from '@/app/components/tools/labels/store'
|
||||
|
||||
type Props = {
|
||||
active: boolean
|
||||
collection: Collection
|
||||
onSelect: () => void
|
||||
}
|
||||
|
||||
const ProviderCard = ({
|
||||
active,
|
||||
collection,
|
||||
onSelect,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const labelList = useLabelStore(s => s.labelList)
|
||||
|
||||
const labelContent = useMemo(() => {
|
||||
if (!collection.labels)
|
||||
return ''
|
||||
return collection.labels.map((name) => {
|
||||
const label = labelList.find(item => item.name === name)
|
||||
return label?.label[language]
|
||||
}).filter(Boolean).join(', ')
|
||||
}, [collection.labels, labelList, language])
|
||||
|
||||
return (
|
||||
<div className={cn('group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg', active && '!border-primary-400')} onClick={onSelect}>
|
||||
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
|
||||
<div className='relative shrink-0'>
|
||||
{typeof collection.icon === 'string' && (
|
||||
<div className='w-10 h-10 bg-center bg-cover bg-no-repeat rounded-md' style={{ backgroundImage: `url(${collection.icon})` }}/>
|
||||
)}
|
||||
{typeof collection.icon !== 'string' && (
|
||||
<AppIcon
|
||||
size='large'
|
||||
icon={collection.icon.content}
|
||||
background={collection.icon.background}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='grow w-0 py-[1px]'>
|
||||
<div className='flex items-center text-sm leading-5 font-semibold text-gray-800'>
|
||||
<div className='truncate' title={collection.label[language]}>{collection.label[language]}</div>
|
||||
</div>
|
||||
<div className='flex items-center text-[10px] leading-[18px] text-gray-500 font-medium'>
|
||||
<div className='truncate'>{t('tools.author')} {collection.author}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'grow mb-2 px-[14px] max-h-[72px] text-xs leading-normal text-gray-500',
|
||||
collection.labels?.length ? 'line-clamp-2' : 'line-clamp-4',
|
||||
collection.labels?.length > 0 && 'group-hover:line-clamp-2 group-hover:max-h-[36px]',
|
||||
)}
|
||||
title={collection.description[language]}
|
||||
>
|
||||
{collection.description[language]}
|
||||
</div>
|
||||
{collection.labels?.length > 0 && (
|
||||
<div className='flex items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]'>
|
||||
<div className='relative w-full flex items-center gap-1 py-[7px] rounded-md text-gray-500' title={labelContent}>
|
||||
<Tag01 className='shrink-0 w-3 h-3' />
|
||||
<div className='grow text-xs text-start leading-[18px] font-normal truncate'>{labelContent}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ProviderCard
|
||||
38
web/app/components/tools/provider/contribute.tsx
Normal file
38
web/app/components/tools/provider/contribute.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ToolsActive } from '@/app/components/base/icons/src/public/header-nav/tools'
|
||||
import { Heart02 } from '@/app/components/base/icons/src/vender/solid/education'
|
||||
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
|
||||
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
const Contribute: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<a
|
||||
href='https://github.com/langgenius/dify/blob/main/api/core/tools/README.md'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="group flex col-span-1 bg-white bg-cover bg-no-repeat bg-[url('~@/app/components/tools/provider/grid_bg.svg')] border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg"
|
||||
>
|
||||
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
|
||||
<div className='relative shrink-0 flex items-center'>
|
||||
<div className='z-10 flex p-3 rounded-[10px] bg-white border-[0.5px] border-primary-100 shadow-md'><ToolsActive className='w-4 h-4 text-primary-600'/></div>
|
||||
<div className='-translate-x-2 flex p-3 rounded-[10px] bg-[#FEF6FB] border-[0.5px] border-[#FCE7F6] shadow-md'><Heart02 className='w-4 h-4 text-[#EE46BC]'/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-3 px-[14px] text-[15px] leading-5 font-semibold'>
|
||||
<div className='text-gradient'>{t('tools.contribute.line1')}</div>
|
||||
<div className='text-gradient'>{t('tools.contribute.line2')}</div>
|
||||
</div>
|
||||
<div className='px-4 py-3 border-t-[0.5px] border-black/5 flex items-center space-x-1 text-[#155EEF]'>
|
||||
<BookOpen01 className='w-3 h-3' />
|
||||
<div className='grow leading-[18px] text-xs font-normal'>{t('tools.contribute.viewGuide')}</div>
|
||||
<ArrowUpRight className='w-3 h-3' />
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
export default React.memo(Contribute)
|
||||
70
web/app/components/tools/provider/custom-create-card.tsx
Normal file
70
web/app/components/tools/provider/custom-create-card.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type { CustomCollectionBackend } from '../types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
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'
|
||||
|
||||
type Props = {
|
||||
onRefreshData: () => void
|
||||
}
|
||||
|
||||
const Contribute = ({ onRefreshData }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
|
||||
const linkUrl = useMemo(() => {
|
||||
if (language.startsWith('zh_'))
|
||||
return 'https://docs.dify.ai/v/zh-hans/guides/gong-ju/quick-tool-integration'
|
||||
return 'https://docs.dify.ai/tutorials/quick-tool-integration'
|
||||
}, [language])
|
||||
|
||||
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
|
||||
const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => {
|
||||
await createCustomCollection(data)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
setIsShowEditCustomCollectionModal(false)
|
||||
onRefreshData()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[160px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-gray-50 hover:shadow-lg'>
|
||||
<div className='group grow rounded-t-xl hover:bg-white' onClick={() => setIsShowEditCustomCollectionModal(true)}>
|
||||
<div className='shrink-0 flex items-center p-4 pb-3'>
|
||||
<div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg group-hover:border-primary-100 group-hover:bg-primary-50'>
|
||||
<Plus className='w-4 h-4 text-gray-500 group-hover:text-primary-600'/>
|
||||
</div>
|
||||
<div className='ml-3 text-sm font-semibold leading-5 text-gray-800 group-hover:text-primary-600'>{t('tools.createCustomTool')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-4 py-3 rounded-b-xl border-t-[0.5px] border-black/5 text-gray-500 hover:text-[#155EEF] hover:bg-white'>
|
||||
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
|
||||
<BookOpen01 className='shrink-0 w-3 h-3' />
|
||||
<div className='grow leading-[18px] text-xs font-normal truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
|
||||
<ArrowUpRight className='shrink-0 w-3 h-3' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{isShowEditCollectionToolModal && (
|
||||
<EditCustomToolModal
|
||||
payload={null}
|
||||
onHide={() => setIsShowEditCustomCollectionModal(false)}
|
||||
onAdd={doCreateCustomToolCollection}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Contribute
|
||||
343
web/app/components/tools/provider/detail.tsx
Normal file
343
web/app/components/tools/provider/detail.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
'use client'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import cn from 'classnames'
|
||||
import { AuthHeaderPrefix, AuthType, CollectionType } from '../types'
|
||||
import type { Collection, CustomCollectionBackend, Tool, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '../types'
|
||||
import ToolItem from './tool-item'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { LinkExternal02, Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
|
||||
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
|
||||
import WorkflowToolModal from '@/app/components/tools/workflow-tool'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import {
|
||||
deleteWorkflowTool,
|
||||
fetchBuiltInToolList,
|
||||
fetchCustomCollection,
|
||||
fetchCustomToolList,
|
||||
fetchModelToolList,
|
||||
fetchWorkflowToolDetail,
|
||||
removeBuiltInToolCredential,
|
||||
removeCustomCollection,
|
||||
saveWorkflowToolProvider,
|
||||
updateBuiltInToolCredential,
|
||||
updateCustomCollection,
|
||||
} from '@/service/tools'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { ConfigurateMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
||||
type Props = {
|
||||
collection: Collection
|
||||
onRefreshData: () => void
|
||||
}
|
||||
|
||||
const ProviderDetail = ({
|
||||
collection,
|
||||
onRefreshData,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
|
||||
const needAuth = collection.allow_delete || collection.type === CollectionType.model
|
||||
const isAuthed = collection.is_team_authorization
|
||||
const isBuiltIn = collection.type === CollectionType.builtIn
|
||||
const isModel = collection.type === CollectionType.model
|
||||
|
||||
const [isDetailLoading, setIsDetailLoading] = useState(false)
|
||||
|
||||
// built in provider
|
||||
const [showSettingAuth, setShowSettingAuth] = useState(false)
|
||||
const { setShowModelModal } = useModalContext()
|
||||
const { modelProviders: providers } = useProviderContext()
|
||||
const showSettingAuthModal = () => {
|
||||
if (isModel) {
|
||||
const provider = providers.find(item => item.provider === collection?.id)
|
||||
if (provider) {
|
||||
setShowModelModal({
|
||||
payload: {
|
||||
currentProvider: provider,
|
||||
currentConfigurateMethod: ConfigurateMethodEnum.predefinedModel,
|
||||
currentCustomConfigrationModelFixedFields: undefined,
|
||||
},
|
||||
onSaveCallback: () => {
|
||||
onRefreshData()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
setShowSettingAuth(true)
|
||||
}
|
||||
}
|
||||
// custom provider
|
||||
const [customCollection, setCustomCollection] = useState<CustomCollectionBackend | WorkflowToolProviderResponse | null>(null)
|
||||
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
|
||||
const doUpdateCustomToolCollection = async (data: CustomCollectionBackend) => {
|
||||
await updateCustomCollection(data)
|
||||
onRefreshData()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
setIsShowEditCustomCollectionModal(false)
|
||||
}
|
||||
const doRemoveCustomToolCollection = async () => {
|
||||
await removeCustomCollection(collection?.name as string)
|
||||
onRefreshData()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
setIsShowEditCustomCollectionModal(false)
|
||||
}
|
||||
const getCustomProvider = useCallback(async () => {
|
||||
setIsDetailLoading(true)
|
||||
const res = await fetchCustomCollection(collection.name)
|
||||
if (res.credentials.auth_type === AuthType.apiKey && !res.credentials.api_key_header_prefix) {
|
||||
if (res.credentials.api_key_value)
|
||||
res.credentials.api_key_header_prefix = AuthHeaderPrefix.custom
|
||||
}
|
||||
setCustomCollection({
|
||||
...res,
|
||||
labels: collection.labels,
|
||||
provider: collection.name,
|
||||
})
|
||||
setIsDetailLoading(false)
|
||||
}, [collection.name])
|
||||
// workflow provider
|
||||
const [isShowEditWorkflowToolModal, setIsShowEditWorkflowToolModal] = useState(false)
|
||||
const getWorkflowToolProvider = useCallback(async () => {
|
||||
setIsDetailLoading(true)
|
||||
const res = await fetchWorkflowToolDetail(collection.id)
|
||||
const payload = {
|
||||
...res,
|
||||
parameters: res.tool?.parameters.map((item) => {
|
||||
return {
|
||||
name: item.name,
|
||||
description: item.llm_description,
|
||||
form: item.form,
|
||||
required: item.required,
|
||||
type: item.type,
|
||||
}
|
||||
}) || [],
|
||||
labels: res.tool?.labels || [],
|
||||
}
|
||||
setCustomCollection(payload)
|
||||
setIsDetailLoading(false)
|
||||
}, [collection.id])
|
||||
const removeWorkflowToolProvider = async () => {
|
||||
await deleteWorkflowTool(collection.id)
|
||||
onRefreshData()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
setIsShowEditWorkflowToolModal(false)
|
||||
}
|
||||
const updateWorkflowToolProvider = async (data: WorkflowToolProviderRequest & Partial<{
|
||||
workflow_app_id: string
|
||||
workflow_tool_id: string
|
||||
}>) => {
|
||||
await saveWorkflowToolProvider(data)
|
||||
onRefreshData()
|
||||
getWorkflowToolProvider()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
setIsShowEditWorkflowToolModal(false)
|
||||
}
|
||||
|
||||
// ToolList
|
||||
const [toolList, setToolList] = useState<Tool[]>([])
|
||||
const getProviderToolList = useCallback(async () => {
|
||||
setIsDetailLoading(true)
|
||||
try {
|
||||
if (collection.type === CollectionType.builtIn) {
|
||||
const list = await fetchBuiltInToolList(collection.name)
|
||||
setToolList(list)
|
||||
}
|
||||
else if (collection.type === CollectionType.model) {
|
||||
const list = await fetchModelToolList(collection.name)
|
||||
setToolList(list)
|
||||
}
|
||||
else if (collection.type === CollectionType.workflow) {
|
||||
setToolList([])
|
||||
}
|
||||
else {
|
||||
const list = await fetchCustomToolList(collection.name)
|
||||
setToolList(list)
|
||||
}
|
||||
}
|
||||
catch (e) { }
|
||||
setIsDetailLoading(false)
|
||||
}, [collection.name, collection.type])
|
||||
|
||||
useEffect(() => {
|
||||
if (collection.type === CollectionType.custom)
|
||||
getCustomProvider()
|
||||
if (collection.type === CollectionType.workflow)
|
||||
getWorkflowToolProvider()
|
||||
getProviderToolList()
|
||||
}, [collection.name, collection.type, getCustomProvider, getProviderToolList, getWorkflowToolProvider])
|
||||
|
||||
return (
|
||||
<div className='px-6 py-3'>
|
||||
<div className='flex items-center py-1 gap-2'>
|
||||
<div className='relative shrink-0'>
|
||||
{typeof collection.icon === 'string' && (
|
||||
<div className='w-8 h-8 bg-center bg-cover bg-no-repeat rounded-md' style={{ backgroundImage: `url(${collection.icon})` }}/>
|
||||
)}
|
||||
{typeof collection.icon !== 'string' && (
|
||||
<AppIcon
|
||||
size='small'
|
||||
icon={collection.icon.content}
|
||||
background={collection.icon.background}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='grow w-0 py-[1px]'>
|
||||
<div className='flex items-center text-md leading-6 font-semibold text-gray-900'>
|
||||
<div className='truncate' title={collection.label[language]}>{collection.label[language]}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-2 min-h-[36px] text-gray-500 text-sm leading-[18px]'>{collection.description[language]}</div>
|
||||
<div className='flex gap-1 border-b-[0.5px] border-black/5'>
|
||||
{(collection.type === CollectionType.builtIn) && needAuth && (
|
||||
<Button
|
||||
type={isAuthed ? 'default' : 'primary'}
|
||||
className={cn('shrink-0 my-3 w-full flex items-center', isAuthed && 'bg-white')}
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
>
|
||||
{isAuthed && <Indicator className='mr-2' color={'green'} />}
|
||||
<div className={cn('text-white leading-[18px] text-[13px] font-medium', isAuthed && '!text-gray-700')}>
|
||||
{isAuthed ? t('tools.auth.authorized') : t('tools.auth.unauthorized')}
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
{collection.type === CollectionType.custom && !isDetailLoading && (
|
||||
<Button
|
||||
className={cn('shrink-0 my-3 w-full flex items-center bg-white')}
|
||||
onClick={() => setIsShowEditCustomCollectionModal(true)}
|
||||
>
|
||||
<Settings01 className='mr-1 w-4 h-4 text-gray-500' />
|
||||
<div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
|
||||
</Button>
|
||||
)}
|
||||
{collection.type === CollectionType.workflow && !isDetailLoading && customCollection && (
|
||||
<>
|
||||
<Button
|
||||
type='primary'
|
||||
className={cn('shrink-0 my-3 w-[183px] flex items-center')}
|
||||
>
|
||||
<a className='flex items-center text-white' href={`/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel='noreferrer' target='_blank'>
|
||||
<div className='leading-5 text-sm font-medium'>{t('tools.openInStudio')}</div>
|
||||
<LinkExternal02 className='ml-1 w-4 h-4' />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('shrink-0 my-3 w-[183px] flex items-center bg-white')}
|
||||
onClick={() => setIsShowEditWorkflowToolModal(true)}
|
||||
>
|
||||
<div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Tools */}
|
||||
<div className='pt-3'>
|
||||
{isDetailLoading && <div className='flex h-[200px]'><Loading type='app'/></div>}
|
||||
{!isDetailLoading && (
|
||||
<div className='text-xs font-medium leading-6 text-gray-500'>
|
||||
{collection.type === CollectionType.workflow && <span className=''>{t('tools.createTool.toolInput.title').toLocaleUpperCase()}</span>}
|
||||
{collection.type !== CollectionType.workflow && <span className=''>{t('tools.includeToolNum', { num: toolList.length }).toLocaleUpperCase()}</span>}
|
||||
{needAuth && (isBuiltIn || isModel) && !isAuthed && (
|
||||
<>
|
||||
<span className='px-1'>·</span>
|
||||
<span className='text-[#DC6803]'>{t('tools.auth.setup').toLocaleUpperCase()}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isDetailLoading && (
|
||||
<div className='mt-1'>
|
||||
{collection.type !== CollectionType.workflow && toolList.map(tool => (
|
||||
<ToolItem
|
||||
key={tool.name}
|
||||
disabled={needAuth && (isBuiltIn || isModel) && !isAuthed}
|
||||
collection={collection}
|
||||
tool={tool}
|
||||
isBuiltIn={isBuiltIn}
|
||||
isModel={isModel}
|
||||
/>
|
||||
))}
|
||||
{collection.type === CollectionType.workflow && (customCollection as WorkflowToolProviderResponse)?.tool?.parameters.map(item => (
|
||||
<div key={item.name} className='mb-2 px-4 py-3 rounded-xl bg-gray-25 border-[0.5px] border-gray-200'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='font-medium text-sm text-gray-900'>{item.name}</span>
|
||||
<span className='text-xs leading-[18px] text-gray-500'>{item.type}</span>
|
||||
<span className='font-medium text-xs leading-[18px] text-[#ec4a0a]'>{item.required ? t('tools.createTool.toolInput.required') : ''}</span>
|
||||
</div>
|
||||
<div className='h-[18px] leading-[18px] text-gray-500 text-xs'>{item.llm_description}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showSettingAuth && (
|
||||
<ConfigCredential
|
||||
collection={collection}
|
||||
onCancel={() => setShowSettingAuth(false)}
|
||||
onSaved={async (value) => {
|
||||
await updateBuiltInToolCredential(collection.name, value)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
await onRefreshData()
|
||||
setShowSettingAuth(false)
|
||||
}}
|
||||
onRemove={async () => {
|
||||
await removeBuiltInToolCredential(collection.name)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
await onRefreshData()
|
||||
setShowSettingAuth(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isShowEditCollectionToolModal && (
|
||||
<EditCustomToolModal
|
||||
payload={customCollection}
|
||||
onHide={() => setIsShowEditCustomCollectionModal(false)}
|
||||
onEdit={doUpdateCustomToolCollection}
|
||||
onRemove={doRemoveCustomToolCollection}
|
||||
/>
|
||||
)}
|
||||
{isShowEditWorkflowToolModal && (
|
||||
<WorkflowToolModal
|
||||
payload={customCollection}
|
||||
onHide={() => setIsShowEditWorkflowToolModal(false)}
|
||||
onRemove={removeWorkflowToolProvider}
|
||||
onSave={updateWorkflowToolProvider}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ProviderDetail
|
||||
14
web/app/components/tools/provider/grid_bg.svg
Normal file
14
web/app/components/tools/provider/grid_bg.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
53
web/app/components/tools/provider/tool-item.tsx
Normal file
53
web/app/components/tools/provider/tool-item.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type { Collection, Tool } from '../types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import SettingBuiltInTool from '@/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool'
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean
|
||||
collection: Collection
|
||||
tool: Tool
|
||||
isBuiltIn: boolean
|
||||
isModel: boolean
|
||||
}
|
||||
|
||||
const ToolItem = ({
|
||||
disabled,
|
||||
collection,
|
||||
tool,
|
||||
isBuiltIn,
|
||||
isModel,
|
||||
}: Props) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const [showDetail, setShowDetail] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn('mb-2 px-4 py-3 rounded-xl bg-gray-25 border-[0.5px] border-gary-200 shadow-xs cursor-pointer', disabled && 'opacity-50 !cursor-not-allowed')}
|
||||
onClick={() => !disabled && setShowDetail(true)}
|
||||
>
|
||||
<div className='text-gray-800 font-semibold text-sm leading-5'>{tool.label[language]}</div>
|
||||
<div className='mt-0.5 text-xs leading-[18px] text-gray-500 line-clamp-2' title={tool.description[language]}>{tool.description[language]}</div>
|
||||
</div>
|
||||
{showDetail && (
|
||||
<SettingBuiltInTool
|
||||
collection={collection}
|
||||
toolName={tool.name}
|
||||
readonly
|
||||
onHide={() => {
|
||||
setShowDetail(false)
|
||||
}}
|
||||
isBuiltIn={isBuiltIn}
|
||||
isModel={isModel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default ToolItem
|
||||
@@ -1,41 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
value: string
|
||||
onChange: (v: string) => void
|
||||
}
|
||||
|
||||
const Search: FC<Props> = ({
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'flex relative')}>
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="query"
|
||||
className="block w-0 grow bg-gray-200 shadow-sm rounded-md border-0 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-8"
|
||||
placeholder={t('common.operation.search')!}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Search)
|
||||
@@ -48,8 +48,8 @@ const ConfigCredential: FC<Props> = ({
|
||||
onHide={onCancel}
|
||||
title={t('tools.auth.setupModalTitle') as string}
|
||||
titleDescription={t('tools.auth.setupModalTitleDescription') as string}
|
||||
panelClassName='mt-2 !w-[480px]'
|
||||
maxWidthClassName='!max-w-[480px]'
|
||||
panelClassName='mt-2 !w-[405px]'
|
||||
maxWidthClassName='!max-w-[405px]'
|
||||
height='calc(100vh - 16px)'
|
||||
contentClassName='!bg-gray-100'
|
||||
headerClassName='!border-b-black/5'
|
||||
@@ -88,7 +88,7 @@ const ConfigCredential: FC<Props> = ({
|
||||
)
|
||||
}
|
||||
< div className='flex space-x-2'>
|
||||
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700 bg-white' onClick={onCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium' type='primary' onClick={() => onSaved(tempCredential)}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Collection } from '../types'
|
||||
import { CollectionType, LOC } from '../types'
|
||||
import { Settings01 } from '../../base/icons/src/vender/line/general'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
type Props = {
|
||||
icon: JSX.Element
|
||||
collection: Collection
|
||||
loc: LOC
|
||||
onShowAuth: () => void
|
||||
onShowEditCustomCollection: () => void
|
||||
}
|
||||
|
||||
const Header: FC<Props> = ({
|
||||
icon,
|
||||
collection,
|
||||
loc,
|
||||
onShowAuth,
|
||||
onShowEditCustomCollection,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
const { t } = useTranslation()
|
||||
const isInToolsPage = loc === LOC.tools
|
||||
const isInDebugPage = !isInToolsPage
|
||||
|
||||
const needAuth = collection?.allow_delete || collection?.type === CollectionType.model
|
||||
const isAuthed = collection.is_team_authorization
|
||||
return (
|
||||
<div className={cn(isInToolsPage ? 'py-4 px-6' : 'py-[11px] pl-4 pr-3', 'flex justify-between items-start border-b border-gray-200')}>
|
||||
<div className='flex items-start w-full'>
|
||||
{icon}
|
||||
<div className='ml-3 grow w-0'>
|
||||
<div className='flex items-center h-6 space-x-1'>
|
||||
<div className={cn(isInDebugPage && 'truncate', 'text-base font-semibold text-gray-900')}>{collection.label[language]}</div>
|
||||
<div className='text-xs font-normal text-gray-500'>·</div>
|
||||
<div className='text-xs font-normal text-gray-500'>{t('tools.author')} {collection.author}</div>
|
||||
</div>
|
||||
{collection.description && (
|
||||
<div className={cn('leading-[18px] text-[13px] font-normal text-gray-500')}>
|
||||
{collection.description[language]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && needAuth && (
|
||||
<div
|
||||
className={cn('cursor-pointer', 'ml-1 shrink-0 flex items-center h-8 border border-gray-200 rounded-lg px-3 space-x-2 shadow-xs')}
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
onShowAuth()
|
||||
}}
|
||||
>
|
||||
<div className={cn(isAuthed ? 'border-[#12B76A] bg-[#32D583]' : 'border-gray-400 bg-gray-300', 'rounded h-2 w-2 border')}></div>
|
||||
<div className='leading-5 text-sm font-medium text-gray-700'>{t(`tools.auth.${isAuthed ? 'authorized' : 'unauthorized'}`)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{collection.type === CollectionType.custom && (
|
||||
<div
|
||||
className={cn('cursor-pointer', 'ml-1 shrink-0 flex items-center h-8 border border-gray-200 rounded-lg px-3 space-x-2 shadow-xs')}
|
||||
onClick={() => onShowEditCustomCollection()}
|
||||
>
|
||||
<Settings01 className='w-4 h-4 text-gray-700' />
|
||||
<div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(Header)
|
||||
@@ -1,220 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { AuthHeaderPrefix, AuthType, CollectionType, LOC } from '../types'
|
||||
import type { Collection, CustomCollectionBackend, Tool } from '../types'
|
||||
import Loading from '../../base/loading'
|
||||
import { ArrowNarrowRight } from '../../base/icons/src/vender/line/arrows'
|
||||
import Toast from '../../base/toast'
|
||||
import { ConfigurateMethodEnum } from '../../header/account-setting/model-provider-page/declarations'
|
||||
import Header from './header'
|
||||
import Item from './item'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
|
||||
import { fetchCustomCollection, removeBuiltInToolCredential, removeCustomCollection, updateBuiltInToolCredential, updateCustomCollection } from '@/service/tools'
|
||||
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
|
||||
import type { AgentTool } from '@/types/app'
|
||||
import { MAX_TOOLS_NUM } from '@/config'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
type Props = {
|
||||
collection: Collection | null
|
||||
list: Tool[]
|
||||
// onToolListChange: () => void // custom tools change
|
||||
loc: LOC
|
||||
addedTools?: AgentTool[]
|
||||
onAddTool?: (collection: Collection, payload: Tool) => void
|
||||
onRefreshData: () => void
|
||||
onCollectionRemoved: () => void
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const ToolList: FC<Props> = ({
|
||||
collection,
|
||||
list,
|
||||
loc,
|
||||
addedTools,
|
||||
onAddTool,
|
||||
onRefreshData,
|
||||
onCollectionRemoved,
|
||||
isLoading,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isInToolsPage = loc === LOC.tools
|
||||
const isBuiltIn = collection?.type === CollectionType.builtIn
|
||||
const isModel = collection?.type === CollectionType.model
|
||||
const needAuth = collection?.allow_delete
|
||||
|
||||
const { setShowModelModal } = useModalContext()
|
||||
const [showSettingAuth, setShowSettingAuth] = useState(false)
|
||||
const { modelProviders: providers } = useProviderContext()
|
||||
const showSettingAuthModal = () => {
|
||||
if (isModel) {
|
||||
const provider = providers.find(item => item.provider === collection?.id)
|
||||
if (provider) {
|
||||
setShowModelModal({
|
||||
payload: {
|
||||
currentProvider: provider,
|
||||
currentConfigurateMethod: ConfigurateMethodEnum.predefinedModel,
|
||||
currentCustomConfigrationModelFixedFields: undefined,
|
||||
},
|
||||
onSaveCallback: () => {
|
||||
onRefreshData()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
setShowSettingAuth(true)
|
||||
}
|
||||
}
|
||||
|
||||
const [customCollection, setCustomCollection] = useState<CustomCollectionBackend | null>(null)
|
||||
useEffect(() => {
|
||||
if (!collection)
|
||||
return
|
||||
(async () => {
|
||||
if (collection.type === CollectionType.custom) {
|
||||
const res = await fetchCustomCollection(collection.name)
|
||||
if (res.credentials.auth_type === AuthType.apiKey && !res.credentials.api_key_header_prefix) {
|
||||
if (res.credentials.api_key_value)
|
||||
res.credentials.api_key_header_prefix = AuthHeaderPrefix.custom
|
||||
}
|
||||
setCustomCollection({
|
||||
...res,
|
||||
provider: collection.name,
|
||||
})
|
||||
}
|
||||
})()
|
||||
}, [collection])
|
||||
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
|
||||
|
||||
const doUpdateCustomToolCollection = async (data: CustomCollectionBackend) => {
|
||||
await updateCustomCollection(data)
|
||||
onRefreshData()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
setIsShowEditCustomCollectionModal(false)
|
||||
}
|
||||
|
||||
const doRemoveCustomToolCollection = async () => {
|
||||
await removeCustomCollection(collection?.name as string)
|
||||
onCollectionRemoved()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
setIsShowEditCustomCollectionModal(false)
|
||||
}
|
||||
|
||||
if (!collection || isLoading)
|
||||
return <Loading type='app' />
|
||||
|
||||
const icon = <>{typeof collection.icon === 'string'
|
||||
? (
|
||||
<div
|
||||
className='p-2 bg-cover bg-center border border-gray-100 rounded-lg'
|
||||
>
|
||||
<div className='w-6 h-6 bg-center bg-contain rounded-md'
|
||||
style={{
|
||||
backgroundImage: `url(${collection.icon})`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<AppIcon
|
||||
size='large'
|
||||
icon={collection.icon.content}
|
||||
background={collection.icon.background}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
return (
|
||||
<div className='flex flex-col h-full pb-4'>
|
||||
<Header
|
||||
icon={icon}
|
||||
collection={collection}
|
||||
loc={loc}
|
||||
onShowAuth={() => showSettingAuthModal()}
|
||||
onShowEditCustomCollection={() => setIsShowEditCustomCollectionModal(true)}
|
||||
/>
|
||||
<div className={cn(isInToolsPage ? 'px-6 pt-4' : 'px-4 pt-3')}>
|
||||
<div className='flex items-center h-[4.5] space-x-2 text-xs font-medium text-gray-500'>
|
||||
<div className=''>{t('tools.includeToolNum', {
|
||||
num: list.length,
|
||||
})}</div>
|
||||
{needAuth && (isBuiltIn || isModel) && !collection.is_team_authorization && (
|
||||
<>
|
||||
<div>·</div>
|
||||
<div
|
||||
className='flex items-center text-[#155EEF] cursor-pointer'
|
||||
onClick={() => showSettingAuthModal()}
|
||||
>
|
||||
<div>{t('tools.auth.setup')}</div>
|
||||
<ArrowNarrowRight className='ml-0.5 w-3 h-3' />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(isInToolsPage ? 'px-6' : 'px-4', 'grow h-0 pt-2 overflow-y-auto')}>
|
||||
{/* list */}
|
||||
<div className={cn(isInToolsPage ? 'grid-cols-3 gap-4' : 'grid-cols-1 gap-2', 'grid')}>
|
||||
{list.map(item => (
|
||||
<Item
|
||||
key={item.name}
|
||||
icon={icon}
|
||||
payload={item}
|
||||
collection={collection}
|
||||
isInToolsPage={isInToolsPage}
|
||||
isToolNumMax={(addedTools?.length || 0) >= MAX_TOOLS_NUM}
|
||||
added={!!addedTools?.find(v => v.provider_id === collection.id && v.provider_type === collection.type && v.tool_name === item.name)}
|
||||
onAdd={!isInToolsPage ? tool => onAddTool?.(collection as Collection, tool) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{showSettingAuth && (
|
||||
<ConfigCredential
|
||||
collection={collection}
|
||||
onCancel={() => setShowSettingAuth(false)}
|
||||
onSaved={async (value) => {
|
||||
await updateBuiltInToolCredential(collection.name, value)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
await onRefreshData()
|
||||
setShowSettingAuth(false)
|
||||
}}
|
||||
onRemove={async () => {
|
||||
await removeBuiltInToolCredential(collection.name)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
await onRefreshData()
|
||||
setShowSettingAuth(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isShowEditCollectionToolModal && (
|
||||
<EditCustomToolModal
|
||||
payload={customCollection}
|
||||
onHide={() => setIsShowEditCustomCollectionModal(false)}
|
||||
onEdit={doUpdateCustomToolCollection}
|
||||
onRemove={doRemoveCustomToolCollection}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ToolList)
|
||||
@@ -1,84 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Collection, Tool } from '../types'
|
||||
import Button from '../../base/button'
|
||||
import { CollectionType } from '../types'
|
||||
import TooltipPlus from '../../base/tooltip-plus'
|
||||
import I18n from '@/context/i18n'
|
||||
import SettingBuiltInTool from '@/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
type Props = {
|
||||
collection: Collection
|
||||
icon: JSX.Element
|
||||
payload: Tool
|
||||
isInToolsPage: boolean
|
||||
isToolNumMax: boolean
|
||||
added?: boolean
|
||||
onAdd?: (payload: Tool) => void
|
||||
}
|
||||
|
||||
const Item: FC<Props> = ({
|
||||
collection,
|
||||
icon,
|
||||
payload,
|
||||
isInToolsPage,
|
||||
isToolNumMax,
|
||||
added,
|
||||
onAdd,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
|
||||
const isBuiltIn = collection.type === CollectionType.builtIn
|
||||
const isModel = collection.type === CollectionType.model
|
||||
const canShowDetail = isInToolsPage
|
||||
const [showDetail, setShowDetail] = useState(false)
|
||||
const addBtn = <Button className='shrink-0 flex items-center h-7 !px-3 !text-xs !font-medium !text-gray-700' disabled={added || !collection.is_team_authorization} onClick={() => onAdd?.(payload)}>{t(`common.operation.${added ? 'added' : 'add'}`)}</Button>
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(canShowDetail && 'cursor-pointer', 'flex justify-between items-start p-4 rounded-xl border border-gray-200 bg-gray-50 shadow-xs')}
|
||||
onClick={() => canShowDetail && setShowDetail(true)}
|
||||
>
|
||||
<div className='flex items-start w-full'>
|
||||
{icon}
|
||||
<div className='ml-3 w-0 grow'>
|
||||
<div className={cn('text-base font-semibold text-gray-900 truncate')}>{payload.label[language]}</div>
|
||||
<div className={cn('leading-[18px] text-[13px] font-normal text-gray-500')}>
|
||||
{payload.description[language]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='shrink-0'>
|
||||
{!isToolNumMax && onAdd && (
|
||||
!collection.is_team_authorization
|
||||
? <TooltipPlus popupContent={t('tools.auth.unauthorized')}>
|
||||
{addBtn}
|
||||
</TooltipPlus>
|
||||
: addBtn
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showDetail && (
|
||||
<SettingBuiltInTool
|
||||
collection={collection}
|
||||
toolName={payload.name}
|
||||
readonly
|
||||
onHide={() => {
|
||||
setShowDetail(false)
|
||||
}}
|
||||
isBuiltIn={isBuiltIn}
|
||||
isModel={isModel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(Item)
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import Item from './item'
|
||||
import type { Collection } from '@/app/components/tools/types'
|
||||
type Props = {
|
||||
className?: string
|
||||
currentIndex: number
|
||||
list: Collection[]
|
||||
onChosen: (index: number) => void
|
||||
}
|
||||
|
||||
const ToolNavList: FC<Props> = ({
|
||||
className,
|
||||
currentIndex,
|
||||
list,
|
||||
onChosen,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
{list.map((item, index) => (
|
||||
<Item isCurrent={index === currentIndex} key={index} payload={item} onClick={() => onChosen(index)}></Item>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ToolNavList)
|
||||
@@ -1,50 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import cn from 'classnames'
|
||||
import AppIcon from '../../base/app-icon'
|
||||
import type { Collection } from '@/app/components/tools/types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
|
||||
type Props = {
|
||||
isCurrent: boolean
|
||||
payload: Collection
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const Item: FC<Props> = ({
|
||||
isCurrent,
|
||||
payload,
|
||||
onClick,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
return (
|
||||
<div
|
||||
className={cn(isCurrent && 'bg-white shadow-xs rounded-lg', 'mt-1 flex h-9 items-center px-2 space-x-2 cursor-pointer')}
|
||||
onClick={() => !isCurrent && onClick()}
|
||||
>
|
||||
{typeof payload.icon === 'string'
|
||||
? (
|
||||
<div
|
||||
className='w-6 h-6 bg-cover bg-center rounded-md'
|
||||
style={{
|
||||
backgroundImage: `url(${payload.icon})`,
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
: (
|
||||
<AppIcon
|
||||
size='tiny'
|
||||
icon={payload.icon.content}
|
||||
background={payload.icon.background}
|
||||
/>
|
||||
)}
|
||||
<div className={cn(isCurrent && 'text-primary-600 font-semibold', 'leading-5 text-sm font-normal truncate')}>{payload.label[language]}</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Item)
|
||||
@@ -27,6 +27,7 @@ export enum CollectionType {
|
||||
builtIn = 'builtin',
|
||||
custom = 'api',
|
||||
model = 'model',
|
||||
workflow = 'workflow',
|
||||
}
|
||||
|
||||
export type Emoji = {
|
||||
@@ -45,6 +46,7 @@ export type Collection = {
|
||||
team_credentials: Record<string, any>
|
||||
is_team_authorization: boolean
|
||||
allow_delete: boolean
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
export type ToolParameter = {
|
||||
@@ -52,19 +54,25 @@ export type ToolParameter = {
|
||||
label: TypeWithI18N
|
||||
human_description: TypeWithI18N
|
||||
type: string
|
||||
form: string
|
||||
llm_description: string
|
||||
required: boolean
|
||||
default: string
|
||||
options?: {
|
||||
label: TypeWithI18N
|
||||
value: string
|
||||
}[]
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
|
||||
export type Tool = {
|
||||
name: string
|
||||
author: string
|
||||
label: TypeWithI18N
|
||||
description: any
|
||||
parameters: ToolParameter[]
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
export type ToolCredential = {
|
||||
@@ -91,13 +99,17 @@ export type CustomCollectionBackend = {
|
||||
privacy_policy: string
|
||||
custom_disclaimer: string
|
||||
tools?: ParamItem[]
|
||||
id: string
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
export type ParamItem = {
|
||||
name: string
|
||||
label: TypeWithI18N
|
||||
human_description: TypeWithI18N
|
||||
llm_description: string
|
||||
type: string
|
||||
form: string
|
||||
required: boolean
|
||||
default: string
|
||||
min?: number
|
||||
@@ -115,3 +127,39 @@ export type CustomParamSchema = {
|
||||
method: string
|
||||
parameters: ParamItem[]
|
||||
}
|
||||
|
||||
export type WorkflowToolProviderParameter = {
|
||||
name: string
|
||||
form: string
|
||||
description: string
|
||||
required?: boolean
|
||||
type?: string
|
||||
}
|
||||
|
||||
export type WorkflowToolProviderRequest = {
|
||||
name: string
|
||||
icon: Emoji
|
||||
description: string
|
||||
parameters: WorkflowToolProviderParameter[]
|
||||
labels: string[]
|
||||
privacy_policy: string
|
||||
}
|
||||
|
||||
export type WorkflowToolProviderResponse = {
|
||||
workflow_app_id: string
|
||||
workflow_tool_id: string
|
||||
label: string
|
||||
name: string
|
||||
icon: Emoji
|
||||
description: string
|
||||
synced: boolean
|
||||
tool: {
|
||||
author: string
|
||||
name: string
|
||||
label: TypeWithI18N
|
||||
description: TypeWithI18N
|
||||
labels: string[]
|
||||
parameters: ParamItem[]
|
||||
}
|
||||
privacy_policy: string
|
||||
}
|
||||
|
||||
225
web/app/components/tools/workflow-tool/configure-button.tsx
Normal file
225
web/app/components/tools/workflow-tool/configure-button.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
'use client'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Tools } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import WorkflowToolModal from '@/app/components/tools/workflow-tool'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { createWorkflowToolProvider, fetchWorkflowToolDetailByAppID, saveWorkflowToolProvider } from '@/service/tools'
|
||||
import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
disabled: boolean
|
||||
published: boolean
|
||||
detailNeedUpdate: boolean
|
||||
workflowAppId: string
|
||||
icon: Emoji
|
||||
name: string
|
||||
description: string
|
||||
inputs?: InputVar[]
|
||||
handlePublish: () => void
|
||||
onRefreshData?: () => void
|
||||
}
|
||||
|
||||
const WorkflowToolConfigureButton = ({
|
||||
disabled,
|
||||
published,
|
||||
detailNeedUpdate,
|
||||
workflowAppId,
|
||||
icon,
|
||||
name,
|
||||
description,
|
||||
inputs,
|
||||
handlePublish,
|
||||
onRefreshData,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [detail, setDetail] = useState<WorkflowToolProviderResponse>()
|
||||
|
||||
const outdated = useMemo(() => {
|
||||
if (!detail)
|
||||
return false
|
||||
if (detail.tool.parameters.length !== inputs?.length) {
|
||||
return true
|
||||
}
|
||||
else {
|
||||
for (const item of inputs || []) {
|
||||
const param = detail.tool.parameters.find(toolParam => toolParam.name === item.variable)
|
||||
if (!param) {
|
||||
return true
|
||||
}
|
||||
else if (param.required !== item.required) {
|
||||
return true
|
||||
}
|
||||
else {
|
||||
if (item.type === 'paragraph' && param.type !== 'string')
|
||||
return true
|
||||
if (param.type !== item.type && !(param.type === 'string' && item.type === 'paragraph'))
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, [detail, inputs])
|
||||
|
||||
const payload = useMemo(() => {
|
||||
let parameters: WorkflowToolProviderParameter[] = []
|
||||
if (!published) {
|
||||
parameters = (inputs || []).map((item) => {
|
||||
return {
|
||||
name: item.variable,
|
||||
description: '',
|
||||
form: 'llm',
|
||||
required: item.required,
|
||||
type: item.type,
|
||||
}
|
||||
})
|
||||
}
|
||||
else if (detail && detail.tool) {
|
||||
parameters = (inputs || []).map((item) => {
|
||||
return {
|
||||
name: item.variable,
|
||||
required: item.required,
|
||||
type: item.type === 'paragraph' ? 'string' : item.type,
|
||||
description: detail.tool.parameters.find(param => param.name === item.variable)?.llm_description || '',
|
||||
form: detail.tool.parameters.find(param => param.name === item.variable)?.form || 'llm',
|
||||
}
|
||||
})
|
||||
}
|
||||
return {
|
||||
icon: detail?.icon || icon,
|
||||
label: detail?.label || name,
|
||||
name: detail?.name || '',
|
||||
description: detail?.description || description,
|
||||
parameters,
|
||||
labels: detail?.tool?.labels || [],
|
||||
privacy_policy: detail?.privacy_policy || '',
|
||||
...(published
|
||||
? {
|
||||
workflow_tool_id: detail?.workflow_tool_id,
|
||||
}
|
||||
: {
|
||||
workflow_app_id: workflowAppId,
|
||||
}),
|
||||
}
|
||||
}, [detail, published, workflowAppId, icon, name, description, inputs])
|
||||
|
||||
const getDetail = useCallback(async (workflowAppId: string) => {
|
||||
setIsLoading(true)
|
||||
const res = await fetchWorkflowToolDetailByAppID(workflowAppId)
|
||||
setDetail(res)
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (published)
|
||||
getDetail(workflowAppId)
|
||||
}, [getDetail, published, workflowAppId])
|
||||
|
||||
useEffect(() => {
|
||||
if (detailNeedUpdate)
|
||||
getDetail(workflowAppId)
|
||||
}, [detailNeedUpdate, getDetail, workflowAppId])
|
||||
|
||||
const createHandle = async (data: WorkflowToolProviderRequest & { workflow_app_id: string }) => {
|
||||
try {
|
||||
await createWorkflowToolProvider(data)
|
||||
onRefreshData?.()
|
||||
getDetail(workflowAppId)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
setShowModal(false)
|
||||
}
|
||||
catch (e) {
|
||||
Toast.notify({ type: 'error', message: (e as Error).message })
|
||||
}
|
||||
}
|
||||
|
||||
const updateWorkflowToolProvider = async (data: WorkflowToolProviderRequest & Partial<{
|
||||
workflow_app_id: string
|
||||
workflow_tool_id: string
|
||||
}>) => {
|
||||
try {
|
||||
await handlePublish()
|
||||
await saveWorkflowToolProvider(data)
|
||||
onRefreshData?.()
|
||||
getDetail(workflowAppId)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
setShowModal(false)
|
||||
}
|
||||
catch (e) {
|
||||
Toast.notify({ type: 'error', message: (e as Error).message })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mt-2 pt-2 border-t-[0.5px] border-t-black/5'>
|
||||
{(!published || !isLoading) && (
|
||||
<div className={cn(
|
||||
'group bg-gray-100 rounded-lg transition-colors',
|
||||
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'cursor-pointer',
|
||||
!published && 'hover:bg-primary-50',
|
||||
)}>
|
||||
<div
|
||||
className='flex justify-start items-center gap-2 px-2.5 py-2'
|
||||
onClick={() => !published && setShowModal(true)}
|
||||
>
|
||||
<Tools className={cn('relative w-4 h-4', !published && 'group-hover:text-primary-600')}/>
|
||||
<div title={t('workflow.common.workflowAsTool') || ''} className={cn('grow shrink basis-0 text-[13px] font-medium leading-[18px] truncate', !published && 'group-hover:text-primary-600')}>{t('workflow.common.workflowAsTool')}</div>
|
||||
{!published && (
|
||||
<span className='shrink-0 px-1 border border-black/8 rounded-[5px] bg-white text-[10px] font-medium leading-[18px] text-gray-500'>{t('workflow.common.configureRequired').toLocaleUpperCase()}</span>
|
||||
)}
|
||||
</div>
|
||||
{published && (
|
||||
<div className='px-2.5 py-2 border-t-[0.5px] border-black/5'>
|
||||
<div className='flex justify-between'>
|
||||
<Button
|
||||
className='px-2 w-[140px] py-0 h-6 shadow-xs rounded-md text-xs font-medium text-gray-700 border-[0.5px] bg-white border-gray-200'
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
{t('workflow.common.configure')}
|
||||
{outdated && <Indicator className='ml-1' color={'yellow'} />}
|
||||
</Button>
|
||||
<Button
|
||||
className='px-2 w-[140px] py-0 h-6 shadow-xs rounded-md text-xs font-medium text-gray-700 border-[0.5px] bg-white border-gray-200'
|
||||
onClick={() => router.push('/tools?category=workflow')}
|
||||
>
|
||||
{t('workflow.common.manageInTools')}
|
||||
<ArrowUpRight className='ml-1' />
|
||||
</Button>
|
||||
</div>
|
||||
{outdated && <div className='mt-1 text-xs leading-[18px] text-[#dc6803]'>{t('workflow.common.workflowAsToolTip')}</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{published && isLoading && <div className='pt-2'><Loading type='app'/></div>}
|
||||
</div>
|
||||
{showModal && (
|
||||
<WorkflowToolModal
|
||||
isAdd={!published}
|
||||
payload={payload}
|
||||
onHide={() => setShowModal(false)}
|
||||
onCreate={createHandle}
|
||||
onSave={updateWorkflowToolProvider}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default WorkflowToolConfigureButton
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import s from './style.module.css'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
|
||||
type ConfirmModalProps = {
|
||||
show: boolean
|
||||
onConfirm?: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const ConfirmModal = ({ show, onConfirm, onClose }: ConfirmModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
wrapperClassName='!z-[1020]'
|
||||
className={cn('p-8 max-w-[600px] w-[600px]', s.bg)}
|
||||
isShow={show}
|
||||
onClose={() => {}}
|
||||
>
|
||||
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
<div className='w-12 h-12 p-3 bg-white rounded-xl border-[0.5px] border-gray-100 shadow-xl'>
|
||||
<AlertTriangle className='w-6 h-6 text-[rgb(247,144,9)]' />
|
||||
</div>
|
||||
<div className='relative mt-3 text-xl font-semibold leading-[30px] text-gray-900'>{t('tools.createTool.confirmTitle')}</div>
|
||||
<div className='my-1 text-gray-500 text-sm leading-5'>
|
||||
{t('tools.createTool.confirmTip')}
|
||||
</div>
|
||||
<div className='pt-6 flex justify-end items-center'>
|
||||
<div className='flex items-center'>
|
||||
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='text-sm font-medium border-red-700 border-[0.5px]' type="warning" onClick={onConfirm}>{t('common.operation.confirm')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfirmModal
|
||||
@@ -0,0 +1,3 @@
|
||||
.bg {
|
||||
background: linear-gradient(180deg, rgba(247, 144, 9, 0.05) 0%, rgba(247, 144, 9, 0.00) 24.41%), #F9FAFB;
|
||||
}
|
||||
282
web/app/components/tools/workflow-tool/index.tsx
Normal file
282
web/app/components/tools/workflow-tool/index.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import produce from 'immer'
|
||||
import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest } from '../types'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import MethodSelector from '@/app/components/tools/workflow-tool/method-selector'
|
||||
import LabelSelector from '@/app/components/tools/labels/selector'
|
||||
import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type Props = {
|
||||
isAdd?: boolean
|
||||
payload: any
|
||||
onHide: () => void
|
||||
onRemove?: () => void
|
||||
onCreate?: (payload: WorkflowToolProviderRequest & { workflow_app_id: string }) => void
|
||||
onSave?: (payload: WorkflowToolProviderRequest & Partial<{
|
||||
workflow_app_id: string
|
||||
workflow_tool_id: string
|
||||
}>) => void
|
||||
}
|
||||
// Add and Edit
|
||||
const WorkflowToolAsModal: FC<Props> = ({
|
||||
isAdd,
|
||||
payload,
|
||||
onHide,
|
||||
onRemove,
|
||||
onSave,
|
||||
onCreate,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState<Boolean>(false)
|
||||
const [emoji, setEmoji] = useState<Emoji>(payload.icon)
|
||||
const [label, setLabel] = useState<string>(payload.label)
|
||||
const [name, setName] = useState(payload.name)
|
||||
const [description, setDescription] = useState(payload.description)
|
||||
const [parameters, setParameters] = useState<WorkflowToolProviderParameter[]>(payload.parameters)
|
||||
const handleParameterChange = (key: string, value: string, index: number) => {
|
||||
const newData = produce(parameters, (draft: WorkflowToolProviderParameter[]) => {
|
||||
if (key === 'description')
|
||||
draft[index].description = value
|
||||
else
|
||||
draft[index].form = value
|
||||
})
|
||||
setParameters(newData)
|
||||
}
|
||||
const [labels, setLabels] = useState<string[]>(payload.labels)
|
||||
const handleLabelSelect = (value: string[]) => {
|
||||
setLabels(value)
|
||||
}
|
||||
const [privacyPolicy, setPrivacyPolicy] = useState(payload.privacy_policy)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
const isNameValid = (name: string) => {
|
||||
return /^[a-zA-Z0-9_]+$/.test(name)
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
if (!label) {
|
||||
return Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Please enter the tool name',
|
||||
})
|
||||
}
|
||||
if (!name) {
|
||||
return Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Please enter the name for tool call',
|
||||
})
|
||||
}
|
||||
else if (!isNameValid(name)) {
|
||||
return Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Name for tool call can only contain numbers, letters, and underscores',
|
||||
})
|
||||
}
|
||||
const requestParams = {
|
||||
name,
|
||||
description,
|
||||
icon: emoji,
|
||||
label,
|
||||
parameters: parameters.map(item => ({
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
form: item.form,
|
||||
})),
|
||||
labels,
|
||||
privacy_policy: privacyPolicy,
|
||||
}
|
||||
if (!isAdd) {
|
||||
onSave?.({
|
||||
...requestParams,
|
||||
workflow_tool_id: payload.workflow_tool_id,
|
||||
})
|
||||
}
|
||||
else {
|
||||
onCreate?.({
|
||||
...requestParams,
|
||||
workflow_app_id: payload.workflow_app_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
isShow
|
||||
onHide={onHide}
|
||||
title={t('workflow.common.workflowAsTool')!}
|
||||
panelClassName='mt-2 !w-[640px]'
|
||||
maxWidthClassName='!max-w-[640px]'
|
||||
height='calc(100vh - 16px)'
|
||||
headerClassName='!border-b-black/5'
|
||||
body={
|
||||
<div className='flex flex-col h-full'>
|
||||
<div className='grow h-0 overflow-y-auto px-6 py-3 space-y-4'>
|
||||
{/* name & icon */}
|
||||
<div>
|
||||
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.name')}</div>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.content} background={emoji.background} />
|
||||
<input
|
||||
type='text'
|
||||
className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
|
||||
placeholder={t('tools.createTool.toolNamePlaceHolder')!}
|
||||
value={label}
|
||||
onChange={e => setLabel(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* name for tool call */}
|
||||
<div>
|
||||
<div className='flex items-center py-2 leading-5 text-sm font-medium text-gray-900'>
|
||||
{t('tools.createTool.nameForToolCall')}
|
||||
<Tooltip
|
||||
htmlContent={
|
||||
<div className='w-[180px]'>
|
||||
{t('tools.createTool.nameForToolCallPlaceHolder')}
|
||||
</div>
|
||||
}
|
||||
selector='workflow-tool-modal-tooltip'
|
||||
>
|
||||
<HelpCircle className='ml-2 w-[14px] h-[14px] text-gray-400' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<input
|
||||
type='text'
|
||||
className='w-full h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
|
||||
placeholder={t('tools.createTool.nameForToolCallPlaceHolder')!}
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
{!isNameValid(name) && (
|
||||
<div className='text-xs leading-[18px] text-[#DC6803]'>{t('tools.createTool.nameForToolCallTip')}</div>
|
||||
)}
|
||||
</div>
|
||||
{/* description */}
|
||||
<div>
|
||||
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.description')}</div>
|
||||
<textarea
|
||||
className='w-full h-10 px-3 py-2 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs h-[80px] resize-none'
|
||||
placeholder={t('tools.createTool.descriptionPlaceholder') || ''}
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Tool Input */}
|
||||
<div>
|
||||
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.title')}</div>
|
||||
<div className='rounded-lg border border-gray-200 w-full overflow-x-auto'>
|
||||
<table className='w-full leading-[18px] text-xs text-gray-700 font-normal'>
|
||||
<thead className='text-gray-500 uppercase'>
|
||||
<tr className='border-b border-gray-200'>
|
||||
<th className="p-2 pl-3 font-medium w-[156px]">{t('tools.createTool.toolInput.name')}</th>
|
||||
<th className="p-2 pl-3 font-medium w-[102px]">{t('tools.createTool.toolInput.method')}</th>
|
||||
<th className="p-2 pl-3 font-medium">{t('tools.createTool.toolInput.description')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{parameters.map((item, index) => (
|
||||
<tr key={index} className='border-b last:border-0 border-gray-200'>
|
||||
<td className="p-2 pl-3 max-w-[156px]">
|
||||
<div className='text-[13px] leading-[18px]'>
|
||||
<div title={item.name} className='flex'>
|
||||
<span className='font-medium text-gray-900 truncate'>{item.name}</span>
|
||||
<span className='shrink-0 pl-1 text-[#ec4a0a] text-xs leading-[18px]'>{item.required ? t('tools.createTool.toolInput.required') : ''}</span>
|
||||
</div>
|
||||
<div className='text-gray-500'>{item.type}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{item.name === '__image' && (
|
||||
<div className={cn(
|
||||
'flex items-center gap-1 min-h-[56px] px-3 py-2 h-9 bg-white cursor-default',
|
||||
)}>
|
||||
<div className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate')}>
|
||||
{t('tools.createTool.toolInput.methodParameter')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item.name !== '__image' && (
|
||||
<MethodSelector value={item.form} onChange={value => handleParameterChange('form', value, index)}/>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2 pl-3 text-gray-500 w-[236px]">
|
||||
<input
|
||||
type='text'
|
||||
className='grow text-gray-700 text-[13px] leading-[18px] font-normal bg-white outline-none appearance-none caret-primary-600 placeholder:text-gray-300'
|
||||
placeholder={t('tools.createTool.toolInput.descriptionPlaceholder')!}
|
||||
value={item.description}
|
||||
onChange={e => handleParameterChange('description', e.target.value, index)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div>
|
||||
<LabelSelector value={labels} onChange={handleLabelSelect} />
|
||||
</div>
|
||||
{/* Privacy Policy */}
|
||||
<div>
|
||||
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.privacyPolicy')}</div>
|
||||
<input
|
||||
value={privacyPolicy}
|
||||
onChange={e => setPrivacyPolicy(e.target.value)}
|
||||
className='grow w-full h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs' placeholder={t('tools.createTool.privacyPolicyPlaceholder') || ''} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn((!isAdd && onRemove) ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-gray-50 border-t border-black/5')} >
|
||||
{!isAdd && onRemove && (
|
||||
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onRemove}>{t('common.operation.remove')}</Button>
|
||||
)}
|
||||
<div className='flex space-x-2 '>
|
||||
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button disabled={!label || !name || !isNameValid(name)} className='flex items-center h-8 !px-3 !text-[13px] font-medium' type='primary' onClick={() => {
|
||||
if (isAdd)
|
||||
onConfirm()
|
||||
else
|
||||
setShowModal(true)
|
||||
}}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
isShowMask={true}
|
||||
clickOutsideNotOpen={true}
|
||||
/>
|
||||
{showEmojiPicker && <EmojiPicker
|
||||
onSelect={(icon, icon_background) => {
|
||||
setEmoji({ content: icon, background: icon_background })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
/>}
|
||||
{showModal && (
|
||||
<ConfirmModal
|
||||
show={showModal}
|
||||
onClose={() => setShowModal(false)}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(WorkflowToolAsModal)
|
||||
77
web/app/components/tools/workflow-tool/method-selector.tsx
Normal file
77
web/app/components/tools/workflow-tool/method-selector.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type MethodSelectorProps = {
|
||||
value?: string
|
||||
onChange: (v: string) => void
|
||||
}
|
||||
const MethodSelector: FC<MethodSelectorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<div className='relative'>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className='block'
|
||||
>
|
||||
<div className={cn(
|
||||
'flex items-center gap-1 min-h-[56px] px-3 py-2 h-9 bg-white cursor-pointer hover:bg-gray-100',
|
||||
open && '!bg-gray-100 hover:bg-gray-100',
|
||||
)}>
|
||||
<div className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate')}>
|
||||
{value === 'llm' ? t('tools.createTool.toolInput.methodParameter') : t('tools.createTool.toolInput.methodSetting')}
|
||||
</div>
|
||||
<div className='shrink-0 ml-1 text-gray-700 opacity-60'>
|
||||
<ChevronDown className='h-4 w-4'/>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1040]'>
|
||||
<div className='relative w-[320px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
|
||||
<div className='p-1'>
|
||||
<div className='pl-3 pr-2 py-2.5 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => onChange('llm')}>
|
||||
<div className='flex item-center gap-1'>
|
||||
<div className='shrink-0 w-4 h-4'>
|
||||
{value === 'llm' && <Check className='shrink-0 w-4 h-4 text-primary-600'/>}
|
||||
</div>
|
||||
<div className='text-[13px] text-gray-700 font-medium leading-[18px]'>{t('tools.createTool.toolInput.methodParameter')}</div>
|
||||
</div>
|
||||
<div className='pl-5 text-gray-500 text-[13px] leading-[18px]'>{t('tools.createTool.toolInput.methodParameterTip')}</div>
|
||||
</div>
|
||||
<div className='pl-3 pr-2 py-2.5 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => onChange('form')}>
|
||||
<div className='flex item-center gap-1'>
|
||||
<div className='shrink-0 w-4 h-4'>
|
||||
{value === 'form' && <Check className='shrink-0 w-4 h-4 text-primary-600'/>}
|
||||
</div>
|
||||
<div className='text-[13px] text-gray-700 font-medium leading-[18px]'>{t('tools.createTool.toolInput.methodSetting')}</div>
|
||||
</div>
|
||||
<div className='pl-5 text-gray-500 text-[13px] leading-[18px]'>{t('tools.createTool.toolInput.methodSettingTip')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default MethodSelector
|
||||
Reference in New Issue
Block a user