chore: compatibility api

This commit is contained in:
jialin
2025-03-31 16:21:40 +08:00
parent f9469a1ea3
commit bd6c689d91
24 changed files with 848 additions and 374 deletions
-4
View File
@@ -80,10 +80,6 @@ export default defineConfig({
clickToComponent: {}, clickToComponent: {},
antd: { antd: {
style: 'less' style: 'less'
// configProvider: {
// componentSize: 'large',
// theme
// }
}, },
hash: true, hash: true,
access: {}, access: {},
-4
View File
@@ -21,10 +21,6 @@
background-color: var(--ant-color-warning-bg); background-color: var(--ant-color-warning-bg);
} }
.content {
word-break: break-all;
}
.title { .title {
position: absolute; position: absolute;
left: 0; left: 0;
+21 -2
View File
@@ -2,6 +2,8 @@ import { WarningFilled } from '@ant-design/icons';
import { Typography } from 'antd'; import { Typography } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import OverlayScroller from '../overlay-scroller';
import './block.less'; import './block.less';
interface AlertInfoProps { interface AlertInfoProps {
type: 'danger' | 'warning'; type: 'danger' | 'warning';
@@ -13,8 +15,22 @@ interface AlertInfoProps {
title: React.ReactNode; 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<AlertInfoProps> = (props) => { const AlertInfo: React.FC<AlertInfoProps> = (props) => {
const { message, type, rows = 1, ellipsis, style } = props; const { message, type, rows = 1, ellipsis, style, title } = props;
return ( return (
<> <>
@@ -34,7 +50,10 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
<div className={classNames('title', type)}> <div className={classNames('title', type)}>
<WarningFilled className={classNames('info-icon', type)} /> <WarningFilled className={classNames('info-icon', type)} />
</div> </div>
<span className="content">{message}</span> {title && <TitleWrapper>{title}</TitleWrapper>}
<OverlayScroller maxHeight={80}>
<ContentWrapper $hasTitle={!!title}>{message}</ContentWrapper>
</OverlayScroller>
</Typography.Paragraph> </Typography.Paragraph>
</div> </div>
) : null} ) : null}
+34
View File
@@ -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<any> = ({ children, maxHeight, theme }) => {
const scroller = React.useRef<any>(null);
const { initialize } = useOverlayScroller({
options: {
theme: theme || 'os-theme-light'
}
});
React.useEffect(() => {
if (scroller.current) {
initialize(scroller.current);
}
}, []);
return (
<Wrapper ref={scroller} $maxHeight={maxHeight || '100%'} hidden={false}>
{children}
</Wrapper>
);
};
export default OverlayScroller;
+1 -1
View File
@@ -64,7 +64,7 @@ export default {
}, },
token: { token: {
fontFamily: 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)', colorText: 'rgba(0,0,0,1)',
colorPrimary: '#007BFF', colorPrimary: '#007BFF',
colorSuccess: '#54cc98', colorSuccess: '#54cc98',
+19 -3
View File
@@ -5,6 +5,8 @@ import qs from 'query-string';
import { import {
CatalogItem, CatalogItem,
CatalogSpec, CatalogSpec,
EvaluateResult,
EvaluateSpec,
FormData, FormData,
GPUListItem, GPUListItem,
ListItem, ListItem,
@@ -16,6 +18,8 @@ export const MODELS_API = '/models';
export const MODEL_INSTANCE_API = '/model-instances'; export const MODEL_INSTANCE_API = '/model-instances';
export const MODEL_EVALUATIONS = '/model-evaluations';
const setProxyUrl = (url: string) => { const setProxyUrl = (url: string) => {
return `/proxy?url=${encodeURIComponent(url)}`; return `/proxy?url=${encodeURIComponent(url)}`;
}; };
@@ -189,7 +193,7 @@ export async function queryModelScopeModels(
...params, ...params,
...Criterion, ...Criterion,
Name: `${params.Name}`, Name: `${params.Name}`,
PageSize: 100, PageSize: 10,
PageNumber: 1 PageNumber: 1
}) })
}); });
@@ -251,7 +255,7 @@ export async function queryHuggingfaceModels(
for await (const model of listModels({ for await (const model of listModels({
...params, ...params,
...options, ...options,
limit: 100, limit: 10,
additionalFields: ['sha', 'tags'], additionalFields: ['sha', 'tags'],
fetch(_url: string, config: any) { fetch(_url: string, config: any) {
const url = params.search.sort const url = params.search.sort
@@ -289,7 +293,6 @@ export async function queryHuggingfaceModelFiles(
signal: options?.signal signal: options?.signal
}); });
} catch (error) { } catch (error) {
console.log('queryHuggingfaceModels error===', error);
// ignore // ignore
return []; 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
});
}
@@ -1,6 +1,5 @@
import useOverlayScroller from '@/hooks/use-overlay-scroller'; import useOverlayScroller from '@/hooks/use-overlay-scroller';
import React from 'react'; import React from 'react';
import 'simplebar-react/dist/simplebar.min.css';
import '../style/column-wrapper.less'; import '../style/column-wrapper.less';
const ColumnWrapper: React.FC<any> = ({ const ColumnWrapper: React.FC<any> = ({
@@ -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<CompatibilityAlertProps> = (props) => {
const { warningStatus } = props;
const { title, show, message, isHtml } = warningStatus;
const renderMessage = useMemo(() => {
if (!message || !show) {
return '';
}
if (isHtml) {
return (
<span
dangerouslySetInnerHTML={{
__html: message as string
}}
></span>
);
}
if (typeof message === 'string') {
return message;
}
if (isArray(message)) {
return (
<MessageWrapper>
{message.map((item, index) => (
<div key={index}>{item}</div>
))}
</MessageWrapper>
);
}
return '';
}, [message, show]);
return (
show && (
<DivWrapper>
<AlertBlockInfo
ellipsis={false}
message={renderMessage}
title={title}
type="warning"
></AlertBlockInfo>
</DivWrapper>
)
);
};
export default CompatibilityAlert;
+140 -71
View File
@@ -6,23 +6,25 @@ import TooltipList from '@/components/tooltip-list';
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 { createAxiosToken } from '@/hooks/use-chunk-request';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form, Typography } from 'antd'; import { Form, Typography } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { import React, {
forwardRef, forwardRef,
useCallback,
useEffect,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useRef, useRef,
useState useState
} from 'react'; } from 'react';
import { evaluationsModelSpec } from '../apis';
import { import {
HuggingFaceTaskMap, HuggingFaceTaskMap,
ModelscopeTaskMap, ModelscopeTaskMap,
backendOptionsMap, backendOptionsMap,
backendTipsList, backendTipsList,
excludeFields,
getSourceRepoConfigValue,
localPathTipsList, localPathTipsList,
modelSourceMap, modelSourceMap,
modelTaskMap, modelTaskMap,
@@ -30,7 +32,7 @@ import {
sourceOptions sourceOptions
} from '../config'; } from '../config';
import { identifyModelTask } from '../config/audio-catalog'; import { identifyModelTask } from '../config/audio-catalog';
import { FormData } from '../config/types'; import { EvaluateResult, FormData } from '../config/types';
import AdvanceConfig from './advance-config'; import AdvanceConfig from './advance-config';
interface DataFormProps { interface DataFormProps {
@@ -48,12 +50,19 @@ interface DataFormProps {
sourceList?: Global.BaseOption<string>[]; sourceList?: Global.BaseOption<string>[];
gpuOptions: any[]; gpuOptions: any[];
modelFileOptions?: 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; onSizeChange?: (val: number) => void;
onQuantizationChange?: (val: string) => void; onQuantizationChange?: (val: string) => void;
onSourceChange?: (value: string) => void; onSourceChange?: (value: string) => void;
onOk: (values: FormData) => void; onOk: (values: FormData) => void;
onBackendChange?: (value: string) => void; onBackendChange?: (value: string) => void;
fields?: string[];
} }
const SEARCH_SOURCE = [ const SEARCH_SOURCE = [
@@ -75,6 +84,8 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
sizeOptions = [], sizeOptions = [],
quantizationOptions = [], quantizationOptions = [],
fields = ['source'], fields = ['source'],
handleUpdateWarning,
handleShowCompatibleAlert,
onSourceChange, onSourceChange,
onOk onOk
} = props; } = props;
@@ -88,75 +99,55 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
text2speech: false, text2speech: false,
speech2text: false speech2text: false
}); });
const checkTokenRef = useRef<any>(null);
const localPathCache = useRef<string>(''); const localPathCache = useRef<string>('');
const handleSumit = () => { const handleSumit = () => {
form.submit(); form.submit();
}; };
useImperativeHandle( const handleRecognizeAudioModel = (selectModel: any) => {
ref, const modelTaskType = identifyModelTask(props.source, selectModel.name);
() => {
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 modelTask = const modelTask =
HuggingFaceTaskMap.audio.includes(props.selectedModel.task) || HuggingFaceTaskMap.audio.includes(selectModel.task) ||
ModelscopeTaskMap.audio.includes(props.selectedModel.task) ModelscopeTaskMap.audio.includes(selectModel.task)
? modelTaskMap.audio ? modelTaskMap.audio
: ''; : '';
setModelTask({ const modelTaskData = {
value: props.selectedModel.task, value: selectModel.task,
type: modelTaskType || modelTask, type: modelTaskType || modelTask,
text2speech: text2speech:
HuggingFaceTaskMap[modelTaskMap.textToSpeech] === HuggingFaceTaskMap[modelTaskMap.textToSpeech] === selectModel.task ||
props.selectedModel.task || ModelscopeTaskMap[modelTaskMap.textToSpeech] === selectModel.task,
ModelscopeTaskMap[modelTaskMap.textToSpeech] ===
props.selectedModel.task,
speech2text: speech2text:
HuggingFaceTaskMap[modelTaskMap.speechToText] === HuggingFaceTaskMap[modelTaskMap.speechToText] === selectModel.task ||
props.selectedModel.task || ModelscopeTaskMap[modelTaskMap.speechToText] === selectModel.task
ModelscopeTaskMap[modelTaskMap.speechToText] === };
props.selectedModel.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)) { if (SEARCH_SOURCE.includes(props.source)) {
form.setFieldsValue({ form.setFieldsValue({
repo_id: props.selectedModel.name, repo_id: selectModel.name,
name: name name: name,
backend:
modelTaskData.type === modelTaskMap.audio
? backendOptionsMap.voxBox
: form.getFieldValue('backend')
}); });
} else { } else {
form.setFieldsValue({ form.setFieldsValue({
ollama_library_model_name: props.selectedModel.name, ollama_library_model_name: selectModel.name,
name: name name: name
}); });
} }
@@ -166,8 +157,52 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
localPathCache.current = form.getFieldValue('local_path'); 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 handleLocalPathBlur = (e: any) => {
const value = e.target.value; const value = e.target.value;
console.log('handleLocalPathBlur:', e, localPathCache.current);
if (value === localPathCache.current && value) { if (value === localPathCache.current && value) {
return; return;
} }
@@ -177,8 +212,9 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
if (!isEndwithGGUF && !isBlobFile) { if (!isEndwithGGUF && !isBlobFile) {
backend = backendOptionsMap.vllm; backend = backendOptionsMap.vllm;
} }
props.onBackendChange?.(backend);
form.setFieldValue('backend', backend); form.setFieldValue('backend', backend);
handleBackendChangeHook();
props.onBackendChange?.(backend);
}; };
const renderHuggingfaceFields = () => { const renderHuggingfaceFields = () => {
@@ -393,17 +429,21 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
} }
}; };
const handleBackendChange = useCallback((val: string) => { const handleBackendChange = async (val: string) => {
const updates = {
backend_version: ''
};
if (val === backendOptionsMap.llamaBox) { if (val === backendOptionsMap.llamaBox) {
form.setFieldsValue({ Object.assign(updates, {
distributed_inference_across_workers: true, distributed_inference_across_workers: true,
cpu_offloading: true cpu_offloading: true
}); });
} }
form.setFieldValue('backend_version', ''); form.setFieldsValue(updates);
handleSetGPUIds(val); handleSetGPUIds(val);
handleBackendChangeHook();
props.onBackendChange?.(val); props.onBackendChange?.(val);
}, []); };
const generateGPUIds = (data: FormData) => { const generateGPUIds = (data: FormData) => {
const gpu_ids = _.get(data, 'gpu_selector.gpu_ids', []); const gpu_ids = _.get(data, 'gpu_selector.gpu_ids', []);
@@ -452,21 +492,49 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
onSourceChange?.(val); onSourceChange?.(val);
}; };
useEffect(() => { const handleOnValuesChange = async (changedValues: any, allValues: any) => {
if (action === PageAction.EDIT) return; const keys = Object.keys(changedValues);
if (modelTask.type === modelTaskMap.audio) { const isExcludeField = keys.some((key) => excludeFields.includes(key));
form.setFieldValue('backend', backendOptionsMap.voxBox); if (
} else { !isExcludeField &&
form.setFieldValue( !_.has(changedValues, 'backend') &&
'backend', !_.has(changedValues, 'local_path')
isGGUF ? backendOptionsMap.llamaBox : backendOptionsMap.vllm ) {
); 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 ( return (
<Form <Form
name="deployModel" name="deployModel"
@@ -475,6 +543,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
preserve={false} preserve={false}
style={{ padding: '16px 24px' }} style={{ padding: '16px 24px' }}
clearOnDestroy={true} clearOnDestroy={true}
onValuesChange={handleOnValuesChange}
initialValues={{ initialValues={{
replicas: 1, replicas: 1,
source: props.source, source: props.source,
@@ -4,7 +4,7 @@ import { createAxiosToken } from '@/hooks/use-chunk-request';
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { Button, Drawer } from 'antd'; import { Button, Drawer } from 'antd';
import _ from 'lodash'; 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 { queryCatalogItemSpec } from '../apis';
import { import {
backendOptionsMap, backendOptionsMap,
@@ -308,6 +308,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
if (defaultSpec.backend === backendOptionsMap.llamaBox) { if (defaultSpec.backend === backendOptionsMap.llamaBox) {
setIsGGUF(true); setIsGGUF(true);
} }
console.log('values====', form.current.form.getFieldsValue());
} catch (error) { } catch (error) {
// ignore // ignore
} }
+47 -102
View File
@@ -1,14 +1,16 @@
import AlertBlockInfo from '@/components/alert-info/block';
import ModalFooter from '@/components/modal-footer'; import ModalFooter from '@/components/modal-footer';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Drawer } from 'antd'; import { Button, Drawer } from 'antd';
import _, { debounce } from 'lodash'; import { debounce } from 'lodash';
import { FC, useCallback, useEffect, useRef, useState } from 'react'; import { FC, useCallback, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { backendOptionsMap, modelSourceMap } from '../config'; import { backendOptionsMap, modelSourceMap } from '../config';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import { useCheckCompatibility } from '../hooks';
import ColumnWrapper from './column-wrapper'; import ColumnWrapper from './column-wrapper';
import CompatibilityAlert from './compatible-alert';
import DataForm from './data-form'; import DataForm from './data-form';
import HFModelFile from './hf-model-file'; import HFModelFile from './hf-model-file';
import ModelCard from './model-card'; import ModelCard from './model-card';
@@ -16,6 +18,18 @@ import SearchModel from './search-model';
import Separator from './separator'; import Separator from './separator';
import TitleWrapper from './title-wrapper'; 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 = { type AddModalProps = {
title: string; title: string;
action: PageActionType; action: PageActionType;
@@ -48,26 +62,28 @@ const AddModal: FC<AddModalProps> = (props) => {
modelSourceMap.modelscope_value modelSourceMap.modelscope_value
]; ];
const { handleShowCompatibleAlert, handleUpdateWarning, warningStatus } =
useCheckCompatibility();
const form = useRef<any>({}); const form = useRef<any>({});
const intl = useIntl(); const intl = useIntl();
const [selectedModel, setSelectedModel] = useState<any>({}); const [selectedModel, setSelectedModel] = useState<any>({});
const [collapsed, setCollapsed] = useState<boolean>(false); const [collapsed, setCollapsed] = useState<boolean>(false);
const [isGGUF, setIsGGUF] = useState<boolean>(props.isGGUF || false); const [isGGUF, setIsGGUF] = useState<boolean>(props.isGGUF || false);
const modelFileRef = useRef<any>(null); const modelFileRef = useRef<any>(null);
const [warningStatus, setWarningStatus] = useState<{
show: boolean;
message: string;
}>({
show: false,
message: ''
});
const handleSelectModelFile = useCallback((item: any) => { 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) => { const handleOnSelectModel = (item: any) => {
setSelectedModel(item); setSelectedModel(item);
form.current?.handleOnSelectModel?.(item);
}; };
const handleSumit = () => { const handleSumit = () => {
@@ -76,48 +92,18 @@ const AddModal: FC<AddModalProps> = (props) => {
const debounceFetchModelFiles = debounce(() => { const debounceFetchModelFiles = debounce(() => {
modelFileRef.current?.fetchModelFiles?.(); modelFileRef.current?.fetchModelFiles?.();
}, 300); }, 100);
const handleSetIsGGUF = (flag: boolean) => { const handleSetIsGGUF = (flag: boolean) => {
setIsGGUF(flag); setIsGGUF(flag);
if (flag) { if (flag) {
debounceFetchModelFiles(); debounceFetchModelFiles();
} else {
handleShowCompatibleAlert(selectedModel.evaluateResult);
} }
}; };
const updateShowWarning = (backend: string) => { const handleBackendChange = async (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) => {
if (backend === backendOptionsMap.vllm) { if (backend === backendOptionsMap.vllm) {
setIsGGUF(false); setIsGGUF(false);
} }
@@ -125,43 +111,30 @@ const AddModal: FC<AddModalProps> = (props) => {
if (backend === backendOptionsMap.llamaBox) { if (backend === backendOptionsMap.llamaBox) {
setIsGGUF(true); setIsGGUF(true);
} }
updateShowWarning(backend);
}; };
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
onCancel?.(); onCancel?.();
}, [onCancel]); }, [onCancel]);
useEffect(() => {
if (!_.isEmpty(selectedModel)) {
handleSelectModelFile({ fakeName: '' });
}
}, [selectedModel]);
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
setIsGGUF(false); return;
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);
} }
if (props.deploymentType === 'modelFiles') {
if (props.deploymentType === 'modelFiles' && open) {
form.current?.form?.setFieldsValue({ form.current?.form?.setFieldsValue({
...props.initialValues ...props.initialValues
}); });
setIsGGUF(props.isGGUF || false); setIsGGUF(props.isGGUF || false);
} else {
form.current?.setFieldValue?.('backend', backendOptionsMap.vllm);
setIsGGUF(false);
} }
return () => { return () => {
setSelectedModel({}); setSelectedModel({});
}; };
}, [open, source, props.isGGUF, props.initialValues, props.deploymentType]); }, [open, props.isGGUF, props.initialValues, props.deploymentType]);
return ( return (
<Drawer <Drawer
@@ -197,13 +170,7 @@ const AddModal: FC<AddModalProps> = (props) => {
{SEARCH_SOURCE.includes(props.source) && {SEARCH_SOURCE.includes(props.source) &&
deploymentType === 'modelList' && ( deploymentType === 'modelList' && (
<> <>
<div <ColWrapper>
style={{
display: 'flex',
flex: 1,
maxWidth: '33.33%'
}}
>
<ColumnWrapper> <ColumnWrapper>
<SearchModel <SearchModel
modelSource={props.source} modelSource={props.source}
@@ -211,14 +178,8 @@ const AddModal: FC<AddModalProps> = (props) => {
></SearchModel> ></SearchModel>
</ColumnWrapper> </ColumnWrapper>
<Separator></Separator> <Separator></Separator>
</div> </ColWrapper>
<div <ColWrapper>
style={{
display: 'flex',
flex: 1,
maxWidth: '33.33%'
}}
>
<ColumnWrapper> <ColumnWrapper>
<ModelCard <ModelCard
selectedModel={selectedModel} selectedModel={selectedModel}
@@ -238,35 +199,17 @@ const AddModal: FC<AddModalProps> = (props) => {
)} )}
</ColumnWrapper> </ColumnWrapper>
<Separator></Separator> <Separator></Separator>
</div> </ColWrapper>
</> </>
)} )}
<div style={{ display: 'flex', flex: 1, maxWidth: '100%' }}> <FormWrapper>
<ColumnWrapper <ColumnWrapper
paddingBottom={warningStatus.show ? 125 : 50} paddingBottom={warningStatus.show ? 125 : 50}
footer={ footer={
<> <>
<div style={{ paddingInline: 12 }}> <CompatibilityAlert
{warningStatus.show && ( warningStatus={warningStatus}
<AlertBlockInfo ></CompatibilityAlert>
ellipsis={false}
message={
<span
style={{}}
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
id: warningStatus.message
})
}}
></span>
}
title={intl.formatMessage({
id: 'common.text.tips'
})}
type="warning"
></AlertBlockInfo>
)}
</div>
<ModalFooter <ModalFooter
onCancel={handleCancel} onCancel={handleCancel}
onOk={handleSumit} onOk={handleSumit}
@@ -296,11 +239,13 @@ const AddModal: FC<AddModalProps> = (props) => {
isGGUF={isGGUF} isGGUF={isGGUF}
gpuOptions={props.gpuOptions} gpuOptions={props.gpuOptions}
modelFileOptions={props.modelFileOptions} modelFileOptions={props.modelFileOptions}
handleShowCompatibleAlert={handleShowCompatibleAlert}
handleUpdateWarning={handleUpdateWarning}
onBackendChange={handleBackendChange} onBackendChange={handleBackendChange}
></DataForm> ></DataForm>
</> </>
</ColumnWrapper> </ColumnWrapper>
</div> </FormWrapper>
</div> </div>
</Drawer> </Drawer>
); );
+98 -40
View File
@@ -1,10 +1,11 @@
import { createAxiosToken } from '@/hooks/use-chunk-request';
import { convertFileSize } from '@/utils'; import { convertFileSize } from '@/utils';
import { InfoCircleOutlined } from '@ant-design/icons'; import { InfoCircleOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Col, Empty, Row, Select, Spin, Tag, Tooltip } from 'antd'; import { Col, Empty, Row, Select, Spin, Tag, Tooltip } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import { import React, {
forwardRef, forwardRef,
memo, memo,
useCallback, useCallback,
@@ -15,11 +16,16 @@ import {
} from 'react'; } from 'react';
import SimpleBar from 'simplebar-react'; import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css'; import 'simplebar-react/dist/simplebar.min.css';
import { queryHuggingfaceModelFiles, queryModelScopeModelFiles } from '../apis'; import {
evaluationsModelSpec,
queryHuggingfaceModelFiles,
queryModelScopeModelFiles
} from '../apis';
import { modelSourceMap } from '../config'; import { modelSourceMap } from '../config';
import { getFileType } from '../config/file-type'; import { getFileType } from '../config/file-type';
import '../style/hf-model-file.less'; import '../style/hf-model-file.less';
import FileParts from './file-parts'; import FileParts from './file-parts';
import IncompatiableInfo from './incompatiable-info';
import TitleWrapper from './title-wrapper'; import TitleWrapper from './title-wrapper';
interface HFModelFileProps { interface HFModelFileProps {
@@ -37,6 +43,35 @@ const filterReg = /\.(safetensors|gguf)$/i;
const includeReg = /\.(safetensors|gguf)$/i; const includeReg = /\.(safetensors|gguf)$/i;
const filterRegGGUF = /\.(gguf)$/i; const filterRegGGUF = /\.(gguf)$/i;
const FilePartsTag = (props: { parts: any[] }) => {
if (!props.parts || !props.parts.length) {
return null;
}
const { parts } = props;
return (
<Tooltip
overlayInnerStyle={{
width: 180,
padding: 0
}}
title={<FileParts fileList={parts}></FileParts>}
>
<Tag
className="tag-item"
color="purple"
style={{
marginRight: 0
}}
>
<span style={{ opacity: 1 }}>
<InfoCircleOutlined className="m-r-5" />
{parts.length} parts
</span>
</Tag>
</Tooltip>
);
};
const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => { const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
const { collapsed, modelSource } = props; const { collapsed, modelSource } = props;
const intl = useIntl(); const intl = useIntl();
@@ -57,9 +92,9 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
} }
]); ]);
const axiosTokenRef = useRef<any>(null); const axiosTokenRef = useRef<any>(null);
const checkTokenRef = useRef<any>(null);
const handleSelectModelFile = (item: any) => { const handleSelectModelFile = (item: any) => {
console.log('handleSelectModelFile', item);
props.onSelectFile?.(item); props.onSelectFile?.(item);
setCurrent(item.path); setCurrent(item.path);
}; };
@@ -188,6 +223,24 @@ const HFModelFile: React.FC<HFModelFileProps> = 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 () => { const handleFetchModelFiles = async () => {
if (!props.selectedModel.name) { if (!props.selectedModel.name) {
setDataSource({ fileList: [], loading: false }); setDataSource({ fileList: [], loading: false });
@@ -211,8 +264,32 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
return sortType === 'size' ? item.size : item.path; return sortType === 'size' ? item.size : item.path;
}); });
handleSelectModelFile(sortList[0]); const evaluateFileList = sortList.map((item: any) => {
setDataSource({ fileList: sortList, loading: false }); 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) { } catch (error) {
setDataSource({ fileList: [], loading: false }); setDataSource({ fileList: [], loading: false });
handleSelectModelFile({}); handleSelectModelFile({});
@@ -269,6 +346,7 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
useEffect(() => { useEffect(() => {
return () => { return () => {
axiosTokenRef.current?.abort?.(); axiosTokenRef.current?.abort?.();
checkTokenRef.current?.cancel?.();
}; };
}, []); }, []);
@@ -322,43 +400,23 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
onKeyDown={(e) => handleOnEnter(e, item)} onKeyDown={(e) => handleOnEnter(e, item)}
> >
<div className="title">{item.path}</div> <div className="title">{item.path}</div>
<div className="tags"> <div className="tags flex-between">
<Tag <span className="flex-center gap-8">
className="tag-item" <Tag
color="green" className="tag-item"
style={{ color="green"
marginRight: 0 style={{
}} marginRight: 0
>
<span style={{ opacity: 0.65 }}>
{convertFileSize(item.size)}
</span>
</Tag>
{getModelQuantizationType(item)}
{item.parts && item.parts.length > 1 && (
<Tooltip
overlayInnerStyle={{
width: 180,
padding: 0
}} }}
title={
<FileParts fileList={item.parts}></FileParts>
}
> >
<Tag {convertFileSize(item.size)}
className="tag-item" </Tag>
color="purple" {getModelQuantizationType(item)}
style={{ <FilePartsTag parts={item.parts}></FilePartsTag>
marginRight: 0 </span>
}} <IncompatiableInfo
> data={item.evaluateResult}
<span style={{ opacity: 1 }}> ></IncompatiableInfo>
<InfoCircleOutlined className="m-r-5" />
{item.parts.length} parts
</span>
</Tag>
</Tooltip>
)}
</div> </div>
</div> </div>
</Col> </Col>
+23 -41
View File
@@ -6,12 +6,15 @@ import {
WarningOutlined WarningOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Tag, Tooltip } from 'antd'; import { Tooltip } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
import React, { useMemo } from 'react';
import { modelSourceMap } from '../config'; import { modelSourceMap } from '../config';
import { EvaluateResult } from '../config/types';
import '../style/hf-model-item.less'; import '../style/hf-model-item.less';
import IncompatiableInfo from './incompatiable-info';
interface HFModelItemProps { interface HFModelItemProps {
title: string; title: string;
@@ -22,6 +25,7 @@ interface HFModelItemProps {
active: boolean; active: boolean;
source?: string; source?: string;
tags?: string[]; tags?: string[];
evaluateResult?: EvaluateResult;
} }
const warningTask = ['video']; const warningTask = ['video'];
@@ -31,15 +35,18 @@ const SUPPORTEDSOURCE = [
]; ];
const HFModelItem: React.FC<HFModelItemProps> = (props) => { const HFModelItem: React.FC<HFModelItemProps> = (props) => {
const { evaluateResult } = props;
console.log('evaluateResult', evaluateResult);
const intl = useIntl(); const intl = useIntl();
const isExcludeTask = () => { const isExcludeTask = useMemo(() => {
if (!props.task) { if (!props.task) {
return false; return false;
} }
return _.some(warningTask, (item: string) => { return _.some(warningTask, (item: string) => {
return props.task?.toLowerCase().includes(item); return props.task?.toLowerCase().includes(item);
}); });
}; }, [props.task]);
return ( return (
<div <div
tabIndex={0} tabIndex={0}
@@ -53,7 +60,7 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
style={{ color: 'var(--ant-color-text-tertiary)' }} style={{ color: 'var(--ant-color-text-tertiary)' }}
/> />
{props.title} {props.title}
{isExcludeTask() && ( {isExcludeTask && (
<Tooltip <Tooltip
title={intl.formatMessage({ id: 'models.search.unsupport' })} title={intl.formatMessage({ id: 'models.search.unsupport' })}
> >
@@ -62,43 +69,18 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
)} )}
</div> </div>
<div className="info"> <div className="info">
{SUPPORTEDSOURCE.includes(props.source || '') ? ( <div className="info-item">
<div className="info-item"> <span>{dayjs().to(dayjs(props.updatedAt))}</span>
<span> <span className="flex-center">
{dayjs().to( <HeartOutlined className="m-r-5" />
dayjs(dayjs(props.updatedAt).format('YYYY-MM-DD HH:mm:ss')) {props.likes}
)} </span>
</span> <span className="flex-center">
<span className="flex-center"> <DownloadOutlined className="m-r-5" />
<HeartOutlined className="m-r-5" /> {formatNumber(props.downloads)}
{props.likes} </span>
</span> </div>
<span className="flex-center"> {<IncompatiableInfo data={evaluateResult}></IncompatiableInfo>}
<DownloadOutlined className="m-r-5" />
{formatNumber(props.downloads)}
</span>
</div>
) : (
<div className="flex-between">
<div className="tags">
{_.map(props.tags, (tag: string, index: string) => {
return (
<Tag
key={index}
style={{
backgroundColor: 'var(--color-white-1)',
marginRight: 0
}}
>
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
{tag}
</span>
</Tag>
);
})}
</div>
</div>
)}
</div> </div>
</div> </div>
); );
@@ -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<IncompatiableInfoProps> = (props) => {
const { data } = props;
if (data?.compatible) {
return null;
}
return (
<Tooltip
overlayInnerStyle={{ paddingInline: 0 }}
title={
<OverlayScroller maxHeight={200}>
<IncompatibleInfo>
<SMTitle $isTitle={!!data?.scheduling_messages?.length}>
{data?.compatibility_messages}
</SMTitle>
{!!data?.scheduling_messages?.length && (
<ul>
{data?.scheduling_messages.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
)}
</IncompatibleInfo>
</OverlayScroller>
}
>
<CompatibleTag icon={<WarningOutlined />} color="warning">
Incompatible
</CompatibleTag>
</Tooltip>
);
};
export default IncompatiableInfo;
+22 -21
View File
@@ -88,6 +88,12 @@ const ModelCard: React.FC<{
} }
}, []); }, []);
const handleOnCollapse = (readmeText: any) => {
if (!readmeText) {
onCollapse(false);
}
};
const loadConfig = useCallback(async (repo: string, sha: string) => { const loadConfig = useCallback(async (repo: string, sha: string) => {
try { try {
loadConfigTokenRef.current?.abort?.(); loadConfigTokenRef.current?.abort?.();
@@ -145,13 +151,14 @@ const ModelCard: React.FC<{
const newReadme = removeMetadata(readme); const newReadme = removeMetadata(readme);
setReadmeText(newReadme); setReadmeText(newReadme);
handleOnCollapse(newReadme);
const isGGUF = modelcard.tags?.includes('gguf'); const isGGUF = modelcard.tags?.includes('gguf');
console.log('modelData++++++++++++', isGGUF);
setIsGGUF(isGGUF); setIsGGUF(isGGUF);
setIsGGUFModel(isGGUF); setIsGGUFModel(isGGUF);
} catch (error) { } catch (error) {
setModelData(null); setModelData(null);
setReadmeText(null); setReadmeText(null);
handleOnCollapse(null);
setIsGGUF(false); setIsGGUF(false);
setIsGGUFModel(false); setIsGGUFModel(false);
} }
@@ -189,6 +196,7 @@ const ModelCard: React.FC<{
name: `${data.Data?.Path}/${data.Data?.Name}` name: `${data.Data?.Path}/${data.Data?.Name}`
}); });
setReadmeText(data?.Data?.ReadMeContent); setReadmeText(data?.Data?.ReadMeContent);
handleOnCollapse(data?.Data?.ReadMeContent);
const isGGUF = some( const isGGUF = some(
data?.Data?.Tags, data?.Data?.Tags,
(tag: string) => tag?.indexOf('gguf') > -1 (tag: string) => tag?.indexOf('gguf') > -1
@@ -198,6 +206,7 @@ const ModelCard: React.FC<{
} catch (error) { } catch (error) {
setModelData(null); setModelData(null);
setReadmeText(null); setReadmeText(null);
handleOnCollapse(null);
setIsGGUF(false); setIsGGUF(false);
setIsGGUFModel(false); setIsGGUFModel(false);
} }
@@ -207,6 +216,7 @@ const ModelCard: React.FC<{
if (!props.selectedModel.name) { if (!props.selectedModel.name) {
setModelData(null); setModelData(null);
setReadmeText(null); setReadmeText(null);
handleOnCollapse(null);
return; return;
} }
requestToken.current?.cancel?.(); requestToken.current?.cancel?.();
@@ -267,32 +277,23 @@ const ModelCard: React.FC<{
return null; return null;
}; };
const generateModeScopeImgLink = useCallback( const generateModeScopeImgLink = (imgSrc: string) => {
(imgSrc: string) => { if (!imgSrc) {
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}`;
}
return ''; 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(() => { useEffect(() => {
getModelCardData(); getModelCardData();
}, [props.selectedModel.name]); }, [props.selectedModel.name]);
useEffect(() => {
if (!readmeText) {
onCollapse(false);
}
}, [readmeText]);
useEffect(() => { useEffect(() => {
return () => { return () => {
requestToken.current?.cancel?.(); requestToken.current?.cancel?.();
+9 -11
View File
@@ -33,19 +33,17 @@ const SearchInput: React.FC<{
{ source: modelSourceValueMap[modelSource] } { source: modelSourceValueMap[modelSource] }
)} )}
prefix={ prefix={
<> <IconFont
<IconFont className="font-size-16"
className="font-size-16" type={
type={ modelSource === modelSourceMap.huggingface_value
modelSource === modelSourceMap.huggingface_value ? 'icon-huggingface'
? 'icon-huggingface' : 'icon-tu2'
: 'icon-tu2' }
} ></IconFont>
></IconFont>
</>
} }
></Input> ></Input>
); );
}; };
export default React.memo(SearchInput); export default SearchInput;
+90 -58
View File
@@ -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 { useIntl } from '@umijs/max';
import { Checkbox, Select, Tooltip } from 'antd'; import { Checkbox, Select, Tooltip } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, {
import { queryHuggingfaceModels, queryModelScopeModels } from '../apis'; useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import {
evaluationsModelSpec,
queryHuggingfaceModels,
queryModelScopeModels
} from '../apis';
import { import {
HuggingFaceTaskMap, HuggingFaceTaskMap,
ModelScopeSortType, ModelScopeSortType,
@@ -44,6 +55,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
const [current, setCurrent] = useState<string>(''); const [current, setCurrent] = useState<string>('');
const cacheRepoOptions = useRef<any[]>([]); const cacheRepoOptions = useRef<any[]>([]);
const axiosTokenRef = useRef<any>(null); const axiosTokenRef = useRef<any>(null);
const checkTokenRef = useRef<any>(null);
const searchInputRef = useRef<any>(''); const searchInputRef = useRef<any>('');
const filterGGUFRef = useRef<boolean | undefined>(); const filterGGUFRef = useRef<boolean | undefined>();
const filterTaskRef = useRef<string>(''); const filterTaskRef = useRef<string>('');
@@ -134,6 +146,24 @@ const SearchModel: React.FC<SearchInputProps> = (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( const handleOnSearchRepo = useCallback(
async (sortType?: string) => { async (sortType?: string) => {
if (!SUPPORTEDSOURCE.includes(modelSource)) { if (!SUPPORTEDSOURCE.includes(modelSource)) {
@@ -156,6 +186,26 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
list = await getModelsFromModelscope(sort); list = await getModelsFromModelscope(sort);
} }
cacheRepoOptions.current = list; 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({ setDataSource({
repoOptions: list, repoOptions: list,
loading: false, loading: false,
@@ -176,20 +226,14 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
cacheRepoOptions.current = []; cacheRepoOptions.current = [];
} }
}, },
[dataSource] [dataSource.sortType, modelSource]
); );
const handleSearchInputChange = useCallback((e: any) => { const handleSearchInputChange = useCallback((e: any) => {
searchInputRef.current = e.target.value; searchInputRef.current = e.target.value;
console.log('change:', searchInputRef.current); console.log('change:', searchInputRef.current);
}, []); }, []);
const handlerSearchModels = useCallback(
async (e: any) => { const handlerSearchModels = _.debounce(() => handleOnSearchRepo(), 100);
setTimeout(() => {
handleOnSearchRepo();
}, 100);
},
[handleOnSearchRepo]
);
const handleOnOpen = () => { const handleOnOpen = () => {
if ( if (
@@ -225,6 +269,28 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
handleOnSearchRepo(); handleOnSearchRepo();
}, []); }, []);
const renderGGUFTips = useMemo(() => {
return (
<Tooltip
overlayInnerStyle={{ width: 'max-content' }}
title={
<ul className="tips-desc-list">
<li>{intl.formatMessage({ id: 'models.search.gguf.tips' })}</li>
<li>{intl.formatMessage({ id: 'models.search.vllm.tips' })}</li>
<li>
{intl.formatMessage({
id: 'models.search.voxbox.tips'
})}
</li>
</ul>
}
>
GGUF
<QuestionCircleOutlined className="m-l-4" />
</Tooltip>
);
}, [intl]);
const renderHFSearch = () => { const renderHFSearch = () => {
return ( return (
<> <>
@@ -254,27 +320,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
className="m-r-5" className="m-r-5"
checked={filterGGUFRef.current} checked={filterGGUFRef.current}
> >
<Tooltip {renderGGUFTips}
overlayInnerStyle={{ width: 'max-content' }}
title={
<ul className="tips-desc-list">
<li>
{intl.formatMessage({ id: 'models.search.gguf.tips' })}
</li>
<li>
{intl.formatMessage({ id: 'models.search.vllm.tips' })}
</li>
<li>
{intl.formatMessage({
id: 'models.search.voxbox.tips'
})}
</li>
</ul>
}
>
GGUF
<QuestionCircleOutlined className="m-l-4" />
</Tooltip>
</Checkbox> </Checkbox>
</span> </span>
<Select <Select
@@ -305,37 +351,23 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
useEffect(() => { useEffect(() => {
return () => { return () => {
axiosTokenRef.current?.abort?.(); axiosTokenRef.current?.abort?.();
checkTokenRef.current?.cancel?.();
}; };
}, []); }, []);
return ( return (
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div className={SearchStyle['search-bar']}> <div className={SearchStyle['search-bar']}>{renderHFSearch()}</div>
{SUPPORTEDSOURCE.includes(modelSource) ? ( <SearchResult
renderHFSearch() loading={dataSource.loading}
) : ( resultList={dataSource.repoOptions}
<div style={{ lineHeight: '18px' }}> networkError={dataSource.networkError}
<BulbOutlined className="font-size-14 m-r-5" /> current={current}
{intl.formatMessage( source={modelSource}
{ id: 'model.form.ollamatips' }, onSelect={handleOnSelectModel}
{ name: intl.formatMessage({ id: 'model.form.ollama.model' }) } ></SearchResult>
)}
</div>
)}
</div>
{
<SearchResult
loading={dataSource.loading}
resultList={dataSource.repoOptions}
networkError={dataSource.networkError}
current={current}
source={modelSource}
onSelect={handleOnSelectModel}
></SearchResult>
}
</div> </div>
); );
}; };
export default React.memo(SearchModel); export default SearchModel;
@@ -2,7 +2,7 @@ import IconFont from '@/components/icon-font';
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Col, Empty, Row, Spin } from 'antd'; import { Button, Col, Empty, Row, Spin } from 'antd';
import React from 'react'; import React, { useMemo } from 'react';
import SimpleBar from 'simplebar-react'; import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css'; import 'simplebar-react/dist/simplebar.min.css';
import { modelSourceMap } from '../config'; import { modelSourceMap } from '../config';
@@ -34,7 +34,7 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
} }
}; };
const renderEmpty = () => { const renderEmpty = useMemo(() => {
if (networkError) { if (networkError) {
return ( return (
<Empty <Empty
@@ -85,7 +85,8 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
description={intl.formatMessage({ id: 'models.search.noresult' })} description={intl.formatMessage({ id: 'models.search.noresult' })}
/> />
); );
}; }, [networkError, source, intl]);
return ( return (
<SimpleBar style={{ height: 'calc(100vh - 224px)' }}> <SimpleBar style={{ height: 'calc(100vh - 224px)' }}>
<div style={{ ...props.style }} className="search-result-wrap"> <div style={{ ...props.style }} className="search-result-wrap">
@@ -108,6 +109,7 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
likes={item.likes} likes={item.likes}
task={item.task} task={item.task}
updatedAt={item.updatedAt} updatedAt={item.updatedAt}
evaluateResult={item.evaluateResult}
active={item.id === props.current} active={item.id === props.current}
/> />
</div> </div>
@@ -115,7 +117,7 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
))} ))}
</Row> </Row>
) : ( ) : (
!props.loading && renderEmpty() !props.loading && renderEmpty
)} )}
</div> </div>
</Spin> </Spin>
+22
View File
@@ -439,3 +439,25 @@ export const modelLabels = [
{ label: 'reranker', value: 'reranker' }, { label: 'reranker', value: 'reranker' },
{ label: 'Embedding', value: 'embedding_only' } { label: 'Embedding', value: 'embedding_only' }
]; ];
export const CHECK_FIELDS = new Set([
'backend',
'local_path',
'scheduleType',
'placement_strategy',
'worker_selector',
'gpu_selector',
'backend_parameters',
'backend_version',
'quantization',
'size'
]);
export const excludeFields = [
'replicas',
'categories',
'name',
'description',
'env',
'source'
];
+37
View File
@@ -175,3 +175,40 @@ export interface CatalogSpec {
quantization: string; quantization: string;
size: number; size: number;
} }
export interface EvaluateSpec {
source?: string;
huggingface_repo_id?: string;
huggingface_filename?: string;
ollama_library_model_name?: string;
model_scope_model_id?: string;
model_scope_file_path?: string;
local_path?: string;
name?: string;
description?: string;
meta?: Record<string, any>;
replicas?: number;
ready_replicas?: number;
categories?: any[];
placement_strategy?: string;
cpu_offloading?: boolean;
distributed_inference_across_workers?: boolean;
worker_selector?: Record<string, any>;
gpu_selector?: {
gpu_ids: string[];
};
backend?: string;
backend_version?: string;
backend_parameters?: any[];
env?: Record<string, any>;
distributable?: boolean;
quantization?: string;
size?: number;
}
export interface EvaluateResult {
compatible: boolean;
compatibility_messages: string[];
scheduling_messages: string[];
default_backend_parameters: any[];
}
+114 -3
View File
@@ -1,10 +1,15 @@
import { queryModelFilesList } from '@/pages/resources/apis'; import { queryModelFilesList } from '@/pages/resources/apis';
import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
import { useIntl } from '@umijs/max';
import _ from 'lodash'; import _ from 'lodash';
import { useRef } from 'react'; import { useRef, useState } from 'react';
import { queryGPUList } from '../apis'; import { queryGPUList } from '../apis';
import { backendOptionsMap, setSourceRepoConfigValue } from '../config'; import {
import { GPUListItem, ListItem } from '../config/types'; backendOptionsMap,
modelSourceMap,
setSourceRepoConfigValue
} from '../config';
import { EvaluateResult, GPUListItem, ListItem } from '../config/types';
export const useGenerateFormEditInitialValues = () => { export const useGenerateFormEditInitialValues = () => {
const gpuDeviceList = useRef<any[]>([]); const gpuDeviceList = useRef<any[]>([]);
@@ -176,3 +181,109 @@ export const useGenerateModelFileOptions = () => {
generateModelFileOptions generateModelFileOptions
}; };
}; };
export const useCheckCompatibility = () => {
const intl = useIntl();
const [warningStatus, setWarningStatus] = useState<{
show: boolean;
title?: string;
message: string | string[];
}>({
show: false,
title: '',
message: []
});
const handleCheckCompatibility = (evaluateResult: EvaluateResult | null) => {
if (!evaluateResult) {
return {
show: false,
message: ''
};
}
const {
compatible,
compatibility_messages = [],
scheduling_messages = []
} = evaluateResult || {};
return {
show: !compatible,
title:
scheduling_messages?.length > 0
? compatibility_messages?.join(' ')
: '',
message:
scheduling_messages?.length > 0
? scheduling_messages
: compatibility_messages?.join(' ')
};
};
const handleShowCompatibleAlert = (evaluateResult: EvaluateResult | null) => {
const result = handleCheckCompatibility(evaluateResult);
setWarningStatus(result);
};
const updateShowWarning = (params: {
backend: string;
localPath: string;
source: string;
}) => {
const { backend, localPath, source } = params;
if (source !== modelSourceMap.local_path_value || !localPath) {
return {
show: false,
message: ''
};
}
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 = intl.formatMessage({
id: 'models.form.ollama.warning'
});
} else if (isGGUFFile && backend !== backendOptionsMap.llamaBox) {
warningMessage = intl.formatMessage({
id: 'models.form.backend.warning'
});
} else if (!isGGUFFile && backend === backendOptionsMap.llamaBox) {
warningMessage = intl.formatMessage({
id: 'models.form.backend.warning.llamabox'
});
}
return {
show: !!warningMessage,
isHtml: true,
message: warningMessage
};
};
const handleUpdateWarning = (params: {
backend: string;
localPath: string;
source: string;
}) => {
const warningMessage = updateShowWarning(params);
setWarningStatus(warningMessage);
return warningMessage;
};
return {
handleShowCompatibleAlert,
handleUpdateWarning,
warningStatus
};
};
+5 -5
View File
@@ -3,6 +3,7 @@
.hf-model-file { .hf-model-file {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between;
padding: 12px 14px; padding: 12px 14px;
border: 1px solid var(--ant-color-border); border: 1px solid var(--ant-color-border);
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);
@@ -16,18 +17,17 @@
background-color: var(--color-fill-sider); background-color: var(--color-fill-sider);
} }
.title {
margin-bottom: 12px;
}
.tags { .tags {
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: flex-start;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
} }
.title {
margin-bottom: 14px;
}
.tag-item { .tag-item {
display: flex; display: flex;
align-items: center; align-items: center;
+3 -1
View File
@@ -18,12 +18,14 @@
.title { .title {
font-size: var(--font-size-base); font-size: var(--font-size-base);
font-weight: var(--font-weight-500);
} }
.info { .info {
display: flex;
align-items: center;
color: var(--ant-color-text-tertiary); color: var(--ant-color-text-tertiary);
font-size: var(--font-size-small); font-size: var(--font-size-small);
justify-content: space-between;
.info-item { .info-item {
display: flex; display: flex;
@@ -362,7 +362,9 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
type="icon-upload_image" type="icon-upload_image"
className="font-size-24" className="font-size-24"
></IconFont> ></IconFont>
<h3>{intl.formatMessage({ id: 'playground.image.edit.tips' })}</h3> <span>
{intl.formatMessage({ id: 'playground.image.edit.tips' })}
</span>
</div> </div>
</UploadImg> </UploadImg>
</> </>