Преглед изворни кода

fix: filter knowledge list by keywords and clear the selected file list after the file is uploaded successfully and add ellipsis pattern to chunk list (#628)

### What problem does this PR solve?

#627 
fix: filter knowledge list by keywords
fix: clear the selected file list after the file is uploaded
successfully
feat: add ellipsis pattern to chunk list

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
tags/v0.5.0
balibabu пре 1 година
родитељ
комит
9703633a57
No account linked to committer's email address

+ 2
- 0
web/src/components/chunk-method-modal/index.tsx Прегледај датотеку

visible, visible,
documentExtension, documentExtension,
parserConfig, parserConfig,
loading,
}) => { }) => {
const { parserList, handleChange, selectedTag } = useFetchParserListOnMount( const { parserList, handleChange, selectedTag } = useFetchParserListOnMount(
documentId, documentId,
onOk={handleOk} onOk={handleOk}
onCancel={hideModal} onCancel={hideModal}
afterClose={afterClose} afterClose={afterClose}
confirmLoading={loading}
> >
<Space size={[0, 8]} wrap> <Space size={[0, 8]} wrap>
<Form.Item label={t('chunkMethod')} className={styles.chunkMethod}> <Form.Item label={t('chunkMethod')} className={styles.chunkMethod}>

+ 10
- 4
web/src/components/file-upload-modal/index.tsx Прегледај датотеку

const [fileList, setFileList] = useState<UploadFile[]>([]); const [fileList, setFileList] = useState<UploadFile[]>([]);
const [directoryFileList, setDirectoryFileList] = useState<UploadFile[]>([]); const [directoryFileList, setDirectoryFileList] = useState<UploadFile[]>([]);


const clearFileList = () => {
setFileList([]);
setDirectoryFileList([]);
};

const onOk = async () => { const onOk = async () => {
const ret = await onFileUploadOk?.([...fileList, ...directoryFileList]); const ret = await onFileUploadOk?.([...fileList, ...directoryFileList]);
if (ret !== undefined && ret === 0) {
setFileList([]);
setDirectoryFileList([]);
}
return ret; return ret;
}; };


const afterClose = () => {
clearFileList();
};

const items: TabsProps['items'] = [ const items: TabsProps['items'] = [
{ {
key: '1', key: '1',
onOk={onOk} onOk={onOk}
onCancel={hideModal} onCancel={hideModal}
confirmLoading={loading} confirmLoading={loading}
afterClose={afterClose}
> >
<Flex gap={'large'} vertical> <Flex gap={'large'} vertical>
<Segmented <Segmented

+ 7
- 1
web/src/hooks/knowledgeHook.ts Прегледај датотеку

}, [fetchKnowledgeBaseConfiguration]); }, [fetchKnowledgeBaseConfiguration]);
}; };


export const useSelectKnowledgeList = () => {
const knowledgeModel = useSelector((state) => state.knowledgeModel);
const { data = [] } = knowledgeModel;
return data;
};

export const useFetchKnowledgeList = ( export const useFetchKnowledgeList = (
shouldFilterListWithoutDocument: boolean = false, shouldFilterListWithoutDocument: boolean = false,
) => { ) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const loading = useOneNamespaceEffectsLoading('knowledgeModel', ['getList']); const loading = useOneNamespaceEffectsLoading('knowledgeModel', ['getList']);


const knowledgeModel = useSelector((state: any) => state.knowledgeModel);
const knowledgeModel = useSelector((state) => state.knowledgeModel);
const { data = [] } = knowledgeModel; const { data = [] } = knowledgeModel;
const list: IKnowledge[] = useMemo(() => { const list: IKnowledge[] = useMemo(() => {
return shouldFilterListWithoutDocument return shouldFilterListWithoutDocument

+ 14
- 0
web/src/less/mixins.less Прегледај датотеку

} }
} }
} }

.textEllipsis() {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.multipleLineEllipsis(@line) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: @line;
overflow: hidden;
text-overflow: ellipsis;
}

