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