feat: external knowledge api crud frontend & connect external knowledge base

This commit is contained in:
Yi 2024-09-26 01:00:49 +08:00
parent d6c604a356
commit cfa4825073
32 changed files with 1237 additions and 138 deletions

View File

@ -19,6 +19,7 @@ import TagManagementModal from '@/app/components/base/tag-management'
import TagFilter from '@/app/components/base/tag-management/filter'
import Button from '@/app/components/base/button'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context'
// Services
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
@ -70,48 +71,49 @@ const Container = () => {
useEffect(() => {
if (currentWorkspace.role === 'normal')
return router.replace('/apps')
}, [currentWorkspace])
}, [currentWorkspace, router])
return (
<div ref={containerRef} className='grow relative flex flex-col bg-gray-100 overflow-y-auto'>
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
<TabSliderNew
value={activeTab}
onChange={newActiveTab => setActiveTab(newActiveTab)}
options={options}
/>
{activeTab === 'dataset' && (
<div className='flex items-center gap-2'>
<TagFilter type='knowledge' value={tagFilterValue} onChange={handleTagsChange} />
<SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
<div className="w-[1px] h-4 bg-divider-regular" />
<Button
className='gap-0.5 shadows-shadow-xs'
onClick={() => setShowExternalApiPanel(true)}
>
<ApiConnectionMod className='w-4 h-4 text-components-button-secondary-text' />
<div className='flex px-0.5 justify-center items-center gap-1 text-components-button-secondary-text system-sm-medium'>{t('dataset.externalAPI')}</div>
</Button>
</div>
)}
{activeTab === 'api' && data && <ApiServer apiBaseUrl={data.api_base_url || ''} />}
</div>
{activeTab === 'dataset' && (
<>
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} />
<DatasetFooter />
{showTagManagementModal && (
<TagManagementModal type='knowledge' show={showTagManagementModal} />
<ExternalKnowledgeApiProvider>
<div ref={containerRef} className='grow relative flex flex-col bg-gray-100 overflow-y-auto'>
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
<TabSliderNew
value={activeTab}
onChange={newActiveTab => setActiveTab(newActiveTab)}
options={options}
/>
{activeTab === 'dataset' && (
<div className='flex items-center gap-2'>
<TagFilter type='knowledge' value={tagFilterValue} onChange={handleTagsChange} />
<SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
<div className="w-[1px] h-4 bg-divider-regular" />
<Button
className='gap-0.5 shadows-shadow-xs'
onClick={() => setShowExternalApiPanel(true)}
>
<ApiConnectionMod className='w-4 h-4 text-components-button-secondary-text' />
<div className='flex px-0.5 justify-center items-center gap-1 text-components-button-secondary-text system-sm-medium'>{t('dataset.externalAPI')}</div>
</Button>
</div>
)}
</>
)}
{activeTab === 'api' && data && <ApiServer apiBaseUrl={data.api_base_url || ''} />}
</div>
{activeTab === 'api' && data && <Doc apiBaseUrl={data.api_base_url || ''} />}
{activeTab === 'dataset' && (
<>
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} />
<DatasetFooter />
{showTagManagementModal && (
<TagManagementModal type='knowledge' show={showTagManagementModal} />
)}
</>
)}
{showExternalApiPanel && <ExternalAPIPanel onClose={() => setShowExternalApiPanel(false)} isShow={showExternalApiPanel} />}
</div>
{activeTab === 'api' && data && <Doc apiBaseUrl={data.api_base_url || ''} />}
{showExternalApiPanel && <ExternalAPIPanel onClose={() => setShowExternalApiPanel(false)} isShow={showExternalApiPanel} datasetBindings={[]} />}
</div>
</ExternalKnowledgeApiProvider>
)
}

View File