+ 3
- 0
web/src/locales/en.ts Прегледај датотеку

name: 'Name', name: 'Name',
namePlaceholder: 'Please input name!', namePlaceholder: 'Please input name!',
doc: 'Docs', doc: 'Docs',
searchKnowledgePlaceholder: 'Search',
}, },
knowledgeDetails: { knowledgeDetails: {
dataset: 'Dataset', dataset: 'Dataset',
keyword: 'Keyword', keyword: 'Keyword',
function: 'Function', function: 'Function',
chunkMessage: 'Please input value!', chunkMessage: 'Please input value!',
full: 'Full text',
ellipse: 'Ellipse',
}, },
chat: { chat: {
createAssistant: 'Create an Assistant', createAssistant: 'Create an Assistant',

+ 3
- 0
web/src/locales/zh-traditional.ts Прегледај датотеку

name: '名稱', name: '名稱',
namePlaceholder: '請輸入名稱', namePlaceholder: '請輸入名稱',
doc: '文件', doc: '文件',
searchKnowledgePlaceholder: '搜索',
}, },
knowledgeDetails: { knowledgeDetails: {
dataset: '數據集', dataset: '數據集',
keyword: '關鍵詞', keyword: '關鍵詞',
function: '函數', function: '函數',
chunkMessage: '請輸入值!', chunkMessage: '請輸入值!',
full: '全文',
ellipse: '省略',
}, },
chat: { chat: {
createAssistant: '新建助理', createAssistant: '新建助理',

+ 3
- 0
web/src/locales/zh.ts Прегледај датотеку

name: '名称', name: '名称',
namePlaceholder: '请输入名称', namePlaceholder: '请输入名称',
doc: '文档', doc: '文档',
searchKnowledgePlaceholder: '搜索',
}, },
knowledgeDetails: { knowledgeDetails: {
dataset: '数据集', dataset: '数据集',
keyword: '关键词', keyword: '关键词',
function: '函数', function: '函数',
chunkMessage: '请输入值!', chunkMessage: '请输入值!',
full: '全文',
ellipse: '省略',
}, },
chat: { chat: {
createAssistant: '新建助理', createAssistant: '新建助理',

+ 4
- 0
web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.less Прегледај датотеку

.chunkText; .chunkText;
} }


.contentEllipsis {
.multipleLineEllipsis(3);
}

.chunkCard { .chunkCard {
width: 100%; width: 100%;
} }

+ 12
- 2
web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx Прегледај датотеку

import classNames from 'classnames'; import classNames from 'classnames';
import { useState } from 'react'; import { useState } from 'react';


import { ChunkTextMode } from '../../constant';
import styles from './index.less'; import styles from './index.less';


interface IProps { interface IProps {
handleCheckboxClick: (chunkId: string, checked: boolean) => void; handleCheckboxClick: (chunkId: string, checked: boolean) => void;
selected: boolean; selected: boolean;
clickChunkCard: (chunkId: string) => void; clickChunkCard: (chunkId: string) => void;
textMode: ChunkTextMode;
} }


const ChunkCard = ({ const ChunkCard = ({
switchChunk, switchChunk,
selected, selected,
clickChunkCard, clickChunkCard,
textMode,
}: IProps) => { }: IProps) => {
const available = Number(item.available_int); const available = Number(item.available_int);
const [enabled, setEnabled] = useState(available === 1); const [enabled, setEnabled] = useState(available === 1);
onDoubleClick={handleContentDoubleClick} onDoubleClick={handleContentDoubleClick}
onClick={handleContentClick} onClick={handleContentClick}
className={styles.content} className={styles.content}
dangerouslySetInnerHTML={{ __html: item.content_with_weight }}
></section>
>
<div
dangerouslySetInnerHTML={{ __html: item.content_with_weight }}
className={classNames({
[styles.contentEllipsis]: textMode === ChunkTextMode.Ellipse,
})}
></div>
</section>

<div> <div>
<Switch checked={enabled} onChange={onChange} /> <Switch checked={enabled} onChange={onChange} />
</div> </div>

+ 18
- 1
web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx Прегледај датотеку

Popover, Popover,
Radio, Radio,
RadioChangeEvent, RadioChangeEvent,
Segmented,
SegmentedProps,
Space, Space,
Typography,
} from 'antd'; } from 'antd';
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
import { Link, useDispatch, useSelector } from 'umi'; import { Link, useDispatch, useSelector } from 'umi';
import { ChunkTextMode } from '../../constant';
import { ChunkModelState } from '../../model'; import { ChunkModelState } from '../../model';


