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 React, { useEffect, useRef, useState } from 'react';
import { LoadingContent } from './components/not-found-content';
import { SealFormItemProps } from './types';
import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select';
const Link = Typography.Link;
const SealAutoComplete: React.FC<
AutoCompleteProps & SealFormItemProps & { onInput?: (e: Event) => void }
AutoCompleteProps &
SealFormItemProps & {
onInput?: (e: Event) => void;
clearSpaceOnBlur?: boolean;
}
> = (props) => {
const {
label,
@@ -22,6 +30,8 @@ const SealAutoComplete: React.FC<
style,
addAfter,
loading,
allowClear,
clearSpaceOnBlur,
...rest
} = props;
const [isFocus, setIsFocus] = useState(false);
@@ -46,6 +56,7 @@ const SealAutoComplete: React.FC<
};
const handleChange = (val: string, option: any) => {
console.log('handleChange val:', val);
let value = val;
if (trim) {
value = value?.trim?.();
@@ -62,7 +73,13 @@ const SealAutoComplete: React.FC<
if (!props.value) {
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);
};
@@ -75,9 +92,20 @@ const SealAutoComplete: React.FC<
};
const renderAfter = () => {
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 (
@@ -91,7 +119,6 @@ const SealAutoComplete: React.FC<
required={required}
description={description}
disabled={props.disabled}
addAfter={renderAfter()}
onClick={handleClickWrapper}
>
<AutoComplete
@@ -104,6 +131,8 @@ const SealAutoComplete: React.FC<
''
)
}
allowClear={!loading && allowClear}
suffixIcon={renderAfter()}
// @ts-ignore
status={checkStatus || status}
onSelect={handleOnSelect}
@@ -111,6 +140,7 @@ const SealAutoComplete: React.FC<
onBlur={handleOnBlur}
onSearch={handleSearch}
onChange={handleChange}
popupRender={popupRender}
></AutoComplete>
</Wrapper>
</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 React, { useEffect, useMemo, useRef, useState } from 'react';
import BaseSelect from './base/select';
import NotFoundContent from './components/not-found-content';
import { SealFormItemProps } from './types';
import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select';
@@ -20,6 +21,7 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
allowNull,
isInFormItems = true,
notFoundContent = null,
loading,
...rest
} = props;
const intl = useIntl();
@@ -107,7 +109,12 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onChange={handleChange}
notFoundContent={notFoundContent}
notFoundContent={
<NotFoundContent
loading={loading}
notFoundContent={notFoundContent}
/>
}
>
{children}
</BaseSelect>
+5
View File
@@ -39,6 +39,11 @@ declare namespace Global {
meta?: Record<string, any>;
} & Partial<U>;
type BaseOptionGroup<T, U extends object = EmptyObject> = {
label: string;
options?: BaseOption<T, U>[];
};
interface HintOptions {
label: string;
value: string;
@@ -3,7 +3,6 @@ import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useAppUtils from '@/hooks/use-app-utils';
import { LoadingOutlined } from '@ant-design/icons';
import { Link, useIntl } from '@umijs/max';
import { Form } from 'antd';
import { useAtom } from 'jotai';
@@ -13,13 +12,6 @@ import styled from 'styled-components';
import { ClusterFormData as FormData } from '../config/types';
import { useProviderRegions } from '../hooks/use-provider-regions';
type OptionData = {
label: string;
datacenter: string;
value: string | number;
icon: string;
};
const OptionItem = styled.div`
display: flex;
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 {
provider: string; // 'kubernetes' | 'digitalocean';
credentialList: Global.BaseOption<number>[];
@@ -56,22 +41,6 @@ interface CloudProviderProps {
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 [, setFromClusterCreation] = useAtom(fromClusterCreationAtom);
const intl = useIntl();
@@ -81,11 +50,9 @@ const NotFoundCredentialContent: React.FC = () => {
};
return (
<NoContent>
<Link to={'/cluster-management/credentials'} onClick={handleOnClick}>
{intl.formatMessage({ id: 'clusters.button.addCredential' })}
</Link>
</NoContent>
<Link to={'/cluster-management/credentials'} onClick={handleOnClick}>
{intl.formatMessage({ id: 'clusters.button.addCredential' })}
</Link>
);
};
@@ -204,7 +171,9 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
labelRender={labelRender}
optionRender={optionRender}
onChange={handleRegionChange}
notFoundContent={<NotFoundContent loading={loading} />}
notFoundContent={intl.formatMessage({
id: 'clusters.create.noRegions'
})}
></SealSelect>
</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 SealSelect from '@/components/seal-form/seal-select';
import useAppUtils from '@/hooks/use-app-utils';
import useDeferredRequest from '@/hooks/use-deferred-request';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import _ from 'lodash';
import { useEffect, useRef, useState } from 'react';
import {
queryDraftModelList,
queryHuggingfaceModels,
queryModelScopeModels
} from '../apis';
import { modelSourceMap } from '../config';
import { useRef } from 'react';
import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types';
import useQueryDraftModels from '../hooks/use-query-draftModels';
const AlgorithmMap = {
Eagle3: 'eagle3',
@@ -34,144 +27,12 @@ const SpeculativeDecode = () => {
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 axiosTokenRef = useRef<AbortController | null>(null);
const [loading, setLoading] = useState(false);
const fetchDraftModels = async () => {
const response = await queryDraftModelList({
page: 1,
perPage: 100
const { draftModelList, loading, resetDraftModels, onSearch } =
useQueryDraftModels({
source
});
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) => {
if (e.target.checked) {
@@ -191,11 +52,11 @@ const SpeculativeDecode = () => {
}
};
useEffect(() => {
if (algorithm === AlgorithmMap.Eagle3) {
fetchDraftModels();
const handleAlgorithemChange = (value: string) => {
if (value === AlgorithmMap.Eagle3) {
resetDraftModels();
}
}, [algorithm]);
};
return (
<>
@@ -228,11 +89,14 @@ const SpeculativeDecode = () => {
>
<SealSelect
required
onChange={handleAlgorithemChange}
label={intl.formatMessage({ id: 'models.form.algorithm' })}
options={[
{ label: 'Eagle3', value: AlgorithmMap.Eagle3 },
{ label: 'MTP', value: AlgorithmMap.MTP },
{ label: 'N-gram', value: AlgorithmMap.Ngram }
[
{ label: 'Eagle3', value: AlgorithmMap.Eagle3 },
{ label: 'MTP', value: AlgorithmMap.MTP },
{ label: 'N-gram', value: AlgorithmMap.Ngram }
]
]}
></SealSelect>
</Form.Item>
@@ -252,6 +116,9 @@ const SpeculativeDecode = () => {
<AutoComlete
required
allowClear
loading={loading}
trim={false}
clearSpaceOnBlur={true}
label={intl.formatMessage({ id: 'models.form.draftModel' })}
placeholder={intl.formatMessage({
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
};
}