feat: catalog page
This commit is contained in:
@@ -67,6 +67,15 @@ export default [
|
|||||||
access: 'canSeeAdmin',
|
access: 'canSeeAdmin',
|
||||||
component: './llmodels'
|
component: './llmodels'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'modelsCatalog',
|
||||||
|
path: '/models/catalog',
|
||||||
|
key: 'modelsCatalog',
|
||||||
|
icon: 'Block',
|
||||||
|
access: 'canSeeAdmin',
|
||||||
|
hideInMenu: true,
|
||||||
|
component: './llmodels/catalog'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'resources',
|
name: 'resources',
|
||||||
path: '/resources',
|
path: '/resources',
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.tags-wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 22px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { MoreOutlined } from '@ant-design/icons';
|
||||||
|
import { Dropdown, Tag } from 'antd';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
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 uid = useRef(0);
|
||||||
|
|
||||||
|
const updateUid = () => {
|
||||||
|
uid.current += 1;
|
||||||
|
return uid.current;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropItems = useMemo(() => {
|
||||||
|
return React.Children.toArray(children)
|
||||||
|
.slice(hiddenIndex)
|
||||||
|
.map((child, index) => ({
|
||||||
|
label: child,
|
||||||
|
key: updateUid()
|
||||||
|
}));
|
||||||
|
}, [children, hiddenIndex]);
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const childrenList = wrapperRef.current.children;
|
||||||
|
Array.from(childrenList).forEach((child) => {
|
||||||
|
observer.current?.observe(child);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.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 && (
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: dropItems
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tag className="more">
|
||||||
|
<MoreOutlined rotate={90} />
|
||||||
|
</Tag>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TagsWrapper);
|
||||||
@@ -8,6 +8,8 @@ export default {
|
|||||||
'menu.playground.text2images': 'Image',
|
'menu.playground.text2images': 'Image',
|
||||||
'menu.compare': 'Compare',
|
'menu.compare': 'Compare',
|
||||||
'menu.models': 'Models',
|
'menu.models': 'Models',
|
||||||
|
'menu.models.catalog': 'Model Catalog',
|
||||||
|
'menu.modelsCatalog': 'Model Catalog',
|
||||||
'menu.resources': 'Resources',
|
'menu.resources': 'Resources',
|
||||||
'menu.apikeys': 'API Keys',
|
'menu.apikeys': 'API Keys',
|
||||||
'menu.users': 'Users',
|
'menu.users': 'Users',
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export default {
|
|||||||
'menu.playground.text2images': '文生图',
|
'menu.playground.text2images': '文生图',
|
||||||
'menu.compare': '多模型对比',
|
'menu.compare': '多模型对比',
|
||||||
'menu.models': '模型',
|
'menu.models': '模型',
|
||||||
|
'menu.models.catalog': '模型库',
|
||||||
|
'menu.modelsCatalog': '模型库',
|
||||||
'menu.resources': '资源',
|
'menu.resources': '资源',
|
||||||
'menu.apikeys': 'API 密钥',
|
'menu.apikeys': 'API 密钥',
|
||||||
'menu.users': '用户',
|
'menu.users': '用户',
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import PageTools from '@/components/page-tools';
|
||||||
|
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 _ from 'lodash';
|
||||||
|
import ResizeObserver from 'rc-resize-observer';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { createModel } from './apis';
|
||||||
|
import CatalogItem from './components/catalog-item';
|
||||||
|
import DelopyBuiltInModal from './components/deploy-builtin-modal';
|
||||||
|
import { getSourceRepoConfigValue, modelSourceMap } from './config';
|
||||||
|
import { FormData } from './config/types';
|
||||||
|
|
||||||
|
const Catalog: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [span, setSpan] = React.useState(6);
|
||||||
|
const [activeId, setActiveId] = React.useState(-1);
|
||||||
|
const [openDeployModal, setOpenDeployModal] = useState<any>({
|
||||||
|
show: false,
|
||||||
|
width: 600,
|
||||||
|
source: modelSourceMap.huggingface_value
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResize = useCallback(
|
||||||
|
_.throttle((size: { width: number; height: number }) => {
|
||||||
|
const { width } = size;
|
||||||
|
if (width < breakpoints.xs) {
|
||||||
|
setSpan(24);
|
||||||
|
} else if (width < breakpoints.sm) {
|
||||||
|
setSpan(24);
|
||||||
|
} else if (width < breakpoints.md) {
|
||||||
|
setSpan(12);
|
||||||
|
} else if (width < breakpoints.lg) {
|
||||||
|
setSpan(8);
|
||||||
|
} else {
|
||||||
|
setSpan(6);
|
||||||
|
}
|
||||||
|
console.log('size:', size);
|
||||||
|
}, 100),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeployModalCancel = () => {
|
||||||
|
setOpenDeployModal({
|
||||||
|
...openDeployModal,
|
||||||
|
show: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnDeploy = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
setActiveId(index);
|
||||||
|
setOpenDeployModal({
|
||||||
|
show: true,
|
||||||
|
source: modelSourceMap.huggingface_value,
|
||||||
|
width: 600
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[openDeployModal]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateModel = useCallback(
|
||||||
|
async (data: FormData) => {
|
||||||
|
try {
|
||||||
|
console.log('data:', data, openDeployModal);
|
||||||
|
|
||||||
|
const result = getSourceRepoConfigValue(openDeployModal.source, data);
|
||||||
|
|
||||||
|
const modelData = await createModel({
|
||||||
|
data: {
|
||||||
|
...result.values,
|
||||||
|
..._.omit(data, result.omits)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setOpenDeployModal({
|
||||||
|
...openDeployModal,
|
||||||
|
show: false
|
||||||
|
});
|
||||||
|
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
[openDeployModal]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
ghost
|
||||||
|
header={{
|
||||||
|
title: intl.formatMessage({ id: 'menu.models.catalog' }),
|
||||||
|
breadcrumb: {}
|
||||||
|
}}
|
||||||
|
extra={[]}
|
||||||
|
>
|
||||||
|
<PageTools
|
||||||
|
marginBottom={22}
|
||||||
|
left={
|
||||||
|
<Space>
|
||||||
|
<Input
|
||||||
|
placeholder={intl.formatMessage({ id: 'common.filter.name' })}
|
||||||
|
style={{ width: 300 }}
|
||||||
|
size="large"
|
||||||
|
allowClear
|
||||||
|
></Input>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||||
|
icon={<SyncOutlined></SyncOutlined>}
|
||||||
|
></Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
></PageTools>
|
||||||
|
<ResizeObserver onResize={handleResize}>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{Array.from({ length: 12 }).map((_, index) => {
|
||||||
|
return (
|
||||||
|
<Col span={span} key={index}>
|
||||||
|
<CatalogItem
|
||||||
|
onDeploy={() => handleOnDeploy(index)}
|
||||||
|
activeId={activeId}
|
||||||
|
itemId={index}
|
||||||
|
></CatalogItem>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
</ResizeObserver>
|
||||||
|
<div style={{ marginBlock: '32px 16px' }}>
|
||||||
|
<Pagination align="end" defaultCurrent={1} total={50} showSizeChanger />
|
||||||
|
</div>
|
||||||
|
<DelopyBuiltInModal
|
||||||
|
open={openDeployModal.show}
|
||||||
|
action={PageAction.CREATE}
|
||||||
|
title={intl.formatMessage({ id: 'models.button.deploy' })}
|
||||||
|
source={openDeployModal.source}
|
||||||
|
width={openDeployModal.width}
|
||||||
|
onCancel={handleDeployModalCancel}
|
||||||
|
onOk={handleCreateModel}
|
||||||
|
></DelopyBuiltInModal>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Catalog;
|
||||||
@@ -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);
|
||||||
@@ -6,7 +6,7 @@ import { Button, Drawer } from 'antd';
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { backendOptionsMap, modelSourceMap } from '../config';
|
import { backendOptionsMap, modelSourceMap } from '../config';
|
||||||
import { FormData, ListItem } from '../config/types';
|
import { FormData } from '../config/types';
|
||||||
import ColumnWrapper from './column-wrapper';
|
import ColumnWrapper from './column-wrapper';
|
||||||
import DataForm from './data-form';
|
import DataForm from './data-form';
|
||||||
import HFModelFile from './hf-model-file';
|
import HFModelFile from './hf-model-file';
|
||||||
@@ -19,7 +19,6 @@ type AddModalProps = {
|
|||||||
title: string;
|
title: string;
|
||||||
action: PageActionType;
|
action: PageActionType;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
data?: ListItem;
|
|
||||||
source: string;
|
source: string;
|
||||||
width?: string | number;
|
width?: string | number;
|
||||||
onOk: (values: FormData) => void;
|
onOk: (values: FormData) => void;
|
||||||
@@ -57,10 +56,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
modelSourceMap.huggingface_value,
|
modelSourceMap.huggingface_value,
|
||||||
modelSourceMap.modelscope_value
|
modelSourceMap.modelscope_value
|
||||||
];
|
];
|
||||||
// const { start } = useDriver({
|
|
||||||
// steps,
|
|
||||||
// id: 'deploy-model'
|
|
||||||
// });
|
|
||||||
const form = useRef<any>({});
|
const form = useRef<any>({});
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [selectedModel, setSelectedModel] = useState<any>({});
|
const [selectedModel, setSelectedModel] = useState<any>({});
|
||||||
@@ -126,14 +122,6 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
|||||||
};
|
};
|
||||||
}, [open, source]);
|
}, [open, source]);
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (open && loadfinish) {
|
|
||||||
// setTimeout(() => {
|
|
||||||
// start();
|
|
||||||
// }, 1000);
|
|
||||||
// }
|
|
||||||
// }, [loadfinish, open]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
DownOutlined,
|
DownOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
ExperimentOutlined,
|
ExperimentOutlined,
|
||||||
|
GoldOutlined,
|
||||||
PictureOutlined,
|
PictureOutlined,
|
||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
WechatWorkOutlined
|
WechatWorkOutlined
|
||||||
@@ -221,61 +222,35 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
}, [deleteIds]);
|
}, [deleteIds]);
|
||||||
|
|
||||||
const sourceOptions = [
|
const sourceOptions = [
|
||||||
|
{
|
||||||
|
label: 'Model Catalog',
|
||||||
|
value: 'model_catalog',
|
||||||
|
key: 'model_catalog',
|
||||||
|
icon: <GoldOutlined />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Hugging Face',
|
label: 'Hugging Face',
|
||||||
value: modelSourceMap.huggingface_value,
|
value: modelSourceMap.huggingface_value,
|
||||||
key: 'huggingface',
|
key: 'huggingface',
|
||||||
icon: <IconFont type="icon-huggingface"></IconFont>,
|
icon: <IconFont type="icon-huggingface"></IconFont>
|
||||||
onClick: (e: any) => {
|
|
||||||
setOpenDeployModal({
|
|
||||||
show: true,
|
|
||||||
width: 'calc(100vw - 220px)',
|
|
||||||
source: modelSourceMap.huggingface_value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Ollama Library',
|
label: 'Ollama Library',
|
||||||
value: modelSourceMap.ollama_library_value,
|
value: modelSourceMap.ollama_library_value,
|
||||||
key: 'ollama_library',
|
key: 'ollama_library',
|
||||||
icon: <IconFont type="icon-ollama"></IconFont>,
|
icon: <IconFont type="icon-ollama"></IconFont>
|
||||||
onClick: (e: any) => {
|
|
||||||
setOpenDeployModal(() => {
|
|
||||||
return {
|
|
||||||
show: true,
|
|
||||||
width: 600,
|
|
||||||
source: modelSourceMap.ollama_library_value
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'ModelScope',
|
label: 'ModelScope',
|
||||||
value: modelSourceMap.modelscope_value,
|
value: modelSourceMap.modelscope_value,
|
||||||
key: 'modelscope',
|
key: 'modelscope',
|
||||||
icon: <IconFont type="icon-tu2"></IconFont>,
|
icon: <IconFont type="icon-tu2"></IconFont>
|
||||||
onClick: (e: any) => {
|
|
||||||
setOpenDeployModal({
|
|
||||||
show: true,
|
|
||||||
width: 'calc(100vw - 220px)',
|
|
||||||
source: modelSourceMap.modelscope_value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({ id: 'models.form.localPath' }),
|
label: intl.formatMessage({ id: 'models.form.localPath' }),
|
||||||
value: modelSourceMap.local_path_value,
|
value: modelSourceMap.local_path_value,
|
||||||
key: 'local_path',
|
key: 'local_path',
|
||||||
icon: <IconFont type="icon-hard-disk"></IconFont>,
|
icon: <IconFont type="icon-hard-disk"></IconFont>
|
||||||
onClick: (e: any) => {
|
|
||||||
setOpenDeployModal(() => {
|
|
||||||
return {
|
|
||||||
show: true,
|
|
||||||
width: 600,
|
|
||||||
source: modelSourceMap.local_path_value
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -673,6 +648,7 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
},
|
},
|
||||||
[intl]
|
[intl]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderChildren = useCallback(
|
const renderChildren = useCallback(
|
||||||
(list: any, parent?: any) => {
|
(list: any, parent?: any) => {
|
||||||
return (
|
return (
|
||||||
@@ -704,6 +680,43 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
return '';
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageContainer
|
<PageContainer
|
||||||
@@ -734,7 +747,13 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
<Space size={20}>
|
<Space size={20}>
|
||||||
<Dropdown menu={{ items: sourceOptions }} placement="bottomRight">
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: sourceOptions,
|
||||||
|
onClick: handleClickDropdown
|
||||||
|
}}
|
||||||
|
placement="bottomRight"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
icon={<DownOutlined></DownOutlined>}
|
icon={<DownOutlined></DownOutlined>}
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -883,7 +902,6 @@ const Models: React.FC<ModelsProps> = ({
|
|||||||
open={openDeployModal.show}
|
open={openDeployModal.show}
|
||||||
action={PageAction.CREATE}
|
action={PageAction.CREATE}
|
||||||
title={intl.formatMessage({ id: 'models.button.deploy' })}
|
title={intl.formatMessage({ id: 'models.button.deploy' })}
|
||||||
data={currentData}
|
|
||||||
source={openDeployModal.source}
|
source={openDeployModal.source}
|
||||||
width={openDeployModal.width}
|
width={openDeployModal.width}
|
||||||
onCancel={handleDeployModalCancel}
|
onCancel={handleDeployModalCancel}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
.catalog-item {
|
||||||
|
height: 146px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
padding: 12px 14px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.img {
|
||||||
|
width: 32px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
// height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-fill-sider);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-fill-sider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--font-size-middle);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-footer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 22px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user