diff --git a/.eslintrc.js b/.eslintrc.js index fdfe3d2b..e089b074 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,6 +3,7 @@ module.exports = { rules: { 'react/no-unstable-nested-components': 1, 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': 'off' + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/class-name-casing': 'off' } }; diff --git a/.stylelintrc.js b/.stylelintrc.js index 08bc02ce..e212f587 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,3 +1,6 @@ module.exports = { extends: require.resolve('@umijs/max/stylelint'), + rules: { + 'selector-class-pattern': null + } }; diff --git a/src/assets/styles/common.less b/src/assets/styles/common.less index 9a796b1f..c3f461d9 100644 --- a/src/assets/styles/common.less +++ b/src/assets/styles/common.less @@ -2,6 +2,14 @@ margin-bottom: 20px; } +.m-b-8 { + margin-bottom: 8px; +} + +.m-b-5 { + margin-bottom: 5px; +} + .m-l-10 { margin-left: 10px; } @@ -10,6 +18,10 @@ margin-left: 5px; } +.m-l-2 { + margin-left: 2px; +} + .m-l-8 { margin-left: 8px; } @@ -75,6 +87,14 @@ gap: 5px; } +.gap-10 { + gap: 10px; +} + +.gap-8 { + gap: 8px; +} + .relative { position: relative; } diff --git a/src/components/highlight-code/code-viewer-light.tsx b/src/components/highlight-code/code-viewer-light.tsx new file mode 100644 index 00000000..9a39f32b --- /dev/null +++ b/src/components/highlight-code/code-viewer-light.tsx @@ -0,0 +1,77 @@ +import hljs from 'highlight.js'; +import { memo, useMemo } from 'react'; +import CopyButton from '../copy-button'; +import './styles/light.less'; +import { escapeHtml } from './utils'; + +interface CodeViewerProps { + code: string; + lang: string; + autodetect?: boolean; + ignoreIllegals?: boolean; + copyable?: boolean; +} +const CodeViewer: React.FC = (props) => { + const { + code, + lang, + autodetect = true, + ignoreIllegals = true, + copyable = true + } = props || {}; + + const highlightedCode = useMemo(() => { + const autodetectLang = autodetect && !lang; + const cannotDetectLanguage = !autodetectLang && !hljs.getLanguage(lang); + let className = ''; + + if (!cannotDetectLanguage) { + className = `hljs ${lang}`; + } + + // No idea what language to use, return raw code + if (cannotDetectLanguage) { + console.warn(`The language "${lang}" you specified could not be found.`); + return { + value: escapeHtml(code), + className: className + }; + } + + if (autodetectLang) { + const result = hljs.highlightAuto(code); + return { + value: result.value, + className: className + }; + } + const result = hljs.highlight(code, { + language: lang, + ignoreIllegals: ignoreIllegals + }); + return { + value: result.value, + className: className + }; + }, [code, lang, autodetect, ignoreIllegals]); + + return ( +
+      
+      {copyable && (
+        
+      )}
+    
+ ); +}; + +export default memo(CodeViewer); diff --git a/src/components/highlight-code/code-viewer.tsx b/src/components/highlight-code/code-viewer.tsx index fdfd7c75..69e78b25 100644 --- a/src/components/highlight-code/code-viewer.tsx +++ b/src/components/highlight-code/code-viewer.tsx @@ -1,7 +1,7 @@ import hljs from 'highlight.js'; -import 'highlight.js/styles/atom-one-dark.css'; import { memo, useMemo } from 'react'; import CopyButton from '../copy-button'; +import './styles/dark.less'; import { escapeHtml } from './utils'; interface CodeViewerProps { @@ -56,7 +56,7 @@ const CodeViewer: React.FC = (props) => { }, [code, lang, autodetect, ignoreIllegals]); return ( -
+    
        = (props) => {
-  const { code, lang = 'bash', copyable = true } = props;
+  const { code, lang = 'bash', copyable = true, theme = 'dark' } = props;
 
   return (
     
- + {theme === 'dark' ? ( + + ) : ( + + )}
); }; diff --git a/src/components/highlight-code/styles/dark.less b/src/components/highlight-code/styles/dark.less new file mode 100644 index 00000000..60a2656e --- /dev/null +++ b/src/components/highlight-code/styles/dark.less @@ -0,0 +1,106 @@ +// @ts-ingore +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em; +} + +code.hljs { + padding: 3px 5px; +} + +/* + +Atom One Dark by Daniel Gamage +Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax + +base: #282c34 +mono-1: #abb2bf +mono-2: #818896 +mono-3: #5c6370 +hue-1: #56b6c2 +hue-2: #61aeee +hue-3: #c678dd +hue-4: #98c379 +hue-5: #e06c75 +hue-5-2: #be5046 +hue-6: #d19a66 +hue-6-2: #e6c07b + +*/ +.code-pre.dark { + .hljs { + color: #abb2bf; + background: #282c34; + } + + .hljs-comment, + .hljs-quote { + color: #5c6370; + font-style: italic; + } + + .hljs-doctag, + .hljs-keyword, + .hljs-formula { + color: #c678dd; + } + + .hljs-section, + .hljs-name, + .hljs-selector-tag, + .hljs-deletion, + .hljs-subst { + color: #e06c75; + } + + .hljs-literal { + color: #56b6c2; + } + + .hljs-string, + .hljs-regexp, + .hljs-addition, + .hljs-attribute, + .hljs-meta .hljs-string { + color: #98c379; + } + + .hljs-attr, + .hljs-variable, + .hljs-template-variable, + .hljs-type, + .hljs-selector-class, + .hljs-selector-attr, + .hljs-selector-pseudo, + .hljs-number { + color: #d19a66; + } + + .hljs-symbol, + .hljs-bullet, + .hljs-link, + .hljs-meta, + .hljs-selector-id, + .hljs-title { + color: #61aeee; + } + + .hljs-built_in, + .hljs-title.class_, + .hljs-class .hljs-title { + color: #e6c07b; + } + + .hljs-emphasis { + font-style: italic; + } + + .hljs-strong { + font-weight: bold; + } + + .hljs-link { + text-decoration: underline; + } +} diff --git a/src/components/highlight-code/style.less b/src/components/highlight-code/styles/index.less similarity index 85% rename from src/components/highlight-code/style.less rename to src/components/highlight-code/styles/index.less index e60785c0..d7e8e3ba 100644 --- a/src/components/highlight-code/style.less +++ b/src/components/highlight-code/styles/index.less @@ -27,7 +27,6 @@ .code-pre { padding-inline: 12px 32px; position: relative; - background-color: #282c34; border-radius: var(--border-radius-mini); .copy-button { @@ -35,5 +34,13 @@ top: 6px; right: 6px; } + + &.dark { + background-color: #282c34; + } + + &.light { + background-color: rgb(250, 250, 250); + } } } diff --git a/src/components/highlight-code/styles/light.less b/src/components/highlight-code/styles/light.less new file mode 100644 index 00000000..b2979c33 --- /dev/null +++ b/src/components/highlight-code/styles/light.less @@ -0,0 +1,106 @@ +// @ts-ingore +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em; +} + +code.hljs { + padding: 3px 5px; +} + +/* + +Atom One Light by Daniel Gamage +Original One Light Syntax theme from https://github.com/atom/one-light-syntax + +base: #fafafa +mono-1: #383a42 +mono-2: #686b77 +mono-3: #a0a1a7 +hue-1: #0184bb +hue-2: #4078f2 +hue-3: #a626a4 +hue-4: #50a14f +hue-5: #e45649 +hue-5-2: #c91243 +hue-6: #986801 +hue-6-2: #c18401 + +*/ +.code-pre.light { + .hljs { + color: #383a42; + background: #fafafa; + } + + .hljs-comment, + .hljs-quote { + color: #a0a1a7; + font-style: italic; + } + + .hljs-doctag, + .hljs-keyword, + .hljs-formula { + color: #a626a4; + } + + .hljs-section, + .hljs-name, + .hljs-selector-tag, + .hljs-deletion, + .hljs-subst { + color: #e45649; + } + + .hljs-literal { + color: #0184bb; + } + + .hljs-string, + .hljs-regexp, + .hljs-addition, + .hljs-attribute, + .hljs-meta .hljs-string { + color: #50a14f; + } + + .hljs-attr, + .hljs-variable, + .hljs-template-variable, + .hljs-type, + .hljs-selector-class, + .hljs-selector-attr, + .hljs-selector-pseudo, + .hljs-number { + color: #986801; + } + + .hljs-symbol, + .hljs-bullet, + .hljs-link, + .hljs-meta, + .hljs-selector-id, + .hljs-title { + color: #4078f2; + } + + .hljs-built_in, + .hljs-title.class_, + .hljs-class .hljs-title { + color: #c18401; + } + + .hljs-emphasis { + font-style: italic; + } + + .hljs-strong { + font-weight: bold; + } + + .hljs-link { + text-decoration: underline; + } +} diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index c6e24385..255fe11f 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -33,5 +33,7 @@ export default { 'models.search.noresult': 'No related models found', 'models.search.nofiles': 'No available files', 'models.search.networkerror': 'Network connection exception!', - 'models.search.hfvisit': 'Please make sure you can visit' + 'models.search.hfvisit': 'Please make sure you can visit', + 'models.search.unsupport': + 'This model is not supported and may be unusable after deployment.' }; diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index b267e698..1941a112 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -33,5 +33,6 @@ export default { 'models.search.noresult': '未找到相关模型', 'models.search.nofiles': '无可用文件', 'models.search.networkerror': '网络连接异常!', - 'models.search.hfvisit': '请确保您可以访问' + 'models.search.hfvisit': '请确保您可以访问', + 'models.search.unsupport': '暂不支持该模型,部署后可能无法使用' }; diff --git a/src/pages/llmodels/apis/index.ts b/src/pages/llmodels/apis/index.ts index 378f92f3..14d7cec7 100644 --- a/src/pages/llmodels/apis/index.ts +++ b/src/pages/llmodels/apis/index.ts @@ -148,7 +148,10 @@ export async function queryHuggingfaceModels( additionalFields: ['sha'], fetch(url: string, config: any) { try { - return fetch(`${url}&sort=${params.search.sort}`, { + const newUrl = params.search.sort + ? `${url}&sort=${params.search.sort}` + : url; + return fetch(`${newUrl}`, { ...config, signal: options.signal }); diff --git a/src/pages/llmodels/components/file-parts.tsx b/src/pages/llmodels/components/file-parts.tsx new file mode 100644 index 00000000..e52886c9 --- /dev/null +++ b/src/pages/llmodels/components/file-parts.tsx @@ -0,0 +1,26 @@ +import { convertFileSize } from '@/utils'; +import React from 'react'; +import SimpleBar from 'simplebar-react'; +import 'simplebar-react/dist/simplebar.min.css'; + +const FileParts: React.FC<{ + fileList: any[]; +}> = ({ fileList }) => { + return ( + + {fileList.map((file, index) => { + return ( +
+ + {' '} + Part {file.part} of {file.total} + + {convertFileSize(file.size)} +
+ ); + })} +
+ ); +}; + +export default FileParts; diff --git a/src/pages/llmodels/components/hf-model-file.tsx b/src/pages/llmodels/components/hf-model-file.tsx index 46ddd36b..6784ac47 100644 --- a/src/pages/llmodels/components/hf-model-file.tsx +++ b/src/pages/llmodels/components/hf-model-file.tsx @@ -1,14 +1,16 @@ import { convertFileSize } from '@/utils'; +import { InfoCircleOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Col, Empty, Row, Select, Space, Spin, Tag } from 'antd'; +import { Col, Empty, Row, Select, Spin, Tag, Tooltip } from 'antd'; import classNames from 'classnames'; import _ from 'lodash'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import SimpleBar from 'simplebar-react'; import 'simplebar-react/dist/simplebar.min.css'; import { queryHuggingfaceModelFiles } from '../apis'; import FileType from '../config/file-type'; import '../style/hf-model-file.less'; +import FileParts from './file-parts'; import TitleWrapper from './title-wrapper'; interface HFModelFileProps { @@ -18,6 +20,8 @@ interface HFModelFileProps { onSelectFile?: (file: any) => void; } +const pattern = /^(.*)-(\d+)-of-(\d+)\.gguf$/; + const HFModelFile: React.FC = (props) => { const { collapsed, loadingModel } = props; const intl = useIntl(); @@ -44,6 +48,50 @@ const HFModelFile: React.FC = (props) => { setCurrent(item.path); }; + const parseFilename = (filename: string) => { + const match = filename.match(pattern); + + if (match) { + return { + filename: match[1], + part: parseInt(match[2], 10), + total: parseInt(match[3], 10) + }; + } else { + return null; + } + }; + + const generateGroupByFilename = useCallback((list: any[]) => { + const data = _.find(list, (item: any) => { + const parsed = parseFilename(item.path); + return !!parsed; + }); + + // general file + if (!data) { + return list; + } + + const newList = _.map(list, (item: any) => { + const parsed = parseFilename(item.path); + return { + ...item, + ...parsed + }; + }); + + const group = _.groupBy(newList, 'filename'); + + return _.map(group, (value: any[], key: string) => { + return { + path: key, + size: _.sumBy(value, 'size'), + parts: value + }; + }); + }, []); + const handleFetchModelFiles = async () => { if (!props.repo) { setDataSource({ fileList: [], loading: false }); @@ -68,11 +116,15 @@ const HFModelFile: React.FC = (props) => { const list = _.filter(fileList, (file: any) => { return _.endsWith(file.path, '.gguf') || _.includes(file.path, '.gguf'); }); - const sortList = _.sortBy(list, (item: any) => { + + const newList = generateGroupByFilename(list); + + console.log('newList==========', newList); + const sortList = _.sortBy(newList, (item: any) => { return sortType === 'size' ? item.size : item.path; }); setDataSource({ fileList: sortList, loading: false }); - handleSelectModelFile(list[0]); + handleSelectModelFile(sortList[0]); } catch (error) { setDataSource({ fileList: [], loading: false }); handleSelectModelFile({}); @@ -87,22 +139,33 @@ const HFModelFile: React.FC = (props) => { setDataSource({ ...dataSource, fileList: list }); }; - const getModelQuantizationType = (item: any) => { - const name = _.split(item.path, '.').slice(0, -1).join('.'); + const getModelQuantizationType = useCallback((item: any) => { + let itemPath = item.path; + let path = _.split(itemPath, '/').pop(); + + if (!_.endsWith(path, '.gguf') && !_.includes(path, '.gguf')) { + path = `${path}.gguf`; + } + const name = _.split(path, '.').slice(0, -1).join('.'); let quanType = _.toUpper(name.split('-').slice(-1)[0]); if (quanType.indexOf('.') > -1) { quanType = _.split(quanType, '.').pop(); } - console.log('quanType', quanType, FileType[quanType]); if (FileType[quanType] !== undefined) { return ( - + {quanType} ); } return null; - }; + }, []); const handleOnEnter = (e: any, item: any) => { e.stopPropagation(); @@ -143,7 +206,7 @@ const HFModelFile: React.FC = (props) => { >
= (props) => { onKeyDown={(e) => handleOnEnter(e, item)} >
{item.path}
- +
= (props) => { {getModelQuantizationType(item)} - -
+ {item.parts && item.parts.length > 1 && ( + + } + > + + + + {item.parts.length} parts + + + + )} +
); diff --git a/src/pages/llmodels/components/hf-model-item.tsx b/src/pages/llmodels/components/hf-model-item.tsx index 849bc962..e2ecf18f 100644 --- a/src/pages/llmodels/components/hf-model-item.tsx +++ b/src/pages/llmodels/components/hf-model-item.tsx @@ -2,9 +2,11 @@ import { formatNumber } from '@/utils'; import { DownloadOutlined, FolderOutlined, - HeartOutlined + HeartOutlined, + WarningOutlined } from '@ant-design/icons'; -import { Space, Tag } from 'antd'; +import { useIntl } from '@umijs/max'; +import { Tag, Tooltip } from 'antd'; import classNames from 'classnames'; import dayjs from 'dayjs'; import _ from 'lodash'; @@ -21,8 +23,18 @@ interface HFModelItemProps { source?: string; tags?: string[]; } +const warningTask = ['image', 'audio', 'video']; const HFModelItem: React.FC = (props) => { + const intl = useIntl(); + const isExcludeTask = () => { + if (!props.task) { + return false; + } + return _.some(warningTask, (item: string) => { + return props.task?.toLowerCase().includes(item); + }); + }; return (
= (props) => { style={{ color: 'var(--ant-color-text-tertiary)' }} /> {props.title} + {isExcludeTask() && ( + + + + )}
{props.source === modelSourceMap.huggingface_value ? ( - +
{/* {props.task && ( = (props) => { {formatNumber(props.downloads)} - +
) : (
- +
{_.map(props.tags, (tag: string, index: string) => { return ( = (props) => { ); })} - -
- {/* */}
)} diff --git a/src/pages/llmodels/components/model-card.tsx b/src/pages/llmodels/components/model-card.tsx index 9580161c..2e112271 100644 --- a/src/pages/llmodels/components/model-card.tsx +++ b/src/pages/llmodels/components/model-card.tsx @@ -98,24 +98,23 @@ const ModelCard: React.FC<{ return ( <> - {intl.formatMessage({ id: 'models.data.card' })} +
{modelData?.id}
+ {modelData?.id && ( + + + + )}
-
+
{modelData ? (
-
- {modelData.id}{' '} - - - -
{modelData.config?.model_type && ( @@ -132,7 +131,6 @@ const ModelCard: React.FC<{
diff --git a/src/pages/llmodels/components/search-input.tsx b/src/pages/llmodels/components/search-input.tsx index 37ba36f2..6af52770 100644 --- a/src/pages/llmodels/components/search-input.tsx +++ b/src/pages/llmodels/components/search-input.tsx @@ -1,9 +1,8 @@ -import IconFont from '@/components/icon-font'; import hotkeys from '@/config/hotkeys'; import { platformCall } from '@/utils'; import { SearchOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Input, Tag } from 'antd'; +import { Input } from 'antd'; import React, { useRef, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -31,19 +30,6 @@ const SearchInput: React.FC<{ placeholder={intl.formatMessage({ id: 'model.deploy.search.placeholder' })} - suffix={ - !isFocus && ( - - {platform.isMac ? ( - <> - + K - - ) : ( - <>CTRL + K - )} - - ) - } prefix={ <> = (props) => { axiosTokenRef.current?.abort?.(); axiosTokenRef.current = new AbortController(); if (dataSource.loading) return; + const sort = sortType ?? dataSource.sortType; try { setDataSource((pre) => { pre.loading = true; @@ -72,17 +73,19 @@ const SearchModel: React.FC = (props) => { }); setLoadingModel?.(true); cacheRepoOptions.current = []; + const task: any = searchInputRef.current ? '' : 'text-generation'; const params = { search: { query: searchInputRef.current || '', - sort: sortType || dataSource.sortType, - tags: ['gguf'] + sort: sort, + tags: ['gguf'], + task } }; const models = await queryHuggingfaceModels(params, { signal: axiosTokenRef.current.signal }); - const list = _.map(models || [], (item: any) => { + let list = _.map(models || [], (item: any) => { return { ...item, value: item.name, @@ -95,7 +98,7 @@ const SearchModel: React.FC = (props) => { repoOptions: list, loading: false, networkError: false, - sortType: sortType || dataSource.sortType + sortType: sort }); setLoadingModel?.(false); handleOnSelectModel(list[0]); @@ -103,7 +106,7 @@ const SearchModel: React.FC = (props) => { setDataSource({ repoOptions: [], loading: false, - sortType: sortType || dataSource.sortType, + sortType: sort, networkError: error?.message === 'Failed to fetch' }); setLoadingModel?.(false); @@ -188,8 +191,9 @@ const SearchModel: React.FC = (props) => { }; const handleSortChange = (value: string) => { - handleOnSearchRepo(value); + handleOnSearchRepo(value || ''); }; + const renderHFSearch = () => { return ( <> @@ -204,6 +208,7 @@ const SearchModel: React.FC = (props) => {