Feat: Delete useless knowledge base, chat, and search files. #10427 (#11568)

### What problem does this PR solve?

Feat: Delete useless knowledge base, chat, and search files.  #10427
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-11-27 17:54:27 +08:00
committed by GitHub
parent c71d25f744
commit 89d82ff031
171 changed files with 161 additions and 11112 deletions

View File

@ -6,7 +6,9 @@ import {
} from '@/hooks/document-hooks';
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
import {
currentReg,
preprocessLaTeX,
replaceTextByOldReg,
replaceThinkToSection,
showImage,
} from '@/utils/chat';
@ -32,7 +34,6 @@ import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import { visitParents } from 'unist-util-visit-parents';
import { currentReg, replaceTextByOldReg } from '../pages/next-chats/utils';
import styles from './floating-chat-widget-markdown.less';
import { useIsDarkTheme } from './theme-provider';

View File

@ -21,11 +21,12 @@ import { useTranslation } from 'react-i18next';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
import {
currentReg,
preprocessLaTeX,
replaceTextByOldReg,
replaceThinkToSection,
showImage,
} from '@/utils/chat';
import { currentReg, replaceTextByOldReg } from '../utils';
import classNames from 'classnames';
import { omit } from 'lodash';

View File

@ -1,6 +1,10 @@
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
import { MessageType } from '@/constants/chat';
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
import {
IMessage,
IReference,
IReferenceChunk,
} from '@/interfaces/database/chat';
import classNames from 'classnames';
import { memo, useCallback, useEffect, useMemo } from 'react';
@ -10,9 +14,8 @@ import {
} from '@/hooks/document-hooks';
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import { cn } from '@/lib/utils';
import { IMessage } from '@/pages/chat/interface';
import MarkdownContent from '@/pages/chat/markdown-content';
import { Avatar, Flex, Space } from 'antd';
import MarkdownContent from '../markdown-content';
import { ReferenceDocumentList } from '../next-message-item/reference-document-list';
import { InnerUploadedMessageFiles } from '../next-message-item/uploaded-message-files';
import { useTheme } from '../theme-provider';

View File

@ -19,13 +19,14 @@ import { useTranslation } from 'react-i18next';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
import {
currentReg,
preprocessLaTeX,
replaceTextByOldReg,
replaceThinkToSection,
showImage,
} from '@/utils/chat';
import { cn } from '@/lib/utils';
import { currentReg, replaceTextByOldReg } from '@/pages/chat/utils';
import classNames from 'classnames';
import { omit } from 'lodash';
import { pipe } from 'lodash/fp';

View File

@ -1,6 +1,10 @@
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
import { MessageType } from '@/constants/chat';
import { IReferenceChunk, IReferenceObject } from '@/interfaces/database/chat';
import {
IMessage,
IReferenceChunk,
IReferenceObject,
} from '@/interfaces/database/chat';
import classNames from 'classnames';
import {
PropsWithChildren,
@ -17,7 +21,6 @@ import { INodeEvent, MessageEventType } from '@/hooks/use-send-message';
import { cn } from '@/lib/utils';
import { AgentChatContext } from '@/pages/agent/context';
import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workflow-timeline';
import { IMessage } from '@/pages/chat/interface';
import { downloadFile } from '@/services/file-manager-service';
import { downloadFileFromBlob } from '@/utils/file-util';
import { isEmpty } from 'lodash';

View File

@ -5,8 +5,8 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { TagRenameId } from '@/constants/knowledge';
import { IModalProps } from '@/interfaces/common';
import { TagRenameId } from '@/pages/add-knowledge/constant';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { ButtonLoading } from '../ui/button';

View File

@ -13,8 +13,8 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { TagRenameId } from '@/constants/knowledge';
import { IModalProps } from '@/interfaces/common';
import { TagRenameId } from '@/pages/add-knowledge/constant';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';

View File

@ -92,3 +92,5 @@ export enum DocumentParserType {
Tag = 'tag',
KnowledgeGraph = 'knowledge_graph',
}
export const TagRenameId = 'tagRename';

View File

@ -1,5 +1,6 @@
import { ChatSearchParams } from '@/constants/chat';
import {
IClientConversation,
IConversation,
IDialog,
IStats,
@ -10,8 +11,7 @@ import {
IFeedbackRequestBody,
} from '@/interfaces/request/chat';
import i18n from '@/locales/config';
import { IClientConversation } from '@/pages/chat/interface';
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
import chatService from '@/services/chat-service';
import {
buildMessageListWithUuid,

View File

@ -1,7 +1,7 @@
import { DSL, IFlow } from '@/interfaces/database/flow';
import { IDebugSingleRequestBody } from '@/interfaces/request/flow';
import i18n from '@/locales/config';
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
import flowService from '@/services/flow-service';
import { buildMessageListWithUuid } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';

View File

@ -2,9 +2,13 @@ import { Authorization } from '@/constants/authorization';
import { MessageType } from '@/constants/chat';
import { LanguageTranslationMap } from '@/constants/common';
import { ResponseType } from '@/interfaces/database/base';
import { IAnswer, Message } from '@/interfaces/database/chat';
import {
IAnswer,
IClientConversation,
IMessage,
Message,
} from '@/interfaces/database/chat';
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { IClientConversation, IMessage } from '@/pages/chat/interface';
import api from '@/utils/api';
import { getAuthorization } from '@/utils/authorization-util';
import { buildMessageUuid } from '@/utils/chat';

View File

@ -14,7 +14,7 @@ import { IDebugSingleRequestBody } from '@/interfaces/request/agent';
import i18n from '@/locales/config';
import { BeginId } from '@/pages/agent/constant';
import { IInputs } from '@/pages/agent/interface';
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
import agentService, {
fetchAgentLogsByCanvasId,
fetchPipeLineList,

View File

@ -2,12 +2,12 @@ import { FileUploadProps } from '@/components/file-upload';
import message from '@/components/ui/message';
import { ChatSearchParams } from '@/constants/chat';
import {
IClientConversation,
IConversation,
IDialog,
IExternalChatInfo,
} from '@/interfaces/database/chat';
import { IAskRequestBody } from '@/interfaces/request/chat';
import { IClientConversation } from '@/pages/next-chats/chat/interface';
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
import { isConversationIdExist } from '@/pages/next-chats/utils';
import chatService from '@/services/next-chat-service';

View File

@ -183,3 +183,12 @@ export interface IExternalChatInfo {
title: string;
prologue?: string;
}
export interface IMessage extends Message {
id: string;
reference?: IReference; // the latest news has reference
}
export interface IClientConversation extends IConversation {
message: IMessage[];
}

View File

@ -1,34 +0,0 @@
.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;
}

View File

@ -1,101 +0,0 @@
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;

View File

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

View File

@ -1,107 +0,0 @@
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>
);
};

View File

@ -1,221 +0,0 @@
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;

View File

@ -1,55 +0,0 @@
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;
};

View File

@ -1,12 +0,0 @@
.documentContainer {
width: 100%;
height: calc(100vh - 284px);
position: relative;
:global(.PdfHighlighter) {
overflow-x: hidden;
}
:global(.Highlight--scrolledTo .Highlight__part) {
overflow-x: hidden;
background-color: rgba(255, 226, 143, 1);
}
}

View File

@ -1,121 +0,0 @@
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}>
<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);

View File

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

View File

@ -1,129 +0,0 @@
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,
};
};

View File

@ -1,92 +0,0 @@
.chunkPage {
padding: 24px;
display: flex;
// height: calc(100vh - 112px);
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);
}
.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;
}

View File

@ -1,202 +0,0 @@
import { useFetchNextChunkList, useSwitchChunk } from '@/hooks/chunk-hooks';
import type { PaginationProps } from 'antd';
import { Divider, 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 ChunkToolBar from './components/chunk-toolbar';
import DocumentPreview from './components/document-preview/preview';
import {
useChangeChunkTextMode,
useDeleteChunkByIds,
useGetChunkHighlights,
useHandleChunkCardClick,
useUpdateChunk,
} from './hooks';
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'}>
<Flex
vertical
className={isPdf ? styles.pagePdfWrapper : styles.pageWrapper}
>
<Spin spinning={loading} className={styles.spin} size="large">
<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>
</Spin>
<div className={styles.pageFooter}>
<Pagination
{...pagination}
total={total}
size={'small'}
onChange={onPaginationChange}
/>
</div>
</Flex>
{isPdf && (
<section className={styles.documentPreview}>
<DocumentPreview
highlights={highlights}
setWidthAndHeight={setWidthAndHeight}
></DocumentPreview>
</section>
)}
</Flex>
</div>
{chunkUpdatingVisible && (
<CreatingModal
doc_id={documentId}
chunkId={chunkId}
hideModal={hideChunkUpdatingModal}
visible={chunkUpdatingVisible}
loading={chunkUpdatingLoading}
onOk={onChunkUpdatingOk}
parserId={documentInfo.parser_id}
/>
)}
</>
);
};
export default Chunk;

View File

@ -1,24 +0,0 @@
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;
}, []);
}

View File

@ -1,7 +0,0 @@
import { Outlet } from 'umi';
export const KnowledgeDataset = () => {
return <Outlet></Outlet>;
};
export default KnowledgeDataset;

View File

@ -1,17 +0,0 @@
import { RunningStatus } from '@/constants/knowledge';
export const RunningStatusMap = {
[RunningStatus.UNSTART]: {
label: 'UNSTART',
color: 'cyan',
},
[RunningStatus.RUNNING]: {
label: 'Parsing',
color: 'blue',
},
[RunningStatus.CANCEL]: { label: 'CANCEL', color: 'orange' },
[RunningStatus.DONE]: { label: 'SUCCESS', color: 'geekblue' },
[RunningStatus.FAIL]: { label: 'FAIL', color: 'red' },
};
export * from '@/constants/knowledge';

View File

@ -1,49 +0,0 @@
import { IModalManagerChildrenProps } from '@/components/modal-manager';
import { Form, Input, Modal } from 'antd';
import React from 'react';
type FieldType = {
name?: string;
};
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
loading: boolean;
onOk: (name: string) => void;
showModal?(): void;
}
const FileCreatingModal: React.FC<IProps> = ({ visible, hideModal, onOk }) => {
const [form] = Form.useForm();
const handleOk = async () => {
const values = await form.validateFields();
onOk(values.name);
};
return (
<Modal
title="File Name"
open={visible}
onOk={handleOk}
onCancel={hideModal}
>
<Form
form={form}
name="validateOnly"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
style={{ maxWidth: 600 }}
autoComplete="off"
>
<Form.Item<FieldType>
label="File Name"
name="name"
rules={[{ required: true, message: 'Please input name!' }]}
>
<Input />
</Form.Item>
</Form>
</Modal>
);
};
export default FileCreatingModal;

View File

@ -1,240 +0,0 @@
import { ReactComponent as CancelIcon } from '@/assets/svg/cancel.svg';
import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg';
import { ReactComponent as DisableIcon } from '@/assets/svg/disable.svg';
import { ReactComponent as EnableIcon } from '@/assets/svg/enable.svg';
import { ReactComponent as RunIcon } from '@/assets/svg/run.svg';
import { useShowDeleteConfirm, useTranslate } from '@/hooks/common-hooks';
import {
useRemoveNextDocument,
useRunNextDocument,
useSetNextDocumentStatus,
} from '@/hooks/document-hooks';
import { IDocumentInfo } from '@/interfaces/database/document';
import {
DownOutlined,
FileOutlined,
FileTextOutlined,
PlusOutlined,
SearchOutlined,
} from '@ant-design/icons';
import { Button, Dropdown, Flex, Input, MenuProps, Space } from 'antd';
import { useCallback, useMemo } from 'react';
import { toast } from 'sonner';
import { RunningStatus } from './constant';
import styles from './index.less';
interface IProps {
selectedRowKeys: string[];
showCreateModal(): void;
showWebCrawlModal(): void;
showDocumentUploadModal(): void;
searchString: string;
handleInputChange: React.ChangeEventHandler<HTMLInputElement>;
documents: IDocumentInfo[];
}
const DocumentToolbar = ({
searchString,
selectedRowKeys,
showCreateModal,
showDocumentUploadModal,
handleInputChange,
documents,
}: IProps) => {
const { t } = useTranslate('knowledgeDetails');
const { removeDocument } = useRemoveNextDocument();
const showDeleteConfirm = useShowDeleteConfirm();
const { runDocumentByIds } = useRunNextDocument();
const { setDocumentStatus } = useSetNextDocumentStatus();
const actionItems: MenuProps['items'] = useMemo(() => {
return [
{
key: '1',
onClick: showDocumentUploadModal,
label: (
<div>
<Button type="link">
<Space>
<FileTextOutlined />
{t('localFiles')}
</Space>
</Button>
</div>
),
},
{ type: 'divider' },
{
key: '3',
onClick: showCreateModal,
label: (
<div>
<Button type="link">
<FileOutlined />
{t('emptyFiles')}
</Button>
</div>
),
},
];
}, [showDocumentUploadModal, showCreateModal, t]);
const handleDelete = useCallback(() => {
const deletedKeys = selectedRowKeys.filter(
(x) =>
!documents
.filter((y) => y.run === RunningStatus.RUNNING)
.some((y) => y.id === x),
);
if (deletedKeys.length === 0) {
toast.error(t('theDocumentBeingParsedCannotBeDeleted'));
return;
}
showDeleteConfirm({
onOk: () => {
removeDocument(deletedKeys);
},
});
}, [selectedRowKeys, showDeleteConfirm, documents, t, removeDocument]);
const runDocument = useCallback(
(run: number) => {
runDocumentByIds({
documentIds: selectedRowKeys,
run,
shouldDelete: false,
});
},
[runDocumentByIds, selectedRowKeys],
);
const handleRunClick = useCallback(() => {
runDocument(1);
}, [runDocument]);
const handleCancelClick = useCallback(() => {
runDocument(2);
}, [runDocument]);
const onChangeStatus = useCallback(
(enabled: boolean) => {
selectedRowKeys.forEach((id) => {
setDocumentStatus({ status: enabled, documentId: id });
});
},
[selectedRowKeys, setDocumentStatus],
);
const handleEnableClick = useCallback(() => {
onChangeStatus(true);
}, [onChangeStatus]);
const handleDisableClick = useCallback(() => {
onChangeStatus(false);
}, [onChangeStatus]);
const disabled = selectedRowKeys.length === 0;
const items: MenuProps['items'] = useMemo(() => {
return [
{
key: '0',
onClick: handleEnableClick,
label: (
<Flex gap={10}>
<EnableIcon></EnableIcon>
<b>{t('enabled')}</b>
</Flex>
),
},
{
key: '1',
onClick: handleDisableClick,
label: (
<Flex gap={10}>
<DisableIcon></DisableIcon>
<b>{t('disabled')}</b>
</Flex>
),
},
{ type: 'divider' },
{
key: '2',
onClick: handleRunClick,
label: (
<Flex gap={10}>
<RunIcon></RunIcon>
<b>{t('run')}</b>
</Flex>
),
},
{
key: '3',
onClick: handleCancelClick,
label: (
<Flex gap={10}>
<CancelIcon />
<b>{t('cancel')}</b>
</Flex>
),
},
{ type: 'divider' },
{
key: '4',
onClick: handleDelete,
label: (
<Flex gap={10}>
<span className={styles.deleteIconWrapper}>
<DeleteIcon width={18} />
</span>
<b>{t('delete', { keyPrefix: 'common' })}</b>
</Flex>
),
},
];
}, [
handleDelete,
handleRunClick,
handleCancelClick,
t,
handleDisableClick,
handleEnableClick,
]);
return (
<div className={styles.filter}>
<Dropdown
menu={{ items }}
placement="bottom"
arrow={false}
disabled={disabled}
>
<Button>
<Space>
<b> {t('bulk')}</b>
<DownOutlined />
</Space>
</Button>
</Dropdown>
<Space>
<Input
placeholder={t('searchFiles')}
value={searchString}
style={{ width: 220 }}
allowClear
onChange={handleInputChange}
prefix={<SearchOutlined />}
/>
<Dropdown menu={{ items: actionItems }} trigger={['click']}>
<Button type="primary" icon={<PlusOutlined />}>
{t('addFile')}
</Button>
</Dropdown>
</Space>
</div>
);
};
export default DocumentToolbar;

