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