const { Text } = Typography;

interface IProps { interface IProps {
checked: boolean; checked: boolean;
getChunkList: () => void; getChunkList: () => void;
createChunk: () => void; createChunk: () => void;
removeChunk: () => void; removeChunk: () => void;
switchChunk: (available: number) => void; switchChunk: (available: number) => void;
changeChunkTextMode(mode: ChunkTextMode): void;
} }


const ChunkToolBar = ({ const ChunkToolBar = ({
createChunk, createChunk,
removeChunk, removeChunk,
switchChunk, switchChunk,
changeChunkTextMode,
}: IProps) => { }: IProps) => {
const { documentInfo, available, searchString }: ChunkModelState = const { documentInfo, available, searchString }: ChunkModelState =
useSelector((state: any) => state.chunkModel); useSelector((state: any) => state.chunkModel);
<ArrowLeftOutlined /> <ArrowLeftOutlined />
</Link> </Link>
<FilePdfOutlined /> <FilePdfOutlined />
{documentInfo.name}
<Text ellipsis={{ tooltip: documentInfo.name }} style={{ width: 150 }}>
{documentInfo.name}
</Text>
</Space> </Space>
<Space> <Space>
<Segmented
options={[
{ label: t(ChunkTextMode.Full), value: ChunkTextMode.Full },
{ label: t(ChunkTextMode.Ellipse), value: ChunkTextMode.Ellipse },
]}
onChange={changeChunkTextMode as SegmentedProps['onChange']}
/>
<Popover content={content} placement="bottom" arrow={false}> <Popover content={content} placement="bottom" arrow={false}>
<Button> <Button>
{t('bulk')} {t('bulk')}

+ 4
- 0
web/src/pages/add-knowledge/components/knowledge-chunk/constant.ts Прегледај датотеку

export enum ChunkTextMode {
Full = 'full',
Ellipse = 'ellipse',
}

+ 12
- 0
web/src/pages/add-knowledge/components/knowledge-chunk/hooks.ts Прегледај датотеку

import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { IHighlight } from 'react-pdf-highlighter'; import { IHighlight } from 'react-pdf-highlighter';
import { useSelector } from 'umi'; import { useSelector } from 'umi';
import { ChunkTextMode } from './constant';


export const useSelectDocumentInfo = () => { export const useSelectDocumentInfo = () => {
const documentInfo: IKnowledgeFile = useSelector( const documentInfo: IKnowledgeFile = useSelector(
'switch_chunk', 'switch_chunk',
]); ]);
}; };

// Switch chunk text to be fully displayed or ellipse
export const useChangeChunkTextMode = () => {
const [textMode, setTextMode] = useState<ChunkTextMode>(ChunkTextMode.Full);

const changeChunkTextMode = useCallback((mode: ChunkTextMode) => {
setTextMode(mode);
}, []);

return { textMode, changeChunkTextMode };
};

+ 15
- 4
web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx Прегледај датотеку

