chore: compatibility api

main
jialin 6 months ago
parent f9469a1ea3
commit bd6c689d91

@ -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;

@ -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>
</>

Loading…
Cancel
Save