mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-09 10:56:52 +08:00
Introduce Plugins (#13836)
Signed-off-by: yihong0618 <zouzou0208@gmail.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: xhe <xw897002528@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: takatost <takatost@gmail.com> Co-authored-by: kurokobo <kuro664@gmail.com> Co-authored-by: Novice Lee <novicelee@NoviPro.local> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: AkaraChen <akarachen@outlook.com> Co-authored-by: Yi <yxiaoisme@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: twwu <twwu@dify.ai> Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com> Co-authored-by: AkaraChen <85140972+AkaraChen@users.noreply.github.com> Co-authored-by: NFish <douxc512@gmail.com> Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Novice <857526207@qq.com> Co-authored-by: Hiroki Nagai <82458324+nagaihiroki-git@users.noreply.github.com> Co-authored-by: Gen Sato <52241300+halogen22@users.noreply.github.com> Co-authored-by: eux <euxuuu@gmail.com> Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com> Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com> Co-authored-by: lotsik <lotsik@mail.ru> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: gakkiyomi <gakkiyomi@aliyun.com> Co-authored-by: CN-P5 <heibai2006@gmail.com> Co-authored-by: CN-P5 <heibai2006@qq.com> Co-authored-by: Chuehnone <1897025+chuehnone@users.noreply.github.com> Co-authored-by: yihong <zouzou0208@gmail.com> Co-authored-by: Kevin9703 <51311316+Kevin9703@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Boris Feld <lothiraldan@gmail.com> Co-authored-by: mbo <himabo@gmail.com> Co-authored-by: mabo <mabo@aeyes.ai> Co-authored-by: Warren Chen <warren.chen830@gmail.com> Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com> Co-authored-by: jiandanfeng <chenjh3@wangsu.com> Co-authored-by: zhu-an <70234959+xhdd123321@users.noreply.github.com> Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com> Co-authored-by: 海狸大師 <86974027+yenslife@users.noreply.github.com> Co-authored-by: Xu Song <xusong.vip@gmail.com> Co-authored-by: rayshaw001 <396301947@163.com> Co-authored-by: Ding Jiatong <dingjiatong@gmail.com> Co-authored-by: Bowen Liang <liangbowen@gf.com.cn> Co-authored-by: JasonVV <jasonwangiii@outlook.com> Co-authored-by: le0zh <newlight@qq.com> Co-authored-by: zhuxinliang <zhuxinliang@didiglobal.com> Co-authored-by: k-zaku <zaku99@outlook.jp> Co-authored-by: luckylhb90 <luckylhb90@gmail.com> Co-authored-by: hobo.l <hobo.l@binance.com> Co-authored-by: jiangbo721 <365065261@qq.com> Co-authored-by: 刘江波 <jiangbo721@163.com> Co-authored-by: Shun Miyazawa <34241526+miya@users.noreply.github.com> Co-authored-by: EricPan <30651140+Egfly@users.noreply.github.com> Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: sino <sino2322@gmail.com> Co-authored-by: Jhvcc <37662342+Jhvcc@users.noreply.github.com> Co-authored-by: lowell <lowell.hu@zkteco.in> Co-authored-by: Boris Polonsky <BorisPolonsky@users.noreply.github.com> Co-authored-by: Ademílson Tonato <ademilsonft@outlook.com> Co-authored-by: Ademílson Tonato <ademilson.tonato@refurbed.com> Co-authored-by: IWAI, Masaharu <iwaim.sub@gmail.com> Co-authored-by: Yueh-Po Peng (Yabi) <94939112+y10ab1@users.noreply.github.com> Co-authored-by: Jason <ggbbddjm@gmail.com> Co-authored-by: Xin Zhang <sjhpzx@gmail.com> Co-authored-by: yjc980121 <3898524+yjc980121@users.noreply.github.com> Co-authored-by: heyszt <36215648+hieheihei@users.noreply.github.com> Co-authored-by: Abdullah AlOsaimi <osaimiacc@gmail.com> Co-authored-by: Abdullah AlOsaimi <189027247+osaimi@users.noreply.github.com> Co-authored-by: Yingchun Lai <laiyingchun@apache.org> Co-authored-by: Hash Brown <hi@xzd.me> Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com> Co-authored-by: Masashi Tomooka <tmokmss@users.noreply.github.com> Co-authored-by: aplio <ryo.091219@gmail.com> Co-authored-by: Obada Khalili <54270856+obadakhalili@users.noreply.github.com> Co-authored-by: Nam Vu <zuzoovn@gmail.com> Co-authored-by: Kei YAMAZAKI <1715090+kei-yamazaki@users.noreply.github.com> Co-authored-by: TechnoHouse <13776377+deephbz@users.noreply.github.com> Co-authored-by: Riddhimaan-Senapati <114703025+Riddhimaan-Senapati@users.noreply.github.com> Co-authored-by: MaFee921 <31881301+2284730142@users.noreply.github.com> Co-authored-by: te-chan <t-nakanome@sakura-is.co.jp> Co-authored-by: HQidea <HQidea@users.noreply.github.com> Co-authored-by: Joshbly <36315710+Joshbly@users.noreply.github.com> Co-authored-by: xhe <xw897002528@gmail.com> Co-authored-by: weiwenyan-dev <154779315+weiwenyan-dev@users.noreply.github.com> Co-authored-by: ex_wenyan.wei <ex_wenyan.wei@tcl.com> Co-authored-by: engchina <12236799+engchina@users.noreply.github.com> Co-authored-by: engchina <atjapan2015@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 呆萌闷油瓶 <253605712@qq.com> Co-authored-by: Kemal <kemalmeler@outlook.com> Co-authored-by: Lazy_Frog <4590648+lazyFrogLOL@users.noreply.github.com> Co-authored-by: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com> Co-authored-by: Steven sun <98230804+Tuyohai@users.noreply.github.com> Co-authored-by: steven <sunzwj@digitalchina.com> Co-authored-by: Kalo Chin <91766386+fdb02983rhy@users.noreply.github.com> Co-authored-by: Katy Tao <34019945+KatyTao@users.noreply.github.com> Co-authored-by: depy <42985524+h4ckdepy@users.noreply.github.com> Co-authored-by: 胡春东 <gycm520@gmail.com> Co-authored-by: Junjie.M <118170653@qq.com> Co-authored-by: MuYu <mr.muzea@gmail.com> Co-authored-by: Naoki Takashima <39912547+takatea@users.noreply.github.com> Co-authored-by: Summer-Gu <37869445+gubinjie@users.noreply.github.com> Co-authored-by: Fei He <droxer.he@gmail.com> Co-authored-by: ybalbert001 <120714773+ybalbert001@users.noreply.github.com> Co-authored-by: Yuanbo Li <ybalbert@amazon.com> Co-authored-by: douxc <7553076+douxc@users.noreply.github.com> Co-authored-by: liuzhenghua <1090179900@qq.com> Co-authored-by: Wu Jiayang <62842862+Wu-Jiayang@users.noreply.github.com> Co-authored-by: Your Name <you@example.com> Co-authored-by: kimjion <45935338+kimjion@users.noreply.github.com> Co-authored-by: AugNSo <song.tiankai@icloud.com> Co-authored-by: llinvokerl <38915183+llinvokerl@users.noreply.github.com> Co-authored-by: liusurong.lsr <liusurong.lsr@alibaba-inc.com> Co-authored-by: Vasu Negi <vasu-negi@users.noreply.github.com> Co-authored-by: Hundredwz <1808096180@qq.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { Credential } from '@/app/components/tools/types'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Radio from '@/app/components/base/radio/ui'
|
||||
@@ -16,7 +17,6 @@ type Props = {
|
||||
onChange: (credential: Credential) => void
|
||||
onHide: () => void
|
||||
}
|
||||
const keyClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
|
||||
|
||||
type ItemProps = {
|
||||
text: string
|
||||
@@ -28,11 +28,11 @@ type ItemProps = {
|
||||
const SelectItem: FC<ItemProps> = ({ text, value, isChecked, onClick }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(isChecked ? 'border-[2px] border-indigo-600 shadow-sm bg-white' : 'border border-gray-100', 'mb-2 flex items-center h-9 pl-3 w-[150px] rounded-xl bg-gray-25 hover:bg-gray-50 cursor-pointer space-x-2')}
|
||||
className={cn(isChecked ? 'border-[2px] border-util-colors-indigo-indigo-600 shadow-sm bg-components-panel-on-panel-item-bg' : 'border border-components-card-border', 'mb-2 flex items-center h-9 pl-3 w-[150px] rounded-xl bg-components-panel-on-panel-item-bg hover:bg-components-panel-on-panel-item-bg-hover cursor-pointer space-x-2')}
|
||||
onClick={() => onClick(value)}
|
||||
>
|
||||
<Radio isChecked={isChecked} />
|
||||
<div className='text-sm font-normal text-gray-900'>{text}</div>
|
||||
<div className='system-sm-regular text-text-primary'>{text}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -55,12 +55,12 @@ const ConfigCredential: FC<Props> = ({
|
||||
panelClassName='mt-2 !w-[520px] h-fit'
|
||||
maxWidthClassName='!max-w-[520px]'
|
||||
height={'fit-content'}
|
||||
headerClassName='!border-b-black/5'
|
||||
headerClassName='!border-b-divider-regular'
|
||||
body={
|
||||
<div className='pt-2 px-6'>
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<div className={keyClassNames}>{t('tools.createTool.authMethod.type')}</div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authMethod.type')}</div>
|
||||
<div className='flex space-x-3'>
|
||||
<SelectItem
|
||||
text={t('tools.createTool.authMethod.types.none')}
|
||||
@@ -84,52 +84,52 @@ const ConfigCredential: FC<Props> = ({
|
||||
</div>
|
||||
{tempCredential.auth_type === AuthType.apiKey && (
|
||||
<>
|
||||
<div className={keyClassNames}>{t('tools.createTool.authHeaderPrefix.title')}</div>
|
||||
<div className='flex space-x-3'>
|
||||
<SelectItem
|
||||
text={t('tools.createTool.authHeaderPrefix.types.basic')}
|
||||
value={AuthHeaderPrefix.basic}
|
||||
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.basic}
|
||||
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
|
||||
/>
|
||||
<SelectItem
|
||||
text={t('tools.createTool.authHeaderPrefix.types.bearer')}
|
||||
value={AuthHeaderPrefix.bearer}
|
||||
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.bearer}
|
||||
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
|
||||
/>
|
||||
<SelectItem
|
||||
text={t('tools.createTool.authHeaderPrefix.types.custom')}
|
||||
value={AuthHeaderPrefix.custom}
|
||||
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.custom}
|
||||
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
|
||||
/>
|
||||
<div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authHeaderPrefix.title')}</div>
|
||||
<div className='flex space-x-3'>
|
||||
<SelectItem
|
||||
text={t('tools.createTool.authHeaderPrefix.types.basic')}
|
||||
value={AuthHeaderPrefix.basic}
|
||||
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.basic}
|
||||
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
|
||||
/>
|
||||
<SelectItem
|
||||
text={t('tools.createTool.authHeaderPrefix.types.bearer')}
|
||||
value={AuthHeaderPrefix.bearer}
|
||||
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.bearer}
|
||||
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
|
||||
/>
|
||||
<SelectItem
|
||||
text={t('tools.createTool.authHeaderPrefix.types.custom')}
|
||||
value={AuthHeaderPrefix.custom}
|
||||
isChecked={tempCredential.api_key_header_prefix === AuthHeaderPrefix.custom}
|
||||
onClick={value => setTempCredential({ ...tempCredential, api_key_header_prefix: value as AuthHeaderPrefix })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='flex items-center h-8 text-[13px] font-medium text-gray-900'>
|
||||
<div className='flex items-center py-2 system-sm-medium text-text-primary'>
|
||||
{t('tools.createTool.authMethod.key')}
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[261px] text-gray-500'>
|
||||
<div className='w-[261px] text-text-tertiary'>
|
||||
{t('tools.createTool.authMethod.keyTooltip')}
|
||||
</div>
|
||||
}
|
||||
triggerClassName='ml-0.5 w-4 h-4'
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
<Input
|
||||
value={tempCredential.api_key_header}
|
||||
onChange={e => setTempCredential({ ...tempCredential, api_key_header: e.target.value })}
|
||||
className='w-full h-10 px-3 text-sm font-normal border border-transparent bg-gray-100 rounded-lg grow outline-none focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs'
|
||||
placeholder={t('tools.createTool.authMethod.types.apiKeyPlaceholder')!}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={keyClassNames}>{t('tools.createTool.authMethod.value')}</div>
|
||||
<input
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authMethod.value')}</div>
|
||||
<Input
|
||||
value={tempCredential.api_key_value}
|
||||
onChange={e => setTempCredential({ ...tempCredential, api_key_value: e.target.value })}
|
||||
className='w-full h-10 px-3 text-sm font-normal border border-transparent bg-gray-100 rounded-lg grow outline-none focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs'
|
||||
placeholder={t('tools.createTool.authMethod.types.apiValuePlaceholder')!}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import Toast from '../../base/toast'
|
||||
import examples from './examples'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { importSchemaFromURL } from '@/service/tools'
|
||||
|
||||
type Props = {
|
||||
@@ -63,14 +64,14 @@ const GetSchema: FC<Props> = ({
|
||||
onClick={() => { setShowImportFromUrl(!showImportFromUrl) }}
|
||||
>
|
||||
<RiAddLine className='w-3 h-3' />
|
||||
<div className='text-xs font-medium text-gray-700'>{t('tools.createTool.importFromUrl')}</div>
|
||||
<div className='system-xs-medium text-text-secondary'>{t('tools.createTool.importFromUrl')}</div>
|
||||
</Button>
|
||||
{showImportFromUrl && (
|
||||
<div className=' absolute left-[-35px] top-[26px] p-2 rounded-lg border border-gray-200 bg-white shadow-lg'>
|
||||
<div className=' absolute left-[-35px] top-[26px] p-2 rounded-lg border border-components-panel-border bg-components-panel-bg shadow-lg'>
|
||||
<div className='relative'>
|
||||
<input
|
||||
<Input
|
||||
type='text'
|
||||
className='w-[244px] h-8 pl-1.5 pr-[44px] overflow-x-auto border border-gray-200 rounded-lg text-[13px] focus:outline-none focus:border-components-input-border-active'
|
||||
className='w-[244px]'
|
||||
placeholder={t('tools.createTool.importFromUrlPlaceHolder')!}
|
||||
value={importUrl}
|
||||
onChange={e => setImportUrl(e.target.value)}
|
||||
@@ -95,11 +96,11 @@ const GetSchema: FC<Props> = ({
|
||||
className='space-x-1'
|
||||
onClick={() => { setShowExamples(!showExamples) }}
|
||||
>
|
||||
<div className='text-xs font-medium text-gray-700'>{t('tools.createTool.examples')}</div>
|
||||
<div className='system-xs-medium text-text-secondary'>{t('tools.createTool.examples')}</div>
|
||||
<RiArrowDownSLine className='w-3 h-3' />
|
||||
</Button>
|
||||
{showExamples && (
|
||||
<div className='absolute top-7 right-0 p-1 rounded-lg bg-white shadow-sm'>
|
||||
<div className='absolute top-7 right-0 p-1 rounded-lg bg-components-panel-bg shadow-sm'>
|
||||
{examples.map(item => (
|
||||
<div
|
||||
key={item.key}
|
||||
@@ -107,7 +108,7 @@ const GetSchema: FC<Props> = ({
|
||||
onChange(item.content)
|
||||
setShowExamples(false)
|
||||
}}
|
||||
className='px-3 py-1.5 rounded-lg hover:bg-gray-50 leading-5 text-sm font-normal text-gray-700 cursor-pointer whitespace-nowrap'
|
||||
className='px-3 py-1.5 rounded-lg hover:bg-components-panel-on-panel-item-bg-hover leading-5 system-sm-regular text-text-secondary cursor-pointer whitespace-nowrap'
|
||||
>
|
||||
{t(`tools.createTool.exampleOptions.${item.key}`)}
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,9 @@ import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounce, useGetState } from 'ahooks'
|
||||
import { RiSettings2Line } from '@remixicon/react'
|
||||
import produce from 'immer'
|
||||
import { LinkExternal02, Settings01 } from '../../base/icons/src/vender/line/general'
|
||||
import { LinkExternal02 } from '../../base/icons/src/vender/line/general'
|
||||
import type { Credential, CustomCollectionBackend, CustomParamSchema, Emoji } from '../types'
|
||||
import { AuthHeaderPrefix, AuthType } from '../types'
|
||||
import GetSchema from './get-schema'
|
||||
@@ -21,7 +22,6 @@ import { parseParamsSchema } from '@/service/tools'
|
||||
import LabelSelector from '@/app/components/tools/labels/selector'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
const fieldNameClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
|
||||
type Props = {
|
||||
positionLeft?: boolean
|
||||
payload: any
|
||||
@@ -189,12 +189,12 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
panelClassName='mt-2 !w-[640px]'
|
||||
maxWidthClassName='!max-w-[640px]'
|
||||
height='calc(100vh - 16px)'
|
||||
headerClassName='!border-b-black/5'
|
||||
headerClassName='!border-b-divider-regular'
|
||||
body={
|
||||
<div className='flex flex-col h-full'>
|
||||
<div className='grow h-0 overflow-y-auto px-6 py-3 space-y-4'>
|
||||
<div>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></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
|
||||
@@ -214,12 +214,12 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
<div className='select-none'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex items-center'>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.schema')}<span className='ml-1 text-red-500'>*</span></div>
|
||||
<div className='mx-2 w-px h-3 bg-black/5'></div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.schema')}<span className='ml-1 text-red-500'>*</span></div>
|
||||
<div className='mx-2 w-px h-3 bg-divider-regular'></div>
|
||||
<a
|
||||
href="https://swagger.io/specification/"
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='flex items-center h-[18px] space-x-1 text-[#155EEF]'
|
||||
className='flex items-center h-[18px] space-x-1 text-text-accent'
|
||||
>
|
||||
<div className='text-xs font-normal'>{t('tools.createTool.viewSchemaSpec')}</div>
|
||||
<LinkExternal02 className='w-3 h-3' />
|
||||
@@ -238,11 +238,11 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
|
||||
{/* Available Tools */}
|
||||
<div>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.availableTools.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={cn(paramsSchemas.length > 0 && 'border-b', 'border-gray-200')}>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.availableTools.title')}</div>
|
||||
<div className='rounded-lg border border-divider-regular w-full overflow-x-auto'>
|
||||
<table className='w-full system-xs-regular text-text-secondary'>
|
||||
<thead className='text-text-tertiary uppercase'>
|
||||
<tr className={cn(paramsSchemas.length > 0 && 'border-b', 'border-divider-regular')}>
|
||||
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.name')}</th>
|
||||
<th className="p-2 pl-3 font-medium w-[236px]">{t('tools.createTool.availableTools.description')}</th>
|
||||
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.method')}</th>
|
||||
@@ -252,9 +252,9 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
</thead>
|
||||
<tbody>
|
||||
{paramsSchemas.map((item, index) => (
|
||||
<tr key={index} className='border-b last:border-0 border-gray-200'>
|
||||
<tr key={index} className='border-b last:border-0 border-divider-regular'>
|
||||
<td className="p-2 pl-3">{item.operation_id}</td>
|
||||
<td className="p-2 pl-3 text-gray-500 w-[236px]">{item.summary}</td>
|
||||
<td className="p-2 pl-3 w-[236px]">{item.summary}</td>
|
||||
<td className="p-2 pl-3">{item.method}</td>
|
||||
<td className="p-2 pl-3">{getPath(item.server_url)}</td>
|
||||
<td className="p-2 pl-3 w-[62px]">
|
||||
@@ -277,22 +277,22 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
|
||||
{/* Authorization method */}
|
||||
<div>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.authMethod.title')}</div>
|
||||
<div className='flex items-center h-9 justify-between px-2.5 bg-gray-100 rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
|
||||
<div className='text-sm font-normal text-gray-900'>{t(`tools.createTool.authMethod.types.${credential.auth_type}`)}</div>
|
||||
<Settings01 className='w-4 h-4 text-gray-700 opacity-60' />
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authMethod.title')}</div>
|
||||
<div className='flex items-center h-9 justify-between px-2.5 bg-components-input-bg-normal rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
|
||||
<div className='system-xs-regular text-text-primary'>{t(`tools.createTool.authMethod.types.${credential.auth_type}`)}</div>
|
||||
<RiSettings2Line className='w-4 h-4 text-text-secondary' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
<div>
|
||||
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.toolInput.label')}</div>
|
||||
<LabelSelector value={labels} onChange={handleLabelSelect} />
|
||||
</div>
|
||||
|
||||
{/* Privacy Policy */}
|
||||
<div>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.privacyPolicy')}</div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.privacyPolicy')}</div>
|
||||
<Input
|
||||
value={customCollection.privacy_policy}
|
||||
onChange={(e) => {
|
||||
@@ -305,7 +305,7 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={fieldNameClassNames}>{t('tools.createTool.customDisclaimer')}</div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.customDisclaimer')}</div>
|
||||
<Input
|
||||
value={customCollection.custom_disclaimer}
|
||||
onChange={(e) => {
|
||||
@@ -318,10 +318,10 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className={cn(isEdit ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-gray-50 border-t border-black/5')} >
|
||||
<div className={cn(isEdit ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-background-section-burn border-t border-divider-regular')} >
|
||||
{
|
||||
isEdit && (
|
||||
<Button onClick={onRemove} className='text-red-500 border-red-50 hover:border-red-500'>{t('common.operation.delete')}</Button>
|
||||
<Button variant='warning' onClick={onRemove}>{t('common.operation.delete')}</Button>
|
||||
)
|
||||
}
|
||||
<div className='flex space-x-2 '>
|
||||
|
||||
360
web/app/components/tools/edit-custom-collection-modal/modal.tsx
Normal file
360
web/app/components/tools/edit-custom-collection-modal/modal.tsx
Normal file
@@ -0,0 +1,360 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounce, useGetState } from 'ahooks'
|
||||
import produce from 'immer'
|
||||
import { LinkExternal02, Settings01 } from '../../base/icons/src/vender/line/general'
|
||||
import type { Credential, CustomCollectionBackend, CustomParamSchema, Emoji } from '../types'
|
||||
import { AuthHeaderPrefix, AuthType } from '../types'
|
||||
import GetSchema from './get-schema'
|
||||
import ConfigCredentials from './config-credentials'
|
||||
import TestApi from './test-api'
|
||||
import cn from '@/utils/classnames'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
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'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Modal from '../../base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type Props = {
|
||||
positionLeft?: boolean
|
||||
payload: any
|
||||
onHide: () => void
|
||||
onAdd?: (payload: CustomCollectionBackend) => void
|
||||
onRemove?: () => void
|
||||
onEdit?: (payload: CustomCollectionBackend) => void
|
||||
}
|
||||
// Add and Edit
|
||||
const EditCustomCollectionModal: FC<Props> = ({
|
||||
payload,
|
||||
onHide,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isAdd = !payload
|
||||
const isEdit = !!payload
|
||||
|
||||
const [editFirst, setEditFirst] = useState(!isAdd)
|
||||
const [paramsSchemas, setParamsSchemas] = useState<CustomParamSchema[]>(payload?.tools || [])
|
||||
const [customCollection, setCustomCollection, getCustomCollection] = useGetState<CustomCollectionBackend>(isAdd
|
||||
? {
|
||||
provider: '',
|
||||
credentials: {
|
||||
auth_type: AuthType.none,
|
||||
api_key_header: 'Authorization',
|
||||
api_key_header_prefix: AuthHeaderPrefix.basic,
|
||||
},
|
||||
icon: {
|
||||
content: '🕵️',
|
||||
background: '#FEF7C3',
|
||||
},
|
||||
schema_type: '',
|
||||
schema: '',
|
||||
}
|
||||
: payload)
|
||||
|
||||
const originalProvider = isEdit ? payload.provider : ''
|
||||
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const emoji = customCollection.icon
|
||||
const setEmoji = (emoji: Emoji) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.icon = emoji
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}
|
||||
const schema = customCollection.schema
|
||||
const debouncedSchema = useDebounce(schema, { wait: 500 })
|
||||
const setSchema = (schema: any) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.schema = schema
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!debouncedSchema)
|
||||
return
|
||||
if (isEdit && editFirst) {
|
||||
setEditFirst(false)
|
||||
return
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const { parameters_schema, schema_type } = await parseParamsSchema(debouncedSchema)
|
||||
const customCollection = getCustomCollection()
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.schema_type = schema_type
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
setParamsSchemas(parameters_schema)
|
||||
}
|
||||
catch (e) {
|
||||
const customCollection = getCustomCollection()
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.schema_type = ''
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
setParamsSchemas([])
|
||||
}
|
||||
})()
|
||||
}, [debouncedSchema])
|
||||
|
||||
const [credentialsModalShow, setCredentialsModalShow] = useState(false)
|
||||
const credential = customCollection.credentials
|
||||
const setCredential = (credential: Credential) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.credentials = credential
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}
|
||||
|
||||
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) => {
|
||||
delete draft.tools
|
||||
|
||||
if (draft.credentials.auth_type === AuthType.none) {
|
||||
delete draft.credentials.api_key_header
|
||||
delete draft.credentials.api_key_header_prefix
|
||||
delete draft.credentials.api_key_value
|
||||
}
|
||||
|
||||
draft.labels = labels
|
||||
})
|
||||
|
||||
let errorMessage = ''
|
||||
if (!postData.provider)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t('tools.createTool.name') })
|
||||
|
||||
if (!postData.schema)
|
||||
errorMessage = t('common.errorMsg.fieldRequired', { field: t('tools.createTool.schema') })
|
||||
|
||||
if (errorMessage) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isAdd) {
|
||||
onAdd?.(postData)
|
||||
return
|
||||
}
|
||||
|
||||
onEdit?.({
|
||||
...postData,
|
||||
original_provider: originalProvider,
|
||||
})
|
||||
}
|
||||
|
||||
const getPath = (url: string) => {
|
||||
if (!url)
|
||||
return ''
|
||||
|
||||
try {
|
||||
const path = decodeURI(new URL(url).pathname)
|
||||
return path || ''
|
||||
}
|
||||
catch (e) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onHide}
|
||||
closable
|
||||
className='!p-0 !max-w-[630px] !h-[calc(100vh-16px)]'
|
||||
>
|
||||
<div className='flex flex-col h-full'>
|
||||
<div className='ml-6 mt-6 text-base font-semibold text-text-primary'>
|
||||
{t('tools.createTool.title')}
|
||||
</div>
|
||||
<div className='grow h-0 overflow-y-auto px-6 py-3 space-y-4'>
|
||||
<div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></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
|
||||
className='h-10 grow' placeholder={t('tools.createTool.toolNamePlaceHolder')!}
|
||||
value={customCollection.provider}
|
||||
onChange={(e) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.provider = e.target.value
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schema */}
|
||||
<div className='select-none'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex items-center'>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.schema')}<span className='ml-1 text-red-500'>*</span></div>
|
||||
<div className='mx-2 w-px h-3 bg-divider-regular'></div>
|
||||
<a
|
||||
href="https://swagger.io/specification/"
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='flex items-center h-[18px] space-x-1 text-text-accent'
|
||||
>
|
||||
<div className='text-xs font-normal'>{t('tools.createTool.viewSchemaSpec')}</div>
|
||||
<LinkExternal02 className='w-3 h-3' />
|
||||
</a>
|
||||
</div>
|
||||
<GetSchema onChange={setSchema} />
|
||||
|
||||
</div>
|
||||
<Textarea
|
||||
className='h-[240px] resize-none'
|
||||
value={schema}
|
||||
onChange={e => setSchema(e.target.value)}
|
||||
placeholder={t('tools.createTool.schemaPlaceHolder')!}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Available Tools */}
|
||||
<div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.availableTools.title')}</div>
|
||||
<div className='rounded-lg border border-divider-regular w-full overflow-x-auto'>
|
||||
<table className='w-full system-xs-regular text-text-secondary'>
|
||||
<thead className='text-text-tertiary uppercase'>
|
||||
<tr className={cn(paramsSchemas.length > 0 && 'border-b', 'border-divider-regular')}>
|
||||
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.name')}</th>
|
||||
<th className="p-2 pl-3 font-medium w-[236px]">{t('tools.createTool.availableTools.description')}</th>
|
||||
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.method')}</th>
|
||||
<th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.path')}</th>
|
||||
<th className="p-2 pl-3 font-medium w-[54px]">{t('tools.createTool.availableTools.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paramsSchemas.map((item, index) => (
|
||||
<tr key={index} className='border-b last:border-0 border-divider-regular'>
|
||||
<td className="p-2 pl-3">{item.operation_id}</td>
|
||||
<td className="p-2 pl-3 w-[236px]">{item.summary}</td>
|
||||
<td className="p-2 pl-3">{item.method}</td>
|
||||
<td className="p-2 pl-3">{getPath(item.server_url)}</td>
|
||||
<td className="p-2 pl-3 w-[62px]">
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() => {
|
||||
setCurrTool(item)
|
||||
setIsShowTestApi(true)
|
||||
}}
|
||||
>
|
||||
{t('tools.createTool.availableTools.test')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Authorization method */}
|
||||
<div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authMethod.title')}</div>
|
||||
<div className='flex items-center h-9 justify-between px-2.5 bg-components-input-bg-normal rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
|
||||
<div className='system-xs-regular text-text-primary'>{t(`tools.createTool.authMethod.types.${credential.auth_type}`)}</div>
|
||||
<Settings01 className='w-4 h-4 text-text-secondary' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
<div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.toolInput.label')}</div>
|
||||
<LabelSelector value={labels} onChange={handleLabelSelect} />
|
||||
</div>
|
||||
|
||||
{/* Privacy Policy */}
|
||||
<div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.privacyPolicy')}</div>
|
||||
<Input
|
||||
value={customCollection.privacy_policy}
|
||||
onChange={(e) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.privacy_policy = e.target.value
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}}
|
||||
className='h-10 grow' placeholder={t('tools.createTool.privacyPolicyPlaceholder') || ''} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.customDisclaimer')}</div>
|
||||
<Input
|
||||
value={customCollection.custom_disclaimer}
|
||||
onChange={(e) => {
|
||||
const newCollection = produce(customCollection, (draft) => {
|
||||
draft.custom_disclaimer = e.target.value
|
||||
})
|
||||
setCustomCollection(newCollection)
|
||||
}}
|
||||
className='h-10 grow' placeholder={t('tools.createTool.customDisclaimerPlaceholder') || ''} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className={cn(isEdit ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-background-section-burn border-t border-divider-regular')} >
|
||||
{
|
||||
isEdit && (
|
||||
<Button variant='warning' onClick={onRemove}>{t('common.operation.delete')}</Button>
|
||||
)
|
||||
}
|
||||
<div className='flex space-x-2 '>
|
||||
<Button onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
{showEmojiPicker && <EmojiPicker
|
||||
onSelect={(icon, icon_background) => {
|
||||
setEmoji({ content: icon, background: icon_background })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
/>}
|
||||
{credentialsModalShow && (
|
||||
<ConfigCredentials
|
||||
positionCenter={isAdd}
|
||||
credential={credential}
|
||||
onChange={setCredential}
|
||||
onHide={() => setCredentialsModalShow(false)}
|
||||
/>)
|
||||
}
|
||||
{isShowTestApi && (
|
||||
<TestApi
|
||||
positionCenter={isAdd}
|
||||
tool={currTool as CustomParamSchema}
|
||||
customCollection={customCollection}
|
||||
onHide={() => setIsShowTestApi(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(EditCustomCollectionModal)
|
||||
@@ -3,10 +3,11 @@ import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { Settings01 } from '../../base/icons/src/vender/line/general'
|
||||
import { RiSettings2Line } from '@remixicon/react'
|
||||
import ConfigCredentials from './config-credentials'
|
||||
import { AuthType, type Credential, type CustomCollectionBackend, type CustomParamSchema } from '@/app/components/tools/types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import I18n from '@/context/i18n'
|
||||
import { testAPIAvailable } from '@/service/tools'
|
||||
@@ -19,8 +20,6 @@ type Props = {
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const keyClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
|
||||
|
||||
const TestApi: FC<Props> = ({
|
||||
positionCenter,
|
||||
customCollection,
|
||||
@@ -65,39 +64,40 @@ const TestApi: FC<Props> = ({
|
||||
panelClassName='mt-2 !w-[600px]'
|
||||
maxWidthClassName='!max-w-[600px]'
|
||||
height='calc(100vh - 16px)'
|
||||
headerClassName='!border-b-black/5'
|
||||
headerClassName='!border-b-divider-regular'
|
||||
body={
|
||||
<div className='pt-2 px-6 overflow-y-auto'>
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<div className={keyClassNames}>{t('tools.createTool.authMethod.title')}</div>
|
||||
<div className='flex items-center h-9 justify-between px-2.5 bg-gray-100 rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
|
||||
<div className='text-sm font-normal text-gray-900'>{t(`tools.createTool.authMethod.types.${tempCredential.auth_type}`)}</div>
|
||||
<Settings01 className='w-4 h-4 text-gray-700 opacity-60' />
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.authMethod.title')}</div>
|
||||
<div className='flex items-center h-9 justify-between px-2.5 bg-components-input-bg-normal rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}>
|
||||
<div className='system-xs-regular text-text-primary'>{t(`tools.createTool.authMethod.types.${tempCredential.auth_type}`)}</div>
|
||||
<RiSettings2Line className='w-4 h-4 text-text-secondary' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={keyClassNames}>{t('tools.test.parametersValue')}</div>
|
||||
<div className='rounded-lg border border-gray-200'>
|
||||
<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'>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.test.parametersValue')}</div>
|
||||
<div className='rounded-lg border border-divider-regular'>
|
||||
<table className='w-full system-xs-regular text-text-secondary font-normal'>
|
||||
<thead className='text-text-tertiary uppercase'>
|
||||
<tr className='border-b border-divider-regular'>
|
||||
<th className="p-2 pl-3 font-medium">{t('tools.test.parameters')}</th>
|
||||
<th className="p-2 pl-3 font-medium">{t('tools.test.value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{parameters.map((item, index) => (
|
||||
<tr key={index} className='border-b last:border-0 border-gray-200'>
|
||||
<tr key={index} className='border-b last:border-0 border-divider-regular'>
|
||||
<td className="py-2 pl-3 pr-2.5">
|
||||
{item.label[language]}
|
||||
</td>
|
||||
<td className="">
|
||||
<input
|
||||
<Input
|
||||
value={parametersValue[item.name] || ''}
|
||||
onChange={e => setParametersValue({ ...parametersValue, [item.name]: e.target.value })}
|
||||
type='text' className='px-3 h-[34px] w-full outline-none focus:bg-gray-100' ></input>
|
||||
type='text'
|
||||
className='!bg-transparent !border-transparent !hover:border-transparent !hover:bg-transparent !focus:border-transparent !focus:bg-transparent' />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -110,11 +110,11 @@ const TestApi: FC<Props> = ({
|
||||
<Button variant='primary' className=' mt-4 w-full h-10' onClick={handleTest}>{t('tools.test.title')}</Button>
|
||||
<div className='mt-6'>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<div className='leading-[18px] text-xs font-semibold text-gray-500'>{t('tools.test.testResult')}</div>
|
||||
<div className='system-xs-semibold text-text-tertiary'>{t('tools.test.testResult')}</div>
|
||||
<div className='grow w-0 h-px bg-[rgb(243, 244, 246)]'></div>
|
||||
</div>
|
||||
<div className='mt-2 px-3 py-2 h-[200px] overflow-y-auto overflow-x-hidden rounded-lg bg-gray-100 leading-4 text-xs font-normal text-gray-700'>
|
||||
{result || <span className='text-gray-400'>{t('tools.test.testResultPlaceholder')}</span>}
|
||||
<div className='mt-2 px-3 py-2 h-[200px] overflow-y-auto overflow-x-hidden rounded-lg bg-components-input-bg-normal system-xs-regular text-text-secondary'>
|
||||
{result || <span className='text-text-quaternary'>{t('tools.test.testResultPlaceholder')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
export type Label = {
|
||||
name: string
|
||||
icon: string
|
||||
label: TypeWithI18N
|
||||
label: string
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
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 { useDebounceFn } from 'ahooks'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { useStore as useLabelStore } from './store'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
@@ -16,9 +14,7 @@ import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financ
|
||||
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'
|
||||
import { useTags } from '@/app/components/plugins/hooks'
|
||||
|
||||
type LabelFilterProps = {
|
||||
value: string[]
|
||||
@@ -29,12 +25,9 @@ const LabelFilter: FC<LabelFilterProps> = ({
|
||||
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 { tags: labelList } = useTags()
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
@@ -61,12 +54,6 @@ const LabelFilter: FC<LabelFilterProps> = ({
|
||||
onChange([...value, label.name])
|
||||
}
|
||||
|
||||
useMount(() => {
|
||||
fetchLabelList().then((res) => {
|
||||
setLabelList(res)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
@@ -80,24 +67,23 @@ const LabelFilter: FC<LabelFilterProps> = ({
|
||||
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',
|
||||
'flex items-center gap-1 px-2 h-8 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal cursor-pointer hover:bg-components-input-bg-hover',
|
||||
!open && !!value.length && 'shadow-xs',
|
||||
open && !!value.length && 'shadow-xs',
|
||||
)}>
|
||||
<div className='p-[1px]'>
|
||||
<Tag01 className='h-3.5 w-3.5 text-gray-700' />
|
||||
<Tag01 className='h-3.5 w-3.5 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='text-[13px] leading-[18px] text-gray-700'>
|
||||
<div className='text-[13px] leading-[18px] text-text-tertiary'>
|
||||
{!value.length && t('common.tag.placeholder')}
|
||||
{!!value.length && currentLabel?.label[language]}
|
||||
{!!value.length && currentLabel?.label}
|
||||
</div>
|
||||
{value.length > 1 && (
|
||||
<div className='text-xs font-medium leading-[18px] text-gray-500'>{`+${value.length - 1}`}</div>
|
||||
<div className='text-xs font-medium leading-[18px] text-text-tertiary'>{`+${value.length - 1}`}</div>
|
||||
)}
|
||||
{!value.length && (
|
||||
<div className='p-[1px]'>
|
||||
<RiArrowDownSLine className='h-3.5 w-3.5 text-gray-700' />
|
||||
<RiArrowDownSLine className='h-3.5 w-3.5 text-text-tertiary' />
|
||||
</div>
|
||||
)}
|
||||
{!!value.length && (
|
||||
@@ -105,14 +91,14 @@ const LabelFilter: FC<LabelFilterProps> = ({
|
||||
e.stopPropagation()
|
||||
onChange([])
|
||||
}}>
|
||||
<XCircle className='h-3.5 w-3.5 text-gray-400 group-hover/clear:text-gray-600' />
|
||||
<XCircle className='h-3.5 w-3.5 text-text-tertiary group-hover/clear:text-text-secondary' />
|
||||
</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'>
|
||||
<div className='relative w-[240px] bg-components-panel-bg-blur rounded-lg border-[0.5px] backdrop-blur-[5px] border-components-panel-border shadow-lg'>
|
||||
<div className='p-2'>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
@@ -125,17 +111,17 @@ const LabelFilter: FC<LabelFilterProps> = ({
|
||||
{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'
|
||||
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-state-base-hover'
|
||||
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 title={label.label} className='grow text-sm text-text-secondary leading-5 truncate'>{label.label}</div>
|
||||
{value.includes(label.name) && <Check className='shrink-0 w-4 h-4 text-text-accent' />}
|
||||
</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>
|
||||
<Tag03 className='h-6 w-6 text-text-quaternary' />
|
||||
<div className='text-text-tertiary text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
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 { useDebounceFn } from 'ahooks'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { useStore as useLabelStore } from './store'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
@@ -15,9 +13,7 @@ import Input from '@/app/components/base/input'
|
||||
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'
|
||||
import { useTags } from '@/app/components/plugins/hooks'
|
||||
|
||||
type LabelSelectorProps = {
|
||||
value: string[]
|
||||
@@ -28,12 +24,9 @@ const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
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 { tags: labelList } = useTags()
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
@@ -50,8 +43,8 @@ const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
}, [labelList, searchKeywords])
|
||||
|
||||
const selectedLabels = useMemo(() => {
|
||||
return value.map(v => labelList.find(l => l.name === v)?.label[language]).join(', ')
|
||||
}, [value, labelList, language])
|
||||
return value.map(v => labelList.find(l => l.name === v)?.label).join(', ')
|
||||
}, [value, labelList])
|
||||
|
||||
const selectLabel = (label: Label) => {
|
||||
if (value.includes(label.name))
|
||||
@@ -60,12 +53,6 @@ const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
onChange([...value, label.name])
|
||||
}
|
||||
|
||||
useMount(() => {
|
||||
fetchLabelList().then((res) => {
|
||||
setLabelList(res)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
@@ -79,21 +66,21 @@ const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
className='block'
|
||||
>
|
||||
<div className={cn(
|
||||
'flex items-center gap-1 px-3 h-10 rounded-lg border-[0.5px] border-transparent bg-gray-100 cursor-pointer hover:bg-gray-200',
|
||||
open && '!bg-gray-200 hover:bg-gray-200',
|
||||
'flex items-center gap-1 px-3 h-10 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal cursor-pointer hover:bg-components-input-bg-hover',
|
||||
open && '!hover:bg-components-input-bg-hover hover:bg-components-input-bg-hover',
|
||||
)}>
|
||||
<div title={value.length > 0 ? selectedLabels : ''} className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate', !value.length && '!text-gray-400')}>
|
||||
<div title={value.length > 0 ? selectedLabels : ''} className={cn('grow text-[13px] leading-[18px] text-text-secondary truncate', !value.length && '!text-text-quaternary')}>
|
||||
{!value.length && t('tools.createTool.toolInput.labelPlaceholder')}
|
||||
{!!value.length && selectedLabels}
|
||||
</div>
|
||||
<div className='shrink-0 ml-1 text-gray-700 opacity-60'>
|
||||
<div className='shrink-0 ml-1 text-text-secondary opacity-60'>
|
||||
<RiArrowDownSLine 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'>
|
||||
<div className='relative w-[591px] bg-components-panel-bg-blur backdrop-blur-[5px] rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
|
||||
<div className='p-2 border-b-[0.5px] border-divider-regular'>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
@@ -106,7 +93,7 @@ const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
{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'
|
||||
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover'
|
||||
onClick={() => selectLabel(label)}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -114,13 +101,13 @@ const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
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 title={label.label} className='grow text-sm text-text-secondary leading-5 truncate'>{label.label}</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>
|
||||
<Tag03 className='h-6 w-6 text-text-quaternary' />
|
||||
<div className='text-text-tertiary text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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 })),
|
||||
}))
|
||||
117
web/app/components/tools/marketplace/hooks.ts
Normal file
117
web/app/components/tools/marketplace/hooks.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
useMarketplaceCollectionsAndPlugins,
|
||||
useMarketplacePlugins,
|
||||
} from '@/app/components/plugins/marketplace/hooks'
|
||||
import { PluginType } from '@/app/components/plugins/types'
|
||||
import { getMarketplaceListCondition } from '@/app/components/plugins/marketplace/utils'
|
||||
import { useAllToolProviders } from '@/service/use-tools'
|
||||
|
||||
export const useMarketplace = (searchPluginText: string, filterPluginTags: string[]) => {
|
||||
const { data: toolProvidersData, isSuccess } = useAllToolProviders()
|
||||
const exclude = useMemo(() => {
|
||||
if (isSuccess)
|
||||
return toolProvidersData?.filter(toolProvider => !!toolProvider.plugin_id).map(toolProvider => toolProvider.plugin_id!)
|
||||
}, [isSuccess, toolProvidersData])
|
||||
const {
|
||||
isLoading,
|
||||
marketplaceCollections,
|
||||
marketplaceCollectionPluginsMap,
|
||||
queryMarketplaceCollectionsAndPlugins,
|
||||
} = useMarketplaceCollectionsAndPlugins()
|
||||
const {
|
||||
plugins,
|
||||
resetPlugins,
|
||||
queryPlugins,
|
||||
queryPluginsWithDebounced,
|
||||
isLoading: isPluginsLoading,
|
||||
total: pluginsTotal,
|
||||
} = useMarketplacePlugins()
|
||||
const [page, setPage] = useState(1)
|
||||
const pageRef = useRef(page)
|
||||
const searchPluginTextRef = useRef(searchPluginText)
|
||||
const filterPluginTagsRef = useRef(filterPluginTags)
|
||||
|
||||
useEffect(() => {
|
||||
searchPluginTextRef.current = searchPluginText
|
||||
filterPluginTagsRef.current = filterPluginTags
|
||||
}, [searchPluginText, filterPluginTags])
|
||||
useEffect(() => {
|
||||
if ((searchPluginText || filterPluginTags.length) && isSuccess) {
|
||||
setPage(1)
|
||||
pageRef.current = 1
|
||||
|
||||
if (searchPluginText) {
|
||||
queryPluginsWithDebounced({
|
||||
category: PluginType.tool,
|
||||
query: searchPluginText,
|
||||
tags: filterPluginTags,
|
||||
exclude,
|
||||
type: 'plugin',
|
||||
page: pageRef.current,
|
||||
})
|
||||
return
|
||||
}
|
||||
queryPlugins({
|
||||
category: PluginType.tool,
|
||||
query: searchPluginText,
|
||||
tags: filterPluginTags,
|
||||
exclude,
|
||||
type: 'plugin',
|
||||
page: pageRef.current,
|
||||
})
|
||||
}
|
||||
else {
|
||||
if (isSuccess) {
|
||||
queryMarketplaceCollectionsAndPlugins({
|
||||
category: PluginType.tool,
|
||||
condition: getMarketplaceListCondition(PluginType.tool),
|
||||
exclude,
|
||||
type: 'plugin',
|
||||
})
|
||||
resetPlugins()
|
||||
}
|
||||
}
|
||||
}, [searchPluginText, filterPluginTags, queryPlugins, queryMarketplaceCollectionsAndPlugins, queryPluginsWithDebounced, resetPlugins, exclude, isSuccess])
|
||||
|
||||
const handleScroll = useCallback((e: Event) => {
|
||||
const target = e.target as HTMLDivElement
|
||||
const {
|
||||
scrollTop,
|
||||
scrollHeight,
|
||||
clientHeight,
|
||||
} = target
|
||||
if (scrollTop + clientHeight >= scrollHeight - 5 && scrollTop > 0) {
|
||||
const searchPluginText = searchPluginTextRef.current
|
||||
const filterPluginTags = filterPluginTagsRef.current
|
||||
if (pluginsTotal && plugins && pluginsTotal > plugins.length && (!!searchPluginText || !!filterPluginTags.length)) {
|
||||
setPage(pageRef.current + 1)
|
||||
pageRef.current++
|
||||
|
||||
queryPlugins({
|
||||
category: PluginType.tool,
|
||||
query: searchPluginText,
|
||||
tags: filterPluginTags,
|
||||
exclude,
|
||||
type: 'plugin',
|
||||
page: pageRef.current,
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [exclude, plugins, pluginsTotal, queryPlugins])
|
||||
|
||||
return {
|
||||
isLoading: isLoading || isPluginsLoading,
|
||||
marketplaceCollections,
|
||||
marketplaceCollectionPluginsMap,
|
||||
plugins,
|
||||
handleScroll,
|
||||
page,
|
||||
}
|
||||
}
|
||||
117
web/app/components/tools/marketplace/index.tsx
Normal file
117
web/app/components/tools/marketplace/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import {
|
||||
RiArrowRightUpLine,
|
||||
RiArrowUpDoubleLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMarketplace } from './hooks'
|
||||
import List from '@/app/components/plugins/marketplace/list'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { getLocaleOnClient } from '@/i18n'
|
||||
import { MARKETPLACE_URL_PREFIX } from '@/config'
|
||||
|
||||
type MarketplaceProps = {
|
||||
searchPluginText: string
|
||||
filterPluginTags: string[]
|
||||
onMarketplaceScroll: () => void
|
||||
}
|
||||
const Marketplace = ({
|
||||
searchPluginText,
|
||||
filterPluginTags,
|
||||
onMarketplaceScroll,
|
||||
}: MarketplaceProps) => {
|
||||
const locale = getLocaleOnClient()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
marketplaceCollections,
|
||||
marketplaceCollectionPluginsMap,
|
||||
plugins,
|
||||
handleScroll,
|
||||
page,
|
||||
} = useMarketplace(searchPluginText, filterPluginTags)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (container)
|
||||
container.addEventListener('scroll', handleScroll)
|
||||
|
||||
return () => {
|
||||
if (container)
|
||||
container.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [handleScroll])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='grow flex flex-col shrink-0 sticky bottom-[-442px] h-[530px] overflow-y-auto px-12 py-2 pt-0 bg-background-default-subtle'
|
||||
>
|
||||
<RiArrowUpDoubleLine
|
||||
className='absolute top-2 left-1/2 -translate-x-1/2 w-4 h-4 text-text-quaternary cursor-pointer'
|
||||
onClick={() => onMarketplaceScroll()}
|
||||
/>
|
||||
<div className='sticky top-0 pt-5 pb-3 bg-background-default-subtle z-10'>
|
||||
<div className='title-2xl-semi-bold bg-gradient-to-r from-[rgba(11,165,236,0.95)] to-[rgba(21,90,239,0.95)] bg-clip-text text-transparent'>
|
||||
{t('plugin.marketplace.moreFrom')}
|
||||
</div>
|
||||
<div className='flex items-center text-center body-md-regular text-text-tertiary'>
|
||||
{t('plugin.marketplace.discover')}
|
||||
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
||||
{t('plugin.category.models')}
|
||||
</span>
|
||||
,
|
||||
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
||||
{t('plugin.category.tools')}
|
||||
</span>
|
||||
,
|
||||
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
||||
{t('plugin.category.agents')}
|
||||
</span>
|
||||
,
|
||||
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
||||
{t('plugin.category.extensions')}
|
||||
</span>
|
||||
{t('plugin.marketplace.and')}
|
||||
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
|
||||
{t('plugin.category.bundles')}
|
||||
</span>
|
||||
{t('common.operation.in')}
|
||||
<a
|
||||
href={`${MARKETPLACE_URL_PREFIX}?language=${locale}&q=${searchPluginText}&tags=${filterPluginTags.join(',')}`}
|
||||
className='flex items-center ml-1 system-sm-medium text-text-accent'
|
||||
target='_blank'
|
||||
>
|
||||
{t('plugin.marketplace.difyMarketplace')}
|
||||
<RiArrowRightUpLine className='w-4 h-4' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
isLoading && page === 1 && (
|
||||
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(!isLoading || page > 1) && (
|
||||
<List
|
||||
marketplaceCollections={marketplaceCollections || []}
|
||||
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap || {}}
|
||||
plugins={plugins}
|
||||
showInstallButton
|
||||
locale={locale}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Marketplace
|
||||
@@ -1,33 +1,36 @@
|
||||
'use client'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import type { Collection } from './types'
|
||||
import Marketplace from './marketplace'
|
||||
import cn from '@/utils/classnames'
|
||||
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 Input from '@/app/components/base/input'
|
||||
import { DotsGrid } 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'
|
||||
import Empty from '@/app/components/plugins/marketplace/empty'
|
||||
import CustomCreateCard from '@/app/components/tools/provider/custom-create-card'
|
||||
import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty'
|
||||
import Card from '@/app/components/plugins/card'
|
||||
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { useAllToolProviders } from '@/service/use-tools'
|
||||
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
|
||||
const ProviderList = () => {
|
||||
const { t } = useTranslation()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
|
||||
|
||||
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' /> },
|
||||
{ value: 'builtin', text: t('tools.type.builtIn') },
|
||||
{ value: 'api', text: t('tools.type.custom') },
|
||||
{ value: 'workflow', text: t('tools.type.workflow') },
|
||||
]
|
||||
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
|
||||
const handleTagsChange = (value: string[]) => {
|
||||
@@ -37,8 +40,7 @@ const ProviderList = () => {
|
||||
const handleKeywordsChange = (value: string) => {
|
||||
setKeywords(value)
|
||||
}
|
||||
|
||||
const [collectionList, setCollectionList] = useState<Collection[]>([])
|
||||
const { data: collectionList = [], refetch } = useAllToolProviders()
|
||||
const filteredCollectionList = useMemo(() => {
|
||||
return collectionList.filter((collection) => {
|
||||
if (collection.type !== activeTab)
|
||||
@@ -50,75 +52,110 @@ const ProviderList = () => {
|
||||
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])
|
||||
const { data: pluginList } = useInstalledPluginList()
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
const currentPluginDetail = useMemo(() => {
|
||||
const detail = pluginList?.plugins.find(plugin => plugin.plugin_id === currentProvider?.plugin_id)
|
||||
return detail
|
||||
}, [currentProvider?.plugin_id, pluginList?.plugins])
|
||||
|
||||
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(
|
||||
'sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-20 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} />
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
wrapperClassName='w-[200px]'
|
||||
value={keywords}
|
||||
onChange={e => handleKeywordsChange(e.target.value)}
|
||||
onClear={() => handleKeywordsChange('')}
|
||||
<>
|
||||
<div className='relative flex overflow-hidden shrink-0 h-0 grow'>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='relative flex flex-col overflow-y-auto bg-background-body grow'
|
||||
>
|
||||
<div className={cn(
|
||||
'sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-background-body z-20 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} />
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
wrapperClassName='w-[200px]'
|
||||
value={keywords}
|
||||
onChange={e => handleKeywordsChange(e.target.value)}
|
||||
onClear={() => handleKeywordsChange('')}
|
||||
/>
|
||||
</div>
|
||||
</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>}
|
||||
{(filteredCollectionList.length > 0 || activeTab !== 'builtin') && (
|
||||
<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 shrink-0',
|
||||
!filteredCollectionList.length && activeTab === 'workflow' && 'grow',
|
||||
)}>
|
||||
{activeTab === 'api' && <CustomCreateCard onRefreshData={refetch} />}
|
||||
{filteredCollectionList.map(collection => (
|
||||
<div
|
||||
key={collection.id}
|
||||
onClick={() => setCurrentProvider(collection)}
|
||||
>
|
||||
<Card
|
||||
className={cn(
|
||||
'border-[1.5px] border-transparent cursor-pointer',
|
||||
currentProvider?.id === collection.id && 'border-components-option-card-option-selected-border',
|
||||
)}
|
||||
hideCornerMark
|
||||
payload={{
|
||||
...collection,
|
||||
brief: collection.description,
|
||||
org: collection.plugin_id ? collection.plugin_id.split('/')[0] : '',
|
||||
name: collection.plugin_id ? collection.plugin_id.split('/')[1] : collection.name,
|
||||
} as any}
|
||||
footer={
|
||||
<CardMoreInfo
|
||||
tags={collection.labels}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty /></div>}
|
||||
</div>
|
||||
)}
|
||||
{!filteredCollectionList.length && activeTab === 'builtin' && (
|
||||
<Empty lightCard text={t('tools.noTools')} className='px-12 h-[224px]' />
|
||||
)}
|
||||
{
|
||||
enable_marketplace && activeTab === 'builtin' && (
|
||||
<Marketplace
|
||||
onMarketplaceScroll={() => {
|
||||
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
|
||||
}}
|
||||
searchPluginText={keywords}
|
||||
filterPluginTags={tagFilterValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</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)}><RiCloseLine className='w-4 h-4' /></div>
|
||||
</div>
|
||||
{currentProvider && !currentProvider.plugin_id && (
|
||||
<ProviderDetail
|
||||
collection={currentProvider}
|
||||
onHide={() => setCurrentProvider(undefined)}
|
||||
onRefreshData={refetch}
|
||||
/>
|
||||
)}
|
||||
<PluginDetailPanel
|
||||
detail={currentPluginDetail}
|
||||
onUpdate={() => invalidateInstalledPluginList()}
|
||||
onHide={() => setCurrentProvider(undefined)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
ProviderList.displayName = 'ToolProviderList'
|
||||
|
||||
@@ -45,16 +45,16 @@ const Contribute = ({ onRefreshData }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{isCurrentWorkspaceManager && (
|
||||
<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='flex flex-col col-span-1 bg-components-panel-on-panel-item-bg border-[0.5px] border-divider-subtle rounded-xl min-h-[135px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg'>
|
||||
<div className='group grow rounded-t-xl hover:bg-background-body' onClick={() => setIsShowEditCustomCollectionModal(true)}>
|
||||
<div className='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'>
|
||||
<RiAddLine className='w-4 h-4 text-gray-500 group-hover:text-primary-600'/>
|
||||
<div className='w-10 h-10 flex items-center justify-center border border-components-option-card-option-border bg-components-option-card-option-bg rounded-lg group-hover:border-components-option-card-option-border-hover group-hover:bg-components-option-card-option-bg-hover'>
|
||||
<RiAddLine className='w-4 h-4 text-text-tertiary group-hover:text-text-accent'/>
|
||||
</div>
|
||||
<div className='ml-3 text-sm font-semibold leading-5 text-gray-800 group-hover:text-primary-600'>{t('tools.createCustomTool')}</div>
|
||||
<div className='ml-3 text-sm font-semibold leading-5 text-text-primary group-hover:text-text-accent'>{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'>
|
||||
<div className='px-4 py-3 rounded-b-xl border-t-[0.5px] border-divider-regular text-text-tertiary hover:text-text-accent hover:bg-background-body'>
|
||||
<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>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
import { AuthHeaderPrefix, AuthType, CollectionType } from '../types'
|
||||
import type { Collection, CustomCollectionBackend, Tool, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '../types'
|
||||
import ToolItem from './tool-item'
|
||||
@@ -9,14 +12,20 @@ import cn from '@/utils/classnames'
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
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 Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import Title from '@/app/components/plugins/card/base/title'
|
||||
import OrgInfo from '@/app/components/plugins/card/base/org-info'
|
||||
import Description from '@/app/components/plugins/card/base/description'
|
||||
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 Drawer from '@/app/components/base/drawer'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
import {
|
||||
deleteWorkflowTool,
|
||||
fetchBuiltInToolList,
|
||||
@@ -35,14 +44,17 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useInvalidateAllWorkflowTools } from '@/service/use-tools'
|
||||
|
||||
type Props = {
|
||||
collection: Collection
|
||||
onHide: () => void
|
||||
onRefreshData: () => void
|
||||
}
|
||||
|
||||
const ProviderDetail = ({
|
||||
collection,
|
||||
onHide,
|
||||
onRefreshData,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -54,7 +66,7 @@ const ProviderDetail = ({
|
||||
const isBuiltIn = collection.type === CollectionType.builtIn
|
||||
const isModel = collection.type === CollectionType.model
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools()
|
||||
const [isDetailLoading, setIsDetailLoading] = useState(false)
|
||||
|
||||
// built in provider
|
||||
@@ -153,6 +165,7 @@ const ProviderDetail = ({
|
||||
workflow_tool_id: string
|
||||
}>) => {
|
||||
await saveWorkflowToolProvider(data)
|
||||
invalidateAllWorkflowTools()
|
||||
onRefreshData()
|
||||
getWorkflowToolProvider()
|
||||
Toast.notify({
|
||||
@@ -213,164 +226,203 @@ const ProviderDetail = ({
|
||||
}, [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
|
||||
variant={isAuthed ? 'secondary' : 'primary'}
|
||||
className={cn('shrink-0 my-3 w-full', isAuthed && 'bg-white')}
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
{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')}
|
||||
<Drawer
|
||||
isOpen={!!collection}
|
||||
clickOutsideNotOpen={false}
|
||||
onClose={onHide}
|
||||
footer={null}
|
||||
mask={false}
|
||||
positionCenter={false}
|
||||
panelClassname={cn('justify-start mt-[64px] mr-2 mb-2 !w-[420px] !max-w-[420px] !p-0 !bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-xl')}
|
||||
>
|
||||
<div className='p-4'>
|
||||
<div className='mb-3 flex'>
|
||||
<Icon src={collection.icon} />
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="flex items-center h-5">
|
||||
<Title title={collection.label[language]} />
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
{collection.type === CollectionType.custom && !isDetailLoading && (
|
||||
<Button
|
||||
className={cn('shrink-0 my-3 w-full')}
|
||||
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
|
||||
variant='primary'
|
||||
className={cn('shrink-0 my-3 w-[183px]')}
|
||||
>
|
||||
<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]')}
|
||||
onClick={() => setIsShowEditWorkflowToolModal(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<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}
|
||||
<div className='mb-1 flex justify-between items-center h-4'>
|
||||
<OrgInfo
|
||||
className="mt-0.5"
|
||||
packageNameClassName='w-auto'
|
||||
orgName={collection.author}
|
||||
packageName={collection.name}
|
||||
/>
|
||||
))}
|
||||
{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>
|
||||
<div className='flex gap-1'>
|
||||
<ActionButton onClick={onHide}>
|
||||
<RiCloseLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
{!!collection.description[language] && (
|
||||
<Description text={collection.description[language]} descriptionLineRows={2}></Description>
|
||||
)}
|
||||
<div className='flex gap-1 border-b-[0.5px] border-divider-subtle'>
|
||||
{collection.type === CollectionType.custom && !isDetailLoading && (
|
||||
<Button
|
||||
className={cn('shrink-0 my-3 w-full')}
|
||||
onClick={() => setIsShowEditCustomCollectionModal(true)}
|
||||
>
|
||||
<Settings01 className='mr-1 w-4 h-4 text-text-tertiary' />
|
||||
<div className='system-sm-medium text-text-secondary'>{t('tools.createTool.editAction')}</div>
|
||||
</Button>
|
||||
)}
|
||||
{collection.type === CollectionType.workflow && !isDetailLoading && customCollection && (
|
||||
<>
|
||||
<Button
|
||||
variant='primary'
|
||||
className={cn('shrink-0 my-3 w-[183px]')}
|
||||
>
|
||||
<a className='flex items-center text-text-primary' href={`/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel='noreferrer' target='_blank'>
|
||||
<div className='system-sm-medium'>{t('tools.openInStudio')}</div>
|
||||
<LinkExternal02 className='ml-1 w-4 h-4' />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('shrink-0 my-3 w-[183px]')}
|
||||
onClick={() => setIsShowEditWorkflowToolModal(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<div className='system-sm-medium text-text-secondary'>{t('tools.createTool.editAction')}</div>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Tools */}
|
||||
<div className='pt-3'>
|
||||
{isDetailLoading && <div className='flex h-[200px]'><Loading type='app' /></div>}
|
||||
{/* Builtin type */}
|
||||
{!isDetailLoading && (collection.type === CollectionType.builtIn) && isAuthed && (
|
||||
<div className='mb-1 h-6 flex items-center justify-between text-text-secondary system-sm-semibold-uppercase'>
|
||||
{t('plugin.detailPanel.actionNum', { num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' })}
|
||||
{needAuth && (
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className='mr-2' color={'green'} />
|
||||
{t('tools.auth.authorized')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isDetailLoading && (collection.type === CollectionType.builtIn) && needAuth && !isAuthed && (
|
||||
<>
|
||||
<div className='text-text-secondary system-sm-semibold-uppercase'>
|
||||
<span className=''>{t('tools.includeToolNum', { num: toolList.length, action: toolList.length > 1 ? 'actions' : 'action' }).toLocaleUpperCase()}</span>
|
||||
<span className='px-1'>·</span>
|
||||
<span className='text-util-colors-orange-orange-600'>{t('tools.auth.setup').toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant='primary'
|
||||
className={cn('shrink-0 my-3 w-full')}
|
||||
onClick={() => {
|
||||
if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
|
||||
showSettingAuthModal()
|
||||
}}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('tools.auth.unauthorized')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{/* Custom type */}
|
||||
{!isDetailLoading && (collection.type === CollectionType.custom) && (
|
||||
<div className='text-text-secondary system-sm-semibold-uppercase'>
|
||||
<span className=''>{t('tools.includeToolNum', { num: toolList.length }).toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Workflow type */}
|
||||
{!isDetailLoading && (collection.type === CollectionType.workflow) && (
|
||||
<div className='text-text-secondary system-sm-semibold-uppercase'>
|
||||
<span className=''>{t('tools.createTool.toolInput.title').toLocaleUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
{!isDetailLoading && (
|
||||
<div className='mt-1 py-2'>
|
||||
{collection.type !== CollectionType.workflow && toolList.map(tool => (
|
||||
<ToolItem
|
||||
key={tool.name}
|
||||
disabled={false}
|
||||
// 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-1 py-1'>
|
||||
<div className='mb-1 flex items-center gap-2'>
|
||||
<span className='text-text-secondary code-sm-semibold'>{item.name}</span>
|
||||
<span className='text-text-tertiary system-xs-regular'>{item.type}</span>
|
||||
<span className='text-text-warning-secondary system-xs-medium'>{item.required ? t('tools.createTool.toolInput.required') : ''}</span>
|
||||
</div>
|
||||
<div className='text-text-tertiary system-xs-regular'>{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={onClickCustomToolDelete}
|
||||
/>
|
||||
)}
|
||||
{isShowEditWorkflowToolModal && (
|
||||
<WorkflowToolModal
|
||||
payload={customCollection}
|
||||
onHide={() => setIsShowEditWorkflowToolModal(false)}
|
||||
onRemove={onClickWorkflowToolDelete}
|
||||
onSave={updateWorkflowToolProvider}
|
||||
/>
|
||||
)}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('tools.createTool.deleteToolConfirmTitle')}
|
||||
content={t('tools.createTool.deleteToolConfirmContent')}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
</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={onClickCustomToolDelete}
|
||||
/>
|
||||
)}
|
||||
{isShowEditWorkflowToolModal && (
|
||||
<WorkflowToolModal
|
||||
payload={customCollection}
|
||||
onHide={() => setIsShowEditWorkflowToolModal(false)}
|
||||
onRemove={onClickWorkflowToolDelete}
|
||||
onSave={updateWorkflowToolProvider}
|
||||
/>
|
||||
)}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('tools.createTool.deleteToolConfirmTitle')}
|
||||
content={t('tools.createTool.deleteToolConfirmContent')}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
export default ProviderDetail
|
||||
|
||||
@@ -29,14 +29,15 @@ const ToolItem = ({
|
||||
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')}
|
||||
className={cn('mb-2 px-4 py-3 bg-components-panel-item-bg rounded-xl border-[0.5px] border-components-panel-border-subtle shadow-xs cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover', 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 className='pb-0.5 text-text-secondary system-md-semibold'>{tool.label[language]}</div>
|
||||
<div className='text-text-tertiary system-xs-regular line-clamp-2' title={tool.description[language]}>{tool.description[language]}</div>
|
||||
</div>
|
||||
{showDetail && (
|
||||
<SettingBuiltInTool
|
||||
showBackButton
|
||||
collection={collection}
|
||||
toolName={tool.name}
|
||||
readonly
|
||||
|
||||
@@ -20,6 +20,7 @@ type Props = {
|
||||
onSaved: (value: Record<string, any>) => void
|
||||
isHideRemoveBtn?: boolean
|
||||
onRemove?: () => void
|
||||
isSaving?: boolean
|
||||
}
|
||||
|
||||
const ConfigCredential: FC<Props> = ({
|
||||
@@ -28,12 +29,14 @@ const ConfigCredential: FC<Props> = ({
|
||||
onSaved,
|
||||
isHideRemoveBtn,
|
||||
onRemove = () => { },
|
||||
isSaving,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const [credentialSchema, setCredentialSchema] = useState<any>(null)
|
||||
const { name: collectionName } = collection
|
||||
const [tempCredential, setTempCredential] = React.useState<any>({})
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
useEffect(() => {
|
||||
fetchBuiltInToolCredentialSchema(collectionName).then(async (res) => {
|
||||
const toolCredentialSchemas = toolCredentialToFormSchemas(res)
|
||||
@@ -45,14 +48,21 @@ const ConfigCredential: FC<Props> = ({
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleSave = () => {
|
||||
const handleSave = async () => {
|
||||
for (const field of credentialSchema) {
|
||||
if (field.required && !tempCredential[field.name]) {
|
||||
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: field.label[language] || field.label.en_US }) })
|
||||
return
|
||||
}
|
||||
}
|
||||
onSaved(tempCredential)
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await onSaved(tempCredential)
|
||||
setIsLoading(false)
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -61,11 +71,11 @@ const ConfigCredential: FC<Props> = ({
|
||||
onHide={onCancel}
|
||||
title={t('tools.auth.setupModalTitle') as string}
|
||||
titleDescription={t('tools.auth.setupModalTitleDescription') as string}
|
||||
panelClassName='mt-2 !w-[405px]'
|
||||
maxWidthClassName='!max-w-[405px]'
|
||||
height='calc(100vh - 16px)'
|
||||
contentClassName='!bg-gray-100'
|
||||
headerClassName='!border-b-black/5'
|
||||
panelClassName='mt-[64px] mb-2 !w-[420px] border-components-panel-border'
|
||||
maxWidthClassName='!max-w-[420px]'
|
||||
height='calc(100vh - 64px)'
|
||||
contentClassName='!bg-components-panel-bg'
|
||||
headerClassName='!border-b-divider-subtle'
|
||||
body={
|
||||
|
||||
<div className='px-6 py-3 h-full'>
|
||||
@@ -82,12 +92,12 @@ const ConfigCredential: FC<Props> = ({
|
||||
isEditMode={true}
|
||||
showOnVariableMap={{}}
|
||||
validating={false}
|
||||
inputClassName='!bg-gray-50'
|
||||
inputClassName='!bg-components-input-bg-normal'
|
||||
fieldMoreInfo={item => item.url
|
||||
? (<a
|
||||
href={item.url}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='inline-flex items-center text-xs text-primary-600'
|
||||
className='inline-flex items-center text-xs text-text-accent'
|
||||
>
|
||||
{t('tools.howToGet')}
|
||||
<LinkExternal02 className='ml-1 w-3 h-3' />
|
||||
@@ -102,7 +112,7 @@ const ConfigCredential: FC<Props> = ({
|
||||
}
|
||||
< div className='flex space-x-2'>
|
||||
<Button onClick={onCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
<Button loading={isLoading || isSaving} disabled={isLoading || isSaving} variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TypeWithI18N } from '../header/account-setting/model-provider-page/declarations'
|
||||
|
||||
export enum LOC {
|
||||
tools = 'tools',
|
||||
app = 'app',
|
||||
@@ -16,10 +17,10 @@ export enum AuthHeaderPrefix {
|
||||
}
|
||||
|
||||
export type Credential = {
|
||||
'auth_type': AuthType
|
||||
'api_key_header'?: string
|
||||
'api_key_value'?: string
|
||||
'api_key_header_prefix'?: AuthHeaderPrefix
|
||||
auth_type: AuthType
|
||||
api_key_header?: string
|
||||
api_key_value?: string
|
||||
api_key_header_prefix?: AuthHeaderPrefix
|
||||
}
|
||||
|
||||
export enum CollectionType {
|
||||
@@ -47,6 +48,8 @@ export type Collection = {
|
||||
is_team_authorization: boolean
|
||||
allow_delete: boolean
|
||||
labels: string[]
|
||||
plugin_id?: string
|
||||
letter?: string
|
||||
}
|
||||
|
||||
export type ToolParameter = {
|
||||
@@ -66,6 +69,7 @@ export type ToolParameter = {
|
||||
max?: number
|
||||
}
|
||||
|
||||
// Action
|
||||
export type Tool = {
|
||||
name: string
|
||||
author: string
|
||||
@@ -73,12 +77,13 @@ export type Tool = {
|
||||
description: any
|
||||
parameters: ToolParameter[]
|
||||
labels: string[]
|
||||
output_schema: Record<string, any>
|
||||
}
|
||||
|
||||
export type ToolCredential = {
|
||||
name: string
|
||||
label: TypeWithI18N
|
||||
help: TypeWithI18N
|
||||
help: TypeWithI18N | null
|
||||
placeholder: TypeWithI18N
|
||||
type: string
|
||||
required: boolean
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ToolCredential, ToolParameter } from '../types'
|
||||
const toType = (type: string) => {
|
||||
export const toType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return 'text-input'
|
||||
@@ -63,3 +63,34 @@ export const addDefaultValue = (value: Record<string, any>, formSchemas: { varia
|
||||
})
|
||||
return newValues
|
||||
}
|
||||
|
||||
export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[], isReasoning = false) => {
|
||||
const newValues = {} as any
|
||||
formSchemas.forEach((formSchema) => {
|
||||
const itemValue = value[formSchema.variable]
|
||||
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
|
||||
newValues[formSchema.variable] = {
|
||||
...(isReasoning ? { value: null, auto: 1 } : { value: formSchema.default }),
|
||||
}
|
||||
}
|
||||
})
|
||||
return newValues
|
||||
}
|
||||
|
||||
export const getPlainValue = (value: Record<string, any>) => {
|
||||
const plainValue = { ...value } as any
|
||||
Object.keys(plainValue).forEach((key) => {
|
||||
plainValue[key] = value[key].value
|
||||
})
|
||||
return plainValue
|
||||
}
|
||||
|
||||
export const getStructureValue = (value: Record<string, any>) => {
|
||||
const newValue = { ...value } as any
|
||||
Object.keys(newValue).forEach((key) => {
|
||||
newValue[key] = {
|
||||
value: value[key],
|
||||
}
|
||||
})
|
||||
return newValue
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createWorkflowToolProvider, fetchWorkflowToolDetailByAppID, saveWorkflo
|
||||
import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useInvalidateAllWorkflowTools } from '@/service/use-tools'
|
||||
|
||||
type Props = {
|
||||
disabled: boolean
|
||||
@@ -46,6 +47,7 @@ const WorkflowToolConfigureButton = ({
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [detail, setDetail] = useState<WorkflowToolProviderResponse>()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools()
|
||||
|
||||
const outdated = useMemo(() => {
|
||||
if (!detail)
|
||||
@@ -135,6 +137,7 @@ const WorkflowToolConfigureButton = ({
|
||||
const createHandle = async (data: WorkflowToolProviderRequest & { workflow_app_id: string }) => {
|
||||
try {
|
||||
await createWorkflowToolProvider(data)
|
||||
invalidateAllWorkflowTools()
|
||||
onRefreshData?.()
|
||||
getDetail(workflowAppId)
|
||||
Toast.notify({
|
||||
@@ -156,6 +159,7 @@ const WorkflowToolConfigureButton = ({
|
||||
await handlePublish()
|
||||
await saveWorkflowToolProvider(data)
|
||||
onRefreshData?.()
|
||||
invalidateAllWorkflowTools()
|
||||
getDetail(workflowAppId)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
@@ -170,35 +174,35 @@ const WorkflowToolConfigureButton = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mt-2 pt-2 border-t-[0.5px] border-t-black/5'>
|
||||
<div className='mt-2 pt-2 border-t-[0.5px] border-divider-regular'>
|
||||
{(!published || !isLoading) && (
|
||||
<div className={cn(
|
||||
'group bg-gray-100 rounded-lg transition-colors',
|
||||
'group bg-background-section-burn rounded-lg transition-colors',
|
||||
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'cursor-pointer',
|
||||
!published && 'hover:bg-primary-50',
|
||||
)}>
|
||||
{isCurrentWorkspaceManager
|
||||
? (
|
||||
<div
|
||||
className='flex justify-start items-center gap-2 px-2.5 py-2'
|
||||
className='flex justify-start items-center text-text-primary 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>
|
||||
<span className='shrink-0 px-1 border border-divider-regular rounded-[5px] bg-background-default-subtle text-[10px] font-medium leading-[18px] text-text-tertiary'>{t('workflow.common.configureRequired').toLocaleUpperCase()}</span>
|
||||
)}
|
||||
</div>)
|
||||
: (
|
||||
<div
|
||||
className='flex justify-start items-center gap-2 px-2.5 py-2'
|
||||
>
|
||||
<Tools className='w-4 h-4 text-gray-500' />
|
||||
<div title={t('workflow.common.workflowAsTool') || ''} className='grow shrink basis-0 text-[13px] font-medium leading-[18px] truncate text-gray-500'>{t('workflow.common.workflowAsTool')}</div>
|
||||
<Tools className='w-4 h-4 text-text-tertiary' />
|
||||
<div title={t('workflow.common.workflowAsTool') || ''} className='grow shrink basis-0 text-[13px] font-medium leading-[18px] truncate text-text-tertiary'>{t('workflow.common.workflowAsTool')}</div>
|
||||
</div>
|
||||
)}
|
||||
{published && (
|
||||
<div className='px-2.5 py-2 border-t-[0.5px] border-black/5'>
|
||||
<div className='px-2.5 py-2 border-t-[0.5px] border-divider-regular'>
|
||||
<div className='flex justify-between'>
|
||||
<Button
|
||||
size='small'
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
@@ -19,24 +18,24 @@ const ConfirmModal = ({ show, onConfirm, onClose }: ConfirmModalProps) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={cn('p-8 max-w-[600px] w-[600px]', s.bg)}
|
||||
className={cn('p-8 max-w-[600px] w-[600px]')}
|
||||
isShow={show}
|
||||
onClose={() => { }}
|
||||
>
|
||||
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
|
||||
<RiCloseLine className='w-4 h-4 text-gray-500' />
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='w-12 h-12 p-3 bg-white rounded-xl border-[0.5px] border-gray-100 shadow-xl'>
|
||||
<div className='w-12 h-12 p-3 bg-background-section rounded-xl border-[0.5px] border-divider-regular 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'>
|
||||
<div className='relative mt-3 text-xl font-semibold leading-[30px] text-text-primary'>{t('tools.createTool.confirmTitle')}</div>
|
||||
<div className='my-1 text-text-tertiary 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' onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='border-red-700' variant="warning" onClick={onConfirm}>{t('common.operation.confirm')}</Button>
|
||||
<Button variant="warning" onClick={onConfirm}>{t('common.operation.confirm')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.bg {
|
||||
background: linear-gradient(180deg, rgba(247, 144, 9, 0.05) 0%, rgba(247, 144, 9, 0.00) 24.41%), #F9FAFB;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ const WorkflowToolAsModal: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState<Boolean>(false)
|
||||
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)
|
||||
@@ -124,13 +124,13 @@ const WorkflowToolAsModal: FC<Props> = ({
|
||||
panelClassName='mt-2 !w-[640px]'
|
||||
maxWidthClassName='!max-w-[640px]'
|
||||
height='calc(100vh - 16px)'
|
||||
headerClassName='!border-b-black/5'
|
||||
headerClassName='!border-b-divider'
|
||||
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')} <span className='ml-1 text-red-500'>*</span></div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></div>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' iconType='emoji' icon={emoji.content} background={emoji.background} />
|
||||
<Input
|
||||
@@ -143,7 +143,7 @@ const WorkflowToolAsModal: FC<Props> = ({
|
||||
</div>
|
||||
{/* name for tool call */}
|
||||
<div>
|
||||
<div className='flex items-center py-2 leading-5 text-sm font-medium text-gray-900'>
|
||||
<div className='flex items-center py-2 system-sm-medium text-text-primary'>
|
||||
{t('tools.createTool.nameForToolCall')} <span className='ml-1 text-red-500'>*</span>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
@@ -165,7 +165,7 @@ const WorkflowToolAsModal: FC<Props> = ({
|
||||
</div>
|
||||
{/* description */}
|
||||
<div>
|
||||
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.description')}</div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.description')}</div>
|
||||
<Textarea
|
||||
placeholder={t('tools.createTool.descriptionPlaceholder') || ''}
|
||||
value={description}
|
||||
@@ -174,11 +174,11 @@ const WorkflowToolAsModal: FC<Props> = ({
|
||||
</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'>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.toolInput.title')}</div>
|
||||
<div className='rounded-lg border border-divider-regular w-full overflow-x-auto'>
|
||||
<table className='w-full leading-[18px] text-xs text-text-secondary font-normal'>
|
||||
<thead className='text-text-tertiary uppercase'>
|
||||
<tr className='border-b border-divider-regular'>
|
||||
<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>
|
||||
@@ -186,22 +186,22 @@ const WorkflowToolAsModal: FC<Props> = ({
|
||||
</thead>
|
||||
<tbody>
|
||||
{parameters.map((item, index) => (
|
||||
<tr key={index} className='border-b last:border-0 border-gray-200'>
|
||||
<tr key={index} className='border-b last:border-0 border-divider-regular'>
|
||||
<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='font-medium text-text-primary 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 className='text-text-tertiary'>{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',
|
||||
'flex items-center gap-1 min-h-[56px] px-3 py-2 h-9 bg-transparent cursor-default',
|
||||
)}>
|
||||
<div className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate')}>
|
||||
<div className={cn('grow text-[13px] leading-[18px] text-text-secondary truncate')}>
|
||||
{t('tools.createTool.toolInput.methodParameter')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,10 +210,10 @@ const WorkflowToolAsModal: FC<Props> = ({
|
||||
<MethodSelector value={item.form} onChange={value => handleParameterChange('form', value, index)} />
|
||||
)}
|
||||
</td>
|
||||
<td className="p-2 pl-3 text-gray-500 w-[236px]">
|
||||
<td className="p-2 pl-3 text-text-tertiary 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'
|
||||
className='w-full text-text-secondary text-[13px] leading-[18px] font-normal bg-transparent outline-none appearance-none caret-primary-600 placeholder:text-text-quaternary'
|
||||
placeholder={t('tools.createTool.toolInput.descriptionPlaceholder')!}
|
||||
value={item.description}
|
||||
onChange={e => handleParameterChange('description', e.target.value, index)}
|
||||
@@ -227,12 +227,12 @@ const WorkflowToolAsModal: FC<Props> = ({
|
||||
</div>
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{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>
|
||||
<div className='py-2 system-sm-medium text-text-primary'>{t('tools.createTool.privacyPolicy')}</div>
|
||||
<Input
|
||||
className='h-10'
|
||||
value={privacyPolicy}
|
||||
@@ -240,9 +240,9 @@ const WorkflowToolAsModal: FC<Props> = ({
|
||||
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')} >
|
||||
<div className={cn((!isAdd && onRemove) ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-background-section-burn border-t border-divider-regular')} >
|
||||
{!isAdd && onRemove && (
|
||||
<Button onClick={onRemove} className='text-red-500 border-red-50 hover:border-red-500'>{t('common.operation.delete')}</Button>
|
||||
<Button variant='warning' onClick={onRemove}>{t('common.operation.delete')}</Button>
|
||||
)}
|
||||
<div className='flex space-x-2 '>
|
||||
<Button onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
|
||||
@@ -34,37 +34,37 @@ const MethodSelector: FC<MethodSelectorProps> = ({
|
||||
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',
|
||||
'flex items-center gap-1 min-h-[56px] px-3 py-2 h-9 bg-transparent cursor-pointer hover:bg-background-section-burn',
|
||||
open && '!bg-background-section-burn hover:bg-background-section-burn',
|
||||
)}>
|
||||
<div className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate')}>
|
||||
<div className={cn('grow text-[13px] leading-[18px] text-text-secondary 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'>
|
||||
<div className='shrink-0 ml-1 text-text-secondary opacity-60'>
|
||||
<RiArrowDownSLine 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='relative w-[320px] bg-components-panel-bg-blur backdrop-blur-sm rounded-lg border-[0.5px] border-components-panel-border 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='pl-3 pr-2 py-2.5 rounded-lg hover:bg-components-panel-on-panel-item-bg-hover 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' />}
|
||||
{value === 'llm' && <Check className='shrink-0 w-4 h-4 text-text-accent' />}
|
||||
</div>
|
||||
<div className='text-[13px] text-gray-700 font-medium leading-[18px]'>{t('tools.createTool.toolInput.methodParameter')}</div>
|
||||
<div className='text-[13px] text-text-secondary 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 className='pl-5 text-text-tertiary 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='pl-3 pr-2 py-2.5 rounded-lg hover:bg-components-panel-on-panel-item-bg-hover 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' />}
|
||||
{value === 'form' && <Check className='shrink-0 w-4 h-4 text-text-accent' />}
|
||||
</div>
|
||||
<div className='text-[13px] text-gray-700 font-medium leading-[18px]'>{t('tools.createTool.toolInput.methodSetting')}</div>
|
||||
<div className='text-[13px] text-text-secondary 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 className='pl-5 text-text-tertiary text-[13px] leading-[18px]'>{t('tools.createTool.toolInput.methodSettingTip')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user