Quellcode durchsuchen

feat: modify routing to nested mode and rename document (#52)

* feat: modify routing to nested mode

* feat: rename document
tags/v0.1.0
balibabu vor 1 Jahr
Ursprung
Commit
7b71fb2db6
33 geänderte Dateien mit 681 neuen und 175 gelöschten Zeilen
  1. 0
    0
      web/src/components/deleting-confirm/index.less
  2. 28
    0
      web/src/components/deleting-confirm/index.tsx
  3. 24
    0
      web/src/components/modal-manager.tsx
  4. 9
    1
      web/src/constants/knowledge.ts
  5. 8
    0
      web/src/hooks/knowledgeHook.ts
  6. 21
    0
      web/src/hooks/routeHook.ts
  7. 26
    0
      web/src/interfaces/database/knowledge.ts
  8. 10
    14
      web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx
  9. 0
    0
      web/src/pages/add-knowledge/components/knowledge-dataset/index.less
  10. 7
    0
      web/src/pages/add-knowledge/components/knowledge-dataset/index.tsx
  11. 5
    0
      web/src/pages/add-knowledge/components/knowledge-dataset/knowledge-upload-file/index.tsx
  12. 17
    0
      web/src/pages/add-knowledge/components/knowledge-file/constant.ts
  13. 12
    7
      web/src/pages/add-knowledge/components/knowledge-file/createEFileModal.tsx
  14. 22
    16
      web/src/pages/add-knowledge/components/knowledge-file/index.less
  15. 82
    68
      web/src/pages/add-knowledge/components/knowledge-file/index.tsx
  16. 34
    1
      web/src/pages/add-knowledge/components/knowledge-file/model.ts
  17. 88
    0
      web/src/pages/add-knowledge/components/knowledge-file/parsing-action-cell/index.tsx
  18. 3
    0
      web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.less
  19. 68
    0
      web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx
  20. 88
    0
      web/src/pages/add-knowledge/components/knowledge-file/rename-modal/index.tsx
  21. 0
    1
      web/src/pages/add-knowledge/components/knowledge-file/segmentSetModal.tsx
  22. 9
    10
      web/src/pages/add-knowledge/components/knowledge-search/index.tsx
  23. 12
    11
      web/src/pages/add-knowledge/components/knowledge-setting/index.tsx
  24. 5
    5
      web/src/pages/add-knowledge/components/knowledge-sidebar/index.tsx
  25. 11
    1
      web/src/pages/add-knowledge/constant.ts
  26. 3
    0
      web/src/pages/add-knowledge/index.less
  27. 36
    30
      web/src/pages/add-knowledge/index.tsx
  28. 12
    6
      web/src/pages/knowledge/index.tsx
  29. 1
    1
      web/src/pages/knowledge/knowledge-card/index.less
  30. 30
    1
      web/src/routes.ts
  31. 5
    0
      web/src/services/kbService.ts
  32. 2
    1
      web/src/utils/api.ts
  33. 3
    1
      web/typings.d.ts

+ 0
- 0
web/src/components/deleting-confirm/index.less Datei anzeigen


+ 28
- 0
web/src/components/deleting-confirm/index.tsx Datei anzeigen

import { ExclamationCircleFilled } from '@ant-design/icons';
import { Modal } from 'antd';

const { confirm } = Modal;

interface IProps {
onOk?: (...args: any[]) => any;
onCancel?: (...args: any[]) => any;
}

export const showDeleteConfirm = ({ onOk, onCancel }: IProps) => {
confirm({
title: 'Are you sure delete this item?',
icon: <ExclamationCircleFilled />,
content: 'Some descriptions',
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
onOk() {
onOk?.();
},
onCancel() {
onCancel?.();
},
});
};

export default showDeleteConfirm;

+ 24
- 0
web/src/components/modal-manager.tsx Datei anzeigen

import { useState } from 'react';

interface IProps {
children: (props: {
showModal(): void;
hideModal(): void;
visible: boolean;
}) => React.ReactNode;
}

const ModalManager = ({ children }: IProps) => {
const [visible, setVisible] = useState(false);

const showModal = () => {
setVisible(true);
};
const hideModal = () => {
setVisible(false);
};

return children({ visible, showModal, hideModal });
};

export default ModalManager;

+ 9
- 1
web/src/constants/knowledge.ts Datei anzeigen

export enum KnowledgeRouteKey { export enum KnowledgeRouteKey {
Dataset = 'dataset', Dataset = 'dataset',
Testing = 'testing', Testing = 'testing',
Configration = 'configration',
Configuration = 'configuration',
}

export enum RunningStatus {
UNSTART = '0',
RUNNING = '1',
CANCEL = '2',
DONE = '3',
FAIL = '4',
} }

+ 8
- 0
web/src/hooks/knowledgeHook.ts Datei anzeigen

import { useSearchParams } from 'umi';

export const useKnowledgeBaseId = (): string => {
const [searchParams] = useSearchParams();
const knowledgeBaseId = searchParams.get('id');

return knowledgeBaseId || '';
};

+ 21
- 0
web/src/hooks/routeHook.ts Datei anzeigen

import { useLocation } from 'umi';

export enum SegmentIndex {
Second = '2',
Third = '3',
}

export const useSegmentedPathName = (index: SegmentIndex) => {
const { pathname } = useLocation();

const pathArray = pathname.split('/');
return pathArray[index] || '';
};

export const useSecondPathName = () => {
return useSegmentedPathName(SegmentIndex.Second);
};

export const useThirdPathName = () => {
return useSegmentedPathName(SegmentIndex.Third);
};

+ 26
- 0
web/src/interfaces/database/knowledge.ts Datei anzeigen

import { RunningStatus } from '@/constants/knowledge';

export interface IKnowledgeFile {
chunk_num: number;
create_date: string;
create_time: number;
created_by: string;
id: string;
kb_id: string;
location: string;
name: string;
parser_id: string;
process_begin_at?: any;
process_duation: number;
progress: number; // parsing process
progress_msg: string; // parsing log
run: RunningStatus; // parsing status
size: number;
source_type: string;
status: string; // enabled
thumbnail?: any; // base64
token_num: number;
type: string;
update_date: string;
update_time: number;
}

+ 10
- 14
web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx Datei anzeigen

Spin, Spin,
Switch, Switch,
} from 'antd'; } from 'antd';
import { debounce } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useNavigate, useSelector } from 'umi';
import { useDispatch, useSearchParams, useSelector } from 'umi';
import CreateModal from './components/createModal'; import CreateModal from './components/createModal';


