chore: check compatibility ux
This commit is contained in:
@@ -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,12 +244,51 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleEvaluate = async (list: any[]) => {
|
||||
if (isDownload) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const evaluateFileList = list.map((item: any) => {
|
||||
return {
|
||||
source: modelSource,
|
||||
...(modelSource === modelSourceMap.huggingface_value
|
||||
? {
|
||||
huggingface_repo_id: props.selectedModel.name,
|
||||
huggingface_filename: item.fakeName
|
||||
}
|
||||
: {
|
||||
model_scope_model_id: props.selectedModel.name,
|
||||
model_scope_file_path: item.fakeName
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
setIsEvaluating(true);
|
||||
|
||||
const evaluationList = await getEvaluateResults(evaluateFileList);
|
||||
|
||||
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 });
|
||||
@@ -264,32 +306,16 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
|
||||
return sortType === 'size' ? item.size : item.path;
|
||||
});
|
||||
|
||||
const evaluateFileList = sortList.map((item: any) => {
|
||||
return {
|
||||
source: modelSource,
|
||||
...(modelSource === modelSourceMap.huggingface_value
|
||||
? {
|
||||
huggingface_repo_id: props.selectedModel.name,
|
||||
huggingface_filename: item.fakeName
|
||||
}
|
||||
: {
|
||||
model_scope_model_id: props.selectedModel.name,
|
||||
model_scope_file_path: item.fakeName
|
||||
})
|
||||
};
|
||||
});
|
||||
handleSelectModelFile(sortList[0]);
|
||||
setDataSource({ fileList: sortList, loading: false });
|
||||
|
||||
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 });
|
||||
// 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
|
||||
})
|
||||
...pre,
|
||||
repoOptions: resultList
|
||||
};
|
||||
});
|
||||
const evaluations = await getEvaluateResults(repoList);
|
||||
list = list.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
evaluateResult: evaluations[index]
|
||||
};
|
||||
});
|
||||
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']
|
||||
};
|
||||
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: Record<string, 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;
|
||||
|
||||
Reference in New Issue
Block a user