601 lines
16 KiB
TypeScript
601 lines
16 KiB
TypeScript
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: 'AWQ', value: 'awq' },
|
|
{ label: 'GPTQ', value: 'gptq' }
|
|
// { label: 'GGUF', value: 'gguf' }
|
|
];
|
|
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 || {};
|
|
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(
|
|
{
|
|
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,
|
|
repoOptions: 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="gguf-tips">
|
|
<span>
|
|
{intl.formatMessage({ id: 'models.form.search.gguftips' })}
|
|
</span>
|
|
</div> */}
|
|
<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: 'common.input.type' })}
|
|
style={{ width: 150 }}
|
|
></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)'}>
|
|
<SearchResult
|
|
loading={dataSource.loading}
|
|
resultList={dataSource.dataList}
|
|
networkError={dataSource.networkError}
|
|
current={current}
|
|
source={modelSource}
|
|
isEvaluating={isEvaluating}
|
|
onSelect={handleSelectModelManually}
|
|
></SearchResult>
|
|
</ColumnWrapper>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SearchModel;
|