feat: Use Tree to display the knowledge base list on the search page #2247 (#2385)

### What problem does this PR solve?

feat: Use Tree to display the knowledge base list on the search page
#2247
### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-09-12 17:23:32 +08:00 committed by GitHub
parent f8e9a0590f
commit 68d0210e92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 134 additions and 60 deletions

View File

@ -21,6 +21,7 @@ export interface IKnowledge {
update_date: string; update_date: string;
update_time: number; update_time: number;
vector_similarity_weight: number; vector_similarity_weight: number;
embd_id: string;
} }
export interface Parserconfig { export interface Parserconfig {

View File

@ -27,6 +27,7 @@
.searchSide { .searchSide {
position: relative; position: relative;
:global(.ant-layout-sider-children) { :global(.ant-layout-sider-children) {
height: auto; height: auto;
} }
@ -42,9 +43,12 @@
height: 100%; height: 100%;
} }
.list { .list {
padding-top: 10px;
width: 100%; width: 100%;
height: calc(100vh - 152px); // height: 100%;
height: calc(100vh - 76px);
overflow: auto; overflow: auto;
background-color: transparent;
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
@ -53,7 +57,10 @@
width: 100%; width: 100%;
} }
.knowledgeName { .knowledgeName {
width: 130px; width: 116px;
}
.embeddingId {
width: 170px;
} }
} }

View File

