chore: clear space onblur

This commit is contained in:
jialin
2025-11-03 10:14:30 +08:00
parent 1a92cc8543
commit 303429c52c
7 changed files with 309 additions and 195 deletions
+36 -6
View File
@@ -1,12 +1,20 @@
import { AutoComplete, Form, Spin } from 'antd'; import { LoadingOutlined } from '@ant-design/icons';
import { AutoComplete, Form, Typography } from 'antd';
import type { AutoCompleteProps } from 'antd/lib'; import type { AutoCompleteProps } from 'antd/lib';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { LoadingContent } from './components/not-found-content';
import { SealFormItemProps } from './types'; import { SealFormItemProps } from './types';
import Wrapper from './wrapper'; import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select'; import SelectWrapper from './wrapper/select';
const Link = Typography.Link;
const SealAutoComplete: React.FC< const SealAutoComplete: React.FC<
AutoCompleteProps & SealFormItemProps & { onInput?: (e: Event) => void } AutoCompleteProps &
SealFormItemProps & {
onInput?: (e: Event) => void;
clearSpaceOnBlur?: boolean;
}
> = (props) => { > = (props) => {
const { const {
label, label,
@@ -22,6 +30,8 @@ const SealAutoComplete: React.FC<
style, style,
addAfter, addAfter,
loading, loading,
allowClear,
clearSpaceOnBlur,
...rest ...rest
} = props; } = props;
const [isFocus, setIsFocus] = useState(false); const [isFocus, setIsFocus] = useState(false);
@@ -46,6 +56,7 @@ const SealAutoComplete: React.FC<
}; };
const handleChange = (val: string, option: any) => { const handleChange = (val: string, option: any) => {
console.log('handleChange val:', val);
let value = val; let value = val;
if (trim) { if (trim) {
value = value?.trim?.(); value = value?.trim?.();
@@ -62,7 +73,13 @@ const SealAutoComplete: React.FC<
if (!props.value) { if (!props.value) {
setIsFocus(false); setIsFocus(false);
} }
e.target.value = e.target.value?.trim?.();
if (clearSpaceOnBlur) {
e.target.value = e.target.value?.replace(/\s+/g, '');
props.onChange?.(e.target.value);
} else {
e.target.value = e.target.value?.trim();
}
props.onBlur?.(e); props.onBlur?.(e);
}; };
@@ -75,9 +92,20 @@ const SealAutoComplete: React.FC<
}; };
const renderAfter = () => { const renderAfter = () => {
if (loading) { if (loading) {
return <Spin size="small"></Spin>; return (
<Link>
<LoadingOutlined />
</Link>
);
} }
return addAfter; return null;
};
const popupRender = (originNode: React.ReactElement): React.ReactElement => {
if (loading) {
return <LoadingContent />;
}
return originNode || null;
}; };
return ( return (
@@ -91,7 +119,6 @@ const SealAutoComplete: React.FC<
required={required} required={required}
description={description} description={description}
disabled={props.disabled} disabled={props.disabled}
addAfter={renderAfter()}
onClick={handleClickWrapper} onClick={handleClickWrapper}
> >
<AutoComplete <AutoComplete
@@ -104,6 +131,8 @@ const SealAutoComplete: React.FC<
'' ''
) )
} }
allowClear={!loading && allowClear}
suffixIcon={renderAfter()}
// @ts-ignore // @ts-ignore
status={checkStatus || status} status={checkStatus || status}
onSelect={handleOnSelect} onSelect={handleOnSelect}
@@ -111,6 +140,7 @@ const SealAutoComplete: React.FC<
onBlur={handleOnBlur} onBlur={handleOnBlur}
onSearch={handleSearch} onSearch={handleSearch}
onChange={handleChange} onChange={handleChange}
popupRender={popupRender}
></AutoComplete> ></AutoComplete>
</Wrapper> </Wrapper>
</SelectWrapper> </SelectWrapper>
@@ -0,0 +1,43 @@
import { LoadingOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Empty, Typography } from 'antd';
import React from 'react';
import styled from 'styled-components';
const { Link } = Typography;
const NoContent = styled.div`
display: flex;
justify-content: center;
align-items: center;
padding-block: 12px;
`;
export const LoadingContent: React.FC = () => {
return (
<NoContent>
<Link>
<LoadingOutlined />
</Link>
</NoContent>
);
};
const NotFoundContent: React.FC<{
loading?: boolean;
notFoundContent: React.ReactNode;
}> = ({ loading, notFoundContent }) => {
const intl = useIntl();
if (loading) {
return <LoadingContent />;
}
return (
<NoContent>
{notFoundContent || (
<Empty description={intl.formatMessage({ id: 'common.data.empty' })} />
)}
</NoContent>
);
};
export default NotFoundContent;
+8 -1
View File
@@ -5,6 +5,7 @@ import { Form } from 'antd';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import BaseSelect from './base/select'; import BaseSelect from './base/select';
import NotFoundContent from './components/not-found-content';
import { SealFormItemProps } from './types'; import { SealFormItemProps } from './types';
import Wrapper from './wrapper'; import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select'; import SelectWrapper from './wrapper/select';
@@ -20,6 +21,7 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
allowNull, allowNull,
isInFormItems = true, isInFormItems = true,
notFoundContent = null, notFoundContent = null,
loading,
...rest ...rest
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
@@ -107,7 +109,12 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
onFocus={handleOnFocus} onFocus={handleOnFocus}
onBlur={handleOnBlur} onBlur={handleOnBlur}
onChange={handleChange} onChange={handleChange}
notFoundContent={notFoundContent} notFoundContent={
<NotFoundContent
loading={loading}
notFoundContent={notFoundContent}
/>
}
> >
{children} {children}
</BaseSelect> </BaseSelect>
+5
View File
@@ -39,6 +39,11 @@ declare namespace Global {
meta?: Record<string, any>; meta?: Record<string, any>;
} & Partial<U>; } & Partial<U>;
type BaseOptionGroup<T, U extends object = EmptyObject> = {
label: string;
options?: BaseOption<T, U>[];
};
interface HintOptions { interface HintOptions {
label: string; label: string;
value: string; value: string;
@@ -3,7 +3,6 @@ import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useAppUtils from '@/hooks/use-app-utils'; import useAppUtils from '@/hooks/use-app-utils';
import { LoadingOutlined } from '@ant-design/icons';
import { Link, useIntl } from '@umijs/max'; import { Link, useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
@@ -13,13 +12,6 @@ import styled from 'styled-components';
import { ClusterFormData as FormData } from '../config/types'; import { ClusterFormData as FormData } from '../config/types';
import { useProviderRegions } from '../hooks/use-provider-regions'; import { useProviderRegions } from '../hooks/use-provider-regions';
type OptionData = {
label: string;
datacenter: string;
value: string | number;
icon: string;
};
const OptionItem = styled.div` const OptionItem = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
@@ -42,13 +34,6 @@ const OptionItem = styled.div`
} }
`; `;
const NoContent = styled.div`
display: flex;
justify-content: center;
align-items: center;
padding-block: 12px;
`;
interface CloudProviderProps { interface CloudProviderProps {
provider: string; // 'kubernetes' | 'digitalocean'; provider: string; // 'kubernetes' | 'digitalocean';
credentialList: Global.BaseOption<number>[]; credentialList: Global.BaseOption<number>[];
@@ -56,22 +41,6 @@ interface CloudProviderProps {
credentialID?: number; credentialID?: number;
} }
const NotFoundContent: React.FC<{ loading: boolean }> = ({ loading }) => {
const intl = useIntl();
if (loading) {
return (
<NoContent>
<LoadingOutlined />
</NoContent>
);
}
return (
<NoContent>
{intl.formatMessage({ id: 'clusters.create.noRegions' })}
</NoContent>
);
};
const NotFoundCredentialContent: React.FC = () => { const NotFoundCredentialContent: React.FC = () => {
const [, setFromClusterCreation] = useAtom(fromClusterCreationAtom); const [, setFromClusterCreation] = useAtom(fromClusterCreationAtom);
const intl = useIntl(); const intl = useIntl();
@@ -81,11 +50,9 @@ const NotFoundCredentialContent: React.FC = () => {
}; };
return ( return (
<NoContent> <Link to={'/cluster-management/credentials'} onClick={handleOnClick}>
<Link to={'/cluster-management/credentials'} onClick={handleOnClick}> {intl.formatMessage({ id: 'clusters.button.addCredential' })}
{intl.formatMessage({ id: 'clusters.button.addCredential' })} </Link>
</Link>
</NoContent>
); );
}; };
@@ -204,7 +171,9 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
labelRender={labelRender} labelRender={labelRender}
optionRender={optionRender} optionRender={optionRender}
onChange={handleRegionChange} onChange={handleRegionChange}
notFoundContent={<NotFoundContent loading={loading} />} notFoundContent={intl.formatMessage({
id: 'clusters.create.noRegions'
})}
></SealSelect> ></SealSelect>
</Form.Item> </Form.Item>
</> </>
+18 -151
View File
@@ -4,19 +4,12 @@ import SealInputNumber from '@/components/seal-form/input-number';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import useAppUtils from '@/hooks/use-app-utils'; import useAppUtils from '@/hooks/use-app-utils';
import useDeferredRequest from '@/hooks/use-deferred-request';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import _ from 'lodash'; import { useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import {
queryDraftModelList,
queryHuggingfaceModels,
queryModelScopeModels
} from '../apis';
import { modelSourceMap } from '../config';
import { useFormContext } from '../config/form-context'; import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import useQueryDraftModels from '../hooks/use-query-draftModels';
const AlgorithmMap = { const AlgorithmMap = {
Eagle3: 'eagle3', Eagle3: 'eagle3',
@@ -34,144 +27,12 @@ const SpeculativeDecode = () => {
form form
); );
const algorithm = Form.useWatch(['speculative_config', 'algorithm'], form); const algorithm = Form.useWatch(['speculative_config', 'algorithm'], form);
const [draftModelList, setDraftModelList] = useState<
Global.BaseOption<string, { options: Global.BaseOption<string>[] }>[]
>([]);
const presetDraftModelListRef = useRef<Global.BaseOption<string>[]>([]);
const speculativeConfigRef = useRef<any>({}); const speculativeConfigRef = useRef<any>({});
const axiosTokenRef = useRef<AbortController | null>(null);
const [loading, setLoading] = useState(false);
const fetchDraftModels = async () => { const { draftModelList, loading, resetDraftModels, onSearch } =
const response = await queryDraftModelList({ useQueryDraftModels({
page: 1, source
perPage: 100
}); });
const options = response.items.map((item) => ({
label: item.name,
value: item.name
}));
presetDraftModelListRef.current = options;
setDraftModelList(options);
};
const getHuggingfaceModels = async (query: string) => {
if (axiosTokenRef.current) {
axiosTokenRef.current.abort();
}
axiosTokenRef.current = new AbortController();
try {
const params = {
limit: 10,
search: {
query: query
}
};
setLoading(true);
const data = await queryHuggingfaceModels(params, {
signal: axiosTokenRef.current.signal
});
const list = _.map(data || [], (item: any) => {
return {
value: item.name,
label: item.name
};
});
const catalogModelList =
presetDraftModelListRef.current.length > 0
? [
{
label: `${intl.formatMessage({ id: 'models.form.source' })}: ${intl.formatMessage({ id: 'menu.models.modelCatalog' })}`,
title: `${intl.formatMessage({ id: 'models.form.source' })}: ${intl.formatMessage({ id: 'menu.models.modelCatalog' })}`,
options: presetDraftModelListRef.current || []
}
]
: [];
setDraftModelList([
...catalogModelList,
{
label: `${intl.formatMessage({ id: 'models.form.source' })}: Hugging Face`,
title: `${intl.formatMessage({ id: 'models.form.source' })}: Hugging Face`,
options: list
}
]);
} catch (error) {
setDraftModelList(presetDraftModelListRef.current);
} finally {
setLoading(false);
}
};
const getModelScopeModels = async (query: string) => {
if (axiosTokenRef.current) {
axiosTokenRef.current.abort();
}
axiosTokenRef.current = new AbortController();
try {
const params = {
Name: query,
PageSize: 10,
PageNumber: 1,
tasks: []
};
setLoading(true);
const data = await queryModelScopeModels(params, {
signal: axiosTokenRef.current.signal
});
const list = _.map(
_.get(data, 'Data.Model.Models') || [],
(item: any) => {
return {
label: `${item.Path}/${item.Name}`,
value: `${item.Path}/${item.Name}`
};
}
);
const catalogModelList =
presetDraftModelListRef.current.length > 0
? [
{
label: `${intl.formatMessage({ id: 'models.form.source' })}: ${intl.formatMessage({ id: 'menu.models.modelCatalog' })}`,
title: `${intl.formatMessage({ id: 'models.form.source' })}: ${intl.formatMessage({ id: 'menu.models.modelCatalog' })}`,
options: presetDraftModelListRef.current || []
}
]
: [];
setDraftModelList([
...catalogModelList,
{
label: `${intl.formatMessage({ id: 'models.form.source' })}: ModelScope`,
title: `${intl.formatMessage({ id: 'models.form.source' })}: ModelScope`,
options: list
}
]);
} catch (error) {
setDraftModelList(presetDraftModelListRef.current);
} finally {
setLoading(false);
}
};
const handleOnSearch = async (value: string) => {
if (!value) {
setDraftModelList(presetDraftModelListRef.current);
return;
}
if (source === modelSourceMap.huggingface_value) {
await getHuggingfaceModels(value);
} else if (source === modelSourceMap.modelscope_value) {
await getModelScopeModels(value);
}
};
const { run: onSearch } = useDeferredRequest(
(value: string) => handleOnSearch(value),
150
);
const handleSpeculativeEnabledChange = (e: any) => { const handleSpeculativeEnabledChange = (e: any) => {
if (e.target.checked) { if (e.target.checked) {
@@ -191,11 +52,11 @@ const SpeculativeDecode = () => {
} }
}; };
useEffect(() => { const handleAlgorithemChange = (value: string) => {
if (algorithm === AlgorithmMap.Eagle3) { if (value === AlgorithmMap.Eagle3) {
fetchDraftModels(); resetDraftModels();
} }
}, [algorithm]); };
return ( return (
<> <>
@@ -228,11 +89,14 @@ const SpeculativeDecode = () => {
> >
<SealSelect <SealSelect
required required
onChange={handleAlgorithemChange}
label={intl.formatMessage({ id: 'models.form.algorithm' })} label={intl.formatMessage({ id: 'models.form.algorithm' })}
options={[ options={[
{ label: 'Eagle3', value: AlgorithmMap.Eagle3 }, [
{ label: 'MTP', value: AlgorithmMap.MTP }, { label: 'Eagle3', value: AlgorithmMap.Eagle3 },
{ label: 'N-gram', value: AlgorithmMap.Ngram } { label: 'MTP', value: AlgorithmMap.MTP },
{ label: 'N-gram', value: AlgorithmMap.Ngram }
]
]} ]}
></SealSelect> ></SealSelect>
</Form.Item> </Form.Item>
@@ -252,6 +116,9 @@ const SpeculativeDecode = () => {
<AutoComlete <AutoComlete
required required
allowClear allowClear
loading={loading}
trim={false}
clearSpaceOnBlur={true}
label={intl.formatMessage({ id: 'models.form.draftModel' })} label={intl.formatMessage({ id: 'models.form.draftModel' })}
placeholder={intl.formatMessage({ placeholder={intl.formatMessage({
id: 'models.form.draftModel.placeholder' id: 'models.form.draftModel.placeholder'
@@ -0,0 +1,193 @@
import { useIntl } from '@umijs/max';
import { useRequest } from 'ahooks';
import _ from 'lodash';
import { useEffect, useRef, useState } from 'react';
import {
queryDraftModelList,
queryHuggingfaceModels,
queryModelScopeModels
} from '../apis';
import { modelSourceMap } from '../config';
export default function useQueryDraftModels({ source }: { source: string }) {
const intl = useIntl();
const presetDraftModelListRef = useRef<Global.BaseOptionGroup<string>[]>([]);
const axiosTokenRef = useRef<AbortController | null>(null);
const [loading, setLoading] = useState(false);
const [draftModelList, setDraftModelList] = useState<
Global.BaseOptionGroup<string>[]
>([]);
const fetchDraftModels = async () => {
const response = await queryDraftModelList({
page: 1,
perPage: 100
});
const options = response.items.map((item) => ({
label: item.name,
value: item.name
}));
presetDraftModelListRef.current = options;
setDraftModelList(options);
};
const generateSourceLabel = (source: string) => {
let label = '';
const sourceLabel = intl.formatMessage({ id: 'models.form.source' });
if (source === modelSourceMap.huggingface_value) {
label = `${sourceLabel}: Hugging Face`;
}
if (source === modelSourceMap.modelscope_value) {
label = `${sourceLabel}: ModelScope`;
}
if (source === 'catalog') {
label = `${sourceLabel}: ${intl.formatMessage({ id: 'menu.models.modelCatalog' })}`;
}
return {
label: label
};
};
const getHuggingfaceModels = async (query: string) => {
if (axiosTokenRef.current) {
axiosTokenRef.current.abort();
}
axiosTokenRef.current = new AbortController();
try {
const params = {
limit: 10,
search: {
query: query
}
};
setLoading(true);
const data = await queryHuggingfaceModels(params, {
signal: axiosTokenRef.current.signal
});
const list = _.map(data || [], (item: any) => {
return {
value: item.name,
label: item.name
};
});
const catalogModelList =
presetDraftModelListRef.current.length > 0
? [
{
...generateSourceLabel('catalog'),
options: presetDraftModelListRef.current || []
}
]
: [];
setDraftModelList([
...catalogModelList,
{
...generateSourceLabel(modelSourceMap.huggingface_value),
options: list
}
]);
} catch (error) {
setDraftModelList(presetDraftModelListRef.current);
} finally {
setLoading(false);
}
};
const getModelScopeModels = async (query: string) => {
if (axiosTokenRef.current) {
axiosTokenRef.current.abort();
}
axiosTokenRef.current = new AbortController();
try {
const params = {
Name: query,
PageSize: 10,
PageNumber: 1,
tasks: []
};
setLoading(true);
const data = await queryModelScopeModels(params, {
signal: axiosTokenRef.current.signal
});
const list = _.map(
_.get(data, 'Data.Model.Models') || [],
(item: any) => {
return {
label: `${item.Path}/${item.Name}`,
value: `${item.Path}/${item.Name}`
};
}
);
const catalogModelList =
presetDraftModelListRef.current.length > 0
? [
{
...generateSourceLabel('catalog'),
options: presetDraftModelListRef.current || []
}
]
: [];
setDraftModelList([
...catalogModelList,
{
...generateSourceLabel(modelSourceMap.modelscope_value),
options: list
}
]);
} catch (error) {
setDraftModelList(presetDraftModelListRef.current);
} finally {
setLoading(false);
}
};
const handleOnSearch = async (value: string) => {
if (!value) {
setDraftModelList(presetDraftModelListRef.current);
return;
}
if (source === modelSourceMap.huggingface_value) {
await getHuggingfaceModels(value);
} else if (source === modelSourceMap.modelscope_value) {
await getModelScopeModels(value);
}
};
const { run: onSearch, cancel: cancelSearch } = useRequest(
(value: string) => handleOnSearch(value),
{
manual: true,
debounceWait: 300
}
);
const resetDraftModels = () => {
setDraftModelList(presetDraftModelListRef.current);
};
useEffect(() => {
fetchDraftModels();
return () => {
if (axiosTokenRef.current) {
axiosTokenRef.current.abort();
}
cancelSearch();
};
}, []);
return {
draftModelList,
loading,
fetchDraftModels,
onSearch,
resetDraftModels
};
}