### 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
| @@ -48,6 +48,7 @@ const ChunkMethodModal: React.FC<IProps> = ({ | |||
| visible, | |||
| documentExtension, | |||
| parserConfig, | |||
| loading, | |||
| }) => { | |||
| const { parserList, handleChange, selectedTag } = useFetchParserListOnMount( | |||
| documentId, | |||
| @@ -109,6 +110,7 @@ const ChunkMethodModal: React.FC<IProps> = ({ | |||
| onOk={handleOk} | |||
| onCancel={hideModal} | |||
| afterClose={afterClose} | |||
| confirmLoading={loading} | |||
| > | |||
| <Space size={[0, 8]} wrap> | |||
| <Form.Item label={t('chunkMethod')} className={styles.chunkMethod}> | |||
| @@ -68,15 +68,20 @@ const FileUploadModal = ({ | |||
| const [fileList, setFileList] = useState<UploadFile[]>([]); | |||
| const [directoryFileList, setDirectoryFileList] = useState<UploadFile[]>([]); | |||
| const clearFileList = () => { | |||
| setFileList([]); | |||
| setDirectoryFileList([]); | |||
| }; | |||
| const onOk = async () => { | |||
| const ret = await onFileUploadOk?.([...fileList, ...directoryFileList]); | |||
| if (ret !== undefined && ret === 0) { | |||
| setFileList([]); | |||
| setDirectoryFileList([]); | |||
| } | |||
| return ret; | |||
| }; | |||
| const afterClose = () => { | |||
| clearFileList(); | |||
| }; | |||
| const items: TabsProps['items'] = [ | |||
| { | |||
| key: '1', | |||
| @@ -110,6 +115,7 @@ const FileUploadModal = ({ | |||
| onOk={onOk} | |||
| onCancel={hideModal} | |||
| confirmLoading={loading} | |||
| afterClose={afterClose} | |||
| > | |||
| <Flex gap={'large'} vertical> | |||
| <Segmented | |||
| @@ -125,13 +125,19 @@ export const useFetchKnowledgeBaseConfiguration = () => { | |||
| }, [fetchKnowledgeBaseConfiguration]); | |||
| }; | |||
| export const useSelectKnowledgeList = () => { | |||
| const knowledgeModel = useSelector((state) => state.knowledgeModel); | |||
| const { data = [] } = knowledgeModel; | |||
| return data; | |||
| }; | |||
| export const useFetchKnowledgeList = ( | |||
| shouldFilterListWithoutDocument: boolean = false, | |||
| ) => { | |||
| const dispatch = useDispatch(); | |||
| const loading = useOneNamespaceEffectsLoading('knowledgeModel', ['getList']); | |||
| const knowledgeModel = useSelector((state: any) => state.knowledgeModel); | |||
| const knowledgeModel = useSelector((state) => state.knowledgeModel); | |||
| const { data = [] } = knowledgeModel; | |||
| const list: IKnowledge[] = useMemo(() => { | |||
| return shouldFilterListWithoutDocument | |||
| @@ -42,3 +42,17 @@ | |||
| } | |||
| } | |||
| } | |||
| .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; | |||
| } | |||
| @@ -64,6 +64,7 @@ export default { | |||
| name: 'Name', | |||
| namePlaceholder: 'Please input name!', | |||
| doc: 'Docs', | |||
| searchKnowledgePlaceholder: 'Search', | |||
| }, | |||
| knowledgeDetails: { | |||
| dataset: 'Dataset', | |||
| @@ -278,6 +279,8 @@ export default { | |||
| keyword: 'Keyword', | |||
| function: 'Function', | |||
| chunkMessage: 'Please input value!', | |||
| full: 'Full text', | |||
| ellipse: 'Ellipse', | |||
| }, | |||
| chat: { | |||
| createAssistant: 'Create an Assistant', | |||
| @@ -64,6 +64,7 @@ export default { | |||
| name: '名稱', | |||
| namePlaceholder: '請輸入名稱', | |||
| doc: '文件', | |||
| searchKnowledgePlaceholder: '搜索', | |||
| }, | |||
| knowledgeDetails: { | |||
| dataset: '數據集', | |||
| @@ -251,6 +252,8 @@ export default { | |||
| keyword: '關鍵詞', | |||
| function: '函數', | |||
| chunkMessage: '請輸入值!', | |||
| full: '全文', | |||
| ellipse: '省略', | |||
| }, | |||
| chat: { | |||
| createAssistant: '新建助理', | |||
| @@ -64,6 +64,7 @@ export default { | |||
| name: '名称', | |||
| namePlaceholder: '请输入名称', | |||
| doc: '文档', | |||
| searchKnowledgePlaceholder: '搜索', | |||
| }, | |||
| knowledgeDetails: { | |||
| dataset: '数据集', | |||
| @@ -268,6 +269,8 @@ export default { | |||
| keyword: '关键词', | |||
| function: '函数', | |||
| chunkMessage: '请输入值!', | |||
| full: '全文', | |||
| ellipse: '省略', | |||
| }, | |||
| chat: { | |||
| createAssistant: '新建助理', | |||
| @@ -14,6 +14,10 @@ | |||
| .chunkText; | |||
| } | |||
| .contentEllipsis { | |||
| .multipleLineEllipsis(3); | |||
| } | |||
| .chunkCard { | |||
| width: 100%; | |||
| } | |||
| @@ -4,6 +4,7 @@ import { Card, Checkbox, CheckboxProps, Flex, Popover, Switch } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useState } from 'react'; | |||
| import { ChunkTextMode } from '../../constant'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| @@ -14,6 +15,7 @@ interface IProps { | |||
| handleCheckboxClick: (chunkId: string, checked: boolean) => void; | |||
| selected: boolean; | |||
| clickChunkCard: (chunkId: string) => void; | |||
| textMode: ChunkTextMode; | |||
| } | |||
| const ChunkCard = ({ | |||
| @@ -24,6 +26,7 @@ const ChunkCard = ({ | |||
| switchChunk, | |||
| selected, | |||
| clickChunkCard, | |||
| textMode, | |||
| }: IProps) => { | |||
| const available = Number(item.available_int); | |||
| const [enabled, setEnabled] = useState(available === 1); | |||
| @@ -68,8 +71,15 @@ const ChunkCard = ({ | |||
| onDoubleClick={handleContentDoubleClick} | |||
| onClick={handleContentClick} | |||
| 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> | |||
| <Switch checked={enabled} onChange={onChange} /> | |||
| </div> | |||
| @@ -22,12 +22,18 @@ import { | |||
| Popover, | |||
| Radio, | |||
| RadioChangeEvent, | |||
| Segmented, | |||
| SegmentedProps, | |||
| Space, | |||
| Typography, | |||
| } from 'antd'; | |||
| import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; | |||
| import { Link, useDispatch, useSelector } from 'umi'; | |||
| import { ChunkTextMode } from '../../constant'; | |||
| import { ChunkModelState } from '../../model'; | |||
| const { Text } = Typography; | |||
| interface IProps { | |||
| checked: boolean; | |||
| getChunkList: () => void; | |||
| @@ -35,6 +41,7 @@ interface IProps { | |||
| createChunk: () => void; | |||
| removeChunk: () => void; | |||
| switchChunk: (available: number) => void; | |||
| changeChunkTextMode(mode: ChunkTextMode): void; | |||
| } | |||
| const ChunkToolBar = ({ | |||
| @@ -44,6 +51,7 @@ const ChunkToolBar = ({ | |||
| createChunk, | |||
| removeChunk, | |||
| switchChunk, | |||
| changeChunkTextMode, | |||
| }: IProps) => { | |||
| const { documentInfo, available, searchString }: ChunkModelState = | |||
| useSelector((state: any) => state.chunkModel); | |||
| @@ -170,9 +178,18 @@ const ChunkToolBar = ({ | |||
| <ArrowLeftOutlined /> | |||
| </Link> | |||
| <FilePdfOutlined /> | |||
| {documentInfo.name} | |||
| <Text ellipsis={{ tooltip: documentInfo.name }} style={{ width: 150 }}> | |||
| {documentInfo.name} | |||
| </Text> | |||
| </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}> | |||
| <Button> | |||
| {t('bulk')} | |||
| @@ -0,0 +1,4 @@ | |||
| export enum ChunkTextMode { | |||
| Full = 'full', | |||
| Ellipse = 'ellipse', | |||
| } | |||
| @@ -4,6 +4,7 @@ import { buildChunkHighlights } from '@/utils/documentUtils'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| import { IHighlight } from 'react-pdf-highlighter'; | |||
| import { useSelector } from 'umi'; | |||
| import { ChunkTextMode } from './constant'; | |||
| export const useSelectDocumentInfo = () => { | |||
| const documentInfo: IKnowledgeFile = useSelector( | |||
| @@ -63,3 +64,14 @@ export const useSelectChunkListLoading = () => { | |||
| '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 }; | |||
| }; | |||
| @@ -10,6 +10,7 @@ import CreatingModal from './components/chunk-creating-modal'; | |||
| import ChunkToolBar from './components/chunk-toolbar'; | |||
| import DocumentPreview from './components/document-preview/preview'; | |||
| import { | |||
| useChangeChunkTextMode, | |||
| useHandleChunkCardClick, | |||
| useSelectChunkListLoading, | |||
| useSelectDocumentInfo, | |||
| @@ -35,6 +36,7 @@ const Chunk = () => { | |||
| const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick(); | |||
| const isPdf = documentInfo.type === 'pdf'; | |||
| const { t } = useTranslation(); | |||
| const { changeChunkTextMode, textMode } = useChangeChunkTextMode(); | |||
| const getChunkList = useFetchChunkList(); | |||
| @@ -87,9 +89,10 @@ const Chunk = () => { | |||
| }, | |||
| [], | |||
| ); | |||
| const showSelectedChunkWarning = () => { | |||
| const showSelectedChunkWarning = useCallback(() => { | |||
| message.warning(t('message.pleaseSelectChunk')); | |||
| }; | |||
| }, [t]); | |||
| const handleRemoveChunk = useCallback(async () => { | |||
| if (selectedChunkIds.length > 0) { | |||
| @@ -100,7 +103,7 @@ const Chunk = () => { | |||
| } else { | |||
| showSelectedChunkWarning(); | |||
| } | |||
| }, [selectedChunkIds, documentId, removeChunk]); | |||
| }, [selectedChunkIds, documentId, removeChunk, showSelectedChunkWarning]); | |||
| const switchChunk = useCallback( | |||
| async (available?: number, chunkIds?: string[]) => { | |||
| @@ -125,7 +128,13 @@ const Chunk = () => { | |||
| getChunkList(); | |||
| } | |||
| }, | |||
| [dispatch, documentId, getChunkList, selectedChunkIds], | |||
| [ | |||
| dispatch, | |||
| documentId, | |||
| getChunkList, | |||
| selectedChunkIds, | |||
| showSelectedChunkWarning, | |||
| ], | |||
| ); | |||
| useEffect(() => { | |||
| @@ -147,6 +156,7 @@ const Chunk = () => { | |||
| removeChunk={handleRemoveChunk} | |||
| checked={selectedChunkIds.length === data.length} | |||
| switchChunk={switchChunk} | |||
| changeChunkTextMode={changeChunkTextMode} | |||
| ></ChunkToolBar> | |||
| <Divider></Divider> | |||
| <Flex flex={1} gap={'middle'}> | |||
| @@ -175,6 +185,7 @@ const Chunk = () => { | |||
| switchChunk={switchChunk} | |||
| clickChunkCard={handleChunkCardClick} | |||
| selected={item.chunk_id === selectedChunkId} | |||
| textMode={textMode} | |||
| ></ChunkCard> | |||
| ))} | |||
| </Space> | |||
| @@ -23,7 +23,6 @@ import { | |||
| useFetchDocumentListOnMount, | |||
| useGetPagination, | |||
| useHandleSearchChange, | |||
| useNavigateToOtherPage, | |||
| } from './hooks'; | |||
| import styles from './index.less'; | |||
| @@ -44,7 +43,6 @@ const DocumentToolbar = ({ | |||
| const { handleInputChange } = useHandleSearchChange(setPagination); | |||
| const removeDocument = useRemoveDocument(); | |||
| const showDeleteConfirm = useShowDeleteConfirm(); | |||
| const { linkToUploadPage } = useNavigateToOtherPage(); | |||
| const runDocumentByIds = useRunDocument(); | |||
| const { knowledgeId } = useGetKnowledgeSearchParams(); | |||
| const changeStatus = useSetDocumentStatus(); | |||
| @@ -77,7 +75,6 @@ const DocumentToolbar = ({ | |||
| </Button> | |||
| </div> | |||
| ), | |||
| // disabled: true, | |||
| }, | |||
| ]; | |||
| }, [showDocumentUploadModal, showCreateModal, t]); | |||
| @@ -1,6 +1,12 @@ | |||
| .datasetWrapper { | |||
| padding: 30px; | |||
| flex: 1; | |||
| padding: 30px 30px 0; | |||
| height: 100%; | |||
| } | |||
| .documentTable { | |||
| tbody { | |||
| // height: calc(100vh - 508px); | |||
| } | |||
| } | |||
| .filter { | |||
| @@ -179,7 +179,8 @@ const KnowledgeFile = () => { | |||
| // loading={loading} | |||
| pagination={pagination} | |||
| rowSelection={rowSelection} | |||
| scroll={{ scrollToFirstRowOnChange: true, x: 1300, y: 'fill' }} | |||
| className={styles.documentTable} | |||
| scroll={{ scrollToFirstRowOnChange: true, x: 1300 }} | |||
| /> | |||
| <CreateFileModal | |||
| visible={createVisible} | |||
| @@ -1,8 +0,0 @@ | |||
| .uploader { | |||
| :global { | |||
| .ant-upload-list { | |||
| max-height: 40vh; | |||
| overflow-y: auto; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,136 +0,0 @@ | |||
| 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; | |||
| @@ -16,13 +16,13 @@ import { | |||
| useSelectFileListLoading, | |||
| } from './hooks'; | |||
| import FileUploadModal from '@/components/file-upload-modal'; | |||
| import RenameModal from '@/components/rename-modal'; | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { useTranslate } from '@/hooks/commonHooks'; | |||
| import { formatNumberWithThousandsSeparator } from '@/utils/commonUtil'; | |||
| import { getExtension } from '@/utils/documentUtils'; | |||
| import ConnectToKnowledgeModal from './connect-to-knowledge-modal'; | |||
| import FileUploadModal from './file-upload-modal'; | |||
| import FolderCreateModal from './folder-create-modal'; | |||
| import styles from './index.less'; | |||
| @@ -0,0 +1,19 @@ | |||
| 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)); | |||
| }; | |||
| @@ -1,16 +1,19 @@ | |||
| import ModalManager from '@/components/modal-manager'; | |||
| import { useFetchKnowledgeList } from '@/hooks/knowledgeHook'; | |||
| 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 KnowledgeCreatingModal from './knowledge-creating-modal'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useSearchKnowledge, useSelectKnowledgeListByKeywords } from './hooks'; | |||
| 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 { t } = useTranslation('translation', { keyPrefix: 'knowledgeList' }); | |||
| @@ -24,9 +27,15 @@ const Knowledge = () => { | |||
| <p className={styles.description}>{t('description')}</p> | |||
| </div> | |||
| <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> | |||
| {({ visible, hideModal, showModal }) => ( | |||
| <> | |||
| @@ -70,4 +79,4 @@ const Knowledge = () => { | |||
| ); | |||
| }; | |||
| export default Knowledge; | |||
| export default KnowledgeList; | |||