import ChunkToolBar from './components/chunk-toolbar'; import ChunkToolBar from './components/chunk-toolbar';
import DocumentPreview from './components/document-preview/preview'; import DocumentPreview from './components/document-preview/preview';
import { import {
useChangeChunkTextMode,
useHandleChunkCardClick, useHandleChunkCardClick,
useSelectChunkListLoading, useSelectChunkListLoading,
useSelectDocumentInfo, useSelectDocumentInfo,
const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick(); const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick();
const isPdf = documentInfo.type === 'pdf'; const isPdf = documentInfo.type === 'pdf';
const { t } = useTranslation(); const { t } = useTranslation();
const { changeChunkTextMode, textMode } = useChangeChunkTextMode();


const getChunkList = useFetchChunkList(); const getChunkList = useFetchChunkList();


}, },
[], [],
); );
const showSelectedChunkWarning = () => {

const showSelectedChunkWarning = useCallback(() => {
message.warning(t('message.pleaseSelectChunk')); message.warning(t('message.pleaseSelectChunk'));
};
}, [t]);


const handleRemoveChunk = useCallback(async () => { const handleRemoveChunk = useCallback(async () => {
if (selectedChunkIds.length > 0) { if (selectedChunkIds.length > 0) {
} else { } else {
showSelectedChunkWarning(); showSelectedChunkWarning();
} }
}, [selectedChunkIds, documentId, removeChunk]);
}, [selectedChunkIds, documentId, removeChunk, showSelectedChunkWarning]);


const switchChunk = useCallback( const switchChunk = useCallback(
async (available?: number, chunkIds?: string[]) => { async (available?: number, chunkIds?: string[]) => {
getChunkList(); getChunkList();
} }
}, },
[dispatch, documentId, getChunkList, selectedChunkIds],
[
dispatch,
documentId,
getChunkList,
selectedChunkIds,
showSelectedChunkWarning,
],
); );


useEffect(() => { useEffect(() => {
removeChunk={handleRemoveChunk} removeChunk={handleRemoveChunk}
checked={selectedChunkIds.length === data.length} checked={selectedChunkIds.length === data.length}
switchChunk={switchChunk} switchChunk={switchChunk}
changeChunkTextMode={changeChunkTextMode}
></ChunkToolBar> ></ChunkToolBar>
<Divider></Divider> <Divider></Divider>
<Flex flex={1} gap={'middle'}> <Flex flex={1} gap={'middle'}>
switchChunk={switchChunk} switchChunk={switchChunk}
clickChunkCard={handleChunkCardClick} clickChunkCard={handleChunkCardClick}
selected={item.chunk_id === selectedChunkId} selected={item.chunk_id === selectedChunkId}
textMode={textMode}
></ChunkCard> ></ChunkCard>
))} ))}
</Space> </Space>

+ 0
- 3
web/src/pages/add-knowledge/components/knowledge-file/document-toolbar.tsx Прегледај датотеку

useFetchDocumentListOnMount, useFetchDocumentListOnMount,
useGetPagination, useGetPagination,
useHandleSearchChange, useHandleSearchChange,
useNavigateToOtherPage,
} from './hooks'; } from './hooks';
import styles from './index.less'; import styles from './index.less';


const { handleInputChange } = useHandleSearchChange(setPagination); const { handleInputChange } = useHandleSearchChange(setPagination);
const removeDocument = useRemoveDocument(); const removeDocument = useRemoveDocument();
const showDeleteConfirm = useShowDeleteConfirm(); const showDeleteConfirm = useShowDeleteConfirm();
const { linkToUploadPage } = useNavigateToOtherPage();
const runDocumentByIds = useRunDocument(); const runDocumentByIds = useRunDocument();
const { knowledgeId } = useGetKnowledgeSearchParams(); const { knowledgeId } = useGetKnowledgeSearchParams();
const changeStatus = useSetDocumentStatus(); const changeStatus = useSetDocumentStatus();
</Button> </Button>
</div> </div>
), ),
// disabled: true,
}, },
]; ];
}, [showDocumentUploadModal, showCreateModal, t]); }, [showDocumentUploadModal, showCreateModal, t]);

