feat: support assistant frontend (#2139)

Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
This commit is contained in:
Joel
2024-01-23 19:31:56 +08:00
committed by GitHub
parent e65a2a400d
commit 7bbe12b2bd
194 changed files with 8726 additions and 1586 deletions

View File

@@ -0,0 +1,31 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Heart02 } from '../base/icons/src/vender/solid/education'
import { BookOpen01 } from '../base/icons/src/vender/line/education'
const Contribute: FC = () => {
const { t } = useTranslation()
return (
<div className='shrink-0 p-2'>
<div className='inline-block p-2 bg-white shadow-lg rounded-lg'>
<Heart02 className='w-3 h-3 text-[#EE46BC]' />
</div>
<div className='mt-2'>
<div className='text-gradient'>
{t('tools.contribute.line1')}
</div>
<div className='text-gradient'>
{t('tools.contribute.line2')}
</div>
</div>
<a href='https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md' target='_blank' className='mt-1 flex items-center space-x-1 text-[#155EEF]'>
<BookOpen01 className='w-3 h-3' />
<div className='leading-[18px] text-xs font-normal'>{t('tools.contribute.viewGuide')}</div>
</a>
</div>
)
}
export default React.memo(Contribute)

View File

@@ -0,0 +1,108 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import type { Credential } from '@/app/components/tools/types'
import Drawer from '@/app/components/base/drawer-plus'
import Button from '@/app/components/base/button'
import Radio from '@/app/components/base/radio/ui'
import { AuthType } from '@/app/components/tools/types'
type Props = {
credential: Credential
onChange: (credential: Credential) => void
onHide: () => void
}
const keyClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
type ItemProps = {
text: string
value: AuthType
isChecked: boolean
onClick: (value: AuthType) => void
}
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')}
onClick={() => onClick(value)}
>
<Radio isChecked={isChecked} />
<div className='text-sm font-normal text-gray-900'>{text}</div>
</div>
)
}
const ConfigCredential: FC<Props> = ({
credential,
onChange,
onHide,
}) => {
const { t } = useTranslation()
const [tempCredential, setTempCredential] = React.useState<Credential>(credential)
return (
<Drawer
isShow
onHide={onHide}
title={t('tools.createTool.authMethod.title')!}
panelClassName='mt-2 !w-[520px]'
maxWidthClassName='!max-w-[520px]'
height='calc(100vh - 16px)'
headerClassName='!border-b-black/5'
body={
<div className='pt-2 px-6'>
<div className='space-y-4'>
<div>
<div className={keyClassNames}>{t('tools.createTool.authMethod.type')}</div>
<div className='flex space-x-3'>
<SelectItem
text={t('tools.createTool.authMethod.types.none')}
value={AuthType.none}
isChecked={tempCredential.auth_type === AuthType.none}
onClick={value => setTempCredential({ ...tempCredential, auth_type: value })}
/>
<SelectItem
text={t('tools.createTool.authMethod.types.api_key')}
value={AuthType.apiKey}
isChecked={tempCredential.auth_type === AuthType.apiKey}
onClick={value => setTempCredential({ ...tempCredential, auth_type: value })}
/>
</div>
</div>
{tempCredential.auth_type === AuthType.apiKey && (
<>
<div>
<div className={keyClassNames}>{t('tools.createTool.authMethod.key')}</div>
<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 bg-gray-100 rounded-lg grow' />
</div>
<div>
<div className={keyClassNames}>{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 bg-gray-100 rounded-lg grow' />
</div>
</>)}
</div>
<div className='mt-4 shrink-0 flex justify-end space-x-2 py-4'>
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium' type='primary' onClick={() => {
onChange(tempCredential)
onHide()
}}>{t('common.operation.save')}</Button>
</div>
</div>
}
/>
)
}
export default React.memo(ConfigCredential)

View File

@@ -0,0 +1,181 @@
const examples = [
{
key: 'json',
content: `{
"openapi": "3.1.0",
"info": {
"title": "Get weather data",
"description": "Retrieves current weather data for a location.",
"version": "v1.0.0"
},
"servers": [
{
"url": "https://weather.example.com"
}
],
"paths": {
"/location": {
"get": {
"description": "Get temperature for a specific location",
"operationId": "GetCurrentWeather",
"parameters": [
{
"name": "location",
"in": "query",
"description": "The city and state to retrieve the weather for",
"required": true,
"schema": {
"type": "string"
}
}
],
"deprecated": false
}
}
},
"components": {
"schemas": {}
}
}`,
},
{
key: 'yaml',
content: `# Taken from https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v3.0/petstore.yaml
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: https://petstore.swagger.io/v1
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
maximum: 100
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
summary: Create a pet
operationId: createPets
tags:
- pets
responses:
'201':
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/pets/{petId}:
get:
summary: Info for a specific pet
operationId: showPetById
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
maxItems: 100
items:
$ref: "#/components/schemas/Pet"
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string`,
},
{
key: 'blankTemplate',
content: `{
"openapi": "3.1.0",
"info": {
"title": "Untitled",
"description": "Your OpenAPI specification",
"version": "v1.0.0"
},
"servers": [
{
"url": ""
}
],
"paths": {},
"components": {
"schemas": {}
}
}`,
},
]
export default examples

View File