View File

@ -1,364 +0,0 @@
import { useSetModalState } from '@/hooks/common-hooks';
import {
useCreateNextDocument,
useNextWebCrawl,
useRunNextDocument,
useSaveNextDocumentName,
useSetDocumentMeta,
useSetNextDocumentParser,
useUploadNextDocument,
} from '@/hooks/document-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { IDocumentInfo } from '@/interfaces/database/document';
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
import { UploadFile } from 'antd';
import { TableRowSelection } from 'antd/es/table/interface';
import { useCallback, useState } from 'react';
import { useNavigate } from 'umi';
import { KnowledgeRouteKey } from './constant';
export const useNavigateToOtherPage = () => {
const navigate = useNavigate();
const { knowledgeId } = useGetKnowledgeSearchParams();
const linkToUploadPage = useCallback(() => {
navigate(`/knowledge/dataset/upload?id=${knowledgeId}`);
}, [navigate, knowledgeId]);
const toChunk = useCallback(
(id: string) => {
navigate(
`/knowledge/${KnowledgeRouteKey.Dataset}/chunk?id=${knowledgeId}&doc_id=${id}`,
);
},
[navigate, knowledgeId],
);
return { linkToUploadPage, toChunk };
};
export const useRenameDocument = (documentId: string) => {
const { saveName, loading } = useSaveNextDocumentName();
const {
visible: renameVisible,
hideModal: hideRenameModal,
showModal: showRenameModal,
} = useSetModalState();
const onRenameOk = useCallback(
async (name: string) => {
const ret = await saveName({ documentId, name });
if (ret === 0) {
hideRenameModal();
}
},
[hideRenameModal, saveName, documentId],
);
return {
renameLoading: loading,
onRenameOk,
renameVisible,
hideRenameModal,
showRenameModal,
};
};
export const useCreateEmptyDocument = () => {
const { createDocument, loading } = useCreateNextDocument();
const {
visible: createVisible,
hideModal: hideCreateModal,
showModal: showCreateModal,
} = useSetModalState();
const onCreateOk = useCallback(
async (name: string) => {
const ret = await createDocument(name);
if (ret === 0) {
hideCreateModal();
}
},
[hideCreateModal, createDocument],
);
return {
createLoading: loading,
onCreateOk,
createVisible,
hideCreateModal,
showCreateModal,
};
};
export const useChangeDocumentParser = (documentId: string) => {
const { setDocumentParser, loading } = useSetNextDocumentParser();
const {
visible: changeParserVisible,
hideModal: hideChangeParserModal,
showModal: showChangeParserModal,
} = useSetModalState();
const onChangeParserOk = useCallback(
async (parserId: string, parserConfig: IChangeParserConfigRequestBody) => {
const ret = await setDocumentParser({
parserId,
documentId,
parserConfig,
});
if (ret === 0) {
hideChangeParserModal();
}
},
[hideChangeParserModal, setDocumentParser, documentId],
);
return {
changeParserLoading: loading,
onChangeParserOk,
changeParserVisible,
hideChangeParserModal,
showChangeParserModal,
};
};
export const useGetRowSelection = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const rowSelection: TableRowSelection<IDocumentInfo> = {
selectedRowKeys,
onChange: (newSelectedRowKeys: React.Key[]) => {
setSelectedRowKeys(newSelectedRowKeys);
},
};
return rowSelection;
};
export const useHandleUploadDocument = () => {
const {
visible: documentUploadVisible,
hideModal: hideDocumentUploadModal,
showModal: showDocumentUploadModal,
} = useSetModalState();
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [uploadProgress, setUploadProgress] = useState<number>(0);
const { uploadDocument, loading } = useUploadNextDocument();
const { runDocumentByIds } = useRunNextDocument();
const onDocumentUploadOk = useCallback(
async ({
parseOnCreation,
directoryFileList,
}: {
directoryFileList: UploadFile[];
parseOnCreation: boolean;
}): Promise<number | undefined> => {
const processFileGroup = async (filesPart: UploadFile[]) => {
// set status to uploading on files
setFileList(
fileList.map((file) => {
if (!filesPart.includes(file)) {
return file;
}
let newFile = file;
newFile.status = 'uploading';
newFile.percent = 1;
return newFile;
}),
);
const ret = await uploadDocument(filesPart);
const files = ret?.data || [];
const successfulFilenames = files.map((file: any) => file.name);
// set status to done or error on files (based on response)
setFileList(
fileList.map((file) => {
if (!filesPart.includes(file)) {
return file;
}
let newFile = file;
newFile.status = successfulFilenames.includes(file.name)
? 'done'
: 'error';
newFile.percent = 100;
newFile.response = ret.message;
return newFile;
}),
);
return {
code: ret?.code,
fileIds: files.map((file: any) => file.id),
totalSuccess: successfulFilenames.length,
};
};
const totalFiles = fileList.length;
if (directoryFileList.length > 0) {
const ret = await uploadDocument(directoryFileList);
if (ret?.code === 0) {
hideDocumentUploadModal();
}
if (totalFiles === 0) {
return 0;
}
}
if (totalFiles === 0) {
console.log('No files to upload');
hideDocumentUploadModal();
return 0;
}
let totalSuccess = 0;
let codes = [];
let toRunFileIds: any[] = [];
for (let i = 0; i < totalFiles; i += 10) {
setUploadProgress(Math.floor((i / totalFiles) * 100));
const files = fileList.slice(i, i + 10);
const {
code,
totalSuccess: count,
fileIds,
} = await processFileGroup(files);
codes.push(code);
totalSuccess += count;
toRunFileIds = toRunFileIds.concat(fileIds);
}
const allSuccess = codes.every((code) => code === 0);
const any500 = codes.some((code) => code === 500);
let code = 500;
if (allSuccess || (any500 && totalSuccess === totalFiles)) {
code = 0;
hideDocumentUploadModal();
}
if (parseOnCreation) {
await runDocumentByIds({
documentIds: toRunFileIds,
run: 1,
shouldDelete: false,
});
}
setUploadProgress(100);
return code;
},
[fileList, uploadDocument, hideDocumentUploadModal, runDocumentByIds],
);
return {
documentUploadLoading: loading,
onDocumentUploadOk,
documentUploadVisible,
hideDocumentUploadModal,
showDocumentUploadModal,
uploadFileList: fileList,
setUploadFileList: setFileList,
uploadProgress,
setUploadProgress,
};
};
export const useHandleWebCrawl = () => {
const {
visible: webCrawlUploadVisible,
hideModal: hideWebCrawlUploadModal,
showModal: showWebCrawlUploadModal,
} = useSetModalState();
const { webCrawl, loading } = useNextWebCrawl();
const onWebCrawlUploadOk = useCallback(
async (name: string, url: string) => {
const ret = await webCrawl({ name, url });
if (ret === 0) {
hideWebCrawlUploadModal();
return 0;
}
return -1;
},
[webCrawl, hideWebCrawlUploadModal],
);
return {
webCrawlUploadLoading: loading,
onWebCrawlUploadOk,
webCrawlUploadVisible,
hideWebCrawlUploadModal,
showWebCrawlUploadModal,
};
};
export const useHandleRunDocumentByIds = (id: string) => {
const { runDocumentByIds, loading } = useRunNextDocument();
const [currentId, setCurrentId] = useState<string>('');
const isLoading = loading && currentId !== '' && currentId === id;
const handleRunDocumentByIds = async (
documentId: string,
isRunning: boolean,
shouldDelete: boolean = false,
) => {
if (isLoading) {
return;
}
setCurrentId(documentId);
try {
await runDocumentByIds({
documentIds: [documentId],
run: isRunning ? 2 : 1,
shouldDelete,
});
setCurrentId('');
} catch (error) {
setCurrentId('');
}
};
return {
handleRunDocumentByIds,
loading: isLoading,
};
};
export const useShowMetaModal = (documentId: string) => {
const { setDocumentMeta, loading } = useSetDocumentMeta();
const {
visible: setMetaVisible,
hideModal: hideSetMetaModal,
showModal: showSetMetaModal,
} = useSetModalState();
const onSetMetaModalOk = useCallback(
async (meta: string) => {
const ret = await setDocumentMeta({
documentId,
meta,
});
if (ret === 0) {
hideSetMetaModal();
}
},
[setDocumentMeta, documentId, hideSetMetaModal],
);
return {
setMetaLoading: loading,
onSetMetaModalOk,
setMetaVisible,
hideSetMetaModal,
showSetMetaModal,
};
};

View File

@ -1,54 +0,0 @@
.datasetWrapper {
padding: 30px 30px 0;
height: 100%;
}
.documentTable {
tbody {
// height: calc(100vh - 508px);
}
}
.filter {
height: 32px;
display: flex;
margin: 10px 0;
justify-content: space-between;
padding: 24px 0;
align-items: center;
}
.deleteIconWrapper {
width: 22px;
text-align: center;
}
.img {
height: 24px;
width: 24px;
display: inline-block;
vertical-align: middle;
}
.column {
min-width: 200px;
}
.toChunks {
cursor: pointer;
}
.pageInputNumber {
width: 220px;
}
.questionIcon {
margin-inline-start: 4px;
color: rgba(0, 0, 0, 0.45);
cursor: help;
writing-mode: horizontal-tb;
}
.nameText {
color: #1677ff;
}

View File

