diff --git a/src/pages/_components/scroll-spy-tabs/index.tsx b/src/pages/_components/scroll-spy-tabs/index.tsx new file mode 100644 index 00000000..eaa172ac --- /dev/null +++ b/src/pages/_components/scroll-spy-tabs/index.tsx @@ -0,0 +1,113 @@ +import SegmentLine from '@/components/segment-line'; +import { useMemoizedFn } from 'ahooks'; +import _ from 'lodash'; +import React, { forwardRef, useImperativeHandle } from 'react'; +import styled from 'styled-components'; +import useFieldScroll from './use-field-scroll'; + +const SegmentedHeader = styled.div<{ $top?: number }>` + position: sticky; + top: ${(props) => props.$top || 0}px; + z-index: 10; + margin-bottom: 16px; + border-bottom: 1px solid var(--ant-color-split); + background-color: var(--ant-color-bg-elevated); +`; + +/** + * ScrollSpyTabs component + * defaultTarget: The default active target tab + * segmentedTop: { // Mostlty, It's always a constants. + * top: number; // The top offset for the sticky header + * offsetTop: number; // The offset top for the target + * } + * getScrollElementScrollableHeight: function to get the scrollable height of the scroll element + * segmentOptions.field: The target data-field={segmentOptions.field} to scroll to + * activeKey: The current active keys for collapsible sections + * setActiveKey: The function to set active keys for collapsible sections + */ +interface ScrollSpyTabsProps { + ref?: any; + children?: React.ReactNode; + defaultTarget?: string; + segmentedTop: { + top: number; + offsetTop: number; + }; + activeKey: string[]; + setActiveKey: (keys: string[]) => void; + getScrollElementScrollableHeight?: () => { + scrollHeight: number; + scrollTop: number; + }; + segmentOptions: { + label: string; + value: string; + icon?: React.ReactNode; + field: string; + }[]; +} + +const ScrollSpyTabs: React.FC = forwardRef( + ( + { + getScrollElementScrollableHeight, + segmentedTop, + segmentOptions, + defaultTarget, + activeKey, + setActiveKey, + children + }, + ref + ) => { + const [target, setTarget] = React.useState( + defaultTarget || segmentOptions[0]?.value || '' + ); + + const { scrollToSegment, holderHeight } = useFieldScroll({ + activeKey, + setActiveKey, + segmentOptions, + segmentedTop: segmentedTop, + getScrollElementScrollableHeight: getScrollElementScrollableHeight + }); + + const throttleScrollToSegment = useMemoizedFn( + _.throttle( + async (val: string) => { + setTarget(val); + scrollToSegment(val, { offsetTop: segmentedTop.offsetTop }); + }, + 500, + { trailing: true } + ) + ); + + const handleTargetChange = async (val: any) => { + throttleScrollToSegment(val); + }; + + useImperativeHandle(ref, () => ({ + handleTargetChange + })); + + return ( +
+ + + + {children} +
+
+ ); + } +); + +export default ScrollSpyTabs; diff --git a/src/pages/llmodels/hooks/use-field-scroll.ts b/src/pages/_components/scroll-spy-tabs/use-field-scroll.ts similarity index 94% rename from src/pages/llmodels/hooks/use-field-scroll.ts rename to src/pages/_components/scroll-spy-tabs/use-field-scroll.ts index f28ca18c..3e820930 100644 --- a/src/pages/llmodels/hooks/use-field-scroll.ts +++ b/src/pages/_components/scroll-spy-tabs/use-field-scroll.ts @@ -9,7 +9,6 @@ interface ScrollOptions { } export default function useScrollAfterExpand({ - form, activeKey, setActiveKey, segmentOptions, @@ -17,9 +16,8 @@ export default function useScrollAfterExpand({ segmentedTop = { top: 0, offsetTop: 96 }, getScrollElementScrollableHeight }: { - form: any; activeKey: string[]; - setActiveKey: React.Dispatch>; + setActiveKey: (keys: string[]) => void; segmentOptions: { value: string; field: string }[]; getScrollElementScrollableHeight?: () => { scrollHeight: number; @@ -27,8 +25,8 @@ export default function useScrollAfterExpand({ }; defaultWait?: number; segmentedTop: { - top: number; - offsetTop: number; + top: number; // The top offset for the sticky header + offsetTop: number; // The offset top for the target }; }) { const [holderHeight, setHolderHeight] = useState(0); @@ -67,7 +65,7 @@ export default function useScrollAfterExpand({ const scrollToSegment = useMemoizedFn( async (val: string, options?: ScrollOptions) => { if (!activeKey.includes(val)) { - setActiveKey((prev) => [...prev, val]); + setActiveKey([...activeKey, val]); await new Promise((r) => { setTimeout(r, options?.wait ?? defaultWait); }); diff --git a/src/pages/llmodels/forms/index.tsx b/src/pages/llmodels/forms/index.tsx index b37b688b..792ff91c 100644 --- a/src/pages/llmodels/forms/index.tsx +++ b/src/pages/llmodels/forms/index.tsx @@ -1,11 +1,10 @@ import IconFont from '@/components/icon-font'; -import SegmentLine from '@/components/segment-line'; import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import CollapsePanel from '@/pages/_components/collapse-panel'; import { useWrapperContext } from '@/pages/_components/column-wrapper/use-wrapper-context'; +import ScrollSpyTabs from '@/pages/_components/scroll-spy-tabs'; import { useIntl } from '@umijs/max'; -import useMemoizedFn from 'ahooks/lib/useMemoizedFn'; import { Form } from 'antd'; import _ from 'lodash'; import React, { @@ -14,7 +13,6 @@ import React, { useImperativeHandle, useMemo } from 'react'; -import styled from 'styled-components'; import { DeployFormKeyMap, DO_NOT_NOTIFY_RECREATE, @@ -31,7 +29,6 @@ import { SourceType } from '../config/types'; import { generateGPUIds } from '../config/utils'; -import useFieldScroll from '../hooks/use-field-scroll'; import { useGenerateGPUOptions } from '../hooks/use-form-initial-values'; import useQueryBackends from '../hooks/use-query-backends'; import { useQueryContextLength } from '../services/use-query-context-length'; @@ -48,15 +45,6 @@ const scheduleRequiredFields = ['gpu_selector']; const performanceRequiredFields = ['speculative_config']; -const SegmentedHeader = styled.div<{ $top?: number }>` - position: sticky; - top: ${(props) => props.$top || 0}px; - z-index: 10; - margin-bottom: 16px; - border-bottom: 1px solid var(--ant-color-split); - background-color: var(--ant-color-bg-elevated); -`; - interface DataFormProps { initialValues?: FormData; ref?: any; @@ -108,11 +96,11 @@ const DataForm: React.FC = forwardRef((props, ref) => { const [form] = Form.useForm(); const intl = useIntl(); const [activeKey, setActiveKey] = React.useState([]); - const [target, setTarget] = React.useState(TABKeysMap.BASIC); const { modelContextData, fetchContextLength } = useQueryContextLength(); const localPath = Form.useWatch('local_path', form); const modelScopeModelId = Form.useWatch('model_scope_model_id', form); const huggingfaceRepoId = Form.useWatch('huggingface_repo_id', form); + const scrollTabsRef = React.useRef(null); const segmentOptions = [ { @@ -159,15 +147,6 @@ const DataForm: React.FC = forwardRef((props, ref) => { }; }, [source, formKey, action]); - const { scrollToSegment, holderHeight } = useFieldScroll({ - form, - activeKey, - setActiveKey, - segmentOptions, - segmentedTop: segmentedTop, - getScrollElementScrollableHeight: getScrollElementScrollableHeight - }); - const handleSumit = () => { form.submit(); }; @@ -303,21 +282,6 @@ const DataForm: React.FC = forwardRef((props, ref) => { setActiveKey(Array.isArray(keys) ? keys : [keys]); }; - const throttleScrollToSegment = useMemoizedFn( - _.throttle( - async (val: string) => { - setTarget(val); - scrollToSegment(val, { offsetTop: segmentedTop.offsetTop }); - }, - 500, - { trailing: true } - ) - ); - - const handleTargetChange = async (val: any) => { - throttleScrollToSegment(val); - }; - const handleOnFinishFailed = (errorInfo: any) => { const { errorFields } = errorInfo; if (errorFields && errorFields.length > 0) { @@ -352,18 +316,18 @@ const DataForm: React.FC = forwardRef((props, ref) => { } if (isBaseRequired) { - handleTargetChange(TABKeysMap.BASIC); + scrollTabsRef.current?.handleTargetChange(TABKeysMap.BASIC); } else if (isScheduleRequired) { - handleTargetChange(TABKeysMap.SCHEDULING); + scrollTabsRef.current?.handleTargetChange(TABKeysMap.SCHEDULING); } else if (isPerformanceRequired) { - handleTargetChange(TABKeysMap.PERFORMANCE); + scrollTabsRef.current?.handleTargetChange(TABKeysMap.PERFORMANCE); } else if (isAdvancedRequired && formKey === DeployFormKeyMap.CATALOG) { - handleTargetChange(TABKeysMap.ADVANCED); + scrollTabsRef.current?.handleTargetChange(TABKeysMap.ADVANCED); } else if ( isAdvancedRequired && formKey === DeployFormKeyMap.DEPLOYMENT ) { - handleTargetChange(TABKeysMap.BASIC); + scrollTabsRef.current?.handleTargetChange(TABKeysMap.BASIC); } setActiveKey((prev: string[]) => [ @@ -417,6 +381,10 @@ const DataForm: React.FC = forwardRef((props, ref) => { // fetchContextLength({ ...params, source }); }, [isGGUF, source, localPath, modelScopeModelId, huggingfaceRepoId]); + const handleActiveChange = (key: string[]) => { + setActiveKey(key); + }; + return ( = forwardRef((props, ref) => { onBackendChange: handleBackendChange }} > - - - -
- - + + speculative_config: { + enabled: false, + algorithm: '', + draft_model: null, + num_draft_tokens: + initialValues?.speculative_config?.num_draft_tokens || 4, + ngram_min_match_length: + initialValues?.speculative_config?.ngram_min_match_length || 1, + ngram_max_match_length: + initialValues?.speculative_config?.ngram_max_match_length || 10 }, - { - key: TABKeysMap.ADVANCED, - label: intl.formatMessage({ id: 'resources.form.advanced' }), - forceRender: true, - children: - } - ]} - > -
-
+ ...initialValues, + backend_version: initialValues?.backend_version || null, + max_context_len: initialValues?.max_context_len || 2048 + }} + > + + + }, + { + key: TABKeysMap.SCHEDULING, + label: intl.formatMessage({ id: 'models.form.scheduling' }), + forceRender: true, + children: + }, + { + key: TABKeysMap.ADVANCED, + label: intl.formatMessage({ id: 'resources.form.advanced' }), + forceRender: true, + children: + } + ]} + > + +
); });