Compare commits

..
29 Commits
Author SHA1 Message Date
jialin daa10617c0 chore: prefetch chunk file 2025-12-05 13:31:56 +08:00
jialin c3b5c13cc5 fix: show info when adding worker error 2025-12-05 11:45:44 +08:00
jialin 1dd83645bd fix: reset first login flag after changing password 2025-12-05 11:26:32 +08:00
jialin 2df7c17bc1 chore: remove experimental from hygon 2025-12-04 22:35:15 +08:00
jialin a274349ace chore: upgrade info 2025-12-04 16:43:33 +08:00
jialin 1c699fe307 chore: tidy code 2025-12-04 16:43:33 +08:00
jialin f0a3e84e4f fix: set default os image 2025-12-04 10:48:34 +08:00
jialin f87989d32e fix: clear os_image when it's not in new image list 2025-12-04 10:12:19 +08:00
jialin 7355be9e3a fix: init cluster id when deploy model 2025-12-03 19:18:07 +08:00
jialin d8f1e95e1a fix: use image label in inputbox 2025-12-03 16:00:59 +08:00
jialin 59eace702b fix: add worker init steps failed 2025-12-03 12:32:04 +08:00
jialin 8a24ba0252 fix: cluster option disabled 2025-12-03 11:50:35 +08:00
jialin 03999a9d2c chore: os image filter 2025-12-03 11:38:16 +08:00
jialin f335cb1775 fix: click icon do not worker on upload 2025-12-02 17:24:14 +08:00
jialin b9c034469b fix: input img url fall back image 2025-12-02 15:58:17 +08:00
jialin ce3a82ddda fix: edit message add image 2025-12-02 14:00:58 +08:00
jialin 5cbb38922c feat: add image from url 2025-12-01 19:01:33 +08:00
jialin 14f0afec08 chore: allow space in backend parameters 2025-12-01 10:06:00 +08:00
jialin 769cc09906 fix: logs are not fully loaded 2025-11-30 20:33:16 +08:00
jialin 306daf7591 chore: amd note for add worker 2025-11-27 19:35:04 +08:00
jialin d1c1132e6e style: button for empty 2025-11-27 15:53:23 +08:00
jialin 962df859f1 chore: get started for add cluster 2025-11-27 11:35:45 +08:00
jialin 97601280d1 fix: model download progress text overflow 2025-11-26 10:28:40 +08:00
jialin ede42e99de fix: do not reset the backend after selecting a gguf file 2025-11-25 17:20:09 +08:00
jialin 47cae0134c feat: display deprecated backend versions group 2025-11-25 16:04:27 +08:00
jialin 33defab7df fix: gpus link 2025-11-25 11:34:11 +08:00
EVGENY Mandjialin 6b402e944b Update Russian translations in models.ts 2025-11-24 09:38:32 +08:00
EVGENY Mandjialin f8a6f4c5e5 Translate cluster messages to Russian 2025-11-24 09:38:14 +08:00
EVGENY Mandjialin adb999d3a9 Update dashboard.ts 2025-11-24 09:37:42 +08:00
94 changed files with 1299 additions and 765 deletions
+3 -1
View File
@@ -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);
@@ -79,6 +78,9 @@ export default defineConfig({
model: {},
initialState: {},
request: {},
routePrefetch: {
defaultPrefetch: 'intent'
},
keepalive: keepAlive,
locale: {
antd: true,
+7 -2
View File
@@ -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);
+2
View File
@@ -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
View File
@@ -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<{
+3 -2
View File
@@ -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;
+5 -7
View File
@@ -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;
+4 -3
View File
@@ -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}
/>
);
})}
+12 -2
View File
@@ -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;
+3 -3
View File
@@ -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>
+57 -41
View File
@@ -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;
+1
View File
@@ -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}
>
+3 -1
View File
@@ -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 -2
View File
@@ -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>
);
};
+1 -1
View File
@@ -87,4 +87,4 @@ const UploadAudio: React.FC<UploadAudioProps> = (props) => {
);
};
export default React.memo(UploadAudio);
export default UploadAudio;
+21 -17
View File
@@ -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 }
+1 -1
View File
@@ -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>;
+1 -1
View File
@@ -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'
};
+4 -4
View File
@@ -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
View File
@@ -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>
+1
View File
@@ -164,6 +164,7 @@ const SiderMenu: React.FC<SiderMenuProps> = (props) => {
key={key}
>
<Link
prefetch="intent"
to={menuItem.path.replace('/*', '')}
target={menuItem.target}
className={cx(styles.menuItemWrapper, 'menu-item', {
+4 -1
View File
@@ -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>.`
};
+12 -4
View File
@@ -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.`
};
+7 -1
View File
@@ -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'
};
+5 -1
View File
@@ -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'
};
+7 -2
View File
@@ -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 ==========
+18 -5
View File
@@ -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 ==========
+7 -1
View File
@@ -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'
};
+8 -1
View File
@@ -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 ==========
+27 -43
View File
@@ -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>.',
// ================================================================
+2 -2
View File
@@ -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
View File
@@ -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 ==========
+18 -3
View File
@@ -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'
// ========================================================================
+11 -3
View File
@@ -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 ==========
+5 -1
View File
@@ -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>。'
};
+9 -4
View File
@@ -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}页面并编辑对应的后端以添加版本。`
};
+5 -1
View File
@@ -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': '添加节点'
};
+6 -1
View File
@@ -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;
+22 -3
View File
@@ -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>
+4 -6
View File
@@ -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} />
+20 -9
View File
@@ -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}}
+1 -2
View File
@@ -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);
`;
+1 -1
View File
@@ -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>
+16 -6
View File
@@ -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>
);
+41 -9
View File
@@ -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);
+4 -9
View File
@@ -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;
};
+13 -5
View File
@@ -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}
+6 -9
View File
@@ -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,
+6 -1
View File
@@ -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({
+124 -27
View File
@@ -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>
)}
</>
-1
View File
@@ -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'
+17 -2
View File
@@ -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$/, '')
}))
};
+1 -1
View File
@@ -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();
+8 -2
View File
@@ -1,7 +1,11 @@
import { initialPasswordAtom, userAtom } from '@/atoms/user';
import SealInput from '@/components/seal-form/seal-input';
import { PasswordReg } from '@/config';
import { CRYPT_TEXT } from '@/utils/localstore/index';
import {
CRYPT_TEXT,
IS_FIRST_LOGIN,
writeState
} from '@/utils/localstore/index';
import { LockOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Form, message } from 'antd';
@@ -34,10 +38,12 @@ const PasswordForm: React.FC = () => {
});
await setUserInfo({
...userInfo,
...(userInfo || {}),
require_password_change: false
});
setInitialPassword('');
// Reset first login flag
writeState(IS_FIRST_LOGIN, null);
gotoDefaultPage(userInfo);
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {
@@ -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;
+9 -5
View File
@@ -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;
}
+25 -20
View File
@@ -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,
@@ -188,27 +187,33 @@ const Workers: React.FC = () => {
if (!currentData) {
currentData = clusterData.list[0];
}
handleAddWorker(currentData as ClusterListItem);
if (currentData) {
handleAddWorker(currentData as ClusterListItem);
} else {
message.info(intl.formatMessage({ id: 'noresult.resources.cluster' }));
}
};
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) => {
+25 -4
View File
@@ -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]: [
+4 -6
View File
@@ -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>
);
};
+22 -30
View File
@@ -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);
}
};