feat: add loading to ChatContainer and set font family to inter and add tooltip to Form.Item and download documents on the document list page (#136)

* feat: download documents on the document list page

* feat: add tooltip to Form.Item

* feat: set font family to inter

* feat: add loading to ChatContainer
This commit is contained in:
balibabu 2024-03-20 18:20:42 +08:00 committed by GitHub
parent 6999598101
commit fce14ee187
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 434 additions and 59 deletions

View File

@ -12,11 +12,13 @@ export default defineConfig({
icons: {},
hash: true,
favicons: ['/logo.svg'],
clickToComponent: {},
history: {
type: 'browser',
},
plugins: ['@react-dev-inspector/umi4-plugin', '@umijs/plugins/dist/dva'],
dva: {},
lessLoader: {
modifyVars: {
hack: `true; @import "~@/less/index.less";`,

View File

@ -1,6 +1,16 @@
import { ConfigProvider } from 'antd';
import React, { ReactNode } from 'react';
import { Inspector } from 'react-dev-inspector';
export function rootContainer(container: ReactNode) {
return React.createElement(Inspector, null, container);
return React.createElement(
ConfigProvider,
{
theme: {
token: {
fontFamily: 'Inter',
},
},
},
container,
);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -15,7 +15,7 @@ const SimilaritySlider = ({ isTooltipShown = false }: IProps) => {
<Form.Item<FieldType>
label="Similarity threshold"
name={'similarity_threshold'}
tooltip={isTooltipShown && 'xxx'}
tooltip={isTooltipShown && 'coming soon'}
initialValue={0.2}
>
<Slider max={1} step={0.01} />
@ -24,7 +24,7 @@ const SimilaritySlider = ({ isTooltipShown = false }: IProps) => {
label="Vector similarity weight"
name={'vector_similarity_weight'}
initialValue={0.3}
tooltip={isTooltipShown && 'xxx'}
tooltip={isTooltipShown && 'coming soon'}
>
<Slider max={1} step={0.01} />
</Form.Item>

5
web/src/global.less Normal file
View File

@ -0,0 +1,5 @@
@import url(./inter.less);
body {
font-family: Inter;
}

View File

@ -1,7 +1,6 @@
import authorizationUtil from '@/utils/authorizationUtil';
import { message } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import { Nullable } from 'typings';
import { useNavigate, useSearchParams } from 'umi';
export const useLoginWithGithub = () => {

273
web/src/inter.less Normal file
View File

@ -0,0 +1,273 @@
/* Variable fonts usage:
:root { font-family: "Inter", sans-serif; }
@supports (font-variation-settings: normal) {
:root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; }
} */
@font-face {
font-family: InterVariable;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('@/assets/inter/InterVariable.woff2') format('woff2');
}
@font-face {
font-family: InterVariable;
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url('@/assets/inter/InterVariable-Italic.woff2') format('woff2');
}
/* static fonts */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url('@/assets/inter/Inter-Thin.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url('@/assets/inter/Inter-ThinItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url('@/assets/inter/Inter-ExtraLight.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url('@/assets/inter/Inter-ExtraLightItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('@/assets/inter/Inter-Light.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('@/assets/inter/Inter-LightItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('@/assets/inter/Inter-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('@/assets/inter/Inter-Italic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('@/assets/inter/Inter-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url('@/assets/inter/Inter-MediumItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('@/assets/inter/Inter-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('@/assets/inter/Inter-SemiBoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('@/assets/inter/Inter-Bold.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('@/assets/inter/Inter-BoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('@/assets/inter/Inter-ExtraBold.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('@/assets/inter/Inter-ExtraBoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url('@/assets/inter/Inter-Black.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url('@/assets/inter/Inter-BlackItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url('@/assets/inter/InterDisplay-Thin.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url('@/assets/inter/InterDisplay-ThinItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url('@/assets/inter/InterDisplay-ExtraLight.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url('@/assets/inter/InterDisplay-ExtraLightItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('@/assets/inter/InterDisplay-Light.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('@/assets/inter/InterDisplay-LightItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('@/assets/inter/InterDisplay-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('@/assets/inter/InterDisplay-Italic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('@/assets/inter/InterDisplay-Medium.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url('@/assets/inter/InterDisplay-MediumItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('@/assets/inter/InterDisplay-SemiBold.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('@/assets/inter/InterDisplay-SemiBoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('@/assets/inter/InterDisplay-Bold.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('@/assets/inter/InterDisplay-BoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('@/assets/inter/InterDisplay-ExtraBold.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url('@/assets/inter/InterDisplay-ExtraBoldItalic.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url('@/assets/inter/InterDisplay-Black.woff2') format('woff2');
}
@font-face {
font-family: 'InterDisplay';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url('@/assets/inter/InterDisplay-BlackItalic.woff2') format('woff2');
}

View File

@ -36,7 +36,6 @@
border: 0 !important;
background-color: rgba(249, 249, 249, 1);
font-weight: @fontWeight700;
font-family: 'Nunito Sans';
color: rgba(29, 25, 41, 1);
&::before {
display: none !important;

View File

@ -15,5 +15,3 @@
@fontSize14: 14px;
@fontSize16: 16px;
@fontSize18: 18px;
@fontFamilyNunitoSans: 'Nunito Sans';

View File

@ -4,7 +4,6 @@ import kbService, { getDocumentFile } from '@/services/kbService';
import { message } from 'antd';
import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { Nullable } from 'typings';
import { DvaModel } from 'umi';
export interface KFModelState extends BaseState {

View File

@ -1,10 +1,17 @@
import showDeleteConfirm from '@/components/deleting-confirm';
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { DeleteOutlined, EditOutlined, ToolOutlined } from '@ant-design/icons';
import {
DeleteOutlined,
DownloadOutlined,
EditOutlined,
ToolOutlined,
} from '@ant-design/icons';
import { Button, Dropdown, MenuProps, Space, Tooltip } from 'antd';
import { useDispatch } from 'umi';
import { isParserRunning } from '../utils';
import { api_host } from '@/utils/api';
import { downloadFile } from '@/utils/fileUtil';
import styles from './index.less';
interface IProps {
@ -38,6 +45,13 @@ const ParsingActionCell = ({
}
};
const onDownloadDocument = () => {
downloadFile({
url: `${api_host}/document/get/${documentId}`,
filename: record.name,
});
};
const setCurrentRecord = () => {
dispatch({
type: 'kFModel/setCurrentRecord',
@ -110,6 +124,14 @@ const ParsingActionCell = ({
>
<DeleteOutlined size={20} />
</Button>
<Button
type="text"
disabled={isRunning}
onClick={onDownloadDocument}
className={styles.iconButton}
>
<DownloadOutlined size={20} />
</Button>
</Space>
);
};

View File

@ -91,6 +91,7 @@ const Configuration = () => {
<Form.Item
name="permission"
label="Permissions"
tooltip="coming soon"
rules={[{ required: true }]}
>
<Radio.Group>

View File

@ -8,7 +8,6 @@
.knowledgeLogo {
}
.knowledgeTitle {
font-family: 'Nunito Sans';
font-size: 16px;
line-height: 24px;
font-weight: @fontWeight700;
@ -16,7 +15,6 @@
margin-bottom: 6px;
}
.knowledgeDescription {
font-family: 'Nunito Sans';
font-size: 12px;
font-weight: @fontWeight600;
color: @gray8;
@ -55,7 +53,6 @@
.menuText {
color: @gray3;
font-family: @fontFamilyNunitoSans;
font-size: @fontSize14;
font-weight: @fontWeight700;
}

View File

@ -51,8 +51,12 @@ const TestingControl = ({ form, handleTesting }: IProps) => {
top_k: 1024,
}}
>
<SimilaritySlider></SimilaritySlider>
<Form.Item<FieldType> label="Top k" name={'top_k'}>
<SimilaritySlider isTooltipShown></SimilaritySlider>
<Form.Item<FieldType>
label="Top k"
name={'top_k'}
tooltip="coming soon"
>
<Slider marks={{ 0: 0, 2048: 2048 }} max={2048} />
</Form.Item>
<Card size="small" title="Test text">

View File

@ -50,7 +50,12 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => {
</button>
</Upload>
</Form.Item>
<Form.Item name={'language'} label="Language" initialValue={'Chinese'}>
<Form.Item
name={'language'}
label="Language"
initialValue={'Chinese'}
tooltip="coming soon"
>
<Select
options={[
{ value: 'Chinese', label: 'Chinese' },
@ -61,12 +66,14 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => {
<Form.Item
name={['prompt_config', 'empty_response']}
label="Empty response"
tooltip="coming soon"
>
<Input placeholder="" />
</Form.Item>
<Form.Item
name={['prompt_config', 'prologue']}
label="Set an opener"
tooltip="coming soon"
initialValue={"Hi! I'm your assistant, what can I do for you?"}
>
<Input.TextArea autoSize={{ minRows: 5 }} />
@ -74,6 +81,7 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => {
<Form.Item
label="Select one context"
name="kb_ids"
tooltip="coming soon"
rules={[
{
required: true,

View File

@ -5,11 +5,18 @@
.variableContainer {
padding-bottom: 20px;
.variableAlign {
text-align: right;
text-align: end;
}
.variableLabel {
margin-right: 16px;
margin-right: 14px;
}
.variableIcon {
margin-inline-start: 4px;
color: rgba(0, 0, 0, 0.45);
cursor: help;
writing-mode: horizontal-tb;
}
.variableTable {

View File

@ -46,6 +46,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => {
<Form.Item
label="Model"
name="llm_id"
tooltip="coming soon"
rules={[{ required: true, message: 'Please select!' }]}
>
<Select options={modelOptions} showSearch />
@ -54,6 +55,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => {
<Form.Item
label="Parameters"
name="parameters"
tooltip="coming soon"
initialValue={ModelVariableType.Precise}
// rules={[{ required: true, message: 'Please input!' }]}
>

View File

@ -1,5 +1,5 @@
import SimilaritySlider from '@/components/similarity-slider';
import { DeleteOutlined } from '@ant-design/icons';
import { DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import {
Button,
Col,
@ -11,6 +11,7 @@ import {
Switch,
Table,
TableProps,
Tooltip,
} from 'antd';
import classNames from 'classnames';
import {
@ -153,6 +154,7 @@ const PromptEngine = (
<Form.Item
label="System"
rules={[{ required: true, message: 'Please input!' }]}
tooltip="coming soon"
name={['prompt_config', 'system']}
initialValue={`你是一个智能助手,请总结知识库的内容来回答问题,请列举知识库中的数据详细回答。当所有知识库内容都与问题无关时,你的回答必须包括“知识库中未找到您要的答案!”这句话。回答需要考虑聊天历史。
@ -173,10 +175,15 @@ const PromptEngine = (
</Form.Item>
<section className={classNames(styles.variableContainer)}>
<Row align={'middle'} justify="end">
<Col span={6} className={styles.variableAlign}>
<label className={styles.variableLabel}>Variables</label>
<Col span={7} className={styles.variableAlign}>
<label className={styles.variableLabel}>
Variables
<Tooltip title="coming soon">
<QuestionCircleOutlined className={styles.variableIcon} />
</Tooltip>
</label>
</Col>
<Col span={18} className={styles.variableAlign}>
<Col span={17} className={styles.variableAlign}>
<Button size="small" onClick={handleAdd}>
Add
</Button>
@ -184,8 +191,8 @@ const PromptEngine = (
</Row>
{dataSource.length > 0 && (
<Row>
<Col span={6}></Col>
<Col span={18}>
<Col span={7}> </Col>
<Col span={17}>
<Table
dataSource={dataSource}
columns={columns}

View File

@ -18,6 +18,7 @@ import {
Popover,
Skeleton,
Space,
Spin,
} from 'antd';
import classNames from 'classnames';
import { useCallback, useMemo } from 'react';
@ -31,6 +32,7 @@ import {
useFetchConversationOnMount,
useGetFileIcon,
useGetSendButtonDisabled,
useSelectConversationLoading,
useSendMessage,
} from '../hooks';
@ -259,29 +261,32 @@ const ChatContainer = () => {
useClickDrawer();
const disabled = useGetSendButtonDisabled();
useGetFileIcon();
const loading = useSelectConversationLoading();
return (
<>
<Flex flex={1} className={styles.chatContainer} vertical>
<Flex flex={1} vertical className={styles.messageContainer}>
<div>
{conversation?.message?.map((message) => {
const assistantMessages = conversation?.message
?.filter((x) => x.role === MessageType.Assistant)
.slice(1);
const referenceIndex = assistantMessages.findIndex(
(x) => x.id === message.id,
);
const reference = conversation.reference[referenceIndex];
return (
<MessageItem
key={message.id}
item={message}
reference={reference}
clickDocumentButton={clickDocumentButton}
></MessageItem>
);
})}
<Spin spinning={loading}>
{conversation?.message?.map((message) => {
const assistantMessages = conversation?.message
?.filter((x) => x.role === MessageType.Assistant)
.slice(1);
const referenceIndex = assistantMessages.findIndex(
(x) => x.id === message.id,
);
const reference = conversation.reference[referenceIndex];
return (
<MessageItem
key={message.id}
item={message}
reference={reference}
clickDocumentButton={clickDocumentButton}
></MessageItem>
);
})}
</Spin>
</div>
<div ref={ref} />
</Flex>

View File

@ -773,6 +773,9 @@ export const useSelectDialogListLoading = () => {
export const useSelectConversationListLoading = () => {
return useOneNamespaceEffectsLoading('chatModel', ['listConversation']);
};
export const useSelectConversationLoading = () => {
return useOneNamespaceEffectsLoading('chatModel', ['getConversation']);
};
export const useGetSendButtonDisabled = () => {
const { dialogId, conversationId } = useGetChatSearchParams();

View File

@ -40,6 +40,7 @@
line-height: 32px;
font-weight: 600;
color: rgba(0, 0, 0, 0.88);
word-break: break-all;
}
.description {
font-size: 12px;

View File

@ -4,7 +4,6 @@
// width: 100%;
display: flex;
.loginLeft {
// width: 610px;
width: 40%;
@ -12,9 +11,7 @@
height: 100vh;
}
@media screen and (max-width:957px) {
@media screen and (max-width: 957px) {
.loginLeft {
// width: 610px;
width: 100%;
@ -29,13 +26,11 @@
.loginRight {
flex: 1;
background-color: #F2F4F7;
;
background-color: #f2f4f7;
}
.loginTitle {
//styleName: Heading/1;
font-family: SF Pro Text;
font-size: 38px;
font-weight: 600;
line-height: 46px;
@ -48,9 +43,8 @@
font-size: 16px;
line-height: 24px;
color: #000000A6;
color: #000000a6;
}
}
}
@ -59,5 +53,5 @@
width: 60%;
height: 724px;
padding: 5px, 0px, 5px, 0px;
margin: 80px auto
}
margin: 80px auto;
}

View File

@ -7,7 +7,6 @@ import {
import { IUserInfo } from '@/interfaces/database/userSetting';
import userService from '@/services/userService';
import { message } from 'antd';
import { Nullable } from 'typings';
import { DvaModel } from 'umi';
export interface SettingModelState {

View File

@ -66,6 +66,7 @@ const ApiKeyModal = ({
<Form.Item<FieldType>
label="Api-Key"
name="api_key"
tooltip="coming soon"
rules={[{ required: true, message: 'Please input api key!' }]}
>
<Input />

View File

@ -43,16 +43,24 @@ const SystemModelSettingModal = ({
confirmLoading={loading}
>
<Form form={form} onValuesChange={onFormLayoutChange} layout={'vertical'}>
<Form.Item label="Sequence2txt model" name="asr_id">
<Form.Item
label="Sequence2txt model"
name="asr_id"
tooltip="coming soon"
>
<Select options={allOptions[LlmModelType.Speech2text]} />
</Form.Item>
<Form.Item label="Embedding model" name="embd_id">
<Form.Item label="Embedding model" name="embd_id" tooltip="coming soon">
<Select options={allOptions[LlmModelType.Embedding]} />
</Form.Item>
<Form.Item label="Img2txt model" name="img2txt_id">
<Form.Item
label="Img2txt model"
name="img2txt_id"
tooltip="coming soon"
>
<Select options={allOptions[LlmModelType.Image2text]} />
</Form.Item>
<Form.Item label="Chat model" name="llm_id">
<Form.Item label="Chat model" name="llm_id" tooltip="coming soon">
<Select options={allOptions[LlmModelType.Chat]} />
</Form.Item>
</Form>

View File

@ -110,7 +110,7 @@ const UserSettingProfile = () => {
<div>
<Space>
Your photo
<Tooltip title="prompt text">
<Tooltip title="coming soon">
<QuestionCircleOutlined />
</Tooltip>
</Space>
@ -140,6 +140,7 @@ const UserSettingProfile = () => {
<Form.Item<FieldType>
label="Color schema"
name="color_schema"
tooltip="coming soon"
rules={[
{ required: true, message: 'Please select your color schema!' },
]}
@ -153,6 +154,7 @@ const UserSettingProfile = () => {
<Form.Item<FieldType>
label="Language"
name="language"
tooltip="coming soon"
rules={[{ required: true, message: 'Please input your language!' }]}
>
<Select placeholder="select your language">
@ -164,6 +166,7 @@ const UserSettingProfile = () => {
<Form.Item<FieldType>
label="Timezone"
name="timezone"
tooltip="coming soon"
rules={[{ required: true, message: 'Please input your timezone!' }]}
>
<Select placeholder="select your timezone" showSearch>

View File

@ -61,3 +61,27 @@ export const getBase64FromUploadFileList = async (fileList?: UploadFile[]) => {
return '';
};
export const downloadFile = ({
url,
filename,
target,
}: {
url: string;
filename?: string;
target?: string;
}) => {
const downloadElement = document.createElement('a');
downloadElement.style.display = 'none';
downloadElement.href = url;
if (target) {
downloadElement.target = '_blank';
}
downloadElement.rel = 'noopener noreferrer';
if (filename) {
downloadElement.download = filename;
}
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
};

6
web/typings.d.ts vendored
View File

@ -1,4 +1,8 @@
import 'umi/typings';
declare module 'lodash';
export type Nullable<T> = T | null;
// declare type Nullable<T> = T | null; invalid
declare global {
type Nullable<T> = T | null;
}