From bd6c689d913ebf83bc6fc555f6047d2aa307a7fb Mon Sep 17 00:00:00 2001 From: jialin Date: Sat, 29 Mar 2025 21:39:44 +0800 Subject: [PATCH] chore: compatibility api --- config/config.ts | 4 - src/components/alert-info/block.less | 4 - src/components/alert-info/block.tsx | 23 +- src/components/overlay-scroller/index.tsx | 34 +++ src/config/theme.ts | 2 +- src/pages/llmodels/apis/index.ts | 22 +- .../llmodels/components/column-wrapper.tsx | 1 - .../llmodels/components/compatible-alert.tsx | 72 ++++++ src/pages/llmodels/components/data-form.tsx | 211 ++++++++++++------ .../components/deploy-builtin-modal.tsx | 3 +- .../llmodels/components/deploy-modal.tsx | 149 ++++--------- .../llmodels/components/hf-model-file.tsx | 138 ++++++++---- .../llmodels/components/hf-model-item.tsx | 64 ++---- .../components/incompatiable-info.tsx | 80 +++++++ src/pages/llmodels/components/model-card.tsx | 43 ++-- .../llmodels/components/search-input.tsx | 20 +- .../llmodels/components/search-model.tsx | 148 +++++++----- .../llmodels/components/search-result.tsx | 10 +- src/pages/llmodels/config/index.ts | 22 ++ src/pages/llmodels/config/types.ts | 37 +++ src/pages/llmodels/hooks/index.ts | 117 +++++++++- src/pages/llmodels/style/hf-model-file.less | 10 +- src/pages/llmodels/style/hf-model-item.less | 4 +- .../playground/components/image-edit.tsx | 4 +- 24 files changed, 848 insertions(+), 374 deletions(-) create mode 100644 src/components/overlay-scroller/index.tsx create mode 100644 src/pages/llmodels/components/compatible-alert.tsx create mode 100644 src/pages/llmodels/components/incompatiable-info.tsx diff --git a/config/config.ts b/config/config.ts index 307f9c9f..dfc598be 100644 --- a/config/config.ts +++ b/config/config.ts @@ -80,10 +80,6 @@ export default defineConfig({ clickToComponent: {}, antd: { style: 'less' - // configProvider: { - // componentSize: 'large', - // theme - // } }, hash: true, access: {}, diff --git a/src/components/alert-info/block.less b/src/components/alert-info/block.less index ed6d64a7..7e57d71a 100644 --- a/src/components/alert-info/block.less +++ b/src/components/alert-info/block.less @@ -21,10 +21,6 @@ background-color: var(--ant-color-warning-bg); } - .content { - word-break: break-all; - } - .title { position: absolute; left: 0; diff --git a/src/components/alert-info/block.tsx b/src/components/alert-info/block.tsx index 908b8bf4..60c5ed5f 100644 --- a/src/components/alert-info/block.tsx +++ b/src/components/alert-info/block.tsx @@ -2,6 +2,8 @@ import { WarningFilled } from '@ant-design/icons'; import { Typography } from 'antd'; import classNames from 'classnames'; import React from 'react'; +import styled from 'styled-components'; +import OverlayScroller from '../overlay-scroller'; import './block.less'; interface AlertInfoProps { type: 'danger' | 'warning'; @@ -13,8 +15,22 @@ interface AlertInfoProps { title: React.ReactNode; } +const TitleWrapper = styled.div` + font-weight: 700; + color: var(--ant-color-text); +`; + +const ContentWrapper = styled.div<{ $hasTitle: boolean }>` + word-break: break-word; + color: ${(props) => + props.$hasTitle + ? 'var(--ant-color-text-secondary)' + : 'var(--ant-color-text)'}; + font-weight: var(--font-weight-500); +`; + const AlertInfo: React.FC = (props) => { - const { message, type, rows = 1, ellipsis, style } = props; + const { message, type, rows = 1, ellipsis, style, title } = props; return ( <> @@ -34,7 +50,10 @@ const AlertInfo: React.FC = (props) => {
- {message} + {title && {title}} + + {message} + ) : null} diff --git a/src/components/overlay-scroller/index.tsx b/src/components/overlay-scroller/index.tsx new file mode 100644 index 00000000..50eb52ff --- /dev/null +++ b/src/components/overlay-scroller/index.tsx @@ -0,0 +1,34 @@ +import useOverlayScroller from '@/hooks/use-overlay-scroller'; +import React from 'react'; +import styled from 'styled-components'; + +const Wrapper = styled.div<{ $maxHeight?: number }>` + max-height: ${({ $maxHeight }) => + typeof $maxHeight === 'number' ? `${$maxHeight}px` : $maxHeight}; + overflow-y: auto; + width: 100%; + padding-inline: 10px; +`; + +const OverlayScroller: React.FC = ({ children, maxHeight, theme }) => { + const scroller = React.useRef(null); + const { initialize } = useOverlayScroller({ + options: { + theme: theme || 'os-theme-light' + } + }); + + React.useEffect(() => { + if (scroller.current) { + initialize(scroller.current); + } + }, []); + + return ( + + ); +}; + +export default OverlayScroller; diff --git a/src/config/theme.ts b/src/config/theme.ts index 77bbc18f..828894b8 100644 --- a/src/config/theme.ts +++ b/src/config/theme.ts @@ -64,7 +64,7 @@ export default { }, token: { fontFamily: - "'Segoe UI', Roboto, Helvetica, -apple-system, BlinkMacSystemFont, Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", + "Helvetica Neue, -apple-system, BlinkMacSystemFont, Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", colorText: 'rgba(0,0,0,1)', colorPrimary: '#007BFF', colorSuccess: '#54cc98', diff --git a/src/pages/llmodels/apis/index.ts b/src/pages/llmodels/apis/index.ts index d29c39c8..0a865407 100644 --- a/src/pages/llmodels/apis/index.ts +++ b/src/pages/llmodels/apis/index.ts @@ -5,6 +5,8 @@ import qs from 'query-string'; import { CatalogItem, CatalogSpec, + EvaluateResult, + EvaluateSpec, FormData, GPUListItem, ListItem, @@ -16,6 +18,8 @@ export const MODELS_API = '/models'; export const MODEL_INSTANCE_API = '/model-instances'; +export const MODEL_EVALUATIONS = '/model-evaluations'; + const setProxyUrl = (url: string) => { return `/proxy?url=${encodeURIComponent(url)}`; }; @@ -189,7 +193,7 @@ export async function queryModelScopeModels( ...params, ...Criterion, Name: `${params.Name}`, - PageSize: 100, + PageSize: 10, PageNumber: 1 }) }); @@ -251,7 +255,7 @@ export async function queryHuggingfaceModels( for await (const model of listModels({ ...params, ...options, - limit: 100, + limit: 10, additionalFields: ['sha', 'tags'], fetch(_url: string, config: any) { const url = params.search.sort @@ -289,7 +293,6 @@ export async function queryHuggingfaceModelFiles( signal: options?.signal }); } catch (error) { - console.log('queryHuggingfaceModels error===', error); // ignore return []; } @@ -363,3 +366,16 @@ export async function queryCatalogItemSpec( } ); } + +export async function evaluationsModelSpec( + data: { + model_specs: EvaluateSpec[]; + }, + options: { token: any } +) { + return request<{ results: EvaluateResult[] }>(`${MODEL_EVALUATIONS}`, { + method: 'POST', + data, + cancelToken: options?.token + }); +} diff --git a/src/pages/llmodels/components/column-wrapper.tsx b/src/pages/llmodels/components/column-wrapper.tsx index 710068b0..1bc29649 100644 --- a/src/pages/llmodels/components/column-wrapper.tsx +++ b/src/pages/llmodels/components/column-wrapper.tsx @@ -1,6 +1,5 @@ import useOverlayScroller from '@/hooks/use-overlay-scroller'; import React from 'react'; -import 'simplebar-react/dist/simplebar.min.css'; import '../style/column-wrapper.less'; const ColumnWrapper: React.FC = ({ diff --git a/src/pages/llmodels/components/compatible-alert.tsx b/src/pages/llmodels/components/compatible-alert.tsx new file mode 100644 index 00000000..8d95144e --- /dev/null +++ b/src/pages/llmodels/components/compatible-alert.tsx @@ -0,0 +1,72 @@ +import AlertBlockInfo from '@/components/alert-info/block'; +import { isArray } from 'lodash'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +interface CompatibilityAlertProps { + warningStatus: { + show: boolean; + title?: string; + isHtml?: boolean; + message: string | string[]; + }; +} + +const DivWrapper = styled.div` + padding-inline: 12px; +`; + +const MessageWrapper = styled.div` + display: flex; + flex-direction: column; + font-size: var(--font-size-small); + gap: 4px; +`; + +const CompatibilityAlert: React.FC = (props) => { + const { warningStatus } = props; + const { title, show, message, isHtml } = warningStatus; + + const renderMessage = useMemo(() => { + if (!message || !show) { + return ''; + } + if (isHtml) { + return ( + + ); + } + if (typeof message === 'string') { + return message; + } + if (isArray(message)) { + return ( + + {message.map((item, index) => ( +
{item}
+ ))} +
+ ); + } + return ''; + }, [message, show]); + + return ( + show && ( + + + + ) + ); +}; + +export default CompatibilityAlert; diff --git a/src/pages/llmodels/components/data-form.tsx b/src/pages/llmodels/components/data-form.tsx index 6f138a73..84815b05 100644 --- a/src/pages/llmodels/components/data-form.tsx +++ b/src/pages/llmodels/components/data-form.tsx @@ -6,23 +6,25 @@ import TooltipList from '@/components/tooltip-list'; import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import useAppUtils from '@/hooks/use-app-utils'; +import { createAxiosToken } from '@/hooks/use-chunk-request'; import { useIntl } from '@umijs/max'; import { Form, Typography } from 'antd'; import _ from 'lodash'; import React, { forwardRef, - useCallback, - useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { evaluationsModelSpec } from '../apis'; import { HuggingFaceTaskMap, ModelscopeTaskMap, backendOptionsMap, backendTipsList, + excludeFields, + getSourceRepoConfigValue, localPathTipsList, modelSourceMap, modelTaskMap, @@ -30,7 +32,7 @@ import { sourceOptions } from '../config'; import { identifyModelTask } from '../config/audio-catalog'; -import { FormData } from '../config/types'; +import { EvaluateResult, FormData } from '../config/types'; import AdvanceConfig from './advance-config'; interface DataFormProps { @@ -48,12 +50,19 @@ interface DataFormProps { sourceList?: Global.BaseOption[]; gpuOptions: any[]; modelFileOptions?: any[]; + fields?: string[]; + handleUpdateWarning?: (params: { + backend: string; + localPath: string; + source: string; + }) => any; + handleShowCompatibleAlert?: (data: EvaluateResult | null) => void; + onValuesChange?: (changedValues: any, allValues: any) => void; onSizeChange?: (val: number) => void; onQuantizationChange?: (val: string) => void; onSourceChange?: (value: string) => void; onOk: (values: FormData) => void; onBackendChange?: (value: string) => void; - fields?: string[]; } const SEARCH_SOURCE = [ @@ -75,6 +84,8 @@ const DataForm: React.FC = forwardRef((props, ref) => { sizeOptions = [], quantizationOptions = [], fields = ['source'], + handleUpdateWarning, + handleShowCompatibleAlert, onSourceChange, onOk } = props; @@ -88,75 +99,55 @@ const DataForm: React.FC = forwardRef((props, ref) => { text2speech: false, speech2text: false }); - + const checkTokenRef = useRef(null); const localPathCache = useRef(''); const handleSumit = () => { form.submit(); }; - useImperativeHandle( - ref, - () => { - return { - form: form, - submit: handleSumit, - setFieldsValue: (values: FormData) => { - form.setFieldsValue(values); - }, - setFieldValue: (name: string, value: any) => { - form.setFieldValue(name, value); - }, - getFieldValue: (name: string) => { - return form.getFieldValue(name); - }, - resetFields() { - form.resetFields(); - } - }; - }, - [] - ); - - const handleOnSelectModel = () => { - let name = _.split(props.selectedModel.name, '/').slice(-1)[0]; - const reg = /(-gguf)$/i; - name = _.toLower(name).replace(reg, ''); - - const modelTaskType = identifyModelTask( - props.source, - props.selectedModel.name - ); + const handleRecognizeAudioModel = (selectModel: any) => { + const modelTaskType = identifyModelTask(props.source, selectModel.name); const modelTask = - HuggingFaceTaskMap.audio.includes(props.selectedModel.task) || - ModelscopeTaskMap.audio.includes(props.selectedModel.task) + HuggingFaceTaskMap.audio.includes(selectModel.task) || + ModelscopeTaskMap.audio.includes(selectModel.task) ? modelTaskMap.audio : ''; - setModelTask({ - value: props.selectedModel.task, + const modelTaskData = { + value: selectModel.task, type: modelTaskType || modelTask, text2speech: - HuggingFaceTaskMap[modelTaskMap.textToSpeech] === - props.selectedModel.task || - ModelscopeTaskMap[modelTaskMap.textToSpeech] === - props.selectedModel.task, + HuggingFaceTaskMap[modelTaskMap.textToSpeech] === selectModel.task || + ModelscopeTaskMap[modelTaskMap.textToSpeech] === selectModel.task, speech2text: - HuggingFaceTaskMap[modelTaskMap.speechToText] === - props.selectedModel.task || - ModelscopeTaskMap[modelTaskMap.speechToText] === - props.selectedModel.task - }); + HuggingFaceTaskMap[modelTaskMap.speechToText] === selectModel.task || + ModelscopeTaskMap[modelTaskMap.speechToText] === selectModel.task + }; + return modelTaskData; + }; + + const handleOnSelectModel = (selectModel: any) => { + let name = _.split(selectModel.name, '/').slice(-1)[0]; + const reg = /(-gguf)$/i; + name = _.toLower(name).replace(reg, ''); + + const modelTaskData = handleRecognizeAudioModel(selectModel); + setModelTask(modelTaskData); if (SEARCH_SOURCE.includes(props.source)) { form.setFieldsValue({ - repo_id: props.selectedModel.name, - name: name + repo_id: selectModel.name, + name: name, + backend: + modelTaskData.type === modelTaskMap.audio + ? backendOptionsMap.voxBox + : form.getFieldValue('backend') }); } else { form.setFieldsValue({ - ollama_library_model_name: props.selectedModel.name, + ollama_library_model_name: selectModel.name, name: name }); } @@ -166,8 +157,52 @@ const DataForm: React.FC = forwardRef((props, ref) => { localPathCache.current = form.getFieldValue('local_path'); }; + const handleEvaluate = async (data: any) => { + try { + checkTokenRef.current?.cancel(); + checkTokenRef.current = createAxiosToken(); + const evalution = await evaluationsModelSpec( + { + model_specs: [ + { + ..._.omit(data, ['scheduleType']), + categories: data.categories ? [data.categories] : [] + } + ] + }, + { + token: checkTokenRef.current.token + } + ); + return evalution.results?.[0]; + } catch (error) { + console.log('error=====', error); + return null; + } + }; + + // trigger from local_path change or backend change + const handleBackendChangeHook = async () => { + const localPath = form.getFieldValue?.('local_path'); + const backend = form.getFieldValue?.('backend'); + + const res = handleUpdateWarning?.({ + backend, + localPath: localPath, + source: props.source + }); + + if (!res.show) { + const values = form.getFieldsValue?.(); + const data = getSourceRepoConfigValue(props.source, values); + const evalutionData = await handleEvaluate(data.values); + handleShowCompatibleAlert?.(evalutionData); + } + }; + const handleLocalPathBlur = (e: any) => { const value = e.target.value; + console.log('handleLocalPathBlur:', e, localPathCache.current); if (value === localPathCache.current && value) { return; } @@ -177,8 +212,9 @@ const DataForm: React.FC = forwardRef((props, ref) => { if (!isEndwithGGUF && !isBlobFile) { backend = backendOptionsMap.vllm; } - props.onBackendChange?.(backend); form.setFieldValue('backend', backend); + handleBackendChangeHook(); + props.onBackendChange?.(backend); }; const renderHuggingfaceFields = () => { @@ -393,17 +429,21 @@ const DataForm: React.FC = forwardRef((props, ref) => { } }; - const handleBackendChange = useCallback((val: string) => { + const handleBackendChange = async (val: string) => { + const updates = { + backend_version: '' + }; if (val === backendOptionsMap.llamaBox) { - form.setFieldsValue({ + Object.assign(updates, { distributed_inference_across_workers: true, cpu_offloading: true }); } - form.setFieldValue('backend_version', ''); + form.setFieldsValue(updates); handleSetGPUIds(val); + handleBackendChangeHook(); props.onBackendChange?.(val); - }, []); + }; const generateGPUIds = (data: FormData) => { const gpu_ids = _.get(data, 'gpu_selector.gpu_ids', []); @@ -452,21 +492,49 @@ const DataForm: React.FC = forwardRef((props, ref) => { onSourceChange?.(val); }; - useEffect(() => { - if (action === PageAction.EDIT) return; - if (modelTask.type === modelTaskMap.audio) { - form.setFieldValue('backend', backendOptionsMap.voxBox); - } else { - form.setFieldValue( - 'backend', - isGGUF ? backendOptionsMap.llamaBox : backendOptionsMap.vllm - ); + const handleOnValuesChange = async (changedValues: any, allValues: any) => { + const keys = Object.keys(changedValues); + const isExcludeField = keys.some((key) => excludeFields.includes(key)); + if ( + !isExcludeField && + !_.has(changedValues, 'backend') && + !_.has(changedValues, 'local_path') + ) { + const values = form.getFieldsValue?.(); + const data = getSourceRepoConfigValue(props.source, values); + + const evalutionData = await handleEvaluate(data.values); + handleShowCompatibleAlert?.(evalutionData); } - }, [isGGUF, modelTask]); + }; + + useImperativeHandle( + ref, + () => { + return { + form: form, + handleOnSelectModel: handleOnSelectModel, + submit: handleSumit, + setFieldsValue: (values: FormData) => { + form.setFieldsValue(values); + }, + setFieldValue: (name: string, value: any) => { + form.setFieldValue(name, value); + }, + getFieldValue: (name: string) => { + return form.getFieldValue(name); + }, + getFieldsValue: () => { + return form.getFieldsValue(); + }, + resetFields() { + form.resetFields(); + } + }; + }, + [] + ); - useEffect(() => { - handleOnSelectModel(); - }, [props.selectedModel.name]); return (
= forwardRef((props, ref) => { preserve={false} style={{ padding: '16px 24px' }} clearOnDestroy={true} + onValuesChange={handleOnValuesChange} initialValues={{ replicas: 1, source: props.source, diff --git a/src/pages/llmodels/components/deploy-builtin-modal.tsx b/src/pages/llmodels/components/deploy-builtin-modal.tsx index 4bea2594..04633f95 100644 --- a/src/pages/llmodels/components/deploy-builtin-modal.tsx +++ b/src/pages/llmodels/components/deploy-builtin-modal.tsx @@ -4,7 +4,7 @@ import { createAxiosToken } from '@/hooks/use-chunk-request'; import { CloseOutlined } from '@ant-design/icons'; import { Button, Drawer } from 'antd'; import _ from 'lodash'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; import { queryCatalogItemSpec } from '../apis'; import { backendOptionsMap, @@ -308,6 +308,7 @@ const AddModal: React.FC = (props) => { if (defaultSpec.backend === backendOptionsMap.llamaBox) { setIsGGUF(true); } + console.log('values====', form.current.form.getFieldsValue()); } catch (error) { // ignore } diff --git a/src/pages/llmodels/components/deploy-modal.tsx b/src/pages/llmodels/components/deploy-modal.tsx index b6f86cc7..711c84e2 100644 --- a/src/pages/llmodels/components/deploy-modal.tsx +++ b/src/pages/llmodels/components/deploy-modal.tsx @@ -1,14 +1,16 @@ -import AlertBlockInfo from '@/components/alert-info/block'; import ModalFooter from '@/components/modal-footer'; import { PageActionType } from '@/config/types'; import { CloseOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; import { Button, Drawer } from 'antd'; -import _, { debounce } from 'lodash'; +import { debounce } from 'lodash'; import { FC, useCallback, useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; import { backendOptionsMap, modelSourceMap } from '../config'; import { FormData } from '../config/types'; +import { useCheckCompatibility } from '../hooks'; import ColumnWrapper from './column-wrapper'; +import CompatibilityAlert from './compatible-alert'; import DataForm from './data-form'; import HFModelFile from './hf-model-file'; import ModelCard from './model-card'; @@ -16,6 +18,18 @@ import SearchModel from './search-model'; import Separator from './separator'; import TitleWrapper from './title-wrapper'; +const ColWrapper = styled.div` + display: flex; + flex: 1; + maxwidth: 33.33%; +`; + +const FormWrapper = styled.div` + display: flex; + flex: 1; + maxwidth: 100%; +`; + type AddModalProps = { title: string; action: PageActionType; @@ -48,26 +62,28 @@ const AddModal: FC = (props) => { modelSourceMap.modelscope_value ]; + const { handleShowCompatibleAlert, handleUpdateWarning, warningStatus } = + useCheckCompatibility(); const form = useRef({}); const intl = useIntl(); const [selectedModel, setSelectedModel] = useState({}); const [collapsed, setCollapsed] = useState(false); const [isGGUF, setIsGGUF] = useState(props.isGGUF || false); const modelFileRef = useRef(null); - const [warningStatus, setWarningStatus] = useState<{ - show: boolean; - message: string; - }>({ - show: false, - message: '' - }); const handleSelectModelFile = useCallback((item: any) => { - form.current?.setFieldValue?.('file_name', item.fakeName); + form.current?.setFieldsValue?.({ + file_name: item.fakeName, + backend: backendOptionsMap.llamaBox + }); + if (item.fakeName) { + handleShowCompatibleAlert(item.evaluateResult); + } }, []); const handleOnSelectModel = (item: any) => { setSelectedModel(item); + form.current?.handleOnSelectModel?.(item); }; const handleSumit = () => { @@ -76,48 +92,18 @@ const AddModal: FC = (props) => { const debounceFetchModelFiles = debounce(() => { modelFileRef.current?.fetchModelFiles?.(); - }, 300); + }, 100); const handleSetIsGGUF = (flag: boolean) => { setIsGGUF(flag); if (flag) { debounceFetchModelFiles(); + } else { + handleShowCompatibleAlert(selectedModel.evaluateResult); } }; - const updateShowWarning = (backend: string) => { - const localPath = form.current?.getFieldValue?.('local_path'); - - if (source !== modelSourceMap.local_path_value || !localPath) { - return; - } - - const isBlobFile = localPath?.split('/').pop()?.includes('sha256'); - const isOllamaModel = localPath?.includes('ollama'); - const isGGUFFile = localPath.endsWith('.gguf'); - - let warningMessage = ''; - if (isBlobFile && isOllamaModel && backend === backendOptionsMap.llamaBox) { - warningMessage = ''; - } else if ( - isBlobFile && - isOllamaModel && - backend !== backendOptionsMap.llamaBox - ) { - warningMessage = 'models.form.ollama.warning'; - } else if (isGGUFFile && backend !== backendOptionsMap.llamaBox) { - warningMessage = 'models.form.backend.warning'; - } else if (!isGGUFFile && backend === backendOptionsMap.llamaBox) { - warningMessage = 'models.form.backend.warning.llamabox'; - } - - setWarningStatus({ - show: !!warningMessage, - message: warningMessage - }); - }; - - const handleBackendChange = (backend: string) => { + const handleBackendChange = async (backend: string) => { if (backend === backendOptionsMap.vllm) { setIsGGUF(false); } @@ -125,43 +111,30 @@ const AddModal: FC = (props) => { if (backend === backendOptionsMap.llamaBox) { setIsGGUF(true); } - updateShowWarning(backend); }; const handleCancel = useCallback(() => { onCancel?.(); }, [onCancel]); - useEffect(() => { - if (!_.isEmpty(selectedModel)) { - handleSelectModelFile({ fakeName: '' }); - } - }, [selectedModel]); - useEffect(() => { if (!open) { - setIsGGUF(false); - setWarningStatus({ - show: false, - message: '' - }); - form.current?.setFieldValue?.('backend', backendOptionsMap.vllm); - } else if (source === modelSourceMap.ollama_library_value) { - form.current?.setFieldValue?.('backend', backendOptionsMap.llamaBox); - setIsGGUF(true); + return; } - - if (props.deploymentType === 'modelFiles' && open) { + if (props.deploymentType === 'modelFiles') { form.current?.form?.setFieldsValue({ ...props.initialValues }); setIsGGUF(props.isGGUF || false); + } else { + form.current?.setFieldValue?.('backend', backendOptionsMap.vllm); + setIsGGUF(false); } return () => { setSelectedModel({}); }; - }, [open, source, props.isGGUF, props.initialValues, props.deploymentType]); + }, [open, props.isGGUF, props.initialValues, props.deploymentType]); return ( = (props) => { {SEARCH_SOURCE.includes(props.source) && deploymentType === 'modelList' && ( <> -
+ = (props) => { > -
-
+ + = (props) => { )} -
+ )} -
+ -
- {warningStatus.show && ( - - } - title={intl.formatMessage({ - id: 'common.text.tips' - })} - type="warning" - > - )} -
+ = (props) => { isGGUF={isGGUF} gpuOptions={props.gpuOptions} modelFileOptions={props.modelFileOptions} + handleShowCompatibleAlert={handleShowCompatibleAlert} + handleUpdateWarning={handleUpdateWarning} onBackendChange={handleBackendChange} >
-
+
); diff --git a/src/pages/llmodels/components/hf-model-file.tsx b/src/pages/llmodels/components/hf-model-file.tsx index 9a4aa8fe..d808ba99 100644 --- a/src/pages/llmodels/components/hf-model-file.tsx +++ b/src/pages/llmodels/components/hf-model-file.tsx @@ -1,10 +1,11 @@ +import { createAxiosToken } from '@/hooks/use-chunk-request'; import { convertFileSize } from '@/utils'; import { InfoCircleOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; import { Col, Empty, Row, Select, Spin, Tag, Tooltip } from 'antd'; import classNames from 'classnames'; import _ from 'lodash'; -import { +import React, { forwardRef, memo, useCallback, @@ -15,11 +16,16 @@ import { } from 'react'; import SimpleBar from 'simplebar-react'; import 'simplebar-react/dist/simplebar.min.css'; -import { queryHuggingfaceModelFiles, queryModelScopeModelFiles } from '../apis'; +import { + evaluationsModelSpec, + queryHuggingfaceModelFiles, + queryModelScopeModelFiles +} from '../apis'; import { modelSourceMap } from '../config'; import { getFileType } from '../config/file-type'; import '../style/hf-model-file.less'; import FileParts from './file-parts'; +import IncompatiableInfo from './incompatiable-info'; import TitleWrapper from './title-wrapper'; interface HFModelFileProps { @@ -37,6 +43,35 @@ const filterReg = /\.(safetensors|gguf)$/i; const includeReg = /\.(safetensors|gguf)$/i; const filterRegGGUF = /\.(gguf)$/i; +const FilePartsTag = (props: { parts: any[] }) => { + if (!props.parts || !props.parts.length) { + return null; + } + const { parts } = props; + return ( + } + > + + + + {parts.length} parts + + + + ); +}; + const HFModelFile: React.FC = forwardRef((props, ref) => { const { collapsed, modelSource } = props; const intl = useIntl(); @@ -57,9 +92,9 @@ const HFModelFile: React.FC = forwardRef((props, ref) => { } ]); const axiosTokenRef = useRef(null); + const checkTokenRef = useRef(null); const handleSelectModelFile = (item: any) => { - console.log('handleSelectModelFile', item); props.onSelectFile?.(item); setCurrent(item.path); }; @@ -188,6 +223,24 @@ const HFModelFile: React.FC = forwardRef((props, ref) => { } }; + const getEvaluateResults = useCallback(async (repoList: any[]) => { + try { + checkTokenRef.current?.cancel?.(); + checkTokenRef.current = createAxiosToken(); + const evaluations = await evaluationsModelSpec( + { + model_specs: repoList + }, + { + token: checkTokenRef.current.token + } + ); + return evaluations.results || []; + } catch (error) { + return []; + } + }, []); + const handleFetchModelFiles = async () => { if (!props.selectedModel.name) { setDataSource({ fileList: [], loading: false }); @@ -211,8 +264,32 @@ const HFModelFile: React.FC = forwardRef((props, ref) => { return sortType === 'size' ? item.size : item.path; }); - handleSelectModelFile(sortList[0]); - setDataSource({ fileList: sortList, loading: false }); + const evaluateFileList = sortList.map((item: any) => { + return { + source: modelSource, + ...(modelSource === modelSourceMap.huggingface_value + ? { + huggingface_repo_id: props.selectedModel.name, + huggingface_filename: item.fakeName + } + : { + model_scope_model_id: props.selectedModel.name, + model_scope_file_path: item.fakeName + }) + }; + }); + + const evaluationList = await getEvaluateResults(evaluateFileList); + + const resultList = _.map(sortList, (item: any, index: number) => { + return { + ...item, + evaluateResult: evaluationList[index] + }; + }); + + handleSelectModelFile(resultList[0]); + setDataSource({ fileList: resultList, loading: false }); } catch (error) { setDataSource({ fileList: [], loading: false }); handleSelectModelFile({}); @@ -269,6 +346,7 @@ const HFModelFile: React.FC = forwardRef((props, ref) => { useEffect(() => { return () => { axiosTokenRef.current?.abort?.(); + checkTokenRef.current?.cancel?.(); }; }, []); @@ -322,43 +400,23 @@ const HFModelFile: React.FC = forwardRef((props, ref) => { onKeyDown={(e) => handleOnEnter(e, item)} >
{item.path}
-
- - - {convertFileSize(item.size)} - - - {getModelQuantizationType(item)} - {item.parts && item.parts.length > 1 && ( - + + - } > - - - - {item.parts.length} parts - - - - )} + {convertFileSize(item.size)} + + {getModelQuantizationType(item)} + + +
diff --git a/src/pages/llmodels/components/hf-model-item.tsx b/src/pages/llmodels/components/hf-model-item.tsx index be45ce4a..f7b8b1ec 100644 --- a/src/pages/llmodels/components/hf-model-item.tsx +++ b/src/pages/llmodels/components/hf-model-item.tsx @@ -6,12 +6,15 @@ import { WarningOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Tag, Tooltip } from 'antd'; +import { Tooltip } from 'antd'; import classNames from 'classnames'; import dayjs from 'dayjs'; import _ from 'lodash'; +import React, { useMemo } from 'react'; import { modelSourceMap } from '../config'; +import { EvaluateResult } from '../config/types'; import '../style/hf-model-item.less'; +import IncompatiableInfo from './incompatiable-info'; interface HFModelItemProps { title: string; @@ -22,6 +25,7 @@ interface HFModelItemProps { active: boolean; source?: string; tags?: string[]; + evaluateResult?: EvaluateResult; } const warningTask = ['video']; @@ -31,15 +35,18 @@ const SUPPORTEDSOURCE = [ ]; const HFModelItem: React.FC = (props) => { + const { evaluateResult } = props; + console.log('evaluateResult', evaluateResult); const intl = useIntl(); - const isExcludeTask = () => { + const isExcludeTask = useMemo(() => { if (!props.task) { return false; } return _.some(warningTask, (item: string) => { return props.task?.toLowerCase().includes(item); }); - }; + }, [props.task]); + return (
= (props) => { style={{ color: 'var(--ant-color-text-tertiary)' }} /> {props.title} - {isExcludeTask() && ( + {isExcludeTask && ( @@ -62,43 +69,18 @@ const HFModelItem: React.FC = (props) => { )}
- {SUPPORTEDSOURCE.includes(props.source || '') ? ( -
- - {dayjs().to( - dayjs(dayjs(props.updatedAt).format('YYYY-MM-DD HH:mm:ss')) - )} - - - - {props.likes} - - - - {formatNumber(props.downloads)} - -
- ) : ( -
-
- {_.map(props.tags, (tag: string, index: string) => { - return ( - - - {tag} - - - ); - })} -
-
- )} +
+ {dayjs().to(dayjs(props.updatedAt))} + + + {props.likes} + + + + {formatNumber(props.downloads)} + +
+ {}
); diff --git a/src/pages/llmodels/components/incompatiable-info.tsx b/src/pages/llmodels/components/incompatiable-info.tsx new file mode 100644 index 00000000..686f1136 --- /dev/null +++ b/src/pages/llmodels/components/incompatiable-info.tsx @@ -0,0 +1,80 @@ +import OverlayScroller from '@/components/overlay-scroller'; +import { WarningOutlined } from '@ant-design/icons'; +import { Tag, Tooltip } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; +import { EvaluateResult } from '../config/types'; + +interface IncompatiableInfoProps { + data?: EvaluateResult; +} + +const CompatibleTag = styled(Tag)` + border-radius: 4px; + margin-right: 0; +`; + +const IncompatibleInfo = styled.div` + display: flex; + flex-direction: column; + ul { + margin: 0; + font-size: var(--font-size-small); + padding: 0; + padding-left: 16px; + color: var(--color-white-secondary); + list-style: none; + li { + position: relative; + } + li::before { + position: absolute; + content: ''; + display: inline-block; + width: 6px; + height: 6px; + left: -14px; + top: 8px; + border-radius: 50%; + background-color: var(--color-white-secondary); + } + } +`; + +const SMTitle = styled.div<{ $isTitle?: boolean }>` + font-weight: ${(props) => (props.$isTitle ? 'bold' : 'normal')}; + font-size: var(--font-size-small); +`; +const IncompatiableInfo: React.FC = (props) => { + const { data } = props; + if (data?.compatible) { + return null; + } + return ( + + + + {data?.compatibility_messages} + + {!!data?.scheduling_messages?.length && ( +
    + {data?.scheduling_messages.map((item, index) => ( +
  • {item}
  • + ))} +
+ )} +
+ + } + > + } color="warning"> + Incompatible + +
+ ); +}; + +export default IncompatiableInfo; diff --git a/src/pages/llmodels/components/model-card.tsx b/src/pages/llmodels/components/model-card.tsx index 2bca6ae0..0f979b78 100644 --- a/src/pages/llmodels/components/model-card.tsx +++ b/src/pages/llmodels/components/model-card.tsx @@ -88,6 +88,12 @@ const ModelCard: React.FC<{ } }, []); + const handleOnCollapse = (readmeText: any) => { + if (!readmeText) { + onCollapse(false); + } + }; + const loadConfig = useCallback(async (repo: string, sha: string) => { try { loadConfigTokenRef.current?.abort?.(); @@ -145,13 +151,14 @@ const ModelCard: React.FC<{ const newReadme = removeMetadata(readme); setReadmeText(newReadme); + handleOnCollapse(newReadme); const isGGUF = modelcard.tags?.includes('gguf'); - console.log('modelData++++++++++++', isGGUF); setIsGGUF(isGGUF); setIsGGUFModel(isGGUF); } catch (error) { setModelData(null); setReadmeText(null); + handleOnCollapse(null); setIsGGUF(false); setIsGGUFModel(false); } @@ -189,6 +196,7 @@ const ModelCard: React.FC<{ name: `${data.Data?.Path}/${data.Data?.Name}` }); setReadmeText(data?.Data?.ReadMeContent); + handleOnCollapse(data?.Data?.ReadMeContent); const isGGUF = some( data?.Data?.Tags, (tag: string) => tag?.indexOf('gguf') > -1 @@ -198,6 +206,7 @@ const ModelCard: React.FC<{ } catch (error) { setModelData(null); setReadmeText(null); + handleOnCollapse(null); setIsGGUF(false); setIsGGUFModel(false); } @@ -207,6 +216,7 @@ const ModelCard: React.FC<{ if (!props.selectedModel.name) { setModelData(null); setReadmeText(null); + handleOnCollapse(null); return; } requestToken.current?.cancel?.(); @@ -267,32 +277,23 @@ const ModelCard: React.FC<{ return null; }; - const generateModeScopeImgLink = useCallback( - (imgSrc: string) => { - if (!imgSrc) { - return ''; - } - if (modelSource === modelSourceMap.modelscope_value) { - return `https://modelscope.cn/api/v1/models/${modelData?.name}/repo?Revision=${modelData?.Revision}&View=true&FilePath=${imgSrc}`; - } - if (modelSource === modelSourceMap.huggingface_value) { - return `https://huggingface.co/${modelData?.id}/resolve/main/${imgSrc}`; - } + const generateModeScopeImgLink = (imgSrc: string) => { + if (!imgSrc) { return ''; - }, - [modelData, modelSource] - ); + } + if (modelSource === modelSourceMap.modelscope_value) { + return `https://modelscope.cn/api/v1/models/${modelData?.name}/repo?Revision=${modelData?.Revision}&View=true&FilePath=${imgSrc}`; + } + if (modelSource === modelSourceMap.huggingface_value) { + return `https://huggingface.co/${modelData?.id}/resolve/main/${imgSrc}`; + } + return ''; + }; useEffect(() => { getModelCardData(); }, [props.selectedModel.name]); - useEffect(() => { - if (!readmeText) { - onCollapse(false); - } - }, [readmeText]); - useEffect(() => { return () => { requestToken.current?.cancel?.(); diff --git a/src/pages/llmodels/components/search-input.tsx b/src/pages/llmodels/components/search-input.tsx index d19ef601..31b9a8cf 100644 --- a/src/pages/llmodels/components/search-input.tsx +++ b/src/pages/llmodels/components/search-input.tsx @@ -33,19 +33,17 @@ const SearchInput: React.FC<{ { source: modelSourceValueMap[modelSource] } )} prefix={ - <> - - + } > ); }; -export default React.memo(SearchInput); +export default SearchInput; diff --git a/src/pages/llmodels/components/search-model.tsx b/src/pages/llmodels/components/search-model.tsx index 67358069..882f6ff7 100644 --- a/src/pages/llmodels/components/search-model.tsx +++ b/src/pages/llmodels/components/search-model.tsx @@ -1,9 +1,20 @@ -import { BulbOutlined, QuestionCircleOutlined } from '@ant-design/icons'; +import { createAxiosToken } from '@/hooks/use-chunk-request'; +import { QuestionCircleOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; import { Checkbox, Select, Tooltip } from 'antd'; import _ from 'lodash'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { queryHuggingfaceModels, queryModelScopeModels } from '../apis'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState +} from 'react'; +import { + evaluationsModelSpec, + queryHuggingfaceModels, + queryModelScopeModels +} from '../apis'; import { HuggingFaceTaskMap, ModelScopeSortType, @@ -44,6 +55,7 @@ const SearchModel: React.FC = (props) => { const [current, setCurrent] = useState(''); const cacheRepoOptions = useRef([]); const axiosTokenRef = useRef(null); + const checkTokenRef = useRef(null); const searchInputRef = useRef(''); const filterGGUFRef = useRef(); const filterTaskRef = useRef(''); @@ -134,6 +146,24 @@ const SearchModel: React.FC = (props) => { } }, []); + const getEvaluateResults = useCallback(async (repoList: any[]) => { + try { + checkTokenRef.current?.cancel?.(); + checkTokenRef.current = createAxiosToken(); + const evaluations = await evaluationsModelSpec( + { + model_specs: repoList + }, + { + token: checkTokenRef.current?.token + } + ); + return evaluations.results || []; + } catch (error) { + return []; + } + }, []); + const handleOnSearchRepo = useCallback( async (sortType?: string) => { if (!SUPPORTEDSOURCE.includes(modelSource)) { @@ -156,6 +186,26 @@ const SearchModel: React.FC = (props) => { list = await getModelsFromModelscope(sort); } cacheRepoOptions.current = list; + const repoList = list.map((item) => { + return { + source: modelSource, + ...(modelSource === modelSourceMap.huggingface_value + ? { + huggingface_repo_id: item.name + } + : { + model_scope_model_id: item.name + }) + }; + }); + const evaluations = await getEvaluateResults(repoList); + list = list.map((item, index) => { + return { + ...item, + evaluateResult: evaluations[index] + }; + }); + console.log('list:', evaluations); setDataSource({ repoOptions: list, loading: false, @@ -176,20 +226,14 @@ const SearchModel: React.FC = (props) => { cacheRepoOptions.current = []; } }, - [dataSource] + [dataSource.sortType, modelSource] ); const handleSearchInputChange = useCallback((e: any) => { searchInputRef.current = e.target.value; console.log('change:', searchInputRef.current); }, []); - const handlerSearchModels = useCallback( - async (e: any) => { - setTimeout(() => { - handleOnSearchRepo(); - }, 100); - }, - [handleOnSearchRepo] - ); + + const handlerSearchModels = _.debounce(() => handleOnSearchRepo(), 100); const handleOnOpen = () => { if ( @@ -225,6 +269,28 @@ const SearchModel: React.FC = (props) => { handleOnSearchRepo(); }, []); + const renderGGUFTips = useMemo(() => { + return ( + +
  • {intl.formatMessage({ id: 'models.search.gguf.tips' })}
  • +
  • {intl.formatMessage({ id: 'models.search.vllm.tips' })}
  • +
  • + {intl.formatMessage({ + id: 'models.search.voxbox.tips' + })} +
  • + + } + > + GGUF + +
    + ); + }, [intl]); + const renderHFSearch = () => { return ( <> @@ -254,27 +320,7 @@ const SearchModel: React.FC = (props) => { className="m-r-5" checked={filterGGUFRef.current} > - -
  • - {intl.formatMessage({ id: 'models.search.gguf.tips' })} -
  • -
  • - {intl.formatMessage({ id: 'models.search.vllm.tips' })} -
  • -
  • - {intl.formatMessage({ - id: 'models.search.voxbox.tips' - })} -
  • - - } - > - GGUF - -
    + {renderGGUFTips}