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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 629 additions and 473 deletions

View File

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

View File

@ -0,0 +1,24 @@
import { useCallback } from 'react';
import { useDispatch } from 'umi';
import { useGetKnowledgeSearchParams } from './routeHook';
interface PayloadType {
doc_id: string;
keywords?: string;
}
export const useFetchChunkList = () => {
const dispatch = useDispatch();
const { documentId } = useGetKnowledgeSearchParams();
const fetchChunkList = useCallback(() => {
dispatch({
type: 'chunkModel/chunk_list',
payload: {
doc_id: documentId,
},
});
}, [dispatch, documentId]);
return fetchChunkList;
};

View File

@ -1,8 +1,9 @@
import showDeleteConfirm from '@/components/deleting-confirm'; import showDeleteConfirm from '@/components/deleting-confirm';
import { KnowledgeSearchParams } from '@/constants/knowledge';
import { IKnowledge } from '@/interfaces/database/knowledge'; import { IKnowledge } from '@/interfaces/database/knowledge';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSearchParams, useSelector } from 'umi'; import { useDispatch, useSearchParams, useSelector } from 'umi';
import { useGetKnowledgeSearchParams } from './routeHook';
import { useOneNamespaceEffectsLoading } from './storeHooks';
export const useKnowledgeBaseId = (): string => { export const useKnowledgeBaseId = (): string => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -11,17 +12,6 @@ export const useKnowledgeBaseId = (): string => {
return knowledgeBaseId || ''; return knowledgeBaseId || '';
}; };
export const useGetKnowledgeSearchParams = () => {
const [currentQueryParameters] = useSearchParams();
return {
documentId:
currentQueryParameters.get(KnowledgeSearchParams.DocumentId) || '',
knowledgeId:
currentQueryParameters.get(KnowledgeSearchParams.KnowledgeId) || '',
};
};
export const useDeleteDocumentById = (): { export const useDeleteDocumentById = (): {
removeDocument: (documentId: string) => Promise<number>; removeDocument: (documentId: string) => Promise<number>;
} => { } => {
@ -135,8 +125,9 @@ export const useFetchKnowledgeBaseConfiguration = () => {
export const useFetchKnowledgeList = ( export const useFetchKnowledgeList = (
shouldFilterListWithoutDocument: boolean = false, shouldFilterListWithoutDocument: boolean = false,
): IKnowledge[] => { ): { list: IKnowledge[]; loading: boolean } => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const loading = useOneNamespaceEffectsLoading('knowledgeModel', ['getList']);
const knowledgeModel = useSelector((state: any) => state.knowledgeModel); const knowledgeModel = useSelector((state: any) => state.knowledgeModel);
const { data = [] } = knowledgeModel; const { data = [] } = knowledgeModel;
@ -156,7 +147,7 @@ export const useFetchKnowledgeList = (
fetchList(); fetchList();
}, [fetchList]); }, [fetchList]);
return list; return { list, loading };
}; };
export const useSelectFileThumbnails = () => { export const useSelectFileThumbnails = () => {
@ -189,3 +180,29 @@ export const useFetchFileThumbnails = (docIds?: Array<string>) => {
return { fileThumbnails, fetchFileThumbnails }; return { fileThumbnails, fetchFileThumbnails };
}; };
//#region knowledge configuration
export const useUpdateKnowledge = () => {
const dispatch = useDispatch();
const saveKnowledgeConfiguration = useCallback(
(payload: any) => {
dispatch({
type: 'kSModel/updateKb',
payload,
});
},
[dispatch],
);
return saveKnowledgeConfiguration;
};
export const useSelectKnowledgeDetails = () => {
const knowledgeDetails: IKnowledge = useSelector(
(state: any) => state.kSModel.knowledgeDetails,
);
return knowledgeDetails;
};
//#endregion

View File

@ -8,8 +8,8 @@ import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'umi'; import { useDispatch, useSelector } from 'umi';
export const useFetchLlmList = ( export const useFetchLlmList = (
isOnMountFetching: boolean = true,
modelType?: LlmModelType, modelType?: LlmModelType,
isOnMountFetching: boolean = true,
) => { ) => {
const dispatch = useDispatch(); const dispatch = useDispatch();

View File

@ -1,4 +1,5 @@
import { useLocation } from 'umi'; import { KnowledgeSearchParams } from '@/constants/knowledge';
import { useLocation, useSearchParams } from 'umi';
export enum SegmentIndex { export enum SegmentIndex {
Second = '2', Second = '2',
@ -19,3 +20,14 @@ export const useSecondPathName = () => {
export const useThirdPathName = () => { export const useThirdPathName = () => {
return useSegmentedPathName(SegmentIndex.Third); return useSegmentedPathName(SegmentIndex.Third);
}; };
export const useGetKnowledgeSearchParams = () => {
const [currentQueryParameters] = useSearchParams();
return {
documentId:
currentQueryParameters.get(KnowledgeSearchParams.DocumentId) || '',
knowledgeId:
currentQueryParameters.get(KnowledgeSearchParams.KnowledgeId) || '',
};
};

View File

@ -1,4 +1,4 @@
import { useGetKnowledgeSearchParams } from '@/hooks/knowledgeHook'; import { useGetKnowledgeSearchParams } from '@/hooks/routeHook';
import { api_host } from '@/utils/api'; import { api_host } from '@/utils/api';
import { useSize } from 'ahooks'; import { useSize } from 'ahooks';
import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types'; 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 { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge';
import { buildChunkHighlights } from '@/utils/documentUtils'; import { buildChunkHighlights } from '@/utils/documentUtils';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
@ -46,3 +47,11 @@ export const useGetChunkHighlights = (
return highlights; 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 { useDeleteChunkByIds } from '@/hooks/knowledgeHook';
import { getOneNamespaceEffectsLoading } from '@/utils/storeUtil';
import type { PaginationProps } from 'antd'; import type { PaginationProps } from 'antd';
import { Divider, Flex, Pagination, Space, Spin, message } from 'antd'; import { Divider, Flex, Pagination, Space, Spin, message } from 'antd';
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSearchParams, useSelector } from 'umi'; import { useDispatch, useSearchParams, useSelector } from 'umi';
import ChunkCard from './components/chunk-card'; import ChunkCard from './components/chunk-card';
import CreatingModal from './components/chunk-creating-modal'; import CreatingModal from './components/chunk-creating-modal';
import ChunkToolBar from './components/chunk-toolbar'; import ChunkToolBar from './components/chunk-toolbar';
// import DocumentPreview from './components/document-preview';
import classNames from 'classnames';
import DocumentPreview from './components/document-preview/preview'; import DocumentPreview from './components/document-preview/preview';
import { useHandleChunkCardClick, useSelectDocumentInfo } from './hooks'; import {
useHandleChunkCardClick,
useSelectChunkListLoading,
useSelectDocumentInfo,
} from './hooks';
import { ChunkModelState } from './model'; import { ChunkModelState } from './model';
import styles from './index.less'; import styles from './index.less';
interface PayloadType {
doc_id: string;
keywords?: string;
}
const Chunk = () => { const Chunk = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -27,12 +26,7 @@ const Chunk = () => {
const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]); const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { data = [], total, pagination } = chunkModel; const { data = [], total, pagination } = chunkModel;
const effects = useSelector((state: any) => state.loading.effects); const loading = useSelectChunkListLoading();
const loading = getOneNamespaceEffectsLoading('chunkModel', effects, [
'create_hunk',
'chunk_list',
'switch_chunk',
]);
const documentId: string = searchParams.get('doc_id') || ''; const documentId: string = searchParams.get('doc_id') || '';
const [chunkId, setChunkId] = useState<string | undefined>(); const [chunkId, setChunkId] = useState<string | undefined>();
const { removeChunk } = useDeleteChunkByIds(); const { removeChunk } = useDeleteChunkByIds();
@ -40,18 +34,7 @@ const Chunk = () => {
const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick(); const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick();
const isPdf = documentInfo.type === 'pdf'; const isPdf = documentInfo.type === 'pdf';
const getChunkList = useCallback(() => { const getChunkList = useFetchChunkList();
const payload: PayloadType = {
doc_id: documentId,
};
dispatch({
type: 'chunkModel/chunk_list',
payload: {
...payload,
},
});
}, [dispatch, documentId]);
const handleEditChunk = useCallback( const handleEditChunk = useCallback(
(chunk_id?: string) => { (chunk_id?: string) => {
@ -169,8 +152,8 @@ const Chunk = () => {
vertical vertical
className={isPdf ? styles.pagePdfWrapper : styles.pageWrapper} 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 <Space
direction="vertical" direction="vertical"
size={'middle'} size={'middle'}
@ -193,8 +176,8 @@ const Chunk = () => {
></ChunkCard> ></ChunkCard>
))} ))}
</Space> </Space>
</Spin> </div>
</div> </Spin>
<div className={styles.pageFooter}> <div className={styles.pageFooter}>
<Pagination <Pagination
responsive responsive

View File

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

View File

@ -1,19 +1,4 @@
import { import { normFile } from '@/utils/fileUtil';
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 { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { import {
Button, Button,
@ -26,14 +11,14 @@ import {
Select, Select,
Slider, Slider,
Space, Space,
Spin,
Typography, Typography,
Upload, Upload,
UploadFile,
} from 'antd'; } from 'antd';
import pick from 'lodash/pick'; import {
import { useEffect } from 'react'; useFetchKnowledgeConfigurationOnMount,
import { useDispatch, useSelector } from 'umi'; useSubmitKnowledgeConfiguration,
import { LlmModelType } from '../../constant'; } from './hooks';
import styles from './index.less'; import styles from './index.less';
@ -41,205 +26,165 @@ const { Title } = Typography;
const { Option } = Select; const { Option } = Select;
const Configuration = () => { const Configuration = () => {
const [form] = Form.useForm(); const { submitKnowledgeConfiguration, submitLoading } =
const dispatch = useDispatch(); useSubmitKnowledgeConfiguration();
const knowledgeBaseId = useKnowledgeBaseId(); const { form, parserList, embeddingModelOptions, loading } =
const loading = useOneNamespaceEffectsLoading('kSModel', ['updateKb']); useFetchKnowledgeConfigurationOnMount();
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 onFinishFailed = (errorInfo: any) => { const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo); 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 ( return (
<div className={styles.configurationWrapper}> <div className={styles.configurationWrapper}>
<Title level={5}>Configuration</Title> <Title level={5}>Configuration</Title>
<p>Update your knowledge base details especially parsing method here.</p> <p>Update your knowledge base details especially parsing method here.</p>
<Divider></Divider> <Divider></Divider>
<Form <Spin spinning={loading}>
form={form} <Form
name="validateOnly" form={form}
layout="vertical" name="validateOnly"
autoComplete="off" layout="vertical"
onFinish={onFinish} autoComplete="off"
onFinishFailed={onFinishFailed} onFinish={submitKnowledgeConfiguration}
> onFinishFailed={onFinishFailed}
<Form.Item
name="name"
label="Knowledge base name"
rules={[{ required: true }]}
> >
<Input /> <Form.Item
</Form.Item> name="name"
<Form.Item label="Knowledge base name"
name="avatar" rules={[{ required: true }]}
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"> <Input />
<PlusOutlined /> </Form.Item>
<div style={{ marginTop: 8 }}>Upload</div> <Form.Item
</button> name="avatar"
</Upload> label="Knowledge base photo"
</Form.Item> valuePropName="fileList"
<Form.Item name="description" label="Description"> getValueFromEvent={normFile}
<Input /> >
</Form.Item> <Upload
<Form.Item listType="picture-card"
label="Language" maxCount={1}
name="language" beforeUpload={() => false}
initialValue={'Chinese'} showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
rules={[{ required: true, message: 'Please input your language!' }]} >
> <button style={{ border: 0, background: 'none' }} type="button">
<Select placeholder="select your language"> <PlusOutlined />
<Option value="English">English</Option> <div style={{ marginTop: 8 }}>Upload</div>
<Option value="Chinese">Chinese</Option> </button>
</Select> </Upload>
</Form.Item> </Form.Item>
<Form.Item <Form.Item name="description" label="Description">
name="permission" <Input />
label="Permissions" </Form.Item>
rules={[{ required: true }]} <Form.Item
> label="Language"
<Radio.Group> name="language"
<Radio value="me">Only me</Radio> initialValue={'Chinese'}
<Radio value="team">Team</Radio> rules={[{ required: true, message: 'Please input your language!' }]}
</Radio.Group> >
</Form.Item> <Select placeholder="select your language">
<Form.Item <Option value="English">English</Option>
name="embd_id" <Option value="Chinese">Chinese</Option>
label="Embedding Model" </Select>
rules={[{ required: true }]} </Form.Item>
tooltip="xx" <Form.Item
> name="permission"
<Select label="Permissions"
placeholder="Please select a country" rules={[{ required: true }]}
options={embeddingModelOptions} >
></Select> <Radio.Group>
</Form.Item> <Radio value="me">Only me</Radio>
<Form.Item <Radio value="team">Team</Radio>
name="parser_id" </Radio.Group>
label="Knowledge base category" </Form.Item>
tooltip="xx" <Form.Item
rules={[{ required: true }]} name="embd_id"
> label="Embedding Model"
<Select placeholder="Please select a country"> rules={[{ required: true }]}
{parserList.map((x) => ( tooltip="xx"
<Option value={x.value} key={x.value}> >
{x.label} <Select
</Option> placeholder="Please select a country"
))} options={embeddingModelOptions}
</Select> ></Select>
</Form.Item> </Form.Item>
<Form.Item noStyle dependencies={['parser_id']}> <Form.Item
{({ getFieldValue }) => { name="parser_id"
const parserId = getFieldValue('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') { if (parserId === 'naive') {
return ( return (
<Form.Item label="Chunk token number" tooltip="xxx"> <Form.Item label="Chunk token number" tooltip="xxx">
<Flex gap={20} align="center"> <Flex gap={20} align="center">
<Flex flex={1}> <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 <Form.Item
name={['parser_config', 'chunk_token_num']} name={['parser_config', 'chunk_token_num']}
noStyle noStyle
initialValue={128}
rules={[ 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> </Form.Item>
</Flex> </Flex>
<Form.Item </Form.Item>
name={['parser_config', 'chunk_token_num']} );
noStyle }
initialValue={128} return null;
rules={[ }}
{ required: true, message: 'Street is required' }, </Form.Item>
]} <Form.Item>
> <div className={styles.buttonWrapper}>
<InputNumber <Space>
className={styles.sliderInputNumber} <Button htmlType="reset" size={'middle'}>
max={2048} Cancel
min={0} </Button>
/> <Button
</Form.Item> htmlType="submit"
</Flex> type="primary"
</Form.Item> size={'middle'}
); loading={submitLoading}
} >
return null; Save
}} </Button>
</Form.Item> </Space>
<Form.Item> </div>
<div className={styles.buttonWrapper}> </Form.Item>
<Space> </Form>
<Button htmlType="reset" size={'middle'}> </Spin>
Cancel
</Button>
<Button
htmlType="submit"
type="primary"
size={'middle'}
loading={loading}
>
Save
</Button>
</Space>
</div>
</Form.Item>
</Form>
</div> </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 { data } = yield call(kbService.createKb, payload);
const { retcode } = data; const { retcode } = data;
if (retcode === 0) { if (retcode === 0) {
message.success('Created successfully!'); message.success('Created!');
} }
return data; return data;
}, },
@ -43,7 +43,7 @@ const model: DvaModel<KSModelState> = {
const { retcode } = data; const { retcode } = data;
if (retcode === 0) { if (retcode === 0) {
yield put({ type: 'getKbDetail', payload: { kb_id: payload.kb_id } }); yield put({ type: 'getKbDetail', payload: { kb_id: payload.kb_id } });
message.success('Updated successfully!'); message.success('Updated!');
} }
}, },
*getKbDetail({ payload = {} }, { call, put }) { *getKbDetail({ payload = {} }, { call, put }) {

View File

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

View File

@ -1,14 +1,13 @@
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import { PlusOutlined } from '@ant-design/icons';
import { Form, Input, Select, Upload } from 'antd'; import { Form, Input, Select, Upload } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { ISegmentedContentProps } from '../interface'; import { ISegmentedContentProps } from '../interface';
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import { PlusOutlined } from '@ant-design/icons';
import styles from './index.less'; import styles from './index.less';
const AssistantSetting = ({ show }: ISegmentedContentProps) => { const AssistantSetting = ({ show }: ISegmentedContentProps) => {
const knowledgeList = useFetchKnowledgeList(true); const { list: knowledgeList } = useFetchKnowledgeList(true);
const knowledgeOptions = knowledgeList.map((x) => ({ const knowledgeOptions = knowledgeList.map((x) => ({
label: x.name, label: x.name,
value: x.id, value: x.id,

View File

@ -30,6 +30,7 @@ import {
useClickDrawer, useClickDrawer,
useFetchConversationOnMount, useFetchConversationOnMount,
useGetFileIcon, useGetFileIcon,
useGetSendButtonDisabled,
useSendMessage, useSendMessage,
} from '../hooks'; } from '../hooks';
@ -248,11 +249,15 @@ const ChatContainer = () => {
addNewestConversation, addNewestConversation,
removeLatestMessage, removeLatestMessage,
} = useFetchConversationOnMount(); } = useFetchConversationOnMount();
const { handleInputChange, handlePressEnter, value, loading } = const {
useSendMessage(conversation, addNewestConversation, removeLatestMessage); handleInputChange,
handlePressEnter,
value,
loading: sendLoading,
} = useSendMessage(conversation, addNewestConversation, removeLatestMessage);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer(); useClickDrawer();
const disabled = useGetSendButtonDisabled();
useGetFileIcon(); useGetFileIcon();
return ( return (
@ -284,8 +289,14 @@ const ChatContainer = () => {
size="large" size="large"
placeholder="Message Resume Assistant..." placeholder="Message Resume Assistant..."
value={value} value={value}
disabled={disabled}
suffix={ suffix={
<Button type="primary" onClick={handlePressEnter} loading={loading}> <Button
type="primary"
onClick={handlePressEnter}
loading={sendLoading}
disabled={disabled}
>
Send Send
</Button> </Button>
} }

View File

@ -767,4 +767,16 @@ export const useClickDrawer = () => {
}; };
}; };
export const useSelectDialogListLoading = () => {
return useOneNamespaceEffectsLoading('chatModel', ['listDialog']);
};
export const useSelectConversationListLoading = () => {
return useOneNamespaceEffectsLoading('chatModel', ['listConversation']);
};
export const useGetSendButtonDisabled = () => {
const { dialogId, conversationId } = useGetChatSearchParams();
return dialogId === '' && conversationId === '';
};
//#endregion //#endregion

View File

@ -41,6 +41,14 @@
overflow: auto; overflow: auto;
} }
.chatSpin {
:global(.ant-spin-container) {
display: flex;
flex-direction: column;
gap: 10px;
}
}
.chatTitleCard { .chatTitleCard {
:global(.ant-card-body) { :global(.ant-card-body) {
padding: 8px; padding: 8px;

View File

@ -1,5 +1,4 @@
import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg'; import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg';
import { useSetModalState } from '@/hooks/commonHooks';
import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons'; import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons';
import { import {
Avatar, Avatar,
@ -10,6 +9,7 @@ import {
Flex, Flex,
MenuProps, MenuProps,
Space, Space,
Spin,
Tag, Tag,
} from 'antd'; } from 'antd';
import { MenuItemProps } from 'antd/lib/menu/MenuItem'; import { MenuItemProps } from 'antd/lib/menu/MenuItem';
@ -29,8 +29,9 @@ import {
useRemoveDialog, useRemoveDialog,
useRenameConversation, useRenameConversation,
useSelectConversationList, useSelectConversationList,
useSelectConversationListLoading,
useSelectDialogListLoading,
useSelectFirstDialogOnMount, useSelectFirstDialogOnMount,
useSetCurrentDialog,
} from './hooks'; } from './hooks';
import RenameModal from '@/components/rename-modal'; import RenameModal from '@/components/rename-modal';
@ -38,8 +39,6 @@ import styles from './index.less';
const Chat = () => { const Chat = () => {
const dialogList = useSelectFirstDialogOnMount(); const dialogList = useSelectFirstDialogOnMount();
const { visible, hideModal, showModal } = useSetModalState();
const { setCurrentDialog, currentDialog } = useSetCurrentDialog();
const { onRemoveDialog } = useRemoveDialog(); const { onRemoveDialog } = useRemoveDialog();
const { onRemoveConversation } = useRemoveConversation(); const { onRemoveConversation } = useRemoveConversation();
const { handleClickDialog } = useClickDialogCard(); const { handleClickDialog } = useClickDialogCard();
@ -70,6 +69,8 @@ const Chat = () => {
hideDialogEditModal, hideDialogEditModal,
showDialogEditModal, showDialogEditModal,
} = useEditDialog(); } = useEditDialog();
const dialogLoading = useSelectDialogListLoading();
const conversationLoading = useSelectConversationListLoading();
useFetchDialogOnMount(dialogId, true); useFetchDialogOnMount(dialogId, true);
@ -204,35 +205,39 @@ const Chat = () => {
</Button> </Button>
<Divider></Divider> <Divider></Divider>
<Flex className={styles.chatAppContent} vertical gap={10}> <Flex className={styles.chatAppContent} vertical gap={10}>
{dialogList.map((x) => ( <Spin spinning={dialogLoading} wrapperClassName={styles.chatSpin}>
<Card {dialogList.map((x) => (
key={x.id} <Card
hoverable key={x.id}
className={classNames(styles.chatAppCard, { hoverable
[styles.chatAppCardSelected]: dialogId === x.id, className={classNames(styles.chatAppCard, {
})} [styles.chatAppCardSelected]: dialogId === x.id,
onMouseEnter={handleAppCardEnter(x.id)} })}
onMouseLeave={handleItemLeave} onMouseEnter={handleAppCardEnter(x.id)}
onClick={handleDialogCardClick(x.id)} onMouseLeave={handleItemLeave}
> onClick={handleDialogCardClick(x.id)}
<Flex justify="space-between" align="center"> >
<Space size={15}> <Flex justify="space-between" align="center">
<Avatar src={x.icon} shape={'square'} /> <Space size={15}>
<section> <Avatar src={x.icon} shape={'square'} />
<b>{x.name}</b> <section>
<div>{x.description}</div> <b>{x.name}</b>
</section> <div>{x.description}</div>
</Space> </section>
{activated === x.id && ( </Space>
<section> {activated === x.id && (
<Dropdown menu={{ items: buildAppItems(x.id) }}> <section>
<ChatAppCube className={styles.cubeIcon}></ChatAppCube> <Dropdown menu={{ items: buildAppItems(x.id) }}>
</Dropdown> <ChatAppCube
</section> className={styles.cubeIcon}
)} ></ChatAppCube>
</Flex> </Dropdown>
</Card> </section>
))} )}
</Flex>
</Card>
))}
</Spin>
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Flex>
@ -254,29 +259,38 @@ const Chat = () => {
</Flex> </Flex>
<Divider></Divider> <Divider></Divider>
<Flex vertical gap={10} className={styles.chatTitleContent}> <Flex vertical gap={10} className={styles.chatTitleContent}>
{conversationList.map((x) => ( <Spin
<Card spinning={conversationLoading}
key={x.id} wrapperClassName={styles.chatSpin}
hoverable >
onClick={handleConversationCardClick(x.id)} {conversationList.map((x) => (
onMouseEnter={handleConversationCardEnter(x.id)} <Card
onMouseLeave={handleConversationItemLeave} key={x.id}
className={classNames(styles.chatTitleCard, { hoverable
[styles.chatTitleCardSelected]: x.id === conversationId, onClick={handleConversationCardClick(x.id)}
})} onMouseEnter={handleConversationCardEnter(x.id)}
> onMouseLeave={handleConversationItemLeave}
<Flex justify="space-between" align="center"> className={classNames(styles.chatTitleCard, {
<div>{x.name}</div> [styles.chatTitleCardSelected]: x.id === conversationId,
{conversationActivated === x.id && x.id !== '' && ( })}
<section> >
<Dropdown menu={{ items: buildConversationItems(x.id) }}> <Flex justify="space-between" align="center">
<ChatAppCube className={styles.cubeIcon}></ChatAppCube> <div>{x.name}</div>
</Dropdown> {conversationActivated === x.id && x.id !== '' && (
</section> <section>
)} <Dropdown
</Flex> menu={{ items: buildConversationItems(x.id) }}
</Card> >
))} <ChatAppCube
className={styles.cubeIcon}
></ChatAppCube>
</Dropdown>
</section>
)}
</Flex>
</Card>
))}
</Spin>
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Flex>

View File

@ -1,16 +1,16 @@
import { ReactComponent as FilterIcon } from '@/assets/filter.svg'; import { ReactComponent as FilterIcon } from '@/assets/filter.svg';
import ModalManager from '@/components/modal-manager'; import ModalManager from '@/components/modal-manager';
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import { useSelectUserInfo } from '@/hooks/userSettingHook';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { Button, Empty, Flex, Space } from 'antd'; import { Button, Empty, Flex, Space, Spin } from 'antd';
import KnowledgeCard from './knowledge-card'; import KnowledgeCard from './knowledge-card';
import KnowledgeCreatingModal from './knowledge-creating-modal'; import KnowledgeCreatingModal from './knowledge-creating-modal';
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import { useSelectUserInfo } from '@/hooks/userSettingHook';
import styles from './index.less'; import styles from './index.less';
const Knowledge = () => { const Knowledge = () => {
const list = useFetchKnowledgeList(); const { list, loading } = useFetchKnowledgeList();
const userInfo = useSelectUserInfo(); const userInfo = useSelectUserInfo();
return ( return (
@ -50,15 +50,23 @@ const Knowledge = () => {
</ModalManager> </ModalManager>
</Space> </Space>
</div> </div>
<Flex gap={'large'} wrap="wrap" className={styles.knowledgeCardContainer}> <Spin spinning={loading}>
{list.length > 0 ? ( <Flex
list.map((item: any) => { gap={'large'}
return <KnowledgeCard item={item} key={item.name}></KnowledgeCard>; wrap="wrap"
}) className={styles.knowledgeCardContainer}
) : ( >
<Empty></Empty> {list.length > 0 ? (
)} list.map((item: any) => {
</Flex> return (
<KnowledgeCard item={item} key={item.name}></KnowledgeCard>
);
})
) : (
<Empty></Empty>
)}
</Flex>
</Spin>
</Flex> </Flex>
); );
}; };

View File

@ -19,5 +19,8 @@ export const useValidateSubmittable = () => {
return { submittable, form }; return { submittable, form };
}; };
export const useGetUserInfoLoading = () => export const useSelectSubmitUserInfoLoading = () =>
useOneNamespaceEffectsLoading('settingModel', ['setting']); useOneNamespaceEffectsLoading('settingModel', ['setting']);
export const useSelectUserInfoLoading = () =>
useOneNamespaceEffectsLoading('settingModel', ['getUserInfo']);

View File

@ -113,3 +113,12 @@ export const useFetchSystemModelSettingOnMount = (visible: boolean) => {
return { systemSetting, allOptions }; return { systemSetting, allOptions };
}; };
export const useSelectModelProvidersLoading = () => {
const loading = useOneNamespaceEffectsLoading('settingModel', [
'my_llm',
'factories_list',
]);
return loading;
};

View File

@ -23,12 +23,17 @@ import {
List, List,
Row, Row,
Space, Space,
Spin,
Typography, Typography,
} from 'antd'; } from 'antd';
import { useCallback } from 'react'; import { useCallback } from 'react';
import SettingTitle from '../components/setting-title'; import SettingTitle from '../components/setting-title';
import ApiKeyModal from './api-key-modal'; import ApiKeyModal from './api-key-modal';
import { useSubmitApiKey, useSubmitSystemModelSetting } from './hooks'; import {
useSelectModelProvidersLoading,
useSubmitApiKey,
useSubmitSystemModelSetting,
} from './hooks';
import SystemModelSettingModal from './system-model-setting-modal'; import SystemModelSettingModal from './system-model-setting-modal';
import styles from './index.less'; import styles from './index.less';
@ -111,6 +116,7 @@ const ModelCard = ({ item, clickApiKey }: IModelCardProps) => {
const UserSettingModel = () => { const UserSettingModel = () => {
const factoryList = useFetchLlmFactoryListOnMount(); const factoryList = useFetchLlmFactoryListOnMount();
const llmList = useFetchMyLlmListOnMount(); const llmList = useFetchMyLlmListOnMount();
const loading = useSelectModelProvidersLoading();
const { const {
saveApiKeyLoading, saveApiKeyLoading,
initialApiKey, initialApiKey,
@ -191,16 +197,18 @@ const UserSettingModel = () => {
return ( return (
<> <>
<section className={styles.modelWrapper}> <Spin spinning={loading}>
<SettingTitle <section className={styles.modelWrapper}>
title="Model Setting" <SettingTitle
description="Manage your account settings and preferences here." title="Model Setting"
showRightButton description="Manage your account settings and preferences here."
clickButton={showSystemSettingModal} showRightButton
></SettingTitle> clickButton={showSystemSettingModal}
<Divider></Divider> ></SettingTitle>
<Collapse defaultActiveKey={['1']} ghost items={items} /> <Divider></Divider>
</section> <Collapse defaultActiveKey={['1']} ghost items={items} />
</section>
</Spin>
<ApiKeyModal <ApiKeyModal
visible={apiKeyVisible} visible={apiKeyVisible}
hideModal={hideApiKeyModal} hideModal={hideApiKeyModal}

View File

@ -1,4 +1,8 @@
import { useSaveSetting, useSelectUserInfo } from '@/hooks/userSettingHook'; import {
useFetchUserInfo,
useSaveSetting,
useSelectUserInfo,
} from '@/hooks/userSettingHook';
import { import {
getBase64FromUploadFileList, getBase64FromUploadFileList,
getUploadFileListFromBase64, getUploadFileListFromBase64,
@ -12,6 +16,7 @@ import {
Input, Input,
Select, Select,
Space, Space,
Spin,
Tooltip, Tooltip,
Upload, Upload,
UploadFile, UploadFile,
@ -19,7 +24,11 @@ import {
import { useEffect } from 'react'; import { useEffect } from 'react';
import SettingTitle from '../components/setting-title'; import SettingTitle from '../components/setting-title';
import { TimezoneList } from '../constants'; import { TimezoneList } from '../constants';
import { useGetUserInfoLoading, useValidateSubmittable } from '../hooks'; import {
useSelectSubmitUserInfoLoading,
useSelectUserInfoLoading,
useValidateSubmittable,
} from '../hooks';
import parentStyles from '../index.less'; import parentStyles from '../index.less';
import styles from './index.less'; import styles from './index.less';
@ -42,8 +51,10 @@ const tailLayout = {
const UserSettingProfile = () => { const UserSettingProfile = () => {
const userInfo = useSelectUserInfo(); const userInfo = useSelectUserInfo();
const saveSetting = useSaveSetting(); const saveSetting = useSaveSetting();
const loading = useGetUserInfoLoading(); const submitLoading = useSelectSubmitUserInfoLoading();
const { form, submittable } = useValidateSubmittable(); const { form, submittable } = useValidateSubmittable();
const loading = useSelectUserInfoLoading();
useFetchUserInfo();
const onFinish = async (values: any) => { const onFinish = async (values: any) => {
const avatar = await getBase64FromUploadFileList(values.avatar); const avatar = await getBase64FromUploadFileList(values.avatar);
@ -66,131 +77,133 @@ const UserSettingProfile = () => {
description="Update your photo and personal details here." description="Update your photo and personal details here."
></SettingTitle> ></SettingTitle>
<Divider /> <Divider />
<Form <Spin spinning={loading}>
colon={false} <Form
name="basic" colon={false}
labelAlign={'left'} name="basic"
labelCol={{ span: 8 }} labelAlign={'left'}
wrapperCol={{ span: 16 }} labelCol={{ span: 8 }}
style={{ width: '100%' }} wrapperCol={{ span: 16 }}
initialValues={{ remember: true }} style={{ width: '100%' }}
onFinish={onFinish} initialValues={{ remember: true }}
onFinishFailed={onFinishFailed} onFinish={onFinish}
form={form} onFinishFailed={onFinishFailed}
autoComplete="off" form={form}
> autoComplete="off"
<Form.Item<FieldType>
label="Username"
name="nickname"
rules={[
{
required: true,
message: 'Please input your username!',
whitespace: true,
},
]}
> >
<Input /> <Form.Item<FieldType>
</Form.Item> label="Username"
<Divider /> name="nickname"
<Form.Item<FieldType> rules={[
label={ {
<div> required: true,
<Space> message: 'Please input your username!',
Your photo whitespace: true,
<Tooltip title="prompt text"> },
<QuestionCircleOutlined /> ]}
</Tooltip>
</Space>
<div>This will be displayed on your profile.</div>
</div>
}
name="avatar"
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload
listType="picture-card"
maxCount={1}
accept="image/*"
beforeUpload={() => {
return false;
}}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
> >
<button style={{ border: 0, background: 'none' }} type="button"> <Input />
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</button>
</Upload>
</Form.Item>
<Divider />
<Form.Item<FieldType>
label="Color schema"
name="color_schema"
rules={[
{ required: true, message: 'Please select your color schema!' },
]}
>
<Select placeholder="select your color schema">
<Option value="Bright">Bright</Option>
<Option value="Dark">Dark</Option>
</Select>
</Form.Item>
<Divider />
<Form.Item<FieldType>
label="Language"
name="language"
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>
<Divider />
<Form.Item<FieldType>
label="Timezone"
name="timezone"
rules={[{ required: true, message: 'Please input your timezone!' }]}
>
<Select placeholder="select your timezone" showSearch>
{TimezoneList.map((x) => (
<Option value={x} key={x}>
{x}
</Option>
))}
</Select>
</Form.Item>
<Divider />
<Form.Item label="Email address">
<Form.Item<FieldType> name="email" noStyle>
<Input disabled />
</Form.Item> </Form.Item>
<p className={parentStyles.itemDescription}> <Divider />
Once registered, an account cannot be changed and can only be <Form.Item<FieldType>
cancelled. label={
</p> <div>
</Form.Item> <Space>
<Form.Item Your photo
{...tailLayout} <Tooltip title="prompt text">
shouldUpdate={(prevValues, curValues) => <QuestionCircleOutlined />
prevValues.additional !== curValues.additional </Tooltip>
} </Space>
> <div>This will be displayed on your profile.</div>
<Space> </div>
<Button htmlType="button">Cancel</Button> }
<Button name="avatar"
type="primary" valuePropName="fileList"
htmlType="submit" getValueFromEvent={normFile}
disabled={!submittable} >
loading={loading} <Upload
listType="picture-card"
maxCount={1}
accept="image/*"
beforeUpload={() => {
return false;
}}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
> >
Save <button style={{ border: 0, background: 'none' }} type="button">
</Button> <PlusOutlined />
</Space> <div style={{ marginTop: 8 }}>Upload</div>
</Form.Item> </button>
</Form> </Upload>
</Form.Item>
<Divider />
<Form.Item<FieldType>
label="Color schema"
name="color_schema"
rules={[
{ required: true, message: 'Please select your color schema!' },
]}
>
<Select placeholder="select your color schema">
<Option value="Bright">Bright</Option>
<Option value="Dark">Dark</Option>
</Select>
</Form.Item>
<Divider />
<Form.Item<FieldType>
label="Language"
name="language"
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>
<Divider />
<Form.Item<FieldType>
label="Timezone"
name="timezone"
rules={[{ required: true, message: 'Please input your timezone!' }]}
>
<Select placeholder="select your timezone" showSearch>
{TimezoneList.map((x) => (
<Option value={x} key={x}>
{x}
</Option>
))}
</Select>
</Form.Item>
<Divider />
<Form.Item label="Email address">
<Form.Item<FieldType> name="email" noStyle>
<Input disabled />
</Form.Item>
<p className={parentStyles.itemDescription}>
Once registered, an account cannot be changed and can only be
cancelled.
</p>
</Form.Item>
<Form.Item
{...tailLayout}
shouldUpdate={(prevValues, curValues) =>
prevValues.additional !== curValues.additional
}
>
<Space>
<Button htmlType="button">Cancel</Button>
<Button
type="primary"
htmlType="submit"
disabled={!submittable}
loading={submitLoading}
>
Save
</Button>
</Space>
</Form.Item>
</Form>
</Spin>
</section> </section>
); );
}; };

View File

@ -48,8 +48,14 @@ export const getUploadFileListFromBase64 = (avatar: string) => {
export const getBase64FromUploadFileList = async (fileList?: UploadFile[]) => { export const getBase64FromUploadFileList = async (fileList?: UploadFile[]) => {
if (Array.isArray(fileList) && fileList.length > 0) { if (Array.isArray(fileList) && fileList.length > 0) {
const base64 = await transformFile2Base64(fileList[0].originFileObj); const file = fileList[0];
return base64; const originFileObj = file.originFileObj;
if (originFileObj) {
const base64 = await transformFile2Base64(originFileObj);
return base64;
} else {
return file.thumbUrl;
}
// return fileList[0].thumbUrl; TODO: Even JPG files will be converted to base64 parameters in png format // return fileList[0].thumbUrl; TODO: Even JPG files will be converted to base64 parameters in png format
} }