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:
Yeuoly
2025-02-17 17:05:13 +08:00
committed by GitHub
parent 222df44d21
commit 403e2d58b9
3272 changed files with 66339 additions and 281594 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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,
}
}

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}

View File

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

View File

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