Просмотр исходного кода

Refa: Pdf 2 Slices page to new style (#8386)

### What problem does this PR solve?

Refactor Pdf 2 Slices page to new style

### Type of change

- [X] Refactoring
tags/v0.19.1
BlueYu-0221 4 месяцев назад
Родитель
Сommit
bb3d3f921a
Аккаунт пользователя с таким Email не найден
22 измененных файлов: 1629 добавлений и 5 удалений
  1. 2
    1
      web/src/hooks/logic-hooks/navigate-hooks.ts
  2. 91
    0
      web/src/hooks/use-chunk-request.ts
  3. 34
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.less
  4. 101
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx
  5. 140
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx
  6. 107
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/tag-feature-item.tsx
  7. 39
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-result-bar/checkbox-sets.tsx
  8. 72
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-result-bar/index.tsx
  9. 221
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx
  10. 21
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/document-header.tsx
  11. 55
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/hooks.ts
  12. 13
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/index.less
  13. 123
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/preview.tsx
  14. 4
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/constant.ts
  15. 129
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/hooks.ts
  16. 95
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.less
  17. 235
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.tsx
  18. 24
    0
      web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/utils.ts
  19. 21
    0
      web/src/pages/chunk/parsed-result/add-knowledge/constant.ts
  20. 19
    0
      web/src/pages/chunk/parsed-result/add-knowledge/index.less
  21. 74
    0
      web/src/pages/chunk/parsed-result/add-knowledge/index.tsx
  22. 9
    4
      web/src/routes.ts

+ 2
- 1
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],

+ 91
- 0
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<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,
};
};

+ 34
- 0
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;
}

+ 101
- 0
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 (
<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;

+ 140
- 0
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<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;

+ 107
- 0
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 (
<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>
);
};

+ 39
- 0
web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-result-bar/checkbox-sets.tsx Просмотреть файл

@@ -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>
);
};

+ 72
- 0
web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-result-bar/index.tsx Просмотреть файл

@@ -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>
);
};

+ 221
- 0
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: (
<>
<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;

+ 21
- 0
web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/document-header.tsx Просмотреть файл

@@ -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>
);
};

+ 55
- 0
web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/hooks.ts Просмотреть файл

@@ -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;
};

+ 13
- 0
web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/index.less Просмотреть файл

@@ -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);
}
}

+ 123
- 0
web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/preview.tsx Просмотреть файл

@@ -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);

+ 4
- 0
web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/constant.ts Просмотреть файл

@@ -0,0 +1,4 @@
export enum ChunkTextMode {
Full = 'full',
Ellipse = 'ellipse',
}

+ 129
- 0
web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/hooks.ts Просмотреть файл

@@ -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,
};
};

+ 95
- 0
web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.less Просмотреть файл

@@ -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;
}

+ 235
- 0
web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.tsx Просмотреть файл

@@ -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;

+ 24
- 0
web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/utils.ts Просмотреть файл

@@ -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;
}, []);
}

+ 21
- 0
web/src/pages/chunk/parsed-result/add-knowledge/constant.ts Просмотреть файл

@@ -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';

+ 19
- 0
web/src/pages/chunk/parsed-result/add-knowledge/index.less Просмотреть файл

@@ -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;
}
}

+ 74
- 0
web/src/pages/chunk/parsed-result/add-knowledge/index.tsx Просмотреть файл

@@ -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;

+ 9
- 4
web/src/routes.ts Просмотреть файл

@@ -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`,

Загрузка…
Отмена
Сохранить