@@ -0,0 +1,117 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useClickAway } from 'ahooks'
import Toast from '../../base/toast'
import { Plus } from '../../base/icons/src/vender/line/general'
import { ChevronDown } from '../../base/icons/src/vender/line/arrows'
import examples from './examples'
import Button from '@/app/components/base/button'
import { importSchemaFromURL } from '@/service/tools'
type Props = {
onChange: (value: string) => void
}
const GetSchema: FC<Props> = ({
onChange,
}) => {
const { t } = useTranslation()
const [showImportFromUrl, setShowImportFromUrl] = useState(false)
const [importUrl, setImportUrl] = useState('')
const [isParsing, setIsParsing] = useState(false)
const handleImportFromUrl = async () => {
if (!importUrl.startsWith('http://') && !importUrl.startsWith('https://')) {
Toast.notify({
type: 'error',
message: t('tools.createTool.urlError'),
})
return
}
setIsParsing(true)
try {
const { schema } = await importSchemaFromURL(importUrl) as any
setImportUrl('')
onChange(schema)
}
finally {
setIsParsing(false)
setShowImportFromUrl(false)
}
}
const importURLRef = React.useRef(null)
useClickAway(() => {
setShowImportFromUrl(false)
}, importURLRef)
const [showExamples, setShowExamples] = useState(false)
const showExamplesRef = React.useRef(null)
useClickAway(() => {
setShowExamples(false)
}, showExamplesRef)
return (
<div className='flex space-x-1 justify-end relative w-[224px]'>
<div ref={importURLRef}>
<Button
className='flex items-center !h-6 !px-2 space-x-1 '
onClick={() => { setShowImportFromUrl(!showImportFromUrl) }}
>
<Plus className='w-3 h-3' />
<div className='text-xs font-medium text-gray-700'>{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='relative'>
<input
type='text'
className='w-[244px] h-8 pl-1.5 pr-[44px] overflow-x-auto border border-gray-200 rounded-lg text-[13px]'
placeholder={t('tools.createTool.importFromUrlPlaceHolder')!}
value={importUrl}
onChange={e => setImportUrl(e.target.value)}
/>
<Button
className='absolute top-1 right-1 !h-6 !px-2 text-xs font-medium'
type='primary'
disabled={!importUrl}
onClick={handleImportFromUrl}
loading={isParsing}
>
{isParsing ? '' : t('common.operation.ok')}
</Button>
</div>
</div>
)}
</div>
<div className='relative' ref={showExamplesRef}>
<Button
className='flex items-center !h-6 !px-2 space-x-1'
onClick={() => { setShowExamples(!showExamples) }}
>
<div className='text-xs font-medium text-gray-700'>{t('tools.createTool.examples')}</div>
<ChevronDown className='w-3 h-3' />
</Button>
{showExamples && (
<div className='absolute top-7 right-0 p-1 rounded-lg bg-white shadow-sm'>
{examples.map(item => (
<div
key={item.key}
onClick={() => {
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'
>
{t(`tools.createTool.exampleOptions.${item.key}`)}
</div>
))}
</div>
)}
</div>
</div>
)
}
export default React.memo(GetSchema)

View File

@@ -0,0 +1,290 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { useDebounce, useGetState } from 'ahooks'
import { clone } from 'lodash-es'
import cn from 'classnames'
import { LinkExternal02, Settings01 } from '../../base/icons/src/vender/line/general'
import type { Credential, CustomCollectionBackend, CustomParamSchema, Emoji } from '../types'
import { AuthType } from '../types'
import GetSchema from './get-schema'
import ConfigCredentials from './config-credentials'
import TestApi from './test-api'
import Drawer from '@/app/components/base/drawer-plus'
import Button from '@/app/components/base/button'
import EmojiPicker from '@/app/components/base/emoji-picker'
import AppIcon from '@/app/components/base/app-icon'
import { parseParamsSchema } from '@/service/tools'
const fieldNameClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
type Props = {
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,
},
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: string) => {
const newCollection = produce(customCollection, (draft) => {
draft.schema = schema
})
setCustomCollection(newCollection)
}
useEffect(() => {
if (!debouncedSchema)
return
if (isEdit && editFirst) {
setEditFirst(false)
return
}
(async () => {
const customCollection = getCustomCollection()
try {
const { parameters_schema, schema_type } = await parseParamsSchema(debouncedSchema) as any
const newCollection = produce(customCollection, (draft) => {
draft.schema_type = schema_type
})
setCustomCollection(newCollection)
setParamsSchemas(parameters_schema)
}
catch (e) {
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 handleSave = () => {
const postData = clone(customCollection)
delete postData.tools
if (isAdd) {
onAdd?.(postData)
return
}
onEdit?.({
...postData,
original_provider: originalProvider,
})
}
return (
<>
<Drawer
isShow
onHide={onHide}
title={t(`tools.createTool.${isAdd ? 'title' : 'editTitle'}`)!}
panelClassName='mt-2 !w-[640px]'
maxWidthClassName='!max-w-[640px]'
height='calc(100vh - 16px)'
headerClassName='!border-b-black/5'
body={
<div className='flex flex-col h-full'>
<div className='grow h-0 overflow-y-auto px-6 py-3 space-y-4'>
<div>
<div className={fieldNameClassNames}>{t('tools.createTool.name')}</div>
<div className='flex items-center justify-between gap-3'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.content} background={emoji.background} />
<input
className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg 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={fieldNameClassNames}>{t('tools.createTool.schema')}</div>
<div className='mx-2 w-px h-3 bg-black/5'></div>
<a
href="https://swagger.io/specification/"
target='_blank'
className='flex items-center h-[18px] space-x-1 text-[#155EEF]'
>
<div className='text-xs font-normal'>{t('tools.createTool.viewSchemaSpec')}</div>
<LinkExternal02 className='w-3 h-3' />
</a>
</div>
<GetSchema onChange={setSchema} />
</div>
<textarea
value={schema}
onChange={e => setSchema(e.target.value)}
className='w-full h-[240px] px-3 py-2 leading-4 text-xs font-normal text-gray-900 bg-gray-100 rounded-lg overflow-y-auto'
placeholder={t('tools.createTool.schemaPlaceHolder')!}
></textarea>
</div>
{/* 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')}>
<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-gray-200'>
<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">{item.method}</td>
<td className="p-2 pl-3">{item.server_url ? new URL(item.server_url).pathname : ''}</td>
<td className="p-2 pl-3 w-[62px]">
<Button
className='!h-6 !px-2 text-xs font-medium text-gray-700'
onClick={() => {
setCurrTool(item)
setIsShowTestApi(true)
}}
>
{t('tools.createTool.availableTools.test')}
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* 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>
</div>
<div>
<div className={fieldNameClassNames}>{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='w-full h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' placeholder={t('tools.createTool.privacyPolicyPlaceholder') || ''} />
</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')} >
{
isEdit && (
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onRemove}>{t('common.operation.remove')}</Button>
)
}
<div className='flex space-x-2 '>
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium' type='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
</div>
</div>
</div>
}
isShowMask={true}
clickOutsideNotOpen={true}
/>
{showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => {
setEmoji({ content: icon, background: icon_background })
setShowEmojiPicker(false)
}}
onClose={() => {
setShowEmojiPicker(false)
}}
/>}
{credentialsModalShow && (
<ConfigCredentials
credential={credential}
onChange={setCredential}
onHide={() => setCredentialsModalShow(false)}
/>)
}
{isShowTestApi && (
<TestApi
tool={currTool as CustomParamSchema}
customCollection={customCollection}
onHide={() => setIsShowTestApi(false)}
/>
)}
</>
)
}
export default React.memo(EditCustomCollectionModal)

View File

@@ -0,0 +1,120 @@
'use client'
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 ConfigCredentials from './config-credentials'
import type { Credential, CustomCollectionBackend, CustomParamSchema } from '@/app/components/tools/types'
import Button from '@/app/components/base/button'
import Drawer from '@/app/components/base/drawer-plus'
import I18n from '@/context/i18n'
import { testAPIAvailable } from '@/service/tools'
type Props = {
customCollection: CustomCollectionBackend
tool: CustomParamSchema
onHide: () => void
}
const keyClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
const TestApi: FC<Props> = ({
customCollection,
tool,
onHide,
}) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const [credentialsModalShow, setCredentialsModalShow] = useState(false)
const [tempCredential, setTempCredential] = React.useState<Credential>(customCollection.credentials)
const [result, setResult] = useState<string>('')
const { operation_id: toolName, parameters } = tool
const [parametersValue, setParametersValue] = useState<Record<string, string>>({})
const handleTest = async () => {
const data = {
tool_name: toolName,
credentials: tempCredential,
schema_type: customCollection.schema_type,
schema: customCollection.schema,
parameters: parametersValue,
}
const res = await testAPIAvailable(data) as any
setResult(res.error || res.result)
}
return (
<>
<Drawer
isShow
onHide={onHide}
title={`${t('tools.test.title')} ${toolName}`}
panelClassName='mt-2 !w-[600px]'
maxWidthClassName='!max-w-[600px]'
height='calc(100vh - 16px)'
headerClassName='!border-b-black/5'
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>
</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'>
<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'>
<td className="py-2 pl-3 pr-2.5">
{item.label[locale === 'en' ? 'en_US' : 'zh_Hans']}
</td>
<td className="">
<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>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
<Button type='primary' className=' mt-4 w-full h-10 !text-[13px] leading-[18px] font-medium' 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='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>
</div>
</div>
}
/>
{credentialsModalShow && (
<ConfigCredentials
credential={tempCredential}
onChange={setTempCredential}
onHide={() => setCredentialsModalShow(false)}
/>)
}
</>
)
}
export default React.memo(TestApi)

View File

@@ -0,0 +1,232 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import Button from '../base/button'
import { Plus } from '../base/icons/src/vender/line/general'
import Toast from '../base/toast'
import type { Collection, CustomCollectionBackend, Tool } from './types'
import { CollectionType, LOC } from './types'
import ToolNavList from './tool-nav-list'
import Search from './search'
import Contribute from './contribute'
import ToolList from './tool-list'
import EditCustomToolModal from './edit-custom-collection-modal'
import NoCustomTool from './info/no-custom-tool'
import NoSearchRes from './info/no-search-res'
import TabSlider from '@/app/components/base/tab-slider'
import { createCustomCollection, fetchCollectionList as doFetchCollectionList, fetchBuiltInToolList, fetchCustomToolList } from '@/service/tools'
import type { AgentTool } from '@/types/app'
type Props = {
loc: LOC
addedTools?: AgentTool[]
onAddTool?: (collection: Collection, payload: Tool) => void
selectedProviderId?: string
}
const Tools: FC<Props> = ({
loc,
addedTools,
onAddTool,
selectedProviderId,
}) => {
const { t } = useTranslation()
const isInToolsPage = loc === LOC.tools
const isInDebugPage = !isInToolsPage
const [collectionList, setCollectionList] = useState<Collection[]>([])
const [currCollectionIndex, setCurrCollectionIndex] = useState<number | null>(null)
const [isDetailLoading, setIsDetailLoading] = useState(false)
const fetchCollectionList = async () => {
const list = await doFetchCollectionList() as Collection[]
setCollectionList(list)
if (list.length > 0 && currCollectionIndex === null) {
let index = 0
if (selectedProviderId)
index = list.findIndex(item => item.id === selectedProviderId)
setCurrCollectionIndex(index || 0)
}
}
useEffect(() => {
fetchCollectionList()
}, [])
const collectionTypeOptions = (() => {
const res = [
{ value: CollectionType.builtIn, text: t('tools.type.builtIn') },
{ value: CollectionType.custom, text: t('tools.type.custom') },
]
if (!isInToolsPage)
res.unshift({ value: CollectionType.all, text: t('tools.type.all') })
return res
})()
const [query, setQuery] = useState('')
const [collectionType, setCollectionType] = useState<CollectionType>(collectionTypeOptions[0].value)
const showCollectionList = (() => {
let typeFilteredList: Collection[] = []
if (collectionType === CollectionType.all)
typeFilteredList = collectionList
else
typeFilteredList = collectionList.filter(item => item.type === collectionType)
if (query)
return typeFilteredList.filter(item => item.name.includes(query))
return typeFilteredList
})()
const hasNoCustomCollection = !collectionList.find(item => item.type === CollectionType.custom)
useEffect(() => {
setCurrCollectionIndex(0)
}, [collectionType])
const currCollection = (() => {
if (currCollectionIndex === null)
return null
return showCollectionList[currCollectionIndex]
})()
const [currTools, setCurrentTools] = useState<Tool[]>([])
useEffect(() => {
if (!currCollection)
return
(async () => {
setIsDetailLoading(true)
try {
if (currCollection.type === CollectionType.builtIn) {
const list = await fetchBuiltInToolList(currCollection.name) as Tool[]
setCurrentTools(list)
}
else {
const list = await fetchCustomToolList(currCollection.name) as Tool[]
setCurrentTools(list)
}
}
catch (e) { }
setIsDetailLoading(false)
})()
}, [currCollection?.name])
const [isShowEditCollectionToolModal, setIsShowEditCollectionToolModal] = useState(false)
const handleCreateToolCollection = () => {
setIsShowEditCollectionToolModal(true)
}
const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => {
await createCustomCollection(data)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
await fetchCollectionList()
setIsShowEditCollectionToolModal(false)
}
return (
<>
<div className='flex h-full'>
{/* sidebar */}
<div className={cn(isInToolsPage ? 'sm:w-[216px] px-4' : 'sm:w-[256px] px-3', 'flex flex-col w-16 shrink-0 pb-2')}>
{isInToolsPage && (
<Button className='mt-6 flex items-center !h-8 pl-4' type='primary' onClick={handleCreateToolCollection}>
<Plus className='w-4 h-4 mr-1' />
<div className='leading-[18px] text-[13px] font-medium'>{t('tools.createCustomTool')}</div>
</Button>
)}
{isInDebugPage && (
<div className='mt-6 flex space-x-1 items-center'>
<Search
className='grow'
value={query}
onChange={setQuery}
/>
<Button className='flex items-center justify-center !w-8 !h-8 !p-0' type='primary'>
<Plus className='w-4 h-4' onClick={handleCreateToolCollection} />
</Button>
</div>
)}
<TabSlider
className='mt-3'
itemWidth={isInToolsPage ? 89 : 75}
value={collectionType}
onChange={v => setCollectionType(v as CollectionType)}
options={collectionTypeOptions}
/>
{isInToolsPage && (
<Search
className='mt-5'
value={query}
onChange={setQuery}
/>
)}
{(collectionType === CollectionType.custom && hasNoCustomCollection)
? (
<div className='grow h-0 p-2 pt-8'>
<NoCustomTool onCreateTool={handleCreateToolCollection} />
</div>
)
: (
(showCollectionList.length > 0 || !query)
? <ToolNavList
className='mt-2 grow height-0 overflow-y-auto'
currentName={currCollection?.name || ''}
list={showCollectionList}
onChosen={setCurrCollectionIndex}
/>
: (
<div className='grow h-0 p-2 pt-8'>
<NoSearchRes
onReset={() => { setQuery('') }}
/>
</div>
)
)}
{loc === LOC.tools && (
<Contribute />
)}
</div>
{/* tools */}
<div className={cn('grow h-full overflow-hidden p-2')}>
<div className='h-full bg-white rounded-2xl'>
{!(collectionType === CollectionType.custom && hasNoCustomCollection) && showCollectionList.length > 0 && (
<ToolList
collection={currCollection}
list={currTools}
loc={loc}
addedTools={addedTools}
onAddTool={onAddTool}
onRefreshData={fetchCollectionList}
onCollectionRemoved={() => {
setCurrCollectionIndex(0)
fetchCollectionList()
}}
isLoading={isDetailLoading}
/>
)}
</div>
</div>
</div>
{isShowEditCollectionToolModal && (
<EditCustomToolModal
payload={null}
onHide={() => setIsShowEditCollectionToolModal(false)}
onAdd={doCreateCustomToolCollection}
/>
)}
</>
)
}
export default React.memo(Tools)

View File

@@ -0,0 +1,38 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Tools } from '@/app/components/base/icons/src/public/header-nav/tools'
type Props = {
onCreateTool: () => void
}
const NoCustomTool: FC<Props> = ({
onCreateTool,
}) => {
const { t } = useTranslation()
return (
<div>
<div className='inline-flex p-3 rounded-lg bg-gray-50 border border-[#EAECF5]'>
<Tools className='w-5 h-5 text-gray-500' />
</div>
<div className='mt-2'>
<div className='leading-5 text-sm font-medium text-gray-500'>
{t('tools.noCustomTool.title')}
</div>
<div className='mt-1 leading-[18px] text-xs font-normal text-gray-500'>
{t('tools.noCustomTool.content')}
</div>
<div
className='mt-2 leading-[18px] text-xs font-medium text-[#155EEF] uppercase cursor-pointer'
onClick={onCreateTool}
>
{t('tools.noCustomTool.createTool')}
</div>
</div>
</div>
)
}
export default React.memo(NoCustomTool)

View File

@@ -0,0 +1,38 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { SearchMd } from '../../base/icons/src/vender/solid/general'
type Props = {
onReset: () => void
}
const NoSearchRes: FC<Props> = ({
onReset,
}) => {
const { t } = useTranslation()
return (
<div>
<div className='inline-flex p-3 rounded-lg bg-gray-50 border border-[#EAECF5]'>
<SearchMd className='w-5 h-5 text-gray-500' />
</div>
<div className='mt-2'>
<div className='leading-5 text-sm font-medium text-gray-500'>
{t('tools.noSearchRes.title')}
</div>
<div className='mt-1 leading-[18px] text-xs font-normal text-gray-500'>
{t('tools.noSearchRes.content')}
</div>
<div
className='mt-2 leading-[18px] text-xs font-medium text-[#155EEF] uppercase cursor-pointer'
onClick={onReset}
>
{t('tools.noSearchRes.reset')}
</div>
</div>
</div>
)
}
export default React.memo(NoSearchRes)

View File

@@ -0,0 +1,41 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from 'classnames'
import {
MagnifyingGlassIcon,
} from '@heroicons/react/24/solid'
import { useTranslation } from 'react-i18next'
type Props = {
className?: string
value: string
onChange: (v: string) => void
}
const Search: FC<Props> = ({
className,
value,
onChange,
}) => {
const { t } = useTranslation()
return (
<div className={cn(className, 'flex relative')}>
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="query"
className="block w-0 grow bg-gray-200 shadow-sm rounded-md border-0 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-8"
placeholder={t('common.operation.search')!}
value={value}
onChange={(e) => {
onChange(e.target.value)
}}
/>
</div>
)
}
export default React.memo(Search)

View File

@@ -0,0 +1,88 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { toolCredentialToFormSchemas } from '../../utils/to-form-schema'
import type { Collection } from '../../types'
import Drawer from '@/app/components/base/drawer-plus'
import Button from '@/app/components/base/button'
import { fetchBuiltInToolCredentialSchema } from '@/service/tools'
import Loading from '@/app/components/base/loading'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
type Props = {
collection: Collection
onCancel: () => void
onSaved: (value: Record<string, any>) => void
onRemove: () => void
}
const ConfigCredential: FC<Props> = ({
collection,
onCancel,
onSaved,
onRemove,
}) => {
const { t } = useTranslation()
const [credentialSchema, setCredentialSchema] = useState<any>(null)
const { team_credentials: credentialValue, name: collectionName } = collection
useEffect(() => {
fetchBuiltInToolCredentialSchema(collectionName).then((res) => {
setCredentialSchema(toolCredentialToFormSchemas(res as any))
})
}, [])
const [tempCredential, setTempCredential] = React.useState<any>(credentialValue)
return (
<Drawer
isShow
onHide={onCancel}
title={t('tools.auth.setupModalTitle') as string}
titleDescription={t('tools.auth.setupModalTitleDescription') as string}
panelClassName='mt-2 !w-[480px]'
maxWidthClassName='!max-w-[480px]'
height='calc(100vh - 16px)'
contentClassName='!bg-gray-100'
headerClassName='!border-b-black/5'
body={
<div className='px-6 py-3 h-full'>
{!credentialSchema
? <Loading type='app' />
: (
<>
<Form
value={tempCredential}
onChange={(v) => {
setTempCredential(v)
}}
formSchemas={credentialSchema}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='!bg-gray-50'
/>
<div className={cn(collection.is_team_authorization ? 'justify-between' : 'justify-end', 'mt-2 flex ')} >
{
collection.is_team_authorization && (
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onRemove}>{t('common.operation.remove')}</Button>
)
}
< div className='flex space-x-2'>
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onCancel}>{t('common.operation.cancel')}</Button>
<Button className='flex items-center h-8 !px-3 !text-[13px] font-medium' type='primary' onClick={() => onSaved(tempCredential)}>{t('common.operation.save')}</Button>
</div>
</div>
</>
)
}
</div >
}
isShowMask={true}
clickOutsideNotOpen={false}
/>
)
}
export default React.memo(ConfigCredential)

View File

@@ -0,0 +1,74 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import type { Collection } from '../types'
import { CollectionType, LOC } from '../types'
import { Settings01 } from '../../base/icons/src/vender/line/general'
import I18n from '@/context/i18n'
type Props = {
icon: JSX.Element
collection: Collection
loc: LOC
onShowAuth: () => void
onShowEditCustomCollection: () => void
}
const Header: FC<Props> = ({
icon,
collection,
loc,
onShowAuth,
onShowEditCustomCollection,
}) => {
const { locale } = useContext(I18n)
const { t } = useTranslation()
const isInToolsPage = loc === LOC.tools
const isInDebugPage = !isInToolsPage
const needAuth = collection?.allow_delete
// const isBuiltIn = collection.type === CollectionType.builtIn
const isAuthed = collection.is_team_authorization
return (
<div className={cn(isInToolsPage ? 'py-4 px-6' : 'py-[11px] pl-4 pr-3', 'flex justify-between items-start border-b border-gray-200')}>
<div className='flex items-start w-full'>
{icon}
<div className='ml-3 grow w-0'>
<div className='flex items-center h-6 space-x-1'>
<div className={cn(isInDebugPage && 'truncate', 'text-base font-semibold text-gray-900')}>{collection.label[locale === 'en' ? 'en_US' : 'zh_Hans']}</div>
<div className='text-xs font-normal text-gray-500'>·</div>
<div className='text-xs font-normal text-gray-500'>{t('tools.author')}&nbsp;{collection.author}</div>
</div>
{collection.description && (
<div className={cn('leading-[18px] text-[13px] font-normal text-gray-500')}>
{collection.description[locale === 'en' ? 'en_US' : 'zh_Hans']}
</div>
)}
</div>
</div>
{collection.type === CollectionType.builtIn && needAuth && (
<div
className={cn('cursor-pointer', 'ml-1 shrink-0 flex items-center h-8 border border-gray-200 rounded-lg px-3 space-x-2 shadow-xs')}
onClick={() => onShowAuth()}
>
<div className={cn(isAuthed ? 'border-[#12B76A] bg-[#32D583]' : 'border-gray-400 bg-gray-300', 'rounded h-2 w-2 border')}></div>
<div className='leading-5 text-sm font-medium text-gray-700'>{t(`tools.auth.${isAuthed ? 'authorized' : 'unauthorized'}`)}</div>
</div>
)}
{collection.type === CollectionType.custom && (
<div
className={cn('cursor-pointer', 'ml-1 shrink-0 flex items-center h-8 border border-gray-200 rounded-lg px-3 space-x-2 shadow-xs')}
onClick={() => onShowEditCustomCollection()}
>
<Settings01 className='w-4 h-4 text-gray-700' />
<div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
</div>
)}
</div >
)
}
export default React.memo(Header)

View File

@@ -0,0 +1,190 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { CollectionType, LOC } from '../types'
import type { Collection, CustomCollectionBackend, Tool } from '../types'
import Loading from '../../base/loading'
import { ArrowNarrowRight } from '../../base/icons/src/vender/line/arrows'
import Toast from '../../base/toast'
import Header from './header'
import Item from './item'
import AppIcon from '@/app/components/base/app-icon'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import { fetchCustomCollection, removeBuiltInToolCredential, removeCustomCollection, updateBuiltInToolCredential, updateCustomCollection } from '@/service/tools'
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
import type { AgentTool } from '@/types/app'
import { MAX_TOOLS_NUM } from '@/config'
type Props = {
collection: Collection | null
list: Tool[]
// onToolListChange: () => void // custom tools change
loc: LOC
addedTools?: AgentTool[]
onAddTool?: (collection: Collection, payload: Tool) => void
onRefreshData: () => void
onCollectionRemoved: () => void
isLoading: boolean
}
const ToolList: FC<Props> = ({
collection,
list,
loc,
addedTools,
onAddTool,
onRefreshData,
onCollectionRemoved,
isLoading,
}) => {
const { t } = useTranslation()
const isInToolsPage = loc === LOC.tools
const isBuiltIn = collection?.type === CollectionType.builtIn
const needAuth = collection?.allow_delete
const [showSettingAuth, setShowSettingAuth] = useState(false)
const [customCollection, setCustomCollection] = useState<CustomCollectionBackend | null>(null)
useEffect(() => {
if (!collection)
return
(async () => {
if (collection.type === CollectionType.custom) {
const res = await fetchCustomCollection(collection.name) as any
setCustomCollection({
...res,
provider: collection.name,
} as CustomCollectionBackend)
}
})()
}, [collection])
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
const doUpdateCustomToolCollection = async (data: CustomCollectionBackend) => {
await updateCustomCollection(data)
onRefreshData()
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setIsShowEditCustomCollectionModal(false)
}
const doRemoveCustomToolCollection = async () => {
await removeCustomCollection(collection?.name as string)
onCollectionRemoved()
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setIsShowEditCustomCollectionModal(false)
}
if (!collection || isLoading)
return <Loading type='app' />
const icon = <>{typeof collection.icon === 'string'
? (
<div
className='p-2 bg-cover bg-center border border-gray-100 rounded-lg'
>
<div className='w-6 h-6 bg-center bg-contain rounded-md'
style={{
backgroundImage: `url(${collection.icon})`,
}}
></div>
</div>
)
: (
<AppIcon
size='large'
icon={collection.icon.content}
background={collection.icon.background}
/>
)}
</>
return (
<div className='flex flex-col h-full pb-4'>
<Header
icon={icon}
collection={collection}
loc={loc}
onShowAuth={() => setShowSettingAuth(true)}
onShowEditCustomCollection={() => setIsShowEditCustomCollectionModal(true)}
/>
<div className={cn(isInToolsPage ? 'px-6 pt-4' : 'px-4 pt-3')}>
<div className='flex items-center h-[4.5] space-x-2 text-xs font-medium text-gray-500'>
<div className=''>{t('tools.includeToolNum', {
num: list.length,
})}</div>
{needAuth && isBuiltIn && !collection.is_team_authorization && (
<>
<div>·</div>
<div
className='flex items-center text-[#155EEF] cursor-pointer'
onClick={() => setShowSettingAuth(true)}
>
<div>{t('tools.auth.setup')}</div>
<ArrowNarrowRight className='ml-0.5 w-3 h-3' />
</div>
</>
)}
</div>
</div>
<div className={cn(isInToolsPage ? 'px-6' : 'px-4', 'grow h-0 pt-2 overflow-y-auto')}>
{/* list */}
<div className={cn(isInToolsPage ? 'grid-cols-3 gap-4' : 'grid-cols-1 gap-2', 'grid')}>
{list.map(item => (
<Item
key={item.name}
icon={icon}
payload={item}
collection={collection}
isInToolsPage={isInToolsPage}
isToolNumMax={(addedTools?.length || 0) >= MAX_TOOLS_NUM}
added={!!addedTools?.find(v => v.provider_id === collection.id && v.tool_name === item.name)}
onAdd={!isInToolsPage ? tool => onAddTool?.(collection as Collection, tool) : undefined}
/>
))}
</div>
</div>
{showSettingAuth && (
<ConfigCredential
collection={collection}
onCancel={() => setShowSettingAuth(false)}
onSaved={async (value) => {
await updateBuiltInToolCredential(collection.name, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
await onRefreshData()
setShowSettingAuth(false)
}}
onRemove={async () => {
await removeBuiltInToolCredential(collection.name)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
await onRefreshData()
setShowSettingAuth(false)
}}
/>
)}
{isShowEditCollectionToolModal && (
<EditCustomToolModal
payload={customCollection}
onHide={() => setIsShowEditCustomCollectionModal(false)}
onEdit={doUpdateCustomToolCollection}
onRemove={doRemoveCustomToolCollection}
/>
)}
</div>
)
}
export default React.memo(ToolList)

View File

@@ -0,0 +1,78 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useContext } from 'use-context-selector'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import type { Collection, Tool } from '../types'
import Button from '../../base/button'
import { CollectionType } from '../types'
import TooltipPlus from '../../base/tooltip-plus'
import I18n from '@/context/i18n'
import SettingBuiltInTool from '@/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool'
type Props = {
collection: Collection
icon: JSX.Element
payload: Tool
isInToolsPage: boolean
isToolNumMax: boolean
added?: boolean
onAdd?: (payload: Tool) => void
}
const Item: FC<Props> = ({
collection,
icon,
payload,
isInToolsPage,
isToolNumMax,
added,
onAdd,
}) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const isBuiltIn = collection.type === CollectionType.builtIn
const canShowDetail = !isBuiltIn || (isBuiltIn && isInToolsPage)
const [showDetail, setShowDetail] = useState(false)
const addBtn = <Button className='shrink-0 flex items-center h-7 !px-3 !text-xs !font-medium !text-gray-700' disabled={added || !collection.is_team_authorization} onClick={() => onAdd?.(payload)}>{t(`common.operation.${added ? 'added' : 'add'}`)}</Button>
return (
<>
<div
className={cn(canShowDetail && 'cursor-pointer', 'flex justify-between items-start p-4 rounded-xl border border-gray-200 bg-gray-50 shadow-xs')}
onClick={() => canShowDetail && setShowDetail(true)}
>
<div className='flex items-start w-full'>
{icon}
<div className='ml-3 w-0 grow'>
<div className={cn('text-base font-semibold text-gray-900 truncate')}>{payload.label[locale === 'en' ? 'en_US' : 'zh_Hans']}</div>
<div className={cn('leading-[18px] text-[13px] font-normal text-gray-500')}>
{payload.description[locale === 'en' ? 'en_US' : 'zh_Hans']}
</div>
</div>
</div>
<div className='shrink-0'>
{!isToolNumMax && onAdd && (
!collection.is_team_authorization
? <TooltipPlus popupContent={t('tools.auth.unauthorized')}>
{addBtn}
</TooltipPlus>
: addBtn
)}
</div>
</div>
{showDetail && isBuiltIn && (
<SettingBuiltInTool
collection={collection}
toolName={payload.name}
readonly
onHide={() => {
setShowDetail(false)
}}
/>
)}
</>
)
}
export default React.memo(Item)