import { debounce } from 'lodash';
import styles from './index.less'; import styles from './index.less';


interface PayloadType { interface PayloadType {
available_int?: number; available_int?: number;
} }


interface IProps {
doc_id: string;
}

const Chunk = ({ doc_id }: IProps) => {
const Chunk = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const chunkModel = useSelector((state: any) => state.chunkModel); const chunkModel = useSelector((state: any) => state.chunkModel);
const [keywords, SetKeywords] = useState(''); const [keywords, SetKeywords] = useState('');
const [available_int, setAvailableInt] = useState(-1); const [available_int, setAvailableInt] = useState(-1);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [pagination, setPagination] = useState({ page: 1, size: 30 }); const [pagination, setPagination] = useState({ page: 1, size: 30 });
// const [datas, setDatas] = useState(data)
const { data = [], total, chunk_id, isShowCreateModal } = chunkModel; const { data = [], total, chunk_id, isShowCreateModal } = chunkModel;
const effects = useSelector((state: any) => state.loading.effects); const effects = useSelector((state: any) => state.loading.effects);
const loading = getOneNamespaceEffectsLoading('chunkModel', effects, [ const loading = getOneNamespaceEffectsLoading('chunkModel', effects, [
'chunk_list', 'chunk_list',
'switch_chunk', 'switch_chunk',
]); ]);
const documentId: string = searchParams.get('doc_id') || '';


const getChunkList = (value?: string) => { const getChunkList = (value?: string) => {
const payload: PayloadType = { const payload: PayloadType = {
doc_id,
doc_id: documentId,
keywords: value || keywords, keywords: value || keywords,
available_int, available_int,
}; };
payload: { payload: {
isShowCreateModal: true, isShowCreateModal: true,
chunk_id, chunk_id,
doc_id,
doc_id: documentId,
}, },
}); });
getChunkList(); getChunkList();
payload: { payload: {
chunk_ids: [id], chunk_ids: [id],
available_int: Number(available_int), available_int: Number(available_int),
doc_id,
doc_id: documentId,
}, },
}); });




useEffect(() => { useEffect(() => {
getChunkList(); getChunkList();
}, [doc_id, available_int, pagination]);
}, [documentId, available_int, pagination]);


