chore: catalog card style

main
jialin 1 year ago
parent d9319f5734
commit 1a792254ca

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

@ -204,6 +204,10 @@
margin-bottom: 16px;
}
.m-r-16 {
margin-right: 16px;
}
.font-400 {
font-weight: var(--font-weight-normal);
}

@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
// import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_4613488_trmdczqx9z.js'
scriptUrl: '//at.alicdn.com/t/c/font_4613488_nlj2pbwi.js'
});
export default IconFont;

@ -6,7 +6,8 @@ import './index.less';
const TagsWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const wrapperRef = useRef<HTMLDivElement>(null);
const observer = useRef<IntersectionObserver | null>(null);
const [hiddenIndex, setHiddenIndex] = useState<number>(0);
const resizeObserver = useRef<ResizeObserver | null>(null);
const [hiddenIndex, setHiddenIndex] = useState<number>(4);
const uid = useRef(0);
const updateUid = () => {
@ -17,55 +18,72 @@ const TagsWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const dropItems = useMemo(() => {
return React.Children.toArray(children)
.slice(hiddenIndex)
.map((child, index) => ({
.map((child) => ({
label: child,
key: updateUid()
}));
}, [children, hiddenIndex]);
const updateHiddenIndex = () => {
if (!wrapperRef.current || !observer.current) return;
const childrenList = Array.from(wrapperRef.current.children);
let newHiddenIndex = 0;
childrenList.forEach((child, index) => {
const rect = child.getBoundingClientRect();
// visible in wrapperRef
const issivible =
rect.top < wrapperRef.current!.clientHeight &&
rect.bottom > 0 &&
rect.left < wrapperRef.current!.clientWidth &&
rect.right > 0;
if (!issivible) {
newHiddenIndex = Math.max(newHiddenIndex, index + 1);
}
});
setHiddenIndex(newHiddenIndex);
};
useEffect(() => {
if (!wrapperRef.current) return;
observer.current = new IntersectionObserver(
(entries) => {
let newHiddenIndex = 0;
entries.forEach((entry, index) => {
if (entry.intersectionRatio < 1) {
newHiddenIndex = Math.min(newHiddenIndex, index);
}
console.log(
'isIntersecting=======',
entry.intersectionRatio,
newHiddenIndex
);
});
setHiddenIndex(() => newHiddenIndex);
},
{
root: wrapperRef.current,
threshold: 0
}
);
// observer.current = new IntersectionObserver(
// (entries) => {
// const lastEntry = entries[entries.length - 1];
// if (lastEntry && lastEntry.intersectionRatio < 1) {
// setHiddenIndex((prev) =>
// Math.max(prev, React.Children.count(children))
// );
// }
// },
// { root: wrapperRef.current, threshold: 1 }
// );
const childrenList = wrapperRef.current.children;
Array.from(childrenList).forEach((child) => {
observer.current?.observe(child);
// const childrenList = wrapperRef.current.children;
// if (childrenList.length > 0) {
// observer.current.observe(childrenList[childrenList.length - 1]);
// }
resizeObserver.current = new ResizeObserver(() => {
// updateHiddenIndex();
});
resizeObserver.current.observe(wrapperRef.current);
return () => {
observer.current?.disconnect();
resizeObserver.current?.disconnect();
};
}, [children]);
return (
<div className="tags-wrapper" ref={wrapperRef}>
<span>{hiddenIndex}</span>
{React.Children.toArray(children).slice(
0,
React.Children.toArray(children).length - hiddenIndex
)}
{hiddenIndex > 0 && (
{React.Children.toArray(children).slice(0, hiddenIndex)}
{React.Children.toArray(children).length > 4 && (
<Dropdown
trigger={['hover']}
menu={{
items: dropItems
}}

@ -4,6 +4,7 @@ import { request } from '@umijs/max';
import qs from 'query-string';
import {
CatalogItem,
CatalogSpec,
FormData,
GPUListItem,
ListItem,
@ -322,15 +323,25 @@ export async function queryCatalogList(
params: Global.SearchParams,
options?: any
) {
return request<Global.PageResponse<CatalogItem>>(`/model-sets`, {
methos: 'GET',
...options,
params
});
return request<Global.PageResponse<CatalogItem>>(
`/model-sets?${qs.stringify(params)}`,
{
methos: 'GET',
...options
}
);
}
export async function queryCatalogItemSpec(id: number) {
return request(`/model-sets/${id}/specs`, {
method: 'GET'
});
export async function queryCatalogItemSpec(
params: { id: number },
options?: any
) {
return await request<Global.PageResponse<CatalogSpec>>(
`/model-sets/${params.id}/specs`,
{
method: 'GET',
...options,
params
}
);
}

@ -3,19 +3,29 @@ import { PageAction } from '@/config';
import breakpoints from '@/config/breakpoints';
import { SyncOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import { Button, Col, Input, Pagination, Row, Space, message } from 'antd';
import { useIntl, useNavigate } from '@umijs/max';
import {
Button,
Col,
Input,
Pagination,
Row,
Select,
Space,
message
} from 'antd';
import _ from 'lodash';
import ResizeObserver from 'rc-resize-observer';
import React, { useCallback, useEffect, useState } from 'react';
import { createModel, queryCatalogList } from './apis';
import CatalogItem from './components/catalog-item';
import DelopyBuiltInModal from './components/deploy-builtin-modal';
import { getSourceRepoConfigValue, modelSourceMap } from './config';
import { modelCategories, modelSourceMap } from './config';
import { CatalogItem as CatalogItemType, FormData } from './config/types';
const Catalog: React.FC = () => {
const intl = useIntl();
const navigate = useNavigate();
const [span, setSpan] = React.useState(8);
const [activeId, setActiveId] = React.useState(-1);
const [dataSource, setDataSource] = useState<{
@ -30,11 +40,13 @@ const Catalog: React.FC = () => {
const [queryParams, setQueryParams] = useState({
page: 1,
perPage: 9,
search: ''
search: '',
categories: []
});
const [openDeployModal, setOpenDeployModal] = useState<any>({
show: false,
width: 600,
current: {},
source: modelSourceMap.huggingface_value
});
@ -47,7 +59,7 @@ const Catalog: React.FC = () => {
const params = {
..._.pickBy(queryParams, (val: any) => !!val)
};
const res = await queryCatalogList(params);
const res: any = await queryCatalogList(params);
setDataSource({
dataList: res.items,
@ -94,6 +106,7 @@ const Catalog: React.FC = () => {
setOpenDeployModal({
show: true,
source: modelSourceMap.huggingface_value,
current: item,
width: 600
});
}, []);
@ -103,12 +116,9 @@ const Catalog: React.FC = () => {
try {
console.log('data:', data, openDeployModal);
const result = getSourceRepoConfigValue(openDeployModal.source, data);
const modelData = await createModel({
data: {
...result.values,
..._.omit(data, result.omits)
..._.omit(data, ['size', 'quantization'])
}
});
setOpenDeployModal({
@ -116,18 +126,22 @@ const Catalog: React.FC = () => {
show: false
});
message.success(intl.formatMessage({ id: 'common.message.success' }));
navigate('/models/list');
} catch (error) {}
},
[openDeployModal]
);
const handleOnPageChange = useCallback((page: number, pageSize?: number) => {
setQueryParams({
...queryParams,
page,
perPage: pageSize || 10
});
}, []);
const handleOnPageChange = useCallback(
(page: number, pageSize?: number) => {
setQueryParams({
...queryParams,
page,
perPage: pageSize || 10
});
},
[queryParams]
);
const handleSearch = (e: any) => {
fetchData();
@ -136,10 +150,19 @@ const Catalog: React.FC = () => {
const handleNameChange = (e: any) => {
setQueryParams({
...queryParams,
page: 1,
search: e.target.value
});
};
const handleCategoryChange = (value: any) => {
setQueryParams({
...queryParams,
page: 1,
categories: value
});
};
useEffect(() => {
fetchData();
}, [queryParams]);
@ -159,11 +182,21 @@ const Catalog: React.FC = () => {
<Space>
<Input
placeholder={intl.formatMessage({ id: 'common.filter.name' })}
style={{ width: 300 }}
style={{ width: 200 }}
size="large"
allowClear
onChange={handleNameChange}
></Input>
<Select
allowClear
placeholder="Filter by category"
style={{ width: 240 }}
size="large"
mode="multiple"
maxTagCount={1}
onChange={handleCategoryChange}
options={modelCategories.filter((item) => item.value)}
></Select>
<Button
type="text"
style={{ color: 'var(--ant-color-text-tertiary)' }}
@ -190,6 +223,7 @@ const Catalog: React.FC = () => {
</ResizeObserver>
<div style={{ marginBlock: '32px 16px' }}>
<Pagination
pageSizeOptions={['9', '12', '36', '100']}
hideOnSinglePage={queryParams.perPage === 9}
align="end"
defaultCurrent={1}
@ -205,6 +239,7 @@ const Catalog: React.FC = () => {
title={intl.formatMessage({ id: 'models.button.deploy' })}
source={openDeployModal.source}
width={openDeployModal.width}
current={openDeployModal.current}
onCancel={handleDeployModalCancel}
onOk={handleCreateModel}
></DelopyBuiltInModal>

@ -140,16 +140,6 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
form.setFieldValue('backend_parameters', list);
}, []);
const handleBackendChange = useCallback((val: string) => {
if (val === backendOptionsMap.llamaBox) {
form.setFieldsValue({
distributed_inference_across_workers: true,
cpu_offloading: true
});
}
form.setFieldValue('backend_version', '');
}, []);
const collapseItems = useMemo(() => {
const children = (
<>

@ -1,11 +1,13 @@
import fallbackImg from '@/assets/images/img.png';
import IMG from '@/assets/images/small-logo-200x200.png';
import AutoTooltip from '@/components/auto-tooltip';
import IconFont from '@/components/icon-font';
import { ClockCircleOutlined } from '@ant-design/icons';
import { Divider, Tag, Typography } from 'antd';
import TagWrapper from '@/components/tags-wrapper';
import { Tag, Typography } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import React, { useCallback, useMemo } from 'react';
import { modelCategories } from '../config';
import { CatalogItem as CatalogItemType } from '../config/types';
import '../style/catalog-item.less';
@ -26,13 +28,13 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
const home = data.home?.replace(/\/$/, '');
const icon = data.icon?.replace(/^\//, '');
if (icon) {
return `${home}/${icon}`;
return data.icon;
}
return IMG;
}, [data]);
const handleOnError = (e: any) => {
e.target.src = IMG;
e.target.src = fallbackImg;
};
return (
@ -43,35 +45,56 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
<div className="content">
<div className="title">
<div className="img">
<img src={icon} alt="" onError={handleOnError} />
<img
src={data.icon || fallbackImg}
alt=""
onError={handleOnError}
/>
</div>
<AutoTooltip ghost style={{ flex: 1 }}>
{data.name}
</AutoTooltip>
</div>
<Typography.Paragraph className="desc" ellipsis={{ rows: 2 }}>
<Typography.Paragraph
className="desc"
ellipsis={{
rows: 2,
tooltip: (
<div
className="custome-scrollbar"
style={{
display: 'flex',
justifyContent: 'flex-start',
maxHeight: 300,
maxWidth: 300,
overflow: 'auto'
}}
>
{data.description}
</div>
)
}}
>
{data.description}
</Typography.Paragraph>
</div>
<div className="item-footer">
<div className="update-time">
<span className="flex-center">
<ClockCircleOutlined
<IconFont
type="icon-new_release_outlined"
className="m-r-5"
style={{ color: 'var(--ant-color-text-secondary)' }}
/>
></IconFont>
{data.release_date}
</span>
<span className="flex-center">
<IconFont type="icon-justice" className="m-r-5"></IconFont>
{_.map(data.licenses, (license: string, index: number) => {
return (
<>
<span key={license}>{license}</span>
{index !== data.licenses.length - 1 && (
<Divider type="vertical" />
)}
</>
<span key={license} className="flex-center m-r-16">
<IconFont type="icon-justice" className="m-r-5"></IconFont>
<span>{license}</span>
</span>
);
})}
</span>
@ -79,21 +102,37 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
<div className="tags">
{data.categories.map((sItem, i) => {
return (
<Tag key={sItem} className="tag-item" color={COLORS[i]}>
{sItem}
<Tag key={sItem} className="tag-item" color="blue">
{_.find(modelCategories, { value: sItem })?.label || sItem}
</Tag>
);
})}
{data.capabilities?.length > 0 &&
data.capabilities.map((sItem, i) => {
return (
<Tag key={sItem} className="tag-item" color="purple">
{_.map(_.split(sItem, '/'), (s: string) => {
return _.split(s, '_').join(' ');
})
.reverse()
.join(' ')}
</Tag>
);
})}
{data.sizes?.length > 0 && (
<>
<span className="dot"></span>
{data.sizes.map((sItem, i) => {
return (
<Tag key={sItem} className="tag-item">
{sItem}B
</Tag>
);
})}
<div className="box">
<TagWrapper>
{data.sizes.map((sItem, i) => {
return (
<Tag key={sItem} className="tag-item">
{sItem}B
</Tag>
);
})}
</TagWrapper>
</div>
</>
)}
</div>

@ -23,7 +23,8 @@ import {
backendOptionsMap,
modelSourceMap,
modelTaskMap,
ollamaModelOptions
ollamaModelOptions,
sourceOptions
} from '../config';
import { HuggingFaceModels, ModelScopeModels } from '../config/audio-catalog';
import { FormData, GPUListItem } from '../config/types';
@ -40,6 +41,10 @@ interface DataFormProps {
sourceDisable?: boolean;
byBuiltIn?: boolean;
backendOptions?: Global.BaseOption<string>[];
sourceList?: Global.BaseOption<string>[];
onSizeChange?: (val: number) => void;
onQuantizationChange?: (val: string) => void;
onSourceChange?: (value: string) => void;
onOk: (values: FormData) => void;
onBackendChange?: (value: string) => void;
}
@ -49,37 +54,17 @@ const SEARCH_SOURCE = [
modelSourceMap.modelscope_value
];
const sourceOptions = [
{
label: 'Hugging Face',
value: modelSourceMap.huggingface_value,
key: 'huggingface'
},
{
label: 'Ollama Library',
value: modelSourceMap.ollama_library_value,
key: 'ollama_library'
},
{
label: 'ModelScope',
value: modelSourceMap.modelscope_value,
key: 'model_scope'
},
{
label: 'models.form.localPath',
locale: true,
value: modelSourceMap.local_path_value,
key: 'local_path'
}
];
const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const {
action,
isGGUF,
sourceDisable = true,
backendOptions,
sourceList,
byBuiltIn,
sizeOptions = [],
quantizationOptions = [],
onSourceChange,
onOk
} = props;
const [form] = Form.useForm();
@ -349,71 +334,83 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
};
const handleSizeChange = (val: any) => {
form.setFieldsValue({
quantization: ''
});
props.onSizeChange?.(val);
};
const handleOnQuantizationChange = (val: any) => {
props.onQuantizationChange?.(val);
};
// from catalog
const renderFieldsFromCatalog = useMemo(() => {
if (!byBuiltIn) {
if (!byBuiltIn && !sizeOptions?.length && !quantizationOptions?.length) {
return null;
}
return (
<>
<Form.Item<FormData>
name="size"
key="size"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.select'
},
{ name: 'size' }
)
}
]}
>
<SealAutoComplete
filterOption
onChange={handleSizeChange}
defaultActiveFirstOption
disabled={false}
options={props.sizeOptions}
label="Size"
required
></SealAutoComplete>
</Form.Item>
<Form.Item<FormData>
name="quantization"
key="quantization"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.select'
},
{ name: 'quantization' }
)
}
]}
>
<SealAutoComplete
filterOption
defaultActiveFirstOption
disabled={false}
options={props.quantizationOptions}
label="Quantization"
required
></SealAutoComplete>
</Form.Item>
{sizeOptions?.length > 0 && (
<Form.Item<FormData>
name="size"
key="size"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.select'
},
{ name: 'size' }
)
}
]}
>
<SealSelect
filterOption
onChange={handleSizeChange}
defaultActiveFirstOption
disabled={false}
options={sizeOptions}
label="Size"
required
></SealSelect>
</Form.Item>
)}
{quantizationOptions?.length > 0 && (
<Form.Item<FormData>
name="quantization"
key="quantization"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.select'
},
{ name: 'quantization' }
)
}
]}
>
<SealSelect
filterOption
defaultActiveFirstOption
disabled={false}
options={quantizationOptions}
onChange={handleOnQuantizationChange}
label="Quantization"
required
></SealSelect>
</Form.Item>
)}
</>
);
}, [props.sizeOptions, props.quantizationOptions, byBuiltIn]);
}, [sizeOptions, quantizationOptions, byBuiltIn]);
const renderFieldsBySource = useMemo(() => {
// from catalog
if (byBuiltIn) {
return null;
}
if (SEARCH_SOURCE.includes(props.source)) {
return renderHuggingfaceFields();
}
@ -427,7 +424,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
}
return null;
}, [props.source, isGGUF, intl]);
}, [props.source, isGGUF, byBuiltIn, intl]);
const handleBackendChange = useCallback((val: string) => {
if (val === backendOptionsMap.llamaBox) {
@ -437,6 +434,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
});
}
form.setFieldValue('backend_version', '');
props.onBackendChange?.(val);
}, []);
const handleOk = (formdata: FormData) => {
@ -451,6 +449,10 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
});
};
const handleOnSourceChange = (val: string) => {
onSourceChange?.(val);
};
useEffect(() => {
if (action === PageAction.EDIT) return;
if (modelTask.type === modelTaskMap.audio) {
@ -523,11 +525,12 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
>
{
<SealSelect
onChange={handleOnSourceChange}
disabled={sourceDisable}
label={intl.formatMessage({
id: 'models.form.source'
})}
options={sourceOptions}
options={sourceList ?? sourceOptions}
required
></SealSelect>
}
@ -535,33 +538,9 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
}
{renderFieldsBySource}
<Form.Item<FormData>
name="replicas"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.input'
},
{
name: intl.formatMessage({ id: 'models.form.replicas' })
}
)
}
]}
>
<SealInput.Number
style={{ width: '100%' }}
label={intl.formatMessage({
id: 'models.form.replicas'
})}
required
min={0}
></SealInput.Number>
</Form.Item>
<Form.Item name="backend">
<Form.Item name="backend" rules={[{ required: true }]}>
<SealSelect
required
onChange={handleBackendChange}
label={intl.formatMessage({ id: 'models.form.backend' })}
description={
@ -609,6 +588,31 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
></SealSelect>
</Form.Item>
{renderFieldsFromCatalog}
<Form.Item<FormData>
name="replicas"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.input'
},
{
name: intl.formatMessage({ id: 'models.form.replicas' })
}
)
}
]}
>
<SealInput.Number
style={{ width: '100%' }}
label={intl.formatMessage({
id: 'models.form.replicas'
})}
required
min={0}
></SealInput.Number>
</Form.Item>
<Form.Item<FormData> name="description">
<SealInput.TextArea
label={intl.formatMessage({

@ -1,12 +1,14 @@
import ModalFooter from '@/components/modal-footer';
import { PageActionType } from '@/config/types';
import { createAxiosToken } from '@/hooks/use-chunk-request';
import { CloseOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Drawer } from 'antd';
import { debounce } from 'lodash';
import _ from 'lodash';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { backendOptionsMap, modelSourceMap } from '../config';
import { FormData, ListItem } from '../config/types';
import { queryCatalogItemSpec } from '../apis';
import { backendOptionsMap, modelSourceMap, sourceOptions } from '../config';
import { CatalogSpec, FormData, ListItem } from '../config/types';
import ColumnWrapper from './column-wrapper';
import DataForm from './data-form';
@ -17,27 +19,27 @@ type AddModalProps = {
data?: ListItem;
source: string;
width?: string | number;
current?: any;
onOk: (values: FormData) => void;
onCancel: () => void;
};
const steps = [
const backendOptions = [
{
element: '#filterGGUF',
popover: {
title: '筛选模型',
description: 'Select a model from the list'
}
label: `llama-box`,
value: backendOptionsMap.llamaBox
},
{
element: '#backend-field',
popover: {
title: '选择推理后端',
description: 'Select a model from the list'
}
label: 'vLLM',
value: backendOptionsMap.vllm
},
{
label: 'vox-box',
value: backendOptionsMap.voxBox
}
];
const defaultQuant = ['Q4_K_M'];
const AddModal: React.FC<AddModalProps> = (props) => {
const {
title,
@ -46,41 +48,166 @@ const AddModal: React.FC<AddModalProps> = (props) => {
onCancel,
source,
action,
current,
width = 600
} = props || {};
const SEARCH_SOURCE = [];
const form = useRef<any>({});
const intl = useIntl();
const [selectedModel, setSelectedModel] = useState<any>({});
const [collapsed, setCollapsed] = useState<boolean>(false);
const [loadingModel, setLoadingModel] = useState<boolean>(false);
const [isGGUF, setIsGGUF] = useState<boolean>(false);
const modelFileRef = useRef<any>(null);
const [loadfinish, setLoadfinish] = useState<boolean>(false);
const handleSelectModelFile = useCallback((item: any) => {
form.current?.setFieldValue?.('file_name', item.fakeName);
setLoadfinish(true);
}, []);
const handleOnSelectModel = (item: any) => {
setSelectedModel(item);
};
const [isGGUF, setIsGGUF] = useState<boolean>(false);
const [sourceList, setSourceList] = useState<any[]>([]);
const [backendList, setBackendList] = useState<any[]>([]);
const [sizeOptions, setSizeOptions] = useState<any[]>([]);
const [quantizationOptions, setQuantizationOptions] = useState<any[]>([]);
const sourceGroupMap = useRef<any>({});
const axiosToken = useRef<any>(null);
const selectSpecRef = useRef<CatalogSpec>({} as CatalogSpec);
const handleSumit = () => {
form.current?.submit?.();
};
const debounceFetchModelFiles = debounce(() => {
modelFileRef.current?.fetchModelFiles?.();
}, 300);
const getModelFile = (spec: CatalogSpec) => {
let modelInfo = {};
if (spec.source === modelSourceMap.huggingface_value) {
modelInfo = {
huggingface_repo_id: spec?.huggingface_repo_id,
huggingface_filename: spec?.huggingface_filename
};
}
if (spec.source === modelSourceMap.modelscope_value) {
modelInfo = {
model_scope_model_id: spec?.model_scope_model_id,
model_scope_file_path: spec?.model_scope_file_path
};
}
const handleSetIsGGUF = (flag: boolean) => {
setIsGGUF(flag);
if (flag) {
debounceFetchModelFiles();
if (spec.source === modelSourceMap.ollama_library_value) {
modelInfo = {
ollama_library_model_name: spec?.ollama_library_model_name
};
}
if (spec.source === modelSourceMap.local_path_value) {
modelInfo = {
local_path: spec?.local_path
};
}
return modelInfo;
};
const getModelSpec = (data: {
source: string;
backend: string;
size: number;
quantization: string;
}) => {
const groupList = sourceGroupMap.current[data.source];
const spec = _.find(groupList, (item: CatalogSpec) => {
if (data.size && data.quantization) {
return (
item.size === data.size &&
item.backend === data.backend &&
item.quantization === data.quantization
);
}
if (data.size) {
return item.size === data.size && item.backend === data.backend;
}
if (data.quantization) {
return (
item.quantization === data.quantization &&
item.backend === data.backend
);
}
return item.backend === data.backend;
});
selectSpecRef.current = spec;
return {
..._.omit(spec, ['name']),
categories: _.get(spec, 'categories.0', null)
};
};
const initFormDataBySource = (data: CatalogSpec) => {
selectSpecRef.current = data;
form.current?.setFieldsValue({
..._.omit(data, ['name']),
categories: _.get(data, 'categories.0', null)
});
};
const handleSetSizeOptions = (data: { source: string; backend: string }) => {
const groupList = sourceGroupMap.current[source];
const list = _.filter(groupList, (item: CatalogSpec) => item.size);
const sizeGroup = _.groupBy(
_.filter(list, (item: CatalogSpec) => {
return item.backend === data.backend;
}),
'size'
);
const sizeList = _.keys(sizeGroup).map((size: string) => {
return {
label: `${size}B`,
value: _.toNumber(size)
};
});
setSizeOptions(sizeList);
return sizeList;
};
const handleSetQuantizationOptions = (data: {
source: string;
size: number;
backend: string;
}) => {
const groupList = sourceGroupMap.current[data.source];
console.log('groupList====', data, groupList);
const sizeGroup = _.filter(groupList, (item: CatalogSpec) => {
return item.size === data.size && item.backend === data.backend;
});
const quantizationList = _.map(sizeGroup, (item: CatalogSpec) => {
return {
label: item.quantization,
value: item.quantization
};
});
setQuantizationOptions(quantizationList);
return quantizationList;
};
const handleSetBackendOptions = (source: string) => {
const groupList = sourceGroupMap.current[source];
const backendGroup = _.groupBy(groupList, 'backend');
console.log('backendGroup====', backendGroup, source);
const backendList = _.filter(backendOptions, (item: any) => {
return backendGroup[item.value];
});
setBackendList(backendList);
return backendList;
};
const handleSourceChange = (source: string) => {
const defaultSpec = _.get(sourceGroupMap.current, `${source}.0`, {});
console.log('source====', source, defaultSpec);
initFormDataBySource(defaultSpec);
handleSetSizeOptions({
source: source,
backend: defaultSpec.backend
});
handleSetQuantizationOptions({
source: source,
size: defaultSpec.size,
backend: defaultSpec.backend
});
// set form value
initFormDataBySource(defaultSpec);
};
const handleBackendChange = (backend: string) => {
@ -91,29 +218,138 @@ const AddModal: React.FC<AddModalProps> = (props) => {
if (backend === backendOptionsMap.llamaBox) {
setIsGGUF(true);
}
const sizeList = handleSetSizeOptions({
source: form.current.getFieldValue('source'),
backend: backend
});
const quantizaList = handleSetQuantizationOptions({
source: form.current.getFieldValue('source'),
size: _.get(sizeList, '0.value', 0),
backend: backend
});
const data = getModelSpec({
source: form.current.getFieldValue('source'),
backend: backend,
size: _.get(sizeList, '0.value', 0),
quantization:
_.find(quantizaList, (item: { label: string; value: string }) =>
defaultQuant.includes(item.value)
)?.value || _.get(quantizaList, '0.value', '')
});
form.current.setFieldsValue({
...data
});
};
const fetchSpecData = async () => {
try {
axiosToken.current?.cancel?.();
axiosToken.current = createAxiosToken();
const res: any = await queryCatalogItemSpec(
{
id: current.id
},
{
token: axiosToken.current.token
}
);
const groupList = _.groupBy(res.items, 'source');
sourceGroupMap.current = groupList;
const sources = _.filter(sourceOptions, (item: any) => {
return groupList[item.value];
});
const source = _.get(sources, '0.value', '');
const defaultSpec =
_.find(groupList[source], (item: CatalogSpec) => {
return defaultQuant.includes(item.quantization);
}) || _.get(groupList, `${source}.0`, {});
setSourceList(sources);
handleSetBackendOptions(source);
handleSetSizeOptions({
source: source,
backend: defaultSpec.backend
});
handleSetQuantizationOptions({
source: source,
size: defaultSpec.size,
backend: defaultSpec.backend
});
initFormDataBySource(defaultSpec);
form.current.setFieldValue(
'name',
_.toLower(current.name).replace(/\s/g, '-') || ''
);
if (defaultSpec.backend === backendOptionsMap.vllm) {
setIsGGUF(false);
}
if (defaultSpec.backend === backendOptionsMap.llamaBox) {
setIsGGUF(true);
}
} catch (error) {
// ignore
}
};
const handleOnQuantizationChange = (val: string) => {
const data = getModelSpec({
source: form.current.getFieldValue('source'),
backend: form.current.getFieldValue('backend'),
size: form.current.getFieldValue('size'),
quantization: val
});
form.current.setFieldsValue({
...data
});
};
const handleOnSizeChange = (val: number) => {
const list = handleSetQuantizationOptions({
source: form.current.getFieldValue('source'),
backend: form.current.getFieldValue('backend'),
size: val
});
const data = getModelSpec({
source: form.current.getFieldValue('source'),
backend: form.current.getFieldValue('backend'),
size: val,
quantization:
_.find(list, (item: { label: string; value: string }) =>
defaultQuant.includes(item.value)
)?.value || _.get(list, '0.value', '')
});
// set form data
form.current.setFieldsValue({
...data
});
};
const handleOk = (values: FormData) => {
onOk({
...values,
...getModelFile(selectSpecRef.current)
});
};
const handleCancel = useCallback(() => {
onCancel?.();
axiosToken.current?.cancel?.();
}, [onCancel]);
useEffect(() => {
handleSelectModelFile({ fakeName: '' });
}, [selectedModel]);
useEffect(() => {
if (!open) {
setIsGGUF(false);
form.current?.setFieldValue?.('backend', backendOptionsMap.vllm);
} else if (source === modelSourceMap.ollama_library_value) {
form.current?.setFieldValue?.('backend', backendOptionsMap.llamaBox);
setIsGGUF(true);
if (open) {
fetchSpecData();
}
return () => {
setSelectedModel({});
axiosToken.current?.cancel?.();
};
}, [open, source]);
}, [open, current]);
return (
<Drawer
@ -170,11 +406,20 @@ const AddModal: React.FC<AddModalProps> = (props) => {
<DataForm
source={source}
action={action}
selectedModel={selectedModel}
onOk={onOk}
selectedModel={{}}
onOk={handleOk}
ref={form}
isGGUF={isGGUF}
byBuiltIn={true}
sourceDisable={false}
backendOptions={backendList}
sourceList={sourceList}
quantizationOptions={quantizationOptions}
sizeOptions={sizeOptions}
onBackendChange={handleBackendChange}
onSourceChange={handleSourceChange}
onQuantizationChange={handleOnQuantizationChange}
onSizeChange={handleOnSizeChange}
></DataForm>
</>
</ColumnWrapper>

@ -25,23 +25,6 @@ type AddModalProps = {
onCancel: () => void;
};
const steps = [
{
element: '#filterGGUF',
popover: {
title: '筛选模型',
description: 'Select a model from the list'
}
},
{
element: '#backend-field',
popover: {
title: '选择推理后端',
description: 'Select a model from the list'
}
}
];
const AddModal: React.FC<AddModalProps> = (props) => {
const {
title,

@ -221,6 +221,12 @@ const Models: React.FC<ModelsProps> = ({
}, [deleteIds]);
const sourceOptions = [
{
label: intl.formatMessage({ id: 'menu.models.modelCatalog' }),
value: 'catalog',
key: 'catalog',
icon: <IconFont type="icon-catalog"></IconFont>
},
{
label: 'Hugging Face',
value: modelSourceMap.huggingface_value,
@ -705,6 +711,9 @@ const Models: React.FC<ModelsProps> = ({
source: modelSourceMap.local_path_value
});
}
if (item.key === 'catalog') {
navigate('/models/catalog');
}
};
return (

@ -453,33 +453,9 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
)}
</Form.Item>
{renderFieldsBySource}
<Form.Item<FormData>
name="replicas"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.input'
},
{
name: intl.formatMessage({ id: 'models.form.replicas' })
}
)
}
]}
>
<SealInput.Number
style={{ width: '100%' }}
label={intl.formatMessage({
id: 'models.form.replicas'
})}
required
min={0}
></SealInput.Number>
</Form.Item>
<Form.Item name="backend">
<Form.Item name="backend" rules={[{ required: true }]}>
<SealSelect
required
onChange={handleBackendChange}
label={intl.formatMessage({ id: 'models.form.backend' })}
options={[
@ -512,6 +488,31 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
}
></SealSelect>
</Form.Item>
<Form.Item<FormData>
name="replicas"
rules={[
{
required: true,
message: intl.formatMessage(
{
id: 'common.form.rule.input'
},
{
name: intl.formatMessage({ id: 'models.form.replicas' })
}
)
}
]}
>
<SealInput.Number
style={{ width: '100%' }}
label={intl.formatMessage({
id: 'models.form.replicas'
})}
required
min={0}
></SealInput.Number>
</Form.Item>
<Form.Item<FormData> name="description">
<SealInput.TextArea
label={intl.formatMessage({

@ -159,6 +159,30 @@ export const modelSourceValueMap = {
[modelSourceMap.local_path_value]: modelSourceMap.local_path
};
export const sourceOptions = [
{
label: 'Hugging Face',
value: modelSourceMap.huggingface_value,
key: 'huggingface'
},
{
label: 'Ollama Library',
value: modelSourceMap.ollama_library_value,
key: 'ollama_library'
},
{
label: 'ModelScope',
value: modelSourceMap.modelscope_value,
key: 'model_scope'
},
{
label: 'models.form.localPath',
locale: true,
value: modelSourceMap.local_path_value,
key: 'local_path'
}
];
export const InstanceStatusMap = {
Initializing: 'initializing',
Starting: 'starting',
@ -255,11 +279,11 @@ export const modelCategoriesMap = {
export const modelCategories = [
{ label: 'common.options.auto', value: null, locale: true },
{ label: 'LLM', value: modelCategoriesMap.llm },
{ label: 'Image', value: 'image' },
{ label: 'Text-to-speech', value: 'text_to_speech' },
{ label: 'Speech-to-text', value: 'speech_to_text' },
{ label: 'Embedding', value: 'embedding' },
{ label: 'Reranker', value: 'reranker' }
{ label: 'Image', value: modelCategoriesMap.image },
{ label: 'Text-to-Speech', value: modelCategoriesMap.text_to_speech },
{ label: 'Speech-to-Text', value: modelCategoriesMap.speech_to_text },
{ label: 'Embedding', value: modelCategoriesMap.embedding },
{ label: 'Reranker', value: modelCategoriesMap.reranker }
];
export const sourceRepoConfig = {

@ -133,3 +133,31 @@ export interface CatalogItem {
licenses: string[];
release_date: string;
}
export interface CatalogSpec {
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[];
quantization: string;
size: number;
}

@ -85,13 +85,19 @@
border-radius: 50%;
background-color: var(--ant-color-text-quaternary);
margin-right: 8px;
flex: none;
}
.box {
flex: 1;
overflow: hidden;
}
.tags {
display: flex;
align-items: center;
flex: 1;
overflow: hidden;
width: 100%;
}
.tag-item {

Loading…
Cancel
Save