View File

@@ -0,0 +1,28 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from 'classnames'
import Item from './item'
import type { Collection } from '@/app/components/tools/types'
type Props = {
className?: string
currentName: string
list: Collection[]
onChosen: (index: number) => void
}
const ToolNavList: FC<Props> = ({
className,
currentName,
list,
onChosen,
}) => {
return (
<div className={cn(className)}>
{list.map((item, index) => (
<Item isCurrent={item.name === currentName} key={item.name} payload={item} onClick={() => onChosen(index)}></Item>
))}
</div>
)
}
export default React.memo(ToolNavList)

View File

@@ -0,0 +1,49 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector'
import cn from 'classnames'
import AppIcon from '../../base/app-icon'
import type { Collection } from '@/app/components/tools/types'
import I18n from '@/context/i18n'
type Props = {
isCurrent: boolean
payload: Collection
onClick: () => void
}
const Item: FC<Props> = ({
isCurrent,
payload,
onClick,
}) => {
const { locale } = useContext(I18n)
return (
<div
className={cn(isCurrent && 'bg-white shadow-xs rounded-lg', 'mt-1 flex h-9 items-center px-2 space-x-2 cursor-pointer')}
onClick={() => !isCurrent && onClick()}
>
{typeof payload.icon === 'string'
? (
<div
className='w-6 h-6 bg-cover bg-center rounded-md'
style={{
backgroundImage: `url(${payload.icon})`,
}}
></div>
)
: (
<AppIcon
size='tiny'
icon={payload.icon.content}
background={payload.icon.background}
/>
)}
<div className={cn(isCurrent && 'text-primary-600 font-semibold', 'leading-5 text-sm font-normal truncate')}>{payload.label[locale === 'en' ? 'en_US' : 'zh_Hans']}</div>
</div>
)
}
export default React.memo(Item)

