fix: disable sending messages if both application and conversation are empty and add loading to all pages (#134)

* feat: add loading to all pages

* fix: disable sending messages if both application and conversation are empty

* feat: add chatSpin class to Spin of chat
This commit is contained in:
balibabu
2024-03-20 11:13:51 +08:00
committed by GitHub
parent d38e92aac8
commit 78727c8809
24 changed files with 629 additions and 473 deletions

View File

@@ -1,4 +1,4 @@
import { useGetKnowledgeSearchParams } from '@/hooks/knowledgeHook';
import { useGetKnowledgeSearchParams } from '@/hooks/routeHook';
import { api_host } from '@/utils/api';
import { useSize } from 'ahooks';
import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types';

View File

@@ -1,3 +1,4 @@
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge';
import { buildChunkHighlights } from '@/utils/documentUtils';
import { useCallback, useMemo, useState } from 'react';
@@ -46,3 +47,11 @@ export const useGetChunkHighlights = (
return highlights;
};
export const useSelectChunkListLoading = () => {
return useOneNamespaceEffectsLoading('chunkModel', [
'create_hunk',
'chunk_list',
'switch_chunk',
]);
};

View File

@@ -1,23 +1,22 @@
import { useFetchChunkList } from '@/hooks/chunkHooks';
import { useDeleteChunkByIds } from '@/hooks/knowledgeHook';
import { getOneNamespaceEffectsLoading } from '@/utils/storeUtil';
import type { PaginationProps } from 'antd';
import { Divider, Flex, Pagination, Space, Spin, message } from 'antd';
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSearchParams, useSelector } from 'umi';
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';
import classNames from 'classnames';
import DocumentPreview from './components/document-preview/preview';
import { useHandleChunkCardClick, useSelectDocumentInfo } from './hooks';
import {
useHandleChunkCardClick,
useSelectChunkListLoading,
useSelectDocumentInfo,
} from './hooks';
import { ChunkModelState } from './model';
import styles from './index.less';
interface PayloadType {
doc_id: string;
keywords?: string;
}
const Chunk = () => {
const dispatch = useDispatch();
@@ -27,12 +26,7 @@ const Chunk = () => {
const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]);
const [searchParams] = useSearchParams();
const { data = [], total, pagination } = chunkModel;
const effects = useSelector((state: any) => state.loading.effects);
const loading = getOneNamespaceEffectsLoading('chunkModel', effects, [
'create_hunk',
'chunk_list',
'switch_chunk',
]);
const loading = useSelectChunkListLoading();
const documentId: string = searchParams.get('doc_id') || '';
const [chunkId, setChunkId] = useState<string | undefined>();
const { removeChunk } = useDeleteChunkByIds();
@@ -40,18 +34,7 @@ const Chunk = () => {
const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick();
const isPdf = documentInfo.type === 'pdf';
const getChunkList = useCallback(() => {
const payload: PayloadType = {
doc_id: documentId,
};
dispatch({
type: 'chunkModel/chunk_list',
payload: {
...payload,
},
});
}, [dispatch, documentId]);
const getChunkList = useFetchChunkList();
const handleEditChunk = useCallback(
(chunk_id?: string) => {
@@ -169,8 +152,8 @@ const Chunk = () => {
vertical
className={isPdf ? styles.pagePdfWrapper : styles.pageWrapper}
>
<div className={styles.pageContent}>
<Spin spinning={loading} className={styles.spin} size="large">
<Spin spinning={loading} className={styles.spin} size="large">
<div className={styles.pageContent}>
<Space
direction="vertical"
size={'middle'}
@@ -193,8 +176,8 @@ const Chunk = () => {
></ChunkCard>
))}
</Space>
</Spin>
</div>
</div>
</Spin>
<div className={styles.pageFooter}>
<Pagination
responsive

View File

@@ -52,8 +52,10 @@ const RenameModal = () => {
};
useEffect(() => {
form.setFieldValue('name', initialName);
}, [initialName, documentId, form]);
if (isModalOpen) {
form.setFieldValue('name', initialName);
}
}, [initialName, documentId, form, isModalOpen]);
return (
<Modal

View File

@@ -1,19 +1,4 @@
import {
useFetchKnowledgeBaseConfiguration,
useKnowledgeBaseId,
} from '@/hooks/knowledgeHook';
import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import {
useFetchTenantInfo,
useSelectParserList,
} from '@/hooks/userSettingHook';
import { IKnowledge } from '@/interfaces/database/knowledge';
import {
getBase64FromUploadFileList,
getUploadFileListFromBase64,
normFile,
} from '@/utils/fileUtil';
import { normFile } from '@/utils/fileUtil';
import { PlusOutlined } from '@ant-design/icons';
import {
Button,
@@ -26,14 +11,14 @@ import {
Select,
Slider,
Space,
Spin,
Typography,
Upload,
UploadFile,
} from 'antd';
import pick from 'lodash/pick';
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'umi';
import { LlmModelType } from '../../constant';
import {
useFetchKnowledgeConfigurationOnMount,
useSubmitKnowledgeConfiguration,
} from './hooks';
import styles from './index.less';
@@ -41,205 +26,165 @@ const { Title } = Typography;
const { Option } = Select;
const Configuration = () => {
const [form] = Form.useForm();
const dispatch = useDispatch();
const knowledgeBaseId = useKnowledgeBaseId();
const loading = useOneNamespaceEffectsLoading('kSModel', ['updateKb']);
const knowledgeDetails: IKnowledge = useSelector(
(state: any) => state.kSModel.knowledgeDetails,
);
const parserList = useSelectParserList();
const embeddingModelOptions = useSelectLlmOptions();
const onFinish = async (values: any) => {
const avatar = await getBase64FromUploadFileList(values.avatar);
dispatch({
type: 'kSModel/updateKb',
payload: {
...values,
avatar,
kb_id: knowledgeBaseId,
},
});
};
const { submitKnowledgeConfiguration, submitLoading } =
useSubmitKnowledgeConfiguration();
const { form, parserList, embeddingModelOptions, loading } =
useFetchKnowledgeConfigurationOnMount();
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
useEffect(() => {
const fileList: UploadFile[] = getUploadFileListFromBase64(
knowledgeDetails.avatar,
);
form.setFieldsValue({
...pick(knowledgeDetails, [
'description',
'name',
'permission',
'embd_id',
'parser_id',
'language',
'parser_config.chunk_token_num',
]),
avatar: fileList,
});
}, [form, knowledgeDetails]);
useFetchTenantInfo();
useFetchKnowledgeBaseConfiguration();
useFetchLlmList(LlmModelType.Embedding);
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 }]}
<Spin spinning={loading}>
<Form
form={form}
name="validateOnly"
layout="vertical"
autoComplete="off"
onFinish={submitKnowledgeConfiguration}
onFinishFailed={onFinishFailed}
>
<Input />
</Form.Item>
<Form.Item
name="avatar"
label="Knowledge base photo"
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload
listType="picture-card"
maxCount={1}
beforeUpload={() => false}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
<Form.Item
name="name"
label="Knowledge base name"
rules={[{ required: true }]}
>
<button style={{ border: 0, background: 'none' }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</button>
</Upload>
</Form.Item>
<Form.Item name="description" label="Description">
<Input />
</Form.Item>
<Form.Item
label="Language"
name="language"
initialValue={'Chinese'}
rules={[{ required: true, message: 'Please input your language!' }]}
>
<Select placeholder="select your language">
<Option value="English">English</Option>
<Option value="Chinese">Chinese</Option>
</Select>
</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 }]}
tooltip="xx"
>
<Select
placeholder="Please select a country"
options={embeddingModelOptions}
></Select>
</Form.Item>
<Form.Item
name="parser_id"
label="Knowledge base category"
tooltip="xx"
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 noStyle dependencies={['parser_id']}>
{({ getFieldValue }) => {
const parserId = getFieldValue('parser_id');
<Input />
</Form.Item>
<Form.Item
name="avatar"
label="Knowledge base 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 }}>Upload</div>
</button>
</Upload>
</Form.Item>
<Form.Item name="description" label="Description">
<Input />
</Form.Item>
<Form.Item
label="Language"
name="language"
initialValue={'Chinese'}
rules={[{ required: true, message: 'Please input your language!' }]}
>
<Select placeholder="select your language">
<Option value="English">English</Option>
<Option value="Chinese">Chinese</Option>
</Select>
</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 }]}
tooltip="xx"
>
<Select
placeholder="Please select a country"
options={embeddingModelOptions}
></Select>
</Form.Item>
<Form.Item
name="parser_id"
label="Knowledge base category"
tooltip="xx"
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 noStyle dependencies={['parser_id']}>
{({ getFieldValue }) => {
const parserId = getFieldValue('parser_id');
if (parserId === 'naive') {
return (
<Form.Item label="Chunk token number" tooltip="xxx">
<Flex gap={20} align="center">
<Flex flex={1}>
if (parserId === 'naive') {
return (
<Form.Item label="Chunk token number" tooltip="xxx">
<Flex gap={20} align="center">
<Flex flex={1}>
<Form.Item
name={['parser_config', 'chunk_token_num']}
noStyle
initialValue={128}
rules={[
{ required: true, message: 'Province is required' },
]}
>
<Slider
className={styles.variableSlider}
max={2048}
/>
</Form.Item>
</Flex>
<Form.Item
name={['parser_config', 'chunk_token_num']}
noStyle
initialValue={128}
rules={[
{ required: true, message: 'Province is required' },
{ required: true, message: 'Street is required' },
]}
>
<Slider className={styles.variableSlider} max={2048} />
<InputNumber
className={styles.sliderInputNumber}
max={2048}
min={0}
/>
</Form.Item>
</Flex>
<Form.Item
name={['parser_config', 'chunk_token_num']}
noStyle
initialValue={128}
rules={[
{ required: true, message: 'Street is required' },
]}
>
<InputNumber
className={styles.sliderInputNumber}
max={2048}
min={0}
/>
</Form.Item>
</Flex>
</Form.Item>
);
}
return null;
}}
</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>
</Form.Item>
);
}
return null;
}}
</Form.Item>
<Form.Item>
<div className={styles.buttonWrapper}>
<Space>
<Button htmlType="reset" size={'middle'}>
Cancel
</Button>
<Button
htmlType="submit"
type="primary"
size={'middle'}
loading={submitLoading}
>
Save
</Button>
</Space>
</div>
</Form.Item>
</Form>
</Spin>
</div>
);
};

