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 = (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; }>({ dataList: [], loading: false, networkError: false, sortType: ModelSortType.trendingScore, filters: { tag: null } }); const SUPPORTEDSOURCE = [ modelSourceMap.huggingface_value, modelSourceMap.modelscope_value ]; const [isEvaluating, setIsEvaluating] = useState(false); const [current, setCurrent] = useState(''); const currentRef = useRef(''); const cacheRepoOptions = useRef([]); const axiosTokenRef = useRef(null); const checkTokenRef = useRef(null); const searchInputRef = useRef(''); const timer = useRef(null); const requestIdRef = useRef(0); const searchRepoRequestIdRef = useRef(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; }) => { 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; }) => { 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; }) => { 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 ( <>
{intl.formatMessage({ id: 'model.deploy.sort' })}: } options={modelFilesSortOptions} size="middle" style={{ width: '150px' }} >
); }; useEffect(() => { handleOnOpen(); }, [modelSource]); useEffect(() => { return () => { axiosTokenRef.current?.abort?.(); checkTokenRef.current?.cancel?.(); if (timer.current) { clearTimeout(timer.current); } }; }, []); return (
{renderHFSearch()}
); }; export default SearchModel;