const debounceChange = debounce(getChunkList, 300); const debounceChange = debounce(getChunkList, 300);
const debounceCallback = useCallback( const debounceCallback = useCallback(
</div> </div>
</div> </div>
<CreateModal <CreateModal
doc_id={doc_id}
doc_id={documentId}
isShowCreateModal={isShowCreateModal} isShowCreateModal={isShowCreateModal}
chunk_id={chunk_id} chunk_id={chunk_id}
getChunkList={getChunkList} getChunkList={getChunkList}

+ 0
- 0
web/src/pages/add-knowledge/components/knowledge-dataset/index.less Datei anzeigen


+ 7
- 0
web/src/pages/add-knowledge/components/knowledge-dataset/index.tsx Datei anzeigen

import { Outlet } from 'umi';

export const KnowledgeDataset = () => {
return <Outlet></Outlet>;
};

export default KnowledgeDataset;

+ 5
- 0
web/src/pages/add-knowledge/components/knowledge-dataset/knowledge-upload-file/index.tsx Datei anzeigen

const KnowledgeUploadFile = () => {
return <div>KnowledgeUploadFile</div>;
};

export default KnowledgeUploadFile;

+ 17
- 0
web/src/pages/add-knowledge/components/knowledge-file/constant.ts Datei anzeigen

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

+ 12
- 7
web/src/pages/add-knowledge/components/knowledge-file/createEFileModal.tsx Datei anzeigen

const FileCreatingModal: React.FC<kFProps> = ({ getKfList, kb_id }) => { const FileCreatingModal: React.FC<kFProps> = ({ getKfList, kb_id }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [form] = Form.useForm();
const kFModel = useSelector((state: any) => state.kFModel); const kFModel = useSelector((state: any) => state.kFModel);
const { isShowCEFwModal } = kFModel; const { isShowCEFwModal } = kFModel;
const [form] = Form.useForm();
const { t } = useTranslation(); const { t } = useTranslation();
const handleCancel = () => { const handleCancel = () => {
}, },
}); });
}; };
const handleOk = async () => {
const createDocument = async () => {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
const retcode = await dispatch<any>({ const retcode = await dispatch<any>({
} }
}; };
const handleOk = async () => {
createDocument();
};
return ( return (
<Modal <Modal
title="Basic Modal"
title="File Name"
open={isShowCEFwModal} open={isShowCEFwModal}
onOk={handleOk} onOk={handleOk}
onCancel={handleCancel} onCancel={handleCancel}
<Form <Form
form={form} form={form}
name="validateOnly" name="validateOnly"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
style={{ maxWidth: 600 }} style={{ maxWidth: 600 }}
autoComplete="off" autoComplete="off"
> >
<Form.Item<FieldType> <Form.Item<FieldType>
label="文件名"
label="File Name"
name="name" name="name"
rules={[{ required: true, message: 'Please input value!' }]}
rules={[{ required: true, message: 'Please input name!' }]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>

+ 22
- 16
web/src/pages/add-knowledge/components/knowledge-file/index.less Datei anzeigen

.datasetWrapper {
padding: 30px;
flex: 1;
}
.filter { .filter {
height: 32px;
display: flex;
margin: 10px 0;
justify-content: space-between;
height: 32px;
display: flex;
margin: 10px 0;
justify-content: space-between;
padding: 24px 20px;
.search {
flex: 1;
}
// .search {
// flex: 1;
// }
.operate {
width: 200px;
}
// .operate {
// width: 200px;
// }
} }
.img { .img {
height: 16px;
width: 16px;
margin-right: 6px;
height: 16px;
width: 16px;
margin-right: 6px;
} }
.column { .column {
min-width: 200px
min-width: 200px;
} }
.tochunks { .tochunks {
cursor: pointer;
}
cursor: pointer;
}

+ 82
- 68
web/src/pages/add-knowledge/components/knowledge-file/index.tsx Datei anzeigen

import { KnowledgeRouteKey } from '@/constants/knowledge'; import { KnowledgeRouteKey } from '@/constants/knowledge';
import { useKnowledgeBaseId } from '@/hooks/knowledgeHook';
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { getOneNamespaceEffectsLoading } from '@/utils/stroreUtil'; import { getOneNamespaceEffectsLoading } from '@/utils/stroreUtil';
import { DownOutlined } from '@ant-design/icons';
import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
import { Button, Dropdown, Input, Space, Switch, Table } from 'antd';
import {
Button,
Divider,
Dropdown,
Input,
Space,
Switch,
Table,
Tag,
} from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useNavigate, useSelector } from 'umi'; import { useDispatch, useNavigate, useSelector } from 'umi';
import CreateEPModal from './createEFileModal'; import CreateEPModal from './createEFileModal';
import styles from './index.less'; import styles from './index.less';
import ParsingActionCell from './parsing-action-cell';
import ParsingStatusCell from './parsing-status-cell';
import RenameModal from './rename-modal';
import SegmentSetModal from './segmentSetModal'; import SegmentSetModal from './segmentSetModal';
import UploadFile from './upload'; import UploadFile from './upload';
interface DataType {
name: string;
chunk_num: string;
token_num: number;
update_date: string;
size: string;
status: string;
id: string;
parser_id: string;
}
interface KFProps {
kb_id: string;
}
const KnowledgeFile: React.FC<KFProps> = ({ kb_id }) => {
const KnowledgeFile = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const kFModel = useSelector((state: any) => state.kFModel); const kFModel = useSelector((state: any) => state.kFModel);
const effects = useSelector((state: any) => state.loading.effects); const effects = useSelector((state: any) => state.loading.effects);
const { data } = kFModel; const { data } = kFModel;
const knowledgeBaseId = useKnowledgeBaseId();
const loading = getOneNamespaceEffectsLoading('kFModel', effects, [ const loading = getOneNamespaceEffectsLoading('kFModel', effects, [
'getKfList', 'getKfList',
'updateDocumentStatus', 'updateDocumentStatus',
const getKfList = (keywords?: string) => { const getKfList = (keywords?: string) => {
const payload = { const payload = {
kb_id,
kb_id: knowledgeBaseId,
keywords, keywords,
}; };
if (!keywords) { if (!keywords) {
}; };
useEffect(() => { useEffect(() => {
if (kb_id) {
if (knowledgeBaseId) {
getKfList(); getKfList();
} }
}, [kb_id]);
}, [knowledgeBaseId]);
const debounceChange = debounce(getKfList, 300); const debounceChange = debounce(getKfList, 300);
const debounceCallback = useCallback( const debounceCallback = useCallback(
payload: { payload: {
doc_id, doc_id,
status: Number(e), status: Number(e),
kb_id,
kb_id: knowledgeBaseId,
}, },
}); });
}; };
type: 'kFModel/document_rm', type: 'kFModel/document_rm',
payload: { payload: {
doc_id, doc_id,
kb_id,
kb_id: knowledgeBaseId,
}, },
}); });
}; };
}, },
}); });
}; };
const actionItems: MenuProps['items'] = useMemo(() => { const actionItems: MenuProps['items'] = useMemo(() => {
return [ return [
{ {
key: '1', key: '1',
label: ( label: (
<div> <div>
<UploadFile kb_id={kb_id} getKfList={getKfList} />
<UploadFile kb_id={knowledgeBaseId} getKfList={getKfList} />
</div> </div>
), ),
}, },
// disabled: true, // disabled: true,
}, },
]; ];
}, [kb_id]);
}, [knowledgeBaseId]);
const chunkItems: MenuProps['items'] = [ const chunkItems: MenuProps['items'] = [
{ {
key: '1', key: '1',
// disabled: true, // disabled: true,
}, },
]; ];
const toChunk = (id: string) => { const toChunk = (id: string) => {
navigate( navigate(
`/knowledge/${KnowledgeRouteKey.Dataset}?id=${kb_id}&doc_id=${id}`,
`/knowledge/${KnowledgeRouteKey.Dataset}/chunk?id=${knowledgeBaseId}&doc_id=${id}`,
); );
}; };
const columns: ColumnsType<DataType> = [
const setDocumentAndParserId = (record: IKnowledgeFile) => () => {
setDocId(record.id);
setParserId(record.parser_id);
};
const columns: ColumnsType<IKnowledgeFile> = [
{ {
title: '名称',
title: 'Name',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
render: (text: any, { id }) => ( render: (text: any, { id }) => (
{text} {text}
</div> </div>
), ),
className: `${styles.column}`,
}, },
{ {
title: '数据总量',
title: 'Chunk Number',
dataIndex: 'chunk_num', dataIndex: 'chunk_num',
key: 'chunk_num', key: 'chunk_num',
className: `${styles.column}`,
}, },
{ {
title: 'Tokens',
dataIndex: 'token_num',
key: 'token_num',
className: `${styles.column}`,
title: 'Upload Date',
dataIndex: 'create_date',
key: 'create_date',
}, },
{ {
title: '文件大小',
dataIndex: 'size',
key: 'size',
className: `${styles.column}`,
title: 'Parsing Status',
dataIndex: 'run',
key: 'run',
render: (text, record) => {
return <ParsingStatusCell record={record}></ParsingStatusCell>;
},
}, },
{ {
title: '状态',
title: 'Enabled',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
className: `${styles.column}`,
render: (_, { status: string, id }) => (
render: (_, { status, id }) => (
<> <>
<Switch <Switch
defaultChecked={status === '1'} defaultChecked={status === '1'}
{ {
title: 'Action', title: 'Action',
key: 'action', key: 'action',
className: `${styles.column}`,
render: (_, record) => ( render: (_, record) => (
<Space size="middle">
<Dropdown menu={{ items: chunkItems }} trigger={['click']}>
<a
onClick={() => {
setDocId(record.id);
setParserId(record.parser_id);
}}
>
分段设置 <DownOutlined />
</a>
</Dropdown>
</Space>
<ParsingActionCell
documentId={doc_id}
knowledgeBaseId={knowledgeBaseId}
setDocumentAndParserId={setDocumentAndParserId(record)}
record={record}
></ParsingActionCell>
), ),
}, },
]; ];
const finalColumns = columns.map((x) => ({
...x,
className: `${styles.column}`,
}));
return ( return (
<>
<div className={styles.datasetWrapper}>
<h3>Dataset</h3>
<p>Hey, don't forget to adjust the chunk after adding the dataset! 😉</p>
<Divider></Divider>
<div className={styles.filter}> <div className={styles.filter}>
<div className="search">
<Space>
<h3>Total</h3>
<Tag color="purple">100 files</Tag>
</Space>
<Space>
<Input <Input
placeholder="搜索"
placeholder="Seach your files"
value={inputValue} value={inputValue}
style={{ width: 220 }} style={{ width: 220 }}
allowClear allowClear
onChange={handleInputChange} onChange={handleInputChange}
prefix={<SearchOutlined />}
/> />
</div>
<div className="operate">
<Dropdown menu={{ items: actionItems }} trigger={['click']}> <Dropdown menu={{ items: actionItems }} trigger={['click']}>
<a>
导入文件 <DownOutlined />
</a>
<Button type="primary" icon={<PlusOutlined />}>
Add file
</Button>
</Dropdown> </Dropdown>
</div>
</Space>
</div> </div>
<Table <Table
rowKey="id" rowKey="id"
columns={columns}
columns={finalColumns}
dataSource={data} dataSource={data}
loading={loading} loading={loading}
pagination={false} pagination={false}
scroll={{ scrollToFirstRowOnChange: true, x: true }}
scroll={{ scrollToFirstRowOnChange: true, x: true, y: 'fill' }}
/> />
<CreateEPModal getKfList={getKfList} kb_id={kb_id} />
<CreateEPModal getKfList={getKfList} kb_id={knowledgeBaseId} />
<SegmentSetModal <SegmentSetModal
getKfList={getKfList} getKfList={getKfList}
parser_id={parser_id} parser_id={parser_id}
doc_id={doc_id} doc_id={doc_id}
/> />
</>
<RenameModal></RenameModal>
</div>
); );
}; };

+ 34
- 1
web/src/pages/add-knowledge/components/knowledge-file/model.ts Datei anzeigen

import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import kbService from '@/services/kbService'; import kbService from '@/services/kbService';
import { message } from 'antd'; import { message } from 'antd';
import omit from 'lodash/omit';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import { Nullable } from 'typings';
import { DvaModel } from 'umi'; import { DvaModel } from 'umi';
export interface KFModelState { export interface KFModelState {
isShowCEFwModal: boolean; isShowCEFwModal: boolean;
isShowTntModal: boolean; isShowTntModal: boolean;
isShowSegmentSetModal: boolean; isShowSegmentSetModal: boolean;
isShowRenameModal: boolean;
tenantIfo: any; tenantIfo: any;
data: any[];
data: IKnowledgeFile[];
currentRecord: Nullable<IKnowledgeFile>;
} }
const model: DvaModel<KFModelState> = { const model: DvaModel<KFModelState> = {
isShowCEFwModal: false, isShowCEFwModal: false,
isShowTntModal: false, isShowTntModal: false,
isShowSegmentSetModal: false, isShowSegmentSetModal: false,
isShowRenameModal: false,
tenantIfo: {}, tenantIfo: {},
data: [], data: [],
currentRecord: null,
}, },
reducers: { reducers: {
updateState(state, { payload }) { updateState(state, { payload }) {
...payload, ...payload,
}; };
}, },
setIsShowRenameModal(state, { payload }) {
return { ...state, isShowRenameModal: payload };
},
setCurrentRecord(state, { payload }) {
return { ...state, currentRecord: payload };
},
}, },
subscriptions: { subscriptions: {
setup({ dispatch, history }) { setup({ dispatch, history }) {
}); });
} }
}, },
*document_rename({ payload = {} }, { call, put }) {
const { data } = yield call(
kbService.document_rename,
omit(payload, ['kb_id']),
);
const { retcode, data: res, retmsg } = data;
if (retcode === 0) {
message.success('rename success!');
yield put({
type: 'setIsShowRenameModal',
payload: false,
});
yield put({
type: 'getKfList',
payload: { kb_id: payload.kb_id },
});
}
return retcode;
},
*document_create({ payload = {} }, { call, put }) { *document_create({ payload = {} }, { call, put }) {
const { data, response } = yield call(kbService.document_create, payload); const { data, response } = yield call(kbService.document_create, payload);
const { retcode, data: res, retmsg } = data; const { retcode, data: res, retmsg } = data;

+ 88
- 0
web/src/pages/add-knowledge/components/knowledge-file/parsing-action-cell/index.tsx Datei anzeigen

import showDeleteConfirm from '@/components/deleting-confirm';
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { DeleteOutlined, EditOutlined, ToolOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps, Space, Tooltip } from 'antd';
import { useDispatch } from 'umi';

interface IProps {
documentId: string;
knowledgeBaseId: string;
record: IKnowledgeFile;
setDocumentAndParserId: () => void;
}

const ParsingActionCell = ({
documentId,
knowledgeBaseId,
record,
setDocumentAndParserId,
}: IProps) => {
const dispatch = useDispatch();

const removeDocument = () => {
dispatch({
type: 'kFModel/document_rm',
payload: {
doc_id: documentId,
kb_id: knowledgeBaseId,
},
});
};

const onRmDocument = () => {
showDeleteConfirm({ onOk: removeDocument });
};

const setCurrentRecord = () => {
dispatch({
type: 'kFModel/setCurrentRecord',
payload: record,
});
};

const showSegmentSetModal = () => {
dispatch({
type: 'kFModel/updateState',
payload: {
isShowSegmentSetModal: true,
},
});
};

const showRenameModal = () => {
setCurrentRecord();
dispatch({
type: 'kFModel/setIsShowRenameModal',
payload: true,
});
};

const onRename = () => {};

const chunkItems: MenuProps['items'] = [
{
key: '1',
label: (
<div>
<Button type="link" onClick={showSegmentSetModal}>
分段设置
</Button>
</div>
),
},
];

return (
<Space size={'middle'}>
<Dropdown menu={{ items: chunkItems }} trigger={['click']}>
<ToolOutlined size={20} onClick={setDocumentAndParserId} />
</Dropdown>
<Tooltip title="Rename">
<EditOutlined size={20} onClick={showRenameModal} />
</Tooltip>
<DeleteOutlined size={20} onClick={onRmDocument} />
</Space>
);
};

export default ParsingActionCell;

+ 3
- 0
web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.less Datei anzeigen

.popover-content {
width: 300px;
}

+ 68
- 0
web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx Datei anzeigen

import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { Badge, DescriptionsProps, Flex, Popover, Space, Tag } from 'antd';
import { RunningStatus, RunningStatusMap } from '../constant';

import styles from './index.less';

interface IProps {
record: IKnowledgeFile;
}

const PopoverContent = ({ record }: IProps) => {
const items: DescriptionsProps['items'] = [
{
key: 'process_begin_at',
label: 'Process Begin At',
children: record.process_begin_at,
},
{
key: 'process_duation',
label: 'Process Duration',
children: record.process_duation,
},
{
key: 'progress_msg',
label: 'Progress Msg',
children: record.progress_msg,
},
];

return (
<Flex vertical className={styles['popover-content']}>
{items.map((x) => {
return (
<div>
<b>{x.label}:</b>
<p>{x.children}</p>
</div>
);
})}
</Flex>
);
};

export const ParsingStatusCell = ({ record }: IProps) => {
const text = record.run;
const runningStatus = RunningStatusMap[text];

const isRunning = text === RunningStatus.RUNNING;

return (
<Popover
content={isRunning && <PopoverContent record={record}></PopoverContent>}
>
<Tag color={runningStatus.color}>
{isRunning ? (
<Space>
<Badge color={runningStatus.color} />
`${runningStatus.label}${record.progress * 100}%`
</Space>
) : (
runningStatus.label
)}
</Tag>
</Popover>
);
};

export default ParsingStatusCell;

+ 88
- 0
web/src/pages/add-knowledge/components/knowledge-file/rename-modal/index.tsx Datei anzeigen

import { useKnowledgeBaseId } from '@/hooks/knowledgeHook';
import { Form, Input, Modal } from 'antd';
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'umi';

const RenameModal = () => {
const [form] = Form.useForm();
const dispatch = useDispatch();
const kFModel = useSelector((state: any) => state.kFModel);
const loading = useSelector(
(state: any) => state.loading.effects['kFModel/document_rename'],
);
const knowledgeBaseId = useKnowledgeBaseId();
const isModalOpen = kFModel.isShowRenameModal;
const initialName = kFModel.currentRecord?.name;
const documentId = kFModel.currentRecord?.id;

type FieldType = {
name?: string;
};

const closeModal = () => {
dispatch({
type: 'kFModel/setIsShowRenameModal',
payload: false,
});
};

const handleOk = async () => {
const ret = await form.validateFields();

dispatch({
type: 'kFModel/document_rename',
payload: {
doc_id: documentId,
name: ret.name,
kb_id: knowledgeBaseId,
},
});
};

const handleCancel = () => {
closeModal();
};

const onFinish = (values: any) => {
console.log('Success:', values);
};

const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};

useEffect(() => {
form.setFieldValue('name', initialName);
}, [initialName, documentId]);

return (
<Modal
title="Rename"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
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="Name"
name="name"
rules={[{ required: true, message: 'Please input name!' }]}
>
<Input />
</Form.Item>
</Form>
</Modal>
);
};

export default RenameModal;

+ 0
- 1
web/src/pages/add-knowledge/components/knowledge-file/segmentSetModal.tsx Datei anzeigen

}; };
const handleOk = async () => { const handleOk = async () => {
console.log(1111, selectedTag);
const retcode = await dispatch<any>({ const retcode = await dispatch<any>({
type: 'kFModel/document_change_parser', type: 'kFModel/document_change_parser',
payload: { payload: {

+ 9
- 10
web/src/pages/add-knowledge/components/knowledge-search/index.tsx Datei anzeigen

import { useKnowledgeBaseId } from '@/hooks/knowledgeHook';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import { api_host } from '@/utils/api'; import { api_host } from '@/utils/api';
import { DeleteOutlined, MinusSquareOutlined } from '@ant-design/icons'; import { DeleteOutlined, MinusSquareOutlined } from '@ant-design/icons';
import type { PaginationProps } from 'antd'; import type { PaginationProps } from 'antd';
Spin, Spin,
Switch, Switch,
} from 'antd'; } from 'antd';
import { debounce } from 'lodash';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'umi'; import { useDispatch, useSelector } from 'umi';
import CreateModal from '../knowledge-chunk/components/createModal'; import CreateModal from '../knowledge-chunk/components/createModal';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import { debounce } from 'lodash';
import styles from './index.less'; import styles from './index.less';
interface chunkProps {
kb_id: string;
}
const KnowledgeSearching: React.FC<chunkProps> = ({ kb_id }) => {
const KnowledgeSearching = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const kSearchModel = useSelector((state: any) => state.kSearchModel); const kSearchModel = useSelector((state: any) => state.kSearchModel);
const chunkModel = useSelector((state: any) => state.chunkModel); const chunkModel = useSelector((state: any) => state.chunkModel);
'chunk_list', 'chunk_list',
'switch_chunk', 'switch_chunk',
]); ]);
const knowledgeBaseId = useKnowledgeBaseId();
const { const {
data = [], data = [],
dispatch({ dispatch({
type: 'kSearchModel/chunk_list', type: 'kSearchModel/chunk_list',
payload: { payload: {
kb_id,
kb_id: knowledgeBaseId,
}, },
}); });
}; };
type: 'kSearchModel/rm_chunk', type: 'kSearchModel/rm_chunk',
payload: { payload: {
chunk_ids: [id], chunk_ids: [id],
kb_id,
kb_id: knowledgeBaseId,
}, },
}); });
}; };
dispatch({ dispatch({
type: 'kSearchModel/getKfList', type: 'kSearchModel/getKfList',
payload: { payload: {
kb_id,
kb_id: knowledgeBaseId,
}, },
}); });
}, []); }, []);
chunk_ids: [chunk_id], chunk_ids: [chunk_id],
doc_id, doc_id,
available_int, available_int,
kb_id,
kb_id: knowledgeBaseId,
}, },
}); });
}; };

