diff --git a/package.json b/package.json index a92d6d2b..e729789d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@ant-design/icons": "^5.3.7", "@ant-design/pro-components": "^2.7.1", + "@huggingface/gguf": "^0.1.7", "@huggingface/hub": "^0.15.1", "@monaco-editor/react": "^4.6.0", "@types/lodash": "^4.17.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95c2731e..236137ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@ant-design/pro-components': specifier: ^2.7.1 version: 2.7.1(antd@5.18.3)(rc-field-form@1.44.0)(react-dom@18.2.0)(react@18.2.0) + '@huggingface/gguf': + specifier: ^0.1.7 + version: 0.1.7 '@huggingface/hub': specifier: ^0.15.1 version: 0.15.1 @@ -4030,6 +4033,11 @@ packages: deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package dev: false + /@huggingface/gguf@0.1.7: + resolution: {integrity: sha512-RQN1WwuusLjiBTNFuAJCUlhRejIhKt395ywnTmc+Jy8dajGwk8k7EsfgtxVqkBSTWy9D55XzCII7jUjkHEv3JA==, tarball: https://registry.npmjs.org/@huggingface/gguf/-/gguf-0.1.7.tgz} + engines: {node: '>=20'} + dev: false + /@huggingface/hub@0.15.1: resolution: {integrity: sha512-uHb4aFkJDoGfLeRHfFTjkI36Z8IV6Z1c+KzhMDqUSC56opyr7Mn1Nsx7Rri/C7KDwROhQfBp/fOOqqjTzn6Cgg==, tarball: https://registry.npmjs.org/@huggingface/hub/-/hub-0.15.1.tgz} engines: {node: '>=18'} diff --git a/src/components/modal-footer/index.tsx b/src/components/modal-footer/index.tsx index e3722dad..cac3dbcc 100644 --- a/src/components/modal-footer/index.tsx +++ b/src/components/modal-footer/index.tsx @@ -9,8 +9,10 @@ type ModalFooterProps = { htmlType?: 'button' | 'submit'; okBtnProps?: any; cancelBtnProps?: any; + align?: 'start' | 'end' | 'center' | 'baseline'; form?: any; loading?: boolean; + style?: React.CSSProperties; }; const ModalFooter: React.FC = ({ onOk, @@ -21,11 +23,13 @@ const ModalFooter: React.FC = ({ cancelBtnProps, loading, htmlType = 'button', + align = 'end', + style, form }) => { const intl = useIntl(); return ( - + diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index 42618af4..34d391fe 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -1,5 +1,5 @@ export default { - 'models.button.deploy': 'Deploy Model', + 'models.button.deploy': 'Deploy Model From', 'models.title': 'Models', 'models.title.edit': 'Edit Model', 'models.table.models': 'models', diff --git a/src/pages/llmodels/apis/index.ts b/src/pages/llmodels/apis/index.ts index 7b3ddf00..c64cc0e1 100644 --- a/src/pages/llmodels/apis/index.ts +++ b/src/pages/llmodels/apis/index.ts @@ -116,6 +116,14 @@ export async function callHuggingfaceQuickSearch(params: any) { }); } +const HUGGINGFACE_API = 'https://huggingface.co/api/models'; + +export async function queryHuggingfaceModelDetail(params: { repo: string }) { + return request(`${HUGGINGFACE_API}/${params.repo}`, { + method: 'GET' + }); +} + export async function queryHuggingfaceModels( params: { search: { @@ -130,7 +138,18 @@ export async function queryHuggingfaceModels( ...params, ...options, limit: 50, - additionalFields: ['cardData'] + additionalFields: ['sha'], + fetch(url: string, config: any) { + try { + return fetch(`${url}`, { + ...config, + signal: options.signal + }); + } catch (error) { + // ignore + return []; + } + } })) { result.push(model); } @@ -139,7 +158,9 @@ export async function queryHuggingfaceModels( export async function queryHuggingfaceModelFiles(params: { repo: string }) { const result = []; - for await (const fileInfo of listFiles(params)) { + for await (const fileInfo of listFiles({ + ...params + })) { result.push(fileInfo); } return result; diff --git a/src/pages/llmodels/components/add-modal.tsx b/src/pages/llmodels/components/add-modal.tsx index f6a28860..109c2b57 100644 --- a/src/pages/llmodels/components/add-modal.tsx +++ b/src/pages/llmodels/components/add-modal.tsx @@ -5,15 +5,13 @@ import SealSelect from '@/components/seal-form/seal-select'; import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import { convertFileSize } from '@/utils'; -import { SearchOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Form, Input, Modal } from 'antd'; +import { Form, Modal } from 'antd'; import _ from 'lodash'; import { memo, useCallback, useEffect, useState } from 'react'; import { queryHuggingfaceModelFiles, queryHuggingfaceModels } from '../apis'; import { modelSourceMap } from '../config'; import { FormData, ListItem } from '../config/types'; -import SearchInput from './search-input'; type AddModalProps = { title: string; @@ -50,7 +48,6 @@ const AddModal: React.FC = (props) => { const [fileOptions, setFileOptions] = useState< { label: string; value: string }[] >([]); - const [ollamaTags, setOllamaTags] = useState([]); const initFormValue = () => { if (action === PageAction.CREATE && open) { @@ -154,24 +151,11 @@ const AddModal: React.FC = (props) => { } ]} > - - - - } disabled={true} - description={intl.formatMessage({ id: 'models.form.repoid.desc' })} - > - - + > name="huggingface_filename" @@ -246,14 +230,6 @@ const AddModal: React.FC = (props) => { } ]} > - {/* */} = (props) => { required > - - - ); }; @@ -286,7 +256,6 @@ const AddModal: React.FC = (props) => { const handleOnSelectModel = useCallback((item: any) => { const repo = item.name; - setOllamaTags(_.map(item.tags, (tag: string) => _.toLower(tag))); if (form.getFieldValue('source') === modelSourceMap.huggingface_value) { form.setFieldValue('huggingface_repo_id', repo); handleFetchModelFiles(repo); @@ -320,13 +289,6 @@ const AddModal: React.FC = (props) => { } > - {action === PageAction.CREATE && ( - - )}
name="name" @@ -362,7 +324,6 @@ const AddModal: React.FC = (props) => { ) } ]} - noStyle={action === PageAction.CREATE} > {action === PageAction.EDIT && ( void; + onCancel: () => void; +}; + +const sourceOptions = [ + { + label: 'Hugging Face', + value: modelSourceMap.huggingface_value, + key: 'huggingface' + }, + { + label: 'Ollama Library', + value: modelSourceMap.ollama_library_value, + key: 'ollama_library' + } +]; + +const AddModal: React.FC = (props) => { + console.log('addmodel===='); + const { title, action, open, source, onOk, onCancel } = props || {}; + const [form] = Form.useForm(); + const intl = useIntl(); + const modelSource = Form.useWatch('source', form); + const huggingfaceRepoId = Form.useWatch('huggingface_repo_id', form); + const [loading, setLoading] = useState(false); + const [repoOptions, setRepoOptions] = useState< + { label: string; value: string }[] + >([]); + const [fileOptions, setFileOptions] = useState< + { label: string; value: string }[] + >([]); + + const initFormValue = () => { + form.setFieldsValue({ + source: props.source, + replicas: 1 + }); + }; + + useEffect(() => { + initFormValue(); + console.log('source========', props.source); + }, [open]); + + const fileNamLabel = (item: any) => { + return ( + + {item.path} + + ({convertFileSize(item.size)}) + + + ); + }; + const handleFetchModelFiles = async (repo: string) => { + try { + setLoading(true); + const res = await queryHuggingfaceModelFiles({ repo }); + const list = _.filter(res, (file: any) => { + return _.endsWith(file.path, '.gguf'); + }).map((item: any) => { + return { + label: fileNamLabel(item), + value: item.path, + size: item.size + }; + }); + setFileOptions(list); + setLoading(false); + } catch (error) { + setFileOptions([]); + setLoading(false); + } + }; + + const handleRepoOnBlur = (e: any) => { + const repo = form.getFieldValue('huggingface_repo_id'); + handleFetchModelFiles(repo); + }; + + const handleSelectModelFile = useCallback((item: any) => { + form.setFieldValue('huggingface_filename', item.path); + }, []); + + const handleOnSearchRepo = async (text: string) => { + try { + const params = { + search: { + query: text, + tags: ['gguf'] + } + }; + const models = await queryHuggingfaceModels(params); + const list = _.map(models || [], (item: any) => { + return { + ...item, + value: item.name, + label: item.name + }; + }); + setRepoOptions(list); + } catch (error) { + setRepoOptions([]); + } + }; + + const debounceSearch = _.debounce((text: string) => { + handleOnSearchRepo(text); + }, 300); + + const renderHuggingfaceFields = () => { + return ( + <> + + name="huggingface_repo_id" + rules={[ + { + required: true, + message: intl.formatMessage( + { + id: 'common.form.rule.input' + }, + { name: intl.formatMessage({ id: 'models.form.repoid' }) } + ) + } + ]} + > + + + + name="huggingface_filename" + rules={[ + { + required: true, + message: intl.formatMessage( + { + id: 'common.form.rule.input' + }, + { name: intl.formatMessage({ id: 'models.form.filename' }) } + ) + } + ]} + > + + + + ); + }; + + const renderS3Fields = () => { + return ( + <> + + name="s3_address" + rules={[ + { + required: true, + message: intl.formatMessage( + { + id: 'common.form.rule.input' + }, + { name: intl.formatMessage({ id: 'models.form.s3address' }) } + ) + } + ]} + > + + + + ); + }; + + const renderOllamaModelFields = () => { + return ( + <> + + name="ollama_library_model_name" + rules={[ + { + required: true, + message: intl.formatMessage( + { + id: 'common.form.rule.input' + }, + { name: intl.formatMessage({ id: 'models.table.name' }) } + ) + } + ]} + > + + + + ); + }; + + const renderFieldsBySource = () => { + switch (props.source) { + case modelSourceMap.huggingface_value: + return renderHuggingfaceFields(); + case modelSourceMap.ollama_library_value: + return renderOllamaModelFields(); + case modelSourceMap.s3_value: + return renderS3Fields(); + default: + return null; + } + }; + + const handleOnSelectModel = useCallback((item: any) => { + const repo = item.name; + if (form.getFieldValue('source') === modelSourceMap.huggingface_value) { + form.setFieldValue('huggingface_repo_id', repo); + } else { + form.setFieldValue('ollama_library_model_name', repo); + } + }, []); + + const handleSumit = () => { + form.submit(); + }; + + return ( + + } + > +
+
+ +
+ + +
+ +
+ +
+

+ Configuration +

+ + + name="name" + rules={[ + { + required: true, + message: intl.formatMessage( + { + id: 'common.form.rule.input' + }, + { name: intl.formatMessage({ id: 'common.table.name' }) } + ) + } + ]} + > + + + + name="source" + rules={[ + { + required: true, + message: intl.formatMessage( + { + id: 'common.form.rule.select' + }, + { name: intl.formatMessage({ id: 'models.form.source' }) } + ) + } + ]} + > + { + + } + + {renderFieldsBySource()} + + name="replicas" + rules={[ + { + required: true, + message: intl.formatMessage( + { + id: 'common.form.rule.input' + }, + { name: intl.formatMessage({ id: 'models.form.replicas' }) } + ) + } + ]} + > + + + name="description"> + + + +
+
+
+ ); +}; + +export default memo(AddModal); diff --git a/src/pages/llmodels/components/hf-model-file.tsx b/src/pages/llmodels/components/hf-model-file.tsx new file mode 100644 index 00000000..6b435bdd --- /dev/null +++ b/src/pages/llmodels/components/hf-model-file.tsx @@ -0,0 +1,101 @@ +import { convertFileSize } from '@/utils'; +import { SearchOutlined } from '@ant-design/icons'; +import { Button, Col, Empty, Row, Space, Spin } from 'antd'; +import classNames from 'classnames'; +import _ from 'lodash'; +import { useEffect, useState } from 'react'; +import { queryHuggingfaceModelFiles } from '../apis'; +import '../style/hf-model-file.less'; + +interface HFModelFileProps { + repo: string; + onSelectFile?: (file: any) => void; +} +const HFModelFile: React.FC = (props) => { + const [dataSource, setDataSource] = useState({ + fileList: [], + loading: false + }); + + const [current, setCurrent] = useState(''); + + const handleSelectModelFile = (item: any) => { + props.onSelectFile?.(item); + setCurrent(item.path); + }; + + const handleFetchModelFiles = async () => { + if (!props.repo) { + setDataSource({ fileList: [], loading: false }); + return; + } + setDataSource({ ...dataSource, loading: true }); + setCurrent(''); + try { + const res = await queryHuggingfaceModelFiles({ repo: props.repo }); + const list = _.filter(res, (file: any) => { + return _.endsWith(file.path, '.gguf'); + }); + setDataSource({ fileList: list, loading: false }); + handleSelectModelFile(list[0]); + } catch (error) { + setDataSource({ fileList: [], loading: false }); + handleSelectModelFile({}); + } + }; + + useEffect(() => { + handleFetchModelFiles(); + }, [props.repo]); + return ( +
+

+ Available Files({dataSource.fileList.length || 0}) +

+
+ + {dataSource.fileList.length ? ( + + {_.map(dataSource.fileList, (item: any) => { + return ( + +
handleSelectModelFile(item)} + > +
{item.path}
+ + + {convertFileSize(item.size)} + + +
+ +
+
+ + ); + })} +
+ ) : ( + !dataSource.loading && ( + + } + description="No models found" + /> + ) + )} +
+
+ ); +}; + +export default HFModelFile; diff --git a/src/pages/llmodels/components/search-input.tsx b/src/pages/llmodels/components/search-model.tsx similarity index 87% rename from src/pages/llmodels/components/search-input.tsx rename to src/pages/llmodels/components/search-model.tsx index 5694d169..5da60cab 100644 --- a/src/pages/llmodels/components/search-input.tsx +++ b/src/pages/llmodels/components/search-model.tsx @@ -1,9 +1,8 @@ import IconFont from '@/components/icon-font'; -import RadioButtons from '@/components/radio-buttons'; import { SearchOutlined } from '@ant-design/icons'; import { Input } from 'antd'; import _ from 'lodash'; -import React, { useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { queryHuggingfaceModels } from '../apis'; import { modelSourceMap, ollamaModelOptions } from '../config'; import SearchStyle from '../style/search-result.less'; @@ -11,7 +10,7 @@ import SearchResult from './search-result'; interface SearchInputProps { modelSource: string; - onSourceChange: (source: string) => void; + onSourceChange?: (source: string) => void; onSelectModel: (model: any) => void; } @@ -37,6 +36,12 @@ const SearchInput: React.FC = (props) => { const [loading, setLoading] = useState(false); const cacheRepoOptions = useRef([]); const axiosTokenRef = useRef(null); + const [current, setCurrent] = useState(''); + + const handleOnSelectModel = (item: any) => { + onSelectModel(item); + setCurrent(item.id); + }; const handleOnSearchRepo = async (text: string) => { axiosTokenRef.current?.abort?.(); @@ -67,8 +72,10 @@ const SearchInput: React.FC = (props) => { ).reverse(); cacheRepoOptions.current = sortedList; setRepoOptions(sortedList); + handleOnSelectModel(sortedList[0]); } catch (error) { setRepoOptions([]); + handleOnSelectModel({}); cacheRepoOptions.current = []; } finally { setLoading(false); @@ -81,7 +88,6 @@ const SearchInput: React.FC = (props) => { }; const handleOnFocus = () => { - setShowSearch(true); if ( !repoOptions.length && !cacheRepoOptions.current.length && @@ -95,17 +101,6 @@ const SearchInput: React.FC = (props) => { } }; - const handleOnSelectModel = (item: any) => { - onSelectModel(item); - setShowSearch(false); - }; - - const handleOnBlur = () => { - setTimeout(() => { - setShowSearch(false); - }, 200); - }; - const handleFilterModels = (e: any) => { const text = e.target.value; const list = _.filter(cacheRepoOptions.current, (item: any) => { @@ -121,26 +116,30 @@ const SearchInput: React.FC = (props) => { const handleSourceChange = (source: string) => { axiosTokenRef.current?.abort?.(); - onSourceChange(source); + onSourceChange?.(source); setRepoOptions([]); cacheRepoOptions.current = []; }; + useEffect(() => { + handleOnFocus(); + return () => { + axiosTokenRef.current?.abort?.(); + }; + }, [modelSource]); + return ( - <> +
- + > */} handleOnBlur()} - className="m-l-20" placeholder={ modelSource === 'huggingface' ? 'Search models from hugging face ' @@ -156,17 +155,16 @@ const SearchInput: React.FC = (props) => { } >
- {showSearch && ( + { - )} -
- + } +
); }; diff --git a/src/pages/llmodels/components/search-result.tsx b/src/pages/llmodels/components/search-result.tsx index 51324ba4..d9310310 100644 --- a/src/pages/llmodels/components/search-result.tsx +++ b/src/pages/llmodels/components/search-result.tsx @@ -26,7 +26,7 @@ const SearchResult: React.FC = (props) => { {resultList.length ? ( {resultList.map((item, index) => ( - +
handleSelect(e, item)}> = ({ const [openViewCodeModal, setOpenViewCodeModal] = useState(false); const [openLogModal, setOpenLogModal] = useState(false); const [openAddModal, setOpenAddModal] = useState(false); - const [action, setAction] = useState(PageAction.CREATE); + const [openDeployModal, setOpenDeployModal] = useState(false); const [title, setTitle] = useState(''); const [currentData, setCurrentData] = useState( undefined ); const [currentInstanceUrl, setCurrentInstanceUrl] = useState(''); const modalRef = useRef(null); + const sourceRef = useRef(null); + + const sourceOptions = [ + { + label: 'Hugging Face', + value: modelSourceMap.huggingface_value, + key: 'huggingface', + icon: , + onClick: () => { + sourceRef.current = modelSourceMap.huggingface_value; + setOpenDeployModal(true); + } + }, + { + label: 'Ollama Library', + value: modelSourceMap.ollama_library_value, + key: 'ollama_library', + icon: , + onClick: () => { + sourceRef.current = modelSourceMap.ollama_library_value; + setOpenDeployModal(true); + } + } + ]; const ActionList = [ { @@ -133,7 +157,6 @@ const Models: React.FC = ({ const handleAddModal = () => { setOpenAddModal(true); - setAction(PageAction.CREATE); setTitle(intl.formatMessage({ id: 'models.button.deploy' })); }; @@ -141,21 +164,12 @@ const Models: React.FC = ({ async (data: FormData) => { try { console.log('data:', data); - - if (data.source === modelSourceMap.ollama_library_value) { - data.ollama_library_model_name = `${data.ollama_library_model_name}:${data.tag}`; - } - if (action === PageAction.CREATE) { - await createModel({ data }); - } - if (action === PageAction.EDIT) { - await updateModel({ data, id: currentData?.id as number }); - } + await updateModel({ data, id: currentData?.id as number }); setOpenAddModal(false); message.success(intl.formatMessage({ id: 'common.message.success' })); } catch (error) {} }, - [action, currentData] + [currentData] ); const handleModalCancel = useCallback(() => { @@ -163,6 +177,20 @@ const Models: React.FC = ({ setOpenAddModal(false); }, []); + const handleDeployModalCancel = useCallback(() => { + setOpenDeployModal(false); + }, []); + + const handleCreateModel = useCallback(async (data: FormData) => { + try { + console.log('data:', data); + + await createModel({ data }); + setOpenDeployModal(false); + message.success(intl.formatMessage({ id: 'common.message.success' })); + } catch (error) {} + }, []); + const handleLogModalCancel = useCallback(() => { setOpenLogModal(false); }, []); @@ -230,7 +258,6 @@ const Models: React.FC = ({ const handleEdit = (row: ListItem) => { setCurrentData(row); setOpenAddModal(true); - setAction(PageAction.EDIT); setTitle(intl.formatMessage({ id: 'models.title.edit' })); }; @@ -314,13 +341,15 @@ const Models: React.FC = ({ } right={ - + + +