Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c041d768b2 | ||
|
|
5df6154abe | ||
|
|
1fed02d23f | ||
|
|
2d34d07da4 | ||
|
|
ea25291d75 | ||
|
|
5f77a0082c | ||
|
|
3978ec683d | ||
|
|
458f467985 | ||
|
|
69ffdd4604 | ||
|
|
5c2503b2a8 | ||
|
|
bc921ff835 | ||
|
|
197488bff6 | ||
|
|
e48d21c8cb | ||
|
|
7e992fad99 | ||
|
|
49c0467428 | ||
|
|
7e985991cd | ||
|
|
cc7d2ded6c | ||
|
|
f813f56c1f | ||
|
|
a698c65a07 | ||
|
|
3cd96ba2e8 | ||
|
|
0652f850d6 | ||
|
|
3ab29f9e01 | ||
|
|
b041cdc766 | ||
|
|
c04b9342b8 | ||
|
|
1abaa164f5 | ||
|
|
4f4f00f0c8 | ||
|
|
2e5f0d9210 | ||
|
|
af077c4706 | ||
|
|
a5cebb88ef | ||
|
|
6baf8f08c8 |
@@ -1,3 +1,7 @@
|
||||
# GPUStack UI
|
||||
|
||||
UI for [GPUStack](https://github.com/gpustack/gpustack).
|
||||
|
||||
## Installation
|
||||
|
||||
1. [Nodejs](https://nodejs.org/en) 16.0+(with NPM)
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ const isProduction = env === 'production';
|
||||
const t = Date.now();
|
||||
export default defineConfig({
|
||||
proxy: {
|
||||
...proxy('http://192.168.50.166:8080')
|
||||
...proxy()
|
||||
},
|
||||
history: {
|
||||
type: 'hash'
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const proxyTableList = ['cli', 'v1', 'auth', 'v1-openai', 'version'];
|
||||
const proxyTableList = ['cli', 'v1', 'auth', 'v1-openai', 'version', 'proxy'];
|
||||
|
||||
// @ts-ingore
|
||||
export default function createProxyTable(target?: string) {
|
||||
|
||||
@@ -31,13 +31,6 @@ export default [
|
||||
access: 'canSeeAdmin',
|
||||
component: './resources'
|
||||
},
|
||||
// {
|
||||
// name: 'usage',
|
||||
// path: '/usage',
|
||||
// key: 'usage',
|
||||
// icon: 'BarChartOutlined',
|
||||
// component: './usage'
|
||||
// },
|
||||
{
|
||||
name: 'apikeys',
|
||||
path: '/api-keys',
|
||||
|
||||
+7
-1
@@ -28,6 +28,10 @@ export default {
|
||||
Message: {
|
||||
contentPadding: '12px 16px'
|
||||
},
|
||||
Tooltip: {
|
||||
colorBgSpotlight: 'rgba(110,110,110,1)'
|
||||
// sizePopupArrow: 0
|
||||
},
|
||||
Slider: {
|
||||
handleSize: 8,
|
||||
handleSizeHover: 8,
|
||||
@@ -42,7 +46,9 @@ export default {
|
||||
},
|
||||
token: {
|
||||
colorPrimary: '#007BFF',
|
||||
borderRadius: 8,
|
||||
colorSuccess: '#54cc98',
|
||||
borderRadius: 4,
|
||||
borderRadiusSM: 2,
|
||||
fontSize: 12,
|
||||
motion: true
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"author": "jialin <jialinkuang@126.com>",
|
||||
"author": "jialin",
|
||||
"scripts": {
|
||||
"build": "max build",
|
||||
"dev": "max dev",
|
||||
@@ -38,6 +38,8 @@
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^14.1.0",
|
||||
"numeral": "^2.0.6",
|
||||
"overlayscrollbars": "^2.10.0",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"query-string": "^9.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
Generated
+20
@@ -86,6 +86,12 @@ dependencies:
|
||||
numeral:
|
||||
specifier: ^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:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
@@ -12606,6 +12612,20 @@ packages:
|
||||
mem: 5.1.1
|
||||
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:
|
||||
resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==, tarball: https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.m-l-40 {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.m-r-10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
@@ -66,6 +70,10 @@
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.p-l-8 {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -85,6 +93,10 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flex-gap-2 {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
@@ -98,6 +110,11 @@
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tag, Tooltip } from 'antd';
|
||||
import { Tag, Tooltip, type TagProps } from 'antd';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, {
|
||||
useCallback,
|
||||
@@ -8,20 +8,26 @@ import React, {
|
||||
useState
|
||||
} from 'react';
|
||||
|
||||
interface AutoTooltipProps extends React.ComponentProps<typeof Tag> {
|
||||
// type TagProps = React.ComponentProps<typeof Tag>;
|
||||
|
||||
interface AutoTooltipProps extends Omit<TagProps, 'title'> {
|
||||
children: React.ReactNode;
|
||||
maxWidth?: number | string;
|
||||
color?: string;
|
||||
style?: React.CSSProperties;
|
||||
ghost?: boolean;
|
||||
title?: React.ReactNode;
|
||||
showTitle?: boolean;
|
||||
tooltipProps?: React.ComponentProps<typeof Tooltip>;
|
||||
}
|
||||
|
||||
const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
||||
children,
|
||||
maxWidth = '100%',
|
||||
ghost = false,
|
||||
title,
|
||||
showTitle = false,
|
||||
tooltipProps,
|
||||
...tagProps
|
||||
}) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -32,7 +38,7 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
||||
const { scrollWidth, clientWidth } = contentRef.current;
|
||||
setIsOverflowing(scrollWidth > clientWidth);
|
||||
}
|
||||
}, []);
|
||||
}, [contentRef.current]);
|
||||
|
||||
const debouncedCheckOverflow = useMemo(
|
||||
() => debounce(checkOverflow, 200),
|
||||
@@ -64,7 +70,10 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip title={isOverflowing || showTitle ? children : ''}>
|
||||
<Tooltip
|
||||
title={isOverflowing || showTitle ? title || children : ''}
|
||||
{...tooltipProps}
|
||||
>
|
||||
{ghost ? (
|
||||
<div ref={contentRef} style={tagStyle}>
|
||||
{children}
|
||||
|
||||
@@ -18,7 +18,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
text,
|
||||
disabled,
|
||||
type = 'text',
|
||||
shape = 'circle',
|
||||
shape = 'default',
|
||||
fontSize = '14px',
|
||||
style,
|
||||
size = 'middle'
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
.editor-wrap {
|
||||
border-radius: var(--border-radius-mini);
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
|
||||
.code-pre {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
padding: 6px 10px;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: rgb(56, 56, 56);
|
||||
@@ -12,6 +18,7 @@
|
||||
.ant-select-selector {
|
||||
background-color: rgba(255, 255, 255, 9%) !important;
|
||||
color: rgba(255, 255, 255, 70%);
|
||||
border-radius: var(--border-radius-2px);
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: rgba(255, 255, 255, 70%);
|
||||
|
||||
@@ -10,6 +10,11 @@ interface EditorwrapProps {
|
||||
copyText: string;
|
||||
defaultValue?: string;
|
||||
langOptions?: { label: string; value: string }[];
|
||||
styles?: {
|
||||
wrapper?: React.CSSProperties;
|
||||
header?: React.CSSProperties;
|
||||
content?: React.CSSProperties;
|
||||
};
|
||||
onChangeLang?: (value: string) => void;
|
||||
}
|
||||
const EditorWrap: React.FC<EditorwrapProps> = ({
|
||||
@@ -19,6 +24,7 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
|
||||
langOptions,
|
||||
onChangeLang,
|
||||
defaultValue,
|
||||
styles = {},
|
||||
showHeader = true
|
||||
}) => {
|
||||
const handleChangeLang = (value: string) => {
|
||||
@@ -52,7 +58,7 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
|
||||
return null;
|
||||
};
|
||||
return (
|
||||
<div className="editor-wrap">
|
||||
<div className="editor-wrap" style={{ ...styles.wrapper }}>
|
||||
{renderHeader()}
|
||||
<div className="editor-content">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
ignoreIllegals?: boolean;
|
||||
copyable?: boolean;
|
||||
height?: string | number;
|
||||
}
|
||||
const DarkViewer: React.FC<CodeViewerProps> = (props) => {
|
||||
const { code, lang, autodetect, ignoreIllegals, copyable } = props || {};
|
||||
const {
|
||||
code,
|
||||
lang,
|
||||
autodetect,
|
||||
ignoreIllegals,
|
||||
copyable,
|
||||
height = 'auto'
|
||||
} = props || {};
|
||||
|
||||
return (
|
||||
<CodeViewer
|
||||
height={height}
|
||||
code={code}
|
||||
lang={lang}
|
||||
theme="dark"
|
||||
|
||||
@@ -8,12 +8,21 @@ interface CodeViewerProps {
|
||||
autodetect?: boolean;
|
||||
ignoreIllegals?: boolean;
|
||||
copyable?: boolean;
|
||||
height?: string | number;
|
||||
}
|
||||
const LightViewer: React.FC<CodeViewerProps> = (props) => {
|
||||
const { code, lang, autodetect, ignoreIllegals, copyable } = props || {};
|
||||
const {
|
||||
code,
|
||||
lang,
|
||||
autodetect,
|
||||
ignoreIllegals,
|
||||
copyable,
|
||||
height = 'auto'
|
||||
} = props || {};
|
||||
|
||||
return (
|
||||
<CodeViewer
|
||||
height={height}
|
||||
code={code}
|
||||
lang={lang}
|
||||
theme="light"
|
||||
|
||||
@@ -10,6 +10,7 @@ interface CodeViewerProps {
|
||||
autodetect?: boolean;
|
||||
ignoreIllegals?: boolean;
|
||||
copyable?: boolean;
|
||||
height?: string | number;
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
||||
@@ -18,7 +19,8 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
||||
lang,
|
||||
autodetect = true,
|
||||
ignoreIllegals = true,
|
||||
copyable = true
|
||||
copyable = true,
|
||||
height = 'auto'
|
||||
} = props || {};
|
||||
|
||||
const highlightedCode = useMemo(() => {
|
||||
@@ -58,11 +60,17 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
|
||||
|
||||
return (
|
||||
<pre
|
||||
className={classNames('code-pre', {
|
||||
dark: props.theme === 'dark',
|
||||
light: props.theme === 'light',
|
||||
copyable: copyable
|
||||
})}
|
||||
className={classNames(
|
||||
'code-pre custome-scrollbar custom-scrollbar-horizontal ',
|
||||
{
|
||||
dark: props.theme === 'dark',
|
||||
light: props.theme === 'light',
|
||||
copyable: copyable
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
height: height
|
||||
}}
|
||||
>
|
||||
<code
|
||||
className={highlightedCode.className}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import CodeViewerDark from './code-viewer-dark';
|
||||
import CodeViewerLight from './code-viewer-light';
|
||||
import './styles/index.less';
|
||||
@@ -7,18 +8,35 @@ const HighlightCode: React.FC<{
|
||||
lang?: string;
|
||||
copyable?: boolean;
|
||||
theme?: 'light' | 'dark';
|
||||
height?: string | number;
|
||||
}> = (props) => {
|
||||
const { code, lang = 'bash', copyable = true, theme = 'dark' } = props;
|
||||
const {
|
||||
code,
|
||||
lang = 'bash',
|
||||
copyable = true,
|
||||
theme = 'dark',
|
||||
height = 'auto'
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="high-light-wrapper">
|
||||
<div className="high-light-wrapper hj-wrapper">
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
export default HighlightCode;
|
||||
export default React.memo(HighlightCode);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.high-light-wrapper {
|
||||
text-align: left;
|
||||
font-size: var(--font-size-base);
|
||||
|
||||
.hljs {
|
||||
font-weight: var(--font-weight-normal);
|
||||
@@ -7,21 +8,24 @@
|
||||
padding-block: 1.2em;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
height: var(--scrollbar-size);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(211, 211, 220, 50%);
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(190, 190, 190, 100%);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-pre {
|
||||
@@ -29,6 +33,10 @@
|
||||
position: relative;
|
||||
border-radius: var(--border-radius-mini);
|
||||
|
||||
code {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&.copyable {
|
||||
padding-inline: 12px 32px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
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;
|
||||
|
||||
@@ -52,7 +52,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
||||
// has duplicate key
|
||||
const duplicates = _.filter(
|
||||
labelList,
|
||||
(item: Global.BaseListItem) => val && val === item.key
|
||||
(item: Global.BaseListItem<string>) => val && val === item.key
|
||||
);
|
||||
if (duplicates.length > 1) {
|
||||
setOpen(true);
|
||||
@@ -77,7 +77,8 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
||||
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
|
||||
>
|
||||
<SealInput.Input
|
||||
label="Key"
|
||||
checkStatus="success"
|
||||
label={intl.formatMessage({ id: 'common.input.key' })}
|
||||
value={label.key}
|
||||
onChange={handleOnKeyChange}
|
||||
onBlur={handleKeyOnBlur}
|
||||
@@ -89,7 +90,8 @@ const LabelItem: React.FC<LabelItemProps> = ({
|
||||
<div className="label-value">
|
||||
{valueAddon ?? (
|
||||
<SealInput.Input
|
||||
label="Value"
|
||||
checkStatus={label.value ? 'success' : ''}
|
||||
label={intl.formatMessage({ id: 'common.input.value' })}
|
||||
value={label.value}
|
||||
onChange={handleOnValueChange}
|
||||
></SealInput.Input>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
:global {
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,14 @@
|
||||
}
|
||||
|
||||
.xterm {
|
||||
height: 100% !important;
|
||||
// height: 100% !important;
|
||||
|
||||
.xterm-viewport {
|
||||
overflow-y: auto !important;
|
||||
// custom scrollbar
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 6px;
|
||||
width: var(--scrollbar-size);
|
||||
height: var(--scrollbar-size);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
|
||||
@@ -26,7 +26,7 @@ const LogsViewer: React.FC<LogsViewerProps> = (props) => {
|
||||
const [logs, setLogs] = useState('');
|
||||
const size = useSize(scroller);
|
||||
|
||||
const throttleScroll = _.debounce(() => {
|
||||
const throttleScroll = _.throttle(() => {
|
||||
termRef.current?.scrollToBottom?.();
|
||||
}, 100);
|
||||
|
||||
@@ -36,6 +36,7 @@ const LogsViewer: React.FC<LogsViewerProps> = (props) => {
|
||||
termRef.current?.clear?.();
|
||||
cacheDataRef.current = data;
|
||||
termRef.current?.write?.(data);
|
||||
setLogs(data);
|
||||
}, 100),
|
||||
[]
|
||||
);
|
||||
@@ -78,11 +79,11 @@ const LogsViewer: React.FC<LogsViewerProps> = (props) => {
|
||||
termRef.current.open(termwrapRef.current);
|
||||
|
||||
// add event
|
||||
termRef.current.onLineFeed((e: any) => {
|
||||
if (cacheDataRef.current) {
|
||||
throttleScroll();
|
||||
}
|
||||
});
|
||||
// termRef.current.onLineFeed((e: any) => {
|
||||
// if (cacheDataRef.current) {
|
||||
// throttleScroll();
|
||||
// }
|
||||
// });
|
||||
};
|
||||
|
||||
const handleResize = _.throttle(() => {
|
||||
@@ -111,6 +112,10 @@ const LogsViewer: React.FC<LogsViewerProps> = (props) => {
|
||||
}
|
||||
}, [size]);
|
||||
|
||||
useEffect(() => {
|
||||
throttleScroll();
|
||||
}, [logs]);
|
||||
|
||||
return (
|
||||
<div className="logs-viewer-wrap-w2">
|
||||
<div className="wrap" style={{ height: height }} ref={scroller}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,181 @@
|
||||
import { marked, Tokens } from 'marked';
|
||||
import React from 'react';
|
||||
import { EyeOutlined } from '@ant-design/icons';
|
||||
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 {
|
||||
content: string;
|
||||
height?: string;
|
||||
theme?: 'light' | 'dark';
|
||||
generateImgLink?: (src: string) => string;
|
||||
}
|
||||
|
||||
const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
|
||||
content,
|
||||
height = 'auto'
|
||||
generateImgLink,
|
||||
height = 'auto',
|
||||
theme = 'light'
|
||||
}) => {
|
||||
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>`;
|
||||
};
|
||||
|
||||
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 (
|
||||
<div style={{ height, overflow: 'auto' }}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: marked(content, { renderer }) }}
|
||||
/>
|
||||
<div
|
||||
style={{ height }}
|
||||
className="markdown-viewer custom-scrollbar-horizontal"
|
||||
>
|
||||
{renderTokens(tokens)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
@@ -0,0 +1,16 @@
|
||||
const htmlUnescapes: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'"
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -4,9 +4,9 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import Wrapper from './components/wrapper';
|
||||
import { SealFormItemProps } from './types';
|
||||
|
||||
const SealAutoComplete: React.FC<AutoCompleteProps & SealFormItemProps> = (
|
||||
props
|
||||
) => {
|
||||
const SealAutoComplete: React.FC<
|
||||
AutoCompleteProps & SealFormItemProps & { onInput?: (e: Event) => void }
|
||||
> = (props) => {
|
||||
const {
|
||||
label,
|
||||
placeholder,
|
||||
@@ -75,7 +75,7 @@ const SealAutoComplete: React.FC<AutoCompleteProps & SealFormItemProps> = (
|
||||
<Wrapper
|
||||
status={status}
|
||||
extra={extra}
|
||||
label={label || (placeholder as string)}
|
||||
label={label}
|
||||
isFocus={isFocus}
|
||||
required={required}
|
||||
description={description}
|
||||
@@ -87,7 +87,7 @@ const SealAutoComplete: React.FC<AutoCompleteProps & SealFormItemProps> = (
|
||||
{...rest}
|
||||
ref={inputRef}
|
||||
placeholder={
|
||||
isFocus ? (
|
||||
isFocus || !label ? (
|
||||
<span style={{ paddingLeft: '12px' }}>{placeholder}</span>
|
||||
) : (
|
||||
''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
:local(.wrapper-box) {
|
||||
@borderRadius: 8px;
|
||||
@borderRadius: var(--border-radius-base);
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -95,6 +95,7 @@
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
padding-block: 20px 0;
|
||||
max-width: 100%;
|
||||
|
||||
&.no-wrapper-style {
|
||||
padding-block: 0;
|
||||
|
||||
@@ -28,7 +28,11 @@ const Wrapper: React.FC<WrapperProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
style={{ ...style }}
|
||||
style={{
|
||||
padding: '0 calc(var(--ant-padding-sm) - 5px)',
|
||||
width: '100%',
|
||||
...style
|
||||
}}
|
||||
className={classNames(
|
||||
wrapperStyle.wrapper,
|
||||
wrapperStyle[`validate-status-${status}`],
|
||||
@@ -51,8 +55,8 @@ const Wrapper: React.FC<WrapperProps> = ({
|
||||
></LabelInfo>
|
||||
</label>
|
||||
<div
|
||||
className="child-inner"
|
||||
style={{
|
||||
padding: '0 calc(var(--ant-padding-sm) - 5px)',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -62,7 +62,7 @@ const SealInputNumber: React.FC<InputNumberProps & SealFormItemProps> = (
|
||||
return (
|
||||
<Wrapper
|
||||
status={status}
|
||||
label={label || (placeholder as string)}
|
||||
label={label}
|
||||
isFocus={isFocus}
|
||||
required={required}
|
||||
description={description}
|
||||
|
||||
@@ -68,7 +68,7 @@ const SealInputSearch: React.FC<SearchProps & SealFormItemProps> = (props) => {
|
||||
return (
|
||||
<Wrapper
|
||||
status={status}
|
||||
label={label || (placeholder as string)}
|
||||
label={label}
|
||||
isFocus={isFocus}
|
||||
required={required}
|
||||
description={description}
|
||||
|
||||
@@ -57,7 +57,7 @@ const SealPassword: React.FC<InputProps & SealFormItemProps> = (props) => {
|
||||
return (
|
||||
<Wrapper
|
||||
status={status}
|
||||
label={label || (placeholder as string)}
|
||||
label={label}
|
||||
isFocus={isFocus}
|
||||
required={required}
|
||||
description={description}
|
||||
|
||||
@@ -17,6 +17,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
||||
isInFormItems = true,
|
||||
variant,
|
||||
addAfter,
|
||||
checkStatus,
|
||||
trim = true,
|
||||
...rest
|
||||
} = props;
|
||||
@@ -70,8 +71,8 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
status={status}
|
||||
label={label || (placeholder as string)}
|
||||
status={checkStatus || status}
|
||||
label={label}
|
||||
isFocus={isFocus}
|
||||
required={required}
|
||||
description={description}
|
||||
@@ -82,7 +83,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
|
||||
>
|
||||
<Input
|
||||
{...rest}
|
||||
placeholder={isFocus ? placeholder : ''}
|
||||
placeholder={isFocus || !label ? placeholder : ''}
|
||||
ref={inputRef}
|
||||
autoComplete="off"
|
||||
onInput={handleInput}
|
||||
|
||||
@@ -60,7 +60,7 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
|
||||
return (
|
||||
<Wrapper
|
||||
status={status}
|
||||
label={label || (placeholder as string)}
|
||||
label={label}
|
||||
isFocus={isFocus}
|
||||
required={required}
|
||||
description={description}
|
||||
|
||||
@@ -79,7 +79,7 @@ const SealTextArea: React.FC<TextAreaProps & SealFormItemProps> = (props) => {
|
||||
return (
|
||||
<Wrapper
|
||||
status={status}
|
||||
label={label || (placeholder as string)}
|
||||
label={label}
|
||||
isFocus={isFocus}
|
||||
required={required}
|
||||
description={description}
|
||||
|
||||
@@ -9,4 +9,5 @@ export interface SealFormItemProps {
|
||||
addAfter?: React.ReactNode;
|
||||
loading?: React.ReactNode;
|
||||
trim?: boolean;
|
||||
checkStatus?: 'success' | 'error' | 'warning' | '';
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ const TableRow: React.FC<
|
||||
}, []);
|
||||
|
||||
const renderChildrenData = () => {
|
||||
return renderChildren?.(childrenData);
|
||||
return renderChildren?.(childrenData, record);
|
||||
};
|
||||
|
||||
const handlePolling = async () => {
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
height: 68px;
|
||||
word-break: break-word;
|
||||
min-width: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.cell-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&-left {
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -52,7 +52,7 @@ export interface SealTableProps {
|
||||
onCell?: (record: any, dataIndex: string) => void;
|
||||
onSort?: (dataIndex: string, order: 'ascend' | 'descend') => 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[]>;
|
||||
loadChildrenAPI?: (record: any) => string;
|
||||
contentRendered?: () => void;
|
||||
|
||||
@@ -52,7 +52,7 @@ const StatusTag: React.FC<StatusTagProps> = ({
|
||||
const renderContent = () => {
|
||||
const percent = download?.percent || 0;
|
||||
|
||||
if (download && percent > 0 && percent < 100) {
|
||||
if (download && percent > 0 && percent <= 100) {
|
||||
return (
|
||||
<>
|
||||
<span className="progress">{download?.percent || 0}%</span>
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
/* 只隐藏垂直滚动条轨道 */
|
||||
&::-webkit-scrollbar-track {
|
||||
width: 0 !important;
|
||||
color: transparent;
|
||||
|
||||
Vendored
+10
-4
@@ -22,14 +22,20 @@ declare namespace Global {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface BaseListItem {
|
||||
interface BaseListItem<T> {
|
||||
key: string;
|
||||
value: string | number;
|
||||
value: T;
|
||||
}
|
||||
|
||||
interface BaseOption {
|
||||
interface BaseOption<T> {
|
||||
label: string;
|
||||
value: string | number;
|
||||
value: T;
|
||||
}
|
||||
|
||||
interface HintOptions {
|
||||
label: string;
|
||||
value: string;
|
||||
opts?: Array<BaseOption<string>>;
|
||||
}
|
||||
|
||||
type SearchParams = Pagination & { search?: string };
|
||||
|
||||
+24
-20
@@ -1,23 +1,27 @@
|
||||
import { platformCall } from '@/utils';
|
||||
|
||||
const platform = platformCall();
|
||||
const KeybindingsMap = {
|
||||
CREATE: ['alt+ctrl+N', 'alt+meta+N'],
|
||||
CLEAR: ['alt+ctrl+W', 'alt+meta+W'],
|
||||
RIGHT: ['ctrl+RIGHT', 'meta+RIGHT'],
|
||||
SAVE: ['ctrl+S', 'meta+S'],
|
||||
SUBMIT: ['ctrl+enter', 'meta+enter'],
|
||||
SAVEAS: ['alt+ctrl+S', 'alt+meta+S'],
|
||||
OPEN: ['alt+ctrl+O', 'alt+meta+O'],
|
||||
CANCEL: ['ctrl+W', 'meta+W'],
|
||||
CREATE: ['Alt+Ctrl+N', 'Alt+Meta+N'],
|
||||
CLEAR: ['Alt+Ctrl+K', 'Alt+Meta+K'],
|
||||
RIGHT: ['Ctrl+RIGHT', 'Meta+RIGHT'],
|
||||
SAVE: ['Ctrl+S', 'Meta+S'],
|
||||
SUBMIT: ['Ctrl+Enter', 'Meta+Enter'],
|
||||
SAVEAS: ['Alt+Ctrl+S', 'Alt+Meta+S'],
|
||||
OPEN: ['Alt+Ctrl+O', 'Alt+Meta+O'],
|
||||
CANCEL: ['Ctrl+W', 'Meta+W'],
|
||||
DELETE: ['delete'],
|
||||
COPY: ['ctrl+C', 'meta+C'],
|
||||
REFRESH: ['ctrl+R', 'meta+R'],
|
||||
EDIT: ['ctrl+E', 'meta+E'],
|
||||
SEARCH: ['ctrl+K', 'meta+K'],
|
||||
RESET: ['alt+ctrl+R', 'alt+meta+R'],
|
||||
INPUT: ['ctrl+K', 'meta+K'],
|
||||
NEW1: ['ctrl+1', 'meta+1'],
|
||||
NEW2: ['ctrl+2', 'meta+2']
|
||||
COPY: ['Ctrl+C', 'Meta+C'],
|
||||
REFRESH: ['Ctrl+R', 'Meta+R'],
|
||||
EDIT: ['Ctrl+E', 'Meta+E'],
|
||||
SEARCH: ['Ctrl+K', 'Meta+K'],
|
||||
RESET: ['Alt+Ctrl+R', 'Alt+Meta+R'],
|
||||
INPUT: ['Ctrl+K', 'Meta+K'],
|
||||
NEW1: ['Ctrl+1', 'Meta+1'],
|
||||
NEW2: ['Ctrl+2', 'Meta+2'],
|
||||
NEW3: ['Ctrl+3', 'Meta+3'],
|
||||
FOCUS: ['/', '/'],
|
||||
ADD: ['Alt+Ctrl+Enter', 'Alt+Meta+Enter']
|
||||
};
|
||||
|
||||
type KeyBindingType = keyof typeof KeybindingsMap;
|
||||
@@ -36,11 +40,11 @@ const KeybiningList: KeybindingValue[] = Object.entries(KeybindingsMap).map(
|
||||
keybinding: keybinding,
|
||||
command: key,
|
||||
textKeybinding: platform.isMac
|
||||
? keybinding.replace('meta', 'Command').replace('alt', 'Option')
|
||||
: keybinding.replace('ctrl', 'Ctrl'),
|
||||
? keybinding.replace('Meta', 'Cmd').replace('Alt', 'Option')
|
||||
: keybinding.replace('Ctrl', 'Ctrl'),
|
||||
iconKeybinding: platform.isMac
|
||||
? keybinding.replace('meta', '⌘').replace('alt', '⌥')
|
||||
: keybinding.replace('ctrl', 'Ctrl')
|
||||
? keybinding.replace('Meta', '⌘').replace('Alt', '⌥')
|
||||
: keybinding.replace('Ctrl', 'Ctrl')
|
||||
} as KeybindingValue;
|
||||
}
|
||||
);
|
||||
|
||||
+137
-15
@@ -2,15 +2,22 @@
|
||||
@import url('src/assets/styles/menu.less');
|
||||
|
||||
html {
|
||||
--font-family: 'noto sans', sans-serif;
|
||||
--ant-color-text-secondary: rgba(0, 0, 0, 65%);
|
||||
--ant-color-text-tertiary: rgba(0, 0, 0, 45%);
|
||||
--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-tertiary: rgba(0, 0, 0, 4%);
|
||||
--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-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-fill-1: #fff;
|
||||
--ant-color-text: #000;
|
||||
@@ -21,12 +28,14 @@ html {
|
||||
--color-logs-text: #d4d4d4;
|
||||
--layout-content-blockpadding: 32px;
|
||||
--layout-content-inlinepadding: 32px;
|
||||
--menu-border-radius-base: 8px;
|
||||
--border-radius-base: 8px;
|
||||
--border-radius-modal: 6px;
|
||||
--menu-border-radius-base: 4px;
|
||||
--border-radius-base: 4px;
|
||||
--border-radius-middle: 20px;
|
||||
--border-radius-small: 8px;
|
||||
--border-radius-mdium: 6px;
|
||||
--border-radius-small: 4px;
|
||||
--border-radius-mdium: 4px;
|
||||
--border-radius-mini: 4px;
|
||||
--border-radius-2px: 2px;
|
||||
--color-white-1: rgba(255, 255, 255, 100%);
|
||||
--color-fill-sider: #f4f5f4;
|
||||
--font-weight-normal: 500;
|
||||
@@ -36,14 +45,15 @@ html {
|
||||
--color-text-3: rgba(0, 0, 0, 45%);
|
||||
--color-text-2: rgba(0, 0, 0, 65%);
|
||||
--color-bg-light-1: #e6f6ff;
|
||||
--font-size-small: 13px;
|
||||
--font-size-base: 12px;
|
||||
--font-size-large: 16px;
|
||||
--font-size-middle: 14px;
|
||||
--table-td-radius: 8px;
|
||||
--checkbox-border-radius: 4px;
|
||||
--table-td-radius: 4px;
|
||||
--checkbox-border-radius: 2px;
|
||||
--ant-table-cell-padding-inline: 16px;
|
||||
--ant-table-cell-padding-block: 8px;
|
||||
--ant-table-header-border-radius: 8px;
|
||||
--ant-table-cell-padding-block: 6px;
|
||||
--ant-table-header-border-radius: 4px;
|
||||
--ant-table-header-split-color: #f0f0f0;
|
||||
--ant-table-row-selected-bg: #e6f6ff;
|
||||
--ant-table-row-selected-hover-bg: #e6f6ff;
|
||||
@@ -59,7 +69,7 @@ html {
|
||||
--ant-input-active-border-color: #007bff;
|
||||
--ant-input-hover-border-color: #2997ff;
|
||||
--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);
|
||||
--color-selected-bg: rgba(230, 230, 230, 88%);
|
||||
// --box-shadow-base: none;
|
||||
@@ -75,7 +85,7 @@ html {
|
||||
|
||||
&.ant-menu-css-var {
|
||||
// --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-color: var(--color-text-1);
|
||||
}
|
||||
@@ -92,7 +102,7 @@ html {
|
||||
--ant-font-size-xl: 20px;
|
||||
--ant-font-size: var(--font-size-base);
|
||||
--ant-padding-sm: 14px;
|
||||
--ant-border-radius-lg: 8px;
|
||||
--ant-border-radius-lg: 4px;
|
||||
--ant-color-text: #000;
|
||||
--ant-color-error: #ff4d4f;
|
||||
--ant-color-bg-mask: rgba(0, 0, 0, 35%);
|
||||
@@ -113,6 +123,10 @@ html {
|
||||
--ant-table-row-hover-bg: #e6e6e6;
|
||||
}
|
||||
|
||||
&.ant-progress {
|
||||
--ant-progress-line-border-radius: 2px;
|
||||
}
|
||||
|
||||
&.ant-modal-css-var {
|
||||
--ant-modal-title-font-size: 14px;
|
||||
--ant-modal-header-margin-bottom: 20px;
|
||||
@@ -126,7 +140,7 @@ html {
|
||||
.css-var-ri.ant-menu-css-var,
|
||||
.css-var-rh.ant-menu-css-var {
|
||||
--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-color: var(--color-text-1);
|
||||
--ant-menu-item-active-bg: rgba(0, 0, 0, 4%);
|
||||
@@ -269,7 +283,7 @@ body {
|
||||
|
||||
// form item help
|
||||
.ant-form-item-with-help .ant-form-item-explain {
|
||||
padding-left: 24px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
// icon
|
||||
@@ -293,12 +307,26 @@ body {
|
||||
}
|
||||
|
||||
.ant-pro-sider-logo {
|
||||
position: relative;
|
||||
padding-left: 18px;
|
||||
padding-block: 12px;
|
||||
border-block-end: none;
|
||||
}
|
||||
|
||||
.ant-pro-sider-logo-collapsed {
|
||||
padding-left: 12px;
|
||||
|
||||
.collapse-wrap {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.collapse-wrap {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pro-sider .ant-layout-sider-children {
|
||||
@@ -427,6 +455,10 @@ body {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-submenu-title {
|
||||
@@ -442,7 +474,83 @@ body {
|
||||
}
|
||||
// ======== monaco editor style ============
|
||||
.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 {
|
||||
@@ -500,6 +608,12 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// =============== segment start==============
|
||||
.ant-segmented .ant-segmented-item::after {
|
||||
border-radius: var(--border-radius-mini);
|
||||
}
|
||||
// =============== segment end ===============
|
||||
|
||||
.background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -560,6 +674,14 @@ body {
|
||||
|
||||
.ant-modal {
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { WatchEventType } from '@/config';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
interface ChunkedCollection {
|
||||
@@ -19,6 +18,7 @@ export function useUpdateChunkedList(options: {
|
||||
}) {
|
||||
const cacheDataListRef = useRef<any[]>(options.dataList || []);
|
||||
const timerRef = useRef<any>(null);
|
||||
const countRef = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
cacheDataListRef.current = [...(options.dataList || [])];
|
||||
@@ -30,74 +30,71 @@ export function useUpdateChunkedList(options: {
|
||||
options.setDataList?.([...cacheDataListRef.current]);
|
||||
}, 80);
|
||||
};
|
||||
const updateChunkedList = (
|
||||
data: ChunkedCollection,
|
||||
dataList?: { id: string | number }[]
|
||||
) => {
|
||||
const updateChunkedList = (data: ChunkedCollection) => {
|
||||
console.log('updateChunkedList=====', {
|
||||
ids: data?.ids,
|
||||
type: data?.type,
|
||||
collection: data?.collection.length,
|
||||
dataList: _.map(dataList, (o: any) => o.id)
|
||||
collection: data?.collection
|
||||
});
|
||||
let collections = data?.collection || [];
|
||||
if (options?.computedID) {
|
||||
collections = _.map(collections, (item: any) => {
|
||||
collections = collections?.map((item: any) => {
|
||||
item.id = options?.computedID?.(item);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
if (options?.filterFun) {
|
||||
collections = _.filter(data?.collection, options?.filterFun);
|
||||
collections = data?.collection?.filter(options?.filterFun);
|
||||
}
|
||||
if (options?.mapFun) {
|
||||
collections = _.map(data?.collection, options?.mapFun);
|
||||
collections = data?.collection?.map(options?.mapFun);
|
||||
}
|
||||
const ids = data?.ids || [];
|
||||
// CREATE
|
||||
if (data?.type === WatchEventType.CREATE) {
|
||||
const newDataList: any[] = [];
|
||||
_.each(collections, (item: any) => {
|
||||
const updateIndex = _.findIndex(
|
||||
cacheDataListRef.current,
|
||||
const newDataList = collections.reduce((acc: any[], item: any) => {
|
||||
const updateIndex = cacheDataListRef.current?.findIndex(
|
||||
(sItem: any) => sItem.id === item.id
|
||||
);
|
||||
const updateItem = { ...item };
|
||||
if (updateIndex === -1) {
|
||||
const updateItem = _.cloneDeep(item);
|
||||
newDataList.push(updateItem);
|
||||
acc.push(updateItem);
|
||||
} else {
|
||||
cacheDataListRef.current[updateIndex] = updateItem;
|
||||
}
|
||||
console.log('create=========', updateIndex, collections);
|
||||
});
|
||||
cacheDataListRef.current = [...newDataList, ...cacheDataListRef.current];
|
||||
// options.setDataList?.([...cacheDataListRef.current]);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
cacheDataListRef.current = [
|
||||
...newDataList,
|
||||
...cacheDataListRef.current
|
||||
].slice(0, 10);
|
||||
}
|
||||
// DELETE
|
||||
if (data?.type === WatchEventType.DELETE) {
|
||||
cacheDataListRef.current = _.filter(
|
||||
cacheDataListRef.current,
|
||||
cacheDataListRef.current = cacheDataListRef.current?.filter(
|
||||
(item: any) => {
|
||||
return !_.includes(ids, item.id);
|
||||
return !ids?.includes(item.id);
|
||||
}
|
||||
);
|
||||
options.setDataList?.([...cacheDataListRef.current]);
|
||||
}
|
||||
// UPDATE
|
||||
if (data?.type === WatchEventType.UPDATE) {
|
||||
_.each(collections, (item: any) => {
|
||||
const updateIndex = _.findIndex(
|
||||
cacheDataListRef.current,
|
||||
collections?.forEach((item: any) => {
|
||||
const updateIndex = cacheDataListRef.current?.findIndex(
|
||||
(sItem: any) => sItem.id === item.id
|
||||
);
|
||||
const updateItem = { ...item };
|
||||
if (updateIndex > -1) {
|
||||
const updateItem = _.cloneDeep(item);
|
||||
cacheDataListRef.current[updateIndex] = updateItem;
|
||||
} else if (updateIndex === -1) {
|
||||
const updateItem = _.cloneDeep(item);
|
||||
cacheDataListRef.current.push(updateItem);
|
||||
cacheDataListRef.current = [
|
||||
updateItem,
|
||||
...cacheDataListRef.current.slice(0, 9)
|
||||
];
|
||||
}
|
||||
});
|
||||
console.log('updateChunkedList=====update', cacheDataListRef.current);
|
||||
// options.setDataList?.([...cacheDataListRef.current]);
|
||||
}
|
||||
|
||||
debounceUpdateChunckedList();
|
||||
|
||||
+57
-22
@@ -8,6 +8,7 @@ import VersionInfo, { modalConfig } from '@/components/version-info';
|
||||
import { logout } from '@/pages/login/apis';
|
||||
import { useAccessMarkedRoutes } from '@@/plugin-access';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
|
||||
import { ProLayout } from '@ant-design/pro-components';
|
||||
import {
|
||||
Link,
|
||||
@@ -20,9 +21,9 @@ import {
|
||||
useNavigate,
|
||||
type IRoute
|
||||
} from '@umijs/max';
|
||||
import { Modal } from 'antd';
|
||||
import { Button, Modal } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import Exception from './Exception';
|
||||
import './Layout.css';
|
||||
import { LogoIcon, SLogoIcon } from './Logo';
|
||||
@@ -92,6 +93,7 @@ export default (props: any) => {
|
||||
const intl = useIntl();
|
||||
const { clientRoutes, pluginManager } = useAppData();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [collapseValue, setCollapseValue] = useState(false);
|
||||
|
||||
const initialInfo = (useModel && useModel('@@initialState')) || {
|
||||
initialState: undefined,
|
||||
@@ -139,6 +141,11 @@ export default (props: any) => {
|
||||
notFound: <span>404 not found</span>
|
||||
};
|
||||
|
||||
const handleToggleCollapse = (e: any) => {
|
||||
e.stopPropagation();
|
||||
setCollapsed(!collapsed);
|
||||
};
|
||||
|
||||
const newRoutes = filterRoutes(
|
||||
clientRoutes.filter((route) => route.id === '@@/global-layout'),
|
||||
(route) => {
|
||||
@@ -151,7 +158,6 @@ export default (props: any) => {
|
||||
|
||||
const role = initialState?.currentUser?.is_admin ? 'admin' : 'user';
|
||||
const [route] = useAccessMarkedRoutes(mapRoutes(newRoutes, role));
|
||||
console.log('clientRoutes===========', route, clientRoutes, newRoutes);
|
||||
|
||||
patchRoutes({
|
||||
routes: route.children,
|
||||
@@ -162,7 +168,52 @@ export default (props: any) => {
|
||||
() => matchRoutes(route?.children || [], location.pathname)?.pop?.()?.route,
|
||||
[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 (
|
||||
<div>
|
||||
<div className="background"></div>
|
||||
@@ -179,15 +230,11 @@ export default (props: any) => {
|
||||
e.preventDefault();
|
||||
navigate('/');
|
||||
}}
|
||||
menuHeaderRender={renderMenuHeader}
|
||||
collapsed={collapsed}
|
||||
onCollapse={(collapsed) => {
|
||||
setCollapsed(collapsed);
|
||||
}}
|
||||
onPageChange={(route) => {
|
||||
const { location } = history;
|
||||
|
||||
console.log('onPageChange', userInfo, initialState);
|
||||
|
||||
// if user is not change password, redirect to change password page
|
||||
if (
|
||||
location.pathname !== loginPath &&
|
||||
@@ -242,19 +289,7 @@ export default (props: any) => {
|
||||
fixSiderbar
|
||||
fixedHeader
|
||||
{...runtimeConfig}
|
||||
actionsRender={(layoutProps) => {
|
||||
const dom = getRightRenderContent({
|
||||
runtimeConfig,
|
||||
loading,
|
||||
initialState,
|
||||
setInitialState,
|
||||
intl,
|
||||
siderWidth: layoutProps.siderWidth,
|
||||
collapsed: layoutProps.collapsed
|
||||
});
|
||||
|
||||
return dom;
|
||||
}}
|
||||
actionsRender={actionRender}
|
||||
>
|
||||
<Exception
|
||||
route={matchedRoute}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import avatarImg from '@/assets/images/avatar.png';
|
||||
import externalLinks from '@/constants/external-links';
|
||||
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 _ from 'lodash';
|
||||
|
||||
export function getRightRenderContent(opts: {
|
||||
export const getRightRenderContent = (opts: {
|
||||
runtimeConfig: any;
|
||||
loading: boolean;
|
||||
initialState: any;
|
||||
@@ -26,7 +25,7 @@ export function getRightRenderContent(opts: {
|
||||
setInitialState: any;
|
||||
siderWidth: number;
|
||||
intl: any;
|
||||
}) {
|
||||
}) => {
|
||||
const { intl, collapsed, siderWidth } = opts;
|
||||
|
||||
const allLocals = getAllLocales();
|
||||
@@ -121,7 +120,9 @@ export function getRightRenderContent(opts: {
|
||||
key: 'help',
|
||||
icon: <QuestionCircleOutlined />,
|
||||
label: (
|
||||
<span>{intl?.formatMessage?.({ id: 'common.button.help' })}</span>
|
||||
<span className="sub-title">
|
||||
{intl?.formatMessage?.({ id: 'common.button.help' })}
|
||||
</span>
|
||||
),
|
||||
children: helpList.map((item) => ({
|
||||
key: item.key,
|
||||
@@ -169,7 +170,7 @@ export function getRightRenderContent(opts: {
|
||||
key: 'lang',
|
||||
icon: <GlobalOutlined />,
|
||||
label: (
|
||||
<span>
|
||||
<span className="sub-title">
|
||||
{intl?.formatMessage?.({ id: 'common.settings.language' })}
|
||||
</span>
|
||||
),
|
||||
@@ -280,4 +281,4 @@ export function getRightRenderContent(opts: {
|
||||
></Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+17
-22
@@ -1,23 +1,18 @@
|
||||
import apikeys from './en-US/apikeys';
|
||||
import common from './en-US/common';
|
||||
import dashboard from './en-US/dashboard';
|
||||
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';
|
||||
/**
|
||||
* @description The directory name used in require.context must match the language configuration names defined in lang-config-map.ts.
|
||||
* @example Directories like 'en-US' or 'zh-CN' should correspond exactly to the configuration names in lang-config-map.ts.
|
||||
*/
|
||||
|
||||
export default {
|
||||
...common,
|
||||
...menu,
|
||||
...models,
|
||||
...playground,
|
||||
...resources,
|
||||
...apikeys,
|
||||
...users,
|
||||
...dashboard,
|
||||
...usage,
|
||||
...shortcuts
|
||||
};
|
||||
const requireContext = require.context(`./en-US`, false, /\.ts$/);
|
||||
|
||||
let languageConfig: Record<string, string> = {};
|
||||
|
||||
requireContext.keys().forEach((fileName: any) => {
|
||||
const moduleConfig = requireContext(fileName).default;
|
||||
languageConfig = {
|
||||
...languageConfig,
|
||||
...moduleConfig
|
||||
};
|
||||
});
|
||||
|
||||
export default languageConfig;
|
||||
|
||||
@@ -113,9 +113,9 @@ export default {
|
||||
'common.button.logs': 'Logs',
|
||||
'common.ws.close': 'The connection was lost, do you want to reconnect?',
|
||||
'common.ws.reconnect': 'Reconnect',
|
||||
'common.input.key': 'key',
|
||||
'common.input.value': 'value',
|
||||
'common.input.type': 'type',
|
||||
'common.input.key': 'Key',
|
||||
'common.input.value': 'Value',
|
||||
'common.input.type': 'Type',
|
||||
'common.input.visible': 'visibility',
|
||||
'common.input.description': 'description',
|
||||
'common.time.day': 'day',
|
||||
@@ -205,5 +205,6 @@ export default {
|
||||
'common.button.version': 'Version',
|
||||
'common.title.delete.confirm': 'Confirm delete',
|
||||
'common.button.addLabel': 'Add Labels',
|
||||
'common.button.addSelector': 'Add Selectors'
|
||||
'common.button.addSelector': 'Add Selectors',
|
||||
'common.button.addParams': 'Add Parameter'
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export default {
|
||||
'menu.dashboard': 'Dashboard',
|
||||
'menu.playground': 'Playground',
|
||||
'menu.chat': 'Chat',
|
||||
'menu.compare': 'Compare',
|
||||
'menu.models': 'Models',
|
||||
'menu.resources': 'Resources',
|
||||
'menu.apikeys': 'API Keys',
|
||||
|
||||
@@ -22,7 +22,7 @@ export default {
|
||||
'model.form.ollama.model': 'Ollama Model',
|
||||
'model.form.ollamaholder': 'Please select or input model name',
|
||||
'model.deploy.sort': 'Sort',
|
||||
'model.deploy.search.placeholder': 'Search models from Hugging Face',
|
||||
'model.deploy.search.placeholder': 'Search models from {source}',
|
||||
'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.',
|
||||
'models.sort.name': 'Name',
|
||||
@@ -35,6 +35,7 @@ export default {
|
||||
'models.data.card': 'Model Card',
|
||||
'models.available.files': 'Available Files',
|
||||
'models.viewin.hf': 'View in Hugging Face',
|
||||
'models.viewin.modelscope': 'View in ModelScope',
|
||||
'models.architecture': 'Architecture',
|
||||
'models.search.noresult': 'No related models found',
|
||||
'models.search.nofiles': 'No available files',
|
||||
@@ -54,5 +55,14 @@ export default {
|
||||
'models.table.backend': 'Backends',
|
||||
'models.table.acrossworker': 'Distribution Across Workers',
|
||||
'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'
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export default {
|
||||
'playground.system.tips': 'Enter system message here',
|
||||
'playground.system.tips': 'Type system instructions here',
|
||||
'playground.title': 'Playground',
|
||||
'playground.system': 'System',
|
||||
'playground.systemMessage': 'System instructions',
|
||||
'playground.user': 'User',
|
||||
'playground.assistant': 'Assistant',
|
||||
'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.',
|
||||
'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.',
|
||||
'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.'
|
||||
};
|
||||
|
||||
@@ -29,9 +29,9 @@ export default {
|
||||
'resources.table.vram': 'VRAM',
|
||||
'resources.table.index': 'Index',
|
||||
'resources.table.workername': 'Worker Name',
|
||||
'resources.table.vender': 'Vender',
|
||||
'resources.table.vender': 'Vendor',
|
||||
'resources.table.temperature': 'Temperature',
|
||||
'resources.table.core': 'Core',
|
||||
'resources.table.core': 'Cores',
|
||||
'resources.table.utilization': 'Utilization',
|
||||
'resources.table.gpuutilization': 'GPU Utilization',
|
||||
'resources.table.vramutilization': 'VRAM Utilization',
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
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 = {
|
||||
'ar-EG': {
|
||||
lang: 'ar-EG',
|
||||
|
||||
+17
-22
@@ -1,23 +1,18 @@
|
||||
import apikeys from './zh-CN/apikeys';
|
||||
import common from './zh-CN/common';
|
||||
import dashboard from './zh-CN/dashboard';
|
||||
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';
|
||||
/**
|
||||
* @description The directory name used in require.context must match the language configuration names defined in lang-config-map.ts.
|
||||
* @example Directories like 'en-US' or 'zh-CN' should correspond exactly to the configuration names in lang-config-map.ts.
|
||||
*/
|
||||
|
||||
export default {
|
||||
...common,
|
||||
...menu,
|
||||
...models,
|
||||
...playground,
|
||||
...resources,
|
||||
...apikeys,
|
||||
...users,
|
||||
...dashboard,
|
||||
...usage,
|
||||
...shortcuts
|
||||
};
|
||||
const requireContext = require.context(`./zh-CN`, false, /\.ts$/);
|
||||
|
||||
let languageConfig: Record<string, string> = {};
|
||||
|
||||
requireContext.keys().forEach((fileName: any) => {
|
||||
const moduleConfig = requireContext(fileName).default;
|
||||
languageConfig = {
|
||||
...languageConfig,
|
||||
...moduleConfig
|
||||
};
|
||||
});
|
||||
|
||||
export default languageConfig;
|
||||
|
||||
@@ -198,5 +198,6 @@ export default {
|
||||
'common.button.version': '版本',
|
||||
'common.title.delete.confirm': '确认删除',
|
||||
'common.button.addLabel': '添加标签',
|
||||
'common.button.addSelector': '添加选择器'
|
||||
'common.button.addSelector': '添加选择器',
|
||||
'common.button.addParams': '添加参数'
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export default {
|
||||
'menu.dashboard': '概览',
|
||||
'menu.playground': '试验场',
|
||||
'menu.chat': '聊天',
|
||||
'menu.compare': '多模型对比',
|
||||
'menu.models': '模型',
|
||||
'menu.resources': '资源',
|
||||
'menu.apikeys': 'API 密钥',
|
||||
|
||||
@@ -22,7 +22,7 @@ export default {
|
||||
'model.form.ollama.model': 'Ollama 模型',
|
||||
'model.form.ollamaholder': '请选择或输入模型名称',
|
||||
'model.deploy.sort': '排序',
|
||||
'model.deploy.search.placeholder': '从 Hugging Face 搜索模型',
|
||||
'model.deploy.search.placeholder': '从 {source} 搜索模型',
|
||||
'model.form.ollamatips':
|
||||
'提示:以下为 GPUStack 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。',
|
||||
'models.sort.name': '名称',
|
||||
@@ -35,6 +35,7 @@ export default {
|
||||
'models.data.card': '模型简介',
|
||||
'models.available.files': '可用文件',
|
||||
'models.viewin.hf': '在 Hugging Face 中查看',
|
||||
'models.viewin.modelscope': '在 ModelScope 中查看',
|
||||
'models.architecture': '架构',
|
||||
'models.search.noresult': '未找到相关模型',
|
||||
'models.search.nofiles': '无可用文件',
|
||||
@@ -53,5 +54,14 @@ export default {
|
||||
'models.table.backend': '后端',
|
||||
'models.table.acrossworker': '跨 worker 推理',
|
||||
'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'
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ export default {
|
||||
'playground.system.tips': '在这里输入系统消息',
|
||||
'playground.title': '试验场',
|
||||
'playground.system': '系统',
|
||||
'playground.systemMessage': '系统消息',
|
||||
'playground.user': '用户',
|
||||
'playground.assistant': '小助手',
|
||||
'playground.newMessage': '新消息',
|
||||
@@ -29,5 +30,21 @@ export default {
|
||||
'停止序列是一个预定义或用户指定的文本字符串,当这些序列出现时,它会提示 AI 停止生成后续的 token。',
|
||||
'playground.viewcode.tips':
|
||||
'{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>
|
||||
<div>
|
||||
<Tag
|
||||
bordered={false}
|
||||
color="error"
|
||||
style={{ padding: '6px 8px', marginBottom: 16 }}
|
||||
>
|
||||
|
||||
@@ -2,71 +2,9 @@ import PageTools from '@/components/page-tools';
|
||||
import { convertFileSize } from '@/utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Col, Row, Table } from 'antd';
|
||||
import { useContext } from 'react';
|
||||
import { memo, useContext } from 'react';
|
||||
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 intl = useIntl();
|
||||
const data = useContext(DashboardContext).active_models || [];
|
||||
@@ -76,14 +14,6 @@ const ActiveTable = () => {
|
||||
dataIndex: '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' }),
|
||||
dataIndex: 'resource_claim.memory',
|
||||
@@ -133,4 +63,4 @@ const ActiveTable = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveTable;
|
||||
export default memo(ActiveTable);
|
||||
|
||||
@@ -29,7 +29,7 @@ const renderCardItem = (data: {
|
||||
const Overview: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const data = useContext(DashboardContext).resource_counts || {};
|
||||
console.log('overview===');
|
||||
|
||||
const renderValue = (
|
||||
value:
|
||||
| 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 = () => {
|
||||
console.log('systemload=====================');
|
||||
const intl = useIntl();
|
||||
const data = useContext(DashboardContext)?.system_load?.history || {};
|
||||
|
||||
@@ -147,7 +146,7 @@ const UtilizationOvertime: React.FC = () => {
|
||||
legendData,
|
||||
xAxisData: _.uniq(xAxisData)
|
||||
};
|
||||
}, [data]);
|
||||
}, [data, intl]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -5,9 +5,8 @@ import breakpoints from '@/config/breakpoints';
|
||||
import useWindowResize from '@/hooks/use-window-resize';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Col, Row } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import _ from 'lodash';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { memo, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { DashboardContext } from '../config/dashboard-context';
|
||||
import ResourceUtilization from './resource-utilization';
|
||||
|
||||
@@ -28,11 +27,29 @@ const SystemLoad = () => {
|
||||
const [paddingRight, setPaddingRight] = useState<string>('20px');
|
||||
const [smallChartHeight, setSmallChartHeight] = useState<number>(190);
|
||||
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(() => {
|
||||
if (size.width < breakpoints.xl) {
|
||||
@@ -70,8 +87,8 @@ const SystemLoad = () => {
|
||||
<Col span={12} style={{ height: smallChartHeight }}>
|
||||
<GaugeChart
|
||||
height={smallChartHeight}
|
||||
value={_.round(data.gpu || 0, 1)}
|
||||
color={strokeColorFunc(data.gpu)}
|
||||
value={chartData.gpu.data}
|
||||
color={chartData.gpu.color}
|
||||
title={intl.formatMessage({
|
||||
id: 'dashboard.gpuutilization'
|
||||
})}
|
||||
@@ -83,8 +100,8 @@ const SystemLoad = () => {
|
||||
id: 'dashboard.vramutilization'
|
||||
})}
|
||||
height={smallChartHeight}
|
||||
color={strokeColorFunc(data.vram)}
|
||||
value={_.round(data.vram || 0, 1)}
|
||||
color={chartData.vram.color}
|
||||
value={chartData.vram.data}
|
||||
></GaugeChart>
|
||||
</Col>
|
||||
<Col span={12} style={{ height: smallChartHeight }}>
|
||||
@@ -93,8 +110,8 @@ const SystemLoad = () => {
|
||||
id: 'dashboard.cpuutilization'
|
||||
})}
|
||||
height={smallChartHeight}
|
||||
color={strokeColorFunc(data.cpu)}
|
||||
value={_.round(data.cpu || 0, 1)}
|
||||
color={chartData.cpu.color}
|
||||
value={chartData.cpu.data}
|
||||
></GaugeChart>
|
||||
</Col>
|
||||
<Col span={12} style={{ height: smallChartHeight }}>
|
||||
@@ -103,8 +120,8 @@ const SystemLoad = () => {
|
||||
id: 'dashboard.memoryutilization'
|
||||
})}
|
||||
height={smallChartHeight}
|
||||
color={strokeColorFunc(data.ram)}
|
||||
value={_.round(data.ram || 0, 1)}
|
||||
color={chartData.ram.color}
|
||||
value={chartData.ram.data}
|
||||
></GaugeChart>
|
||||
</Col>
|
||||
</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 dayjs from 'dayjs';
|
||||
import _ from 'lodash';
|
||||
import { memo, useCallback, useContext } from 'react';
|
||||
import { memo, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { DashboardContext } from '../../config/dashboard-context';
|
||||
import RequestTokenInner from './request-token-inner';
|
||||
import TopUser from './top-user';
|
||||
@@ -33,18 +33,29 @@ const getCurrentMonthDays = () => {
|
||||
|
||||
const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
||||
const intl = useIntl();
|
||||
const currentDate = dayjs();
|
||||
const currentMonthDays = getCurrentMonthDays();
|
||||
let requestData: {
|
||||
name: string;
|
||||
color: string;
|
||||
areaStyle: any;
|
||||
data: { time: string; value: number }[];
|
||||
}[] = [];
|
||||
let tokenData: { time: string; value: number }[] = [];
|
||||
let userData: { name: string; value: number }[] = [];
|
||||
const xAxisData: string[] = currentMonthDays;
|
||||
let topUserList: string[] = [];
|
||||
|
||||
const [topUserData, setTopUserData] = useState<{
|
||||
userData: { name: string; value: number }[];
|
||||
topUserList: string[];
|
||||
}>({
|
||||
userData: [],
|
||||
topUserList: []
|
||||
});
|
||||
|
||||
const [requestTokenData, setRequestTokenData] = useState<{
|
||||
requestData: {
|
||||
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 data = model_usage || {};
|
||||
@@ -54,6 +65,7 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
||||
};
|
||||
|
||||
const generateData = useCallback(() => {
|
||||
const currentMonthDays = getCurrentMonthDays();
|
||||
const requestList: {
|
||||
name: string;
|
||||
color: string;
|
||||
@@ -88,7 +100,7 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
||||
data: []
|
||||
};
|
||||
|
||||
_.each(xAxisData, (date: string) => {
|
||||
_.each(currentMonthDays, (date: string) => {
|
||||
// tokens data
|
||||
const item = _.find(data.completion_token_history, (item: any) => {
|
||||
return dayjs(item.timestamp * 1000).format('YYYY-MM-DD') === date;
|
||||
@@ -140,8 +152,10 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
||||
|
||||
// ========== top users ============
|
||||
if (!data.top_users?.length) {
|
||||
userData = [];
|
||||
topUserList = [];
|
||||
setTopUserData({
|
||||
userData: [],
|
||||
topUserList: []
|
||||
});
|
||||
} else {
|
||||
const users: string[] = [];
|
||||
_.each(data.top_users, (item: any) => {
|
||||
@@ -155,15 +169,23 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
||||
value: item.completion_token_count
|
||||
});
|
||||
});
|
||||
topUserList = _.uniq(users);
|
||||
userData = [topUserCompletion, topUserPrompt];
|
||||
|
||||
setTopUserData({
|
||||
userData: [topUserCompletion, topUserPrompt],
|
||||
topUserList: _.uniq(users)
|
||||
});
|
||||
}
|
||||
|
||||
requestData = [requestList];
|
||||
tokenData = [completionData, prompData];
|
||||
}, [data, xAxisData]);
|
||||
setRequestTokenData({
|
||||
requestData: [requestList],
|
||||
tokenData: [completionData, prompData],
|
||||
xAxisData: currentMonthDays
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
generateData();
|
||||
useEffect(() => {
|
||||
generateData();
|
||||
}, [generateData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -181,13 +203,16 @@ const UsageInner: React.FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
||||
style={{ paddingRight: paddingRight }}
|
||||
>
|
||||
<RequestTokenInner
|
||||
requestData={requestData}
|
||||
xAxisData={xAxisData}
|
||||
tokenData={tokenData}
|
||||
requestData={requestTokenData.requestData}
|
||||
xAxisData={requestTokenData.xAxisData}
|
||||
tokenData={requestTokenData.tokenData}
|
||||
></RequestTokenInner>
|
||||
</Col>
|
||||
<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>
|
||||
</Row>
|
||||
</>
|
||||
|
||||
@@ -17,7 +17,6 @@ interface RequestTokenInnerProps {
|
||||
xAxisData: string[];
|
||||
}
|
||||
const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
|
||||
console.log('request token inner=====================');
|
||||
const { requestData, tokenData, xAxisData } = props;
|
||||
const intl = useIntl();
|
||||
const labelFormatter = (v: any) => {
|
||||
|
||||
@@ -8,7 +8,6 @@ interface TopUserProps {
|
||||
topUserList: string[];
|
||||
}
|
||||
const TopUser: React.FC<TopUserProps> = (props) => {
|
||||
console.log('TopUser=====================');
|
||||
const { userData, topUserList } = props;
|
||||
const intl = useIntl();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { downloadFile, listFiles, listModels } from '@huggingface/hub';
|
||||
import { PipelineType } from '@huggingface/tasks';
|
||||
import { request } from '@umijs/max';
|
||||
import qs from 'query-string';
|
||||
import {
|
||||
FormData,
|
||||
GPUListItem,
|
||||
@@ -126,6 +127,15 @@ export async function callHuggingfaceQuickSearch(params: any) {
|
||||
|
||||
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(
|
||||
params: { repo: string },
|
||||
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(
|
||||
params: {
|
||||
search: {
|
||||
@@ -152,7 +237,7 @@ export async function queryHuggingfaceModels(
|
||||
...params,
|
||||
...options,
|
||||
limit: 100,
|
||||
additionalFields: ['sha'],
|
||||
additionalFields: ['sha', 'tags'],
|
||||
fetch(url: string, config: any) {
|
||||
try {
|
||||
const newUrl = params.search.sort
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import LabelSelector from '@/components/label-selector';
|
||||
import ListInput from '@/components/list-input';
|
||||
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 { useIntl } from '@umijs/max';
|
||||
import {
|
||||
@@ -13,22 +16,27 @@ import {
|
||||
} from 'antd';
|
||||
import _ from 'lodash';
|
||||
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 vllmConfig from '../config/vllm-config';
|
||||
import dataformStyles from '../style/data-form.less';
|
||||
import GPUCard from './gpu-card';
|
||||
|
||||
interface AdvanceConfigProps {
|
||||
isGGUF: boolean;
|
||||
form: FormInstance;
|
||||
gpuOptions: Array<any>;
|
||||
action: PageActionType;
|
||||
}
|
||||
|
||||
const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
const { form, gpuOptions } = props;
|
||||
const { form, gpuOptions, isGGUF, action } = props;
|
||||
|
||||
const intl = useIntl();
|
||||
const wokerSelector = Form.useWatch('worker_selector', form);
|
||||
const scheduleType = Form.useWatch('scheduleType', form);
|
||||
const backend = Form.useWatch('backend', form);
|
||||
|
||||
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 }>) => {
|
||||
return (
|
||||
<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 children = (
|
||||
<>
|
||||
@@ -202,33 +218,71 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
</SealSelect>
|
||||
</Form.Item>
|
||||
)}
|
||||
<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 style={{ color: 'var(--ant-color-text-tertiary)' }}>
|
||||
{intl.formatMessage({
|
||||
id: 'resources.form.enablePartialOffload'
|
||||
<Form.Item name="backend">
|
||||
<SealSelect
|
||||
label={intl.formatMessage({ id: 'models.form.backend' })}
|
||||
options={[
|
||||
{
|
||||
label: `llama-box(llama.cpp)`,
|
||||
value: backendOptionsMap.llamaBox,
|
||||
disabled: !isGGUF
|
||||
},
|
||||
{
|
||||
label: 'vLLM',
|
||||
value: backendOptionsMap.vllm,
|
||||
disabled: isGGUF
|
||||
}
|
||||
]}
|
||||
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
|
||||
className="m-l-4"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{scheduleType === 'auto' && (
|
||||
>
|
||||
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
|
||||
{intl.formatMessage({
|
||||
id: 'resources.form.enablePartialOffload'
|
||||
})}
|
||||
</span>
|
||||
<InfoCircleOutlined
|
||||
className="m-l-4"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
{scheduleType === 'auto' && isGGUF && (
|
||||
<div style={{ paddingBottom: 22, paddingLeft: 10 }}>
|
||||
<Form.Item<FormData>
|
||||
name="distributed_inference_across_workers"
|
||||
@@ -266,16 +320,26 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
{intl.formatMessage({ id: 'resources.form.advanced' })}
|
||||
</span>
|
||||
),
|
||||
forceRender: true,
|
||||
children
|
||||
}
|
||||
];
|
||||
}, [form, intl, gpuOptions, scheduleType, wokerSelector]);
|
||||
}, [
|
||||
form,
|
||||
intl,
|
||||
gpuOptions,
|
||||
paramsConfig,
|
||||
scheduleType,
|
||||
wokerSelector,
|
||||
isGGUF
|
||||
]);
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
expandIconPosition="start"
|
||||
bordered={false}
|
||||
ghost
|
||||
destroyInactivePanel={false}
|
||||
className={dataformStyles['advanced-collapse']}
|
||||
expandIcon={({ isActive }) => (
|
||||
<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">
|
||||
<SimpleBar
|
||||
style={{
|
||||
height: height || 'calc(100vh - 89px)',
|
||||
maxHeight: height || 'calc(100vh - 89px)',
|
||||
paddingBottom: '50px'
|
||||
}}
|
||||
>
|
||||
@@ -23,7 +23,7 @@ const ColumnWrapper: React.FC<any> = ({ children, footer, height }) => {
|
||||
}
|
||||
return (
|
||||
<div className="column-wrapper">
|
||||
<SimpleBar style={{ height: height || 'calc(100vh - 89px)' }}>
|
||||
<SimpleBar style={{ maxHeight: height || 'calc(100vh - 89px)' }}>
|
||||
{children}
|
||||
</SimpleBar>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import SealAutoComplete from '@/components/seal-form/auto-complete';
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import SealSelect from '@/components/seal-form/seal-select';
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import { Form, Tooltip, Typography } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react';
|
||||
import { queryGPUList } from '../apis';
|
||||
import { modelSourceMap, ollamaModelOptions } from '../config';
|
||||
import {
|
||||
backendOptionsMap,
|
||||
modelSourceMap,
|
||||
ollamaModelOptions
|
||||
} from '../config';
|
||||
import { FormData, GPUListItem } from '../config/types';
|
||||
import AdvanceConfig from './advance-config';
|
||||
|
||||
@@ -21,7 +27,8 @@ interface DataFormProps {
|
||||
ref?: any;
|
||||
source: string;
|
||||
action: PageActionType;
|
||||
repo: string;
|
||||
selectedModel: any;
|
||||
isGGUF: boolean;
|
||||
onOk: (values: FormData) => void;
|
||||
}
|
||||
|
||||
@@ -35,11 +42,20 @@ const sourceOptions = [
|
||||
label: 'Ollama Library',
|
||||
value: modelSourceMap.ollama_library_value,
|
||||
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 { action, repo, onOk } = props;
|
||||
const { action, isGGUF, onOk } = props;
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
const [gpuOptions, setGpuOptions] = useState<
|
||||
@@ -86,22 +102,18 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
);
|
||||
|
||||
const handleOnSelectModel = () => {
|
||||
console.log('repo=============', repo);
|
||||
if (!repo) {
|
||||
return;
|
||||
}
|
||||
let name = _.split(repo, '/').slice(-1)[0];
|
||||
let name = _.split(props.selectedModel.name, '/').slice(-1)[0];
|
||||
const reg = /(-gguf)$/i;
|
||||
name = _.toLower(name).replace(reg, '');
|
||||
|
||||
if (props.source === modelSourceMap.huggingface_value) {
|
||||
if (SEARCH_SOURCE.includes(props.source)) {
|
||||
form.setFieldsValue({
|
||||
huggingface_repo_id: repo,
|
||||
repo_id: props.selectedModel.name,
|
||||
name: name
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
ollama_library_model_name: repo,
|
||||
ollama_library_model_name: props.selectedModel.name,
|
||||
name: name
|
||||
});
|
||||
}
|
||||
@@ -111,8 +123,8 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
return (
|
||||
<>
|
||||
<Form.Item<FormData>
|
||||
name="huggingface_repo_id"
|
||||
key="huggingface_repo_id"
|
||||
name="repo_id"
|
||||
key="repo_id"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -131,26 +143,29 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
disabled={true}
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="huggingface_filename"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{
|
||||
id: 'common.form.rule.input'
|
||||
},
|
||||
{ name: intl.formatMessage({ id: 'models.form.filename' }) }
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({ id: 'models.form.filename' })}
|
||||
required
|
||||
disabled={true}
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
{isGGUF && (
|
||||
<Form.Item<FormData>
|
||||
name="file_name"
|
||||
key="file_name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{
|
||||
id: 'common.form.rule.input'
|
||||
},
|
||||
{ name: intl.formatMessage({ id: 'models.form.filename' }) }
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({ id: 'models.form.filename' })}
|
||||
required
|
||||
disabled={true}
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -203,29 +218,49 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
>
|
||||
<SealAutoComplete
|
||||
filterOption
|
||||
defaultActiveFirstOption
|
||||
disabled={action === PageAction.EDIT}
|
||||
options={ollamaModelOptions}
|
||||
label={intl.formatMessage({ id: 'model.form.ollama.model' })}
|
||||
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
|
||||
options={ollamaModelOptions}
|
||||
></SealAutoComplete>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFieldsBySource = () => {
|
||||
switch (props.source) {
|
||||
case modelSourceMap.huggingface_value:
|
||||
return renderHuggingfaceFields();
|
||||
case modelSourceMap.ollama_library_value:
|
||||
return renderOllamaModelFields();
|
||||
case modelSourceMap.s3_value:
|
||||
return renderS3Fields();
|
||||
default:
|
||||
return null;
|
||||
const renderFieldsBySource = useMemo(() => {
|
||||
if (SEARCH_SOURCE.includes(props.source)) {
|
||||
return renderHuggingfaceFields();
|
||||
}
|
||||
};
|
||||
|
||||
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 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(() => {
|
||||
handleOnSelectModel();
|
||||
}, [repo]);
|
||||
}, [props.selectedModel.name]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
@@ -263,9 +306,9 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
replicas: 1,
|
||||
source: props.source,
|
||||
placement_strategy: 'spread',
|
||||
cpu_offloading: false,
|
||||
cpu_offloading: true,
|
||||
scheduleType: 'auto',
|
||||
distributed_inference_across_workers: false
|
||||
distributed_inference_across_workers: true
|
||||
}}
|
||||
>
|
||||
<Form.Item<FormData>
|
||||
@@ -314,7 +357,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
></SealSelect>
|
||||
}
|
||||
</Form.Item>
|
||||
{renderFieldsBySource()}
|
||||
{renderFieldsBySource}
|
||||
<Form.Item<FormData>
|
||||
name="replicas"
|
||||
rules={[
|
||||
@@ -347,7 +390,12 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
})}
|
||||
></SealInput.TextArea>
|
||||
</Form.Item>
|
||||
<AdvanceConfig form={form} gpuOptions={gpuOptions}></AdvanceConfig>
|
||||
<AdvanceConfig
|
||||
form={form}
|
||||
gpuOptions={gpuOptions}
|
||||
isGGUF={isGGUF}
|
||||
action={action}
|
||||
></AdvanceConfig>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -36,30 +36,51 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
||||
action,
|
||||
width = 600
|
||||
} = props || {};
|
||||
const SEARCH_SOURCE = [
|
||||
modelSourceMap.huggingface_value,
|
||||
modelSourceMap.modelscope_value
|
||||
];
|
||||
const form = useRef<any>({});
|
||||
const intl = useIntl();
|
||||
const [huggingfaceRepoId, setHuggingfaceRepoId] = useState<string>('');
|
||||
const [selectedModel, setSelectedModel] = useState<any>({});
|
||||
const [collapsed, setCollapsed] = 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) => {
|
||||
form.current?.setFieldValue?.('huggingface_filename', item.fakeName);
|
||||
form.current?.setFieldValue?.('file_name', item.fakeName);
|
||||
}, []);
|
||||
|
||||
const handleOnSelectModel = (item: any) => {
|
||||
setHuggingfaceRepoId(item.name);
|
||||
setSelectedModel(item);
|
||||
};
|
||||
|
||||
const handleSumit = () => {
|
||||
form.current?.submit?.();
|
||||
};
|
||||
|
||||
const handleSetIsGGUF = (flag: boolean) => {
|
||||
setIsGGUF(flag);
|
||||
if (flag) {
|
||||
setTimeout(() => {
|
||||
modelFileRef.current?.fetchModelFiles?.();
|
||||
}, 50);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setHuggingfaceRepoId('');
|
||||
setSelectedModel({});
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (source === modelSourceMap.ollama_library_value) {
|
||||
setIsGGUF(true);
|
||||
}
|
||||
}, [source]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={
|
||||
@@ -91,41 +112,47 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
||||
overflowX: 'hidden'
|
||||
},
|
||||
content: {
|
||||
borderRadius: '8px 0 0 8px'
|
||||
borderRadius: '6px 0 0 6px'
|
||||
}
|
||||
}}
|
||||
width={width}
|
||||
footer={false}
|
||||
>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{props.source === modelSourceMap.huggingface_value && (
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
<ColumnWrapper>
|
||||
<SearchModel
|
||||
modelSource={props.source}
|
||||
onSelectModel={handleOnSelectModel}
|
||||
setLoadingModel={setLoadingModel}
|
||||
></SearchModel>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
)}
|
||||
{props.source === modelSourceMap.huggingface_value && (
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
<ColumnWrapper>
|
||||
<ModelCard
|
||||
repo={huggingfaceRepoId}
|
||||
onCollapse={setCollapsed}
|
||||
collapsed={collapsed}
|
||||
></ModelCard>
|
||||
<HFModelFile
|
||||
repo={huggingfaceRepoId}
|
||||
onSelectFile={handleSelectModelFile}
|
||||
collapsed={collapsed}
|
||||
></HFModelFile>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
<div style={{ display: 'flex', height: '100%' }}>
|
||||
{SEARCH_SOURCE.includes(props.source) && (
|
||||
<>
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
<ColumnWrapper>
|
||||
<SearchModel
|
||||
modelSource={props.source}
|
||||
onSelectModel={handleOnSelectModel}
|
||||
setLoadingModel={setLoadingModel}
|
||||
></SearchModel>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
<ColumnWrapper>
|
||||
<ModelCard
|
||||
selectedModel={selectedModel}
|
||||
onCollapse={setCollapsed}
|
||||
collapsed={collapsed}
|
||||
modelSource={props.source}
|
||||
setIsGGUF={handleSetIsGGUF}
|
||||
></ModelCard>
|
||||
{isGGUF && (
|
||||
<HFModelFile
|
||||
ref={modelFileRef}
|
||||
selectedModel={selectedModel}
|
||||
modelSource={props.source}
|
||||
onSelectFile={handleSelectModelFile}
|
||||
collapsed={collapsed}
|
||||
></HFModelFile>
|
||||
)}
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<ColumnWrapper
|
||||
footer={
|
||||
@@ -141,7 +168,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
||||
}
|
||||
>
|
||||
<>
|
||||
{source === modelSourceMap.huggingface_value && (
|
||||
{SEARCH_SOURCE.includes(source) && (
|
||||
<TitleWrapper>
|
||||
{intl.formatMessage({ id: 'models.form.configurations' })}
|
||||
<span style={{ display: 'flex', height: 24 }}></span>
|
||||
@@ -150,9 +177,10 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
||||
<DataForm
|
||||
source={source}
|
||||
action={action}
|
||||
repo={huggingfaceRepoId}
|
||||
selectedModel={selectedModel}
|
||||
onOk={onOk}
|
||||
ref={form}
|
||||
isGGUF={isGGUF}
|
||||
></DataForm>
|
||||
</>
|
||||
</ColumnWrapper>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import { convertFileSize } from '@/utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import _ from 'lodash';
|
||||
@@ -6,33 +7,43 @@ import '../style/gpu-card.less';
|
||||
|
||||
const GPUCard: React.FC<{
|
||||
data: any;
|
||||
}> = ({ data }) => {
|
||||
header?: React.ReactNode;
|
||||
info?: React.ReactNode;
|
||||
}> = ({ data, header, info }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className="gpu-card">
|
||||
<div className="header">
|
||||
{data.label}({data.worker_name})[
|
||||
{intl.formatMessage({ id: 'resources.table.index' })}:{data.index}]
|
||||
<div className="header" style={{ width: '100%' }}>
|
||||
{header ?? (
|
||||
<AutoTooltip ghost>
|
||||
{data.label}({data.worker_name})[
|
||||
{intl.formatMessage({ id: 'resources.table.index' })}:{data.index}]
|
||||
</AutoTooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="info">
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'resources.table.vram' })}(
|
||||
{intl.formatMessage({ id: 'resources.table.used' })}/
|
||||
{intl.formatMessage({ id: 'resources.table.total' })}):{' '}
|
||||
<span>
|
||||
{convertFileSize(data?.memory?.used || 0)} /{' '}
|
||||
{convertFileSize(data?.memory?.total || 0)}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'resources.table.gpuutilization' })}:{' '}
|
||||
</span>
|
||||
{_.round(data?.memory?.utilization_rate || 0, 2)}%
|
||||
</span>
|
||||
{info ?? (
|
||||
<>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'resources.table.vram' })}(
|
||||
{intl.formatMessage({ id: 'resources.table.used' })}/
|
||||
{intl.formatMessage({ id: 'resources.table.total' })}):{' '}
|
||||
<span>
|
||||
{convertFileSize(data?.memory?.used || 0)} /{' '}
|
||||
{convertFileSize(data?.memory?.total || 0)}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'resources.table.gpuutilization' })}:{' '}
|
||||
</span>
|
||||
{_.round(data?.memory?.utilization_rate || 0, 2)}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GPUCard;
|
||||
export default React.memo(GPUCard);
|
||||
|
||||
@@ -4,26 +4,41 @@ import { useIntl } from '@umijs/max';
|
||||
import { Col, Empty, Row, Select, Spin, Tag, Tooltip } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
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-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 '../style/hf-model-file.less';
|
||||
import FileParts from './file-parts';
|
||||
import TitleWrapper from './title-wrapper';
|
||||
|
||||
interface HFModelFileProps {
|
||||
repo: string;
|
||||
selectedModel: any;
|
||||
collapsed?: boolean;
|
||||
loadingModel?: boolean;
|
||||
modelSource: string;
|
||||
ref: any;
|
||||
onSelectFile?: (file: any) => void;
|
||||
}
|
||||
|
||||
const pattern = /^(.*)-(\d+)-of-(\d+)\.gguf$/;
|
||||
const pattern = /^(.*)-(\d+)-of-(\d+)\.(.*)$/;
|
||||
|
||||
const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
||||
const { collapsed, loadingModel } = props;
|
||||
const filterReg = /\.(safetensors|gguf)$/i;
|
||||
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 [dataSource, setDataSource] = useState<any>({
|
||||
fileList: [],
|
||||
@@ -56,7 +71,8 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
||||
return {
|
||||
filename: match[1],
|
||||
part: parseInt(match[2], 10),
|
||||
total: parseInt(match[3], 10)
|
||||
total: parseInt(match[3], 10),
|
||||
extension: match[4]
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
@@ -99,7 +115,7 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
||||
(value: any[], filename: string) => {
|
||||
return {
|
||||
path: filename,
|
||||
fakeName: `${filename}*.gguf`,
|
||||
fakeName: `${filename}*.${_.get(value, '[0].extension')}`,
|
||||
size: _.sumBy(value, 'size'),
|
||||
parts: value
|
||||
};
|
||||
@@ -109,19 +125,11 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
||||
return [...shardFileListResult, ...newGeneralFileList];
|
||||
}, []);
|
||||
|
||||
const handleFetchModelFiles = async () => {
|
||||
if (!props.repo) {
|
||||
setDataSource({ fileList: [], loading: false });
|
||||
handleSelectModelFile({});
|
||||
return;
|
||||
}
|
||||
axiosTokenRef.current?.abort?.();
|
||||
axiosTokenRef.current = new AbortController();
|
||||
setDataSource({ ...dataSource, loading: true });
|
||||
setCurrent('');
|
||||
// hugging face files
|
||||
const getHuggingfaceFiles = async () => {
|
||||
try {
|
||||
const res = await queryHuggingfaceModelFiles(
|
||||
{ repo: props.repo },
|
||||
{ repo: props.selectedModel.name || '' },
|
||||
{
|
||||
signal: axiosTokenRef.current.signal
|
||||
}
|
||||
@@ -131,9 +139,61 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
||||
});
|
||||
|
||||
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 sortList = _.sortBy(newList, (item: any) => {
|
||||
return sortType === 'size' ? item.size : item.path;
|
||||
@@ -183,10 +243,16 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
||||
handleSelectModelFile(item);
|
||||
}
|
||||
};
|
||||
useImperativeHandle(ref, () => ({
|
||||
fetchModelFiles: handleFetchModelFiles
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
handleFetchModelFiles();
|
||||
}, [props.repo]);
|
||||
if (!props.selectedModel.name) {
|
||||
setDataSource({ fileList: [], loading: false });
|
||||
handleSelectModelFile({});
|
||||
}
|
||||
}, [props.selectedModel.name]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -197,7 +263,7 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<TitleWrapper>
|
||||
<span>
|
||||
<span className="title">
|
||||
{intl.formatMessage({ id: 'models.available.files' })} (
|
||||
{dataSource.fileList.length || 0})
|
||||
</span>
|
||||
@@ -252,11 +318,9 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
||||
{getModelQuantizationType(item)}
|
||||
{item.parts && item.parts.length > 1 && (
|
||||
<Tooltip
|
||||
color="var(--color-white-1)"
|
||||
overlayInnerStyle={{
|
||||
width: 150,
|
||||
padding: 0,
|
||||
color: 'var(--ant-color-text-secondary)'
|
||||
width: 180,
|
||||
padding: 0
|
||||
}}
|
||||
title={
|
||||
<FileParts fileList={item.parts}></FileParts>
|
||||
@@ -299,6 +363,6 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
|
||||
</SimpleBar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(HFModelFile);
|
||||
|
||||
@@ -23,7 +23,12 @@ interface HFModelItemProps {
|
||||
source?: 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 intl = useIntl();
|
||||
@@ -57,7 +62,7 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="info">
|
||||
{props.source === modelSourceMap.huggingface_value ? (
|
||||
{SUPPORTEDSOURCE.includes(props.source || '') ? (
|
||||
<div className="info-item">
|
||||
{/* {props.task && (
|
||||
<Tag
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import DropdownButtons from '@/components/drop-down-buttons';
|
||||
import RowChildren from '@/components/seal-table/components/row-children';
|
||||
import SimpleTabel from '@/components/simple-table';
|
||||
@@ -23,6 +24,7 @@ interface InstanceItemProps {
|
||||
list: ModelInstanceListItem[];
|
||||
gpuDeviceList: GPUDeviceItem[];
|
||||
workerList: WorkerListItem[];
|
||||
modelData?: any;
|
||||
handleChildSelect: (
|
||||
val: string,
|
||||
item: ModelInstanceListItem,
|
||||
@@ -33,6 +35,7 @@ interface InstanceItemProps {
|
||||
const InstanceItem: React.FC<InstanceItemProps> = ({
|
||||
list,
|
||||
workerList,
|
||||
modelData,
|
||||
handleChildSelect
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
@@ -96,7 +99,11 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
|
||||
<div>{workerIp}</div>
|
||||
<div>
|
||||
{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>
|
||||
);
|
||||
@@ -119,7 +126,7 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
|
||||
worker_name: `${row.worker_name}`,
|
||||
worker_ip: `${row.worker_ip}`,
|
||||
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)'
|
||||
}}
|
||||
>
|
||||
<Tooltip title={renderWorkerInfo(item)}>
|
||||
<span className="m-r-5">{item.name}</span>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
<span className="flex-center">
|
||||
<AutoTooltip title={item.name} ghost>
|
||||
<span className="m-r-5">{item.name}</span>
|
||||
</AutoTooltip>
|
||||
<Tooltip title={renderWorkerInfo(item)}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<span
|
||||
@@ -243,9 +254,17 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
|
||||
: undefined
|
||||
}
|
||||
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],
|
||||
message: item.state_message
|
||||
message:
|
||||
item.state === InstanceStatusMap.Downloading &&
|
||||
item.download_progress === 100
|
||||
? ''
|
||||
: item.state_message
|
||||
}}
|
||||
></StatusTag>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import HighlightCode from '@/components/highlight-code';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import MarkdownViewer from '@/components/markdown-viewer';
|
||||
import useRequestToken from '@/hooks/use-request-token';
|
||||
import {
|
||||
DownOutlined,
|
||||
@@ -7,27 +7,38 @@ import {
|
||||
RightOutlined
|
||||
} from '@ant-design/icons';
|
||||
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 SimpleBar from 'simplebar-react';
|
||||
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 TitleWrapper from './title-wrapper';
|
||||
|
||||
const ModelCard: React.FC<{
|
||||
repo: string;
|
||||
onCollapse: (flag: boolean) => void;
|
||||
setIsGGUF: (flag: boolean) => void;
|
||||
selectedModel: any;
|
||||
collapsed: boolean;
|
||||
loadingModel?: boolean;
|
||||
modelSource: string;
|
||||
}> = (props) => {
|
||||
const { repo, onCollapse, collapsed, loadingModel } = props;
|
||||
const { onCollapse, setIsGGUF, collapsed, modelSource } = props;
|
||||
const intl = useIntl();
|
||||
const requestSource = useRequestToken();
|
||||
const [modelData, setModelData] = useState<any>({});
|
||||
const [modelData, setModelData] = useState<any>(null);
|
||||
const [readmeText, setReadmeText] = useState<string | null>(null);
|
||||
const requestToken = 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) => {
|
||||
try {
|
||||
@@ -49,38 +60,138 @@ const ModelCard: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const getModelCardData = async () => {
|
||||
if (!repo) {
|
||||
setModelData(null);
|
||||
return;
|
||||
}
|
||||
requestToken.current?.cancel?.();
|
||||
requestToken.current = requestSource();
|
||||
// huggingface model card data
|
||||
const getHuggingfaceModelDetail = async () => {
|
||||
try {
|
||||
const [modelcard, readme] = await Promise.all([
|
||||
queryHuggingfaceModelDetail(
|
||||
{ repo },
|
||||
{ repo: props.selectedModel.name },
|
||||
{
|
||||
token: requestToken.current.token
|
||||
}
|
||||
),
|
||||
loadFile(repo, 'main')
|
||||
loadFile(props.selectedModel.name, 'main')
|
||||
]);
|
||||
|
||||
setModelData(modelcard);
|
||||
setReadmeText(readme);
|
||||
const isGGUF = modelcard.tags?.includes('gguf');
|
||||
setIsGGUF(isGGUF);
|
||||
setIsGGUFModel(isGGUF);
|
||||
} 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(() => {
|
||||
onCollapse(!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(() => {
|
||||
getModelCardData();
|
||||
}, [repo]);
|
||||
}, [props.selectedModel.name]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!readmeText) {
|
||||
@@ -98,24 +209,13 @@ const ModelCard: React.FC<{
|
||||
return (
|
||||
<>
|
||||
<TitleWrapper>
|
||||
<div className="title">{modelData?.id} </div>
|
||||
{modelData?.id && (
|
||||
<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>
|
||||
)}
|
||||
<div className="title">{modelData?.id || modelData?.name} </div>
|
||||
{generateModelLink()}
|
||||
</TitleWrapper>
|
||||
<div className="card-wrapper">
|
||||
{modelData ? (
|
||||
<div className="model-card-wrap">
|
||||
<div className="flex-between flex-center">
|
||||
<div className="flex-center">
|
||||
{modelData.config?.model_type && (
|
||||
<Tag className="tag-item" color="gold">
|
||||
<span style={{ opacity: 0.65 }}>
|
||||
@@ -126,8 +226,13 @@ const ModelCard: React.FC<{
|
||||
</span>
|
||||
</Tag>
|
||||
)}
|
||||
{isGGUFModel && (
|
||||
<Tag className="tag-item" color="magenta">
|
||||
<span style={{ opacity: 0.65 }}>GGUF</span>
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
{readmeText && (
|
||||
{readmeText && isGGUFModel && (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
@@ -146,15 +251,19 @@ const ModelCard: React.FC<{
|
||||
</span>
|
||||
<SimpleBar
|
||||
style={{
|
||||
paddingTop: collapsed ? 12 : 0,
|
||||
maxHeight: collapsed ? 300 : 0
|
||||
}}
|
||||
>
|
||||
<HighlightCode
|
||||
code={readmeText}
|
||||
lang="markdown"
|
||||
copyable={false}
|
||||
<MarkdownViewer
|
||||
generateImgLink={
|
||||
modelSource === modelSourceMap.modelscope_value
|
||||
? generateModeScopeImgLink
|
||||
: undefined
|
||||
}
|
||||
content={readmeText}
|
||||
theme="light"
|
||||
></HighlightCode>
|
||||
></MarkdownViewer>
|
||||
</SimpleBar>
|
||||
</div>
|
||||
)}
|
||||
@@ -166,6 +275,26 @@ const ModelCard: React.FC<{
|
||||
></Empty>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import hotkeys from '@/config/hotkeys';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Input } from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { modelSourceMap, modelSourceValueMap } from '../config';
|
||||
|
||||
const SearchInput: React.FC<{
|
||||
modelSource: string;
|
||||
onSearch: (e: any) => void;
|
||||
}> = (props) => {
|
||||
const { onSearch } = props;
|
||||
const { onSearch, modelSource } = props;
|
||||
const intl = useIntl();
|
||||
const [isFocus, setIsFocus] = useState(false);
|
||||
const inputRef = useRef<any>(null);
|
||||
|
||||
useHotkeys(hotkeys.INPUT.join(','), () => {
|
||||
useHotkeys(hotkeys.FOCUS, (e: any) => {
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus?.();
|
||||
setIsFocus(true);
|
||||
});
|
||||
|
||||
return (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onPressEnter={onSearch}
|
||||
onFocus={() => setIsFocus(true)}
|
||||
onBlur={() => setIsFocus(false)}
|
||||
allowClear
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'model.deploy.search.placeholder'
|
||||
})}
|
||||
placeholder={intl.formatMessage(
|
||||
{
|
||||
id: 'model.deploy.search.placeholder'
|
||||
},
|
||||
{ source: modelSourceValueMap[modelSource] }
|
||||
)}
|
||||
prefix={
|
||||
<>
|
||||
<SearchOutlined
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
color: 'var(--ant-color-text-quaternary)'
|
||||
}}
|
||||
/>
|
||||
<IconFont
|
||||
className="font-size-16"
|
||||
type={
|
||||
modelSource === modelSourceMap.huggingface_value
|
||||
? 'icon-huggingface'
|
||||
: 'icon-tu2'
|
||||
}
|
||||
></IconFont>
|
||||
</>
|
||||
}
|
||||
></Input>
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { BulbOutlined } from '@ant-design/icons';
|
||||
import { BulbOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Input, Select } from 'antd';
|
||||
import { Checkbox, Select, Tooltip } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { queryHuggingfaceModels } from '../apis';
|
||||
import { ModelSortType, modelSourceMap, ollamaModelOptions } from '../config';
|
||||
import { queryHuggingfaceModels, queryModelScopeModels } from '../apis';
|
||||
import {
|
||||
ModelScopeSortType,
|
||||
ModelSortType,
|
||||
modelSourceMap,
|
||||
ollamaModelOptions
|
||||
} from '../config';
|
||||
import SearchStyle from '../style/search-result.less';
|
||||
import SearchInput from './search-input';
|
||||
import SearchResult from './search-result';
|
||||
@@ -17,7 +22,6 @@ interface SearchInputProps {
|
||||
}
|
||||
|
||||
const SearchModel: React.FC<SearchInputProps> = (props) => {
|
||||
console.log('SearchModel======');
|
||||
const intl = useIntl();
|
||||
const { modelSource, setLoadingModel, onSourceChange, onSelectModel } = props;
|
||||
const [dataSource, setDataSource] = useState<{
|
||||
@@ -31,11 +35,15 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
|
||||
networkError: false,
|
||||
sortType: ModelSortType.trendingScore
|
||||
});
|
||||
const SUPPORTEDSOURCE = [
|
||||
modelSourceMap.huggingface_value,
|
||||
modelSourceMap.modelscope_value
|
||||
];
|
||||
const [current, setCurrent] = useState<string>('');
|
||||
const cacheRepoOptions = useRef<any[]>([]);
|
||||
const axiosTokenRef = useRef<any>(null);
|
||||
const customOllamaModelRef = useRef<any>(null);
|
||||
const searchInputRef = useRef<any>('');
|
||||
const filterGGUFRef = useRef<boolean | undefined>();
|
||||
const modelFilesSortOptions = useRef<any[]>([
|
||||
{
|
||||
label: intl.formatMessage({ id: 'models.sort.trending' }),
|
||||
@@ -56,16 +64,76 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
|
||||
]);
|
||||
|
||||
const handleOnSelectModel = useCallback((item: any) => {
|
||||
console.log('handleOnSelectModel', item);
|
||||
onSelectModel(item);
|
||||
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(
|
||||
async (sortType?: string) => {
|
||||
if (modelSource === modelSourceMap.ollama_library_value) {
|
||||
if (!SUPPORTEDSOURCE.includes(modelSource)) {
|
||||
return;
|
||||
}
|
||||
console.log('handleOnSearchRepo', dataSource.loading);
|
||||
axiosTokenRef.current?.abort?.();
|
||||
axiosTokenRef.current = new AbortController();
|
||||
if (dataSource.loading) return;
|
||||
@@ -77,26 +145,12 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
|
||||
});
|
||||
setLoadingModel?.(true);
|
||||
cacheRepoOptions.current = [];
|
||||
const task: any = searchInputRef.current ? '' : 'text-generation';
|
||||
const params = {
|
||||
search: {
|
||||
query: searchInputRef.current || '',
|
||||
sort: 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
|
||||
};
|
||||
});
|
||||
|
||||
let list: any[] = [];
|
||||
if (modelSource === modelSourceMap.huggingface_value) {
|
||||
list = await getModelsFromHuggingface(sort);
|
||||
} else if (modelSource === modelSourceMap.modelscope_value) {
|
||||
list = await getModelsFromModelscope(sort);
|
||||
}
|
||||
cacheRepoOptions.current = list;
|
||||
setDataSource({
|
||||
repoOptions: list,
|
||||
@@ -133,7 +187,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
|
||||
if (
|
||||
!dataSource.repoOptions.length &&
|
||||
!cacheRepoOptions.current.length &&
|
||||
modelSource === modelSourceMap.huggingface_value
|
||||
SUPPORTEDSOURCE.includes(modelSource)
|
||||
) {
|
||||
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) => {
|
||||
handleOnSearchRepo(value || '');
|
||||
};
|
||||
|
||||
const handleFilterGGUFChange = (e: any) => {
|
||||
console.log('filterggufChange:', e.target.checked);
|
||||
filterGGUFRef.current = e.target.checked;
|
||||
handleOnSearchRepo();
|
||||
};
|
||||
|
||||
const renderHFSearch = () => {
|
||||
return (
|
||||
<>
|
||||
<SearchInput onSearch={handlerSearchModels}></SearchInput>
|
||||
<SearchInput
|
||||
onSearch={handlerSearchModels}
|
||||
modelSource={modelSource}
|
||||
></SearchInput>
|
||||
<div className={SearchStyle.filter}>
|
||||
<span>
|
||||
<span className="value">
|
||||
@@ -211,38 +229,45 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<Select
|
||||
allowClear
|
||||
value={dataSource.sortType}
|
||||
onChange={handleSortChange}
|
||||
labelRender={({ label }) => {
|
||||
return (
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'model.deploy.sort' })}: {label}
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
options={modelFilesSortOptions.current}
|
||||
size="middle"
|
||||
style={{ width: '150px' }}
|
||||
></Select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOllamaCustom = () => {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="Input ollama model name"
|
||||
onChange={handleInputChange}
|
||||
></Input>
|
||||
<div className={SearchStyle.filter}>
|
||||
<Button type="primary" onClick={handleConfirm}>
|
||||
{intl.formatMessage({ id: 'common.button.confirm' })}
|
||||
</Button>
|
||||
<span>
|
||||
<Checkbox
|
||||
onChange={handleFilterGGUFChange}
|
||||
className="m-r-5"
|
||||
checked={filterGGUFRef.current}
|
||||
>
|
||||
<Tooltip
|
||||
overlayInnerStyle={{ width: 'max-content' }}
|
||||
title={
|
||||
<div>
|
||||
<div>
|
||||
{intl.formatMessage({ id: 'models.search.gguf.tips' })}
|
||||
</div>
|
||||
<div>
|
||||
{intl.formatMessage({ id: 'models.search.vllm.tips' })}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
GGUF
|
||||
<InfoCircleOutlined className="m-l-4" />
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
<Select
|
||||
allowClear
|
||||
value={dataSource.sortType}
|
||||
onChange={handleSortChange}
|
||||
labelRender={({ label }) => {
|
||||
return (
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'model.deploy.sort' })}: {label}
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
options={modelFilesSortOptions.current}
|
||||
size="middle"
|
||||
style={{ width: '150px' }}
|
||||
></Select>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -262,7 +287,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
|
||||
return (
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className={SearchStyle['search-bar']}>
|
||||
{modelSource === modelSourceMap.huggingface_value ? (
|
||||
{SUPPORTEDSOURCE.includes(modelSource) ? (
|
||||
renderHFSearch()
|
||||
) : (
|
||||
<div style={{ lineHeight: '18px' }}>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button, Col, Empty, Row, Spin } from 'antd';
|
||||
import React from 'react';
|
||||
import SimpleBar from 'simplebar-react';
|
||||
import 'simplebar-react/dist/simplebar.min.css';
|
||||
import { modelSourceMap } from '../config';
|
||||
import '../style/search-result.less';
|
||||
import HFModelItem from './hf-model-item';
|
||||
|
||||
@@ -19,7 +20,6 @@ interface SearchResultProps {
|
||||
}
|
||||
|
||||
const SearchResult: React.FC<SearchResultProps> = (props) => {
|
||||
console.log('SearchResult======');
|
||||
const { resultList, onSelect, source, networkError } = props;
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -49,24 +49,26 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
|
||||
></IconFont>
|
||||
}
|
||||
description={
|
||||
<div className="flex-column gap-5">
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'models.search.networkerror' })}
|
||||
</span>
|
||||
<span>
|
||||
source === modelSourceMap.huggingface_value ? (
|
||||
<div className="flex-column gap-5">
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'models.search.hfvisit' })}
|
||||
{intl.formatMessage({ id: 'models.search.networkerror' })}
|
||||
</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
href="https://huggingface.co/"
|
||||
target="_blank"
|
||||
>
|
||||
Hugging Face
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'models.search.hfvisit' })}
|
||||
</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
href="https://huggingface.co/"
|
||||
target="_blank"
|
||||
>
|
||||
Hugging Face
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import DeleteModal from '@/components/delete-modal';
|
||||
import DropdownButtons from '@/components/drop-down-buttons';
|
||||
import IconFont from '@/components/icon-font';
|
||||
@@ -38,7 +39,7 @@ import {
|
||||
queryModelInstancesList,
|
||||
updateModel
|
||||
} from '../apis';
|
||||
import { modelSourceMap } from '../config';
|
||||
import { getSourceRepoConfigValue, modelSourceMap } from '../config';
|
||||
import { FormData, ListItem, ModelInstanceListItem } from '../config/types';
|
||||
import DeployModal from './deploy-modal';
|
||||
import InstanceItem from './instance-item';
|
||||
@@ -94,10 +95,7 @@ const Models: React.FC<ModelsProps> = ({
|
||||
width: 600,
|
||||
source: modelSourceMap.huggingface_value
|
||||
});
|
||||
const [title, setTitle] = useState<string>('');
|
||||
const [currentData, setCurrentData] = useState<ListItem | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [currentData, setCurrentData] = useState<ListItem>({} as ListItem);
|
||||
const [currentInstanceUrl, setCurrentInstanceUrl] = useState<string>('');
|
||||
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(
|
||||
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(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
console.log('data:', data, openDeployModal);
|
||||
const result = getSourceRepoConfigValue(currentData?.source, data);
|
||||
await updateModel({
|
||||
data: {
|
||||
...data
|
||||
...result.values,
|
||||
..._.omit(data, result.omits)
|
||||
},
|
||||
id: currentData?.id as number
|
||||
});
|
||||
@@ -236,7 +265,6 @@ const Models: React.FC<ModelsProps> = ({
|
||||
);
|
||||
|
||||
const handleModalCancel = useCallback(() => {
|
||||
console.log('handleModalCancel');
|
||||
setOpenAddModal(false);
|
||||
}, []);
|
||||
|
||||
@@ -252,7 +280,14 @@ const Models: React.FC<ModelsProps> = ({
|
||||
try {
|
||||
console.log('data:', data, openDeployModal);
|
||||
|
||||
await createModel({ data });
|
||||
const result = getSourceRepoConfigValue(openDeployModal.source, data);
|
||||
|
||||
await createModel({
|
||||
data: {
|
||||
...result.values,
|
||||
..._.omit(data, result.omits)
|
||||
}
|
||||
});
|
||||
setOpenDeployModal({
|
||||
...openDeployModal,
|
||||
show: false
|
||||
@@ -330,7 +365,6 @@ const Models: React.FC<ModelsProps> = ({
|
||||
const handleEdit = (row: ListItem) => {
|
||||
setCurrentData(row);
|
||||
setOpenAddModal(true);
|
||||
setTitle(intl.formatMessage({ id: 'models.title.edit' }));
|
||||
};
|
||||
|
||||
const handleSelect = useCallback((val: any, row: ListItem) => {
|
||||
@@ -367,10 +401,11 @@ const Models: React.FC<ModelsProps> = ({
|
||||
);
|
||||
|
||||
const renderChildren = useCallback(
|
||||
(list: any) => {
|
||||
(list: any, parent?: any) => {
|
||||
return (
|
||||
<InstanceItem
|
||||
list={list}
|
||||
modelData={parent}
|
||||
gpuDeviceList={gpuDeviceList}
|
||||
workerList={workerList}
|
||||
handleChildSelect={handleChildSelect}
|
||||
@@ -380,6 +415,16 @@ const Models: React.FC<ModelsProps> = ({
|
||||
[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(() => {
|
||||
setEmbeddingParams({
|
||||
params: {},
|
||||
@@ -472,15 +517,20 @@ const Models: React.FC<ModelsProps> = ({
|
||||
span={5}
|
||||
render={(text, record: ListItem) => {
|
||||
return (
|
||||
<span>
|
||||
<span className="m-r-5">{text}</span>
|
||||
<span>
|
||||
{record.embedding_only && (
|
||||
<Tag style={{ marginTop: 6 }} color="geekblue">
|
||||
<span
|
||||
className="flex-center flex-wrap"
|
||||
style={{ maxWidth: '100%' }}
|
||||
>
|
||||
<AutoTooltip ghost>
|
||||
<span className="m-r-5">{text}</span>
|
||||
</AutoTooltip>
|
||||
{record.embedding_only && (
|
||||
<span>
|
||||
<Tag style={{ margin: 0 }} color="geekblue">
|
||||
Embedding Only
|
||||
</Tag>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
@@ -493,11 +543,7 @@ const Models: React.FC<ModelsProps> = ({
|
||||
render={(text, record: ListItem) => {
|
||||
return (
|
||||
<span className="flex flex-column">
|
||||
<span>
|
||||
{record.source === modelSourceMap.huggingface_value
|
||||
? `${modelSourceMap.huggingface} / ${record.huggingface_filename}`
|
||||
: `${modelSourceMap.ollama_library} / ${record.ollama_library_model_name}`}
|
||||
</span>
|
||||
<span>{generateSource(record)}</span>
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
@@ -551,7 +597,7 @@ const Models: React.FC<ModelsProps> = ({
|
||||
<UpdateModel
|
||||
open={openAddModal}
|
||||
action={PageAction.EDIT}
|
||||
title={title}
|
||||
title={intl.formatMessage({ id: 'models.title.edit' })}
|
||||
data={currentData}
|
||||
onCancel={handleModalCancel}
|
||||
onOk={handleModalOk}
|
||||
|
||||
@@ -4,15 +4,18 @@ import SealInput from '@/components/seal-form/seal-input';
|
||||
import SealSelect from '@/components/seal-form/seal-select';
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { convertFileSize } from '@/utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form, Modal } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import SimpleBar from 'simplebar-react';
|
||||
import 'simplebar-react/dist/simplebar.min.css';
|
||||
import { queryGPUList, queryHuggingfaceModelFiles } from '../apis';
|
||||
import { modelSourceMap } from '../config';
|
||||
import { queryGPUList } from '../apis';
|
||||
import {
|
||||
backendOptionsMap,
|
||||
modelSourceMap,
|
||||
setSourceRepoConfigValue
|
||||
} from '../config';
|
||||
import { FormData, GPUListItem, ListItem } from '../config/types';
|
||||
import AdvanceConfig from './advance-config';
|
||||
|
||||
@@ -35,20 +38,27 @@ const sourceOptions = [
|
||||
label: 'Ollama Library',
|
||||
value: modelSourceMap.ollama_library_value,
|
||||
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) => {
|
||||
console.log('addmodel====');
|
||||
const { title, action, open, onOk, onCancel } = props || {};
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
const [gpuOptions, setGpuOptions] = useState<any[]>([]);
|
||||
const [isGGUF, setIsGGUF] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const modelSource = Form.useWatch('source', form);
|
||||
const [fileOptions, setFileOptions] = useState<
|
||||
{ label: string; value: string }[]
|
||||
>([]);
|
||||
|
||||
const getGPUList = async () => {
|
||||
const data = await queryGPUList();
|
||||
@@ -70,8 +80,14 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
|
||||
});
|
||||
}
|
||||
if (action === PageAction.EDIT && open) {
|
||||
const result = setSourceRepoConfigValue(
|
||||
props.data?.source || '',
|
||||
props.data
|
||||
);
|
||||
|
||||
form.setFieldsValue({
|
||||
...props.data,
|
||||
...result.values,
|
||||
..._.omit(props.data, result.omits),
|
||||
scheduleType: props.data?.gpu_selector ? 'manual' : 'auto',
|
||||
gpu_selector: props.data?.gpu_selector
|
||||
? `${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();
|
||||
}, [open]);
|
||||
|
||||
const fileNamLabel = (item: any) => {
|
||||
return (
|
||||
<span>
|
||||
{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);
|
||||
};
|
||||
useEffect(() => {
|
||||
setIsGGUF(props.data?.backend === backendOptionsMap.llamaBox);
|
||||
}, [props.data?.backend]);
|
||||
|
||||
const renderHuggingfaceFields = () => {
|
||||
return (
|
||||
<>
|
||||
<Form.Item<FormData>
|
||||
name="huggingface_repo_id"
|
||||
name="repo_id"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -145,30 +127,31 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
|
||||
disabled={true}
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="huggingface_filename"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{
|
||||
id: 'common.form.rule.input'
|
||||
},
|
||||
{ name: intl.formatMessage({ id: 'models.form.filename' }) }
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealAutoComplete
|
||||
filterOption
|
||||
label={intl.formatMessage({ id: 'models.form.filename' })}
|
||||
required
|
||||
options={fileOptions}
|
||||
loading={loading}
|
||||
onFocus={handleRepoOnBlur}
|
||||
disabled={action === PageAction.EDIT}
|
||||
></SealAutoComplete>
|
||||
</Form.Item>
|
||||
{isGGUF && (
|
||||
<Form.Item<FormData>
|
||||
name="file_name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{
|
||||
id: 'common.form.rule.input'
|
||||
},
|
||||
{ name: intl.formatMessage({ id: 'models.form.filename' }) }
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealAutoComplete
|
||||
filterOption
|
||||
label={intl.formatMessage({ id: 'models.form.filename' })}
|
||||
required
|
||||
options={[]}
|
||||
loading={loading}
|
||||
disabled={action === PageAction.EDIT}
|
||||
></SealAutoComplete>
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -229,18 +212,21 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderFieldsBySource = () => {
|
||||
switch (modelSource) {
|
||||
case modelSourceMap.huggingface_value:
|
||||
return renderHuggingfaceFields();
|
||||
case modelSourceMap.ollama_library_value:
|
||||
return renderOllamaModelFields();
|
||||
case modelSourceMap.s3_value:
|
||||
return renderS3Fields();
|
||||
default:
|
||||
return null;
|
||||
const renderFieldsBySource = useMemo(() => {
|
||||
if (SEARCH_SOURCE.includes(props.data?.source || '')) {
|
||||
return renderHuggingfaceFields();
|
||||
}
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
form.submit();
|
||||
@@ -367,7 +353,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
|
||||
></SealSelect>
|
||||
)}
|
||||
</Form.Item>
|
||||
{renderFieldsBySource()}
|
||||
{renderFieldsBySource}
|
||||
<Form.Item<FormData>
|
||||
name="replicas"
|
||||
rules={[
|
||||
@@ -401,7 +387,14 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
|
||||
></SealInput.TextArea>
|
||||
</Form.Item>
|
||||
|
||||
<AdvanceConfig form={form} gpuOptions={gpuOptions}></AdvanceConfig>
|
||||
<AdvanceConfig
|
||||
form={form}
|
||||
gpuOptions={gpuOptions}
|
||||
action={PageAction.EDIT}
|
||||
isGGUF={
|
||||
form.getFieldValue('backend') === backendOptionsMap.llamaBox
|
||||
}
|
||||
></AdvanceConfig>
|
||||
</Form>
|
||||
</SimpleBar>
|
||||
</Modal>
|
||||
|
||||
@@ -3,24 +3,24 @@ import { EditOutlined } from '@ant-design/icons';
|
||||
|
||||
export const ollamaModelOptions = [
|
||||
{
|
||||
label: 'llama3.1',
|
||||
value: 'llama3.1',
|
||||
name: 'llama3.1',
|
||||
id: 'llama3.1',
|
||||
tags: ['8B', '70B', '405B']
|
||||
label: 'llama3.2',
|
||||
value: 'llama3.2',
|
||||
name: 'llama3.2',
|
||||
id: 'llama3.2',
|
||||
tags: ['Tools', '1B', '3B']
|
||||
},
|
||||
{
|
||||
label: 'llama3',
|
||||
value: 'llama3',
|
||||
name: 'llama3',
|
||||
tags: ['8B', '70B'],
|
||||
id: 'llama3'
|
||||
label: 'qwen2.5',
|
||||
value: 'qwen2.5',
|
||||
name: 'qwen2.5',
|
||||
tags: ['Tools', '0.5B', '1.5B', '3B', '7B', '14B', '32B', '72B'],
|
||||
id: 'gemma2'
|
||||
},
|
||||
{
|
||||
label: 'gemma2',
|
||||
value: 'gemma2',
|
||||
name: 'gemma2',
|
||||
tags: ['9B', '27B'],
|
||||
tags: ['2B', '9B', '27B'],
|
||||
id: 'gemma2'
|
||||
},
|
||||
{
|
||||
@@ -44,20 +44,6 @@ export const ollamaModelOptions = [
|
||||
tags: ['7B'],
|
||||
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',
|
||||
value: 'phi3.5',
|
||||
@@ -81,13 +67,27 @@ export const ollamaModelOptions = [
|
||||
}
|
||||
];
|
||||
|
||||
export const backendOptionsMap = {
|
||||
llamaBox: 'llama-box',
|
||||
vllm: 'vllm'
|
||||
};
|
||||
|
||||
export const modelSourceMap: Record<string, string> = {
|
||||
huggingface: 'Hugging Face',
|
||||
ollama_library: 'Ollama Library',
|
||||
s3: 'S3',
|
||||
huggingface_value: 'huggingface',
|
||||
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 = {
|
||||
@@ -148,6 +148,13 @@ export const ModelSortType = {
|
||||
lastModified: 'lastModified'
|
||||
};
|
||||
|
||||
export const ModelScopeSortType = {
|
||||
[ModelSortType.trendingScore]: 'Default',
|
||||
[ModelSortType.likes]: 'StarsCount',
|
||||
[ModelSortType.downloads]: 'DownloadsCount',
|
||||
[ModelSortType.lastModified]: 'GmtModified'
|
||||
};
|
||||
|
||||
export const placementStrategyOptions = [
|
||||
{
|
||||
label: 'Spread',
|
||||
@@ -158,3 +165,80 @@ export const placementStrategyOptions = [
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -1,9 +1,12 @@
|
||||
export interface ListItem {
|
||||
source: string;
|
||||
backend: string;
|
||||
huggingface_repo_id: string;
|
||||
huggingface_file_name: string;
|
||||
huggingface_filename: string;
|
||||
ollama_library_model_name: string;
|
||||
model_scope_file_path: string;
|
||||
model_scope_model_id: string;
|
||||
embedding_only?: boolean;
|
||||
ready_replicas: number;
|
||||
replicas: number;
|
||||
@@ -22,12 +25,18 @@ export interface ListItem {
|
||||
}
|
||||
|
||||
export interface FormData {
|
||||
backend?: string;
|
||||
backend_parameters?: string[];
|
||||
source: string;
|
||||
repo_id: string;
|
||||
file_name: string;
|
||||
huggingface_repo_id: string;
|
||||
huggingface_filename: string;
|
||||
s3_address: string;
|
||||
ollama_library_model_name: 'string';
|
||||
ollama_library_model_name: string;
|
||||
distributed_inference_across_workers?: boolean;
|
||||
model_scope_model_id?: string;
|
||||
model_scope_file_path?: string;
|
||||
gpu_selector?: {
|
||||
worker_name: string;
|
||||
gpu_index: number;
|
||||
@@ -77,8 +86,8 @@ export interface ModelInstanceFormData {
|
||||
model_id: number;
|
||||
model_name: string;
|
||||
source: string;
|
||||
huggingface_repo_id: 'string';
|
||||
huggingface_filename: 'string';
|
||||
huggingface_repo_id: string;
|
||||
huggingface_filename: string;
|
||||
}
|
||||
|
||||
export interface GPUListItem {
|
||||
|
||||
@@ -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;
|
||||
@@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
.simplebar-scrollbar::before {
|
||||
background: rgba(0, 0, 0, 50%);
|
||||
width: var(--scrollbar-size);
|
||||
}
|
||||
|
||||
.column-wrapper-footer {
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
font-size: 12px;
|
||||
height: 22px;
|
||||
opacity: 0.7;
|
||||
// border: 1px solid var(--ant-color-border);
|
||||
// color: var(--ant-color-text-secondary);
|
||||
}
|
||||
|
||||
.btn {
|
||||
|
||||
@@ -8,10 +8,6 @@
|
||||
// height: calc(100vh - 194px);
|
||||
}
|
||||
|
||||
.simplebar-scrollbar::before {
|
||||
background: rgba(0, 0, 0, 50%);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
position: absolute;
|
||||
border-radius: 0 2px 0 0;
|
||||
top: 10px;
|
||||
left: -10px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
left: -13px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid var(--ant-color-split);
|
||||
transform: rotate(45deg);
|
||||
border-left: none;
|
||||
border-bottom: none;
|
||||
background-color: var(--color-white-1);
|
||||
z-index: 100;
|
||||
clip-path: polygon(0 -1px, 24px 0, 24px 23px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,10 @@
|
||||
padding: @padding;
|
||||
padding-top: 10px;
|
||||
margin-bottom: 0;
|
||||
font-weight: var(--font-weight-bold);
|
||||
background-color: var(--color-white-1);
|
||||
|
||||
.title {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
:local(.login-form-wrapper) {
|
||||
margin: 0 auto;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--border-radius-modal);
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
padding: 32px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import HotKeys from '@/config/hotkeys';
|
||||
import { platformCall } from '@/utils';
|
||||
import { DeleteOutlined, EnterOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { ClearOutlined, EnterOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Col, Row, Space } from 'antd';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
@@ -64,7 +64,7 @@ const ChatFooter: React.FC<ChatFooterProps> = (props) => {
|
||||
{intl.formatMessage({ id: 'playground.newMessage' })}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DeleteOutlined></DeleteOutlined>}
|
||||
icon={<ClearOutlined />}
|
||||
onClick={onClear}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user