feat: catalog page

This commit is contained in:
jialin
2025-01-03 20:51:22 +08:00
parent 95058f7890
commit 39fff0e57d
11 changed files with 699 additions and 52 deletions
@@ -0,0 +1,62 @@
import IMG from '@/assets/images/small-logo-200x200.png';
import AutoTooltip from '@/components/auto-tooltip';
import { Button, Tag, Typography } from 'antd';
import classNames from 'classnames';
import React from 'react';
import '../style/catalog-item.less';
interface CatalogItemProps {
activeId: number;
itemId: number;
onDeploy: () => void;
}
const CatalogItem: React.FC<CatalogItemProps> = (props) => {
const { onDeploy, itemId, activeId } = props;
const handleOnDeploy = () => {
onDeploy();
};
return (
<div
className={classNames('catalog-item', { active: activeId === itemId })}
>
<div className="content">
<div className="title">
<div className="img">
<img src={IMG} alt="" />
</div>
<AutoTooltip ghost style={{ flex: 1 }}>
gpustack/stable-diffusion-v3-5-medium-GGUF
</AutoTooltip>
</div>
<Typography.Paragraph className="desc" ellipsis={{ rows: 2 }}>
this is description this is description this is description this is
this is description this isthis is description this isthis is
description this is this is description this isthis is description
this isthis is description this isthis is description this isthis is
description this isthis is description this isthis is description this
is this is description this isthis is description this isthis is
description this isthis is description this is
</Typography.Paragraph>
</div>
<div className="item-footer">
<div className="tags">
<Tag color="blue" className="tag-item">
Audio
</Tag>
<Tag color="purple" className="tag-item">
GGUF
</Tag>
<Tag color="orange" className="tag-item">
Qwen
</Tag>
</div>
<Button size="small" type="primary" onClick={handleOnDeploy}>
Deploy
</Button>
</div>
</div>
);
};
export default React.memo(CatalogItem);
@@ -0,0 +1,232 @@
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 { memo, useCallback, useEffect, useRef, useState } from 'react';
import { backendOptionsMap, modelSourceMap } from '../config';
import { FormData, ListItem } from '../config/types';
import ColumnWrapper from './column-wrapper';
import DataForm from './data-form';
import HFModelFile from './hf-model-file';
import ModelCard from './model-card';
import SearchModel from './search-model';
import Separator from './separator';
import TitleWrapper from './title-wrapper';
type AddModalProps = {
title: string;
action: PageActionType;
open: boolean;
data?: ListItem;
source: string;
width?: string | number;
onOk: (values: FormData) => 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 {
title,
open,
onOk,
onCancel,
source,
action,
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 handleSumit = () => {
form.current?.submit?.();
};
const debounceFetchModelFiles = debounce(() => {
modelFileRef.current?.fetchModelFiles?.();
}, 300);
const handleSetIsGGUF = (flag: boolean) => {
setIsGGUF(flag);
if (flag) {
debounceFetchModelFiles();
}
};
const handleBackendChange = (backend: string) => {
if (backend === backendOptionsMap.vllm) {
setIsGGUF(false);
}
if (backend === backendOptionsMap.llamaBox) {
setIsGGUF(true);
}
};
const handleCancel = useCallback(() => {
onCancel?.();
}, [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);
}
return () => {
setSelectedModel({});
};
}, [open, source]);
return (
<Drawer
title={
<div className="flex-between flex-center">
<span
style={{
color: 'var(--ant-color-text)',
fontWeight: 'var(--font-weight-medium)',
fontSize: 'var(--font-size-middle)'
}}
>
{title}
</span>
<Button type="text" size="small" onClick={handleCancel}>
<CloseOutlined></CloseOutlined>
</Button>
</div>
}
open={open}
onClose={handleCancel}
destroyOnClose={true}
closeIcon={false}
maskClosable={false}
keyboard={false}
styles={{
body: {
height: 'calc(100vh - 57px)',
padding: '16px 0',
overflowX: 'hidden'
},
content: {
borderRadius: '6px 0 0 6px'
}
}}
width={width}
footer={false}
>
<div style={{ display: 'flex', height: '100%' }}>
{SEARCH_SOURCE.includes(props.source) && (
<>
<div style={{ display: 'flex', flex: 1 }}>
<ColumnWrapper>
<SearchModel
modelSource={props.source}
onSelectModel={handleOnSelectModel}
setLoadingModel={setLoadingModel}
></SearchModel>
</ColumnWrapper>
<Separator></Separator>
</div>
<div style={{ display: 'flex', flex: 1 }}>
<ColumnWrapper>
<ModelCard
selectedModel={selectedModel}
onCollapse={setCollapsed}
collapsed={collapsed}
modelSource={props.source}
setIsGGUF={handleSetIsGGUF}
></ModelCard>
{isGGUF && (
<HFModelFile
ref={modelFileRef}
selectedModel={selectedModel}
modelSource={props.source}
onSelectFile={handleSelectModelFile}
collapsed={collapsed}
></HFModelFile>
)}
</ColumnWrapper>
<Separator></Separator>
</div>
</>
)}
<ColumnWrapper
footer={
<ModalFooter
onCancel={handleCancel}
onOk={handleSumit}
style={{
padding: '16px 24px',
display: 'flex',
justifyContent: 'flex-end'
}}
></ModalFooter>
}
>
<>
{SEARCH_SOURCE.includes(source) && (
<TitleWrapper>
{intl.formatMessage({ id: 'models.form.configurations' })}
<span style={{ display: 'flex', height: 24 }}></span>
</TitleWrapper>
)}
<DataForm
source={source}
action={action}
selectedModel={selectedModel}
onOk={onOk}
ref={form}
isGGUF={isGGUF}
onBackendChange={handleBackendChange}
></DataForm>
</>
</ColumnWrapper>
</div>
</Drawer>
);
};
export default memo(AddModal);
+2 -14
View File
@@ -6,7 +6,7 @@ import { Button, Drawer } from 'antd';
import { debounce } from 'lodash';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { backendOptionsMap, modelSourceMap } from '../config';
import { FormData, ListItem } from '../config/types';
import { FormData } from '../config/types';
import ColumnWrapper from './column-wrapper';
import DataForm from './data-form';
import HFModelFile from './hf-model-file';
@@ -19,7 +19,6 @@ type AddModalProps = {
title: string;
action: PageActionType;
open: boolean;
data?: ListItem;
source: string;
width?: string | number;
onOk: (values: FormData) => void;
@@ -57,10 +56,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
// const { start } = useDriver({
// steps,
// id: 'deploy-model'
// });
const form = useRef<any>({});
const intl = useIntl();
const [selectedModel, setSelectedModel] = useState<any>({});
@@ -126,14 +122,6 @@ const AddModal: React.FC<AddModalProps> = (props) => {
};
}, [open, source]);
// useEffect(() => {
// if (open && loadfinish) {
// setTimeout(() => {
// start();
// }, 1000);
// }
// }, [loadfinish, open]);
return (
<Drawer
title={
+56 -38
View File
@@ -22,6 +22,7 @@ import {
DownOutlined,
EditOutlined,
ExperimentOutlined,
GoldOutlined,
PictureOutlined,
SyncOutlined,
WechatWorkOutlined
@@ -221,61 +222,35 @@ const Models: React.FC<ModelsProps> = ({
}, [deleteIds]);
const sourceOptions = [
{
label: 'Model Catalog',
value: 'model_catalog',
key: 'model_catalog',
icon: <GoldOutlined />
},
{
label: 'Hugging Face',
value: modelSourceMap.huggingface_value,
key: 'huggingface',
icon: <IconFont type="icon-huggingface"></IconFont>,
onClick: (e: any) => {
setOpenDeployModal({
show: true,
width: 'calc(100vw - 220px)',
source: modelSourceMap.huggingface_value
});
}
icon: <IconFont type="icon-huggingface"></IconFont>
},
{
label: 'Ollama Library',
value: modelSourceMap.ollama_library_value,
key: 'ollama_library',
icon: <IconFont type="icon-ollama"></IconFont>,
onClick: (e: any) => {
setOpenDeployModal(() => {
return {
show: true,
width: 600,
source: modelSourceMap.ollama_library_value
};
});
}
icon: <IconFont type="icon-ollama"></IconFont>
},
{
label: 'ModelScope',
value: modelSourceMap.modelscope_value,
key: 'modelscope',
icon: <IconFont type="icon-tu2"></IconFont>,
onClick: (e: any) => {
setOpenDeployModal({
show: true,
width: 'calc(100vw - 220px)',
source: modelSourceMap.modelscope_value
});
}
icon: <IconFont type="icon-tu2"></IconFont>
},
{
label: intl.formatMessage({ id: 'models.form.localPath' }),
value: modelSourceMap.local_path_value,
key: 'local_path',
icon: <IconFont type="icon-hard-disk"></IconFont>,
onClick: (e: any) => {
setOpenDeployModal(() => {
return {
show: true,
width: 600,
source: modelSourceMap.local_path_value
};
});
}
icon: <IconFont type="icon-hard-disk"></IconFont>
}
];
@@ -673,6 +648,7 @@ const Models: React.FC<ModelsProps> = ({
},
[intl]
);
const renderChildren = useCallback(
(list: any, parent?: any) => {
return (
@@ -704,6 +680,43 @@ const Models: React.FC<ModelsProps> = ({
return '';
}, []);
const handleClickDropdown = (item: any) => {
if (item.key === 'huggingface') {
setOpenDeployModal({
show: true,
width: 'calc(100vw - 220px)',
source: modelSourceMap.huggingface_value
});
}
if (item.key === 'model_catalog') {
navigate('/models/catalog');
}
if (item.key === 'ollama_library') {
setOpenDeployModal({
show: true,
width: 600,
source: modelSourceMap.ollama_library_value
});
}
if (item.key === 'modelscope') {
setOpenDeployModal({
show: true,
width: 'calc(100vw - 220px)',
source: modelSourceMap.modelscope_value
});
}
if (item.key === 'local_path') {
setOpenDeployModal({
show: true,
width: 600,
source: modelSourceMap.local_path_value
});
}
};
return (
<>
<PageContainer
@@ -734,7 +747,13 @@ const Models: React.FC<ModelsProps> = ({
}
right={
<Space size={20}>
<Dropdown menu={{ items: sourceOptions }} placement="bottomRight">
<Dropdown
menu={{
items: sourceOptions,
onClick: handleClickDropdown
}}
placement="bottomRight"
>
<Button
icon={<DownOutlined></DownOutlined>}
type="primary"
@@ -883,7 +902,6 @@ const Models: React.FC<ModelsProps> = ({
open={openDeployModal.show}
action={PageAction.CREATE}
title={intl.formatMessage({ id: 'models.button.deploy' })}
data={currentData}
source={openDeployModal.source}
width={openDeployModal.width}
onCancel={handleDeployModalCancel}