@ -1,275 +0,0 @@
import ChunkMethodModal from '@/components/chunk-method-modal';
import SvgIcon from '@/components/svg-icon';
import {
useFetchNextDocumentList,
useSetNextDocumentStatus,
} from '@/hooks/document-hooks';
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
import { useSelectParserList } from '@/hooks/user-setting-hooks';
import { getExtension } from '@/utils/document-util';
import { Divider, Flex, Switch, Table, Tooltip, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useTranslation } from 'react-i18next';
import CreateFileModal from './create-file-modal';
import DocumentToolbar from './document-toolbar';
import {
useChangeDocumentParser,
useCreateEmptyDocument,
useGetRowSelection,
useHandleUploadDocument,
useHandleWebCrawl,
useNavigateToOtherPage,
useRenameDocument,
useShowMetaModal,
} from './hooks';
import ParsingActionCell from './parsing-action-cell';
import ParsingStatusCell from './parsing-status-cell';
import RenameModal from './rename-modal';
import WebCrawlModal from './web-crawl-modal';
import FileUploadModal from '@/components/file-upload-modal';
import { RunningStatus } from '@/constants/knowledge';
import { IDocumentInfo } from '@/interfaces/database/document';
import { formatDate } from '@/utils/date';
import { CircleHelp } from 'lucide-react';
import styles from './index.less';
import { SetMetaModal } from './set-meta-modal';
const { Text } = Typography;
const KnowledgeFile = () => {
const { searchString, documents, pagination, handleInputChange } =
useFetchNextDocumentList();
const parserList = useSelectParserList();
const { setDocumentStatus } = useSetNextDocumentStatus();
const { toChunk } = useNavigateToOtherPage();
const { currentRecord, setRecord } = useSetSelectedRecord<IDocumentInfo>();
const {
renameLoading,
onRenameOk,
renameVisible,
hideRenameModal,
showRenameModal,
} = useRenameDocument(currentRecord.id);
const {
createLoading,
onCreateOk,
createVisible,
hideCreateModal,
showCreateModal,
} = useCreateEmptyDocument();
const {
changeParserLoading,
onChangeParserOk,
changeParserVisible,
hideChangeParserModal,
showChangeParserModal,
} = useChangeDocumentParser(currentRecord.id);
const {
documentUploadVisible,
hideDocumentUploadModal,
showDocumentUploadModal,
onDocumentUploadOk,
documentUploadLoading,
uploadFileList,
setUploadFileList,
uploadProgress,
setUploadProgress,
} = useHandleUploadDocument();
const {
webCrawlUploadVisible,
hideWebCrawlUploadModal,
showWebCrawlUploadModal,
onWebCrawlUploadOk,
webCrawlUploadLoading,
} = useHandleWebCrawl();
const { t } = useTranslation('translation', {
keyPrefix: 'knowledgeDetails',
});
const {
showSetMetaModal,
hideSetMetaModal,
setMetaVisible,
setMetaLoading,
onSetMetaModalOk,
} = useShowMetaModal(currentRecord.id);
const rowSelection = useGetRowSelection();
const columns: ColumnsType<IDocumentInfo> = [
{
title: t('name'),
dataIndex: 'name',
key: 'name',
fixed: 'left',
render: (text: any, { id, thumbnail, name }) => (
<div className={styles.toChunks} onClick={() => toChunk(id)}>
<Flex gap={10} align="center">
{thumbnail ? (
<img className={styles.img} src={thumbnail} alt="" />
) : (
<SvgIcon
name={`file-icon/${getExtension(name)}`}
width={24}
></SvgIcon>
)}
<Text ellipsis={{ tooltip: text }} className={styles.nameText}>
{text}
</Text>
</Flex>
</div>
),
},
{
title: t('chunkNumber'),
dataIndex: 'chunk_num',
key: 'chunk_num',
},
{
title: t('uploadDate'),
dataIndex: 'create_time',
key: 'create_time',
render(value) {
return formatDate(value);
},
},
{
title: t('chunkMethod'),
dataIndex: 'parser_id',
key: 'parser_id',
render: (text) => {
return parserList.find((x) => x.value === text)?.label;
},
},
{
title: t('enabled'),
key: 'status',
dataIndex: 'status',
render: (_, { status, id }) => (
<>
<Switch
checked={status === '1'}
onChange={(e) => {
setDocumentStatus({ status: e, documentId: id });
}}
/>
</>
),
},
{
title: (
<span className="flex items-center gap-2">
{t('parsingStatus')}
<Tooltip title={t('parsingStatusTip')}>
<CircleHelp className="size-3" />
</Tooltip>
</span>
),
dataIndex: 'run',
key: 'run',
filters: Object.values(RunningStatus).map((value) => ({
text: t(`runningStatus${value}`),
value: value,
})),
onFilter: (value, record: IDocumentInfo) => record.run === value,
render: (text, record) => {
return <ParsingStatusCell record={record}></ParsingStatusCell>;
},
},
{
title: t('action'),
key: 'action',
render: (_, record) => (
<ParsingActionCell
setCurrentRecord={setRecord}
showRenameModal={showRenameModal}
showChangeParserModal={showChangeParserModal}
showSetMetaModal={showSetMetaModal}
record={record}
></ParsingActionCell>
),
},
];
const finalColumns = columns.map((x) => ({
...x,
className: `${styles.column}`,
}));
return (
<div className={styles.datasetWrapper}>
<h3>{t('dataset')}</h3>
<p>{t('datasetDescription')}</p>
<Divider></Divider>
<DocumentToolbar
selectedRowKeys={rowSelection.selectedRowKeys as string[]}
showCreateModal={showCreateModal}
showWebCrawlModal={showWebCrawlUploadModal}
showDocumentUploadModal={showDocumentUploadModal}
searchString={searchString}
handleInputChange={handleInputChange}
documents={documents}
></DocumentToolbar>
<Table
rowKey="id"
columns={finalColumns}
dataSource={documents}
pagination={pagination}
rowSelection={rowSelection}
className={styles.documentTable}
scroll={{ scrollToFirstRowOnChange: true, x: 1300 }}
/>
<CreateFileModal
visible={createVisible}
hideModal={hideCreateModal}
loading={createLoading}
onOk={onCreateOk}
/>
<ChunkMethodModal
documentId={currentRecord.id}
parserId={currentRecord.parser_id}
parserConfig={currentRecord.parser_config}
documentExtension={getExtension(currentRecord.name)}
onOk={onChangeParserOk}
visible={changeParserVisible}
hideModal={hideChangeParserModal}
loading={changeParserLoading}
/>
<RenameModal
visible={renameVisible}
onOk={onRenameOk}
loading={renameLoading}
hideModal={hideRenameModal}
initialName={currentRecord.name}
></RenameModal>
<FileUploadModal
visible={documentUploadVisible}
hideModal={hideDocumentUploadModal}
loading={documentUploadLoading}
onOk={onDocumentUploadOk}
uploadFileList={uploadFileList}
setUploadFileList={setUploadFileList}
uploadProgress={uploadProgress}
setUploadProgress={setUploadProgress}
></FileUploadModal>
<WebCrawlModal
visible={webCrawlUploadVisible}
hideModal={hideWebCrawlUploadModal}
loading={webCrawlUploadLoading}
onOk={onWebCrawlUploadOk}
></WebCrawlModal>
{setMetaVisible && (
<SetMetaModal
visible={setMetaVisible}
hideModal={hideSetMetaModal}
onOk={onSetMetaModalOk}
loading={setMetaLoading}
initialMetaData={currentRecord.meta_fields}
></SetMetaModal>
)}
</div>
);
};
export default KnowledgeFile;

View File

@ -1,3 +0,0 @@
.iconButton {
padding: 4px 8px;
}

View File

@ -1,149 +0,0 @@
import { useShowDeleteConfirm, useTranslate } from '@/hooks/common-hooks';
import { useRemoveNextDocument } from '@/hooks/document-hooks';
import { IDocumentInfo } from '@/interfaces/database/document';
import { downloadDocument } from '@/utils/file-util';
import {
DeleteOutlined,
DownloadOutlined,
EditOutlined,
ToolOutlined,
} from '@ant-design/icons';
import { Button, Dropdown, MenuProps, Space, Tooltip } from 'antd';
import { isParserRunning } from '../utils';
import { useCallback } from 'react';
import { DocumentType } from '../constant';
import styles from './index.less';
interface IProps {
record: IDocumentInfo;
setCurrentRecord: (record: IDocumentInfo) => void;
showRenameModal: () => void;
showChangeParserModal: () => void;
showSetMetaModal: () => void;
}
const ParsingActionCell = ({
record,
setCurrentRecord,
showRenameModal,
showChangeParserModal,
showSetMetaModal,
}: IProps) => {
const documentId = record.id;
const isRunning = isParserRunning(record.run);
const { t } = useTranslate('knowledgeDetails');
const { removeDocument } = useRemoveNextDocument();
const showDeleteConfirm = useShowDeleteConfirm();
const isVirtualDocument = record.type === DocumentType.Virtual;
const onRmDocument = () => {
if (!isRunning) {
showDeleteConfirm({
onOk: () => removeDocument([documentId]),
content: record?.parser_config?.graphrag?.use_graphrag
? t('deleteDocumentConfirmContent')
: '',
});
}
};
const onDownloadDocument = () => {
downloadDocument({
id: documentId,
filename: record.name,
});
};
const setRecord = useCallback(() => {
setCurrentRecord(record);
}, [record, setCurrentRecord]);
const onShowRenameModal = () => {
setRecord();
showRenameModal();
};
const onShowChangeParserModal = () => {
setRecord();
showChangeParserModal();
};
const onShowSetMetaModal = useCallback(() => {
setRecord();
showSetMetaModal();
}, [setRecord, showSetMetaModal]);
const chunkItems: MenuProps['items'] = [
{
key: '1',
label: (
<div className="flex flex-col">
<Button type="link" onClick={onShowChangeParserModal}>
{t('chunkMethod')}
</Button>
</div>
),
},
{ type: 'divider' },
{
key: '2',
label: (
<div className="flex flex-col">
<Button type="link" onClick={onShowSetMetaModal}>
{t('setMetaData')}
</Button>
</div>
),
},
];
return (
<Space size={0}>
{isVirtualDocument || (
<Dropdown
menu={{ items: chunkItems }}
trigger={['click']}
disabled={isRunning || record.parser_id === 'tag'}
>
<Button type="text" className={styles.iconButton}>
<ToolOutlined size={20} />
</Button>
</Dropdown>
)}
<Tooltip title={t('rename', { keyPrefix: 'common' })}>
<Button
type="text"
disabled={isRunning}
onClick={onShowRenameModal}
className={styles.iconButton}
>
<EditOutlined size={20} />
</Button>
</Tooltip>
<Tooltip title={t('delete', { keyPrefix: 'common' })}>
<Button
type="text"
disabled={isRunning}
onClick={onRmDocument}
className={styles.iconButton}
>
<DeleteOutlined size={20} />
</Button>
</Tooltip>
{isVirtualDocument || (
<Tooltip title={t('download', { keyPrefix: 'common' })}>
<Button
type="text"
disabled={isRunning}
onClick={onDownloadDocument}
className={styles.iconButton}
>
<DownloadOutlined size={20} />
</Button>
</Tooltip>
)}
</Space>
);
};
export default ParsingActionCell;

View File

@ -1,36 +0,0 @@
.popoverContent {
width: 40vw;
.popoverContentItem {
display: flex;
gap: 10px;
}
.popoverContentText {
white-space: pre-line;
max-height: 50vh;
overflow: auto;
.popoverContentErrorLabel {
color: red;
}
}
}
.operationIcon {
text-align: center;
display: flex;
&:hover {
cursor: pointer;
}
}
.operationIconSpin {
animation: spin 1s linear infinite;
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}

View File

@ -1,143 +0,0 @@
import { ReactComponent as CancelIcon } from '@/assets/svg/cancel.svg';
import { ReactComponent as RefreshIcon } from '@/assets/svg/refresh.svg';
import { ReactComponent as RunIcon } from '@/assets/svg/run.svg';
import { useTranslate } from '@/hooks/common-hooks';
import { IDocumentInfo } from '@/interfaces/database/document';
import {
Badge,
DescriptionsProps,
Flex,
Popconfirm,
Popover,
Space,
Tag,
} from 'antd';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import reactStringReplace from 'react-string-replace';
import { DocumentType, RunningStatus, RunningStatusMap } from '../constant';
import { useHandleRunDocumentByIds } from '../hooks';
import { isParserRunning } from '../utils';
import styles from './index.less';
const iconMap = {
[RunningStatus.UNSTART]: RunIcon,
[RunningStatus.RUNNING]: CancelIcon,
[RunningStatus.CANCEL]: RefreshIcon,
[RunningStatus.DONE]: RefreshIcon,
[RunningStatus.FAIL]: RefreshIcon,
};
interface IProps {
record: IDocumentInfo;
}
const PopoverContent = ({ record }: IProps) => {
const { t } = useTranslate('knowledgeDetails');
const replaceText = (text: string) => {
// Remove duplicate \n
const nextText = text.replace(/(\n)\1+/g, '$1');
const replacedText = reactStringReplace(
nextText,
/(\[ERROR\].+\s)/g,
(match, i) => {
return (
<span key={i} className={styles.popoverContentErrorLabel}>
{match}
</span>
);
},
);
return replacedText;
};
const items: DescriptionsProps['items'] = [
{
key: 'process_begin_at',
label: t('processBeginAt'),
children: record.process_begin_at,
},
{
key: 'process_duration',
label: t('processDuration'),
children: `${record.process_duration.toFixed(2)} s`,
},
{
key: 'progress_msg',
label: t('progressMsg'),
children: replaceText(record.progress_msg.trim()),
},
];
return (
<Flex vertical className={styles.popoverContent}>
{items.map((x, idx) => {
return (
<div key={x.key} className={idx < 2 ? styles.popoverContentItem : ''}>
<b>{x.label}:</b>
<div className={styles.popoverContentText}>{x.children}</div>
</div>
);
})}
</Flex>
);
};
export const ParsingStatusCell = ({ record }: IProps) => {
const text = record.run;
const runningStatus = RunningStatusMap[text];
const { t } = useTranslation();
const { handleRunDocumentByIds } = useHandleRunDocumentByIds(record.id);
const isRunning = isParserRunning(text);
const OperationIcon = iconMap[text];
const label = t(`knowledgeDetails.runningStatus${text}`);
const handleOperationIconClick =
(shouldDelete: boolean = false) =>
() => {
handleRunDocumentByIds(record.id, isRunning, shouldDelete);
};
return record.type === DocumentType.Virtual ? null : (
<Flex justify={'space-between'} align="center">
<Popover content={<PopoverContent record={record}></PopoverContent>}>
<Tag color={runningStatus.color}>
{isRunning ? (
<Space>
<Badge color={runningStatus.color} />
{label}
<span>{(record.progress * 100).toFixed(2)}%</span>
</Space>
) : (
label
)}
</Tag>
</Popover>
<Popconfirm
title={t(`knowledgeDetails.redo`, { chunkNum: record.chunk_num })}
onConfirm={handleOperationIconClick(true)}
onCancel={handleOperationIconClick(false)}
disabled={record.chunk_num === 0}
okText={t('common.yes')}
cancelText={t('common.no')}
>
<div
className={classNames(styles.operationIcon)}
onClick={
record.chunk_num === 0 ? handleOperationIconClick(false) : () => {}
}
>
<OperationIcon />
</div>
</Popconfirm>
</Flex>
);
};
export default ParsingStatusCell;

View File

@ -1,75 +0,0 @@
import { IModalManagerChildrenProps } from '@/components/modal-manager';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Modal } from 'antd';
import { useEffect } from 'react';
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
loading: boolean;
initialName: string;
onOk: (name: string) => void;
showModal?(): void;
}
const RenameModal = ({
visible,
onOk,
loading,
initialName,
hideModal,
}: IProps) => {
const [form] = Form.useForm();
const { t } = useTranslate('common');
type FieldType = {
name?: string;
};
const handleOk = async () => {
const ret = await form.validateFields();
onOk(ret.name);
};
const onFinish = (values: any) => {
console.log('Success:', values);
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
useEffect(() => {
if (visible) {
form.setFieldValue('name', initialName);
}
}, [initialName, form, visible]);
return (
<Modal
title={t('rename')}
open={visible}
onOk={handleOk}
onCancel={hideModal}
okButtonProps={{ loading }}
>
<Form
name="basic"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
style={{ maxWidth: 600 }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
form={form}
>
<Form.Item<FieldType>
label={t('name')}
name="name"
rules={[{ required: true, message: t('namePlaceholder') }]}
>
<Input />
</Form.Item>
</Form>
</Modal>
);
};
export default RenameModal;

View File

@ -1,81 +0,0 @@
import { IModalProps } from '@/interfaces/common';
import { IDocumentInfo } from '@/interfaces/database/document';
import Editor, { loader } from '@monaco-editor/react';
import { Form, Modal } from 'antd';
import DOMPurify from 'dompurify';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
loader.config({ paths: { vs: '/vs' } });
type FieldType = {
meta?: string;
};
export function SetMetaModal({
visible,
hideModal,
onOk,
initialMetaData,
}: IModalProps<any> & { initialMetaData?: IDocumentInfo['meta_fields'] }) {
const { t } = useTranslation();
const [form] = Form.useForm();
const handleOk = useCallback(async () => {
const values = await form.validateFields();
onOk?.(values.meta);
}, [form, onOk]);
useEffect(() => {
form.setFieldValue('meta', JSON.stringify(initialMetaData, null, 4));
}, [form, initialMetaData]);
return (
<Modal
title={t('knowledgeDetails.setMetaData')}
open={visible}
onOk={handleOk}
onCancel={hideModal}
>
<Form
name="basic"
initialValues={{ remember: true }}
autoComplete="off"
layout={'vertical'}
form={form}
>
<Form.Item<FieldType>
label={t('knowledgeDetails.metaData')}
name="meta"
rules={[
{
required: true,
validator(rule, value) {
try {
JSON.parse(value);
return Promise.resolve();
} catch (error) {
return Promise.reject(
new Error(t('knowledgeDetails.pleaseInputJson')),
);
}
},
},
]}
tooltip={
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
t('knowledgeDetails.documentMetaTips'),
),
}}
></div>
}
>
<Editor height={200} defaultLanguage="json" theme="vs-dark" />
</Form.Item>
</Form>
</Modal>
);
}

