chore: check compatibility ux

main
jialin 1 year ago
parent bd6c689d91
commit f83e49bd3d

@ -12,6 +12,7 @@ interface AlertInfoProps {
icon?: React.ReactNode;
ellipsis?: boolean;
style?: React.CSSProperties;
contentStyle?: React.CSSProperties;
title: React.ReactNode;
}
@ -30,7 +31,15 @@ const ContentWrapper = styled.div<{ $hasTitle: boolean }>`
`;
const AlertInfo: React.FC<AlertInfoProps> = (props) => {
const { message, type, rows = 1, ellipsis, style, title } = props;
const {
message,
type,
rows = 1,
ellipsis,
style,
title,
contentStyle
} = props;
return (
<>
@ -51,7 +60,7 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
<WarningFilled className={classNames('info-icon', type)} />
</div>
{title && <TitleWrapper>{title}</TitleWrapper>}
<OverlayScroller maxHeight={80}>
<OverlayScroller maxHeight={80} style={{ ...contentStyle }}>
<ContentWrapper $hasTitle={!!title}>{message}</ContentWrapper>
</OverlayScroller>
</Typography.Paragraph>

@ -1,5 +1,6 @@
import { useIntl } from '@umijs/max';
import { Button, Space } from 'antd';
import React from 'react';
type ModalFooterProps = {
onOk?: () => void;
@ -9,10 +10,12 @@ type ModalFooterProps = {
htmlType?: 'button' | 'submit';
okBtnProps?: any;
cancelBtnProps?: any;
align?: 'start' | 'end' | 'center' | 'baseline';
form?: any;
loading?: boolean;
style?: React.CSSProperties;
showOkBtn?: boolean;
showCancelBtn?: boolean;
extra?: React.ReactNode;
form?: any;
};
const ModalFooter: React.FC<ModalFooterProps> = ({
onOk,
@ -23,8 +26,9 @@ const ModalFooter: React.FC<ModalFooterProps> = ({
cancelBtnProps,
loading,
htmlType = 'button',
align = 'end',
style,
showOkBtn = true,
extra,
form
}) => {
const intl = useIntl();
@ -33,16 +37,19 @@ const ModalFooter: React.FC<ModalFooterProps> = ({
<Button onClick={onCancel} style={{ width: '88px' }} {...cancelBtnProps}>
{cancelText || intl.formatMessage({ id: 'common.button.cancel' })}
</Button>
<Button
type="primary"
onClick={onOk}
style={{ width: '88px' }}
loading={loading}
htmlType={htmlType}
{...okBtnProps}
>
{okText || intl.formatMessage({ id: 'common.button.save' })}
</Button>
{extra}
{showOkBtn && (
<Button
type="primary"
onClick={onOk}
style={{ width: '88px' }}
loading={loading}
htmlType={htmlType}
{...okBtnProps}
>
{okText || intl.formatMessage({ id: 'common.button.save' })}
</Button>
)}
</Space>
);
};

@ -10,7 +10,12 @@ const Wrapper = styled.div<{ $maxHeight?: number }>`
padding-inline: 10px;
`;
const OverlayScroller: React.FC<any> = ({ children, maxHeight, theme }) => {
const OverlayScroller: React.FC<any> = ({
children,
maxHeight,
theme,
style
}) => {
const scroller = React.useRef<any>(null);
const { initialize } = useOverlayScroller({
options: {
@ -25,7 +30,15 @@ const OverlayScroller: React.FC<any> = ({ children, maxHeight, theme }) => {
}, []);
return (
<Wrapper ref={scroller} $maxHeight={maxHeight || '100%'} hidden={false}>
<Wrapper
ref={scroller}
$maxHeight={maxHeight || '100%'}
hidden={false}
as="div"
style={{
...style
}}
>
{children}
</Wrapper>
);

@ -121,10 +121,8 @@ export default {
'models.form.keyvalue.paste':
'Paste multiple lines of text, with each line containing a key-value pair. The key and value are separated by an = sign, and different key-value pairs are separated by newline characters.',
'models.form.files': 'files',
'models.table.status': 'Status'
'models.table.status': 'Status',
'models.form.submit.anyway': 'Submit Anyway',
'models.form.evaluating': 'Evaluating...',
'models.form.incompatible': 'Incompatible'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.form.files',
// 2. 'models.table.status',
// ========== End of To-Do List ==========

@ -118,10 +118,16 @@ export default {
'models.form.keyvalue.paste':
'複数行のテキストを貼り付けます。各行にはキーと値のペアが含まれ、キーと値は=記号で区切られ、異なるキーと値のペアは改行文字で区切られます。',
'models.form.files': 'files',
'models.table.status': 'Status'
'models.table.status': 'Status',
'models.form.submit.anyway': 'Submit Anyway',
'models.form.evaluating': 'Evaluating...',
'models.form.incompatible': 'Incompatible'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.form.files',
// 2. 'models.table.status'
// 1. 'models.form.files',
// 2. 'models.table.status',
// 3. 'models.form.submit.anyway',
// 4. 'models.form.evaluating',
// 5. 'models.form.incompatible',
// ========== End of To-Do List ==========

@ -121,9 +121,15 @@ export default {
'models.form.keyvalue.paste':
'Вставьте несколько строк текста, где каждая строка содержит пару ключ-значение. Ключ и значение разделяются знаком равенства (=), а разные пары — символами новой строки.',
'models.form.files': 'файлы',
'models.table.status': 'Status'
'models.table.status': 'Status',
'models.form.submit.anyway': 'Submit Anyway',
'models.form.evaluating': 'Evaluating...',
'models.form.incompatible': 'Incompatible'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. models.table.status
// 1. models.table.status,
// 2. 'models.form.submit.anyway',
// 3. 'models.form.evaluating',
// 4. 'models.form.incompatible',
// ========== End of To-Do List ==========

@ -114,5 +114,8 @@ export default {
'models.form.keyvalue.paste':
'粘贴多行文本,每行包含一个键值对,键和值之间用 = 号分隔,不同的键值对之间用换行符分隔。',
'models.form.files': '文件',
'models.table.status': '状态'
'models.table.status': '状态',
'models.form.submit.anyway': '仍然提交',
'models.form.evaluating': '评估中...',
'models.form.incompatible': '不兼容'
};

@ -0,0 +1,52 @@
const MODEL_EVALUATIONS = '/model-evaluations';
let controller = new AbortController();
let signal = controller.signal;
self.onmessage = async (event) => {
const { list, modelSource, modelSourceMap } = event.data;
const repoList = list.map((item: any) => ({
source: modelSource,
...(modelSource === modelSourceMap.huggingface_value
? { huggingface_repo_id: item.name }
: { model_scope_model_id: item.name })
}));
try {
controller?.abort();
controller = new AbortController();
signal = controller.signal;
const response = await fetch(`v1/${MODEL_EVALUATIONS}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
signal,
body: JSON.stringify({ model_specs: repoList })
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const evaluations = await response.json();
const { results } = evaluations;
const resultList = list.map((item: any, index: number) => {
return {
...item,
evaluateResult: results[index] || null
};
});
self.postMessage({ success: true, resultList });
} catch (error) {
self.postMessage({ success: false, resultList: list });
}
};
self.onmessage = (event) => {
if (event.data === 'abort') {
controller.abort();
}
};

@ -379,3 +379,17 @@ export async function evaluationsModelSpec(
cancelToken: options?.token
});
}
// export const evaluationsModelSpec = async (
// data: {
// model_specs: EvaluateSpec[];
// },
// options: { token: any }
// ) => {
// const response = await fetch(`v1/${MODEL_EVALUATIONS}`, {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(data)
// });
// return response.json();
// };

@ -1,4 +1,5 @@
import AlertBlockInfo from '@/components/alert-info/block';
import { CloseOutlined } from '@ant-design/icons';
import { isArray } from 'lodash';
import React, { useMemo } from 'react';
import styled from 'styled-components';
@ -10,12 +11,23 @@ interface CompatibilityAlertProps {
isHtml?: boolean;
message: string | string[];
};
contentStyle?: React.CSSProperties;
showClose?: boolean;
onClose?: () => void;
}
const DivWrapper = styled.div`
position: relative;
padding-inline: 12px;
`;
const CloseWrapper = styled.div`
position: absolute;
top: 6px;
right: 18px;
cursor: pointer;
`;
const MessageWrapper = styled.div`
display: flex;
flex-direction: column;
@ -24,7 +36,7 @@ const MessageWrapper = styled.div`
`;
const CompatibilityAlert: React.FC<CompatibilityAlertProps> = (props) => {
const { warningStatus } = props;
const { warningStatus, contentStyle, showClose, onClose } = props;
const { title, show, message, isHtml } = warningStatus;
const renderMessage = useMemo(() => {
@ -62,8 +74,14 @@ const CompatibilityAlert: React.FC<CompatibilityAlertProps> = (props) => {
ellipsis={false}
message={renderMessage}
title={title}
contentStyle={contentStyle}
type="warning"
></AlertBlockInfo>
{showClose && (
<CloseWrapper onClick={onClose}>
<CloseOutlined />
</CloseWrapper>
)}
</DivWrapper>
)
);

@ -1,65 +1,45 @@
import IconFont from '@/components/icon-font';
import SealAutoComplete from '@/components/seal-form/auto-complete';
import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select';
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 { Form } from 'antd';
import _ from 'lodash';
import React, {
forwardRef,
useImperativeHandle,
useMemo,
useRef,
useState
} from 'react';
import { evaluationsModelSpec } from '../apis';
import React, { forwardRef, useImperativeHandle, useState } from 'react';
import {
HuggingFaceTaskMap,
ModelscopeTaskMap,
backendOptionsMap,
backendTipsList,
excludeFields,
getSourceRepoConfigValue,
localPathTipsList,
modelSourceMap,
modelTaskMap,
ollamaModelOptions,
sourceOptions
} from '../config';
import { identifyModelTask } from '../config/audio-catalog';
import { EvaluateResult, FormData } from '../config/types';
import { FormInnerContext } from '../config/form-context';
import { FormData, SourceType } from '../config/types';
import CatalogFrom from '../forms/catalog';
import HuggingFaceForm from '../forms/hugging-face';
import LocalPathForm from '../forms/local-path';
import OllamaForm from '../forms/ollama-library';
import AdvanceConfig from './advance-config';
interface DataFormProps {
initialValues?: any;
ref?: any;
source: string;
source: SourceType;
action: PageActionType;
selectedModel: any;
isGGUF: boolean;
sizeOptions?: Global.BaseOption<number>[];
quantizationOptions?: Global.BaseOption<string>[];
sourceDisable?: boolean;
byBuiltIn?: boolean;
backendOptions?: Global.BaseOption<string>[];
sourceList?: Global.BaseOption<string>[];
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;
@ -78,15 +58,11 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
sourceDisable = true,
backendOptions,
sourceList,
byBuiltIn,
gpuOptions = [],
modelFileOptions = [],
sizeOptions = [],
quantizationOptions = [],
fields = ['source'],
handleUpdateWarning,
handleShowCompatibleAlert,
onSourceChange,
onValuesChange,
onOk
} = props;
console.log('modelFileOptions--------', modelFileOptions);
@ -99,8 +75,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
text2speech: false,
speech2text: false
});
const checkTokenRef = useRef<any>(null);
const localPathCache = useRef<string>('');
const handleSumit = () => {
form.submit();
@ -153,268 +127,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
}
};
const handleOnFocus = () => {
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;
}
const isEndwithGGUF = _.endsWith(value, '.gguf');
const isBlobFile = value.split('/').pop().includes('sha256');
let backend = backendOptionsMap.llamaBox;
if (!isEndwithGGUF && !isBlobFile) {
backend = backendOptionsMap.vllm;
}
form.setFieldValue('backend', backend);
handleBackendChangeHook();
props.onBackendChange?.(backend);
};
const renderHuggingfaceFields = () => {
return (
<>
<Form.Item<FormData>
name="repo_id"
key="repo_id"
rules={[
{
required: true,
message: getRuleMessage('input', 'models.form.repoid')
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'models.form.repoid' })}
required
disabled={true}
></SealInput.Input>
</Form.Item>
{isGGUF && (
<Form.Item<FormData>
name="file_name"
key="file_name"
rules={[
{
required: true,
message: getRuleMessage('input', 'models.form.filename')
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'models.form.filename' })}
required
disabled={true}
></SealInput.Input>
</Form.Item>
)}
</>
);
};
const renderOllamaModelFields = () => {
return (
<>
<Form.Item<FormData>
name="ollama_library_model_name"
key="ollama_library_model_name"
rules={[
{
required: true,
message: getRuleMessage('input', 'models.table.name')
}
]}
>
<SealAutoComplete
allowClear
filterOption
defaultActiveFirstOption
disabled={false}
options={ollamaModelOptions}
description={
<span>
<span>
{intl.formatMessage({ id: 'models.form.ollamalink' })}
</span>
<Typography.Link
className="flex-center"
href="https://www.ollama.com/library"
target="_blank"
>
<IconFont
type="icon-external-link"
className="font-size-14"
></IconFont>
</Typography.Link>
</span>
}
label={intl.formatMessage({ id: 'model.form.ollama.model' })}
placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })}
required
></SealAutoComplete>
</Form.Item>
</>
);
};
const renderLocalPathFields = () => {
return (
<>
<Form.Item<FormData>
name="local_path"
key="local_path"
rules={[
{
required: true,
message: getRuleMessage('input', 'models.form.filePath')
}
]}
>
<SealAutoComplete
allowClear
required
filterOption
defaultActiveFirstOption
options={modelFileOptions}
onBlur={handleLocalPathBlur}
onFocus={handleOnFocus}
label={intl.formatMessage({ id: 'models.form.filePath' })}
description={<TooltipList list={localPathTipsList}></TooltipList>}
></SealAutoComplete>
</Form.Item>
</>
);
};
const handleSizeChange = (val: any) => {
props.onSizeChange?.(val);
};
const handleOnQuantizationChange = (val: any) => {
props.onQuantizationChange?.(val);
};
// from catalog
const renderFieldsFromCatalog = useMemo(() => {
if (!byBuiltIn && !sizeOptions?.length && !quantizationOptions?.length) {
return null;
}
return (
<>
{sizeOptions?.length > 0 && (
<Form.Item<FormData>
name="size"
key="size"
rules={[
{
required: true,
message: getRuleMessage('input', 'size', false)
}
]}
>
<SealSelect
filterOption
onChange={handleSizeChange}
defaultActiveFirstOption
disabled={false}
options={sizeOptions}
label="Size"
required
></SealSelect>
</Form.Item>
)}
{quantizationOptions?.length > 0 && (
<Form.Item<FormData>
name="quantization"
key="quantization"
rules={[
{
required: true,
message: getRuleMessage('select', 'quantization', false)
}
]}
>
<SealSelect
filterOption
defaultActiveFirstOption
disabled={false}
options={quantizationOptions}
onChange={handleOnQuantizationChange}
label="Quantization"
required
></SealSelect>
</Form.Item>
)}
</>
);
}, [sizeOptions, quantizationOptions, byBuiltIn]);
const renderFieldsBySource = useMemo(() => {
// from catalog
if (byBuiltIn) {
return null;
}
if (SEARCH_SOURCE.includes(props.source)) {
return renderHuggingfaceFields();
}
if (props.source === modelSourceMap.ollama_library_value) {
return renderOllamaModelFields();
}
if (props.source === modelSourceMap.local_path_value) {
return renderLocalPathFields();
}
return null;
}, [props.source, isGGUF, byBuiltIn, intl]);
const handleSetGPUIds = (backend: string) => {
if (backend === backendOptionsMap.llamaBox) {
return;
@ -441,7 +153,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
}
form.setFieldsValue(updates);
handleSetGPUIds(val);
handleBackendChangeHook();
props.onBackendChange?.(val);
};
@ -474,7 +185,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
return {};
};
const handleOk = (formdata: FormData) => {
const handleOk = async (formdata: FormData) => {
let data = _.cloneDeep(formdata);
if (data.categories) {
data.categories = [data.categories];
@ -482,10 +193,12 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
data.categories = [];
}
const gpuSelector = generateGPUIds(data);
onOk({
const allValues = {
..._.omit(data, ['scheduleType']),
...gpuSelector
});
};
onOk(allValues);
};
const handleOnSourceChange = (val: string) => {
@ -493,19 +206,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
};
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);
}
onValuesChange?.(changedValues, allValues);
};
useImperativeHandle(
@ -595,7 +296,15 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
</Form.Item>
)}
{renderFieldsBySource}
<FormInnerContext.Provider
value={{
onBackendChange: handleBackendChange
}}
>
<HuggingFaceForm></HuggingFaceForm>
<OllamaForm></OllamaForm>
<LocalPathForm></LocalPathForm>
</FormInnerContext.Provider>
<Form.Item name="backend" rules={[{ required: true }]}>
<SealSelect
required
@ -637,7 +346,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
}
></SealSelect>
</Form.Item>
{renderFieldsFromCatalog}
<CatalogFrom></CatalogFrom>
<Form.Item<FormData>
name="replicas"
rules={[

@ -2,18 +2,26 @@ import ModalFooter from '@/components/modal-footer';
import { PageActionType } from '@/config/types';
import { createAxiosToken } from '@/hooks/use-chunk-request';
import { CloseOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Drawer } from 'antd';
import _ from 'lodash';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { queryCatalogItemSpec } from '../apis';
import {
backendOptionsMap,
excludeFields,
modelCategoriesMap,
sourceOptions
} from '../config';
import { CatalogSpec, FormData, ListItem } from '../config/types';
import { useGenerateFormEditInitialValues } from '../hooks';
import { FormContext } from '../config/form-context';
import { CatalogSpec, FormData, ListItem, SourceType } from '../config/types';
import {
useCheckCompatibility,
useGenerateFormEditInitialValues
} from '../hooks';
import ColumnWrapper from './column-wrapper';
import CompatibilityAlert from './compatible-alert';
import DataForm from './data-form';
type AddModalProps = {
@ -21,13 +29,20 @@ type AddModalProps = {
action: PageActionType;
open: boolean;
data?: ListItem;
source: string;
source: SourceType;
width?: string | number;
current?: any;
onOk: (values: FormData) => void;
onCancel: () => void;
};
const FormWrapper = styled.div`
display: flex;
flex: 1;
height: 100%;
maxwidth: 100%;
`;
const backendOptions = [
{
label: `llama-box`,
@ -64,6 +79,13 @@ const AddModal: React.FC<AddModalProps> = (props) => {
current,
width = 600
} = props || {};
const {
handleShowCompatibleAlert,
setWarningStatus,
handleEvaluate,
warningStatus
} = useCheckCompatibility();
const intl = useIntl();
const { getGPUList } = useGenerateFormEditInitialValues();
const form = useRef<any>({});
const [gpuOptions, setGpuOptions] = useState<any[]>([]);
@ -76,11 +98,26 @@ const AddModal: React.FC<AddModalProps> = (props) => {
const axiosToken = useRef<any>(null);
const selectSpecRef = useRef<CatalogSpec>({} as CatalogSpec);
const specListRef = useRef<any[]>([]);
const submitAnyway = useRef<any>(null);
const handleSumit = () => {
form.current?.submit?.();
};
const handleSubmitAnyway = async () => {
submitAnyway.current = true;
form.current?.submit?.();
};
const generateSubmitData = (formData: FormData) => {
const data = {
..._.omit(selectSpecRef.current, ['name']),
...formData
};
return data;
};
const getDefaultQuant = (data: { category: string; quantOption: string }) => {
if (
data.category === modelCategoriesMap.embedding ||
@ -181,6 +218,20 @@ const AddModal: React.FC<AddModalProps> = (props) => {
return backendList;
};
const handleCheckCompatibility = async (formData: FormData) => {
const evalutionData = await handleEvaluate(formData);
if (evalutionData?.compatible) {
setWarningStatus({
show: false,
message: ''
});
} else {
handleShowCompatibleAlert?.(evalutionData);
}
return evalutionData;
};
const handleSourceChange = (source: string) => {
const defaultSpec = _.get(sourceGroupMap.current, `${source}.0`, {});
initFormDataBySource(defaultSpec);
@ -193,6 +244,8 @@ const AddModal: React.FC<AddModalProps> = (props) => {
});
// set form value
initFormDataBySource(defaultSpec);
const data = generateSubmitData(form.current.getFieldsValue());
handleCheckCompatibility(data);
};
const checkSize = (list: any[]) => {
@ -222,6 +275,16 @@ const AddModal: React.FC<AddModalProps> = (props) => {
);
};
const handleOnValuesChange = async (changedValues: any, allValues: any) => {
const keys = Object.keys(changedValues);
const isExcludeField = keys.some((key) => excludeFields.includes(key));
if (!isExcludeField) {
const values = form.current?.form.getFieldsValue?.();
const data = generateSubmitData(values);
handleCheckCompatibility(data);
}
};
const handleBackendChange = (backend: string) => {
if (backend === backendOptionsMap.vllm) {
setIsGGUF(false);
@ -252,6 +315,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
form.current.setFieldsValue({
...data
});
handleCheckCompatibility(data);
};
const fetchSpecData = async () => {
@ -323,6 +387,8 @@ const AddModal: React.FC<AddModalProps> = (props) => {
form.current.setFieldsValue({
...data
});
const allValues = generateSubmitData(data);
handleCheckCompatibility(allValues);
};
const handleOnSizeChange = (val: number) => {
@ -343,13 +409,21 @@ const AddModal: React.FC<AddModalProps> = (props) => {
form.current.setFieldsValue({
...data
});
const allValues = generateSubmitData(data);
handleCheckCompatibility(allValues);
};
const handleOk = (values: FormData) => {
onOk({
..._.omit(selectSpecRef.current, ['name']),
...values
});
const handleOk = async (values: FormData) => {
const data = generateSubmitData(values);
if (submitAnyway.current) {
onOk(data);
return;
}
const evaluateResult = await handleCheckCompatibility(data);
if (evaluateResult?.compatible) {
onOk(data);
}
};
const handleCancel = useCallback(() => {
@ -363,6 +437,11 @@ const AddModal: React.FC<AddModalProps> = (props) => {
}
return () => {
axiosToken.current?.cancel?.();
setWarningStatus({
show: false,
title: '',
message: []
});
};
}, [open, current]);
@ -409,44 +488,85 @@ const AddModal: React.FC<AddModalProps> = (props) => {
width={width}
footer={false}
>
<div style={{ display: 'flex', height: '100%' }}>
<ColumnWrapper
footer={
<ModalFooter
onCancel={handleCancel}
onOk={handleSumit}
style={{
padding: '16px 24px',
display: 'flex',
justifyContent: 'flex-end'
}}
></ModalFooter>
}
>
<>
<DataForm
fields={[]}
source={source}
action={action}
selectedModel={{}}
onOk={handleOk}
ref={form}
isGGUF={isGGUF}
byBuiltIn={true}
sourceDisable={false}
backendOptions={backendList}
sourceList={sourceList}
quantizationOptions={quantizationOptions}
sizeOptions={sizeOptions}
gpuOptions={gpuOptions}
onBackendChange={handleBackendChange}
onSourceChange={handleSourceChange}
onQuantizationChange={handleOnQuantizationChange}
onSizeChange={handleOnSizeChange}
></DataForm>
</>
</ColumnWrapper>
</div>
<FormContext.Provider
value={{
isGGUF: isGGUF,
byBuiltIn: true,
sizeOptions: sizeOptions,
quantizationOptions: quantizationOptions,
onSizeChange: handleOnSizeChange,
onQuantizationChange: handleOnQuantizationChange
}}
>
<FormWrapper>
<ColumnWrapper
paddingBottom={
warningStatus.show
? Array.isArray(warningStatus.message)
? 150
: 125
: 50
}
footer={
<>
<CompatibilityAlert
showClose={true}
onClose={() => {
setWarningStatus({
show: false,
message: ''
});
}}
warningStatus={warningStatus}
contentStyle={{ paddingInline: 0 }}
></CompatibilityAlert>
<ModalFooter
onCancel={handleCancel}
onOk={handleSumit}
showOkBtn={!warningStatus.show}
extra={
warningStatus.show && (
<Button
type="primary"
onClick={handleSubmitAnyway}
style={{ width: '130px' }}
>
{intl.formatMessage({
id: 'models.form.submit.anyway'
})}
</Button>
)
}
style={{
padding: '16px 24px',
display: 'flex',
justifyContent: 'flex-end'
}}
></ModalFooter>
</>
}
>
<>
<DataForm
fields={[]}
source={source}
action={action}
selectedModel={{}}
onOk={handleOk}
ref={form}
isGGUF={isGGUF}
sourceDisable={false}
backendOptions={backendList}
sourceList={sourceList}
gpuOptions={gpuOptions}
onBackendChange={handleBackendChange}
onSourceChange={handleSourceChange}
onValuesChange={handleOnValuesChange}
></DataForm>
</>
</ColumnWrapper>
</FormWrapper>
</FormContext.Provider>
</Drawer>
);
};

@ -3,11 +3,17 @@ 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 {
backendOptionsMap,
excludeFields,
getSourceRepoConfigValue,
modelSourceMap
} from '../config';
import { FormContext } from '../config/form-context';
import { FormData, SourceType } from '../config/types';
import { useCheckCompatibility } from '../hooks';
import ColumnWrapper from './column-wrapper';
import CompatibilityAlert from './compatible-alert';
@ -18,6 +24,12 @@ import SearchModel from './search-model';
import Separator from './separator';
import TitleWrapper from './title-wrapper';
const ModalFooterStyle = {
padding: '16px 24px',
display: 'flex',
justifyContent: 'flex-end'
};
const ColWrapper = styled.div`
display: flex;
flex: 1;
@ -34,7 +46,7 @@ type AddModalProps = {
title: string;
action: PageActionType;
open: boolean;
source: string;
source: SourceType;
isGGUF?: boolean;
width?: string | number;
gpuOptions: any[];
@ -62,14 +74,21 @@ const AddModal: FC<AddModalProps> = (props) => {
modelSourceMap.modelscope_value
];
const { handleShowCompatibleAlert, handleUpdateWarning, warningStatus } =
useCheckCompatibility();
const {
handleShowCompatibleAlert,
handleUpdateWarning,
setWarningStatus,
handleEvaluate,
checkTokenRef,
warningStatus
} = useCheckCompatibility();
const form = useRef<any>({});
const intl = useIntl();
const [selectedModel, setSelectedModel] = useState<any>({});
const [collapsed, setCollapsed] = useState<boolean>(false);
const [isGGUF, setIsGGUF] = useState<boolean>(props.isGGUF || false);
const modelFileRef = useRef<any>(null);
const submitAnyway = useRef<boolean>(false);
const handleSelectModelFile = useCallback((item: any) => {
form.current?.setFieldsValue?.({
@ -81,9 +100,31 @@ const AddModal: FC<AddModalProps> = (props) => {
}
}, []);
const handleOnSelectModel = (item: any) => {
const handleOnSelectModel = (item: any, isgguf?: boolean) => {
setSelectedModel(item);
form.current?.handleOnSelectModel?.(item);
console.log('isgguf+++++++', isgguf, item);
if (!isgguf) {
handleShowCompatibleAlert(item.evaluateResult);
}
};
const handleOnOk = async (allValues: FormData) => {
if (submitAnyway.current) {
onOk(allValues);
return;
}
const result = getSourceRepoConfigValue(props.source, allValues);
const evalutionData = await handleEvaluate(result.values);
handleShowCompatibleAlert?.(evalutionData);
if (evalutionData?.compatible) {
onOk(allValues);
}
};
const handleSubmitAnyway = async () => {
submitAnyway.current = true;
form.current?.submit?.();
};
const handleSumit = () => {
@ -98,12 +139,37 @@ const AddModal: FC<AddModalProps> = (props) => {
setIsGGUF(flag);
if (flag) {
debounceFetchModelFiles();
}
};
// trigger from local_path change or backend change
const handleBackendChangeHook = async () => {
const localPath = form.current.form.getFieldValue?.('local_path');
const backend = form.current.form.getFieldValue?.('backend');
const res = handleUpdateWarning?.({
backend,
localPath: localPath,
source: props.source
});
if (!res.show) {
const values = form.current.form.getFieldsValue?.();
const data = getSourceRepoConfigValue(props.source, values);
const evalutionData = await handleEvaluate(
_.omit(data.values, [
'cpu_offloading',
'distributed_inference_across_workers'
])
);
handleShowCompatibleAlert?.(evalutionData);
} else {
handleShowCompatibleAlert(selectedModel.evaluateResult);
setWarningStatus?.(res);
}
};
const handleBackendChange = async (backend: string) => {
handleBackendChangeHook();
if (backend === backendOptionsMap.vllm) {
setIsGGUF(false);
}
@ -113,6 +179,40 @@ const AddModal: FC<AddModalProps> = (props) => {
}
};
const handleOnValuesChange = async (changedValues: any, allValues: any) => {
const keys = Object.keys(changedValues);
const isExcludeField = keys.some((key) => excludeFields.includes(key));
const hasValue = keys.every((key) => {
return !!changedValues[key];
});
// let hasExcludeField = false;
// let allFieldsHaveValue = true;
// for (const key of keys) {
// if (excludeFields.includes(key)) {
// hasExcludeField = true;
// break;
// }
// if (!changedValues[key]) {
// allFieldsHaveValue = false;
// }
// }
if (
!isExcludeField &&
hasValue &&
!_.has(changedValues, 'backend') &&
!_.has(changedValues, 'local_path')
) {
const values = form.current?.form.getFieldsValue?.();
const data = getSourceRepoConfigValue(props.source, values);
const evalutionData = await handleEvaluate(data.values);
handleShowCompatibleAlert?.(evalutionData);
}
};
const handleCancel = useCallback(() => {
onCancel?.();
}, [onCancel]);
@ -127,14 +227,24 @@ const AddModal: FC<AddModalProps> = (props) => {
});
setIsGGUF(props.isGGUF || false);
} else {
form.current?.setFieldValue?.('backend', backendOptionsMap.vllm);
setIsGGUF(false);
const backend =
source === modelSourceMap.ollama_library_value
? backendOptionsMap.llamaBox
: backendOptionsMap.vllm;
form.current?.setFieldValue?.('backend', backend);
setIsGGUF(backend === backendOptionsMap.llamaBox);
}
return () => {
setSelectedModel({});
setWarningStatus({
show: false,
title: '',
message: []
});
checkTokenRef.current?.cancel();
};
}, [open, props.isGGUF, props.initialValues, props.deploymentType]);
}, [open, props.isGGUF, source, props.initialValues, props.deploymentType]);
return (
<Drawer
@ -202,50 +312,80 @@ const AddModal: FC<AddModalProps> = (props) => {
</ColWrapper>
</>
)}
<FormWrapper>
<ColumnWrapper
paddingBottom={warningStatus.show ? 125 : 50}
footer={
<FormContext.Provider
value={{
isGGUF: isGGUF,
modelFileOptions: props.modelFileOptions
}}
>
<FormWrapper>
<ColumnWrapper
paddingBottom={
warningStatus.show
? Array.isArray(warningStatus.message)
? 150
: 125
: 50
}
footer={
<>
<CompatibilityAlert
showClose={true}
onClose={() => {
setWarningStatus({
show: false,
message: ''
});
}}
warningStatus={warningStatus}
contentStyle={{ paddingInline: 0 }}
></CompatibilityAlert>
<ModalFooter
onCancel={handleCancel}
onOk={handleSumit}
showOkBtn={!warningStatus.show}
extra={
warningStatus.show && (
<Button
type="primary"
onClick={handleSubmitAnyway}
style={{ width: '130px' }}
>
{intl.formatMessage({
id: 'models.form.submit.anyway'
})}
</Button>
)
}
style={ModalFooterStyle}
></ModalFooter>
</>
}
>
<>
<CompatibilityAlert
warningStatus={warningStatus}
></CompatibilityAlert>
<ModalFooter
onCancel={handleCancel}
onOk={handleSumit}
style={{
padding: '16px 24px',
display: 'flex',
justifyContent: 'flex-end'
}}
></ModalFooter>
{SEARCH_SOURCE.includes(source) &&
deploymentType === 'modelList' && (
<TitleWrapper>
{intl.formatMessage({ id: 'models.form.configurations' })}
</TitleWrapper>
)}
<DataForm
initialValues={initialValues}
source={source}
action={action}
selectedModel={selectedModel}
onOk={handleOnOk}
ref={form}
isGGUF={isGGUF}
gpuOptions={props.gpuOptions}
modelFileOptions={props.modelFileOptions}
onBackendChange={handleBackendChange}
onValuesChange={handleOnValuesChange}
></DataForm>
</>
}
>
<>
{SEARCH_SOURCE.includes(source) &&
deploymentType === 'modelList' && (
<TitleWrapper>
{intl.formatMessage({ id: 'models.form.configurations' })}
</TitleWrapper>
)}
<DataForm
initialValues={initialValues}
source={source}
action={action}
selectedModel={selectedModel}
onOk={onOk}
ref={form}
isGGUF={isGGUF}
gpuOptions={props.gpuOptions}
modelFileOptions={props.modelFileOptions}
handleShowCompatibleAlert={handleShowCompatibleAlert}
handleUpdateWarning={handleUpdateWarning}
onBackendChange={handleBackendChange}
></DataForm>
</>
</ColumnWrapper>
</FormWrapper>
</ColumnWrapper>
</FormWrapper>
</FormContext.Provider>
</div>
</Drawer>
);

@ -29,6 +29,7 @@ import IncompatiableInfo from './incompatiable-info';
import TitleWrapper from './title-wrapper';
interface HFModelFileProps {
isDownload?: boolean;
selectedModel: any;
collapsed?: boolean;
loadingModel?: boolean;
@ -73,8 +74,9 @@ const FilePartsTag = (props: { parts: any[] }) => {
};
const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
const { collapsed, modelSource } = props;
const { collapsed, modelSource, isDownload } = props;
const intl = useIntl();
const [isEvaluating, setIsEvaluating] = useState(false);
const [dataSource, setDataSource] = useState<any>({
fileList: [],
loading: false
@ -93,6 +95,7 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
]);
const axiosTokenRef = useRef<any>(null);
const checkTokenRef = useRef<any>(null);
const timer = useRef<any>(null);
const handleSelectModelFile = (item: any) => {
props.onSelectFile?.(item);
@ -241,30 +244,12 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
}
}, []);
const handleFetchModelFiles = async () => {
if (!props.selectedModel.name) {
setDataSource({ fileList: [], loading: false });
handleSelectModelFile({});
const handleEvaluate = async (list: any[]) => {
if (isDownload) {
return;
}
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
setDataSource({ ...dataSource, loading: true });
setCurrent('');
try {
let list = [];
if (modelSourceMap.huggingface_value === modelSource) {
list = await getHuggingfaceFiles();
} else if (modelSourceMap.modelscope_value === modelSource) {
list = await getModelScopeFiles();
}
const newList = generateGroupByFilename(list);
const sortList = _.sortBy(newList, (item: any) => {
return sortType === 'size' ? item.size : item.path;
});
const evaluateFileList = sortList.map((item: any) => {
const evaluateFileList = list.map((item: any) => {
return {
source: modelSource,
...(modelSource === modelSourceMap.huggingface_value
@ -279,17 +264,58 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
};
});
setIsEvaluating(true);
const evaluationList = await getEvaluateResults(evaluateFileList);
const resultList = _.map(sortList, (item: any, index: number) => {
const resultList = _.map(list, (item: any, index: number) => {
return {
...item,
evaluateResult: evaluationList[index]
};
});
handleSelectModelFile(resultList[0]);
setDataSource({ fileList: resultList, loading: false });
setIsEvaluating(false);
} catch (error) {
setIsEvaluating(false);
}
};
const handleFetchModelFiles = async () => {
if (!props.selectedModel.name) {
setDataSource({ fileList: [], loading: false });
handleSelectModelFile({});
return;
}
checkTokenRef.current?.cancel?.();
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
setDataSource({ ...dataSource, loading: true });
setCurrent('');
try {
let list = [];
if (modelSourceMap.huggingface_value === modelSource) {
list = await getHuggingfaceFiles();
} else if (modelSourceMap.modelscope_value === modelSource) {
list = await getModelScopeFiles();
}
const newList = generateGroupByFilename(list);
const sortList = _.sortBy(newList, (item: any) => {
return sortType === 'size' ? item.size : item.path;
});
handleSelectModelFile(sortList[0]);
setDataSource({ fileList: sortList, loading: false });
// if (timer.current) {
// clearTimeout(timer.current);
// }
// timer.current = setTimeout(() => {
// handleEvaluate(sortList);
// }, 200);
handleEvaluate(sortList);
} catch (error) {
setDataSource({ fileList: [], loading: false });
handleSelectModelFile({});
@ -341,12 +367,15 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
setDataSource({ fileList: [], loading: false });
handleSelectModelFile({});
}
}, [props.selectedModel.name]);
}, [props.selectedModel?.name]);
useEffect(() => {
return () => {
axiosTokenRef.current?.abort?.();
checkTokenRef.current?.cancel?.();
if (timer.current) {
clearTimeout(timer.current);
}
};
}, []);
@ -415,6 +444,7 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
<FilePartsTag parts={item.parts}></FilePartsTag>
</span>
<IncompatiableInfo
isEvaluating={isEvaluating}
data={item.evaluateResult}
></IncompatiableInfo>
</div>

@ -11,7 +11,6 @@ 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';
@ -26,16 +25,12 @@ interface HFModelItemProps {
source?: string;
tags?: string[];
evaluateResult?: EvaluateResult;
isEvaluating?: boolean;
}
const warningTask = ['video'];
const SUPPORTEDSOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const HFModelItem: React.FC<HFModelItemProps> = (props) => {
const { evaluateResult } = props;
const { evaluateResult, isEvaluating } = props;
console.log('evaluateResult', evaluateResult);
const intl = useIntl();
const isExcludeTask = useMemo(() => {
@ -80,7 +75,12 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
{formatNumber(props.downloads)}
</span>
</div>
{<IncompatiableInfo data={evaluateResult}></IncompatiableInfo>}
{
<IncompatiableInfo
data={evaluateResult}
isEvaluating={isEvaluating}
></IncompatiableInfo>
}
</div>
</div>
);

@ -1,5 +1,6 @@
import OverlayScroller from '@/components/overlay-scroller';
import { WarningOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Tag, Tooltip } from 'antd';
import React from 'react';
import styled from 'styled-components';
@ -7,6 +8,7 @@ import { EvaluateResult } from '../config/types';
interface IncompatiableInfoProps {
data?: EvaluateResult;
isEvaluating?: boolean;
}
const CompatibleTag = styled(Tag)`
@ -46,8 +48,17 @@ const SMTitle = styled.div<{ $isTitle?: boolean }>`
font-size: var(--font-size-small);
`;
const IncompatiableInfo: React.FC<IncompatiableInfoProps> = (props) => {
const { data } = props;
if (data?.compatible) {
const { data, isEvaluating } = props;
const intl = useIntl();
console.log('IncompatiableInfo', data, isEvaluating);
if (isEvaluating) {
return (
<CompatibleTag color="warning">
{intl.formatMessage({ id: 'models.form.evaluating' })}
</CompatibleTag>
);
}
if (!data || data?.compatible) {
return null;
}
return (
@ -71,7 +82,7 @@ const IncompatiableInfo: React.FC<IncompatiableInfoProps> = (props) => {
}
>
<CompatibleTag icon={<WarningOutlined />} color="warning">
Incompatible
{intl.formatMessage({ id: 'models.form.incompatible' })}
</CompatibleTag>
</Tooltip>
);

@ -213,7 +213,7 @@ const ModelCard: React.FC<{
};
const getModelCardData = async () => {
if (!props.selectedModel.name) {
if (!props.selectedModel?.name) {
setModelData(null);
setReadmeText(null);
handleOnCollapse(null);
@ -292,7 +292,7 @@ const ModelCard: React.FC<{
useEffect(() => {
getModelCardData();
}, [props.selectedModel.name]);
}, [props.selectedModel?.name]);
useEffect(() => {
return () => {

@ -20,8 +20,7 @@ import {
ModelScopeSortType,
ModelSortType,
ModelscopeTaskMap,
modelSourceMap,
ollamaModelOptions
modelSourceMap
} from '../config';
import SearchStyle from '../style/search-result.less';
import SearchInput from './search-input';
@ -29,14 +28,15 @@ import SearchResult from './search-result';
interface SearchInputProps {
modelSource: string;
isDownload?: boolean;
setLoadingModel?: (flag: boolean) => void;
onSourceChange?: (source: string) => void;
onSelectModel: (model: any) => void;
onSelectModel: (model: any, isGGUF?: boolean) => void;
}
const SearchModel: React.FC<SearchInputProps> = (props) => {
const intl = useIntl();
const { modelSource, setLoadingModel, onSelectModel } = props;
const { modelSource, isDownload, setLoadingModel, onSelectModel } = props;
const [dataSource, setDataSource] = useState<{
repoOptions: any[];
loading: boolean;
@ -52,13 +52,17 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const [isEvaluating, setIsEvaluating] = useState<boolean>(false);
const [current, setCurrent] = useState<string>('');
const cacheRepoOptions = useRef<any[]>([]);
const axiosTokenRef = useRef<any>(null);
const checkTokenRef = useRef<any>(null);
const evaluateTokenRef = useRef<any>(null);
const searchInputRef = useRef<any>('');
const filterGGUFRef = useRef<boolean | undefined>();
const filterTaskRef = useRef<string>('');
const timer = useRef<any>(null);
const workerRef = useRef<any>(null);
const modelFilesSortOptions = useRef<any[]>([
{
label: intl.formatMessage({ id: 'models.sort.trending' }),
@ -78,10 +82,20 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
}
]);
const handleOnSelectModel = useCallback((item: any) => {
onSelectModel(item);
const checkIsGGUF = (item: any) => {
const isGGUF = _.some(item.tags, (tag: string) => {
return tag.toLowerCase() === 'gguf';
});
const isGGUFFromMs = _.some(item.libraries, (tag: string) => {
return tag.toLowerCase() === 'gguf';
});
return isGGUF || isGGUFFromMs;
};
const handleOnSelectModel = (item: any) => {
onSelectModel(item, checkIsGGUF(item));
setCurrent(item.id);
}, []);
};
// huggeface
const getModelsFromHuggingface = useCallback(async (sort: string) => {
@ -136,7 +150,9 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
value: item.Name,
label: item.Name,
revision: item.Revision,
task: item.Tasks?.map((sItem: any) => sItem.Name).join(',')
task: item.Tasks?.map((sItem: any) => sItem.Name).join(','),
tags: item.Tags,
libraries: item.Libraries
};
});
@ -164,70 +180,131 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
}
}, []);
const handleOnSearchRepo = useCallback(
async (sortType?: string) => {
if (!SUPPORTEDSOURCE.includes(modelSource)) {
return;
}
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
const sort = sortType ?? dataSource.sortType;
try {
const handleEvaluate = async (list: any[]) => {
if (isDownload) {
return;
}
try {
const repoList = list.map((item) => {
return {
source: modelSource,
...(modelSource === modelSourceMap.huggingface_value
? {
huggingface_repo_id: item.name
}
: {
model_scope_model_id: item.name
})
};
});
setIsEvaluating(true);
const evaluations = await getEvaluateResults(repoList);
const resultList = list.map((item, index) => {
return {
...item,
evaluateResult: evaluations[index] || null
};
});
setIsEvaluating(false);
setDataSource((pre) => {
return {
...pre,
loading: false,
repoOptions: resultList
};
});
handleOnSelectModel(resultList[0]);
} catch (error) {
setIsEvaluating(false);
}
};
const handleEvaluateWorker = (params: {
list: any[];
modelSource: string;
modelSourceMap: any;
}) => {
console.log('handleEvaluateWorker=======');
const { list, modelSource, modelSourceMap } = params;
workerRef.current?.terminate();
setIsEvaluating(true);
workerRef.current = new Worker(
// @ts-ignore
new URL('../apis/evaluateWorker.ts', import.meta.url)
);
workerRef.current.postMessage({
list,
modelSource,
modelSourceMap
});
workerRef.current.onmessage = function (event: any) {
const { success, resultList } = event.data;
if (success) {
setDataSource((pre) => {
pre.loading = true;
return { ...pre };
});
setLoadingModel?.(true);
cacheRepoOptions.current = [];
let list: any[] = [];
if (modelSource === modelSourceMap.huggingface_value) {
list = await getModelsFromHuggingface(sort);
} else if (modelSource === modelSourceMap.modelscope_value) {
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]
...pre,
repoOptions: resultList
};
});
console.log('list:', evaluations);
setDataSource({
repoOptions: list,
loading: false,
networkError: false,
sortType: sort
});
setLoadingModel?.(false);
handleOnSelectModel(list[0]);
} catch (error: any) {
setDataSource({
repoOptions: [],
loading: false,
sortType: sort,
networkError: error?.message === 'Failed to fetch'
});
setLoadingModel?.(false);
handleOnSelectModel({});
cacheRepoOptions.current = [];
}
},
[dataSource.sortType, modelSource]
);
setIsEvaluating(false);
handleOnSelectModel(resultList[0]);
workerRef.current.terminate();
workerRef.current = null;
};
};
const handleOnSearchRepo = async (sortType?: string) => {
if (!SUPPORTEDSOURCE.includes(modelSource)) {
return;
}
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
checkTokenRef.current?.cancel?.();
if (timer.current) {
clearTimeout(timer.current);
}
const sort = sortType ?? dataSource.sortType;
try {
setDataSource((pre) => {
pre.loading = true;
return { ...pre };
});
setLoadingModel?.(true);
cacheRepoOptions.current = [];
let list: any[] = [];
if (modelSource === modelSourceMap.huggingface_value) {
list = await getModelsFromHuggingface(sort);
} else if (modelSource === modelSourceMap.modelscope_value) {
list = await getModelsFromModelscope(sort);
}
cacheRepoOptions.current = list;
console.log('list=========', list);
setDataSource({
repoOptions: list,
loading: false,
networkError: false,
sortType: sort
});
handleOnSelectModel(list[0]);
setLoadingModel?.(false);
timer.current = setTimeout(() => {
handleEvaluate(list);
}, 200);
} catch (error: any) {
setDataSource({
repoOptions: [],
loading: false,
sortType: sort,
networkError: error?.message === 'Failed to fetch'
});
setLoadingModel?.(false);
handleOnSelectModel({});
cacheRepoOptions.current = [];
}
};
const handleSearchInputChange = useCallback((e: any) => {
searchInputRef.current = e.target.value;
console.log('change:', searchInputRef.current);
@ -243,16 +320,6 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
) {
handleOnSearchRepo();
}
if (modelSourceMap.ollama_library_value === modelSource) {
setDataSource({
repoOptions: ollamaModelOptions,
loading: false,
networkError: false,
sortType: dataSource.sortType
});
cacheRepoOptions.current = ollamaModelOptions;
handleOnSelectModel(ollamaModelOptions[0]);
}
};
const handleSortChange = (value: string) => {
@ -264,11 +331,6 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
handleOnSearchRepo();
};
const handleFilterTaskChange = useCallback((value: string) => {
filterTaskRef.current = value;
handleOnSearchRepo();
}, []);
const renderGGUFTips = useMemo(() => {
return (
<Tooltip
@ -345,13 +407,16 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
useEffect(() => {
handleOnOpen();
console.log('SearchModel useEffect', modelSource);
}, [modelSource]);
useEffect(() => {
return () => {
axiosTokenRef.current?.abort?.();
checkTokenRef.current?.cancel?.();
// workerRef.current?.terminate();
if (timer.current) {
clearTimeout(timer.current);
}
};
}, []);
@ -364,6 +429,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
networkError={dataSource.networkError}
current={current}
source={modelSource}
isEvaluating={isEvaluating}
onSelect={handleOnSelectModel}
></SearchResult>
</div>

@ -17,10 +17,11 @@ interface SearchResultProps {
style?: React.CSSProperties;
loading?: boolean;
networkError?: boolean;
isEvaluating?: boolean;
}
const SearchResult: React.FC<SearchResultProps> = (props) => {
const { resultList, onSelect, source, networkError } = props;
const { resultList, onSelect, isEvaluating, source, networkError } = props;
const intl = useIntl();
const handleSelect = (e: any, item: any) => {
@ -111,6 +112,7 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
updatedAt={item.updatedAt}
evaluateResult={item.evaluateResult}
active={item.id === props.current}
isEvaluating={isEvaluating}
/>
</div>
</Col>

@ -71,7 +71,12 @@ import {
setModelActionList,
sourceOptions
} from '../config/button-actions';
import { FormData, ListItem, ModelInstanceListItem } from '../config/types';
import {
FormData,
ListItem,
ModelInstanceListItem,
SourceType
} from '../config/types';
import { useGenerateFormEditInitialValues } from '../hooks';
import DeployModal from './deploy-modal';
import Instances from './instances';
@ -174,13 +179,13 @@ const Models: React.FC<ModelsProps> = ({
const [openDeployModal, setOpenDeployModal] = useState<{
show: boolean;
width: number | string;
source: string;
source: SourceType;
gpuOptions: any[];
modelFileOptions?: any[];
}>({
show: false,
width: 600,
source: modelSourceMap.huggingface_value,
source: modelSourceMap.huggingface_value as SourceType,
gpuOptions: [],
modelFileOptions: []
});

@ -0,0 +1,41 @@
import React from 'react';
interface FormContextProps {
isGGUF: boolean;
byBuiltIn?: boolean;
sizeOptions?: Global.BaseOption<number>[];
quantizationOptions?: Global.BaseOption<string>[];
modelFileOptions?: any[];
onSizeChange?: (val: number) => void;
onQuantizationChange?: (val: string) => void;
}
interface FormInnerContextProps {
onBackendChange?: (backend: string) => void;
}
export const FormContext = React.createContext<FormContextProps>(
{} as FormContextProps
);
export const FormInnerContext = React.createContext<FormInnerContextProps>(
{} as FormInnerContextProps
);
export const useFormContext = () => {
const context = React.useContext(FormContext);
if (!context) {
throw new Error('useFormContext must be used within a FormProvider');
}
return context;
};
export const useFormInnerContext = () => {
const context = React.useContext(FormInnerContext);
if (!context) {
throw new Error(
'useFormInnerContext must be used within a FormInnerProvider'
);
}
return context;
};

@ -177,14 +177,22 @@ export const AudioModeTypeMap = {
Whisper: ['Whisper', 'whisper'],
CosyVoice: ['CosyVoice', 'cosyvoice', 'cosy-voice', 'cosy_voice']
};
export const modelSourceMap: Record<string, string> = {
interface ModelSource {
huggingface: string;
huggingface_value: string;
ollama_library: string;
ollama_library_value: string;
modelScope: string;
modelscope_value: string;
local_path: string;
local_path_value: string;
}
export const modelSourceMap: ModelSource = {
huggingface: 'Hugging Face',
ollama_library: 'Ollama Library',
s3: 'S3',
huggingface_value: 'huggingface',
ollama_library: 'Ollama Library',
ollama_library_value: 'ollama_library',
s3_value: 's3',
modelScope: 'ModelScope',
modelscope_value: 'model_scope',
local_path: 'Local Path',
@ -194,7 +202,6 @@ export const modelSourceMap: Record<string, string> = {
export const modelSourceValueMap = {
[modelSourceMap.huggingface_value]: modelSourceMap.huggingface,
[modelSourceMap.ollama_library_value]: modelSourceMap.ollama_library,
[modelSourceMap.s3_value]: modelSourceMap.s3,
[modelSourceMap.modelscope_value]: modelSourceMap.modelScope,
[modelSourceMap.local_path_value]: modelSourceMap.local_path
};
@ -459,5 +466,7 @@ export const excludeFields = [
'name',
'description',
'env',
'source'
'source',
'quantization',
'size'
];

@ -29,6 +29,12 @@ export interface ListItem {
worker_selector?: object;
}
export type SourceType =
| 'huggingface'
| 'model_scope'
| 'local_path'
| 'ollama_library';
export interface FormData {
backend: string;
env?: Record<string, any>;

@ -174,6 +174,7 @@ const DownloadModel: React.FC<AddModalProps> = (props) => {
<SearchModel
modelSource={props.source}
onSelectModel={handleOnSelectModel}
isDownload={true}
></SearchModel>
</ColumnWrapper>
<Separator></Separator>
@ -200,6 +201,7 @@ const DownloadModel: React.FC<AddModalProps> = (props) => {
modelSource={props.source}
onSelectFile={handleSelectModelFile}
collapsed={collapsed}
isDownload={true}
></HFModelFile>
)}
</ColumnWrapper>

@ -0,0 +1,84 @@
import SealSelect from '@/components/seal-form/seal-select';
import useAppUtils from '@/hooks/use-app-utils';
import { Form } from 'antd';
import React from 'react';
import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types';
const CatalogForm: React.FC = () => {
const formCtx = useFormContext();
const { getRuleMessage } = useAppUtils();
const {
isGGUF,
byBuiltIn,
sizeOptions,
quantizationOptions,
onSizeChange,
onQuantizationChange
} = formCtx;
const source = Form.useWatch('source');
console.log('HuggingFaceForm', { source, isGGUF });
if (!byBuiltIn && !sizeOptions?.length && !quantizationOptions?.length) {
return null;
}
const handleSizeChange = (val: any) => {
onSizeChange?.(val);
};
const handleOnQuantizationChange = (val: any) => {
onQuantizationChange?.(val);
};
return (
<>
{sizeOptions && sizeOptions?.length > 0 && (
<Form.Item<FormData>
name="size"
key="size"
rules={[
{
required: true,
message: getRuleMessage('input', 'size', false)
}
]}
>
<SealSelect
filterOption
onChange={handleSizeChange}
defaultActiveFirstOption
disabled={false}
options={sizeOptions}
label="Size"
required
></SealSelect>
</Form.Item>
)}
{quantizationOptions && quantizationOptions?.length > 0 && (
<Form.Item<FormData>
name="quantization"
key="quantization"
rules={[
{
required: true,
message: getRuleMessage('select', 'quantization', false)
}
]}
>
<SealSelect
filterOption
defaultActiveFirstOption
disabled={false}
options={quantizationOptions}
onChange={handleOnQuantizationChange}
label="Quantization"
required
></SealSelect>
</Form.Item>
)}
</>
);
};
export default CatalogForm;

@ -0,0 +1,68 @@
import SealInput from '@/components/seal-form/seal-input';
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import React from 'react';
import { modelSourceMap } from '../config';
import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types';
const HuggingFaceForm: React.FC = () => {
const formCtx = useFormContext();
const { getRuleMessage } = useAppUtils();
const intl = useIntl();
const { isGGUF, byBuiltIn } = formCtx;
const source = Form.useWatch('source');
console.log('HuggingFaceForm', { source, isGGUF });
if (
![
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
].includes(source) ||
byBuiltIn
) {
return null;
}
return (
<>
<Form.Item<FormData>
name="repo_id"
key="repo_id"
rules={[
{
required: true,
message: getRuleMessage('input', 'models.form.repoid')
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'models.form.repoid' })}
required
disabled={true}
></SealInput.Input>
</Form.Item>
{isGGUF && (
<Form.Item<FormData>
name="file_name"
key="file_name"
rules={[
{
required: true,
message: getRuleMessage('input', 'models.form.filename')
}
]}
>
<SealInput.Input
label={intl.formatMessage({ id: 'models.form.filename' })}
required
disabled={true}
></SealInput.Input>
</Form.Item>
)}
</>
);
};
export default HuggingFaceForm;

@ -0,0 +1,79 @@
import SealAutoComplete from '@/components/seal-form/auto-complete';
import TooltipList from '@/components/tooltip-list';
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import _ from 'lodash';
import React, { useRef } from 'react';
import {
backendOptionsMap,
localPathTipsList,
modelSourceMap
} from '../config';
import { useFormContext, useFormInnerContext } from '../config/form-context';
import { FormData } from '../config/types';
const LocalPathForm: React.FC = () => {
const form = Form.useFormInstance();
const formCtx = useFormContext();
const formInnerCtx = useFormInnerContext();
const source = Form.useWatch('source', form);
const { onBackendChange } = formInnerCtx;
const { modelFileOptions, byBuiltIn } = formCtx;
const { getRuleMessage } = useAppUtils();
const intl = useIntl();
const localPathCache = useRef<string>(form.getFieldValue('local_path'));
if (![modelSourceMap.local_path_value].includes(source) || byBuiltIn) {
return null;
}
const handleLocalPathBlur = (e: any) => {
const value = e.target.value;
if (value === localPathCache.current && value) {
return;
}
const isEndwithGGUF = _.endsWith(value, '.gguf');
const isBlobFile = value.split('/').pop().includes('sha256');
let backend = backendOptionsMap.llamaBox;
if (!isEndwithGGUF && !isBlobFile) {
backend = backendOptionsMap.vllm;
}
form.setFieldValue('backend', backend);
onBackendChange?.(backend);
};
const handleOnFocus = () => {
localPathCache.current = form.getFieldValue('local_path');
};
return (
<>
<Form.Item<FormData>
name="local_path"
key="local_path"
rules={[
{
required: true,
message: getRuleMessage('input', 'models.form.filePath')
}
]}
>
<SealAutoComplete
allowClear
required
filterOption
defaultActiveFirstOption
options={modelFileOptions}
onBlur={handleLocalPathBlur}
onFocus={handleOnFocus}
label={intl.formatMessage({ id: 'models.form.filePath' })}
description={<TooltipList list={localPathTipsList}></TooltipList>}
></SealAutoComplete>
</Form.Item>
</>
);
};
export default LocalPathForm;

@ -0,0 +1,67 @@
import IconFont from '@/components/icon-font';
import SealAutoComplete from '@/components/seal-form/auto-complete';
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max';
import { Form, Typography } from 'antd';
import React from 'react';
import { modelSourceMap, ollamaModelOptions } from '../config';
import { useFormContext, useFormInnerContext } from '../config/form-context';
import { FormData } from '../config/types';
const OllamaForm: React.FC = () => {
const formCtx = useFormContext();
const formInnerCtx = useFormInnerContext();
const { byBuiltIn } = formCtx;
const { getRuleMessage } = useAppUtils();
const intl = useIntl();
const source = Form.useWatch('source');
const formInstance = Form.useFormInstance();
if (![modelSourceMap.ollama_library_value].includes(source) || byBuiltIn) {
return null;
}
return (
<>
<Form.Item<FormData>
name="ollama_library_model_name"
key="ollama_library_model_name"
rules={[
{
required: true,
message: getRuleMessage('input', 'models.table.name')
}
]}
>
<SealAutoComplete
allowClear
filterOption
defaultActiveFirstOption
disabled={false}
options={ollamaModelOptions}
description={
<span>
<span>
{intl.formatMessage({ id: 'models.form.ollamalink' })}
</span>
<Typography.Link
className="flex-center"
href="https://www.ollama.com/library"
target="_blank"
>
<IconFont
type="icon-external-link"
className="font-size-14"
></IconFont>
</Typography.Link>
</span>
}
label={intl.formatMessage({ id: 'model.form.ollama.model' })}
placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })}
required
></SealAutoComplete>
</Form.Item>
</>
);
};
export default OllamaForm;

@ -1,9 +1,10 @@
import { createAxiosToken } from '@/hooks/use-chunk-request';
import { queryModelFilesList } from '@/pages/resources/apis';
import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
import { useIntl } from '@umijs/max';
import _ from 'lodash';
import { useRef, useState } from 'react';
import { queryGPUList } from '../apis';
import { useEffect, useRef, useState } from 'react';
import { evaluationsModelSpec, queryGPUList } from '../apis';
import {
backendOptionsMap,
modelSourceMap,
@ -185,6 +186,7 @@ export const useGenerateModelFileOptions = () => {
export const useCheckCompatibility = () => {
const intl = useIntl();
const checkTokenRef = useRef<any>(null);
const [warningStatus, setWarningStatus] = useState<{
show: boolean;
title?: string;
@ -195,6 +197,38 @@ export const useCheckCompatibility = () => {
message: []
});
const handleEvaluate = async (data: any) => {
try {
checkTokenRef.current?.cancel();
checkTokenRef.current = createAxiosToken();
setWarningStatus({
show: true,
title: '',
message: 'Evaluating compatibility...'
});
const evalution = await evaluationsModelSpec(
{
model_specs: [
{
..._.omit(data, ['scheduleType']),
categories: Array.isArray(data.categories)
? data.categories
: data.categories
? [data.categories]
: []
}
]
},
{
token: checkTokenRef.current.token
}
);
return evalution.results?.[0];
} catch (error) {
return null;
}
};
const handleCheckCompatibility = (evaluateResult: EvaluateResult | null) => {
if (!evaluateResult) {
return {
@ -277,13 +311,22 @@ export const useCheckCompatibility = () => {
source: string;
}) => {
const warningMessage = updateShowWarning(params);
setWarningStatus(warningMessage);
return warningMessage;
};
useEffect(() => {
return () => {
checkTokenRef.current?.cancel();
checkTokenRef.current = null;
};
}, []);
return {
handleShowCompatibleAlert,
handleUpdateWarning,
warningStatus
warningStatus,
checkTokenRef,
handleEvaluate,
setWarningStatus
};
};

@ -26,6 +26,7 @@
color: var(--ant-color-text-tertiary);
font-size: var(--font-size-small);
justify-content: space-between;
height: 22px;
.info-item {
display: flex;

Loading…
Cancel
Save