chore: catalog card style

This commit is contained in:
jialin
2025-01-03 20:51:22 +08:00
parent d9319f5734
commit 1a792254ca
16 changed files with 696 additions and 299 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

+4
View File
@@ -204,6 +204,10 @@
margin-bottom: 16px; margin-bottom: 16px;
} }
.m-r-16 {
margin-right: 16px;
}
.font-400 { .font-400 {
font-weight: var(--font-weight-normal); font-weight: var(--font-weight-normal);
} }
+1 -1
View File
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
// import './iconfont/iconfont.js'; // import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({ 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; export default IconFont;
+49 -31
View File
@@ -6,7 +6,8 @@ import './index.less';
const TagsWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { const TagsWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const observer = useRef<IntersectionObserver | null>(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 uid = useRef(0);
const updateUid = () => { const updateUid = () => {
@@ -17,55 +18,72 @@ const TagsWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const dropItems = useMemo(() => { const dropItems = useMemo(() => {
return React.Children.toArray(children) return React.Children.toArray(children)
.slice(hiddenIndex) .slice(hiddenIndex)
.map((child, index) => ({ .map((child) => ({
label: child, label: child,
key: updateUid() key: updateUid()
})); }));
}, [children, hiddenIndex]); }, [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(() => { useEffect(() => {
if (!wrapperRef.current) return; if (!wrapperRef.current) return;
observer.current = new IntersectionObserver( // observer.current = new IntersectionObserver(
(entries) => { // (entries) => {
let newHiddenIndex = 0; // const lastEntry = entries[entries.length - 1];
entries.forEach((entry, index) => { // if (lastEntry && lastEntry.intersectionRatio < 1) {
if (entry.intersectionRatio < 1) { // setHiddenIndex((prev) =>
newHiddenIndex = Math.min(newHiddenIndex, index); // Math.max(prev, React.Children.count(children))
} // );
console.log( // }
'isIntersecting=======', // },
entry.intersectionRatio, // { root: wrapperRef.current, threshold: 1 }
newHiddenIndex // );
);
});
setHiddenIndex(() => newHiddenIndex);
},
{
root: wrapperRef.current,
threshold: 0
}
);
const childrenList = wrapperRef.current.children; // const childrenList = wrapperRef.current.children;
Array.from(childrenList).forEach((child) => { // if (childrenList.length > 0) {
observer.current?.observe(child); // observer.current.observe(childrenList[childrenList.length - 1]);
// }
resizeObserver.current = new ResizeObserver(() => {
// updateHiddenIndex();
}); });
resizeObserver.current.observe(wrapperRef.current);
return () => { return () => {
observer.current?.disconnect(); observer.current?.disconnect();
resizeObserver.current?.disconnect();
}; };
}, [children]); }, [children]);
return ( return (
<div className="tags-wrapper" ref={wrapperRef}> <div className="tags-wrapper" ref={wrapperRef}>
<span>{hiddenIndex}</span> {React.Children.toArray(children).slice(0, hiddenIndex)}
{React.Children.toArray(children).slice( {React.Children.toArray(children).length > 4 && (
0,
React.Children.toArray(children).length - hiddenIndex
)}
{hiddenIndex > 0 && (
<Dropdown <Dropdown
trigger={['hover']}
menu={{ menu={{
items: dropItems items: dropItems
}} }}
+20 -9
View File
@@ -4,6 +4,7 @@ import { request } from '@umijs/max';
import qs from 'query-string'; import qs from 'query-string';
import { import {
CatalogItem, CatalogItem,
CatalogSpec,
FormData, FormData,
GPUListItem, GPUListItem,
ListItem, ListItem,
@@ -322,15 +323,25 @@ export async function queryCatalogList(
params: Global.SearchParams, params: Global.SearchParams,
options?: any options?: any
) { ) {
return request<Global.PageResponse<CatalogItem>>(`/model-sets`, { return request<Global.PageResponse<CatalogItem>>(
methos: 'GET', `/model-sets?${qs.stringify(params)}`,
...options, {
params methos: 'GET',
}); ...options
}
);
} }
export async function queryCatalogItemSpec(id: number) { export async function queryCatalogItemSpec(
return request(`/model-sets/${id}/specs`, { params: { id: number },
method: 'GET' options?: any
}); ) {
return await request<Global.PageResponse<CatalogSpec>>(
`/model-sets/${params.id}/specs`,
{
method: 'GET',
...options,
params
}
);
} }
+52 -17
View File
@@ -3,19 +3,29 @@ import { PageAction } from '@/config';
import breakpoints from '@/config/breakpoints'; import breakpoints from '@/config/breakpoints';
import { SyncOutlined } from '@ant-design/icons'; import { SyncOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components'; import { PageContainer } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max'; import { useIntl, useNavigate } from '@umijs/max';
import { Button, Col, Input, Pagination, Row, Space, message } from 'antd'; import {
Button,
Col,
Input,
Pagination,
Row,
Select,
Space,
message
} from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import ResizeObserver from 'rc-resize-observer'; import ResizeObserver from 'rc-resize-observer';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { createModel, queryCatalogList } from './apis'; import { createModel, queryCatalogList } from './apis';
import CatalogItem from './components/catalog-item'; import CatalogItem from './components/catalog-item';
import DelopyBuiltInModal from './components/deploy-builtin-modal'; 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'; import { CatalogItem as CatalogItemType, FormData } from './config/types';
const Catalog: React.FC = () => { const Catalog: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigate();
const [span, setSpan] = React.useState(8); const [span, setSpan] = React.useState(8);
const [activeId, setActiveId] = React.useState(-1); const [activeId, setActiveId] = React.useState(-1);
const [dataSource, setDataSource] = useState<{ const [dataSource, setDataSource] = useState<{
@@ -30,11 +40,13 @@ const Catalog: React.FC = () => {
const [queryParams, setQueryParams] = useState({ const [queryParams, setQueryParams] = useState({
page: 1, page: 1,
perPage: 9, perPage: 9,
search: '' search: '',
categories: []
}); });
const [openDeployModal, setOpenDeployModal] = useState<any>({ const [openDeployModal, setOpenDeployModal] = useState<any>({
show: false, show: false,
width: 600, width: 600,
current: {},
source: modelSourceMap.huggingface_value source: modelSourceMap.huggingface_value
}); });
@@ -47,7 +59,7 @@ const Catalog: React.FC = () => {
const params = { const params = {
..._.pickBy(queryParams, (val: any) => !!val) ..._.pickBy(queryParams, (val: any) => !!val)
}; };
const res = await queryCatalogList(params); const res: any = await queryCatalogList(params);
setDataSource({ setDataSource({
dataList: res.items, dataList: res.items,
@@ -94,6 +106,7 @@ const Catalog: React.FC = () => {
setOpenDeployModal({ setOpenDeployModal({
show: true, show: true,
source: modelSourceMap.huggingface_value, source: modelSourceMap.huggingface_value,
current: item,
width: 600 width: 600
}); });
}, []); }, []);
@@ -103,12 +116,9 @@ const Catalog: React.FC = () => {
try { try {
console.log('data:', data, openDeployModal); console.log('data:', data, openDeployModal);
const result = getSourceRepoConfigValue(openDeployModal.source, data);
const modelData = await createModel({ const modelData = await createModel({
data: { data: {
...result.values, ..._.omit(data, ['size', 'quantization'])
..._.omit(data, result.omits)
} }
}); });
setOpenDeployModal({ setOpenDeployModal({
@@ -116,18 +126,22 @@ const Catalog: React.FC = () => {
show: false show: false
}); });
message.success(intl.formatMessage({ id: 'common.message.success' })); message.success(intl.formatMessage({ id: 'common.message.success' }));
navigate('/models/list');
} catch (error) {} } catch (error) {}
}, },
[openDeployModal] [openDeployModal]
); );
const handleOnPageChange = useCallback((page: number, pageSize?: number) => { const handleOnPageChange = useCallback(
setQueryParams({ (page: number, pageSize?: number) => {
...queryParams, setQueryParams({
page, ...queryParams,
perPage: pageSize || 10 page,
}); perPage: pageSize || 10
}, []); });
},
[queryParams]
);
const handleSearch = (e: any) => { const handleSearch = (e: any) => {
fetchData(); fetchData();
@@ -136,10 +150,19 @@ const Catalog: React.FC = () => {
const handleNameChange = (e: any) => { const handleNameChange = (e: any) => {
setQueryParams({ setQueryParams({
...queryParams, ...queryParams,
page: 1,
search: e.target.value search: e.target.value
}); });
}; };
const handleCategoryChange = (value: any) => {
setQueryParams({
...queryParams,
page: 1,
categories: value
});
};
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, [queryParams]); }, [queryParams]);
@@ -159,11 +182,21 @@ const Catalog: React.FC = () => {
<Space> <Space>
<Input <Input
placeholder={intl.formatMessage({ id: 'common.filter.name' })} placeholder={intl.formatMessage({ id: 'common.filter.name' })}
style={{ width: 300 }} style={{ width: 200 }}
size="large" size="large"
allowClear allowClear
onChange={handleNameChange} onChange={handleNameChange}
></Input> ></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 <Button
type="text" type="text"
style={{ color: 'var(--ant-color-text-tertiary)' }} style={{ color: 'var(--ant-color-text-tertiary)' }}
@@ -190,6 +223,7 @@ const Catalog: React.FC = () => {
</ResizeObserver> </ResizeObserver>
<div style={{ marginBlock: '32px 16px' }}> <div style={{ marginBlock: '32px 16px' }}>
<Pagination <Pagination
pageSizeOptions={['9', '12', '36', '100']}
hideOnSinglePage={queryParams.perPage === 9} hideOnSinglePage={queryParams.perPage === 9}
align="end" align="end"
defaultCurrent={1} defaultCurrent={1}
@@ -205,6 +239,7 @@ const Catalog: React.FC = () => {
title={intl.formatMessage({ id: 'models.button.deploy' })} title={intl.formatMessage({ id: 'models.button.deploy' })}
source={openDeployModal.source} source={openDeployModal.source}
width={openDeployModal.width} width={openDeployModal.width}
current={openDeployModal.current}
onCancel={handleDeployModalCancel} onCancel={handleDeployModalCancel}
onOk={handleCreateModel} onOk={handleCreateModel}
></DelopyBuiltInModal> ></DelopyBuiltInModal>
@@ -140,16 +140,6 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
form.setFieldValue('backend_parameters', list); 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 collapseItems = useMemo(() => {
const children = ( const children = (
<> <>
+63 -24
View File
@@ -1,11 +1,13 @@
import fallbackImg from '@/assets/images/img.png';
import IMG from '@/assets/images/small-logo-200x200.png'; import IMG from '@/assets/images/small-logo-200x200.png';
import AutoTooltip from '@/components/auto-tooltip'; import AutoTooltip from '@/components/auto-tooltip';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import { ClockCircleOutlined } from '@ant-design/icons'; import TagWrapper from '@/components/tags-wrapper';
import { Divider, Tag, Typography } from 'antd'; import { Tag, Typography } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { modelCategories } from '../config';
import { CatalogItem as CatalogItemType } from '../config/types'; import { CatalogItem as CatalogItemType } from '../config/types';
import '../style/catalog-item.less'; import '../style/catalog-item.less';
@@ -26,13 +28,13 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
const home = data.home?.replace(/\/$/, ''); const home = data.home?.replace(/\/$/, '');
const icon = data.icon?.replace(/^\//, ''); const icon = data.icon?.replace(/^\//, '');
if (icon) { if (icon) {
return `${home}/${icon}`; return data.icon;
} }
return IMG; return IMG;
}, [data]); }, [data]);
const handleOnError = (e: any) => { const handleOnError = (e: any) => {
e.target.src = IMG; e.target.src = fallbackImg;
}; };
return ( return (
@@ -43,35 +45,56 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
<div className="content"> <div className="content">
<div className="title"> <div className="title">
<div className="img"> <div className="img">
<img src={icon} alt="" onError={handleOnError} /> <img
src={data.icon || fallbackImg}
alt=""
onError={handleOnError}
/>
</div> </div>
<AutoTooltip ghost style={{ flex: 1 }}> <AutoTooltip ghost style={{ flex: 1 }}>
{data.name} {data.name}
</AutoTooltip> </AutoTooltip>
</div> </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} {data.description}
</Typography.Paragraph> </Typography.Paragraph>
</div> </div>
<div className="item-footer"> <div className="item-footer">
<div className="update-time"> <div className="update-time">
<span className="flex-center"> <span className="flex-center">
<ClockCircleOutlined <IconFont
type="icon-new_release_outlined"
className="m-r-5" className="m-r-5"
style={{ color: 'var(--ant-color-text-secondary)' }} style={{ color: 'var(--ant-color-text-secondary)' }}
/> ></IconFont>
{data.release_date} {data.release_date}
</span> </span>
<span className="flex-center"> <span className="flex-center">
<IconFont type="icon-justice" className="m-r-5"></IconFont>
{_.map(data.licenses, (license: string, index: number) => { {_.map(data.licenses, (license: string, index: number) => {
return ( return (
<> <span key={license} className="flex-center m-r-16">
<span key={license}>{license}</span> <IconFont type="icon-justice" className="m-r-5"></IconFont>
{index !== data.licenses.length - 1 && ( <span>{license}</span>
<Divider type="vertical" /> </span>
)}
</>
); );
})} })}
</span> </span>
@@ -79,21 +102,37 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
<div className="tags"> <div className="tags">
{data.categories.map((sItem, i) => { {data.categories.map((sItem, i) => {
return ( return (
<Tag key={sItem} className="tag-item" color={COLORS[i]}> <Tag key={sItem} className="tag-item" color="blue">
{sItem} {_.find(modelCategories, { value: sItem })?.label || sItem}
</Tag> </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 && ( {data.sizes?.length > 0 && (
<> <>
<span className="dot"></span> <span className="dot"></span>
{data.sizes.map((sItem, i) => { <div className="box">
return ( <TagWrapper>
<Tag key={sItem} className="tag-item"> {data.sizes.map((sItem, i) => {
{sItem}B return (
</Tag> <Tag key={sItem} className="tag-item">
); {sItem}B
})} </Tag>
);
})}
</TagWrapper>
</div>
</> </>
)} )}
</div> </div>
+111 -107
View File
@@ -23,7 +23,8 @@ import {
backendOptionsMap, backendOptionsMap,
modelSourceMap, modelSourceMap,
modelTaskMap, modelTaskMap,
ollamaModelOptions ollamaModelOptions,
sourceOptions
} from '../config'; } from '../config';
import { HuggingFaceModels, ModelScopeModels } from '../config/audio-catalog'; import { HuggingFaceModels, ModelScopeModels } from '../config/audio-catalog';
import { FormData, GPUListItem } from '../config/types'; import { FormData, GPUListItem } from '../config/types';
@@ -40,6 +41,10 @@ interface DataFormProps {
sourceDisable?: boolean; sourceDisable?: boolean;
byBuiltIn?: boolean; byBuiltIn?: boolean;
backendOptions?: Global.BaseOption<string>[]; backendOptions?: Global.BaseOption<string>[];
sourceList?: Global.BaseOption<string>[];
onSizeChange?: (val: number) => void;
onQuantizationChange?: (val: string) => void;
onSourceChange?: (value: string) => void;
onOk: (values: FormData) => void; onOk: (values: FormData) => void;
onBackendChange?: (value: string) => void; onBackendChange?: (value: string) => void;
} }
@@ -49,37 +54,17 @@ const SEARCH_SOURCE = [
modelSourceMap.modelscope_value 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 DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const { const {
action, action,
isGGUF, isGGUF,
sourceDisable = true, sourceDisable = true,
backendOptions, backendOptions,
sourceList,
byBuiltIn, byBuiltIn,
sizeOptions = [],
quantizationOptions = [],
onSourceChange,
onOk onOk
} = props; } = props;
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -349,71 +334,83 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
}; };
const handleSizeChange = (val: any) => { const handleSizeChange = (val: any) => {
form.setFieldsValue({ props.onSizeChange?.(val);
quantization: ''
});
}; };
const handleOnQuantizationChange = (val: any) => {
props.onQuantizationChange?.(val);
};
// from catalog
const renderFieldsFromCatalog = useMemo(() => { const renderFieldsFromCatalog = useMemo(() => {
if (!byBuiltIn) { if (!byBuiltIn && !sizeOptions?.length && !quantizationOptions?.length) {
return null; return null;
} }
return ( return (
<> <>
<Form.Item<FormData> {sizeOptions?.length > 0 && (
name="size" <Form.Item<FormData>
key="size" name="size"
rules={[ key="size"
{ rules={[
required: true, {
message: intl.formatMessage( required: true,
{ message: intl.formatMessage(
id: 'common.form.rule.select' {
}, id: 'common.form.rule.select'
{ name: 'size' } },
) { name: 'size' }
} )
]} }
> ]}
<SealAutoComplete >
filterOption <SealSelect
onChange={handleSizeChange} filterOption
defaultActiveFirstOption onChange={handleSizeChange}
disabled={false} defaultActiveFirstOption
options={props.sizeOptions} disabled={false}
label="Size" options={sizeOptions}
required label="Size"
></SealAutoComplete> required
</Form.Item> ></SealSelect>
<Form.Item<FormData> </Form.Item>
name="quantization" )}
key="quantization" {quantizationOptions?.length > 0 && (
rules={[ <Form.Item<FormData>
{ name="quantization"
required: true, key="quantization"
message: intl.formatMessage( rules={[
{ {
id: 'common.form.rule.select' required: true,
}, message: intl.formatMessage(
{ name: 'quantization' } {
) id: 'common.form.rule.select'
} },
]} { name: 'quantization' }
> )
<SealAutoComplete }
filterOption ]}
defaultActiveFirstOption >
disabled={false} <SealSelect
options={props.quantizationOptions} filterOption
label="Quantization" defaultActiveFirstOption
required disabled={false}
></SealAutoComplete> options={quantizationOptions}
</Form.Item> onChange={handleOnQuantizationChange}
label="Quantization"
required
></SealSelect>
</Form.Item>
)}
</> </>
); );
}, [props.sizeOptions, props.quantizationOptions, byBuiltIn]); }, [sizeOptions, quantizationOptions, byBuiltIn]);
const renderFieldsBySource = useMemo(() => { const renderFieldsBySource = useMemo(() => {
// from catalog
if (byBuiltIn) {
return null;
}
if (SEARCH_SOURCE.includes(props.source)) { if (SEARCH_SOURCE.includes(props.source)) {
return renderHuggingfaceFields(); return renderHuggingfaceFields();
} }
@@ -427,7 +424,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
} }
return null; return null;
}, [props.source, isGGUF, intl]); }, [props.source, isGGUF, byBuiltIn, intl]);
const handleBackendChange = useCallback((val: string) => { const handleBackendChange = useCallback((val: string) => {
if (val === backendOptionsMap.llamaBox) { if (val === backendOptionsMap.llamaBox) {
@@ -437,6 +434,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
}); });
} }
form.setFieldValue('backend_version', ''); form.setFieldValue('backend_version', '');
props.onBackendChange?.(val);
}, []); }, []);
const handleOk = (formdata: FormData) => { const handleOk = (formdata: FormData) => {
@@ -451,6 +449,10 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
}); });
}; };
const handleOnSourceChange = (val: string) => {
onSourceChange?.(val);
};
useEffect(() => { useEffect(() => {
if (action === PageAction.EDIT) return; if (action === PageAction.EDIT) return;
if (modelTask.type === modelTaskMap.audio) { if (modelTask.type === modelTaskMap.audio) {
@@ -523,11 +525,12 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
> >
{ {
<SealSelect <SealSelect
onChange={handleOnSourceChange}
disabled={sourceDisable} disabled={sourceDisable}
label={intl.formatMessage({ label={intl.formatMessage({
id: 'models.form.source' id: 'models.form.source'
})} })}
options={sourceOptions} options={sourceList ?? sourceOptions}
required required
></SealSelect> ></SealSelect>
} }
@@ -535,33 +538,9 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
} }
{renderFieldsBySource} {renderFieldsBySource}
<Form.Item<FormData> <Form.Item name="backend" rules={[{ required: true }]}>
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">
<SealSelect <SealSelect
required
onChange={handleBackendChange} onChange={handleBackendChange}
label={intl.formatMessage({ id: 'models.form.backend' })} label={intl.formatMessage({ id: 'models.form.backend' })}
description={ description={
@@ -609,6 +588,31 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
></SealSelect> ></SealSelect>
</Form.Item> </Form.Item>
{renderFieldsFromCatalog} {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"> <Form.Item<FormData> name="description">
<SealInput.TextArea <SealInput.TextArea
label={intl.formatMessage({ label={intl.formatMessage({
@@ -1,12 +1,14 @@
import ModalFooter from '@/components/modal-footer'; import ModalFooter from '@/components/modal-footer';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { createAxiosToken } from '@/hooks/use-chunk-request';
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 _ from 'lodash';
import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { backendOptionsMap, modelSourceMap } from '../config'; import { queryCatalogItemSpec } from '../apis';
import { FormData, ListItem } from '../config/types'; import { backendOptionsMap, modelSourceMap, sourceOptions } from '../config';
import { CatalogSpec, FormData, ListItem } from '../config/types';
import ColumnWrapper from './column-wrapper'; import ColumnWrapper from './column-wrapper';
import DataForm from './data-form'; import DataForm from './data-form';
@@ -17,27 +19,27 @@ type AddModalProps = {
data?: ListItem; data?: ListItem;
source: string; source: string;
width?: string | number; width?: string | number;
current?: any;
onOk: (values: FormData) => void; onOk: (values: FormData) => void;
onCancel: () => void; onCancel: () => void;
}; };
const steps = [ const backendOptions = [
{ {
element: '#filterGGUF', label: `llama-box`,
popover: { value: backendOptionsMap.llamaBox
title: '筛选模型',
description: 'Select a model from the list'
}
}, },
{ {
element: '#backend-field', label: 'vLLM',
popover: { value: backendOptionsMap.vllm
title: '选择推理后端', },
description: 'Select a model from the list' {
} label: 'vox-box',
value: backendOptionsMap.voxBox
} }
]; ];
const defaultQuant = ['Q4_K_M'];
const AddModal: React.FC<AddModalProps> = (props) => { const AddModal: React.FC<AddModalProps> = (props) => {
const { const {
title, title,
@@ -46,41 +48,166 @@ const AddModal: React.FC<AddModalProps> = (props) => {
onCancel, onCancel,
source, source,
action, action,
current,
width = 600 width = 600
} = props || {}; } = props || {};
const SEARCH_SOURCE = []; const SEARCH_SOURCE = [];
const form = useRef<any>({}); const form = useRef<any>({});
const intl = useIntl(); 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 [isGGUF, setIsGGUF] = useState<boolean>(false);
const modelFileRef = useRef<any>(null); const [sourceList, setSourceList] = useState<any[]>([]);
const [loadfinish, setLoadfinish] = useState<boolean>(false); const [backendList, setBackendList] = useState<any[]>([]);
const [sizeOptions, setSizeOptions] = useState<any[]>([]);
const handleSelectModelFile = useCallback((item: any) => { const [quantizationOptions, setQuantizationOptions] = useState<any[]>([]);
form.current?.setFieldValue?.('file_name', item.fakeName); const sourceGroupMap = useRef<any>({});
setLoadfinish(true); const axiosToken = useRef<any>(null);
}, []); const selectSpecRef = useRef<CatalogSpec>({} as CatalogSpec);
const handleOnSelectModel = (item: any) => {
setSelectedModel(item);
};
const handleSumit = () => { const handleSumit = () => {
form.current?.submit?.(); form.current?.submit?.();
}; };
const debounceFetchModelFiles = debounce(() => { const getModelFile = (spec: CatalogSpec) => {
modelFileRef.current?.fetchModelFiles?.(); let modelInfo = {};
}, 300); if (spec.source === modelSourceMap.huggingface_value) {
modelInfo = {
const handleSetIsGGUF = (flag: boolean) => { huggingface_repo_id: spec?.huggingface_repo_id,
setIsGGUF(flag); huggingface_filename: spec?.huggingface_filename
if (flag) { };
debounceFetchModelFiles();
} }
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
};
}
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) => { const handleBackendChange = (backend: string) => {
@@ -91,29 +218,138 @@ const AddModal: React.FC<AddModalProps> = (props) => {
if (backend === backendOptionsMap.llamaBox) { if (backend === backendOptionsMap.llamaBox) {
setIsGGUF(true); 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(() => { const handleCancel = useCallback(() => {
onCancel?.(); onCancel?.();
axiosToken.current?.cancel?.();
}, [onCancel]); }, [onCancel]);
useEffect(() => { useEffect(() => {
handleSelectModelFile({ fakeName: '' }); if (open) {
}, [selectedModel]); fetchSpecData();
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);
} }
return () => { return () => {
setSelectedModel({}); axiosToken.current?.cancel?.();
}; };
}, [open, source]); }, [open, current]);
return ( return (
<Drawer <Drawer
@@ -170,11 +406,20 @@ const AddModal: React.FC<AddModalProps> = (props) => {
<DataForm <DataForm
source={source} source={source}
action={action} action={action}
selectedModel={selectedModel} selectedModel={{}}
onOk={onOk} onOk={handleOk}
ref={form} ref={form}
isGGUF={isGGUF} isGGUF={isGGUF}
byBuiltIn={true}
sourceDisable={false}
backendOptions={backendList}
sourceList={sourceList}
quantizationOptions={quantizationOptions}
sizeOptions={sizeOptions}
onBackendChange={handleBackendChange} onBackendChange={handleBackendChange}
onSourceChange={handleSourceChange}
onQuantizationChange={handleOnQuantizationChange}
onSizeChange={handleOnSizeChange}
></DataForm> ></DataForm>
</> </>
</ColumnWrapper> </ColumnWrapper>
@@ -25,23 +25,6 @@ type AddModalProps = {
onCancel: () => void; 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 AddModal: React.FC<AddModalProps> = (props) => {
const { const {
title, title,
@@ -221,6 +221,12 @@ const Models: React.FC<ModelsProps> = ({
}, [deleteIds]); }, [deleteIds]);
const sourceOptions = [ const sourceOptions = [
{
label: intl.formatMessage({ id: 'menu.models.modelCatalog' }),
value: 'catalog',
key: 'catalog',
icon: <IconFont type="icon-catalog"></IconFont>
},
{ {
label: 'Hugging Face', label: 'Hugging Face',
value: modelSourceMap.huggingface_value, value: modelSourceMap.huggingface_value,
@@ -705,6 +711,9 @@ const Models: React.FC<ModelsProps> = ({
source: modelSourceMap.local_path_value source: modelSourceMap.local_path_value
}); });
} }
if (item.key === 'catalog') {
navigate('/models/catalog');
}
}; };
return ( return (
+27 -26
View File
@@ -453,33 +453,9 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
)} )}
</Form.Item> </Form.Item>
{renderFieldsBySource} {renderFieldsBySource}
<Form.Item<FormData> <Form.Item name="backend" rules={[{ required: true }]}>
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">
<SealSelect <SealSelect
required
onChange={handleBackendChange} onChange={handleBackendChange}
label={intl.formatMessage({ id: 'models.form.backend' })} label={intl.formatMessage({ id: 'models.form.backend' })}
options={[ options={[
@@ -512,6 +488,31 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
} }
></SealSelect> ></SealSelect>
</Form.Item> </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"> <Form.Item<FormData> name="description">
<SealInput.TextArea <SealInput.TextArea
label={intl.formatMessage({ label={intl.formatMessage({
+29 -5
View File
@@ -159,6 +159,30 @@ export const modelSourceValueMap = {
[modelSourceMap.local_path_value]: modelSourceMap.local_path [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 = { export const InstanceStatusMap = {
Initializing: 'initializing', Initializing: 'initializing',
Starting: 'starting', Starting: 'starting',
@@ -255,11 +279,11 @@ export const modelCategoriesMap = {
export const modelCategories = [ export const modelCategories = [
{ label: 'common.options.auto', value: null, locale: true }, { label: 'common.options.auto', value: null, locale: true },
{ label: 'LLM', value: modelCategoriesMap.llm }, { label: 'LLM', value: modelCategoriesMap.llm },
{ label: 'Image', value: 'image' }, { label: 'Image', value: modelCategoriesMap.image },
{ label: 'Text-to-speech', value: 'text_to_speech' }, { label: 'Text-to-Speech', value: modelCategoriesMap.text_to_speech },
{ label: 'Speech-to-text', value: 'speech_to_text' }, { label: 'Speech-to-Text', value: modelCategoriesMap.speech_to_text },
{ label: 'Embedding', value: 'embedding' }, { label: 'Embedding', value: modelCategoriesMap.embedding },
{ label: 'Reranker', value: 'reranker' } { label: 'Reranker', value: modelCategoriesMap.reranker }
]; ];
export const sourceRepoConfig = { export const sourceRepoConfig = {
+28
View File
@@ -133,3 +133,31 @@ export interface CatalogItem {
licenses: string[]; licenses: string[];
release_date: 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;
}
+7 -1
View File
@@ -85,13 +85,19 @@
border-radius: 50%; border-radius: 50%;
background-color: var(--ant-color-text-quaternary); background-color: var(--ant-color-text-quaternary);
margin-right: 8px; margin-right: 8px;
flex: none;
}
.box {
flex: 1;
overflow: hidden;
} }
.tags { .tags {
display: flex; display: flex;
align-items: center; align-items: center;
flex: 1; flex: 1;
overflow: hidden; width: 100%;
} }
.tag-item { .tag-item {