+ 12
- 11
web/src/pages/add-knowledge/components/knowledge-setting/index.tsx Datei anzeigen

import { KnowledgeRouteKey } from '@/constants/knowledge'; import { KnowledgeRouteKey } from '@/constants/knowledge';
import { useKnowledgeBaseId } from '@/hooks/knowledgeHook';
import { Button, Form, Input, Radio, Select, Space, Tag } from 'antd'; import { Button, Form, Input, Radio, Select, Space, Tag } from 'antd';
import React, { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useNavigate, useSelector } from 'umi'; import { useDispatch, useNavigate, useSelector } from 'umi';
import styles from './index.less'; import styles from './index.less';
const { Option } = Select; const { Option } = Select;
/* eslint-disable no-template-curly-in-string */ /* eslint-disable no-template-curly-in-string */
interface kSProps {
kb_id: string;
}
const KnowledgeSetting: React.FC<kSProps> = ({ kb_id }) => {
const KnowledgeSetting = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const settingModel = useSelector((state: any) => state.settingModel); const settingModel = useSelector((state: any) => state.settingModel);
let navigate = useNavigate(); let navigate = useNavigate();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [selectedTag, setSelectedTag] = useState(''); const [selectedTag, setSelectedTag] = useState('');
const values = Form.useWatch([], form); const values = Form.useWatch([], form);
const knowledgeBaseId = useKnowledgeBaseId();
const getTenantInfo = useCallback(async () => { const getTenantInfo = useCallback(async () => {
dispatch({ dispatch({
type: 'settingModel/getTenantInfo', type: 'settingModel/getTenantInfo',
payload: {}, payload: {},
}); });
if (kb_id) {
if (knowledgeBaseId) {
const data = await dispatch<any>({ const data = await dispatch<any>({
type: 'kSModel/getKbDetail', type: 'kSModel/getKbDetail',
payload: { payload: {
kb_id,
kb_id: knowledgeBaseId,
}, },
}); });
if (data.retcode === 0) { if (data.retcode === 0) {
setSelectedTag(data.data.parser_id); setSelectedTag(data.data.parser_id);
} }
} }
}, [kb_id]);
}, [knowledgeBaseId]);
const onFinish = async () => { const onFinish = async () => {
try { try {
await form.validateFields(); await form.validateFields();
if (kb_id) {
if (knowledgeBaseId) {
dispatch({ dispatch({
type: 'kSModel/updateKb', type: 'kSModel/updateKb',
payload: { payload: {
...values, ...values,
parser_id: selectedTag, parser_id: selectedTag,
kb_id,
kb_id: knowledgeBaseId,
embd_id: undefined, embd_id: undefined,
}, },
}); });
}, },
}); });
retcode === 0 && retcode === 0 &&
navigate(`/knowledge/${KnowledgeRouteKey.Dataset}?id=${kb_id}`);
navigate(
`/knowledge/${KnowledgeRouteKey.Dataset}?id=${knowledgeBaseId}`,
);
} }
} catch (error) { } catch (error) {
console.warn(error); console.warn(error);

+ 5
- 5
web/src/pages/add-knowledge/components/knowledge-sidebar/index.tsx Datei anzeigen

import { ReactComponent as ConfigrationIcon } from '@/assets/svg/knowledge-configration.svg'; import { ReactComponent as ConfigrationIcon } from '@/assets/svg/knowledge-configration.svg';
import { ReactComponent as DatasetIcon } from '@/assets/svg/knowledge-dataset.svg'; import { ReactComponent as DatasetIcon } from '@/assets/svg/knowledge-dataset.svg';
import { ReactComponent as TestingIcon } from '@/assets/svg/knowledge-testing.svg'; import { ReactComponent as TestingIcon } from '@/assets/svg/knowledge-testing.svg';
import { useSecondPathName } from '@/hooks/routeHook';
import { getWidth } from '@/utils'; import { getWidth } from '@/utils';
import { AntDesignOutlined } from '@ant-design/icons'; import { AntDesignOutlined } from '@ant-design/icons';
import { Avatar, Menu, MenuProps, Space } from 'antd'; import { Avatar, Menu, MenuProps, Space } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams, useSelector } from 'umi';
import { useNavigate, useSelector } from 'umi';
import { KnowledgeRouteKey, routeMap } from '../../constant'; import { KnowledgeRouteKey, routeMap } from '../../constant';
import styles from './index.less'; import styles from './index.less';