View File

@ -1,6 +0,0 @@
import { RunningStatus } from './constant';
export const isParserRunning = (text: RunningStatus) => {
const isRunning = text === RunningStatus.RUNNING;
return isRunning;
};

View File

@ -1,67 +0,0 @@
import { IModalManagerChildrenProps } from '@/components/modal-manager';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Modal } from 'antd';
import React from 'react';
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
loading: boolean;
onOk: (name: string, url: string) => void;
showModal?(): void;
}
const WebCrawlModal: React.FC<IProps> = ({ visible, hideModal, onOk }) => {
const [form] = Form.useForm();
const { t } = useTranslate('knowledgeDetails');
const handleOk = async () => {
const values = await form.validateFields();
onOk(values.name, values.url);
};
return (
<Modal
title={t('webCrawl')}
open={visible}
onOk={handleOk}
onCancel={hideModal}
>
<Form
form={form}
name="validateOnly"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
style={{ maxWidth: 600 }}
autoComplete="off"
>
<Form.Item
label="Name"
name="name"
rules={[
{ required: true, message: 'Please input name!' },
{
max: 10,
message: 'The maximum length of name is 128 characters',
},
]}
>
<Input placeholder="Document name" />
</Form.Item>
<Form.Item
label="URL"
name="url"
rules={[
{ required: true, message: 'Please input url!' },
{
pattern: new RegExp(
'(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]',
),
message: 'Please enter a valid URL!',
},
]}
>
<Input placeholder="https://www.baidu.com" />
</Form.Item>
</Form>
</Modal>
);
};
export default WebCrawlModal;

View File

@ -1,241 +0,0 @@
const nodes = [
{
type: '"ORGANIZATION"',
description:
'"厦门象屿是一家公司其营业收入和市场占有率在2018年至2022年间有所变化。"',
source_id: '0',
id: '"厦门象屿"',
},
{
type: '"EVENT"',
description:
'"2018年是一个时间点标志着厦门象屿营业收入和市场占有率的记录开始。"',
source_id: '0',
entity_type: '"EVENT"',
id: '"2018"',
},
{
type: '"EVENT"',
description:
'"2019年是一个时间点厦门象屿的营业收入和市场占有率在此期间有所变化。"',
source_id: '0',
entity_type: '"EVENT"',
id: '"2019"',
},
{
type: '"EVENT"',
description:
'"2020年是一个时间点厦门象屿的营业收入和市场占有率在此期间有所变化。"',
source_id: '0',
entity_type: '"EVENT"',
id: '"2020"',
},
{
type: '"EVENT"',
description:
'"2021年是一个时间点厦门象屿的营业收入和市场占有率在此期间有所变化。"',
source_id: '0',
entity_type: '"EVENT"',
id: '"2021"',
},
{
type: '"EVENT"',
description:
'"2022年是一个时间点厦门象屿的营业收入和市场占有率在此期间有所变化。"',
source_id: '0',
entity_type: '"EVENT"',
id: '"2022"',
},
{
type: '"ORGANIZATION"',
description:
'"厦门象屿股份有限公司是一家公司中文简称为厦门象屿外文名称为Xiamen Xiangyu Co.,Ltd.外文名称缩写为Xiangyu法定代表人为邓启东。"',
source_id: '1',
id: '"厦门象屿股份有限公司"',
},
{
type: '"PERSON"',
description: '"邓启东是厦门象屿股份有限公司的法定代表人。"',
source_id: '1',
entity_type: '"PERSON"',
id: '"邓启东"',
},
{
type: '"GEO"',
description: '"厦门是一个地理位置,与厦门象屿股份有限公司相关。"',
source_id: '1',
entity_type: '"GEO"',
id: '"厦门"',
},
{
type: '"PERSON"',
description:
'"廖杰 is the Board Secretary, responsible for handling board-related matters and communications."',
source_id: '2',
id: '"廖杰"',
},
{
type: '"PERSON"',
description:
'"史经洋 is the Securities Affairs Representative, responsible for handling securities-related matters and communications."',
source_id: '2',
entity_type: '"PERSON"',
id: '"史经洋"',
},
{
type: '"GEO"',
description:
'"A geographic location in Xiamen, specifically in the Free Trade Zone, where the company\'s office is situated."',
source_id: '2',
entity_type: '"GEO"',
id: '"厦门市湖里区自由贸易试验区厦门片区"',
},
{
type: '"GEO"',
description:
'"The building where the company\'s office is located, situated at Xiangyu Road, Xiamen."',
source_id: '2',
entity_type: '"GEO"',
id: '"象屿集团大厦"',
},
{
type: '"EVENT"',
description:
'"Refers to the year 2021, used for comparing financial metrics with the year 2022."',
source_id: '3',
id: '"2021年"',
},
{
type: '"EVENT"',
description:
'"Refers to the year 2022, used for presenting current financial metrics and comparing them with the year 2021."',
source_id: '3',
entity_type: '"EVENT"',
id: '"2022年"',
},
{
type: '"EVENT"',
description:
'"Indicates the focus on key financial metrics in the table, such as weighted averages and percentages."',
source_id: '3',
entity_type: '"EVENT"',
id: '"主要财务指标"',
},
].map(({ type, ...x }) => ({ ...x }));
const edges = [
{
weight: 2.0,
description: '"厦门象屿在2018年的营业收入和市场占有率被记录。"',
source_id: '0',
source: '"厦门象屿"',
target: '"2018"',
},
{
weight: 2.0,
description: '"厦门象屿在2019年的营业收入和市场占有率有所变化。"',
source_id: '0',
source: '"厦门象屿"',
target: '"2019"',
},
{
weight: 2.0,
description: '"厦门象屿在2020年的营业收入和市场占有率有所变化。"',
source_id: '0',
source: '"厦门象屿"',
target: '"2020"',
},
{
weight: 2.0,
description: '"厦门象屿在2021年的营业收入和市场占有率有所变化。"',
source_id: '0',
source: '"厦门象屿"',
target: '"2021"',
},
{
weight: 2.0,
description: '"厦门象屿在2022年的营业收入和市场占有率有所变化。"',
source_id: '0',
source: '"厦门象屿"',
target: '"2022"',
},
{
weight: 2.0,
description: '"厦门象屿股份有限公司的法定代表人是邓启东。"',
source_id: '1',
source: '"厦门象屿股份有限公司"',
target: '"邓启东"',
},
{
weight: 2.0,
description: '"厦门象屿股份有限公司位于厦门。"',
source_id: '1',
source: '"厦门象屿股份有限公司"',
target: '"厦门"',
},
{
weight: 2.0,
description:
'"廖杰\'s office is located in the Xiangyu Group Building, indicating his workplace."',
source_id: '2',
source: '"廖杰"',
target: '"象屿集团大厦"',
},
{
weight: 2.0,
description:
'"廖杰 works in the Xiamen Free Trade Zone, a specific area within Xiamen."',
source_id: '2',
source: '"廖杰"',
target: '"厦门市湖里区自由贸易试验区厦门片区"',
},
{
weight: 2.0,
description:
'"史经洋\'s office is also located in the Xiangyu Group Building, indicating his workplace."',
source_id: '2',
source: '"史经洋"',
target: '"象屿集团大厦"',
},
{
weight: 2.0,
description:
'"史经洋 works in the Xiamen Free Trade Zone, a specific area within Xiamen."',
source_id: '2',
source: '"史经洋"',
target: '"厦门市湖里区自由贸易试验区厦门片区"',
},
{
weight: 2.0,
description:
'"The years 2021 and 2022 are related as they are used for comparing financial metrics, showing changes and adjustments over time."',
source_id: '3',
source: '"2021年"',
target: '"2022年"',
},
{
weight: 2.0,
description:
'"The \'主要财务指标\' is related to the year 2021 as it provides the basis for financial comparisons and adjustments."',
source_id: '3',
source: '"2021年"',
target: '"主要财务指标"',
},
{
weight: 2.0,
description:
'"The \'主要财务指标\' is related to the year 2022 as it presents the current financial metrics and their changes compared to 2021."',
source_id: '3',
source: '"2022年"',
target: '"主要财务指标"',
},
];
export const graphData = {
directed: false,
multigraph: false,
graph: {},
nodes,
edges,
combos: [],
};

View File

@ -1,141 +0,0 @@
import { ElementDatum, Graph, IElementEvent } from '@antv/g6';
import isEmpty from 'lodash/isEmpty';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { buildNodesAndCombos } from './util';
import styles from './index.less';
const TooltipColorMap = {
combo: 'red',
node: 'black',
edge: 'blue',
};
interface IProps {
data: any;
show: boolean;
}
const ForceGraph = ({ data, show }: IProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const graphRef = useRef<Graph | null>(null);
const nextData = useMemo(() => {
if (!isEmpty(data)) {
const graphData = data;
const mi = buildNodesAndCombos(graphData.nodes);
return { edges: graphData.edges, ...mi };
}
return { nodes: [], edges: [] };
}, [data]);
const render = useCallback(() => {
const graph = new Graph({
container: containerRef.current!,
autoFit: 'view',
autoResize: true,
behaviors: [
'drag-element',
'drag-canvas',
'zoom-canvas',
'collapse-expand',
{
type: 'hover-activate',
degree: 1, // 👈🏻 Activate relations.
},
],
plugins: [
{
type: 'tooltip',
enterable: true,
getContent: (e: IElementEvent, items: ElementDatum) => {
if (Array.isArray(items)) {
if (items.some((x) => x?.isCombo)) {
return `<p style="font-weight:600;color:red">${items?.[0]?.data?.label}</p>`;
}
let result = ``;
items.forEach((item) => {
result += `<section style="color:${TooltipColorMap[e['targetType'] as keyof typeof TooltipColorMap]};"><h3>${item?.id}</h3>`;
if (item?.entity_type) {
result += `<div style="padding-bottom: 6px;"><b>Entity type: </b>${item?.entity_type}</div>`;
}
if (item?.weight) {
result += `<div><b>Weight: </b>${item?.weight}</div>`;
}
if (item?.description) {
result += `<p>${item?.description}</p>`;
}
});
return result + '</section>';
}
return undefined;
},
},
],
layout: {
type: 'combo-combined',
preventOverlap: true,
comboPadding: 1,
spacing: 100,
},
node: {
style: {
size: 150,
labelText: (d) => d.id,
// labelPadding: 30,
labelFontSize: 40,
// labelOffsetX: 20,
labelOffsetY: 20,
labelPlacement: 'center',
labelWordWrap: true,
},
palette: {
type: 'group',
field: (d) => {
return d?.entity_type as string;
},
},
},
edge: {
style: (model) => {
const weight: number = Number(model?.weight) || 2;
const lineWeight = weight * 4;
return {
stroke: '#99ADD1',
lineWidth: lineWeight > 10 ? 10 : lineWeight,
};
},
},
});
if (graphRef.current) {
graphRef.current.destroy();
}
graphRef.current = graph;
graph.setData(nextData);
graph.render();
}, [nextData]);
useEffect(() => {
if (!isEmpty(data)) {
render();
}
}, [data, render]);
return (
<div
ref={containerRef}
className={styles.forceContainer}
style={{
width: '100%',
height: '100%',
display: show ? 'block' : 'none',
}}
/>
);
};
export default ForceGraph;

View File

@ -1,5 +0,0 @@
.forceContainer {
:global(.tooltip) {
border-radius: 10px !important;
}
}

View File

@ -1,31 +0,0 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { Button } from '@/components/ui/button';
import { useFetchKnowledgeGraph } from '@/hooks/knowledge-hooks';
import { Trash2 } from 'lucide-react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import ForceGraph from './force-graph';
import { useDeleteKnowledgeGraph } from './use-delete-graph';
const KnowledgeGraph: React.FC = () => {
const { data } = useFetchKnowledgeGraph();
const { t } = useTranslation();
const { handleDeleteKnowledgeGraph } = useDeleteKnowledgeGraph();
return (
<section className={'w-full h-full relative'}>
<ConfirmDeleteDialog onOk={handleDeleteKnowledgeGraph}>
<Button
variant="outline"
size={'sm'}
className="absolute right-0 top-0 z-50"
>
<Trash2 /> {t('common.delete')}
</Button>
</ConfirmDeleteDialog>
<ForceGraph data={data?.graph} show></ForceGraph>
</section>
);
};
export default KnowledgeGraph;

View File

@ -1,21 +0,0 @@
import {
useKnowledgeBaseId,
useRemoveKnowledgeGraph,
} from '@/hooks/knowledge-hooks';
import { useCallback } from 'react';
import { useNavigate } from 'umi';
export function useDeleteKnowledgeGraph() {
const { removeKnowledgeGraph, loading } = useRemoveKnowledgeGraph();
const navigate = useNavigate();
const knowledgeBaseId = useKnowledgeBaseId();
const handleDeleteKnowledgeGraph = useCallback(async () => {
const ret = await removeKnowledgeGraph();
if (ret === 0) {
navigate(`/knowledge/dataset?id=${knowledgeBaseId}`);
}
}, [knowledgeBaseId, navigate, removeKnowledgeGraph]);
return { handleDeleteKnowledgeGraph, loading };
}

