feat: remove KnowledgeSearching and add knowledge configuration page and add a run button to the document (#64)

* feat: add a run button to the document

* feat: add knowledge configuration page

* feat: remove KnowledgeSearching
This commit is contained in:
balibabu 2024-02-18 18:18:20 +08:00 committed by GitHub
parent 53be70c7a9
commit f3d0ebd293
22 changed files with 495 additions and 687 deletions

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M14.1667 4.27108C15.9345 5.55903 17.0834 7.6453 17.0834 9.99992C17.0834 13.9119 13.9121 17.0833 10.0001 17.0833H9.58342M5.83341 15.7288C4.06564 14.4408 2.91675 12.3545 2.91675 9.99992C2.91675 6.0879 6.08806 2.91659 10.0001 2.91659H10.4167M10.8334 18.6666L9.16675 16.9999L10.8334 15.3333M9.16675 4.66659L10.8334 2.99992L9.16675 1.33325"
stroke="#17B26A" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 557 B

View File

@ -0,0 +1,15 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_490_5128)">
<path
d="M10.0001 18.3334C14.6025 18.3334 18.3334 14.6025 18.3334 10.0001C18.3334 5.39771 14.6025 1.66675 10.0001 1.66675C5.39771 1.66675 1.66675 5.39771 1.66675 10.0001C1.66675 14.6025 5.39771 18.3334 10.0001 18.3334Z"
stroke="#17B26A" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M7.91675 7.47119C7.91675 7.07345 7.91675 6.87459 7.99987 6.76356C8.0723 6.66681 8.18318 6.60628 8.30373 6.59767C8.44207 6.58779 8.60935 6.69533 8.94392 6.91041L12.8777 9.4393C13.1681 9.62593 13.3132 9.71925 13.3634 9.83791C13.4072 9.94159 13.4072 10.0586 13.3634 10.1623C13.3132 10.2809 13.1681 10.3742 12.8777 10.5609L8.94392 13.0898C8.60935 13.3048 8.44207 13.4124 8.30373 13.4025C8.18318 13.3939 8.0723 13.3334 7.99987 13.2366C7.91675 13.1256 7.91675 12.9267 7.91675 12.529V7.47119Z"
stroke="#17B26A" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
</g>
<defs>
<clipPath id="clip0_490_5128">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -2,13 +2,12 @@ export enum KnowledgeRouteKey {
Dataset = 'dataset',
Testing = 'testing',
Configuration = 'configuration',
TempTesting = 'tempTesting',
}
export enum RunningStatus {
UNSTART = '0',
RUNNING = '1',
CANCEL = '2',
DONE = '3',
FAIL = '4',
UNSTART = '0', // need to run
RUNNING = '1', // need to cancel
CANCEL = '2', // need to refresh
DONE = '3', // need to refresh
FAIL = '4', // need to refresh
}

View File

@ -1,6 +1,6 @@
import showDeleteConfirm from '@/components/deleting-confirm';
import { IKnowledge } from '@/interfaces/database/knowledge';
import { useCallback } from 'react';
import { IKnowledge, ITenantInfo } from '@/interfaces/database/knowledge';
import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSearchParams, useSelector } from 'umi';
export const useKnowledgeBaseId = (): string => {
@ -77,3 +77,50 @@ export const useDeleteChunkByIds = (): {
removeChunk: onRemoveChunk,
};
};
export const useSelectParserList = (): Array<{
value: string;
label: string;
}> => {
const tenantIfo: Nullable<ITenantInfo> = useSelector(
(state: any) => state.settingModel.tenantIfo,
);
const parserList = useMemo(() => {
const parserArray: Array<string> = tenantIfo?.parser_ids.split(',') ?? [];
return parserArray.map((x) => {
const arr = x.split(':');
return { value: arr[0], label: arr[1] };
});
}, [tenantIfo]);
return parserList;
};
export const useFetchParserList = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch({
type: 'settingModel/getTenantInfo',
});
}, [dispatch]);
};
export const useFetchKnowledgeBaseConfiguration = () => {
const dispatch = useDispatch();
const knowledgeBaseId = useKnowledgeBaseId();
const fetchKnowledgeBaseConfiguration = useCallback(() => {
dispatch({
type: 'kSModel/getKbDetail',
payload: {
kb_id: knowledgeBaseId,
},
});
}, [dispatch, knowledgeBaseId]);
useEffect(() => {
fetchKnowledgeBaseConfiguration();
}, [fetchKnowledgeBaseConfiguration]);
};

View File

@ -0,0 +1,16 @@
export interface IThirdOAIModel {
available: boolean;
create_date: string;
create_time: number;
fid: string;
id: number;
llm_name: string;
max_tokens: number;
model_type: string;
status: string;
tags: string;
update_date: string;
update_time: number;
}
export type IThirdOAIModelCollection = Record<string, IThirdOAIModel[]>;

