diff --git a/web/src/components/Knowledge/Knowledge.tsx b/web/src/components/Knowledge/Knowledge.tsx new file mode 100644 index 00000000..b1c9b78f --- /dev/null +++ b/web/src/components/Knowledge/Knowledge.tsx @@ -0,0 +1,217 @@ +import { type FC, useRef, useState, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { Space, Button, Flex } from 'antd' + +import knowledgeEmpty from '@/assets/images/application/knowledgeEmpty.svg' +import type { + KnowledgeConfigForm, + KnowledgeConfig, + RerankerConfig, + KnowledgeBase, + KnowledgeModalRef, + KnowledgeConfigModalRef, + KnowledgeGlobalConfigModalRef, +} from './types' +import Empty from '@/components/Empty' +import KnowledgeListModal from './KnowledgeListModal' +import KnowledgeConfigModal from './KnowledgeConfigModal' +import KnowledgeGlobalConfigModal from './KnowledgeGlobalConfigModal' +import Tag from '@/components/Tag' +import { getKnowledgeBaseList } from '@/api/knowledgeBase' +import RbCard from '@/components/RbCard/Card' + +interface KnowledgeProps { + value?: KnowledgeConfig; + onChange?: (config: KnowledgeConfig) => void; + /** 'app' renders inside a Card with empty state; 'workflow' renders inline with dashed add button */ + variant?: 'app' | 'workflow'; +} + +const Knowledge: FC = ({ value = { knowledge_bases: [] }, onChange, variant = 'workflow' }) => { + const { t } = useTranslation() + const knowledgeModalRef = useRef(null) + const knowledgeConfigModalRef = useRef(null) + const knowledgeGlobalConfigModalRef = useRef(null) + const [knowledgeList, setKnowledgeList] = useState([]) + const [editConfig, setEditConfig] = useState({} as KnowledgeConfig) + + useEffect(() => { + if (value && JSON.stringify(value) !== JSON.stringify(editConfig)) { + setEditConfig({ ...(value || {}) }) + const knowledge_bases = [...(value.knowledge_bases || [])] + const basesWithoutName = knowledge_bases.filter(base => !base.name) + if (basesWithoutName.length > 0) { + getKnowledgeBaseList().then(res => { + const fullBases = knowledge_bases.map(base => { + if (!base.name) { + const fullBase = res.items.find((item: any) => item.id === base.kb_id) + return fullBase ? { ...base, ...fullBase } : base + } + return base + }) + setKnowledgeList(fullBases) + }).catch(() => setKnowledgeList(knowledge_bases)) + } else { + setKnowledgeList(knowledge_bases) + } + } + }, [value]) + + const handleKnowledgeConfig = () => knowledgeGlobalConfigModalRef.current?.handleOpen() + const handleAddKnowledge = () => knowledgeModalRef.current?.handleOpen() + + const handleDeleteKnowledge = (id: string) => { + const list = knowledgeList.filter(item => item.id !== id) + setKnowledgeList([...list]) + onChange?.({ ...editConfig, knowledge_bases: [...list] }) + } + + const handleEditKnowledge = (item: KnowledgeBase) => knowledgeConfigModalRef.current?.handleOpen(item) + + const refresh = (values: KnowledgeBase[] | KnowledgeConfigForm | RerankerConfig, type: 'knowledge' | 'knowledgeConfig' | 'rerankerConfig') => { + if (type === 'knowledge') { + let list = [...knowledgeList] + if (list.length > 0) { + (Array.isArray(values) ? values : [values]).forEach(vo => { + const index = list.findIndex(item => item.id === (vo as KnowledgeBase).id) + if (index === -1) list.push(vo as KnowledgeBase) + }) + } else { + list = [...values as KnowledgeBase[]] + } + setKnowledgeList([...list]) + onChange?.({ ...editConfig, knowledge_bases: [...list] }) + } else if (type === 'knowledgeConfig') { + const index = knowledgeList.findIndex(item => item.id === (values as KnowledgeBase).kb_id) + const list = [...knowledgeList] + list[index] = { ...list[index], ...values, config: { ...values as KnowledgeConfigForm } } + setKnowledgeList([...list]) + onChange?.({ ...editConfig, knowledge_bases: [...list] }) + } else if (type === 'rerankerConfig') { + const rerankerValues = values as RerankerConfig + setEditConfig(prev => { + const next = { + ...prev, + ...rerankerValues, + reranker_id: rerankerValues.rerank_model ? rerankerValues.reranker_id : undefined, + reranker_top_k: rerankerValues.rerank_model ? rerankerValues.reranker_top_k : undefined, + } + onChange?.(next) + return next + }) + } + } + + const modals = ( + <> + + + + + ) + + const knowledgeItems = knowledgeList.map(item => { + if (!item.id) return null + return ( + +
+ {item.name} + + {item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')} + +
+ {t('application.contains', { include_count: item.doc_num })} +
+
+ + {variant === 'app' ? ( + <> +
handleEditKnowledge(item)} /> +
handleDeleteKnowledge(item.id)} /> + + ) : ( + <> +
handleEditKnowledge(item)} /> +
handleDeleteKnowledge(item.id)} /> + + )} + + + ) + }) + + if (variant === 'app') { + return ( + +
} + onClick={handleKnowledgeConfig} + >{t('application.globalConfig')} + + + } + headerType="borderless" + headerClassName="rb:h-11.5! rb:py-3! rb:leading-5.5!" + titleClassName="rb:font-[MiSans-Bold] rb:font-bold" + > +
+ {t('application.associatedKnowledgeBase')} +
+ {knowledgeList.length === 0 + ?
+ +
+ : {knowledgeItems} + } + {modals} + + ) + } + + return ( +
+ +
+ * + {t('application.knowledgeBaseAssociation')} +
+
} + className="rb:py-0! rb:px-1! rb:text-[12px]! rb:group rb:gap-0.5!" + size="small" + disabled={knowledgeList.length === 0} + > + {t('application.globalConfig')} + + + + + {knowledgeList.length > 0 && knowledgeItems} + + {modals} +
+ ) +} + +export default Knowledge diff --git a/web/src/components/Knowledge/KnowledgeConfigModal.tsx b/web/src/components/Knowledge/KnowledgeConfigModal.tsx new file mode 100644 index 00000000..c91230ee --- /dev/null +++ b/web/src/components/Knowledge/KnowledgeConfigModal.tsx @@ -0,0 +1,124 @@ +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import { Form, Select, InputNumber, Flex } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { KnowledgeConfigModalRef, KnowledgeBase, KnowledgeConfigForm, RetrieveType } from './types' +import RbModal from '@/components/RbModal' +import RbSlider from '@/components/RbSlider' +import { formatDateTime } from '@/utils/format'; + +const FormItem = Form.Item; + +interface KnowledgeConfigModalProps { + refresh: (values: KnowledgeConfigForm, type: 'knowledgeConfig') => void; +} +const retrieveTypes: RetrieveType[] = ['participle', 'semantic', 'hybrid', 'graph'] + +const KnowledgeConfigModal = forwardRef(({ refresh }, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [data, setData] = useState(null); + const values = Form.useWatch([], form); + + const handleClose = () => { + setVisible(false); + form.resetFields(); + setData(null) + }; + + const handleOpen = (data: KnowledgeBase) => { + form.setFieldsValue({ + retrieve_type: data?.config?.retrieve_type || retrieveTypes[0], + kb_id: data.id, + top_k: data?.config?.top_k || 5, + similarity_threshold: data?.config?.similarity_threshold || 0.5, + vector_similarity_weight: data?.config?.vector_similarity_weight || 0.5, + ...(data || {}), + ...(data?.config || {}), + }) + setData({...data}) + setVisible(true); + }; + + const handleSave = () => { + form.validateFields() + .then(() => { + refresh(values, 'knowledgeConfig') + handleClose() + }) + .catch((err) => console.log('err', err)); + } + + useImperativeHandle(ref, () => ({ handleOpen, handleClose })); + + useEffect(() => { + if (values?.retrieve_type) { + const fieldsToReset = Object.keys(values).filter(key => + key !== 'kb_id' && key !== 'retrieve_type' && key !== 'top_k' + ) as (keyof KnowledgeConfigForm)[]; + form.resetFields(fieldsToReset); + } + }, [values?.retrieve_type]) + + return ( + +
+ {data && ( + +
+ {data.name} +
{t('application.contains', {include_count: data.doc_num})}
+
+
{formatDateTime(data.updated_at, 'YYYY-MM-DD HH:mm:ss')}
+
+ )} +