View File

@ -1,94 +0,0 @@
import { isEmpty } from 'lodash';
import { v4 as uuid } from 'uuid';
class KeyGenerator {
idx = 0;
chars: string[] = [];
constructor() {
const chars = Array(26)
.fill(1)
.map((x, idx) => String.fromCharCode(97 + idx)); // 26 char
this.chars = chars;
}
generateKey() {
const key = this.chars[this.idx];
this.idx++;
return key;
}
}
// Classify nodes based on edge relationships
export class Converter {
keyGenerator;
dict: Record<string, string> = {}; // key is node id, value is combo
constructor() {
this.keyGenerator = new KeyGenerator();
}
buildDict(edges: { source: string; target: string }[]) {
edges.forEach((x) => {
if (this.dict[x.source] && !this.dict[x.target]) {
this.dict[x.target] = this.dict[x.source];
} else if (!this.dict[x.source] && this.dict[x.target]) {
this.dict[x.source] = this.dict[x.target];
} else if (!this.dict[x.source] && !this.dict[x.target]) {
this.dict[x.source] = this.dict[x.target] =
this.keyGenerator.generateKey();
}
});
return this.dict;
}
buildNodesAndCombos(nodes: any[], edges: any[]) {
this.buildDict(edges);
const nextNodes = nodes.map((x) => ({ ...x, combo: this.dict[x.id] }));
const combos = Object.values(this.dict).reduce<any[]>((pre, cur) => {
if (pre.every((x) => x.id !== cur)) {
pre.push({
id: cur,
data: {
label: `Combo ${cur}`,
},
});
}
return pre;
}, []);
return { nodes: nextNodes, combos };
}
}
export const isDataExist = (data: any) => {
return (
data?.data && typeof data?.data !== 'boolean' && !isEmpty(data?.data?.graph)
);
};
const findCombo = (communities: string[]) => {
const combo = Array.isArray(communities) ? communities[0] : undefined;
return combo;
};
export const buildNodesAndCombos = (nodes: any[]) => {
const combos: any[] = [];
nodes.forEach((x) => {
const combo = findCombo(x?.communities);
if (combo && combos.every((y) => y.data.label !== combo)) {
combos.push({
isCombo: true,
id: uuid(),
data: {
label: combo,
},
});
}
});
const nextNodes = nodes.map((x) => {
return {
...x,
combo: combos.find((y) => y.data.label === findCombo(x?.communities))?.id,
};
});
return { nodes: nextNodes, combos };
};

View File

@ -1,77 +0,0 @@
import SvgIcon from '@/components/svg-icon';
import { useTranslate } from '@/hooks/common-hooks';
import { useSelectParserList } from '@/hooks/user-setting-hooks';
import { Col, Divider, Empty, Row, Typography } from 'antd';
import DOMPurify from 'dompurify';
import camelCase from 'lodash/camelCase';
import { useMemo } from 'react';
import styles from './index.less';
import { TagTabs } from './tag-tabs';
import { ImageMap } from './utils';
const { Text } = Typography;
const CategoryPanel = ({ chunkMethod }: { chunkMethod: string }) => {
const parserList = useSelectParserList();
const { t } = useTranslate('knowledgeConfiguration');
const item = useMemo(() => {
const item = parserList.find((x) => x.value === chunkMethod);
if (item) {
return {
title: item.label,
description: t(camelCase(item.value)),
};
}
return { title: '', description: '' };
}, [parserList, chunkMethod, t]);
const imageList = useMemo(() => {
if (chunkMethod in ImageMap) {
return ImageMap[chunkMethod as keyof typeof ImageMap];
}
return [];
}, [chunkMethod]);
return (
<section className={styles.categoryPanelWrapper}>
{imageList.length > 0 ? (
<>
<h5 className="font-semibold text-base mt-0 mb-1">
{`"${item.title}" ${t('methodTitle')}`}
</h5>
<p
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(item.description),
}}
></p>
<h5 className="font-semibold text-base mt-4 mb-1">{`"${item.title}" ${t('methodExamples')}`}</h5>
<Text>{t('methodExamplesDescription')}</Text>
<Row gutter={[10, 10]} className={styles.imageRow}>
{imageList.map((x) => (
<Col span={12} key={x}>
<SvgIcon
name={x}
width={'100%'}
className={styles.image}
></SvgIcon>
</Col>
))}
</Row>
<h5 className="font-semibold text-base mt-4 mb-1">
{item.title} {t('dialogueExamplesTitle')}
</h5>
<Divider></Divider>
</>
) : (
<Empty description={''} image={null}>
<p>{t('methodEmpty')}</p>
<SvgIcon name={'chunk-method/chunk-empty'} width={'100%'}></SvgIcon>
</Empty>
)}
{chunkMethod === 'tag' && <TagTabs></TagTabs>}
</section>
);
};
export default CategoryPanel;

View File

@ -1,31 +0,0 @@
import {
AutoKeywordsItem,
AutoQuestionsItem,
} from '@/components/auto-keywords-item';
import PageRank from '@/components/page-rank';
import ParseConfiguration from '@/components/parse-configuration';
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function AudioConfiguration() {
return (
<>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<>
<AutoKeywordsItem></AutoKeywordsItem>
<AutoQuestionsItem></AutoQuestionsItem>
</>
<ParseConfiguration></ParseConfiguration>
<GraphRagItems marginBottom></GraphRagItems>
<TagItems></TagItems>
</>
);
}

View File

@ -1,33 +0,0 @@
import {
AutoKeywordsItem,
AutoQuestionsItem,
} from '@/components/auto-keywords-item';
import LayoutRecognize from '@/components/layout-recognize';
import PageRank from '@/components/page-rank';
import ParseConfiguration from '@/components/parse-configuration';
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function BookConfiguration() {
return (
<>
<LayoutRecognize></LayoutRecognize>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<>
<AutoKeywordsItem></AutoKeywordsItem>
<AutoQuestionsItem></AutoQuestionsItem>
</>
<ParseConfiguration></ParseConfiguration>
<GraphRagItems marginBottom></GraphRagItems>
<TagItems></TagItems>
</>
);
}

View File

@ -1,52 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { useHandleChunkMethodSelectChange } from '@/hooks/logic-hooks';
import { Form, Select } from 'antd';
import { memo } from 'react';
import {
useHasParsedDocument,
useSelectChunkMethodList,
useSelectEmbeddingModelOptions,
} from '../hooks';
export const EmbeddingModelItem = memo(function EmbeddingModelItem() {
const { t } = useTranslate('knowledgeConfiguration');
const embeddingModelOptions = useSelectEmbeddingModelOptions();
const disabled = useHasParsedDocument();
return (
<Form.Item
name="embd_id"
label={t('embeddingModel')}
rules={[{ required: true }]}
tooltip={t('embeddingModelTip')}
>
<Select
placeholder={t('embeddingModelPlaceholder')}
options={embeddingModelOptions}
disabled={disabled}
></Select>
</Form.Item>
);
});
export const ChunkMethodItem = memo(function ChunkMethodItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = Form.useFormInstance();
const handleChunkMethodSelectChange = useHandleChunkMethodSelectChange(form);
const parserList = useSelectChunkMethodList();
return (
<Form.Item
name="parser_id"
label={t('chunkMethod')}
tooltip={t('chunkMethodTip')}
rules={[{ required: true }]}
>
<Select
placeholder={t('chunkMethodPlaceholder')}
onChange={handleChunkMethodSelectChange}
options={parserList}
></Select>
</Form.Item>
);
});

View File

@ -1,31 +0,0 @@
import {
AutoKeywordsItem,
AutoQuestionsItem,
} from '@/components/auto-keywords-item';
import PageRank from '@/components/page-rank';
import ParseConfiguration from '@/components/parse-configuration';
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function EmailConfiguration() {
return (
<>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<>
<AutoKeywordsItem></AutoKeywordsItem>
<AutoQuestionsItem></AutoQuestionsItem>
</>
<ParseConfiguration></ParseConfiguration>
<GraphRagItems marginBottom></GraphRagItems>
<TagItems></TagItems>
</>
);
}

View File

