diff --git a/src/components/label-selector/index.tsx b/src/components/label-selector/index.tsx new file mode 100644 index 00000000..3b8ba5fc --- /dev/null +++ b/src/components/label-selector/index.tsx @@ -0,0 +1,100 @@ +import { PlusOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import _ from 'lodash'; +import React, { useEffect } from 'react'; +import LabelItem from './label-item'; +import Wrapper from './wrapper'; + +interface LabelSelectorProps { + labels: Record; + label?: string; + onChange?: (labels: Record) => void; +} + +const LabelSelector: React.FC = ({ + labels, + onChange, + label +}) => { + const [labelList, setLabelList] = React.useState([]); + + useEffect(() => { + const list = _.map(_.keys(labels), (key: string) => { + return { + key, + value: labels[key] + }; + }); + setLabelList(list); + }, [labels]); + + const handleOnChange = (index: string, label: any) => { + const list = _.cloneDeep(labelList); + list[index] = label; + const newLabels = _.reduce( + list, + (result: any, item: any) => { + result[item.key] = item.value; + return result; + }, + {} + ); + onChange?.(newLabels); + }; + + const handleAddLabel = () => { + setLabelList([ + ...labelList, + { + key: '', + value: '' + } + ]); + }; + + const handleOnDelete = (index: string) => { + const list = _.cloneDeep(labelList); + list.splice(parseInt(index), 1); + setLabelList(list); + const newLabels = _.reduce( + list, + (result: any, item: any) => { + result[item.key] = item.value; + return result; + }, + {} + ); + onChange?.(newLabels); + }; + + return ( + + {_.map(labelList, (item: any, index: string) => { + return ( + handleOnDelete(index)} + onChange={(obj) => handleOnChange(index, obj)} + /> + ); + })} +
+ +
+
+ ); +}; + +export default React.memo(LabelSelector); diff --git a/src/components/label-selector/label-item.tsx b/src/components/label-selector/label-item.tsx new file mode 100644 index 00000000..de38ed0a --- /dev/null +++ b/src/components/label-selector/label-item.tsx @@ -0,0 +1,78 @@ +import SealInput from '@/components/seal-form/seal-input'; +import { MinusOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import React from 'react'; +import './styles/label-item.less'; + +interface LabelItemProps { + label: { + key: string; + value: string; + }; + labelKey?: string; + labelValue?: string; + keyAddon?: React.ReactNode; + valueAddon?: React.ReactNode; + seperator?: string; + onDelete?: () => void; + onChange?: (params: { key: string; value: string }) => void; +} +const LabelItem: React.FC = ({ + label, + seperator, + keyAddon, + valueAddon, + onChange, + onDelete +}) => { + const handleOnValueChange = (e: any) => { + const value = e.target.value; + onChange?.({ + key: label.key, + value: value + }); + }; + + const handleOnKeyChange = (e: any) => { + const key = e.target.value; + onChange?.({ + key: key, + value: label.value + }); + }; + + return ( +
+
+ {keyAddon ?? ( + + )} +
+ {seperator && {seperator}} +
+ {valueAddon ?? ( + + )} +
+ +
+ ); +}; + +export default LabelItem; diff --git a/src/components/label-selector/styles/label-item.less b/src/components/label-selector/styles/label-item.less new file mode 100644 index 00000000..fab81bfe --- /dev/null +++ b/src/components/label-selector/styles/label-item.less @@ -0,0 +1,29 @@ +.label-item { + display: flex; + margin-bottom: 12px; + align-items: center; + justify-content: flex-start; + + .seprator { + display: flex; + flex: none; + width: 12px; + align-items: center; + justify-content: center; + color: var(--ant-color-text-tertiary); + } + + .btn { + width: 24px; + margin-left: 10px; + flex: none; + } + + .label-key { + flex: 1; + } + + .label-value { + flex: 1; + } +} diff --git a/src/components/label-selector/styles/wrapper.less b/src/components/label-selector/styles/wrapper.less new file mode 100644 index 00000000..26239ced --- /dev/null +++ b/src/components/label-selector/styles/wrapper.less @@ -0,0 +1,18 @@ +.wrapper { + position: relative; + padding: 16px 24px; + padding-top: 30px; + border: 1px solid var(--ant-color-border); + border-radius: var(--border-radius-base); + display: flex; + flex-direction: column; + + :global { + .label { + position: absolute; + left: 24px; + line-height: 1; + top: 10px; + } + } +} diff --git a/src/components/label-selector/wrapper.tsx b/src/components/label-selector/wrapper.tsx new file mode 100644 index 00000000..f379ef98 --- /dev/null +++ b/src/components/label-selector/wrapper.tsx @@ -0,0 +1,21 @@ +import LabelInfo from '@/components/seal-form/components/label-info'; +import React from 'react'; +import styles from './styles/wrapper.less'; + +const Wrapper: React.FC<{ + label?: string; + children: React.ReactNode; +}> = ({ children, label }) => { + return ( +
+ {label && ( + + + + )} + {children} +
+ ); +}; + +export default Wrapper; diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index 0780fbbe..5e199bf7 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -9,6 +9,7 @@ export default { 'models.form.repoid.desc': 'Only .gguf format is supported', 'models.form.filename': 'File Name', 'models.form.replicas': 'Replicas', + 'models.form.configurations': 'Configurations', 'models.form.s3address': 'S3 Address', 'models.openinplayground': 'Open in Playground', 'models.instances': 'instances', diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index f297aec9..f97d99d5 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -9,6 +9,7 @@ export default { 'models.form.repoid.desc': '只支持 .gguf 格式', 'models.form.filename': '文件名', 'models.form.replicas': '副本数', + 'models.form.configurations': '配置', 'models.form.s3address': 'S3 地址', 'models.openinplayground': '在 Playground 中打开', 'models.instances': '实例', diff --git a/src/pages/llmodels/components/data-form.tsx b/src/pages/llmodels/components/data-form.tsx new file mode 100644 index 00000000..874756d7 --- /dev/null +++ b/src/pages/llmodels/components/data-form.tsx @@ -0,0 +1,301 @@ +import SealAutoComplete from '@/components/seal-form/auto-complete'; +import SealInput from '@/components/seal-form/seal-input'; +import SealSelect from '@/components/seal-form/seal-select'; +import { PageAction } from '@/config'; +import { PageActionType } from '@/config/types'; +import { useIntl } from '@umijs/max'; +import { Form } from 'antd'; +import _ from 'lodash'; +import React, { forwardRef, useEffect, useImperativeHandle } from 'react'; +import { modelSourceMap, ollamaModelOptions } from '../config'; +import { FormData } from '../config/types'; + +interface DataFormProps { + ref?: any; + source: string; + action: PageActionType; + repo: string; + onOk: (values: FormData) => void; +} + +const sourceOptions = [ + { + label: 'Hugging Face', + value: modelSourceMap.huggingface_value, + key: 'huggingface' + }, + { + label: 'Ollama Library', + value: modelSourceMap.ollama_library_value, + key: 'ollama_library' + } +]; + +const DataForm: React.FC = forwardRef((props, ref) => { + const { action, repo, onOk } = props; + const [form] = Form.useForm(); + const intl = useIntl(); + + const handleSumit = () => { + form.submit(); + }; + + useImperativeHandle( + ref, + () => { + return { + submit: handleSumit, + setFieldsValue: (values: FormData) => { + form.setFieldsValue(values); + }, + setFieldValue: (name: string, value: any) => { + form.setFieldValue(name, value); + }, + getFieldValue: (name: string) => { + return form.getFieldValue(name); + } + }; + }, + [] + ); + + const handleOnSelectModel = () => { + console.log('repo=============', repo); + if (!repo) { + return; + } + let name = _.split(repo, '/').slice(-1)[0]; + const reg = /(-gguf)$/i; + name = _.toLower(name).replace(reg, ''); + + if (props.source === modelSourceMap.huggingface_value) { + form.setFieldsValue({ + huggingface_repo_id: repo, + name: name + }); + } else { + form.setFieldsValue({ + ollama_library_model_name: repo, + name: name + }); + } + }; + + const renderHuggingfaceFields = () => { + return ( + <> + + name="huggingface_repo_id" + key="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" + key="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; + } + }; + + useEffect(() => { + handleOnSelectModel(); + }, [repo]); + + return ( +
+ + 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 React.memo(DataForm); diff --git a/src/pages/llmodels/components/deploy-modal.tsx b/src/pages/llmodels/components/deploy-modal.tsx index a2bc5e5e..db5d1124 100644 --- a/src/pages/llmodels/components/deploy-modal.tsx +++ b/src/pages/llmodels/components/deploy-modal.tsx @@ -1,18 +1,13 @@ import ModalFooter from '@/components/modal-footer'; -import SealInput from '@/components/seal-form/seal-input'; -import SealSelect from '@/components/seal-form/seal-select'; -import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; -import { convertFileSize } from '@/utils'; import { CloseOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Button, Drawer, Form } from 'antd'; -import _ from 'lodash'; -import { memo, useCallback, useEffect, useState } from 'react'; -import { queryHuggingfaceModelFiles, queryHuggingfaceModels } from '../apis'; +import { Button, Drawer } from 'antd'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { modelSourceMap } from '../config'; import { FormData, ListItem } from '../config/types'; import ColumnWrapper from './column-wrapper'; +import DataForm from './data-form'; import HFModelFile from './hf-model-file'; import ModelCard from './model-card'; import SearchModel from './search-model'; @@ -24,257 +19,44 @@ type AddModalProps = { open: boolean; data?: ListItem; source: string; + width?: string | number; onOk: (values: FormData) => 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 { + title, + open, + onOk, + onCancel, + source, + action, + width = 600 + } = props || {}; + const form = useRef({}); 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 [huggingfaceRepoId, setHuggingfaceRepoId] = useState(''); - const initFormValue = () => { - form.setFieldsValue({ - source: props.source, - replicas: 1 - }); + const handleSelectModelFile = useCallback((item: any) => { + form.current?.setFieldValue?.('huggingface_filename', item.path); + }, []); + + const handleOnSelectModel = (item: any) => { + setHuggingfaceRepoId(item.name); + }; + + const handleSumit = () => { + form.current?.submit?.(); }; useEffect(() => { - initFormValue(); - console.log('source========', props.source); + return () => { + setHuggingfaceRepoId(''); + }; }, [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; - 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.setFieldsValue({ - huggingface_repo_id: repo, - name: name - }); - } else { - form.setFieldsValue({ - ollama_library_model_name: repo, - name: name - }); - } - }, []); - - const handleSumit = () => { - form.submit(); - }; - return ( = (props) => { borderRadius: '8px 0 0 8px' } }} - width={ - props.source === modelSourceMap.huggingface_value - ? 'calc(100vw - 220px)' - : 600 - } + width={width} footer={false} >
{props.source === modelSourceMap.huggingface_value && ( @@ -348,94 +126,16 @@ const AddModal: React.FC = (props) => { } > <> - 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"> - - - + + {intl.formatMessage({ id: 'models.form.configurations' })} + +
@@ -443,4 +143,4 @@ const AddModal: React.FC = (props) => { ); }; -export default memo(AddModal); +export default AddModal; diff --git a/src/pages/llmodels/components/hf-model-file.tsx b/src/pages/llmodels/components/hf-model-file.tsx index 34634592..4f817d28 100644 --- a/src/pages/llmodels/components/hf-model-file.tsx +++ b/src/pages/llmodels/components/hf-model-file.tsx @@ -39,7 +39,7 @@ const HFModelFile: React.FC = (props) => { try { const res = await queryHuggingfaceModelFiles({ repo: props.repo }); const list = _.filter(res, (file: any) => { - return _.endsWith(file.path, '.gguf'); + return _.endsWith(file.path, '.gguf') || _.includes(file.path, '.gguf'); }); const sortList = _.sortBy(list, (item: any) => item.size); setDataSource({ fileList: sortList, loading: false }); @@ -58,7 +58,11 @@ const HFModelFile: React.FC = (props) => { } console.log('quanType', quanType, FileType[quanType]); if (FileType[quanType] !== undefined) { - return {quanType}; + return ( + + {quanType} + + ); } return null; }; @@ -100,7 +104,7 @@ const HFModelFile: React.FC = (props) => { */} = (props) => { {props.task && ( + <> + + } > ); diff --git a/src/pages/llmodels/components/search-model.tsx b/src/pages/llmodels/components/search-model.tsx index 6a0d9e95..b93260b0 100644 --- a/src/pages/llmodels/components/search-model.tsx +++ b/src/pages/llmodels/components/search-model.tsx @@ -232,6 +232,7 @@ const SearchModel: React.FC = (props) => { useEffect(() => { handleOnOpen(); + console.log('SearchModel useEffect', modelSource); return () => { axiosTokenRef.current?.abort?.(); }; diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index c58a60e2..ea024c00 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -80,14 +80,17 @@ const Models: React.FC = ({ const [openViewCodeModal, setOpenViewCodeModal] = useState(false); const [openLogModal, setOpenLogModal] = useState(false); const [openAddModal, setOpenAddModal] = useState(false); - const [openDeployModal, setOpenDeployModal] = useState(false); + const [openDeployModal, setOpenDeployModal] = useState({ + show: false, + width: 600, + source: modelSourceMap.huggingface_value + }); const [title, setTitle] = useState(''); const [currentData, setCurrentData] = useState( undefined ); const [currentInstanceUrl, setCurrentInstanceUrl] = useState(''); const modalRef = useRef(null); - const sourceRef = useRef(null); const sourceOptions = [ { @@ -96,8 +99,11 @@ const Models: React.FC = ({ key: 'huggingface', icon: , onClick: () => { - sourceRef.current = modelSourceMap.huggingface_value; - setOpenDeployModal(true); + setOpenDeployModal({ + show: true, + width: 'calc(100vw - 220px)', + source: modelSourceMap.huggingface_value + }); } }, { @@ -106,8 +112,11 @@ const Models: React.FC = ({ key: 'ollama_library', icon: , onClick: () => { - sourceRef.current = modelSourceMap.ollama_library_value; - setOpenDeployModal(true); + setOpenDeployModal({ + show: true, + width: 600, + source: modelSourceMap.ollama_library_value + }); } } ]; @@ -177,16 +186,22 @@ const Models: React.FC = ({ setOpenAddModal(false); }, []); - const handleDeployModalCancel = useCallback(() => { - setOpenDeployModal(false); - }, []); + const handleDeployModalCancel = () => { + setOpenDeployModal({ + ...openDeployModal, + show: false + }); + }; const handleCreateModel = useCallback(async (data: FormData) => { try { console.log('data:', data); await createModel({ data }); - setOpenDeployModal(false); + setOpenDeployModal({ + ...openDeployModal, + show: false + }); message.success(intl.formatMessage({ id: 'common.message.success' })); } catch (error) {} }, []); @@ -476,11 +491,12 @@ const Models: React.FC = ({ onOk={handleModalOk} > diff --git a/src/pages/llmodels/style/hf-model-file.less b/src/pages/llmodels/style/hf-model-file.less index 4a850893..56a3c274 100644 --- a/src/pages/llmodels/style/hf-model-file.less +++ b/src/pages/llmodels/style/hf-model-file.less @@ -27,8 +27,9 @@ border-radius: 4px; font-size: 12px; height: 22px; - border: 1px solid var(--ant-color-border); - color: var(--ant-color-text-secondary); + opacity: 0.7; + // border: 1px solid var(--ant-color-border); + // color: var(--ant-color-text-secondary); } .btn { diff --git a/src/pages/llmodels/style/hf-model-item.less b/src/pages/llmodels/style/hf-model-item.less index 23442e27..f90d3764 100644 --- a/src/pages/llmodels/style/hf-model-item.less +++ b/src/pages/llmodels/style/hf-model-item.less @@ -31,8 +31,9 @@ border-radius: 4px; font-size: 12px; height: 22px; - border: 1px solid var(--ant-color-border); - color: var(--ant-color-text-secondary); + opacity: 0.9; + // border: 1px solid var(--ant-color-border); + // color: var(--ant-color-text-secondary); } } }