diff --git a/web/src/hooks/logic-hooks/navigate-hooks.ts b/web/src/hooks/logic-hooks/navigate-hooks.ts index 5cdff81b9..c5131f2cf 100644 --- a/web/src/hooks/logic-hooks/navigate-hooks.ts +++ b/web/src/hooks/logic-hooks/navigate-hooks.ts @@ -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], diff --git a/web/src/hooks/use-chunk-request.ts b/web/src/hooks/use-chunk-request.ts new file mode 100644 index 000000000..1cb802423 --- /dev/null +++ b/web/src/hooks/use-chunk-request.ts @@ -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(); + 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 = 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, + }; +}; diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.less b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.less new file mode 100644 index 000000000..127a5c790 --- /dev/null +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.less @@ -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; +} diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx new file mode 100644 index 000000000..b7e61e06a --- /dev/null +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx @@ -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 ( + + + + {item.image_id && ( + + } + > + + + )} + +
+
+
+ +
+ +
+
+
+ ); +}; + +export default ChunkCard; diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx new file mode 100644 index 000000000..abcf1742c --- /dev/null +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx @@ -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 & 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 ( + +
+ + label={t('chunk.chunk')} + name="content_with_weight" + rules={[{ required: true, message: t('chunk.chunkMessage') }]} + > + + + + label={t('chunk.keyword')} name="important_kwd"> + + + + label={t('chunk.question')} + name="question_kwd" + tooltip={t('chunk.questionTip')} + > + + + {isTagParser && ( + + label={t('knowledgeConfiguration.tagName')} + name="tag_kwd" + > + + + )} + + {!isTagParser && } + + + {chunkId && ( +
+ + + + + + {t('common.delete')} + + +
+ )} +
+ ); +}; +export default ChunkCreatingModal; diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/tag-feature-item.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/tag-feature-item.tsx new file mode 100644 index 000000000..64c192eb7 --- /dev/null +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/tag-feature-item.tsx @@ -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 ( + + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( +
+
+ + } + allowClear + onChange={handleInputChange} + value={searchString} + /> +
+ +
+ ); +}; diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx new file mode 100644 index 000000000..6a513ba78 --- /dev/null +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx @@ -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: ( + <> + + {t('selectAll')} + + + ), + }, + { type: 'divider' }, + { + key: '2', + label: ( + + + {t('enabledSelected')} + + ), + }, + { + key: '3', + label: ( + + + {t('disabledSelected')} + + ), + }, + { type: 'divider' }, + { + key: '4', + label: ( + + + {t('deleteSelected')} + + ), + }, + ]; + }, [ + checked, + handleSelectAllCheck, + handleDelete, + handleEnabledClick, + handleDisabledClick, + t, + ]); + + const content = ( + + ); + + const handleFilterChange = (e: RadioChangeEvent) => { + selectAllChunk(false); + handleSetAvailable(e.target.value); + }; + + const filterContent = ( + + + {t('all')} + {t('enabled')} + {t('disabled')} + + + ); + + return ( + + + + + + + + {documentInfo?.name} + + + + + + + + {isShowSearchBox ? ( + } + allowClear + onChange={handleInputChange} + onBlur={handleSearchBlur} + value={searchString} + /> + ) : ( +