View File

@ -26,6 +26,7 @@
.chunkContainer {
height: calc(100vh - 320px);
overflow: auto;
width: 100%;
}
.pageFooter {

View File

@ -2,10 +2,11 @@ import { ReactComponent as SelectFilesEndIcon } from '@/assets/svg/select-files-
import { ReactComponent as SelectFilesStartIcon } from '@/assets/svg/select-files-start.svg';
import {
useDeleteDocumentById,
useFetchParserList,
useGetDocumentDefaultParser,
useKnowledgeBaseId,
useSelectParserList,
} from '@/hooks/knowledgeHook';
import { ITenantInfo } from '@/interfaces/database/knowledge';
import uploadService from '@/services/uploadService';
import {
ArrowLeftOutlined,
@ -28,9 +29,8 @@ import {
UploadProps,
} from 'antd';
import classNames from 'classnames';
import { ReactElement, useEffect, useMemo, useRef, useState } from 'react';
import { Nullable } from 'typings';
import { Link, useDispatch, useNavigate, useSelector } from 'umi';
import { ReactElement, useEffect, useRef, useState } from 'react';
import { Link, useDispatch, useNavigate } from 'umi';
import { KnowledgeRouteKey } from '@/constants/knowledge';
import styles from './index.less';
@ -45,13 +45,11 @@ const UploaderItem = ({
file,
actions,
isUpload,
parserArray,
}: {
isUpload: boolean;
originNode: ReactElement;
file: UploadFile;
fileList: object[];
parserArray: string[];
actions: { download: Function; preview: Function; remove: any };
}) => {
const { parserConfig, defaultParserId } = useGetDocumentDefaultParser(
@ -63,12 +61,7 @@ const UploaderItem = ({
const documentId = file?.response?.id;
const parserList = useMemo(() => {
return parserArray.map((x) => {
const arr = x.split(':');
return { value: arr[0], label: arr[1] };
});
}, [parserArray]);
const parserList = useSelectParserList();
const saveParser = (parserId: string) => {
dispatch({
@ -154,14 +147,10 @@ const KnowledgeUploadFile = () => {
const knowledgeBaseId = useKnowledgeBaseId();
const [isUpload, setIsUpload] = useState(true);
const dispatch = useDispatch();
const tenantIfo: Nullable<ITenantInfo> = useSelector(
(state: any) => state.settingModel.tenantIfo,
);
const navigate = useNavigate();
const fileListRef = useRef<UploadFile[]>([]);
const parserArray = tenantIfo?.parser_ids.split(',') ?? [];
const createRequest: (props: UploadRequestOption) => void = async function ({
file,
onSuccess,
@ -170,9 +159,13 @@ const KnowledgeUploadFile = () => {
}) {
const { data } = await uploadService.uploadFile(file, knowledgeBaseId);
if (data.retcode === 0) {
onSuccess && onSuccess(data.data);
if (onSuccess) {
onSuccess(data.data);
}
} else {
onError && onError(data.data);
if (onError) {
onError(data.data);
}
}
};
@ -188,7 +181,6 @@ const KnowledgeUploadFile = () => {
fileList={fileList}
originNode={originNode}
actions={actions}
parserArray={parserArray}
></UploaderItem>
);
},
@ -215,11 +207,7 @@ const KnowledgeUploadFile = () => {
}
};
useEffect(() => {
dispatch({
type: 'settingModel/getTenantInfo',
});
}, []);
useFetchParserList();
return (
<div className={styles.uploadWrapper}>

View File

@ -1,8 +1,5 @@
import { KnowledgeRouteKey } from '@/constants/knowledge';
import {
useDeleteDocumentById,
useKnowledgeBaseId,
} from '@/hooks/knowledgeHook';
import { useKnowledgeBaseId } from '@/hooks/knowledgeHook';
import { Pagination } from '@/interfaces/common';
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { getOneNamespaceEffectsLoading } from '@/utils/storeUtil';
@ -40,7 +37,6 @@ const KnowledgeFile = () => {
const effects = useSelector((state: any) => state.loading.effects);
const { data, total } = kFModel;
const knowledgeBaseId = useKnowledgeBaseId();
const { removeDocument } = useDeleteDocumentById();
const loading = getOneNamespaceEffectsLoading('kFModel', effects, [
'getKfList',
@ -132,9 +128,7 @@ const KnowledgeFile = () => {
},
});
};
const onRmDocument = () => {
removeDocument(doc_id);
};
const showCEFModal = () => {
dispatch({
type: 'kFModel/updateState',
@ -144,15 +138,6 @@ const KnowledgeFile = () => {
});
};
const showSegmentSetModal = () => {
dispatch({
type: 'kFModel/updateState',
payload: {
isShowSegmentSetModal: true,
},
});
};
const actionItems: MenuProps['items'] = useMemo(() => {
return [
{
@ -185,31 +170,6 @@ const KnowledgeFile = () => {
},
];
}, []);
const chunkItems: MenuProps['items'] = [
{
key: '1',
label: (
<div>
<Button type="link" onClick={showSegmentSetModal}>
{' '}
</Button>
</div>
),
},
{
key: '2',
label: (
<div>
<Button type="link" onClick={onRmDocument}>
{' '}
Delete
</Button>
</div>
),
// disabled: true,
},
];
const toChunk = (id: string) => {
navigate(

View File

@ -55,33 +55,23 @@ const model: DvaModel<KFModelState> = {
return { ...state, pagination: { ...state.pagination, ...payload } };
},
},
subscriptions: {
setup({ dispatch, history }) {
history.listen((location) => {});
},
},
effects: {
*createKf({ payload = {} }, { call, put }) {
const { data, response } = yield call(kbService.createKb, payload);
const { retcode, data: res, retmsg } = data;
*createKf({ payload = {} }, { call }) {
const { data } = yield call(kbService.createKb, payload);
const { retcode } = data;
if (retcode === 0) {
message.success('创建成功!');
}
},
*updateKf({ payload = {} }, { call, put }) {
const { data, response } = yield call(kbService.updateKb, payload);
const { retcode, data: res, retmsg } = data;
*updateKf({ payload = {} }, { call }) {
const { data } = yield call(kbService.updateKb, payload);
const { retcode } = data;
if (retcode === 0) {
message.success('修改成功!');
}
},
*getKfDetail({ payload = {}, callback }, { call, put }) {
const { data, response } = yield call(kbService.get_kb_detail, payload);
const { retcode, data: res, retmsg } = data;
if (retcode === 0) {
// localStorage.setItem('userInfo',res.)
callback && callback(res);
}
*getKfDetail({ payload = {} }, { call }) {
const { data } = yield call(kbService.get_kb_detail, payload);
},
*getKfList({ payload = {} }, { call, put, select }) {
const state: KFModelState = yield select((state: any) => state.kFModel);
@ -119,11 +109,11 @@ const model: DvaModel<KFModelState> = {
{ type: 'poll', delay: 5000 }, // TODO: Provide type support for this effect
],
*updateDocumentStatus({ payload = {} }, { call, put }) {
const { data, response } = yield call(
const { data } = yield call(
kbService.document_change_status,
pick(payload, ['doc_id', 'status']),
);
const { retcode, data: res, retmsg } = data;
const { retcode } = data;
if (retcode === 0) {
message.success('修改成功!');
put({
@ -133,10 +123,10 @@ const model: DvaModel<KFModelState> = {
}
},
*document_rm({ payload = {} }, { call, put }) {
const { data, response } = yield call(kbService.document_rm, {
const { data } = yield call(kbService.document_rm, {
doc_id: payload.doc_id,
});
const { retcode, data: res, retmsg } = data;
const { retcode } = data;
if (retcode === 0) {
message.success('删除成功!');
yield put({
@ -151,7 +141,7 @@ const model: DvaModel<KFModelState> = {
kbService.document_rename,
omit(payload, ['kb_id']),
);
const { retcode, data: res, retmsg } = data;
const { retcode } = data;
if (retcode === 0) {
message.success('rename success');
yield put({
@ -168,7 +158,7 @@ const model: DvaModel<KFModelState> = {
},
*document_create({ payload = {} }, { call, put }) {
const { data } = yield call(kbService.document_create, payload);
const { retcode, data: res } = data;
const { retcode } = data;
if (retcode === 0) {
put({
type: 'kFModel/updateState',
@ -181,19 +171,25 @@ const model: DvaModel<KFModelState> = {
return retcode;
},
*document_run({ payload = {} }, { call, put }) {
const { data } = yield call(kbService.document_run, payload);
const { data } = yield call(
kbService.document_run,
omit(payload, ['knowledgeBaseId']),
);
const { retcode } = data;
if (retcode === 0) {
message.success('Run successfully ');
if (payload.knowledgeBaseId) {
yield put({
type: 'getKfList',
payload: { kb_id: payload.knowledgeBaseId },
});
}
message.success('Operation successfully ');
}
return retcode;
},
*document_change_parser({ payload = {} }, { call, put }) {
const { data, response } = yield call(
kbService.document_change_parser,
payload,
);
const { retcode, data: res, retmsg } = data;
const { data } = yield call(kbService.document_change_parser, payload);
const { retcode } = data;
if (retcode === 0) {
put({
type: 'updateState',

View File

@ -1,3 +1,12 @@
.popover-content {
width: 300px;
}
.operationIcon {
text-align: center;
margin-right: 20%;
width: 20px;
&:hover {
cursor: pointer;
}
}

View File

@ -1,9 +1,21 @@
import { ReactComponent as RefreshIcon } from '@/assets/svg/refresh.svg';
import { ReactComponent as RunIcon } from '@/assets/svg/run.svg';
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { Badge, DescriptionsProps, Flex, Popover, Space, Tag } from 'antd';
import { RunningStatus, RunningStatusMap } from '../constant';
import { CloseCircleOutlined } from '@ant-design/icons';
import { useDispatch } from 'umi';
import styles from './index.less';
const iconMap = {
[RunningStatus.UNSTART]: RunIcon,
[RunningStatus.RUNNING]: CloseCircleOutlined,
[RunningStatus.CANCEL]: RefreshIcon,
[RunningStatus.DONE]: RefreshIcon,
[RunningStatus.FAIL]: RefreshIcon,
};
interface IProps {
record: IKnowledgeFile;
}
@ -31,7 +43,7 @@ const PopoverContent = ({ record }: IProps) => {
<Flex vertical className={styles['popover-content']}>
{items.map((x) => {
return (
<div>
<div key={x.key}>
<b>{x.label}:</b>
<p>{x.children}</p>
</div>
@ -42,12 +54,27 @@ const PopoverContent = ({ record }: IProps) => {
};
export const ParsingStatusCell = ({ record }: IProps) => {
const dispatch = useDispatch();
const text = record.run;
const runningStatus = RunningStatusMap[text];
const isRunning = text === RunningStatus.RUNNING;
const OperationIcon = iconMap[text];
const handleOperationIconClick = () => {
dispatch({
type: 'kFModel/document_run',
payload: {
doc_ids: [record.id],
run: isRunning ? 2 : 1,
knowledgeBaseId: record.kb_id,
},
});
};
return (
<Flex justify={'space-between'}>
<Popover
content={isRunning && <PopoverContent record={record}></PopoverContent>}
>
@ -56,13 +83,17 @@ export const ParsingStatusCell = ({ record }: IProps) => {
<Space>
<Badge color={runningStatus.color} />
{runningStatus.label}
<span>{record.progress * 100}%</span>
<span>{(record.progress * 100).toFixed(2)}%</span>
</Space>
) : (
runningStatus.label
)}
</Tag>
</Popover>
<div onClick={handleOperationIconClick} className={styles.operationIcon}>
<OperationIcon />
</div>
</Flex>
);
};

View File

@ -1,79 +0,0 @@
.chunkPage {
padding: 24px;
display: flex;
height: calc(100vh - 112px);
// flex-direction: column;
.filter {
margin-right: 20px;
display: flex;
height: 32px;
width: 300px;
flex-wrap: wrap;
justify-content: space-between;
}
.pageContainer {
flex: 1;
display: flex;
flex-direction: column;
.pageContent {
flex: 1;
width: 100%;
padding-right: 12px;
overflow-y: auto;
.spin {
min-height: 400px;
}
}
.pageFooter {
height: 32px;
float: right;
}
}
}
.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,276 +0,0 @@
import { useKnowledgeBaseId } from '@/hooks/knowledgeHook';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import { api_host } from '@/utils/api';
import { DeleteOutlined, MinusSquareOutlined } from '@ant-design/icons';
import type { PaginationProps } from 'antd';
import {
Card,
Col,
Input,
Pagination,
Popconfirm,
Row,
Select,
Spin,
Switch,
} from 'antd';
import { debounce } from 'lodash';
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'umi';
import CreateModal from '../knowledge-chunk/components/chunk-creating-modal';
import styles from './index.less';
const KnowledgeSearching = () => {
const dispatch = useDispatch();
const kSearchModel = useSelector((state: any) => state.kSearchModel);
const chunkModel = useSelector((state: any) => state.chunkModel);
const loading = useOneNamespaceEffectsLoading('kSearchModel', [
'chunk_list',
'switch_chunk',
]);
const knowledgeBaseId = useKnowledgeBaseId();
const {
data = [],
total,
d_list = [],
question,
doc_ids,
pagination,
} = kSearchModel;
const { chunk_id, doc_id, isShowCreateModal } = chunkModel;
const getChunkList = () => {
dispatch({
type: 'kSearchModel/chunk_list',
payload: {
kb_id: knowledgeBaseId,
},
});
};
const confirm = (id: string) => {
dispatch({
type: 'kSearchModel/rm_chunk',
payload: {
chunk_ids: [id],
kb_id: knowledgeBaseId,
},
});
};
const handleEditchunk = (item: any) => {
const { chunk_id, doc_id } = item;
dispatch({
type: 'chunkModel/updateState',
payload: {
isShowCreateModal: true,
chunk_id,
doc_id,
},
});
getChunkList();
};
const onShowSizeChange: PaginationProps['onShowSizeChange'] = (
page,
size,
) => {
dispatch({
type: 'kSearchModel/updateState',
payload: {
pagination: { page, size },
},
});
};
useEffect(() => {
dispatch({
type: 'kSearchModel/updateState',
payload: {
doc_ids: [],
question: '',
},
});
dispatch({
type: 'kSearchModel/getKfList',
payload: {
kb_id: knowledgeBaseId,
},
});
}, []);
const switchChunk = (item: any, available_int: boolean) => {
const { chunk_id, doc_id } = item;
dispatch({
type: 'kSearchModel/switch_chunk',
payload: {
chunk_ids: [chunk_id],
doc_id,
available_int,
kb_id: knowledgeBaseId,
},
});
};
useEffect(() => {
getChunkList();
}, [doc_ids, pagination, question]);
const debounceChange = debounce((value) => {
dispatch({
type: 'kSearchModel/updateState',
payload: {
question: value,
},
});
}, 300);
const debounceCallback = useCallback(
(value: string) => debounceChange(value),
[],
);
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const value = e.target.value;
debounceCallback(value);
};
const handleSelectChange = (value: any[]) => {
dispatch({
type: 'kSearchModel/updateState',
payload: {
doc_ids: value,
},
});
};
return (
<>
<div className={styles.chunkPage}>
<div className={styles.filter}>
<Select
showSearch
placeholder="文件列表"
optionFilterProp="children"
onChange={handleSelectChange}
style={{ width: 300, marginBottom: 20 }}
options={d_list}
fieldNames={{ label: 'name', value: 'id' }}
mode="multiple"
/>
<Input.TextArea
autoSize={{ minRows: 6, maxRows: 6 }}
placeholder="搜索"
style={{ width: 300 }}
allowClear
onChange={handleInputChange}
/>
</div>
<div className={styles.pageContainer}>
<div className={styles.pageContent}>
<Spin spinning={loading} className={styles.spin} size="large">
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24 }}>
{data.map((item: any) => {
return (
<Col
className="gutter-row"
key={item.chunk_id}
xs={24}
sm={12}
md={12}
lg={8}
>
<Card
className={styles.card}
onClick={() => {
handleEditchunk(item);
}}
>
<img
style={{ width: '50px' }}
src={`${api_host}/document/image/${item.img_id}`}
alt=""
/>
<div className={styles.container}>
<div className={styles.content}>
<span className={styles.context}>
{item.content_ltks}
</span>
<span className={styles.delete}>
<Switch
size="small"
defaultValue={item.doc_ids == '1'}
onChange={(checked: boolean, e: any) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
switchChunk(item, checked);
}}
/>
</span>
</div>
<div className={styles.footer}>
<span className={styles.text}>
<MinusSquareOutlined />
{item.doc_num}
</span>
<span className={styles.text}>
<MinusSquareOutlined />
{item.chunk_num}
</span>
<span className={styles.text}>
<MinusSquareOutlined />
{item.token_num}
</span>
<span style={{ float: 'right' }}>
<Popconfirm
title="Delete the task"
description="Are you sure to delete this task?"
onConfirm={(e: any) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
console.log(confirm);
confirm(item.chunk_id);
}}
okText="Yes"
cancelText="No"
>
<DeleteOutlined
onClick={(e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}}
/>
</Popconfirm>
</span>
</div>
</div>
</Card>
</Col>
);
})}
</Row>
</Spin>
</div>
<div className={styles.pageFooter}>
<Pagination
responsive
showLessItems
showQuickJumper
showSizeChanger
onChange={onShowSizeChange}
defaultPageSize={30}
pageSizeOptions={[30, 60, 90]}
defaultCurrent={pagination.page}
total={total}
/>
</div>
</div>
</div>
<CreateModal
isShowCreateModal={isShowCreateModal}
chunk_id={chunk_id}
doc_id={doc_id}
/>
</>
);
};
export default KnowledgeSearching;

View File

@ -1,160 +0,0 @@
import kbService from '@/services/kbService';
import omit from 'lodash/omit';
import { DvaModel } from 'umi';
export interface KSearchModelState {
loading: boolean;
data: any[];
total: number;
isShowCreateModal: boolean;
chunk_id: string;
chunkInfo: any;
d_list: any[];
question: string;
doc_ids: any[];
pagination: any;
doc_id: string;
}
const model: DvaModel<KSearchModelState> = {
namespace: 'kSearchModel',
state: {
loading: false,
data: [],
total: 0,
isShowCreateModal: false,
chunk_id: '',
chunkInfo: {},
d_list: [],
question: '',
doc_ids: [],
pagination: { page: 1, size: 30 },
doc_id: '',
},
reducers: {
updateState(state, { payload }) {
return {
...state,
...payload,
};
},
},
subscriptions: {
setup({ dispatch, history }) {
history.listen((location) => {
console.log(location);
});
},
},
effects: {
*getKfList({ payload = {} }, { call, put }) {
const { data, response } = yield call(
kbService.get_document_list,
payload,
);
const { retcode, data: res, retmsg } = data;
if (retcode === 0) {
yield put({
type: 'updateState',
payload: {
d_list: res,
},
});
}
},
*chunk_list({ payload = {} }, { call, put, select }) {
const { question, doc_ids, pagination }: KSearchModelState = yield select(
(state: any) => state.kSearchModel,
);
const { data } = yield call(kbService.retrieval_test, {
...payload,
...pagination,
question,
doc_ids,
similarity_threshold: 0.1,
});
const { retcode, data: res, retmsg } = data;
if (retcode === 0) {
yield put({
type: 'updateState',
payload: {
data: res.chunks,
total: res.total,
},
});
}
},
*switch_chunk({ payload = {} }, { call, put }) {
const { data } = yield call(
kbService.switch_chunk,
omit(payload, ['kb_id']),
);
const { retcode } = data;
if (retcode === 0) {
yield put({
type: 'chunk_list',
payload: {
kb_id: payload.kb_id,
},
});
}
},
*rm_chunk({ payload = {} }, { call, put }) {
const { data } = yield call(kbService.rm_chunk, {
chunk_ids: payload.chunk_ids,
});
const { retcode, data: res, retmsg } = data;
if (retcode === 0) {
// TODO: Can be extracted
yield put({
type: 'chunk_list',
payload: {
kb_id: payload.kb_id,
},
});
}
},
*get_chunk({ payload = {} }, { call, put }) {
const { data, response } = yield call(kbService.get_chunk, payload);
const { retcode, data: res, retmsg } = data;
if (retcode === 0) {
yield put({
type: 'updateState',
payload: {
chunkInfo: res,
},
});
}
},
*create_hunk({ payload = {} }, { call, put }) {
yield put({
type: 'updateState',
payload: {
loading: true,
},
});
let service = kbService.create_chunk;
if (payload.chunk_id) {
service = kbService.set_chunk;
}
const { data } = yield call(service, payload);
const { retcode } = data;
yield put({
type: 'updateState',
payload: {
loading: false,
},
});
if (retcode === 0) {
yield put({
type: 'updateState',
payload: {
isShowCreateModal: false,
},
});
}
},
},
};
export default model;

View File

@ -0,0 +1,217 @@
import {
useFetchKnowledgeBaseConfiguration,
useFetchParserList,
useKnowledgeBaseId,
useSelectParserList,
} from '@/hooks/knowledgeHook';
import {
Button,
Divider,
Form,
Input,
Radio,
Select,
Space,
Typography,
Upload,
UploadFile,
} from 'antd';
import pick from 'lodash/pick';
import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'umi';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import { IKnowledge } from '@/interfaces/database/knowledge';
import { IThirdOAIModelCollection } from '@/interfaces/database/llm';
import { PlusOutlined } from '@ant-design/icons';
import styles from './index.less';
const { Title } = Typography;
const { Option } = Select;
const Configuration = () => {
const [form] = Form.useForm();
const dispatch = useDispatch();
const knowledgeBaseId = useKnowledgeBaseId();
const loading = useOneNamespaceEffectsLoading('kSModel', ['updateKb']);
const llmInfo: IThirdOAIModelCollection = useSelector(
(state: any) => state.settingModel.llmInfo,
);
const knowledgeDetails: IKnowledge = useSelector(
(state: any) => state.kSModel.knowledgeDetails,
);
const normFile = (e: any) => {
if (Array.isArray(e)) {
return e;
}
return e?.fileList;
};
const parserList = useSelectParserList();
const embeddingModelOptions = useMemo(() => {
return Object.entries(llmInfo).map(([key, value]) => {
return {
label: key,
options: value.map((x) => ({
label: x.llm_name,
value: x.llm_name,
})),
};
});
}, [llmInfo]);
const onFinish = async (values: any) => {
console.info(values);
const fileList = values.avatar;
let avatar;
if (Array.isArray(fileList)) {
avatar = fileList[0].thumbUrl;
}
dispatch({
type: 'kSModel/updateKb',
payload: {
...values,
avatar,
kb_id: knowledgeBaseId,
},
});
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const fetchLlmList = useCallback(() => {
dispatch({
type: 'settingModel/llm_list',
payload: { model_type: 'embedding' },
});
}, [dispatch]);
useEffect(() => {
const avatar = knowledgeDetails.avatar;
let fileList: UploadFile[] = [];
if (avatar) {
fileList = [{ uid: '1', name: 'file', thumbUrl: avatar, status: 'done' }];
}
form.setFieldsValue({
...pick(knowledgeDetails, [
'description',
'name',
'permission',
'embd_id',
'parser_id',
]),
avatar: fileList,
});
}, [form, knowledgeDetails]);
useFetchParserList();
useFetchKnowledgeBaseConfiguration();
useEffect(() => {
fetchLlmList();
}, [fetchLlmList]);
return (
<div className={styles.configurationWrapper}>
<Title level={5}>Configuration</Title>
<p>Update your knowledge base details especially parsing method here.</p>
<Divider></Divider>
<Form
form={form}
name="validateOnly"
layout="vertical"
autoComplete="off"
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
name="name"
label="Knowledge base name"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
name="avatar"
label="Knowledge base photo"
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload
listType="picture-card"
maxCount={1}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
>
<button style={{ border: 0, background: 'none' }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</button>
</Upload>
</Form.Item>
<Form.Item name="description" label="Knowledge base bio">
<Input />
</Form.Item>
<Form.Item
name="permission"
label="Permissions"
rules={[{ required: true }]}
>
<Radio.Group>
<Radio value="me">Only me</Radio>
<Radio value="team">Team</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name="embd_id"
label="Embedding Model"
rules={[{ required: true }]}
>
<Select
placeholder="Please select a country"
options={embeddingModelOptions}
></Select>
</Form.Item>
<Form.Item
name="parser_id"
label="Knowledge base category"
rules={[{ required: true }]}
>
<Select placeholder="Please select a country">
{parserList.map((x) => (
<Option value={x.value} key={x.value}>
{x.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item>
<div className={styles.buttonWrapper}>
<Space>
<Button htmlType="reset" size={'middle'}>
Cancel
</Button>
<Button
htmlType="submit"
type="primary"
size={'middle'}
loading={loading}
>
Save
</Button>
</Space>
</div>
</Form.Item>
</Form>
</div>
);
};
export default Configuration;

View File

@ -12,7 +12,6 @@
.left {
flex: 1;
}
.right {
@ -22,3 +21,10 @@
padding: 5px;
}
}
.configurationWrapper {
padding: 0 52px;
.buttonWrapper {
text-align: right;
}
}

View File

@ -3,6 +3,8 @@ import { useKnowledgeBaseId } from '@/hooks/knowledgeHook';
import { Button, Form, Input, Radio, Select, Space, Tag } from 'antd';
import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useNavigate, useSelector } from 'umi';
import Configuration from './configuration';
import styles from './index.less';
const { CheckableTag } = Tag;
@ -12,7 +14,6 @@ const layout = {
labelAlign: 'left' as const,
};
const { Option } = Select;
/* eslint-disable no-template-curly-in-string */
const KnowledgeSetting = () => {
const dispatch = useDispatch();
@ -44,7 +45,7 @@ const KnowledgeSetting = () => {
setSelectedTag(data.data.parser_id);
}
}
}, [knowledgeBaseId]);
}, [knowledgeBaseId, dispatch, form]);
const onFinish = async () => {
try {
@ -68,11 +69,12 @@ const KnowledgeSetting = () => {
parser_id: selectedTag,
},
});
retcode === 0 &&
if (retcode === 0) {
navigate(
`/knowledge/${KnowledgeRouteKey.Dataset}?id=${knowledgeBaseId}`,
);
}
}
} catch (error) {
console.warn(error);
}
@ -158,4 +160,6 @@ const KnowledgeSetting = () => {
);
};
export default KnowledgeSetting;
// export default KnowledgeSetting;
export default Configuration;

View File

@ -1,3 +1,4 @@
import { IKnowledge } from '@/interfaces/database/knowledge';
import kbService from '@/services/kbService';
import { message } from 'antd';
import { DvaModel } from 'umi';
@ -6,6 +7,7 @@ export interface KSModelState {
isShowPSwModal: boolean;
isShowTntModal: boolean;
tenantIfo: any;
knowledgeDetails: IKnowledge;
}
const model: DvaModel<KSModelState> = {
@ -14,6 +16,7 @@ const model: DvaModel<KSModelState> = {
isShowPSwModal: false,
isShowTntModal: false,
tenantIfo: {},
knowledgeDetails: {} as any,
},
reducers: {
updateState(state, { payload }) {
@ -22,31 +25,32 @@ const model: DvaModel<KSModelState> = {
...payload,
};
},
},
subscriptions: {
setup({ dispatch, history }) {
history.listen((location) => {});
setKnowledgeDetails(state, { payload }) {
return { ...state, knowledgeDetails: payload };
},
},
effects: {
*createKb({ payload = {} }, { call, put }) {
*createKb({ payload = {} }, { call }) {
const { data } = yield call(kbService.createKb, payload);
const { retcode } = data;
if (retcode === 0) {
message.success('创建知识库成功!');
message.success('Created successfully!');
}
return data;
},
*updateKb({ payload = {} }, { call, put }) {
const { data } = yield call(kbService.updateKb, payload);
const { retcode, data: res, retmsg } = data;
const { retcode } = data;
if (retcode === 0) {
message.success('更新知识库成功!');
yield put({ type: 'getKbDetail', payload: { kb_id: payload.kb_id } });
message.success('Updated successfully!');
}
},
*getKbDetail({ payload = {} }, { call, put }) {
const { data } = yield call(kbService.get_kb_detail, payload);
if (data.retcode === 0) {
yield put({ type: 'setKnowledgeDetails', payload: data.data });
}
return data;
},
},

View File

@ -1,9 +1,10 @@
import { ReactComponent as ConfigrationIcon } from '@/assets/svg/knowledge-configration.svg';
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 } from '@/hooks/knowledgeHook';
import { useSecondPathName } from '@/hooks/routeHook';
import { IKnowledge } from '@/interfaces/database/knowledge';
import { getWidth } from '@/utils';
import { AntDesignOutlined } from '@ant-design/icons';
import { Avatar, Menu, MenuProps, Space } from 'antd';
import classNames from 'classnames';
import { useCallback, useEffect, useMemo, useState } from 'react';
@ -16,6 +17,9 @@ const KnowledgeSidebar = () => {
const { id } = kAModel;
let navigate = useNavigate();
const activeKey = useSecondPathName();
const knowledgeDetails: IKnowledge = useSelector(
(state: any) => state.kSModel.knowledgeDetails,
);
const [windowWidth, setWindowWidth] = useState(getWidth());
const [collapsed, setCollapsed] = useState(false);
@ -62,12 +66,7 @@ const KnowledgeSidebar = () => {
getItem(
routeMap[KnowledgeRouteKey.Configuration],
KnowledgeRouteKey.Configuration,
<ConfigrationIcon />,
),
getItem(
routeMap[KnowledgeRouteKey.TempTesting],
KnowledgeRouteKey.TempTesting,
<TestingIcon />,
<ConfigurationIcon />,
),
];
}, [getItem]);
@ -93,16 +92,17 @@ const KnowledgeSidebar = () => {
};
}, []);
useFetchKnowledgeBaseConfiguration();
return (
<div className={styles.sidebarWrapper}>
<div className={styles.sidebarTop}>
<Space size={8} direction="vertical">
<Avatar size={64} icon={<AntDesignOutlined />} />
<div className={styles.knowledgeTitle}>Cloud Computing</div>
<Avatar size={64} src={knowledgeDetails.avatar} />
<div className={styles.knowledgeTitle}>{knowledgeDetails.name}</div>
</Space>
<p className={styles.knowledgeDescription}>
A scalable, secure cloud-based database optimized for high-performance
computing and data storage.
{knowledgeDetails.description}
</p>
</div>
<div className={styles.divider}></div>

View File

@ -1,4 +1,5 @@
import { ITenantInfo } from '@/interfaces/database/knowledge';
import { IThirdOAIModelCollection as IThirdAiModelCollection } from '@/interfaces/database/llm';
import userService from '@/services/userService';
import authorizationUtil from '@/utils/authorizationUtil';
import { message } from 'antd';
@ -12,7 +13,7 @@ export interface SettingModelState {
isShowSSModal: boolean;
llm_factory: string;
tenantIfo: Nullable<ITenantInfo>;
llmInfo: any;
llmInfo: IThirdAiModelCollection;
myLlm: any[];
factoriesList: any[];
}
@ -126,8 +127,8 @@ const model: DvaModel<SettingModelState> = {
}
},
*llm_list({ payload = {} }, { call, put }) {
const { data, response } = yield call(userService.llm_list, payload);
const { retcode, data: res, retmsg } = data;
const { data } = yield call(userService.llm_list, payload);
const { retcode, data: res } = data;
if (retcode === 0) {
yield put({
type: 'updateState',

View File

@ -44,10 +44,6 @@ const routes = [
},
{
path: '/knowledge/testing',
component: '@/pages/add-knowledge/components/knowledge-search',
},
{
path: '/knowledge/tempTesting',
component: '@/pages/add-knowledge/components/knowledge-testing',
},
],

28
web/src/utils/fileUtil.ts Normal file
View File

@ -0,0 +1,28 @@
export const transformFile2Base64 = (val: any): Promise<any> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(val);
reader.onload = (): void => {
resolve(reader.result);
};
reader.onerror = reject;
});
};
export const transformBase64ToFile = (
dataUrl: string,
filename: string = 'file',
) => {
let arr = dataUrl.split(','),
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
const mime = arr[0].match(/:(.*?);/);
const mimeType = mime ? mime[1] : 'image/png';
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mimeType });
};