const kAModel = useSelector((state: any) => state.kAModel); const kAModel = useSelector((state: any) => state.kAModel);
const { id } = kAModel; const { id } = kAModel;
let navigate = useNavigate(); let navigate = useNavigate();
const params = useParams();
const activeKey = params.module || KnowledgeRouteKey.Dataset;
const activeKey = useSecondPathName();


const [windowWidth, setWindowWidth] = useState(getWidth()); const [windowWidth, setWindowWidth] = useState(getWidth());
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
<TestingIcon />, <TestingIcon />,
), ),
getItem( getItem(
routeMap[KnowledgeRouteKey.Configration],
KnowledgeRouteKey.Configration,
routeMap[KnowledgeRouteKey.Configuration],
KnowledgeRouteKey.Configuration,
<ConfigrationIcon />, <ConfigrationIcon />,
), ),
]; ];

+ 11
- 1
web/src/pages/add-knowledge/constant.ts Datei anzeigen

export const routeMap = { export const routeMap = {
[KnowledgeRouteKey.Dataset]: 'Dataset', [KnowledgeRouteKey.Dataset]: 'Dataset',
[KnowledgeRouteKey.Testing]: 'Retrieval testing', [KnowledgeRouteKey.Testing]: 'Retrieval testing',
[KnowledgeRouteKey.Configration]: 'Configuration',
[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 * from '@/constants/knowledge';

+ 3
- 0
web/src/pages/add-knowledge/index.less Datei anzeigen

height: 100%; height: 100%;
background-color: rgba(247, 248, 250, 1); background-color: rgba(247, 248, 250, 1);
padding: 16px 20px 28px 40px; padding: 16px 20px 28px 40px;
display: flex;
flex-direction: column;
} }
.content { .content {
background-color: white; background-color: white;
margin-top: 16px; margin-top: 16px;
// flex: 1;
} }
} }

