### What problem does this PR solve? Refactor Pdf 2 Slices page to new style ### Type of change - [X] Refactoringtags/v0.19.1
| @@ -64,7 +64,8 @@ export const useNavigatePage = () => { | |||
| const navigateToChunkParsedResult = useCallback( | |||
| (id: string, knowledgeId?: string) => () => { | |||
| navigate( | |||
| `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`, | |||
| // `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`, | |||
| `${Routes.ParsedResult}/chunks?id=${knowledgeId}&doc_id=${id}`, | |||
| ); | |||
| }, | |||
| [navigate], | |||
| @@ -0,0 +1,91 @@ | |||
| import { ResponseGetType } from '@/interfaces/database/base'; | |||
| import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge'; | |||
| import kbService from '@/services/knowledge-service'; | |||
| import { useQuery } from '@tanstack/react-query'; | |||
| import { useDebounce } from 'ahooks'; | |||
| import { useCallback, useState } from 'react'; | |||
| import { IChunkListResult } from './chunk-hooks'; | |||
| import { | |||
| useGetPaginationWithRouter, | |||
| useHandleSearchChange, | |||
| } from './logic-hooks'; | |||
| import { useGetKnowledgeSearchParams } from './route-hook'; | |||
| export const useFetchNextChunkList = (): ResponseGetType<{ | |||
| data: IChunk[]; | |||
| total: number; | |||
| documentInfo: IKnowledgeFile; | |||
| }> & | |||
| IChunkListResult => { | |||
| const { pagination, setPagination } = useGetPaginationWithRouter(); | |||
| const { documentId } = useGetKnowledgeSearchParams(); | |||
| const { searchString, handleInputChange } = useHandleSearchChange(); | |||
| const [available, setAvailable] = useState<number | undefined>(); | |||
| const debouncedSearchString = useDebounce(searchString, { wait: 500 }); | |||
| const { data, isFetching: loading } = useQuery({ | |||
| queryKey: [ | |||
| 'fetchChunkList', | |||
| documentId, | |||
| pagination.current, | |||
| pagination.pageSize, | |||
| debouncedSearchString, | |||
| available, | |||
| ], | |||
| placeholderData: (previousData: any) => | |||
| previousData ?? { data: [], total: 0, documentInfo: {} }, // https://github.com/TanStack/query/issues/8183 | |||
| gcTime: 0, | |||
| queryFn: async () => { | |||
| const { data } = await kbService.chunk_list({ | |||
| doc_id: documentId, | |||
| page: pagination.current, | |||
| size: pagination.pageSize, | |||
| available_int: available, | |||
| keywords: searchString, | |||
| }); | |||
| if (data.code === 0) { | |||
| const res = data.data; | |||
| return { | |||
| data: res.chunks, | |||
| total: res.total, | |||
| documentInfo: res.doc, | |||
| }; | |||
| } | |||
| return ( | |||
| data?.data ?? { | |||
| data: [], | |||
| total: 0, | |||
| documentInfo: {}, | |||
| } | |||
| ); | |||
| }, | |||
| }); | |||
| const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback( | |||
| (e) => { | |||
| setPagination({ page: 1 }); | |||
| handleInputChange(e); | |||
| }, | |||
| [handleInputChange, setPagination], | |||
| ); | |||
| const handleSetAvailable = useCallback( | |||
| (a: number | undefined) => { | |||
| setPagination({ page: 1 }); | |||
| setAvailable(a); | |||
| }, | |||
| [setAvailable, setPagination], | |||
| ); | |||
| return { | |||
| data, | |||
| loading, | |||
| pagination, | |||
| setPagination, | |||
| searchString, | |||
| handleInputChange: onInputChange, | |||
| available, | |||
| handleSetAvailable, | |||
| }; | |||
| }; | |||
| @@ -0,0 +1,34 @@ | |||
| .image { | |||
| width: 100px !important; | |||
| object-fit: contain; | |||
| } | |||
| .imagePreview { | |||
| max-width: 50vw; | |||
| max-height: 50vh; | |||
| object-fit: contain; | |||
| } | |||
| .content { | |||
| flex: 1; | |||
| .chunkText; | |||
| } | |||
| .contentEllipsis { | |||
| .multipleLineEllipsis(3); | |||
| } | |||
| .contentText { | |||
| word-break: break-all !important; | |||
| } | |||
| .chunkCard { | |||
| width: 100%; | |||
| } | |||
| .cardSelected { | |||
| background-color: @selectedBackgroundColor; | |||
| } | |||
| .cardSelectedDark { | |||
| background-color: #ffffff2f; | |||
| } | |||
| @@ -0,0 +1,101 @@ | |||
| import Image from '@/components/image'; | |||
| import { IChunk } from '@/interfaces/database/knowledge'; | |||
| import { Card, Checkbox, CheckboxProps, Flex, Popover, Switch } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import DOMPurify from 'dompurify'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useTheme } from '@/components/theme-provider'; | |||
| import { ChunkTextMode } from '../../constant'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| item: IChunk; | |||
| checked: boolean; | |||
| switchChunk: (available?: number, chunkIds?: string[]) => void; | |||
| editChunk: (chunkId: string) => void; | |||
| handleCheckboxClick: (chunkId: string, checked: boolean) => void; | |||
| selected: boolean; | |||
| clickChunkCard: (chunkId: string) => void; | |||
| textMode: ChunkTextMode; | |||
| } | |||
| const ChunkCard = ({ | |||
| item, | |||
| checked, | |||
| handleCheckboxClick, | |||
| editChunk, | |||
| switchChunk, | |||
| selected, | |||
| clickChunkCard, | |||
| textMode, | |||
| }: IProps) => { | |||
| const available = Number(item.available_int); | |||
| const [enabled, setEnabled] = useState(false); | |||
| const { theme } = useTheme(); | |||
| const onChange = (checked: boolean) => { | |||
| setEnabled(checked); | |||
| switchChunk(available === 0 ? 1 : 0, [item.chunk_id]); | |||
| }; | |||
| const handleCheck: CheckboxProps['onChange'] = (e) => { | |||
| handleCheckboxClick(item.chunk_id, e.target.checked); | |||
| }; | |||
| const handleContentDoubleClick = () => { | |||
| editChunk(item.chunk_id); | |||
| }; | |||
| const handleContentClick = () => { | |||
| clickChunkCard(item.chunk_id); | |||
| }; | |||
| useEffect(() => { | |||
| setEnabled(available === 1); | |||
| }, [available]); | |||
| return ( | |||
| <Card | |||
| className={classNames(styles.chunkCard, { | |||
| [`${theme === 'dark' ? styles.cardSelectedDark : styles.cardSelected}`]: | |||
| selected, | |||
| })} | |||
| > | |||
| <Flex gap={'middle'} justify={'space-between'}> | |||
| <Checkbox onChange={handleCheck} checked={checked}></Checkbox> | |||
| {item.image_id && ( | |||
| <Popover | |||
| placement="right" | |||
| content={ | |||
| <Image id={item.image_id} className={styles.imagePreview}></Image> | |||
| } | |||
| > | |||
| <Image id={item.image_id} className={styles.image}></Image> | |||
| </Popover> | |||
| )} | |||
| <section | |||
| onDoubleClick={handleContentDoubleClick} | |||
| onClick={handleContentClick} | |||
| className={styles.content} | |||
| > | |||
| <div | |||
| dangerouslySetInnerHTML={{ | |||
| __html: DOMPurify.sanitize(item.content_with_weight), | |||
| }} | |||
| className={classNames(styles.contentText, { | |||
| [styles.contentEllipsis]: textMode === ChunkTextMode.Ellipse, | |||
| })} | |||
| ></div> | |||
| </section> | |||
| <div> | |||
| <Switch checked={enabled} onChange={onChange} /> | |||
| </div> | |||
| </Flex> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default ChunkCard; | |||
| @@ -0,0 +1,140 @@ | |||
| import EditTag from '@/components/edit-tag'; | |||
| import { useFetchChunk } from '@/hooks/chunk-hooks'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { IChunk } from '@/interfaces/database/knowledge'; | |||
| import { DeleteOutlined } from '@ant-design/icons'; | |||
| import { Divider, Form, Input, Modal, Space, Switch } from 'antd'; | |||
| import React, { useCallback, useEffect, useState } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useDeleteChunkByIds } from '../../hooks'; | |||
| import { | |||
| transformTagFeaturesArrayToObject, | |||
| transformTagFeaturesObjectToArray, | |||
| } from '../../utils'; | |||
| import { TagFeatureItem } from './tag-feature-item'; | |||
| type FieldType = Pick< | |||
| IChunk, | |||
| 'content_with_weight' | 'tag_kwd' | 'question_kwd' | 'important_kwd' | |||
| >; | |||
| interface kFProps { | |||
| doc_id: string; | |||
| chunkId: string | undefined; | |||
| parserId: string; | |||
| } | |||
| const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({ | |||
| doc_id, | |||
| chunkId, | |||
| hideModal, | |||
| onOk, | |||
| loading, | |||
| parserId, | |||
| }) => { | |||
| const [form] = Form.useForm(); | |||
| const [checked, setChecked] = useState(false); | |||
| const { removeChunk } = useDeleteChunkByIds(); | |||
| const { data } = useFetchChunk(chunkId); | |||
| const { t } = useTranslation(); | |||
| const isTagParser = parserId === 'tag'; | |||
| const handleOk = useCallback(async () => { | |||
| try { | |||
| const values = await form.validateFields(); | |||
| console.log('🚀 ~ handleOk ~ values:', values); | |||
| onOk?.({ | |||
| ...values, | |||
| tag_feas: transformTagFeaturesArrayToObject(values.tag_feas), | |||
| available_int: checked ? 1 : 0, // available_int | |||
| }); | |||
| } catch (errorInfo) { | |||
| console.log('Failed:', errorInfo); | |||
| } | |||
| }, [checked, form, onOk]); | |||
| const handleRemove = useCallback(() => { | |||
| if (chunkId) { | |||
| return removeChunk([chunkId], doc_id); | |||
| } | |||
| }, [chunkId, doc_id, removeChunk]); | |||
| const handleCheck = useCallback(() => { | |||
| setChecked(!checked); | |||
| }, [checked]); | |||
| useEffect(() => { | |||
| if (data?.code === 0) { | |||
| const { available_int, tag_feas } = data.data; | |||
| form.setFieldsValue({ | |||
| ...(data.data || {}), | |||
| tag_feas: transformTagFeaturesObjectToArray(tag_feas), | |||
| }); | |||
| setChecked(available_int !== 0); | |||
| } | |||
| }, [data, form, chunkId]); | |||
| return ( | |||
| <Modal | |||
| title={`${chunkId ? t('common.edit') : t('common.create')} ${t('chunk.chunk')}`} | |||
| open={true} | |||
| onOk={handleOk} | |||
| onCancel={hideModal} | |||
| okButtonProps={{ loading }} | |||
| destroyOnClose | |||
| > | |||
| <Form form={form} autoComplete="off" layout={'vertical'}> | |||
| <Form.Item<FieldType> | |||
| label={t('chunk.chunk')} | |||
| name="content_with_weight" | |||
| rules={[{ required: true, message: t('chunk.chunkMessage') }]} | |||
| > | |||
| <Input.TextArea autoSize={{ minRows: 4, maxRows: 10 }} /> | |||
| </Form.Item> | |||
| <Form.Item<FieldType> label={t('chunk.keyword')} name="important_kwd"> | |||
| <EditTag></EditTag> | |||
| </Form.Item> | |||
| <Form.Item<FieldType> | |||
| label={t('chunk.question')} | |||
| name="question_kwd" | |||
| tooltip={t('chunk.questionTip')} | |||
| > | |||
| <EditTag></EditTag> | |||
| </Form.Item> | |||
| {isTagParser && ( | |||
| <Form.Item<FieldType> | |||
| label={t('knowledgeConfiguration.tagName')} | |||
| name="tag_kwd" | |||
| > | |||
| <EditTag></EditTag> | |||
| </Form.Item> | |||
| )} | |||
| {!isTagParser && <TagFeatureItem></TagFeatureItem>} | |||
| </Form> | |||
| {chunkId && ( | |||
| <section> | |||
| <Divider></Divider> | |||
| <Space size={'large'}> | |||
| <Switch | |||
| checkedChildren={t('chunk.enabled')} | |||
| unCheckedChildren={t('chunk.disabled')} | |||
| onChange={handleCheck} | |||
| checked={checked} | |||
| /> | |||
| <span onClick={handleRemove}> | |||
| <DeleteOutlined /> {t('common.delete')} | |||
| </span> | |||
| </Space> | |||
| </section> | |||
| )} | |||
| </Modal> | |||
| ); | |||
| }; | |||
| export default ChunkCreatingModal; | |||
| @@ -0,0 +1,107 @@ | |||
| import { | |||
| useFetchKnowledgeBaseConfiguration, | |||
| useFetchTagListByKnowledgeIds, | |||
| } from '@/hooks/knowledge-hooks'; | |||
| import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; | |||
| import { Button, Form, InputNumber, Select } from 'antd'; | |||
| import { useCallback, useEffect, useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { FormListItem } from '../../utils'; | |||
| const FieldKey = 'tag_feas'; | |||
| export const TagFeatureItem = () => { | |||
| const form = Form.useFormInstance(); | |||
| const { t } = useTranslation(); | |||
| const { data: knowledgeConfiguration } = useFetchKnowledgeBaseConfiguration(); | |||
| const { setKnowledgeIds, list } = useFetchTagListByKnowledgeIds(); | |||
| const tagKnowledgeIds = useMemo(() => { | |||
| return knowledgeConfiguration?.parser_config?.tag_kb_ids ?? []; | |||
| }, [knowledgeConfiguration?.parser_config?.tag_kb_ids]); | |||
| const options = useMemo(() => { | |||
| return list.map((x) => ({ | |||
| value: x[0], | |||
| label: x[0], | |||
| })); | |||
| }, [list]); | |||
| const filterOptions = useCallback( | |||
| (index: number) => { | |||
| const tags: FormListItem[] = form.getFieldValue(FieldKey) ?? []; | |||
| // Exclude it's own current data | |||
| const list = tags | |||
| .filter((x, idx) => x && index !== idx) | |||
| .map((x) => x.tag); | |||
| // Exclude the selected data from other options from one's own options. | |||
| return options.filter((x) => !list.some((y) => x.value === y)); | |||
| }, | |||
| [form, options], | |||
| ); | |||
| useEffect(() => { | |||
| setKnowledgeIds(tagKnowledgeIds); | |||
| }, [setKnowledgeIds, tagKnowledgeIds]); | |||
| return ( | |||
| <Form.Item label={t('knowledgeConfiguration.tags')}> | |||
| <Form.List name={FieldKey} initialValue={[]}> | |||
| {(fields, { add, remove }) => ( | |||
| <> | |||
| {fields.map(({ key, name, ...restField }) => ( | |||
| <div key={key} className="flex gap-3 items-center"> | |||
| <div className="flex flex-1 gap-8"> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'tag']} | |||
| rules={[ | |||
| { required: true, message: t('common.pleaseSelect') }, | |||
| ]} | |||
| className="w-2/3" | |||
| > | |||
| <Select | |||
| showSearch | |||
| placeholder={t('knowledgeConfiguration.tagName')} | |||
| options={filterOptions(name)} | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'frequency']} | |||
| rules={[ | |||
| { required: true, message: t('common.pleaseInput') }, | |||
| ]} | |||
| > | |||
| <InputNumber | |||
| placeholder={t('knowledgeConfiguration.frequency')} | |||
| max={10} | |||
| min={0} | |||
| /> | |||
| </Form.Item> | |||
| </div> | |||
| <MinusCircleOutlined | |||
| onClick={() => remove(name)} | |||
| className="mb-6" | |||
| /> | |||
| </div> | |||
| ))} | |||
| <Form.Item> | |||
| <Button | |||
| type="dashed" | |||
| onClick={() => add()} | |||
| block | |||
| icon={<PlusOutlined />} | |||
| > | |||
| {t('knowledgeConfiguration.addTag')} | |||
| </Button> | |||
| </Form.Item> | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| </Form.Item> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,39 @@ | |||
| import { Checkbox } from '@/components/ui/checkbox'; | |||
| import { Label } from '@/components/ui/label'; | |||
| import { Ban, CircleCheck, Trash2 } from 'lucide-react'; | |||
| import { useCallback } from 'react'; | |||
| export default ({ selectAllChunk, checked }) => { | |||
| const handleSelectAllCheck = useCallback( | |||
| (e: any) => { | |||
| console.log('eee=', e); | |||
| selectAllChunk(e); | |||
| }, | |||
| [selectAllChunk], | |||
| ); | |||
| return ( | |||
| <div className="flex gap-[40px]"> | |||
| <div className="flex items-center gap-3 cursor-pointer"> | |||
| <Checkbox | |||
| id="all_chunks_checkbox" | |||
| onCheckedChange={handleSelectAllCheck} | |||
| checked={checked} | |||
| /> | |||
| <Label htmlFor="all_chunks_checkbox">All Chunks</Label> | |||
| </div> | |||
| <div className="flex items-center cursor-pointer"> | |||
| <CircleCheck size={16} /> | |||
| <span className="block ml-1">Enable</span> | |||
| </div> | |||
| <div className="flex items-center cursor-pointer"> | |||
| <Ban size={16} /> | |||
| <span className="block ml-1">Disable</span> | |||
| </div> | |||
| <div className="flex items-center text-red-500 cursor-pointer"> | |||
| <Trash2 size={16} /> | |||
| <span className="block ml-1">Delete</span> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,72 @@ | |||
| import { ReactComponent as FilterIcon } from '@/assets/filter.svg'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { PlusOutlined, SearchOutlined } from '@ant-design/icons'; | |||
| import { | |||
| Button, | |||
| Input, | |||
| Popover, | |||
| Radio, | |||
| RadioChangeEvent, | |||
| Segmented, | |||
| SegmentedProps, | |||
| Space, | |||
| } from 'antd'; | |||
| import { ChunkTextMode } from '../../constant'; | |||
| export default ({ | |||
| changeChunkTextMode, | |||
| available, | |||
| selectAllChunk, | |||
| handleSetAvailable, | |||
| createChunk, | |||
| handleInputChange, | |||
| searchString, | |||
| }) => { | |||
| const { t } = useTranslate('chunk'); | |||
| const handleFilterChange = (e: RadioChangeEvent) => { | |||
| selectAllChunk(false); | |||
| handleSetAvailable(e.target.value); | |||
| }; | |||
| const filterContent = ( | |||
| <Radio.Group onChange={handleFilterChange} value={available}> | |||
| <Space direction="vertical"> | |||
| <Radio value={undefined}>{t('all')}</Radio> | |||
| <Radio value={1}>{t('enabled')}</Radio> | |||
| <Radio value={0}>{t('disabled')}</Radio> | |||
| </Space> | |||
| </Radio.Group> | |||
| ); | |||
| return ( | |||
| <div className="flex pr-[25px]"> | |||
| <Segmented | |||
| options={[ | |||
| { label: t(ChunkTextMode.Full), value: ChunkTextMode.Full }, | |||
| { label: t(ChunkTextMode.Ellipse), value: ChunkTextMode.Ellipse }, | |||
| ]} | |||
| onChange={changeChunkTextMode as SegmentedProps['onChange']} | |||
| /> | |||
| <div className="ml-auto"></div> | |||
| <Input | |||
| style={{ width: 200 }} | |||
| size="middle" | |||
| placeholder={t('search')} | |||
| prefix={<SearchOutlined />} | |||
| allowClear | |||
| onChange={handleInputChange} | |||
| value={searchString} | |||
| /> | |||
| <div className="w-[20px]"></div> | |||
| <Popover content={filterContent} placement="bottom" arrow={false}> | |||
| <Button icon={<FilterIcon />} /> | |||
| </Popover> | |||
| <div className="w-[20px]"></div> | |||
| <Button | |||
| icon={<PlusOutlined />} | |||
| type="primary" | |||
| onClick={() => createChunk()} | |||
| /> | |||
| </div> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,221 @@ | |||
| import { ReactComponent as FilterIcon } from '@/assets/filter.svg'; | |||
| import { KnowledgeRouteKey } from '@/constants/knowledge'; | |||
| import { IChunkListResult, useSelectChunkList } from '@/hooks/chunk-hooks'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useKnowledgeBaseId } from '@/hooks/knowledge-hooks'; | |||
| import { | |||
| ArrowLeftOutlined, | |||
| CheckCircleOutlined, | |||
| CloseCircleOutlined, | |||
| DeleteOutlined, | |||
| DownOutlined, | |||
| FilePdfOutlined, | |||
| PlusOutlined, | |||
| SearchOutlined, | |||
| } from '@ant-design/icons'; | |||
| import { | |||
| Button, | |||
| Checkbox, | |||
| Flex, | |||
| Input, | |||
| Menu, | |||
| MenuProps, | |||
| Popover, | |||
| Radio, | |||
| RadioChangeEvent, | |||
| Segmented, | |||
| SegmentedProps, | |||
| Space, | |||
| Typography, | |||
| } from 'antd'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| import { Link } from 'umi'; | |||
| import { ChunkTextMode } from '../../constant'; | |||
| const { Text } = Typography; | |||
| interface IProps | |||
| extends Pick< | |||
| IChunkListResult, | |||
| 'searchString' | 'handleInputChange' | 'available' | 'handleSetAvailable' | |||
| > { | |||
| checked: boolean; | |||
| selectAllChunk: (checked: boolean) => void; | |||
| createChunk: () => void; | |||
| removeChunk: () => void; | |||
| switchChunk: (available: number) => void; | |||
| changeChunkTextMode(mode: ChunkTextMode): void; | |||
| } | |||
| const ChunkToolBar = ({ | |||
| selectAllChunk, | |||
| checked, | |||
| createChunk, | |||
| removeChunk, | |||
| switchChunk, | |||
| changeChunkTextMode, | |||
| available, | |||
| handleSetAvailable, | |||
| searchString, | |||
| handleInputChange, | |||
| }: IProps) => { | |||
| const data = useSelectChunkList(); | |||
| const documentInfo = data?.documentInfo; | |||
| const knowledgeBaseId = useKnowledgeBaseId(); | |||
| const [isShowSearchBox, setIsShowSearchBox] = useState(false); | |||
| const { t } = useTranslate('chunk'); | |||
| const handleSelectAllCheck = useCallback( | |||
| (e: any) => { | |||
| selectAllChunk(e.target.checked); | |||
| }, | |||
| [selectAllChunk], | |||
| ); | |||
| const handleSearchIconClick = () => { | |||
| setIsShowSearchBox(true); | |||
| }; | |||
| const handleSearchBlur = () => { | |||
| if (!searchString?.trim()) { | |||
| setIsShowSearchBox(false); | |||
| } | |||
| }; | |||
| const handleDelete = useCallback(() => { | |||
| removeChunk(); | |||
| }, [removeChunk]); | |||
| const handleEnabledClick = useCallback(() => { | |||
| switchChunk(1); | |||
| }, [switchChunk]); | |||
| const handleDisabledClick = useCallback(() => { | |||
| switchChunk(0); | |||
| }, [switchChunk]); | |||
| const items: MenuProps['items'] = useMemo(() => { | |||
| return [ | |||
| { | |||
| key: '1', | |||
| label: ( | |||
| <> | |||
| <Checkbox onChange={handleSelectAllCheck} checked={checked}> | |||
| <b>{t('selectAll')}</b> | |||
| </Checkbox> | |||
| </> | |||
| ), | |||
| }, | |||
| { type: 'divider' }, | |||
| { | |||
| key: '2', | |||
| label: ( | |||
| <Space onClick={handleEnabledClick}> | |||
| <CheckCircleOutlined /> | |||
| <b>{t('enabledSelected')}</b> | |||
| </Space> | |||
| ), | |||
| }, | |||
| { | |||
| key: '3', | |||
| label: ( | |||
| <Space onClick={handleDisabledClick}> | |||
| <CloseCircleOutlined /> | |||
| <b>{t('disabledSelected')}</b> | |||
| </Space> | |||
| ), | |||
| }, | |||
| { type: 'divider' }, | |||
| { | |||
| key: '4', | |||
| label: ( | |||
| <Space onClick={handleDelete}> | |||
| <DeleteOutlined /> | |||
| <b>{t('deleteSelected')}</b> | |||
| </Space> | |||
| ), | |||
| }, | |||
| ]; | |||
| }, [ | |||
| checked, | |||
| handleSelectAllCheck, | |||
| handleDelete, | |||
| handleEnabledClick, | |||
| handleDisabledClick, | |||
| t, | |||
| ]); | |||
| const content = ( | |||
| <Menu style={{ width: 200 }} items={items} selectable={false} /> | |||
| ); | |||
| const handleFilterChange = (e: RadioChangeEvent) => { | |||
| selectAllChunk(false); | |||
| handleSetAvailable(e.target.value); | |||
| }; | |||
| const filterContent = ( | |||
| <Radio.Group onChange={handleFilterChange} value={available}> | |||
| <Space direction="vertical"> | |||
| <Radio value={undefined}>{t('all')}</Radio> | |||
| <Radio value={1}>{t('enabled')}</Radio> | |||
| <Radio value={0}>{t('disabled')}</Radio> | |||
| </Space> | |||
| </Radio.Group> | |||
| ); | |||
| return ( | |||
| <Flex justify="space-between" align="center"> | |||
| <Space size={'middle'}> | |||
| <Link | |||
| to={`/knowledge/${KnowledgeRouteKey.Dataset}?id=${knowledgeBaseId}`} | |||
| > | |||
| <ArrowLeftOutlined /> | |||
| </Link> | |||
| <FilePdfOutlined /> | |||
| <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')} | |||
| <DownOutlined /> | |||
| </Button> | |||
| </Popover> | |||
| {isShowSearchBox ? ( | |||
| <Input | |||
| size="middle" | |||
| placeholder={t('search')} | |||
| prefix={<SearchOutlined />} | |||
| allowClear | |||
| onChange={handleInputChange} | |||
| onBlur={handleSearchBlur} | |||
| value={searchString} | |||
| /> | |||
| ) : ( | |||
| <Button icon={<SearchOutlined />} onClick={handleSearchIconClick} /> | |||
| )} | |||
| <Popover content={filterContent} placement="bottom" arrow={false}> | |||
| <Button icon={<FilterIcon />} /> | |||
| </Popover> | |||
| <Button | |||
| icon={<PlusOutlined />} | |||
| type="primary" | |||
| onClick={() => createChunk()} | |||
| /> | |||
| </Space> | |||
| </Flex> | |||
| ); | |||
| }; | |||
| export default ChunkToolBar; | |||
| @@ -0,0 +1,21 @@ | |||
| import { formatDate } from '@/utils/date'; | |||
| import { formatBytes } from '@/utils/file-util'; | |||
| type Props = { | |||
| size: number; | |||
| name: string; | |||
| create_date: string; | |||
| }; | |||
| export default ({ size, name, create_date }: Props) => { | |||
| const sizeName = formatBytes(size); | |||
| const dateStr = formatDate(create_date); | |||
| return ( | |||
| <div> | |||
| <h2 className="text-[24px]">{name}</h2> | |||
| <div className="text-[#979AAB] pt-[5px]"> | |||
| Size:{sizeName} Uploaded Time:{dateStr} | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,55 @@ | |||
| import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; | |||
| import { api_host } from '@/utils/api'; | |||
| import { useSize } from 'ahooks'; | |||
| import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types'; | |||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||
| export const useDocumentResizeObserver = () => { | |||
| const [containerWidth, setContainerWidth] = useState<number>(); | |||
| const [containerRef, setContainerRef] = useState<HTMLElement | null>(null); | |||
| const size = useSize(containerRef); | |||
| const onResize = useCallback((width?: number) => { | |||
| if (width) { | |||
| setContainerWidth(width); | |||
| } | |||
| }, []); | |||
| useEffect(() => { | |||
| onResize(size?.width); | |||
| }, [size?.width, onResize]); | |||
| return { containerWidth, setContainerRef }; | |||
| }; | |||
| function highlightPattern(text: string, pattern: string, pageNumber: number) { | |||
| if (pageNumber === 2) { | |||
| return `<mark>${text}</mark>`; | |||
| } | |||
| if (text.trim() !== '' && pattern.match(text)) { | |||
| // return pattern.replace(text, (value) => `<mark>${value}</mark>`); | |||
| return `<mark>${text}</mark>`; | |||
| } | |||
| return text.replace(pattern, (value) => `<mark>${value}</mark>`); | |||
| } | |||
| export const useHighlightText = (searchText: string = '') => { | |||
| const textRenderer: CustomTextRenderer = useCallback( | |||
| (textItem) => { | |||
| return highlightPattern(textItem.str, searchText, textItem.pageNumber); | |||
| }, | |||
| [searchText], | |||
| ); | |||
| return textRenderer; | |||
| }; | |||
| export const useGetDocumentUrl = () => { | |||
| const { documentId } = useGetKnowledgeSearchParams(); | |||
| const url = useMemo(() => { | |||
| return `${api_host}/document/get/${documentId}`; | |||
| }, [documentId]); | |||
| return url; | |||
| }; | |||
| @@ -0,0 +1,13 @@ | |||
| .documentContainer { | |||
| width: 100%; | |||
| // height: calc(100vh - 284px); | |||
| height: calc(100vh - 170px); | |||
| position: relative; | |||
| :global(.PdfHighlighter) { | |||
| overflow-x: hidden; | |||
| } | |||
| :global(.Highlight--scrolledTo .Highlight__part) { | |||
| overflow-x: hidden; | |||
| background-color: rgba(255, 226, 143, 1); | |||
| } | |||
| } | |||
| @@ -0,0 +1,123 @@ | |||
| import { Skeleton } from 'antd'; | |||
| import { memo, useEffect, useRef } from 'react'; | |||
| import { | |||
| AreaHighlight, | |||
| Highlight, | |||
| IHighlight, | |||
| PdfHighlighter, | |||
| PdfLoader, | |||
| Popup, | |||
| } from 'react-pdf-highlighter'; | |||
| import { useGetDocumentUrl } from './hooks'; | |||
| import { useCatchDocumentError } from '@/components/pdf-previewer/hooks'; | |||
| import FileError from '@/pages/document-viewer/file-error'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| highlights: IHighlight[]; | |||
| setWidthAndHeight: (width: number, height: number) => void; | |||
| } | |||
| const HighlightPopup = ({ | |||
| comment, | |||
| }: { | |||
| comment: { text: string; emoji: string }; | |||
| }) => | |||
| comment.text ? ( | |||
| <div className="Highlight__popup"> | |||
| {comment.emoji} {comment.text} | |||
| </div> | |||
| ) : null; | |||
| // TODO: merge with DocumentPreviewer | |||
| const Preview = ({ highlights: state, setWidthAndHeight }: IProps) => { | |||
| const url = useGetDocumentUrl(); | |||
| const ref = useRef<(highlight: IHighlight) => void>(() => {}); | |||
| const error = useCatchDocumentError(url); | |||
| const resetHash = () => {}; | |||
| useEffect(() => { | |||
| if (state.length > 0) { | |||
| ref?.current(state[0]); | |||
| } | |||
| }, [state]); | |||
| return ( | |||
| <div | |||
| className={`${styles.documentContainer} rounded-[10px] overflow-hidden `} | |||
| > | |||
| <PdfLoader | |||
| url={url} | |||
| beforeLoad={<Skeleton active />} | |||
| workerSrc="/pdfjs-dist/pdf.worker.min.js" | |||
| errorMessage={<FileError>{error}</FileError>} | |||
| > | |||
| {(pdfDocument) => { | |||
| pdfDocument.getPage(1).then((page) => { | |||
| const viewport = page.getViewport({ scale: 1 }); | |||
| const width = viewport.width; | |||
| const height = viewport.height; | |||
| setWidthAndHeight(width, height); | |||
| }); | |||
| return ( | |||
| <PdfHighlighter | |||
| pdfDocument={pdfDocument} | |||
| enableAreaSelection={(event) => event.altKey} | |||
| onScrollChange={resetHash} | |||
| scrollRef={(scrollTo) => { | |||
| ref.current = scrollTo; | |||
| }} | |||
| onSelectionFinished={() => null} | |||
| highlightTransform={( | |||
| highlight, | |||
| index, | |||
| setTip, | |||
| hideTip, | |||
| viewportToScaled, | |||
| screenshot, | |||
| isScrolledTo, | |||
| ) => { | |||
| const isTextHighlight = !Boolean( | |||
| highlight.content && highlight.content.image, | |||
| ); | |||
| const component = isTextHighlight ? ( | |||
| <Highlight | |||
| isScrolledTo={isScrolledTo} | |||
| position={highlight.position} | |||
| comment={highlight.comment} | |||
| /> | |||
| ) : ( | |||
| <AreaHighlight | |||
| isScrolledTo={isScrolledTo} | |||
| highlight={highlight} | |||
| onChange={() => {}} | |||
| /> | |||
| ); | |||
| return ( | |||
| <Popup | |||
| popupContent={<HighlightPopup {...highlight} />} | |||
| onMouseOver={(popupContent) => | |||
| setTip(highlight, () => popupContent) | |||
| } | |||
| onMouseOut={hideTip} | |||
| key={index} | |||
| > | |||
| {component} | |||
| </Popup> | |||
| ); | |||
| }} | |||
| highlights={state} | |||
| /> | |||
| ); | |||
| }} | |||
| </PdfLoader> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default memo(Preview); | |||
| @@ -0,0 +1,4 @@ | |||
| export enum ChunkTextMode { | |||
| Full = 'full', | |||
| Ellipse = 'ellipse', | |||
| } | |||
| @@ -0,0 +1,129 @@ | |||
| import { | |||
| useCreateChunk, | |||
| useDeleteChunk, | |||
| useSelectChunkList, | |||
| } from '@/hooks/chunk-hooks'; | |||
| import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks'; | |||
| import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; | |||
| import { IChunk } from '@/interfaces/database/knowledge'; | |||
| import { buildChunkHighlights } from '@/utils/document-util'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| import { IHighlight } from 'react-pdf-highlighter'; | |||
| import { ChunkTextMode } from './constant'; | |||
| export const useHandleChunkCardClick = () => { | |||
| const [selectedChunkId, setSelectedChunkId] = useState<string>(''); | |||
| const handleChunkCardClick = useCallback((chunkId: string) => { | |||
| setSelectedChunkId(chunkId); | |||
| }, []); | |||
| return { handleChunkCardClick, selectedChunkId }; | |||
| }; | |||
| export const useGetSelectedChunk = (selectedChunkId: string) => { | |||
| const data = useSelectChunkList(); | |||
| return ( | |||
| data?.data?.find((x) => x.chunk_id === selectedChunkId) ?? ({} as IChunk) | |||
| ); | |||
| }; | |||
| export const useGetChunkHighlights = (selectedChunkId: string) => { | |||
| const [size, setSize] = useState({ width: 849, height: 1200 }); | |||
| const selectedChunk: IChunk = useGetSelectedChunk(selectedChunkId); | |||
| const highlights: IHighlight[] = useMemo(() => { | |||
| return buildChunkHighlights(selectedChunk, size); | |||
| }, [selectedChunk, size]); | |||
| const setWidthAndHeight = useCallback((width: number, height: number) => { | |||
| setSize((pre) => { | |||
| if (pre.height !== height || pre.width !== width) { | |||
| return { height, width }; | |||
| } | |||
| return pre; | |||
| }); | |||
| }, []); | |||
| return { highlights, setWidthAndHeight }; | |||
| }; | |||
| // 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 }; | |||
| }; | |||
| export const useDeleteChunkByIds = (): { | |||
| removeChunk: (chunkIds: string[], documentId: string) => Promise<number>; | |||
| } => { | |||
| const { deleteChunk } = useDeleteChunk(); | |||
| const showDeleteConfirm = useShowDeleteConfirm(); | |||
| const removeChunk = useCallback( | |||
| (chunkIds: string[], documentId: string) => () => { | |||
| return deleteChunk({ chunkIds, doc_id: documentId }); | |||
| }, | |||
| [deleteChunk], | |||
| ); | |||
| const onRemoveChunk = useCallback( | |||
| (chunkIds: string[], documentId: string): Promise<number> => { | |||
| return showDeleteConfirm({ onOk: removeChunk(chunkIds, documentId) }); | |||
| }, | |||
| [removeChunk, showDeleteConfirm], | |||
| ); | |||
| return { | |||
| removeChunk: onRemoveChunk, | |||
| }; | |||
| }; | |||
| export const useUpdateChunk = () => { | |||
| const [chunkId, setChunkId] = useState<string | undefined>(''); | |||
| const { | |||
| visible: chunkUpdatingVisible, | |||
| hideModal: hideChunkUpdatingModal, | |||
| showModal, | |||
| } = useSetModalState(); | |||
| const { createChunk, loading } = useCreateChunk(); | |||
| const { documentId } = useGetKnowledgeSearchParams(); | |||
| const onChunkUpdatingOk = useCallback( | |||
| async (params: IChunk) => { | |||
| const code = await createChunk({ | |||
| ...params, | |||
| doc_id: documentId, | |||
| chunk_id: chunkId, | |||
| }); | |||
| if (code === 0) { | |||
| hideChunkUpdatingModal(); | |||
| } | |||
| }, | |||
| [createChunk, hideChunkUpdatingModal, chunkId, documentId], | |||
| ); | |||
| const handleShowChunkUpdatingModal = useCallback( | |||
| async (id?: string) => { | |||
| setChunkId(id); | |||
| showModal(); | |||
| }, | |||
| [showModal], | |||
| ); | |||
| return { | |||
| chunkUpdatingLoading: loading, | |||
| onChunkUpdatingOk, | |||
| chunkUpdatingVisible, | |||
| hideChunkUpdatingModal, | |||
| showChunkUpdatingModal: handleShowChunkUpdatingModal, | |||
| chunkId, | |||
| documentId, | |||
| }; | |||
| }; | |||
| @@ -0,0 +1,95 @@ | |||
| .chunkPage { | |||
| padding: 24px; | |||
| padding-top: 2px; | |||
| display: flex; | |||
| // height: calc(100vh - 112px); | |||
| height: 100vh; | |||
| flex-direction: column; | |||
| .filter { | |||
| margin: 10px 0; | |||
| display: flex; | |||
| height: 32px; | |||
| justify-content: space-between; | |||
| } | |||
| .pagePdfWrapper { | |||
| width: 60%; | |||
| } | |||
| .pageWrapper { | |||
| width: 100%; | |||
| } | |||
| .pageContent { | |||
| flex: 1; | |||
| width: 100%; | |||
| padding-right: 12px; | |||
| overflow-y: auto; | |||
| .spin { | |||
| min-height: 400px; | |||
| } | |||
| } | |||
| .documentPreview { | |||
| // width: 40%; | |||
| height: 100%; | |||
| } | |||
| .chunkContainer { | |||
| display: flex; | |||
| // height: calc(100vh - 332px); | |||
| height: calc(100vh - 270px); | |||
| } | |||
| .chunkOtherContainer { | |||
| width: 100%; | |||
| } | |||
| .pageFooter { | |||
| padding-top: 10px; | |||
| height: 32px; | |||
| } | |||
| } | |||
| .container { | |||
| height: 100px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| .content { | |||
| display: flex; | |||
| justify-content: space-between; | |||
| .context { | |||
| flex: 1; | |||
| // width: 207px; | |||
| height: 88px; | |||
| overflow: hidden; | |||
| } | |||
| } | |||
| .footer { | |||
| height: 20px; | |||
| .text { | |||
| margin-left: 10px; | |||
| } | |||
| } | |||
| } | |||
| .card { | |||
| :global { | |||
| .ant-card-body { | |||
| padding: 10px; | |||
| margin: 0; | |||
| } | |||
| margin-bottom: 10px; | |||
| } | |||
| cursor: pointer; | |||
| } | |||
| @@ -0,0 +1,235 @@ | |||
| import { useFetchNextChunkList, useSwitchChunk } from '@/hooks/chunk-hooks'; | |||
| import type { PaginationProps } from 'antd'; | |||
| import { Flex, Pagination, Space, Spin, message } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useCallback, useState } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import ChunkCard from './components/chunk-card'; | |||
| import CreatingModal from './components/chunk-creating-modal'; | |||
| import DocumentPreview from './components/document-preview/preview'; | |||
| import { | |||
| useChangeChunkTextMode, | |||
| useDeleteChunkByIds, | |||
| useGetChunkHighlights, | |||
| useHandleChunkCardClick, | |||
| useUpdateChunk, | |||
| } from './hooks'; | |||
| import ChunkResultBar from './components/chunk-result-bar'; | |||
| import CheckboxSets from './components/chunk-result-bar/checkbox-sets'; | |||
| import DocumentHeader from './components/document-preview/document-header'; | |||
| import styles from './index.less'; | |||
| const Chunk = () => { | |||
| const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]); | |||
| const { removeChunk } = useDeleteChunkByIds(); | |||
| const { | |||
| data: { documentInfo, data = [], total }, | |||
| pagination, | |||
| loading, | |||
| searchString, | |||
| handleInputChange, | |||
| available, | |||
| handleSetAvailable, | |||
| } = useFetchNextChunkList(); | |||
| const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick(); | |||
| const isPdf = documentInfo?.type === 'pdf'; | |||
| const { t } = useTranslation(); | |||
| const { changeChunkTextMode, textMode } = useChangeChunkTextMode(); | |||
| const { switchChunk } = useSwitchChunk(); | |||
| const { | |||
| chunkUpdatingLoading, | |||
| onChunkUpdatingOk, | |||
| showChunkUpdatingModal, | |||
| hideChunkUpdatingModal, | |||
| chunkId, | |||
| chunkUpdatingVisible, | |||
| documentId, | |||
| } = useUpdateChunk(); | |||
| const onPaginationChange: PaginationProps['onShowSizeChange'] = ( | |||
| page, | |||
| size, | |||
| ) => { | |||
| setSelectedChunkIds([]); | |||
| pagination.onChange?.(page, size); | |||
| }; | |||
| const selectAllChunk = useCallback( | |||
| (checked: boolean) => { | |||
| setSelectedChunkIds(checked ? data.map((x) => x.chunk_id) : []); | |||
| }, | |||
| [data], | |||
| ); | |||
| const handleSingleCheckboxClick = useCallback( | |||
| (chunkId: string, checked: boolean) => { | |||
| setSelectedChunkIds((previousIds) => { | |||
| const idx = previousIds.findIndex((x) => x === chunkId); | |||
| const nextIds = [...previousIds]; | |||
| if (checked && idx === -1) { | |||
| nextIds.push(chunkId); | |||
| } else if (!checked && idx !== -1) { | |||
| nextIds.splice(idx, 1); | |||
| } | |||
| return nextIds; | |||
| }); | |||
| }, | |||
| [], | |||
| ); | |||
| const showSelectedChunkWarning = useCallback(() => { | |||
| message.warning(t('message.pleaseSelectChunk')); | |||
| }, [t]); | |||
| const handleRemoveChunk = useCallback(async () => { | |||
| if (selectedChunkIds.length > 0) { | |||
| const resCode: number = await removeChunk(selectedChunkIds, documentId); | |||
| if (resCode === 0) { | |||
| setSelectedChunkIds([]); | |||
| } | |||
| } else { | |||
| showSelectedChunkWarning(); | |||
| } | |||
| }, [selectedChunkIds, documentId, removeChunk, showSelectedChunkWarning]); | |||
| const handleSwitchChunk = useCallback( | |||
| async (available?: number, chunkIds?: string[]) => { | |||
| let ids = chunkIds; | |||
| if (!chunkIds) { | |||
| ids = selectedChunkIds; | |||
| if (selectedChunkIds.length === 0) { | |||
| showSelectedChunkWarning(); | |||
| return; | |||
| } | |||
| } | |||
| const resCode: number = await switchChunk({ | |||
| chunk_ids: ids, | |||
| available_int: available, | |||
| doc_id: documentId, | |||
| }); | |||
| if (!chunkIds && resCode === 0) { | |||
| } | |||
| }, | |||
| [switchChunk, documentId, selectedChunkIds, showSelectedChunkWarning], | |||
| ); | |||
| const { highlights, setWidthAndHeight } = | |||
| useGetChunkHighlights(selectedChunkId); | |||
| return ( | |||
| <> | |||
| <div className={styles.chunkPage}> | |||
| {/* <ChunkToolBar | |||
| selectAllChunk={selectAllChunk} | |||
| createChunk={showChunkUpdatingModal} | |||
| removeChunk={handleRemoveChunk} | |||
| checked={selectedChunkIds.length === data.length} | |||
| switchChunk={handleSwitchChunk} | |||
| changeChunkTextMode={changeChunkTextMode} | |||
| searchString={searchString} | |||
| handleInputChange={handleInputChange} | |||
| available={available} | |||
| handleSetAvailable={handleSetAvailable} | |||
| ></ChunkToolBar> */} | |||
| {/* <Divider></Divider> */} | |||
| <Flex flex={1} gap={'middle'}> | |||
| <div className="w-[40%]"> | |||
| <div className="h-[100px] flex flex-col justify-end pb-[5px]"> | |||
| <DocumentHeader {...documentInfo} /> | |||
| </div> | |||
| {isPdf && ( | |||
| <section className={styles.documentPreview}> | |||
| <DocumentPreview | |||
| highlights={highlights} | |||
| setWidthAndHeight={setWidthAndHeight} | |||
| ></DocumentPreview> | |||
| </section> | |||
| )} | |||
| </div> | |||
| <Flex | |||
| vertical | |||
| className={isPdf ? styles.pagePdfWrapper : styles.pageWrapper} | |||
| > | |||
| <Spin spinning={loading} className={styles.spin} size="large"> | |||
| <div className="h-[100px] flex flex-col justify-end pb-[5px]"> | |||
| <div> | |||
| <h2 className="text-[24px]">Chunk Result</h2> | |||
| <div className="text-[14px] text-[#979AAB]"> | |||
| View the chunked segments used for embedding and retrieval. | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div className=" rounded-[16px] bg-[#FFF]/10 pl-[20px] pb-[20px] pt-[20px] box-border "> | |||
| <ChunkResultBar | |||
| handleInputChange={handleInputChange} | |||
| searchString={searchString} | |||
| changeChunkTextMode={changeChunkTextMode} | |||
| createChunk={showChunkUpdatingModal} | |||
| available={available} | |||
| selectAllChunk={selectAllChunk} | |||
| handleSetAvailable={handleSetAvailable} | |||
| /> | |||
| <div className="pt-[5px] pb-[5px]"> | |||
| <CheckboxSets | |||
| selectAllChunk={selectAllChunk} | |||
| checked={selectedChunkIds.length === data.length} | |||
| /> | |||
| </div> | |||
| <div className={styles.pageContent}> | |||
| <Space | |||
| direction="vertical" | |||
| size={'middle'} | |||
| className={classNames(styles.chunkContainer, { | |||
| [styles.chunkOtherContainer]: !isPdf, | |||
| })} | |||
| > | |||
| {data.map((item) => ( | |||
| <ChunkCard | |||
| item={item} | |||
| key={item.chunk_id} | |||
| editChunk={showChunkUpdatingModal} | |||
| checked={selectedChunkIds.some( | |||
| (x) => x === item.chunk_id, | |||
| )} | |||
| handleCheckboxClick={handleSingleCheckboxClick} | |||
| switchChunk={handleSwitchChunk} | |||
| clickChunkCard={handleChunkCardClick} | |||
| selected={item.chunk_id === selectedChunkId} | |||
| textMode={textMode} | |||
| ></ChunkCard> | |||
| ))} | |||
| </Space> | |||
| </div> | |||
| </div> | |||
| </Spin> | |||
| <div className={styles.pageFooter}> | |||
| <Pagination | |||
| {...pagination} | |||
| total={total} | |||
| size={'small'} | |||
| onChange={onPaginationChange} | |||
| /> | |||
| </div> | |||
| </Flex> | |||
| </Flex> | |||
| </div> | |||
| {chunkUpdatingVisible && ( | |||
| <CreatingModal | |||
| doc_id={documentId} | |||
| chunkId={chunkId} | |||
| hideModal={hideChunkUpdatingModal} | |||
| visible={chunkUpdatingVisible} | |||
| loading={chunkUpdatingLoading} | |||
| onOk={onChunkUpdatingOk} | |||
| parserId={documentInfo.parser_id} | |||
| /> | |||
| )} | |||
| </> | |||
| ); | |||
| }; | |||
| export default Chunk; | |||
| @@ -0,0 +1,24 @@ | |||
| export type FormListItem = { | |||
| frequency: number; | |||
| tag: string; | |||
| }; | |||
| export function transformTagFeaturesArrayToObject( | |||
| list: Array<FormListItem> = [], | |||
| ) { | |||
| return list.reduce<Record<string, number>>((pre, cur) => { | |||
| pre[cur.tag] = cur.frequency; | |||
| return pre; | |||
| }, {}); | |||
| } | |||
| export function transformTagFeaturesObjectToArray( | |||
| object: Record<string, number> = {}, | |||
| ) { | |||
| return Object.keys(object).reduce<Array<FormListItem>>((pre, key) => { | |||
| pre.push({ frequency: object[key], tag: key }); | |||
| return pre; | |||
| }, []); | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| import { KnowledgeRouteKey } from '@/constants/knowledge'; | |||
| export const routeMap = { | |||
| [KnowledgeRouteKey.Dataset]: 'Dataset', | |||
| [KnowledgeRouteKey.Testing]: 'Retrieval testing', | |||
| [KnowledgeRouteKey.Configuration]: 'Configuration', | |||
| }; | |||
| export enum KnowledgeDatasetRouteKey { | |||
| Chunk = 'chunk', | |||
| File = 'file', | |||
| } | |||
| export const datasetRouteMap = { | |||
| [KnowledgeDatasetRouteKey.Chunk]: 'Chunk', | |||
| [KnowledgeDatasetRouteKey.File]: 'File Upload', | |||
| }; | |||
| export * from '@/constants/knowledge'; | |||
| export const TagRenameId = 'tagRename'; | |||
| @@ -0,0 +1,19 @@ | |||
| .container { | |||
| display: flex; | |||
| height: 100%; | |||
| width: 100%; | |||
| .contentWrapper { | |||
| flex: 1; | |||
| overflow-x: auto; | |||
| height: 100%; | |||
| background-color: rgba(255, 255, 255, 0.1); | |||
| padding: 16px 20px 28px 40px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| .content { | |||
| background-color: rgba(255, 255, 255, 0.1); | |||
| margin-top: 16px; | |||
| flex: 1; | |||
| } | |||
| } | |||
| @@ -0,0 +1,74 @@ | |||
| import { useKnowledgeBaseId } from '@/hooks/knowledge-hooks'; | |||
| import { | |||
| useNavigateWithFromState, | |||
| useSecondPathName, | |||
| useThirdPathName, | |||
| } from '@/hooks/route-hook'; | |||
| import { Breadcrumb } from 'antd'; | |||
| import { ItemType } from 'antd/es/breadcrumb/Breadcrumb'; | |||
| import { useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { Link, Outlet } from 'umi'; | |||
| import Siderbar from './components/knowledge-sidebar'; | |||
| import { KnowledgeDatasetRouteKey, KnowledgeRouteKey } from './constant'; | |||
| import styles from './index.less'; | |||
| const KnowledgeAdding = () => { | |||
| const knowledgeBaseId = useKnowledgeBaseId(); | |||
| const { t } = useTranslation(); | |||
| const activeKey: KnowledgeRouteKey = | |||
| (useSecondPathName() as KnowledgeRouteKey) || KnowledgeRouteKey.Dataset; | |||
| const datasetActiveKey: KnowledgeDatasetRouteKey = | |||
| useThirdPathName() as KnowledgeDatasetRouteKey; | |||
| const gotoList = useNavigateWithFromState(); | |||
| const breadcrumbItems: ItemType[] = useMemo(() => { | |||
| const items: ItemType[] = [ | |||
| { | |||
| title: ( | |||
| <a onClick={() => gotoList('/knowledge')}> | |||
| {t('header.knowledgeBase')} | |||
| </a> | |||
| ), | |||
| }, | |||
| { | |||
| title: datasetActiveKey ? ( | |||
| <Link | |||
| to={`/knowledge/${KnowledgeRouteKey.Dataset}?id=${knowledgeBaseId}`} | |||
| > | |||
| {t(`knowledgeDetails.${activeKey}`)} | |||
| </Link> | |||
| ) : ( | |||
| t(`knowledgeDetails.${activeKey}`) | |||
| ), | |||
| }, | |||
| ]; | |||
| if (datasetActiveKey) { | |||
| items.push({ | |||
| title: t(`knowledgeDetails.${datasetActiveKey}`), | |||
| }); | |||
| } | |||
| return items; | |||
| }, [activeKey, datasetActiveKey, gotoList, knowledgeBaseId, t]); | |||
| return ( | |||
| <> | |||
| <div className={styles.container}> | |||
| <Siderbar></Siderbar> | |||
| <div className={styles.contentWrapper}> | |||
| <Breadcrumb items={breadcrumbItems} /> | |||
| <div className={styles.content}> | |||
| <Outlet></Outlet> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </> | |||
| ); | |||
| }; | |||
| export default KnowledgeAdding; | |||
| @@ -262,6 +262,11 @@ const routes = [ | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| path: `${Routes.ParsedResult}/chunks`, | |||
| layout: false, | |||
| component: `@/pages${Routes.Chunk}/parsed-result/add-knowledge/components/knowledge-chunk`, | |||
| }, | |||
| { | |||
| path: Routes.Chunk, | |||
| layout: false, | |||
| @@ -270,10 +275,10 @@ const routes = [ | |||
| path: Routes.Chunk, | |||
| component: `@/pages${Routes.Chunk}`, | |||
| routes: [ | |||
| { | |||
| path: `${Routes.ParsedResult}/:id`, | |||
| component: `@/pages${Routes.Chunk}/parsed-result`, | |||
| }, | |||
| // { | |||
| // path: `${Routes.ParsedResult}/:id`, | |||
| // component: `@/pages${Routes.Chunk}/parsed-result`, | |||
| // }, | |||
| { | |||
| path: `${Routes.ChunkResult}/:id`, | |||
| component: `@/pages${Routes.Chunk}/chunk-result`, | |||