@ -1,133 +0,0 @@
import { DocumentParserType } from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { normFile } from '@/utils/file-util';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Radio, Space, Upload } from 'antd';
import { FormInstance } from 'antd/lib';
import { useEffect, useMemo, useState } from 'react';
import {
useFetchKnowledgeConfigurationOnMount,
useSubmitKnowledgeConfiguration,
} from '../hooks';
import { AudioConfiguration } from './audio';
import { BookConfiguration } from './book';
import { EmailConfiguration } from './email';
import { KnowledgeGraphConfiguration } from './knowledge-graph';
import { LawsConfiguration } from './laws';
import { ManualConfiguration } from './manual';
import { NaiveConfiguration } from './naive';
import { OneConfiguration } from './one';
import { PaperConfiguration } from './paper';
import { PictureConfiguration } from './picture';
import { PresentationConfiguration } from './presentation';
import { QAConfiguration } from './qa';
import { ResumeConfiguration } from './resume';
import { TableConfiguration } from './table';
import { TagConfiguration } from './tag';
import styles from '../index.less';
const ConfigurationComponentMap = {
[DocumentParserType.Naive]: NaiveConfiguration,
[DocumentParserType.Qa]: QAConfiguration,
[DocumentParserType.Resume]: ResumeConfiguration,
[DocumentParserType.Manual]: ManualConfiguration,
[DocumentParserType.Table]: TableConfiguration,
[DocumentParserType.Paper]: PaperConfiguration,
[DocumentParserType.Book]: BookConfiguration,
[DocumentParserType.Laws]: LawsConfiguration,
[DocumentParserType.Presentation]: PresentationConfiguration,
[DocumentParserType.Picture]: PictureConfiguration,
[DocumentParserType.One]: OneConfiguration,
[DocumentParserType.Audio]: AudioConfiguration,
[DocumentParserType.Email]: EmailConfiguration,
[DocumentParserType.Tag]: TagConfiguration,
[DocumentParserType.KnowledgeGraph]: KnowledgeGraphConfiguration,
};
function EmptyComponent() {
return <div></div>;
}
export const ConfigurationForm = ({ form }: { form: FormInstance }) => {
const { submitKnowledgeConfiguration, submitLoading, navigateToDataset } =
useSubmitKnowledgeConfiguration(form);
const { t } = useTranslate('knowledgeConfiguration');
const [finalParserId, setFinalParserId] = useState<DocumentParserType>();
const knowledgeDetails = useFetchKnowledgeConfigurationOnMount(form);
const parserId: DocumentParserType = Form.useWatch('parser_id', form);
const ConfigurationComponent = useMemo(() => {
return finalParserId
? ConfigurationComponentMap[finalParserId]
: EmptyComponent;
}, [finalParserId]);
useEffect(() => {
setFinalParserId(parserId);
}, [parserId]);
useEffect(() => {
setFinalParserId(knowledgeDetails.parser_id as DocumentParserType);
}, [knowledgeDetails.parser_id]);
return (
<Form form={form} name="validateOnly" layout="vertical" autoComplete="off">
<Form.Item name="name" label={t('name')} rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item
name="avatar"
label={t('photo')}
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload
listType="picture-card"
maxCount={1}
beforeUpload={() => false}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
>
<button style={{ border: 0, background: 'none' }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>{t('upload')}</div>
</button>
</Upload>
</Form.Item>
<Form.Item name="description" label={t('description')}>
<Input />
</Form.Item>
<Form.Item
name="permission"
label={t('permissions')}
tooltip={t('permissionsTip')}
rules={[{ required: true }]}
>
<Radio.Group>
<Radio value="me">{t('me')}</Radio>
<Radio value="team">{t('team')}</Radio>
</Radio.Group>
</Form.Item>
<ConfigurationComponent></ConfigurationComponent>
<Form.Item>
<div className={styles.buttonWrapper}>
<Space>
<Button size={'middle'} onClick={navigateToDataset}>
{t('cancel')}
</Button>
<Button
type="primary"
size={'middle'}
loading={submitLoading}
onClick={submitKnowledgeConfiguration}
>
{t('save')}
</Button>
</Space>
</div>
</Form.Item>
</Form>
);
};

View File

@ -1,22 +0,0 @@
import Delimiter from '@/components/delimiter';
import EntityTypesItem from '@/components/entity-types-item';
import MaxTokenNumber from '@/components/max-token-number';
import PageRank from '@/components/page-rank';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function KnowledgeGraphConfiguration() {
return (
<>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<>
<EntityTypesItem></EntityTypesItem>
<MaxTokenNumber max={8192 * 2}></MaxTokenNumber>
<Delimiter></Delimiter>
</>
</>
);
}

View File

@ -1,33 +0,0 @@
import {
AutoKeywordsItem,
AutoQuestionsItem,
} from '@/components/auto-keywords-item';
import LayoutRecognize from '@/components/layout-recognize';
import PageRank from '@/components/page-rank';
import ParseConfiguration from '@/components/parse-configuration';
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function LawsConfiguration() {
return (
<>
<LayoutRecognize></LayoutRecognize>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<>
<AutoKeywordsItem></AutoKeywordsItem>
<AutoQuestionsItem></AutoQuestionsItem>
</>
<ParseConfiguration></ParseConfiguration>
<GraphRagItems marginBottom></GraphRagItems>
<TagItems></TagItems>
</>
);
}

View File

@ -1,33 +0,0 @@
import {
AutoKeywordsItem,
AutoQuestionsItem,
} from '@/components/auto-keywords-item';
import LayoutRecognize from '@/components/layout-recognize';
import PageRank from '@/components/page-rank';
import ParseConfiguration from '@/components/parse-configuration';
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function ManualConfiguration() {
return (
<>
<LayoutRecognize></LayoutRecognize>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<>
<AutoKeywordsItem></AutoKeywordsItem>
<AutoQuestionsItem></AutoQuestionsItem>
</>
<ParseConfiguration></ParseConfiguration>
<GraphRagItems marginBottom></GraphRagItems>
<TagItems></TagItems>
</>
);
}

View File

@ -1,43 +0,0 @@
import {
AutoKeywordsItem,
AutoQuestionsItem,
} from '@/components/auto-keywords-item';
import { DatasetConfigurationContainer } from '@/components/dataset-configuration-container';
import Delimiter from '@/components/delimiter';
import ExcelToHtml from '@/components/excel-to-html';
import LayoutRecognize from '@/components/layout-recognize';
import MaxTokenNumber from '@/components/max-token-number';
import PageRank from '@/components/page-rank';
import ParseConfiguration from '@/components/parse-configuration';
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
import { Divider } from 'antd';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function NaiveConfiguration() {
return (
<section className="space-y-4 mb-4">
<DatasetConfigurationContainer>
<LayoutRecognize></LayoutRecognize>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<MaxTokenNumber></MaxTokenNumber>
<Delimiter></Delimiter>
</DatasetConfigurationContainer>
<Divider></Divider>
<DatasetConfigurationContainer>
<PageRank></PageRank>
<AutoKeywordsItem></AutoKeywordsItem>
<AutoQuestionsItem></AutoQuestionsItem>
<ExcelToHtml></ExcelToHtml>
<TagItems></TagItems>
</DatasetConfigurationContainer>
<Divider></Divider>
<DatasetConfigurationContainer>
<ParseConfiguration></ParseConfiguration>
</DatasetConfigurationContainer>
<Divider></Divider>
<GraphRagItems></GraphRagItems>
</section>
);
}

View File

@ -1,30 +0,0 @@
import {
AutoKeywordsItem,
AutoQuestionsItem,
} from '@/components/auto-keywords-item';
import LayoutRecognize from '@/components/layout-recognize';
import PageRank from '@/components/page-rank';
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function OneConfiguration() {
return (
<>
<LayoutRecognize></LayoutRecognize>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<>
<AutoKeywordsItem></AutoKeywordsItem>
<AutoQuestionsItem></AutoQuestionsItem>
</>
<GraphRagItems marginBottom></GraphRagItems>
<TagItems></TagItems>
</>
);
}

View File

@ -1,33 +0,0 @@
import {
AutoKeywordsItem,
AutoQuestionsItem,
} from '@/components/auto-keywords-item';
import LayoutRecognize from '@/components/layout-recognize';
import PageRank from '@/components/page-rank';
import ParseConfiguration from '@/components/parse-configuration';
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function PaperConfiguration() {
return (
<>
<LayoutRecognize></LayoutRecognize>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<>
<AutoKeywordsItem></AutoKeywordsItem>
<AutoQuestionsItem></AutoQuestionsItem>
</>
<ParseConfiguration></ParseConfiguration>
<GraphRagItems marginBottom></GraphRagItems>
<TagItems></TagItems>
</>
);
}

View File

@ -1,24 +0,0 @@
import {
AutoKeywordsItem,
AutoQuestionsItem,
} from '@/components/auto-keywords-item';
import PageRank from '@/components/page-rank';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function PictureConfiguration() {
return (
<>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<>
<AutoKeywordsItem></AutoKeywordsItem>
<AutoQuestionsItem></AutoQuestionsItem>
</>
<TagItems></TagItems>
</>
);
}

View File

@ -1,33 +0,0 @@
import {
AutoKeywordsItem,
AutoQuestionsItem,
} from '@/components/auto-keywords-item';
import LayoutRecognize from '@/components/layout-recognize';
import PageRank from '@/components/page-rank';
import ParseConfiguration from '@/components/parse-configuration';
import GraphRagItems from '@/components/parse-configuration/graph-rag-items';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function PresentationConfiguration() {
return (
<>
<LayoutRecognize></LayoutRecognize>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<>
<AutoKeywordsItem></AutoKeywordsItem>
<AutoQuestionsItem></AutoQuestionsItem>
</>
<ParseConfiguration></ParseConfiguration>
<GraphRagItems marginBottom></GraphRagItems>
<TagItems></TagItems>
</>
);
}

View File

@ -1,16 +0,0 @@
import PageRank from '@/components/page-rank';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function QAConfiguration() {
return (
<>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<TagItems></TagItems>
</>
);
}

View File

@ -1,16 +0,0 @@
import PageRank from '@/components/page-rank';
import { TagItems } from '../tag-item';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function ResumeConfiguration() {
return (
<>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
<TagItems></TagItems>
</>
);
}

View File

@ -1,13 +0,0 @@
import PageRank from '@/components/page-rank';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function TableConfiguration() {
return (
<>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
</>
);
}

View File

@ -1,13 +0,0 @@
import PageRank from '@/components/page-rank';
import { ChunkMethodItem, EmbeddingModelItem } from './common-item';
export function TagConfiguration() {
return (
<>
<EmbeddingModelItem></EmbeddingModelItem>
<ChunkMethodItem></ChunkMethodItem>
<PageRank></PageRank>
</>
);
}

View File

@ -1,121 +0,0 @@
import { LlmModelType } from '@/constants/knowledge';
import { useSetModalState } from '@/hooks/common-hooks';
import {
useFetchKnowledgeBaseConfiguration,
useUpdateKnowledge,
} from '@/hooks/knowledge-hooks';
import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks';
import { useNavigateToDataset } from '@/hooks/route-hook';
import { useSelectParserList } from '@/hooks/user-setting-hooks';
import {
getBase64FromUploadFileList,
getUploadFileListFromBase64,
} from '@/utils/file-util';
import { useIsFetching } from '@tanstack/react-query';
import { Form, UploadFile } from 'antd';
import { FormInstance } from 'antd/lib';
import pick from 'lodash/pick';
import { useCallback, useEffect, useState } from 'react';
export const useSubmitKnowledgeConfiguration = (form: FormInstance) => {
const { saveKnowledgeConfiguration, loading } = useUpdateKnowledge();
const navigateToDataset = useNavigateToDataset();
const submitKnowledgeConfiguration = useCallback(async () => {
const values = await form.validateFields();
const avatar = await getBase64FromUploadFileList(values.avatar);
saveKnowledgeConfiguration({
...values,
avatar,
});
navigateToDataset();
}, [saveKnowledgeConfiguration, form, navigateToDataset]);
return {
submitKnowledgeConfiguration,
submitLoading: loading,
navigateToDataset,
};
};
// The value that does not need to be displayed in the analysis method Select
const HiddenFields = ['email', 'picture', 'audio'];
export function useSelectChunkMethodList() {
const parserList = useSelectParserList();
return parserList.filter((x) => !HiddenFields.some((y) => y === x.value));
}
export function useSelectEmbeddingModelOptions() {
const allOptions = useSelectLlmOptionsByModelType();
return allOptions[LlmModelType.Embedding];
}
export function useHasParsedDocument() {
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
return knowledgeDetails.chunk_num > 0;
}
export const useFetchKnowledgeConfigurationOnMount = (form: FormInstance) => {
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
useEffect(() => {
const fileList: UploadFile[] = getUploadFileListFromBase64(
knowledgeDetails.avatar,
);
form.setFieldsValue({
...pick(knowledgeDetails, [
'description',
'name',
'permission',
'embd_id',
'parser_id',
'language',
'parser_config',
'pagerank',
]),
avatar: fileList,
});
}, [form, knowledgeDetails]);
return knowledgeDetails;
};
export const useSelectKnowledgeDetailsLoading = () =>
useIsFetching({ queryKey: ['fetchKnowledgeDetail'] }) > 0;
export const useHandleChunkMethodChange = () => {
const [form] = Form.useForm();
const chunkMethod = Form.useWatch('parser_id', form);
useEffect(() => {
console.log('🚀 ~ useHandleChunkMethodChange ~ chunkMethod:', chunkMethod);
}, [chunkMethod]);
return { form, chunkMethod };
};
export const useRenameKnowledgeTag = () => {
const [tag, setTag] = useState<string>('');
const {
visible: tagRenameVisible,
hideModal: hideTagRenameModal,
showModal: showFileRenameModal,
} = useSetModalState();
const handleShowTagRenameModal = useCallback(
(record: string) => {
setTag(record);
showFileRenameModal();
},
[showFileRenameModal],
);
return {
initialName: tag,
tagRenameVisible,
hideTagRenameModal,
showTagRenameModal: handleShowTagRenameModal,
};
};

View File

@ -1,45 +0,0 @@
.tags {
margin-bottom: 24px;
}
.preset {
display: flex;
height: 80px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 5px;
padding: 5px;
margin-bottom: 24px;
.left {
flex: 1;
}
.right {
width: 100px;
border-left: 1px solid rgba(0, 0, 0, 0.4);
margin: 10px 0px;
padding: 5px;
}
}
.configurationWrapper {
padding: 0 52px;
.buttonWrapper {
text-align: right;
}
.variableSlider {
width: 100%;
}
}
.categoryPanelWrapper {
.topTitle {
margin-top: 0;
}
.imageRow {
margin-top: 16px;
}
.image {
width: 100%;
}
}

View File

@ -1,40 +0,0 @@
import { Col, Divider, Row, Spin, Typography } from 'antd';
import CategoryPanel from './category-panel';
import { ConfigurationForm } from './configuration';
import {
useHandleChunkMethodChange,
useSelectKnowledgeDetailsLoading,
} from './hooks';
import { useTranslate } from '@/hooks/common-hooks';
import styles from './index.less';
const { Title } = Typography;
const Configuration = () => {
const loading = useSelectKnowledgeDetailsLoading();
const { form, chunkMethod } = useHandleChunkMethodChange();
const { t } = useTranslate('knowledgeConfiguration');
return (
<div className={styles.configurationWrapper}>
<Title level={5}>
{t('configuration', { keyPrefix: 'knowledgeDetails' })}
</Title>
<p>{t('titleDescription')}</p>
<Divider></Divider>
<Spin spinning={loading}>
<Row gutter={32}>
<Col span={8}>
<ConfigurationForm form={form}></ConfigurationForm>
</Col>
<Col span={16}>
<CategoryPanel chunkMethod={chunkMethod}></CategoryPanel>
</Col>
</Row>
</Spin>
</div>
);
};
export default Configuration;

View File

@ -1,90 +0,0 @@
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
import { UserOutlined } from '@ant-design/icons';
import { Avatar, Flex, Form, InputNumber, Select, Slider, Space } from 'antd';
import DOMPurify from 'dompurify';
import { useTranslation } from 'react-i18next';
export const TagSetItem = () => {
const { t } = useTranslation();
const { list: knowledgeList } = useFetchKnowledgeList(true);
const knowledgeOptions = knowledgeList
.filter((x) => x.parser_id === 'tag')
.map((x) => ({
label: (
<Space>
<Avatar size={20} icon={<UserOutlined />} src={x.avatar} />
{x.name}
</Space>
),
value: x.id,
}));
return (
<Form.Item
label={t('knowledgeConfiguration.tagSet')}
name={['parser_config', 'tag_kb_ids']}
tooltip={
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(t('knowledgeConfiguration.tagSetTip')),
}}
></div>
}
rules={[
{
message: t('chat.knowledgeBasesMessage'),
type: 'array',
},
]}
>
<Select
mode="multiple"
options={knowledgeOptions}
placeholder={t('chat.knowledgeBasesMessage')}
></Select>
</Form.Item>
);
};
export const TopNTagsItem = () => {
const { t } = useTranslation();
return (
<Form.Item label={t('knowledgeConfiguration.topnTags')}>
<Flex gap={20} align="center">
<Flex flex={1}>
<Form.Item
name={['parser_config', 'topn_tags']}
noStyle
initialValue={3}
>
<Slider max={10} min={1} style={{ width: '100%' }} />
</Form.Item>
</Flex>
<Form.Item name={['parser_config', 'topn_tags']} noStyle>
<InputNumber max={10} min={1} />
</Form.Item>
</Flex>
</Form.Item>
);
};
export function TagItems() {
return (
<>
<TagSetItem></TagSetItem>
<Form.Item noStyle dependencies={[['parser_config', 'tag_kb_ids']]}>
{({ getFieldValue }) => {
const ids: string[] = getFieldValue(['parser_config', 'tag_kb_ids']);
return (
Array.isArray(ids) &&
ids.length > 0 && <TopNTagsItem></TopNTagsItem>
);
}}
</Form.Item>
</>
);
}

View File