View File

@@ -0,0 +1,73 @@
import {
useFetchKnowledgeBaseConfiguration,
useKnowledgeBaseId,
useSelectKnowledgeDetails,
useUpdateKnowledge,
} from '@/hooks/knowledgeHook';
import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import {
useFetchTenantInfo,
useSelectParserList,
} from '@/hooks/userSettingHook';
import {
getBase64FromUploadFileList,
getUploadFileListFromBase64,
} from '@/utils/fileUtil';
import { Form, UploadFile } from 'antd';
import pick from 'lodash/pick';
import { useCallback, useEffect } from 'react';
import { LlmModelType } from '../../constant';
export const useSubmitKnowledgeConfiguration = () => {
const save = useUpdateKnowledge();
const knowledgeBaseId = useKnowledgeBaseId();
const submitLoading = useOneNamespaceEffectsLoading('kSModel', ['updateKb']);
const submitKnowledgeConfiguration = useCallback(
async (values: any) => {
const avatar = await getBase64FromUploadFileList(values.avatar);
save({
...values,
avatar,
kb_id: knowledgeBaseId,
});
},
[save, knowledgeBaseId],
);
return { submitKnowledgeConfiguration, submitLoading };
};
export const useFetchKnowledgeConfigurationOnMount = () => {
const [form] = Form.useForm();
const loading = useOneNamespaceEffectsLoading('kSModel', ['getKbDetail']);
const knowledgeDetails = useSelectKnowledgeDetails();
const parserList = useSelectParserList();
const embeddingModelOptions = useSelectLlmOptions();
useFetchTenantInfo();
useFetchKnowledgeBaseConfiguration();
useFetchLlmList(LlmModelType.Embedding);
useEffect(() => {
const fileList: UploadFile[] = getUploadFileListFromBase64(
knowledgeDetails.avatar,
);
form.setFieldsValue({
...pick(knowledgeDetails, [
'description',
'name',
'permission',
'embd_id',
'parser_id',
'language',
'parser_config.chunk_token_num',
]),
avatar: fileList,
});
}, [form, knowledgeDetails]);
return { form, parserList, embeddingModelOptions, loading };
};