+ 8
- 2
web/src/pages/add-knowledge/components/knowledge-file/index.less Прегледај датотеку

.datasetWrapper { .datasetWrapper {
padding: 30px;
flex: 1;
padding: 30px 30px 0;
height: 100%;
}
.documentTable {
tbody {
// height: calc(100vh - 508px);
}
} }
.filter { .filter {

+ 2
- 1
web/src/pages/add-knowledge/components/knowledge-file/index.tsx Прегледај датотеку

// loading={loading} // loading={loading}
pagination={pagination} pagination={pagination}
rowSelection={rowSelection} rowSelection={rowSelection}
scroll={{ scrollToFirstRowOnChange: true, x: 1300, y: 'fill' }}
className={styles.documentTable}
scroll={{ scrollToFirstRowOnChange: true, x: 1300 }}
/> />
<CreateFileModal <CreateFileModal
visible={createVisible} visible={createVisible}

+ 0
- 8
web/src/pages/file-manager/file-upload-modal/index.less Прегледај датотеку

.uploader {
:global {
.ant-upload-list {
max-height: 40vh;
overflow-y: auto;
}
}
}

+ 0
- 136
web/src/pages/file-manager/file-upload-modal/index.tsx Прегледај датотеку

import { useTranslate } from '@/hooks/commonHooks';
import { IModalProps } from '@/interfaces/common';
import { InboxOutlined } from '@ant-design/icons';
import {
Flex,
Modal,
Segmented,
Tabs,
TabsProps,
Upload,
UploadFile,
UploadProps,
} from 'antd';
import { Dispatch, SetStateAction, useState } from 'react';

import styles from './index.less';

const { Dragger } = Upload;

const FileUpload = ({
directory,
fileList,
setFileList,
}: {
directory: boolean;
fileList: UploadFile[];
setFileList: Dispatch<SetStateAction<UploadFile[]>>;
}) => {
const { t } = useTranslate('fileManager');
const props: UploadProps = {
multiple: true,
onRemove: (file) => {
const index = fileList.indexOf(file);
const newFileList = fileList.slice();
newFileList.splice(index, 1);
setFileList(newFileList);
},
beforeUpload: (file) => {
setFileList((pre) => {
return [...pre, file];
});

return false;
},
directory,
fileList,
};

return (
<Dragger {...props} className={styles.uploader}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">{t('uploadTitle')}</p>
<p className="ant-upload-hint">{t('uploadDescription')}</p>
</Dragger>
);
};

const FileUploadModal = ({
visible,
hideModal,
loading,
onOk: onFileUploadOk,
}: IModalProps<UploadFile[]>) => {
const { t } = useTranslate('fileManager');
const [value, setValue] = useState<string | number>('local');
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [directoryFileList, setDirectoryFileList] = useState<UploadFile[]>([]);

const onOk = async () => {
const ret = await onFileUploadOk?.([...fileList, ...directoryFileList]);
console.info(ret);
if (ret !== undefined && ret === 0) {
setFileList([]);
setDirectoryFileList([]);
}
return ret;
};

const items: TabsProps['items'] = [
{
key: '1',
label: t('file'),
children: (
<FileUpload
directory={false}
fileList={fileList}
setFileList={setFileList}
></FileUpload>
),
},
{
key: '2',
label: t('directory'),
children: (
<FileUpload
directory
fileList={directoryFileList}
setFileList={setDirectoryFileList}
></FileUpload>
),
},
];

return (
<>
<Modal
title={t('uploadFile')}
open={visible}
onOk={onOk}
onCancel={hideModal}
confirmLoading={loading}
>
<Flex gap={'large'} vertical>
<Segmented
options={[
{ label: t('local'), value: 'local' },
{ label: t('s3'), value: 's3' },
]}
block
value={value}
onChange={setValue}
/>
{value === 'local' ? (
<Tabs defaultActiveKey="1" items={items} />
) : (
t('comingSoon', { keyPrefix: 'common' })
)}
</Flex>
</Modal>
</>
);
};

export default FileUploadModal;

+ 1
- 1
web/src/pages/file-manager/index.tsx Прегледај датотеку

useSelectFileListLoading, useSelectFileListLoading,
} from './hooks'; } from './hooks';