View File

@@ -0,0 +1,111 @@
import type { TypeWithI18N } from '../header/account-setting/model-provider-page/declarations'
export enum LOC {
tools = 'tools',
app = 'app',
}
export enum AuthType {
none = 'none',
apiKey = 'api_key',
}
export type Credential = {
'auth_type': AuthType
'api_key_header'?: string
'api_key_value'?: string
}
export enum CollectionType {
all = 'all',
builtIn = 'builtin',
custom = 'api',
}
export type Emoji = {
background: string
content: string
}
export type Collection = {
id: string
name: string
author: string
description: TypeWithI18N
icon: string | Emoji
label: TypeWithI18N
type: CollectionType
team_credentials: Record<string, any>
is_team_authorization: boolean
allow_delete: boolean
}
export type ToolParameter = {
name: string
label: TypeWithI18N
human_description: TypeWithI18N
type: string
required: boolean
default: string
options?: {
label: TypeWithI18N
value: string
}[]
}
export type Tool = {
name: string
label: TypeWithI18N
description: {
zh_Hans: string
en_US: string
}
parameters: ToolParameter[]
}
export type ToolCredential = {
name: string
label: TypeWithI18N
help: TypeWithI18N
placeholder: TypeWithI18N
type: string
required: boolean
default: string
options?: {
label: TypeWithI18N
value: string
}[]
}
export type CustomCollectionBackend = {
provider: string
original_provider?: string
credentials: Credential
icon: Emoji
schema_type: string
schema: string
privacy_policy: string
tools?: ParamItem[]
}
export type ParamItem = {
name: string
label: TypeWithI18N
human_description: TypeWithI18N
type: string
required: boolean
default: string
min?: number
max?: number
options?: {
label: TypeWithI18N
value: string
}[]
}
export type CustomParamSchema = {
operation_id: string // name
summary: string
server_url: string
method: string
parameters: ParamItem[]
}

