Compare commits

..
31 Commits
Author SHA1 Message Date
jialin c041d768b2 style: model readme.md table cell align left 2024-09-30 20:12:01 +08:00
jialin 5df6154abe fix: system message can not sync to others 2024-09-30 19:51:01 +08:00
jialin 1fed02d23f fix: modal title does not upate after change language 2024-09-30 19:10:13 +08:00
jialin 2d34d07da4 fix: language does not refresh after toggling 2024-09-30 18:54:48 +08:00
jialin ea25291d75 fix(style): model readme img render 2024-09-30 16:33:38 +08:00
jialin 5f77a0082c fix: models compare system message sync to all 2024-09-30 11:45:42 +08:00
jialin 3978ec683d style: no available models tips 2024-09-29 20:34:11 +08:00
jialin 458f467985 chore: modelscope downloading state 2024-09-29 18:58:12 +08:00
jialin 69ffdd4604 fix(style): markdown viewer text-intent 2024-09-29 16:54:00 +08:00
jialin 5c2503b2a8 chore: add no available models notes 2024-09-29 11:52:50 +08:00
jialin bc921ff835 fix(style): hide table cell content if too long 2024-09-28 19:23:56 +08:00
jialin 197488bff6 fix: chat error message 2024-09-27 19:19:56 +08:00
jialin e48d21c8cb fix: overlayscrollbars.css 2024-09-27 17:13:53 +08:00
jialin 7e992fad99 fix: update lock.yaml 2024-09-27 17:00:22 +08:00
jialin 49c0467428 fix: query modelscope files by recursive 2024-09-27 16:50:09 +08:00
jialin 7e985991cd fix(style): compare sroll 2024-09-26 19:52:54 +08:00
jialin cc7d2ded6c fix: update model backend not show 2024-09-26 14:04:19 +08:00
jialin f813f56c1f chore: vllm params 2024-09-26 10:30:40 +08:00
jialin a698c65a07 fix: playground edit message 2024-09-25 16:17:25 +08:00
jialin 3cd96ba2e8 feat: vllm support 2024-09-25 16:17:25 +08:00
jialin 0652f850d6 fix: modelscope model name 2024-09-25 16:17:25 +08:00
jialin 3ab29f9e01 feat: modelscope model 2024-09-22 14:55:23 +08:00
jialin b041cdc766 style: playground style 2024-09-22 14:55:23 +08:00
jialin c04b9342b8 refactor: playground add comparsion 2024-09-22 14:55:23 +08:00
jialin 1abaa164f5 refactor: playground chat 2024-09-22 14:55:23 +08:00
jialin 4f4f00f0c8 feat: add models compare 2024-09-22 14:55:23 +08:00
jialin 2e5f0d9210 fix: submit message 2024-09-22 14:55:23 +08:00
jialin af077c4706 style: upload image in message input 2024-09-22 14:55:23 +08:00
Arne Sundandjialin a5cebb88ef Fix typos in Resources table headings 2024-09-22 12:10:25 +08:00
gitlawr 6baf8f08c8 docs: add title 2024-09-17 09:34:41 +08:00
gitlawr a00fbd0046 ci: tag format 2024-09-15 16:40:29 +08:00
139 changed files with 6342 additions and 1426 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ on:
branches: branches:
- 'main' - 'main'
tags: tags:
- 'v*.*.*' - '*.*.*'
pull_request: pull_request:
branches: branches:
- 'main' - 'main'
+4
View File
@@ -1,3 +1,7 @@
# GPUStack UI
UI for [GPUStack](https://github.com/gpustack/gpustack).
## Installation ## Installation
1. [Nodejs](https://nodejs.org/en) 16.0+(with NPM) 1. [Nodejs](https://nodejs.org/en) 16.0+(with NPM)
+1 -1
View File
@@ -14,7 +14,7 @@ const isProduction = env === 'production';
const t = Date.now(); const t = Date.now();
export default defineConfig({ export default defineConfig({
proxy: { proxy: {
...proxy('http://192.168.50.166:8080') ...proxy()
}, },
history: { history: {
type: 'hash' type: 'hash'
+1 -1
View File
@@ -1,4 +1,4 @@
const proxyTableList = ['cli', 'v1', 'auth', 'v1-openai', 'version']; const proxyTableList = ['cli', 'v1', 'auth', 'v1-openai', 'version', 'proxy'];
// @ts-ingore // @ts-ingore
export default function createProxyTable(target?: string) { export default function createProxyTable(target?: string) {
-7
View File
@@ -31,13 +31,6 @@ export default [
access: 'canSeeAdmin', access: 'canSeeAdmin',
component: './resources' component: './resources'
}, },
// {
// name: 'usage',
// path: '/usage',
// key: 'usage',
// icon: 'BarChartOutlined',
// component: './usage'
// },
{ {
name: 'apikeys', name: 'apikeys',
path: '/api-keys', path: '/api-keys',
+7 -1
View File
@@ -28,6 +28,10 @@ export default {
Message: { Message: {
contentPadding: '12px 16px' contentPadding: '12px 16px'
}, },
Tooltip: {
colorBgSpotlight: 'rgba(110,110,110,1)'
// sizePopupArrow: 0
},
Slider: { Slider: {
handleSize: 8, handleSize: 8,
handleSizeHover: 8, handleSizeHover: 8,
@@ -42,7 +46,9 @@ export default {
}, },
token: { token: {
colorPrimary: '#007BFF', colorPrimary: '#007BFF',
borderRadius: 8, colorSuccess: '#54cc98',
borderRadius: 4,
borderRadiusSM: 2,
fontSize: 12, fontSize: 12,
motion: true motion: true
} }
+3 -1
View File
@@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"author": "jialin <jialinkuang@126.com>", "author": "jialin",
"scripts": { "scripts": {
"build": "max build", "build": "max build",
"dev": "max dev", "dev": "max dev",
@@ -38,6 +38,8 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "^14.1.0", "marked": "^14.1.0",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"overlayscrollbars": "^2.10.0",
"overlayscrollbars-react": "^0.5.6",
"query-string": "^9.0.0", "query-string": "^9.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
+20
View File
@@ -86,6 +86,12 @@ dependencies:
numeral: numeral:
specifier: ^2.0.6 specifier: ^2.0.6
version: 2.0.6 version: 2.0.6
overlayscrollbars:
specifier: ^2.10.0
version: 2.10.0
overlayscrollbars-react:
specifier: ^0.5.6
version: 0.5.6(overlayscrollbars@2.10.0)(react@18.2.0)
query-string: query-string:
specifier: ^9.0.0 specifier: ^9.0.0
version: 9.0.0 version: 9.0.0
@@ -12606,6 +12612,20 @@ packages:
mem: 5.1.1 mem: 5.1.1
dev: false dev: false
/overlayscrollbars-react@0.5.6(overlayscrollbars@2.10.0)(react@18.2.0):
resolution: {integrity: sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==, tarball: https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.6.tgz}
peerDependencies:
overlayscrollbars: ^2.0.0
react: '>=16.8.0'
dependencies:
overlayscrollbars: 2.10.0
react: 18.2.0
dev: false
/overlayscrollbars@2.10.0:
resolution: {integrity: sha512-diNMeEafWTE0A4GJfwRpdBp2rE/BEvrhptBdBcDu8/UeytWcdCy9Td8tZWnztJeJ26f8/uHCWfPnPUC/dtgJdw==, tarball: https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.10.0.tgz}
dev: false
/p-defer@1.0.0: /p-defer@1.0.0:
resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==, tarball: https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz} resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==, tarball: https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz}
engines: {node: '>=4'} engines: {node: '>=4'}
+17
View File
@@ -38,6 +38,10 @@
margin-left: 20px; margin-left: 20px;
} }
.m-l-40 {
margin-left: 40px;
}
.m-r-10 { .m-r-10 {
margin-right: 10px; margin-right: 10px;
} }
@@ -66,6 +70,10 @@
padding-left: 6px; padding-left: 6px;
} }
.p-l-8 {
padding-left: 8px;
}
.flex { .flex {
display: flex; display: flex;
} }
@@ -85,6 +93,10 @@
align-items: center; align-items: center;
} }
.flex-wrap {
flex-wrap: wrap;
}
.flex-gap-2 { .flex-gap-2 {
display: flex; display: flex;
gap: 2px; gap: 2px;
@@ -98,6 +110,11 @@
opacity: 0.8; opacity: 0.8;
} }
.flex-end {
display: flex;
justify-content: flex-end;
}
.flex-column { .flex-column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+13 -4
View File
@@ -1,4 +1,4 @@
import { Tag, Tooltip } from 'antd'; import { Tag, Tooltip, type TagProps } from 'antd';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React, { import React, {
useCallback, useCallback,
@@ -8,20 +8,26 @@ import React, {
useState useState
} from 'react'; } from 'react';
interface AutoTooltipProps extends React.ComponentProps<typeof Tag> { // type TagProps = React.ComponentProps<typeof Tag>;
interface AutoTooltipProps extends Omit<TagProps, 'title'> {
children: React.ReactNode; children: React.ReactNode;
maxWidth?: number | string; maxWidth?: number | string;
color?: string; color?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
ghost?: boolean; ghost?: boolean;
title?: React.ReactNode;
showTitle?: boolean; showTitle?: boolean;
tooltipProps?: React.ComponentProps<typeof Tooltip>;
} }
const AutoTooltip: React.FC<AutoTooltipProps> = ({ const AutoTooltip: React.FC<AutoTooltipProps> = ({
children, children,
maxWidth = '100%', maxWidth = '100%',
ghost = false, ghost = false,
title,
showTitle = false, showTitle = false,
tooltipProps,
...tagProps ...tagProps
}) => { }) => {
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
@@ -32,7 +38,7 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
const { scrollWidth, clientWidth } = contentRef.current; const { scrollWidth, clientWidth } = contentRef.current;
setIsOverflowing(scrollWidth > clientWidth); setIsOverflowing(scrollWidth > clientWidth);
} }
}, []); }, [contentRef.current]);
const debouncedCheckOverflow = useMemo( const debouncedCheckOverflow = useMemo(
() => debounce(checkOverflow, 200), () => debounce(checkOverflow, 200),
@@ -64,7 +70,10 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
); );
return ( return (
<Tooltip title={isOverflowing || showTitle ? children : ''}> <Tooltip
title={isOverflowing || showTitle ? title || children : ''}
{...tooltipProps}
>
{ghost ? ( {ghost ? (
<div ref={contentRef} style={tagStyle}> <div ref={contentRef} style={tagStyle}>
{children} {children}
+1 -1
View File
@@ -18,7 +18,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({
text, text,
disabled, disabled,
type = 'text', type = 'text',
shape = 'circle', shape = 'default',
fontSize = '14px', fontSize = '14px',
style, style,
size = 'middle' size = 'middle'
+8 -1
View File
@@ -1,10 +1,16 @@
.editor-wrap { .editor-wrap {
border-radius: var(--border-radius-mini); border-radius: var(--border-radius-mini);
overflow: hidden; overflow: hidden;
font-size: 0;
.code-pre {
margin-bottom: 0;
}
.editor-header { .editor-header {
display: flex; display: flex;
padding: 6px 10px; height: 40px;
padding: 0 10px;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background-color: rgb(56, 56, 56); background-color: rgb(56, 56, 56);
@@ -12,6 +18,7 @@
.ant-select-selector { .ant-select-selector {
background-color: rgba(255, 255, 255, 9%) !important; background-color: rgba(255, 255, 255, 9%) !important;
color: rgba(255, 255, 255, 70%); color: rgba(255, 255, 255, 70%);
border-radius: var(--border-radius-2px);
.ant-select-selection-item { .ant-select-selection-item {
color: rgba(255, 255, 255, 70%); color: rgba(255, 255, 255, 70%);
+7 -1
View File
@@ -10,6 +10,11 @@ interface EditorwrapProps {
copyText: string; copyText: string;
defaultValue?: string; defaultValue?: string;
langOptions?: { label: string; value: string }[]; langOptions?: { label: string; value: string }[];
styles?: {
wrapper?: React.CSSProperties;
header?: React.CSSProperties;
content?: React.CSSProperties;
};
onChangeLang?: (value: string) => void; onChangeLang?: (value: string) => void;
} }
const EditorWrap: React.FC<EditorwrapProps> = ({ const EditorWrap: React.FC<EditorwrapProps> = ({
@@ -19,6 +24,7 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
langOptions, langOptions,
onChangeLang, onChangeLang,
defaultValue, defaultValue,
styles = {},
showHeader = true showHeader = true
}) => { }) => {
const handleChangeLang = (value: string) => { const handleChangeLang = (value: string) => {
@@ -52,7 +58,7 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
return null; return null;
}; };
return ( return (
<div className="editor-wrap"> <div className="editor-wrap" style={{ ...styles.wrapper }}>
{renderHeader()} {renderHeader()}
<div className="editor-content">{children}</div> <div className="editor-content">{children}</div>
</div> </div>
+89
View File
@@ -0,0 +1,89 @@
import Editor from '@monaco-editor/react';
import React, { useEffect, useRef, useState } from 'react';
import EditorWrap from '../editor-wrap';
interface ViewerProps {
lang: string;
defaultLang?: string;
langOptions?: Global.BaseOption<string>[];
config?: any;
value: string;
height?: string | number;
theme?: string;
showHeader?: boolean;
}
const ViewerEditor: React.FC<ViewerProps> = (props) => {
const editorRef = useRef<any>(null);
const {
lang,
value,
config,
langOptions,
defaultLang,
height = 380,
showHeader
} = props;
const [langType, setLangType] = useState(defaultLang);
const handleBeforeMount = (monaco: any) => {
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: false,
noSyntaxValidation: false,
diagnosticCodesToIgnore: [80001]
});
};
const handleEditorDidMount = (editor: any, monaco: any) => {
editorRef.current = editor;
console.log('loaded====', editor, monaco);
};
const handleOnChangeLang = (value: string) => {
setLangType(value);
};
const formatCode = () => {
if (editorRef.current) {
setTimeout(() => {
editorRef.current
?.getAction?.('editor.action.formatDocument')
?.run()
.then(() => {
console.log('format success');
});
}, 100);
}
};
useEffect(() => {
formatCode();
setTimeout(() => {
const lineCount = editorRef.current?.getModel().getLineCount(); // 获取总行数
editorRef.current?.revealLine(lineCount); // 滚动到最后一行
}, 100); // 可以调整延时,确保编辑器完全加载
}, [value]);
return (
<EditorWrap
copyText={value}
langOptions={langOptions}
defaultValue={lang}
showHeader={showHeader}
onChangeLang={handleOnChangeLang}
>
<Editor
height={height}
theme="vs-dark"
className="monaco-editor"
defaultLanguage={defaultLang}
language={langType}
value={value}
options={config}
beforeMount={handleBeforeMount}
onMount={handleEditorDidMount}
/>
</EditorWrap>
);
};
export default React.memo(ViewerEditor);
@@ -8,12 +8,21 @@ interface CodeViewerProps {
autodetect?: boolean; autodetect?: boolean;
ignoreIllegals?: boolean; ignoreIllegals?: boolean;
copyable?: boolean; copyable?: boolean;
height?: string | number;
} }
const DarkViewer: React.FC<CodeViewerProps> = (props) => { const DarkViewer: React.FC<CodeViewerProps> = (props) => {
const { code, lang, autodetect, ignoreIllegals, copyable } = props || {}; const {
code,
lang,
autodetect,
ignoreIllegals,
copyable,
height = 'auto'
} = props || {};
return ( return (
<CodeViewer <CodeViewer
height={height}
code={code} code={code}
lang={lang} lang={lang}
theme="dark" theme="dark"
@@ -8,12 +8,21 @@ interface CodeViewerProps {
autodetect?: boolean; autodetect?: boolean;
ignoreIllegals?: boolean; ignoreIllegals?: boolean;
copyable?: boolean; copyable?: boolean;
height?: string | number;
} }
const LightViewer: React.FC<CodeViewerProps> = (props) => { const LightViewer: React.FC<CodeViewerProps> = (props) => {
const { code, lang, autodetect, ignoreIllegals, copyable } = props || {}; const {
code,
lang,
autodetect,
ignoreIllegals,
copyable,
height = 'auto'
} = props || {};
return ( return (
<CodeViewer <CodeViewer
height={height}
code={code} code={code}
lang={lang} lang={lang}
theme="light" theme="light"
+14 -6
View File
@@ -10,6 +10,7 @@ interface CodeViewerProps {
autodetect?: boolean; autodetect?: boolean;
ignoreIllegals?: boolean; ignoreIllegals?: boolean;
copyable?: boolean; copyable?: boolean;
height?: string | number;
theme?: 'light' | 'dark'; theme?: 'light' | 'dark';
} }
const CodeViewer: React.FC<CodeViewerProps> = (props) => { const CodeViewer: React.FC<CodeViewerProps> = (props) => {
@@ -18,7 +19,8 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
lang, lang,
autodetect = true, autodetect = true,
ignoreIllegals = true, ignoreIllegals = true,
copyable = true copyable = true,
height = 'auto'
} = props || {}; } = props || {};
const highlightedCode = useMemo(() => { const highlightedCode = useMemo(() => {
@@ -58,11 +60,17 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
return ( return (
<pre <pre
className={classNames('code-pre', { className={classNames(
dark: props.theme === 'dark', 'code-pre custome-scrollbar custom-scrollbar-horizontal ',
light: props.theme === 'light', {
copyable: copyable dark: props.theme === 'dark',
})} light: props.theme === 'light',
copyable: copyable
}
)}
style={{
height: height
}}
> >
<code <code
className={highlightedCode.className} className={highlightedCode.className}
+23 -5
View File
@@ -1,3 +1,4 @@
import React from 'react';
import CodeViewerDark from './code-viewer-dark'; import CodeViewerDark from './code-viewer-dark';
import CodeViewerLight from './code-viewer-light'; import CodeViewerLight from './code-viewer-light';
import './styles/index.less'; import './styles/index.less';
@@ -7,18 +8,35 @@ const HighlightCode: React.FC<{
lang?: string; lang?: string;
copyable?: boolean; copyable?: boolean;
theme?: 'light' | 'dark'; theme?: 'light' | 'dark';
height?: string | number;
}> = (props) => { }> = (props) => {
const { code, lang = 'bash', copyable = true, theme = 'dark' } = props; const {
code,
lang = 'bash',
copyable = true,
theme = 'dark',
height = 'auto'
} = props;
return ( return (
<div className="high-light-wrapper"> <div className="high-light-wrapper hj-wrapper">
{theme === 'dark' ? ( {theme === 'dark' ? (
<CodeViewerDark lang={lang} code={code} copyable={copyable} /> <CodeViewerDark
lang={lang}
code={code}
copyable={copyable}
height={height}
/>
) : ( ) : (
<CodeViewerLight lang={lang} code={code} copyable={copyable} /> <CodeViewerLight
lang={lang}
code={code}
copyable={copyable}
height={height}
/>
)} )}
</div> </div>
); );
}; };
export default HighlightCode; export default React.memo(HighlightCode);
@@ -1,5 +1,6 @@
.high-light-wrapper { .high-light-wrapper {
text-align: left; text-align: left;
font-size: var(--font-size-base);
.hljs { .hljs {
font-weight: var(--font-weight-normal); font-weight: var(--font-weight-normal);
@@ -7,21 +8,24 @@
padding-block: 1.2em; padding-block: 1.2em;
&::-webkit-scrollbar { &::-webkit-scrollbar {
height: 8px; height: var(--scrollbar-size);
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: rgba(211, 211, 220, 50%); background-color: transparent;
border-radius: 6px; border-radius: 4px;
&:hover {
background-color: rgba(190, 190, 190, 100%);
}
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background-color: transparent; background-color: transparent;
} }
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb);
border-radius: 4px;
}
}
} }
.code-pre { .code-pre {
@@ -29,6 +33,10 @@
position: relative; position: relative;
border-radius: var(--border-radius-mini); border-radius: var(--border-radius-mini);
code {
line-height: 1.5;
}
&.copyable { &.copyable {
padding-inline: 12px 32px; padding-inline: 12px 32px;
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons'; import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({ const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_4613488_3n3aubqzylv.js' scriptUrl: '//at.alicdn.com/t/c/font_4613488_4jkdkc8jcf7.js'
}); });
export default IconFont; export default IconFont;
+5 -3
View File
@@ -52,7 +52,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
// has duplicate key // has duplicate key
const duplicates = _.filter( const duplicates = _.filter(
labelList, labelList,
(item: Global.BaseListItem) => val && val === item.key (item: Global.BaseListItem<string>) => val && val === item.key
); );
if (duplicates.length > 1) { if (duplicates.length > 1) {
setOpen(true); setOpen(true);
@@ -77,7 +77,8 @@ const LabelItem: React.FC<LabelItemProps> = ({
title={intl.formatMessage({ id: 'resources.table.key.tips' })} title={intl.formatMessage({ id: 'resources.table.key.tips' })}
> >
<SealInput.Input <SealInput.Input
label="Key" checkStatus="success"
label={intl.formatMessage({ id: 'common.input.key' })}
value={label.key} value={label.key}
onChange={handleOnKeyChange} onChange={handleOnKeyChange}
onBlur={handleKeyOnBlur} onBlur={handleKeyOnBlur}
@@ -89,7 +90,8 @@ const LabelItem: React.FC<LabelItemProps> = ({
<div className="label-value"> <div className="label-value">
{valueAddon ?? ( {valueAddon ?? (
<SealInput.Input <SealInput.Input
label="Value" checkStatus={label.value ? 'success' : ''}
label={intl.formatMessage({ id: 'common.input.value' })}
value={label.value} value={label.value}
onChange={handleOnValueChange} onChange={handleOnValueChange}
></SealInput.Input> ></SealInput.Input>
@@ -5,6 +5,7 @@
border: 1px solid var(--ant-color-border); border: 1px solid var(--ant-color-border);
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);
display: flex; display: flex;
width: 100%;
flex-direction: column; flex-direction: column;
:global { :global {
+99
View File
@@ -0,0 +1,99 @@
import AutoComplete from '@/components/seal-form/auto-complete';
import _ from 'lodash';
import React from 'react';
interface HintInputProps {
value: string;
label?: string;
onChange: (value: string) => void;
placeholder?: string;
sourceOptions?: Global.HintOptions[];
}
const matchReg = /[^=]+=[^=]*$/;
const HintInput: React.FC<HintInputProps> = (props) => {
const { value, label, onChange, sourceOptions } = props;
const cursorPosRef = React.useRef(0);
const contextBeforeCursorRef = React.useRef('');
const [options, setOptions] = React.useState<
Array<Global.BaseOption<string>>
>([]);
const generateOptions = (context: string) => {
if (!context) {
setOptions(sourceOptions || []);
return;
}
const match = context.match(matchReg);
if (!match) {
const list = _.filter(sourceOptions, (item: Global.HintOptions) =>
item.label.includes(context)
);
setOptions(list);
return;
}
const [key, value] = _.split(match[0], '=');
const data = _.find(
sourceOptions,
(item: Global.HintOptions) => item.label === key
);
if (!data) {
setOptions([]);
return;
}
const list = _.filter(data.opts, (item: Global.BaseOption<string>) =>
item.label.includes(value)
);
setOptions(list);
};
const replaceLastEqual = (value: string) => {
const matchStr = contextBeforeCursorRef.current.match(matchReg);
if (matchStr) {
const arr = _.split(matchStr[0], '=');
onChange(`${arr[0]}=${value}`);
} else {
onChange(value?.trim());
}
};
const getContextBeforeCursor = _.debounce((e: any) => {
cursorPosRef.current = e.target.selectionStart;
contextBeforeCursorRef.current = e.target.value.slice(
0,
cursorPosRef.current
);
generateOptions(contextBeforeCursorRef.current);
}, 100);
const handleInput = (e: any) => {
getContextBeforeCursor(e);
onChange(e.target.value?.trim());
};
const handleOnChange = (value: string) => {
onChange(value?.trim());
};
const handleOnSelect = (value: string) => {
replaceLastEqual(value);
setOptions([]);
};
return (
<AutoComplete
placeholder={props.placeholder}
defaultActiveFirstOption={true}
value={value}
onInput={handleInput}
onSelect={handleOnSelect}
onFocus={getContextBeforeCursor}
label={label}
options={options}
style={{ width: '100%' }}
/>
);
};
export default React.memo(HintInput);
+110
View File
@@ -0,0 +1,110 @@
import { PlusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import _ from 'lodash';
import React from 'react';
import Wrapper from '../label-selector/wrapper';
import ListItem from './list-item';
interface ListInputProps {
dataList: string[];
label: string;
description?: string;
btnText?: string;
options?: Global.HintOptions[];
placeholder?: string;
onChange: (data: string[]) => void;
}
const ListInput: React.FC<ListInputProps> = (props) => {
const intl = useIntl();
const { dataList, label, description, onChange, btnText, options } = props;
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
const countRef = React.useRef(0);
const buttonRef = React.useRef<HTMLButtonElement>(null);
const updateCountRef = () => {
countRef.current = countRef.current + 1;
};
const handleOnRemove = (index: number) => {
const values = _.cloneDeep(list);
values.splice(index, 1);
const valueList = _.map(values, 'value').filter((val: string) => !!val);
setList(values);
onChange(valueList);
};
const handleOnChange = (value: string, index: number) => {
const values = _.cloneDeep(list);
values[index].value = value;
const valueList = _.map(values, 'value').filter((val: string) => !!val);
setList(values);
onChange(valueList);
};
const handleOnAdd = () => {
updateCountRef();
const values = _.cloneDeep(list);
values.push({
value: '',
uid: countRef.current
});
setList(values);
setTimeout(() => {
buttonRef.current?.scrollIntoView?.({ behavior: 'smooth' });
}, 100);
};
React.useEffect(() => {
const valueList = _.map(list, 'value').filter((val: string) => !!val);
if (!_.isEqual(valueList, dataList)) {
const values = _.map(dataList, (value: string) => {
updateCountRef();
return {
value,
uid: countRef.current
};
});
setList(values);
}
}, [dataList]);
return (
<Wrapper label={label} description={description}>
<>
{_.map(list, (item: any, index: number) => {
return (
<ListItem
placeholder={props.placeholder}
options={options}
key={item.uid}
value={item.value}
onRemove={() => handleOnRemove(index)}
onChange={(val) => handleOnChange(val, index)}
/>
);
})}
<div className="flex justify-center">
<Button
ref={buttonRef}
type="text"
block
style={{
marginTop: 16,
backgroundColor: 'var(--ant-color-fill-secondary)'
}}
onClick={handleOnAdd}
>
<PlusOutlined className="font-size-14" />{' '}
{intl.formatMessage({
id: btnText
})}
</Button>
</div>
</>
</Wrapper>
);
};
export default React.memo(ListInput);
+45
View File
@@ -0,0 +1,45 @@
// import AutoComplete from '@/components/seal-form/auto-complete';
import { MinusOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import React from 'react';
import HintInput from './hint-input';
import './styles/list-item.less';
interface LabelItemProps {
onRemove: () => void;
onChange: (value: string) => void;
value: string;
label?: string;
placeholder?: string;
options?: Global.HintOptions[];
}
const ListItem: React.FC<LabelItemProps> = (props) => {
const { onRemove, onChange, label, value, options } = props;
const handleOnChange = (value: any) => {
onChange(value);
};
return (
<div className="list-item">
<HintInput
value={value}
onChange={handleOnChange}
label={label}
sourceOptions={options}
placeholder={props.placeholder}
/>
<Button
size="small"
className="btn"
type="default"
shape="circle"
icon={<MinusOutlined />}
onClick={onRemove}
/>
</div>
);
};
export default React.memo(ListItem);
@@ -0,0 +1,15 @@
.list-item {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
margin-bottom: 12px;
.field-wrapper {
flex: 1;
}
.btn {
margin-left: 10px;
}
}
@@ -0,0 +1,15 @@
.list-item {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
margin-bottom: 12px;
.field-wrapper {
flex: 1;
}
.btn {
margin-left: 10px;
}
}
+4 -4
View File
@@ -26,14 +26,14 @@
} }
.xterm { .xterm {
height: 100% !important; // height: 100% !important;
.xterm-viewport { .xterm-viewport {
overflow-y: auto !important; overflow-y: auto !important;
// custom scrollbar
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 8px; width: var(--scrollbar-size);
height: 6px; height: var(--scrollbar-size);
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
+11 -6
View File
@@ -26,7 +26,7 @@ const LogsViewer: React.FC<LogsViewerProps> = (props) => {
const [logs, setLogs] = useState(''); const [logs, setLogs] = useState('');
const size = useSize(scroller); const size = useSize(scroller);
const throttleScroll = _.debounce(() => { const throttleScroll = _.throttle(() => {
termRef.current?.scrollToBottom?.(); termRef.current?.scrollToBottom?.();
}, 100); }, 100);
@@ -36,6 +36,7 @@ const LogsViewer: React.FC<LogsViewerProps> = (props) => {
termRef.current?.clear?.(); termRef.current?.clear?.();
cacheDataRef.current = data; cacheDataRef.current = data;
termRef.current?.write?.(data); termRef.current?.write?.(data);
setLogs(data);
}, 100), }, 100),
[] []
); );
@@ -78,11 +79,11 @@ const LogsViewer: React.FC<LogsViewerProps> = (props) => {
termRef.current.open(termwrapRef.current); termRef.current.open(termwrapRef.current);
// add event // add event
termRef.current.onLineFeed((e: any) => { // termRef.current.onLineFeed((e: any) => {
if (cacheDataRef.current) { // if (cacheDataRef.current) {
throttleScroll(); // throttleScroll();
} // }
}); // });
}; };
const handleResize = _.throttle(() => { const handleResize = _.throttle(() => {
@@ -111,6 +112,10 @@ const LogsViewer: React.FC<LogsViewerProps> = (props) => {
} }
}, [size]); }, [size]);
useEffect(() => {
throttleScroll();
}, [logs]);
return ( return (
<div className="logs-viewer-wrap-w2"> <div className="logs-viewer-wrap-w2">
<div className="wrap" style={{ height: height }} ref={scroller}> <div className="wrap" style={{ height: height }} ref={scroller}>
+100
View File
@@ -0,0 +1,100 @@
.markdown-viewer {
font-family: var(--font-family) !important;
.hr {
border: none;
height: 1px;
background-color: var(--ant-color-split);
}
p {
margin-bottom: 0;
}
h1,
h2,
h3,
h4 {
font-weight: 700;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-small);
}
.hj-wrapper {
margin-top: 16px;
}
strong {
font-weight: var(--font-weight-bold);
}
ul {
margin-bottom: 0;
padding-left: 20px;
line-height: 2;
}
ol {
margin-bottom: 0;
padding-left: 20px;
line-height: 2;
}
a * {
color: inherit;
}
table {
width: 100%;
th {
font-weight: var(--font-weight-bold);
line-height: 1.5;
padding-inline: 6px;
border-bottom: 1px solid var(--ant-color-split);
word-break: break-word;
text-align: left !important;
}
td {
line-height: 1.5;
padding-inline: 6px;
border-bottom: 1px solid var(--ant-color-split);
word-break: break-word;
text-align: left !important;
}
div {
display: inline;
}
}
a {
div {
display: inline;
}
}
img {
max-width: 100%;
height: auto;
}
video {
max-width: 100%;
height: auto;
border-radius: var(--border-radius-base);
}
audio {
max-width: 100%;
height: auto;
border-radius: var(--border-radius-base);
}
.item-token {
color: inherit;
font-size: inherit;
font-weight: inherit;
}
}
+163 -8
View File
@@ -1,26 +1,181 @@
import { marked, Tokens } from 'marked'; import { EyeOutlined } from '@ant-design/icons';
import React from 'react'; import { Image, Typography } from 'antd';
import { unescape } from 'lodash';
import { TokensList, marked } from 'marked';
import React, { Fragment, useCallback, useEffect } from 'react';
import HighlightCode from '../highlight-code';
import './index.less';
const { Text, Link, Paragraph } = Typography;
interface MarkdownViewerProps { interface MarkdownViewerProps {
content: string; content: string;
height?: string; height?: string;
theme?: 'light' | 'dark';
generateImgLink?: (src: string) => string;
} }
const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
content, content,
height = 'auto' generateImgLink,
height = 'auto',
theme = 'light'
}) => { }) => {
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
const tokens = marked.lexer(content);
const reDefineTypes = [
'code',
'link',
'hr',
'heading',
'paragraph',
'codespan',
'strong',
'text',
'image',
'em',
'list',
'list_item',
'br',
'html',
'escape'
];
renderer.link = ({ href, title, text }: Tokens.Link) => { renderer.link = ({ href, title, text }) => {
return `<a href="${href}" title="${title || ''}" target="_blank" rel="noopener noreferrer">${text}</a>`; return `<a href="${href}" title="${title || ''}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}; };
const isValidURL = useCallback((url: string) => {
const pattern = /^(https?:\/\/|\/\/)([^\s/$.?#].[^\s]*)$/;
return pattern.test(url);
}, []);
const renderItem = useCallback(
(token: any, render: any) => {
if (!reDefineTypes.includes(token.type)) {
return (
<span
dangerouslySetInnerHTML={{
__html: marked.parser([token], { renderer })
}}
></span>
);
}
let htmlstr: any = null;
let child: any = null;
if (token.tokens?.length) {
child = render?.(token.tokens as TokensList, render);
}
const text = child ? child : unescape(token.text);
if (token.type === 'escape') {
htmlstr = text;
}
if (token.type === 'html') {
htmlstr = <div dangerouslySetInnerHTML={{ __html: token.text }} />;
}
if (token.type === 'list') {
htmlstr = token.order ? (
<ol>{render?.(token.items, render)}</ol>
) : (
<ul>{render?.(token.items, render)}</ul>
);
}
if (token.type === 'list_item') {
htmlstr = <li>{text}</li>;
}
if (token.type === 'br') {
htmlstr = <br />;
}
if (token.type === 'em') {
htmlstr = <em>{text}</em>;
}
if (token.type === 'image') {
let href = token.href;
if (!isValidURL(token.href)) {
href = generateImgLink ? generateImgLink(token.href) : token.href;
}
htmlstr = (
<Image
src={href}
alt={token.text}
preview={{
mask: <EyeOutlined />
}}
/>
);
}
if (token.type === 'text') {
htmlstr = text;
}
if (token.type === 'codespan') {
htmlstr = <Text code>{text}</Text>;
}
if (token.type === 'strong') {
htmlstr = <Text strong>{text}</Text>;
}
if (token.type === 'heading') {
htmlstr = <Typography.Title level={4}>{text}</Typography.Title>;
}
if (token.type === 'paragraph') {
htmlstr = <Paragraph> {text}</Paragraph>;
}
if (token.type === 'code') {
htmlstr = (
<HighlightCode theme={theme} code={token.text} lang={token.lang} />
);
}
if (token.type === 'link') {
htmlstr = (
<Link
href={token.href}
title={token.title || ''}
target="_blank"
rel="noopener noreferrer"
>
{text}
</Link>
);
}
if (token.type === 'hr') {
htmlstr = <hr className="hr" />;
}
return htmlstr;
},
[generateImgLink]
);
const renderTokens = (tokens: TokensList): any => {
return tokens?.map((token: any, index: number) => {
return <Fragment key={index}>{renderItem(token, renderTokens)}</Fragment>;
});
};
useEffect(() => {
if (!content) {
return;
}
const imgs = document.querySelectorAll('.markdown-viewer img');
imgs.forEach((img) => {
const src = img.getAttribute('src');
if (src && !isValidURL(src)) {
img.setAttribute('src', generateImgLink ? generateImgLink(src) : src);
}
});
}, [content, generateImgLink]);
return ( return (
<div style={{ height, overflow: 'auto' }}> <div
<div style={{ height }}
dangerouslySetInnerHTML={{ __html: marked(content, { renderer }) }} className="markdown-viewer custom-scrollbar-horizontal"
/> >
{renderTokens(tokens)}
</div> </div>
); );
}; };
@@ -0,0 +1 @@
export default {};
+16
View File
@@ -0,0 +1,16 @@
const htmlUnescapes: Record<string, string> = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'"
};
const reEscapedHtml = /&(?:amp|lt|gt|quot|#(?:0+)?39);/g;
const reHasEscapedHtml = RegExp(reEscapedHtml.source);
export const unescape = (str = '') => {
return reHasEscapedHtml.test(str)
? str.replace(reEscapedHtml, (entity) => htmlUnescapes[entity] || "'")
: str;
};
+5 -5
View File
@@ -4,9 +4,9 @@ import { useEffect, useRef, useState } from 'react';
import Wrapper from './components/wrapper'; import Wrapper from './components/wrapper';
import { SealFormItemProps } from './types'; import { SealFormItemProps } from './types';
const SealAutoComplete: React.FC<AutoCompleteProps & SealFormItemProps> = ( const SealAutoComplete: React.FC<
props AutoCompleteProps & SealFormItemProps & { onInput?: (e: Event) => void }
) => { > = (props) => {
const { const {
label, label,
placeholder, placeholder,
@@ -75,7 +75,7 @@ const SealAutoComplete: React.FC<AutoCompleteProps & SealFormItemProps> = (
<Wrapper <Wrapper
status={status} status={status}
extra={extra} extra={extra}
label={label || (placeholder as string)} label={label}
isFocus={isFocus} isFocus={isFocus}
required={required} required={required}
description={description} description={description}
@@ -87,7 +87,7 @@ const SealAutoComplete: React.FC<AutoCompleteProps & SealFormItemProps> = (
{...rest} {...rest}
ref={inputRef} ref={inputRef}
placeholder={ placeholder={
isFocus ? ( isFocus || !label ? (
<span style={{ paddingLeft: '12px' }}>{placeholder}</span> <span style={{ paddingLeft: '12px' }}>{placeholder}</span>
) : ( ) : (
'' ''
@@ -1,5 +1,5 @@
:local(.wrapper-box) { :local(.wrapper-box) {
@borderRadius: 8px; @borderRadius: var(--border-radius-base);
position: relative; position: relative;
display: flex; display: flex;
@@ -95,6 +95,7 @@
align-items: flex-start; align-items: flex-start;
flex: 1; flex: 1;
padding-block: 20px 0; padding-block: 20px 0;
max-width: 100%;
&.no-wrapper-style { &.no-wrapper-style {
padding-block: 0; padding-block: 0;
+6 -2
View File
@@ -28,7 +28,11 @@ const Wrapper: React.FC<WrapperProps> = ({
}) => { }) => {
return ( return (
<div <div
style={{ ...style }} style={{
padding: '0 calc(var(--ant-padding-sm) - 5px)',
width: '100%',
...style
}}
className={classNames( className={classNames(
wrapperStyle.wrapper, wrapperStyle.wrapper,
wrapperStyle[`validate-status-${status}`], wrapperStyle[`validate-status-${status}`],
@@ -51,8 +55,8 @@ const Wrapper: React.FC<WrapperProps> = ({
></LabelInfo> ></LabelInfo>
</label> </label>
<div <div
className="child-inner"
style={{ style={{
padding: '0 calc(var(--ant-padding-sm) - 5px)',
width: '100%' width: '100%'
}} }}
> >
+1 -1
View File
@@ -62,7 +62,7 @@ const SealInputNumber: React.FC<InputNumberProps & SealFormItemProps> = (
return ( return (
<Wrapper <Wrapper
status={status} status={status}
label={label || (placeholder as string)} label={label}
isFocus={isFocus} isFocus={isFocus}
required={required} required={required}
description={description} description={description}
+1 -1
View File
@@ -68,7 +68,7 @@ const SealInputSearch: React.FC<SearchProps & SealFormItemProps> = (props) => {
return ( return (
<Wrapper <Wrapper
status={status} status={status}
label={label || (placeholder as string)} label={label}
isFocus={isFocus} isFocus={isFocus}
required={required} required={required}
description={description} description={description}
+1 -1
View File
@@ -57,7 +57,7 @@ const SealPassword: React.FC<InputProps & SealFormItemProps> = (props) => {
return ( return (
<Wrapper <Wrapper
status={status} status={status}
label={label || (placeholder as string)} label={label}
isFocus={isFocus} isFocus={isFocus}
required={required} required={required}
description={description} description={description}
+4 -3
View File
@@ -17,6 +17,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
isInFormItems = true, isInFormItems = true,
variant, variant,
addAfter, addAfter,
checkStatus,
trim = true, trim = true,
...rest ...rest
} = props; } = props;
@@ -70,8 +71,8 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
return ( return (
<Wrapper <Wrapper
status={status} status={checkStatus || status}
label={label || (placeholder as string)} label={label}
isFocus={isFocus} isFocus={isFocus}
required={required} required={required}
description={description} description={description}
@@ -82,7 +83,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
> >
<Input <Input
{...rest} {...rest}
placeholder={isFocus ? placeholder : ''} placeholder={isFocus || !label ? placeholder : ''}
ref={inputRef} ref={inputRef}
autoComplete="off" autoComplete="off"
onInput={handleInput} onInput={handleInput}
+1 -1
View File
@@ -60,7 +60,7 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
return ( return (
<Wrapper <Wrapper
status={status} status={status}
label={label || (placeholder as string)} label={label}
isFocus={isFocus} isFocus={isFocus}
required={required} required={required}
description={description} description={description}
+1 -1
View File
@@ -79,7 +79,7 @@ const SealTextArea: React.FC<TextAreaProps & SealFormItemProps> = (props) => {
return ( return (
<Wrapper <Wrapper
status={status} status={status}
label={label || (placeholder as string)} label={label}
isFocus={isFocus} isFocus={isFocus}
required={required} required={required}
description={description} description={description}
+1
View File
@@ -9,4 +9,5 @@ export interface SealFormItemProps {
addAfter?: React.ReactNode; addAfter?: React.ReactNode;
loading?: React.ReactNode; loading?: React.ReactNode;
trim?: boolean; trim?: boolean;
checkStatus?: 'success' | 'error' | 'warning' | '';
} }
@@ -79,7 +79,7 @@ const TableRow: React.FC<
}, []); }, []);
const renderChildrenData = () => { const renderChildrenData = () => {
return renderChildren?.(childrenData); return renderChildren?.(childrenData, record);
}; };
const handlePolling = async () => { const handlePolling = async () => {
@@ -5,6 +5,13 @@
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
height: 68px; height: 68px;
word-break: break-word;
min-width: 20px;
overflow: hidden;
.cell-content {
max-width: 100%;
}
&-left { &-left {
justify-content: flex-start; justify-content: flex-start;
+1 -1
View File
@@ -52,7 +52,7 @@ export interface SealTableProps {
onCell?: (record: any, dataIndex: string) => void; onCell?: (record: any, dataIndex: string) => void;
onSort?: (dataIndex: string, order: 'ascend' | 'descend') => void; onSort?: (dataIndex: string, order: 'ascend' | 'descend') => void;
onExpand?: (expanded: boolean, record: any, rowKey: any) => void; onExpand?: (expanded: boolean, record: any, rowKey: any) => void;
renderChildren?: (data: any) => React.ReactNode; renderChildren?: (data: any, parent?: any) => React.ReactNode;
loadChildren?: (record: any) => Promise<any[]>; loadChildren?: (record: any) => Promise<any[]>;
loadChildrenAPI?: (record: any) => string; loadChildrenAPI?: (record: any) => string;
contentRendered?: () => void; contentRendered?: () => void;
+1 -1
View File
@@ -52,7 +52,7 @@ const StatusTag: React.FC<StatusTagProps> = ({
const renderContent = () => { const renderContent = () => {
const percent = download?.percent || 0; const percent = download?.percent || 0;
if (download && percent > 0 && percent < 100) { if (download && percent > 0 && percent <= 100) {
return ( return (
<> <>
<span className="progress">{download?.percent || 0}%</span> <span className="progress">{download?.percent || 0}%</span>
-1
View File
@@ -37,7 +37,6 @@
} }
.ant-input { .ant-input {
/* 只隐藏垂直滚动条轨道 */
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
width: 0 !important; width: 0 !important;
color: transparent; color: transparent;
+10 -4
View File
@@ -22,14 +22,20 @@ declare namespace Global {
id: number; id: number;
} }
interface BaseListItem { interface BaseListItem<T> {
key: string; key: string;
value: string | number; value: T;
} }
interface BaseOption { interface BaseOption<T> {
label: string; label: string;
value: string | number; value: T;
}
interface HintOptions {
label: string;
value: string;
opts?: Array<BaseOption<string>>;
} }
type SearchParams = Pagination & { search?: string }; type SearchParams = Pagination & { search?: string };
+24 -20
View File
@@ -1,23 +1,27 @@
import { platformCall } from '@/utils'; import { platformCall } from '@/utils';
const platform = platformCall(); const platform = platformCall();
const KeybindingsMap = { const KeybindingsMap = {
CREATE: ['alt+ctrl+N', 'alt+meta+N'], CREATE: ['Alt+Ctrl+N', 'Alt+Meta+N'],
CLEAR: ['alt+ctrl+W', 'alt+meta+W'], CLEAR: ['Alt+Ctrl+K', 'Alt+Meta+K'],
RIGHT: ['ctrl+RIGHT', 'meta+RIGHT'], RIGHT: ['Ctrl+RIGHT', 'Meta+RIGHT'],
SAVE: ['ctrl+S', 'meta+S'], SAVE: ['Ctrl+S', 'Meta+S'],
SUBMIT: ['ctrl+enter', 'meta+enter'], SUBMIT: ['Ctrl+Enter', 'Meta+Enter'],
SAVEAS: ['alt+ctrl+S', 'alt+meta+S'], SAVEAS: ['Alt+Ctrl+S', 'Alt+Meta+S'],
OPEN: ['alt+ctrl+O', 'alt+meta+O'], OPEN: ['Alt+Ctrl+O', 'Alt+Meta+O'],
CANCEL: ['ctrl+W', 'meta+W'], CANCEL: ['Ctrl+W', 'Meta+W'],
DELETE: ['delete'], DELETE: ['delete'],
COPY: ['ctrl+C', 'meta+C'], COPY: ['Ctrl+C', 'Meta+C'],
REFRESH: ['ctrl+R', 'meta+R'], REFRESH: ['Ctrl+R', 'Meta+R'],
EDIT: ['ctrl+E', 'meta+E'], EDIT: ['Ctrl+E', 'Meta+E'],
SEARCH: ['ctrl+K', 'meta+K'], SEARCH: ['Ctrl+K', 'Meta+K'],
RESET: ['alt+ctrl+R', 'alt+meta+R'], RESET: ['Alt+Ctrl+R', 'Alt+Meta+R'],
INPUT: ['ctrl+K', 'meta+K'], INPUT: ['Ctrl+K', 'Meta+K'],
NEW1: ['ctrl+1', 'meta+1'], NEW1: ['Ctrl+1', 'Meta+1'],
NEW2: ['ctrl+2', 'meta+2'] NEW2: ['Ctrl+2', 'Meta+2'],
NEW3: ['Ctrl+3', 'Meta+3'],
FOCUS: ['/', '/'],
ADD: ['Alt+Ctrl+Enter', 'Alt+Meta+Enter']
}; };
type KeyBindingType = keyof typeof KeybindingsMap; type KeyBindingType = keyof typeof KeybindingsMap;
@@ -36,11 +40,11 @@ const KeybiningList: KeybindingValue[] = Object.entries(KeybindingsMap).map(
keybinding: keybinding, keybinding: keybinding,
command: key, command: key,
textKeybinding: platform.isMac textKeybinding: platform.isMac
? keybinding.replace('meta', 'Command').replace('alt', 'Option') ? keybinding.replace('Meta', 'Cmd').replace('Alt', 'Option')
: keybinding.replace('ctrl', 'Ctrl'), : keybinding.replace('Ctrl', 'Ctrl'),
iconKeybinding: platform.isMac iconKeybinding: platform.isMac
? keybinding.replace('meta', '⌘').replace('alt', '⌥') ? keybinding.replace('Meta', '⌘').replace('Alt', '⌥')
: keybinding.replace('ctrl', 'Ctrl') : keybinding.replace('Ctrl', 'Ctrl')
} as KeybindingValue; } as KeybindingValue;
} }
); );
+137 -15
View File
@@ -2,15 +2,22 @@
@import url('src/assets/styles/menu.less'); @import url('src/assets/styles/menu.less');
html { html {
--font-family: 'noto sans', sans-serif;
--ant-color-text-secondary: rgba(0, 0, 0, 65%); --ant-color-text-secondary: rgba(0, 0, 0, 65%);
--ant-color-text-tertiary: rgba(0, 0, 0, 45%); --ant-color-text-tertiary: rgba(0, 0, 0, 45%);
--ant-color-text-quaternary: rgba(0, 0, 0, 25%); --ant-color-text-quaternary: rgba(0, 0, 0, 25%);
--ant-color-fill: rgba(0, 0, 0, 15%); --ant-color-fill: rgba(223, 168, 168, 15%);
--ant-color-fill-secondary: rgba(0, 0, 0, 6%); --ant-color-fill-secondary: rgba(0, 0, 0, 6%);
--ant-color-fill-tertiary: rgba(0, 0, 0, 4%); --ant-color-fill-tertiary: rgba(0, 0, 0, 4%);
--ant-color-fill-quaternary: rgba(0, 0, 0, 2%); --ant-color-fill-quaternary: rgba(0, 0, 0, 2%);
--color-text-light-1: rgba(255, 255, 255, 90%);
--color-fill-1: var(--ant-color-fill-tertiary); --color-fill-1: var(--ant-color-fill-tertiary);
--color-scrollbar-thumb: rgba(193, 193, 193, 80%); --color-scrollbar-thumb: rgba(193, 193, 193, 80%);
--scrollbar-size: 6px;
--scrollbar-handle-bg: rgba(0, 0, 0, 44%);
--scrollbar-handle-hover-bg: rgba(0, 0, 0, 55%);
--color-editor-dark: #282c34;
--color-editor-light: #fafafa;
--color-scrollbar-track: var(--ant-color-fill-tertiary); --color-scrollbar-track: var(--ant-color-fill-tertiary);
// --color-fill-1: #fff; // --color-fill-1: #fff;
--ant-color-text: #000; --ant-color-text: #000;
@@ -21,12 +28,14 @@ html {
--color-logs-text: #d4d4d4; --color-logs-text: #d4d4d4;
--layout-content-blockpadding: 32px; --layout-content-blockpadding: 32px;
--layout-content-inlinepadding: 32px; --layout-content-inlinepadding: 32px;
--menu-border-radius-base: 8px; --border-radius-modal: 6px;
--border-radius-base: 8px; --menu-border-radius-base: 4px;
--border-radius-base: 4px;
--border-radius-middle: 20px; --border-radius-middle: 20px;
--border-radius-small: 8px; --border-radius-small: 4px;
--border-radius-mdium: 6px; --border-radius-mdium: 4px;
--border-radius-mini: 4px; --border-radius-mini: 4px;
--border-radius-2px: 2px;
--color-white-1: rgba(255, 255, 255, 100%); --color-white-1: rgba(255, 255, 255, 100%);
--color-fill-sider: #f4f5f4; --color-fill-sider: #f4f5f4;
--font-weight-normal: 500; --font-weight-normal: 500;
@@ -36,14 +45,15 @@ html {
--color-text-3: rgba(0, 0, 0, 45%); --color-text-3: rgba(0, 0, 0, 45%);
--color-text-2: rgba(0, 0, 0, 65%); --color-text-2: rgba(0, 0, 0, 65%);
--color-bg-light-1: #e6f6ff; --color-bg-light-1: #e6f6ff;
--font-size-small: 13px;
--font-size-base: 12px; --font-size-base: 12px;
--font-size-large: 16px; --font-size-large: 16px;
--font-size-middle: 14px; --font-size-middle: 14px;
--table-td-radius: 8px; --table-td-radius: 4px;
--checkbox-border-radius: 4px; --checkbox-border-radius: 2px;
--ant-table-cell-padding-inline: 16px; --ant-table-cell-padding-inline: 16px;
--ant-table-cell-padding-block: 8px; --ant-table-cell-padding-block: 6px;
--ant-table-header-border-radius: 8px; --ant-table-header-border-radius: 4px;
--ant-table-header-split-color: #f0f0f0; --ant-table-header-split-color: #f0f0f0;
--ant-table-row-selected-bg: #e6f6ff; --ant-table-row-selected-bg: #e6f6ff;
--ant-table-row-selected-hover-bg: #e6f6ff; --ant-table-row-selected-hover-bg: #e6f6ff;
@@ -59,7 +69,7 @@ html {
--ant-input-active-border-color: #007bff; --ant-input-active-border-color: #007bff;
--ant-input-hover-border-color: #2997ff; --ant-input-hover-border-color: #2997ff;
--box-shadow-base: 0 4px 6px rgba(227, 232, 240, 70%); --box-shadow-base: 0 4px 6px rgba(227, 232, 240, 70%);
--ant-border-radius-lg: 8px; --ant-border-radius-lg: 4px;
--ant-menu-item-color: var(--color-text-1); --ant-menu-item-color: var(--color-text-1);
--color-selected-bg: rgba(230, 230, 230, 88%); --color-selected-bg: rgba(230, 230, 230, 88%);
// --box-shadow-base: none; // --box-shadow-base: none;
@@ -75,7 +85,7 @@ html {
&.ant-menu-css-var { &.ant-menu-css-var {
// --ant-menu-item-selected-bg: var(--color-white-1); // --ant-menu-item-selected-bg: var(--color-white-1);
--ant-menu-item-border-radius: 8px; --ant-menu-item-border-radius: 4px;
--ant-menu-item-selected-color: var(--ant-color-primary); --ant-menu-item-selected-color: var(--ant-color-primary);
--ant-menu-item-color: var(--color-text-1); --ant-menu-item-color: var(--color-text-1);
} }
@@ -92,7 +102,7 @@ html {
--ant-font-size-xl: 20px; --ant-font-size-xl: 20px;
--ant-font-size: var(--font-size-base); --ant-font-size: var(--font-size-base);
--ant-padding-sm: 14px; --ant-padding-sm: 14px;
--ant-border-radius-lg: 8px; --ant-border-radius-lg: 4px;
--ant-color-text: #000; --ant-color-text: #000;
--ant-color-error: #ff4d4f; --ant-color-error: #ff4d4f;
--ant-color-bg-mask: rgba(0, 0, 0, 35%); --ant-color-bg-mask: rgba(0, 0, 0, 35%);
@@ -113,6 +123,10 @@ html {
--ant-table-row-hover-bg: #e6e6e6; --ant-table-row-hover-bg: #e6e6e6;
} }
&.ant-progress {
--ant-progress-line-border-radius: 2px;
}
&.ant-modal-css-var { &.ant-modal-css-var {
--ant-modal-title-font-size: 14px; --ant-modal-title-font-size: 14px;
--ant-modal-header-margin-bottom: 20px; --ant-modal-header-margin-bottom: 20px;
@@ -126,7 +140,7 @@ html {
.css-var-ri.ant-menu-css-var, .css-var-ri.ant-menu-css-var,
.css-var-rh.ant-menu-css-var { .css-var-rh.ant-menu-css-var {
--ant-menu-item-height: 46px; --ant-menu-item-height: 46px;
--ant-menu-item-border-radius: 8px; --ant-menu-item-border-radius: 4px;
--ant-menu-item-selected-color: var(--ant-color-primary); --ant-menu-item-selected-color: var(--ant-color-primary);
--ant-menu-item-color: var(--color-text-1); --ant-menu-item-color: var(--color-text-1);
--ant-menu-item-active-bg: rgba(0, 0, 0, 4%); --ant-menu-item-active-bg: rgba(0, 0, 0, 4%);
@@ -269,7 +283,7 @@ body {
// form item help // form item help
.ant-form-item-with-help .ant-form-item-explain { .ant-form-item-with-help .ant-form-item-explain {
padding-left: 24px; padding-left: 16px;
} }
// icon // icon
@@ -293,12 +307,26 @@ body {
} }
.ant-pro-sider-logo { .ant-pro-sider-logo {
position: relative;
padding-left: 18px; padding-left: 18px;
padding-block: 12px;
border-block-end: none; border-block-end: none;
} }
.ant-pro-sider-logo-collapsed { .ant-pro-sider-logo-collapsed {
padding-left: 12px; padding-left: 12px;
.collapse-wrap {
display: none;
position: absolute;
top: 12px;
}
&:hover {
.collapse-wrap {
display: block;
}
}
} }
.ant-pro-sider .ant-layout-sider-children { .ant-pro-sider .ant-layout-sider-children {
@@ -427,6 +455,10 @@ body {
} }
} }
} }
.sub-title {
display: none !important;
}
} }
.ant-menu-submenu-title { .ant-menu-submenu-title {
@@ -442,7 +474,83 @@ body {
} }
// ======== monaco editor style ============ // ======== monaco editor style ============
.monaco-editor { .monaco-editor {
border-radius: 16px; border-radius: 8px;
}
.custome-scrollbar {
&::-webkit-scrollbar {
width: var(--scrollbar-size);
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb);
border-radius: 4px;
}
}
}
.custom-scrollbar-horizontal {
&::-webkit-scrollbar {
height: var(--scrollbar-size);
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb);
border-radius: 4px;
}
}
}
.simplebar-scrollbar.simplebar-visible::before {
opacity: 1;
}
.simplebar-scrollbar::before {
background: var(--scrollbar-handle-bg);
width: var(--scrollbar-size);
}
.simplebar-scrollbar.simplebar-visible.simplebar-hover {
&::before {
background: var(--scrollbar-handle-hover-bg);
}
}
.rc-virtual-list-scrollbar {
width: var(--scrollbar-size) !important;
}
.ant-dropdown-menu
.ant-dropdown-menu-item.ant-dropdown-menu-item-danger:not(
.ant-dropdown-menu-item-disabled
):hover {
background-color: var(--ant-color-error-bg) !important;
color: var(--ant-color-error) !important;
}
.ant-image {
border-radius: var(--border-radius-base);
overflow: hidden;
} }
.ant-message-notice-wrapper { .ant-message-notice-wrapper {
@@ -500,6 +608,12 @@ body {
} }
} }
// =============== segment start==============
.ant-segmented .ant-segmented-item::after {
border-radius: var(--border-radius-mini);
}
// =============== segment end ===============
.background { .background {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -560,6 +674,14 @@ body {
.ant-modal { .ant-modal {
max-width: 100vw; max-width: 100vw;
.ant-modal-title {
font-weight: var(--font-weight-bold);
}
}
.ant-drawer .ant-drawer-title {
font-weight: var(--font-weight-bold);
} }
.tooltip-wrapper { .tooltip-wrapper {
+72
View File
@@ -0,0 +1,72 @@
import { throttle } from 'lodash';
import {
useOverlayScrollbars,
UseOverlayScrollbarsParams
} from 'overlayscrollbars-react';
import React from 'react';
export const overlaySollerOptions: UseOverlayScrollbarsParams = {
options: {
update: {
debounce: 0
},
overflow: {
x: 'hidden'
},
scrollbars: {
autoHide: 'scroll',
autoHideDelay: 600,
clickScroll: 'instant'
}
},
defer: true
};
export default function useOverlayScroller() {
const scrollEventElement = React.useRef<any>(null);
const instanceRef = React.useRef<any>(null);
const [initialize, instance] = useOverlayScrollbars({
...overlaySollerOptions
});
instanceRef.current = instance?.();
scrollEventElement.current =
instanceRef.current?.elements()?.scrollEventElement;
const throttledScroll = React.useMemo(
() =>
throttle(() => {
scrollEventElement.current?.scrollTo?.({
top: scrollEventElement.current.scrollHeight,
behavior: 'smooth'
});
instanceRef.current?.update?.();
}, 300),
[scrollEventElement, instanceRef]
);
const throttledUpdateScrollerPosition = React.useCallback(() => {
throttledScroll();
}, [throttledScroll]);
// const createInstance = React.useCallback((el: any) => {
// if (el) {
// instanceRef.current?.destroy?.();
// initialize(el);
// instanceRef.current = instance?.();
// scrollEventElement.current =
// instanceRef.current?.elements()?.scrollEventElement;
// }
// }, []);
React.useEffect(() => {
return () => {
instanceRef.current?.destroy?.();
};
}, []);
return {
initialize,
instance: instanceRef.current,
updateScrollerPosition: throttledUpdateScrollerPosition
};
}
+28 -31
View File
@@ -1,5 +1,4 @@
import { WatchEventType } from '@/config'; import { WatchEventType } from '@/config';
import _ from 'lodash';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
interface ChunkedCollection { interface ChunkedCollection {
@@ -19,6 +18,7 @@ export function useUpdateChunkedList(options: {
}) { }) {
const cacheDataListRef = useRef<any[]>(options.dataList || []); const cacheDataListRef = useRef<any[]>(options.dataList || []);
const timerRef = useRef<any>(null); const timerRef = useRef<any>(null);
const countRef = useRef<number>(0);
useEffect(() => { useEffect(() => {
cacheDataListRef.current = [...(options.dataList || [])]; cacheDataListRef.current = [...(options.dataList || [])];
@@ -30,74 +30,71 @@ export function useUpdateChunkedList(options: {
options.setDataList?.([...cacheDataListRef.current]); options.setDataList?.([...cacheDataListRef.current]);
}, 80); }, 80);
}; };
const updateChunkedList = ( const updateChunkedList = (data: ChunkedCollection) => {
data: ChunkedCollection,
dataList?: { id: string | number }[]
) => {
console.log('updateChunkedList=====', { console.log('updateChunkedList=====', {
ids: data?.ids, ids: data?.ids,
type: data?.type, type: data?.type,
collection: data?.collection.length, collection: data?.collection
dataList: _.map(dataList, (o: any) => o.id)
}); });
let collections = data?.collection || []; let collections = data?.collection || [];
if (options?.computedID) { if (options?.computedID) {
collections = _.map(collections, (item: any) => { collections = collections?.map((item: any) => {
item.id = options?.computedID?.(item); item.id = options?.computedID?.(item);
return item; return item;
}); });
} }
if (options?.filterFun) { if (options?.filterFun) {
collections = _.filter(data?.collection, options?.filterFun); collections = data?.collection?.filter(options?.filterFun);
} }
if (options?.mapFun) { if (options?.mapFun) {
collections = _.map(data?.collection, options?.mapFun); collections = data?.collection?.map(options?.mapFun);
} }
const ids = data?.ids || []; const ids = data?.ids || [];
// CREATE // CREATE
if (data?.type === WatchEventType.CREATE) { if (data?.type === WatchEventType.CREATE) {
const newDataList: any[] = []; const newDataList = collections.reduce((acc: any[], item: any) => {
_.each(collections, (item: any) => { const updateIndex = cacheDataListRef.current?.findIndex(
const updateIndex = _.findIndex(
cacheDataListRef.current,
(sItem: any) => sItem.id === item.id (sItem: any) => sItem.id === item.id
); );
const updateItem = { ...item };
if (updateIndex === -1) { if (updateIndex === -1) {
const updateItem = _.cloneDeep(item); acc.push(updateItem);
newDataList.push(updateItem); } else {
cacheDataListRef.current[updateIndex] = updateItem;
} }
console.log('create=========', updateIndex, collections);
}); return acc;
cacheDataListRef.current = [...newDataList, ...cacheDataListRef.current]; }, []);
// options.setDataList?.([...cacheDataListRef.current]); cacheDataListRef.current = [
...newDataList,
...cacheDataListRef.current
].slice(0, 10);
} }
// DELETE // DELETE
if (data?.type === WatchEventType.DELETE) { if (data?.type === WatchEventType.DELETE) {
cacheDataListRef.current = _.filter( cacheDataListRef.current = cacheDataListRef.current?.filter(
cacheDataListRef.current,
(item: any) => { (item: any) => {
return !_.includes(ids, item.id); return !ids?.includes(item.id);
} }
); );
options.setDataList?.([...cacheDataListRef.current]); options.setDataList?.([...cacheDataListRef.current]);
} }
// UPDATE // UPDATE
if (data?.type === WatchEventType.UPDATE) { if (data?.type === WatchEventType.UPDATE) {
_.each(collections, (item: any) => { collections?.forEach((item: any) => {
const updateIndex = _.findIndex( const updateIndex = cacheDataListRef.current?.findIndex(
cacheDataListRef.current,
(sItem: any) => sItem.id === item.id (sItem: any) => sItem.id === item.id
); );
const updateItem = { ...item };
if (updateIndex > -1) { if (updateIndex > -1) {
const updateItem = _.cloneDeep(item);
cacheDataListRef.current[updateIndex] = updateItem; cacheDataListRef.current[updateIndex] = updateItem;
} else if (updateIndex === -1) { } else if (updateIndex === -1) {
const updateItem = _.cloneDeep(item); cacheDataListRef.current = [
cacheDataListRef.current.push(updateItem); updateItem,
...cacheDataListRef.current.slice(0, 9)
];
} }
}); });
console.log('updateChunkedList=====update', cacheDataListRef.current);
// options.setDataList?.([...cacheDataListRef.current]);
} }
debounceUpdateChunckedList(); debounceUpdateChunckedList();
+57 -22
View File
@@ -8,6 +8,7 @@ import VersionInfo, { modalConfig } from '@/components/version-info';
import { logout } from '@/pages/login/apis'; import { logout } from '@/pages/login/apis';
import { useAccessMarkedRoutes } from '@@/plugin-access'; import { useAccessMarkedRoutes } from '@@/plugin-access';
import { useModel } from '@@/plugin-model'; import { useModel } from '@@/plugin-model';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
import { ProLayout } from '@ant-design/pro-components'; import { ProLayout } from '@ant-design/pro-components';
import { import {
Link, Link,
@@ -20,9 +21,9 @@ import {
useNavigate, useNavigate,
type IRoute type IRoute
} from '@umijs/max'; } from '@umijs/max';
import { Modal } from 'antd'; import { Button, Modal } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import Exception from './Exception'; import Exception from './Exception';
import './Layout.css'; import './Layout.css';
import { LogoIcon, SLogoIcon } from './Logo'; import { LogoIcon, SLogoIcon } from './Logo';
@@ -92,6 +93,7 @@ export default (props: any) => {
const intl = useIntl(); const intl = useIntl();
const { clientRoutes, pluginManager } = useAppData(); const { clientRoutes, pluginManager } = useAppData();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [collapseValue, setCollapseValue] = useState(false);
const initialInfo = (useModel && useModel('@@initialState')) || { const initialInfo = (useModel && useModel('@@initialState')) || {
initialState: undefined, initialState: undefined,
@@ -139,6 +141,11 @@ export default (props: any) => {
notFound: <span>404 not found</span> notFound: <span>404 not found</span>
}; };
const handleToggleCollapse = (e: any) => {
e.stopPropagation();
setCollapsed(!collapsed);
};
const newRoutes = filterRoutes( const newRoutes = filterRoutes(
clientRoutes.filter((route) => route.id === '@@/global-layout'), clientRoutes.filter((route) => route.id === '@@/global-layout'),
(route) => { (route) => {
@@ -151,7 +158,6 @@ export default (props: any) => {
const role = initialState?.currentUser?.is_admin ? 'admin' : 'user'; const role = initialState?.currentUser?.is_admin ? 'admin' : 'user';
const [route] = useAccessMarkedRoutes(mapRoutes(newRoutes, role)); const [route] = useAccessMarkedRoutes(mapRoutes(newRoutes, role));
console.log('clientRoutes===========', route, clientRoutes, newRoutes);
patchRoutes({ patchRoutes({
routes: route.children, routes: route.children,
@@ -162,7 +168,52 @@ export default (props: any) => {
() => matchRoutes(route?.children || [], location.pathname)?.pop?.()?.route, () => matchRoutes(route?.children || [], location.pathname)?.pop?.()?.route,
[location.pathname] [location.pathname]
); );
console.log('route===========', matchedRoute, route);
const renderMenuHeader = useCallback(
(logo, title) => {
return (
<>
{logo}
<div className="collapse-wrap">
<Button
style={{ marginRight: collapsed ? 0 : -14 }}
size="small"
type={collapsed ? 'default' : 'text'}
onClick={handleToggleCollapse}
>
<>
<MenuUnfoldOutlined
style={{ display: collapsed ? 'block' : 'none' }}
/>
<MenuFoldOutlined
style={{ display: !collapsed ? 'block' : 'none' }}
/>
</>
</Button>
</div>
</>
);
},
[collapsed]
);
const actionRender = useCallback(
(layoutProps) => {
const dom = getRightRenderContent({
runtimeConfig,
loading,
initialState,
setInitialState,
intl,
siderWidth: layoutProps.siderWidth,
collapsed: layoutProps.collapsed
});
return dom;
},
[intl]
);
return ( return (
<div> <div>
<div className="background"></div> <div className="background"></div>
@@ -179,15 +230,11 @@ export default (props: any) => {
e.preventDefault(); e.preventDefault();
navigate('/'); navigate('/');
}} }}
menuHeaderRender={renderMenuHeader}
collapsed={collapsed} collapsed={collapsed}
onCollapse={(collapsed) => {
setCollapsed(collapsed);
}}
onPageChange={(route) => { onPageChange={(route) => {
const { location } = history; const { location } = history;
console.log('onPageChange', userInfo, initialState);
// if user is not change password, redirect to change password page // if user is not change password, redirect to change password page
if ( if (
location.pathname !== loginPath && location.pathname !== loginPath &&
@@ -242,19 +289,7 @@ export default (props: any) => {
fixSiderbar fixSiderbar
fixedHeader fixedHeader
{...runtimeConfig} {...runtimeConfig}
actionsRender={(layoutProps) => { actionsRender={actionRender}
const dom = getRightRenderContent({
runtimeConfig,
loading,
initialState,
setInitialState,
intl,
siderWidth: layoutProps.siderWidth,
collapsed: layoutProps.collapsed
});
return dom;
}}
> >
<Exception <Exception
route={matchedRoute} route={matchedRoute}
+7 -6
View File
@@ -1,5 +1,4 @@
// @ts-nocheck // @ts-nocheck
import avatarImg from '@/assets/images/avatar.png'; import avatarImg from '@/assets/images/avatar.png';
import externalLinks from '@/constants/external-links'; import externalLinks from '@/constants/external-links';
import langConfigMap from '@/locales/lang-config-map'; import langConfigMap from '@/locales/lang-config-map';
@@ -18,7 +17,7 @@ import { getAllLocales, history, setLocale } from '@umijs/max';
import { Avatar, Menu, Spin } from 'antd'; import { Avatar, Menu, Spin } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
export function getRightRenderContent(opts: { export const getRightRenderContent = (opts: {
runtimeConfig: any; runtimeConfig: any;
loading: boolean; loading: boolean;
initialState: any; initialState: any;
@@ -26,7 +25,7 @@ export function getRightRenderContent(opts: {
setInitialState: any; setInitialState: any;
siderWidth: number; siderWidth: number;
intl: any; intl: any;
}) { }) => {
const { intl, collapsed, siderWidth } = opts; const { intl, collapsed, siderWidth } = opts;
const allLocals = getAllLocales(); const allLocals = getAllLocales();
@@ -121,7 +120,9 @@ export function getRightRenderContent(opts: {
key: 'help', key: 'help',
icon: <QuestionCircleOutlined />, icon: <QuestionCircleOutlined />,
label: ( label: (
<span>{intl?.formatMessage?.({ id: 'common.button.help' })}</span> <span className="sub-title">
{intl?.formatMessage?.({ id: 'common.button.help' })}
</span>
), ),
children: helpList.map((item) => ({ children: helpList.map((item) => ({
key: item.key, key: item.key,
@@ -169,7 +170,7 @@ export function getRightRenderContent(opts: {
key: 'lang', key: 'lang',
icon: <GlobalOutlined />, icon: <GlobalOutlined />,
label: ( label: (
<span> <span className="sub-title">
{intl?.formatMessage?.({ id: 'common.settings.language' })} {intl?.formatMessage?.({ id: 'common.settings.language' })}
</span> </span>
), ),
@@ -280,4 +281,4 @@ export function getRightRenderContent(opts: {
></Menu> ></Menu>
</div> </div>
); );
} };
+17 -22
View File
@@ -1,23 +1,18 @@
import apikeys from './en-US/apikeys'; /**
import common from './en-US/common'; * @description The directory name used in require.context must match the language configuration names defined in lang-config-map.ts.
import dashboard from './en-US/dashboard'; * @example Directories like 'en-US' or 'zh-CN' should correspond exactly to the configuration names in lang-config-map.ts.
import menu from './en-US/menu'; */
import models from './en-US/models';
import playground from './en-US/playground';
import resources from './en-US/resources';
import shortcuts from './en-US/shortcuts';
import usage from './en-US/usage';
import users from './en-US/users';
export default { const requireContext = require.context(`./en-US`, false, /\.ts$/);
...common,
...menu, let languageConfig: Record<string, string> = {};
...models,
...playground, requireContext.keys().forEach((fileName: any) => {
...resources, const moduleConfig = requireContext(fileName).default;
...apikeys, languageConfig = {
...users, ...languageConfig,
...dashboard, ...moduleConfig
...usage, };
...shortcuts });
};
export default languageConfig;
+5 -4
View File
@@ -113,9 +113,9 @@ export default {
'common.button.logs': 'Logs', 'common.button.logs': 'Logs',
'common.ws.close': 'The connection was lost, do you want to reconnect?', 'common.ws.close': 'The connection was lost, do you want to reconnect?',
'common.ws.reconnect': 'Reconnect', 'common.ws.reconnect': 'Reconnect',
'common.input.key': 'key', 'common.input.key': 'Key',
'common.input.value': 'value', 'common.input.value': 'Value',
'common.input.type': 'type', 'common.input.type': 'Type',
'common.input.visible': 'visibility', 'common.input.visible': 'visibility',
'common.input.description': 'description', 'common.input.description': 'description',
'common.time.day': 'day', 'common.time.day': 'day',
@@ -205,5 +205,6 @@ export default {
'common.button.version': 'Version', 'common.button.version': 'Version',
'common.title.delete.confirm': 'Confirm delete', 'common.title.delete.confirm': 'Confirm delete',
'common.button.addLabel': 'Add Labels', 'common.button.addLabel': 'Add Labels',
'common.button.addSelector': 'Add Selectors' 'common.button.addSelector': 'Add Selectors',
'common.button.addParams': 'Add Parameter'
}; };
+2
View File
@@ -1,6 +1,8 @@
export default { export default {
'menu.dashboard': 'Dashboard', 'menu.dashboard': 'Dashboard',
'menu.playground': 'Playground', 'menu.playground': 'Playground',
'menu.chat': 'Chat',
'menu.compare': 'Compare',
'menu.models': 'Models', 'menu.models': 'Models',
'menu.resources': 'Resources', 'menu.resources': 'Resources',
'menu.apikeys': 'API Keys', 'menu.apikeys': 'API Keys',
+12 -2
View File
@@ -22,7 +22,7 @@ export default {
'model.form.ollama.model': 'Ollama Model', '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.deploy.sort': 'Sort', 'model.deploy.sort': 'Sort',
'model.deploy.search.placeholder': 'Search models from Hugging Face', 'model.deploy.search.placeholder': 'Search models from {source}',
'model.form.ollamatips': '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.', '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.',
'models.sort.name': 'Name', 'models.sort.name': 'Name',
@@ -35,6 +35,7 @@ export default {
'models.data.card': 'Model Card', 'models.data.card': 'Model Card',
'models.available.files': 'Available Files', 'models.available.files': 'Available Files',
'models.viewin.hf': 'View in Hugging Face', 'models.viewin.hf': 'View in Hugging Face',
'models.viewin.modelscope': 'View in ModelScope',
'models.architecture': 'Architecture', 'models.architecture': 'Architecture',
'models.search.noresult': 'No related models found', 'models.search.noresult': 'No related models found',
'models.search.nofiles': 'No available files', 'models.search.nofiles': 'No available files',
@@ -54,5 +55,14 @@ export default {
'models.table.backend': 'Backends', 'models.table.backend': 'Backends',
'models.table.acrossworker': 'Distribution Across Workers', 'models.table.acrossworker': 'Distribution Across Workers',
'models.table.cpuoffload': 'CPU Offload', 'models.table.cpuoffload': 'CPU Offload',
'models.table.layers': 'Layers' 'models.table.layers': 'Layers',
'models.form.backend': 'Backend',
'models.form.backend_parameters': 'Backend Parameters',
'models.search.gguf.tips': '1. GGUF model backend is llama-box(llama.cpp).',
'models.search.vllm.tips': '2. Non-GGUF model backend is vLLM.',
'models.form.ollamalink': 'Find More in Ollama Library',
'models.form.backend_parameters.llamabox.placeholder':
'e.g., --ctx-size=8192',
'models.form.backend_parameters.vllm.placeholder':
'e.g., --max-model-len=8192'
}; };
+19 -2
View File
@@ -1,7 +1,8 @@
export default { export default {
'playground.system.tips': 'Enter system message here', 'playground.system.tips': 'Type system instructions here',
'playground.title': 'Playground', 'playground.title': 'Playground',
'playground.system': 'System', 'playground.system': 'System',
'playground.systemMessage': 'System instructions',
'playground.user': 'User', 'playground.user': 'User',
'playground.assistant': 'Assistant', 'playground.assistant': 'Assistant',
'playground.newMessage': 'New Message', 'playground.newMessage': 'New Message',
@@ -29,5 +30,21 @@ export default {
'A stop sequence is a predefined or user-specified text string that signals the AI to stop generating further tokens when these sequences appear.', 'A stop sequence is a predefined or user-specified text string that signals the AI to stop generating further tokens when these sequences appear.',
'playground.viewcode.tips': 'playground.viewcode.tips':
'Your API Key can be found {here}.You should use environment variables or a secret management tool to expose your key to your applications.', 'Your API Key can be found {here}.You should use environment variables or a secret management tool to expose your key to your applications.',
'playground.viewcode.here': 'here' 'playground.viewcode.here': 'here',
'playground.delete.img': 'Delete Image',
'playground.img.upload': 'Upload Image',
'playground.img.upload.success': 'Upload Success',
'playground.img.upload.error': 'Upload Error',
'playground.toolbar.clearmsg': 'Clear Messages',
'playground.toolbar.prompts': 'Prompts',
'playground.toolbar.compare2Model': '2-Model Comparison',
'playground.toolbar.compare3Model': '3-Model Comparison',
'playground.toolbar.compare4Model': '4-Model Comparison',
'playground.toolbar.compare6Model': '6-Model Comparison',
'playground.input.holder': 'Type <kbd>/</kbd> to input message',
'playground.compare.apply': 'Apply',
'playground.compare.applytoall': 'Apply to all models',
'playground.model.noavailable': 'No available models',
'playground.model.noavailable.tips':
'Please deploy a model first, and it should not be an embedding-only model.'
}; };
+2 -2
View File
@@ -29,9 +29,9 @@ export default {
'resources.table.vram': 'VRAM', 'resources.table.vram': 'VRAM',
'resources.table.index': 'Index', 'resources.table.index': 'Index',
'resources.table.workername': 'Worker Name', 'resources.table.workername': 'Worker Name',
'resources.table.vender': 'Vender', 'resources.table.vender': 'Vendor',
'resources.table.temperature': 'Temperature', 'resources.table.temperature': 'Temperature',
'resources.table.core': 'Core', 'resources.table.core': 'Cores',
'resources.table.utilization': 'Utilization', 'resources.table.utilization': 'Utilization',
'resources.table.gpuutilization': 'GPU Utilization', 'resources.table.gpuutilization': 'GPU Utilization',
'resources.table.vramutilization': 'VRAM Utilization', 'resources.table.vramutilization': 'VRAM Utilization',
+6
View File
@@ -1,5 +1,11 @@
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
/**
* Language configuration map
* Warning: The key of the map must be the same as the directory name in the locales directory
* Do not modify the key of the map
*
*/
const langConfigMap = { const langConfigMap = {
'ar-EG': { 'ar-EG': {
lang: 'ar-EG', lang: 'ar-EG',
+17 -22
View File
@@ -1,23 +1,18 @@
import apikeys from './zh-CN/apikeys'; /**
import common from './zh-CN/common'; * @description The directory name used in require.context must match the language configuration names defined in lang-config-map.ts.
import dashboard from './zh-CN/dashboard'; * @example Directories like 'en-US' or 'zh-CN' should correspond exactly to the configuration names in lang-config-map.ts.
import menu from './zh-CN/menu'; */
import models from './zh-CN/models';
import playground from './zh-CN/playground';
import resources from './zh-CN/resources';
import shortcuts from './zh-CN/shortcuts';
import usage from './zh-CN/usage';
import users from './zh-CN/users';
export default { const requireContext = require.context(`./zh-CN`, false, /\.ts$/);
...common,
...menu, let languageConfig: Record<string, string> = {};
...models,
...playground, requireContext.keys().forEach((fileName: any) => {
...resources, const moduleConfig = requireContext(fileName).default;
...apikeys, languageConfig = {
...users, ...languageConfig,
...dashboard, ...moduleConfig
...usage, };
...shortcuts });
};
export default languageConfig;
+2 -1
View File
@@ -198,5 +198,6 @@ export default {
'common.button.version': '版本', 'common.button.version': '版本',
'common.title.delete.confirm': '确认删除', 'common.title.delete.confirm': '确认删除',
'common.button.addLabel': '添加标签', 'common.button.addLabel': '添加标签',
'common.button.addSelector': '添加选择器' 'common.button.addSelector': '添加选择器',
'common.button.addParams': '添加参数'
}; };
+2
View File
@@ -1,6 +1,8 @@
export default { export default {
'menu.dashboard': '概览', 'menu.dashboard': '概览',
'menu.playground': '试验场', 'menu.playground': '试验场',
'menu.chat': '聊天',
'menu.compare': '多模型对比',
'menu.models': '模型', 'menu.models': '模型',
'menu.resources': '资源', 'menu.resources': '资源',
'menu.apikeys': 'API 密钥', 'menu.apikeys': 'API 密钥',
+12 -2
View File
@@ -22,7 +22,7 @@ export default {
'model.form.ollama.model': 'Ollama 模型', 'model.form.ollama.model': 'Ollama 模型',
'model.form.ollamaholder': '请选择或输入模型名称', 'model.form.ollamaholder': '请选择或输入模型名称',
'model.deploy.sort': '排序', 'model.deploy.sort': '排序',
'model.deploy.search.placeholder': '从 Hugging Face 搜索模型', 'model.deploy.search.placeholder': '从 {source} 搜索模型',
'model.form.ollamatips': 'model.form.ollamatips':
'提示:以下为 GPUStack 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。', '提示:以下为 GPUStack 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。',
'models.sort.name': '名称', 'models.sort.name': '名称',
@@ -35,6 +35,7 @@ export default {
'models.data.card': '模型简介', 'models.data.card': '模型简介',
'models.available.files': '可用文件', 'models.available.files': '可用文件',
'models.viewin.hf': '在 Hugging Face 中查看', 'models.viewin.hf': '在 Hugging Face 中查看',
'models.viewin.modelscope': '在 ModelScope 中查看',
'models.architecture': '架构', 'models.architecture': '架构',
'models.search.noresult': '未找到相关模型', 'models.search.noresult': '未找到相关模型',
'models.search.nofiles': '无可用文件', 'models.search.nofiles': '无可用文件',
@@ -53,5 +54,14 @@ export default {
'models.table.backend': '后端', 'models.table.backend': '后端',
'models.table.acrossworker': '跨 worker 推理', 'models.table.acrossworker': '跨 worker 推理',
'models.table.cpuoffload': 'CPU 卸载', 'models.table.cpuoffload': 'CPU 卸载',
'models.table.layers': '层' 'models.table.layers': '层',
'models.form.backend': '后端',
'models.form.backend_parameters': '后端参数',
'models.search.gguf.tips': '1. GGUF 模型后端为 llama-box(llama.cpp)',
'models.search.vllm.tips': '2. 非 GGUF 模型后端为 vLLM',
'models.form.ollamalink': '在 Ollama Library 中查找',
'models.form.backend_parameters.llamabox.placeholder':
'例如,--ctx-size=8192',
'models.form.backend_parameters.vllm.placeholder':
'例如,--max-model-len=8192'
}; };
+18 -1
View File
@@ -2,6 +2,7 @@ export default {
'playground.system.tips': '在这里输入系统消息', 'playground.system.tips': '在这里输入系统消息',
'playground.title': '试验场', 'playground.title': '试验场',
'playground.system': '系统', 'playground.system': '系统',
'playground.systemMessage': '系统消息',
'playground.user': '用户', 'playground.user': '用户',
'playground.assistant': '小助手', 'playground.assistant': '小助手',
'playground.newMessage': '新消息', 'playground.newMessage': '新消息',
@@ -29,5 +30,21 @@ export default {
'停止序列是一个预定义或用户指定的文本字符串,当这些序列出现时,它会提示 AI 停止生成后续的 token。', '停止序列是一个预定义或用户指定的文本字符串,当这些序列出现时,它会提示 AI 停止生成后续的 token。',
'playground.viewcode.tips': 'playground.viewcode.tips':
'{here} 查看 API 密钥。您应该使用环境变量或密钥管理工具将您的密钥暴露给您的应用程序。', '{here} 查看 API 密钥。您应该使用环境变量或密钥管理工具将您的密钥暴露给您的应用程序。',
'playground.viewcode.here': '这里' 'playground.viewcode.here': '这里',
'playground.delete.img': '删除图片',
'playground.img.upload': '上传图片',
'playground.img.upload.success': '上传成功',
'playground.img.upload.error': '上传失败',
'playground.toolbar.clearmsg': '清空消息',
'playground.toolbar.prompts': '提示词',
'playground.toolbar.compare2Model': '2 模型对比',
'playground.toolbar.compare3Model': '3 模型对比',
'playground.toolbar.compare4Model': '4 模型对比',
'playground.toolbar.compare6Model': '6 模型对比',
'playground.input.holder': '按 <kbd>/</kbd> 开始输入',
'playground.compare.apply': '应用',
'playground.compare.applytoall': '应用到所有模型',
'playground.model.noavailable': '无可用模型',
'playground.model.noavailable.tips':
'请先部署模型,且不是 Embedding Only 的模型'
}; };
@@ -185,6 +185,7 @@ const AddModal: React.FC<AddModalProps> = ({
<Form.Item> <Form.Item>
<div> <div>
<Tag <Tag
bordered={false}
color="error" color="error"
style={{ padding: '6px 8px', marginBottom: 16 }} style={{ padding: '6px 8px', marginBottom: 16 }}
> >
@@ -2,71 +2,9 @@ import PageTools from '@/components/page-tools';
import { convertFileSize } from '@/utils'; import { convertFileSize } from '@/utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Col, Row, Table } from 'antd'; import { Col, Row, Table } from 'antd';
import { useContext } from 'react'; import { memo, useContext } from 'react';
import { DashboardContext } from '../config/dashboard-context'; import { DashboardContext } from '../config/dashboard-context';
const projectColumns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name'
},
{
title: 'Token Quota',
dataIndex: 'quota',
key: 'quota',
render: (text: any, record: any) => <span>{record.quota}k</span>
},
{
title: 'Token Utilization',
dataIndex: 'utilization',
key: 'utilization',
render: (text: any, record: any) => <span>{record.utilization}%</span>
},
{
title: 'Members',
dataIndex: 'members',
key: 'members'
}
];
const projectData = [
{
id: 1,
name: 'copilot-dev',
quota: 100,
utilization: 50,
members: 4
},
{
id: 2,
name: 'rag-wiki',
quota: 200,
utilization: 70,
members: 3
},
{
id: 3,
name: 'smart-auto-agent',
quota: 100,
utilization: 20,
members: 5
},
{
id: 4,
name: 'office-auto-docs',
quota: 100,
utilization: 25,
members: 1
},
{
id: 5,
name: 'smart-customer-service',
quota: 100,
utilization: 46,
members: 2
}
];
const ActiveTable = () => { const ActiveTable = () => {
const intl = useIntl(); const intl = useIntl();
const data = useContext(DashboardContext).active_models || []; const data = useContext(DashboardContext).active_models || [];
@@ -76,14 +14,6 @@ const ActiveTable = () => {
dataIndex: 'name', dataIndex: 'name',
key: 'name' key: 'name'
}, },
// {
// title: intl.formatMessage({ id: 'dashboard.gpuutilization' }),
// dataIndex: 'gpu_utilization',
// key: 'gpu_utilization',
// render: (text: any, record: any) => (
// <ProgressBar percent={_.round(text, 0)}></ProgressBar>
// )
// },
{ {
title: intl.formatMessage({ id: 'dashboard.allocatevram' }), title: intl.formatMessage({ id: 'dashboard.allocatevram' }),
dataIndex: 'resource_claim.memory', dataIndex: 'resource_claim.memory',
@@ -133,4 +63,4 @@ const ActiveTable = () => {
); );
}; };
export default ActiveTable; export default memo(ActiveTable);
+2 -2
View File
@@ -29,7 +29,7 @@ const renderCardItem = (data: {
const Overview: React.FC = () => { const Overview: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const data = useContext(DashboardContext).resource_counts || {}; const data = useContext(DashboardContext).resource_counts || {};
console.log('overview===');
const renderValue = ( const renderValue = (
value: value:
| number | number
@@ -74,4 +74,4 @@ const Overview: React.FC = () => {
); );
}; };
export default Overview; export default React.memo(Overview);
@@ -110,7 +110,6 @@ const option = {
}; };
const UtilizationOvertime: React.FC = () => { const UtilizationOvertime: React.FC = () => {
console.log('systemload=====================');
const intl = useIntl(); const intl = useIntl();
const data = useContext(DashboardContext)?.system_load?.history || {}; const data = useContext(DashboardContext)?.system_load?.history || {};
@@ -147,7 +146,7 @@ const UtilizationOvertime: React.FC = () => {
legendData, legendData,
xAxisData: _.uniq(xAxisData) xAxisData: _.uniq(xAxisData)
}; };
}, [data]); }, [data, intl]);
return ( return (
<> <>
+32 -15
View File
@@ -5,9 +5,8 @@ import breakpoints from '@/config/breakpoints';
import useWindowResize from '@/hooks/use-window-resize'; import useWindowResize from '@/hooks/use-window-resize';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Col, Row } from 'antd'; import { Col, Row } from 'antd';
import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
import { useContext, useEffect, useState } from 'react'; import { memo, useContext, useEffect, useMemo, useState } from 'react';
import { DashboardContext } from '../config/dashboard-context'; import { DashboardContext } from '../config/dashboard-context';
import ResourceUtilization from './resource-utilization'; import ResourceUtilization from './resource-utilization';
@@ -28,11 +27,29 @@ const SystemLoad = () => {
const [paddingRight, setPaddingRight] = useState<string>('20px'); const [paddingRight, setPaddingRight] = useState<string>('20px');
const [smallChartHeight, setSmallChartHeight] = useState<number>(190); const [smallChartHeight, setSmallChartHeight] = useState<number>(190);
const [largeChartHeight, setLargeChartHeight] = useState<number>(400); const [largeChartHeight, setLargeChartHeight] = useState<number>(400);
const thresholds = [0.5, 0.7, 1];
const height = 400;
const currentDate = dayjs().format('YYYY-MM-DD');
const handleSelectDate = (date: any) => {}; const height = 400;
const chartData = useMemo(() => {
return {
gpu: {
data: _.round(data.gpu || 0, 1),
color: strokeColorFunc(data.gpu)
},
vram: {
data: _.round(data.vram || 0, 1),
color: strokeColorFunc(data.vram)
},
cpu: {
data: _.round(data.cpu || 0, 1),
color: strokeColorFunc(data.cpu)
},
ram: {
data: _.round(data.ram || 0, 1),
color: strokeColorFunc(data.ram)
}
};
}, [data]);
useEffect(() => { useEffect(() => {
if (size.width < breakpoints.xl) { if (size.width < breakpoints.xl) {
@@ -70,8 +87,8 @@ const SystemLoad = () => {
<Col span={12} style={{ height: smallChartHeight }}> <Col span={12} style={{ height: smallChartHeight }}>
<GaugeChart <GaugeChart
height={smallChartHeight} height={smallChartHeight}
value={_.round(data.gpu || 0, 1)} value={chartData.gpu.data}
color={strokeColorFunc(data.gpu)} color={chartData.gpu.color}
title={intl.formatMessage({ title={intl.formatMessage({
id: 'dashboard.gpuutilization' id: 'dashboard.gpuutilization'
})} })}
@@ -83,8 +100,8 @@ const SystemLoad = () => {
id: 'dashboard.vramutilization' id: 'dashboard.vramutilization'
})} })}
height={smallChartHeight} height={smallChartHeight}
color={strokeColorFunc(data.vram)} color={chartData.vram.color}
value={_.round(data.vram || 0, 1)} value={chartData.vram.data}
></GaugeChart> ></GaugeChart>
</Col> </Col>
<Col span={12} style={{ height: smallChartHeight }}> <Col span={12} style={{ height: smallChartHeight }}>
@@ -93,8 +110,8 @@ const SystemLoad = () => {
id: 'dashboard.cpuutilization' id: 'dashboard.cpuutilization'
})} })}
height={smallChartHeight} height={smallChartHeight}
color={strokeColorFunc(data.cpu)} color={chartData.cpu.color}
value={_.round(data.cpu || 0, 1)} value={chartData.cpu.data}
></GaugeChart> ></GaugeChart>
</Col> </Col>
<Col span={12} style={{ height: smallChartHeight }}> <Col span={12} style={{ height: smallChartHeight }}>
@@ -103,8 +120,8 @@ const SystemLoad = () => {
id: 'dashboard.memoryutilization' id: 'dashboard.memoryutilization'
})} })}
height={smallChartHeight} height={smallChartHeight}
color={strokeColorFunc(data.ram)} color={chartData.ram.color}
value={_.round(data.ram || 0, 1)} value={chartData.ram.data}
></GaugeChart> ></GaugeChart>
</Col> </Col>
</Row> </Row>
@@ -116,4 +133,4 @@ const SystemLoad = () => {
); );
}; };
export default SystemLoad; export default memo(SystemLoad);
@@ -4,7 +4,7 @@ import { useIntl } from '@umijs/max';
import { Col, Row } from 'antd'; import { Col, Row } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
import { memo, useCallback, useContext } from 'react'; import { memo, useCallback, useContext, useEffect, useState } from 'react';
import { DashboardContext } from '../../config/dashboard-context'; import { DashboardContext } from '../../config/dashboard-context';
import RequestTokenInner from './request-token-inner'; import RequestTokenInner from './request-token-inner';
import TopUser from './top-user'; import TopUser from './top-user';
@@ -33,18 +33,29 @@ const getCurrentMonthDays = () => {
const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => { const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
const intl = useIntl(); const intl = useIntl();
const currentDate = dayjs();
const currentMonthDays = getCurrentMonthDays(); const [topUserData, setTopUserData] = useState<{
let requestData: { userData: { name: string; value: number }[];
name: string; topUserList: string[];
color: string; }>({
areaStyle: any; userData: [],
data: { time: string; value: number }[]; topUserList: []
}[] = []; });
let tokenData: { time: string; value: number }[] = [];
let userData: { name: string; value: number }[] = []; const [requestTokenData, setRequestTokenData] = useState<{
const xAxisData: string[] = currentMonthDays; requestData: {
let topUserList: string[] = []; name: string;
color: string;
areaStyle: any;
data: { time: string; value: number }[];
}[];
tokenData: { time: string; value: number }[];
xAxisData: string[];
}>({
requestData: [],
tokenData: [],
xAxisData: []
});
const { model_usage } = useContext(DashboardContext); const { model_usage } = useContext(DashboardContext);
const data = model_usage || {}; const data = model_usage || {};
@@ -54,6 +65,7 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
}; };
const generateData = useCallback(() => { const generateData = useCallback(() => {
const currentMonthDays = getCurrentMonthDays();
const requestList: { const requestList: {
name: string; name: string;
color: string; color: string;
@@ -88,7 +100,7 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
data: [] data: []
}; };
_.each(xAxisData, (date: string) => { _.each(currentMonthDays, (date: string) => {
// tokens data // tokens data
const item = _.find(data.completion_token_history, (item: any) => { const item = _.find(data.completion_token_history, (item: any) => {
return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date; return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
@@ -140,8 +152,10 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
// ========== top users ============ // ========== top users ============
if (!data.top_users?.length) { if (!data.top_users?.length) {
userData = []; setTopUserData({
topUserList = []; userData: [],
topUserList: []
});
} else { } else {
const users: string[] = []; const users: string[] = [];
_.each(data.top_users, (item: any) => { _.each(data.top_users, (item: any) => {
@@ -155,15 +169,23 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
value: item.completion_token_count value: item.completion_token_count
}); });
}); });
topUserList = _.uniq(users);
userData = [topUserCompletion, topUserPrompt]; setTopUserData({
userData: [topUserCompletion, topUserPrompt],
topUserList: _.uniq(users)
});
} }
requestData = [requestList]; setRequestTokenData({
tokenData = [completionData, prompData]; requestData: [requestList],
}, [data, xAxisData]); tokenData: [completionData, prompData],
xAxisData: currentMonthDays
});
}, [data]);
generateData(); useEffect(() => {
generateData();
}, [generateData]);
return ( return (
<> <>
@@ -181,13 +203,16 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
style={{ paddingRight: paddingRight }} style={{ paddingRight: paddingRight }}
> >
<RequestTokenInner <RequestTokenInner
requestData={requestData} requestData={requestTokenData.requestData}
xAxisData={xAxisData} xAxisData={requestTokenData.xAxisData}
tokenData={tokenData} tokenData={requestTokenData.tokenData}
></RequestTokenInner> ></RequestTokenInner>
</Col> </Col>
<Col xs={24} sm={24} md={24} lg={24} xl={8}> <Col xs={24} sm={24} md={24} lg={24} xl={8}>
<TopUser userData={userData} topUserList={topUserList}></TopUser> <TopUser
userData={topUserData.userData}
topUserList={topUserData.topUserList}
></TopUser>
</Col> </Col>
</Row> </Row>
</> </>
@@ -17,7 +17,6 @@ interface RequestTokenInnerProps {
xAxisData: string[]; xAxisData: string[];
} }
const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => { const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
console.log('request token inner=====================');
const { requestData, tokenData, xAxisData } = props; const { requestData, tokenData, xAxisData } = props;
const intl = useIntl(); const intl = useIntl();
const labelFormatter = (v: any) => { const labelFormatter = (v: any) => {
@@ -8,7 +8,6 @@ interface TopUserProps {
topUserList: string[]; topUserList: string[];
} }
const TopUser: React.FC<TopUserProps> = (props) => { const TopUser: React.FC<TopUserProps> = (props) => {
console.log('TopUser=====================');
const { userData, topUserList } = props; const { userData, topUserList } = props;
const intl = useIntl(); const intl = useIntl();
+86 -1
View File
@@ -1,6 +1,7 @@
import { downloadFile, listFiles, listModels } from '@huggingface/hub'; import { downloadFile, listFiles, listModels } from '@huggingface/hub';
import { PipelineType } from '@huggingface/tasks'; import { PipelineType } from '@huggingface/tasks';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import qs from 'query-string';
import { import {
FormData, FormData,
GPUListItem, GPUListItem,
@@ -126,6 +127,15 @@ export async function callHuggingfaceQuickSearch(params: any) {
const HUGGINGFACE_API = 'https://huggingface.co/api/models'; const HUGGINGFACE_API = 'https://huggingface.co/api/models';
const MODEL_SCOPE_LIST_MODEL_API =
'/proxy?url=https://www.modelscope.cn/api/v1/dolphin/models';
const MODEL_SCOPE_DETAIL_MODEL_API =
'/proxy?url=https://www.modelscope.cn/api/v1/dolphin/models/';
const MODE_SCOPE_MODEL_FIELS_API =
'/proxy?url=https://modelscope.cn/api/v1/models/';
export async function queryHuggingfaceModelDetail( export async function queryHuggingfaceModelDetail(
params: { repo: string }, params: { repo: string },
options?: any options?: any
@@ -136,6 +146,81 @@ export async function queryHuggingfaceModelDetail(
}); });
} }
export async function queryModelScopeModels(
params: {
PageSize?: number;
PageNumber?: number;
SortBy?: string;
Target?: string;
SingleCriterion?: any[];
Name: string;
filterGGUF?: boolean;
},
config?: any
) {
const Criterion = params.filterGGUF
? {
Criterion: [
{ category: 'tags', predicate: 'contains', values: ['gguf'] }
]
}
: {};
const res = await fetch(`${MODEL_SCOPE_LIST_MODEL_API}`, {
method: 'PUT',
signal: config?.signal,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...params,
...Criterion,
Name: `${params.Name}`,
PageSize: 100,
PageNumber: 1
})
});
if (!res.ok) {
throw new Error('Network response was not ok');
return null;
}
return res.json();
}
export async function queryModelScopeModelDetail(
params: { name: string },
options?: any
) {
return request(`${MODE_SCOPE_MODEL_FIELS_API}${params.name}`, {
method: 'GET',
cancelToken: options?.token
});
}
export async function queryModelScopeModelFiles(
params: { name: string; revision: string },
options?: any
) {
const res = await fetch(
`${MODE_SCOPE_MODEL_FIELS_API}${params.name}/repo/files?${qs.stringify({
Revision: params.revision,
Recursive: true,
Root: ''
})}`,
{
method: 'GET',
signal: options?.signal,
body: null
}
);
if (!res.ok) {
throw new Error('Network response was not ok');
return null;
}
return res.json();
}
export async function queryHuggingfaceModels( export async function queryHuggingfaceModels(
params: { params: {
search: { search: {
@@ -152,7 +237,7 @@ export async function queryHuggingfaceModels(
...params, ...params,
...options, ...options,
limit: 100, limit: 100,
additionalFields: ['sha'], additionalFields: ['sha', 'tags'],
fetch(url: string, config: any) { fetch(url: string, config: any) {
try { try {
const newUrl = params.search.sort const newUrl = params.search.sort
@@ -1,5 +1,8 @@
import LabelSelector from '@/components/label-selector'; import LabelSelector from '@/components/label-selector';
import ListInput from '@/components/list-input';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { InfoCircleOutlined, RightOutlined } from '@ant-design/icons'; import { InfoCircleOutlined, RightOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { import {
@@ -13,22 +16,27 @@ import {
} from 'antd'; } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { placementStrategyOptions } from '../config'; import { backendOptionsMap, placementStrategyOptions } from '../config';
import llamaConfig from '../config/llama-config';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import vllmConfig from '../config/vllm-config';
import dataformStyles from '../style/data-form.less'; import dataformStyles from '../style/data-form.less';
import GPUCard from './gpu-card'; import GPUCard from './gpu-card';
interface AdvanceConfigProps { interface AdvanceConfigProps {
isGGUF: boolean;
form: FormInstance; form: FormInstance;
gpuOptions: Array<any>; gpuOptions: Array<any>;
action: PageActionType;
} }
const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => { const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
const { form, gpuOptions } = props; const { form, gpuOptions, isGGUF, action } = props;
const intl = useIntl(); const intl = useIntl();
const wokerSelector = Form.useWatch('worker_selector', form); const wokerSelector = Form.useWatch('worker_selector', form);
const scheduleType = Form.useWatch('scheduleType', form); const scheduleType = Form.useWatch('scheduleType', form);
const backend = Form.useWatch('backend', form);
const placementStrategyTips = [ const placementStrategyTips = [
{ {
@@ -64,6 +72,10 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
} }
]; ];
const paramsConfig = useMemo(() => {
return backend === backendOptionsMap.llamaBox ? llamaConfig : vllmConfig;
}, [backend]);
const renderSelectTips = (list: Array<{ title: string; tips: string }>) => { const renderSelectTips = (list: Array<{ title: string; tips: string }>) => {
return ( return (
<div> <div>
@@ -96,6 +108,10 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
[] []
); );
const handleBackendParametersChange = useCallback((list: string[]) => {
form.setFieldValue('backend_parameters', list);
}, []);
const collapseItems = useMemo(() => { const collapseItems = useMemo(() => {
const children = ( const children = (
<> <>
@@ -202,33 +218,71 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
</SealSelect> </SealSelect>
</Form.Item> </Form.Item>
)} )}
<div style={{ paddingBottom: 22, paddingLeft: 10 }}> <Form.Item name="backend">
<Form.Item<FormData> <SealSelect
name="cpu_offloading" label={intl.formatMessage({ id: 'models.form.backend' })}
valuePropName="checked" options={[
style={{ padding: '0 10px', marginBottom: 0 }} {
noStyle label: `llama-box(llama.cpp)`,
> value: backendOptionsMap.llamaBox,
<Checkbox className="p-l-6"> disabled: !isGGUF
<Tooltip },
title={intl.formatMessage({ {
id: 'models.form.partialoffload.tips' label: 'vLLM',
})} value: backendOptionsMap.vllm,
> disabled: isGGUF
<span style={{ color: 'var(--ant-color-text-tertiary)' }}> }
{intl.formatMessage({ ]}
id: 'resources.form.enablePartialOffload' disabled={action === PageAction.EDIT}
></SealSelect>
</Form.Item>
<Form.Item<FormData> name="backend_parameters">
<ListInput
placeholder={
backend === backendOptionsMap.llamaBox
? intl.formatMessage({
id: 'models.form.backend_parameters.llamabox.placeholder'
})
: intl.formatMessage({
id: 'models.form.backend_parameters.vllm.placeholder'
})
}
btnText="common.button.addParams"
label={intl.formatMessage({ id: 'models.form.backend_parameters' })}
dataList={form.getFieldValue('backend_parameters') || []}
onChange={handleBackendParametersChange}
options={paramsConfig}
></ListInput>
</Form.Item>
{isGGUF && (
<div style={{ paddingBottom: 22, paddingLeft: 10 }}>
<Form.Item<FormData>
name="cpu_offloading"
valuePropName="checked"
style={{ padding: '0 10px', marginBottom: 0 }}
noStyle
>
<Checkbox className="p-l-6">
<Tooltip
title={intl.formatMessage({
id: 'models.form.partialoffload.tips'
})} })}
</span> >
<InfoCircleOutlined <span style={{ color: 'var(--ant-color-text-tertiary)' }}>
className="m-l-4" {intl.formatMessage({
style={{ color: 'var(--ant-color-text-tertiary)' }} id: 'resources.form.enablePartialOffload'
/> })}
</Tooltip> </span>
</Checkbox> <InfoCircleOutlined
</Form.Item> className="m-l-4"
</div> style={{ color: 'var(--ant-color-text-tertiary)' }}
{scheduleType === 'auto' && ( />
</Tooltip>
</Checkbox>
</Form.Item>
</div>
)}
{scheduleType === 'auto' && isGGUF && (
<div style={{ paddingBottom: 22, paddingLeft: 10 }}> <div style={{ paddingBottom: 22, paddingLeft: 10 }}>
<Form.Item<FormData> <Form.Item<FormData>
name="distributed_inference_across_workers" name="distributed_inference_across_workers"
@@ -266,16 +320,26 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
{intl.formatMessage({ id: 'resources.form.advanced' })} {intl.formatMessage({ id: 'resources.form.advanced' })}
</span> </span>
), ),
forceRender: true,
children children
} }
]; ];
}, [form, intl, gpuOptions, scheduleType, wokerSelector]); }, [
form,
intl,
gpuOptions,
paramsConfig,
scheduleType,
wokerSelector,
isGGUF
]);
return ( return (
<Collapse <Collapse
expandIconPosition="start" expandIconPosition="start"
bordered={false} bordered={false}
ghost ghost
destroyInactivePanel={false}
className={dataformStyles['advanced-collapse']} className={dataformStyles['advanced-collapse']}
expandIcon={({ isActive }) => ( expandIcon={({ isActive }) => (
<RightOutlined <RightOutlined
@@ -288,4 +352,4 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
); );
}; };
export default AdvanceConfig; export default React.memo(AdvanceConfig);
@@ -10,7 +10,7 @@ const ColumnWrapper: React.FC<any> = ({ children, footer, height }) => {
<div className="column-wrapper"> <div className="column-wrapper">
<SimpleBar <SimpleBar
style={{ style={{
height: height || 'calc(100vh - 89px)', maxHeight: height || 'calc(100vh - 89px)',
paddingBottom: '50px' paddingBottom: '50px'
}} }}
> >
@@ -23,7 +23,7 @@ const ColumnWrapper: React.FC<any> = ({ children, footer, height }) => {
} }
return ( return (
<div className="column-wrapper"> <div className="column-wrapper">
<SimpleBar style={{ height: height || 'calc(100vh - 89px)' }}> <SimpleBar style={{ maxHeight: height || 'calc(100vh - 89px)' }}>
{children} {children}
</SimpleBar> </SimpleBar>
</div> </div>
+99 -51
View File
@@ -1,19 +1,25 @@
import IconFont from '@/components/icon-font';
import SealAutoComplete from '@/components/seal-form/auto-complete'; import SealAutoComplete from '@/components/seal-form/auto-complete';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form, Tooltip, Typography } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { import React, {
forwardRef, forwardRef,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useMemo,
useState useState
} from 'react'; } from 'react';
import { queryGPUList } from '../apis'; import { queryGPUList } from '../apis';
import { modelSourceMap, ollamaModelOptions } from '../config'; import {
backendOptionsMap,
modelSourceMap,
ollamaModelOptions
} from '../config';
import { FormData, GPUListItem } from '../config/types'; import { FormData, GPUListItem } from '../config/types';
import AdvanceConfig from './advance-config'; import AdvanceConfig from './advance-config';
@@ -21,7 +27,8 @@ interface DataFormProps {
ref?: any; ref?: any;
source: string; source: string;
action: PageActionType; action: PageActionType;
repo: string; selectedModel: any;
isGGUF: boolean;
onOk: (values: FormData) => void; onOk: (values: FormData) => void;
} }
@@ -35,11 +42,20 @@ const sourceOptions = [
label: 'Ollama Library', label: 'Ollama Library',
value: modelSourceMap.ollama_library_value, value: modelSourceMap.ollama_library_value,
key: 'ollama_library' key: 'ollama_library'
},
{
label: 'ModelScope',
value: modelSourceMap.modelscope_value,
key: 'model_scope'
} }
]; ];
const SEARCH_SOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => { const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const { action, repo, onOk } = props; const { action, isGGUF, onOk } = props;
const [form] = Form.useForm(); const [form] = Form.useForm();
const intl = useIntl(); const intl = useIntl();
const [gpuOptions, setGpuOptions] = useState< const [gpuOptions, setGpuOptions] = useState<
@@ -86,22 +102,18 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
); );
const handleOnSelectModel = () => { const handleOnSelectModel = () => {
console.log('repo=============', repo); let name = _.split(props.selectedModel.name, '/').slice(-1)[0];
if (!repo) {
return;
}
let name = _.split(repo, '/').slice(-1)[0];
const reg = /(-gguf)$/i; const reg = /(-gguf)$/i;
name = _.toLower(name).replace(reg, ''); name = _.toLower(name).replace(reg, '');
if (props.source === modelSourceMap.huggingface_value) { if (SEARCH_SOURCE.includes(props.source)) {
form.setFieldsValue({ form.setFieldsValue({
huggingface_repo_id: repo, repo_id: props.selectedModel.name,
name: name name: name
}); });
} else { } else {
form.setFieldsValue({ form.setFieldsValue({
ollama_library_model_name: repo, ollama_library_model_name: props.selectedModel.name,
name: name name: name
}); });
} }
@@ -111,8 +123,8 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
return ( return (
<> <>
<Form.Item<FormData> <Form.Item<FormData>
name="huggingface_repo_id" name="repo_id"
key="huggingface_repo_id" key="repo_id"
rules={[ rules={[
{ {
required: true, required: true,
@@ -131,26 +143,29 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
disabled={true} disabled={true}
></SealInput.Input> ></SealInput.Input>
</Form.Item> </Form.Item>
<Form.Item<FormData> {isGGUF && (
name="huggingface_filename" <Form.Item<FormData>
rules={[ name="file_name"
{ key="file_name"
required: true, rules={[
message: intl.formatMessage( {
{ required: true,
id: 'common.form.rule.input' message: intl.formatMessage(
}, {
{ name: intl.formatMessage({ id: 'models.form.filename' }) } id: 'common.form.rule.input'
) },
} { name: intl.formatMessage({ id: 'models.form.filename' }) }
]} )
> }
<SealInput.Input ]}
label={intl.formatMessage({ id: 'models.form.filename' })} >
required <SealInput.Input
disabled={true} label={intl.formatMessage({ id: 'models.form.filename' })}
></SealInput.Input> required
</Form.Item> disabled={true}
></SealInput.Input>
</Form.Item>
)}
</> </>
); );
}; };
@@ -203,29 +218,49 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
> >
<SealAutoComplete <SealAutoComplete
filterOption filterOption
defaultActiveFirstOption
disabled={action === PageAction.EDIT} disabled={action === PageAction.EDIT}
options={ollamaModelOptions}
label={intl.formatMessage({ id: 'model.form.ollama.model' })} label={intl.formatMessage({ id: 'model.form.ollama.model' })}
placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })} placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })}
addAfter={
<Typography.Link
href="https://www.ollama.com/library"
target="_blank"
>
<Tooltip
title={intl.formatMessage({ id: 'models.form.ollamalink' })}
placement="topRight"
>
<IconFont
type="icon-external-link"
className="font-size-14"
></IconFont>
</Tooltip>
</Typography.Link>
}
required required
options={ollamaModelOptions}
></SealAutoComplete> ></SealAutoComplete>
</Form.Item> </Form.Item>
</> </>
); );
}; };
const renderFieldsBySource = () => { const renderFieldsBySource = useMemo(() => {
switch (props.source) { if (SEARCH_SOURCE.includes(props.source)) {
case modelSourceMap.huggingface_value: return renderHuggingfaceFields();
return renderHuggingfaceFields();
case modelSourceMap.ollama_library_value:
return renderOllamaModelFields();
case modelSourceMap.s3_value:
return renderS3Fields();
default:
return null;
} }
};
if (props.source === modelSourceMap.ollama_library_value) {
return renderOllamaModelFields();
}
if (props.source === modelSourceMap.s3_value) {
return renderS3Fields();
}
return null;
}, [props.source, isGGUF, intl]);
const handleOk = (formdata: FormData) => { const handleOk = (formdata: FormData) => {
const gpu = _.find(gpuOptions, (item: any) => { const gpu = _.find(gpuOptions, (item: any) => {
@@ -247,9 +282,17 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
} }
}; };
useEffect(() => {
if (action === PageAction.CREATE) {
form.setFieldValue(
'backend',
isGGUF ? backendOptionsMap.llamaBox : backendOptionsMap.vllm
);
}
}, [isGGUF]);
useEffect(() => { useEffect(() => {
handleOnSelectModel(); handleOnSelectModel();
}, [repo]); }, [props.selectedModel.name]);
return ( return (
<Form <Form
@@ -263,9 +306,9 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
replicas: 1, replicas: 1,
source: props.source, source: props.source,
placement_strategy: 'spread', placement_strategy: 'spread',
cpu_offloading: false, cpu_offloading: true,
scheduleType: 'auto', scheduleType: 'auto',
distributed_inference_across_workers: false distributed_inference_across_workers: true
}} }}
> >
<Form.Item<FormData> <Form.Item<FormData>
@@ -314,7 +357,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
></SealSelect> ></SealSelect>
} }
</Form.Item> </Form.Item>
{renderFieldsBySource()} {renderFieldsBySource}
<Form.Item<FormData> <Form.Item<FormData>
name="replicas" name="replicas"
rules={[ rules={[
@@ -347,7 +390,12 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
})} })}
></SealInput.TextArea> ></SealInput.TextArea>
</Form.Item> </Form.Item>
<AdvanceConfig form={form} gpuOptions={gpuOptions}></AdvanceConfig> <AdvanceConfig
form={form}
gpuOptions={gpuOptions}
isGGUF={isGGUF}
action={action}
></AdvanceConfig>
</Form> </Form>
); );
}); });
+64 -36
View File
@@ -36,30 +36,51 @@ const AddModal: React.FC<AddModalProps> = (props) => {
action, action,
width = 600 width = 600
} = props || {}; } = props || {};
const SEARCH_SOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const form = useRef<any>({}); const form = useRef<any>({});
const intl = useIntl(); const intl = useIntl();
const [huggingfaceRepoId, setHuggingfaceRepoId] = useState<string>(''); const [selectedModel, setSelectedModel] = useState<any>({});
const [collapsed, setCollapsed] = useState<boolean>(false); const [collapsed, setCollapsed] = useState<boolean>(false);
const [loadingModel, setLoadingModel] = useState<boolean>(false); const [loadingModel, setLoadingModel] = useState<boolean>(false);
const [isGGUF, setIsGGUF] = useState<boolean>(false);
const modelFileRef = useRef<any>(null);
const handleSelectModelFile = useCallback((item: any) => { const handleSelectModelFile = useCallback((item: any) => {
form.current?.setFieldValue?.('huggingface_filename', item.fakeName); form.current?.setFieldValue?.('file_name', item.fakeName);
}, []); }, []);
const handleOnSelectModel = (item: any) => { const handleOnSelectModel = (item: any) => {
setHuggingfaceRepoId(item.name); setSelectedModel(item);
}; };
const handleSumit = () => { const handleSumit = () => {
form.current?.submit?.(); form.current?.submit?.();
}; };
const handleSetIsGGUF = (flag: boolean) => {
setIsGGUF(flag);
if (flag) {
setTimeout(() => {
modelFileRef.current?.fetchModelFiles?.();
}, 50);
}
};
useEffect(() => { useEffect(() => {
return () => { return () => {
setHuggingfaceRepoId(''); setSelectedModel({});
}; };
}, [open]); }, [open]);
useEffect(() => {
if (source === modelSourceMap.ollama_library_value) {
setIsGGUF(true);
}
}, [source]);
return ( return (
<Drawer <Drawer
title={ title={
@@ -91,41 +112,47 @@ const AddModal: React.FC<AddModalProps> = (props) => {
overflowX: 'hidden' overflowX: 'hidden'
}, },
content: { content: {
borderRadius: '8px 0 0 8px' borderRadius: '6px 0 0 6px'
} }
}} }}
width={width} width={width}
footer={false} footer={false}
> >
<div style={{ display: 'flex' }}> <div style={{ display: 'flex', height: '100%' }}>
{props.source === modelSourceMap.huggingface_value && ( {SEARCH_SOURCE.includes(props.source) && (
<div style={{ display: 'flex', flex: 1 }}> <>
<ColumnWrapper> <div style={{ display: 'flex', flex: 1 }}>
<SearchModel <ColumnWrapper>
modelSource={props.source} <SearchModel
onSelectModel={handleOnSelectModel} modelSource={props.source}
setLoadingModel={setLoadingModel} onSelectModel={handleOnSelectModel}
></SearchModel> setLoadingModel={setLoadingModel}
</ColumnWrapper> ></SearchModel>
<Separator></Separator> </ColumnWrapper>
</div> <Separator></Separator>
)} </div>
{props.source === modelSourceMap.huggingface_value && ( <div style={{ display: 'flex', flex: 1 }}>
<div style={{ display: 'flex', flex: 1 }}> <ColumnWrapper>
<ColumnWrapper> <ModelCard
<ModelCard selectedModel={selectedModel}
repo={huggingfaceRepoId} onCollapse={setCollapsed}
onCollapse={setCollapsed} collapsed={collapsed}
collapsed={collapsed} modelSource={props.source}
></ModelCard> setIsGGUF={handleSetIsGGUF}
<HFModelFile ></ModelCard>
repo={huggingfaceRepoId} {isGGUF && (
onSelectFile={handleSelectModelFile} <HFModelFile
collapsed={collapsed} ref={modelFileRef}
></HFModelFile> selectedModel={selectedModel}
</ColumnWrapper> modelSource={props.source}
<Separator></Separator> onSelectFile={handleSelectModelFile}
</div> collapsed={collapsed}
></HFModelFile>
)}
</ColumnWrapper>
<Separator></Separator>
</div>
</>
)} )}
<ColumnWrapper <ColumnWrapper
footer={ footer={
@@ -141,7 +168,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
} }
> >
<> <>
{source === modelSourceMap.huggingface_value && ( {SEARCH_SOURCE.includes(source) && (
<TitleWrapper> <TitleWrapper>
{intl.formatMessage({ id: 'models.form.configurations' })} {intl.formatMessage({ id: 'models.form.configurations' })}
<span style={{ display: 'flex', height: 24 }}></span> <span style={{ display: 'flex', height: 24 }}></span>
@@ -150,9 +177,10 @@ const AddModal: React.FC<AddModalProps> = (props) => {
<DataForm <DataForm
source={source} source={source}
action={action} action={action}
repo={huggingfaceRepoId} selectedModel={selectedModel}
onOk={onOk} onOk={onOk}
ref={form} ref={form}
isGGUF={isGGUF}
></DataForm> ></DataForm>
</> </>
</ColumnWrapper> </ColumnWrapper>
+31 -20
View File
@@ -1,3 +1,4 @@
import AutoTooltip from '@/components/auto-tooltip';
import { convertFileSize } from '@/utils'; import { convertFileSize } from '@/utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import _ from 'lodash'; import _ from 'lodash';
@@ -6,33 +7,43 @@ import '../style/gpu-card.less';
const GPUCard: React.FC<{ const GPUCard: React.FC<{
data: any; data: any;
}> = ({ data }) => { header?: React.ReactNode;
info?: React.ReactNode;
}> = ({ data, header, info }) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
<div className="gpu-card"> <div className="gpu-card">
<div className="header"> <div className="header" style={{ width: '100%' }}>
{data.label}({data.worker_name})[ {header ?? (
{intl.formatMessage({ id: 'resources.table.index' })}:{data.index}] <AutoTooltip ghost>
{data.label}({data.worker_name})[
{intl.formatMessage({ id: 'resources.table.index' })}:{data.index}]
</AutoTooltip>
)}
</div> </div>
<div className="info"> <div className="info">
<span> {info ?? (
{intl.formatMessage({ id: 'resources.table.vram' })}( <>
{intl.formatMessage({ id: 'resources.table.used' })}/ <span>
{intl.formatMessage({ id: 'resources.table.total' })}):{' '} {intl.formatMessage({ id: 'resources.table.vram' })}(
<span> {intl.formatMessage({ id: 'resources.table.used' })}/
{convertFileSize(data?.memory?.used || 0)} /{' '} {intl.formatMessage({ id: 'resources.table.total' })}):{' '}
{convertFileSize(data?.memory?.total || 0)} <span>
</span> {convertFileSize(data?.memory?.used || 0)} /{' '}
</span> {convertFileSize(data?.memory?.total || 0)}
<span> </span>
<span> </span>
{intl.formatMessage({ id: 'resources.table.gpuutilization' })}:{' '} <span>
</span> <span>
{_.round(data?.memory?.utilization_rate || 0, 2)}% {intl.formatMessage({ id: 'resources.table.gpuutilization' })}:{' '}
</span> </span>
{_.round(data?.memory?.utilization_rate || 0, 2)}%
</span>
</>
)}
</div> </div>
</div> </div>
); );
}; };
export default GPUCard; export default React.memo(GPUCard);
+92 -28
View File
@@ -4,26 +4,41 @@ import { useIntl } from '@umijs/max';
import { Col, Empty, Row, Select, Spin, Tag, Tooltip } from 'antd'; import { Col, Empty, Row, Select, Spin, Tag, Tooltip } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import { memo, useCallback, useEffect, useRef, useState } from 'react'; import {
forwardRef,
memo,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState
} from 'react';
import SimpleBar from 'simplebar-react'; import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css'; import 'simplebar-react/dist/simplebar.min.css';
import { queryHuggingfaceModelFiles } from '../apis'; import { queryHuggingfaceModelFiles, queryModelScopeModelFiles } from '../apis';
import { modelSourceMap } from '../config';
import { getFileType } from '../config/file-type'; import { getFileType } from '../config/file-type';
import '../style/hf-model-file.less'; import '../style/hf-model-file.less';
import FileParts from './file-parts'; import FileParts from './file-parts';
import TitleWrapper from './title-wrapper'; import TitleWrapper from './title-wrapper';
interface HFModelFileProps { interface HFModelFileProps {
repo: string; selectedModel: any;
collapsed?: boolean; collapsed?: boolean;
loadingModel?: boolean; loadingModel?: boolean;
modelSource: string;
ref: any;
onSelectFile?: (file: any) => void; onSelectFile?: (file: any) => void;
} }
const pattern = /^(.*)-(\d+)-of-(\d+)\.gguf$/; const pattern = /^(.*)-(\d+)-of-(\d+)\.(.*)$/;
const HFModelFile: React.FC<HFModelFileProps> = (props) => { const filterReg = /\.(safetensors|gguf)$/i;
const { collapsed, loadingModel } = props; const includeReg = /\.(safetensors|gguf)$/i;
const filterRegGGUF = /\.(gguf)$/i;
const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
const { collapsed, modelSource } = props;
const intl = useIntl(); const intl = useIntl();
const [dataSource, setDataSource] = useState<any>({ const [dataSource, setDataSource] = useState<any>({
fileList: [], fileList: [],
@@ -56,7 +71,8 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
return { return {
filename: match[1], filename: match[1],
part: parseInt(match[2], 10), part: parseInt(match[2], 10),
total: parseInt(match[3], 10) total: parseInt(match[3], 10),
extension: match[4]
}; };
} else { } else {
return null; return null;
@@ -99,7 +115,7 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
(value: any[], filename: string) => { (value: any[], filename: string) => {
return { return {
path: filename, path: filename,
fakeName: `${filename}*.gguf`, fakeName: `${filename}*.${_.get(value, '[0].extension')}`,
size: _.sumBy(value, 'size'), size: _.sumBy(value, 'size'),
parts: value parts: value
}; };
@@ -109,19 +125,11 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
return [...shardFileListResult, ...newGeneralFileList]; return [...shardFileListResult, ...newGeneralFileList];
}, []); }, []);
const handleFetchModelFiles = async () => { // hugging face files
if (!props.repo) { const getHuggingfaceFiles = async () => {
setDataSource({ fileList: [], loading: false });
handleSelectModelFile({});
return;
}
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
setDataSource({ ...dataSource, loading: true });
setCurrent('');
try { try {
const res = await queryHuggingfaceModelFiles( const res = await queryHuggingfaceModelFiles(
{ repo: props.repo }, { repo: props.selectedModel.name || '' },
{ {
signal: axiosTokenRef.current.signal signal: axiosTokenRef.current.signal
} }
@@ -131,9 +139,61 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
}); });
const list = _.filter(fileList, (file: any) => { const list = _.filter(fileList, (file: any) => {
return _.endsWith(file.path, '.gguf') || _.includes(file.path, '.gguf'); return filterRegGGUF.test(file.path) || _.includes(file.path, '.gguf');
}); });
return list;
} catch (error) {
return [];
}
};
// modelscope files
const getModelScopeFiles = async () => {
try {
const data = await queryModelScopeModelFiles(
{
name: props.selectedModel.name || '',
revision: props.selectedModel.revision || 'master'
},
{
signal: axiosTokenRef.current.signal
}
);
const fileList = _.filter(_.get(data, ['Data', 'Files']), (file: any) => {
return filterRegGGUF.test(file.Path) || _.includes(file.Path, '.gguf');
});
const list = _.map(fileList, (item: any) => {
return {
path: item.Path,
size: item.Size
};
});
return list;
} catch (error) {
return [];
}
};
const handleFetchModelFiles = async () => {
if (!props.selectedModel.name) {
setDataSource({ fileList: [], loading: false });
handleSelectModelFile({});
return;
}
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
setDataSource({ ...dataSource, loading: true });
setCurrent('');
try {
let list = [];
if (modelSourceMap.huggingface_value === modelSource) {
list = await getHuggingfaceFiles();
} else if (modelSourceMap.modelscope_value === modelSource) {
list = await getModelScopeFiles();
}
const newList = generateGroupByFilename(list); const newList = generateGroupByFilename(list);
const sortList = _.sortBy(newList, (item: any) => { const sortList = _.sortBy(newList, (item: any) => {
return sortType === 'size' ? item.size : item.path; return sortType === 'size' ? item.size : item.path;
@@ -183,10 +243,16 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
handleSelectModelFile(item); handleSelectModelFile(item);
} }
}; };
useImperativeHandle(ref, () => ({
fetchModelFiles: handleFetchModelFiles
}));
useEffect(() => { useEffect(() => {
handleFetchModelFiles(); if (!props.selectedModel.name) {
}, [props.repo]); setDataSource({ fileList: [], loading: false });
handleSelectModelFile({});
}
}, [props.selectedModel.name]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -197,7 +263,7 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
return ( return (
<div> <div>
<TitleWrapper> <TitleWrapper>
<span> <span className="title">
{intl.formatMessage({ id: 'models.available.files' })} ( {intl.formatMessage({ id: 'models.available.files' })} (
{dataSource.fileList.length || 0}) {dataSource.fileList.length || 0})
</span> </span>
@@ -252,11 +318,9 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
{getModelQuantizationType(item)} {getModelQuantizationType(item)}
{item.parts && item.parts.length > 1 && ( {item.parts && item.parts.length > 1 && (
<Tooltip <Tooltip
color="var(--color-white-1)"
overlayInnerStyle={{ overlayInnerStyle={{
width: 150, width: 180,
padding: 0, padding: 0
color: 'var(--ant-color-text-secondary)'
}} }}
title={ title={
<FileParts fileList={item.parts}></FileParts> <FileParts fileList={item.parts}></FileParts>
@@ -299,6 +363,6 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
</SimpleBar> </SimpleBar>
</div> </div>
); );
}; });
export default memo(HFModelFile); export default memo(HFModelFile);
@@ -23,7 +23,12 @@ interface HFModelItemProps {
source?: string; source?: string;
tags?: string[]; tags?: string[];
} }
const warningTask = ['image', 'audio', 'video']; const warningTask = ['audio', 'video'];
const SUPPORTEDSOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const HFModelItem: React.FC<HFModelItemProps> = (props) => { const HFModelItem: React.FC<HFModelItemProps> = (props) => {
const intl = useIntl(); const intl = useIntl();
@@ -57,7 +62,7 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
)} )}
</div> </div>
<div className="info"> <div className="info">
{props.source === modelSourceMap.huggingface_value ? ( {SUPPORTEDSOURCE.includes(props.source || '') ? (
<div className="info-item"> <div className="info-item">
{/* {props.task && ( {/* {props.task && (
<Tag <Tag
@@ -1,3 +1,4 @@
import AutoTooltip from '@/components/auto-tooltip';
import DropdownButtons from '@/components/drop-down-buttons'; import DropdownButtons from '@/components/drop-down-buttons';
import RowChildren from '@/components/seal-table/components/row-children'; import RowChildren from '@/components/seal-table/components/row-children';
import SimpleTabel from '@/components/simple-table'; import SimpleTabel from '@/components/simple-table';
@@ -23,6 +24,7 @@ interface InstanceItemProps {
list: ModelInstanceListItem[]; list: ModelInstanceListItem[];
gpuDeviceList: GPUDeviceItem[]; gpuDeviceList: GPUDeviceItem[];
workerList: WorkerListItem[]; workerList: WorkerListItem[];
modelData?: any;
handleChildSelect: ( handleChildSelect: (
val: string, val: string,
item: ModelInstanceListItem, item: ModelInstanceListItem,
@@ -33,6 +35,7 @@ interface InstanceItemProps {
const InstanceItem: React.FC<InstanceItemProps> = ({ const InstanceItem: React.FC<InstanceItemProps> = ({
list, list,
workerList, workerList,
modelData,
handleChildSelect handleChildSelect
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@@ -96,7 +99,11 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
<div>{workerIp}</div> <div>{workerIp}</div>
<div> <div>
{intl.formatMessage({ id: 'models.table.gpuindex' })}: [ {intl.formatMessage({ id: 'models.table.gpuindex' })}: [
{_.join(item.gpu_indexes, ',')}] {_.join(item.gpu_indexes?.sort?.(), ',')}]
</div>
<div>
{intl.formatMessage({ id: 'models.form.backend' })}:{' '}
{modelData?.backend || ''}
</div> </div>
</div> </div>
); );
@@ -119,7 +126,7 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
worker_name: `${row.worker_name}`, worker_name: `${row.worker_name}`,
worker_ip: `${row.worker_ip}`, worker_ip: `${row.worker_ip}`,
port: row.port, port: row.port,
gpu_index: `${row.gpu_indexes} (main)` gpu_index: `${row.gpu_indexes?.sort?.()} (main)`
} }
]; ];
@@ -153,10 +160,14 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
paddingInline: 'var(--ant-table-cell-padding-inline)' paddingInline: 'var(--ant-table-cell-padding-inline)'
}} }}
> >
<Tooltip title={renderWorkerInfo(item)}> <span className="flex-center">
<span className="m-r-5">{item.name}</span> <AutoTooltip title={item.name} ghost>
<InfoCircleOutlined /> <span className="m-r-5">{item.name}</span>
</Tooltip> </AutoTooltip>
<Tooltip title={renderWorkerInfo(item)}>
<InfoCircleOutlined />
</Tooltip>
</span>
</Col> </Col>
<Col span={6}> <Col span={6}>
<span <span
@@ -243,9 +254,17 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
: undefined : undefined
} }
statusValue={{ statusValue={{
status: status[item.state] as any, status:
item.state === InstanceStatusMap.Downloading &&
item.download_progress === 100
? status[InstanceStatusMap.Running]
: (status[item.state] as any),
text: InstanceStatusMapValue[item.state], text: InstanceStatusMapValue[item.state],
message: item.state_message message:
item.state === InstanceStatusMap.Downloading &&
item.download_progress === 100
? ''
: item.state_message
}} }}
></StatusTag> ></StatusTag>
)} )}
+166 -37
View File
@@ -1,5 +1,5 @@
import HighlightCode from '@/components/highlight-code';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import MarkdownViewer from '@/components/markdown-viewer';
import useRequestToken from '@/hooks/use-request-token'; import useRequestToken from '@/hooks/use-request-token';
import { import {
DownOutlined, DownOutlined,
@@ -7,27 +7,38 @@ import {
RightOutlined RightOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Empty, Tag, Tooltip } from 'antd'; import { Button, Empty, Spin, Tag, Tooltip } from 'antd';
import { some } from 'lodash';
import 'overlayscrollbars/overlayscrollbars.css';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import SimpleBar from 'simplebar-react'; import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css'; import 'simplebar-react/dist/simplebar.min.css';
import { downloadModelFile, queryHuggingfaceModelDetail } from '../apis'; import {
downloadModelFile,
queryHuggingfaceModelDetail,
queryModelScopeModelDetail
} from '../apis';
import { modelSourceMap } from '../config';
import '../style/model-card.less'; import '../style/model-card.less';
import TitleWrapper from './title-wrapper'; import TitleWrapper from './title-wrapper';
const ModelCard: React.FC<{ const ModelCard: React.FC<{
repo: string;
onCollapse: (flag: boolean) => void; onCollapse: (flag: boolean) => void;
setIsGGUF: (flag: boolean) => void;
selectedModel: any;
collapsed: boolean; collapsed: boolean;
loadingModel?: boolean; loadingModel?: boolean;
modelSource: string;
}> = (props) => { }> = (props) => {
const { repo, onCollapse, collapsed, loadingModel } = props; const { onCollapse, setIsGGUF, collapsed, modelSource } = props;
const intl = useIntl(); const intl = useIntl();
const requestSource = useRequestToken(); const requestSource = useRequestToken();
const [modelData, setModelData] = useState<any>({}); const [modelData, setModelData] = useState<any>(null);
const [readmeText, setReadmeText] = useState<string | null>(null); const [readmeText, setReadmeText] = useState<string | null>(null);
const requestToken = useRef<any>(null); const requestToken = useRef<any>(null);
const axiosTokenRef = useRef<any>(null); const axiosTokenRef = useRef<any>(null);
const [isGGUFModel, setIsGGUFModel] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const loadFile = async (repo: string, sha: string) => { const loadFile = async (repo: string, sha: string) => {
try { try {
@@ -49,38 +60,138 @@ const ModelCard: React.FC<{
} }
}; };
const getModelCardData = async () => { // huggingface model card data
if (!repo) { const getHuggingfaceModelDetail = async () => {
setModelData(null);
return;
}
requestToken.current?.cancel?.();
requestToken.current = requestSource();
try { try {
const [modelcard, readme] = await Promise.all([ const [modelcard, readme] = await Promise.all([
queryHuggingfaceModelDetail( queryHuggingfaceModelDetail(
{ repo }, { repo: props.selectedModel.name },
{ {
token: requestToken.current.token token: requestToken.current.token
} }
), ),
loadFile(repo, 'main') loadFile(props.selectedModel.name, 'main')
]); ]);
setModelData(modelcard); setModelData(modelcard);
setReadmeText(readme); setReadmeText(readme);
const isGGUF = modelcard.tags?.includes('gguf');
setIsGGUF(isGGUF);
setIsGGUFModel(isGGUF);
} catch (error) { } catch (error) {
setModelData({}); setModelData(null);
setReadmeText(null);
setIsGGUF(false);
setIsGGUFModel(false);
} }
}; };
const getModelScopeModelDetail = async () => {
try {
const data = await queryModelScopeModelDetail(
{
name: props.selectedModel.name
},
{
token: requestToken.current.token
}
);
setModelData({
...data?.Data,
name: `${data.Data?.Path}/${data.Data?.Name}`
});
setReadmeText(data?.Data?.ReadMeContent);
const isGGUF = some(
data?.Data?.Tags,
(tag: string) => tag?.indexOf('gguf') > -1
);
setIsGGUF(isGGUF);
setIsGGUFModel(isGGUF);
} catch (error) {
setModelData(null);
setReadmeText(null);
setIsGGUF(false);
setIsGGUFModel(false);
}
};
const getModelCardData = async () => {
if (!props.selectedModel.name) {
setModelData(null);
setReadmeText(null);
return;
}
requestToken.current?.cancel?.();
requestToken.current = requestSource();
setLoading(true);
if (modelSource === modelSourceMap.huggingface_value) {
await getHuggingfaceModelDetail();
} else if (modelSource === modelSourceMap.modelscope_value) {
await getModelScopeModelDetail();
}
setLoading(false);
};
const handleCollapse = useCallback(() => { const handleCollapse = useCallback(() => {
onCollapse(!collapsed); onCollapse(!collapsed);
}, [collapsed]); }, [collapsed]);
const generateModelLink = () => {
const name = modelData?.id || modelData?.name;
if (!name) {
return null;
}
if (modelSource === modelSourceMap.huggingface_value) {
return (
<Tooltip title={intl.formatMessage({ id: 'models.viewin.hf' })}>
<Button
size="small"
type="link"
target="_blank"
href={`https://huggingface.co/${modelData?.id}`}
>
<IconFont
type="icon-external-link"
className="font-size-14"
></IconFont>
</Button>
</Tooltip>
);
}
if (modelSource === modelSourceMap.modelscope_value) {
return (
<Tooltip title={intl.formatMessage({ id: 'models.viewin.modelscope' })}>
<Button
size="small"
type="link"
target="_blank"
href={`https://modelscope.cn/models/${modelData?.name}`}
>
<IconFont
type="icon-external-link"
className="font-size-14"
></IconFont>
</Button>
</Tooltip>
);
}
return null;
};
const generateModeScopeImgLink = useCallback(
(imgSrc: string) => {
if (!imgSrc) {
return '';
}
return `https://modelscope.cn/api/v1/models/${modelData?.name}/repo?Revision=${modelData?.Revision}&View=true&FilePath=${imgSrc}`;
},
[modelData]
);
useEffect(() => { useEffect(() => {
getModelCardData(); getModelCardData();
}, [repo]); }, [props.selectedModel.name]);
useEffect(() => { useEffect(() => {
if (!readmeText) { if (!readmeText) {
@@ -98,24 +209,13 @@ const ModelCard: React.FC<{
return ( return (
<> <>
<TitleWrapper> <TitleWrapper>
<div className="title">{modelData?.id} </div> <div className="title">{modelData?.id || modelData?.name} </div>
{modelData?.id && ( {generateModelLink()}
<Tooltip title={intl.formatMessage({ id: 'models.viewin.hf' })}>
<Button
size="small"
type="link"
target="_blank"
href={`https://huggingface.co/${modelData.id}`}
>
<IconFont type="icon-external-link"></IconFont>
</Button>
</Tooltip>
)}
</TitleWrapper> </TitleWrapper>
<div className="card-wrapper"> <div className="card-wrapper">
{modelData ? ( {modelData ? (
<div className="model-card-wrap"> <div className="model-card-wrap">
<div className="flex-between flex-center"> <div className="flex-center">
{modelData.config?.model_type && ( {modelData.config?.model_type && (
<Tag className="tag-item" color="gold"> <Tag className="tag-item" color="gold">
<span style={{ opacity: 0.65 }}> <span style={{ opacity: 0.65 }}>
@@ -126,8 +226,13 @@ const ModelCard: React.FC<{
</span> </span>
</Tag> </Tag>
)} )}
{isGGUFModel && (
<Tag className="tag-item" color="magenta">
<span style={{ opacity: 0.65 }}>GGUF</span>
</Tag>
)}
</div> </div>
{readmeText && ( {readmeText && isGGUFModel && (
<div <div
style={{ style={{
borderRadius: 4, borderRadius: 4,
@@ -146,15 +251,19 @@ const ModelCard: React.FC<{
</span> </span>
<SimpleBar <SimpleBar
style={{ style={{
paddingTop: collapsed ? 12 : 0,
maxHeight: collapsed ? 300 : 0 maxHeight: collapsed ? 300 : 0
}} }}
> >
<HighlightCode <MarkdownViewer
code={readmeText} generateImgLink={
lang="markdown" modelSource === modelSourceMap.modelscope_value
copyable={false} ? generateModeScopeImgLink
: undefined
}
content={readmeText}
theme="light" theme="light"
></HighlightCode> ></MarkdownViewer>
</SimpleBar> </SimpleBar>
</div> </div>
)} )}
@@ -166,6 +275,26 @@ const ModelCard: React.FC<{
></Empty> ></Empty>
)} )}
</div> </div>
{!isGGUFModel && readmeText && (
<div>
<TitleWrapper>
<div className="title">README.md</div>
</TitleWrapper>
<div className="card-wrapper">
<Spin spinning={loading}>
<MarkdownViewer
generateImgLink={
modelSource === modelSourceMap.modelscope_value
? generateModeScopeImgLink
: undefined
}
content={readmeText}
theme="light"
></MarkdownViewer>
</Spin>
</div>
</div>
)}
</> </>
); );
}; };
+21 -17
View File
@@ -1,41 +1,45 @@
import IconFont from '@/components/icon-font';
import hotkeys from '@/config/hotkeys'; import hotkeys from '@/config/hotkeys';
import { SearchOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Input } from 'antd'; import { Input } from 'antd';
import React, { useRef, useState } from 'react'; import React, { useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { modelSourceMap, modelSourceValueMap } from '../config';
const SearchInput: React.FC<{ const SearchInput: React.FC<{
modelSource: string;
onSearch: (e: any) => void; onSearch: (e: any) => void;
}> = (props) => { }> = (props) => {
const { onSearch } = props; const { onSearch, modelSource } = props;
const intl = useIntl(); const intl = useIntl();
const [isFocus, setIsFocus] = useState(false);
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
useHotkeys(hotkeys.INPUT.join(','), () => { useHotkeys(hotkeys.FOCUS, (e: any) => {
e.preventDefault();
inputRef.current?.focus?.(); inputRef.current?.focus?.();
setIsFocus(true);
}); });
return ( return (
<Input <Input
ref={inputRef} ref={inputRef}
onPressEnter={onSearch} onPressEnter={onSearch}
onFocus={() => setIsFocus(true)}
onBlur={() => setIsFocus(false)}
allowClear allowClear
placeholder={intl.formatMessage({ placeholder={intl.formatMessage(
id: 'model.deploy.search.placeholder' {
})} id: 'model.deploy.search.placeholder'
},
{ source: modelSourceValueMap[modelSource] }
)}
prefix={ prefix={
<> <>
<SearchOutlined <IconFont
style={{ className="font-size-16"
fontSize: '16px', type={
color: 'var(--ant-color-text-quaternary)' modelSource === modelSourceMap.huggingface_value
}} ? 'icon-huggingface'
/> : 'icon-tu2'
}
></IconFont>
</> </>
} }
></Input> ></Input>
+133 -108
View File
@@ -1,10 +1,15 @@
import { BulbOutlined } from '@ant-design/icons'; import { BulbOutlined, InfoCircleOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Input, Select } from 'antd'; import { Checkbox, Select, Tooltip } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { queryHuggingfaceModels } from '../apis'; import { queryHuggingfaceModels, queryModelScopeModels } from '../apis';
import { ModelSortType, modelSourceMap, ollamaModelOptions } from '../config'; import {
ModelScopeSortType,
ModelSortType,
modelSourceMap,
ollamaModelOptions
} from '../config';
import SearchStyle from '../style/search-result.less'; import SearchStyle from '../style/search-result.less';
import SearchInput from './search-input'; import SearchInput from './search-input';
import SearchResult from './search-result'; import SearchResult from './search-result';
@@ -17,7 +22,6 @@ interface SearchInputProps {
} }
const SearchModel: React.FC<SearchInputProps> = (props) => { const SearchModel: React.FC<SearchInputProps> = (props) => {
console.log('SearchModel======');
const intl = useIntl(); const intl = useIntl();
const { modelSource, setLoadingModel, onSourceChange, onSelectModel } = props; const { modelSource, setLoadingModel, onSourceChange, onSelectModel } = props;
const [dataSource, setDataSource] = useState<{ const [dataSource, setDataSource] = useState<{
@@ -31,11 +35,15 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
networkError: false, networkError: false,
sortType: ModelSortType.trendingScore sortType: ModelSortType.trendingScore
}); });
const SUPPORTEDSOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const [current, setCurrent] = useState<string>(''); const [current, setCurrent] = useState<string>('');
const cacheRepoOptions = useRef<any[]>([]); const cacheRepoOptions = useRef<any[]>([]);
const axiosTokenRef = useRef<any>(null); const axiosTokenRef = useRef<any>(null);
const customOllamaModelRef = useRef<any>(null);
const searchInputRef = useRef<any>(''); const searchInputRef = useRef<any>('');
const filterGGUFRef = useRef<boolean | undefined>();
const modelFilesSortOptions = useRef<any[]>([ const modelFilesSortOptions = useRef<any[]>([
{ {
label: intl.formatMessage({ id: 'models.sort.trending' }), label: intl.formatMessage({ id: 'models.sort.trending' }),
@@ -56,16 +64,76 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
]); ]);
const handleOnSelectModel = useCallback((item: any) => { const handleOnSelectModel = useCallback((item: any) => {
console.log('handleOnSelectModel', item);
onSelectModel(item); onSelectModel(item);
setCurrent(item.id); setCurrent(item.id);
}, []); }, []);
// huggeface
const getModelsFromHuggingface = useCallback(async (sort: string) => {
try {
const task: any = searchInputRef.current ? '' : 'text-generation';
const params = {
search: {
query: searchInputRef.current || '',
sort: sort,
tags: filterGGUFRef.current ? ['gguf'] : [],
task
}
};
const data = await queryHuggingfaceModels(params, {
signal: axiosTokenRef.current.signal
});
let list = _.map(data || [], (item: any) => {
return {
...item,
value: item.name,
label: item.name
};
});
return list;
} catch (error) {
return [];
}
}, []);
// modelscope
const getModelsFromModelscope = useCallback(async (sort: string) => {
try {
const params = {
Name: `${searchInputRef.current}`,
filterGGUF: filterGGUFRef.current,
SortBy: ModelScopeSortType[sort]
};
const data = await queryModelScopeModels(params, {
signal: axiosTokenRef.current.signal
});
let list = _.map(_.get(data, 'Data.Model.Models') || [], (item: any) => {
return {
path: item.Path,
name: `${item.Path}/${item.Name}`,
downloads: item.Downloads,
id: `${item.Path}/${item.Name}`,
updatedAt: item.LastUpdatedTime * 1000,
likes: item.Stars,
value: item.Name,
label: item.Name,
revision: item.Revision,
task: item.Tasks?.map((sItem: any) => sItem.Name).join(',')
};
});
return list;
} catch (error) {
return [];
}
}, []);
const handleOnSearchRepo = useCallback( const handleOnSearchRepo = useCallback(
async (sortType?: string) => { async (sortType?: string) => {
if (modelSource === modelSourceMap.ollama_library_value) { if (!SUPPORTEDSOURCE.includes(modelSource)) {
return; return;
} }
console.log('handleOnSearchRepo', dataSource.loading);
axiosTokenRef.current?.abort?.(); axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController(); axiosTokenRef.current = new AbortController();
if (dataSource.loading) return; if (dataSource.loading) return;
@@ -77,26 +145,12 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
}); });
setLoadingModel?.(true); setLoadingModel?.(true);
cacheRepoOptions.current = []; cacheRepoOptions.current = [];
const task: any = searchInputRef.current ? '' : 'text-generation'; let list: any[] = [];
const params = { if (modelSource === modelSourceMap.huggingface_value) {
search: { list = await getModelsFromHuggingface(sort);
query: searchInputRef.current || '', } else if (modelSource === modelSourceMap.modelscope_value) {
sort: sort, list = await getModelsFromModelscope(sort);
tags: ['gguf'], }
task
}
};
const models = await queryHuggingfaceModels(params, {
signal: axiosTokenRef.current.signal
});
let list = _.map(models || [], (item: any) => {
return {
...item,
value: item.name,
label: item.name
};
});
cacheRepoOptions.current = list; cacheRepoOptions.current = list;
setDataSource({ setDataSource({
repoOptions: list, repoOptions: list,
@@ -133,7 +187,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
if ( if (
!dataSource.repoOptions.length && !dataSource.repoOptions.length &&
!cacheRepoOptions.current.length && !cacheRepoOptions.current.length &&
modelSource === modelSourceMap.huggingface_value SUPPORTEDSOURCE.includes(modelSource)
) { ) {
handleOnSearchRepo(); handleOnSearchRepo();
} }
@@ -149,59 +203,23 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
} }
}; };
const handleFilterModels = (e: any) => {
const text = e.target.value;
const list = _.filter(cacheRepoOptions.current, (item: any) => {
return item.name.includes(text);
});
setDataSource({
repoOptions: list,
loading: false,
networkError: false,
sortType: dataSource.sortType
});
};
const debounceFilter = _.debounce((e: any) => {
handleFilterModels(e);
}, 300);
const handleSourceChange = (source: string) => {
axiosTokenRef.current?.abort?.();
onSourceChange?.(source);
setDataSource({
repoOptions: [],
loading: false,
networkError: false,
sortType: dataSource.sortType
});
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 handleSortChange = (value: string) => {
handleOnSearchRepo(value || ''); handleOnSearchRepo(value || '');
}; };
const handleFilterGGUFChange = (e: any) => {
console.log('filterggufChange:', e.target.checked);
filterGGUFRef.current = e.target.checked;
handleOnSearchRepo();
};
const renderHFSearch = () => { const renderHFSearch = () => {
return ( return (
<> <>
<SearchInput onSearch={handlerSearchModels}></SearchInput> <SearchInput
onSearch={handlerSearchModels}
modelSource={modelSource}
></SearchInput>
<div className={SearchStyle.filter}> <div className={SearchStyle.filter}>
<span> <span>
<span className="value"> <span className="value">
@@ -211,38 +229,45 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
)} )}
</span> </span>
</span> </span>
<Select <span>
allowClear <Checkbox
value={dataSource.sortType} onChange={handleFilterGGUFChange}
onChange={handleSortChange} className="m-r-5"
labelRender={({ label }) => { checked={filterGGUFRef.current}
return ( >
<span> <Tooltip
{intl.formatMessage({ id: 'model.deploy.sort' })}: {label} overlayInnerStyle={{ width: 'max-content' }}
</span> title={
); <div>
}} <div>
options={modelFilesSortOptions.current} {intl.formatMessage({ id: 'models.search.gguf.tips' })}
size="middle" </div>
style={{ width: '150px' }} <div>
></Select> {intl.formatMessage({ id: 'models.search.vllm.tips' })}
</div> </div>
</> </div>
); }
}; >
GGUF
const renderOllamaCustom = () => { <InfoCircleOutlined className="m-l-4" />
return ( </Tooltip>
<> </Checkbox>
<Input <Select
allowClear allowClear
placeholder="Input ollama model name" value={dataSource.sortType}
onChange={handleInputChange} onChange={handleSortChange}
></Input> labelRender={({ label }) => {
<div className={SearchStyle.filter}> return (
<Button type="primary" onClick={handleConfirm}> <span>
{intl.formatMessage({ id: 'common.button.confirm' })} {intl.formatMessage({ id: 'model.deploy.sort' })}: {label}
</Button> </span>
);
}}
options={modelFilesSortOptions.current}
size="middle"
style={{ width: '150px' }}
></Select>
</span>
</div> </div>
</> </>
); );
@@ -262,7 +287,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
return ( return (
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div className={SearchStyle['search-bar']}> <div className={SearchStyle['search-bar']}>
{modelSource === modelSourceMap.huggingface_value ? ( {SUPPORTEDSOURCE.includes(modelSource) ? (
renderHFSearch() renderHFSearch()
) : ( ) : (
<div style={{ lineHeight: '18px' }}> <div style={{ lineHeight: '18px' }}>
+19 -17
View File
@@ -5,6 +5,7 @@ import { Button, Col, Empty, Row, Spin } from 'antd';
import React from 'react'; import React from 'react';
import SimpleBar from 'simplebar-react'; import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css'; import 'simplebar-react/dist/simplebar.min.css';
import { modelSourceMap } from '../config';
import '../style/search-result.less'; import '../style/search-result.less';
import HFModelItem from './hf-model-item'; import HFModelItem from './hf-model-item';
@@ -19,7 +20,6 @@ interface SearchResultProps {
} }
const SearchResult: React.FC<SearchResultProps> = (props) => { const SearchResult: React.FC<SearchResultProps> = (props) => {
console.log('SearchResult======');
const { resultList, onSelect, source, networkError } = props; const { resultList, onSelect, source, networkError } = props;
const intl = useIntl(); const intl = useIntl();
@@ -49,24 +49,26 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
></IconFont> ></IconFont>
} }
description={ description={
<div className="flex-column gap-5"> source === modelSourceMap.huggingface_value ? (
<span> <div className="flex-column gap-5">
{intl.formatMessage({ id: 'models.search.networkerror' })}
</span>
<span>
<span> <span>
{intl.formatMessage({ id: 'models.search.hfvisit' })} {intl.formatMessage({ id: 'models.search.networkerror' })}
</span> </span>
<Button <span>
type="link" <span>
size="small" {intl.formatMessage({ id: 'models.search.hfvisit' })}
href="https://huggingface.co/" </span>
target="_blank" <Button
> type="link"
Hugging Face size="small"
</Button> href="https://huggingface.co/"
</span> target="_blank"
</div> >
Hugging Face
</Button>
</span>
</div>
) : null
} }
/> />
); );
+69 -23
View File
@@ -1,3 +1,4 @@
import AutoTooltip from '@/components/auto-tooltip';
import DeleteModal from '@/components/delete-modal'; import DeleteModal from '@/components/delete-modal';
import DropdownButtons from '@/components/drop-down-buttons'; import DropdownButtons from '@/components/drop-down-buttons';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
@@ -38,7 +39,7 @@ import {
queryModelInstancesList, queryModelInstancesList,
updateModel updateModel
} from '../apis'; } from '../apis';
import { modelSourceMap } from '../config'; import { getSourceRepoConfigValue, modelSourceMap } from '../config';
import { FormData, ListItem, ModelInstanceListItem } from '../config/types'; import { FormData, ListItem, ModelInstanceListItem } from '../config/types';
import DeployModal from './deploy-modal'; import DeployModal from './deploy-modal';
import InstanceItem from './instance-item'; import InstanceItem from './instance-item';
@@ -94,10 +95,7 @@ const Models: React.FC<ModelsProps> = ({
width: 600, width: 600,
source: modelSourceMap.huggingface_value source: modelSourceMap.huggingface_value
}); });
const [title, setTitle] = useState<string>(''); const [currentData, setCurrentData] = useState<ListItem>({} as ListItem);
const [currentData, setCurrentData] = useState<ListItem | undefined>(
undefined
);
const [currentInstanceUrl, setCurrentInstanceUrl] = useState<string>(''); const [currentInstanceUrl, setCurrentInstanceUrl] = useState<string>('');
const modalRef = useRef<any>(null); const modalRef = useRef<any>(null);
@@ -116,6 +114,21 @@ const Models: React.FC<ModelsProps> = ({
} }
); );
useHotkeys(
HotKeys.NEW3.join(','),
() => {
setOpenDeployModal({
show: true,
width: 'calc(100vw - 220px)',
source: modelSourceMap.modelscope_value
});
},
{
preventDefault: true,
enabled: !openAddModal && !openDeployModal.show && !openLogModal
}
);
useHotkeys( useHotkeys(
HotKeys.NEW2.join(','), HotKeys.NEW2.join(','),
() => { () => {
@@ -159,6 +172,19 @@ const Models: React.FC<ModelsProps> = ({
}; };
}); });
} }
},
{
label: 'ModelScope',
value: modelSourceMap.modelscope_value,
key: 'modelscope',
icon: <IconFont type="icon-tu2"></IconFont>,
onClick: (e: any) => {
setOpenDeployModal({
show: true,
width: 'calc(100vw - 220px)',
source: modelSourceMap.modelscope_value
});
}
} }
]; ];
@@ -222,9 +248,12 @@ const Models: React.FC<ModelsProps> = ({
const handleModalOk = useCallback( const handleModalOk = useCallback(
async (data: FormData) => { async (data: FormData) => {
try { try {
console.log('data:', data, openDeployModal);
const result = getSourceRepoConfigValue(currentData?.source, data);
await updateModel({ await updateModel({
data: { data: {
...data ...result.values,
..._.omit(data, result.omits)
}, },
id: currentData?.id as number id: currentData?.id as number
}); });
@@ -236,7 +265,6 @@ const Models: React.FC<ModelsProps> = ({
); );
const handleModalCancel = useCallback(() => { const handleModalCancel = useCallback(() => {
console.log('handleModalCancel');
setOpenAddModal(false); setOpenAddModal(false);
}, []); }, []);
@@ -252,7 +280,14 @@ const Models: React.FC<ModelsProps> = ({
try { try {
console.log('data:', data, openDeployModal); console.log('data:', data, openDeployModal);
await createModel({ data }); const result = getSourceRepoConfigValue(openDeployModal.source, data);
await createModel({
data: {
...result.values,
..._.omit(data, result.omits)
}
});
setOpenDeployModal({ setOpenDeployModal({
...openDeployModal, ...openDeployModal,
show: false show: false
@@ -330,7 +365,6 @@ const Models: React.FC<ModelsProps> = ({
const handleEdit = (row: ListItem) => { const handleEdit = (row: ListItem) => {
setCurrentData(row); setCurrentData(row);
setOpenAddModal(true); setOpenAddModal(true);
setTitle(intl.formatMessage({ id: 'models.title.edit' }));
}; };
const handleSelect = useCallback((val: any, row: ListItem) => { const handleSelect = useCallback((val: any, row: ListItem) => {
@@ -367,10 +401,11 @@ const Models: React.FC<ModelsProps> = ({
); );
const renderChildren = useCallback( const renderChildren = useCallback(
(list: any) => { (list: any, parent?: any) => {
return ( return (
<InstanceItem <InstanceItem
list={list} list={list}
modelData={parent}
gpuDeviceList={gpuDeviceList} gpuDeviceList={gpuDeviceList}
workerList={workerList} workerList={workerList}
handleChildSelect={handleChildSelect} handleChildSelect={handleChildSelect}
@@ -380,6 +415,16 @@ const Models: React.FC<ModelsProps> = ({
[workerList] [workerList]
); );
const generateSource = useCallback((record: ListItem) => {
if (record.source === modelSourceMap.modelscope_value) {
return `${modelSourceMap.modelScope}/${record.model_scope_file_path || record.model_scope_model_id}`;
}
if (record.source === modelSourceMap.huggingface_value) {
return `${modelSourceMap.huggingface}/${record.huggingface_filename || record.huggingface_repo_id}`;
}
return `${modelSourceMap.ollama_library}/${record.ollama_library_model_name}`;
}, []);
const handleCloseViewCode = useCallback(() => { const handleCloseViewCode = useCallback(() => {
setEmbeddingParams({ setEmbeddingParams({
params: {}, params: {},
@@ -472,15 +517,20 @@ const Models: React.FC<ModelsProps> = ({
span={5} span={5}
render={(text, record: ListItem) => { render={(text, record: ListItem) => {
return ( return (
<span> <span
<span className="m-r-5">{text}</span> className="flex-center flex-wrap"
<span> style={{ maxWidth: '100%' }}
{record.embedding_only && ( >
<Tag style={{ marginTop: 6 }} color="geekblue"> <AutoTooltip ghost>
<span className="m-r-5">{text}</span>
</AutoTooltip>
{record.embedding_only && (
<span>
<Tag style={{ margin: 0 }} color="geekblue">
Embedding Only Embedding Only
</Tag> </Tag>
)} </span>
</span> )}
</span> </span>
); );
}} }}
@@ -493,11 +543,7 @@ const Models: React.FC<ModelsProps> = ({
render={(text, record: ListItem) => { render={(text, record: ListItem) => {
return ( return (
<span className="flex flex-column"> <span className="flex flex-column">
<span> <span>{generateSource(record)}</span>
{record.source === modelSourceMap.huggingface_value
? `${modelSourceMap.huggingface} / ${record.huggingface_filename}`
: `${modelSourceMap.ollama_library} / ${record.ollama_library_model_name}`}
</span>
</span> </span>
); );
}} }}
@@ -551,7 +597,7 @@ const Models: React.FC<ModelsProps> = ({
<UpdateModel <UpdateModel
open={openAddModal} open={openAddModal}
action={PageAction.EDIT} action={PageAction.EDIT}
title={title} title={intl.formatMessage({ id: 'models.title.edit' })}
data={currentData} data={currentData}
onCancel={handleModalCancel} onCancel={handleModalCancel}
onOk={handleModalOk} onOk={handleModalOk}
+77 -84
View File
@@ -4,15 +4,18 @@ import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { convertFileSize } from '@/utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form, Modal } from 'antd'; import { Form, Modal } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { memo, useEffect, useState } from 'react'; import { memo, useEffect, useMemo, useState } from 'react';
import SimpleBar from 'simplebar-react'; import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css'; import 'simplebar-react/dist/simplebar.min.css';
import { queryGPUList, queryHuggingfaceModelFiles } from '../apis'; import { queryGPUList } from '../apis';
import { modelSourceMap } from '../config'; import {
backendOptionsMap,
modelSourceMap,
setSourceRepoConfigValue
} from '../config';
import { FormData, GPUListItem, ListItem } from '../config/types'; import { FormData, GPUListItem, ListItem } from '../config/types';
import AdvanceConfig from './advance-config'; import AdvanceConfig from './advance-config';
@@ -35,20 +38,27 @@ const sourceOptions = [
label: 'Ollama Library', label: 'Ollama Library',
value: modelSourceMap.ollama_library_value, value: modelSourceMap.ollama_library_value,
key: 'ollama_library' key: 'ollama_library'
},
{
label: 'ModelScope',
value: modelSourceMap.modelscope_value,
key: 'model_scope'
} }
]; ];
const SEARCH_SOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const UpdateModal: React.FC<AddModalProps> = (props) => { const UpdateModal: React.FC<AddModalProps> = (props) => {
console.log('addmodel===='); console.log('addmodel====');
const { title, action, open, onOk, onCancel } = props || {}; const { title, action, open, onOk, onCancel } = props || {};
const [form] = Form.useForm(); const [form] = Form.useForm();
const intl = useIntl(); const intl = useIntl();
const [gpuOptions, setGpuOptions] = useState<any[]>([]); const [gpuOptions, setGpuOptions] = useState<any[]>([]);
const [isGGUF, setIsGGUF] = useState<boolean>(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const modelSource = Form.useWatch('source', form);
const [fileOptions, setFileOptions] = useState<
{ label: string; value: string }[]
>([]);
const getGPUList = async () => { const getGPUList = async () => {
const data = await queryGPUList(); const data = await queryGPUList();
@@ -70,8 +80,14 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
}); });
} }
if (action === PageAction.EDIT && open) { if (action === PageAction.EDIT && open) {
const result = setSourceRepoConfigValue(
props.data?.source || '',
props.data
);
form.setFieldsValue({ form.setFieldsValue({
...props.data, ...result.values,
..._.omit(props.data, result.omits),
scheduleType: props.data?.gpu_selector ? 'manual' : 'auto', scheduleType: props.data?.gpu_selector ? 'manual' : 'auto',
gpu_selector: props.data?.gpu_selector gpu_selector: props.data?.gpu_selector
? `${props.data?.gpu_selector.worker_name}-${props.data?.gpu_selector.gpu_name}-${props.data?.gpu_selector.gpu_index}` ? `${props.data?.gpu_selector.worker_name}-${props.data?.gpu_selector.gpu_name}-${props.data?.gpu_selector.gpu_index}`
@@ -84,49 +100,15 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
initFormValue(); initFormValue();
}, [open]); }, [open]);
const fileNamLabel = (item: any) => { useEffect(() => {
return ( setIsGGUF(props.data?.backend === backendOptionsMap.llamaBox);
<span> }, [props.data?.backend]);
{item.path}
<span
style={{ color: 'var(--ant-color-text-tertiary)', marginLeft: '4px' }}
>
({convertFileSize(item.size)})
</span>
</span>
);
};
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 renderHuggingfaceFields = () => { const renderHuggingfaceFields = () => {
return ( return (
<> <>
<Form.Item<FormData> <Form.Item<FormData>
name="huggingface_repo_id" name="repo_id"
rules={[ rules={[
{ {
required: true, required: true,
@@ -145,30 +127,31 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
disabled={true} disabled={true}
></SealInput.Input> ></SealInput.Input>
</Form.Item> </Form.Item>
<Form.Item<FormData> {isGGUF && (
name="huggingface_filename" <Form.Item<FormData>
rules={[ name="file_name"
{ rules={[
required: true, {
message: intl.formatMessage( required: true,
{ message: intl.formatMessage(
id: 'common.form.rule.input' {
}, id: 'common.form.rule.input'
{ name: intl.formatMessage({ id: 'models.form.filename' }) } },
) { name: intl.formatMessage({ id: 'models.form.filename' }) }
} )
]} }
> ]}
<SealAutoComplete >
filterOption <SealAutoComplete
label={intl.formatMessage({ id: 'models.form.filename' })} filterOption
required label={intl.formatMessage({ id: 'models.form.filename' })}
options={fileOptions} required
loading={loading} options={[]}
onFocus={handleRepoOnBlur} loading={loading}
disabled={action === PageAction.EDIT} disabled={action === PageAction.EDIT}
></SealAutoComplete> ></SealAutoComplete>
</Form.Item> </Form.Item>
)}
</> </>
); );
}; };
@@ -229,18 +212,21 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
); );
}; };
const renderFieldsBySource = () => { const renderFieldsBySource = useMemo(() => {
switch (modelSource) { if (SEARCH_SOURCE.includes(props.data?.source || '')) {
case modelSourceMap.huggingface_value: return renderHuggingfaceFields();
return renderHuggingfaceFields();
case modelSourceMap.ollama_library_value:
return renderOllamaModelFields();
case modelSourceMap.s3_value:
return renderS3Fields();
default:
return null;
} }
};
if (props.data?.source === modelSourceMap.ollama_library_value) {
return renderOllamaModelFields();
}
if (props.data?.source === modelSourceMap.s3_value) {
return renderS3Fields();
}
return null;
}, [props.data?.source, isGGUF, intl]);
const handleSumit = () => { const handleSumit = () => {
form.submit(); form.submit();
@@ -367,7 +353,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
></SealSelect> ></SealSelect>
)} )}
</Form.Item> </Form.Item>
{renderFieldsBySource()} {renderFieldsBySource}
<Form.Item<FormData> <Form.Item<FormData>
name="replicas" name="replicas"
rules={[ rules={[
@@ -401,7 +387,14 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
></SealInput.TextArea> ></SealInput.TextArea>
</Form.Item> </Form.Item>
<AdvanceConfig form={form} gpuOptions={gpuOptions}></AdvanceConfig> <AdvanceConfig
form={form}
gpuOptions={gpuOptions}
action={PageAction.EDIT}
isGGUF={
form.getFieldValue('backend') === backendOptionsMap.llamaBox
}
></AdvanceConfig>
</Form> </Form>
</SimpleBar> </SimpleBar>
</Modal> </Modal>
+110 -26
View File
@@ -3,24 +3,24 @@ import { EditOutlined } from '@ant-design/icons';
export const ollamaModelOptions = [ export const ollamaModelOptions = [
{ {
label: 'llama3.1', label: 'llama3.2',
value: 'llama3.1', value: 'llama3.2',
name: 'llama3.1', name: 'llama3.2',
id: 'llama3.1', id: 'llama3.2',
tags: ['8B', '70B', '405B'] tags: ['Tools', '1B', '3B']
}, },
{ {
label: 'llama3', label: 'qwen2.5',
value: 'llama3', value: 'qwen2.5',
name: 'llama3', name: 'qwen2.5',
tags: ['8B', '70B'], tags: ['Tools', '0.5B', '1.5B', '3B', '7B', '14B', '32B', '72B'],
id: 'llama3' id: 'gemma2'
}, },
{ {
label: 'gemma2', label: 'gemma2',
value: 'gemma2', value: 'gemma2',
name: 'gemma2', name: 'gemma2',
tags: ['9B', '27B'], tags: ['2B', '9B', '27B'],
id: 'gemma2' id: 'gemma2'
}, },
{ {
@@ -44,20 +44,6 @@ export const ollamaModelOptions = [
tags: ['7B'], tags: ['7B'],
id: 'mistral' id: 'mistral'
}, },
// {
// label: 'llava',
// value: 'llava',
// name: 'llava',
// tags: ['7B', '13B', '34B'],
// id: 'llava'
// },
{
label: 'qwen2',
value: 'qwen2',
name: 'qwen2',
tags: ['0.5B', '1.5B', '7B', '72B'],
id: 'qwen2'
},
{ {
label: 'phi3.5', label: 'phi3.5',
value: 'phi3.5', value: 'phi3.5',
@@ -81,13 +67,27 @@ export const ollamaModelOptions = [
} }
]; ];
export const backendOptionsMap = {
llamaBox: 'llama-box',
vllm: 'vllm'
};
export const modelSourceMap: Record<string, string> = { export const modelSourceMap: Record<string, string> = {
huggingface: 'Hugging Face', huggingface: 'Hugging Face',
ollama_library: 'Ollama Library', ollama_library: 'Ollama Library',
s3: 'S3', s3: 'S3',
huggingface_value: 'huggingface', huggingface_value: 'huggingface',
ollama_library_value: 'ollama_library', ollama_library_value: 'ollama_library',
s3_value: 's3' s3_value: 's3',
modelScope: 'ModelScope',
modelscope_value: 'model_scope'
};
export const modelSourceValueMap = {
[modelSourceMap.huggingface_value]: modelSourceMap.huggingface,
[modelSourceMap.ollama_library_value]: modelSourceMap.ollama_library,
[modelSourceMap.s3_value]: modelSourceMap.s3,
[modelSourceMap.modelscope_value]: modelSourceMap.modelScope
}; };
export const InstanceStatusMap = { export const InstanceStatusMap = {
@@ -148,6 +148,13 @@ export const ModelSortType = {
lastModified: 'lastModified' lastModified: 'lastModified'
}; };
export const ModelScopeSortType = {
[ModelSortType.trendingScore]: 'Default',
[ModelSortType.likes]: 'StarsCount',
[ModelSortType.downloads]: 'DownloadsCount',
[ModelSortType.lastModified]: 'GmtModified'
};
export const placementStrategyOptions = [ export const placementStrategyOptions = [
{ {
label: 'Spread', label: 'Spread',
@@ -158,3 +165,80 @@ export const placementStrategyOptions = [
value: 'binpack' value: 'binpack'
} }
]; ];
export const sourceRepoConfig = {
[modelSourceMap.huggingface_value]: {
repo_id: 'huggingface_repo_id',
file_name: 'huggingface_filename'
},
[modelSourceMap.modelscope_value]: {
repo_id: 'model_scope_model_id',
file_name: 'model_scope_file_path'
}
};
export const getSourceRepoConfigValue = (
source: string,
data: any
): {
values: Record<string, any>;
omits: string[];
} => {
const config: Record<string, any> = sourceRepoConfig[source] || {};
const result: Record<string, any> = {};
const omits: string[] = [];
Object.keys(config)?.forEach((key: string) => {
if (config[key]) {
result[config[key]] = data[key];
omits.push(key);
}
});
return {
values: result,
omits: omits
};
};
export const setSourceRepoConfigValue = (
source: string,
data: any
): {
values: Record<string, any>;
omits: string[];
} => {
const config: Record<string, any> = sourceRepoConfig[source] || {};
const result: Record<string, any> = {};
const omits: string[] = [];
Object.keys(config)?.forEach((key: string) => {
if (config[key]) {
result[key] = data[config[key]];
omits.push(config[key]);
}
});
return {
values: result,
omits: omits
};
};
export const getbackendParameters = (data: any) => {
const backendParameters = data.backend_parameters || {};
const result: string[] = [];
Object.keys(backendParameters)?.forEach((key: string) => {
result.push(`${key}=${backendParameters[key]}`);
});
return result;
};
export const setbackendParameters = (data: any) => {
const result: Record<string, string> = {};
const backendParameters = data.backend_parameters || [];
backendParameters.forEach((item: string) => {
const [key, value] = item.split('=');
result[key] = value;
});
return result;
};
+42
View File
@@ -0,0 +1,42 @@
const options = [
{
label: '--chat-template',
value: '--chat-template'
},
{
label: '--ctx-size',
value: '--ctx-size',
options: []
},
{
label: '--flash-attn',
value: '--flash-attn'
},
{
label: '--parallel',
value: '--parallel'
},
{
label: '--batch-size',
value: '--batch-size'
},
{
label: '--ubatch-size',
value: '--ubatch-size'
}
];
const resultList = options.map((option) => {
return {
label: option.label,
value: option.value,
opts: option.options?.map((opt) => {
return {
label: opt,
value: opt
};
})
};
});
export default resultList;
+12 -3
View File
@@ -1,9 +1,12 @@
export interface ListItem { export interface ListItem {
source: string; source: string;
backend: string;
huggingface_repo_id: string; huggingface_repo_id: string;
huggingface_file_name: string; huggingface_file_name: string;
huggingface_filename: string; huggingface_filename: string;
ollama_library_model_name: string; ollama_library_model_name: string;
model_scope_file_path: string;
model_scope_model_id: string;
embedding_only?: boolean; embedding_only?: boolean;
ready_replicas: number; ready_replicas: number;
replicas: number; replicas: number;
@@ -22,12 +25,18 @@ export interface ListItem {
} }
export interface FormData { export interface FormData {
backend?: string;
backend_parameters?: string[];
source: string; source: string;
repo_id: string;
file_name: string;
huggingface_repo_id: string; huggingface_repo_id: string;
huggingface_filename: string; huggingface_filename: string;
s3_address: string; s3_address: string;
ollama_library_model_name: 'string'; ollama_library_model_name: string;
distributed_inference_across_workers?: boolean; distributed_inference_across_workers?: boolean;
model_scope_model_id?: string;
model_scope_file_path?: string;
gpu_selector?: { gpu_selector?: {
worker_name: string; worker_name: string;
gpu_index: number; gpu_index: number;
@@ -77,8 +86,8 @@ export interface ModelInstanceFormData {
model_id: number; model_id: number;
model_name: string; model_name: string;
source: string; source: string;
huggingface_repo_id: 'string'; huggingface_repo_id: string;
huggingface_filename: 'string'; huggingface_filename: string;
} }
export interface GPUListItem { export interface GPUListItem {
+593
View File
@@ -0,0 +1,593 @@
const options = [
{
label: '--uvicorn-log-level',
value: '--uvicorn-log-level',
options: ['debug', 'info', 'warning', 'error', 'critical', 'trace']
},
{
label: '--allow-credentials',
value: '--allow-credentials',
options: []
},
{
label: '--allowed-origins',
value: '--allowed-origins',
options: []
},
{
label: '--allowed-methods',
value: '--allowed-methods',
options: []
},
{
label: '--allowed-headers',
value: '--allowed-headers',
options: []
},
{
label: '--api-key',
value: '--api-key',
options: []
},
{
label: '--lora-modules',
value: '--lora-modules',
options: []
},
{
label: '--prompt-adapters',
value: '--prompt-adapters',
options: []
},
{
label: '--chat-template',
value: '--chat-template',
options: []
},
{
label: '--response-role',
value: '--response-role',
options: []
},
{
label: '--ssl-keyfile',
value: '--ssl-keyfile',
options: []
},
{
label: '--ssl-certfile',
value: '--ssl-certfile',
options: []
},
{
label: '--ssl-ca-certs',
value: '--ssl-ca-certs',
options: []
},
{
label: '--ssl-cert-reqs',
value: '--ssl-cert-reqs',
options: []
},
{
label: '--root-path',
value: '--root-path',
options: []
},
{
label: '--middleware',
value: '--middleware',
options: []
},
{
label: '--return-tokens-as-token-ids',
value: '--return-tokens-as-token-ids',
options: []
},
{
label: '--disable-frontend-multiprocessing',
value: '--disable-frontend-multiprocessing',
options: []
},
{
label: '--enable-auto-tool-choice',
value: '--enable-auto-tool-choice',
options: []
},
{
label: '--tool-call-parser',
value: '--tool-call-parser',
options: ['mistral', 'hermes']
},
{
label: '--model',
value: '--model',
options: []
},
{
label: '--tokenizer',
value: '--tokenizer',
options: []
},
{
label: '--skip-tokenizer-init',
value: '--skip-tokenizer-init',
options: []
},
{
label: '--revision',
value: '--revision',
options: []
},
{
label: '--code-revision',
value: '--code-revision',
options: []
},
{
label: '--tokenizer-revision',
value: '--tokenizer-revision',
options: []
},
{
label: '--tokenizer-mode',
value: '--tokenizer-mode',
options: ['auto', 'slow', 'mistral']
},
{
label: '--trust-remote-code',
value: '--trust-remote-code',
options: []
},
{
label: '--download-dir',
value: '--download-dir',
options: []
},
{
label: '--load-format',
value: '--load-format',
options: [
'auto',
'pt',
'safetensors',
'npcache',
'dummy',
'tensorizer',
'sharded_state',
'gguf',
'bitsandbytes',
'mistral'
]
},
{
label: '--config-format',
value: '--config-format',
options: ['auto', 'hf', 'mistral']
},
{
label: '--dtype',
value: '--dtype',
options: ['auto', 'half', 'float16', 'bfloat16', 'float', 'float32']
},
{
label: '--kv-cache-dtype',
value: '--kv-cache-dtype',
options: ['auto', 'fp8', 'fp8_e5m2', 'fp8_e4m3']
},
{
label: '--quantization-param-path',
value: '--quantization-param-path',
options: []
},
{
label: '--max-model-len',
value: '--max-model-len',
options: []
},
{
label: '--guided-decoding-backend',
value: '--guided-decoding-backend',
options: ['outlines', 'lm-format-enforcer']
},
{
label: '--distributed-executor-backend',
value: '--distributed-executor-backend',
options: ['ray', 'mp']
},
{
label: '--worker-use-ray',
value: '--worker-use-ray',
options: []
},
{
label: '--pipeline-parallel-size',
value: '--pipeline-parallel-size',
options: []
},
{
label: '--tensor-parallel-size',
value: '--tensor-parallel-size',
options: []
},
{
label: '--max-parallel-loading-workers',
value: '--max-parallel-loading-workers',
options: []
},
{
label: '--ray-workers-use-nsight',
value: '--ray-workers-use-nsight',
options: []
},
{
label: '--block-size',
value: '--block-size',
options: ['8', '16', '32']
},
{
label: '--enable-prefix-caching',
value: '--enable-prefix-caching',
options: []
},
{
label: '--disable-sliding-window',
value: '--disable-sliding-window',
options: []
},
{
label: '--use-v2-block-manager',
value: '--use-v2-block-manager',
options: []
},
{
label: '--num-lookahead-slots',
value: '--num-lookahead-slots',
options: []
},
{
label: '--seed',
value: '--seed',
options: []
},
{
label: '--swap-space',
value: '--swap-space',
options: []
},
{
label: '--cpu-offload-gb',
value: '--cpu-offload-gb',
options: []
},
{
label: '--gpu-memory-utilization',
value: '--gpu-memory-utilization',
options: []
},
{
label: '--num-gpu-blocks-override',
value: '--num-gpu-blocks-override',
options: []
},
{
label: '--max-num-batched-tokens',
value: '--max-num-batched-tokens',
options: []
},
{
label: '--max-num-seqs',
value: '--max-num-seqs',
options: []
},
{
label: '--max-logprobs',
value: '--max-logprobs',
options: []
},
{
label: '--disable-log-stats',
value: '--disable-log-stats',
options: []
},
{
label: '--quantization',
value: '--quantization',
options: [
'aqlm',
'awq',
'deepspeedfp',
'tpu_int8',
'fp8',
'fbgemm_fp8',
'modelopt',
'marlin',
'gguf',
'gptq_marlin_24',
'gptq_marlin',
'awq_marlin',
'gptq',
'compressed-tensors',
'bitsandbytes',
'qqq',
'experts_int8',
'neuron_quant',
'None'
]
},
{
label: '--rope-scaling',
value: '--rope-scaling',
options: []
},
{
label: '--rope-theta',
value: '--rope-theta',
options: []
},
{
label: '--enforce-eager',
value: '--enforce-eager',
options: []
},
{
label: '--max-context-len-to-capture',
value: '--max-context-len-to-capture',
options: []
},
{
label: '--max-seq-len-to-capture',
value: '--max-seq-len-to-capture',
options: []
},
{
label: '--disable-custom-all-reduce',
value: '--disable-custom-all-reduce',
options: []
},
{
label: '--tokenizer-pool-size',
value: '--tokenizer-pool-size',
options: []
},
{
label: '--tokenizer-pool-type',
value: '--tokenizer-pool-type',
options: []
},
{
label: '--tokenizer-pool-extra-config',
value: '--tokenizer-pool-extra-config',
options: []
},
{
label: '--limit-mm-per-prompt',
value: '--limit-mm-per-prompt',
options: []
},
{
label: '--enable-lora',
value: '--enable-lora',
options: []
},
{
label: '--max-loras',
value: '--max-loras',
options: []
},
{
label: '--max-lora-rank',
value: '--max-lora-rank',
options: []
},
{
label: '--lora-extra-vocab-size',
value: '--lora-extra-vocab-size',
options: []
},
{
label: '--lora-dtype',
value: '--lora-dtype',
options: ['auto', 'float16', 'bfloat16', 'float32']
},
{
label: '--long-lora-scaling-factors',
value: '--long-lora-scaling-factors',
options: []
},
{
label: '--max-cpu-loras',
value: '--max-cpu-loras',
options: []
},
{
label: '--fully-sharded-loras',
value: '--fully-sharded-loras',
options: []
},
{
label: '--enable-prompt-adapter',
value: '--enable-prompt-adapter',
options: []
},
{
label: '--max-prompt-adapters',
value: '--max-prompt-adapters',
options: []
},
{
label: '--max-prompt-adapter-token',
value: '--max-prompt-adapter-token',
options: []
},
{
label: '--device',
value: '--device',
options: ['auto', 'cuda', 'neuron', 'cpu', 'openvino', 'tpu', 'xpu']
},
{
label: '--num-scheduler-steps',
value: '--num-scheduler-steps',
options: []
},
{
label: '--scheduler-delay-factor',
value: '--scheduler-delay-factor',
options: []
},
{
label: '--enable-chunked-prefill',
value: '--enable-chunked-prefill',
options: []
},
{
label: '--speculative-model',
value: '--speculative-model',
options: []
},
{
label: '--speculative-model-quantization',
value: '--speculative-model-quantization',
options: [
'aqlm',
'awq',
'deepspeedfp',
'tpu_int8',
'fp8',
'fbgemm_fp8',
'modelopt',
'marlin',
'gguf',
'gptq_marlin_24',
'gptq_marlin',
'awq_marlin',
'gptq',
'compressed-tensors',
'bitsandbytes',
'qqq',
'experts_int8',
'neuron_quant',
'None'
]
},
{
label: '--num-speculative-tokens',
value: '--num-speculative-tokens',
options: []
},
{
label: '--speculative-draft-tensor-parallel-size',
value: '--speculative-draft-tensor-parallel-size',
options: []
},
{
label: '--speculative-max-model-len',
value: '--speculative-max-model-len',
options: []
},
{
label: '--speculative-disable-by-batch-size',
value: '--speculative-disable-by-batch-size',
options: []
},
{
label: '--ngram-prompt-lookup-max',
value: '--ngram-prompt-lookup-max',
options: []
},
{
label: '--ngram-prompt-lookup-min',
value: '--ngram-prompt-lookup-min',
options: []
},
{
label: '--spec-decoding-acceptance-method',
value: '--spec-decoding-acceptance-method',
options: ['rejection_sampler', 'typical_acceptance_sampler']
},
{
label: '--typical-acceptance-sampler-posterior-threshold',
value: '--typical-acceptance-sampler-posterior-threshold',
options: []
},
{
label: '--typical-acceptance-sampler-posterior-alpha',
value: '--typical-acceptance-sampler-posterior-alpha',
options: []
},
{
label: '--disable-logprobs-during-spec-decoding',
value: '--disable-logprobs-during-spec-decoding',
options: []
},
{
label: '--model-loader-extra-config',
value: '--model-loader-extra-config',
options: []
},
{
label: '--ignore-patterns',
value: '--ignore-patterns',
options: []
},
{
label: '--preemption-mode',
value: '--preemption-mode',
options: []
},
{
label: '--served-model-name',
value: '--served-model-name',
options: []
},
{
label: '--qlora-adapter-name-or-path',
value: '--qlora-adapter-name-or-path',
options: []
},
{
label: '--otlp-traces-endpoint',
value: '--otlp-traces-endpoint',
options: []
},
{
label: '--collect-detailed-traces',
value: '--collect-detailed-traces',
options: []
},
{
label: '--disable-async-output-proc',
value: '--disable-async-output-proc',
options: []
},
{
label: '--override-neuron-config',
value: '--override-neuron-config',
options: []
},
{
label: '--disable-log-requests',
value: '--disable-log-requests',
options: []
},
{
label: '--max-log-len',
value: '--max-log-len',
options: []
}
];
const resultList = options.map((option) => {
return {
label: option.label,
value: option.value,
opts: option.options.map((opt) => {
return {
label: opt,
value: opt
};
})
};
});
export default resultList;
+1 -1
View File
@@ -5,7 +5,7 @@
} }
.simplebar-scrollbar::before { .simplebar-scrollbar::before {
background: rgba(0, 0, 0, 50%); width: var(--scrollbar-size);
} }
.column-wrapper-footer { .column-wrapper-footer {
@@ -37,8 +37,6 @@
font-size: 12px; font-size: 12px;
height: 22px; height: 22px;
opacity: 0.7; opacity: 0.7;
// border: 1px solid var(--ant-color-border);
// color: var(--ant-color-text-secondary);
} }
.btn { .btn {
@@ -8,10 +8,6 @@
// height: calc(100vh - 194px); // height: calc(100vh - 194px);
} }
.simplebar-scrollbar::before {
background: rgba(0, 0, 0, 50%);
}
.search-bar { .search-bar {
left: 0; left: 0;
right: 0; right: 0;
+4 -3
View File
@@ -5,14 +5,15 @@
position: absolute; position: absolute;
border-radius: 0 2px 0 0; border-radius: 0 2px 0 0;
top: 10px; top: 10px;
left: -10px; left: -13px;
width: 22px; width: 24px;
height: 22px; height: 24px;
border: 2px solid var(--ant-color-split); border: 2px solid var(--ant-color-split);
transform: rotate(45deg); transform: rotate(45deg);
border-left: none; border-left: none;
border-bottom: none; border-bottom: none;
background-color: var(--color-white-1); background-color: var(--color-white-1);
z-index: 100; z-index: 100;
clip-path: polygon(0 -1px, 24px 0, 24px 23px);
} }
} }
@@ -11,5 +11,10 @@
padding: @padding; padding: @padding;
padding-top: 10px; padding-top: 10px;
margin-bottom: 0; margin-bottom: 0;
font-weight: var(--font-weight-bold);
background-color: var(--color-white-1); background-color: var(--color-white-1);
.title {
font-weight: var(--font-weight-bold);
}
} }
+1 -1
View File
@@ -1,6 +1,6 @@
:local(.login-form-wrapper) { :local(.login-form-wrapper) {
margin: 0 auto; margin: 0 auto;
border-radius: 8px; border-radius: var(--border-radius-modal);
width: max-content; width: max-content;
height: max-content; height: max-content;
padding: 32px; padding: 32px;

Some files were not shown because too many files have changed in this diff Show More