import FileUploadModal from '@/components/file-upload-modal';
import RenameModal from '@/components/rename-modal'; import RenameModal from '@/components/rename-modal';
import SvgIcon from '@/components/svg-icon'; import SvgIcon from '@/components/svg-icon';
import { useTranslate } from '@/hooks/commonHooks'; import { useTranslate } from '@/hooks/commonHooks';
import { formatNumberWithThousandsSeparator } from '@/utils/commonUtil'; import { formatNumberWithThousandsSeparator } from '@/utils/commonUtil';
import { getExtension } from '@/utils/documentUtils'; import { getExtension } from '@/utils/documentUtils';
import ConnectToKnowledgeModal from './connect-to-knowledge-modal'; import ConnectToKnowledgeModal from './connect-to-knowledge-modal';
import FileUploadModal from './file-upload-modal';
import FolderCreateModal from './folder-create-modal'; import FolderCreateModal from './folder-create-modal';
import styles from './index.less'; import styles from './index.less';



+ 19
- 0
web/src/pages/knowledge/hooks.ts Прегледај датотеку

import { useSelectKnowledgeList } from '@/hooks/knowledgeHook';
import { useState } from 'react';

export const useSearchKnowledge = () => {
const [searchString, setSearchString] = useState<string>('');

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchString(e.target.value);
};
return {
searchString,
handleInputChange,
};
};

export const useSelectKnowledgeListByKeywords = (keywords: string) => {
const list = useSelectKnowledgeList();
return list.filter((x) => x.name.includes(keywords));
};

+ 17
- 8
web/src/pages/knowledge/index.tsx Прегледај датотеку

import ModalManager from '@/components/modal-manager'; import ModalManager from '@/components/modal-manager';
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook'; import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import { useSelectUserInfo } from '@/hooks/userSettingHook'; import { useSelectUserInfo } from '@/hooks/userSettingHook';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Empty, Flex, Space, Spin } from 'antd';
import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
import { Button, Empty, Flex, Input, Space, Spin } from 'antd';
import KnowledgeCard from './knowledge-card'; import KnowledgeCard from './knowledge-card';
import KnowledgeCreatingModal from './knowledge-creating-modal'; import KnowledgeCreatingModal from './knowledge-creating-modal';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSearchKnowledge, useSelectKnowledgeListByKeywords } from './hooks';
import styles from './index.less'; import styles from './index.less';
const Knowledge = () => {
const { list, loading } = useFetchKnowledgeList();
const KnowledgeList = () => {
const { searchString, handleInputChange } = useSearchKnowledge();
const { loading } = useFetchKnowledgeList();
const list = useSelectKnowledgeListByKeywords(searchString);
const userInfo = useSelectUserInfo(); const userInfo = useSelectUserInfo();
const { t } = useTranslation('translation', { keyPrefix: 'knowledgeList' }); const { t } = useTranslation('translation', { keyPrefix: 'knowledgeList' });
<p className={styles.description}>{t('description')}</p> <p className={styles.description}>{t('description')}</p>
</div> </div>
<Space size={'large'}> <Space size={'large'}>
{/* <Button icon={<FilterIcon />} className={styles.filterButton}>
Filters
</Button> */}
<Input
placeholder={t('searchKnowledgePlaceholder')}
value={searchString}
style={{ width: 220 }}
allowClear
onChange={handleInputChange}
prefix={<SearchOutlined />}
/>
<ModalManager> <ModalManager>
{({ visible, hideModal, showModal }) => ( {({ visible, hideModal, showModal }) => (
<> <>
); );
}; };
export default Knowledge;
export default KnowledgeList;

Loading…
Откажи
Сачувај