View File

@@ -0,0 +1,26 @@
import type { ThoughtItem } from '../../app/chat/type'
import type { VisionFile } from '@/types/app'
export const sortAgentSorts = (list: ThoughtItem[]) => {
if (!list)
return list
if (list.some(item => item.position === undefined))
return list
const temp = [...list]
temp.sort((a, b) => a.position - b.position)
return temp
}
export const addFileInfos = (list: ThoughtItem[], messageFiles: VisionFile[]) => {
if (!list || !messageFiles)
return list
return list.map((item) => {
if (item.files && item.files?.length > 0) {
return {
...item,
message_files: item.files.map(fileId => messageFiles.find(file => file.id === fileId)) as VisionFile[],
}
}
return item
})
}

View File

@@ -0,0 +1,64 @@
import type { ToolCredential, ToolParameter } from '../types'
const toType = (type: string) => {
switch (type) {
case 'string':
return 'text-input'
case 'number':
return 'number-input'
default:
return type
}
}
export const toolParametersToFormSchemas = (parameters: ToolParameter[]) => {
if (!parameters)
return []
const formSchemas = parameters.map((parameter) => {
return {
...parameter,
variable: parameter.name,
type: toType(parameter.type),
show_on: [],
options: parameter.options?.map((option) => {
return {
...option,
show_on: [],
}
}),
tooltip: parameter.human_description,
}
})
return formSchemas
}
export const toolCredentialToFormSchemas = (parameters: ToolCredential[]) => {
if (!parameters)
return []
const formSchemas = parameters.map((parameter) => {
return {
...parameter,
variable: parameter.name,
label: parameter.label,
tooltip: parameter.help,
show_on: [],
options: parameter.options?.map((option) => {
return {
...option,
show_on: [],
}
}),
}
})
return formSchemas
}
export const addDefaultValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[]) => {
const newValues = { ...value }
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
if (formSchema.default && (value === undefined || itemValue === null || itemValue === ''))
newValues[formSchema.variable] = formSchema.default
})
return newValues
}