@ -1,307 +0,0 @@
'use client';
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { ArrowUpDown, Pencil, Trash2 } from 'lucide-react';
import * as React from 'react';
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useDeleteTag, useFetchTagList } from '@/hooks/knowledge-hooks';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRenameKnowledgeTag } from '../hooks';
import { RenameDialog } from './rename-dialog';
export type ITag = {
tag: string;
frequency: number;
};
export function TagTable() {
const { t } = useTranslation();
const { list } = useFetchTagList();
const [tagList, setTagList] = useState<ITag[]>([]);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = useState({});
const { deleteTag } = useDeleteTag();
useEffect(() => {
setTagList(list.map((x) => ({ tag: x[0], frequency: x[1] })));
}, [list]);
const handleDeleteTag = useCallback(
(tags: string[]) => () => {
deleteTag(tags);
},
[deleteTag],
);
const {
showTagRenameModal,
hideTagRenameModal,
tagRenameVisible,
initialName,
} = useRenameKnowledgeTag();
const columns: ColumnDef<ITag>[] = [
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && 'indeterminate')
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'tag',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
{t('knowledgeConfiguration.tagName')}
<ArrowUpDown />
</Button>
);
},
cell: ({ row }) => {
const value: string = row.getValue('tag');
return <div>{value}</div>;
},
},
{
accessorKey: 'frequency',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
{t('knowledgeConfiguration.frequency')}
<ArrowUpDown />
</Button>
);
},
cell: ({ row }) => (
<div className="capitalize ">{row.getValue('frequency')}</div>
),
},
{
id: 'actions',
enableHiding: false,
header: t('common.action'),
cell: ({ row }) => {
return (
<div className="flex gap-1">
<Tooltip>
<ConfirmDeleteDialog onOk={handleDeleteTag([row.original.tag])}>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<Trash2 />
</Button>
</TooltipTrigger>
</ConfirmDeleteDialog>
<TooltipContent>
<p>{t('common.delete')}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => showTagRenameModal(row.original.tag)}
>
<Pencil />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t('common.rename')}</p>
</TooltipContent>
</Tooltip>
</div>
);
},
},
];
const table = useReactTable({
data: tagList,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
const selectedRowLength = table.getFilteredSelectedRowModel().rows.length;
return (
<TooltipProvider>
<div className="w-full">
<div className="flex items-center justify-between py-4 ">
<Input
placeholder={t('knowledgeConfiguration.searchTags')}
value={(table.getColumn('tag')?.getFilterValue() as string) ?? ''}
onChange={(event) =>
table.getColumn('tag')?.setFilterValue(event.target.value)
}
className="w-1/2"
/>
{selectedRowLength > 0 && (
<ConfirmDeleteDialog
onOk={handleDeleteTag(
table
.getFilteredSelectedRowModel()
.rows.map((x) => x.original.tag),
)}
>
<Button variant="outline" size="icon">
<Trash2 />
</Button>
</ConfirmDeleteDialog>
)}
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{selectedRowLength} of {table.getFilteredRowModel().rows.length}{' '}
row(s) selected.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
{t('common.previousPage')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
{t('common.nextPage')}
</Button>
</div>
</div>
</div>
{tagRenameVisible && (
<RenameDialog
hideModal={hideTagRenameModal}
initialName={initialName}
></RenameDialog>
)}
</TooltipProvider>
);
}

View File

@ -1,40 +0,0 @@
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { LoadingButton } from '@/components/ui/loading-button';
import { useTagIsRenaming } from '@/hooks/knowledge-hooks';
import { IModalProps } from '@/interfaces/common';
import { TagRenameId } from '@/pages/add-knowledge/constant';
import { useTranslation } from 'react-i18next';
import { RenameForm } from './rename-form';
export function RenameDialog({
hideModal,
initialName,
}: IModalProps<any> & { initialName: string }) {
const { t } = useTranslation();
const loading = useTagIsRenaming();
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{t('common.rename')}</DialogTitle>
</DialogHeader>
<RenameForm
initialName={initialName}
hideModal={hideModal}
></RenameForm>
<DialogFooter>
<LoadingButton type="submit" form={TagRenameId} loading={loading}>
{t('common.save')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -1,83 +0,0 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useRenameTag } from '@/hooks/knowledge-hooks';
import { IModalProps } from '@/interfaces/common';
import { TagRenameId } from '@/pages/add-knowledge/constant';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
export function RenameForm({
initialName,
hideModal,
}: IModalProps<any> & { initialName: string }) {
const { t } = useTranslation();
const FormSchema = z.object({
name: z
.string()
.min(1, {
message: t('common.namePlaceholder'),
})
.trim(),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
name: '',
},
});
const { renameTag } = useRenameTag();
async function onSubmit(data: z.infer<typeof FormSchema>) {
const ret = await renameTag({ fromTag: initialName, toTag: data.name });
if (ret) {
hideModal?.();
}
}
useEffect(() => {
form.setValue('name', initialName);
}, [form, initialName]);
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
id={TagRenameId}
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.name')}</FormLabel>
<FormControl>
<Input
placeholder={t('common.namePlaceholder')}
{...field}
autoComplete="off"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
}

View File

@ -1,40 +0,0 @@
import { Segmented } from 'antd';
import { SegmentedLabeledOption } from 'antd/es/segmented';
import { upperFirst } from 'lodash';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { TagTable } from './tag-table';
import { TagWordCloud } from './tag-word-cloud';
enum TagType {
Cloud = 'cloud',
Table = 'table',
}
const TagContentMap = {
[TagType.Cloud]: <TagWordCloud></TagWordCloud>,
[TagType.Table]: <TagTable></TagTable>,
};
export function TagTabs() {
const [value, setValue] = useState<TagType>(TagType.Cloud);
const { t } = useTranslation();
const options: SegmentedLabeledOption[] = [TagType.Cloud, TagType.Table].map(
(x) => ({
label: t(`knowledgeConfiguration.tag${upperFirst(x)}`),
value: x,
}),
);
return (
<section className="mt-4">
<Segmented
value={value}
options={options}
onChange={(val) => setValue(val as TagType)}
/>
{TagContentMap[value]}
</section>
);
}

View File

@ -1,62 +0,0 @@
import { useFetchTagList } from '@/hooks/knowledge-hooks';
import { Chart } from '@antv/g2';
import { sumBy } from 'lodash';
import { useCallback, useEffect, useMemo, useRef } from 'react';
export function TagWordCloud() {
const domRef = useRef<HTMLDivElement>(null);
let chartRef = useRef<Chart>();
const { list } = useFetchTagList();
const { list: tagList } = useMemo(() => {
const nextList = list.sort((a, b) => b[1] - a[1]).slice(0, 256);
return {
list: nextList.map((x) => ({ text: x[0], value: x[1], name: x[0] })),
sumValue: sumBy(nextList, (x: [string, number]) => x[1]),
length: nextList.length,
};
}, [list]);
const renderWordCloud = useCallback(() => {
if (domRef.current) {
chartRef.current = new Chart({ container: domRef.current });
chartRef.current.options({
type: 'wordCloud',
autoFit: true,
layout: {
fontSize: [10, 50],
// fontSize: (d: any) => {
// if (d.value) {
// return (d.value / sumValue) * 100 * (length / 10);
// }
// return 0;
// },
},
data: {
type: 'inline',
value: tagList,
},
encode: { color: 'text' },
legend: false,
tooltip: {
title: 'name', // title
items: ['value'], // data item
},
});
chartRef.current.render();
}
}, [tagList]);
useEffect(() => {
renderWordCloud();
return () => {
chartRef.current?.destroy();
};
}, [renderWordCloud]);
return <div ref={domRef} className="w-full h-[38vh]"></div>;
}

View File

@ -1,20 +0,0 @@
const getImageName = (prefix: string, length: number) =>
new Array(length)
.fill(0)
.map((x, idx) => `chunk-method/${prefix}-0${idx + 1}`);
export const ImageMap = {
book: getImageName('book', 4),
laws: getImageName('law', 2),
manual: getImageName('manual', 4),
picture: getImageName('media', 2),
naive: getImageName('naive', 2),
paper: getImageName('paper', 2),
presentation: getImageName('presentation', 2),
qa: getImageName('qa', 2),
resume: getImageName('resume', 2),
table: getImageName('table', 2),
one: getImageName('one', 2),
knowledge_graph: getImageName('knowledge-graph', 2),
tag: getImageName('tag', 2),
};

View File

@ -1,63 +0,0 @@
.sidebarWrapper {
max-width: 288px;
padding: 32px 24px 24px 24px;
flex-direction: column;
.sidebarTop {
text-align: center;
.knowledgeLogo {
}
.knowledgeTitle {
font-size: 16px;
line-height: 24px;
font-weight: @fontWeight700;
margin-bottom: 6px;
}
.knowledgeDescription {
font-size: 12px;
font-weight: @fontWeight600;
color: @gray8;
margin: 0;
}
padding-bottom: 20px;
}
.divider {
height: 2px;
background-image: linear-gradient(
to right,
@gray11 0%,
@gray11 50%,
transparent 50%
);
background-size: 10px 2px;
background-repeat: repeat-x;
}
.menuWrapper {
padding-top: 10px;
.menu {
border: none;
font-size: @fontSize16;
font-weight: @fontWeight600;
:global(.ant-menu-item) {
display: flex;
align-items: center;
}
}
.defaultWidth {
width: 240px;
}
.minWidth {
width: 50px;
}
.menuText {
color: @gray3;
font-size: @fontSize14;
font-weight: @fontWeight700;
}
}
}

View File

@ -1,135 +0,0 @@
import { ReactComponent as ConfigurationIcon } from '@/assets/svg/knowledge-configration.svg';
import { ReactComponent as DatasetIcon } from '@/assets/svg/knowledge-dataset.svg';
import { ReactComponent as TestingIcon } from '@/assets/svg/knowledge-testing.svg';
import {
useFetchKnowledgeBaseConfiguration,
useFetchKnowledgeGraph,
} from '@/hooks/knowledge-hooks';
import {
useGetKnowledgeSearchParams,
useSecondPathName,
} from '@/hooks/route-hook';
import { getWidth } from '@/utils';
import { Avatar, Menu, MenuProps, Space } from 'antd';
import classNames from 'classnames';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'umi';
import { KnowledgeRouteKey } from '../../constant';
import { isEmpty } from 'lodash';
import { GitGraph } from 'lucide-react';
import styles from './index.less';
const KnowledgeSidebar = () => {
let navigate = useNavigate();
const activeKey = useSecondPathName();
const { knowledgeId } = useGetKnowledgeSearchParams();
const [windowWidth, setWindowWidth] = useState(getWidth());
const { t } = useTranslation();
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
const handleSelect: MenuProps['onSelect'] = (e) => {
navigate(`/knowledge/${e.key}?id=${knowledgeId}`);
};
const { data } = useFetchKnowledgeGraph();
type MenuItem = Required<MenuProps>['items'][number];
const getItem = useCallback(
(
label: string,
key: React.Key,
icon?: React.ReactNode,
disabled?: boolean,
children?: MenuItem[],
type?: 'group',
): MenuItem => {
return {
key,
icon,
children,
label: t(`knowledgeDetails.${label}`),
type,
disabled,
} as MenuItem;
},
[t],
);
const items: MenuItem[] = useMemo(() => {
const list = [
getItem(
KnowledgeRouteKey.Dataset, // TODO: Change icon color when selected
KnowledgeRouteKey.Dataset,
<DatasetIcon />,
),
getItem(
KnowledgeRouteKey.Testing,
KnowledgeRouteKey.Testing,
<TestingIcon />,
),
getItem(
KnowledgeRouteKey.Configuration,
KnowledgeRouteKey.Configuration,
<ConfigurationIcon />,
),
];
if (!isEmpty(data?.graph)) {
list.push(
getItem(
KnowledgeRouteKey.KnowledgeGraph,
KnowledgeRouteKey.KnowledgeGraph,
<GitGraph />,
),
);
}
return list;
}, [data, getItem]);
useEffect(() => {
const widthSize = () => {
const width = getWidth();
setWindowWidth(width);
};
window.addEventListener('resize', widthSize);
return () => {
window.removeEventListener('resize', widthSize);
};
}, []);
return (
<div className={styles.sidebarWrapper}>
<div className={styles.sidebarTop}>
<Space size={8} direction="vertical">
<Avatar size={64} src={knowledgeDetails.avatar} />
<div className={styles.knowledgeTitle}>{knowledgeDetails.name}</div>
</Space>
<p className={styles.knowledgeDescription}>
{knowledgeDetails.description}
</p>
</div>
<div className={styles.divider}></div>
<div className={styles.menuWrapper}>
<Menu
selectedKeys={[activeKey]}
// mode="inline"
className={classNames(styles.menu, {
[styles.defaultWidth]: windowWidth.width > 957,
[styles.minWidth]: windowWidth.width <= 957,
})}
// inlineCollapsed={collapsed}
items={items}
onSelect={handleSelect}
/>
</div>
</div>
);
};
export default KnowledgeSidebar;

View File

@ -1,4 +0,0 @@
.testingWrapper {
flex: 1;
height: 100%;
}

View File

@ -1,68 +0,0 @@
import {
useTestChunkAllRetrieval,
useTestChunkRetrieval,
} from '@/hooks/knowledge-hooks';
import { Flex, Form } from 'antd';
import { useMemo, useState } from 'react';
import TestingControl from './testing-control';
import TestingResult from './testing-result';
import styles from './index.less';
const KnowledgeTesting = () => {
const [form] = Form.useForm();
const {
data: retrievalData,
testChunk,
loading: retrievalLoading,
} = useTestChunkRetrieval();
const {
data: allRetrievalData,
testChunkAll,
loading: allRetrievalLoading,
} = useTestChunkAllRetrieval();
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
const handleTesting = async (documentIds: string[] = []) => {
const values = await form.validateFields();
const params = {
...values,
vector_similarity_weight: 1 - values.vector_similarity_weight,
};
if (Array.isArray(documentIds) && documentIds.length > 0) {
testChunk({
...params,
doc_ids: documentIds,
});
} else {
testChunkAll({
...params,
doc_ids: [],
});
}
};
const testingResult = useMemo(() => {
return selectedDocumentIds.length > 0 ? retrievalData : allRetrievalData;
}, [allRetrievalData, retrievalData, selectedDocumentIds.length]);
return (
<Flex className={styles.testingWrapper} gap={16}>
<TestingControl
form={form}
handleTesting={handleTesting}
selectedDocumentIds={selectedDocumentIds}
></TestingControl>
<TestingResult
data={testingResult}
loading={retrievalLoading || allRetrievalLoading}
handleTesting={handleTesting}
selectedDocumentIds={selectedDocumentIds}
setSelectedDocumentIds={setSelectedDocumentIds}
></TestingResult>
</Flex>
);
};
export default KnowledgeTesting;

View File

@ -1,29 +0,0 @@
.testingControlWrapper {
width: 350px;
background-color: rgba(255, 255, 255, 0.1);
padding: 30px 20px;
overflow: auto;
height: calc(100vh - 160px);
.historyTitle {
padding: 30px 0 20px;
}
.historyIcon {
vertical-align: middle;
}
.historyCardWrapper {
width: 100%;
}
.historyCard {
width: 100%;
:global(.ant-card-body) {
padding: 10px;
}
}
.historyText {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
}

View File

@ -1,109 +0,0 @@
import Rerank from '@/components/rerank';
import SimilaritySlider from '@/components/similarity-slider';
import { useTranslate } from '@/hooks/common-hooks';
import { useChunkIsTesting } from '@/hooks/knowledge-hooks';
import { Button, Card, Divider, Flex, Form, Input } from 'antd';
import { FormInstance } from 'antd/lib';
import { LabelWordCloud } from './label-word-cloud';
import { CrossLanguageItem } from '@/components/cross-language-item';
import { UseKnowledgeGraphItem } from '@/components/use-knowledge-graph-item';
import styles from './index.less';
type FieldType = {
similarity_threshold?: number;
vector_similarity_weight?: number;
question: string;
};
interface IProps {
form: FormInstance;
handleTesting: (documentIds?: string[]) => Promise<any>;
selectedDocumentIds: string[];
}
const TestingControl = ({
form,
handleTesting,
selectedDocumentIds,
}: IProps) => {
const question = Form.useWatch('question', { form, preserve: true });
const loading = useChunkIsTesting();
const { t } = useTranslate('knowledgeDetails');
const buttonDisabled =
!question || (typeof question === 'string' && question.trim() === '');
const onClick = () => {
handleTesting(selectedDocumentIds);
};
return (
<section className={styles.testingControlWrapper}>
<div>
<b>{t('testing')}</b>
</div>
<p>{t('testingDescription')}</p>
<Divider></Divider>
<section>
<Form name="testing" layout="vertical" form={form}>
<SimilaritySlider isTooltipShown></SimilaritySlider>
<Rerank></Rerank>
<UseKnowledgeGraphItem filedName={['use_kg']}></UseKnowledgeGraphItem>
<CrossLanguageItem name={'cross_languages'}></CrossLanguageItem>
<Card size="small" title={t('testText')}>
<Form.Item<FieldType>
name={'question'}
rules={[{ required: true, message: t('testTextPlaceholder') }]}
>
<Input.TextArea autoSize={{ minRows: 8 }}></Input.TextArea>
</Form.Item>
<Flex justify={'end'}>
<Button
type="primary"
size="small"
onClick={onClick}
disabled={buttonDisabled}
loading={loading}
>
{t('testingLabel')}
</Button>
</Flex>
</Card>
</Form>
</section>
<LabelWordCloud></LabelWordCloud>
{/* <section>
<div className={styles.historyTitle}>
<Space size={'middle'}>
<HistoryOutlined className={styles.historyIcon} />
<b>Test history</b>
</Space>
</div>
<Space
direction="vertical"
size={'middle'}
className={styles.historyCardWrapper}
>
{list.map((x) => (
<Card className={styles.historyCard} key={x}>
<Flex justify={'space-between'} gap={'small'}>
<span>{x}</span>
<div className={styles.historyText}>
content dcjsjl snldsh svnodvn svnodrfn svjdoghdtbnhdo
sdvhodhbuid sldghdrlh
</div>
<Flex gap={'small'}>
<span>time</span>
<DeleteOutlined></DeleteOutlined>
</Flex>
</Flex>
</Card>
))}
</Space>
</section> */}
</section>
);
};
export default TestingControl;

View File

@ -1,59 +0,0 @@
import { useSelectTestingResult } from '@/hooks/knowledge-hooks';
import { Chart } from '@antv/g2';
import { useCallback, useEffect, useMemo, useRef } from 'react';
export function LabelWordCloud() {
const domRef = useRef<HTMLDivElement>(null);
let chartRef = useRef<Chart>();
const { labels } = useSelectTestingResult();
const list = useMemo(() => {
if (!labels) {
return [];
}
return Object.keys(labels).reduce<
Array<{ text: string; name: string; value: number }>
>((pre, cur) => {
pre.push({ name: cur, text: cur, value: labels[cur] });
return pre;
}, []);
}, [labels]);
const renderWordCloud = useCallback(() => {
if (domRef.current && list.length) {
chartRef.current = new Chart({ container: domRef.current });
chartRef.current.options({
type: 'wordCloud',
autoFit: true,
layout: {
fontSize: [6, 15],
},
data: {
type: 'inline',
value: list,
},
encode: { color: 'text' },
legend: false,
tooltip: {
title: 'name', // title
items: ['value'], // data item
},
});
chartRef.current.render();
}
}, [list]);
useEffect(() => {
renderWordCloud();
return () => {
chartRef.current?.destroy();
};
}, [renderWordCloud]);
return <div ref={domRef} className="w-full h-[13vh]"></div>;
}

View File

@ -1,46 +0,0 @@
.testingResultWrapper {
flex: 1;
background-color: rgba(255, 255, 255, 0.1);
padding: 30px 20px;
overflow: auto;
height: calc(100vh - 160px);
display: flex;
flex-direction: column;
justify-content: space-between;
.selectFilesCollapse {
:global(.ant-collapse-header) {
padding-left: 22px;
}
margin-bottom: 32px;
overflow-y: auto;
}
.selectFilesTitle {
padding-right: 10px;
}
.similarityCircle {
width: 24px;
height: 24px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.1);
font-size: 10px;
font-weight: normal;
}
.similarityText {
font-size: 12px;
font-weight: 500;
}
.image {
width: 100%;
max-height: 30vh;
object-fit: contain;
}
}
.imagePreview {
display: block;
max-width: 45vw;
max-height: 40vh;
}

View File

@ -1,147 +0,0 @@
import { ReactComponent as SelectedFilesCollapseIcon } from '@/assets/svg/selected-files-collapse.svg';
import { useTranslate } from '@/hooks/common-hooks';
import { ITestingChunk, ITestingResult } from '@/interfaces/database/knowledge';
import {
Card,
Collapse,
Empty,
Flex,
Image,
Pagination,
PaginationProps,
Space,
} from 'antd';
import camelCase from 'lodash/camelCase';
import SelectFiles from './select-files';
import { useGetPaginationWithRouter } from '@/hooks/logic-hooks';
import { api_host } from '@/utils/api';
import { showImage } from '@/utils/chat';
import { useCallback } from 'react';
import styles from './index.less';
const similarityList: Array<{ field: keyof ITestingChunk; label: string }> = [
{ field: 'similarity', label: 'Hybrid Similarity' },
{ field: 'term_similarity', label: 'Term Similarity' },
{ field: 'vector_similarity', label: 'Vector Similarity' },
];
const ChunkTitle = ({ item }: { item: ITestingChunk }) => {
const { t } = useTranslate('knowledgeDetails');
return (
<Flex gap={10}>
{similarityList.map((x) => (
<Space key={x.field}>
<span className={styles.similarityCircle}>
{((item[x.field] as number) * 100).toFixed(2)}
</span>
<span className={styles.similarityText}>{t(camelCase(x.field))}</span>
</Space>
))}
</Flex>
);
};
interface IProps {
handleTesting: (documentIds?: string[]) => Promise<any>;
selectedDocumentIds: string[];
setSelectedDocumentIds: (ids: string[]) => void;
data?: ITestingResult;
loading?: boolean;
}
const TestingResult = ({
handleTesting,
selectedDocumentIds,
setSelectedDocumentIds,
data,
loading,
}: IProps) => {
const { documents, chunks, total } = data || {};
const { t } = useTranslate('knowledgeDetails');
const { pagination, setPagination } = useGetPaginationWithRouter();
const onChange: PaginationProps['onChange'] = (pageNumber, pageSize) => {
pagination.onChange?.(pageNumber, pageSize);
handleTesting(selectedDocumentIds);
};
const onTesting = useCallback(
(ids: string[]) => {
setPagination({ page: 1 });
handleTesting(ids);
},
[setPagination, handleTesting],
);
return (
<section className={styles.testingResultWrapper}>
<Collapse
expandIcon={() => (
<SelectedFilesCollapseIcon></SelectedFilesCollapseIcon>
)}
className={styles.selectFilesCollapse}
items={[
{
key: '1',
label: (
<Flex
justify={'space-between'}
align="center"
className={styles.selectFilesTitle}
>
<Space>
<span>
{selectedDocumentIds?.length ?? 0}/{documents?.length ?? 0}
</span>
{t('filesSelected')}
</Space>
</Flex>
),
children: (
<div>
<SelectFiles
setSelectedDocumentIds={setSelectedDocumentIds}
handleTesting={onTesting}
></SelectFiles>
</div>
),
},
]}
/>
<Flex
gap={'large'}
vertical
flex={1}
className={styles.selectFilesCollapse}
>
{loading === false && chunks && chunks.length > 0 ? (
chunks?.map((x) => (
<Card key={x.chunk_id} title={<ChunkTitle item={x}></ChunkTitle>}>
<div className="flex justify-center">
{showImage(x.doc_type_kwd) && (
<Image
id={x.image_id}
className={'object-contain max-h-[30vh] w-full text-center'}
src={`${api_host}/document/image/${x.image_id}`}
></Image>
)}
</div>
<div className="pt-4">{x.content_with_weight}</div>
</Card>
))
) : loading === false && chunks && chunks.length === 0 ? (
<Empty></Empty>
) : null}
</Flex>
<Pagination
{...pagination}
size={'small'}
total={total}
onChange={onChange}
/>
</section>
);
};
export default TestingResult;

View File

@ -1,73 +0,0 @@
import NewDocumentLink from '@/components/new-document-link';
import { useTranslate } from '@/hooks/common-hooks';
import { useAllTestingResult } from '@/hooks/knowledge-hooks';
import { ITestingDocument } from '@/interfaces/database/knowledge';
import { EyeOutlined } from '@ant-design/icons';
import { Button, Table, TableProps, Tooltip } from 'antd';
interface IProps {
handleTesting: (ids: string[]) => void;
setSelectedDocumentIds: (ids: string[]) => void;
}
const SelectFiles = ({ setSelectedDocumentIds, handleTesting }: IProps) => {
const { documents } = useAllTestingResult();
const { t } = useTranslate('fileManager');
const columns: TableProps<ITestingDocument>['columns'] = [
{
title: 'Name',
dataIndex: 'doc_name',
key: 'doc_name',
render: (text) => <p>{text}</p>,
},
{
title: 'Hits',
dataIndex: 'count',
key: 'count',
width: 80,
},
{
title: 'View',
key: 'view',
width: 50,
render: (_, { doc_id, doc_name }) => (
<NewDocumentLink
documentName={doc_name}
documentId={doc_id}
prefix="document"
>
<Tooltip title={t('preview')}>
<Button type="text">
<EyeOutlined size={20} />
</Button>
</Tooltip>
</NewDocumentLink>
),
},
];
const rowSelection = {
onChange: (selectedRowKeys: React.Key[]) => {
setSelectedDocumentIds(selectedRowKeys as string[]);
handleTesting(selectedRowKeys as string[]);
},
getCheckboxProps: (record: ITestingDocument) => ({
disabled: record.doc_name === 'Disabled User', // Column configuration not to be checked
name: record.doc_name,
}),
};
return (
<Table
columns={columns}
dataSource={documents}
showHeader={false}
rowSelection={rowSelection}
rowKey={'doc_id'}
/>
);
};
export default SelectFiles;

View File

@ -1,21 +0,0 @@
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';

View File

@ -1,19 +0,0 @@
.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;
}
}

