feat: external knowledge api crud frontend & connect external knowledge base
parent
d6c604a356
commit
cfa4825073
@ -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
|
||||||
@ -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
|
||||||
@ -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 |
@ -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"
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -1,2 +1,3 @@
|
|||||||
|
export { default as Corner } from './Corner'
|
||||||
export { default as Star04 } from './Star04'
|
export { default as Star04 } from './Star04'
|
||||||
export { default as Star06 } from './Star06'
|
export { default as Star06 } from './Star06'
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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]
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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]
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue