### What problem does this PR solve? Refactor Pdf 2 Slices page to new style ### Type of change - [X] Refactoringtags/v0.19.1
| const navigateToChunkParsedResult = useCallback( | const navigateToChunkParsedResult = useCallback( | ||||
| (id: string, knowledgeId?: string) => () => { | (id: string, knowledgeId?: string) => () => { | ||||
| navigate( | navigate( | ||||
| `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`, | |||||
| // `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`, | |||||
| `${Routes.ParsedResult}/chunks?id=${knowledgeId}&doc_id=${id}`, | |||||
| ); | ); | ||||
| }, | }, | ||||
| [navigate], | [navigate], |
| 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, | |||||
| }; | |||||
| }; |
| .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; | |||||
| } |
| 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; |
| 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; |
| 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> | |||||
| ); | |||||
| }; |
| 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> | |||||
| ); | |||||
| }; |
| 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> | |||||
| ); | |||||
| }; |
| 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; |
| 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> | |||||
| ); | |||||
| }; |
| 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; | |||||
| }; |
| .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); | |||||
| } | |||||
| } |
| 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); |
| export enum ChunkTextMode { | |||||
| Full = 'full', | |||||
| Ellipse = 'ellipse', | |||||
| } |
| 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, | |||||
| }; | |||||
| }; |
| .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; | |||||
| } |
| 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; |
| 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; | |||||
| }, []); | |||||
| } |
| 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'; |
| .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; | |||||
| } | |||||
| } |
| 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; |
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| { | |||||
| path: `${Routes.ParsedResult}/chunks`, | |||||
| layout: false, | |||||
| component: `@/pages${Routes.Chunk}/parsed-result/add-knowledge/components/knowledge-chunk`, | |||||
| }, | |||||
| { | { | ||||
| path: Routes.Chunk, | path: Routes.Chunk, | ||||
| layout: false, | layout: false, | ||||
| path: Routes.Chunk, | path: Routes.Chunk, | ||||
| component: `@/pages${Routes.Chunk}`, | component: `@/pages${Routes.Chunk}`, | ||||
| routes: [ | 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`, | path: `${Routes.ChunkResult}/:id`, | ||||
| component: `@/pages${Routes.Chunk}/chunk-result`, | component: `@/pages${Routes.Chunk}/chunk-result`, |