refactor: deployments dir tree
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { convertFileSize } from '@/utils';
|
||||
import React from 'react';
|
||||
import 'simplebar-react/dist/simplebar.min.css';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.file-part-item {
|
||||
width: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`;
|
||||
|
||||
const FileParts: React.FC<{
|
||||
showSize?: boolean;
|
||||
fileList: any[];
|
||||
}> = ({ fileList, showSize = true }) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
{fileList.map((file, index) => {
|
||||
return (
|
||||
<div key={index} className="file-part-item">
|
||||
<span>
|
||||
Part {file.part} of {file.total}
|
||||
</span>
|
||||
{<span>{convertFileSize(file.size)}</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileParts;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Flex, Skeleton, Space } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled(Flex)`
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
background-color: var(--ant-color-bg-container);
|
||||
`;
|
||||
|
||||
const FileSkeleton: React.FC<{ counts: number; itemHeight?: number }> = ({
|
||||
counts = 2,
|
||||
itemHeight
|
||||
}) => {
|
||||
return (
|
||||
<Wrapper
|
||||
vertical
|
||||
justify={'space-between'}
|
||||
style={{ height: itemHeight || 'auto' }}
|
||||
>
|
||||
<Skeleton paragraph={{ rows: 1, width: '100%' }} title={false}></Skeleton>
|
||||
<Space>
|
||||
{Array.from({ length: counts }).map((_, index) => (
|
||||
<Skeleton.Node
|
||||
style={{ width: 60, height: 22 }}
|
||||
key={index}
|
||||
></Skeleton.Node>
|
||||
))}
|
||||
</Space>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSkeleton;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Result } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const GGUFResult: React.FC = () => {
|
||||
return (
|
||||
<Result
|
||||
status="info"
|
||||
icon={
|
||||
<WarningOutlined
|
||||
style={{ color: 'var(--ant-color-text-quaternary)' }}
|
||||
/>
|
||||
}
|
||||
title={false}
|
||||
subTitle="GGUF model is not supported."
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GGUFResult;
|
||||
@@ -0,0 +1,370 @@
|
||||
import { getRequestId } from '@/atoms/models';
|
||||
import BaseSelect from '@/components/seal-form/base/select';
|
||||
import SimpleOverlay from '@/components/simple-overlay';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Empty, Spin } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import 'simplebar-react/dist/simplebar.min.css';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
queryHuggingfaceModelFiles,
|
||||
queryModelScopeModelFiles
|
||||
} from '../../apis';
|
||||
import { modelSourceMap } from '../../config';
|
||||
import '../../style/hf-model-file.less';
|
||||
import TitleWrapper from '../title-wrapper';
|
||||
import FileSkeleton from './file-skeleton';
|
||||
import ModelFileItem from './model-file-item';
|
||||
|
||||
const ItemFileWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 24px;
|
||||
`;
|
||||
|
||||
interface HFModelFileProps {
|
||||
isDownload?: boolean;
|
||||
selectedModel: any;
|
||||
collapsed?: boolean;
|
||||
loadingModel?: boolean;
|
||||
modelSource: string;
|
||||
ref: any;
|
||||
onSelectFile?: (
|
||||
file: any,
|
||||
options: { requestModelId: number; manual?: boolean }
|
||||
) => void;
|
||||
onSelectFileAfterEvaluate?: (file: any) => void;
|
||||
}
|
||||
|
||||
const pattern = /^(.*)-(\d+)-of-(\d+)\.(.*)$/;
|
||||
|
||||
const filterReg = /\.(safetensors|gguf)$/i;
|
||||
const includeReg = /\.(safetensors|gguf)$/i;
|
||||
const filterRegGGUF = /\.(gguf)$/i;
|
||||
|
||||
const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
|
||||
const { collapsed, modelSource, isDownload, onSelectFileAfterEvaluate } =
|
||||
props;
|
||||
const intl = useIntl();
|
||||
const [isEvaluating, setIsEvaluating] = useState(false);
|
||||
const [dataSource, setDataSource] = useState<any>({
|
||||
fileList: [],
|
||||
loading: false
|
||||
});
|
||||
const [sortType, setSortType] = useState<string>('size');
|
||||
const [current, setCurrent] = useState<string>('');
|
||||
const currentPathRef = useRef<string>('');
|
||||
const modelFilesSortOptions = useRef<any[]>([
|
||||
{
|
||||
label: intl.formatMessage({ id: 'models.sort.size' }),
|
||||
value: 'size'
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'models.sort.name' }),
|
||||
value: 'name'
|
||||
}
|
||||
]);
|
||||
const axiosTokenRef = useRef<any>(null);
|
||||
const checkTokenRef = useRef<any>(null);
|
||||
const timer = useRef<any>(null);
|
||||
const parentRequestModelId = useRef<number>(0);
|
||||
|
||||
const handleSelectModelFile = (item: any, manual?: boolean) => {
|
||||
props.onSelectFile?.(item, {
|
||||
requestModelId: parentRequestModelId.current,
|
||||
manual: manual
|
||||
});
|
||||
setCurrent(item.path);
|
||||
currentPathRef.current = item.path;
|
||||
};
|
||||
|
||||
const handleSelectModelFileManually = (data: any) => {
|
||||
if (data.path === currentPathRef.current) {
|
||||
return;
|
||||
}
|
||||
handleSelectModelFile(data, true);
|
||||
};
|
||||
|
||||
const parseFilename = (filename: string) => {
|
||||
const match = filename.match(pattern);
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
filename: match[1],
|
||||
part: parseInt(match[2], 10),
|
||||
total: parseInt(match[3], 10),
|
||||
extension: match[4]
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const generateGroupByFilename = (list: any[]) => {
|
||||
// general file
|
||||
const generalFileList = _.filter(list, (item: any) => {
|
||||
const parsed = parseFilename(item.path);
|
||||
return !parsed;
|
||||
});
|
||||
|
||||
const newGeneralFileList = _.map(generalFileList, (item: any) => {
|
||||
return {
|
||||
...item,
|
||||
fakeName: item.path
|
||||
};
|
||||
});
|
||||
|
||||
// shard file
|
||||
|
||||
const shardFileList = _.filter(list, (item: any) => {
|
||||
const parsed = parseFilename(item.path);
|
||||
return !!parsed;
|
||||
});
|
||||
|
||||
const newShardFileList = _.map(shardFileList, (item: any) => {
|
||||
const parsed = parseFilename(item.path);
|
||||
return {
|
||||
...item,
|
||||
...parsed
|
||||
};
|
||||
});
|
||||
|
||||
const group = _.groupBy(newShardFileList, 'filename');
|
||||
|
||||
const shardFileListResult = _.map(
|
||||
group,
|
||||
(value: any[], filename: string) => {
|
||||
return {
|
||||
path: filename,
|
||||
fakeName: `${filename}-*.${_.get(value, '[0].extension')}`,
|
||||
size: _.sumBy(value, 'size'),
|
||||
parts: value
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return [...shardFileListResult, ...newGeneralFileList];
|
||||
};
|
||||
|
||||
const hfFileFilter = (file: any) => {
|
||||
return filterRegGGUF.test(file.path) || _.includes(file.path, '.gguf');
|
||||
};
|
||||
|
||||
const isNormalGGUFModelFile = (filename: string) => {
|
||||
const file = filename?.toLowerCase() ?? '';
|
||||
return file.indexOf('mmproj') === -1 && file.indexOf('imatrix') === -1;
|
||||
};
|
||||
|
||||
// hugging face files
|
||||
const getHuggingfaceFiles = async () => {
|
||||
try {
|
||||
const res = await queryHuggingfaceModelFiles(
|
||||
{ repo: props.selectedModel.name || '' },
|
||||
{
|
||||
signal: axiosTokenRef.current.signal
|
||||
}
|
||||
);
|
||||
const fileList = _.filter(res, (file: any) => {
|
||||
return file.type === 'file';
|
||||
});
|
||||
|
||||
const list = _.filter(fileList, (file: any) => {
|
||||
return hfFileFilter(file) && isNormalGGUFModelFile(file.path);
|
||||
});
|
||||
|
||||
return list;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const modelscopeFileFilter = (file: any) => {
|
||||
return (
|
||||
filterRegGGUF.test(file.Path) &&
|
||||
file.Type === 'blob' &&
|
||||
isNormalGGUFModelFile(file.Path)
|
||||
);
|
||||
};
|
||||
|
||||
// modelscope files
|
||||
const getModelScopeFiles = async () => {
|
||||
try {
|
||||
const data = await queryModelScopeModelFiles(
|
||||
{
|
||||
name: props.selectedModel.name || '',
|
||||
revision: props.selectedModel.revision || 'master'
|
||||
},
|
||||
{
|
||||
signal: axiosTokenRef.current.signal
|
||||
}
|
||||
);
|
||||
const fileList = _.filter(_.get(data, ['Data', 'Files']), (file: any) => {
|
||||
return modelscopeFileFilter(file);
|
||||
});
|
||||
|
||||
const list = _.map(fileList, (item: any) => {
|
||||
return {
|
||||
path: item.Path,
|
||||
size: item.Size
|
||||
};
|
||||
});
|
||||
return list;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetchModelFiles = async () => {
|
||||
if (!props.selectedModel.name) {
|
||||
setDataSource({ fileList: [], loading: false });
|
||||
handleSelectModelFile({});
|
||||
return;
|
||||
}
|
||||
parentRequestModelId.current = getRequestId();
|
||||
checkTokenRef.current?.cancel?.();
|
||||
axiosTokenRef.current?.abort?.();
|
||||
axiosTokenRef.current = new AbortController();
|
||||
setDataSource({ ...dataSource, loading: true });
|
||||
|
||||
setCurrent('');
|
||||
try {
|
||||
let list = [];
|
||||
const currentParentRequestId = getRequestId();
|
||||
if (modelSourceMap.huggingface_value === modelSource) {
|
||||
list = await getHuggingfaceFiles();
|
||||
} else if (modelSourceMap.modelscope_value === modelSource) {
|
||||
list = await getModelScopeFiles();
|
||||
}
|
||||
|
||||
if (currentParentRequestId !== getRequestId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newList = generateGroupByFilename(list);
|
||||
const sortList = _.sortBy(newList, (item: any) => {
|
||||
return sortType === 'size' ? item.size : item.path;
|
||||
});
|
||||
|
||||
handleSelectModelFile(sortList[0] || {});
|
||||
setDataSource({ fileList: sortList, loading: false });
|
||||
} catch (error) {
|
||||
setDataSource({ fileList: [], loading: false });
|
||||
handleSelectModelFile({});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSortChange = (value: string) => {
|
||||
const list = _.sortBy(dataSource.fileList, (item: any) => {
|
||||
return value === 'size' ? item.size : item.path;
|
||||
});
|
||||
setSortType(value);
|
||||
setDataSource({ ...dataSource, fileList: list });
|
||||
};
|
||||
|
||||
const cancelRequest = () => {
|
||||
axiosTokenRef.current?.abort?.();
|
||||
checkTokenRef.current?.cancel?.();
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
fetchModelFiles: handleFetchModelFiles,
|
||||
cancelRequest: cancelRequest
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.selectedModel.name) {
|
||||
setDataSource({ fileList: [], loading: false });
|
||||
}
|
||||
}, [props.selectedModel?.name]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancelRequest();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="files-wrap">
|
||||
<TitleWrapper style={{ paddingInline: '24px' }}>
|
||||
<span className="title">
|
||||
{intl.formatMessage({ id: 'models.available.files' })} (
|
||||
{dataSource.fileList.length || 0})
|
||||
</span>
|
||||
<BaseSelect
|
||||
value={sortType}
|
||||
onChange={handleSortChange}
|
||||
labelRender={({ label }) => {
|
||||
return (
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'model.deploy.sort' })}: {label}
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
options={modelFilesSortOptions.current}
|
||||
size="middle"
|
||||
style={{ width: '120px', fontWeight: 400 }}
|
||||
></BaseSelect>
|
||||
</TitleWrapper>
|
||||
{dataSource.loading && (
|
||||
<div className="spin-wrapper">
|
||||
<Spin
|
||||
spinning={dataSource.loading}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
></Spin>
|
||||
</div>
|
||||
)}
|
||||
<SimpleOverlay height={collapsed ? 'max-content' : 'calc(100vh - 300px)'}>
|
||||
<div style={{ padding: '16px 24px' }}>
|
||||
{dataSource.loading ? (
|
||||
<ItemFileWrapper>
|
||||
{_.times(5, (index: number) => {
|
||||
return <FileSkeleton key={index} counts={2}></FileSkeleton>;
|
||||
})}
|
||||
</ItemFileWrapper>
|
||||
) : dataSource.fileList.length ? (
|
||||
<ItemFileWrapper>
|
||||
{_.map(dataSource.fileList, (item: any) => {
|
||||
return (
|
||||
<ModelFileItem
|
||||
key={item.path}
|
||||
data={item}
|
||||
isEvaluating={isEvaluating}
|
||||
active={item.path === current}
|
||||
handleSelectModelFile={handleSelectModelFileManually}
|
||||
></ModelFileItem>
|
||||
);
|
||||
})}
|
||||
</ItemFileWrapper>
|
||||
) : (
|
||||
<Empty
|
||||
styles={{
|
||||
image: {
|
||||
height: 'auto',
|
||||
marginTop: '20px'
|
||||
}
|
||||
}}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={intl.formatMessage({
|
||||
id: 'models.search.nofiles'
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SimpleOverlay>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default HFModelFile;
|
||||
@@ -0,0 +1,69 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { formatNumber } from '@/utils';
|
||||
import { DownloadOutlined, HeartOutlined } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
import React from 'react';
|
||||
import { modelSourceMap } from '../../config';
|
||||
import { EvaluateResult } from '../../config/types';
|
||||
import '../../style/hf-model-item.less';
|
||||
import IncompatiableInfo from '../incompatiable-info';
|
||||
|
||||
interface HFModelItemProps {
|
||||
title: string;
|
||||
downloads: number;
|
||||
likes: number;
|
||||
task?: string;
|
||||
updatedAt: string;
|
||||
active: boolean;
|
||||
source?: string;
|
||||
tags?: string[];
|
||||
evaluateResult?: EvaluateResult;
|
||||
isEvaluating?: boolean;
|
||||
}
|
||||
|
||||
const HFModelItem: React.FC<HFModelItemProps> = (props) => {
|
||||
const { evaluateResult, isEvaluating } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('hf-model-item', {
|
||||
active: props.active
|
||||
})}
|
||||
>
|
||||
<div className="title">
|
||||
<IconFont
|
||||
type={
|
||||
props.source === modelSourceMap.huggingface_value
|
||||
? 'icon-huggingface1'
|
||||
: 'icon-modelscope_light'
|
||||
}
|
||||
className="m-r-5"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)', fontSize: 16 }}
|
||||
/>
|
||||
{props.title}
|
||||
</div>
|
||||
<div className="info">
|
||||
<div className="info-item">
|
||||
<span>{dayjs().to(dayjs(props.updatedAt))}</span>
|
||||
<span className="flex-center">
|
||||
<HeartOutlined className="m-r-5" />
|
||||
{props.likes}
|
||||
</span>
|
||||
<span className="flex-center">
|
||||
<DownloadOutlined className="m-r-5" />
|
||||
{formatNumber(props.downloads)}
|
||||
</span>
|
||||
</div>
|
||||
{
|
||||
<IncompatiableInfo
|
||||
data={evaluateResult}
|
||||
isEvaluating={isEvaluating}
|
||||
></IncompatiableInfo>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HFModelItem;
|
||||
@@ -0,0 +1,409 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import MarkdownViewer from '@/components/markdown-viewer';
|
||||
import SimpleOverlay from '@/components/simple-overlay';
|
||||
import ThemeTag from '@/components/tags-wrapper/theme-tag';
|
||||
import { GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||
import useRequestToken from '@/hooks/use-request-token';
|
||||
import {
|
||||
DownOutlined,
|
||||
FileMarkdownOutlined,
|
||||
RightOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Empty, Spin, Tooltip } from 'antd';
|
||||
import { some } from 'lodash';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import 'simplebar-react/dist/simplebar.min.css';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
downloadModelFile,
|
||||
queryHuggingfaceModelDetail,
|
||||
queryModelScopeModelDetail
|
||||
} from '../../apis';
|
||||
import { modelSourceMap } from '../../config';
|
||||
import '../../style/model-card.less';
|
||||
import TitleWrapper from '../title-wrapper';
|
||||
|
||||
const MkdTitle = styled.span`
|
||||
cursor: pointer;
|
||||
background-color: var(--ant-color-fill-tertiary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
height: 36px;
|
||||
`;
|
||||
|
||||
const MarkDownTitle: React.FC<{
|
||||
collapsed: boolean;
|
||||
loading: boolean;
|
||||
onCollapse: () => void;
|
||||
}> = ({ collapsed, loading, onCollapse }) => {
|
||||
return (
|
||||
<MkdTitle onClick={onCollapse}>
|
||||
<span>
|
||||
<FileMarkdownOutlined className="m-r-2 text-tertiary" /> README.md
|
||||
</span>
|
||||
<span>
|
||||
{collapsed ? (
|
||||
<DownOutlined />
|
||||
) : loading ? (
|
||||
<Spin spinning={true} size="small"></Spin>
|
||||
) : (
|
||||
<RightOutlined />
|
||||
)}
|
||||
</span>
|
||||
</MkdTitle>
|
||||
);
|
||||
};
|
||||
|
||||
const ModelCard: React.FC<{
|
||||
onCollapse: (flag: boolean) => void;
|
||||
setIsGGUF: (flag: boolean) => void;
|
||||
isGGUF?: boolean;
|
||||
selectedModel: any;
|
||||
collapsed: boolean;
|
||||
loadingModel?: boolean;
|
||||
modelSource: string;
|
||||
}> = (props) => {
|
||||
const { onCollapse, setIsGGUF, collapsed, modelSource, isGGUF } = props;
|
||||
const intl = useIntl();
|
||||
const requestSource = useRequestToken();
|
||||
const [modelData, setModelData] = useState<any>(null);
|
||||
const [readmeText, setReadmeText] = useState<string | null>(null);
|
||||
const requestToken = useRef<any>(null);
|
||||
const axiosTokenRef = useRef<any>(null);
|
||||
const loadConfigTokenRef = useRef<any>(null);
|
||||
const loadConfigJsonTokenRef = useRef<any>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const modelTags = useMemo(() => {
|
||||
if (modelSource === modelSourceMap.huggingface_value) {
|
||||
return modelData?.pipeline_tag ? [modelData?.pipeline_tag] : [];
|
||||
}
|
||||
if (modelSource === modelSourceMap.modelscope_value) {
|
||||
const tasks = modelData?.Tasks || [];
|
||||
return tasks.map((task: any) => task?.Name)?.filter((val: string) => val);
|
||||
}
|
||||
return [];
|
||||
}, [modelSource, modelData]);
|
||||
|
||||
const modelType = useMemo(() => {
|
||||
if (modelSource === modelSourceMap.huggingface_value) {
|
||||
return modelData?.config?.model_type || modelData?.ModelType?.[0];
|
||||
}
|
||||
if (modelSource === modelSourceMap.modelscope_value) {
|
||||
return modelData?.ModelType?.[0];
|
||||
}
|
||||
}, [modelData, modelSource]);
|
||||
|
||||
const loadFile = useCallback(async (repo: string, sha: string) => {
|
||||
try {
|
||||
axiosTokenRef.current?.abort?.();
|
||||
axiosTokenRef.current = new AbortController();
|
||||
const res = await downloadModelFile(
|
||||
{
|
||||
repo,
|
||||
revision: sha,
|
||||
path: 'README.md'
|
||||
},
|
||||
{
|
||||
signal: axiosTokenRef.current.signal
|
||||
}
|
||||
);
|
||||
return res || '';
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleOnCollapse = (readmeText: any) => {
|
||||
if (!readmeText) {
|
||||
onCollapse(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeMetadata = useCallback((str: string) => {
|
||||
let indexes = [];
|
||||
let index = str.indexOf('---');
|
||||
|
||||
while (index !== -1) {
|
||||
indexes.push(index);
|
||||
if (indexes.length >= 2) {
|
||||
break;
|
||||
}
|
||||
index = str.indexOf('---', index + 1);
|
||||
}
|
||||
if (indexes.length >= 2) {
|
||||
return str.slice(indexes[1] + 3);
|
||||
}
|
||||
return str;
|
||||
}, []);
|
||||
|
||||
// huggingface model card data
|
||||
const getHuggingfaceModelDetail = async () => {
|
||||
try {
|
||||
const [modelcard, readme] = await Promise.all([
|
||||
queryHuggingfaceModelDetail(
|
||||
{ repo: props.selectedModel.name },
|
||||
{
|
||||
token: requestToken.current.token
|
||||
}
|
||||
),
|
||||
loadFile(props.selectedModel.name, 'main')
|
||||
]);
|
||||
|
||||
setModelData(modelcard);
|
||||
|
||||
// remove the meta data from readme
|
||||
const newReadme = removeMetadata(readme);
|
||||
|
||||
setReadmeText(newReadme);
|
||||
handleOnCollapse(newReadme);
|
||||
const isGGUF = modelcard.tags?.includes('gguf');
|
||||
setIsGGUF(isGGUF || props.selectedModel?.isGGUF);
|
||||
} catch (error) {
|
||||
setModelData(null);
|
||||
setReadmeText(null);
|
||||
handleOnCollapse(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getModelScopeModelDetail = async () => {
|
||||
try {
|
||||
const data = await queryModelScopeModelDetail(
|
||||
{
|
||||
name: props.selectedModel.name
|
||||
},
|
||||
{
|
||||
token: requestToken.current.token
|
||||
}
|
||||
);
|
||||
setModelData({
|
||||
...data?.Data,
|
||||
name: `${data.Data?.Path}/${data.Data?.Name}`
|
||||
});
|
||||
setReadmeText(data?.Data?.ReadMeContent);
|
||||
handleOnCollapse(data?.Data?.ReadMeContent);
|
||||
const isGGUF = some(
|
||||
data?.Data?.Tags,
|
||||
(tag: string) => tag?.indexOf('gguf') > -1
|
||||
);
|
||||
setIsGGUF(isGGUF || props.selectedModel?.isGGUF);
|
||||
} catch (error) {
|
||||
setModelData(null);
|
||||
setReadmeText(null);
|
||||
handleOnCollapse(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getModelCardData = async () => {
|
||||
if (!props.selectedModel?.name) {
|
||||
setModelData(null);
|
||||
setReadmeText(null);
|
||||
handleOnCollapse(null);
|
||||
return;
|
||||
}
|
||||
requestToken.current?.cancel?.();
|
||||
requestToken.current = requestSource();
|
||||
setLoading(true);
|
||||
if (modelSource === modelSourceMap.huggingface_value) {
|
||||
await getHuggingfaceModelDetail();
|
||||
} else if (modelSource === modelSourceMap.modelscope_value) {
|
||||
await getModelScopeModelDetail();
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleCollapse = useCallback(() => {
|
||||
onCollapse(!collapsed);
|
||||
}, [collapsed]);
|
||||
|
||||
const generateModelLink = () => {
|
||||
const name = modelData?.id || modelData?.name;
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
if (modelSource === modelSourceMap.huggingface_value) {
|
||||
return (
|
||||
<Tooltip title={intl.formatMessage({ id: 'models.viewin.hf' })}>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
target="_blank"
|
||||
href={`https://huggingface.co/${modelData?.id}`}
|
||||
>
|
||||
<IconFont
|
||||
type="icon-external-link"
|
||||
className="font-size-14"
|
||||
></IconFont>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (modelSource === modelSourceMap.modelscope_value) {
|
||||
return (
|
||||
<Tooltip title={intl.formatMessage({ id: 'models.viewin.modelscope' })}>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
target="_blank"
|
||||
href={`https://modelscope.cn/models/${modelData?.name}`}
|
||||
>
|
||||
<IconFont
|
||||
type="icon-external-link"
|
||||
className="font-size-14"
|
||||
></IconFont>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const generateModeScopeImgLink = useCallback(
|
||||
(imgSrc: string) => {
|
||||
if (!imgSrc) {
|
||||
return '';
|
||||
}
|
||||
if (modelSource === modelSourceMap.modelscope_value) {
|
||||
return `https://modelscope.cn/api/${GPUSTACK_API_BASE_URL}/models/${modelData?.name}/repo?Revision=${modelData?.Revision}&View=true&FilePath=${imgSrc}`;
|
||||
}
|
||||
if (modelSource === modelSourceMap.huggingface_value) {
|
||||
return `https://huggingface.co/${modelData?.id}/resolve/main/${imgSrc}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
[modelSource, modelData?.name, modelData?.id, modelData?.Revision]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.selectedModel.name) return;
|
||||
getModelCardData();
|
||||
setModelData(() => ({
|
||||
id: props.selectedModel.name,
|
||||
name: props.selectedModel.name,
|
||||
isGGUF: props.selectedModel.isGGUF
|
||||
}));
|
||||
}, [props.selectedModel?.name, props.selectedModel?.isGGUF]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
requestToken.current?.cancel?.();
|
||||
axiosTokenRef.current?.abort?.();
|
||||
loadConfigTokenRef.current?.abort?.();
|
||||
loadConfigJsonTokenRef.current?.abort?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleWrapper style={{ paddingInline: 24 }}>
|
||||
<div className="title">{modelData?.id || modelData?.name} </div>
|
||||
{generateModelLink()}
|
||||
</TitleWrapper>
|
||||
<div className="card-wrapper">
|
||||
{modelData ? (
|
||||
<div className="model-card-wrap">
|
||||
<div className="flex-center flex-wrap gap-8">
|
||||
{modelType && (
|
||||
<ThemeTag className="tag-item" color="gold" opacity={0.65}>
|
||||
<span className="m-r-5">
|
||||
{intl.formatMessage({ id: 'models.architecture' })}:
|
||||
</span>
|
||||
{modelType}
|
||||
</ThemeTag>
|
||||
)}
|
||||
{isGGUF && (
|
||||
<ThemeTag className="tag-item" color="magenta" opacity={0.65}>
|
||||
GGUF
|
||||
</ThemeTag>
|
||||
)}
|
||||
{!!modelTags.length &&
|
||||
modelTags.map((tag: string, index: number) => {
|
||||
return (
|
||||
<ThemeTag
|
||||
className="tag-item"
|
||||
color="geekblue"
|
||||
key={index}
|
||||
opacity={0.65}
|
||||
>
|
||||
{tag}
|
||||
</ThemeTag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{readmeText && isGGUF && (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
marginTop: 16,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<MarkDownTitle
|
||||
onCollapse={handleCollapse}
|
||||
collapsed={collapsed}
|
||||
loading={loading}
|
||||
></MarkDownTitle>
|
||||
<Spin spinning={loading && collapsed} size="middle">
|
||||
<SimpleOverlay
|
||||
style={{
|
||||
paddingTop: collapsed ? 12 : 0,
|
||||
maxHeight: collapsed ? 300 : 0
|
||||
}}
|
||||
>
|
||||
<MarkdownViewer
|
||||
generateImgLink={generateModeScopeImgLink}
|
||||
content={readmeText}
|
||||
theme="light"
|
||||
></MarkdownViewer>
|
||||
</SimpleOverlay>
|
||||
</Spin>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!loading && (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
style={{ marginBlock: 20 }}
|
||||
></Empty>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isGGUF && (
|
||||
<Spin spinning={loading} size="middle">
|
||||
<div style={{ minHeight: 200 }}>
|
||||
{readmeText && (
|
||||
<>
|
||||
<TitleWrapper style={{ paddingInline: 24 }}>
|
||||
<span className="title">README.md</span>
|
||||
</TitleWrapper>
|
||||
<div className="card-wrapper">
|
||||
<MarkdownViewer
|
||||
generateImgLink={generateModeScopeImgLink}
|
||||
content={readmeText}
|
||||
theme="light"
|
||||
></MarkdownViewer>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Spin>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelCard;
|
||||
@@ -0,0 +1,109 @@
|
||||
import { TooltipOverlayScroller } from '@/components/overlay-scroller';
|
||||
import ThemeTag from '@/components/tags-wrapper/theme-tag';
|
||||
import { convertFileSize } from '@/utils';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import 'simplebar-react/dist/simplebar.min.css';
|
||||
import { getFileType } from '../../constants/file-type';
|
||||
import '../../style/hf-model-file.less';
|
||||
import IncompatiableInfo from '../incompatiable-info';
|
||||
import FileParts from './file-parts';
|
||||
|
||||
interface ModelFileItemProps {
|
||||
data: Record<string, any>;
|
||||
isEvaluating: boolean;
|
||||
active: boolean;
|
||||
handleSelectModelFile: (item: any) => void;
|
||||
}
|
||||
|
||||
const FilePartsTag = (props: { parts: any[] }) => {
|
||||
const { parts } = props;
|
||||
const intl = useIntl();
|
||||
if (!props.parts || !props.parts.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipOverlayScroller title={<FileParts fileList={parts}></FileParts>}>
|
||||
<ThemeTag
|
||||
opacity={0.7}
|
||||
className="tag-item"
|
||||
color="purple"
|
||||
style={{
|
||||
marginRight: 0
|
||||
}}
|
||||
>
|
||||
<span style={{ opacity: 1 }}>
|
||||
<InfoCircleOutlined className="m-r-5" />
|
||||
{intl.formatMessage(
|
||||
{ id: 'models.search.parts' },
|
||||
{ n: parts.length }
|
||||
)}
|
||||
</span>
|
||||
</ThemeTag>
|
||||
</TooltipOverlayScroller>
|
||||
);
|
||||
};
|
||||
|
||||
const ModelFileItem: React.FC<ModelFileItemProps> = (props) => {
|
||||
const { data: item, isEvaluating, active, handleSelectModelFile } = props;
|
||||
|
||||
const getModelQuantizationType = (item: any) => {
|
||||
let path = item.path;
|
||||
if (item?.parts?.length) {
|
||||
path = `${item.path}.gguf`;
|
||||
}
|
||||
const quanType = getFileType(path);
|
||||
if (quanType) {
|
||||
return (
|
||||
<ThemeTag
|
||||
opacity={0.7}
|
||||
className="tag-item"
|
||||
color="cyan"
|
||||
style={{
|
||||
marginRight: 0
|
||||
}}
|
||||
>
|
||||
{_.toUpper(quanType)}
|
||||
</ThemeTag>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('hf-model-file', {
|
||||
active: active
|
||||
})}
|
||||
onClick={() => handleSelectModelFile(item)}
|
||||
>
|
||||
<div className="title">{item.path}</div>
|
||||
<div className="tags flex-between">
|
||||
<span className="flex-center gap-8">
|
||||
<ThemeTag
|
||||
opacity={0.7}
|
||||
className="tag-item"
|
||||
color="green"
|
||||
style={{
|
||||
marginRight: 0
|
||||
}}
|
||||
>
|
||||
{convertFileSize(item.size)}
|
||||
</ThemeTag>
|
||||
{getModelQuantizationType(item)}
|
||||
<FilePartsTag parts={item.parts}></FilePartsTag>
|
||||
</span>
|
||||
<IncompatiableInfo
|
||||
isEvaluating={isEvaluating}
|
||||
data={item.evaluateResult}
|
||||
></IncompatiableInfo>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelFileItem;
|
||||
@@ -0,0 +1,98 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import hotkeys from '@/config/hotkeys';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Input } from 'antd';
|
||||
import React, { useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import styled from 'styled-components';
|
||||
import { modelSourceMap, modelSourceValueMap } from '../../config';
|
||||
|
||||
const SearchInputWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
const Holder = styled.div`
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
left: 34px;
|
||||
color: var(--ant-color-text-quaternary);
|
||||
font-size: var(--font-size-small);
|
||||
z-index: 10;
|
||||
kbd {
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: 3px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
`;
|
||||
const SearchInput: React.FC<{
|
||||
modelSource: string;
|
||||
onChange: (e: any) => void;
|
||||
onSearch: (e: any) => void;
|
||||
}> = (props) => {
|
||||
const { onSearch, onChange, modelSource } = props;
|
||||
const intl = useIntl();
|
||||
const inputRef = useRef<any>(null);
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
const [value, setValue] = React.useState('');
|
||||
|
||||
useHotkeys(hotkeys.FOCUS, (e: any) => {
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus?.();
|
||||
});
|
||||
|
||||
const handleOnChange = (e: any) => {
|
||||
setValue(e.target.value);
|
||||
onChange(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchInputWrapper>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onPressEnter={onSearch}
|
||||
onChange={handleOnChange}
|
||||
onFocus={(e) => {
|
||||
setIsFocused(true);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
setIsFocused(false);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
allowClear
|
||||
suffix={
|
||||
<SearchOutlined
|
||||
className="font-size-16"
|
||||
style={{ color: 'var(--ant-color-text-placeholder)' }}
|
||||
/>
|
||||
}
|
||||
prefix={
|
||||
<IconFont
|
||||
className="font-size-16"
|
||||
type={
|
||||
modelSource === modelSourceMap.huggingface_value
|
||||
? 'icon-huggingface'
|
||||
: 'icon-tu2'
|
||||
}
|
||||
></IconFont>
|
||||
}
|
||||
></Input>
|
||||
{!value && !isFocused && (
|
||||
<Holder
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: intl.formatMessage(
|
||||
{
|
||||
id: 'model.deploy.search.placeholder'
|
||||
},
|
||||
{ source: modelSourceValueMap[modelSource] }
|
||||
)
|
||||
}}
|
||||
></Holder>
|
||||
)}
|
||||
</SearchInputWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchInput;
|
||||
@@ -0,0 +1,610 @@
|
||||
import { getRequestId, setRquestId } from '@/atoms/models';
|
||||
import BaseSelect from '@/components/seal-form/base/select';
|
||||
import { createAxiosToken } from '@/hooks/use-chunk-request';
|
||||
import ColumnWrapper from '@/pages/_components/column-wrapper';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Pagination } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
evaluationsModelSpec,
|
||||
queryHuggingfaceModels,
|
||||
queryModelScopeModels
|
||||
} from '../../apis';
|
||||
import {
|
||||
ModelScopeSortType,
|
||||
ModelSortType,
|
||||
modelSourceMap
|
||||
} from '../../config';
|
||||
import { MessageStatus, WarningStausOptions } from '../../hooks';
|
||||
import useCheckBackend from '../../hooks/use-check-backend';
|
||||
import useRecognizeAudio from '../../hooks/use-recognize-audio';
|
||||
import SearchStyle from '../../style/search-result.less';
|
||||
import SearchInput from './search-input';
|
||||
import SearchResult from './search-result';
|
||||
|
||||
const filterOptions = [
|
||||
{ label: 'FP8', value: 'fp8' },
|
||||
{ label: 'AWQ', value: 'awq' },
|
||||
{ label: 'GPTQ', value: 'gptq' }
|
||||
];
|
||||
const PaginationMain = styled(Pagination)`
|
||||
.ant-pagination-slash {
|
||||
margin-inline: 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface SearchInputProps {
|
||||
hasLinuxWorker?: boolean;
|
||||
modelSource: string;
|
||||
isDownload?: boolean;
|
||||
gpuOptions?: any[];
|
||||
clusterId?: number;
|
||||
setLoadingModel?: (flag: boolean) => void;
|
||||
onSourceChange?: (source: string) => void;
|
||||
onSelectModel: (model: any, manul?: boolean) => void;
|
||||
onSelectModelAfterEvaluate?: (model: any, manual?: boolean) => void;
|
||||
displayEvaluateStatus?: (
|
||||
data: MessageStatus,
|
||||
options?: WarningStausOptions
|
||||
) => void;
|
||||
}
|
||||
|
||||
const SearchModel: React.FC<SearchInputProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
modelSource,
|
||||
isDownload,
|
||||
gpuOptions,
|
||||
clusterId,
|
||||
setLoadingModel,
|
||||
onSelectModel,
|
||||
onSelectModelAfterEvaluate,
|
||||
displayEvaluateStatus
|
||||
} = props;
|
||||
|
||||
const { recognizeAudioModel } = useRecognizeAudio();
|
||||
const { checkCurrentbackend } = useCheckBackend();
|
||||
const [dataSource, setDataSource] = useState<{
|
||||
dataList: any[];
|
||||
loading: boolean;
|
||||
networkError: boolean;
|
||||
sortType: string;
|
||||
filters: Record<string, any>;
|
||||
}>({
|
||||
dataList: [],
|
||||
loading: false,
|
||||
networkError: false,
|
||||
sortType: ModelSortType.trendingScore,
|
||||
filters: {
|
||||
tag: null
|
||||
}
|
||||
});
|
||||
const SUPPORTEDSOURCE = [
|
||||
modelSourceMap.huggingface_value,
|
||||
modelSourceMap.modelscope_value
|
||||
];
|
||||
|
||||
const [isEvaluating, setIsEvaluating] = useState<boolean>(false);
|
||||
const [current, setCurrent] = useState<string>('');
|
||||
const currentRef = useRef<string>('');
|
||||
const cacheRepoOptions = useRef<any[]>([]);
|
||||
const axiosTokenRef = useRef<any>(null);
|
||||
const checkTokenRef = useRef<any>(null);
|
||||
const searchInputRef = useRef<any>('');
|
||||
const timer = useRef<any>(null);
|
||||
const requestIdRef = useRef<number>(0);
|
||||
const searchRepoRequestIdRef = useRef<number>(0);
|
||||
const [paginationInfo, setPaginationInfo] = useState({
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 0
|
||||
});
|
||||
const modelFilesSortOptions = [
|
||||
{
|
||||
label: intl.formatMessage({ id: 'models.sort.trending' }),
|
||||
value: ModelSortType.trendingScore
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'models.sort.likes' }),
|
||||
value: ModelSortType.likes
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'models.sort.downloads' }),
|
||||
value: ModelSortType.downloads
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'models.sort.updated' }),
|
||||
value: ModelSortType.lastModified
|
||||
}
|
||||
];
|
||||
|
||||
const updateSearchRepoRequestId = () => {
|
||||
searchRepoRequestIdRef.current += 1;
|
||||
return searchRepoRequestIdRef.current;
|
||||
};
|
||||
|
||||
const updateRequestId = () => {
|
||||
requestIdRef.current += 1;
|
||||
return requestIdRef.current;
|
||||
};
|
||||
|
||||
const checkIsGGUF = (item: any) => {
|
||||
const isGGUF = _.some(item.tags, (tag: string) => {
|
||||
return tag.toLowerCase() === 'gguf';
|
||||
});
|
||||
const isGGUFFromMs = _.some(item.libraries, (tag: string) => {
|
||||
return tag.toLowerCase() === 'gguf';
|
||||
});
|
||||
return isGGUF || isGGUFFromMs;
|
||||
};
|
||||
|
||||
const handleOnSelectModel = (model: any, manual?: boolean) => {
|
||||
const item = model || {};
|
||||
// because need cancel the fetch file request when select another model, so check the empty model in the parent level handler.
|
||||
if (!item.evaluated || item.isGGUF) {
|
||||
onSelectModel(item, manual);
|
||||
} else {
|
||||
onSelectModelAfterEvaluate?.(item, manual);
|
||||
}
|
||||
setCurrent(item.id);
|
||||
currentRef.current = item.id;
|
||||
};
|
||||
|
||||
// huggeface
|
||||
const getModelsFromHuggingface = async (query: {
|
||||
sort: string;
|
||||
filters?: Record<string, any>;
|
||||
}) => {
|
||||
const currentSearchId = setRquestId();
|
||||
const task: any = searchInputRef.current ? '' : 'text-generation';
|
||||
const params = {
|
||||
search: {
|
||||
query: searchInputRef.current || '',
|
||||
sort: query.sort,
|
||||
tags: query.filters?.tag ? [query.filters.tag] : [],
|
||||
task: task
|
||||
}
|
||||
};
|
||||
const data = await queryHuggingfaceModels(params, {
|
||||
signal: axiosTokenRef.current.signal
|
||||
});
|
||||
if (getRequestId() !== currentSearchId) {
|
||||
throw 'new request has been sent';
|
||||
}
|
||||
let list = _.map(data || [], (item: any) => {
|
||||
return {
|
||||
...item,
|
||||
value: item.name,
|
||||
label: item.name,
|
||||
isGGUF: checkIsGGUF(item),
|
||||
source: modelSource
|
||||
};
|
||||
});
|
||||
return list;
|
||||
};
|
||||
|
||||
// modelscope, only modelscope has page and perPage
|
||||
const getModelsFromModelscope = async (queryParams: {
|
||||
sortType: string;
|
||||
page: number;
|
||||
perPage?: number;
|
||||
filters?: Record<string, any>;
|
||||
}) => {
|
||||
const currentSearchId = setRquestId();
|
||||
try {
|
||||
const params = {
|
||||
Name: `${searchInputRef.current}`,
|
||||
tags: queryParams.filters?.tag ? [queryParams.filters.tag] : [],
|
||||
SortBy: ModelScopeSortType[queryParams.sortType],
|
||||
PageNumber: queryParams.page,
|
||||
PageSize: queryParams.perPage,
|
||||
tasks: []
|
||||
};
|
||||
const data = await queryModelScopeModels(params, {
|
||||
signal: axiosTokenRef.current.signal
|
||||
});
|
||||
|
||||
if (getRequestId() !== currentSearchId) {
|
||||
throw 'new request has been sent';
|
||||
}
|
||||
|
||||
let list = _.map(_.get(data, 'Data.Model.Models') || [], (item: any) => {
|
||||
return {
|
||||
path: item.Path,
|
||||
name: `${item.Path}/${item.Name}`,
|
||||
downloads: item.Downloads,
|
||||
id: `${item.Path}/${item.Name}`,
|
||||
updatedAt: item.LastUpdatedTime * 1000,
|
||||
likes: item.Stars,
|
||||
value: item.Name,
|
||||
label: item.Name,
|
||||
revision: item.Revision,
|
||||
task: item.Tasks?.map((sItem: any) => sItem.Name).join(','),
|
||||
tags: item.Tags,
|
||||
libraries: item.Libraries,
|
||||
avatar: item.Avatar,
|
||||
isGGUF: checkIsGGUF({
|
||||
tags: item.Tags,
|
||||
libraries: item.Libraries
|
||||
}),
|
||||
source: modelSource
|
||||
};
|
||||
});
|
||||
|
||||
setPaginationInfo((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
page: queryParams.page,
|
||||
total: _.get(data, 'Data.Model.TotalCount', 0)
|
||||
};
|
||||
});
|
||||
return list;
|
||||
} catch (error) {
|
||||
setPaginationInfo((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
page: queryParams.page
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getEvaluateResults = async (repoList: any[]) => {
|
||||
checkTokenRef.current?.cancel?.();
|
||||
checkTokenRef.current = createAxiosToken();
|
||||
const evaluations = await evaluationsModelSpec(
|
||||
{
|
||||
cluster_id: clusterId!,
|
||||
model_specs: repoList
|
||||
},
|
||||
{
|
||||
token: checkTokenRef.current?.token
|
||||
}
|
||||
);
|
||||
return evaluations.results;
|
||||
};
|
||||
|
||||
const handleEvaluate = async (list: any[]) => {
|
||||
if (isDownload) {
|
||||
return;
|
||||
}
|
||||
const currentRequestId = updateRequestId();
|
||||
const currentSearchId = getRequestId();
|
||||
try {
|
||||
const repoList = list.map((item) => {
|
||||
const res = recognizeAudioModel(item, modelSource);
|
||||
|
||||
let backendObj = {};
|
||||
const backend = checkCurrentbackend({
|
||||
isGGUF: item.isGGUF,
|
||||
isAudio: res.isAudio,
|
||||
gpuOptions: gpuOptions || []
|
||||
});
|
||||
|
||||
if (backend) {
|
||||
backendObj = {
|
||||
backend: backend
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...backendObj,
|
||||
cluster_id: clusterId,
|
||||
source: modelSource,
|
||||
...(modelSource === modelSourceMap.huggingface_value
|
||||
? {
|
||||
huggingface_repo_id: item.name
|
||||
}
|
||||
: {
|
||||
model_scope_model_id: item.name
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
setIsEvaluating(true);
|
||||
const evaluations = await getEvaluateResults(repoList);
|
||||
// bind the requestId to the current request and searchId
|
||||
if (
|
||||
requestIdRef.current !== currentRequestId &&
|
||||
currentSearchId !== getRequestId()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const resultList = list.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
evaluated: true,
|
||||
evaluateResult: evaluations[index] || null
|
||||
};
|
||||
});
|
||||
setIsEvaluating(false);
|
||||
|
||||
setDataSource((pre) => {
|
||||
return {
|
||||
...pre,
|
||||
loading: false,
|
||||
dataList: resultList
|
||||
};
|
||||
});
|
||||
// current selected item
|
||||
const currentItem = resultList.find(
|
||||
(item) => item.id === currentRef.current
|
||||
);
|
||||
|
||||
// if it is gguf, would trigger a evaluation after select a model file
|
||||
if (currentItem && !currentItem.isGGUF) {
|
||||
onSelectModelAfterEvaluate?.(currentItem);
|
||||
}
|
||||
} catch (error) {
|
||||
// cancel the corrponding request
|
||||
if (requestIdRef.current === currentRequestId) {
|
||||
setIsEvaluating(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentPage = (page: number) => {
|
||||
const start = (page - 1) * paginationInfo.perPage;
|
||||
const end = start + paginationInfo.perPage;
|
||||
return cacheRepoOptions.current.slice(start, end);
|
||||
};
|
||||
|
||||
const handleOnSearchRepo = async (params: {
|
||||
sortType: string;
|
||||
page: number;
|
||||
perPage: number;
|
||||
filters: Record<string, any>;
|
||||
}) => {
|
||||
if (!SUPPORTEDSOURCE.includes(modelSource)) {
|
||||
return;
|
||||
}
|
||||
const currentSearchId = updateSearchRepoRequestId();
|
||||
axiosTokenRef.current?.abort?.('cancel previous request');
|
||||
axiosTokenRef.current = new AbortController();
|
||||
checkTokenRef.current?.cancel?.();
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
try {
|
||||
setDataSource((pre) => {
|
||||
pre.loading = true;
|
||||
return { ...pre };
|
||||
});
|
||||
setLoadingModel?.(true);
|
||||
cacheRepoOptions.current = [];
|
||||
let list: any[] = [];
|
||||
if (modelSource === modelSourceMap.huggingface_value) {
|
||||
const resultList = await getModelsFromHuggingface({
|
||||
sort: params.sortType,
|
||||
filters: params.filters
|
||||
});
|
||||
|
||||
cacheRepoOptions.current = resultList;
|
||||
|
||||
// hf has no page and perPage, so we need to slice the resultList
|
||||
list = getCurrentPage(params.page);
|
||||
setPaginationInfo((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
page: params.page,
|
||||
total: resultList.length
|
||||
};
|
||||
});
|
||||
} else if (modelSource === modelSourceMap.modelscope_value) {
|
||||
list = await getModelsFromModelscope(params);
|
||||
console.log('list:', list);
|
||||
cacheRepoOptions.current = list;
|
||||
}
|
||||
|
||||
setDataSource({
|
||||
dataList: list,
|
||||
loading: false,
|
||||
networkError: false,
|
||||
sortType: params.sortType,
|
||||
filters: params.filters
|
||||
});
|
||||
|
||||
handleOnSelectModel(list[0]);
|
||||
setLoadingModel?.(false);
|
||||
|
||||
handleEvaluate(list);
|
||||
} catch (error: any) {
|
||||
console.log('error:', error);
|
||||
setDataSource({
|
||||
dataList: [],
|
||||
loading: currentSearchId !== searchRepoRequestIdRef.current,
|
||||
sortType: params.sortType,
|
||||
filters: params.filters,
|
||||
networkError: error?.message === 'Failed to fetch'
|
||||
});
|
||||
|
||||
setLoadingModel?.(currentSearchId !== searchRepoRequestIdRef.current);
|
||||
displayEvaluateStatus?.({
|
||||
show: false,
|
||||
message: ''
|
||||
});
|
||||
handleOnSelectModel({});
|
||||
cacheRepoOptions.current = [];
|
||||
}
|
||||
};
|
||||
const handleSearchInputChange = (e: any) => {
|
||||
searchInputRef.current = e.target.value;
|
||||
};
|
||||
const handlerSearchModels = _.debounce(
|
||||
() =>
|
||||
handleOnSearchRepo({
|
||||
sortType: dataSource.sortType,
|
||||
page: 1,
|
||||
perPage: paginationInfo.perPage,
|
||||
filters: dataSource.filters
|
||||
}),
|
||||
100
|
||||
);
|
||||
|
||||
const handleOnOpen = () => {
|
||||
if (
|
||||
!dataSource.dataList.length &&
|
||||
!cacheRepoOptions.current.length &&
|
||||
SUPPORTEDSOURCE.includes(modelSource)
|
||||
) {
|
||||
handleOnSearchRepo({
|
||||
sortType: dataSource.sortType,
|
||||
page: 1,
|
||||
perPage: paginationInfo.perPage,
|
||||
filters: dataSource.filters
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSortChange = (value: string) => {
|
||||
handleOnSearchRepo({
|
||||
sortType: value,
|
||||
page: 1,
|
||||
perPage: paginationInfo.perPage,
|
||||
filters: dataSource.filters
|
||||
});
|
||||
};
|
||||
|
||||
const handleFilterChange = (value: string) => {
|
||||
handleOnSearchRepo({
|
||||
sortType: dataSource.sortType,
|
||||
page: 1,
|
||||
perPage: paginationInfo.perPage,
|
||||
filters: {
|
||||
...dataSource.filters,
|
||||
tag: value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnPageChange = (page: number) => {
|
||||
if (modelSource === modelSourceMap.huggingface_value) {
|
||||
const currentList = getCurrentPage(page);
|
||||
setPaginationInfo((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
page: page
|
||||
};
|
||||
});
|
||||
setDataSource((pre) => {
|
||||
return {
|
||||
...pre,
|
||||
dataList: currentList
|
||||
};
|
||||
});
|
||||
handleOnSelectModel(currentList[0]);
|
||||
handleEvaluate(currentList);
|
||||
} else if (modelSource === modelSourceMap.modelscope_value) {
|
||||
setPaginationInfo((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
page: page
|
||||
};
|
||||
});
|
||||
handleOnSearchRepo({
|
||||
sortType: dataSource.sortType,
|
||||
page: page,
|
||||
perPage: paginationInfo.perPage,
|
||||
filters: dataSource.filters
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectModelManually = (model: any) => {
|
||||
if (model.id === currentRef.current) {
|
||||
return;
|
||||
}
|
||||
setRquestId();
|
||||
handleOnSelectModel(model, true);
|
||||
};
|
||||
|
||||
const renderHFSearch = () => {
|
||||
return (
|
||||
<>
|
||||
<SearchInput
|
||||
onSearch={handlerSearchModels}
|
||||
onChange={handleSearchInputChange}
|
||||
modelSource={modelSource}
|
||||
></SearchInput>
|
||||
<div className={SearchStyle.filter}>
|
||||
<span className="flex-center gap-8">
|
||||
<BaseSelect
|
||||
value={dataSource.sortType}
|
||||
onChange={handleSortChange}
|
||||
prefix={
|
||||
<span>{intl.formatMessage({ id: 'model.deploy.sort' })}:</span>
|
||||
}
|
||||
options={modelFilesSortOptions}
|
||||
size="middle"
|
||||
style={{ width: '150px' }}
|
||||
></BaseSelect>
|
||||
<BaseSelect
|
||||
allowClear
|
||||
value={dataSource.filters.tag}
|
||||
onChange={handleFilterChange}
|
||||
options={filterOptions}
|
||||
size="middle"
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'models.form.quantization'
|
||||
})}
|
||||
style={{ width: 130 }}
|
||||
></BaseSelect>
|
||||
</span>
|
||||
<PaginationMain
|
||||
simple={{ readOnly: true }}
|
||||
total={paginationInfo.total}
|
||||
current={paginationInfo.page}
|
||||
pageSize={paginationInfo.perPage}
|
||||
onChange={handleOnPageChange}
|
||||
showSizeChanger={false}
|
||||
hideOnSinglePage={paginationInfo.total <= paginationInfo.perPage}
|
||||
></PaginationMain>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleOnOpen();
|
||||
}, [modelSource]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
axiosTokenRef.current?.abort?.();
|
||||
checkTokenRef.current?.cancel?.();
|
||||
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<div className={SearchStyle['search-bar']}>{renderHFSearch()}</div>
|
||||
<ColumnWrapper
|
||||
maxHeight={'calc(100vh - 210px)'}
|
||||
styles={{
|
||||
container: {
|
||||
paddingTop: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SearchResult
|
||||
loading={dataSource.loading}
|
||||
resultList={dataSource.dataList}
|
||||
networkError={dataSource.networkError}
|
||||
current={current}
|
||||
source={modelSource}
|
||||
isEvaluating={isEvaluating}
|
||||
onSelect={handleSelectModelManually}
|
||||
></SearchResult>
|
||||
</ColumnWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchModel;
|
||||
@@ -0,0 +1,173 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Empty, Spin } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import 'simplebar-react/dist/simplebar.min.css';
|
||||
import styled from 'styled-components';
|
||||
import { modelSourceMap } from '../../config';
|
||||
import '../../style/search-result.less';
|
||||
import FileSkeleton from './file-skeleton';
|
||||
import HFModelItem from './hf-model-item';
|
||||
|
||||
interface SearchResultProps {
|
||||
resultList: any[];
|
||||
onSelect?: (item: any) => void;
|
||||
current?: string;
|
||||
source?: string;
|
||||
style?: React.CSSProperties;
|
||||
loading?: boolean;
|
||||
networkError?: boolean;
|
||||
isEvaluating?: boolean;
|
||||
}
|
||||
|
||||
const ItemFileWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 24px;
|
||||
`;
|
||||
|
||||
const SpinWrapper = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const SearchResult: React.FC<SearchResultProps> = (props) => {
|
||||
const { resultList, onSelect, isEvaluating, source, networkError } = props;
|
||||
const intl = useIntl();
|
||||
|
||||
const handleSelect = (e: any, item: any) => {
|
||||
e.stopPropagation();
|
||||
onSelect?.(item);
|
||||
};
|
||||
const handleOnEnter = (e: any, item: any) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter') {
|
||||
onSelect?.(item);
|
||||
}
|
||||
};
|
||||
|
||||
const renderEmpty = useMemo(() => {
|
||||
if (networkError) {
|
||||
return (
|
||||
<Empty
|
||||
styles={{
|
||||
image: {
|
||||
height: 'auto',
|
||||
marginTop: '20px'
|
||||
}
|
||||
}}
|
||||
image={
|
||||
<IconFont
|
||||
type="icon-networkerror"
|
||||
style={{
|
||||
color: 'var(--ant-color-text-tertiary)',
|
||||
fontSize: '66px'
|
||||
}}
|
||||
></IconFont>
|
||||
}
|
||||
description={
|
||||
source === modelSourceMap.huggingface_value ? (
|
||||
<div className="flex-column gap-5">
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'models.search.networkerror' })}
|
||||
</span>
|
||||
<span>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'models.search.hfvisit' })}
|
||||
</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
href="https://huggingface.co/"
|
||||
target="_blank"
|
||||
>
|
||||
Hugging Face
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Empty
|
||||
styles={{
|
||||
image: {
|
||||
height: 'auto',
|
||||
marginTop: '20px'
|
||||
}
|
||||
}}
|
||||
image={
|
||||
<SearchOutlined
|
||||
className="font-size-16"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
></SearchOutlined>
|
||||
}
|
||||
description={intl.formatMessage({ id: 'models.search.noresult' })}
|
||||
/>
|
||||
);
|
||||
}, [networkError, source, intl]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ ...props.style }} className="search-result-wrap">
|
||||
<Spin spinning={props.loading} size={'middle'}>
|
||||
<div style={{ minHeight: 200 }}>
|
||||
{resultList.length ? (
|
||||
<ItemFileWrapper>
|
||||
{resultList.map((item, index) => (
|
||||
<div
|
||||
key={item.name}
|
||||
onClick={(e) => handleSelect(e, item)}
|
||||
onKeyDown={(e) => handleOnEnter(e, item)}
|
||||
>
|
||||
<HFModelItem
|
||||
source={source}
|
||||
tags={item.tags}
|
||||
key={index}
|
||||
title={item.name}
|
||||
downloads={item.downloads}
|
||||
likes={item.likes}
|
||||
task={item.task}
|
||||
updatedAt={item.updatedAt}
|
||||
evaluateResult={item.evaluateResult}
|
||||
active={item.id === props.current}
|
||||
isEvaluating={isEvaluating}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</ItemFileWrapper>
|
||||
) : props.loading ? (
|
||||
<ItemFileWrapper>
|
||||
{_.times(10, (index: number) => {
|
||||
return (
|
||||
<FileSkeleton
|
||||
key={index}
|
||||
counts={3}
|
||||
itemHeight={82}
|
||||
></FileSkeleton>
|
||||
);
|
||||
})}
|
||||
</ItemFileWrapper>
|
||||
) : (
|
||||
renderEmpty
|
||||
)}
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchResult;
|
||||
Reference in New Issue
Block a user