Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2df7c17bc1 | ||
|
|
a274349ace | ||
|
|
1c699fe307 | ||
|
|
f0a3e84e4f | ||
|
|
f87989d32e | ||
|
|
7355be9e3a | ||
|
|
d8f1e95e1a | ||
|
|
59eace702b | ||
|
|
8a24ba0252 | ||
|
|
03999a9d2c | ||
|
|
f335cb1775 | ||
|
|
b9c034469b | ||
|
|
ce3a82ddda | ||
|
|
5cbb38922c | ||
|
|
14f0afec08 | ||
|
|
769cc09906 | ||
|
|
306daf7591 | ||
|
|
d1c1132e6e | ||
|
|
962df859f1 | ||
|
|
97601280d1 | ||
|
|
ede42e99de | ||
|
|
47cae0134c | ||
|
|
33defab7df | ||
|
|
6b402e944b | ||
|
|
f8a6f4c5e5 | ||
|
|
adb999d3a9 |
@@ -4,7 +4,6 @@ import { compressionPluginConfig, monacoPluginConfig } from './plugins';
|
||||
import proxy from './proxy';
|
||||
import routes from './routes';
|
||||
import { getBranchInfo } from './utils';
|
||||
const CompressionWebpackPlugin = require('compression-webpack-plugin');
|
||||
|
||||
const versionInfo = getBranchInfo();
|
||||
process.env.VERSION = JSON.stringify(versionInfo);
|
||||
|
||||
+7
-2
@@ -79,10 +79,15 @@ export async function getInitialState(): Promise<{
|
||||
const getAppVersionInfo = async () => {
|
||||
try {
|
||||
const data = await queryVersionInfo();
|
||||
const isProduction = data.version?.indexOf('0.0.0') === -1;
|
||||
|
||||
const isDev = data.version?.indexOf('0.0.0') > -1;
|
||||
const isRc = data.version?.indexOf('rc') > -1;
|
||||
|
||||
setAtomStorage(GPUStackVersionAtom, {
|
||||
...data,
|
||||
isProduction
|
||||
isProd: !isDev && !isRc,
|
||||
isDev,
|
||||
isRc
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('queryVersionInfo error', error);
|
||||
|
||||
@@ -29,6 +29,7 @@ export const regionOSImageListAtom = atom<
|
||||
{
|
||||
label: string;
|
||||
value: string;
|
||||
os_image: string;
|
||||
name: string;
|
||||
description: string;
|
||||
vendor: string;
|
||||
@@ -60,4 +61,5 @@ export const fromClusterCreationAtom = atom(false);
|
||||
*/
|
||||
export const clusterSessionAtom = atom<{
|
||||
firstAddWorker: boolean;
|
||||
firstAddCluster: boolean;
|
||||
} | null>(null);
|
||||
|
||||
+6
-2
@@ -6,11 +6,15 @@ export const userAtom = atomWithStorage<any>('userInfo', null);
|
||||
export const GPUStackVersionAtom = atom<{
|
||||
version: string;
|
||||
git_commit: string;
|
||||
isProduction: boolean;
|
||||
isProd: boolean;
|
||||
isDev?: boolean;
|
||||
isRc?: boolean;
|
||||
}>({
|
||||
version: '',
|
||||
git_commit: '',
|
||||
isProduction: false
|
||||
isProd: false,
|
||||
isDev: false,
|
||||
isRc: false
|
||||
});
|
||||
|
||||
export const UpdateCheckAtom = atom<{
|
||||
|
||||
@@ -70,8 +70,9 @@ const AutoImage: React.FC<
|
||||
setIsError(false);
|
||||
}, [props.onLoad]);
|
||||
|
||||
const handleOnError = useCallback(() => {
|
||||
const handleOnError = useCallback((e: any) => {
|
||||
setIsError(true);
|
||||
e.target.src = fallbackImg;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -127,4 +128,4 @@ const AutoImage: React.FC<
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AutoImage);
|
||||
export default AutoImage;
|
||||
|
||||
@@ -8,13 +8,14 @@ interface HintInputProps {
|
||||
onChange: (value: string) => void;
|
||||
onBlur?: (e: any) => void;
|
||||
placeholder?: string;
|
||||
trim?: boolean;
|
||||
sourceOptions?: Global.HintOptions[];
|
||||
}
|
||||
|
||||
const matchReg = /[^=]+=[^=]*$/;
|
||||
|
||||
const HintInput: React.FC<HintInputProps> = (props) => {
|
||||
const { value, label, onChange, onBlur, sourceOptions } = props;
|
||||
const { value, label, onChange, onBlur, sourceOptions, trim = true } = props;
|
||||
const cursorPosRef = React.useRef(0);
|
||||
const contextBeforeCursorRef = React.useRef('');
|
||||
const [options, setOptions] = React.useState<
|
||||
@@ -70,11 +71,7 @@ const HintInput: React.FC<HintInputProps> = (props) => {
|
||||
|
||||
const handleInput = (e: any) => {
|
||||
getContextBeforeCursor(e);
|
||||
onChange(e.target.value?.trim());
|
||||
};
|
||||
|
||||
const handleOnChange = (value: string) => {
|
||||
onChange(value?.trim());
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
||||
const handleOnSelect = (value: string) => {
|
||||
@@ -93,9 +90,10 @@ const HintInput: React.FC<HintInputProps> = (props) => {
|
||||
onBlur={onBlur}
|
||||
label={label}
|
||||
options={options}
|
||||
trim={trim}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(HintInput);
|
||||
export default HintInput;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useIntl } from '@umijs/max';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import Wrapper from '../label-selector/wrapper';
|
||||
@@ -12,13 +11,13 @@ interface ListInputProps {
|
||||
options?: Global.HintOptions[];
|
||||
placeholder?: string;
|
||||
labelExtra?: React.ReactNode;
|
||||
trim?: boolean;
|
||||
onChange: (data: string[]) => void;
|
||||
onBlur?: (e: any, index: number) => void;
|
||||
onDelete?: (index: number) => void;
|
||||
}
|
||||
|
||||
const ListInput: React.FC<ListInputProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
dataList,
|
||||
label,
|
||||
@@ -28,7 +27,8 @@ const ListInput: React.FC<ListInputProps> = (props) => {
|
||||
onDelete,
|
||||
btnText,
|
||||
options,
|
||||
labelExtra
|
||||
labelExtra,
|
||||
trim = true
|
||||
} = props;
|
||||
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
|
||||
const countRef = React.useRef(0);
|
||||
@@ -97,6 +97,7 @@ const ListInput: React.FC<ListInputProps> = (props) => {
|
||||
onBlur={(e) => onBlur?.(e, index)}
|
||||
onRemove={() => handleOnRemove(index)}
|
||||
onChange={(val) => handleOnChange(val, index)}
|
||||
trim={trim}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -13,10 +13,19 @@ interface LabelItemProps {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
options?: Global.HintOptions[];
|
||||
trim?: boolean;
|
||||
}
|
||||
|
||||
const ListItem: React.FC<LabelItemProps> = (props) => {
|
||||
const { onRemove, onChange, onBlur, label, value, options } = props;
|
||||
const {
|
||||
onRemove,
|
||||
onChange,
|
||||
onBlur,
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
trim = true
|
||||
} = props;
|
||||
|
||||
const handleOnChange = (value: any) => {
|
||||
onChange(value);
|
||||
@@ -30,6 +39,7 @@ const ListItem: React.FC<LabelItemProps> = (props) => {
|
||||
onBlur={onBlur}
|
||||
label={label}
|
||||
sourceOptions={options}
|
||||
trim={trim}
|
||||
placeholder={props.placeholder}
|
||||
/>
|
||||
<Button
|
||||
@@ -44,4 +54,4 @@ const ListItem: React.FC<LabelItemProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ListItem);
|
||||
export default ListItem;
|
||||
|
||||
@@ -64,9 +64,9 @@ const LogsList: React.FC<LogsListProps> = forwardRef((props, ref) => {
|
||||
|
||||
const handleOnWheel = useCallback(
|
||||
(e: any) => {
|
||||
const scrollTop = scrollEventElement?.scrollTop;
|
||||
const scrollHeight = scrollEventElement?.scrollHeight;
|
||||
const clientHeight = scrollEventElement?.clientHeight;
|
||||
const scrollTop = scrollEventElement?.current.scrollTop;
|
||||
const scrollHeight = scrollEventElement?.current.scrollHeight;
|
||||
const clientHeight = scrollEventElement?.current.clientHeight;
|
||||
|
||||
stopScroll.current = scrollTop + clientHeight <= scrollHeight;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import useSetChunkFetch from '@/hooks/use-chunk-fetch';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Spin } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
@@ -177,7 +178,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnScroll = useCallback(
|
||||
const handleOnScroll = useMemoizedFn(
|
||||
async (data: { isTop: boolean; isBottom: boolean }) => {
|
||||
const { isTop, isBottom } = data;
|
||||
setIsAtTop(isTop);
|
||||
@@ -225,17 +226,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
|
||||
} else if (isBottom && page < totalPage) {
|
||||
// getNextPage();
|
||||
}
|
||||
},
|
||||
[
|
||||
loading,
|
||||
logs.length,
|
||||
pageSize,
|
||||
enableScorllLoad,
|
||||
page,
|
||||
totalPage,
|
||||
setScrollPos,
|
||||
createChunkConnection
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
const debouncedScroll = useCallback(
|
||||
|
||||
@@ -90,6 +90,14 @@ const SealAutoComplete: React.FC<
|
||||
const handleOnSelect = (value: any, option: any) => {
|
||||
onSelect?.(value, option);
|
||||
};
|
||||
|
||||
const handleOnInput = (e: any) => {
|
||||
if (trim) {
|
||||
e.target.value = e.target.value?.trim();
|
||||
}
|
||||
props.onInput?.(e);
|
||||
};
|
||||
|
||||
const renderAfter = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -123,6 +131,7 @@ const SealAutoComplete: React.FC<
|
||||
>
|
||||
<AutoComplete
|
||||
{...rest}
|
||||
trim={trim}
|
||||
ref={inputRef}
|
||||
placeholder={
|
||||
isFocus || !label ? (
|
||||
@@ -141,6 +150,7 @@ const SealAutoComplete: React.FC<
|
||||
onSearch={handleSearch}
|
||||
onChange={handleChange}
|
||||
popupRender={popupRender}
|
||||
onInput={handleOnInput}
|
||||
></AutoComplete>
|
||||
</Wrapper>
|
||||
</SelectWrapper>
|
||||
|
||||
@@ -2,52 +2,68 @@ import IconFont from '@/components/icon-font';
|
||||
import type { SelectProps } from 'antd';
|
||||
import { Select } from 'antd';
|
||||
import React, { forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import NotFoundContent from '../components/not-found-content';
|
||||
|
||||
const BaseSelect: React.FC<SelectProps & { ref?: any }> = forwardRef(
|
||||
(props, ref) => {
|
||||
const { notFoundContent, loading, ...restProps } = props;
|
||||
const [isFocus, setIsFocus] = React.useState(false);
|
||||
const inputRef = React.useRef<any>(null);
|
||||
const Footer = styled.div`
|
||||
color: var(--ant-color-text-tertiary);
|
||||
margin-top: 8px;
|
||||
margin-bottom: 0;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--ant-color-split);
|
||||
`;
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
...(inputRef.current || ({} as any))
|
||||
}));
|
||||
const BaseSelect: React.FC<
|
||||
SelectProps & { ref?: any; footer?: React.ReactNode }
|
||||
> = forwardRef((props, ref) => {
|
||||
const { notFoundContent, loading, ...restProps } = props;
|
||||
const [isFocus, setIsFocus] = React.useState(false);
|
||||
const inputRef = React.useRef<any>(null);
|
||||
|
||||
const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||
setIsFocus(true);
|
||||
props.onFocus?.(e);
|
||||
};
|
||||
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||
setIsFocus(false);
|
||||
props.onBlur?.(e);
|
||||
};
|
||||
const renderSuffixIcon = () => {
|
||||
if (props.suffixIcon) {
|
||||
return props.suffixIcon;
|
||||
useImperativeHandle(ref, () => ({
|
||||
...(inputRef.current || ({} as any))
|
||||
}));
|
||||
|
||||
const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||
setIsFocus(true);
|
||||
props.onFocus?.(e);
|
||||
};
|
||||
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||
setIsFocus(false);
|
||||
props.onBlur?.(e);
|
||||
};
|
||||
const renderSuffixIcon = () => {
|
||||
if (props.suffixIcon) {
|
||||
return props.suffixIcon;
|
||||
}
|
||||
if (!props.showSearch) {
|
||||
return <IconFont type="icon-down"></IconFont>;
|
||||
}
|
||||
return !isFocus ? <IconFont type="icon-down"></IconFont> : undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...restProps}
|
||||
notFoundContent={
|
||||
<NotFoundContent loading={loading} notFoundContent={notFoundContent} />
|
||||
}
|
||||
if (!props.showSearch) {
|
||||
return <IconFont type="icon-down"></IconFont>;
|
||||
ref={inputRef}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
suffixIcon={renderSuffixIcon()}
|
||||
popupRender={
|
||||
props.footer
|
||||
? (originNode) => (
|
||||
<>
|
||||
{originNode}
|
||||
{props.footer && <Footer>{props.footer}</Footer>}
|
||||
</>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
return !isFocus ? <IconFont type="icon-down"></IconFont> : undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...restProps}
|
||||
notFoundContent={
|
||||
<NotFoundContent
|
||||
loading={loading}
|
||||
notFoundContent={notFoundContent}
|
||||
/>
|
||||
}
|
||||
ref={inputRef}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
suffixIcon={renderSuffixIcon()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default BaseSelect;
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
|
||||
.note-info {
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.star {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const NoteInfo: React.FC<NoteInfoProps> = (props) => {
|
||||
if (!label) return null;
|
||||
const renderRequiredStar = required ? (
|
||||
<span className="star" style={{ color: 'red' }}>
|
||||
*
|
||||
﹡
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ const SealPassword: React.FC<InputProps & SealFormItemProps> = (props) => {
|
||||
required={required}
|
||||
description={description}
|
||||
disabled={props.disabled}
|
||||
labelExtra={props.labelExtra}
|
||||
hasPrefix={!!props.prefix}
|
||||
onClick={handleClickWrapper}
|
||||
>
|
||||
|
||||
@@ -10,7 +10,9 @@ import { SealFormItemProps } from './types';
|
||||
import Wrapper from './wrapper';
|
||||
import SelectWrapper from './wrapper/select';
|
||||
|
||||
const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
|
||||
const SealSelect: React.FC<
|
||||
SelectProps & SealFormItemProps & { footer?: React.ReactNode }
|
||||
> = (props) => {
|
||||
const {
|
||||
label,
|
||||
placeholder,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import breakpoints from '@/config/breakpoints';
|
||||
import InfiniteScroller from '@/pages/_components/infinite-scroller';
|
||||
import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context';
|
||||
import { Col, FloatButton, Row, Spin } from 'antd';
|
||||
import { Col, Row, Spin } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import ResizeObserver from 'rc-resize-observer';
|
||||
import React, { useCallback } from 'react';
|
||||
@@ -122,7 +122,6 @@ const CardList: React.FC<CatalogListProps> = (props) => {
|
||||
/>
|
||||
</InfiniteScroller>
|
||||
</ResizeObserver>
|
||||
<FloatButton.BackTop visibilityHeight={1000} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -87,4 +87,4 @@ const UploadAudio: React.FC<UploadAudioProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UploadAudio);
|
||||
export default UploadAudio;
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import Logo from '@/assets/images/gpustack-logo.png';
|
||||
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||
import { getAtomStorage } from '@/atoms/utils';
|
||||
import externalLinks from '@/constants/external-links';
|
||||
import { Button } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import React from 'react';
|
||||
import './index.less';
|
||||
|
||||
const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
||||
const latestVersion = getAtomStorage(UpdateCheckAtom).latest_version;
|
||||
const currentVersion = getAtomStorage(GPUStackVersionAtom)?.version;
|
||||
const [gpuStackVersionAtom] = useAtom(GPUStackVersionAtom);
|
||||
const [userDataAtom] = useAtom(userAtom);
|
||||
const [updateCheck] = useAtom(UpdateCheckAtom);
|
||||
|
||||
const isProd =
|
||||
currentVersion?.indexOf('rc') === -1 &&
|
||||
currentVersion?.indexOf('0.0.0') === -1;
|
||||
// current version info
|
||||
const {
|
||||
version: currentVersion,
|
||||
git_commit,
|
||||
isProd,
|
||||
isDev
|
||||
} = gpuStackVersionAtom;
|
||||
|
||||
// user info
|
||||
const { is_admin } = userDataAtom || {};
|
||||
|
||||
// update check latest version info
|
||||
const { latest_version: latestVersion } = updateCheck;
|
||||
|
||||
const uiVersion = document.documentElement.getAttribute('data-version');
|
||||
|
||||
@@ -29,10 +40,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
||||
</span>
|
||||
)}
|
||||
{isProd ? (
|
||||
<span className="val">
|
||||
{getAtomStorage(GPUStackVersionAtom)?.version ||
|
||||
getAtomStorage(GPUStackVersionAtom)?.git_commit}
|
||||
</span>
|
||||
<span className="val">{currentVersion || git_commit}</span>
|
||||
) : (
|
||||
<span className="val dev">
|
||||
<span className="item">
|
||||
@@ -40,9 +48,7 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
||||
{' '}
|
||||
{intl.formatMessage({ id: 'common.footer.version.server' })}
|
||||
</span>
|
||||
{currentVersion.indexOf('0.0.0') > -1
|
||||
? getAtomStorage(GPUStackVersionAtom)?.git_commit
|
||||
: getAtomStorage(GPUStackVersionAtom)?.version}
|
||||
{isDev ? git_commit : currentVersion}
|
||||
</span>
|
||||
<span className="item">
|
||||
<span className="tl">UI</span>
|
||||
@@ -51,12 +57,10 @@ const VersionInfo: React.FC<{ intl: any }> = ({ intl }) => {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{getAtomStorage(userAtom)?.is_admin && isProd && (
|
||||
{is_admin && isProd && (
|
||||
<div className="upgrade-text">
|
||||
<span className="m-l-5">
|
||||
{latestVersion &&
|
||||
latestVersion !== currentVersion &&
|
||||
latestVersion.indexOf('0.0.0') === -1
|
||||
{latestVersion && latestVersion !== currentVersion && !isDev
|
||||
? intl.formatMessage(
|
||||
{ id: 'users.version.update' },
|
||||
{ version: latestVersion }
|
||||
|
||||
Vendored
+1
-1
@@ -23,7 +23,7 @@ declare namespace Global {
|
||||
require_password_change: boolean;
|
||||
id: number;
|
||||
source: string;
|
||||
avatar: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
type EmptyObject = Record<never, never>;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ export const HEADER_HEIGHT = 56;
|
||||
|
||||
export const DEFAULT_ENTER_PAGE = {
|
||||
adminForNormal: '/dashboard',
|
||||
adminForFirst: '/models/deployments',
|
||||
adminForFirst: '/resources/workers',
|
||||
user: '/models/user-models',
|
||||
login: '/login'
|
||||
};
|
||||
|
||||
@@ -122,12 +122,12 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
initialState?.currentUser?.is_admin &&
|
||||
updateCheck.latest_version &&
|
||||
updateCheck.latest_version !== version?.version &&
|
||||
updateCheck.latest_version?.indexOf('0.0.0') === -1 &&
|
||||
updateCheck.latest_version?.indexOf('rc') === -1
|
||||
version?.isProd
|
||||
);
|
||||
}, [
|
||||
updateCheck.latest_version,
|
||||
version.version,
|
||||
version.isProd,
|
||||
initialState?.currentUser?.is_admin
|
||||
]);
|
||||
|
||||
@@ -272,7 +272,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
<Avatar
|
||||
size={24}
|
||||
style={{ ...avatarStyle }}
|
||||
src={initialState?.currentUser?.avatar}
|
||||
src={initialState?.currentUser?.avatar_url}
|
||||
icon={
|
||||
<IconFont type="icon-user-filled" className="font-size-24" />
|
||||
}
|
||||
@@ -322,7 +322,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
||||
<Avatar
|
||||
size={24}
|
||||
style={{ ...avatarStyle }}
|
||||
src={initialState?.currentUser?.avatar}
|
||||
src={initialState?.currentUser?.avatar_url}
|
||||
icon={<IconFont type="icon-user-filled" className="font-size-24" />}
|
||||
/>
|
||||
</IconWrapper>
|
||||
|
||||
+18
-221
@@ -1,16 +1,11 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { routeCacheAtom, setRouteCache } from '@/atoms/route-cache';
|
||||
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||
import { userAtom } from '@/atoms/user';
|
||||
import DarkMask from '@/components/dark-mask';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import ShortCuts, {
|
||||
modalConfig as ShortCutsConfig
|
||||
} from '@/components/short-cuts';
|
||||
import VersionInfo, { modalConfig } from '@/components/version-info';
|
||||
import routeCachekey from '@/config/route-cachekey';
|
||||
import { DEFAULT_ENTER_PAGE } from '@/config/settings';
|
||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||
import useOverlayScroller from '@/hooks/use-overlay-scroller';
|
||||
import useUserSettings from '@/hooks/use-user-settings';
|
||||
import { logout } from '@/pages/login/apis';
|
||||
@@ -18,7 +13,6 @@ import { useAccessMarkedRoutes } from '@@/plugin-access';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { ProLayout } from '@ant-design/pro-components';
|
||||
import {
|
||||
Link,
|
||||
Outlet,
|
||||
dropByCacheKey,
|
||||
history,
|
||||
@@ -29,18 +23,17 @@ import {
|
||||
useNavigate,
|
||||
type IRoute
|
||||
} from '@umijs/max';
|
||||
import { Button, ConfigProvider, Modal, Tooltip, theme } from 'antd';
|
||||
import { Button, ConfigProvider, Modal, theme } from 'antd';
|
||||
import 'driver.js/dist/driver.css';
|
||||
import { useAtom } from 'jotai';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { PageContainerInner } from '../pages/_components/page-box';
|
||||
import Exception from './Exception';
|
||||
import './Layout.css';
|
||||
import { LogoIcon, SLogoIcon } from './Logo';
|
||||
import ErrorBoundary from './error-boundary';
|
||||
import { ExtraContent } from './extraRender';
|
||||
import { getRightRenderContent } from './rightRender';
|
||||
import { patchRoutes } from './runtime';
|
||||
import SiderMenu from './sider-menu';
|
||||
|
||||
@@ -57,16 +50,11 @@ const NO_CONTAINER_PAGES = [
|
||||
|
||||
const loginPath = DEFAULT_ENTER_PAGE.login;
|
||||
|
||||
type InitialStateType = {
|
||||
fetchUserInfo: () => Promise<Global.UserInfo>;
|
||||
currentUser?: Global.UserInfo;
|
||||
};
|
||||
|
||||
// Filter out the routes that need to be displayed, where filterFn indicates the levels that should not be shown
|
||||
const filterRoutes = (
|
||||
routes: IRoute[],
|
||||
filterFn: (route: IRoute) => boolean
|
||||
) => {
|
||||
): any[] => {
|
||||
if (routes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -117,21 +105,13 @@ export default (props: any) => {
|
||||
defer: false
|
||||
});
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const { themeData, setTheme, setUserSettings, userSettings, isDarkTheme } =
|
||||
useUserSettings();
|
||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||
const { initialize: initializeMenu } = useOverlayScroller();
|
||||
const { themeData, setUserSettings, userSettings } = useUserSettings();
|
||||
const [userInfo] = useAtom(userAtom);
|
||||
const [routeCache] = useAtom(routeCacheAtom);
|
||||
const [version] = useAtom(GPUStackVersionAtom);
|
||||
const [updateCheck] = useAtom(UpdateCheckAtom);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const intl = useIntl();
|
||||
const { clientRoutes, pluginManager } = useAppData();
|
||||
// const [collapsed, setCollapsed] = useState(userSettings.collapsed || false);
|
||||
const [collapseValue, setCollapseValue] = useState(false);
|
||||
const [collapseKeys, setCollapseKeys] = useState<Set<string>>(new Set());
|
||||
const { clientRoutes } = useAppData();
|
||||
|
||||
const initialInfo = (useModel && useModel('@@initialState')) || {
|
||||
initialState: undefined,
|
||||
@@ -150,23 +130,6 @@ export default (props: any) => {
|
||||
return intl.formatMessage({ id: args.id });
|
||||
};
|
||||
|
||||
const showVersion = () => {
|
||||
saveScrollHeight();
|
||||
modal.info({
|
||||
...modalConfig,
|
||||
width: 460,
|
||||
content: <VersionInfo intl={intl} />,
|
||||
onCancel: restoreScrollHeight
|
||||
});
|
||||
};
|
||||
|
||||
const showShortcuts = () => {
|
||||
Modal.info({
|
||||
...ShortCutsConfig,
|
||||
content: <ShortCuts intl={intl} />
|
||||
});
|
||||
};
|
||||
|
||||
const initRouteCacheValue = (pathname) => {
|
||||
if (routeCache.get(pathname) === undefined && routeCachekey[pathname]) {
|
||||
setRouteCache(pathname, false);
|
||||
@@ -184,22 +147,17 @@ export default (props: any) => {
|
||||
|
||||
const runtimeConfig = {
|
||||
...initialInfo,
|
||||
logout: async (userInfo) => {
|
||||
logout: async () => {
|
||||
await logout();
|
||||
navigate(loginPath);
|
||||
},
|
||||
showVersion: () => {
|
||||
return showVersion();
|
||||
},
|
||||
showShortcuts: () => {
|
||||
return showShortcuts();
|
||||
},
|
||||
showVersion: () => {},
|
||||
showShortcuts: () => {},
|
||||
notFound: <span>404 not found</span>
|
||||
};
|
||||
|
||||
const handleToggleCollapse = (e: any) => {
|
||||
e.stopPropagation();
|
||||
// setCollapsed(!collapsed);
|
||||
setUserSettings({
|
||||
...userSettings,
|
||||
collapsed: !userSettings.collapsed
|
||||
@@ -235,38 +193,6 @@ export default (props: any) => {
|
||||
|
||||
console.log('matchedRoute=========', matchedRoute, route);
|
||||
|
||||
const allRouteKeys = useMemo(() => {
|
||||
const keys = new Set<string>();
|
||||
const childrenRoutes = route?.children || [];
|
||||
const traverseRoutes = (routes) => {
|
||||
routes.forEach((r) => {
|
||||
if (r.path) {
|
||||
keys.add(r.path);
|
||||
}
|
||||
if (r.children) {
|
||||
traverseRoutes(r.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
traverseRoutes(childrenRoutes);
|
||||
|
||||
return keys;
|
||||
}, [route?.children]);
|
||||
|
||||
const showUpgrade = useMemo(() => {
|
||||
return (
|
||||
initialState?.currentUser?.is_admin &&
|
||||
updateCheck.latest_version &&
|
||||
updateCheck.latest_version !== version?.version &&
|
||||
updateCheck.latest_version?.indexOf('0.0.0') === -1 &&
|
||||
updateCheck.latest_version?.indexOf('rc') === -1
|
||||
);
|
||||
}, [
|
||||
updateCheck.latest_version,
|
||||
version.version,
|
||||
initialState?.currentUser?.is_admin
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const body = document.querySelector('body');
|
||||
if (body) {
|
||||
@@ -306,92 +232,13 @@ export default (props: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleToggleGroup = (menuItemProps, e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (collapseKeys.has(menuItemProps.key)) {
|
||||
collapseKeys.delete(menuItemProps.key);
|
||||
} else {
|
||||
collapseKeys.add(menuItemProps.key);
|
||||
}
|
||||
setCollapseKeys(new Set(collapseKeys));
|
||||
};
|
||||
|
||||
const menuContentRender = (menuProps, defaultDom) => {
|
||||
const menuContentRender = (menuProps: any, defaultDom: React.ReactNode) => {
|
||||
return <SiderMenu {...menuProps}></SiderMenu>;
|
||||
};
|
||||
|
||||
const actionRender = (layoutProps) => {
|
||||
const dom = getRightRenderContent({
|
||||
runtimeConfig,
|
||||
loading,
|
||||
initialState,
|
||||
setInitialState,
|
||||
intl,
|
||||
isDarkTheme: userSettings.isDarkTheme,
|
||||
siderWidth: layoutProps?.siderWidth,
|
||||
collapsed: layoutProps?.collapsed,
|
||||
showUpgrade
|
||||
});
|
||||
|
||||
return dom;
|
||||
};
|
||||
|
||||
const menuItemRender = (menuItemProps, defaultDom) => {
|
||||
if (menuItemProps.isUrl || menuItemProps.children) {
|
||||
return defaultDom;
|
||||
}
|
||||
if (menuItemProps.path && location.pathname !== menuItemProps.path) {
|
||||
return (
|
||||
<Tooltip
|
||||
title={collapsed ? menuItemProps.name : false}
|
||||
placement="right"
|
||||
>
|
||||
<Link
|
||||
to={menuItemProps.path.replace('/*', '')}
|
||||
target={menuItemProps.target}
|
||||
>
|
||||
{defaultDom}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip title={collapsed ? menuItemProps.name : false} placement="right">
|
||||
{defaultDom}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const menuDataRender = (menuData) => {
|
||||
const currentItem = menuData.find((s) => location.pathname === s.path);
|
||||
const result = menuData.map((item) => {
|
||||
const newItem = { ...item };
|
||||
|
||||
const selected =
|
||||
location.pathname === newItem.path ||
|
||||
location.pathname.indexOf(newItem.path) > -1;
|
||||
|
||||
if (newItem.icon) {
|
||||
newItem.icon = selected ? (
|
||||
<IconFont type={newItem.selectedIcon} />
|
||||
) : (
|
||||
<IconFont type={newItem.defaultIcon} />
|
||||
);
|
||||
}
|
||||
if (newItem.children) {
|
||||
newItem.children = menuDataRender(newItem.children);
|
||||
}
|
||||
return newItem;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const onPageChange = (route) => {
|
||||
const onPageChange = (route: any) => {
|
||||
const { location } = history;
|
||||
const { pathname } = location;
|
||||
console.log('onPageChange', pathname, route);
|
||||
|
||||
initRouteCacheValue(pathname);
|
||||
dropRouteCache(pathname);
|
||||
@@ -413,7 +260,7 @@ export default (props: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onMenuHeaderClick = (e) => {
|
||||
const onMenuHeaderClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const pagepath = initialState?.currentUser?.is_admin
|
||||
@@ -423,65 +270,13 @@ export default (props: any) => {
|
||||
navigate(pagepath);
|
||||
};
|
||||
|
||||
const onCollapse = (value) => {
|
||||
const onCollapse = (value: boolean) => {
|
||||
setUserSettings({
|
||||
...userSettings,
|
||||
collapsed: value
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// clear previous marks and measures
|
||||
performance.clearMarks();
|
||||
performance.clearMeasures();
|
||||
|
||||
// record start time
|
||||
performance.mark('route-start');
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// record end time
|
||||
performance.mark('route-end');
|
||||
|
||||
// make sure `route-start` exists
|
||||
if (performance.getEntriesByName('route-start').length > 0) {
|
||||
performance.measure('route-change', 'route-start', 'route-end');
|
||||
|
||||
const measure = performance.getEntriesByName('route-change')[0];
|
||||
console.log(
|
||||
`[Performance] Route change to ${location.pathname} took ${measure.duration.toFixed(2)}ms`
|
||||
);
|
||||
|
||||
// clear marks and measures
|
||||
performance.clearMarks();
|
||||
performance.clearMeasures();
|
||||
} else {
|
||||
console.warn('Missing performance mark: route-start');
|
||||
}
|
||||
});
|
||||
}, [location.pathname]);
|
||||
|
||||
const onRenderCallback = (id, phase, actualDuration) => {
|
||||
console.log(
|
||||
`[Profiler] Route: ${id} - Phase: ${phase} - Render time: ${actualDuration.toFixed(2)}ms`
|
||||
);
|
||||
};
|
||||
|
||||
const currentTheme = useMemo(() => {
|
||||
const data = {
|
||||
algorithm: userSettings.isDarkTheme
|
||||
? theme.darkAlgorithm
|
||||
: theme.defaultAlgorithm,
|
||||
...themeData
|
||||
};
|
||||
return data;
|
||||
}, [userSettings.isDarkTheme, themeData]);
|
||||
|
||||
const outlet = runtimeConfig.childrenRender ? (
|
||||
runtimeConfig.childrenRender(<Outlet />, props)
|
||||
) : (
|
||||
<Outlet />
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
componentSize="large"
|
||||
@@ -530,7 +325,7 @@ export default (props: any) => {
|
||||
type: 'group'
|
||||
}}
|
||||
splitMenus={true}
|
||||
logo={collapsed ? SLogoIcon : LogoIcon}
|
||||
logo={userSettings.collapsed ? <SLogoIcon /> : <LogoIcon />}
|
||||
menuContentRender={menuContentRender}
|
||||
disableContentMargin
|
||||
{...runtimeConfig}
|
||||
@@ -544,9 +339,11 @@ export default (props: any) => {
|
||||
noAccessible={runtimeConfig?.noAccessible}
|
||||
>
|
||||
{isNoContainerPage ? (
|
||||
outlet
|
||||
<Outlet />
|
||||
) : (
|
||||
<PageContainerInner>{outlet}</PageContainerInner>
|
||||
<PageContainerInner>
|
||||
<Outlet />
|
||||
</PageContainerInner>
|
||||
)}
|
||||
</Exception>
|
||||
</ProLayout>
|
||||
|
||||
@@ -80,5 +80,8 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
|
||||
'clusters.addworker.autoDetect': 'Auto-detect',
|
||||
'clusters.addworker.extraVolume.holder':
|
||||
'e.g. /data/models (path must start with /)',
|
||||
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device'
|
||||
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
|
||||
'clusters.button.genToken':
|
||||
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
|
||||
'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`
|
||||
};
|
||||
|
||||
@@ -69,11 +69,11 @@ export default {
|
||||
'models.form.ollamalink':
|
||||
'Find More in <a href="https://www.ollama.com/library" target="_blank">Ollama Library</a>.',
|
||||
'models.form.backend_parameters.llamabox.placeholder':
|
||||
'e.g., --ctx-size=8192 (use = to separate name and value)',
|
||||
'e.g., --ctx-size=8192 (use = or a space to separate name and value)',
|
||||
'models.form.backend_parameters.vllm.placeholder':
|
||||
'e.g., --max-model-len=8192 (use = to separate name and value)',
|
||||
'e.g., --max-model-len=8192 (use = or a space to separate name and value)',
|
||||
'models.form.backend_parameters.sglang.placeholder':
|
||||
'e.g., --context-length=8192 (use = to separate name and value)',
|
||||
'e.g., --context-length=8192 (use = or a space to separate name and value)',
|
||||
'models.form.backend_parameters.vllm.tips':
|
||||
'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.',
|
||||
'models.logs.pagination.prev': 'Previous {lines} Lines',
|
||||
@@ -260,5 +260,13 @@ export default {
|
||||
'models.form.generic_proxy.button': 'Generic Proxy',
|
||||
'models.accessControlModal.includeusers': 'Include Users',
|
||||
'models.table.genericProxy':
|
||||
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.'
|
||||
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.',
|
||||
'models.form.backendVersion.deprecated': 'Deprecated',
|
||||
'models.accessSettings.public.desc':
|
||||
'Accessible to anyone without authentication.',
|
||||
'models.accessSettings.authed.tips':
|
||||
'Accessible to all authenticated platform users.',
|
||||
'models.accessSettings.allowedUsers.tips':
|
||||
'Only designated users can access the model.',
|
||||
'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`
|
||||
};
|
||||
|
||||
@@ -33,5 +33,11 @@ export default {
|
||||
'noresult.keys.nofound': 'No matching API keys found.',
|
||||
'noresult.catalog.title': 'No Models',
|
||||
'noresult.catalog.subTitle': 'No models have been configured yet.',
|
||||
'noresult.catalog.nofound': 'No matching models found.'
|
||||
'noresult.catalog.nofound': 'No matching models found.',
|
||||
'noresult.resources.cluster':
|
||||
'No clusters available. Add a cluster to get started.',
|
||||
'noresult.resources.worker':
|
||||
'No workers available. Add a worker to get started.',
|
||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||
'noresult.resources.gotoworker': 'Add Worker'
|
||||
};
|
||||
|
||||
@@ -156,5 +156,9 @@ export default {
|
||||
'playground.image.generate.error':
|
||||
'Something went wrong. The image could not be generated.',
|
||||
'playground.uploadfile.sizeError':
|
||||
'File size exceeds the limit. Maximum allowed: {size}.'
|
||||
'File size exceeds the limit. Maximum allowed: {size}.',
|
||||
'playground.uploadImage.url.invalid':
|
||||
'Please enter a direct image URL (e.g. https://.../image.png). Press ESC to cancel.',
|
||||
'playground.uploadImage.url.holder': 'Enter an image URL',
|
||||
'playground.uploadImage.url.button': 'Add Image from URL'
|
||||
};
|
||||
|
||||
@@ -80,7 +80,10 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
|
||||
'clusters.addworker.autoDetect': 'Auto-detect',
|
||||
'clusters.addworker.extraVolume.holder':
|
||||
'e.g. /data/models (path must start with /)',
|
||||
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device'
|
||||
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
|
||||
'clusters.button.genToken':
|
||||
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
|
||||
'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -151,5 +154,7 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
|
||||
// 65. 'clusters.addworker.notSpecified': 'Not Specified',
|
||||
// 66. 'clusters.addworker.autoDetect': 'Auto-detect',
|
||||
// 67. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)'
|
||||
// 68. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device'
|
||||
// 68. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
|
||||
// 69. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
|
||||
// 70. 'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -70,11 +70,11 @@ export default {
|
||||
'models.form.ollamalink':
|
||||
'<a href="https://www.ollama.com/library" target="_blank">Ollamaライブラリ</a>でさらに探す',
|
||||
'models.form.backend_parameters.llamabox.placeholder':
|
||||
'例: --ctx-size=8192(=で名前と値を分ける)',
|
||||
'例: --ctx-size=8192(=または空白で名前と値を分ける)',
|
||||
'models.form.backend_parameters.vllm.placeholder':
|
||||
'例: --max-model-len=8192(=で名前と値を分ける)',
|
||||
'例: --max-model-len=8192(=または空白で名前と値を分ける)',
|
||||
'models.form.backend_parameters.sglang.placeholder':
|
||||
'例: --context-length=8192(=で名前と値を分ける)',
|
||||
'例: --context-length=8192(=または空白で名前と値を分ける)',
|
||||
'models.form.backend_parameters.vllm.tips':
|
||||
'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.',
|
||||
'models.logs.pagination.prev': '前の{lines}行',
|
||||
@@ -260,7 +260,15 @@ export default {
|
||||
'models.form.generic_proxy.button': 'Generic Proxy',
|
||||
'models.accessControlModal.includeusers': 'Include Users',
|
||||
'models.table.genericProxy':
|
||||
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.'
|
||||
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.',
|
||||
'models.form.backendVersion.deprecated': 'Deprecated',
|
||||
'models.accessSettings.public.desc':
|
||||
'Accessible to anyone without authentication.',
|
||||
'models.accessSettings.authed.tips':
|
||||
'Accessible to all authenticated platform users.',
|
||||
'models.accessSettings.allowedUsers.tips':
|
||||
'Only designated users can access the model.',
|
||||
'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -347,5 +355,10 @@ export default {
|
||||
// 64. 'models.table.userSelection.tips': 'Admin users can access all models by default.',
|
||||
// 65. 'models.form.partialoffload.tips': `When CPU offloading is enabled, GPUStack will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`,
|
||||
// 66. 'models.form.backend.warning': 'The selected backend does not support GGUF models. Please add a backend with GGUF support in the Inference Backend.',
|
||||
// 67. 'models.form.backend.warning.gguf': 'Please ensure that the selected custom backend supports GGUF models.',
|
||||
// 67. 'models.form.backend.warning.gguf': 'Please ensure that the selected custom backend supports GGUF models.',,
|
||||
// 68. 'models.form.backendVersion.deprecated': 'Deprecated',
|
||||
// 69. 'models.accessSettings.public.desc': 'Accessible to anyone without authentication.',
|
||||
// 70. 'models.accessSettings.authed.tips': 'Accessible to all authenticated platform users.',
|
||||
// 71.'models.accessSettings.allowedUsers.tips': 'Only designated users can access the model.',
|
||||
// 72. 'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -33,5 +33,11 @@ export default {
|
||||
'noresult.keys.nofound': 'No matching API keys found.',
|
||||
'noresult.catalog.title': 'No Models',
|
||||
'noresult.catalog.subTitle': 'No models have been configured yet.',
|
||||
'noresult.catalog.nofound': 'No matching models found.'
|
||||
'noresult.catalog.nofound': 'No matching models found.',
|
||||
'noresult.resources.cluster':
|
||||
'No clusters available. Add a cluster to get started.',
|
||||
'noresult.resources.worker':
|
||||
'No workers available. Add a worker to get started.',
|
||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||
'noresult.resources.gotoworker': 'Add Worker'
|
||||
};
|
||||
|
||||
@@ -159,11 +159,18 @@ export default {
|
||||
'playground.image.generate.error':
|
||||
'Something went wrong. The image could not be generated.',
|
||||
'playground.uploadfile.sizeError':
|
||||
'File size exceeds the limit. Maximum allowed: {size}.'
|
||||
'File size exceeds the limit. Maximum allowed: {size}.',
|
||||
'playground.uploadImage.url.invalid':
|
||||
'Please enter a direct image URL (e.g. https://.../image.png). Press ESC to cancel.',
|
||||
'playground.uploadImage.url.holder': 'Enter an image URL',
|
||||
'playground.uploadImage.url.button': 'Add Image from URL'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
// 1. 'playground.rerank.query.validate': 'The query is required.'
|
||||
// 2. 'playground.image.generate.error': 'Something went wrong. The image could not be generated.',
|
||||
// 3. 'playground.uploadfile.sizeError': 'File size exceeds the limit. Maximum allowed: {size}.'
|
||||
// 4. 'playground.uploadImage.url.invalid': 'Please enter a direct image URL(e.g. https://.../image.png). Press ESC to cancel.',
|
||||
// 5. 'playground.uploadImage.url.holder': 'Enter an image URL'
|
||||
// 6. 'playground.uploadImage.url.button': 'Add Image from URL'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -48,9 +48,9 @@ export default {
|
||||
'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.',
|
||||
'cluster.provider.comingsoon': 'Скоро будет',
|
||||
'clusters.addworker.nvidiaNotes-01':
|
||||
'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with <span class="bold-text">hostname -I | xargs -n1</span>.',
|
||||
'Если существует несколько исходящих IP-адресов, укажите тот, который должен использовать воркер. Пожалуйста, перепроверьте с помощью <span class="bold-text">hostname -I | xargs -n1</span>.',
|
||||
'clusters.addworker.nvidiaNotes-02':
|
||||
'If a model directory already exists on the worker, you can specify the path to mount it.',
|
||||
'Если директория с моделями уже существует на воркере, вы можете указать путь для её монтирования.',
|
||||
'clusters.addworker.hygonNotes':
|
||||
'Если директория <span class="bold-text">/opt/hyhal</span> не существует, создайте символическую ссылку на путь установки Hygon: <span class="bold-text">/opt/hyhal</span>. Аналогично для директории <span class="bold-text">/opt/dtk</span>.',
|
||||
'clusters.addworker.corexNotes':
|
||||
@@ -60,49 +60,33 @@ export default {
|
||||
'clusters.addworker.cambriconNotes':
|
||||
'Если директория <span class="bold-text">/usr/local/neuware</span> не существует, создайте символическую ссылку на путь установки Cambricon: <span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.',
|
||||
'clusters.addworker.hygonNotes-02':
|
||||
'If failed to detect devices, please try to remove <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
|
||||
'clusters.addworker.selectCluster': 'Select Cluster',
|
||||
'Если не удается обнаружить устройства, попробуйте удалить <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
|
||||
'clusters.addworker.selectCluster': 'Выбрать кластер',
|
||||
'clusters.addworker.selectCluster.tips':
|
||||
'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.',
|
||||
'clusters.addworker.selectGPU': 'Select GPU Vendor',
|
||||
'clusters.addworker.checkEnv': 'Check Environment',
|
||||
'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||
'clusters.addworker.runCommand': 'Run Command',
|
||||
'clusters.addworker.specifyWorkerIP': 'Specify Worker IP',
|
||||
'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
|
||||
'clusters.addworker.enterWorkerIP': 'Enter worker IP',
|
||||
'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
|
||||
'clusters.addworker.extraVolume': 'Additional Volume Mount',
|
||||
'clusters.addworker.configSummary': 'Configuration Summary',
|
||||
'clusters.addworker.gpuVendor': 'GPU Vendor',
|
||||
'clusters.addworker.workerIP': 'Worker IP',
|
||||
'clusters.addworker.notSpecified': 'Not Specified',
|
||||
'clusters.addworker.autoDetect': 'Auto-detect',
|
||||
'Для не-Docker кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.',
|
||||
'clusters.addworker.selectGPU': 'Выбрать производителя GPU',
|
||||
'clusters.addworker.checkEnv': 'Проверить окружение',
|
||||
'clusters.addworker.specifyArgs': 'Указать аргументы',
|
||||
'clusters.addworker.runCommand': 'Выполнить команду',
|
||||
'clusters.addworker.specifyWorkerIP': 'Указать IP воркера',
|
||||
'clusters.addworker.detectWorkerIP': 'Автоопределение IP воркера',
|
||||
'clusters.addworker.enterWorkerIP': 'Введите IP воркера',
|
||||
'clusters.addworker.enterWorkerIP.error': 'Пожалуйста, введите IP воркера.',
|
||||
'clusters.addworker.extraVolume': 'Дополнительное монтирование тома',
|
||||
'clusters.addworker.configSummary': 'Сводка конфигурации',
|
||||
'clusters.addworker.gpuVendor': 'Производитель GPU',
|
||||
'clusters.addworker.workerIP': 'IP воркера',
|
||||
'clusters.addworker.notSpecified': 'Не указано',
|
||||
'clusters.addworker.autoDetect': 'Автоопределение',
|
||||
'clusters.addworker.extraVolume.holder':
|
||||
'e.g. /data/models (path must start with /)',
|
||||
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device'
|
||||
'напр. /data/models (путь должен начинаться с /)',
|
||||
'clusters.addworker.vendorNotes.title': 'Примечания для устройств {vendor}',
|
||||
'clusters.button.genToken':
|
||||
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
|
||||
'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
// 1. 'clusters.addworker.hygonNotes-02': 'If failed to detect devices, please try to remove <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
|
||||
// 2. 'clusters.addworker.selectCluster': 'Select Cluster',
|
||||
// 3. 'clusters.addworker.selectCluster.tips': 'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.',
|
||||
// 4. 'clusters.addworker.selectGPU': 'Select GPU Vendor',
|
||||
// 5. 'clusters.addworker.checkEnv': 'Check Environment',
|
||||
// 6. 'clusters.addworker.specifyArgs': 'Specify Arguments',
|
||||
// 7. 'clusters.addworker.runCommand': 'Run Command',
|
||||
// 8. 'clusters.addworker.specifyWorkerIP': 'Specify Worker IP',
|
||||
// 9. 'clusters.addworker.detectWorkerIP': 'Auto-detect Worker IP',
|
||||
// 10. 'clusters.addworker.enterWorkerIP': 'Enter worker IP',
|
||||
// 11. 'clusters.addworker.enterWorkerIP.error': 'Please enter the worker IP.',
|
||||
// 12. 'clusters.addworker.extraVolume': 'Additional Volume Mount',
|
||||
// 13. 'clusters.addworker.configSummary': 'Configuration Summary',
|
||||
// 14. 'clusters.addworker.gpuVendor': 'GPU Vendor',
|
||||
// 15. 'clusters.addworker.workerIP': 'Worker IP',
|
||||
// 16. 'clusters.addworker.notSpecified': 'Not Specified',
|
||||
// 17. 'clusters.addworker.nvidiaNotes-01': 'If multiple outbound IPs exist, specify the one you want the worker to use. Please double-check with <span class="bold-text">hostname -I | xargs -n1</span>.',
|
||||
// 18. 'clusters.addworker.nvidiaNotes-02': 'If a model directory already exists on the worker, you can specify the path to mount it.',
|
||||
// 19. 'clusters.addworker.autoDetect': 'Auto-detect',
|
||||
// 20. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)',
|
||||
// 21. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device'
|
||||
// ========== End of To-Do List ==========
|
||||
// 1. 'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`,
|
||||
// 2. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
|
||||
// ================================================================
|
||||
|
||||
@@ -31,9 +31,9 @@ export default {
|
||||
'dashboard.usage.datePicker.last7days': 'Последние 7 Days',
|
||||
'dashboard.usage.datePicker.last30days': 'Последние 30 Days',
|
||||
'dashboard.usage.datePicker.last60days': 'Последние 60 Days',
|
||||
'dashboard.clusters': 'Clusters'
|
||||
'dashboard.clusters': 'Кластеры'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
// 1. 'dashboard.clusters': 'Clusters',
|
||||
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
+38
-36
@@ -2,7 +2,7 @@ export default {
|
||||
'models.button.deploy': 'Развернуть модель',
|
||||
'models.title': 'Модели',
|
||||
'models.title.edit': 'Редактировать модель',
|
||||
'models.table.models': 'модели',
|
||||
'models.table.models': 'Модели',
|
||||
'models.table.name': 'Название модели',
|
||||
'models.form.source': 'Источник',
|
||||
'models.form.repoid': 'ID репозитория',
|
||||
@@ -13,8 +13,10 @@ export default {
|
||||
'models.form.env': 'Переменные окружения',
|
||||
'models.form.configurations': 'Конфигурации',
|
||||
'models.form.s3address': 'S3-адрес',
|
||||
'models.form.partialoffload.tips': `When CPU offloading is enabled, GPUStack will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`, // Already translated
|
||||
'models.form.distribution.tips': `Позволяет переносить часть слоёв модели на один или несколько удалённых воркеров, когда ресурсов текущего воркера недостаточно.`,
|
||||
'models.form.partialoffload.tips':
|
||||
'При включении CPU оффлоудинга GPUStack будет выделять оперативную память, если ресурсов GPU недостаточно. Вы должны правильно настроить бэкенд вывода для использования гибридного CPU+GPU или полного CPU вывода.',
|
||||
'models.form.distribution.tips':
|
||||
'Позволяет переносить часть слоёв модели на один или несколько удалённых воркеров, когда ресурсов текущего воркера недостаточно.',
|
||||
'models.openinplayground': 'Открыть в Песочнице',
|
||||
'models.instances': 'инстансы',
|
||||
'models.table.replicas.edit': 'Редактировать реплики',
|
||||
@@ -69,13 +71,13 @@ export default {
|
||||
'models.form.ollamalink':
|
||||
'Больше моделей в библиотеке <a href="https://www.ollama.com/library" target="_blank">Ollama</a>',
|
||||
'models.form.backend_parameters.llamabox.placeholder':
|
||||
'например: --ctx-size=8192(параметр и значение разделены знаком =)',
|
||||
'например: --ctx-size=8192(параметр и значение разделены знаком = или пробелом)',
|
||||
'models.form.backend_parameters.vllm.placeholder':
|
||||
'например: --max-model-len=8192(параметр и значение разделены знаком =)',
|
||||
'например: --max-model-len=8192(параметр и значение разделены знаком = или пробелом)',
|
||||
'models.form.backend_parameters.sglang.placeholder':
|
||||
'например: --context-length=8192(параметр и значение разделены знаком =)',
|
||||
'например: --context-length=8192(параметр и значение разделены знаком = или пробелом)',
|
||||
'models.form.backend_parameters.vllm.tips':
|
||||
'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.',
|
||||
'Для получения подробной информации о параметрах {backend} см. <a href={link} target="_blank">здесь</a>.',
|
||||
'models.logs.pagination.prev': 'Предыдущие {lines} строк',
|
||||
'models.logs.pagination.next': 'Следующие {lines} строк',
|
||||
'models.logs.pagination.last': 'Последняя страница',
|
||||
@@ -89,11 +91,11 @@ export default {
|
||||
'models.form.backend.llamabox':
|
||||
'Для моделей формата GGUF. Поддержка Linux, macOS и Windows.',
|
||||
'models.form.backend.vllm':
|
||||
'Built-in support for NVIDIA, AMD, Ascend, Hygon, Iluvatar, and MetaX devices.',
|
||||
'models.form.backend.voxbox': 'Only supports NVIDIA GPUs and CPUs.',
|
||||
'models.form.backend.mindie': 'Only supports Ascend NPUs.',
|
||||
'Встроенная поддержка устройств NVIDIA, AMD, Ascend, Hygon, Iluvatar и MetaX.',
|
||||
'models.form.backend.voxbox': 'Поддерживает только GPU NVIDIA и CPU.',
|
||||
'models.form.backend.mindie': 'Поддерживает только Ascend NPU.',
|
||||
'models.form.backend.sglang':
|
||||
'Built-in support for NVIDIA/AMD GPUs and Ascend NPUs.',
|
||||
'Встроенная поддержка GPU NVIDIA/AMD и Ascend NPU.',
|
||||
'models.form.search.gguftips':
|
||||
'Для воркеров на macOS/Windows отметьте GGUF (для аудиомоделей снимите).',
|
||||
'models.form.button.addlabel': 'Добавить метку',
|
||||
@@ -120,9 +122,9 @@ export default {
|
||||
'models.form.moreparameters': 'Описание параметров',
|
||||
'models.table.vram.allocated': 'Выделенная VRAM',
|
||||
'models.form.backend.warning':
|
||||
'The selected backend does not support GGUF models. Please add a backend with GGUF support in the Inference Backend.',
|
||||
'Выбранный бэкенд не поддерживает модели GGUF. Пожалуйста, добавьте бэкенд с поддержкой GGUF в разделе Бэкенды вывода.',
|
||||
'models.form.backend.warning.gguf':
|
||||
'Please ensure that the selected custom backend supports GGUF models.',
|
||||
'Пожалуйста, убедитесь, что выбранный пользовательский бэкенд поддерживает модели GGUF.',
|
||||
'models.form.ollama.warning':
|
||||
'Чтобы развернуть бэкенд для моделей Ollama с использованием llama-box , выполните следующие шаги.',
|
||||
'models.form.backend.warning.llamabox':
|
||||
@@ -147,7 +149,8 @@ export default {
|
||||
'Изменения вступят в силу только после удаления и повторного создания инстанса.',
|
||||
'models.table.download.progress': 'Прогресс',
|
||||
'models.table.button.apiAccessInfo': 'Доступ к API',
|
||||
'models.table.button.apiAccessInfo.tips': `Для интеграции этой модели со сторонними приложениями используйте следующие данные: URL доступа, имя модели и ключ API. Эти учетные данные необходимы для обеспечения правильного подключения и использования сервиса модели.`, // Translated
|
||||
'models.table.button.apiAccessInfo.tips':
|
||||
'Для интеграции этой модели со сторонними приложениями используйте следующие данные: URL доступа, имя модели и ключ API. Эти учетные данные необходимы для обеспечения правильного подключения и использования сервиса модели.',
|
||||
'models.table.apiAccessInfo.endpoint': 'URL доступа',
|
||||
'models.table.apiAccessInfo.modelName': 'Имя модели',
|
||||
'models.table.apiAccessInfo.apikey': 'Ключ API',
|
||||
@@ -165,7 +168,8 @@ export default {
|
||||
'<span class="bold-text">После обновления до версии (v0.7.0),</span> все ранее развёрнутые модели продолжат работать в обычном режиме.',
|
||||
'models.ollama.deprecated.issue':
|
||||
'См. связанную проблему: <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">#1979 on GitHub</a>.',
|
||||
'models.ollama.deprecated.notice': `Источник моделей Ollama объявлен устаревшим начиная с версии v0.6.1. Подробности см. в <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">соответствующем issue на GitHub</a>.`,
|
||||
'models.ollama.deprecated.notice':
|
||||
'Источник моделей Ollama объявлен устаревшим начиная с версии v0.6.1. Подробности см. в <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">соответствующем issue на GitHub</a>.',
|
||||
'models.backend.mindie.310p':
|
||||
'Ascend 310P поддерживает только FP16, поэтому необходимо установить --dtype=float16.',
|
||||
'models.form.gpuCount': 'GPU на реплику',
|
||||
@@ -181,9 +185,9 @@ export default {
|
||||
'models.table.accessScope.all': 'Все пользователи',
|
||||
'models.table.userSelection': 'Выбор пользователей',
|
||||
'models.button.accessSettings.tips':
|
||||
'Changes to access settings take effect after one minute.',
|
||||
'Изменения в настройках доступа вступают в силу через одну минуту.',
|
||||
'models.table.userSelection.tips':
|
||||
'Admin users can access all models by default.',
|
||||
'Администраторы по умолчанию имеют доступ ко всем моделям.',
|
||||
'models.table.filterByName': 'Фильтр по имени пользователя',
|
||||
'models.table.admin': 'Администратор',
|
||||
'models.table.noselected': 'Пользователи не выбраны',
|
||||
@@ -197,7 +201,7 @@ export default {
|
||||
'models.form.maxCPUSize': 'Максимальный размер CPU кэша (ГиБ)',
|
||||
'models.form.remoteURL': 'URL удаленного хранилища',
|
||||
'models.form.remoteURL.tips':
|
||||
'Refer to the <a href="https://docs.lmcache.ai/api_reference/configurations.html" target="_blank">configuration documentation</a> for details.',
|
||||
'Подробности см. в <a href="https://docs.lmcache.ai/api_reference/configurations.html" target="_blank">документации по конфигурации</a>.',
|
||||
'models.form.runCommandPlaceholder':
|
||||
'напр., vllm serve Qwen/Qwen2.5-1.5B-Instruct',
|
||||
'models.accessSettings.public': 'Публичный',
|
||||
@@ -212,7 +216,7 @@ export default {
|
||||
'models.form.gpusAllocationType.auto': 'Авто',
|
||||
'models.form.gpusAllocationType.custom': 'Вручную',
|
||||
'models.form.gpusAllocationType.auto.tips':
|
||||
'The system automatically calculates the GPU count per replica, using powers of two by default and capped by the selected GPUs.',
|
||||
'Система автоматически вычисляет количество GPU на реплику, по умолчанию используя степени двойки и ограничиваясь выбранными GPU.',
|
||||
'models.form.gpusAllocationType.custom.tips':
|
||||
'Вы можете указать точное количество GPU на реплику.',
|
||||
'models.mymodels.status.inactive': 'Остановлен',
|
||||
@@ -252,31 +256,29 @@ export default {
|
||||
'models.form.backend.custom': 'Пользовательский',
|
||||
'models.form.rules.name':
|
||||
'До 63 символов; только буквы, цифры, точки (.), подчёркивания (_) и дефисы (-); должно начинаться и заканчиваться буквенно-цифровым символом.',
|
||||
'models.catalog.button.explore': 'Explore More Models',
|
||||
'models.catalog.button.explore': 'Исследовать больше моделей',
|
||||
'models.catalog.precision': 'Точность',
|
||||
'models.form.gpuPerReplica.tips': 'Введите произвольное число',
|
||||
'models.form.generic_proxy': 'Включить универсальный прокси',
|
||||
'models.form.generic_proxy.tips':
|
||||
'After enabling the generic proxy, you can access URI paths that do not follow the OpenAI API standard.',
|
||||
'После включения универсального прокси вы можете получать доступ к URI-путям, которые не следуют стандарту OpenAI API.',
|
||||
'models.form.generic_proxy.button': 'Универсальный прокси',
|
||||
'models.accessControlModal.includeusers': 'Включить пользователей',
|
||||
'models.table.genericProxy':
|
||||
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.'
|
||||
'Используйте следующий префикс пути и укажите имя модели либо в заголовке запроса <span class="bold-text">X-GPUStack-Model</span>, либо в поле model в теле запроса. Все запросы с этим префиксом пути будут перенаправлены в бэкенд вывода.',
|
||||
'models.form.backendVersion.deprecated': 'Устаревший',
|
||||
'models.accessSettings.public.desc':
|
||||
'Accessible to anyone without authentication.',
|
||||
'models.accessSettings.authed.tips':
|
||||
'Accessible to all authenticated platform users.',
|
||||
'models.accessSettings.allowedUsers.tips':
|
||||
'Only designated users can access the model.',
|
||||
'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
// 1. 'models.catalog.button.explore': 'Explore More Models',
|
||||
// 2. 'models.table.genericProxy': 'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.',
|
||||
// 3. 'models.form.backend.vllm': 'Built-in support for NVIDIA, AMD, Ascend, Hygon, Iluvatar, and MetaX devices.',
|
||||
// 4. 'models.form.backend.voxbox': 'Only supports NVIDIA GPUs and CPUs.',
|
||||
// 5. 'models.form.backend.mindie': 'Only supports Ascend NPUs.',
|
||||
// 6. 'models.form.backend.sglang': 'Built-in support for NVIDIA/AMD GPUs and Ascend NPUs.',
|
||||
// 7. 'models.form.gpusAllocationType.auto.tips': 'The system automatically calculates the GPU count per replica, using powers of two by default and capped by the selected GPUs.',
|
||||
// 8. 'models.form.backend_parameters.vllm.tips': 'For more details about {backend} parameters, see <a href={link} target="_blank">here</a>.',
|
||||
// 9. 'models.button.accessSettings.tips': 'Changes to access settings take effect after one minute.',
|
||||
// 10. 'models.table.userSelection.tips': 'Admin users can access all models by default.',
|
||||
// 11. 'models.form.generic_proxy.tips': 'After enabling the generic proxy, you can access URI paths that do not follow the OpenAI API standard.',
|
||||
// 12. 'models.form.partialoffload.tips': `When CPU offloading is enabled, GPUStack will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`,
|
||||
// 13. 'models.form.backend.warning': 'The selected backend does not support GGUF models. Please add a backend with GGUF support in the Inference Backend.',
|
||||
// 14. 'models.form.backend.warning.gguf': 'Please ensure that the selected custom backend supports GGUF models.',
|
||||
// 1. 'models.accessSettings.public.desc': 'Accessible to anyone without authentication.',
|
||||
// 2. 'models.accessSettings.authed.tips': 'Accessible to all authenticated platform users.',
|
||||
// 3. 'models.accessSettings.allowedUsers.tips': 'Only designated users can access the model.',
|
||||
// 4. 'models.form.backendVersions.tips': `To use more versions, go to the {link} page and edit the backend to add versions.`
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
export default {
|
||||
'noresult.button.add': 'Добавить сейчас',
|
||||
'noresult.mymodels.title': 'Нет доступных моделей',
|
||||
'noresult.mymodels.subTitle': 'Обратитесь к администратору для получения доступа.',
|
||||
'noresult.mymodels.subTitle':
|
||||
'Обратитесь к администратору для получения доступа.',
|
||||
'noresult.mymodels.nofound': 'Подходящие модели не найдены',
|
||||
'noresult.deployments.title': 'Нет развернутых моделей',
|
||||
'noresult.deployments.subTitle': 'Вы еще не развернули ни одной модели. Нажмите кнопку ниже, чтобы начать.',
|
||||
'noresult.deployments.subTitle':
|
||||
'Вы еще не развернули ни одной модели. Нажмите кнопку ниже, чтобы начать.',
|
||||
'noresult.gpus.title': 'GPU устройства не обнаружены',
|
||||
'noresult.gpus.subTitle': 'Проверьте, что статус Worker READY.',
|
||||
'noresult.gpus.nofound': 'Подходящие GPU устройства не найдены.',
|
||||
@@ -32,5 +34,18 @@ export default {
|
||||
'noresult.keys.nofound': 'Подходящие API-ключи не найдены.',
|
||||
'noresult.catalog.title': 'Нет моделей',
|
||||
'noresult.catalog.subTitle': 'Модели еще не настроены.',
|
||||
'noresult.catalog.nofound': 'Подходящие модели не найдены.'
|
||||
'noresult.catalog.nofound': 'Подходящие модели не найдены.',
|
||||
'noresult.resources.cluster':
|
||||
'No clusters available. Add a cluster to get started.',
|
||||
'noresult.resources.worker':
|
||||
'No workers available. Add a worker to get started.',
|
||||
'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||
'noresult.resources.gotoworker': 'Add Worker'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
// 1. 'noresult.resources.cluster': 'No clusters available. Add a cluster to get started.',
|
||||
// 2. 'noresult.resources.worker': 'No workers available. Add a worker to get started.',
|
||||
// 3. 'noresult.resources.gotocluster': 'Create Your First Cluster',
|
||||
// 4. 'noresult.resources.gotoworker': 'Add Worker'
|
||||
// ========================================================================
|
||||
|
||||
@@ -150,10 +150,18 @@ export default {
|
||||
'playground.model.noavailable.tips2':
|
||||
'Если нужная модель не отображается, убедитесь, что она запущена и ей присвоена правильная категория. Если категория указана неверно, её можно изменить вручную в настройках модели.',
|
||||
'playground.rerank.query.validate': 'Необходимо указать запрос.',
|
||||
'playground.image.generate.error': 'Произошла ошибка. Не удалось сгенерировать изображение.',
|
||||
'playground.uploadfile.sizeError': 'Размер файла превышает ограничение. Максимальный размер: {size}.'
|
||||
'playground.image.generate.error':
|
||||
'Произошла ошибка. Не удалось сгенерировать изображение.',
|
||||
'playground.uploadfile.sizeError':
|
||||
'Размер файла превышает ограничение. Максимальный размер: {size}.',
|
||||
'playground.uploadImage.url.invalid':
|
||||
'Please enter a direct image URL (e.g. https://.../image.png). Press ESC to cancel.',
|
||||
'playground.uploadImage.url.holder': 'Enter an image URL',
|
||||
'playground.uploadImage.url.button': 'Add Image from URL'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
// 1. 'playground.uploadImage.url.invalid': 'Please enter a direct image URL(e.g. https://.../image.png). Press ESC to cancel.',
|
||||
// 2. 'playground.uploadImage.url.holder': 'Enter an image URL',
|
||||
// 3. 'playground.uploadImage.url.button': 'Add Image from URL'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -78,5 +78,9 @@ export default {
|
||||
'clusters.addworker.autoDetect': '自动检测',
|
||||
'clusters.addworker.extraVolume.holder':
|
||||
'例如:/data/models(路径需以 / 开头)',
|
||||
'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项'
|
||||
'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项',
|
||||
'clusters.button.genToken':
|
||||
'需要创建令牌?点击<a href="{link}" target="_blank">这里</a>。',
|
||||
'clusters.addworker.amdNotes-01':
|
||||
'如果 <span class="bold-text">/opt/rocm</span> 目录不存在,请创建一个指向已安装 ROCm 路径的符号链接:<span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>。'
|
||||
};
|
||||
|
||||
@@ -68,11 +68,11 @@ export default {
|
||||
'models.form.ollamalink':
|
||||
'在 <a href="https://www.ollama.com/library" target="_blank">Ollama Library</a> 中查找',
|
||||
'models.form.backend_parameters.llamabox.placeholder':
|
||||
'例如,--ctx-size=8192(参数名和值用 = 号分隔)',
|
||||
'例如,--ctx-size=8192(参数名和值用 = 号或空格分隔)',
|
||||
'models.form.backend_parameters.vllm.placeholder':
|
||||
'例如,--max-model-len=8192(参数名和值用 = 号分隔)',
|
||||
'例如,--max-model-len=8192(参数名和值用 = 号或空格分隔)',
|
||||
'models.form.backend_parameters.sglang.placeholder':
|
||||
'例如,--context-length=8192(参数名和值用 = 号分隔)',
|
||||
'例如,--context-length=8192(参数名和值用 = 号或空格分隔)',
|
||||
'models.form.backend_parameters.vllm.tips':
|
||||
'更多 {backend} 参数说明查看<a href={link} target="_blank">这里</a>。',
|
||||
'models.logs.pagination.prev': '上一 {lines} 行',
|
||||
@@ -247,5 +247,10 @@ export default {
|
||||
'models.form.generic_proxy.button': '通用代理',
|
||||
'models.accessControlModal.includeusers': '显示用户',
|
||||
'models.table.genericProxy':
|
||||
'使用以下路径前缀,并在请求头 <span class="bold-text">X-GPUStack-Model</span> 或请求体中的 model 字段设置模型名称后访问该模型。该路径的所有子路径请求会被转发到推理后端。'
|
||||
'使用以下路径前缀,并在请求头 <span class="bold-text">X-GPUStack-Model</span> 或请求体中的 model 字段设置模型名称后访问该模型。该路径的所有子路径请求会被转发到推理后端。',
|
||||
'models.form.backendVersion.deprecated': '已弃用',
|
||||
'models.accessSettings.public.desc': '任何人无需认证即可访问。',
|
||||
'models.accessSettings.authed.tips': '平台内所有已认证用户可访问。',
|
||||
'models.accessSettings.allowedUsers.tips': '仅允许选定的特定用户访问。',
|
||||
'models.form.backendVersions.tips': `如需使用更多版本,请前往{link}页面并编辑对应的后端以添加版本。`
|
||||
};
|
||||
|
||||
@@ -33,5 +33,9 @@ export default {
|
||||
'noresult.keys.nofound': '未找到匹配的 API 密钥',
|
||||
'noresult.catalog.title': '暂无模型',
|
||||
'noresult.catalog.subTitle': '尚未配置任何模型。',
|
||||
'noresult.catalog.nofound': '未找到匹配的模型'
|
||||
'noresult.catalog.nofound': '未找到匹配的模型',
|
||||
'noresult.resources.cluster': '暂无可用集群,请添加集群以开始使用。',
|
||||
'noresult.resources.worker': '暂无可用节点,请添加节点以开始使用。',
|
||||
'noresult.resources.gotocluster': '创建您的第一个集群',
|
||||
'noresult.resources.gotoworker': '添加节点'
|
||||
};
|
||||
|
||||
@@ -148,5 +148,10 @@ export default {
|
||||
'若预期的模型未显示,请检查模型是否已正常运行并被正确分类。如分类不正确,请编辑模型并手动调整其类别。',
|
||||
'playground.rerank.query.validate': '查询内容不能为空',
|
||||
'playground.image.generate.error': '出了一点问题,图片未能生成。',
|
||||
'playground.uploadfile.sizeError': '上传的文件大小超过限制,最大允许 {size}。'
|
||||
'playground.uploadfile.sizeError':
|
||||
'上传的文件大小超过限制,最大允许 {size}。',
|
||||
'playground.uploadImage.url.invalid':
|
||||
'请输入直接的图片链接(例如:https://…/image.png)。按 ESC 可取消。',
|
||||
'playground.uploadImage.url.holder': '请输入图片链接',
|
||||
'playground.uploadImage.url.button': '从链接添加图片'
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DoubleRightOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import { Button, FloatButton } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
@@ -45,6 +45,7 @@ const InfiniteScroller: React.FC<
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FloatButton.BackTop visibilityHeight={1000} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
interface ScrollerContextProps {
|
||||
total: number;
|
||||
total: number; // total pages
|
||||
current: number;
|
||||
loading: boolean;
|
||||
refresh: (nextPage: number) => void;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Empty, EmptyProps, Typography } from 'antd';
|
||||
import { Button, Empty, EmptyProps, Typography } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@@ -54,9 +54,19 @@ const NoResult: React.FC<
|
||||
loading?: boolean;
|
||||
loadend?: boolean;
|
||||
dataSource?: any[];
|
||||
buttonText?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
> = (props) => {
|
||||
const { filters, noFoundText, loadend, loading, dataSource } = props;
|
||||
const {
|
||||
filters,
|
||||
noFoundText,
|
||||
loadend,
|
||||
loading,
|
||||
dataSource,
|
||||
buttonText,
|
||||
onClick
|
||||
} = props;
|
||||
|
||||
const hasFilters = useMemo(() => {
|
||||
const filterValues = _.omit(filters, ['page', 'perPage']);
|
||||
@@ -70,6 +80,15 @@ const NoResult: React.FC<
|
||||
});
|
||||
}, [filters]);
|
||||
|
||||
const renderChildren = () => {
|
||||
if (!buttonText || !onClick) return null;
|
||||
return (
|
||||
<Button color="primary" variant="filled" onClick={onClick}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!loading && loadend && !dataSource?.length ? (
|
||||
@@ -96,7 +115,7 @@ const NoResult: React.FC<
|
||||
</Description>
|
||||
}
|
||||
>
|
||||
{!hasFilters && props.children}
|
||||
{!hasFilters && renderChildren()}
|
||||
</StyledEmpty>
|
||||
) : (
|
||||
<span></span>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { PageActionType } from '@/config/types';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
||||
import { Button, ConfigProvider, Table } from 'antd';
|
||||
import { ConfigProvider, Table } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import NoResult from '../_components/no-result';
|
||||
import PageBox from '../_components/page-box';
|
||||
@@ -111,11 +111,9 @@ const APIKeys: React.FC = () => {
|
||||
})}
|
||||
title={intl.formatMessage({ id: 'noresult.keys.title' })}
|
||||
subTitle={intl.formatMessage({ id: 'noresult.keys.subTitle' })}
|
||||
>
|
||||
<Button type="primary" onClick={handleAddKey}>
|
||||
{intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
</Button>
|
||||
</NoResult>
|
||||
onClick={handleAddKey}
|
||||
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
></NoResult>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import CardSkeleton from '@/components/templates/card-skelton';
|
||||
import breakpoints from '@/config/breakpoints';
|
||||
import InfiniteScroller from '@/pages/_components/infinite-scroller';
|
||||
import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context';
|
||||
import { Col, FloatButton, Row, Spin } from 'antd';
|
||||
import { Col, Row, Spin } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import ResizeObserver from 'rc-resize-observer';
|
||||
import React, { useCallback } from 'react';
|
||||
@@ -139,7 +139,6 @@ const CardList: React.FC<BackendListProps> = (props) => {
|
||||
<ListSkeleton span={span} loading={loading} isFirst={isFirst} />
|
||||
</InfiniteScroller>
|
||||
</ResizeObserver>
|
||||
<FloatButton.BackTop visibilityHeight={1000} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import ScrollerModal from '@/components/scroller-modal';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { VersionListItem } from '../config/types';
|
||||
import VersionInfo from '../forms/version-info';
|
||||
@@ -7,12 +9,14 @@ import VersionInfo from '../forms/version-info';
|
||||
interface VersionInfoModalProps {
|
||||
open?: boolean;
|
||||
currentData?: any;
|
||||
addVersion?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const VersionInfoModal: React.FC<VersionInfoModalProps> = ({
|
||||
open,
|
||||
currentData,
|
||||
addVersion,
|
||||
onClose
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
@@ -55,7 +59,14 @@ const VersionInfoModal: React.FC<VersionInfoModalProps> = ({
|
||||
return (
|
||||
<ScrollerModal
|
||||
open={open}
|
||||
title={intl.formatMessage({ id: 'backend.versions' })}
|
||||
title={
|
||||
<div className="flex-center gap-16">
|
||||
<span>{intl.formatMessage({ id: 'backend.versions' })}</span>
|
||||
<Button onClick={addVersion} type="link" size="small">
|
||||
<PlusOutlined /> {intl.formatMessage({ id: 'backend.addVersion' })}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
width={600}
|
||||
centered
|
||||
destroyOnHidden
|
||||
@@ -63,22 +74,6 @@ const VersionInfoModal: React.FC<VersionInfoModalProps> = ({
|
||||
maskClosable={false}
|
||||
onOk={onClose}
|
||||
onCancel={onClose}
|
||||
styles={{
|
||||
content: {
|
||||
padding: '0 0 16px 0'
|
||||
},
|
||||
header: {
|
||||
padding: 'var(--ant-modal-content-padding)',
|
||||
paddingBottom: '0'
|
||||
},
|
||||
body: {
|
||||
padding: '16px 24px 32px'
|
||||
},
|
||||
footer: {
|
||||
padding: '16px 24px',
|
||||
margin: '0'
|
||||
}
|
||||
}}
|
||||
footer={null}
|
||||
>
|
||||
<VersionInfo versionConfigs={versionConfigs} />
|
||||
|
||||
@@ -3,7 +3,6 @@ import SGLangLogo from '@/assets/logo/sglang.png';
|
||||
import vLLMLogo from '@/assets/logo/vllm.png';
|
||||
import VoxBoxLogo from '@/assets/logo/voxbox.png';
|
||||
import icons from '@/components/icon-font/icons';
|
||||
import { GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||
import { backendOptionsMap } from '@/pages/llmodels/config/backend-parameters';
|
||||
import {
|
||||
GPUDriverMap,
|
||||
@@ -153,37 +152,37 @@ export const frameworks = [
|
||||
label: 'CANN',
|
||||
value: GPUDriverMap.ASCEND,
|
||||
tips: ManufacturerMap[GPUDriverMap.ASCEND],
|
||||
locale: true
|
||||
tipLocale: true
|
||||
},
|
||||
{
|
||||
label: 'DTK',
|
||||
value: GPUDriverMap.HYGON,
|
||||
tips: ManufacturerMap[GPUDriverMap.HYGON],
|
||||
locale: true
|
||||
tipLocale: true
|
||||
},
|
||||
{
|
||||
label: 'MACA',
|
||||
value: GPUDriverMap.METAX,
|
||||
tips: ManufacturerMap[GPUDriverMap.METAX],
|
||||
locale: true
|
||||
tipLocale: true
|
||||
},
|
||||
{
|
||||
label: 'CoreX',
|
||||
value: GPUDriverMap.ILUVATAR,
|
||||
tips: ManufacturerMap[GPUDriverMap.ILUVATAR],
|
||||
locale: true
|
||||
tipLocale: true
|
||||
},
|
||||
{
|
||||
label: 'MUSA',
|
||||
value: GPUDriverMap.MOORE_THREADS,
|
||||
tips: ManufacturerMap[GPUDriverMap.MOORE_THREADS],
|
||||
locale: true
|
||||
tipLocale: true
|
||||
},
|
||||
{
|
||||
label: 'Neuware',
|
||||
value: GPUDriverMap.CAMBRICON,
|
||||
tips: ManufacturerMap[GPUDriverMap.CAMBRICON],
|
||||
locale: true
|
||||
tipLocale: true
|
||||
},
|
||||
{
|
||||
label: 'CPU',
|
||||
@@ -191,11 +190,23 @@ export const frameworks = [
|
||||
}
|
||||
];
|
||||
|
||||
export const yamlTemplate = `# backend configuration template
|
||||
export const yamlTemplate = `# ----------------------------------------
|
||||
# custom backend configuration template
|
||||
# ----------------------------------------
|
||||
# backend_name:
|
||||
# - required
|
||||
# - must be endwith '-custom'
|
||||
# version_configs:
|
||||
# - image_name: required
|
||||
# - run_command: optional
|
||||
# - custom_framework:
|
||||
# - optional
|
||||
# - choose from: ${Object.values(GPUDriverMap).join(', ')}, CPU
|
||||
|
||||
backend_name: vllm-custom
|
||||
description: this is my custom vllm backend
|
||||
default_version: v0.11.0
|
||||
health_check_path: /${GPUSTACK_API_BASE_URL}/models
|
||||
health_check_path: /v1/models
|
||||
default_backend_param:
|
||||
- --host
|
||||
default_run_command: vllm serve {{model_path}} --port {{port}} --host {{worker_ip}} --served-model-name {{model_name}}
|
||||
|
||||
@@ -66,8 +66,7 @@ const FilterBox = styled.div`
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
padding-bottom: 16px;
|
||||
padding-top: 16px;
|
||||
margin-block: 16px;
|
||||
background-color: var(--ant-color-bg-container);
|
||||
`;
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ const VersionsForm: React.FC<AddModalProps> = ({
|
||||
<span>
|
||||
{option.label}
|
||||
{data.tips ? (
|
||||
data.locale ? (
|
||||
data.tipLocale ? (
|
||||
<span className="text-tertiary m-l-4">{` [${intl.formatMessage({ id: data.tips })}]`}</span>
|
||||
) : (
|
||||
<span className="text-tertiary m-l-4">{` [${data.tips}]`}</span>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { PageActionType } from '@/config/types';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
||||
import { Button } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useState } from 'react';
|
||||
import { ScrollerContext } from '../_components/infinite-scroller/use-scroller-context';
|
||||
@@ -155,6 +154,18 @@ const BackendList = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddVersion = () => {
|
||||
setOpenVersionInfoModal({
|
||||
open: false,
|
||||
currentData: undefined
|
||||
});
|
||||
setOpenModalStatus({
|
||||
open: true,
|
||||
action: 'edit',
|
||||
currentData: openVersionInfoModal.currentData
|
||||
});
|
||||
};
|
||||
|
||||
const loadMore = useMemoizedFn((nextPage: number) => {
|
||||
fetchData({
|
||||
query: {
|
||||
@@ -209,11 +220,9 @@ const BackendList = () => {
|
||||
})}
|
||||
title={intl.formatMessage({ id: 'noresult.backend.title' })}
|
||||
subTitle={intl.formatMessage({ id: 'noresult.backend.subTitle' })}
|
||||
>
|
||||
<Button type="primary" onClick={handleAddBackend}>
|
||||
{intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
</Button>
|
||||
</NoResult>
|
||||
onClick={handleAddBackend}
|
||||
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
></NoResult>
|
||||
</ScrollerContext.Provider>
|
||||
<AddModal
|
||||
action={openModalStatus.action}
|
||||
@@ -229,6 +238,7 @@ const BackendList = () => {
|
||||
}
|
||||
></AddModal>
|
||||
<VersionInfoModal
|
||||
addVersion={handleAddVersion}
|
||||
open={openVersionInfoModal.open}
|
||||
currentData={openVersionInfoModal.currentData as ListItem}
|
||||
onClose={() =>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { clusterSessionAtom } from '@/atoms/clusters';
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import PageBreadcrumb from '@/pages/_components/page-breadcrumb';
|
||||
import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
|
||||
import { useAtom } from 'jotai';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@@ -16,6 +18,7 @@ import FooterButtons from './components/footer-buttons';
|
||||
import ProviderCatalog from './components/provider-catalog';
|
||||
import { ProviderType, ProviderValueMap } from './config';
|
||||
import providerList from './config/providers';
|
||||
import { StepsContext } from './config/steps-context';
|
||||
import { ClusterFormData } from './config/types';
|
||||
import { moduleMap, moduleRegistry } from './step-forms/module-registry';
|
||||
import useStepList from './step-forms/use-step-list';
|
||||
@@ -39,6 +42,7 @@ const ClusterCreate = () => {
|
||||
const action =
|
||||
(searchParams.get('action') as PageActionType) || PageAction.CREATE;
|
||||
const navigate = useNavigate();
|
||||
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
|
||||
const [credentialList, setCredentialList] = useState<
|
||||
Global.BaseOption<number, { provider: ProviderType }>[]
|
||||
>([]);
|
||||
@@ -86,6 +90,7 @@ const ClusterCreate = () => {
|
||||
}));
|
||||
}, [action, extraData.provider, stepList]);
|
||||
|
||||
// before moving to the next step, get all form values
|
||||
const getFormFieldsValue = () => {
|
||||
setFormValues((prev) => {
|
||||
const newFormValues = _.cloneDeep(prev);
|
||||
@@ -203,7 +208,7 @@ const ClusterCreate = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* this function is used to render the modules in the current step
|
||||
* this function is used to render the modules in the current step, they are not forms
|
||||
* @returns
|
||||
*/
|
||||
const renderModules = () => {
|
||||
@@ -253,6 +258,14 @@ const ClusterCreate = () => {
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (clusterSession?.firstAddCluster) {
|
||||
setClusterSession({
|
||||
firstAddCluster: false,
|
||||
firstAddWorker: false
|
||||
});
|
||||
navigate(`/cluster-management/clusters/list`);
|
||||
return;
|
||||
}
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
@@ -293,10 +306,16 @@ const ClusterCreate = () => {
|
||||
current={extraData.provider}
|
||||
/>
|
||||
)}
|
||||
{renderModules()}
|
||||
<Container>
|
||||
<Content>{renderForms()}</Content>
|
||||
</Container>
|
||||
<StepsContext.Provider
|
||||
value={{
|
||||
formValues: formValues
|
||||
}}
|
||||
>
|
||||
{renderModules()}
|
||||
<Container>
|
||||
<Content>{renderForms()}</Content>
|
||||
</Container>
|
||||
</StepsContext.Provider>
|
||||
</div>
|
||||
</PageContainerInner>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expandKeysAtom } from '@/atoms/clusters';
|
||||
import { clusterSessionAtom, expandKeysAtom } from '@/atoms/clusters';
|
||||
import DeleteModal from '@/components/delete-modal';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { FilterBar } from '@/components/page-tools';
|
||||
@@ -11,7 +11,7 @@ import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import useWatchList from '@/hooks/use-watch-list';
|
||||
import { useIntl, useNavigate } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Button, message } from 'antd';
|
||||
import { message } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useEffect, useState } from 'react';
|
||||
import NoResult from '../_components/no-result';
|
||||
@@ -33,7 +33,11 @@ import {
|
||||
K8sStepsFromCluter
|
||||
} from './components/add-worker/config';
|
||||
import PoolRows from './components/pool-rows';
|
||||
import { ProviderType, ProviderValueMap } from './config';
|
||||
import {
|
||||
ClusterStatusValueMap,
|
||||
ProviderType,
|
||||
ProviderValueMap
|
||||
} from './config';
|
||||
import {
|
||||
ClusterListItem,
|
||||
CredentialListItem,
|
||||
@@ -65,6 +69,7 @@ const Clusters: React.FC = () => {
|
||||
});
|
||||
const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API);
|
||||
const [expandAtom] = useAtom(expandKeysAtom);
|
||||
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
|
||||
const { handleExpandChange, handleExpandAll, expandedRowKeys } =
|
||||
useExpandedRowKeys(expandAtom);
|
||||
const navigate = useNavigate();
|
||||
@@ -116,7 +121,7 @@ const Clusters: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickDropdown = (item: any) => {
|
||||
const handleClickDropdown = () => {
|
||||
navigate(`/cluster-management/clusters/create?action=${PageAction.CREATE}`);
|
||||
};
|
||||
|
||||
@@ -241,6 +246,35 @@ const Clusters: React.FC = () => {
|
||||
fetchCredentialList();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
clusterSession?.firstAddWorker &&
|
||||
dataSource.loadend &&
|
||||
dataSource.dataList?.length > 0
|
||||
) {
|
||||
const targetCluster = dataSource.dataList.find(
|
||||
(cluster) =>
|
||||
cluster.state === ClusterStatusValueMap.Ready &&
|
||||
!cluster.workers &&
|
||||
!cluster.worker_pools?.length
|
||||
);
|
||||
|
||||
if (targetCluster) {
|
||||
const actionMap = {
|
||||
[ProviderValueMap.Docker]: 'add_worker',
|
||||
[ProviderValueMap.Kubernetes]: 'register_cluster',
|
||||
[ProviderValueMap.DigitalOcean]: 'addPool'
|
||||
};
|
||||
handleSelect(
|
||||
actionMap[targetCluster.provider as string],
|
||||
targetCluster
|
||||
);
|
||||
// reset session
|
||||
setClusterSession(null);
|
||||
}
|
||||
}
|
||||
}, [clusterSession, dataSource.loadend, dataSource.dataList]);
|
||||
|
||||
const renderChildren = (
|
||||
list: any,
|
||||
options: { parent?: any; [key: string]: any }
|
||||
@@ -305,11 +339,9 @@ const Clusters: React.FC = () => {
|
||||
subTitle={intl.formatMessage({
|
||||
id: 'noresult.cluster.subTitle'
|
||||
})}
|
||||
>
|
||||
<Button type="primary" onClick={handleClickDropdown}>
|
||||
{intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
</Button>
|
||||
</NoResult>
|
||||
onClick={handleClickDropdown}
|
||||
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
></NoResult>
|
||||
}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
|
||||
@@ -7,12 +7,24 @@ import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ProviderType, ProviderValueMap } from '../config';
|
||||
import {
|
||||
CredentialFormData as FormData,
|
||||
CredentialListItem as ListItem
|
||||
} from '../config/types';
|
||||
|
||||
const ExtraContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 32px;
|
||||
z-index: 1;
|
||||
background: var(--ant-color-bg-container);
|
||||
padding-inline: 8px;
|
||||
`;
|
||||
type AddModalProps = {
|
||||
title: string;
|
||||
action: PageActionType;
|
||||
@@ -100,8 +112,24 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
]}
|
||||
>
|
||||
<SealInput.Password
|
||||
label={intl.formatMessage({ id: 'clusters.credential.token' })}
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.credential.token'
|
||||
})}
|
||||
required={action === PageAction.CREATE}
|
||||
description={
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: intl.formatMessage(
|
||||
{
|
||||
id: 'clusters.button.genToken'
|
||||
},
|
||||
{
|
||||
link: 'https://cloud.digitalocean.com/account/api/tokens'
|
||||
}
|
||||
)
|
||||
}}
|
||||
></span>
|
||||
}
|
||||
></SealInput.Password>
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
@@ -45,7 +45,7 @@ const SelectVendor = () => {
|
||||
updateField('currentGPU', GPUDriverMap.NVIDIA);
|
||||
updateField('workerCommand', {
|
||||
label: 'NVIDIA',
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#nvidia-cuda',
|
||||
link: 'https://docs.gpustack.ai/latest/installation/nvidia/installation/#prerequisites',
|
||||
notes: AddWorkerDockerNotes[GPUDriverMap.NVIDIA]
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -17,6 +17,7 @@ import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { CardContainer } from '@/pages/llmodels/components/gpu-card';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Button, Form } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import _ from 'lodash';
|
||||
@@ -24,6 +25,7 @@ import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react';
|
||||
import styled from 'styled-components';
|
||||
@@ -85,15 +87,6 @@ const NotFoundContent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const NotFoundImageContent = () => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<NoContent>
|
||||
{intl.formatMessage({ id: 'clusters.create.noImages' })}
|
||||
</NoContent>
|
||||
);
|
||||
};
|
||||
|
||||
const RenderLabel = (data: {
|
||||
label: React.ReactNode;
|
||||
vendor: string;
|
||||
@@ -191,37 +184,45 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (currentData) {
|
||||
// when change the region, shoudle check the instance type and the os Image.
|
||||
const selectOSImage = osImageList.find(
|
||||
(item) => item.os_image === currentData.os_image
|
||||
);
|
||||
const selectInstanceType = instanceTypeList.find(
|
||||
(item) => item.value === currentData.instance_type
|
||||
);
|
||||
form.setFieldsValue({
|
||||
...currentData
|
||||
...currentData,
|
||||
instance_type: selectInstanceType?.value || '',
|
||||
os_image: selectOSImage?.os_image || '',
|
||||
image_name: selectOSImage?.value || ''
|
||||
});
|
||||
setInstanceSpec({
|
||||
...currentData.instance_spec
|
||||
});
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
const imageLabelRender = (data: {
|
||||
label: React.ReactNode;
|
||||
value: string | number;
|
||||
}) => {
|
||||
if (action === PageAction.EDIT) {
|
||||
const vendor = _.split(currentData?.image_name || '', ' ')[0];
|
||||
const iconType = _.get(vendorIconMap, vendor.toLowerCase());
|
||||
return (
|
||||
<div className="flex-center gap-8">
|
||||
{iconType && <IconFont type={iconType}></IconFont>}
|
||||
{currentData?.image_name || currentData?.os_image}
|
||||
</div>
|
||||
);
|
||||
setInstanceSpec(() => {
|
||||
return selectInstanceType ? currentData.instance_spec : {};
|
||||
});
|
||||
}
|
||||
const selectImage = osImageList.find((item) => item.value === data.value);
|
||||
if (selectImage) {
|
||||
return (
|
||||
<RenderLabel label={data.label} vendor={selectImage.vendor || ''} />
|
||||
);
|
||||
}, [currentData, instanceTypeList, osImageList]);
|
||||
|
||||
const updateImageList = useMemoizedFn((instanceSpec: Record<string, any>) => {
|
||||
if (instanceSpec.count === 8) {
|
||||
return osImageList.filter((item) => item.os_image === 'gpu-h100x8-base');
|
||||
}
|
||||
return data.value;
|
||||
};
|
||||
|
||||
if (instanceSpec.count === 1 && instanceSpec.vendor === 'amd') {
|
||||
return osImageList.filter((item) => item.os_image === 'gpu-amd-base');
|
||||
}
|
||||
|
||||
if (instanceSpec.count === 1 && instanceSpec.vendor === 'nvidia') {
|
||||
return osImageList.filter((item) => item.os_image === 'gpu-h100x1-base');
|
||||
}
|
||||
|
||||
return osImageList;
|
||||
});
|
||||
|
||||
const imageList = useMemo(() => {
|
||||
return updateImageList(instanceSpec);
|
||||
}, [osImageList, instanceSpec, updateImageList]);
|
||||
|
||||
const instanceLabelRender = (data: {
|
||||
label: React.ReactNode;
|
||||
@@ -230,6 +231,10 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
const currentInstanceSpec =
|
||||
instanceTypeList.find((item) => item.value === data.value) ||
|
||||
instanceSpec;
|
||||
|
||||
if (!currentInstanceSpec || _.isEmpty(currentInstanceSpec)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<RenderLabel
|
||||
label={data.label || currentInstanceSpec?.label || data.value}
|
||||
@@ -238,27 +243,28 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleOsImageChange = (value: string) => {
|
||||
const handleOsImageChange = (value: string, option: any) => {
|
||||
form.setFieldsValue({
|
||||
image_name:
|
||||
osImageList.find((item) => item.value === value)?.label || value
|
||||
os_image: option.os_image || value
|
||||
});
|
||||
};
|
||||
|
||||
const handleInstanceTypeChange = (value: string, option: any) => {
|
||||
setInstanceSpec({
|
||||
const newInstanceSpec = {
|
||||
...option.specInfo,
|
||||
label: option.label,
|
||||
vendor: option.vendor,
|
||||
description: option.description
|
||||
});
|
||||
description: option.description,
|
||||
count: option.count
|
||||
};
|
||||
setInstanceSpec({ ...newInstanceSpec });
|
||||
|
||||
const newImageList = updateImageList({ ...newInstanceSpec });
|
||||
|
||||
form.setFieldsValue({
|
||||
instance_spec: {
|
||||
...option.specInfo,
|
||||
label: option.label,
|
||||
vendor: option.vendor,
|
||||
description: option.description
|
||||
}
|
||||
os_image: newImageList[0]?.os_image,
|
||||
image_name: newImageList[0]?.value,
|
||||
instance_spec: { ...newInstanceSpec }
|
||||
});
|
||||
};
|
||||
|
||||
@@ -402,7 +408,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FormData>
|
||||
name="os_image"
|
||||
name="image_name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -420,9 +426,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
styles: { header: { marginBlock: 5 } }
|
||||
})
|
||||
}
|
||||
labelRender={imageLabelRender}
|
||||
placeholder={currentData?.image_name}
|
||||
options={osImageList}
|
||||
options={imageList}
|
||||
disabled={action === PageAction.EDIT}
|
||||
label={intl.formatMessage({
|
||||
id: 'clusters.workerpool.osImage'
|
||||
@@ -462,7 +466,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
></LabelSelector>
|
||||
</Form.Item>
|
||||
<VolumesConfig disabled={action === PageAction.EDIT}></VolumesConfig>
|
||||
<Form.Item<FormData> name="image_name" hidden>
|
||||
<Form.Item<FormData> name="os_image" hidden>
|
||||
<SealInput.Input></SealInput.Input>
|
||||
</Form.Item>
|
||||
<InstanceSpecData instanceSpec={instanceSpec} />
|
||||
|
||||
@@ -57,7 +57,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
||||
key: GPUDriverMap.NVIDIA,
|
||||
locale: false,
|
||||
notes: AddWorkerDockerNotes[GPUDriverMap.NVIDIA],
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#nvidia-cuda',
|
||||
link: 'https://docs.gpustack.ai/latest/installation/nvidia/installation/#prerequisites',
|
||||
icon: <IconFont type="icon-nvidia2" style={{ fontSize: 32 }} />
|
||||
},
|
||||
{
|
||||
@@ -67,7 +67,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
||||
key: GPUDriverMap.AMD,
|
||||
locale: false,
|
||||
notes: AddWorkerDockerNotes[GPUDriverMap.AMD],
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#amd-rocm',
|
||||
link: 'https://docs.gpustack.ai/latest/installation/amd/installation/#prerequisites',
|
||||
icon: (
|
||||
<IconFont
|
||||
type="icon-amd"
|
||||
@@ -82,17 +82,17 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
||||
key: GPUDriverMap.ASCEND,
|
||||
locale: false,
|
||||
notes: AddWorkerDockerNotes[GPUDriverMap.ASCEND],
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#ascend-cann',
|
||||
link: 'https://docs.gpustack.ai/latest/installation/ascend/installation/#prerequisites',
|
||||
icon: <ProviderImage src={ascendLogo} showBg />
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'vendor.hygon' }),
|
||||
description: 'common.tag.experimental',
|
||||
description: '',
|
||||
value: GPUDriverMap.HYGON,
|
||||
key: GPUDriverMap.HYGON,
|
||||
locale: false,
|
||||
notes: AddWorkerDockerNotes[GPUDriverMap.HYGON],
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#hygon-dtk',
|
||||
link: 'https://docs.gpustack.ai/latest/installation/hygon/installation/#prerequisites',
|
||||
icon: <ProviderImage src={hyponPNG} />
|
||||
},
|
||||
{
|
||||
@@ -102,7 +102,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
||||
key: GPUDriverMap.MOORE_THREADS,
|
||||
locale: false,
|
||||
notes: AddWorkerDockerNotes[GPUDriverMap.MOORE_THREADS],
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#moore-threads-musa',
|
||||
link: 'https://docs.gpustack.ai/latest/installation/mthreads/installation/#prerequisites',
|
||||
icon: <ProviderImage src={moorePNG} />
|
||||
},
|
||||
{
|
||||
@@ -112,7 +112,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
||||
key: GPUDriverMap.ILUVATAR,
|
||||
locale: false,
|
||||
notes: AddWorkerDockerNotes[GPUDriverMap.ILUVATAR],
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#iluvatar-corex',
|
||||
link: 'https://docs.gpustack.ai/latest/installation/iluvatar/installation/#prerequisites',
|
||||
icon: <ProviderImage src={iluvatarWEBP} showBg />
|
||||
},
|
||||
{
|
||||
@@ -122,7 +122,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
||||
key: GPUDriverMap.CAMBRICON,
|
||||
locale: false,
|
||||
notes: AddWorkerDockerNotes[GPUDriverMap.CAMBRICON],
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#cambricon-mlu',
|
||||
link: 'https://docs.gpustack.ai/latest/installation/cambricon/installation/#prerequisites',
|
||||
icon: <ProviderImage src={CambriconPNG} />
|
||||
},
|
||||
{
|
||||
@@ -131,6 +131,7 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
||||
value: GPUDriverMap.METAX,
|
||||
key: GPUDriverMap.METAX,
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/metax/installation/?h=meta#prerequisites',
|
||||
notes: AddWorkerDockerNotes[GPUDriverMap.METAX],
|
||||
icon: <ProviderImage src={metaxLogo} showBg />
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export interface StepsContextProps {
|
||||
formValues: Record<string, any>;
|
||||
}
|
||||
|
||||
export const StepsContext = createContext<StepsContextProps>({
|
||||
formValues: {}
|
||||
});
|
||||
|
||||
export const useStepsContext = () => useContext(StepsContext);
|
||||
@@ -7,7 +7,7 @@ import type { PageActionType } from '@/config/types';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import { useIntl, useNavigate } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Button, ConfigProvider, Table, message } from 'antd';
|
||||
import { ConfigProvider, Table, message } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
import NoResult from '../_components/no-result';
|
||||
@@ -164,14 +164,9 @@ const Credentials: React.FC = () => {
|
||||
subTitle={intl.formatMessage({
|
||||
id: 'noresult.credentials.subTitle'
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => handleAddCredential(addActions[0])}
|
||||
>
|
||||
{intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
</Button>
|
||||
</NoResult>
|
||||
onClick={() => handleAddCredential(addActions[0])}
|
||||
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
></NoResult>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export interface StepsContextProps {
|
||||
formValues: Record<string, any>;
|
||||
}
|
||||
|
||||
export const StepsContext = createContext<StepsContextProps>({
|
||||
formValues: {}
|
||||
});
|
||||
|
||||
export const useStepsContext = () => useContext(StepsContext);
|
||||
@@ -126,6 +126,7 @@ export const useProviderRegions = () => {
|
||||
const label = formatLabel(item);
|
||||
const description = `${label} ${item.gpu_info?.count}X`;
|
||||
return {
|
||||
count: item.gpu_info?.count,
|
||||
label: `${description} - ${formatSpec(specInfo)}`,
|
||||
value: item.slug,
|
||||
description: description,
|
||||
@@ -150,7 +151,8 @@ export const useProviderRegions = () => {
|
||||
.map((item: any) => {
|
||||
return {
|
||||
label: item.description,
|
||||
value: item.slug,
|
||||
value: item.description,
|
||||
os_image: item.slug,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
vendor: _.camelCase(item.distribution),
|
||||
@@ -171,9 +173,12 @@ export const useProviderRegions = () => {
|
||||
};
|
||||
|
||||
const updateOSImages = (region: string, allImages?: any[]) => {
|
||||
const list = (allImages || allOSImageList).filter((item) =>
|
||||
item.regions.includes(region)
|
||||
const list = (allImages || allOSImageList).filter(
|
||||
(item) =>
|
||||
item.regions.includes(region) &&
|
||||
['debian', 'ubuntu'].includes(item.vendor)
|
||||
);
|
||||
console.log('osimagelist========', list);
|
||||
setOSImageList(list);
|
||||
};
|
||||
|
||||
|
||||
@@ -108,7 +108,6 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
||||
}
|
||||
return {};
|
||||
});
|
||||
console.log('gatherFormValues========', resultList);
|
||||
return resultList.filter((item) => item);
|
||||
};
|
||||
|
||||
@@ -135,7 +134,6 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
||||
const newWorkerPools = worker_pools.map(
|
||||
(poolData: NodePoolFormData, index: number) => [index, poolData]
|
||||
);
|
||||
console.log('newWorkerPools========', newWorkerPools);
|
||||
setWorkerPoolList(new Map(newWorkerPools));
|
||||
};
|
||||
|
||||
@@ -143,14 +141,12 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
||||
const values = Object.values(formRefs.current).map((form) =>
|
||||
form?.getFieldsValue()
|
||||
);
|
||||
console.log('getFieldsValue========', values);
|
||||
return {
|
||||
worker_pools: values
|
||||
};
|
||||
};
|
||||
|
||||
const handleOnToggle = (open: boolean, key: number) => {
|
||||
console.log('Active keys changed:', key);
|
||||
if (open) {
|
||||
setActiveKey((prev) => new Set([key]));
|
||||
} else {
|
||||
@@ -170,7 +166,6 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (currentData) {
|
||||
console.log('currentData===========1=', currentData);
|
||||
setFieldsValue(currentData);
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
box-shadow: none !important;
|
||||
|
||||
:global(.ant-card-body) {
|
||||
height: 110px;
|
||||
height: 96px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border-radius: var(--ant-border-radius-lg);
|
||||
border: 1px solid var(--ant-color-border);
|
||||
padding: 16px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import AlertBlockInfo from '@/components/alert-info/block';
|
||||
import TooltipList from '@/components/tooltip-list';
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import TransferInner from '@/pages/_components/transfer';
|
||||
@@ -29,7 +30,34 @@ import { AccessControlFormData, ListItem } from '../../config/types';
|
||||
|
||||
type TransferKey = string | number | bigint;
|
||||
|
||||
const accessScopeTips = [
|
||||
{
|
||||
title: {
|
||||
text: 'models.accessSettings.authed',
|
||||
locale: true
|
||||
},
|
||||
tips: 'models.accessSettings.authed.tips'
|
||||
},
|
||||
{
|
||||
title: {
|
||||
text: 'models.accessSettings.allowedUsers',
|
||||
locale: true
|
||||
},
|
||||
tips: 'models.accessSettings.allowedUsers.tips'
|
||||
},
|
||||
{
|
||||
title: {
|
||||
text: 'models.accessSettings.public',
|
||||
locale: true
|
||||
},
|
||||
tips: 'models.accessSettings.public.desc'
|
||||
}
|
||||
];
|
||||
|
||||
const Label = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
margin-block: 8px 12px;
|
||||
font-size: 14px;
|
||||
@@ -263,7 +291,13 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
|
||||
access_policy: action === PageAction.CREATE ? 'authed' : undefined
|
||||
}}
|
||||
>
|
||||
<Label>{intl.formatMessage({ id: 'models.table.accessScope' })}</Label>
|
||||
<Label>
|
||||
{intl.formatMessage({ id: 'models.table.accessScope' })}
|
||||
<Tooltip title={<TooltipList list={accessScopeTips}></TooltipList>}>
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</Label>
|
||||
|
||||
<Form.Item<AccessControlFormData> name="access_policy" noStyle>
|
||||
<Radio.Group
|
||||
onChange={handleOnPolicyChange}
|
||||
@@ -307,7 +341,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
|
||||
id: 'models.table.userSelection.tips'
|
||||
})}
|
||||
>
|
||||
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</Label>
|
||||
<Form.Item<AccessControlFormData> name="users">
|
||||
|
||||
@@ -3,7 +3,7 @@ import breakpoints from '@/config/breakpoints';
|
||||
import InfiniteScroller from '@/pages/_components/infinite-scroller';
|
||||
import { useScrollerContext } from '@/pages/_components/infinite-scroller/use-scroller-context';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Col, FloatButton, Row, Spin } from 'antd';
|
||||
import { Col, Row, Spin } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import ResizeObserver from 'rc-resize-observer';
|
||||
import React, { useCallback } from 'react';
|
||||
@@ -106,7 +106,6 @@ const CatalogList: React.FC<CatalogListProps> = (props) => {
|
||||
<ListSkeleton span={span} loading={loading} isFirst={isFirst} />
|
||||
</InfiniteScroller>
|
||||
</ResizeObserver>
|
||||
<FloatButton.BackTop visibilityHeight={1000} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -177,9 +177,9 @@ const AddModal: React.FC<AddModalProps> = (props) => {
|
||||
};
|
||||
|
||||
const initClusterId = (): number => {
|
||||
const cluster_id = clusterList?.find(
|
||||
(item) => item.state === ClusterStatusValueMap.Ready
|
||||
)?.value;
|
||||
const cluster_id =
|
||||
clusterList?.find((item) => item.state === ClusterStatusValueMap.Ready)
|
||||
?.value || clusterList?.[0]?.value;
|
||||
|
||||
return cluster_id as number;
|
||||
};
|
||||
|
||||
@@ -110,7 +110,6 @@ const AddModal: FC<AddModalProps> = (props) => {
|
||||
|
||||
const { checkOnlyAscendNPU } = useCheckBackend();
|
||||
const {
|
||||
handleShowCompatibleAlert,
|
||||
setWarningStatus,
|
||||
handleBackendChangeBefore,
|
||||
cancelEvaluate,
|
||||
@@ -258,7 +257,10 @@ const AddModal: FC<AddModalProps> = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const modelInfo = onSelectModel(selectedModel, props.source);
|
||||
const modelInfo = onSelectModel(selectedModel, {
|
||||
source: props.source,
|
||||
defaultBackend: form.current?.getFieldValue?.('backend')
|
||||
});
|
||||
|
||||
form.current?.setFieldsValue?.({
|
||||
..._.omit(modelInfo, ['name']),
|
||||
@@ -323,13 +325,17 @@ const AddModal: FC<AddModalProps> = (props) => {
|
||||
|
||||
// TODO
|
||||
form.current?.resetFields(resetFields);
|
||||
const modelInfo = onSelectModel(item, props.source);
|
||||
const modelInfo = onSelectModel(item, {
|
||||
source: props.source
|
||||
});
|
||||
form.current?.setFieldsValue?.({
|
||||
...defaultFormValues,
|
||||
...modelInfo,
|
||||
categories: getCategory(item)
|
||||
});
|
||||
|
||||
console.log('modelInfo:', modelInfo);
|
||||
|
||||
let warningStatus: MessageStatus = {
|
||||
show: true,
|
||||
title: '',
|
||||
@@ -359,7 +365,9 @@ const AddModal: FC<AddModalProps> = (props) => {
|
||||
requestModelId: updateRequestModelId()
|
||||
});
|
||||
handleCancelFiles();
|
||||
const modelInfo = onSelectModel(item, props.source);
|
||||
const modelInfo = onSelectModel(item, {
|
||||
source: props.source
|
||||
});
|
||||
|
||||
if (
|
||||
evaluateStateRef.current.state === EvaluateProccess.model &&
|
||||
@@ -435,7 +443,7 @@ const AddModal: FC<AddModalProps> = (props) => {
|
||||
}
|
||||
const cluster_id =
|
||||
clusterList?.find((item) => item.state === ClusterStatusValueMap.Ready)
|
||||
?.value || '';
|
||||
?.value || clusterList?.[0]?.value;
|
||||
|
||||
return cluster_id;
|
||||
};
|
||||
|
||||
@@ -94,6 +94,9 @@ const draftModelDownloadList: ColumnProps[] = [
|
||||
title: 'models.form.draftModel',
|
||||
locale: true,
|
||||
key: 'draft_model',
|
||||
style: {
|
||||
wordBreak: 'break-word'
|
||||
},
|
||||
width: 280
|
||||
},
|
||||
...statusColumn
|
||||
|
||||
@@ -580,7 +580,14 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<div className={SearchStyle['search-bar']}>{renderHFSearch()}</div>
|
||||
<ColumnWrapper maxHeight={'calc(100vh - 210px)'}>
|
||||
<ColumnWrapper
|
||||
maxHeight={'calc(100vh - 210px)'}
|
||||
styles={{
|
||||
container: {
|
||||
paddingTop: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SearchResult
|
||||
loading={dataSource.loading}
|
||||
resultList={dataSource.dataList}
|
||||
|
||||
@@ -429,6 +429,7 @@ const Models: React.FC<ModelsProps> = ({
|
||||
data: row
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSelect = useMemoizedFn(async (val: any, row: ListItem) => {
|
||||
try {
|
||||
if (val === 'edit') {
|
||||
@@ -716,15 +717,11 @@ const Models: React.FC<ModelsProps> = ({
|
||||
subTitle={intl.formatMessage({
|
||||
id: 'noresult.deployments.subTitle'
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
iconPosition="end"
|
||||
onClick={() => handleClickDropdown({ key: 'catalog' })}
|
||||
>
|
||||
{intl?.formatMessage?.({ id: 'models.table.button.deploy' })}
|
||||
</Button>
|
||||
</NoResult>
|
||||
onClick={() => handleClickDropdown({ key: 'catalog' })}
|
||||
buttonText={intl?.formatMessage?.({
|
||||
id: 'models.table.button.deploy'
|
||||
})}
|
||||
></NoResult>
|
||||
}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
|
||||
@@ -307,7 +307,12 @@ export interface BackendOption {
|
||||
default_backend_param: string[];
|
||||
default_version: string;
|
||||
isBuiltIn: boolean;
|
||||
versions: { label: string; value: string; title?: string }[];
|
||||
versions: {
|
||||
label: string;
|
||||
value: string;
|
||||
title?: string;
|
||||
is_deprecated: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface AccessControlFormData {
|
||||
|
||||
@@ -39,6 +39,7 @@ const BackendParametersList: React.FC = () => {
|
||||
return (
|
||||
<Form.Item<FormData> name="backend_parameters">
|
||||
<ListInput
|
||||
trim={false}
|
||||
placeholder={
|
||||
backendParamsHolderTips[backend]
|
||||
? intl.formatMessage({
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
import SealSelect from '@/components/seal-form/seal-select';
|
||||
import TooltipList from '@/components/tooltip-list';
|
||||
import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import { CaretDownOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { useIntl, useNavigate } from '@umijs/max';
|
||||
import { Form, Select } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { backendTipsList } from '../config';
|
||||
import { backendOptionsMap } from '../config/backend-parameters';
|
||||
import { useFormContext } from '../config/form-context';
|
||||
|
||||
const CaretDownWrapper = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
.anticon {
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const BackendFields: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
const form = Form.useFormInstance();
|
||||
const { onValuesChange, backendOptions, onBackendChange } = useFormContext();
|
||||
const backend = Form.useWatch('backend', form);
|
||||
const [showDeprecated, setShowDeprecated] = React.useState<boolean>(false);
|
||||
|
||||
const handleBackendVersionOnChange = (value: any) => {
|
||||
onValuesChange?.({}, form.getFieldsValue());
|
||||
@@ -45,9 +61,17 @@ const BackendFields: React.FC = () => {
|
||||
return options;
|
||||
}, [backendOptions, intl]);
|
||||
|
||||
const backendVersions = useMemo(() => {
|
||||
const backendVersions = useMemo((): {
|
||||
builtIn: any[];
|
||||
custom: any[];
|
||||
deprecated: any[];
|
||||
} => {
|
||||
if (!backend || backend === backendOptionsMap.custom) {
|
||||
return [];
|
||||
return {
|
||||
builtIn: [],
|
||||
custom: [],
|
||||
deprecated: []
|
||||
};
|
||||
}
|
||||
|
||||
// find the backend item from backendOptions
|
||||
@@ -57,34 +81,33 @@ const BackendFields: React.FC = () => {
|
||||
|
||||
// if it's a custom backend,
|
||||
if (backendItem && !backendItem.isBuiltIn) {
|
||||
return versions;
|
||||
return {
|
||||
builtIn: [],
|
||||
custom: versions.filter((item) => !item.is_deprecated),
|
||||
deprecated: versions.filter((item) => item.is_deprecated)
|
||||
};
|
||||
}
|
||||
|
||||
// check the value if endts with '-custom', if true, remove the suffix add to "Cutom" group, if not , add to "Built-in" group
|
||||
|
||||
// ============ Built-in Versions ============
|
||||
const builtInVersions = versions.filter(
|
||||
(item) => !item.value?.endsWith('-custom')
|
||||
);
|
||||
const customVersions = versions.filter((item) =>
|
||||
item.value?.endsWith('-custom')
|
||||
(item) => !item.value?.endsWith('-custom') && !item.is_deprecated
|
||||
);
|
||||
|
||||
const options = [];
|
||||
// ============ Custom Versions ============
|
||||
const customVersions = versions.filter(
|
||||
(item) => item.value?.endsWith('-custom') && !item.is_deprecated
|
||||
);
|
||||
|
||||
if (builtInVersions.length > 0) {
|
||||
options.push({
|
||||
label: intl.formatMessage({ id: 'backend.builtin' }),
|
||||
options: builtInVersions
|
||||
});
|
||||
}
|
||||
// ============ Deprecated Versions ============
|
||||
const deprecatedVersions = versions.filter((item) => item.is_deprecated);
|
||||
|
||||
if (customVersions.length > 0) {
|
||||
options.push({
|
||||
label: intl.formatMessage({ id: 'models.form.backend.custom' }),
|
||||
options: customVersions
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
return {
|
||||
builtIn: builtInVersions,
|
||||
custom: customVersions,
|
||||
deprecated: deprecatedVersions
|
||||
};
|
||||
}, [backend, backendOptions, intl]);
|
||||
|
||||
const optionRender = (option: any) => {
|
||||
@@ -95,6 +118,50 @@ const BackendFields: React.FC = () => {
|
||||
return option.title;
|
||||
};
|
||||
|
||||
const renderVersionOptions = (values: any[], label: string) => {
|
||||
if (!values || values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Select.OptGroup label={label}>
|
||||
{values.map((item) => (
|
||||
<Select.Option key={item.value} value={item.value} label={item.label}>
|
||||
{item.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select.OptGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDeprecatedVersionOptions = (values: any[]) => {
|
||||
if (!values || values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Select.OptGroup
|
||||
label={
|
||||
<CaretDownWrapper onClick={() => setShowDeprecated(!showDeprecated)}>
|
||||
{intl.formatMessage({
|
||||
id: 'models.form.backendVersion.deprecated'
|
||||
})}
|
||||
<CaretDownOutlined rotate={showDeprecated ? 0 : -90} />
|
||||
</CaretDownWrapper>
|
||||
}
|
||||
>
|
||||
{showDeprecated &&
|
||||
values.map((item) => (
|
||||
<Select.Option
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
label={item.label}
|
||||
>
|
||||
{item.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select.OptGroup>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
@@ -120,15 +187,45 @@ const BackendFields: React.FC = () => {
|
||||
<Form.Item name="backend_version">
|
||||
<SealSelect
|
||||
allowClear
|
||||
options={backendVersions}
|
||||
optionRender={optionRender}
|
||||
showSearch
|
||||
labelRender={labelRender}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'models.form.backendVersion.holder'
|
||||
})}
|
||||
onChange={handleBackendVersionOnChange}
|
||||
label={intl.formatMessage({ id: 'models.form.backendVersion' })}
|
||||
></SealSelect>
|
||||
footer={
|
||||
<dl className="flex" style={{ marginBottom: 0 }}>
|
||||
<dt>
|
||||
<InfoCircleOutlined />
|
||||
</dt>
|
||||
<dd style={{ marginLeft: 8, marginBottom: 0 }}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: 'models.form.backendVersions.tips'
|
||||
},
|
||||
{
|
||||
link: (
|
||||
<a onClick={() => navigate('/resources/backends')}>
|
||||
{intl.formatMessage({ id: 'backends.title' })}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
}
|
||||
>
|
||||
{renderVersionOptions(
|
||||
backendVersions.builtIn,
|
||||
intl.formatMessage({ id: 'backend.builtin' })
|
||||
)}
|
||||
{renderVersionOptions(
|
||||
backendVersions.custom,
|
||||
intl.formatMessage({ id: 'models.form.backend.custom' })
|
||||
)}
|
||||
{renderDeprecatedVersionOptions(backendVersions.deprecated)}
|
||||
</SealSelect>
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -51,7 +51,6 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
|
||||
item.state === ClusterStatusValueMap.Ready
|
||||
? item.label
|
||||
: `${item.label} [${ClusterStatusLabelMap[item.state as string]}]`,
|
||||
disabled: item.state !== ClusterStatusValueMap.Ready,
|
||||
value: item.value
|
||||
};
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ const CustomBackend: React.FC = () => {
|
||||
<SealInput.Input
|
||||
required
|
||||
allowClear
|
||||
onBlur={handleImageNameOnBlur}
|
||||
label={intl.formatMessage({ id: 'backend.imageName' })}
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
@@ -65,6 +66,7 @@ const CustomBackend: React.FC = () => {
|
||||
scaleSize={false}
|
||||
alwaysFocus={true}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
onBlur={handleRunCommandOnBlur}
|
||||
label={intl.formatMessage({ id: 'backend.runCommand' })}
|
||||
description={intl.formatMessage({
|
||||
id: 'backend.form.defaultExecuteCommand.tips'
|
||||
|
||||
@@ -173,6 +173,16 @@ export const useCheckCompatibility = () => {
|
||||
|
||||
const handleEvaluate = async (data: any) => {
|
||||
try {
|
||||
// when no cluster selected, show warning and prompt user to add cluster first
|
||||
if (!data.cluster_id) {
|
||||
setWarningStatus({
|
||||
show: true,
|
||||
title: '',
|
||||
type: 'warning',
|
||||
message: intl.formatMessage({ id: 'noresult.resources.cluster' })
|
||||
});
|
||||
return;
|
||||
}
|
||||
checkTokenRef.current?.cancel();
|
||||
checkTokenRef.current = createAxiosToken();
|
||||
setWarningStatus({
|
||||
@@ -407,6 +417,7 @@ export const useCheckCompatibility = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// when custom backend, and no run_command or image_name, skip evaluate
|
||||
if (
|
||||
backendOptionsMap.custom === allValues.backend &&
|
||||
(!allValues.run_command || !allValues.image_name)
|
||||
@@ -504,7 +515,11 @@ export const useSelectModel = (data: { gpuOptions: any[] }) => {
|
||||
// just for setting the model name or repo_id, and the backend, Since the model type is fixed.
|
||||
const { gpuOptions } = data;
|
||||
|
||||
const onSelectModel = (selectModel: any, source: string) => {
|
||||
const onSelectModel = (
|
||||
selectModel: any,
|
||||
options: { source: string; defaultBackend?: string }
|
||||
) => {
|
||||
const { source, defaultBackend } = options;
|
||||
let name = _.split(selectModel.name, '/').slice(-1)[0];
|
||||
const reg = /(-gguf)$/i;
|
||||
name = _.toLower(name).replace(reg, '');
|
||||
@@ -512,7 +527,7 @@ export const useSelectModel = (data: { gpuOptions: any[] }) => {
|
||||
const modelTaskData = recognizeAudioModel(selectModel, source);
|
||||
|
||||
const backend = checkCurrentbackend({
|
||||
defaultBackend: backendOptionsMap.vllm,
|
||||
defaultBackend: defaultBackend || backendOptionsMap.vllm,
|
||||
isAudio: modelTaskData.type === modelTaskMap.audio,
|
||||
isGGUF: selectModel.isGGUF,
|
||||
gpuOptions: gpuOptions
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import DropdownButtons from '@/components/drop-down-buttons';
|
||||
import { SealColumnProps } from '@/components/seal-table/types';
|
||||
import { GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||
import { OPENAI_COMPATIBLE } from '@/config/settings';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Tooltip } from 'antd';
|
||||
@@ -94,7 +94,7 @@ const useModelsColumns = ({
|
||||
<Tooltip
|
||||
title={intl.formatMessage(
|
||||
{ id: 'models.form.replicas.tips' },
|
||||
{ api: `${window.location.origin}/${GPUSTACK_API_BASE_URL}` }
|
||||
{ api: `${window.location.origin}/${OPENAI_COMPATIBLE}` }
|
||||
)}
|
||||
>
|
||||
<span style={{ fontWeight: 'var(--font-weight-medium)' }}>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { clusterSessionAtom } from '@/atoms/clusters';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { PageAction } from '@/config';
|
||||
import NoResult from '@/pages/_components/no-result';
|
||||
import { useIntl, useNavigate } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { useAtom } from 'jotai';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
const useNoResourceResult = (props: {
|
||||
iconType: string;
|
||||
loading?: boolean;
|
||||
loadend?: boolean;
|
||||
dataSource?: any[];
|
||||
queryParams?: Record<string, any>;
|
||||
title: React.ReactNode;
|
||||
noClusters?: boolean;
|
||||
noWorkers?: boolean;
|
||||
subTitle?: React.ReactNode;
|
||||
defaultContent?: {
|
||||
subTitle: string;
|
||||
noFoundText: string;
|
||||
buttonText: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
noClusters,
|
||||
noWorkers,
|
||||
defaultContent,
|
||||
loading,
|
||||
loadend,
|
||||
dataSource,
|
||||
queryParams,
|
||||
iconType,
|
||||
title,
|
||||
subTitle
|
||||
} = props;
|
||||
const [, setClusterSession] = useAtom(clusterSessionAtom);
|
||||
|
||||
const handleClick = useMemoizedFn(() => {
|
||||
if (noClusters) {
|
||||
setClusterSession({
|
||||
firstAddWorker: false,
|
||||
firstAddCluster: true
|
||||
});
|
||||
|
||||
navigate(
|
||||
`/cluster-management/clusters/create?action=${PageAction.CREATE}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (noWorkers) {
|
||||
setClusterSession({
|
||||
firstAddWorker: true,
|
||||
firstAddCluster: false
|
||||
});
|
||||
navigate(`/cluster-management/clusters/list`);
|
||||
}
|
||||
});
|
||||
|
||||
const statusContent = useMemo(() => {
|
||||
if (noClusters) {
|
||||
return {
|
||||
subTitle:
|
||||
subTitle || intl.formatMessage({ id: 'noresult.resources.cluster' }),
|
||||
noFoundText: defaultContent?.noFoundText || '',
|
||||
buttonText: intl.formatMessage({
|
||||
id: 'noresult.resources.gotocluster'
|
||||
}),
|
||||
onClick: handleClick
|
||||
};
|
||||
}
|
||||
|
||||
if (noWorkers) {
|
||||
return {
|
||||
subTitle:
|
||||
subTitle || intl.formatMessage({ id: 'noresult.resources.worker' }),
|
||||
noFoundText: defaultContent?.noFoundText || '',
|
||||
buttonText: intl.formatMessage({ id: 'noresult.resources.gotoworker' }),
|
||||
onClick: handleClick
|
||||
};
|
||||
}
|
||||
return {
|
||||
...defaultContent
|
||||
};
|
||||
}, [noClusters, noWorkers, intl]);
|
||||
|
||||
const noResourceResult = (
|
||||
<NoResult
|
||||
loading={loading}
|
||||
loadend={loadend}
|
||||
dataSource={dataSource}
|
||||
image={<IconFont type={iconType} />}
|
||||
filters={queryParams}
|
||||
noFoundText={statusContent.noFoundText}
|
||||
title={title}
|
||||
subTitle={statusContent.subTitle}
|
||||
onClick={statusContent.onClick}
|
||||
buttonText={statusContent.buttonText}
|
||||
></NoResult>
|
||||
);
|
||||
return {
|
||||
noResourceResult
|
||||
};
|
||||
};
|
||||
|
||||
export default useNoResourceResult;
|
||||
@@ -26,9 +26,10 @@ export default function useQueryBackends() {
|
||||
default_backend_param: item.default_backend_param || [],
|
||||
default_version: item.default_version,
|
||||
isBuiltIn: item.is_built_in,
|
||||
versions: (item.versions || []).map((vItem) => ({
|
||||
versions: (item.versions || []).map((vItem, index) => ({
|
||||
label: vItem.version,
|
||||
value: vItem.version,
|
||||
is_deprecated: vItem.is_deprecated,
|
||||
title: vItem.version.replace(/-custom$/, '')
|
||||
}))
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ const useStyles = createStyles(({ token, css }) => ({
|
||||
const LoginForm = () => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const { styles } = useStyles();
|
||||
const [userInfo, setUserInfo] = useAtom(userAtom);
|
||||
const [, setUserInfo] = useAtom(userAtom);
|
||||
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
||||
const [authError, setAuthError] = useState<Error | null>(null);
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -34,7 +34,7 @@ const PasswordForm: React.FC = () => {
|
||||
});
|
||||
|
||||
await setUserInfo({
|
||||
...userInfo,
|
||||
...(userInfo || {}),
|
||||
require_password_change: false
|
||||
});
|
||||
setInitialPassword('');
|
||||
|
||||
@@ -201,9 +201,11 @@ const GroundSTT: React.FC<MessageProps> = forwardRef((props, ref) => {
|
||||
|
||||
const handleUploadChange = useCallback(
|
||||
async (data: { file: any; fileList: any }) => {
|
||||
const res = await readAudioFile(data.file);
|
||||
setAudioData(res);
|
||||
setTokenResult(null);
|
||||
try {
|
||||
const res = await readAudioFile(data.file);
|
||||
setAudioData(res);
|
||||
setTokenResult(null);
|
||||
} catch (error) {}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -28,9 +28,9 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import styled from 'styled-components';
|
||||
import { Roles } from '../config';
|
||||
import { AudioFormat, MessageItem } from '../config/types';
|
||||
import useAddImage from '../hooks/use-add-image';
|
||||
import '../style/message-input.less';
|
||||
import ThumbImg from './thumb-img';
|
||||
import UploadImg from './upload-img';
|
||||
|
||||
const AudioWrapper = styled.div`
|
||||
padding-block: 10px;
|
||||
@@ -163,8 +163,11 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
||||
content: '',
|
||||
imgs: []
|
||||
});
|
||||
const [isFromUrl, setIsFromUrl] = useState(false);
|
||||
const [openImgTips, setOpenImgTips] = useState(false);
|
||||
const uidCountRef = useRef(0);
|
||||
const inputRef = useRef<any>(null);
|
||||
const inputImgRef = useRef<any>(null);
|
||||
|
||||
const updateUidCount = () => {
|
||||
uidCountRef.current += 1;
|
||||
@@ -358,6 +361,11 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
||||
}
|
||||
};
|
||||
|
||||
const { ImageURLInput, UploadImageButton } = useAddImage({
|
||||
handleUpdateImgList: handleUpdateImgList,
|
||||
updateUidCount: updateUidCount
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleInputChange: handleInputChange
|
||||
}));
|
||||
@@ -411,12 +419,10 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
||||
{checkLabel}
|
||||
</Checkbox>
|
||||
)}
|
||||
{actions.includes('upload') && message.role === Roles.User && (
|
||||
<UploadImg
|
||||
handleUpdateImgList={handleUpdateImgList}
|
||||
size="middle"
|
||||
></UploadImg>
|
||||
)}
|
||||
{actions.includes('upload') &&
|
||||
message.role === Roles.User &&
|
||||
UploadImageButton}
|
||||
|
||||
{actions.includes('upload') && message.role === Roles.User && (
|
||||
<UploadAudio
|
||||
maxFileSize={1024 * 1024}
|
||||
@@ -462,6 +468,7 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{ImageURLInput}
|
||||
</div>
|
||||
<div className="actions">
|
||||
{actions.includes('add') && (
|
||||
@@ -533,6 +540,7 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
||||
</AudioWrapper>
|
||||
)}
|
||||
</ImgsWrapper>
|
||||
|
||||
<div className="input-box">
|
||||
{actions.includes('paste') ? (
|
||||
<TextArea
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { Roles } from '../../config';
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
MessageItem,
|
||||
MessageItemAction
|
||||
} from '../../config/types';
|
||||
import UploadImg from '../upload-img';
|
||||
import useAddImage from '../../hooks/use-add-image';
|
||||
|
||||
interface MessageActionsProps {
|
||||
data: MessageItem;
|
||||
@@ -57,32 +58,47 @@ const MessageActions: React.FC<MessageActionsProps> = ({
|
||||
file: any;
|
||||
fileList: any[];
|
||||
}) => {
|
||||
const base64Audio = await convertFileToBase64(audio.file);
|
||||
const audioData = await readAudioFile(audio.file);
|
||||
updateMessage?.({
|
||||
role: data.role,
|
||||
content: data.content,
|
||||
uid: data.uid,
|
||||
imgs: data.imgs || [],
|
||||
audio: [
|
||||
{
|
||||
uid: audio.file.uid || audio.fileList[0].uid,
|
||||
format: audioTypeMap[audio.file.type] as AudioFormat,
|
||||
base64: base64Audio.split(',')[1],
|
||||
data: _.pick(audioData, ['url', 'name', 'duration'])
|
||||
}
|
||||
]
|
||||
});
|
||||
try {
|
||||
const base64Audio = await convertFileToBase64(audio.file);
|
||||
const audioData = await readAudioFile(audio.file);
|
||||
updateMessage?.({
|
||||
role: data.role,
|
||||
content: data.content,
|
||||
uid: data.uid,
|
||||
imgs: data.imgs || [],
|
||||
audio: [
|
||||
{
|
||||
uid: audio.file.uid || audio.fileList[0].uid,
|
||||
format: audioTypeMap[audio.file.type] as AudioFormat,
|
||||
base64: base64Audio.split(',')[1],
|
||||
data: _.pick(audioData, ['url', 'name', 'duration'])
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('error uploading audio file', error);
|
||||
}
|
||||
};
|
||||
|
||||
const { ImageURLInput, UploadImageButton, isFromUrl } = useAddImage({
|
||||
size: 'small',
|
||||
handleUpdateImgList: handleUpdateImgList,
|
||||
updateUidCount: () => `img-${Date.now()}`
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{ImageURLInput}
|
||||
{actions.length > 1 && !loading ? (
|
||||
<div className="actions">
|
||||
<div
|
||||
className={classNames('actions', {
|
||||
'has-url-input': isFromUrl
|
||||
})}
|
||||
>
|
||||
<div className="actions-wrap gap-5">
|
||||
{actions.includes('upload') && data.role === Roles.User && (
|
||||
<UploadImg handleUpdateImgList={handleUpdateImgList} />
|
||||
)}
|
||||
{actions.includes('upload') &&
|
||||
data.role === Roles.User &&
|
||||
UploadImageButton}
|
||||
{actions.includes('upload') && data.role === Roles.User && (
|
||||
<UploadAudio
|
||||
type="text"
|
||||
|
||||
@@ -16,10 +16,8 @@ import ThumbImg from '../thumb-img';
|
||||
import ThinkContent from './think-content';
|
||||
|
||||
const AudioWrapper = styled.div`
|
||||
padding-top: 10px;
|
||||
height: max-content;
|
||||
width: max-content;
|
||||
margin-inline: 10px;
|
||||
`;
|
||||
|
||||
const ThumbImgWrapper = styled.div.attrs({
|
||||
@@ -29,6 +27,12 @@ const ThumbImgWrapper = styled.div.attrs({
|
||||
justify-content: flex-start;
|
||||
overflow-x: auto;
|
||||
flex-direction: column;
|
||||
padding: 0px;
|
||||
gap: 8px;
|
||||
&.has-content {
|
||||
padding-inline: 8px;
|
||||
padding-block: 8px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
interface MessageBodyProps {
|
||||
@@ -179,31 +183,6 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteLastImage = useCallback(() => {
|
||||
if (data.imgs && data.imgs?.length > 0) {
|
||||
const newImgList = [...(data.imgs || [])];
|
||||
const lastImage = newImgList.pop();
|
||||
if (lastImage) {
|
||||
handleDeleteImg(lastImage.uid);
|
||||
}
|
||||
}
|
||||
}, [data.imgs, handleDeleteImg]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: any) => {
|
||||
if (
|
||||
event.key === 'Backspace' &&
|
||||
data.content === '' &&
|
||||
data.imgs &&
|
||||
data.imgs?.length > 0
|
||||
) {
|
||||
// inputref blur
|
||||
event.preventDefault();
|
||||
handleDeleteLastImage();
|
||||
}
|
||||
},
|
||||
[data, handleDeleteLastImage]
|
||||
);
|
||||
const handleClickWrapper = (e: any) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -218,7 +197,7 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
||||
data.imgs?.length || (data.audio && data.audio?.length > 0)
|
||||
})}
|
||||
>
|
||||
<div className="justify-start ">
|
||||
<div className="justify-start">
|
||||
<ThumbImg
|
||||
editable={editable}
|
||||
dataList={data.imgs || []}
|
||||
@@ -249,7 +228,12 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
||||
})}
|
||||
onClick={handleClickWrapper}
|
||||
>
|
||||
<ThumbImgWrapper>
|
||||
<ThumbImgWrapper
|
||||
className={classNames({
|
||||
'has-content':
|
||||
data.imgs?.length || (data.audio && data.audio?.length > 0)
|
||||
})}
|
||||
>
|
||||
<ThumbImg
|
||||
style={{ paddingBlockEnd: 0 }}
|
||||
editable={editable}
|
||||
@@ -257,7 +241,7 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
||||
onDelete={handleDeleteImg}
|
||||
/>
|
||||
{data.audio && data.audio.length > 0 && (
|
||||
<AudioWrapper className={data.imgs?.length ? '' : 'm-l-10'}>
|
||||
<AudioWrapper>
|
||||
<SimpleAudio
|
||||
url={data.audio?.[0]?.data.url}
|
||||
name={data.audio?.[0]?.data.name}
|
||||
|
||||
@@ -184,4 +184,4 @@ const UploadImg: React.FC<UploadImgProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UploadImg);
|
||||
export default UploadImg;
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import DropDownActions from '@/components/drop-down-actions';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
LinkOutlined,
|
||||
PictureOutlined,
|
||||
UploadOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Input, Tooltip } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import UploadImg from '../components/upload-img';
|
||||
|
||||
const ImgInputWrapper = styled.div`
|
||||
position: relative;
|
||||
.del-btn {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--ant-color-bg-container);
|
||||
}
|
||||
&:hover {
|
||||
.del-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const useAddImage = (options: {
|
||||
size?: 'small' | 'middle' | 'large';
|
||||
handleUpdateImgList: (
|
||||
list: { uid: number | string; dataUrl: string }[]
|
||||
) => void;
|
||||
updateUidCount: () => number | string;
|
||||
}) => {
|
||||
const { handleUpdateImgList, updateUidCount, size = 'middle' } = options;
|
||||
const intl = useIntl();
|
||||
const [isFromUrl, setIsFromUrl] = useState(false);
|
||||
const [openImgTips, setOpenImgTips] = useState(false);
|
||||
const inputImgRef = useRef<any>(null);
|
||||
|
||||
const handleAddImgFromUrl = () => {
|
||||
setIsFromUrl(true);
|
||||
setTimeout(() => {
|
||||
inputImgRef.current?.focus?.();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleInputImageUrl = async (e: any) => {
|
||||
const url = e.target.value?.trim();
|
||||
|
||||
if (url) {
|
||||
handleUpdateImgList([
|
||||
{
|
||||
uid: updateUidCount(),
|
||||
dataUrl: url
|
||||
}
|
||||
]);
|
||||
setOpenImgTips(false);
|
||||
setIsFromUrl(false);
|
||||
} else {
|
||||
setOpenImgTips(true);
|
||||
}
|
||||
// set openImgTips to false after next frame if is not valid
|
||||
if (!url) {
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
setOpenImgTips(false);
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsFromUrl(false);
|
||||
setOpenImgTips(false);
|
||||
};
|
||||
|
||||
const handleOnEscape = (e: any) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const ImageURLInput = isFromUrl ? (
|
||||
<Tooltip
|
||||
open={openImgTips}
|
||||
title={intl.formatMessage({
|
||||
id: 'playground.uploadImage.url.invalid'
|
||||
})}
|
||||
>
|
||||
<ImgInputWrapper>
|
||||
<Input
|
||||
ref={inputImgRef}
|
||||
status={openImgTips ? 'error' : ''}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'playground.uploadImage.url.holder'
|
||||
})}
|
||||
style={{ width: 360, height: 32 }}
|
||||
onBlur={handleInputImageUrl}
|
||||
onPressEnter={handleInputImageUrl}
|
||||
onKeyDown={handleOnEscape}
|
||||
></Input>
|
||||
<div className="del-btn">
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
type="text"
|
||||
></Button>
|
||||
</div>
|
||||
</ImgInputWrapper>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
|
||||
const UploadImageButton = (
|
||||
<DropDownActions
|
||||
placement={'topLeft'}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: (
|
||||
<UploadImg
|
||||
handleUpdateImgList={handleUpdateImgList}
|
||||
size="middle"
|
||||
>
|
||||
<UploadOutlined className="m-r-8" />
|
||||
{intl.formatMessage({ id: 'playground.img.upload' })}
|
||||
</UploadImg>
|
||||
),
|
||||
key: 'upload_image'
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'playground.uploadImage.url.button'
|
||||
}),
|
||||
key: 'add_image_url',
|
||||
icon: <LinkOutlined />,
|
||||
onClick: handleAddImgFromUrl
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
<Button type="text" size={size} icon={<PictureOutlined />}></Button>
|
||||
</DropDownActions>
|
||||
);
|
||||
|
||||
return {
|
||||
isFromUrl,
|
||||
ImageURLInput,
|
||||
UploadImageButton
|
||||
};
|
||||
};
|
||||
|
||||
export default useAddImage;
|
||||
@@ -156,11 +156,15 @@ const Playground: React.FC = () => {
|
||||
activeKey={activeKey}
|
||||
key="view-code-buttons"
|
||||
/>,
|
||||
<Divider
|
||||
key="divider"
|
||||
type="vertical"
|
||||
style={{ height: 24, marginInline: 16 }}
|
||||
/>,
|
||||
<div key="divider-wrapper">
|
||||
{activeKey === 'chat' && (
|
||||
<Divider
|
||||
key="divider"
|
||||
type="vertical"
|
||||
style={{ height: 24, marginInline: 16 }}
|
||||
/>
|
||||
)}
|
||||
</div>,
|
||||
<ExtraContent key="extra-content" />
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
background-color: var(--color-white-1);
|
||||
|
||||
&.has-url-input {
|
||||
display: flex;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions-wrap {
|
||||
|
||||
@@ -58,5 +58,4 @@
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import DeleteModal from '@/components/delete-modal';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { FilterBar } from '@/components/page-tools';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import PageBox from '@/pages/_components/page-box';
|
||||
@@ -11,11 +10,11 @@ import {
|
||||
} from '@/pages/cluster-management/config';
|
||||
import { ClusterListItem } from '@/pages/cluster-management/config/types';
|
||||
import useAddWorker from '@/pages/cluster-management/hooks/use-add-worker';
|
||||
import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Button, ConfigProvider, Table, message } from 'antd';
|
||||
import { ConfigProvider, Table, message } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import NoResult from '../../_components/no-result';
|
||||
import {
|
||||
WORKERS_API,
|
||||
deleteWorker,
|
||||
@@ -191,24 +190,26 @@ const Workers: React.FC = () => {
|
||||
handleAddWorker(currentData as ClusterListItem);
|
||||
};
|
||||
|
||||
const { noResourceResult } = useNoResourceResult({
|
||||
loadend: dataSource.loadend,
|
||||
loading: dataSource.loading,
|
||||
dataSource: dataSource.dataList,
|
||||
queryParams: queryParams,
|
||||
iconType: 'icon-resources',
|
||||
title: intl.formatMessage({ id: 'noresult.workers.title' }),
|
||||
noClusters: !clusterData.list.length,
|
||||
noWorkers: dataSource.dataList.length === 0 && clusterData.list.length > 0,
|
||||
defaultContent: {
|
||||
subTitle: intl.formatMessage({ id: 'noresult.workers.subTitle' }),
|
||||
noFoundText: intl.formatMessage({ id: 'noresult.workers.nofound' }),
|
||||
buttonText: intl.formatMessage({ id: 'noresult.workers.button.add' }),
|
||||
onClick: handleOnAddWorker
|
||||
}
|
||||
});
|
||||
|
||||
const renderEmpty = (type?: string) => {
|
||||
if (type !== 'Table') return;
|
||||
return (
|
||||
<NoResult
|
||||
loading={dataSource.loading}
|
||||
loadend={dataSource.loadend}
|
||||
dataSource={dataSource.dataList}
|
||||
image={<IconFont type="icon-resources" />}
|
||||
filters={queryParams}
|
||||
noFoundText={intl.formatMessage({ id: 'noresult.workers.nofound' })}
|
||||
title={intl.formatMessage({ id: 'noresult.workers.title' })}
|
||||
subTitle={intl.formatMessage({ id: 'noresult.workers.subTitle' })}
|
||||
>
|
||||
<Button type="primary" onClick={handleOnAddWorker}>
|
||||
{intl.formatMessage({ id: 'noresult.workers.button.add' })}
|
||||
</Button>
|
||||
</NoResult>
|
||||
);
|
||||
return noResourceResult;
|
||||
};
|
||||
|
||||
const handleClusterChange = (value: number) => {
|
||||
|
||||
@@ -57,7 +57,7 @@ export const GPUsConfigs: Record<
|
||||
[GPUDriverMap.ILUVATAR]: {
|
||||
label: ManufacturerMap[GPUDriverMap.ILUVATAR],
|
||||
value: GPUDriverMap.ILUVATAR,
|
||||
runtime: 'iluvatar', // TODO: confirm runtime name
|
||||
runtime: 'iluvatar',
|
||||
driver: 'ixsmi'
|
||||
},
|
||||
[GPUDriverMap.CAMBRICON]: {
|
||||
@@ -142,7 +142,7 @@ const setImageArgs = (params: any) => {
|
||||
--token ${params.token} \\`;
|
||||
};
|
||||
|
||||
// avaliable for NVIDIA、AMD、MThreads
|
||||
// avaliable for NVIDIA、MThreads
|
||||
const registerWorker = (params: {
|
||||
server: string;
|
||||
tag: string;
|
||||
@@ -162,6 +162,27 @@ const registerWorker = (params: {
|
||||
${params.workerIP ? `--advertise-address ${params.workerIP} \\` : ''}`;
|
||||
};
|
||||
|
||||
// avaliable for AMD
|
||||
const registerAMDWorker = (params: {
|
||||
server: string;
|
||||
tag: string;
|
||||
token: string;
|
||||
image: string;
|
||||
gpu: string;
|
||||
workerIP?: string;
|
||||
modelDir?: string;
|
||||
}) => {
|
||||
const config = GPUsConfigs[params.gpu];
|
||||
const commonArgs = setNormalArgs(params);
|
||||
const imageArgs = setImageArgs(params);
|
||||
// remove empty enter lines and trailing backslash
|
||||
return `${commonArgs}
|
||||
--volume /opt/rocm:/opt/rocm:ro \\
|
||||
--runtime ${config.runtime} \\
|
||||
${imageArgs}
|
||||
${params.workerIP ? `--advertise-address ${params.workerIP} \\` : ''}`;
|
||||
};
|
||||
|
||||
// avaliable for Ascend
|
||||
const registerAscendWorker = (params: {
|
||||
server: string;
|
||||
@@ -264,7 +285,7 @@ const registerCambriconWorker = (params: {
|
||||
|
||||
export const registerAddWokerCommandMap = {
|
||||
[GPUDriverMap.NVIDIA]: registerWorker,
|
||||
[GPUDriverMap.AMD]: registerWorker,
|
||||
[GPUDriverMap.AMD]: registerAMDWorker,
|
||||
[GPUDriverMap.ASCEND]: registerAscendWorker,
|
||||
[GPUDriverMap.HYGON]: registerHygonWorker,
|
||||
[GPUDriverMap.ILUVATAR]: registerIluvatarWorker,
|
||||
@@ -275,7 +296,7 @@ export const registerAddWokerCommandMap = {
|
||||
|
||||
export const AddWorkerDockerNotes: Record<string, string[]> = {
|
||||
[GPUDriverMap.NVIDIA]: [],
|
||||
[GPUDriverMap.AMD]: [],
|
||||
[GPUDriverMap.AMD]: ['clusters.addworker.amdNotes-01'],
|
||||
[GPUDriverMap.MOORE_THREADS]: [],
|
||||
[GPUDriverMap.ASCEND]: [],
|
||||
[GPUDriverMap.HYGON]: [
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { PageActionType } from '@/config/types';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import { useIntl, useModel } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Button, ConfigProvider, message, Table } from 'antd';
|
||||
import { ConfigProvider, message, Table } from 'antd';
|
||||
import { useMemo, useState } from 'react';
|
||||
import NoResult from '../_components/no-result';
|
||||
import PageBox from '../_components/page-box';
|
||||
@@ -150,11 +150,9 @@ const Users: React.FC = () => {
|
||||
})}
|
||||
title={intl.formatMessage({ id: 'noresult.users.title' })}
|
||||
subTitle={intl.formatMessage({ id: 'noresult.users.subTitle' })}
|
||||
>
|
||||
<Button type="primary" onClick={handleAddUser}>
|
||||
{intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
</Button>
|
||||
</NoResult>
|
||||
onClick={handleAddUser}
|
||||
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
></NoResult>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,50 +28,43 @@ export const loadAudioData = async (
|
||||
url: string;
|
||||
}> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const audioBlob = new Blob([data], { type: type });
|
||||
const fileSize = convertFileSize(audioBlob.size);
|
||||
const audioBlob = new Blob([data], { type: type });
|
||||
const fileSize = convertFileSize(audioBlob.size);
|
||||
|
||||
const audio = document.createElement('audio');
|
||||
const url = URL.createObjectURL(audioBlob);
|
||||
audio.src = url;
|
||||
const audio = document.createElement('audio');
|
||||
const url = URL.createObjectURL(audioBlob);
|
||||
audio.src = url;
|
||||
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
const duration = audio.duration;
|
||||
resolve({
|
||||
data: audioBlob,
|
||||
size: fileSize,
|
||||
type: type,
|
||||
duration: Math.ceil(duration),
|
||||
url: url
|
||||
});
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
const duration = audio.duration;
|
||||
resolve({
|
||||
data: audioBlob,
|
||||
size: fileSize,
|
||||
type: type,
|
||||
duration: Math.ceil(duration),
|
||||
url: url
|
||||
});
|
||||
});
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
URL.revokeObjectURL(audio.src);
|
||||
});
|
||||
audio.addEventListener('ended', () => {
|
||||
URL.revokeObjectURL(audio.src);
|
||||
});
|
||||
|
||||
audio.addEventListener('error', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
message.error('Failed to load audio metadata invalid file');
|
||||
reject(new Error('Failed to load audio metadata invalid file'));
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('error====', error);
|
||||
reject(error);
|
||||
}
|
||||
audio.addEventListener('error', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
message.error('Failed to load audio metadata invalid file');
|
||||
reject(new Error('Failed to load audio metadata invalid file'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const readAudioFile = async (
|
||||
file: File
|
||||
): Promise<{ url: string; name: string; duration: number }> => {
|
||||
console.log('file====', file);
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async function (e: any) {
|
||||
try {
|
||||
console.log('file====', file);
|
||||
const arrayBuffer = e.target.result;
|
||||
const audioData = await loadAudioData(arrayBuffer, file.type);
|
||||
resolve({
|
||||
@@ -79,7 +72,6 @@ export const readAudioFile = async (
|
||||
name: file.name
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('error====', error);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user