From 77f38394e6401e06d4e7ed84be3d95b60c651574 Mon Sep 17 00:00:00 2001 From: jialin Date: Wed, 7 Aug 2024 15:03:17 +0800 Subject: [PATCH] chore: drawer ux --- package.json | 1 + pnpm-lock.yaml | 7 + src/config/hotkeys.ts | 3 +- src/locales/en-US/models.ts | 4 +- src/locales/zh-CN/models.ts | 4 +- src/pages/llmodels/apis/index.ts | 4 +- .../llmodels/components/column-wrapper.tsx | 16 +++ .../llmodels/components/deploy-modal.tsx | 83 +++++++----- .../llmodels/components/hf-model-file.tsx | 111 +++++++++------ .../llmodels/components/hf-model-item.tsx | 63 ++++++--- src/pages/llmodels/components/model-card.tsx | 48 +++++++ .../llmodels/components/search-input.tsx | 55 ++++++++ .../llmodels/components/search-model.tsx | 128 +++++++++++++----- .../llmodels/components/search-result.tsx | 7 +- src/pages/llmodels/components/table-list.tsx | 2 +- .../llmodels/components/title-wrapper.tsx | 12 ++ src/pages/llmodels/config/index.ts | 47 +++++-- src/pages/llmodels/style/column-wrapper.less | 27 ++++ src/pages/llmodels/style/hf-model-file.less | 5 + src/pages/llmodels/style/model-card.less | 28 ++++ src/pages/llmodels/style/search-result.less | 30 +++- src/pages/llmodels/style/title-wrapper.less | 12 ++ 22 files changed, 547 insertions(+), 150 deletions(-) create mode 100644 src/pages/llmodels/components/column-wrapper.tsx create mode 100644 src/pages/llmodels/components/model-card.tsx create mode 100644 src/pages/llmodels/components/search-input.tsx create mode 100644 src/pages/llmodels/components/title-wrapper.tsx create mode 100644 src/pages/llmodels/style/column-wrapper.less create mode 100644 src/pages/llmodels/style/model-card.less create mode 100644 src/pages/llmodels/style/title-wrapper.less diff --git a/package.json b/package.json index e729789d..c1f1c710 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@ant-design/pro-components": "^2.7.1", "@huggingface/gguf": "^0.1.7", "@huggingface/hub": "^0.15.1", + "@huggingface/tasks": "^0.11.6", "@monaco-editor/react": "^4.6.0", "@types/lodash": "^4.17.4", "@umijs/max": "^4.2.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 236137ba..94cd874b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@huggingface/hub': specifier: ^0.15.1 version: 0.15.1 + '@huggingface/tasks': + specifier: ^0.11.6 + version: 0.11.6 '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.50.0)(react-dom@18.2.0)(react@18.2.0) @@ -4050,6 +4053,10 @@ packages: resolution: {integrity: sha512-8Q3aqTO+ldTTqtK4OfMz/h5DiiMBzUnZKdV0Dq2+JX+UXvqnTDVOk+bJd0QVytJYyNeZgKsj7XQHvEQGyo9cFg==, tarball: https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.10.14.tgz} dev: false + /@huggingface/tasks@0.11.6: + resolution: {integrity: sha512-jIPlnJjSOqQCTpyCyIZCyamw3vOvMZrlaEdoB/PInHLnoaoqJKVIc0ijULKJxC3ClkgmehdoOu4J/yU+eGQLRw==, tarball: https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.11.6.tgz} + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} diff --git a/src/config/hotkeys.ts b/src/config/hotkeys.ts index 6856393e..3f92e0f8 100644 --- a/src/config/hotkeys.ts +++ b/src/config/hotkeys.ts @@ -10,5 +10,6 @@ export default { REFRESH: ['ctrl+r', 'meta+r'], EDIT: ['ctrl+e', 'meta+e'], SEARCH: ['ctrl+f', 'meta+f'], - RESET: ['ctrl+shift+r', 'meta+shift+r'] + RESET: ['ctrl+shift+r', 'meta+shift+r'], + INPUT: ['ctrl+k', 'meta+k'] }; diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index 34d391fe..fe3ceebb 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -13,5 +13,7 @@ export default { 'models.openinplayground': 'Open in Playground', 'models.instances': 'instances', 'model.form.ollama.model': 'Ollama Model', - 'model.form.ollamaholder': 'Please select or input model name' + 'model.form.ollamaholder': 'Please select or input model name', + 'model.form.ollamatips': + 'Tip: The following are the preconfigured Ollama models in GPUStack. Please select the model you want, or directly enter the model you wish to deploy in the 【{name}】 input box on the right.' }; diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index a817a5a0..8d276b49 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -13,5 +13,7 @@ export default { 'models.openinplayground': '在 Playground 中打开', 'models.instances': '实例', 'model.form.ollama.model': 'Ollama 模型', - 'model.form.ollamaholder': '请选择或输入模型名称' + 'model.form.ollamaholder': '请选择或输入模型名称', + 'model.form.ollamatips': + '提示:以下为 GPUStack 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。' }; diff --git a/src/pages/llmodels/apis/index.ts b/src/pages/llmodels/apis/index.ts index c64cc0e1..d3cc6561 100644 --- a/src/pages/llmodels/apis/index.ts +++ b/src/pages/llmodels/apis/index.ts @@ -1,4 +1,5 @@ import { listFiles, listModels } from '@huggingface/hub'; +import { PipelineType } from '@huggingface/tasks'; import { request } from '@umijs/max'; import { FormData, @@ -129,6 +130,7 @@ export async function queryHuggingfaceModels( search: { query: string; tags: string[]; + task?: PipelineType; }; }, options?: any @@ -137,7 +139,7 @@ export async function queryHuggingfaceModels( for await (const model of listModels({ ...params, ...options, - limit: 50, + limit: 100, additionalFields: ['sha'], fetch(url: string, config: any) { try { diff --git a/src/pages/llmodels/components/column-wrapper.tsx b/src/pages/llmodels/components/column-wrapper.tsx new file mode 100644 index 00000000..1028e6f8 --- /dev/null +++ b/src/pages/llmodels/components/column-wrapper.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import '../style/column-wrapper.less'; + +const ColumnWrapper: React.FC = ({ children, footer }) => { + if (footer) { + return ( +
+
{children}
+ {
{footer}
} +
+ ); + } + return
{children}
; +}; + +export default ColumnWrapper; diff --git a/src/pages/llmodels/components/deploy-modal.tsx b/src/pages/llmodels/components/deploy-modal.tsx index fa9f84a5..316e455d 100644 --- a/src/pages/llmodels/components/deploy-modal.tsx +++ b/src/pages/llmodels/components/deploy-modal.tsx @@ -5,14 +5,17 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import { convertFileSize } from '@/utils'; import { useIntl } from '@umijs/max'; -import { Divider, Drawer, Form } from 'antd'; +import { Drawer, Form } 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 ColumnWrapper from './column-wrapper'; import HFModelFile from './hf-model-file'; +import ModelCard from './model-card'; import SearchModel from './search-model'; +import TitleWrapper from './title-wrapper'; type AddModalProps = { title: string; @@ -250,10 +253,20 @@ const AddModal: React.FC = (props) => { const handleOnSelectModel = useCallback((item: any) => { const repo = item.name; + let name = _.split(item.name, '/').slice(-1)[0]; + const reg = /(-gguf)$/i; + name = _.toLower(name).replace(reg, ''); + if (form.getFieldValue('source') === modelSourceMap.huggingface_value) { - form.setFieldValue('huggingface_repo_id', repo); + form.setFieldsValue({ + huggingface_repo_id: repo, + name: name + }); } else { - form.setFieldValue('ollama_library_model_name', repo); + form.setFieldsValue({ + ollama_library_model_name: repo, + name: name + }); } }, []); @@ -272,42 +285,50 @@ const AddModal: React.FC = (props) => { keyboard={false} styles={{ body: { - height: 'calc(100vh - 110px)' + height: 'calc(100vh - 53px)', + padding: '16px 0' } }} width="90%" - footer={ - - } + footer={false} >
-
+ -
- - -
- -
- -
-

- Configuration -

-
+ + {modelSource === modelSourceMap.huggingface_value && ( + + + + + )} + + } + > + Configuration + name="name" rules={[ @@ -386,7 +407,7 @@ const AddModal: React.FC = (props) => { > -
+
); diff --git a/src/pages/llmodels/components/hf-model-file.tsx b/src/pages/llmodels/components/hf-model-file.tsx index 6b435bdd..178d7b98 100644 --- a/src/pages/llmodels/components/hf-model-file.tsx +++ b/src/pages/llmodels/components/hf-model-file.tsx @@ -1,11 +1,16 @@ import { convertFileSize } from '@/utils'; import { SearchOutlined } from '@ant-design/icons'; +import { + GGMLQuantizationType, + GGUF_QUANT_DESCRIPTIONS +} from '@huggingface/gguf'; 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'; +import TitleWrapper from './title-wrapper'; interface HFModelFileProps { repo: string; @@ -36,7 +41,8 @@ const HFModelFile: React.FC = (props) => { const list = _.filter(res, (file: any) => { return _.endsWith(file.path, '.gguf'); }); - setDataSource({ fileList: list, loading: false }); + const sortList = _.sortBy(list, (item: any) => item.size); + setDataSource({ fileList: sortList, loading: false }); handleSelectModelFile(list[0]); } catch (error) { setDataSource({ fileList: [], loading: false }); @@ -44,55 +50,72 @@ const HFModelFile: React.FC = (props) => { } }; + const getModelQuantizationType = (item: any) => { + const name = _.split(item.path, '.').slice(0, -1).join('.'); + let quanType = _.toUpper(name.split('-').slice(-1)[0]); + if (quanType.indexOf('.') > -1) { + quanType = _.split(quanType, '.')[1]; + } + if (_.get(GGUF_QUANT_DESCRIPTIONS, GGMLQuantizationType[quanType])) { + return {quanType}; + } + return null; + }; + 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)} - - -
- + + Available Files ({dataSource.fileList.length || 0}) + +
+ + {dataSource.fileList.length ? ( + + {_.map(dataSource.fileList, (item: any) => { + return ( + +
handleSelectModelFile(item)} + > +
{item.path}
+ + + {convertFileSize(item.size)} + + {getModelQuantizationType(item)} + +
+ +
-
- - ); - })} - - ) : ( - !dataSource.loading && ( - - } - description="No models found" - /> - ) - )} + + ); + })} + + ) : ( + !dataSource.loading && ( + + } + description="No files found" + /> + ) + )} +
); diff --git a/src/pages/llmodels/components/hf-model-item.tsx b/src/pages/llmodels/components/hf-model-item.tsx index ac66fdd8..2dc90617 100644 --- a/src/pages/llmodels/components/hf-model-item.tsx +++ b/src/pages/llmodels/components/hf-model-item.tsx @@ -4,7 +4,7 @@ import { FolderOutlined, HeartOutlined } from '@ant-design/icons'; -import { Space, Tag } from 'antd'; +import { Button, Space, Tag } from 'antd'; import classNames from 'classnames'; import dayjs from 'dayjs'; import _ from 'lodash'; @@ -15,7 +15,8 @@ interface HFModelItemProps { title: string; downloads: number; likes: number; - lastModified: string; + task?: string; + updatedAt: string; active: boolean; source?: string; tags?: string[]; @@ -29,15 +30,30 @@ const HFModelItem: React.FC = (props) => { })} >
- + {props.title}
{props.source === modelSourceMap.huggingface_value ? ( + {props.task && ( + + + {props.task} + + + )} {dayjs().to( - dayjs(dayjs(props.lastModified).format('YYYY-MM-DD HH:mm:ss')) + dayjs(dayjs(props.updatedAt).format('YYYY-MM-DD HH:mm:ss')) )} @@ -50,22 +66,29 @@ const HFModelItem: React.FC = (props) => { ) : ( - - {_.map(props.tags, (tag: string) => { - return ( - - - {tag} - - - ); - })} - +
+ + {_.map(props.tags, (tag: string) => { + return ( + + + {tag} + + + ); + })} + +
+ +
+
)}
diff --git a/src/pages/llmodels/components/model-card.tsx b/src/pages/llmodels/components/model-card.tsx new file mode 100644 index 00000000..03892343 --- /dev/null +++ b/src/pages/llmodels/components/model-card.tsx @@ -0,0 +1,48 @@ +import { Space, Tag } from 'antd'; +import React, { useEffect, useState } from 'react'; +import { queryHuggingfaceModelDetail } from '../apis'; +import '../style/model-card.less'; +import TitleWrapper from './title-wrapper'; + +const ModelCard: React.FC<{ repo: string }> = (props) => { + const { repo } = props; + const [modelData, setModelData] = useState({}); + + const getModelCardData = async () => { + if (!repo) { + return; + } + try { + const res = await queryHuggingfaceModelDetail({ repo }); + console.log('modelcarddata==========', res); + setModelData(res); + } catch (error) { + setModelData({}); + } + }; + + useEffect(() => { + getModelCardData(); + }, [repo]); + + return ( + <> + + Model Card + +
+
+
{modelData.id}
+ + + Architecture: + {modelData.config?.model_type} + + +
+
+ + ); +}; + +export default React.memo(ModelCard); diff --git a/src/pages/llmodels/components/search-input.tsx b/src/pages/llmodels/components/search-input.tsx new file mode 100644 index 00000000..fb8b6a19 --- /dev/null +++ b/src/pages/llmodels/components/search-input.tsx @@ -0,0 +1,55 @@ +import IconFont from '@/components/icon-font'; +import hotkeys from '@/config/hotkeys'; +import { platformCall } from '@/utils'; +import { SearchOutlined } from '@ant-design/icons'; +import { Input, Tag } from 'antd'; +import React, { useRef, useState } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; + +const SearchInput: React.FC<{ + onSearch: (e: any) => void; +}> = (props) => { + const { onSearch } = props; + const [isFocus, setIsFocus] = useState(false); + const inputRef = useRef(null); + const platform = platformCall(); + + useHotkeys(hotkeys.INPUT.join(','), () => { + inputRef.current?.focus?.(); + setIsFocus(true); + }); + + return ( + setIsFocus(true)} + onBlur={() => setIsFocus(false)} + allowClear + placeholder="Search models from Hugging Face" + suffix={ + !isFocus && ( + + {platform.isMac ? ( + <> + + K + + ) : ( + <>CTRL + K + )} + + ) + } + prefix={ + + } + > + ); +}; + +export default React.memo(SearchInput); diff --git a/src/pages/llmodels/components/search-model.tsx b/src/pages/llmodels/components/search-model.tsx index 5da60cab..e443f0f2 100644 --- a/src/pages/llmodels/components/search-model.tsx +++ b/src/pages/llmodels/components/search-model.tsx @@ -1,11 +1,16 @@ import IconFont from '@/components/icon-font'; -import { SearchOutlined } from '@ant-design/icons'; -import { Input } from 'antd'; +import { useIntl } from '@umijs/max'; +import { Button, Input, Select } from 'antd'; import _ from 'lodash'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { queryHuggingfaceModels } from '../apis'; -import { modelSourceMap, ollamaModelOptions } from '../config'; +import { + modelFilesSortOptions, + modelSourceMap, + ollamaModelOptions +} from '../config'; import SearchStyle from '../style/search-result.less'; +import SearchInput from './search-input'; import SearchResult from './search-result'; interface SearchInputProps { @@ -29,14 +34,16 @@ const sourceList = [ } ]; -const SearchInput: React.FC = (props) => { +const SearchModel: React.FC = (props) => { + const intl = useIntl(); const { modelSource, onSourceChange, onSelectModel } = props; - const [showSearch, setShowSearch] = useState(false); const [repoOptions, setRepoOptions] = useState([]); const [loading, setLoading] = useState(false); + const [current, setCurrent] = useState(''); + const [sortType, setSortType] = useState('downloads'); const cacheRepoOptions = useRef([]); const axiosTokenRef = useRef(null); - const [current, setCurrent] = useState(''); + const customOllamaModelRef = useRef(null); const handleOnSelectModel = (item: any) => { onSelectModel(item); @@ -68,7 +75,7 @@ const SearchInput: React.FC = (props) => { }); const sortedList = _.sortBy( list, - (item: any) => item.downloads + (item: any) => item[sortType] ).reverse(); cacheRepoOptions.current = sortedList; setRepoOptions(sortedList); @@ -82,12 +89,12 @@ const SearchInput: React.FC = (props) => { } }; - const handlerSearchModels = async (e: any) => { + const handlerSearchModels = useCallback(async (e: any) => { const text = e.target.value; handleOnSearchRepo(text); - }; + }, []); - const handleOnFocus = () => { + const handleOnOpen = () => { if ( !repoOptions.length && !cacheRepoOptions.current.length && @@ -98,6 +105,7 @@ const SearchInput: React.FC = (props) => { if (modelSourceMap.ollama_library_value === modelSource) { setRepoOptions(ollamaModelOptions); cacheRepoOptions.current = ollamaModelOptions; + handleOnSelectModel(ollamaModelOptions[0]); } }; @@ -107,7 +115,6 @@ const SearchInput: React.FC = (props) => { return item.name.includes(text); }); setRepoOptions(list); - console.log('handleFilterModels', text); }; const debounceFilter = _.debounce((e: any) => { @@ -121,8 +128,73 @@ const SearchInput: React.FC = (props) => { cacheRepoOptions.current = []; }; + const handleInputChange = (e: any) => { + const value = e.target.value; + customOllamaModelRef.current = value; + }; + + const handleConfirm = () => { + const model = { + label: customOllamaModelRef.current, + value: customOllamaModelRef.current, + name: customOllamaModelRef.current, + id: '' + }; + onSelectModel(model); + setCurrent(''); + }; + + const handleSortChange = (value: string) => { + const sortedList = _.sortBy( + repoOptions, + (item: any) => item[value] + ).reverse(); + setSortType(value); + setRepoOptions(sortedList); + }; + + const renderHFSearch = () => { + return ( + <> + +
+ + {repoOptions.length}results + + +
+ + ); + }; + + const renderOllamaCustom = () => { + return ( + <> + +
+ +
+ + ); + }; + useEffect(() => { - handleOnFocus(); + handleOnOpen(); return () => { axiosTokenRef.current?.abort?.(); }; @@ -136,24 +208,16 @@ const SearchInput: React.FC = (props) => { value={modelSource} onChange={handleSourceChange} > */} - - } - > + {modelSource === modelSourceMap.huggingface_value ? ( + renderHFSearch() + ) : ( +
+ {intl.formatMessage( + { id: 'model.form.ollamatips' }, + { name: intl.formatMessage({ id: 'model.form.ollama.model' }) } + )} +
+ )}
{ = (props) => { ); }; -export default React.memo(SearchInput); +export default React.memo(SearchModel); diff --git a/src/pages/llmodels/components/search-result.tsx b/src/pages/llmodels/components/search-result.tsx index d9310310..fff3641c 100644 --- a/src/pages/llmodels/components/search-result.tsx +++ b/src/pages/llmodels/components/search-result.tsx @@ -24,10 +24,10 @@ const SearchResult: React.FC = (props) => {
{resultList.length ? ( - + {resultList.map((item, index) => ( -
handleSelect(e, item)}> +
handleSelect(e, item)} tabIndex={0}> = (props) => { title={item.name} downloads={item.downloads} likes={item.likes} - lastModified={item.lastModified} + task={item.task} + updatedAt={item.updatedAt} active={item.id === props.current} />
diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 5b2a17f7..e45a44bb 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -341,7 +341,7 @@ const Models: React.FC = ({ } right={ - +