View File

@ -1,74 +0,0 @@
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;

View File

@ -1,5 +1,4 @@
import { MessageType } from '@/constants/chat';
import { useGetFileIcon } from '@/pages/chat/hooks';
import { useSendAgentMessage } from './use-send-agent-message';
@ -19,6 +18,7 @@ import { useParams } from 'umi';
import DebugContent from '../debug-content';
import { useAwaitCompentData } from '../hooks/use-chat-logic';
import { useIsTaskMode } from '../hooks/use-get-begin-query';
import { useGetFileIcon } from './use-get-file-icon';
function AgentChatBox() {
const { data: canvasInfo, refetch } = useFetchAgent();

View File

@ -0,0 +1,12 @@
import { fileIconMap } from '@/constants/common';
import { getFileExtension } from '@/utils';
export const useGetFileIcon = () => {
const getFileIcon = (filename: string) => {
const ext: string = getFileExtension(filename);
const iconPath = fileIconMap[ext as keyof typeof fileIconMap];
return `@/assets/svg/file-icon/${iconPath}`;
};
return getFileIcon;
};

View File

@ -11,7 +11,7 @@ import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { IMessage } from '@/pages/chat/interface';
import { IMessage } from '@/interfaces/database/chat';
import { zodResolver } from '@hookform/resolvers/zod';
import React, { ReactNode, useCallback, useMemo } from 'react';
import { useForm } from 'react-hook-form';

View File

@ -1,6 +1,5 @@
import { MessageType } from '@/constants/chat';
import { Message } from '@/interfaces/database/chat';
import { IMessage } from '@/pages/chat/interface';
import { IMessage, Message } from '@/interfaces/database/chat';
import { get } from 'lodash';
import { useCallback, useMemo } from 'react';
import { BeginQuery } from '../interface';

View File

@ -11,12 +11,12 @@ import i18n from '@/locales/config';
import DebugContent from '@/pages/agent/debug-content';
import { useCacheChatLog } from '@/pages/agent/hooks/use-cache-chat-log';
import { useAwaitCompentData } from '@/pages/agent/hooks/use-chat-logic';
import { useSendButtonDisabled } from '@/pages/chat/hooks';
import { buildMessageUuidWithRole } from '@/utils/chat';
import { isEmpty } from 'lodash';
import React, { forwardRef, useCallback } from 'react';
import {
useGetSharedChatSearchParams,
useSendButtonDisabled,
useSendNextSharedMessage,
} from '../hooks/use-send-shared-message';
import { ParameterDialog } from './parameter-dialog';

View File

@ -1,6 +1,5 @@
import { MessageType } from '@/constants/chat';
import { IReference } from '@/interfaces/database/chat';
import { IMessage } from '@/pages/chat/interface';
import { IMessage, IReference } from '@/interfaces/database/chat';
import { isEmpty } from 'lodash';
export const buildAgentMessageItemReference = (

View File

@ -3,10 +3,13 @@ import { Modal } from '@/components/ui/modal/modal';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { IAgentLogMessage } from '@/interfaces/database/agent';
import { IReferenceObject, Message } from '@/interfaces/database/chat';
import {
IMessage,
IReferenceObject,
Message,
} from '@/interfaces/database/chat';
import { buildMessageUuidWithRole } from '@/utils/chat';
import React, { useMemo } from 'react';
import { IMessage } from '../chat/interface';
interface CustomModalProps {
isOpen: boolean;

View File

@ -6,7 +6,7 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { TagRenameId } from '@/pages/add-knowledge/constant';
import { TagRenameId } from '@/constants/knowledge';
import { useTranslation } from 'react-i18next';
import { CreateAgentForm, CreateAgentFormProps } from './create-agent-form';

View File

@ -7,9 +7,9 @@ import { z } from 'zod';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Card, CardContent } from '@/components/ui/card';
import { Form } from '@/components/ui/form';
import { TagRenameId } from '@/constants/knowledge';
import { IModalProps } from '@/interfaces/common';
import { cn } from '@/lib/utils';
import { TagRenameId } from '@/pages/add-knowledge/constant';
import { BrainCircuit, Check, Route } from 'lucide-react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';

Some files were not shown because too many files have changed in this diff Show More