@ -4,7 +4,10 @@ import IndentedTree from '@/components/indented-tree/indented-tree';
import PdfDrawer from '@/components/pdf-drawer'; import PdfDrawer from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks'; import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import RetrievalDocuments from '@/components/retrieval-documents'; import RetrievalDocuments from '@/components/retrieval-documents';
import { useSelectTestingResult } from '@/hooks/knowledge-hooks'; import {
useNextFetchKnowledgeList,
useSelectTestingResult,
} from '@/hooks/knowledge-hooks';
import { useGetPaginationWithRouter } from '@/hooks/logic-hooks'; import { useGetPaginationWithRouter } from '@/hooks/logic-hooks';
import { IReference } from '@/interfaces/database/chat'; import { IReference } from '@/interfaces/database/chat';
import { import {
@ -35,6 +38,11 @@ const SearchPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [checkedList, setCheckedList] = useState<string[]>([]); const [checkedList, setCheckedList] = useState<string[]>([]);
const { chunks, total } = useSelectTestingResult(); const { chunks, total } = useSelectTestingResult();
const { list: knowledgeList } = useNextFetchKnowledgeList();
const checkedWithoutEmbeddingIdList = useMemo(() => {
return checkedList.filter((x) => knowledgeList.some((y) => y.id === x));
}, [checkedList, knowledgeList]);
const { const {
sendQuestion, sendQuestion,
handleClickRelatedQuestion, handleClickRelatedQuestion,
@ -50,7 +58,7 @@ const SearchPage = () => {
loading, loading,
isFirstRender, isFirstRender,
selectedDocumentIds, selectedDocumentIds,
} = useSendQuestion(checkedList); } = useSendQuestion(checkedWithoutEmbeddingIdList);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer(); useClickDrawer();
const imgUrl = useFetchBackgroundImage(); const imgUrl = useFetchBackgroundImage();
@ -79,7 +87,7 @@ const SearchPage = () => {
onSearch={sendQuestion} onSearch={sendQuestion}
size="large" size="large"
loading={sendingLoading} loading={sendingLoading}
disabled={checkedList.length === 0} disabled={checkedWithoutEmbeddingIdList.length === 0}
className={isFirstRender ? styles.globalInput : styles.partialInput} className={isFirstRender ? styles.globalInput : styles.partialInput}
/> />
); );
@ -92,7 +100,7 @@ const SearchPage = () => {
> >
<SearchSidebar <SearchSidebar
isFirstRender={isFirstRender} isFirstRender={isFirstRender}
checkedList={checkedList} checkedList={checkedWithoutEmbeddingIdList}
setCheckedList={setCheckedList} setCheckedList={setCheckedList}
></SearchSidebar> ></SearchSidebar>
<Layout className={isFirstRender ? styles.mainLayout : ''}> <Layout className={isFirstRender ? styles.mainLayout : ''}>

View File

@ -1,9 +1,7 @@
import { useNextFetchKnowledgeList } from '@/hooks/knowledge-hooks'; import { useNextFetchKnowledgeList } from '@/hooks/knowledge-hooks';
import { UserOutlined } from '@ant-design/icons'; import { UserOutlined } from '@ant-design/icons';
import type { CheckboxProps } from 'antd'; import type { TreeDataNode, TreeProps } from 'antd';
import { Avatar, Checkbox, Layout, List, Space, Typography } from 'antd'; import { Avatar, Layout, Space, Spin, Tree, Typography } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { CheckboxValueType } from 'antd/es/checkbox/Group';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
Dispatch, Dispatch,
@ -11,6 +9,7 @@ import {
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useState,
} from 'react'; } from 'react';
import styles from './index.less'; import styles from './index.less';
@ -29,30 +28,109 @@ const SearchSidebar = ({
setCheckedList, setCheckedList,
}: IProps) => { }: IProps) => {
const { list, loading } = useNextFetchKnowledgeList(); const { list, loading } = useNextFetchKnowledgeList();
const ids = useMemo(() => list.map((x) => x.id), [list]);
const checkAll = list.length === checkedList.length; const groupedList = useMemo(() => {
return list.reduce((pre: TreeDataNode[], cur) => {
const parentItem = pre.find((x) => x.key === cur.embd_id);
const childItem: TreeDataNode = {
title: cur.name,
key: cur.id,
isLeaf: true,
};
if (parentItem) {
parentItem.children?.push(childItem);
} else {
pre.push({
title: cur.embd_id,
key: cur.embd_id,
isLeaf: false,
children: [childItem],
});
}
const indeterminate = return pre;
checkedList.length > 0 && checkedList.length < list.length; }, []);
}, [list]);
const onChange = useCallback( const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
(list: CheckboxValueType[]) => { const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
setCheckedList(list as string[]); const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);
},
[setCheckedList], const onExpand: TreeProps['onExpand'] = (expandedKeysValue) => {
// if not set autoExpandParent to false, if children expanded, parent can not collapse.
// or, you can remove all expanded children keys.
setExpandedKeys(expandedKeysValue);
setAutoExpandParent(false);
};
const onCheck: TreeProps['onCheck'] = (checkedKeysValue, info) => {
console.log('onCheck', checkedKeysValue, info);
const currentCheckedKeysValue = checkedKeysValue as string[];
let nextSelectedKeysValue: string[] = [];
const { isLeaf, checked, key, children } = info.node;
if (isLeaf) {
const item = list.find((x) => x.id === key);
if (!checked) {
const embeddingIds = currentCheckedKeysValue
.filter((x) => list.some((y) => y.id === x))
.map((x) => list.find((y) => y.id === x)?.embd_id);
if (embeddingIds.some((x) => x !== item?.embd_id)) {
nextSelectedKeysValue = [key as string];
} else {
nextSelectedKeysValue = currentCheckedKeysValue;
}
} else {
nextSelectedKeysValue = currentCheckedKeysValue;
}
} else {
if (!checked) {
nextSelectedKeysValue = [
key as string,
...(children?.map((x) => x.key as string) ?? []),
];
} else {
nextSelectedKeysValue = [];
}
}
setCheckedList(nextSelectedKeysValue);
};
const onSelect: TreeProps['onSelect'] = (selectedKeysValue, info) => {
console.log('onSelect', info);
setSelectedKeys(selectedKeysValue);
};
const renderTitle = useCallback(
(node: TreeDataNode) => {
const item = list.find((x) => x.id === node.key);
return (
<Space>
{node.isLeaf && (
<Avatar size={24} icon={<UserOutlined />} src={item?.avatar} />
)}
<Typography.Text
ellipsis={{ tooltip: node.title as string }}
className={node.isLeaf ? styles.knowledgeName : styles.embeddingId}
>
{node.title as string}
</Typography.Text>
</Space>
); );
const onCheckAllChange: CheckboxProps['onChange'] = useCallback(
(e: CheckboxChangeEvent) => {
setCheckedList(e.target.checked ? ids : []);
}, },
[ids, setCheckedList], [list],
); );
useEffect(() => { useEffect(() => {
setCheckedList(ids); const firstGroup = groupedList[0]?.children?.map((x) => x.key as string);
}, [ids, setCheckedList]); if (firstGroup) {
setCheckedList(firstGroup);
}
setExpandedKeys(groupedList.map((x) => x.key));
}, [groupedList, setExpandedKeys, setCheckedList]);
return ( return (
<Sider <Sider
@ -62,41 +140,21 @@ const SearchSidebar = ({
theme={'light'} theme={'light'}
width={240} width={240}
> >
<Checkbox <Spin spinning={loading}>
className={styles.modelForm} <Tree
indeterminate={indeterminate}
onChange={onCheckAllChange}
checked={checkAll}
>
All
</Checkbox>
<Checkbox.Group
className={styles.checkGroup}
onChange={onChange}
value={checkedList}
>
<List
bordered
dataSource={list}
className={styles.list} className={styles.list}
loading={loading} checkable
renderItem={(item) => ( onExpand={onExpand}
<List.Item> expandedKeys={expandedKeys}
<Checkbox value={item.id} className={styles.checkbox}> autoExpandParent={autoExpandParent}
<Space> onCheck={onCheck}
<Avatar size={30} icon={<UserOutlined />} src={item.avatar} /> checkedKeys={checkedList}
<Typography.Text onSelect={onSelect}
ellipsis={{ tooltip: item.name }} selectedKeys={selectedKeys}
className={styles.knowledgeName} treeData={groupedList}
> titleRender={renderTitle}
{item.name}
</Typography.Text>
</Space>
</Checkbox>
</List.Item>
)}
/> />
</Checkbox.Group> </Spin>
</Sider> </Sider>
); );
}; };