diff --git a/src/components/seal-form/auto-complete.tsx b/src/components/seal-form/auto-complete.tsx index 46d4a913..679e47a0 100644 --- a/src/components/seal-form/auto-complete.tsx +++ b/src/components/seal-form/auto-complete.tsx @@ -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 ; + return ( + + + + ); } - return addAfter; + return null; + }; + + const popupRender = (originNode: React.ReactElement): React.ReactElement => { + if (loading) { + return ; + } + return originNode || null; }; return ( @@ -91,7 +119,6 @@ const SealAutoComplete: React.FC< required={required} description={description} disabled={props.disabled} - addAfter={renderAfter()} onClick={handleClickWrapper} > diff --git a/src/components/seal-form/components/not-found-content.tsx b/src/components/seal-form/components/not-found-content.tsx new file mode 100644 index 00000000..6d7bc13d --- /dev/null +++ b/src/components/seal-form/components/not-found-content.tsx @@ -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 ( + + + + + + ); +}; + +const NotFoundContent: React.FC<{ + loading?: boolean; + notFoundContent: React.ReactNode; +}> = ({ loading, notFoundContent }) => { + const intl = useIntl(); + if (loading) { + return ; + } + return ( + + {notFoundContent || ( + + )} + + ); +}; + +export default NotFoundContent; diff --git a/src/components/seal-form/seal-select.tsx b/src/components/seal-form/seal-select.tsx index 51137b92..a2ef5112 100644 --- a/src/components/seal-form/seal-select.tsx +++ b/src/components/seal-form/seal-select.tsx @@ -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 = (props) => { allowNull, isInFormItems = true, notFoundContent = null, + loading, ...rest } = props; const intl = useIntl(); @@ -107,7 +109,12 @@ const SealSelect: React.FC = (props) => { onFocus={handleOnFocus} onBlur={handleOnBlur} onChange={handleChange} - notFoundContent={notFoundContent} + notFoundContent={ + + } > {children} diff --git a/src/config/global.d.ts b/src/config/global.d.ts index 693ecb76..2a059853 100644 --- a/src/config/global.d.ts +++ b/src/config/global.d.ts @@ -39,6 +39,11 @@ declare namespace Global { meta?: Record; } & Partial; + type BaseOptionGroup = { + label: string; + options?: BaseOption[]; + }; + interface HintOptions { label: string; value: string; diff --git a/src/pages/cluster-management/components/cloud-provider-form.tsx b/src/pages/cluster-management/components/cloud-provider-form.tsx index fa7ca274..f328f335 100644 --- a/src/pages/cluster-management/components/cloud-provider-form.tsx +++ b/src/pages/cluster-management/components/cloud-provider-form.tsx @@ -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[]; @@ -56,22 +41,6 @@ interface CloudProviderProps { credentialID?: number; } -const NotFoundContent: React.FC<{ loading: boolean }> = ({ loading }) => { - const intl = useIntl(); - if (loading) { - return ( - - - - ); - } - return ( - - {intl.formatMessage({ id: 'clusters.create.noRegions' })} - - ); -}; - const NotFoundCredentialContent: React.FC = () => { const [, setFromClusterCreation] = useAtom(fromClusterCreationAtom); const intl = useIntl(); @@ -81,11 +50,9 @@ const NotFoundCredentialContent: React.FC = () => { }; return ( - - - {intl.formatMessage({ id: 'clusters.button.addCredential' })} - - + + {intl.formatMessage({ id: 'clusters.button.addCredential' })} + ); }; @@ -204,7 +171,9 @@ const CloudProvider: React.FC = (props) => { labelRender={labelRender} optionRender={optionRender} onChange={handleRegionChange} - notFoundContent={} + notFoundContent={intl.formatMessage({ + id: 'clusters.create.noRegions' + })} > diff --git a/src/pages/llmodels/forms/speculative-decode.tsx b/src/pages/llmodels/forms/speculative-decode.tsx index 9d92be9e..277e926a 100644 --- a/src/pages/llmodels/forms/speculative-decode.tsx +++ b/src/pages/llmodels/forms/speculative-decode.tsx @@ -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[] }>[] - >([]); - const presetDraftModelListRef = useRef[]>([]); const speculativeConfigRef = useRef({}); - const axiosTokenRef = useRef(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 = () => { > @@ -252,6 +116,9 @@ const SpeculativeDecode = () => { []>([]); + const axiosTokenRef = useRef(null); + const [loading, setLoading] = useState(false); + const [draftModelList, setDraftModelList] = useState< + Global.BaseOptionGroup[] + >([]); + + 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 + }; +}