View File

@@ -34,7 +34,7 @@ const model: DvaModel<KSModelState> = {
const { data } = yield call(kbService.createKb, payload);
const { retcode } = data;
if (retcode === 0) {
message.success('Created successfully!');
message.success('Created!');
}
return data;
},
@@ -43,7 +43,7 @@ const model: DvaModel<KSModelState> = {
const { retcode } = data;
if (retcode === 0) {
yield put({ type: 'getKbDetail', payload: { kb_id: payload.kb_id } });
message.success('Updated successfully!');
message.success('Updated!');
}
},
*getKbDetail({ payload = {} }, { call, put }) {

View File

@@ -37,9 +37,9 @@ const TestingControl = ({ form, handleTesting }: IProps) => {
return (
<section className={styles.testingControlWrapper}>
<p>
<div>
<b>Retrieval testing</b>
</p>
</div>
<p>Final step! After success, leave the rest to Infiniflow AI.</p>
<Divider></Divider>
<section>
@@ -48,8 +48,6 @@ const TestingControl = ({ form, handleTesting }: IProps) => {
layout="vertical"
form={form}
initialValues={{
similarity_threshold: 0.2,
vector_similarity_weight: 0.3,
top_k: 1024,
}}
>
@@ -81,12 +79,12 @@ const TestingControl = ({ form, handleTesting }: IProps) => {
</Form>
</section>
<section>
<p className={styles.historyTitle}>
<div className={styles.historyTitle}>
<Space size={'middle'}>
<HistoryOutlined className={styles.historyIcon} />
<b>Test history</b>
</Space>
</p>
</div>
<Space
direction="vertical"
size={'middle'}