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

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