+ 36
- 30
web/src/pages/add-knowledge/index.tsx Datei anzeigen

import { useKnowledgeBaseId } from '@/hooks/knowledgeHook';
import { useSecondPathName, useThirdPathName } from '@/hooks/routeHook';
import { Breadcrumb } from 'antd'; import { Breadcrumb } from 'antd';
import { ItemType } from 'antd/es/breadcrumb/Breadcrumb';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import {
useDispatch,
useLocation,
useNavigate,
useParams,
useSelector,
} from 'umi';
import Chunk from './components/knowledge-chunk';
import File from './components/knowledge-file';
import Search from './components/knowledge-search';
import Setting from './components/knowledge-setting';
import { Link, Outlet, useDispatch, useLocation, useNavigate } from 'umi';
import Siderbar from './components/knowledge-sidebar'; import Siderbar from './components/knowledge-sidebar';
import { KnowledgeRouteKey, routeMap } from './constant';
import {
KnowledgeDatasetRouteKey,
KnowledgeRouteKey,
datasetRouteMap,
routeMap,
} from './constant';
import styles from './index.less'; import styles from './index.less';
const KnowledgeAdding = () => { const KnowledgeAdding = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const kAModel = useSelector((state: any) => state.kAModel);
const navigate = useNavigate(); const navigate = useNavigate();
const { id, doc_id } = kAModel;
const knowledgeBaseId = useKnowledgeBaseId();
const location = useLocation(); const location = useLocation();
const params = useParams();
const activeKey: KnowledgeRouteKey = const activeKey: KnowledgeRouteKey =
(params.module as KnowledgeRouteKey) || KnowledgeRouteKey.Dataset;
(useSecondPathName() as KnowledgeRouteKey) || KnowledgeRouteKey.Dataset;
const datasetActiveKey: KnowledgeDatasetRouteKey =
useThirdPathName() as KnowledgeDatasetRouteKey;
const gotoList = () => { const gotoList = () => {
navigate('/knowledge'); navigate('/knowledge');
}; };
const breadcrumbItems = useMemo(() => {
return [
const breadcrumbItems: ItemType[] = useMemo(() => {
const items: ItemType[] = [
{ {
title: <a onClick={gotoList}>Knowledge Base</a>, title: <a onClick={gotoList}>Knowledge Base</a>,
}, },
{ {
title: routeMap[activeKey],
title: datasetActiveKey ? (
<Link
to={`/knowledge/${KnowledgeRouteKey.Dataset}?id=${knowledgeBaseId}`}
>
{routeMap[activeKey]}
</Link>
) : (
routeMap[activeKey]
),
}, },
]; ];
}, [activeKey]);
if (datasetActiveKey) {
items.push({
title: datasetRouteMap[datasetActiveKey],
});
}
return items;
}, [activeKey, datasetActiveKey]);
useEffect(() => { useEffect(() => {
const search: string = location.search.slice(1); const search: string = location.search.slice(1);
<div className={styles.contentWrapper}> <div className={styles.contentWrapper}>
<Breadcrumb items={breadcrumbItems} /> <Breadcrumb items={breadcrumbItems} />
<div className={styles.content}> <div className={styles.content}>
{activeKey === KnowledgeRouteKey.Dataset && !doc_id && (
<File kb_id={id} />
)}
{activeKey === KnowledgeRouteKey.Configration && (
<Setting kb_id={id} />
)}
{activeKey === KnowledgeRouteKey.Testing && <Search kb_id={id} />}
{activeKey === KnowledgeRouteKey.Dataset && !!doc_id && (
<Chunk doc_id={doc_id} />
)}
<Outlet></Outlet>
</div> </div>
</div> </div>
</div> </div>

+ 12
- 6
web/src/pages/knowledge/index.tsx Datei anzeigen

import { ReactComponent as FilterIcon } from '@/assets/filter.svg'; import { ReactComponent as FilterIcon } from '@/assets/filter.svg';
import { KnowledgeRouteKey } from '@/constants/knowledge';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { Button, Col, Row, Space } from 'antd';
import { Button, Flex, Space } from 'antd';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useDispatch, useNavigate, useSelector } from 'umi'; import { useDispatch, useNavigate, useSelector } from 'umi';
import styles from './index.less'; import styles from './index.less';
}, []); }, []);
const handleAddKnowledge = () => { const handleAddKnowledge = () => {
navigate(`add/setting`);
navigate(`/knowledge/${KnowledgeRouteKey.Configuration}`);
}; };
useEffect(() => { useEffect(() => {
</Button> </Button>
</Space> </Space>
</div> </div>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
{/* <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
{data.map((item: any) => { {data.map((item: any) => {
return ( return (
<Col <Col
key={item.name} key={item.name}
xs={24} xs={24}
sm={12} sm={12}
md={8}
lg={6}
md={10}
lg={8}
> >
<KnowledgeCard item={item}></KnowledgeCard> <KnowledgeCard item={item}></KnowledgeCard>
</Col> </Col>
); );
})} })}
</Row>
</Row> */}
<Flex gap="large" wrap="wrap">
{data.map((item: any) => {
return <KnowledgeCard item={item} key={item.name}></KnowledgeCard>;
})}
</Flex>
</div> </div>
); );
}; };

