diff --git a/src/components/alert-info/block.less b/src/components/alert-info/block.less index 41795d20..042ecb86 100644 --- a/src/components/alert-info/block.less +++ b/src/components/alert-info/block.less @@ -27,6 +27,21 @@ border-color: var(--ant-geekblue-3); } + &.success { + border: 1px solid var(--color-progress-green); + color: var(--color-progress-green); + + .title-text { + color: var(--color-progress-green); + } + + .content.success { + color: var(--color-progress-green); + font-weight: var(--font-weight-normal); + opacity: 0.85; + } + } + .title { position: absolute; left: 0; @@ -48,6 +63,10 @@ &.transition { color: var(--ant-geekblue-7); } + + &.success { + color: var(--color-progress-green); + } } .text { diff --git a/src/components/alert-info/block.tsx b/src/components/alert-info/block.tsx index 8f486c8c..9c50003c 100644 --- a/src/components/alert-info/block.tsx +++ b/src/components/alert-info/block.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components'; import OverlayScroller from '../overlay-scroller'; import './block.less'; interface AlertInfoProps { - type: 'danger' | 'warning' | 'transition' | 'info'; + type: Global.MessageType; message: React.ReactNode; rows?: number; icon?: React.ReactNode; @@ -62,9 +62,16 @@ const AlertInfo: React.FC = (props) => { {icon ?? } - {title && {title}} + {title && ( + {title} + )} - {message} + + {message} + diff --git a/src/config/global.d.ts b/src/config/global.d.ts index 4af3812c..e6cd0273 100644 --- a/src/config/global.d.ts +++ b/src/config/global.d.ts @@ -42,6 +42,8 @@ declare namespace Global { } type SearchParams = Pagination & { search?: string }; + + type MessageType = 'transition' | 'warning' | 'danger' | 'success' | 'info'; } interface Window { diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index 877628e2..a632879c 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -126,5 +126,8 @@ export default { 'models.form.restart.onerror': 'Auto-Restart On Error', 'models.form.restart.onerror.tips': 'When an error occurs, it will automatically attempt to restart.', - 'models.form.check.params': 'Checking configuration...' + 'models.form.check.params': 'Checking configuration...', + 'models.form.check.passed': 'Check Compatibility Passed', + 'models.form.check.claims': + 'The model requires approximately {vram} VRAM and {ram} RAM.' }; diff --git a/src/locales/ja-JP/models.ts b/src/locales/ja-JP/models.ts index 1811e02a..57ac9117 100644 --- a/src/locales/ja-JP/models.ts +++ b/src/locales/ja-JP/models.ts @@ -123,7 +123,10 @@ export default { 'models.form.restart.onerror': 'Auto-Restart On Error', 'models.form.restart.onerror.tips': 'When an error occurs, it will automatically attempt to restart.', - 'models.form.check.params': 'Checking configuration...' + 'models.form.check.params': 'Checking configuration...', + 'models.form.check.passed': 'Check Compatibility Passed', + 'models.form.check.claims': + 'The model requires approximately {vram} VRAM and {ram} RAM.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -136,5 +139,7 @@ export default { // 7. 'models.form.restart.onerror.tips', // 8. 'models.form.check.params', // 9. 'models.form.partialoffload.tips', -// 10. 'models.form.distribution.tips +// 10. 'models.form.distribution.tips, +// 11. 'models.form.check.passed', +// 12. 'models.form.check.claims', // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/models.ts b/src/locales/ru-RU/models.ts index 72eaf5c2..e02f0098 100644 --- a/src/locales/ru-RU/models.ts +++ b/src/locales/ru-RU/models.ts @@ -126,10 +126,15 @@ export default { 'models.form.restart.onerror': 'Автоперезапуск при ошибке', 'models.form.restart.onerror.tips': 'При возникновении ошибки система автоматически попытается перезапуститься.', - 'models.form.check.params': 'Проверка конфигурации...' + 'models.form.check.params': 'Проверка конфигурации...', + 'models.form.check.passed': 'Check Compatibility Passed', + 'models.form.check.claims': + 'The model requires approximately {vram} VRAM and {ram} RAM.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== // 1. 'models.form.partialoffload.tips', -// 2. 'models.form.distribution.tips +// 2. 'models.form.distribution.tips, +// 3. 'models.form.check.passed', +// 4. 'models.form.check.claims', // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/resources.ts b/src/locales/ru-RU/resources.ts index 875c10e8..5aab1728 100644 --- a/src/locales/ru-RU/resources.ts +++ b/src/locales/ru-RU/resources.ts @@ -75,5 +75,5 @@ export default { }; // ========== To-Do: Translate Keys (Remove After Translation) ========== -// 1. 'resources.filter.path' +// 1. 'resources.filter.path', // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index cab052d5..1515374b 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -120,5 +120,7 @@ export default { 'models.form.incompatible': '检测到不兼容', 'models.form.restart.onerror': '错误时重启', 'models.form.restart.onerror.tips': '当发生错误时,将自动尝试恢复', - 'models.form.check.params': '正在校验配置...' + 'models.form.check.params': '正在校验配置...', + 'models.form.check.passed': '兼容性检查通过', + 'models.form.check.claims': '该模型大约需要 {vram} 显存和 {ram} 内存.' }; diff --git a/src/pages/llmodels/catalog.tsx b/src/pages/llmodels/catalog.tsx index 93cfdce0..6f0bc35b 100644 --- a/src/pages/llmodels/catalog.tsx +++ b/src/pages/llmodels/catalog.tsx @@ -237,7 +237,7 @@ const Catalog: React.FC = () => { allowClear showSearch={false} placeholder={intl.formatMessage({ id: 'models.filter.category' })} - style={{ width: 230 }} + style={{ width: 180 }} size="large" maxTagCount={1} onChange={handleCategoryChange} diff --git a/src/pages/llmodels/components/advance-config.tsx b/src/pages/llmodels/components/advance-config.tsx index 5e71f48c..0dcafd21 100644 --- a/src/pages/llmodels/components/advance-config.tsx +++ b/src/pages/llmodels/components/advance-config.tsx @@ -162,14 +162,18 @@ const AdvanceConfig: React.FC = (props) => { const handleSelectorOnBlur = () => { const workerSelector = form.getFieldValue('worker_selector'); - onValuesChange?.({}, form.getFieldsValue()); + // check if all keys have values + const hasEmptyValue = _.some(_.keys(workerSelector), (k: string) => { + return !workerSelector[k]; + }); + if (!hasEmptyValue) { + onValuesChange?.({}, form.getFieldsValue()); + } }; const handleBackendVersionOnBlur = () => { const backendVersion = form.getFieldValue('backend_version'); - if (backendVersion) { - onValuesChange?.({}, form.getFieldsValue()); - } + onValuesChange?.({}, form.getFieldsValue()); }; const collapseItems = useMemo(() => { diff --git a/src/pages/llmodels/components/compatible-alert.tsx b/src/pages/llmodels/components/compatible-alert.tsx index 36c684bd..24f80832 100644 --- a/src/pages/llmodels/components/compatible-alert.tsx +++ b/src/pages/llmodels/components/compatible-alert.tsx @@ -1,5 +1,6 @@ import AlertBlockInfo from '@/components/alert-info/block'; import { + CheckCircleFilled, CloseOutlined, LoadingOutlined, WarningFilled @@ -13,7 +14,7 @@ interface CompatibilityAlertProps { show: boolean; title?: string; isHtml?: boolean; - type?: 'danger' | 'warning' | 'transition'; + type?: Global.MessageType; message: string | string[]; }; contentStyle?: React.CSSProperties; @@ -28,7 +29,7 @@ const DivWrapper = styled.div` const CloseWrapper = styled.div` position: absolute; - top: 6px; + top: 10px; right: 18px; cursor: pointer; background-color: var(--ant-color-warning-bg); @@ -43,7 +44,7 @@ const MessageWrapper = styled.div` const CompatibilityAlert: React.FC = (props) => { const { warningStatus, contentStyle, showClose, onClose } = props; - const { title, show, message, isHtml, type } = warningStatus; + const { title, show, message, isHtml, type = 'warning' } = warningStatus; const renderMessage = useMemo(() => { if (!message || !show) { @@ -73,6 +74,16 @@ const CompatibilityAlert: React.FC = (props) => { return ''; }, [message, show]); + const renderIcon = useMemo(() => { + if (type === 'transition') { + return ; + } + if (type === 'success') { + return ; + } + return ; + }, [type]); + return ( show && ( @@ -82,9 +93,9 @@ const CompatibilityAlert: React.FC = (props) => { title={title} contentStyle={contentStyle} type={type || 'warning'} - icon={type === 'transition' ? : } + icon={renderIcon} > - {showClose && type !== 'transition' && ( + {showClose && !['transition', 'success'].includes(type) && ( diff --git a/src/pages/llmodels/components/deploy-builtin-modal.tsx b/src/pages/llmodels/components/deploy-builtin-modal.tsx index f3892206..1cb6ce11 100644 --- a/src/pages/llmodels/components/deploy-builtin-modal.tsx +++ b/src/pages/llmodels/components/deploy-builtin-modal.tsx @@ -79,9 +79,8 @@ const AddModal: React.FC = (props) => { width = 600 } = props || {}; const { - handleShowCompatibleAlert, setWarningStatus, - handleEvaluate, + handleDoEvalute, generateGPUIds, cancelEvaluate, submitAnyway, @@ -228,17 +227,7 @@ const AddModal: React.FC = (props) => { }; const handleCheckCompatibility = async (formData: FormData) => { - const evalutionData = await handleEvaluate(formData); - - if (evalutionData?.compatible) { - setWarningStatus({ - show: false, - message: '' - }); - } else { - handleShowCompatibleAlert?.(evalutionData); - } - return evalutionData; + handleDoEvalute(formData); }; const handleCheckFormData = () => { @@ -551,9 +540,12 @@ const AddModal: React.FC = (props) => { = (props) => { const { handleShowCompatibleAlert, setWarningStatus, + handleBackendChangeBefore, handleOnValuesChange, checkTokenRef, warningStatus, @@ -144,7 +145,12 @@ const AddModal: FC = (props) => { } else { setIsGGUF(false); } + const data = form.current.form.getFieldsValue?.(); + const res = handleBackendChangeBefore(data); + if (res.show) { + return; + } if (data.local_path || props.source !== modelSourceMap.local_path_value) { handleOnValuesChange?.({ changedValues: {}, @@ -303,9 +309,12 @@ const AddModal: FC = (props) => { = forwardRef((props, ref) => { }); const [sortType, setSortType] = useState('size'); const [current, setCurrent] = useState(''); + const currentPathRef = useRef(''); const modelFilesSortOptions = useRef([ { label: intl.formatMessage({ id: 'models.sort.size' }), @@ -100,6 +101,7 @@ const HFModelFile: React.FC = forwardRef((props, ref) => { const handleSelectModelFile = (item: any) => { props.onSelectFile?.(item); setCurrent(item.path); + currentPathRef.current = item.path; }; const parseFilename = (filename: string) => { @@ -274,7 +276,13 @@ const HFModelFile: React.FC = forwardRef((props, ref) => { evaluateResult: evaluationList[index] }; }); - handleSelectModelFile(resultList[0]); + const currentItem = _.find( + resultList, + (item: any) => item.path === currentPathRef.current + ); + if (currentItem) { + handleSelectModelFile(currentItem); + } setDataSource({ fileList: resultList, loading: false }); setIsEvaluating(false); } catch (error) { diff --git a/src/pages/llmodels/components/incompatiable-info.tsx b/src/pages/llmodels/components/incompatiable-info.tsx index 38f20fd3..581a9235 100644 --- a/src/pages/llmodels/components/incompatiable-info.tsx +++ b/src/pages/llmodels/components/incompatiable-info.tsx @@ -18,6 +18,12 @@ const CompatibleTag = styled(Tag)` background: transparent !important; `; +const ClaimTag = styled(Tag)` + margin: 0; + opacity: 0.7; + border-radius: var(--border-radius-base); +`; + const IncompatibleInfo = styled.div` display: flex; flex-direction: column; diff --git a/src/pages/llmodels/components/search-model.tsx b/src/pages/llmodels/components/search-model.tsx index a0f5ef61..6ea254aa 100644 --- a/src/pages/llmodels/components/search-model.tsx +++ b/src/pages/llmodels/components/search-model.tsx @@ -61,6 +61,7 @@ const SearchModel: React.FC = (props) => { ]; const [isEvaluating, setIsEvaluating] = useState(false); const [current, setCurrent] = useState(''); + const currentRef = useRef(''); const cacheRepoOptions = useRef([]); const axiosTokenRef = useRef(null); const checkTokenRef = useRef(null); @@ -100,6 +101,7 @@ const SearchModel: React.FC = (props) => { const handleOnSelectModel = (item: any) => { onSelectModel(item); setCurrent(item.id); + currentRef.current = item.id; }; // huggeface @@ -222,7 +224,12 @@ const SearchModel: React.FC = (props) => { repoOptions: resultList }; }); - handleOnSelectModel(resultList[0]); + const currentItem = resultList.find( + (item) => item.id === currentRef.current + ); + if (currentItem) { + handleOnSelectModel(currentItem); + } } catch (error) { setIsEvaluating(false); } @@ -264,9 +271,7 @@ const SearchModel: React.FC = (props) => { handleOnSelectModel(list[0]); setLoadingModel?.(false); - timer.current = setTimeout(() => { - handleEvaluate(list); - }, 200); + handleEvaluate(list); } catch (error: any) { setDataSource({ repoOptions: [], diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 202a9a66..e4d5a406 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -740,7 +740,7 @@ const Models: React.FC = ({ placeholder={intl.formatMessage({ id: 'models.filter.category' })} - style={{ width: 230 }} + style={{ width: 180 }} size="large" maxTagCount={1} onChange={handleCategoryChange} diff --git a/src/pages/llmodels/components/update-modal.tsx b/src/pages/llmodels/components/update-modal.tsx index e6d43072..7d07cb90 100644 --- a/src/pages/llmodels/components/update-modal.tsx +++ b/src/pages/llmodels/components/update-modal.tsx @@ -58,7 +58,7 @@ const UpdateModal: React.FC = (props) => { const { setWarningStatus, generateGPUIds, - handleOnValuesChange, + handleBackendChangeBefore, checkTokenRef, warningStatus } = useCheckCompatibility(); @@ -68,6 +68,8 @@ const UpdateModal: React.FC = (props) => { const localPathCache = useRef(''); const submitAnyway = useRef(false); + const handleOnValuesChange = (data: any) => {}; + // voxbox is not support multi gpu const handleSetGPUIds = (backend: string) => { const gpuids = form.getFieldValue(['gpu_selector', 'gpu_ids']) || []; @@ -91,6 +93,10 @@ const UpdateModal: React.FC = (props) => { handleSetGPUIds(backend); const data = form.getFieldsValue?.(); + const res = handleBackendChangeBefore(data); + if (res.show) { + return; + } if (data.local_path || data.source !== modelSourceMap.local_path_value) { handleOnValuesChange?.({ changedValues: {}, @@ -390,9 +396,10 @@ const UpdateModal: React.FC = (props) => { ; + resource_claim?: { + ram: number; + vram: number; + }; } diff --git a/src/pages/llmodels/hooks/index.ts b/src/pages/llmodels/hooks/index.ts index 0000e3a7..143e171e 100644 --- a/src/pages/llmodels/hooks/index.ts +++ b/src/pages/llmodels/hooks/index.ts @@ -2,6 +2,7 @@ import { createAxiosToken } from '@/hooks/use-chunk-request'; import { queryModelFilesList, queryWorkersList } from '@/pages/resources/apis'; import { WorkerStatusMap } from '@/pages/resources/config'; import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; +import { convertFileSize } from '@/utils'; import { useIntl } from '@umijs/max'; import _ from 'lodash'; import { useEffect, useRef, useState } from 'react'; @@ -19,6 +20,15 @@ import { ListItem } from '../config/types'; +type MessageStatus = { + show: boolean; + title?: string; + type?: Global.MessageType; + isHtml?: boolean; + message: string | string[]; + evaluateResult?: EvaluateResult; +}; + export const useGenerateFormEditInitialValues = () => { const gpuDeviceList = useRef([]); const workerList = useRef([]); @@ -220,12 +230,7 @@ export const useCheckCompatibility = () => { const submitAnyway = useRef(false); const requestIdRef = useRef(0); const updateStatusTimer = useRef(null); - const [warningStatus, setWarningStatus] = useState<{ - show: boolean; - title?: string; - type?: 'transition' | 'warning' | 'danger'; - message: string | string[]; - }>({ + const [warningStatus, setWarningStatus] = useState({ show: false, title: '', message: [] @@ -269,7 +274,9 @@ export const useCheckCompatibility = () => { } }; - const handleCheckCompatibility = (evaluateResult: EvaluateResult | null) => { + const handleCheckCompatibility = ( + evaluateResult: EvaluateResult | null + ): MessageStatus => { if (!evaluateResult) { return { show: false, @@ -279,11 +286,13 @@ export const useCheckCompatibility = () => { const { compatible, compatibility_messages = [], - scheduling_messages = [] + scheduling_messages = [], + resource_claim } = evaluateResult || {}; - return { - show: !compatible, + const hasClaim = !!resource_claim?.ram || !!resource_claim?.vram; + + let msgData = { title: scheduling_messages?.length > 0 ? compatibility_messages?.join(' ') @@ -293,6 +302,24 @@ export const useCheckCompatibility = () => { ? scheduling_messages : compatibility_messages?.join(' ') }; + if (hasClaim) { + const ram = convertFileSize(resource_claim.ram, 1); + const vram = convertFileSize(resource_claim.vram, 1); + msgData = { + title: intl.formatMessage({ id: 'models.form.check.passed' }), + message: intl.formatMessage( + { id: 'models.form.check.claims' }, + { ram, vram } + ) + }; + } + + return { + show: !compatible || hasClaim, + type: !compatible ? 'warning' : 'success', + isHtml: hasClaim, + ...msgData + }; }; const handleShowCompatibleAlert = (evaluateResult: EvaluateResult | null) => { @@ -387,14 +414,22 @@ export const useCheckCompatibility = () => { }; }; + const handleDoEvalute = async (formData: FormData) => { + const currentRequestId = updateRequestId(); + const evalutionData = await handleEvaluate(formData); + + if (currentRequestId === requestIdRef.current) { + handleShowCompatibleAlert?.(evalutionData); + } + return evalutionData; + }; + const handleOnValuesChange = async (params: { changedValues: any; allValues: any; source: string; }) => { - const { changedValues, allValues, source } = params; - console.log('params+++++++', params); - + const { allValues, source } = params; if ( _.isEqual(cacheFormValuesRef.current, allValues) || (allValues.source === modelSourceMap.local_path_value && @@ -405,16 +440,30 @@ export const useCheckCompatibility = () => { cacheFormValuesRef.current = allValues; const data = getSourceRepoConfigValue(source, allValues); const gpuSelector = generateGPUIds(data.values); - - const currentRequestId = updateRequestId(); - const evalutionData = await handleEvaluate({ + await handleDoEvalute({ ...data.values, ...gpuSelector }); + }; - if (currentRequestId === requestIdRef.current) { - handleShowCompatibleAlert?.(evalutionData); + // trigger from local_path change or backend change + const handleBackendChangeBefore = (params: { + local_path: string; + backend: string; + source: string; + }) => { + const { local_path, backend, source } = params; + + const res = handleUpdateWarning?.({ + backend, + localPath: local_path, + source: source + }); + + if (res.show) { + setWarningStatus?.(res); } + return res; }; const debounceHandleValuesChange = _.debounce(handleOnValuesChange, 500); @@ -434,13 +483,15 @@ export const useCheckCompatibility = () => { return { handleShowCompatibleAlert, handleUpdateWarning, - handleOnValuesChange: debounceHandleValuesChange, - warningStatus, - checkTokenRef, - submitAnyway, + handleDoEvalute, generateGPUIds, handleEvaluate, setWarningStatus, - cancelEvaluate + cancelEvaluate, + handleBackendChangeBefore, + handleOnValuesChange: debounceHandleValuesChange, + warningStatus, + checkTokenRef, + submitAnyway }; }; diff --git a/src/pages/resources/components/model-files.tsx b/src/pages/resources/components/model-files.tsx index 53e2f7ed..193f8bdc 100644 --- a/src/pages/resources/components/model-files.tsx +++ b/src/pages/resources/components/model-files.tsx @@ -68,6 +68,8 @@ import { ListItem as WorkerListItem } from '../config/types'; +const { Paragraph } = Typography; + const filterPattern = /^(.*?)(?:-\d+-of-\d+)?(\.gguf)?$/; const PathWrapper = styled.div` @@ -115,11 +117,18 @@ const FilesTag = styled(Tag)` border-radius: var(--border-radius-base); `; +const TypographyPara = styled(Paragraph)` + background: transparent; + color: inherit; + margin-bottom: 0; + font-size: 13px; +`; + const TooltipTitle: React.FC<{ path: string }> = ({ path }) => { const intl = useIntl(); return ( - , @@ -133,7 +142,7 @@ const TooltipTitle: React.FC<{ path: string }> = ({ path }) => { }} > {path} - + ); };