@ -18,6 +18,7 @@ import Divider from '@/app/components/base/divider'
import RenameDatasetModal from '@/app/components/datasets/rename-modal'
import type { Tag } from '@/app/components/base/tag-management/constant'
import TagSelector from '@/app/components/base/tag-management/selector'
import CornerLabel from '@/app/components/base/corner-label'
import { useAppContext } from '@/context/app-context'
export type DatasetCardProps = {
@ -108,13 +109,14 @@ const DatasetCard = ({
return (
<>
<div
className='group col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
className='group relative col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
data-disable-nprogress={true}
onClick={(e) => {
e.preventDefault()
push(`/datasets/${dataset.id}/documents`)
}}
>
{dataset.provider === 'external' && <CornerLabel label='External' className='absolute right-0' labelClassName='rounded-tr-xl' />}
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className={cn(
'shrink-0 flex items-center justify-center p-2.5 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#E0EAFF]',

View File

@ -0,0 +1,13 @@
import React from 'react'
import ExternalKnowledgeBaseConnector from '@/app/components/datasets/external-knowledge-base/connector'
import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context'
const ExternalKnowledgeBaseCreation = async () => {
return (
<ExternalKnowledgeApiProvider>
<ExternalKnowledgeBaseConnector />
</ExternalKnowledgeApiProvider>
)
}
export default ExternalKnowledgeBaseCreation

View File

@ -0,0 +1,21 @@
import { Corner } from '../icons/src/vender/solid/shapes'
import cn from '@/utils/classnames'
type CornerLabelProps = {
label: string
className?: string
labelClassName?: string
}
const CornerLabel: React.FC<CornerLabelProps> = ({ label, className, labelClassName }) => {
return (
<div className={cn('group/corner-label inline-flex items-start', className)}>
<Corner className='w-[13px] h-5 text-background-section group-hover/corner-label:text-background-section-burn' />
<div className={cn('flex py-1 pr-2 items-center gap-0.5 bg-background-section group-hover/corner-label:bg-background-section-burn', labelClassName)}>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{label}</div>
</div>
</div>
)
}
export default CornerLabel

View File

@ -0,0 +1,3 @@
<svg width="13" height="20" viewBox="0 0 13 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Shape" d="M0 0H13V20C9.98017 20 7.26458 18.1615 6.14305 15.3576L0 0Z" fill="#F9FAFB"/>
</svg>

After

Width:  |  Height:  |  Size: 200 B

View File

@ -0,0 +1,27 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "13",
"height": "20",
"viewBox": "0 0 13 20",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Shape",
"d": "M0 0H13V20C9.98017 20 7.26458 18.1615 6.14305 15.3576L0 0Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "Corner"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Corner.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Corner'
export default Icon

View File

@ -1,2 +1,3 @@
export { default as Corner } from './Corner'
export { default as Star04 } from './Star04'
export { default as Star06 } from './Star06'

View File

@ -37,6 +37,7 @@ const ParamItem: FC<Props> = ({ className, id, name, noTooltip, tip, step = 0.1,
<span className="mx-1 text-gray-900 text-[13px] leading-[18px] font-medium">{name}</span>
{!noTooltip && (
<Tooltip
triggerClassName='w-4 h-4 shrink-0'
popupContent={<div className="w-[200px]">{tip}</div>}
/>
)}

View File

@ -87,7 +87,7 @@ const Select: FC<ISelectProps> = ({
<div className='group text-gray-800'>
{allowSearch
? <Combobox.Input
className={`w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-not-allowed`}
className={`w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}
onChange={(event) => {
if (!disabled)
setQuery(event.target.value)

View File

@ -0,0 +1,16 @@
export type CreateExternalAPIReq = {
name: string
settings: {
endpoint: string
api_key: string
}
}
export type FormSchema = {
variable: string
type: 'text' | 'secret'
label: {
[key: string]: string
}
required: boolean
}

View File

@ -0,0 +1,42 @@
import type { Dispatch, SetStateAction } from 'react'
export enum ValidatedEndpointStatus {
Success = 'success',
Error = 'error',
}
export type ValidatedStatusState = {
status?: ValidatedEndpointStatus
message?: string
}
export type Status = 'add' | 'fail' | 'success'
export type ValidateValue = string
export type ValidateCallback = {
before: (v?: ValidateValue) => boolean | undefined
run?: (v?: ValidateValue) => Promise<ValidatedStatusState>
}
export type Form = {
key: string
title: string
placeholder: string
value?: string
validate?: ValidateCallback
handleFocus?: (v: ValidateValue, dispatch: Dispatch<SetStateAction<ValidateValue>>) => void
}
export type KeyFrom = {
text: string
link: string
}
export type KeyValidatorProps = {
type: string
title: React.ReactNode
status: Status
forms: Form[]
keyFrom: KeyFrom
}

View File

@ -0,0 +1,31 @@
import { useState } from 'react'
import { useDebounceFn } from 'ahooks'
import type { DebouncedFunc } from 'lodash-es'
import { ValidatedEndpointStatus } from './declarations'
import type { ValidateCallback, ValidateValue, ValidatedStatusState } from './declarations'
export const useValidateEndpoint: (value: ValidateValue) => [DebouncedFunc<(validateCallback: ValidateCallback) => Promise<void>>, boolean, ValidatedStatusState] = (value) => {
const [validating, setValidating] = useState(false)
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>({})
const { run } = useDebounceFn(async (validateCallback: ValidateCallback) => {
if (!validateCallback.before(value)) {
setValidating(false)
setValidatedStatus({})
return
}
setValidating(true)
if (validateCallback.run) {
const res = await validateCallback?.run(value)
setValidatedStatus(
res.status === 'success'
? { status: ValidatedEndpointStatus.Success }
: { status: ValidatedEndpointStatus.Error, message: res.message })
setValidating(false)
}
}, { wait: 1000 })
return [run, validating, validatedStatus]
}

View File

@ -0,0 +1,84 @@
import React, { useState } from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import type { CreateExternalAPIReq, FormSchema } from '../declarations'
import Input from '@/app/components/base/input'
import cn from '@/utils/classnames'
type FormProps = {
className?: string
itemClassName?: string
fieldLabelClassName?: string
value: CreateExternalAPIReq
onChange: (val: CreateExternalAPIReq) => void
validatingEndpoint: boolean
validatedApiKeySuccess?: boolean
validatingApiKey: boolean
validatedEndpointSuccess?: boolean
formSchemas: FormSchema[]
inputClassName?: string
}
const Form: FC<FormProps> = React.memo(({
className,
itemClassName,
fieldLabelClassName,
value,
onChange,
formSchemas,
validatingEndpoint,
validatingApiKey,
validatedApiKeySuccess,
validatedEndpointSuccess,
inputClassName,
}) => {
const { t, i18n } = useTranslation()
const [changeKey, setChangeKey] = useState('')
const handleFormChange = (key: string, val: string) => {
setChangeKey(key)
if (key === 'name') {
onChange({ ...value, [key]: val })
}
else {
onChange({
...value,
settings: {
...value.settings,
[key]: val,
},
})
}
}
const renderField = (formSchema: FormSchema) => {
const { variable, type, label, required } = formSchema
const fieldValue = variable === 'name' ? value[variable] : (value.settings[variable as keyof typeof value.settings] || '')
return (
<div key={variable} className={cn(itemClassName, 'flex flex-col items-start gap-1 self-stretch')}>
<label className={cn(fieldLabelClassName, 'text-text-secondary system-sm-semibold')} htmlFor={variable}>
{label[i18n.language] || label.en_US}
{required && <span className='ml-1 text-red-500'>*</span>}
</label>
<Input
type={type === 'secret' ? 'password' : 'text'}
id={variable}
name={variable}
value={fieldValue}
onChange={val => handleFormChange(variable, val.target.value)}
required={required}
className={cn(inputClassName)}
/>
</div>
)
}
return (
<form className={cn('flex flex-col justify-center items-start gap-4 self-stretch', className)}>
{formSchemas.map(formSchema => renderField(formSchema))}
</form>
)
})
export default Form

View File

@ -1,46 +1,111 @@
import type { FC } from 'react'
import {
memo,
useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiBook2Line,
RiCloseLine,
RiInformation2Line,
RiLock2Fill,
} from '@remixicon/react'
import { useToastContext } from '@/app/components/base/toast'
import { useValidateApiKey } from '../key-validator/hooks'
import { ValidatedApiKeyStatus } from '../key-validator/declarations'
import { ValidatedEndpointStatus } from '../endpoint-validator/declarations'
import { useValidateEndpoint } from '../endpoint-validator/hooks'
import type { CreateExternalAPIReq, FormSchema } from '../declarations'
import Form from './Form'
import ActionButton from '@/app/components/base/action-button'
import Confirm from '@/app/components/base/confirm'
import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
import ActionButton from '@/app/components/base/action-button'
import Input from '@/app/components/base/input'
import { createExternalAPI } from '@/service/datasets'
import { useToastContext } from '@/app/components/base/toast'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
type AddExternalAPIModalProps = {
show: boolean
onHide: () => void
data?: CreateExternalAPIReq
onSave: (formValue: CreateExternalAPIReq) => void
onCancel: () => void
onEdit?: (formValue: CreateExternalAPIReq) => Promise<void>
datasetBindings?: { id: string; name: string }[]
isEditMode: boolean
}
const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ show, onHide }) => {
const formSchemas: FormSchema[] = [
{
variable: 'name',
type: 'text',
label: {
en_US: 'Name',
},
required: true,
},
{
variable: 'endpoint',
type: 'text',
label: {
en_US: 'API Endpoint',
},
required: true,
},
{
variable: 'api_key',
type: 'secret',
label: {
en_US: 'API Key',
},
required: true,
},
]
const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ data, onSave, onCancel, datasetBindings, isEditMode, onEdit }) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const [loading, setLoading] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
const [formData, setFormData] = useState({ name: '', endpoint: '', apiKey: '' })
const isEditMode = true
const [formData, setFormData] = useState<CreateExternalAPIReq>({ name: '', settings: { endpoint: '', api_key: '' } })
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setFormData({ ...formData, [name]: value })
useEffect(() => {
if (isEditMode && data)
setFormData(data)
}, [isEditMode, data])
const [, validatingApiKey, validatedApiKeyStatusState] = useValidateApiKey(formData.settings.api_key)
const [, validatingEndpoint, validatedEndpointStatusState] = useValidateEndpoint(formData.settings.endpoint)
const hasEmptyInputs = Object.values(formData).includes('')
const handleDataChange = (val: CreateExternalAPIReq) => {
setFormData(val)
}
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Handle form submission logic here
console.log('Form Data:', formData)
onHide()
const handleSave = async () => {
try {
setLoading(true)
if (isEditMode && onEdit) {
await onEdit(formData)
notify({ type: 'success', message: 'External API updated successfully' })
}
else {
const res = await createExternalAPI({ body: formData })
if (res && res.id) {
notify({ type: 'success', message: 'External API saved successfully' })
onSave(res)
}
}
onCancel()
}
catch (error) {
console.error('Error saving/updating external API:', error)
notify({ type: 'error', message: 'Failed to save/update External API' })
}
finally {
setLoading(false)
}
}
return (
@ -51,69 +116,69 @@ const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ show, onHide }) =>
<div className='flex flex-col pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
<div className='self-stretch text-text-primary title-2xl-semi-bold flex-grow'>
{
isEditMode ? t('dataset.editExternalAPIFormTitle') : t('dataset.createExternalAPIFormTitle')
isEditMode ? t('dataset.editExternalAPIFormTitle') : t('dataset.createExternalAPI')
}
</div>
{isEditMode && (
{isEditMode && (datasetBindings?.length ?? 0) > 0 && (
<div className='text-text-tertiary system-xs-regular flex items-center'>
{t('dataset.editExternalAPIFormWarning.front')}
<span className='text-text-accent cursor-pointer flex items-center'>
&nbsp;3 {t('dataset.editExternalAPIFormWarning.end')}&nbsp;<Tooltip popupContent={'3 LINKED KNOWLEDGE --- needs to be modified'} asChild={false} position='bottom'><RiInformation2Line className='w-3.5 h-3.5' /></Tooltip>
&nbsp;{datasetBindings?.length} {t('dataset.editExternalAPIFormWarning.end')}&nbsp;
<Tooltip
popupClassName='flex items-center self-stretch w-[320px]'
popupContent={
<div className='p-1'>
<div className='flex pt-1 pb-0.5 pl-2 pr-3 items-start self-stretch'>
<div className='text-text-tertiary system-xs-medium-uppercase'>{`${datasetBindings?.length} ${t('dataset.editExternalAPITooltipTitle')}`}</div>
{datasetBindings?.map(binding => (
<div key={binding.id} className='flex px-2 py-1 items-center gap-1 self-stretch'>
<RiBook2Line className='w-4 h-4 text-text-secondary' />
<div className='text-text-secondary system-sm-medium'>{binding.name}</div>
</div>
))}
</div>
</div>
}
asChild={false}
position='bottom'
>
<RiInformation2Line className='w-3.5 h-3.5' />
</Tooltip>
</span>
</div>
)}
</div>
<ActionButton className='absolute top-5 right-5' onClick={onHide}>
<ActionButton className='absolute top-5 right-5' onClick={onCancel}>
<RiCloseLine className='w-[18px] h-[18px] text-text-tertiary flex-shrink-0' />
</ActionButton>
<form onSubmit={handleFormSubmit} className='flex px-6 py-3 flex-col justify-center items-start gap-4 self-stretch'>
<div className='flex flex-col justify-center items-start gap-4 self-stretch'>
<div className='flex flex-col items-start gap-1 self-stretch'>
<label className='text-text-secondary system-sm-semibold' htmlFor='name'>
{t('dataset.externalAPIForm.name')}
</label>
<Input
type='text'
id='name'
name='name'
value={formData.name}
onChange={handleInputChange}
required
/>
</div>
<div className='flex flex-col items-start gap-1 self-stretch'>
<label className='text-text-secondary system-sm-semibold' htmlFor='endpoint'>
{t('dataset.externalAPIForm.endpoint')}
</label>
<Input
type='text'
id='endpoint'
name='endpoint'
value={formData.endpoint}
onChange={handleInputChange}
required
/>
</div>
<div className='flex flex-col items-start gap-1 self-stretch'>
<label className='text-text-secondary system-sm-semibold' htmlFor='apiKey'>
{t('dataset.externalAPIForm.apiKey')}
</label>
<Input
type='text'
id='apiKey'
name='apiKey'
value={formData.apiKey}
onChange={handleInputChange}
required
/>
</div>
</div>
</form>
<Form
value={formData}
onChange={handleDataChange}
validatingApiKey={validatingApiKey}
validatedApiKeySuccess={validatedApiKeyStatusState?.status === ValidatedApiKeyStatus.Success}
validatingEndpoint={validatingEndpoint}
validatedEndpointSuccess={validatedEndpointStatusState?.status === ValidatedEndpointStatus.Success}
formSchemas={formSchemas}
className='flex px-6 py-3 flex-col justify-center items-start gap-4 self-stretch'
/>
<div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'>
<Button type='button' variant='secondary' onClick={onHide}>
<Button type='button' variant='secondary' onClick={onCancel}>
{t('dataset.externalAPIForm.cancel')}
</Button>
<Button type='submit' variant='primary'>
<Button
type='submit'
variant='primary'
onClick={() => {
if (isEditMode && (datasetBindings?.length ?? 0) > 0)
setShowConfirm(true)
else if (isEditMode && onEdit)
onEdit(formData)
else
handleSave()
}}
disabled={hasEmptyInputs || loading}
>
{t('dataset.externalAPIForm.save')}
</Button>
</div>
@ -132,6 +197,16 @@ const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ show, onHide }) =>
{t('dataset.externalAPIForm.encrypted.end')}
</div>
</div>
{showConfirm && (datasetBindings?.length ?? 0) > 0 && (
<Confirm
isShow={showConfirm}
type='warning'
title='Warning'
content={`${t('datasets.editExternalAPIConfirmWarningContent.front')} ${datasetBindings?.length} ${t('datasets.editExternalAPIConfirmWarningContent.end')}`}
onCancel={() => setShowConfirm(false)}
onConfirm={handleSave}
/>
)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@ -5,23 +5,37 @@ import {
RiCloseLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import ExternalKnowledgeAPICard from '../external-knowledge-api-card'
import cn from '@/utils/classnames'
// import AddExternalAPIForm from '../create/add-external-api'
import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import { useModalContext } from '@/context/modal-context'
type ExternalAPIPanelProps = {
onClose: () => void
isShow: boolean
datasetBindings: { id: string; name: string }[]
}
const ExternalAPIPanel: React.FC<ExternalAPIPanelProps> = ({ onClose, isShow }) => {
const ExternalAPIPanel: React.FC<ExternalAPIPanelProps> = ({ onClose, isShow, datasetBindings }) => {
const { t } = useTranslation()
const { setShowExternalAPIModal } = useModalContext()
const { setShowExternalKnowledgeAPIModal } = useModalContext()
const { externalKnowledgeApiList, mutateExternalKnowledgeApis, isLoading } = useExternalKnowledgeApi()
const handleOpenExternalAPIModal = () => {
setShowExternalAPIModal()
setShowExternalKnowledgeAPIModal({
payload: { name: '', settings: { endpoint: '', api_key: '' } },
datasetBindings: [],
onSaveCallback: () => {
mutateExternalKnowledgeApis()
},
onCancelCallback: () => {
mutateExternalKnowledgeApis()
},
isEditMode: false,
})
}
return (
@ -60,7 +74,15 @@ const ExternalAPIPanel: React.FC<ExternalAPIPanelProps> = ({ onClose, isShow })
</Button>
</div>
<div className='flex py-0 px-4 flex-col items-start gap-1 flex-grow self-stretch'>
{isLoading
? (
<Loading />
)
: (
externalKnowledgeApiList.map(api => (
<ExternalKnowledgeAPICard key={api.id} api={api} />
))
)}
</div>
</div>
</div>

View File

@ -0,0 +1,151 @@
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiDeleteBinLine,
RiEditLine,
} from '@remixicon/react'
import type { CreateExternalAPIReq } from '../declarations'
import type { ExternalAPIItem } from '@/models/datasets'
import { checkUsageExternalAPI, deleteExternalAPI, fetchExternalAPI, updateExternalAPI } from '@/service/datasets'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
import { useModalContext } from '@/context/modal-context'
import ActionButton from '@/app/components/base/action-button'
import Confirm from '@/app/components/base/confirm'
type ExternalKnowledgeAPICardProps = {
api: ExternalAPIItem
}
const ExternalKnowledgeAPICard: React.FC<ExternalKnowledgeAPICardProps> = ({ api }) => {
const { setShowExternalKnowledgeAPIModal } = useModalContext()
const [showConfirm, setShowConfirm] = useState(false)
const [isHovered, setIsHovered] = useState(false)
const [usageCount, setUsageCount] = useState(0)
const { mutateExternalKnowledgeApis } = useExternalKnowledgeApi()
const { t } = useTranslation()
const handleEditClick = async () => {
try {
const response = await fetchExternalAPI({ apiTemplateId: api.id })
const formValue: CreateExternalAPIReq = {
name: response.name,
settings: {
endpoint: response.settings.endpoint,
api_key: response.settings.api_key,
},
}
setShowExternalKnowledgeAPIModal({
payload: formValue,
onSaveCallback: () => {
mutateExternalKnowledgeApis()
},
onCancelCallback: () => {
mutateExternalKnowledgeApis()
},
isEditMode: true,
datasetBindings: response.dataset_bindings,
onEditCallback: async (updatedData: CreateExternalAPIReq) => {
try {
await updateExternalAPI({
apiTemplateId: api.id,
body: {
...response,
name: updatedData.name,
settings: {
...response.settings,
endpoint: updatedData.settings.endpoint,
api_key: updatedData.settings.api_key,
},
},
})
mutateExternalKnowledgeApis()
}
catch (error) {
console.error('Error updating external knowledge API:', error)
}
},
})
}
catch (error) {
console.error('Error fetching external knowledge API data:', error)
}
}
const handleDeleteClick = async () => {
try {
const usage = await checkUsageExternalAPI({ apiTemplateId: api.id })
if (usage.is_using)
setUsageCount(usage.count)
setShowConfirm(true)
}
catch (error) {
console.error('Error checking external API usage:', error)
}
}
const handleConfirmDelete = async () => {
try {
const response = await deleteExternalAPI({ apiTemplateId: api.id })
if (response && response.result === 'success') {
setShowConfirm(false)
mutateExternalKnowledgeApis()
}
else {
console.error('Failed to delete external API')
}
}
catch (error) {
console.error('Error deleting external knowledge API:', error)
}
}
return (
<>
<div className={`flex p-2 pl-3 items-start self-stretch rounded-lg border-[0.5px]
border-components-panel-border-subtle bg-components-panel-on-panel-item-bg
shadows-shadow-xs ${isHovered ? 'bg-state-destructive-hover border-state-destructive-border' : ''}`}
>
<div className='flex py-1 flex-col justify-center items-start gap-1.5 flex-grow'>
<div className='flex items-center gap-1 self-stretch text-text-secondary'>
<ApiConnectionMod className='w-4 h-4' />
<div className='system-sm-medium'>{api.name}</div>
</div>
<div className='self-stretch text-text-tertiary system-xs-regular'>{api.settings.endpoint}</div>
</div>
<div className='flex items-start gap-1'>
<ActionButton onClick={handleEditClick}>
<RiEditLine className='w-4 h-4 text-text-tertiary hover:text-text-secondary' />
</ActionButton>
<ActionButton
className='hover:bg-state-destructive-hover'
onClick={handleDeleteClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary hover:text-text-destructive' />
</ActionButton>
</div>
</div>
{showConfirm && (
<Confirm
isShow={showConfirm}
title={`${t('dataset.deleteExternalAPIConfirmWarningContent.title.front')} ${api.name}${t('dataset.deleteExternalAPIConfirmWarningContent.title.end')}`}
content={
usageCount > 0
? `${t('dataset.deleteExternalAPIConfirmWarningContent.content.front')} ${usageCount} ${t('dataset.deleteExternalAPIConfirmWarningContent.content.end')}`
: t('dataset.deleteExternalAPIConfirmWarningContent.noConnectionContent')
}
type='warning'
onConfirm={handleConfirmDelete}
onCancel={() => setShowConfirm(false)}
/>
)}
</>
)
}
export default ExternalKnowledgeAPICard

View File

@ -0,0 +1,42 @@
import type { Dispatch, SetStateAction } from 'react'
export enum ValidatedApiKeyStatus {
Success = 'success',
Error = 'error',
}
export type ValidatedStatusState = {
status?: ValidatedApiKeyStatus
message?: string
}
export type Status = 'add' | 'fail' | 'success'
export type ValidateValue = string
export type ValidateCallback = {
before: (v?: ValidateValue) => boolean | undefined
run?: (v?: ValidateValue) => Promise<ValidatedStatusState>
}
export type Form = {
key: string
title: string
placeholder: string
value?: string
validate?: ValidateCallback
handleFocus?: (v: ValidateValue, dispatch: Dispatch<SetStateAction<ValidateValue>>) => void
}
export type KeyFrom = {
text: string
link: string
}
export type KeyValidatorProps = {
type: string
title: React.ReactNode
status: Status
forms: Form[]
keyFrom: KeyFrom
}

View File

@ -0,0 +1,31 @@
import { useState } from 'react'
import { useDebounceFn } from 'ahooks'
import type { DebouncedFunc } from 'lodash-es'
import { ValidatedApiKeyStatus } from './declarations'
import type { ValidateCallback, ValidateValue, ValidatedStatusState } from './declarations'
export const useValidateApiKey: (value: ValidateValue) => [DebouncedFunc<(validateCallback: ValidateCallback) => Promise<void>>, boolean, ValidatedStatusState] = (value) => {
const [validating, setValidating] = useState(false)
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>({})
const { run } = useDebounceFn(async (validateCallback: ValidateCallback) => {
if (!validateCallback.before(value)) {
setValidating(false)
setValidatedStatus({})
return
}
setValidating(true)
if (validateCallback.run) {
const res = await validateCallback?.run(value)
setValidatedStatus(
res.status === 'success'
? { status: ValidatedApiKeyStatus.Success }
: { status: ValidatedApiKeyStatus.Error, message: res.message })
setValidating(false)
}
}, { wait: 1000 })
return [run, validating, validatedStatus]
}

View File

@ -0,0 +1,20 @@
'use client'
import React from 'react'
import ExternalKnowledgeBaseCreate from '@/app/components/datasets/external-knowledge-base/create'
import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external-knowledge-base/create/declarations'
import { createExternalKnowledgeBase } from '@/service/datasets'
const ExternalKnowledgeBaseConnector = () => {
const handleConnect = async (formValue: CreateKnowledgeBaseReq) => {
try {
const result = await createExternalKnowledgeBase({ body: formValue })
}
catch (error) {
console.error('Error creating external knowledge base:', error)
}
}
return <ExternalKnowledgeBaseCreate onConnect={handleConnect} />
}
export default ExternalKnowledgeBaseConnector

View File

@ -0,0 +1,47 @@
import { useTranslation } from 'react-i18next'
import Select from '@/app/components/base/select'
import Input from '@/app/components/base/input'
import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
type ExternalApiSelectionProps = {
external_knowledge_api_id: string
external_knowledge_id: string
onChange: (data: { external_knowledge_api_id?: string; external_knowledge_id?: string }) => void
}
const ExternalApiSelection = ({ external_knowledge_api_id, external_knowledge_id, onChange }: ExternalApiSelectionProps) => {
const { t } = useTranslation()
const { externalKnowledgeApiList } = useExternalKnowledgeApi()
const apiItems = externalKnowledgeApiList.map(api => ({
value: api.id,
name: api.name,
}))
return (
<form className='flex flex-col gap-4 self-stretch'>
<div className='flex flex-col gap-1 self-stretch'>
<div className='flex flex-col self-stretch'>
<label className='text-text-secondary system-sm-semibold'>{t('dataset.externalAPIPanelTitle')}</label>
</div>
<Select
className='w-full'
items={apiItems}
defaultValue={apiItems.length > 0 ? apiItems[0].value : ''}
onSelect={e => onChange({ external_knowledge_api_id: e.value as string, external_knowledge_id })}
/>
</div>
<div className='flex flex-col gap-1 self-stretch'>
<div className='flex flex-col self-stretch'>
<label className='text-text-secondary system-sm-semibold'>{t('dataset.externalKnowledgeId')}</label>
</div>
<Input
value={external_knowledge_id}
onChange={e => onChange({ external_knowledge_id: e.target.value, external_knowledge_api_id })}
placeholder={t('dataset.externalKnowledgeIdPlaceholder') ?? ''}
/>
</div>
</form>
)
}
export default ExternalApiSelection

View File

@ -0,0 +1,29 @@
import { RiBookOpenLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
const InfoPanel = () => {
const { t } = useTranslation()
return (
<div className='flex w-[360px] pt-[108px] pb-2 pr-8 flex-col items-start'>
<div className='flex min-w-[240px] p-6 flex-col items-start gap-3 self-stretch rounded-xl bg-background-section'>
<div className='flex p-1 w-10 h-10 justify-center items-center gap-2 flex-grow self-stretch rounded-lg border-0.5 border-components-card-border bg-components-card-bg'>
<RiBookOpenLine className='w-5 h-5 text-text-accent' />
</div>
<p className='flex flex-col items-start gap-2 self-stretch'>
<span className='self-stretch text-text-secondary system-xl-semibold'>
{t('dataset.connectDatasetIntro.title')}
</span>
<span className='self-stretch text-text-tertiary system-sm-regular'>
{t('dataset.connectDatasetIntro.content')}
</span>
<a className='self-stretch text-text-accent system-sm-regular' href='www.google.com' target='_blank' rel="noopener noreferrer">
{t('dataset.connectDatasetIntro.learnMore')}
</a>
</p>
</div>
</div>
)
}
export default InfoPanel

View File

@ -0,0 +1,85 @@
import React, { useEffect, useState } from 'react'
import { RiBookOpenLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
type KnowledgeBaseInfoProps = {
name: string
description: string
onChange: (data: { name?: string; description?: string }) => void
}
const KnowledgeBaseInfo: React.FC<KnowledgeBaseInfoProps> = ({ name: initialName, description: initialDescription, onChange }) => {
const { t } = useTranslation()
const [name, setName] = useState(initialName)
const [description, setDescription] = useState(initialDescription)
useEffect(() => {
const savedName = localStorage.getItem('knowledgeBaseName')
const savedDescription = localStorage.getItem('knowledgeBaseDescription')
if (savedName)
setName(savedName)
if (savedDescription)
setDescription(savedDescription)
onChange({ name: savedName || initialName, description: savedDescription || initialDescription })
}, [])
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newName = e.target.value
setName(newName)
localStorage.setItem('knowledgeBaseName', newName)
onChange({ name: newName })
}
const handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newDescription = e.target.value
setDescription(newDescription)
localStorage.setItem('knowledgeBaseDescription', newDescription)
onChange({ description: newDescription })
}
return (
<form className='flex flex-col gap-4 self-stretch'>
<div className='flex flex-col gap-4 self-stretch'>
<div className='flex flex-col gap-1 self-stretch'>
<div className='flex flex-col justify-center self-stretch'>
<label className='text-text-secondary system-sm-semibold'>{t('dataset.externalKnowledgeName')}</label>
</div>
<Input
value={name}
onChange={handleNameChange}
placeholder={t('dataset.externalKnowledgeNamePlaceholder') ?? ''}
/>
</div>
<div className='flex flex-col gap-1 self-stretch'>
<div className='flex flex-col justify-center self-stretch'>
<label className='text-text-secondary system-sm-semibold'>{t('dataset.externalKnowledgeDescription')}</label>
</div>
<div className='flex flex-col gap-1 self-stretch'>
<Input
value={description}
onChange={handleDescriptionChange}
placeholder={t('dataset.externalKnowledgeDescriptionPlaceholder') ?? ''}
className='flex h-20 p-2 self-stretch items-start'
/>
<div className='flex py-0.5 gap-1 self-stretch'>
<div className='flex p-0.5 items-center gap-2'>
<RiBookOpenLine className='w-3 h-3 text-text-tertiary' />
</div>
<div className='flex-grow text-text-tertiary body-xs-regular'>{t('dataset.learnHowToWriteGoodKnowledgeDescription')}</div>
</div>
</div>
</div>
</div>
</form>
)
}
export const clearKnowledgeBaseInfo = () => {
localStorage.removeItem('knowledgeBaseName')
localStorage.removeItem('knowledgeBaseDescription')
}
export default KnowledgeBaseInfo

View File

@ -0,0 +1,45 @@
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import TopKItem from '@/app/components/base/param-item/top-k-item'
import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item'
type RetrievalSettingsProps = {
topK: number
scoreThreshold: number
onChange: (data: { top_k?: number; score_threshold?: number }) => void
}
const RetrievalSettings: FC<RetrievalSettingsProps> = ({ topK, scoreThreshold, onChange }) => {
const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(false)
const { t } = useTranslation()
return (
<div className='flex flex-col gap-2 self-stretch'>
<div className='flex h-7 pt-1 flex-col gap-2 self-stretch'>
<label className='text-text-secondary system-sm-semibold'>{t('dataset.retrievalSettings')}</label>
</div>
<div className='flex gap-4 self-stretch'>
<div className='flex flex-col gap-1 flex-grow'>
<TopKItem
className='grow'
value={topK}
onChange={(_key, v) => onChange({ top_k: v })}
enable={true}
/>
</div>
<div className='flex flex-col gap-1 flex-grow'>
<ScoreThresholdItem
className='grow'
value={scoreThreshold}
onChange={(_key, v) => onChange({ score_threshold: v })}
enable={scoreThresholdEnabled}
hasSwitch={true}
onSwitchChange={(_key, v) => setScoreThresholdEnabled(v)}
/>
</div>
</div>
</div>
)
}
export default RetrievalSettings

View File

@ -0,0 +1,11 @@
export type CreateKnowledgeBaseReq = {
name: string
description?: string
external_knowledge_api_id: string
provider: 'external'
external_knowledge_id: string
external_retrieval_modal: {
top_k: number
score_threshold: number
}
}

View File

@ -0,0 +1,110 @@
'use client'
import { useCallback, useState } from 'react'
import { useRouter } from 'next/navigation'
import { RiArrowLeftLine, RiArrowRightLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import KnowledgeBaseInfo from './KnowledgeBaseInfo'
import ExternalApiSelection from './ExternalApiSelection'
import RetrievalSettings from './RetrievalSettings'
import InfoPanel from './InfoPanel'
import type { CreateKnowledgeBaseReq } from './declarations'
import Divider from '@/app/components/base/divider'
import Button from '@/app/components/base/button'
type ExternalKnowledgeBaseCreateProps = {
onConnect: (formValue: CreateKnowledgeBaseReq) => void
}
const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> = ({ onConnect }) => {
const { t } = useTranslation()
const router = useRouter()
const [formData, setFormData] = useState<CreateKnowledgeBaseReq>({
name: '',
description: '',
external_knowledge_api_id: '',
external_knowledge_id: '',
external_retrieval_modal: {
top_k: 2,
score_threshold: 0.5,
},
provider: 'external',
})
const navBackHandle = useCallback(() => {
router.replace('/datasets')
}, [router])
const handleFormChange = (newData: CreateKnowledgeBaseReq) => {
setFormData(newData)
console.log(formData)
}
const isFormValid = formData.name !== ''
&& formData.external_knowledge_api_id !== ''
&& formData.external_knowledge_id !== ''
&& formData.external_retrieval_modal.top_k !== undefined
&& formData.external_retrieval_modal.score_threshold !== undefined
return (
<div className='flex flex-col flex-grow self-stretch rounded-t-2xl border-t border-effects-highlight bg-components-panel-bg'>
<div className='flex justify-center flex-grow self-stretch'>
<div className='flex w-full max-w-[960px] px-14 py-0 flex-col items-center'>
<div className='flex w-full max-w-[640px] pt-6 pb-8 flex-col grow items-center gap-4'>
<div className='relative flex py-2 items-center gap-2 self-stretch'>
<div className='flex-grow text-text-primary system-xl-semibold'>{t('dataset.connectDataset')}</div>
<Button
className='flex w-8 h-8 p-2 items-center justify-center absolute left-[-44px] top-1 rounded-full'
variant='tertiary'
onClick={navBackHandle}
>
<RiArrowLeftLine className='w-4 h-4 text-text-tertiary' />
</Button>
</div>
<KnowledgeBaseInfo
name={formData.name}
description={formData.description ?? ''}
onChange={data => handleFormChange({
...formData,
...data,
})}
/>
<Divider />
<ExternalApiSelection
external_knowledge_api_id={formData.external_knowledge_api_id}
external_knowledge_id={formData.external_knowledge_id}
onChange={data => handleFormChange({
...formData,
...data,
})}
/>
<RetrievalSettings
topK={formData.external_retrieval_modal.top_k}
scoreThreshold={formData.external_retrieval_modal.score_threshold}
onChange={data => handleFormChange({
...formData,
external_retrieval_modal: {
...formData.external_retrieval_modal,
...data,
},
})}
/>
<div className='flex py-2 justify-end items-center gap-2 self-stretch'>
<Button variant='secondary' onClick={navBackHandle}>
<div className='text-components-button-secondary-text system-sm-medium'>{t('dataset.externalKnowledgeForm.cancel')}</div>
</Button>
<Button variant='primary' onClick={() => onConnect(formData)} disabled={!isFormValid}>
<div className='text-components-button-primary-text system-sm-medium'>{t('dataset.externalKnowledgeForm.connect')}</div>
<RiArrowRightLine className='w-4 h-4 text-components-button-primary-text' />
</Button>
</div>
</div>
</div>
<InfoPanel />
</div>
</div>
)
}
export default ExternalKnowledgeBaseCreate

View File

@ -0,0 +1,46 @@
'use client'
import { createContext, useContext, useMemo } from 'react'
import type { FC, ReactNode } from 'react'
import useSWR from 'swr'
import type { ExternalAPIItem, ExternalAPIListResponse } from '@/models/datasets'
import { fetchExternalAPIList } from '@/service/datasets'
type ExternalKnowledgeApiContextType = {
externalKnowledgeApiList: ExternalAPIItem[]
mutateExternalKnowledgeApis: () => Promise<ExternalAPIListResponse | undefined>
isLoading: boolean
}
const ExternalKnowledgeApiContext = createContext<ExternalKnowledgeApiContextType | undefined>(undefined)
export type ExternalKnowledgeApiProviderProps = {
children: ReactNode
}
export const ExternalKnowledgeApiProvider: FC<ExternalKnowledgeApiProviderProps> = ({ children }) => {
const { data, mutate: mutateExternalKnowledgeApis, isLoading } = useSWR<ExternalAPIListResponse>(
{ url: '/datasets/external-knowledge-api' },
fetchExternalAPIList,
)
const contextValue = useMemo<ExternalKnowledgeApiContextType>(() => ({
externalKnowledgeApiList: data?.data || [],
mutateExternalKnowledgeApis,
isLoading,
}), [data, mutateExternalKnowledgeApis, isLoading])
return (
<ExternalKnowledgeApiContext.Provider value={contextValue}>
{children}
</ExternalKnowledgeApiContext.Provider>
)
}
export const useExternalKnowledgeApi = () => {
const context = useContext(ExternalKnowledgeApiContext)
if (context === undefined)
throw new Error('useExternalKnowledgeApi must be used within a ExternalKnowledgeApiProvider')
return context
}

View File

@ -24,6 +24,7 @@ import type {
ApiBasedExtension,
ExternalDataTool,
} from '@/models/common'
import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations'
import ModelLoadBalancingEntryModal from '@/app/components/header/account-setting/model-provider-page/model-modal/model-load-balancing-entry-modal'
import type { ModelLoadBalancingModalProps } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'
import ModelLoadBalancingModal from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'
@ -33,7 +34,10 @@ export type ModalState<T> = {
onCancelCallback?: () => void
onSaveCallback?: (newPayload: T) => void
onRemoveCallback?: (newPayload: T) => void
onEditCallback?: (newPayload: T) => void
onValidateBeforeSaveCallback?: (newPayload: T) => boolean
isEditMode?: boolean
datasetBindings?: { id: string; name: string }[]
}
export type ModelModalType = {
@ -53,7 +57,7 @@ export type ModalContextState = {
setShowPricingModal: () => void
setShowAnnotationFullModal: () => void
setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>>
setShowExternalAPIModal: () => void
setShowExternalKnowledgeAPIModal: Dispatch<SetStateAction<ModalState<CreateExternalAPIReq> | null>>
setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>>
setShowModelLoadBalancingEntryModal: Dispatch<SetStateAction<ModalState<LoadBalancingEntryModalType> | null>>
}
@ -65,7 +69,7 @@ const ModalContext = createContext<ModalContextState>({
setShowPricingModal: () => { },
setShowAnnotationFullModal: () => { },
setShowModelModal: () => { },
setShowExternalAPIModal: () => { },
setShowExternalKnowledgeAPIModal: () => { },
setShowModelLoadBalancingModal: () => { },
setShowModelLoadBalancingEntryModal: () => { },
})
@ -89,7 +93,7 @@ export const ModalContextProvider = ({
const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null)
const [showExternalDataToolModal, setShowExternalDataToolModal] = useState<ModalState<ExternalDataTool> | null>(null)
const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null)
const [showExternalAPIModal, setShowExternalAPIModal] = useState(false)
const [showExternalKnowledgeAPIModal, setShowExternalKnowledgeAPIModal] = useState<ModalState<CreateExternalAPIReq> | null>(null)
const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState<ModelLoadBalancingModalProps | null>(null)
const [showModelLoadBalancingEntryModal, setShowModelLoadBalancingEntryModal] = useState<ModalState<LoadBalancingEntryModalType> | null>(null)
const searchParams = useSearchParams()
@ -126,17 +130,23 @@ export const ModalContextProvider = ({
setShowModelModal(null)
}, [showModelModal])
// const handleCancelExternalApiModal = useCallback(() => {
// setShowExternalAPIModal(null)
// if (showExternalAPIModal?.onCancelCallback)
// showExternalAPIModal.onCancelCallback()
// }, [showExternalAPIModal])
const handleCancelExternalApiModal = useCallback(() => {
setShowExternalKnowledgeAPIModal(null)
if (showExternalKnowledgeAPIModal?.onCancelCallback)
showExternalKnowledgeAPIModal.onCancelCallback()
}, [showExternalKnowledgeAPIModal])
// const handleSaveExternalApiModal = useCallback(() => {
// if (showExternalAPIModal?.onSaveCallback)
// showExternalAPIModal.onSaveCallback(null)
// setShowExternalAPIModal(null)
// }, [showExternalAPIModal])
const handleSaveExternalApiModal = useCallback(async (updatedFormValue: CreateExternalAPIReq) => {
if (showExternalKnowledgeAPIModal?.onSaveCallback)
showExternalKnowledgeAPIModal.onSaveCallback(updatedFormValue)
setShowExternalKnowledgeAPIModal(null)
}, [showExternalKnowledgeAPIModal])
const handleEditExternalApiModal = useCallback(async (updatedFormValue: CreateExternalAPIReq) => {
if (showExternalKnowledgeAPIModal?.onEditCallback)
showExternalKnowledgeAPIModal.onEditCallback(updatedFormValue)
setShowExternalKnowledgeAPIModal(null)
}, [showExternalKnowledgeAPIModal])
const handleCancelModelLoadBalancingEntryModal = useCallback(() => {
showModelLoadBalancingEntryModal?.onCancelCallback?.()
@ -189,7 +199,7 @@ export const ModalContextProvider = ({
setShowPricingModal: () => setShowPricingModal(true),
setShowAnnotationFullModal: () => setShowAnnotationFullModal(true),
setShowModelModal,
setShowExternalAPIModal: () => setShowExternalAPIModal(true),
setShowExternalKnowledgeAPIModal,
setShowModelLoadBalancingModal,
setShowModelLoadBalancingEntryModal,
}}>
@ -263,10 +273,14 @@ export const ModalContextProvider = ({
)
}
{
!!showExternalAPIModal && (
!!showExternalKnowledgeAPIModal && (
<ExternalAPIModal
show={showExternalAPIModal}
onHide={() => setShowExternalAPIModal(false)}
data={showExternalKnowledgeAPIModal.payload}
datasetBindings={showExternalKnowledgeAPIModal.datasetBindings ?? []}
onSave={handleSaveExternalApiModal}
onCancel={handleCancelExternalApiModal}
onEdit={handleEditExternalApiModal}
isEditMode={showExternalKnowledgeAPIModal.isEditMode ?? false}
/>
)
}

View File

@ -2,20 +2,47 @@ const translation = {
knowledge: 'Knowledge',
externalAPI: 'External API',
externalAPIPanelTitle: 'External Knowledge API',
externalKnowledgeId: 'External Knowledge ID',
externalKnowledgeName: 'External Knowledge Name',
externalKnowledgeDescription: 'Knowledge Description',
externalKnowledgeIdPlaceholder: 'Please enter the Knowledge ID',
externalKnowledgeNamePlaceholder: 'Please enter the name of the knowledge base',
externalKnowledgeDescriptionPlaceholder: 'Describe what\'s in this Knowledge Base (optional)',
learnHowToWriteGoodKnowledgeDescription: 'Learn how to write a good knowledge description',
externalAPIPanelDescription: 'The external knowledge API is used to connect to a knowledge base outside of Dify and retrieve knowledge from that knowledge base.',
externalAPIPanelDocumentation: 'Learn how to create an external API',
documentCount: ' docs',
wordCount: ' k words',
appCount: ' linked apps',
createDataset: 'Create Knowledge',
createExternalAPI: 'Add an External API',
createExternalAPIFormTitle: 'Add an External Knowledge API',
createExternalAPI: 'Add an External Knowledge API',
editExternalAPIFormTitle: 'Edit the External Knowledge API',
editExternalAPITooltipTitle: 'LINKED KNOWLEDGE',
editExternalAPIConfirmWarningContent: {
front: 'This External Knowledge API is linked to',
end: 'external knowledge, and this modification will be applied to all of them. Are you sure you want to save this change?',
},
editExternalAPIFormWarning: {
front: 'This External API is linked to',
end: 'external knowledge',
},
connectDataset: 'Connect to an external knowledge base',
deleteExternalAPIConfirmWarningContent: {
title: {
front: 'Delete',
end: '?',
},
content: {
front: 'This External Knowledge API is linked to',
end: 'external knowledge. Deleting this API will invalidate all of them. Are you sure you want to delete this API?',
},
noConnectionContent: 'Are you sure to delete this API?',
},
connectDataset: 'Connect to an External Knowledge Base',
connectDatasetIntro: {
title: 'How to Connect to an External Knowledge Base',
content: 'To connect to an external knowledge base, you need to create an external API first. Please read carefully and refer to learn how to create an external API. Then find the corresponding knowledge ID and fill it in the form on the left. If all the information is correct, it will automatically jump to the retrieval test in the knowledge base after clicking the connect button.',
learnMore: 'Learn More',
},
createDatasetIntro: 'Import your own text data or write data in real-time via Webhook for LLM context enhancement.',
deleteDatasetConfirmTitle: 'Delete this Knowledge?',
deleteDatasetConfirmContent:
@ -34,6 +61,10 @@ const translation = {
unavailableTip: 'Embedding model is not available, the default embedding model needs to be configured',
datasets: 'KNOWLEDGE',
datasetsApi: 'API ACCESS',
externalKnowledgeForm: {
connect: 'Connect',
cancel: 'Cancel',
},
externalAPIForm: {
name: 'Name',
endpoint: 'API Endpoint',

View File

@ -2,19 +2,46 @@ const translation = {
knowledge: '知识库',
externalAPI: '外部 API',
externalAPIPanelTitle: '外部知识库 API',
externalKnowledgeId: '外部知识库 ID',
externalKnowledgeName: '外部知识库名称',
externalKnowledgeDescription: '知识库描述',
externalKnowledgeIdPlaceholder: '请输入外部知识库 ID',
externalKnowledgeNamePlaceholder: '请输入外部知识库名称',
externalKnowledgeDescriptionPlaceholder: '描述知识库内容(可选)',
learnHowToWriteGoodKnowledgeDescription: '了解如何编写良好的知识库描述',
externalAPIPanelDescription: '外部知识库 API 用于连接到 Dify 之外的知识库并从中检索知识。',
externalAPIPanelDocumentation: '了解如何创建外部 API',
documentCount: ' 文档',
wordCount: ' 千字符',
appCount: ' 关联应用',
createDataset: '创建知识库',
createExternalAPI: '添加外部 API',
createExternalAPIFormTitle: '添加外部知识库 API',
createExternalAPI: '添加外部知识库 API',
editExternalAPIFormTitle: '编辑外部知识库 API',
editExternalAPITooltipTitle: '个关联知识库',
editExternalAPIConfirmWarningContent: {
front: '此外部知识库 API 已链接到',
end: '个外部知识库,此修改将应用于所有这些知识库。您确定要保存此更改吗?',
},
editExternalAPIFormWarning: {
front: '此外部 API 已链接到',
end: '外部知识库',
},
deleteExternalAPIConfirmWarningContent: {
title: {
front: '删除',
end: '',
},
content: {
front: '此外部知识库 API 已链接到',
end: '个外部知识库。删除此 API 将使所有这些知识库失效。您确定要删除此 API 吗?',
},
noConnectionContent: '您确定要删除此 API 吗?',
},
connectDatasetIntro: {
title: '如何连接到外部知识库',
content: '要连接到外部知识库,您需要先创建一个外部 API。请仔细阅读并参考如何创建外部 API。然后找到相应的知识 ID 并将其填写在左侧表单中。如果所有信息都正确,点击连接按钮后会自动跳转到知识库的检索测试。',
learnMore: '了解更多',
},
connectDataset: '连接外部知识库',
createDatasetIntro: '导入您自己的文本数据或通过 Webhook 实时写入数据以增强 LLM 的上下文。',
deleteDatasetConfirmTitle: '要删除知识库吗?',
@ -34,6 +61,10 @@ const translation = {
unavailableTip: '由于 embedding 模型不可用,需要配置默认 embedding 模型',
datasets: '知识库',
datasetsApi: 'API',
externalKnowledgeForm: {
connect: '连接',
cancel: '取消',
},
externalAPIForm: {
name: '名称',
endpoint: 'API 端点',

View File

@ -25,6 +25,7 @@ export type DataSet = {
app_count: number
document_count: number
word_count: number
provider: string
embedding_model: string
embedding_model_provider: string
embedding_available: boolean
@ -42,15 +43,39 @@ export type ExternalAPIItem = {
settings: {
endpoint: string
api_key: string
document_retrieval_setting: {
top_k: number
score_threshold: number
}
}
dataset_bindings: { id: string; name: string }[]
created_by: string
created_at: string
}
export type ExternalKnowledgeItem = {
id: string
name: string
description: string | null
provider: 'external'
permission: DatasetPermission
data_source_type: null
indexing_technique: null
app_count: number
document_count: number
word_count: number
created_by: string
created_at: string
updated_by: string
updated_at: string
tags: Tag[]
}
export type ExternalAPIDeleteResponse = {
result: 'success' | 'error'
}
export type ExternalAPIUsage = {
is_using: boolean
count: number
}
export type CustomFile = File & {
id?: string
extension?: string

View File

@ -8,8 +8,11 @@ import type {
DocumentDetailResponse,
DocumentListResponse,
ErrorDocsResponse,
ExternalAPIDeleteResponse,
ExternalAPIItem,
ExternalAPIListResponse,
ExternalAPIUsage,
ExternalKnowledgeItem,
FileIndexingEstimateResponse,
HitTestingRecordsResponse,
HitTestingResponse,
@ -25,6 +28,8 @@ import type {
SegmentsResponse,
createDocumentResponse,
} from '@/models/datasets'
import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external-knowledge-base/create/declarations'
import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations.ts'
import type { CommonResponse, DataSourceNotionWorkspace } from '@/models/common'
import type {
ApiKeysListResponse,
@ -84,12 +89,32 @@ export const deleteDataset: Fetcher<DataSet, string> = (datasetID) => {
return del<DataSet>(`/datasets/${datasetID}`)
}
export const fetchExternalAPIList: Fetcher<ExternalAPIListResponse, { url: string; params: { page: number; limit: number } }> = ({ url, params }) => {
return get<ExternalAPIListResponse>(url, { params })
export const fetchExternalAPIList: Fetcher<ExternalAPIListResponse, { url: string }> = ({ url }) => {
return get<ExternalAPIListResponse>(url)
}
export const createExternalAPI: Fetcher<ExternalAPIItem, { body: ExternalAPIItem }> = ({ body }) => {
return post<ExternalAPIItem>('/datasets/api-template', { body })
export const fetchExternalAPI: Fetcher<ExternalAPIItem, { apiTemplateId: string }> = ({ apiTemplateId }) => {
return get<ExternalAPIItem>(`/datasets/external-knowledge-api/${apiTemplateId}`)
}
export const updateExternalAPI: Fetcher<ExternalAPIItem, { apiTemplateId: string; body: ExternalAPIItem }> = ({ apiTemplateId, body }) => {
return patch<ExternalAPIItem>(`/datasets/external-knowledge-api/${apiTemplateId}`, { body })
}
export const deleteExternalAPI: Fetcher<ExternalAPIDeleteResponse, { apiTemplateId: string }> = ({ apiTemplateId }) => {
return del<ExternalAPIDeleteResponse>(`/datasets/external-knowledge-api/${apiTemplateId}`)
}
export const checkUsageExternalAPI: Fetcher<ExternalAPIUsage, { apiTemplateId: string }> = ({ apiTemplateId }) => {
return get<ExternalAPIUsage>(`/datasets/external-knowledge-api/${apiTemplateId}/use-check`)
}
export const createExternalAPI: Fetcher<ExternalAPIItem, { body: CreateExternalAPIReq }> = ({ body }) => {
return post<ExternalAPIItem>('/datasets/external-knowledge-api', { body })
}
export const createExternalKnowledgeBase: Fetcher<ExternalKnowledgeItem, { body: CreateKnowledgeBaseReq }> = ({ body }) => {
return post<ExternalKnowledgeItem>('/datasets/external', { body })
}
export const fetchDefaultProcessRule: Fetcher<ProcessRuleResponse, { url: string }> = ({ url }) => {