+ 1
- 1
web/src/pages/knowledge/knowledge-card/index.less Datei anzeigen

border: 1px solid rgba(234, 236, 240, 1); border: 1px solid rgba(234, 236, 240, 1);
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
padding: 24px; padding: 24px;
min-width: 300px;
max-width: 300px;
cursor: pointer; cursor: pointer;


.titleWrapper { .titleWrapper {

+ 30
- 1
web/src/routes.ts Datei anzeigen

component: '@/pages/knowledge', component: '@/pages/knowledge',
}, },
{ {
path: '/knowledge/:module',
path: '/knowledge',
component: '@/pages/add-knowledge', component: '@/pages/add-knowledge',
routes: [
{
path: '/knowledge/dataset',
component: '@/pages/add-knowledge/components/knowledge-dataset',
routes: [
{
path: '/knowledge/dataset',
component: '@/pages/add-knowledge/components/knowledge-file',
},
{
path: '/knowledge/dataset/upload',
component:
'@/pages/add-knowledge/components/knowledge-dataset/knowledge-upload-file',
},
{
path: '/knowledge/dataset/chunk',
component: '@/pages/add-knowledge/components/knowledge-chunk',
},
],
},
{
path: '/knowledge/configuration',
component: '@/pages/add-knowledge/components/knowledge-setting',
},
{
path: '/knowledge/testing',
component: '@/pages/add-knowledge/components/knowledge-search',
},
],
}, },
{ {
path: '/chat', path: '/chat',

+ 5
- 0
web/src/services/kbService.ts Datei anzeigen

switch_chunk, switch_chunk,
rm_chunk, rm_chunk,
retrieval_test, retrieval_test,
document_rename,
} = api; } = api;
const methods = { const methods = {
url: document_rm, url: document_rm,
method: 'post', method: 'post',
}, },
document_rename: {
url: document_rename,
method: 'post',
},
document_create: { document_create: {
url: document_create, url: document_create,
method: 'post', method: 'post',

+ 2
- 1
web/src/utils/api.ts Datei anzeigen

rm_chunk: `${api_host}/chunk/rm`, rm_chunk: `${api_host}/chunk/rm`,
retrieval_test: `${api_host}/chunk/retrieval_test`, retrieval_test: `${api_host}/chunk/retrieval_test`,
// 上传
// 文件管理
upload: `${api_host}/document/upload`, upload: `${api_host}/document/upload`,
get_document_list: `${api_host}/document/list`, get_document_list: `${api_host}/document/list`,
document_change_status: `${api_host}/document/change_status`, document_change_status: `${api_host}/document/change_status`,
document_rm: `${api_host}/document/rm`, document_rm: `${api_host}/document/rm`,
document_rename: `${api_host}/document/rename`,
document_create: `${api_host}/document/create`, document_create: `${api_host}/document/create`,
document_change_parser: `${api_host}/document/change_parser`, document_change_parser: `${api_host}/document/change_parser`,
}; };

+ 3
- 1
web/typings.d.ts Datei anzeigen

import 'umi/typings'; import 'umi/typings';
declare module 'lodash'
declare module 'lodash';
export type Nullable<T> = T | null;

Laden…
Abbrechen
Speichern